coverband 2.0.0.alpha1 → 2.0.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.
data/Rakefile CHANGED
@@ -14,3 +14,8 @@ Rake::TestTask.new(:test) do |test|
14
14
  test.test_files = FileList['test/unit/*_test.rb']
15
15
  test.verbose = true
16
16
  end
17
+
18
+ desc 'load irb with this gem'
19
+ task :console do
20
+ exec 'irb -I lib -r coverband'
21
+ end
data/changes.md CHANGED
@@ -1,6 +1,37 @@
1
+ # Future Roadmap
2
+
3
+ ### Coverband 3.0
4
+
5
+ Will be the fully modern release that drops maintenance legacy support in favor of increased performance, ease of use, and maintainability.
6
+
7
+ * expects to drop Tracepoint collection engine
8
+ * expects to drop anything below Ruby 2.3
9
+ * Release will be aimed as significantly simplifying ease of use
10
+ * expects to drop the concept of baseline recordings
11
+ * improve support for eager-loading
12
+ * add built-in support for easy loading via Railties
13
+ * expects to add safe list support to force reload files one wants coverage on that may happen outside of the standard load order
14
+ * built in support for activejob, sidekiq, and other common frameworks
15
+
16
+ # Released
17
+
18
+ ### 2.0.0
19
+
20
+ Major release with various backwards compatibility breaking changes (generally related to the configuration). The 2.0 lifecycle will act as a mostly easy upgrade that supports past users looking to move to the much faster new Coverage Adapter.
21
+
22
+ * Continues to support Ruby 2.0 and up
23
+ * supports multiple collect engines, introducing the concept of multiple collector adapters
24
+ * extends the concepts of multiple storage adapters, enabling additional authors to help support Kafka, graphite, other adapters
25
+ * old require based loading, but working towards deprecating the entire baseline concept
26
+ * Introduces massive performance enhancements by moving to Ruby `Coverage` based collection
27
+ * Opposed to sampling this is now a reporting frequency, when using `Coverage` collector
28
+ * Reduced configuration complexity
29
+ * Refactoring the code preparing for more varied storage and reporting options
30
+ * Drop Redis as a gem runtime_dependency
31
+
1
32
  ### 1.5.0
2
33
 
3
- This is a major release with significant refactoring a stepping stone for a 2.0 release.
34
+ This is a significant release with significant refactoring a stepping stone for a 2.0 release.
4
35
 
5
36
  * staging a changes.md document!
6
37
  * refactored out full abstraction for stores
@@ -20,4 +51,4 @@ This is a major release with significant refactoring a stepping stone for a 2.0
20
51
 
21
52
  * This was a small fix release addressing some issues
22
53
  * mostly readme updates
23
- * last release prior to having a changes document!
54
+ * last release prior to having a changes document!
@@ -28,11 +28,14 @@ Gem::Specification.new do |spec|
28
28
  spec.add_development_dependency 'rake'
29
29
  spec.add_development_dependency 'sinatra'
30
30
  spec.add_development_dependency 'test-unit'
31
+ spec.add_development_dependency 'redis'
32
+ spec.add_development_dependency 'benchmark-ips'
31
33
  # add when debugging
32
34
  # require 'byebug'; byebug
33
- spec.add_development_dependency 'byebug'
35
+ #spec.add_development_dependency 'byebug'
36
+ # deprecate when dropping support for older ruby
34
37
  spec.add_runtime_dependency 'json'
38
+ # todo make an optional dependency for simplecov reports
39
+ # also likely should just require simplecov-html not the whole lib
35
40
  spec.add_runtime_dependency 'simplecov', '> 0.11.1'
36
- # TODO: make redis optional dependancy as we add additional adapters
37
- spec.add_runtime_dependency 'redis'
38
41
  end
@@ -2,11 +2,10 @@
2
2
 
3
3
  require 'logger'
4
4
  require 'json'
5
- # TODO: move to only be request if using redis store
6
- require 'redis'
7
5
 
8
6
  require 'coverband/version'
9
7
  require 'coverband/configuration'
