coverband 4.2.0 → 4.2.4

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.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +29 -10
  3. data/.travis.yml +16 -8
  4. data/CODE_OF_CONDUCT.md +76 -0
  5. data/Gemfile +3 -1
  6. data/Gemfile.rails4 +1 -0
  7. data/README.md +108 -59
  8. data/Rakefile +19 -3
  9. data/changes.md +85 -10
  10. data/coverband.gemspec +3 -5
  11. data/lib/coverband/adapters/base.rb +64 -45
  12. data/lib/coverband/adapters/file_store.rb +15 -9
  13. data/lib/coverband/adapters/hash_redis_store.rb +190 -0
  14. data/lib/coverband/adapters/redis_store.rb +35 -11
  15. data/lib/coverband/at_exit.rb +2 -11
  16. data/lib/coverband/collectors/coverage.rb +35 -41
  17. data/lib/coverband/collectors/delta.rb +34 -28
  18. data/lib/coverband/collectors/view_tracker.rb +157 -0
  19. data/lib/coverband/configuration.rb +115 -33
  20. data/lib/coverband/integrations/background.rb +12 -4
  21. data/lib/coverband/integrations/{middleware.rb → background_middleware.rb} +2 -6
  22. data/lib/coverband/integrations/bundler.rb +8 -0
  23. data/lib/coverband/integrations/report_middleware.rb +15 -0
  24. data/lib/coverband/integrations/resque.rb +3 -5
  25. data/lib/coverband/reporters/base.rb +40 -16
  26. data/lib/coverband/reporters/console_report.rb +1 -0
  27. data/lib/coverband/reporters/html_report.rb +31 -27
  28. data/lib/coverband/reporters/web.rb +63 -18
  29. data/lib/coverband/utils/absolute_file_converter.rb +47 -0
  30. data/lib/coverband/utils/file_hasher.rb +16 -0
  31. data/lib/coverband/utils/file_list.rb +16 -5
  32. data/lib/coverband/utils/html_formatter.rb +45 -8
  33. data/lib/coverband/utils/lines_classifier.rb +5 -0
  34. data/lib/coverband/utils/railtie.rb +28 -10
  35. data/lib/coverband/utils/relative_file_converter.rb +41 -0
  36. data/lib/coverband/utils/result.rb +6 -39
  37. data/lib/coverband/utils/results.rb +51 -0
  38. data/lib/coverband/utils/s3_report.rb +2 -3
  39. data/lib/coverband/utils/source_file.rb +34 -8
  40. data/lib/coverband/utils/tasks.rb +10 -10
  41. data/lib/coverband/version.rb +5 -1
  42. data/lib/coverband.rb +49 -27
  43. data/lua/install.sh +15 -0
  44. data/lua/lib/persist-coverage.lua +28 -0
  45. data/lua/test/bootstrap.lua +5 -0
  46. data/lua/test/harness.lua +19 -0
  47. data/lua/test/redis-call.lua +55 -0
  48. data/lua/test/test-persist-coverage.lua +132 -0
  49. data/public/application.css +14 -6
  50. data/public/application.js +27 -0
  51. data/test/benchmarks/benchmark.rake +167 -73
  52. data/test/benchmarks/init_rails.rake +10 -0
  53. data/test/coverband/adapters/base_test.rb +73 -42
  54. data/test/coverband/adapters/file_store_test.rb +48 -37
  55. data/test/coverband/adapters/hash_redis_store_test.rb +195 -0
  56. data/test/coverband/adapters/redis_store_test.rb +102 -59
  57. data/test/coverband/at_exit_test.rb +0 -2
  58. data/test/coverband/collectors/coverage_test.rb +71 -13
  59. data/test/coverband/collectors/delta_test.rb +36 -6
  60. data/test/coverband/collectors/view_tracker_test.rb +113 -0
  61. data/test/coverband/configuration_test.rb +47 -7
  62. data/test/coverband/coverband_test.rb +14 -2
  63. data/test/coverband/integrations/background_middleware_test.rb +44 -0
  64. data/test/coverband/integrations/background_test.rb +11 -3
  65. data/test/coverband/integrations/report_middleware_test.rb +44 -0
  66. data/test/coverband/integrations/resque_worker_test.rb +5 -6
  67. data/test/coverband/integrations/test_resque_job.rb +3 -1
  68. data/test/coverband/reporters/base_test.rb +5 -81
  69. data/test/coverband/reporters/console_test.rb +3 -10
  70. data/test/coverband/reporters/html_test.rb +71 -21
  71. data/test/coverband/reporters/web_test.rb +0 -10
  72. data/test/coverband/utils/absolute_file_converter_test.rb +56 -0
  73. data/test/coverband/utils/file_groups_test.rb +11 -5
  74. data/test/coverband/utils/file_hasher_test.rb +29 -0
  75. data/test/coverband/utils/file_list_test.rb +5 -5
  76. data/test/coverband/utils/html_formatter_test.rb +41 -0
  77. data/test/coverband/utils/relative_file_converter_test.rb +39 -0
  78. data/test/coverband/utils/result_test.rb +6 -47
  79. data/test/coverband/utils/results_test.rb +54 -0
  80. data/test/coverband/utils/s3_report_test.rb +2 -0
  81. data/test/coverband/utils/source_file_test.rb +54 -0
  82. data/test/dog.rb.erb +12 -0
  83. data/test/forked/rails_full_stack_test.rb +106 -0
  84. data/test/forked/rails_rake_full_stack_test.rb +58 -0
  85. data/test/integration/full_stack_test.rb +15 -16
  86. data/test/rails4_dummy/Rakefile +6 -0
  87. data/test/rails4_dummy/config/application.rb +8 -9
  88. data/test/rails4_dummy/config/coverband.rb +3 -11
  89. data/test/rails4_dummy/config/coverband_missing_redis.rb +3 -0
  90. data/test/rails5_dummy/Rakefile +6 -0
  91. data/test/rails5_dummy/config/application.rb +6 -10
  92. data/test/rails5_dummy/config/coverband.rb +7 -3
  93. data/test/rails5_dummy/config/coverband_missing_redis.rb +15 -0
  94. data/test/rails_test_helper.rb +22 -5
  95. data/test/test_helper.rb +52 -11
  96. data/test/unique_files.rb +17 -9
  97. data/views/data.erb +1 -0
  98. data/views/file_list.erb +13 -12
  99. data/views/gem_list.erb +10 -1
  100. data/views/layout.erb +6 -18
  101. data/views/nav.erb +36 -0
  102. data/views/source_file.erb +20 -4
  103. data/views/source_file_loader.erb +1 -1
  104. data/views/view_tracker.erb +52 -0
  105. metadata +71 -12
  106. data/lib/coverband/utils/file_path_helper.rb +0 -57
  107. data/test/coverband/integrations/middleware_test.rb +0 -96
  108. data/test/integration/rails_full_stack_test.rb +0 -95
