smartest 0.3.3.alpha4 → 0.5.0.alpha1

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
@@ -30,7 +30,7 @@ Fixture definitions:
30
30
  class WebFixture < Smartest::Fixture
31
31
  fixture :server do
32
32
  server = TestServer.start
33
- cleanup { server.stop }
33
+ on_teardown { server.stop }
34
34
 
35
35
  server.wait_until_ready!
36
36
  server
@@ -195,12 +195,12 @@ server teardown
195
195
 
196
196
  This is especially complex when fixtures depend on other fixtures.
197
197
 
198
- Smartest instead chooses `cleanup` for the MVP:
198
+ Smartest instead chooses `on_teardown` for the MVP:
199
199
 
200
200
  ```ruby
201
201
  fixture :server do
202
202
  server = TestServer.start
203
- cleanup { server.stop }
203
+ on_teardown { server.stop }
204
204
 
205
205
  server.wait_until_ready!
206
206
  server
@@ -214,7 +214,7 @@ This has several advantages:
214
214
  - teardown is local to the fixture that owns the resource
215
215
  - implementation is simple
216
216
  - fixture dependencies remain ordinary recursive resolution
217
- - cleanup runs in `ensure`
217
+ - teardown runs in `ensure`
218
218
 
219
219
  Not every fixture needs teardown, so teardown should not shape the entire fixture API.
220
220
 
@@ -236,12 +236,12 @@ fixture :article do |user:|
236
236
  end
237
237
  ```
238
238
 
239
- A fixture may register cleanup.
239
+ A fixture may register teardown.
240
240
 
241
241
  ```ruby
242
242
  fixture :temp_dir do
243
243
  dir = Dir.mktmpdir
244
- cleanup { FileUtils.rm_rf(dir) }
244
+ on_teardown { FileUtils.rm_rf(dir) }
245
245
  dir
246
246
  end
247
247
  ```
@@ -316,7 +316,7 @@ Runner
316
316
  ├── creates FixtureSet
317
317
  ├── resolves keyword fixtures
318
318
  ├── executes test body
319
- ├── runs cleanup
319
+ ├── runs teardown
320
320
  └── reports TestResult
321
321
  ```
322
322
 
@@ -343,7 +343,7 @@ end
343
343
 
344
344
  fixture :server do
345
345
  server = TestServer.start
346
- cleanup { server.stop }
346
+ on_teardown { server.stop }
347
347
  server
348
348
  end
349
349
 
@@ -363,7 +363,7 @@ resolve logged_in_client
363
363
  requires server
364
364
  resolve server
365
365
  evaluate server block
366
- register cleanup
366
+ register teardown
367
367
  cache server
368
368
  evaluate client block with server:
369
369
  cache client
@@ -376,7 +376,7 @@ resolve logged_in_client
376
376
 
377
377
  execute test body with logged_in_client:
378
378
 
379
- run cleanup stack in reverse order
379
+ run teardown stack in reverse order
380
380
  ```
381
381
 
382
382
  ## Fixture caching
@@ -397,33 +397,33 @@ shared for the runner lifetime.
397
397
  This keeps regular fixtures isolated while allowing explicit suite fixtures for
398
398
  expensive shared resources.
399
399
 
400
- ## Cleanup stack
400
+ ## Teardown stack
401
401
 
402
- `FixtureSet` owns a cleanup stack for one fixture scope.
402
+ `FixtureSet` owns a teardown stack for one fixture scope.
403
403
 
404
404
  ```ruby
405
- @cleanups = []
405
+ @teardowns = []
406
406
  ```
407
407
 
408
408
  Fixture blocks can call:
409
409
 
410
410
  ```ruby
411
- cleanup { resource.close }
411
+ on_teardown { resource.close }
412
412
  ```
413
413
 
414
414
  This delegates to:
415
415
 
416
416
  ```ruby
417
- fixture_set.add_cleanup(&block)
417
+ fixture_set.add_teardown(&block)
418
418
  ```
419
419
 
