Skip to content

Commit c85668a

Browse files
committed
test: harden gh push-cancel assert against races
The TestGithubSecondPushRequestGitOpsCommentCancel flow was flaky under timing variance because its primary cancel detection path depended on non-deterministic status propagation and compared against the wrong Tekton reason constant. Use PipelineRunReasonCancelled instead of TaskRunReasonCancelled, add UntilRepositoryHasStatusReason(...) scoped by target SHA, make cancellation waits explicit, keep controller-log matching as diagnostic fallback, and add unit coverage for the new wait helper. Signed-off-by: Chmouel Boudjnah <chmouel@redhat.com>
1 parent 42902df commit c85668a

File tree

3 files changed

+190
-16
lines changed

3 files changed

+190
-16
lines changed

test/github_push_retest_test.go

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"regexp"
99
"testing"
10+
"time"
1011

1112
"github.com/google/go-github/v81/github"
1213
"github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/keys"
@@ -183,22 +184,21 @@ func TestGithubSecondPushRequestGitOpsCommentCancel(t *testing.T) {
183184
&github.RepositoryComment{Body: github.Ptr(comment)})
184185
assert.NilError(t, err)
185186

186-
g.Cnx.Clients.Log.Infof("Waiting for Repository to be updated still to %d since it has been cancelled", numberOfStatus)
187-
repo, _ := twait.UntilRepositoryUpdated(ctx, g.Cnx.Clients, waitOpts) // don't check for error, because cancelled is not success and this will fail
188-
cancelled := false
189-
for _, c := range repo.Status {
190-
if c.Conditions[0].Reason == tektonv1.TaskRunReasonCancelled.String() {
191-
cancelled = true
192-
}
193-
}
187+
cancelWaitOpts := waitOpts
188+
cancelWaitOpts.MinNumberStatus = 1
189+
cancelWaitOpts.PollTimeout = 90 * time.Second
194190

195-
// this went too fast so at least we check it was requested for it
196-
if !cancelled {
191+
// wait for the cancellation to propagate through PipelineRun status and repository status
192+
err = twait.UntilPipelineRunHasReason(ctx, g.Cnx.Clients, tektonv1.PipelineRunReasonCancelled, cancelWaitOpts)
193+
if err == nil {
194+
_, err = twait.UntilRepositoryHasStatusReason(ctx, g.Cnx.Clients, cancelWaitOpts, tektonv1.PipelineRunReasonCancelled.String())
195+
}
196+
if err != nil {
197197
numLines := int64(1000)
198-
reg := regexp.MustCompile(".*pipelinerun.*skipping cancelling pipelinerun.*on-push.*already done.*")
199-
err = twait.RegexpMatchingInControllerLog(ctx, g.Cnx, *reg, 10, "ghe-controller", &numLines)
200-
if err != nil {
201-
t.Errorf("neither a cancelled pipelinerun in repo status or a request to skip the cancellation in the controller log was found: %s", err.Error())
198+
reg := regexp.MustCompile(".*cancel-in-progress:.*pipelinerun.*on-push.*")
199+
logErr := twait.RegexpMatchingInControllerLog(ctx, g.Cnx, *reg, 10, "ghe-controller", &numLines)
200+
if logErr != nil {
201+
t.Errorf("neither a cancelled pipelinerun in repo status or a cancellation request in the controller log was found: status wait error: %s, log wait error: %s", err.Error(), logErr.Error())
202202
}
203203
return
204204
}
@@ -209,9 +209,9 @@ func TestGithubSecondPushRequestGitOpsCommentCancel(t *testing.T) {
209209
})
210210
assert.NilError(t, err)
211211
assert.Equal(t, len(pruns.Items), numberOfStatus)
212-
cancelled = false
212+
cancelled := false
213213
for _, pr := range pruns.Items {
214-
if pr.Status.Conditions[0].Reason == tektonv1.TaskRunReasonCancelled.String() {
214+
if len(pr.Status.Conditions) > 0 && pr.Status.Conditions[0].Reason == tektonv1.PipelineRunReasonCancelled.String() {
215215
cancelled = true
216216
}
217217
}

test/pkg/wait/wait.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,42 @@ func UntilRepositoryUpdated(ctx context.Context, clients clients.Clients, opts O
8787
})
8888
}
8989

