path_coverage 0.0.1 → 0.0.2

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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.md +22 -0
  3. data/README.md +316 -0
  4. data/exe/path_coverage_collector +26 -0
  5. data/lib/path_coverage/auto.rb +5 -0
  6. data/lib/path_coverage/collector.rb +46 -0
  7. data/lib/path_coverage/condition.rb +45 -0
  8. data/lib/path_coverage/configuration.rb +127 -0
  9. data/lib/path_coverage/gaps.rb +60 -0
  10. data/lib/path_coverage/html/file.html +33 -0
  11. data/lib/path_coverage/html/index.html +38 -0
  12. data/lib/path_coverage/html/side.js +531 -0
  13. data/lib/path_coverage/html/sort.js +28 -0
  14. data/lib/path_coverage/html/style.css +334 -0
  15. data/lib/path_coverage/html/toggle.js +12 -0
  16. data/lib/path_coverage/html_file.rb +109 -0
  17. data/lib/path_coverage/html_index.rb +43 -0
  18. data/lib/path_coverage/html_nav.rb +67 -0
  19. data/lib/path_coverage/html_paths.rb +138 -0
  20. data/lib/path_coverage/html_report.rb +103 -0
  21. data/lib/path_coverage/html_template.rb +20 -0
  22. data/lib/path_coverage/html_tree.rb +137 -0
  23. data/lib/path_coverage/json_writer.rb +21 -0
  24. data/lib/path_coverage/kernel.rb +179 -0
  25. data/lib/path_coverage/manifest.rb +177 -0
  26. data/lib/path_coverage/manifest_choices.rb +113 -0
  27. data/lib/path_coverage/merge.rb +35 -0
  28. data/lib/path_coverage/path_gaps.rb +41 -0
  29. data/lib/path_coverage/path_manifest.rb +110 -0
  30. data/lib/path_coverage/paths_report.rb +55 -0
  31. data/lib/path_coverage/post.rb +26 -0
  32. data/lib/path_coverage/recorder.rb +99 -0
  33. data/lib/path_coverage/render.rb +92 -0
  34. data/lib/path_coverage/result.rb +59 -0
  35. data/lib/path_coverage/rewriter.rb +120 -0
  36. data/lib/path_coverage/site.rb +11 -0
  37. data/lib/path_coverage/site_paths.rb +74 -0
  38. data/lib/path_coverage/site_tree.rb +94 -0
  39. data/lib/path_coverage/source_lines.rb +79 -0
  40. data/lib/path_coverage/summary.rb +33 -0
  41. data/lib/path_coverage/version.rb +3 -0
  42. data/lib/path_coverage.rb +2 -5
  43. metadata +68 -13
