-
Notifications
You must be signed in to change notification settings - Fork 0
test(parity): retry parity sides on transient docker build flakes #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| package cli | ||
|
|
||
| import ( | ||
| "context" | ||
| "testing" | ||
| ) | ||
|
|
||
| // TestIsRetryableFailure guards the classifier that decides whether a non-zero | ||
| // parity side is a transient build/environment flake (retryable) or a stable | ||
| // product divergence (not). The two positive fixtures are the exact signatures | ||
| // that turned dependabot PRs #9/#10 red: a BuildKit "failed to solve" on one | ||
| // side and the reference's container-setup wrapper on the other, while the other | ||
| // side succeeded. The negative fixtures must stay non-retryable so a real CLI | ||
| // contract failure is never masked by a retry. | ||
| func TestIsRetryableFailure(t *testing.T) { | ||
| cases := []struct { | ||
| name string | ||
| stdout string | ||
| stderr string | ||
| wantRetryable bool | ||
| }{ | ||
| { | ||
| name: "buildkit failed to solve (isInfraError)", | ||
| stderr: "ERROR: failed to solve: rpc error: code = Unknown desc = failed to fetch", | ||
| wantRetryable: true, | ||
| }, | ||
| { | ||
| name: "docker daemon unavailable (isInfraError)", | ||
| stderr: "Cannot connect to the Docker daemon at unix:///var/run/docker.sock", | ||
| wantRetryable: true, | ||
| }, | ||
| { | ||
| // PR #10 / main 07-14 signature: TS reference build flaked and the | ||
| // low-level BuildKit error is hidden behind the generic wrapper. | ||
| name: "reference container-setup wrapper", | ||
| stdout: `{"description":"An error occurred setting up the container.","message":"Command failed: docker build -f /tmp/x -t vsc-parity-dotfiles --platform linux/amd64 /tmp/x","outcome":"error"}`, | ||
| wantRetryable: true, | ||
| }, | ||
| { | ||
| name: "config not found is a stable contract error", | ||
| stdout: `{"outcome":"error","message":"Dev container config (path/devcontainer.json) not found."}`, | ||
| wantRetryable: false, | ||
| }, | ||
| { | ||
| name: "flag validation is a stable contract error", | ||
| stderr: "Unknown argument: --bogus-flag", | ||
| wantRetryable: false, | ||
| }, | ||
| { | ||
| name: "empty output is not retryable", | ||
| wantRetryable: false, | ||
| }, | ||
| } | ||
| for _, tc := range cases { | ||
| t.Run(tc.name, func(t *testing.T) { | ||
| if got := isRetryableFailure(tc.stdout, tc.stderr); got != tc.wantRetryable { | ||
| t.Errorf("isRetryableFailure(%q, %q) = %v, want %v", tc.stdout, tc.stderr, got, tc.wantRetryable) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // TestRunWithInfraRetry asserts the retry loop absorbs a transient build flake | ||
| // (success on the second attempt) while never masking a stable failure: a | ||
| // deterministic contract error runs exactly once, and a persistent transient | ||
| // error is retried up to the cap and then surfaced as the final non-zero result. | ||
| func TestRunWithInfraRetry(t *testing.T) { | ||
| const transient = "ERROR: failed to solve: connection reset by peer" | ||
| const stable = `{"outcome":"error","message":"Dev container config not found."}` | ||
|
|
||
| t.Run("transient failure then success is absorbed", func(t *testing.T) { | ||
| calls := 0 | ||
| _, _, exit := runWithInfraRetry(context.Background(), parityInfraRetries, func() (string, string, int) { | ||
| calls++ | ||
| if calls == 1 { | ||
| return "", transient, 1 | ||
| } | ||
| return "ok", "", 0 | ||
| }) | ||
| if exit != 0 { | ||
| t.Errorf("exit = %d, want 0 (flake should be absorbed)", exit) | ||
| } | ||
| if calls != 2 { | ||
| t.Errorf("calls = %d, want 2 (one retry)", calls) | ||
| } | ||
| }) | ||
|
|
||
| t.Run("stable contract failure is not retried", func(t *testing.T) { | ||
| calls := 0 | ||
| _, _, exit := runWithInfraRetry(context.Background(), parityInfraRetries, func() (string, string, int) { | ||
| calls++ | ||
| return stable, "", 1 | ||
| }) | ||
| if exit != 1 { | ||
| t.Errorf("exit = %d, want 1", exit) | ||
| } | ||
| if calls != 1 { | ||
| t.Errorf("calls = %d, want 1 (no retry for a stable contract error)", calls) | ||
| } | ||
| }) | ||
|
|
||
| t.Run("persistent transient failure surfaces after the cap", func(t *testing.T) { | ||
| calls := 0 | ||
| _, stderr, exit := runWithInfraRetry(context.Background(), parityInfraRetries, func() (string, string, int) { | ||
| calls++ | ||
| return "", transient, 1 | ||
| }) | ||
| if exit != 1 { | ||
| t.Errorf("exit = %d, want 1 (real failure must still go red)", exit) | ||
| } | ||
| if calls != parityInfraRetries { | ||
| t.Errorf("calls = %d, want %d (retry cap)", calls, parityInfraRetries) | ||
| } | ||
| if stderr != transient { | ||
| t.Errorf("stderr = %q, want the final attempt's output", stderr) | ||
| } | ||
| }) | ||
|
|
||
| t.Run("cancelled context stops retries", func(t *testing.T) { | ||
| ctx, cancel := context.WithCancel(context.Background()) | ||
| cancel() | ||
| calls := 0 | ||
| _, _, exit := runWithInfraRetry(ctx, parityInfraRetries, func() (string, string, int) { | ||
| calls++ | ||
| return "", transient, 1 | ||
| }) | ||
| if exit != 1 { | ||
| t.Errorf("exit = %d, want 1", exit) | ||
| } | ||
| if calls != 1 { | ||
| t.Errorf("calls = %d, want 1 (no retry once the deadline passed)", calls) | ||
| } | ||
| }) | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.