v0.14.0 815fb98

ci_concurrency_test.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package usecase_test
4
5import (
6	"context"
7	"fmt"
8	"strings"
9	"testing"
10
11	"github.com/stretchr/testify/assert"
12	"github.com/stretchr/testify/require"
13
14	"bnema.dev/ermite-actions/expr"
15	"bnema.dev/ermite-actions/template"
16	"bnema.dev/ermite-actions/workflow"
17	"bnema.dev/ermite/internal/domain"
18	"bnema.dev/ermite/internal/usecase"
19)
20
21const concurrencyYAML = `on: push
22concurrency: %s
23jobs:
24  j:
25    runs-on: ubuntu-latest
26    steps:
27      - run: echo
28`
29
30func evalGroup(t *testing.T, value string) (domain.CIConcurrency, error) {
31	t.Helper()
32	wf, errs := workflow.Parse("w.yml", []byte(fmt.Sprintf(concurrencyYAML, value)))
33	require.Empty(t, errs)
34	gh, err := expr.FromJSON([]byte(`{"ref":"refs/heads/main","workflow":"ci"}`))
35	require.NoError(t, err)
36	env := template.Env{Contexts: map[string]expr.Value{"github": gh, "inputs": expr.NewDictionary(), "vars": expr.NewDictionary()}}
37	return usecase.EvalConcurrency(context.Background(), wf.Concurrency, env)
38}
39
40func TestEvalConcurrency(t *testing.T) {
41	t.Parallel()
42	tests := []struct {
43		name, yaml string
44		want       domain.CIConcurrency
45		err        string
46	}{
47		{"a string is the group", `Deploy`, domain.CIConcurrency{Group: "Deploy"}, ""},
48		{"an expression is the group", `${{ github.workflow }}-${{ github.ref }}`, domain.CIConcurrency{Group: "ci-refs/heads/main"}, ""},
49		{"mapping", "{group: g}", domain.CIConcurrency{Group: "g"}, ""},
50		{"cancel true", "{group: g, cancel-in-progress: true}", domain.CIConcurrency{Group: "g", Cancel: true}, ""},
51		{"cancel expression", "{group: g, cancel-in-progress: \"${{ github.ref == 'refs/heads/main' }}\"}", domain.CIConcurrency{Group: "g", Cancel: true}, ""},
52		{"cancel expression false", "{group: g, cancel-in-progress: \"${{ github.ref != 'refs/heads/main' }}\"}", domain.CIConcurrency{Group: "g"}, ""},
53		{"queue single", "{group: g, queue: single}", domain.CIConcurrency{Group: "g"}, ""},
54		{"queue max", "{group: g, queue: max}", domain.CIConcurrency{}, "not supported yet: queue: max"},
55		{"queue max with cancel true", "{group: g, queue: max, cancel-in-progress: true}", domain.CIConcurrency{}, "'queue: max' cannot be combined with 'cancel-in-progress: true'."},
56		{"empty group", `${{ vars.NOPE }}`, domain.CIConcurrency{}, "concurrency group name cannot be empty"},
57	}
58	for _, tt := range tests {
59		t.Run(tt.name, func(t *testing.T) {
60			t.Parallel()
61			got, err := evalGroup(t, tt.yaml)
62			if tt.err != "" {
63				require.Error(t, err)
64				assert.Contains(t, err.Error(), tt.err)
65				return
66			}
67			require.NoError(t, err)
68			assert.Equal(t, tt.want, got)
69		})
70	}
71	// GitHub's limit counts UTF-16 code units, like C#'s String.Length.
72	for _, tc := range []struct {
73		name, group string
74		ok          bool
75	}{
76		{"400 BMP characters", strings.Repeat("é", 400), true},
77		{"401 BMP characters", strings.Repeat("é", 401), false},
78		{"200 emoji are 400 units", strings.Repeat("😀", 200), true},
79		{"201 emoji are 402 units", strings.Repeat("😀", 201), false},
80		{"199 emoji and 2 letters", strings.Repeat("😀", 199) + "ab", true},
81		{"199 emoji and 3 letters", strings.Repeat("😀", 199) + "abc", false},
82	} {
83		t.Run(tc.name, func(t *testing.T) {
84			t.Parallel()
85			_, err := evalGroup(t, `"`+tc.group+`"`)
86			if tc.ok {
87				assert.NoError(t, err)
88				return
89			}
90			require.Error(t, err)
91			assert.Contains(t, err.Error(), "less than 400 characters")
92		})
93	}
94	t.Run("too long", func(t *testing.T) {
95		t.Parallel()
96		_, err := evalGroup(t, strings.Repeat("a", 401))
97		require.Error(t, err)
98		assert.Contains(t, err.Error(), "less than 400 characters")
99	})
100}
101
102func TestConcurrencyIdentity(t *testing.T) {
103	t.Parallel()
104	assert.Equal(t, domain.ConcurrencyKey("Deploy"), domain.ConcurrencyKey("DEPLOY"))
105	assert.Equal(t, "Canceling since a higher priority waiting request for 'g' exists", domain.ConcurrencyCancelMessage("g"))
106	pending := domain.CIJob{Phase: domain.CIPhaseWaiting, Concurrency: domain.CIConcurrency{State: domain.CIConcurrencyPending}}
107	assert.Equal(t, domain.CIPending, pending.Status())
108	pending.Phase = domain.CIPhaseEnded
109	assert.Equal(t, domain.CICompleted, pending.Status(), "an ended job is not pending any more")
110	assert.Equal(t, domain.CIQueued, domain.CIJob{Phase: domain.CIPhaseWaiting}.Status())
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 44

↑↓ moveenter openesc close