coverband 4.2.1 → 4.2.2

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -9
  3. data/CODE_OF_CONDUCT.md +76 -0
  4. data/README.md +32 -28
  5. data/changes.md +31 -11
  6. data/coverband.gemspec +0 -5
  7. data/lib/coverband/adapters/base.rb +8 -13
  8. data/lib/coverband/adapters/file_store.rb +10 -4
  9. data/lib/coverband/adapters/hash_redis_store.rb +182 -0
  10. data/lib/coverband/adapters/redis_store.rb +15 -1
  11. data/lib/coverband/collectors/coverage.rb +5 -7
  12. data/lib/coverband/collectors/view_tracker.rb +124 -0
  13. data/lib/coverband/configuration.rb +21 -1
  14. data/lib/coverband/integrations/background.rb +2 -1
  15. data/lib/coverband/reporters/base.rb +4 -5
  16. data/lib/coverband/reporters/console_report.rb +1 -0
  17. data/lib/coverband/reporters/web.rb +6 -0
  18. data/lib/coverband/utils/absolute_file_converter.rb +47 -0
  19. data/lib/coverband/utils/file_hasher.rb +16 -0
  20. data/lib/coverband/utils/html_formatter.rb +12 -0
  21. data/lib/coverband/utils/railtie.rb +15 -3
  22. data/lib/coverband/utils/relative_file_converter.rb +41 -0
  23. data/lib/coverband/utils/s3_report.rb +2 -2
  24. data/lib/coverband/utils/source_file.rb +1 -3
  25. data/lib/coverband/utils/tasks.rb +1 -0
  26. data/lib/coverband/version.rb +5 -1
  27. data/lib/coverband.rb +7 -1
  28. data/public/application.css +6 -6
  29. data/test/benchmarks/benchmark.rake +25 -49
  30. data/test/benchmarks/init_rails.rake +10 -0
  31. data/test/coverband/adapters/hash_redis_store_test.rb +190 -0
  32. data/test/coverband/adapters/redis_store_test.rb +90 -88
  33. data/test/coverband/collectors/coverage_test.rb +6 -2
  34. data/test/coverband/collectors/view_tracker_test.rb +77 -0
  35. data/test/coverband/integrations/resque_worker_test.rb +2 -3
  36. data/test/coverband/reporters/base_test.rb +1 -78
  37. data/test/coverband/reporters/console_test.rb +1 -4
  38. data/test/coverband/reporters/html_test.rb +9 -9
  39. data/test/coverband/utils/absolute_file_converter_test.rb +56 -0
  40. data/test/coverband/utils/file_hasher_test.rb +29 -0
  41. data/test/coverband/utils/relative_file_converter_test.rb +39 -0
  42. data/test/forked/rails_rake_full_stack_test.rb +9 -1
  43. data/test/integration/full_stack_test.rb +1 -2
  44. data/test/test_helper.rb +6 -7
  45. data/test/unique_files.rb +1 -1
  46. data/views/layout.erb +1 -20
  47. data/views/nav.erb +35 -0
  48. data/views/view_tracker.erb +37 -0
  49. metadata +22 -31
  50. data/lib/coverband/utils/file_path_helper.rb +0 -68
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0736c0a54102da0309de53707d866be707e72f0c977855bcf69586d281c9d2fe
4
- data.tar.gz: 4616a0e6a769500797b89e751d8a8b96fa11ad11237c20bf359d592853e5e194
3
+ metadata.gz: 33ec68822b5c228e86048d62ded36cfeba06adf77022d07b39cdcb897c215f6f
4
+ data.tar.gz: 7494e163926def8f8406da850efe02f5cd79f6ba6377abaf14ccb09aedc6ca85
5
5
  SHA512:
6
- metadata.gz: c16f4e0351ffdd49ba3732c203b9932d4ee2ee8e9890b59f0c8d1e0beafcb2e8632d5a47ffc946248813284ebcac727785ceba3e38a6f041b0f5c000c42870f2
7
- data.tar.gz: a1119724f958c01dfdb333efe95ed43acc6f2c1396d24efe7a208d8606e33d8de687d73a1099a3a20d5a37dbcef6b43c4ba36fb9d9d194d0a584e02957fdd016
6
+ metadata.gz: fda6dd49e21244819834f20e857ba2f4c8fddec4b1096a2b7ecfad7acdf6352cb2d4a8783f1cb5efc1ea2dc1ef5d03e7712f709fb928e29461dd41fc2dff76d8
7
+ data.tar.gz: b318161bc9c3709d41a35bb8a4e108a5ab38bea2feb095b1fc8d900f6e8a9bcfcc37dbf90392408d2efa3d510a58ceff5c69d19a422e4923049c08ee69d20c9c
data/.travis.yml CHANGED
@@ -11,18 +11,11 @@ services:
11
11
  - redis-server
