coverband 6.1.7 → 6.1.8

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +10 -3
  3. data/Gemfile +3 -0
  4. data/Gemfile.rails7.0 +3 -0
  5. data/Gemfile.rails7.1 +4 -1
  6. data/Gemfile.rails7.2 +3 -0
  7. data/Gemfile.rails8.0 +3 -0
  8. data/changes.md +14 -0
  9. data/lib/coverband/adapters/base.rb +21 -6
  10. data/lib/coverband/adapters/file_store.rb +1 -2
  11. data/lib/coverband/adapters/hash_redis_store.rb +2 -1
  12. data/lib/coverband/adapters/redis_store.rb +1 -2
  13. data/lib/coverband/adapters/web_service_store.rb +1 -2
  14. data/lib/coverband/collectors/abstract_tracker.rb +4 -4
  15. data/lib/coverband/collectors/delta.rb +4 -4
  16. data/lib/coverband/collectors/view_tracker.rb +12 -2
  17. data/lib/coverband/mcp/tools/get_file_coverage.rb +2 -0
  18. data/lib/coverband/reporters/json_report.rb +7 -0
  19. data/lib/coverband/utils/absolute_file_converter.rb +37 -7
  20. data/lib/coverband/utils/file_list.rb +15 -7
  21. data/lib/coverband/utils/html_formatter.rb +5 -2
  22. data/lib/coverband/utils/relative_file_converter.rb +22 -7
  23. data/lib/coverband/utils/result.rb +0 -2
  24. data/lib/coverband/utils/source_file/line.rb +84 -0
  25. data/lib/coverband/utils/source_file.rb +33 -81
  26. data/lib/coverband/version.rb +1 -1
  27. data/test/benchmarks/benchmark.rake +4 -0
  28. data/test/benchmarks/benchmark_delta.rb +54 -0
  29. data/test/benchmarks/benchmark_file_list.rb +58 -0
  30. data/test/benchmarks/benchmark_unused_keys.rb +52 -0
  31. data/test/coverband/collectors/route_tracker_test.rb +3 -3
  32. data/test/coverband/collectors/translation_tracker_test.rb +1 -1
  33. data/test/coverband/collectors/view_tracker_test.rb +2 -2
  34. data/test/coverband/mcp/tools/get_file_coverage_test.rb +44 -0
  35. data/test/coverband/reporters/console_test.rb +1 -0
  36. data/test/coverband/reporters/json_test.rb +3 -3
  37. data/test/coverband/utils/absolute_file_converter_test.rb +35 -0
  38. data/test/coverband/utils/relative_file_converter_test.rb +26 -0
  39. data/test/coverband/utils/result_test.rb +19 -0
  40. data/test/coverband/utils/{source_file_line_test.rb → source_file/line_test.rb} +1 -1
  41. metadata +6 -3
  42. data/coverband/log.272267 +0 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e3ae4783bfea151c9e3b5d58e397467085206359b6c3446df24ad136fc067ca3
4
- data.tar.gz: c7491d46394412b813447b80e21ca29e1cfe62060b54a28a14a6145a8ec5eb84
3
+ metadata.gz: '01680ffc24d42fce1b45f90fff31a9d190b029af8a83b0a1405ed81d2785bc10'
4
+ data.tar.gz: 04daf91b9d7f64849d217075e71ff00d54cac2e64fd1b5c7fdd7e0b27943b56e
5
5
  SHA512:
6
- metadata.gz: 2cbd8715bf8a5ea95e5705038acec36bd8549fb9ca01b99c4266919904122fb9fd81cbc3e1488d6eaa0c0bb955875c429751523843449abc0c5d01107d620085
7
- data.tar.gz: f4ac2904d9cde83162aa3f1d350aeb25e337f8c86178b79668170ae00ef64df9d564e15cdd94597cb1ea6652daa244b8454cdc8c3401e14b74662527ff7ea34e
6
+ metadata.gz: 0de107048e072e97c1bded932a59ac9b6be2c6ac94b045118977aae38282a109772b183bed1b7ca99d7867d3fc694f326a53e2e93174bbf372e7f0ba16e8bd13
7
+ data.tar.gz: 57d8e1921056acb7f13e47b02caeb6207e635984a461c1952aafd0dbf4e445519d99ca8b369b60d11c3121b86da6f5f3d3d2a4e6aaf6d68fd2d3a8aa155d14fb
@@ -28,11 +28,18 @@ jobs:
28
28
  - gemfile: 'rails_8.0'
