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}