Skip to content

Commit 8cebeba

Browse files
authored
feat: add “regression” suite of sorts (#60)
* feat: provide a custom `isSorted` function for determining whether a slice is sorted ascending. Inject the function into the playground, and fix the test that needs to verify sort order. fixes #5 * remove superfluous sort * feat: add “regression” suite of sorts Something to run against the examples in the Playground to ensure we don’t accidentally publish an example that should work and doesn’t. * Add license header
1 parent 54745ed commit 8cebeba

File tree

2 files changed

+192
-1
lines changed

2 files changed

+192
-1
lines changed

eval/eval.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ var exprEnvOptions = []expr.Option{
3535
// Inject a custom isSorted function into the environment.
3636
functions.IsSorted(),
3737

38-
// Provide a constant timestamp to the expression environment.
38+
// Provide a constant timestamp to the expression environment.
3939
expr.DisableBuiltin("now"),
4040
expr.Function("now", func(...any) (any, error) {
4141
return time.Date(2024, 2, 26, 0, 0, 0, 0, time.UTC).Format(time.RFC3339), nil

tests/regressions_test.go

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
// Copyright 2024 Peter Olds <[email protected]>
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package tests
16+
17+
import (
18+
"encoding/json"
19+
"fmt"
20+
"os"
21+
"strconv"
22+
"testing"
23+
24+
"gopkg.in/yaml.v3"
25+
26+
"github.com/polds/expr-playground/eval"
27+
)
28+
29+
// TestExamples serves as a regression test for the examples presented in the playground UI.
30+
// If any of the examples change, this test will fail, to help ensure the playground UI is
31+
// updated accordingly, and especially so we don't accidentally push a broken sample.
32+
func TestExamples(t *testing.T) {
33+
examples := setup(t)
34+
35+
// lookup should exactly match the "name" field in the examples.yaml file.
36+
tests := []struct {
37+
lookup string
38+
want string
39+
wantErr bool
40+
}{
41+
{
42+
lookup: "default",
43+
want: "true",
44+
},
45+
{
46+
lookup: "Check image registry",
47+
want: "true",
48+
},
49+
{
50+
lookup: "Disallow HostPorts",
51+
want: "false",
52+
},
53+
{
54+
lookup: "Require non-root containers",
55+
want: "false",
56+
},
57+
{
58+
lookup: "Drop ALL capabilities",
59+
want: "true",
60+
},
61+
{
62+
lookup: "Semantic version check for image tags (Regex)",
63+
want: "false",
64+
},
65+
{
66+
lookup: "URLs",
67+
wantErr: true,
68+
},
69+
{
70+
lookup: "Check JWT custom claims",
71+
want: "true",
72+
},
73+
{
74+
lookup: "Optional",
75+
want: "fallback",
76+
},
77+
{
78+
lookup: "Duration and timestamp",
79+
want: "true",
80+
},
81+
{
82+
lookup: "Quantity",
83+
wantErr: true,
84+
},
85+
{
86+
lookup: "Access Log Filtering",
87+
want: "true",
88+
},
89+
{
90+
lookup: "Custom Metrics",
91+
want: "echo",
92+
},
93+
{
94+
lookup: "Blank",
95+
wantErr: true,
96+
},
97+
}
98+
for _, tc := range tests {
99+
t.Run(tc.lookup, func(t *testing.T) {
100+
var exp Example
101+
for _, e := range examples {
102+
if e.Name == tc.lookup {
103+
exp = e
104+
break
105+
}
106+
}
107+
if exp.Name == "" {
108+
t.Fatalf("failed to find example %q", tc.lookup)
109+
}
110+
111+
got, err := eval.Eval(exp.Expr, marshal(t, exp.Data))
112+
if (err != nil) != tc.wantErr {
113+
t.Errorf("Eval() got error %v, expected error %v", err, tc.wantErr)
114+
}
115+
if tc.wantErr {
116+
return
117+
}
118+
119+
var obj map[string]AlwaysString
120+
if err := json.Unmarshal([]byte(got), &obj); err != nil {
121+
t.Fatalf("failed to unmarshal response: %v", err)
122+
}
123+
if s := obj["result"].Value; s != tc.want {
124+
t.Errorf("Eval() got %q, expected %q", s, tc.want)
125+
}
126+
})
127+
}
128+
// Ensure these tests are updated when the examples are updated.
129+
// Not a perfect solution, but it's better than nothing.
130+
if len(examples) != len(tests) {
131+
t.Errorf("Regression test counts got %d, expected %d", len(tests), len(examples))
132+
}
133+
}
134+
135+
type Example struct {
136+
Name string `yaml:"name"`
137+
Expr string `yaml:"expr"`
138+
Data string `yaml:"data"`
139+
}
140+
141+
func setup(t *testing.T) []Example {
142+
t.Helper()
143+
144+
out, err := os.ReadFile("../examples.yaml")
145+
if err != nil {
146+
t.Fatalf("failed to read examples.yaml: %v", err)
147+
}
148+
var examples struct {
149+
Examples []Example `yaml:"examples"`
150+
}
151+
if err := yaml.Unmarshal(out, &examples); err != nil {
152+
t.Fatalf("failed to unmarshal examples.yaml: %v", err)
153+
}
154+
return examples.Examples
155+
}
156+
157+
// Attempt to get the data into either yaml or json format.
158+
func marshal(t *testing.T, s string) map[string]any {
159+
t.Helper()
160+
161+
var v map[string]any
162+
if yamlErr := yaml.Unmarshal([]byte(s), &v); yamlErr != nil {
163+
if err := json.Unmarshal([]byte(s), &v); err != nil {
164+
t.Errorf("failed to unmarshal %q as yaml: %v", s, yamlErr)
165+
t.Fatalf("failed to unmarshal %q as json: %v", s, err)
166+
}
167+
}
168+
return v
169+
}
170+
171+
// AlwaysString attempts to unmarshal the value as a string.
172+
type AlwaysString struct {
173+
Value string
174+
}
175+
176+
func (c *AlwaysString) UnmarshalJSON(b []byte) error {
177+
var raw any
178+
if err := json.Unmarshal(b, &raw); err != nil {
179+
return err
180+
}
181+
182+
switch v := raw.(type) {
183+
case bool:
184+
c.Value = strconv.FormatBool(v)
185+
case string:
186+
c.Value = v
187+
default:
188+
return fmt.Errorf("unsupported type %T", v)
189+
}
190+
return nil
191+
}

0 commit comments

Comments
 (0)