fix(parity): dry-run delete protections, delete-delay actual-removal budget, --info name2

- -n/--delete sends the same protected/size-skipped/scope as a real run, so
  the read-only would-delete walk no longer over-reports (row -> caveat)
- --delete-delay charges --max-delete on actual removals and recursively
  re-scans a refilled deferred directory at commit; independent deferred cap
- --info=name emits the leading ./ root line and name2 'is uptodate' lines
- differential tests promoted from residual pins to rsync parity assertions
This commit is contained in:
2026-09-19 09:25:07 +02:00
parent 00d628d4ba
commit 25062352f6
17 changed files with 428 additions and 163 deletions
+28 -29
View File
@@ -23,10 +23,9 @@ static void send_plan_frame(int fd, const char* dir) {
}
/* --delete-delay: a directory snapshotted into the plan that is refilled before
* the commit must NOT be counted as deleted once its unlink fails ENOTEMPTY.
* Regression for delete_plan.c counting at snapshot (defer_add) instead of at
* the actual removal. */
static void test_delete_delay_refilled_dir_not_counted(void) {
* the commit is re-scanned and removed recursively (rsync parity). Regression
* for the old single-unlink ENOTEMPTY path that left the directory behind. */
static void test_delete_delay_refilled_dir_removed_recursively(void) {
char root[] = "/tmp/fastsync_dp_refill_XXXXXX";
EXPECT_TRUE(mkdtemp(root) != NULL);
char extra[1024];
@@ -57,16 +56,14 @@ static void test_delete_delay_refilled_dir_not_counted(void) {
close(fd);
EXPECT_EQ_INT(delete_plan_session_commit(session, config), DELETE_COMMIT_OK);
/* ENOTEMPTY: the directory survives, so it must not be reported as deleted. */
EXPECT_EQ_INT((int)delete_plan_session_deleted(session), 0);
/* The late content and the directory itself are both removed. */
EXPECT_EQ_INT((int)delete_plan_session_deleted(session), 2);
struct stat st;
EXPECT_EQ_INT(lstat(extra, &st), 0);
EXPECT_TRUE(lstat(extra, &st) != 0);
delete_plan_session_destroy(session);
close(p[0]);
close(p[1]);
unlink(refill);
rmdir(extra);
rmdir(root);
config_delete(config);
}
@@ -106,8 +103,9 @@ static void test_delete_delay_removed_file_counted(void) {
config_delete(config);
}
/* --max-delete still bounds the deferred plan; the actual (removed) count must
* not exceed the limit even though more extras existed. */
/* --max-delete is charged on actual removals, not at plan/snapshot time: after
* receiving the plans the budget is untouched, and only the commit removes up to
* the limit. */
static void test_delete_delay_max_delete_bounds_actual(void) {
char root[] = "/tmp/fastsync_dp_max_XXXXXX";
EXPECT_TRUE(mkdtemp(root) != NULL);
@@ -133,8 +131,11 @@ static void test_delete_delay_max_delete_bounds_actual(void) {
EXPECT_NOT_NULL(session);
send_plan_frame(p[1], ".");
EXPECT_EQ_INT(delete_plan_session_receive(session, config, p[0]), 0);
EXPECT_TRUE(delete_plan_session_limit_reached(session));
/* Nothing removed yet, so the budget is not consumed at snapshot time. */
EXPECT_FALSE(delete_plan_session_limit_reached(session));
EXPECT_EQ_INT((int)delete_plan_session_deleted(session), 0);
EXPECT_EQ_INT(delete_plan_session_commit(session, config), DELETE_COMMIT_LIMIT_REACHED);
EXPECT_TRUE(delete_plan_session_limit_reached(session));
EXPECT_EQ_INT((int)delete_plan_session_deleted(session), 1);
delete_plan_session_destroy(session);
@@ -149,13 +150,12 @@ static void test_delete_delay_max_delete_bounds_actual(void) {
config_delete(config);
}
/* --max-delete is charged at plan/snapshot time, not at actual removal: a
* deferred entry that survives ENOTEMPTY still consumes its budget slot, so a
* later directory's extra is skipped even though nothing was actually removed.
* The reported count stays 0 (actual removals) while the run is partial. The
* two plans are sent as separate frames for "a" then "b", so the ordering that
* decides which entry gets the budget is deterministic (unlike readdir order). */
static void test_delete_delay_refilled_dir_charges_budget_at_plan(void) {
/* --max-delete is charged on ACTUAL removals: the refilled directory's late
* content is removed first (consuming the single budget slot), so the directory
* itself and the later extra are skipped, matching rsync. The two plans are
* sent as separate frames for "a" then "b", so the ordering that decides which
* entry gets the budget is deterministic (unlike readdir order). */
static void test_delete_delay_actual_removal_charges_budget(void) {
char root[] = "/tmp/fastsync_dp_planbudget_XXXXXX";
EXPECT_TRUE(mkdtemp(root) != NULL);
char adir[1024], bdir[1024], xdir[1024], ydir[1024];
@@ -180,18 +180,16 @@ static void test_delete_delay_refilled_dir_charges_budget_at_plan(void) {
DeletePlanSession* session = delete_plan_session_create(config);
EXPECT_NOT_NULL(session);
/* Plan "a" first: its empty extra dir snapshots and charges the budget. */
/* Both plan snapshots are taken; neither consumes budget yet. */
send_plan_frame(p[1], "a");
EXPECT_EQ_INT(delete_plan_session_receive(session, config, p[0]), 0);
EXPECT_FALSE(delete_plan_session_limit_reached(session));
/* Plan "b": the budget is already spent at snapshot time, so b/y is skipped
even though a/x has not (and will not) be removed. */
send_plan_frame(p[1], "b");
EXPECT_EQ_INT(delete_plan_session_receive(session, config, p[0]), 0);
EXPECT_TRUE(delete_plan_session_limit_reached(session));
EXPECT_FALSE(delete_plan_session_limit_reached(session));
EXPECT_EQ_INT((int)delete_plan_session_deleted(session), 0);
/* Refill a/x so its deferred rmdir fails ENOTEMPTY. */
/* Refill a/x after its plan: the recursive commit must remove this content. */
char refill[1200];
snprintf(refill, sizeof(refill), "%s/new.txt", xdir);
int fd = open(refill, O_WRONLY | O_CREAT | O_TRUNC, 0600);
@@ -199,16 +197,17 @@ static void test_delete_delay_refilled_dir_charges_budget_at_plan(void) {
close(fd);
EXPECT_EQ_INT(delete_plan_session_commit(session, config), DELETE_COMMIT_LIMIT_REACHED);
/* Nothing was actually removed, and the plan-time budget still stopped b/y. */
EXPECT_EQ_INT((int)delete_plan_session_deleted(session), 0);
/* The one budget slot removed the late content; the two directories survive. */
EXPECT_EQ_INT((int)delete_plan_session_deleted(session), 1);
EXPECT_TRUE(delete_plan_session_limit_reached(session));
struct stat st;
EXPECT_TRUE(lstat(refill, &st) != 0);
EXPECT_EQ_INT(lstat(xdir, &st), 0);
EXPECT_EQ_INT(lstat(ydir, &st), 0);
delete_plan_session_destroy(session);
close(p[0]);
close(p[1]);
unlink(refill);
rmdir(xdir);
rmdir(ydir);
rmdir(adir);
@@ -218,8 +217,8 @@ static void test_delete_delay_refilled_dir_charges_budget_at_plan(void) {
}
void test_delete_plan(void) {
test_delete_delay_refilled_dir_not_counted();
test_delete_delay_refilled_dir_removed_recursively();
test_delete_delay_removed_file_counted();
test_delete_delay_max_delete_bounds_actual();
test_delete_delay_refilled_dir_charges_budget_at_plan();
test_delete_delay_actual_removal_charges_budget();
}