8
+ require 'coverband/adapters/base'
10
9
  require 'coverband/adapters/redis_store'
11
10
  require 'coverband/adapters/memory_cache_store'
12
11
  require 'coverband/adapters/file_store'
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coverband
4
+ module Adapters
5
+ class Base
6
+ def initialize
7
+ raise 'abstract'
8
+ end
9
+
10
+ def clear!
11
+ raise 'abstract'
12
+ end
13
+
14
+ def save_report(report)
15
+ raise 'abstract'
16
+ end
17
+
18
+ def coverage
19
+ raise 'abstract'
20
+ end
21
+
22
+ def covered_files
23
+ raise 'abstract'
24
+ end
25
+
26
+ def covered_lines_for_file(file)
27
+ raise 'abstract'
28
+ end
29
+ end
30
+ end
31
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Coverband
4
4
  module Adapters
5
- class FileStore
5
+ class FileStore < Base
6
6
  attr_accessor :path
7
7
 
8
8
  def initialize(path, _opts = {})
@@ -1,8 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ ###
4
+ # TODO current benchmarks aren't showing much advantage from this wrapped cache approach
5
+ # re-evaluate before 2.0.0 release
6
+ ###
3
7
  module Coverband
4
8
  module Adapters
5
- class MemoryCacheStore
9
+ class MemoryCacheStore < Base
6
10
  attr_accessor :store
7
11
 
8
12
  def initialize(store)
@@ -2,8 +2,8 @@
2
2
 
3
3
  module Coverband
4
4
  module Adapters
5
- class RedisStore
6
- BASE_KEY = 'coverband1'
5
+ class RedisStore < Base
6
+ BASE_KEY = 'coverband2'
7
7
 
8
8
  def initialize(redis, opts = {})
9
9
  @redis = redis
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ ####
4
+ # TODO refactor this along with the coverage and trace collector
5
+ ####
3
6
  module Coverband
4
7
  module Collectors
5
8
  class Base
@@ -20,7 +23,7 @@ module Coverband
20
23
 
21
24
  def stop
22
25
  @enabled = false
23
- unset_tracer
26
+ stop_coverage
24
27
  end
25
28
 
26
29
  def sample
@@ -49,7 +52,6 @@ module Coverband
49
52
  @sample_percentage = Coverband.configuration.percentage
50
53
  @store = Coverband.configuration.store
51
54
  @store = Coverband::Adapters::MemoryCacheStore.new(@store) if Coverband.configuration.memory_caching
52
- @stats = Coverband.configuration.stats
53
55
  @verbose = Coverband.configuration.verbose
54
56
  @logger = Coverband.configuration.logger
55
57
  @current_thread = Thread.current
@@ -67,19 +69,11 @@ module Coverband
67
69
  end
68
70
 
69
71
  def record_coverage
70
- if @enabled && !failed_recently?
71
- set_tracer
72
- else
73
- unset_tracer
74
- end
75
- # support old ruby before &. safe digging
76
- @stats.increment "coverband.request.recorded.#{@enabled}" if @stats
77
- rescue RuntimeError => err
78
- failed!
79
- if @verbose
80
- @logger.info 'error stating recording coverage'
81
- @logger.info "error: #{err.inspect} #{err.message}"
82
- end
72
+ raise 'abstract'
73
+ end
74
+
75
+ def stop_coverage
76
+ raise 'abstract'
83
77
  end
84
78
 
85
79
  def report_coverage
@@ -92,14 +86,6 @@ module Coverband
92
86
  @ignore_patterns.none? { |pattern| file.include?(pattern) } && file.start_with?(@project_directory)
93
87
  end
94
88
 
95
- def set_tracer
96
- raise 'abstract'
97
- end
98
-
99
- def unset_tracer
100
- raise 'abstract'
101
- end
102
-
103
89
  def output_file_line_usage
104
90
  @logger.info 'coverband debug coverband file:line usage:'
105
91
  @file_line_usage.sort_by { |_key, value| value.length }.each do |pair|
@@ -7,6 +7,10 @@ module Coverband
7
7
  # noop
8
8
  end
9
9
 
