coverband 4.2.1 → 4.2.2.rc.1

Sign up to get free protection for your applications and to get access to all the features.
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 +7 -22
  5. data/changes.md +13 -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 +174 -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 +24 -33
  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: ca39325a3a42ab0820e60c4162eb0d9aa22d9f2cd0a085cd315aff45445a06a7
4
+ data.tar.gz: 2695638a09c7e6c673cfaba58cd94f59e8997cdf92ca8067bb565f25d8fa907d
5
5
  SHA512:
6
- metadata.gz: c16f4e0351ffdd49ba3732c203b9932d4ee2ee8e9890b59f0c8d1e0beafcb2e8632d5a47ffc946248813284ebcac727785ceba3e38a6f041b0f5c000c42870f2
7
- data.tar.gz: a1119724f958c01dfdb333efe95ed43acc6f2c1396d24efe7a208d8606e33d8de687d73a1099a3a20d5a37dbcef6b43c4ba36fb9d9d194d0a584e02957fdd016
6
+ metadata.gz: 830ddef421419a22aa741d53f78b169e9d1d52fe6b30c72757993e09008bc0a07dd733d67e0381b2eea951410e7217fdb6046e6c19901e44fab211334f84859c
7
+ data.tar.gz: 1df3167a89d98df21658b21b06a7d7c3472b9ccd2eed53b95dc3edf243ee2b066cb780fd9885f823998a09e2deb501a502a3fb73284306318be0539614f63996
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
@@ -12,7 +12,8 @@
12
12
  <a href="#coverage-report">Coverage Report</a> •
13
13
  <a href="#advanced-config">Advanced Config</a> •
14
14
  <a href="#license">License</a> •
15
- <a href="/changes.md">Change Log / Roadmap</a>
15
+ <a href="/changes.md">Change Log / Roadmap</a>
16
+ <a href="/CODE_OF_CONDUCT.md">Code of Conduct</a>
16
17
  </p>
17
18
 
18
19
  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 +162,10 @@ Coverband.configure do |config|
161
162
  config.s3_access_key_id = ENV['AWS_ACCESS_KEY_ID']
162
163
  config.s3_secret_access_key = ENV['AWS_SECRET_ACCESS_KEY']
163
164
 
164
- # config options false, true, or 'debug'. Always use false in production
165
+ # config options false, true. (defaults to false)
165
166
  # 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.
167
+ # and is safe to use if one is investigating issues in production, but it will slightly
168
+ # hit perf.
168
169
  config.verbose = false
169
170
  end
