smartest 0.2.0.alpha1 → 0.2.0.alpha3
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 +1 -1
- data/DEVELOPMENT.md +56 -5
- data/README.md +47 -0
- data/SMARTEST_DESIGN.md +49 -11
- data/lib/smartest/errors.rb +24 -0
- data/lib/smartest/execution_context.rb +14 -0
- data/lib/smartest/expectation_target.rb +6 -0
- data/lib/smartest/fixture.rb +6 -0
- data/lib/smartest/hook_contexts.rb +10 -1
- data/lib/smartest/matchers.rb +209 -0
- data/lib/smartest/reporter.rb +22 -2
- data/lib/smartest/runner.rb +31 -13
- data/lib/smartest/test_result.rb +35 -2
- data/lib/smartest/test_run_state.rb +16 -0
- data/lib/smartest/version.rb +1 -1
- data/lib/smartest.rb +1 -0
- data/smartest/smartest_test.rb +257 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 17a325d82883047881ac13538f95b3af038e361668869860034acc530ce2e398
|
|
4
|
+
data.tar.gz: 19c6a1a345e7e0fd02a12ae007a48f38c1be264debd3b23b0b6085d687a867bf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5b4aeb31472fca3b0911dfb29d4034405f493ac37e3019888a67f1c13c0b1f4aac99dc33b87c7e79a8d363b7bf12f7682b97bea396ebc56b558b592a7658eee9
|
|
7
|
+
data.tar.gz: 4e1c0cc3b739b62af8c4eeef394662f8115e35f890cdde87ce32117eca26c4064b627a08a2ca204c856276d2a603de8c4c55e2f9c71d2f68c44fd864e13c2ecc
|
data/CHANGELOG.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
- Support required keyword-argument fixture injection and fixture dependencies.
|
|
9
9
|
- Support per-test fixture caching and cleanup.
|
|
10
10
|
- Support suite-scoped fixtures through `suite_fixture`.
|
|
11
|
-
- Support `eq`, `include`, `be_nil`, and `
|
|
11
|
+
- Support `eq`, `include`, `start_with`, `end_with`, `be_nil`, `raise_error`, and `change` matchers.
|
|
12
12
|
- Support custom matcher modules through `use_matcher`.
|
|
13
13
|
- Generate an opt-in `PredicateMatcher` custom matcher for `be_<predicate>` calls.
|
|
14
14
|
- Add the `smartest` CLI.
|
data/DEVELOPMENT.md
CHANGED
|
@@ -224,6 +224,7 @@ Responsibilities:
|
|
|
224
224
|
- supports inheritance
|
|
225
225
|
- exposes `cleanup` to fixture blocks
|
|
226
226
|
- optionally delegates helper methods to `ExecutionContext`
|
|
227
|
+
- does not delegate `skip` or `pending` to fixture blocks
|
|
227
228
|
|
|
228
229
|
Fixture definitions should not execute at declaration time.
|
|
229
230
|
|
|
@@ -306,6 +307,7 @@ Responsibilities:
|
|
|
306
307
|
|
|
307
308
|
- include expectation methods
|
|
308
309
|
- include matchers
|
|
310
|
+
- expose `skip` and `pending` to test bodies
|
|
309
311
|
- expose helper methods
|
|
310
312
|
- optionally provide integration helpers later
|
|
311
313
|
|
|
@@ -329,6 +331,7 @@ Responsibilities:
|
|
|
329
331
|
- resolve test keyword fixtures
|
|
330
332
|
- run test body
|
|
331
333
|
- run cleanup in `ensure`
|
|
334
|
+
- track skipped and pending test state
|
|
332
335
|
- run suite fixture cleanup after all tests
|
|
333
336
|
- produce `TestResult`
|
|
334
337
|
- notify reporter
|
|
@@ -342,7 +345,8 @@ Pseudo-code:
|
|
|
342
345
|
|
|
343
346
|
```ruby
|
|
344
347
|
def run_one(test_case)
|
|
345
|
-
|
|
348
|
+
run_state = TestRunState.new
|
|
349
|
+
context = ExecutionContext.new(run_state: run_state)
|
|
346
350
|
fixture_set = FixtureSet.new(
|
|
347
351
|
@fixture_classes,
|
|
348
352
|
context: context,
|
|
@@ -353,9 +357,19 @@ def run_one(test_case)
|
|
|
353
357
|
|
|
354
358
|
context.instance_exec(**kwargs, &test_case.block)
|
|
355
359
|
|
|
356
|
-
|
|
360
|
+
if run_state.pending?
|
|
361
|
+
TestResult.failed(test_case, PendingPassedError.new(run_state.pending_reason))
|
|
362
|
+
else
|
|
363
|
+
TestResult.passed(test_case)
|
|
364
|
+
end
|
|
365
|
+
rescue Skipped => skipped
|
|
366
|
+
TestResult.skipped(test_case, skipped.reason)
|
|
357
367
|
rescue Exception => error
|
|
358
|
-
|
|
368
|
+
if run_state.pending?
|
|
369
|
+
TestResult.pending(test_case, run_state.pending_reason)
|
|
370
|
+
else
|
|
371
|
+
TestResult.failed(test_case, error)
|
|
372
|
+
end
|
|
359
373
|
ensure
|
|
360
374
|
fixture_set&.run_cleanups
|
|
361
375
|
end
|
|
@@ -370,13 +384,15 @@ Fields:
|
|
|
370
384
|
- test case
|
|
371
385
|
- status
|
|
372
386
|
- error
|
|
387
|
+
- reason
|
|
373
388
|
- duration
|
|
374
389
|
|
|
375
390
|
Statuses:
|
|
376
391
|
|
|
377
392
|
- passed
|
|
378
393
|
- failed
|
|
379
|
-
- skipped
|
|
394
|
+
- skipped
|
|
395
|
+
- pending
|
|
380
396
|
|
|
381
397
|
### `Smartest::Reporter`
|
|
382
398
|
|
|
@@ -385,7 +401,7 @@ Console reporter for MVP.
|
|
|
385
401
|
Responsibilities:
|
|
386
402
|
|
|
387
403
|
- print run start
|
|
388
|
-
- print per-test pass/fail
|
|
404
|
+
- print per-test pass/fail/skip/pending
|
|
389
405
|
- print failure details
|
|
390
406
|
- print summary
|
|
391
407
|
- return appropriate exit status through runner
|
|
@@ -565,6 +581,8 @@ module Smartest
|
|
|
565
581
|
class CircularFixtureDependencyError < Error; end
|
|
566
582
|
class InvalidFixtureParameterError < Error; end
|
|
567
583
|
class AssertionFailed < Error; end
|
|
584
|
+
class Skipped < Error; end
|
|
585
|
+
class PendingPassedError < AssertionFailed; end
|
|
568
586
|
end
|
|
569
587
|
```
|
|
570
588
|
|
|
@@ -649,6 +667,15 @@ A practical approach:
|
|
|
649
667
|
- expose `use_fixture` and `use_matcher` only inside hook contexts
|
|
650
668
|
- make `around_test` registered from `around_suite` suite-wide
|
|
651
669
|
|
|
670
|
+
### Phase 9: Skipped and pending tests
|
|
671
|
+
|
|
672
|
+
- `skip "reason"` inside test bodies and `around_test` hooks
|
|
673
|
+
- `pending "reason"` inside test bodies and `around_test` hooks
|
|
674
|
+
- skipped tests do not fail the run
|
|
675
|
+
- pending tests pass only when the remaining execution fails
|
|
676
|
+
- pending tests fail with `Smartest::PendingPassedError` when they pass
|
|
677
|
+
- `skip` and `pending` are not `test` metadata
|
|
678
|
+
|
|
652
679
|
## MVP API rules
|
|
653
680
|
|
|
654
681
|
Supported:
|
|
@@ -658,6 +685,19 @@ test("name") do
|
|
|
658
685
|
end
|
|
659
686
|
```
|
|
660
687
|
|
|
688
|
+
```ruby
|
|
689
|
+
test("name") do
|
|
690
|
+
skip "reason" if runtime_condition?
|
|
691
|
+
end
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
```ruby
|
|
695
|
+
test("name") do
|
|
696
|
+
pending "reason" if expected_to_fail?
|
|
697
|
+
expect(actual).to eq(expected)
|
|
698
|
+
end
|
|
699
|
+
```
|
|
700
|
+
|
|
661
701
|
```ruby
|
|
662
702
|
test("name") do |user:|
|
|
663
703
|
end
|
|
@@ -685,6 +725,7 @@ end
|
|
|
685
725
|
|
|
686
726
|
```ruby
|
|
687
727
|
around_test do |test|
|
|
728
|
+
pending "reason" if expected_to_fail?
|
|
688
729
|
test.run
|
|
689
730
|
end
|
|
690
731
|
```
|
|
@@ -706,6 +747,16 @@ fixture :client, with: [:server] do |server|
|
|
|
706
747
|
end
|
|
707
748
|
```
|
|
708
749
|
|
|
750
|
+
```ruby
|
|
751
|
+
test("name", skip: true) do
|
|
752
|
+
end
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
```ruby
|
|
756
|
+
test("name", pending: true) do
|
|
757
|
+
end
|
|
758
|
+
```
|
|
759
|
+
|
|
709
760
|
```ruby
|
|
710
761
|
resource :server do |use|
|
|
711
762
|
end
|
data/README.md
CHANGED
|
@@ -143,6 +143,34 @@ end
|
|
|
143
143
|
|
|
144
144
|
This makes fixture usage explicit and avoids relying on positional argument order.
|
|
145
145
|
|
|
146
|
+
## Skipping and pending tests
|
|
147
|
+
|
|
148
|
+
Use `skip` at the start of a test when the rest of the body should not run:
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
test("PDF export") do |browser:|
|
|
152
|
+
skip "firefox is not supported" if browser.firefox?
|
|
153
|
+
|
|
154
|
+
export_pdf(browser)
|
|
155
|
+
end
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Use `pending` when the test should continue running but is expected to fail. If
|
|
159
|
+
the test passes after `pending`, Smartest fails it so the stale pending marker is
|
|
160
|
+
removed.
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
test("PDF export") do |browser:|
|
|
164
|
+
pending "Not supported by WebDriver BiDi yet" if browser.bidi?
|
|
165
|
+
|
|
166
|
+
export_pdf(browser)
|
|
167
|
+
end
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`skip` and `pending` are available in test bodies and `around_test` hooks, but
|
|
171
|
+
not as `test` metadata or fixture APIs. See
|
|
172
|
+
[Skipping Tests](documentation/docs/skipping-tests.md).
|
|
173
|
+
|
|
146
174
|
## Expectations
|
|
147
175
|
|
|
148
176
|
Smartest uses an expectation style:
|
|
@@ -150,6 +178,8 @@ Smartest uses an expectation style:
|
|
|
150
178
|
```ruby
|
|
151
179
|
expect(actual).to eq(expected)
|
|
152
180
|
expect(actual).not_to eq(expected)
|
|
181
|
+
expect { action }.to raise_error(ErrorClass)
|
|
182
|
+
expect { action }.to change { value }
|
|
153
183
|
```
|
|
154
184
|
|
|
155
185
|
Examples:
|
|
@@ -162,6 +192,14 @@ end
|
|
|
162
192
|
test("array") do
|
|
163
193
|
expect([1, 2, 3]).to include(2)
|
|
164
194
|
end
|
|
195
|
+
|
|
196
|
+
test("URL") do
|
|
197
|
+
expect("about:blank").to start_with("about:")
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
test("download") do
|
|
201
|
+
expect("screenshot.png").to end_with(".png")
|
|
202
|
+
end
|
|
165
203
|
```
|
|
166
204
|
|
|
167
205
|
Supported matchers include:
|
|
@@ -169,10 +207,18 @@ Supported matchers include:
|
|
|
169
207
|
```ruby
|
|
170
208
|
eq(expected)
|
|
171
209
|
include(expected)
|
|
210
|
+
start_with(prefix, ...)
|
|
211
|
+
end_with(suffix, ...)
|
|
172
212
|
be_nil
|
|
173
213
|
raise_error(ErrorClass)
|
|
214
|
+
change { value }
|
|
215
|
+
change { value }.from(before).to(after)
|
|
216
|
+
change { value }.by(delta)
|
|
174
217
|
```
|
|
175
218
|
|
|
219
|
+
`change` is only supported with `expect { ... }` block expectations and must be
|
|
220
|
+
written with a value block.
|
|
221
|
+
|
|
176
222
|
Custom matcher modules can be registered from `around_suite` or `around_test`
|
|
177
223
|
with `use_matcher`. The generated scaffold includes a `PredicateMatcher` custom
|
|
178
224
|
matcher for `be_<predicate>` calls. See [Matchers](documentation/docs/matchers.md).
|
|
@@ -648,6 +694,7 @@ The intended MVP includes:
|
|
|
648
694
|
- fixture cleanup
|
|
649
695
|
- suite hooks with `around_suite`
|
|
650
696
|
- test hooks with `around_test`
|
|
697
|
+
- skipped and pending tests through `skip` and `pending`
|
|
651
698
|
- `expect(...).to eq(...)`
|
|
652
699
|
- console reporter
|
|
653
700
|
- CLI runner
|
data/SMARTEST_DESIGN.md
CHANGED
|
@@ -299,7 +299,8 @@ This keeps the top-level DSL small.
|
|
|
299
299
|
|
|
300
300
|
Only `test`, `around_suite`, and `around_test` should be globally available when
|
|
301
301
|
using `smartest/autorun`. `use_fixture` and `use_matcher` are available only
|
|
302
|
-
inside hook execution contexts.
|
|
302
|
+
inside hook execution contexts. `skip` and `pending` are available only inside
|
|
303
|
+
test bodies and `around_test` hook execution contexts.
|
|
303
304
|
|
|
304
305
|
## Core architecture
|
|
305
306
|
|
|
@@ -691,6 +692,8 @@ end
|
|
|
691
692
|
Fixture blocks can call private methods because they execute with `instance_exec`.
|
|
692
693
|
|
|
693
694
|
Fixture classes may optionally delegate missing methods to the execution context.
|
|
695
|
+
They should not delegate `skip` or `pending`; those are test-body and
|
|
696
|
+
`around_test` controls, not fixture APIs.
|
|
694
697
|
|
|
695
698
|
This is useful for integration helpers.
|
|
696
699
|
|
|
@@ -714,6 +717,19 @@ MVP expectation API:
|
|
|
714
717
|
```ruby
|
|
715
718
|
expect(actual).to eq(expected)
|
|
716
719
|
expect(actual).not_to eq(expected)
|
|
720
|
+
expect { action }.to change { value }
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
Current built-in matchers include:
|
|
724
|
+
|
|
725
|
+
```ruby
|
|
726
|
+
eq(expected)
|
|
727
|
+
include(expected)
|
|
728
|
+
start_with(prefix, ...)
|
|
729
|
+
end_with(suffix, ...)
|
|
730
|
+
be_nil
|
|
731
|
+
raise_error(ErrorClass)
|
|
732
|
+
change { value }
|
|
717
733
|
```
|
|
718
734
|
|
|
719
735
|
Internal model:
|
|
@@ -831,28 +847,38 @@ Care must be taken not to run twice if both CLI and autorun are used.
|
|
|
831
847
|
|
|
832
848
|
## Exit status
|
|
833
849
|
|
|
834
|
-
- all tests passed: `0`
|
|
850
|
+
- all tests passed, skipped, or pending as expected: `0`
|
|
835
851
|
- any test failed: `1`
|
|
852
|
+
- pending test unexpectedly passed: `1`
|
|
836
853
|
- configuration/load error: `1`
|
|
837
854
|
- interrupted: re-raise or exit non-zero
|
|
838
855
|
|
|
839
856
|
## Metadata
|
|
840
857
|
|
|
841
|
-
`test`
|
|
858
|
+
`test` accepts metadata and stores it on `TestCase`, but metadata does not drive
|
|
859
|
+
runner behavior in the MVP:
|
|
842
860
|
|
|
843
861
|
```ruby
|
|
844
|
-
test("name", skip: true) do
|
|
845
|
-
end
|
|
846
|
-
|
|
847
862
|
test("name", tags: [:db]) do
|
|
848
863
|
end
|
|
849
864
|
```
|
|
850
865
|
|
|
851
|
-
|
|
866
|
+
Runtime skipping and pending behavior are method calls inside the test body or
|
|
867
|
+
`around_test`, not metadata:
|
|
868
|
+
|
|
869
|
+
```ruby
|
|
870
|
+
test("name") do
|
|
871
|
+
skip "reason" if runtime_condition?
|
|
872
|
+
end
|
|
873
|
+
|
|
874
|
+
test("name") do
|
|
875
|
+
pending "reason" if expected_to_fail?
|
|
876
|
+
expect(actual).to eq(expected)
|
|
877
|
+
end
|
|
878
|
+
```
|
|
852
879
|
|
|
853
880
|
Useful metadata later:
|
|
854
881
|
|
|
855
|
-
- `skip: true`
|
|
856
882
|
- `only: true`
|
|
857
883
|
- `tags: [:db]`
|
|
858
884
|
- `timeout: 5`
|
|
@@ -1198,18 +1224,30 @@ test("GET /health") do |client:|
|
|
|
1198
1224
|
end
|
|
1199
1225
|
```
|
|
1200
1226
|
|
|
1227
|
+
```ruby
|
|
1228
|
+
test("PDF export") do |browser:|
|
|
1229
|
+
skip "firefox is not supported" if browser.firefox?
|
|
1230
|
+
|
|
1231
|
+
export_pdf(browser)
|
|
1232
|
+
end
|
|
1233
|
+
|
|
1234
|
+
test("PDF export over BiDi") do |browser:|
|
|
1235
|
+
pending "Not supported by WebDriver BiDi yet" if browser.bidi?
|
|
1236
|
+
|
|
1237
|
+
export_pdf(browser)
|
|
1238
|
+
expect(browser.downloads).not_to be_empty
|
|
1239
|
+
end
|
|
1240
|
+
```
|
|
1241
|
+
|
|
1201
1242
|
## Future ideas
|
|
1202
1243
|
|
|
1203
1244
|
Possible future features:
|
|
1204
1245
|
|
|
1205
|
-
- `skip`
|
|
1206
1246
|
- `only`
|
|
1207
1247
|
- tags
|
|
1208
1248
|
- custom reporters
|
|
1209
1249
|
- JSON output
|
|
1210
1250
|
- richer matchers
|
|
1211
|
-
- block expectations
|
|
1212
|
-
- `raise_error`
|
|
1213
1251
|
- file-scoped fixtures
|
|
1214
1252
|
- parallel execution
|
|
1215
1253
|
- watch mode
|
data/lib/smartest/errors.rb
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Smartest
|
|
4
|
+
module StatusReason
|
|
5
|
+
DEFAULT_REASON = "No reason given"
|
|
6
|
+
|
|
7
|
+
def self.normalize(reason)
|
|
8
|
+
text = reason.to_s
|
|
9
|
+
text.empty? ? DEFAULT_REASON : text
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
4
13
|
class Error < StandardError; end
|
|
5
14
|
|
|
6
15
|
class FixtureNotFoundError < Error
|
|
@@ -65,4 +74,19 @@ module Smartest
|
|
|
65
74
|
class AroundTestRunError < Error; end
|
|
66
75
|
|
|
67
76
|
class AssertionFailed < Error; end
|
|
77
|
+
|
|
78
|
+
class Skipped < Error
|
|
79
|
+
attr_reader :reason
|
|
80
|
+
|
|
81
|
+
def initialize(reason = nil)
|
|
82
|
+
@reason = StatusReason.normalize(reason)
|
|
83
|
+
super(@reason)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
class PendingPassedError < AssertionFailed
|
|
88
|
+
def initialize(reason = nil)
|
|
89
|
+
super("expected pending test to fail, but it passed: #{StatusReason.normalize(reason)}")
|
|
90
|
+
end
|
|
91
|
+
end
|
|
68
92
|
end
|
|
@@ -4,5 +4,19 @@ module Smartest
|
|
|
4
4
|
class ExecutionContext
|
|
5
5
|
include Expectations
|
|
6
6
|
include Matchers
|
|
7
|
+
|
|
8
|
+
def initialize(run_state: TestRunState.new)
|
|
9
|
+
@run_state = run_state
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def skip(reason = nil)
|
|
15
|
+
raise Skipped, reason
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def pending(reason = nil)
|
|
19
|
+
@run_state.pending(reason)
|
|
20
|
+
end
|
|
7
21
|
end
|
|
8
22
|
end
|
|
@@ -13,6 +13,12 @@ module Smartest
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def not_to(matcher)
|
|
16
|
+
if matcher.respond_to?(:does_not_match?)
|
|
17
|
+
return self if matcher.does_not_match?(@actual)
|
|
18
|
+
|
|
19
|
+
raise AssertionFailed, matcher.negated_failure_message
|
|
20
|
+
end
|
|
21
|
+
|
|
16
22
|
return self unless matcher.matches?(@actual)
|
|
17
23
|
|
|
18
24
|
raise AssertionFailed, matcher.negated_failure_message
|
data/lib/smartest/fixture.rb
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Smartest
|
|
4
4
|
class Fixture
|
|
5
|
+
RESERVED_CONTEXT_METHODS = %i[skip pending].freeze
|
|
6
|
+
|
|
5
7
|
class << self
|
|
6
8
|
def fixture(name, scope: :test, &block)
|
|
7
9
|
define_fixture(
|
|
@@ -64,6 +66,8 @@ module Smartest
|
|
|
64
66
|
end
|
|
65
67
|
|
|
66
68
|
def method_missing(method_name, *args, &block)
|
|
69
|
+
return super if RESERVED_CONTEXT_METHODS.include?(method_name)
|
|
70
|
+
|
|
67
71
|
if @context.respond_to?(method_name, true)
|
|
68
72
|
@context.__send__(method_name, *args, &block)
|
|
69
73
|
else
|
|
@@ -72,6 +76,8 @@ module Smartest
|
|
|
72
76
|
end
|
|
73
77
|
|
|
74
78
|
def respond_to_missing?(method_name, include_private = false)
|
|
79
|
+
return super if RESERVED_CONTEXT_METHODS.include?(method_name)
|
|
80
|
+
|
|
75
81
|
@context.respond_to?(method_name, true) || super
|
|
76
82
|
end
|
|
77
83
|
end
|
|
@@ -30,8 +30,9 @@ module Smartest
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
class AroundTestContext
|
|
33
|
-
def initialize(test_run)
|
|
33
|
+
def initialize(test_run, run_state:)
|
|
34
34
|
@test_run = test_run
|
|
35
|
+
@run_state = run_state
|
|
35
36
|
end
|
|
36
37
|
|
|
37
38
|
def call(hook, run_target = @test_run)
|
|
@@ -47,5 +48,13 @@ module Smartest
|
|
|
47
48
|
def use_matcher(matcher_module)
|
|
48
49
|
@test_run.add_matcher_module(matcher_module)
|
|
49
50
|
end
|
|
51
|
+
|
|
52
|
+
def skip(reason = nil)
|
|
53
|
+
raise Skipped, reason
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def pending(reason = nil)
|
|
57
|
+
@run_state.pending(reason)
|
|
58
|
+
end
|
|
50
59
|
end
|
|
51
60
|
end
|
data/lib/smartest/matchers.rb
CHANGED
|
@@ -10,6 +10,14 @@ module Smartest
|
|
|
10
10
|
IncludeMatcher.new(expected)
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
+
def start_with(*prefixes)
|
|
14
|
+
StartWithMatcher.new(*prefixes)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def end_with(*suffixes)
|
|
18
|
+
EndWithMatcher.new(*suffixes)
|
|
19
|
+
end
|
|
20
|
+
|
|
13
21
|
def be_nil
|
|
14
22
|
BeNilMatcher.new
|
|
15
23
|
end
|
|
@@ -17,6 +25,13 @@ module Smartest
|
|
|
17
25
|
def raise_error(expected_error = StandardError)
|
|
18
26
|
RaiseErrorMatcher.new(expected_error)
|
|
19
27
|
end
|
|
28
|
+
|
|
29
|
+
def change(*args, &block)
|
|
30
|
+
raise ArgumentError, "change does not support arguments; use change { ... }" if args.any?
|
|
31
|
+
raise ArgumentError, "change requires a block" unless block
|
|
32
|
+
|
|
33
|
+
ChangeMatcher.new(block)
|
|
34
|
+
end
|
|
20
35
|
end
|
|
21
36
|
|
|
22
37
|
class EqMatcher
|
|
@@ -59,6 +74,64 @@ module Smartest
|
|
|
59
74
|
end
|
|
60
75
|
end
|
|
61
76
|
|
|
77
|
+
class StartWithMatcher
|
|
78
|
+
def initialize(*prefixes)
|
|
79
|
+
@prefixes = prefixes
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def matches?(actual)
|
|
83
|
+
@actual = actual
|
|
84
|
+
actual.start_with?(*@prefixes)
|
|
85
|
+
rescue NoMethodError
|
|
86
|
+
false
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def failure_message
|
|
90
|
+
"expected #{@actual.inspect} to start with #{expected_description}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def negated_failure_message
|
|
94
|
+
"expected #{@actual.inspect} not to start with #{expected_description}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def expected_description
|
|
100
|
+
return "no prefixes" if @prefixes.empty?
|
|
101
|
+
|
|
102
|
+
@prefixes.map(&:inspect).join(" or ")
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
class EndWithMatcher
|
|
107
|
+
def initialize(*suffixes)
|
|
108
|
+
@suffixes = suffixes
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def matches?(actual)
|
|
112
|
+
@actual = actual
|
|
113
|
+
actual.end_with?(*@suffixes)
|
|
114
|
+
rescue NoMethodError
|
|
115
|
+
false
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def failure_message
|
|
119
|
+
"expected #{@actual.inspect} to end with #{expected_description}"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def negated_failure_message
|
|
123
|
+
"expected #{@actual.inspect} not to end with #{expected_description}"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def expected_description
|
|
129
|
+
return "no suffixes" if @suffixes.empty?
|
|
130
|
+
|
|
131
|
+
@suffixes.map(&:inspect).join(" or ")
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
62
135
|
class BeNilMatcher
|
|
63
136
|
def matches?(actual)
|
|
64
137
|
@actual = actual
|
|
@@ -106,4 +179,140 @@ module Smartest
|
|
|
106
179
|
"expected block not to raise #{@expected_error}, but raised #{@actual_error.class}: #{@actual_error.message}"
|
|
107
180
|
end
|
|
108
181
|
end
|
|
182
|
+
|
|
183
|
+
class ChangeMatcher
|
|
184
|
+
UNSET = Object.new
|
|
185
|
+
|
|
186
|
+
def initialize(value_block)
|
|
187
|
+
@value_block = value_block
|
|
188
|
+
@expected_from = UNSET
|
|
189
|
+
@expected_to = UNSET
|
|
190
|
+
@expected_delta = UNSET
|
|
191
|
+
reset_result
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def from(expected)
|
|
195
|
+
@expected_from = expected
|
|
196
|
+
self
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def to(expected)
|
|
200
|
+
@expected_to = expected
|
|
201
|
+
self
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def by(expected_delta)
|
|
205
|
+
@expected_delta = expected_delta
|
|
206
|
+
self
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def matches?(actual)
|
|
210
|
+
run_change(actual)
|
|
211
|
+
return false unless @callable
|
|
212
|
+
|
|
213
|
+
positive_failures.empty?
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def does_not_match?(actual)
|
|
217
|
+
run_change(actual)
|
|
218
|
+
return false unless @callable
|
|
219
|
+
|
|
220
|
+
negated_failures.empty?
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def failure_message
|
|
224
|
+
return "expected a block to change value" unless @callable
|
|
225
|
+
|
|
226
|
+
"expected value to #{expected_description}, but #{observed_description}#{failed_modifier_description}"
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def negated_failure_message
|
|
230
|
+
return "expected a block not to change value" unless @callable
|
|
231
|
+
|
|
232
|
+
"expected value not to change, but #{observed_description}"
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
private
|
|
236
|
+
|
|
237
|
+
def reset_result
|
|
238
|
+
@callable = true
|
|
239
|
+
@before_value = nil
|
|
240
|
+
@after_value = nil
|
|
241
|
+
@actual_delta = UNSET
|
|
242
|
+
@failed_modifiers = []
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def run_change(actual)
|
|
246
|
+
reset_result
|
|
247
|
+
@callable = actual.respond_to?(:call)
|
|
248
|
+
return unless @callable
|
|
249
|
+
|
|
250
|
+
@before_value = @value_block.call
|
|
251
|
+
actual.call
|
|
252
|
+
@after_value = @value_block.call
|
|
253
|
+
calculate_delta if delta_expected?
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def positive_failures
|
|
257
|
+
@failed_modifiers = []
|
|
258
|
+
@failed_modifiers << "change" if !delta_expected? && @before_value == @after_value
|
|
259
|
+
@failed_modifiers << "from(#{@expected_from.inspect})" if from_expected? && @before_value != @expected_from
|
|
260
|
+
@failed_modifiers << "to(#{@expected_to.inspect})" if to_expected? && @after_value != @expected_to
|
|
261
|
+
@failed_modifiers << "by(#{@expected_delta.inspect})" if delta_expected? && @actual_delta != @expected_delta
|
|
262
|
+
@failed_modifiers
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def negated_failures
|
|
266
|
+
@failed_modifiers = []
|
|
267
|
+
@failed_modifiers << "change" unless @before_value == @after_value
|
|
268
|
+
@failed_modifiers
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def expected_description
|
|
272
|
+
parts = ["change"]
|
|
273
|
+
parts << "from #{@expected_from.inspect}" if from_expected?
|
|
274
|
+
parts << "to #{@expected_to.inspect}" if to_expected?
|
|
275
|
+
parts << "by #{@expected_delta.inspect}" if delta_expected?
|
|
276
|
+
parts.join(" ")
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def observed_description
|
|
280
|
+
if delta_expected?
|
|
281
|
+
delta_description = if @actual_delta.equal?(UNSET)
|
|
282
|
+
"could not calculate a numeric difference"
|
|
283
|
+
else
|
|
284
|
+
"changed by #{@actual_delta.inspect}"
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
"#{delta_description} from #{@before_value.inspect} before to #{@after_value.inspect} after"
|
|
288
|
+
else
|
|
289
|
+
"was #{@before_value.inspect} before and #{@after_value.inspect} after"
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def failed_modifier_description
|
|
294
|
+
failures = positive_failures
|
|
295
|
+
return "" if failures.empty?
|
|
296
|
+
|
|
297
|
+
"; failed modifiers: #{failures.join(', ')}"
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def calculate_delta
|
|
301
|
+
@actual_delta = @after_value - @before_value
|
|
302
|
+
rescue NoMethodError, TypeError
|
|
303
|
+
@actual_delta = UNSET
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def from_expected?
|
|
307
|
+
!@expected_from.equal?(UNSET)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def to_expected?
|
|
311
|
+
!@expected_to.equal?(UNSET)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def delta_expected?
|
|
315
|
+
!@expected_delta.equal?(UNSET)
|
|
316
|
+
end
|
|
317
|
+
end
|
|
109
318
|
end
|
data/lib/smartest/reporter.rb
CHANGED
|
@@ -4,6 +4,8 @@ module Smartest
|
|
|
4
4
|
class Reporter
|
|
5
5
|
PASS_MARK = "\u2713"
|
|
6
6
|
FAIL_MARK = "\u2717"
|
|
7
|
+
SKIP_MARK = "-"
|
|
8
|
+
PENDING_MARK = "*"
|
|
7
9
|
|
|
8
10
|
def initialize(io = $stdout)
|
|
9
11
|
@io = io
|
|
@@ -15,12 +17,13 @@ module Smartest
|
|
|
15
17
|
end
|
|
16
18
|
|
|
17
19
|
def record(result)
|
|
18
|
-
|
|
19
|
-
@io.puts "#{mark} #{result.test_case.name}"
|
|
20
|
+
@io.puts record_line(result)
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
def finish(results, suite_cleanup_errors: [], suite_errors: [])
|
|
23
24
|
failures = results.select(&:failed?)
|
|
25
|
+
skipped = results.select(&:skipped?)
|
|
26
|
+
pending = results.select(&:pending?)
|
|
24
27
|
|
|
25
28
|
report_failures(failures) if failures.any?
|
|
26
29
|
report_suite_errors(suite_errors) if suite_errors.any?
|
|
@@ -28,6 +31,8 @@ module Smartest
|
|
|
28
31
|
|
|
29
32
|
@io.puts
|
|
30
33
|
summary = "#{results.count} #{results.count == 1 ? 'test' : 'tests'}, #{results.count(&:passed?)} passed, #{failures.count} failed"
|
|
34
|
+
summary = "#{summary}, #{skipped.count} skipped" if skipped.any?
|
|
35
|
+
summary = "#{summary}, #{pending.count} pending" if pending.any?
|
|
31
36
|
if suite_errors.any?
|
|
32
37
|
suite_label = suite_errors.count == 1 ? "suite failure" : "suite failures"
|
|
33
38
|
summary = "#{summary}, #{suite_errors.count} #{suite_label}"
|
|
@@ -41,6 +46,21 @@ module Smartest
|
|
|
41
46
|
|
|
42
47
|
private
|
|
43
48
|
|
|
49
|
+
def record_line(result)
|
|
50
|
+
case result.status
|
|
51
|
+
when :passed
|
|
52
|
+
"#{PASS_MARK} #{result.test_case.name}"
|
|
53
|
+
when :failed
|
|
54
|
+
"#{FAIL_MARK} #{result.test_case.name}"
|
|
55
|
+
when :skipped
|
|
56
|
+
"#{SKIP_MARK} #{result.test_case.name} (skipped: #{result.reason})"
|
|
57
|
+
when :pending
|
|
58
|
+
"#{PENDING_MARK} #{result.test_case.name} (pending: #{result.reason})"
|
|
59
|
+
else
|
|
60
|
+
"#{FAIL_MARK} #{result.test_case.name}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
44
64
|
def report_failures(failures)
|
|
45
65
|
@io.puts
|
|
46
66
|
@io.puts "Failures:"
|
data/lib/smartest/runner.rb
CHANGED
|
@@ -67,16 +67,20 @@ module Smartest
|
|
|
67
67
|
def run_one(test_case)
|
|
68
68
|
started_at = now
|
|
69
69
|
error = nil
|
|
70
|
+
skipped = nil
|
|
70
71
|
cleanup_errors = []
|
|
72
|
+
run_state = TestRunState.new
|
|
71
73
|
test_run = TestRun.new(
|
|
72
74
|
fixture_classes: @suite.fixture_classes,
|
|
73
75
|
matcher_modules: @suite.matcher_modules
|
|
74
76
|
) do |fixture_classes:, matcher_modules:|
|
|
75
|
-
|
|
77
|
+
run_test_body(test_case, fixture_classes, matcher_modules, run_state, cleanup_errors)
|
|
76
78
|
end
|
|
77
79
|
|
|
78
80
|
begin
|
|
79
|
-
run_around_test_hooks(@suite.around_test_hooks + test_case.around_test_hooks, test_run)
|
|
81
|
+
run_around_test_hooks(@suite.around_test_hooks + test_case.around_test_hooks, test_run, run_state)
|
|
82
|
+
rescue Skipped => skipped_error
|
|
83
|
+
skipped = skipped_error
|
|
80
84
|
rescue Exception => rescued_error
|
|
81
85
|
raise if Smartest.fatal_exception?(rescued_error)
|
|
82
86
|
|
|
@@ -85,6 +89,19 @@ module Smartest
|
|
|
85
89
|
|
|
86
90
|
duration = now - started_at
|
|
87
91
|
|
|
92
|
+
return TestResult.failed(test_case: test_case, error: nil, duration: duration, cleanup_errors: cleanup_errors) if skipped && cleanup_errors.any?
|
|
93
|
+
return TestResult.skipped(test_case: test_case, reason: skipped.reason, duration: duration) if skipped
|
|
94
|
+
|
|
95
|
+
if run_state.pending?
|
|
96
|
+
if error && !around_test_protocol_error?(error)
|
|
97
|
+
return TestResult.failed(test_case: test_case, error: nil, duration: duration, cleanup_errors: cleanup_errors) if cleanup_errors.any?
|
|
98
|
+
|
|
99
|
+
return TestResult.pending(test_case: test_case, reason: run_state.pending_reason, duration: duration)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
error ||= PendingPassedError.new(run_state.pending_reason)
|
|
103
|
+
end
|
|
104
|
+
|
|
88
105
|
if error || cleanup_errors.any?
|
|
89
106
|
TestResult.failed(
|
|
90
107
|
test_case: test_case,
|
|
@@ -97,23 +114,20 @@ module Smartest
|
|
|
97
114
|
end
|
|
98
115
|
end
|
|
99
116
|
|
|
100
|
-
def run_test_body(test_case, fixture_classes, matcher_modules)
|
|
101
|
-
context = build_context(matcher_modules)
|
|
117
|
+
def run_test_body(test_case, fixture_classes, matcher_modules, run_state, cleanup_errors)
|
|
118
|
+
context = build_context(matcher_modules, run_state)
|
|
102
119
|
fixture_set = nil
|
|
103
|
-
cleanup_errors = []
|
|
104
120
|
|
|
105
121
|
begin
|
|
106
122
|
fixture_set = FixtureSet.new(fixture_classes, context: context, parent: suite_fixture_set)
|
|
107
123
|
fixtures = fixture_set.resolve_keywords(test_case.fixture_names)
|
|
108
124
|
context.instance_exec(**fixtures, &test_case.block)
|
|
109
125
|
ensure
|
|
110
|
-
cleanup_errors
|
|
126
|
+
cleanup_errors.concat(fixture_set.run_cleanups) if fixture_set
|
|
111
127
|
end
|
|
112
|
-
|
|
113
|
-
cleanup_errors
|
|
114
128
|
end
|
|
115
129
|
|
|
116
|
-
def run_around_test_hooks(hooks, test_run, index = 0)
|
|
130
|
+
def run_around_test_hooks(hooks, test_run, run_state, index = 0)
|
|
117
131
|
return test_run.run if index >= hooks.length
|
|
118
132
|
|
|
119
133
|
hook = hooks[index]
|
|
@@ -121,10 +135,10 @@ module Smartest
|
|
|
121
135
|
fixture_classes: [],
|
|
122
136
|
matcher_modules: []
|
|
123
137
|
) do |**_keywords|
|
|
124
|
-
run_around_test_hooks(hooks, test_run, index + 1)
|
|
138
|
+
run_around_test_hooks(hooks, test_run, run_state, index + 1)
|
|
125
139
|
end
|
|
126
140
|
|
|
127
|
-
AroundTestContext.new(test_run).call(hook, next_run)
|
|
141
|
+
AroundTestContext.new(test_run, run_state: run_state).call(hook, next_run)
|
|
128
142
|
raise AroundTestRunError, "around_test hook did not call test.run" unless next_run.ran?
|
|
129
143
|
|
|
130
144
|
next_run.result
|
|
@@ -138,12 +152,16 @@ module Smartest
|
|
|
138
152
|
)
|
|
139
153
|
end
|
|
140
154
|
|
|
141
|
-
def build_context(matcher_modules = @suite.matcher_modules)
|
|
142
|
-
ExecutionContext.new.tap do |context|
|
|
155
|
+
def build_context(matcher_modules = @suite.matcher_modules, run_state = TestRunState.new)
|
|
156
|
+
ExecutionContext.new(run_state: run_state).tap do |context|
|
|
143
157
|
matcher_modules.each { |matcher_module| context.extend(matcher_module) }
|
|
144
158
|
end
|
|
145
159
|
end
|
|
146
160
|
|
|
161
|
+
def around_test_protocol_error?(error)
|
|
162
|
+
error.is_a?(AroundTestRunError)
|
|
163
|
+
end
|
|
164
|
+
|
|
147
165
|
def now
|
|
148
166
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
149
167
|
end
|
data/lib/smartest/test_result.rb
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
module Smartest
|
|
4
4
|
class TestResult
|
|
5
|
-
attr_reader :test_case, :status, :error, :duration, :cleanup_errors
|
|
5
|
+
attr_reader :test_case, :status, :error, :duration, :cleanup_errors, :reason
|
|
6
6
|
|
|
7
7
|
def self.passed(test_case:, duration:, cleanup_errors: [])
|
|
8
8
|
new(
|
|
9
9
|
test_case: test_case,
|
|
10
10
|
status: :passed,
|
|
11
11
|
error: nil,
|
|
12
|
+
reason: nil,
|
|
12
13
|
duration: duration,
|
|
13
14
|
cleanup_errors: cleanup_errors
|
|
14
15
|
)
|
|
@@ -19,15 +20,39 @@ module Smartest
|
|
|
19
20
|
test_case: test_case,
|
|
20
21
|
status: :failed,
|
|
21
22
|
error: error,
|
|
23
|
+
reason: nil,
|
|
22
24
|
duration: duration,
|
|
23
25
|
cleanup_errors: cleanup_errors
|
|
24
26
|
)
|
|
25
27
|
end
|
|
26
28
|
|
|
27
|
-
def
|
|
29
|
+
def self.skipped(test_case:, reason:, duration:, cleanup_errors: [])
|
|
30
|
+
new(
|
|
31
|
+
test_case: test_case,
|
|
32
|
+
status: :skipped,
|
|
33
|
+
error: nil,
|
|
34
|
+
reason: reason,
|
|
35
|
+
duration: duration,
|
|
36
|
+
cleanup_errors: cleanup_errors
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.pending(test_case:, reason:, duration:, cleanup_errors: [])
|
|
41
|
+
new(
|
|
42
|
+
test_case: test_case,
|
|
43
|
+
status: :pending,
|
|
44
|
+
error: nil,
|
|
45
|
+
reason: reason,
|
|
46
|
+
duration: duration,
|
|
47
|
+
cleanup_errors: cleanup_errors
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def initialize(test_case:, status:, error:, reason:, duration:, cleanup_errors:)
|
|
28
52
|
@test_case = test_case
|
|
29
53
|
@status = status
|
|
30
54
|
@error = error
|
|
55
|
+
@reason = reason
|
|
31
56
|
@duration = duration
|
|
32
57
|
@cleanup_errors = cleanup_errors
|
|
33
58
|
end
|
|
@@ -39,5 +64,13 @@ module Smartest
|
|
|
39
64
|
def failed?
|
|
40
65
|
status == :failed
|
|
41
66
|
end
|
|
67
|
+
|
|
68
|
+
def skipped?
|
|
69
|
+
status == :skipped
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def pending?
|
|
73
|
+
status == :pending
|
|
74
|
+
end
|
|
42
75
|
end
|
|
43
76
|
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smartest
|
|
4
|
+
class TestRunState
|
|
5
|
+
attr_reader :pending_reason
|
|
6
|
+
|
|
7
|
+
def pending(reason = nil)
|
|
8
|
+
@pending_reason = StatusReason.normalize(reason)
|
|
9
|
+
nil
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def pending?
|
|
13
|
+
!@pending_reason.nil?
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/smartest/version.rb
CHANGED
data/lib/smartest.rb
CHANGED
|
@@ -12,6 +12,7 @@ require_relative "smartest/matcher_registry"
|
|
|
12
12
|
require_relative "smartest/fixture_set"
|
|
13
13
|
require_relative "smartest/suite"
|
|
14
14
|
require_relative "smartest/suite_run"
|
|
15
|
+
require_relative "smartest/test_run_state"
|
|
15
16
|
require_relative "smartest/test_run"
|
|
16
17
|
require_relative "smartest/hook_contexts"
|
|
17
18
|
require_relative "smartest/expectations"
|
data/smartest/smartest_test.rb
CHANGED
|
@@ -79,6 +79,92 @@ test("reports expectation failures") do
|
|
|
79
79
|
expect(output).to include("1 test, 0 passed, 1 failed")
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
+
test("skip marks a test as skipped and stops the body") do
|
|
83
|
+
events = []
|
|
84
|
+
suite = Smartest::Suite.new
|
|
85
|
+
suite.tests.add(
|
|
86
|
+
SmartestSelfTest.test_case(
|
|
87
|
+
"unsupported export",
|
|
88
|
+
proc do
|
|
89
|
+
events << :before_skip
|
|
90
|
+
skip "firefox is not supported"
|
|
91
|
+
events << :after_skip
|
|
92
|
+
end
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
status, output = SmartestSelfTest.run_suite(suite)
|
|
97
|
+
|
|
98
|
+
expect(status).to eq(0)
|
|
99
|
+
expect(events).to eq([:before_skip])
|
|
100
|
+
expect(output).to include("- unsupported export (skipped: firefox is not supported)")
|
|
101
|
+
expect(output).to include("1 test, 0 passed, 0 failed, 1 skipped")
|
|
102
|
+
expect(output).not_to include("Failures:")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
test("pending marks a failing test as pending") do
|
|
106
|
+
events = []
|
|
107
|
+
suite = Smartest::Suite.new
|
|
108
|
+
suite.tests.add(
|
|
109
|
+
SmartestSelfTest.test_case(
|
|
110
|
+
"bidi export",
|
|
111
|
+
proc do
|
|
112
|
+
pending "Not supported by WebDriver BiDi yet"
|
|
113
|
+
events << :after_pending
|
|
114
|
+
expect("actual").to eq("expected")
|
|
115
|
+
events << :after_failure
|
|
116
|
+
end
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
status, output = SmartestSelfTest.run_suite(suite)
|
|
121
|
+
|
|
122
|
+
expect(status).to eq(0)
|
|
123
|
+
expect(events).to eq([:after_pending])
|
|
124
|
+
expect(output).to include("* bidi export (pending: Not supported by WebDriver BiDi yet)")
|
|
125
|
+
expect(output).to include("1 test, 0 passed, 0 failed, 1 pending")
|
|
126
|
+
expect(output).not_to include("Failures:")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
test("pending fails when the test passes") do
|
|
130
|
+
suite = Smartest::Suite.new
|
|
131
|
+
suite.tests.add(
|
|
132
|
+
SmartestSelfTest.test_case(
|
|
133
|
+
"fixed bidi export",
|
|
134
|
+
proc do
|
|
135
|
+
pending "Not supported by WebDriver BiDi yet"
|
|
136
|
+
expect("actual").to eq("actual")
|
|
137
|
+
end
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
status, output = SmartestSelfTest.run_suite(suite)
|
|
142
|
+
|
|
143
|
+
expect(status).to eq(1)
|
|
144
|
+
expect(output).to include("expected pending test to fail, but it passed: Not supported by WebDriver BiDi yet")
|
|
145
|
+
expect(output).to include("1 test, 0 passed, 1 failed")
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
test("skip and pending are not available inside fixtures") do
|
|
149
|
+
%i[skip pending].each do |method_name|
|
|
150
|
+
fixture_class = Class.new(Smartest::Fixture) do
|
|
151
|
+
fixture :value do
|
|
152
|
+
__send__(method_name, "reason")
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
suite = Smartest::Suite.new
|
|
157
|
+
suite.fixture_classes.add(fixture_class)
|
|
158
|
+
suite.tests.add(SmartestSelfTest.test_case("#{method_name} fixture", proc { |value:| expect(value).to eq(:value) }))
|
|
159
|
+
|
|
160
|
+
status, output = SmartestSelfTest.run_suite(suite)
|
|
161
|
+
|
|
162
|
+
expect(status).to eq(1)
|
|
163
|
+
expect(output).to include("NoMethodError")
|
|
164
|
+
expect(output).to include(method_name.to_s)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
82
168
|
test("supports basic matchers") do
|
|
83
169
|
suite = Smartest::Suite.new
|
|
84
170
|
suite.tests.add(
|
|
@@ -86,6 +172,15 @@ test("supports basic matchers") do
|
|
|
86
172
|
"matchers",
|
|
87
173
|
proc do
|
|
88
174
|
expect([1, 2, 3]).to include(2)
|
|
175
|
+
expect("about:blank").to start_with("about:")
|
|
176
|
+
expect("https://cdn-b.test/assets/app.js").to start_with(
|
|
177
|
+
"https://cdn-a.test",
|
|
178
|
+
"https://cdn-b.test"
|
|
179
|
+
)
|
|
180
|
+
expect("screenshot.png").to end_with(".jpg", ".png")
|
|
181
|
+
expect("https://example.test").not_to start_with("about:")
|
|
182
|
+
expect("report.txt").not_to end_with(".png")
|
|
183
|
+
expect(Object.new).not_to start_with("prefix")
|
|
89
184
|
expect(nil).to be_nil
|
|
90
185
|
expect("value").not_to be_nil
|
|
91
186
|
expect { raise ArgumentError, "bad" }.to raise_error(ArgumentError)
|
|
@@ -98,6 +193,93 @@ test("supports basic matchers") do
|
|
|
98
193
|
expect(status).to eq(0)
|
|
99
194
|
end
|
|
100
195
|
|
|
196
|
+
test("reports start_with and end_with matcher failures") do
|
|
197
|
+
suite = Smartest::Suite.new
|
|
198
|
+
suite.tests.add(
|
|
199
|
+
SmartestSelfTest.test_case(
|
|
200
|
+
"bad prefix",
|
|
201
|
+
proc { expect("https://example.test/path").to start_with("about:") }
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
suite.tests.add(
|
|
205
|
+
SmartestSelfTest.test_case(
|
|
206
|
+
"bad suffix",
|
|
207
|
+
proc { expect("asset.gif").to end_with(".jpg", ".png") }
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
suite.tests.add(
|
|
211
|
+
SmartestSelfTest.test_case(
|
|
212
|
+
"bad negated suffix",
|
|
213
|
+
proc { expect("screenshot.png").not_to end_with(".png") }
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
status, output = SmartestSelfTest.run_suite(suite)
|
|
218
|
+
|
|
219
|
+
expect(status).to eq(1)
|
|
220
|
+
expect(output).to include('expected "https://example.test/path" to start with "about:"')
|
|
221
|
+
expect(output).to include('expected "asset.gif" to end with ".jpg" or ".png"')
|
|
222
|
+
expect(output).to include('expected "screenshot.png" not to end with ".png"')
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
test("supports change matcher for block expectations") do
|
|
226
|
+
value = 0
|
|
227
|
+
action_calls = 0
|
|
228
|
+
|
|
229
|
+
expect {
|
|
230
|
+
action_calls += 1
|
|
231
|
+
value += 2
|
|
232
|
+
}.to change { value }.from(0).to(2).by(2)
|
|
233
|
+
|
|
234
|
+
expect(action_calls).to eq(1)
|
|
235
|
+
expect { value }.not_to change { value }
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
test("reports change matcher failures with before and after values") do
|
|
239
|
+
value = 0
|
|
240
|
+
|
|
241
|
+
error = SmartestSelfTest.capture_error(Smartest::AssertionFailed) do
|
|
242
|
+
expect { value += 1 }.to change { value }.from(0).to(2).by(2)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
expect(error.message).to include("0 before")
|
|
246
|
+
expect(error.message).to include("1 after")
|
|
247
|
+
expect(error.message).to include("to(2)")
|
|
248
|
+
expect(error.message).to include("by(2)")
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
test("fails negated change matcher when the value changes") do
|
|
252
|
+
value = 0
|
|
253
|
+
|
|
254
|
+
error = SmartestSelfTest.capture_error(Smartest::AssertionFailed) do
|
|
255
|
+
expect { value += 1 }.not_to change { value }
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
expect(error.message).to include("expected value not to change")
|
|
259
|
+
expect(error.message).to include("0 before")
|
|
260
|
+
expect(error.message).to include("1 after")
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
test("requires change matcher value and action blocks") do
|
|
264
|
+
error = SmartestSelfTest.capture_error(ArgumentError) do
|
|
265
|
+
change
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
expect(error.message).to eq("change requires a block")
|
|
269
|
+
|
|
270
|
+
error = SmartestSelfTest.capture_error(ArgumentError) do
|
|
271
|
+
change(:value) { :other }
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
expect(error.message).to include("change does not support arguments")
|
|
275
|
+
|
|
276
|
+
error = SmartestSelfTest.capture_error(Smartest::AssertionFailed) do
|
|
277
|
+
expect(:value).to change { :other }
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
expect(error.message).to include("expected a block to change value")
|
|
281
|
+
end
|
|
282
|
+
|
|
101
283
|
test("registers matcher modules for suite execution contexts") do
|
|
102
284
|
status_matcher = Class.new do
|
|
103
285
|
def initialize(expected)
|
|
@@ -563,6 +745,81 @@ test("around_test must call test.run") do
|
|
|
563
745
|
expect(output).to include("Smartest::AroundTestRunError: around_test hook did not call test.run")
|
|
564
746
|
end
|
|
565
747
|
|
|
748
|
+
test("around_test can skip before test.run") do
|
|
749
|
+
events = []
|
|
750
|
+
suite = Smartest::Suite.new
|
|
751
|
+
suite.tests.add(
|
|
752
|
+
Smartest::TestCase.new(
|
|
753
|
+
name: "skipped by hook",
|
|
754
|
+
metadata: {},
|
|
755
|
+
location: caller_locations(1, 1).first,
|
|
756
|
+
around_test_hooks: [
|
|
757
|
+
proc do |test_run|
|
|
758
|
+
events << :around_before
|
|
759
|
+
skip "browser is not supported"
|
|
760
|
+
test_run.run
|
|
761
|
+
events << :around_after
|
|
762
|
+
end
|
|
763
|
+
],
|
|
764
|
+
block: proc { events << :test_body }
|
|
765
|
+
)
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
status, output = SmartestSelfTest.run_suite(suite)
|
|
769
|
+
|
|
770
|
+
expect(status).to eq(0)
|
|
771
|
+
expect(events).to eq([:around_before])
|
|
772
|
+
expect(output).to include("- skipped by hook (skipped: browser is not supported)")
|
|
773
|
+
expect(output).to include("1 test, 0 passed, 0 failed, 1 skipped")
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
test("around_test can mark a failing test as pending") do
|
|
777
|
+
suite = Smartest::Suite.new
|
|
778
|
+
suite.tests.add(
|
|
779
|
+
Smartest::TestCase.new(
|
|
780
|
+
name: "pending by hook",
|
|
781
|
+
metadata: {},
|
|
782
|
+
location: caller_locations(1, 1).first,
|
|
783
|
+
around_test_hooks: [
|
|
784
|
+
proc do |test_run|
|
|
785
|
+
pending "driver bug"
|
|
786
|
+
test_run.run
|
|
787
|
+
end
|
|
788
|
+
],
|
|
789
|
+
block: proc { expect("actual").to eq("expected") }
|
|
790
|
+
)
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
status, output = SmartestSelfTest.run_suite(suite)
|
|
794
|
+
|
|
795
|
+
expect(status).to eq(0)
|
|
796
|
+
expect(output).to include("* pending by hook (pending: driver bug)")
|
|
797
|
+
expect(output).to include("1 test, 0 passed, 0 failed, 1 pending")
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
test("pending around_test hooks must still call test.run") do
|
|
801
|
+
suite = Smartest::Suite.new
|
|
802
|
+
suite.tests.add(
|
|
803
|
+
Smartest::TestCase.new(
|
|
804
|
+
name: "pending hook missing run",
|
|
805
|
+
metadata: {},
|
|
806
|
+
location: caller_locations(1, 1).first,
|
|
807
|
+
around_test_hooks: [
|
|
808
|
+
proc do |_test_run|
|
|
809
|
+
pending "driver bug"
|
|
810
|
+
end
|
|
811
|
+
],
|
|
812
|
+
block: proc { expect(true).to eq(false) }
|
|
813
|
+
)
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
status, output = SmartestSelfTest.run_suite(suite)
|
|
817
|
+
|
|
818
|
+
expect(status).to eq(1)
|
|
819
|
+
expect(output).to include("Smartest::AroundTestRunError: around_test hook did not call test.run")
|
|
820
|
+
expect(output).to include("1 test, 0 passed, 1 failed")
|
|
821
|
+
end
|
|
822
|
+
|
|
566
823
|
test("use_fixture and use_matcher are only available inside hooks") do
|
|
567
824
|
{
|
|
568
825
|
"use_fixture Object" => "use_fixture",
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: smartest
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.0.
|
|
4
|
+
version: 0.2.0.alpha3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yusuke Iwaki
|
|
@@ -65,6 +65,7 @@ files:
|
|
|
65
65
|
- lib/smartest/test_registry.rb
|
|
66
66
|
- lib/smartest/test_result.rb
|
|
67
67
|
- lib/smartest/test_run.rb
|
|
68
|
+
- lib/smartest/test_run_state.rb
|
|
68
69
|
- lib/smartest/version.rb
|
|
69
70
|
- smartest.gemspec
|
|
70
71
|
- smartest/smartest_test.rb
|