cover_rage 1.2.0 → 1.4.0

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: 5621019f54428ba46ed131c128c14da48bb2e9fd2bc10e283a3c0c8f8642b2c0
4
- data.tar.gz: a1a8912ce53c591c178923f790c6eba60651fb22834489513b89368a785dfea2
3
+ metadata.gz: 2a74ea3de571687288c9efaad0bf9005ffe9eb69ccc3f23e4826aa9edf54ee60
4
+ data.tar.gz: b3c50a447fd07a467635dddfa5c849c40043eba0b20543bd00f02d6bbfeb0871
5
5
  SHA512:
6
- metadata.gz: 10fafa62c5ddc3c37b1be895d7875c116c44343ea2bed5b18ff3571003958c6867ef40c2945ec70f4747a84760dc786e9ef5fc6c14865e770f7283a05a6004a0
7
- data.tar.gz: 5d60032466d783830936617366c326df471fde464428fbabf00b0e8e390b1f9b1c79d9c384ea15f5d80448a1f6edd4d3ec5e369b273dbc659e19f6ba053da3d9
6
+ metadata.gz: ab94c0ab0dff6a0e47106a123a436999e311e62f4822b63feee8f40bfcc855854121bf8a1a279d633fe161f8aae07bab547dfc1582b5cce3a988583b7d19f158
7
+ data.tar.gz: 75e16d91fae6150723cc17034e574b7512996e4c4a2fb4b1ae42700b6267a473ffc6feb8e172760fd6f2b9d9bf2acfdea73f49c4633351a8056b8249edcb745f
@@ -25,7 +25,7 @@ module CoverRage
25
25
  if item.nil? && other.last_executed_at[index].nil? then nil
26
26
  elsif item.nil? then other.last_executed_at[index]
27
27
  elsif other.last_executed_at[index].nil? then item
28
- else [item.to_i, other.last_executed_at[index].to_i].max
28
+ else [item, other.last_executed_at[index]].max
29
29
  end
30
30
  end
31
31
  )
@@ -2,7 +2,6 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <meta http-equiv="X-UA-Compatible" content="IE=edge" />
6
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
6
  <title>CoverRage</title>
8
7
  <link
@@ -15,50 +14,41 @@
15
14
  display: inline-block;
16
15
  counter-increment: line_number;
17
16
  }
18
-
19
- .green {
20
- background-color: rgb(221, 255, 221);
21
- }
22
-
23
- .red {
24
- background-color: rgb(255, 212, 212);
25
- }
26
-
17
+ .green { background-color: rgb(221, 255, 221); }
18
+ .blue { background-color: rgb(221, 221, 255); }
19
+ .red { background-color: rgb(255, 212, 212); }
27
20
  .number {
28
21
  display: inline-block;
29
22
  text-align: right;
30
23
  background-color: lightgray;
31
24
  padding: 0 0.5em 0 1.5em;
32
25
  }
33
-
34
26
  .timestamp {
35
27
  display: inline-block;
36
28
  text-align: right;
37
29
  margin-right: 0.5em;
38
30
  background-color: lightgray;
39
- padding: 0 0.5em 0 0.5em;
31
+ padding: 0 0.5em;
40
32
  }
41
-
42
33
  .nav {
43
34
  display: flex;
44
35
  list-style: none;
45
36
  padding-left: 0;
46
- }
47
- .nav > * {
48
- margin-left: 8px;
37
+ gap: 8px;
49
38
  }
50
39
  </style>
51
40
  </head>
52
41
  <body>
53
42
  <main id="main"></main>
54
43
 
55
- <div style="display: none">
56
- <div id="page-index">
44
+ <template id="tmpl-index">
45
+ <div>
57
46
  <nav>
58
47
  <ul class="nav">
59
48
  <li><a href="#/">Index</a></li>
60
49
  </ul>
61
50
  </nav>
51
+ <label>stale threshold <input id="stale-threshold" type="number" min="0" value="30"> days</label>
62
52
  <table>
63
53
  <thead>
64
54
  <tr>
@@ -67,160 +57,165 @@
67
57
  <th>relevancy</th>
68
58
  <th>hit</th>
69
59
  <th>miss</th>
70
- <th>coverage (%)</th>
60
+ <th>stale</th>
61
+ <th class="sortable" data-sort="coverage" style="cursor:pointer">coverage (%) ▼</th>
62
+ <th class="sortable" data-sort="staleness" style="cursor:pointer">staleness (%)</th>
71
63
  </tr>
