smartest 0.1.0.alpha1 → 0.1.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 +2 -0
- data/DEVELOPMENT.md +3 -0
- data/README.md +44 -5
- data/SMARTEST_DESIGN.md +9 -5
- data/exe/smartest +5 -12
- data/lib/smartest/cli_arguments.rb +66 -0
- data/lib/smartest/dsl.rb +5 -1
- data/lib/smartest/init_generator.rb +61 -0
- data/lib/smartest/matcher_registry.rb +27 -0
- data/lib/smartest/runner.rb +12 -5
- data/lib/smartest/suite.rb +2 -1
- data/lib/smartest/test_case.rb +46 -0
- data/lib/smartest/version.rb +1 -1
- data/lib/smartest.rb +2 -0
- data/smartest/smartest_test.rb +268 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c1ce8aea4b74c7ca0de7f3a2b1472b79ea86849be91801e11922883b6423afdf
|
|
4
|
+
data.tar.gz: be5f8e88c1bd93eb061eb0a854b46a6c6094627b36cd22f8778026e35161992a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 864cea53cff77bf90e73bdf6528c9e6ff7ba633c89c8edbf011bd50552d9bf1aea606c0a9e4af83352847f88b1ca8571433987d2f933d20b50270b2d2b81079d
|
|
7
|
+
data.tar.gz: d587ffbf743f4fe75d8977346ae653fe2616ab70e4480453442f4677f7d4d6a4f84166f64c17b50417616693232149ef36214c8c12d530de41a9ff5f36c75a51
|
data/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
- Support per-test fixture caching and cleanup.
|
|
10
10
|
- Support suite-scoped fixtures through `suite_fixture`.
|
|
11
11
|
- Support `eq`, `include`, `be_nil`, and `raise_error` matchers.
|
|
12
|
+
- Support custom matcher modules through `use_matcher`.
|
|
13
|
+
- Generate an opt-in `PredicateMatcher` custom matcher for `be_<predicate>` calls.
|
|
12
14
|
- Add the `smartest` CLI.
|
|
13
15
|
- Add `--help` and `--version` CLI options.
|
|
14
16
|
- Use `smartest/**/*_test.rb` as the default CLI glob so Smartest can coexist with Minitest files under `test/`.
|
data/DEVELOPMENT.md
CHANGED
|
@@ -126,6 +126,7 @@ Required methods:
|
|
|
126
126
|
```ruby
|
|
127
127
|
test(name, **metadata, &block)
|
|
128
128
|
use_fixture(klass)
|
|
129
|
+
use_matcher(matcher_module)
|
|
129
130
|
```
|
|
130
131
|
|
|
131
132
|
Possible later methods:
|
|
@@ -613,6 +614,8 @@ A practical approach:
|
|
|
613
614
|
- `exe/smartest`
|
|
614
615
|
- load files from ARGV
|
|
615
616
|
- default glob `smartest/**/*_test.rb`
|
|
617
|
+
- support `path:line` and `path:start-end` filters that run tests whose `test`
|
|
618
|
+
blocks contain or intersect the lines
|
|
616
619
|
- add `smartest/` to the load path before loading tests
|
|
617
620
|
- generate a `smartest/test_helper.rb` that loads `smartest/fixtures/**/*.rb`
|
|
618
621
|
- exit code 0 on success, 1 on failure
|
data/README.md
CHANGED
|
@@ -59,6 +59,8 @@ This creates:
|
|
|
59
59
|
```text
|
|
60
60
|
smartest/test_helper.rb
|
|
61
61
|
smartest/fixtures/
|
|
62
|
+
smartest/matchers/
|
|
63
|
+
smartest/matchers/predicate_matcher.rb
|
|
62
64
|
smartest/example_test.rb
|
|
63
65
|
```
|
|
64
66
|
|
|
@@ -88,6 +90,14 @@ You can also pass explicit paths:
|
|
|
88
90
|
bundle exec smartest smartest/**/*_test.rb
|
|
89
91
|
```
|
|
90
92
|
|
|
93
|
+
To run tests by line number, append `:line` or `:start-end` to the file path.
|
|
94
|
+
Smartest runs tests whose `test` blocks contain or intersect the selected lines:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
bundle exec smartest smartest/user_test.rb:12
|
|
98
|
+
bundle exec smartest smartest/user_test.rb:3-12
|
|
99
|
+
```
|
|
100
|
+
|
|
91
101
|
CLI help and version output are available with:
|
|
92
102
|
|
|
93
103
|
```bash
|
|
@@ -163,6 +173,10 @@ be_nil
|
|
|
163
173
|
raise_error(ErrorClass)
|
|
164
174
|
```
|
|
165
175
|
|
|
176
|
+
Custom matcher modules can be registered with `use_matcher`. The generated
|
|
177
|
+
scaffold includes a `PredicateMatcher` custom matcher for `be_<predicate>` calls.
|
|
178
|
+
See [Matchers](documentation/docs/matchers.md).
|
|
179
|
+
|
|
166
180
|
## Fixtures
|
|
167
181
|
|
|
168
182
|
Fixtures are defined in classes.
|
|
@@ -176,9 +190,17 @@ class AppFixture < Smartest::Fixture
|
|
|
176
190
|
)
|
|
177
191
|
end
|
|
178
192
|
end
|
|
193
|
+
```
|
|
179
194
|
|
|
195
|
+
Register fixture classes from `smartest/test_helper.rb`:
|
|
196
|
+
|
|
197
|
+
```ruby
|
|
180
198
|
use_fixture AppFixture
|
|
199
|
+
```
|
|
181
200
|
|
|
201
|
+
Tests request fixtures by keyword:
|
|
202
|
+
|
|
203
|
+
```ruby
|
|
182
204
|
test("user") do |user:|
|
|
183
205
|
expect(user.name).to eq("Alice")
|
|
184
206
|
end
|
|
@@ -329,8 +351,16 @@ class WebFixture < Smartest::Fixture
|
|
|
329
351
|
client
|
|
330
352
|
end
|
|
331
353
|
end
|
|
354
|
+
```
|
|
332
355
|
|
|
356
|
+
```ruby
|
|
357
|
+
# smartest/test_helper.rb
|
|
333
358
|
use_fixture WebFixture
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
```ruby
|
|
362
|
+
# smartest/web_test.rb
|
|
363
|
+
require "test_helper"
|
|
334
364
|
|
|
335
365
|
test("GET /me") do |logged_in_client:|
|
|
336
366
|
response = logged_in_client.get("/me")
|
|
@@ -361,7 +391,7 @@ server cleanup
|
|
|
361
391
|
|
|
362
392
|
## Registering fixture classes
|
|
363
393
|
|
|
364
|
-
Use `use_fixture`:
|
|
394
|
+
Use `use_fixture` from `smartest/test_helper.rb`:
|
|
365
395
|
|
|
366
396
|
```ruby
|
|
367
397
|
use_fixture AppFixture
|
|
@@ -421,6 +451,9 @@ smartest/
|
|
|
421
451
|
fixtures/
|
|
422
452
|
app_fixture.rb
|
|
423
453
|
web_fixture.rb
|
|
454
|
+
matchers/
|
|
455
|
+
predicate_matcher.rb
|
|
456
|
+
have_status_matcher.rb
|
|
424
457
|
example_test.rb
|
|
425
458
|
```
|
|
426
459
|
|
|
@@ -431,10 +464,18 @@ require "smartest/autorun"
|
|
|
431
464
|
Dir[File.join(__dir__, "fixtures", "**", "*.rb")].sort.each do |fixture_file|
|
|
432
465
|
require fixture_file
|
|
433
466
|
end
|
|
467
|
+
|
|
468
|
+
Dir[File.join(__dir__, "matchers", "**", "*.rb")].sort.each do |matcher_file|
|
|
469
|
+
require matcher_file
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
use_fixture WebFixture
|
|
473
|
+
use_matcher PredicateMatcher
|
|
434
474
|
```
|
|
435
475
|
|
|
436
|
-
The generated helper loads Ruby files under `smartest/fixtures/`
|
|
437
|
-
|
|
476
|
+
The generated helper loads Ruby files under `smartest/fixtures/` and
|
|
477
|
+
`smartest/matchers/` in sorted order. Register fixture classes and matcher
|
|
478
|
+
modules from the helper with `use_fixture` and `use_matcher`.
|
|
438
479
|
|
|
439
480
|
Example:
|
|
440
481
|
|
|
@@ -457,8 +498,6 @@ end
|
|
|
457
498
|
# smartest/example_test.rb
|
|
458
499
|
require "test_helper"
|
|
459
500
|
|
|
460
|
-
use_fixture WebFixture
|
|
461
|
-
|
|
462
501
|
test("GET /health") do |client:|
|
|
463
502
|
expect(client.get("/health").status).to eq(200)
|
|
464
503
|
end
|
data/SMARTEST_DESIGN.md
CHANGED
|
@@ -294,7 +294,7 @@ context.instance_exec(**fixtures, &block)
|
|
|
294
294
|
|
|
295
295
|
This keeps the top-level DSL small.
|
|
296
296
|
|
|
297
|
-
Only `test`, `fixture`, and `
|
|
297
|
+
Only `test`, `fixture`, `use_fixture`, and `use_matcher` need to be globally available when using `smartest/autorun`.
|
|
298
298
|
|
|
299
299
|
## Core architecture
|
|
300
300
|
|
|
@@ -794,15 +794,19 @@ CLI flow:
|
|
|
794
794
|
```ruby
|
|
795
795
|
require "smartest"
|
|
796
796
|
|
|
797
|
-
|
|
797
|
+
Smartest.disable_autorun!
|
|
798
|
+
Kernel.prepend Smartest::DSL
|
|
798
799
|
$LOAD_PATH.unshift File.expand_path("smartest", Dir.pwd)
|
|
799
800
|
|
|
800
|
-
|
|
801
|
-
files.each { |file|
|
|
801
|
+
arguments = Smartest::CLIArguments.new(ARGV)
|
|
802
|
+
arguments.files.each { |file| load File.expand_path(file) }
|
|
802
803
|
|
|
803
|
-
exit Smartest::Runner.new.run
|
|
804
|
+
exit Smartest::Runner.new(tests: arguments.select_tests(Smartest.suite.tests)).run
|
|
804
805
|
```
|
|
805
806
|
|
|
807
|
+
`Smartest::CLIArguments` should support file paths, shell globs, `path:line`,
|
|
808
|
+
and `path:start-end` filters.
|
|
809
|
+
|
|
806
810
|
`smartest/autorun` should use `at_exit`.
|
|
807
811
|
|
|
808
812
|
```ruby
|
data/exe/smartest
CHANGED
|
@@ -8,6 +8,7 @@ require "smartest"
|
|
|
8
8
|
usage = <<~USAGE
|
|
9
9
|
Usage:
|
|
10
10
|
smartest [paths...]
|
|
11
|
+
smartest path/to/test_file.rb:line[-line]
|
|
11
12
|
smartest --init
|
|
12
13
|
smartest --version
|
|
13
14
|
smartest --help
|
|
@@ -38,21 +39,13 @@ begin
|
|
|
38
39
|
test_load_path = File.expand_path("smartest", Dir.pwd)
|
|
39
40
|
$LOAD_PATH.unshift(test_load_path) if Dir.exist?(test_load_path) && !$LOAD_PATH.include?(test_load_path)
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
else
|
|
45
|
-
ARGV.flat_map do |pattern|
|
|
46
|
-
matches = Dir[pattern]
|
|
47
|
-
matches.empty? ? [pattern] : matches
|
|
48
|
-
end.uniq
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
files.each do |file|
|
|
42
|
+
arguments = Smartest::CLIArguments.new(ARGV)
|
|
43
|
+
|
|
44
|
+
arguments.files.each do |file|
|
|
52
45
|
load File.expand_path(file)
|
|
53
46
|
end
|
|
54
47
|
|
|
55
|
-
exit Smartest::Runner.new.run
|
|
48
|
+
exit Smartest::Runner.new(tests: arguments.select_tests(Smartest.suite.tests)).run
|
|
56
49
|
rescue Exception => error
|
|
57
50
|
raise if Smartest.fatal_exception?(error)
|
|
58
51
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module Smartest
|
|
6
|
+
class CLIArguments
|
|
7
|
+
attr_reader :files, :line_filters
|
|
8
|
+
|
|
9
|
+
def initialize(argv)
|
|
10
|
+
@files = []
|
|
11
|
+
@whole_files = Set.new
|
|
12
|
+
@line_filters = Hash.new { |hash, key| hash[key] = Set.new }
|
|
13
|
+
|
|
14
|
+
parse(argv.empty? ? ["smartest/**/*_test.rb"] : argv)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def filter_tests?
|
|
18
|
+
@line_filters.any?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def select_tests(tests)
|
|
22
|
+
return tests unless filter_tests?
|
|
23
|
+
|
|
24
|
+
tests.select do |test_case|
|
|
25
|
+
next false unless test_case.location
|
|
26
|
+
|
|
27
|
+
path = File.expand_path(test_case.location.path)
|
|
28
|
+
@whole_files.include?(path) ||
|
|
29
|
+
@line_filters.fetch(path, []).any? { |line_filter| test_case.includes_line_range?(line_filter) }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def parse(argv)
|
|
36
|
+
argv.each do |argument|
|
|
37
|
+
pattern, line_filter = split_line_filter(argument)
|
|
38
|
+
matches = Dir[pattern]
|
|
39
|
+
files = matches.empty? ? [pattern] : matches
|
|
40
|
+
|
|
41
|
+
files.each do |file|
|
|
42
|
+
@files << file
|
|
43
|
+
|
|
44
|
+
path = File.expand_path(file)
|
|
45
|
+
if line_filter
|
|
46
|
+
@line_filters[path].add(line_filter)
|
|
47
|
+
else
|
|
48
|
+
@whole_files.add(path)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
@files.uniq!
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def split_line_filter(argument)
|
|
57
|
+
match = argument.match(/\A(.+):(\d+)(?:-(\d+))?\z/)
|
|
58
|
+
return [argument, nil] unless match
|
|
59
|
+
|
|
60
|
+
start_line = match[2].to_i
|
|
61
|
+
end_line = match[3] ? match[3].to_i : start_line
|
|
62
|
+
|
|
63
|
+
[match[1], start_line..end_line]
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
data/lib/smartest/dsl.rb
CHANGED
|
@@ -17,6 +17,10 @@ module Smartest
|
|
|
17
17
|
Smartest.suite.fixture_classes.add(klass)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
def use_matcher(matcher_module)
|
|
21
|
+
Smartest.suite.matcher_modules.add(matcher_module)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private :test, :use_fixture, :use_matcher
|
|
21
25
|
end
|
|
22
26
|
end
|
|
@@ -13,6 +13,66 @@ module Smartest
|
|
|
13
13
|
Dir[File.join(__dir__, "fixtures", "**", "*.rb")].sort.each do |fixture_file|
|
|
14
14
|
require fixture_file
|
|
15
15
|
end
|
|
16
|
+
|
|
17
|
+
Dir[File.join(__dir__, "matchers", "**", "*.rb")].sort.each do |matcher_file|
|
|
18
|
+
require matcher_file
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
use_matcher PredicateMatcher
|
|
22
|
+
RUBY
|
|
23
|
+
"smartest/matchers/predicate_matcher.rb" => <<~RUBY,
|
|
24
|
+
# frozen_string_literal: true
|
|
25
|
+
|
|
26
|
+
module PredicateMatcher
|
|
27
|
+
def method_missing(name, *arguments, &block)
|
|
28
|
+
matcher_name = name.to_s
|
|
29
|
+
return super unless matcher_name.match?(/\\Abe_.+\\z/)
|
|
30
|
+
|
|
31
|
+
Matcher.new(matcher_name.delete_prefix("be_"), arguments, block)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def respond_to_missing?(name, include_private = false)
|
|
35
|
+
name.to_s.match?(/\\Abe_.+\\z/) || super
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class Matcher
|
|
39
|
+
def initialize(predicate_name, arguments, block)
|
|
40
|
+
@predicate_name = predicate_name
|
|
41
|
+
@predicate = "\#{predicate_name}?"
|
|
42
|
+
@arguments = arguments
|
|
43
|
+
@block = block
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def matches?(actual)
|
|
47
|
+
@actual = actual
|
|
48
|
+
return false unless actual.respond_to?(@predicate)
|
|
49
|
+
|
|
50
|
+
!!actual.public_send(@predicate, *@arguments, &@block)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def failure_message
|
|
54
|
+
return "expected \#{@actual.inspect} to respond to \#{@predicate}" unless @actual.respond_to?(@predicate)
|
|
55
|
+
|
|
56
|
+
"expected \#{@actual.inspect} to be \#{description}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def negated_failure_message
|
|
60
|
+
"expected \#{@actual.inspect} not to be \#{description}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def description
|
|
66
|
+
return @predicate_name if @arguments.empty?
|
|
67
|
+
|
|
68
|
+
"\#{@predicate_name} \#{argument_description}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def argument_description
|
|
72
|
+
@arguments.map(&:inspect).join(", ")
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
16
76
|
RUBY
|
|
17
77
|
"smartest/example_test.rb" => <<~RUBY
|
|
18
78
|
# frozen_string_literal: true
|
|
@@ -33,6 +93,7 @@ module Smartest
|
|
|
33
93
|
def run
|
|
34
94
|
create_directory("smartest")
|
|
35
95
|
create_directory("smartest/fixtures")
|
|
96
|
+
create_directory("smartest/matchers")
|
|
36
97
|
FILES.each { |path, contents| create_file(path, contents) }
|
|
37
98
|
|
|
38
99
|
@output.puts
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smartest
|
|
4
|
+
class MatcherRegistry
|
|
5
|
+
include Enumerable
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@matcher_modules = []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def add(matcher_module)
|
|
12
|
+
unless matcher_module.is_a?(Module) && !matcher_module.is_a?(Class)
|
|
13
|
+
raise ArgumentError, "matcher must be a module"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
@matcher_modules << matcher_module unless @matcher_modules.include?(matcher_module)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def each(&block)
|
|
20
|
+
@matcher_modules.each(&block)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_a
|
|
24
|
+
@matcher_modules.dup
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/smartest/runner.rb
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module Smartest
|
|
4
4
|
class Runner
|
|
5
|
-
def initialize(suite: Smartest.suite, reporter: Reporter.new)
|
|
5
|
+
def initialize(suite: Smartest.suite, reporter: Reporter.new, tests: nil)
|
|
6
6
|
@suite = suite
|
|
7
7
|
@reporter = reporter
|
|
8
|
+
@tests = tests || suite.tests
|
|
8
9
|
end
|
|
9
10
|
|
|
10
11
|
def run
|
|
@@ -12,10 +13,10 @@ module Smartest
|
|
|
12
13
|
suite_cleanup_errors = []
|
|
13
14
|
@suite_fixture_set = nil
|
|
14
15
|
|
|
15
|
-
@reporter.start(@
|
|
16
|
+
@reporter.start(@tests.count)
|
|
16
17
|
|
|
17
18
|
begin
|
|
18
|
-
@
|
|
19
|
+
@tests.each do |test_case|
|
|
19
20
|
result = run_one(test_case)
|
|
20
21
|
results << result
|
|
21
22
|
@reporter.record(result)
|
|
@@ -34,7 +35,7 @@ module Smartest
|
|
|
34
35
|
|
|
35
36
|
def run_one(test_case)
|
|
36
37
|
started_at = now
|
|
37
|
-
context =
|
|
38
|
+
context = build_context
|
|
38
39
|
fixture_set = nil
|
|
39
40
|
error = nil
|
|
40
41
|
cleanup_errors = []
|
|
@@ -68,11 +69,17 @@ module Smartest
|
|
|
68
69
|
def suite_fixture_set
|
|
69
70
|
@suite_fixture_set ||= FixtureSet.new(
|
|
70
71
|
@suite.fixture_classes,
|
|
71
|
-
context:
|
|
72
|
+
context: build_context,
|
|
72
73
|
scope: :suite
|
|
73
74
|
)
|
|
74
75
|
end
|
|
75
76
|
|
|
77
|
+
def build_context
|
|
78
|
+
ExecutionContext.new.tap do |context|
|
|
79
|
+
@suite.matcher_modules.each { |matcher_module| context.extend(matcher_module) }
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
76
83
|
def now
|
|
77
84
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
78
85
|
end
|
data/lib/smartest/suite.rb
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
module Smartest
|
|
4
4
|
class Suite
|
|
5
|
-
attr_reader :tests, :fixture_classes
|
|
5
|
+
attr_reader :tests, :fixture_classes, :matcher_modules
|
|
6
6
|
|
|
7
7
|
def initialize
|
|
8
8
|
@tests = TestRegistry.new
|
|
9
9
|
@fixture_classes = FixtureClassRegistry.new
|
|
10
|
+
@matcher_modules = MatcherRegistry.new
|
|
10
11
|
end
|
|
11
12
|
end
|
|
12
13
|
end
|
data/lib/smartest/test_case.rb
CHANGED
|
@@ -14,5 +14,51 @@ module Smartest
|
|
|
14
14
|
@location = location
|
|
15
15
|
@fixture_names = ParameterExtractor.required_keyword_names(block, usage: :test)
|
|
16
16
|
end
|
|
17
|
+
|
|
18
|
+
def includes_line?(line)
|
|
19
|
+
includes_line_range?(line..line)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def includes_line_range?(range)
|
|
23
|
+
return false unless location
|
|
24
|
+
|
|
25
|
+
current_range = line_range
|
|
26
|
+
current_range.begin <= range.end && range.begin <= current_range.end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def line_range
|
|
32
|
+
location.lineno..end_lineno
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def end_lineno
|
|
36
|
+
@end_lineno ||= inferred_end_lineno
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def inferred_end_lineno
|
|
40
|
+
code_location = instruction_sequence_metadata[:code_location]
|
|
41
|
+
return code_location[2] if code_location&.length == 4
|
|
42
|
+
|
|
43
|
+
instruction_sequence_line_numbers.max || location.lineno
|
|
44
|
+
rescue StandardError
|
|
45
|
+
location.lineno
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def instruction_sequence_metadata
|
|
49
|
+
return {} unless defined?(RubyVM::InstructionSequence)
|
|
50
|
+
|
|
51
|
+
sequence = RubyVM::InstructionSequence.of(block)
|
|
52
|
+
metadata = sequence&.to_a&.[](4)
|
|
53
|
+
metadata.is_a?(Hash) ? metadata : {}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def instruction_sequence_line_numbers
|
|
57
|
+
return [] unless defined?(RubyVM::InstructionSequence)
|
|
58
|
+
|
|
59
|
+
sequence = RubyVM::InstructionSequence.of(block)
|
|
60
|
+
body = sequence&.to_a&.last
|
|
61
|
+
body.is_a?(Array) ? body.select { |entry| entry.is_a?(Integer) } : []
|
|
62
|
+
end
|
|
17
63
|
end
|
|
18
64
|
end
|
data/lib/smartest/version.rb
CHANGED
data/lib/smartest.rb
CHANGED
|
@@ -8,6 +8,7 @@ require_relative "smartest/test_registry"
|
|
|
8
8
|
require_relative "smartest/fixture_definition"
|
|
9
9
|
require_relative "smartest/fixture"
|
|
10
10
|
require_relative "smartest/fixture_class_registry"
|
|
11
|
+
require_relative "smartest/matcher_registry"
|
|
11
12
|
require_relative "smartest/fixture_set"
|
|
12
13
|
require_relative "smartest/suite"
|
|
13
14
|
require_relative "smartest/expectations"
|
|
@@ -19,6 +20,7 @@ require_relative "smartest/test_result"
|
|
|
19
20
|
require_relative "smartest/reporter"
|
|
20
21
|
require_relative "smartest/runner"
|
|
21
22
|
require_relative "smartest/init_generator"
|
|
23
|
+
require_relative "smartest/cli_arguments"
|
|
22
24
|
|
|
23
25
|
module Smartest
|
|
24
26
|
class << self
|
data/smartest/smartest_test.rb
CHANGED
|
@@ -95,6 +95,58 @@ test("supports basic matchers") do
|
|
|
95
95
|
expect(status).to eq(0)
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
+
test("registers matcher modules for suite execution contexts") do
|
|
99
|
+
status_matcher = Class.new do
|
|
100
|
+
def initialize(expected)
|
|
101
|
+
@expected = expected
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def matches?(actual)
|
|
105
|
+
@actual = actual
|
|
106
|
+
actual.status == @expected
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def failure_message
|
|
110
|
+
"expected #{@actual.inspect} to have status #{@expected.inspect}"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def negated_failure_message
|
|
114
|
+
"expected #{@actual.inspect} not to have status #{@expected.inspect}"
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
custom_matchers = Module.new do
|
|
119
|
+
define_method(:have_status) do |expected|
|
|
120
|
+
status_matcher.new(expected)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
response = Struct.new(:status).new(200)
|
|
125
|
+
suite = Smartest::Suite.new
|
|
126
|
+
suite.matcher_modules.add(custom_matchers)
|
|
127
|
+
suite.tests.add(SmartestSelfTest.test_case("custom matcher", proc { expect(response).to have_status(200) }))
|
|
128
|
+
|
|
129
|
+
status, = SmartestSelfTest.run_suite(suite)
|
|
130
|
+
|
|
131
|
+
expect(status).to eq(0)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
test("rejects non-module matcher registrations") do
|
|
135
|
+
error = SmartestSelfTest.capture_error(ArgumentError) do
|
|
136
|
+
Smartest::MatcherRegistry.new.add(Object.new)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
expect(error.message).to include("matcher must be a module")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
test("rejects class matcher registrations") do
|
|
143
|
+
error = SmartestSelfTest.capture_error(ArgumentError) do
|
|
144
|
+
Smartest::MatcherRegistry.new.add(Class.new)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
expect(error.message).to include("matcher must be a module")
|
|
148
|
+
end
|
|
149
|
+
|
|
98
150
|
test("resolves keyword fixture dependencies per test") do
|
|
99
151
|
calls = []
|
|
100
152
|
|
|
@@ -477,6 +529,157 @@ test("cli loads files and returns failure status") do
|
|
|
477
529
|
end
|
|
478
530
|
end
|
|
479
531
|
|
|
532
|
+
test("cli loads matcher files registered in test helper") do
|
|
533
|
+
Dir.mktmpdir do |dir|
|
|
534
|
+
smartest_dir = File.join(dir, "smartest")
|
|
535
|
+
matchers_dir = File.join(smartest_dir, "matchers")
|
|
536
|
+
FileUtils.mkdir_p(matchers_dir)
|
|
537
|
+
File.write(File.join(smartest_dir, "test_helper.rb"), <<~RUBY)
|
|
538
|
+
require "smartest/autorun"
|
|
539
|
+
|
|
540
|
+
Dir[File.join(__dir__, "matchers", "**", "*.rb")].sort.each do |matcher_file|
|
|
541
|
+
require matcher_file
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
use_matcher HaveStatusMatcher
|
|
545
|
+
RUBY
|
|
546
|
+
File.write(File.join(matchers_dir, "have_status_matcher.rb"), <<~RUBY)
|
|
547
|
+
module HaveStatusMatcher
|
|
548
|
+
class MatcherImpl
|
|
549
|
+
def initialize(expected)
|
|
550
|
+
@expected = expected
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def matches?(actual)
|
|
554
|
+
@actual = actual
|
|
555
|
+
actual.status == @expected
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def failure_message
|
|
559
|
+
"expected \#{@actual.inspect} to have status \#{@expected.inspect}"
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def negated_failure_message
|
|
563
|
+
"expected \#{@actual.inspect} not to have status \#{@expected.inspect}"
|
|
564
|
+
end
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
def have_status(expected)
|
|
568
|
+
MatcherImpl.new(expected)
|
|
569
|
+
end
|
|
570
|
+
end
|
|
571
|
+
RUBY
|
|
572
|
+
|
|
573
|
+
File.write(File.join(smartest_dir, "sample_test.rb"), <<~RUBY)
|
|
574
|
+
require "test_helper"
|
|
575
|
+
|
|
576
|
+
Response = Struct.new(:status)
|
|
577
|
+
|
|
578
|
+
test("custom matcher") do
|
|
579
|
+
expect(Response.new(200)).to have_status(200)
|
|
580
|
+
end
|
|
581
|
+
RUBY
|
|
582
|
+
|
|
583
|
+
stdout, stderr, status = Open3.capture3(
|
|
584
|
+
{ "RUBYLIB" => File.expand_path("../lib", __dir__) },
|
|
585
|
+
"ruby",
|
|
586
|
+
File.expand_path("../exe/smartest", __dir__),
|
|
587
|
+
"smartest/sample_test.rb",
|
|
588
|
+
chdir: dir
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
expect(status.success?).to eq(true)
|
|
592
|
+
expect(stderr).to eq("")
|
|
593
|
+
expect(stdout).to include("custom matcher")
|
|
594
|
+
expect(stdout).to include("1 test, 1 passed, 0 failed")
|
|
595
|
+
end
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
test("cli runs tests matching a file line filter") do
|
|
599
|
+
Dir.mktmpdir do |dir|
|
|
600
|
+
smartest_dir = File.join(dir, "smartest")
|
|
601
|
+
FileUtils.mkdir_p(smartest_dir)
|
|
602
|
+
File.write(File.join(smartest_dir, "test_helper.rb"), <<~RUBY)
|
|
603
|
+
require "smartest/autorun"
|
|
604
|
+
RUBY
|
|
605
|
+
|
|
606
|
+
test_file = File.join(smartest_dir, "sample_test.rb")
|
|
607
|
+
File.write(test_file, <<~RUBY)
|
|
608
|
+
require "test_helper"
|
|
609
|
+
|
|
610
|
+
test("line one") do
|
|
611
|
+
expect(1).to eq(1)
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
test("line two") do
|
|
615
|
+
expect(2).to eq(2)
|
|
616
|
+
end
|
|
617
|
+
RUBY
|
|
618
|
+
line_number = File.readlines(test_file).find_index { |line| line.include?("expect(2)") } + 1
|
|
619
|
+
|
|
620
|
+
stdout, stderr, status = Open3.capture3(
|
|
621
|
+
{ "RUBYLIB" => File.expand_path("../lib", __dir__) },
|
|
622
|
+
"ruby",
|
|
623
|
+
File.expand_path("../exe/smartest", __dir__),
|
|
624
|
+
"smartest/sample_test.rb:#{line_number}",
|
|
625
|
+
chdir: dir
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
expect(status.success?).to eq(true)
|
|
629
|
+
expect(stderr).to eq("")
|
|
630
|
+
expect(stdout).to include("Running 1 test")
|
|
631
|
+
expect(stdout).not_to include("line one")
|
|
632
|
+
expect(stdout).to include("line two")
|
|
633
|
+
expect(stdout).to include("1 test, 1 passed, 0 failed")
|
|
634
|
+
end
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
test("cli runs tests intersecting a file line range filter") do
|
|
638
|
+
Dir.mktmpdir do |dir|
|
|
639
|
+
smartest_dir = File.join(dir, "smartest")
|
|
640
|
+
FileUtils.mkdir_p(smartest_dir)
|
|
641
|
+
File.write(File.join(smartest_dir, "test_helper.rb"), <<~RUBY)
|
|
642
|
+
require "smartest/autorun"
|
|
643
|
+
RUBY
|
|
644
|
+
|
|
645
|
+
test_file = File.join(smartest_dir, "sample_test.rb")
|
|
646
|
+
File.write(test_file, <<~RUBY)
|
|
647
|
+
require "test_helper"
|
|
648
|
+
|
|
649
|
+
test("range one") do
|
|
650
|
+
expect(1).to eq(1)
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
test("range two") do
|
|
654
|
+
expect(2).to eq(2)
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
test("range three") do
|
|
658
|
+
expect(3).to eq(3)
|
|
659
|
+
end
|
|
660
|
+
RUBY
|
|
661
|
+
lines = File.readlines(test_file)
|
|
662
|
+
start_line = lines.find_index { |line| line.include?("expect(1)") } + 1
|
|
663
|
+
end_line = lines.find_index { |line| line.include?("expect(2)") } + 1
|
|
664
|
+
|
|
665
|
+
stdout, stderr, status = Open3.capture3(
|
|
666
|
+
{ "RUBYLIB" => File.expand_path("../lib", __dir__) },
|
|
667
|
+
"ruby",
|
|
668
|
+
File.expand_path("../exe/smartest", __dir__),
|
|
669
|
+
"smartest/sample_test.rb:#{start_line}-#{end_line}",
|
|
670
|
+
chdir: dir
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
expect(status.success?).to eq(true)
|
|
674
|
+
expect(stderr).to eq("")
|
|
675
|
+
expect(stdout).to include("Running 2 tests")
|
|
676
|
+
expect(stdout).to include("range one")
|
|
677
|
+
expect(stdout).to include("range two")
|
|
678
|
+
expect(stdout).not_to include("range three")
|
|
679
|
+
expect(stdout).to include("2 tests, 2 passed, 0 failed")
|
|
680
|
+
end
|
|
681
|
+
end
|
|
682
|
+
|
|
480
683
|
test("cli default suite ignores minitest-style test directory") do
|
|
481
684
|
Dir.mktmpdir do |dir|
|
|
482
685
|
smartest_dir = File.join(dir, "smartest")
|
|
@@ -541,6 +744,7 @@ test("cli prints help") do
|
|
|
541
744
|
expect(stderr).to eq("")
|
|
542
745
|
expect(stdout).to include("Usage:")
|
|
543
746
|
expect(stdout).to include("smartest [paths...]")
|
|
747
|
+
expect(stdout).to include("smartest path/to/test_file.rb:line[-line]")
|
|
544
748
|
expect(stdout).to include("smartest --init")
|
|
545
749
|
expect(stdout).to include("smartest/**/*_test.rb")
|
|
546
750
|
end
|
|
@@ -559,11 +763,17 @@ test("cli initializes a runnable test scaffold") do
|
|
|
559
763
|
expect(stderr).to eq("")
|
|
560
764
|
expect(stdout).to include("create smartest")
|
|
561
765
|
expect(stdout).to include("create smartest/fixtures")
|
|
766
|
+
expect(stdout).to include("create smartest/matchers")
|
|
562
767
|
expect(stdout).to include("create smartest/test_helper.rb")
|
|
768
|
+
expect(stdout).to include("create smartest/matchers/predicate_matcher.rb")
|
|
563
769
|
expect(stdout).to include("create smartest/example_test.rb")
|
|
564
770
|
helper_contents = File.read(File.join(dir, "smartest/test_helper.rb"))
|
|
565
771
|
expect(helper_contents).to include('require "smartest/autorun"')
|
|
566
772
|
expect(helper_contents).to include('Dir[File.join(__dir__, "fixtures", "**", "*.rb")].sort.each')
|
|
773
|
+
expect(helper_contents).to include('Dir[File.join(__dir__, "matchers", "**", "*.rb")].sort.each')
|
|
774
|
+
expect(helper_contents).to include("use_matcher PredicateMatcher")
|
|
775
|
+
predicate_matcher_contents = File.read(File.join(dir, "smartest/matchers/predicate_matcher.rb"))
|
|
776
|
+
expect(predicate_matcher_contents).to include("module PredicateMatcher")
|
|
567
777
|
expect(File.read(File.join(dir, "smartest/example_test.rb"))).to include('require "test_helper"')
|
|
568
778
|
|
|
569
779
|
nested_fixtures_dir = File.join(dir, "smartest/fixtures/nested")
|
|
@@ -586,6 +796,54 @@ test("cli initializes a runnable test scaffold") do
|
|
|
586
796
|
end
|
|
587
797
|
RUBY
|
|
588
798
|
|
|
799
|
+
nested_matchers_dir = File.join(dir, "smartest/matchers/nested")
|
|
800
|
+
FileUtils.mkdir_p(nested_matchers_dir)
|
|
801
|
+
File.write(File.join(nested_matchers_dir, "auto_loaded_matcher.rb"), <<~RUBY)
|
|
802
|
+
module AutoLoadedMatcher
|
|
803
|
+
class Matcher
|
|
804
|
+
def initialize(expected)
|
|
805
|
+
@expected = expected
|
|
806
|
+
end
|
|
807
|
+
|
|
808
|
+
def matches?(actual)
|
|
809
|
+
@actual = actual
|
|
810
|
+
actual == @expected
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
def failure_message
|
|
814
|
+
"expected \#{@actual.inspect} to auto-eq \#{@expected.inspect}"
|
|
815
|
+
end
|
|
816
|
+
|
|
817
|
+
def negated_failure_message
|
|
818
|
+
"expected \#{@actual.inspect} not to auto-eq \#{@expected.inspect}"
|
|
819
|
+
end
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
def auto_eq(expected)
|
|
823
|
+
Matcher.new(expected)
|
|
824
|
+
end
|
|
825
|
+
end
|
|
826
|
+
RUBY
|
|
827
|
+
|
|
828
|
+
File.write(File.join(dir, "smartest/auto_loaded_matcher_test.rb"), <<~RUBY)
|
|
829
|
+
require "test_helper"
|
|
830
|
+
|
|
831
|
+
use_matcher AutoLoadedMatcher
|
|
832
|
+
|
|
833
|
+
test("auto-loaded matcher") do
|
|
834
|
+
expect("loaded from smartest/matchers").to auto_eq("loaded from smartest/matchers")
|
|
835
|
+
end
|
|
836
|
+
RUBY
|
|
837
|
+
|
|
838
|
+
File.write(File.join(dir, "smartest/predicate_matcher_test.rb"), <<~RUBY)
|
|
839
|
+
require "test_helper"
|
|
840
|
+
|
|
841
|
+
test("generated predicate matcher") do
|
|
842
|
+
expect("").to be_empty
|
|
843
|
+
expect(2).to be_between(1, 3)
|
|
844
|
+
end
|
|
845
|
+
RUBY
|
|
846
|
+
|
|
589
847
|
run_stdout, run_stderr, run_status = Open3.capture3(
|
|
590
848
|
{ "RUBYLIB" => File.expand_path("../lib", __dir__) },
|
|
591
849
|
"ruby",
|
|
@@ -597,7 +855,9 @@ test("cli initializes a runnable test scaffold") do
|
|
|
597
855
|
expect(run_stderr).to eq("")
|
|
598
856
|
expect(run_stdout).to include("example")
|
|
599
857
|
expect(run_stdout).to include("auto-loaded fixture")
|
|
600
|
-
expect(run_stdout).to include("
|
|
858
|
+
expect(run_stdout).to include("auto-loaded matcher")
|
|
859
|
+
expect(run_stdout).to include("generated predicate matcher")
|
|
860
|
+
expect(run_stdout).to include("4 tests, 4 passed, 0 failed")
|
|
601
861
|
end
|
|
602
862
|
end
|
|
603
863
|
|
|
@@ -605,13 +865,17 @@ test("cli init does not overwrite existing scaffold files") do
|
|
|
605
865
|
Dir.mktmpdir do |dir|
|
|
606
866
|
smartest_dir = File.join(dir, "smartest")
|
|
607
867
|
fixture_dir = File.join(smartest_dir, "fixtures")
|
|
868
|
+
matcher_dir = File.join(smartest_dir, "matchers")
|
|
608
869
|
FileUtils.mkdir_p(fixture_dir)
|
|
870
|
+
FileUtils.mkdir_p(matcher_dir)
|
|
609
871
|
helper_path = File.join(smartest_dir, "test_helper.rb")
|
|
610
872
|
example_path = File.join(smartest_dir, "example_test.rb")
|
|
611
873
|
fixture_path = File.join(fixture_dir, "custom_fixture.rb")
|
|
874
|
+
matcher_path = File.join(matcher_dir, "predicate_matcher.rb")
|
|
612
875
|
File.write(helper_path, "# custom helper\n")
|
|
613
876
|
File.write(example_path, "# custom test\n")
|
|
614
877
|
File.write(fixture_path, "# custom fixture\n")
|
|
878
|
+
File.write(matcher_path, "# custom matcher\n")
|
|
615
879
|
|
|
616
880
|
stdout, stderr, status = Open3.capture3(
|
|
617
881
|
{ "RUBYLIB" => File.expand_path("../lib", __dir__) },
|
|
@@ -625,10 +889,13 @@ test("cli init does not overwrite existing scaffold files") do
|
|
|
625
889
|
expect(stderr).to eq("")
|
|
626
890
|
expect(stdout).to include("exist smartest")
|
|
627
891
|
expect(stdout).to include("exist smartest/fixtures")
|
|
892
|
+
expect(stdout).to include("exist smartest/matchers")
|
|
628
893
|
expect(stdout).to include("exist smartest/test_helper.rb")
|
|
894
|
+
expect(stdout).to include("exist smartest/matchers/predicate_matcher.rb")
|
|
629
895
|
expect(stdout).to include("exist smartest/example_test.rb")
|
|
630
896
|
expect(File.read(helper_path)).to eq("# custom helper\n")
|
|
631
897
|
expect(File.read(example_path)).to eq("# custom test\n")
|
|
632
898
|
expect(File.read(fixture_path)).to eq("# custom fixture\n")
|
|
899
|
+
expect(File.read(matcher_path)).to eq("# custom matcher\n")
|
|
633
900
|
end
|
|
634
901
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: smartest
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.0.
|
|
4
|
+
version: 0.1.0.alpha3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yusuke Iwaki
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake
|
|
@@ -42,6 +42,7 @@ files:
|
|
|
42
42
|
- exe/smartest
|
|
43
43
|
- lib/smartest.rb
|
|
44
44
|
- lib/smartest/autorun.rb
|
|
45
|
+
- lib/smartest/cli_arguments.rb
|
|
45
46
|
- lib/smartest/dsl.rb
|
|
46
47
|
- lib/smartest/errors.rb
|
|
47
48
|
- lib/smartest/execution_context.rb
|
|
@@ -52,6 +53,7 @@ files:
|
|
|
52
53
|
- lib/smartest/fixture_definition.rb
|
|
53
54
|
- lib/smartest/fixture_set.rb
|
|
54
55
|
- lib/smartest/init_generator.rb
|
|
56
|
+
- lib/smartest/matcher_registry.rb
|
|
55
57
|
- lib/smartest/matchers.rb
|
|
56
58
|
- lib/smartest/parameter_extractor.rb
|
|
57
59
|
- lib/smartest/reporter.rb
|