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}