cover_rage 0.0.6 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7bea62a74d8c6c9e2bbcd7918e28a2ee43f71f0548b43d4813438a02a3bffa32
4
- data.tar.gz: 47c5ef4df517fd22406db7dd4b93b10089a2d2b08bccd7a24b0e2ae23c76e55c
3
+ metadata.gz: 2d22a15a12e4376aa1a441dd5f4d21e7a6043390e1cef64e8739da1e6fcb81eb
4
+ data.tar.gz: ec07f2d32071bce21d03771e3d3ba3c6dd24e0e1cae745962fe0a36b89bc00ef
5
5
  SHA512:
6
- metadata.gz: d4fecf3fc7435ba7e96b171e864d2463d7256c4c2f987546c3d10ea13543da59e779530a38094dd45bde32aba6cefc9c4663ebdd0df52b0a178d4ca318c4b658
7
- data.tar.gz: f91e8dce7f3770d31547899baa4e5add2641af32596568b83bfc76525b1a44b82faa51064debc420c24535572dd9e89372a77f487054de15aea3b17a2d7043c7
6
+ metadata.gz: 16754fdef9cc29c0f6304e56a6f0c70dd91bc18c294bc944a7d3f927b642b07127acb079ecb9617156c3d0f703ec440536433f97b0f53d9742f6541d7df087cf
7
+ data.tar.gz: 94bddf50ab05b0f8227e42743910343b5f7c595c74774994bc60f5ecfc807dc1ec3f4bfcd5227ccb24a5207a2cfad77864077e934a986f91d4c8e254921cf359
@@ -3,13 +3,13 @@
3
3
  require 'uri'
4
4
  module CoverRage
5
5
  module Config
6
- def self.root_path
7
- @root_path ||= ENV.fetch('COVER_RAGE_ROOT_PATH', defined?(Rails) && Rails.root ? Rails.root.to_s : Dir.pwd)
6
+ def self.path_prefix
7
+ @path_prefix ||= ENV.fetch('COVER_RAGE_PATH_PREFIX', defined?(Rails) && Rails.root ? Rails.root.to_s : Dir.pwd)
8
8
  end
9
9
 
10
10
  def self.store
11
11
  @store ||= begin
12
- uri = URI.parse(ENV.fetch('COVER_RAGE_STORE_URL'))
12
+ uri = URI.parse(ENV.fetch('COVER_RAGE_STORE_URL', "pstore:#{File.join(Dir.pwd, 'cover_rage.pstore')}"))
13
13
  case uri.scheme
14
14
  when 'redis', 'rediss'
15
15
  require 'cover_rage/stores/redis'
@@ -17,21 +17,17 @@ module CoverRage
17
17
  when 'sqlite'
18
18
  require 'cover_rage/stores/sqlite'
19
19
  CoverRage::Stores::Sqlite.new(uri.path)
20
+ when 'pstore'
21
+ require 'cover_rage/stores/pstore'
22
+ CoverRage::Stores::Pstore.new(uri.path)
23
+ else
24
+ raise "Unsupported store: #{uri.scheme}"
20
25
  end
21
26
  end
22
27
  end
23
28
 
24
- def self.sleep_duration
25
- @sleep_duration ||= begin
26
- args =
27
- ENV.fetch('COVER_RAGE_SLEEP_DURATION', '60:90').split(':').map!(&:to_i).first(2)
28
- args.push(args.first.succ) if args.length < 2
29
- Range.new(*args, true)
30
- end
31
- end
32
-
33
- def self.disable?
34
- @disable ||= ENV.key?('COVER_RAGE_DISABLE')
29
+ def self.interval
30
+ @interval ||= ENV.fetch('COVER_RAGE_INTERVAL', '60').to_i
35
31
  end
36
32
  end
37
33
  end
@@ -7,5 +7,12 @@ module CoverRage
7
7
  CoverRage::Launcher.start if pid.zero?
8
8
  pid
9
9
  end
10
+
11
+ def daemon(...)
12
+ CoverRage::Launcher.recorder.save(Coverage.result(stop: false))
13
+ returned = super
14
+ CoverRage::Launcher.start
15
+ returned
16
+ end
10
17
  end
11
18
  end
@@ -7,18 +7,17 @@ require 'coverage'
7
7
 
8
8
  module CoverRage
9
9
  class Launcher
10
+ singleton_class.attr_reader :recorder
11
+
10
12
  def self.start(**kwargs)
11
13
  if @recorder.nil?
12
14
  @recorder = CoverRage::Recorder.new(
13
15
  store: CoverRage::Config.store,
14
- root_path: CoverRage::Config.root_path,
16
+ path_prefix: CoverRage::Config.path_prefix,
15
17
  **kwargs
16
18
  )
17
19
  end
18
20
  @recorder.start
19
- return unless Process.respond_to?(:_fork)
20
- return if Process.singleton_class < CoverRage::ForkHook
21
-
22
21
  Process.singleton_class.prepend(CoverRage::ForkHook)
