lumitrace 0.6.2 → 0.7.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 +4 -4
- data/README.md +7 -5
- data/docs/ai-help.md +7 -4
- data/docs/ai-schema.md +1 -1
- data/docs/lumitrace_html_example.png +0 -0
- data/docs/spec.md +3 -3
- data/docs/tutorial.ja.md +3 -3
- data/docs/tutorial.md +3 -3
- data/exe/lumitrace +11 -0
- data/lib/lumitrace/generate_resulted_html.rb +22 -1
- data/lib/lumitrace/generate_resulted_html_renderer.js +321 -7
- data/lib/lumitrace/help_manifest.rb +7 -4
- data/lib/lumitrace/record_require.rb +1 -1
- data/lib/lumitrace/version.rb +1 -1
- data/lib/lumitrace.rb +6 -3
- data/sample/sample_project/.github/workflows/lumitrace-sample.yml +1 -1
- data/sample/sample_project/README.md +1 -1
- data/test/test_lumitrace.rb +2 -2
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 95fd4185e9243312420705cc551f30f16ce4399e19c79165683bf97dbf6da70b
|
|
4
|
+
data.tar.gz: '0998518cd551365fd8c146855fa8a767316665ba1f2a4bdd6db3e83680cc17a9'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7e2adb4bd754617497dc8d8b7925d44356aa010a7ddf0942bc2f3631d83fc7aba6468fcd008e01742aaf83b26ebe2b1809f244eefd4b728b0321e9fe11b71221
|
|
7
|
+
data.tar.gz: 82ba698e166b72ff0d47d0f630a76348641189bfccc4393ecea714fb974b24fb0921666784c34a9627ad143aae0443a1e0823eaa1c51f30e82a2af99cf8419d3
|
data/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Lumitrace instruments Ruby source code at load time, records expression results, and renders an HTML view that overlays recorded values on your code. It is designed for quick, local “what happened here?” inspection during test runs or scripts.
|
|
4
4
|
|
|
5
|
+
[HTML view example](./docs/lumitrace_html_example.png)
|
|
6
|
+
|
|
5
7
|
## Useful links
|
|
6
8
|
|
|
7
9
|
- [runv/](https://ko1.github.io/lumitrace/runv/): Lumitrace demonstration Ruby playground with inlined tracing
|
|
@@ -50,7 +52,7 @@ Write JSON output explicitly:
|
|
|
50
52
|
|
|
51
53
|
```bash
|
|
52
54
|
lumitrace -j path/to/entry.rb
|
|
53
|
-
lumitrace --json
|
|
55
|
+
lumitrace --json out/lumitrace_recorded.json path/to/entry.rb
|
|
54
56
|
```
|
|
55
57
|
|
|
56
58
|
Restrict to specific line ranges:
|
|
@@ -94,10 +96,10 @@ require "lumitrace/enable"
|
|
|
94
96
|
|
|
95
97
|
## Output
|
|
96
98
|
|
|
97
|
-
- Text: printed by default; use `--text
|
|
98
|
-
- HTML: `lumitrace_recorded.html` by default, or `--html
|
|
99
|
+
- Text: printed by default; use `--text PATH` to write to a file.
|
|
100
|
+
- HTML: `lumitrace_recorded.html` by default, or `--html PATH`.
|
|
99
101
|
- JSON: written only when `--json` (CLI) or `LUMITRACE_JSON` (library/CLI) is provided. Default filename is `lumitrace_recorded.json`.
|
|
100
|
-
- JSON collection mode: `--collect-mode
|
|
102
|
+
- JSON collection mode: `--collect-mode last|types|history` (default `last`).
|
|
101
103
|
- Fork/exec: merged by default. Child processes write fragments under `LUMITRACE_RESULTS_DIR`.
|
|
102
104
|
|
|
103
105
|
JSON event entries always include `types` (type-name => count).
|
|
@@ -151,4 +153,4 @@ Run the CLI locally:
|
|
|
151
153
|
|
|
152
154
|
```bash
|
|
153
155
|
lumitrace path/to/entry.rb
|
|
154
|
-
```
|
|
156
|
+
```
|
data/docs/ai-help.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
AI agents: run `lumitrace help --format json` to get structured help.
|
|
6
6
|
|
|
7
|
-
- Version: 0.
|
|
7
|
+
- Version: 0.7.0
|
|
8
8
|
- Help version: 1
|
|
9
9
|
- Primary JSON entrypoint: `lumitrace help --format json`
|
|
10
10
|
- Schema JSON entrypoint: `lumitrace schema --format json`
|
|
@@ -20,6 +20,7 @@ AI agents: run `lumitrace help --format json` to get structured help.
|
|
|
20
20
|
- Then switch to `--collect-mode last` for final value inspection on suspicious lines.
|
|
21
21
|
- Use `--collect-mode history --max-samples N` only when value transitions matter.
|
|
22
22
|
- Combine `--range` or `--git-diff` to keep outputs small and focused.
|
|
23
|
+
- -j, -h, -t are flags that enable output with default paths. Use --json PATH, --html PATH, --text PATH to specify output paths.
|
|
23
24
|
|
|
24
25
|
## Commands
|
|
25
26
|
- `lumitrace [options] script.rb [ruby_opt]`
|
|
@@ -39,9 +40,9 @@ AI agents: run `lumitrace help --format json` to get structured help.
|
|
|
39
40
|
## Key Options
|
|
40
41
|
- `--collect-mode` (default="last"; values=last,types,history)
|
|
41
42
|
- `--max-samples` (default=3; Used by history mode.)
|
|
42
|
-
- `-j
|
|
43
|
-
- `-h
|
|
44
|
-
- `-t
|
|
43
|
+
- `-j` / `--json PATH` (Emit JSON output. -j uses default path; --json PATH writes to PATH.)
|
|
44
|
+
- `-h` / `--html PATH` (Emit HTML output. -h uses default path; --html PATH writes to PATH.)
|
|
45
|
+
- `-t` / `--text PATH` (Emit text output. -t uses default path; --text PATH writes to PATH.)
|
|
45
46
|
- `-g, --git-diff[=MODE]` (Restrict instrumentation to diff hunks.)
|
|
46
47
|
- `--range SPEC` (repeatable=true; Restrict instrumentation to file ranges.)
|
|
47
48
|
- `--git-diff-context N` (Expand diff hunks by +/-N lines.)
|
|
@@ -54,5 +55,7 @@ AI agents: run `lumitrace help --format json` to get structured help.
|
|
|
54
55
|
- `lumitrace --collect-mode history --max-samples 5 -j app.rb`
|
|
55
56
|
- `lumitrace --collect-mode types -h -j app.rb`
|
|
56
57
|
- `lumitrace --collect-mode last -j exec bin/rails test`
|
|
58
|
+
- `lumitrace --json output.json exec bin/rails test`
|
|
59
|
+
- `lumitrace -j --html report.html app.rb`
|
|
57
60
|
- `lumitrace help --format json`
|
|
58
61
|
- `lumitrace schema --format json`
|
data/docs/ai-schema.md
CHANGED
|
Binary file
|
data/docs/spec.md
CHANGED
|
@@ -269,9 +269,9 @@ lumitrace [options] exec CMD [args...]
|
|
|
269
269
|
```
|
|
270
270
|
|
|
271
271
|
- Text is rendered by default (from in-memory events; no JSON file is required).
|
|
272
|
-
- `-t` enables text output to stdout. `--text
|
|
273
|
-
- `-h` enables HTML output (default path). `--html
|
|
274
|
-
- `-j` enables JSON output (default path). `--json
|
|
272
|
+
- `-t` enables text output to stdout. `--text PATH` writes to a file.
|
|
273
|
+
- `-h` enables HTML output (default path). `--html PATH` writes to a file.
|
|
274
|
+
- `-j` enables JSON output (default path). `--json PATH` writes to a file.
|
|
275
275
|
- `-g` enables git diff with `working` mode. `--git-diff=MODE` selects `staged|base:REV|range:SPEC`.
|
|
276
276
|
- `--max-samples` sets max samples per expression in `collect_mode=history`.
|
|
277
277
|
- `--collect-mode` sets value collection mode (`last|types|history`).
|
data/docs/tutorial.ja.md
CHANGED
|
@@ -325,7 +325,7 @@ lumitrace -t path/to/entry.rb
|
|
|
325
325
|
CI アーティファクトなどに残したいときに便利です。
|
|
326
326
|
|
|
327
327
|
```bash
|
|
328
|
-
lumitrace --text
|
|
328
|
+
lumitrace --text /tmp/lumi.txt path/to/entry.rb
|
|
329
329
|
```
|
|
330
330
|
|
|
331
331
|
|
|
@@ -342,7 +342,7 @@ lumitrace -t -h path/to/entry.rb
|
|
|
342
342
|
テストなど別コマンドをラップして計測します。
|
|
343
343
|
|
|
344
344
|
```bash
|
|
345
|
-
lumitrace --html
|
|
345
|
+
lumitrace --html sample/lumitrace_rake.html exec rake
|
|
346
346
|
```
|
|
347
347
|
|
|
348
348
|
HTML 出力:
|
|
@@ -423,7 +423,7 @@ require "lumitrace"
|
|
|
423
423
|
`LUMITRACE_ENABLE` に CLI 互換のオプションを渡すこともできます:
|
|
424
424
|
|
|
425
425
|
```ruby
|
|
426
|
-
ENV["LUMITRACE_ENABLE"] = "-t --html
|
|
426
|
+
ENV["LUMITRACE_ENABLE"] = "-t --html /tmp/lumi.html -j"
|
|
427
427
|
require "lumitrace"
|
|
428
428
|
```
|
|
429
429
|
|
data/docs/tutorial.md
CHANGED
|
@@ -325,7 +325,7 @@ lumitrace -t path/to/entry.rb
|
|
|
325
325
|
Send text output to a file when you want to archive results or attach them to CI artifacts.
|
|
326
326
|
|
|
327
327
|
```bash
|
|
328
|
-
lumitrace --text
|
|
328
|
+
lumitrace --text /tmp/lumi.txt path/to/entry.rb
|
|
329
329
|
```
|
|
330
330
|
|
|
331
331
|
|
|
@@ -342,7 +342,7 @@ lumitrace -t -h path/to/entry.rb
|
|
|
342
342
|
Wrap another command (like tests) so Lumitrace instruments what that command runs.
|
|
343
343
|
|
|
344
344
|
```bash
|
|
345
|
-
lumitrace --html
|
|
345
|
+
lumitrace --html sample/lumitrace_rake.html exec rake
|
|
346
346
|
```
|
|
347
347
|
|
|
348
348
|
HTML output:
|
|
@@ -422,7 +422,7 @@ require "lumitrace"
|
|
|
422
422
|
You can also pass CLI-style options via `LUMITRACE_ENABLE`:
|
|
423
423
|
|
|
424
424
|
```ruby
|
|
425
|
-
ENV["LUMITRACE_ENABLE"] = "-t --html
|
|
425
|
+
ENV["LUMITRACE_ENABLE"] = "-t --html /tmp/lumi.html -j"
|
|
426
426
|
require "lumitrace"
|
|
427
427
|
```
|
|
428
428
|
|
data/exe/lumitrace
CHANGED
|
@@ -58,6 +58,17 @@ if args.empty?
|
|
|
58
58
|
exit 1
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
+
# Auto-correct: `lumitrace [options] ruby ...` -> exec mode
|
|
62
|
+
# If the first remaining arg is not `exec` and not a `.rb` file,
|
|
63
|
+
# check whether it looks like an external command.
|
|
64
|
+
if args[0] != "exec" && !args[0].end_with?(".rb")
|
|
65
|
+
cmd = args[0]
|
|
66
|
+
if system("command -v #{cmd} > /dev/null 2>&1")
|
|
67
|
+
warn "lumitrace: auto-inserting 'exec' before '#{cmd}' (use 'lumitrace exec #{cmd} ...' to be explicit)"
|
|
68
|
+
args.unshift("exec")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
61
72
|
if opts[:git_diff_mode] == "exec" && args.first != "exec"
|
|
62
73
|
args.unshift("exec")
|
|
63
74
|
opts[:git_diff_mode] = "working"
|
|
@@ -60,7 +60,9 @@ module GenerateResultedHtml
|
|
|
60
60
|
meta = {
|
|
61
61
|
mode: mode_info[:mode],
|
|
62
62
|
mode_text: mode_info[:text],
|
|
63
|
-
max_samples: mode_info[:max_samples]
|
|
63
|
+
max_samples: mode_info[:max_samples],
|
|
64
|
+
ruby_version: RUBY_DESCRIPTION,
|
|
65
|
+
lumitrace_version: defined?(Lumitrace::VERSION) ? Lumitrace::VERSION : nil
|
|
64
66
|
}
|
|
65
67
|
meta[:command] = command_text if command_text && !command_text.to_s.empty?
|
|
66
68
|
{
|
|
@@ -202,6 +204,25 @@ module GenerateResultedHtml
|
|
|
202
204
|
.marker:focus-within .tooltip,
|
|
203
205
|
.marker .tooltip:hover { display: block; }
|
|
204
206
|
.noscript { color: #666; }
|
|
207
|
+
.tree-overview-btn { font-weight: bold; margin-bottom: 8px; }
|
|
208
|
+
.overview-section { background: #fffdf7; border: 1px solid #e5dfd0; border-radius: 8px; padding: 20px; }
|
|
209
|
+
.overview-heading { font-size: 14px; color: #333; margin: 20px 0 8px; border-bottom: 1px solid #e5dfd0; padding-bottom: 4px; }
|
|
210
|
+
.overview-heading:first-child { margin-top: 0; }
|
|
211
|
+
.overview-dl { display: grid; grid-template-columns: auto 1fr; gap: 4px 16px; margin: 0; font-size: 13px; }
|
|
212
|
+
.overview-dl dt { color: #666; font-weight: bold; }
|
|
213
|
+
.overview-dl dd { margin: 0; word-break: break-all; }
|
|
214
|
+
.overview-table { border-collapse: collapse; width: 100%; font-size: 13px; margin: 4px 0; }
|
|
215
|
+
.overview-table th, .overview-table td { padding: 4px 10px; text-align: left; border-bottom: 1px solid #e5dfd0; }
|
|
216
|
+
.overview-table th { color: #555; font-weight: bold; font-size: 12px; }
|
|
217
|
+
.overview-table th.sortable { cursor: pointer; user-select: none; }
|
|
218
|
+
.overview-table th.sortable:hover { color: #2f6f8e; }
|
|
219
|
+
.overview-table th[data-sort="asc"]::after { content: " \\25B2"; font-size: 10px; }
|
|
220
|
+
.overview-table th[data-sort="desc"]::after { content: " \\25BC"; font-size: 10px; }
|
|
221
|
+
.overview-table td { color: #1f1f1f; }
|
|
222
|
+
.overview-summary-table { max-width: 400px; }
|
|
223
|
+
.overview-multitype-table td { font-size: 12px; }
|
|
224
|
+
.overview-file-link { color: #2f6f8e; text-decoration: none; }
|
|
225
|
+
.overview-file-link:hover { text-decoration: underline; }
|
|
205
226
|
@media (max-width: 900px) {
|
|
206
227
|
body { padding: 16px; }
|
|
207
228
|
.report-layout { grid-template-columns: 1fr; }
|
|
@@ -638,6 +638,279 @@
|
|
|
638
638
|
}
|
|
639
639
|
}
|
|
640
640
|
|
|
641
|
+
function computeExpressionCoverage(trace) {
|
|
642
|
+
const seen = new Set();
|
|
643
|
+
let total = 0;
|
|
644
|
+
let executed = 0;
|
|
645
|
+
for (const event of trace || []) {
|
|
646
|
+
if (!event || event.kind === "arg") continue;
|
|
647
|
+
const loc = event.location;
|
|
648
|
+
if (!Array.isArray(loc) || loc.length < 4) continue;
|
|
649
|
+
const key = `${loc[SL]}:${loc[SC]}:${loc[EL]}:${loc[EC]}`;
|
|
650
|
+
if (seen.has(key)) continue;
|
|
651
|
+
seen.add(key);
|
|
652
|
+
total += 1;
|
|
653
|
+
if (Number(event.total) > 0) executed += 1;
|
|
654
|
+
}
|
|
655
|
+
return { total, executed };
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function computeOverviewData(payload) {
|
|
659
|
+
const files = payload.files || [];
|
|
660
|
+
const meta = payload.meta || {};
|
|
661
|
+
|
|
662
|
+
let totalExprs = 0, executedExprs = 0;
|
|
663
|
+
let totalLines = 0, executedLines = 0;
|
|
664
|
+
const multiTypeExprs = [];
|
|
665
|
+
const perFile = [];
|
|
666
|
+
|
|
667
|
+
for (let i = 0; i < files.length; i++) {
|
|
668
|
+
const file = files[i];
|
|
669
|
+
const trace = Array.isArray(file.trace) ? file.trace : [];
|
|
670
|
+
const display = fileDisplayPath(file, i);
|
|
671
|
+
|
|
672
|
+
const exprCov = computeExpressionCoverage(trace);
|
|
673
|
+
const lineCov = expressionLineCoverageForTrace(trace);
|
|
674
|
+
|
|
675
|
+
totalExprs += exprCov.total;
|
|
676
|
+
executedExprs += exprCov.executed;
|
|
677
|
+
totalLines += lineCov.expected;
|
|
678
|
+
executedLines += lineCov.executed;
|
|
679
|
+
|
|
680
|
+
const seen = new Set();
|
|
681
|
+
for (const event of trace) {
|
|
682
|
+
if (!event) continue;
|
|
683
|
+
const loc = event.location;
|
|
684
|
+
if (!Array.isArray(loc) || loc.length < 4) continue;
|
|
685
|
+
const key = `${loc[SL]}:${loc[SC]}:${loc[EL]}:${loc[EC]}`;
|
|
686
|
+
if (seen.has(key)) continue;
|
|
687
|
+
seen.add(key);
|
|
688
|
+
const types = normalizeTypeCounts(event.types);
|
|
689
|
+
const typeKeys = Object.keys(types);
|
|
690
|
+
if (typeKeys.length > 1) {
|
|
691
|
+
multiTypeExprs.push({
|
|
692
|
+
file: display,
|
|
693
|
+
fileKey: fileUrlKey(file, i),
|
|
694
|
+
line: Number(loc[SL]),
|
|
695
|
+
types: types
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
perFile.push({
|
|
701
|
+
display,
|
|
702
|
+
key: fileUrlKey(file, i),
|
|
703
|
+
exprTotal: exprCov.total,
|
|
704
|
+
exprExecuted: exprCov.executed,
|
|
705
|
+
lineTotal: lineCov.expected,
|
|
706
|
+
lineExecuted: lineCov.executed
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return {
|
|
711
|
+
command: meta.command || null,
|
|
712
|
+
mode: meta.mode_text || null,
|
|
713
|
+
rubyVersion: meta.ruby_version || null,
|
|
714
|
+
lumitraceVersion: meta.lumitrace_version || null,
|
|
715
|
+
totalExprs, executedExprs,
|
|
716
|
+
totalLines, executedLines,
|
|
717
|
+
perFile,
|
|
718
|
+
multiTypeExprs
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function pct(n, d) {
|
|
723
|
+
if (d === 0) return "—";
|
|
724
|
+
return (n / d * 100).toFixed(1) + "%";
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function renderOverviewSection(data, onSelectFile) {
|
|
728
|
+
const section = document.createElement("section");
|
|
729
|
+
section.className = "overview-section";
|
|
730
|
+
|
|
731
|
+
// Context
|
|
732
|
+
const ctxH = document.createElement("h2");
|
|
733
|
+
ctxH.className = "overview-heading";
|
|
734
|
+
ctxH.textContent = "Execution Context";
|
|
735
|
+
section.appendChild(ctxH);
|
|
736
|
+
|
|
737
|
+
const ctxDl = document.createElement("dl");
|
|
738
|
+
ctxDl.className = "overview-dl";
|
|
739
|
+
const ctxItems = [];
|
|
740
|
+
if (data.command) ctxItems.push(["Command", data.command]);
|
|
741
|
+
if (data.mode) ctxItems.push(["Collect Mode", data.mode]);
|
|
742
|
+
if (data.rubyVersion) ctxItems.push(["Ruby", data.rubyVersion]);
|
|
743
|
+
if (data.lumitraceVersion) ctxItems.push(["Lumitrace", data.lumitraceVersion]);
|
|
744
|
+
for (const [label, value] of ctxItems) {
|
|
745
|
+
const dt = document.createElement("dt");
|
|
746
|
+
dt.textContent = label;
|
|
747
|
+
ctxDl.appendChild(dt);
|
|
748
|
+
const dd = document.createElement("dd");
|
|
749
|
+
dd.textContent = value;
|
|
750
|
+
ctxDl.appendChild(dd);
|
|
751
|
+
}
|
|
752
|
+
section.appendChild(ctxDl);
|
|
753
|
+
|
|
754
|
+
// Coverage summary
|
|
755
|
+
const covH = document.createElement("h2");
|
|
756
|
+
covH.className = "overview-heading";
|
|
757
|
+
covH.textContent = "Coverage Summary";
|
|
758
|
+
section.appendChild(covH);
|
|
759
|
+
|
|
760
|
+
const covTable = document.createElement("table");
|
|
761
|
+
covTable.className = "overview-table overview-summary-table";
|
|
762
|
+
covTable.innerHTML = `<thead><tr><th></th><th>Executed</th><th>Total</th><th>Coverage</th></tr></thead>` +
|
|
763
|
+
`<tbody>` +
|
|
764
|
+
`<tr><td>Expressions</td><td>${data.executedExprs}</td><td>${data.totalExprs}</td><td>${pct(data.executedExprs, data.totalExprs)}</td></tr>` +
|
|
765
|
+
`<tr><td>Lines</td><td>${data.executedLines}</td><td>${data.totalLines}</td><td>${pct(data.executedLines, data.totalLines)}</td></tr>` +
|
|
766
|
+
`</tbody>`;
|
|
767
|
+
section.appendChild(covTable);
|
|
768
|
+
|
|
769
|
+
// Per-file coverage table
|
|
770
|
+
if (data.perFile.length > 0) {
|
|
771
|
+
const fileH = document.createElement("h2");
|
|
772
|
+
fileH.className = "overview-heading";
|
|
773
|
+
fileH.textContent = `File Coverage (${data.perFile.length} files)`;
|
|
774
|
+
section.appendChild(fileH);
|
|
775
|
+
|
|
776
|
+
const fileTable = document.createElement("table");
|
|
777
|
+
fileTable.className = "overview-table overview-file-table";
|
|
778
|
+
const thead = document.createElement("thead");
|
|
779
|
+
const headerRow = document.createElement("tr");
|
|
780
|
+
|
|
781
|
+
const columns = [
|
|
782
|
+
{ key: "display", label: "File", type: "string" },
|
|
783
|
+
{ key: "exprPct", label: "Expr %", type: "number" },
|
|
784
|
+
{ key: "exprExecuted", label: "Expr Exec", type: "number" },
|
|
785
|
+
{ key: "exprTotal", label: "Expr Total", type: "number" },
|
|
786
|
+
{ key: "linePct", label: "Line %", type: "number" },
|
|
787
|
+
{ key: "lineExecuted", label: "Line Exec", type: "number" },
|
|
788
|
+
{ key: "lineTotal", label: "Line Total", type: "number" }
|
|
789
|
+
];
|
|
790
|
+
|
|
791
|
+
let sortCol = "exprPct";
|
|
792
|
+
let sortAsc = true;
|
|
793
|
+
|
|
794
|
+
const rows = data.perFile.map((f) => ({
|
|
795
|
+
...f,
|
|
796
|
+
exprPct: f.exprTotal > 0 ? f.exprExecuted / f.exprTotal * 100 : -1,
|
|
797
|
+
linePct: f.lineTotal > 0 ? f.lineExecuted / f.lineTotal * 100 : -1
|
|
798
|
+
}));
|
|
799
|
+
|
|
800
|
+
function sortRows() {
|
|
801
|
+
const col = columns.find((c) => c.key === sortCol);
|
|
802
|
+
if (!col) return;
|
|
803
|
+
rows.sort((a, b) => {
|
|
804
|
+
let av = a[sortCol], bv = b[sortCol];
|
|
805
|
+
if (col.type === "string") {
|
|
806
|
+
av = String(av || "");
|
|
807
|
+
bv = String(bv || "");
|
|
808
|
+
return sortAsc ? av.localeCompare(bv) : bv.localeCompare(av);
|
|
809
|
+
}
|
|
810
|
+
return sortAsc ? av - bv : bv - av;
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function renderFileTableBody() {
|
|
815
|
+
sortRows();
|
|
816
|
+
const oldTbody = fileTable.querySelector("tbody");
|
|
817
|
+
if (oldTbody) oldTbody.remove();
|
|
818
|
+
const tbody = document.createElement("tbody");
|
|
819
|
+
for (const row of rows) {
|
|
820
|
+
const tr = document.createElement("tr");
|
|
821
|
+
const tdFile = document.createElement("td");
|
|
822
|
+
const link = document.createElement("a");
|
|
823
|
+
link.href = "#";
|
|
824
|
+
link.className = "overview-file-link";
|
|
825
|
+
link.textContent = row.display;
|
|
826
|
+
link.addEventListener("click", (e) => { e.preventDefault(); onSelectFile(row.key); });
|
|
827
|
+
tdFile.appendChild(link);
|
|
828
|
+
tr.appendChild(tdFile);
|
|
829
|
+
tr.appendChild(cellTd(row.exprPct >= 0 ? row.exprPct.toFixed(1) + "%" : "—"));
|
|
830
|
+
tr.appendChild(cellTd(row.exprExecuted));
|
|
831
|
+
tr.appendChild(cellTd(row.exprTotal));
|
|
832
|
+
tr.appendChild(cellTd(row.linePct >= 0 ? row.linePct.toFixed(1) + "%" : "—"));
|
|
833
|
+
tr.appendChild(cellTd(row.lineExecuted));
|
|
834
|
+
tr.appendChild(cellTd(row.lineTotal));
|
|
835
|
+
tbody.appendChild(tr);
|
|
836
|
+
}
|
|
837
|
+
fileTable.appendChild(tbody);
|
|
838
|
+
|
|
839
|
+
// update header sort indicators
|
|
840
|
+
headerRow.querySelectorAll("th").forEach((th) => {
|
|
841
|
+
const col = th.dataset.col;
|
|
842
|
+
if (col === sortCol) {
|
|
843
|
+
th.dataset.sort = sortAsc ? "asc" : "desc";
|
|
844
|
+
} else {
|
|
845
|
+
delete th.dataset.sort;
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function cellTd(val) {
|
|
851
|
+
const td = document.createElement("td");
|
|
852
|
+
td.textContent = String(val);
|
|
853
|
+
return td;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
for (const col of columns) {
|
|
857
|
+
const th = document.createElement("th");
|
|
858
|
+
th.textContent = col.label;
|
|
859
|
+
th.dataset.col = col.key;
|
|
860
|
+
th.className = "sortable";
|
|
861
|
+
th.addEventListener("click", () => {
|
|
862
|
+
if (sortCol === col.key) {
|
|
863
|
+
sortAsc = !sortAsc;
|
|
864
|
+
} else {
|
|
865
|
+
sortCol = col.key;
|
|
866
|
+
sortAsc = col.type === "string";
|
|
867
|
+
}
|
|
868
|
+
renderFileTableBody();
|
|
869
|
+
});
|
|
870
|
+
headerRow.appendChild(th);
|
|
871
|
+
}
|
|
872
|
+
thead.appendChild(headerRow);
|
|
873
|
+
fileTable.appendChild(thead);
|
|
874
|
+
renderFileTableBody();
|
|
875
|
+
section.appendChild(fileTable);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Multi-type expressions
|
|
879
|
+
if (data.multiTypeExprs.length > 0) {
|
|
880
|
+
const mtH = document.createElement("h2");
|
|
881
|
+
mtH.className = "overview-heading";
|
|
882
|
+
mtH.textContent = `Multi-type Expressions (${data.multiTypeExprs.length})`;
|
|
883
|
+
section.appendChild(mtH);
|
|
884
|
+
|
|
885
|
+
const mtTable = document.createElement("table");
|
|
886
|
+
mtTable.className = "overview-table overview-multitype-table";
|
|
887
|
+
mtTable.innerHTML = `<thead><tr><th>File</th><th>Line</th><th>Types</th></tr></thead>`;
|
|
888
|
+
const tbody = document.createElement("tbody");
|
|
889
|
+
for (const entry of data.multiTypeExprs) {
|
|
890
|
+
const tr = document.createElement("tr");
|
|
891
|
+
const tdFile = document.createElement("td");
|
|
892
|
+
const link = document.createElement("a");
|
|
893
|
+
link.href = "#";
|
|
894
|
+
link.className = "overview-file-link";
|
|
895
|
+
link.textContent = entry.file;
|
|
896
|
+
link.addEventListener("click", (e) => { e.preventDefault(); onSelectFile(entry.fileKey, entry.line); });
|
|
897
|
+
tdFile.appendChild(link);
|
|
898
|
+
tr.appendChild(tdFile);
|
|
899
|
+
const tdLine = document.createElement("td");
|
|
900
|
+
tdLine.textContent = String(entry.line);
|
|
901
|
+
tr.appendChild(tdLine);
|
|
902
|
+
const tdTypes = document.createElement("td");
|
|
903
|
+
tdTypes.textContent = Object.entries(entry.types).sort(([, a], [, b]) => b - a).map(([t, c]) => `${t}(${c})`).join(", ");
|
|
904
|
+
tr.appendChild(tdTypes);
|
|
905
|
+
tbody.appendChild(tr);
|
|
906
|
+
}
|
|
907
|
+
mtTable.appendChild(tbody);
|
|
908
|
+
section.appendChild(mtTable);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
return section;
|
|
912
|
+
}
|
|
913
|
+
|
|
641
914
|
function render(payload) {
|
|
642
915
|
app.textContent = "";
|
|
643
916
|
|
|
@@ -702,20 +975,45 @@
|
|
|
702
975
|
viewer.className = "report-viewer";
|
|
703
976
|
main.appendChild(viewer);
|
|
704
977
|
|
|
978
|
+
const OVERVIEW_KEY = "__overview__";
|
|
979
|
+
const overviewData = computeOverviewData(payload);
|
|
980
|
+
|
|
705
981
|
const fileKeyToEntry = new Map();
|
|
706
982
|
files.forEach((file, idx) => fileKeyToEntry.set(fileUrlKey(file, idx), { file, idx }));
|
|
707
983
|
|
|
708
|
-
let selectedKey = preferredInitialFileKey(files);
|
|
709
|
-
let selectedLine = preferredInitialLineNumber();
|
|
984
|
+
let selectedKey = files.length > 1 ? OVERVIEW_KEY : preferredInitialFileKey(files);
|
|
985
|
+
let selectedLine = files.length > 1 ? null : preferredInitialLineNumber();
|
|
710
986
|
|
|
711
987
|
function renderTree() {
|
|
712
988
|
treeMount.textContent = "";
|
|
989
|
+
|
|
990
|
+
// Overview button
|
|
991
|
+
if (files.length > 1) {
|
|
992
|
+
const overviewBtn = document.createElement("button");
|
|
993
|
+
overviewBtn.type = "button";
|
|
994
|
+
overviewBtn.className = "tree-file-btn tree-overview-btn";
|
|
995
|
+
if (selectedKey === OVERVIEW_KEY) {
|
|
996
|
+
overviewBtn.classList.add("active");
|
|
997
|
+
overviewBtn.setAttribute("aria-current", "page");
|
|
998
|
+
}
|
|
999
|
+
overviewBtn.textContent = "Overview";
|
|
1000
|
+
overviewBtn.addEventListener("click", () => selectOverview(true));
|
|
1001
|
+
treeMount.appendChild(overviewBtn);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
713
1004
|
const treeRoot = buildFileTree(files);
|
|
714
1005
|
treeMount.appendChild(renderFileTreeNode(treeRoot, selectedKey, (key) => selectFile(key, true, null), 0));
|
|
715
1006
|
}
|
|
716
1007
|
|
|
717
|
-
function
|
|
1008
|
+
function renderSelectedView() {
|
|
718
1009
|
viewer.textContent = "";
|
|
1010
|
+
|
|
1011
|
+
if (selectedKey === OVERVIEW_KEY) {
|
|
1012
|
+
currentPath.textContent = "Overview";
|
|
1013
|
+
viewer.appendChild(renderOverviewSection(overviewData, (key, line) => selectFile(key, true, line || null)));
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
719
1017
|
const entry = fileKeyToEntry.get(selectedKey);
|
|
720
1018
|
if (!entry) return;
|
|
721
1019
|
|
|
@@ -733,10 +1031,18 @@
|
|
|
733
1031
|
function selectLine(lineno, updateHash) {
|
|
734
1032
|
const n = Number(lineno);
|
|
735
1033
|
selectedLine = Number.isFinite(n) && n > 0 ? n : null;
|
|
736
|
-
|
|
1034
|
+
renderSelectedView();
|
|
737
1035
|
if (updateHash) setLocationHashSelection(selectedKey, selectedLine);
|
|
738
1036
|
}
|
|
739
1037
|
|
|
1038
|
+
function selectOverview(updateHash) {
|
|
1039
|
+
selectedKey = OVERVIEW_KEY;
|
|
1040
|
+
selectedLine = null;
|
|
1041
|
+
renderTree();
|
|
1042
|
+
renderSelectedView();
|
|
1043
|
+
if (updateHash) setLocationHashSelection("overview", null);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
740
1046
|
function selectFile(key, updateHash, nextLine) {
|
|
741
1047
|
if (!fileKeyToEntry.has(key)) return;
|
|
742
1048
|
if (arguments.length >= 3) {
|
|
@@ -746,23 +1052,31 @@
|
|
|
746
1052
|
}
|
|
747
1053
|
selectedKey = key;
|
|
748
1054
|
renderTree();
|
|
749
|
-
|
|
1055
|
+
renderSelectedView();
|
|
750
1056
|
if (updateHash) setLocationHashSelection(key, selectedLine);
|
|
751
1057
|
}
|
|
752
1058
|
|
|
753
1059
|
window.addEventListener("hashchange", () => {
|
|
754
1060
|
const state = parseHashStateFromLocation();
|
|
1061
|
+
if (state.fileKey === "overview") {
|
|
1062
|
+
if (selectedKey !== OVERVIEW_KEY) selectOverview(false);
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
755
1065
|
if (state.fileKey && state.fileKey !== selectedKey && fileKeyToEntry.has(state.fileKey)) {
|
|
756
1066
|
selectFile(state.fileKey, false, state.line);
|
|
757
1067
|
return;
|
|
758
1068
|
}
|
|
759
1069
|
if (state.fileKey === selectedKey || (!state.fileKey && selectedKey)) {
|
|
760
1070
|
selectedLine = state.line;
|
|
761
|
-
|
|
1071
|
+
renderSelectedView();
|
|
762
1072
|
}
|
|
763
1073
|
});
|
|
764
1074
|
|
|
765
|
-
|
|
1075
|
+
if (selectedKey === OVERVIEW_KEY) {
|
|
1076
|
+
selectOverview(true);
|
|
1077
|
+
} else {
|
|
1078
|
+
selectFile(selectedKey, true);
|
|
1079
|
+
}
|
|
766
1080
|
}
|
|
767
1081
|
|
|
768
1082
|
try {
|
|
@@ -23,7 +23,8 @@ module Lumitrace
|
|
|
23
23
|
"First run with `--collect-mode types` to get a compact shape of runtime behavior.",
|
|
24
24
|
"Then switch to `--collect-mode last` for final value inspection on suspicious lines.",
|
|
25
25
|
"Use `--collect-mode history --max-samples N` only when value transitions matter.",
|
|
26
|
-
"Combine `--range` or `--git-diff` to keep outputs small and focused."
|
|
26
|
+
"Combine `--range` or `--git-diff` to keep outputs small and focused.",
|
|
27
|
+
"-j, -h, -t are flags that enable output with default paths. Use --json PATH, --html PATH, --text PATH to specify output paths."
|
|
27
28
|
],
|
|
28
29
|
commands: [
|
|
29
30
|
{
|
|
@@ -56,9 +57,9 @@ module Lumitrace
|
|
|
56
57
|
key_options: [
|
|
57
58
|
{ name: "--collect-mode", values: COLLECT_MODES, default: "last" },
|
|
58
59
|
{ name: "--max-samples", type: "Integer", default: 3, note: "Used by history mode." },
|
|
59
|
-
{ name: "-j
|
|
60
|
-
{ name: "-h
|
|
61
|
-
{ name: "-t
|
|
60
|
+
{ name: "-j / --json PATH", type: "bool|string", note: "Emit JSON output. -j uses default path; --json PATH writes to PATH." },
|
|
61
|
+
{ name: "-h / --html PATH", type: "bool|string", note: "Emit HTML output. -h uses default path; --html PATH writes to PATH." },
|
|
62
|
+
{ name: "-t / --text PATH", type: "bool|string", note: "Emit text output. -t uses default path; --text PATH writes to PATH." },
|
|
62
63
|
{ name: "-g, --git-diff[=MODE]", type: "string", note: "Restrict instrumentation to diff hunks." },
|
|
63
64
|
{ name: "--range SPEC", type: "string", repeatable: true, note: "Restrict instrumentation to file ranges." },
|
|
64
65
|
{ name: "--git-diff-context N", type: "Integer", note: "Expand diff hunks by +/-N lines." },
|
|
@@ -76,6 +77,8 @@ module Lumitrace
|
|
|
76
77
|
"lumitrace --collect-mode history --max-samples 5 -j app.rb",
|
|
77
78
|
"lumitrace --collect-mode types -h -j app.rb",
|
|
78
79
|
"lumitrace --collect-mode last -j exec bin/rails test",
|
|
80
|
+
"lumitrace --json output.json exec bin/rails test",
|
|
81
|
+
"lumitrace -j --html report.html app.rb",
|
|
79
82
|
"lumitrace help --format json",
|
|
80
83
|
"lumitrace schema --format json"
|
|
81
84
|
]
|
|
@@ -94,7 +94,7 @@ if defined?(RubyVM::InstructionSequence)
|
|
|
94
94
|
return recordrequire_orig_translate(iseq) if respond_to?(:recordrequire_orig_translate) && !RecordRequire.enabled?
|
|
95
95
|
path = iseq.path
|
|
96
96
|
abs = File.expand_path(path)
|
|
97
|
-
if RecordRequire.in_root?(abs) && !RecordRequire.excluded?(abs) && !RecordRequire.already_processed?(abs) &&
|
|
97
|
+
if RecordRequire.in_root?(abs) && File.file?(abs) && !RecordRequire.excluded?(abs) && !RecordRequire.already_processed?(abs) &&
|
|
98
98
|
(iseq.label == "<main>" || iseq.label == "<top (required)>")
|
|
99
99
|
if RecordRequire.ranges_filtering? && !RecordRequire.listed_file?(abs)
|
|
100
100
|
return recordrequire_orig_translate(iseq) if respond_to?(:recordrequire_orig_translate)
|
data/lib/lumitrace/version.rb
CHANGED
data/lib/lumitrace.rb
CHANGED
|
@@ -290,9 +290,12 @@ module Lumitrace
|
|
|
290
290
|
o.banner = banner if banner
|
|
291
291
|
o.separator ""
|
|
292
292
|
o.separator "Options:"
|
|
293
|
-
o.on("-t", "
|
|
294
|
-
o.on("
|
|
295
|
-
o.on("-
|
|
293
|
+
o.on("-t", "Enable text output (stdout or default path)") { opts[:text] = true }
|
|
294
|
+
o.on("--text PATH", "Text output to PATH") { |v| opts[:text] = v }
|
|
295
|
+
o.on("-h", "Enable HTML output (default path)") { opts[:html] = true }
|
|
296
|
+
o.on("--html PATH", "HTML output to PATH") { |v| opts[:html] = v }
|
|
297
|
+
o.on("-j", "Enable JSON output (default path)") { opts[:json] = true }
|
|
298
|
+
o.on("--json PATH", "JSON output to PATH") { |v| opts[:json] = v }
|
|
296
299
|
o.on("-g", "--git-diff[=MODE]", "Diff ranges (working, staged, base:REV, range:SPEC)") { |v| opts[:git_diff_mode] = v.nil? || v.empty? ? "working" : v }
|
|
297
300
|
o.on("--max-samples N", Integer, "Max samples per expression") { |v| opts[:max_samples] = v }
|
|
298
301
|
o.on("--collect-mode MODE", "Collect mode: last, types, history") { |v| opts[:collect_mode] = v }
|
|
@@ -27,7 +27,7 @@ jobs:
|
|
|
27
27
|
- name: Run tests with Lumitrace
|
|
28
28
|
env:
|
|
29
29
|
LUMITRACE_GIT_DIFF: range:origin/${{ github.base_ref }}...HEAD
|
|
30
|
-
run: bundle exec lumitrace -v -t --html
|
|
30
|
+
run: bundle exec lumitrace -v -t --html lumitrace_recorded.html exec rake test
|
|
31
31
|
|
|
32
32
|
- name: Prepare Pages content
|
|
33
33
|
run: |
|
|
@@ -42,7 +42,7 @@ steps:
|
|
|
42
42
|
- name: Run tests with Lumitrace
|
|
43
43
|
env:
|
|
44
44
|
LUMITRACE_GIT_DIFF: range:origin/${{ github.base_ref }}...HEAD
|
|
45
|
-
run: bundle exec lumitrace -v -t --html
|
|
45
|
+
run: bundle exec lumitrace -v -t --html lumitrace_recorded.html exec rake test
|
|
46
46
|
|
|
47
47
|
- name: Prepare Pages content
|
|
48
48
|
run: |
|
data/test/test_lumitrace.rb
CHANGED
|
@@ -128,7 +128,7 @@ class LumiTraceTest < Minitest::Test
|
|
|
128
128
|
end
|
|
129
129
|
|
|
130
130
|
def test_parse_cli_options_basic
|
|
131
|
-
argv = ["-t", "--html
|
|
131
|
+
argv = ["-t", "--html", "/tmp/out.html", "-j", "--max-samples", "7", "--collect-mode", "history", "--root", "/tmp/root",
|
|
132
132
|
"--range", "a.rb:1-3,5-6", "--verbose", "file.rb"]
|
|
133
133
|
opts, args, _parser = Lumitrace.parse_cli_options(argv, allow_help: true)
|
|
134
134
|
|
|
@@ -144,7 +144,7 @@ class LumiTraceTest < Minitest::Test
|
|
|
144
144
|
end
|
|
145
145
|
|
|
146
146
|
def test_parse_enable_args_cli_string
|
|
147
|
-
opts = Lumitrace.parse_enable_args("--text
|
|
147
|
+
opts = Lumitrace.parse_enable_args("--text /tmp/out.txt -h --json /tmp/out.json --max-samples 5 --collect-mode types --root /tmp/root")
|
|
148
148
|
|
|
149
149
|
assert_equal "/tmp/out.txt", opts[:text]
|
|
150
150
|
assert_equal true, opts[:html]
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lumitrace
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Koichi Sasada
|
|
@@ -23,6 +23,7 @@ files:
|
|
|
23
23
|
- bench/bench_sample.rb
|
|
24
24
|
- docs/ai-help.md
|
|
25
25
|
- docs/ai-schema.md
|
|
26
|
+
- docs/lumitrace_html_example.png
|
|
26
27
|
- docs/spec.md
|
|
27
28
|
- docs/supported_syntax.md
|
|
28
29
|
- docs/tutorial.ja.md
|