Skip to content

Commit

Permalink
testutils: Add script-based test utilities
Browse files Browse the repository at this point in the history
Add testutils.CommandBuilder for creating commands for testcript.

This allows implementing tests as scripts, which becomes useful when
tests perform multiple steps on tables and need to verify the output
each step.

Signed-off-by: Jussi Maki <[email protected]>
  • Loading branch information
joamaki committed Sep 20, 2024
1 parent f96349e commit ac2acec
Show file tree
Hide file tree
Showing 5 changed files with 265 additions and 1 deletion.
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ go 1.23
require (
github.com/cilium/hive v0.0.0-20240209163124-bd6ebb4ec11d
github.com/cilium/stream v0.0.0-20240209152734-a0792b51812d
github.com/rogpeppe/go-internal v1.11.0
github.com/spf13/cobra v1.8.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.4
go.uber.org/goleak v1.3.0
golang.org/x/time v0.5.0
gopkg.in/yaml.v3 v3.0.1
)

require (
Expand All @@ -34,7 +36,7 @@ require (
golang.org/x/sys v0.17.0 // indirect
golang.org/x/term v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.17.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
Expand Down
164 changes: 164 additions & 0 deletions testutils/script.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Cilium

package testutils

import (
"bytes"
"flag"
"fmt"
"slices"
"strings"
"text/tabwriter"
"time"

"github.com/cilium/statedb"
"github.com/rogpeppe/go-internal/testscript"
"gopkg.in/yaml.v3"
)

type Cmd = func(ts *testscript.TestScript, neg bool, args []string)

func ShowTableCmd[Obj statedb.TableWritable](db *statedb.DB, tbl statedb.Table[Obj]) Cmd {
var zero Obj
return func(ts *testscript.TestScript, neg bool, args []string) {
var buf bytes.Buffer
w := tabwriter.NewWriter(&buf, 5, 4, 3, ' ', 0)
fmt.Fprintf(w, "%s\n", strings.Join(zero.TableHeader(), "\t"))
for obj := range tbl.All(db.ReadTxn()) {
fmt.Fprintf(w, "%s\n", strings.Join(obj.TableRow(), "\t"))
}
w.Flush()
ts.Logf("%s", buf.String())
}
}

func CompareTableCmd[Obj statedb.TableWritable](db *statedb.DB, tbl statedb.Table[Obj]) Cmd {
var zero Obj
return func(ts *testscript.TestScript, neg bool, args []string) {
var flags flag.FlagSet
wait := flags.Duration("wait", 0, "Wait for the table contents to match")
err := flags.Parse(args)
args = args[len(args)-flags.NArg():]
if err != nil || len(args) != 1 {
ts.Fatalf("usage: cmp [-wait=<duration>] file")
}

data := ts.ReadFile(args[0])
lines := strings.Split(data, "\n")
if len(lines) < 1 {
ts.Fatalf("input too short")
}
lines = slices.DeleteFunc(lines, func(line string) bool {
return strings.TrimSpace(line) == ""
})

header := zero.TableHeader()
columnNames := strings.Split(lines[0], "\t")
columns := make([]int, 0, len(header))
loop:
for _, name := range columnNames {
for i, name2 := range header {
if strings.EqualFold(name, name2) {
columns = append(columns, i)
continue loop
}
}
ts.Fatalf("column %q not part of %v", name, header)
}
lines = lines[1:]
origLines := lines

tryUntil := time.Now().Add(*wait)
for {
lines = origLines

equal := true
var diff bytes.Buffer
w := tabwriter.NewWriter(&diff, 5, 4, 3, ' ', 0)

fmt.Fprintf(w, " %s\n", strings.Join(zero.TableHeader(), "\t"))

for obj := range tbl.All(db.ReadTxn()) {
row := strings.Join(takeColumns(obj.TableRow(), columns), "\t")
if len(lines) == 0 {
equal = false
fmt.Fprintf(w, "- %s\n", row)
continue
}
line := lines[0]
if row == line {
fmt.Fprintf(w, " %s\n", row)
} else {
fmt.Fprintf(w, "- %s\n", row)
fmt.Fprintf(w, "+ %s\n", line)
equal = false
}
lines = lines[1:]
}
for _, line := range lines {
fmt.Fprintf(w, "+ %s\n", line)
equal = false
}

if equal {
break
}
w.Flush()
if time.Now().After(tryUntil) {
ts.Fatalf("table mismatch (check tabs!):\n%s", diff.String())
}
time.Sleep(10 * time.Millisecond)
}
}
}

func takeColumns[T any](xs []T, idxs []int) []T {
// Assuming idxs is sorted.
for i, idx := range idxs {
xs[i] = xs[idx]
}
return xs[:len(idxs)]
}

func InsertCmd[Obj any](db *statedb.DB, tbl statedb.RWTable[Obj]) Cmd {
return func(ts *testscript.TestScript, neg bool, args []string) {
if len(args) == 0 {
ts.Fatalf("usage: insert path...")
}
wtxn := db.WriteTxn(tbl)
defer wtxn.Commit()
for _, arg := range args {
data := ts.ReadFile(arg)
var obj Obj
if err := yaml.Unmarshal([]byte(data), &obj); err != nil {
ts.Fatalf("Unmarshal(%s): %s", arg, err)
}
_, _, err := tbl.Insert(wtxn, obj)
if err != nil {
ts.Fatalf("Insert(%s): %s", arg, err)
}
}
}
}

func DeleteCmd[Obj any](db *statedb.DB, tbl statedb.RWTable[Obj]) Cmd {
return func(ts *testscript.TestScript, neg bool, args []string) {
if len(args) == 0 {
ts.Fatalf("usage: delete path...")
}
wtxn := db.WriteTxn(tbl)
defer wtxn.Commit()
for _, arg := range args {
data := ts.ReadFile(arg)
var obj Obj
if err := yaml.Unmarshal([]byte(data), &obj); err != nil {
ts.Fatalf("Unmarshal(%s): %s", arg, err)
}
_, _, err := tbl.Delete(wtxn, obj)
if err != nil {
ts.Fatalf("Delete(%s): %s", arg, err)
}
}
}
}
69 changes: 69 additions & 0 deletions testutils/script_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Cilium

package testutils_test

import (
"strconv"
"strings"
"testing"

"github.com/cilium/statedb"
"github.com/cilium/statedb/index"
"github.com/cilium/statedb/testutils"
"github.com/rogpeppe/go-internal/testscript"
)

type object struct {
ID uint64
Name string
Tags []string
}

func (o object) TableHeader() []string {
return []string{"ID", "Name", "Tags"}
}

func (o object) TableRow() []string {
return []string{
strconv.FormatUint(o.ID, 10),
o.Name,
strings.Join(o.Tags, ", "),
}
}

var idIdx = statedb.Index[object, uint64]{
Name: "id",
FromObject: func(obj object) index.KeySet {
return index.NewKeySet(index.Uint64(obj.ID))
},
FromKey: index.Uint64,
Unique: true,
}

func TestScriptCommands(t *testing.T) {
db := statedb.New()
tbl, err := statedb.NewTable("test", idIdx)
if err != nil {
t.Fatalf("NewTable: %s", err)
}
if err := db.RegisterTable(tbl); err != nil {
t.Fatalf("RegisterTable: %s", err)
}

testscript.Run(t, testscript.Params{
Dir: "testdata",
Setup: func(e *testscript.Env) error {
txn := db.WriteTxn(tbl)
tbl.DeleteAll(txn)
txn.Commit()
return nil
},
Cmds: map[string]testutils.Cmd{
"cmp_objects": testutils.CompareTableCmd(db, tbl),
"show_objects": testutils.ShowTableCmd(db, tbl),
"insert_object": testutils.InsertCmd(db, tbl),
"delete_object": testutils.DeleteCmd(db, tbl),
},
})
}
27 changes: 27 additions & 0 deletions testutils/testdata/test.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
insert_object example.yaml
show_objects
cmp_objects example.table
cmp_objects example_only_ids.table

cmp_objects -wait=1s example.table

delete_object example.yaml
cmp_objects empty.table

-- example.yaml --
id: 123
name: quux
tags:
- foo
- bar

-- example.table --
ID Name Tags
123 quux foo, bar

-- example_only_ids.table --
ID
123

-- empty.table --
ID

0 comments on commit ac2acec

Please sign in to comment.