minitest-markdown 0.0.0.pre → 0.0.0

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: 1d1ee2803d0dd9c5009efdbfb7510395c875617f325d9f506b3e6f5dcf011565
4
- data.tar.gz: 39534d84dbfdef4d58e1c55e9e5479e3b9d697637cd04dd82a75b3e2892b57c2
3
+ metadata.gz: 9d404e86afc3911d3c33f3c6db07e8895fcba5dca70490ceebe23d7e419f18fd
4
+ data.tar.gz: 6943203dde54269f4c4b4932d0aca354fc6a90b9531ca587956cdc6f90bb4686
5
5
  SHA512:
6
- metadata.gz: 1703b4fe13cd34c08c999dfc5dbbe8a5cd85a439e9411b3c645575424d0df365798c85745707c275dd59e750ff34eb1ee67ec30724d4f45700d78f66c297a59e
7
- data.tar.gz: fc492517b52b8c6542d444dba33f2b5c0f532c156b19d6d2c336a0f11e3bf6ee717aab2652334b7a2f37ecce1cbb6cbfbbe56a9affa25e06339cffa60213edb3
6
+ metadata.gz: 8975c4e35527de62d85c8e92298fe67789ae74e1dcec8b39f2d3edf0527d78ea40ab995b8a08c6e5c303acb2b9bcb2957986a24d225a0f6003e438efdee46671
7
+ data.tar.gz: 92578fe079a4cf4d90e5b74a3dc629b0517dbae6bdd0a08c236d9ce0f2b6ddb7fa36f380a315165006e2a3e5637b3d8bca5604d52124b0a2275c44bc26880695
data/.rubocop.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- require:
2
+ plugins:
3
3
  - rubocop-minitest
4
4
  - rubocop-performance
5
5
  - rubocop-rake
@@ -7,6 +7,9 @@ require:
7
7
  AllCops:
8
8
  TargetRubyVersion: 3.1
9
9
  NewCops: enable
10
+ Exclude:
11
+ - 'vendor/**/*' # GitLab caching..
12
+ - test/fixtures/klass.rb
10
13
 
11
14
  Lint/InterpolationCheck:
12
15
  Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.0.0] - 2025-05-10
4
+
5
+ - Fix bug in TestCodeBlock error message
6
+ - Add binding to Kernel.eval fixes #1
7
+ - pass kwargs to stub methods in StubProc
8
+
9
+ ## [0.2.2.pre] - 2025-05-08
10
+
11
+ - Improve magic comment & hook comment parsing
12
+ - Fix bug with empty sting after magic comment
13
+ - Fix bug in config if Bundler undefined
14
+
15
+ ## [0.2.1.pre] - 2025-05-07
16
+
17
+ - Fix issue with require in eval
18
+ - Fix bug in StubChain where instance was mutated after #call
19
+
20
+ ## [0.2.0.pre] - 2025-05-06
21
+
22
+ - Add ability to pass any number of stubs to a test code block
23
+
24
+ ### Breaking changes
25
+ - StubChain class replaces Stubb module see https://gitlab.com/matzfan/minitest-markdown#stubbing
26
+
27
+ ## [0.1.0.pre] - 2025-05-01
28
+
29
+ - Add ability to pass stubs to test class
30
+ - Add dependency; minitest-stub_any_instance
31
+
32
+ ## [0.0.1.pre] - 2025-04-24
33
+
34
+ - Bump minitest dep to >= 5.25.2
35
+ - Fix 'method redefined' warning in tests
36
+ - Fix undefined method 'assert' for module 'Minitest::Assertions' in AssertionExtensions
37
+ - Fix bug with expected value strings with multiple spaces
38
+
3
39
  ## [0.0.0.pre] - 2024-09-26
4
40
 
5
41
  - Initial release