420
- For test-scoped fixtures, cleanup runs after the test in reverse order:
420
+ For test-scoped fixtures, teardown runs after the test in reverse order:
421
421
 
422
422
  ```ruby
423
- @cleanups.reverse_each(&:call)
423
+ @teardowns.reverse_each(&:call)
424
424
  ```
425
425
 
426
- For suite-scoped fixtures, cleanup runs after all tests. Reverse order matters
426
+ For suite-scoped fixtures, teardown runs after all tests. Reverse order matters
427
427
  because later resources may depend on earlier ones.
428
428
 
429
429
  Example:
@@ -431,18 +431,18 @@ Example:
431
431
  ```ruby
432
432
  fixture :server do
433
433
  server = TestServer.start
434
- cleanup { server.stop }
434
+ on_teardown { server.stop }
435
435
  server
436
436
  end
437
437
 
438
438
  fixture :browser do |server:|
439
439
  browser = Browser.launch(server.url)
440
- cleanup { browser.close }
440
+ on_teardown { browser.close }
441
441
  browser
442
442
  end
443
443
  ```
444
444
 
445
- Cleanup should run:
445
+ Teardown should run:
446
446
 
447
447
  ```text
448
448
  browser.close
@@ -669,7 +669,7 @@ Fixture block execution happens on the fixture instance:
669
669
  fixture_instance.instance_exec(**dependencies, &definition.block)
670
670
  ```
671
671
 
672
- This allows fixture helper methods and `cleanup` to be private instance methods.
672
+ This allows fixture helper methods and `on_teardown` to be private instance methods.
673
673
 
674
674
  ## Helper methods in fixtures
675
675
 
@@ -856,7 +856,24 @@ If no paths are given:
856
856
  bundle exec smartest
857
857
  ```
858
858
 
859
- should default to:
859
+ should default to `smartest/**/*_test.rb` when `smartest/` exists. If the
860
+ directory does not exist, the CLI should print scaffold guidance and exit with
861
+ status `1`:
862
+
863
+ ```text
864
+ No smartest/ directory found.
865
+
866
+ To create a Smartest test scaffold:
867
+ bundle exec smartest --init
868
+
869
+ For browser tests:
870
+ bundle exec smartest --init-browser
871
+
872
+ See all commands:
873
+ bundle exec smartest --help
874
+ ```
875
+
876
+ Default paths:
860
877
 
861
878
  ```text
862
879
  smartest/**/*_test.rb
@@ -877,8 +894,8 @@ arguments.files.each { |file| load File.expand_path(file) }
877
894
  exit Smartest::Runner.new(tests: arguments.select_tests(Smartest.suite.tests)).run
878
895
  ```
879
896
 
880
- `Smartest::CLIArguments` should support file paths, shell globs, `path:line`,
881
- and `path:start-end` filters.
897
+ `Smartest::CLIArguments` should support file paths, directory paths, shell
898
+ globs, `path:line`, and `path:start-end` filters.
882
899
 
883
900
  `smartest/autorun` should use `at_exit`.
884
901
 
@@ -947,7 +964,7 @@ end
947
964
  ```
948
965
 
949
966
  `around_suite` wraps the full suite body, including all tests and suite fixture
950
- cleanup. The hook receives a run target and must call `suite.run` exactly once.
967
+ teardown. The hook receives a run target and must call `suite.run` exactly once.
951
968
  Multiple hooks compose in registration order, with the first hook as the
952
969
  outermost wrapper.
953
970
 
@@ -996,7 +1013,7 @@ For `around_test`, those registrations are test-run local and must happen before
996
1013
  `test.run`.
997
1014
 
998
1015
  Fixture classes registered from `around_test` must not define `suite_fixture`.
999
- Suite-scoped fixtures need suite-level cache and cleanup ownership, so classes
1016
+ Suite-scoped fixtures need suite-level cache and teardown ownership, so classes
1000
1017
  with suite-scoped fixtures must be registered from `around_suite`.
1001
1018
 
1002
1019
  Potential simpler per-test API:
