From 5f4a48dbda174fc0cf189aebfcf496bc8654f475 Mon Sep 17 00:00:00 2001 From: Alexey Karimov Date: Wed, 4 Nov 2026 13:50:11 +0500 Subject: [PATCH 1/2] test(ci): say what the self-update test can and cannot observe assert() returns only once the self-replace helper (which inherits the updater's pipes) has finished, so its single delete attempt is already over when the leftovers are checked: a file still present means that attempt failed, not that the helper is slow. Correct the comments and make the failure message say so. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01WeiJxQWhjhQvvURAjiDkti --- .github/workflows/ci.yml | 4 ++-- tests/update_e2e.rs | 16 +++++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f6e8df..6eda3f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -145,8 +145,8 @@ jobs: - uses: actions/checkout@v7 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - # One test at a time: each self-replace starts a helper process that makes a single attempt - # to delete the leftovers, and the test watches for exactly that. + # One test at a time, so a failure names the test that caused it and a slow runner is not + # also running the other update tests; it does not change the helper's single delete attempt. - run: cargo test --test update_e2e -- --nocapture --test-threads=1 test: diff --git a/tests/update_e2e.rs b/tests/update_e2e.rs index a7ba749..01ffb3d 100644 --- a/tests/update_e2e.rs +++ b/tests/update_e2e.rs @@ -511,12 +511,13 @@ async fn update_on_windows_leaves_a_runnable_binary_that_updates_again_and_no_li "round {round}: the binary should be the downloaded release" ); - // The helper that removes the old binary runs after the updater exits: give it time, then - // require that only the binary is left, and that nothing is left in the temp directory. - // This comes BEFORE launching the new binary, so nothing else is touching the files while - // the helper makes its single attempt to delete them. + // By the time `.assert()` returned, the helper is done: it and its `cmd.exe` inherit the + // updater's stdout/stderr, and `assert()` reads those pipes until every holder has closed + // them. The helper makes ONE attempt to delete the old binary and never retries, so a file + // still here means that attempt failed (nothing will remove it later), not that the helper + // is slow. The short wait is only for the filesystem to settle. let (mut left, mut in_temp) = (names_in(&bindir), names_in(&systemp)); - for _ in 0..80 { + for _ in 0..8 { if left == [exe_name.clone()] && in_temp.is_empty() { break; } @@ -526,11 +527,12 @@ async fn update_on_windows_leaves_a_runnable_binary_that_updates_again_and_no_li assert_eq!( left, [exe_name.clone()], - "round {round}: self-replace left files next to the binary" + "round {round}: the helper exited without deleting the old binary next to the new one" ); assert!( in_temp.is_empty(), - "round {round}: self-replace left files in the temp directory: {in_temp:?}" + "round {round}: the helper exited without deleting the old binary (still in the temp \ + directory, and nothing will remove it later): {in_temp:?}" ); // It is a program, not just bytes: it starts and knows its version. From d11dbcf68cca11d8994e5b370830454211e6f886 Mon Sep 17 00:00:00 2001 From: Alexey Karimov Date: Wed, 4 Nov 2026 14:47:28 +0500 Subject: [PATCH 2/2] test(ci): keep the generous wait for what the pipes do not cover The helper's own .__selfdelete__.exe is removed when the last handle to it closes, which assert() returning does not prove. Keep the 20s wait and soften the failure message accordingly. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01WeiJxQWhjhQvvURAjiDkti --- tests/update_e2e.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/update_e2e.rs b/tests/update_e2e.rs index 01ffb3d..1428515 100644 --- a/tests/update_e2e.rs +++ b/tests/update_e2e.rs @@ -514,10 +514,11 @@ async fn update_on_windows_leaves_a_runnable_binary_that_updates_again_and_no_li // By the time `.assert()` returned, the helper is done: it and its `cmd.exe` inherit the // updater's stdout/stderr, and `assert()` reads those pipes until every holder has closed // them. The helper makes ONE attempt to delete the old binary and never retries, so a file - // still here means that attempt failed (nothing will remove it later), not that the helper - // is slow. The short wait is only for the filesystem to settle. + // that is still here after a generous wait means that attempt failed. The wait is for what + // is not covered by the pipes: the helper's own `.__selfdelete__.exe` goes away only when + // the last handle to it closes (`FILE_FLAG_DELETE_ON_CLOSE`), and a scanner may hold one. let (mut left, mut in_temp) = (names_in(&bindir), names_in(&systemp)); - for _ in 0..8 { + for _ in 0..80 { if left == [exe_name.clone()] && in_temp.is_empty() { break; } @@ -527,12 +528,12 @@ async fn update_on_windows_leaves_a_runnable_binary_that_updates_again_and_no_li assert_eq!( left, [exe_name.clone()], - "round {round}: the helper exited without deleting the old binary next to the new one" + "round {round}: the old binary was not deleted from next to the new one" ); assert!( in_temp.is_empty(), - "round {round}: the helper exited without deleting the old binary (still in the temp \ - directory, and nothing will remove it later): {in_temp:?}" + "round {round}: the helper did not delete what it was meant to (a failed delete is never \ + retried): {in_temp:?}" ); // It is a program, not just bytes: it starts and knows its version.