23
22
  end
24
23
  end
@@ -6,13 +6,11 @@ require 'digest'
6
6
 
7
7
  module CoverRage
8
8
  class Recorder
9
- SLEEP_DURATION = Config.sleep_duration
10
- COVERAGE_STOP_STATES = %i[idle suspended].freeze
11
9
  attr_reader :store
12
10
 
13
- def initialize(root_path:, store:)
11
+ def initialize(path_prefix:, store:)
14
12
  @store = store
15
- @root_path = root_path.end_with?('/') ? root_path : "#{root_path}/"
13
+ @path_prefix = path_prefix.end_with?('/') ? path_prefix : "#{path_prefix}/"
16
14
  @digest = Digest::MD5.new
17
15
  @file_cache = {}
18
16
  end
@@ -20,27 +18,28 @@ module CoverRage
20
18
  def start
21
19
  return if @thread&.alive?
22
20
 
23
- Coverage.start if COVERAGE_STOP_STATES.include?(Coverage.state)
21
+ unless Coverage.running?
22
+ Coverage.start
23
+ at_exit { save(Coverage.result) }
24
+ end
24
25
  @thread = Thread.new do
26
+ interval = Config.interval
27
+ jitter = 0.15
25
28
  loop do
26
- sleep(rand(SLEEP_DURATION))
29
+ sleep(interval + rand * interval * jitter)
27
30
  save(Coverage.result(stop: false, clear: true))
28
31
  end
29
- ensure
30
- save(Coverage.result)
31
32
  end
32
33
  end
33
34
 
34
- private
35
-
36
35
  def save(coverage_result)
37
36
  records = []
38
37
  coverage_result.map do |filepath, execution_count|
39
38
  filepath = File.expand_path(filepath) unless filepath.start_with?('/')
40
- next unless filepath.start_with?(@root_path)
39
+ next unless filepath.start_with?(@path_prefix)
41
40
  next if execution_count.all? { _1.nil? || _1.zero? }
42
41
 
43
- relative_path = filepath.delete_prefix(@root_path)
42
+ relative_path = filepath.delete_prefix(@path_prefix)
44
43
  revision, source = read_file_with_revision(filepath)
45
44
 
46
45
  records << Record.new(
@@ -53,6 +52,8 @@ module CoverRage
53
52
  @store.import(records) if records.any?
54
53
  end
55
54
 
55
+ private
56
+
56
57
  def read_file_with_revision(path)
57
58
  return @file_cache[path] if @file_cache.key?(path)
58
59
 
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cover_rage/record'
4
+ require 'pstore'
5
+
6
+ module CoverRage
7
+ module Stores
8
+ class Pstore
9
+ def initialize(path)
10
+ @store = PStore.new(path, true)
11
+ end
12
+
13
+ def import(records)
14
+ @store.transaction do
15
+ persisted_records = @store.keys.map { @store[_1] }
16
+ records_to_save = Record.merge(persisted_records, records)
17
+ records_to_save.each { @store[_1.path] = _1 }
18
+ end
19
+ end
20
+
21
+ def find(path)
22
+ @store.transaction { @store[path] }
23
+ end
24
+
25
+ def list
26
+ @store.transaction do
27
+ @store.keys.map { @store[_1] }
28
+ end
29
+ end
30
+
31
+ def clear
32
+ @store.transaction do
33
+ @store.keys.each { @store.delete(_1) }
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -22,13 +22,11 @@ module CoverRage
22
22
  loop do
23
23
  break if @redis.watch(KEY) do
24
24
  records_to_save = Record.merge(list, records)
25
- if records_to_save.any?
26
- arguments = []
27
- records_to_save.each do |record|
28
- arguments.push(record.path, JSON.dump(record.to_h))
29
- end
30
- @redis.multi { _1.hset(KEY, *arguments) }
25
+ arguments = []
26
+ records_to_save.each do |record|
27
+ arguments.push(record.path, JSON.dump(record.to_h))
31
28
  end
29
+ @redis.multi { _1.hset(KEY, *arguments) }
32
30
  end
33
31
  end
34
32
  end
@@ -17,27 +17,37 @@ module CoverRage
17
17
  execution_count text not null
18
18
  )
19
19
  SQL
20
+ process_ext = Module.new
21
+ process_ext.module_exec(self) do |store|
22
+ define_method :_fork do
23
+ store.instance_variable_get(:@db).close
24
+ pid = super()
25
+ store.instance_variable_set(:@db, SQLite3::Database.new(path))
26
+ pid
27
+ end
28
+ end
29
+ Process.singleton_class.prepend(process_ext)
20
30
  end
21
31
 
22
32
  def import(records)
23
- @db.transaction do
33
+ @db.transaction(:exclusive) do
24
34
  records_to_save = Record.merge(list, records)
