condition_coverage 0.0.2 → 0.0.4
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/README.md +17 -4
- data/lib/condition_coverage/condition.rb +10 -2
- data/lib/condition_coverage/gaps.rb +3 -1
- data/lib/condition_coverage/kernel.rb +4 -1
- data/lib/condition_coverage/manifest.rb +40 -3
- data/lib/condition_coverage/result.rb +13 -3
- data/lib/condition_coverage/rewriter.rb +10 -7
- data/lib/condition_coverage/source_lines.rb +5 -7
- data/lib/condition_coverage/summary.rb +1 -1
- data/lib/condition_coverage/version.rb +1 -1
- metadata +3 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eb36ad570a0050db876bba37548a59c929604eb35b88ed4b8ac4a155dde4db6e
|
|
4
|
+
data.tar.gz: 77f2dd1b7da7815a216fac0bee73f251f1c8d2c9490ad416d26125ac1adcfbe8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5571a2753478ab9ed18b6a1e04b47df6f4f96b03f10637ac9bf8e0ad4149b8a02d6b6abf754b45e533c0eb37a417fffe9a7a937c3c86a6bf15b51fe13d2bed03
|
|
7
|
+
data.tar.gz: 5d97b90e7c1bff46242a833f234ef1a5ef62e8732bd2a2fe3f9b1f65fe441b0bcad5da1eeaf2ad0912ae7406ce3baf1c77067571b0751e6a9c6f602e831eca62
|
data/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Condition coverage for Ruby, to run alongside SimpleCov's line and branch
|
|
4
4
|
coverage.
|
|
5
5
|
|
|
6
|
-
Status: 0.0.
|
|
6
|
+
Status: 0.0.4 covers `&&`, `||`, `and` and `or`: each leaf operand of a
|
|
7
7
|
condition, however its operators are chained, nested or grouped. Decisions
|
|
8
8
|
hidden inside calls come next (see Roadmap).
|
|
9
9
|
|
|
@@ -75,6 +75,15 @@ A chain of one operator is flat; a change of operator, by precedence or by
|
|
|
75
75
|
parentheses, nests. Parentheses are not part of the tree, since they decide
|
|
76
76
|
nothing.
|
|
77
77
|
|
|
78
|
+
An operand whose form allows one outcome only needs only to be evaluated:
|
|
79
|
+
its other outcome is impossible, not a gap. `Foo.new`, literals (`'s'`,
|
|
80
|
+
`1`, `:k`, `[]`, `{}`, `true`) and lambdas can only be truthy; `nil` and
|
|
81
|
+
`false` only falsy:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
@clock = options[:clock] || MonotonicClock.new # operand 1: never evaluated, or no gap
|
|
85
|
+
```
|
|
86
|
+
|
|
78
87
|
- `:html` writes `index.html` to `coverage_dir`: the covered files with
|
|
79
88
|
their sites, outcomes seen (with a percent) and outcomes missing, each
|
|
80
89
|
column sortable. Each file name opens `files/<path>.html`, the file's
|
|
@@ -96,11 +105,15 @@ the file:
|
|
|
96
105
|
```ruby
|
|
97
106
|
ok?(id) && new?(id)
|
|
98
107
|
# compiles as
|
|
99
|
-
ConditionCoverage.record("k.rb:2:2", 0,
|
|
108
|
+
ConditionCoverage.record("k.rb:2:2", 0, ok?(id)) && ConditionCoverage.record("k.rb:2:2", 1, new?(id))
|
|
100
109
|
```
|
|
101
110
|
|
|
102
|
-
|
|
103
|
-
|
|
111
|
+
A command call such as `ok? id` (after `and`) is not valid as a bare
|
|
112
|
+
argument, so that operand alone gets parentheses of its own. No other operand
|
|
113
|
+
does: parentheses make a statement, and a statement on a continuation line
|
|
114
|
+
gives that line a line-coverage count of its own, which would change
|
|
115
|
+
SimpleCov's line totals. With that one exception, an instrumented file has
|
|
116
|
+
the same relevant lines as an uninstrumented one.
|
|
104
117
|
|
|
105
118
|
`record` counts whether the operand was truthy or falsy and returns it
|
|
106
119
|
unchanged, so short-circuiting and the value of the expression are
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
module ConditionCoverage
|
|
2
2
|
# One site's condition as the manifest found it: its operator
|
|
3
3
|
# and its operands, each the byte range of a leaf or a nested
|
|
4
|
-
# Condition.
|
|
5
|
-
|
|
4
|
+
# Condition. A site's commands are the byte ranges of its
|
|
5
|
+
# leaves that are command calls, eg `ok? id`; its fixed are
|
|
6
|
+
# { byte range => :truthy } for leaves that can have that one
|
|
7
|
+
# outcome only, eg `Clock.new`.
|
|
8
|
+
Condition = Data.define(:operator, :operands, :commands, :fixed) do
|
|
9
|
+
# A condition with no command-call or fixed leaves by default.
|
|
10
|
+
def initialize(operator:, operands:, commands: [], fixed: {})
|
|
11
|
+
super
|
|
12
|
+
end
|
|
13
|
+
|
|
6
14
|
# The byte range of every leaf operand, left to right; a
|
|
7
15
|
# leaf's index here is the index it is recorded under.
|
|
8
16
|
def leaves
|
|
@@ -25,9 +25,11 @@ module ConditionCoverage
|
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
# The one outcome this operand is missing, if any
|
|
28
|
+
# The one outcome this operand is missing, if any; a fixed
|
|
29
|
+
# leaf, which can have one outcome only, needs only to run.
|
|
29
30
|
def self.operand_gap(counts)
|
|
30
31
|
return ['never evaluated'] if never_ran?(counts)
|
|
32
|
+
return [] if counts[:only]
|
|
31
33
|
return ['never true'] if counts[:truthy].zero?
|
|
32
34
|
return ['never false'] if counts[:falsy].zero?
|
|
33
35
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require 'fileutils'
|
|
1
2
|
require_relative 'configuration'
|
|
2
3
|
require_relative 'gaps'
|
|
3
4
|
require_relative 'html_report'
|
|
@@ -73,10 +74,12 @@ module ConditionCoverage
|
|
|
73
74
|
end
|
|
74
75
|
|
|
75
76
|
# Writes the JSON report, then runs the chosen reporters, for
|
|
76
|
-
# every covered file whether or not the run loaded it
|
|
77
|
+
# every covered file whether or not the run loaded it, into
|
|
78
|
+
# coverage_dir, created first if need be.
|
|
77
79
|
def self.report(config)
|
|
78
80
|
files = parsed_files(config)
|
|
79
81
|
result = Result.new(merged_sites(files), @recorder.counts)
|
|
82
|
+
FileUtils.mkdir_p(config.output_dir)
|
|
80
83
|
JsonWriter.write(result, config.json_path)
|
|
81
84
|
run_reporters(result, files, config)
|
|
82
85
|
end
|
|
@@ -19,6 +19,7 @@ module ConditionCoverage
|
|
|
19
19
|
def initialize(path)
|
|
20
20
|
@path = path
|
|
21
21
|
@sites = {}
|
|
22
|
+
@notes = nil
|
|
22
23
|
super()
|
|
23
24
|
end
|
|
24
25
|
|
|
@@ -43,7 +44,11 @@ module ConditionCoverage
|
|
|
43
44
|
def add_site(node)
|
|
44
45
|
site = Site.of(@path, node)
|
|
45
46
|
@sites[site] = nil
|
|
46
|
-
|
|
47
|
+
outer = @notes
|
|
48
|
+
@notes = { commands: [], fixed: {} }
|
|
49
|
+
condition = condition_of(node)
|
|
50
|
+
@sites[site] = condition.with(**@notes)
|
|
51
|
+
@notes = outer
|
|
47
52
|
end
|
|
48
53
|
|
|
49
54
|
# node as a Condition, operands and all, named by its
|
|
@@ -72,11 +77,43 @@ module ConditionCoverage
|
|
|
72
77
|
end
|
|
73
78
|
|
|
74
79
|
# The byte range of a leaf operand, once any site inside
|
|
75
|
-
# it is noted too
|
|
80
|
+
# it is noted too, and the range noted among its site's
|
|
81
|
+
# commands and fixed leaves where it is one.
|
|
76
82
|
def leaf(operand)
|
|
77
83
|
operand.accept(self)
|
|
78
84
|
loc = operand.location
|
|
79
|
-
loc.start_offset...loc.end_offset
|
|
85
|
+
range = loc.start_offset...loc.end_offset
|
|
86
|
+
@notes[:commands] << range if command?(operand)
|
|
87
|
+
outcome = fixed_outcome(operand)
|
|
88
|
+
@notes[:fixed][range] = outcome if outcome
|
|
89
|
+
range
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
TRUTHY_TYPES = %i[
|
|
93
|
+
string_node integer_node symbol_node array_node hash_node
|
|
94
|
+
true_node lambda_node
|
|
95
|
+
].freeze
|
|
96
|
+
FALSY_TYPES = %i[nil_node false_node].freeze
|
|
97
|
+
|
|
98
|
+
# The one outcome a leaf can have, eg :truthy for `Clock.new`
|
|
99
|
+
# or a literal, :falsy for `nil`; nil for a leaf that can
|
|
100
|
+
# have either.
|
|
101
|
+
def fixed_outcome(node)
|
|
102
|
+
return :falsy if FALSY_TYPES.include?(node.type)
|
|
103
|
+
|
|
104
|
+
:truthy if TRUTHY_TYPES.include?(node.type) || new_call?(node)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# True for a call to new, eg `Clock.new`.
|
|
108
|
+
def new_call?(node)
|
|
109
|
+
node.type == :call_node && node.name == :new
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# True for a call to a named method with arguments and no
|
|
113
|
+
# parentheses, eg `id.include? 'N'`, but not `id.size == 6`.
|
|
114
|
+
def command?(node)
|
|
115
|
+
node.type == :call_node && node.arguments &&
|
|
116
|
+
node.opening_loc.nil? && node.name.match?(/\A[a-z_]/i)
|
|
80
117
|
end
|
|
81
118
|
|
|
82
119
|
# The one statement inside a parenthesised operand, or the
|
|
@@ -13,16 +13,26 @@ module ConditionCoverage
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
# { site => [counts of operand 0, counts of operand 1, ...] },
|
|
16
|
-
# with zero counts for an operand that never ran
|
|
16
|
+
# with zero counts for an operand that never ran, and
|
|
17
|
+
# only: naming the one outcome a fixed leaf can have.
|
|
17
18
|
def operands
|
|
18
19
|
@sites.to_h do |site, condition|
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
leaves = condition.leaves.each_with_index.map do |range, i|
|
|
21
|
+
leaf(site, condition, range, i)
|
|
22
|
+
end
|
|
23
|
+
[site, leaves]
|
|
21
24
|
end
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
private
|
|
25
28
|
|
|
29
|
+
# The counts of one leaf, with only: when it is fixed.
|
|
30
|
+
def leaf(site, condition, range, index)
|
|
31
|
+
counts = counts_of(site, index)
|
|
32
|
+
only = condition.fixed[range]
|
|
33
|
+
only ? counts.merge(only:) : counts
|
|
34
|
+
end
|
|
35
|
+
|
|
26
36
|
# The recorded counts of one operand, or zeros.
|
|
27
37
|
def counts_of(site, index)
|
|
28
38
|
@counts.fetch([site, index], NEVER_RAN)
|
|
@@ -9,18 +9,21 @@ module ConditionCoverage
|
|
|
9
9
|
def self.rewrite(source, sites)
|
|
10
10
|
inserts = sites.flat_map do |site, condition|
|
|
11
11
|
condition.leaves.each_with_index.flat_map do |range, index|
|
|
12
|
-
|
|
12
|
+
command = condition.commands.include?(range)
|
|
13
|
+
wrap(site, index, range, command)
|
|
13
14
|
end
|
|
14
15
|
end
|
|
15
16
|
apply(source.dup, inserts)
|
|
16
17
|
end
|
|
17
18
|
|
|
18
|
-
# The two inserts that wrap one operand in a record call.
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
[
|
|
19
|
+
# The two inserts that wrap one operand in a record call. A
|
|
20
|
+
# command call is parenthesised, as it is no bare argument;
|
|
21
|
+
# nothing else is, since parentheses make a statement, which
|
|
22
|
+
# gives its line a line-coverage count of its own.
|
|
23
|
+
def self.wrap(site, index, range, command)
|
|
24
|
+
open, close = command ? ['(', '))'] : ['', ')']
|
|
25
|
+
call = "ConditionCoverage.record(#{site.inspect}, #{index}, "
|
|
26
|
+
[[range.begin, call + open], [range.end, close]]
|
|
24
27
|
end
|
|
25
28
|
|
|
26
29
|
# Splices each [offset, text] insert into source,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require_relative 'gaps'
|
|
2
|
+
|
|
1
3
|
module ConditionCoverage
|
|
2
4
|
# The lines of one file's page, each with the status of the
|
|
3
5
|
# conditions on it.
|
|
@@ -58,14 +60,10 @@ module ConditionCoverage
|
|
|
58
60
|
[one, other].include?(:gaps) ? :gaps : :covered
|
|
59
61
|
end
|
|
60
62
|
|
|
61
|
-
# :covered when
|
|
63
|
+
# :covered when no operand has a gap, else :gaps.
|
|
62
64
|
def self.status_of(counts)
|
|
63
|
-
counts.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
# True when the operand was seen both true and false.
|
|
67
|
-
def self.seen_both?(counts)
|
|
68
|
-
counts[:truthy].positive? && counts[:falsy].positive?
|
|
65
|
+
gaps = counts.flat_map { |operand| Gaps.operand_gap(operand) }
|
|
66
|
+
gaps.empty? ? :covered : :gaps
|
|
69
67
|
end
|
|
70
68
|
|
|
71
69
|
# The line a site starts on, from its path:line:column name.
|
|
@@ -10,7 +10,7 @@ module ConditionCoverage
|
|
|
10
10
|
# The summary row of one file's sites.
|
|
11
11
|
def self.row(path, sites)
|
|
12
12
|
operands = sites.flat_map { |_, counts| counts }
|
|
13
|
-
outcomes = operands.
|
|
13
|
+
outcomes = operands.sum { |counts| counts[:only] ? 1 : 2 }
|
|
14
14
|
seen = operands.sum { |counts| seen_in(counts) }
|
|
15
15
|
{
|
|
16
16
|
path:, sites: sites.size, seen:, outcomes:,
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: condition_coverage
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jon Jagger
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: prism
|
|
@@ -26,7 +25,6 @@ dependencies:
|
|
|
26
25
|
version: '1.0'
|
|
27
26
|
description: Records whether each operand of every &&, ||, and and or condition was
|
|
28
27
|
seen both true and false, and reports the gaps in the terminal and as HTML.
|
|
29
|
-
email:
|
|
30
28
|
executables: []
|
|
31
29
|
extensions: []
|
|
32
30
|
extra_rdoc_files: []
|
|
@@ -62,7 +60,6 @@ licenses:
|
|
|
62
60
|
- BSD-2-Clause
|
|
63
61
|
metadata:
|
|
64
62
|
rubygems_mfa_required: 'true'
|
|
65
|
-
post_install_message:
|
|
66
63
|
rdoc_options: []
|
|
67
64
|
require_paths:
|
|
68
65
|
- lib
|
|
@@ -77,8 +74,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
77
74
|
- !ruby/object:Gem::Version
|
|
78
75
|
version: '0'
|
|
79
76
|
requirements: []
|
|
80
|
-
rubygems_version:
|
|
81
|
-
signing_key:
|
|
77
|
+
rubygems_version: 4.0.23
|
|
82
78
|
specification_version: 4
|
|
83
79
|
summary: Condition coverage for Ruby, alongside SimpleCov branch coverage
|
|
84
80
|
test_files: []
|