megatest 0.1.1 → 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: b3e0e26b309c6dc9a83268b28c18cb4d47b0faec34fe15f6fb5914a694350ae7
4
- data.tar.gz: bcfe062aa918605ef748d2bf5464a95a46f99ed84b3d2e7881877f68de96cf01
3
+ metadata.gz: dce0e1b9ec47a98020fabc266db9d93f73961574ed6ef0bf29b4885064114216
4
+ data.tar.gz: c4dd6ed24dea6de74effca99474a850af28b77142a0c388dff2f4faf2f4b5338
5
5
  SHA512:
6
- metadata.gz: afa34f618e96539d243b93c08f51b8e431c90eba33d253c90ae0ead4bfcd8e87ba20f5a484a62211d160eeefd9017bcf24a5a8299b744013e210350e3adb99d9
7
- data.tar.gz: c6ae412861315e06207a462ae528230778075e9fec55b7e4f91860b90feea5bad4a784a44fa0034c81d69c77be4b7d27821703810162d5367f537f0380a3df44
6
+ metadata.gz: 827fa2e37b36c4e8d7fae1317be65d259bac20210276245c119cbf33d506e967514295d4d74cc9fefd499e60aefacfd4a752f17f83feb58a46387af3d516c302
7
+ data.tar.gz: 3bfe1897107565dd27ae0eef08857b123eea0f181009669ab3e6c676f7fcbed7266fa3708f7b88589aadd518cb43cfaff8402a9648db5828373759b8219e2f6f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
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
+
11
+ - Make the VerboseReporter work with concurrent executors.
12
+ - Fix isolated tests on forkless platforms when the config contains procs.
13
+ - Add a `job_teardown callback` to to stand off for at_exit.
14
+ - Add `stub`, `stub_const` and `stub_any_instance_of`.
15
+ - Add support for `-I` in the CLI.
16
+
3
17
  ## [0.1.1] - 2024-08-20
4
18
 
5
19
  - Fix `$PATH` prefix detection.
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,12 +1,5 @@
1
1
  ### Wants
2
2
 
3
- - Test leak bisect
4
- - See ci-queue bisect.
5
-
6
- - List slow tests
7
- - Not just X slowest test, but up to X tests that are significantly slower than average.
8
- - Exclude them with `:slow` tag.
9
-
10
3
  - `-j` for forkless environments (Windows / JRuby / TruffleRuby)
11
4
 
12
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,12 +282,23 @@ 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 ""
196
293
  opts.separator "Options:"
197
294
  opts.separator ""
198
295
 
296
+ opts.on("-I PATHS", "specify $LOAD_PATH directory (may be used more than once)") do |paths|
297
+ paths.split(":").each do |path|
298
+ $LOAD_PATH.unshift(path)
299
+ end
300
+ end
301
+
199
302
  opts.on("-b", "--backtrace", "Print full backtraces") do
200
303
  @config.backtrace.full!
201
304
  end
@@ -208,11 +311,13 @@ module Megatest
208
311
  @junit = path
209
312
  end
210
313
 
211
- if runner == :run
314
+ if %i[run bisect].include?(runner)
212
315
  opts.on("--seed SEED", Integer, "The seed used to define run order") do |seed|
213
316
  @config.seed = seed
214
317
  end
318
+ end
215
319
 
320
+ if runner == :run
216
321
  opts.on("-j", "--jobs JOBS", Integer, "Number of processes to use") do |jobs|
217
322
  @config.jobs_count = jobs
218
323
  end
@@ -239,8 +344,10 @@ module Megatest
239
344
  @config.queue_url = queue_url
240
345
  end
241
346
 
242
- opts.on("--build-id ID", String, "Unique identifier for the CI build") do |build_id|
243
- @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
244
351
  end
245
352
 
246
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
 
@@ -158,6 +158,7 @@ module Megatest
158
158
  @before_fork_callbacks = []
159
159
  @global_setup_callbacks = []
160
160
  @job_setup_callbacks = []
161
+ @job_teardown_callbacks = []
161
162
  @heartbeat_frequency = 5
162
163
  @backtrace = Backtrace.new
163
164
  @program_name = nil
@@ -170,6 +171,11 @@ module Megatest
170
171
  CIService.configure(self, env)
171
172
  end
172
173
 
174
+ def initialize_dup(_)
175
+ super
176
+ @circuit_breaker = @circuit_breaker.dup
177
+ end
178
+
173
179
  def program_name
174
180
  @program_name || "megatest"
175
181
  end
@@ -213,10 +219,9 @@ module Megatest
213
219
  @differ&.call(expected, actual)
214
220
  end
215
221
 
216
- def pretty_print(object)
222
+ def render_object(object)
217
223
  @pretty_printer.pretty_print(object)
