coverband 2.0.0.alpha1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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']