@@ -1019,7 +1036,7 @@ Order:
1019
1036
  before hooks
1020
1037
  fixture setup
1021
1038
  test body
1022
- fixture cleanup
1039
+ fixture teardown
1023
1040
  after hooks
1024
1041
  ```
1025
1042
 
@@ -1030,12 +1047,12 @@ fixture setup
1030
1047
  before hooks
1031
1048
  test body
1032
1049
  after hooks
1033
- fixture cleanup
1050
+ fixture teardown
1034
1051
  ```
1035
1052
 
1036
1053
  This needs a final decision later.
1037
1054
 
1038
- Fixture cleanup already handles resource-specific teardown.
1055
+ Fixture teardown already handles resource-specific teardown.
1039
1056
 
1040
1057
  ### Around-test parallelism note
1041
1058
 
@@ -1069,7 +1086,7 @@ Expensive shared resources can use `suite_fixture`:
1069
1086
  ```ruby
1070
1087
  suite_fixture :server do
1071
1088
  server = TestServer.start
1072
- cleanup { server.stop }
1089
+ on_teardown { server.stop }
1073
1090
  server
1074
1091
  end
1075
1092
  ```
@@ -1082,7 +1099,7 @@ Supported scopes:
1082
1099
  `fixture :name do ... end` creates a test-scoped fixture.
1083
1100
 
1084
1101
  `suite_fixture :name do ... end` creates a suite-scoped fixture. It is lazy:
1085
- setup runs the first time a test requests it, and cleanup runs after all tests.
1102
+ setup runs the first time a test requests it, and teardown runs after all tests.
1086
1103
 
1087
1104
  Test-scoped fixtures may depend on suite-scoped fixtures. Suite-scoped fixtures
1088
1105
  may depend only on other suite-scoped fixtures.
@@ -1119,7 +1136,7 @@ Benefits:
1119
1136
  - reusable fixture modules
1120
1137
  - clearer organization
1121
1138
  - fewer global definitions
1122
- - natural place for cleanup helper
1139
+ - natural place for teardown helper
1123
1140
 
1124
1141
  Example:
1125
1142
 
@@ -1180,7 +1197,7 @@ Pros:
1180
1197
  - easy dependency extraction
1181
1198
  - easy duplicate detection
1182
1199
  - easy source locations
1183
- - easy cleanup integration
1200
+ - easy teardown integration
1184
1201
 
1185
1202
  ### `fixture def user`
1186
1203
 
@@ -1224,7 +1241,7 @@ Reason:
1224
1241
 
1225
1242
  - requires around-chain execution
1226
1243
  - complicates dependency handling
1227
- - not needed if `cleanup` exists
1244
+ - not needed if `on_teardown` exists
1228
1245
  - makes fixture API more complex
1229
1246
 
1230
1247
  Could be added later as advanced API.
@@ -1254,7 +1271,7 @@ class AppFixture < Smartest::Fixture
1254
1271
 
1255
1272
  fixture :server do
1256
1273
  server = TestServer.start
1257
- cleanup { server.stop }
1274
+ on_teardown { server.stop }
1258
1275
  server
1259
1276
  end
1260
1277
 
data/exe/smartest CHANGED
@@ -7,18 +7,51 @@ require "smartest"
7
7
 
8
8
  usage = <<~USAGE
9
9
  Usage:
