smartest 0.1.0.alpha2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0bd00e8f67e5e3b8fd9931ade12ab688147fb68858b3bf614b2f6aa103b4849d
4
- data.tar.gz: d3d7293dc006d7ddeafb406c66ef2dd6341af47e6f4cb1ed07f09a0f06667741
3
+ metadata.gz: c1ce8aea4b74c7ca0de7f3a2b1472b79ea86849be91801e11922883b6423afdf
4
+ data.tar.gz: be5f8e88c1bd93eb061eb0a854b46a6c6094627b36cd22f8778026e35161992a
5
5
  SHA512:
6
- metadata.gz: 9cc36fee51acacabb449c0e8973e92530cd0707a434ed1216db9318378b41473a0ce688e63eb02571da90c72e34874983740907eb93086ff9b5e0c9b9b128df6
7
- data.tar.gz: 59b6db66b22561660b93608a843313e38569a9eb634286e54539c434052745637773bf1f3e2f86114c5115d1981330471e279cad637577df5e748e26bd186308
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:
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
 
@@ -171,6 +173,10 @@ be_nil
171
173
  raise_error(ErrorClass)
172
174
  ```
173
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
+
174
180
  ## Fixtures
175
181
 
176
182
  Fixtures are defined in classes.
@@ -184,9 +190,17 @@ class AppFixture < Smartest::Fixture
184
190
  )
185
191
  end
186
192
  end
193
+ ```
194
+
195
+ Register fixture classes from `smartest/test_helper.rb`:
187
196
 
197
+ ```ruby
188
198
  use_fixture AppFixture
199
+ ```
189
200
 
201
+ Tests request fixtures by keyword:
202
+
203
+ ```ruby
190
204
  test("user") do |user:|
191
205
  expect(user.name).to eq("Alice")
192
206
  end
@@ -337,8 +351,16 @@ class WebFixture < Smartest::Fixture
337
351
  client
338
352
  end
339
353
  end
354
+ ```
340
355
 
356
+ ```ruby
357
+ # smartest/test_helper.rb
341
358
  use_fixture WebFixture
359
+ ```
360
+
361
+ ```ruby
362
+ # smartest/web_test.rb
363
+ require "test_helper"
342
364
 
343
365
  test("GET /me") do |logged_in_client:|
344
366
  response = logged_in_client.get("/me")
@@ -369,7 +391,7 @@ server cleanup
369
391
 
370
392
  ## Registering fixture classes
371
393
 
372
- Use `use_fixture`:
394
+ Use `use_fixture` from `smartest/test_helper.rb`:
373
395
 
374
396
  ```ruby
375
397
  use_fixture AppFixture
@@ -429,6 +451,9 @@ smartest/
429
451
  fixtures/
430
452
  app_fixture.rb
431
453
  web_fixture.rb
454
+ matchers/
455
+ predicate_matcher.rb
456
+ have_status_matcher.rb
432
457
  example_test.rb
433
458
  ```
434
459
 
@@ -439,10 +464,18 @@ require "smartest/autorun"
439
464
  Dir[File.join(__dir__, "fixtures", "**", "*.rb")].sort.each do |fixture_file|
440
465
  require fixture_file
441
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
442
474
  ```
443
475
 