218
224
  end
219
- alias_method :pp, :pretty_print
220
225
 
221
226
  # We always return a new generator with the same seed as to
222
227
  # best reproduce remote builds locally if the same seed is given.
@@ -236,7 +241,11 @@ module Megatest
236
241
  require "megatest/redis_queue"
237
242
  RedisQueue.build(self)
238
243
  else
239
- 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
240
249
  end
241
250
  end
242
251
 
@@ -264,6 +273,14 @@ module Megatest
264
273
  @job_setup_callbacks << block
265
274
  end
266
275
 
276
+ def run_job_teardown_callbacks(job_index)
277
+ @job_teardown_callbacks.each { |c| c.call(self, job_index) }
278
+ end
279
+
280
+ def job_teardown(&block)
281
+ @job_teardown_callbacks << block
282
+ end
283
+
267
284
  def retries?
268
285
  @max_retries.positive?
269
286
  end
@@ -275,6 +292,19 @@ module Megatest
275
292
  @max_retries * size
276
293
  end
277
294
  end
295
+
296
+ NOT_SERIALIZED = %i(@job_teardown_callbacks @job_setup_callbacks @global_setup_callbacks).freeze
297
+ def marshal_dump
298
+ instance_variables.reject { |k| NOT_SERIALIZED.include?(k) }.to_h do |name|
299
+ [name, instance_variable_get(name)]
300
+ end
301
+ end
302
+
303
+ def marshal_load(hash)
304
+ hash.each do |name, value|
305
+ instance_variable_set(name, value)
306
+ end
307
+ end
278
308
  end
279
309
 
280
310
  @config = Config.new({})
@@ -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)
@@ -53,6 +53,10 @@ module Megatest
53
53
  @out = Output.new(out, colors: @config.colors)
54
54
  end
55
55
 
56
+ def concurrent?
57
+ false
58
+ end
59
+
56
60
  def run(queue, reporters)
57
61
  start_time = Megatest.now
58
62
 
@@ -98,6 +102,8 @@ module Megatest
98
102
  @out.error("Exited early because too many failures were encountered")
99
103
  end
100
104
 
105
+ @config.run_job_teardown_callbacks(nil)
106
+
101
107
  queue.cleanup
102
108
  end
103
109
  end
@@ -27,7 +27,7 @@ module Megatest
27
27
  def <<(message)
28
28
  begin
29
29
  @socket.write(Marshal.dump(message))
30
- rescue Errno::EPIPE
30
+ rescue Errno::EPIPE, Errno::ENOTCONN
31
31
  return nil # Other side was closed
32
32
  end
33
33
  self
@@ -113,6 +113,7 @@ module Megatest
113
113
 
114
114
  # We don't want to run at_exit hooks the app may have
115
115
  # installed.
116
+ @config.run_job_teardown_callbacks(@index)
116
117
  Process.exit!(0)
117
118
  end
118
119
  @child_socket.close
@@ -157,15 +158,17 @@ module Megatest
157
158
  @parent_socket.close
158
159
  when :pop
159
160
  if @assigned_test = queue.pop_test
161
+ reporters.each { |r| r.before_test_case(queue, @assigned_test) }
160
162
  @parent_socket << @assigned_test&.id
161
163
  else
162
164
  @idle = true
163
165
  end
164
166
  when :record
165
167
  result = queue.record_result(*args)
168
+ test_case = @assigned_test
166
169
  @assigned_test = nil
167
170
  @parent_socket << result
168
- reporters.each { |r| r.after_test_case(queue, nil, result) }
171
+ reporters.each { |r| r.after_test_case(queue, test_case, result) }
169
172
  @config.circuit_breaker.record_result(result)
170
173
  else
171
174
  raise "Unexpected message: #{message.inspect}"
@@ -203,9 +206,14 @@ module Megatest
203
206
  class Executor
204
207
  attr_reader :wall_time
205
208
 
206
- def initialize(config, out)
209
+ def initialize(config, out, managed: false)
207
210
  @config = config
208
211
  @out = Output.new(out, colors: config.colors)
212
+ @managed = managed
213
+ end
214
+
215
+ def concurrent?
216
+ true
209
217
  end
210
218
 
211
219
  def after_fork_in_child(active_job)
@@ -217,6 +225,7 @@ module Megatest
217
225
  def run(queue, reporters)
218
226
  start_time = Megatest.now
219
227
  @config.run_global_setup_callbacks
228
+ reporters.each { |r| r.start(self, queue) }
220
229
  @jobs = @config.jobs_count.times.map { |index| Job.new(@config, index) }
221
230
 
222
231
  @config.before_fork_callbacks.each(&:call)