data/changes.md CHANGED
@@ -16,6 +16,8 @@ Will be the fully modern release that drops maintenance legacy support in favor
16
16
  - add built-in support for easy loading via Railties
17
17
  - built in support for activejob, sidekiq, and other common frameworks
18
18
  - reduced configuration options
19
+ - support oneshot
20
+ - drop middleware figure out a way to kick off background without middelware
19
21
  - options on reporting
20
22
  - background reporting
21
23
  - or middleware reporting
@@ -47,6 +49,7 @@ Will be the fully modern release that drops maintenance legacy support in favor
47
49
  - move all code to work with relative paths leaving only stdlib Coverage working on full paths
48
50
  - add gem_safe_lists to track only some gems
49
51
  - add gem_details_safe list to report on details on some gems
52
+ - - display gems that are in loaded with 0 coverage, thanks @kbaum
50
53
 
51
54
  ### Coverband_jam_session
52
55
 
@@ -54,32 +57,104 @@ This is a possible gem to host experimental or more complex features, which woul
54
57
 
55
58
  Feature Ideas:
56
59
 
60
+ - per request coverage implemented via Ruby 2.6.0 coverage.clear https://bugs.ruby-lang.org/issues/15022
57
61
  - statsd adapters (it would allow passing in date ranges on usage)
58
- - move to SimpleCov console out, or make similar console tabular output
59
62
  - Possibly add ability to record code run for a given route
60
63
  - integrate recording with deploy tag or deploy timestamp
61
64
  - diff code usage across deployed versions
62
65
  - what methods increased usage or decreased
63
- - Improve client code api, around manual usage of sampling (like event usage)
64
66
  - 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.
65
67
  - 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
66
68
  - additional adapters (tracepoint, ruby-profiler, etc)
