path_coverage 0.1.0 → 0.2.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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +287 -78
  3. data/lib/path_coverage/calls.rb +54 -15
  4. data/lib/path_coverage/condition.rb +25 -2
  5. data/lib/path_coverage/configuration.rb +44 -7
  6. data/lib/path_coverage/configuration_paths.rb +56 -0
  7. data/lib/path_coverage/console_summary.rb +77 -0
  8. data/lib/path_coverage/gaps.rb +25 -9
  9. data/lib/path_coverage/html/index.html +3 -17
  10. data/lib/path_coverage/html/side.js +21 -2
  11. data/lib/path_coverage/html/sort.js +24 -21
  12. data/lib/path_coverage/html/style.css +28 -1
  13. data/lib/path_coverage/html/tabs.js +15 -0
  14. data/lib/path_coverage/html_index.rb +86 -31
  15. data/lib/path_coverage/html_index_head.rb +49 -0
  16. data/lib/path_coverage/html_report.rb +30 -13
  17. data/lib/path_coverage/html_tabs.rb +26 -0
  18. data/lib/path_coverage/html_tree.rb +41 -20
  19. data/lib/path_coverage/json_reader.rb +94 -0
  20. data/lib/path_coverage/json_writer.rb +41 -9
  21. data/lib/path_coverage/kernel.rb +42 -20
  22. data/lib/path_coverage/kinds/base.rb +71 -0
  23. data/lib/path_coverage/kinds/block.rb +136 -0
  24. data/lib/path_coverage/kinds/case.rb +89 -0
  25. data/lib/path_coverage/kinds/chain.rb +45 -0
  26. data/lib/path_coverage/kinds/choice.rb +68 -0
  27. data/lib/path_coverage/kinds/contains.rb +111 -0
  28. data/lib/path_coverage/kinds/safe.rb +151 -0
  29. data/lib/path_coverage/kinds/write.rb +54 -0
  30. data/lib/path_coverage/kinds.rb +37 -0
  31. data/lib/path_coverage/manifest.rb +58 -19
  32. data/lib/path_coverage/manifest_blocks.rb +49 -10
  33. data/lib/path_coverage/manifest_chains.rb +76 -0
  34. data/lib/path_coverage/manifest_choices.rb +30 -17
  35. data/lib/path_coverage/manifest_folding.rb +91 -0
  36. data/lib/path_coverage/manifest_ignoring.rb +35 -0
  37. data/lib/path_coverage/manifest_writes.rb +67 -0
  38. data/lib/path_coverage/merge.rb +19 -3
  39. data/lib/path_coverage/post.rb +8 -8
  40. data/lib/path_coverage/recorder.rb +9 -49
  41. data/lib/path_coverage/recorder_blocks.rb +68 -0
  42. data/lib/path_coverage/recorder_cases.rb +23 -0
  43. data/lib/path_coverage/recorder_chains.rb +40 -0
  44. data/lib/path_coverage/recorder_writes.rb +26 -0
  45. data/lib/path_coverage/render.rb +58 -46
  46. data/lib/path_coverage/result.rb +19 -0
  47. data/lib/path_coverage/rewriter.rb +58 -77
  48. data/lib/path_coverage/rewriter_nested.rb +62 -0
  49. data/lib/path_coverage/site_paths.rb +9 -58
  50. data/lib/path_coverage/site_tree.rb +12 -57
  51. data/lib/path_coverage/summary.rb +32 -4
  52. data/lib/path_coverage/totals.rb +1 -1
  53. data/lib/path_coverage/version.rb +1 -1
  54. metadata +30 -6
@@ -1,78 +1,29 @@
1
1
  require_relative 'condition'
2
+ require_relative 'kinds'
2
3
 
3
4
  module PathCoverage
4
5
  # The paths an evaluation of a site can take, worked out
5
6
  # from its condition alone: the outcome of each leaf, in leaf
6
7
  # order, nil for a leaf that evaluation never reaches.
7
8
  module SitePaths
8
- # Every possible path through site's condition, true
9
- # before false; a stop-early block method's are fixed by its
10
- # shape alone.
9
+ # Every possible path through site's condition, true before
10
+ # false, as its kind gives the ways a site of it can be
11
+ # evaluated.
11
12
  def self.possible(site)
12
- shape = BLOCK_PATHS[site.operator]
13
- return shape if shape
14
-
15
- walk(site, site.fixed).map do |outcomes, _value|
13
+ ways = Kinds.of(site.operator).root_ways(site, site.fixed)
14
+ ways.map do |outcomes, _value|
16
15
  site.leaves.map { |range| outcomes[range] }