90+
func UntilRepositoryHasStatusReason(ctx context.Context, clients clients.Clients, opts Opts, reason string) (*pacv1alpha1.Repository, error) {
91+
ctx, cancel := context.WithTimeout(ctx, opts.PollTimeout)
92+
defer cancel()
93+
var repo *pacv1alpha1.Repository
94+
return repo, kubeinteraction.PollImmediateWithContext(ctx, opts.PollTimeout, func() (bool, error) {
95+
var err error
96+
if repo, err = clients.PipelineAsCode.PipelinesascodeV1alpha1().Repositories(opts.Namespace).Get(ctx, opts.RepoName, metav1.GetOptions{}); err != nil {
97+
return true, err
98+
}
99+
100+
matchingStatuses := 0
101+
reasons := []string{}
102+
for _, status := range repo.Status {
103+
if opts.TargetSHA != "" {
104+
if status.SHA == nil || *status.SHA != opts.TargetSHA {
105+
continue
106+
}
107+
}
108+
matchingStatuses++
109+
if len(status.Conditions) == 0 {
110+
continue
111+
}
112+
reasons = append(reasons, status.Conditions[0].Reason)
113+
if status.Conditions[0].Reason == reason {
114+
return true, nil
115+
}
116+
}
117+
118+
clients.Log.Infof(
119+
"Still waiting for repository status reason to be updated: wanted=%q matching statuses=%d reasons=%v",
120+
reason, matchingStatuses, reasons,
121+
)
122+
return false, nil
123+
})
124+
}
125+
90126
func UntilPipelineRunCreated(ctx context.Context, clients clients.Clients, opts Opts) error {
91127
ctx, cancel := context.WithTimeout(ctx, opts.PollTimeout)
92128
defer cancel()

test/pkg/wait/wait_test.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package wait
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
pacv1alpha1 "github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/v1alpha1"
8+
paramsclients "github.com/openshift-pipelines/pipelines-as-code/pkg/params/clients"
9+
testclient "github.com/openshift-pipelines/pipelines-as-code/pkg/test/clients"
10+
"go.uber.org/zap"
11+
"gotest.tools/v3/assert"
12+
corev1 "k8s.io/api/core/v1"
13+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14+
knativeapis "knative.dev/pkg/apis"
15+
duckv1 "knative.dev/pkg/apis/duck/v1"
16+
rtesting "knative.dev/pkg/reconciler/testing"
17+
)
18+
19+
func makeRepositoryRunStatus(sha *string, reason string) pacv1alpha1.RepositoryRunStatus {
20+
status := pacv1alpha1.RepositoryRunStatus{
21+
SHA: sha,
22+
}
23+
if reason != "" {
24+
status.Status = duckv1.Status{
25+
Conditions: []knativeapis.Condition{
26+
{Reason: reason},
27+
},
28+
}
29+
}
30+
return status
31+
}
32+
33+
func strPtr(v string) *string {
34+
return &v
35+
}
36+
37+
func TestUntilRepositoryHasStatusReason(t *testing.T) {
38+
tests := []struct {
39+
name string
40+
statuses []pacv1alpha1.RepositoryRunStatus
41+
targetSHA string
42+
reason string
43+
wantErr bool
44+
}{
45+
{
46+
name: "match by target sha and reason",
47+
statuses: []pacv1alpha1.RepositoryRunStatus{
48+
makeRepositoryRunStatus(strPtr("sha-1"), "Succeeded"),
49+
makeRepositoryRunStatus(strPtr("sha-2"), "Cancelled"),
50+
},
51+
targetSHA: "sha-2",
52+
reason: "Cancelled",
53+
},
54+
{
55+
name: "wrong reason for matching sha",
56+
statuses: []pacv1alpha1.RepositoryRunStatus{
57+
makeRepositoryRunStatus(strPtr("sha-2"), "Succeeded"),
58+
},
59+
targetSHA: "sha-2",
60+
reason: "Cancelled",
61+
wantErr: true,
62+
},
63+
{
64+
name: "reason exists on a different sha only",
65+
statuses: []pacv1alpha1.RepositoryRunStatus{
66+
makeRepositoryRunStatus(strPtr("sha-1"), "Cancelled"),
67+
},
68+
targetSHA: "sha-2",
69+
reason: "Cancelled",
70+
wantErr: true,
71+
},
72+
{
73+
name: "match without target sha filter",
74+
statuses: []pacv1alpha1.RepositoryRunStatus{
75+
makeRepositoryRunStatus(strPtr("sha-1"), "Cancelled"),
76+
},
77+
reason: "Cancelled",
78+
},
79+
{
80+
name: "status without conditions",
81+
statuses: []pacv1alpha1.RepositoryRunStatus{
82+
makeRepositoryRunStatus(strPtr("sha-2"), ""),
83+
},
84+
targetSHA: "sha-2",
85+
reason: "Cancelled",
86+
wantErr: true,
87+
},
88+
}
89+
90+
for _, tt := range tests {
91+
t.Run(tt.name, func(t *testing.T) {
92+
ctx, _ := rtesting.SetupFakeContext(t)
93+
repositoryNS := "test-repository-ns"
94+
repositoryName := "test-repository"
95+
seeded, _ := testclient.SeedTestData(t, ctx, testclient.Data{
96+
Namespaces: []*corev1.Namespace{
97+
{
98+
ObjectMeta: metav1.ObjectMeta{Name: repositoryNS},
99+
},
100+
},
101+
Repositories: []*pacv1alpha1.Repository{
102+
{
103+
ObjectMeta: metav1.ObjectMeta{
104+
Name: repositoryName,
105+
Namespace: repositoryNS,
106+
},
107+
Spec: pacv1alpha1.RepositorySpec{
108+
URL: "https://ghe.pipelinesascode.com/org/repo",
109+
},
110+
Status: tt.statuses,
111+
},
112+
},
113+
})
114+
115+
clients := paramsclients.Clients{
116+
PipelineAsCode: seeded.PipelineAsCode,
117+
Log: zap.NewNop().Sugar(),
118+
}
119+
opts := Opts{
120+
RepoName: repositoryName,
121+
Namespace: repositoryNS,
122+
TargetSHA: tt.targetSHA,
123+
PollTimeout: 10 * time.Millisecond,
124+
}
125+
126+
repository, err := UntilRepositoryHasStatusReason(ctx, clients, opts, tt.reason)
127+
if tt.wantErr {
128+
assert.Assert(t, err != nil)
129+
assert.ErrorContains(t, err, "timed out")
130+
return
131+
}
132+
133+
assert.NilError(t, err)
134+
assert.Assert(t, repository != nil)
135+
assert.Equal(t, repository.GetName(), repositoryName)
136+
})
137+
}
138+
}

0 commit comments

Comments
 (0)