1package main
2
3import (
4 "fmt"
5 "io/ioutil"
6 "os"
7 "sort"
8 "strconv"
9 "strings"
10 "testing"
11)
12
13const name = "Testinput"
14
15var reader *os.File
16var writer *os.File
17
18type Msg struct {
19 l uint
20 c uint
21 s string
22}
23
24type byPos []string
25
26func (p byPos) Len() int {
27 return len(p)
28}
29
30func (p byPos) Swap(i, j int) {
31 p[i], p[j] = p[j], p[i]
32}
33
34func (p byPos) Less(i, j int) bool {
35 linei := p[i]
36 linej := p[j]
37
38 li, ci, _ := parseLine(linei)
39 if li == 0 {
40 return true
41 }
42
43 lj, cj, _ := parseLine(linej)
44 if li == lj {
45 return ci < cj
46 }
47
48 return li < lj
49}
50
51func setup() {
52 var err error
53 reader, writer, err = os.Pipe()
54 if err != nil {
55 panic(err)
56 }
57}
58
59func parseLine(line string) (uint, uint, string) {
60 sep := strings.Index(line, ":")
61 if line[sep+1] == ' ' {
62 return 0, 0, line[sep+2:]
63 }
64 index := sep + 1
65
66 lineLen := strings.Index(line[index:], ":")
67 lineInfo := line[index : index+lineLen]
68
69 l, err := strconv.ParseUint(lineInfo, 10, 16)
70 if err != nil {
71 panic(err)
72 }
73 index += lineLen + 1
74
75 columLen := strings.Index(line[index:], ":")
76 columnInfo := line[index : index+columLen]
77
78 c, err := strconv.ParseUint(columnInfo, 10, 16)
79 if err != nil {
80 panic(err)
81 }
82
83 index += columLen + 2
84 return uint(l), uint(c), line[index:]
85}
86
87func newLinter(input string) *Linter {
88 reader := strings.NewReader(input)
89 abuild, err := Parse(reader, name)
90 if err != nil {
91 panic(err)
92 }
93
94 linter := Linter{f: abuild, w: writer}
95 return &linter
96}
97
98func expMsg(t *testing.T, msgs ...Msg) {
99 writer.Close() // Write EOF
100 defer setup()
101
102 data, err := ioutil.ReadAll(reader)
103 if err != nil {
104 t.Fatal("ioutil.ReadAll failed:", err)
105 }
106
107 str := string(data)
108 lines := strings.Split(str[0:len(str)-1], "\n")
109 if len(msgs) != len(lines) {
110 t.Fatalf("Expected %d violations, got %d",
111 len(msgs), len(lines))
112 }
113
114 // Output of some linter functions is non-deterministic,
115 // e.g. lintLocalVariables → sort output by token position.
116 sort.Sort(byPos(lines))
117
118 for n, m := range msgs {
119 line, column, text := parseLine(lines[n])
120 if line != m.l {
121 t.Fatalf("expFail: Line didn't match, expected %d - got %d", m.l, line)
122 }
123 if column != m.c {
124 t.Fatalf("expFail: Column didn't match, expected %d - got %d", m.c, column)
125 }
126 if text != m.s {
127 t.Fatalf("expFail Expected string %q - got %q", m.s, text)
128 }
129 }
130}
131
132func TestLintComments(t *testing.T) {
133 input := `#barfoo
134#
135# foobar
136# bazbar
137#foobaz`
138
139 l := newLinter(input)
140 l.lintComments()
141
142 expMsg(t,
143 Msg{1, 1, badCommentPrefix},
144 Msg{4, 1, badCommentPrefix},
145 Msg{5, 1, badCommentPrefix})
146}
147
148func TestLintAddressComments(t *testing.T) {
149 input := `# foo: foo bar <foo@bar.com>
150# faz:
151# foo:
152# foo:
153# foo:foo bar <foo@bar.com>
154# foo: …`
155
156 l := newLinter(input)
157 n, addrs := l.lintAddressComments(" foo:")
158 if n != 5 || len(addrs) != 1 || !l.v {
159 t.Fail()
160 }
161
162 if addrs[0].a.String() != "\"foo bar\" <foo@bar.com>" {
163 t.Fatalf("Expected %q - got %q", "foo",
164 addrs[0].a.String())
165 }
166
167 expMsg(t,
168 Msg{3, 1, missingAddress},
169 Msg{4, 1, missingAddress},
170 Msg{5, 1, noAddressSeparator},
171 Msg{6, 1, invalidAddress})
172}
173
174func TestLintMaintainerAndContributors(t *testing.T) {
175 t.Run("missingMaintainer", func(t *testing.T) {
176 l := newLinter("")
177 l.lintMaintainerAndContributors()
178 expMsg(t, Msg{0, 0, missingMaintainer})
179 })
180
181 t.Run("emptyMaintainer", func(t *testing.T) {
182 l := newLinter("# Maintainer:")
183 l.lintMaintainerAndContributors()
184 expMsg(t, Msg{1, 1, missingAddress})
185 })
186
187 t.Run("tooManyMaintainers", func(t *testing.T) {
188 l := newLinter(`# Maintainer: A <a@a>
189# Maintainer: B <b@b>`)
190 l.lintMaintainerAndContributors()
191 expMsg(t, Msg{2, 1, tooManyMaintainers})
192 })
193
194 t.Run("maintainerAfterAssign", func(t *testing.T) {
195 l := newLinter(`pkgname=foo
196# Maintainer: A <a@b>`)
197 l.lintMaintainerAndContributors()
198 expMsg(t, Msg{2, 1, maintainerAfterAssign})
199 })
200
201 t.Run("wrongAddrCommentOrder", func(t *testing.T) {
202 l := newLinter(`# Maintainer: A <a@b>
203# Contributor: B <b@c>`)
204 l.lintMaintainerAndContributors()
205 expMsg(t, Msg{2, 1, wrongAddrCommentOrder})
206 })
207
208 t.Run("repeatedAddrComment", func(t *testing.T) {
209 l := newLinter(`# Contributor: A <a@b>
210# Contributor: A <a@b>
211# Maintainer: M <m@m>`)
212 l.lintMaintainerAndContributors()
213 expMsg(t, Msg{2, 1, repeatedAddrComment})
214 })
215
216 t.Run("oneMaintainer", func(t *testing.T) {
217 l := newLinter("# Maintainer: J <a@k>")
218 l.lintMaintainerAndContributors()
219 if l.v {
220 t.Fail()
221 }
222 })
223
224 t.Run("oneMaintainerAndContributors", func(t *testing.T) {
225 l := newLinter(`# Contributor: A <a@a>
226# Contributor: B <b@b>
227# Maintainer: C <c@c>`)
228 l.lintMaintainerAndContributors()
229 if l.v {
230 t.Fail()
231 }
232 })
233}
234
235func TestListGlobalVariables(t *testing.T) {
236 input := `pkgname=foobar
237foo=42
238_foo=9001
239__foo=bar
240export ENV=23`
241
242 l := newLinter(input)
243 l.lintGlobalVariables()
244
245 expMsg(t,
246 Msg{2, 1, fmt.Sprintf(invalidGlobalVar, "foo")},
247 Msg{4, 1, fmt.Sprintf(invalidGlobalVar, "__foo")})
248}
249
250func TestLintUnusedVariables(t *testing.T) {
251 input := `pkgname=foobar
252_foo=23
253_bar=42
254f1() {
255foo=lol
256}
257f2() {
258echo $_bar
259}
260f3() {
261FOO=bar make
262}
263f4() {
264export ENV=42
265}`
266
267 l := newLinter(input)
268 l.lintUnusedVariables()
269
270 expMsg(t,
271 Msg{2, 1, fmt.Sprintf(variableUnused, "_foo")},
272 Msg{5, 1, fmt.Sprintf(variableUnused, "foo")})
273}
274
275func TestLintGlobalCmdSubsts(t *testing.T) {
276 input := `pkgname=bar
277_bar=$(ls)
278f1() {
279local v1=${_bar}
280}
281_baz=$(cp -h)
282f2() {
283local v2=${_baz}
284}
285_baz=${foo} bar`
286
287 l := newLinter(input)
288 l.lintGlobalCmdSubsts()
289
290 expMsg(t,
291 Msg{2, 6, cmdSubstInGlobalVar},
292 Msg{6, 6, cmdSubstInGlobalVar})
293}
294
295func TestLintLocalVariables(t *testing.T) {
296 input := `f1() {
297foo=123
298}
299f2() {
300local foo=123
301}
302f3() {
303local bar=456
304}
305f4() {
306for foobar in "a" "b" "c"; do echo "$foobar"; done
307}
308f5() {
309export foo="bar"; echo "$foo"
310}
311VARFORCALLEXPR=23 ls`
312
313 l := newLinter(input)
314 l.lintLocalVariables()
315
316 expMsg(t,
317 Msg{2, 1, fmt.Sprintf(nonLocalVariable, "foo")},
318 Msg{11, 5, fmt.Sprintf(nonLocalVariable, "foobar")})
319}
320
321func TestLintParamExpression(t *testing.T) {
322 input := `# foobar
323foo=${pkgname}
324bar=$foo
325# barfoo
326foo=${pkgname##.*}
327foo=${foobar}foobar
328foo=${foobar}.$barfoo`
329
330 l := newLinter(input)
331 l.lintParamExpression()
332
333 expMsg(t,
334 Msg{2, 5, fmt.Sprintf(trivialLongParamExp, "pkgname", "pkgname")},
335 Msg{7, 5, fmt.Sprintf(trivialLongParamExp, "foobar", "foobar")})
336}
337
338func TestVariablePlacement(t *testing.T) {
339 input := `sha512sums=foobar
340myfunc() {
341echo myfunc
342}
343pkgname=barfoo`
344
345 l := newLinter(input)
346 l.lintMetadataPlacement()
347
348 expMsg(t,
349 Msg{1, 1, fmt.Sprintf(metadataAfterFunc, "sha512sums")},
350 Msg{5, 1, fmt.Sprintf(metadataBeforeFunc, "pkgname")})
351}
352
353func TestLintRequiredMetadata(t *testing.T) {
354 t.Run("allVarsDefined", func(t *testing.T) {
355 input := `pkgname=foobar
356pkgver=1337
357pkgrel=2342
358pkgdesc="foobar"
359url=http://example.org
360arch=all
361license=MIT
362sha512sums=1234`
363
364 l := newLinter(input)
365 l.lintRequiredMetadata()
366 if l.v {
367 t.Fail()
368 }
369 })
370
371 t.Run("missingOneVar", func(t *testing.T) {
372 input := `pkgname=foobar
373pkgrel=2342
374pkgdesc="foobar"
375url=http://example.org
376arch=all
377license=MIT
378sha512sums=1234`
379
380 l := newLinter(input)
381 l.lintRequiredMetadata()
382
383 expMsg(t,
384 Msg{0, 0, fmt.Sprintf(missingMetadata, "pkgver")})
385 })
386}
387
388func TestLintFunctionOrder(t *testing.T) {
389 t.Run("wrongFuncOrder", func(t *testing.T) {
390 input := `package() {
391}
392build() {
393}`
394
395 l := newLinter(input)
396 l.lintFunctionOrder()
397
398 expMsg(t,
399 Msg{1, 1, fmt.Sprintf(wrongFuncOrder, "package", "build")})
400 })
401
402 t.Run("rightFuncOrder", func(t *testing.T) {
403 input := `prepare() {
404}
405build() {
406}
407check() {
408}
409package() {
410}`
411
412 l := newLinter(input)
413 l.lintFunctionOrder()
414
415 if l.v {
416 t.Fail()
417 }
418 })
419}
420
421func TestLintBashisms(t *testing.T) {
422 input := `[[ -e "$builddir" ]] && foo=bar
423bar=*(foo bar)
424echo >(true)
425let x
426declare -A foobar
427readonly x
428typeset -r x
429nameref foo
430echo ${#foo}
431select s in foo bar baz; do
432 echo $s
433done
434function f() {
435return 1
436}`
437
438 l := newLinter(input)
439 l.lintBashisms()
440
441 expMsg(t,
442 Msg{1, 1, fmt.Sprintf(forbiddenBashism, "test clause")},
443 Msg{2, 5, fmt.Sprintf(forbiddenBashism, "extended globbing expression")},
444 Msg{3, 6, fmt.Sprintf(forbiddenBashism, "process substitution")},
445 Msg{4, 1, fmt.Sprintf(forbiddenBashism, "let clause")},
446 Msg{5, 1, fmt.Sprintf(forbiddenBashism, "declare")},
447 Msg{6, 1, fmt.Sprintf(forbiddenBashism, "readonly")},
448 Msg{7, 1, fmt.Sprintf(forbiddenBashism, "typeset")},
449 Msg{8, 1, fmt.Sprintf(forbiddenBashism, "nameref")},
450 Msg{9, 6, fmt.Sprintf(forbiddenBashism, "advanced parameter expression")},
451 Msg{10, 1, fmt.Sprintf(forbiddenBashism, "select clause")},
452 Msg{13, 1, fmt.Sprintf(forbiddenBashism, "non-POSIX function declaration")})
453}
454
455func TestMain(m *testing.M) {
456 setup()
457 os.Exit(m.Run())
458}