10
- smartest [--profile N] [paths...]
11
- smartest [--profile N] path/to/test_file.rb:line[-line]
12
- smartest --init
13
- smartest --init-browser
14
- smartest --version
15
- smartest --help
16
-
17
- When no paths are given, Smartest loads smartest/**/*_test.rb.
18
- Smartest prints the 5 slowest tests after the run by default.
19
- Use --profile N to choose how many slowest tests are printed.
10
+ bundle exec smartest [options] [paths...]
11
+
12
+ Common commands:
13
+ bundle exec smartest
14
+ Run tests under smartest/**/*_test.rb
15
+
16
+ bundle exec smartest smartest/suite1/
17
+ Run test files matching smartest/suite1/**/*_test.rb
18
+
19
+ bundle exec smartest smartest/user_test.rb
20
+ Run one test file
21
+
22
+ bundle exec smartest smartest/user_test.rb:12
23
+ Run tests around line 12
24
+
25
+ bundle exec smartest --init
26
+ Generate a basic Smartest scaffold
27
+
28
+ bundle exec smartest --init-browser
29
+ Generate a Playwright browser-test scaffold
30
+
31
+ Options:
32
+ --profile N
33
+ Print the N slowest tests. Defaults to 5.
34
+
35
+ --version, -v
36
+ Print the installed Smartest version.
37
+
38
+ --help, -h
39
+ Print this help.
20
40
  USAGE
21
41
 
42
+ missing_smartest_directory_message = <<~MESSAGE
43
+ No smartest/ directory found.
44
+
45
+ To create a Smartest test scaffold:
46
+ bundle exec smartest --init
47
+
48
+ For browser tests:
49
+ bundle exec smartest --init-browser
50
+
51
+ See all commands:
52
+ bundle exec smartest --help
53
+ MESSAGE
54
+
22
55
  command = :run
23
56
 
24
57
  begin
@@ -45,10 +78,16 @@ begin
45
78
  Smartest.disable_autorun!
46
79
  Smartest.install_dsl!
47
80
  test_load_path = File.expand_path("smartest", Dir.pwd)
48
- $LOAD_PATH.unshift(test_load_path) if Dir.exist?(test_load_path) && !$LOAD_PATH.include?(test_load_path)
49
81
 
50
82
  arguments = Smartest::CLIArguments.new(ARGV)
51
83
 
84
+ if arguments.default_paths? && !Dir.exist?(test_load_path)
85
+ puts missing_smartest_directory_message
86
+ exit 1
87
+ end
88
+
89
+ $LOAD_PATH.unshift(test_load_path) if Dir.exist?(test_load_path) && !$LOAD_PATH.include?(test_load_path)
90
+
52
91
  arguments.files.each do |file|
53
92
  load File.expand_path(file)
54
93
  end
@@ -5,6 +5,7 @@ require "set"
5
5
  module Smartest
6
6
  class CLIArguments
7
7
  DEFAULT_PROFILE_COUNT = 5
8
+ DEFAULT_PATHS = ["smartest/**/*_test.rb"].freeze
8
9
 
9
10
  attr_reader :files, :line_filters, :profile_count
10
11
 
@@ -13,15 +14,25 @@ module Smartest
13
14
  @whole_files = Set.new
14
15
  @line_filters = Hash.new { |hash, key| hash[key] = Set.new }
15
16
  @profile_count = DEFAULT_PROFILE_COUNT
17
+ @default_paths = false
16
18
 
17
19
  paths = extract_options(argv)
18
- parse_paths(paths.empty? ? ["smartest/**/*_test.rb"] : paths)
20
+ if paths.empty?
21
+ @default_paths = true
22
+ paths = DEFAULT_PATHS
23
+ end
24
+
25
+ parse_paths(paths)
19
26
  end
20
27
 
21
28
  def filter_tests?
22
29
  @line_filters.any?
23
30
  end
24
31
 
32
+ def default_paths?
33
+ @default_paths
34
+ end
35
+
25
36
  def select_tests(tests)
26
37
  return tests unless filter_tests?
27
38
 
@@ -64,8 +75,7 @@ module Smartest
64
75
  def parse_paths(paths)
65
76
  paths.each do |argument|
66
77
  pattern, line_filter = split_line_filter(argument)
67
- matches = Dir[pattern]
68
- files = matches.empty? ? [pattern] : matches
78
+ files = expand_path_pattern(pattern)
69
79
 
70
80
  files.each do |file|
71
81
  @files << file
@@ -82,6 +92,19 @@ module Smartest
82
92
  @files.uniq!
83
93
  end
84
94
 