10
+ def stop_coverage
11
+ # noop
12
+ end
13
+
10
14
  def report_coverage
11
15
  unless @enabled
12
16
  @logger.info 'coverage disabled' if @verbose
@@ -32,16 +36,7 @@ module Coverband
32
36
  end
33
37
 
34
38
  if @store
35
- if @stats
36
- @before_time = Time.now
37
- @stats.count 'coverband.files.recorded_files', @file_line_usage.length
38
- end
39
-
40
39
  @store.save_report(@file_line_usage)
41
- if @stats
42
- @time_spent = Time.now - @before_time
43
- @stats.timing 'coverband.files.recorded_time', @time_spent
44
- end
45
40
  @file_line_usage.clear
46
41
  elsif @verbose
47
42
  @logger.info 'coverage report: '
@@ -59,16 +54,6 @@ module Coverband
59
54
  end
60
55
  end
61
56
 
62
- protected
63
-
64
- def set_tracer
65
- # no op
66
- end
67
-
68
- def unset_tracer
69
- # no op
70
- end
71
-
72
57
  private
73
58
 
74
59
  def array_diff(latest, original)
@@ -97,22 +82,6 @@ module Coverband
97
82
  new_results = current_coverage
98
83
  end
99
84
 
100
- # if current_coverage['/Users/danmayer/projects/coverage_rails_benchmark/app/controllers/posts_controller.rb']
101
- # puts "total"
102
- # puts current_coverage['/Users/danmayer/projects/coverage_rails_benchmark/app/controllers/posts_controller.rb'].inspect
103
- # end
104
- #
105
- #
106
- # if previous_results && previous_results['/Users/danmayer/projects/coverage_rails_benchmark/app/controllers/posts_controller.rb']
107
- # puts "prev"
108
- # puts previous_results['/Users/danmayer/projects/coverage_rails_benchmark/app/controllers/posts_controller.rb'].inspect
109
- # end
110
- #
111
- # if new_results['/Users/danmayer/projects/coverage_rails_benchmark/app/controllers/posts_controller.rb']
112
- # puts "new"
113
- # puts new_results['/Users/danmayer/projects/coverage_rails_benchmark/app/controllers/posts_controller.rb'].inspect
114
- # end
115
-
116
85
  add_previous_results(current_coverage)
117
86
  new_results.dup
118
87
  end
@@ -7,10 +7,29 @@ module Coverband
7
7
  def reset_instance
8
8
  super
9
9
  @tracer_set = false
10
+ @trace_point_events = [:line]
10
11
  @trace = create_trace_point
11
12
  self
12
13
  end
13
14
 
15
+ def record_coverage
16
+ if @enabled && !failed_recently?
17
+ set_tracer
18
+ else
19
+ unset_tracer
20
+ end
21
+ rescue RuntimeError => err
22
+ failed!
23
+ if @verbose
24
+ @logger.info 'error stating recording coverage'
25
+ @logger.info "error: #{err.inspect} #{err.message}"
26
+ end
27
+ end
28
+
29
+ def stop_coverage
30
+ unset_tracer
31
+ end
32
+
14
33
  def report_coverage
15
34
  unless @enabled
16
35
  @logger.info 'coverage disabled' if @verbose
@@ -30,15 +49,7 @@ module Coverband
30
49
  end
31
50
 
32
51
  if @store
33
- if @stats
34
- @before_time = Time.now
35
- @stats.count 'coverband.files.recorded_files', @file_line_usage.length
36
- end
37
52
  @store.save_report(@file_line_usage)
38
- if @stats
39
- @time_spent = Time.now - @before_time
40
- @stats.timing 'coverband.files.recorded_time', @time_spent
41
- end
42
53
  @file_line_usage.clear
43
54
  elsif @verbose
44
55
  @logger.info 'coverage report: '
@@ -82,9 +93,10 @@ module Coverband
82
93
  end
83
94
 
84
95
  def create_trace_point
85
- TracePoint.new(*Coverband.configuration.trace_point_events) do |tp|
96
+ TracePoint.new(*@trace_point_events) do |tp|
86
97
  if Thread.current == @current_thread