72
64
  </thead>
73
65
  <tbody></tbody>
74
66
  </table>
75
67
  </div>
76
- <div id="page-file">
68
+ </template>
69
+
70
+ <template id="tmpl-file">
71
+ <div>
77
72
  <nav>
78
73
  <ul class="nav">
79
74
  <li><a href="#/">Index</a></li>
80
- <li>></li>
75
+ <li>&gt;</li>
81
76
  <li id="title"></li>
82
77
  </ul>
83
78
  </nav>
79
+ <label>stale threshold <input id="stale-threshold" type="number" min="0" value="30"> days</label>
84
80
  <pre><code id="code"></code></pre>
85
81
  </div>
86
- </div>
82
+ </template>
87
83
 
88
84
  <script id="records" type="application/json"><%= records %></script>
89
85
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
90
86
  <script>
87
+ const records = JSON.parse(document.getElementById("records").textContent);
91
88
  const main = document.getElementById("main");
92
- const page_index = document.getElementById("page-index");
93
- const title = document.createTextNode("");
94
- document.getElementById("title").appendChild(title);
95
- const code = document.getElementById("code");
96
- const page_file = document.getElementById("page-file");
97
- const records = JSON.parse(
98
- document.getElementById("records").childNodes[0].nodeValue
99
- );
100
89
 