@@ -252,7 +261,7 @@ module Megatest
252
261
  @wall_time = Megatest.now - start_time
253
262
  reporters.each { |r| r.summary(self, queue, queue.summary) }
254
263
 
255
- if @config.circuit_breaker.break?
264
+ if @config.circuit_breaker.break? && !@managed
256
265
  @out.error("Exited early because too many failures were encountered")
257
266
  end
258
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,15 +135,48 @@ 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
149
+ def start(executor, _queue)
150
+ @concurrent = executor.concurrent?
151
+ end
152
+
128
153
  def before_test_case(_queue, test_case)
129
- @out.print("#{test_case.id} = ")
154
+ unless @concurrent
155
+ @out.print("#{test_case.id} = ")
156
+ end
130
157
  end
131
158
 
132
- def after_test_case(_queue, _test_case, result)
133
- super
159
+ def after_test_case(_queue, test_case, result)
160
+ if @concurrent
161
+ @out.print("#{test_case.id} = ")
162
+ end
163
+
164
+ if result.skipped?
165
+ @out.print(@out.yellow("SKIPPED"))
166
+ elsif result.retried?
167
+ @out.print(@out.yellow("RETRIED"))
168
+ elsif result.error?
169
+ @out.print(@out.red("ERROR"))
170
+ elsif result.failed?
171
+ @out.print(@out.red("FAILED"))
172
+ else
173
+ @out.print(@out.green("SUCCESS"))
174
+ end
175
+
176
+ if result.duration
177
+ @out.print " (in #{result.duration.round(3)}s)"
178
+ end
179
+
134
180
  @out.puts
135
181
  if result.bad?
136
182
  @out.puts @out.colored(render_failure(result))
@@ -88,6 +88,8 @@ module Megatest
88
88
 
89
89
  result.ensure_assertions unless @config.minitest_compatibility
90
90
  ensure
91
+ runtime.teardown
92
+
91
93
  runtime.record_failures do
92
94
  instance.before_teardown
93
95
  end
@@ -4,13 +4,14 @@
4
4
 
5
5
  module Megatest
6
6
  class Runtime
7
- attr_reader :test_case, :result
7
+ attr_reader :config, :test_case, :result, :on_teardown
8
8
 
9
9
  def initialize(config, test_case, result)
10
10
  @config = config
11
11
  @test_case = test_case
12
12
  @result = result
13
13
  @asserting = false
14
+ @on_teardown = []
14
15
  end
15
16
 
16
17
  support_locations = begin
@@ -143,13 +144,21 @@ module Megatest
143
144
  end
144
145
 
145
146
  def pp(object)
146
- @config.pretty_print(object)
147
+ @config.render_object(object)
147
148
  end
148
149
 
149
150
  def diff(expected, actual)
150
151
  @config.diff(expected, actual)
151
152
  end
152
153
 
154
+ def teardown
155
+ until @on_teardown.empty?
156
+ record_failures do
157
+ @on_teardown.pop.call
158
+ end
159
+ end
160
+ end
161
+
153
162
  def record_failures(downlevel: 1, &block)
154
163
  expect_no_failures(&block)
155
164
  rescue Assertion => assertion
@@ -106,6 +106,10 @@ module Megatest
106
106
  @loader = loader
107
107
  end
108
108
 
109
+ def path
110
+ nil
111
+ end
112
+
109
113
  def partial?
110
114
  @loader.partial?
111
115
  end
@@ -232,7 +232,7 @@ module Megatest
232
232
  class SharedSuite < Suite
233
233
  def initialize(registry, test_suite)
234
234
  super(registry)
235
- @mod = test_suite
235
+ @klass = test_suite
236
236
  @test_cases = {}
237
237
  test_suite.instance_methods.each do |name|
238
238
  if name.start_with?("test_")
