From 81805103deada24b12b7d7861b2df5a5c788c86b Mon Sep 17 00:00:00 2001 From: Dmitriy Matrenichev Date: Mon, 9 Dec 2024 21:49:29 +0300 Subject: [PATCH] chore: enable proper parallel usage of TestDepth Rework the inners of `RunCLI` to support this. Signed-off-by: Dmitriy Matrenichev --- internal/integration/base/cli.go | 19 ++++--- internal/integration/base/run.go | 54 +++++++++++--------- internal/integration/cli/list.go | 87 +++++++++++++++++--------------- 3 files changed, 90 insertions(+), 70 deletions(-) diff --git a/internal/integration/base/cli.go b/internal/integration/base/cli.go index ad8c94d56c..ab7c35f769 100644 --- a/internal/integration/base/cli.go +++ b/internal/integration/base/cli.go @@ -14,6 +14,7 @@ import ( "path/filepath" "regexp" "strings" + "testing" "time" "github.com/siderolabs/go-cmd/pkg/cmd" @@ -100,27 +101,33 @@ func (cliSuite *CLISuite) discoverKubectl() cluster.Info { return nodeInfo } -// buildCLICmd builds exec.Cmd from TalosSuite and args. +// RunCLI runs talosctl binary with the options provided. +func (cliSuite *CLISuite) RunCLI(args []string, options ...RunOption) (stdout, stderr string) { + return run(cliSuite.T(), cliSuite.MakeCMDFn(args), options...) +} + +// MakeCMDFn returns a function that creates a new exec.Cmd with the provided args. // TalosSuite flags are added at the beginning so they can be overridden by args. -func (cliSuite *CLISuite) buildCLICmd(args []string) *exec.Cmd { +func (cliSuite *CLISuite) MakeCMDFn(args []string) func() *exec.Cmd { if cliSuite.Endpoint != "" { args = append([]string{"--endpoints", cliSuite.Endpoint}, args...) } args = append([]string{"--talosconfig", cliSuite.TalosConfig}, args...) + path := cliSuite.TalosctlPath - return exec.Command(cliSuite.TalosctlPath, args...) + return func() *exec.Cmd { return exec.Command(path, args...) } } // RunCLI runs talosctl binary with the options provided. -func (cliSuite *CLISuite) RunCLI(args []string, options ...RunOption) (stdout, stderr string) { - return run(&cliSuite.Suite, func() *exec.Cmd { return cliSuite.buildCLICmd(args) }, options...) +func RunCLI(t *testing.T, f func() *exec.Cmd, options ...RunOption) (stdout, stderr string) { + return run(t, f, options...) } // RunAndWaitForMatch retries command until output matches. func (cliSuite *CLISuite) RunAndWaitForMatch(args []string, regex *regexp.Regexp, duration time.Duration, options ...retry.Option) { cliSuite.Assert().NoError(retry.Constant(duration, options...).Retry(func() error { - stdout, _, err := runAndWait(&cliSuite.Suite, cliSuite.buildCLICmd(args)) + stdout, _, err := runAndWait(cliSuite.Suite.T(), cliSuite.MakeCMDFn(args)()) if err != nil { return err } diff --git a/internal/integration/base/run.go b/internal/integration/base/run.go index be1ca52f23..960f0eb12e 100644 --- a/internal/integration/base/run.go +++ b/internal/integration/base/run.go @@ -8,13 +8,16 @@ package base import ( "bytes" + "errors" "os" "os/exec" "regexp" "strings" + "testing" "github.com/siderolabs/go-retry/retry" - "github.com/stretchr/testify/suite" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // RunOption configures options for Run. @@ -115,7 +118,7 @@ func StderrMatchFunc(f MatchFunc) RunOption { // runAndWait launches the command and waits for completion. // // runAndWait doesn't do any assertions on result. -func runAndWait(suite *suite.Suite, cmd *exec.Cmd) (stdoutBuf, stderrBuf *bytes.Buffer, err error) { +func runAndWait(t *testing.T, cmd *exec.Cmd) (stdoutBuf, stderrBuf *bytes.Buffer, err error) { var stdout, stderr bytes.Buffer cmd.Stdin = nil @@ -140,9 +143,9 @@ func runAndWait(suite *suite.Suite, cmd *exec.Cmd) (stdoutBuf, stderrBuf *bytes. } } - suite.T().Logf("Running %q", strings.Join(cmd.Args, " ")) + t.Logf("Running %q", strings.Join(cmd.Args, " ")) - suite.Require().NoError(cmd.Start()) + require.NoError(t, cmd.Start()) err = cmd.Wait() @@ -150,11 +153,13 @@ func runAndWait(suite *suite.Suite, cmd *exec.Cmd) (stdoutBuf, stderrBuf *bytes. } // retryRunAndWait retries runAndWait if the command fails to run. -func retryRunAndWait(suite *suite.Suite, cmdFunc func() *exec.Cmd, retryer retry.Retryer) (stdoutBuf, stderrBuf *bytes.Buffer, err error) { +func retryRunAndWait(t *testing.T, cmdFunc func() *exec.Cmd, retryer retry.Retryer) (stdoutBuf, stderrBuf *bytes.Buffer, err error) { err = retryer.Retry(func() error { - stdoutBuf, stderrBuf, err = runAndWait(suite, cmdFunc()) + stdoutBuf, stderrBuf, err = runAndWait(t, cmdFunc()) - if _, ok := err.(*exec.ExitError); ok { + var exitError *exec.ExitError + + if errors.As(err, &exitError) { return retry.ExpectedErrorf("command failed, stderr %v: %w", stderrBuf.String(), err) } @@ -167,7 +172,7 @@ func retryRunAndWait(suite *suite.Suite, cmdFunc func() *exec.Cmd, retryer retry // run executes command, asserts on its exit status/output, and returns stdout. // //nolint:gocyclo,nakedret -func run(suite *suite.Suite, cmdFunc func() *exec.Cmd, options ...RunOption) (stdout, stderr string) { +func run(t *testing.T, cmdFunc func() *exec.Cmd, options ...RunOption) (stdout, stderr string) { var opts runOptions for _, o := range options { @@ -180,15 +185,16 @@ func run(suite *suite.Suite, cmdFunc func() *exec.Cmd, options ...RunOption) (st ) if opts.retryer != nil { - stdoutBuf, stderrBuf, err = retryRunAndWait(suite, cmdFunc, opts.retryer) + stdoutBuf, stderrBuf, err = retryRunAndWait(t, cmdFunc, opts.retryer) } else { - stdoutBuf, stderrBuf, err = runAndWait(suite, cmdFunc()) + stdoutBuf, stderrBuf, err = runAndWait(t, cmdFunc()) } if err != nil { // check that command failed, not something else happened - _, ok := err.(*exec.ExitError) - suite.Require().True(ok, "%s", err) + var exitError *exec.ExitError + + require.True(t, errors.As(err, &exitError), "%s", err) } if stdoutBuf != nil { @@ -200,45 +206,45 @@ func run(suite *suite.Suite, cmdFunc func() *exec.Cmd, options ...RunOption) (st } if opts.shouldFail { - suite.Assert().Error(err, "command expected to fail, but did not") + assert.Error(t, err, "command expected to fail, but did not") } else { - suite.Assert().NoError(err, "command failed, stdout: %q, stderr: %q", stdout, stderr) + assert.NoError(t, err, "command failed, stdout: %q, stderr: %q", stdout, stderr) } if opts.stdoutEmpty { - suite.Assert().Empty(stdout, "stdout should be empty") + assert.Empty(t, stdout, "stdout should be empty") } else { - suite.Assert().NotEmpty(stdout, "stdout should be not empty") + assert.NotEmpty(t, stdout, "stdout should be not empty") } if opts.stderrNotEmpty { - suite.Assert().NotEmpty(stderr, "stderr should be not empty") + assert.NotEmpty(t, stderr, "stderr should be not empty") } else { - suite.Assert().Empty(stderr, "stderr should be empty") + assert.Empty(t, stderr, "stderr should be empty") } for _, rx := range opts.stdoutRegexps { - suite.Assert().Regexp(rx, stdout) + assert.Regexp(t, rx, stdout) } for _, rx := range opts.stderrRegexps { - suite.Assert().Regexp(rx, stderr) + assert.Regexp(t, rx, stderr) } for _, rx := range opts.stdoutNegativeRegexps { - suite.Assert().NotRegexp(rx, stdout) + assert.NotRegexp(t, rx, stdout) } for _, rx := range opts.stderrNegativeRegexps { - suite.Assert().NotRegexp(rx, stderr) + assert.NotRegexp(t, rx, stderr) } for _, f := range opts.stdoutMatchers { - suite.Assert().NoError(f(stdout), "stdout match: %q", stdout) + assert.NoError(t, f(stdout), "stdout match: %q", stdout) } for _, f := range opts.stderrMatchers { - suite.Assert().NoError(f(stderr), "stderr match: %q", stderr) + assert.NoError(t, f(stderr), "stderr match: %q", stderr) } return diff --git a/internal/integration/cli/list.go b/internal/integration/cli/list.go index 2999f10598..2e805b1f53 100644 --- a/internal/integration/cli/list.go +++ b/internal/integration/cli/list.go @@ -8,7 +8,9 @@ package cli import ( "os" + "os/exec" "regexp" + "slices" "strings" "testing" @@ -38,6 +40,8 @@ func (suite *ListSuite) TestSuccess() { } // TestDepth tests various combinations of --recurse and --depth flags. +// +//nolint:tparallel func (suite *ListSuite) TestDepth() { node := suite.RandomDiscoveredNodeInternalIP(machine.TypeControlPlane) @@ -51,44 +55,6 @@ func (suite *ListSuite) TestDepth() { } // checks that enough separators are encountered in the output - runAndCheck := func(t *testing.T, expectedSeparators int, flags ...string) { - args := append([]string{"list", "--nodes", node, "/system"}, flags...) - stdout, _ := suite.RunCLI(args) - - lines := strings.Split(strings.TrimSpace(stdout), "\n") - assert.Greater(t, len(lines), 2) - assert.Equal(t, []string{"NODE", "NAME"}, strings.Fields(lines[0])) - assert.Equal(t, []string{"."}, strings.Fields(lines[1])[1:]) - - var maxActualSeparators int - - for _, line := range lines[2:] { - actualSeparators := strings.Count(strings.Fields(line)[1], string(os.PathSeparator)) - - if !assert.LessOrEqual( - t, - actualSeparators, - expectedSeparators, - "too many separators, flags: %s\nlines:\n%s", - strings.Join(flags, " "), - stdout, - ) { - return - } - - maxActualSeparators = max(maxActualSeparators, actualSeparators) - } - - assert.Equal( - t, - expectedSeparators, - maxActualSeparators, - "not enough separators, \nflags: %s\nlines:\n%s", - strings.Join(flags, " "), - stdout, - ) - } - for _, test := range []struct { separators int flags []string @@ -102,12 +68,53 @@ func (suite *ListSuite) TestDepth() { {separators: 2, flags: []string{"--depth=3"}}, {separators: maxSeps, flags: []string{"--recurse=true"}}, } { - suite.Run(strings.Join(test.flags, ","), func() { - runAndCheck(suite.T(), test.separators, test.flags...) + cmdFn := suite.MakeCMDFn(slices.Insert(test.flags, 0, "list", "--nodes", node, "/system")) + + suite.T().Run(strings.Join(test.flags, ","), func(t *testing.T) { + t.Parallel() + + runAndCheck(t, test.separators, cmdFn, test.flags...) }) } } +func runAndCheck(t *testing.T, expectedSeparators int, cmdFn func() *exec.Cmd, flags ...string) { + stdout, _ := base.RunCLI(t, cmdFn) + + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Greater(t, len(lines), 2) + assert.Equal(t, []string{"NODE", "NAME"}, strings.Fields(lines[0])) + assert.Equal(t, []string{"."}, strings.Fields(lines[1])[1:]) + + var maxActualSeparators int + + for _, line := range lines[2:] { + actualSeparators := strings.Count(strings.Fields(line)[1], string(os.PathSeparator)) + + if !assert.LessOrEqual( + t, + actualSeparators, + expectedSeparators, + "too many separators, flags: %s\nlines:\n%s", + strings.Join(flags, " "), + stdout, + ) { + return + } + + maxActualSeparators = max(maxActualSeparators, actualSeparators) + } + + assert.Equal( + t, + expectedSeparators, + maxActualSeparators, + "not enough separators, \nflags: %s\nlines:\n%s", + strings.Join(flags, " "), + stdout, + ) +} + func init() { allSuites = append(allSuites, new(ListSuite)) }