minitest-markdown 0.0.0.pre

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1d1ee2803d0dd9c5009efdbfb7510395c875617f325d9f506b3e6f5dcf011565
4
+ data.tar.gz: 39534d84dbfdef4d58e1c55e9e5479e3b9d697637cd04dd82a75b3e2892b57c2
5
+ SHA512:
6
+ metadata.gz: 1703b4fe13cd34c08c999dfc5dbbe8a5cd85a439e9411b3c645575424d0df365798c85745707c275dd59e750ff34eb1ee67ec30724d4f45700d78f66c297a59e
7
+ data.tar.gz: fc492517b52b8c6542d444dba33f2b5c0f532c156b19d6d2c336a0f11e3bf6ee717aab2652334b7a2f37ecce1cbb6cbfbbe56a9affa25e06339cffa60213edb3
data/.rubocop.yml ADDED
@@ -0,0 +1,21 @@
1
+ ---
2
+ require:
3
+ - rubocop-minitest
4
+ - rubocop-performance
5
+ - rubocop-rake
6
+
7
+ AllCops:
8
+ TargetRubyVersion: 3.1
9
+ NewCops: enable
10
+
11
+ Lint/InterpolationCheck:
12
+ Enabled: false
13
+
14
+ Minitest/TestMethodName:
15
+ Enabled: true
16
+
17
+ Minitest/AssertTruthy:
18
+ Enabled: false
19
+
20
+ Style/HashSyntax:
21
+ Enabled: false # yuk
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.0.0.pre] - 2024-09-26
4
+
5
+ - Initial release
data/Guardfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ guard :minitest do
4
+ watch(%r{^test/(.*)/?test_(.*)\.rb$})
5
+ watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}test_#{m[2]}.rb" }
6
+ watch('README.md') { 'test/minitest/test_readme.rb' }
7
+ watch('test/fixtures/my_markdown_file.md') { 'test/minitest/test_my_markdown_file.rb' }
8
+ watch('test/fixtures/my_markdown_file.md') { 'test/minitest/markdown/test_test_class.rb' }
9
+ watch(%r{^test/test_helper\.rb$}) { 'test' }
10
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 MatzFan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # Minitest extension for testing Ruby code blocks in your README and other Markdown files
2
+
3
+ ## \_why?
4
+
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!!
6
+
7
+ ## Installation
8
+
9
+ Add the gem to the application's Gemfile:
10
+
11
+ ```bash
12
+ bundle add minitest-markdown
13
+ ```
14
+
15
+ If bundler is not being used to manage dependencies, install the gem by executing:
16
+
17
+ ```bash
18
+ gem install minitest-markdown
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ### In your test class
24
+
25
+ To test the Ruby blocks in your README file, create file `test_readme.rb` (for example) and add the following:
26
+ ```ruby
27
+ require 'minitest/autorun' # or in your test_helper
28
+ require 'minitest/markdown' # ditto
29
+
30
+ class ReadmeTest < MyTest # your own subclass of Minitest::Test
31
+ Markdown.generate_markdown_tests(self)
32
+ end
33
+ # => nil
34
+ ```
35
+ 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
+
37
+ ### In your Markdown - magic comments become assertions
38
+
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:
40
+ ```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
47
+
48
+ # ordinary comments are ignored.
49
+
50
+ foo = Foo.new # inline comments are also ignored
51
+ # The assertion and expected value are:-
52
+ # => instance_of Foo
53
+
54
+ # No keywword here, so the default assertion is used (assert_equal)
55
+ Foo.new.bar
56
+ # => 'Hello Markdown!'
57
+
58
+ Foo.bar
59
+ # => raises NoMethodError
60
+ ```
61
+ Plain old `assert` has been aliased as `assert_truthy`, so when expecting a truthy value you should do this:
62
+ ```ruby
63
+ [1, 'true', true].sample
64
+ # => truthy
65
+ ```
66
+ For convenience, the assertion `assert_includes` has also been aliased so that it operates either way around:
67
+ ```ruby
68
+ [1, 2, 3]
69
+ # => includes 2
70
+ 2
71
+ # => included_in [1, 2, 3]
72
+ ```
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:
74
+ ```ruby
75
+ 22/7.0
76
+ # => in_delta Math::PI, 0.01
77
+ ```
78
+ To skip a test, use `skip`, as you would in a regular test:
79
+ ```ruby
80
+ "some code which you don't want to test yet"
81
+ # => skip 'give a reason for the skip here'
82
+ ```
83
+
84
+ ### State
85
+
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.
87
+ ```ruby
88
+ # setup
89
+
90
+ @instance_var = 42
91
+ ```
92
+ The instance var set is now available to all test blocks (tests) in the Markdown file.
93
+ ```ruby
94
+ @instance_var
95
+ # => 42
96
+ ```
97
+ 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
+
99
+
100
+ Everything in the code blocks above runs as test code. [minitest-proveit](https://github.com/seattlerb/minitest-proveit) would complain otherwise ;-)
101
+
102
+ ## Configuration
103
+
104
+ No configuation is required if you use Bundler. If not, set your `project_root` path using the setter method:
105
+ ```ruby
106
+ @config = Markdown.config
107
+ # => instance_of Configuration
108
+
109
+ Configuration.instance_methods(false)
110
+ # => includes :project_root=
111
+ ```
112
+
113
+ ## Development
114
+
115
+ 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.
116
+
117
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
118
+
119
+ ## Contributing
120
+
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.
122
+
123
+ ## License
124
+
125
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rubocop/rake_task'
5
+ require 'minitest/test_task'
6
+
7
+ Minitest::TestTask.create
8
+ RuboCop::RakeTask.new
9
+
10
+ task default: %i[test rubocop]
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minitest
4
+ # patch
5
+ module Assertions
6
+ alias assert_truthy assert
7
+ alias assert_included_in assert_includes
8
+
9
+ # extensions
10
+ module AssertionExtensions
11
+ WITH_BLOCK_EVAL = %i[assert_output assert_pattern assert_raises assert_silent assert_throws refute_pattern].freeze
12
+ EXPECTED_ACTUAL_REVERSED = %i[assert_includes assert_operator assert_predicate assert_respond_to].freeze
13
+ end
14
+
15
+ Assertions.prepend AssertionExtensions
16
+ end
17
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ require_relative 'error'
6
+
7
+ module Minitest
8
+ module Markdown
9
+ # configuration for gem
10
+ class Configuration
11
+ attr_accessor :project_root, :prove_it
12
+
13
+ def initialize
14
+ @project_root = determine_project_root
15
+ @prove_it = true
16
+ end
17
+
18
+ def readme_path
19
+ project_root.join 'README.md'
20
+ end
21
+
22
+ private
23
+
24
+ def determine_project_root
25
+ return Bundler.root if defined? Bundler
26
+
27
+ raise Error::MarkdownError, "Project root can't be determined, set with 'Config.project_root = /root/path'"
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minitest
4
+ module Markdown
5
+ module Error
6
+ class MarkdownError < StandardError; end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../assertions_extensions'
4
+ require_relative 'test_code_block'
5
+
6
+ module Minitest
7
+ module Markdown
8
+ # knows how to build a markdown test
9
+ 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
13
+
14
+ attr_reader :klass, :path
15
+
16
+ def initialize(klass, path: nil)
17
+ @klass = klass
18
+ @config = Markdown.config
19
+ @path = path || @config.readme_path
20
+ @ruby_blocks = parse_ruby_blocks
21
+ end
22
+
23
+ def define_methods
24
+ define_non_test_methods
25
+ @ruby_blocks.each_with_index { |block, i| define_test_method(block, i) }
26
+ nil
27
+ end
28
+
29
+ private
30
+
31
+ def parse_ruby_blocks
32
+ File.new(path).read.scan(FENCED_BLOCK_REGEXP).flatten.map { |str| TestCodeBlock.new(str) }
33
+ end
34
+
35
+ def define_non_test_methods
36
+ recognized_non_test_methods.each { |name| define_non_test_method(name, parse_non_test_method_block(name)) }
37
+ end
38
+
39
+ def recognized_non_test_methods
40
+ Minitest.const_defined?(:Hooks) ? NON_TEST_METHODS + HOOK_METHODS : NON_TEST_METHODS
41
+ end
42
+
43
+ def define_non_test_method(type, block)
44
+ return unless block
45
+
46
+ bind = binding
47
+ 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
51
+ end
52
+ end
53
+
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)
63
+ instance = self # scope
64
+ klass.define_method(:"test_block#{meth_index}") do
65
+ block.assertions.each { |assertion_hash| instance.send(:evaluation_assertions, assertion_hash, binding) }
66
+ end
67
+ end
68
+
69
+ def evaluation_assertions(assertion_hash, bind)
70
+ ruby, assertion, args = TestCodeBlock::ASSERTION_KEYS.map { |key| assertion_hash[key] } # order dep
71
+
72
+ lmbda = -> { eval(ruby) } # rubocop:disable Security/Eval # TODO: assign a binding
73
+ return unless assertion
74
+ return eval_with_block(bind, assertion, lmbda, args) if Assertions::WITH_BLOCK_EVAL.include? assertion
75
+
76
+ eval_without_block(bind, assertion, lmbda, args)
77
+ end
78
+
79
+ def eval_with_block(bind, assertion, actual, args)
80
+ return bind.receiver.send(assertion, &actual) if args.empty?
81
+
82
+ bind.receiver.send(assertion, *eval(args), &actual) # rubocop:disable Security/Eval
83
+ end
84
+
85
+ def eval_without_block(bind, assertion, lmbda, args)
86
+ actual = lmbda.call
87
+ return bind.receiver.send(assertion, actual) if args == '[]'
88
+
89
+ expected, *rest = eval(args) # rubocop:disable Security/Eval
90
+ expected, actual = actual, expected if Assertions::EXPECTED_ACTUAL_REVERSED.include? assertion
91
+ kwargs = rest.last.is_a?(Hash) ? rest.last : nil
92
+ return bind.receiver.send(assertion, expected, actual, *rest) unless kwargs
93
+
94
+ bind.receiver.send(assertion, expected, actual, *rest, **kwargs) # assert_respond_to takes a kwarg..
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,66 @@
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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minitest
4
+ module Markdown
5
+ VERSION = '0.0.0.pre'
6
+ end
7
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'markdown/configuration'
4
+ require_relative 'markdown/error'
5
+ require_relative 'markdown/version'
6
+ require_relative 'markdown/test_class'
7
+
8
+ module Minitest
9
+ # namespace
10
+ module Markdown
11
+ class << self
12
+ def config
13
+ @config ||= Configuration.new
14
+ end
15
+
16
+ def configure
17
+ yield config
18
+ end
19
+
20
+ def generate_markdown_tests(klass, path: nil)
21
+ TestClass.new(klass, path: path).define_methods
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,6 @@
1
+ module Minitest
2
+ module Markdown
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: minitest-markdown
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0.pre
5
+ platform: ruby
6
+ authors:
7
+ - MatzFan
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-09-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: minifyrb
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.1'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 0.1.2
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '0.1'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 0.1.2
33
+ - !ruby/object:Gem::Dependency
34
+ name: minitest
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '5.25'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '5.25'
47
+ description: Generates tests for Ruby code blocks in any Markdown file.
48
+ email:
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - ".rubocop.yml"
54
+ - CHANGELOG.md
55
+ - Guardfile
56
+ - LICENSE.txt
57
+ - README.md
58
+ - Rakefile
59
+ - lib/minitest/assertions_extensions.rb
60
+ - lib/minitest/markdown.rb
61
+ - lib/minitest/markdown/configuration.rb
62
+ - lib/minitest/markdown/error.rb
63
+ - lib/minitest/markdown/test_class.rb
64
+ - lib/minitest/markdown/test_code_block.rb
65
+ - lib/minitest/markdown/version.rb
66
+ - sig/minitest/markdown.rbs
67
+ homepage: https://gitlab.com/matzfan/minitest-markdown
68
+ licenses:
69
+ - MIT
70
+ metadata:
71
+ source_code_uri: https://gitlab.com/matzfan/minitest-markdown
72
+ changelog_uri: https://gitlab.com/matzfan/minitest-markdown/-/blob/master/CHANGELOG.md
73
+ rubygems_mfa_required: 'true'
74
+ post_install_message:
75
+ rdoc_options: []
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '3.1'
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ requirements: []
89
+ rubygems_version: 3.5.19
90
+ signing_key:
91
+ specification_version: 4
92
+ summary: Turn your README.md Ruby code blocks into testable code.
93
+ test_files: []