condition_coverage 0.0.3 → 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 +10 -1
- data/lib/condition_coverage/condition.rb +6 -4
- data/lib/condition_coverage/gaps.rb +3 -1
- data/lib/condition_coverage/manifest.rb +30 -8
- data/lib/condition_coverage/result.rb +13 -3
- 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 +1 -1
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
|
|
@@ -2,10 +2,12 @@ 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
4
|
# Condition. A site's commands are the byte ranges of its
|
|
5
|
-
# leaves that are command calls, eg `ok? id
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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: {})
|
|
9
11
|
super
|
|
10
12
|
end
|
|
11
13
|
|
|
@@ -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
|
|
|
@@ -19,7 +19,7 @@ module ConditionCoverage
|
|
|
19
19
|
def initialize(path)
|
|
20
20
|
@path = path
|
|
21
21
|
@sites = {}
|
|
22
|
-
@
|
|
22
|
+
@notes = nil
|
|
23
23
|
super()
|
|
24
24
|
end
|
|
25
25
|
|
|
@@ -44,11 +44,11 @@ module ConditionCoverage
|
|
|
44
44
|
def add_site(node)
|
|
45
45
|
site = Site.of(@path, node)
|
|
46
46
|
@sites[site] = nil
|
|
47
|
-
outer = @
|
|
48
|
-
@
|
|
47
|
+
outer = @notes
|
|
48
|
+
@notes = { commands: [], fixed: {} }
|
|
49
49
|
condition = condition_of(node)
|
|
50
|
-
@sites[site] = condition.with(
|
|
51
|
-
@
|
|
50
|
+
@sites[site] = condition.with(**@notes)
|
|
51
|
+
@notes = outer
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
# node as a Condition, operands and all, named by its
|
|
@@ -77,16 +77,38 @@ module ConditionCoverage
|
|
|
77
77
|
end
|
|
78
78
|
|
|
79
79
|
# The byte range of a leaf operand, once any site inside
|
|
80
|
-
# it is noted too, and the range among its site's
|
|
81
|
-
#
|
|
80
|
+
# it is noted too, and the range noted among its site's
|
|
81
|
+
# commands and fixed leaves where it is one.
|
|
82
82
|
def leaf(operand)
|
|
83
83
|
operand.accept(self)
|
|
84
84
|
loc = operand.location
|
|
85
85
|
range = loc.start_offset...loc.end_offset
|
|
86
|
-
@commands << range if command?(operand)
|
|
86
|
+
@notes[:commands] << range if command?(operand)
|
|
87
|
+
outcome = fixed_outcome(operand)
|
|
88
|
+
@notes[:fixed][range] = outcome if outcome
|
|
87
89
|
range
|
|
88
90
|
end
|
|
89
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
|
+
|
|
90
112
|
# True for a call to a named method with arguments and no
|
|
91
113
|
# parentheses, eg `id.include? 'N'`, but not `id.size == 6`.
|
|
92
114
|
def command?(node)
|
|
@@ -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)
|
|
@@ -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:,
|