diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36f6518b..8aea4504 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,6 +80,16 @@ jobs: - name: Pytest run: uv run --no-sync pytest test/ --ignore=test/examples/test_soc.py --cov=chipcompiler --cov-report= + - name: Upload integration flow logs on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: integration-flow-logs + if-no-files-found: ignore + path: | + test/examples/*/home/flow.json + test/examples/**/log/** + - name: Publish coverage summary if: always() run: | diff --git a/chipcompiler/data/home.py b/chipcompiler/data/home.py index 1a370fec..9e606760 100644 --- a/chipcompiler/data/home.py +++ b/chipcompiler/data/home.py @@ -9,43 +9,12 @@ from .checklist import Checklist -# home_json = { -# "parameters" : "", -# "flow" : "", -# "layout" : "", -# "monitor" : { -# "step" : [], -# "memory" : [], -# "runtime" : [], -# "instance" : [], -# "frequency" : [] -# }, -# "metrics":{ -# "instance dist." : "", -# "layer via dist." : "", -# "layer wire dist." : "", -# "pin dist." : "", -# "drc dist." : "", -# "CTS skew map" : "" -# }, -# "checklist" : "" -# } home_json = { "parameters": "", "flow": "", "layout": "", "checklist": "", "metrics": {}, - "monitor": {"step": [], "memory": [], "runtime": [], "instance": [], "frequency": []}, -} - -_monitor_keys = ("step", "memory", "runtime", "instance", "frequency") -_monitor_defaults = { - "step": "", - "memory": "", - "runtime": "", - "instance": 0, - "frequency": 0.0, } @@ -59,33 +28,20 @@ def _normalize_home_data(data: dict) -> tuple[dict, bool]: if isinstance(data, dict): for key, value in data.items(): + if key == "monitor": + changed = True + continue normalized[key] = value if not isinstance(normalized.get("metrics"), dict): normalized["metrics"] = {} changed = True - if not isinstance(normalized.get("monitor"), dict): - normalized["monitor"] = _default_home_data()["monitor"] - changed = True - for key in home_json: if key not in normalized: normalized[key] = _default_home_data()[key] changed = True - for key in _monitor_keys: - if not isinstance(normalized["monitor"].get(key), list): - normalized["monitor"][key] = [] - changed = True - - monitor_length = max(len(normalized["monitor"][key]) for key in _monitor_keys) - for key in _monitor_keys: - missing_count = monitor_length - len(normalized["monitor"][key]) - if missing_count > 0: - normalized["monitor"][key].extend([_monitor_defaults[key]] * missing_count) - changed = True - if isinstance(data, dict) and normalized != data: changed = True @@ -204,51 +160,6 @@ def set_metrics_drc_dist(self, image_path: Path): def set_metrics_cts_skew_map(self, image_path: Path): self._set_metric("CTS skew map", image_path) - def update_monitor( - self, - step: str, - sub_step: str, - memory: str, - runtime: str, - instance: int = 0, - frequency: float = 0.0, - ): - def mutator(data: dict) -> bool: - target_instance = instance - target_frequency = frequency - - # if not set, use last value - if target_instance == 0: - instance_values = data["monitor"]["instance"] - target_instance = instance_values[-1] if len(instance_values) > 0 else 0 - if target_frequency == 0.0: - frequency_values = data["monitor"]["frequency"] - target_frequency = frequency_values[-1] if len(frequency_values) > 0 else 0.0 - - step_name = f"{step} - {sub_step}" - for i, existing_step in enumerate(data["monitor"]["step"]): - if existing_step == step_name: - changed = ( - data["monitor"]["memory"][i] != memory - or data["monitor"]["runtime"][i] != runtime - or data["monitor"]["instance"][i] != target_instance - or data["monitor"]["frequency"][i] != target_frequency - ) - data["monitor"]["memory"][i] = memory - data["monitor"]["runtime"][i] = runtime - data["monitor"]["instance"][i] = target_instance - data["monitor"]["frequency"][i] = target_frequency - return changed - - data["monitor"]["step"].append(step_name) - data["monitor"]["memory"].append(memory) - data["monitor"]["runtime"].append(runtime) - data["monitor"]["instance"].append(target_instance) - data["monitor"]["frequency"].append(target_frequency) - return True - - self._update(mutator) - def set_checklist(self, checklist_path: Path): path = checklist_path if not path.exists(): diff --git a/chipcompiler/data/workspace/__init__.py b/chipcompiler/data/workspace/__init__.py index 95482d3c..5b534366 100644 --- a/chipcompiler/data/workspace/__init__.py +++ b/chipcompiler/data/workspace/__init__.py @@ -396,6 +396,11 @@ def _flag_to_int(value: Any) -> int: StepEnum.NETLIST_OPT.value, ("max_fanout",), ), + WorkspaceConfigParameterMapping( + "Max fanout", + StepEnum.CTS.value, + ("max_fanout",), + ), WorkspaceConfigParameterMapping( "Bottom layer", "db", @@ -733,8 +738,9 @@ def refresh_workspace_config(workspace: Workspace) -> None: f"Netlist opt config missing or corrupt: " f"{workspace.config[f'{StepEnum.NETLIST_OPT.value}']}" ) + max_fanout = workspace.parameters.data.get("Max fanout", 32) fixfanout["insert_buffer"] = workspace.pdk.buffers[0] if len(workspace.pdk.buffers) > 0 else "" - fixfanout["max_fanout"] = workspace.parameters.data.get("Max fanout", 32) + fixfanout["max_fanout"] = max_fanout json_write(workspace.config[f"{StepEnum.NETLIST_OPT.value}"], fixfanout) filler_path = workspace.config[f"{StepEnum.FILLER.value}"] @@ -754,6 +760,7 @@ def refresh_workspace_config(workspace: Workspace) -> None: f"CTS config missing or corrupt: {workspace.config[f'{StepEnum.CTS.value}']}" ) cts["buffer_type"] = workspace.pdk.buffers + cts["max_fanout"] = max_fanout json_write(workspace.config[f"{StepEnum.CTS.value}"], cts) router = json_read(workspace.config[f"{StepEnum.ROUTING.value}"]) diff --git a/chipcompiler/engine/signoff.py b/chipcompiler/engine/signoff.py index cad96c3f..678bf65a 100644 --- a/chipcompiler/engine/signoff.py +++ b/chipcompiler/engine/signoff.py @@ -199,10 +199,19 @@ def add_file( ) db_config = self._read_json(config_dir / "db_ecc.json") - configured_filelist = getattr(self.workspace.design, "input_filelist", None) + flow_starts_at_floorplan = self._flow_starts_at_floorplan(flow_data) + configured_filelist = ( + None + if flow_starts_at_floorplan + else getattr(self.workspace.design, "input_filelist", None) + ) origin_rtl = resolve_initial_rtl( configured_filelist, - getattr(self.workspace.design, "origin_verilog", None), + ( + None + if flow_starts_at_floorplan + else getattr(self.workspace.design, "origin_verilog", None) + ), workspace_dir / "origin", ) if origin_rtl is not None: @@ -273,8 +282,10 @@ def add_file( # Like synthesis, a configured input_filelist is always a filelist; # runtime-created ones are suffixless (origin/filelist), so the # suffix check alone would miss them. - is_filelist = origin_rtl.suffix.lower() in FILELIST_SUFFIXES or ( - configured_filelist is not None and origin_rtl == Path(configured_filelist) + is_filelist = ( + origin_rtl.suffix.lower() in FILELIST_SUFFIXES + or origin_rtl.name == "filelist" + or (configured_filelist is not None and origin_rtl == Path(configured_filelist)) ) if is_filelist: # Workspace creation copies filelist sources into origin/ keeping @@ -354,13 +365,14 @@ def add_file( required=True, ) - synthesis_verilog = self._synthesis_output_verilog() - add_file( - role="synthesis.verilog", - source=synthesis_verilog, - destination=f"synthesis/{design}.v.gz", - required=True, - ) + if not flow_starts_at_floorplan: + synthesis_verilog = self._synthesis_output_verilog() + add_file( + role="synthesis.verilog", + source=synthesis_verilog, + destination=f"synthesis/{design}.v.gz", + required=True, + ) add_file( role="final.design.verilog", @@ -557,7 +569,6 @@ def add_file( "sdc": f"initial/{design}.sdc", "parameters": "initial/parameters.json", }, - "synthesis": {"verilog": f"synthesis/{design}.v.gz"}, "config": "config/", "harden": { "gds": f"harden/{design}.gds", @@ -576,6 +587,8 @@ def add_file( "missing_optional": missing_optional, "warnings": warnings, } + if not flow_starts_at_floorplan: + summary["synthesis"] = {"verilog": f"synthesis/{design}.v.gz"} summary_path = package_dir / "summary.json" manifest = { @@ -602,13 +615,19 @@ def add_file( manifest_path.write_text(json.dumps(manifest, indent=2)) readme_path = package_dir / "README.md" + input_verilog_description = "- Mapped synthesis netlist is under `synthesis/`.\n" + if flow_starts_at_floorplan: + input_verilog_description = ( + "- Original imported RTL is under `initial/` because this flow " + "starts at Floorplan.\n" + ) readme_path.write_text( f"# {design} Signoff Package\n\n" - f"- Workspace: {workspace_dir.resolve()}\n" - f"- Status: {summary['status']}\n" - "- Mapped synthesis netlist is under `synthesis/`.\n" - "- Harden outputs are under `harden/`.\n" - "- Final physical resources are under `final/`.\n" + + f"- Workspace: {workspace_dir.resolve()}\n" + + f"- Status: {summary['status']}\n" + + input_verilog_description + + "- Harden outputs are under `harden/`.\n" + + "- Final physical resources are under `final/`.\n" ) if options.archive and (ok or options.allow_incomplete): @@ -874,6 +893,20 @@ def _required_step_states(self, flow_data: dict) -> dict: } return {step: state_by_step.get(step, "") for step in required} + def _flow_starts_at_floorplan(self, flow_data: dict) -> bool: + """Whether this workspace intentionally omits synthesis before Floorplan.""" + steps = flow_data.get("steps", []) + if not isinstance(steps, list): + return False + first_step = next( + (step for step in steps if isinstance(step, dict) and step.get("name")), + None, + ) + return bool( + first_step + and str(first_step.get("name", "")).strip().lower() == StepEnum.FLOORPLAN.value.lower() + ) + def _refresh_workspace_analysis(self, workspace_dir: Path) -> list[SignoffPackageIssue]: """Rebuild current V3 analysis and checklist snapshots for completed steps.""" flow_data = self.workspace.flow.data or self._read_json( diff --git a/chipcompiler/thirdparty/ecc-tools b/chipcompiler/thirdparty/ecc-tools index a7344be1..c9093e7b 160000 --- a/chipcompiler/thirdparty/ecc-tools +++ b/chipcompiler/thirdparty/ecc-tools @@ -1 +1 @@ -Subproject commit a7344be1c39000b2ce701e12e483dd2428362058 +Subproject commit c9093e7b8c9d6903a559a7d8459102279d5ce181 diff --git a/chipcompiler/tools/ecc/configs/fixfanout_ecc.json b/chipcompiler/tools/ecc/configs/fixfanout_ecc.json index 15c27523..37018d4f 100644 --- a/chipcompiler/tools/ecc/configs/fixfanout_ecc.json +++ b/chipcompiler/tools/ecc/configs/fixfanout_ecc.json @@ -1,14 +1,4 @@ { - "file_path": { - "design_work_space": "", - "sdc_file": "", - "lib_files": [], - "lef_files": [], - "def_file": "", - "output_def": "", - "report_file": "", - "gds_file": "" - }, "insert_buffer": "", "max_fanout": 30 } \ No newline at end of file diff --git a/chipcompiler/tools/ecc/module.py b/chipcompiler/tools/ecc/module.py index b0d21bb5..b732ca09 100644 --- a/chipcompiler/tools/ecc/module.py +++ b/chipcompiler/tools/ecc/module.py @@ -8,6 +8,10 @@ from chipcompiler.tools.ecc.sta_artifacts import discard_sta_run_outputs, publish_sta_artifacts from chipcompiler.utility.path import path_text, path_texts +# ecc-tools loggers terminate the host process on error by default; embedded in +# Python they must raise instead so failures surface as Python exceptions. +os.environ.setdefault("ECC_LOGGER_THROW_ON_ERROR", "1") + # Path arguments to the native-wrapper methods are normalized via path_text(), # so they accept a Path, a str, or None (a step group field is Path | None). PathArg: TypeAlias = str | Path | None diff --git a/chipcompiler/tools/ecc/subflow.py b/chipcompiler/tools/ecc/subflow.py index ba9509a4..d528539f 100644 --- a/chipcompiler/tools/ecc/subflow.py +++ b/chipcompiler/tools/ecc/subflow.py @@ -204,14 +204,4 @@ def update_step(self, step_name: str, state: str | StateEnum, info: dict | None publish_subflow_stage(self.workspace, self.workspace_step, step_dict) - # update home page monitor - self.workspace.home.update_monitor( - step=self.workspace_step.name, - sub_step=step_name, - memory=str(peak_memory), - runtime=runtime, - instance=info.get("instance", 0), - frequency=info.get("frequency", 0), - ) - break diff --git a/chipcompiler/tools/ecc_sizer/subflow.py b/chipcompiler/tools/ecc_sizer/subflow.py index efbc8a55..dfee297b 100644 --- a/chipcompiler/tools/ecc_sizer/subflow.py +++ b/chipcompiler/tools/ecc_sizer/subflow.py @@ -103,10 +103,4 @@ def update_step( publish_subflow_stage(self.workspace, self.workspace_step, step_dict) - self.workspace.home.update_monitor( - step=self.workspace_step.name, - sub_step=step_name, - memory=str(peak_memory), - runtime=runtime, - ) break diff --git a/chipcompiler/tools/yosys/subflow.py b/chipcompiler/tools/yosys/subflow.py index a62cce8b..eca2cf7b 100644 --- a/chipcompiler/tools/yosys/subflow.py +++ b/chipcompiler/tools/yosys/subflow.py @@ -113,12 +113,4 @@ def update_step(self, step_name: str, state: str | StateEnum, info: dict | None publish_subflow_stage(self.workspace, self.workspace_step, step_dict) - # update home page monitor - self.workspace.home.update_monitor( - step=self.workspace_step.name, - sub_step=step_name, - memory=str(peak_memory), - runtime=runtime, - ) - break diff --git a/test/data/test_home_data.py b/test/data/test_home_data.py index 7689571f..c4031547 100644 --- a/test/data/test_home_data.py +++ b/test/data/test_home_data.py @@ -15,15 +15,12 @@ def test_init_writes_complete_schema_for_missing_file(tmp_path): home = HomeData() home.init(path) - data = _read_json(path) - assert data["layout"] == "" - assert data["metrics"] == {} - assert data["monitor"] == { - "step": [], - "memory": [], - "runtime": [], - "instance": [], - "frequency": [], + assert _read_json(path) == { + "parameters": "", + "flow": "", + "layout": "", + "checklist": "", + "metrics": {}, } @@ -42,104 +39,18 @@ def test_init_repairs_partial_home_json_preserving_existing_values(tmp_path): home = HomeData() home.init(path) - data = _read_json(path) - assert data["flow"] == "/ws/home/flow.json" - assert data["checklist"] == "/ws/home/checklist.json" - assert data["parameters"] == "/ws/home/parameters.json" - assert data["layout"] == "" - assert data["metrics"] == {} - assert data["monitor"] == { - "step": [], - "memory": [], - "runtime": [], - "instance": [], - "frequency": [], + assert _read_json(path) == { + "parameters": "/ws/home/parameters.json", + "flow": "/ws/home/flow.json", + "layout": "", + "checklist": "/ws/home/checklist.json", + "metrics": {}, } -def test_update_monitor_repairs_partial_home_json(tmp_path): - path = tmp_path / "home.json" - path.write_text(json.dumps({"metrics": {}})) - - home = HomeData() - home.init(path) - home.update_monitor( - step="Floorplan", - sub_step="place", - memory="12M", - runtime="3s", - instance=42, - frequency=100.0, - ) - - data = _read_json(path) - assert data["monitor"]["step"] == ["Floorplan - place"] - assert data["monitor"]["memory"] == ["12M"] - assert data["monitor"]["runtime"] == ["3s"] - assert data["monitor"]["instance"] == [42] - assert data["monitor"]["frequency"] == [100.0] - - -def test_update_monitor_repairs_short_monitor_columns_preserving_history(tmp_path): - path = tmp_path / "home.json" - path.write_text( - json.dumps( - { - "monitor": { - "step": ["Floorplan - place"], - "memory": [], - "runtime": [], - "instance": [], - "frequency": [], - } - } - ) - ) - - home = HomeData() - home.init(path) - data = _read_json(path) - assert data["monitor"]["step"] == ["Floorplan - place"] - assert data["monitor"]["memory"] == [""] - assert data["monitor"]["runtime"] == [""] - assert data["monitor"]["instance"] == [0] - assert data["monitor"]["frequency"] == [0.0] - - home.update_monitor( - step="Floorplan", - sub_step="place", - memory="12M", - runtime="3s", - instance=42, - frequency=100.0, - ) - - data = _read_json(path) - assert data["monitor"]["step"] == ["Floorplan - place"] - assert data["monitor"]["memory"] == ["12M"] - assert data["monitor"]["runtime"] == ["3s"] - assert data["monitor"]["instance"] == [42] - assert data["monitor"]["frequency"] == [100.0] - - -def test_instances_do_not_share_nested_monitor_lists(tmp_path): - first_path = tmp_path / "first.json" - second_path = tmp_path / "second.json" - - first = HomeData() - first.init(first_path) - second = HomeData() - second.init(second_path) - - first.update_monitor("Synthesis", "yosys", "10M", "1s") - - assert _read_json(first_path)["monitor"]["step"] == ["Synthesis - yosys"] - assert _read_json(second_path)["monitor"]["step"] == [] - - def test_set_metrics_repairs_missing_metrics(tmp_path): path = tmp_path / "home.json" - path.write_text(json.dumps({"monitor": {"step": []}})) + path.write_text(json.dumps({})) home = HomeData() home.init(path) @@ -147,7 +58,6 @@ def test_set_metrics_repairs_missing_metrics(tmp_path): data = _read_json(path) assert data["metrics"]["pin dist."] == "/tmp/pin.png" - assert data["monitor"]["step"] == [] def test_setters_do_not_rewrite_healthy_current_values(tmp_path): @@ -187,25 +97,16 @@ def _set_parameters(path, value): home.set_parameters(value) -def _update_monitor(path): - home = HomeData() - home.init(path) - home.update_monitor("Floorplan", "place", "12M", "3s", instance=42, frequency=100.0) - - -def test_concurrent_home_updates_preserve_schema_and_monitor_rows(tmp_path): +def test_concurrent_home_updates_preserve_schema(tmp_path): path = tmp_path / "home.json" home = HomeData() home.init(path) home.set_layout(Path("/ws/Floorplan_ecc/output/layout.png")) home.set_metrics_pin_dist(Path("/ws/Floorplan_ecc/output/pin.png")) - home.update_monitor("Synthesis", "yosys", "10M", "1s", instance=10, frequency=50.0) - processes = [ Process(target=_set_flow, args=(path, Path("/ws/home/flow.json"))), Process(target=_set_checklist, args=(path, Path("/ws/home/checklist.json"))), Process(target=_set_parameters, args=(path, Path("/ws/home/parameters.json"))), - Process(target=_update_monitor, args=(path,)), ] for process in processes: @@ -213,12 +114,10 @@ def test_concurrent_home_updates_preserve_schema_and_monitor_rows(tmp_path): for process in processes: process.join(timeout=10) - assert [process.exitcode for process in processes] == [0, 0, 0, 0] + assert [process.exitcode for process in processes] == [0, 0, 0] data = _read_json(path) assert data["layout"] == "/ws/Floorplan_ecc/output/layout.png" assert data["metrics"]["pin dist."] == "/ws/Floorplan_ecc/output/pin.png" - assert data["monitor"]["step"] == ["Synthesis - yosys", "Floorplan - place"] - assert data["monitor"]["memory"] == ["10M", "12M"] assert data["flow"] == "/ws/home/flow.json" assert data["checklist"] == "/ws/home/checklist.json" assert data["parameters"] == "/ws/home/parameters.json" diff --git a/test/data/test_workspace.py b/test/data/test_workspace.py index 6d13f865..0a29c615 100644 --- a/test/data/test_workspace.py +++ b/test/data/test_workspace.py @@ -77,7 +77,7 @@ def test_create_workspace_returns_path_fields_and_persists_string_paths( origin_def="", origin_verilog=rtl_path, pdk="ics55", - parameters=default_ics55_parameters, + parameters={**default_ics55_parameters, "Max fanout": 37}, pdk_root=pdk_root, ) @@ -101,6 +101,10 @@ def test_create_workspace_returns_path_fields_and_persists_string_paths( assert flow_config["ConfigPath"]["idb_path"] == str(workspace.config["db"]) assert isinstance(flow_config["ConfigPath"]["idb_path"], str) + cts = json_read(workspace.config[StepEnum.CTS.value]) + assert cts["max_fanout"] == 37 + assert cts["buffer_type"] == workspace.pdk.buffers + def test_create_workspace_rejects_existing_non_empty_directory(tmp_path): workspace_dir = tmp_path / "workspace" @@ -724,6 +728,10 @@ def test_refresh_workspace_config_updates_all_parameter_derived_fields( params["Routability opt flag"] = 0 json_write(parameter_path, params) + cts = json_read(workspace.config[StepEnum.CTS.value]) + cts["skew_bound"] = "0.13" + json_write(workspace.config[StepEnum.CTS.value], cts) + refresh_workspace_config(workspace) fixfanout = json_read(workspace.config["fixFanout"]) @@ -731,9 +739,13 @@ def test_refresh_workspace_config_updates_all_parameter_derived_fields( db = json_read(workspace.config["db"]) floorplan = json_read(workspace.config[StepEnum.FLOORPLAN.value]) routing = json_read(workspace.config["route"]) + cts = json_read(workspace.config[StepEnum.CTS.value]) dreamplace = json_read(workspace.config["dreamplace"]) assert fixfanout["max_fanout"] == 91 + assert cts["max_fanout"] == 91 + assert cts["buffer_type"] == workspace.pdk.buffers + assert cts["skew_bound"] == "0.13" assert filler == {"-min_filler_width": 1} assert db["LayerSettings"]["routing_layer_1st"] == "MET3" assert routing["RT"]["-bottom_routing_layer"] == "MET3" @@ -837,6 +849,31 @@ def test_sync_workspace_config_to_parameters_updates_routing_layers_and_refreshe assert db["LayerSettings"]["routing_layer_1st"] == "MET4" +def test_sync_workspace_config_to_parameters_propagates_cts_max_fanout( + tmp_path, minimal_ics55_pdk_factory, default_ics55_parameters +): + workspace_dir, workspace = _create_loaded_ics55_workspace( + tmp_path, + "workspace_cts_max_fanout", + minimal_ics55_pdk_factory, + default_ics55_parameters, + ) + cts_path = workspace.config[StepEnum.CTS.value] + cts = json_read(cts_path) + cts["max_fanout"] = 48 + json_write(cts_path, cts) + + assert sync_workspace_config_to_parameters(workspace, cts_path) is True + refresh_workspace_config(workspace) + + parameters = json_read(workspace_dir / "home" / "parameters.json") + fixfanout = json_read(workspace.config[StepEnum.NETLIST_OPT.value]) + cts = json_read(cts_path) + assert parameters["Max fanout"] == 48 + assert fixfanout["max_fanout"] == 48 + assert cts["max_fanout"] == 48 + + def test_sync_workspace_config_to_parameters_preserves_routability_flag_string_coercion( tmp_path, minimal_ics55_pdk_factory, default_ics55_parameters ): @@ -929,13 +966,6 @@ def test_prepare_workspace_for_rerun_deletes_old_artifacts_and_resets_home_state home = json_read(home_path) home["layout"] = str(step_dir / "output" / "gcd_floorplan.png") home["metrics"] = {"instances dist.": str(step_dir / "feature" / "floorplan.db.inst_dist.png")} - home["monitor"] = { - "step": ["Floorplan - init"], - "memory": ["1"], - "runtime": ["2"], - "instance": [3], - "frequency": [4.0], - } json_write(home_path, home) flow_path = workspace_dir / "home" / "flow.json" @@ -1033,7 +1063,6 @@ def create_step_workspaces(self): assert reset_home["checklist"] == str(checklist_path) assert reset_home["layout"] == "" assert reset_home["metrics"] == {} - assert reset_home["monitor"]["step"] == [] reset_flow = json_read(flow_path) assert reset_flow["steps"][0]["state"] == "Unstart" diff --git a/test/test_signoff_package.py b/test/test_signoff_package.py index 17849d5c..037593f6 100644 --- a/test/test_signoff_package.py +++ b/test/test_signoff_package.py @@ -255,6 +255,37 @@ def test_collect_signoff_package_requires_synthesis_verilog(tmp_path): ) +def test_collect_signoff_package_uses_origin_rtl_for_floorplan_start(tmp_path): + workspace_dir = _make_signoff_workspace(tmp_path) + (workspace_dir / "Synthesis_yosys" / "output" / "gcd_Synthesis.v.gz").unlink() + _write( + workspace_dir / "origin" / "gcd.v", + "module gcd; // original imported RTL\nendmodule\n", + ) + flow = json.loads((workspace_dir / "home" / "flow.json").read_text()) + flow["steps"].insert( + 0, + {"name": "Floorplan", "tool": "ecc", "state": StateEnum.Success.value}, + ) + _write_json(workspace_dir / "home" / "flow.json", flow) + engine_flow = _make_engine_flow(workspace_dir) + external_rtl = tmp_path / "external" / "gcd.v" + _write(external_rtl, "module gcd; // outside the workspace\nendmodule\n") + engine_flow.workspace.design.origin_verilog = external_rtl + + result = engine_flow.collect_signoff_package(SignoffPackageOptions(archive=True)) + + assert result.ok is True + package_dir = Path(result.package_dir) + assert (package_dir / "initial" / "gcd.v").read_text() == ( + "module gcd; // original imported RTL\nendmodule\n" + ) + assert not (package_dir / "synthesis" / "gcd.v.gz").exists() + summary = json.loads((package_dir / "summary.json").read_text()) + assert summary["initial"]["verilog"] == "initial/gcd.v" + assert "synthesis" not in summary + + def test_collect_signoff_package_tolerates_missing_sta_power_report(tmp_path): # Workspaces completed before power collection have no per-corner power.rpt; # it is packaged when present but must not be required for export.