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.
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.