Skip to content

Commit

Permalink
chore: enable proper parallel usage of TestDepth
Browse files Browse the repository at this point in the history
Rework the inners of `RunCLI` to support this.

Signed-off-by: Dmitriy Matrenichev <dmitry.matrenichev@siderolabs.com>
  • Loading branch information
DmitriyMV committed Dec 11, 2024
1 parent e1b824e commit 8180510
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 70 deletions.
19 changes: 13 additions & 6 deletions internal/integration/base/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"path/filepath"
"regexp"
"strings"
"testing"
"time"

"github.com/siderolabs/go-cmd/pkg/cmd"
Expand Down Expand Up @@ -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
}
Expand Down
54 changes: 30 additions & 24 deletions internal/integration/base/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -140,21 +143,23 @@ 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()

return &stdout, &stderr, err
}

// 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)
}

Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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
Expand Down
87 changes: 47 additions & 40 deletions internal/integration/cli/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ package cli

import (
"os"
"os/exec"
"regexp"
"slices"
"strings"
"testing"

Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand All @@ -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))
}

0 comments on commit 8180510

Please sign in to comment.