67
- - code route tracing (entry point to all code executed for example /some_path -> code coverage of that path)
68
- - tagging of reported Coverage
69
- - allow only to collect coverage based on route (limiting or scoped coverage)
70
- - coverage over some other variable like a set of alpha users
69
+ - tagged coverage reports
70
+ - code route tracing (entry point to all code executed for example /some_path -> code coverage of that path)
71
+ - deploy git hash tagging of reported Coverage
72
+ - allow only to collect coverage based on route (limiting or scoped coverage)
73
+ - coverage over some other variable like a set of alpha users
71
74
  - document how to use this on staging / selenium test suite runs
72
75
  - possible add API to pull report at end of run
73
76
 
74
- # Alpha
77
+ # Alpha / Beta / Release Candidates
75
78
 
76
- ### Coverband 4.3.0?
79
+ ### Coverband 4.2.5
77
80
 
78
- - further improvements on eager_loading detection
79
81
  - ?
80
82
 
81
83
  # Released
82
84
 
85
+ ### Coverband 4.2.4
86
+
87
+ - fixes related to startup without Redis, skipping Coverband on common rake tasks (assets:precompile), etc
88
+ - avoid calls to redis if coverage not updated
89
+ - split out lua work and added testing around lua scripts use for the hash redis store
90
+ - improvements to hash redis store resulting in 25% faster perf
91
+ - avoid duplicate hash calculations
92
+ - debug data now includes md5_hashes
93
+ - added support to download coverage and view data in JSON format
94
+ - documentation about working with environment variables
95
+ - add cache wiggle to avoid Redis CPU spikes (cache stampede on Redis server)
96
+ - make the nocov consistant on the data download and html view
97
+ - small performance improvements
98
+
99
+ ### Coverband 4.2.3
100
+
101
+ - readme fixes
102
+ - improved defaults for the next redis hash adapter
103
+ - add tooltips
104
+ - improved messaging around non-loaded files
105
+ - fix on last updated nil issue
106
+ - view tracker improvements
107
+ - clear all
108
+ - reset individual file
109
+ - timestamps on last seen activity
110
+
111
+
112
+ ### Coverband 4.2.2
113
+
114
+ - new experimental hash redis store for high volume collection
115
+ (hundreds of clients), thanks @kbaum
116
+ - view_tracker supports tracking view layer files like `.html.erb`
117
+ - documentation improvements, thanks @brossetti1, @jjb, @kbaum
118
+ - we now have discordapp for discussions, thanks @kbaum,
119
+ https://discordapp.com/channels/609509533999562753/609509533999562756
120
+ - perf fix on Rails initialization, thanks @skangg
121
+ - simplified logging
122
+
123
+ ### Coverband 4.2.1
124
+
125
+ - larger changes
126
+ - reduce memory usage
127
+ - fix issue where reports didn't include files with 0 activity
128
+ - updated runtime relavent lines and runtime percentages
129
+ - add Oneshot coverage support for Ruby 2.6.0 thanks @zwalker
130
+ - I would consider this our test oneshot release, please report any issues
131
+ - improved control over memory vs functionality
132
+ - oneshot support is the best memory and speed if you are on Ruby 2.6.\*
133
+ - simulated_oneshot works for Ruby prior to 2.6.\*, keeps lower runtime memory, at a trade of only being able to detect eager_load or runtime hits not both
134
+ - full hit tracking (The previous and current default), this uses more memory than other options, but provides full robust data
135
+
136
+ * small changes fixes
137
+ - further improvements on eager_loading detection, thanks @kbaum
138
+ - fix on ignore configuration options
139
+ - fix broken static server
140
+ - fix issue where clear and coverage trigger coverage reports
141
+ - improved logging
142
+ - fix on gem runtime code coverage support, thanks @kbaum
143
+ - fix sorting of runtime coverage
144
+ - add runtime coverage to gem summary pages
145
+ - documented redis TTL, thanks @jjb
146
+ - resolve regression on coverband rake tasks recording coverage
147
+ - improved runtime / eager loading tracking
148
+ - fix on small memory leaks
149
+ - added warnings that gem tracking isn't production ready
150
+ - built in support for basic auth on the web rack app, thanks @jjb
151
+ - don't clobber rake environment method definition, thanks @shioyama
152
+ - readme improvements, thanks @yuriyalekseyev
153
+ - fix duplicate requires thanks @yuriyalekseyev
154
+ - various cleanups and improvements, thanks @kbaum
155
+ - additional benchmarks to ensure proper memory handling, thanks @kbaum
156
+ - default redis TTL, thanks @jjb
157
+
83
158
  ### Coverband 4.2.0
84
159
 
