Skip to content

Commit

Permalink
feat: add “regression” suite of sorts (#60)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
polds authored Jan 26, 2024
1 parent 54745ed commit 8cebeba
Show file tree
Hide file tree
Showing 2 changed files with 192 additions and 1 deletion.
2 changes: 1 addition & 1 deletion eval/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ var exprEnvOptions = []expr.Option{
// Inject a custom isSorted function into the environment.
functions.IsSorted(),

// Provide a constant timestamp to the expression environment.
// Provide a constant timestamp to the expression environment.
expr.DisableBuiltin("now"),
expr.Function("now", func(...any) (any, error) {
return time.Date(2024, 2, 26, 0, 0, 0, 0, time.UTC).Format(time.RFC3339), nil
Expand Down
191 changes: 191 additions & 0 deletions tests/regressions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// Copyright 2024 Peter Olds <[email protected]>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package tests

import (
"encoding/json"
"fmt"
"os"
"strconv"
"testing"

"gopkg.in/yaml.v3"

"github.com/polds/expr-playground/eval"
)

// TestExamples serves as a regression test for the examples presented in the playground UI.
// If any of the examples change, this test will fail, to help ensure the playground UI is
// updated accordingly, and especially so we don't accidentally push a broken sample.
func TestExamples(t *testing.T) {
examples := setup(t)

// lookup should exactly match the "name" field in the examples.yaml file.
tests := []struct {
lookup string
want string
wantErr bool
}{
{
lookup: "default",
want: "true",
},
{
lookup: "Check image registry",
want: "true",
},
{
lookup: "Disallow HostPorts",
want: "false",
},
{
lookup: "Require non-root containers",
want: "false",
},
{
lookup: "Drop ALL capabilities",
want: "true",
},
{
lookup: "Semantic version check for image tags (Regex)",
want: "false",
},
{
lookup: "URLs",
wantErr: true,
},
{
lookup: "Check JWT custom claims",
want: "true",
},
{
lookup: "Optional",
want: "fallback",
},
{
lookup: "Duration and timestamp",
want: "true",
},
{
lookup: "Quantity",
wantErr: true,
},
{
lookup: "Access Log Filtering",
want: "true",
},
{
lookup: "Custom Metrics",
want: "echo",
},
{
lookup: "Blank",
wantErr: true,
},
}
for _, tc := range tests {
t.Run(tc.lookup, func(t *testing.T) {
var exp Example
for _, e := range examples {
if e.Name == tc.lookup {
exp = e
break
}
}
if exp.Name == "" {
t.Fatalf("failed to find example %q", tc.lookup)
}

got, err := eval.Eval(exp.Expr, marshal(t, exp.Data))
if (err != nil) != tc.wantErr {
t.Errorf("Eval() got error %v, expected error %v", err, tc.wantErr)
}
if tc.wantErr {
return
}

var obj map[string]AlwaysString
if err := json.Unmarshal([]byte(got), &obj); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if s := obj["result"].Value; s != tc.want {
t.Errorf("Eval() got %q, expected %q", s, tc.want)
}
})
}
// Ensure these tests are updated when the examples are updated.
// Not a perfect solution, but it's better than nothing.
if len(examples) != len(tests) {
t.Errorf("Regression test counts got %d, expected %d", len(tests), len(examples))
}
}

type Example struct {
Name string `yaml:"name"`
Expr string `yaml:"expr"`
Data string `yaml:"data"`
}

func setup(t *testing.T) []Example {
t.Helper()

out, err := os.ReadFile("../examples.yaml")
if err != nil {
t.Fatalf("failed to read examples.yaml: %v", err)
}
var examples struct {
Examples []Example `yaml:"examples"`
}
if err := yaml.Unmarshal(out, &examples); err != nil {
t.Fatalf("failed to unmarshal examples.yaml: %v", err)
}
return examples.Examples
}

// Attempt to get the data into either yaml or json format.
func marshal(t *testing.T, s string) map[string]any {
t.Helper()

var v map[string]any
if yamlErr := yaml.Unmarshal([]byte(s), &v); yamlErr != nil {
if err := json.Unmarshal([]byte(s), &v); err != nil {
t.Errorf("failed to unmarshal %q as yaml: %v", s, yamlErr)
t.Fatalf("failed to unmarshal %q as json: %v", s, err)
}
}
return v
}

// AlwaysString attempts to unmarshal the value as a string.
type AlwaysString struct {
Value string
}

func (c *AlwaysString) UnmarshalJSON(b []byte) error {
var raw any
if err := json.Unmarshal(b, &raw); err != nil {
return err
}

switch v := raw.(type) {
case bool:
c.Value = strconv.FormatBool(v)
case string:
c.Value = v
default:
return fmt.Errorf("unsupported type %T", v)
}
return nil
}

0 comments on commit 8cebeba

Please sign in to comment.