main 98594d0

read_test.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package template
4
5import (
6	"strings"
7	"testing"
8)
9
10func readWorkflow(t testing.TB, src string) (Token, []string) {
11	t.Helper()
12	s, err := WorkflowSchema()
13	if err != nil {
14		t.Fatal(err)
15	}
16	ctx := NewContext(s)
17	tok := ReadYAML(ctx, "workflow-root-strict", "a.yml", []byte(src))
18	return tok, ctx.Messages()
19}
20
21const head = "on: push\njobs:\n  a:\n    runs-on: x\n    steps:\n"
22
23func TestReadErrors(t *testing.T) {
24	// Expected messages come from @actions/workflow-parser 0.3.61 run on the
25	// same inputs, except that expression errors carry the position suffix of
26	// the C# runner (package expr follows it).
27	tests := []struct {
28		name, src string
29		want      []string
30	}{
31		{"unknown context, block scalar", head + "      - run: |\n          echo hi\n          echo ${{ foo.bar }}\n",
32			[]string{"a.yml (Line: 6, Col: 14): Unrecognized named-value: 'foo'. Located at position 1 within expression: foo.bar"}},
33		{"unknown context, plain", head + "      - run: echo ${{ foo.bar }} abc\n",
34			[]string{"a.yml (Line: 6, Col: 14): Unrecognized named-value: 'foo'. Located at position 1 within expression: foo.bar"}},
35		{"unknown context, double quoted", head + "      - run: \"echo ${{ foo.bar }} abc\"\n",
36			[]string{"a.yml (Line: 6, Col: 14): Unrecognized named-value: 'foo'. Located at position 1 within expression: foo.bar"}},
37		{"bad expression and insert in a name", head + "      - run: echo ${{ 1 +  }}\n      - name: ${{ insert }}\n        run: x\n",
38			[]string{"a.yml (Line: 6, Col: 14): Unexpected symbol: '+'. Located at position 3 within expression: 1 +", "a.yml (Line: 7, Col: 15): Unexpected value '${{ insert }}'"}},
39		{"not closed", head + "      - run: echo ${{ 1\n",
40			[]string{"a.yml (Line: 6, Col: 14): The expression is not closed. An unescaped ${{ sequence was found, but the closing }} sequence was not found."}},
41		{"empty document", "", []string{"a.yml: Expected a scalar value, a sequence, or a mapping"}},
42		{"comment only", "# c\n", []string{"a.yml: Expected a scalar value, a sequence, or a mapping"}},
43		{"sequence root", "[1,2]", []string{"a.yml (Line: 1, Col: 1): A sequence was not expected"}},
44		{"duplicate key", "on: push\non: pull_request\njobs: {}", []string{"a.yml (Line: 2, Col: 1): 'on' is already defined"}},
45		{"alias is resolved", "on: push\njobs:\n  a: &x\n    runs-on: x\n    steps: [{run: a}]\n  b: *x\n", nil},
46		{"valid", head + "      - run: echo ${{ github.ref }}\n", nil},
47	}
48	for _, tc := range tests {
49		t.Run(tc.name, func(t *testing.T) {
50			_, got := readWorkflow(t, tc.src)
51			if strings.Join(got, "\n") != strings.Join(tc.want, "\n") {
52				t.Errorf("errors:\n got %q\nwant %q", got, tc.want)
53			}
54		})
55	}
56}
57
58func TestReadSyntaxErrorsHaveNoTokens(t *testing.T) {
59	for _, src := range []string{"on: [push\n", "a: b: c", "on: push\n---\nx: 1\n", "\t"} {
60		tok, errs := readWorkflow(t, src)
61		if tok != nil || len(errs) == 0 {
62			t.Errorf("%q: tok=%v errs=%q", src, tok, errs)
63		}
64	}
65}
66
67func TestMixedStringBecomesFormat(t *testing.T) {
68	tok, errs := readWorkflow(t, head+"      - run: it's ${{ github.ref }} {x} ${{ 1 }}\n")
69	if len(errs) > 0 {
70		t.Fatal(errs)
71	}
72	run := tok.(*Mapping).Find("jobs").(*Mapping).Find("a").(*Mapping).Find("steps").(*Sequence).Items[0].(*Mapping).Find("run")
73	be, ok := run.(*BasicExpression)
74	if !ok {
75		t.Fatalf("run is %T", run)
76	}
77	if want := `format('it''s {0} {{x}} {1}', github.ref, 1)`; be.Expression != want {
78		t.Errorf("expression = %s, want %s", be.Expression, want)
79	}
80	if len(be.Original) != 2 {
81		t.Errorf("originals = %d", len(be.Original))
82	}
83}
84
85func TestEscapedLiteralExpression(t *testing.T) {
86	tok, errs := readWorkflow(t, head+"      - run: ${{ '{{ literal }}' }}\n")
87	if len(errs) > 0 {
88		t.Fatal(errs)
89	}
90	run := tok.(*Mapping).Find("jobs").(*Mapping).Find("a").(*Mapping).Find("steps").(*Sequence).Items[0].(*Mapping).Find("run")
91	if s, ok := run.(*String); !ok || s.Value != "{{ literal }}" {
92		t.Errorf("run = %#v", run)
93	}
94}
95
96func TestScalarTypes(t *testing.T) {
97	tok, errs := readWorkflow(t, "on: push\njobs:\n  a:\n    runs-on: x\n    timeout-minutes: 0x10\n    continue-on-error: yes\n    steps: [{run: 1}]\n")
98	// yes is a string in YAML 1.2, so continue-on-error is rejected.
99	if len(errs) == 0 || !strings.Contains(errs[0], "Unexpected value 'yes'") {
100		t.Errorf("errs = %q", errs)
101	}
102	job := tok.(*Mapping).Find("jobs").(*Mapping).Find("a").(*Mapping)
103	if n, ok := job.Find("timeout-minutes").(*Number); !ok || n.Value != 16 {
104		t.Errorf("timeout-minutes = %#v", job.Find("timeout-minutes"))
105	}
106	step := job.Find("steps").(*Sequence).Items[0].(*Mapping)
107	if s, ok := step.Find("run").(*String); !ok || s.Value != "1" {
108		t.Errorf("run = %#v", step.Find("run"))
109	}
110}
111
112func TestAliasBombIsRejected(t *testing.T) {
113	var b strings.Builder
114	b.WriteString("a0: &a0 [x, x, x, x, x, x, x, x, x, x]\n")
115	for i := 1; i < 8; i++ {
116		b.WriteString("a")
117		b.WriteString(string(rune('0' + i)))
118		b.WriteString(": &a")
119		b.WriteString(string(rune('0' + i)))
120		b.WriteString(" [")
121		for j := 0; j < 10; j++ {
122			b.WriteString("*a")
123			b.WriteString(string(rune('0' + i - 1)))
124			b.WriteString(",")
125		}
126		b.WriteString("]\n")
127	}
128	_, errs := readWorkflow(t, b.String())
129	if len(errs) == 0 || !strings.Contains(strings.Join(errs, "\n"), "too large") {
130		t.Errorf("errs = %q", errs)
131	}
132}
133
134func TestFileSizeLimit(t *testing.T) {
135	s, err := WorkflowSchema()
136	if err != nil {
137		t.Fatal(err)
138	}
139	big := []byte("on: push\n# " + strings.Repeat("x", MaxFileBytes) + "\n")
140	ctx := NewContext(s)
141	if tok := ReadYAML(ctx, "workflow-root-strict", "big.yml", big); tok != nil {
142		t.Error("token returned for an oversized file")
143	}
144	errs := ctx.Messages()
145	if len(errs) != 1 || !strings.HasPrefix(errs[0], "big.yml: The file is too large: ") {
146		t.Errorf("errors = %q", errs)
147	}
148	// Exactly at the limit is read.
149	ok := append([]byte("on: push\njobs: {}\n# "), make([]byte, 0)...)
150	pad := MaxFileBytes - len(ok) - 1
151	ok = append(ok, []byte(strings.Repeat("x", pad)+"\n")...)
152	if len(ok) != MaxFileBytes {
153		t.Fatalf("test file is %d bytes", len(ok))
154	}
155	ctx = NewContext(s)
156	ReadYAML(ctx, "workflow-root-strict", "ok.yml", ok)
157	for _, m := range ctx.Messages() {
158		if strings.Contains(m, "too large") {
159			t.Errorf("file at the limit refused: %s", m)
160		}
161	}
162}

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 1

Tags 3

↑↓ moveenter openesc close