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 +4 -4
- data/.rubocop.yml +4 -1
- data/CHANGELOG.md +36 -0
- data/README.md +110 -29
- data/lib/minitest/assertions_extensions.rb +4 -2
- data/lib/minitest/markdown/configuration.rb +13 -10
- data/lib/minitest/markdown/{error.rb → errors.rb} +1 -3
- data/lib/minitest/markdown/ruby_code_block.rb +85 -0
- data/lib/minitest/markdown/test_class.rb +58 -38
- data/lib/minitest/markdown/version.rb +1 -1
- data/lib/minitest/markdown.rb +12 -3
- data/lib/minitest/stub_chain.rb +41 -0
- metadata +26 -9
- data/lib/minitest/markdown/test_code_block.rb +0 -66
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9d404e86afc3911d3c33f3c6db07e8895fcba5dca70490ceebe23d7e419f18fd
|
4
|
+
data.tar.gz: 6943203dde54269f4c4b4932d0aca354fc6a90b9531ca587956cdc6f90bb4686
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8975c4e35527de62d85c8e92298fe67789ae74e1dcec8b39f2d3edf0527d78ea40ab995b8a08c6e5c303acb2b9bcb2957986a24d225a0f6003e438efdee46671
|
7
|
+
data.tar.gz: 92578fe079a4cf4d90e5b74a3dc629b0517dbae6bdd0a08c236d9ce0f2b6ddb7fa36f380a315165006e2a3e5637b3d8bca5604d52124b0a2275c44bc26880695
|
data/.rubocop.yml
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
---
|
2
|
-
|
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
|
+
[](https://badge.fury.io/rb/minitest-markdown)
|
2
|
+
[](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
|
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 <
|
45
|
+
class ReadmeTest < Minitest::Test # or your own subclass of Minitest::Test
|
31
46
|
Markdown.generate_markdown_tests(self)
|
32
47
|
end
|
33
|
-
# =>
|
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.
|
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
|
-
|
42
|
-
class
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
68
|
+
Klass.new # inline comments are also ignored
|
51
69
|
# The assertion and expected value are:-
|
52
|
-
# => instance_of
|
70
|
+
# => instance_of Klass
|
53
71
|
|
54
|
-
# No
|
55
|
-
|
72
|
+
# No keyword here, so the default assertion is used (assert_equal)
|
73
|
+
Klass.new.hello
|
56
74
|
# => 'Hello Markdown!'
|
57
75
|
|
58
|
-
|
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 `# =>
|
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
|
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
|
-
|
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
|
-
|
95
|
-
|
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
|
-
|
137
|
+
### Mocks
|
103
138
|
|
104
|
-
|
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
|
-
@
|
107
|
-
|
141
|
+
@mymock = Minitest::Mock.new
|
142
|
+
@mymock.expect(:puts, nil, ['Hello World!'])
|
108
143
|
|
109
|
-
|
110
|
-
# =>
|
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
|
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
|
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
|
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 '
|
5
|
+
require_relative 'errors'
|
6
6
|
|
7
7
|
module Minitest
|
8
8
|
module Markdown
|
9
9
|
# configuration for gem
|
10
10
|
class Configuration
|
11
|
-
|
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
|
19
|
-
|
21
|
+
def self.determine_project_root
|
22
|
+
Bundler.root if defined? Bundler
|
20
23
|
end
|
21
24
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
return Bundler.root if defined? Bundler
|
25
|
+
def project_root
|
26
|
+
@project_root || (raise Error, PROJECT_ROOT_UNDETERMINED)
|
27
|
+
end
|
26
28
|
|
27
|
-
|
29
|
+
def readme_path
|
30
|
+
project_root.join 'README.md'
|
28
31
|
end
|
29
32
|
end
|
30
33
|
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 '
|
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
|
11
|
-
|
12
|
-
|
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
|
-
@
|
19
|
-
@
|
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
|
-
|
25
|
-
|
26
|
-
|
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
|
32
|
-
|
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
|
36
|
-
|
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
|
40
|
-
|
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(
|
44
|
-
|
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
|
49
|
-
bind.eval(block)
|
50
|
-
super() if
|
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
|
55
|
-
|
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
|
-
|
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 =
|
83
|
+
ruby, assertion, args = RubyCodeBlock::ASSERTION_KEYS.map { |key| assertion_hash[key] } # order dep
|
71
84
|
|
72
|
-
lmbda = -> {
|
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, *
|
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 =
|
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
|
data/lib/minitest/markdown.rb
CHANGED
@@ -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/
|
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
|
-
|
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
|
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:
|
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/
|
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.
|
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
|