cover_rage 0.0.1 → 0.0.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bb5a49ec54d2f39963fa0704561b1d49f27fd04b4070b760ad1a0dd361eaa2e4
4
- data.tar.gz: 926ef7443cf1efecfe908d9d6e2a74799b1cf5ddf260d16f03ddc9cd6c821ada
3
+ metadata.gz: 9f3618b26d2fc9a7c3f32354d8e30bed52f529d9d6f4de7c648a66245300e1c4
4
+ data.tar.gz: 4bfb79ee930c1886138dc25d90d804bed14e200e0576fc63c3aee9693ca348a9
5
5
  SHA512:
6
- metadata.gz: 9ada124a26bd41c144f54391c5d424f396a1f260e2c2210d4d971c0905ff04578d3bd346d33115b5de1168e8476a869fb8710ab3f3b0b1201c01fcfaf5629bb9
7
- data.tar.gz: 0264676faf4d418cc51b27560fa3645b6a7d806aae8ac22aed6b835463d3318bc42083b6ca00766378bcbc8948276ac9d5a49c9412c1a855d1881aeb572a9bda
6
+ metadata.gz: 6ea002a43c449f4c7b2f0c323e30c1f1354a8bbdcf10ca3659262acb62ace07138f64cb80a34e1d2a47d07055f5a6b28902d40d3e4234a36fe25322755c179c7
7
+ data.tar.gz: 93a1ea6738c01fcf076fabc73f6ca0f7708e0db1897efec3944ad1cbdcd33419b42807988356441e564646db51fa72936d82ab0263b0b555a3658447d31e3ee9
data/bin/cover_rage CHANGED
@@ -1,5 +1,37 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'rackup'
5
- Dir.chdir(__dir__) { Rackup::Server.start }
4
+ if ARGV.first == 'clear'
5
+ require 'cover_rage/config'
6
+ CoverRage::Config.store.clear
7
+ exit
8
+ end
9
+
10
+ require 'optparse'
11
+ options = { format: 'html' }
12
+ OptionParser.new do |parser|
13
+ parser.banner = <<~USAGE
14
+ Clear store:
15
+ #{$PROGRAM_NAME} clear
16
+
17
+ Export report:
18
+ #{$PROGRAM_NAME} [options]
19
+ USAGE
20
+ parser.on('-fFORMAT', '--format=FORMAT', 'output format (json|html)') { options[:format] = _1 }
21
+ end.parse!
22
+
23
+ reporter =
24
+ case options[:format]
25
+ when 'json'
26
+ require 'cover_rage/reporters/json_reporter'
27
+ CoverRage::Reporters::JsonReporter.new
28
+ when 'html'
29
+ require 'cover_rage/reporters/html_reporter'
30
+ CoverRage::Reporters::HtmlReporter.new
31
+ else
32
+ warn "Unknown format: #{options[:format]}"
33
+ exit 1
34
+ end
35
+
36
+ require 'cover_rage/config'
37
+ puts reporter.report(CoverRage::Config.store.list)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cover_rage
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Weihang Jian
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-04-07 00:00:00.000000000 Z
11
+ date: 2023-05-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -24,27 +24,34 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '13.0'
27
- description: coverage recorder
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5.18'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5.18'
41
+ description: |
42
+ A Ruby production code coverage tool designed to assist you in identifying unused code, offering the following features:
43
+
44
+ 1. easy setup
45
+ 2. minimal performance overhead
46
+ 3. minimal external dependencies
28
47
  email: tonytonyjan@gmail.com
29
48
  executables:
30
49
  - cover_rage
31
50
  extensions: []
32
51
  extra_rdoc_files: []
33
52
  files:
34
- - bin/config.ru
35
53
  - bin/cover_rage
36
54
  - lib/cover_rage.rb
37
- - lib/cover_rage/config.rb
38
- - lib/cover_rage/fork_hook.rb
39
- - lib/cover_rage/launcher.rb
40
- - lib/cover_rage/record.rb
41
- - lib/cover_rage/recorder.rb
42
- - lib/cover_rage/stores/redis.rb
43
- - lib/cover_rage/stores/sqlite.rb
44
- - lib/cover_rage/viewer.rb
45
- - lib/cover_rage/viewer/file.erb
46
- - lib/cover_rage/viewer/index.erb
47
- - lib/cover_rage/viewer/layout.erb
48
55
  homepage: https://github.com/tonytonyjan/cover_rage
