From 4aff42e77cc15200140e8f04aa6bf8f8b16039ef Mon Sep 17 00:00:00 2001 From: Gaurav Chaudhary Date: Wed, 28 Oct 2026 21:59:45 +0530 Subject: [PATCH 1/2] Fix silent restart checkpoint write failures Signed-off-by: Gaurav Chaudhary --- src/sipnet/restart.c | 8 ++++- .../testRestartMVP.c | 34 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/sipnet/restart.c b/src/sipnet/restart.c index 7541a991..9a6b5956 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_FAILURE); + } } static void checkRestartContextCompatibility(void) { diff --git a/tests/sipnet/test_restart_infrastructure/testRestartMVP.c b/tests/sipnet/test_restart_infrastructure/testRestartMVP.c index a40265e1..26e42e1c 100644 --- a/tests/sipnet/test_restart_infrastructure/testRestartMVP.c +++ b/tests/sipnet/test_restart_infrastructure/testRestartMVP.c @@ -716,6 +716,39 @@ 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) { + 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_FAILURE); + 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 +767,7 @@ int run(void) { status |= testBuildInfoMismatchWarnsAndSucceeds(); status |= testTruncatedCheckpointFails(); status |= testMalformedCheckpointFails(); + status |= testCheckpointWriteFailure(); return status; } From 92f20ff9fb689fa214d41638cdc42168718157d8 Mon Sep 17 00:00:00 2001 From: Gaurav Chaudhary Date: Fri, 6 Nov 2026 17:28:09 +0530 Subject: [PATCH 2/2] Address checkpoint write failure review feedback Signed-off-by: Gaurav Chaudhary --- docs/CHANGELOG.md | 1 + src/common/exitCodes.h | 1 + src/sipnet/restart.c | 2 +- tests/sipnet/test_restart_infrastructure/testRestartMVP.c | 3 ++- 4 files changed, 5 insertions(+), 2 deletions(-) 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 9a6b5956..514473ab 100644 --- a/src/sipnet/restart.c +++ b/src/sipnet/restart.c @@ -834,7 +834,7 @@ static void writeRestartState(const char *restartOut, const RestartState *state, int closeFailed = fclose(out); if (writeFailed || closeFailed != 0) { logError("Failed to write restart checkpoint '%s'\n", restartOut); - exit(EXIT_CODE_FAILURE); + exit(EXIT_CODE_FILE_CLOSE_OR_WRITE_ERROR); } } diff --git a/tests/sipnet/test_restart_infrastructure/testRestartMVP.c b/tests/sipnet/test_restart_infrastructure/testRestartMVP.c index 26e42e1c..8f6db38a 100644 --- a/tests/sipnet/test_restart_infrastructure/testRestartMVP.c +++ b/tests/sipnet/test_restart_infrastructure/testRestartMVP.c @@ -726,6 +726,7 @@ static int testCheckpointWriteFailure(void) { int status = prepRunFiles("restart_segment1.clim", "events_segment1.in"); if (status) { + logTest("Failed to prepare files for checkpoint write failure test\n"); return status; } @@ -736,7 +737,7 @@ static int testCheckpointWriteFailure(void) { "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_FAILURE); + 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'");