rspec-documentation 0.0.1 → 0.0.3
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/.rspec +1 -0
- data/.rubocop.yml +3 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +25 -2
- data/Makefile +9 -0
- data/README.md +18 -22
- data/Rakefile +1 -0
- data/exe/rspec-documentation +17 -0
- data/lib/rspec/documentation/version.rb +2 -2
- data/lib/rspec/documentation.rb +15 -2
- data/lib/rspec_documentation/configuration.rb +28 -0
- data/lib/rspec_documentation/document.rb +75 -0
- data/lib/rspec_documentation/documentation.rb +85 -0
- data/lib/rspec_documentation/formatters/ansi.rb +44 -0
- data/lib/rspec_documentation/formatters/html.rb +31 -0
- data/lib/rspec_documentation/formatters/json.rb +32 -0
- data/lib/rspec_documentation/formatters/ruby.rb +31 -0
- data/lib/rspec_documentation/formatters/yaml.rb +36 -0
- data/lib/rspec_documentation/formatters.rb +20 -0
- data/lib/rspec_documentation/html_element.rb +58 -0
- data/lib/rspec_documentation/javascript_bundle.rb +17 -0
- data/lib/rspec_documentation/markdown_renderer.rb +7 -0
- data/lib/rspec_documentation/page_collection.rb +64 -0
- data/lib/rspec_documentation/page_tree.rb +81 -0
- data/lib/rspec_documentation/page_tree_element.rb +78 -0
- data/lib/rspec_documentation/parsed_document.rb +72 -0
- data/lib/rspec_documentation/project_initialization.rb +52 -0
- data/lib/rspec_documentation/rspec/failure.rb +52 -0
- data/lib/rspec_documentation/rspec.rb +12 -0
- data/lib/rspec_documentation/spec.rb +108 -0
- data/lib/rspec_documentation/stylesheet_bundle.rb +17 -0
- data/lib/rspec_documentation/summary.rb +87 -0
- data/lib/rspec_documentation/util.rb +58 -0
- data/lib/rspec_documentation.rb +71 -0
- data/lib/tasks/rspec/documentation/generate.rake +10 -0
- data/lib/templates/000-Introduction.md.erb +35 -0
- data/lib/templates/010-Usage.md.erb +3 -0
- data/lib/templates/500-License.md.erb +3 -0
- data/lib/templates/bootstrap.js.erb +7 -0
- data/lib/templates/footer.html.erb +6 -0
- data/lib/templates/header.html.erb +20 -0
- data/lib/templates/layout.css.erb +418 -0
- data/lib/templates/layout.html.erb +31 -0
- data/lib/templates/layout.js.erb +67 -0
- data/lib/templates/modal_spec.html.erb +51 -0
- data/lib/templates/moon.svg.erb +1 -0
- data/lib/templates/script_tags.html.erb +1 -0
- data/lib/templates/stylesheet_links.html.erb +1 -0
- data/lib/templates/sun.svg.erb +1 -0
- data/lib/templates/tabbed_spec.html.erb +82 -0
- data/lib/templates/themes/bootstrap.min.css +11 -0
- data/lib/templates/themes/cerulean.css +11 -0
- data/lib/templates/themes/cosmo.css +11 -0
- data/lib/templates/themes/cyborg.css +11 -0
- data/lib/templates/themes/darkly.css +11 -0
- data/lib/templates/themes/flatly.css +11 -0
- data/lib/templates/themes/journal.css +11 -0
- data/lib/templates/themes/litera.css +11 -0
- data/lib/templates/themes/lumen.css +11 -0
- data/lib/templates/themes/lux.css +11 -0
- data/lib/templates/themes/materia.css +11 -0
- data/lib/templates/themes/minty.css +11 -0
- data/lib/templates/themes/morph.css +11 -0
- data/lib/templates/themes/pulse.css +11 -0
- data/lib/templates/themes/quartz.css +11 -0
- data/lib/templates/themes/sandstone.css +11 -0
- data/lib/templates/themes/simplex.css +11 -0
- data/lib/templates/themes/sketchy.css +11 -0
- data/lib/templates/themes/slate.css +11 -0
- data/lib/templates/themes/solar.css +11 -0
- data/lib/templates/themes/spacelab.css +11 -0
- data/lib/templates/themes/superhero.css +11 -0
- data/lib/templates/themes/united.css +11 -0
- data/lib/templates/themes/vapor.css +11 -0
- data/lib/templates/themes/yeti.css +11 -0
- data/lib/templates/themes/zephyr.css +11 -0
- data/rspec-documentation/pages/000-Introduction/000-Quickstart.md +17 -0
- data/rspec-documentation/pages/000-Introduction.md +23 -0
- data/rspec-documentation/pages/010-File System/000-Ordering.md +14 -0
- data/rspec-documentation/pages/010-File System/010-Documentation Bundle.md +9 -0
- data/rspec-documentation/pages/010-File System.md +26 -0
- data/rspec-documentation/pages/020-Running Tests.md +41 -0
- data/rspec-documentation/pages/030-Examples/010-Basic.md +51 -0
- data/rspec-documentation/pages/030-Examples/020-HTML.md +45 -0
- data/rspec-documentation/pages/030-Examples/030-ANSI.md +33 -0
- data/rspec-documentation/pages/030-Examples/040-JSON.md +39 -0
- data/rspec-documentation/pages/030-Examples/050-YAML.md +40 -0
- data/rspec-documentation/pages/030-Examples.md +7 -0
- data/rspec-documentation/pages/040-Spec Helper.md +11 -0
- data/rspec-documentation/pages/050-Linking.md +20 -0
- data/rspec-documentation/pages/060-Configuration/010-Context.md +26 -0
- data/rspec-documentation/pages/060-Configuration/020-Build Paths.md +33 -0
- data/rspec-documentation/pages/060-Configuration/030-Attribution.md +23 -0
- data/rspec-documentation/pages/060-Configuration.md +13 -0
- data/rspec-documentation/pages/070-Publishing.md +13 -0
- data/rspec-documentation/pages/500-License.md +11 -0
- data/rspec-documentation/spec_helper.rb +8 -0
- data/rspec-documentation.gemspec +10 -1
- data/sig/rspec/documentation.rbs +1 -1
- metadata +193 -5
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSpecDocumentation
|
|
4
|
+
# Receives a spec manifest and generates a `Kramdown::Document` contaning an HTML node with a
|
|
5
|
+
# tabbed view displaying the example's code, its output and, if format is `:html`, the rendered
|
|
6
|
+
# HTML of the output. Injected into the parsed `Kramdown::Document` for the root Markdown file,
|
|
7
|
+
# replacing the code block it was produced from.
|
|
8
|
+
class HtmlElement
|
|
9
|
+
def initialize(spec:)
|
|
10
|
+
@spec = spec
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def element
|
|
14
|
+
Kramdown::Document.new(tabbed_spec, input: 'html').root
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def code_source
|
|
18
|
+
formatter = Rouge::Formatters::HTML.new
|
|
19
|
+
lexer = Rouge::Lexers::Ruby.new
|
|
20
|
+
Formatters.with_translated_html_entities(formatter.format(lexer.lex(spec.source)))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def prettified_output
|
|
24
|
+
Formatters.with_translated_html_entities(formatter.prettified_output)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def rendered_output
|
|
28
|
+
return formatter.rendered_output if render_raw?
|
|
29
|
+
|
|
30
|
+
Formatters.with_translated_html_entities(formatter.rendered_output)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def render_raw?
|
|
34
|
+
formatter.render_raw?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def element_id
|
|
38
|
+
@element_id ||= SecureRandom.uuid
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
attr_reader :spec
|
|
44
|
+
|
|
45
|
+
def formatter
|
|
46
|
+
@formatter ||= {
|
|
47
|
+
html: Formatters::Html,
|
|
48
|
+
ansi: Formatters::Ansi,
|
|
49
|
+
json: Formatters::Json,
|
|
50
|
+
yaml: Formatters::Yaml
|
|
51
|
+
}.fetch(spec.format, Formatters::Ruby).new(subject: spec.subject)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def tabbed_spec
|
|
55
|
+
RSpecDocumentation.template('tabbed_spec').result(binding)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSpecDocumentation
|
|
4
|
+
# Compiles Javascript assets into a single file.
|
|
5
|
+
class JavascriptBundle
|
|
6
|
+
def flush
|
|
7
|
+
Util.bundle_dir.join('assets').mkpath
|
|
8
|
+
javascript_bundle_path.write(RSpecDocumentation.template(:layout, :js).result(binding))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def javascript_bundle_path
|
|
14
|
+
Util.bundle_dir.join('assets/bundle.js')
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSpecDocumentation
|
|
4
|
+
# Builds content for a collection of page paths, collates failures from embedded examples.
|
|
5
|
+
# Writes the final structure to disk.
|
|
6
|
+
class PageCollection
|
|
7
|
+
attr_reader :failures, :page_paths
|
|
8
|
+
|
|
9
|
+
def initialize(page_paths:)
|
|
10
|
+
@page_paths = page_paths.sort
|
|
11
|
+
@buffer = {}
|
|
12
|
+
@failures = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def generate
|
|
16
|
+
page_paths.zip(documents).each do |path, document|
|
|
17
|
+
buffer[bundle_path_for(path)] = document.render
|
|
18
|
+
failures.concat(document.failures)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def documents
|
|
23
|
+
@documents ||= page_paths.map do |path|
|
|
24
|
+
Document.new(document: path.read, path: path, page_tree: page_tree(path: path))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def flush
|
|
29
|
+
Util.bundle_dir.rmtree if Util.bundle_dir.directory?
|
|
30
|
+
Util.bundle_dir.mkpath
|
|
31
|
+
|
|
32
|
+
buffer.each do |path, content|
|
|
33
|
+
path.dirname.mkpath
|
|
34
|
+
Util.bundle_path(path).write(content)
|
|
35
|
+
end
|
|
36
|
+
write_index unless buffer.empty?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def examples_count
|
|
40
|
+
documents.map(&:specs).flatten.size
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
attr_reader :buffer
|
|
46
|
+
|
|
47
|
+
def write_index
|
|
48
|
+
_path, content = buffer.first
|
|
49
|
+
Util.bundle_index_path.write(content)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def page_tree(path:)
|
|
53
|
+
PageTree.new(page_paths: page_paths, current_path: path)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def bundle_path_for(path)
|
|
57
|
+
Util.bundle_path(path)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def root_path
|
|
61
|
+
Pathname.new(Dir.pwd).join('rspec-documentation')
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSpecDocumentation
|
|
4
|
+
# A hierarchical structure of all pages in the documentation tree. Used for rendering a navigation section.
|
|
5
|
+
class PageTree
|
|
6
|
+
def initialize(page_paths:, current_path:)
|
|
7
|
+
@page_paths = page_paths
|
|
8
|
+
@current_path = current_path
|
|
9
|
+
@structure = {}
|
|
10
|
+
@nodes = []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def elements
|
|
14
|
+
build_nodes(
|
|
15
|
+
root: tree['rspec-documentation']['pages'],
|
|
16
|
+
path: root_path.join('rspec-documentation/pages')
|
|
17
|
+
)
|
|
18
|
+
nodes.flatten.compact
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
attr_reader :page_paths, :structure, :nodes, :current_path
|
|
24
|
+
|
|
25
|
+
def tree
|
|
26
|
+
@tree ||= begin
|
|
27
|
+
build_tree
|
|
28
|
+
structure
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def build_nodes(root:, path:)
|
|
33
|
+
root[:children].sort.each do |child|
|
|
34
|
+
node = page_tree_node(path: path, child: child)
|
|
35
|
+
next nil if node.nil?
|
|
36
|
+
|
|
37
|
+
li_open, *li_body, li_close = node
|
|
38
|
+
nodes.push(li_open)
|
|
39
|
+
nodes.concat(li_body)
|
|
40
|
+
push_children(root: root, path: path, child: child)
|
|
41
|
+
nodes.push(li_close)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def push_children(root:, path:, child:)
|
|
46
|
+
nodes.push('<ol>')
|
|
47
|
+
build_nodes(root: root[child], path: path.join(child)) unless root[child].nil?
|
|
48
|
+
nodes.last == '<ol>' ? nodes.pop : nodes.push('</ol>')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def build_tree(branch: structure, depth: 0)
|
|
52
|
+
normalized_paths.each do |path|
|
|
53
|
+
first, second, *rest = path_segments(path: path, depth: depth)
|
|
54
|
+
next if second.nil?
|
|
55
|
+
|
|
56
|
+
branch[first] ||= {}
|
|
57
|
+
branch[first][:children] ||= Set.new
|
|
58
|
+
branch[first][:children].add(second)
|
|
59
|
+
build_tree(branch: branch[first], depth: depth + 1)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def path_segments(path:, depth:)
|
|
64
|
+
path.to_s.split('/')[depth..]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def root_path
|
|
68
|
+
@root_path ||= Pathname.new(Dir.pwd)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def normalized_paths
|
|
72
|
+
@normalized_paths ||= page_paths.sort.map do |path|
|
|
73
|
+
path.relative_path_from(root_path)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def page_tree_node(path:, child:)
|
|
78
|
+
PageTreeElement.new(path: path, child: child, current_path: current_path).node
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSpecDocumentation
|
|
4
|
+
# An element in a page tree, generates a single entry as a list item, linked to the relevant
|
|
5
|
+
# document (or just a text entry if no document exists, i.e. if it is a directory without an
|
|
6
|
+
# index file).
|
|
7
|
+
class PageTreeElement
|
|
8
|
+
def initialize(path:, child:, current_path:)
|
|
9
|
+
@path = path
|
|
10
|
+
@child = child
|
|
11
|
+
@current_path = current_path
|
|
12
|
+
@nodes = []
|
|
13
|
+
raise MissingFileError, "Missing file: #{entry_path.relative_path_from(Util.base_dir)}" unless entry?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def node
|
|
17
|
+
return nil if entry_and_directory?
|
|
18
|
+
|
|
19
|
+
nodes.push(li_open)
|
|
20
|
+
nodes.push(link)
|
|
21
|
+
nodes.push('</li>')
|
|
22
|
+
nodes
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
attr_reader :path, :child, :current_path, :nodes
|
|
28
|
+
|
|
29
|
+
def entry_and_directory?
|
|
30
|
+
return false unless path.join(child).sub_ext('.md').file?
|
|
31
|
+
return false unless path.join(child).sub_ext('').directory?
|
|
32
|
+
return false unless path.join(child).file?
|
|
33
|
+
|
|
34
|
+
true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def entry_path
|
|
38
|
+
path.join(child).sub_ext('.md')
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def entry?
|
|
42
|
+
entry_path.file?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def li_open
|
|
46
|
+
"<li id='#{Util.path_id(path.join(child))}' #{active_class} data-list-item-id='##{path_id}' " \
|
|
47
|
+
"data-parent-id='##{parent_path_id}'>"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def link
|
|
51
|
+
"<a #{active_class} href='#{href}'>#{title}</a>"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def path_id
|
|
55
|
+
@path_id ||= Util.path_id(path.join(child))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def parent_path_id
|
|
59
|
+
@parent_path_id ||= Util.path_id(path)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def active_class
|
|
63
|
+
active? ? 'class="active"' : nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def href
|
|
67
|
+
Util.href(path.join(child))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def active?
|
|
71
|
+
Util.href(current_path) == href
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def title
|
|
75
|
+
Util.label(child)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSpecDocumentation
|
|
4
|
+
# A parsed Markdown file, provides matched RSpec examples from the document and their location.
|
|
5
|
+
class ParsedDocument
|
|
6
|
+
attr_reader :failures
|
|
7
|
+
|
|
8
|
+
def initialize(document, path:)
|
|
9
|
+
@document = Kramdown::Document.new(document, input: 'GFM', syntax_highlighter: 'rouge')
|
|
10
|
+
@path = path
|
|
11
|
+
@failures = []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def html
|
|
15
|
+
document.to_html
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def execute_and_substitute_examples!
|
|
19
|
+
specs.each do |spec|
|
|
20
|
+
spec.run
|
|
21
|
+
break failures << spec.failure unless spec.failure.nil?
|
|
22
|
+
|
|
23
|
+
spec.parent.children[spec.index] = spec_element(spec)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def specs
|
|
28
|
+
@specs ||= recursive_specs
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
attr_reader :document, :path
|
|
34
|
+
|
|
35
|
+
def recursive_specs(element: document.root)
|
|
36
|
+
element.children.each.with_index.map do |child, index|
|
|
37
|
+
next recursive_specs(element: child) unless child.children.empty?
|
|
38
|
+
next nil unless rspec_codeblock?(child)
|
|
39
|
+
|
|
40
|
+
spec_for(element: child, parent: element, index: index)
|
|
41
|
+
end.flatten.compact
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def rspec_codeblock?(element)
|
|
45
|
+
return false unless element.type == :codeblock
|
|
46
|
+
return false unless element.options.key?(:lang)
|
|
47
|
+
|
|
48
|
+
element.options[:lang] == 'rspec' || element.options[:lang].start_with?('rspec:')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def spec_for(element:, parent:, index:)
|
|
52
|
+
Spec.new(
|
|
53
|
+
spec: element.value,
|
|
54
|
+
format: element.options[:lang].partition(':').last,
|
|
55
|
+
parent: parent,
|
|
56
|
+
index: index,
|
|
57
|
+
path: path,
|
|
58
|
+
location: element.options[:location]
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def spec_element(spec)
|
|
63
|
+
Kramdown::Element.new(:html_element, 'div', { location: spec.location }).tap do |element|
|
|
64
|
+
element.children = HtmlElement.new(spec: spec).element.children
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def report_error(spec)
|
|
69
|
+
$stderr.write(spec.failure.message)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSpecDocumentation
|
|
4
|
+
# Initializes a new project if `rspec-documentation/` directory is empty.
|
|
5
|
+
class ProjectInitialization
|
|
6
|
+
include Paintbrush
|
|
7
|
+
|
|
8
|
+
def flush
|
|
9
|
+
print_welcome
|
|
10
|
+
create_base_dir
|
|
11
|
+
create_sample_files
|
|
12
|
+
print_initialization_complete
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def print_welcome
|
|
18
|
+
warn(paintbrush { blue "\nWelcome to #{cyan 'RSpec Documentation'}. A new project is being initialized.\n" })
|
|
19
|
+
warn(paintbrush { blue "If you want undo at any point, simply delete #{cyan 'rspec-documentation/'}.\n" })
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def create_base_dir
|
|
23
|
+
Util.base_dir.mkpath
|
|
24
|
+
print_created(Util.base_dir)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def create_sample_files
|
|
28
|
+
sample_files.each do |sample_file|
|
|
29
|
+
Util.base_dir.join(sample_file).sub_ext('.md').write(RSpecDocumentation.template(sample_file, :md).result)
|
|
30
|
+
print_created(Util.base_dir.join(sample_file).sub_ext('.md'))
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def print_created(path)
|
|
35
|
+
warn(paintbrush { " #{green_b 'create'} #{cyan path.relative_path_from(pwd)}" })
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def print_initialization_complete
|
|
39
|
+
warn(paintbrush do
|
|
40
|
+
blue "\nYour documentation project has been #{green 'initialized'} wlth smoe example pages."
|
|
41
|
+
end)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def pwd
|
|
45
|
+
Pathname.new(Dir.pwd)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def sample_files
|
|
49
|
+
%w[000-Introduction 010-Usage 500-License]
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSpecDocumentation
|
|
4
|
+
module RSpec
|
|
5
|
+
# Stores information about a failed RSpec example. Thin wrapper around `RSpec::Failure`.
|
|
6
|
+
class Failure
|
|
7
|
+
include Paintbrush
|
|
8
|
+
|
|
9
|
+
attr_reader :spec
|
|
10
|
+
|
|
11
|
+
def initialize(cause:, spec:)
|
|
12
|
+
@cause = cause
|
|
13
|
+
@spec = spec
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def message
|
|
17
|
+
"\n#{formatted_header}\n\n#{formatted_source}\n\n#{formatted_cause}\n\n"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
attr_reader :cause
|
|
23
|
+
|
|
24
|
+
def formatted_header
|
|
25
|
+
paintbrush { cyan indented("# #{path}:#{spec.location}") }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def path
|
|
29
|
+
spec.path.relative_path_from(Pathname.new(Dir.pwd))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def formatted_source
|
|
33
|
+
paintbrush { white indented(spec.source) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def formatted_cause
|
|
37
|
+
paintbrush { red indented(without_anonymous_group_text(cause.message)) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def indented(text)
|
|
41
|
+
text.split("\n").map { |line| " #{line}" }.join("\n")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# If an error occurs outside of a test, RSpec will provide an error to the Reporter
|
|
45
|
+
# referring to an anonymous group due to the way specs are evaluated. This does not help
|
|
46
|
+
# with debugging so we remove it. TODO: Find a better way ?
|
|
47
|
+
def without_anonymous_group_text(text)
|
|
48
|
+
text&.gsub(/ for #<RSpec::ExampleGroups::Anonymous.*$/, '')
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'rspec/failure'
|
|
4
|
+
|
|
5
|
+
module RSpecDocumentation
|
|
6
|
+
# Provides various classes that wrap or interface RSpec internals.
|
|
7
|
+
module RSpec
|
|
8
|
+
def self.with_failure_notifier(callable, &block)
|
|
9
|
+
::RSpec::Support.with_failure_notifier(callable, &block)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSpecDocumentation
|
|
4
|
+
# Executes specs from a Markdown code block and provides the outcome in the appropriate format.
|
|
5
|
+
class Spec
|
|
6
|
+
attr_reader :parent, :location, :index, :format, :path
|
|
7
|
+
|
|
8
|
+
@durations = []
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
attr_accessor :subjects, :durations
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(spec:, format:, parent:, location:, path:, index:) # rubocop:disable Metrics/ParameterLists
|
|
15
|
+
@spec = spec
|
|
16
|
+
@format = format.empty? ? :plaintext : format.to_sym
|
|
17
|
+
@parent = parent
|
|
18
|
+
@location = location
|
|
19
|
+
@path = path
|
|
20
|
+
@index = index
|
|
21
|
+
@failures = []
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def subject
|
|
25
|
+
raise Error, "Code block did not define an example (e.g. with `it`).\n#{spec}" if examples.empty?
|
|
26
|
+
raise Error, "Code block did not define a subject:\n#{spec}" if subjects.empty?
|
|
27
|
+
raise Error, "Cannot define more than one example per code block:\n#{spec}" if subjects.size > 1
|
|
28
|
+
|
|
29
|
+
subjects.last
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def source
|
|
33
|
+
spec
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def failure
|
|
37
|
+
failures.first
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def reporter
|
|
41
|
+
@reporter ||= ::RSpec::Core::Reporter.new(::RSpec::Core::Configuration.new)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def run
|
|
45
|
+
self.class.subjects = []
|
|
46
|
+
RSpec.with_failure_notifier(failure_notifier) do
|
|
47
|
+
succeeded = example_group.run(reporter)
|
|
48
|
+
durations << run_time if run_time
|
|
49
|
+
next succeeded if succeeded
|
|
50
|
+
|
|
51
|
+
notify_failure(reported_failure)
|
|
52
|
+
succeeded
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
attr_reader :spec, :failures
|
|
59
|
+
|
|
60
|
+
def subjects
|
|
61
|
+
self.class.subjects
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def durations
|
|
65
|
+
self.class.durations
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def examples
|
|
69
|
+
@examples ||= example_group.examples
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def example_group
|
|
73
|
+
# rubocop:disable Style/DocumentDynamicEvalDefinition, Security/Eval
|
|
74
|
+
@example_group ||= binding.eval(
|
|
75
|
+
<<-SPEC, __FILE__, __LINE__.to_i
|
|
76
|
+
::RSpec::Core::ExampleGroup.describe do
|
|
77
|
+
after { RSpecDocumentation::Spec.subjects << subject }
|
|
78
|
+
include_context '__rspec_documentation' do
|
|
79
|
+
#{spec}
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
SPEC
|
|
83
|
+
)
|
|
84
|
+
# rubocop:enable Style/DocumentDynamicEvalDefinition, Security/Eval
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def run_time
|
|
88
|
+
return nil unless reporter&.examples&.first
|
|
89
|
+
|
|
90
|
+
@run_time ||= reporter.examples.first.execution_result.run_time
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def notify_failure(failure)
|
|
94
|
+
failures << RSpec::Failure.new(cause: failure, spec: self)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def reported_failure
|
|
98
|
+
exception = reporter.failed_examples.first.exception
|
|
99
|
+
return exception unless exception.is_a?(::RSpec::Core::MultipleExceptionError)
|
|
100
|
+
|
|
101
|
+
exception.all_exceptions.first
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def failure_notifier
|
|
105
|
+
proc { |failure| notify_failure(failure) }
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSpecDocumentation
|
|
4
|
+
# Compiles CSS assets into a single file.
|
|
5
|
+
class StylesheetBundle
|
|
6
|
+
def flush
|
|
7
|
+
Util.bundle_dir.join('assets').mkpath
|
|
8
|
+
stylesheet_bundle_path.write(RSpecDocumentation.template(:layout, :css).result(binding))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def stylesheet_bundle_path
|
|
14
|
+
Util.bundle_dir.join('assets/bundle.css')
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|