lumitrace 0.4.2 → 0.5.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/docs/spec.md +79 -1
- data/docs/tutorial.ja.md +4 -0
- data/docs/tutorial.md +4 -0
- data/lib/lumitrace/generate_resulted_html.rb +323 -391
- data/lib/lumitrace/generate_resulted_html_renderer.js +774 -0
- data/lib/lumitrace/record_instrument.rb +79 -22
- data/lib/lumitrace/version.rb +1 -1
- data/lib/lumitrace.rb +31 -4
- data/runv/index.html +1182 -420
- data/runv/sync_inline.rb +13 -1
- data/test/test_lumitrace.rb +137 -0
- metadata +2 -1
data/runv/index.html
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>LumiTrace Playground</title>
|
|
7
|
-
<meta name="description" content="Run Ruby in the browser and see traced values inline with lumitrace 0.
|
|
7
|
+
<meta name="description" content="Run Ruby in the browser and see traced values inline with lumitrace 0.5.0.">
|
|
8
8
|
<meta property="og:title" content="LumiTrace Playground">
|
|
9
|
-
<meta property="og:description" content="Run Ruby in the browser and see traced values inline with lumitrace 0.
|
|
9
|
+
<meta property="og:description" content="Run Ruby in the browser and see traced values inline with lumitrace 0.5.0.">
|
|
10
10
|
<meta property="og:type" content="website">
|
|
11
11
|
<meta property="og:url" content="https://github.com/ko1/lumitrace/tree/master/runv">
|
|
12
12
|
<meta property="og:see_also" content="https://github.com/ko1/lumitrace/tree/master/runv">
|
|
@@ -290,7 +290,7 @@
|
|
|
290
290
|
<header>
|
|
291
291
|
<div>
|
|
292
292
|
<h1>LumiTrace Playground</h1>
|
|
293
|
-
<p class="sub">Run Ruby in the browser and see traced values inline with lumitrace 0.
|
|
293
|
+
<p class="sub">Run Ruby in the browser and see traced values inline with lumitrace 0.5.0.</p>
|
|
294
294
|
</div>
|
|
295
295
|
</header>
|
|
296
296
|
|
|
@@ -448,6 +448,8 @@ module Lumitrace
|
|
|
448
448
|
end
|
|
449
449
|
|
|
450
450
|
module RecordInstrument
|
|
451
|
+
IDENTIFIER_METHOD_NAME_RE = /\A[a-z_]\w*[!?=]?\z/.freeze
|
|
452
|
+
|
|
451
453
|
SKIP_NODE_CLASSES = [
|
|
452
454
|
Prism::DefNode,
|
|
453
455
|
Prism::ClassNode,
|
|
@@ -488,7 +490,7 @@ module RecordInstrument
|
|
|
488
490
|
Prism::GlobalVariableReadNode
|
|
489
491
|
].freeze
|
|
490
492
|
|
|
491
|
-
def self.instrument_source(src, ranges, file_label: nil, record_method: "Lumitrace::R")
|
|
493
|
+
def self.instrument_source(src, ranges, file_label: nil, record_method: "::Lumitrace::R")
|
|
492
494
|
file_label ||= "(unknown)"
|
|
493
495
|
ranges = normalize_ranges(ranges)
|
|
494
496
|
|
|
@@ -540,7 +542,7 @@ module RecordInstrument
|
|
|
540
542
|
end
|
|
541
543
|
end
|
|
542
544
|
|
|
543
|
-
node.
|
|
545
|
+
instrumentable_child_nodes(node).each { |child| stack << [child, node] }
|
|
544
546
|
end
|
|
545
547
|
locs
|
|
546
548
|
end
|
|
@@ -577,7 +579,7 @@ module RecordInstrument
|
|
|
577
579
|
end
|
|
578
580
|
end
|
|
579
581
|
|
|
580
|
-
node.
|
|
582
|
+
instrumentable_child_nodes(node).each { |child| stack << [child, node] }
|
|
581
583
|
end
|
|
582
584
|
|
|
583
585
|
inserts
|
|
@@ -618,6 +620,8 @@ module RecordInstrument
|
|
|
618
620
|
def self.wrap_expr?(node, parent = nil)
|
|
619
621
|
return false unless node.respond_to?(:location)
|
|
620
622
|
return false if literal_value_node?(node)
|
|
623
|
+
return false if command_style_call_node?(node)
|
|
624
|
+
return false if parent.is_a?(Prism::DefinedNode)
|
|
621
625
|
if parent.is_a?(Prism::AliasGlobalVariableNode) || parent.is_a?(Prism::AliasMethodNode)
|
|
622
626
|
return false
|
|
623
627
|
end
|
|
@@ -637,6 +641,27 @@ module RecordInstrument
|
|
|
637
641
|
WRAP_NODE_CLASSES.include?(node.class)
|
|
638
642
|
end
|
|
639
643
|
|
|
644
|
+
def self.command_style_call_node?(node)
|
|
645
|
+
return false unless node.is_a?(Prism::CallNode)
|
|
646
|
+
return false unless node.respond_to?(:arguments) && node.arguments
|
|
647
|
+
return false if node.respond_to?(:opening_loc) && node.opening_loc
|
|
648
|
+
|
|
649
|
+
name = node.respond_to?(:name) ? node.name : nil
|
|
650
|
+
return false unless name
|
|
651
|
+
|
|
652
|
+
IDENTIFIER_METHOD_NAME_RE.match?(name.to_s)
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
def self.instrumentable_child_nodes(node)
|
|
656
|
+
return [] unless node
|
|
657
|
+
return [] if node.is_a?(Prism::DefinedNode)
|
|
658
|
+
if command_style_call_node?(node) && node.respond_to?(:block) && node.block
|
|
659
|
+
[node.block]
|
|
660
|
+
else
|
|
661
|
+
node.child_nodes
|
|
662
|
+
end
|
|
663
|
+
end
|
|
664
|
+
|
|
640
665
|
def self.expr_location(node)
|
|
641
666
|
loc = node.location
|
|
642
667
|
return {
|
|
@@ -782,22 +807,31 @@ module RecordInstrument
|
|
|
782
807
|
|
|
783
808
|
def self.events_from_ids
|
|
784
809
|
out = []
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
loc = @loc_by_id[id]
|
|
810
|
+
summary_cache = {}
|
|
811
|
+
@loc_by_id.each_with_index do |loc, id|
|
|
788
812
|
next unless loc
|
|
813
|
+
|
|
814
|
+
e = @events_by_id[id]
|
|
789
815
|
case collect_mode
|
|
790
816
|
when :history
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
all_types
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
817
|
+
if e
|
|
818
|
+
raw_values = values_from_ring(e)
|
|
819
|
+
all_types = history_type_set(e)
|
|
820
|
+
if all_types.nil? || all_types.empty?
|
|
821
|
+
all_types = {}
|
|
822
|
+
raw_values.each do |v|
|
|
823
|
+
t = value_type_name(v)
|
|
824
|
+
all_types[t] = (all_types[t] || 0) + 1
|
|
825
|
+
end
|
|
798
826
|
end
|
|
827
|
+
max = history_ring_size(e)
|
|
828
|
+
total = e[max + 1]
|
|
829
|
+
else
|
|
830
|
+
raw_values = []
|
|
831
|
+
all_types = {}
|
|
832
|
+
total = 0
|
|
799
833
|
end
|
|
800
|
-
|
|
834
|
+
|
|
801
835
|
out << {
|
|
802
836
|
file: loc[:file],
|
|
803
837
|
start_line: loc[:start_line],
|
|
@@ -806,9 +840,20 @@ module RecordInstrument
|
|
|
806
840
|
end_col: loc[:end_col],
|
|
807
841
|
kind: loc[:kind].to_s,
|
|
808
842
|
name: loc[:name],
|
|
809
|
-
sampled_values: raw_values.map
|
|
843
|
+
sampled_values: raw_values.map do |v|
|
|
844
|
+
key = v.__id__
|
|
845
|
+
cached = summary_cache[key]
|
|
846
|
+
if cached
|
|
847
|
+
cached.dup
|
|
848
|
+
else
|
|
849
|
+
type = value_type_name(v)
|
|
850
|
+
summary = summarize_value(v, type: type)
|
|
851
|
+
summary_cache[key] = summary
|
|
852
|
+
summary.dup
|
|
853
|
+
end
|
|
854
|
+
end,
|
|
810
855
|
types: sorted_type_counts(all_types),
|
|
811
|
-
total:
|
|
856
|
+
total: total
|
|
812
857
|
}
|
|
813
858
|
when :types
|
|
814
859
|
out << {
|
|
@@ -819,12 +864,12 @@ module RecordInstrument
|
|
|
819
864
|
end_col: loc[:end_col],
|
|
820
865
|
kind: loc[:kind].to_s,
|
|
821
866
|
name: loc[:name],
|
|
822
|
-
types: sorted_type_counts(e[:types]),
|
|
823
|
-
total: e[:total]
|
|
867
|
+
types: sorted_type_counts(e ? e[:types] : nil),
|
|
868
|
+
total: e ? e[:total] : 0
|
|
824
869
|
}
|
|
825
870
|
else # :last
|
|
826
|
-
last_raw = e[:last_value]
|
|
827
|
-
last_type = value_type_name(last_raw)
|
|
871
|
+
last_raw = e && e[:last_value]
|
|
872
|
+
last_type = value_type_name(last_raw) if e
|
|
828
873
|
out << {
|
|
829
874
|
file: loc[:file],
|
|
830
875
|
start_line: loc[:start_line],
|
|
@@ -833,9 +878,21 @@ module RecordInstrument
|
|
|
833
878
|
end_col: loc[:end_col],
|
|
834
879
|
kind: loc[:kind].to_s,
|
|
835
880
|
name: loc[:name],
|
|
836
|
-
last_value:
|
|
837
|
-
|
|
838
|
-
|
|
881
|
+
last_value: if e
|
|
882
|
+
key = last_raw.__id__
|
|
883
|
+
cached = summary_cache[key]
|
|
884
|
+
if cached
|
|
885
|
+
cached.dup
|
|
886
|
+
else
|
|
887
|
+
summary = summarize_value(last_raw, type: last_type)
|
|
888
|
+
summary_cache[key] = summary
|
|
889
|
+
summary.dup
|
|
890
|
+
end
|
|
891
|
+
else
|
|
892
|
+
nil
|
|
893
|
+
end,
|
|
894
|
+
types: sorted_type_counts(e ? e[:types] : nil),
|
|
895
|
+
total: e ? e[:total] : 0
|
|
839
896
|
}
|
|
840
897
|
end
|
|
841
898
|
end
|
|
@@ -1292,6 +1349,20 @@ require "json"
|
|
|
1292
1349
|
|
|
1293
1350
|
module Lumitrace
|
|
1294
1351
|
module GenerateResultedHtml
|
|
1352
|
+
RENDERER_JS_PATH = File.expand_path("generate_resulted_html_renderer.js", __dir__)
|
|
1353
|
+
|
|
1354
|
+
def self.monotonic_now
|
|
1355
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
1356
|
+
end
|
|
1357
|
+
|
|
1358
|
+
def self.time_step(label, logger = nil)
|
|
1359
|
+
start = monotonic_now
|
|
1360
|
+
result = yield
|
|
1361
|
+
elapsed_ms = (monotonic_now - start) * 1000.0
|
|
1362
|
+
logger&.call(format("html render: %s %.1fms", label, elapsed_ms))
|
|
1363
|
+
result
|
|
1364
|
+
end
|
|
1365
|
+
|
|
1295
1366
|
def self.render(source_path, events_path, ranges: nil, collect_mode: nil, max_samples: nil)
|
|
1296
1367
|
unless File.exist?(events_path)
|
|
1297
1368
|
abort "missing #{events_path}"
|
|
@@ -1301,50 +1372,970 @@ module GenerateResultedHtml
|
|
|
1301
1372
|
end
|
|
1302
1373
|
|
|
1303
1374
|
raw_events = JSON.parse(File.read(events_path))
|
|
1375
|
+
src = File.read(source_path)
|
|
1304
1376
|
mode_info = resolve_mode_info(raw_events, collect_mode: collect_mode, max_samples: max_samples)
|
|
1305
|
-
|
|
1306
|
-
events =
|
|
1377
|
+
normalized_ranges = normalize_ranges(ranges)
|
|
1378
|
+
events = normalize_events(raw_events).select { |e| e[:file] == source_path }
|
|
1379
|
+
|
|
1380
|
+
payload = build_html_payload(
|
|
1381
|
+
mode_info: mode_info,
|
|
1382
|
+
files: [
|
|
1383
|
+
build_html_payload_file(
|
|
1384
|
+
path: source_path,
|
|
1385
|
+
display_path: File.basename(source_path),
|
|
1386
|
+
source: src,
|
|
1387
|
+
ranges: normalized_ranges,
|
|
1388
|
+
trace_events: events
|
|
1389
|
+
)
|
|
1390
|
+
]
|
|
1391
|
+
)
|
|
1307
1392
|
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
ranges = normalize_ranges(ranges)
|
|
1311
|
-
expected_by_line, executed_by_line = line_stats(src, ranges, events, source_path)
|
|
1393
|
+
render_payload_html(payload)
|
|
1394
|
+
end
|
|
1312
1395
|
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
last_lineno = lineno
|
|
1396
|
+
def self.esc(s)
|
|
1397
|
+
s.to_s
|
|
1398
|
+
.gsub("&", "&")
|
|
1399
|
+
.gsub("<", "<")
|
|
1400
|
+
.gsub(">", ">")
|
|
1401
|
+
.gsub('"', """)
|
|
1402
|
+
end
|
|
1403
|
+
|
|
1404
|
+
def self.build_html_payload(mode_info:, files:, command_text: nil)
|
|
1405
|
+
meta = {
|
|
1406
|
+
mode: mode_info[:mode],
|
|
1407
|
+
mode_text: mode_info[:text],
|
|
1408
|
+
max_samples: mode_info[:max_samples]
|
|
1409
|
+
}
|
|
1410
|
+
meta[:command] = command_text if command_text && !command_text.to_s.empty?
|
|
1411
|
+
{
|
|
1412
|
+
version: 1,
|
|
1413
|
+
meta: meta,
|
|
1414
|
+
files: files
|
|
1415
|
+
}
|
|
1416
|
+
end
|
|
1417
|
+
|
|
1418
|
+
def self.build_html_payload_file(path:, display_path:, source:, ranges:, trace_events:, logger: nil)
|
|
1419
|
+
sort_start = logger ? monotonic_now : nil
|
|
1420
|
+
sorted_events = Array(trace_events).sort_by do |e|
|
|
1421
|
+
[e[:start_line].to_i, e[:start_col].to_i, e[:end_line].to_i, e[:end_col].to_i]
|
|
1340
1422
|
end
|
|
1341
|
-
|
|
1342
|
-
|
|
1423
|
+
sort_ms = sort_start ? (monotonic_now - sort_start) * 1000.0 : nil
|
|
1424
|
+
|
|
1425
|
+
map_start = logger ? monotonic_now : nil
|
|
1426
|
+
trace_payload = sorted_events.map { |e| event_to_html_trace_payload(e) }
|
|
1427
|
+
map_ms = map_start ? (monotonic_now - map_start) * 1000.0 : nil
|
|
1428
|
+
if logger
|
|
1429
|
+
logger.call(format("html render: payload_file %s sort=%.1fms map=%.1fms events=%d", display_path, sort_ms, map_ms, sorted_events.length))
|
|
1343
1430
|
end
|
|
1344
|
-
|
|
1345
|
-
|
|
1431
|
+
|
|
1432
|
+
{
|
|
1433
|
+
path: path,
|
|
1434
|
+
display_path: display_path,
|
|
1435
|
+
source: source,
|
|
1436
|
+
ranges: ranges,
|
|
1437
|
+
trace: trace_payload
|
|
1438
|
+
}
|
|
1439
|
+
end
|
|
1440
|
+
|
|
1441
|
+
def self.event_to_html_trace_payload(e)
|
|
1442
|
+
sampled_values = e[:sampled_values]
|
|
1443
|
+
if (sampled_values.nil? || sampled_values.empty?) && e[:last_value]
|
|
1444
|
+
sampled_values = [e[:last_value]]
|
|
1346
1445
|
end
|
|
1446
|
+
{
|
|
1447
|
+
location: [
|
|
1448
|
+
e[:start_line].to_i,
|
|
1449
|
+
e[:start_col].to_i,
|
|
1450
|
+
e[:end_line].to_i,
|
|
1451
|
+
e[:end_col].to_i
|
|
1452
|
+
],
|
|
1453
|
+
kind: (e[:kind] || "expr").to_s,
|
|
1454
|
+
name: e[:name],
|
|
1455
|
+
sampled_values: sampled_values || [],
|
|
1456
|
+
types: sorted_type_counts(e[:types]),
|
|
1457
|
+
total: e[:total].to_i
|
|
1458
|
+
}
|
|
1459
|
+
end
|
|
1460
|
+
|
|
1461
|
+
def self.payload_json_for_script(payload)
|
|
1462
|
+
JSON.generate(payload)
|
|
1463
|
+
.gsub("</", "<\\/")
|
|
1464
|
+
.gsub("\u2028", "\\u2028")
|
|
1465
|
+
.gsub("\u2029", "\\u2029")
|
|
1466
|
+
end
|
|
1467
|
+
|
|
1468
|
+
def self.html_renderer_js
|
|
1469
|
+
@html_renderer_js ||= '(function() {
|
|
1470
|
+
const payloadEl = document.getElementById("lumitrace-payload");
|
|
1471
|
+
const app = document.getElementById("lumitrace-app");
|
|
1472
|
+
if (!payloadEl || !app) return;
|
|
1473
|
+
|
|
1474
|
+
const SL = 0;
|
|
1475
|
+
const SC = 1;
|
|
1476
|
+
const EL = 2;
|
|
1477
|
+
const EC = 3;
|
|
1478
|
+
|
|
1479
|
+
function escHtml(s) {
|
|
1480
|
+
return String(s)
|
|
1481
|
+
.replace(/&/g, "&")
|
|
1482
|
+
.replace(/</g, "<")
|
|
1483
|
+
.replace(/>/g, ">")
|
|
1484
|
+
.replace(/\\"/g, """);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
function normalizeTypeCounts(types) {
|
|
1488
|
+
if (!types) return {};
|
|
1489
|
+
if (Array.isArray(types)) {
|
|
1490
|
+
const out = {};
|
|
1491
|
+
for (const t of types) {
|
|
1492
|
+
const key = String(t || "");
|
|
1493
|
+
if (!key) continue;
|
|
1494
|
+
out[key] = (out[key] || 0) + 1;
|
|
1495
|
+
}
|
|
1496
|
+
return out;
|
|
1497
|
+
}
|
|
1498
|
+
if (typeof types === "object") {
|
|
1499
|
+
const out = {};
|
|
1500
|
+
for (const [k, v] of Object.entries(types)) {
|
|
1501
|
+
const key = String(k || "");
|
|
1502
|
+
if (!key) continue;
|
|
1503
|
+
let count = Number(v) || 0;
|
|
1504
|
+
if (count <= 0) count = 1;
|
|
1505
|
+
out[key] = (out[key] || 0) + count;
|
|
1506
|
+
}
|
|
1507
|
+
return out;
|
|
1508
|
+
}
|
|
1509
|
+
const key = String(types || "");
|
|
1510
|
+
return key ? { [key]: 1 } : {};
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
function typeListText(types, onlyIfMultiple) {
|
|
1514
|
+
const counts = normalizeTypeCounts(types);
|
|
1515
|
+
const entries = Object.entries(counts).sort(([a], [b]) => a.localeCompare(b));
|
|
1516
|
+
if (onlyIfMultiple && entries.length <= 1) return null;
|
|
1517
|
+
if (entries.length === 0) return "(no types)";
|
|
1518
|
+
return "types: " + entries.map(([k, v]) => `${k}(${v})`).join(", ");
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
function valueTypeName(v) {
|
|
1522
|
+
if (v === null) return "NilClass";
|
|
1523
|
+
if (Array.isArray(v)) return "Array";
|
|
1524
|
+
switch (typeof v) {
|
|
1525
|
+
case "number": return Number.isInteger(v) ? "Integer" : "Float";
|
|
1526
|
+
case "string": return "String";
|
|
1527
|
+
case "boolean": return "Boolean";
|
|
1528
|
+
case "undefined": return "NilClass";
|
|
1529
|
+
case "object": return "Object";
|
|
1530
|
+
default: return typeof v;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
function formatValue(v, type) {
|
|
1535
|
+
const value = v == null ? "nil" : String(v);
|
|
1536
|
+
return `${value} (${type || valueTypeName(v)})`;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
function lastValueToPair(lastValue) {
|
|
1540
|
+
if (lastValue == null) return [null, null];
|
|
1541
|
+
if (typeof lastValue !== "object" || Array.isArray(lastValue)) return [lastValue, null];
|
|
1542
|
+
const type = lastValue.type || null;
|
|
1543
|
+
if (Object.prototype.hasOwnProperty.call(lastValue, "value")) return [lastValue.value, type];
|
|
1544
|
+
if (Object.prototype.hasOwnProperty.call(lastValue, "preview")) return [lastValue.preview, type];
|
|
1545
|
+
if (Object.prototype.hasOwnProperty.call(lastValue, "inspect")) return [lastValue.inspect, type];
|
|
1546
|
+
return [JSON.stringify(lastValue), type];
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
function summarizeValues(values, total, allTypes) {
|
|
1550
|
+
if (!values || values.length === 0) {
|
|
1551
|
+
const multi = typeListText(allTypes, false);
|
|
1552
|
+
return multi || "";
|
|
1553
|
+
}
|
|
1347
1554
|
|
|
1555
|
+
const n = total == null ? values.length : Number(total);
|
|
1556
|
+
const lastVals = values.slice(-3);
|
|
1557
|
+
const firstIndex = n - lastVals.length + 1;
|
|
1558
|
+
const lines = [];
|
|
1559
|
+
const extra = n - lastVals.length;
|
|
1560
|
+
if (extra > 0) lines.push(`... (+${extra} more)`);
|
|
1561
|
+
|
|
1562
|
+
lastVals.forEach((v, i) => {
|
|
1563
|
+
const idx = firstIndex + i;
|
|
1564
|
+
const [valueText, typeText] = lastValueToPair(v);
|
|
1565
|
+
lines.push(`#${idx}: ${formatValue(valueText, typeText)}`);
|
|
1566
|
+
});
|
|
1567
|
+
|
|
1568
|
+
const multi = typeListText(allTypes, true);
|
|
1569
|
+
if (multi) lines.push(multi);
|
|
1570
|
+
return lines.join("\\n");
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
function lineClassFor(expected, executed) {
|
|
1574
|
+
if (executed > 0) return " hit";
|
|
1575
|
+
if (expected > 0) return " miss";
|
|
1576
|
+
return "";
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
function lineInRanges(line, ranges) {
|
|
1580
|
+
if (!ranges || ranges.length === 0) return true;
|
|
1581
|
+
return ranges.some((range) => {
|
|
1582
|
+
if (!Array.isArray(range) || range.length < 2) return false;
|
|
1583
|
+
const start = Number(range[0]);
|
|
1584
|
+
const end = Number(range[1]);
|
|
1585
|
+
return line >= start && line <= end;
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
function sourceLines(source) {
|
|
1590
|
+
const matches = String(source || "").match(/[^\\n]*\\n|[^\\n]+/g);
|
|
1591
|
+
if (!matches) return [];
|
|
1592
|
+
return matches.map((line) => line.endsWith("\\n") ? line.slice(0, -1) : line);
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
function lineStatsForFile(trace) {
|
|
1596
|
+
const expectedByLine = Object.create(null);
|
|
1597
|
+
const executedByLine = Object.create(null);
|
|
1598
|
+
const seen = Object.create(null);
|
|
1599
|
+
|
|
1600
|
+
for (const event of trace || []) {
|
|
1601
|
+
const loc = event && event.location;
|
|
1602
|
+
if (!Array.isArray(loc) || loc.length < 4) continue;
|
|
1603
|
+
const sl = Number(loc[SL]);
|
|
1604
|
+
const el = Number(loc[EL]);
|
|
1605
|
+
if (!(sl > 0) || !(el > 0)) continue;
|
|
1606
|
+
const key = `${sl}:${loc[SC]}:${el}:${loc[EC]}`;
|
|
1607
|
+
if (seen[key]) continue;
|
|
1608
|
+
seen[key] = true;
|
|
1609
|
+
for (let line = sl; line <= el; line += 1) {
|
|
1610
|
+
expectedByLine[line] = (expectedByLine[line] || 0) + 1;
|
|
1611
|
+
if (Number(event.total) > 0) {
|
|
1612
|
+
executedByLine[line] = (executedByLine[line] || 0) + 1;
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
return { expectedByLine, executedByLine };
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
function expressionLineCoverageForTrace(trace) {
|
|
1621
|
+
const expectedLines = new Set();
|
|
1622
|
+
const executedLines = new Set();
|
|
1623
|
+
const seen = new Set();
|
|
1624
|
+
|
|
1625
|
+
for (const event of trace || []) {
|
|
1626
|
+
if (!event || event.kind === "arg") continue;
|
|
1627
|
+
const loc = event.location;
|
|
1628
|
+
if (!Array.isArray(loc) || loc.length < 4) continue;
|
|
1629
|
+
|
|
1630
|
+
const sl = Number(loc[SL]);
|
|
1631
|
+
const sc = Number(loc[SC]);
|
|
1632
|
+
const el = Number(loc[EL]);
|
|
1633
|
+
const ec = Number(loc[EC]);
|
|
1634
|
+
if (!(sl > 0) || !(el > 0)) continue;
|
|
1635
|
+
|
|
1636
|
+
const key = `${sl}:${sc}:${el}:${ec}`;
|
|
1637
|
+
if (seen.has(key)) continue;
|
|
1638
|
+
seen.add(key);
|
|
1639
|
+
|
|
1640
|
+
for (let line = sl; line <= el; line += 1) {
|
|
1641
|
+
expectedLines.add(line);
|
|
1642
|
+
if (Number(event.total) > 0) executedLines.add(line);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
return {
|
|
1647
|
+
executed: executedLines.size,
|
|
1648
|
+
expected: expectedLines.size
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
function aggregateEventsForLine(trace, lineno, lineLen, fileIndex) {
|
|
1653
|
+
const buckets = new Map();
|
|
1654
|
+
const spans = [];
|
|
1655
|
+
|
|
1656
|
+
for (const event of trace || []) {
|
|
1657
|
+
const loc = event && event.location;
|
|
1658
|
+
if (!Array.isArray(loc) || loc.length < 4) continue;
|
|
1659
|
+
const sline = Number(loc[SL]);
|
|
1660
|
+
const scol = Number(loc[SC]);
|
|
1661
|
+
const eline = Number(loc[EL]);
|
|
1662
|
+
const ecol = Number(loc[EC]);
|
|
1663
|
+
if (lineno < sline || lineno > eline) continue;
|
|
1664
|
+
|
|
1665
|
+
let s;
|
|
1666
|
+
let t;
|
|
1667
|
+
let marker;
|
|
1668
|
+
if (sline === eline) {
|
|
1669
|
+
s = scol;
|
|
1670
|
+
t = ecol;
|
|
1671
|
+
marker = true;
|
|
1672
|
+
} else if (lineno === sline) {
|
|
1673
|
+
s = scol;
|
|
1674
|
+
t = lineLen;
|
|
1675
|
+
marker = false;
|
|
1676
|
+
} else if (lineno === eline) {
|
|
1677
|
+
s = 0;
|
|
1678
|
+
t = ecol;
|
|
1679
|
+
marker = true;
|
|
1680
|
+
} else {
|
|
1681
|
+
s = 0;
|
|
1682
|
+
t = lineLen;
|
|
1683
|
+
marker = false;
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
if (!(t > s)) continue;
|
|
1687
|
+
spans.push({ start_col: s, end_col: t });
|
|
1688
|
+
|
|
1689
|
+
const keyId = `${fileIndex}:${sline}:${scol}:${eline}:${ecol}`;
|
|
1690
|
+
buckets.set(keyId, {
|
|
1691
|
+
key_id: keyId,
|
|
1692
|
+
start_col: s,
|
|
1693
|
+
end_col: t,
|
|
1694
|
+
marker,
|
|
1695
|
+
kind: event.kind || "expr",
|
|
1696
|
+
name: event.name || null,
|
|
1697
|
+
sampled_values: event.sampled_values || [],
|
|
1698
|
+
types: event.types || {},
|
|
1699
|
+
total: Number(event.total) || 0,
|
|
1700
|
+
suppress_miss: false
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
const out = Array.from(buckets.values());
|
|
1705
|
+
for (const b of out) {
|
|
1706
|
+
const depth = spans.filter((sp) => b.start_col >= sp.start_col && b.end_col <= sp.end_col).length;
|
|
1707
|
+
b.depth = Math.min(5, Math.max(1, depth));
|
|
1708
|
+
}
|
|
1709
|
+
out.sort((a, b) => a.start_col - b.start_col);
|
|
1710
|
+
return out;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
function renderLineHtml(lineText, events) {
|
|
1714
|
+
const opens = Object.create(null);
|
|
1715
|
+
const closes = Object.create(null);
|
|
1716
|
+
|
|
1717
|
+
for (const e of events) {
|
|
1718
|
+
const s = Number(e.start_col);
|
|
1719
|
+
const t = Number(e.end_col);
|
|
1720
|
+
if (!(t > s)) continue;
|
|
1721
|
+
|
|
1722
|
+
const values = e.sampled_values || [];
|
|
1723
|
+
const allTypes = e.types || {};
|
|
1724
|
+
const total = Number(e.total) || 0;
|
|
1725
|
+
const label = e.kind === "arg" && e.name ? `arg ${e.name}` : null;
|
|
1726
|
+
|
|
1727
|
+
let valueText;
|
|
1728
|
+
if (total === 0) {
|
|
1729
|
+
valueText = label ? `${label}: (not hit)` : "(not hit)";
|
|
1730
|
+
} else {
|
|
1731
|
+
const summary = summarizeValues(values, total, allTypes);
|
|
1732
|
+
if (label) {
|
|
1733
|
+
valueText = summary ? `${label}: ${summary}` : label;
|
|
1734
|
+
} else {
|
|
1735
|
+
valueText = summary;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
const tooltipHtml = escHtml(valueText);
|
|
1740
|
+
const depthClass = `depth-${e.depth || 1}`;
|
|
1741
|
+
const missClass = total === 0 && !e.suppress_miss ? " miss" : "";
|
|
1742
|
+
const keyAttr = escHtml(e.key_id || "");
|
|
1743
|
+
const openTag = `<span class=\\"expr hit ${depthClass}${missClass}\\" data-key=\\"${keyAttr}\\">`;
|
|
1744
|
+
|
|
1745
|
+
let closeTag = "</span>";
|
|
1746
|
+
if (e.marker !== false) {
|
|
1747
|
+
let marker = "🔎";
|
|
1748
|
+
if (total === 0) marker = "∅";
|
|
1749
|
+
else if (e.kind === "arg") marker = "🧷";
|
|
1750
|
+
|
|
1751
|
+
let markerClass = "marker";
|
|
1752
|
+
if (total === 0 && !e.suppress_miss) markerClass = "marker miss";
|
|
1753
|
+
if (e.kind === "arg") markerClass += " arg";
|
|
1754
|
+
closeTag = `<span class=\\"${markerClass}\\" data-key=\\"${keyAttr}\\" aria-hidden=\\"true\\">${marker}<span class=\\"tooltip\\">${tooltipHtml}</span></span></span>`;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
const len = t - s;
|
|
1758
|
+
(opens[s] ||= []).push({ len, start: s, end: t, tag: openTag });
|
|
1759
|
+
(closes[t] ||= []).push({ len, start: s, end: t, tag: closeTag });
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
const positions = Array.from(new Set([
|
|
1763
|
+
...Object.keys(opens).map(Number),
|
|
1764
|
+
...Object.keys(closes).map(Number)
|
|
1765
|
+
])).sort((a, b) => a - b);
|
|
1766
|
+
|
|
1767
|
+
let out = "";
|
|
1768
|
+
let cursor = 0;
|
|
1769
|
+
|
|
1770
|
+
for (const pos of positions) {
|
|
1771
|
+
if (pos > cursor) out += escHtml(lineText.slice(cursor, pos));
|
|
1772
|
+
if (closes[pos]) {
|
|
1773
|
+
closes[pos]
|
|
1774
|
+
.slice()
|
|
1775
|
+
.sort((a, b) => (b.start - a.start) || (a.len - b.len))
|
|
1776
|
+
.forEach((c) => { out += c.tag; });
|
|
1777
|
+
}
|
|
1778
|
+
if (opens[pos]) {
|
|
1779
|
+
opens[pos]
|
|
1780
|
+
.slice()
|
|
1781
|
+
.sort((a, b) => b.end - a.end)
|
|
1782
|
+
.forEach((o) => { out += o.tag; });
|
|
1783
|
+
}
|
|
1784
|
+
cursor = pos;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
if (cursor < lineText.length) out += escHtml(lineText.slice(cursor));
|
|
1788
|
+
return out;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
function buildLineNode(lineno, lineText, lineClass, innerHtml, options) {
|
|
1792
|
+
const opts = options || {};
|
|
1793
|
+
const line = document.createElement("span");
|
|
1794
|
+
line.className = `line${lineClass}`;
|
|
1795
|
+
line.dataset.line = String(lineno);
|
|
1796
|
+
line.id = `line-${lineno}`;
|
|
1797
|
+
|
|
1798
|
+
const ln = opts.lineHref ? document.createElement("a") : document.createElement("span");
|
|
1799
|
+
ln.className = `ln${opts.lineHref ? " ln-link" : ""}`;
|
|
1800
|
+
ln.textContent = String(lineno);
|
|
1801
|
+
if (opts.lineHref) {
|
|
1802
|
+
ln.href = opts.lineHref(lineno);
|
|
1803
|
+
ln.title = `Link to line ${lineno}`;
|
|
1804
|
+
ln.addEventListener("click", (event) => {
|
|
1805
|
+
if (!opts.onLineClick) return;
|
|
1806
|
+
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
1807
|
+
event.preventDefault();
|
|
1808
|
+
opts.onLineClick(lineno);
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
line.appendChild(ln);
|
|
1812
|
+
line.appendChild(document.createTextNode(" "));
|
|
1813
|
+
|
|
1814
|
+
if (innerHtml == null) {
|
|
1815
|
+
line.appendChild(document.createTextNode(lineText));
|
|
1816
|
+
} else {
|
|
1817
|
+
const wrapper = document.createElement("span");
|
|
1818
|
+
wrapper.innerHTML = innerHtml;
|
|
1819
|
+
while (wrapper.firstChild) line.appendChild(wrapper.firstChild);
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
return line;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
function buildEllipsisNode() {
|
|
1826
|
+
const line = document.createElement("span");
|
|
1827
|
+
line.className = "line ellipsis";
|
|
1828
|
+
line.dataset.line = "...";
|
|
1829
|
+
|
|
1830
|
+
const ln = document.createElement("span");
|
|
1831
|
+
ln.className = "ln";
|
|
1832
|
+
ln.textContent = "...";
|
|
1833
|
+
line.appendChild(ln);
|
|
1834
|
+
return line;
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
function renderFileSection(file, fileIndex, options) {
|
|
1838
|
+
const opts = options || {};
|
|
1839
|
+
const section = document.createElement("section");
|
|
1840
|
+
section.className = "file-section";
|
|
1841
|
+
|
|
1842
|
+
if (opts.includeTitle !== false) {
|
|
1843
|
+
const h2 = document.createElement("h2");
|
|
1844
|
+
h2.className = "file";
|
|
1845
|
+
h2.textContent = file.display_path || file.path || `file-${fileIndex + 1}`;
|
|
1846
|
+
section.appendChild(h2);
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
const pre = document.createElement("pre");
|
|
1850
|
+
pre.className = "code";
|
|
1851
|
+
const code = document.createElement("code");
|
|
1852
|
+
pre.appendChild(code);
|
|
1853
|
+
|
|
1854
|
+
const lines = sourceLines(file.source || "");
|
|
1855
|
+
const ranges = Array.isArray(file.ranges) ? file.ranges : null;
|
|
1856
|
+
const trace = Array.isArray(file.trace) ? file.trace : [];
|
|
1857
|
+
const { expectedByLine, executedByLine } = lineStatsForFile(trace);
|
|
1858
|
+
|
|
1859
|
+
let prevLineno = null;
|
|
1860
|
+
let firstLineno = null;
|
|
1861
|
+
let lastLineno = null;
|
|
1862
|
+
|
|
1863
|
+
lines.forEach((lineText, idx) => {
|
|
1864
|
+
const lineno = idx + 1;
|
|
1865
|
+
if (!lineInRanges(lineno, ranges)) return;
|
|
1866
|
+
if (firstLineno == null) firstLineno = lineno;
|
|
1867
|
+
if (prevLineno != null && lineno > prevLineno + 1) {
|
|
1868
|
+
code.appendChild(buildEllipsisNode());
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
const evs = aggregateEventsForLine(trace, lineno, lineText.length, fileIndex);
|
|
1872
|
+
const expected = expectedByLine[lineno] || 0;
|
|
1873
|
+
const executed = executedByLine[lineno] || 0;
|
|
1874
|
+
const lineClass = lineClassFor(expected, executed);
|
|
1875
|
+
if (expected > 0 && executed === 0) {
|
|
1876
|
+
evs.forEach((e) => { e.suppress_miss = true; });
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
if (evs.length === 0) {
|
|
1880
|
+
code.appendChild(buildLineNode(lineno, lineText, lineClass, null, opts));
|
|
1881
|
+
} else {
|
|
1882
|
+
code.appendChild(buildLineNode(lineno, lineText, lineClass, renderLineHtml(lineText, evs), opts));
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
prevLineno = lineno;
|
|
1886
|
+
lastLineno = lineno;
|
|
1887
|
+
});
|
|
1888
|
+
|
|
1889
|
+
if (firstLineno != null && firstLineno > 1) {
|
|
1890
|
+
code.insertBefore(buildEllipsisNode(), code.firstChild);
|
|
1891
|
+
}
|
|
1892
|
+
if (lastLineno != null && lastLineno < lines.length) {
|
|
1893
|
+
code.appendChild(buildEllipsisNode());
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
section.appendChild(pre);
|
|
1897
|
+
return section;
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
function bindMarkerHover(root) {
|
|
1901
|
+
root.querySelectorAll(".marker").forEach((marker) => {
|
|
1902
|
+
marker.addEventListener("mouseenter", () => {
|
|
1903
|
+
root.querySelectorAll(".expr.active").forEach((el) => el.classList.remove("active"));
|
|
1904
|
+
const key = marker.dataset.key;
|
|
1905
|
+
if (key) {
|
|
1906
|
+
root.querySelectorAll(`.expr[data-key=\\"${key}\\"]`).forEach((el) => el.classList.add("active"));
|
|
1907
|
+
} else {
|
|
1908
|
+
const expr = marker.closest(".expr");
|
|
1909
|
+
if (expr) expr.classList.add("active");
|
|
1910
|
+
}
|
|
1911
|
+
});
|
|
1912
|
+
marker.addEventListener("mouseleave", () => {
|
|
1913
|
+
const key = marker.dataset.key;
|
|
1914
|
+
if (key) {
|
|
1915
|
+
root.querySelectorAll(`.expr[data-key=\\"${key}\\"]`).forEach((el) => el.classList.remove("active"));
|
|
1916
|
+
} else {
|
|
1917
|
+
const expr = marker.closest(".expr");
|
|
1918
|
+
if (expr) expr.classList.remove("active");
|
|
1919
|
+
}
|
|
1920
|
+
});
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
function fileDisplayPath(file, idx) {
|
|
1925
|
+
return String((file && (file.display_path || file.path)) || `file-${idx + 1}`);
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
function fileUrlKey(file, idx) {
|
|
1929
|
+
return fileDisplayPath(file, idx).replace(/\\\\/g, "/");
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
function encodeHashFileKey(key) {
|
|
1933
|
+
return encodeURIComponent(String(key || "")).replace(/%2F/g, "/");
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
function decodeHashFileKey(value) {
|
|
1937
|
+
try {
|
|
1938
|
+
return decodeURIComponent(String(value || ""));
|
|
1939
|
+
} catch (_e) {
|
|
1940
|
+
return String(value || "");
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
function parseHashStateFromLocation() {
|
|
1945
|
+
const raw = String(window.location.hash || "").replace(/^#/, "");
|
|
1946
|
+
if (!raw) return { fileKey: null, line: null };
|
|
1947
|
+
|
|
1948
|
+
if (!raw.includes("=")) {
|
|
1949
|
+
return { fileKey: decodeHashFileKey(raw), line: null };
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
const params = new URLSearchParams(raw);
|
|
1953
|
+
const fileRaw = params.get("file");
|
|
1954
|
+
const lineRaw = params.get("line");
|
|
1955
|
+
const lineNum = lineRaw == null ? null : Number.parseInt(lineRaw, 10);
|
|
1956
|
+
|
|
1957
|
+
return {
|
|
1958
|
+
fileKey: fileRaw ? decodeHashFileKey(fileRaw) : null,
|
|
1959
|
+
line: Number.isFinite(lineNum) && lineNum > 0 ? lineNum : null
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
function buildHashForSelection(fileKey, line) {
|
|
1964
|
+
const parts = [];
|
|
1965
|
+
if (fileKey) parts.push(`file=${encodeHashFileKey(fileKey)}`);
|
|
1966
|
+
if (line && Number(line) > 0) parts.push(`line=${Number(line)}`);
|
|
1967
|
+
return `#${parts.join("&")}`;
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
function setLocationHashSelection(fileKey, line) {
|
|
1971
|
+
const hash = buildHashForSelection(fileKey, line);
|
|
1972
|
+
if (window.location.hash === hash) return;
|
|
1973
|
+
if (window.history && typeof window.history.replaceState === "function") {
|
|
1974
|
+
try {
|
|
1975
|
+
window.history.replaceState(null, "", hash);
|
|
1976
|
+
return;
|
|
1977
|
+
} catch (_e) {
|
|
1978
|
+
// `about:srcdoc` (runv preview) can reject replaceState with a file:// hash URL.
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
try {
|
|
1982
|
+
window.location.hash = hash;
|
|
1983
|
+
} catch (_e) {
|
|
1984
|
+
// Ignore hash update failures in restricted embedding contexts.
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
function buildFileTree(files) {
|
|
1989
|
+
const root = { dirs: new Map(), files: [] };
|
|
1990
|
+
|
|
1991
|
+
files.forEach((file, idx) => {
|
|
1992
|
+
const key = fileUrlKey(file, idx);
|
|
1993
|
+
const display = fileDisplayPath(file, idx).replace(/\\\\/g, "/");
|
|
1994
|
+
const parts = display.split("/").filter(Boolean);
|
|
1995
|
+
const filename = parts.pop() || display || `file-${idx + 1}`;
|
|
1996
|
+
|
|
1997
|
+
let node = root;
|
|
1998
|
+
let currentPath = "";
|
|
1999
|
+
for (const part of parts) {
|
|
2000
|
+
currentPath = currentPath ? `${currentPath}/${part}` : part;
|
|
2001
|
+
if (!node.dirs.has(part)) {
|
|
2002
|
+
node.dirs.set(part, { name: part, path: currentPath, dirs: new Map(), files: [] });
|
|
2003
|
+
}
|
|
2004
|
+
node = node.dirs.get(part);
|
|
2005
|
+
}
|
|
2006
|
+
const coverage = expressionLineCoverageForTrace(Array.isArray(file.trace) ? file.trace : []);
|
|
2007
|
+
const coverageText = coverage.expected > 0 ? ` (${coverage.executed}/${coverage.expected})` : "";
|
|
2008
|
+
node.files.push({ label: filename, key, file, index: idx, path: display, coverageText });
|
|
2009
|
+
});
|
|
2010
|
+
|
|
2011
|
+
return root;
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
function treeNodeContainsSelection(node, selectedKey) {
|
|
2015
|
+
if (!node) return false;
|
|
2016
|
+
if ((node.files || []).some((f) => f.key === selectedKey)) return true;
|
|
2017
|
+
for (const child of (node.dirs || new Map()).values()) {
|
|
2018
|
+
if (treeNodeContainsSelection(child, selectedKey)) return true;
|
|
2019
|
+
}
|
|
2020
|
+
return false;
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
function renderFileTreeNode(node, selectedKey, onSelect, level) {
|
|
2024
|
+
const ul = document.createElement("ul");
|
|
2025
|
+
ul.className = "tree-list";
|
|
2026
|
+
ul.dataset.level = String(level || 0);
|
|
2027
|
+
|
|
2028
|
+
const dirs = Array.from((node.dirs || new Map()).values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
2029
|
+
const files = Array.from(node.files || []).sort((a, b) => a.path.localeCompare(b.path));
|
|
2030
|
+
|
|
2031
|
+
dirs.forEach((dirNode) => {
|
|
2032
|
+
const li = document.createElement("li");
|
|
2033
|
+
li.className = "tree-dir";
|
|
2034
|
+
|
|
2035
|
+
const details = document.createElement("details");
|
|
2036
|
+
details.className = "tree-folder";
|
|
2037
|
+
details.open = true;
|
|
2038
|
+
|
|
2039
|
+
const summary = document.createElement("summary");
|
|
2040
|
+
summary.className = "tree-folder-label";
|
|
2041
|
+
summary.textContent = dirNode.name;
|
|
2042
|
+
details.appendChild(summary);
|
|
2043
|
+
|
|
2044
|
+
details.appendChild(renderFileTreeNode(dirNode, selectedKey, onSelect, (level || 0) + 1));
|
|
2045
|
+
li.appendChild(details);
|
|
2046
|
+
ul.appendChild(li);
|
|
2047
|
+
});
|
|
2048
|
+
|
|
2049
|
+
files.forEach((entry) => {
|
|
2050
|
+
const li = document.createElement("li");
|
|
2051
|
+
li.className = "tree-file";
|
|
2052
|
+
|
|
2053
|
+
const btn = document.createElement("button");
|
|
2054
|
+
btn.type = "button";
|
|
2055
|
+
btn.className = "tree-file-btn";
|
|
2056
|
+
const name = document.createElement("span");
|
|
2057
|
+
name.className = "tree-file-name";
|
|
2058
|
+
name.textContent = entry.label;
|
|
2059
|
+
btn.appendChild(name);
|
|
2060
|
+
|
|
2061
|
+
if (entry.coverageText) {
|
|
2062
|
+
const meta = document.createElement("span");
|
|
2063
|
+
meta.className = "tree-file-meta";
|
|
2064
|
+
meta.textContent = entry.coverageText;
|
|
2065
|
+
btn.appendChild(meta);
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
btn.dataset.fileKey = entry.key;
|
|
2069
|
+
if (entry.key === selectedKey) {
|
|
2070
|
+
btn.classList.add("active");
|
|
2071
|
+
btn.setAttribute("aria-current", "page");
|
|
2072
|
+
}
|
|
2073
|
+
btn.addEventListener("click", () => onSelect(entry.key));
|
|
2074
|
+
li.appendChild(btn);
|
|
2075
|
+
ul.appendChild(li);
|
|
2076
|
+
});
|
|
2077
|
+
|
|
2078
|
+
return ul;
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
function preferredInitialFileKey(files) {
|
|
2082
|
+
if (!files || files.length === 0) return null;
|
|
2083
|
+
const hashKey = parseHashStateFromLocation().fileKey;
|
|
2084
|
+
if (hashKey) {
|
|
2085
|
+
const byDisplay = files.find((file, idx) => fileUrlKey(file, idx) === hashKey);
|
|
2086
|
+
if (byDisplay) return fileUrlKey(byDisplay, files.indexOf(byDisplay));
|
|
2087
|
+
|
|
2088
|
+
const byPath = files.find((file) => String(file.path || "") === hashKey);
|
|
2089
|
+
if (byPath) return fileUrlKey(byPath, files.indexOf(byPath));
|
|
2090
|
+
}
|
|
2091
|
+
return fileUrlKey(files[0], 0);
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
function preferredInitialLineNumber() {
|
|
2095
|
+
return parseHashStateFromLocation().line;
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
function focusLineInViewer(viewer, lineno) {
|
|
2099
|
+
viewer.querySelectorAll(".line.line-target").forEach((el) => el.classList.remove("line-target"));
|
|
2100
|
+
if (!(lineno > 0)) return;
|
|
2101
|
+
const target = viewer.querySelector(`.line[data-line="${lineno}"]`);
|
|
2102
|
+
if (!target) return;
|
|
2103
|
+
target.classList.add("line-target");
|
|
2104
|
+
if (typeof target.scrollIntoView === "function") {
|
|
2105
|
+
target.scrollIntoView({ block: "center", inline: "nearest" });
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
function render(payload) {
|
|
2110
|
+
app.textContent = "";
|
|
2111
|
+
|
|
2112
|
+
const files = payload && Array.isArray(payload.files) ? payload.files : [];
|
|
2113
|
+
if (files.length === 0) {
|
|
2114
|
+
const empty = document.createElement("p");
|
|
2115
|
+
empty.className = "hint";
|
|
2116
|
+
empty.textContent = "No files to render.";
|
|
2117
|
+
app.appendChild(empty);
|
|
2118
|
+
return;
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
const hint = document.createElement("div");
|
|
2122
|
+
hint.className = "hint";
|
|
2123
|
+
hint.textContent = "Hover highlighted text to see recorded values.";
|
|
2124
|
+
app.appendChild(hint);
|
|
2125
|
+
|
|
2126
|
+
const mode = document.createElement("div");
|
|
2127
|
+
mode.className = "mode";
|
|
2128
|
+
mode.textContent = payload && payload.meta && payload.meta.mode_text ? payload.meta.mode_text : "";
|
|
2129
|
+
|
|
2130
|
+
const commandText = payload && payload.meta && payload.meta.command ? String(payload.meta.command) : "";
|
|
2131
|
+
if (commandText) {
|
|
2132
|
+
const command = document.createElement("div");
|
|
2133
|
+
command.className = "command";
|
|
2134
|
+
command.textContent = `Command: ${commandText}`;
|
|
2135
|
+
app.appendChild(command);
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
app.appendChild(mode);
|
|
2139
|
+
|
|
2140
|
+
const shell = document.createElement("div");
|
|
2141
|
+
shell.className = `report-layout${files.length <= 1 ? " single-file" : ""}`;
|
|
2142
|
+
app.appendChild(shell);
|
|
2143
|
+
|
|
2144
|
+
const sidebar = document.createElement("aside");
|
|
2145
|
+
sidebar.className = "report-sidebar";
|
|
2146
|
+
shell.appendChild(sidebar);
|
|
2147
|
+
|
|
2148
|
+
const treeTitle = document.createElement("div");
|
|
2149
|
+
treeTitle.className = "tree-title";
|
|
2150
|
+
treeTitle.textContent = `Files (${files.length})`;
|
|
2151
|
+
sidebar.appendChild(treeTitle);
|
|
2152
|
+
|
|
2153
|
+
const treeMount = document.createElement("div");
|
|
2154
|
+
treeMount.className = "tree-scroll";
|
|
2155
|
+
sidebar.appendChild(treeMount);
|
|
2156
|
+
|
|
2157
|
+
const main = document.createElement("section");
|
|
2158
|
+
main.className = "report-main";
|
|
2159
|
+
shell.appendChild(main);
|
|
2160
|
+
|
|
2161
|
+
const mainHead = document.createElement("div");
|
|
2162
|
+
mainHead.className = "report-main-head";
|
|
2163
|
+
main.appendChild(mainHead);
|
|
2164
|
+
|
|
2165
|
+
const currentPath = document.createElement("div");
|
|
2166
|
+
currentPath.className = "current-file";
|
|
2167
|
+
mainHead.appendChild(currentPath);
|
|
2168
|
+
|
|
2169
|
+
const viewer = document.createElement("div");
|
|
2170
|
+
viewer.className = "report-viewer";
|
|
2171
|
+
main.appendChild(viewer);
|
|
2172
|
+
|
|
2173
|
+
const fileKeyToEntry = new Map();
|
|
2174
|
+
files.forEach((file, idx) => fileKeyToEntry.set(fileUrlKey(file, idx), { file, idx }));
|
|
2175
|
+
|
|
2176
|
+
let selectedKey = preferredInitialFileKey(files);
|
|
2177
|
+
let selectedLine = preferredInitialLineNumber();
|
|
2178
|
+
|
|
2179
|
+
function renderTree() {
|
|
2180
|
+
treeMount.textContent = "";
|
|
2181
|
+
const treeRoot = buildFileTree(files);
|
|
2182
|
+
treeMount.appendChild(renderFileTreeNode(treeRoot, selectedKey, (key) => selectFile(key, true, null), 0));
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
function renderSelectedFile() {
|
|
2186
|
+
viewer.textContent = "";
|
|
2187
|
+
const entry = fileKeyToEntry.get(selectedKey);
|
|
2188
|
+
if (!entry) return;
|
|
2189
|
+
|
|
2190
|
+
currentPath.textContent = fileDisplayPath(entry.file, entry.idx);
|
|
2191
|
+
|
|
2192
|
+
viewer.appendChild(renderFileSection(entry.file, entry.idx, {
|
|
2193
|
+
includeTitle: false,
|
|
2194
|
+
lineHref: (lineno) => buildHashForSelection(selectedKey, lineno),
|
|
2195
|
+
onLineClick: (lineno) => selectLine(lineno, true)
|
|
2196
|
+
}));
|
|
2197
|
+
bindMarkerHover(viewer);
|
|
2198
|
+
focusLineInViewer(viewer, selectedLine);
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
function selectLine(lineno, updateHash) {
|
|
2202
|
+
const n = Number(lineno);
|
|
2203
|
+
selectedLine = Number.isFinite(n) && n > 0 ? n : null;
|
|
2204
|
+
renderSelectedFile();
|
|
2205
|
+
if (updateHash) setLocationHashSelection(selectedKey, selectedLine);
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
function selectFile(key, updateHash, nextLine) {
|
|
2209
|
+
if (!fileKeyToEntry.has(key)) return;
|
|
2210
|
+
if (arguments.length >= 3) {
|
|
2211
|
+
selectedLine = nextLine && Number(nextLine) > 0 ? Number(nextLine) : null;
|
|
2212
|
+
} else if (key !== selectedKey) {
|
|
2213
|
+
selectedLine = null;
|
|
2214
|
+
}
|
|
2215
|
+
selectedKey = key;
|
|
2216
|
+
renderTree();
|
|
2217
|
+
renderSelectedFile();
|
|
2218
|
+
if (updateHash) setLocationHashSelection(key, selectedLine);
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
window.addEventListener("hashchange", () => {
|
|
2222
|
+
const state = parseHashStateFromLocation();
|
|
2223
|
+
if (state.fileKey && state.fileKey !== selectedKey && fileKeyToEntry.has(state.fileKey)) {
|
|
2224
|
+
selectFile(state.fileKey, false, state.line);
|
|
2225
|
+
return;
|
|
2226
|
+
}
|
|
2227
|
+
if (state.fileKey === selectedKey || (!state.fileKey && selectedKey)) {
|
|
2228
|
+
selectedLine = state.line;
|
|
2229
|
+
renderSelectedFile();
|
|
2230
|
+
}
|
|
2231
|
+
});
|
|
2232
|
+
|
|
2233
|
+
selectFile(selectedKey, true);
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
try {
|
|
2237
|
+
const payload = JSON.parse(payloadEl.textContent || "{}");
|
|
2238
|
+
render(payload);
|
|
2239
|
+
} catch (error) {
|
|
2240
|
+
app.textContent = `Failed to render Lumitrace HTML: ${error}`;
|
|
2241
|
+
}
|
|
2242
|
+
})();'
|
|
2243
|
+
end
|
|
2244
|
+
|
|
2245
|
+
def self.html_report_styles
|
|
2246
|
+
<<~CSS
|
|
2247
|
+
body { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; background: #f7f5f0; color: #1f1f1f; padding: 24px; }
|
|
2248
|
+
.report-layout { display: grid; grid-template-columns: minmax(220px, 320px) minmax(0, 1fr); gap: 16px; align-items: start; }
|
|
2249
|
+
.report-layout.single-file { grid-template-columns: minmax(0, 1fr); }
|
|
2250
|
+
.report-sidebar { background: #fffdf7; border: 1px solid #e5dfd0; border-radius: 8px; padding: 12px; position: sticky; top: 16px; max-height: calc(100vh - 48px); overflow: hidden; }
|
|
2251
|
+
.report-layout.single-file .report-sidebar { display: none; }
|
|
2252
|
+
.tree-title { color: #444; font-size: 12px; margin-bottom: 8px; }
|
|
2253
|
+
.tree-scroll { overflow: auto; max-height: calc(100vh - 96px); }
|
|
2254
|
+
.tree-list { list-style: none; margin: 0; padding: 0; }
|
|
2255
|
+
.tree-list[data-level]:not([data-level="0"]) { margin-left: 14px; border-left: 1px dashed #e5dfd0; padding-left: 8px; }
|
|
2256
|
+
.tree-dir, .tree-file { margin: 2px 0; }
|
|
2257
|
+
.tree-folder { }
|
|
2258
|
+
.tree-folder-label { cursor: pointer; color: #4d473f; user-select: none; }
|
|
2259
|
+
.tree-folder-label::marker { color: #999; }
|
|
2260
|
+
.tree-file-btn { width: 100%; text-align: left; border: 0; background: transparent; color: #2a2a2a; padding: 4px 6px; border-radius: 6px; cursor: pointer; font: inherit; display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
|
|
2261
|
+
.tree-file-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2262
|
+
.tree-file-meta { color: #6f6a62; font-size: 12px; white-space: nowrap; }
|
|
2263
|
+
.tree-file-btn:hover { background: #fff2c6; }
|
|
2264
|
+
.tree-file-btn.active { background: #f0ffe7; color: #1b5e3d; }
|
|
2265
|
+
.tree-file-btn.active .tree-file-meta { color: #1b5e3d; }
|
|
2266
|
+
.report-main { min-width: 0; }
|
|
2267
|
+
.report-main-head { display: flex; gap: 12px; align-items: center; justify-content: space-between; margin-bottom: 8px; }
|
|
2268
|
+
.current-file { color: #333; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2269
|
+
.report-viewer { min-width: 0; }
|
|
2270
|
+
.file-section { min-width: 0; }
|
|
2271
|
+
.code { background: #fffdf7; border: 1px solid #e5dfd0; border-radius: 8px; padding: 16px; line-height: 1.5; }
|
|
2272
|
+
.line { display: block; box-sizing: border-box; padding: 2px 8px; }
|
|
2273
|
+
.line:hover { background: #fff2c6; }
|
|
2274
|
+
.line.hit { background: #f0ffe7; }
|
|
2275
|
+
.line.miss { background: #ffecec; }
|
|
2276
|
+
.line.line-target { box-shadow: inset 3px 0 #2f6f8e; background: #e9f4fb; }
|
|
2277
|
+
.line.ellipsis { color: #999; }
|
|
2278
|
+
.ln { display: inline-block; width: 3em; color: #888; user-select: none; }
|
|
2279
|
+
.ln-link { color: inherit; text-decoration: none; }
|
|
2280
|
+
.ln-link:hover { text-decoration: underline; color: #2f6f8e; }
|
|
2281
|
+
.hint { color: #666; margin-bottom: 4px; }
|
|
2282
|
+
.command { color: #555; margin-bottom: 4px; font-size: 12px; overflow-wrap: anywhere; }
|
|
2283
|
+
.mode { color: #444; margin-bottom: 8px; }
|
|
2284
|
+
.report-footer { margin-top: 16px; color: #666; font-size: 12px; }
|
|
2285
|
+
.report-footer a { color: #2f6f8e; text-decoration: none; }
|
|
2286
|
+
.report-footer a:hover { text-decoration: underline; }
|
|
2287
|
+
.file { margin: 24px 0 8px; font-size: 16px; color: #333; }
|
|
2288
|
+
.expr { position: relative; display: inline-block; padding-bottom: 1px; }
|
|
2289
|
+
.expr.hit { }
|
|
2290
|
+
.expr.depth-1 { --hl: #7fbf7f; }
|
|
2291
|
+
.expr.depth-2 { --hl: #6fa8ff; }
|
|
2292
|
+
.expr.depth-3 { --hl: #ffb347; }
|
|
2293
|
+
.expr.depth-4 { --hl: #d78bff; }
|
|
2294
|
+
.expr.depth-5 { --hl: #ff6f91; }
|
|
2295
|
+
.expr.active { background: rgba(127, 191, 127, 0.15); box-shadow: inset 0 -2px var(--hl, #7fbf7f); }
|
|
2296
|
+
.expr.miss { background: rgba(255, 120, 120, 0.18); box-shadow: inset 0 -2px rgba(200, 120, 120, 0.6); }
|
|
2297
|
+
.marker { position: relative; display: inline-block; margin-left: 4px; cursor: help; font-size: 10px; line-height: 1; user-select: none; -webkit-user-select: none; -moz-user-select: none; }
|
|
2298
|
+
.marker.miss { color: #c07070; }
|
|
2299
|
+
.marker.arg { color: #2f6f8e; }
|
|
2300
|
+
.marker .tooltip {
|
|
2301
|
+
display: none;
|
|
2302
|
+
position: absolute;
|
|
2303
|
+
left: 0;
|
|
2304
|
+
bottom: 100%;
|
|
2305
|
+
margin-bottom: 6px;
|
|
2306
|
+
background: #2b2b2b;
|
|
2307
|
+
color: #fff;
|
|
2308
|
+
padding: 4px 6px;
|
|
2309
|
+
border-radius: 4px;
|
|
2310
|
+
font-size: 12px;
|
|
2311
|
+
white-space: pre;
|
|
2312
|
+
min-width: 16ch;
|
|
2313
|
+
max-width: 90vw;
|
|
2314
|
+
overflow-x: auto;
|
|
2315
|
+
overflow-y: hidden;
|
|
2316
|
+
z-index: 10;
|
|
2317
|
+
pointer-events: auto;
|
|
2318
|
+
}
|
|
2319
|
+
.marker:hover .tooltip,
|
|
2320
|
+
.marker:focus-within .tooltip,
|
|
2321
|
+
.marker .tooltip:hover { display: block; }
|
|
2322
|
+
.noscript { color: #666; }
|
|
2323
|
+
@media (max-width: 900px) {
|
|
2324
|
+
body { padding: 16px; }
|
|
2325
|
+
.report-layout { grid-template-columns: 1fr; }
|
|
2326
|
+
.report-sidebar { position: static; max-height: none; }
|
|
2327
|
+
.tree-scroll { max-height: 220px; }
|
|
2328
|
+
.report-main-head { flex-direction: column; align-items: flex-start; }
|
|
2329
|
+
}
|
|
2330
|
+
CSS
|
|
2331
|
+
end
|
|
2332
|
+
|
|
2333
|
+
def self.footer_version_suffix
|
|
2334
|
+
return "" unless defined?(Lumitrace::VERSION) && Lumitrace::VERSION
|
|
2335
|
+
" v#{Lumitrace::VERSION}"
|
|
2336
|
+
end
|
|
2337
|
+
|
|
2338
|
+
def self.render_payload_html(payload)
|
|
1348
2339
|
<<~HTML
|
|
1349
2340
|
<!doctype html>
|
|
1350
2341
|
<html>
|
|
@@ -1352,71 +2343,22 @@ module GenerateResultedHtml
|
|
|
1352
2343
|
<meta charset="utf-8">
|
|
1353
2344
|
<title>Recorded Result View</title>
|
|
1354
2345
|
<style>
|
|
1355
|
-
|
|
1356
|
-
.code { background: #fffdf7; border: 1px solid #e5dfd0; border-radius: 8px; padding: 16px; line-height: 1.5; }
|
|
1357
|
-
.line { display: inline-block; width: 100%; box-sizing: border-box; padding: 2px 8px; }
|
|
1358
|
-
.line:hover { background: #fff2c6; }
|
|
1359
|
-
.line.hit { background: #f0ffe7; }
|
|
1360
|
-
.line.miss { background: #ffecec; }
|
|
1361
|
-
.line.ellipsis { color: #999; }
|
|
1362
|
-
.ln { display: inline-block; width: 3em; color: #888; user-select: none; }
|
|
1363
|
-
.hint { color: #666; margin-bottom: 4px; }
|
|
1364
|
-
.mode { color: #444; margin-bottom: 8px; }
|
|
1365
|
-
.expr { position: relative; display: inline-block; padding-bottom: 1px; }
|
|
1366
|
-
.expr.hit { }
|
|
1367
|
-
.expr.depth-1 { --hl: #7fbf7f; }
|
|
1368
|
-
.expr.depth-2 { --hl: #6fa8ff; }
|
|
1369
|
-
.expr.depth-3 { --hl: #ffb347; }
|
|
1370
|
-
.expr.depth-4 { --hl: #d78bff; }
|
|
1371
|
-
.expr.depth-5 { --hl: #ff6f91; }
|
|
1372
|
-
.expr.active { background: rgba(127, 191, 127, 0.15); box-shadow: inset 0 -2px var(--hl, #7fbf7f); }
|
|
1373
|
-
.expr.miss { background: rgba(255, 120, 120, 0.18); box-shadow: inset 0 -2px rgba(200, 120, 120, 0.6); }
|
|
1374
|
-
.marker { position: relative; display: inline-block; margin-left: 4px; cursor: help; font-size: 10px; line-height: 1; user-select: none; -webkit-user-select: none; -moz-user-select: none; }
|
|
1375
|
-
.marker.miss { color: #c07070; }
|
|
1376
|
-
.marker.arg { color: #2f6f8e; }
|
|
1377
|
-
.marker .tooltip {
|
|
1378
|
-
display: none;
|
|
1379
|
-
position: absolute;
|
|
1380
|
-
left: 0;
|
|
1381
|
-
top: 100%;
|
|
1382
|
-
margin-top: 4px;
|
|
1383
|
-
background: #2b2b2b;
|
|
1384
|
-
color: #fff;
|
|
1385
|
-
padding: 4px 6px;
|
|
1386
|
-
border-radius: 4px;
|
|
1387
|
-
font-size: 12px;
|
|
1388
|
-
white-space: pre;
|
|
1389
|
-
min-width: 16ch;
|
|
1390
|
-
max-width: 90vw;
|
|
1391
|
-
overflow-x: auto;
|
|
1392
|
-
overflow-y: hidden;
|
|
1393
|
-
z-index: 10;
|
|
1394
|
-
pointer-events: auto;
|
|
1395
|
-
}
|
|
1396
|
-
.marker:hover .tooltip,
|
|
1397
|
-
.marker:focus-within .tooltip,
|
|
1398
|
-
.marker .tooltip:hover { display: block; }
|
|
2346
|
+
#{html_report_styles}
|
|
1399
2347
|
</style>
|
|
1400
2348
|
</head>
|
|
1401
2349
|
<body>
|
|
1402
|
-
<div
|
|
1403
|
-
<div class="
|
|
1404
|
-
<
|
|
1405
|
-
|
|
1406
|
-
|
|
2350
|
+
<div id="lumitrace-app"></div>
|
|
2351
|
+
<div class="report-footer">Generated by <a href="https://ko1.github.io/lumitrace/" target="_blank" rel="noopener noreferrer">lumitrace</a>#{footer_version_suffix}.</div>
|
|
2352
|
+
<noscript><p class="noscript">Lumitrace HTML report requires JavaScript to render the source and trace view.</p></noscript>
|
|
2353
|
+
<script id="lumitrace-payload" type="application/json">#{payload_json_for_script(payload)}#{'</scr' + 'ipt>'}
|
|
2354
|
+
<script>
|
|
2355
|
+
#{html_renderer_js}
|
|
2356
|
+
#{'</scr' + 'ipt>'}
|
|
1407
2357
|
</body>
|
|
1408
2358
|
</html>
|
|
1409
2359
|
HTML
|
|
1410
2360
|
end
|
|
1411
2361
|
|
|
1412
|
-
def self.esc(s)
|
|
1413
|
-
s.to_s
|
|
1414
|
-
.gsub("&", "&")
|
|
1415
|
-
.gsub("<", "<")
|
|
1416
|
-
.gsub(">", ">")
|
|
1417
|
-
.gsub('"', """)
|
|
1418
|
-
end
|
|
1419
|
-
|
|
1420
2362
|
def self.detect_collect_mode(events)
|
|
1421
2363
|
arr = Array(events)
|
|
1422
2364
|
return "history" if arr.any? { |e| e.key?(:sampled_values) || e.key?("sampled_values") }
|
|
@@ -1566,6 +2508,7 @@ module GenerateResultedHtml
|
|
|
1566
2508
|
def self.comment_value_with_total_for_line(events)
|
|
1567
2509
|
best = best_event_for_line(events)
|
|
1568
2510
|
return nil unless best
|
|
2511
|
+
return nil if best[:total].to_i <= 0
|
|
1569
2512
|
|
|
1570
2513
|
sampled_last = best[:sampled_values]&.last
|
|
1571
2514
|
v, t = last_value_to_pair(sampled_last)
|
|
@@ -1669,9 +2612,16 @@ module GenerateResultedHtml
|
|
|
1669
2612
|
|
|
1670
2613
|
next if t <= s
|
|
1671
2614
|
spans << { start_col: s, end_col: t }
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
2615
|
+
event_key = e[:key] || [
|
|
2616
|
+
e[:file],
|
|
2617
|
+
e[:start_line].to_i,
|
|
2618
|
+
e[:start_col].to_i,
|
|
2619
|
+
e[:end_line].to_i,
|
|
2620
|
+
e[:end_col].to_i
|
|
2621
|
+
]
|
|
2622
|
+
key_id = event_key.join(":")
|
|
2623
|
+
buckets[event_key] = {
|
|
2624
|
+
key: event_key,
|
|
1675
2625
|
key_id: key_id,
|
|
1676
2626
|
start_col: s,
|
|
1677
2627
|
end_col: t,
|
|
@@ -1799,34 +2749,6 @@ module GenerateResultedHtml
|
|
|
1799
2749
|
ranges.any? { |(s, e)| line >= s && line <= e }
|
|
1800
2750
|
end
|
|
1801
2751
|
|
|
1802
|
-
def self.add_missing_events(events, source, filename, ranges)
|
|
1803
|
-
expected = RecordInstrument.collect_locations_from_source(source, ranges || [])
|
|
1804
|
-
existing = {}
|
|
1805
|
-
events.each do |e|
|
|
1806
|
-
key = [e[:file], e[:start_line], e[:start_col], e[:end_line], e[:end_col]]
|
|
1807
|
-
existing[key] = true
|
|
1808
|
-
end
|
|
1809
|
-
expected.each do |loc|
|
|
1810
|
-
key = [filename, loc[:start_line], loc[:start_col], loc[:end_line], loc[:end_col]]
|
|
1811
|
-
next if existing[key]
|
|
1812
|
-
events << {
|
|
1813
|
-
key: key,
|
|
1814
|
-
file: key[0],
|
|
1815
|
-
start_line: key[1],
|
|
1816
|
-
start_col: key[2],
|
|
1817
|
-
end_line: key[3],
|
|
1818
|
-
end_col: key[4],
|
|
1819
|
-
kind: loc[:kind],
|
|
1820
|
-
name: loc[:name],
|
|
1821
|
-
sampled_values: [],
|
|
1822
|
-
types: {},
|
|
1823
|
-
total: 0
|
|
1824
|
-
}
|
|
1825
|
-
existing[key] = true
|
|
1826
|
-
end
|
|
1827
|
-
events
|
|
1828
|
-
end
|
|
1829
|
-
|
|
1830
2752
|
def self.line_stats(source, ranges, events, filename)
|
|
1831
2753
|
expected_by_line = Hash.new(0)
|
|
1832
2754
|
RecordInstrument.collect_locations_from_source(source, ranges || []).each do |loc|
|
|
@@ -1850,293 +2772,130 @@ module GenerateResultedHtml
|
|
|
1850
2772
|
[expected_by_line, executed_by_line]
|
|
1851
2773
|
end
|
|
1852
2774
|
|
|
1853
|
-
def self.render_all(events_path, root: Dir.pwd, ranges_by_file: nil, collect_mode: nil, max_samples: nil)
|
|
2775
|
+
def self.render_all(events_path, root: Dir.pwd, ranges_by_file: nil, collect_mode: nil, max_samples: nil, logger: nil, command_text: nil)
|
|
1854
2776
|
raw_events = JSON.parse(File.read(events_path))
|
|
1855
|
-
render_all_from_events(
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
2777
|
+
render_all_from_events(
|
|
2778
|
+
raw_events,
|
|
2779
|
+
root: root,
|
|
2780
|
+
ranges_by_file: ranges_by_file,
|
|
2781
|
+
collect_mode: collect_mode,
|
|
2782
|
+
max_samples: max_samples,
|
|
2783
|
+
logger: logger,
|
|
2784
|
+
command_text: command_text
|
|
2785
|
+
)
|
|
2786
|
+
end
|
|
2787
|
+
|
|
2788
|
+
def self.render_all_from_events(events, root: Dir.pwd, ranges_by_file: nil, collect_mode: nil, max_samples: nil, logger: nil, command_text: nil)
|
|
2789
|
+
normalized = time_step("normalize_events", logger) { normalize_events(events) }
|
|
2790
|
+
render_all_from_normalized_events(
|
|
2791
|
+
normalized,
|
|
2792
|
+
root: root,
|
|
2793
|
+
ranges_by_file: ranges_by_file,
|
|
2794
|
+
collect_mode: collect_mode,
|
|
2795
|
+
max_samples: max_samples,
|
|
2796
|
+
logger: logger,
|
|
2797
|
+
command_text: command_text
|
|
2798
|
+
)
|
|
2799
|
+
end
|
|
2800
|
+
|
|
2801
|
+
def self.render_all_from_normalized_events(events, root: Dir.pwd, ranges_by_file: nil, collect_mode: nil, max_samples: nil, logger: nil, command_text: nil)
|
|
2802
|
+
total_start = monotonic_now
|
|
2803
|
+
|
|
2804
|
+
mode_info = time_step("resolve_mode", logger) do
|
|
2805
|
+
resolve_mode_info(events, collect_mode: collect_mode, max_samples: max_samples)
|
|
2806
|
+
end
|
|
2807
|
+
by_file = time_step("group_by_file", logger) { events.group_by { |e| e[:file] } }
|
|
2808
|
+
ranges_by_file = time_step("normalize_ranges_by_file", logger) { normalize_ranges_by_file(ranges_by_file) }
|
|
2809
|
+
|
|
2810
|
+
files = []
|
|
2811
|
+
by_file.keys.sort.each do |path|
|
|
1867
2812
|
next unless File.exist?(path)
|
|
2813
|
+
file_start = monotonic_now
|
|
2814
|
+
read_start = logger ? monotonic_now : nil
|
|
1868
2815
|
src = File.read(path)
|
|
2816
|
+
read_ms = read_start ? (monotonic_now - read_start) * 1000.0 : nil
|
|
1869
2817
|
if ranges_by_file
|
|
1870
2818
|
next unless ranges_by_file.key?(path)
|
|
1871
2819
|
ranges = ranges_by_file[path] || []
|
|
1872
2820
|
else
|
|
1873
2821
|
ranges = nil
|
|
1874
2822
|
end
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
line_class = line_class_for(expected, executed)
|
|
1893
|
-
if expected > 0 && executed == 0
|
|
1894
|
-
evs.each { |e| e[:suppress_miss] = true }
|
|
1895
|
-
end
|
|
1896
|
-
if evs.empty?
|
|
1897
|
-
html_lines << "<span class=\"line#{line_class}\" data-line=\"#{lineno}\"><span class=\"ln\">#{lineno}</span> #{esc(line_text)}</span>\n"
|
|
1898
|
-
else
|
|
1899
|
-
rendered = render_line_with_events(line_text, evs)
|
|
1900
|
-
html_lines << "<span class=\"line#{line_class}\" data-line=\"#{lineno}\"><span class=\"ln\">#{lineno}</span> #{rendered}</span>\n"
|
|
1901
|
-
end
|
|
1902
|
-
prev_lineno = lineno
|
|
1903
|
-
last_lineno = lineno
|
|
1904
|
-
end
|
|
1905
|
-
if first_lineno && first_lineno > 1
|
|
1906
|
-
html_lines.unshift("<span class=\"line ellipsis\" data-line=\"...\"><span class=\"ln\">...</span></span>\n")
|
|
1907
|
-
end
|
|
1908
|
-
if last_lineno && last_lineno < src.lines.length
|
|
1909
|
-
html_lines << "<span class=\"line ellipsis\" data-line=\"...\"><span class=\"ln\">...</span></span>\n"
|
|
2823
|
+
rel = path.start_with?(root) ? path.sub(root + File::SEPARATOR, "") : path
|
|
2824
|
+
select_start = logger ? monotonic_now : nil
|
|
2825
|
+
file_events = by_file[path] || []
|
|
2826
|
+
select_ms = select_start ? (monotonic_now - select_start) * 1000.0 : nil
|
|
2827
|
+
payload_file_start = logger ? monotonic_now : nil
|
|
2828
|
+
files << build_html_payload_file(
|
|
2829
|
+
path: path,
|
|
2830
|
+
display_path: rel,
|
|
2831
|
+
source: src,
|
|
2832
|
+
ranges: ranges,
|
|
2833
|
+
trace_events: file_events,
|
|
2834
|
+
logger: logger
|
|
2835
|
+
)
|
|
2836
|
+
payload_file_ms = payload_file_start ? (monotonic_now - payload_file_start) * 1000.0 : nil
|
|
2837
|
+
if logger
|
|
2838
|
+
elapsed_ms = (monotonic_now - file_start) * 1000.0
|
|
2839
|
+
logger.call(format("html render: file %s read=%.1fms select=%.1fms payload=%.1fms events=%d bytes=%d total=%.1fms", rel, read_ms, select_ms, payload_file_ms, file_events.length, src.bytesize, elapsed_ms))
|
|
1910
2840
|
end
|
|
2841
|
+
end
|
|
1911
2842
|
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
2843
|
+
payload = time_step("build_payload", logger) { build_html_payload(mode_info: mode_info, files: files, command_text: command_text) }
|
|
2844
|
+
html = time_step("render_payload_html", logger) { render_payload_html(payload) }
|
|
2845
|
+
if logger
|
|
2846
|
+
total_ms = (monotonic_now - total_start) * 1000.0
|
|
2847
|
+
logger.call(format("html render: total files=%d events=%d html_bytes=%d %.1fms", files.length, events.length, html.bytesize, total_ms))
|
|
2848
|
+
end
|
|
2849
|
+
html
|
|
2850
|
+
end
|
|
1920
2851
|
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
.line:hover { background: #fff2c6; }
|
|
1932
|
-
.line.hit { background: #f0ffe7; }
|
|
1933
|
-
.line.miss { background: #ffecec; }
|
|
1934
|
-
.line.ellipsis { color: #999; }
|
|
1935
|
-
.ln { display: inline-block; width: 3em; color: #888; user-select: none; }
|
|
1936
|
-
.hint { color: #666; margin-bottom: 4px; }
|
|
1937
|
-
.mode { color: #444; margin-bottom: 8px; }
|
|
1938
|
-
.file { margin: 24px 0 8px; font-size: 16px; color: #333; }
|
|
1939
|
-
.expr { position: relative; display: inline-block; padding-bottom: 1px; }
|
|
1940
|
-
.expr.hit { }
|
|
1941
|
-
.expr.depth-1 { --hl: #7fbf7f; }
|
|
1942
|
-
.expr.depth-2 { --hl: #6fa8ff; }
|
|
1943
|
-
.expr.depth-3 { --hl: #ffb347; }
|
|
1944
|
-
.expr.depth-4 { --hl: #d78bff; }
|
|
1945
|
-
.expr.depth-5 { --hl: #ff6f91; }
|
|
1946
|
-
.expr.active { background: rgba(127, 191, 127, 0.15); box-shadow: inset 0 -2px var(--hl, #7fbf7f); }
|
|
1947
|
-
.expr.miss { background: rgba(255, 120, 120, 0.18); box-shadow: inset 0 -2px rgba(200, 120, 120, 0.6); }
|
|
1948
|
-
.marker { position: relative; display: inline-block; margin-left: 4px; cursor: help; font-size: 10px; line-height: 1; user-select: none; -webkit-user-select: none; -moz-user-select: none; }
|
|
1949
|
-
.marker.miss { color: #c07070; }
|
|
1950
|
-
.marker.arg { color: #2f6f8e; }
|
|
1951
|
-
.marker .tooltip {
|
|
1952
|
-
display: none;
|
|
1953
|
-
position: absolute;
|
|
1954
|
-
left: 0;
|
|
1955
|
-
top: 100%;
|
|
1956
|
-
margin-top: 4px;
|
|
1957
|
-
background: #2b2b2b;
|
|
1958
|
-
color: #fff;
|
|
1959
|
-
padding: 4px 6px;
|
|
1960
|
-
border-radius: 4px;
|
|
1961
|
-
font-size: 12px;
|
|
1962
|
-
white-space: pre;
|
|
1963
|
-
min-width: 16ch;
|
|
1964
|
-
max-width: 90vw;
|
|
1965
|
-
overflow-x: auto;
|
|
1966
|
-
overflow-y: hidden;
|
|
1967
|
-
z-index: 10;
|
|
1968
|
-
pointer-events: auto;
|
|
1969
|
-
}
|
|
1970
|
-
.marker:hover .tooltip,
|
|
1971
|
-
.marker:focus-within .tooltip,
|
|
1972
|
-
.marker .tooltip:hover { display: block; }
|
|
1973
|
-
</style>
|
|
1974
|
-
</head>
|
|
1975
|
-
<body>
|
|
1976
|
-
<div class="hint">Hover highlighted text to see recorded values.</div>
|
|
1977
|
-
<div class="mode">#{esc(mode_info[:text])}</div>
|
|
1978
|
-
#{sections}
|
|
1979
|
-
<script>
|
|
1980
|
-
(function() {
|
|
1981
|
-
document.querySelectorAll('.marker').forEach(marker => {
|
|
1982
|
-
marker.addEventListener('mouseenter', () => {
|
|
1983
|
-
document.querySelectorAll('.expr').forEach(e => e.classList.remove('active'));
|
|
1984
|
-
const key = marker.dataset.key;
|
|
1985
|
-
if (key) {
|
|
1986
|
-
document.querySelectorAll(`.expr[data-key="${key}"]`).forEach(e => e.classList.add('active'));
|
|
1987
|
-
} else {
|
|
1988
|
-
marker.closest('.expr')?.classList.add('active');
|
|
1989
|
-
}
|
|
1990
|
-
});
|
|
1991
|
-
marker.addEventListener('mouseleave', () => {
|
|
1992
|
-
const key = marker.dataset.key;
|
|
1993
|
-
if (key) {
|
|
1994
|
-
document.querySelectorAll(`.expr[data-key="${key}"]`).forEach(e => e.classList.remove('active'));
|
|
1995
|
-
} else {
|
|
1996
|
-
marker.closest('.expr')?.classList.remove('active');
|
|
1997
|
-
}
|
|
1998
|
-
});
|
|
1999
|
-
});
|
|
2000
|
-
})();
|
|
2001
|
-
#{'</scr' + 'ipt>'}
|
|
2002
|
-
</body>
|
|
2003
|
-
</html>
|
|
2004
|
-
HTML
|
|
2852
|
+
def self.render_source_from_events(source, events, filename: "script.rb", ranges: nil, collect_mode: nil, max_samples: nil, command_text: nil)
|
|
2853
|
+
render_source_from_normalized_events(
|
|
2854
|
+
source,
|
|
2855
|
+
normalize_events(events),
|
|
2856
|
+
filename: filename,
|
|
2857
|
+
ranges: ranges,
|
|
2858
|
+
collect_mode: collect_mode,
|
|
2859
|
+
max_samples: max_samples,
|
|
2860
|
+
command_text: command_text
|
|
2861
|
+
)
|
|
2005
2862
|
end
|
|
2006
2863
|
|
|
2007
|
-
def self.
|
|
2864
|
+
def self.render_source_from_normalized_events(source, events, filename: "script.rb", ranges: nil, collect_mode: nil, max_samples: nil, command_text: nil)
|
|
2008
2865
|
mode_info = resolve_mode_info(events, collect_mode: collect_mode, max_samples: max_samples)
|
|
2009
|
-
events = normalize_events(events)
|
|
2010
2866
|
ranges = normalize_ranges(ranges)
|
|
2011
|
-
target_events =
|
|
2012
|
-
expected_by_line, executed_by_line = line_stats(source, ranges, target_events, filename)
|
|
2867
|
+
target_events = events.select { |e| e[:file] == filename }
|
|
2013
2868
|
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
expected = expected_by_line[lineno]
|
|
2028
|
-
executed = executed_by_line[lineno]
|
|
2029
|
-
line_class = line_class_for(expected, executed)
|
|
2030
|
-
if expected > 0 && executed == 0
|
|
2031
|
-
evs.each { |e| e[:suppress_miss] = true }
|
|
2032
|
-
end
|
|
2033
|
-
if evs.empty?
|
|
2034
|
-
html_lines << "<span class=\"line#{line_class}\" data-line=\"#{lineno}\"><span class=\"ln\">#{lineno}</span> #{esc(line_text)}</span>\n"
|
|
2035
|
-
else
|
|
2036
|
-
rendered = render_line_with_events(line_text, evs)
|
|
2037
|
-
html_lines << "<span class=\"line#{line_class}\" data-line=\"#{lineno}\"><span class=\"ln\">#{lineno}</span> #{rendered}</span>\n"
|
|
2038
|
-
end
|
|
2039
|
-
prev_lineno = lineno
|
|
2040
|
-
last_lineno = lineno
|
|
2041
|
-
end
|
|
2042
|
-
if first_lineno && first_lineno > 1
|
|
2043
|
-
html_lines.unshift("<span class=\"line ellipsis\" data-line=\"...\"><span class=\"ln\">...</span></span>\n")
|
|
2044
|
-
end
|
|
2045
|
-
if last_lineno && last_lineno < source.lines.length
|
|
2046
|
-
html_lines << "<span class=\"line ellipsis\" data-line=\"...\"><span class=\"ln\">...</span></span>\n"
|
|
2047
|
-
end
|
|
2869
|
+
payload = build_html_payload(
|
|
2870
|
+
mode_info: mode_info,
|
|
2871
|
+
command_text: command_text,
|
|
2872
|
+
files: [
|
|
2873
|
+
build_html_payload_file(
|
|
2874
|
+
path: filename,
|
|
2875
|
+
display_path: filename,
|
|
2876
|
+
source: source,
|
|
2877
|
+
ranges: ranges,
|
|
2878
|
+
trace_events: target_events
|
|
2879
|
+
)
|
|
2880
|
+
]
|
|
2881
|
+
)
|
|
2048
2882
|
|
|
2049
|
-
|
|
2050
|
-
<!doctype html>
|
|
2051
|
-
<html>
|
|
2052
|
-
<head>
|
|
2053
|
-
<meta charset="utf-8">
|
|
2054
|
-
<title>Recorded Result View</title>
|
|
2055
|
-
<style>
|
|
2056
|
-
body { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; background: #f7f5f0; color: #1f1f1f; padding: 24px; }
|
|
2057
|
-
.code { background: #fffdf7; border: 1px solid #e5dfd0; border-radius: 8px; padding: 16px; line-height: 1.5; }
|
|
2058
|
-
.line { display: inline-block; width: 100%; box-sizing: border-box; padding: 2px 8px; }
|
|
2059
|
-
.line:hover { background: #fff2c6; }
|
|
2060
|
-
.line.hit { background: #f0ffe7; }
|
|
2061
|
-
.line.miss { background: #ffecec; }
|
|
2062
|
-
.line.ellipsis { color: #999; }
|
|
2063
|
-
.ln { display: inline-block; width: 3em; color: #888; user-select: none; }
|
|
2064
|
-
.hint { color: #666; margin-bottom: 4px; }
|
|
2065
|
-
.mode { color: #444; margin-bottom: 8px; }
|
|
2066
|
-
.file { margin: 24px 0 8px; font-size: 16px; color: #333; }
|
|
2067
|
-
.expr { position: relative; display: inline-block; padding-bottom: 1px; }
|
|
2068
|
-
.expr.hit { }
|
|
2069
|
-
.expr.depth-1 { --hl: #7fbf7f; }
|
|
2070
|
-
.expr.depth-2 { --hl: #6fa8ff; }
|
|
2071
|
-
.expr.depth-3 { --hl: #ffb347; }
|
|
2072
|
-
.expr.depth-4 { --hl: #d78bff; }
|
|
2073
|
-
.expr.depth-5 { --hl: #ff6f91; }
|
|
2074
|
-
.expr.active { background: rgba(127, 191, 127, 0.15); box-shadow: inset 0 -2px var(--hl, #7fbf7f); }
|
|
2075
|
-
.expr.miss { background: rgba(255, 120, 120, 0.18); box-shadow: inset 0 -2px rgba(200, 120, 120, 0.6); }
|
|
2076
|
-
.marker { position: relative; display: inline-block; margin-left: 4px; cursor: help; font-size: 10px; line-height: 1; user-select: none; -webkit-user-select: none; -moz-user-select: none; }
|
|
2077
|
-
.marker.miss { color: #c07070; }
|
|
2078
|
-
.marker.arg { color: #2f6f8e; }
|
|
2079
|
-
.marker .tooltip {
|
|
2080
|
-
display: none;
|
|
2081
|
-
position: absolute;
|
|
2082
|
-
left: 0;
|
|
2083
|
-
top: 100%;
|
|
2084
|
-
margin-top: 4px;
|
|
2085
|
-
background: #2b2b2b;
|
|
2086
|
-
color: #fff;
|
|
2087
|
-
padding: 4px 6px;
|
|
2088
|
-
border-radius: 4px;
|
|
2089
|
-
font-size: 12px;
|
|
2090
|
-
white-space: pre;
|
|
2091
|
-
min-width: 16ch;
|
|
2092
|
-
max-width: 90vw;
|
|
2093
|
-
overflow-x: auto;
|
|
2094
|
-
overflow-y: hidden;
|
|
2095
|
-
z-index: 10;
|
|
2096
|
-
pointer-events: auto;
|
|
2097
|
-
}
|
|
2098
|
-
.marker:hover .tooltip,
|
|
2099
|
-
.marker:focus-within .tooltip,
|
|
2100
|
-
.marker .tooltip:hover { display: block; }
|
|
2101
|
-
</style>
|
|
2102
|
-
</head>
|
|
2103
|
-
<body>
|
|
2104
|
-
<div class="hint">Hover highlighted text to see recorded values.</div>
|
|
2105
|
-
<div class="mode">#{esc(mode_info[:text])}</div>
|
|
2106
|
-
<h2 class="file">#{esc(filename)}</h2>
|
|
2107
|
-
<pre class="code"><code>
|
|
2108
|
-
#{html_lines.join("")}
|
|
2109
|
-
</code></pre>
|
|
2110
|
-
<script>
|
|
2111
|
-
(function() {
|
|
2112
|
-
document.querySelectorAll('.marker').forEach(marker => {
|
|
2113
|
-
marker.addEventListener('mouseenter', () => {
|
|
2114
|
-
document.querySelectorAll('.expr').forEach(e => e.classList.remove('active'));
|
|
2115
|
-
const key = marker.dataset.key;
|
|
2116
|
-
if (key) {
|
|
2117
|
-
document.querySelectorAll(`.expr[data-key="${key}"]`).forEach(e => e.classList.add('active'));
|
|
2118
|
-
} else {
|
|
2119
|
-
marker.closest('.expr')?.classList.add('active');
|
|
2120
|
-
}
|
|
2121
|
-
});
|
|
2122
|
-
marker.addEventListener('mouseleave', () => {
|
|
2123
|
-
const key = marker.dataset.key;
|
|
2124
|
-
if (key) {
|
|
2125
|
-
document.querySelectorAll(`.expr[data-key="${key}"]`).forEach(e => e.classList.remove('active'));
|
|
2126
|
-
} else {
|
|
2127
|
-
marker.closest('.expr')?.classList.remove('active');
|
|
2128
|
-
}
|
|
2129
|
-
});
|
|
2130
|
-
});
|
|
2131
|
-
})();
|
|
2132
|
-
#{'</scr' + 'ipt>'}
|
|
2133
|
-
</body>
|
|
2134
|
-
</html>
|
|
2135
|
-
HTML
|
|
2883
|
+
render_payload_html(payload)
|
|
2136
2884
|
end
|
|
2137
2885
|
|
|
2138
2886
|
def self.render_text_from_events(source, events, filename: "script.rb", ranges: nil, with_header: true, header_label: nil, tty: nil)
|
|
2139
|
-
|
|
2887
|
+
render_text_from_normalized_events(
|
|
2888
|
+
source,
|
|
2889
|
+
normalize_events(events),
|
|
2890
|
+
filename: filename,
|
|
2891
|
+
ranges: ranges,
|
|
2892
|
+
with_header: with_header,
|
|
2893
|
+
header_label: header_label,
|
|
2894
|
+
tty: tty
|
|
2895
|
+
)
|
|
2896
|
+
end
|
|
2897
|
+
|
|
2898
|
+
def self.render_text_from_normalized_events(source, events, filename: "script.rb", ranges: nil, with_header: true, header_label: nil, tty: nil)
|
|
2140
2899
|
ranges = normalize_ranges(ranges)
|
|
2141
2900
|
target_events = events.select { |e| e[:file] == filename }
|
|
2142
2901
|
term_width = tty ? terminal_width : nil
|
|
@@ -2158,7 +2917,7 @@ module GenerateResultedHtml
|
|
|
2158
2917
|
]
|
|
2159
2918
|
next if seen[key]
|
|
2160
2919
|
seen[key] = true
|
|
2161
|
-
executed_by_line[line] += 1 if line
|
|
2920
|
+
executed_by_line[line] += 1 if line && (e[:total] || e["total"]).to_i > 0
|
|
2162
2921
|
end
|
|
2163
2922
|
|
|
2164
2923
|
out = +""
|
|
@@ -2259,7 +3018,10 @@ module GenerateResultedHtml
|
|
|
2259
3018
|
end
|
|
2260
3019
|
|
|
2261
3020
|
def self.render_text_all_from_events(events, root: Dir.pwd, ranges_by_file: nil, tty: nil)
|
|
2262
|
-
|
|
3021
|
+
render_text_all_from_normalized_events(normalize_events(events), root: root, ranges_by_file: ranges_by_file, tty: tty)
|
|
3022
|
+
end
|
|
3023
|
+
|
|
3024
|
+
def self.render_text_all_from_normalized_events(events, root: Dir.pwd, ranges_by_file: nil, tty: nil)
|
|
2263
3025
|
by_file = events.group_by { |e| e[:file] }
|
|
2264
3026
|
ranges_by_file = normalize_ranges_by_file(ranges_by_file)
|
|
2265
3027
|
|
|
@@ -2273,7 +3035,7 @@ module GenerateResultedHtml
|
|
|
2273
3035
|
ranges = nil
|
|
2274
3036
|
end
|
|
2275
3037
|
rel = path.start_with?(root) ? path.sub(root + File::SEPARATOR, "") : path
|
|
2276
|
-
|
|
3038
|
+
render_text_from_normalized_events(src, events, filename: path, ranges: ranges, with_header: true, header_label: rel, tty: tty)
|
|
2277
3039
|
end.compact
|
|
2278
3040
|
|
|
2279
3041
|
header = "\n=== Lumitrace Results (text) ===\n\n"
|