smartest 0.1.0 → 0.2.0.alpha2
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/DEVELOPMENT.md +102 -10
- data/README.md +161 -34
- data/SMARTEST_DESIGN.md +137 -18
- data/lib/smartest/dsl.rb +13 -6
- data/lib/smartest/errors.rb +40 -0
- data/lib/smartest/execution_context.rb +14 -0
- data/lib/smartest/fixture.rb +6 -0
- data/lib/smartest/hook_contexts.rb +60 -0
- data/lib/smartest/init_generator.rb +4 -1
- data/lib/smartest/reporter.rb +40 -3
- data/lib/smartest/runner.rb +97 -15
- data/lib/smartest/suite.rb +34 -1
- data/lib/smartest/suite_run.rb +24 -0
- data/lib/smartest/test_case.rb +3 -2
- data/lib/smartest/test_result.rb +35 -2
- data/lib/smartest/test_run.rb +58 -0
- data/lib/smartest/test_run_state.rb +16 -0
- data/lib/smartest/version.rb +1 -1
- data/lib/smartest.rb +4 -0
- data/smartest/smartest_test.rb +470 -4
- metadata +7 -3
data/SMARTEST_DESIGN.md
CHANGED
|
@@ -50,7 +50,10 @@ class WebFixture < Smartest::Fixture
|
|
|
50
50
|
end
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
around_suite do |suite|
|
|
54
|
+
use_fixture WebFixture
|
|
55
|
+
suite.run
|
|
56
|
+
end
|
|
54
57
|
```
|
|
55
58
|
|
|
56
59
|
The core decision is:
|
|
@@ -294,7 +297,10 @@ context.instance_exec(**fixtures, &block)
|
|
|
294
297
|
|
|
295
298
|
This keeps the top-level DSL small.
|
|
296
299
|
|
|
297
|
-
Only `test`, `
|
|
300
|
+
Only `test`, `around_suite`, and `around_test` should be globally available when
|
|
301
|
+
using `smartest/autorun`. `use_fixture` and `use_matcher` are available only
|
|
302
|
+
inside hook execution contexts. `skip` and `pending` are available only inside
|
|
303
|
+
test bodies and `around_test` hook execution contexts.
|
|
298
304
|
|
|
299
305
|
## Core architecture
|
|
300
306
|
|
|
@@ -543,8 +549,11 @@ class AdminFixture < Smartest::Fixture
|
|
|
543
549
|
end
|
|
544
550
|
end
|
|
545
551
|
|
|
546
|
-
|
|
547
|
-
use_fixture
|
|
552
|
+
around_suite do |suite|
|
|
553
|
+
use_fixture UserFixture
|
|
554
|
+
use_fixture AdminFixture
|
|
555
|
+
suite.run
|
|
556
|
+
end
|
|
548
557
|
```
|
|
549
558
|
|
|
550
559
|
Error:
|
|
@@ -683,6 +692,8 @@ end
|
|
|
683
692
|
Fixture blocks can call private methods because they execute with `instance_exec`.
|
|
684
693
|
|
|
685
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.
|
|
686
697
|
|
|
687
698
|
This is useful for integration helpers.
|
|
688
699
|
|
|
@@ -823,28 +834,38 @@ Care must be taken not to run twice if both CLI and autorun are used.
|
|
|
823
834
|
|
|
824
835
|
## Exit status
|
|
825
836
|
|
|
826
|
-
- all tests passed: `0`
|
|
837
|
+
- all tests passed, skipped, or pending as expected: `0`
|
|
827
838
|
- any test failed: `1`
|
|
839
|
+
- pending test unexpectedly passed: `1`
|
|
828
840
|
- configuration/load error: `1`
|
|
829
841
|
- interrupted: re-raise or exit non-zero
|
|
830
842
|
|
|
831
843
|
## Metadata
|
|
832
844
|
|
|
833
|
-
`test`
|
|
845
|
+
`test` accepts metadata and stores it on `TestCase`, but metadata does not drive
|
|
846
|
+
runner behavior in the MVP:
|
|
834
847
|
|
|
835
848
|
```ruby
|
|
836
|
-
test("name", skip: true) do
|
|
837
|
-
end
|
|
838
|
-
|
|
839
849
|
test("name", tags: [:db]) do
|
|
840
850
|
end
|
|
841
851
|
```
|
|
842
852
|
|
|
843
|
-
|
|
853
|
+
Runtime skipping and pending behavior are method calls inside the test body or
|
|
854
|
+
`around_test`, not metadata:
|
|
855
|
+
|
|
856
|
+
```ruby
|
|
857
|
+
test("name") do
|
|
858
|
+
skip "reason" if runtime_condition?
|
|
859
|
+
end
|
|
860
|
+
|
|
861
|
+
test("name") do
|
|
862
|
+
pending "reason" if expected_to_fail?
|
|
863
|
+
expect(actual).to eq(expected)
|
|
864
|
+
end
|
|
865
|
+
```
|
|
844
866
|
|
|
845
867
|
Useful metadata later:
|
|
846
868
|
|
|
847
|
-
- `skip: true`
|
|
848
869
|
- `only: true`
|
|
849
870
|
- `tags: [:db]`
|
|
850
871
|
- `timeout: 5`
|
|
@@ -853,7 +874,70 @@ Useful metadata later:
|
|
|
853
874
|
|
|
854
875
|
Hooks are separate from fixtures.
|
|
855
876
|
|
|
856
|
-
|
|
877
|
+
Supported suite API:
|
|
878
|
+
|
|
879
|
+
```ruby
|
|
880
|
+
around_suite do |suite|
|
|
881
|
+
Async do
|
|
882
|
+
suite.run
|
|
883
|
+
end
|
|
884
|
+
end
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
`around_suite` wraps the full suite body, including all tests and suite fixture
|
|
888
|
+
cleanup. The hook receives a run target and must call `suite.run` exactly once.
|
|
889
|
+
Multiple hooks compose in registration order, with the first hook as the
|
|
890
|
+
outermost wrapper.
|
|
891
|
+
|
|
892
|
+
Supported per-test API:
|
|
893
|
+
|
|
894
|
+
```ruby
|
|
895
|
+
around_test do |test|
|
|
896
|
+
SomeAutoCloseResource.new do
|
|
897
|
+
test.run
|
|
898
|
+
end
|
|
899
|
+
end
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
When `around_test` is written directly in a test file, Smartest treats it as
|
|
903
|
+
file-scoped by snapshotting the file's current around-test hooks into each
|
|
904
|
+
`TestCase` at test registration time. Hooks defined later in the file apply only
|
|
905
|
+
to later tests.
|
|
906
|
+
|
|
907
|
+
`around_test` can also be registered inside `around_suite`. In that case the hook
|
|
908
|
+
is suite-wide and applies when `suite.run` executes:
|
|
909
|
+
|
|
910
|
+
```ruby
|
|
911
|
+
around_suite do |suite|
|
|
912
|
+
around_test do |test|
|
|
913
|
+
TestServer.run do
|
|
914
|
+
test.run
|
|
915
|
+
end
|
|
916
|
+
end
|
|
917
|
+
|
|
918
|
+
suite.run
|
|
919
|
+
end
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
`use_fixture` and `use_matcher` are not top-level DSL methods. They are available
|
|
923
|
+
inside `around_suite` and `around_test` contexts:
|
|
924
|
+
|
|
925
|
+
```ruby
|
|
926
|
+
around_test do |test|
|
|
927
|
+
use_fixture LocalFixture
|
|
928
|
+
use_matcher LocalMatcher
|
|
929
|
+
test.run
|
|
930
|
+
end
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
For `around_test`, those registrations are test-run local and must happen before
|
|
934
|
+
`test.run`.
|
|
935
|
+
|
|
936
|
+
Fixture classes registered from `around_test` must not define `suite_fixture`.
|
|
937
|
+
Suite-scoped fixtures need suite-level cache and cleanup ownership, so classes
|
|
938
|
+
with suite-scoped fixtures must be registered from `around_suite`.
|
|
939
|
+
|
|
940
|
+
Potential simpler per-test API:
|
|
857
941
|
|
|
858
942
|
```ruby
|
|
859
943
|
before do
|
|
@@ -889,10 +973,29 @@ fixture cleanup
|
|
|
889
973
|
|
|
890
974
|
This needs a final decision later.
|
|
891
975
|
|
|
892
|
-
For MVP, hooks can be omitted.
|
|
893
|
-
|
|
894
976
|
Fixture cleanup already handles resource-specific teardown.
|
|
895
977
|
|
|
978
|
+
### Around-test parallelism note
|
|
979
|
+
|
|
980
|
+
The file-scoped `around_test` design is intended to remain compatible with
|
|
981
|
+
future parallel execution if it is hardened in these directions:
|
|
982
|
+
|
|
983
|
+
- keep registration protected by a `Mutex` when tests may be loaded from
|
|
984
|
+
multiple threads
|
|
985
|
+
- use copy-on-write arrays for `around_test` hooks by file
|
|
986
|
+
- snapshot the current file-local hook stack into each `TestCase` at registration
|
|
987
|
+
time
|
|
988
|
+
- treat `TestCase` as immutable after registration
|
|
989
|
+
- expose `use_fixture` and `use_matcher` through a per-run `TestRun` object, not
|
|
990
|
+
by mutating the global suite registry during test execution
|
|
991
|
+
- snapshot the suite's fixture classes and matcher modules before a test starts
|
|
992
|
+
- keep test-run fixture and matcher additions local to that run
|
|
993
|
+
|
|
994
|
+
That shape lets multiple tests execute concurrently by reading immutable
|
|
995
|
+
`TestCase` state and using per-test fixture/matcher registries. The current
|
|
996
|
+
implementation can be simpler, but it should not rely on refinements or global
|
|
997
|
+
method rewriting for file-local behavior.
|
|
998
|
+
|
|
896
999
|
## Scoping
|
|
897
1000
|
|
|
898
1001
|
Fixtures are test-scoped by default.
|
|
@@ -1073,6 +1176,11 @@ require "smartest/autorun"
|
|
|
1073
1176
|
Dir[File.join(__dir__, "fixtures", "**", "*.rb")].sort.each do |fixture_file|
|
|
1074
1177
|
require fixture_file
|
|
1075
1178
|
end
|
|
1179
|
+
|
|
1180
|
+
around_suite do |suite|
|
|
1181
|
+
use_fixture AppFixture
|
|
1182
|
+
suite.run
|
|
1183
|
+
end
|
|
1076
1184
|
```
|
|
1077
1185
|
|
|
1078
1186
|
```ruby
|
|
@@ -1098,18 +1206,30 @@ end
|
|
|
1098
1206
|
# smartest/example_test.rb
|
|
1099
1207
|
require "test_helper"
|
|
1100
1208
|
|
|
1101
|
-
use_fixture AppFixture
|
|
1102
|
-
|
|
1103
1209
|
test("GET /health") do |client:|
|
|
1104
1210
|
expect(client.get("/health").status).to eq(200)
|
|
1105
1211
|
end
|
|
1106
1212
|
```
|
|
1107
1213
|
|
|
1214
|
+
```ruby
|
|
1215
|
+
test("PDF export") do |browser:|
|
|
1216
|
+
skip "firefox is not supported" if browser.firefox?
|
|
1217
|
+
|
|
1218
|
+
export_pdf(browser)
|
|
1219
|
+
end
|
|
1220
|
+
|
|
1221
|
+
test("PDF export over BiDi") do |browser:|
|
|
1222
|
+
pending "Not supported by WebDriver BiDi yet" if browser.bidi?
|
|
1223
|
+
|
|
1224
|
+
export_pdf(browser)
|
|
1225
|
+
expect(browser.downloads).not_to be_empty
|
|
1226
|
+
end
|
|
1227
|
+
```
|
|
1228
|
+
|
|
1108
1229
|
## Future ideas
|
|
1109
1230
|
|
|
1110
1231
|
Possible future features:
|
|
1111
1232
|
|
|
1112
|
-
- `skip`
|
|
1113
1233
|
- `only`
|
|
1114
1234
|
- tags
|
|
1115
1235
|
- custom reporters
|
|
@@ -1117,7 +1237,6 @@ Possible future features:
|
|
|
1117
1237
|
- richer matchers
|
|
1118
1238
|
- block expectations
|
|
1119
1239
|
- `raise_error`
|
|
1120
|
-
- hooks
|
|
1121
1240
|
- file-scoped fixtures
|
|
1122
1241
|
- parallel execution
|
|
1123
1242
|
- watch mode
|
data/lib/smartest/dsl.rb
CHANGED
|
@@ -3,24 +3,31 @@
|
|
|
3
3
|
module Smartest
|
|
4
4
|
module DSL
|
|
5
5
|
def test(name, **metadata, &block)
|
|
6
|
+
location = caller_locations(1, 1).first
|
|
7
|
+
|
|
6
8
|
Smartest.suite.tests.add(
|
|
7
9
|
TestCase.new(
|
|
8
10
|
name: name,
|
|
9
11
|
metadata: metadata,
|
|
10
12
|
block: block,
|
|
11
|
-
location:
|
|
13
|
+
location: location,
|
|
14
|
+
around_test_hooks: Smartest.suite.around_test_hooks_for(location)
|
|
12
15
|
)
|
|
13
16
|
)
|
|
14
17
|
end
|
|
15
18
|
|
|
16
|
-
def
|
|
17
|
-
|
|
19
|
+
def around_suite(&block)
|
|
20
|
+
raise ArgumentError, "around_suite block is required" unless block
|
|
21
|
+
|
|
22
|
+
Smartest.suite.around_suite_hooks << block
|
|
18
23
|
end
|
|
19
24
|
|
|
20
|
-
def
|
|
21
|
-
|
|
25
|
+
def around_test(&block)
|
|
26
|
+
raise ArgumentError, "around_test block is required" unless block
|
|
27
|
+
|
|
28
|
+
Smartest.suite.add_around_test_hook(caller_locations(1, 1).first, block)
|
|
22
29
|
end
|
|
23
30
|
|
|
24
|
-
private :test, :
|
|
31
|
+
private :test, :around_suite, :around_test
|
|
25
32
|
end
|
|
26
33
|
end
|
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
|
|
@@ -48,5 +57,36 @@ module Smartest
|
|
|
48
57
|
|
|
49
58
|
class InvalidFixtureParameterError < Error; end
|
|
50
59
|
|
|
60
|
+
class AroundSuiteRunError < Error; end
|
|
61
|
+
|
|
62
|
+
class AroundTestFixtureScopeError < Error
|
|
63
|
+
def initialize(fixture_class, fixture_names)
|
|
64
|
+
class_name = fixture_class.name || fixture_class.inspect
|
|
65
|
+
names = fixture_names.map { |fixture_name| ":#{fixture_name}" }.join(", ")
|
|
66
|
+
|
|
67
|
+
super(
|
|
68
|
+
"#{class_name} cannot be registered from around_test because it defines suite-scoped fixtures: #{names}. " \
|
|
69
|
+
"Register fixture classes with suite_fixture from around_suite instead."
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class AroundTestRunError < Error; end
|
|
75
|
+
|
|
51
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
|
|
52
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
|
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
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smartest
|
|
4
|
+
class AroundSuiteContext
|
|
5
|
+
def initialize(suite)
|
|
6
|
+
@suite = suite
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def call(hook, suite_run)
|
|
10
|
+
@suite.around_suite_hook do
|
|
11
|
+
instance_exec(suite_run, &hook)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def use_fixture(klass)
|
|
18
|
+
@suite.fixture_classes.add(klass)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def use_matcher(matcher_module)
|
|
22
|
+
@suite.matcher_modules.add(matcher_module)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def around_test(&block)
|
|
26
|
+
raise ArgumentError, "around_test block is required" unless block
|
|
27
|
+
|
|
28
|
+
@suite.add_around_test_hook(caller_locations(1, 1).first, block)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class AroundTestContext
|
|
33
|
+
def initialize(test_run, run_state:)
|
|
34
|
+
@test_run = test_run
|
|
35
|
+
@run_state = run_state
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def call(hook, run_target = @test_run)
|
|
39
|
+
instance_exec(run_target, &hook)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def use_fixture(klass)
|
|
45
|
+
@test_run.add_fixture_class(klass)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def use_matcher(matcher_module)
|
|
49
|
+
@test_run.add_matcher_module(matcher_module)
|
|
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
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -18,7 +18,10 @@ module Smartest
|
|
|
18
18
|
require matcher_file
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
around_suite do |suite|
|
|
22
|
+
use_matcher PredicateMatcher
|
|
23
|
+
suite.run
|
|
24
|
+
end
|
|
22
25
|
RUBY
|
|
23
26
|
"smartest/matchers/predicate_matcher.rb" => <<~RUBY,
|
|
24
27
|
# frozen_string_literal: true
|
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,18 +17,26 @@ 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
|
-
def finish(results, suite_cleanup_errors: [])
|
|
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?
|
|
29
|
+
report_suite_errors(suite_errors) if suite_errors.any?
|
|
26
30
|
report_suite_cleanup_errors(suite_cleanup_errors) if suite_cleanup_errors.any?
|
|
27
31
|
|
|
28
32
|
@io.puts
|
|
29
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?
|
|
36
|
+
if suite_errors.any?
|
|
37
|
+
suite_label = suite_errors.count == 1 ? "suite failure" : "suite failures"
|
|
38
|
+
summary = "#{summary}, #{suite_errors.count} #{suite_label}"
|
|
39
|
+
end
|
|
30
40
|
if suite_cleanup_errors.any?
|
|
31
41
|
cleanup_label = suite_cleanup_errors.count == 1 ? "suite cleanup" : "suite cleanups"
|
|
32
42
|
summary = "#{summary}, #{suite_cleanup_errors.count} #{cleanup_label} failed"
|
|
@@ -36,6 +46,21 @@ module Smartest
|
|
|
36
46
|
|
|
37
47
|
private
|
|
38
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
|
+
|
|
39
64
|
def report_failures(failures)
|
|
40
65
|
@io.puts
|
|
41
66
|
@io.puts "Failures:"
|
|
@@ -50,6 +75,18 @@ module Smartest
|
|
|
50
75
|
end
|
|
51
76
|
end
|
|
52
77
|
|
|
78
|
+
def report_suite_errors(errors)
|
|
79
|
+
@io.puts
|
|
80
|
+
@io.puts "Suite failures:"
|
|
81
|
+
@io.puts
|
|
82
|
+
|
|
83
|
+
errors.each_with_index do |error, index|
|
|
84
|
+
@io.puts "#{index + 1}) suite"
|
|
85
|
+
report_error(error)
|
|
86
|
+
@io.puts
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
53
90
|
def report_suite_cleanup_errors(errors)
|
|
54
91
|
@io.puts
|
|
55
92
|
@io.puts "Suite cleanup failures:"
|
data/lib/smartest/runner.rb
CHANGED
|
@@ -11,10 +11,33 @@ module Smartest
|
|
|
11
11
|
def run
|
|
12
12
|
results = []
|
|
13
13
|
suite_cleanup_errors = []
|
|
14
|
+
suite_errors = []
|
|
14
15
|
@suite_fixture_set = nil
|
|
15
16
|
|
|
16
17
|
@reporter.start(@tests.count)
|
|
17
18
|
|
|
19
|
+
begin
|
|
20
|
+
run_around_suite_hooks(@suite.around_suite_hooks.dup) do
|
|
21
|
+
run_tests(results, suite_cleanup_errors)
|
|
22
|
+
end
|
|
23
|
+
rescue Exception => error
|
|
24
|
+
raise if Smartest.fatal_exception?(error)
|
|
25
|
+
|
|
26
|
+
suite_errors << error
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
@reporter.finish(
|
|
30
|
+
results,
|
|
31
|
+
suite_cleanup_errors: suite_cleanup_errors,
|
|
32
|
+
suite_errors: suite_errors
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
results.any?(&:failed?) || suite_cleanup_errors.any? || suite_errors.any? ? 1 : 0
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def run_tests(results, suite_cleanup_errors)
|
|
18
41
|
begin
|
|
19
42
|
@tests.each do |test_case|
|
|
20
43
|
result = run_one(test_case)
|
|
@@ -22,38 +45,63 @@ module Smartest
|
|
|
22
45
|
@reporter.record(result)
|
|
23
46
|
end
|
|
24
47
|
ensure
|
|
25
|
-
suite_cleanup_errors
|
|
48
|
+
suite_cleanup_errors.concat(@suite_fixture_set.run_cleanups) if @suite_fixture_set
|
|
26
49
|
@suite_fixture_set = nil
|
|
27
50
|
end
|
|
51
|
+
end
|
|
28
52
|
|
|
29
|
-
|
|
53
|
+
def run_around_suite_hooks(hooks, index = 0, &block)
|
|
54
|
+
return yield if index >= hooks.length
|
|
30
55
|
|
|
31
|
-
|
|
32
|
-
|
|
56
|
+
hook = hooks[index]
|
|
57
|
+
suite_run = SuiteRun.new do
|
|
58
|
+
run_around_suite_hooks(hooks, index + 1, &block)
|
|
59
|
+
end
|
|
33
60
|
|
|
34
|
-
|
|
61
|
+
AroundSuiteContext.new(@suite).call(hook, suite_run)
|
|
62
|
+
raise AroundSuiteRunError, "around_suite hook did not call suite.run" unless suite_run.ran?
|
|
63
|
+
|
|
64
|
+
suite_run.result
|
|
65
|
+
end
|
|
35
66
|
|
|
36
67
|
def run_one(test_case)
|
|
37
68
|
started_at = now
|
|
38
|
-
context = build_context
|
|
39
|
-
fixture_set = nil
|
|
40
69
|
error = nil
|
|
70
|
+
skipped = nil
|
|
41
71
|
cleanup_errors = []
|
|
72
|
+
run_state = TestRunState.new
|
|
73
|
+
test_run = TestRun.new(
|
|
74
|
+
fixture_classes: @suite.fixture_classes,
|
|
75
|
+
matcher_modules: @suite.matcher_modules
|
|
76
|
+
) do |fixture_classes:, matcher_modules:|
|
|
77
|
+
run_test_body(test_case, fixture_classes, matcher_modules, run_state, cleanup_errors)
|
|
78
|
+
end
|
|
42
79
|
|
|
43
80
|
begin
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
47
84
|
rescue Exception => rescued_error
|
|
48
85
|
raise if Smartest.fatal_exception?(rescued_error)
|
|
49
86
|
|
|
50
87
|
error = rescued_error
|
|
51
|
-
ensure
|
|
52
|
-
cleanup_errors = fixture_set.run_cleanups if fixture_set
|
|
53
88
|
end
|
|
54
89
|
|
|
55
90
|
duration = now - started_at
|
|
56
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
|
+
|
|
57
105
|
if error || cleanup_errors.any?
|
|
58
106
|
TestResult.failed(
|
|
59
107
|
test_case: test_case,
|
|
@@ -66,6 +114,36 @@ module Smartest
|
|
|
66
114
|
end
|
|
67
115
|
end
|
|
68
116
|
|
|
117
|
+
def run_test_body(test_case, fixture_classes, matcher_modules, run_state, cleanup_errors)
|
|
118
|
+
context = build_context(matcher_modules, run_state)
|
|
119
|
+
fixture_set = nil
|
|
120
|
+
|
|
121
|
+
begin
|
|
122
|
+
fixture_set = FixtureSet.new(fixture_classes, context: context, parent: suite_fixture_set)
|
|
123
|
+
fixtures = fixture_set.resolve_keywords(test_case.fixture_names)
|
|
124
|
+
context.instance_exec(**fixtures, &test_case.block)
|
|
125
|
+
ensure
|
|
126
|
+
cleanup_errors.concat(fixture_set.run_cleanups) if fixture_set
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def run_around_test_hooks(hooks, test_run, run_state, index = 0)
|
|
131
|
+
return test_run.run if index >= hooks.length
|
|
132
|
+
|
|
133
|
+
hook = hooks[index]
|
|
134
|
+
next_run = TestRun.new(
|
|
135
|
+
fixture_classes: [],
|
|
136
|
+
matcher_modules: []
|
|
137
|
+
) do |**_keywords|
|
|
138
|
+
run_around_test_hooks(hooks, test_run, run_state, index + 1)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
AroundTestContext.new(test_run, run_state: run_state).call(hook, next_run)
|
|
142
|
+
raise AroundTestRunError, "around_test hook did not call test.run" unless next_run.ran?
|
|
143
|
+
|
|
144
|
+
next_run.result
|
|
145
|
+
end
|
|
146
|
+
|
|
69
147
|
def suite_fixture_set
|
|
70
148
|
@suite_fixture_set ||= FixtureSet.new(
|
|
71
149
|
@suite.fixture_classes,
|
|
@@ -74,12 +152,16 @@ module Smartest
|
|
|
74
152
|
)
|
|
75
153
|
end
|
|
76
154
|
|
|
77
|
-
def build_context
|
|
78
|
-
ExecutionContext.new.tap do |context|
|
|
79
|
-
|
|
155
|
+
def build_context(matcher_modules = @suite.matcher_modules, run_state = TestRunState.new)
|
|
156
|
+
ExecutionContext.new(run_state: run_state).tap do |context|
|
|
157
|
+
matcher_modules.each { |matcher_module| context.extend(matcher_module) }
|
|
80
158
|
end
|
|
81
159
|
end
|
|
82
160
|
|
|
161
|
+
def around_test_protocol_error?(error)
|
|
162
|
+
error.is_a?(AroundTestRunError)
|
|
163
|
+
end
|
|
164
|
+
|
|
83
165
|
def now
|
|
84
166
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
85
167
|
end
|