megatest 0.2.0 → 0.3.0

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: 5ef5044b5a70005d87f32c0da4add1e5270d3a298b20a12f4da2fefcb3b64781
4
- data.tar.gz: 273e19eed16d3ad7ade77b297ee8e5819629e48d4c106808d52fbf0572538643
3
+ metadata.gz: dce0e1b9ec47a98020fabc266db9d93f73961574ed6ef0bf29b4885064114216
4
+ data.tar.gz: c4dd6ed24dea6de74effca99474a850af28b77142a0c388dff2f4faf2f4b5338
5
5
  SHA512:
6
- metadata.gz: 1179b7d68e1bfd7fd614404ef3ab4e9c724af6be4944a68160de44ec0857c120f08ba363acacee46efed133d532cc9aec27d8b8432831d61373a8c845df54a33
7
- data.tar.gz: 81950c2704d1553a4f6239c3bf45b96213b1418c79293d8dd9070405f884c0514e2851cb8b528ce12e84525cd96b7e0eb59a76a3a24d25abe958a56ec5f142cd
6
+ metadata.gz: 827fa2e37b36c4e8d7fae1317be65d259bac20210276245c119cbf33d506e967514295d4d74cc9fefd499e60aefacfd4a752f17f83feb58a46387af3d516c302
7
+ data.tar.gz: 3bfe1897107565dd27ae0eef08857b123eea0f181009669ab3e6c676f7fcbed7266fa3708f7b88589aadd518cb43cfaff8402a9648db5828373759b8219e2f6f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2025-06-20
4
+
5
+ - Added missing MIT license.
6
+ - Add bisection support.
7
+ - List slowest tests on success.
8
+
9
+ ## [0.2.0] - 2024-08-26
10
+
3
11
  - Make the VerboseReporter work with concurrent executors.
4
12
  - Fix isolated tests on forkless platforms when the config contains procs.
5
13
  - Add a `job_teardown callback` to to stand off for at_exit.
data/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Jean Boussier
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -4,17 +4,55 @@ Megatest is a test-unit like framework with a focus on usability, and designed w
4
4
 
5
5
  ## Installation
6
6
 
7
- Install the gem and add to the application's Gemfile by executing:
7
+ Install the gem and add it to the application's Gemfile by executing:
8
8
 
9
9
  $ bundle add megatest
10
10
 
11
11
  ## Usage
12
12
 
13
+ ### Special Files And Directories
14
+
15
+ By default, tests are assumed to live in the `test` directory. If present, the
16
+ optional files `test/test_config.rb` and `test/test_helper.rb` are loaded
17
+ automatically, and in that order.
18
+
19
+ #### test/test_config.rb
20
+
21
+ The `megatest` CLI offers options to override default settings, but if you'd
22
+ like to have some always set, please do so in the optional
23
+ `test/test_config.rb`:
24
+
25
+ ```ruby
26
+ # test/test_config.rb
27
+
28
+ Megatest.config do |config|
29
+ # See Megatest::Config.
30
+ end
31
+ ```
32
+
33
+ #### test/test_helper.rb
34
+
35
+ The optional file `test/test_helper.rb` is meant to centralize dependencies and
36
+ define test helpers:
37
+
38
+ ```ruby
39
+ # test/test_helper.rb
40
+
41
+ require "some_dependency"
42
+
43
+ module MyApp
44
+ class Test < Megatest::Test
45
+ def some_helper(arg)
46
+ end
47
+ end
48
+ end
49
+ ```
50
+
13
51
  ### Writing Tests
14
52
 
15
53
  Test suites are Ruby classes that inherit from `Megatest::Test`.
16
54
 
17
- Test cases are be defined with the `test` macro, or for compatibility with existing test suites,
55
+ Test cases are defined with the `test` macro, or for compatibility with existing test suites,
18
56
  by defining a method starting with `test_`.
19
57
 