87
98
  file = tp.path
99
+
88
100
  unless @ignored_files.include?(file)
89
101
  if track_file?(file)
90
102
  add_file(file, tp.lineno)
@@ -3,16 +3,15 @@
3
3
  module Coverband
4
4
  class Configuration
5
5
  attr_accessor :redis, :root_paths, :root,
6
- :ignore, :additional_files, :percentage, :verbose, :reporter,
7
- :stats, :startup_delay, :trace_point_events,
8
- :include_gems, :memory_caching, :s3_bucket, :coverage_file,
6
+ :ignore, :additional_files, :percentage, :verbose,
7
+ :reporter, :startup_delay, :memory_caching,
8
+ :include_gems, :s3_bucket,
9
9
  :collector, :disable_on_failure_for
10
- attr_writer :logger, :store
10
+ attr_writer :logger
11
11
 
12
12
  def initialize
13
13
  @root = Dir.pwd
14
14
  @redis = nil
15
- @stats = nil
16
15
  @root_paths = []
17
16
  @ignore = []
18
17
  @additional_files = []
@@ -20,12 +19,14 @@ module Coverband
20
19
  @percentage = 0.0
21
20
  @verbose = false
22
21
  @reporter = 'scov'
23
- @collector = 'trace'
22
+ if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.3.0')
23
+ @collector = 'trace'
24
+ else
25
+ @collector = 'coverage'
26
+ end
24
27
  @logger = Logger.new(STDOUT)
25
28
  @startup_delay = 0
26
- @trace_point_events = [:line]
27
29
  @memory_caching = false
28
- @coverage_file = nil
29
30
  @store = nil
30
31
  @disable_on_failure_for = nil
31
32
  end
@@ -34,15 +35,19 @@ module Coverband
34
35
  @logger ||= Logger.new(STDOUT)
35
36
  end
36
37
 
37
- # TODO: considering removing @redis / @coveragefile and have user set store directly
38
38
  def store
39
39
  return @store if @store
40
- if redis
40
+ raise 'no valid store configured'
41
+ end
42
+
43
+ def store=(store)
44
+ if store.is_a?(Coverband::Adapters::Base)
45
+ @store = store
46
+ elsif defined?(Redis) && store.is_a?(Redis)
41
47
  @store = Coverband::Adapters::RedisStore.new(redis)
42
- elsif Coverband.configuration.coverage_file
48
+ elsif store.is_a?(String)
43
49
  @store = Coverband::Adapters::FileStore.new(coverage_file)
44
50
  end
45
- @store
46
51
  end
47
52
  end
48
53
  end
@@ -4,7 +4,7 @@ require 'sinatra/base'
4
4
 
5
5
  module Coverband
6
6
  class S3Web < Sinatra::Base
7
- set :public_folder, proc { File.expand_path('public', Gem::Specification.find_by_name('simplecov-html').full_gem_path) }
7
+ set :public_folder, proc(){ File.expand_path('public', Gem::Specification.find_by_name('simplecov-html').full_gem_path) }
8
8
 
9
9
  get '/' do
10
10
  s3.get_object(bucket: Coverband.configuration.s3_bucket, key: 'coverband/index.html').body.read
@@ -13,6 +13,12 @@ module Coverband
13
13
  private
14
14
 
15
15
  def s3
16
+ begin
17
+ require 'aws-sdk'
18
+ rescue StandardError
19
+ Coverband.configuration.logger.error "coverband requires 'aws-sdk' in order use S3ReportWriter."
20
+ return
21
+ end
16
22
  @s3 ||= Aws::S3::Client.new
17
23
  end
18
24
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Coverband
4
- VERSION = '2.0.0.alpha1'
4
+ VERSION = '2.0.0'
5
5
  end
@@ -1,10 +1,34 @@
1
1
  # frozen_string_literal: true
2
+ namespace :benchmarks do
2
3
 
