From de2c2bd60fdce52cc7ba38a25f3e9436442af604 Mon Sep 17 00:00:00 2001 From: Luke Gruber Date: Thu, 4 Dec 2025 16:10:21 -0500 Subject: [PATCH 1/7] Take VM lock in `class_switch_superclass` (#15356) Safe multi-ractor subclass list mutation We need to lock around mutation and accesses of a class's subclasses list. Unfortunately we also need to do this when creating singleton classes, as the singleton class does need to go into `super`'s subclasses list for CC invalidation purposes. --- class.c | 6 ++++-- test/ruby/test_class.rb | 13 +++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/class.c b/class.c index 0cec526b74fe98..9716ba07dae2e2 100644 --- a/class.c +++ b/class.c @@ -734,8 +734,10 @@ class_detach_subclasses(VALUE klass, VALUE arg) static void class_switch_superclass(VALUE super, VALUE klass) { - class_detach_subclasses(klass, Qnil); - rb_class_subclass_add(super, klass); + RB_VM_LOCKING() { + class_detach_subclasses(klass, Qnil); + rb_class_subclass_add(super, klass); + } } /** diff --git a/test/ruby/test_class.rb b/test/ruby/test_class.rb index 10b7655e9a5562..1faecd0cb25e13 100644 --- a/test/ruby/test_class.rb +++ b/test/ruby/test_class.rb @@ -917,4 +917,17 @@ class T end end; end + + def test_safe_multi_ractor_subclasses_list_mutation + assert_ractor "#{<<~"begin;"}\n#{<<~'end;'}" + begin; + 4.times.map do + Ractor.new do + 20_000.times do + Object.new.singleton_class + end + end + end.each(&:join) + end; + end end From 1d3fe2c382fd543bf12b2e12c2633162ea4b3a0a Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Thu, 30 Oct 2025 12:05:25 -0700 Subject: [PATCH 2/7] Change bmethod defined_ractor to use id instead When defining a bmethod, we recorded the current Ractor's object in the method. However that was never marked and so could be GC'd and reused by a future Ractor. Instead we can use the Ractor's id, which we expect to be unique forever. Co-authored-by: Luke Gruber --- bootstraptest/test_ractor.rb | 25 +++++++++++++++++++++++++ method.h | 2 +- vm_insnhelper.c | 4 ++-- vm_method.c | 3 +-- 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb index 271abb3c84006a..b19349391cce81 100644 --- a/bootstraptest/test_ractor.rb +++ b/bootstraptest/test_ractor.rb @@ -2413,3 +2413,28 @@ def call_test(obj) :ok end RUBY + +# When creating bmethods in Ractors, they should only be usable from their +# defining ractor, even if it is GC'd +assert_equal 'ok', <<~'RUBY' +CLASSES = 1000.times.map do + Ractor.new do + Class.new do + define_method(:foo) {} + end + end +end.map(&:value).freeze + +any = 1000.times.map do + Ractor.new do + CLASSES.any? do |klass| + begin + klass.new.foo + true + rescue RuntimeError + false + end + end + end +end.map(&:value).none? && :ok +RUBY diff --git a/method.h b/method.h index 87fb2498a054d7..b174c6fccb4d94 100644 --- a/method.h +++ b/method.h @@ -167,7 +167,7 @@ typedef struct rb_method_refined_struct { typedef struct rb_method_bmethod_struct { VALUE proc; /* should be marked */ struct rb_hook_list_struct *hooks; - VALUE defined_ractor; + rb_serial_t defined_ractor_id; } rb_method_bmethod_t; enum method_optimized_type { diff --git a/vm_insnhelper.c b/vm_insnhelper.c index 60e1b647ae5d8d..8c1992de43d495 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -4121,7 +4121,7 @@ vm_call_bmethod_body(rb_execution_context_t *ec, struct rb_calling_info *calling VALUE procv = cme->def->body.bmethod.proc; if (!RB_OBJ_SHAREABLE_P(procv) && - cme->def->body.bmethod.defined_ractor != rb_ractor_self(rb_ec_ractor_ptr(ec))) { + cme->def->body.bmethod.defined_ractor_id != rb_ractor_id(rb_ec_ractor_ptr(ec))) { rb_raise(rb_eRuntimeError, "defined with an un-shareable Proc in a different Ractor"); } @@ -4144,7 +4144,7 @@ vm_call_iseq_bmethod(rb_execution_context_t *ec, rb_control_frame_t *cfp, struct VALUE procv = cme->def->body.bmethod.proc; if (!RB_OBJ_SHAREABLE_P(procv) && - cme->def->body.bmethod.defined_ractor != rb_ractor_self(rb_ec_ractor_ptr(ec))) { + cme->def->body.bmethod.defined_ractor_id != rb_ractor_id(rb_ec_ractor_ptr(ec))) { rb_raise(rb_eRuntimeError, "defined with an un-shareable Proc in a different Ractor"); } diff --git a/vm_method.c b/vm_method.c index 179deb749daeec..c4f391b5afe38a 100644 --- a/vm_method.c +++ b/vm_method.c @@ -1020,7 +1020,7 @@ rb_method_definition_set(const rb_method_entry_t *me, rb_method_definition_t *de } case VM_METHOD_TYPE_BMETHOD: RB_OBJ_WRITE(me, &def->body.bmethod.proc, (VALUE)opts); - RB_OBJ_WRITE(me, &def->body.bmethod.defined_ractor, rb_ractor_self(GET_RACTOR())); + def->body.bmethod.defined_ractor_id = rb_ractor_id(rb_ec_ractor_ptr(GET_EC())); return; case VM_METHOD_TYPE_NOTIMPLEMENTED: setup_method_cfunc_struct(UNALIGNED_MEMBER_PTR(def, body.cfunc), (VALUE(*)(ANYARGS))rb_f_notimplement_internal, -1); @@ -1060,7 +1060,6 @@ method_definition_reset(const rb_method_entry_t *me) break; case VM_METHOD_TYPE_BMETHOD: RB_OBJ_WRITTEN(me, Qundef, def->body.bmethod.proc); - RB_OBJ_WRITTEN(me, Qundef, def->body.bmethod.defined_ractor); /* give up to check all in a list */ if (def->body.bmethod.hooks) rb_gc_writebarrier_remember((VALUE)me); break; From 7d9558f99b5bb8279c138860670b56735a28a3e4 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Mon, 1 Dec 2025 19:31:38 -0800 Subject: [PATCH 3/7] Adjust test to avoid bug --- bootstraptest/test_ractor.rb | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb index b19349391cce81..7d09ac52838f66 100644 --- a/bootstraptest/test_ractor.rb +++ b/bootstraptest/test_ractor.rb @@ -2417,13 +2417,24 @@ def call_test(obj) # When creating bmethods in Ractors, they should only be usable from their # defining ractor, even if it is GC'd assert_equal 'ok', <<~'RUBY' -CLASSES = 1000.times.map do - Ractor.new do - Class.new do - define_method(:foo) {} - end +CLASSES = 1000.times.map { Class.new }.freeze + +# This would be better to run in parallel, but there's a bug with lambda +# creation and YJIT causing crashes in dev mode +ractors = CLASSES.map do |klass| + Ractor.new(klass) do |klass| + Ractor.receive + klass.define_method(:foo) {} end -end.map(&:value).freeze +end + +ractors.each do |ractor| + ractor << nil + ractor.join +end + +ractors.clear +GC.start any = 1000.times.map do Ractor.new do From 8d8159e7d87e4fd1594ce2fad3d2653e47fb1026 Mon Sep 17 00:00:00 2001 From: Luke Gruber Date: Thu, 4 Dec 2025 16:51:11 -0500 Subject: [PATCH 4/7] Fix thread scheduler issue with thread_sched_wait_events (#15392) Fix race between timer thread dequeuing waiting thread and thread skipping sleeping due to being dequeued. We now use `th->event_serial` which is protected by `thread_sched_lock`. When a thread is put on timer thread's waiting list, the event serial is saved on the item. The timer thread checks that the saved serial is the same as current thread's serial before calling `thread_sched_to_ready`. The following script (taken from a test in `test_thread.rb` used to crash on scheduler debug assertions. It would likely crash in non-debug mode as well. ```ruby def assert_nil(val) if val != nil raise "Expected #{val} to be nil" end end def assert_equal(expected, actual) if expected != actual raise "Expected #{expected} to be #{actual}" end end def test_join2 ok = false t1 = Thread.new { ok = true; sleep } Thread.pass until ok Thread.pass until t1.stop? t2 = Thread.new do Thread.pass while ok t1.join(0.01) end t3 = Thread.new do ok = false t1.join end assert_nil(t2.value) t1.wakeup assert_equal(t1, t3.value) ensure t1&.kill&.join t2&.kill&.join t3&.kill&.join end rs = 30.times.map do Ractor.new do test_join2 end end rs.each(&:join) ``` --- thread_pthread.c | 31 ++++++++++++++++++------------- thread_pthread.h | 1 + thread_pthread_mn.c | 34 ++++++++++++++++++++++------------ vm_core.h | 1 + 4 files changed, 42 insertions(+), 25 deletions(-) diff --git a/thread_pthread.c b/thread_pthread.c index 2eaa407f10ca94..93b32e55c0f72b 100644 --- a/thread_pthread.c +++ b/thread_pthread.c @@ -1122,8 +1122,10 @@ thread_sched_to_waiting_until_wakeup(struct rb_thread_sched *sched, rb_thread_t { if (!RUBY_VM_INTERRUPTED(th->ec)) { bool can_direct_transfer = !th_has_dedicated_nt(th); + th->status = THREAD_STOPPED_FOREVER; thread_sched_wakeup_next_thread(sched, th, can_direct_transfer); thread_sched_wait_running_turn(sched, th, can_direct_transfer); + th->status = THREAD_RUNNABLE; } else { RUBY_DEBUG_LOG("th:%u interrupted", rb_th_serial(th)); @@ -1149,6 +1151,7 @@ thread_sched_yield(struct rb_thread_sched *sched, rb_thread_t *th) bool can_direct_transfer = !th_has_dedicated_nt(th); thread_sched_to_ready_common(sched, th, false, can_direct_transfer); thread_sched_wait_running_turn(sched, th, can_direct_transfer); + th->status = THREAD_RUNNABLE; } else { VM_ASSERT(sched->readyq_cnt == 0); @@ -1338,7 +1341,7 @@ void rb_ractor_lock_self(rb_ractor_t *r); void rb_ractor_unlock_self(rb_ractor_t *r); // The current thread for a ractor is put to "sleep" (descheduled in the STOPPED_FOREVER state) waiting for -// a ractor action to wake it up. See docs for `ractor_sched_sleep_with_cleanup` for more info. +// a ractor action to wake it up. void rb_ractor_sched_wait(rb_execution_context_t *ec, rb_ractor_t *cr, rb_unblock_function_t *ubf, void *ubf_arg) { @@ -2868,7 +2871,7 @@ static struct { static void timer_thread_check_timeslice(rb_vm_t *vm); static int timer_thread_set_timeout(rb_vm_t *vm); -static void timer_thread_wakeup_thread(rb_thread_t *th); +static void timer_thread_wakeup_thread(rb_thread_t *th, uint32_t event_serial); #include "thread_pthread_mn.c" @@ -2970,7 +2973,7 @@ timer_thread_check_exceed(rb_hrtime_t abs, rb_hrtime_t now) } static rb_thread_t * -timer_thread_deq_wakeup(rb_vm_t *vm, rb_hrtime_t now) +timer_thread_deq_wakeup(rb_vm_t *vm, rb_hrtime_t now, uint32_t *event_serial) { struct rb_thread_sched_waiting *w = ccan_list_top(&timer_th.waiting, struct rb_thread_sched_waiting, node); @@ -2987,32 +2990,31 @@ timer_thread_deq_wakeup(rb_vm_t *vm, rb_hrtime_t now) w->flags = thread_sched_waiting_none; w->data.result = 0; - return thread_sched_waiting_thread(w); + rb_thread_t *th = thread_sched_waiting_thread(w); + *event_serial = w->data.event_serial; + return th; } return NULL; } static void -timer_thread_wakeup_thread_locked(struct rb_thread_sched *sched, rb_thread_t *th) +timer_thread_wakeup_thread_locked(struct rb_thread_sched *sched, rb_thread_t *th, uint32_t event_serial) { - if (sched->running != th) { + if (sched->running != th && th->event_serial == event_serial) { thread_sched_to_ready_common(sched, th, true, false); } - else { - // will be release the execution right - } } static void -timer_thread_wakeup_thread(rb_thread_t *th) +timer_thread_wakeup_thread(rb_thread_t *th, uint32_t event_serial) { RUBY_DEBUG_LOG("th:%u", rb_th_serial(th)); struct rb_thread_sched *sched = TH_SCHED(th); thread_sched_lock(sched, th); { - timer_thread_wakeup_thread_locked(sched, th); + timer_thread_wakeup_thread_locked(sched, th, event_serial); } thread_sched_unlock(sched, th); } @@ -3022,11 +3024,14 @@ timer_thread_check_timeout(rb_vm_t *vm) { rb_hrtime_t now = rb_hrtime_now(); rb_thread_t *th; + uint32_t event_serial; rb_native_mutex_lock(&timer_th.waiting_lock); { - while ((th = timer_thread_deq_wakeup(vm, now)) != NULL) { - timer_thread_wakeup_thread(th); + while ((th = timer_thread_deq_wakeup(vm, now, &event_serial)) != NULL) { + rb_native_mutex_unlock(&timer_th.waiting_lock); + timer_thread_wakeup_thread(th, event_serial); + rb_native_mutex_lock(&timer_th.waiting_lock); } } rb_native_mutex_unlock(&timer_th.waiting_lock); diff --git a/thread_pthread.h b/thread_pthread.h index d635948c4bbb7c..f579e53781a74c 100644 --- a/thread_pthread.h +++ b/thread_pthread.h @@ -39,6 +39,7 @@ struct rb_thread_sched_waiting { #else uint64_t timeout; #endif + uint32_t event_serial; int fd; // -1 for timeout only int result; } data; diff --git a/thread_pthread_mn.c b/thread_pthread_mn.c index 8100fd534e461e..2211d4d1067c39 100644 --- a/thread_pthread_mn.c +++ b/thread_pthread_mn.c @@ -3,7 +3,7 @@ #if USE_MN_THREADS static void timer_thread_unregister_waiting(rb_thread_t *th, int fd, enum thread_sched_waiting_flag flags); -static void timer_thread_wakeup_thread_locked(struct rb_thread_sched *sched, rb_thread_t *th); +static void timer_thread_wakeup_thread_locked(struct rb_thread_sched *sched, rb_thread_t *th, uint32_t event_serial); static bool timer_thread_cancel_waiting(rb_thread_t *th) @@ -15,9 +15,7 @@ timer_thread_cancel_waiting(rb_thread_t *th) if (th->sched.waiting_reason.flags) { canceled = true; ccan_list_del_init(&th->sched.waiting_reason.node); - if (th->sched.waiting_reason.flags & (thread_sched_waiting_io_read | thread_sched_waiting_io_write)) { - timer_thread_unregister_waiting(th, th->sched.waiting_reason.data.fd, th->sched.waiting_reason.flags); - } + timer_thread_unregister_waiting(th, th->sched.waiting_reason.data.fd, th->sched.waiting_reason.flags); th->sched.waiting_reason.flags = thread_sched_waiting_none; } } @@ -57,7 +55,7 @@ ubf_event_waiting(void *ptr) thread_sched_unlock(sched, th); } -static bool timer_thread_register_waiting(rb_thread_t *th, int fd, enum thread_sched_waiting_flag flags, rb_hrtime_t *rel); +static bool timer_thread_register_waiting(rb_thread_t *th, int fd, enum thread_sched_waiting_flag flags, rb_hrtime_t *rel, uint32_t event_serial); // return true if timed out static bool @@ -67,13 +65,15 @@ thread_sched_wait_events(struct rb_thread_sched *sched, rb_thread_t *th, int fd, volatile bool timedout = false, need_cancel = false; + uint32_t event_serial = ++th->event_serial; // overflow is okay + if (ubf_set(th, ubf_event_waiting, (void *)th)) { return false; } thread_sched_lock(sched, th); { - if (timer_thread_register_waiting(th, fd, events, rel)) { + if (timer_thread_register_waiting(th, fd, events, rel, event_serial)) { RUBY_DEBUG_LOG("wait fd:%d", fd); RB_VM_SAVE_MACHINE_CONTEXT(th); @@ -81,9 +81,11 @@ thread_sched_wait_events(struct rb_thread_sched *sched, rb_thread_t *th, int fd, RB_INTERNAL_THREAD_HOOK(RUBY_INTERNAL_THREAD_EVENT_SUSPENDED, th); if (th->sched.waiting_reason.flags == thread_sched_waiting_none) { - // already awaken + th->event_serial++; + // timer thread has dequeued us already, but it won't try to wake us because we bumped our serial } else if (RUBY_VM_INTERRUPTED(th->ec)) { + th->event_serial++; // make sure timer thread doesn't try to wake us need_cancel = true; } else { @@ -111,7 +113,8 @@ thread_sched_wait_events(struct rb_thread_sched *sched, rb_thread_t *th, int fd, } thread_sched_unlock(sched, th); - ubf_clear(th); // TODO: maybe it is already NULL? + // if ubf triggered between sched unlock and ubf clear, sched->running == th here + ubf_clear(th); VM_ASSERT(sched->running == th); @@ -680,7 +683,7 @@ kqueue_already_registered(int fd) // return false if the fd is not waitable or not need to wait. static bool -timer_thread_register_waiting(rb_thread_t *th, int fd, enum thread_sched_waiting_flag flags, rb_hrtime_t *rel) +timer_thread_register_waiting(rb_thread_t *th, int fd, enum thread_sched_waiting_flag flags, rb_hrtime_t *rel, uint32_t event_serial) { RUBY_DEBUG_LOG("th:%u fd:%d flag:%d rel:%lu", rb_th_serial(th), fd, flags, rel ? (unsigned long)*rel : 0); @@ -807,6 +810,7 @@ timer_thread_register_waiting(rb_thread_t *th, int fd, enum thread_sched_waiting th->sched.waiting_reason.data.timeout = abs; th->sched.waiting_reason.data.fd = fd; th->sched.waiting_reason.data.result = 0; + th->sched.waiting_reason.data.event_serial = event_serial; } if (abs == 0) { // no timeout @@ -855,6 +859,10 @@ timer_thread_register_waiting(rb_thread_t *th, int fd, enum thread_sched_waiting static void timer_thread_unregister_waiting(rb_thread_t *th, int fd, enum thread_sched_waiting_flag flags) { + if (!(th->sched.waiting_reason.flags & (thread_sched_waiting_io_read | thread_sched_waiting_io_write))) { + return; + } + RUBY_DEBUG_LOG("th:%u fd:%d", rb_th_serial(th), fd); #if HAVE_SYS_EVENT_H kqueue_unregister_waiting(fd, flags); @@ -885,7 +893,7 @@ timer_thread_setup_mn(void) #endif RUBY_DEBUG_LOG("comm_fds:%d/%d", timer_th.comm_fds[0], timer_th.comm_fds[1]); - timer_thread_register_waiting(NULL, timer_th.comm_fds[0], thread_sched_waiting_io_read | thread_sched_waiting_io_force, NULL); + timer_thread_register_waiting(NULL, timer_th.comm_fds[0], thread_sched_waiting_io_read | thread_sched_waiting_io_force, NULL, 0); } static int @@ -986,8 +994,9 @@ timer_thread_polling(rb_vm_t *vm) th->sched.waiting_reason.flags = thread_sched_waiting_none; th->sched.waiting_reason.data.fd = -1; th->sched.waiting_reason.data.result = filter; + uint32_t event_serial = th->sched.waiting_reason.data.event_serial; - timer_thread_wakeup_thread_locked(sched, th); + timer_thread_wakeup_thread_locked(sched, th, event_serial); } else { // already released @@ -1031,8 +1040,9 @@ timer_thread_polling(rb_vm_t *vm) th->sched.waiting_reason.flags = thread_sched_waiting_none; th->sched.waiting_reason.data.fd = -1; th->sched.waiting_reason.data.result = (int)events; + uint32_t event_serial = th->sched.waiting_reason.data.event_serial; - timer_thread_wakeup_thread_locked(sched, th); + timer_thread_wakeup_thread_locked(sched, th, event_serial); } else { // already released diff --git a/vm_core.h b/vm_core.h index 9d11457966ed71..09f27f6e859fd1 100644 --- a/vm_core.h +++ b/vm_core.h @@ -1127,6 +1127,7 @@ typedef struct rb_thread_struct { struct rb_thread_sched_item sched; bool mn_schedulable; rb_atomic_t serial; // only for RUBY_DEBUG_LOG() + uint32_t event_serial; VALUE last_status; /* $? */ From 6d0c9598f5c62901e0bd9c39e34f0aa2fc7846ed Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Tue, 2 Dec 2025 19:59:15 +0000 Subject: [PATCH 5/7] [DOC] Cross-link between README pages --- README.ja.md | 2 ++ README.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/README.ja.md b/README.ja.md index 278285dc83db3c..9bbc3a83a5fdee 100644 --- a/README.ja.md +++ b/README.ja.md @@ -4,6 +4,8 @@ [![AppVeyor status](https://ci.appveyor.com/api/projects/status/0sy8rrxut4o0k960/branch/master?svg=true)](https://ci.appveyor.com/project/ruby/ruby/branch/master) [![Travis Status](https://app.travis-ci.com/ruby/ruby.svg?branch=master)](https://app.travis-ci.com/ruby/ruby) +[English](rdoc-ref:README.md) + # Rubyとは Rubyはシンプルかつ強力なオブジェクト指向スクリプト言語です. Rubyは純粋なオブジェクト指向言語として設計されているので, diff --git a/README.md b/README.md index 5ed312cca872a0..02435b419e026d 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ [![Actions Status: Windows](https://github.com/ruby/ruby/workflows/Windows/badge.svg)](https://github.com/ruby/ruby/actions?query=workflow%3A"Windows") [![Travis Status](https://app.travis-ci.com/ruby/ruby.svg?branch=master)](https://app.travis-ci.com/ruby/ruby) +[日本語](rdoc-ref:README.ja.md) + # What is Ruby? Ruby is an interpreted object-oriented programming language often From 412895ae84c204466f085d1aae6ca90db88e255a Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 4 Dec 2025 14:19:00 +0900 Subject: [PATCH 6/7] [ruby/rubygems] Try to split and run three runners for Windows I organized all examples the followings: ``` Total test time: 2468.41 seconds Total files: 168 Group A: 42 files, 617.08 seconds Group B: 42 files, 617.05 seconds Group C: 42 files, 617.14 seconds Group D: 42 files, 617.14 seconds ``` https://github.com/ruby/rubygems/commit/94d41e6c7c --- spec/bundler/spec_helper.rb | 10 ++ spec/bundler/support/windows_tag_group.rb | 185 ++++++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100644 spec/bundler/support/windows_tag_group.rb diff --git a/spec/bundler/spec_helper.rb b/spec/bundler/spec_helper.rb index 96cd7be4720a66..f0d6bce006ef20 100644 --- a/spec/bundler/spec_helper.rb +++ b/spec/bundler/spec_helper.rb @@ -38,6 +38,7 @@ require_relative "support/matchers" require_relative "support/permissions" require_relative "support/platforms" +require_relative "support/windows_tag_group" $debug = false @@ -56,6 +57,7 @@ def self.ruby=(ruby) config.include Spec::Path config.include Spec::Platforms config.include Spec::Permissions + config.include Spec::WindowsTagGroup # Enable flags like --only-failures and --next-failure config.example_status_persistence_file_path = ".rspec_status" @@ -129,4 +131,12 @@ def self.ruby=(ruby) ensure reset! end + + Spec::WindowsTagGroup::EXAMPLE_MAPPINGS.each do |tag, file_paths| + file_pattern = Regexp.union(file_paths.map {|path| Regexp.new(Regexp.escape(path) + "$") }) + + config.define_derived_metadata(file_path: file_pattern) do |metadata| + metadata[tag] = true + end + end end diff --git a/spec/bundler/support/windows_tag_group.rb b/spec/bundler/support/windows_tag_group.rb new file mode 100644 index 00000000000000..77112a2c6cd1ce --- /dev/null +++ b/spec/bundler/support/windows_tag_group.rb @@ -0,0 +1,185 @@ +# frozen_string_literal: true + +module Spec + module WindowsTagGroup + EXAMPLE_MAPPINGS = { + windows_a: [ + "spec/runtime/setup_spec.rb", + "spec/commands/install_spec.rb", + "spec/commands/add_spec.rb", + "spec/install/gems/compact_index_spec.rb", + "spec/commands/config_spec.rb", + "spec/commands/pristine_spec.rb", + "spec/install/gemfile/path_spec.rb", + "spec/update/git_spec.rb", + "spec/commands/open_spec.rb", + "spec/commands/remove_spec.rb", + "spec/commands/show_spec.rb", + "spec/plugins/source/example_spec.rb", + "spec/commands/console_spec.rb", + "spec/runtime/require_spec.rb", + "spec/runtime/env_helpers_spec.rb", + "spec/runtime/gem_tasks_spec.rb", + "spec/install/gemfile_spec.rb", + "spec/commands/fund_spec.rb", + "spec/commands/init_spec.rb", + "spec/bundler/ruby_dsl_spec.rb", + "spec/bundler/mirror_spec.rb", + "spec/bundler/source/git/git_proxy_spec.rb", + "spec/bundler/source_list_spec.rb", + "spec/bundler/plugin/installer_spec.rb", + "spec/bundler/friendly_errors_spec.rb", + "spec/resolver/platform_spec.rb", + "spec/bundler/fetcher/downloader_spec.rb", + "spec/update/force_spec.rb", + "spec/bundler/env_spec.rb", + "spec/install/gems/mirror_spec.rb", + "spec/install/failure_spec.rb", + "spec/bundler/yaml_serializer_spec.rb", + "spec/bundler/environment_preserver_spec.rb", + "spec/install/gemfile/install_if_spec.rb", + "spec/install/gems/gemfile_source_header_spec.rb", + "spec/bundler/fetcher/base_spec.rb", + "spec/bundler/rubygems_integration_spec.rb", + "spec/bundler/worker_spec.rb", + "spec/bundler/dependency_spec.rb", + "spec/bundler/ui_spec.rb", + "spec/bundler/plugin/source_list_spec.rb", + "spec/bundler/source/path_spec.rb", + ], + windows_b: [ + "spec/install/gemfile/git_spec.rb", + "spec/install/gems/standalone_spec.rb", + "spec/commands/lock_spec.rb", + "spec/cache/gems_spec.rb", + "spec/other/major_deprecation_spec.rb", + "spec/install/gems/dependency_api_spec.rb", + "spec/install/gemfile/gemspec_spec.rb", + "spec/plugins/install_spec.rb", + "spec/commands/binstubs_spec.rb", + "spec/install/gems/flex_spec.rb", + "spec/runtime/inline_spec.rb", + "spec/commands/post_bundle_message_spec.rb", + "spec/runtime/executable_spec.rb", + "spec/lock/git_spec.rb", + "spec/plugins/hook_spec.rb", + "spec/install/allow_offline_install_spec.rb", + "spec/install/gems/post_install_spec.rb", + "spec/install/gemfile/ruby_spec.rb", + "spec/install/security_policy_spec.rb", + "spec/install/yanked_spec.rb", + "spec/update/gemfile_spec.rb", + "spec/runtime/load_spec.rb", + "spec/plugins/command_spec.rb", + "spec/commands/version_spec.rb", + "spec/install/prereleases_spec.rb", + "spec/bundler/uri_credentials_filter_spec.rb", + "spec/bundler/plugin_spec.rb", + "spec/install/gems/mirror_probe_spec.rb", + "spec/plugins/list_spec.rb", + "spec/bundler/compact_index_client/parser_spec.rb", + "spec/bundler/gem_version_promoter_spec.rb", + "spec/other/cli_dispatch_spec.rb", + "spec/bundler/source/rubygems_spec.rb", + "spec/cache/platform_spec.rb", + "spec/update/gems/fund_spec.rb", + "spec/bundler/stub_specification_spec.rb", + "spec/bundler/retry_spec.rb", + "spec/bundler/installer/spec_installation_spec.rb", + "spec/bundler/spec_set_spec.rb", + "spec/quality_es_spec.rb", + "spec/bundler/index_spec.rb", + "spec/other/cli_man_pages_spec.rb", + ], + windows_c: [ + "spec/commands/newgem_spec.rb", + "spec/commands/exec_spec.rb", + "spec/commands/clean_spec.rb", + "spec/commands/platform_spec.rb", + "spec/cache/git_spec.rb", + "spec/install/gemfile/groups_spec.rb", + "spec/commands/cache_spec.rb", + "spec/commands/check_spec.rb", + "spec/commands/list_spec.rb", + "spec/install/path_spec.rb", + "spec/bundler/cli_spec.rb", + "spec/install/bundler_spec.rb", + "spec/install/git_spec.rb", + "spec/commands/doctor_spec.rb", + "spec/bundler/dsl_spec.rb", + "spec/install/gems/fund_spec.rb", + "spec/install/gems/env_spec.rb", + "spec/bundler/ruby_version_spec.rb", + "spec/bundler/definition_spec.rb", + "spec/install/gemfile/eval_gemfile_spec.rb", + "spec/plugins/source_spec.rb", + "spec/install/gems/dependency_api_fallback_spec.rb", + "spec/plugins/uninstall_spec.rb", + "spec/bundler/plugin/index_spec.rb", + "spec/bundler/bundler_spec.rb", + "spec/bundler/fetcher_spec.rb", + "spec/bundler/source/rubygems/remote_spec.rb", + "spec/bundler/lockfile_parser_spec.rb", + "spec/cache/cache_path_spec.rb", + "spec/bundler/source/git_spec.rb", + "spec/bundler/source_spec.rb", + "spec/commands/ssl_spec.rb", + "spec/bundler/fetcher/compact_index_spec.rb", + "spec/bundler/plugin/api_spec.rb", + "spec/bundler/endpoint_specification_spec.rb", + "spec/bundler/fetcher/index_spec.rb", + "spec/bundler/settings/validator_spec.rb", + "spec/bundler/build_metadata_spec.rb", + "spec/bundler/current_ruby_spec.rb", + "spec/bundler/installer/gem_installer_spec.rb", + "spec/bundler/cli_common_spec.rb", + "spec/bundler/ci_detector_spec.rb", + ], + windows_d: [ + "spec/commands/outdated_spec.rb", + "spec/commands/update_spec.rb", + "spec/lock/lockfile_spec.rb", + "spec/install/deploy_spec.rb", + "spec/install/gemfile/sources_spec.rb", + "spec/runtime/self_management_spec.rb", + "spec/install/gemfile/specific_platform_spec.rb", + "spec/commands/info_spec.rb", + "spec/install/gems/resolving_spec.rb", + "spec/install/gemfile/platform_spec.rb", + "spec/bundler/gem_helper_spec.rb", + "spec/install/global_cache_spec.rb", + "spec/runtime/platform_spec.rb", + "spec/update/gems/post_install_spec.rb", + "spec/install/gems/native_extensions_spec.rb", + "spec/install/force_spec.rb", + "spec/cache/path_spec.rb", + "spec/install/gemspecs_spec.rb", + "spec/commands/help_spec.rb", + "spec/bundler/shared_helpers_spec.rb", + "spec/bundler/settings_spec.rb", + "spec/resolver/basic_spec.rb", + "spec/install/gemfile/force_ruby_platform_spec.rb", + "spec/commands/licenses_spec.rb", + "spec/install/gemfile/lockfile_spec.rb", + "spec/bundler/fetcher/dependency_spec.rb", + "spec/quality_spec.rb", + "spec/bundler/remote_specification_spec.rb", + "spec/install/process_lock_spec.rb", + "spec/install/binstubs_spec.rb", + "spec/bundler/compact_index_client/updater_spec.rb", + "spec/bundler/ui/shell_spec.rb", + "spec/other/ext_spec.rb", + "spec/commands/issue_spec.rb", + "spec/update/path_spec.rb", + "spec/bundler/plugin/api/source_spec.rb", + "spec/install/gems/win32_spec.rb", + "spec/bundler/plugin/dsl_spec.rb", + "spec/runtime/requiring_spec.rb", + "spec/bundler/plugin/events_spec.rb", + "spec/bundler/resolver/candidate_spec.rb", + "spec/bundler/digest_spec.rb", + "spec/bundler/fetcher/gem_remote_fetcher_spec.rb", + ], + }.freeze + end +end From d105709f2b989c03261371306b08f8fc5ccaa680 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 5 Dec 2025 06:33:34 +0900 Subject: [PATCH 7/7] [ruby/rubygems] Add before(:context) hook to warn when spec files are not assigned to any Windows runner group https://github.com/ruby/rubygems/commit/3ddb740969 --- spec/bundler/spec_helper.rb | 7 +++++++ spec/bundler/support/windows_tag_group.rb | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/spec/bundler/spec_helper.rb b/spec/bundler/spec_helper.rb index f0d6bce006ef20..fad1d4ce3266d4 100644 --- a/spec/bundler/spec_helper.rb +++ b/spec/bundler/spec_helper.rb @@ -139,4 +139,11 @@ def self.ruby=(ruby) metadata[tag] = true end end + + config.before(:context) do |example| + metadata = example.class.metadata + if metadata[:type] != :aruba && metadata.keys.none? {|k| Spec::WindowsTagGroup::EXAMPLE_MAPPINGS.keys.include?(k) } + warn "#{metadata[:file_path]} is not assigned to any Windows runner group. see spec/support/windows_tag_group.rb for details." + end + end end diff --git a/spec/bundler/support/windows_tag_group.rb b/spec/bundler/support/windows_tag_group.rb index 77112a2c6cd1ce..8eb0a749dafbaf 100644 --- a/spec/bundler/support/windows_tag_group.rb +++ b/spec/bundler/support/windows_tag_group.rb @@ -1,5 +1,10 @@ # frozen_string_literal: true +# This group classifies test files into 4 groups by running `bin/rspec --profile 10000` +# to ensure balanced execution times. When adding new test files, it is recommended to +# re-aggregate and adjust the groups to keep them balanced. +# For now, please add new files to group 'windows_d'. + module Spec module WindowsTagGroup EXAMPLE_MAPPINGS = {