smartest 0.2.0.alpha2 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e0953c11bf61f80c4a310701920e5b6a22727149ed135d86678b3e49b25c8a92
4
- data.tar.gz: d51839a9cb597da3d63a15aa803661946ceb3ae2a6982317ea108797f4203380
3
+ metadata.gz: 17a325d82883047881ac13538f95b3af038e361668869860034acc530ce2e398
4
+ data.tar.gz: 19c6a1a345e7e0fd02a12ae007a48f38c1be264debd3b23b0b6085d687a867bf
5
5
  SHA512:
6
- metadata.gz: 2e3be2ebdc5b116e6f16e396d4befda198c0063bb4da6e7f487266642d1edd8083bcc4dce9b37746969fdcbd9916424068f043344cc732edf4738f849fb87d9c
7
- data.tar.gz: 0c299af8b8dd05fac80ec1eefaeade8f747f8d2776e87ba6cb684ff84c619be8a21dfd3e33c99fa43d05ee0d40aabc361f86ab1e33af696a5a9ef4c9ee78054d
6
+ metadata.gz: 5b4aeb31472fca3b0911dfb29d4034405f493ac37e3019888a67f1c13c0b1f4aac99dc33b87c7e79a8d363b7bf12f7682b97bea396ebc56b558b592a7658eee9
7
+ data.tar.gz: 4e1c0cc3b739b62af8c4eeef394662f8115e35f890cdde87ce32117eca26c4064b627a08a2ca204c856276d2a603de8c4c55e2f9c71d2f68c44fd864e13c2ecc
data/CHANGELOG.md CHANGED
@@ -8,7 +8,7 @@
8
8
  - Support required keyword-argument fixture injection and fixture dependencies.
9
9
  - Support per-test fixture caching and cleanup.
10
10
  - Support suite-scoped fixtures through `suite_fixture`.
11
- - Support `eq`, `include`, `be_nil`, and `raise_error` matchers.
11
+ - Support `eq`, `include`, `start_with`, `end_with`, `be_nil`, `raise_error`, and `change` matchers.
12
12
  - Support custom matcher modules through `use_matcher`.
13
13
  - Generate an opt-in `PredicateMatcher` custom matcher for `be_<predicate>` calls.
14
14
  - Add the `smartest` CLI.
data/README.md CHANGED
@@ -178,6 +178,8 @@ Smartest uses an expectation style:
178
178
  ```ruby
179
179
  expect(actual).to eq(expected)
180
180
  expect(actual).not_to eq(expected)
181
+ expect { action }.to raise_error(ErrorClass)
182
+ expect { action }.to change { value }
181
183
  ```
182
184
 
183
185
  Examples:
@@ -190,6 +192,14 @@ end
190
192
  test("array") do
191
193
  expect([1, 2, 3]).to include(2)
192
194
  end
195
+
196
+ test("URL") do
197
+ expect("about:blank").to start_with("about:")
198
+ end
199
+
200
+ test("download") do
201
+ expect("screenshot.png").to end_with(".png")
202
+ end
193
203
  ```
194
204
 
195
205
  Supported matchers include:
@@ -197,10 +207,18 @@ Supported matchers include:
197
207
  ```ruby
198
208
  eq(expected)
199
209
  include(expected)
210
+ start_with(prefix, ...)
211
+ end_with(suffix, ...)
200
212
  be_nil
201
213
  raise_error(ErrorClass)
214
+ change { value }
215
+ change { value }.from(before).to(after)
216
+ change { value }.by(delta)
202
217
  ```
203
218
 
219
+ `change` is only supported with `expect { ... }` block expectations and must be
220
+ written with a value block.
221
+
204
222
  Custom matcher modules can be registered from `around_suite` or `around_test`
205
223
  with `use_matcher`. The generated scaffold includes a `PredicateMatcher` custom
206
224
  matcher for `be_<predicate>` calls. See [Matchers](documentation/docs/matchers.md).
data/SMARTEST_DESIGN.md CHANGED
@@ -717,6 +717,19 @@ MVP expectation API:
717
717
  ```ruby
718
718
  expect(actual).to eq(expected)
719
719
  expect(actual).not_to eq(expected)
