v0.4.1 122e784

commands_parity_test.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package executor
4
5import (
6	"regexp"
7	"strings"
8	"testing"
9	"time"
10
11	"bnema.dev/ermite-actions/expr"
12)
13
14// Texts recorded on github.com (ermite-conformance, scenarios match-basic).
15func TestMatcherFileErrorsAreTheRunners(t *testing.T) {
16	_, err := parseMatchersConfig([]byte("not json\n"))
17	const want = "Unexpected character encountered while parsing value: n. Path '', line 0, position 0."
18	if err == nil || err.Error() != want {
19		t.Errorf("not JSON: %v", err)
20	}
21	cfg, err := parseMatchersConfig([]byte(`{"problemMatcher":[{"owner":"badregexp","pattern":[{"regexp":"^INVALID (.+$","message":1}]}]}`))
22	if err != nil {
23		t.Fatal(err)
24	}
25	err = cfg.validate()
26	const wantRe = "Invalid pattern '^INVALID (.+$' at offset 13. Not enough )'s."
27	if err == nil || err.Error() != wantRe {
28		t.Errorf("regexp: %v", err)
29	}
30	// Json.NET accepts what RFC 8259 does not.
31	if _, err := parseMatchersConfig([]byte(`{'problemMatcher':[],}`)); err != nil {
32		t.Errorf("lenient JSON: %v", err)
33	}
34}
35
36// cmd-add-path-disabled: ArgUtil.NotNullOrEmpty throws ArgumentNullException,
37// whose message is "Value cannot be null. (Parameter 'path')".
38func TestAddPathWithoutPathMessage(t *testing.T) {
39	needTools(t, "bash")
40	rec, _ := runLocal(t, wfHead+"    steps:\n      - name: p\n        env:\n          ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'\n        run: echo '::add-path::'\n", "j", nil)
41	if got := logOf(rec.step(t, "p")); !strings.Contains(got, "##[error]Value cannot be null. (Parameter 'path')") || strings.Contains(got, "null or empty") {
42		t.Error(got)
43	}
44}
45
46// cmd-stop-commands: the exception of ValidateStopToken is not inside the try
47// block of TryProcessCommand, so the step fails with the message alone (no
48// "Unable to process command") and the output after it is not read.
49func TestInvalidStopTokenFailsTheStepWithTheMessageOnly(t *testing.T) {
50	needTools(t, "bash")
51	for _, tok := range []string{"pause-logging", "", "warning", "PAUSE-LOGGING"} {
52		rec, res := runLocal(t, wfHead+"    steps:\n      - name: s\n        run: |\n          echo '::stop-commands::"+tok+"'\n          echo '::warning::still processed'\n", "j", nil)
53		got := logOf(rec.step(t, "s"))
54		if strings.Contains(got, "Unable to process command") || strings.Contains(got, "still processed\n") && strings.Contains(got, "##[warning]") {
55			t.Errorf("%q: %s", tok, got)
56		}
57		if !strings.Contains(got, "##[error]"+unsupportedStopTokenDisabled) || rec.step(t, "s").Conclusion != ConclusionFailure || res.Conclusion != ConclusionFailure {
58			t.Errorf("%q: %s", tok, got)
59		}
60	}
61}
62
63// ann-levels, ann-error-fails, match-basic: GitHub reports end_line = line (and
64// end_column = col) when the command gave only the start.
65func TestAnnotationEndsWhereItStartsByDefault(t *testing.T) {
66	j := &job{}
67	j.masker.Store(newMasker())
68	a := j.annotationOf(issue{Level: LevelWarning, Props: map[string]string{"line": "2", "col": "3"}}, "m", 0)
69	if a.StartLine != 2 || a.EndLine != 2 || a.StartColumn != 3 || a.EndColumn != 3 {
70		t.Errorf("%+v", a)
71	}
72	a = j.annotationOf(issue{Level: LevelWarning, Props: map[string]string{"line": "2", "endline": "5"}}, "m", 0)
73	if a.EndLine != 5 || a.EndColumn != 0 {
74		t.Errorf("%+v", a)
75	}
76}
77
78// sum-env: the file of a step is still there in the next one.
79func TestFileCommandsOfAStepSurviveTheNextStep(t *testing.T) {
80	needTools(t, "bash")
81	rec, _ := runLocal(t, wfHead+`    steps:
82      - run: echo "$GITHUB_STEP_SUMMARY" > "$RUNNER_TEMP/p"
83      - name: next
84        run: test -e "$(cat "$RUNNER_TEMP/p")" && echo yes || echo no
85`, "j", nil)
86	if got := logOf(rec.step(t, "next")); !strings.HasSuffix(got, "\nyes") {
87		t.Error(got)
88	}
89}
90
91// ctr-*, svc-*, act-docker-image: a step made by the runner is in the steps
92// context under a 32-hex id (a Guid in "N" format), with its outcome and
93// conclusion and no outputs.
94func TestRunnerStepsAreInTheStepsContext(t *testing.T) {
95	st := newTestState(t)
96	j := &job{state: st}
97	s := &stepRun{j: j, ctxID: newRunnerStepID()}
98	j.finishStep(s, resSucceeded)
99	v, ok := st.Values(nil)["steps"].(*expr.Dictionary)
100	if !ok || len(v.Pairs()) != 1 {
101		t.Fatalf("%v", v)
102	}
103	p := v.Pairs()[0]
104	if !regexp.MustCompile(`^[0-9a-f]{32}$`).MatchString(p.Key) {
105		t.Errorf("id %q", p.Key)
106	}
107	d, _ := p.Value.(*expr.Dictionary)
108	if d == nil || strings.Join(d.Keys(), ",") != "outputs,outcome,conclusion" {
109		t.Errorf("%v", p.Value)
110	}
111	if o, _ := d.Get("outcome"); o != expr.String("success") {
112		t.Errorf("outcome %v", o)
113	}
114}
115
116// sum-too-large: a summary of exactly 1 MiB is accepted (the runner tests the
117// size of the file), 1 MiB plus one byte is refused with "got 1024k", 2 MiB
118// with "got 2048k".
119func TestStepSummarySizeLimit(t *testing.T) {
120	needTools(t, "bash")
121	rr, _ := runResults(t, wfHead+`    steps:
122      - name: exact
123        run: head -c $((1024 * 1024)) /dev/zero | tr '\0' b >> "$GITHUB_STEP_SUMMARY"
124      - name: plus1
125        run: head -c $((1024 * 1024 + 1)) /dev/zero | tr '\0' c >> "$GITHUB_STEP_SUMMARY"
126      - name: two
127        run: head -c $((2 * 1024 * 1024)) /dev/zero | tr '\0' d >> "$GITHUB_STEP_SUMMARY"
128`, nil)
129	const tail = ", supports content up to a size of 1024k, got "
130	if got := logOf(rr.step(t, "exact")); strings.Contains(got, "upload aborted") {
131		t.Errorf("exact: %.200s", got)
132	}
133	if r := rr.results[2]; !r.HasSummary || len(r.Summary) != stepSummaryLimit {
134		t.Errorf("exact: has=%v len=%d", r.HasSummary, len(r.Summary))
135	}
136	if got := logOf(rr.step(t, "plus1")); !strings.Contains(got, "##[error]$GITHUB_STEP_SUMMARY upload aborted"+tail+"1024k.") {
137		t.Errorf("plus1: %.300s", got)
138	}
139	if got := logOf(rr.step(t, "two")); !strings.Contains(got, "##[error]$GITHUB_STEP_SUMMARY upload aborted"+tail+"2048k.") {
140		t.Errorf("two: %.300s", got)
141	}
142}
143
144// An invalid stop-commands token ends the process: the step does not wait for
145// the script (here 600 s) and fails with the endToken message.
146func TestInvalidStopTokenStopsTheProcess(t *testing.T) {
147	needTools(t, "bash", "sleep")
148	start := time.Now()
149	rec, res := runLocal(t, wfHead+"    steps:\n      - name: s\n        run: |\n          echo '::stop-commands::warning'\n          sleep 600\n          echo after\n", "j", nil)
150	if d := time.Since(start); d > 30*time.Second {
151		t.Fatalf("the step took %v", d)
152	}
153	got := logOf(rec.step(t, "s"))
154	if !strings.Contains(got, "##[error]"+unsupportedStopTokenDisabled) || strings.Contains(got, "The operation was canceled.") {
155		t.Error(got)
156	}
157	for _, l := range rec.step(t, "s").Log {
158		if strings.TrimSpace(l) == "after" {
159			t.Errorf("output after the fatal line was logged: %q", l)
160		}
161	}
162	if rec.step(t, "s").Conclusion != ConclusionFailure || res.Conclusion != ConclusionFailure {
163		t.Errorf("%v %v", rec.step(t, "s").Conclusion, res.Conclusion)
164	}
165}
166
167// Output written after the fatal line, a lot of it, does not block the process
168// or the executor.
169func TestInvalidStopTokenDrainsHighVolumeOutput(t *testing.T) {
170	needTools(t, "bash", "yes", "head")
171	done := make(chan struct{})
172	var rec *recorder
173	go func() {
174		defer close(done)
175		rec, _ = runLocal(t, wfHead+"    steps:\n      - name: s\n        run: |\n          echo '::stop-commands::'\n          yes '::warning::x' | head -c 200000000\n          sleep 600\n", "j", nil)
176	}()
177	select {
178	case <-done:
179	case <-time.After(60 * time.Second):
180		t.Fatal("deadlock")
181	}
182	got := logOf(rec.step(t, "s"))
183	if !strings.Contains(got, "##[error]"+unsupportedStopTokenDisabled) || strings.Contains("\n"+got+"\n", "\n::warning::x\n") || strings.Contains(got, "##[warning]x") {
184		t.Errorf("%.300s", got)
185	}
186}
187
188// svc-redis (unhealthy): the github context of a step that runs before any
189// other step ran has no action_ref, action_repository, event_path nor the file
190// command paths: the runner sets them while it runs a step (ActionRunner.cs:
191// 147-153, FileCommandManager.cs:71, ExecutionContext.cs:1334), after the env
192// of the step was evaluated; the context object is shared by the steps, so a
193// later step has them. The recorded dump step of the unhealthy job of svc-redis
194// is the first step that ran and has none; a step run before it (the harness's
195// ermite-mask) gives it all of them.
196func TestGithubContextOfTheFirstStepThatRunsHasNoStepKeys(t *testing.T) {
197	needTools(t, "bash")
198	rec, _ := runLocal(t, wfHead+`    steps:
199      - name: skipped
200        if: false
201        run: echo no
202      - name: dump
203        run: echo "KEYS=$KEYS"
204        env:
205          KEYS: ${{ join(github.*, ',') != '' && 'x' }}${{ contains(toJSON(github), 'action_ref') }},${{ contains(toJSON(github), 'event_path') }},${{ contains(toJSON(github), 'artifacts') }},${{ contains(toJSON(github), 'step_summary') }}
206`, "j", nil)
207	if got := logOf(rec.step(t, "dump")); !strings.Contains(got, "KEYS=xfalse,false,false,false") {
208		t.Error(got)
209	}
210}

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 4

Tags 11

↑↓ moveenter openesc close