29
29
  ruby: '3.1'
30
30
  runs-on: ${{ matrix.os }}-latest
31
+ services:
32
+ redis:
33
+ image: redis:${{ matrix.redis-version }}
34
+ ports:
35
+ - 6379:6379
36
+ options: >-
37
+ --health-cmd "redis-cli ping"
38
+ --health-interval 10s
39
+ --health-timeout 5s
40
+ --health-retries 5
31
41
  steps:
32
42
  - uses: actions/checkout@v6
33
- - uses: supercharge/redis-github-action@v2
34
- with:
35
- redis-version: ${{ matrix.redis-version }}
36
43
  - uses: ruby/setup-ruby@v1
37
44
  with:
38
45
  ruby-version: ${{ matrix.ruby }}
data/Gemfile CHANGED
@@ -8,3 +8,6 @@ gem "rails" # latest
8
8
  gem "haml"
9
9
  gem "slim"
10
10
  gem "webrick"
11
+
12
+ # Required for Ruby 3.4+ (extracted from stdlib)
13
+ gem "cgi"
data/Gemfile.rails7.0 CHANGED
@@ -8,3 +8,6 @@ gem 'rails', '~>7.0.0'
8
8
  gem "haml"
9
9
  gem "slim"
10
10
  gem "webrick"
11
+
12
+ # Required for Ruby 3.4+ (extracted from stdlib)
13
+ gem "cgi"
data/Gemfile.rails7.1 CHANGED
@@ -7,4 +7,7 @@ gemspec
7
7
  gem 'rails', '~>7.1.0'
8
8
  gem "haml"
9
9
  gem "slim"
10
- gem "webrick"
10
+ gem "webrick"
11
+
12
+ # Required for Ruby 3.4+ (extracted from stdlib)
13
+ gem "cgi"
data/Gemfile.rails7.2 CHANGED
@@ -8,3 +8,6 @@ gem 'rails', '~>7.2.0'
8
8
  gem "haml"
9
9
  gem "slim"
10
10
  gem "webrick"
11
+
12
+ # Required for Ruby 3.4+ (extracted from stdlib)
13
+ gem "cgi"
data/Gemfile.rails8.0 CHANGED
@@ -8,3 +8,6 @@ gem 'rails', '~>8.0.0'
8
8
  gem "haml"
9
9
  gem "slim"
10
10
  gem "webrick"
