deep-cover 0.2.0 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: de76a03a06ec4a16d33c5a24a9c510678f83b33a
4
- data.tar.gz: db775778d2b7d2c65ce0c299f3ff4f3900796e02
3
+ metadata.gz: 3a8450fe7655e95139f1443a7e81f3c73b726e03
4
+ data.tar.gz: b04f9ef3b69060ff9568cae7224bc89a845137bf
5
5
  SHA512:
6
- metadata.gz: 246c598d3da7d0fca5d5c4e453dc134f2e36cc59e7529f4148fa159c8e31c3288316e731faec5d08be37167fdd605454f0155b12cea3fdcbd22235f3e9ad9df3
7
- data.tar.gz: 96fea731d9cd93158718857999a9da092b9ec619126dc0814b339b8e3c460a224dca594194b35d686b8dbf5c5c4cde03de69bd17a1bcd482e92ab1851bbb4ee7
6
+ metadata.gz: 822e717af608dd6b6ed0010ce00f75aff18f4f60cfddc866df90ceefaf6131820a19f8a7a0e2d5eb00d961412dbcac23295e4b3aebb8be9cd1ae7ca7ecf13ab5
7
+ data.tar.gz: a5849e499b6f4eb7975af2e4529f5564e99a982d0cda525e0d246e6bce698f289e24cadf9a44ba3a85c4e78ae60da41813ef443f3f7114a261a6a856cd6a024e
@@ -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
@@ -13,7 +13,7 @@ before_install:
13
13
  before_script:
14
14
  - bundle exec rake dev:install
15
15
  script:
16
- - rake test:all
16
+ - bundle exec rake test:all
17
17
  matrix:
18
18
  allow_failures:
19
19
  - rvm: jruby-9.1.9.0
@@ -38,6 +38,9 @@ Gem::Specification.new do |spec|
38
38
  spec.add_runtime_dependency 'term-ansicolor'
39
39
  spec.add_runtime_dependency 'with_progress'
40
40
 
41
+ # Reporters
42
+ spec.add_runtime_dependency 'terminal-table'
43
+
41
44
  # While in 0.x
42
45
  spec.add_runtime_dependency 'pry'
43
46
 
@@ -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
- # External dependencies (ex parser)
6
- require 'parser'
7
- require 'term/ansicolor'
8
- require 'pry'
4
+ require_relative 'deep_cover/load'
9
5
 
10
- # Bootstrapping
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
@@ -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
@@ -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 '../deep_cover/core_ext/coverage_replacement'
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
- module DeepCover
11
- extend self
10
+ module CLI::DeepCover
11
+ extend self
12
12
 
13
- def show_version
14
- puts "deep-cover v#{::DeepCover::VERSION}; parser v#{::Parser::VERSION}"
15
- end
13
+ def show_version
14
+ puts "deep-cover v#{DeepCover::VERSION}; parser v#{Parser::VERSION}"
15
+ end
16
16
 
17
- def show_help
18
- puts menu
19
- end
17
+ def show_help
18
+ puts menu
19
+ end
20
20
 
21
- class Parser < 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]}]"
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
- def parse
32
- Slop.parse do |o|
33
- yield Parser.new(o)
34
- end
31
+ def parse
32
+ Slop.parse do |o|
33
+ yield OptionParser.new(o)
35
34
  end
35
+ end
36
36
 
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'
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
- o.separator "\nOther available commands:"
59
- o.on('--version', 'print the version') do
60
- show_version
61
- exit
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
- 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)
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
- def go
76
- options = convert_options(menu.to_h)
77
- if options[:help]
78
- show_help
79
- elsif options[:expression]
80
- Debugger.new(options[:expression], **options).show
81
- else
82
- path = menu.arguments.first || '.'
83
- InstrumentedCloneReporter.new(path, **options).run
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
@@ -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
- def initialize(notify = nil, **options)
11
+ OPTIONALLY_COVERED = %i[raise default_argument case_implicit_else trivial_if]
12
+
13
+ def initialize(notify = nil)
12
14
  @notify = notify
13
- @options = copy(DEFAULTS.merge(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
- Analyser::Node.optionally_covered - @options[:ignore_uncovered]
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 - Analyser::Node.optionally_covered
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)
@@ -7,6 +7,8 @@
7
7
  # overrides Kernel#require)
8
8
 
9
9
  module DeepCover
10
+ load_all
11
+
10
12
  module RequireOverride
11
13
  def require(path)
12
14
  result = catch(:use_fallback) { DeepCover.custom_requirer.require(path) }
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeepCover
4
- require_relative 'covered_code'
5
- Coverage = Class.new
4
+ bootstrap
5
+
6
+ class Coverage
7
+ end
6
8
  require_relative_dir 'coverage'
7
9
  Coverage.include Coverage::Istanbul
8
10
  end
@@ -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
- basic_report
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 = ::Parser::Source::Buffer.new(path, lineno)
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 = DeepCover.parser.parse(@buffer)
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 = ::Parser::Source::TreeRewriter.new(@buffer)
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
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeepCover
4
+ bootstrap
5
+
4
6
  # Memoize is a quick way to prepend a module that defines
5
7
  # the memoized methods as `@_cache ||= super.freeze`
6
8
  # It also refines `freeze` to precache memoized methods
@@ -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
@@ -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: -> { ::Parser::Source::Range.new(covered_code.buffer, 0, 0) },
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.is_a?(Parser::Source::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
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeepCover
4
+ bootstrap
5
+
4
6
  module Reporter
5
7
  end
6
8
  require_relative 'node'
9
+ require_relative_dir 'reporter/util'
7
10
  require_relative_dir 'reporter'
8
11
  end
@@ -1,23 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeepCover
4
- module Reporter
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 HTML::Tree
13
+ include Util::Tree
15
14
  include HTML::Base
16
15
 
17
16
  def stats_to_data
18
- @map = Tools.transform_keys(analysis.stat_map, &:name)
19
- tree = paths_to_tree(@map.keys)
20
- transform_data(populate(tree))
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(tree)
67
- return unless tree
68
- tree.each do |node|
69
- node[:data] = Tools.merge(
70
- node[:data].transform_values(&:to_h),
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 = ::Parser::Source::TreeRewriter.new(covered_code.buffer)
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
  {'<' => '&lt;', '>' => '&gt;', '&' => '&amp;'}.each do |char, escaped|
127
127
  source.scan(char) do
128
128
  m = Regexp.last_match
129
- range = ::Parser::Source::Range.new(buffer, m.begin(0), m.end(0))
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
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeepCover
4
- Tools = Module.new
4
+ module Tools
5
+ end
5
6
 
6
7
  require_relative 'tools/require_relative_dir'
7
8
  extend Tools::RequireRelativeDir
@@ -2,9 +2,8 @@
2
2
 
3
3
  module DeepCover
4
4
  module Tools::BuiltinCoverage
5
- require 'coverage'
6
-
7
5
  def builtin_coverage(source, fn, lineno)
6
+ require 'coverage'
8
7
  fn = File.absolute_path(File.expand_path(fn))
9
8
  ::Coverage.start
10
9
  Tools.silence_warnings do
@@ -1,10 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeepCover
4
- module Tools::Camelize
5
- # Poor man's camelize. 'an_example' => 'AnExample'
6
- def camelize(string)
7
- string.to_s.gsub(/([a-z\d]*)[_?!]?/) { Regexp.last_match(1).capitalize }
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeepCover
4
- VERSION = '0.2.0'
4
+ VERSION = '0.3.0'
5
5
  end
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.2.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-08 00:00:00.000000000 Z
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