data/README.md CHANGED
@@ -1,11 +1,15 @@
1
+ [![Gem Version](https://badge.fury.io/rb/minitest-markdown.svg)](https://badge.fury.io/rb/minitest-markdown)
2
+ [![Ruby Style Guide](https://img.shields.io/badge/code_style-rubocop-brightgreen.svg)](https://github.com/rubocop/rubocop)
3
+
1
4
  # Minitest extension for testing Ruby code blocks in your README and other Markdown files
2
5
 
3
6
  ## \_why?
4
7
 
5
- Document your Gem's usage, examples etc. with fully testable code! Better still, use your README as a BDD aid and specify functionaility in your README/wiki code blocks *before* you write your code!!
8
+ Document your Gem's usage, examples etc. with fully testable code! Better still, use your README as a BDD aid and specify functionality in your README/wiki code blocks *before* you write your code!!
6
9
 
7
10
  ## Installation
8
11
 
12
+
9
13
  Add the gem to the application's Gemfile:
10
14
 
11
15
  ```bash
@@ -18,6 +22,17 @@ If bundler is not being used to manage dependencies, install the gem by executin
18
22
  gem install minitest-markdown
19
23
  ```
20
24
 
25
+ ## Configuration
26
+
27
+ No configuration is required if you use Bundler. If not, set your `project_root` path using the setter method:
28
+ ```ruby
29
+ @config = Minitest::Markdown.config
30
+ # => instance_of Configuration
31
+
32
+ Configuration.instance_methods(false)
33
+ # => includes :project_root=
34
+ ```
35
+
21
36
  ## Usage
22
37
 
23
38
  ### In your test class
@@ -27,36 +42,42 @@ To test the Ruby blocks in your README file, create file `test_readme.rb` (for e
27
42
  require 'minitest/autorun' # or in your test_helper
28
43
  require 'minitest/markdown' # ditto
29
44
 
30
- class ReadmeTest < MyTest # your own subclass of Minitest::Test
45
+ class ReadmeTest < Minitest::Test # or your own subclass of Minitest::Test
31
46
  Markdown.generate_markdown_tests(self)
32
47
  end
33
- # => nil
48
+ # => truthy
34
49
  ```
35
50
  To test Ruby blocks in another Markdown file, create another test file and pass the path to your Markdown file using the `:path` keyword arg i.e. `Markdown.generate_markdown_tests(self, path: '/path/to/your/markdown/file.md')`
36
51
 
37
52
  ### In your Markdown - magic comments become assertions
38
53
 
39
- Each Markdown file is represented by a single test class and each Ruby block in a file becomes a test method with zero or more assertions. The syntax used is `# => ` followed by an assertion keyword. Keywords may be one of the [Minitest "assert_" assertions](https://docs.seattlerb.org/minitest/Minitest/Assertions.html) less the "assert_" prefix (refutations are not implemented at this time). If the keyword is omitted, the default assertion; `assert_equal` is used. The actual value passed to the assertion is the result of the evaluation of the Ruby code above each magic comment. The following block (a single test) includes 3 assertions:
54
+ Each Markdown file is represented by a single test class and each Ruby block* in a file becomes a test method with zero or more assertions. Test methods are named according to their index; `test_block0`, `test_block1` etc.
55
+
56
+ \*Any 'state' blocks are excluded from indexing - see State section below
57
+
58
+ The syntax used for assertions is `# => ` followed by an assertion keyword. Keywords may be any one of the [Minitest "assert_" assertions](https://docs.seattlerb.org/minitest/Minitest/Assertions.html) less the "assert_" prefix (refutations are not implemented at this time). If the keyword is omitted, the default assertion; `assert_equal` is used. The actual value passed to the assertion is the result of the evaluation of the Ruby code above each magic comment. The following block (a single test) includes 6 assertions:
40
59
  ```ruby
41
- # the code will of course be in your lib's, but for demonstration purposes:
42
- class Foo
43
- def bar
44
- 'Hello Markdown!'
45
- end
46
- end
60
+ File.read 'test/fixtures/klass.rb'
61
+ # => "class Klass\n def hello\n 'Hello Markdown!'\n end\nend"
62
+
63
+ require 'test/fixtures/klass' # a demonstration
64
+ # => true
47
65
 
48
66
  # ordinary comments are ignored.
49
67
 
50
- foo = Foo.new # inline comments are also ignored
68
+ Klass.new # inline comments are also ignored
51
69
  # The assertion and expected value are:-
52
- # => instance_of Foo
70
+ # => instance_of Klass
53
71
 
54
- # No keywword here, so the default assertion is used (assert_equal)
55
- Foo.new.bar
72
+ # No keyword here, so the default assertion is used (assert_equal)
73
+ Klass.new.hello
56
74
  # => 'Hello Markdown!'
57
75
 
58
- Foo.bar
76
+ Klass.hello
59
77
  # => raises NoMethodError
78
+
79
+ self
80
+ # => instance_of Markdown::TestClass
60
81
  ```
61
82
  Plain old `assert` has been aliased as `assert_truthy`, so when expecting a truthy value you should do this:
62
83
  ```ruby
@@ -70,7 +91,7 @@ For convenience, the assertion `assert_includes` has also been aliased so that i
70
91
  2
71
92
  # => included_in [1, 2, 3]
72
93
  ```
73
- Everything on the magic comment line after the keyword, or `# => `, if one is omitted, is evaluated as Ruby code. Note: **inline comments are NOT ALLOWED on magic comment lines**. Where an assertion takes multiple positional args, these are simply separated by commas. Note that the assertion keyword itself is **not an argument**. The syntax is as follows:
94
+ Everything on the magic comment line after the assertion keyword, or `# => ` if one is omitted, is evaluated as Ruby code. Note: **inline comments are NOT ALLOWED on magic comment lines**. Where an assertion takes multiple positional args, these are simply separated by commas. Note that the assertion keyword itself is **not an argument**. The syntax is as follows:
74
95
  ```ruby
75
96
  22/7.0
76
97
  # => in_delta Math::PI, 0.01
@@ -80,36 +101,96 @@ To skip a test, use `skip`, as you would in a regular test:
80
101
  "some code which you don't want to test yet"
81
102
  # => skip 'give a reason for the skip here'
82
103
  ```
104
+ Test failures will look like this - note the method name `test_block10` in this example:
105
+ ```
106
+ Minitest::Markdown::ReadmeTest#test_block10 [lib/minitest/markdown/test_class.rb:118]:
107
+ Expected: 42
108
+ Actual: 0
109
+ ```
83
110
 
84
111
  ### State
85
112
 
86
- Minitest's `setup` and `teardown` methods are generated by using the appropriate comment on the first line of a code block. Magic comments are ignored in such blocks, as these are not tests. E.g.
113
+ Instance vars are shared across test methods within a class, but as Minitest's default is to run tests in random order you may want to use a setup block in order to ensure a stored value is available to all test blocks (tests) in the Markdown file test class (see below):
114
+ ```ruby
115
+ @instance_var
116
+ # => 7
117
+ ```
118
+ Minitest's `setup` and `teardown` methods are generated by using the appropriate comment on the first line of a code block. Assertion magic comments are ignored in such blocks, as these are not tests. E.g.
119
+
87
120
  ```ruby
88
121
  # setup
89
122
 
90
- @instance_var = 42
123
+ # do some setup task - or:-
124
+ @instance_var = 7 # now available in all test method blocks, including the one above
125
+ # => ignored
91
126
  ```
92
- The instance var set is now available to all test blocks (tests) in the Markdown file.
93
127
  ```ruby
94
- @instance_var
95
- # => 42
128
+ # teardown
129
+
130
+ # do some teardown task
96
131
  ```
97
132
  The hook methods defined in the [minitest-hooks](https://github.com/jeremyevans/minitest-hooks?tab=readme-ov-file#in-tests-minitesttest-) extension (`before_all`, `after_all`, `around` & `around_all`)are also available in this way if `minitest-hooks` is installed and `Minitest::Hooks` is included in your test class.
98
133
 
99
134
 
100
- Everything in the code blocks above runs as test code. [minitest-proveit](https://github.com/seattlerb/minitest-proveit) would complain otherwise ;-)
135
+ Everything in the Ruby code blocks above and below here runs as test code. [minitest-proveit](https://github.com/seattlerb/minitest-proveit) would complain otherwise ;-)
101
136
 
102
- ## Configuration
137
+ ### Mocks
103
138
 
104
- No configuation is required if you use Bundler. If not, set your `project_root` path using the setter method:
139
+ Mocks use the "mock" keyword for [assert_mock](https://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_mock) in place of the equivalent `mock.verify`:
105
140
  ```ruby
106
- @config = Markdown.config
107
- # => instance_of Configuration
141
+ @mymock = Minitest::Mock.new
142
+ @mymock.expect(:puts, nil, ['Hello World!'])
108
143
 
109
- Configuration.instance_methods(false)
110
- # => includes :project_root=
144
+ @mymock.puts 'Hello World!'
145
+ # => mock @mymock
146
+ ```
147
+
148
+ ### Stubbing
149
+
150
+ It is possible to pass stubs to the generated tests. This is done using the stubs keyword. Hash keys represent the index of the test code block. **Important: any 'state' blocks are ignored for test code block indexing**. The hash value must be an instance of `Minitest::StubChain` which holds a proc for each stub in the chain. For convenience `StubChain.stubproc` returns a suitable stub proc object which is called around the relevant test code. Zero or more of these reusable procs can be used to instantiate a StubChain object:
151
+ ```ruby
152
+ class MarkdownTest < Minitest::Test
153
+ set_stubs_new = Minitest::StubChain.stubproc(Set, :new, []) # returns a proc which stubs Set.new to return an empty array
154
+ array_stubs_size = Minitest::StubChain.stubproc(Array, :size, 42, any_instance: true) # uses the bundled minitest-stub_any_instance gem
155
+
156
+ stub_chain = Minitest::StubChain.new([set_stubs_new]) # initialized with zero or more stub procs
157
+ stub_chain.stubs << array_stubs_size # add another like this if need be
158
+
159
+ stubs = {}
160
+ stubs[10] = stub_chain
161
+ stubs[11] = stub_chain # StubChain instances themselves may be reused
162
+
163
+ Markdown.generate_markdown_tests(self, stubs: stubs)
164
+ end
165
+ # => truthy
166
+ ```
167
+ The 2 stubs in the `StubChain` instance above are demonstrated in the following example:
168
+ ```ruby
169
+ # This is test_block10
170
+ Set.new([1, 'c', :s]).size
171
+ # => 42
172
+
173
+ # Because we added `stubs[10] = stub_chain` above, this is exactly equivalent to:
174
+ #
175
+ # def test_block10
176
+ # Set.stub(:new, []) do
177
+ # Array.stub_any_instance(:size, 42) do
178
+ # assert_equal 42, Set.new([1, 'c', :s]).size
179
+ # end
180
+ # end
181
+ # end
182
+ ```
183
+ Example showing the reuse of a StubChain:
184
+ ```ruby
185
+ # This is test_block11
186
+ Set.new.size
187
+ # => 42
111
188
  ```
112
189
 
190
+ ### Errors
191
+
192
+ All errors subclass `Markdown::Error`
193
+
113
194
  ## Development
114
195
 
115
196
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -118,7 +199,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
118
199
 
119
200
  ## Contributing
120
201
 
121
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/minitest-markdown. Please checkout a suitably named branch before submitting a PR.
202
+ Bug reports and pull requests are welcome on GitLab at https://gitlab.com/matzfan/minitest-markdown. Please checkout a suitably named branch before submitting a PR.
122
203
 
123
204
  ## License
124
205
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'minitest/assertions'
4
+
3
5
  module Minitest
4
6
  # patch
5
7
  module Assertions
@@ -7,11 +9,11 @@ module Minitest
7
9
  alias assert_included_in assert_includes
8
10
 
9
11
  # extensions
10
- module AssertionExtensions
12
+ module AssertionsExtensions
11
13
  WITH_BLOCK_EVAL = %i[assert_output assert_pattern assert_raises assert_silent assert_throws refute_pattern].freeze
12
14
  EXPECTED_ACTUAL_REVERSED = %i[assert_includes assert_operator assert_predicate assert_respond_to].freeze
13
15
  end
14
16
 
15
- Assertions.prepend AssertionExtensions
17
+ Assertions.prepend AssertionsExtensions
16
18
  end
17
19
  end
@@ -2,29 +2,32 @@
2
2
 
3
3
  require 'pathname'
4
4
 
5
- require_relative 'error'
5
+ require_relative 'errors'
6
6
 
7
7
  module Minitest
8
8
  module Markdown
9
9
  # configuration for gem
10
10
  class Configuration
11
- attr_accessor :project_root, :prove_it
11
+ PROJECT_ROOT_UNDETERMINED = "Project root can't be determined, set with 'Markdown.config.project_root = /path'"
12
+
13
+ attr_writer :project_root
14
+ attr_accessor :prove_it
12
15
 
13
16
  def initialize
14
- @project_root = determine_project_root
17
+ @project_root = self.class.determine_project_root
15
18
  @prove_it = true
16
19
  end
17
20
 
18
- def readme_path
19
- project_root.join 'README.md'
21
+ def self.determine_project_root
22
+ Bundler.root if defined? Bundler
20
23
  end
21
24
 
22
- private
23
-
24
- def determine_project_root
25
- return Bundler.root if defined? Bundler
25
+ def project_root
26
+ @project_root || (raise Error, PROJECT_ROOT_UNDETERMINED)
27
+ end
26
28
 
27
- raise Error::MarkdownError, "Project root can't be determined, set with 'Config.project_root = /root/path'"
29
+ def readme_path
30
+ project_root.join 'README.md'
28
31
  end
29
32
  end
30
33
  end
@@ -2,8 +2,6 @@
2
2
 
3
3
  module Minitest
4
4
  module Markdown
5
- module Error
6
- class MarkdownError < StandardError; end
7
- end
5
+ class Error < StandardError; end
8
6
  end
9
7
  end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minifyrb'
4
+
5
+ require_relative '../assertions_extensions' # for assert_truthy
6
+ require_relative '../markdown/errors'
7
+
8
+ module Minitest
9
+ module Markdown
10
+ # A Ruby code block representing test code, with 0 or more assertions or a 'state' block
11
+ class RubyCodeBlock
12
+ ASSERT_ = 'assert_'
13
+ ASSERT_REGEXP = /\A#{ASSERT_}/
14
+ SKIP = { skip: :skip }.freeze
15
+ DEFAULT_ASSERTION = :assert_equal
16
+
17
+ ASSERTION_KEYS = %i[ruby assertion test_args].freeze
18
+
19
+ HOOK_METHODS = %i[before_all after_all around around_all].freeze # minitest-hooks extension
20
+ STATE_METHODS = %i[setup teardown].freeze
21
+ STATE_BLOCK_TYPES = Minitest.const_defined?(:Hooks) ? STATE_METHODS + HOOK_METHODS : STATE_METHODS
22
+
23
+ MAGIC_COMMENT_REGEXP = /^\s*#\s*=>.*$/
24
+ MAGIC_COMMENT_SCAN_REGEXP = /^\s*#\s*=>\s*(.*)$/ # strips delimiter prefix
25
+
26
+ MISSING_ASSERTION_OR_VALUE = "Magic comment missing assertion or value. Something must follow '# =>'"
27
+
28
+ attr_reader :fenced_block_str
29
+
30
+ def self.asserts
31
+ @asserts ||= Assertions.instance_methods.grep(ASSERT_REGEXP) - [:assert_send] # deprecated
32
+ end
33
+
34
+ def self.assertions_map
35
+ @assertions_map ||= asserts.inject(SKIP) { |memo, m| memo.merge(m.to_s.split(ASSERT_)[1..].join.to_sym => m) }
36
+ end
37
+
38
+ def initialize(fenced_block_str)
39
+ @fenced_block_str = fenced_block_str
40
+ end
41
+
42
+ def type
43
+ STATE_BLOCK_TYPES.each { |type| return type if fenced_block_str.lines.first&.match?(/^\s*#\s*#{type}\s*\n$/) }
44
+
45
+ :test
46
+ end
47
+
48
+ def assertions
49
+ assertions_arr.map { |arr| Hash[*ASSERTION_KEYS.zip(arr).flatten] }
50
+ end
51
+
52
+ private
53
+
54
+ def assertions_arr
55
+ code_strings.zip(magic_comments.map { |s| parse(s) }).map(&:flatten)
56
+ end
57
+
58
+ def parse(magic_comment_string)
59
+ raise ArgumentError, MISSING_ASSERTION_OR_VALUE if magic_comment_string.strip.empty?
60
+
61
+ assertion = parse_assertion(magic_comment_string)
62
+ return [DEFAULT_ASSERTION, "[#{magic_comment_string}]"] if assertion.nil?
63
+ return [assertion, '[]'] if magic_comment_string.split.size == 1
64
+
65
+ [assertion, "[#{magic_comment_string.sub(/#{assertion.to_s.sub(ASSERT_, '')}\s+/, '')}]"]
66
+ end
67
+
68
+ def parse_assertion(str)
69
+ self.class.assertions_map[str.split.first.to_sym] # 'nil' => :assert_nil, but 'nil?' => nil
70
+ end
71
+
72
+ def magic_comments
73
+ @fenced_block_str.scan(MAGIC_COMMENT_SCAN_REGEXP).flatten
74
+ end
75
+
76
+ def code_strings
77
+ @fenced_block_str.split(MAGIC_COMMENT_REGEXP).map do |str|
78
+ Minifyrb::Minifier.new(str).minify.strip
79
+ rescue SyntaxError
80
+ raise Error, "Syntax error in:\n\n#{str}"
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -1,75 +1,88 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../assertions_extensions'
4
- require_relative 'test_code_block'
4
+ require_relative 'ruby_code_block'
5
+
6
+ $LOAD_PATH << '.' # fix #2
5
7
 
6
8
  module Minitest
7
9
  module Markdown
8
- # knows how to build a markdown test
10
+ # knows how to build a markdown test class, test methods and 'state' methods
9
11
  class TestClass
10
- FENCED_BLOCK_REGEXP = /^```ruby\n(.*?)\n```/m # TODO: use Lexer/RDoc::Parser? which can deal with md comments
11
- HOOK_METHODS = %i[before_all after_all around around_all].freeze # minitest-hooks extension
12
- NON_TEST_METHODS = %i[setup teardown].freeze
12
+ FENCED_BLOCK_REGEXP = /^```ruby\n(.*?)\n```/m
13
+
14
+ BAD_KLASS = 'TestClass must be instantiated with Minitest::Test or subclass thereof'
15
+ BAD_PATH = 'Path does not exist, is not readable or is not a Markdown file:'
16
+ MD = '.md'
17
+
18
+ HOOK_METHODS_BEFORE = %i[before_all].freeze
19
+ HOOK_METHODS_AFTER = %i[after_all around around_all].freeze
13
20
 
14
21
  attr_reader :klass, :path
15
22
 
16
23
  def initialize(klass, path: nil)
17
- @klass = klass
18
- @config = Markdown.config
19
- @path = path || @config.readme_path
20
- @ruby_blocks = parse_ruby_blocks
24
+ @klass = validate_class klass
25
+ @path = validate_path path
26
+ @state_methods_defined = []
21
27
  end
22
28
 
23
- def define_methods
24
- define_non_test_methods
25
- @ruby_blocks.each_with_index { |block, i| define_test_method(block, i) }
26
- nil
29
+ def define_methods(stubs: {})
30
+ i = 0
31
+ parse_ruby_blocks.each do |block|
32
+ next define_non_test_method(block) unless block.type == :test
33
+
34
+ define_test_method(block, i, stub_chain: stubs[i])
35
+ i += 1
36
+ end
27
37
  end
28
38
 
29
39
  private
30
40
 
31
- def parse_ruby_blocks
32
- File.new(path).read.scan(FENCED_BLOCK_REGEXP).flatten.map { |str| TestCodeBlock.new(str) }
41
+ def validate_class(klass)
42
+ raise ArgumentError, BAD_KLASS unless klass.respond_to?(:ancestors) && klass.ancestors.include?(Minitest::Test)
43
+
44
+ klass
33
45
  end
34
46
 
35
- def define_non_test_methods
36
- recognized_non_test_methods.each { |name| define_non_test_method(name, parse_non_test_method_block(name)) }
47
+ def validate_path(pth)
48
+ return Markdown.config.readme_path if pth.nil?
49
+ raise ArgumentError, "#{BAD_PATH} #{pth}" unless File.exist?(pth) && File.file?(pth) && File.extname(pth) == MD
50
+
51
+ pth
37
52
  end
38
53
 
39
- def recognized_non_test_methods
40
- Minitest.const_defined?(:Hooks) ? NON_TEST_METHODS + HOOK_METHODS : NON_TEST_METHODS
54
+ def parse_ruby_blocks
55
+ File.new(path).read.scan(FENCED_BLOCK_REGEXP).flatten.map { |str| RubyCodeBlock.new(str) }
41
56
  end
42
57
 
43
- def define_non_test_method(type, block)
44
- return unless block
58
+ def define_non_test_method(block)
59
+ type = block.type
60
+ raise Error, "State method: #{type} is already defined" if @state_methods_defined.include? type
45
61
 
46
62
  bind = binding
47
63
  klass.define_method(type) do
48
- super() if type == :before_all
49
- bind.eval(block)
50
- super() if %i[after_all around around_all].include? type
64
+ super() if HOOK_METHODS_BEFORE.include? type
65
+ bind.eval(block.fenced_block_str)
66
+ super() if HOOK_METHODS_AFTER.include? type
51
67
  end
68
+ @state_methods_defined << type
52
69
  end
53
70
 
54
- def parse_non_test_method_block(type)
55
- blocks = @ruby_blocks.map.select { |blk| blk.fenced_block_str.lines.first == "# #{type}\n" }
56
- return if blocks.empty?
57
- raise Error::MarkdownError, "Multiple #{type} blocks are not allowed" if blocks.size > 1
58
-
59
- @ruby_blocks.delete(blocks.first).fenced_block_str # hook method blocks can't be test methods
60
- end
61
-
62
- def define_test_method(block, meth_index)
71
+ def define_test_method(block, meth_index, stub_chain:)
72
+ stub_chain ||= StubChain.new
63
73
  instance = self # scope
64
74
  klass.define_method(:"test_block#{meth_index}") do
65
- block.assertions.each { |assertion_hash| instance.send(:evaluation_assertions, assertion_hash, binding) }
75
+ test_code_proc = proc do
76
+ block.assertions.each { |assertion_hash| instance.send(:evaluation_assertions, assertion_hash, binding) }
77
+ end
78
+ stub_chain.call(test_code_proc)
66
79
  end
67
80
  end
68
81
 
69
82
  def evaluation_assertions(assertion_hash, bind)
70
- ruby, assertion, args = TestCodeBlock::ASSERTION_KEYS.map { |key| assertion_hash[key] } # order dep
83
+ ruby, assertion, args = RubyCodeBlock::ASSERTION_KEYS.map { |key| assertion_hash[key] } # order dep
71
84
 
72
- lmbda = -> { eval(ruby) } # rubocop:disable Security/Eval # TODO: assign a binding
85
+ lmbda = -> { kernel_eval(ruby) }
73
86
  return unless assertion
74
87
  return eval_with_block(bind, assertion, lmbda, args) if Assertions::WITH_BLOCK_EVAL.include? assertion
75
88
 
@@ -79,20 +92,27 @@ module Minitest
79
92
  def eval_with_block(bind, assertion, actual, args)
80
93
  return bind.receiver.send(assertion, &actual) if args.empty?
81
94
 
82
- bind.receiver.send(assertion, *eval(args), &actual) # rubocop:disable Security/Eval
95
+ bind.receiver.send(assertion, *kernel_eval(args), &actual)
83
96
  end
84
97
 
85
98
  def eval_without_block(bind, assertion, lmbda, args)
86
99
  actual = lmbda.call
87
100
  return bind.receiver.send(assertion, actual) if args == '[]'
88
101
 
89
- expected, *rest = eval(args) # rubocop:disable Security/Eval
102
+ expected, *rest = kernel_eval(args)
90
103
  expected, actual = actual, expected if Assertions::EXPECTED_ACTUAL_REVERSED.include? assertion
91
104
  kwargs = rest.last.is_a?(Hash) ? rest.last : nil
92
105
  return bind.receiver.send(assertion, expected, actual, *rest) unless kwargs
93
106
 
94
107
  bind.receiver.send(assertion, expected, actual, *rest, **kwargs) # assert_respond_to takes a kwarg..
95
108
  end
109
+
110
+ def kernel_eval(str)
111
+ bind = binding
112
+ bind.eval str
113
+ rescue SyntaxError
114
+ raise ArgumentError, "Invalid test code, failed to parse #{str}"
115
+ end
96
116
  end
97
117
  end
98
118
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Minitest
4
4
  module Markdown
5
- VERSION = '0.0.0.pre'
5
+ VERSION = '0.0.0'
6
6
  end
7
7
  end
@@ -1,13 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'minitest/stub_any_instance'
4
+
5
+ require_relative 'stub_chain'
3
6
  require_relative 'markdown/configuration'
4
- require_relative 'markdown/error'
7
+ require_relative 'markdown/errors'
5
8
  require_relative 'markdown/version'
6
9
  require_relative 'markdown/test_class'
7
10
 
8
11
  module Minitest
9
12
  # namespace
10
13
  module Markdown
14
+ ARG_ERR = 'stubs keyword takes a hash. Keys are integers and values are StubChain instances'
15
+
11
16
  class << self
12
17
  def config
13
18
  @config ||= Configuration.new
@@ -17,8 +22,12 @@ module Minitest
17
22
  yield config
18
23
  end
19
24
 
20
- def generate_markdown_tests(klass, path: nil)
21
- TestClass.new(klass, path: path).define_methods
25
+ def generate_markdown_tests(klass, path: nil, stubs: {})
26
+ raise ArgumentError, ARG_ERR unless stubs.is_a? Hash
27
+ raise ArgumentError, ARG_ERR unless stubs.keys.all? { |o| o.instance_of? Integer }
28
+ raise ArgumentError, ARG_ERR unless stubs.values.all? { |o| o.instance_of? StubChain }
29
+
30
+ TestClass.new(klass, path: path).define_methods(stubs: stubs)
22
31
  end
23
32
  end
24
33
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minitest
4
+ # respresentaion of zero or more stub blocks around one or more test assertions
5
+ class StubChain
6
+ NOT_CALLABLE_ERR = 'StubChain#call takes a callable argument'
7
+ MUST_NOT_CALL_A_BLOCK_ERR = 'StubChain#call takes a callable argument which must not call a block'
8
+
9
+ attr_reader :stubs
10
+
11
+ def initialize(stubs = [])
12
+ @stubs = *stubs
13
+ end
14
+
15
+ def self.stubproc(klass, *args, any_instance: false, **kwargs)
16
+ return proc { |&blk| klass.stub(*args, **kwargs) { blk.call } } unless any_instance
17
+
18
+ proc { |&blk| klass.stub_any_instance(*args, **kwargs) { blk.call } }
19
+ end
20
+
21
+ def call(test_code_proc)
22
+ validate(test_code_proc)
23
+ call_recursive [*stubs, test_code_proc]
24
+ end
25
+
26
+ private
27
+
28
+ def validate(test_code_proc)
29
+ raise ArgumentError, NOT_CALLABLE_ERR unless test_code_proc.respond_to?(:call)
30
+
31
+ test_code_proc
32
+ end
33
+
34
+ def call_recursive(call_chain)
35
+ prok = call_chain.shift
36
+ return prok.call if call_chain.empty?
37
+
38
+ prok.call { call_recursive(call_chain) }
39
+ end
40
+ end
41
+ end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: minitest-markdown
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0.pre
4
+ version: 0.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - MatzFan
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2024-09-26 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: minifyrb
@@ -37,6 +36,9 @@ dependencies:
37
36
  - - "~>"
38
37
  - !ruby/object:Gem::Version
39
38
  version: '5.25'
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: 5.25.2
40
42
  type: :runtime
41
43
  prerelease: false
42
44
  version_requirements: !ruby/object:Gem::Requirement
@@ -44,8 +46,24 @@ dependencies:
44
46
  - - "~>"
45
47
  - !ruby/object:Gem::Version
46
48
  version: '5.25'
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: 5.25.2
52
+ - !ruby/object:Gem::Dependency
53
+ name: minitest-stub_any_instance
54
+ requirement: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - "~>"
57
+ - !ruby/object:Gem::Version
58
+ version: '1.0'
59
+ type: :runtime
60
+ prerelease: false
61
+ version_requirements: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - "~>"
64
+ - !ruby/object:Gem::Version
65
+ version: '1.0'
47
66
  description: Generates tests for Ruby code blocks in any Markdown file.
48
- email:
49
67
  executables: []
50
68
  extensions: []
51
69
  extra_rdoc_files: []
@@ -59,10 +77,11 @@ files:
59
77
  - lib/minitest/assertions_extensions.rb
60
78
  - lib/minitest/markdown.rb
61
79
  - lib/minitest/markdown/configuration.rb
62
- - lib/minitest/markdown/error.rb
80
+ - lib/minitest/markdown/errors.rb
81
+ - lib/minitest/markdown/ruby_code_block.rb
63
82
  - lib/minitest/markdown/test_class.rb
64
- - lib/minitest/markdown/test_code_block.rb
65
83
  - lib/minitest/markdown/version.rb
84
+ - lib/minitest/stub_chain.rb
66
85
  - sig/minitest/markdown.rbs
67
86
  homepage: https://gitlab.com/matzfan/minitest-markdown
68
87
  licenses:
@@ -71,7 +90,6 @@ metadata:
71
90
  source_code_uri: https://gitlab.com/matzfan/minitest-markdown
72
91
  changelog_uri: https://gitlab.com/matzfan/minitest-markdown/-/blob/master/CHANGELOG.md
73
92
  rubygems_mfa_required: 'true'
74
- post_install_message:
75
93
  rdoc_options: []
76
94
  require_paths:
77
95
  - lib
@@ -86,8 +104,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
86
104
  - !ruby/object:Gem::Version
87
105
  version: '0'
88
106
  requirements: []
89
- rubygems_version: 3.5.19
90
- signing_key:
107
+ rubygems_version: 3.6.8
91
108
  specification_version: 4
92
109
  summary: Turn your README.md Ruby code blocks into testable code.
93
110
  test_files: []
@@ -1,66 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'minifyrb' # DEP
4
-
5
- require_relative '../assertions_extensions' # for assert_truthy
6
- require_relative '../markdown/error'
7
-
8
- module Minitest
9
- module Markdown
10
- # A Ruby code block reprenting test code, with 0 or more assertions
11
- class TestCodeBlock
12
- ASSERT_ = 'assert_'
13
- ASSERT_REGEXP = /\A#{ASSERT_}/
14
- SKIP = { skip: :skip }.freeze
15
- DEFAULT_ASSERTION = :assert_equal
16
-
17
- ASSERTION_KEYS = %i[ruby assertion test_args].freeze
18
-
19
- MAGIC_COMMENT_DELIMITER = '# => '
20
- MAGIC_COMMENT_REGEXP = /^#{MAGIC_COMMENT_DELIMITER}.*$/
21
- MAGIC_COMMENT_SCAN_REGEXP = /^#{MAGIC_COMMENT_DELIMITER}(.*)$/ # strips delimiter prefix
22
-
23
- attr_reader :fenced_block_str
24
-
25
- def self.asserts
26
- Assertions.instance_methods.grep(ASSERT_REGEXP) - [:assert_send] # deprecated
27
- end
28
-
29
- def self.assertions_map
30
- asserts.inject({}) { |memo, m| memo.merge(m.to_s.split(ASSERT_)[1..].join.to_sym => m) }.merge(SKIP)
31
- end
32
-
33
- def initialize(fenced_block_str)
34
- @fenced_block_str = fenced_block_str
35
- end
36
-
37
- def assertions
38
- assertions_arr.map { |arr| Hash[*ASSERTION_KEYS.zip(arr).flatten] }
39
- end
40
-
41
- private
42
-
43
- def assertions_arr
44
- code_strings.zip(magic_comments.map { |s| parse(s) }).map(&:flatten)
45
- end
46
-
47
- def parse(magic_comment_string)
48
- arr = magic_comment_string.split
49
- assertion = self.class.assertions_map[arr.first.to_sym]
50
- assertion ? [assertion, "[#{arr[1..].join(' ')}]"] : [DEFAULT_ASSERTION, "[#{arr.join(' ')}]"] # args in []
51
- end
52
-
53
- def magic_comments
54
- @fenced_block_str.scan(MAGIC_COMMENT_SCAN_REGEXP).flatten
55
- end
56
-
57
- def code_strings
58
- @fenced_block_str.split(MAGIC_COMMENT_REGEXP).map do |str|
59
- Minifyrb::Minifier.new(str).minify.strip
60
- rescue SyntaxError
61
- raise Error::MarkdownError, "Syntax error in:\n\n#{str}"
62
- end
63
- end
64
- end
65
- end
66
- end