coverband 1.3.1 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/README.md +39 -6
  4. data/changes.md +23 -0
  5. data/coverband.gemspec +2 -1
  6. data/lib/coverband.rb +12 -11
  7. data/lib/coverband/adapters/file_store.rb +59 -0
  8. data/lib/coverband/adapters/memory_cache_store.rb +46 -0
  9. data/lib/coverband/adapters/redis_store.rb +101 -0
  10. data/lib/coverband/base.rb +4 -7
  11. data/lib/coverband/baseline.rb +35 -0
  12. data/lib/coverband/configuration.rb +20 -5
  13. data/lib/coverband/reporters/base.rb +150 -0
  14. data/lib/coverband/reporters/console_report.rb +17 -0
  15. data/lib/coverband/reporters/simple_cov_report.rb +46 -0
  16. data/lib/coverband/s3_report_writer.rb +6 -1
  17. data/lib/coverband/tasks.rb +26 -16
  18. data/lib/coverband/version.rb +1 -1
  19. data/test/benchmarks/benchmark.rake +57 -7
  20. data/test/test_helper.rb +20 -0
  21. data/test/unit/adapters_file_store_test.rb +41 -0
  22. data/test/unit/{memory_cache_store_test.rb → adapters_memory_cache_store_test.rb} +12 -12
  23. data/test/unit/adapters_redis_store_test.rb +164 -0
  24. data/test/unit/base_test.rb +8 -7
  25. data/test/unit/baseline_test.rb +50 -0
  26. data/test/unit/middleware_test.rb +8 -8
  27. data/test/unit/reports_base_test.rb +140 -0
  28. data/test/unit/reports_console_test.rb +37 -0
  29. data/test/unit/reports_simple_cov_test.rb +68 -0
  30. data/test/unit/s3_report_writer_test.rb +1 -0
  31. metadata +37 -24
  32. data/lib/coverband/memory_cache_store.rb +0 -42
  33. data/lib/coverband/redis_store.rb +0 -57
  34. data/lib/coverband/reporter.rb +0 -223
  35. data/test/unit/redis_store_test.rb +0 -107
  36. 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, :coverage_baseline, :root_paths, :root,
4
+ attr_accessor :redis, :root_paths, :root,
5
5
  :ignore, :percentage, :verbose, :reporter, :stats,
6
- :logger, :startup_delay, :baseline_file, :trace_point_events,
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
@@ -2,21 +2,23 @@ namespace :coverband do
2
2
 
3
3
  desc "record coverband coverage baseline"
4
4
  task :baseline do
5
- Coverband::Reporter.baseline {
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
- else
11
- baseline_files = [File.expand_path('./config/boot.rb', Dir.pwd),
12
- File.expand_path('./config/application.rb', Dir.pwd),
13
- File.expand_path('./config/environment.rb', Dir.pwd)]
14
-
15
- baseline_files.each do |baseline_file|
16
- if File.exists?(baseline_file)
17
- require baseline_file
18
- end
19
- end
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::Reporter.report
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
- clear_simplecov_filters
59
- Coverband::Reporter.report
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 redis data.
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::Reporter.clear_coverage
79
+ Coverband.configuration.store.clear!
70
80
  end
71
81
 
72
82
  end
@@ -1,3 +1,3 @@
1
1
  module Coverband
2
- VERSION = "1.3.1"
2
+ VERSION = "1.5.0"
3
3
  end
@@ -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
- desc 'runs benchmarks'
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"