Skip to content

Commit

Permalink
script: Add -scripttest.update to update txtar files
Browse files Browse the repository at this point in the history
To make it easy to create and update expected test outputs, add the
-scripttest.update flag which updates the txtar files with the 'cmp'
inputs.

To allow waiting for the inputs to reach expected state, add the '!*'
prefix for retrying until failure, e.g. to allow writing:

  # Wait until no 'Pending' columns exist
  db show -columns=Status -o out.table test-table
  !* grep Pending out.table
  * cmp expected.table out.table

Now running "go test . -scripttest.update" will update the 'expected.table',
but only after it passes the grep.

Signed-off-by: Jussi Maki <[email protected]>
  • Loading branch information
joamaki committed Oct 25, 2024
1 parent 424a8cb commit e8f5948
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 19 deletions.
10 changes: 9 additions & 1 deletion script/cmds.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,14 @@ func doCompare(s *State, env bool, args ...string) error {
}

if text1 != text2 {
if s.DoUpdate {
// Updates requested, store the file contents and
// ignore mismatches.
s.FileUpdates[name1] = text2
s.FileUpdates[name2] = text1
return nil
}

if !quiet {
diffText := diff.Diff(name1, []byte(text1), name2, []byte(text2))
s.Logf("%s\n", diffText)
Expand Down Expand Up @@ -653,7 +661,7 @@ func match(s *State, args []string, text, name string) error {
isGrep := name == "grep"

wantArgs := 1
if len(args) != wantArgs {
if !isGrep && len(args) != wantArgs {
return ErrUsage
}

Expand Down
27 changes: 17 additions & 10 deletions script/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ type Engine struct {
Quiet bool

// RetryInterval for retrying commands marked with '*'. If zero, then
// retries are disabled.
// the default retry interval is used.
RetryInterval time.Duration
}

Expand All @@ -84,10 +84,12 @@ func NewEngine() *Engine {
return &Engine{
Cmds: DefaultCmds(),
Conds: DefaultConds(),
RetryInterval: 100 * time.Millisecond,
RetryInterval: defaultRetryInterval,
}
}

const defaultRetryInterval = 100 * time.Millisecond

// A Cmd is a command that is available to a script.
type Cmd interface {
// Run begins running the command.
Expand Down Expand Up @@ -172,6 +174,11 @@ func (e *Engine) Execute(s *State, file string, script *bufio.Reader, log io.Wri
defer func(prev io.Writer) { s.logOut = prev }(s.logOut)
s.logOut = log

retryInterval := e.RetryInterval
if retryInterval == 0 {
retryInterval = defaultRetryInterval
}

var (
sectionStart time.Time
sectionCmds []*command
Expand Down Expand Up @@ -307,14 +314,13 @@ func (e *Engine) Execute(s *State, file string, script *bufio.Reader, log io.Wri
// Run the command.
err = e.runCommand(s, cmd, impl)
if err != nil {
if cmd.want == successRetryOnFailure && e.RetryInterval > 0 {
if cmd.want == successRetryOnFailure || cmd.want == failureRetryOnSuccess {
// Command wants retries. Retry the whole section
for err != nil {
select {
case <-s.Context().Done():
err = lineErr(s.Context().Err())
break
case <-time.After(e.RetryInterval):
return lineErr(s.Context().Err())
case <-time.After(retryInterval):
}
for _, cmd := range sectionCmds {
impl := e.Cmds[cmd.name]
Expand Down Expand Up @@ -427,6 +433,7 @@ const (
failure expectedStatus = "!"
successOrFailure expectedStatus = "?"
successRetryOnFailure expectedStatus = "*"
failureRetryOnSuccess expectedStatus = "!*"
)

type argFragment struct {
Expand Down Expand Up @@ -469,9 +476,9 @@ func parse(filename string, lineno int, line string) (cmd *command, err error) {
// Prefix ? means allow either success or failure.
// Prefix * means to retry the command a few times.
switch want := expectedStatus(arg); want {
case failure, successOrFailure, successRetryOnFailure:
case failure, successOrFailure, successRetryOnFailure, failureRetryOnSuccess:
if cmd.want != "" {
return errors.New("duplicated '!', '?' or '*' token")
return errors.New("duplicated '!', '?', '*' or '!*' token")
}
cmd.want = want
return nil
Expand Down Expand Up @@ -705,7 +712,7 @@ func (e *Engine) runCommand(s *State, cmd *command, impl Cmd) error {

func checkStatus(cmd *command, err error) error {
if err == nil {
if cmd.want == failure {
if cmd.want == failure || cmd.want == failureRetryOnSuccess {
return cmdError(cmd, ErrUnexpectedSuccess)
}
return nil
Expand All @@ -730,7 +737,7 @@ func checkStatus(cmd *command, err error) error {
return cmdError(cmd, err)
}

if cmd.want == failure && (errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled)) {
if (cmd.want == failure || cmd.want == failureRetryOnSuccess) && (errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled)) {
// The command was terminated because the script is no longer interested in
// its output, so we don't know what it would have done had it run to
// completion — for all we know, it could have exited without error if it
Expand Down
27 changes: 27 additions & 0 deletions script/scripttest/scripttest.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"bytes"
"context"
"errors"
"flag"
"io"
"os"
"os/exec"
Expand All @@ -20,9 +21,12 @@ import (
"time"

"github.com/cilium/hive/script"
"golang.org/x/exp/slices"
"golang.org/x/tools/txtar"
)

var updateFlag = flag.Bool("scripttest.update", false, "Update scripttest files")

// DefaultCmds returns a set of broadly useful script commands.
//
// This set includes all of the commands in script.DefaultCmds,
Expand Down Expand Up @@ -181,6 +185,8 @@ func Test(t *testing.T, ctx context.Context, newEngine func(testing.TB) *script.
}
for _, file := range files {
file := file
wd, _ := os.Getwd()
absFile := filepath.Join(wd, file)
name := strings.TrimSuffix(filepath.Base(file), ".txt")
t.Run(name, func(t *testing.T) {
t.Parallel()
Expand All @@ -190,6 +196,7 @@ func Test(t *testing.T, ctx context.Context, newEngine func(testing.TB) *script.
if err != nil {
t.Fatal(err)
}
s.DoUpdate = *updateFlag

// Unpack archive.
a, err := txtar.ParseFile(file)
Expand All @@ -210,6 +217,26 @@ func Test(t *testing.T, ctx context.Context, newEngine func(testing.TB) *script.
// will work better seeing the full path relative to cmd/go
// (where the "go test" command is usually run).
Run(t, newEngine(t), s, file, bytes.NewReader(a.Comment))

if *updateFlag {
updated := false
for name, contents := range s.FileUpdates {
idx := slices.IndexFunc(a.Files, func(f txtar.File) bool { return f.Name == name })
if idx < 0 {
continue
}
a.Files[idx].Data = []byte(contents)
t.Logf("Updated %q", name)
updated = true
}
if updated {
err := os.WriteFile(absFile, txtar.Format(a), 0644)
if err != nil {
t.Fatal(err)
}
t.Logf("Wrote %q", absFile)
}
}
})
}
}
Expand Down
5 changes: 3 additions & 2 deletions script/scripttest/testdata/basic.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ cat hello.txt
stdout 'hello world'
! stderr 'hello world'

exec sh -c 'sleep 0.1 && echo > out.txt' &
exec sh -c 'sleep 0.1 && echo world > out.txt' &

# Retry section test
echo hello
* exec cat out.txt
* grep world out.txt
!* grep blah out.txt

-- hello.txt --
hello world
16 changes: 10 additions & 6 deletions script/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ type State struct {
stdout string // standard output from last 'go' command; for 'stdout' command
stderr string // standard error from last 'go' command; for 'stderr' command

DoUpdate bool
FileUpdates map[string]string

background []backgroundCmd
}

Expand Down Expand Up @@ -78,12 +81,13 @@ func NewState(ctx context.Context, workdir string, initialEnv []string) (*State,
}

s := &State{
ctx: ctx,
cancel: cancel,
workdir: absWork,
pwd: absWork,
env: env,
envMap: envMap,
ctx: ctx,
cancel: cancel,
workdir: absWork,
pwd: absWork,
env: env,
envMap: envMap,
FileUpdates: make(map[string]string),
}
s.Setenv("PWD", absWork)
return s, nil
Expand Down

0 comments on commit e8f5948

Please sign in to comment.