101
- const route = (path) => {
102
- if (path.startsWith("/file/")) {
103
- const filepath = path.slice(6);
104
- if (records.find((record) => record.path === filepath))
105
- return { page: "file", path: filepath };
90
+ function route() {
91
+ const hash = window.location.hash.slice(1);
92
+ if (hash.startsWith("/file/")) {
93
+ const path = hash.slice(6);
94
+ const record = records.find((r) => r.path === path);
95
+ if (record) return { page: "file", path, record };
106
96
  }
107
-
108
97
  return { page: "index" };
109
- };
98
+ }
110
99
 
111
- const render = (routing) => {
112
- let page;
113
- if (routing.page === "file") {
114
- const record = records.find(({ path }) => path === routing.path);
115
- page = page_file;
116
- title.nodeValue = routing.path;
117
- const digit_width =
118
- Math.floor(Math.log10(Math.max(...record.execution_count))) + 1;
119
- code.innerHTML = hljs
120
- .highlight(
121
- records.find(({ path }) => path === routing.path).source,
122
- {
123
- language: "ruby",
124
- }
125
- )
126
- .value.split("\n")
127
- .map((line, index) => {
128
- const value = record.execution_count[index];
129
- let color = "";
130
- if (typeof value === "number")
131
- color = value > 0 ? "green" : "red";
132
- const number = typeof value === "number" ? value : "-";
133
- const posix_time = record.last_executed_at[index];
134
- const iso8601_time = typeof posix_time === "number" ? new Date(posix_time * 1000).toISOString().slice(0, 10) : "-";
135
- return `<span class="line ${color}"><span class="number">${
136
- number.toString().padStart(digit_width, " ")
137
- }</span><time class="timestamp">${
138
- iso8601_time.padStart(10, " ")
139
- }</time>${line}</span>`;
140
- })
141
- .join("\n");
100
+ function cloneTemplate(id) {
101
+ return document.getElementById(id).content.cloneNode(true).firstElementChild;
102
+ }
103
+
104
+ function formatDate(posixTime) {
105
+ if (typeof posixTime !== "number") return "-";
106
+ return new Date(posixTime * 1000).toISOString().slice(0, 10);
107
+ }
108
+
109
+ function colorClass(count, lastExecutedAt, staleThresholdDays) {
110
+ if (typeof count !== "number") return "";
111
+ if (count === 0) return "red";
112
+ if (typeof lastExecutedAt === "number") {
113
+ const ageDays = (Date.now() / 1000 - lastExecutedAt) / 86400;
114
+ if (ageDays > staleThresholdDays) return "blue";
142
115
  }
143
- if (routing.page === "index") {
144
- page = page_index;
116
+ return "green";
117
+ }
118
+
119
+ function summarize({ path, execution_count, last_executed_at }, staleThresholdDays) {
120
+ let hit = 0, miss = 0, relevancy = 0, stale = 0;
121
+ const staleCutoff = Date.now() / 1000 - staleThresholdDays * 86400;
122
+ for (let i = 0; i < execution_count.length; i++) {
123
+ const count = execution_count[i];
124
+ if (count !== null) {
125
+ relevancy++;
126
+ if (count > 0) {
127
+ hit++;
128
+ const t = last_executed_at[i];
129
+ if (typeof t === "number" && t < staleCutoff) stale++;
130
+ } else {
131
+ miss++;
132
+ }
133
+ }
145
134
  }
146
- if (main.childNodes.length === 0) main.appendChild(page);
147
- else main.replaceChild(page, main.childNodes[0]);
148
- };
149
- const createTdTextNode = (text) => {
150
- const td = document.createElement("td");
151
- td.appendChild(document.createTextNode(text));
152
- return td;
153
- };
135
+ return {
136
+ path,
137
+ lines: execution_count.length,
138
+ relevancy,
139
+ hit,
140
+ miss,
141
+ stale,
142
+ coverage: hit / relevancy,
143
+ staleness: stale / relevancy,
144
+ };
145
+ }
154
146
 
155
- page_index.querySelector("tbody").appendChild(
156
- records
157
- .map(({ path, revision, source, execution_count }) => {
158
- const hit = execution_count.reduce(
159
- (accumulator, current) =>
160
- current > 0 ? accumulator + 1 : accumulator,
161
- 0
162
- );
163
- const miss = execution_count.reduce(
164
- (accumulator, current) =>
165
- current === 0 ? accumulator + 1 : accumulator,
166
- 0
167
- );
168
- const relevancy = execution_count.reduce(
169
- (accumulator, current) =>
170
- current !== null ? accumulator + 1 : accumulator,
171
- 0
172
- );
173
- const coverage = hit / relevancy;
174
- return {
175
- path,
176
- lines: execution_count.length,
177
- hit,
178
- miss,
179
- relevancy,
180
- coverage,
181
- };
182
- })
183
- .sort(({ coverage: a }, { coverage: b }) => {
184
- if (isNaN(b)) return -1;
185
- if (isNaN(a)) return 1;
186
- return a - b;
147
+ let staleThresholdDays = 30;
148
+ let sortKey = "coverage";
149
+ let sortAsc = true;
150
+
151
+ function getStaleThreshold(el) {
152
+ const input = el.querySelector("#stale-threshold");
153
+ input.value = staleThresholdDays;
154
+ input.addEventListener("change", () => {
155
+ staleThresholdDays = Number(input.value) || 30;
156
+ render();
157
+ });
158
+ return staleThresholdDays;
159
+ }
160
+
161
+ function renderIndex() {
162
+ const el = cloneTemplate("tmpl-index");
163
+ const staleThresholdDays = getStaleThreshold(el);
164
+ const tbody = el.querySelector("tbody");
165
+ el.querySelectorAll(".sortable").forEach((th) => {
166
+ const key = th.dataset.sort;
167
+ th.textContent = th.textContent.replace(/ [▲▼]$/, "") + (sortKey === key ? (sortAsc ? " ▲" : " ▼") : "");
168
+ th.addEventListener("click", () => {
169
+ if (sortKey === key) { sortAsc = !sortAsc; } else { sortKey = key; sortAsc = true; }
170
+ render();
171
+ });
172
+ });
173
+ const rows = records.map((r) => summarize(r, staleThresholdDays)).sort((a, b) => {
174
+ const av = a[sortKey], bv = b[sortKey];
175
+ if (isNaN(bv)) return -1;
176
+ if (isNaN(av)) return 1;
177
+ return sortAsc ? av - bv : bv - av;
178
+ });
179
+ for (const { path, lines, relevancy, hit, miss, stale, coverage, staleness } of rows) {
180
+ const tr = tbody.insertRow();
181
+ tr.insertCell().innerHTML = `<a href="#/file/${path}">${path}</a>`;
182
+ tr.insertCell().textContent = lines;
183
+ tr.insertCell().textContent = relevancy;
184
+ tr.insertCell().textContent = hit;
185
+ tr.insertCell().textContent = miss;
186
+ tr.insertCell().textContent = stale;
187
+ tr.insertCell().textContent = Math.round(coverage * 10000) / 100;
188
+ tr.insertCell().textContent = Math.round(staleness * 10000) / 100;
189
+ }
190
+ return el;
191
+ }
192
+
193
+ function renderFile(record) {
194
+ const el = cloneTemplate("tmpl-file");
195
+ const staleThresholdDays = getStaleThreshold(el);
196
+ el.querySelector("#title").textContent = record.path;
197
+ const digitWidth = Math.floor(Math.log10(Math.max(...record.execution_count))) + 1;
198
+ const highlighted = hljs.highlight(record.source, { language: "ruby" }).value;
199
+ el.querySelector("#code").innerHTML = highlighted
200
+ .split("\n")
201
+ .map((line, i) => {
202
+ const count = record.execution_count[i];
203
+ const displayCount = typeof count === "number" ? String(count).padStart(digitWidth) : "-".padStart(digitWidth);
204
+ const date = formatDate(record.last_executed_at[i]).padStart(10);
205
+ return `<span class="line ${colorClass(count, record.last_executed_at[i], staleThresholdDays)}"><span class="number">${displayCount}</span><time class="timestamp">${date}</time>${line}</span>`;
187
206
  })
188
- .reduce(
189
- (fragment, { path, lines, hit, miss, relevancy, coverage }) => {
190
- const tr = document.createElement("tr");
191
- tr.appendChild(
192
- (() => {
193
- const anchor = document.createElement("a");
194
- anchor.href = `#/file/${path}`;
195
- anchor.appendChild(document.createTextNode(path));
196
- const td = document.createElement("td");
197
- td.appendChild(anchor);
198
- return td;
199
- })()
200
- );
201
- tr.appendChild(createTdTextNode(lines));
202
- tr.appendChild(createTdTextNode(relevancy));
203
- tr.appendChild(createTdTextNode(hit));
204
- tr.appendChild(createTdTextNode(miss));
205
- tr.appendChild(
206
- createTdTextNode(Math.round(coverage * 10000) / 100)
207
- );
208
- fragment.appendChild(tr);
209
- return fragment;
210
- },
211
- document.createDocumentFragment()
212
- )
213
- );
207
+ .join("\n");
208
+ return el;
209
+ }
214
210
 
215
- document.addEventListener("DOMContentLoaded", () => {
216
- const routing = route(window.location.hash.slice(1));
217
- render(routing);
218
- });
211
+ function render() {
212
+ const routing = route();
213
+ const page = routing.page === "file" ? renderFile(routing.record) : renderIndex();
214
+ main.replaceChildren(page);
215
+ }
219
216
 
220
- window.addEventListener("popstate", (event) => {
221
- const routing = route(window.location.hash.slice(1));
222
- render(routing);
223
- });
217
+ render();
218
+ window.addEventListener("hashchange", render);
224
219
  </script>
225
220
  </body>
226
221
  </html>
@@ -9,13 +9,10 @@ module CoverRage
9
9
  module Stores
10
10
  class Redis
11
11
  KEY = 'cover_rage_records'
12
+ IS_REDIS_BELOW_V5 = Gem::Version.new(::Redis::VERSION) < Gem::Version.new('5')
12
13
  def initialize(url)
13
- @redis =
14
- if url.start_with?('rediss')
15
- ::Redis.new(url:, ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE })
16
- else
17
- ::Redis.new(url:)
18
- end
14
+ @redis = new_redis(url)
15
+ @redis_for_below_v5 = new_redis(url) if IS_REDIS_BELOW_V5
19
16
  end
20
17
 
21
18
  def transaction(&)
@@ -42,7 +39,14 @@ module CoverRage
42
39
  end
43
40
 
44
41
  def list
45
- result = @redis.hgetall(KEY)
42
+ # For Redis versions below 5, we need to use the separate client to read
43
+ # the data while the transaction is in progress.
44
+ client = if Thread.current[:redis_multi] && IS_REDIS_BELOW_V5
45
+ @redis_for_below_v5
46
+ else
47
+ @redis
48
+ end
49
+ result = client.hgetall(KEY)
46
50
  return [] if result.empty?
47
51
 
48
52
  result.map { |_, value| Record.new(**JSON.parse(value)) }
@@ -51,6 +55,16 @@ module CoverRage
51
55
  def clear
52
56
  @redis.del(KEY)
53
57
  end
58
+
59
+ private
60
+
61
+ def new_redis(url)
62
+ if url.start_with?('rediss')
63
+ ::Redis.new(url:, ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE })
64
+ else
65
+ ::Redis.new(url:)
66
+ end
67
+ end
54
68
  end
55
69
  end
56
70
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cover_rage
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Weihang Jian
@@ -69,7 +69,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
69
69
  - !ruby/object:Gem::Version
70
70
  version: '0'
71
71
  requirements: []
72
- rubygems_version: 4.0.4
72
+ rubygems_version: 4.0.9
73
73
  specification_version: 4
74
74
  summary: cover_rage is a Ruby code coverage tool designed to be simple and easy to
75
75
  use. It can be used not only for test coverage but also in production services to