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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e0b1aba7a743838c35d3b4d139b40efebafd48edef44b82ffc48f7c045eb6861
4
- data.tar.gz: '074496c6beaf83b9c6ec438019eb20e7b4f8231230ba94cd5e7a87cc2dc01eb1'
3
+ metadata.gz: eb36ad570a0050db876bba37548a59c929604eb35b88ed4b8ac4a155dde4db6e
4
+ data.tar.gz: 77f2dd1b7da7815a216fac0bee73f251f1c8d2c9490ad416d26125ac1adcfbe8
5
5
  SHA512:
6
- metadata.gz: debb313edef170467b8146405bc25f212b852798c19492b20699a111d255c05015ade7357a90cc9666e9a5642674fa31a0dcea3f930978bd2ef8bad2b37cab04
7
- data.tar.gz: e90aa8e71930f57e5e50b5f70155d7166ace8741f300abb7092bafbb3d47bafaaa3ce409cfe7102fc1795dfe7cea5224961a8497e027a617351b371010d83432
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.3 covers `&&`, `||`, `and` and `or`: each leaf operand of a
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
- Condition = Data.define(:operator, :operands, :commands) do
7
- # A condition with no command-call leaves by default.
8
- def initialize(operator:, operands:, commands: [])
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
- @commands = []
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 = @commands
48
- @commands = []
47
+ outer = @notes
48
+ @notes = { commands: [], fixed: {} }
49
49
  condition = condition_of(node)
50
- @sites[site] = condition.with(commands: @commands)
51
- @commands = outer
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 commands
81
- # if it is a command call.
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
- size = condition.leaves.size
20
- [site, Array.new(size) { |i| counts_of(site, i) }]
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 every operand was seen both ways, else :gaps.
63
+ # :covered when no operand has a gap, else :gaps.
62
64
  def self.status_of(counts)
63
- counts.all? { |operand| seen_both?(operand) } ? :covered : :gaps
64
- end
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.size * 2
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:,
@@ -1,3 +1,3 @@
1
1
  module ConditionCoverage
2
- VERSION = '0.0.3'.freeze
2
+ VERSION = '0.0.4'.freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: condition_coverage
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jon Jagger