From c223d22981894de8f2e6c563ac878c509aec58a7 Mon Sep 17 00:00:00 2001 From: tack Date: Thu, 23 Apr 2026 15:53:07 +0800 Subject: [PATCH] feat(cli): --json machine-readable output mode --- .../tests/test_cli_validate.py | 33 +++++++++++ tk_comfyui_batch_image/validate.py | 58 ++++++++++++++++--- 2 files changed, 84 insertions(+), 7 deletions(-) diff --git a/tk_comfyui_batch_image/tests/test_cli_validate.py b/tk_comfyui_batch_image/tests/test_cli_validate.py index 77f1c78..63c54dc 100644 --- a/tk_comfyui_batch_image/tests/test_cli_validate.py +++ b/tk_comfyui_batch_image/tests/test_cli_validate.py @@ -70,3 +70,36 @@ def test_cli_unreadable_json_exits_3(tmp_path, capsys): p = _write(tmp_path, "broken.json", "{not json") code = main([str(p)]) assert code == 3 + + +def test_cli_json_output_structure_on_success(tmp_path, capsys): + p = _write(tmp_path, "ok.json", _valid_minimal_book()) + code = main([str(p), "--json"]) + out = capsys.readouterr().out + payload = json.loads(out) + assert code == 0 + assert payload["summary"] == {"total": 1, "ok": 1, "fail": 0} + assert len(payload["files"]) == 1 + f0 = payload["files"][0] + assert f0["status"] == "ok" + assert f0["errors"] == [] + assert f0["info"]["job_id"] == "cli_test" + assert f0["info"]["page_count"] == 1 + assert f0["info"]["panel_count"] == 1 + + +def test_cli_json_output_structure_on_failure(tmp_path, capsys): + book = _valid_minimal_book() + book["pages"][0]["page_index"] = 5 + p = _write(tmp_path, "bad.json", book) + code = main([str(p), "--json"]) + payload = json.loads(capsys.readouterr().out) + assert code == 1 + assert payload["summary"] == {"total": 1, "ok": 0, "fail": 1} + f0 = payload["files"][0] + assert f0["status"] == "fail" + assert len(f0["errors"]) >= 1 + err = f0["errors"][0] + assert err["layer"] == 2 + assert err["path"] == "pages[0].page_index" + assert "expected 1" in err["message"] diff --git a/tk_comfyui_batch_image/validate.py b/tk_comfyui_batch_image/validate.py index a3069f0..febb7e2 100644 --- a/tk_comfyui_batch_image/validate.py +++ b/tk_comfyui_batch_image/validate.py @@ -8,6 +8,7 @@ from __future__ import annotations import argparse import json import sys +from dataclasses import asdict from pathlib import Path from .core.validator import CheckError, collect_errors @@ -55,6 +56,17 @@ def _print_file_human(path: Path, status: str, errors: list[CheckError], f"(raise --max-errors to see all)") +def _file_result_dict(path: Path, status: str, errors: list[CheckError], info: dict | None) -> dict: + d = { + "path": str(path), + "status": status, + "errors": [asdict(e) for e in errors], + } + if info is not None: + d["info"] = info + return d + + def _ensure_utf8_stdio() -> None: """Reconfigure stdout/stderr to UTF-8 if the current encoding can't handle it.""" import contextlib @@ -76,33 +88,65 @@ def main(argv: list[str] | None = None) -> int: description="Validate comic script JSON(s).", ) parser.add_argument("files", nargs="*", help="JSON file(s) to validate") + parser.add_argument("--json", action="store_true", + help="Emit machine-readable JSON instead of human-readable text") args = parser.parse_args(argv) if not args.files: parser.print_usage(sys.stderr) return 2 + results: list[dict] = [] any_fail = False for raw in args.files: path = Path(raw) if not path.exists(): - print(f"✗ {path} (file not found)", file=sys.stderr) - any_fail = True - return 3 + if args.json: + results.append({"path": str(path), "status": "fail", + "errors": [{"layer": 0, "path": "", + "message": "file not found", "hint": None}]}) + any_fail = True + continue + else: + print(f"✗ {path} (file not found)", file=sys.stderr) + return 3 try: data = _load_json(path) except (OSError, json.JSONDecodeError) as e: - print(f"✗ {path} (I/O or JSON parse error: {e})", file=sys.stderr) - return 3 + if args.json: + results.append({"path": str(path), "status": "fail", + "errors": [{"layer": 0, "path": "", + "message": f"I/O or JSON parse error: {e}", + "hint": None}]}) + any_fail = True + continue + else: + print(f"✗ {path} (I/O or JSON parse error: {e})", file=sys.stderr) + return 3 errors = collect_errors(data) if errors: any_fail = True - _print_file_human(path, "fail", errors, None) + if args.json: + results.append(_file_result_dict(path, "fail", errors, None)) + else: + _print_file_human(path, "fail", errors, None) else: info = {"version": data.get("version"), **_summarize_file(path, data)} - _print_file_human(path, "ok", [], info) + if args.json: + results.append(_file_result_dict(path, "ok", [], info)) + else: + _print_file_human(path, "ok", [], info) + + if args.json: + summary = { + "total": len(results), + "ok": sum(1 for r in results if r["status"] == "ok"), + "fail": sum(1 for r in results if r["status"] == "fail"), + } + print(json.dumps({"summary": summary, "files": results}, + indent=2, ensure_ascii=False)) return 1 if any_fail else 0