diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 45a2d96..f6d9b42 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,10 +30,18 @@ jobs: cache: true cache-dependency-path: examples/go/go.sum + - name: Tidy Go examples (absorb web4 drift) + working-directory: examples/go + run: go mod tidy + - name: Build Go examples working-directory: examples/go run: go build ./... + - name: Test Go examples (compile smoke) + working-directory: examples/go + run: go test ./... + python-examples: runs-on: ubuntu-latest steps: @@ -49,6 +57,13 @@ jobs: done echo "all Python examples parse" + - name: Install pytest + run: python -m pip install --upgrade pip pytest + + - name: Run Python example smoke tests + working-directory: python_sdk + run: python -m pytest tests/ -v + shell-examples: runs-on: ubuntu-latest steps: diff --git a/go/client/main_test.go b/go/client/main_test.go new file mode 100644 index 0000000..bd9d12c --- /dev/null +++ b/go/client/main_test.go @@ -0,0 +1,13 @@ +package main + +import "testing" + +// TestExampleCompiles is a placeholder so `go test ./...` exercises the +// compile + link path for this example. The example itself is a main +// package; running its main() requires a live daemon, so we only assert +// that it builds cleanly. Compile = type-check + link, which catches +// upstream pkg/protocol or SDK API breakage as web4 evolves. +func TestExampleCompiles(t *testing.T) { + // Build success is implicit: the test binary cannot link if main.go + // references a missing or renamed symbol in github.com/TeoSlayer/pilotprotocol. +} diff --git a/go/echo/main_test.go b/go/echo/main_test.go new file mode 100644 index 0000000..bd9d12c --- /dev/null +++ b/go/echo/main_test.go @@ -0,0 +1,13 @@ +package main + +import "testing" + +// TestExampleCompiles is a placeholder so `go test ./...` exercises the +// compile + link path for this example. The example itself is a main +// package; running its main() requires a live daemon, so we only assert +// that it builds cleanly. Compile = type-check + link, which catches +// upstream pkg/protocol or SDK API breakage as web4 evolves. +func TestExampleCompiles(t *testing.T) { + // Build success is implicit: the test binary cannot link if main.go + // references a missing or renamed symbol in github.com/TeoSlayer/pilotprotocol. +} diff --git a/go/go.mod b/go/go.mod index 2b670ff..dae89b2 100644 --- a/go/go.mod +++ b/go/go.mod @@ -1,6 +1,6 @@ module github.com/pilot-protocol/examples/go -go 1.25.3 +go 1.25.10 require github.com/TeoSlayer/pilotprotocol v0.0.0 diff --git a/go/httpclient/main_test.go b/go/httpclient/main_test.go new file mode 100644 index 0000000..bd9d12c --- /dev/null +++ b/go/httpclient/main_test.go @@ -0,0 +1,13 @@ +package main + +import "testing" + +// TestExampleCompiles is a placeholder so `go test ./...` exercises the +// compile + link path for this example. The example itself is a main +// package; running its main() requires a live daemon, so we only assert +// that it builds cleanly. Compile = type-check + link, which catches +// upstream pkg/protocol or SDK API breakage as web4 evolves. +func TestExampleCompiles(t *testing.T) { + // Build success is implicit: the test binary cannot link if main.go + // references a missing or renamed symbol in github.com/TeoSlayer/pilotprotocol. +} diff --git a/go/secure/main_test.go b/go/secure/main_test.go new file mode 100644 index 0000000..bd9d12c --- /dev/null +++ b/go/secure/main_test.go @@ -0,0 +1,13 @@ +package main + +import "testing" + +// TestExampleCompiles is a placeholder so `go test ./...` exercises the +// compile + link path for this example. The example itself is a main +// package; running its main() requires a live daemon, so we only assert +// that it builds cleanly. Compile = type-check + link, which catches +// upstream pkg/protocol or SDK API breakage as web4 evolves. +func TestExampleCompiles(t *testing.T) { + // Build success is implicit: the test binary cannot link if main.go + // references a missing or renamed symbol in github.com/TeoSlayer/pilotprotocol. +} diff --git a/go/webserver/main_test.go b/go/webserver/main_test.go new file mode 100644 index 0000000..bd9d12c --- /dev/null +++ b/go/webserver/main_test.go @@ -0,0 +1,13 @@ +package main + +import "testing" + +// TestExampleCompiles is a placeholder so `go test ./...` exercises the +// compile + link path for this example. The example itself is a main +// package; running its main() requires a live daemon, so we only assert +// that it builds cleanly. Compile = type-check + link, which catches +// upstream pkg/protocol or SDK API breakage as web4 evolves. +func TestExampleCompiles(t *testing.T) { + // Build success is implicit: the test binary cannot link if main.go + // references a missing or renamed symbol in github.com/TeoSlayer/pilotprotocol. +} diff --git a/python_sdk/tests/__init__.py b/python_sdk/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python_sdk/tests/test_examples_syntax.py b/python_sdk/tests/test_examples_syntax.py new file mode 100644 index 0000000..e74ff7d --- /dev/null +++ b/python_sdk/tests/test_examples_syntax.py @@ -0,0 +1,93 @@ +"""Smoke tests for the Python SDK example scripts. + +Goals +----- +Catch regressions in the user-facing demos as the Pilot Protocol Python +SDK evolves. The demos themselves require a live daemon and trusted +peers to execute, so we cannot import them and run them under pytest. +Instead, we validate two cheap properties per demo: + +1. The file is syntactically valid Python (``ast.parse``). +2. The demo gates its side-effect entry point on + ``if __name__ == "__main__":`` so that future ``importlib``-based + loaders can introspect it without firing real network calls. + +Adding a new demo? Drop it next to ``basic_usage.py`` and it is picked +up automatically by ``iter_demo_files``. +""" + +from __future__ import annotations + +import ast +from pathlib import Path + +import pytest + + +EXAMPLES_DIR = Path(__file__).resolve().parent.parent + + +def iter_demo_files() -> list[Path]: + """Return every top-level ``*.py`` file in ``python_sdk/``. + + Tests under ``python_sdk/tests/`` are intentionally excluded so the + suite never tries to syntax-check itself. + """ + return sorted(p for p in EXAMPLES_DIR.glob("*.py") if p.is_file()) + + +DEMOS = iter_demo_files() + + +def test_demo_directory_is_non_empty() -> None: + """Guard against the glob silently matching zero files. + + Without this, a future refactor that moves the demos to a sub-package + would turn every parameterized test below into a no-op and the suite + would still pass green. + """ + assert DEMOS, f"no demos found in {EXAMPLES_DIR}" + + +@pytest.mark.parametrize("demo", DEMOS, ids=lambda p: p.name) +def test_demo_parses_as_valid_python(demo: Path) -> None: + """The demo must be syntactically valid Python. + + ``ast.parse`` is a pure parse — no imports run, so a missing SDK + install does not cause spurious failures. This catches accidental + syntax breakage from search-and-replace edits across the demos. + """ + source = demo.read_text(encoding="utf-8") + try: + ast.parse(source, filename=str(demo)) + except SyntaxError as e: # pragma: no cover - failure path + pytest.fail(f"{demo.name} failed to parse: {e}") + + +@pytest.mark.parametrize("demo", DEMOS, ids=lambda p: p.name) +def test_demo_guards_entry_point(demo: Path) -> None: + """The demo must gate side effects on ``if __name__ == "__main__"``. + + Importing a demo (via ``importlib`` or ``python -c "import foo"``) + should never connect to a daemon, open sockets, or block on input. + Without the guard, downstream tooling that snapshots the example + catalogue would hang or crash on import. + """ + tree = ast.parse(demo.read_text(encoding="utf-8"), filename=str(demo)) + for node in ast.walk(tree): + if ( + isinstance(node, ast.If) + and isinstance(node.test, ast.Compare) + and isinstance(node.test.left, ast.Name) + and node.test.left.id == "__name__" + and len(node.test.ops) == 1 + and isinstance(node.test.ops[0], ast.Eq) + and len(node.test.comparators) == 1 + and isinstance(node.test.comparators[0], ast.Constant) + and node.test.comparators[0].value == "__main__" + ): + return + pytest.fail( + f"{demo.name} has no `if __name__ == \"__main__\":` guard; " + "importing it would execute side effects." + )