85
160
  **NOTE:** This release introduces load time and runtime Coverage. To get the full benifit of this feature you need to reset you coverage data (`rake coverband:clear` or clear via the web UI). Until you clear the combined result is still correct, but your runtime data will be mixed with previous data. This is due to an additive change in how we store coverage. We didn't reset by default as the coverage history for some folks may be more important than runtime breakdown.
@@ -96,7 +171,7 @@ Feature Ideas:
96
171
  - move from thread based reporter instance to process based instance
97
172
  - thanks Karl Baum for much of the above mentioned work
98
173
  - clear coverage on individual files
99
- - we have a logo, thanks Dave Woodall
174
+ - we have a logo, thanks Dave Woodall, http://davewoodall.com
100
175
 
101
176
  ### Coverband 4.1.1
102
177
 
data/coverband.gemspec CHANGED
@@ -28,17 +28,15 @@ Gem::Specification.new do |spec|
28
28
  spec.add_development_dependency 'm'
29
29
  spec.add_development_dependency 'memory_profiler'
30
30
  spec.add_development_dependency 'minitest'
31
+ spec.add_development_dependency 'minitest-fork_executor'
31
32
  spec.add_development_dependency 'mocha', '~> 1.7.0'
32
33
  spec.add_development_dependency 'rack'
33
34
  spec.add_development_dependency 'rack-test'
34
35
  spec.add_development_dependency 'rake'
35
36
  spec.add_development_dependency 'resque'
36
- # temporarily needed to fun tests for classifier-reborn as part of benchmarks
37
- spec.add_development_dependency 'minitest-reporters'
37
+ spec.add_development_dependency 'rubocop'
38
+ spec.add_development_dependency 'rubocop-performance'
38
39
 
39
- # TODO: used for benchmarking and tests I think we have other better benchmarking
40
- # perhaps time to drop this and refactor.
41
- spec.add_development_dependency 'classifier-reborn'
42
40
  spec.add_development_dependency 'coveralls'
43
41
  # add when debugging
44
42
  # require 'byebug'; byebug
@@ -3,45 +3,48 @@
3
3
  module Coverband
4
4
  module Adapters
5
5
  class Base
6
- include Coverband::Utils::FilePathHelper
6
+ DATA_KEY = 'data'
7
+ FIRST_UPDATED_KEY = 'first_updated_at'
8
+ LAST_UPDATED_KEY = 'last_updated_at'
9
+ FILE_HASH = 'file_hash'
10
+ ABSTRACT_KEY = 'abstract'
11
+
7
12
  attr_accessor :type
8
13
 
9
14
  def initialize
10
- @file_hash_cache = {}
15
+ @type = Coverband::RUNTIME_TYPE
11
16
  end
12
17
 
13
18
  def clear!
14
- raise 'abstract'
19
+ raise ABSTRACT_KEY
15
20
  end
16
21
 
17
- def clear_file!(_file)
18
- raise 'abstract'
22
+ def clear_file!(_file = nil)
23
+ raise ABSTRACT_KEY
19
24
  end
20
25
 
21
26
  def migrate!
22
- raise 'abstract'
27
+ raise ABSTRACT_KEY
23
28
  end
24
29
 
25
30
  def size
26
- raise 'abstract'
31
+ raise ABSTRACT_KEY
27
32
  end
28
33
 
29
- def size_in_mib
30
- format('%.2f', (size.to_f / 2**20))
34
+ def save_coverage
35
+ raise ABSTRACT_KEY
36
+ end
37
+
38
+ def coverage(_local_type = nil)
39
+ raise ABSTRACT_KEY
31
40
  end
32
41
 
33
- # Note: This could lead to slight race on redis
34
- # where multiple processes pull the old coverage and add to it then push
35
- # the Coverband 2 had the same issue,
36
- # and the tradeoff has always been acceptable
37
- def save_report(report)
38
- data = report.dup
39
- data = merge_reports(data, get_report)
40
- save_coverage(data)
42
+ def size_in_mib
43
+ format('%<size>.2f', size: (size.to_f / 2**20))
41
44
  end
42
45
 
43
- def coverage
44
- get_report
46
+ def save_report(_report)
47
+ raise 'abstract'
45
48
  end
46
49
 
47
50
  def get_coverage_report
@@ -53,60 +56,69 @@ module Coverband
53
56
  coverage.keys || []
54
57
  end
55
58
 
56
- # TODO: deprecate / remove?
57
- def covered_lines_for_file(file)
58
- Array(coverage.dig(file, 'data'))
59
+ def raw_store
60
+ raise ABSTRACT_KEY
59
61
  end