@@ -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
 
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Megatest
4
+ class Stubber < Module
5
+ DEFAULT = ->(*) {}
6
+ DEFAULT.ruby2_keywords if DEFAULT.respond_to?(:ruby2_keywords)
7
+
8
+ class << self
9
+ def for(object)
10
+ for_class(class << object; self; end)
11
+ end
12
+
13
+ def for_class(klass)
14
+ unless stubber = klass.included_modules.find { |m| Stubber === m }
15
+ stubber = Stubber.new
16
+ klass.prepend(stubber)
17
+ end
18
+ stubber
19
+ end
20
+ end
21
+
22
+ def stub_method(method, proc)
23
+ proc ||= DEFAULT
24
+
25
+ if method_defined?(method, false) # Already stubbed that method
26
+ old_method = instance_method(method)
27
+ alias_method(method, method) # Silence redefinition warnings
28
+ define_method(method, &proc)
29
+ -> do
30
+ alias_method(method, method) # Silence redefinition warnings
31
+ define_method(method, old_method)
32
+ end
33
+ else
34
+ define_method(method, &proc)
35
+ -> { remove_method(method) }
36
+ end
37
+ end
38
+ end
39
+
40
+ module Stubs
41
+ def stub(object, method, proc = nil)
42
+ stubber = ::Megatest::Stubber.for(object)
43
+ teardown = stubber.stub_method(method, proc)
44
+
45
+ if block_given?
46
+ begin
47
+ yield
48
+ ensure
49
+ teardown.call
50
+ end
51
+ else
52
+ @__m.on_teardown << teardown
53
+ end
54
+ end
55
+
56
+ def stub_any_instance_of(klass, method, proc = nil)
57
+ raise ArgumentError, "stub_any_instance_of expects a Module or Class" unless Module === klass
58
+
59
+ stubber = ::Megatest::Stubber.for_class(klass)
60
+ teardown = stubber.stub_method(method, proc)
61
+
62
+ if block_given?
63
+ begin
64
+ yield
65
+ ensure
66
+ teardown.call
67
+ end
68
+ else
69
+ @__m.on_teardown << teardown
70
+ end
71
+ end
72
+
73
+ def stub_const(mod, constant, new_value, exists: true)
74
+ if exists
75
+ old_value = mod.const_get(constant, false)
76
+ teardown = -> do
77
+ mod.send(:remove_const, constant) if mod.const_defined?(constant, false)
78
+ mod.const_set(constant, old_value)
79
+ end
80
+ else
81
+ if mod.const_defined?(constant)
82
+ raise NameError, "already defined constant #{constant} in #{mod.name || mod.inspect}"
83
+ end
84
+
85
+ teardown = -> do
86
+ mod.send(:remove_const, constant) if mod.const_defined?(constant, false)
87
+ end
88
+ end
89
+
90
+ apply = -> do
91
+ mod.send(:remove_const, constant) if exists
92
+ mod.const_set(constant, new_value)
93
+ end
94
+
95
+ if block_given?
96
+ begin
97
+ apply.call
98
+ yield
99
+ ensure
100
+ teardown.call
101
+ end
102
+ else
103
+ begin
104
+ apply.call
105
+ rescue
106
+ teardown.call
107
+ raise
108
+ end
109
+ @__m.on_teardown << teardown
110
+ end
111
+ end
112
+ end
113
+ end
data/lib/megatest/test.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "megatest/assertions"
4
+ require "megatest/stubs"
4
5
 
5
6
  module Megatest
6
7
  ##
@@ -99,6 +100,7 @@ module Megatest
99
100
  # :startdoc:
100
101
  extend DSL
101
102
  include Assertions
103
+ include Stubs
102
104
 
103
105
  # Returns the current Megatest::State::TestCase instance
104
106
  # Can be used for self introspection
@@ -106,6 +108,11 @@ module Megatest
106
108
  @__m.test_case
107
109
  end
108
110
 
111
+ # Returns the global megatest config object.
112
+ def __config__
113
+ @__m.config
114
+ end
115
+
109
116
  # Returns the current Megatest::TestCaseResult instance
110
117
  # Can be used for self introspection during teardown
111
118
  def __result__
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Megatest
4
- VERSION = "0.1.1"
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.1.1
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-20 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
@@ -45,20 +45,21 @@ files:
45
45
  - lib/megatest/runtime.rb
46
46
  - lib/megatest/selector.rb
47
47
  - lib/megatest/state.rb
48
+ - lib/megatest/stubs.rb
48
49
  - lib/megatest/subprocess.rb
49
50
  - lib/megatest/subprocess/main.rb
50
51
  - lib/megatest/test.rb
51
52
  - lib/megatest/test_task.rb
52
53
  - lib/megatest/version.rb
53
54
  homepage: https://github.com/byroot/megatest
54
- licenses: []
55
+ licenses:
56
+ - MIT
55
57
  metadata:
56
58
  allowed_push_host: https://rubygems.org/
57
59
  rubygems_mfa_required: 'true'
58
60
  homepage_uri: https://github.com/byroot/megatest
59
61
  source_code_uri: https://github.com/byroot/megatest
60
62
  changelog_uri: https://github.com/byroot/megatest/blob/main/CHANGELOG.md
61
- post_install_message:
62
63
  rdoc_options: []
63
64
  require_paths:
64
65
  - lib
@@ -73,8 +74,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
73
74
  - !ruby/object:Gem::Version
74
75
  version: '0'
75
76
  requirements: []
76
- rubygems_version: 3.0.3.1
77
- signing_key:
77
+ rubygems_version: 3.6.2
78
78
  specification_version: 4
79
79
  summary: Modern test-unit style test framework
80
80
  test_files: []