diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 57a4a002..91f3c2fc 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -33,6 +33,7 @@ sections to include in release notes: ### Fixed - Return a non-zero exit status from the unit test runner when a test fails or is missing. (#432) +- Report restart checkpoint write or close failures and exit with file write error status 11, including in quiet mode. (#420) - Restore format specifiers for stock value in `ensureNonNegative()` error logging to align arguments and prevent undefined behavior. (#406) - Terminating harvest events will now remove all biomass at end of time step, instead of allowing small amounts to remain in the plant pools due to growth in that step. (#400) - Leaf-off mechanics refined to ensure that leaf-off events do not remove more leaf carbon than is available in the leaf pool. (#400) diff --git a/src/common/exitCodes.h b/src/common/exitCodes.h index 2c399490..26052738 100644 --- a/src/common/exitCodes.h +++ b/src/common/exitCodes.h @@ -21,6 +21,7 @@ typedef enum { EXIT_CODE_UNKNOWN_EVENT_TYPE_OR_PARAM = 4, EXIT_CODE_INPUT_FILE_ERROR = 5, EXIT_CODE_FILE_OPEN_OR_READ_ERROR = 6, + EXIT_CODE_FILE_CLOSE_OR_WRITE_ERROR = 11, EXIT_CODE_INTERNAL_ERROR = 7, EXIT_CODE_BAD_CLI_ARGUMENT = 8, EXIT_CODE_BAD_RESTART_PARAMETER = 9, diff --git a/src/sipnet/restart.c b/src/sipnet/restart.c index 7541a991..514473ab 100644 --- a/src/sipnet/restart.c +++ b/src/sipnet/restart.c @@ -829,7 +829,13 @@ static void writeRestartState(const char *restartOut, const RestartState *state, fprintf(out, "end_restart 1\n"); - fclose(out); + int writeFailed = ferror(out); + // Always close the stream: buffered writes can fail during the final flush. + int closeFailed = fclose(out); + if (writeFailed || closeFailed != 0) { + logError("Failed to write restart checkpoint '%s'\n", restartOut); + exit(EXIT_CODE_FILE_CLOSE_OR_WRITE_ERROR); + } } static void checkRestartContextCompatibility(void) { diff --git a/tests/sipnet/test_restart_infrastructure/testRestartMVP.c b/tests/sipnet/test_restart_infrastructure/testRestartMVP.c index a40265e1..8f6db38a 100644 --- a/tests/sipnet/test_restart_infrastructure/testRestartMVP.c +++ b/tests/sipnet/test_restart_infrastructure/testRestartMVP.c @@ -716,6 +716,40 @@ static int testMalformedCheckpointFails(void) { return status; } +static int testCheckpointWriteFailure(void) { + logTest("Starting testCheckpointWriteFailure\n"); + + if (access("/dev/full", W_OK) != 0) { + logTest("Skipping checkpoint write failure test: /dev/full unavailable\n"); + return 0; + } + + int status = prepRunFiles("restart_segment1.clim", "events_segment1.in"); + if (status) { + logTest("Failed to prepare files for checkpoint write failure test\n"); + return status; + } + + const char *args[] = { + "--restart-out /dev/full --no-do-main-output --no-quiet", + "--restart-out /dev/full --no-do-main-output --quiet"}; + const char *logs[] = {"checkpoint_write_failure.log", + "checkpoint_write_failure_quiet.log"}; + for (int i = 0; i < 2; ++i) { + int rc = runModelWithArgs("restart_seg1.in", logs[i], args[i]); + int failed = (rc != EXIT_CODE_FILE_CLOSE_OR_WRITE_ERROR); + failed |= !fileContains(logs[i], "[ERROR ]"); + failed |= !fileContains(logs[i], + "Failed to write restart checkpoint '/dev/full'"); + if (failed) { + logTest("Checkpoint write failure test failed (quiet=%d, rc=%d)\n", i, + rc); + } + status |= failed; + } + return status; +} + int run(void) { int status = 0; @@ -734,6 +768,7 @@ int run(void) { status |= testBuildInfoMismatchWarnsAndSucceeds(); status |= testTruncatedCheckpointFails(); status |= testMalformedCheckpointFails(); + status |= testCheckpointWriteFailure(); return status; }