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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4e4f7c810900bee83c3516d394e950fde6cb5186
|
4
|
+
data.tar.gz: ba60c4f523b5d50e145cf179113dd6431fd6be4c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ca67d3de1ce546e6d5d3050bde171f70ef258b0792a2e871d8d4c4b93c5609f71e85afcb7d39b9a08d582381e6476fe13c791c45d764726eccaada3170dfd25a
|
7
|
+
data.tar.gz: a1543e402bcc3ef6a0c68c478235d34d1794f695ea9c3099eb31f7c4b54b56015622f1fb920804e0c20614118516fabcf12364e5c97a220108e820e53c29683b
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -13,12 +13,31 @@ A gem to measure production code usage, showing each line of code that is execut
|
|
13
13
|
__Notes:__ Latest versions of Coverband drop support for anything less than Ruby 2.0, and Ruby 2.1+ is recommended.
|
14
14
|
|
15
15
|
###### Success:
|
16
|
+
|
16
17
|
After running in production for 30 minutes, we were able very easily delete 2000 LOC after looking through the data. We expect to be able to clean up much more after it has collected more data.
|
17
18
|
|
18
19
|
###### Performance Impact
|
19
20
|
|
20
21
|
The performance impact on Ruby 2.1+ is fairly small and no longer requires a C-extension. Look at the benchmark rake task for specific details.
|
21
22
|
|
23
|
+
## How I use Coverband
|
24
|
+
|
25
|
+
* install coverband.
|
26
|
+
* take baseline: `rake coverband:baseline`
|
27
|
+
* validate baseline with `rake coverband:coverage`
|
28
|
+
* test setup in development (hit endpoints and generate new report)
|
29
|
+
* deploy to staging and verify functionality
|
30
|
+
* deploy to production and verify functionality
|
31
|
+
* every 2 weeks or with major releases
|
32
|
+
* clear old coverage: `rake coverband:clear`
|
33
|
+
* take new baseline: `rake coverband:baseline`
|
34
|
+
* deploy and verify coverage is matching expectations
|
35
|
+
* __COVERAGE DRIFT__
|
36
|
+
* if you never clear you have lines of code drift from when they were recorded
|
37
|
+
* if you clear on every deploy you don't capture as useful of data
|
38
|
+
* there is a tradeoff on accuracy and data value
|
39
|
+
* I recommend clearing after major code changes, significant releases, or some regular schedule.
|
40
|
+
|
22
41
|
## Installation
|
23
42
|
|
24
43
|
Add this line to your application's Gemfile:
|
@@ -82,10 +101,16 @@ You need to configure cover band you can either do that passing in all configura
|
|
82
101
|
#config/coverband.rb
|
83
102
|
Coverband.configure do |config|
|
84
103
|
config.root = Dir.pwd
|
104
|
+
|
85
105
|
if defined? Redis
|
86
106
|
config.redis = Redis.new(:host => 'redis.host.com', :port => 49182, :db => 1)
|
87
107
|
end
|
88
|
-
|
108
|
+
# don't want to use redis, store to file system ;)
|
109
|
+
# config.coverage_file = './tmp/coverband_coverage.json'
|
110
|
+
|
111
|
+
# DEPRECATED now will use redis or file store
|
112
|
+
# config.coverage_baseline = Coverband.parse_baseline
|
113
|
+
|
89
114
|
config.root_paths = ['/app/'] # /app/ is needed for heroku deployments
|
90
115
|
# regex paths can help if you are seeing files duplicated for each capistrano deployment release
|
91
116
|
#config.root_paths = ['/server/apps/my_app/releases/\d+/']
|
@@ -103,6 +128,7 @@ Coverband.configure do |config|
|
|
103
128
|
if defined? Statsd
|
104
129
|
config.stats = Statsd.new('statsd.host.com', 8125)
|
105
130
|
end
|
131
|
+
|
106
132
|
# config options false, true, or 'debug'. Always use false in production
|
107
133
|
# true and debug can give helpful and interesting code usage information
|
108
134
|
# they both increase the performance overhead of the gem a little.
|
@@ -260,7 +286,8 @@ coverband.sample {
|
|
260
286
|
|
261
287
|
### Clearing Line Coverage Data
|
262
288
|
|
263
|
-
After a deploy where code has changed.
|
289
|
+
After a deploy where code has changed significantly.
|
290
|
+
|
264
291
|
The line numbers previously recorded in Redis may no longer match the current state of the files.
|
265
292
|
If being slightly out of sync isn't as important as gathering data over a long period,
|
266
293
|
you can live with minor inconsistency for some files.
|
@@ -300,6 +327,8 @@ If you are trying to debug locally wondering what code is being run during a req
|
|
300
327
|
|
301
328
|
If you are clearing data on every deploy. You might want to write the data out to a file first. Then you could merge the data into the final results later.
|
302
329
|
|
330
|
+
__note:__ I don't actually recommend clearing on every deploy, but only following significant releases where many line numbers would be off. If you follow that approach you don't need to merge data over time as this example shows how.
|
331
|
+
|
303
332
|
```ruby
|
304
333
|
data = JSON.generate Coverband::Reporter.get_current_scov_data
|
305
334
|
File.write("blah.json", data)
|
@@ -321,20 +350,24 @@ If you are working on adding features, PRs, or bugfixes to Coverband this sectio
|
|
321
350
|
|
322
351
|
### Known issues
|
323
352
|
|
353
|
+
* __total fail__ on front end code, because of the precompiled template step basically coverage doesn't work well for `erb`, `slim`, and the like.
|
324
354
|
* If you don't have a baseline recorded your coverage can look odd like you are missing a bunch of data. It would be good if Coverband gave a more actionable warning in this situation.
|
325
355
|
* If you have SimpleCov filters, you need to clear them prior to generating your coverage report. As the filters will be applied to Coverband as well and can often filter out everything we are recording.
|
326
356
|
* the line numbers reported for `ERB` files are often off and aren't considered useful. I recommend filtering out .erb using the `config.ignore` option.
|
357
|
+
* coverage doesn't show for Rails `config/application.rb` or `config/boot.rb` as they get loaded when loading the Rake environment prior to starting to record the baseline..
|
327
358
|
|
328
359
|
## TODO
|
329
360
|
|
361
|
+
* graphite adapters (it would allow passing in date ranges on usage)
|
362
|
+
* perf test for array vs hash
|
363
|
+
* redis pipeline around hash (or batch get then push)
|
364
|
+
* pass in namespace to redis (coverage vs baseline)
|
365
|
+
* what about having baseline a onetime recording into redis no merge later
|
366
|
+
* move to SimpleCov console out, or make similar console tabular output
|
330
367
|
* Fix network performance by logging to files that purge later (like NR) (far more time lost in TracePoint than sending files, hence not a high priority, but would be cool)
|
331
368
|
* Add support for [zadd](http://redis.io/topics/data-types-intro) so one could determine single call versus multiple calls on a line, letting us determine the most executed code in production.
|
332
369
|
* Possibly add ability to record code run for a given route
|
333
370
|
* Improve client code api, around manual usage of sampling (like event usage)
|
334
|
-
* Provide a better lighter example app, to show how to use Coverband.
|
335
|
-
* blank rails app
|
336
|
-
* blank Sinatra app
|
337
|
-
* report on Coverband files that haven't recorded any coverage (find things like events and crons that aren't recording, or dead files)
|
338
371
|
* ability to change the Coverband config at runtime by changing the config pushed to the Redis hash. In memory cache around the changes to only make that call periodically.
|
339
372
|
* Opposed to just showing code usage on a route allow 'tagging' events which would record line coverage for that tag (this would allow tagging all code that modified an ActiveRecord model for example
|
340
373
|
* mountable rack app to view coverage similar to flipper-ui
|
data/changes.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
### 1.5.0
|
2
|
+
|
3
|
+
This is a major release with significant refactoring a stepping stone for a 2.0 release.
|
4
|
+
|
5
|
+
* staging a changes.md document!
|
6
|
+
* refactored out full abstraction for stores
|
7
|
+
* supports hit counts vs binary covered / not covered for lines
|
8
|
+
* this will let you find density of code usage just not if it was used
|
9
|
+
* this is a slight performance hit, so you can fall back to the old system if you want `redisstore.new(@redis, array: true)`
|
10
|
+
* this is the primary new feature in 1.5.0
|
11
|
+
* Redis has configurable base name, so I can safely change storage formats between releases
|
12
|
+
* improved documentation
|
13
|
+
* supports `SimpleCov.root`
|
14
|
+
* show files that were never touched
|
15
|
+
* apply coverband filters to ignore files in report not just collection
|
16
|
+
* improved test coverage
|
17
|
+
* improved benchmarks including support for multiple stores ;)
|
18
|
+
|
19
|
+
### 1.3.1
|
20
|
+
|
21
|
+
* This was a small fix release addressing some issues
|
22
|
+
* mostly readme updates
|
23
|
+
* last release prior to having a changes document!
|
data/coverband.gemspec
CHANGED
@@ -26,8 +26,9 @@ Gem::Specification.new do |spec|
|
|
26
26
|
spec.add_development_dependency "test-unit"
|
27
27
|
spec.add_development_dependency 'sinatra'
|
28
28
|
spec.add_development_dependency 'classifier-reborn'
|
29
|
+
spec.add_development_dependency 'aws-sdk', '~> 2'
|
29
30
|
spec.add_runtime_dependency "simplecov"
|
30
31
|
spec.add_runtime_dependency "json"
|
32
|
+
# TODO make redis optional dependancy as we add additional adapters
|
31
33
|
spec.add_runtime_dependency "redis"
|
32
|
-
spec.add_runtime_dependency 'aws-sdk', '~> 2'
|
33
34
|
end
|
data/lib/coverband.rb
CHANGED
@@ -1,30 +1,31 @@
|
|
1
|
-
require 'redis'
|
2
1
|
require 'logger'
|
3
|
-
require '
|
2
|
+
require 'json'
|
3
|
+
# todo move to only be request if using redis store
|
4
|
+
require 'redis'
|
4
5
|
|
5
6
|
require 'coverband/version'
|
6
7
|
require 'coverband/configuration'
|
7
|
-
require 'coverband/redis_store'
|
8
|
-
require 'coverband/memory_cache_store'
|
8
|
+
require 'coverband/adapters/redis_store'
|
9
|
+
require 'coverband/adapters/memory_cache_store'
|
10
|
+
require 'coverband/adapters/file_store'
|
9
11
|
require 'coverband/base'
|
10
|
-
require 'coverband/
|
12
|
+
require 'coverband/baseline'
|
13
|
+
require 'coverband/reporters/base'
|
14
|
+
require 'coverband/reporters/simple_cov_report'
|
15
|
+
require 'coverband/reporters/console_report'
|
11
16
|
require 'coverband/middleware'
|
12
17
|
require 'coverband/s3_report_writer'
|
13
18
|
|
14
19
|
module Coverband
|
15
|
-
|
16
20
|
CONFIG_FILE = './config/coverband.rb'
|
17
21
|
|
18
22
|
class << self
|
19
23
|
attr_accessor :configuration_data
|
20
24
|
end
|
21
25
|
|
26
|
+
# this method is left for backwards compatibility with existing configs
|
22
27
|
def self.parse_baseline(baseline_file = './tmp/coverband_baseline.json')
|
23
|
-
|
24
|
-
JSON.parse(File.read(baseline_file))
|
25
|
-
else
|
26
|
-
{}
|
27
|
-
end
|
28
|
+
Coverband::Baseline.parse_baseline(baseline_file)
|
28
29
|
end
|
29
30
|
|
30
31
|
def self.configure(file = nil)
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Coverband
|
2
|
+
module Adapters
|
3
|
+
class FileStore
|
4
|
+
attr_accessor :path
|
5
|
+
|
6
|
+
def initialize(path, opts = {})
|
7
|
+
@path = path
|
8
|
+
|
9
|
+
config_dir = File.dirname(@path)
|
10
|
+
Dir.mkdir config_dir unless File.exist?(config_dir)
|
11
|
+
end
|
12
|
+
|
13
|
+
def clear!
|
14
|
+
if File.exist?(path)
|
15
|
+
File.delete(path)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def save_report(report)
|
20
|
+
results = existing_data(path)
|
21
|
+
report.each_pair do |file, values|
|
22
|
+
if results.has_key?(file)
|
23
|
+
# convert the keys to "3" opposed to 3
|
24
|
+
values = JSON.parse(values.to_json)
|
25
|
+
results[file].merge!( values ){|k, old_v, new_v| old_v.to_i + new_v.to_i}
|
26
|
+
else
|
27
|
+
results[file] = values
|
28
|
+
end
|
29
|
+
end
|
30
|
+
File.open(path, 'w') { |f| f.write(results.to_json) }
|
31
|
+
end
|
32
|
+
|
33
|
+
def coverage
|
34
|
+
existing_data(path)
|
35
|
+
end
|
36
|
+
|
37
|
+
def covered_files
|
38
|
+
report = existing_data(path)
|
39
|
+
existing_data(path).merge(report).keys || []
|
40
|
+
end
|
41
|
+
|
42
|
+
def covered_lines_for_file(file)
|
43
|
+
report = existing_data(path)
|
44
|
+
report[file] || []
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def existing_data(path)
|
50
|
+
if File.exist?(path)
|
51
|
+
JSON.parse(File.read(path))
|
52
|
+
else
|
53
|
+
{}
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Coverband
|
2
|
+
module Adapters
|
3
|
+
class MemoryCacheStore
|
4
|
+
attr_accessor :store
|
5
|
+
|
6
|
+
def initialize(store)
|
7
|
+
@store = store
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.reset!
|
11
|
+
files_cache.clear
|
12
|
+
end
|
13
|
+
|
14
|
+
def clear!
|
15
|
+
self.class.reset!
|
16
|
+
end
|
17
|
+
|
18
|
+
def save_report(files)
|
19
|
+
filtered_files = filter(files)
|
20
|
+
store.save_report(filtered_files) if filtered_files.any?
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def self.files_cache
|
26
|
+
@files_cache ||= Hash.new
|
27
|
+
end
|
28
|
+
|
29
|
+
def files_cache
|
30
|
+
self.class.files_cache
|
31
|
+
end
|
32
|
+
|
33
|
+
def filter(files)
|
34
|
+
files.each_with_object(Hash.new) do |(file, lines), filtered_file_hash|
|
35
|
+
#first time we see a file, we pre-init the in memory cache to whatever is in store(redis)
|
36
|
+
line_cache = files_cache[file] ||= Set.new(store.covered_lines_for_file(file))
|
37
|
+
lines.reject! do |line|
|
38
|
+
line_cache.include?(line) ? true : (line_cache << line and false)
|
39
|
+
end
|
40
|
+
filtered_file_hash[file] = lines if lines.any?
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Coverband
|
2
|
+
module Adapters
|
3
|
+
class RedisStore
|
4
|
+
BASE_KEY = 'coverband1'
|
5
|
+
|
6
|
+
def initialize(redis, opts = {})
|
7
|
+
@redis = redis
|
8
|
+
#remove check for coverband 2.0
|
9
|
+
@_sadd_supports_array = recent_gem_version? && recent_server_version?
|
10
|
+
#possibly drop array storage for 2.0
|
11
|
+
@store_as_array = opts.fetch(:array){ false }
|
12
|
+
end
|
13
|
+
|
14
|
+
def clear!
|
15
|
+
@redis.smembers(BASE_KEY).each { |key| @redis.del("#{BASE_KEY}.#{key}") }
|
16
|
+
@redis.del(BASE_KEY)
|
17
|
+
end
|
18
|
+
|
19
|
+
def save_report(report)
|
20
|
+
if @store_as_array
|
21
|
+
redis.pipelined do
|
22
|
+
store_array(BASE_KEY, report.keys)
|
23
|
+
|
24
|
+
report.each do |file, lines|
|
25
|
+
store_array("#{BASE_KEY}.#{file}", lines.keys)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
else
|
29
|
+
store_array(BASE_KEY, report.keys)
|
30
|
+
|
31
|
+
report.each do |file, lines|
|
32
|
+
store_map("#{BASE_KEY}.#{file}", lines)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def coverage
|
38
|
+
data = {}
|
39
|
+
redis.smembers(BASE_KEY).each do |key|
|
40
|
+
data[key] = covered_lines_for_file(key)
|
41
|
+
end
|
42
|
+
data
|
43
|
+
end
|
44
|
+
|
45
|
+
def covered_files
|
46
|
+
redis.smembers(BASE_KEY)
|
47
|
+
end
|
48
|
+
|
49
|
+
def covered_lines_for_file(file)
|
50
|
+
if @store_as_array
|
51
|
+
@redis.smembers("#{BASE_KEY}.#{file}").map(&:to_i)
|
52
|
+
else
|
53
|
+
@redis.hgetall("#{BASE_KEY}.#{file}")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
attr_reader :redis
|
60
|
+
|
61
|
+
def sadd_supports_array?
|
62
|
+
@_sadd_supports_array
|
63
|
+
end
|
64
|
+
|
65
|
+
def store_map(key, values)
|
66
|
+
unless values.empty?
|
67
|
+
existing = redis.hgetall(key)
|
68
|
+
#in redis all keys are strings
|
69
|
+
values = Hash[values.map{|k,val| [k.to_s,val] } ]
|
70
|
+
values.merge!( existing ){|k, old_v, new_v| old_v.to_i + new_v.to_i}
|
71
|
+
redis.mapped_hmset(key, values)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def store_array(key, values)
|
76
|
+
if sadd_supports_array?
|
77
|
+
redis.sadd(key, values) if (values.length > 0)
|
78
|
+
else
|
79
|
+
values.each do |value|
|
80
|
+
redis.sadd(key, value)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
values
|
84
|
+
end
|
85
|
+
|
86
|
+
def recent_server_version?
|
87
|
+
info_data = redis.info
|
88
|
+
if info_data.is_a?(Hash)
|
89
|
+
Gem::Version.new(info_data['redis_version']) >= Gem::Version.new('2.4')
|
90
|
+
else
|
91
|
+
#guess supported
|
92
|
+
true
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def recent_gem_version?
|
97
|
+
Gem::Version.new(Redis::VERSION) >= Gem::Version.new('3.0')
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
data/lib/coverband/base.rb
CHANGED
@@ -45,10 +45,8 @@ module Coverband
|
|
45
45
|
@ignore_patterns = Coverband.configuration.ignore + ["internal:prelude"]
|
46
46
|
@ignore_patterns += ['gems'] unless Coverband.configuration.include_gems
|
47
47
|
@sample_percentage = Coverband.configuration.percentage
|
48
|
-
|
49
|
-
|
50
|
-
@reporter = Coverband::MemoryCacheStore.new(@reporter) if Coverband.configuration.memory_caching
|
51
|
-
end
|
48
|
+
@store = Coverband.configuration.store
|
49
|
+
@store = Coverband::Adapters::MemoryCacheStore.new(@store) if Coverband.configuration.memory_caching
|
52
50
|
@stats = Coverband.configuration.stats
|
53
51
|
@verbose = Coverband.configuration.verbose
|
54
52
|
@logger = Coverband.configuration.logger
|
@@ -95,12 +93,12 @@ module Coverband
|
|
95
93
|
end
|
96
94
|
end
|
97
95
|
|
98
|
-
if @
|
96
|
+
if @store
|
99
97
|
if @stats
|
100
98
|
@before_time = Time.now
|
101
99
|
@stats.count "coverband.files.recorded_files", @file_line_usage.length
|
102
100
|
end
|
103
|
-
@
|
101
|
+
@store.save_report(@file_line_usage)
|
104
102
|
if @stats
|
105
103
|
@time_spent = Time.now - @before_time
|
106
104
|
@stats.timing "coverband.files.recorded_time", @time_spent
|
@@ -123,7 +121,6 @@ module Coverband
|
|
123
121
|
!@ignore_patterns.any?{ |pattern| file.include?(pattern) } && file.start_with?(@project_directory)
|
124
122
|
end
|
125
123
|
|
126
|
-
|
127
124
|
def set_tracer
|
128
125
|
unless @tracer_set
|
129
126
|
@trace.enable
|