test-unit 3.7.3 → 3.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dd21cedf50b5fd312063641964e1aa8e40c1a31a8e800d3d1614cc80b7f0dec0
4
- data.tar.gz: 4ee73eac75a4fad3424c60de60b21ffb049990b15bbc363d57913195b302a638
3
+ metadata.gz: 5d16114e7ad17600025f6908efd1fea35e59c5b5ff953e31bf0048a573a742e5
4
+ data.tar.gz: 857951ac70e9f668102998ace9099388fd66c5fa6a889d36b2e9ecb1decc3315
5
5
  SHA512:
6
- metadata.gz: fe16e356b97ef49cb0dcf737b5cbda6f2d620e27dd25eee14032227804bce23a72f18a061e30c7802c566c6e9a24eaeb2633b27f8be45f322c3cbeee25ce6495
7
- data.tar.gz: d1f952865068e5f721b0bdeca26470167e2ffb45a7d20ef62031bfe128c278fa15674eb25fc78ceae7bda1659695cbdd65b3c0a48a34ffe2856f299f7d7e6bc4
6
+ metadata.gz: 1d2d8e9bf468101d11b2436cbe02d66d76d8a1c262f6e3b09792fb6d068876caa6dce7d64516792b69342bd3e0077b805f038123063f9ca8cc9d93ead8b28edc
7
+ data.tar.gz: 01c8e73369f1a4d63c62de6fc181539b4866bbac3dee7307658d7f4c121557e132aacfd15f7afcc4376d30415bee03a7ab690a799a8587da71b5fda0cd4453a4
data/doc/text/news.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # News
2
2
 
3
+ ## 3.7.4 - 2025-12-23 {#version-3-7-4}
4
+
5
+ ### Improvements
6
+
7
+ * Added support for process based parallel test running. You can use
8
+ it by the `--parallel=process` option. You need to specify load
9
+ path options (e.g. `-Ilib`). Tests that startup/shutdown (test case
10
+ level setup/teardown) are defined run in the same process.
11
+ * GH-235
12
+ * GH-339
13
+ * GH-340
14
+ * GH-341
15
+ * GH-342
16
+ * GH-344
17
+ * GH-346
18
+ * GH-348
19
+ * Co authored by Naoto Ono
20
+
21
+ * Added support for `worker_id` for the parallel runner. You can
22
+ use it in your test, setup/teardown and startup/shutdown (test case
23
+ level setup/teardown).
24
+ * GH-235
25
+ * GH-345
26
+ * Co authored by Naoto Ono
27
+
28
+ * parallel: thread: Improved shutdown (test case level teardown)
29
+ timing. As a result, tests that startup (test case level setup) or
30
+ teardown are defined now run in the same thread.
31
+ * GH-235
32
+ * GH-343
33
+ * Co authored by Naoto Ono
34
+
35
+ ### Fixes
36
+
37
+ * parallel: thread: Fixed a bug that running tests failed with the
38
+ `--verbose` option.
39
+ * GH-350
40
+ * GH-351
41
+
42
+ ### Thanks
43
+
44
+ * Naoto Ono
45
+
3
46
  ## 3.7.3 - 2025-11-26 {#version-3-7-3}
4
47
 
5
48
  ### Improvements
@@ -6,6 +6,7 @@ require_relative "priority"
6
6
  require_relative "attribute-matcher"
7
7
  require_relative "testcase"
8
8
  require_relative "test-suite-thread-runner"
9
+ require_relative "test-suite-process-runner"
9
10
  require_relative "version"
10
11
 
11
12
  module Test
@@ -415,6 +416,7 @@ module Test
415
416
 
416
417
  parallel_options = [
417
418
  :thread,
419
+ :process,
418
420
  ]
