From e8f5948dc6a0765a4e9b6820adc9650274287d04 Mon Sep 17 00:00:00 2001 From: Jussi Maki Date: Fri, 25 Oct 2024 13:30:35 +0200 Subject: [PATCH] script: Add -scripttest.update to update txtar files 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 --- script/cmds.go | 10 +++++++++- script/engine.go | 27 +++++++++++++++++---------- script/scripttest/scripttest.go | 27 +++++++++++++++++++++++++++ script/scripttest/testdata/basic.txt | 5 +++-- script/state.go | 16 ++++++++++------ 5 files changed, 66 insertions(+), 19 deletions(-) diff --git a/script/cmds.go b/script/cmds.go index acea412..7ae0a79 100644 --- a/script/cmds.go +++ b/script/cmds.go @@ -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) @@ -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 } diff --git a/script/engine.go b/script/engine.go index e45124c..e7ffbf3 100644 --- a/script/engine.go +++ b/script/engine.go @@ -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 } @@ -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. @@ -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 @@ -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] @@ -427,6 +433,7 @@ const ( failure expectedStatus = "!" successOrFailure expectedStatus = "?" successRetryOnFailure expectedStatus = "*" + failureRetryOnSuccess expectedStatus = "!*" ) type argFragment struct { @@ -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 @@ -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 @@ -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 diff --git a/script/scripttest/scripttest.go b/script/scripttest/scripttest.go index f02d9ca..5e4a34e 100644 --- a/script/scripttest/scripttest.go +++ b/script/scripttest/scripttest.go @@ -10,6 +10,7 @@ import ( "bytes" "context" "errors" + "flag" "io" "os" "os/exec" @@ -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, @@ -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() @@ -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) @@ -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) + } + } }) } } diff --git a/script/scripttest/testdata/basic.txt b/script/scripttest/testdata/basic.txt index 6f05cdb..fc51939 100644 --- a/script/scripttest/testdata/basic.txt +++ b/script/scripttest/testdata/basic.txt @@ -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 diff --git a/script/state.go b/script/state.go index f6b2745..9a3580b 100644 --- a/script/state.go +++ b/script/state.go @@ -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 } @@ -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