60
62
 
61
63
  protected
62
64
 
63
65
  def split_coverage(types)
64
66
  types.reduce({}) do |data, type|
65
- data.update(type => get_report(type))
67
+ if type == Coverband::RUNTIME_TYPE && Coverband.configuration.simulate_oneshot_lines_coverage
68
+ data.update(type => simulated_runtime_coverage)
69
+ else
70
+ data.update(type => coverage(type))
71
+ end
66
72
  end
67
73
  end
68
74
 
69
- def merged_coverage(types)
70
- types.reduce({}) do |data, type|
71
- merge_reports(data, get_report(type), skip_expansion: true)
75
+ def simulated_runtime_coverage
76
+ runtime_data = coverage(Coverband::RUNTIME_TYPE)
77
+ eager_data = coverage(Coverband::EAGER_TYPE)
78
+ eager_data.values do |vals|
79
+ vals['data'].map! { |line_coverage| line_coverage ? (0 - line_coverage) : line_coverage }
72
80
  end
81
+ merge_reports(runtime_data, eager_data, skip_expansion: true)
73
82
  end
74
83
 
75
- def save_coverage
76
- raise 'abstract'
77
- end
78
-
79
- def get_report
80
- raise 'abstract'
84
+ def merged_coverage(types)
85
+ types.reduce({}) do |data, type|
86
+ merge_reports(data, coverage(type), skip_expansion: true)
87
+ end
81
88
  end
82
89
 
83
90
  def file_hash(file)
84
- @file_hash_cache[file] ||= Digest::MD5.file(file).hexdigest
91
+ Coverband::Utils::FileHasher.hash(file)
85
92
  end
86
93
 
94
+ # TODO: modify to extend report inline?
87
95
  def expand_report(report)
88
96
  expanded = {}
89
97
  report_time = Time.now.to_i
90
- updated_time = self.type == Coverband::EAGER_TYPE ? nil : report_time
98
+ updated_time = type == Coverband::EAGER_TYPE ? nil : report_time
91
99
  report.each_pair do |key, line_data|
92
100
  extended_data = {
93
- 'first_updated_at' => report_time,
94
- 'last_updated_at' => updated_time,
95
- 'file_hash' => file_hash(key),
96
- 'data' => line_data
101
+ FIRST_UPDATED_KEY => report_time,
102
+ LAST_UPDATED_KEY => updated_time,
103
+ FILE_HASH => file_hash(key),
104
+ DATA_KEY => line_data
97
105
  }
98
- expanded[full_path_to_relative(key)] = extended_data
106
+ expanded[Utils::RelativeFileConverter.convert(key)] = extended_data
99
107
  end
100
108
  expanded
101
109
  end
102
110
 
103
111
  def merge_reports(new_report, old_report, options = {})
112
+ # transparently update from RUNTIME_TYPE = nil to RUNTIME_TYPE = :runtime
113
+ # transparent update for format coveband_3_2
114
+ old_report = coverage(nil, override_type: nil) if old_report.nil? && type == Coverband::RUNTIME_TYPE
115
+
104
116
  new_report = expand_report(new_report) unless options[:skip_expansion]
105
117
  keys = (new_report.keys + old_report.keys).uniq
106
118
  keys.each do |file|
107
119
  new_report[file] = if new_report[file] &&
108
120
  old_report[file] &&
109
- new_report[file]['file_hash'] == old_report[file]['file_hash']
121
+ new_report[file][FILE_HASH] == old_report[file][FILE_HASH]
110
122
  merge_expanded_data(new_report[file], old_report[file])
111
123
  elsif new_report[file]
112
124
  new_report[file]
@@ -119,15 +131,22 @@ module Coverband
119
131
 
120
132
  def merge_expanded_data(new_expanded, old_expanded)
121
133
  {
122
- 'first_updated_at' => old_expanded['first_updated_at'],
123
- 'last_updated_at' => new_expanded['last_updated_at'],
124
- 'file_hash' => new_expanded['file_hash'],
125
- 'data' => array_add(new_expanded['data'], old_expanded['data'])
134
+ FIRST_UPDATED_KEY => old_expanded[FIRST_UPDATED_KEY],
135
+ LAST_UPDATED_KEY => new_expanded[LAST_UPDATED_KEY],
136
+ FILE_HASH => new_expanded[FILE_HASH],
137
+ DATA_KEY => array_add(new_expanded[DATA_KEY], old_expanded[DATA_KEY])
126
138
  }