419
421
  o.on("--[no-]parallel=[thread]", parallel_options,
420
422
  "Runs tests in parallel",
@@ -422,6 +424,8 @@ module Test
422
424
  case parallel
423
425
  when nil, :thread
424
426
  @test_suite_runner_class = TestSuiteThreadRunner
427
+ when :process
428
+ @test_suite_runner_class = TestSuiteProcessRunner
425
429
  else
426
430
  @test_suite_runner_class = TestSuiteRunner
427
431
  end
@@ -0,0 +1,55 @@
1
+ #--
2
+ #
3
+ # Author:: Tsutomu Katsube.
4
+ # Copyright:: Copyright (c) 2025 Tsutomu Katsube. All rights reserved.
5
+ # License:: Ruby license.
6
+
7
+ module Test
8
+ module Unit
9
+ class ProcessTestResult
10
+ def initialize(output)
11
+ @output = output
12
+ end
13
+
14
+ def add_run
15
+ send_result(__method__)
16
+ end
17
+
18
+ def add_pass
19
+ send_result(__method__)
20
+ end
21
+
22
+ # Records an individual assertion.
23
+ def add_assertion
24
+ send_result(__method__)
25
+ end
26
+
27
+ def add_error(error)
28
+ send_result(__method__, error)
29
+ end
30
+
31
+ def add_failure(failure)
32
+ send_result(__method__, failure)
33
+ end
34
+
35
+ def add_pending(pending)
36
+ send_result(__method__, pending)
37
+ end
38
+
39
+ def add_omission(omission)
40
+ send_result(__method__, omission)
41
+ end
42
+
43
+ def add_notification(notification)
44
+ send_result(__method__, notification)
45
+ end
46
+
47
+ private
48
+
49
+ def send_result(action, *args)
50
+ Marshal.dump({status: :result, action: action, args: args}, @output)
51
+ @output.flush
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,83 @@
1
+ #--
2
+ #
3
+ # Author:: Tsutomu Katsube.
4
+ # Copyright:: Copyright (c) 2025 Tsutomu Katsube. All rights reserved.
5
+ # License:: Ruby license.
6
+
7
+ require "optparse"
8
+ require "socket"
9
+
10
+ parser = OptionParser.new
11
+ parser.on("--load-path=PATH") do |path|
12
+ $LOAD_PATH << path
13
+ end
14
+ base_directory = nil
15
+ parser.on("--base-directory=PATH") do |path|
16
+ base_directory = path
17
+ end
18
+ worker_id = nil
19
+ parser.on("--worker-id=ID", Integer) do |id|
20
+ worker_id = id
21
+ end
22
+ remote_ip_address = nil
23
+ parser.on("--ip-address=ADDRESS") do |address|
24
+ remote_ip_address = address
25
+ end
26
+ remote_ip_port = nil
27
+ parser.on("--ip-port=PORT", Integer) do |port|
28
+ remote_ip_port = port
29
+ end
30
+ test_paths = parser.parse!
31
+
32
+ require_relative "../unit"
33
+ require_relative "collector/load"
34
+ require_relative "process-test-result"
35
+ require_relative "worker-context"
36
+ Test::Unit::AutoRunner.need_auto_run = false
37
+ collector = Test::Unit::Collector::Load.new
38
+ collector.base = base_directory
39
+ suite = collector.collect(*test_paths)
40
+
41
+ io_open = lambda do |&block|
42
+ if Gem.win_platform?
43
+ TCPSocket.open(remote_ip_address, remote_ip_port) do |data_socket|
44
+ block.call(data_socket, data_socket)
45
+ end
46
+ else
47
+ IO.open(Test::Unit::TestSuiteProcessRunner::MAIN_TO_WORKER_INPUT_FILENO) do |data_input|
48
+ IO.open(Test::Unit::TestSuiteProcessRunner::WORKER_TO_MAIN_OUTPUT_FILENO) do |data_output|
49
+ block.call(data_input, data_output)
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ io_open.call do |data_input, data_output|
56
+ loop do
57
+ Marshal.dump({status: :ready}, data_output)
58
+ data_output.flush
59
+ test_name = Marshal.load(data_input)
60
+ break if test_name.nil?
61
+ test = suite.find(test_name)
62
+ result = Test::Unit::ProcessTestResult.new(data_output)
63
+ run_context = Test::Unit::TestRunContext.new(Test::Unit::TestSuiteRunner)
64
+
65
+ event_listener = lambda do |event_name, *args|
66
+ Marshal.dump({status: :event, event_name: event_name, args: args}, data_output)
67
+ data_output.flush
68
+ end
69
+ if test.is_a?(Test::Unit::TestSuite)
70
+ test_suite = test
71
+ else
72
+ test_suite = Test::Unit::TestSuite.new(test.class.name, test.class)
73
+ test_suite << test
74
+ end
75
+ runner = Test::Unit::TestSuiteRunner.new(test_suite)
76
+ worker_context = Test::Unit::WorkerContext.new(worker_id, run_context, result)
77
+ runner.run(worker_context, &event_listener)
78
+ end
79
+ Marshal.dump({status: :done}, data_output)
80
+ data_output.flush
81
+
82
+ Marshal.load(data_input)
83
+ end
@@ -0,0 +1,21 @@
1
+ #--
2
+ #
3
+ # Author:: Tsutomu Katsube.
4
+ # Copyright:: Copyright (c) 2025 Tsutomu Katsube. All rights reserved.
5
+ # License:: Ruby license.
6
+
7
+ require_relative "test-run-context"
8
+
9
+ module Test
10
+ module Unit
11
+ class TestProcessRunContext < TestRunContext
12
+ attr_reader :test_names
13
+ attr_accessor :progress_block
14
+ def initialize(runner_class)
15
+ super(runner_class)
16
+ @test_names = []
17
+ @progress_block = nil
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,197 @@
1
+ #--
2
+ #
3
+ # Author:: Tsutomu Katsube.
4
+ # Copyright:: Copyright (c) 2025 Tsutomu Katsube. All rights reserved.
5
+ # License:: Ruby license.
6
+
7
+ require "socket"
8
+
9
+ require_relative "process-test-result"
10
+ require_relative "sub-test-result"
11
+ require_relative "test-process-run-context"
12
+ require_relative "test-suite-runner"
13
+
14
+ module Test
15
+ module Unit
16
+ class TestSuiteProcessRunner < TestSuiteRunner
17
+ MAIN_TO_WORKER_INPUT_FILENO = 3
18
+ WORKER_TO_MAIN_OUTPUT_FILENO = 4
19
+
20
+ class << self
21
+ class Worker
22
+ attr_reader :main_to_worker_output, :worker_to_main_input
23
+ def initialize(pid, main_to_worker_output, worker_to_main_input)
24
+ @pid = pid
25
+ @main_to_worker_output = main_to_worker_output
26
+ @worker_to_main_input = worker_to_main_input
27
+ end
28
+
29
+ def receive
30
+ Marshal.load(@worker_to_main_input)
31
+ end
32
+
33
+ def send(data)
34
+ Marshal.dump(data, @main_to_worker_output)
35
+ @main_to_worker_output.flush
36
+ end
37
+
38
+ def wait
39
+ begin
40
+ Process.waitpid(@pid)
41
+ ensure
42
+ begin
43
+ @main_to_worker_output.close
44
+ ensure
45
+ @worker_to_main_input.close
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ def run_all_tests(result, options)
52
+ n_workers = TestSuiteRunner.n_workers
53
+ test_suite = options[:test_suite]
54
+
55
+ start_tcp_server do |tcp_server|
56
+ workers = []
57
+ begin
58
+ n_workers.times do |i|
59
+ load_paths = options[:load_paths]
60
+ base_directory = options[:base_directory]
61
+ test_paths = options[:test_paths]
62
+ command_line = [Gem.ruby, File.join(__dir__, "process-worker.rb")]
63
+ load_paths.each do |load_path|
64
+ command_line << "--load-path" << load_path
65
+ end
66
+ unless base_directory.nil?
67
+ command_line << "--base-directory" << base_directory
68
+ end
69
+ command_line << "--worker-id" << (i + 1).to_s
70
+ if Gem.win_platform?
71
+ local_address = tcp_server.local_address
72
+ command_line << "--ip-address" << local_address.ip_address
73
+ command_line << "--ip-port" << local_address.ip_port.to_s
74
+ end
75
+ command_line.concat(test_paths)
76
+ if Gem.win_platform?
77
+ # On Windows, file descriptors 3 and above cannot be passed to
78
+ # child processes.
79
+ pid = spawn(*command_line)
80
+ data_socket = tcp_server.accept
81
+ workers << Worker.new(pid, data_socket, data_socket)
82
+ else
83
+ main_to_worker_input, main_to_worker_output = IO.pipe
84
+ worker_to_main_input, worker_to_main_output = IO.pipe
85
+ pid = spawn(*command_line, {MAIN_TO_WORKER_INPUT_FILENO => main_to_worker_input,
86
+ WORKER_TO_MAIN_OUTPUT_FILENO => worker_to_main_output})
87
+ main_to_worker_input.close
88
+ worker_to_main_output.close
89
+ workers << Worker.new(pid, main_to_worker_output, worker_to_main_input)
90
+ end
91
+ end
92
+
93
+ run_context = TestProcessRunContext.new(self)
94
+ yield(run_context)
95
+ run_context.progress_block.call(TestSuite::STARTED, test_suite.name)
96
+ run_context.progress_block.call(TestSuite::STARTED_OBJECT, test_suite)
97
+
98
+ worker_inputs = workers.collect(&:worker_to_main_input)
99
+ until run_context.test_names.empty? do
100
+ select_each_worker(worker_inputs, workers) do |_, worker, data|
101
+ case data[:status]
102
+ when :ready
103
+ test_name = run_context.test_names.shift
104
+ break if test_name.nil?
105
+ worker.send(test_name)
106
+ when :result
107
+ add_result(result, data)
108
+ when :event
109
+ emit_event(options[:event_listener], data)
110
+ end
111
+ end
112
+ end
113
+ workers.each do |worker|
114
+ worker.send(nil)
115
+ end
116
+ until worker_inputs.empty? do
117
+ select_each_worker(worker_inputs, workers) do |worker_to_main_input, worker, data|
118
+ case data[:status]
119
+ when :result
120
+ add_result(result, data)
121
+ when :event
122
+ emit_event(options[:event_listener], data)
123
+ when :done
124
+ worker_inputs.delete(worker_to_main_input)
125
+ worker.send(nil)
126
+ end
127
+ end
128
+ end
129
+ ensure
130
+ workers.each do |worker|
131
+ worker.wait
132
+ end
133
+ end
134
+
135
+ run_context.progress_block.call(TestSuite::FINISHED, test_suite.name)
136
+ run_context.progress_block.call(TestSuite::FINISHED_OBJECT, test_suite)
137
+ end
138
+ end
139
+
140
+ private
141
+ def start_tcp_server
142
+ if Gem.win_platform?
143
+ TCPServer.open("127.0.0.1", 0) do |tcp_server|
144
+ yield(tcp_server)
145
+ end
146
+ else
147
+ yield
148
+ end
149
+ end
150
+
151
+ def select_each_worker(worker_inputs, workers)
152
+ readables, = IO.select(worker_inputs)
153
+ readables.each do |worker_to_main_input|
154
+ worker = workers.find do |w|
155
+ w.worker_to_main_input == worker_to_main_input
156
+ end
157
+ data = worker.receive
158
+ yield(worker_to_main_input, worker, data)
159
+ end
160
+ end
161
+
162
+ def add_result(result, data)
163
+ action = data[:action]
164
+ args = data[:args]
165
+ result.__send__(action, *args)
166
+ end
167
+
168
+ def emit_event(event_listener, data)
169
+ event_name = data[:event_name]
170
+ args = data[:args]
171
+ event_listener.call(event_name, *args)
172
+ end
173
+ end
174
+
175
+ def run(worker_context, &progress_block)
176
+ worker_context.run_context.progress_block = progress_block
177
+ run_tests_recursive(@test_suite, worker_context, &progress_block)
178
+ end
179
+
180
+ private
181
+ def run_tests_recursive(test_suite, worker_context, &progress_block)
182
+ run_context = worker_context.run_context
183
+ if test_suite.have_fixture?
184
+ run_context.test_names << test_suite.name
185
+ else
186
+ test_suite.tests.each do |test|
187
+ if test.is_a?(TestSuite)
188
+ run_tests_recursive(test, worker_context, &progress_block)
189
+ else
190
+ run_context.test_names << test.name
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
@@ -3,7 +3,7 @@
3
3
  # Author:: Nathaniel Talbott.
4
4
  # Copyright:: Copyright (c) 2000-2003 Nathaniel Talbott. All rights reserved.
5
5
  # Copyright:: Copyright (c) 2008-2011 Kouhei Sutou. All rights reserved.
6
- # Copyright:: Copyright (c) 2024 Tsutomu Katsube. All rights reserved.
6
+ # Copyright:: Copyright (c) 2024-2025 Tsutomu Katsube. All rights reserved.
7
7
  # License:: Ruby license.
8
8
 
9
9
  require "etc"
@@ -15,7 +15,7 @@ module Test
15
15
  class TestSuiteRunner
16
16
  @n_workers = Etc.respond_to?(:nprocessors) ? Etc.nprocessors : 1
17
17
  class << self
18
- def run_all_tests
18
+ def run_all_tests(result, options)
19
19
  yield(TestRunContext.new(self))
20
20
  end
21
21
 
@@ -32,14 +32,14 @@ module Test
32
32
  @test_suite = test_suite
33
33
  end
34
34
 
35
- def run(result, run_context: nil, &progress_block)
35
+ def run(worker_context, &progress_block)
36
36
  yield(TestSuite::STARTED, @test_suite.name)
37
37
  yield(TestSuite::STARTED_OBJECT, @test_suite)
38
- run_startup(result)
39
- run_tests(result, run_context: run_context, &progress_block)
38
+ run_startup(worker_context)
39
+ run_tests(worker_context, &progress_block)
40
40
  ensure
41
41
  begin
42
- run_shutdown(result)
42
+ run_shutdown(worker_context)
43
43
  ensure
44
44
  yield(TestSuite::FINISHED, @test_suite.name)
45
45
  yield(TestSuite::FINISHED_OBJECT, @test_suite)
@@ -47,23 +47,24 @@ module Test
47
47
  end
48
48
 
49
49
  private
50
- def run_startup(result)
50
+ def run_startup(worker_context)
51
51
  test_case = @test_suite.test_case
52
52
  return if test_case.nil? or !test_case.respond_to?(:startup)
53
+ test_case.worker_id = worker_context.id
53
54
  begin
54
55
  test_case.startup
55
56
  rescue Exception
56
- raise unless handle_exception($!, result)
57
+ raise unless handle_exception($!, worker_context.result)
57
58
  end
58
59
  end
59
60
 
60
- def run_tests(result, run_context: nil, &progress_block)
61
+ def run_tests(worker_context, &progress_block)
61
62
  @test_suite.tests.each do |test|
62
- run_test(test, result, run_context: run_context, &progress_block)
63
+ run_test(test, worker_context, &progress_block)
63
64
  end
64
65
  end
65
66
 
66
- def run_test(test, result, run_context: nil)
67
+ def run_test(test, worker_context)
67
68
  finished_is_yielded = false
68
69
  finished_object_is_yielded = false
69
70
  previous_event_name = nil
@@ -92,12 +93,12 @@ module Test
92
93
  yield(event_name, *args)
93
94
  end
94
95
 
95
- if test.method(:run).arity == -2
96
- test.run(result, run_context: run_context, &event_listener)
96
+ if test.method(:run).parameters[0] == [:req, :worker_context]
97
+ test.run(worker_context, &event_listener)
97
98
  else
98
99
  # For backward compatibility. There are scripts that overrides
99
100
  # Test::Unit::TestCase#run without keyword arguments.
100
- test.run(result, &event_listener)
101
+ test.run(worker_context.result, &event_listener)
101
102
  end
102
103
 
103
104
  if finished_is_yielded and not finished_object_is_yielded
@@ -105,13 +106,14 @@ module Test
105
106
  end
106
107
  end
107
108
 
108
- def run_shutdown(result)
109
+ def run_shutdown(worker_context)
109
110
  test_case = @test_suite.test_case
110
111
  return if test_case.nil? or !test_case.respond_to?(:shutdown)
112
+ test_case.worker_id = worker_context.id
111
113
  begin
112
114
  test_case.shutdown
113
115
  rescue Exception
114
- raise unless handle_exception($!, result)
116
+ raise unless handle_exception($!, worker_context.result)
115
117
  end
116
118
  end
117
119
 
@@ -1,7 +1,7 @@
1
1
  #--
2
2
  #
3
3
  # Author:: Tsutomu Katsube.
4
- # Copyright:: Copyright (c) 2024 Tsutomu Katsube. All rights reserved.
4
+ # Copyright:: Copyright (c) 2024-2025 Tsutomu Katsube. All rights reserved.
5
5
  # License:: Ruby license.
6
6
 
7
7
  require_relative "sub-test-result"
@@ -12,12 +12,16 @@ module Test
12
12
  module Unit
13
13
  class TestSuiteThreadRunner < TestSuiteRunner
14
14
  class << self
15
- def run_all_tests
15
+ def run_all_tests(result, options)
16
16
  n_workers = TestSuiteRunner.n_workers
17
+ test_suite = options[:test_suite]
17
18
 
18
19
  queue = Thread::Queue.new
19
- shutdowns = []
20
- yield(TestThreadRunContext.new(self, queue, shutdowns))
20
+ run_context = TestThreadRunContext.new(self, queue)
21
+ yield(run_context)
22
+ run_context.progress_block.call(TestSuite::STARTED, test_suite.name)
23
+ run_context.progress_block.call(TestSuite::STARTED_OBJECT, test_suite)
24
+ run_context.parallel_unsafe_tests.each(&:call)
21
25
  n_workers.times do
22
26
  queue << nil
23
27
  end
@@ -25,13 +29,13 @@ module Test
25
29
  workers = []
26
30
  sub_exceptions = []
27
31
  n_workers.times do |i|
28
- workers << Thread.new(i) do |worker_id|
32
+ workers << Thread.new(i + 1) do |worker_id|
29
33
  begin
30
34
  loop do
31
35
  task = queue.pop
32
36
  break if task.nil?
33
37
  catch do |stop_tag|
34
- task.call(stop_tag)
38
+ task.call(stop_tag, worker_id)
35
39
  end
36
40
  end
37
41
  rescue Exception => exception
@@ -41,41 +45,49 @@ module Test
41
45
  end
42
46
  workers.each(&:join)
43
47
 
44
- shutdowns.each(&:call)
48
+ run_context.progress_block.call(TestSuite::FINISHED, test_suite.name)
49
+ run_context.progress_block.call(TestSuite::FINISHED_OBJECT, test_suite)
50
+
45
51
  sub_exceptions.each do |exception|
46
52
  raise exception
47
53
  end
48
54
  end
49
55
  end
50
56
 
51
- def run(result, run_context: nil, &progress_block)
52
- yield(TestSuite::STARTED, @test_suite.name)
53
- yield(TestSuite::STARTED_OBJECT, @test_suite)
54
- run_startup(result)
55
- run_tests(result, run_context: run_context, &progress_block)
56
- ensure
57
- run_context.shutdowns << lambda do
58
- begin
59
- run_shutdown(result)
60
- ensure
61
- yield(TestSuite::FINISHED, @test_suite.name)
62
- yield(TestSuite::FINISHED_OBJECT, @test_suite)
63
- end
64
- end
57
+ def run(worker_context, &progress_block)
58
+ worker_context.run_context.progress_block = progress_block
59
+ run_tests_recursive(@test_suite, worker_context, &progress_block)
65
60
  end
66
61
 
67
62
  private
68
- def run_tests(result, run_context: nil, &progress_block)
69
- @test_suite.tests.each do |test|
70
- if test.is_a?(TestSuite) or not @test_suite.parallel_safe?
71
- run_test(test, result, run_context: run_context, &progress_block)
72
- else
73
- task = lambda do |stop_tag|
74
- sub_result = SubTestResult.new(result)
75
- sub_result.stop_tag = stop_tag
76
- run_test(test, sub_result, run_context: run_context, &progress_block)
63
+ def run_tests_recursive(test_suite, worker_context, &progress_block)
64
+ run_context = worker_context.run_context
65
+ if test_suite.have_fixture?
66
+ task = lambda do |stop_tag, worker_id|
67
+ sub_result = SubTestResult.new(worker_context.result)
68
+ sub_result.stop_tag = stop_tag
69
+ sub_runner = TestSuiteRunner.new(test_suite)
70
+ sub_worker_context = WorkerContext.new(worker_id, run_context, sub_result)
71
+ sub_runner.run(sub_worker_context, &progress_block)
72
+ end
73
+ run_context.queue << task
74
+ else
75
+ test_suite.tests.each do |test|
76
+ if test.is_a?(TestSuite)
77
+ run_tests_recursive(test, worker_context, &progress_block)
78
+ elsif test_suite.parallel_safe?
79
+ task = lambda do |stop_tag, worker_id|
80
+ sub_result = SubTestResult.new(worker_context.result)
81
+ sub_result.stop_tag = stop_tag
82
+ sub_worker_context = WorkerContext.new(worker_id, run_context, sub_result)
83
+ run_test(test, sub_worker_context, &progress_block)
84
+ end
85
+ run_context.queue << task
86
+ else
87
+ run_context.parallel_unsafe_tests << lambda do
88
+ run_test(test, worker_context, &progress_block)
89
+ end
77
90
  end
78
- run_context.queue << task
79
91
  end
80
92
  end
81
93
  end
@@ -9,11 +9,14 @@ require_relative "test-run-context"
9
9
  module Test
10
10
  module Unit
11
11
  class TestThreadRunContext < TestRunContext
12
- attr_reader :queue, :shutdowns
13
- def initialize(runner_class, queue, shutdowns)
12
+ attr_reader :queue
13
+ attr_accessor :progress_block
14
+ attr_accessor :parallel_unsafe_tests
15
+ def initialize(runner_class, queue)
14
16
  super(runner_class)
15
17
  @queue = queue
16
- @shutdowns = shutdowns
18
+ @progress_block = nil
19
+ @parallel_unsafe_tests = []
17
20
  end
18
21
  end
19
22
  end
@@ -439,8 +439,9 @@ module Test
439
439
  #
440
440
  # The difference of them are the following:
441
441
  #
442
- # * Test case created by {sub_test_case} is an anonymous class.
443
- # So you can't refer the test case by name.
442
+ # * Test case created by {sub_test_case} is backed by an anonymous class,
443
+ # but it is assigned to an auto generated constant. So it can be referred
444
+ # to by name (e.g. for `Marshal.dump` and across process use).
444
445
  # * The class name of class style must follow
445
446
  # constant naming rule in Ruby. But the name of test case
446
447
  # created by {sub_test_case} doesn't need to follow the rule.
@@ -500,6 +501,24 @@ module Test
500
501
  available_locations
501
502
  end
502
503
 
504
+ # Returns a current worker ID for the test case. This return depends on
505
+ # how tests are run:
506
+ #
507
+ # * Sequential: always returns `0`
508
+ #
509
+ # * Parallel: returns a one-based to the number of workers.
510
+ #
511
+ # @return [Integer]
512
+ #
513
+ # @since 3.7.4
514
+ def worker_id
515
+ @worker_id || 0
516
+ end
517
+
518
+ def worker_id=(worker_id) # :nodoc:
519
+ @worker_id = worker_id
520
+ end
521
+
503
522
  private
504
523
  # @private
505
524
  @@method_locations = {}
@@ -545,12 +564,16 @@ module Test
545
564
  # @private
546
565
  def sub_test_case_class(name)
547
566
  parent_test_case = self
548
- Class.new(self) do
567
+ sub_test_case = Class.new(self) do
549
568
  singleton_class = class << self; self; end
550
569
  singleton_class.__send__(:define_method, :name) do
551
570
  [parent_test_case.name, name].compact.join("::")
552
571
  end
553
572
  end
573
+ # Give the anonymous class a unique, Base64 encoded constant name.
574
+ # So it becomes a named class that `Marshal` can safely dump even across processes.
575
+ const_set(:"TEST_#{[sub_test_case.name].pack("m").delete("\n=")}", sub_test_case)
576
+ sub_test_case
554
577
  end
555
578
  end
556
579
 
@@ -585,11 +608,17 @@ module Test
585
608
  # Runs the individual test method represented by this
586
609
  # instance of the fixture, collecting statistics, failures
587
610
  # and errors in result.
588
- def run(result, run_context: nil)
611
+ def run(worker_context)
589
612
  begin
613
+ unless worker_context.is_a?(WorkerContext)
614
+ result = worker_context
615
+ worker_context = WorkerContext.new(nil, nil, result)
616
+ end
617
+ result = worker_context.result
590
618
  @_result = result
591
619
  instance_variables_before = instance_variables
592
- @internal_data.run_context = run_context
620
+ @internal_data.run_context = worker_context.run_context
621
+ @internal_data.worker_id = worker_context.id
593
622
  @internal_data.test_started
594
623
  yield(STARTED, name)
595
624
  yield(STARTED_OBJECT, self)
@@ -782,6 +811,16 @@ module Test
782
811
  1
783
812
  end
784
813
 
814
+ # Returns a current worker ID for the test. See {TestCase.worker_id}
815
+ # for details.
816
+ #
817
+ # @return [Integer]
818
+ #
819
+ # @since 3.7.4
820
+ def worker_id
821
+ @internal_data.worker_id || 0
822
+ end
823
+
785
824
  # Returns a label of test data for the test. If the
786
825
  # test isn't associated with any test data, it returns
787
826
  # `nil`.
@@ -887,6 +926,18 @@ module Test
887
926
  current_result.add_pass
888
927
  end
889
928
 
929
+ def marshal_dump
930
+ {
931
+ method_name: @method_name,
932
+ internal_data: @internal_data,
933
+ }
934
+ end
935
+
936
+ def marshal_load(data)
937
+ @method_name = data[:method_name]
938
+ @internal_data = data[:internal_data]
939
+ end
940
+
890
941
  private
891
942
  def current_result
892
943
  @_result
@@ -935,6 +986,7 @@ module Test
935
986
  attr_reader :start_time, :elapsed_time
936
987
  attr_reader :test_data_label, :test_data
937
988
  attr_accessor :run_context
989
+ attr_accessor :worker_id
938
990
  def initialize
939
991
  @start_time = nil
940
992
  @elapsed_time = nil
@@ -943,6 +995,7 @@ module Test
943
995
  @test_data_label = nil
944
996
  @test_data = nil
945
997
  @run_context = nil
998
+ @worker_id = nil
946
999
  end
947
1000
 
948
1001
  def passed?
@@ -977,6 +1030,24 @@ module Test
977
1030
  def interrupted
978
1031
  @interrupted = true
979
1032
  end
1033
+
1034
+ def marshal_dump
1035
+ {
1036
+ start_time: @start_time,
1037
+ elapsed_time: @elapsed_time,
1038
+ passed: @passed,
1039
+ interrupted: @interrupted,
1040
+ test_data_label: @test_data_label,
1041
+ }
1042
+ end
1043
+
1044
+ def marshal_load(data)
1045
+ @start_time = data[:start_time]
1046
+ @elapsed_time = data[:elapsed_time]
1047
+ @passed = data[:passed]
1048
+ @interrupted = data[:interrupted]
1049
+ @test_data_label = data[:test_data_label]
1050
+ end
980
1051
  end
981
1052
  end
982
1053
  end
@@ -40,20 +40,41 @@ module Test
40
40
  @elapsed_time = nil
41
41
  end
42
42
 
43
+ def find(name)
44
+ return self if @name == name
45
+ @tests.each do |test|
46
+ if test.is_a?(self.class)
47
+ t = test.find(name)
48
+ return t if t
49
+ else
50
+ return test if test.name == name
51
+ end
52
+ end
53
+ nil
54
+ end
55
+
43
56
  def parallel_safe?
44
57
  return true if @test_case.nil?
45
58
  @test_case.parallel_safe?
46
59
  end
47
60
 
61
+ def have_fixture?
62
+ return false if @test_case.nil?
63
+ return true if @test_case.method(:startup).owner != TestCase.singleton_class
64
+ return true if @test_case.method(:shutdown).owner != TestCase.singleton_class
65
+ false
66
+ end
67
+
48
68
  # Runs the tests and/or suites contained in this
49
69
  # TestSuite.
50
- def run(result, run_context: nil, &progress_block)
70
+ def run(worker_context, &progress_block)
71
+ run_context = worker_context.run_context
51
72
  if run_context
52
73
  runner_class = run_context.runner_class
53
74
  else
54
75
  runner_class = TestSuiteRunner
55
76
  end
56
- runner_class.new(self).run(result, run_context: run_context) do |event, *args|
77
+ runner_class.new(self).run(worker_context) do |event, *args|
57
78
  case event
58
79
  when STARTED
59
80
  @start_time = Time.now
@@ -6,6 +6,7 @@
6
6
 
7
7
  require_relative '../util/observable'
8
8
  require_relative '../testresult'
9
+ require_relative '../worker-context'
9
10
 
10
11
  module Test
11
12
  module Unit
@@ -40,13 +41,18 @@ module Test
40
41
  start_time = Time.now
41
42
  begin
42
43
  with_listener(result) do
43
- @test_suite_runner_class.run_all_tests do |run_context|
44
+ event_listener = lambda do |channel, value|
45
+ notify_listeners(channel, value)
46
+ end
47
+ @options[:event_listener] = event_listener
48
+ @options[:test_suite] = @suite
49
+ @test_suite_runner_class.run_all_tests(result, @options) do |run_context|
44
50
  catch do |stop_tag|
45
51
  result.stop_tag = stop_tag
46
52
  notify_listeners(RESET, @suite.size)
47
53
  notify_listeners(STARTED, result)
48
54
 
49
- run_suite(result, run_context)
55
+ run_suite(result, run_context: run_context)
50
56
  end
51
57
  end
52
58
  end
@@ -65,13 +71,12 @@ module Test
65
71
  #
66
72
  # See GitHub#38
67
73
  # https://github.com/test-unit/test-unit/issues/38
68
- def run_suite(result=nil, run_context=nil)
74
+ def run_suite(result=nil, run_context: nil)
69
75
  if result.nil?
70
76
  run
71
77
  else
72
- @suite.run(result, run_context: run_context) do |channel, value|
73
- notify_listeners(channel, value)
74
- end
78
+ worker_context = WorkerContext.new(nil, run_context, result)
79
+ @suite.run(worker_context, &@options[:event_listener])
75
80
  end
76
81
  end
77
82
 
@@ -1,5 +1,5 @@
1
1
  module Test
2
2
  module Unit
3
- VERSION = "3.7.3"
3
+ VERSION = "3.7.4"
4
4
  end
5
5
  end
@@ -0,0 +1,20 @@
1
+ #--
2
+ #
3
+ # Author:: Tsutomu Katsube.
4
+ # Copyright:: Copyright (c) 2025 Tsutomu Katsube. All rights reserved.
5
+ # License:: Ruby license.
6
+
7
+ module Test
8
+ module Unit
9
+ class WorkerContext
10
+ attr_reader :id
11
+ attr_reader :run_context
12
+ attr_reader :result
13
+ def initialize(id, run_context, result)
14
+ @id = id
15
+ @run_context = run_context
16
+ @result = result
17
+ end
18
+ end
19
+ end
20
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: test-unit
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.7.3
4
+ version: 3.7.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kouhei Sutou
@@ -75,12 +75,16 @@ files:
75
75
  - lib/test/unit/omission.rb
76
76
  - lib/test/unit/pending.rb
77
77
  - lib/test/unit/priority.rb
78
+ - lib/test/unit/process-test-result.rb
79
+ - lib/test/unit/process-worker.rb
78
80
  - lib/test/unit/runner/console.rb
79
81
  - lib/test/unit/runner/emacs.rb
80
82
  - lib/test/unit/runner/xml.rb
81
83
  - lib/test/unit/sub-test-result.rb
84
+ - lib/test/unit/test-process-run-context.rb
82
85
  - lib/test/unit/test-run-context.rb
83
86
  - lib/test/unit/test-suite-creator.rb
87
+ - lib/test/unit/test-suite-process-runner.rb
84
88
  - lib/test/unit/test-suite-runner.rb
85
89
  - lib/test/unit/test-suite-thread-runner.rb
86
90
  - lib/test/unit/test-thread-run-context.rb
@@ -102,6 +106,7 @@ files:
102
106
  - lib/test/unit/util/procwrapper.rb
103
107
  - lib/test/unit/version.rb
104
108
  - lib/test/unit/warning.rb
109
+ - lib/test/unit/worker-context.rb
105
110
  - sample/adder.rb
106
111
  - sample/subtracter.rb
107
112
  - sample/test_adder.rb