170
171
  ```
@@ -307,27 +308,11 @@ Coverband.start
307
308
 
308
309
  ### Verbose Debug / Development Mode
309
310
 
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.
311
+ 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
312
 
312
313
  ---
313
314
 
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]]
315
+ 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
316
 
332
317
  # Prerequisites
333
318
 
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,12 @@ Feature Ideas:
76
76
 
77
77
  # Alpha / Beta / Release Candidates
78
78
 
79
+ ### Coverband 4.2.2
80
+
81
+ - ?
82
+
83
+ # Released
84
+
79
85
  ### Coverband 4.2.1
80
86
 
81
87
  - larger changes
@@ -83,14 +89,13 @@ Feature Ideas:
83
89
  - fix issue where reports didn't include files with 0 activity
84
90
  - updated runtime relavent lines and runtime percentages
85
91
  - 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
92
+ - I would consider this our test oneshot release, please report any issues
93
+ - improved control over memory vs functionality
94
+ - oneshot support is the best memory and speed if you are on Ruby 2.6.\*
95
+ - 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
96
+ - full hit tracking (The previous and current default), this uses more memory than other options, but provides full robust data
91
97
 
92
-
93
- - small changes fixes
98
+ * small changes fixes
94
99
  - further improvements on eager_loading detection, thanks @kbaum
95
100
  - fix on ignore configuration options
96
101
  - fix broken static server
@@ -112,9 +117,6 @@ Feature Ideas:
112
117
  - additional benchmarks to ensure proper memory handling, thanks @kbaum
113
118
  - default redis TTL, thanks @jjb
114
119
 
115
-
116
- # Released
117
-
118
120
  ### Coverband 4.2.0
119
121
 
120
122
  **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,174 @@
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
+ private
88
+
89
+ def add_coverage_for_file(key, hash)
90
+ data_from_redis = @redis.hgetall(key)
91
+
92
+ return if data_from_redis.empty?
93
+
94
+ file = data_from_redis[FILE_KEY]
95
+ return unless file_hash(file) == data_from_redis[FILE_HASH]
96
+
97
+ data = coverage_data_from_redis(data_from_redis)
98
+ hash[file] = data_from_redis.select { |meta_data_key, _value| META_DATA_KEYS.include?(meta_data_key) }.merge!('data' => data)
99
+ hash[file].merge!(LAST_UPDATED_KEY => hash[file][LAST_UPDATED_KEY].to_i, FIRST_UPDATED_KEY => hash[file][FIRST_UPDATED_KEY].to_i)
100
+ end
101
+
102
+ def coverage_data_from_redis(data_from_redis)
103
+ max = data_from_redis[FILE_LENGTH_KEY].to_i - 1
104
+ Array.new(max + 1) do |index|
105
+ line_coverage = data_from_redis[index.to_s]
106
+ line_coverage.nil? ? nil : line_coverage.to_i
107
+ end
108
+ end
109
+
110
+ def save_report_script_input(key:, file:, file_hash:, data:, report_time:, updated_time:)
111
+ data.each_with_index
112
+ .each_with_object(keys: [key], args: [report_time, updated_time, file, file_hash, @ttl, data.length]) do |(coverage, index), hash|
113
+ if coverage
114
+ hash[:keys] << index
115
+ hash[:args] << coverage
116
+ end
117
+ end
118
+ end
119
+
120
+ def hash_incr_script
121
+ @hash_incr_script ||= @redis.script(:load, <<~LUA)
122
+ local first_updated_at = table.remove(ARGV, 1)
123
+ local last_updated_at = table.remove(ARGV, 1)
124
+ local file = table.remove(ARGV, 1)
125
+ local file_hash = table.remove(ARGV, 1)
126
+ local ttl = table.remove(ARGV, 1)
127
+ local file_length = table.remove(ARGV, 1)
128
+ local hash_key = table.remove(KEYS, 1)
129
+ redis.call('HMSET', hash_key, '#{LAST_UPDATED_KEY}', last_updated_at, '#{FILE_KEY}', file, '#{FILE_HASH}', file_hash, '#{FILE_LENGTH_KEY}', file_length)
130
+ redis.call('HSETNX', hash_key, '#{FIRST_UPDATED_KEY}', first_updated_at)
131
+ for i, key in ipairs(KEYS) do
132
+ if ARGV[i] == '-1' then
133
+ redis.call("HSET", hash_key, key, ARGV[i])
134
+ else
135
+ redis.call("HINCRBY", hash_key, key, ARGV[i])
136
+ end
137
+ end
138
+ if ttl ~= '-1' then
139
+ redis.call("EXPIRE", hash_key, ttl)
140
+ end
141
+ LUA
142
+ end
143
+
144
+ def values_from_redis(local_type, files)
145
+ return files if files.empty?
146
+
147
+ @redis.mget(*files.map { |file| key(file, local_type) }).map do |value|
148
+ value.nil? ? {} : JSON.parse(value)
149
+ end
150
+ end
151
+
152
+ def relative_paths(files)
153
+ files&.map! { |file| full_path_to_relative(file) }
154
+ end
155
+
156
+ def files_set(local_type = nil)
157
+ @redis.smembers(files_key(local_type))
158
+ end
159
+
160
+ def files_key(local_type = nil)
161
+ "#{key_prefix(local_type)}.files"
162
+ end
163
+
164
+ def key(file, local_type = nil, file_hash:)
165
+ [key_prefix(local_type), file, file_hash].join('.')
166
+ end
167
+
168
+ def key_prefix(local_type = nil)
169
+ local_type ||= type
170
+ [@format_version, @redis_namespace, local_type].compact.join('.')
171
+ end
172
+ end
173
+ end
174
+ 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
 
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+
5
+ module Coverband
6
+ module Collectors
7
+ ###
8
+ # This class tracks view file usage via ActiveSupport::Notifications
9
+ #
10
+ # This is a port of Flatfoot, a project I open sourced years ago, but am now rolling into Coverband
11
+ # https://github.com/livingsocial/flatfoot
12
+ #
13
+ # TODO: test and ensure slim, haml, and other support
14
+ ###
15
+ class ViewTracker
16
+ DEFAULT_TARGET = Dir.glob('app/views/**/*.html.erb').reject { |file| file.match(/(_mailer)/) }
17
+ attr_accessor :target, :logged_views, :views_to_record
18
+ attr_reader :logger, :roots, :store, :ignore_patterns
19
+
20
+ def initialize(options = {})
21
+ raise NotImplementedError, 'View Tracker requires Rails 4 or greater' unless self.class.supported_version?
22
+
23
+ @project_directory = File.expand_path(Coverband.configuration.root)
24
+ @ignore_patterns = Coverband.configuration.ignore
25
+ @store = options.fetch(:store) { Coverband.configuration.store }
26
+ @logger = options.fetch(:logger) { Coverband.configuration.logger }
27
+ @target = options.fetch(:target) { DEFAULT_TARGET }
28
+
29
+ @roots = options.fetch(:roots) { Coverband.configuration.all_root_patterns }
30
+ @roots = @roots.split(',') if @roots.is_a?(String)
31
+
32
+ @logged_views = []
33
+ @views_to_record = []
34
+ end
35
+
36
+ ###
37
+ # This method is called on every render call, so we try to reduce method calls
38
+ # and ensure high performance
39
+ ###
40
+ def track_views(_name, _start, _finish, _id, payload)
41
+ if (file = payload[:identifier])
42
+ if newly_seen_file?(file)
43
+ logged_views << file
44
+ views_to_record << file if track_file?(file)
45
+ end
46
+ end
47
+
48
+ ###
49
+ # Annoyingly while you get full path for templates
50
+ # notifications only pass part of the path for layouts dropping any format info
51
+ # such as .html.erb or .js.erb
52
+ # http://edgeguides.rubyonrails.org/active_support_instrumentation.html#render_partial-action_view
53
+ ###
54
+ return unless (layout_file = payload[:layout])
55
+ return unless newly_seen_file?(layout_file)
56
+
57
+ logged_views << layout_file
58
+ views_to_record << layout_file if track_file?(layout_file, layout: true)
59
+ end
60
+
61
+ def used_views
62
+ views = redis_store.smembers(tracker_key)
63
+ normalized_views = []
64
+ views.each do |view|
65
+ roots.each do |root|
66
+ view = view.gsub(/#{root}/, '')
67
+ end
68
+ normalized_views << view
69
+ end
70
+ normalized_views
71
+ end
72
+
73
+ def unused_views
74
+ recently_used_views = used_views
75
+ all_views = target.reject { |view| recently_used_views.include?(view) }
76
+ # since layouts don't include format we count them used if they match with ANY formats
77
+ all_views.reject { |view| view.match(/\/layouts\//) && recently_used_views.any? { |used_view| view.include?(used_view) } }
78
+ end
79
+
80
+ def reset_recordings
81
+ redis_store.del(tracker_key)
82
+ end
83
+
84
+ def self.supported_version?
85
+ defined?(Rails) && defined?(Rails::VERSION) && Rails::VERSION::STRING.split('.').first.to_i >= 4
86
+ end
87
+
88
+ def report_views_tracked
89
+ views_to_record.each do |file|
90
+ redis_store.sadd(tracker_key, file)
91
+ end
92
+ self.views_to_record = []
93
+ rescue StandardError => e
94
+ # we don't want to raise errors if Coverband can't reach redis.
95
+ # This is a nice to have not a bring the system down
96
+ logger&.error "Coverband: view_tracker failed to store, error #{e.class.name}"
97
+ end
98
+
99
+ protected
100
+
101
+ def newly_seen_file?(file)
102
+ return false if logged_views.include?(file)
103
+
104
+ true
105
+ end
106
+
107
+ def track_file?(file, options = {})
108
+ @ignore_patterns.none? do |pattern|
109
+ file.include?(pattern)
110
+ end && (file.start_with?(@project_directory) || options[:layout])
111
+ end
112
+
113
+ private
114
+
115
+ def redis_store
116
+ store.raw_store
117
+ end
118
+
119
+ def tracker_key
120
+ 'render_tracker'
121
+ end
122
+ end
123
+ end
124
+ end