path_coverage 0.0.1 → 0.1.0
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/LICENSE.md +22 -0
- data/README.md +353 -0
- data/exe/path_coverage_collector +37 -0
- data/lib/path_coverage/auto.rb +5 -0
- data/lib/path_coverage/calls.rb +67 -0
- data/lib/path_coverage/collector.rb +112 -0
- data/lib/path_coverage/condition.rb +96 -0
- data/lib/path_coverage/configuration.rb +130 -0
- data/lib/path_coverage/gaps.rb +60 -0
- data/lib/path_coverage/html/file.html +33 -0
- data/lib/path_coverage/html/index.html +41 -0
- data/lib/path_coverage/html/side.js +542 -0
- data/lib/path_coverage/html/sort.js +31 -0
- data/lib/path_coverage/html/style.css +338 -0
- data/lib/path_coverage/html/toggle.js +12 -0
- data/lib/path_coverage/html_file.rb +109 -0
- data/lib/path_coverage/html_index.rb +74 -0
- data/lib/path_coverage/html_nav.rb +67 -0
- data/lib/path_coverage/html_paths.rb +138 -0
- data/lib/path_coverage/html_report.rb +118 -0
- data/lib/path_coverage/html_template.rb +20 -0
- data/lib/path_coverage/html_tree.rb +146 -0
- data/lib/path_coverage/json_writer.rb +25 -0
- data/lib/path_coverage/kernel.rb +165 -0
- data/lib/path_coverage/manifest.rb +205 -0
- data/lib/path_coverage/manifest_blocks.rb +57 -0
- data/lib/path_coverage/manifest_case.rb +54 -0
- data/lib/path_coverage/manifest_choices.rb +132 -0
- data/lib/path_coverage/merge.rb +35 -0
- data/lib/path_coverage/path_gaps.rb +41 -0
- data/lib/path_coverage/path_manifest.rb +110 -0
- data/lib/path_coverage/paths_report.rb +55 -0
- data/lib/path_coverage/post.rb +26 -0
- data/lib/path_coverage/recorder.rb +182 -0
- data/lib/path_coverage/render.rb +92 -0
- data/lib/path_coverage/result.rb +59 -0
- data/lib/path_coverage/rewriter.rb +166 -0
- data/lib/path_coverage/site.rb +16 -0
- data/lib/path_coverage/site_paths.rb +86 -0
- data/lib/path_coverage/site_tree.rb +134 -0
- data/lib/path_coverage/source_lines.rb +79 -0
- data/lib/path_coverage/summary.rb +52 -0
- data/lib/path_coverage/totals.rb +29 -0
- data/lib/path_coverage/version.rb +3 -0
- data/lib/path_coverage.rb +2 -5
- metadata +73 -14
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
require_relative 'condition'
|
|
2
|
+
|
|
3
|
+
module PathCoverage
|
|
4
|
+
# The tree shown under a site's line on its file's page: a
|
|
5
|
+
# grid whose columns are the site's paths, never taken first.
|
|
6
|
+
module SiteTree
|
|
7
|
+
# { operator:, counts:, cells:, operands: } for one site,
|
|
8
|
+
# from its paths, [{path:, count:}, ...]. counts are the
|
|
9
|
+
# columns' counts. Every node has cells, one per column: a
|
|
10
|
+
# leaf { text:, cells: } its outcome, nil where unreached;
|
|
11
|
+
# a condition { operator:, cells:, operands: } its value;
|
|
12
|
+
# an arm { arm:, text:, cells:, body: } whether the column
|
|
13
|
+
# took it, body being its condition's tree, nil for a plain
|
|
14
|
+
# arm. Texts are cut from source at the manifest's ranges.
|
|
15
|
+
def self.of(source, condition, paths)
|
|
16
|
+
columns = paths.sort_by.with_index do |taken, i|
|
|
17
|
+
[taken[:count].zero? ? 0 : 1, i]
|
|
18
|
+
end
|
|
19
|
+
outcomes = columns.map { |taken| taken[:path] }
|
|
20
|
+
tree = node(source, condition, outcomes, (0..).each)
|
|
21
|
+
tree.merge(counts: columns.map { |taken| taken[:count] })
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The tree of condition, each leaf taking the next index of
|
|
25
|
+
# leaves, so leaves pair with path entries left to right.
|
|
26
|
+
def self.node(source, condition, outcomes, leaves)
|
|
27
|
+
if condition.operator == 'case'
|
|
28
|
+
return case_arms(source, condition.operands, outcomes, leaves)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
operands = operands_of(source, condition, outcomes, leaves)
|
|
32
|
+
arms(source, condition, operands, outcomes, leaves)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# condition's operands' nodes, leaves and nested conditions
|
|
36
|
+
# built in order, its arms left for arms to build after.
|
|
37
|
+
def self.operands_of(source, condition, outcomes, leaves)
|
|
38
|
+
condition.operands.map do |operand|
|
|
39
|
+
case operand
|
|
40
|
+
when Condition then node(source, operand, outcomes, leaves)
|
|
41
|
+
when Arm then operand
|
|
42
|
+
else leaf(source, operand, outcomes, leaves.next)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# condition's node from its operands' nodes, its ternary
|
|
48
|
+
# arms built in order after its predicate, as each arm's
|
|
49
|
+
# cells follow from the predicate's.
|
|
50
|
+
def self.arms(source, condition, operands, outcomes, leaves)
|
|
51
|
+
predicate = operands.first
|
|
52
|
+
operands = operands.map do |operand|
|
|
53
|
+
next operand unless operand.is_a?(Arm)
|
|
54
|
+
|
|
55
|
+
decided = [predicate[:cells], THEN_ON[condition.operator]]
|
|
56
|
+
arm(source, operand, decided, outcomes, leaves)
|
|
57
|
+
end
|
|
58
|
+
cells = value(condition.operator, operands)
|
|
59
|
+
{ operator: condition.operator, cells:, operands: }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# A case's node from its arms, built in order so its whens'
|
|
63
|
+
# patterns pair with path entries left to right: a when is
|
|
64
|
+
# taken on each path one of its patterns matched, and the
|
|
65
|
+
# else on each none did. A case's value is that of no
|
|
66
|
+
# choice, so is nil.
|
|
67
|
+
def self.case_arms(source, arms, outcomes, leaves)
|
|
68
|
+
matched = []
|
|
69
|
+
operands = arms.map do |arm|
|
|
70
|
+
body = arm.body && node(source, arm.body, outcomes, leaves)
|
|
71
|
+
matched << body[:cells].map { |cell| cell == true } if body
|
|
72
|
+
cells = body ? matched.last : unmatched(matched, outcomes)
|
|
73
|
+
{ arm: arm.kind, text: text_of(source, arm), cells:, body: }
|
|
74
|
+
end
|
|
75
|
+
{ operator: 'case', cells: Array.new(outcomes.size), operands: }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# An arm's text, nil for one with no statements.
|
|
79
|
+
def self.text_of(source, arm)
|
|
80
|
+
arm.extent && source.byteslice(arm.extent)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Whether each column matched none of its whens' patterns.
|
|
84
|
+
def self.unmatched(matched, outcomes)
|
|
85
|
+
outcomes.each_index.map do |column|
|
|
86
|
+
matched.none? { |taken| taken[column] }
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# One arm of a ternary, an if or an unless: its text, nil
|
|
91
|
+
# for an arm with no statements, whether each column took
|
|
92
|
+
# it, from the predicate's cells and the outcome on which
|
|
93
|
+
# its choice takes its then arm, and the tree of its
|
|
94
|
+
# condition.
|
|
95
|
+
def self.arm(source, arm, (decided, then_on), outcomes, leaves)
|
|
96
|
+
picked = arm.kind == :then ? then_on : !then_on
|
|
97
|
+
body = arm.body && node(source, arm.body, outcomes, leaves)
|
|
98
|
+
{
|
|
99
|
+
arm: arm.kind, text: text_of(source, arm),
|
|
100
|
+
cells: decided.map { |outcome| outcome == picked }, body:
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# A condition's value in each column: a ternary's or an
|
|
105
|
+
# if's is its taken arm's body's, nil for a plain arm; a
|
|
106
|
+
# chain's is that of the last operand it reached.
|
|
107
|
+
def self.value(operator, operands)
|
|
108
|
+
if CHOICES.include?(operator)
|
|
109
|
+
_, *arms = operands
|
|
110
|
+
arms.map { |arm| arm[:body]&.fetch(:cells) }
|
|
111
|
+
.then { |bodies| ternary_value(arms, bodies) }
|
|
112
|
+
else
|
|
113
|
+
operands.map { |operand| operand[:cells] }
|
|
114
|
+
.transpose.map { |column| column.compact.last }
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Each column's value from the arm it took.
|
|
119
|
+
def self.ternary_value(arms, bodies)
|
|
120
|
+
arms.first[:cells].each_index.map do |column|
|
|
121
|
+
taken = arms.index { |arm| arm[:cells][column] }
|
|
122
|
+
taken && bodies[taken]&.fetch(column)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# One leaf's text and its outcome in each column.
|
|
127
|
+
def self.leaf(source, range, outcomes, index)
|
|
128
|
+
{
|
|
129
|
+
text: source.byteslice(range),
|
|
130
|
+
cells: outcomes.map { |path| path[index] }
|
|
131
|
+
}
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
require_relative 'gaps'
|
|
2
|
+
|
|
3
|
+
module PathCoverage
|
|
4
|
+
# The lines of one file's page, each with the status of the
|
|
5
|
+
# conditions on it.
|
|
6
|
+
module SourceLines
|
|
7
|
+
# [{number:, text:, status:, span:}] for each line of
|
|
8
|
+
# source. A line in a site's span shares one button with
|
|
9
|
+
# the span's other lines; status and span are nil on a line
|
|
10
|
+
# with no condition.
|
|
11
|
+
def self.of(source, sites, operands)
|
|
12
|
+
marks = marks(source, sites, operands)
|
|
13
|
+
source.lines.each_with_index.map do |text, index|
|
|
14
|
+
number = index + 1
|
|
15
|
+
mark = marks.fetch(number, {})
|
|
16
|
+
{
|
|
17
|
+
number:, text: text.chomp,
|
|
18
|
+
status: mark[:status], span: mark[:span]
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# { line number => {status:, span:} } for each line a site
|
|
24
|
+
# covers, every line of a merged span sharing that span.
|
|
25
|
+
def self.marks(source, sites, operands)
|
|
26
|
+
blocks = merged(spans_of(source, sites, operands))
|
|
27
|
+
blocks.each_with_object({}) do |(span, status), marks|
|
|
28
|
+
span.each { |number| marks[number] = { status:, span: } }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# [span, status] for each site: from the line it starts on
|
|
33
|
+
# to the line its last operand ends on.
|
|
34
|
+
def self.spans_of(source, sites, operands)
|
|
35
|
+
sites.map do |site, condition|
|
|
36
|
+
last = condition.leaves.last
|
|
37
|
+
span = line_of(site)..end_line(source, last)
|
|
38
|
+
[span, status_of(operands.fetch(site))]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The spans merged wherever they overlap, in line order. A
|
|
43
|
+
# merged span has :gaps if any of its sites has, so a
|
|
44
|
+
# covered site never hides another's gaps.
|
|
45
|
+
def self.merged(spans)
|
|
46
|
+
sorted = spans.sort_by { |span, _| span.begin }
|
|
47
|
+
sorted.each_with_object([]) do |(span, status), blocks|
|
|
48
|
+
last_span, last_status = blocks.last
|
|
49
|
+
if last_span && last_span.end >= span.begin
|
|
50
|
+
joined = last_span.begin..[last_span.end, span.end].max
|
|
51
|
+
blocks[-1] = [joined, worse(last_status, status)]
|
|
52
|
+
else
|
|
53
|
+
blocks << [span, status]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# :gaps if either status is :gaps, else :covered.
|
|
59
|
+
def self.worse(one, other)
|
|
60
|
+
[one, other].include?(:gaps) ? :gaps : :covered
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# :covered when no operand has a gap, else :gaps.
|
|
64
|
+
def self.status_of(counts)
|
|
65
|
+
gaps = counts.flat_map { |operand| Gaps.operand_gap(operand) }
|
|
66
|
+
gaps.empty? ? :covered : :gaps
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The line a site starts on, from its path:line:column name.
|
|
70
|
+
def self.line_of(site)
|
|
71
|
+
site[/:(\d+):\d+\z/, 1].to_i
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The line of source on which byte range ends.
|
|
75
|
+
def self.end_line(source, range)
|
|
76
|
+
source.byteslice(0, range.end).count("\n") + 1
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
require_relative 'totals'
|
|
2
|
+
|
|
3
|
+
module PathCoverage
|
|
4
|
+
# The per-file numbers the HTML index page shows, and their
|
|
5
|
+
# total over every file.
|
|
6
|
+
module Summary
|
|
7
|
+
# One row per file that has sites in site_paths, as
|
|
8
|
+
# Result#paths gives them, in path order: its sites, and
|
|
9
|
+
# its decisions total. Its methods total is nil unless
|
|
10
|
+
# methods, as PathManifest.methods gives them, and paths, as
|
|
11
|
+
# the recorder counts them, are given, as they are when
|
|
12
|
+
# methods are measured.
|
|
13
|
+
def self.files(site_paths, methods = {}, paths = nil)
|
|
14
|
+
by_path = site_paths.group_by { |site, _| path_of(site) }
|
|
15
|
+
by_path.sort.map do |path, sites|
|
|
16
|
+
{
|
|
17
|
+
path:, sites: sites.size,
|
|
18
|
+
decisions: Totals.decisions(sites.to_h),
|
|
19
|
+
methods: paths && methods_of(path, methods, paths)
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The methods total of the file at path; [0, 0] for a file
|
|
25
|
+
# with no method of two or more decisions.
|
|
26
|
+
def self.methods_of(path, methods, paths)
|
|
27
|
+
own = methods.select { |site, _| path_of(site) == path }
|
|
28
|
+
Totals.methods(own, paths)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The sum of each column over rows, a methods total being
|
|
32
|
+
# nil when the rows' are.
|
|
33
|
+
def self.total(rows)
|
|
34
|
+
methods = rows.map { |row| row[:methods] }
|
|
35
|
+
{
|
|
36
|
+
sites: rows.sum { |row| row[:sites] },
|
|
37
|
+
decisions: add(rows.map { |row| row[:decisions] }),
|
|
38
|
+
methods: methods.first && add(methods)
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# [taken, possible] pairs added together.
|
|
43
|
+
def self.add(pairs)
|
|
44
|
+
pairs.transpose.map(&:sum)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The file a site is in: its name minus :line:column.
|
|
48
|
+
def self.path_of(site)
|
|
49
|
+
site.sub(/:\d+:\d+\z/, '')
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require_relative 'path_gaps'
|
|
2
|
+
|
|
3
|
+
module PathCoverage
|
|
4
|
+
# The two coverage totals, each [taken, possible] paths over
|
|
5
|
+
# every covered file. They count the very paths the gap
|
|
6
|
+
# lines list as never taken.
|
|
7
|
+
module Totals
|
|
8
|
+
# Paths within each decision: site_paths, as Result#paths
|
|
9
|
+
# gives them, lists every possible path of each site with
|
|
10
|
+
# its count; one with a count above zero was taken.
|
|
11
|
+
def self.decisions(site_paths)
|
|
12
|
+
every = site_paths.values.flatten
|
|
13
|
+
[every.count { |each| each[:count].positive? }, every.size]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Paths across each method's decisions: methods, as
|
|
17
|
+
# PathManifest.methods gives them, each with every path
|
|
18
|
+
# through its decisions; paths, as the recorder counts
|
|
19
|
+
# them, holds the ones taken.
|
|
20
|
+
def self.methods(methods, paths)
|
|
21
|
+
methods.reduce([0, 0]) do |(taken, possible), (site, method)|
|
|
22
|
+
every = PathGaps.possible(method.decisions)
|
|
23
|
+
seen = paths.fetch(site, {})
|
|
24
|
+
[taken + every.count { |path| seen.key?(path) },
|
|
25
|
+
possible + every.size]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/path_coverage.rb
CHANGED
metadata
CHANGED
|
@@ -1,29 +1,88 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: path_coverage
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0
|
|
4
|
+
version: 0.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jon Jagger
|
|
8
|
-
autorequire:
|
|
9
|
-
bindir:
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-11-
|
|
12
|
-
dependencies:
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
date: 2026-11-03 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: prism
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.0'
|
|
27
|
+
description: Records which paths each condition and each method took, and reports
|
|
28
|
+
the paths never taken in the terminal and as HTML.
|
|
29
|
+
email:
|
|
30
|
+
executables:
|
|
31
|
+
- path_coverage_collector
|
|
17
32
|
extensions: []
|
|
18
33
|
extra_rdoc_files: []
|
|
19
34
|
files:
|
|
35
|
+
- LICENSE.md
|
|
36
|
+
- README.md
|
|
37
|
+
- exe/path_coverage_collector
|
|
20
38
|
- lib/path_coverage.rb
|
|
21
|
-
|
|
39
|
+
- lib/path_coverage/auto.rb
|
|
40
|
+
- lib/path_coverage/calls.rb
|
|
41
|
+
- lib/path_coverage/collector.rb
|
|
42
|
+
- lib/path_coverage/condition.rb
|
|
43
|
+
- lib/path_coverage/configuration.rb
|
|
44
|
+
- lib/path_coverage/gaps.rb
|
|
45
|
+
- lib/path_coverage/html/file.html
|
|
46
|
+
- lib/path_coverage/html/index.html
|
|
47
|
+
- lib/path_coverage/html/side.js
|
|
48
|
+
- lib/path_coverage/html/sort.js
|
|
49
|
+
- lib/path_coverage/html/style.css
|
|
50
|
+
- lib/path_coverage/html/toggle.js
|
|
51
|
+
- lib/path_coverage/html_file.rb
|
|
52
|
+
- lib/path_coverage/html_index.rb
|
|
53
|
+
- lib/path_coverage/html_nav.rb
|
|
54
|
+
- lib/path_coverage/html_paths.rb
|
|
55
|
+
- lib/path_coverage/html_report.rb
|
|
56
|
+
- lib/path_coverage/html_template.rb
|
|
57
|
+
- lib/path_coverage/html_tree.rb
|
|
58
|
+
- lib/path_coverage/json_writer.rb
|
|
59
|
+
- lib/path_coverage/kernel.rb
|
|
60
|
+
- lib/path_coverage/manifest.rb
|
|
61
|
+
- lib/path_coverage/manifest_blocks.rb
|
|
62
|
+
- lib/path_coverage/manifest_case.rb
|
|
63
|
+
- lib/path_coverage/manifest_choices.rb
|
|
64
|
+
- lib/path_coverage/merge.rb
|
|
65
|
+
- lib/path_coverage/path_gaps.rb
|
|
66
|
+
- lib/path_coverage/path_manifest.rb
|
|
67
|
+
- lib/path_coverage/paths_report.rb
|
|
68
|
+
- lib/path_coverage/post.rb
|
|
69
|
+
- lib/path_coverage/recorder.rb
|
|
70
|
+
- lib/path_coverage/render.rb
|
|
71
|
+
- lib/path_coverage/result.rb
|
|
72
|
+
- lib/path_coverage/rewriter.rb
|
|
73
|
+
- lib/path_coverage/site.rb
|
|
74
|
+
- lib/path_coverage/site_paths.rb
|
|
75
|
+
- lib/path_coverage/site_tree.rb
|
|
76
|
+
- lib/path_coverage/source_lines.rb
|
|
77
|
+
- lib/path_coverage/summary.rb
|
|
78
|
+
- lib/path_coverage/totals.rb
|
|
79
|
+
- lib/path_coverage/version.rb
|
|
80
|
+
homepage: https://github.com/cyber-dojo/path_coverage
|
|
22
81
|
licenses:
|
|
23
82
|
- BSD-2-Clause
|
|
24
83
|
metadata:
|
|
25
84
|
rubygems_mfa_required: 'true'
|
|
26
|
-
post_install_message:
|
|
85
|
+
post_install_message:
|
|
27
86
|
rdoc_options: []
|
|
28
87
|
require_paths:
|
|
29
88
|
- lib
|
|
@@ -38,8 +97,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
38
97
|
- !ruby/object:Gem::Version
|
|
39
98
|
version: '0'
|
|
40
99
|
requirements: []
|
|
41
|
-
rubygems_version: 3.
|
|
42
|
-
signing_key:
|
|
100
|
+
rubygems_version: 3.5.22
|
|
101
|
+
signing_key:
|
|
43
102
|
specification_version: 4
|
|
44
|
-
summary: Path coverage for Ruby, alongside SimpleCov
|
|
103
|
+
summary: Path coverage for Ruby, alongside SimpleCov branch coverage
|
|
45
104
|
test_files: []
|