720
+ expect { action }.to change { value }
721
+ ```
722
+
723
+ Current built-in matchers include:
724
+
725
+ ```ruby
726
+ eq(expected)
727
+ include(expected)
728
+ start_with(prefix, ...)
729
+ end_with(suffix, ...)
730
+ be_nil
731
+ raise_error(ErrorClass)
732
+ change { value }
720
733
  ```
721
734
 
722
735
  Internal model:
@@ -1235,8 +1248,6 @@ Possible future features:
1235
1248
  - custom reporters
1236
1249
  - JSON output
1237
1250
  - richer matchers
1238
- - block expectations
1239
- - `raise_error`
1240
1251
  - file-scoped fixtures
1241
1252
  - parallel execution
1242
1253
  - watch mode
@@ -13,6 +13,12 @@ module Smartest
13
13
  end
14
14
 
15
15
  def not_to(matcher)
16
+ if matcher.respond_to?(:does_not_match?)
17
+ return self if matcher.does_not_match?(@actual)
18
+
19
+ raise AssertionFailed, matcher.negated_failure_message
20
+ end
21
+
16
22
  return self unless matcher.matches?(@actual)
17
23
 
18
24
  raise AssertionFailed, matcher.negated_failure_message
@@ -10,6 +10,14 @@ module Smartest
10
10
  IncludeMatcher.new(expected)
11
11
  end
12
12
 
13
+ def start_with(*prefixes)
14
+ StartWithMatcher.new(*prefixes)
15
+ end
16
+
17
+ def end_with(*suffixes)
18
+ EndWithMatcher.new(*suffixes)
19
+ end
20
+
13
21
  def be_nil
14
22
  BeNilMatcher.new
15
23
  end
@@ -17,6 +25,13 @@ module Smartest
17
25
  def raise_error(expected_error = StandardError)
18
26
  RaiseErrorMatcher.new(expected_error)
19
27
  end
28
+
29
+ def change(*args, &block)
30
+ raise ArgumentError, "change does not support arguments; use change { ... }" if args.any?
31
+ raise ArgumentError, "change requires a block" unless block
32
+
33
+ ChangeMatcher.new(block)
34
+ end
20
35
  end
21
36
 
22
37
  class EqMatcher
@@ -59,6 +74,64 @@ module Smartest
59
74
  end
60
75
  end
61
76
 
77
+ class StartWithMatcher
78
+ def initialize(*prefixes)
79
+ @prefixes = prefixes
80
+ end
81
+
82
+ def matches?(actual)
83
+ @actual = actual
84
+ actual.start_with?(*@prefixes)
85
+ rescue NoMethodError
86
+ false
87
+ end
88
+
89
+ def failure_message
90
+ "expected #{@actual.inspect} to start with #{expected_description}"
91
+ end
92
+
93
+ def negated_failure_message
94
+ "expected #{@actual.inspect} not to start with #{expected_description}"
95
+ end
96
+
97
+ private
98
+
99
+ def expected_description
100
+ return "no prefixes" if @prefixes.empty?
101
+
102
+ @prefixes.map(&:inspect).join(" or ")
103
+ end
104
+ end
105
+
106
+ class EndWithMatcher
107
+ def initialize(*suffixes)
108
+ @suffixes = suffixes
109
+ end
110
+
111
+ def matches?(actual)
112
+ @actual = actual
113
+ actual.end_with?(*@suffixes)
114
+ rescue NoMethodError
115
+ false
116
+ end
117
+
118
+ def failure_message
119
+ "expected #{@actual.inspect} to end with #{expected_description}"
120
+ end
121
+
122
+ def negated_failure_message
123
+ "expected #{@actual.inspect} not to end with #{expected_description}"
124
+ end
125
+
126
+ private
127
+
128
+ def expected_description
129
+ return "no suffixes" if @suffixes.empty?
130
+
131
+ @suffixes.map(&:inspect).join(" or ")
132
+ end
133
+ end
134
+
62
135
  class BeNilMatcher
63
136
  def matches?(actual)
64
137
  @actual = actual
@@ -106,4 +179,140 @@ module Smartest
106
179
  "expected block not to raise #{@expected_error}, but raised #{@actual_error.class}: #{@actual_error.message}"
107
180
  end
108
181
  end
182
+
183
+ class ChangeMatcher
184
+ UNSET = Object.new
185
+
186
+ def initialize(value_block)
187
+ @value_block = value_block
188
+ @expected_from = UNSET
189
+ @expected_to = UNSET
190
+ @expected_delta = UNSET
191
+ reset_result
192
+ end
193
+
194
+ def from(expected)
195
+ @expected_from = expected
196
+ self
197
+ end
198
+
199
+ def to(expected)
200
+ @expected_to = expected
201
+ self
202
+ end
203
+
204
+ def by(expected_delta)
205
+ @expected_delta = expected_delta
206
+ self
207
+ end
208
+
209
+ def matches?(actual)
210
+ run_change(actual)
211
+ return false unless @callable
212
+
213
+ positive_failures.empty?
214
+ end
215
+
216
+ def does_not_match?(actual)
217
+ run_change(actual)
218
+ return false unless @callable
219
+
220
+ negated_failures.empty?
221
+ end
222
+
223
+ def failure_message
224
+ return "expected a block to change value" unless @callable
225
+
226
+ "expected value to #{expected_description}, but #{observed_description}#{failed_modifier_description}"
227
+ end
228
+
229
+ def negated_failure_message
230
+ return "expected a block not to change value" unless @callable
231
+
232
+ "expected value not to change, but #{observed_description}"
233
+ end
234
+
235
+ private
236
+
237
+ def reset_result
238
+ @callable = true
239
+ @before_value = nil
240
+ @after_value = nil
241
+ @actual_delta = UNSET
242
+ @failed_modifiers = []
243
+ end
244
+
245
+ def run_change(actual)
246
+ reset_result
247
+ @callable = actual.respond_to?(:call)
248
+ return unless @callable
249
+
250
+ @before_value = @value_block.call
251
+ actual.call
252
+ @after_value = @value_block.call
253
+ calculate_delta if delta_expected?
254
+ end
255
+
256
+ def positive_failures
257
+ @failed_modifiers = []
258
+ @failed_modifiers << "change" if !delta_expected? && @before_value == @after_value
259
+ @failed_modifiers << "from(#{@expected_from.inspect})" if from_expected? && @before_value != @expected_from
260
+ @failed_modifiers << "to(#{@expected_to.inspect})" if to_expected? && @after_value != @expected_to
261
+ @failed_modifiers << "by(#{@expected_delta.inspect})" if delta_expected? && @actual_delta != @expected_delta
262
+ @failed_modifiers
263
+ end
264
+
265
+ def negated_failures
266
+ @failed_modifiers = []
267
+ @failed_modifiers << "change" unless @before_value == @after_value
268
+ @failed_modifiers
269
+ end
270
+
271
+ def expected_description
272
+ parts = ["change"]
273
+ parts << "from #{@expected_from.inspect}" if from_expected?
274
+ parts << "to #{@expected_to.inspect}" if to_expected?
275
+ parts << "by #{@expected_delta.inspect}" if delta_expected?
276
+ parts.join(" ")
277
+ end
278
+
279
+ def observed_description
280
+ if delta_expected?
281
+ delta_description = if @actual_delta.equal?(UNSET)
282
+ "could not calculate a numeric difference"
283
+ else
284
+ "changed by #{@actual_delta.inspect}"
285
+ end
286
+
287
+ "#{delta_description} from #{@before_value.inspect} before to #{@after_value.inspect} after"
288
+ else
289
+ "was #{@before_value.inspect} before and #{@after_value.inspect} after"
290
+ end
291
+ end
292
+
293
+ def failed_modifier_description
294
+ failures = positive_failures
295
+ return "" if failures.empty?
296
+
297
+ "; failed modifiers: #{failures.join(', ')}"
298
+ end
299
+
300
+ def calculate_delta
301
+ @actual_delta = @after_value - @before_value
302
+ rescue NoMethodError, TypeError
303
+ @actual_delta = UNSET
304
+ end
305
+
306
+ def from_expected?
307
+ !@expected_from.equal?(UNSET)
308
+ end
309
+
310
+ def to_expected?
311
+ !@expected_to.equal?(UNSET)
312
+ end
313
+
314
+ def delta_expected?
315
+ !@expected_delta.equal?(UNSET)
316
+ end
317
+ end
109
318
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Smartest
4
- VERSION = "0.2.0.alpha2"
4
+ VERSION = "0.2.0.alpha3"
5
5
  end
@@ -172,6 +172,15 @@ test("supports basic matchers") do
172
172
  "matchers",
173
173
  proc do
174
174
  expect([1, 2, 3]).to include(2)
175
+ expect("about:blank").to start_with("about:")
176
+ expect("https://cdn-b.test/assets/app.js").to start_with(
177
+ "https://cdn-a.test",
178
+ "https://cdn-b.test"
179
+ )
180
+ expect("screenshot.png").to end_with(".jpg", ".png")
181
+ expect("https://example.test").not_to start_with("about:")
182
+ expect("report.txt").not_to end_with(".png")
183
+ expect(Object.new).not_to start_with("prefix")
175
184
  expect(nil).to be_nil
176
185
  expect("value").not_to be_nil
177
186
  expect { raise ArgumentError, "bad" }.to raise_error(ArgumentError)
@@ -184,6 +193,93 @@ test("supports basic matchers") do
184
193
  expect(status).to eq(0)
185
194
  end
186
195
 
196
+ test("reports start_with and end_with matcher failures") do
197
+ suite = Smartest::Suite.new
198
+ suite.tests.add(
199
+ SmartestSelfTest.test_case(
200
+ "bad prefix",
201
+ proc { expect("https://example.test/path").to start_with("about:") }
202
+ )
203
+ )
204
+ suite.tests.add(
205
+ SmartestSelfTest.test_case(
206
+ "bad suffix",
207
+ proc { expect("asset.gif").to end_with(".jpg", ".png") }
208
+ )
209
+ )
210
+ suite.tests.add(
211
+ SmartestSelfTest.test_case(
212
+ "bad negated suffix",
213
+ proc { expect("screenshot.png").not_to end_with(".png") }
214
+ )
215
+ )
216
+
217
+ status, output = SmartestSelfTest.run_suite(suite)
218
+
219
+ expect(status).to eq(1)
220
+ expect(output).to include('expected "https://example.test/path" to start with "about:"')
221
+ expect(output).to include('expected "asset.gif" to end with ".jpg" or ".png"')
222
+ expect(output).to include('expected "screenshot.png" not to end with ".png"')
223
+ end
224
+
225
+ test("supports change matcher for block expectations") do
226
+ value = 0
227
+ action_calls = 0
228
+
229
+ expect {
230
+ action_calls += 1
231
+ value += 2
232
+ }.to change { value }.from(0).to(2).by(2)
233
+
234
+ expect(action_calls).to eq(1)
235
+ expect { value }.not_to change { value }
236
+ end
237
+
238
+ test("reports change matcher failures with before and after values") do
239
+ value = 0
240
+
241
+ error = SmartestSelfTest.capture_error(Smartest::AssertionFailed) do
242
+ expect { value += 1 }.to change { value }.from(0).to(2).by(2)
243
+ end
244
+
245
+ expect(error.message).to include("0 before")
246
+ expect(error.message).to include("1 after")
247
+ expect(error.message).to include("to(2)")
248
+ expect(error.message).to include("by(2)")
249
+ end
250
+
251
+ test("fails negated change matcher when the value changes") do
252
+ value = 0
253
+
254
+ error = SmartestSelfTest.capture_error(Smartest::AssertionFailed) do
255
+ expect { value += 1 }.not_to change { value }
256
+ end
257
+
258
+ expect(error.message).to include("expected value not to change")
259
+ expect(error.message).to include("0 before")
260
+ expect(error.message).to include("1 after")
261
+ end
262
+
263
+ test("requires change matcher value and action blocks") do
264
+ error = SmartestSelfTest.capture_error(ArgumentError) do
265
+ change
266
+ end
267
+
268
+ expect(error.message).to eq("change requires a block")
269
+
270
+ error = SmartestSelfTest.capture_error(ArgumentError) do
271
+ change(:value) { :other }
272
+ end
273
+
274
+ expect(error.message).to include("change does not support arguments")
275
+
276
+ error = SmartestSelfTest.capture_error(Smartest::AssertionFailed) do
277
+ expect(:value).to change { :other }
278
+ end
279
+
280
+ expect(error.message).to include("expected a block to change value")
281
+ end
282
+
187
283
  test("registers matcher modules for suite execution contexts") do
188
284
  status_matcher = Class.new do
189
285
  def initialize(expected)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smartest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0.alpha2
4
+ version: 0.2.0.alpha3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yusuke Iwaki