v0.7.0 45bfc5b

ci_test.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package rpc
4
5import (
6	"context"
7	"testing"
8	"time"
9
10	"connectrpc.com/connect"
11	"github.com/stretchr/testify/assert"
12	"github.com/stretchr/testify/mock"
13	"github.com/stretchr/testify/require"
14
15	pb "bnema.dev/ermite-api/gen/ermite"
16	"bnema.dev/ermite/internal/domain"
17	"bnema.dev/ermite/internal/ports"
18	"bnema.dev/ermite/internal/ports/mocks"
19)
20
21// RunService.GetJob: the job with its steps and the range of their lines, the
22// states and conclusions of the contract, and the codes of the refusals.
23func TestRunServiceGetJob(t *testing.T) {
24	t.Parallel()
25	at := time.Date(2026, 10, 12, 10, 0, 0, 0, time.UTC)
26	job := ports.JobView{CIJob: domain.CIJob{
27		ID: "7-0-0", RunID: 7, Attempt: 2, RunAttempt: 2, Repo: mustRepoName(t, "demo"), Key: "build", Name: "build (linux)",
28		Phase: domain.CIPhaseEnded, Conclusion: domain.CIFailure, Error: "boom", Labels: []string{"self-hosted"}, Needs: []string{"lint"},
29		RunnerName: "alpha", Isolation: domain.CIIsolationVM, CreatedAt: at, StartedAt: at, CompletedAt: at.Add(time.Minute), Version: 9,
30		Steps: []domain.CIStep{
31			{Number: 1, Name: "Set up job", State: domain.CIStepCompleted, Conclusion: domain.CISuccess, StartedAt: at, CompletedAt: at, HasLines: true, LogStart: 0},
32			{Number: 2, Name: "Run make", State: domain.CIStepCompleted, Conclusion: domain.CIFailure, StartedAt: at, CompletedAt: at, HasLines: true, LogStart: 3},
33		},
34	}, Lines: 8}
35
36	runs := mocks.NewRunService(t)
37	runs.EXPECT().GetJob(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), "7-0-0", int32(2)).Return(job, nil).Once()
38	resp, err := (&runService{runs: runs}).GetJob(ownerCtx(), connect.NewRequest(&pb.GetJobRequest{Repo: "demo", JobId: "7-0-0", Attempt: 2}))
39	require.NoError(t, err)
40	got := resp.Msg.GetJob()
41	assert.Equal(t, "7-0-0", got.GetId())
42	assert.EqualValues(t, 2, got.GetAttempt())
43	assert.Equal(t, "build (linux)", got.GetName())
44	assert.Equal(t, pb.CiStatus_CI_STATUS_COMPLETED, got.GetStatus())
45	assert.Equal(t, pb.CiConclusion_CI_CONCLUSION_FAILURE, got.GetConclusion())
46	assert.Equal(t, "boom", got.GetError())
47	assert.Equal(t, pb.RunnerIsolation_RUNNER_ISOLATION_VM, got.GetIsolation())
48	assert.Equal(t, []string{"lint"}, got.GetNeeds())
49	assert.EqualValues(t, 9, got.GetVersion())
50	require.Len(t, got.GetSteps(), 2)
51	assert.Equal(t, []int64{0, 3}, []int64{got.GetSteps()[0].GetLogStart(), got.GetSteps()[0].GetLogEnd()}, "a step ends where the next begins")
52	assert.Equal(t, []int64{3, 8}, []int64{got.GetSteps()[1].GetLogStart(), got.GetSteps()[1].GetLogEnd()}, "the last ends at the end of the log")
53	assert.Equal(t, pb.StepState_STEP_STATE_COMPLETED, got.GetSteps()[1].GetState())
54
55	for name, tc := range map[string]struct {
56		repo string
57		err  error
58		code connect.Code
59	}{
60		"invalid repo":  {"../x", nil, connect.CodeInvalidArgument},
61		"unknown job":   {"demo", domain.ErrNotFound, connect.CodeNotFound},
62		"not readable":  {"demo", domain.ErrNotFound, connect.CodeNotFound},
63		"store failure": {"demo", assert.AnError, connect.CodeInternal},
64	} {
65		t.Run(name, func(t *testing.T) {
66			t.Parallel()
67			runs := mocks.NewRunService(t)
68			if tc.err != nil {
69				runs.EXPECT().GetJob(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(ports.JobView{}, tc.err).Once()
70			}
71			_, err := (&runService{runs: runs}).GetJob(ownerCtx(), connect.NewRequest(&pb.GetJobRequest{Repo: tc.repo, JobId: "7-0-0"}))
72			requireCode(t, tc.code, err)
73		})
74	}
75}
76
77// The end of a job reaches the runner as failed_precondition with the JobEnded
78// detail; a bad credential counts against the client address.
79func TestRunnerServiceErrors(t *testing.T) {
80	t.Parallel()
81	ended := &domain.JobEndedError{Conclusion: domain.CICancelled, Message: "stopped"}
82	err := ciError(ownerCtx(), ended)
83	requireCode(t, connect.CodeFailedPrecondition, err)
84	var ce *connect.Error
85	require.ErrorAs(t, err, &ce)
86	require.Len(t, ce.Details(), 1)
87	msg, derr := ce.Details()[0].Value()
88	require.NoError(t, derr)
89	assert.Equal(t, pb.CiConclusion_CI_CONCLUSION_CANCELLED, msg.(*pb.JobEnded).GetConclusion())
90	assert.Equal(t, "stopped", msg.(*pb.JobEnded).GetError())
91
92	requireCode(t, connect.CodeOutOfRange, ciError(ownerCtx(), &ports.ErrCILogGap{Count: 4}))
93
94	limiter := mocks.NewFailureLimiter(t)
95	limiter.EXPECT().Fail(mock.Anything, "client-key").Return().Once()
96	s := &runnerService{runners: mocks.NewRunnerService(t), limiter: limiter}
97	ctx := t.Context()
98	ctx = context.WithValue(ctx, addrKey{}, "client-key")
99	requireCode(t, connect.CodeUnauthenticated, s.failed(ctx, domain.ErrUnauthenticated))
100	requireCode(t, connect.CodePermissionDenied, s.failed(ctx, domain.ErrForbidden)) // not a bad credential: no Fail
101}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 45

↑↓ moveenter openesc close