49
56
  licenses:
50
57
  - MIT
@@ -64,8 +71,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
64
71
  - !ruby/object:Gem::Version
65
72
  version: '0'
66
73
  requirements: []
67
- rubygems_version: 3.4.10
74
+ rubygems_version: 3.4.12
68
75
  signing_key:
69
76
  specification_version: 4
70
- summary: coverage recorder
77
+ summary: A Ruby production code coverage tool
71
78
  test_files: []
data/bin/config.ru DELETED
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'cover_rage/viewer'
4
- require 'cover_rage/config'
5
- run CoverRage::Viewer.new(store: CoverRage::Config.store)
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'uri'
4
- module CoverRage
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)
8
- end
9
-
10
- def self.store
11
- @store ||= begin
12
- uri = URI.parse(ENV.fetch('COVER_RAGE_STORE_URL'))
13
- case uri.scheme
14
- when 'redis'
15
- require 'cover_rage/stores/redis'
16
- CoverRage::Stores::Redis.new(uri.to_s)
17
- when 'sqlite'
18
- require 'cover_rage/stores/sqlite'
19
- CoverRage::Stores::Sqlite.new(uri.path)
20
- end
21
- end
22
- end
23
-
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
- end
33
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module CoverRage
4
- module ForkHook
5
- def _fork
6
- pid = super
7
- CoverRage::Launcher.start if pid.zero?
8
- pid
9
- end
10
- end
11
- end
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'cover_rage/recorder'
4
- require 'cover_rage/config'
5
- require 'cover_rage/fork_hook'
6
- require 'coverage'
7
-
8
- module CoverRage
9
- class Launcher
10
- def self.start(**kwargs)
11
- if @recorder.nil?
12
- @recorder = CoverRage::Recorder.new(
13
- store: CoverRage::Config.store,
14
- root_path: CoverRage::Config.root_path,
15
- **kwargs
16
- )
17
- end
18
- @recorder.start
19
- return unless Process.respond_to?(:_fork)
20
- return if Process.singleton_class < CoverRage::ForkHook
21
-
22
- Process.singleton_class.prepend(CoverRage::ForkHook)
23
- end
24
- end
25
- end
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module CoverRage
4
- Record = Data.define(:path, :revision, :source, :execution_count) do
5
- def self.merge(existing, current)
6
- records_to_save = []
7
- current.each do |record|
8
- found = existing.find { _1.path == record.path }
9
- records_to_save <<
10
- if found.nil? || record.revision != found.revision
11
- record
12
- else
13
- record + found
14
- end
15
- end
16
- records_to_save
17
- end
18
-
19
- def +(other)
20
- with(
21
- execution_count: execution_count.map.with_index do |item, index|
22
- item.nil? ? nil : item + other.execution_count[index]
23
- end
24
- )
25
- end
26
- end
27
- end
@@ -1,65 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'cover_rage/config'
4
- require 'cover_rage/record'
5
- require 'digest'
6
-
7
- module CoverRage
8
- class Recorder
9
- SLEEP_DURATION = Config.sleep_duration
10
- COVERAGE_STOP_STATES = %i[idle suspended].freeze
11
- attr_reader :store
12
-
13
- def initialize(root_path:, store:)
14
- @store = store
15
- @root_path = root_path.end_with?('/') ? root_path : "#{root_path}/"
16
- @digest = Digest::MD5.new
17
- @file_cache = {}
18
- end
19
-
20
- def start
21
- return if @thread&.alive?
22
-
23
- Coverage.start if COVERAGE_STOP_STATES.include?(Coverage.state)
24
- @thread = Thread.new do
25
- loop do
26
- sleep(rand(SLEEP_DURATION))
27
- save(Coverage.result(stop: false, clear: true))
28
- end
29
- ensure
30
- save(Coverage.result)
31
- end
32
- end
33
-
34
- private
35
-
36
- def save(coverage_result)
37
- records = []
38
- coverage_result.map do |filepath, execution_count|
39
- filepath = File.expand_path(filepath) unless filepath.start_with?('/')
40
- next unless filepath.start_with?(@root_path)
41
- next if execution_count.all? { _1.nil? || _1.zero? }
42
-
43
- relative_path = filepath.delete_prefix(@root_path)
44
- revision, source = read_file_with_revision(filepath)
45
-
46
- records << Record.new(
47
- path: relative_path,
48
- revision: revision,
49
- source: source,
50
- execution_count: execution_count
51
- )
52
- end
53
- @store.import(records) if records.any?
54
- end
55
-
56
- def read_file_with_revision(path)
57
- return @file_cache[path] if @file_cache.key?(path)
58
-
59
- @file_cache[path] = begin
60
- content = File.read(path)
61
- [@digest.hexdigest(content), content]
62
- end
63
- end
64
- end
65
- end
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'redis'
4
- require 'json'
5
-
6
- module CoverRage
7
- module Stores
8
- class Redis
9
- KEY = 'cover_rage_records'
10
- def initialize(url)
11
- @redis = ::Redis.new(url: url)
12
- end
13
-
14
- def import(records)
15
- loop do
16
- break if @redis.watch(KEY) do
17
- records_to_save = Record.merge(list, records)
18
- if records_to_save.any?
19
- arguments = []
20
- records_to_save.each do |record|
21
- arguments.push(record.path, JSON.dump(record.to_h))
22
- end
23
- @redis.multi { _1.hset(KEY, *arguments) }
24
- end
25
- end
26
- end
27
- end
28
-
29
- def find(path)
30
- result = @redis.hget(KEY, path)
31
- return nil if result.nil?
32
-
33
- Record.new(**JSON.parse(result))
34
- end
35
-
36
- def list
37
- result = @redis.hgetall(KEY)
38
- return [] if result.empty?
39
-
40
- result.map { |_, value| Record.new(**JSON.parse(value)) }
41
- end
42
-
43
- def clear
44
- @redis.del(KEY)
45
- end
46
- end
47
- end
48
- end
@@ -1,77 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'cover_rage/record'
4
- require 'sqlite3'
5
- require 'json'
6
-
7
- module CoverRage
8
- module Stores
9
- class Sqlite
10
- def initialize(path)
11
- @db = SQLite3::Database.new(path)
12
- @db.execute <<-SQL
13
- create table if not exists records (
14
- path text primary key not null,
15
- revision blob not null,
16
- source text not null,
17
- execution_count text not null
18
- )
19
- SQL
20
- end
21
-
22
- def import(records)
23
- @db.transaction do
24
- 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
40
- end
41
- end
42
-
43
- def find(path)
44
- rows = @db.execute(
45
- 'select revision, source, execution_count from records where path = ? limit 1',
46
- [path]
47
- )
48
- return nil if rows.empty?
49
-
50
- revision, source, execution_count = rows.first
51
- Record.new(
52
- path: path,
53
- revision: revision,
54
- source: source,
55
- execution_count: JSON.parse(execution_count)
56
- )
57
- end
58
-
59
- def list
60
- @db
61
- .execute('select path, revision, source, execution_count from records')
62
- .map! do |(path, revision, source, execution_count)|
63
- Record.new(
64
- path: path,
65
- revision: revision,
66
- source: source,
67
- execution_count: JSON.parse(execution_count)
68
- )
69
- end
70
- end
71
-
72
- def clear
73
- @db.execute('delete from records')
74
- end
75
- end
76
- end
77
- end
@@ -1,51 +0,0 @@
1
- <link
2
- rel="stylesheet"
3
- href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/default.min.css"
4
- />
5
- <style>
6
- .line {
7
- width: 100%;
8
- display: inline-block;
9
- counter-increment: line_number;
10
- }
11
-
12
- .green {
13
- background-color: rgb(221, 255, 221);
14
- }
15
-
16
- .red {
17
- background-color: rgb(255, 212, 212);
18
- }
19
-
20
- .number {
21
- display: inline-block;
22
- text-align: right;
23
- margin-right: 0.5em;
24
- background-color: lightgray;
25
- padding: 0 0.5em 0 1.5em;
26
- }
27
- </style>
28
- <h1><%= path %></h1>
29
- <pre><code id="code"><%= source_code.encode(xml: :text) %></code></pre>
30
- <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
31
- <script id="execution_count" type="application/json"><%= execution_count.encode(xml: :text) %></script>
32
- <script>
33
- const execution_count = JSON.parse(
34
- document.querySelector("#execution_count").childNodes[0].nodeValue
35
- );
36
- const digit_width = Math.floor(Math.log10(Math.max(...execution_count))) + 1;
37
- const $code = document.querySelector("#code");
38
- $code.innerHTML = hljs
39
- .highlight($code.textContent, {
40
- language: "ruby",
41
- })
42
- .value.split("\n")
43
- .map((line, index) => {
44
- const value = execution_count[index];
45
- let color = "";
46
- if (typeof value === "number") color = value > 0 ? "green" : "red";
47
- const number = typeof value === "number" ? value : "-";
48
- return `<span class="line ${color}"><span class="number">${number.toString().padStart(digit_width, ' ')}</span>${line}</span>`;
49
- })
50
- .join("\n");
51
- </script>
@@ -1,28 +0,0 @@
1
- <table>
2
- <thead>
3
- <tr>
4
- <th>path</th>
5
- <th>lines</th>
6
- <th>relevancy</th>
7
- <th>hit</th>
8
- <th>miss</th>
9
- <th>coverage (%)</th>
10
- </tr>
11
- </thead>
12
- <tbody>
13
- <% items.each do |item| %>
14
- <tr>
15
- <td>
16
- <a href="/file?path=<%= URI.encode_uri_component(item.path) %>"
17
- ><%= item.path %></a
18
- >
19
- </td>
20
- <td style="text-align: right"><%= item.number_of_lines %></td>
21
- <td style="text-align: right"><%= item.number_of_relevancies %></td>
22
- <td style="text-align: right"><%= item.number_of_hits %></td>
23
- <td style="text-align: right"><%= item.number_of_misses %></td>
24
- <td style="text-align: right"><%= '%.2f' % (item.coverage * 100) %></td>
25
- </tr>
26
- <% end %>
27
- </tbody>
28
- </table>
@@ -1,9 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title>CoverRage</title>
5
- </head>
6
- <body>
7
- <%= yield %>
8
- </body>
9
- </html>
@@ -1,80 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'cover_rage/record'
4
- require 'erb'
5
- require 'json'
6
-
7
- module CoverRage
8
- class Viewer
9
- LAYOUT_PAGE = ERB.new(File.read("#{__dir__}/viewer/layout.erb"))
10
- INDEX_PAGE = ERB.new(File.read("#{__dir__}/viewer/index.erb"))
11
- FILE_PAGE = ERB.new(File.read("#{__dir__}/viewer/file.erb"))
12
-
13
- class Item
14
- attr_reader(
15
- :path,
16
- :execution_count,
17
- :number_of_lines,
18
- :number_of_hits,
19
- :number_of_misses,
20
- :number_of_relevancies,
21
- :coverage
22
- )
23
-
24
- def initialize(record)
25
- @path = record.path
26
- @execution_count = record.execution_count
27
- @number_of_lines = record.execution_count.length
28
- @number_of_hits = record.execution_count.count { _1&.positive? }
29
- @number_of_misses = record.execution_count.count { _1&.zero? }
30
- @number_of_relevancies = record.execution_count.count { !_1.nil? }
31
- @coverage = @number_of_hits.to_f / @number_of_relevancies
32
- end
33
- end
34
-
35
- def initialize(store:)
36
- @store = store
37
- end
38
-
39
- def call(env)
40
- template =
41
- case env['PATH_INFO']
42
- when '/'
43
- items =
44
- @store
45
- .list
46
- .map! { Item.new(_1) }
47
- .sort! do |a, b|
48
- next -1 if b.coverage.nan?
49
- next 1 if a.coverage.nan?
50
-
51
- a.coverage <=> b.coverage
52
- end
53
- INDEX_PAGE
54
- when '/file'
55
- path = URI.decode_www_form(env['QUERY_STRING']).assoc('path').last
56
-
57
- record = @store.find(path)
58
- return [404, {}, []] if record.nil?
59
-
60
- source_code = record.source
61
- execution_count = JSON.dump(record.execution_count)
62
- FILE_PAGE
63
- when '/clear'
64
- return [405, {}, []] unless env['REQUEST_METHOD'] == 'DELETE'
65
-
66
- @store.clear
67
- return [302, { 'location' => '/' }, []]
68
- else return [404, {}, []]
69
- end
70
- body = layout { template.result(binding) }
71
- [200, {}, [body]]
72
- end
73
-
74
- private
75
-
76
- def layout
77
- LAYOUT_PAGE.result(binding)
78
- end
79
- end
80
- end