3
- require 'coverband'
4
- require 'redis'
5
- require File.join(File.dirname(__FILE__), 'dog')
4
+ # https://github.com/evanphx/benchmark-ips
5
+ # Enable and start GC before each job run. Disable GC afterwards.
6
+ #
7
+ # Inspired by https://www.omniref.com/ruby/2.2.1/symbols/Benchmark/bm?#annotation=4095926&line=182
8
+ class GCSuite
9
+ def warming(*)
10
+ run_gc
11
+ end
12
+
13
+ def running(*)
14
+ run_gc
15
+ end
16
+
17
+ def warmup_stats(*)
18
+ end
19
+
20
+ def add_report(*)
21
+ end
22
+
23
+ private
24
+
25
+ def run_gc
26
+ GC.enable
27
+ GC.start
28
+ GC.disable
29
+ end
30
+ end
6
31
 
7
- namespace :benchmarks do
8
32
  def classifier_dir
9
33
  File.join(File.dirname(__FILE__), 'classifier-reborn')
10
34
  end
@@ -17,59 +41,70 @@ namespace :benchmarks do
17
41
  # rubocop:enable Style/IfUnlessModifier
18
42
  end
19
43
 
20
- desc 'set up coverband default redis'
44
+ desc 'setup standard benchmark'
21
45
  task :setup do
22
46
  clone_classifier
23
47
  $LOAD_PATH.unshift(File.join(classifier_dir, 'lib'))
24
48
  require 'benchmark'
49
+ require 'benchmark/ips'
50
+
51
+ # TODO ok this is interesting and weird
52
+ # basically the earlier I require coverage and
53
+ # then require files the larger perf impact
54
+ # this is somewhat expected but can lead to significant perf diffs
55
+ # for example moving `require 'classifier-reborn'` below the coverage.start
56
+ # results in 1.5x slower vs "difference falls within error"
57
+ # moving from 5 second of time to 12 still shows slower based on when classifier is required
58
+ # make sure to be plugged in while benchmarking ;) Otherwise you get very unreliable results
25
59
  require 'classifier-reborn'
60
+ if ENV['COVERAGE']
61
+ puts 'Coverage library loaded and started'
62
+ require 'coverage'
63
+ ::Coverage.start
64
+ end
65
+ require 'redis'
66
+ require 'coverband'
67
+ require File.join(File.dirname(__FILE__), 'dog')
68
+ end
26
69
 
70
+ desc 'set up coverband tracepoint Redis'
71
+ task :setup_redis do
27
72
  Coverband.configure do |config|
28
73
  config.redis = Redis.new
29
74
  config.root = Dir.pwd
30
- config.startup_delay = 0
31
75
  config.percentage = 100.0
32
76
  config.logger = $stdout
33
- config.verbose = false
34
- # config.memory_caching = true
35
- # config.trace_point_events = [:call]
77
+ config.collector = 'trace'
78
+ config.memory_caching = ENV['MEMORY_CACHE'] ? true : false
79
+ config.store = Coverband::Adapters::RedisStore.new(Redis.new)
36
80
  end
37
81
  end
38
82
 
39
- desc 'set up coverband with coverage redis'
40
- task :setup_coverage do
41
- clone_classifier
42
- $LOAD_PATH.unshift(File.join(classifier_dir, 'lib'))
43
- require 'benchmark'
44
- require 'classifier-reborn'
45
-
83
+ desc 'set up coverband tracepoint filestore'
84
+ task :setup_file do
46
85
  Coverband.configure do |config|
47
- config.redis = Redis.new
48
86
  config.root = Dir.pwd
49
- config.startup_delay = 0
50
87
  config.percentage = 100.0
51
88
  config.logger = $stdout
52
- config.verbose = false
53
- config.collector = 'coverage'
89
+ config.collector = 'trace'
90
+ config.memory_caching = ENV['MEMORY_CACHE'] ? true : false
91
+ config.store = Coverband::Adapters::FileStore.new('/tmp/benchmark_store.json')
54
92
  end
55
93
  end
56
94
 
