cover_rage 1.2.0 → 1.3.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: 00ad4585a873585456f2c9948a88bcab8d54438b5c952a7fdb03a0e411763e3e
4
+ data.tar.gz: 231662f71294e8e983e7632c8ad530b3c18752f2e6d4d0a5d2ea3ea2de0edc90
5
5
  SHA512:
6
- metadata.gz: 10fafa62c5ddc3c37b1be895d7875c116c44343ea2bed5b18ff3571003958c6867ef40c2945ec70f4747a84760dc786e9ef5fc6c14865e770f7283a05a6004a0
7
- data.tar.gz: 5d60032466d783830936617366c326df471fde464428fbabf00b0e8e390b1f9b1c79d9c384ea15f5d80448a1f6edd4d3ec5e369b273dbc659e19f6ba053da3d9
6
+ metadata.gz: '048eb40f6ed0df7c75e4f353ce17fa516345a2832d6b25497453d4bb94aa302197e8e57c5fdbe50444d3a35da91bf641d1a0bbbd2b8d911cf457988016588932'
7
+ data.tar.gz: 587134f993d40afb1c4b9242e030aac02ea886436a1e3912413630f1e7f4e97a403b1b2693a4af8907df23e9a4a8f91e7fa2f782089fa7609fe997ed6e45fa88
@@ -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>
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.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Weihang Jian