bobframes 0.1.0__py3-none-any.whl
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.
- bobframes/__init__.py +3 -0
- bobframes/_version.py +1 -0
- bobframes/catalog.py +154 -0
- bobframes/cli.py +266 -0
- bobframes/derive_post_merge.py +365 -0
- bobframes/derives/__init__.py +0 -0
- bobframes/derives/pass_class_breakdown.py +102 -0
- bobframes/derives/texture_usage.py +121 -0
- bobframes/discovery.py +132 -0
- bobframes/global_entities.py +99 -0
- bobframes/html/__init__.py +0 -0
- bobframes/html/template.py +1056 -0
- bobframes/lint.py +114 -0
- bobframes/manifest.py +127 -0
- bobframes/parquetize.py +282 -0
- bobframes/parsers/__init__.py +0 -0
- bobframes/parsers/derive_program_transitions.py +73 -0
- bobframes/parsers/parse_init_state.py +675 -0
- bobframes/paths.py +111 -0
- bobframes/probes/__init__.py +0 -0
- bobframes/probes/whatif.py +165 -0
- bobframes/qrd_harness.py +119 -0
- bobframes/query_examples.py +222 -0
- bobframes/rdcmd.py +72 -0
- bobframes/replay/__init__.py +26 -0
- bobframes/replay/replay_main.py +2305 -0
- bobframes/reports/__init__.py +0 -0
- bobframes/reports/_dashboard.py +425 -0
- bobframes/reports/ab.py +88 -0
- bobframes/reports/base.py +114 -0
- bobframes/reports/cache.py +147 -0
- bobframes/reports/chrome.py +1306 -0
- bobframes/reports/cli.py +99 -0
- bobframes/reports/delta.py +167 -0
- bobframes/reports/discovery.py +118 -0
- bobframes/reports/draws_by_class.py +165 -0
- bobframes/reports/formatters.py +122 -0
- bobframes/reports/instancing_opportunities.py +276 -0
- bobframes/reports/orchestrator.py +59 -0
- bobframes/reports/overdraw.py +293 -0
- bobframes/reports/pass_gpu.py +190 -0
- bobframes/reports/shader_hotlist.py +240 -0
- bobframes/reports/trend_table.py +444 -0
- bobframes/resource_labels.py +162 -0
- bobframes/run.py +480 -0
- bobframes/schemas.py +426 -0
- bobframes/stable_keys.py +83 -0
- bobframes/tests/__init__.py +0 -0
- bobframes/tests/_render_util.py +84 -0
- bobframes/tests/data/golden/_reports/draws_by_class.html +323 -0
- bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/index.html +1560 -0
- bobframes/tests/data/golden/_reports/index.html +264 -0
- bobframes/tests/data/golden/_reports/instancing_opportunities.html +266 -0
- bobframes/tests/data/golden/_reports/overdraw.html +275 -0
- bobframes/tests/data/golden/_reports/pass_gpu.html +277 -0
- bobframes/tests/data/golden/_reports/shader_hotlist.html +265 -0
- bobframes/tests/data/golden/_reports/trend_table.html +390 -0
- bobframes/tests/data/golden/index.html +1175 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/_manifest.json +51 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/buffers.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/clears.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/counters_per_event.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/descriptor_access.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/dispatches.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/draw_bindings.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/draws.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/events.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/fbos.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/frame_totals.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/ibo_samples.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/indirect_args.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/passes.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/pixel_history.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/post_vs_samples.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/program_transitions.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/programs.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/render_targets.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/resource_creation.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/rt_event_timeline.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/samplers.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/shaders.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/state_change_events.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/texture_samples.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/textures.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/vbo_samples.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/vertex_inputs.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/_manifest.json +51 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/buffers.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/clears.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/counters_per_event.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/descriptor_access.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/dispatches.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/draw_bindings.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/draws.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/events.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/fbos.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/frame_totals.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/ibo_samples.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/indirect_args.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/passes.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/pixel_history.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/post_vs_samples.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/program_transitions.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/programs.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/render_targets.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/resource_creation.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/rt_event_timeline.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/samplers.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/shaders.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/state_change_events.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/texture_samples.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/textures.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/vbo_samples.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/vertex_inputs.parquet +0 -0
- bobframes/tests/make_synthetic.py +171 -0
- bobframes/tests/smoke.py +199 -0
- bobframes/tests/test_determinism.py +19 -0
- bobframes/tests/test_discovery.py +97 -0
- bobframes/tests/test_hardening.py +142 -0
- bobframes/tests/test_parity.py +22 -0
- bobframes/tests/test_perf.py +18 -0
- bobframes/tests/test_replay_drift.py +115 -0
- bobframes/tests/test_schemas.py +26 -0
- bobframes/tests/test_schemas_unit.py +55 -0
- bobframes/tests/test_stable_keys.py +61 -0
- bobframes-0.1.0.dist-info/METADATA +144 -0
- bobframes-0.1.0.dist-info/RECORD +130 -0
- bobframes-0.1.0.dist-info/WHEEL +4 -0
- bobframes-0.1.0.dist-info/entry_points.txt +2 -0
- bobframes-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 3,
|
|
3
|
+
"build_timestamp": "2026-01-01T00:00:00+00:00",
|
|
4
|
+
"area": "District 01",
|
|
5
|
+
"drop_date": "2026-05-27",
|
|
6
|
+
"drop_label": "r110565",
|
|
7
|
+
"captures": [
|
|
8
|
+
"1",
|
|
9
|
+
"2",
|
|
10
|
+
"3",
|
|
11
|
+
"4",
|
|
12
|
+
"5"
|
|
13
|
+
],
|
|
14
|
+
"capture_status": {
|
|
15
|
+
"1": "ok",
|
|
16
|
+
"2": "ok",
|
|
17
|
+
"3": "ok",
|
|
18
|
+
"4": "ok",
|
|
19
|
+
"5": "ok"
|
|
20
|
+
},
|
|
21
|
+
"row_counts": {
|
|
22
|
+
"draws": 60,
|
|
23
|
+
"draw_bindings": 60,
|
|
24
|
+
"passes": 60,
|
|
25
|
+
"shaders": 60,
|
|
26
|
+
"textures": 60,
|
|
27
|
+
"render_targets": 45,
|
|
28
|
+
"rt_event_timeline": 60,
|
|
29
|
+
"buffers": 60,
|
|
30
|
+
"programs": 60,
|
|
31
|
+
"samplers": 55,
|
|
32
|
+
"fbos": 60,
|
|
33
|
+
"events": 60,
|
|
34
|
+
"clears": 40,
|
|
35
|
+
"dispatches": 45,
|
|
36
|
+
"state_change_events": 60,
|
|
37
|
+
"indirect_args": 0,
|
|
38
|
+
"vertex_inputs": 60,
|
|
39
|
+
"resource_creation": 60,
|
|
40
|
+
"counters_per_event": 60,
|
|
41
|
+
"descriptor_access": 60,
|
|
42
|
+
"program_transitions": 60,
|
|
43
|
+
"frame_totals": 5,
|
|
44
|
+
"pixel_history": 60,
|
|
45
|
+
"vbo_samples": 60,
|
|
46
|
+
"ibo_samples": 60,
|
|
47
|
+
"post_vs_samples": 60,
|
|
48
|
+
"texture_samples": 60
|
|
49
|
+
},
|
|
50
|
+
"rotated_from": null
|
|
51
|
+
}
|
|
Binary file
|
|
Binary file
|
bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/counters_per_event.parquet
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/program_transitions.parquet
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/state_change_events.parquet
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 3,
|
|
3
|
+
"build_timestamp": "2026-01-01T00:00:00+00:00",
|
|
4
|
+
"area": "District 01",
|
|
5
|
+
"drop_date": "2026-05-28",
|
|
6
|
+
"drop_label": "r110600",
|
|
7
|
+
"captures": [
|
|
8
|
+
"1",
|
|
9
|
+
"2",
|
|
10
|
+
"3",
|
|
11
|
+
"4",
|
|
12
|
+
"5"
|
|
13
|
+
],
|
|
14
|
+
"capture_status": {
|
|
15
|
+
"1": "ok",
|
|
16
|
+
"2": "ok",
|
|
17
|
+
"3": "ok",
|
|
18
|
+
"4": "ok",
|
|
19
|
+
"5": "ok"
|
|
20
|
+
},
|
|
21
|
+
"row_counts": {
|
|
22
|
+
"draws": 60,
|
|
23
|
+
"draw_bindings": 60,
|
|
24
|
+
"passes": 60,
|
|
25
|
+
"shaders": 60,
|
|
26
|
+
"textures": 60,
|
|
27
|
+
"render_targets": 45,
|
|
28
|
+
"rt_event_timeline": 60,
|
|
29
|
+
"buffers": 60,
|
|
30
|
+
"programs": 60,
|
|
31
|
+
"samplers": 55,
|
|
32
|
+
"fbos": 60,
|
|
33
|
+
"events": 60,
|
|
34
|
+
"clears": 40,
|
|
35
|
+
"dispatches": 45,
|
|
36
|
+
"state_change_events": 60,
|
|
37
|
+
"indirect_args": 0,
|
|
38
|
+
"vertex_inputs": 60,
|
|
39
|
+
"resource_creation": 60,
|
|
40
|
+
"counters_per_event": 60,
|
|
41
|
+
"descriptor_access": 60,
|
|
42
|
+
"program_transitions": 60,
|
|
43
|
+
"frame_totals": 5,
|
|
44
|
+
"pixel_history": 60,
|
|
45
|
+
"vbo_samples": 60,
|
|
46
|
+
"ibo_samples": 60,
|
|
47
|
+
"post_vs_samples": 60,
|
|
48
|
+
"texture_samples": 60
|
|
49
|
+
},
|
|
50
|
+
"rotated_from": null
|
|
51
|
+
}
|
|
Binary file
|
|
Binary file
|
bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/counters_per_event.parquet
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/program_transitions.parquet
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/state_change_events.parquet
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Generate the bundled synthetic `_data/` test fixture from a real ingest.
|
|
2
|
+
|
|
3
|
+
Derives a tiny, content-scrubbed dataset from a real `_data/` tree so the golden-parity
|
|
4
|
+
suite has something to render in CI without committing real captures (ADR-8). NOT run in
|
|
5
|
+
CI — run by hand to (re)generate the fixture, then commit the result + refreshed golden.
|
|
6
|
+
|
|
7
|
+
Scrubbing policy (ADR-8 + ADR-6):
|
|
8
|
+
* area name -> generic ("District 01")
|
|
9
|
+
* marker/pass paths -> KEYWORD-PRESERVING (keeps the draw_class keyword so the classifier
|
|
10
|
+
still produces the same class distribution; strips game content)
|
|
11
|
+
* labels / names / file paths -> generic per-row tokens
|
|
12
|
+
* counter names, formats, enums, blend/depth numerics -> KEPT (not content; load-bearing,
|
|
13
|
+
e.g. 'GPU Duration' drives pass_gpu)
|
|
14
|
+
* row counts truncated to --rows per table
|
|
15
|
+
|
|
16
|
+
Usage (from the venv):
|
|
17
|
+
python -m bobframes.tests.make_synthetic \
|
|
18
|
+
--src "c:/Users/vsiva/Downloads/RDC mainline r110565 25-05-2026/_data" \
|
|
19
|
+
--out bobframes/tests/data/synthetic --rows 60
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
|
|
27
|
+
import pyarrow as pa
|
|
28
|
+
import pyarrow.parquet as pq
|
|
29
|
+
|
|
30
|
+
from .. import schemas
|
|
31
|
+
|
|
32
|
+
SYNTH_AREA = "District 01"
|
|
33
|
+
FIXED_TS = "2026-01-01T00:00:00+00:00"
|
|
34
|
+
|
|
35
|
+
# Two synthetic drops (different build labels/dates) so trend/sparkline reports have >1 point.
|
|
36
|
+
DROPS = [
|
|
37
|
+
{"date": "2026-05-27", "label": "r110565"},
|
|
38
|
+
{"date": "2026-05-28", "label": "r110600"},
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
# Derived tables are regenerated by render-only (derive/pass_class_breakdown/texture_usage); skip.
|
|
42
|
+
SKIP_TABLES = {"pass_class_breakdown", "texture_usage"}
|
|
43
|
+
|
|
44
|
+
# String columns that carry game content -> generic per-row token.
|
|
45
|
+
GENERIC_SCRUB = {
|
|
46
|
+
"draw_name", "label", "src_file_path", "declared_label",
|
|
47
|
+
"attribute_name", "usage_name",
|
|
48
|
+
}
|
|
49
|
+
# Marker/pass-path columns -> keyword-preserving (keeps draw_class coverage).
|
|
50
|
+
MARKER_SCRUB = {
|
|
51
|
+
"marker_path", "marker_path_norm", "parent_pass_path", "parent_pass_path_norm",
|
|
52
|
+
"parent_marker_path",
|
|
53
|
+
}
|
|
54
|
+
_STRING_TYPES = (pa.string(), pa.large_string())
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _marker_token(mp: str) -> str:
|
|
58
|
+
"""Return a class-preserving stand-in for a marker path (mirrors _classify_draw order)."""
|
|
59
|
+
s = (mp or "").lower()
|
|
60
|
+
if "shadow" in s:
|
|
61
|
+
return "shadow"
|
|
62
|
+
if "prepass" in s or "depthonly" in s:
|
|
63
|
+
return "prepass"
|
|
64
|
+
if "slate" in s or "/ui" in s or s.endswith("ui"):
|
|
65
|
+
return "slate" # -> ui branch
|
|
66
|
+
if "postprocess" in s or "tonemap" in s or "bloom" in s or "eyeadapt" in s:
|
|
67
|
+
return "postprocess"
|
|
68
|
+
if "decal" in s:
|
|
69
|
+
return "decal"
|
|
70
|
+
if "translucen" in s:
|
|
71
|
+
return "translucent"
|
|
72
|
+
if "basepass" in s:
|
|
73
|
+
return "basepass"
|
|
74
|
+
return "scene" # no keyword -> class falls to blend/depth (preserved)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _scrub_table(t: pa.Table, area: str, date: str, label: str) -> pa.Table:
|
|
78
|
+
n = t.num_rows
|
|
79
|
+
field_type = {f.name: f.type for f in t.schema}
|
|
80
|
+
out = {}
|
|
81
|
+
for name in t.column_names:
|
|
82
|
+
ft = field_type[name]
|
|
83
|
+
if name == "area":
|
|
84
|
+
out[name] = pa.array([area] * n, type=ft)
|
|
85
|
+
elif name == "drop_date":
|
|
86
|
+
out[name] = pa.array([date] * n, type=ft)
|
|
87
|
+
elif name == "drop_label":
|
|
88
|
+
out[name] = pa.array([label] * n, type=ft)
|
|
89
|
+
elif name in MARKER_SCRUB and ft in _STRING_TYPES:
|
|
90
|
+
orig = t.column(name).to_pylist()
|
|
91
|
+
out[name] = pa.array(
|
|
92
|
+
[f"Frame 1/{_marker_token(v)}/p{i}" if v is not None else None
|
|
93
|
+
for i, v in enumerate(orig)],
|
|
94
|
+
type=ft,
|
|
95
|
+
)
|
|
96
|
+
elif name in GENERIC_SCRUB and ft in _STRING_TYPES:
|
|
97
|
+
orig = t.column(name).to_pylist()
|
|
98
|
+
out[name] = pa.array(
|
|
99
|
+
[f"{name}_{i}" if v is not None else None for i, v in enumerate(orig)],
|
|
100
|
+
type=ft,
|
|
101
|
+
)
|
|
102
|
+
else:
|
|
103
|
+
out[name] = t.column(name)
|
|
104
|
+
# Preserve column order exactly (schema regression checks names/order).
|
|
105
|
+
return pa.table({name: out[name] for name in t.column_names})
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _build_drop(src_area_dir: str, src_drop: str, out_drop_dir: str,
|
|
109
|
+
date: str, label: str) -> dict:
|
|
110
|
+
os.makedirs(out_drop_dir, exist_ok=True)
|
|
111
|
+
src_drop_dir = os.path.join(src_area_dir, src_drop)
|
|
112
|
+
row_counts: dict[str, int] = {}
|
|
113
|
+
captures: set[str] = set()
|
|
114
|
+
for stem in schemas.TABLES:
|
|
115
|
+
if stem in SKIP_TABLES:
|
|
116
|
+
continue
|
|
117
|
+
src = os.path.join(src_drop_dir, f"{stem}.parquet")
|
|
118
|
+
if not os.path.exists(src):
|
|
119
|
+
continue
|
|
120
|
+
t = pq.read_table(src)
|
|
121
|
+
t = t.slice(0, ARGS.rows)
|
|
122
|
+
t = _scrub_table(t, SYNTH_AREA, date, label)
|
|
123
|
+
pq.write_table(t, os.path.join(out_drop_dir, f"{stem}.parquet"), compression="snappy")
|
|
124
|
+
row_counts[stem] = t.num_rows
|
|
125
|
+
if "capture" in t.column_names:
|
|
126
|
+
captures.update(str(c) for c in t.column("capture").to_pylist() if c is not None)
|
|
127
|
+
caps = sorted(captures, key=lambda x: (len(x), x))
|
|
128
|
+
manifest = {
|
|
129
|
+
"schema_version": schemas.SCHEMA_VERSION,
|
|
130
|
+
"build_timestamp": FIXED_TS,
|
|
131
|
+
"area": SYNTH_AREA,
|
|
132
|
+
"drop_date": date,
|
|
133
|
+
"drop_label": label,
|
|
134
|
+
"captures": caps,
|
|
135
|
+
"capture_status": {c: "ok" for c in caps},
|
|
136
|
+
"row_counts": row_counts,
|
|
137
|
+
"rotated_from": None,
|
|
138
|
+
}
|
|
139
|
+
with open(os.path.join(out_drop_dir, "_manifest.json"), "w", encoding="utf-8") as f:
|
|
140
|
+
json.dump(manifest, f, indent=2)
|
|
141
|
+
f.write("\n")
|
|
142
|
+
return {"label": label, "tables": len(row_counts), "captures": caps}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def main() -> int:
|
|
146
|
+
src = ARGS.src
|
|
147
|
+
areas = sorted(d for d in os.listdir(src) if os.path.isdir(os.path.join(src, d)))
|
|
148
|
+
if len(areas) < len(DROPS):
|
|
149
|
+
raise SystemExit(f"need >= {len(DROPS)} source areas, found {len(areas)} in {src}")
|
|
150
|
+
out_area = os.path.join(ARGS.out, "_data", SYNTH_AREA)
|
|
151
|
+
for spec, src_area in zip(DROPS, areas):
|
|
152
|
+
src_area_dir = os.path.join(src, src_area)
|
|
153
|
+
src_drops = sorted(d for d in os.listdir(src_area_dir)
|
|
154
|
+
if os.path.isdir(os.path.join(src_area_dir, d)))
|
|
155
|
+
if not src_drops:
|
|
156
|
+
raise SystemExit(f"no drops under {src_area_dir}")
|
|
157
|
+
out_drop = os.path.join(out_area, f"{spec['date']}_{spec['label']}")
|
|
158
|
+
info = _build_drop(src_area_dir, src_drops[0], out_drop, spec["date"], spec["label"])
|
|
159
|
+
print(f" {SYNTH_AREA}/{spec['date']}_{spec['label']} <- {src_area}/{src_drops[0]} "
|
|
160
|
+
f"({info['tables']} tables, captures={info['captures']})")
|
|
161
|
+
print(f"synthetic _data written under {os.path.join(ARGS.out, '_data')}")
|
|
162
|
+
return 0
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
if __name__ == "__main__":
|
|
166
|
+
ap = argparse.ArgumentParser(prog="bobframes.tests.make_synthetic")
|
|
167
|
+
ap.add_argument("--src", required=True, help="real _data/ tree to derive from")
|
|
168
|
+
ap.add_argument("--out", default=os.path.join(os.path.dirname(__file__), "data", "synthetic"))
|
|
169
|
+
ap.add_argument("--rows", type=int, default=60, help="max rows per table")
|
|
170
|
+
ARGS = ap.parse_args()
|
|
171
|
+
raise SystemExit(main())
|
bobframes/tests/smoke.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""End-to-end smoke test (c15 rewrite — G-12).
|
|
2
|
+
|
|
3
|
+
Two modes, no hardcoded area/label/date (the old `Chor bazar` / `r110565` / `2026-05-27`
|
|
4
|
+
constants + the `__file__`-walked project root are gone):
|
|
5
|
+
|
|
6
|
+
bobframes smoke render-only against the bundled synthetic `_data/` fixture
|
|
7
|
+
(no `.rdc`, no qrenderdoc/GPU). Runs everywhere, incl. CI.
|
|
8
|
+
bobframes smoke --data <DIR> full ingest + render against a real capture root; auto-selects
|
|
9
|
+
area + latest drop via discovery.find_drops. Needs Windows +
|
|
10
|
+
RenderDoc; self-hosted / nightly only.
|
|
11
|
+
|
|
12
|
+
Both modes assert: outputs exist, every Parquet matches schemas.expected_columns, entity tables
|
|
13
|
+
carry a populated stable_key, catalog is rebuilt, and every emitted HTML is lint-clean.
|
|
14
|
+
|
|
15
|
+
Run standalone: python -m bobframes.tests.smoke [--data DIR]
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import subprocess
|
|
24
|
+
import sys
|
|
25
|
+
import tempfile
|
|
26
|
+
|
|
27
|
+
import pyarrow.parquet as papq
|
|
28
|
+
|
|
29
|
+
from .. import discovery, lint, paths, schemas
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _fail(msg: str) -> int:
|
|
33
|
+
print(f'FAIL: {msg}')
|
|
34
|
+
return 1
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _lint_html(root: str) -> int:
|
|
38
|
+
"""Lint every emitted HTML under `root` (skipping the parquet cache). Returns hit count."""
|
|
39
|
+
from . import _render_util as u
|
|
40
|
+
hits = 0
|
|
41
|
+
for rel in u.rendered_html_files(root):
|
|
42
|
+
for lineno, label, snip in lint.lint_file(os.path.join(root, rel)):
|
|
43
|
+
print(f' LINT {rel}:{lineno}: [{label}] {snip}')
|
|
44
|
+
hits += 1
|
|
45
|
+
return hits
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _assert_drop_parquet(out_dir: str, check_csv: bool) -> int:
|
|
49
|
+
"""Schema match + stable_key population for one drop's `_data` dir. Returns error count.
|
|
50
|
+
|
|
51
|
+
`check_csv` asserts each Parquet has its `.csv` sidecar — true only for the full-ingest path;
|
|
52
|
+
the committed synthetic fixture is Parquet-only (ADR-8), so render-only skips it.
|
|
53
|
+
"""
|
|
54
|
+
errors = 0
|
|
55
|
+
for stem in schemas.TABLES:
|
|
56
|
+
pq = os.path.join(out_dir, f'{stem}.parquet')
|
|
57
|
+
if not os.path.exists(pq):
|
|
58
|
+
continue
|
|
59
|
+
if check_csv and not os.path.exists(os.path.join(out_dir, f'{stem}.csv')):
|
|
60
|
+
print(f' {stem}: .csv missing alongside .parquet')
|
|
61
|
+
errors += 1
|
|
62
|
+
cols = list(papq.read_schema(pq).names)
|
|
63
|
+
expected = list(schemas.expected_columns(stem))
|
|
64
|
+
if cols != expected:
|
|
65
|
+
print(f' {stem}: cols={cols} != expected={expected}')
|
|
66
|
+
errors += 1
|
|
67
|
+
if schemas.is_entity_table(stem):
|
|
68
|
+
t = papq.read_table(pq, columns=['stable_key'])
|
|
69
|
+
if t.num_rows and not any(t.column('stable_key').to_pylist()):
|
|
70
|
+
print(f' {stem}: stable_key all empty across {t.num_rows} rows')
|
|
71
|
+
errors += 1
|
|
72
|
+
return errors
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# --- render-only (default) ---------------------------------------------------
|
|
76
|
+
|
|
77
|
+
def _render_only_smoke() -> int:
|
|
78
|
+
from . import _render_util as u
|
|
79
|
+
|
|
80
|
+
with tempfile.TemporaryDirectory(prefix='bobframes-smoke-') as td:
|
|
81
|
+
dest = os.path.join(td, 'root')
|
|
82
|
+
print(f'1. render-only against bundled synthetic -> {dest}')
|
|
83
|
+
try:
|
|
84
|
+
u.render_fresh(dest)
|
|
85
|
+
except RuntimeError as e:
|
|
86
|
+
return _fail(str(e))
|
|
87
|
+
|
|
88
|
+
print('2. html emitted')
|
|
89
|
+
html = u.rendered_html_files(dest)
|
|
90
|
+
if not os.path.exists(paths.root_index_html(dest)):
|
|
91
|
+
return _fail('root index.html missing')
|
|
92
|
+
drills = [r for r in html if r.endswith('/index.html') and 'drill' in r]
|
|
93
|
+
reports = [r for r in html if r.startswith('_reports/') and 'drill' not in r]
|
|
94
|
+
if not drills:
|
|
95
|
+
return _fail('no per-drop drill index.html emitted')
|
|
96
|
+
if not reports:
|
|
97
|
+
return _fail('no _reports/*.html emitted')
|
|
98
|
+
|
|
99
|
+
print('3. schema + stable_key (synthetic _data)')
|
|
100
|
+
errors = 0
|
|
101
|
+
for drop in discovery.find_drops(dest):
|
|
102
|
+
out_dir = paths.drop_data_dir(dest, drop.area, os.path.basename(drop.drop_dir))
|
|
103
|
+
errors += _assert_drop_parquet(out_dir, check_csv=False)
|
|
104
|
+
if errors:
|
|
105
|
+
return _fail(f'{errors} parquet schema/stable_key error(s)')
|
|
106
|
+
|
|
107
|
+
print('4. catalog')
|
|
108
|
+
if not os.path.exists(paths.catalog_parquet(dest)):
|
|
109
|
+
return _fail('catalog parquet missing')
|
|
110
|
+
|
|
111
|
+
print('5. lint')
|
|
112
|
+
if _lint_html(dest):
|
|
113
|
+
return _fail('lint hits in rendered HTML')
|
|
114
|
+
|
|
115
|
+
print(f'OK: render-only smoke - {len(drills)} drop(s), {len(html)} HTML pages, lint clean')
|
|
116
|
+
return 0
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# --- full ingest (--data) ----------------------------------------------------
|
|
120
|
+
|
|
121
|
+
def _full_smoke(data_dir: str, pixel_grid: int) -> int:
|
|
122
|
+
data_dir = os.path.abspath(data_dir)
|
|
123
|
+
drops = discovery.find_drops(data_dir)
|
|
124
|
+
if not drops:
|
|
125
|
+
return _fail(f'no drops with .rdc found under {data_dir}')
|
|
126
|
+
print(f'1. discovered {len(drops)} drop(s): ' +
|
|
127
|
+
', '.join(f'{d.area}/{os.path.basename(d.drop_dir)}' for d in drops))
|
|
128
|
+
|
|
129
|
+
print('2. ingest (full pipeline, --force)')
|
|
130
|
+
rc = subprocess.run(
|
|
131
|
+
[sys.executable, '-m', 'bobframes.run', '--root', data_dir, '--force',
|
|
132
|
+
'--pixel-grid', str(pixel_grid)],
|
|
133
|
+
).returncode
|
|
134
|
+
if rc != 0:
|
|
135
|
+
return _fail(f'pipeline exited rc={rc}')
|
|
136
|
+
|
|
137
|
+
print('3. per-drop outputs')
|
|
138
|
+
errors = 0
|
|
139
|
+
for drop in drops:
|
|
140
|
+
dated = os.path.basename(drop.drop_dir)
|
|
141
|
+
out_dir = paths.drop_data_dir(data_dir, drop.area, dated)
|
|
142
|
+
tmp_dir = paths.drop_data_dir_tmp(data_dir, drop.area, dated)
|
|
143
|
+
if not os.path.isdir(out_dir):
|
|
144
|
+
errors += 1
|
|
145
|
+
print(f' {out_dir} missing')
|
|
146
|
+
continue
|
|
147
|
+
if os.path.isdir(tmp_dir):
|
|
148
|
+
errors += 1
|
|
149
|
+
print(f' {tmp_dir} should be gone after atomic commit')
|
|
150
|
+
errors += _assert_drop_parquet(out_dir, check_csv=True)
|
|
151
|
+
|
|
152
|
+
mf = os.path.join(out_dir, '_manifest.json')
|
|
153
|
+
with open(mf, encoding='utf-8') as f:
|
|
154
|
+
m = json.load(f)
|
|
155
|
+
if m.get('schema_version') != schemas.SCHEMA_VERSION:
|
|
156
|
+
errors += 1
|
|
157
|
+
print(f' manifest schema_version={m.get("schema_version")} != {schemas.SCHEMA_VERSION}')
|
|
158
|
+
if not all(s == 'ok' for s in m.get('capture_status', {}).values()):
|
|
159
|
+
errors += 1
|
|
160
|
+
print(f' capture_status not all ok: {m.get("capture_status")}')
|
|
161
|
+
|
|
162
|
+
drill = paths.drop_drill_dir(data_dir, drop.area, dated)
|
|
163
|
+
if not os.path.exists(os.path.join(drill, 'index.html')):
|
|
164
|
+
errors += 1
|
|
165
|
+
print(f' drill index.html missing at {drill}')
|
|
166
|
+
if errors:
|
|
167
|
+
return _fail(f'{errors} ingest output error(s)')
|
|
168
|
+
|
|
169
|
+
print('4. catalog + root index')
|
|
170
|
+
if not os.path.exists(paths.catalog_parquet(data_dir)):
|
|
171
|
+
return _fail('catalog parquet missing')
|
|
172
|
+
if not os.path.exists(paths.root_index_html(data_dir)):
|
|
173
|
+
return _fail('root index.html missing')
|
|
174
|
+
|
|
175
|
+
print('5. lint')
|
|
176
|
+
if _lint_html(data_dir):
|
|
177
|
+
return _fail('lint hits in rendered HTML')
|
|
178
|
+
|
|
179
|
+
print(f'OK: full smoke - {len(drops)} drop(s) ingested + rendered, lint clean')
|
|
180
|
+
return 0
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def main(data: str | None = None, pixel_grid: int = 4) -> int:
|
|
184
|
+
if data:
|
|
185
|
+
return _full_smoke(data, pixel_grid)
|
|
186
|
+
return _render_only_smoke()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _parse(argv: list[str]) -> argparse.Namespace:
|
|
190
|
+
ap = argparse.ArgumentParser(prog='bobframes.tests.smoke',
|
|
191
|
+
description='render-only (default) or full ingest (--data) smoke')
|
|
192
|
+
ap.add_argument('--data', help='capture root for full ingest (default: bundled synthetic)')
|
|
193
|
+
ap.add_argument('--pixel-grid', type=int, default=4)
|
|
194
|
+
return ap.parse_args(argv)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
if __name__ == '__main__':
|
|
198
|
+
a = _parse(sys.argv[1:])
|
|
199
|
+
sys.exit(main(data=a.data, pixel_grid=a.pixel_grid))
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Determinism: rendering the same data twice produces byte-identical HTML (after masking the
|
|
2
|
+
build timestamp). Catches dict-ordering, set-iteration, and other nondeterminism regressions."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from . import _render_util as u
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_render_is_deterministic(tmp_path):
|
|
11
|
+
a = u.render_fresh(str(tmp_path / "a"))
|
|
12
|
+
b = u.render_fresh(str(tmp_path / "b"))
|
|
13
|
+
|
|
14
|
+
fa, fb = u.rendered_html_files(a), u.rendered_html_files(b)
|
|
15
|
+
assert fa == fb, f"page set differs between runs: {set(fa) ^ set(fb)}"
|
|
16
|
+
for rel in fa:
|
|
17
|
+
ca = u.normalize(open(os.path.join(a, rel), encoding="utf-8").read())
|
|
18
|
+
cb = u.normalize(open(os.path.join(b, rel), encoding="utf-8").read())
|
|
19
|
+
assert ca == cb, f"nondeterministic output: {rel}"
|