cover_rage 0.0.1 → 0.0.4

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: 56c953122e72d8aaa3a4a50129595cfc36fb20f1c2feea245b255295a5ba8a90
4
+ data.tar.gz: 7b5b4972c31670c4c9ab6b5298682ce7b8d258f4a0ff31faf8790856f1691428
5
5
  SHA512:
6
- metadata.gz: 9ada124a26bd41c144f54391c5d424f396a1f260e2c2210d4d971c0905ff04578d3bd346d33115b5de1168e8476a869fb8710ab3f3b0b1201c01fcfaf5629bb9
7
- data.tar.gz: 0264676faf4d418cc51b27560fa3645b6a7d806aae8ac22aed6b835463d3318bc42083b6ca00766378bcbc8948276ac9d5a49c9412c1a855d1881aeb572a9bda
6
+ metadata.gz: 26cd15a4e159851270b8c9b351e1047049eb509e1ebc322634d94ab8c53f2e8e42d22ca4521c07af7d784e3d8fa5993fc59e57bbeba0d2c49fa83b86ba5b5a88
7
+ data.tar.gz: 38e670bed372e6117c64967fc32a124e2dcac34734e14f05e954b282de01cd557af0847a69c16474e3e13c4352ec57a878d96b95925fc08c88612a5774b6c89c
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)
@@ -29,5 +29,9 @@ module CoverRage
29
29
  Range.new(*args, true)
30
30
  end
31
31
  end
32
+
33
+ def self.disable?
34
+ @disable ||= ENV.key?('COVER_RAGE_DISABLE')
35
+ end
32
36
  end
33
37
  end
