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.
data/SMARTEST_DESIGN.md CHANGED
@@ -50,7 +50,10 @@ class WebFixture < Smartest::Fixture
50
50
  end
51
51
  end
52
52
 
53
- use_fixture WebFixture
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`, `fixture`, `use_fixture`, and `use_matcher` need to be globally available when using `smartest/autorun`.
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
- use_fixture UserFixture
547
- use_fixture AdminFixture
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` should accept metadata:
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
- MVP can store metadata without implementing all behavior.
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
- Potential API:
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: caller_locations(1, 1).first
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 use_fixture(klass)
17
- Smartest.suite.fixture_classes.add(klass)
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 use_matcher(matcher_module)
21
- Smartest.suite.matcher_modules.add(matcher_module)
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, :use_fixture, :use_matcher
31
+ private :test, :around_suite, :around_test
25
32
  end
26
33
  end
@@ -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
@@ -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
- use_matcher PredicateMatcher
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
@@ -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
- mark = result.passed? ? PASS_MARK : FAIL_MARK
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:"
@@ -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 = @suite_fixture_set.run_cleanups if @suite_fixture_set
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
- @reporter.finish(results, suite_cleanup_errors: suite_cleanup_errors)
53
+ def run_around_suite_hooks(hooks, index = 0, &block)
54
+ return yield if index >= hooks.length
30
55
 
31
- results.any?(&:failed?) || suite_cleanup_errors.any? ? 1 : 0
32
- end
56
+ hook = hooks[index]
57
+ suite_run = SuiteRun.new do
58
+ run_around_suite_hooks(hooks, index + 1, &block)
59
+ end
33
60
 
34
- private
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
- fixture_set = FixtureSet.new(@suite.fixture_classes, context: context, parent: suite_fixture_set)
45
- fixtures = fixture_set.resolve_keywords(test_case.fixture_names)
46
- context.instance_exec(**fixtures, &test_case.block)
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
- @suite.matcher_modules.each { |matcher_module| context.extend(matcher_module) }
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