25
- if records_to_save.any?
26
- @db.execute(
27
- "insert or replace into records (path, revision, source, execution_count) values #{
28
- (['(?,?,?,?)'] * records_to_save.length).join(',')
29
- }",
30
- records_to_save.each_with_object([]) do |record, memo|
31
- memo.push(
32
- record.path,
33
- record.revision,
34
- record.source,
35
- JSON.dump(record.execution_count)
36
- )
37
- end
38
- )
39
- end
35
+ @db.execute(
36
+ "insert or replace into records (path, revision, source, execution_count) values #{
37
+ (['(?,?,?,?)'] * records_to_save.length).join(',')
38
+ }",
39
+ records_to_save.each_with_object([]) do |record, memo|
40
+ memo.push(
41
+ record.path,
42
+ record.revision,
43
+ record.source,
44
+ JSON.dump(record.execution_count)
45
+ )
46
+ end
47
+ )
40
48
  end
49
+ rescue SQLite3::BusyException
50
+ retry
41
51
  end
42
52
 
43
53
  def find(path)
@@ -59,7 +69,7 @@ module CoverRage
59
69
  def list
60
70
  @db
61
71
  .execute('select path, revision, source, execution_count from records')
62
- .map! do |(path, revision, source, execution_count)|
72
+ .map do |(path, revision, source, execution_count)|
63
73
  Record.new(
64
74
  path: path,
65
75
  revision: revision,
metadata CHANGED
@@ -1,15 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cover_rage
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Weihang Jian
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2023-06-02 00:00:00.000000000 Z
10
+ date: 2024-12-28 00:00:00.000000000 Z
12
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: minitest
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '5.18'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '5.18'
13
26
  - !ruby/object:Gem::Dependency
14
27
  name: rake
15
28
  requirement: !ruby/object:Gem::Requirement
@@ -25,25 +38,41 @@ dependencies:
25
38
  - !ruby/object:Gem::Version
26
39
  version: '13.0'
27
40
  - !ruby/object:Gem::Dependency
28
- name: minitest
41
+ name: redis
29
42
  requirement: !ruby/object:Gem::Requirement
30
43
  requirements:
31
44
  - - "~>"
32
45
  - !ruby/object:Gem::Version
33
- version: '5.18'
46
+ version: '5.3'
34
47
  type: :development
35
48
  prerelease: false
36
49
  version_requirements: !ruby/object:Gem::Requirement
37
50
  requirements:
38
51
  - - "~>"
39
52
  - !ruby/object:Gem::Version
40
- version: '5.18'
53
+ version: '5.3'
54
+ - !ruby/object:Gem::Dependency
55
+ name: sqlite3
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '2.5'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '2.5'
41
68
  description: |
42
- A Ruby production code coverage tool designed to assist you in identifying unused code, offering the following features:
69
+ cover_rage is a Ruby code coverage tool designed to be simple and easy to use. It can be used not only for test coverage but also in production services to identify unused code.
70
+
71
+ Key features:
43
72
 
44
- 1. easy setup
45
- 2. minimal performance overhead
46
- 3. minimal external dependencies
73
+ 1. Runs in continuous processes (e.g., Rails servers)
74
+ 2. Zero dependencies
75
+ 3. Supports forking and daemonization without additional setup
47
76
  email: tonytonyjan@gmail.com
48
77
  executables:
49
78
  - cover_rage
@@ -60,13 +89,13 @@ files:
60
89
  - lib/cover_rage/reporters/html_reporter.rb
61
90
  - lib/cover_rage/reporters/html_reporter/index.html.erb
62
91
  - lib/cover_rage/reporters/json_reporter.rb
92
+ - lib/cover_rage/stores/pstore.rb
63
93
  - lib/cover_rage/stores/redis.rb
64
94
  - lib/cover_rage/stores/sqlite.rb
65
95
  homepage: https://github.com/tonytonyjan/cover_rage
66
96
  licenses:
67
97
  - MIT
68
98
  metadata: {}
69
- post_install_message:
70
99
  rdoc_options: []
71
100
  require_paths:
72
101
  - lib
@@ -74,15 +103,16 @@ required_ruby_version: !ruby/object:Gem::Requirement
74
103
  requirements:
75
104
  - - ">="
76
105
  - !ruby/object:Gem::Version
77
- version: 2.3.0
106
+ version: '3.2'
78
107
  required_rubygems_version: !ruby/object:Gem::Requirement
79
108
  requirements:
80
109
  - - ">="
81
110
  - !ruby/object:Gem::Version
82
111
  version: '0'
83
112
  requirements: []
84
- rubygems_version: 3.4.13
85
- signing_key:
113
+ rubygems_version: 3.6.2
86
114
  specification_version: 4
87
- summary: A Ruby production code coverage tool
115
+ summary: cover_rage is a Ruby code coverage tool designed to be simple and easy to
116
+ use. It can be used not only for test coverage but also in production services to
117
+ identify unused code.
88
118
  test_files: []