17
16
  end
18
17
  end
19
18
 
20
- # The outcome of an operand on which each operator goes on
21
- # to its next operand; any other ends the chain there. A
22
- # when's patterns, and a case's whens, stop at a match.
23
- CONTINUES_ON = {
24
- '&&' => true, 'and' => true, '||' => false, 'or' => false,
25
- 'when' => false, 'case' => false
26
- }.freeze
27
-
28
19
  # [[{ leaf range => outcome }, value], ...] for each way
29
20
  # node can be evaluated, value being the outcome of node as
30
- # a whole; fixed is the site's { leaf range => its one
31
- # outcome }. A case is the chain of its whens' conditions.
21
+ # a whole, as its kind gives them; fixed is the site's
22
+ # { leaf range => its one outcome }.
32
23
  def self.walk(node, fixed)
33
24
  return leaf(node, fixed) unless node.is_a?(Condition)
34
- return ternary(node, fixed) if CHOICES.include?(node.operator)
35
-
36
- if node.operator == 'case'
37
- whens = node.operands.filter_map(&:body)
38
- return chain(node.with(operands: whens), fixed)
39
- end
40
-
41
- chain(node, fixed)
42
- end
43
25
 
44
- # The ways a chain of one operator can be evaluated: each
45
- # way of its first operand, going on to the next while each
46
- # outcome is the one its operator goes on with.
47
- def self.chain(node, fixed)
48
- first, *rest = node.operands
49
- goes_on = CONTINUES_ON.fetch(node.operator)
50
- rest.reduce(walk(first, fixed)) do |ways, operand|
51
- ways.flat_map do |outcomes, value|
52
- next [[outcomes, value]] unless value == goes_on
53
-
54
- walk(operand, fixed).map do |more, after|
55
- [outcomes.merge(more), after]
56
- end
57
- end
58
- end
59
- end
60
-
61
- # The ways a ternary, an if or an unless can be evaluated:
62
- # each way of its predicate, then each way of the condition
63
- # the arm it picks is, if that arm is one; one with no else
64
- # arm picks none. Its value is that of no choice, so is nil.
65
- def self.ternary(node, fixed)
66
- predicate, then_arm, else_arm = node.operands
67
- then_on = THEN_ON[node.operator]
68
- walk(predicate, fixed).flat_map do |outcomes, value|
69
- body = (value == then_on ? then_arm : else_arm)&.body
70
- next [[outcomes, nil]] if body.nil?
71
-
72
- walk(body, fixed).map do |more, _|
73
- [outcomes.merge(more), nil]
74
- end
75
- end
26
+ Kinds.of(node.operator).ways(node, fixed)
76
27
  end
77
28
 
78
29
  # The ways a leaf can be evaluated: true, then false, or
@@ -1,4 +1,5 @@
1
1
  require_relative 'condition'
2
+ require_relative 'kinds'
2
3
 
3
4
  module PathCoverage
4
5
  # The tree shown under a site's line on its file's page: a
@@ -21,27 +22,25 @@ module PathCoverage
21
22
  tree.merge(counts: columns.map { |taken| taken[:count] })
22
23
  end
23
24
 
24
- # The tree of condition, each leaf taking the next index of
25
- # leaves, so leaves pair with path entries left to right.
25
+ # The tree of condition, as its kind builds it, each leaf
26
+ # taking the next index of leaves, so leaves pair with path
27
+ # entries left to right.
26
28
  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)
29
+ kind = Kinds.of(condition.operator)
30
+ kind.node(source, condition, outcomes, leaves)
33
31
  end
34
32
 
35
33
  # condition's operands' nodes, leaves and nested conditions
36
34
  # built in order, its arms left for arms to build after.
37
35
  def self.operands_of(source, condition, outcomes, leaves)
38
- condition.operands.map do |operand|
36
+ nodes = condition.operands.map do |operand|
39
37
  case operand
40
38
  when Condition then node(source, operand, outcomes, leaves)
41
39
  when Arm then operand
42
40
  else leaf(source, operand, outcomes, leaves.next)
43
41
  end
44
42
  end
43
+ Kinds.of(condition.operator).texted(source, condition, nodes)
45
44
  end
46
45
 
47
46
  # condition's node from its operands' nodes, its ternary
@@ -55,36 +54,14 @@ module PathCoverage
55
54
  decided = [predicate[:cells], THEN_ON[condition.operator]]