20
58
  All the classic `test-unit` and `minitest` assertion methods are available:
@@ -37,24 +75,7 @@ class SomeTest < MyApp::Test
37
75
  end
38
76
  ```
39
77
 
40
- By convention, all the `test_helper.rb` files are automatically loaded,
41
- which allows to centralize dependencies and define some helpers.
42
-
43
- ```ruby
44
- # test/test_helper.rb
45
-
46
- require "some_dependency"
47
-
48
- module MyApp
49
- class Test < Megatest::Test
50
-
51
- def some_helper(arg)
52
- end
53
- end
54
- end
55
- ```
56
-
57
- It also allow to define test inside `context` blocks, to make it easier to group
78
+ Megatest also allows to define tests inside `context` blocks, to make it easier to group
58
79
  related tests together and have them share a common name prefix.
59
80
 
60
81
  ```ruby
@@ -78,7 +99,7 @@ blocks, nor their own namespaces.
78
99
 
79
100
  ### Command Line
80
101
 
81
- Contrary to many alternatives, `megatest` provide a convenient CLI interface to easily run specific tests.
102
+ Contrary to many alternatives, `megatest` provides a convenient CLI interface to easily run specific tests.
82
103
 
83
104
  Run all tests in a directory:
84
105
 
@@ -109,7 +130,7 @@ For more detailed usage, run `megatest --help`.
109
130
 
110
131
  ### CI Parallelization
111
132
 
112
- Megatest offer multiple feature to allow running test suites in parallel across
133
+ Megatest offers multiple features to allow running test suites in parallel across
113
134
  many CI jobs.
114
135
 
115
136
  #### Sharding
@@ -137,8 +158,8 @@ will be automatically inferred from the environment.
137
158
 
138
159
  A more efficient way to parallelize tests on CI is to use a Redis server to act as a queue.
139
160
 
140
- This allow to efficiently and dynamically ensure a near perfect test case balance across all
141
- the workers. And if for some reason one of the worker is lost or crashes, no test is lost,
161
+ This allows to efficiently and dynamically ensure a near perfect test case balance across all
162
+ the workers. And if for some reason one of the workers is lost or crashes, no test is lost,
142
163
  which for builds with hundreds of parallel jobs, is essential for stability.
143
164
 