95
+ def expand_path_pattern(pattern)
96
+ matches = Dir[pattern]
97
+ return [pattern] if matches.empty?
98
+
99
+ matches.flat_map do |match|
100
+ if File.directory?(match)
101
+ Dir[File.join(match, "**", "*_test.rb")]
102
+ else
103
+ match
104
+ end
105
+ end
106
+ end
107
+
85
108
  def split_line_filter(argument)
86
109
  match = argument.match(/\A(.+):(\d+)(?:-(\d+))?\z/)
87
110
  return [argument, nil] unless match
@@ -4,10 +4,10 @@ module Smartest
4
4
  module ConstantStubHelpers
5
5
  private
6
6
 
7
- def simple_stub_const(constant_path, value)
8
- raise ArgumentError, "simple_stub_const block is required" unless block_given?
7
+ def with_stub_const(constant_path, value)
8
+ raise ArgumentError, "with_stub_const block is required" unless block_given?
9
9
 
10
- owner, constant_name = resolve_simple_stub_constant(constant_path)
10
+ owner, constant_name = resolve_with_stub_constant(constant_path)
11
11
  original_defined = owner.const_defined?(constant_name, false)
12
12
  original_value = owner.const_get(constant_name, false) if original_defined
13
13
 
@@ -22,7 +22,7 @@ module Smartest
22
22
  end
23
23
  end
24
24
 
25
- def resolve_simple_stub_constant(constant_path)
25
+ def resolve_with_stub_constant(constant_path)
26
26
  unless constant_path.is_a?(String) || constant_path.is_a?(Symbol)
27
27
  raise ArgumentError, "constant path must be a String or Symbol"
28
28
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Smartest
4
4
  class Fixture
5
- RESERVED_CONTEXT_METHODS = %i[skip pending simple_stub_const].freeze
5
+ RESERVED_CONTEXT_METHODS = %i[skip pending with_stub_const].freeze
6
6
 
7
7
  class << self
8
8
  def fixture(name, scope: :test, &block)
@@ -59,10 +59,10 @@ module Smartest
59
59
 
60
60
  private
61
61
 
62
- def cleanup(&block)
63
- raise ArgumentError, "cleanup block is required" unless block
62
+ def on_teardown(&block)
63
+ raise ArgumentError, "on_teardown block is required" unless block
64
64
 
65
- @fixture_set.add_cleanup(&block)
65
+ @fixture_set.add_teardown(&block)
66
66
  end
67
67
 
68
68
  def simple_stub_any_instance_of(klass, method_name, &block)
@@ -75,7 +75,7 @@ module Smartest
75
75
 
76
76
  def apply_simple_stub(stub)
77
77
  stub.apply!
78
- cleanup { stub.reset }
78
+ on_teardown { stub.reset }
79
79
  stub
80
80
  end
81
81
 
@@ -9,7 +9,7 @@ module Smartest
9
9
  @parent = parent
10
10
  @cache = {}
11
11
  @setup_errors = {}
12
- @cleanups = []
12
+ @teardowns = []
13
13
  @resolving = []
14
14
 
15
15
  build_fixture_index
@@ -49,17 +49,17 @@ module Smartest
49
49
  @resolving.pop if @resolving.last == symbol_name
50
50
  end
51
51
 
52
- def add_cleanup(&block)
53
- raise ArgumentError, "cleanup block is required" unless block
52
+ def add_teardown(&block)
53
+ raise ArgumentError, "on_teardown block is required" unless block
54
54
 
55
- @cleanups << block
55
+ @teardowns << block
56
56
  end
57
57
 
58
- def run_cleanups
58
+ def run_teardowns
59
59
  errors = []
60
60
 
61
- @cleanups.reverse_each do |cleanup|
62
- cleanup.call
61
+ @teardowns.reverse_each do |teardown|
62
+ teardown.call
63
63
  rescue Exception => error
64
64
  raise if Smartest.fatal_exception?(error)
65
65
 
@@ -14,7 +14,7 @@ module Smartest
14
14
  runtime = Playwright.create(
15
15
  playwright_cli_executable_path: "./node_modules/.bin/playwright",
16
16
  )
