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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.adoc +48 -68
  4. data/features/fixtures/html-slim/Rakefile +5 -11
  5. data/features/generator_html.feature +6 -6
  6. data/features/test_html.feature +70 -28
  7. data/lib/asciidoctor/doctest.rb +11 -14
  8. data/lib/asciidoctor/doctest/asciidoc_converter.rb +85 -0
  9. data/lib/asciidoctor/doctest/{base_example.rb → example.rb} +6 -27
  10. data/lib/asciidoctor/doctest/factory.rb +36 -0
  11. data/lib/asciidoctor/doctest/generator.rb +30 -23
  12. data/lib/asciidoctor/doctest/html/converter.rb +64 -0
  13. data/lib/asciidoctor/doctest/{html/normalizer.rb → html_normalizer.rb} +4 -4
  14. data/lib/asciidoctor/doctest/io.rb +14 -0
  15. data/lib/asciidoctor/doctest/{asciidoc/examples_suite.rb → io/asciidoc.rb} +4 -8
  16. data/lib/asciidoctor/doctest/{base_examples_suite.rb → io/base.rb} +28 -46
  17. data/lib/asciidoctor/doctest/io/xml.rb +69 -0
  18. data/lib/asciidoctor/doctest/no_fallback_template_converter.rb +42 -0
  19. data/lib/asciidoctor/doctest/rake_tasks.rb +229 -0
  20. data/lib/asciidoctor/doctest/test_reporter.rb +110 -0
  21. data/lib/asciidoctor/doctest/tester.rb +134 -0
  22. data/lib/asciidoctor/doctest/version.rb +1 -1
  23. data/spec/asciidoc_converter_spec.rb +64 -0
  24. data/spec/{base_example_spec.rb → example_spec.rb} +4 -5
  25. data/spec/factory_spec.rb +46 -0
  26. data/spec/html/converter_spec.rb +95 -0
  27. data/spec/{html/normalizer_spec.rb → html_normalizer_spec.rb} +1 -1
  28. data/spec/{asciidoc/examples_suite_spec.rb → io/asciidoc_spec.rb} +3 -8
  29. data/spec/{html/examples_suite_spec.rb → io/xml_spec.rb} +3 -106
  30. data/spec/no_fallback_template_converter_spec.rb +38 -0
  31. data/spec/shared_examples/{base_examples_suite.rb → base_examples.rb} +25 -28
  32. data/spec/spec_helper.rb +4 -0
  33. data/spec/tester_spec.rb +153 -0
  34. metadata +52 -59
  35. data/features/fixtures/html-slim/test/html_test.rb +0 -6
  36. data/features/fixtures/html-slim/test/test_helper.rb +0 -5
  37. data/lib/asciidoctor/doctest/asciidoc_renderer.rb +0 -111
  38. data/lib/asciidoctor/doctest/generator_task.rb +0 -115
  39. data/lib/asciidoctor/doctest/html/example.rb +0 -21
  40. data/lib/asciidoctor/doctest/html/examples_suite.rb +0 -118
  41. data/lib/asciidoctor/doctest/test.rb +0 -125
  42. data/spec/asciidoc_renderer_spec.rb +0 -103
  43. 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 BaseExample
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
- # @note The default implementation returns content as-is; subclasses
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
- content_pretty
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 +content_normalized+ (compared
134
- # using +==+), otherwise +false+.
112
+ # +group_name+, +local_name+ and +content+ (compared using +==+),
113
+ # otherwise +false+.
135
114
  def ==(other)
136
- [:group_name, :local_name, :content_normalized].all? do |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
- module Generator
8
+ class Generator
9
9
 
10
10
  ##
11
- # Generates missing, or rewrite existing output examples from the
12
- # input examples converted using the +renderer+.
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 [BaseExamplesSuite] an instance of
15
- # {BaseExamplesSuite} subclass to read and generate the output
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 input_suite [BaseExamplesSuite] an instance of
19
- # {BaseExamplesSuite} subclass to read the reference input
20
- # examples.
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 renderer [#convert]
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 {BaseExample#name_match?}).
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
- # @param log_os [#<<] output stream where to write log messages.
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
- log_os << " --> #{(msg % input.name).color(color)}\n" if log_os
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
- rendered = output_suite.convert_example(input, output.opts, renderer)
47
- rendered.desc = output.desc
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 << rendered
52
- elsif rendered == output
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 << rendered
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::DocTest
7
- module HTML
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 Normalizer
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::HTML::Normalizer
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/base_examples_suite'
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 Asciidoc
9
+ module IO
10
10
  ##
11
- # Subclass of {BaseExamplesSuite} for reference input examples.
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 ExamplesSuite < BaseExamplesSuite
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 DocTest
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 BaseExamplesSuite
13
+ class Base
14
14
 
15
- attr_reader :examples_path, :file_ext
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
- # @param examples_path [String, Array<String>] path of the directory (or
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<BaseExample>] parsed examples.
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<BaseExample>]
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 [BaseExamplesSuite]
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 {#examples_path}, it merges them together. If
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 {#examples_path}).
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
- # +{examples_path.first}/{group_name}{file_ext}+. Already existing files
133
- # will be overwritten!
108
+ # +{path.first}/{group_name}{file_ext}+. Already existing files will
109
+ # be overwritten!
134
110
  #
135
- # @param examples [Array<BaseExample>]
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(@examples_path.first, group_name)
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<BaseExample] the updated examples.
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 {#examples_path}.
141
+ # found on the {#path}.
166
142
  #
167
143
  # @return [Array<String>]
168
144
  #
169
145
  def group_names
170
- @examples_path.reduce(Set.new) { |acc, path|
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
- @examples_path
192
+ @path
211
193
  .map { |dir| file_path(dir, file_name) }
212
194
  .select(&:readable?)
213
195
  .map(&:read)