smartest 0.1.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 +7 -0
- data/CHANGELOG.md +16 -0
- data/DEVELOPMENT.md +774 -0
- data/Gemfile +5 -0
- data/LICENSE +21 -0
- data/README.md +518 -0
- data/Rakefile +12 -0
- data/SMARTEST_DESIGN.md +1137 -0
- data/exe/smartest +63 -0
- data/lib/smartest/autorun.rb +8 -0
- data/lib/smartest/dsl.rb +22 -0
- data/lib/smartest/errors.rb +52 -0
- data/lib/smartest/execution_context.rb +8 -0
- data/lib/smartest/expectation_target.rb +21 -0
- data/lib/smartest/expectations.rb +9 -0
- data/lib/smartest/fixture.rb +78 -0
- data/lib/smartest/fixture_class_registry.rb +27 -0
- data/lib/smartest/fixture_definition.rb +31 -0
- data/lib/smartest/fixture_set.rb +119 -0
- data/lib/smartest/init_generator.rb +70 -0
- data/lib/smartest/matchers.rb +109 -0
- data/lib/smartest/parameter_extractor.rb +51 -0
- data/lib/smartest/reporter.rb +91 -0
- data/lib/smartest/runner.rb +80 -0
- data/lib/smartest/suite.rb +12 -0
- data/lib/smartest/test_case.rb +18 -0
- data/lib/smartest/test_registry.rb +25 -0
- data/lib/smartest/test_result.rb +43 -0
- data/lib/smartest/version.rb +5 -0
- data/lib/smartest.rb +59 -0
- data/smartest/smartest_test.rb +634 -0
- data/smartest.gemspec +48 -0
- metadata +95 -0
data/DEVELOPMENT.md
ADDED
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
# Smartest Development Guide
|
|
2
|
+
|
|
3
|
+
This document describes how to develop Smartest itself.
|
|
4
|
+
|
|
5
|
+
For user-facing usage, see `README.md`.
|
|
6
|
+
|
|
7
|
+
For detailed design rationale, see `SMARTEST_DESIGN.md`.
|
|
8
|
+
|
|
9
|
+
## Project goals
|
|
10
|
+
|
|
11
|
+
Smartest is a Ruby test runner focused on:
|
|
12
|
+
|
|
13
|
+
- top-level test definitions
|
|
14
|
+
- class-based fixtures
|
|
15
|
+
- explicit keyword-argument fixture dependencies
|
|
16
|
+
- optional cleanup for fixtures that need teardown
|
|
17
|
+
- suite-scoped fixtures for expensive shared resources
|
|
18
|
+
- a small internal architecture that is easy to reason about
|
|
19
|
+
|
|
20
|
+
The MVP should avoid becoming a full RSpec clone.
|
|
21
|
+
|
|
22
|
+
## Intended directory structure
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
smartest/
|
|
26
|
+
lib/
|
|
27
|
+
smartest.rb
|
|
28
|
+
smartest/
|
|
29
|
+
autorun.rb
|
|
30
|
+
version.rb
|
|
31
|
+
|
|
32
|
+
dsl.rb
|
|
33
|
+
suite.rb
|
|
34
|
+
test_case.rb
|
|
35
|
+
test_registry.rb
|
|
36
|
+
|
|
37
|
+
fixture.rb
|
|
38
|
+
fixture_definition.rb
|
|
39
|
+
fixture_class_registry.rb
|
|
40
|
+
fixture_set.rb
|
|
41
|
+
parameter_extractor.rb
|
|
42
|
+
|
|
43
|
+
execution_context.rb
|
|
44
|
+
|
|
45
|
+
expectations.rb
|
|
46
|
+
expectation_target.rb
|
|
47
|
+
matchers.rb
|
|
48
|
+
|
|
49
|
+
runner.rb
|
|
50
|
+
test_result.rb
|
|
51
|
+
reporter.rb
|
|
52
|
+
|
|
53
|
+
errors.rb
|
|
54
|
+
|
|
55
|
+
exe/
|
|
56
|
+
smartest
|
|
57
|
+
|
|
58
|
+
smartest/
|
|
59
|
+
smartest_test.rb
|
|
60
|
+
fixtures/
|
|
61
|
+
sample_fixture.rb
|
|
62
|
+
|
|
63
|
+
documentation/
|
|
64
|
+
docs/
|
|
65
|
+
|
|
66
|
+
Gemfile
|
|
67
|
+
smartest.gemspec
|
|
68
|
+
CHANGELOG.md
|
|
69
|
+
Rakefile
|
|
70
|
+
README.md
|
|
71
|
+
DEVELOPMENT.md
|
|
72
|
+
SMARTEST_DESIGN.md
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Core modules and classes
|
|
76
|
+
|
|
77
|
+
### `Smartest`
|
|
78
|
+
|
|
79
|
+
Top-level namespace.
|
|
80
|
+
|
|
81
|
+
Responsibilities:
|
|
82
|
+
|
|
83
|
+
- owns the default suite
|
|
84
|
+
- exposes accessors used by the DSL
|
|
85
|
+
- loads framework components
|
|
86
|
+
|
|
87
|
+
Example:
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
module Smartest
|
|
91
|
+
def self.suite
|
|
92
|
+
@suite ||= Suite.new
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### `Smartest::Suite`
|
|
98
|
+
|
|
99
|
+
A suite groups all mutable test-run state.
|
|
100
|
+
|
|
101
|
+
Responsibilities:
|
|
102
|
+
|
|
103
|
+
- test registry
|
|
104
|
+
- fixture class registry
|
|
105
|
+
- hook registry, if hooks are implemented later
|
|
106
|
+
|
|
107
|
+
Example shape:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
class Smartest::Suite
|
|
111
|
+
attr_reader :tests, :fixture_classes
|
|
112
|
+
|
|
113
|
+
def initialize
|
|
114
|
+
@tests = TestRegistry.new
|
|
115
|
+
@fixture_classes = FixtureClassRegistry.new
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### `Smartest::DSL`
|
|
121
|
+
|
|
122
|
+
Provides top-level user methods.
|
|
123
|
+
|
|
124
|
+
Required methods:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
test(name, **metadata, &block)
|
|
128
|
+
use_fixture(klass)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Possible later methods:
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
before(&block)
|
|
135
|
+
after(&block)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`Kernel.include Smartest::DSL` should happen only from `smartest/autorun` or the CLI entrypoint.
|
|
139
|
+
|
|
140
|
+
Do not include DSL globally from `smartest.rb` itself.
|
|
141
|
+
|
|
142
|
+
### `Smartest::TestCase`
|
|
143
|
+
|
|
144
|
+
Represents a single test.
|
|
145
|
+
|
|
146
|
+
Responsibilities:
|
|
147
|
+
|
|
148
|
+
- stores name
|
|
149
|
+
- stores metadata
|
|
150
|
+
- stores block
|
|
151
|
+
- exposes fixture names required by the test
|
|
152
|
+
|
|
153
|
+
Example:
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
class Smartest::TestCase
|
|
157
|
+
attr_reader :name, :metadata, :block, :location
|
|
158
|
+
|
|
159
|
+
def fixture_names
|
|
160
|
+
ParameterExtractor.required_keyword_names(block)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`location` should be captured from `caller_locations` when the test is registered.
|
|
166
|
+
|
|
167
|
+
### `Smartest::ParameterExtractor`
|
|
168
|
+
|
|
169
|
+
Extracts fixture names from block parameters.
|
|
170
|
+
|
|
171
|
+
Primary rule:
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
do |user:|
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
means the block requires fixture `:user`.
|
|
178
|
+
|
|
179
|
+
Implementation direction:
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
class Smartest::ParameterExtractor
|
|
183
|
+
def self.required_keyword_names(block)
|
|
184
|
+
block.parameters.filter_map do |type, name|
|
|
185
|
+
name if type == :keyreq
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
MVP should support `:keyreq`.
|
|
192
|
+
|
|
193
|
+
Optional keyword support can be added later.
|
|
194
|
+
|
|
195
|
+
### `Smartest::Fixture`
|
|
196
|
+
|
|
197
|
+
Base class for fixture classes.
|
|
198
|
+
|
|
199
|
+
User code:
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
class AppFixture < Smartest::Fixture
|
|
203
|
+
fixture :user do
|
|
204
|
+
User.create!(name: "Alice")
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
fixture :client do |server:|
|
|
208
|
+
Client.new(base_url: server.url)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Responsibilities:
|
|
214
|
+
|
|
215
|
+
- class-level `fixture` DSL
|
|
216
|
+
- class-level `suite_fixture` DSL for suite-scoped fixtures
|
|
217
|
+
- stores fixture definitions
|
|
218
|
+
- supports inheritance
|
|
219
|
+
- exposes `cleanup` to fixture blocks
|
|
220
|
+
- optionally delegates helper methods to `ExecutionContext`
|
|
221
|
+
|
|
222
|
+
Fixture definitions should not execute at declaration time.
|
|
223
|
+
|
|
224
|
+
### `Smartest::FixtureDefinition`
|
|
225
|
+
|
|
226
|
+
Represents one fixture definition.
|
|
227
|
+
|
|
228
|
+
Fields:
|
|
229
|
+
|
|
230
|
+
- name
|
|
231
|
+
- block
|
|
232
|
+
- dependencies
|
|
233
|
+
- location
|
|
234
|
+
- scope
|
|
235
|
+
|
|
236
|
+
Example:
|
|
237
|
+
|
|
238
|
+
```ruby
|
|
239
|
+
class Smartest::FixtureDefinition
|
|
240
|
+
attr_reader :name, :block, :dependencies, :location, :scope
|
|
241
|
+
|
|
242
|
+
def initialize(name:, block:, location:, scope: :test)
|
|
243
|
+
@name = name.to_sym
|
|
244
|
+
@block = block
|
|
245
|
+
@scope = scope
|
|
246
|
+
@dependencies = ParameterExtractor.required_keyword_names(block)
|
|
247
|
+
@location = location
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### `Smartest::FixtureClassRegistry`
|
|
253
|
+
|
|
254
|
+
Stores registered fixture classes.
|
|
255
|
+
|
|
256
|
+
Responsibilities:
|
|
257
|
+
|
|
258
|
+
- add fixture class
|
|
259
|
+
- return all registered classes
|
|
260
|
+
- validate class type if desired
|
|
261
|
+
|
|
262
|
+
Example:
|
|
263
|
+
|
|
264
|
+
```ruby
|
|
265
|
+
use_fixture AppFixture
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
should register `AppFixture`.
|
|
269
|
+
|
|
270
|
+
### `Smartest::FixtureSet`
|
|
271
|
+
|
|
272
|
+
Fixture resolver for one scope.
|
|
273
|
+
|
|
274
|
+
A suite-scoped `FixtureSet` is created lazily for shared fixtures. A new
|
|
275
|
+
test-scoped `FixtureSet` is created for each test and delegates suite-scoped
|
|
276
|
+
fixture requests to the suite set.
|
|
277
|
+
|
|
278
|
+
Responsibilities:
|
|
279
|
+
|
|
280
|
+
- instantiate registered fixture classes
|
|
281
|
+
- find fixture definitions
|
|
282
|
+
- resolve fixture dependencies
|
|
283
|
+
- cache fixture values for its scope
|
|
284
|
+
- collect cleanup blocks
|
|
285
|
+
- run cleanup blocks in reverse order
|
|
286
|
+
- detect duplicate fixture names
|
|
287
|
+
- detect circular dependencies
|
|
288
|
+
|
|
289
|
+
Important: regular fixture values must not leak across tests. Suite fixture
|
|
290
|
+
values are intentionally shared across the run.
|
|
291
|
+
|
|
292
|
+
### `Smartest::ExecutionContext`
|
|
293
|
+
|
|
294
|
+
Object used as `self` when running a test body.
|
|
295
|
+
|
|
296
|
+
Responsibilities:
|
|
297
|
+
|
|
298
|
+
- include expectation methods
|
|
299
|
+
- include matchers
|
|
300
|
+
- expose helper methods
|
|
301
|
+
- optionally provide integration helpers later
|
|
302
|
+
|
|
303
|
+
Tests should run via:
|
|
304
|
+
|
|
305
|
+
```ruby
|
|
306
|
+
context.instance_exec(**kwargs, &test_case.block)
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### `Smartest::Runner`
|
|
310
|
+
|
|
311
|
+
Runs tests.
|
|
312
|
+
|
|
313
|
+
Responsibilities:
|
|
314
|
+
|
|
315
|
+
- iterate over registered test cases
|
|
316
|
+
- create a lazy suite-scoped `FixtureSet`
|
|
317
|
+
- create a fresh `ExecutionContext` per test
|
|
318
|
+
- create a fresh `FixtureSet` per test
|
|
319
|
+
- resolve test keyword fixtures
|
|
320
|
+
- run test body
|
|
321
|
+
- run cleanup in `ensure`
|
|
322
|
+
- run suite fixture cleanup after all tests
|
|
323
|
+
- produce `TestResult`
|
|
324
|
+
- notify reporter
|
|
325
|
+
|
|
326
|
+
Pseudo-code:
|
|
327
|
+
|
|
328
|
+
```ruby
|
|
329
|
+
def run_one(test_case)
|
|
330
|
+
context = ExecutionContext.new
|
|
331
|
+
fixture_set = FixtureSet.new(
|
|
332
|
+
@fixture_classes,
|
|
333
|
+
context: context,
|
|
334
|
+
parent: suite_fixture_set
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
kwargs = fixture_set.resolve_keywords(test_case.fixture_names)
|
|
338
|
+
|
|
339
|
+
context.instance_exec(**kwargs, &test_case.block)
|
|
340
|
+
|
|
341
|
+
TestResult.passed(test_case)
|
|
342
|
+
rescue Exception => error
|
|
343
|
+
TestResult.failed(test_case, error)
|
|
344
|
+
ensure
|
|
345
|
+
fixture_set&.run_cleanups
|
|
346
|
+
end
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### `Smartest::TestResult`
|
|
350
|
+
|
|
351
|
+
Represents one test outcome.
|
|
352
|
+
|
|
353
|
+
Fields:
|
|
354
|
+
|
|
355
|
+
- test case
|
|
356
|
+
- status
|
|
357
|
+
- error
|
|
358
|
+
- duration
|
|
359
|
+
|
|
360
|
+
Statuses:
|
|
361
|
+
|
|
362
|
+
- passed
|
|
363
|
+
- failed
|
|
364
|
+
- skipped, later
|
|
365
|
+
|
|
366
|
+
### `Smartest::Reporter`
|
|
367
|
+
|
|
368
|
+
Console reporter for MVP.
|
|
369
|
+
|
|
370
|
+
Responsibilities:
|
|
371
|
+
|
|
372
|
+
- print run start
|
|
373
|
+
- print per-test pass/fail
|
|
374
|
+
- print failure details
|
|
375
|
+
- print summary
|
|
376
|
+
- return appropriate exit status through runner
|
|
377
|
+
|
|
378
|
+
## Fixture resolution
|
|
379
|
+
|
|
380
|
+
Given:
|
|
381
|
+
|
|
382
|
+
```ruby
|
|
383
|
+
fixture :logged_in_client do |client:, user:|
|
|
384
|
+
client.login(user)
|
|
385
|
+
client
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
fixture :client do |server:|
|
|
389
|
+
Client.new(base_url: server.url)
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
fixture :server do
|
|
393
|
+
server = TestServer.start
|
|
394
|
+
cleanup { server.stop }
|
|
395
|
+
server
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
fixture :user do
|
|
399
|
+
User.create!(email: "alice@example.com")
|
|
400
|
+
end
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
and:
|
|
404
|
+
|
|
405
|
+
```ruby
|
|
406
|
+
test("GET /me") do |logged_in_client:|
|
|
407
|
+
end
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
Resolution flow:
|
|
411
|
+
|
|
412
|
+
```text
|
|
413
|
+
resolve :logged_in_client
|
|
414
|
+
resolve :client
|
|
415
|
+
resolve :server
|
|
416
|
+
evaluate server
|
|
417
|
+
cache server
|
|
418
|
+
evaluate client
|
|
419
|
+
cache client
|
|
420
|
+
resolve :user
|
|
421
|
+
evaluate user
|
|
422
|
+
cache user
|
|
423
|
+
evaluate logged_in_client
|
|
424
|
+
cache logged_in_client
|
|
425
|
+
run test body
|
|
426
|
+
run cleanup stack
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
## Cleanup behavior
|
|
430
|
+
|
|
431
|
+
Fixture cleanup is optional.
|
|
432
|
+
|
|
433
|
+
Fixture without teardown:
|
|
434
|
+
|
|
435
|
+
```ruby
|
|
436
|
+
fixture :user do
|
|
437
|
+
User.create!(name: "Alice")
|
|
438
|
+
end
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
Fixture with teardown:
|
|
442
|
+
|
|
443
|
+
```ruby
|
|
444
|
+
fixture :server do
|
|
445
|
+
server = TestServer.start
|
|
446
|
+
cleanup { server.stop }
|
|
447
|
+
|
|
448
|
+
server.wait_until_ready!
|
|
449
|
+
server
|
|
450
|
+
end
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
`cleanup` should register a block on the current fixture set. Regular fixture
|
|
454
|
+
cleanups run after the test. `suite_fixture` cleanups run after all tests.
|
|
455
|
+
|
|
456
|
+
Cleanup blocks must run:
|
|
457
|
+
|
|
458
|
+
- after the test body
|
|
459
|
+
- after the suite for suite-scoped fixtures
|
|
460
|
+
- after failed tests
|
|
461
|
+
- after fixture setup errors, if cleanup was already registered
|
|
462
|
+
- in reverse registration order
|
|
463
|
+
|
|
464
|
+
Implementation:
|
|
465
|
+
|
|
466
|
+
```ruby
|
|
467
|
+
def add_cleanup(&block)
|
|
468
|
+
@cleanups << block
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def run_cleanups
|
|
472
|
+
@cleanups.reverse_each(&:call)
|
|
473
|
+
end
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
## Circular dependency detection
|
|
477
|
+
|
|
478
|
+
This should fail:
|
|
479
|
+
|
|
480
|
+
```ruby
|
|
481
|
+
fixture :a do |b:|
|
|
482
|
+
b
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
fixture :b do |a:|
|
|
486
|
+
a
|
|
487
|
+
end
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
Expected error:
|
|
491
|
+
|
|
492
|
+
```text
|
|
493
|
+
circular fixture dependency: a -> b -> a
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
Implementation idea:
|
|
497
|
+
|
|
498
|
+
```ruby
|
|
499
|
+
def resolve(name)
|
|
500
|
+
return @cache[name] if @cache.key?(name)
|
|
501
|
+
|
|
502
|
+
if @resolving.include?(name)
|
|
503
|
+
raise CircularFixtureDependencyError, ...
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
@resolving << name
|
|
507
|
+
# resolve
|
|
508
|
+
ensure
|
|
509
|
+
@resolving.pop if @resolving.last == name
|
|
510
|
+
end
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
## Duplicate fixture detection
|
|
514
|
+
|
|
515
|
+
This should fail:
|
|
516
|
+
|
|
517
|
+
```ruby
|
|
518
|
+
class UserFixture < Smartest::Fixture
|
|
519
|
+
fixture :user do
|
|
520
|
+
end
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
class AdminFixture < Smartest::Fixture
|
|
524
|
+
fixture :user do
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
Expected error:
|
|
530
|
+
|
|
531
|
+
```text
|
|
532
|
+
duplicate fixture: user
|
|
533
|
+
defined in:
|
|
534
|
+
UserFixture
|
|
535
|
+
AdminFixture
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
Detect duplicates when creating a `FixtureSet`, because registered fixture classes are known then.
|
|
539
|
+
|
|
540
|
+
## Error classes
|
|
541
|
+
|
|
542
|
+
Recommended errors:
|
|
543
|
+
|
|
544
|
+
```ruby
|
|
545
|
+
module Smartest
|
|
546
|
+
class Error < StandardError; end
|
|
547
|
+
|
|
548
|
+
class FixtureNotFoundError < Error; end
|
|
549
|
+
class DuplicateFixtureError < Error; end
|
|
550
|
+
class CircularFixtureDependencyError < Error; end
|
|
551
|
+
class InvalidFixtureParameterError < Error; end
|
|
552
|
+
class AssertionFailed < Error; end
|
|
553
|
+
end
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
Avoid rescuing only `StandardError` in the runner if the goal is to report test failures robustly.
|
|
557
|
+
|
|
558
|
+
However, be careful with `Exception`, because it includes `SystemExit`, `NoMemoryError`, and interrupt-related exceptions.
|
|
559
|
+
|
|
560
|
+
A practical approach:
|
|
561
|
+
|
|
562
|
+
- assertion and ordinary errors should become failed tests
|
|
563
|
+
- `SystemExit` and `Interrupt` should probably be re-raised
|
|
564
|
+
|
|
565
|
+
## Expected implementation order
|
|
566
|
+
|
|
567
|
+
### Phase 1: Basic test runner
|
|
568
|
+
|
|
569
|
+
- `test`
|
|
570
|
+
- registry
|
|
571
|
+
- runner
|
|
572
|
+
- console reporter
|
|
573
|
+
- `expect(...).to eq(...)`
|
|
574
|
+
|
|
575
|
+
### Phase 2: Keyword fixture injection
|
|
576
|
+
|
|
577
|
+
- `Smartest::Fixture`
|
|
578
|
+
- `fixture :name do ... end`
|
|
579
|
+
- `use_fixture`
|
|
580
|
+
- test block keyword fixture resolution
|
|
581
|
+
|
|
582
|
+
### Phase 3: Fixture dependencies
|
|
583
|
+
|
|
584
|
+
- `fixture :client do |server:| ... end`
|
|
585
|
+
- dependency extraction with `Proc#parameters`
|
|
586
|
+
- recursive fixture resolution
|
|
587
|
+
- per-test caching
|
|
588
|
+
|
|
589
|
+
### Phase 4: Cleanup
|
|
590
|
+
|
|
591
|
+
- `cleanup { ... }`
|
|
592
|
+
- cleanup stack on `FixtureSet`
|
|
593
|
+
- cleanup in `ensure`
|
|
594
|
+
|
|
595
|
+
### Phase 5: Suite-scoped fixtures
|
|
596
|
+
|
|
597
|
+
- `suite_fixture :name do ... end`
|
|
598
|
+
- suite-level fixture cache
|
|
599
|
+
- suite cleanup after all tests
|
|
600
|
+
- test fixtures may depend on suite fixtures
|
|
601
|
+
- suite fixtures may not depend on test fixtures
|
|
602
|
+
|
|
603
|
+
### Phase 6: Hardening
|
|
604
|
+
|
|
605
|
+
- circular dependency detection
|
|
606
|
+
- duplicate fixture detection
|
|
607
|
+
- improved error output
|
|
608
|
+
- source locations
|
|
609
|
+
- invalid positional argument detection
|
|
610
|
+
|
|
611
|
+
### Phase 6: CLI
|
|
612
|
+
|
|
613
|
+
- `exe/smartest`
|
|
614
|
+
- load files from ARGV
|
|
615
|
+
- default glob `smartest/**/*_test.rb`
|
|
616
|
+
- add `smartest/` to the load path before loading tests
|
|
617
|
+
- generate a `smartest/test_helper.rb` that loads `smartest/fixtures/**/*.rb`
|
|
618
|
+
- exit code 0 on success, 1 on failure
|
|
619
|
+
|
|
620
|
+
## MVP API rules
|
|
621
|
+
|
|
622
|
+
Supported:
|
|
623
|
+
|
|
624
|
+
```ruby
|
|
625
|
+
test("name") do
|
|
626
|
+
end
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
```ruby
|
|
630
|
+
test("name") do |user:|
|
|
631
|
+
end
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
```ruby
|
|
635
|
+
fixture :user do
|
|
636
|
+
end
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
```ruby
|
|
640
|
+
fixture :client do |server:|
|
|
641
|
+
end
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
```ruby
|
|
645
|
+
cleanup { ... }
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
Not supported in MVP:
|
|
649
|
+
|
|
650
|
+
```ruby
|
|
651
|
+
test("name") do |user|
|
|
652
|
+
end
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
```ruby
|
|
656
|
+
fixture :client do |server|
|
|
657
|
+
end
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
```ruby
|
|
661
|
+
fixture :client, with: [:server] do |server|
|
|
662
|
+
end
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
```ruby
|
|
666
|
+
resource :server do |use|
|
|
667
|
+
end
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
The unsupported forms may be added later, but the first implementation should keep the API sharp.
|
|
671
|
+
|
|
672
|
+
## Handling positional block parameters
|
|
673
|
+
|
|
674
|
+
If a fixture or test uses positional parameters, fail with a helpful message.
|
|
675
|
+
|
|
676
|
+
Bad:
|
|
677
|
+
|
|
678
|
+
```ruby
|
|
679
|
+
test("bad") do |user|
|
|
680
|
+
end
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
Good error:
|
|
684
|
+
|
|
685
|
+
```text
|
|
686
|
+
Positional fixture parameters are not supported.
|
|
687
|
+
|
|
688
|
+
Use keyword fixture injection:
|
|
689
|
+
|
|
690
|
+
test("bad") do |user:|
|
|
691
|
+
...
|
|
692
|
+
end
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
Bad:
|
|
696
|
+
|
|
697
|
+
```ruby
|
|
698
|
+
fixture :client do |server|
|
|
699
|
+
end
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
Good error:
|
|
703
|
+
|
|
704
|
+
```text
|
|
705
|
+
Positional fixture dependencies are not supported.
|
|
706
|
+
|
|
707
|
+
Use keyword fixture dependencies:
|
|
708
|
+
|
|
709
|
+
fixture :client do |server:|
|
|
710
|
+
...
|
|
711
|
+
end
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
## Running the test suite
|
|
715
|
+
|
|
716
|
+
During development:
|
|
717
|
+
|
|
718
|
+
```bash
|
|
719
|
+
bundle exec ruby exe/smartest
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
or:
|
|
723
|
+
|
|
724
|
+
```bash
|
|
725
|
+
rake test
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
Smartest's own test suite is written with the Smartest DSL and runs through
|
|
729
|
+
the Smartest CLI.
|
|
730
|
+
|
|
731
|
+
## Release checklist
|
|
732
|
+
|
|
733
|
+
Before releasing:
|
|
734
|
+
|
|
735
|
+
- update `Smartest::VERSION` in `lib/smartest/version.rb`
|
|
736
|
+
- update `CHANGELOG.md`
|
|
737
|
+
- run the test suite
|
|
738
|
+
- verify the CLI
|
|
739
|
+
- verify README and documentation examples
|
|
740
|
+
- build the gem
|
|
741
|
+
- install the built gem locally
|
|
742
|
+
- run a sample project against the installed gem
|
|
743
|
+
- push the release tag
|
|
744
|
+
|
|
745
|
+
Example commands:
|
|
746
|
+
|
|
747
|
+
```bash
|
|
748
|
+
rake test
|
|
749
|
+
rake build
|
|
750
|
+
gem install ./pkg/smartest-0.1.0.gem
|
|
751
|
+
git tag 0.1.0
|
|
752
|
+
git push origin 0.1.0
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
`rake build` is provided by Bundler's gem tasks. Pushing a tag that matches
|
|
756
|
+
`Smartest::VERSION`, such as `0.1.0` or `0.1.0.alpha1`, triggers the Deploy
|
|
757
|
+
GitHub Actions workflow. The workflow runs `rake verify`, builds the gem, and
|
|
758
|
+
publishes `pkg/smartest-$VERSION.gem` to RubyGems using the `RUBYGEMS_API_KEY`
|
|
759
|
+
repository secret.
|
|
760
|
+
|
|
761
|
+
## Non-goals for the MVP
|
|
762
|
+
|
|
763
|
+
Do not implement these in the first version:
|
|
764
|
+
|
|
765
|
+
- nested `describe/context`
|
|
766
|
+
- parallel execution
|
|
767
|
+
- file-scoped fixtures
|
|
768
|
+
- resource fixtures using `use.call`
|
|
769
|
+
- RSpec-compatible matcher ecosystem
|
|
770
|
+
- snapshot testing
|
|
771
|
+
- watch mode
|
|
772
|
+
- browser automation integration
|
|
773
|
+
|
|
774
|
+
These can be added after the core fixture model is stable.
|