pytest coverage of Python source

[!NOTE] All paths are relative to <repo-root>/src/main/webapp/plugins/rdfexport/

pvzhelnov commented on Oct 26, 2025

This directory only contains pytest tests for the main legacy/draw_io_parser.py <-> pyodide_pipeline/drawio_pipeline.py Python source that actually gets embedded in dist/rdfexport.js to be used as a Draw.io plugin under Pyodide runtime. The Bun script bun run test relies on exactly these tests, running them through scripts/test_legacy.sh entrypoint.

pytest tests for dev modules like meta_builder/ and debug/ are located under meta_builder/tests/ and debug/tests/, respectively. Tests for meta_builder/ are also included in bun run test (again, through scripts/test_legacy.sh entrypoint) because building of legacy/draw_io_parser.py depends on metabuilder logic (executed using bun run build:py, also performed by the entrypoint). Tests for debug/, however, are not included in that script – instead, bun run test:pytest:all was designed to run all and any pytest tests found across the repo.

Salient points for test developers

Layer 0 – Original Records in Contexts parser for draw.io

This includes both the original post-v0.2.0 commit 5d85cf0 (May 13, 2024) version by Richard Williamson (backed up here in this repo) and a modified version frozen for meta_builder/ overrides. They are archived and should not be tested.

debug/ offers a within-Python round trip for regression testing that runs a given fixture through an older draw_io_parser.py extracted from an arbitrary historical commit. The commit it defaults to is not the original version from Richard Williamson’s release, but the core there is still largely intact.

Layer 1 – Low-level Python SDK

legacy/overrides/ and meta_builder/__main__.py provide patches and a bundling mechanism, respectively, for the modified, frozen version of legacy/original/draw_io_parser.py and are amply described here: meta_builder/readme.md

Tests that access legacy/draw_io_parser.py after it has been successfully built by metabuilder (e.g., through bun run build:py) can be found and written under legacy/tests/. It is a low-level SDK.

For instance:

# legacy/tests/test_patched_parser.py
LEGACY_DIR = Path(__file__).resolve().parents[1]
if str(LEGACY_DIR) not in sys.path:
    sys.path.insert(0, str(LEGACY_DIR))
import draw_io_parser
def test_curie_literal_style_rounding(tmp_path: Path):
    def parse(path: Path) -> Graph:
        return draw_io_parser.parse_drawio_to_graph(
            str(path), metacharacter_substitute=["url"]
        )

    prefixes = draw_io_parser.get_prefixes()
    expected_individual = URIRef(f"{prefixes['rdfs']}Address")

    def literal_present(graph: Graph, value: str) -> bool:
        return any(
            isinstance(obj, Literal) and str(obj) == value for obj in graph.objects()
        )

    base_graph = parse(FIXTURES_DIR / "Class_Diagram_tweaked.drawio")
    assert literal_present(base_graph, "Address")
    assert (expected_individual, RDF.type, OWL.NamedIndividual) not in base_graph

    curie_path = _write_class_diagram_variant(tmp_path, value="rdfs:Address")
    curie_graph = parse(curie_path)
    assert literal_present(curie_graph, "rdfs:Address")
    assert (expected_individual, RDF.type, OWL.NamedIndividual) in curie_graph

    rounded_path = _write_class_diagram_variant(
        tmp_path, value="rdfs:Address", rounded=1
    )
    rounded_graph = parse(rounded_path)
    assert literal_present(rounded_graph, "rdfs:Address")
    assert (
        expected_individual,
        RDF.type,
        OWL.NamedIndividual,
    ) not in rounded_graph, (
        "rounded=1 literal styling should suppress individual classification"
    )

Layer 2 – High-level Python SDK

legacy/tests/ features a neat workflow that allows roundtrip testing of the full Python cycle with just a few lines of code and without leaving Python – and could, thus, effectively be considered its higher-level SDK.

Consider this example:

# legacy/tests/test_pyodide_pipeline.py
def test_parse_drawio_respects_include_label_toggle() -> None:
    reset_graph_store()
    xml_payload = _load_fixture("AA37 Department of Health.drawio")

    _, graph_without_labels = parse_drawio_xml(xml_payload, {"include_label": False})
    without_count = sum(
        1 for _ in graph_without_labels.triples((None, RDFS.label, None))
    )
    assert without_count == 0

    reset_graph_store()
    _, graph_with_labels = parse_drawio_xml(xml_payload, {"include_label": True})
    with_count = sum(1 for _ in graph_with_labels.triples((None, RDFS.label, None)))
    assert with_count > 0

As a bit of context, functions in this snippet (except _load_fixture, which is a test-specific method that simply reads a file from tests/fixtures/ dir) come from pyodide_pipeline/drawio_pipeline.py module, which is a wrapper for the core legacy/draw_io_parser.py that ultimately gets invoked by TypeScript runtime. In particular, src/pyodideRuntime.ts module does the job:

After setting up paths, it invokes from pyodide_pipeline import reset_graph_store; reset_graph_store(), which unsets global graph vars, and then does this: from pyodide_pipeline.drawio_pipeline import parse_drawio_xml_to_json; import json; parse_drawio_xml_to_json(${JSON.stringify(serializedXml)}, json.loads(${JSON.stringify(configJson)}) – and “the returned promise will resolve to the value of this expression” (quote from Pyodide docs).

Layers 3 & 4 – Python/TypeScript CLI and REPL

In contrast, tooling from debug/ exposes a comprehensive roundtrip suite that goes beyond within-Python testing and actually runs inputs through the outer TypeScript layers, of which there are two:

Layer 3: TypeScript/Pyodide wrapper exposing mockBlackBoxModule.runDrawioPipeline from src/mockBlackBox.ts.

Layer 4: The outermost source layer – plugin export hooks defined in src/rdfexport.ts, main plugin source file.

debug/ triggers both layers using a custom debug/run_scenario.ts harness. For the plugin layer, it effectively simulates Draw.io logic by invoking exportRdfXml or exportRml plugin action directly.

Of note, bun run test includes a Bun test suite (from tests/rdfexport.test.ts) that runs its own implementation of Layer 0–3–4 roundtrips for all-fixture regression tests. debug/ specializes in manual/injections but also has a runner (slow) that does all fixtures at once (debug/debug_cli_regression.py).

Layer 5 – Production plugin

scripts/build.ts transpiles src/rdfexport.ts and bundles it together with legacy/draw_io_parser.py and pyodide_pipeline/drawio_pipeline.py code and base64-encoded Python dependency wheels (see the list here: scripts/download_pyodide_assets.sh) to produce dist/rdfexport.js. When run via bun run build:ts, this also conveniently copies the distributable one level higher to ultimately become available to the Draw.io app for import.

After this, a static server can be run to serve precompiled Draw.io application files (e.g., using bun run serve that just starts a default Python HTTP server). The app is available under http://localhost:8000/src/main/webapp/, and the plugin can be accessed by adding ?p=rdf to the URL or through the app settings.

I have not implemented E2E testing (e.g., using Playwright) because Codex does not seem to support it well. It has also been quite enjoyable to just test comprehensively up until Layer 4 and then run sanity checks manually in the browser, especially since the application is highly graphical in nature. However, I recognize that browser automation is still a consideration for apt testers.