From 75268cbddc088c88e40fe1df07424589d8ccbbc0 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 15:39:06 +0000 Subject: [PATCH] Split the tests into unit and integration, and add tests for what had none tests/test_code_node.py (1,341 lines, 55 tests) is now one file per part: tests/unit/ (project, reader, graph, platforms, libraries, deploys, report, picker, history) and tests/integration/ (cli, serve, examples), with what they share in tests/support.py. conftest.py marks each test by its folder, so `pytest -m unit` and `-m integration` run one kind (make test-unit, make test-integration, and the same in makefile.py). New tests (24): - imports inside a function: followed, startup=False, scoped to that function; optional imports, relative and star imports - partial maps instead of tracebacks: a bad entry, a file that doesn't parse, a reader that raises - entry_file, entries_in_file, functions_matching, project_root - the deploy helpers: allowed, statement_actions, resolve_handler, load_yaml with CloudFormation tags, line_of - the code read is never run, through main() and python -m code_node - the command as its own process: -o, the cache folder, --help, bad args - --list, a missing folder, an entry point that matches no file - every entry point of each example read to the end; each example's page balanced HTML with nothing fetched from elsewhere; the CI smoke lines - --serve's /events stream over a real connection, through a broken edit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0135RUJGyhafwLiULmpTn7G4 --- .claude/commands/map.md | 2 +- CLAUDE.md | 18 +- Makefile | 10 +- makefile.py | 15 + pyproject.toml | 6 + tests/conftest.py | 18 + tests/integration/test_cli.py | 215 +++++ tests/integration/test_examples.py | 160 ++++ tests/integration/test_serve.py | 117 +++ tests/support.py | 115 +++ tests/test_code_node.py | 1341 ---------------------------- tests/unit/test_deploys.py | 192 ++++ tests/unit/test_graph.py | 149 ++++ tests/unit/test_history.py | 69 ++ tests/unit/test_libraries.py | 79 ++ tests/unit/test_picker.py | 56 ++ tests/unit/test_platforms.py | 218 +++++ tests/unit/test_project.py | 112 +++ tests/unit/test_reader.py | 243 +++++ tests/unit/test_report.py | 158 ++++ 20 files changed, 1944 insertions(+), 1349 deletions(-) create mode 100644 tests/conftest.py create mode 100644 tests/integration/test_cli.py create mode 100644 tests/integration/test_examples.py create mode 100644 tests/integration/test_serve.py create mode 100644 tests/support.py delete mode 100644 tests/test_code_node.py create mode 100644 tests/unit/test_deploys.py create mode 100644 tests/unit/test_graph.py create mode 100644 tests/unit/test_history.py create mode 100644 tests/unit/test_libraries.py create mode 100644 tests/unit/test_picker.py create mode 100644 tests/unit/test_platforms.py create mode 100644 tests/unit/test_project.py create mode 100644 tests/unit/test_reader.py create mode 100644 tests/unit/test_report.py diff --git a/.claude/commands/map.md b/.claude/commands/map.md index 828c143..3f1e3a3 100644 --- a/.claude/commands/map.md +++ b/.claude/commands/map.md @@ -15,4 +15,4 @@ Then review the result against the code: dispatch it can't follow, the depth limit. List what it **got wrong**, if anything. 3. For each miss that's a pattern many codebases use (a framework's decorators, dependency injection, a dispatch dict), propose where in `src/code_node/analysis/reader.py` (or `project.py` for entry points) it would be - followed, with a test for `tests/test_code_node.py`. Don't change code unless asked. + followed, with a test for `tests/unit/test_reader.py` (or `tests/unit/test_project.py`). Don't change code unless asked. diff --git a/CLAUDE.md b/CLAUDE.md index 16474cf..418aacc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,7 +17,7 @@ command is `code-node` and the import is `code_node`. ```bash uv sync # .venv with every dependency group -uv run pytest # tests +uv run pytest # tests (-m unit, -m integration for one kind) uv run ruff check . # lint uv run ruff format . # format (examples/ and docs/ are left as they are) uv run code-node examples/orders-service --no-open -o /tmp/page.html # try it @@ -124,10 +124,18 @@ rendering and I/O (it uses `platforms/`, which is pure too) so another front end ## Tests -`tests/test_code_node.py` writes small projects into `tmp_path` and checks entry point discovery, the map (functions, -AWS calls, imports), the graph's limits, plain Python against `platform="lambda"`, a plugin platform, and the -command end to end (`main([...])` with `--no-open -o`). Add a test -there for each new kind of entry point or anything new the reader follows. +Small projects written into `tmp_path` (or passed to `map_code` as a dict), checked by what the map, graph and page +say. `tests/support.py` has what they share: `write(root, files)` and the small projects (`HANDLER`, `PACKAGE`, +`STORES`...). `pytest -m unit` or `-m integration` runs one kind (`make test-unit`, `make test-integration`); the +folder marks them, in `tests/conftest.py`. + +| Path | What it checks | +|---|---| +| `tests/unit/` | One part on its own, in memory: `test_project.py` (files, module names, import paths, entry points), `test_reader.py` (what the reader follows and what names hold, imports inside functions, partial maps), `test_graph.py`, `test_platforms.py` (Lambda, FastAPI, Flask, Django, a plugin platform), `test_libraries.py`, `test_deploys.py` (SAM, serverless.yml, the IAM and YAML helpers), `test_report.py` (boxes, code panels, the walk, logos, editor links), `test_picker.py`, `test_history.py` | +| `tests/integration/` | End to end, with real files, processes and HTTP: `test_cli.py` (`main([...])` with `--no-open -o`, and `python -m code_node` as its own process; that the code read is never run, and broken code still gives a page), `test_serve.py` (the live page and its `/events` stream), `test_examples.py` (each example against `tests/snapshots/`, every entry point read to the end, each page whole and self-contained) | + +Add a unit test for each new kind of entry point or anything new the reader follows, in the file for that part. A +new example gets a line in `SMOKE` in `test_examples.py`. `UPDATE_SNAPSHOTS=1 uv run pytest` rewrites the snapshots. ## Releasing diff --git a/Makefile b/Makefile index 58240a5..5124a15 100644 --- a/Makefile +++ b/Makefile @@ -8,14 +8,14 @@ OUT ?= /tmp/code-node-page.html PORT ?= 8765 .DEFAULT_GOAL := help -.PHONY: help install test lint fix try serve mermaid shots site-graph logos figures docs docs-serve package check \ +.PHONY: help install test test-unit test-integration lint fix try serve mermaid shots site-graph logos figures docs docs-serve package check \ tool clean help: # list the targets, by group @printf "\033[1mcode-node\033[0m make \n" @awk 'BEGIN {FS = ":.*## "} \ /^##@ / {printf "\n\033[1;35m%s\033[0m\n", substr($$0, 5); next} \ - /^[a-z-]+:.*## / {printf " \033[1;36m%-13s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) + /^[a-z-]+:.*## / {printf " \033[1;36m%-17s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) @echo "" @echo " REPO= picks the code for try, serve and mermaid (default $(REPO))" @@ -44,6 +44,12 @@ fix: ## ruff check --fix and ruff format test: ## run the tests $(RUN) pytest -q +test-unit: ## run the unit tests (tests/unit/): each part on its own, in memory + $(RUN) pytest -q -m unit + +test-integration: ## run the integration tests (tests/integration/): the command, the server and the examples + $(RUN) pytest -q -m integration + check: lint test try docs package ## everything CI runs ##@ Run diff --git a/makefile.py b/makefile.py index 47798d9..4027714 100644 --- a/makefile.py +++ b/makefile.py @@ -203,6 +203,21 @@ def test(options: Options) -> None: python("-m", "pytest", "-q") +@task("test-unit", "๐Ÿงฉ", "run the unit tests (tests/unit/): each part on its own, in memory", "Check") +def test_unit(options: Options) -> None: + python("-m", "pytest", "-q", "-m", "unit") + + +@task( + "test-integration", + "๐Ÿ”—", + "run the integration tests (tests/integration/): the command, the server and the examples", + "Check", +) +def test_integration(options: Options) -> None: + python("-m", "pytest", "-q", "-m", "integration") + + TASKS["check"] = Task( "check", "โœ…", diff --git a/pyproject.toml b/pyproject.toml index 8cc025f..f53c51e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,3 +76,9 @@ include = ["src/code_node", "tests", "README.md", "CHANGELOG.md", "LICENSE"] [tool.pytest.ini_options] testpaths = ["tests"] +pythonpath = ["tests"] # for tests/support.py +addopts = ["--strict-markers"] +markers = [ + "unit: one part on its own, in memory (tests/unit/)", + "integration: the command, the server or an example end to end, with real files, processes or HTTP (tests/integration/)", +] diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..d3f7b77 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,18 @@ +""" +Marks each test by its folder, so `pytest -m unit` and `pytest -m integration` run one kind. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +HERE = Path(__file__).parent + + +def pytest_collection_modifyitems(items: list[pytest.Item]) -> None: + for item in items: + kind = item.path.relative_to(HERE).parts[0] if item.path.is_relative_to(HERE) else "" + if kind in ("unit", "integration"): + item.add_marker(kind) diff --git a/tests/integration/test_cli.py b/tests/integration/test_cli.py new file mode 100644 index 0000000..35fe00f --- /dev/null +++ b/tests/integration/test_cli.py @@ -0,0 +1,215 @@ +""" +The command end to end, through main(): what it prints and the page it saves. +""" + +from __future__ import annotations + +import os +import re +import subprocess +import sys +from pathlib import Path + +import pytest + +from code_node.analysis.mapping import map_code +from code_node.cli import main +from code_node.project import read_project + +from support import EXAMPLES, FAN, HANDLER, IMPORTED_FROM_SRC, PACKAGE, write + + +def test_the_page_opens_a_box_s_calls_with_javascript_and_works_without(tmp_path: Path) -> None: + write(tmp_path / "code", {"app.py": FAN}) + out = tmp_path / "page.html" + assert main([str(tmp_path / "code"), "--no-open", "-o", str(out), "--functions", "4"]) == 0 + page = out.read_text() + # each graph a box's calls open is a template the script swaps in, with the path down to it + assert '
' in page and page.count("