main 2aca1df

ci_github_test.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package usecase_test
4
5import (
6	"bytes"
7	"context"
8	"encoding/json"
9	"fmt"
10	"math"
11	"strconv"
12	"strings"
13	"testing"
14
15	"bnema.dev/ermite/internal/ports/mocks"
16	"github.com/bnema/zerowrap"
17	"github.com/stretchr/testify/assert"
18	"github.com/stretchr/testify/mock"
19	"github.com/stretchr/testify/require"
20
21	"bnema.dev/ermite-actions/workflow"
22	"bnema.dev/ermite/internal/domain"
23	"bnema.dev/ermite/internal/usecase"
24)
25
26// The title of a run is the run-name when it evaluates to something, else the
27// commit title; a run-name is cut to its first line and 256 characters.
28func TestRunTitle(t *testing.T) {
29	t.Parallel()
30	const commit = "fix: the commit title"
31	const githubJSON = `{"ref_name":"main","event":{"head_commit":{"message":"one\ntwo"}}}`
32	tests := []struct {
33		name    string
34		runName string // the run-name expression, "" : the workflow has none
35		want    string
36	}{
37		{"no run-name", "", commit},
38		{"a run-name", "deploy ${{ github.ref_name }}", "deploy main"},
39		{"an evaluation error", "${{ fromJSON('{') }}", commit},
40		{"an empty result", "${{ '' }}", commit},
41		{"a blank result", "${{ '   ' }}", commit},
42		{"a first line only", "${{ github.event.head_commit.message }}", "one"},
43		{"cut at 256 characters", strings.Repeat("é", 300), strings.Repeat("é", 256)},
44		{"exactly 256 characters", strings.Repeat("a", 256), strings.Repeat("a", 256)},
45	}
46	for _, tt := range tests {
47		t.Run(tt.name, func(t *testing.T) {
48			t.Parallel()
49			u := newCIUnit(t, nil)
50			yaml := "on: push\njobs:\n  a:\n    runs-on: x\n    steps:\n      - run: 'true'\n"
51			if tt.runName != "" {
52				yaml = "run-name: \"" + strings.ReplaceAll(strings.ReplaceAll(tt.runName, `\`, `\\`), `"`, `\"`) + "\"\n" + yaml
53			}
54			wf, errs := workflow.Parse(".github/workflows/a.yml", []byte(yaml))
55			require.Empty(t, errs)
56			require.NotNil(t, wf)
57			assert.Equal(t, tt.want, u.ci.RunTitle(context.Background(), wf, githubJSON, commit))
58		})
59	}
60}
61
62// The check run id keeps the run, its attempt, the job and the leg apart, stays
63// below 2^53, and refuses what does not fit instead of wrapping.
64func TestCheckRunID(t *testing.T) {
65	t.Parallel()
66	ctx := context.Background()
67	run := func(id int64, attempt int32) domain.CIRun { return domain.CIRun{ID: id, Attempt: attempt} }
68	seen := map[int64]string{}
69	for _, id := range []int64{1, 2, 1<<27 - 1} {
70		for _, attempt := range []int32{1, 2, 127} {
71			for _, index := range []int{0, 1, 1023} {
72				for _, combo := range []int{0, 1, 255, 511} {
73					got := usecase.CheckRunID(ctx, run(id, attempt), index, combo)
74					require.NotZero(t, got)
75					assert.Less(t, got, int64(1)<<53)
76					key := fmt.Sprintf("run %d attempt %d job %d leg %d", id, attempt, index, combo)
77					assert.NotContains(t, seen, got, "%s collides", key)
78					seen[got] = key
79				}
80			}
81		}
82	}
83	a := usecase.CheckRunID(ctx, run(7, 1), 0, 0)
84	b := usecase.CheckRunID(ctx, run(7, 2), 0, 0)
85	assert.NotEqual(t, a, b, "a re-run gets new ids")
86	again := usecase.CheckRunID(ctx, run(7, 1), 0, 0)
87	assert.Equal(t, a, again, "stable per attempt")
88
89	// In range, the layout is fixed: leg in the low bits, then job, attempt, run.
90	assert.Equal(t, int64(5)<<26|int64(3)<<19|int64(2)<<9|1, usecase.CheckRunID(ctx, run(5, 3), 2, 1))
91
92	// Out of range, the id is 0 and the overflow is logged at warn, never an
93	// error: the key is informational.
94	for name, c := range map[string]struct {
95		run          domain.CIRun
96		index, combo int
97	}{
98		"job index":    {run(1, 1), 1024, 0},
99		"negative job": {run(1, 1), -1, 0},
100		"leg":          {run(1, 1), 0, 512},
101		"attempt":      {run(1, 128), 0, 0},
102		"run id":       {run(1<<27, 1), 0, 0},
103		"huge run id":  {run(math.MaxInt64, 1), 0, 0},
104		"negative run": {run(-1, 1), 0, 0},
105	} {
106		var logs bytes.Buffer
107		lctx := zerowrap.WithCtx(ctx, zerowrap.New(zerowrap.Config{Level: "debug", Format: "json", Output: &logs}))
108		assert.Zero(t, usecase.CheckRunID(lctx, c.run, c.index, c.combo), name)
109		assert.Contains(t, logs.String(), `"level":"warn"`, name)
110		assert.Contains(t, logs.String(), `"run_id":`+strconv.FormatInt(c.run.ID, 10), name)
111		assert.Contains(t, logs.String(), `"attempt":`+strconv.Itoa(int(c.run.Attempt)), name)
112		assert.Contains(t, logs.String(), `"job_index":`+strconv.Itoa(c.index), name)
113	}
114}
115
116func TestGitHubRepoReportsAllowAutoMerge(t *testing.T) {
117	for _, allow := range []bool{false, true} {
118		t.Run(strconv.FormatBool(allow), func(t *testing.T) {
119			repos := mocks.NewGitRepos(t)
120			u := newCIUnit(t, func(cfg *usecase.CIConfig) { cfg.Repos = repos })
121			repo := domain.Repo{Name: name(t, "proj"), Visibility: domain.VisibilityPublic, Settings: domain.DefaultRepoSettings()}
122			repo.Settings.AllowAutoMerge = allow
123			u.meta.EXPECT().Get(mock.Anything, repo.Name).Return(repo, nil)
124			repos.EXPECT().HeadBranch(mock.Anything, repo.Name).Return("main", nil)
125			data, err := u.ci.GetRepo(context.Background(), domain.Root(), "owner", "proj")
126			require.NoError(t, err)
127			var obj map[string]any
128			require.NoError(t, json.Unmarshal(data, &obj))
129			assert.Equal(t, allow, obj["allow_auto_merge"])
130		})
131	}
132}

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 43

↑↓ moveenter openesc close