asciidoctor-doctest 1.5.2.0 → 2.0.0.beta.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.adoc +48 -68
- data/features/fixtures/html-slim/Rakefile +5 -11
- data/features/generator_html.feature +6 -6
- data/features/test_html.feature +70 -28
- data/lib/asciidoctor/doctest.rb +11 -14
- data/lib/asciidoctor/doctest/asciidoc_converter.rb +85 -0
- data/lib/asciidoctor/doctest/{base_example.rb → example.rb} +6 -27
- data/lib/asciidoctor/doctest/factory.rb +36 -0
- data/lib/asciidoctor/doctest/generator.rb +30 -23
- data/lib/asciidoctor/doctest/html/converter.rb +64 -0
- data/lib/asciidoctor/doctest/{html/normalizer.rb → html_normalizer.rb} +4 -4
- data/lib/asciidoctor/doctest/io.rb +14 -0
- data/lib/asciidoctor/doctest/{asciidoc/examples_suite.rb → io/asciidoc.rb} +4 -8
- data/lib/asciidoctor/doctest/{base_examples_suite.rb → io/base.rb} +28 -46
- data/lib/asciidoctor/doctest/io/xml.rb +69 -0
- data/lib/asciidoctor/doctest/no_fallback_template_converter.rb +42 -0
- data/lib/asciidoctor/doctest/rake_tasks.rb +229 -0
- data/lib/asciidoctor/doctest/test_reporter.rb +110 -0
- data/lib/asciidoctor/doctest/tester.rb +134 -0
- data/lib/asciidoctor/doctest/version.rb +1 -1
- data/spec/asciidoc_converter_spec.rb +64 -0
- data/spec/{base_example_spec.rb → example_spec.rb} +4 -5
- data/spec/factory_spec.rb +46 -0
- data/spec/html/converter_spec.rb +95 -0
- data/spec/{html/normalizer_spec.rb → html_normalizer_spec.rb} +1 -1
- data/spec/{asciidoc/examples_suite_spec.rb → io/asciidoc_spec.rb} +3 -8
- data/spec/{html/examples_suite_spec.rb → io/xml_spec.rb} +3 -106
- data/spec/no_fallback_template_converter_spec.rb +38 -0
- data/spec/shared_examples/{base_examples_suite.rb → base_examples.rb} +25 -28
- data/spec/spec_helper.rb +4 -0
- data/spec/tester_spec.rb +153 -0
- metadata +52 -59
- data/features/fixtures/html-slim/test/html_test.rb +0 -6
- data/features/fixtures/html-slim/test/test_helper.rb +0 -5
- data/lib/asciidoctor/doctest/asciidoc_renderer.rb +0 -111
- data/lib/asciidoctor/doctest/generator_task.rb +0 -115
- data/lib/asciidoctor/doctest/html/example.rb +0 -21
- data/lib/asciidoctor/doctest/html/examples_suite.rb +0 -118
- data/lib/asciidoctor/doctest/test.rb +0 -125
- data/spec/asciidoc_renderer_spec.rb +0 -103
- data/spec/test_spec.rb +0 -164
@@ -6,7 +6,7 @@ module Asciidoctor
|
|
6
6
|
module DocTest
|
7
7
|
##
|
8
8
|
# This class represents a single test example.
|
9
|
-
class
|
9
|
+
class Example
|
10
10
|
|
11
11
|
NAME_SEPARATOR = ':'
|
12
12
|
|
@@ -101,39 +101,18 @@ module Asciidoctor
|
|
101
101
|
end
|
102
102
|
|
103
103
|
##
|
104
|
-
# @
|
105
|
-
# should override this method.
|
106
|
-
#
|
107
|
-
# @return [String] copy of the content in a form that is suitable for
|
108
|
-
# semantic comparison with another content.
|
109
|
-
#
|
110
|
-
def content_normalized
|
111
|
-
content.dup
|
112
|
-
end
|
113
|
-
|
114
|
-
##
|
115
|
-
# @note (see #content_normalized)
|
116
|
-
#
|
117
|
-
# @return [String] copy of the content in a human-readable (formatted)
|
118
|
-
# shape for pretty print.
|
119
|
-
#
|
120
|
-
def content_pretty
|
121
|
-
content.dup
|
122
|
-
end
|
123
|
-
|
124
|
-
##
|
125
|
-
# @return (see #content_pretty)
|
104
|
+
# @return [String] a copy of the content.
|
126
105
|
def to_s
|
127
|
-
|
106
|
+
content.dup
|
128
107
|
end
|
129
108
|
|
130
109
|
##
|
131
110
|
# @param other the object to compare with.
|
132
111
|
# @return [Boolean] +true+ if +self+ and +other+ equals in attributes
|
133
|
-
# +group_name+, +local_name+ and +
|
134
|
-
#
|
112
|
+
# +group_name+, +local_name+ and +content+ (compared using +==+),
|
113
|
+
# otherwise +false+.
|
135
114
|
def ==(other)
|
136
|
-
[:group_name, :local_name, :
|
115
|
+
[:group_name, :local_name, :content].all? do |name|
|
137
116
|
other.respond_to?(name) &&
|
138
117
|
public_send(name) == other.public_send(name)
|
139
118
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Asciidoctor::DocTest
|
2
|
+
module Factory
|
3
|
+
|
4
|
+
##
|
5
|
+
# Registers the given class in the factory under the specified name.
|
6
|
+
#
|
7
|
+
# @param name [#to_sym] the name under which to register the class.
|
8
|
+
# @param klass [Class] the class to register.
|
9
|
+
# @param default_opts [Hash] default options to be passed into the class'
|
10
|
+
# initializer. May be overriden by +opts+ passed to {.create}.
|
11
|
+
# @return [self]
|
12
|
+
#
|
13
|
+
def register(name, klass, default_opts = {})
|
14
|
+
@factory_registry ||= {}
|
15
|
+
@factory_registry[name.to_sym] = ->(opts) { klass.new(default_opts.merge(opts)) }
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
# @param name [#to_sym] name of the class to create.
|
21
|
+
# @param opts [Hash] options to be passed into the class' initializer.
|
22
|
+
# @return [Object] a new instance of the class registered under the
|
23
|
+
# specified name.
|
24
|
+
# @raise ArgumentError if no class was found for the given name.
|
25
|
+
#
|
26
|
+
def create(name, opts = {})
|
27
|
+
@factory_registry ||= {}
|
28
|
+
|
29
|
+
if (obj = @factory_registry[name.to_sym])
|
30
|
+
obj.call(opts)
|
31
|
+
else
|
32
|
+
fail ArgumentError, "No class registered with name: #{name}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -5,62 +5,69 @@ using Corefines::String::color
|
|
5
5
|
|
6
6
|
module Asciidoctor
|
7
7
|
module DocTest
|
8
|
-
|
8
|
+
class Generator
|
9
9
|
|
10
10
|
##
|
11
|
-
#
|
12
|
-
# input examples
|
11
|
+
# @param input_suite [IO::Base] an instance of {IO::Base} subclass to
|
12
|
+
# read the reference input examples.
|
13
13
|
#
|
14
|
-
# @param output_suite [
|
15
|
-
#
|
16
|
-
# examples.
|
14
|
+
# @param output_suite [IO::Base] an instance of {IO::Base} subclass to
|
15
|
+
# read and generate the output examples.
|
17
16
|
#
|
18
|
-
# @param
|
19
|
-
#
|
20
|
-
#
|
17
|
+
# @param converter [#call] a callable that accepts a string content of
|
18
|
+
# an input example and a hash with options for the converter, and
|
19
|
+
# returns the converted content.
|
21
20
|
#
|
22
|
-
# @param
|
21
|
+
# @param io [#<<] output stream where to write log messages.
|
22
|
+
#
|
23
|
+
def initialize(input_suite, output_suite, converter, io = $stdout)
|
24
|
+
@input_suite = input_suite
|
25
|
+
@output_suite = output_suite
|
26
|
+
@converter = converter
|
27
|
+
@io = io
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Generates missing, or rewrite existing output examples from the
|
32
|
+
# input examples converted using the +converter+.
|
23
33
|
#
|
24
34
|
# @param pattern [String] glob-like pattern to select examples to
|
25
|
-
# (re)generate (see {
|
35
|
+
# (re)generate (see {Example#name_match?}).
|
26
36
|
#
|
27
37
|
# @param rewrite [Boolean] whether to rewrite an already existing
|
28
38
|
# example.
|
29
39
|
#
|
30
|
-
|
31
|
-
#
|
32
|
-
def self.generate!(output_suite, input_suite, renderer, pattern: '*:*',
|
33
|
-
rewrite: false, log_os: $stdout)
|
40
|
+
def generate!(pattern: '*:*', rewrite: false)
|
34
41
|
updated = []
|
35
42
|
|
36
|
-
input_suite.pair_with(output_suite).each do |input, output|
|
43
|
+
@input_suite.pair_with(@output_suite).each do |input, output|
|
37
44
|
next unless input.name_match? pattern
|
38
45
|
|
39
46
|
log = ->(msg, color = :default) do
|
40
|
-
|
47
|
+
@io << " --> #{(msg % input.name).color(color)}\n" if @io
|
41
48
|
end
|
42
49
|
|
43
50
|
if input.empty?
|
44
51
|
log["Unknown %s, doesn't exist in input examples!"]
|
45
52
|
else
|
46
|
-
|
47
|
-
|
53
|
+
actual, expected = @converter.convert_examples(input, output)
|
54
|
+
generated = output.dup.tap { |ex| ex.content = actual }
|
48
55
|
|
49
56
|
if output.empty?
|
50
57
|
log['Generating %s', :magenta]
|
51
|
-
updated <<
|
52
|
-
elsif
|
58
|
+
updated << generated
|
59
|
+
elsif actual == expected
|
53
60
|
log['Unchanged %s', :green]
|
54
61
|
elsif rewrite
|
55
62
|
log['Rewriting %s', :red]
|
56
|
-
updated <<
|
63
|
+
updated << generated
|
57
64
|
else
|
58
65
|
log['Skipping %s', :yellow]
|
59
66
|
end
|
60
67
|
end
|
61
68
|
end
|
62
69
|
|
63
|
-
output_suite.update_examples updated unless updated.empty?
|
70
|
+
@output_suite.update_examples updated unless updated.empty?
|
64
71
|
end
|
65
72
|
end
|
66
73
|
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'asciidoctor/doctest/html_normalizer'
|
2
|
+
require 'corefines'
|
3
|
+
require 'htmlbeautifier'
|
4
|
+
require 'nokogiri'
|
5
|
+
|
6
|
+
using Corefines::Object::then
|
7
|
+
|
8
|
+
module Asciidoctor::DocTest
|
9
|
+
module HTML
|
10
|
+
class Converter < AsciidocConverter
|
11
|
+
|
12
|
+
def initialize(paragraph_xpath: './p/node()', **opts)
|
13
|
+
@paragraph_xpath = paragraph_xpath
|
14
|
+
super opts
|
15
|
+
end
|
16
|
+
|
17
|
+
def convert_examples(input_exmpl, output_exmpl)
|
18
|
+
opts = output_exmpl.opts.dup
|
19
|
+
|
20
|
+
# The header & footer are excluded by default; always enable for document examples.
|
21
|
+
opts[:header_footer] ||= input_exmpl.name.start_with?('document')
|
22
|
+
|
23
|
+
# When asserting inline examples, defaults to ignore paragraph "wrapper".
|
24
|
+
opts[:include] ||= (@paragraph_xpath if input_exmpl.name.start_with? 'inline_')
|
25
|
+
|
26
|
+
actual = convert(input_exmpl.content, header_footer: opts[:header_footer])
|
27
|
+
.then { |s| parse_html s, !opts[:header_footer] }
|
28
|
+
.then { |h| find_nodes h, opts[:include] }
|
29
|
+
.then { |h| remove_nodes h, opts[:exclude] }
|
30
|
+
.then { |h| normalize(h) }
|
31
|
+
|
32
|
+
expected = normalize(output_exmpl.content)
|
33
|
+
|
34
|
+
[actual, expected]
|
35
|
+
end
|
36
|
+
|
37
|
+
protected
|
38
|
+
|
39
|
+
def normalize(content)
|
40
|
+
content = parse_html(content) if content.is_a? String
|
41
|
+
HtmlBeautifier.beautify(content.normalize!)
|
42
|
+
end
|
43
|
+
|
44
|
+
def find_nodes(html, xpaths)
|
45
|
+
Array(xpaths).reduce(html) do |htm, xpath|
|
46
|
+
# XPath returns NodeSet, but we need DocumentFragment, so convert it again.
|
47
|
+
parse_html htm.xpath(xpath).to_html
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def remove_nodes(html, xpaths)
|
52
|
+
return html unless xpaths
|
53
|
+
|
54
|
+
Array(xpaths).each_with_object(html.clone) do |xpath, htm|
|
55
|
+
htm.xpath(xpath).remove
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def parse_html(str, fragment = true)
|
60
|
+
fragment ? ::Nokogiri::HTML.fragment(str) : ::Nokogiri::HTML.parse(str)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -3,8 +3,8 @@ require 'nokogiri'
|
|
3
3
|
|
4
4
|
using Corefines::Object::try
|
5
5
|
|
6
|
-
module Asciidoctor
|
7
|
-
module
|
6
|
+
module Asciidoctor
|
7
|
+
module DocTest
|
8
8
|
##
|
9
9
|
# Module to be included into +Nokogiri::HTML::Document+
|
10
10
|
# or +DocumentFragment+ to add {#normalize!} feature.
|
@@ -12,7 +12,7 @@ module Asciidoctor::DocTest
|
|
12
12
|
# @example
|
13
13
|
# Nokogiri::HTML.parse(str).normalize!
|
14
14
|
# Nokogiri::HTML.fragment(str).normalize!
|
15
|
-
module
|
15
|
+
module HtmlNormalizer
|
16
16
|
|
17
17
|
##
|
18
18
|
# Normalizes the HTML document or fragment so it can be easily compared
|
@@ -116,5 +116,5 @@ module Asciidoctor::DocTest
|
|
116
116
|
end
|
117
117
|
|
118
118
|
[Nokogiri::HTML::Document, Nokogiri::HTML::DocumentFragment].each do |klass|
|
119
|
-
klass.send :include, Asciidoctor::DocTest::
|
119
|
+
klass.send :include, Asciidoctor::DocTest::HtmlNormalizer
|
120
120
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'asciidoctor/doctest/io/base'
|
2
|
+
require 'asciidoctor/doctest/io/asciidoc'
|
3
|
+
require 'asciidoctor/doctest/io/xml'
|
4
|
+
require 'asciidoctor/doctest/factory'
|
5
|
+
|
6
|
+
module Asciidoctor::DocTest
|
7
|
+
module IO
|
8
|
+
extend Factory
|
9
|
+
|
10
|
+
register :asciidoc, Asciidoc, file_ext: '.adoc'
|
11
|
+
register :xml, XML, file_ext: '.xml'
|
12
|
+
register :html, XML, file_ext: '.html'
|
13
|
+
end
|
14
|
+
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require 'asciidoctor/doctest/
|
1
|
+
require 'asciidoctor/doctest/io/base'
|
2
2
|
require 'corefines'
|
3
3
|
|
4
4
|
using Corefines::Enumerable::map_send
|
@@ -6,9 +6,9 @@ using Corefines::Object[:blank?, :presence]
|
|
6
6
|
using Corefines::String::concat!
|
7
7
|
|
8
8
|
module Asciidoctor::DocTest
|
9
|
-
module
|
9
|
+
module IO
|
10
10
|
##
|
11
|
-
# Subclass of {
|
11
|
+
# Subclass of {IO::Base} for reference input examples.
|
12
12
|
#
|
13
13
|
# @example Format of the example's header
|
14
14
|
# // .example-name
|
@@ -22,11 +22,7 @@ module Asciidoctor::DocTest
|
|
22
22
|
#
|
23
23
|
# NOTE: The trailing new line (below this) will be removed.
|
24
24
|
#
|
25
|
-
class
|
26
|
-
|
27
|
-
def initialize(file_ext: '.adoc', **kwargs)
|
28
|
-
super
|
29
|
-
end
|
25
|
+
class Asciidoc < Base
|
30
26
|
|
31
27
|
def parse(input, group_name)
|
32
28
|
examples = []
|
@@ -4,28 +4,28 @@ require 'pathname'
|
|
4
4
|
using Corefines::Object::blank?
|
5
5
|
using Corefines::Enumerable::index_by
|
6
6
|
|
7
|
-
module Asciidoctor
|
8
|
-
module
|
7
|
+
module Asciidoctor::DocTest
|
8
|
+
module IO
|
9
9
|
##
|
10
10
|
# @abstract
|
11
11
|
# This is a base class that should be extended for specific example
|
12
12
|
# formats.
|
13
|
-
class
|
13
|
+
class Base
|
14
14
|
|
15
|
-
attr_reader :
|
15
|
+
attr_reader :path, :file_ext
|
16
16
|
|
17
17
|
##
|
18
|
+
# @param path [String, Array<String>] path of the directory (or multiple
|
19
|
+
# directories) where to look for the examples.
|
20
|
+
#
|
18
21
|
# @param file_ext [String] the filename extension (e.g. +.adoc+) of the
|
19
22
|
# examples group files. Must not be +nil+ or blank. (required)
|
20
23
|
#
|
21
|
-
|
22
|
-
# multiple directories) where to look for the examples.
|
23
|
-
#
|
24
|
-
def initialize(file_ext: nil, examples_path: DocTest.examples_path)
|
24
|
+
def initialize(path: DocTest.examples_path, file_ext: nil)
|
25
25
|
fail ArgumentError, 'file_ext must not be blank or nil' if file_ext.blank?
|
26
26
|
|
27
|
+
@path = Array(path).freeze
|
27
28
|
@file_ext = file_ext.strip.freeze
|
28
|
-
@examples_path = Array(examples_path).freeze
|
29
29
|
@examples_cache = {}
|
30
30
|
end
|
31
31
|
|
@@ -36,7 +36,7 @@ module Asciidoctor
|
|
36
36
|
# @abstract
|
37
37
|
# @param input [#each_line] the file content to parse.
|
38
38
|
# @param group_name [String] the examples group name.
|
39
|
-
# @return [Array<
|
39
|
+
# @return [Array<Example>] parsed examples.
|
40
40
|
# :nocov:
|
41
41
|
def parse(input, group_name)
|
42
42
|
fail NotImplementedError
|
@@ -47,7 +47,7 @@ module Asciidoctor
|
|
47
47
|
# Serializes the given examples into string.
|
48
48
|
#
|
49
49
|
# @abstract
|
50
|
-
# @param examples [Array<
|
50
|
+
# @param examples [Array<Example>]
|
51
51
|
# @return [String]
|
52
52
|
# :nocov:
|
53
53
|
def serialize(examples)
|
@@ -55,30 +55,6 @@ module Asciidoctor
|
|
55
55
|
end
|
56
56
|
# :nocov:
|
57
57
|
|
58
|
-
##
|
59
|
-
# Returns a new example based on the given input example.
|
60
|
-
# This method should render (AsciiDoc) content of the given example using
|
61
|
-
# the preconfigured +renderer+ and eventually apply some transformations.
|
62
|
-
#
|
63
|
-
# XXX describe it better...
|
64
|
-
#
|
65
|
-
# @abstract
|
66
|
-
# @param example [BaseExample] the input example to convert.
|
67
|
-
# @param opts [Hash] the options to pass to a new example.
|
68
|
-
# @param renderer [#convert]
|
69
|
-
# @return [BaseExample]
|
70
|
-
# :nocov:
|
71
|
-
def convert_example(example, opts, renderer)
|
72
|
-
fail NotImplementedError
|
73
|
-
end
|
74
|
-
# :nocov:
|
75
|
-
|
76
|
-
##
|
77
|
-
# (see BaseExample#initialize)
|
78
|
-
def create_example(*args)
|
79
|
-
BaseExample.new(*args)
|
80
|
-
end
|
81
|
-
|
82
58
|
##
|
83
59
|
# Returns enumerator that yields pairs of the examples from this suite
|
84
60
|
# and the +other_suite+ (examples with the same name) in order of this
|
@@ -89,7 +65,7 @@ module Asciidoctor
|
|
89
65
|
# In the case of missing example from this suite, the pair is placed at
|
90
66
|
# the end of the examples group.
|
91
67
|
#
|
92
|
-
# @param other_suite [
|
68
|
+
# @param other_suite [Base]
|
93
69
|
# @return [Enumerator]
|
94
70
|
#
|
95
71
|
def pair_with(other_suite)
|
@@ -112,9 +88,9 @@ module Asciidoctor
|
|
112
88
|
|
113
89
|
##
|
114
90
|
# Reads the named examples group from file(s). When multiple matching
|
115
|
-
# files are found on the {#
|
91
|
+
# files are found on the {#path}, it merges them together. If
|
116
92
|
# two files defines example with the same name, then the first wins (i.e.
|
117
|
-
# first on the {#
|
93
|
+
# first on the {#path}).
|
118
94
|
#
|
119
95
|
# @param group_name [String] the examples group name.
|
120
96
|
# @return [Array<Example>] an array of parsed examples, or an empty array
|
@@ -129,14 +105,14 @@ module Asciidoctor
|
|
129
105
|
|
130
106
|
##
|
131
107
|
# Writes the given examples into file(s)
|
132
|
-
# +{
|
133
|
-
#
|
108
|
+
# +{path.first}/{group_name}{file_ext}+. Already existing files will
|
109
|
+
# be overwritten!
|
134
110
|
#
|
135
|
-
# @param examples [Array<
|
111
|
+
# @param examples [Array<Example>]
|
136
112
|
#
|
137
113
|
def write_examples(examples)
|
138
114
|
examples.group_by(&:group_name).each do |group_name, exmpls|
|
139
|
-
path = file_path(@
|
115
|
+
path = file_path(@path.first, group_name)
|
140
116
|
File.write(path, serialize(exmpls))
|
141
117
|
end
|
142
118
|
end
|
@@ -144,7 +120,7 @@ module Asciidoctor
|
|
144
120
|
##
|
145
121
|
# Replaces existing examples with the given ones.
|
146
122
|
#
|
147
|
-
# @param examples [Array<
|
123
|
+
# @param examples [Array<Example] the updated examples.
|
148
124
|
# @see #write_examples
|
149
125
|
#
|
150
126
|
def update_examples(examples)
|
@@ -162,12 +138,12 @@ module Asciidoctor
|
|
162
138
|
|
163
139
|
##
|
164
140
|
# Returns names of all the example groups (files with {#file_ext})
|
165
|
-
# found on the {#
|
141
|
+
# found on the {#path}.
|
166
142
|
#
|
167
143
|
# @return [Array<String>]
|
168
144
|
#
|
169
145
|
def group_names
|
170
|
-
@
|
146
|
+
@path.reduce(Set.new) { |acc, path|
|
171
147
|
acc | Pathname.new(path).each_child
|
172
148
|
.select { |p| p.file? && p.extname == @file_ext }
|
173
149
|
.map { |p| p.sub_ext('').basename.to_s }
|
@@ -176,6 +152,12 @@ module Asciidoctor
|
|
176
152
|
|
177
153
|
protected
|
178
154
|
|
155
|
+
##
|
156
|
+
# (see Example#initialize)
|
157
|
+
def create_example(*args)
|
158
|
+
DocTest::Example.new(*args)
|
159
|
+
end
|
160
|
+
|
179
161
|
##
|
180
162
|
# Converts the given options into the format used in examples file.
|
181
163
|
#
|
@@ -207,7 +189,7 @@ module Asciidoctor
|
|
207
189
|
private
|
208
190
|
|
209
191
|
def read_files(file_name)
|
210
|
-
@
|
192
|
+
@path
|
211
193
|
.map { |dir| file_path(dir, file_name) }
|
212
194
|
.select(&:readable?)
|
213
195
|
.map(&:read)
|