127
139
  end
128
140
 
141
+ # TODO: This should only be 2 cases get our dup / not dups aligned
129
142
  def array_add(latest, original)
130
- latest.map.with_index { |v, i| (v && original[i]) ? v + original[i] : nil }
143
+ if Coverband.configuration.use_oneshot_lines_coverage
144
+ latest.map!.with_index { |v, i| (v + original[i] >= 1 ? 1 : 0) if v && original[i] }
145
+ elsif Coverband.configuration.simulate_oneshot_lines_coverage
146
+ latest.map.with_index { |v, i| (v + original[i] >= 1 ? 1 : 0) if v && original[i] }
147
+ else
148
+ latest.map.with_index { |v, i| (v && original[i]) ? v + original[i] : nil }
149
+ end
131
150
  end
132
151
  end
133
152
  end
@@ -28,21 +28,27 @@ module Coverband
28
28
  raise NotImplementedError, "FileStore doesn't support migrations"
29
29
  end
30
30
 
31
- private
32
-
33
- attr_accessor :path
34
-
35
- def save_coverage(report)
36
- File.open(path, 'w') { |f| f.write(report.to_json) }
37
- end
38
-
39
- def get_report
31
+ def coverage(_local_type = nil)
40
32
  if File.exist?(path)
41
33
  JSON.parse(File.read(path))
42
34
  else
43
35
  {}
44
36
  end
45
37
  end
38
+
39
+ def save_report(report)
40
+ data = report.dup
41
+ data = merge_reports(data, coverage)
42
+ File.open(path, 'w') { |f| f.write(data.to_json) }
43
+ end
44
+
45
+ def raw_store
46
+ raise NotImplementedError, "FileStore doesn't support raw_store"
47
+ end
48
+
49
+ private
50
+
51
+ attr_accessor :path
46
52
  end
47
53
  end
48
54
  end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Coverband
