1// SPDX-License-Identifier: MIT
2
3package action
4
5import (
6 "encoding/json"
7 "os"
8 "path/filepath"
9 "regexp"
10 "strings"
11 "testing"
12)
13
14// testdata/strict.json is what @actions/workflow-parser 0.3.61 answers on the
15// files of testdata/cases with parseAction and convertActionTemplate (see
16// gen/strict.mjs). TestLintAgainstReference replays it against Lint.
17
18type strictAnswer struct {
19 Errors []string `json:"errors"`
20 Name string `json:"name"`
21 Descr string `json:"description"`
22 Author string `json:"author"`
23 Inputs []struct {
24 ID string `json:"id"`
25 Required bool `json:"required"`
26 DeprecationMessage string `json:"deprecationMessage"`
27 Default string `json:"default"`
28 } `json:"inputs"`
29 Outputs []struct {
30 ID string `json:"id"`
31 Value string `json:"value"`
32 } `json:"outputs"`
33 Runs struct {
34 Using string `json:"using"`
35 Main string `json:"main"`
36 Image string `json:"image"`
37 PreIf string `json:"preIf"`
38 PostIf string `json:"postIf"`
39 Steps int `json:"steps"`
40 } `json:"runs"`
41}
42
43var (
44 // The reference words expression parse errors like the JavaScript
45 // expression library: no position suffix. package expr follows the C#
46 // runner, which adds ". Located at position N within expression: ...".
47 exprSuffix = regexp.MustCompile(`\. Located at position \d+ within expression: .*$`)
48 // yaml.v3 and the JavaScript yaml library word syntax errors differently.
49 jsYAMLError = regexp.MustCompile(` at line \d+, column \d+:\n`)
50)
51
52// knownLintDifferences are the files on which the reference's answer is not
53// reproduced, with the reason.
54var knownLintDifferences = map[string]string{
55 "yaml_key_collection.yml": "a mapping key that is a collection: the reference reads it as undefined (\"Unexpected value 'undefined'\"), package template reports \"Unexpected value ''\" (twice); see template/PORTING.md",
56 "fold_inputs_sharp.yml": "duplicate keys: the reference compares with JavaScript's toUpperCase (\u00df is SS), template with strings.ToUpper; Lint is not the runner's OrdinalIgnoreCase, Parse is",
57 "int32_key.yml": jsKey,
58 "key_bool_dup.yml": jsKey,
59 "key_float_dup.yml": jsKey,
60 "key_hex_dup.yml": jsKey,
61 "key_hex_input.yml": jsKey,
62 "key_null_env.yml": nullKey,
63 "key_null_inputs.yml": nullKey,
64 "key_null_top.yml": jsKey,
65 "key_number_runs.yml": jsKey,
66}
67
68// nullKey: Lint misses a null key under a loose-key mapping; the reference and
69// the runner reject it. Fixing it in template would change how workflow files
70// are read, so it is left as it is.
71const nullKey = "Lint misses a null key under a loose-key mapping; the reference and the runner reject it"
72
73// jsKey: the reference resolves a plain key to a number, boolean or null (and
74// then crashes on it, "raw.indexOf is not a function", or words it
75// differently); template keeps keys of the workflow-style readers as strings.
76// The runner's reader resolves keys like values, which Parse reproduces.
77const jsKey = "the reference resolves non-string keys and fails on them with its own wording; Lint keeps keys as strings"
78
79func TestLintAgainstReference(t *testing.T) {
80 data, err := os.ReadFile("testdata/strict.json")
81 if err != nil {
82 t.Fatal(err)
83 }
84 var want map[string]strictAnswer
85 if err := json.Unmarshal(data, &want); err != nil {
86 t.Fatal(err)
87 }
88 files, _ := filepath.Glob("testdata/cases/*.yml")
89 for _, f := range files {
90 name := filepath.Base(f)
91 w, ok := want[name]
92 if !ok {
93 t.Errorf("%s: missing from strict.json", name)
94 continue
95 }
96 t.Run(strings.TrimSuffix(name, ".yml"), func(t *testing.T) {
97 src, err := os.ReadFile(f)
98 if err != nil {
99 t.Fatal(err)
100 }
101 errs := Lint(name, src)
102 if len(w.Errors) == 1 && jsYAMLError.MatchString(w.Errors[0]) {
103 if len(errs) != 1 || !strings.HasPrefix(errs[0].Error(), name+": ") {
104 t.Errorf("syntax error: got %q, reference %q", errTexts(errs), w.Errors)
105 }
106 return
107 }
108 var got []string
109 for _, e := range errs {
110 got = append(got, exprSuffix.ReplaceAllString(e.Error(), ""))
111 }
112 if note, known := knownLintDifferences[name]; known {
113 t.Skip(note)
114 }
115 if strings.Join(got, "\n") != strings.Join(w.Errors, "\n") {
116 t.Fatalf("errors differ\n got: %q\nwant: %q", got, w.Errors)
117 }
118 })
119 }
120}