cli/api/pull_request_test.go
Kynan Ware db1dc976c2 test: use require.Equal instead of reflect.DeepEqual
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-11 16:03:33 -06:00

624 lines
16 KiB
Go

package api
import (
"encoding/json"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestChecksStatus_NoCheckRunsOrStatusContexts(t *testing.T) {
t.Parallel()
payload := `
{ "statusCheckRollup": { "nodes": [] } }
`
var pr PullRequest
require.NoError(t, json.Unmarshal([]byte(payload), &pr))
expectedChecksStatus := PullRequestChecksStatus{
Pending: 0,
Failing: 0,
Passing: 0,
Total: 0,
}
require.Equal(t, expectedChecksStatus, pr.ChecksStatus())
}
func TestChecksStatus_SummarisingCheckRuns(t *testing.T) {
t.Parallel()
tests := []struct {
name string
payload string
expectedChecksStatus PullRequestChecksStatus
}{
{
name: "QUEUED is treated as Pending",
payload: singleCheckRunWithStatus("QUEUED"),
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
},
{
name: "IN_PROGRESS is treated as Pending",
payload: singleCheckRunWithStatus("IN_PROGRESS"),
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
},
{
name: "WAITING is treated as Pending",
payload: singleCheckRunWithStatus("WAITING"),
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
},
{
name: "PENDING is treated as Pending",
payload: singleCheckRunWithStatus("PENDING"),
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
},
{
name: "REQUESTED is treated as Pending",
payload: singleCheckRunWithStatus("REQUESTED"),
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
},
{
name: "COMPLETED with no conclusion is treated as Pending",
payload: singleCheckRunWithStatus("COMPLETED"),
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
},
{
name: "COMPLETED / STARTUP_FAILURE is treated as Pending",
payload: singleCompletedCheckRunWithConclusion("STARTUP_FAILURE"),
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
},
{
name: "COMPLETED / STALE is treated as Pending",
payload: singleCompletedCheckRunWithConclusion("STALE"),
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
},
{
name: "COMPLETED / SUCCESS is treated as Passing",
payload: singleCompletedCheckRunWithConclusion("SUCCESS"),
expectedChecksStatus: PullRequestChecksStatus{Passing: 1, Total: 1},
},
{
name: "COMPLETED / NEUTRAL is treated as Passing",
payload: singleCompletedCheckRunWithConclusion("NEUTRAL"),
expectedChecksStatus: PullRequestChecksStatus{Passing: 1, Total: 1},
},
{
name: "COMPLETED / SKIPPED is treated as Passing",
payload: singleCompletedCheckRunWithConclusion("SKIPPED"),
expectedChecksStatus: PullRequestChecksStatus{Passing: 1, Total: 1},
},
{
name: "COMPLETED / ACTION_REQUIRED is treated as Failing",
payload: singleCompletedCheckRunWithConclusion("ACTION_REQUIRED"),
expectedChecksStatus: PullRequestChecksStatus{Failing: 1, Total: 1},
},
{
name: "COMPLETED / TIMED_OUT is treated as Failing",
payload: singleCompletedCheckRunWithConclusion("TIMED_OUT"),
expectedChecksStatus: PullRequestChecksStatus{Failing: 1, Total: 1},
},
{
name: "COMPLETED / CANCELLED is excluded from counts",
payload: singleCompletedCheckRunWithConclusion("CANCELLED"),
expectedChecksStatus: PullRequestChecksStatus{},
},
{
name: "COMPLETED / CANCELLED is excluded from counts (duplicate)",
payload: singleCompletedCheckRunWithConclusion("CANCELLED"),
expectedChecksStatus: PullRequestChecksStatus{},
},
{
name: "COMPLETED / FAILURE is treated as Failing",
payload: singleCompletedCheckRunWithConclusion("FAILURE"),
expectedChecksStatus: PullRequestChecksStatus{Failing: 1, Total: 1},
},
{
name: "Unrecognized Status are treated as Pending",
payload: singleCheckRunWithStatus("AnUnrecognizedStatusJustForThisTest"),
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
},
{
name: "Unrecognized Conclusions are treated as Pending",
payload: singleCompletedCheckRunWithConclusion("AnUnrecognizedConclusionJustForThisTest"),
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var pr PullRequest
require.NoError(t, json.Unmarshal([]byte(tt.payload), &pr))
require.Equal(t, tt.expectedChecksStatus, pr.ChecksStatus())
})
}
}
func TestChecksStatus_SummarisingStatusContexts(t *testing.T) {
t.Parallel()
tests := []struct {
name string
payload string
expectedChecksStatus PullRequestChecksStatus
}{
{
name: "EXPECTED is treated as Pending",
payload: singleStatusContextWithState("EXPECTED"),
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
},
{
name: "PENDING is treated as Pending",
payload: singleStatusContextWithState("PENDING"),
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
},
{
name: "SUCCESS is treated as Passing",
payload: singleStatusContextWithState("SUCCESS"),
expectedChecksStatus: PullRequestChecksStatus{Passing: 1, Total: 1},
},
{
name: "ERROR is treated as Failing",
payload: singleStatusContextWithState("ERROR"),
expectedChecksStatus: PullRequestChecksStatus{Failing: 1, Total: 1},
},
{
name: "FAILURE is treated as Failing",
payload: singleStatusContextWithState("FAILURE"),
expectedChecksStatus: PullRequestChecksStatus{Failing: 1, Total: 1},
},
{
name: "Unrecognized States are treated as Pending",
payload: singleStatusContextWithState("AnUnrecognizedStateJustForThisTest"),
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var pr PullRequest
require.NoError(t, json.Unmarshal([]byte(tt.payload), &pr))
require.Equal(t, tt.expectedChecksStatus, pr.ChecksStatus())
})
}
}
func TestChecksStatus_SummarisingCheckRunsAndStatusContexts(t *testing.T) {
t.Parallel()
// This might look a bit intimidating, but we're just inserting three nodes
// into the rollup, two completed check run nodes and one status context node.
payload := fmt.Sprintf(`
{ "statusCheckRollup": { "nodes": [{ "commit": {
"statusCheckRollup": {
"contexts": {
"nodes": [
%s,
%s,
%s
]
}
}
} }] } }
`,
completedCheckRunNodeWithName("build", "SUCCESS"),
statusContextNodeWithName("ci/deploy", "PENDING"),
completedCheckRunNodeWithName("lint", "FAILURE"),
)
var pr PullRequest
require.NoError(t, json.Unmarshal([]byte(payload), &pr))
expectedChecksStatus := PullRequestChecksStatus{
Pending: 1,
Failing: 1,
Passing: 1,
Total: 3,
}
require.Equal(t, expectedChecksStatus, pr.ChecksStatus())
}
func TestChecksStatus_SummarisingCheckRunAndStatusContextCountsByState(t *testing.T) {
t.Parallel()
payload := `
{ "statusCheckRollup": { "nodes": [{ "commit": {
"statusCheckRollup": {
"contexts": {
"checkRunCount": 14,
"checkRunCountsByState": [
{
"state": "ACTION_REQUIRED",
"count": 1
},
{
"state": "CANCELLED",
"count": 1
},
{
"state": "COMPLETED",
"count": 1
},
{
"state": "FAILURE",
"count": 1
},
{
"state": "IN_PROGRESS",
"count": 1
},
{
"state": "NEUTRAL",
"count": 1
},
{
"state": "PENDING",
"count": 1
},
{
"state": "QUEUED",
"count": 1
},
{
"state": "SKIPPED",
"count": 1
},
{
"state": "STALE",
"count": 1
},
{
"state": "STARTUP_FAILURE",
"count": 1
},
{
"state": "SUCCESS",
"count": 1
},
{
"state": "TIMED_OUT",
"count": 1
},
{
"state": "WAITING",
"count": 1
},
{
"state": "AnUnrecognizedStateJustForThisTest",
"count": 1
}
],
"statusContextCount": 6,
"statusContextCountsByState": [
{
"state": "EXPECTED",
"count": 1
},
{
"state": "ERROR",
"count": 1
},
{
"state": "FAILURE",
"count": 1
},
{
"state": "PENDING",
"count": 1
},
{
"state": "SUCCESS",
"count": 1
},
{
"state": "AnUnrecognizedStateJustForThisTest",
"count": 1
}
]
}
}
} }] } }
`
var pr PullRequest
require.NoError(t, json.Unmarshal([]byte(payload), &pr))
expectedChecksStatus := PullRequestChecksStatus{
Pending: 11,
Failing: 5,
Passing: 4,
Total: 19,
}
require.Equal(t, expectedChecksStatus, pr.ChecksStatus())
}
// Note that it would be incorrect to provide a status of COMPLETED here
// as the conclusion is always set to null. If you want a COMPLETED status,
// use `singleCompletedCheckRunWithConclusion`.
func singleCheckRunWithStatus(status string) string {
return fmt.Sprintf(`
{ "statusCheckRollup": { "nodes": [{ "commit": {
"statusCheckRollup": {
"contexts": {
"nodes": [
{
"__typename": "CheckRun",
"status": "%s",
"conclusion": null
}
]
}
}
} }] } }
`, status)
}
func singleCompletedCheckRunWithConclusion(conclusion string) string {
return fmt.Sprintf(`
{ "statusCheckRollup": { "nodes": [{ "commit": {
"statusCheckRollup": {
"contexts": {
"nodes": [
{
"__typename": "CheckRun",
"status": "COMPLETED",
"conclusion": "%s"
}
]
}
}
} }] } }
`, conclusion)
}
func singleStatusContextWithState(state string) string {
return fmt.Sprintf(`
{ "statusCheckRollup": { "nodes": [{ "commit": {
"statusCheckRollup": {
"contexts": {
"nodes": [
{
"__typename": "StatusContext",
"state": "%s"
}
]
}
}
} }] } }
`, state)
}
func completedCheckRunNodeWithName(name, conclusion string) string {
return fmt.Sprintf(`
{
"__typename": "CheckRun",
"name": "%s",
"status": "COMPLETED",
"conclusion": "%s"
}`, name, conclusion)
}
func statusContextNodeWithName(context, state string) string {
return fmt.Sprintf(`
{
"__typename": "StatusContext",
"context": "%s",
"state": "%s"
}`, context, state)
}
func TestChecksStatus_DuplicateCheckRunsAreDeduplicated(t *testing.T) {
t.Parallel()
// Simulate cancel-in-progress: a cancelled run followed by a newer successful run
// for the same check name. Only the newer (successful) run should be counted.
pr := PullRequest{}
pr.StatusCheckRollup.Nodes = []StatusCheckRollupNode{
{
Commit: StatusCheckRollupCommit{
StatusCheckRollup: CommitStatusCheckRollup{
Contexts: CheckContexts{
Nodes: []CheckContext{
{
TypeName: "CheckRun",
Name: "Prevent Merging",
Status: "COMPLETED",
Conclusion: CheckConclusionStateCancelled,
StartedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
},
{
TypeName: "CheckRun",
Name: "Prevent Merging",
Status: "COMPLETED",
Conclusion: CheckConclusionStateSuccess,
StartedAt: time.Date(2024, 1, 1, 1, 0, 0, 0, time.UTC),
},
{
TypeName: "CheckRun",
Name: "Build",
Status: "COMPLETED",
Conclusion: CheckConclusionStateSuccess,
StartedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
},
},
},
},
},
},
}
status := pr.ChecksStatus()
require.Equal(t, PullRequestChecksStatus{
Passing: 2,
Failing: 0,
Pending: 0,
Total: 2,
}, status)
}
func TestEliminateDuplicateChecks(t *testing.T) {
t.Parallel()
tests := []struct {
name string
checkContexts []CheckContext
want []CheckContext
}{
{
name: "duplicate CheckRun keeps most recent",
checkContexts: []CheckContext{
{
TypeName: "CheckRun",
Name: "lint",
Status: "COMPLETED",
Conclusion: "FAILURE",
StartedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
},
{
TypeName: "CheckRun",
Name: "lint",
Status: "COMPLETED",
Conclusion: "SUCCESS",
StartedAt: time.Date(2022, 2, 2, 2, 2, 2, 2, time.UTC),
},
{
TypeName: "CheckRun",
Name: "build",
Status: "COMPLETED",
Conclusion: "SUCCESS",
StartedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
},
},
want: []CheckContext{
{
TypeName: "CheckRun",
Name: "lint",
Status: "COMPLETED",
Conclusion: "SUCCESS",
StartedAt: time.Date(2022, 2, 2, 2, 2, 2, 2, time.UTC),
},
{
TypeName: "CheckRun",
Name: "build",
Status: "COMPLETED",
Conclusion: "SUCCESS",
StartedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
},
},
},
{
name: "duplicate StatusContext keeps most recent",
checkContexts: []CheckContext{
{
TypeName: "StatusContext",
Context: "ci/test",
State: "FAILURE",
StartedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
},
{
TypeName: "StatusContext",
Context: "ci/test",
State: "SUCCESS",
StartedAt: time.Date(2022, 2, 2, 2, 2, 2, 2, time.UTC),
},
},
want: []CheckContext{
{
TypeName: "StatusContext",
Context: "ci/test",
State: "SUCCESS",
StartedAt: time.Date(2022, 2, 2, 2, 2, 2, 2, time.UTC),
},
},
},
{
name: "unique checks are preserved",
checkContexts: []CheckContext{
{
TypeName: "CheckRun",
Name: "build",
Status: "COMPLETED",
Conclusion: "SUCCESS",
StartedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
},
{
TypeName: "StatusContext",
Context: "ci/test",
State: "SUCCESS",
StartedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
},
},
want: []CheckContext{
{
TypeName: "CheckRun",
Name: "build",
Status: "COMPLETED",
Conclusion: "SUCCESS",
StartedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
},
{
TypeName: "StatusContext",
Context: "ci/test",
State: "SUCCESS",
StartedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
},
},
},
{
name: "different workflow names are not deduplicated",
checkContexts: []CheckContext{
{
TypeName: "CheckRun",
Name: "build",
Status: "COMPLETED",
Conclusion: "SUCCESS",
StartedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
CheckSuite: CheckSuite{WorkflowRun: WorkflowRun{Event: "push", Workflow: Workflow{Name: "CI"}}},
},
{
TypeName: "CheckRun",
Name: "build",
Status: "COMPLETED",
Conclusion: "SUCCESS",
StartedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
CheckSuite: CheckSuite{WorkflowRun: WorkflowRun{Event: "push", Workflow: Workflow{Name: "Release"}}},
},
},
want: []CheckContext{
{
TypeName: "CheckRun",
Name: "build",
Status: "COMPLETED",
Conclusion: "SUCCESS",
StartedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
CheckSuite: CheckSuite{WorkflowRun: WorkflowRun{Event: "push", Workflow: Workflow{Name: "CI"}}},
},
{
TypeName: "CheckRun",
Name: "build",
Status: "COMPLETED",
Conclusion: "SUCCESS",
StartedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
CheckSuite: CheckSuite{WorkflowRun: WorkflowRun{Event: "push", Workflow: Workflow{Name: "Release"}}},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := EliminateDuplicateChecks(tt.checkContexts)
require.Equal(t, tt.want, got)
})
}
}