6
+ module Adapters
7
+ class HashRedisStore < Base
8
+ FILE_KEY = 'file'
9
+ FILE_LENGTH_KEY = 'file_length'
10
+ META_DATA_KEYS = [DATA_KEY, FIRST_UPDATED_KEY, LAST_UPDATED_KEY, FILE_HASH].freeze
11
+ ###
12
+ # This key isn't related to the coverband version, but to the interal format
13
+ # used to store data to redis. It is changed only when breaking changes to our
14
+ # redis format are required.
15
+ ###
16
+ REDIS_STORAGE_FORMAT_VERSION = 'coverband_hash_3_3'
17
+
18
+ JSON_PAYLOAD_EXPIRATION = 5 * 60
19
+
20
+ attr_reader :redis_namespace
21
+
22
+ def initialize(redis, opts = {})
23
+ super()
24
+ @redis_namespace = opts[:redis_namespace]
25
+ @save_report_batch_size = opts[:save_report_batch_size] || 100
26
+ @format_version = REDIS_STORAGE_FORMAT_VERSION
27
+ @redis = redis
28
+ raise 'HashRedisStore requires redis >= 2.6.0' unless supported?
29
+
30
+ @ttl = opts[:ttl]
31
+ @relative_file_converter = opts[:relative_file_converter] || Utils::RelativeFileConverter
32
+ end
33
+
34
+ def supported?
35
+ Gem::Version.new(@redis.info['redis_version']) >= Gem::Version.new('2.6.0')
36
+ end
37
+
38
+ def clear!
39
+ old_type = type
40
+ Coverband::TYPES.each do |type|
41
+ self.type = type
42
+ file_keys = files_set
43
+ @redis.del(*file_keys) if file_keys.any?
44
+ @redis.del(files_key)
45
+ end
46
+ self.type = old_type
47
+ end
48
+
49
+ def clear_file!(file)
50
+ file_hash = file_hash(file)
51
+ relative_path_file = @relative_file_converter.convert(file)
52
+ Coverband::TYPES.each do |type|
53
+ @redis.del(key(relative_path_file, type, file_hash: file_hash))
54
+ end
55
+ @redis.srem(files_key, relative_path_file)
56
+ end
57
+
58
+ def save_report(report)
59
+ report_time = Time.now.to_i
60
+ updated_time = type == Coverband::EAGER_TYPE ? nil : report_time
61
+ keys = []
62
+ report.each_slice(@save_report_batch_size) do |slice|
63
+ files_data = slice.map do |(file, data)|
64
+ relative_file = @relative_file_converter.convert(file)
65
+ file_hash = file_hash(relative_file)
66
+ key = key(relative_file, file_hash: file_hash)
67
+ keys << key
68
+ script_input(
69
+ key: key,
70
+ file: relative_file,
71
+ file_hash: file_hash,
72
+ data: data,
73
+ report_time: report_time,
74
+ updated_time: updated_time
75
+ )
76
+ end
77
+ next unless files_data.any?
78
+
79
+ arguments_key = [@redis_namespace, SecureRandom.uuid].compact.join('.')
80
+ @redis.set(arguments_key, { ttl: @ttl, files_data: files_data }.to_json, ex: JSON_PAYLOAD_EXPIRATION)
81
+ @redis.evalsha(hash_incr_script, [arguments_key])
82
+ end
83
+ @redis.sadd(files_key, keys) if keys.any?
84
+ end
85
+
86
+ def coverage(local_type = nil)
87
+ files_set = files_set(local_type)
88
+ @redis.pipelined do
89
+ files_set.map do |key|
90
+ @redis.hgetall(key)
91
+ end
92
+ end.each_with_object({}) do |data_from_redis, hash|
93
+ add_coverage_for_file(data_from_redis, hash)
94
+ end
95
+ end
96
+
97
+ def raw_store
98
+ @redis
99
+ end
100
+
101
+ def size
102
+ 'not available'
103
+ end
104
+
105
+ def size_in_mib
106
+ 'not available'
107
+ end
108
+
109
+ private
110
+
111
+ def add_coverage_for_file(data_from_redis, hash)
112
+ return if data_from_redis.empty?
113
+
114
+ file = data_from_redis[FILE_KEY]
115
+ return unless file_hash(file) == data_from_redis[FILE_HASH]
116
+
117
+ data = coverage_data_from_redis(data_from_redis)
118
+ hash[file] = data_from_redis.select { |meta_data_key, _value| META_DATA_KEYS.include?(meta_data_key) }.merge!('data' => data)
119
+ hash[file][LAST_UPDATED_KEY] = hash[file][LAST_UPDATED_KEY].blank? ? nil : hash[file][LAST_UPDATED_KEY].to_i
120
+ hash[file].merge!(LAST_UPDATED_KEY => hash[file][LAST_UPDATED_KEY], FIRST_UPDATED_KEY => hash[file][FIRST_UPDATED_KEY].to_i)
121
+ end
122
+
123
+ def coverage_data_from_redis(data_from_redis)
124
+ max = data_from_redis[FILE_LENGTH_KEY].to_i - 1
125
+ Array.new(max + 1) do |index|
126
+ line_coverage = data_from_redis[index.to_s]
127
+ line_coverage.nil? ? nil : line_coverage.to_i
128
+ end
129
+ end
130
+
131
+ def script_input(key:, file:, file_hash:, data:, report_time:, updated_time:)
132
+ coverage_data = data.each_with_index.each_with_object({}) do |(coverage, index), hash|
133
+ hash[index] = coverage if coverage
134
+ end
135
+ meta = {
136
+ first_updated_at: report_time,
137
+ file: file,
138
+ file_hash: file_hash,
139
+ file_length: data.length,
140
+ hash_key: key
141
+ }
142
+ meta[:last_updated_at] = updated_time if updated_time
143
+ {
144
+ hash_key: key,
145
+ meta: meta,
146
+ coverage: coverage_data
147
+ }
148
+ end
149
+
150
+ def hash_incr_script
151
+ @hash_incr_script ||= @redis.script(:load, lua_script_content)
152
+ end
153
+
154
+ def lua_script_content
155
+ File.read(File.join(
156
+ File.dirname(__FILE__), '../../../lua/lib/persist-coverage.lua'
157
+ ))
158
+ end
159
+
160
+ def values_from_redis(local_type, files)
161
+ return files if files.empty?
162
+
163
+ @redis.mget(*files.map { |file| key(file, local_type) }).map do |value|
164
+ value.nil? ? {} : JSON.parse(value)
165
+ end
166
+ end
167
+
168
+ def relative_paths(files)
169
+ files&.map! { |file| full_path_to_relative(file) }
170
+ end
171
+
172
+ def files_set(local_type = nil)
173
+ @redis.smembers(files_key(local_type))
174
+ end
175
+
176
+ def files_key(local_type = nil)
177
+ "#{key_prefix(local_type)}.files"
178
+ end
179
+
180
+ def key(file, local_type = nil, file_hash:)
181
+ [key_prefix(local_type), file, file_hash].join('.')
182
+ end
183
+
184
+ def key_prefix(local_type = nil)
185
+ local_type ||= type
186
+ [@format_version, @redis_namespace, local_type].compact.join('.')
187
+ end
188
+ end
189
+ end
190
+ end
@@ -13,23 +13,31 @@ module Coverband
13
13
  ###
