megatest 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 81186c745613e5b5ffe4ef277acaed9763ea27b308a2d25c9f155d2369b15624
4
+ data.tar.gz: d6034a325bce53488239699554363bad937fb2c4e77a34f07e2862476ebfad0c
5
+ SHA512:
6
+ metadata.gz: 1086dd76f6bf6ab4ae7e0974cf2c9e760bd20b104b5cbfba3e960d8fb9f3f0980c21c814ba09619a33be89edc6dadfae4edba8885c982354192fb54298301bb6
7
+ data.tar.gz: 06cd253892c7f76f28cf7feddf51f5135739040c6177abd9bc63ab5567b87832675a03aac3ad060831690df03824b0f07662b9d71ca7bd92419e70b764b66fd0
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2024-08-20
4
+
5
+ - Initial release
data/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # Megatest
2
+
3
+ Megatest is a test-unit like framework with a focus on usability, and designed with continuous integration in mind.
4
+
5
+ ## Installation
6
+
7
+ Install the gem and add to the application's Gemfile by executing:
8
+
9
+ $ bundle add megatest
10
+
11
+ ## Usage
12
+
13
+ ### Writing Tests
14
+
15
+ Test suites are Ruby classes that inherit from `Megatest::Test`.
16
+
17
+ Test cases are be defined with the `test` macro, or for compatibility with existing test suites,
18
+ by defining a method starting with `test_`.
19
+
20
+ All the classic `test-unit` and `minitest` assertion methods are available:
21
+
22
+ ```ruby
23
+ # test/some_test.rb
24
+
25
+ class SomeTest < MyApp::Test
26
+ setup do
27
+ @user = User.new("George")
28
+ end
29
+
30
+ test "the truth" do
31
+ assert_equal true, Some.truth
32
+ end
33
+
34
+ def test_it_works
35
+ assert_predicate 2, :even?
36
+ end
37
+ end
38
+ ```
39
+
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
58
+ related tests together and have them share a common name prefix.
59
+
60
+ ```ruby
61
+ class SomeTest < MyApp::Test
62
+ context "when on earth" do
63
+ test "1 is odd" do
64
+ App.location = "earth"
65
+ assert_predicate 1, :odd?
66
+ end
67
+
68
+ test "2 is even" do
69
+ App.location = "earth"
70
+ assert_predicate 2, :even?
71
+ end
72
+ end
73
+ end
74
+ ```
75
+
76
+ Note however that context blocks aren't test suites, they don't have their own setup or teardown
77
+ blocks, nor their own namespaces.
78
+
79
+ ### Command Line
80
+
81
+ Contrary to many alternatives, `megatest` provide a convenient CLI interface to easily run specific tests.
82
+
83
+ Run all tests in a directory:
84
+
85
+ ```bash
86
+ $ megatest # Run all tests in `test/`
87
+ $ megatest test/integration
88
+ ```
89
+
90
+ Runs tests using 8 processes:
91
+
92
+ ```bash
93
+ $ megatest -j 8
94
+ ```
95
+
96
+ Run a test at the specific line:
97
+
98
+ ```bash
99
+ $ megatest test/some_test.rb:42 test/other_test.rb:24
100
+ ```
101
+
102
+ Run all tests matching a pattern:
103
+
104
+ ```bash
105
+ $ megatest test/some_test.rb:/matching
106
+ ```
107
+
108
+ For more detailed usage, run `megatest --help`.
109
+
110
+ ### CI Parallelization
111
+
112
+ Megatest offer multiple feature to allow running test suites in parallel across
113
+ many CI jobs.
114
+
115
+ #### Sharding
116
+
117
+ The simplest way is sharding. Each worker will run its share of the test cases.
118
+
119
+ Many CI systems provide a way to run the same command on multiple nodes,
120
+ and will generally expose environment variables to help split the workload.
121
+
122
+ ```yaml
123
+ - label: "Run Unit Tests"
124
+ run: megatest --workers-count $CI_NODE_INDEX --worker-id $CI_NODE_TOTAL
125
+ parallel: 8
126
+ ```
127
+
128
+ Note that Megatest makes no effort at balancing the shards as it has no
129
+ information about how long each individual test case is expected to take.
130
+ However it does shard test cases individually, so it avoids the most common issue which is
131
+ very large test suites containing lots of slow test cases being sharded as one unit.
132
+
133
+ If you are using CircleCI, Buildkite or HerokuCI, the workers count and worker id
134
+ will be automatically inferred from the environment.
135
+
136
+ ### Redis Distribution
137
+
138
+ A more efficient way to parallelize tests on CI is to use a Redis server to act as a queue.
139
+
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,
142
+ which for builds with hundreds of parallel jobs, is essential for stability.
143
+
144
+ ```yaml
145
+ - label: "Run Unit Tests"
146
+ run: megatest --queue redis://redis-ci.example.com --build-id $CI_BUILD_ID --worker-id $CI_JOB_ID
147
+ parallel: 128
148
+ soft_fail: true # Doesn't matter if they fail or crash, only the "Results" job status matters
149
+
150
+ - label: "Unit Test Results"
151
+ run: megatest report --queue redis://redis-ci.example.com --build-id $CI_BUILD_ID
152
+ ```
153
+
154
+ ## Contributing
155
+
156
+ Bug reports and pull requests are welcome on GitHub at https://github.com/byroot/megatest.
data/TODO.md ADDED
@@ -0,0 +1,17 @@
1
+ ### Wants
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
+ - `-j` for forkless environments (Windows / JRuby / TruffleRuby)
11
+
12
+ - `minitest/mocks`
13
+ - I'm not very fond of those, but could be worth offering it as a side gem or something, for easier transition.
14
+
15
+ ### Maybe
16
+
17
+ - RSpec style pending?
data/exe/megatest ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "megatest"
5
+ require "megatest/cli"
6
+
7
+ Megatest::CLI.run!
@@ -0,0 +1,474 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Megatest
4
+ class Assertion < Exception
5
+ end
6
+
7
+ class NoAssertion < Assertion
8
+ def initialize(message = "No assertions performed")
9
+ super
10
+ end
11
+ end
12
+
13
+ DidNotRun = Class.new(Assertion)
14
+
15
+ class LostTest < Assertion
16
+ def initialize(test_id)
17
+ super("#{test_id} never completed. Might be caused by a crash or early exit?")
18
+ end
19
+ end
20
+
21
+ Skip = Class.new(Assertion)
22
+
23
+ class UnexpectedError < Assertion
24
+ attr_reader :cause
25
+
26
+ def initialize(cause)
27
+ super("Unexpected exception")
28
+ @cause = cause
29
+ end
30
+
31
+ def backtrace
32
+ cause.backtrace
33
+ end
34
+
35
+ def backtrace_locations
36
+ cause.backtrace_locations
37
+ end
38
+ end
39
+
40
+ module Assertions
41
+ def pass
42
+ @__m.assert {}
43
+ end
44
+
45
+ def assert(result, msg = nil, message: nil)
46
+ message = @__m.msg(msg, message)
47
+ @__m.assert do
48
+ return if result
49
+
50
+ if message
51
+ @__m.fail(message)
52
+ else
53
+ @__m.fail(message, "Expected", @__m.pp(result), "to be truthy")
54
+ end
55
+ end
56
+ end
57
+
58
+ def refute(result, msg = nil, message: nil)
59
+ message = @__m.msg(msg, message)
60
+ @__m.assert do
61
+ return unless result
62
+
63
+ if message
64
+ @__m.fail(message)
65
+ else
66
+ @__m.fail(message, "Expected", @__m.pp(result), "to be falsy")
67
+ end
68
+ end
69
+ end
70
+
71
+ def assert_nil(actual, msg = nil, message: nil)
72
+ message = @__m.msg(msg, message)
73
+ @__m.assert do
74
+ unless nil.equal?(actual)
75
+ @__m.fail(message, "Expected", @__m.pp(actual), "to be nil")
76
+ end
77
+ end
78
+ end
79
+
80
+ def refute_nil(actual, msg = nil, message: nil)
81
+ message = @__m.msg(msg, message)
82
+ @__m.assert do
83
+ if nil.equal?(actual)
84
+ @__m.fail(message, "Expected", @__m.pp(actual), "to not be nil")
85
+ end
86
+ end
87
+ end
88
+
89
+ def assert_equal(expected, actual, msg = nil, message: nil, allow_nil: false)
90
+ message = @__m.msg(msg, message)
91
+ @__m.assert do
92
+ if !allow_nil && nil == expected
93
+ @__m.fail(nil, "Use assert_nil if expecting nil, or pass `allow_nil: true`")
94
+ end
95
+
96
+ if expected != actual
97
+ @__m.fail(
98
+ message,
99
+ @__m.diff(expected, actual) ||
100
+ "Expected: #{@__m.pp(expected)}\n" \
101
+ " Actual: #{@__m.pp(actual)}",
102
+ )
103
+ end
104
+ end
105
+ end
106
+
107
+ def refute_equal(expected, actual, msg = nil, message: nil, allow_nil: false)
108
+ message = @__m.msg(msg, message)
109
+ @__m.assert do
110
+ if !allow_nil && nil == expected && !@__m.minitest_compatibility?
111
+ @__m.fail(nil, "Use refute_nil if expecting to not be nil, or pass `allow_nil: true`")
112
+ end
113
+
114
+ if expected == actual
115
+ @__m.fail(message, "Expected", @__m.pp(expected), "to not equal", @__m.pp(actual))
116
+ end
117
+ end
118
+ end
119
+
120
+ def assert_includes(collection, object, msg = nil, message: nil)
121
+ message = @__m.msg(msg, message)
122
+ @__m.assert do
123
+ unless collection.include?(object)
124
+ @__m.fail message, "Expected", @__m.pp(collection), "to include", @__m.pp(object)
125
+ end
126
+ end
127
+ end
128
+
129
+ def refute_includes(collection, object, msg = nil, message: nil)
130
+ message = @__m.msg(msg, message)
131
+ @__m.assert do
132
+ if collection.include?(object)
133
+ @__m.fail message, "Expected", @__m.pp(collection), "to not include", @__m.pp(object)
134
+ end
135
+ end
136
+ end
137
+
138
+ def assert_empty(object, msg = nil, message: nil)
139
+ message = @__m.msg(msg, message)
140
+ @__m.assert do
141
+ unless object.empty?
142
+ @__m.fail message, "Expected", @__m.pp(object), "to be empty"
143
+ end
144
+ end
145
+ end
146
+
147
+ def refute_empty(object, msg = nil, message: nil)
148
+ message = @__m.msg(msg, message)
149
+ @__m.assert do
150
+ if object.empty?
151
+ @__m.fail message, "Expected", @__m.pp(object), "to not be empty"
152
+ end
153
+ end
154
+ end
155
+
156
+ def assert_instance_of(klass, actual, msg = nil, message: nil)
157
+ message = @__m.msg(msg, message)
158
+ @__m.assert do
159
+ unless actual.instance_of?(klass)
160
+ @__m.fail(message, "Expected", @__m.pp(actual), "to be an instance of", @__m.pp(klass), "not", @__m.pp(actual.class))
161
+ end
162
+ end
163
+ end
164
+
165
+ def refute_instance_of(klass, actual, msg = nil, message: nil)
166
+ message = @__m.msg(msg, message)
167
+ @__m.assert do
168
+ if actual.instance_of?(klass)
169
+ @__m.fail(message, "Expected", @__m.pp(actual), "to not be an instance of", @__m.pp(klass))
170
+ end
171
+ end
172
+ end
173
+
174
+ def assert_kind_of(klass, actual, msg = nil, message: nil)
175
+ message = @__m.msg(msg, message)
176
+ @__m.assert do
177
+ unless actual.kind_of?(klass)
178
+ @__m.fail(message, "Expected", @__m.pp(actual), "to be a kind of", @__m.pp(klass), "not", @__m.pp(actual.class))
179
+ end
180
+ end
181
+ end
182
+
183
+ def refute_kind_of(klass, actual, msg = nil, message: nil)
184
+ message = @__m.msg(msg, message)
185
+ @__m.assert do
186
+ if actual.kind_of?(klass)
187
+ @__m.fail(message, "Expected", @__m.pp(actual), "to not be a kind of", @__m.pp(klass))
188
+ end
189
+ end
190
+ end
191
+
192
+ def assert_predicate(actual, predicate, msg = nil, message: nil)
193
+ message = @__m.msg(msg, message)
194
+ @__m.assert do
195
+ unless @__m.expect_no_failures { actual.__send__(predicate) }
196
+ @__m.fail(message, "Expected", @__m.pp(actual), "to be #{predicate}")
197
+ end
198
+ end
199
+ end
200
+
201
+ def refute_predicate(actual, predicate, msg = nil, message: nil)
202
+ message = @__m.msg(msg, message)
203
+ @__m.assert do
204
+ if @__m.expect_no_failures { actual.__send__(predicate) }
205
+ @__m.fail(message, "Expected", @__m.pp(actual), "to not be #{predicate}")
206
+ end
207
+ end
208
+ end
209
+
210
+ def assert_match(original_matcher, obj, msg = nil, message: nil)
211
+ message = @__m.msg(msg, message)
212
+ @__m.assert do
213
+ matcher = if ::String === original_matcher
214
+ ::Regexp.new(::Regexp.escape(original_matcher))
215
+ else
216
+ original_matcher
217
+ end
218
+
219
+ unless match = matcher.match(obj)
220
+ @__m.fail(message, "Expected", @__m.pp(original_matcher), "to match", @__m.pp(obj))
221
+ end
222
+
223
+ match
224
+ end
225
+ end
226
+
227
+ def refute_match(original_matcher, obj, msg = nil, message: nil)
228
+ message = @__m.msg(msg, message)
229
+ @__m.assert do
230
+ matcher = if ::String === original_matcher
231
+ ::Regexp.new(::Regexp.escape(original_matcher))
232
+ else
233
+ original_matcher
234
+ end
235
+
236
+ if matcher.match?(obj)
237
+ @__m.fail(message, "Expected", @__m.pp(original_matcher), "to not match", @__m.pp(obj))
238
+ end
239
+ end
240
+ end
241
+
242
+ def assert_respond_to(object, method, msg = nil, message: nil, include_all: false)
243
+ message = @__m.msg(msg, message)
244
+ @__m.assert do
245
+ unless object.respond_to?(method, include_all)
246
+ @__m.fail(message, "Expected", @__m.pp(object), "to respond to :#{method}")
247
+ end
248
+ end
249
+ end
250
+
251
+ def refute_respond_to(object, method, msg = nil, message: nil, include_all: false)
252
+ message = @__m.msg(msg, message)
253
+ @__m.assert do
254
+ if object.respond_to?(method, include_all)
255
+ @__m.fail(message, "Expected", @__m.pp(object), "to not respond to :#{method}")
256
+ end
257
+ end
258
+ end
259
+
260
+ def assert_same(expected, actual, msg = nil, message: nil)
261
+ message = @__m.msg(msg, message)
262
+ @__m.assert do
263
+ unless expected.equal?(actual)
264
+ @__m.fail message, begin
265
+ actual_pp = @__m.pp(actual)
266
+ expected_pp = @__m.pp(expected)
267
+ if actual_pp == expected_pp
268
+ actual_pp += " (id: #{actual.object_id})"
269
+ expected_pp += " (id: #{expected.object_id})"
270
+ end
271
+
272
+ "Expected #{actual_pp}\n" \
273
+ "To be the same as #{expected_pp}"
274
+ end
275
+ end
276
+ end
277
+ end
278
+
279
+ def refute_same(expected, actual, msg = nil, message: nil)
280
+ message = @__m.msg(msg, message)
281
+ @__m.assert do
282
+ if expected.equal?(actual)
283
+ @__m.fail message, begin
284
+ actual_pp = @__m.pp(actual)
285
+ expected_pp = @__m.pp(expected)
286
+ if actual_pp == expected_pp
287
+ actual_pp += " (id: #{actual.object_id})"
288
+ expected_pp += " (id: #{expected.object_id})"
289
+ end
290
+
291
+ "Expected #{actual_pp}\n" \
292
+ "To not be the same as #{expected_pp}"
293
+ end
294
+ end
295
+ end
296
+ end
297
+
298
+ def assert_raises(expected = StandardError, *expected_exceptions, message: nil)
299
+ msg = expected_exceptions.pop if expected_exceptions.last.is_a?(String)
300
+ message = @__m.msg(msg, message)
301
+ @__m.assert do
302
+ @__m.fail("assert_raises requires a block to capture errors.") unless block_given?
303
+
304
+ begin
305
+ yield
306
+ rescue expected, *expected_exceptions => exception
307
+ return exception
308
+ rescue ::Megatest::Assertion, *::Megatest::IGNORED_ERRORS
309
+ raise # Pass through
310
+ rescue ::Exception => unexepected_exception
311
+ error = @__m.strip_backtrace(unexepected_exception, __FILE__, __LINE__ - 6, 0)
312
+
313
+ expected_pp = if expected_exceptions.empty?
314
+ @__m.pp(expected)
315
+ else
316
+ expected_exceptions.map { |e| @__m.pp(e) }.join(", ") << " or #{@__m.pp(expected)}"
317
+ end
318
+
319
+ @__m.fail(message, "#{expected_pp} exception expected, not:\n#{@__m.pp(error)}")
320
+ end
321
+
322
+ expected_pp = if expected_exceptions.empty?
323
+ @__m.pp(expected)
324
+ else
325
+ expected_exceptions.map { |e| @__m.pp(e) }.join(", ") << " or #{@__m.pp(expected)}"
326
+ end
327
+
328
+ @__m.fail(message, "Expected", expected_pp, "but nothing was raised.")
329
+ end
330
+ end
331
+
332
+ def assert_throws(thrown_object, msg = nil, message: nil)
333
+ message = @__m.msg(msg, message)
334
+ @__m.assert do
335
+ caught = true
336
+ value = catch(thrown_object) do
337
+ @__m.expect_no_failures do
338
+ yield
339
+ rescue UncaughtThrowError => error
340
+ @__m.fail(message, "Expected", @__m.pp(thrown_object), "to have been thrown, not:", @__m.pp(error.tag))
341
+ end
342
+ caught = false
343
+ end
344
+
345
+ unless caught
346
+ @__m.fail(message, "Expected", @__m.pp(thrown_object), "to have been thrown, but it wasn't")
347
+ end
348
+
349
+ value
350
+ end
351
+ end
352
+
353
+ def assert_operator(left, operator, right, msg = nil, message: nil)
354
+ message = @__m.msg(msg, message)
355
+ @__m.assert do
356
+ unless left.__send__(operator, right)
357
+ @__m.fail(message, "Expected", @__m.pp(left), "to be #{operator}", @__m.pp(right))
358
+ end
359
+ end
360
+ end
361
+
362
+ def refute_operator(left, operator, right, msg = nil, message: nil)
363
+ message = @__m.msg(msg, message)
364
+ @__m.assert do
365
+ if left.__send__(operator, right)
366
+ @__m.fail(message, "Expected", @__m.pp(left), "to not be #{operator}", @__m.pp(right))
367
+ end
368
+ end
369
+ end
370
+
371
+ def assert_in_delta(expected, actual, delta = 0.001, msg = nil, message: nil)
372
+ message = @__m.msg(msg, message)
373
+ @__m.assert do
374
+ diff = (expected - actual).abs
375
+ unless delta >= diff
376
+ @__m.fail(message, "Expected", "|#{@__m.pp(expected)} - #{@__m.pp(actual)}| (#{diff})", "to be <= #{delta}")
377
+ end
378
+ end
379
+ end
380
+
381
+ def refute_in_delta(expected, actual, delta = 0.001, msg = nil, message: nil)
382
+ message = @__m.msg(msg, message)
383
+ @__m.assert do
384
+ diff = (expected - actual).abs
385
+ if delta >= diff
386
+ @__m.fail(message, "Expected", "|#{@__m.pp(expected)} - #{@__m.pp(actual)}| (#{diff})", "to not be <= #{delta}")
387
+ end
388
+ end
389
+ end
390
+
391
+ def assert_in_epsilon(expected, actual, epsilon = 0.001, msg = nil, message: nil)
392
+ message = @__m.msg(msg, message)
393
+ @__m.assert do
394
+ diff = (expected - actual).abs
395
+ delta = [expected.abs, actual.abs].min * epsilon
396
+ unless delta >= diff
397
+ @__m.fail(message, "Expected", "|#{@__m.pp(expected)} - #{@__m.pp(actual)}| (#{diff})", "to be <= #{delta}")
398
+ end
399
+ end
400
+ end
401
+
402
+ def refute_in_epsilon(expected, actual, epsilon = 0.001, msg = nil, message: nil)
403
+ message = @__m.msg(msg, message)
404
+ @__m.assert do
405
+ diff = (expected - actual).abs
406
+ delta = [expected.abs, actual.abs].min * epsilon
407
+ if delta >= diff
408
+ @__m.fail(message, "Expected", "|#{@__m.pp(expected)} - #{@__m.pp(actual)}| (#{diff})", "to not be <= #{delta}")
409
+ end
410
+ end
411
+ end
412
+
413
+ def skip(message = nil)
414
+ message ||= "Skipped, no message given"
415
+ ::Kernel.raise(::Megatest::Skip, message, nil)
416
+ end
417
+
418
+ def flunk(msg = nil, message: nil)
419
+ message = @__m.msg(msg, message)
420
+ @__m.assert do
421
+ @__m.fail(message || "Failed")
422
+ end
423
+ end
424
+
425
+ def assert_output(expected_stdout = nil, expected_stderr = nil, &block)
426
+ @__m.assert do
427
+ @__m.fail("assert_output requires a block to capture output.") unless block_given?
428
+
429
+ actual_stdout, actual_stderr = @__m.expect_no_failures do
430
+ capture_io(&block)
431
+ end
432
+
433
+ if expected_stderr
434
+ if Regexp === expected_stderr
435
+ assert_match(expected_stderr, actual_stderr, message: "In stderr")
436
+ else
437
+ assert_equal(expected_stderr, actual_stderr, message: "In stderr")
438
+ end
439
+ end
440
+
441
+ if expected_stdout
442
+ if Regexp === expected_stdout
443
+ assert_match(expected_stdout, actual_stdout, message: "In stdout")
444
+ else
445
+ assert_equal(expected_stdout, actual_stdout, message: "In stdout")
446
+ end
447
+ end
448
+ end
449
+ end
450
+
451
+ def assert_silent(&block)
452
+ @__m.assert do
453
+ assert_output("", "", &block)
454
+ end
455
+ end
456
+
457
+ def capture_io
458
+ require "stringio" unless defined?(::StringIO)
459
+ captured_stdout, captured_stderr = ::StringIO.new, ::StringIO.new
460
+
461
+ orig_stdout, orig_stderr = $stdout, $stderr
462
+ $stdout, $stderr = captured_stdout, captured_stderr
463
+
464
+ begin
465
+ yield
466
+
467
+ [captured_stdout.string, captured_stderr.string]
468
+ ensure
469
+ $stdout = orig_stdout
470
+ $stderr = orig_stderr
471
+ end
472
+ end
473
+ end
474
+ end