rspec-documentation 0.0.1 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|