coverband 1.3.1 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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"
|