cover_rage 0.0.1 → 0.0.3

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: 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