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
21 changes: 21 additions & 0 deletions tests/recipe-tester/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 + <recipe>`, 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/<name>/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:
Expand Down
5 changes: 4 additions & 1 deletion tests/recipe-tester/pyproject.toml.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
58 changes: 53 additions & 5 deletions tests/recipe-tester/stage_recipe.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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")"
Expand Down
Loading