diff --git a/tests/recipe-tester/README.md b/tests/recipe-tester/README.md index aac1d84..b5ca96f 100644 --- a/tests/recipe-tester/README.md +++ b/tests/recipe-tester/README.md @@ -11,6 +11,27 @@ This is the *runner*; the *tests* live in each recipe's `tests/` directory pinning the recipe under test. The same script is used by CI and local devs — one staging mechanism, one source of truth. +## Test-only dependencies + +The app installs `flet + pytest + `, so the only third-party +packages on the device are the recipe's own `Requires-Dist`. If a recipe's +tests need something the recipe itself doesn't require (e.g. `numpy` for +`safetensors`, whose numpy integration is an upstream extra), declare it in +an optional `recipes//tests/requirements.txt`: + +``` +# one PEP 508 spec per line; blanks and #-comments are skipped +numpy +``` + +`stage_recipe.sh` injects those into the generated `pyproject.toml`. Each +dep must be installable for the *mobile* target — pure-Python from PyPI, or +a recipe already published on `pypi.flet.dev` (or seeded into `dist/` via +the workflow's `prebuild_recipes` input) — the same constraint a real app +faces. Keep it minimal: prefer `pytest.importorskip` for genuinely optional +integrations, and use this file only when skipping would hide the coverage +that matters on-device. + ## Local quick-start You'll need: diff --git a/tests/recipe-tester/pyproject.toml.tpl b/tests/recipe-tester/pyproject.toml.tpl index 64aa925..796f51c 100644 --- a/tests/recipe-tester/pyproject.toml.tpl +++ b/tests/recipe-tester/pyproject.toml.tpl @@ -7,8 +7,11 @@ requires-python = ">=3.10" dependencies = [ "flet", "pytest", - # `stage_recipe.sh` rewrites the line below to pin the recipe under test (e.g. `"numpy==2.2.2"`). + # `stage_recipe.sh` rewrites the line below to pin the recipe under test (e.g. `"numpy==2.2.2"`), + # and replaces the token line after it with any test-only deps declared in + # the recipe's tests/requirements.txt (nothing emitted when the file is absent). "__RECIPE_DEP__", + __TEST_DEPS__ ] [dependency-groups] diff --git a/tests/recipe-tester/stage_recipe.sh b/tests/recipe-tester/stage_recipe.sh index 35ab476..65d1b23 100755 --- a/tests/recipe-tester/stage_recipe.sh +++ b/tests/recipe-tester/stage_recipe.sh @@ -45,21 +45,69 @@ else exit 1 fi -# 2. Substitute the __RECIPE_DEP__ token in the pyproject template and write -# a fresh pyproject.toml (which is gitignored). +# 2. Generate pyproject.toml from the template (gitignored): pin the recipe +# under test (__RECIPE_DEP__) and expand test-only deps (__TEST_DEPS__) +# from the recipe's optional tests/requirements.txt. DEP="$RECIPE" [ -n "$VERSION" ] && DEP="$RECIPE==$VERSION" -# Use a temp file + mv so the substitution is sed-portability-friendly -# (BSD sed and GNU sed differ on -i quoting). +# Test-only deps: packages the tests import that are NOT in the recipe's +# Requires-Dist (e.g. numpy for a zero-runtime-dep recipe like safetensors, +# whose numpy integration is extra-gated upstream). One PEP 508 spec per +# line; blanks and full-line comments are skipped. Each dep must resolve for +# the MOBILE target — pure-Python from PyPI, or a recipe published on +# pypi.flet.dev (or seeded into dist/) — the same constraint a real app faces. +REQS_FILE="$TEST_DIR/requirements.txt" +TEST_DEPS=() +if [ -f "$REQS_FILE" ]; then + while IFS= read -r line || [ -n "$line" ]; do + # ltrim/rtrim, then skip blanks and full-line comments + line="${line#"${line%%[![:space:]]*}"}" + line="${line%"${line##*[![:space:]]}"}" + [ -z "$line" ] && continue + [ "${line:0:1}" = "#" ] && continue + # Deps are emitted as TOML literal (single-quoted) strings so PEP 508 + # markers — which legitimately contain double quotes — pass through + # verbatim; a single quote inside the spec would end the TOML string. + if [[ "$line" == *"'"* ]]; then + echo "::error::single quotes are not supported in $REQS_FILE: $line" >&2 + exit 1 + fi + TEST_DEPS+=("$line") + done < "$REQS_FILE" +fi + +# Expand the template line-by-line with printf '%s' rather than sed: the +# replacement text (PEP 508 specs) may contain characters that are unsafe in +# a sed RHS, and BSD/GNU sed disagree on escaping rules. TPL="$SCRIPT_DIR/pyproject.toml.tpl" OUT="$SCRIPT_DIR/pyproject.toml" -sed "s|__RECIPE_DEP__|$DEP|" "$TPL" > "$OUT" +: > "$OUT" +while IFS= read -r tpl_line || [ -n "$tpl_line" ]; do + # Trimmed copy, so the token matches are exact-line (a comment merely + # *mentioning* a token must pass through verbatim, not expand). + trimmed="${tpl_line#"${tpl_line%%[![:space:]]*}"}" + trimmed="${trimmed%"${trimmed##*[![:space:]]}"}" + if [ "$trimmed" = '"__RECIPE_DEP__",' ]; then + printf '%s\n' "${tpl_line/__RECIPE_DEP__/$DEP}" >> "$OUT" + elif [ "$trimmed" = "__TEST_DEPS__" ]; then + # Replaced by zero or more dep lines. The ${arr[@]+...} guard keeps + # the empty-array expansion safe under `set -u` on macOS bash 3.2. + for dep in ${TEST_DEPS[@]+"${TEST_DEPS[@]}"}; do + printf " '%s',\n" "$dep" >> "$OUT" + done + else + printf '%s\n' "$tpl_line" >> "$OUT" + fi +done < "$TPL" echo "Staged recipe '$RECIPE' (dep: $DEP)" echo " recipe_tests/:" ls -1 "$TEST_DIR" | sed 's/^/ /' echo " pyproject.toml: generated (gitignored)" +if [ ${#TEST_DEPS[@]} -gt 0 ]; then + echo " test-only deps (tests/requirements.txt): ${TEST_DEPS[*]}" +fi echo "" echo "Next:" echo " cd $(realpath --relative-to="$PWD" "$SCRIPT_DIR" 2>/dev/null || echo "$SCRIPT_DIR")"