asciidoctor-doctest 1.5.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 +7 -0
- data/CHANGELOG.adoc +0 -0
- data/LICENSE +21 -0
- data/README.adoc +327 -0
- data/Rakefile +12 -0
- data/data/examples/asciidoc/block_admonition.adoc +27 -0
- data/data/examples/asciidoc/block_audio.adoc +13 -0
- data/data/examples/asciidoc/block_colist.adoc +46 -0
- data/data/examples/asciidoc/block_dlist.adoc +99 -0
- data/data/examples/asciidoc/block_example.adoc +21 -0
- data/data/examples/asciidoc/block_floating_title.adoc +27 -0
- data/data/examples/asciidoc/block_image.adoc +28 -0
- data/data/examples/asciidoc/block_listing.adoc +68 -0
- data/data/examples/asciidoc/block_literal.adoc +30 -0
- data/data/examples/asciidoc/block_olist.adoc +55 -0
- data/data/examples/asciidoc/block_open.adoc +40 -0
- data/data/examples/asciidoc/block_outline.adoc +60 -0
- data/data/examples/asciidoc/block_page_break.adoc +6 -0
- data/data/examples/asciidoc/block_paragraph.adoc +17 -0
- data/data/examples/asciidoc/block_pass.adoc +5 -0
- data/data/examples/asciidoc/block_preamble.adoc +19 -0
- data/data/examples/asciidoc/block_quote.adoc +30 -0
- data/data/examples/asciidoc/block_sidebar.adoc +22 -0
- data/data/examples/asciidoc/block_stem.adoc +28 -0
- data/data/examples/asciidoc/block_table.adoc +168 -0
- data/data/examples/asciidoc/block_thematic_break.adoc +2 -0
- data/data/examples/asciidoc/block_toc.adoc +50 -0
- data/data/examples/asciidoc/block_ulist.adoc +43 -0
- data/data/examples/asciidoc/block_verse.adoc +37 -0
- data/data/examples/asciidoc/block_video.adoc +24 -0
- data/data/examples/asciidoc/document.adoc +51 -0
- data/data/examples/asciidoc/embedded.adoc +10 -0
- data/data/examples/asciidoc/inline_anchor.adoc +27 -0
- data/data/examples/asciidoc/inline_break.adoc +8 -0
- data/data/examples/asciidoc/inline_button.adoc +3 -0
- data/data/examples/asciidoc/inline_callout.adoc +5 -0
- data/data/examples/asciidoc/inline_footnote.adoc +9 -0
- data/data/examples/asciidoc/inline_image.adoc +44 -0
- data/data/examples/asciidoc/inline_kbd.adoc +7 -0
- data/data/examples/asciidoc/inline_menu.adoc +11 -0
- data/data/examples/asciidoc/inline_quoted.adoc +59 -0
- data/data/examples/asciidoc/section.adoc +74 -0
- data/doc/img/doctest-diag.odf +0 -0
- data/doc/img/doctest-diag.svg +56 -0
- data/doc/img/failing-test-term.gif +0 -0
- data/lib/asciidoctor-doctest.rb +1 -0
- data/lib/asciidoctor/doctest.rb +30 -0
- data/lib/asciidoctor/doctest/asciidoc/examples_suite.rb +44 -0
- data/lib/asciidoctor/doctest/asciidoc_renderer.rb +103 -0
- data/lib/asciidoctor/doctest/base_example.rb +161 -0
- data/lib/asciidoctor/doctest/base_examples_suite.rb +188 -0
- data/lib/asciidoctor/doctest/core_ext.rb +49 -0
- data/lib/asciidoctor/doctest/generator.rb +63 -0
- data/lib/asciidoctor/doctest/generator_task.rb +111 -0
- data/lib/asciidoctor/doctest/html/example.rb +21 -0
- data/lib/asciidoctor/doctest/html/examples_suite.rb +111 -0
- data/lib/asciidoctor/doctest/html/html_beautifier.rb +17 -0
- data/lib/asciidoctor/doctest/html/normalizer.rb +118 -0
- data/lib/asciidoctor/doctest/minitest_diffy.rb +74 -0
- data/lib/asciidoctor/doctest/test.rb +120 -0
- data/lib/asciidoctor/doctest/version.rb +5 -0
- data/spec/asciidoc/examples_suite_spec.rb +99 -0
- data/spec/base_example_spec.rb +176 -0
- data/spec/core_ext_spec.rb +67 -0
- data/spec/html/examples_suite_spec.rb +249 -0
- data/spec/html/normalizer_spec.rb +70 -0
- data/spec/shared_examples/base_examples_suite.rb +262 -0
- data/spec/spec_helper.rb +33 -0
- data/spec/support/matchers.rb +7 -0
- data/spec/test_spec.rb +164 -0
- metadata +360 -0
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'colorize'
|
2
|
+
require 'diffy'
|
3
|
+
|
4
|
+
module Asciidoctor
|
5
|
+
module DocTest
|
6
|
+
##
|
7
|
+
# Module to be included into +Minitest::Test+ to use Diffy for diff.
|
8
|
+
module MinitestDiffy
|
9
|
+
|
10
|
+
# @private
|
11
|
+
def self.included(base)
|
12
|
+
base.make_my_diffs_pretty!
|
13
|
+
end
|
14
|
+
|
15
|
+
##
|
16
|
+
# Returns diff between +exp+ and +act+ (if needed) using Diffy.
|
17
|
+
#
|
18
|
+
# @note Overrides method from +Minitest::Assertions+.
|
19
|
+
def diff(exp, act)
|
20
|
+
expected = mu_pp_for_diff(exp)
|
21
|
+
actual = mu_pp_for_diff(act)
|
22
|
+
|
23
|
+
if need_diff? expected, actual
|
24
|
+
::Diffy::Diff.new(expected, actual, context: 3).to_s
|
25
|
+
else
|
26
|
+
"Expected: #{mu_pp(exp)}\n Actual: #{mu_pp(act)}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Returns +true+ if diff should be printed (using Diffy) for the given
|
32
|
+
# content, +false+ otherwise.
|
33
|
+
#
|
34
|
+
# @param expected [String]
|
35
|
+
# @param actual [String]
|
36
|
+
#
|
37
|
+
def need_diff?(expected, actual)
|
38
|
+
expected.include?("\n") ||
|
39
|
+
actual.include?("\n") ||
|
40
|
+
expected.size > 30 ||
|
41
|
+
actual.size > 30 ||
|
42
|
+
expected == actual
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
module Diffy
|
49
|
+
module Format
|
50
|
+
|
51
|
+
##
|
52
|
+
# ANSI color output suitable for terminal, customized for minitest.
|
53
|
+
def minitest
|
54
|
+
padding = ' ' * 2
|
55
|
+
ary = map do |line|
|
56
|
+
case line
|
57
|
+
when /^(---|\+\+\+|\\\\)/
|
58
|
+
# ignore
|
59
|
+
when /^\\\s*No newline at end of file/
|
60
|
+
# ignore
|
61
|
+
when /^\+/
|
62
|
+
line.chomp.sub(/^\+/, 'A' + padding).red
|
63
|
+
when /^-/
|
64
|
+
line.chomp.sub(/^\-/, 'E' + padding).green
|
65
|
+
else
|
66
|
+
padding + line.chomp
|
67
|
+
end
|
68
|
+
end
|
69
|
+
"\n" + ary.join("\n")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
Diffy::Diff.default_format = :minitest
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'active_support/core_ext/object/blank'
|
2
|
+
require 'asciidoctor/doctest/asciidoc_renderer'
|
3
|
+
require 'asciidoctor/doctest/core_ext'
|
4
|
+
require 'asciidoctor/doctest/minitest_diffy'
|
5
|
+
require 'asciidoctor/doctest/asciidoc/examples_suite'
|
6
|
+
require 'minitest'
|
7
|
+
|
8
|
+
module Asciidoctor
|
9
|
+
module DocTest
|
10
|
+
##
|
11
|
+
# Test class for Asciidoctor backends.
|
12
|
+
class Test < Minitest::Test
|
13
|
+
include MinitestDiffy
|
14
|
+
|
15
|
+
##
|
16
|
+
# (see AsciidocRenderer#initialize)
|
17
|
+
def self.renderer_opts(**kwargs)
|
18
|
+
@renderer = AsciidocRenderer.new(**kwargs)
|
19
|
+
end
|
20
|
+
|
21
|
+
##
|
22
|
+
# Generates tests for all the input/output examples.
|
23
|
+
# When some output example is missing, it's reported as skipped test.
|
24
|
+
#
|
25
|
+
# @param output_suite [BaseExamplesSuite, Class] the examples suite class
|
26
|
+
# (or its instance) to read the output examples from (i.e. an
|
27
|
+
# expected output).
|
28
|
+
# @param input_suite [BaseExamplesSuite, Class] the examples suite class
|
29
|
+
# (or its instance) to read the reference input examples from.
|
30
|
+
#
|
31
|
+
# If class is given, then it's instantiated with zero arguments.
|
32
|
+
#
|
33
|
+
def self.generate_tests!(output_suite, input_suite = Asciidoc::ExamplesSuite)
|
34
|
+
instance = ->(o) { o.is_a?(Class) ? o.new : o }
|
35
|
+
@output_suite = instance[output_suite]
|
36
|
+
@input_suite = instance[input_suite]
|
37
|
+
|
38
|
+
@input_suite.pair_with(@output_suite).each do |input, output|
|
39
|
+
next if input.empty?
|
40
|
+
|
41
|
+
define_test input.name do
|
42
|
+
if output.empty?
|
43
|
+
skip 'No expected output found'
|
44
|
+
else
|
45
|
+
test_example output, input
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
# Defines a new test method.
|
53
|
+
#
|
54
|
+
# @param name [String] name of the test (method).
|
55
|
+
# @param block [Proc] the test method's body.
|
56
|
+
#
|
57
|
+
def self.define_test(name, &block)
|
58
|
+
(@test_methods ||= []) << name
|
59
|
+
define_method name, block
|
60
|
+
end
|
61
|
+
|
62
|
+
##
|
63
|
+
# @!method self.test
|
64
|
+
# @see .define_test
|
65
|
+
alias_class_method :test, :define_test
|
66
|
+
|
67
|
+
##
|
68
|
+
# @private
|
69
|
+
# @note Overrides method from +Minitest::Test+.
|
70
|
+
# @return [Array] names of the test methods to run.
|
71
|
+
def self.runnable_methods
|
72
|
+
(@test_methods || []) + super - ['test_example']
|
73
|
+
end
|
74
|
+
|
75
|
+
##
|
76
|
+
# Tests if the given reference input is matching the expected output
|
77
|
+
# after conversion through the tested backend.
|
78
|
+
#
|
79
|
+
# @param output_exmpl [BaseExample] the expected output example.
|
80
|
+
# @param input_exmpl [BaseExample] the reference input example.
|
81
|
+
# @raise [Minitest::Assertion] if the assertion fails.
|
82
|
+
#
|
83
|
+
def test_example(output_exmpl, input_exmpl)
|
84
|
+
converted_exmpl = output_suite.convert_example(input_exmpl, output_exmpl.opts, renderer)
|
85
|
+
msg = output_exmpl.desc.presence || input_exmpl.desc
|
86
|
+
|
87
|
+
assert_equal output_exmpl, converted_exmpl, msg
|
88
|
+
end
|
89
|
+
|
90
|
+
##
|
91
|
+
# @private
|
92
|
+
# @note Overrides method from +Minitest::Test+.
|
93
|
+
# @return [String] name of this test that will be printed in a report.
|
94
|
+
def location
|
95
|
+
prefix = self.class.name.split('::').last
|
96
|
+
name = self.name.sub(':', ' : ')
|
97
|
+
"#{prefix} :: #{name}"
|
98
|
+
end
|
99
|
+
|
100
|
+
##
|
101
|
+
# @private
|
102
|
+
# Returns a human-readable (formatted) version of the asserted object.
|
103
|
+
#
|
104
|
+
# @note Overrides method from +Minitest::Assertions+.
|
105
|
+
#
|
106
|
+
# @param example [#to_s]
|
107
|
+
# @return [String]
|
108
|
+
#
|
109
|
+
def mu_pp(example)
|
110
|
+
example.to_s
|
111
|
+
end
|
112
|
+
|
113
|
+
[:input_suite, :output_suite, :renderer].each do |name|
|
114
|
+
define_method name do
|
115
|
+
self.class.instance_variable_get(:"@#{name}")
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'active_support/core_ext/string/strip'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
describe DocTest::Asciidoc::ExamplesSuite do
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
it_should_behave_like DocTest::BaseExamplesSuite
|
8
|
+
|
9
|
+
def_delegator :suite, :create_example
|
10
|
+
|
11
|
+
subject(:suite) { described_class.new }
|
12
|
+
|
13
|
+
|
14
|
+
describe '#initialize' do
|
15
|
+
|
16
|
+
it 'uses ".adoc" as default file_ext' do
|
17
|
+
expect(suite.file_ext).to eq '.adoc'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
describe '#parse' do
|
23
|
+
|
24
|
+
context 'one example' do
|
25
|
+
|
26
|
+
shared_examples :example do
|
27
|
+
subject(:parsed) { suite.parse input, 's' }
|
28
|
+
|
29
|
+
it { is_expected.to have(1).items }
|
30
|
+
|
31
|
+
it 'returns an array with parsed Example object' do
|
32
|
+
expect(parsed.first).to eql output
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'with name only' do
|
37
|
+
let(:input) { "// .basic\n" }
|
38
|
+
let(:output) { create_example 's:basic' }
|
39
|
+
|
40
|
+
include_examples :example
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'with multiline content' do
|
44
|
+
let :content do
|
45
|
+
<<-EOF.strip_heredoc
|
46
|
+
Paragraphs don't require
|
47
|
+
any special markup.
|
48
|
+
|
49
|
+
To begin a new one, separate it by blank line.
|
50
|
+
EOF
|
51
|
+
end
|
52
|
+
|
53
|
+
let(:input) { "// .multiline\n#{content}" }
|
54
|
+
let(:output) { create_example 's:multiline', content: content.chomp }
|
55
|
+
|
56
|
+
include_examples :example
|
57
|
+
end
|
58
|
+
|
59
|
+
context 'with description' do
|
60
|
+
let :input do
|
61
|
+
<<-EOF.strip_heredoc
|
62
|
+
// .strong
|
63
|
+
// This is a description,
|
64
|
+
// see?
|
65
|
+
*allons-y!*
|
66
|
+
EOF
|
67
|
+
end
|
68
|
+
|
69
|
+
let :output do
|
70
|
+
create_example 's:strong', content: '*allons-y!*',
|
71
|
+
desc: "This is a description,\nsee?"
|
72
|
+
end
|
73
|
+
|
74
|
+
include_examples :example
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
context 'multiple examples' do
|
79
|
+
let :input do
|
80
|
+
<<-EOF.strip_heredoc
|
81
|
+
// .basic
|
82
|
+
http://asciidoctor.org
|
83
|
+
|
84
|
+
// .xref
|
85
|
+
Refer to <<section-a>>.
|
86
|
+
EOF
|
87
|
+
end
|
88
|
+
|
89
|
+
subject(:parsed) { suite.parse input, 's' }
|
90
|
+
|
91
|
+
it { is_expected.to have(2).items }
|
92
|
+
|
93
|
+
it 'returns an array with parsed Example objects' do
|
94
|
+
expect(parsed[0]).to eql create_example('s:basic', content: 'http://asciidoctor.org')
|
95
|
+
expect(parsed[1]).to eql create_example('s:xref', content: 'Refer to <<section-a>>.')
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
describe DocTest::BaseExample do
|
2
|
+
|
3
|
+
subject(:o) { described_class.new ['foo', 'bar'] }
|
4
|
+
|
5
|
+
it { is_expected.to respond_to :group_name, :local_name, :content, :desc, :opts }
|
6
|
+
|
7
|
+
describe '#name' do
|
8
|
+
|
9
|
+
it 'returns #{group_name}:#{local_name}' do
|
10
|
+
o.group_name = 'block_olist'
|
11
|
+
o.local_name = 'with-start'
|
12
|
+
|
13
|
+
expect(o.name).to eq "#{o.group_name}:#{o.local_name}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '#name=' do
|
18
|
+
|
19
|
+
shared_examples :example do
|
20
|
+
it 'sets group_name and local_name' do
|
21
|
+
is_expected.to have_attributes group_name: 'section', local_name: 'basic'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'with String' do
|
26
|
+
before { o.name = 'section:basic' }
|
27
|
+
include_examples :example
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'with Array' do
|
31
|
+
before { o.name = ['section', 'basic'] }
|
32
|
+
include_examples :example
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '#name_match?' do
|
37
|
+
name = 'block_ulist:with-title'
|
38
|
+
|
39
|
+
context "when name is e.g. #{name}" do
|
40
|
+
subject(:o) { described_class.new(name) }
|
41
|
+
|
42
|
+
[ '*', '*:*', 'block_ulist:*', '*:with-title', 'block_*:*',
|
43
|
+
'block_ulist:with-*', 'block_ulist:*title'
|
44
|
+
].each do |pattern|
|
45
|
+
it "returns true for #{pattern}" do
|
46
|
+
expect(o.name_match?(pattern)).to be_truthy
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
[ 'block_foo:with-title', 'block_ulist:foo', 'foo:*' '*:foo', 'foo'
|
51
|
+
].each do |pattern|
|
52
|
+
it "returns false for #{pattern}" do
|
53
|
+
expect(o.name_match?(pattern)).to be_falsy
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe '#empty?' do
|
60
|
+
subject { o.empty? }
|
61
|
+
|
62
|
+
context 'when content is nil' do
|
63
|
+
before { o.content = nil }
|
64
|
+
it { is_expected.to be_truthy }
|
65
|
+
end
|
66
|
+
|
67
|
+
context 'when content is blank' do
|
68
|
+
before { o.content = ' ' }
|
69
|
+
it { is_expected.to be_truthy }
|
70
|
+
end
|
71
|
+
|
72
|
+
context 'when content is not blank' do
|
73
|
+
before { o.content = 'allons-y!' }
|
74
|
+
it { is_expected.to be_falsy }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe '#[]' do
|
79
|
+
|
80
|
+
context 'when option exists' do
|
81
|
+
it 'returns the option value' do
|
82
|
+
o.opts[:foo] = 'bar'
|
83
|
+
expect(o['foo']).to eq 'bar'
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
context 'when option does not exist' do
|
88
|
+
it 'returns nil' do
|
89
|
+
expect(o[:nothing]).to be_nil
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe '#[]=' do
|
95
|
+
subject { o.opts }
|
96
|
+
|
97
|
+
context 'with boolean value' do
|
98
|
+
[true, false].each do |value|
|
99
|
+
it "associates the option with #{value}" do
|
100
|
+
o['foo'] = value
|
101
|
+
is_expected.to eq(foo: value)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
context 'with Array value' do
|
107
|
+
it 'associates the option with the value' do
|
108
|
+
o['foo'] = ['a', 'b']
|
109
|
+
is_expected.to eq(foo: ['a', 'b'])
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
context 'with String value' do
|
114
|
+
|
115
|
+
context 'when option is not defined' do
|
116
|
+
it 'associates the option with the value wrapped in an array' do
|
117
|
+
o['key'] = 'foo'
|
118
|
+
is_expected.to eq(key: ['foo'])
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
context 'when option is already defined' do
|
123
|
+
before { o.opts[:key] = ['foo'] }
|
124
|
+
|
125
|
+
it 'adds the value to array associated with the option' do
|
126
|
+
o[:key] = 'bar'
|
127
|
+
is_expected.to eq(key: ['foo', 'bar'])
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
context 'with nil value' do
|
133
|
+
before { o.opts[:key] = ['foo'] }
|
134
|
+
|
135
|
+
it 'deletes the option' do
|
136
|
+
o[:key] = nil
|
137
|
+
is_expected.to be_empty
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
describe '#==' do
|
143
|
+
|
144
|
+
let(:first) { described_class.new('a:b', content: 'allons-y!') }
|
145
|
+
let(:second) { described_class.new('a:b', content: 'allons-y!') }
|
146
|
+
|
147
|
+
it 'returns true for different instances with the same name and content' do
|
148
|
+
expect(first).to eq second
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'returns false for instances with different name' do
|
152
|
+
second.name = 'a:x'
|
153
|
+
expect(first).to_not eq second
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'returns false for instances with different content_normalized' do
|
157
|
+
expect(second).to receive(:content_normalized).and_return('ALLONS-Y!')
|
158
|
+
expect(first).to_not eq second
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
describe '#dup' do
|
163
|
+
it 'returns deep copy' do
|
164
|
+
origo = described_class.new('a:b', content: 'allons-y!', desc: 'who?', opts: {key: ['value']})
|
165
|
+
copy = origo.dup
|
166
|
+
|
167
|
+
expect(origo).to eql copy
|
168
|
+
expect(origo).to_not equal copy
|
169
|
+
|
170
|
+
origo.instance_values.values.zip(copy.instance_values.values).each do |val1, val2|
|
171
|
+
expect(val1).to_not equal val2 unless val1.nil? && val2.nil?
|
172
|
+
end
|
173
|
+
expect(origo.opts[:key].first).to_not equal copy.opts[:key].first
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|