condition_coverage 0.0.5 → 0.0.6
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 +44 -2
- data/exe/condition_coverage_collector +26 -0
- data/lib/condition_coverage/auto.rb +5 -0
- data/lib/condition_coverage/collector.rb +46 -0
- data/lib/condition_coverage/configuration.rb +10 -2
- data/lib/condition_coverage/gaps.rb +6 -6
- data/lib/condition_coverage/html_report.rb +10 -8
- data/lib/condition_coverage/json_writer.rb +9 -4
- data/lib/condition_coverage/kernel.rb +34 -23
- data/lib/condition_coverage/manifest.rb +22 -1
- data/lib/condition_coverage/merge.rb +22 -0
- data/lib/condition_coverage/post.rb +24 -0
- data/lib/condition_coverage/render.rb +55 -0
- data/lib/condition_coverage/summary.rb +4 -3
- data/lib/condition_coverage/version.rb +1 -1
- metadata +10 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f113d970620b81b385c0ac7f1ce0e89251574bf8f14498be64b460decb35d315
|
|
4
|
+
data.tar.gz: 5f081efffd90f7d0c216d9e5cfbdb9746e2d1a4dee2384a54ad5b5f8b31cbc1f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 934738b27336db9ff08212d39601f1a8b64c99bcf2a989ff07369334ecdde3bf8d817963c9800ac010351589203e04dc06be14738a3eae759eae7a61d736946d
|
|
7
|
+
data.tar.gz: c6b92d448eeb7b62623de4c8cc6b135b13e1bea82dcb6a62555f590331fd6e53d5992b0998171eeb145ca032ccfbbaf21e6513c1448ac3497a5ef88bd582d04b
|
data/README.md
CHANGED
|
@@ -43,13 +43,47 @@ end
|
|
|
43
43
|
A misspelt setting or an unknown reporter is refused by name, with the list
|
|
44
44
|
of the ones there are. Calling `start` again replaces the configuration.
|
|
45
45
|
|
|
46
|
+
A run whose code cannot be changed, such as a script in a `docker build`
|
|
47
|
+
step, can be covered from its environment alone:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
RUBYOPT=-rcondition_coverage/auto
|
|
51
|
+
CONDITION_COVERAGE_ROOT=/app # root
|
|
52
|
+
CONDITION_COVERAGE_COVER=src/**/*.rb,lib/*.rb # cover, comma-separated
|
|
53
|
+
CONDITION_COVERAGE_DIR=/tmp/condition # coverage_dir
|
|
54
|
+
CONDITION_COVERAGE_COLLECTOR=http://host.docker.internal:4570/runs
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
This starts with `report :post`. With no `CONDITION_COVERAGE_COLLECTOR` it
|
|
58
|
+
starts nothing and prints nothing, so an image can set `RUBYOPT` for every
|
|
59
|
+
run and measure only those given a collector.
|
|
60
|
+
|
|
61
|
+
The gem's `condition_coverage_collector` command is that collector. It
|
|
62
|
+
saves each report POSTed to it as a numbered file, and once every run is
|
|
63
|
+
done they render as one report:
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
condition_coverage_collector 4570 /tmp/condition/runs # until stopped
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
runs = Dir['/tmp/condition/runs/*.json']
|
|
71
|
+
ConditionCoverage::Render.from_json(config, runs)
|
|
72
|
+
```
|
|
73
|
+
|
|
46
74
|
## What a run reports
|
|
47
75
|
|
|
48
76
|
As the process exits:
|
|
49
77
|
|
|
50
78
|
- `condition_coverage.json` is always written to `coverage_dir`: every site
|
|
51
79
|
of every covered file, with each operand's true and false counts. It is
|
|
52
|
-
the base the other reporters read
|
|
80
|
+
the base the other reporters read, so they can also be run later, in
|
|
81
|
+
another process, from it alone:
|
|
82
|
+
`ConditionCoverage::Render.from_json(config)`, where `config` is a
|
|
83
|
+
`ConditionCoverage::Configuration` built from the same settings block.
|
|
84
|
+
Given several runs' reports, `Render.from_json(config, paths)` adds
|
|
85
|
+
their counts together first, so the report shows only the gaps that
|
|
86
|
+
every run left.
|
|
53
87
|
- `:gaps` prints one line per missing outcome, in manifest order:
|
|
54
88
|
|
|
55
89
|
```text
|
|
@@ -79,7 +113,9 @@ An operand whose form allows one outcome only needs only to be evaluated:
|
|
|
79
113
|
its other outcome is impossible, not a gap. `Foo.new`, literals (`'s'`,
|
|
80
114
|
`1`, `:k`, `[]`, `{}`, `true`), lambdas and constants (`LATEST`,
|
|
81
115
|
`Config::DEFAULT`, since one holding nil or false is rare) can only be
|
|
82
|
-
truthy
|
|
116
|
+
truthy, as can `$stdout` and `$stderr`, which Ruby refuses to set to nil
|
|
117
|
+
or false. Other globals, `$stdin` among them, can be either. `nil` and
|
|
118
|
+
`false` can only be falsy:
|
|
83
119
|
|
|
84
120
|
```ruby
|
|
85
121
|
@clock = options[:clock] || MonotonicClock.new # operand 1: never evaluated, or no gap
|
|
@@ -97,6 +133,12 @@ truthy; `nil` and `false` only falsy:
|
|
|
97
133
|
the top of the page lists every site as a link to its line, covered
|
|
98
134
|
sites first, then those with gaps, and arrows beside each site with
|
|
99
135
|
gaps jump to the previous and next one, wrapping round at either end.
|
|
136
|
+
- `:post` sends the JSON report, as an HTTP POST, to the url set by
|
|
137
|
+
`collector 'http://...'`. It is for a run whose own files are thrown
|
|
138
|
+
away, such as a failing `docker build` step. It prints nothing, never
|
|
139
|
+
goes through an `http_proxy`, and gives up in silence on a collector
|
|
140
|
+
that cannot be reached or does not answer within a second, so the
|
|
141
|
+
run's output and exit status are unchanged.
|
|
100
142
|
|
|
101
143
|
## How it works
|
|
102
144
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
require 'socket'
|
|
4
|
+
require 'condition_coverage/collector'
|
|
5
|
+
|
|
6
|
+
if ARGV.first == '-h'
|
|
7
|
+
puts <<~HELP
|
|
8
|
+
Usage: condition_coverage_collector PORT DIR
|
|
9
|
+
|
|
10
|
+
Listens on PORT and saves each JSON report the :post reporter
|
|
11
|
+
sends as DIR/run-0001.json, DIR/run-0002.json, and so on,
|
|
12
|
+
creating DIR if need be. Runs until stopped.
|
|
13
|
+
|
|
14
|
+
Options:
|
|
15
|
+
-h Show this help
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
condition_coverage_collector 4570 /tmp/condition/runs
|
|
19
|
+
HELP
|
|
20
|
+
exit 0
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
port, dir = ARGV
|
|
24
|
+
FileUtils.mkdir_p(dir)
|
|
25
|
+
server = TCPServer.new('0.0.0.0', Integer(port))
|
|
26
|
+
ConditionCoverage::Collector.serve(server, dir)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module ConditionCoverage
|
|
2
|
+
# Receives the JSON reports the :post reporter sends, so the
|
|
3
|
+
# runs that sent them, eg failing docker build steps, can
|
|
4
|
+
# be merged and rendered once they are all done.
|
|
5
|
+
module Collector
|
|
6
|
+
# Answers every request on server, forever, saving each
|
|
7
|
+
# body in dir as run-0001.json, run-0002.json, and so on,
|
|
8
|
+
# in the order they arrive. A connection sending no
|
|
9
|
+
# request, eg a check that the collector is up, is skipped.
|
|
10
|
+
def self.serve(server, dir)
|
|
11
|
+
n = 0
|
|
12
|
+
loop do
|
|
13
|
+
body = request_body(server.accept)
|
|
14
|
+
next if body.nil?
|
|
15
|
+
|
|
16
|
+
n += 1
|
|
17
|
+
File.write(format('%<dir>s/run-%<n>04d.json', dir:, n:), body)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# The body of the one HTTP request on client, after
|
|
22
|
+
# answering it 200; nil, unanswered, when client closes
|
|
23
|
+
# without sending a request.
|
|
24
|
+
def self.request_body(client)
|
|
25
|
+
headers = []
|
|
26
|
+
while (line = client.gets) && line != "\r\n"
|
|
27
|
+
headers << line
|
|
28
|
+
end
|
|
29
|
+
body = headers.empty? ? nil : answer(client, headers)
|
|
30
|
+
client.close
|
|
31
|
+
body
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The body of a request with headers, read from client,
|
|
35
|
+
# which is then answered 200.
|
|
36
|
+
def self.answer(client, headers)
|
|
37
|
+
length = headers.sum do |line|
|
|
38
|
+
name, value = line.split(':', 2)
|
|
39
|
+
name.casecmp?('content-length') ? value.to_i : 0
|
|
40
|
+
end
|
|
41
|
+
body = client.read(length)
|
|
42
|
+
client.write("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")
|
|
43
|
+
body
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -5,8 +5,8 @@ module ConditionCoverage
|
|
|
5
5
|
# The settings given in a ConditionCoverage.start block.
|
|
6
6
|
class Configuration
|
|
7
7
|
GLOB_FLAGS = File::FNM_PATHNAME | File::FNM_EXTGLOB
|
|
8
|
-
SETTINGS = %w[cover coverage_dir report root].freeze
|
|
9
|
-
REPORTERS = %w[gaps html].freeze
|
|
8
|
+
SETTINGS = %w[collector cover coverage_dir report root].freeze
|
|
9
|
+
REPORTERS = %w[gaps html post].freeze
|
|
10
10
|
|
|
11
11
|
# Evaluates the settings block against a new configuration.
|
|
12
12
|
def initialize(&)
|
|
@@ -23,6 +23,14 @@ module ConditionCoverage
|
|
|
23
23
|
@coverage_dir
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
# The url the :post reporter sends the JSON report to.
|
|
27
|
+
attr_reader :collector_url
|
|
28
|
+
|
|
29
|
+
# Sets the url the :post reporter sends the JSON report to.
|
|
30
|
+
def collector(url)
|
|
31
|
+
@collector_url = url
|
|
32
|
+
end
|
|
33
|
+
|
|
26
34
|
# Sets the dir that cover globs and site names are
|
|
27
35
|
# relative to.
|
|
28
36
|
def root(dir)
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
module ConditionCoverage
|
|
2
2
|
# The terminal worklist: one line per missing outcome, and
|
|
3
3
|
# nothing for a site seen both ways. Every count stays in
|
|
4
|
-
# the
|
|
4
|
+
# the operands for reporters that show them all.
|
|
5
5
|
module Gaps
|
|
6
|
-
# The gap lines of
|
|
7
|
-
# false", in manifest order.
|
|
8
|
-
def self.lines(
|
|
9
|
-
|
|
10
|
-
site_gaps(site,
|
|
6
|
+
# The gap lines of operands, as Result#operands gives them,
|
|
7
|
+
# eg "k.rb:2:2 operand 0 never false", in manifest order.
|
|
8
|
+
def self.lines(operands)
|
|
9
|
+
operands.flat_map do |site, counts|
|
|
10
|
+
site_gaps(site, counts)
|
|
11
11
|
end
|
|
12
12
|
end
|
|
13
13
|
|
|
@@ -10,23 +10,25 @@ module ConditionCoverage
|
|
|
10
10
|
# each covered file, written to the configuration's
|
|
11
11
|
# output_dir.
|
|
12
12
|
module HtmlReport
|
|
13
|
-
# Writes every page.
|
|
14
|
-
#
|
|
15
|
-
|
|
13
|
+
# Writes every page. operands are as Result#operands gives
|
|
14
|
+
# them; files is { full path => {source:, sites:} } for each
|
|
15
|
+
# covered file, as the kernel parsed it.
|
|
16
|
+
def self.write(operands, files, config)
|
|
16
17
|
dir = config.output_dir
|
|
17
|
-
index = HtmlIndex.page(Summary.files(
|
|
18
|
+
index = HtmlIndex.page(Summary.files(operands))
|
|
18
19
|
File.write("#{dir}/index.html", index)
|
|
19
20
|
files.each do |full_path, parsed|
|
|
20
21
|
path = config.relative(full_path)
|
|
21
|
-
|
|
22
|
+
out = "#{dir}/files/#{path}.html"
|
|
23
|
+
write_page(out, path, parsed, operands)
|
|
22
24
|
end
|
|
23
25
|
end
|
|
24
26
|
|
|
25
27
|
# Writes one file's page to out.
|
|
26
|
-
def self.write_page(out, path, parsed,
|
|
28
|
+
def self.write_page(out, path, parsed, operands)
|
|
27
29
|
source = parsed[:source]
|
|
28
|
-
lines = SourceLines.of(source, parsed[:sites],
|
|
29
|
-
trees = trees(source, parsed[:sites], lines,
|
|
30
|
+
lines = SourceLines.of(source, parsed[:sites], operands)
|
|
31
|
+
trees = trees(source, parsed[:sites], lines, operands)
|
|
30
32
|
FileUtils.mkdir_p(File.dirname(out))
|
|
31
33
|
File.write(out, HtmlFile.page(path, lines, trees))
|
|
32
34
|
end
|
|
@@ -4,10 +4,15 @@ module ConditionCoverage
|
|
|
4
4
|
# The base reporter: the result as raw JSON, which every
|
|
5
5
|
# other reporter can read back.
|
|
6
6
|
module JsonWriter
|
|
7
|
-
# Writes
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
File.write(path, json)
|
|
7
|
+
# Writes operands, as Result#operands gives them, to path as
|
|
8
|
+
# {"sites": {site: [counts, ...]}}.
|
|
9
|
+
def self.write(operands, path)
|
|
10
|
+
File.write(path, json(operands))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# operands as the JSON report's text.
|
|
14
|
+
def self.json(operands)
|
|
15
|
+
JSON.pretty_generate('sites' => operands)
|
|
11
16
|
end
|
|
12
17
|
end
|
|
13
18
|
end
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
require 'fileutils'
|
|
2
2
|
require_relative 'configuration'
|
|
3
|
-
require_relative 'gaps'
|
|
4
|
-
require_relative 'html_report'
|
|
5
3
|
require_relative 'json_writer'
|
|
6
4
|
require_relative 'manifest'
|
|
7
5
|
require_relative 'recorder'
|
|
6
|
+
require_relative 'render'
|
|
8
7
|
require_relative 'result'
|
|
9
8
|
require_relative 'rewriter'
|
|
10
9
|
|
|
@@ -47,6 +46,27 @@ module ConditionCoverage
|
|
|
47
46
|
on_exit.call { report(config) }
|
|
48
47
|
end
|
|
49
48
|
|
|
49
|
+
# Starts from settings in env alone, for a run whose code
|
|
50
|
+
# cannot be changed, eg one started by RUBYOPT=-r: covers
|
|
51
|
+
# the comma-separated CONDITION_COVERAGE_COVER globs under
|
|
52
|
+
# CONDITION_COVERAGE_ROOT, writing the JSON report into
|
|
53
|
+
# CONDITION_COVERAGE_DIR and POSTing it to
|
|
54
|
+
# CONDITION_COVERAGE_COLLECTOR. Without a collector it
|
|
55
|
+
# starts nothing, so RUBYOPT can be set for every run.
|
|
56
|
+
def self.start_from_env(env, on_exit: method(:at_exit))
|
|
57
|
+
return unless env.key?('CONDITION_COVERAGE_COLLECTOR')
|
|
58
|
+
|
|
59
|
+
start(on_exit:) do
|
|
60
|
+
root env.fetch('CONDITION_COVERAGE_ROOT')
|
|
61
|
+
env.fetch('CONDITION_COVERAGE_COVER').split(',').each do |glob|
|
|
62
|
+
cover glob
|
|
63
|
+
end
|
|
64
|
+
coverage_dir env.fetch('CONDITION_COVERAGE_DIR')
|
|
65
|
+
collector env.fetch('CONDITION_COVERAGE_COLLECTOR')
|
|
66
|
+
report :post
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
50
70
|
# Routes every later load through instrumented, under
|
|
51
71
|
# whichever configuration is current. Installed once: a
|
|
52
72
|
# second definition would warn that it was redefined.
|
|
@@ -62,37 +82,28 @@ module ConditionCoverage
|
|
|
62
82
|
def self.instrumented(path, config)
|
|
63
83
|
return unless config.covers?(path)
|
|
64
84
|
|
|
65
|
-
parsed = @parsed[path] = parse(path, config)
|
|
85
|
+
parsed = @parsed[path] = Manifest.parse(path, config)
|
|
66
86
|
rewritten = Rewriter.rewrite(parsed[:source], parsed[:sites])
|
|
67
87
|
RubyVM::InstructionSequence.compile(rewritten, path, path)
|
|
68
88
|
end
|
|
69
89
|
|
|
70
|
-
# The source and sites of the covered file at path.
|
|
71
|
-
def self.parse(path, config)
|
|
72
|
-
source = File.read(path)
|
|
73
|
-
{ source:, sites: Manifest.sites(source, config.relative(path)) }
|
|
74
|
-
end
|
|
75
|
-
|
|
76
90
|
# Writes the JSON report, then runs the chosen reporters, for
|
|
77
91
|
# every covered file whether or not the run loaded it, into
|
|
78
92
|
# coverage_dir, created first if need be.
|
|
79
93
|
def self.report(config)
|
|
80
94
|
files = parsed_files(config)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
JsonWriter.write(result, config.json_path)
|
|
84
|
-
run_reporters(result, files, config)
|
|
95
|
+
operands = collect(files, config)
|
|
96
|
+
Render.run(operands, files, config)
|
|
85
97
|
end
|
|
86
98
|
|
|
87
|
-
#
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
HtmlReport.write(result, files, config)
|
|
99
|
+
# The operands of every covered file, with the counts this
|
|
100
|
+
# run recorded, once written as the JSON report.
|
|
101
|
+
def self.collect(files, config)
|
|
102
|
+
sites = merged_sites(files)
|
|
103
|
+
operands = Result.new(sites, @recorder.counts).operands
|
|
104
|
+
FileUtils.mkdir_p(config.output_dir)
|
|
105
|
+
JsonWriter.write(operands, config.json_path)
|
|
106
|
+
operands
|
|
96
107
|
end
|
|
97
108
|
|
|
98
109
|
# { full path => {source:, sites:} } for every covered file:
|
|
@@ -100,7 +111,7 @@ module ConditionCoverage
|
|
|
100
111
|
# never loaded.
|
|
101
112
|
def self.parsed_files(config)
|
|
102
113
|
config.files.to_h do |path|
|
|
103
|
-
[path, @parsed.fetch(path) { parse(path, config) }]
|
|
114
|
+
[path, @parsed.fetch(path) { Manifest.parse(path, config) }]
|
|
104
115
|
end
|
|
105
116
|
end
|
|
106
117
|
|
|
@@ -13,6 +13,12 @@ module ConditionCoverage
|
|
|
13
13
|
collector.sites
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
# The source and sites of the covered file at full_path.
|
|
17
|
+
def self.parse(full_path, config)
|
|
18
|
+
source = File.read(full_path)
|
|
19
|
+
{ source:, sites: sites(source, config.relative(full_path)) }
|
|
20
|
+
end
|
|
21
|
+
|
|
16
22
|
# Collects each site and its Condition.
|
|
17
23
|
class ConditionSites < Prism::Visitor
|
|
18
24
|
# Starts with no sites found.
|
|
@@ -97,13 +103,22 @@ module ConditionCoverage
|
|
|
97
103
|
].freeze
|
|
98
104
|
FALSY_TYPES = %i[nil_node false_node].freeze
|
|
99
105
|
|
|
106
|
+
# Ruby raises TypeError on assigning nil or false to these.
|
|
107
|
+
TRUTHY_GLOBALS = %i[$stdout $stderr].freeze
|
|
108
|
+
|
|
100
109
|
# The one outcome a leaf can have, eg :truthy for `Clock.new`
|
|
101
110
|
# or a literal, :falsy for `nil`; nil for a leaf that can
|
|
102
111
|
# have either.
|
|
103
112
|
def fixed_outcome(node)
|
|
104
113
|
return :falsy if FALSY_TYPES.include?(node.type)
|
|
105
114
|
|
|
106
|
-
:truthy if
|
|
115
|
+
:truthy if truthy_only?(node)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# True for a leaf that can never be falsy.
|
|
119
|
+
def truthy_only?(node)
|
|
120
|
+
TRUTHY_TYPES.include?(node.type) || new_call?(node) ||
|
|
121
|
+
truthy_global?(node)
|
|
107
122
|
end
|
|
108
123
|
|
|
109
124
|
# True for a call to new, eg `Clock.new`.
|
|
@@ -111,6 +126,12 @@ module ConditionCoverage
|
|
|
111
126
|
node.type == :call_node && node.name == :new
|
|
112
127
|
end
|
|
113
128
|
|
|
129
|
+
# True for $stdout or $stderr, which can never be falsy.
|
|
130
|
+
def truthy_global?(node)
|
|
131
|
+
node.type == :global_variable_read_node &&
|
|
132
|
+
TRUTHY_GLOBALS.include?(node.name)
|
|
133
|
+
end
|
|
134
|
+
|
|
114
135
|
# True for a call to a named method with arguments and no
|
|
115
136
|
# parentheses, eg `id.include? 'N'`, but not `id.size == 6`.
|
|
116
137
|
def command?(node)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module ConditionCoverage
|
|
2
|
+
# Combines the operands of several runs of the same source.
|
|
3
|
+
module Merge
|
|
4
|
+
# One operands hash, as Result#operands gives them, whose
|
|
5
|
+
# counts are the sums of every run's.
|
|
6
|
+
def self.operands(runs)
|
|
7
|
+
runs.reduce do |total, run|
|
|
8
|
+
total.merge(run) do |_site, mine, theirs|
|
|
9
|
+
mine.zip(theirs).map { |a, b| sum(a, b) }
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# One operand's counts from two runs, added; a fixed leaf's
|
|
15
|
+
# only comes from the source, so either run's will do.
|
|
16
|
+
def self.sum(mine, theirs)
|
|
17
|
+
mine.merge(theirs) do |key, a, b|
|
|
18
|
+
key == :only ? a : a + b
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
require 'net/http'
|
|
2
|
+
require_relative 'json_writer'
|
|
3
|
+
|
|
4
|
+
module ConditionCoverage
|
|
5
|
+
# The :post reporter: sends the JSON report to a collector,
|
|
6
|
+
# for runs whose own files are thrown away, eg a failing
|
|
7
|
+
# docker build step.
|
|
8
|
+
module Post
|
|
9
|
+
# POSTs operands, as the JSON report, to url, never through
|
|
10
|
+
# an http_proxy, which could not reach a collector local to
|
|
11
|
+
# the run. A collector that cannot be reached, or does not
|
|
12
|
+
# answer within a second, is passed over in silence: the
|
|
13
|
+
# run's own output and exit status must stay as they are.
|
|
14
|
+
def self.deliver(operands, url)
|
|
15
|
+
uri = URI(url)
|
|
16
|
+
http = Net::HTTP.new(uri.host, uri.port, nil)
|
|
17
|
+
http.open_timeout = http.read_timeout = 1
|
|
18
|
+
headers = { 'Content-Type' => 'application/json' }
|
|
19
|
+
http.post(uri.path, JsonWriter.json(operands), headers)
|
|
20
|
+
rescue StandardError
|
|
21
|
+
nil
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require_relative 'gaps'
|
|
3
|
+
require_relative 'html_report'
|
|
4
|
+
require_relative 'manifest'
|
|
5
|
+
require_relative 'merge'
|
|
6
|
+
require_relative 'post'
|
|
7
|
+
|
|
8
|
+
module ConditionCoverage
|
|
9
|
+
# The reporters, run from operands alone: needing nothing the
|
|
10
|
+
# run recorded beyond them, they can run in another process.
|
|
11
|
+
module Render
|
|
12
|
+
# Runs each reporter chosen in config. operands are as
|
|
13
|
+
# Result#operands gives them; files is { full path =>
|
|
14
|
+
# {source:, sites:} } for each covered file.
|
|
15
|
+
def self.run(operands, files, config)
|
|
16
|
+
reporters = config.reporters
|
|
17
|
+
if reporters.include?(:gaps)
|
|
18
|
+
Gaps.lines(operands).each { |line| puts line }
|
|
19
|
+
end
|
|
20
|
+
if reporters.include?(:html)
|
|
21
|
+
HtmlReport.write(operands, files, config)
|
|
22
|
+
end
|
|
23
|
+
return unless reporters.include?(:post)
|
|
24
|
+
|
|
25
|
+
Post.deliver(operands, config.collector_url)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Runs the reporters chosen in config from the JSON reports
|
|
29
|
+
# at paths, their counts merged, parsing each covered file
|
|
30
|
+
# afresh. paths is config.json_path alone by default.
|
|
31
|
+
def self.from_json(config, paths = [config.json_path])
|
|
32
|
+
operands = Merge.operands(paths.map { |path| read(path) })
|
|
33
|
+
files = config.files.to_h do |path|
|
|
34
|
+
[path, Manifest.parse(path, config)]
|
|
35
|
+
end
|
|
36
|
+
run(operands, files, config)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The operands of the JSON report at path.
|
|
40
|
+
def self.read(path)
|
|
41
|
+
json = JSON.parse(File.read(path))
|
|
42
|
+
json['sites'].transform_values do |leaves|
|
|
43
|
+
leaves.map { |leaf| counts(leaf) }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# One operand's counts as JSON holds them, with the keys,
|
|
48
|
+
# and any only: value, as the symbols Result#operands uses.
|
|
49
|
+
def self.counts(leaf)
|
|
50
|
+
counts = { truthy: leaf['truthy'], falsy: leaf['falsy'] }
|
|
51
|
+
only = leaf['only']
|
|
52
|
+
only ? counts.merge(only: only.to_sym) : counts
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
module ConditionCoverage
|
|
2
2
|
# The per-file numbers the HTML index page shows.
|
|
3
3
|
module Summary
|
|
4
|
-
# One row per file that has sites in
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
# One row per file that has sites in operands, as
|
|
5
|
+
# Result#operands gives them, in path order.
|
|
6
|
+
def self.files(operands)
|
|
7
|
+
by_path = operands.group_by { |site, _| path_of(site) }
|
|
7
8
|
by_path.sort.map { |path, sites| row(path, sites) }
|
|
8
9
|
end
|
|
9
10
|
|
metadata
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
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.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jon Jagger
|
|
8
|
-
bindir:
|
|
8
|
+
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
@@ -25,13 +25,17 @@ dependencies:
|
|
|
25
25
|
version: '1.0'
|
|
26
26
|
description: Records whether each operand of every &&, ||, and and or condition was
|
|
27
27
|
seen both true and false, and reports the gaps in the terminal and as HTML.
|
|
28
|
-
executables:
|
|
28
|
+
executables:
|
|
29
|
+
- condition_coverage_collector
|
|
29
30
|
extensions: []
|
|
30
31
|
extra_rdoc_files: []
|
|
31
32
|
files:
|
|
32
33
|
- LICENSE.md
|
|
33
34
|
- README.md
|
|
35
|
+
- exe/condition_coverage_collector
|
|
34
36
|
- lib/condition_coverage.rb
|
|
37
|
+
- lib/condition_coverage/auto.rb
|
|
38
|
+
- lib/condition_coverage/collector.rb
|
|
35
39
|
- lib/condition_coverage/condition.rb
|
|
36
40
|
- lib/condition_coverage/configuration.rb
|
|
37
41
|
- lib/condition_coverage/gaps.rb
|
|
@@ -49,7 +53,10 @@ files:
|
|
|
49
53
|
- lib/condition_coverage/json_writer.rb
|
|
50
54
|
- lib/condition_coverage/kernel.rb
|
|
51
55
|
- lib/condition_coverage/manifest.rb
|
|
56
|
+
- lib/condition_coverage/merge.rb
|
|
57
|
+
- lib/condition_coverage/post.rb
|
|
52
58
|
- lib/condition_coverage/recorder.rb
|
|
59
|
+
- lib/condition_coverage/render.rb
|
|
53
60
|
- lib/condition_coverage/result.rb
|
|
54
61
|
- lib/condition_coverage/rewriter.rb
|
|
55
62
|
- lib/condition_coverage/site.rb
|