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

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