17
- cleanup { runtime.stop }
17
+ on_teardown { runtime.stop }
18
18
  runtime.playwright
19
19
  end
20
20
 
@@ -35,13 +35,13 @@ module Smartest
35
35
  end
36
36
 
37
37
  browser = playwright.send(browser_type).launch(**launch_options)
38
- cleanup { browser.close }
38
+ on_teardown { browser.close }
39
39
  browser
40
40
  end
41
41
 
42
42
  fixture :page do |browser:|
43
43
  context = browser.new_context
44
- cleanup { context.close }
44
+ on_teardown { context.close }
45
45
  context.new_page
46
46
  end
47
47
  end
@@ -21,14 +21,14 @@ module Smartest
21
21
  @io.puts record_line(result)
22
22
  end
23
23
 
24
- def finish(results, suite_cleanup_errors: [], suite_errors: [])
24
+ def finish(results, suite_teardown_errors: [], suite_errors: [])
25
25
  failures = results.select(&:failed?)
26
26
  skipped = results.select(&:skipped?)
27
27
  pending = results.select(&:pending?)
28
28
 
29
29
  report_failures(failures) if failures.any?
30
30
  report_suite_errors(suite_errors) if suite_errors.any?
31
- report_suite_cleanup_errors(suite_cleanup_errors) if suite_cleanup_errors.any?
31
+ report_suite_teardown_errors(suite_teardown_errors) if suite_teardown_errors.any?
32
32
  report_profile(results) if @profile_count && @profile_count.positive?
33
33
 
34
34
  @io.puts
@@ -39,9 +39,9 @@ module Smartest
39
39
  suite_label = suite_errors.count == 1 ? "suite failure" : "suite failures"
40
40
  summary = "#{summary}, #{suite_errors.count} #{suite_label}"
41
41
  end
42
- if suite_cleanup_errors.any?
43
- cleanup_label = suite_cleanup_errors.count == 1 ? "suite cleanup" : "suite cleanups"
44
- summary = "#{summary}, #{suite_cleanup_errors.count} #{cleanup_label} failed"
42
+ if suite_teardown_errors.any?
43
+ teardown_label = suite_teardown_errors.count == 1 ? "suite teardown" : "suite teardowns"
44
+ summary = "#{summary}, #{suite_teardown_errors.count} #{teardown_label} failed"
45
45
  end
46
46
  @io.puts summary
47
47
  end
@@ -72,7 +72,7 @@ module Smartest
72
72
  @io.puts "#{index + 1}) #{result.test_case.name}"
73
73
  report_location(result.test_case.location)
74
74
  report_error(result.error) if result.error
75
- result.cleanup_errors.each { |error| report_cleanup_error(error) }
75
+ result.teardown_errors.each { |error| report_teardown_error(error) }
76
76
  @io.puts
77
77
  end
78
78
  end
@@ -89,14 +89,14 @@ module Smartest
89
89
  end
90
90
  end
91
91
 
92
- def report_suite_cleanup_errors(errors)
92
+ def report_suite_teardown_errors(errors)
93
93
  @io.puts
94
- @io.puts "Suite cleanup failures:"
94
+ @io.puts "Suite teardown failures:"
95
95
  @io.puts
96
96
 
97
97
  errors.each_with_index do |error, index|
98
- @io.puts "#{index + 1}) suite cleanup"
99
- report_cleanup_error(error)
98
+ @io.puts "#{index + 1}) suite teardown"
99
+ report_teardown_error(error)
100
100
  @io.puts
101
101
  end
102
102
  end
@@ -148,8 +148,8 @@ module Smartest
148
148
  report_backtrace(error)
149
149
  end
150
150
 
151
- def report_cleanup_error(error)
152
- @io.puts " cleanup failed: #{error.class}: #{error.message}"
151
+ def report_teardown_error(error)
152
+ @io.puts " teardown failed: #{error.class}: #{error.message}"
153
153
  report_backtrace(error)
154
154
  end
155
155