2026-04-22 21:24:05 -07:00
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
from ..builder import Pather
|
|
|
|
|
from ..error import BuildError
|
2026-06-19 21:04:18 -07:00
|
|
|
from ..library import BuildLibrary, Library, cell
|
2026-04-22 21:24:05 -07:00
|
|
|
from ..pattern import Pattern
|
|
|
|
|
from ..ports import Port
|
|
|
|
|
|
|
|
|
|
|
2026-06-19 21:04:18 -07:00
|
|
|
def _owned_by(report, owner: str) -> set[str]:
|
|
|
|
|
return {
|
|
|
|
|
name for name, prov in report.provenance.items()
|
|
|
|
|
if prov.owner_declared_name == owner
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-04-22 21:24:05 -07:00
|
|
|
def test_build_library_traces_declared_dependencies_out_of_order() -> None:
|
|
|
|
|
builder = BuildLibrary()
|
|
|
|
|
|
|
|
|
|
def make_parent(lib: BuildLibrary) -> Pattern:
|
|
|
|
|
pat = Pattern()
|
|
|
|
|
pat.ref("child")
|
|
|
|
|
assert lib.abstract("child").name == "child"
|
|
|
|
|
return pat
|
|
|
|
|
|
|
|
|
|
builder.cells.parent = cell(make_parent)(builder)
|
|
|
|
|
builder["child"] = Pattern(ports={"p": Port((0, 0), 0)})
|
|
|
|
|
|
2026-06-19 21:04:18 -07:00
|
|
|
built, report = builder.build()
|
2026-04-22 21:24:05 -07:00
|
|
|
|
|
|
|
|
assert "parent" in built
|
|
|
|
|
assert "child" in built
|
2026-06-19 21:04:18 -07:00
|
|
|
assert report.dependency_graph["parent"] == frozenset({"child"})
|
|
|
|
|
assert report.provenance["parent"].kind == "declared"
|
2026-04-22 21:24:05 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_library_tracks_helper_provenance_and_tree_merge_renames() -> None:
|
|
|
|
|
builder = BuildLibrary()
|
|
|
|
|
|
|
|
|
|
def make_top(lib: BuildLibrary) -> Pattern:
|
|
|
|
|
tree = Library({"_helper": Pattern()})
|
|
|
|
|
name_a = lib << tree
|
|
|
|
|
name_b = lib << tree
|
|
|
|
|
top = Pattern()
|
|
|
|
|
top.ref(name_a)
|
|
|
|
|
top.ref(name_b)
|
|
|
|
|
return top
|
|
|
|
|
|
|
|
|
|
builder.cells.top = cell(make_top)(builder)
|
2026-06-19 21:04:18 -07:00
|
|
|
_built, report = builder.build()
|
2026-04-22 21:24:05 -07:00
|
|
|
|
|
|
|
|
helpers = [
|
|
|
|
|
prov for prov in report.provenance.values()
|
|
|
|
|
if prov.owner_declared_name == "top" and prov.kind == "helper"
|
|
|
|
|
]
|
|
|
|
|
|
2026-06-19 21:04:18 -07:00
|
|
|
assert "top" in _owned_by(report, "top")
|
2026-04-22 21:24:05 -07:00
|
|
|
assert len(helpers) == 2
|
|
|
|
|
assert any(prov.renamed_from == "_helper" for prov in helpers)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_library_requires_build_session_for_reads_and_freezes_after_build() -> None:
|
|
|
|
|
builder = BuildLibrary()
|
|
|
|
|
builder["leaf"] = Pattern()
|
|
|
|
|
|
|
|
|
|
with pytest.raises(BuildError, match="validate\\(\\) or build\\(\\)"):
|
|
|
|
|
_ = builder["leaf"]
|
|
|
|
|
|
|
|
|
|
with pytest.raises(BuildError, match="write-only"):
|
|
|
|
|
_ = builder.cells.leaf
|
|
|
|
|
|
2026-06-19 21:04:18 -07:00
|
|
|
built, report = builder.build(output="library")
|
2026-04-22 21:24:05 -07:00
|
|
|
|
2026-06-19 21:04:18 -07:00
|
|
|
assert isinstance(built, Library)
|
|
|
|
|
assert report.requested_roots == ("leaf",)
|
2026-04-22 21:24:05 -07:00
|
|
|
|
|
|
|
|
with pytest.raises(BuildError, match="frozen"):
|
|
|
|
|
builder["later"] = Pattern()
|
|
|
|
|
with pytest.raises(BuildError, match="frozen"):
|
|
|
|
|
builder.build()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_library_validate_is_retryable_after_failure() -> None:
|
|
|
|
|
builder = BuildLibrary()
|
|
|
|
|
|
|
|
|
|
def make_parent(lib: BuildLibrary) -> Pattern:
|
|
|
|
|
pat = Pattern()
|
|
|
|
|
pat.ref("child")
|
|
|
|
|
lib.abstract("child")
|
|
|
|
|
return pat
|
|
|
|
|
|
|
|
|
|
builder.cells.parent = cell(make_parent)(builder)
|
|
|
|
|
|
|
|
|
|
with pytest.raises(BuildError, match='Failed while building declared cell "parent"'):
|
|
|
|
|
builder.validate()
|
|
|
|
|
|
|
|
|
|
builder["child"] = Pattern(ports={"p": Port((0, 0), 0)})
|
|
|
|
|
report = builder.validate()
|
|
|
|
|
|
|
|
|
|
assert report.dependency_graph["parent"] == frozenset({"child"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_library_depends_on_supports_hidden_dependencies_for_partial_validation() -> None:
|
|
|
|
|
builder = BuildLibrary()
|
|
|
|
|
builder["child"] = Pattern()
|
|
|
|
|
|
|
|
|
|
def make_parent() -> Pattern:
|
|
|
|
|
pat = Pattern()
|
|
|
|
|
pat.ref("child")
|
|
|
|
|
return pat
|
|
|
|
|
|
|
|
|
|
builder.cells.parent = cell(make_parent)().depends_on("child")
|
|
|
|
|
report = builder.validate(names=("parent",))
|
|
|
|
|
|
|
|
|
|
assert report.requested_roots == ("parent",)
|
|
|
|
|
assert report.dependency_graph["parent"] == frozenset({"child"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_library_validate_rejects_removed_output_argument() -> None:
|
|
|
|
|
builder = BuildLibrary()
|
|
|
|
|
builder["leaf"] = Pattern()
|
|
|
|
|
|
|
|
|
|
with pytest.raises(TypeError):
|
|
|
|
|
builder.validate(output="library") # type: ignore[call-arg]
|
|
|
|
|
|
|
|
|
|
|
2026-06-19 21:04:18 -07:00
|
|
|
def test_build_library_rejects_unknown_build_output_mode() -> None:
|
|
|
|
|
builder = BuildLibrary()
|
|
|
|
|
builder["leaf"] = Pattern()
|
|
|
|
|
|
|
|
|
|
with pytest.raises(ValueError, match="Unknown build output mode"):
|
|
|
|
|
builder.build(output="bad") # type: ignore[arg-type]
|
|
|
|
|
|
|
|
|
|
|
2026-04-22 21:24:05 -07:00
|
|
|
def test_build_library_allows_helper_writes_via_pather() -> None:
|
|
|
|
|
builder = BuildLibrary()
|
|
|
|
|
builder["leaf"] = Pattern(ports={"a": Port((0, 0), 0)})
|
|
|
|
|
|
|
|
|
|
def make_top(lib: BuildLibrary) -> Pattern:
|
|
|
|
|
helper = Pather(library=lib, ports="leaf", name="_route")
|
|
|
|
|
top = Pattern()
|
|
|
|
|
top.ref("_route")
|
|
|
|
|
top.ref("leaf")
|
|
|
|
|
top.ports.update(helper.pattern.ports)
|
|
|
|
|
return top
|
|
|
|
|
|
|
|
|
|
builder.cells.top = cell(make_top)(builder)
|
2026-06-19 21:04:18 -07:00
|
|
|
_built, report = builder.build()
|
2026-04-22 21:24:05 -07:00
|
|
|
|
2026-06-19 21:04:18 -07:00
|
|
|
helper_prov = report.provenance["_route"]
|
2026-04-22 21:24:05 -07:00
|
|
|
assert helper_prov.kind == "helper"
|
|
|
|
|
assert helper_prov.owner_declared_name == "top"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_library_preserves_source_cells_and_records_source_provenance() -> None:
|
|
|
|
|
source = Library({"src": Pattern()})
|
|
|
|
|
builder = BuildLibrary()
|
|
|
|
|
builder.add_source(source)
|
|
|
|
|
builder.cells.top = cell(lambda: Pattern())()
|
|
|
|
|
|
2026-06-19 21:04:18 -07:00
|
|
|
built, report = builder.build()
|
2026-04-22 21:24:05 -07:00
|
|
|
|
|
|
|
|
assert "src" in built
|
2026-06-19 21:04:18 -07:00
|
|
|
assert report.provenance["src"].kind == "source"
|
|
|
|
|
|
|
|
|
|
|
2026-06-19 22:48:21 -07:00
|
|
|
def test_build_library_add_source_can_rename_every_source_cell() -> None:
|
|
|
|
|
source = Library()
|
|
|
|
|
source["child"] = Pattern()
|
|
|
|
|
parent = Pattern()
|
|
|
|
|
parent.ref("child")
|
|
|
|
|
source["parent"] = parent
|
|
|
|
|
|
|
|
|
|
builder = BuildLibrary()
|
|
|
|
|
rename_map = builder.add_source(
|
|
|
|
|
source,
|
|
|
|
|
rename_theirs=lambda _lib, name: f"mapped_{name}",
|
|
|
|
|
rename_when="always",
|
|
|
|
|
)
|
|
|
|
|
built, report = builder.build()
|
|
|
|
|
|
|
|
|
|
assert rename_map == {
|
|
|
|
|
"child": "mapped_child",
|
|
|
|
|
"parent": "mapped_parent",
|
|
|
|
|
}
|
|
|
|
|
assert "mapped_child" in built["mapped_parent"].refs
|
|
|
|
|
assert report.provenance["mapped_child"].source_name == "child"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_library_rejects_source_cells_added_after_add_source() -> None:
|
|
|
|
|
source = Library({"src": Pattern()})
|
|
|
|
|
builder = BuildLibrary()
|
|
|
|
|
builder.add_source(source)
|
|
|
|
|
source["late"] = Pattern()
|
|
|
|
|
|
|
|
|
|
with pytest.raises(BuildError, match="Do not structurally mutate source libraries"):
|
|
|
|
|
builder.build()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_library_rejects_source_cells_removed_after_add_source() -> None:
|
|
|
|
|
source = Library({"src": Pattern()})
|
|
|
|
|
builder = BuildLibrary()
|
|
|
|
|
builder.add_source(source)
|
|
|
|
|
del source["src"]
|
|
|
|
|
|
|
|
|
|
with pytest.raises(BuildError, match="Do not structurally mutate source libraries"):
|
|
|
|
|
builder.build()
|
|
|
|
|
|
|
|
|
|
|
2026-06-19 21:04:18 -07:00
|
|
|
def test_build_library_rejects_add_source_during_build() -> None:
|
|
|
|
|
builder = BuildLibrary()
|
|
|
|
|
|
|
|
|
|
def make_top(lib: BuildLibrary) -> Pattern:
|
|
|
|
|
lib.add_source(Library({"src": Pattern()}))
|
|
|
|
|
return Pattern()
|
|
|
|
|
|
|
|
|
|
builder.cells.top = cell(make_top)(builder)
|
|
|
|
|
|
|
|
|
|
with pytest.raises(BuildError, match="add_source"):
|
|
|
|
|
builder.build()
|
2026-04-22 21:24:05 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_library_can_rename_imported_source_cells_during_authoring() -> None:
|
|
|
|
|
source = Library()
|
|
|
|
|
source["child"] = Pattern()
|
|
|
|
|
parent = Pattern()
|
|
|
|
|
parent.ref("child")
|
|
|
|
|
source["parent"] = parent
|
|
|
|
|
|
|
|
|
|
builder = BuildLibrary()
|
|
|
|
|
builder.add_source(source)
|
|
|
|
|
builder.rename("child", "renamed_child")
|
|
|
|
|
|
2026-06-19 21:04:18 -07:00
|
|
|
built, report = builder.build()
|
2026-04-22 21:24:05 -07:00
|
|
|
|
|
|
|
|
assert "renamed_child" in built
|
|
|
|
|
assert "child" not in built
|
|
|
|
|
assert "renamed_child" in built["parent"].refs
|
2026-06-19 21:04:18 -07:00
|
|
|
assert report.provenance["renamed_child"].source_name == "child"
|
2026-04-22 21:24:05 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_library_rejects_move_references_for_source_rename() -> None:
|
|
|
|
|
builder = BuildLibrary()
|
|
|
|
|
builder.add_source(Library({"src": Pattern()}))
|
|
|
|
|
|
|
|
|
|
with pytest.raises(BuildError, match="move_references=True"):
|
|
|
|
|
builder.rename("src", "renamed_src", move_references=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_library_rejects_renaming_declared_cells_during_authoring() -> None:
|
|
|
|
|
builder = BuildLibrary()
|
|
|
|
|
builder["declared"] = Pattern()
|
|
|
|
|
|
|
|
|
|
with pytest.raises(BuildError, match='Cannot rename declared build cell "declared"'):
|
|
|
|
|
builder.rename("declared", "renamed_declared")
|
|
|
|
|
|
|
|
|
|
|
2026-06-19 21:04:18 -07:00
|
|
|
def test_build_library_helper_rename_updates_provenance_owner() -> None:
|
2026-04-22 21:24:05 -07:00
|
|
|
builder = BuildLibrary()
|
|
|
|
|
|
|
|
|
|
def make_top(lib: BuildLibrary) -> Pattern:
|
|
|
|
|
lib["_helper"] = Pattern()
|
|
|
|
|
lib.rename("_helper", "final_helper")
|
|
|
|
|
top = Pattern()
|
|
|
|
|
top.ref("final_helper")
|
|
|
|
|
return top
|
|
|
|
|
|
|
|
|
|
builder.cells.top = cell(make_top)(builder)
|
2026-06-19 21:04:18 -07:00
|
|
|
built, report = builder.build()
|
2026-04-22 21:24:05 -07:00
|
|
|
|
|
|
|
|
assert "final_helper" in built
|
|
|
|
|
assert "_helper" not in built
|
2026-06-19 21:04:18 -07:00
|
|
|
owned = _owned_by(report, "top")
|
|
|
|
|
assert "final_helper" in owned
|
|
|
|
|
assert "_helper" not in owned
|
2026-04-22 21:24:05 -07:00
|
|
|
prov = report.provenance["final_helper"]
|
|
|
|
|
assert prov.kind == "helper"
|
|
|
|
|
assert prov.requested_name == "_helper"
|
|
|
|
|
assert prov.renamed_from == "_helper"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_library_helper_delete_removes_provenance_and_ownership() -> None:
|
|
|
|
|
builder = BuildLibrary()
|
|
|
|
|
|
|
|
|
|
def make_top(lib: BuildLibrary) -> Pattern:
|
|
|
|
|
lib["_helper"] = Pattern()
|
|
|
|
|
del lib["_helper"]
|
|
|
|
|
return Pattern()
|
|
|
|
|
|
|
|
|
|
builder.cells.top = cell(make_top)(builder)
|
2026-06-19 21:04:18 -07:00
|
|
|
built, report = builder.build()
|
2026-04-22 21:24:05 -07:00
|
|
|
|
|
|
|
|
assert "_helper" not in built
|
|
|
|
|
assert "_helper" not in report.provenance
|
2026-06-19 21:04:18 -07:00
|
|
|
assert _owned_by(report, "top") == {"top"}
|
2026-04-22 21:24:05 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_library_helper_rename_after_auto_rename_preserves_requested_name() -> None:
|
|
|
|
|
builder = BuildLibrary()
|
|
|
|
|
|
|
|
|
|
def make_top(lib: BuildLibrary) -> Pattern:
|
|
|
|
|
tree = Library({"_helper": Pattern()})
|
|
|
|
|
_ = lib << tree
|
|
|
|
|
renamed = lib << tree
|
|
|
|
|
lib.rename(renamed, "final_helper")
|
|
|
|
|
top = Pattern()
|
|
|
|
|
top.ref("_helper")
|
|
|
|
|
top.ref("final_helper")
|
|
|
|
|
return top
|
|
|
|
|
|
|
|
|
|
builder.cells.top = cell(make_top)(builder)
|
2026-06-19 21:04:18 -07:00
|
|
|
built, report = builder.build()
|
2026-04-22 21:24:05 -07:00
|
|
|
|
|
|
|
|
assert "final_helper" in built
|
|
|
|
|
prov = report.provenance["final_helper"]
|
|
|
|
|
assert prov.requested_name == "_helper"
|
|
|
|
|
assert prov.renamed_from == "_helper"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_library_rejects_renaming_declared_or_source_cells_during_build() -> None:
|
|
|
|
|
declared = BuildLibrary()
|
|
|
|
|
declared["leaf"] = Pattern()
|
|
|
|
|
|
|
|
|
|
def rename_declared(lib: BuildLibrary) -> Pattern:
|
|
|
|
|
lib.rename("leaf", "renamed_leaf")
|
|
|
|
|
return Pattern()
|
|
|
|
|
|
|
|
|
|
declared.cells.top = cell(rename_declared)(declared)
|
|
|
|
|
with pytest.raises(BuildError, match='Cannot rename declared build cell "leaf"'):
|
|
|
|
|
declared.build()
|
|
|
|
|
|
|
|
|
|
source = BuildLibrary()
|
|
|
|
|
source.add_source(Library({"src": Pattern()}))
|
|
|
|
|
|
|
|
|
|
def rename_source(lib: BuildLibrary) -> Pattern:
|
|
|
|
|
lib.rename("src", "renamed_src")
|
|
|
|
|
return Pattern()
|
|
|
|
|
|
|
|
|
|
source.cells.top = cell(rename_source)(source)
|
|
|
|
|
with pytest.raises(BuildError, match='Cannot rename imported source cell "src"'):
|
|
|
|
|
source.build()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_library_rejects_deleting_declared_or_source_cells_during_build() -> None:
|
|
|
|
|
declared = BuildLibrary()
|
|
|
|
|
declared["leaf"] = Pattern()
|
|
|
|
|
|
|
|
|
|
def delete_declared(lib: BuildLibrary) -> Pattern:
|
|
|
|
|
del lib["leaf"]
|
|
|
|
|
return Pattern()
|
|
|
|
|
|
|
|
|
|
declared.cells.top = cell(delete_declared)(declared)
|
|
|
|
|
with pytest.raises(BuildError, match='Cannot delete declared build cell "leaf"'):
|
|
|
|
|
declared.build()
|
|
|
|
|
|
|
|
|
|
source = BuildLibrary()
|
|
|
|
|
source.add_source(Library({"src": Pattern()}))
|
|
|
|
|
|
|
|
|
|
def delete_source(lib: BuildLibrary) -> Pattern:
|
|
|
|
|
del lib["src"]
|
|
|
|
|
return Pattern()
|
|
|
|
|
|
|
|
|
|
source.cells.top = cell(delete_source)(source)
|
|
|
|
|
with pytest.raises(BuildError, match='Cannot delete imported source cell "src"'):
|
|
|
|
|
source.build()
|