Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ruff.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ on:
- '.github/workflows/ruff.yml'
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

Expand Down
39 changes: 39 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Test

on:
pull_request:
paths:
- '**/*.py'
- 'examples/**'
- 'pyproject.toml'
- 'uv.lock'
- '.github/workflows/test.yaml'
push:
branches:
- main
paths:
- '**/*.py'
- 'examples/**'
- 'pyproject.toml'
- 'uv.lock'
- '.github/workflows/test.yaml'
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
python-version: "3.12"
- name: Run example transpile tests
run: uv run pytest examples/ -v
98 changes: 98 additions & 0 deletions examples/test_examples.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# SPDX-License-Identifier: MIT

"""Transpile every template in examples/, execute the emitted nodes, and
compare what they print against the expected values.

- ``quine`` the node prints its own exact source code;
- ``mutual_quine`` each node prints the SHA-256 of the other node's source;
- ``trinity_quine`` each node prints the SHA-256 of the next node's source,
in a 3-cycle.

Run with: ``uv run pytest examples/``
"""

import hashlib
import subprocess
import sys
from pathlib import Path

import pytest

from pyreflect import parse_template, transpile
from pyreflect.cli import node_filename

EXAMPLES_DIR = Path(__file__).resolve().parent
EXAMPLE_NAMES = sorted(p.parent.name for p in EXAMPLES_DIR.glob("*/template.json"))

EXPECTED_NODE_IDS = {
"quine": ["__NODE"],
"mutual_quine": ["__NODE1", "__NODE2"],
"trinity_quine": ["__NODE1", "__NODE2", "__NODE3"],
}

# Which peer each node hashes, as written in the template bodies.
HASH_TARGETS = {
"mutual_quine": {"__NODE1": "__NODE2", "__NODE2": "__NODE1"},
"trinity_quine": {
"__NODE1": "__NODE2",
"__NODE2": "__NODE3",
"__NODE3": "__NODE1",
},
}


@pytest.fixture(scope="session")
def transpiled(tmp_path_factory) -> dict[str, tuple[dict[str, str], Path]]:
"""Transpile every example once and write the emitted nodes to a temporary
directory; return ``{example_name: (nodes, outdir)}``."""
result = {}
for name in EXAMPLE_NAMES:
text = (EXAMPLES_DIR / name / "template.json").read_text(encoding="utf-8")
nodes = transpile(parse_template(text))
outdir = tmp_path_factory.mktemp(name)
for nid, src in nodes.items():
(outdir / node_filename(nid)).write_text(src, encoding="utf-8")
result[name] = (nodes, outdir)
return result


def run_node(outdir: Path, node_id: str) -> str:
"""Execute one emitted node as a standalone program; return its stdout."""
proc = subprocess.run(
[sys.executable, str(outdir / node_filename(node_id))],
capture_output=True,
text=True,
timeout=30,
check=False,
)
assert proc.returncode == 0, proc.stderr
return proc.stdout


def test_examples_discovered():
assert sorted(EXPECTED_NODE_IDS) == EXAMPLE_NAMES


@pytest.mark.parametrize("name", EXAMPLE_NAMES)
def test_transpile_emits_expected_nodes(name, transpiled):
nodes, _ = transpiled[name]
assert list(nodes) == EXPECTED_NODE_IDS[name]


def test_quine_prints_its_own_source(transpiled):
nodes, outdir = transpiled["quine"]
stdout = run_node(outdir, "__NODE")
# The body is ``print(<own source>)``, so stdout is the emitted source
# plus the newline appended by print.
assert stdout == nodes["__NODE"] + "\n"


@pytest.mark.parametrize("name", sorted(HASH_TARGETS))
def test_hash_quines_print_peer_digest(name, transpiled):
"""Each node's printed digest equals the actual SHA-256 of the peer file."""
nodes, outdir = transpiled[name]
for nid, target in HASH_TARGETS[name].items():
actual_source = (outdir / node_filename(target)).read_text(encoding="utf-8")
assert actual_source == nodes[target]
expected = hashlib.sha256(actual_source.encode("utf-8")).hexdigest()
assert run_node(outdir, nid) == expected + "\n"
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ packages = ["src/pyreflect"]
target-version = "py312"

[dependency-groups]
dev = ["ruff>=0.15.4"]
dev = ["pytest >= 9.0.2", "ruff>=0.15.4"]

[tool.ruff.lint]
select = ["B", "E", "F", "I", "PL", "SIM", "TC", "W"]
Expand Down