smartest 0.2.1 → 0.3.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.
- checksums.yaml +4 -4
- data/README.md +37 -1
- data/SMARTEST_DESIGN.md +36 -4
- data/exe/smartest +4 -4
- data/lib/smartest/cli_arguments.rb +4 -8
- data/lib/smartest/expectation_target.rb +4 -0
- data/lib/smartest/init_generator.rb +3 -3
- data/lib/smartest/matchers.rb +194 -10
- data/lib/smartest/version.rb +1 -1
- data/smartest/smartest_test.rb +132 -11
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 47237b7d39a7ec1c0daad3d8f6d63d14ab075bf976c748f21422af3bd64f235e
|
|
4
|
+
data.tar.gz: e665af0f114097daca5ffff0536074c8724c88a7fc6bb2355ced9395e8394355
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8a519183ebb6e32add6cca824a9bfead561a00b4be7f953fe54a1f7255e55cf2490ed5aeb0303d08a9059a6c236a0ad20b46b309bef1d57ddc8abe325b03ae1b
|
|
7
|
+
data.tar.gz: 45dec0f33a22a66382c8ded8647f5cefa33344cb1ac2e38327d62e61e772dac6cc79233732287af214e6de8257b80a98c84cf4b4fd2f9ab34f96e07bc09e49d1
|
data/README.md
CHANGED
|
@@ -98,6 +98,14 @@ bundle exec smartest smartest/user_test.rb:12
|
|
|
98
98
|
bundle exec smartest smartest/user_test.rb:3-12
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
+
Smartest prints the 5 slowest tests after each CLI run by default. Use
|
|
102
|
+
`--profile N` to choose a different count:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
bundle exec smartest --profile 10
|
|
106
|
+
bundle exec smartest --profile 3 smartest/user_test.rb
|
|
107
|
+
```
|
|
108
|
+
|
|
101
109
|
CLI help and version output are available with:
|
|
102
110
|
|
|
103
111
|
```bash
|
|
@@ -105,13 +113,17 @@ bundle exec smartest --help
|
|
|
105
113
|
bundle exec smartest --version
|
|
106
114
|
```
|
|
107
115
|
|
|
108
|
-
|
|
116
|
+
Output resembles:
|
|
109
117
|
|
|
110
118
|
```text
|
|
111
119
|
Running 1 test
|
|
112
120
|
|
|
113
121
|
✓ example
|
|
114
122
|
|
|
123
|
+
Top 1 slowest test (0.00001 seconds, 100.0% of total time):
|
|
124
|
+
example
|
|
125
|
+
0.00001 seconds .../smartest/example_test.rb:3
|
|
126
|
+
|
|
115
127
|
1 test, 1 passed, 0 failed
|
|
116
128
|
```
|
|
117
129
|
|
|
@@ -182,6 +194,8 @@ expect { action }.to raise_error(ErrorClass)
|
|
|
182
194
|
expect { action }.to raise_error(/message/)
|
|
183
195
|
expect { action }.to raise_error(ErrorClass, /message/)
|
|
184
196
|
expect { action }.to change { value }
|
|
197
|
+
expect(actual).to matcher.or(other_matcher)
|
|
198
|
+
expect(actual).to matcher.and(other_matcher)
|
|
185
199
|
```
|
|
186
200
|
|
|
187
201
|
Examples:
|
|
@@ -249,6 +263,28 @@ specific order, preserve duplicate counts, and can use matcher objects such as
|
|
|
249
263
|
`change` is only supported with `expect { ... }` block expectations and must be
|
|
250
264
|
written with a value block.
|
|
251
265
|
|
|
266
|
+
Matchers can be composed with `.or` and `.and`:
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
expect(result).to include("NetworkError").or include("Failed to fetch")
|
|
270
|
+
expect(response.status).to eq(200).or(eq(304))
|
|
271
|
+
expect("report.txt").to start_with("report").and end_with(".txt")
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
`.or` passes when any matcher matches and short-circuits the right-hand matcher
|
|
275
|
+
when the left-hand matcher passes. `.and` passes only when every matcher matches.
|
|
276
|
+
`not_to` does not support composed matchers and raises `ArgumentError` when used
|
|
277
|
+
with `.and` or `.or`.
|
|
278
|
+
|
|
279
|
+
Composed `change` matchers observe one action block execution:
|
|
280
|
+
|
|
281
|
+
```ruby
|
|
282
|
+
expect {
|
|
283
|
+
count += 1
|
|
284
|
+
total += 1
|
|
285
|
+
}.to change { count }.by(1).and change { total }.by(1)
|
|
286
|
+
```
|
|
287
|
+
|
|
252
288
|
Custom matcher modules can be registered from `around_suite` or `around_test`
|
|
253
289
|
with `use_matcher`. The generated scaffold includes a `PredicateMatcher` custom
|
|
254
290
|
matcher for `be_<predicate>` calls. See [Matchers](documentation/docs/matchers.md).
|
data/SMARTEST_DESIGN.md
CHANGED
|
@@ -718,9 +718,11 @@ MVP expectation API:
|
|
|
718
718
|
expect(actual).to eq(expected)
|
|
719
719
|
expect(actual).not_to eq(expected)
|
|
720
720
|
expect { action }.to change { value }
|
|
721
|
+
expect(actual).to matcher.or(other_matcher)
|
|
722
|
+
expect(actual).to matcher.and(other_matcher)
|
|
721
723
|
```
|
|
722
724
|
|
|
723
|
-
Current built-in matchers include:
|
|
725
|
+
Current built-in matchers and composition helpers include:
|
|
724
726
|
|
|
725
727
|
```ruby
|
|
726
728
|
eq(expected)
|
|
@@ -737,6 +739,8 @@ raise_error(ErrorClass)
|
|
|
737
739
|
raise_error(/message/)
|
|
738
740
|
raise_error(ErrorClass, /message/)
|
|
739
741
|
change { value }
|
|
742
|
+
matcher.or(other_matcher)
|
|
743
|
+
matcher.and(other_matcher)
|
|
740
744
|
```
|
|
741
745
|
|
|
742
746
|
Internal model:
|
|
@@ -748,8 +752,17 @@ expect(actual)
|
|
|
748
752
|
eq(expected)
|
|
749
753
|
=> EqMatcher
|
|
750
754
|
|
|
755
|
+
EqMatcher
|
|
756
|
+
< Smartest::Matcher
|
|
757
|
+
|
|
758
|
+
Smartest::Matcher#or(other_matcher)
|
|
759
|
+
=> OrMatcher
|
|
760
|
+
|
|
761
|
+
Smartest::Matcher#and(other_matcher)
|
|
762
|
+
=> AndMatcher
|
|
763
|
+
|
|
751
764
|
ExpectationTarget#to(matcher)
|
|
752
|
-
=> matcher.
|
|
765
|
+
=> matcher.matches?(actual)
|
|
753
766
|
```
|
|
754
767
|
|
|
755
768
|
Example:
|
|
@@ -761,17 +774,36 @@ class Smartest::ExpectationTarget
|
|
|
761
774
|
end
|
|
762
775
|
|
|
763
776
|
def to(matcher)
|
|
764
|
-
matcher.
|
|
777
|
+
return self if matcher.matches?(@actual)
|
|
778
|
+
|
|
779
|
+
raise AssertionFailed, matcher.failure_message
|
|
765
780
|
end
|
|
766
781
|
|
|
767
782
|
def not_to(matcher)
|
|
768
|
-
matcher.
|
|
783
|
+
if matcher.respond_to?(:supports_negated_expectation?) && !matcher.supports_negated_expectation?
|
|
784
|
+
raise ArgumentError, matcher.negated_expectation_error
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
if matcher.respond_to?(:does_not_match?)
|
|
788
|
+
return self if matcher.does_not_match?(@actual)
|
|
789
|
+
|
|
790
|
+
raise AssertionFailed, matcher.negated_failure_message
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
return self unless matcher.matches?(@actual)
|
|
794
|
+
|
|
795
|
+
raise AssertionFailed, matcher.negated_failure_message
|
|
769
796
|
end
|
|
770
797
|
end
|
|
771
798
|
```
|
|
772
799
|
|
|
773
800
|
Assertion failures should raise `Smartest::AssertionFailed`.
|
|
774
801
|
|
|
802
|
+
Matcher composition is logical composition over the same actual value. `or`
|
|
803
|
+
passes when any matcher matches and should short-circuit later matchers after a
|
|
804
|
+
match. `and` passes only when every matcher matches. `not_to` with composed
|
|
805
|
+
matchers is intentionally unsupported and raises `ArgumentError`.
|
|
806
|
+
|
|
775
807
|
## Reporter
|
|
776
808
|
|
|
777
809
|
The initial reporter should be simple.
|
data/exe/smartest
CHANGED
|
@@ -7,15 +7,15 @@ require "smartest"
|
|
|
7
7
|
|
|
8
8
|
usage = <<~USAGE
|
|
9
9
|
Usage:
|
|
10
|
-
smartest [paths...]
|
|
11
|
-
smartest path/to/test_file.rb:line[-line]
|
|
12
|
-
smartest --profile [N]
|
|
10
|
+
smartest [--profile N] [paths...]
|
|
11
|
+
smartest [--profile N] path/to/test_file.rb:line[-line]
|
|
13
12
|
smartest --init
|
|
14
13
|
smartest --version
|
|
15
14
|
smartest --help
|
|
16
15
|
|
|
17
16
|
When no paths are given, Smartest loads smartest/**/*_test.rb.
|
|
18
|
-
|
|
17
|
+
Smartest prints the 5 slowest tests after the run by default.
|
|
18
|
+
Use --profile N to choose how many slowest tests are printed.
|
|
19
19
|
USAGE
|
|
20
20
|
|
|
21
21
|
command = :run
|
|
@@ -12,7 +12,7 @@ module Smartest
|
|
|
12
12
|
@files = []
|
|
13
13
|
@whole_files = Set.new
|
|
14
14
|
@line_filters = Hash.new { |hash, key| hash[key] = Set.new }
|
|
15
|
-
@profile_count =
|
|
15
|
+
@profile_count = DEFAULT_PROFILE_COUNT
|
|
16
16
|
|
|
17
17
|
paths = extract_options(argv)
|
|
18
18
|
parse_paths(paths.empty? ? ["smartest/**/*_test.rb"] : paths)
|
|
@@ -45,17 +45,13 @@ module Smartest
|
|
|
45
45
|
|
|
46
46
|
case argument
|
|
47
47
|
when "--profile"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
@profile_count = next_argument.to_i
|
|
48
|
+
if argv[index + 1]&.match?(/\A\d+\z/)
|
|
49
|
+
@profile_count = argv[index + 1].to_i
|
|
51
50
|
index += 2
|
|
52
51
|
else
|
|
53
|
-
|
|
52
|
+
paths << argument
|
|
54
53
|
index += 1
|
|
55
54
|
end
|
|
56
|
-
when /\A--profile=(\d+)\z/
|
|
57
|
-
@profile_count = Regexp.last_match(1).to_i
|
|
58
|
-
index += 1
|
|
59
55
|
else
|
|
60
56
|
paths << argument
|
|
61
57
|
index += 1
|
|
@@ -13,6 +13,10 @@ module Smartest
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def not_to(matcher)
|
|
16
|
+
if matcher.respond_to?(:supports_negated_expectation?) && !matcher.supports_negated_expectation?
|
|
17
|
+
raise ArgumentError, matcher.negated_expectation_error
|
|
18
|
+
end
|
|
19
|
+
|
|
16
20
|
if matcher.respond_to?(:does_not_match?)
|
|
17
21
|
return self if matcher.does_not_match?(@actual)
|
|
18
22
|
|
|
@@ -38,7 +38,7 @@ module Smartest
|
|
|
38
38
|
name.to_s.match?(/\\Abe_.+\\z/) || super
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
class Matcher
|
|
41
|
+
class Matcher < Smartest::Matcher
|
|
42
42
|
def initialize(predicate_name, arguments, block)
|
|
43
43
|
@predicate_name = predicate_name
|
|
44
44
|
@predicate = "\#{predicate_name}?"
|
|
@@ -63,14 +63,14 @@ module Smartest
|
|
|
63
63
|
"expected \#{@actual.inspect} not to be \#{description}"
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
-
private
|
|
67
|
-
|
|
68
66
|
def description
|
|
69
67
|
return @predicate_name if @arguments.empty?
|
|
70
68
|
|
|
71
69
|
"\#{@predicate_name} \#{argument_description}"
|
|
72
70
|
end
|
|
73
71
|
|
|
72
|
+
private
|
|
73
|
+
|
|
74
74
|
def argument_description
|
|
75
75
|
@arguments.map(&:inspect).join(", ")
|
|
76
76
|
end
|
data/lib/smartest/matchers.rb
CHANGED
|
@@ -54,7 +54,162 @@ module Smartest
|
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
-
class
|
|
57
|
+
class Matcher
|
|
58
|
+
def and(other_matcher)
|
|
59
|
+
AndMatcher.new(self, other_matcher)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def or(other_matcher)
|
|
63
|
+
OrMatcher.new(self, other_matcher)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def description
|
|
67
|
+
self.class.name || "matcher"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
class CompoundMatcher < Matcher
|
|
72
|
+
attr_reader :matchers
|
|
73
|
+
|
|
74
|
+
NEGATED_EXPECTATION_ERROR = "not_to does not support matcher composition with .and or .or"
|
|
75
|
+
|
|
76
|
+
def initialize(*matchers)
|
|
77
|
+
@matchers = matchers.flat_map do |matcher|
|
|
78
|
+
matcher.is_a?(self.class) ? matcher.matchers : matcher
|
|
79
|
+
end
|
|
80
|
+
reset_result
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def supports_negated_expectation?
|
|
84
|
+
false
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def negated_expectation_error
|
|
88
|
+
NEGATED_EXPECTATION_ERROR
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def reset_result
|
|
94
|
+
@actual = nil
|
|
95
|
+
@failed_matchers = []
|
|
96
|
+
@matched_matchers = []
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def actual_description
|
|
100
|
+
@actual.respond_to?(:call) ? "block" : @actual.inspect
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def matcher_description(matcher)
|
|
104
|
+
description = if matcher.respond_to?(:description)
|
|
105
|
+
matcher.description
|
|
106
|
+
else
|
|
107
|
+
matcher.inspect
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
matcher.is_a?(CompoundMatcher) ? "(#{description})" : description
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def joined_description(separator)
|
|
114
|
+
@matchers.map { |matcher| matcher_description(matcher) }.join(separator)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def failure_messages
|
|
118
|
+
@failed_matchers.filter_map do |matcher|
|
|
119
|
+
matcher.failure_message if matcher.respond_to?(:failure_message)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def composed_block_expectation?(actual)
|
|
124
|
+
actual.respond_to?(:call) &&
|
|
125
|
+
@matchers.all? { |matcher| matcher.respond_to?(:composable_block_expectation?) } &&
|
|
126
|
+
@matchers.all?(&:composable_block_expectation?)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def run_composed_block_expectation(actual)
|
|
130
|
+
@matchers.each(&:prepare_composed_block_expectation)
|
|
131
|
+
actual.call
|
|
132
|
+
@matchers.each(&:finish_composed_block_expectation)
|
|
133
|
+
@matchers.each do |matcher|
|
|
134
|
+
if matcher.composed_block_matches?
|
|
135
|
+
@matched_matchers << matcher
|
|
136
|
+
else
|
|
137
|
+
@failed_matchers << matcher
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
class AndMatcher < CompoundMatcher
|
|
144
|
+
def matches?(actual)
|
|
145
|
+
reset_result
|
|
146
|
+
@actual = actual
|
|
147
|
+
|
|
148
|
+
if composed_block_expectation?(actual)
|
|
149
|
+
run_composed_block_expectation(actual)
|
|
150
|
+
return @failed_matchers.empty?
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
@matchers.each do |matcher|
|
|
154
|
+
if matcher.matches?(actual)
|
|
155
|
+
@matched_matchers << matcher
|
|
156
|
+
else
|
|
157
|
+
@failed_matchers << matcher
|
|
158
|
+
return false
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
true
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def failure_message
|
|
166
|
+
message = "expected #{actual_description} to match all of #{description}"
|
|
167
|
+
details = failure_messages
|
|
168
|
+
details.empty? ? message : "#{message}; #{details.join('; ')}"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def negated_failure_message
|
|
172
|
+
"expected #{actual_description} not to match all of #{description}"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def description
|
|
176
|
+
joined_description(" and ")
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
class OrMatcher < CompoundMatcher
|
|
181
|
+
def matches?(actual)
|
|
182
|
+
reset_result
|
|
183
|
+
@actual = actual
|
|
184
|
+
|
|
185
|
+
@matchers.each do |matcher|
|
|
186
|
+
if matcher.matches?(actual)
|
|
187
|
+
@matched_matchers << matcher
|
|
188
|
+
return true
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
@failed_matchers << matcher
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
false
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def failure_message
|
|
198
|
+
message = "expected #{actual_description} to match any of #{description}"
|
|
199
|
+
details = failure_messages
|
|
200
|
+
details.empty? ? message : "#{message}; #{details.join('; ')}"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def negated_failure_message
|
|
204
|
+
"expected #{actual_description} not to match any of #{description}"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def description
|
|
208
|
+
joined_description(" or ")
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
class EqMatcher < Matcher
|
|
58
213
|
def initialize(expected)
|
|
59
214
|
@expected = expected
|
|
60
215
|
end
|
|
@@ -77,7 +232,7 @@ module Smartest
|
|
|
77
232
|
end
|
|
78
233
|
end
|
|
79
234
|
|
|
80
|
-
class IncludeMatcher
|
|
235
|
+
class IncludeMatcher < Matcher
|
|
81
236
|
def initialize(expected)
|
|
82
237
|
@expected = expected
|
|
83
238
|
end
|
|
@@ -102,7 +257,7 @@ module Smartest
|
|
|
102
257
|
end
|
|
103
258
|
end
|
|
104
259
|
|
|
105
|
-
class StartWithMatcher
|
|
260
|
+
class StartWithMatcher < Matcher
|
|
106
261
|
def initialize(*prefixes)
|
|
107
262
|
@prefixes = prefixes
|
|
108
263
|
end
|
|
@@ -135,7 +290,7 @@ module Smartest
|
|
|
135
290
|
end
|
|
136
291
|
end
|
|
137
292
|
|
|
138
|
-
class EndWithMatcher
|
|
293
|
+
class EndWithMatcher < Matcher
|
|
139
294
|
def initialize(*suffixes)
|
|
140
295
|
@suffixes = suffixes
|
|
141
296
|
end
|
|
@@ -168,7 +323,7 @@ module Smartest
|
|
|
168
323
|
end
|
|
169
324
|
end
|
|
170
325
|
|
|
171
|
-
class BeAKindOfMatcher
|
|
326
|
+
class BeAKindOfMatcher < Matcher
|
|
172
327
|
def initialize(expected_class)
|
|
173
328
|
@expected_class = expected_class
|
|
174
329
|
end
|
|
@@ -203,7 +358,7 @@ module Smartest
|
|
|
203
358
|
end
|
|
204
359
|
end
|
|
205
360
|
|
|
206
|
-
class BeNilMatcher
|
|
361
|
+
class BeNilMatcher < Matcher
|
|
207
362
|
def matches?(actual)
|
|
208
363
|
@actual = actual
|
|
209
364
|
actual.nil?
|
|
@@ -222,7 +377,7 @@ module Smartest
|
|
|
222
377
|
end
|
|
223
378
|
end
|
|
224
379
|
|
|
225
|
-
class MatchMatcher
|
|
380
|
+
class MatchMatcher < Matcher
|
|
226
381
|
def initialize(regexp)
|
|
227
382
|
@regexp = regexp
|
|
228
383
|
end
|
|
@@ -247,7 +402,7 @@ module Smartest
|
|
|
247
402
|
end
|
|
248
403
|
end
|
|
249
404
|
|
|
250
|
-
class ContainExactlyMatcher
|
|
405
|
+
class ContainExactlyMatcher < Matcher
|
|
251
406
|
def initialize(expected_items, matcher_name:)
|
|
252
407
|
@expected_items = expected_items
|
|
253
408
|
@matcher_name = matcher_name
|
|
@@ -369,7 +524,7 @@ module Smartest
|
|
|
369
524
|
end
|
|
370
525
|
end
|
|
371
526
|
|
|
372
|
-
class RaiseErrorMatcher
|
|
527
|
+
class RaiseErrorMatcher < Matcher
|
|
373
528
|
def initialize(*expected_error)
|
|
374
529
|
@expected_type = expected_type_for(expected_error)
|
|
375
530
|
@expected_error_class = expected_error.find { |item| error_class?(item) }
|
|
@@ -403,6 +558,10 @@ module Smartest
|
|
|
403
558
|
"expected block not to raise #{expected_description}, but raised #{actual_error_description}"
|
|
404
559
|
end
|
|
405
560
|
|
|
561
|
+
def description
|
|
562
|
+
"raise #{expected_description}"
|
|
563
|
+
end
|
|
564
|
+
|
|
406
565
|
private
|
|
407
566
|
|
|
408
567
|
def expected_type_for(expected_error)
|
|
@@ -446,7 +605,7 @@ module Smartest
|
|
|
446
605
|
end
|
|
447
606
|
end
|
|
448
607
|
|
|
449
|
-
class ChangeMatcher
|
|
608
|
+
class ChangeMatcher < Matcher
|
|
450
609
|
UNSET = Object.new
|
|
451
610
|
|
|
452
611
|
def initialize(value_block)
|
|
@@ -498,6 +657,31 @@ module Smartest
|
|
|
498
657
|
"expected value not to change, but #{observed_description}"
|
|
499
658
|
end
|
|
500
659
|
|
|
660
|
+
def description
|
|
661
|
+
expected_description
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
def composable_block_expectation?
|
|
665
|
+
true
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
def prepare_composed_block_expectation
|
|
669
|
+
reset_result
|
|
670
|
+
@callable = true
|
|
671
|
+
@before_value = @value_block.call
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
def finish_composed_block_expectation
|
|
675
|
+
@after_value = @value_block.call
|
|
676
|
+
calculate_delta if delta_expected?
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
def composed_block_matches?
|
|
680
|
+
return false unless @callable
|
|
681
|
+
|
|
682
|
+
positive_failures.empty?
|
|
683
|
+
end
|
|
684
|
+
|
|
501
685
|
private
|
|
502
686
|
|
|
503
687
|
def reset_result
|
data/lib/smartest/version.rb
CHANGED
data/smartest/smartest_test.rb
CHANGED
|
@@ -218,6 +218,74 @@ test("supports basic matchers") do
|
|
|
218
218
|
expect(status).to eq(0)
|
|
219
219
|
end
|
|
220
220
|
|
|
221
|
+
test("supports matcher composition with and and or") do
|
|
222
|
+
action_calls = 0
|
|
223
|
+
first_value = 0
|
|
224
|
+
second_value = 10
|
|
225
|
+
|
|
226
|
+
expect("NetworkError").to include("Network").or include("Failed to fetch")
|
|
227
|
+
expect(304).to eq(200).or(eq(201)).or(eq(304))
|
|
228
|
+
expect("report.txt").to start_with("report").and end_with(".txt")
|
|
229
|
+
expect {
|
|
230
|
+
action_calls += 1
|
|
231
|
+
first_value += 1
|
|
232
|
+
second_value += 1
|
|
233
|
+
}.to change { first_value }.by(1).and change { second_value }.by(1)
|
|
234
|
+
|
|
235
|
+
expect(action_calls).to eq(1)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
test("reports matcher composition failures") do
|
|
239
|
+
error = SmartestSelfTest.capture_error(Smartest::AssertionFailed) do
|
|
240
|
+
expect("permission denied").to include("NetworkError").or include("Failed to fetch")
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
expect(error.message).to include(
|
|
244
|
+
'expected "permission denied" to match any of include "NetworkError" or include "Failed to fetch"'
|
|
245
|
+
)
|
|
246
|
+
expect(error.message).to include('expected "permission denied" to include "NetworkError"')
|
|
247
|
+
expect(error.message).to include('expected "permission denied" to include "Failed to fetch"')
|
|
248
|
+
|
|
249
|
+
error = SmartestSelfTest.capture_error(Smartest::AssertionFailed) do
|
|
250
|
+
expect("error.log").to start_with("error").and end_with(".txt")
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
expect(error.message).to include('expected "error.log" to match all of start with "error" and end with ".txt"')
|
|
254
|
+
expect(error.message).to include('expected "error.log" to end with ".txt"')
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
test("rejects negated matcher composition") do
|
|
258
|
+
error = SmartestSelfTest.capture_error(ArgumentError) do
|
|
259
|
+
expect("public token").not_to include("password").or include("secret")
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
expect(error.message).to eq("not_to does not support matcher composition with .and or .or")
|
|
263
|
+
|
|
264
|
+
error = SmartestSelfTest.capture_error(ArgumentError) do
|
|
265
|
+
expect("report.log").not_to start_with("report").and end_with(".txt")
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
expect(error.message).to eq("not_to does not support matcher composition with .and or .or")
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
test("short-circuits or matcher composition") do
|
|
272
|
+
right_matcher = Class.new(Smartest::Matcher) do
|
|
273
|
+
def matches?(_actual)
|
|
274
|
+
raise "right matcher should not be evaluated"
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def failure_message
|
|
278
|
+
"right matcher failed"
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def negated_failure_message
|
|
282
|
+
"right matcher matched"
|
|
283
|
+
end
|
|
284
|
+
end.new
|
|
285
|
+
|
|
286
|
+
expect("ok").to eq("ok").or(right_matcher)
|
|
287
|
+
end
|
|
288
|
+
|
|
221
289
|
test("reports be_a and match matcher failures") do
|
|
222
290
|
suite = Smartest::Suite.new
|
|
223
291
|
suite.tests.add(
|
|
@@ -1368,7 +1436,7 @@ test("cli loads matcher files registered in test helper") do
|
|
|
1368
1436
|
RUBY
|
|
1369
1437
|
File.write(File.join(matchers_dir, "have_status_matcher.rb"), <<~RUBY)
|
|
1370
1438
|
module HaveStatusMatcher
|
|
1371
|
-
class MatcherImpl
|
|
1439
|
+
class MatcherImpl < Smartest::Matcher
|
|
1372
1440
|
def initialize(expected)
|
|
1373
1441
|
@expected = expected
|
|
1374
1442
|
end
|
|
@@ -1385,6 +1453,10 @@ test("cli loads matcher files registered in test helper") do
|
|
|
1385
1453
|
def negated_failure_message
|
|
1386
1454
|
"expected \#{@actual.inspect} not to have status \#{@expected.inspect}"
|
|
1387
1455
|
end
|
|
1456
|
+
|
|
1457
|
+
def description
|
|
1458
|
+
"have status \#{@expected.inspect}"
|
|
1459
|
+
end
|
|
1388
1460
|
end
|
|
1389
1461
|
|
|
1390
1462
|
def have_status(expected)
|
|
@@ -1566,10 +1638,10 @@ test("cli prints help") do
|
|
|
1566
1638
|
expect(status.success?).to eq(true)
|
|
1567
1639
|
expect(stderr).to eq("")
|
|
1568
1640
|
expect(stdout).to include("Usage:")
|
|
1569
|
-
expect(stdout).to include("smartest [paths...]")
|
|
1570
|
-
expect(stdout).to include("smartest path/to/test_file.rb:line[-line]")
|
|
1641
|
+
expect(stdout).to include("smartest [--profile N] [paths...]")
|
|
1642
|
+
expect(stdout).to include("smartest [--profile N] path/to/test_file.rb:line[-line]")
|
|
1571
1643
|
expect(stdout).to include("smartest --init")
|
|
1572
|
-
expect(stdout).to include("
|
|
1644
|
+
expect(stdout).to include("Use --profile N")
|
|
1573
1645
|
expect(stdout).to include("smartest/**/*_test.rb")
|
|
1574
1646
|
end
|
|
1575
1647
|
|
|
@@ -1664,15 +1736,58 @@ test("--profile is not printed when there are no results") do
|
|
|
1664
1736
|
expect(output.string).not_to include("slowest")
|
|
1665
1737
|
end
|
|
1666
1738
|
|
|
1667
|
-
test("CLIArguments parses --profile
|
|
1668
|
-
|
|
1739
|
+
test("CLIArguments defaults profile count and parses --profile N") do
|
|
1740
|
+
arguments = Smartest::CLIArguments.new([])
|
|
1741
|
+
|
|
1742
|
+
expect(arguments.profile_count).to eq(5)
|
|
1743
|
+
expect(arguments.files).to eq(Dir["smartest/**/*_test.rb"])
|
|
1669
1744
|
expect(Smartest::CLIArguments.new(["--profile", "3"]).profile_count).to eq(3)
|
|
1670
|
-
expect(Smartest::CLIArguments.new(["--profile=7"]).profile_count).to eq(7)
|
|
1671
|
-
expect(Smartest::CLIArguments.new([]).profile_count).to eq(nil)
|
|
1672
|
-
expect(Smartest::CLIArguments.new(["--profile", "smartest/foo_test.rb"]).profile_count).to eq(5)
|
|
1673
1745
|
end
|
|
1674
1746
|
|
|
1675
|
-
test("
|
|
1747
|
+
test("CLIArguments leaves unsupported profile forms as paths") do
|
|
1748
|
+
equals_form = Smartest::CLIArguments.new(["--profile=7"])
|
|
1749
|
+
missing_count = Smartest::CLIArguments.new(["--profile"])
|
|
1750
|
+
path_after_profile = Smartest::CLIArguments.new(["--profile", "smartest/foo_test.rb"])
|
|
1751
|
+
|
|
1752
|
+
expect(equals_form.profile_count).to eq(5)
|
|
1753
|
+
expect(equals_form.files).to eq(["--profile=7"])
|
|
1754
|
+
expect(missing_count.profile_count).to eq(5)
|
|
1755
|
+
expect(missing_count.files).to eq(["--profile"])
|
|
1756
|
+
expect(path_after_profile.profile_count).to eq(5)
|
|
1757
|
+
expect(path_after_profile.files).to eq(["--profile", "smartest/foo_test.rb"])
|
|
1758
|
+
end
|
|
1759
|
+
|
|
1760
|
+
test("cli runs without --profile N and prints default slowest tests") do
|
|
1761
|
+
Dir.mktmpdir do |dir|
|
|
1762
|
+
smartest_dir = File.join(dir, "smartest")
|
|
1763
|
+
FileUtils.mkdir_p(smartest_dir)
|
|
1764
|
+
File.write(File.join(smartest_dir, "sample_test.rb"), <<~RUBY)
|
|
1765
|
+
require "smartest/autorun"
|
|
1766
|
+
|
|
1767
|
+
test("default first") do
|
|
1768
|
+
expect(1).to eq(1)
|
|
1769
|
+
end
|
|
1770
|
+
|
|
1771
|
+
test("default second") do
|
|
1772
|
+
expect(1).to eq(1)
|
|
1773
|
+
end
|
|
1774
|
+
RUBY
|
|
1775
|
+
|
|
1776
|
+
stdout, stderr, status = Open3.capture3(
|
|
1777
|
+
{ "RUBYLIB" => File.expand_path("../lib", __dir__) },
|
|
1778
|
+
"ruby",
|
|
1779
|
+
File.expand_path("../exe/smartest", __dir__),
|
|
1780
|
+
chdir: dir
|
|
1781
|
+
)
|
|
1782
|
+
|
|
1783
|
+
expect(status.success?).to eq(true)
|
|
1784
|
+
expect(stderr).to eq("")
|
|
1785
|
+
expect(stdout).to include("Top 2 slowest tests")
|
|
1786
|
+
expect(stdout).to include("2 tests, 2 passed, 0 failed")
|
|
1787
|
+
end
|
|
1788
|
+
end
|
|
1789
|
+
|
|
1790
|
+
test("cli runs with --profile N and prints requested slowest tests") do
|
|
1676
1791
|
Dir.mktmpdir do |dir|
|
|
1677
1792
|
smartest_dir = File.join(dir, "smartest")
|
|
1678
1793
|
FileUtils.mkdir_p(smartest_dir)
|
|
@@ -1730,6 +1845,7 @@ test("cli initializes a runnable test scaffold") do
|
|
|
1730
1845
|
expect(helper_contents).to include("use_matcher PredicateMatcher")
|
|
1731
1846
|
predicate_matcher_contents = File.read(File.join(dir, "smartest/matchers/predicate_matcher.rb"))
|
|
1732
1847
|
expect(predicate_matcher_contents).to include("module PredicateMatcher")
|
|
1848
|
+
expect(predicate_matcher_contents).to include("class Matcher < Smartest::Matcher")
|
|
1733
1849
|
expect(File.read(File.join(dir, "smartest/example_test.rb"))).to include('require "test_helper"')
|
|
1734
1850
|
|
|
1735
1851
|
nested_fixtures_dir = File.join(dir, "smartest/fixtures/nested")
|
|
@@ -1759,7 +1875,7 @@ test("cli initializes a runnable test scaffold") do
|
|
|
1759
1875
|
FileUtils.mkdir_p(nested_matchers_dir)
|
|
1760
1876
|
File.write(File.join(nested_matchers_dir, "auto_loaded_matcher.rb"), <<~RUBY)
|
|
1761
1877
|
module AutoLoadedMatcher
|
|
1762
|
-
class Matcher
|
|
1878
|
+
class Matcher < Smartest::Matcher
|
|
1763
1879
|
def initialize(expected)
|
|
1764
1880
|
@expected = expected
|
|
1765
1881
|
end
|
|
@@ -1776,6 +1892,10 @@ test("cli initializes a runnable test scaffold") do
|
|
|
1776
1892
|
def negated_failure_message
|
|
1777
1893
|
"expected \#{@actual.inspect} not to auto-eq \#{@expected.inspect}"
|
|
1778
1894
|
end
|
|
1895
|
+
|
|
1896
|
+
def description
|
|
1897
|
+
"auto-eq \#{@expected.inspect}"
|
|
1898
|
+
end
|
|
1779
1899
|
end
|
|
1780
1900
|
|
|
1781
1901
|
def auto_eq(expected)
|
|
@@ -1803,6 +1923,7 @@ test("cli initializes a runnable test scaffold") do
|
|
|
1803
1923
|
test("generated predicate matcher") do
|
|
1804
1924
|
expect("").to be_empty
|
|
1805
1925
|
expect(2).to be_between(1, 3)
|
|
1926
|
+
expect(2).to be_odd.or be_even
|
|
1806
1927
|
end
|
|
1807
1928
|
RUBY
|
|
1808
1929
|
|
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.
|
|
4
|
+
version: 0.3.0.alpha1
|
|
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-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake
|
|
@@ -91,9 +91,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
91
91
|
version: '3.1'
|
|
92
92
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
93
93
|
requirements:
|
|
94
|
-
- - "
|
|
94
|
+
- - ">"
|
|
95
95
|
- !ruby/object:Gem::Version
|
|
96
|
-
version:
|
|
96
|
+
version: 1.3.1
|
|
97
97
|
requirements: []
|
|
98
98
|
rubygems_version: 3.4.19
|
|
99
99
|
signing_key:
|