Found while running the negative control for #383 / PR #525 (reverting the fix, expecting red): ./spp t spp_base_common exited 0 and printed nothing after "Running tests (this may take a few minutes)...", while the log said 1 failed, 1 error(s) of 10 tests. The CLAUDE.md pitfall "./spp t can exit 0 even when the run failed; read the log" describes the symptom; this is the mechanism.
Mechanism (three separate defects, scripts/test_single_module.sh + spp)
-
set -e aborts the script before the results are parsed. The script runs under set -e (line 21). run_tests_docker() correctly wraps docker compose run in set +e / set -e, captures TEST_EXIT_CODE and returns it (lines 193-217; run_tests_local() does the same). But the call site is a bare statement:
if [ "$MODE" = "docker" ]; then
run_tests_docker # line 514: non-zero return under set -e => script exits here
else
run_tests_local
fi
parse_and_display_results # line 520: never reached when odoo-bin exited non-zero
exit $?
odoo-bin exits non-zero whenever a test fails or the registry fails to load, so exactly the runs that matter skip parse_and_display_results: no TEST RESULTS header, no result line, no --- Test failures --- excerpt, no CRITICAL: line. A green run prints the block; a red run prints nothing. (The test DB cleanup inside the function still runs because it precedes the return.)
-
parse_and_display_results never returns non-zero. Its last statement is echo "", so exit $? at line 521 is always exit 0, even when it just printed Failed: 2 or CRITICAL: ... Failed to load registry. The odoo-bin exit code captured in step 1 is never propagated.
-
spp discards the script's status. spp (cmd_test, ~line 363) calls run(cmd, check=False) and does not sys.exit with the returned code, so ./spp t is exit 0 regardless of what the script did.
Net effect: CI is unaffected (the workflow greps the log's N failed, M error(s) line itself), but every local/agent run has to open /tmp/openspp-test-logs/<module>_unittest_*.log and grep odoo.tests.result to learn the verdict, and any tooling that trusts the exit code (subagents, scripts, && chains) sees success on failure.
Proposed fix
- Call the runner under
set +e (or if ! run_tests_docker; then ...; fi, or run_tests_docker || TEST_EXIT_CODE=$?) so the script always reaches parse_and_display_results.
- Make
parse_and_display_results return 1 when FAILED > 0, TEST_ERRORS > 0, CRITICAL_ERROR is set, or no result line was found at all (the "No tests were executed" branch), and return 0 otherwise; exit with that.
- In
spp, sys.exit(run(cmd, check=False).returncode) so ./spp t mirrors the script.
- Optional: when the result line is missing but the log exists, print the last 20 log lines so the failure is visible without opening the file.
Repro
Any module with a failing test: e.g. on a branch where a test asserts something false, run ./spp t <module>; observe exit 0 and no TEST RESULTS block; grep odoo.tests.result /tmp/openspp-test-logs/<module>_unittest_*.log | tail -1 shows the failures. Logs from the #525 negative control: /tmp/openspp-test-logs/spp_base_common_unittest_20260918_103209.log (local machine).
Found while running the negative control for #383 / PR #525 (reverting the fix, expecting red):
./spp t spp_base_commonexited 0 and printed nothing after "Running tests (this may take a few minutes)...", while the log said1 failed, 1 error(s) of 10 tests. The CLAUDE.md pitfall "./spp tcan exit 0 even when the run failed; read the log" describes the symptom; this is the mechanism.Mechanism (three separate defects,
scripts/test_single_module.sh+spp)set -eaborts the script before the results are parsed. The script runs underset -e(line 21).run_tests_docker()correctly wrapsdocker compose runinset +e/set -e, capturesTEST_EXIT_CODEandreturns it (lines 193-217;run_tests_local()does the same). But the call site is a bare statement:odoo-bin exits non-zero whenever a test fails or the registry fails to load, so exactly the runs that matter skip
parse_and_display_results: noTEST RESULTSheader, no result line, no--- Test failures ---excerpt, noCRITICAL:line. A green run prints the block; a red run prints nothing. (The test DB cleanup inside the function still runs because it precedes thereturn.)parse_and_display_resultsnever returns non-zero. Its last statement isecho "", soexit $?at line 521 is alwaysexit 0, even when it just printedFailed: 2orCRITICAL: ... Failed to load registry. The odoo-bin exit code captured in step 1 is never propagated.sppdiscards the script's status.spp(cmd_test, ~line 363) callsrun(cmd, check=False)and does notsys.exitwith the returned code, so./spp tis exit 0 regardless of what the script did.Net effect: CI is unaffected (the workflow greps the log's
N failed, M error(s)line itself), but every local/agent run has to open/tmp/openspp-test-logs/<module>_unittest_*.logand grepodoo.tests.resultto learn the verdict, and any tooling that trusts the exit code (subagents, scripts,&&chains) sees success on failure.Proposed fix
set +e(orif ! run_tests_docker; then ...; fi, orrun_tests_docker || TEST_EXIT_CODE=$?) so the script always reachesparse_and_display_results.parse_and_display_resultsreturn 1whenFAILED > 0,TEST_ERRORS > 0,CRITICAL_ERRORis set, or no result line was found at all (the "No tests were executed" branch), andreturn 0otherwise;exitwith that.spp,sys.exit(run(cmd, check=False).returncode)so./spp tmirrors the script.Repro
Any module with a failing test: e.g. on a branch where a test asserts something false, run
./spp t <module>; observe exit 0 and noTEST RESULTSblock;grep odoo.tests.result /tmp/openspp-test-logs/<module>_unittest_*.log | tail -1shows the failures. Logs from the #525 negative control:/tmp/openspp-test-logs/spp_base_common_unittest_20260918_103209.log(local machine).