deep-cover 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +15 -0
- data/.travis.yml +1 -1
- data/deep_cover.gemspec +3 -0
- data/lib/deep_cover.rb +2 -17
- data/lib/deep_cover/analyser.rb +2 -3
- data/lib/deep_cover/base.rb +3 -7
- data/lib/deep_cover/builtin_takeover.rb +2 -1
- data/lib/deep_cover/cli/deep_cover.rb +61 -62
- data/lib/deep_cover/config.rb +8 -10
- data/lib/deep_cover/core_ext/require_overrides.rb +2 -0
- data/lib/deep_cover/coverage.rb +4 -2
- data/lib/deep_cover/coverage/analysis.rb +4 -0
- data/lib/deep_cover/coverage/base.rb +2 -17
- data/lib/deep_cover/coverage/persistence.rb +2 -1
- data/lib/deep_cover/covered_code.rb +15 -3
- data/lib/deep_cover/load.rb +50 -0
- data/lib/deep_cover/memoize.rb +2 -0
- data/lib/deep_cover/node.rb +3 -1
- data/lib/deep_cover/node/root.rb +1 -1
- data/lib/deep_cover/problem_with_diagnostic.rb +1 -1
- data/lib/deep_cover/reporter.rb +3 -0
- data/lib/deep_cover/reporter/html/index.rb +24 -40
- data/lib/deep_cover/reporter/html/source.rb +2 -2
- data/lib/deep_cover/reporter/text.rb +66 -0
- data/lib/deep_cover/reporter/util/tree.rb +88 -0
- data/lib/deep_cover/tools.rb +2 -1
- data/lib/deep_cover/tools/builtin_coverage.rb +1 -2
- data/lib/deep_cover/tools/camelize.rb +7 -4
- data/lib/deep_cover/version.rb +1 -1
- metadata +19 -3
- data/lib/deep_cover/reporter/html/tree.rb +0 -55
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3a8450fe7655e95139f1443a7e81f3c73b726e03
|
4
|
+
data.tar.gz: b04f9ef3b69060ff9568cae7224bc89a845137bf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 822e717af608dd6b6ed0010ce00f75aff18f4f60cfddc866df90ceefaf6131820a19f8a7a0e2d5eb00d961412dbcac23295e4b3aebb8be9cd1ae7ca7ecf13ab5
|
7
|
+
data.tar.gz: a5849e499b6f4eb7975af2e4529f5564e99a982d0cda525e0d246e6bce698f289e24cadf9a44ba3a85c4e78ae60da41813ef443f3f7114a261a6a856cd6a024e
|
data/.rubocop.yml
CHANGED
@@ -220,3 +220,18 @@ Style/StructInheritance:
|
|
220
220
|
|
221
221
|
Security/YAMLLoad:
|
222
222
|
Enabled: false
|
223
|
+
|
224
|
+
Style/ExtendSelf:
|
225
|
+
Enabled: false
|
226
|
+
|
227
|
+
Style/CommentedKeyword:
|
228
|
+
Enabled: false # See https://github.com/bbatsov/rubocop/issues/5259
|
229
|
+
|
230
|
+
Gemspec/RequiredRubyVersion:
|
231
|
+
Enabled: false # See https://github.com/bbatsov/rubocop/issues/5260
|
232
|
+
|
233
|
+
Style/MixinUsage:
|
234
|
+
Enabled: false # See https://github.com/bbatsov/rubocop/issues/5261
|
235
|
+
|
236
|
+
Style/EvalWithLocation:
|
237
|
+
Enabled: false
|
data/.travis.yml
CHANGED
data/deep_cover.gemspec
CHANGED
data/lib/deep_cover.rb
CHANGED
@@ -1,24 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# rubocop:disable Style/MixinUsage (See https://github.com/bbatsov/rubocop/issues/5055)
|
4
3
|
module DeepCover
|
5
|
-
|
6
|
-
require 'parser'
|
7
|
-
require 'term/ansicolor'
|
8
|
-
require 'pry'
|
4
|
+
require_relative 'deep_cover/load'
|
9
5
|
|
10
|
-
|
11
|
-
require_relative 'deep_cover/backports'
|
12
|
-
require_relative 'deep_cover/tools'
|
13
|
-
|
14
|
-
# Parser
|
15
|
-
silence_warnings do
|
16
|
-
require 'parser/current'
|
17
|
-
end
|
18
|
-
require_relative_dir 'deep_cover/parser_ext'
|
19
|
-
|
20
|
-
# Main
|
21
|
-
require_relative_dir 'deep_cover', except: %w[auto_run builtin_takeover]
|
6
|
+
load_absolute_basics
|
22
7
|
|
23
8
|
extend Base
|
24
9
|
extend Config::Setter
|
data/lib/deep_cover/analyser.rb
CHANGED
@@ -1,9 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative 'node'
|
4
|
-
require_relative 'covered_code'
|
5
|
-
|
6
3
|
module DeepCover
|
4
|
+
bootstrap
|
5
|
+
|
7
6
|
# An analyser works on a subset of the original Node AST.
|
8
7
|
# The Root node is always considered part of the subset.
|
9
8
|
# One can iterate this subset with `each_node`, or ask
|
data/lib/deep_cover/base.rb
CHANGED
@@ -21,6 +21,7 @@ module DeepCover
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def stop
|
24
|
+
require_relative 'core_ext/require_overrides'
|
24
25
|
AutoloadOverride.active = false if defined? AutoloadOverride
|
25
26
|
RequireOverride.active = false
|
26
27
|
@started = false
|
@@ -72,6 +73,8 @@ module DeepCover
|
|
72
73
|
@autoload_tracker ||= AutoloadTracker.new
|
73
74
|
end
|
74
75
|
|
76
|
+
private
|
77
|
+
|
75
78
|
def handle_relative_filename(filename)
|
76
79
|
unless Pathname.new(filename).absolute?
|
77
80
|
relative_to = File.dirname(caller(2..2).first.partition(/\.rb:\d/).first)
|
@@ -80,12 +83,5 @@ module DeepCover
|
|
80
83
|
filename += '.rb' unless filename =~ /\.rb$/
|
81
84
|
filename
|
82
85
|
end
|
83
|
-
|
84
|
-
def parser
|
85
|
-
Parser::CurrentRuby.new.tap do |parser|
|
86
|
-
parser.diagnostics.all_errors_are_fatal = true
|
87
|
-
parser.diagnostics.ignore_warnings = true
|
88
|
-
end
|
89
|
-
end
|
90
86
|
end
|
91
87
|
end
|
@@ -1,7 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative '../deep_cover'
|
4
|
-
require_relative '
|
4
|
+
require_relative 'coverage'
|
5
|
+
require_relative 'core_ext/coverage_replacement'
|
5
6
|
|
6
7
|
require 'coverage'
|
7
8
|
BuiltinCoverage = Coverage
|
@@ -4,84 +4,83 @@ module DeepCover
|
|
4
4
|
require 'bundler/setup'
|
5
5
|
require 'slop'
|
6
6
|
require 'deep_cover'
|
7
|
+
bootstrap
|
7
8
|
require_relative_dir '.'
|
8
9
|
|
9
|
-
module CLI
|
10
|
-
|
11
|
-
extend self
|
10
|
+
module CLI::DeepCover
|
11
|
+
extend self
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
def show_version
|
14
|
+
puts "deep-cover v#{DeepCover::VERSION}; parser v#{Parser::VERSION}"
|
15
|
+
end
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
|
17
|
+
def show_help
|
18
|
+
puts menu
|
19
|
+
end
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
end
|
27
|
-
delegate.public_send(method, *args, &block)
|
21
|
+
class OptionParser < Struct.new(:delegate)
|
22
|
+
def method_missing(method, *args, &block) # rubocop:disable Style/MethodMissing
|
23
|
+
options = args.last
|
24
|
+
if options.is_a?(Hash) && options.has_key?(:default)
|
25
|
+
args[-2] += " [#{options[:default]}]"
|
28
26
|
end
|
27
|
+
delegate.public_send(method, *args, &block)
|
29
28
|
end
|
29
|
+
end
|
30
30
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
end
|
31
|
+
def parse
|
32
|
+
Slop.parse do |o|
|
33
|
+
yield OptionParser.new(o)
|
35
34
|
end
|
35
|
+
end
|
36
36
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
37
|
+
def menu
|
38
|
+
@menu ||= parse do |o|
|
39
|
+
o.banner = 'usage: deep-cover [options] [path/to/app/or/gem]'
|
40
|
+
o.separator ''
|
41
|
+
o.string '-o', '--output', 'output folder', default: './coverage'
|
42
|
+
o.string '-c', '--command', 'command to run tests', default: 'bundle exec rake'
|
43
|
+
o.string '--reporter', 'reporter', default: 'html'
|
44
|
+
o.bool '--bundle', 'run bundle before the tests', default: true
|
45
|
+
o.bool '--process', 'turn off to only redo the reporting', default: true
|
46
|
+
o.bool '--open', 'open the output coverage', default: false
|
47
|
+
o.separator 'Coverage options'
|
48
|
+
@ignore_uncovered_map = Analyser::Node.optionally_covered.map do |option|
|
49
|
+
default = Config::DEFAULTS[:ignore_uncovered].include?(option)
|
50
|
+
o.bool "--ignore-#{Tools.dasherize(option)}", '', default: default
|
51
|
+
[:"ignore_#{option}", option]
|
52
|
+
end.to_h
|
53
|
+
o.separator "\nFor testing purposes:"
|
54
|
+
o.bool '--profile', 'use profiler' unless RUBY_PLATFORM == 'java'
|
55
|
+
o.string '-e', '--expression', 'test ruby expression instead of a covering a path'
|
56
|
+
o.bool '-d', '--debug', 'enter debugging after cover'
|
57
57
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
end
|
63
|
-
o.boolean('-h', '--help')
|
58
|
+
o.separator "\nOther available commands:"
|
59
|
+
o.on('--version', 'print the version') do
|
60
|
+
show_version
|
61
|
+
exit
|
64
62
|
end
|
63
|
+
o.boolean('-h', '--help')
|
65
64
|
end
|
65
|
+
end
|
66
66
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
end
|
72
|
-
options
|
67
|
+
def convert_options(options)
|
68
|
+
iu = options[:ignore_uncovered] = []
|
69
|
+
@ignore_uncovered_map.each do |cli_option, option|
|
70
|
+
iu << option if options.delete(cli_option)
|
73
71
|
end
|
72
|
+
options
|
73
|
+
end
|
74
74
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
end
|
75
|
+
def go
|
76
|
+
options = convert_options(menu.to_h)
|
77
|
+
if options[:help]
|
78
|
+
show_help
|
79
|
+
elsif options[:expression]
|
80
|
+
CLI::Debugger.new(options[:expression], **options).show
|
81
|
+
else
|
82
|
+
path = menu.arguments.first || '.'
|
83
|
+
CLI::InstrumentedCloneReporter.new(path, **options).run
|
85
84
|
end
|
86
85
|
end
|
87
86
|
end
|
data/lib/deep_cover/config.rb
CHANGED
@@ -3,14 +3,16 @@
|
|
3
3
|
module DeepCover
|
4
4
|
class Config
|
5
5
|
DEFAULTS = {
|
6
|
-
ignore_uncovered: [],
|
7
|
-
paths: %w[./app ./lib],
|
6
|
+
ignore_uncovered: [].freeze,
|
7
|
+
paths: %w[./app ./lib].freeze,
|
8
8
|
allow_partial: false,
|
9
9
|
}.freeze
|
10
10
|
|
11
|
-
|
11
|
+
OPTIONALLY_COVERED = %i[raise default_argument case_implicit_else trivial_if]
|
12
|
+
|
13
|
+
def initialize(notify = nil)
|
12
14
|
@notify = notify
|
13
|
-
@options =
|
15
|
+
@options = DEFAULTS.dup
|
14
16
|
end
|
15
17
|
|
16
18
|
def to_hash
|
@@ -29,7 +31,7 @@ module DeepCover
|
|
29
31
|
|
30
32
|
def detect_uncovered(*keywords)
|
31
33
|
if keywords.empty?
|
32
|
-
|
34
|
+
OPTIONALLY_COVERED - @options[:ignore_uncovered]
|
33
35
|
else
|
34
36
|
check_uncovered(keywords)
|
35
37
|
change(:ignore_uncovered, @options[:ignore_uncovered] - keywords)
|
@@ -53,7 +55,7 @@ module DeepCover
|
|
53
55
|
private
|
54
56
|
|
55
57
|
def check_uncovered(keywords)
|
56
|
-
unknown = keywords -
|
58
|
+
unknown = keywords - OPTIONALLY_COVERED
|
57
59
|
raise ArgumentError, "unknown options: #{unknown.join(', ')}" unless unknown.empty?
|
58
60
|
end
|
59
61
|
|
@@ -65,10 +67,6 @@ module DeepCover
|
|
65
67
|
self
|
66
68
|
end
|
67
69
|
|
68
|
-
def copy(h)
|
69
|
-
h.dup.transform_values(&:dup).transform_values(&:freeze)
|
70
|
-
end
|
71
|
-
|
72
70
|
module Setter
|
73
71
|
def config(notify = self)
|
74
72
|
@config ||= Config.new(notify)
|
data/lib/deep_cover/coverage.rb
CHANGED
@@ -15,6 +15,10 @@ module DeepCover
|
|
15
15
|
analyser_map.transform_values { |a| a.transform_values(&:stats) }
|
16
16
|
end
|
17
17
|
|
18
|
+
def self.template
|
19
|
+
{node: Analyser::Node, per_char: Analyser::PerChar, branch: Analyser::Branch}
|
20
|
+
end
|
21
|
+
|
18
22
|
private
|
19
23
|
|
20
24
|
def compute_analysers(covered_code)
|
@@ -37,6 +37,7 @@ module DeepCover
|
|
37
37
|
case (reporter = options.fetch(:reporter, :html).to_sym)
|
38
38
|
when :html
|
39
39
|
Reporter::HTML.report(self, **options)
|
40
|
+
Reporter::Text.report(self, **options) + "\n\nHTML generated: open #{options[:output]}/index.html"
|
40
41
|
when :istanbul
|
41
42
|
if Reporter::Istanbul.available?
|
42
43
|
report_istanbul(**options)
|
@@ -44,28 +45,12 @@ module DeepCover
|
|
44
45
|
warn 'nyc not available. Please install `nyc` using `yarn global add nyc` or `npm i nyc -g`'
|
45
46
|
end
|
46
47
|
when :text
|
47
|
-
|
48
|
+
Reporter::Text.report(self, **options)
|
48
49
|
else
|
49
50
|
raise ArgumentError, "Unknown reporter: #{reporter}"
|
50
51
|
end
|
51
52
|
end
|
52
53
|
|
53
|
-
def basic_report
|
54
|
-
missing = map do |covered_code|
|
55
|
-
if covered_code.has_executed?
|
56
|
-
missed = covered_code.line_coverage.each_with_index.map do |line_cov, line_index|
|
57
|
-
line_index + 1 if line_cov == 0
|
58
|
-
end.compact
|
59
|
-
else
|
60
|
-
missed = ['all']
|
61
|
-
end
|
62
|
-
[covered_code.buffer.name, missed] unless missed.empty?
|
63
|
-
end.compact.to_h
|
64
|
-
missing.map do |path, lines|
|
65
|
-
"#{File.basename(path)}: #{lines.join(', ')}"
|
66
|
-
end.join("\n")
|
67
|
-
end
|
68
|
-
|
69
54
|
def self.load(dest_path, dirname = 'deep_cover', with_trackers: true)
|
70
55
|
Persistence.new(dest_path, dirname).load(with_trackers: with_trackers)
|
71
56
|
end
|
@@ -3,7 +3,6 @@
|
|
3
3
|
module DeepCover
|
4
4
|
require 'securerandom'
|
5
5
|
class Coverage::Persistence
|
6
|
-
# rubocop:disable Security/MarshalLoad
|
7
6
|
BASENAME = 'coverage.dc'
|
8
7
|
TRACKER_TEMPLATE = 'trackers%{unique}.dct'
|
9
8
|
|
@@ -56,6 +55,7 @@ module DeepCover
|
|
56
55
|
))
|
57
56
|
end
|
58
57
|
|
58
|
+
# rubocop:disable Security/MarshalLoad
|
59
59
|
def load_coverage
|
60
60
|
Marshal.load(dir_path.join(BASENAME).binread).tap do |version: raise, coverage: raise|
|
61
61
|
raise "dump version mismatch: #{version}, currently #{DeepCover::VERSION}" unless version == DeepCover::VERSION
|
@@ -71,6 +71,7 @@ module DeepCover
|
|
71
71
|
end
|
72
72
|
end
|
73
73
|
end
|
74
|
+
# rubocop:enable Security/MarshalLoad
|
74
75
|
|
75
76
|
def merge_trackers(hash, to_merge)
|
76
77
|
hash.merge!(to_merge) do |_key, current, to_add|
|
@@ -1,6 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module DeepCover
|
4
|
+
bootstrap
|
5
|
+
load_parser
|
6
|
+
|
4
7
|
class CoveredCode
|
5
8
|
DEFAULT_TRACKER_GLOBAL = '$_cov'
|
6
9
|
|
@@ -11,7 +14,7 @@ module DeepCover
|
|
11
14
|
def initialize(path: nil, source: nil, lineno: 1, tracker_global: DEFAULT_TRACKER_GLOBAL, local_var: '_temp', name: nil)
|
12
15
|
raise 'Must provide either path or source' unless path || source
|
13
16
|
|
14
|
-
@buffer =
|
17
|
+
@buffer = Parser::Source::Buffer.new(path, lineno)
|
15
18
|
@buffer.source = source || File.read(path)
|
16
19
|
@tracker_count = 0
|
17
20
|
@tracker_global = tracker_global
|
@@ -88,7 +91,7 @@ module DeepCover
|
|
88
91
|
|
89
92
|
def root
|
90
93
|
@root ||= begin
|
91
|
-
ast =
|
94
|
+
ast = parser.parse(@buffer)
|
92
95
|
Node::Root.new(ast, self)
|
93
96
|
end
|
94
97
|
end
|
@@ -98,7 +101,7 @@ module DeepCover
|
|
98
101
|
end
|
99
102
|
|
100
103
|
def instrument_source
|
101
|
-
rewriter =
|
104
|
+
rewriter = Parser::Source::TreeRewriter.new(@buffer)
|
102
105
|
covered_ast.each_node(:postorder) do |node|
|
103
106
|
node.rewriting_rules.each do |range, rule|
|
104
107
|
prefix, _node, suffix = rule.partition('%{node}')
|
@@ -132,5 +135,14 @@ module DeepCover
|
|
132
135
|
def global
|
133
136
|
@@globals[tracker_global]
|
134
137
|
end
|
138
|
+
|
139
|
+
private
|
140
|
+
|
141
|
+
def parser
|
142
|
+
Parser::CurrentRuby.new.tap do |parser|
|
143
|
+
parser.diagnostics.all_errors_are_fatal = true
|
144
|
+
parser.diagnostics.ignore_warnings = true
|
145
|
+
end
|
146
|
+
end
|
135
147
|
end
|
136
148
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeepCover
|
4
|
+
module Load
|
5
|
+
AUTOLOAD = %i[analyser autoload_tracker coverage covered_code custom_requirer memoize
|
6
|
+
module_override node problem_with_diagnostic reporter
|
7
|
+
]
|
8
|
+
|
9
|
+
def load_absolute_basics
|
10
|
+
require_relative 'base'
|
11
|
+
require_relative 'config'
|
12
|
+
require_relative 'tools/camelize'
|
13
|
+
AUTOLOAD.each do |module_name|
|
14
|
+
DeepCover.autoload(Tools::Camelize.camelize(module_name), "#{__dir__}/#{module_name}")
|
15
|
+
end
|
16
|
+
Object.autoload :Term, 'term/ansicolor'
|
17
|
+
Object.autoload :Terminal, 'terminal-table'
|
18
|
+
require 'pry'
|
19
|
+
end
|
20
|
+
|
21
|
+
def bootstrap
|
22
|
+
return if @bootstrapped
|
23
|
+
require_relative 'backports'
|
24
|
+
require_relative 'tools'
|
25
|
+
@bootstrapped = true
|
26
|
+
end
|
27
|
+
|
28
|
+
def load_parser
|
29
|
+
return if @parser_loaded
|
30
|
+
require 'parser'
|
31
|
+
silence_warnings do
|
32
|
+
require 'parser/current'
|
33
|
+
end
|
34
|
+
require_relative_dir 'parser_ext'
|
35
|
+
@parser_loaded
|
36
|
+
end
|
37
|
+
|
38
|
+
def load_all
|
39
|
+
return if @all_loaded
|
40
|
+
bootstrap
|
41
|
+
load_parser
|
42
|
+
AUTOLOAD.each do |module_name|
|
43
|
+
DeepCover.const_get(Tools::Camelize.camelize(module_name))
|
44
|
+
end
|
45
|
+
@all_loaded = true
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
extend Load
|
50
|
+
end
|
data/lib/deep_cover/memoize.rb
CHANGED
data/lib/deep_cover/node.rb
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module DeepCover
|
4
|
+
bootstrap
|
5
|
+
load_parser
|
6
|
+
|
4
7
|
class Node
|
5
8
|
# Reopened in base
|
6
9
|
CLASSES = []
|
@@ -13,7 +16,6 @@ module DeepCover
|
|
13
16
|
require_relative 'node/base'
|
14
17
|
require_relative_dir 'node'
|
15
18
|
|
16
|
-
require_relative 'memoize'
|
17
19
|
Node.include Memoize
|
18
20
|
Node::CLASSES.freeze.each do |klass|
|
19
21
|
klass.memoize :flow_entry_count, :flow_completion_count, :execution_count, :loc_hash
|
data/lib/deep_cover/node/root.rb
CHANGED
@@ -4,7 +4,7 @@ module DeepCover
|
|
4
4
|
class Node::Root < Node
|
5
5
|
has_tracker :root
|
6
6
|
has_child main: Node,
|
7
|
-
can_be_empty: -> {
|
7
|
+
can_be_empty: -> { Parser::Source::Range.new(covered_code.buffer, 0, 0) },
|
8
8
|
is_statement: true,
|
9
9
|
rewrite: -> {
|
10
10
|
"#{covered_code.trackers_setup_source};%{root_tracker};%{local}=nil;%{node}"
|
@@ -6,7 +6,7 @@ module DeepCover
|
|
6
6
|
|
7
7
|
def initialize(covered_code, line_range, original_exception = nil)
|
8
8
|
@covered_code = covered_code
|
9
|
-
if line_range.
|
9
|
+
if line_range.respond_to? :last_line
|
10
10
|
@line_range = line_range.line..line_range.last_line
|
11
11
|
else
|
12
12
|
@line_range = line_range
|
data/lib/deep_cover/reporter.rb
CHANGED
@@ -1,23 +1,35 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module DeepCover
|
4
|
-
|
5
|
-
require_relative 'tree'
|
6
|
-
require_relative 'base'
|
4
|
+
require_relative 'base'
|
7
5
|
|
6
|
+
module Reporter
|
8
7
|
class HTML::Index < Struct.new(:analysis, :options)
|
9
8
|
def initialize(analysis, **options)
|
10
9
|
raise ArgumentError unless analysis.is_a? Coverage::Analysis
|
11
10
|
super
|
12
11
|
end
|
13
12
|
|
14
|
-
include
|
13
|
+
include Util::Tree
|
15
14
|
include HTML::Base
|
16
15
|
|
17
16
|
def stats_to_data
|
18
|
-
|
19
|
-
|
20
|
-
|
17
|
+
populate_stats(analysis) do |full_path, partial_path, data, children|
|
18
|
+
data = transform_data(data)
|
19
|
+
if children.empty?
|
20
|
+
{
|
21
|
+
text: %{<a href="#{full_path}.html">#{partial_path}</a>},
|
22
|
+
data: data,
|
23
|
+
}
|
24
|
+
else
|
25
|
+
{
|
26
|
+
text: partial_path,
|
27
|
+
data: data,
|
28
|
+
children: children,
|
29
|
+
state: {opened: true},
|
30
|
+
}
|
31
|
+
end
|
32
|
+
end
|
21
33
|
end
|
22
34
|
|
23
35
|
def columns
|
@@ -38,40 +50,12 @@ module DeepCover
|
|
38
50
|
|
39
51
|
private
|
40
52
|
|
41
|
-
# {a: {}} => [{text: a, data: stat_map[a]}]
|
42
|
-
# {b: {...}} => [{text: b, data: sum(stats), children: [...]}]
|
43
|
-
def populate(tree, dir = '')
|
44
|
-
tree.map do |path, children_hash|
|
45
|
-
full_path = [dir, path].join
|
46
|
-
if children_hash.empty?
|
47
|
-
{
|
48
|
-
text: %{<a href="#{full_path}.html">#{path}</a>},
|
49
|
-
data: @map[full_path],
|
50
|
-
}
|
51
|
-
else
|
52
|
-
children = populate(children_hash, "#{full_path}/")
|
53
|
-
data = Tools.merge(*children.map { |c| c[:data] }, :+)
|
54
|
-
{
|
55
|
-
text: path,
|
56
|
-
data: data,
|
57
|
-
children: children,
|
58
|
-
state: {opened: true},
|
59
|
-
}
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
# Modifies in place the tree:
|
65
53
|
# {per_char: Stat, ...} => {per_char: {ignored: ...}, per_char_percent: 55.55, ...}
|
66
|
-
def transform_data(
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
*node[:data].map { |type, stat| {:"#{type}_percent" => stat.percent_covered} }
|
72
|
-
)
|
73
|
-
transform_data(node[:children])
|
74
|
-
end
|
54
|
+
def transform_data(data)
|
55
|
+
Tools.merge(
|
56
|
+
data.transform_values(&:to_h),
|
57
|
+
*data.map { |type, stat| {:"#{type}_percent" => stat.percent_covered} }
|
58
|
+
)
|
75
59
|
end
|
76
60
|
end
|
77
61
|
end
|
@@ -25,7 +25,7 @@ module DeepCover
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def convert_source
|
28
|
-
@rewriter =
|
28
|
+
@rewriter = Parser::Source::TreeRewriter.new(covered_code.buffer)
|
29
29
|
insert_node_tags
|
30
30
|
insert_branch_tags
|
31
31
|
html_escape
|
@@ -126,7 +126,7 @@ module DeepCover
|
|
126
126
|
{'<' => '<', '>' => '>', '&' => '&'}.each do |char, escaped|
|
127
127
|
source.scan(char) do
|
128
128
|
m = Regexp.last_match
|
129
|
-
range =
|
129
|
+
range = Parser::Source::Range.new(buffer, m.begin(0), m.end(0))
|
130
130
|
@rewriter.replace(range, escaped)
|
131
131
|
end
|
132
132
|
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeepCover
|
4
|
+
module Reporter
|
5
|
+
class Text
|
6
|
+
include Util::Tree
|
7
|
+
def initialize(coverage, **options)
|
8
|
+
@coverage = coverage
|
9
|
+
@options = options
|
10
|
+
end
|
11
|
+
|
12
|
+
def analysis
|
13
|
+
Coverage::Analysis.new(@coverage.covered_codes, **@options)
|
14
|
+
end
|
15
|
+
|
16
|
+
INDENT = ' '
|
17
|
+
def report
|
18
|
+
formatted_headings = headings.map.with_index { |h, i| {value: h, alignment: :center} }
|
19
|
+
columns = rows.transpose
|
20
|
+
(1...columns.size).step(2) { |i| columns[i] = formatted_stats(columns[i]) }
|
21
|
+
table = Terminal::Table.new(
|
22
|
+
headings: formatted_headings,
|
23
|
+
rows: columns.transpose,
|
24
|
+
style: {border_bottom: false, border_top: false, alignment: :right},
|
25
|
+
)
|
26
|
+
table.align_column 0, :left
|
27
|
+
table.render
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.report(coverage, **options)
|
31
|
+
Text.new(coverage, **options).report
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def formatted_stats(data)
|
37
|
+
columns = data.transpose
|
38
|
+
columns[1..1] = [] if columns[1].none?
|
39
|
+
Terminal::Table.new(
|
40
|
+
rows: columns.transpose,
|
41
|
+
style: {border_x: '', border_bottom: false, border_top: false, alignment: :right}
|
42
|
+
).render.gsub(' | ', ' ').gsub(/ ?\| ?/, '').split("\n")
|
43
|
+
end
|
44
|
+
|
45
|
+
def rows
|
46
|
+
populate_stats(analysis).map do |full_path, partial_path, data, children|
|
47
|
+
[partial_path, *transform_data(data)]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def headings
|
52
|
+
Coverage::Analysis.template.values.flat_map do |analyser|
|
53
|
+
[analyser.human_name, '%']
|
54
|
+
end.unshift('Path')
|
55
|
+
end
|
56
|
+
|
57
|
+
# {per_char: Stat, ...} => ['1 [+2] / 3', '100 %', ...]
|
58
|
+
def transform_data(data)
|
59
|
+
data.flat_map do |type, stat|
|
60
|
+
ignored = "[+#{stat.ignored}]" if stat.ignored > 0
|
61
|
+
[[stat.executed, ignored, '/', stat.potentially_executable], format('%.2f', stat.percent_covered)]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeepCover
|
4
|
+
module Reporter
|
5
|
+
module Util
|
6
|
+
# Utility functions to deal with trees
|
7
|
+
module Tree
|
8
|
+
def paths_to_tree(paths)
|
9
|
+
twigs = paths.map do |path|
|
10
|
+
partials = path_to_partial_paths(path)
|
11
|
+
list_to_twig(partials)
|
12
|
+
end
|
13
|
+
tree = deep_merge(twigs)
|
14
|
+
simplify(tree)
|
15
|
+
end
|
16
|
+
|
17
|
+
# 'some/example/path' => %w[some example path]
|
18
|
+
def path_to_partial_paths(path)
|
19
|
+
path.to_s.split('/')
|
20
|
+
end
|
21
|
+
|
22
|
+
# A twig is a tree with only single branches
|
23
|
+
# [a, b, c] =>
|
24
|
+
# {a: {b: {c: {} } } }
|
25
|
+
def list_to_twig(items)
|
26
|
+
result = {}
|
27
|
+
items.inject(result) do |parent, value|
|
28
|
+
parent[value] = {}
|
29
|
+
end
|
30
|
+
result
|
31
|
+
end
|
32
|
+
|
33
|
+
# [{a: {b: {c: {} } } }
|
34
|
+
# {a: {b: {d: {} } } }]
|
35
|
+
# => {a: {b: {c: {}, d: {} }}}
|
36
|
+
def deep_merge(trees)
|
37
|
+
trees.inject do |result, h|
|
38
|
+
result.merge(h) { |k, val, val_b| deep_merge([val, val_b]) }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# {a: {b: {c: {}, d: {} }}}
|
43
|
+
# => {a/b: {c: {}, d: {} }}
|
44
|
+
def simplify(tree)
|
45
|
+
tree.map do |key, sub_tree|
|
46
|
+
sub_tree = simplify(sub_tree)
|
47
|
+
if sub_tree.size == 1
|
48
|
+
key2, sub_tree = sub_tree.first
|
49
|
+
key = "#{key}/#{key2}"
|
50
|
+
end
|
51
|
+
[key, sub_tree]
|
52
|
+
end.to_h
|
53
|
+
end
|
54
|
+
|
55
|
+
# {a: {b: {}}} => [ra, rb]
|
56
|
+
# where rb = yield('a/b', 'b', [])
|
57
|
+
# and ra = yield('a', 'a', [rb])
|
58
|
+
def populate(tree, dir = '', &block)
|
59
|
+
return to_enum(__method__, tree, dir) unless block_given?
|
60
|
+
tree.map do |path, children_hash|
|
61
|
+
full_path = [dir, path].join
|
62
|
+
children = populate(children_hash, "#{full_path}/", &block)
|
63
|
+
yield full_path, path, children
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Same as populate, but also yields data, which is either the analysis data (for leaves)
|
68
|
+
# of the sum of the children (for subtrees)
|
69
|
+
def populate_stats(analysis)
|
70
|
+
return to_enum(__method__, analysis) unless block_given?
|
71
|
+
map = Tools.transform_keys(analysis.stat_map, &:name)
|
72
|
+
tree = paths_to_tree(map.keys)
|
73
|
+
final_results, _final_data = populate(tree) do |full_path, partial_path, children|
|
74
|
+
if children.empty?
|
75
|
+
data = map[full_path]
|
76
|
+
else
|
77
|
+
child_results, child_data = children.transpose
|
78
|
+
data = Tools.merge(*child_data, :+)
|
79
|
+
end
|
80
|
+
result = yield full_path, partial_path, data, child_results || []
|
81
|
+
[result, data]
|
82
|
+
end.transpose
|
83
|
+
final_results
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
data/lib/deep_cover/tools.rb
CHANGED
@@ -1,10 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module DeepCover
|
4
|
-
module Tools
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
module Tools
|
5
|
+
module Camelize
|
6
|
+
extend self # Loaded before bootstrap
|
7
|
+
# Poor man's camelize. 'an_example' => 'AnExample'
|
8
|
+
def camelize(string)
|
9
|
+
string.to_s.gsub(/([a-z\d]*)[_?!]?/) { Regexp.last_match(1).capitalize }
|
10
|
+
end
|
8
11
|
end
|
9
12
|
end
|
10
13
|
end
|
data/lib/deep_cover/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: deep-cover
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Marc-André Lafortune
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date: 2017-12-
|
12
|
+
date: 2017-12-18 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: parser
|
@@ -109,6 +109,20 @@ dependencies:
|
|
109
109
|
- - ">="
|
110
110
|
- !ruby/object:Gem::Version
|
111
111
|
version: '0'
|
112
|
+
- !ruby/object:Gem::Dependency
|
113
|
+
name: terminal-table
|
114
|
+
requirement: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - ">="
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
type: :runtime
|
120
|
+
prerelease: false
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - ">="
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
112
126
|
- !ruby/object:Gem::Dependency
|
113
127
|
name: pry
|
114
128
|
requirement: !ruby/object:Gem::Requirement
|
@@ -271,6 +285,7 @@ files:
|
|
271
285
|
- lib/deep_cover/coverage/persistence.rb
|
272
286
|
- lib/deep_cover/covered_code.rb
|
273
287
|
- lib/deep_cover/custom_requirer.rb
|
288
|
+
- lib/deep_cover/load.rb
|
274
289
|
- lib/deep_cover/memoize.rb
|
275
290
|
- lib/deep_cover/module_override.rb
|
276
291
|
- lib/deep_cover/node.rb
|
@@ -327,8 +342,9 @@ files:
|
|
327
342
|
- lib/deep_cover/reporter/html/template/assets/throbber.gif
|
328
343
|
- lib/deep_cover/reporter/html/template/index.html.erb
|
329
344
|
- lib/deep_cover/reporter/html/template/source.html.erb
|
330
|
-
- lib/deep_cover/reporter/html/tree.rb
|
331
345
|
- lib/deep_cover/reporter/istanbul.rb
|
346
|
+
- lib/deep_cover/reporter/text.rb
|
347
|
+
- lib/deep_cover/reporter/util/tree.rb
|
332
348
|
- lib/deep_cover/tools.rb
|
333
349
|
- lib/deep_cover/tools/builtin_coverage.rb
|
334
350
|
- lib/deep_cover/tools/camelize.rb
|
@@ -1,55 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module DeepCover
|
4
|
-
module Reporter
|
5
|
-
# Utility functions to deal with trees
|
6
|
-
module HTML::Tree
|
7
|
-
def paths_to_tree(paths)
|
8
|
-
twigs = paths.map do |path|
|
9
|
-
partials = path_to_partial_paths(path)
|
10
|
-
list_to_twig(partials)
|
11
|
-
end
|
12
|
-
tree = deep_merge(twigs)
|
13
|
-
simplify(tree)
|
14
|
-
end
|
15
|
-
|
16
|
-
# 'some/example/path' => %w[some example path]
|
17
|
-
def path_to_partial_paths(path)
|
18
|
-
path.to_s.split('/')
|
19
|
-
end
|
20
|
-
|
21
|
-
# A twig is a tree with only single branches
|
22
|
-
# [a, b, c] =>
|
23
|
-
# {a: {b: {c: {} } } }
|
24
|
-
def list_to_twig(items)
|
25
|
-
result = {}
|
26
|
-
items.inject(result) do |parent, value|
|
27
|
-
parent[value] = {}
|
28
|
-
end
|
29
|
-
result
|
30
|
-
end
|
31
|
-
|
32
|
-
# [{a: {b: {c: {} } } }
|
33
|
-
# {a: {b: {d: {} } } }]
|
34
|
-
# => {a: {b: {c: {}, d: {} }}}
|
35
|
-
def deep_merge(trees)
|
36
|
-
trees.inject do |result, h|
|
37
|
-
result.merge(h) { |k, val, val_b| deep_merge([val, val_b]) }
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
# {a: {b: {c: {}, d: {} }}}
|
42
|
-
# => {a/b: {c: {}, d: {} }}
|
43
|
-
def simplify(tree)
|
44
|
-
tree.map do |key, sub_tree|
|
45
|
-
sub_tree = simplify(sub_tree)
|
46
|
-
if sub_tree.size == 1
|
47
|
-
key2, sub_tree = sub_tree.first
|
48
|
-
key = "#{key}/#{key2}"
|
49
|
-
end
|
50
|
-
[key, sub_tree]
|
51
|
-
end.to_h
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|