11
+
12
+ # Required for Ruby 3.4+ (extracted from stdlib)
13
+ gem "cgi"
data/changes.md CHANGED
@@ -1,3 +1,17 @@
1
+ ### 6.1.8
2
+
3
+ * Feature: Added temporal data to GetFileCoverage tool for MCP by @fabienpiette (#626)
4
+ * Optimization: Significant performance improvements to `FileList` avoiding array allocations and reducing redundant iterations (#619, #620, #622, #623)
5
+ * Optimization: Optimize `HTMLFormatter` with template caching (#616, #618)
6
+ * Optimization: Optimize memory usage by removing redundant `dup` calls across `Coverband::Utils::Result` (#617, #621)
7
+ * Optimization: Optimize `array_add` logic with in-place modification avoiding overhead in newer Ruby (#615)
8
+ * Optimization: Optimize `ViewTracker` unused keys and path normalization (#608, #609, #613)
9
+ * Optimization: Optimize `AbstractTracker` to use bulk Redis hset and direct Hash lookups (#610, #612)
10
+ * Optimization: Cache Regexp in `short_name` and optimize `RelativeFileConverter` with `Regexp.union` (#606, #607)
11
+ * Optimization: Optimize delta collector ignore check order (#611)
12
+ * Refactoring: Extract Line class to `Coverband::Utils::SourceFile::Line` (#605, #614)
13
+ * Fix: Improve symlink handling in file converters (#603)
14
+
1
15
  ### Coverband 6.1.7
2
16
 
3
17
  * add back a running demo site
@@ -125,14 +125,29 @@ module Coverband
125
125
  }
126
126
  end
127
127
 
128
- # TODO: This should have cases reduced
129
128
  def array_add(latest, original)
130
- if latest.empty? && original.empty?
131
- []
132
- elsif Coverband.configuration.use_oneshot_lines_coverage
133
- latest.map!.with_index { |v, i| ((v + original[i] >= 1) ? 1 : 0) if v && original[i] }
129
+ use_oneshot = Coverband.configuration.use_oneshot_lines_coverage
130
+ if latest.frozen?
131
+ latest.map.with_index do |v, i|
132
+ if v && original[i]
133
+ if use_oneshot
134
+ (v + original[i] >= 1) ? 1 : 0
135
+ else
136
+ v + original[i]
137
+ end
138
+ end
139
+ end
134
140
  else
135
- latest.map.with_index { |v, i| (v && original[i]) ? v + original[i] : nil }
141
+ latest.each_with_index do |v, i|
142
+ latest[i] = if v && original[i]
143
+ if use_oneshot
144
+ (v + original[i] >= 1) ? 1 : 0
145
+ else
146
+ v + original[i]
147
+ end
148
+ end
149
+ end
150
+ latest
136
151
  end
137
152
  end
138
153
  end
@@ -65,8 +65,7 @@ module Coverband
65
65
  end
66
66
 
67
67
  def save_report(report)
68
- data = report.dup
69
- data = merge_reports(data, coverage)
68
+ data = merge_reports(report, coverage)
70
69
  File.write(path, JSON.dump(data))
71
70
  end
72
71
 
@@ -29,6 +29,7 @@ module Coverband
29
29
 
30
30
  @ttl = opts[:ttl]
31
31
  @relative_file_converter = opts[:relative_file_converter] || Utils::RelativeFileConverter
32
+ @short_name_regex = /^#{Coverband.configuration.root}/
32
33
  end
33
34
 
34
35
  def supported?
@@ -171,7 +172,7 @@ module Coverband
171
172
  end
172
173
 
173
174
  def short_name(filename)
174
- filename.sub(/^#{Coverband.configuration.root}/, ".")
175
+ filename.sub(@short_name_regex, ".")
175
176
  .gsub(%r{^\./}, "")
176
177
  end
177
178
 
@@ -63,8 +63,7 @@ module Coverband
63
63
  # the Coverband 2 had the same issue,
64
64
  # and the tradeoff has always been acceptable
65
65
  def save_report(report)
66
- data = report.dup
67
- data = merge_reports(data, coverage(nil, skip_hash_check: true))
66
+ data = merge_reports(report, coverage(nil, skip_hash_check: true))
68
67
  save_coverage(data)
69
68
  end
70
69
 
@@ -66,10 +66,9 @@ module Coverband
66
66
  # We set here vs initialize to avoid setting on the primary process vs child processes
67
67
  @pid ||= ::Process.pid
68
68
 
69
- # TODO: do we need dup
70
69
  # TODO: we don't need upstream timestamps, server will track first_seen
71
70
  Thread.new do
72
- data = expand_report(report.dup)
71
+ data = expand_report(report)
73
72
  full_package = {
74
73
  collection_type: "coverage_delta",
75
74
  collection_data: {
@@ -61,8 +61,8 @@ module Coverband
61
61
  end
62
62
 
63
63
  def unused_keys(used_keys = nil)
64
- recently_used_keys = (used_keys || self.used_keys).keys
65
- all_keys.reject { |k| recently_used_keys.include?(k.to_s) }
64
+ recently_used_keys = used_keys || self.used_keys
65
+ all_keys.reject { |k| recently_used_keys.key?(k.to_s) }
66
66
  end
67
67
 
68
68
  def as_json
@@ -97,8 +97,8 @@ module Coverband
97
97
  redis_store.set(tracker_time_key, Time.now.to_i) unless @one_time_timestamp || tracker_time_key_exists?
98
98
  @one_time_timestamp = true
99
99
  reported_time = Time.now.to_i
100
- @keys_to_record.to_a.each do |key|
101
- redis_store.hset(tracker_key, key.to_s, reported_time)
100
+ if @keys_to_record.any?
101
+ redis_store.hset(tracker_key, @keys_to_record.each_with_object({}) { |key, h| h[key.to_s] = reported_time })
102
102
  end
103
103
  @keys_to_record.clear
104
104
  rescue => e
@@ -54,8 +54,8 @@ module Coverband
54
54
  # on the critical performance path, and any refactoring I come up with
55
55
  # would slow down the performance.
56
56
  ###
57
- next unless @@ignore_patterns.none? { |pattern| file.match(pattern) } &&
58
- file.start_with?(@@project_directory)
57
+ next unless file.start_with?(@@project_directory) &&
58
+ @@ignore_patterns.none? { |pattern| file.match(pattern) }
59
59
 
60
60
  # This handles Coverage branch support, setup by default in
61
61
  # simplecov 0.18.x
@@ -84,8 +84,8 @@ module Coverband
84
84
  # on the critical performance path, and any refactoring I come up with
85
85
  # would slow down the performance.
86
86
  ###
87
- next unless @@ignore_patterns.none? { |pattern| file.match(pattern) } &&
88
- file.start_with?(@@project_directory)
87
+ next unless file.start_with?(@@project_directory) &&
88
+ @@ignore_patterns.none? { |pattern| file.match(pattern) }
89
89
 
90
90
  @@stubs[file] ||= ::Coverage.line_stub(file)
91
91
  transformed_line_counts = coverage[:oneshot_lines].each_with_object(@@stubs[file].dup) { |line_number, line_counts|
@@ -78,8 +78,17 @@ module Coverband
78
78
  recently_used_views = used_keys.keys
79
79
  unused_views = all_keys - recently_used_views
80
80
  # since layouts don't include format we count them used if they match with ANY formats
81
- unused_views = unused_views.reject { |view| view.include?("/layouts/") && recently_used_views.any? { |used_view| view.include?(used_view) } }
82
- unused_views.reject { |view| @ignore_patterns.any? { |pattern| view.match?(pattern) } }
81
+ potential_layout_references = recently_used_views.reject { |v| v.end_with?(".erb", ".haml", ".slim") }
82
+
83
+ layout_matcher = if potential_layout_references.any?
84
+ Regexp.union(potential_layout_references)
85
+ end
86
+
87
+ unused_views.reject! do |view|
88
+ (layout_matcher && view.include?("/layouts/") && view.match?(layout_matcher)) ||
89
+ @ignore_patterns.any? { |pattern| view.match?(pattern) }
90
+ end
91
+ unused_views
83
92
  end
84
93
 
85
94
  def clear_key!(filename)
@@ -103,6 +112,7 @@ module Coverband
103
112
 
104
113
  roots.each do |root|
105
114
  normalized = normalized.gsub(root, "")
115
+ break if normalized.length < original_length
106
116
  end
107
117
 
108
118
  # Only remove leading slash if we actually modified the path by removing a root
@@ -52,6 +52,8 @@ module Coverband
52
52
  lines_missed: file_data["lines_missed"],
53
53
  runtime_percentage: file_data["runtime_percentage"],
54
54
  never_loaded: file_data["never_loaded"],
55
+ first_updated_at: file_data["first_updated_at"],
56
+ last_updated_at: file_data["last_updated_at"],
55
57
  coverage: file_data["coverage"]
56
58
  }
57
59
  end
@@ -72,6 +72,11 @@ module Coverband
72
72
  end
73
73
  end
74
74
 
75
+ def format_timestamp(timestamp)
76
+ return nil if timestamp.nil? || timestamp == Coverband::Utils::SourceFile::NOT_AVAILABLE
77
+ timestamp.is_a?(Time) ? timestamp.iso8601 : timestamp.to_s
78
+ end
79
+
75
80
  def report_as_json
76
81
  return filtered_report_files.to_json if for_merged_report
77
82
 
@@ -133,6 +138,8 @@ module Coverband
133
138
  filename: source_file.filename,
134
139
  hash: Digest::SHA1.hexdigest(source_file.filename),
135
140
  never_loaded: source_file.never_loaded,
141
+ first_updated_at: format_timestamp(source_file.first_updated_at),
142
+ last_updated_at: format_timestamp(source_file.last_updated_at),
136
143
  runtime_percentage: result.runtime_relevant_coverage(source_file),
137
144
  lines_of_code: source_file.lines.count,
138
145
  lines_covered: source_file.covered_lines.count,
@@ -5,7 +5,7 @@ module Coverband
5
5
  class AbsoluteFileConverter
6
6
  def initialize(roots)
7
7
  @cache = {}
8
- @roots = roots.map { |root| "#{File.expand_path(root)}/" }
8
+ @roots = convert_roots(roots)
9
9
  end
10
10
 
11
11
  def self.instance
@@ -24,11 +24,25 @@ module Coverband
24
24
  @cache[relative_path] ||= begin
25
25
  relative_filename = relative_path
26
26
  local_filename = relative_filename
27
- @roots.each do |root|
28
- relative_filename = relative_filename.sub(/^#{root}/, "./")
29
- # once we have a relative path break out of the loop
30
- break if relative_filename.start_with? "./"
27
+ @roots.each do |root, root_regexp|
28
+ if relative_filename.match?(root_regexp)
29
+ relative_filename = relative_filename.sub(root_regexp, "./")
30
+ # once we have a relative path break out of the loop
31
+ break
32
+ end
31
33
  end
34
+
35
+ if relative_filename == local_filename && File.exist?(local_filename)
36
+ real_filename = File.realpath(local_filename)
37
+ @roots.each do |root, root_regexp|
38
+ if real_filename.match?(root_regexp)
39
+ relative_filename = real_filename.sub(root_regexp, "./")
40
+ # once we have a relative path break out of the loop
41
+ break
42
+ end
43
+ end
44
+ end
45
+
32
46
  # the filename for our reports is expected to be a full path.
33
47
  # roots.last should be roots << current_root}/
34
48
  # a fully expanded path of config.root
@@ -36,12 +50,28 @@ module Coverband
36
50
  # above only works for app files
37
51
  # we need to rethink some of this logic
38
52
  # gems aren't at project root and can have multiple locations
39
- local_root = @roots.find { |root|
53
+ local_root = @roots.find { |root, _root_regexp|
40
54
  File.exist?(relative_filename.gsub("./", root))
41
- }
55
+ }&.first
42
56
  local_root ? relative_filename.gsub("./", local_root) : local_filename
43
57
  end
44
58
  end
59
+
60
+ private
61
+
62
+ def convert_roots(roots)
63
+ roots.flat_map { |root|
64
+ items = []
65
+ expanded = "#{File.expand_path(root)}/"
66
+ items << [expanded, /^#{expanded}/]
67
+
68
+ if File.exist?(root)
69
+ real = "#{File.realpath(root)}/"
70
+ items << [real, /^#{Regexp.escape(real)}/]
71
+ end
72
+ items
73
+ }.uniq
74
+ end
45
75
  end
46
76
  end
47
77
  end
@@ -14,28 +14,28 @@ module Coverband
14
14
  def covered_lines
15
15
  return 0.0 if empty?
16
16
 
17
- map { |f| f.covered_lines.count }.inject(:+)
17
+ @covered_lines ||= sum(&:covered_lines_count)
18
18
  end
19
19
 
20
20
  # Returns the count of lines that have been missed
21
21
  def missed_lines
22
22
  return 0.0 if empty?
23
23
 
24
- map { |f| f.missed_lines.count }.inject(:+)
24
+ @missed_lines ||= sum(&:missed_lines_count)
25
25
  end
26
26
 
27
27
  # Returns the count of lines that are not relevant for coverage
28
28
  def never_lines
29
29
  return 0.0 if empty?
30
30
 
31
- map { |f| f.never_lines.count }.inject(:+)
31
+ @never_lines ||= sum(&:never_lines_count)
32
32
  end
33
33
 
34
34
  # Returns the count of skipped lines
35
35
  def skipped_lines
36
36
  return 0.0 if empty?
37
37
 
38
- map { |f| f.skipped_lines.count }.inject(:+)
38
+ @skipped_lines ||= sum(&:skipped_lines_count)
39
39
  end
40
40
 
41
41
  # Computes the coverage based upon lines covered and lines missed for each file
@@ -46,7 +46,9 @@ module Coverband
46
46
 
47
47
  # Returns the overall amount of relevant lines of code across all files in this list
48
48
  def lines_of_code
49
- covered_lines + missed_lines
49
+ return 0.0 if empty?
50
+
51
+ @lines_of_code ||= sum(&:lines_of_code)
50
52
  end
51
53
 
52
54
  # Computes the coverage based upon lines covered and lines missed
@@ -68,11 +70,17 @@ module Coverband
68
70
  def covered_strength
69
71
  return 0.0 if empty? || lines_of_code.zero?
70
72
 
71
- Float(map { |f| f.covered_strength * f.lines_of_code }.inject(:+) / lines_of_code)
73
+ Float(sum { |f| f.covered_strength * f.lines_of_code } / lines_of_code)
72
74
  end
73
75
 
74
76
  def first_seen_at
75
- map(&:first_updated_at).reject { |el| el.is_a?(String) }.min
77
+ min = nil
78
+ each do |f|
79
+ val = f.first_updated_at
80
+ next if val.is_a?(String)
81
+ min = val if min.nil? || val < min
82
+ end
83
+ min
76
84
  end
77
85
  end
78
86
  end
@@ -73,9 +73,12 @@ module Coverband
73
73
  template("data").result(binding)
74
74
  end
75
75
 
76
+ TEMPLATE_CACHE = {}
77
+ private_constant :TEMPLATE_CACHE
78
+
76
79
  # Returns the an erb instance for the template of given name
77
80
  def template(name)
78
- ERB.new(File.read(File.join(File.dirname(__FILE__), "../../../views/", "#{name}.erb")))
81
+ TEMPLATE_CACHE[name] ||= ERB.new(File.read(File.expand_path("../../../views/#{name}.erb", __dir__)))
79
82
  end
80
83
 
81
84
  def output_path
@@ -119,7 +122,7 @@ module Coverband
119
122
 
120
123
  # Returns a table containing the given source files
121
124
  def formatted_file_list(title, result, source_files, options = {})
122
- title_id = title.gsub(/^[^a-zA-Z]+/, "").gsub(/[^a-zA-Z0-9\-\_]/, "")
125
+ title_id = title.gsub(/^[^a-zA-Z]+/, "").gsub(/[^a-zA-Z0-9\-_]/, "")
123
126
  # Silence a warning by using the following variable to assign to `_`:
124
127
  # "warning: possibly useless use of a variable in void context"
125
128
  # The variable is used by ERB via binding.
@@ -17,24 +17,39 @@ module Coverband
17
17
 
18
18
  def initialize(roots)
19
19
  @cache = {}
20
- @roots = normalize(roots)
20
+ @roots_regexp = Regexp.union(convert_roots(roots))
21
21
  end
22
22
 
23
23
  def convert(file)
24
24
  @cache[file] ||= begin
25
- relative_file = file
26
- @roots.each do |root|
27
- relative_file = file.gsub(/^#{root}/, ".")
28
- break relative_file if relative_file.start_with?(".")
25
+ relative_file = file.sub(@roots_regexp, "./")
26
+
27
+ if relative_file == file && !file.start_with?(".") && File.exist?(file)
28
+ real_file = File.realpath(file)
29
+ new_relative_file = real_file.sub(@roots_regexp, "./")
30
+ relative_file = ((new_relative_file == real_file) ? file : new_relative_file)
29
31
  end
32
+
30
33
  relative_file
31
34
  end
32
35
  end
33
36
 
34
37
  private
35
38
 
36
- def normalize(paths)
37
- paths.map { |root| File.expand_path(root) }
39
+ def convert_roots(roots)
40
+ roots.flat_map { |root|
41
+ items = []
42
+ expanded = File.expand_path(root)
43
+ expanded += "/" unless expanded.end_with?("/")
44
+ items << /^#{Regexp.escape(expanded)}/
45
+
46
+ if File.exist?(root)
47
+ real = File.realpath(root)
48
+ real += "/" unless real.end_with?("/")
49
+ items << /^#{Regexp.escape(real)}/
50
+ end
51
+ items
52
+ }.uniq
38
53
  end
39
54
  end
40
55
  end
@@ -51,8 +51,6 @@ module Coverband
51
51
  # the line-by-line coverage to zero (if relevant) or nil (comments / whitespace etc).
52
52
  def self.add_not_loaded_files(result, tracked_files)
53
53
  if tracked_files
54
- # TODO: Can we get rid of this dup it wastes memory
55
- result = result.dup
56
54
  Dir[tracked_files].each do |file|
57
55
  absolute = File.expand_path(file)
58
56
  result[absolute] ||= {
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ ####
4
+ # Thanks for all the help SimpleCov https://github.com/colszowka/simplecov
5
+ # initial version pulled into Coverband from Simplecov 12/04/2018
6
+ #
7
+ # Representation of a source file including it's coverage data, source code,
8
+ # source lines and featuring helpers to interpret that data.
9
+ ####
10
+ module Coverband
11
+ module Utils
12
+ class SourceFile
13
+ # Representation of a single line in a source file including
14
+ # this specific line's source code, line_number and code coverage,
15
+ # with the coverage being either nil (coverage not applicable, e.g. comment
16
+ # line), 0 (line not covered) or >1 (the amount of times the line was
17
+ # executed)
18
+ class Line
19
+ # The source code for this line. Aliased as :source
20
+ attr_reader :src
21
+ # The line number in the source file. Aliased as :line, :number
22
+ attr_reader :line_number
23
+ # The coverage data for this line: either nil (never), 0 (missed) or >=1 (times covered)
24
+ attr_reader :coverage
25
+ # Whether this line was skipped
26
+ attr_reader :skipped
27
+ # The coverage data posted time for this line: either nil (never), nil (missed) or Time instance (last posted)
28
+ attr_reader :coverage_posted
29
+
30
+ # Lets grab some fancy aliases, shall we?
31
+ alias_method :source, :src
32
+ alias_method :line, :line_number
33
+ alias_method :number, :line_number
34
+
35
+ def initialize(src, line_number, coverage, coverage_posted = nil)
36
+ raise ArgumentError, "Only String accepted for source" unless src.is_a?(String)
37
+ raise ArgumentError, "Only Integer accepted for line_number" unless line_number.is_a?(Integer)
38
+ raise ArgumentError, "Only Integer and nil accepted for coverage" unless coverage.is_a?(Integer) || coverage.nil?
39
+
40
+ @src = src
41
+ @line_number = line_number
42
+ @coverage = coverage
43
+ @skipped = false
44
+ @coverage_posted = coverage_posted
45
+ end
46
+
47
+ # Returns true if this is a line that should have been covered, but was not
48
+ def missed?
49
+ !never? && !skipped? && coverage.zero?
50
+ end
51
+
52
+ # Returns true if this is a line that has been covered
53
+ def covered?
54
+ !never? && !skipped? && coverage.positive?
55
+ end
56
+
57
+ # Returns true if this line is not relevant for coverage
58
+ def never?
59
+ !skipped? && coverage.nil?
60
+ end
61
+
62
+ # Flags this line as skipped
63
+ def skipped!
64
+ @skipped = true
65
+ end
66
+
67
+ # Returns true if this line was skipped, false otherwise. Lines are skipped if they are wrapped with
68
+ # # :nocov: comment lines.
69
+ def skipped?
70
+ skipped
71
+ end
72
+
73
+ # The status of this line - either covered, missed, skipped or never. Useful i.e. for direct use
74
+ # as a css class in report generation
75
+ def status
76
+ return "skipped" if skipped?
77
+ return "never" if never?
78
+ return "missed" if missed?
79
+ "covered" if covered?
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end