@@ -0,0 +1,138 @@
1
+ require 'cgi/escape'
2
+ require 'prism'
3
+ require_relative 'path_gaps'
4
+
5
+ module PathCoverage
6
+ # SPIKE: the path grids of one covered file's page, hidden in
7
+ # the page until side.js moves them into the rail. Each grid
8
+ # has a row per decision, to sit level with that decision's
9
+ # line, and a column per possible path, never-taken first.
10
+ module HtmlPaths
11
+ # The hidden grids of every method in methods.
12
+ def self.grids(source, methods, paths)
13
+ ends = end_lines(source)
14
+ arms = arm_lines(source)
15
+ methods.map do |site, method|
16
+ grid(site, method, paths.fetch(site, {}), ends, arms)
17
+ end.join("\n")
18
+ end
19
+
20
+ # { an if's first line => its parts' lines } for every if:
21
+ # head, its if and predicate; then, its then arm; else, its
22
+ # else (or elsif) keyword through its arm; end, its end line.
23
+ # Each is [first, last], nil when absent; a modifier if or
24
+ # ternary is all head.
25
+ def self.arm_lines(source)
26
+ arms = {}
27
+ walk = lambda do |node|
28
+ arms[node.location.start_line] = if_parts(node) if node.type == :if_node
29
+ node.compact_child_nodes.each { |child| walk.call(child) }
30
+ end
31
+ walk.call(Prism.parse(source).value)
32
+ arms
33
+ end
34
+
35
+ # The lines of an if's parts, as arm_lines gives them.
36
+ def self.if_parts(node)
37
+ start = node.location.start_line
38
+ return { head: [start, start] } if node.end_keyword_loc.nil?
39
+
40
+ last = node.end_keyword_loc.start_line
41
+ then_arm = span(node.statements)
42
+ else_part = else_lines(node.subsequent, last)
43
+ body = [then_arm&.first, else_part&.first, last].compact.min
44
+ {
45
+ head: [start, [start, body - 1].max], then: then_arm,
46
+ else: else_part, end: [last, last]
47
+ }
48
+ end
49
+
50
+ # The lines from an else or elsif keyword to the end of its
51
+ # arm, the line before the if's end for an elsif.
52
+ def self.else_lines(otherwise, last)
53
+ return nil if otherwise.nil?
54
+ return [otherwise.location.start_line, last - 1] if otherwise.type == :if_node
55
+
56
+ first = otherwise.else_keyword_loc.start_line
57
+ [first, otherwise.statements ? otherwise.statements.location.end_line : first]
58
+ end
59
+
60
+ # The first and last lines of statements, nil for none.
61
+ def self.span(statements)
62
+ return nil if statements.nil?
63
+
64
+ loc = statements.location
65
+ [loc.start_line, loc.end_line]
66
+ end
67
+
68
+ # { def's first line => its last line } for every def.
69
+ def self.end_lines(source)
70
+ ends = {}
71
+ walk = lambda do |node|
72
+ if node.type == :def_node
73
+ loc = node.location
74
+ ends[loc.start_line] = loc.end_line
75
+ end
76
+ node.compact_child_nodes.each { |child| walk.call(child) }
77
+ end
78
+ walk.call(Prism.parse(source).value)
79
+ ends
80
+ end
81
+
82
+ # One method's grid: its calls row, level with its def, then
83
+ # a row per decision, level with that decision's line.
84
+ def self.grid(site, method, taken, ends, arms)
85
+ first = site.split(':')[-2].to_i
86
+ every = PathGaps.possible(method.decisions)
87
+ ordered = every.each_with_index.sort_by do |path, i|
88
+ [taken.key?(path) ? 1 : 0, i]
89
+ end.map(&:first)
90
+ missing = every.count { |path| !taken.key?(path) }
91
+ status = missing.zero? ? 'covered' : 'gaps'
92
+ rows = [calls_row(first, ordered, taken, method, every.size - missing)]
93
+ method.decisions.each_with_index do |decision, i|
94
+ rows << decision_row(decision, i, ordered, taken, arms)
95
+ end
96
+ attrs = [
97
+ %(id="paths-#{first}" data-first="#{first}"),
98
+ %(data-col="#{site.split(':').last}"),
99
+ %(data-last="#{ends.fetch(first, first)}" data-status="#{status}")
100
+ ].join(' ')
101
+ %(<div class="pathgrid" #{attrs} hidden="hidden">#{rows.join}</div>)
102
+ end
103
+
104
+ # The row level with the def: how many calls took each path,
105
+ # never-taken ones marked.
106
+ def self.calls_row(first, ordered, taken, method, seen)
107
+ cells = ordered.map do |path|
108
+ calls = taken.fetch(path, 0)
109
+ css = calls.zero? ? 'pcell never' : 'pcell taken'
110
+ %(<span class="#{css}">#{calls.zero? ? '0' : calls}</span>)
111
+ end
112
+ name = CGI.escapeHTML(method.name.to_s)
113
+ label = %(<span class="plabel">#{seen}/#{ordered.size}</span>)
114
+ title = %( title="#{name}: #{seen} of #{ordered.size} paths taken")
115
+ %(<div class="prow calls" data-line="#{first}"#{title}>#{label}#{cells.join}</div>)
116
+ end
117
+
118
+ # The row level with a decision: T or F for each path that
119
+ # makes it, those of never-taken paths marked, blank for a
120
+ # path that ended before it.
121
+ def self.decision_row(decision, index, ordered, taken, arms)
122
+ cells = ordered.map do |path|
123
+ outcome = path[index]
124
+ next '<span class="pcell ended"></span>' if outcome.nil?
125
+
126
+ never = taken.key?(path) ? '' : ' never'
127
+ %(<span class="pcell #{outcome}#{never}">#{outcome ? 'T' : 'F'}</span>)
128
+ end
129
+ label = %(<span class="plabel">#{decision.line}</span>)
130
+ arm = arms.fetch(decision.line, {})
131
+ parts = %i[head then else end].map do |part|
132
+ %(data-#{part}="#{arm[part]&.join(',')}")
133
+ end
134
+ data = [%(data-line="#{decision.line}"), *parts].join(' ')
135
+ %(<div class="prow" #{data}>#{label}#{cells.join}</div>)
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,103 @@
1
+ require 'fileutils'
2
+ require_relative 'html_file'
3
+ require_relative 'html_index'
4
+ require_relative 'html_paths'
5
+ require_relative 'path_manifest'
6
+ require_relative 'site_tree'
7
+ require_relative 'source_lines'
8
+ require_relative 'summary'
9
+
10
+ module PathCoverage
11
+ # The :html reporter: index.html, and files/<path>.html for
12
+ # each covered file, written to the configuration's
13
+ # output_dir.
14
+ module HtmlReport
15
+ # Writes every page. operands and site_paths are as Result
16
+ # gives them, site_paths holding every site in files; files
17
+ # is { full path => {source:, sites:} } for each covered
18
+ # file, as the kernel parsed it.
19
+ def self.write(operands, files, config, paths = nil, site_paths:)
20
+ dir = config.output_dir
21
+ index = HtmlIndex.page(Summary.files(operands))
22
+ File.write("#{dir}/index.html", index)
23
+ files.each do |full_path, parsed|
24
+ path = config.relative(full_path)
25
+ out = "#{dir}/files/#{path}.html"
26
+ # simplecov:disable
27
+ grids = paths ? path_grids(path, parsed, paths) : ''
28
+ # simplecov:enable
29
+ write_page(out, path, parsed, [operands, site_paths], grids)
30
+ end
31
+ end
32
+
33
+ # simplecov:disable
34
+ # SPIKE: the hidden path grids of one file's methods.
35
+ def self.path_grids(path, parsed, paths)
36
+ source = parsed[:source]
37
+ methods = PathManifest.methods(source, path)
38
+ HtmlPaths.grids(source, methods, paths)
39
+ end
40
+ # simplecov:enable
41
+
42
+ # Writes one file's page to out, from its sites' operand
43
+ # counts and paths.
44
+ def self.write_page(out, path, parsed,
45
+ (operands, site_paths), grids)
46
+ source, sites = parsed.values_at(:source, :sites)
47
+ lines = SourceLines.of(source, sites, operands)
48
+ trees = trees(source, sites, lines, site_paths)
49
+ FileUtils.mkdir_p(File.dirname(out))
50
+ File.write(out, HtmlFile.page(path, lines, trees, grids))
51
+ end
52
+
53
+ # { first line of a span => [SiteTree.of, ...] } for the
54
+ # sites of one file, each under the span its line is in.
55
+ def self.trees(source, sites, lines, site_paths)
56
+ trees = Hash.new { |all, first| all[first] = [] }
57
+ sites.each do |site, condition|
58
+ first = lines[SourceLines.line_of(site) - 1][:span].begin
59
+ tree = SiteTree.of(source, condition, site_paths.fetch(site))
60
+ trees[first] << located(source, condition, tree)
61
+ end
62
+ trees
63
+ end
64
+
65
+ # simplecov:disable
66
+ # SPIKE: tree with an at: of [line, column, line, column]
67
+ # on it and on each of its operands: a leaf's from its byte
68
+ # range, a condition's and an arm's from its extent, the
69
+ # whole of it as written. Columns count characters, not
70
+ # bytes.
71
+ def self.located(source, condition, tree)
72
+ pairs = condition.operands.zip(tree[:operands])
73
+ operands = pairs.map do |operand, node|
74
+ case operand
75
+ when Condition then located(source, operand, node)
76
+ when Arm then located_arm(source, operand, node)
77
+ else node.merge(at: at(source, operand))
78
+ end
79
+ end
80
+ tree.merge(at: at(source, condition.extent), operands:)
81
+ end
82
+
83
+ # An arm's node with its at:, and its body's tree located.
84
+ def self.located_arm(source, arm, node)
85
+ body = arm.body && located(source, arm.body, node[:body])
86
+ node.merge(at: at(source, arm.extent), body:)
87
+ end
88
+
89
+ # [line, column, line, column] of the byte range in source.
90
+ def self.at(source, range)
91
+ [*position(source, range.begin), *position(source, range.end)]
92
+ end
93
+
94
+ # [line, column] of a byte offset in source.
95
+ def self.position(source, offset)
96
+ before = source.byteslice(0, offset)
97
+ newline = before.rindex("\n")
98
+ column = before.length - (newline ? newline + 1 : 0)
99
+ [before.count("\n") + 1, column]
100
+ end
101
+ # simplecov:enable
102
+ end
103
+ end
@@ -0,0 +1,20 @@
1
+ module PathCoverage
2
+ # The HTML report's page templates, styles and scripts, kept
3
+ # as files beside this one, and the filling of their slots.
4
+ module HtmlTemplate
5
+ DIR = File.join(__dir__, 'html')
6
+
7
+ # The text of one of the report's template, style or script
8
+ # files.
9
+ def self.asset(name)
10
+ File.read(File.join(DIR, name))
11
+ end
12
+
13
+ # template with each {{name}} replaced by values[name].
14
+ def self.fill(template, values)
15
+ template.gsub(/\{\{(\w+)\}\}/) do
16
+ values.fetch(Regexp.last_match(1))
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,137 @@
1
+ require 'cgi/escape'
2
+
3
+ module PathCoverage
4
+ # The tree of a span's sites on a file's page, from SiteTree.of
5
+ # for each site: a grid whose columns are a site's paths.
6
+ module HtmlTree
7
+ OUTCOMES = { true => 'T', false => 'F', nil => '' }.freeze
8
+ # A bullet marks a path taking an arm.
9
+ TAKEN = { true => [0x2022].pack('U'), false => '' }.freeze
10
+
11
+ # The tree of a span, open when its sites have gaps, else
12
+ # hidden: each site's rows in a group of their own.
13
+ def self.of(number, site_trees, open)
14
+ groups = site_trees.map do |site|
15
+ rows = site_rows(site).join
16
+ %(<div class="site" data-site="">#{rows}</div>)
17
+ end
18
+ hidden = open ? '' : ' hidden="hidden"'
19
+ body = groups.join
20
+ %(<div class="tree" id="tree-#{number}"#{hidden}>#{body}</div>)
21
+ end
22
+
23
+ # A site's row, naming its operator and each path's count,
24
+ # then the rows of its operands.
25
+ def self.site_rows(site)
26
+ counts = count_cells(site[:counts])
27
+ [row(at(site), operator(site), counts), *operand_rows(site, '')]
28
+ end
29
+
30
+ # One cell per path's count, a path never taken marked as a
31
+ # gap.
32
+ def self.count_cells(counts)
33
+ counts.map do |count|
34
+ css = count.zero? ? 'count never' : 'count'
35
+ %(<span class="#{css}" data-count="">#{count}</span>)
36
+ end.join
37
+ end
38
+
39
+ # One cell per path, naming each with names.
40
+ def self.cells(values, names = OUTCOMES)
41
+ values.map do |value|
42
+ %(<span class="cell" data-cell="">#{names[value]}</span>)
43
+ end.join
44
+ end
45
+
46
+ # SPIKE: the data-at attribute of a node located in its
47
+ # source, '' for one that is not.
48
+ def self.at(node)
49
+ node[:at] ? %( data-at="#{node[:at].join(',')}") : ''
50
+ end
51
+
52
+ # The rows of a condition's operands, each branching from
53
+ # its operator after indent; a nested condition's operands,
54
+ # and those of an arm's condition, follow its own row,
55
+ # indented under its branch.
56
+ def self.operand_rows(condition, indent)
57
+ last = condition[:operands].size - 1
58
+ condition[:operands].each_with_index.flat_map do |operand, i|
59
+ tip = i == last
60
+ branch = indent + (tip ? '└─ ' : '├─ ')
61
+ under = indent + (tip ? ' ' : '│ ')
62
+ node_rows(operand, branch, under, condition[:operator])
63
+ end
64
+ end
65
+
66
+ # The rows of one operand of a condition whose operator is
67
+ # choice, branching after branch, any operands of its own
68
+ # indented to under.
69
+ def self.node_rows(operand, branch, under, choice)
70
+ if operand.key?(:arm)
71
+ return arm_rows(operand, branch, under, choice)
72
+ end
73
+ return [operand_row(operand, branch)] unless operand[:operator]
74
+
75
+ [subtree_row(operand, branch), *operand_rows(operand, under)]
76
+ end
77
+
78
+ # An arm's row: a ternary's, ? for then and : for else, an
79
+ # arm holding a condition showing the condition's operator
80
+ # and value, its operands below, a plain arm its text; an
81
+ # if's named then or else, as its text is statements. A
82
+ # plain arm marks the paths taking it.
83
+ def self.arm_rows(arm, branch, under, choice)
84
+ body = arm[:body]
85
+ values = cells((body || arm)[:cells], body ? OUTCOMES : TAKEN)
86
+ text = CGI.escapeHTML(arm_label(arm, choice))
87
+ lead = branch_cell(branch) +
88
+ %(<span class="arm" data-label="">#{text}</span>)
89
+ row = row(%( data-arm=""#{at(arm)}), lead, values)
90
+ body ? [row, *operand_rows(body, under)] : [row]
91
+ end
92
+
93
+ # The name of an arm of a condition whose operator is choice.
94
+ def self.arm_label(arm, choice)
95
+ return arm[:arm].to_s if choice == 'if'
96
+
97
+ mark = arm[:arm] == :then ? '?' : ':'
98
+ body = arm[:body]
99
+ "#{mark} #{body ? body[:operator] : arm[:text]}"
100
+ end
101
+
102
+ # A nested condition's row: its branch, its operator, and
103
+ # its value on each path.
104
+ def self.subtree_row(condition, branch)
105
+ lead = branch_cell(branch) + operator(condition)
106
+ attrs = %( data-subtree=""#{at(condition)})
107
+ row(attrs, lead, cells(condition[:cells]))
108
+ end
109
+
110
+ # A row of the grid, with attrs, lead's cells naming it,
111
+ # then values, its cells for each path.
112
+ def self.row(attrs, lead, values)
113
+ %(<div class="row"#{attrs}>#{lead}#{values}</div>)
114
+ end
115
+
116
+ # The cell naming a condition's operator.
117
+ def self.operator(condition)
118
+ name = CGI.escapeHTML(condition[:operator])
119
+ %(<span class="operator" data-operator="">#{name}</span>)
120
+ end
121
+
122
+ # One operand's row: its branch, text, and its outcome on
123
+ # each path.
124
+ def self.operand_row(operand, branch)
125
+ text = CGI.escapeHTML(operand[:text].to_s)
126
+ lead = branch_cell(branch) +
127
+ %(<span class="text" data-text="">#{text}</span>)
128
+ attrs = %( data-operand=""#{at(operand)})
129
+ row(attrs, lead, cells(operand[:cells]))
130
+ end
131
+
132
+ # The cell drawing a row's branch from its operator.
133
+ def self.branch_cell(branch)
134
+ %(<span class="branch" data-branch="">#{branch}</span>)
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,21 @@
1
+ require 'json'
2
+
3
+ module PathCoverage
4
+ # The base reporter: the result as raw JSON, which every
5
+ # other reporter can read back.
6
+ module JsonWriter
7
+ # Writes operands, as Result gives them, and paths, as
8
+ # PathsReport gives them, to path as {"sites": {site:
9
+ # [counts, ...]}, "paths": {site: {leaves:, taken:}}}.
10
+ def self.write(operands, path, paths = {})
11
+ File.write(path, json(operands, paths))
12
+ end
13
+
14
+ # operands, and any paths, as the JSON report's text.
15
+ def self.json(operands, paths = {})
16
+ report = { 'sites' => operands }
17
+ report['paths'] = paths unless paths.empty?
18
+ JSON.pretty_generate(report)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,179 @@
1
+ require 'fileutils'
2
+ require_relative 'configuration'
3
+ require_relative 'json_writer'
4
+ require_relative 'manifest'
5
+ require_relative 'path_gaps'
6
+ require_relative 'path_manifest'
7
+ require_relative 'paths_report'
8
+ require_relative 'recorder'
9
+ require_relative 'render'
10
+ require_relative 'result'
11
+ require_relative 'rewriter'
12
+
13
+ # Path coverage for Ruby. This file is the kernel: it loads
14
+ # before any covered file and is never itself instrumented, since
15
+ # an instrumented recorder would record itself.
16
+ module PathCoverage
17
+ @recorder = Recorder.new
18
+ @config = nil
19
+ # { full path => {source:, sites:} } of each covered file
20
+ # parsed as it loaded, so the exit report need not read it
21
+ # again.
22
+ @parsed = {}
23
+
24
+ # The one recorder that every instrumented operand reports to.
25
+ def self.recorder
26
+ @recorder
27
+ end
28
+
29
+ # The configuration of the latest start, which the load
30
+ # hook consults; nil before the first start.
31
+ def self.config
32
+ @config
33
+ end
34
+
35
+ # The call rewritten code makes around each operand; returns
36
+ # value unchanged.
37
+ def self.record(site, index, value)
38
+ @recorder.record(site, index, value)
39
+ end
40
+
41
+ # The call rewritten code makes before a site's first leaf,
42
+ # site having size leaves; returns the evaluation's trail.
43
+ def self.enter(site, size)
44
+ @recorder.enter(site, size)
45
+ end
46
+
47
+ # The call rewritten code makes around a site as a whole,
48
+ # with the trail enter gave it; returns value unchanged.
49
+ def self.leave(trail, value)
50
+ @recorder.leave(trail, value)
51
+ end
52
+
53
+ # The call rewritten code makes as a call reaches its
54
+ # method's first decision; returns the call's new path.
55
+ def self.path(site, size)
56
+ @recorder.path(site, size)
57
+ end
58
+
59
+ # The call rewritten code makes around each decision's
60
+ # predicate, with exits for one whose arm leaves the method;
61
+ # returns value unchanged.
62
+ def self.decide(path, index, value, exits = nil)
63
+ @recorder.decide(path, index, value, exits)
64
+ end
65
+
66
+ # Instruments every file later required that the settings
67
+ # block covers, and reports when the process exits. on_exit
68
+ # is Kernel#at_exit except in tests, which must not print
69
+ # a report as their own process exits.
70
+ def self.start(on_exit: method(:at_exit), &)
71
+ config = Configuration.new(&)
72
+ install_hook if @config.nil?
73
+ @config = config
74
+ on_exit.call { report(config) }
75
+ end
76
+
77
+ # Starts from settings in env alone, for a run whose code
78
+ # cannot be changed, eg one started by RUBYOPT=-r: covers
79
+ # the comma-separated PATH_COVERAGE_COVER globs under
80
+ # PATH_COVERAGE_ROOT, writing the JSON report into
81
+ # PATH_COVERAGE_DIR and POSTing it to
82
+ # PATH_COVERAGE_COLLECTOR. Without a collector it
83
+ # starts nothing, so RUBYOPT can be set for every run.
84
+ def self.start_from_env(env, on_exit: method(:at_exit))
85
+ return unless env.key?('PATH_COVERAGE_COLLECTOR')
86
+
87
+ start(on_exit:) do
88
+ root env.fetch('PATH_COVERAGE_ROOT')
89
+ env.fetch('PATH_COVERAGE_COVER').split(',').each do |glob|
90
+ cover glob
91
+ end
92
+ coverage_dir env.fetch('PATH_COVERAGE_DIR')
93
+ collector env.fetch('PATH_COVERAGE_COLLECTOR')
94
+ report :post
95
+ end
96
+ end
97
+
98
+ # Routes every later load through instrumented, under
99
+ # whichever configuration is current. Installed once: a
100
+ # second definition would warn that it was redefined.
101
+ def self.install_hook
102
+ iseq = RubyVM::InstructionSequence
103
+ iseq.singleton_class.define_method(:load_iseq) do |path|
104
+ PathCoverage.instrumented(path, PathCoverage.config)
105
+ end
106
+ end
107
+
108
+ # The compiled, rewritten code of path, or nil for a path
109
+ # config does not cover, which Ruby then loads as normal.
110
+ def self.instrumented(path, config)
111
+ return unless config.covers?(path)
112
+
113
+ parsed = @parsed[path] = Manifest.parse(path, config)
114
+ methods = path_methods(path, parsed[:source], config)
115
+ sites = parsed[:sites]
116
+ rewritten = Rewriter.rewrite(parsed[:source], sites, methods)
117
+ RubyVM::InstructionSequence.compile(rewritten, path, path)
118
+ end
119
+
120
+ # The methods of the covered file at path whose paths are
121
+ # counted, none unless config measures paths.
122
+ def self.path_methods(path, source, config)
123
+ return {} unless config.measures.include?(:paths)
124
+
125
+ PathManifest.methods(source, config.relative(path))
126
+ end
127
+
128
+ # Writes the JSON report, then runs the chosen reporters, for
129
+ # every covered file whether or not the run loaded it, into
130
+ # coverage_dir, created first if need be. The paths never
131
+ # taken follow the condition gaps.
132
+ def self.report(config)
133
+ files = parsed_files(config)
134
+ result = collect(files, config)
135
+ paths = config.measures.include?(:paths) ? @recorder.paths : nil
136
+ Render.run(
137
+ result.operands, files, config, paths, site_paths: result.paths
138
+ )
139
+ return unless config.reporters.include?(:gaps)
140
+
141
+ methods = path_methods_of(files, config)
142
+ PathGaps.lines(methods, @recorder.paths).each { |line| puts line }
143
+ end
144
+
145
+ # The methods whose paths are counted of every covered file,
146
+ # in one manifest.
147
+ def self.path_methods_of(files, config)
148
+ all = files.map do |path, parsed|
149
+ path_methods(path, parsed[:source], config)
150
+ end
151
+ all.reduce({}, :merge)
152
+ end
153
+
154
+ # The Result of every covered file, with the counts and site
155
+ # paths this run recorded, once its operands and paths are
156
+ # written as the JSON report.
157
+ def self.collect(files, config)
158
+ sites = merged_sites(files)
159
+ result = Result.new(sites, @recorder.counts, @recorder.site_paths)
160
+ FileUtils.mkdir_p(config.output_dir)
161
+ report = PathsReport.of(result.paths, files)
162
+ JsonWriter.write(result.operands, config.json_path, report)
163
+ result
164
+ end
165
+
166
+ # { full path => {source:, sites:} } for every covered file:
167
+ # as parsed when it loaded, or parsed now for a file the run
168
+ # never loaded.
169
+ def self.parsed_files(config)
170
+ config.files.to_h do |path|
171
+ [path, @parsed.fetch(path) { Manifest.parse(path, config) }]
172
+ end
173
+ end
174
+
175
+ # The sites of every covered file, in one manifest.
176
+ def self.merged_sites(files)
177
+ files.values.map { |parsed| parsed[:sites] }.reduce({}, :merge)
178
+ end
179
+ end