Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

py/tests/test_pane_guards.py

"""Guards: the facett-component panes must render through the wasm bridge.

These are anti-regression tests for the specific failure that produced this rewrite —
a Streamlit demo that *looked* like facett but was a parallel pydeck/plotly
re-implementation, which then drifted (a CARTO world basemap made a correctly-clipped
Sweden appear to include Africa).

The rule they encode:

    A pane that shows a facett COMPONENT must embed the real component through
    `facet_host` — this app's thin mount of the pip-installed `facett`
    wheel. No pane may render domain data with a Python renderer.

(The bridge used to be a `facett_embed` package built out of the source tree. It is
gone: the components now ship as the `facett` WHEEL and are mounted via
`facett_demo.facet_host`, so the demo exercises what a user pip-installs.
These guards were left asserting the dead name and passed vacuously on nothing.)
"""

from __future__ import annotations

import ast
from pathlib import Path

import pytest

import facett_demo

PANES = Path(facett_demo.__file__).resolve().parent / "panes"

#: Panes that embed a facett component — these must NOT contain a second renderer.
#: EVERY pane that renders domain data belongs here. There is no exemption for
#: "it's only a table" or "it's only a chart": facett owns facett-table, facett-grid,
#: facett-stat, facett-plot, facett-plotters, facett-matrix, facett-icebergview and
#: facett-warehousedeck, and both korp and dwarves depend on them. The population pane
#: was exempted once and immediately became a third pandas/plotly lookalike.
COMPONENT_PANES = {
    "map_pane.py": "osm2d",
    "graph_pane.py": "graphnav",
    "population_pane.py": "table",
    "errcode_pane.py": "table",
}

#: Renderers that would constitute a lookalike re-implementation of a facett component.
BANNED_RENDERERS = {"pydeck", "plotly", "networkx", "altair", "folium", "matplotlib", "bokeh"}

#: Panes exempt from the renderer ban. Empty ON PURPOSE — see COMPONENT_PANES. If a
#: pane renders domain data, a facett component renders it. Python is allowed only the
#: gRPC plumbing, the layout, and the controls.
ANALYTICS_PANES: set[str] = set()


def _imported_modules(path: Path) -> set[str]:
    """Top-level module names imported by a source file."""
    tree = ast.parse(path.read_text(), filename=str(path))
    mods: set[str] = set()
    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            for a in node.names:
                mods.add(a.name.split(".")[0])
        elif isinstance(node, ast.ImportFrom):
            if node.module and node.level == 0:
                mods.add(node.module.split(".")[0])
    return mods


@pytest.mark.parametrize("filename", sorted(COMPONENT_PANES))
def test_component_pane_has_no_lookalike_renderer(filename: str):
    """A facett-component pane must not import a competing chart/map library."""
    path = PANES / filename
    assert path.exists(), f"{filename} missing"
    offenders = _imported_modules(path) & BANNED_RENDERERS
    assert not offenders, (
        f"{filename} imports {sorted(offenders)} — that is a SECOND renderer competing "
        "with the real facett component. Mount the component via `facet_host` instead."
    )


@pytest.mark.parametrize("filename,component", sorted(COMPONENT_PANES.items()))
def test_component_pane_embeds_the_real_component(filename: str, component: str):
    """It must actually go through the wasm bridge, naming the registry component."""
    src = (PANES / filename).read_text()
    assert "facet_host" in src, f"{filename} must mount through `facet_host`"
    assert "facett_component(" in src, f"{filename} must call facett_component()"
    assert component in src, f"{filename} must embed the `{component}` component"


def test_no_pane_imports_the_removed_source_tree_bridge():
    """`facett_embed` is gone. A pane (or guard) still naming it is dead code."""
    stale = [p.name for p in sorted(PANES.glob("*.py")) if "facett_embed" in p.read_text()]
    assert not stale, f"{stale} reference the removed `facett_embed` bridge"


def test_the_shipped_wheel_exposes_the_data_driven_components_the_panes_need():
    """The INSTALLED wheel must offer every pane's component AS LIVE-DATA.

    If this fails, the wasm bundle is missing or was built from a fixture-only
    registry — the exact condition under which a lookalike gets hand-rolled. It is
    asserted against the pip-installed package, not the source tree, because that
    is what the demo actually mounts.
    """
    fw = pytest.importorskip("facett")
    names = {c["name"] for c in fw.manifest()}
    assert names, "no manifest — run `cargo xtask wrap-wasm` && `python make_wheel.py` to build the wheel"
    assert fw.build_info()["wasm_present"], "the installed wheel ships no wasm"
    for component in set(COMPONENT_PANES.values()):
        assert component in names, f"`{component}` missing from the facet-wrapped registry"
        assert fw.is_data_driven(component), (
            f"`{component}` is registered but NOT data-driven — it could only ever show a "
            "demo fixture, which is what caused the pydeck/plotly lookalikes."
        )


def test_no_pane_renders_domain_data_in_python():
    """The absolute rule, swept over EVERY pane — not just the ones we remembered.

    Any pane importing a chart/map/table renderer, or calling Streamlit's own data
    renderers, is drawing domain data in Python instead of mounting the facett
    component that already exists for it.
    """
    banned_calls = ("st.dataframe(", "st.table(", "st.plotly_chart(", "st.pydeck_chart(",
                    "st.altair_chart(", "st.bar_chart(", "st.line_chart(", "st.map(")
    for path in sorted(PANES.glob("*_pane.py")):
        if path.name in ANALYTICS_PANES:
            continue
        mods = _imported_modules(path)
        offenders = mods & BANNED_RENDERERS
        assert not offenders, f"{path.name} imports a second renderer: {sorted(offenders)}"
        src = path.read_text()
        used = [c for c in banned_calls if c in src]
        assert not used, (
            f"{path.name} renders domain data with Streamlit's own widgets {used} — "
            "mount the facett component instead (table / stat-grid / plot / osm2d / graphnav)."
        )