@@ -0,0 +1,215 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>CoverRage</title>
8
+ <link
9
+ rel="stylesheet"
10
+ href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/default.min.css"
11
+ />
12
+ <style>
13
+ .line {
14
+ width: 100%;
15
+ display: inline-block;
16
+ counter-increment: line_number;
17
+ }
18
+
19
+ .green {
20
+ background-color: rgb(221, 255, 221);
21
+ }
22
+
23
+ .red {
24
+ background-color: rgb(255, 212, 212);
25
+ }
26
+
27
+ .number {
28
+ display: inline-block;
29
+ text-align: right;
30
+ margin-right: 0.5em;
31
+ background-color: lightgray;
32
+ padding: 0 0.5em 0 1.5em;
33
+ }
34
+
35
+ .nav {
36
+ display: flex;
37
+ list-style: none;
38
+ padding-left: 0;
39
+ }
40
+ .nav > * {
41
+ margin-left: 8px;
42
+ }
43
+ </style>
44
+ </head>
45
+ <body>
46
+ <main id="main"></main>
47
+
48
+ <div style="display: none">
49
+ <div id="page-index">
50
+ <nav>
51
+ <ul class="nav">
52
+ <li><a href="#/">Index</a></li>
53
+ </ul>
54
+ </nav>
55
+ <table>
56
+ <thead>
57
+ <tr>
58
+ <th>path</th>
59
+ <th>lines</th>
60
+ <th>relevancy</th>
61
+ <th>hit</th>
62
+ <th>miss</th>
63
+ <th>coverage (%)</th>
64
+ </tr>
65
+ </thead>
66
+ <tbody></tbody>
67
+ </table>
68
+ </div>
69
+ <div id="page-file">
70
+ <nav>
71
+ <ul class="nav">
72
+ <li><a href="#/">Index</a></li>
73
+ <li>></li>
74
+ <li id="title"></li>
75
+ </ul>
76
+ </nav>
77
+ <pre><code id="code"></code></pre>
78
+ </div>
79
+ </div>
80
+
81
+ <script id="records" type="application/json"><%= records %></script>
82
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
83
+ <script>
84
+ const main = document.getElementById("main");
85
+ const page_index = document.getElementById("page-index");
86
+ const title = document.createTextNode("");
87
+ document.getElementById("title").appendChild(title);
88
+ const code = document.getElementById("code");
89
+ const page_file = document.getElementById("page-file");
90
+ const records = JSON.parse(
91
+ document.getElementById("records").childNodes[0].nodeValue
92
+ );
93
+
94
+ const route = (path) => {
95
+ if (path.startsWith("/file/")) {
96
+ const filepath = path.slice(6);
97
+ if (records.find((record) => record.path === filepath))
98
+ return { page: "file", path: filepath };
99
+ }
100
+
101
+ return { page: "index" };
102
+ };
103
+
104
+ const render = (routing) => {
105
+ let page;
106
+ if (routing.page === "file") {
107
+ const record = records.find(({ path }) => path === routing.path);
108
+ page = page_file;
109
+ title.nodeValue = routing.path;
110
+ const digit_width =
111
+ Math.floor(Math.log10(Math.max(...record.execution_count))) + 1;
112
+ code.innerHTML = hljs
113
+ .highlight(
114
+ records.find(({ path }) => path === routing.path).source,
115
+ {
116
+ language: "ruby",
117
+ }
118
+ )
119
+ .value.split("\n")
120
+ .map((line, index) => {
121
+ const value = record.execution_count[index];
122
+ let color = "";
123
+ if (typeof value === "number")
124
+ color = value > 0 ? "green" : "red";
125
+ const number = typeof value === "number" ? value : "-";
126
+ return `<span class="line ${color}"><span class="number">${number
127
+ .toString()
128
+ .padStart(digit_width, " ")}</span>${line}</span>`;
129
+ })
130
+ .join("\n");
131
+ }
132
+ if (routing.page === "index") {
133
+ page = page_index;
134
+ }
135
+ if (main.childNodes.length === 0) main.appendChild(page);
136
+ else main.replaceChild(page, main.childNodes[0]);
137
+ };
138
+ const createTdTextNode = (text) => {
139
+ const td = document.createElement("td");
140
+ td.appendChild(document.createTextNode(text));
141
+ return td;
142
+ };
143
+
144
+ page_index.querySelector("tbody").appendChild(
145
+ records
146
+ .map(({ path, revision, source, execution_count }) => {
147
+ const hit = execution_count.reduce(
148
+ (accumulator, current) =>
149
+ current > 0 ? accumulator + 1 : accumulator,
150
+ 0
151
+ );
152
+ const miss = execution_count.reduce(
153
+ (accumulator, current) =>
154
+ current === 0 ? accumulator + 1 : accumulator,
155
+ 0
156
+ );
157
+ const relevancy = execution_count.reduce(
158
+ (accumulator, current) =>
159
+ current !== null ? accumulator + 1 : accumulator,
160
+ 0
161
+ );
162
+ const coverage = hit / relevancy;
163
+ return {
164
+ path,
165
+ lines: execution_count.length,
166
+ hit,
167
+ miss,
168
+ relevancy,
169
+ coverage,
170
+ };
171
+ })
172
+ .sort(({ coverage: a }, { coverage: b }) => {
173
+ if (isNaN(b)) return -1;
174
+ if (isNaN(a)) return 1;
175
+ return a - b;
176
+ })
177
+ .reduce(
178
+ (fragment, { path, lines, hit, miss, relevancy, coverage }) => {
179
+ const tr = document.createElement("tr");
180
+ tr.appendChild(
181
+ (() => {
182
+ const anchor = document.createElement("a");
183
+ anchor.href = `#/file/${path}`;
184
+ anchor.appendChild(document.createTextNode(path));
185
+ const td = document.createElement("td");
186
+ td.appendChild(anchor);
187
+ return td;
188
+ })()
189
+ );
190
+ tr.appendChild(createTdTextNode(lines));
191
+ tr.appendChild(createTdTextNode(relevancy));
192
+ tr.appendChild(createTdTextNode(hit));
193
+ tr.appendChild(createTdTextNode(miss));
194
+ tr.appendChild(
195
+ createTdTextNode(Math.round(coverage * 10000) / 100)
196
+ );
197
+ fragment.appendChild(tr);
198
+ return fragment;
199
+ },
200
+ document.createDocumentFragment()
201
+ )
202
+ );
203
+
204
+ document.addEventListener("DOMContentLoaded", () => {
205
+ const routing = route(window.location.hash.slice(1));
206
+ render(routing);
207
+ });
208
+
209
+ window.addEventListener("popstate", (event) => {
210
+ const routing = route(window.location.hash.slice(1));
211
+ render(routing);
212
+ });
213
+ </script>
214
+ </body>
215
+ </html>
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+ require 'json'
5
+
6
+ module CoverRage
7
+ module Reporters
8
+ class HtmlReporter
9
+ def report(records)
10
+ records = JSON.dump(records.map(&:to_h))
11
+ ERB.new(File.read("#{__dir__}/html_reporter/index.html.erb")).result(binding)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module CoverRage
6
+ module Reporters
7
+ class JsonReporter
8
+ def report(records)
9
+ JSON.dump(records.map(&:to_h))
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'cover_rage/record'
3
4
  require 'redis'
4
5
  require 'json'
5
6
 
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.4
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,14 +24,32 @@ 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
55
  - lib/cover_rage/config.rb
@@ -39,12 +57,11 @@ files:
39
57
  - lib/cover_rage/launcher.rb
40
58
  - lib/cover_rage/record.rb
41
59
  - lib/cover_rage/recorder.rb
60
+ - lib/cover_rage/reporters/html_reporter.rb
61
+ - lib/cover_rage/reporters/html_reporter/index.html.erb
62
+ - lib/cover_rage/reporters/json_reporter.rb
42
63
  - lib/cover_rage/stores/redis.rb
43
64
  - 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
65
  homepage: https://github.com/tonytonyjan/cover_rage
49
66
  licenses:
50
67
  - MIT
@@ -64,8 +81,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
64
81
  - !ruby/object:Gem::Version
65
82
  version: '0'
66
83
  requirements: []
67
- rubygems_version: 3.4.10
84
+ rubygems_version: 3.4.12
68
85
  signing_key:
69
86
  specification_version: 4
70
- summary: coverage recorder
87
+ summary: A Ruby production code coverage tool
71
88
  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,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