v0.5.1 25d29f5

matchers_test.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package executor
4
5import (
6	"strings"
7	"testing"
8	"time"
9)
10
11func mustConfig(t *testing.T, js string) *matchersConfig {
12	t.Helper()
13	c, err := parseMatchersConfig([]byte(js))
14	if err != nil || c == nil {
15		t.Fatalf("parse: %v", err)
16	}
17	return c
18}
19
20// Cases of actions/runner src/Test/L0/Worker/IssueMatcherL0.cs (MIT, (c) 2019
21// GitHub; see THIRD_PARTY.md): Config_Validate_*.
22func TestMatcherConfigValidate(t *testing.T) {
23	one := func(extra string) string {
24		return `{"problemMatcher":[{"owner":"myMatcher","pattern":[` + extra + `]}]}`
25	}
26	for _, tc := range []struct{ name, js, err string }{
27		{"loop on a single pattern", one(`{"regexp":"^error: (.+)$","message":1,"loop":true}`), "Only the last pattern in a multiline matcher may set 'loop'"},
28		{"loop sanity", one(`{"regexp":"^file: (.+)$","file":1},{"regexp":"^error: (.+)$","message":1,"loop":true}`), ""},
29		{"loop only on the last", one(`{"regexp":"^(error)$","severity":1},{"regexp":"^file: (.+)$","file":1,"loop":true},{"regexp":"^error: (.+)$","message":1}`), "Only the last pattern in a multiline matcher may set 'loop'"},
30		{"loop must set message", one(`{"regexp":"^file: (.+)$","message":1},{"regexp":"^file: (.+)$","file":1,"loop":true}`), "The loop pattern must set 'message'"},
31		{"message in the first pattern", one(`{"regexp":"^file: (.+)$","message":1},{"regexp":"^error: (.+)$","file":1}`), ""},
32		{"message required", one(`{"regexp":"^error: (.+)$","file":1}`), "At least one pattern must set 'message'"},
33		{"owners distinct", `{"problemMatcher":[{"owner":"myMatcher","pattern":[{"regexp":"^error: (.+)$","message":1}]},{"owner":"MYmatcher","pattern":[{"regexp":"^ERR: (.+)$","message":1}]}]}`, "Duplicate owner name 'MYmatcher'"},
34		{"owner required", `{"problemMatcher":[{"pattern":[{"regexp":"^(.+)$","message":1}]}]}`, "Owner must not be empty"},
35		{"pattern required", `{"problemMatcher":[{"owner":"m"}]}`, "Matcher 'm' does not contain any patterns"},
36		{"property set twice", one(`{"regexp":"^(.+) (.+)$","file":1,"message":2},{"regexp":"^(.+)$","file":1}`), "The property 'file' is set twice"},
37		{"out of range", one(`{"regexp":"^(.+)$","message":2}`), "The property 'message' is set to 2 which is out of range"},
38		{"less than zero", one(`{"regexp":"^(.+)$","message":-1}`), "The property 'message' is set to -1 which is out of range"},
39		{"bad severity", `{"problemMatcher":[{"owner":"m","severity":"fatal","pattern":[{"regexp":"(.+)","message":1}]}]}`, "Matcher 'm' contains unexpected default severity 'fatal'"},
40	} {
41		err := mustConfig(t, tc.js).validate()
42		switch {
43		case tc.err == "" && err != nil:
44			t.Errorf("%s: %v", tc.name, err)
45		case tc.err != "" && (err == nil || err.Error() != tc.err):
46			t.Errorf("%s: %v, want %q", tc.name, err, tc.err)
47		}
48	}
49}
50
51func newMatcher(t *testing.T, js string) *issueMatcher {
52	t.Helper()
53	c := mustConfig(t, js)
54	if err := c.validate(); err != nil {
55		t.Fatal(err)
56	}
57	m, err := newIssueMatcher(c.Matchers[0], time.Second)
58	if err != nil {
59		t.Fatal(err)
60	}
61	return m
62}
63
64func matchOf(t *testing.T, m *issueMatcher, line string) *issueMatch {
65	t.Helper()
66	r, err := m.match(line)
67	if err != nil {
68		t.Fatal(err)
69	}
70	return r
71}
72
73// IssueMatcherL0.Matcher_MultiplePatterns_Loop_AccumulatesStatePerLine.
74func TestMatcherLoopAccumulatesState(t *testing.T) {
75	m := newMatcher(t, `{"problemMatcher":[{"owner":"myMatcher","pattern":[
76		{"regexp":"^(.+)$","file":1},{"regexp":"^(.+)$","code":1},{"regexp":"^message:(.+)$","message":1,"loop":true}]}]}`)
77	type want struct{ file, code, msg string }
78	check := func(line string, w *want) {
79		t.Helper()
80		got := matchOf(t, m, line)
81		switch {
82		case w == nil && got != nil:
83			t.Fatalf("%q: matched %+v", line, got)
84		case w != nil && (got == nil || got.file.s != w.file || got.code.s != w.code || got.message.s != w.msg):
85			t.Fatalf("%q: %+v, want %+v", line, got, w)
86		}
87	}
88	check("file1", nil)
89	check("code1", nil)
90	check("message:message1", &want{"file1", "code1", "message1"})
91	check("message:message1-2", &want{"file1", "code1", "message1-2"}) // the loop keeps the running match
92	check("abc", nil)                                                  // discarded
93	check("file2", nil)
94	check("code2", nil)
95	check("message:message2", &want{"file2", "code2", "message2"})
96	check("abc", nil)
97	check("abc", nil)
98	check("file3", nil)
99	check("code3", nil)
100	check("message:message3", &want{"file3", "code3", "message3"})
101}
102
103// IssueMatcherL0.Matcher_MultiplePatterns_NonLoop_*: no loop, a match clears
104// the state.
105func TestMatcherNonLoop(t *testing.T) {
106	m := newMatcher(t, `{"problemMatcher":[{"owner":"m","pattern":[
107		{"regexp":"^(.+)$","file":1},{"regexp":"^message:(.+)$","message":1}]}]}`)
108	if matchOf(t, m, "file1") != nil {
109		t.Fatal("early match")
110	}
111	if got := matchOf(t, m, "message:m1"); got == nil || got.file.s != "file1" || got.message.s != "m1" {
112		t.Fatalf("%+v", got)
113	}
114	// "message:m2" is also a file line (first pattern matches every line).
115	if got := matchOf(t, m, "message:m2"); got != nil {
116		t.Fatalf("a non loop pattern matched without a running match: %+v", got)
117	}
118}
119
120// IssueMatcherL0.Matcher_SinglePattern_ExtractsProperties, DefaultSeverity,
121// DefaultFromPath.
122func TestMatcherSinglePattern(t *testing.T) {
123	m := newMatcher(t, `{"problemMatcher":[{"owner":"m","severity":"warning","fromPath":"web/package.json","pattern":[
124		{"regexp":"^(.+):(\\d+):(\\d+) (\\w+)? (\\w+) (.+)$","file":1,"line":2,"column":3,"severity":4,"code":5,"message":6}]}]}`)
125	got := matchOf(t, m, "file.js:5:10 error E1 something bad")
126	if got == nil || got.file.s != "file.js" || got.line.s != "5" || got.column.s != "10" || got.severity.s != "error" || got.code.s != "E1" || got.message.s != "something bad" || got.fromPath.s != "web/package.json" {
127		t.Fatalf("%+v", got)
128	}
129	// A group that took no part gives "" and the default severity applies.
130	got = matchOf(t, m, "file.js:5:10  E1 msg")
131	if got == nil || got.severity.s != "warning" {
132		t.Fatalf("%+v", got)
133	}
134	if matchOf(t, m, "nothing") != nil {
135		t.Fatal("matched")
136	}
137}
138
139// ECMAScript semantics (\d is ASCII only) and lookarounds, which RE2 lacks.
140func TestMatcherRegexFlavour(t *testing.T) {
141	m := newMatcher(t, `{"problemMatcher":[{"owner":"m","pattern":[{"regexp":"^(?<=ERR )(\\d+) (?!ignored)(.+)$","line":1,"message":2}]}]}`)
142	_ = m
143	m = newMatcher(t, `{"problemMatcher":[{"owner":"m","pattern":[{"regexp":"^(\\d+) (.+)$","line":1,"message":2}]}]}`)
144	if matchOf(t, m, "١٢ arabic digits") != nil {
145		t.Error("\\d must be ASCII with RegexOptions.ECMAScript")
146	}
147	if matchOf(t, m, "12 ascii") == nil {
148		t.Error("ASCII digits")
149	}
150	b := newMatcher(t, `{"problemMatcher":[{"owner":"m","pattern":[{"regexp":"^(a)\\1 (.+)$","message":2}]}]}`)
151	if matchOf(t, b, "aa backreference") == nil {
152		t.Error("backreference")
153	}
154}
155
156// AC6b: a catastrophic pattern times out in about a second and does not stall
157// the step.
158func TestMatcherCatastrophicPatternTimesOut(t *testing.T) {
159	m := newMatcher(t, `{"problemMatcher":[{"owner":"bad","pattern":[{"regexp":"^(a+)+$","message":1}]}]}`)
160	start := time.Now()
161	_, err := m.match(strings.Repeat("a", 30) + "!")
162	took := time.Since(start)
163	if err == nil || !strings.HasPrefix(err.Error(), "The Regex engine has timed out while trying to match a pattern to an input string.") {
164		t.Fatalf("err %v after %v", err, took)
165	}
166	if took < 900*time.Millisecond || took > 3*time.Second {
167		t.Errorf("timed out after %v, want about 1 s", took)
168	}
169}
170
171func TestParseTimeSpan(t *testing.T) {
172	for in, want := range map[string]time.Duration{
173		"0:0:0.01": 10 * time.Millisecond, "00:00:05": 5 * time.Second, "0:2": 2 * time.Minute, "1.02:03:04": 26*time.Hour + 3*time.Minute + 4*time.Second,
174		"1": 24 * time.Hour,
175	} {
176		if got, ok := parseTimeSpan(in); !ok || got != want {
177			t.Errorf("%q: %v %v, want %v", in, got, ok, want)
178		}
179	}
180	for _, in := range []string{"", "abc", "1:2:3:4", "25:00:00", "0:61"} {
181		if _, ok := parseTimeSpan(in); ok {
182			t.Errorf("%q parsed", in)
183		}
184	}
185	env := func(m map[string]string) func(string) string { return func(k string) string { return m[k] } }
186	if d := matcherTimeout(env(nil)); d != time.Second {
187		t.Error(d)
188	}
189	if d := matcherTimeout(env(map[string]string{matcherTimeoutKey: "00:00:05"})); d != 5*time.Second {
190		t.Error(d)
191	}
192	if d := matcherTimeout(env(map[string]string{matcherTimeoutKey: "-1"})); d != time.Second {
193		t.Error(d)
194	}
195}

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