12
12
  script:
13
13
  - bundle exec rake rubocop
14
+ - COVERBAND_HASH_REDIS_STORE=t bundle exec rake
15
+ - COVERBAND_HASH_REDIS_STORE=t bundle exec rake forked_tests
14
16
  - bundle exec rake
15
17
  - bundle exec rake forked_tests
16
18
  # remove this for now as it is flaky
17
19
  # passes locally on 2.3.5 on travis passes sometimes on 2.4 and always on 2.6.1
18
20
  #- bundle exec rake benchmarks:memory
19
21
  - bundle exec rake benchmarks
20
- before_install:
21
- - echo 'this is a hack to clear default bundler and force bundler 1.17.3'
22
- - ls /home/travis/.rvm/gems/
23
- - rm /home/travis/.rvm/gems/ruby-2.3.7@global/specifications/bundler-2.0.1.gemspec || true
24
- - rm /home/travis/.rvm/gems/ruby-2.4.6@global/specifications/bundler-2.0.1.gemspec || true
25
- - rm /home/travis/.rvm/gems/ruby-2.5.5@global/specifications/bundler-2.0.1.gemspec || true
26
- - gem uninstall bundler || true
27
- - gem install bundler -v '1.17.3'
28
- - bundler --version
@@ -0,0 +1,76 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, sex characteristics, gender identity and expression,
9
+ level of experience, education, socio-economic status, nationality, personal
10
+ appearance, race, religion, or sexual identity and orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at danmayer@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72
+
73
+ [homepage]: https://www.contributor-covenant.org
74
+
75
+ For answers to common questions about this code of conduct, see
76
+ https://www.contributor-covenant.org/faq
data/README.md CHANGED
@@ -5,6 +5,8 @@
5
5
  [![Build Status](https://travis-ci.org/danmayer/coverband.svg?branch=master)](https://travis-ci.org/danmayer/coverband)
6
6
  [![Coverage Status](https://coveralls.io/repos/github/danmayer/coverband/badge.svg?branch=master)](https://coveralls.io/github/danmayer/coverband?branch=master)
7
7
  [![Maintainability](https://api.codeclimate.com/v1/badges/1e6682f9540d75f26da7/maintainability)](https://codeclimate.com/github/danmayer/coverband/maintainability)
8
+ [![Discord Shield](https://img.shields.io/discord/609509533999562753)](https://discord.gg/KAH38EV)
9
+
8
10
 
9
11
  <p align="center">
10
12
  <a href="#key-features">Key Features</a> •
@@ -12,7 +14,8 @@
12
14
  <a href="#coverage-report">Coverage Report</a> •
13
15
  <a href="#advanced-config">Advanced Config</a> •
14
16
  <a href="#license">License</a> •
15
- <a href="/changes.md">Change Log / Roadmap</a>
17
+ <a href="/changes.md">Change Log / Roadmap</a>
18
+ <a href="/CODE_OF_CONDUCT.md">Code of Conduct</a>
16
19
  </p>
17
20
 
18
21
  A gem to measure production code usage, showing a counter for the number of times each line of code that is executed. Coverband allows easy configuration to collect and report on production code usage. It reports in the background via a thread or can be used as Rack middleware, or manually configured to meet any need.
@@ -161,10 +164,10 @@ Coverband.configure do |config|
161
164
  config.s3_access_key_id = ENV['AWS_ACCESS_KEY_ID']
162
165
  config.s3_secret_access_key = ENV['AWS_SECRET_ACCESS_KEY']
163
166
 
164
- # config options false, true, or 'debug'. Always use false in production
167
+ # config options false, true. (defaults to false)
165
168
  # true and debug can give helpful and interesting code usage information
166
- # they both increase the performance overhead of the gem a little.
167
- # they can also help with initially debugging the installation.
169
+ # and is safe to use if one is investigating issues in production, but it will slightly
170
+ # hit perf.
168
171
  config.verbose = false
169
172
  end
170
173
  ```
@@ -178,10 +181,19 @@ config.ignore += ['config/application.rb',
178
181
  'config/boot.rb',
179
182
  'config/puma.rb',
180
183
  'config/schedule.rb',
181
- 'bin/*'
184
+ 'bin/*',
182
185
  'config/environments/*',
183
186
  'lib/tasks/*']
184
187
  ```
188
+ ### View Tracking
189
+
190
+ Coverband allows an optional feature to track all view files that are used by an application.
191
+
192
+ To opt-in to this feature... enable the feature in your Coverband config.
193
+
194
+ `config.track_views = true`
195
+
196
+ ![image](https://raw.github.com/danmayer/coverband/master/docs/coverband_view_tracker.png)
185
197
 
186
198
  ### Writing Coverband Results to S3
187
199
 
@@ -204,13 +216,13 @@ ENV['AWS_ACCESS_KEY_ID']
204
216
  ENV['AWS_SECRET_ACCESS_KEY']
205
217
  ```
206
218
 
207
- ### Coverage Data Migration
219
+ ### Redis Hash Store
208
220
 
209
- Between the release of 4.0 and 4.1 our data format changed. This resets all your coverage data. If you want to restore your previous coverage data, feel free to migrate.
221
+ Coverband on very high volume sites with many server processes reporting can have a race condition. To resolve the race condition and reduce Ruby memory overhead we have introduced a new Redis storage option. This moves the some of the work from the Ruby processes to Redis. It is worth noting because of this, it has a larger demands on the Redis server. So adjust your Redis instance accordingly. To help reduce the extra redis load you can also change the background reporting time period.
210
222
 
211
- `rake coverband:migrate`
212
-
213
- - We will be working to support migrations going forward, when possible
223
+ * set the new Redis store: `config.store = Coverband::Adapters::HashRedisStore.new(Redis.new(url: redis_url))`
224
+ * adjust from default 30s reporting `config.background_reporting_sleep_seconds = 120`
225
+ * reminder it is recommended to have a unique Redis per workload (background jobs, caching, Coverband), for this store, it may be more important to have a dedicated Redis.
214
226
 
215
227
  ### Clear Coverage
216
228
 
@@ -218,6 +230,14 @@ Now that Coverband uses MD5 hashes there should be no reason to manually clear c
218
230
 
219
231
  `rake coverband:clear`
220
232
 
233
+ ### Coverage Data Migration
234
+
235
+ Between the release of 4.0 and 4.1 our data format changed. This resets all your coverage data. If you want to restore your previous coverage data, feel free to migrate.
236
+
237
+ `rake coverband:migrate`
238
+
239
+ - We will be working to support migrations going forward, when possible
240
+
221
241
  ### Adding Rake Tasks outside of Rails
222
242
 
223
243
  Rails apps should automaticallly include the tasks via the Railtie.
@@ -307,27 +327,11 @@ Coverband.start
307
327
 
308
328
  ### Verbose Debug / Development Mode
309
329
 
310
- Note: To debug issues getting Coverband working. I recommend running in development mode, by turning verbose logging on `config.verbose = true` and passing in the Rails.logger `config.logger = Rails.logger` to the Coverband config. This makes it easy to follow in development mode. Be careful to not leave these on in production as they will affect performance.
330
+ Note: To debug issues getting Coverband working. I recommend running in development mode, by turning verbose logging on `config.verbose = true` and passing in the Rails.logger `config.logger = Rails.logger` to the Coverband config. We respect the log level, and I would recommend log level info generally, but if you are investigating a prolbem Coverband logs additional data at the `debug` level. This makes it easy to follow in development mode. Be careful to not leave these on in production as they will affect performance.
311
331
 
312
332
  ---
313
333
 
314
- If you are trying to debug locally wondering what code is being run during a request. The verbose modes `config.verbose = true` and `config.verbose = 'debug'` can be useful. With true set it will output the number of lines executed per file, to the passed in log. The files are sorted from least used file to most active file. I have even run that mode in production without much of a problem. The debug verbose mode outputs both file usage and provides the number of calls per line of code. For example if you see something like below which indicates that the `application_helper` has 43150 lines executed. That might seem odd. Then looking at the breakdown of `application_helper` we can see that line `516` was executed 38,577 times. That seems bad, and is likely worth investigating perhaps memoizing or cacheing is required.
315
-
316
- config.verbose = 'debug'
317
-
318
- coverband file usage:
319
- [["/Users/danmayer/projects/app_name/lib/facebook.rb", 6],
320
- ["/Users/danmayer/projects/app_name/app/models/some_modules.rb", 9],
321
- ...
322
- ["/Users/danmayer/projects/app_name/app/models/user.rb", 2606],
323
- ["/Users/danmayer/projects/app_name/app/helpers/application_helper.rb",
324
- 43150]]
325
-
326
- file:
327
- /Users/danmayer/projects/app_name/app/helpers/application_helper.rb =>
328
- [[448, 1], [202, 1],
329
- ...
330
- [517, 1617], [516, 38577]]
334
+ If you are trying to debug locally wondering what code is being run during a request. The verbose modes `config.verbose = true` && `Rails.logger.level = :debug`. With true set it will output the number of lines executed per file, to the passed in log.
331
335
 
332
336
  # Prerequisites
333
337
 
data/changes.md CHANGED
@@ -49,7 +49,7 @@ Will be the fully modern release that drops maintenance legacy support in favor
49
49
  - move all code to work with relative paths leaving only stdlib Coverage working on full paths
50
50
  - add gem_safe_lists to track only some gems
51
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
52
+ - - display gems that are in loaded with 0 coverage, thanks @kbaum
53
53
 
54
54
  ### Coverband_jam_session
55
55
 
@@ -76,6 +76,30 @@ Feature Ideas:
76
76
 
77
77
  # Alpha / Beta / Release Candidates
78
78
 
79
+ ### Coverband 4.2.2
80
+
81
+ - new experimental hash redis store for high volume collection
82
+ (hundreds of clients), thanks @kbaum
83
+ - view_tracker supports tracking view layer files like `.html.erb`
84
+ - documentation improvements, thanks @brossetti1, @jjb, @kbaum
85
+ - we now have discordapp for discussions, thanks @kbaum,
86
+ https://discordapp.com/channels/609509533999562753/609509533999562756
87
+ - perf fix on Rails initialization, thanks @skangg
88
+ - simplified logging
89
+
90
+ # Released
91
+
92
+ ### Coverband 4.2.2
93
+
94
+ - new experimental hash redis store for high volume collection
95
+ (hundreds of clients), thanks @kbaum
96
+ - view_tracker supports tracking view layer files like `.html.erb`
97
+ - documentation improvements, thanks @brossetti1, @jjb, @kbaum
98
+ - we now have discordapp for discussions, thanks @kbaum,
99
+ https://discordapp.com/channels/609509533999562753/609509533999562756
100
+ - perf fix on Rails initialization, thanks @skangg
101
+ - simplified logging
102
+
79
103
  ### Coverband 4.2.1
80
104
 
81
105
  - larger changes
@@ -83,14 +107,13 @@ Feature Ideas:
83
107
  - fix issue where reports didn't include files with 0 activity
84
108
  - updated runtime relavent lines and runtime percentages
85
109
  - add Oneshot coverage support for Ruby 2.6.0 thanks @zwalker
86
- - I would consider this our test oneshot release, please report any issues
87
- - improved control over memory vs functionality
88
- - oneshot support is the best memory and speed if you are on Ruby 2.6.*
89
- - 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
90
- - full hit tracking (The previous and current default), this uses more memory than other options, but provides full robust data
91
-
110
+ - I would consider this our test oneshot release, please report any issues
111
+ - improved control over memory vs functionality
112
+ - oneshot support is the best memory and speed if you are on Ruby 2.6.\*
113
+ - 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
114
+ - full hit tracking (The previous and current default), this uses more memory than other options, but provides full robust data
92
115
 
93
- - small changes fixes
116
+ * small changes fixes
94
117
  - further improvements on eager_loading detection, thanks @kbaum
95
118
  - fix on ignore configuration options
96
119
  - fix broken static server
@@ -112,9 +135,6 @@ Feature Ideas:
112
135
  - additional benchmarks to ensure proper memory handling, thanks @kbaum
113
136
  - default redis TTL, thanks @jjb
114
137
 
115
-
116
- # Released
117
-
118
138
  ### Coverband 4.2.0
119
139
 
120
140
  **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.
data/coverband.gemspec CHANGED
@@ -35,12 +35,7 @@ Gem::Specification.new do |spec|
35
35
  spec.add_development_dependency 'rake'
36
36
  spec.add_development_dependency 'resque'
37
37
  spec.add_development_dependency 'rubocop'
38
- # temporarily needed to run tests for classifier-reborn as part of benchmarks
39
- spec.add_development_dependency 'minitest-reporters'
40
38
 
41
- # TODO: used for benchmarking and tests I think we have other better benchmarking
42
- # perhaps time to drop this and refactor.
43
- spec.add_development_dependency 'classifier-reborn'
44
39
  spec.add_development_dependency 'coveralls'
45
40
  # add when debugging
46
41
  # require 'byebug'; byebug
@@ -3,8 +3,6 @@
3
3
  module Coverband
4
4
  module Adapters
5
5
  class Base
6
- include Coverband::Utils::FilePathHelper
7
-
8
6
  DATA_KEY = 'data'
9
7
  FIRST_UPDATED_KEY = 'first_updated_at'
10
8
  LAST_UPDATED_KEY = 'last_updated_at'
@@ -14,7 +12,6 @@ module Coverband
14
12
  attr_accessor :type
15
13
 
16
14
  def initialize
17
- @file_hash_cache = {}
18
15
  @type = Coverband::RUNTIME_TYPE
19
16
  end
20
17
 
@@ -46,14 +43,8 @@ module Coverband
46
43
  format('%.2f', (size.to_f / 2**20))
47
44
  end
48
45
 
49
- # Note: This could lead to slight race on redis
50
- # where multiple processes pull the old coverage and add to it then push
51
- # the Coverband 2 had the same issue,
52
- # and the tradeoff has always been acceptable
53
- def save_report(report)
54
- data = report.dup
55
- data = merge_reports(data, coverage)
56
- save_coverage(data)
46
+ def save_report(_report)
47
+ raise 'abstract'
57
48
  end
58
49
 
59
50
  def get_coverage_report
@@ -65,6 +56,10 @@ module Coverband
65
56
  coverage.keys || []
66
57
  end
67
58
 
59
+ def raw_store
60
+ raise ABSTRACT_KEY
61
+ end
62
+
68
63
  protected
69
64
 
70
65
  def split_coverage(types)
@@ -93,7 +88,7 @@ module Coverband
93
88
  end
94
89
 
95
90
  def file_hash(file)
96
- @file_hash_cache[file] ||= Digest::MD5.file(file).hexdigest
91
+ Coverband::Utils::FileHasher.hash(file)
97
92
  end
98
93
 
99
94
  # TODO: modify to extend report inline?
@@ -108,7 +103,7 @@ module Coverband
108
103
  FILE_HASH => file_hash(key),
109
104
  DATA_KEY => line_data
110
105
  }
111
- expanded[full_path_to_relative(key)] = extended_data
106
+ expanded[Utils::RelativeFileConverter.convert(key)] = extended_data
112
107
  end
113
108
  expanded
114
109
  end
@@ -36,13 +36,19 @@ module Coverband
36
36
  end
37
37
  end
38
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
+
39
49
  private
40
50
 
41
51
  attr_accessor :path
42
-
43
- def save_coverage(report)
44
- File.open(path, 'w') { |f| f.write(report.to_json) }
45
- end
46
52
  end
47
53
  end
48
54
  end
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coverband
4
+ module Adapters
5
+ class HashRedisStore < Base
6
+ FILE_KEY = 'file'
7
+ FILE_LENGTH_KEY = 'file_length'
8
+ META_DATA_KEYS = [DATA_KEY, FIRST_UPDATED_KEY, LAST_UPDATED_KEY].freeze
9
+ ###
10
+ # This key isn't related to the coverband version, but to the interal format
11
+ # used to store data to redis. It is changed only when breaking changes to our
12
+ # redis format are required.
13
+ ###
14
+ REDIS_STORAGE_FORMAT_VERSION = 'coverband_hash_3_3'
15
+
16
+ attr_reader :redis_namespace
17
+
18
+ def initialize(redis, opts = {})
19
+ super()
20
+ @redis_namespace = opts[:redis_namespace]
21
+ @format_version = REDIS_STORAGE_FORMAT_VERSION
22
+ @redis = redis
23
+ raise 'HashRedisStore requires redis >= 2.6.0' unless supported?
24
+
25
+ @ttl = opts[:ttl] || -1
26
+ @relative_file_converter = opts[:relative_file_converter] || Utils::RelativeFileConverter
27
+ end
28
+
29
+ def supported?
30
+ Gem::Version.new(@redis.info['redis_version']) >= Gem::Version.new('2.6.0')
31
+ end
32
+
33
+ def clear!
34
+ old_type = type
35
+ Coverband::TYPES.each do |type|
36
+ self.type = type
37
+ file_keys = files_set
38
+ @redis.del(*file_keys) if file_keys.any?
39
+ @redis.del(files_key)
40
+ end
41
+ self.type = old_type
42
+ end
43
+
44
+ def clear_file!(file)
45
+ file_hash = file_hash(file)
46
+ relative_path_file = @relative_file_converter.convert(file)
47
+ Coverband::TYPES.each do |type|
48
+ @redis.del(key(relative_path_file, type, file_hash: file_hash))
49
+ end
50
+ @redis.srem(files_key, relative_path_file)
51
+ end
52
+
53
+ def save_report(report)
54
+ report_time = Time.now.to_i
55
+ updated_time = type == Coverband::EAGER_TYPE ? nil : report_time
56
+ script_id = hash_incr_script
57
+ @redis.pipelined do
58
+ keys = report.map do |file, data|
59
+ relative_file = @relative_file_converter.convert(file)
60
+ file_hash = file_hash(relative_file)
61
+ key = key(relative_file, file_hash: file_hash)
62
+ script_input = save_report_script_input(
63
+ key: key,
64
+ file: relative_file,
65
+ file_hash: file_hash,
66
+ data: data,
67
+ report_time: report_time,
68
+ updated_time: updated_time
69
+ )
70
+ @redis.evalsha(script_id, script_input[:keys], script_input[:args])
71
+ key
72
+ end
73
+ @redis.sadd(files_key, keys) if keys.any?
74
+ end
75
+ end
76
+
77
+ def coverage(local_type = nil)
78
+ files_set(local_type).each_with_object({}) do |key, hash|
79
+ add_coverage_for_file(key, hash)
80
+ end
81
+ end
82
+
83
+ def raw_store
84
+ @redis
85
+ end
86
+
87
+ def size
88
+ 'not available'
89
+ end
90
+
91
+ def size_in_mib
92
+ 'not available'
93
+ end
94
+
95
+ private
96
+
97
+ def add_coverage_for_file(key, hash)
98
+ data_from_redis = @redis.hgetall(key)
99
+
100
+ return if data_from_redis.empty?
101
+
102
+ file = data_from_redis[FILE_KEY]
103
+ return unless file_hash(file) == data_from_redis[FILE_HASH]
104
+
105
+ data = coverage_data_from_redis(data_from_redis)
106
+ hash[file] = data_from_redis.select { |meta_data_key, _value| META_DATA_KEYS.include?(meta_data_key) }.merge!('data' => data)
107
+ hash[file].merge!(LAST_UPDATED_KEY => hash[file][LAST_UPDATED_KEY].to_i, FIRST_UPDATED_KEY => hash[file][FIRST_UPDATED_KEY].to_i)
108
+ end
109
+
110
+ def coverage_data_from_redis(data_from_redis)
111
+ max = data_from_redis[FILE_LENGTH_KEY].to_i - 1
112
+ Array.new(max + 1) do |index|
113
+ line_coverage = data_from_redis[index.to_s]
114
+ line_coverage.nil? ? nil : line_coverage.to_i
115
+ end
116
+ end
117
+
118
+ def save_report_script_input(key:, file:, file_hash:, data:, report_time:, updated_time:)
119
+ data.each_with_index
120
+ .each_with_object(keys: [key], args: [report_time, updated_time, file, file_hash, @ttl, data.length]) do |(coverage, index), hash|
121
+ if coverage
122
+ hash[:keys] << index
123
+ hash[:args] << coverage
124
+ end
125
+ end
126
+ end
127
+
128
+ def hash_incr_script
129
+ @hash_incr_script ||= @redis.script(:load, <<~LUA)
130
+ local first_updated_at = table.remove(ARGV, 1)
131
+ local last_updated_at = table.remove(ARGV, 1)
132
+ local file = table.remove(ARGV, 1)
133
+ local file_hash = table.remove(ARGV, 1)
134
+ local ttl = table.remove(ARGV, 1)
135
+ local file_length = table.remove(ARGV, 1)
136
+ local hash_key = table.remove(KEYS, 1)
137
+ redis.call('HMSET', hash_key, '#{LAST_UPDATED_KEY}', last_updated_at, '#{FILE_KEY}', file, '#{FILE_HASH}', file_hash, '#{FILE_LENGTH_KEY}', file_length)
138
+ redis.call('HSETNX', hash_key, '#{FIRST_UPDATED_KEY}', first_updated_at)
139
+ for i, key in ipairs(KEYS) do
140
+ if ARGV[i] == '-1' then
141
+ redis.call("HSET", hash_key, key, ARGV[i])
142
+ else
143
+ redis.call("HINCRBY", hash_key, key, ARGV[i])
144
+ end
145
+ end
146
+ if ttl ~= '-1' then
147
+ redis.call("EXPIRE", hash_key, ttl)
148
+ end
149
+ LUA
150
+ end
151
+
152
+ def values_from_redis(local_type, files)
153
+ return files if files.empty?
154
+
155
+ @redis.mget(*files.map { |file| key(file, local_type) }).map do |value|
156
+ value.nil? ? {} : JSON.parse(value)
157
+ end
158
+ end
159
+
160
+ def relative_paths(files)
161
+ files&.map! { |file| full_path_to_relative(file) }
162
+ end
163
+
164
+ def files_set(local_type = nil)
165
+ @redis.smembers(files_key(local_type))
166
+ end
167
+
168
+ def files_key(local_type = nil)
169
+ "#{key_prefix(local_type)}.files"
170
+ end
171
+
172
+ def key(file, local_type = nil, file_hash:)
173
+ [key_prefix(local_type), file, file_hash].join('.')
174
+ end
175
+
176
+ def key_prefix(local_type = nil)
177
+ local_type ||= type
178
+ [@format_version, @redis_namespace, local_type].compact.join('.')
179
+ end
180
+ end
181
+ end
182
+ end
@@ -61,7 +61,7 @@ module Coverband
61
61
  exit 0
62
62
  end
63
63
  relative_path_report = previous_data.each_with_object({}) do |(key, vals), fixed_report|
64
- fixed_report[full_path_to_relative(key)] = vals
64
+ fixed_report[Utils::RelativeFileConverter.convert(key)] = vals
65
65
  end
66
66
  clear!
67
67
  reset_base_key
@@ -80,6 +80,20 @@ module Coverband
80
80
  data ? JSON.parse(data) : {}
81
81
  end
82
82
 
83
+ # Note: This could lead to slight race on redis
84
+ # where multiple processes pull the old coverage and add to it then push
85
+ # the Coverband 2 had the same issue,
86
+ # and the tradeoff has always been acceptable
87
+ def save_report(report)
88
+ data = report.dup
89
+ data = merge_reports(data, coverage)
90
+ save_coverage(data)
91
+ end
92
+
93
+ def raw_store
94
+ @redis
95
+ end
96
+
83
97
  private
84
98
 
85
99
  attr_reader :redis
@@ -22,8 +22,8 @@ module Coverband
22
22
  @project_directory = File.expand_path(Coverband.configuration.root)
23
23
  @ignore_patterns = Coverband.configuration.ignore
24
24
  @store = Coverband.configuration.store
25
- @verbose = Coverband.configuration.verbose
26
- @logger = Coverband.configuration.logger
25
+ @verbose = Coverband.configuration.verbose
26
+ @logger = Coverband.configuration.logger
27
27
  @test_env = Coverband.configuration.test_env
28
28
  @track_gems = Coverband.configuration.track_gems
29
29
  Delta.reset
@@ -55,11 +55,9 @@ module Coverband
55
55
  @store.save_report(files_with_line_usage)
56
56
  end
57
57
  rescue StandardError => e
58
- if @verbose
59
- @logger.error 'coverage failed to store'
60
- @logger.error "error: #{e.inspect} #{e.message}"
61
- @logger.error e.backtrace
62
- end
58
+ @logger&.error 'coverage failed to store'
59
+ @logger&.error "Coverband Error: #{e.inspect} #{e.message}"
60
+ e.backtrace.each { |line| @logger&.error line } if @verbose
63
61
  raise e if @test_env
64
62
  end
65
63