run.sh 5.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. #!/usr/bin/env bash
  2. # Self-test for python-fastapi-ops — fully offline, deterministic, Linux-safe.
  3. #
  4. # scaffold-api.sh emits a FastAPI module to stdout; it never opens a file for
  5. # writing, so it cannot clobber a user's project. Every run here is redirected
  6. # into a mktemp -d sandbox — nothing is ever written into the repo or a real
  7. # project. Asserts the protocol contract (--help, exit codes, stream
  8. # separation), the generated module's structure, byte-identical idempotency,
  9. # and that the script owns no overwrite hazard (the only clobber is the
  10. # caller's shell `>` redirect — a latent caller-side hazard, documented here,
  11. # NOT "fixed" in the script per its contract).
  12. #
  13. # Usage: bash tests/run.sh
  14. # Exit: 0 all pass, 1 one or more failures
  15. set -uo pipefail
  16. HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  17. SKILL="$(dirname "$HERE")"
  18. V="$SKILL/scripts/scaffold-api.sh"
  19. SB="$(mktemp -d)"; trap 'rm -rf "$SB"' EXIT
  20. PASS=0; FAIL=0
  21. ok() { PASS=$((PASS+1)); printf ' PASS %s\n' "$1"; }
  22. no() { FAIL=$((FAIL+1)); printf ' FAIL %s\n' "$1"; }
  23. expect_exit() { [[ "$2" == "$3" ]] && ok "$1 (exit $3)" || no "$1 (want $2 got $3)"; }
  24. expect_has() { case "$3" in *"$2"*) ok "$1";; *) no "$1 (missing '$2')";; esac; }
  25. echo "=== python-fastapi-ops self-test ==="
  26. # ── contract ──────────────────────────────────────────────────────────────────
  27. echo "-- contract --"
  28. bash -n "$V" 2>/dev/null && ok "bash -n scaffold-api.sh" || no "bash -n scaffold-api.sh"
  29. bash "$V" --help >/dev/null 2>&1; expect_exit "--help exits 0" 0 $?
  30. bash "$V" -h >/dev/null 2>&1; expect_exit "-h exits 0" 0 $?
  31. out="$(bash "$V" --help 2>/dev/null)"
  32. expect_has "--help has Examples" "xamples" "$out"
  33. expect_has "--help documents exit 2" "2" "$out"
  34. bash "$V" >/dev/null 2>&1; expect_exit "missing resource -> 2" 2 $?
  35. bash "$V" --bogus >/dev/null 2>&1; expect_exit "unknown flag -> 2" 2 $?
  36. # usage/errors must go to stderr, never polluting the stdout data stream
  37. err="$(bash "$V" 2>&1 >/dev/null)"
  38. expect_has "missing-resource usage on stderr" "Usage:" "$err"
  39. noso="$(bash "$V" --bogus 2>/dev/null)"
  40. [[ -z "$noso" ]] && ok "unknown-flag writes nothing to stdout" || no "unknown-flag leaked to stdout"
  41. # ── generated module structure ────────────────────────────────────────────────
  42. echo "-- generated module --"
  43. bash "$V" user >"$SB/user.py" 2>"$SB/user.err"; expect_exit "generate user -> 0" 0 $?
  44. [[ -s "$SB/user.py" ]] && ok "module written to redirected stdout" || no "module not written"
  45. [[ ! -s "$SB/user.err" ]] && ok "happy path is silent on stderr" || no "happy path wrote to stderr"
  46. out="$(cat "$SB/user.py")"
  47. expect_has "has Pydantic Create model" "class UserCreate(BaseModel):" "$out"
  48. expect_has "has Pydantic Update model" "class UserUpdate(BaseModel):" "$out"
  49. expect_has "has Pydantic Response model" "class UserResponse(BaseModel):" "$out"
  50. expect_has "has APIRouter with prefix" 'router = APIRouter(prefix="/users"' "$out"
  51. expect_has "has create endpoint" "async def create_user" "$out"
  52. expect_has "has list endpoint" "async def list_users" "$out"
  53. expect_has "has get endpoint" "async def get_user" "$out"
  54. expect_has "has update endpoint" "async def update_user" "$out"
  55. expect_has "has delete endpoint" "async def delete_user" "$out"
  56. expect_has "resource name in Create docstring" "Create user request" "$out"
  57. # title-casing + pluralization for mixed-case input
  58. bash "$V" Order >"$SB/order.py" 2>/dev/null; expect_exit "generate Order -> 0" 0 $?
  59. out="$(cat "$SB/order.py")"
  60. expect_has "title-cases Order" "class OrderCreate(BaseModel):" "$out"
  61. expect_has "pluralizes to orders" 'prefix="/orders"' "$out"
  62. expect_has "orders create endpoint" "async def create_order" "$out"
  63. # ── idempotency: re-running produces byte-identical output ───────────────────
  64. echo "-- idempotency --"
  65. bash "$V" user >"$SB/a.py" 2>/dev/null
  66. bash "$V" user >"$SB/b.py" 2>/dev/null
  67. if cmp -s "$SB/a.py" "$SB/b.py"; then ok "re-run produces byte-identical output"; else no "re-run output differs"; fi
  68. # ── overwrite safety ─────────────────────────────────────────────────────────
  69. echo "-- overwrite safety --"
  70. # The script emits to stdout only and never opens a file, so it is
  71. # non-destructive by construction: running it leaves the cwd untouched. There
  72. # is no --force/--no-clobber flag because the script has no file target to
  73. # guard — the sole clobber path is the caller's own shell redirect (`>`),
  74. # which the script cannot see. That caller-side hazard is documented here,
  75. # not "fixed".
  76. mkdir -p "$SB/cwd-check"
  77. ( cd "$SB/cwd-check" && bash "$V" user >/dev/null 2>&1 )
  78. n="$(find "$SB/cwd-check" -type f | wc -l | tr -d ' ')"
  79. [[ "$n" == "0" ]] && ok "script creates no files in cwd (non-destructive)" || no "script wrote files into cwd ($n)"
  80. echo ""
  81. echo "=== $PASS passed, $FAIL failed ==="
  82. [[ "$FAIL" -eq 0 ]] || exit 1
  83. exit 0