1// SPDX-License-Identifier: MIT
2
3package template
4
5import (
6 "math"
7 "strings"
8 "testing"
9)
10
11// The options of Context that the C# runner's loader needs (package action):
12// nesting limit, error limit and the runner's YAML restrictions.
13
14func readWith(t *testing.T, set func(*Context), src string) []string {
15 t.Helper()
16 schema, err := WorkflowSchema()
17 if err != nil {
18 t.Fatal(err)
19 }
20 ctx := NewContext(schema)
21 set(ctx)
22 ReadYAML(ctx, "workflow-root-strict", "f.yml", []byte(src))
23 return ctx.Messages()
24}
25
26func TestMaxDepth(t *testing.T) {
27 nest := func(n int) string { return "x: " + strings.Repeat("[", n) + strings.Repeat("]", n) + "\n" }
28 set := func(c *Context) { c.MaxDepth = 10 }
29 // Like TemplateMemory.IncrementDepth: the root mapping is level one, so
30 // nine brackets fit and ten do not. The unexpected key "x" is skipped, and
31 // skipping counts levels too.
32 got := readWith(t, set, nest(9))
33 for _, m := range got {
34 if strings.Contains(m, "depth") {
35 t.Errorf("9 brackets: %q", got)
36 }
37 }
38 got = readWith(t, set, nest(10))
39 if len(got) == 0 || got[len(got)-1] != "f.yml: Maximum object depth exceeded" {
40 t.Errorf("10 brackets: %q", got)
41 }
42 // No limit by default.
43 for _, m := range readWith(t, func(*Context) {}, nest(500)) {
44 if strings.Contains(m, "depth") {
45 t.Errorf("default limit: %q", m)
46 }
47 }
48}
49
50func TestErrorLimit(t *testing.T) {
51 var b strings.Builder
52 for i := 0; i < 30; i++ {
53 b.WriteString("k" + string(rune('a'+i%26)) + string(rune('a'+i/26)) + ": 1\n")
54 }
55 if got := readWith(t, func(c *Context) { c.ErrorLimit = 10 }, b.String()); len(got) != 10 {
56 t.Errorf("%d errors with a limit of 10", len(got))
57 }
58 if got := readWith(t, func(*Context) {}, b.String()); len(got) < 30 {
59 t.Errorf("%d errors without a limit", len(got))
60 }
61}
62
63func TestRunnerYAML(t *testing.T) {
64 runner := func(c *Context) { c.RunnerYAML = true }
65 tests := []struct {
66 name, src, want string
67 }{
68 {"anchor", "on: &a push\n", "f.yml: Anchors are not currently supported. Remove the anchor 'a'"},
69 {"alias", "a: &a [1]\nb: *a\n", "f.yml: Anchors are not currently supported. Remove the anchor 'a'"},
70 {"!!str is unknown to the runner", "on: !!str push\n", "f.yml: Unexpected tag 'tag:yaml.org,2002:str'"},
71 {"!!int on a string", "on: !!int push\n", "f.yml: The value 'push' on line 1 and column 5 is invalid for the type 'tag:yaml.org,2002:int'"},
72 {"tag on a quoted scalar", "on: !!int '1'\n", "f.yml: The scalar style 'SingleQuoted' on line 1 and column 5 is not valid with the tag 'tag:yaml.org,2002:int'"},
73 {"second document", "on: push\n---\non: pull_request\n", "f.yml: Expected stream end parse event"},
74 {"empty", "", "f.yml: Expected document start parse event"},
75 {"collection key", "? [a]\n: b\n", "f.yml: Expected mapping end"},
76 }
77 for _, tc := range tests {
78 got := readWith(t, runner, tc.src)
79 found := false
80 for _, m := range got {
81 found = found || m == tc.want
82 }
83 if !found {
84 t.Errorf("%s: %q, want %q", tc.name, got, tc.want)
85 }
86 // None of this is an error without the option (except an empty file
87 // and a collection key, which have other messages).
88 }
89 for _, src := range []string{"on: &a push\n", "on: !!str push\n", "on: !!int 1\n"} {
90 for _, m := range readWith(t, func(*Context) {}, src) {
91 if strings.Contains(m, "Anchors") || strings.Contains(m, "Unexpected tag") {
92 t.Errorf("%q without RunnerYAML: %q", src, m)
93 }
94 }
95 }
96 // A valid tag passes.
97 for _, src := range []string{"on: !!null\n", "on: !!int 12\n", "on: !!float 1.5\n", "on: !!bool true\n", "on: !!int 0x1F\n"} {
98 for _, m := range readWith(t, runner, src) {
99 if strings.Contains(m, "tag") || strings.Contains(m, "invalid for the type") {
100 t.Errorf("%q: %q", src, m)
101 }
102 }
103 }
104}
105
106func TestExceptionError(t *testing.T) {
107 schema, _ := WorkflowSchema()
108 ctx := NewContext(schema)
109 ctx.ErrorException("System.ArgumentNullException", "msg")
110 ctx.ErrorException("System.ArgumentOutOfRangeException", "msg")
111 ctx.ErrorException("System.NotSupportedException", "msg")
112 want := []string{
113 "System.ArgumentNullException: Value cannot be null. (Parameter 'msg')",
114 "System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values. (Parameter 'msg')",
115 "System.NotSupportedException: msg",
116 }
117 got := ctx.Messages()
118 if strings.Join(got, "\n") != strings.Join(want, "\n") {
119 t.Errorf("got %q", got)
120 }
121}
122
123func TestDotNetNumberString(t *testing.T) {
124 for _, tc := range []struct {
125 in float64
126 want string
127 }{
128 {0, "0"}, {1, "1"}, {1.5, "1.5"}, {-5, "-5"}, {0.1, "0.1"}, {0.30000000000000004, "0.3"},
129 {100000, "100000"}, {1e14, "100000000000000"}, {1e15, "1E+15"}, {123456789012345678, "1.23456789012346E+17"},
130 {1e20, "1E+20"}, {1e21, "1E+21"}, {0.0001, "0.0001"}, {0.00001, "1E-05"}, {1e-6, "1E-06"}, {1.5e-7, "1.5E-07"},
131 {12345.6789, "12345.6789"}, {999999999999999, "999999999999999"}, {9999999999999999, "1E+16"},
132 } {
133 if got := DotNetNumberString(tc.in); got != tc.want {
134 t.Errorf("DotNetNumberString(%v) = %q, want %q", tc.in, got, tc.want)
135 }
136 }
137 if got := DotNetNumberString(math.Copysign(0, -1)); got != "-0" {
138 t.Errorf("negative zero: %q", got)
139 }
140 if DotNetNumberString(math.Inf(1)) != "Infinity" || DotNetNumberString(math.Inf(-1)) != "-Infinity" || DotNetNumberString(math.NaN()) != "NaN" {
141 t.Error("non-finite numbers")
142 }
143}