144
165
  ```yaml
data/TODO.md CHANGED
@@ -1,14 +1,5 @@
1
1
  ### Wants
2
2
 
3
- - Improve run by line number: if no exact match fallback to the previous one.
4
-
5
- - Test leak bisect
6
- - See ci-queue bisect.
7
-
8
- - List slow tests
9
- - Not just X slowest test, but up to X tests that are significantly slower than average.
10
- - Exclude them with `:slow` tag.
11
-
12
3
  - `-j` for forkless environments (Windows / JRuby / TruffleRuby)
13
4
 
14
5
  - `minitest/mocks`
data/lib/megatest/cli.rb CHANGED
@@ -28,6 +28,7 @@ module Megatest
28
28
  undef_method :puts, :print # Should only use @out.puts or @err.puts
29
29
 
30
30
  RUNNERS = {
31
+ "bisect" => :bisect,
31
32
  "report" => :report,
32
33
  "run" => :run,
33
34
  }.freeze
@@ -51,6 +52,8 @@ module Megatest
51
52
  report
52
53
  when nil, :run
53
54
  run_tests
55
+ when :bisect
56
+ bisect_tests
54
57
  else
55
58
  raise InvalidArgument, "Parsing failure"
56
59
  end
@@ -116,6 +119,32 @@ module Megatest
116
119
  QueueReporter.new(@config, queue, @out).run(default_reporters) ? 0 : 1
117
120
  end
118
121
 
122
+ def bisect_tests
123
+ require "megatest/multi_process"
124
+
125
+ queue = @config.build_queue
126
+ raise InvalidArgument, "Distributed queues can't be bisected" if queue.distributed?
127
+
128
+ @config.selectors = Selector.parse(@argv)
129
+ Megatest.load_config(@config)
130
+ Megatest.init(@config)
131
+ test_cases = Megatest.load_tests(@config)
132
+ queue.populate(test_cases)
133
+ candidates = queue.dup
134
+
135
+ if test_cases.empty?
136
+ @err.puts "No tests to run"
137
+ return 1
138
+ end
139
+
140
+ unless failure = find_failing_test(queue)
141
+ @err.puts "No failing test"
142
+ return 1
143
+ end
144
+
145
+ bisect_queue(candidates, failure.test_id)
146
+ end
147
+
119
148
  private
120
149
 
121
150
  def default_reporters
@@ -141,6 +170,67 @@ module Megatest
141
170
  reporters
142
171
  end
143
172
 
173
+ def find_failing_test(queue)
174
+ @config.max_consecutive_failures = 1
175
+ @config.jobs_count = 1
176
+
177
+ executor = MultiProcess::Executor.new(@config.dup, @out)
178
+ executor.run(queue, default_reporters)
179
+ queue.summary.failures.first
180
+ end
181
+
182
+ def bisect_queue(queue, failing_test_id)
183
+ err = Output.new(@err)
184
+ tests = queue.to_a
185
+ failing_test_index = tests.index { |test| test.id == failing_test_id }
186
+ failing_test = tests[failing_test_index]
187
+ suspects = tests[0...failing_test_index]
188
+
189
+ check_passing = @config.build_queue
190
+ check_passing.populate([failing_test])
191
+ executor = MultiProcess::Executor.new(@config.dup, @out, managed: true)
192
+ executor.run(check_passing, [])
193
+ unless check_passing.success?
194
+ err.puts err.red("Test failed by itself, no need to bisect")
195
+ return 1
196
+ end
197
+
198
+ run_index = 0
199
+ while suspects.size > 1
200
+ run_index += 1
201
+ err.puts "Attempt #{run_index}, #{suspects.size} suspects left."
202
+
203
+ before, after = suspects[0...(suspects.size / 2)], suspects[(suspects.size / 2)..]
204
+ candidates = @config.build_queue
205
+ candidates.populate(before + [failing_test])
206
+
207
+ executor = MultiProcess::Executor.new(@config.dup, @out, managed: true)
208
+ executor.run(candidates, default_reporters)
209
+
210
+ if candidates.success?
211
+ suspects = after
212
+ else
213
+ suspects = before
214
+ end
215
+
216
+ err.puts
217
+ end
218
+ suspect = suspects.first
219
+
220
+ validation_queue = @config.build_queue
221
+ validation_queue.populate([suspect, failing_test])
222
+ executor = MultiProcess::Executor.new(@config.dup, @out, managed: true)
223
+ executor.run(validation_queue, [])
224
+ if validation_queue.success?
225
+ err.puts err.red("Bisect inconclusive")
226
+ return 1
227
+ end
228
+
229
+ err.print "Found test leak: "
230
+ err.puts err.yellow "#{@config.program_name} #{Megatest.relative_path(suspect.location_id)} #{Megatest.relative_path(failing_test.location_id)}"
231
+ 0
232
+ end
233
+
144
234
  def open_file(path)
145
235
  File.open(path, "w+")
146
236
  rescue Errno::ENOENT
@@ -176,6 +266,8 @@ module Megatest
176
266
  opts.banner = "Usage: #{@program_name} report [options]"
177
267
  when :run
178
268
  opts.banner = "Usage: #{@program_name} run [options] [files or directories]"
269
+ when :bisect
270
+ opts.banner = "Usage: #{@program_name} bisect [options] [files or directories]"
179
271
  else
180
272
  opts.banner = "Usage: #{@program_name} command [options] [files or directories]"
181
273
  opts.separator ""
@@ -190,6 +282,11 @@ module Megatest
190
282
  opts.separator "\treport\t\tWait for the queue to be entirely processed and report the status"
191
283
  opts.separator "\t\t\t $ #{@program_name} report --queue redis://ci-queue.example.com --build-id $CI_BUILD_ID"
192
284
  opts.separator ""
285
+
286
+ opts.separator "\tbisect\t\tRepeatedly run subsets of the given tests."
287
+ opts.separator "\t\t\t $ #{@program_name} bisect --seed 12345 test/integration"
288
+ opts.separator "\t\t\t $ #{@program_name} bisect --queue path/to/test_order.log"
289
+ opts.separator ""
193
290
  end
194
291
 
195
292
  opts.separator ""
@@ -214,11 +311,13 @@ module Megatest
214
311
  @junit = path
215
312
  end
216
313
 
217
- if runner == :run
314
+ if %i[run bisect].include?(runner)
218
315
  opts.on("--seed SEED", Integer, "The seed used to define run order") do |seed|
219
316
  @config.seed = seed
220
317
  end
318
+ end
221
319
 
320
+ if runner == :run
222
321
  opts.on("-j", "--jobs JOBS", Integer, "Number of processes to use") do |jobs|
223
322
  @config.jobs_count = jobs
224
323
  end
@@ -245,8 +344,10 @@ module Megatest
245
344
  @config.queue_url = queue_url
246
345
  end
247
346
 
248
- opts.on("--build-id ID", String, "Unique identifier for the CI build") do |build_id|
249
- @config.build_id = build_id
347
+ if %i[run report].include?(runner)
348
+ opts.on("--build-id ID", String, "Unique identifier for the CI build") do |build_id|
349
+ @config.build_id = build_id
350
+ end
250
351
  end
251
352
 
252
353
  if runner == :run
@@ -138,7 +138,7 @@ module Megatest
138
138
  class Config
139
139
  attr_accessor :queue_url, :retry_tolerance, :max_retries, :jobs_count, :job_index, :load_paths, :deprecations,
140
140
  :build_id, :heartbeat_frequency, :minitest_compatibility, :ci, :selectors
141
- attr_reader :before_fork_callbacks, :global_setup_callbacks, :worker_setup_callbacks, :backtrace, :circuit_breaker, :seed,
141
+ attr_reader :before_fork_callbacks, :global_setup_callbacks, :backtrace, :circuit_breaker, :seed,
142
142
  :worker_id, :workers_count
143
143
  attr_writer :differ, :pretty_printer, :program_name, :colors
144
144
 
@@ -171,6 +171,11 @@ module Megatest
171
171
  CIService.configure(self, env)
172
172
  end
173
173
 
174
+ def initialize_dup(_)
175
+ super
176
+ @circuit_breaker = @circuit_breaker.dup
177
+ end
178
+
174
179
  def program_name
175
180
  @program_name || "megatest"
176
181
  end
@@ -214,10 +219,9 @@ module Megatest
214
219
  @differ&.call(expected, actual)
215
220
  end
216
221
 
217
- def pretty_print(object)
222
+ def render_object(object)
218
223
  @pretty_printer.pretty_print(object)
219
224
  end
220
- alias_method :pp, :pretty_print
221
225
 
222
226
  # We always return a new generator with the same seed as to
223
227
  # best reproduce remote builds locally if the same seed is given.
@@ -237,7 +241,11 @@ module Megatest
237
241
  require "megatest/redis_queue"
238
242
  RedisQueue.build(self)
239
243
  else
240
- raise ArgumentError, "Unsupported queue type: #{@queue_url.inspect}"
244
+ if @queue_url.is_a?(String) && File.exist?(@queue_url)
245
+ FileQueue.build(self)
246
+ else
247
+ raise ArgumentError, "Unsupported queue type: #{@queue_url.inspect}"
248
+ end
241
249
  end
242
250
  end
243
251
 
@@ -36,7 +36,7 @@ module Megatest
36
36
  private
37
37
 
38
38
  def pp(object)
39
- @config.pretty_print(object)
39
+ @config.render_object(object)
40
40
  end
41
41
 
42
42
  def object_diff(expected, expected_inspect, actual_inspect)
@@ -206,9 +206,10 @@ module Megatest
206
206
  class Executor
207
207
  attr_reader :wall_time
208
208
 
209
- def initialize(config, out)
209
+ def initialize(config, out, managed: false)
210
210
  @config = config
211
211
  @out = Output.new(out, colors: config.colors)
212
+ @managed = managed
212
213
  end
213
214
 
214
215
  def concurrent?
@@ -260,7 +261,7 @@ module Megatest
260
261
  @wall_time = Megatest.now - start_time
261
262
  reporters.each { |r| r.summary(self, queue, queue.summary) }
262
263
 
263
- if @config.circuit_breaker.break?
264
+ if @config.circuit_breaker.break? && !@managed
264
265
  @out.error("Exited early because too many failures were encountered")
265
266
  end
266
267
 
@@ -94,6 +94,11 @@ module Megatest
94
94
  @results = results
95
95
  end
96
96
 
97
+ def initialize_dup(_)
98
+ super
99
+ @results = @results.dup
100
+ end
101
+
97
102
  # When running distributed queues, it's possible
98
103
  # that a test is considered lost and end up with both
99
104
  # a successful and a failed result.
@@ -173,6 +178,18 @@ module Megatest
173
178
  @leases = {}
174
179
  end
175
180
 
181
+ def initialize_dup(_other)
182
+ super
183
+ @queue = @queue.dup
184
+ @summary = @summary.dup
185
+ @retries = @retries.dup
186
+ @leases = @leases.dup
187
+ end
188
+
189
+ def to_a
190
+ @queue.reverse
191
+ end
192
+
176
193
  def distributed?
177
194
  false
178
195
  end
@@ -236,4 +253,15 @@ module Megatest
236
253
  true
237
254
  end
238
255
  end
256
+
257
+ class FileQueue < Queue
258
+ def populate(test_cases)
259
+ super
260
+
261
+ queue = File.readlines(@config.queue_url, chomp: true)
262
+ queue.reverse!
263
+ queue.map! { |test_id| @test_cases_index.fetch(test_id) }
264
+ @queue = queue
265
+ end
266
+ end
239
267
  end
@@ -88,7 +88,7 @@ module Megatest
88
88
  end
89
89
  end
90
90
 
91
- def summary(executor, _queue, summary)
91
+ def summary(executor, queue, summary)
92
92
  @out.puts
93
93
  @out.puts
94
94
 
@@ -102,14 +102,27 @@ module Megatest
102
102
  end
103
103
  end
104
104
 
105
- if (wall_time = executor.wall_time.to_f) > 0.0
106
- @out.puts format(
107
- "Finished in %.2fs, %d cases/s, %d assertions/s, %.2fs tests runtime.",
108
- wall_time,
109
- (summary.runs_count / wall_time).to_i,
110
- (summary.assertions_count / wall_time).to_i,
111
- summary.total_time,
112
- )
105
+ # In case of failure we'd rather not print slow tests
106
+ # as it would blur the output.
107
+ if queue.success? && !summary.results.empty?
108
+ sorted_results = summary.results.sort_by(&:duration)
109
+ size = sorted_results.size
110
+ average = sorted_results.sum(&:duration) / size
111
+ median = sorted_results[size / 2].duration
112
+ p90 = sorted_results[(size * 0.9).to_i].duration
113
+ p99 = sorted_results[(size * 0.99).to_i].duration
114
+
115
+ @out.puts "Finished in #{s(executor.wall_time.to_f)}, average: #{ms(average)}, median: #{ms(median)}, p90: #{ms(p90)}, p99: #{ms(p99)}"
116
+ cuttoff = p90 * 10
117
+ slowest_tests = sorted_results.last(5).select { |r| r.duration > cuttoff }
118
+ unless slowest_tests.empty?
119
+ @out.puts "Slowest tests:"
120
+ slowest_tests.reverse_each do |result|
121
+ duration_string = ms(result.duration).rjust(10, " ")
122
+ @out.puts " - #{duration_string} #{@out.yellow(result.test_id)} @ #{@out.cyan(Megatest.relative_path(result.test_location))}"
123
+ end
124
+ @out.puts ""
125
+ end
113
126
  end
114
127
 
115
128
  @out.puts format(
@@ -122,6 +135,14 @@ module Megatest
122
135
  summary.skips_count,
123
136
  )
124
137
  end
138
+
139
+ def s(duration)
140
+ format("%.2fs", duration)
141
+ end
142
+
143
+ def ms(duration)
144
+ format("%.1fms", duration * 1000.0)
145
+ end
125
146
  end
126
147
 
127
148
  class VerboseReporter < SimpleReporter
@@ -144,7 +144,7 @@ module Megatest
144
144
  end
145
145
 
146
146
  def pp(object)
147
- @config.pretty_print(object)
147
+ @config.render_object(object)
148
148
  end
149
149
 
150
150
  def diff(expected, actual)
@@ -585,7 +585,7 @@ module Megatest
585
585
  @test_id = test_case.id
586
586
  @test_location = test_case.location_id
587
587
  @assertions_count = 0
588
- @duration = nil
588
+ @duration = 0.0
589
589
  @retried = false
590
590
  @failures = []
591
591
  end
@@ -663,7 +663,6 @@ module Megatest
663
663
 
664
664
  def lost
665
665
  @failures << Failure.new(LostTest.new(@test_id))
666
- @duration = 0.0
667
666
  self
668
667
  end
669
668
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Megatest
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/megatest.rb CHANGED
@@ -38,7 +38,7 @@ module Megatest
38
38
  ::Warning[:deprecated] = true
39
39
  end
40
40
 
41
- # We initiale the seed in case there is some Random use
41
+ # We initialize the seed in case there is some Random use
42
42
  # at code loading time.
43
43
  Random.srand(config.seed)
44
44
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: megatest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jean Boussier
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2024-08-26 00:00:00.000000000 Z
10
+ date: 2025-06-20 00:00:00.000000000 Z
12
11
  dependencies: []
13
12
  description: Largely API compatible with test-unit / minitest, but with lots of extra
14
13
  modern niceties like a proper CLI, test distribution, etc.
@@ -20,6 +19,7 @@ extensions: []
20
19
  extra_rdoc_files: []
21
20
  files:
22
21
  - CHANGELOG.md
22
+ - LICENSE.md
23
23
  - README.md
24
24
  - TODO.md
25
25
  - exe/megatest
@@ -52,14 +52,14 @@ files:
52
52
  - lib/megatest/test_task.rb
53
53
  - lib/megatest/version.rb
54
54
  homepage: https://github.com/byroot/megatest
55
- licenses: []
55
+ licenses:
56
+ - MIT
56
57
  metadata:
57
58
  allowed_push_host: https://rubygems.org/
58
59
  rubygems_mfa_required: 'true'
59
60
  homepage_uri: https://github.com/byroot/megatest
60
61
  source_code_uri: https://github.com/byroot/megatest
61
62
  changelog_uri: https://github.com/byroot/megatest/blob/main/CHANGELOG.md
62
- post_install_message:
63
63
  rdoc_options: []
64
64
  require_paths:
65
65
  - lib
@@ -74,8 +74,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
74
74
  - !ruby/object:Gem::Version
75
75
  version: '0'
76
76
  requirements: []
77
- rubygems_version: 3.5.11
78
- signing_key:
77
+ rubygems_version: 3.6.2
79
78
  specification_version: 4
80
79
  summary: Modern test-unit style test framework
81
80
  test_files: []