56
55
  arm(source, operand, decided, outcomes, leaves)
57
56
  end
58
- cells = value(condition.operator, operands)
57
+ operator = condition.operator
58
+ cells = Kinds.of(operator).value(operator, operands)
59
59
  { operator: condition.operator, cells:, operands: }
60
60
  end
61
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
62
  # An arm's text, nil for one with no statements.
79
63
  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
64
+ arm.extent ? source.byteslice(arm.extent) : nil
88
65
  end
89
66
 
90
67
  # One arm of a ternary, an if or an unless: its text, nil
@@ -94,35 +71,13 @@ module PathCoverage
94
71
  # condition.
95
72
  def self.arm(source, arm, (decided, then_on), outcomes, leaves)
96
73
  picked = arm.kind == :then ? then_on : !then_on
97
- body = arm.body && node(source, arm.body, outcomes, leaves)
74
+ body = arm.body ? node(source, arm.body, outcomes, leaves) : nil
98
75
  {
99
76
  arm: arm.kind, text: text_of(source, arm),
100
77
  cells: decided.map { |outcome| outcome == picked }, body:
101
78
  }
102
79
  end
103
80
 
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
81
  # One leaf's text and its outcome in each column.
127
82
  def self.leaf(source, range, outcomes, index)
128
83
  {
@@ -1,3 +1,4 @@
1
+ require_relative 'configuration'
1
2
  require_relative 'totals'
2
3
 
3
4
  module PathCoverage
@@ -16,7 +17,7 @@ module PathCoverage
16
17
  {
17
18
  path:, sites: sites.size,
18
19
  decisions: Totals.decisions(sites.to_h),
19
- methods: paths && methods_of(path, methods, paths)
20
+ methods: paths ? methods_of(path, methods, paths) : nil
20
21
  }
21
22
  end
22
23
  end
@@ -35,13 +36,40 @@ module PathCoverage
35
36
  {
36
37
  sites: rows.sum { |row| row[:sites] },
37
38
  decisions: add(rows.map { |row| row[:decisions] }),
38
- methods: methods.first && add(methods)
39
+ methods: methods.first ? add(methods) : nil
39
40
  }
40
41
  end
41
42
 
42
- # [taken, possible] pairs added together.
43
+ # [name, rows, total] for each of groups, { name => [glob,
44
+ # ...] }, in order: the rows whose path matches any of its
45
+ # globs, and their total. Then Ungrouped, the rows no group
46
+ # matches, when there are any.
47
+ def self.groups(rows, groups)
48
+ grouped = groups.map do |name, globs|
49
+ own = rows.select { |row| matches?(row[:path], globs) }
50
+ [name, own, total(own)]
51
+ end
52
+ every = groups.values.flatten
53
+ rest = rows.reject { |row| matches?(row[:path], every) }
54
+ return grouped if rest.empty?
55
+
56
+ grouped << ['Ungrouped', rest, total(rest)]
57
+ end
58
+
59
+ # True when path matches any of globs.
60
+ def self.matches?(path, globs)
61
+ globs.any? do |glob|
62
+ File.fnmatch?(glob, path, Configuration::GLOB_FLAGS)
63
+ end
64
+ end
65
+
66
+ # [taken, possible] pairs added together, eg [[1, 3], [2, 4]]
67
+ # gives [3, 7]; no pairs, as for a group no file is in, gives
68
+ # [0, 0].
43
69
  def self.add(pairs)
44
- pairs.transpose.map(&:sum)
70
+ pairs.reduce([0, 0]) do |(taken, possible), (more, of)|
71
+ [taken + more, possible + of]
72
+ end
45
73
  end
46
74
 
47
75
  # The file a site is in: its name minus :line:column.
@@ -13,7 +13,7 @@ module PathCoverage
13
13
  [every.count { |each| each[:count].positive? }, every.size]
14
14
  end
15
15
 
16
- # Paths across each method's decisions: methods, as
16
+ # Paths within each method, through its decisions: methods, as
17
17
  # PathManifest.methods gives them, each with every path
18
18
  # through its decisions; paths, as the recorder counts
19
19
  # them, holds the ones taken.
@@ -1,3 +1,3 @@
1
1
  module PathCoverage
2
- VERSION = '0.1.0'.freeze
2
+ VERSION = '0.2.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,11 +1,11 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: path_coverage
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jon Jagger
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
11
  date: 2026-11-03 00:00:00.000000000 Z
@@ -26,7 +26,7 @@ dependencies:
26
26
  version: '1.0'
27
27
  description: Records which paths each condition and each method took, and reports
28
28
  the paths never taken in the terminal and as HTML.
29
- email:
29
+ email:
30
30
  executables:
31
31
  - path_coverage_collector
32
32
  extensions: []
@@ -41,35 +41,59 @@ files:
41
41
  - lib/path_coverage/collector.rb
42
42
  - lib/path_coverage/condition.rb
43
43
  - lib/path_coverage/configuration.rb
44
+ - lib/path_coverage/configuration_paths.rb
45
+ - lib/path_coverage/console_summary.rb
44
46
  - lib/path_coverage/gaps.rb
45
47
  - lib/path_coverage/html/file.html
46
48
  - lib/path_coverage/html/index.html
47
49
  - lib/path_coverage/html/side.js
48
50
  - lib/path_coverage/html/sort.js
49
51
  - lib/path_coverage/html/style.css
52
+ - lib/path_coverage/html/tabs.js
50
53
  - lib/path_coverage/html/toggle.js
51
54
  - lib/path_coverage/html_file.rb
52
55
  - lib/path_coverage/html_index.rb
56
+ - lib/path_coverage/html_index_head.rb
53
57
  - lib/path_coverage/html_nav.rb
54
58
  - lib/path_coverage/html_paths.rb
55
59
  - lib/path_coverage/html_report.rb
60
+ - lib/path_coverage/html_tabs.rb
56
61
  - lib/path_coverage/html_template.rb
57
62
  - lib/path_coverage/html_tree.rb
63
+ - lib/path_coverage/json_reader.rb
58
64
  - lib/path_coverage/json_writer.rb
59
65
  - lib/path_coverage/kernel.rb
66
+ - lib/path_coverage/kinds.rb
67
+ - lib/path_coverage/kinds/base.rb
68
+ - lib/path_coverage/kinds/block.rb
69
+ - lib/path_coverage/kinds/case.rb
70
+ - lib/path_coverage/kinds/chain.rb
71
+ - lib/path_coverage/kinds/choice.rb
72
+ - lib/path_coverage/kinds/contains.rb
73
+ - lib/path_coverage/kinds/safe.rb
74
+ - lib/path_coverage/kinds/write.rb
60
75
  - lib/path_coverage/manifest.rb
61
76
  - lib/path_coverage/manifest_blocks.rb
62
77
  - lib/path_coverage/manifest_case.rb
78
+ - lib/path_coverage/manifest_chains.rb
63
79
  - lib/path_coverage/manifest_choices.rb
80
+ - lib/path_coverage/manifest_folding.rb
81
+ - lib/path_coverage/manifest_ignoring.rb
82
+ - lib/path_coverage/manifest_writes.rb
64
83
  - lib/path_coverage/merge.rb
65
84
  - lib/path_coverage/path_gaps.rb
66
85
  - lib/path_coverage/path_manifest.rb
67
86
  - lib/path_coverage/paths_report.rb
68
87
  - lib/path_coverage/post.rb
69
88
  - lib/path_coverage/recorder.rb
89
+ - lib/path_coverage/recorder_blocks.rb
90
+ - lib/path_coverage/recorder_cases.rb
91
+ - lib/path_coverage/recorder_chains.rb
92
+ - lib/path_coverage/recorder_writes.rb
70
93
  - lib/path_coverage/render.rb
71
94
  - lib/path_coverage/result.rb
72
95
  - lib/path_coverage/rewriter.rb
96
+ - lib/path_coverage/rewriter_nested.rb
73
97
  - lib/path_coverage/site.rb
74
98
  - lib/path_coverage/site_paths.rb
75
99
  - lib/path_coverage/site_tree.rb
@@ -82,7 +106,7 @@ licenses:
82
106
  - BSD-2-Clause
83
107
  metadata:
84
108
  rubygems_mfa_required: 'true'
85
- post_install_message:
109
+ post_install_message:
86
110
  rdoc_options: []
87
111
  require_paths:
88
112
  - lib
@@ -97,8 +121,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
97
121
  - !ruby/object:Gem::Version
98
122
  version: '0'
99
123
  requirements: []
100
- rubygems_version: 3.5.22
101
- signing_key:
124
+ rubygems_version: 3.0.3.1
125
+ signing_key:
102
126
  specification_version: 4
103
127
  summary: Path coverage for Ruby, alongside SimpleCov branch coverage
104
128
  test_files: []