14
14
  REDIS_STORAGE_FORMAT_VERSION = 'coverband_3_2'
15
15
 
16
+ attr_reader :redis_namespace
17
+
16
18
  def initialize(redis, opts = {})
17
19
  super()
18
20
  @redis = redis
19
21
  @ttl = opts[:ttl]
20
22
  @redis_namespace = opts[:redis_namespace]
21
23
  @format_version = REDIS_STORAGE_FORMAT_VERSION
24
+ @keys = {}
25
+ Coverband::TYPES.each do |type|
26
+ @keys[type] = [@format_version, @redis_namespace, type].compact.join('.')
27
+ end
22
28
  end
23
29
 
24
30
  def clear!
25
31
  Coverband::TYPES.each do |type|
26
32
  @redis.del(type_base_key(type))
27
33
  end
34
+ # temporarily clear the old namespace of coverband_3_2
35
+ @redis.del(type_base_key(nil))
28
36
  end
29
37
 
30
38
  def clear_file!(filename)
31
39
  Coverband::TYPES.each do |type|
32
- data = get_report(type)
40
+ data = coverage(type)
33
41
  data.delete(filename)
34
42
  save_coverage(data, type)
35
43
  end
@@ -47,18 +55,18 @@ module Coverband
47
55
  def migrate!
48
56
  reset_base_key
49
57
  @format_version = 'coverband3_1'
50
- previous_data = get_report
58
+ previous_data = coverage
51
59
  if previous_data.empty?
52
60
  puts 'no previous data to migrate found'
53
61
  exit 0
54
62
  end
55
63
  relative_path_report = previous_data.each_with_object({}) do |(key, vals), fixed_report|
56
- fixed_report[full_path_to_relative(key)] = vals
64
+ fixed_report[Utils::RelativeFileConverter.convert(key)] = vals
57
65
  end
58
66
  clear!
59
67
  reset_base_key
60
68
  @format_version = REDIS_STORAGE_FORMAT_VERSION
61
- save_coverage(merge_reports(get_report, relative_path_report, skip_expansion: true))
69
+ save_coverage(merge_reports(coverage, relative_path_report, skip_expansion: true))
62
70
  end
63
71
 
64
72
  def type=(type)
@@ -66,6 +74,28 @@ module Coverband
66
74
  reset_base_key
67
75
  end
68
76
 
77
+ def coverage(local_type = nil, opts = {})
78
+ local_type ||= opts.key?(:override_type) ? opts[:override_type] : type
79
+ data = redis.get type_base_key(local_type)
80
+ data = data ? JSON.parse(data) : {}
81
+ data.delete_if { |file_path, file_data| file_hash(file_path) != file_data['file_hash'] } unless opts[:skip_hash_check]
82
+ data
83
+ end
84
+
85
+ # Note: This could lead to slight race on redis
86
+ # where multiple processes pull the old coverage and add to it then push
87
+ # the Coverband 2 had the same issue,
88
+ # and the tradeoff has always been acceptable
89
+ def save_report(report)
90
+ data = report.dup
91
+ data = merge_reports(data, coverage(nil, skip_hash_check: true))
92
+ save_coverage(data)
93
+ end
94
+
95
+ def raw_store
96
+ @redis
97
+ end
98
+
69
99
  private
70
100
 
71
101
  attr_reader :redis
@@ -79,7 +109,7 @@ module Coverband
79
109
  end
80
110
 
81
111
  def type_base_key(local_type)
82
- [@format_version, @redis_namespace, local_type].compact.join('.')
112
+ @keys[local_type]
83
113
  end
84
114
 
85
115
  def save_coverage(data, local_type = nil)
@@ -87,12 +117,6 @@ module Coverband
87
117
  redis.set type_base_key(local_type), data.to_json
88
118
  redis.expire(type_base_key(local_type), @ttl) if @ttl
89
119
  end
90
-
91
- def get_report(local_type = nil)
92
- local_type ||= type
93
- data = redis.get type_base_key(local_type)
94
- data ? JSON.parse(data) : {}
95
- end
96
120
  end
97
121
  end
98
122
  end