test(e2e): add e2e tests for reverse deletionOrder when progressive sync enabled#26673
test(e2e): add e2e tests for reverse deletionOrder when progressive sync enabled#26673ranakan19 wants to merge 8 commits intoargoproj:masterfrom
Conversation
Signed-off-by: Kanika Rana <krana@redhat.com>
Signed-off-by: Kanika Rana <krana@redhat.com>
…tep test case with reverse deletionOrder Signed-off-by: Kanika Rana <krana@redhat.com>
❗ Preview Environment deployment failed on BunnyshellSee: Environment Details | Pipeline Logs Available commands (reply to this comment):
|
Signed-off-by: Kanika Rana <krana@redhat.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #26673 +/- ##
=========================================
Coverage ? 62.79%
=========================================
Files ? 414
Lines ? 55952
Branches ? 0
=========================================
Hits ? 35137
Misses ? 17463
Partials ? 3352 ☔ View full report in Codecov by Sentry. |
Signed-off-by: Kanika Rana <krana@redhat.com>
Signed-off-by: Kanika Rana <krana@redhat.com>
There was a problem hiding this comment.
Pull request overview
This PR adds e2e tests that verify the reverse deletion order behavior for ApplicationSets with progressive sync enabled. It also makes the test infrastructure more flexible by parameterizing the Delete action (with or without foreground propagation) and generateExpectedApp (with optional custom finalizers). Additionally, it merges the former TestProgressiveSyncMultipleAppsPerStep into a new test TestProgressiveSyncMultipleAppsPerStepWithReverseDeletionOrder that verifies apps are deleted in the reverse step order (prod → staging → dev), gated by a custom e2e finalizer.
Changes:
Delete()action signature changed toDelete(bool), wherefalseuses foreground propagation (the original behavior for most tests) andtrueomits the propagation policy (needed for reverse deletion order test).generateExpectedAppgains atestFinalizer stringparameter to support attaching extra finalizers to apps under test.- New test
TestProgressiveSyncMultipleAppsPerStepWithReverseDeletionOrderverifies the step-by-step reverse deletion order, controlled by a custom e2e finalizer that gates each wave. - New expectation helpers
ApplicationsBeingDeletedOrGoneandApplicationsExistAndNotBeingDeleted, plus new actionRemoveFinalizerFromApps.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
test/e2e/fixture/applicationsets/actions.go |
Delete() → Delete(bool) with conditional DeleteOptions; new RemoveFinalizerFromApps action |
test/e2e/fixture/applicationsets/expectation.go |
New ApplicationsBeingDeletedOrGone and ApplicationsExistAndNotBeingDeleted expectations |
test/e2e/applicationset_progressive_sync_test.go |
Replaced TestProgressiveSyncMultipleAppsPerStep with new reverse-deletion-order test; updated all Delete() → Delete(false/true) and generateExpectedApp calls |
test/e2e/applicationset_test.go |
Updated all Delete() → Delete(false) |
test/e2e/applicationset_git_generator_test.go |
Updated all Delete() → Delete(false) |
test/e2e/cluster_generator_test.go |
Updated all Delete() → Delete(false) |
test/e2e/clusterdecisiongenerator_e2e_test.go |
Updated all Delete() → Delete(false) |
test/e2e/matrix_e2e_test.go |
Updated all Delete() → Delete(false) |
test/e2e/merge_e2e_test.go |
Updated all Delete() → Delete(false) |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| func ApplicationsBeingDeletedOrGone(appNames []string) Expectation { | ||
| anyapp := false | ||
| return func(c *Consequences) (state, string) { | ||
| for _, appName := range appNames { | ||
| app := c.app(appName) | ||
| if (app != nil && app.DeletionTimestamp != nil) || app == nil { | ||
| anyapp = true | ||
| } | ||
| } | ||
| if !anyapp { | ||
| return pending, "no app in this step is being deleted yet" | ||
| } | ||
| return succeeded, fmt.Sprintf("all apps %v are being deleted or gone", appNames) | ||
| } |
There was a problem hiding this comment.
The anyapp variable in ApplicationsBeingDeletedOrGone is declared outside the returned closure, meaning it persists across all calls to the closure (as ExpectWithDuration/Expect repeatedly invokes the closure in a retry loop). Once anyapp is set to true in any iteration, it can never revert to false in subsequent calls. This means that once at least one app is seen as "being deleted or gone" in any retry iteration, all future invocations of this expectation function will always return succeeded, regardless of the current actual state.
While in this specific deletion test the condition is likely monotonically true, this design is a latent correctness bug. The variable should be declared inside the returned closure so that each invocation starts fresh and accurately reflects the current state.
|
|
||
| // Delete deletes the ApplicationSet within the context | ||
| func (a *Actions) Delete() *Actions { | ||
| func (a *Actions) Delete(deletionOrder bool) *Actions { |
There was a problem hiding this comment.
The parameter name deletionOrder bool in the Delete function is semantically misleading. When the parameter is true, empty DeleteOptions are passed (no propagation policy), and when false, foreground deletion is used. The parameter name doesn't describe what it does - the actual meaning is something like "skipForegroundDeletion" or "useDefaultDeletion". The current name suggests it has something to do with the deletion order feature itself, but it only controls which DeleteOptions to use when issuing the API call. A clearer name such as foreground bool (where true = foreground propagation, false = default) or splitting into a named options struct would make call sites much easier to understand.
| for _, appName := range appNames { | ||
| app, err := fixtureClient.AppClientset.ArgoprojV1alpha1().Applications(fixture.TestNamespace()).Get( | ||
| context.Background(), appName, metav1.GetOptions{}) | ||
| if err != nil { | ||
| a.lastError = err | ||
| continue | ||
| } | ||
|
|
||
| // Remove the finalizer | ||
| finalizers := []string{} | ||
| for _, f := range app.Finalizers { | ||
| if f != finalizer { | ||
| finalizers = append(finalizers, f) | ||
| } | ||
| } | ||
| app.Finalizers = finalizers | ||
|
|
||
| _, err = fixtureClient.AppClientset.ArgoprojV1alpha1().Applications(fixture.TestNamespace()).Update( | ||
| context.Background(), app, metav1.UpdateOptions{}) | ||
| if err != nil { | ||
| a.lastError = err | ||
| } | ||
| } | ||
| a.describeAction = fmt.Sprintf("removing finalizer '%s' from apps %v", finalizer, appNames) | ||
| a.verifyAction() |
There was a problem hiding this comment.
In RemoveFinalizerFromApps, when Get fails on an app, a.lastError is set and the loop continues to the next app. However, if a subsequent Get succeeds, a.lastError is not updated or cleared on success — it retains the previous error. This means the final verifyAction() call may fail with a stale error from an earlier iteration even if all remaining apps were processed successfully. Additionally, if the Update call on the last app succeeds, the error from a Get failure on an earlier app will still cause verifyAction() to fail.
Consider collecting all errors (e.g. in a slice) and reporting them after the loop, or ensuring a.lastError is reset on each successful operation.
| RemoveFinalizerFromApps(stagingApps, testFinalizer). | ||
| Then(). | ||
| And(func() { | ||
| t.Log("removed finalizer from staging apps, confirm prod apps deleted") |
There was a problem hiding this comment.
The log message at line 497 says "removed finalizer from staging apps, confirm prod apps deleted" but the finalizer has just been removed from staging apps — this should say "confirm staging apps deleted" (not "prod apps deleted"). The same copy-paste issue exists in the log message at line 483, which says "confirm prod apps deleted" right after removing the finalizer from prod apps — that one is correct. Only line 497 is wrong.
| t.Log("removed finalizer from staging apps, confirm prod apps deleted") | |
| t.Log("removed finalizer from staging apps, confirm staging apps deleted") |
| if !anyapp { | ||
| return pending, "no app in this step is being deleted yet" | ||
| } | ||
| return succeeded, fmt.Sprintf("all apps %v are being deleted or gone", appNames) |
There was a problem hiding this comment.
The success message in ApplicationsBeingDeletedOrGone says "all apps %v are being deleted or gone", but the function's documented behavior and actual logic only checks that at least one app is being deleted or gone — not all of them. This creates a misleading success message that incorrectly implies all apps satisfy the condition. The message should be something like "at least one app in %v is being deleted or gone".
| return succeeded, fmt.Sprintf("all apps %v are being deleted or gone", appNames) | |
| return succeeded, fmt.Sprintf("at least one app in %v is being deleted or gone", appNames) |
Changes introduced:
generateExpectedAppmore configurable by including finalizers as a parameter to add on applicationsDeleteconfigurable by introducing a boolean variable to control deleteOptionsTestProgressiveSyncMultipleAppsPerStepintoTestProgressiveSyncMultipleAppsPerStepWithReverseDeletionOrderto check muliple apps per step, becoming healthy, and being deleted in reverse order.local runtime with test-e2e-local:
Checklist: