diff --git a/CHANGELOG.md b/CHANGELOG.md index 8aebc41..a2e0ecb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ ## Unreleased +## [1.4.0] - 2026-08-21 + +### Added + +- 旧Verification Platformおよび現行版で保存した入力JSONを、Web画面から + アップロードして計算できるようにした。旧JSONに存在しない項目は、JSONで + 選択されている暖房・冷房方式に対応する現行デフォルトで補完し、明示された値と + 未知の旧キーは保持する。 +- JSON未選択、不正なUTF-8・JSON形式、オブジェクト以外の最上位構造、5 MBを超える + 入力を計算前に検出し、計算ログへ理由を表示するようにした。 + ## [1.3.1] - 2026-08-20 ### Fixed diff --git a/README.md b/README.md index 2eb1a31..d547a9f 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,19 @@ ## 公開Web版 -**[Verification Platform Next ver.1.3.1をブラウザで開く](https://verification-platform-next-dnoeszca4a-an.a.run.app/)** +**[Verification Platform Next ver.1.4.0をブラウザで開く](https://verification-platform-next-dnoeszca4a-an.a.run.app/)** 公開Web版では、インストールせずに入力、計算、結果ファイルの取得を行えます。 計算は1件ずつ処理するため、混雑時は画面に表示される順番をお待ちください。 詳細CSVは通常計算時には作成せず、「出力ファイル」タブの 「CSVファイルを出力」を押したときだけ作成します。 +「JSONファイルから計算」では、旧版と現行版の入力JSONを実行できます。旧JSONで +不足する追加項目は、現在版のデフォルト値で自動的に補完されます。 入力ファイルと計算成果物は一時的に保存されるため、機密情報や個人情報を含む データはアップロードしないでください。 住宅の省エネルギー性能を検証するための計算エンジン、入力スキーマ、Gradio UIを -まとめたPythonモノレポです。正式版は **ver.1.3.1** です。 +まとめたPythonモノレポです。正式版は **ver.1.4.0** です。 本リポジトリは、建築研究所が公開する住宅のエネルギー消費性能計算プログラム `pyhees`を基礎とし、検証用の入力画面、床下空調などの拡張計算、数値回帰試験を diff --git a/apps/gradio/pyproject.toml b/apps/gradio/pyproject.toml index 519de65..baab3be 100644 --- a/apps/gradio/pyproject.toml +++ b/apps/gradio/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "verification-platform-app" -version = "1.3.1" +version = "1.4.0" description = "Gradio application for Verification Platform" authors = ["iguchi-lab"] packages = [{ include = "verification_app", from = "src" }] diff --git a/apps/gradio/src/verification_app/form_app.py b/apps/gradio/src/verification_app/form_app.py index db11ed2..c881ab8 100644 --- a/apps/gradio/src/verification_app/form_app.py +++ b/apps/gradio/src/verification_app/form_app.py @@ -206,6 +206,25 @@ def build_app( elem_classes=["default-reset-note"], ) + with gr.Accordion("JSONファイルから計算", open=False): + gr.Markdown( + "旧Verification Platformまたは現在の画面で保存した入力JSONを" + "そのまま実行できます。旧JSONに存在しない項目は、現在の" + "デフォルト値で補完します。アップロード内容は画面上の入力欄には" + "反映されないため、実行後の「計算入力」タブで確認してください。" + ) + with gr.Row(): + json_upload = gr.File( + label="入力JSONファイル", + file_types=[".json"], + file_count="single", + type="filepath", + ) + json_run = gr.Button( + "JSONファイルをアップロードして計算", + variant="secondary", + ) + components: dict[str, Any] = {} containers: dict[str, Any] = {} field_dom_ids: dict[str, str] = {} @@ -318,6 +337,13 @@ def calculate(*raw_values: Any) -> tuple[Any, ...]: result = calculation_service.run(values, include_graphs=False) return _calculation_outputs(result) + def calculate_uploaded_json(uploaded_file: str | None) -> tuple[Any, ...]: + result = calculation_service.run_uploaded_json( + uploaded_file, + include_graphs=False, + ) + return _calculation_outputs(result) + def generate_graphs(result: CalculationResult | None) -> tuple[Any, ...]: if result is None: return _graph_outputs(None) @@ -400,6 +426,49 @@ def export_csv_files( show_progress="full", ) + json_calculation_started = json_run.click( + _calculation_started_outputs, + outputs=[ + status, + annual_summary, + preview, + log, + graph_status, + *graphs, + csv_status, + export_csv_button, + files, + ], + queue=False, + api_visibility="private", + ) + json_calculation_finished = json_calculation_started.then( + calculate_uploaded_json, + inputs=json_upload, + outputs=[ + result_state, + status, + annual_summary, + preview, + log, + graph_status, + csv_status, + export_csv_button, + files, + ], + concurrency_limit=1, + concurrency_id="calculation", + show_progress="full", + ) + json_calculation_finished.then( + generate_graphs, + inputs=result_state, + outputs=[graph_status, log, *graphs], + concurrency_limit=1, + concurrency_id="calculation", + show_progress="full", + ) + conditional_fields = tuple( field for field in form.schema.fields if field.enabled_when is not None ) diff --git a/apps/gradio/src/verification_app/services.py b/apps/gradio/src/verification_app/services.py index 275fbc9..a84628c 100644 --- a/apps/gradio/src/verification_app/services.py +++ b/apps/gradio/src/verification_app/services.py @@ -15,7 +15,7 @@ from typing import Any, Callable, ContextManager, Mapping from uuid import uuid4 -from verification_core import build_input_data +from verification_core import build_input_data, load_compatible_input_json CalculationFunction = Callable[[dict[str, Any]], Any] VersionFunction = Callable[[], str] @@ -24,6 +24,7 @@ CsvExportSessionFactory = Callable[[], ContextManager[Any]] _CALCULATION_CWD_LOCK = threading.Lock() +_MAX_INPUT_JSON_BYTES = 5 * 1024 * 1024 def _captured_dataframe(csv_exports: Any | None, path: Path) -> Any | None: @@ -96,6 +97,8 @@ def run( values: Mapping[str, Any], *, include_graphs: bool = True, + prepared_input_data: Mapping[str, Any] | None = None, + input_source: str = "画面入力", ) -> CalculationResult: input_data: dict[str, Any] | None = None run_id: str | None = None @@ -103,7 +106,11 @@ def run( csv_exports: Any | None = None output = io.StringIO() try: - input_data = build_input_data(values) + input_data = ( + dict(prepared_input_data) + if prepared_input_data is not None + else build_input_data(values) + ) _validate_case_name(input_data.get("case_name", "default")) csv_session = ( self._csv_export_session() @@ -150,6 +157,7 @@ def run( annual_summary, output.getvalue(), pending_csv_count, + input_source, ), files=files, graph_status="計算完了後にグラフを表示します。", @@ -190,6 +198,55 @@ def run( graphs=(), ) + def run_uploaded_json( + self, + uploaded_file: str | Path | None, + *, + include_graphs: bool = True, + ) -> CalculationResult: + """Run an input JSON saved by an old or current platform version.""" + + try: + if uploaded_file is None: + raise ValueError("実行するJSONファイルを選択してください。") + path = Path(uploaded_file) + if not path.is_file(): + raise ValueError("選択したJSONファイルを読み取れません。") + if path.stat().st_size > _MAX_INPUT_JSON_BYTES: + raise ValueError("入力JSONは5 MB以下にしてください。") + compatible = load_compatible_input_json(path.read_bytes()) + count = len(compatible.supplemented_paths) + input_source = ( + "アップロードJSON(不足項目なし)" + if count == 0 + else f"アップロードJSON(不足していた{count}項目を現行デフォルトで補完)" + ) + return self.run( + {}, + include_graphs=include_graphs, + prepared_input_data=compatible.input_data, + input_source=input_source, + ) + except Exception: + return CalculationResult( + succeeded=False, + status="❌ 入力JSONエラー", + input_data=None, + run_id=None, + artifact_dir=None, + log=_format_failure_log( + None, + None, + self._version_info(), + None, + "", + traceback.format_exc(), + ), + files=(), + graph_status="計算完了後にグラフを表示します。", + graphs=(), + ) + def generate_graphs(self, result: CalculationResult) -> CalculationResult: if not result.succeeded or result.input_data is None: return result @@ -433,6 +490,7 @@ def _format_success_log( annual_summary: AnnualSummary | None, engine_log: str, pending_csv_count: int = 0, + input_source: str = "画面入力", ) -> str: output_step = ( f"4) 詳細CSV {pending_csv_count}件をメモリに保持(画面のボタンで出力)" @@ -446,7 +504,7 @@ def _format_success_log( f"成果物版: {version}", "", "===== 2. 実行した処理 =====", - "1) 画面入力を検証し、計算エンジン用input_dataへ変換", + f"1) {input_source}を検証し、計算エンジン用input_dataを準備", "2) 暖房・冷房の8760時間計算を順番に実行", "3) 一次エネルギー、未処理負荷、機器・ファン電力を集計", output_step, diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3fa92cd..c45cccc 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -48,6 +48,11 @@ Phase 5 完了後は、このモノレポがUI、入力契約、計算エンジ 解釈する評価器は製品APIではなく、Phase 5で旧版との一致を確認するための非公開回帰専用 モジュールです。製品コードから旧フォーム評価器へ依存させません。 +保存済み入力JSONを再実行する場合は、JSON内の明示値を優先し、選択されている +暖房・冷房方式に対応する現行デフォルトを不足キーだけへ補完します。これにより、 +旧JSONを旧フォーム評価器へ戻さず、現行の入力契約と計算サービスへ直接合流させます。 +未知の旧キーは比較・後方互換のため保持し、配列と単一値は部分的に書き換えません。 + ## 製品版と計算成果物 製品版の正本は`jjjexperiment.release`です。UI、入力契約、計算エンジンを diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 40dfe17..c050617 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -5,10 +5,10 @@ Verification Platform Nextは、製品全体を1つの [Semantic Versioning](https://semver.org/) で管理します。 -- Pythonパッケージ版: `1.3.1` -- 表示名: `ver.1.3.1` -- Gitタグ: `v1.3.1` -- 成果物接尾辞: `_v1.3.1` +- Pythonパッケージ版: `1.4.0` +- 表示名: `ver.1.4.0` +- Gitタグ: `v1.4.0` +- 成果物接尾辞: `_v1.4.0` 製品版の正本は `packages/pyhees-jjj/src/jjjexperiment/release.py`です。ルート、 @@ -23,10 +23,10 @@ Verification Platform Nextは、製品全体を1つの 成果物名には安定した版接尾辞を付けます。 ```text -_v1.3.1_input.json -_v1.3.1_output1.csv -_v1.3.1_output2.csv -_v1.3.1_manifest.json +_v1.4.0_input.json +_v1.4.0_output1.csv +_v1.4.0_output2.csv +_v1.4.0_manifest.json ``` 計算が正常終了すると、マニフェストへ次を記録します。 @@ -67,9 +67,9 @@ GitHub Actionsがすべて成功した後、PRをマージし、マージコミ ```bash git switch main git pull --ff-only -git tag -a v1.3.1 -m "Verification Platform Next ver.1.3.1" -git push origin v1.3.1 -gh release create v1.3.1 --title "Verification Platform Next ver.1.3.1" +git tag -a v1.4.0 -m "Verification Platform Next ver.1.4.0" +git push origin v1.4.0 +gh release create v1.4.0 --title "Verification Platform Next ver.1.4.0" ``` GitHub Releaseには`CHANGELOG.md`の該当版、試験結果、既知の制約を記載します。 diff --git a/docs/input_guide.md b/docs/input_guide.md index ced6790..61305c9 100644 --- a/docs/input_guide.md +++ b/docs/input_guide.md @@ -13,6 +13,24 @@ Verification Platform Nextの入力画面は、建築研究所の住宅のエネ 「入力をデフォルトに戻す」を押すと、全項目がデフォルト値へ戻り、選択した方式に 不要な項目も再び非表示になります。 +### JSONファイルから計算する + +画面上部の「JSONファイルから計算」を開くと、次の入力JSONをアップロードして +計算できます。 + +- 旧Verification Platformで保存した入力JSON +- Verification Platform Nextの計算結果に含まれる`*_input.json` + +旧JSONに現在の追加項目がない場合は、その項目だけ現在版のデフォルト値で補完します。 +JSONに記録されている既存項目は、デフォルトで上書きしません。辞書形式の階層は +不足キーごとに補完し、配列と単一の値はJSONの指定をそのまま使用します。 + +アップロードしたJSONは画面の入力欄を書き換えません。実際に計算へ使用した値は、 +計算完了後の「計算入力」タブで確認できます。`climateFile`または`loadFile`に外部 +ファイル名を指定したJSONを公開Web版で使う場合、その外部ファイル自体はJSONに +含まれないため利用できません。標準同梱データを使う場合は従来どおり`"-"`を指定 +してください。 + ## 2. 入力する順番 | 順番 | セクション | 主な内容 | diff --git a/packages/pyhees-jjj/pyproject.toml b/packages/pyhees-jjj/pyproject.toml index 3452d11..fb1913c 100644 --- a/packages/pyhees-jjj/pyproject.toml +++ b/packages/pyhees-jjj/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "pyhees-jjj" -version = "1.3.1" +version = "1.4.0" description = "Pyheesの検証用のフォークプロジェクトです" homepage = "https://github.com/izumi-system-development/pyhees-jjj" authors = ["Pyhees Development Team"] diff --git a/packages/pyhees-jjj/src/jjjexperiment/release.py b/packages/pyhees-jjj/src/jjjexperiment/release.py index 144a4f8..122f4f6 100644 --- a/packages/pyhees-jjj/src/jjjexperiment/release.py +++ b/packages/pyhees-jjj/src/jjjexperiment/release.py @@ -11,10 +11,10 @@ from typing import Any, Mapping -VERSION = "1.3.1" -DISPLAY_VERSION = "ver.1.3.1" -RELEASE_DATE = "2026-08-20" -ARTIFACT_VERSION = "_v1.3.1" +VERSION = "1.4.0" +DISPLAY_VERSION = "ver.1.4.0" +RELEASE_DATE = "2026-08-21" +ARTIFACT_VERSION = "_v1.4.0" UPSTREAM_PYHEES_VERSION = "3.10.0" UPSTREAM_PYHEES_COMMIT = "d5224c4a01def00a8421bcd2fcc0d4b4a5b88644" diff --git a/packages/pyhees-jjj/src/tests/test_release.py b/packages/pyhees-jjj/src/tests/test_release.py index 46f9c63..c887579 100644 --- a/packages/pyhees-jjj/src/tests/test_release.py +++ b/packages/pyhees-jjj/src/tests/test_release.py @@ -22,7 +22,7 @@ def test_source_commit_prefers_deployment_metadata(monkeypatch): def test_write_artifact_manifest_records_release_and_input(tmp_path, monkeypatch): monkeypatch.setenv("VERIFICATION_SOURCE_COMMIT", "0123456789abcdef") input_data = {"case_name": "case", "region": 6} - path = tmp_path / "case_v1.3.1_manifest.json" + path = tmp_path / "case_v1.4.0_manifest.json" release.write_artifact_manifest(path, input_data) @@ -30,10 +30,10 @@ def test_write_artifact_manifest_records_release_and_input(tmp_path, monkeypatch assert manifest == { "schema_version": 1, "product": "Verification Platform Next", - "version": "1.3.1", - "display_version": "ver.1.3.1", - "release_date": "2026-08-20", - "artifact_version": "_v1.3.1", + "version": "1.4.0", + "display_version": "ver.1.4.0", + "release_date": "2026-08-21", + "artifact_version": "_v1.4.0", "source_commit": "0123456789abcdef", "upstream_pyhees_version": "3.10.0", "upstream_pyhees_commit": "d5224c4a01def00a8421bcd2fcc0d4b4a5b88644", @@ -82,7 +82,7 @@ def test_calc_writes_manifest_only_after_success(tmp_path, monkeypatch): assert actual is result assert json.loads( - (tmp_path / "release-case_v1.3.1_input.json").read_text(encoding="utf-8") + (tmp_path / "release-case_v1.4.0_input.json").read_text(encoding="utf-8") ) == input_data assert events == [ ("constants", input_data), @@ -90,7 +90,7 @@ def test_calc_writes_manifest_only_after_success(tmp_path, monkeypatch): ("calculate", experiment_main.calc_main), ( "manifest", - Path("release-case_v1.3.1_manifest.json"), + Path("release-case_v1.4.0_manifest.json"), input_data, ), ] diff --git a/packages/verification-core/pyproject.toml b/packages/verification-core/pyproject.toml index 7decdfc..1637aa1 100644 --- a/packages/verification-core/pyproject.toml +++ b/packages/verification-core/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "verification-core" -version = "1.3.1" +version = "1.4.0" description = "Canonical input schema and shared contracts for Verification Platform" authors = ["iguchi-lab"] packages = [{ include = "verification_core", from = "src" }] diff --git a/packages/verification-core/src/verification_core/__init__.py b/packages/verification-core/src/verification_core/__init__.py index fff0101..f128d3d 100644 --- a/packages/verification-core/src/verification_core/__init__.py +++ b/packages/verification-core/src/verification_core/__init__.py @@ -11,12 +11,14 @@ LegacyInputInventory, load_legacy_inventory, ) +from .json_compat import CompatibleInputData, load_compatible_input_json from .schema import Condition, FieldDefinition, FieldKind, FieldOrigin, InputSchema from .schema_loader import load_input_schema __all__ = [ "BindingCondition", "Condition", + "CompatibleInputData", "FieldDefinition", "FieldKind", "FieldOrigin", @@ -28,6 +30,7 @@ "build_input_data", "default_ui_values", "load_input_bindings", + "load_compatible_input_json", "load_input_schema", "load_legacy_inventory", ] diff --git a/packages/verification-core/src/verification_core/json_compat.py b/packages/verification-core/src/verification_core/json_compat.py new file mode 100644 index 0000000..b4df05d --- /dev/null +++ b/packages/verification-core/src/verification_core/json_compat.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +import json +from copy import deepcopy +from dataclasses import dataclass +from json import JSONDecodeError +from typing import Any, Mapping + +from .declarative_builder import build_input_data +from .legacy import load_legacy_inventory + + +@dataclass(frozen=True, slots=True) +class CompatibleInputData: + """Engine input restored from a saved JSON document.""" + + input_data: dict[str, Any] + supplemented_paths: tuple[str, ...] + + +def load_compatible_input_json( + payload: str | bytes, + version: str = "260812", +) -> CompatibleInputData: + """Load old or current input JSON and fill fields added in newer versions. + + Values explicitly present in the uploaded document always win. Missing mapping + entries are copied from the current declarative defaults. Lists and scalar values + are treated atomically so that an uploaded value is never silently rewritten. + """ + + try: + if isinstance(payload, bytes): + payload = payload.decode("utf-8-sig") + imported = json.loads(payload) + except (JSONDecodeError, UnicodeDecodeError) as error: + if isinstance(error, JSONDecodeError): + location = f"({error.lineno}行{error.colno}列)" + else: + location = "" + raise ValueError( + f"JSONファイルをUTF-8として読み取れませんでした{location}。" + ) from error + + if not isinstance(imported, Mapping): + raise ValueError("入力JSONの最上位はオブジェクト({ ... })である必要があります。") + + defaults = build_input_data( + _equipment_type_defaults(imported, version), + version=version, + ) + supplemented: list[str] = [] + merged = _merge_mapping(defaults, imported, (), supplemented) + return CompatibleInputData(merged, tuple(supplemented)) + + +def _equipment_type_defaults( + imported: Mapping[str, Any], + version: str, +) -> dict[str, Any]: + """Select defaults for the same heating/cooling branch as the saved JSON.""" + + inventory = load_legacy_inventory(version) + fields = {field.id: field for field in inventory.fields} + values: dict[str, Any] = {} + for equipment_key in ("H_A", "C_A"): + equipment = imported.get(equipment_key) + if not isinstance(equipment, Mapping): + continue + equipment_type = equipment.get("type") + if isinstance(equipment_type, bool): + continue + try: + choice_index = int(equipment_type) - 1 + except (TypeError, ValueError): + continue + field_id = f"{equipment_key}_type__0" + field = fields[field_id] + if 0 <= choice_index < len(field.choices): + values[field_id] = field.choices[choice_index] + return values + + +def _merge_mapping( + defaults: Mapping[str, Any], + imported: Mapping[str, Any], + prefix: tuple[str, ...], + supplemented: list[str], +) -> dict[str, Any]: + result: dict[str, Any] = {} + for key, default_value in defaults.items(): + path = (*prefix, key) + if key not in imported: + result[key] = deepcopy(default_value) + supplemented.append(".".join(path)) + continue + + imported_value = imported[key] + if isinstance(default_value, Mapping) and isinstance(imported_value, Mapping): + result[key] = _merge_mapping( + default_value, + imported_value, + path, + supplemented, + ) + else: + result[key] = deepcopy(imported_value) + + for key, imported_value in imported.items(): + if key not in defaults: + result[key] = deepcopy(imported_value) + return result diff --git a/pyproject.toml b/pyproject.toml index 4a3afe9..6060c04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "verification-platform-next" -version = "1.3.1" +version = "1.4.0" description = "Monorepo for the Verification Platform engine, schema, and UI" authors = ["iguchi-lab"] package-mode = false diff --git a/regression/phase5/manifest.json b/regression/phase5/manifest.json index e94fab1..f3d8245 100644 --- a/regression/phase5/manifest.json +++ b/regression/phase5/manifest.json @@ -2,7 +2,7 @@ "schema_version": 1, "legacy_form_version": "260715", "engine_commit": "d5224c4a01def00a8421bcd2fcc0d4b4a5b88644", - "engine_version": "_v1.3.1", + "engine_version": "_v1.4.0", "tolerances": { "relative": 1e-09, "absolute": 1e-08 diff --git a/tests/test_calculation_service.py b/tests/test_calculation_service.py index f227f57..d72573b 100644 --- a/tests/test_calculation_service.py +++ b/tests/test_calculation_service.py @@ -121,6 +121,54 @@ def calculate(input_data: dict[str, object]) -> None: assert result.graphs == () +def test_service_runs_old_uploaded_json_with_current_defaults(tmp_path: Path) -> None: + captured: list[dict[str, object]] = [] + + def calculate(input_data: dict[str, object]) -> None: + captured.append(input_data) + + input_path = tmp_path / "old_input.json" + input_path.write_text( + '{"case_name": "uploaded", "region": 2, "H_A": {"type": 2}}', + encoding="utf-8", + ) + service = CalculationService( + calculate, + lambda: "v1", + workdir=tmp_path, + run_id_factory=lambda: "uploaded-json", + ) + + result = service.run_uploaded_json(input_path, include_graphs=False) + + assert result.succeeded + assert captured[0]["case_name"] == "uploaded" + assert captured[0]["region"] == 2 + assert captured[0]["H_A"]["type"] == 2 + assert captured[0]["C_A"]["type"] == 1 + assert "アップロードJSON(不足していた" in result.log + assert "現行デフォルトで補完" in result.log + + +def test_service_rejects_invalid_uploaded_json_without_running_engine( + tmp_path: Path, +) -> None: + input_path = tmp_path / "invalid.json" + input_path.write_text("[]", encoding="utf-8") + service = CalculationService( + lambda input_data: pytest.fail("engine must not run"), + lambda: "v1", + workdir=tmp_path, + ) + + result = service.run_uploaded_json(input_path, include_graphs=False) + + assert not result.succeeded + assert result.status == "❌ 入力JSONエラー" + assert "最上位はオブジェクト" in result.log + assert not tuple(tmp_path.glob("run-*")) + + def test_service_preserves_successful_calculation_when_graphs_fail(tmp_path: Path) -> None: def calculate(input_data: dict[str, object]) -> None: prefix = f"{input_data['case_name']}v1" diff --git a/tests/test_gradio_app.py b/tests/test_gradio_app.py index b909c22..b691205 100644 --- a/tests/test_gradio_app.py +++ b/tests/test_gradio_app.py @@ -59,8 +59,8 @@ def test_gradio_app_builds_all_schema_inputs_and_events() -> None: } assert gradio.__version__.startswith("6.") - assert config["title"] == "Verification Platform Next ver.1.3.1" - assert component_types["accordion"] == 8 + assert config["title"] == "Verification Platform Next ver.1.4.0" + assert component_types["accordion"] == 9 assert component_types["number"] == 156 assert component_types["dropdown"] == 54 assert component_types["checkbox"] == 10 @@ -113,11 +113,19 @@ def test_gradio_app_builds_all_schema_inputs_and_events() -> None: assert buttons["↩ 入力をデフォルトに戻す"]["props"]["variant"] == "secondary" assert "CSVファイルを出力" in buttons assert buttons["CSVファイルを出力"]["props"]["interactive"] is False + assert "JSONファイルをアップロードして計算" in buttons + assert component_types["file"] == 2 + assert any( + "旧Verification Platformまたは現在の画面で保存した入力JSON" in value + and "現在のデフォルト値で補完" in value + for value in markdown_values + ) assert origin_classes == { "input-origin-bri-web": 47, "input-origin-verification-platform": 176, } assert tuple(accordions) == ( + "JSONファイルから計算", "① 計算条件・外部データ", "② 住宅基本情報", "③ 外皮性能", @@ -127,9 +135,12 @@ def test_gradio_app_builds_all_schema_inputs_and_events() -> None: "⑦ 熱交換型換気設備", "⑧ 詳細設定・比較検証", ) + input_accordions = tuple( + component for label, component in accordions.items() if label[0].isdigit() + ) assert all( "input-section" in component["props"].get("elem_classes", ()) - for component in accordions.values() + for component in input_accordions ) assert all(component["props"]["open"] is False for component in accordions.values()) assert all(component["props"]["visible"] is True for component in accordions.values()) diff --git a/tests/test_json_compat.py b/tests/test_json_compat.py new file mode 100644 index 0000000..1e95a8b --- /dev/null +++ b/tests/test_json_compat.py @@ -0,0 +1,83 @@ +import json +from pathlib import Path + +import pytest + +from verification_core import ( + build_input_data, + load_compatible_input_json, + load_legacy_inventory, +) + +_ROOT = Path(__file__).resolve().parents[1] + + +def test_current_json_is_loaded_without_supplementation() -> None: + current = build_input_data({}) + + loaded = load_compatible_input_json(json.dumps(current)) + + assert loaded.input_data == current + assert loaded.supplemented_paths == () + + +def test_current_rac_json_uses_defaults_for_its_equipment_branch() -> None: + inventory = load_legacy_inventory() + fields = {field.id: field for field in inventory.fields} + current = build_input_data({ + "H_A_type__0": fields["H_A_type__0"].choices[1], + "C_A_type__0": fields["C_A_type__0"].choices[1], + }) + + loaded = load_compatible_input_json(json.dumps(current)) + + assert loaded.input_data == current + assert loaded.supplemented_paths == () + + +def test_old_json_uses_current_defaults_only_for_missing_entries() -> None: + old_input = { + "case_name": "old-case", + "region": 2, + "H_A": {"type": 2}, + "legacy_extension": {"enabled": True}, + } + + loaded = load_compatible_input_json(json.dumps(old_input)) + + assert loaded.input_data["case_name"] == "old-case" + assert loaded.input_data["region"] == 2 + assert loaded.input_data["H_A"]["type"] == 2 + assert loaded.input_data["H_A"]["correct_no_general_ventilation_airflow"] == 1 + assert loaded.input_data["C_A"] == build_input_data({})["C_A"] + assert loaded.input_data["legacy_extension"] == {"enabled": True} + assert "C_A" in loaded.supplemented_paths + assert "H_A.correct_no_general_ventilation_airflow" in loaded.supplemented_paths + + +def test_pre_next_regression_json_is_accepted() -> None: + path = _ROOT / "packages/pyhees-jjj/src/test_utils/input_sample_type2.json" + original = json.loads(path.read_text(encoding="utf-8")) + + loaded = load_compatible_input_json(path.read_bytes()) + + assert loaded.input_data["case_name"] == original["case_name"] + assert loaded.input_data["H_A"]["type"] == original["H_A"]["type"] + assert loaded.input_data["H_A"]["q_rac_rtd_H"] == original["H_A"]["q_rac_rtd_H"] + assert loaded.input_data["C_A"]["type"] == original["C_A"]["type"] + assert loaded.input_data["H_A"]["correct_no_general_ventilation_airflow"] == 1 + assert loaded.supplemented_paths + + +@pytest.mark.parametrize("payload", ("[]", "null", '"input"')) +def test_input_json_requires_an_object_at_the_root(payload: str) -> None: + with pytest.raises(ValueError, match="最上位はオブジェクト"): + load_compatible_input_json(payload) + + +def test_input_json_reports_invalid_utf8_or_json() -> None: + with pytest.raises(ValueError, match="2行1列"): + load_compatible_input_json('{"case_name": "broken",\n}') + + with pytest.raises(ValueError, match="UTF-8"): + load_compatible_input_json(b"\x81") diff --git a/tests/test_release_version.py b/tests/test_release_version.py index 6ba0ed1..c8ce08e 100644 --- a/tests/test_release_version.py +++ b/tests/test_release_version.py @@ -13,9 +13,9 @@ def _project_version(path: str) -> str: def test_release_version_is_consistent_across_projects(): - assert release.VERSION == "1.3.1" - assert release.DISPLAY_VERSION == "ver.1.3.1" - assert release.ARTIFACT_VERSION == "_v1.3.1" + assert release.VERSION == "1.4.0" + assert release.DISPLAY_VERSION == "ver.1.4.0" + assert release.ARTIFACT_VERSION == "_v1.4.0" assert { _project_version("pyproject.toml"), _project_version("packages/verification-core/pyproject.toml"),