444
- The generated helper loads Ruby files under `smartest/fixtures/` in sorted order.
445
- Test files still register the fixture classes they need with `use_fixture`.
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`.
446
479
 
447
480
  Example:
448
481
 
@@ -465,8 +498,6 @@ end
465
498
  # smartest/example_test.rb
466
499
  require "test_helper"
467
500
 
468
- use_fixture WebFixture
469
-
470
501
  test("GET /health") do |client:|
471
502
  expect(client.get("/health").status).to eq(200)
472
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 `use_fixture` need to be globally available when using `smartest/autorun`.
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
 
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
- private :test, :use_fixture
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
@@ -35,7 +35,7 @@ module Smartest
35
35
 
36
36
  def run_one(test_case)
37
37
  started_at = now
38
- context = ExecutionContext.new
38
+ context = build_context
39
39
  fixture_set = nil
40
40
  error = nil
41
41
  cleanup_errors = []
@@ -69,11 +69,17 @@ module Smartest
69
69
  def suite_fixture_set
70
70
  @suite_fixture_set ||= FixtureSet.new(
71
71
  @suite.fixture_classes,
72
- context: ExecutionContext.new,
72
+ context: build_context,
73
73
  scope: :suite
74
74
  )
75
75
  end
76
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
+
77
83
  def now
78
84
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
79
85
  end
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Smartest
4
- VERSION = "0.1.0.alpha2"
4
+ VERSION = "0.1.0.alpha3"
5
5
  end
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"
@@ -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,72 @@ 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
+
480
598
  test("cli runs tests matching a file line filter") do
481
599
  Dir.mktmpdir do |dir|
482
600
  smartest_dir = File.join(dir, "smartest")
@@ -645,11 +763,17 @@ test("cli initializes a runnable test scaffold") do
645
763
  expect(stderr).to eq("")
646
764
  expect(stdout).to include("create smartest")
647
765
  expect(stdout).to include("create smartest/fixtures")
766
+ expect(stdout).to include("create smartest/matchers")
648
767
  expect(stdout).to include("create smartest/test_helper.rb")
768
+ expect(stdout).to include("create smartest/matchers/predicate_matcher.rb")
649
769
  expect(stdout).to include("create smartest/example_test.rb")
650
770
  helper_contents = File.read(File.join(dir, "smartest/test_helper.rb"))
651
771
  expect(helper_contents).to include('require "smartest/autorun"')
652
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")
653
777
  expect(File.read(File.join(dir, "smartest/example_test.rb"))).to include('require "test_helper"')
654
778
 
655
779
  nested_fixtures_dir = File.join(dir, "smartest/fixtures/nested")
@@ -672,6 +796,54 @@ test("cli initializes a runnable test scaffold") do
672
796
  end
673
797
  RUBY
674
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
+
675
847
  run_stdout, run_stderr, run_status = Open3.capture3(
676
848
  { "RUBYLIB" => File.expand_path("../lib", __dir__) },
677
849
  "ruby",
@@ -683,7 +855,9 @@ test("cli initializes a runnable test scaffold") do
683
855
  expect(run_stderr).to eq("")
684
856
  expect(run_stdout).to include("example")
685
857
  expect(run_stdout).to include("auto-loaded fixture")
686
- expect(run_stdout).to include("2 tests, 2 passed, 0 failed")
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")
687
861
  end
688
862
  end
689
863
 
@@ -691,13 +865,17 @@ test("cli init does not overwrite existing scaffold files") do
691
865
  Dir.mktmpdir do |dir|
692
866
  smartest_dir = File.join(dir, "smartest")
693
867
  fixture_dir = File.join(smartest_dir, "fixtures")
868
+ matcher_dir = File.join(smartest_dir, "matchers")
694
869
  FileUtils.mkdir_p(fixture_dir)
870
+ FileUtils.mkdir_p(matcher_dir)
695
871
  helper_path = File.join(smartest_dir, "test_helper.rb")
696
872
  example_path = File.join(smartest_dir, "example_test.rb")
697
873
  fixture_path = File.join(fixture_dir, "custom_fixture.rb")
874
+ matcher_path = File.join(matcher_dir, "predicate_matcher.rb")
698
875
  File.write(helper_path, "# custom helper\n")
699
876
  File.write(example_path, "# custom test\n")
700
877
  File.write(fixture_path, "# custom fixture\n")
878
+ File.write(matcher_path, "# custom matcher\n")
701
879
 
702
880
  stdout, stderr, status = Open3.capture3(
703
881
  { "RUBYLIB" => File.expand_path("../lib", __dir__) },
@@ -711,10 +889,13 @@ test("cli init does not overwrite existing scaffold files") do
711
889
  expect(stderr).to eq("")
712
890
  expect(stdout).to include("exist smartest")
713
891
  expect(stdout).to include("exist smartest/fixtures")
892
+ expect(stdout).to include("exist smartest/matchers")
714
893
  expect(stdout).to include("exist smartest/test_helper.rb")
894
+ expect(stdout).to include("exist smartest/matchers/predicate_matcher.rb")
715
895
  expect(stdout).to include("exist smartest/example_test.rb")
716
896
  expect(File.read(helper_path)).to eq("# custom helper\n")
717
897
  expect(File.read(example_path)).to eq("# custom test\n")
718
898
  expect(File.read(fixture_path)).to eq("# custom fixture\n")
899
+ expect(File.read(matcher_path)).to eq("# custom matcher\n")
719
900
  end
720
901
  end
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.1.0.alpha2
4
+ version: 0.1.0.alpha3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yusuke Iwaki
@@ -53,6 +53,7 @@ files:
53
53
  - lib/smartest/fixture_definition.rb
54
54
  - lib/smartest/fixture_set.rb
55
55
  - lib/smartest/init_generator.rb
56
+ - lib/smartest/matcher_registry.rb
56
57
  - lib/smartest/matchers.rb
57
58
  - lib/smartest/parameter_extractor.rb
58
59
  - lib/smartest/reporter.rb