coverband 1.3.1 → 1.5.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 +4 -4
- data/.gitignore +1 -0
- data/README.md +39 -6
- data/changes.md +23 -0
- data/coverband.gemspec +2 -1
- data/lib/coverband.rb +12 -11
- data/lib/coverband/adapters/file_store.rb +59 -0
- data/lib/coverband/adapters/memory_cache_store.rb +46 -0
- data/lib/coverband/adapters/redis_store.rb +101 -0
- data/lib/coverband/base.rb +4 -7
- data/lib/coverband/baseline.rb +35 -0
- data/lib/coverband/configuration.rb +20 -5
- data/lib/coverband/reporters/base.rb +150 -0
- data/lib/coverband/reporters/console_report.rb +17 -0
- data/lib/coverband/reporters/simple_cov_report.rb +46 -0
- data/lib/coverband/s3_report_writer.rb +6 -1
- data/lib/coverband/tasks.rb +26 -16
- data/lib/coverband/version.rb +1 -1
- data/test/benchmarks/benchmark.rake +57 -7
- data/test/test_helper.rb +20 -0
- data/test/unit/adapters_file_store_test.rb +41 -0
- data/test/unit/{memory_cache_store_test.rb → adapters_memory_cache_store_test.rb} +12 -12
- data/test/unit/adapters_redis_store_test.rb +164 -0
- data/test/unit/base_test.rb +8 -7
- data/test/unit/baseline_test.rb +50 -0
- data/test/unit/middleware_test.rb +8 -8
- data/test/unit/reports_base_test.rb +140 -0
- data/test/unit/reports_console_test.rb +37 -0
- data/test/unit/reports_simple_cov_test.rb +68 -0
- data/test/unit/s3_report_writer_test.rb +1 -0
- metadata +37 -24
- data/lib/coverband/memory_cache_store.rb +0 -42
- data/lib/coverband/redis_store.rb +0 -57
- data/lib/coverband/reporter.rb +0 -223
- data/test/unit/redis_store_test.rb +0 -107
- data/test/unit/reporter_test.rb +0 -207
@@ -0,0 +1,35 @@
|
|
1
|
+
module Coverband
|
2
|
+
class Baseline
|
3
|
+
|
4
|
+
def self.record
|
5
|
+
require 'coverage'
|
6
|
+
Coverage.start
|
7
|
+
yield
|
8
|
+
|
9
|
+
project_directory = File.expand_path(Coverband.configuration.root)
|
10
|
+
results = Coverage.result
|
11
|
+
results = results.reject { |key, val| !key.match(project_directory) || Coverband.configuration.ignore.any? { |pattern| key.match(/#{pattern}/) } }
|
12
|
+
|
13
|
+
Coverband.configuration.store.save_report(convert_coverage_format(results))
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.parse_baseline(back_compat = nil)
|
17
|
+
Coverband.configuration.store.coverage
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def self.convert_coverage_format(results)
|
23
|
+
file_map = {}
|
24
|
+
results.each_pair do |file, data|
|
25
|
+
lines_map = {}
|
26
|
+
data.each_with_index do |hits, index|
|
27
|
+
lines_map[(index+1)] = hits unless hits.nil?
|
28
|
+
end
|
29
|
+
file_map[file] = lines_map
|
30
|
+
end
|
31
|
+
file_map
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -1,17 +1,19 @@
|
|
1
1
|
module Coverband
|
2
2
|
class Configuration
|
3
3
|
|
4
|
-
attr_accessor :redis, :
|
4
|
+
attr_accessor :redis, :root_paths, :root,
|
5
5
|
:ignore, :percentage, :verbose, :reporter, :stats,
|
6
|
-
:logger, :startup_delay, :
|
7
|
-
:include_gems, :memory_caching, :s3_bucket
|
6
|
+
:logger, :startup_delay, :trace_point_events,
|
7
|
+
:include_gems, :memory_caching, :s3_bucket, :coverage_file, :store
|
8
|
+
|
9
|
+
# deprecated, but leaving to allow old configs to 'just work'
|
10
|
+
# remove for 2.0
|
11
|
+
attr_accessor :coverage_baseline
|
8
12
|
|
9
13
|
def initialize
|
10
14
|
@root = Dir.pwd
|
11
15
|
@redis = nil
|
12
16
|
@stats = nil
|
13
|
-
@coverage_baseline = {}
|
14
|
-
@baseline_file = './tmp/coverband_baseline.json'
|
15
17
|
@root_paths = []
|
16
18
|
@ignore = []
|
17
19
|
@include_gems = false
|
@@ -22,11 +24,24 @@ module Coverband
|
|
22
24
|
@startup_delay = 0
|
23
25
|
@trace_point_events = [:line]
|
24
26
|
@memory_caching = false
|
27
|
+
@coverage_file = nil
|
28
|
+
@store = nil
|
25
29
|
end
|
26
30
|
|
27
31
|
def logger
|
28
32
|
@logger ||= Logger.new(STDOUT)
|
29
33
|
end
|
30
34
|
|
35
|
+
#TODO considering removing @redis / @coveragefile and have user set store directly
|
36
|
+
def store
|
37
|
+
return @store if @store
|
38
|
+
if redis
|
39
|
+
@store = Coverband::Adapters::RedisStore.new(redis)
|
40
|
+
elsif Coverband.configuration.coverage_file
|
41
|
+
@store = Coverband::Adapters::FileStore.new(coverage_file)
|
42
|
+
end
|
43
|
+
@store
|
44
|
+
end
|
45
|
+
|
31
46
|
end
|
32
47
|
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
module Coverband
|
2
|
+
module Reporters
|
3
|
+
class Base
|
4
|
+
|
5
|
+
def self.report(store, options = {})
|
6
|
+
roots = get_roots
|
7
|
+
additional_coverage_data = options.fetch(:additional_scov_data) { [] }
|
8
|
+
|
9
|
+
if Coverband.configuration.verbose
|
10
|
+
Coverband.configuration.logger.info "fixing root: #{roots.join(', ')}"
|
11
|
+
Coverband.configuration.logger.info "additional data:\n #{additional_coverage_data}"
|
12
|
+
end
|
13
|
+
|
14
|
+
scov_style_report = report_scov_with_additional_data(store, additional_coverage_data, roots)
|
15
|
+
|
16
|
+
if Coverband.configuration.verbose
|
17
|
+
Coverband.configuration.logger.info "report:\n #{scov_style_report.inspect}"
|
18
|
+
end
|
19
|
+
scov_style_report
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.get_roots
|
23
|
+
roots = Coverband.configuration.root_paths
|
24
|
+
roots << "#{current_root}/"
|
25
|
+
roots
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.current_root
|
29
|
+
File.expand_path(Coverband.configuration.root)
|
30
|
+
end
|
31
|
+
|
32
|
+
protected
|
33
|
+
|
34
|
+
def self.fix_file_names(report_hash, roots)
|
35
|
+
fixed_report = {} #normalize names across servers
|
36
|
+
report_hash.each_pair do |key, values|
|
37
|
+
filename = filename_from_key(key, roots)
|
38
|
+
fixed_report[filename] = values
|
39
|
+
end
|
40
|
+
fixed_report
|
41
|
+
end
|
42
|
+
|
43
|
+
# > merge_arrays([0,0,1,0,1],[nil,0,1,0,0])
|
44
|
+
# [0,0,1,0,1]
|
45
|
+
def self.merge_arrays(first, second)
|
46
|
+
merged = []
|
47
|
+
longest = first.length > second.length ? first : second
|
48
|
+
|
49
|
+
longest.each_with_index do |line, index|
|
50
|
+
if first[index] || second[index]
|
51
|
+
merged[index] = (first[index].to_i + second[index].to_i)
|
52
|
+
else
|
53
|
+
merged[index] = nil
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
merged
|
58
|
+
end
|
59
|
+
|
60
|
+
# > merge_existing_coverage({"file.rb" => [0,1,2,nil,nil,nil]}, {"file.rb" => [0,1,2,nil,0,1,2]})
|
61
|
+
# expects = {"file.rb" => [0,2,4,nil,0,1,2]}
|
62
|
+
def self.merge_existing_coverage(scov_style_report, existing_coverage)
|
63
|
+
existing_coverage.each_pair do |file_key, existing_lines|
|
64
|
+
next if Coverband.configuration.ignore.any?{ |i| file_key.match(i) }
|
65
|
+
if current_line_hits = scov_style_report[file_key]
|
66
|
+
scov_style_report[file_key] = merge_arrays(current_line_hits, existing_lines)
|
67
|
+
else
|
68
|
+
scov_style_report[file_key] = existing_lines
|
69
|
+
end
|
70
|
+
end
|
71
|
+
scov_style_report
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.filename_from_key(key, roots)
|
75
|
+
filename = key
|
76
|
+
roots.each do |root|
|
77
|
+
filename = filename.gsub(/^#{root}/, './')
|
78
|
+
end
|
79
|
+
# the filename for SimpleCov is expected to be a full path.
|
80
|
+
# roots.last should be roots << current_root}/
|
81
|
+
# a fully expanded path of config.root
|
82
|
+
filename = filename.gsub('./', roots.last)
|
83
|
+
filename
|
84
|
+
end
|
85
|
+
|
86
|
+
# > line_hash(store, 'hearno/script/tester.rb', ['/app/', '/Users/danmayer/projects/hearno/'])
|
87
|
+
# {"/Users/danmayer/projects/hearno/script/tester.rb"=>[1, nil, 1, 2, nil, nil, nil]}
|
88
|
+
def self.line_hash(store, key, roots)
|
89
|
+
filename = filename_from_key(key, roots)
|
90
|
+
if File.exists?(filename)
|
91
|
+
|
92
|
+
count = File.foreach(filename).inject(0) { |c, line| c + 1 }
|
93
|
+
line_array = Array.new(count, nil)
|
94
|
+
|
95
|
+
lines_hit = store.covered_lines_for_file(key)
|
96
|
+
if lines_hit.is_a?(Array)
|
97
|
+
line_array.each_with_index{|_,index| line_array[index] = 1 if lines_hit.include?((index + 1)) }
|
98
|
+
else
|
99
|
+
line_array.each_with_index{|_,index| line_array[index] = (line_array[index].to_i + lines_hit[(index + 1).to_s].to_i) if lines_hit.keys.include?((index + 1).to_s) }
|
100
|
+
end
|
101
|
+
{filename => line_array}
|
102
|
+
else
|
103
|
+
Coverband.configuration.logger.info "file #{filename} not found in project"
|
104
|
+
nil
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.get_current_scov_data_imp(store, roots)
|
109
|
+
scov_style_report = {}
|
110
|
+
|
111
|
+
###
|
112
|
+
# why do we need to merge covered files data?
|
113
|
+
# basically because paths on machines or deployed hosts could be different, so
|
114
|
+
# two different keys could point to the same filename or `line_key`
|
115
|
+
# this logic should be pushed to base report
|
116
|
+
###
|
117
|
+
store.covered_files.each do |key|
|
118
|
+
next if Coverband.configuration.ignore.any?{ |i| key.match(i) }
|
119
|
+
line_data = line_hash(store, key, roots)
|
120
|
+
|
121
|
+
if line_data
|
122
|
+
line_key = line_data.keys.first
|
123
|
+
previous_line_hash = scov_style_report[line_key]
|
124
|
+
|
125
|
+
if previous_line_hash
|
126
|
+
line_data[line_key] = merge_arrays(line_data[line_key], previous_line_hash)
|
127
|
+
end
|
128
|
+
|
129
|
+
scov_style_report.merge!(line_data)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
scov_style_report = fix_file_names(scov_style_report, roots)
|
134
|
+
scov_style_report
|
135
|
+
end
|
136
|
+
|
137
|
+
def self.report_scov_with_additional_data(store, additional_scov_data, roots)
|
138
|
+
scov_style_report = get_current_scov_data_imp(store, roots)
|
139
|
+
|
140
|
+
additional_scov_data.each do |data|
|
141
|
+
scov_style_report = merge_existing_coverage(scov_style_report, data)
|
142
|
+
end
|
143
|
+
|
144
|
+
scov_style_report
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Coverband
|
2
|
+
module Reporters
|
3
|
+
class ConsoleReport < Base
|
4
|
+
|
5
|
+
def self.report(store, options = {})
|
6
|
+
scov_style_report = super(store, options)
|
7
|
+
|
8
|
+
scov_style_report.each_pair do |file, usage|
|
9
|
+
Coverband.configuration.logger.info "#{file}: #{usage}"
|
10
|
+
end
|
11
|
+
scov_style_report
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Coverband
|
2
|
+
module Reporters
|
3
|
+
class SimpleCovReport < Base
|
4
|
+
|
5
|
+
def self.report(store, options = {})
|
6
|
+
begin
|
7
|
+
require 'simplecov'
|
8
|
+
rescue
|
9
|
+
Coverband.configuration.logger.error "coverband requires simplecov in order to generate a report, when configured for the scov report style."
|
10
|
+
return
|
11
|
+
end
|
12
|
+
|
13
|
+
scov_style_report = super(store, options)
|
14
|
+
|
15
|
+
open_report = options.fetch(:open_report) { true }
|
16
|
+
|
17
|
+
# set root to show files if user has simplecov profiles
|
18
|
+
# https://github.com/danmayer/coverband/issues/59
|
19
|
+
SimpleCov.root(current_root)
|
20
|
+
|
21
|
+
# add in files never hit in coverband
|
22
|
+
SimpleCov.track_files "#{current_root}/{app,lib,config}/**/*.{rb,haml,erb,slim}"
|
23
|
+
|
24
|
+
# still apply coverband filters
|
25
|
+
report_files = SimpleCov.add_not_loaded_files(scov_style_report)
|
26
|
+
filtered_report_files = {}
|
27
|
+
report_files.each_pair do |file, data|
|
28
|
+
next if Coverband.configuration.ignore.any?{ |i| file.match(i) }
|
29
|
+
filtered_report_files[file] = data
|
30
|
+
end
|
31
|
+
|
32
|
+
SimpleCov::Result.new(filtered_report_files).format!
|
33
|
+
|
34
|
+
if open_report
|
35
|
+
`open #{SimpleCov.coverage_dir}/index.html`
|
36
|
+
else
|
37
|
+
Coverband.configuration.logger.info "report is ready and viewable: open #{SimpleCov.coverage_dir}/index.html"
|
38
|
+
end
|
39
|
+
|
40
|
+
S3ReportWriter.new(Coverband.configuration.s3_bucket).persist! if Coverband.configuration.s3_bucket
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
@@ -2,6 +2,12 @@ class S3ReportWriter
|
|
2
2
|
|
3
3
|
def initialize(bucket_name)
|
4
4
|
@bucket_name = bucket_name
|
5
|
+
begin
|
6
|
+
require 'aws-sdk'
|
7
|
+
rescue
|
8
|
+
Coverband.configuration.logger.error "coverband requires 'aws-sdk' in order use S3ReportWriter."
|
9
|
+
return
|
10
|
+
end
|
5
11
|
end
|
6
12
|
|
7
13
|
def persist!
|
@@ -26,5 +32,4 @@ class S3ReportWriter
|
|
26
32
|
s3.bucket(@bucket_name)
|
27
33
|
end
|
28
34
|
|
29
|
-
|
30
35
|
end
|
data/lib/coverband/tasks.rb
CHANGED
@@ -2,21 +2,23 @@ namespace :coverband do
|
|
2
2
|
|
3
3
|
desc "record coverband coverage baseline"
|
4
4
|
task :baseline do
|
5
|
-
Coverband::
|
5
|
+
Coverband::Baseline.record {
|
6
6
|
if Rake::Task.tasks.any?{ |key| key.to_s.match(/environment$/) }
|
7
|
+
Coverband.configuration.logger.info "invoking rake environment"
|
7
8
|
Rake::Task['environment'].invoke
|
8
9
|
elsif Rake::Task.tasks.any?{ |key| key.to_s.match(/env$/) }
|
10
|
+
Coverband.configuration.logger.info "invoking rake env"
|
9
11
|
Rake::Task["env"].invoke
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
12
|
+
end
|
13
|
+
|
14
|
+
baseline_files = [File.expand_path('./config/boot.rb', Dir.pwd),
|
15
|
+
File.expand_path('./config/application.rb', Dir.pwd),
|
16
|
+
File.expand_path('./config/environment.rb', Dir.pwd)]
|
17
|
+
|
18
|
+
baseline_files.each do |baseline_file|
|
19
|
+
if File.exists?(baseline_file)
|
20
|
+
require baseline_file
|
21
|
+
end
|
20
22
|
end
|
21
23
|
if defined? Rails
|
22
24
|
Dir.glob("#{Rails.root}/app/**/*.rb").sort.each { |file|
|
@@ -44,7 +46,11 @@ namespace :coverband do
|
|
44
46
|
###
|
45
47
|
desc "report runtime coverband code coverage"
|
46
48
|
task :coverage => :environment do
|
47
|
-
Coverband
|
49
|
+
if Coverband.configuration.reporter=='scov'
|
50
|
+
Coverband::Reporters::SimpleCovReport.report(Coverband.configuration.store)
|
51
|
+
else
|
52
|
+
Coverband::Reporters::ConsoleReport.report(Coverband.configuration.store)
|
53
|
+
end
|
48
54
|
end
|
49
55
|
|
50
56
|
def clear_simplecov_filters
|
@@ -55,18 +61,22 @@ namespace :coverband do
|
|
55
61
|
|
56
62
|
desc "report runtime coverband code coverage after disabling simplecov filters"
|
57
63
|
task :coverage_no_filters => :environment do
|
58
|
-
|
59
|
-
|
64
|
+
if Coverband.configuration.reporter=='scov'
|
65
|
+
clear_simplecov_filters
|
66
|
+
Coverband::Reporters::SimpleCovReport.report(Coverband.configuration.store)
|
67
|
+
else
|
68
|
+
puts "coverage without filters only makes sense for SimpleCov reports"
|
69
|
+
end
|
60
70
|
end
|
61
71
|
|
62
72
|
###
|
63
73
|
# You likely want to clear coverage after significant code changes.
|
64
74
|
# You may want to have a hook that saves current coverband data on deploy
|
65
|
-
# and then resets the
|
75
|
+
# and then resets the coverband store data.
|
66
76
|
###
|
67
77
|
desc "reset coverband coverage data"
|
68
78
|
task :clear => :environment do
|
69
|
-
Coverband
|
79
|
+
Coverband.configuration.store.clear!
|
70
80
|
end
|
71
81
|
|
72
82
|
end
|
data/lib/coverband/version.rb
CHANGED
@@ -14,7 +14,7 @@ namespace :benchmarks do
|
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
|
-
desc 'set up coverband'
|
17
|
+
desc 'set up coverband default redis'
|
18
18
|
task :setup do
|
19
19
|
clone_classifier
|
20
20
|
$LOAD_PATH.unshift(File.join(classifier_dir, 'lib'))
|
@@ -34,6 +34,42 @@ namespace :benchmarks do
|
|
34
34
|
|
35
35
|
end
|
36
36
|
|
37
|
+
desc 'set up coverband redis array'
|
38
|
+
task :setup_array do
|
39
|
+
clone_classifier
|
40
|
+
$LOAD_PATH.unshift(File.join(classifier_dir, 'lib'))
|
41
|
+
require 'benchmark'
|
42
|
+
require 'classifier-reborn'
|
43
|
+
|
44
|
+
Coverband.configure do |config|
|
45
|
+
config.redis = Redis.new
|
46
|
+
config.root = Dir.pwd
|
47
|
+
config.startup_delay = 0
|
48
|
+
config.percentage = 100.0
|
49
|
+
config.logger = $stdout
|
50
|
+
config.verbose = false
|
51
|
+
config.store = Coverband::Adapters::RedisStore.new(Redis.new, array: true)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
desc 'set up coverband filestore'
|
56
|
+
task :setup_file do
|
57
|
+
clone_classifier
|
58
|
+
$LOAD_PATH.unshift(File.join(classifier_dir, 'lib'))
|
59
|
+
require 'benchmark'
|
60
|
+
require 'classifier-reborn'
|
61
|
+
|
62
|
+
Coverband.configure do |config|
|
63
|
+
config.redis = nil
|
64
|
+
config.store = nil
|
65
|
+
config.root = Dir.pwd
|
66
|
+
config.startup_delay = 0
|
67
|
+
config.percentage = 100.0
|
68
|
+
config.logger = $stdout
|
69
|
+
config.verbose = false
|
70
|
+
config.coverage_file = '/tmp/benchmark_store.json'
|
71
|
+
end
|
72
|
+
end
|
37
73
|
|
38
74
|
def bayes_classification
|
39
75
|
b = ClassifierReborn::Bayes.new 'Interesting', 'Uninteresting'
|
@@ -65,11 +101,9 @@ namespace :benchmarks do
|
|
65
101
|
10_000.times { Dog.new.bark }
|
66
102
|
end
|
67
103
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
task :run => :setup do
|
72
|
-
SAMPLINGS = 3
|
104
|
+
def run_work
|
105
|
+
puts "benchmark for: #{Coverband.configuration.inspect}"
|
106
|
+
puts "store: #{Coverband.configuration.store.inspect}"
|
73
107
|
bm = Benchmark.bm(15) do |x|
|
74
108
|
|
75
109
|
x.report 'coverband' do
|
@@ -85,10 +119,26 @@ namespace :benchmarks do
|
|
85
119
|
work
|
86
120
|
end
|
87
121
|
end
|
88
|
-
|
89
122
|
end
|
90
123
|
end
|
91
124
|
|
125
|
+
desc 'runs benchmarks on default redis setup'
|
126
|
+
task :run => :setup do
|
127
|
+
SAMPLINGS = 5
|
128
|
+
run_work
|
129
|
+
end
|
130
|
+
|
131
|
+
desc 'runs benchmarks redis array'
|
132
|
+
task :run_array => :setup_array do
|
133
|
+
SAMPLINGS = 5
|
134
|
+
run_work
|
135
|
+
end
|
136
|
+
|
137
|
+
desc 'runs benchmarks file store'
|
138
|
+
task :run_file => :setup_file do
|
139
|
+
SAMPLINGS = 5
|
140
|
+
run_work
|
141
|
+
end
|
92
142
|
end
|
93
143
|
|
94
144
|
desc "runs benchmarks"
|