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 +4 -4
- data/CHANGELOG.md +14 -0
- data/LICENSE.md +21 -0
- data/README.md +45 -24
- data/TODO.md +0 -7
- data/lib/megatest/cli.rb +110 -3
- data/lib/megatest/config.rb +34 -4
- data/lib/megatest/differ.rb +1 -1
- data/lib/megatest/executor.rb +6 -0
- data/lib/megatest/multi_process.rb +13 -4
- data/lib/megatest/queue.rb +28 -0
- data/lib/megatest/reporters.rb +58 -12
- data/lib/megatest/runner.rb +2 -0
- data/lib/megatest/runtime.rb +11 -2
- data/lib/megatest/selector.rb +4 -0
- data/lib/megatest/state.rb +2 -3
- data/lib/megatest/stubs.rb +113 -0
- data/lib/megatest/test.rb +7 -0
- data/lib/megatest/version.rb +1 -1
- data/lib/megatest.rb +1 -1
- metadata +7 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dce0e1b9ec47a98020fabc266db9d93f73961574ed6ef0bf29b4885064114216
|
4
|
+
data.tar.gz: c4dd6ed24dea6de74effca99474a850af28b77142a0c388dff2f4faf2f4b5338
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
-
|
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`
|
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
|
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
|
141
|
-
the workers. And if for some reason one of the
|
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
|
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
|
-
|
243
|
-
|
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
|
data/lib/megatest/config.rb
CHANGED
@@ -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, :
|
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
|
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
|
-
|
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({})
|
data/lib/megatest/differ.rb
CHANGED
data/lib/megatest/executor.rb
CHANGED
@@ -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,
|
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
|
|
data/lib/megatest/queue.rb
CHANGED
@@ -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
|
data/lib/megatest/reporters.rb
CHANGED
@@ -88,7 +88,7 @@ module Megatest
|
|
88
88
|
end
|
89
89
|
end
|
90
90
|
|
91
|
-
def summary(executor,
|
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
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
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
|
-
@
|
154
|
+
unless @concurrent
|
155
|
+
@out.print("#{test_case.id} = ")
|
156
|
+
end
|
130
157
|
end
|
131
158
|
|
132
|
-
def after_test_case(_queue,
|
133
|
-
|
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))
|
data/lib/megatest/runner.rb
CHANGED
data/lib/megatest/runtime.rb
CHANGED
@@ -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.
|
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
|
data/lib/megatest/selector.rb
CHANGED
data/lib/megatest/state.rb
CHANGED
@@ -232,7 +232,7 @@ module Megatest
|
|
232
232
|
class SharedSuite < Suite
|
233
233
|
def initialize(registry, test_suite)
|
234
234
|
super(registry)
|
235
|
-
@
|
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 =
|
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__
|
data/lib/megatest/version.rb
CHANGED
data/lib/megatest.rb
CHANGED
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.
|
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:
|
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.
|
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: []
|