57
- desc 'set up coverband filestore'
58
- task :setup_file do
59
- clone_classifier
60
- $LOAD_PATH.unshift(File.join(classifier_dir, 'lib'))
61
- require 'benchmark'
62
- require 'classifier-reborn'
63
-
95
+ ###
96
+ # This benchmark always needs to be run last
97
+ # as requiring coverage changes how Ruby interprets the code
98
+ ###
99
+ desc 'set up coverband with coverage Redis'
100
+ task :setup_coverage do
64
101
  Coverband.configure do |config|
65
- config.redis = nil
66
- config.store = nil
67
102
  config.root = Dir.pwd
68
- config.startup_delay = 0
69
103
  config.percentage = 100.0
70
104
  config.logger = $stdout
71
- config.verbose = false
72
- config.coverage_file = '/tmp/benchmark_store.json'
105
+ config.collector = 'coverage'
106
+ config.memory_caching = ENV['MEMORY_CACHE'] ? true : false
107
+ config.store = Coverband::Adapters::RedisStore.new(Redis.new)
73
108
  end
74
109
  end
75
110
 
@@ -103,49 +138,52 @@ namespace :benchmarks do
103
138
  10_000.times { Dog.new.bark }
104
139
  end
105
140
 
106
- def run_work
107
- puts "benchmark for: #{Coverband.configuration.inspect}"
108
- puts "store: #{Coverband.configuration.store.inspect}"
109
- Benchmark.bm(15) do |x|
141
+ def run_work(hold_work = false)
142
+ suite = GCSuite.new
143
+ #puts "benchmark for: #{Coverband.configuration.inspect}"
144
+ #puts "store: #{Coverband.configuration.store.inspect}"
145
+ Benchmark.ips do |x|
146
+ x.config(:time => 12, :warmup => 5, :suite => suite)
110
147
  x.report 'coverband' do
111
- SAMPLINGS.times do
112
- Coverband::Collectors::Base.instance.sample do
113
- work
114
- end
148
+ Coverband::Collectors::Base.instance.sample do
149
+ work
115
150
  end
116
151
  end
117
-
152
+ Coverband::Collectors::Base.instance.stop
118
153
  x.report 'no coverband' do
119
- SAMPLINGS.times do
120
- work
121
- end
154
+ work
122
155
  end
156
+ x.hold! 'temp_results' if hold_work
157
+ x.compare!
123
158
  end
124
- Coverband::Collectors::Base.instance.stop
125
159
  Coverband::Collectors::Base.instance.reset_instance
126
160
  end
127
161
 
128
162
  desc 'runs benchmarks on default redis setup'
129
- task run: :setup do
130
- puts 'Coverband tracepoint configured with default redis store'
131
- SAMPLINGS = 5
163
+ task run_redis: [:setup, :setup_redis] do
164
+ puts 'Coverband tracepoint configured with default Redis store'
132
165
  run_work
133
166
  end
134
167
 
135
168
  desc 'runs benchmarks file store'
136
- task run_file: :setup_file do
169
+ task run_file: [:setup, :setup_file] do
137
170
  puts 'Coverband tracepoint configured with file store'
138
- SAMPLINGS = 5
139
171
  run_work
140
172
  end
141
173
 
142
174
  desc 'runs benchmarks coverage'
143
- task run_coverage: :setup_coverage do
144
- puts 'Coverband Coverage configured with to use default redis store'
145
- SAMPLINGS = 5
146
- run_work
175
+ task run_coverage: [:setup, :setup_coverage] do
176
+ puts 'Coverband Coverage configured with to use default Redis store'
177
+ run_work(true)
178
+ end
179
+
180
+ desc 'compare Coverband Ruby Coverage with normal Ruby'
181
+ task :compare_coverage do
182
+ puts 'comparing with Coverage loaded and not, this takes some time for output...'
183
+ puts `COVERAGE=true rake benchmarks:run_coverage`
184
+ puts `rake benchmarks:run_coverage`
147
185
  end
148
186
  end
149
187
 
150
188
  desc 'runs benchmarks'
151
- task benchmarks: ['benchmarks:run_file', 'benchmarks:run', 'benchmarks:run_coverage']
189
+ task benchmarks: ['benchmarks:run_file', 'benchmarks:run_redis', 'benchmarks:compare_coverage']