diff --git a/docs-astro/src/data/commands.json b/docs-astro/src/data/commands.json index 3908c931..6bad518b 100644 --- a/docs-astro/src/data/commands.json +++ b/docs-astro/src/data/commands.json @@ -58,7 +58,7 @@ "name": "stats", "id": "stats", "help": "Show per-pass breakdown, top draws, largest resources.", - "usage": "rdc stats [--json] [--no-header] [--jsonl] [-q]" + "usage": "rdc stats [--no-header] [--json] [--jsonl] [-q]" }, { "name": "events", @@ -88,13 +88,13 @@ "name": "log", "id": "log", "help": "Show debug/validation messages from the capture.", - "usage": "rdc log [--level CHOICE] [--eid INTEGER] [--json] [--no-header] [--jsonl] [-q]" + "usage": "rdc log [--level CHOICE] [--eid INTEGER] [--no-header] [--json] [--jsonl] [-q]" }, { "name": "counters", "id": "counters", "help": "Query GPU performance counters.", - "usage": "rdc counters [--list] [--eid INTEGER] [--name TEXT] [--json] [--no-header] [--jsonl] [-q]" + "usage": "rdc counters [--list] [--eid INTEGER] [--name TEXT] [--no-header] [--json] [--jsonl] [-q]" } ] }, @@ -113,7 +113,7 @@ "name": "bindings", "id": "bindings", "help": "Show bound resources per shader stage.", - "usage": "rdc bindings [EID] [--binding INTEGER] [--set INTEGER] [--json] [--no-header] [--jsonl] [-q]" + "usage": "rdc bindings [EID] [--binding INTEGER] [--set INTEGER] [--no-header] [--json] [--jsonl] [-q]" } ] }, @@ -126,7 +126,7 @@ "name": "resources", "id": "resources", "help": "List all resources.", - "usage": "rdc resources [--json] [--type TEXT] [--name TEXT] [--sort CHOICE] [--no-header] [--jsonl] [-q]" + "usage": "rdc resources [--type TEXT] [--name TEXT] [--sort CHOICE] [--no-header] [--json] [--jsonl] [-q]" }, { "name": "resource", @@ -138,7 +138,7 @@ "name": "usage", "id": "usage", "help": "Show resource usage (which events read/write a resource).", - "usage": "rdc usage [RESOURCE-ID] [--all] [--type TEXT] [--usage TEXT] [--json] [--no-header] [--jsonl] [-q]" + "usage": "rdc usage [RESOURCE-ID] [--all] [--type TEXT] [--usage TEXT] [--no-header] [--json] [--jsonl] [-q]" }, { "name": "shader", @@ -150,7 +150,7 @@ "name": "shaders", "id": "shaders", "help": "List unique shaders in capture.", - "usage": "rdc shaders [--stage CHOICE] [--sort CHOICE] [--json] [--no-header] [--jsonl] [-q]" + "usage": "rdc shaders [--stage CHOICE] [--sort CHOICE] [--no-header] [--json] [--jsonl] [-q]" }, { "name": "shader-map", @@ -168,7 +168,7 @@ "name": "passes", "id": "passes", "help": "List render passes.", - "usage": "rdc passes [--json] [--deps] [--dot] [--graph] [--table] [--no-header] [--jsonl] [-q]" + "usage": "rdc passes [--deps] [--dot] [--graph] [--table] [--no-header] [--json] [--jsonl] [-q]" }, { "name": "pass", @@ -236,7 +236,7 @@ "name": "pixel", "id": "pixel", "help": "Query pixel history at (X, Y) for the current or specified event.", - "usage": "rdc pixel [EID] [--target INTEGER] [--sample INTEGER] [--json] [--no-header] [--jsonl] [-q]" + "usage": "rdc pixel [EID] [--target INTEGER] [--sample INTEGER] [--no-header] [--json] [--jsonl] [-q]" }, { "name": "pick-pixel", diff --git a/docs-astro/src/data/replay.json b/docs-astro/src/data/replay.json index 23772bc1..e27fdc64 100644 --- a/docs-astro/src/data/replay.json +++ b/docs-astro/src/data/replay.json @@ -6,19 +6,19 @@ }, { "cmd": "rdc info", - "output": "capture: hello_triangle.rdc\napi: Vulkan\nevent_count: 14\ndriver: NVIDIA 565.57.01" + "output": "Capture: hello_triangle.rdc\nAPI: Vulkan\nEvents: 6\nDraw Calls: 1 (0 indexed, 1 non-indexed, 0 dispatches)\nClears: 0\nCopies: 0" }, { "cmd": "rdc draws", - "output": "EID\tMARKER\tTRIANGLES\n11\tDrawIndexed\t1" + "output": "EID\tTYPE\tTRIANGLES\tINSTANCES\tPASS\tMARKER\n11\tDraw\t12\t1\tColour Pass #1 (1 Target + Depth)\t-" }, { "cmd": "rdc pipeline 11", - "output": "type: Graphics\ngfx_pipe: 51\ntopology: TriangleList\nvs_shader: 111\nps_shader: 112" + "output": "EID\tAPI\tTOPOLOGY\tGFX_PIPE\tCOMP_PIPE\n11\tVulkan\tTriangleList\t114\t0" }, { "cmd": "rdc search \"main\"", - "output": "SHADER\tSTAGE\tLINE\tCONTENT\n111\tvs\t3\tvoid main() {\n112\tps\t4\tvoid main() {" + "output": "shader:111[vs]:12:EntryPoint(Vertex, main, \"main\", {\nshader:111[vs]:43:void main() {\nshader:112[ps]:12:EntryPoint(Fragment, main, \"main\", {\nshader:112[ps]:18:ExecutionMode(main, OriginUpperLeft);\nshader:112[ps]:32:void main() {" }, { "cmd": "rdc close", @@ -32,23 +32,23 @@ }, { "cmd": "rdc tree / --depth 1", - "output": "/\n draws/\n passes/\n resources/\n shaders/\n info\n log\n stats" + "output": "//\n|-- capabilities\n|-- info\n|-- stats\n|-- log\n|-- events/\n|-- draws/\n|-- passes/\n|-- resources/\n|-- textures/\n|-- buffers/\n|-- shaders/\n|-- counters/\n\\-- current@" }, { "cmd": "rdc ls /draws/11", - "output": "NAME\tKIND\npipeline/\tdir\nshader/\tdir\ncbuffer/\tdir\nvbuffer\tleaf\nibuffer\tleaf\ndescriptors\tleaf" + "output": "pipeline\nshader\nbindings\ntargets\npostvs\ncbuffer\nvbuffer\nibuffer\ndescriptors\npixel" }, { "cmd": "rdc cat /draws/11/pipeline/topology", - "output": "TriangleList" + "output": "eid: 11\ntopology: TriangleList" }, { "cmd": "rdc resources -q | wc -l", - "output": "6" + "output": "46" }, { "cmd": "rdc draws --json | jq \".[0].triangles\"", - "output": "1" + "output": "12" }, { "cmd": "rdc close", @@ -62,19 +62,19 @@ }, { "cmd": "rdc pick-pixel 300 300 11", - "output": "r=0.0000 g=0.5000 b=0.0000 a=1.0000" + "output": "r=0.3373 g=0.3373 b=0.3373 a=0.5216" }, { "cmd": "rdc pixel 300 300 11", - "output": "EID\tTEST\tR\tG\tB\tA\n11\tPassed\t0.0000\t0.5000\t0.0000\t1.0000" + "output": "EID\tFRAG\tDEPTH\tPASSED\tFLAGS\n6\t0\t-\tyes\t-\n11\t0\t0.9581\tyes\t-" }, { "cmd": "rdc debug pixel 11 300 300", - "output": "result: r=0.0000 g=0.5000 b=0.0000 a=1.0000\nshader: 112 (ps)" + "output": "stage: ps\neid: 11\nsteps: 80\ninputs: _99 = [-107374176.0 -107374176.0 -107374176.0 -107374176.0]\noutputs: = [0.0]" }, { "cmd": "rdc debug pixel 11 300 300 --trace", - "output": "STEP\tLINE\tINST\tVAR\tVALUE\n0\t4\t0\tcolor\t(0.0, 1.0, 0.0, 1.0)\n1\t5\t1\toutColor\t(0.0, 0.5, 0.0, 1.0)" + "output": "STEP\tINSTR\tFILE\tLINE\tVAR\tTYPE\tVALUE\n0\t78\t-\t0\t_99\tfloat\t-107374176.0 -107374176.0 -107374176.0 -107374176.0\n1\t79\t-\t0\t_79\tfloat\t-0.015102430246770382 -0.3927634656429291 4.36358118057251\n2\t80\t-\t0\t_80\tfloat\t0.01510667521506548 -8.669495582580566e-05 0.0010061264038085938\n3\t81\t-\t0\t\tfloat\t0.0\n3\t81\t-\t0\t_76\tfloat\t0.01510667521506548 -8.669495582580566e-05 0.0010061264038085938\n4\t82\t-\t0\t\tfloat\t0.0\n4\t82\t-\t0\t_82\tfloat\t-0.015102430246770382 -0.3927634656429291 4.36358118057251\n... (138 more)" }, { "cmd": "rdc close", @@ -88,15 +88,15 @@ }, { "cmd": "rdc draws --json", - "output": "[{\"eid\": 11, \"marker\": \"DrawIndexed\", \"triangles\": 1}]" + "output": "[\n {\n \"eid\": 11,\n \"type\": \"Draw\",\n \"triangles\": 12,\n \"instances\": 1,\n \"pass\": \"Colour Pass #1 (1 Target + Depth)\",\n \"marker\": \"-\"\n }\n]" }, { "cmd": "rdc assert-count draws --expect 1 --op eq", - "output": "PASS: draws == 1" + "output": "pass: draws = 1 (expected eq 1)" }, { "cmd": "rdc assert-count events --expect 5 --op ge", - "output": "PASS: events >= 5 (actual: 14)" + "output": "pass: events = 6 (expected ge 5)" }, { "cmd": "rdc close", @@ -106,11 +106,11 @@ [ { "cmd": "rdc diff before.rdc after.rdc --draws", - "output": "--- before.rdc draws\n+++ after.rdc draws\n DrawIndexed(11): 1 tri\n+ DrawIndexed(14): 256 tri" + "output": "STATUS\tEID_A\tEID_B\tMARKER\tTYPE\tTRI_A\tTRI_B\tINST_A\tINST_B\tCONFIDENCE\n=\t11\t11\t-\tDraw\t12\t12\t1\t1\tmedium" }, { "cmd": "rdc diff before.rdc after.rdc --stats", - "output": "METRIC\tBEFORE\tAFTER\tDELTA\ndraws\t1\t2\t+1\ntriangles\t1\t257\t+256\nresources\t6\t8\t+2" + "output": "STATUS\tPASS\tDRAWS_A\tDRAWS_B\tDRAWS_DELTA\tTRI_A\tTRI_B\tTRI_DELTA\tDISP_A\tDISP_B\tDISP_DELTA\n=\tvkCmdBeginRenderPass(C=Clear, D=Clear)\t1\t1\t0\t12\t12\t0\t0\t0\t0" } ] ] diff --git a/src/rdc/_skills/references/commands-quick-ref.md b/src/rdc/_skills/references/commands-quick-ref.md index 463c0d88..68df33f9 100644 --- a/src/rdc/_skills/references/commands-quick-ref.md +++ b/src/rdc/_skills/references/commands-quick-ref.md @@ -162,8 +162,8 @@ Show bound resources per shader stage. |------|------|------|---------| | `--binding` | Filter by binding index. | integer | | | `--set` | Filter by descriptor set index. | integer | | -| `--json` | Output JSON. | flag | | | `--no-header` | Omit TSV header | flag | | +| `--json` | JSON output | flag | | | `--jsonl` | JSONL output | flag | | | `-q, --quiet` | Print primary key column only | flag | | @@ -350,8 +350,8 @@ Query GPU performance counters. | `--list` | List available counters. | flag | | | `--eid` | Filter to specific event ID. | integer | | | `--name` | Filter counters by name substring. | text | | -| `--json` | JSON output. | flag | | | `--no-header` | Omit TSV header | flag | | +| `--json` | JSON output | flag | | | `--jsonl` | JSONL output | flag | | | `-q, --quiet` | Print primary key column only | flag | | @@ -490,7 +490,7 @@ List draw calls. | `--no-header` | Omit TSV header | flag | | | `--json` | JSON output | flag | | | `--jsonl` | JSONL output | flag | | -| `-q, --quiet` | Only EID column | flag | | +| `-q, --quiet` | Print primary key column only | flag | | ## `rdc event` @@ -523,7 +523,7 @@ List all events. | `--no-header` | Omit TSV header | flag | | | `--json` | JSON output | flag | | | `--jsonl` | JSONL output | flag | | -| `-q, --quiet` | Only EID column | flag | | +| `-q, --quiet` | Print primary key column only | flag | | ## `rdc goto` @@ -576,8 +576,8 @@ Show debug/validation messages from the capture. |------|------|------|---------| | `--level` | Filter by severity. | choice | | | `--eid` | Filter by event ID. | integer | | -| `--json` | JSON output | flag | | | `--no-header` | Omit TSV header | flag | | +| `--json` | JSON output | flag | | | `--jsonl` | JSONL output | flag | | | `-q, --quiet` | Print primary key column only | flag | | @@ -670,12 +670,12 @@ List render passes. | Flag | Help | Type | Default | |------|------|------|---------| -| `--json` | Output JSON. | flag | | | `--deps` | Show pass dependency DAG. | flag | | | `--dot` | Graphviz DOT output (requires --deps). | flag | | | `--graph` | Human-readable graph (requires --deps). | flag | | | `--table` | Per-pass I/O table (requires --deps). | flag | | | `--no-header` | Omit TSV header | flag | | +| `--json` | JSON output | flag | | | `--jsonl` | JSONL output | flag | | | `-q, --quiet` | Print primary key column only | flag | | @@ -733,8 +733,8 @@ Query pixel history at (X, Y) for the current or specified event. |------|------|------|---------| | `--target` | Color target index (default 0) | integer | 0 | | `--sample` | MSAA sample index (default 0) | integer | 0 | -| `--json` | JSON output | flag | | | `--no-header` | Omit TSV header | flag | | +| `--json` | JSON output | flag | | | `--jsonl` | JSONL output | flag | | | `-q, --quiet` | Print primary key column only | flag | | @@ -854,11 +854,11 @@ List all resources. | Flag | Help | Type | Default | |------|------|------|---------| -| `--json` | Output JSON. | flag | | | `--type` | Filter by resource type (exact, case-insensitive). | text | | | `--name` | Filter by name substring (case-insensitive). | text | | | `--sort` | Sort order. | choice | id | | `--no-header` | Omit TSV header | flag | | +| `--json` | JSON output | flag | | | `--jsonl` | JSONL output | flag | | | `-q, --quiet` | Print primary key column only | flag | | @@ -1045,10 +1045,10 @@ Output EID-to-shader mapping as TSV. | Flag | Help | Type | Default | |------|------|------|---------| -| `--no-header` | Omit TSV header row. | flag | | -| `--json` | JSON output. | flag | | -| `--jsonl` | JSONL output. | flag | | -| `-q, --quiet` | Print EID column only. | flag | | +| `--no-header` | Omit TSV header | flag | | +| `--json` | JSON output | flag | | +| `--jsonl` | JSONL output | flag | | +| `-q, --quiet` | Print primary key column only | flag | | ## `rdc shader-replace` @@ -1105,8 +1105,8 @@ List unique shaders in capture. |------|------|------|---------| | `--stage` | Filter by shader stage. | choice | | | `--sort` | Sort order. | choice | name | -| `--json` | Output JSON. | flag | | | `--no-header` | Omit TSV header | flag | | +| `--json` | JSON output | flag | | | `--jsonl` | JSONL output | flag | | | `-q, --quiet` | Print primary key column only | flag | | @@ -1135,8 +1135,8 @@ Show per-pass breakdown, top draws, largest resources. | Flag | Help | Type | Default | |------|------|------|---------| -| `--json` | JSON output | flag | | | `--no-header` | Omit TSV header | flag | | +| `--json` | JSON output | flag | | | `--jsonl` | JSONL output | flag | | | `-q, --quiet` | Print primary key column only | flag | | @@ -1240,7 +1240,7 @@ Show resource usage (which events read/write a resource). | `--all` | Show all resources usage matrix. | flag | | | `--type` | Filter by resource type. | text | | | `--usage` | Filter by usage type. | text | | -| `--json` | JSON output. | flag | | | `--no-header` | Omit TSV header | flag | | +| `--json` | JSON output | flag | | | `--jsonl` | JSONL output | flag | | | `-q, --quiet` | Print primary key column only | flag | | diff --git a/src/rdc/commands/counters.py b/src/rdc/commands/counters.py index bc76d1f4..57effd85 100644 --- a/src/rdc/commands/counters.py +++ b/src/rdc/commands/counters.py @@ -2,14 +2,13 @@ from __future__ import annotations -import sys from typing import Any import click from rdc.commands._helpers import call, complete_eid -from rdc.formatters.json_fmt import write_json, write_jsonl -from rdc.formatters.options import list_output_options +from rdc.formatters.json_fmt import write_json +from rdc.formatters.options import list_output_options, render_list from rdc.formatters.tsv import write_tsv @@ -23,7 +22,6 @@ help="Filter to specific event ID.", ) @click.option("--name", "name_filter", default=None, help="Filter counters by name substring.") -@click.option("--json", "use_json", is_flag=True, help="JSON output.") @list_output_options def counters_cmd( show_list: bool, @@ -45,15 +43,20 @@ def counters_cmd( write_json(result) return counters = result.get("counters", []) - if use_jsonl: - write_jsonl(counters) - elif quiet: - for c in counters: - sys.stdout.write(str(c["id"]) + "\n") - else: + + def _list_table() -> None: tsv_rows = [[c["id"], c["name"], c["unit"], c["type"], c["category"]] for c in counters] hdr = ["ID", "NAME", "UNIT", "TYPE", "CATEGORY"] write_tsv(tsv_rows, header=hdr, no_header=no_header) + + render_list( + counters, + use_json=False, + use_jsonl=use_jsonl, + quiet=quiet, + quiet_key="id", + table=_list_table, + ) return params: dict[str, Any] = {} @@ -66,11 +69,16 @@ def counters_cmd( write_json(result) return rows = result.get("rows", []) - if use_jsonl: - write_jsonl(rows) - elif quiet: - for r in rows: - sys.stdout.write(str(r["eid"]) + "\n") - else: + + def _fetch_table() -> None: tsv_rows = [[r["eid"], r["counter"], r["value"], r["unit"]] for r in rows] write_tsv(tsv_rows, header=["EID", "COUNTER", "VALUE", "UNIT"], no_header=no_header) + + render_list( + rows, + use_json=False, + use_jsonl=use_jsonl, + quiet=quiet, + quiet_key="eid", + table=_fetch_table, + ) diff --git a/src/rdc/commands/events.py b/src/rdc/commands/events.py index 6c913256..d5fa04f1 100644 --- a/src/rdc/commands/events.py +++ b/src/rdc/commands/events.py @@ -2,14 +2,14 @@ from __future__ import annotations -import sys from typing import Any import click from rdc.commands._helpers import call, complete_eid, complete_pass_name -from rdc.formatters.json_fmt import write_json, write_jsonl +from rdc.formatters.json_fmt import write_json from rdc.formatters.kv import write_kv +from rdc.formatters.options import list_output_options, render_list from rdc.formatters.tsv import write_footer, write_tsv @@ -18,10 +18,7 @@ @click.option("--filter", "pattern", default=None, help="Filter by name glob") @click.option("--limit", type=int, default=None, help="Max rows") @click.option("--range", "eid_range", default=None, help="EID range N:M") -@click.option("--no-header", is_flag=True, help="Omit TSV header") -@click.option("--json", "use_json", is_flag=True, help="JSON output") -@click.option("--jsonl", "use_jsonl", is_flag=True, help="JSONL output") -@click.option("-q", "--quiet", is_flag=True, help="Only EID column") +@list_output_options def events_cmd( event_type: str | None, pattern: str | None, @@ -44,19 +41,19 @@ def events_cmd( params["range"] = eid_range result = call("events", params) rows_data = result.get("events", []) - if use_json: - write_json(rows_data) - return - if use_jsonl: - write_jsonl(rows_data) - return - if quiet: - for r in rows_data: - sys.stdout.write(str(r["eid"]) + "\n") - return - header = ["EID", "TYPE", "NAME"] - rows = [[r["eid"], r["type"], r["name"]] for r in rows_data] - write_tsv(rows, header=header, no_header=no_header) + + def _table() -> None: + rows = [[r["eid"], r["type"], r["name"]] for r in rows_data] + write_tsv(rows, header=["EID", "TYPE", "NAME"], no_header=no_header) + + render_list( + rows_data, + use_json=use_json, + use_jsonl=use_jsonl, + quiet=quiet, + quiet_key="eid", + table=_table, + ) @click.command("draws") @@ -69,10 +66,7 @@ def events_cmd( ) @click.option("--sort", "sort_field", default=None, help="Sort field") @click.option("--limit", type=int, default=None, help="Max rows") -@click.option("--no-header", is_flag=True, help="Omit TSV header") -@click.option("--json", "use_json", is_flag=True, help="JSON output") -@click.option("--jsonl", "use_jsonl", is_flag=True, help="JSONL output") -@click.option("-q", "--quiet", is_flag=True, help="Only EID column") +@list_output_options def draws_cmd( pass_name: str | None, sort_field: str | None, @@ -93,31 +87,32 @@ def draws_cmd( result = call("draws", params) rows_data = result.get("draws", []) summary = result.get("summary", "") - if use_json: - write_json(rows_data) - return - if use_jsonl: - write_jsonl(rows_data) - return - if quiet: - for r in rows_data: - sys.stdout.write(str(r["eid"]) + "\n") - return - header = ["EID", "TYPE", "TRIANGLES", "INSTANCES", "PASS", "MARKER"] - rows = [ - [ - r["eid"], - r["type"], - r["triangles"], - r["instances"], - r.get("pass", "-"), - r.get("marker", "-"), + + def _table() -> None: + header = ["EID", "TYPE", "TRIANGLES", "INSTANCES", "PASS", "MARKER"] + rows = [ + [ + r["eid"], + r["type"], + r["triangles"], + r["instances"], + r.get("pass", "-"), + r.get("marker", "-"), + ] + for r in rows_data ] - for r in rows_data - ] - write_tsv(rows, header=header, no_header=no_header) - if summary: - write_footer(summary) + write_tsv(rows, header=header, no_header=no_header) + if summary: + write_footer(summary) + + render_list( + rows_data, + use_json=use_json, + use_jsonl=use_jsonl, + quiet=quiet, + quiet_key="eid", + table=_table, + ) @click.command("event") diff --git a/src/rdc/commands/info.py b/src/rdc/commands/info.py index 78b445d0..d1f7ba2b 100644 --- a/src/rdc/commands/info.py +++ b/src/rdc/commands/info.py @@ -10,7 +10,7 @@ from rdc.commands._helpers import call, complete_eid from rdc.formatters.json_fmt import write_json, write_jsonl from rdc.formatters.kv import write_kv -from rdc.formatters.options import list_output_options +from rdc.formatters.options import list_output_options, render_list from rdc.formatters.tsv import write_tsv @@ -26,7 +26,6 @@ def info_cmd(use_json: bool) -> None: @click.command("stats") -@click.option("--json", "use_json", is_flag=True, help="JSON output") @list_output_options def stats_cmd(use_json: bool, no_header: bool, use_jsonl: bool, quiet: bool) -> None: """Show per-pass breakdown, top draws, largest resources.""" @@ -101,7 +100,6 @@ def stats_cmd(use_json: bool, no_header: bool, use_jsonl: bool, quiet: bool) -> shell_complete=complete_eid, help="Filter by event ID.", ) -@click.option("--json", "use_json", is_flag=True, help="JSON output") @list_output_options def log_cmd( level: str | None, @@ -119,24 +117,23 @@ def log_cmd( rpc_params["eid"] = eid result = call("log", rpc_params) messages = result.get("messages", []) - if use_json: - write_json(messages) - return - - if use_jsonl: - write_jsonl(messages) - return - - if quiet: - for m in messages: - sys.stdout.write(str(m.get("eid", 0)) + "\n") - return - def _sanitize(text: str) -> str: - return text.replace("\t", " ").replace("\n", " ") + def _table() -> None: + def _sanitize(text: str) -> str: + return text.replace("\t", " ").replace("\n", " ") - rows = [ - [m.get("level", "-"), m.get("eid", 0), _sanitize(str(m.get("message", "-")))] - for m in messages - ] - write_tsv(rows, header=["LEVEL", "EID", "MESSAGE"], no_header=no_header) + rows = [ + [m.get("level", "-"), m.get("eid", 0), _sanitize(str(m.get("message", "-")))] + for m in messages + ] + write_tsv(rows, header=["LEVEL", "EID", "MESSAGE"], no_header=no_header) + + render_list( + messages, + use_json=use_json, + use_jsonl=use_jsonl, + quiet=quiet, + quiet_key="eid", + quiet_default=0, + table=_table, + ) diff --git a/src/rdc/commands/pipeline.py b/src/rdc/commands/pipeline.py index 1bb009ea..dd30df32 100644 --- a/src/rdc/commands/pipeline.py +++ b/src/rdc/commands/pipeline.py @@ -2,7 +2,6 @@ from __future__ import annotations -import sys from pathlib import Path from typing import Any @@ -10,8 +9,8 @@ from click.shell_completion import CompletionItem from rdc.commands._helpers import call, complete_eid -from rdc.formatters.json_fmt import write_json, write_jsonl -from rdc.formatters.options import list_output_options +from rdc.formatters.json_fmt import write_json +from rdc.formatters.options import list_output_options, render_list from rdc.formatters.tsv import format_row, write_tsv from rdc.services.query_service import STAGE_MAP @@ -125,7 +124,6 @@ def pipeline_cmd(eid: int | None, section: str | None, use_json: bool) -> None: @click.argument("eid", required=False, type=int, shell_complete=complete_eid) @click.option("--binding", "binding_index", type=int, help="Filter by binding index.") @click.option("--set", "set_index", type=int, help="Filter by descriptor set index.") -@click.option("--json", "use_json", is_flag=True, default=False, help="Output JSON.") @list_output_options def bindings_cmd( eid: int | None, @@ -150,14 +148,8 @@ def bindings_cmd( result = call("bindings", params) rows: list[dict[str, Any]] = result.get("rows", []) - if use_json: - write_json(rows) - elif use_jsonl: - write_jsonl(rows) - elif quiet: - for r in rows: - sys.stdout.write(str(r.get("eid", "")) + "\n") - else: + + def _table() -> None: tsv_rows = [ [ r.get("eid", "-"), @@ -172,6 +164,15 @@ def bindings_cmd( hdr = ["EID", "STAGE", "KIND", "SET", "SLOT", "NAME"] write_tsv(tsv_rows, header=hdr, no_header=no_header) + render_list( + rows, + use_json=use_json, + use_jsonl=use_jsonl, + quiet=quiet, + quiet_key="eid", + table=_table, + ) + @click.command("shader") @click.argument( @@ -429,7 +430,6 @@ def _parse_shader_positionals( default="name", help="Sort order.", ) -@click.option("--json", "use_json", is_flag=True, default=False, help="Output JSON.") @list_output_options def shaders_cmd( stage_filter: str | None, @@ -451,13 +451,16 @@ def shaders_cmd( result = call("shaders", params) rows: list[dict[str, Any]] = result.get("rows", []) - if use_json: - write_json(rows) - elif use_jsonl: - write_jsonl(rows) - elif quiet: - for r in rows: - sys.stdout.write(str(r.get("shader", "")) + "\n") - else: + + def _table() -> None: tsv_rows = [[r.get("shader", "-"), r.get("stages", "-"), r.get("uses", 0)] for r in rows] write_tsv(tsv_rows, header=["SHADER", "STAGES", "USES"], no_header=no_header) + + render_list( + rows, + use_json=use_json, + use_jsonl=use_jsonl, + quiet=quiet, + quiet_key="shader", + table=_table, + ) diff --git a/src/rdc/commands/pixel.py b/src/rdc/commands/pixel.py index a53d267c..fd75f3fa 100644 --- a/src/rdc/commands/pixel.py +++ b/src/rdc/commands/pixel.py @@ -2,15 +2,14 @@ from __future__ import annotations -import sys from typing import Any import click from rdc.commands._helpers import call, complete_eid from rdc.commands.vfs import _fmt_pixel_mod -from rdc.formatters.json_fmt import write_json, write_jsonl -from rdc.formatters.options import list_output_options +from rdc.formatters.json_fmt import write_json +from rdc.formatters.options import list_output_options, render_list @click.command("pixel") @@ -19,7 +18,6 @@ @click.argument("eid", required=False, type=int, shell_complete=complete_eid) @click.option("--target", default=0, type=int, help="Color target index (default 0)") @click.option("--sample", default=0, type=int, help="MSAA sample index (default 0)") -@click.option("--json", "use_json", is_flag=True, help="JSON output") @list_output_options def pixel_cmd( x: int, @@ -45,17 +43,17 @@ def pixel_cmd( modifications = result.get("modifications", []) - if use_jsonl: - write_jsonl(modifications) - return - - if quiet: + def _table() -> None: + if not no_header: + click.echo("EID\tFRAG\tDEPTH\tPASSED\tFLAGS") for m in modifications: - sys.stdout.write(str(m["eid"]) + "\n") - return - - if not no_header: - click.echo("EID\tFRAG\tDEPTH\tPASSED\tFLAGS") - - for m in modifications: - click.echo(_fmt_pixel_mod(m)) + click.echo(_fmt_pixel_mod(m)) + + render_list( + modifications, + use_json=False, + use_jsonl=use_jsonl, + quiet=quiet, + quiet_key="eid", + table=_table, + ) diff --git a/src/rdc/commands/resources.py b/src/rdc/commands/resources.py index 94e96b9a..94abebbe 100644 --- a/src/rdc/commands/resources.py +++ b/src/rdc/commands/resources.py @@ -2,7 +2,6 @@ from __future__ import annotations -import sys from typing import Any import click @@ -14,9 +13,9 @@ complete_pass_identifier, completion_call, ) -from rdc.formatters.json_fmt import write_json, write_jsonl +from rdc.formatters.json_fmt import write_json from rdc.formatters.kv import format_kv -from rdc.formatters.options import list_output_options +from rdc.formatters.options import list_output_options, render_list from rdc.formatters.tsv import format_row, write_tsv @@ -90,7 +89,6 @@ def _complete_resource_id( @click.command("resources") -@click.option("--json", "use_json", is_flag=True, default=False, help="Output JSON.") @click.option( "--type", "type_filter", @@ -114,11 +112,11 @@ def _complete_resource_id( ) @list_output_options def resources_cmd( # noqa: PLR0913 - use_json: bool, type_filter: str | None, name_filter: str | None, sort: str, no_header: bool, + use_json: bool, use_jsonl: bool, quiet: bool, ) -> None: @@ -132,17 +130,20 @@ def resources_cmd( # noqa: PLR0913 params["sort"] = sort result = call("resources", params) rows: list[dict[str, Any]] = result.get("rows", []) - if use_json: - write_json(rows) - elif use_jsonl: - write_jsonl(rows) - elif quiet: - for r in rows: - sys.stdout.write(str(r.get("id", "")) + "\n") - else: + + def _table() -> None: tsv_rows = [[r.get("id", "-"), r.get("type", "-"), r.get("name", "-")] for r in rows] write_tsv(tsv_rows, header=["ID", "TYPE", "NAME"], no_header=no_header) + render_list( + rows, + use_json=use_json, + use_jsonl=use_jsonl, + quiet=quiet, + quiet_key="id", + table=_table, + ) + @click.command("resource") @click.argument("resid", type=int, shell_complete=_complete_resource_id) @@ -161,7 +162,6 @@ def resource_cmd(resid: int, use_json: bool) -> None: @click.command("passes") -@click.option("--json", "use_json", is_flag=True, default=False, help="Output JSON.") @click.option("--deps", is_flag=True, default=False, help="Show pass dependency DAG.") @click.option("--dot", is_flag=True, default=False, help="Graphviz DOT output (requires --deps).") @click.option( @@ -204,12 +204,8 @@ def passes_cmd( # noqa: PLR0913 return passes = tree.get("passes", []) - if use_jsonl: - write_jsonl(passes) - elif quiet: - for p in passes: - sys.stdout.write(str(p.get("name", "")) + "\n") - else: + + def _table() -> None: header = ["NAME", "DRAWS", "DISPATCHES", "TRIANGLES", "BEGIN_EID", "END_EID"] tsv_rows = [ [ @@ -224,6 +220,15 @@ def passes_cmd( # noqa: PLR0913 ] write_tsv(tsv_rows, header=header, no_header=no_header) + render_list( + passes, + use_json=False, + use_jsonl=use_jsonl, + quiet=quiet, + quiet_key="name", + table=_table, + ) + def _passes_deps(use_json: bool, dot: bool, graph: bool, table: bool) -> None: result = call("pass_deps", {}) diff --git a/src/rdc/commands/unix_helpers.py b/src/rdc/commands/unix_helpers.py index 8ca2a7ca..c88b033f 100644 --- a/src/rdc/commands/unix_helpers.py +++ b/src/rdc/commands/unix_helpers.py @@ -2,13 +2,12 @@ from __future__ import annotations -import sys from typing import Any import click from rdc.commands._helpers import call, complete_pass_name -from rdc.formatters.json_fmt import write_json, write_jsonl +from rdc.formatters.options import list_output_options, render_list from rdc.formatters.tsv import write_tsv _COUNT_TARGETS = [ @@ -47,20 +46,20 @@ def count_cmd(what: str, pass_name: str | None) -> None: @click.command("shader-map") -@click.option("--no-header", is_flag=True, default=False, help="Omit TSV header row.") -@click.option("--json", "use_json", is_flag=True, help="JSON output.") -@click.option("--jsonl", "use_jsonl", is_flag=True, help="JSONL output.") -@click.option("-q", "--quiet", is_flag=True, help="Print EID column only.") +@list_output_options def shader_map_cmd(no_header: bool, use_json: bool, use_jsonl: bool, quiet: bool) -> None: """Output EID-to-shader mapping as TSV.""" rows: list[dict[str, Any]] = call("shader_map", {})["rows"] - if use_json: - write_json(rows) - elif use_jsonl: - write_jsonl(rows) - elif quiet: - for row in rows: - sys.stdout.write(str(row["eid"]) + "\n") - else: + + def _table() -> None: tsv_rows = [[row[c] for c in _SHADER_MAP_COLS] for row in rows] write_tsv(tsv_rows, header=_SHADER_MAP_HEADER, no_header=no_header) + + render_list( + rows, + use_json=use_json, + use_jsonl=use_jsonl, + quiet=quiet, + quiet_key="eid", + table=_table, + ) diff --git a/src/rdc/commands/usage.py b/src/rdc/commands/usage.py index 49e14a84..d88ea32e 100644 --- a/src/rdc/commands/usage.py +++ b/src/rdc/commands/usage.py @@ -2,15 +2,14 @@ from __future__ import annotations -import sys from typing import Any import click from click.shell_completion import CompletionItem from rdc.commands._helpers import _sort_numeric_like, call, completion_call -from rdc.formatters.json_fmt import write_json, write_jsonl -from rdc.formatters.options import list_output_options +from rdc.formatters.json_fmt import write_json +from rdc.formatters.options import list_output_options, render_list from rdc.formatters.tsv import write_tsv @@ -94,7 +93,6 @@ def _complete_usage_kind( shell_complete=_complete_usage_kind, help="Filter by usage type.", ) -@click.option("--json", "use_json", is_flag=True, help="JSON output.") @list_output_options def usage_cmd( resource_id: int | None, @@ -122,14 +120,19 @@ def usage_cmd( write_json(result) return rows = result.get("rows", []) - if use_jsonl: - write_jsonl(rows) - elif quiet: - for r in rows: - sys.stdout.write(str(r["id"]) + "\n") - else: + + def _all_table() -> None: tsv_rows = [[r["id"], r["name"], r["eid"], r["usage"]] for r in rows] write_tsv(tsv_rows, header=["ID", "NAME", "EID", "USAGE"], no_header=no_header) + + render_list( + rows, + use_json=False, + use_jsonl=use_jsonl, + quiet=quiet, + quiet_key="id", + table=_all_table, + ) return if resource_id is None: @@ -141,11 +144,16 @@ def usage_cmd( write_json(result) return entries = result.get("entries", []) - if use_jsonl: - write_jsonl(entries) - elif quiet: - for entry in entries: - sys.stdout.write(str(entry["eid"]) + "\n") - else: + + def _entries_table() -> None: tsv_rows = [[entry["eid"], entry["usage"]] for entry in entries] write_tsv(tsv_rows, header=["EID", "USAGE"], no_header=no_header) + + render_list( + entries, + use_json=False, + use_jsonl=use_jsonl, + quiet=quiet, + quiet_key="eid", + table=_entries_table, + ) diff --git a/src/rdc/commands/vfs.py b/src/rdc/commands/vfs.py index 194397e6..9e2f74e6 100644 --- a/src/rdc/commands/vfs.py +++ b/src/rdc/commands/vfs.py @@ -16,8 +16,9 @@ from click.shell_completion import CompletionItem from rdc.commands._helpers import call, fetch_remote_file -from rdc.formatters.json_fmt import write_json, write_jsonl +from rdc.formatters.json_fmt import write_json from rdc.formatters.kv import format_kv +from rdc.formatters.options import render_list from rdc.session_state import load_session as _load_session from rdc.vfs.formatter import render_ls, render_ls_long, render_tree_root from rdc.vfs.router import resolve_path @@ -197,14 +198,19 @@ def ls_cmd( return if use_long and result.get("long"): - if use_jsonl: - write_jsonl(children) - elif quiet: - for child in children: - sys.stdout.write(str(child.get("name", "")) + "\n") - else: + + def _table() -> None: columns = result.get("columns", []) click.echo(render_ls_long(children, columns, no_header=no_header)) + + render_list( + children, + use_json=False, + use_jsonl=use_jsonl, + quiet=quiet, + quiet_key="name", + table=_table, + ) else: click.echo(render_ls(children, classify=classify)) diff --git a/src/rdc/formatters/options.py b/src/rdc/formatters/options.py index b1ea78c8..19b9deeb 100644 --- a/src/rdc/formatters/options.py +++ b/src/rdc/formatters/options.py @@ -1,18 +1,22 @@ -"""Shared output options decorator for list commands.""" +"""Shared output options and the list-output ladder for list commands.""" from __future__ import annotations import functools +import sys from collections.abc import Callable -from typing import Any +from typing import Any, TextIO import click +from rdc.formatters.json_fmt import write_json, write_jsonl + def list_output_options(fn: Callable[..., Any]) -> Callable[..., Any]: - """Attach --no-header, --jsonl, -q to a Click command.""" + """Attach --no-header, --json, --jsonl, -q to a Click command.""" @click.option("--no-header", is_flag=True, help="Omit TSV header") + @click.option("--json", "use_json", is_flag=True, help="JSON output") @click.option("--jsonl", "use_jsonl", is_flag=True, help="JSONL output") @click.option("-q", "--quiet", is_flag=True, help="Print primary key column only") @functools.wraps(fn) @@ -20,3 +24,51 @@ def wrapper(*args: Any, **kwargs: Any) -> Any: return fn(*args, **kwargs) return wrapper + + +def render_list( + rows: list[dict[str, Any]], + *, + use_json: bool, + use_jsonl: bool, + quiet: bool, + quiet_key: str, + quiet_default: Any = "", + table: Callable[[], None], + out: TextIO | None = None, +) -> None: + """Render list-command rows through the json/jsonl/quiet/table ladder. + + The first matching branch wins, mirroring the hand-written ladder used + across the list commands: + + 1. ``use_json`` -> dump ``rows`` as a single JSON array. + 2. ``use_jsonl`` -> dump each row as a JSON line. + 3. ``quiet`` -> print ``row.get(quiet_key, quiet_default)`` per row. + 4. otherwise -> invoke ``table`` (the per-command table renderer). + + The quiet branch uses ``dict.get`` uniformly, so a row missing the quiet + key yields ``quiet_default`` instead of raising ``KeyError``. + + Args: + rows: List of row dicts to render in the json/jsonl/quiet branches. + use_json: Emit a single JSON array. + use_jsonl: Emit one JSON object per line. + quiet: Emit only the quiet-key column. + quiet_key: Row key used for the quiet column. + quiet_default: Value used when a row lacks ``quiet_key``. + table: Callback that renders the heterogeneous table branch. + out: Output stream for the quiet branch. Defaults to ``sys.stdout``. + """ + if use_json: + write_json(rows, out=out) + return + if use_jsonl: + write_jsonl(rows, out=out) + return + if quiet: + dest = out or sys.stdout + for row in rows: + dest.write(str(row.get(quiet_key, quiet_default)) + "\n") + return + table() diff --git a/tests/unit/test_formatters.py b/tests/unit/test_formatters.py index 8c10d77a..64fa570e 100644 --- a/tests/unit/test_formatters.py +++ b/tests/unit/test_formatters.py @@ -2,7 +2,11 @@ import io +from click.testing import CliRunner + +from rdc.cli import main from rdc.formatters.json_fmt import write_json, write_jsonl +from rdc.formatters.options import render_list from rdc.formatters.tsv import escape_field, format_row, write_footer, write_tsv @@ -88,3 +92,255 @@ def test_write_jsonl(self) -> None: assert len(lines) == 2 assert '"a": 1' in lines[0] assert '"b": 2' in lines[1] + + +def _flag(name: str) -> None: + raise AssertionError(f"unexpected branch: {name}") + + +class TestRenderList: + """Cover each branch of the json/jsonl/quiet/table ladder.""" + + _ROWS = [{"eid": 1, "type": "Draw"}, {"eid": 2, "type": "Clear"}] + + def test_json_branch(self) -> None: + out = io.StringIO() + render_list( + self._ROWS, + use_json=True, + use_jsonl=False, + quiet=False, + quiet_key="eid", + table=lambda: _flag("table"), + out=out, + ) + assert out.getvalue() == ( + '[\n {\n "eid": 1,\n "type": "Draw"\n },\n' + ' {\n "eid": 2,\n "type": "Clear"\n }\n]\n' + ) + + def test_jsonl_branch(self) -> None: + out = io.StringIO() + render_list( + self._ROWS, + use_json=False, + use_jsonl=True, + quiet=False, + quiet_key="eid", + table=lambda: _flag("table"), + out=out, + ) + assert out.getvalue() == '{"eid": 1, "type": "Draw"}\n{"eid": 2, "type": "Clear"}\n' + + def test_quiet_branch(self) -> None: + out = io.StringIO() + render_list( + self._ROWS, + use_json=False, + use_jsonl=False, + quiet=True, + quiet_key="eid", + table=lambda: _flag("table"), + out=out, + ) + assert out.getvalue() == "1\n2\n" + + def test_table_branch(self) -> None: + calls: list[str] = [] + render_list( + self._ROWS, + use_json=False, + use_jsonl=False, + quiet=False, + quiet_key="eid", + table=lambda: calls.append("table"), + ) + assert calls == ["table"] + + def test_json_wins_over_jsonl_and_quiet(self) -> None: + out = io.StringIO() + render_list( + [{"eid": 1}], + use_json=True, + use_jsonl=True, + quiet=True, + quiet_key="eid", + table=lambda: _flag("table"), + out=out, + ) + assert out.getvalue().startswith("[") + + def test_quiet_missing_key_uses_default_not_keyerror(self) -> None: + """A row missing the quiet key must yield the default, never KeyError.""" + out = io.StringIO() + render_list( + [{"type": "Draw"}], + use_json=False, + use_jsonl=False, + quiet=True, + quiet_key="eid", + quiet_default="", + table=lambda: _flag("table"), + out=out, + ) + assert out.getvalue() == "\n" + + def test_quiet_custom_default(self) -> None: + out = io.StringIO() + render_list( + [{"type": "Draw"}], + use_json=False, + use_jsonl=False, + quiet=True, + quiet_key="eid", + quiet_default=0, + table=lambda: _flag("table"), + out=out, + ) + assert out.getvalue() == "0\n" + + +def _patch(monkeypatch, mod_name: str, response: dict) -> None: + mod = __import__(f"rdc.commands.{mod_name}", fromlist=["call"]) + monkeypatch.setattr(mod, "call", lambda method, params=None: response) + + +_EVENTS = { + "events": [ + {"eid": 1, "type": "Draw", "name": "vkCmdDraw"}, + {"eid": 2, "type": "Dispatch", "name": "vkCmdDispatch"}, + ] +} +_RESOURCES = { + "rows": [ + {"id": 10, "type": "Texture", "name": "albedo"}, + {"id": 20, "type": "Buffer", "name": "verts"}, + ] +} +_COUNTERS = { + "counters": [ + { + "id": 1, + "name": "GPUDuration", + "unit": "Seconds", + "type": "Float", + "category": "Built-in", + }, + { + "id": 8, + "name": "VSInvocations", + "unit": "Absolute", + "type": "UInt", + "category": "Built-in", + }, + ] +} + + +class TestListGoldenParity: + """Byte-identical golden output for list commands across all four modes.""" + + def test_events_quiet(self, monkeypatch) -> None: + _patch(monkeypatch, "events", _EVENTS) + result = CliRunner().invoke(main, ["events", "-q"]) + assert result.exit_code == 0 + assert result.output == "1\n2\n" + + def test_events_jsonl(self, monkeypatch) -> None: + _patch(monkeypatch, "events", _EVENTS) + result = CliRunner().invoke(main, ["events", "--jsonl"]) + assert result.exit_code == 0 + assert result.output == ( + '{"eid": 1, "type": "Draw", "name": "vkCmdDraw"}\n' + '{"eid": 2, "type": "Dispatch", "name": "vkCmdDispatch"}\n' + ) + + def test_events_json(self, monkeypatch) -> None: + _patch(monkeypatch, "events", _EVENTS) + result = CliRunner().invoke(main, ["events", "--json"]) + assert result.exit_code == 0 + assert result.output == ( + "[\n" + ' {\n "eid": 1,\n "type": "Draw",\n "name": "vkCmdDraw"\n },\n' + ' {\n "eid": 2,\n "type": "Dispatch",\n "name": "vkCmdDispatch"\n }\n' + "]\n" + ) + + def test_events_table(self, monkeypatch) -> None: + _patch(monkeypatch, "events", _EVENTS) + result = CliRunner().invoke(main, ["events"]) + assert result.exit_code == 0 + assert result.output == "EID\tTYPE\tNAME\n1\tDraw\tvkCmdDraw\n2\tDispatch\tvkCmdDispatch\n" + + def test_resources_quiet(self, monkeypatch) -> None: + _patch(monkeypatch, "resources", _RESOURCES) + result = CliRunner().invoke(main, ["resources", "-q"]) + assert result.exit_code == 0 + assert result.output == "10\n20\n" + + def test_resources_jsonl(self, monkeypatch) -> None: + _patch(monkeypatch, "resources", _RESOURCES) + result = CliRunner().invoke(main, ["resources", "--jsonl"]) + assert result.exit_code == 0 + assert result.output == ( + '{"id": 10, "type": "Texture", "name": "albedo"}\n' + '{"id": 20, "type": "Buffer", "name": "verts"}\n' + ) + + def test_resources_json(self, monkeypatch) -> None: + _patch(monkeypatch, "resources", _RESOURCES) + result = CliRunner().invoke(main, ["resources", "--json"]) + assert result.exit_code == 0 + assert result.output == ( + "[\n" + ' {\n "id": 10,\n "type": "Texture",\n "name": "albedo"\n },\n' + ' {\n "id": 20,\n "type": "Buffer",\n "name": "verts"\n }\n' + "]\n" + ) + + def test_resources_table(self, monkeypatch) -> None: + _patch(monkeypatch, "resources", _RESOURCES) + result = CliRunner().invoke(main, ["resources"]) + assert result.exit_code == 0 + assert result.output == "ID\tTYPE\tNAME\n10\tTexture\talbedo\n20\tBuffer\tverts\n" + + def test_counters_quiet(self, monkeypatch) -> None: + _patch(monkeypatch, "counters", _COUNTERS) + result = CliRunner().invoke(main, ["counters", "--list", "-q"]) + assert result.exit_code == 0 + assert result.output == "1\n8\n" + + def test_counters_jsonl(self, monkeypatch) -> None: + _patch(monkeypatch, "counters", _COUNTERS) + result = CliRunner().invoke(main, ["counters", "--list", "--jsonl"]) + assert result.exit_code == 0 + assert result.output == ( + '{"id": 1, "name": "GPUDuration", "unit": "Seconds", ' + '"type": "Float", "category": "Built-in"}\n' + '{"id": 8, "name": "VSInvocations", "unit": "Absolute", ' + '"type": "UInt", "category": "Built-in"}\n' + ) + + def test_counters_table(self, monkeypatch) -> None: + _patch(monkeypatch, "counters", _COUNTERS) + result = CliRunner().invoke(main, ["counters", "--list"]) + assert result.exit_code == 0 + assert result.output == ( + "ID\tNAME\tUNIT\tTYPE\tCATEGORY\n" + "1\tGPUDuration\tSeconds\tFloat\tBuilt-in\n" + "8\tVSInvocations\tAbsolute\tUInt\tBuilt-in\n" + ) + + +class TestQuietKeyNormalization: + """A row missing the quiet key yields the default, not a KeyError. + + This fails on the pre-refactor bracket-access commands (events/draws/etc). + """ + + def test_events_missing_eid_no_keyerror(self, monkeypatch) -> None: + _patch(monkeypatch, "events", {"events": [{"type": "Draw", "name": "noeid"}]}) + result = CliRunner().invoke(main, ["events", "-q"]) + assert result.exit_code == 0 + assert result.exception is None + assert result.output == "\n" diff --git a/tests/unit/test_output_options.py b/tests/unit/test_output_options.py index 99eec172..b7205ec5 100644 --- a/tests/unit/test_output_options.py +++ b/tests/unit/test_output_options.py @@ -11,7 +11,7 @@ def test_decorator_adds_no_header() -> None: @click.command("test-cmd") @list_output_options - def cmd(no_header: bool, use_jsonl: bool, quiet: bool) -> None: + def cmd(no_header: bool, use_json: bool, use_jsonl: bool, quiet: bool) -> None: click.echo(f"no_header={no_header}") result = CliRunner().invoke(cmd, ["--no-header"]) @@ -19,10 +19,21 @@ def cmd(no_header: bool, use_jsonl: bool, quiet: bool) -> None: assert "no_header=True" in result.output +def test_decorator_adds_json() -> None: + @click.command("test-cmd") + @list_output_options + def cmd(no_header: bool, use_json: bool, use_jsonl: bool, quiet: bool) -> None: + click.echo(f"use_json={use_json}") + + result = CliRunner().invoke(cmd, ["--json"]) + assert result.exit_code == 0 + assert "use_json=True" in result.output + + def test_decorator_adds_jsonl() -> None: @click.command("test-cmd") @list_output_options - def cmd(no_header: bool, use_jsonl: bool, quiet: bool) -> None: + def cmd(no_header: bool, use_json: bool, use_jsonl: bool, quiet: bool) -> None: click.echo(f"use_jsonl={use_jsonl}") result = CliRunner().invoke(cmd, ["--jsonl"]) @@ -33,7 +44,7 @@ def cmd(no_header: bool, use_jsonl: bool, quiet: bool) -> None: def test_decorator_adds_quiet() -> None: @click.command("test-cmd") @list_output_options - def cmd(no_header: bool, use_jsonl: bool, quiet: bool) -> None: + def cmd(no_header: bool, use_json: bool, use_jsonl: bool, quiet: bool) -> None: click.echo(f"quiet={quiet}") result = CliRunner().invoke(cmd, ["-q"]) @@ -44,21 +55,27 @@ def cmd(no_header: bool, use_jsonl: bool, quiet: bool) -> None: def test_decorator_defaults_false() -> None: @click.command("test-cmd") @list_output_options - def cmd(no_header: bool, use_jsonl: bool, quiet: bool) -> None: - click.echo(f"{no_header},{use_jsonl},{quiet}") + def cmd(no_header: bool, use_json: bool, use_jsonl: bool, quiet: bool) -> None: + click.echo(f"{no_header},{use_json},{use_jsonl},{quiet}") result = CliRunner().invoke(cmd, []) assert result.exit_code == 0 - assert "False,False,False" in result.output + assert "False,False,False,False" in result.output def test_decorator_preserves_other_options() -> None: @click.command("test-cmd") - @click.option("--json", "use_json", is_flag=True) + @click.option("--type", "type_filter", default=None) @list_output_options - def cmd(use_json: bool, no_header: bool, use_jsonl: bool, quiet: bool) -> None: - click.echo(f"json={use_json},quiet={quiet}") - - result = CliRunner().invoke(cmd, ["--json", "-q"]) + def cmd( + type_filter: str | None, + no_header: bool, + use_json: bool, + use_jsonl: bool, + quiet: bool, + ) -> None: + click.echo(f"type={type_filter},json={use_json},quiet={quiet}") + + result = CliRunner().invoke(cmd, ["--type", "tex", "--json", "-q"]) assert result.exit_code == 0 - assert "json=True,quiet=True" in result.output + assert "type=tex,json=True,quiet=True" in result.output