Skip to content

Commit 16d0ada

Browse files
committed
added lanai-cli basic testing lib and tests for init sub-command
1 parent b07dbef commit 16d0ada

21 files changed

+593
-12
lines changed

cmd/lanai-cli/cmdtest/cmd.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package cmdtest
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"github.com/cisco-open/go-lanai/cmd/lanai-cli/cmdutils"
7+
"github.com/cisco-open/go-lanai/test"
8+
"github.com/spf13/cobra"
9+
"os"
10+
"path/filepath"
11+
"testing"
12+
)
13+
14+
const (
15+
TestTmpDir = ".tmp"
16+
TestOutputDir = "output"
17+
)
18+
19+
var (
20+
testRootCmd = &cobra.Command{
21+
Use: "lanai-cli-test",
22+
Short: "lanai-cli for test",
23+
FParseErrWhitelist: cobra.FParseErrWhitelist{UnknownFlags: true},
24+
PersistentPreRunE: cmdutils.MergeRunE(
25+
cmdutils.EnsureGlobalDirectories(),
26+
cmdutils.PrintEnvironment(),
27+
),
28+
}
29+
)
30+
31+
func init() {
32+
cmdutils.PersistentFlags(testRootCmd, &cmdutils.GlobalArgs)
33+
}
34+
35+
func SetupResetPackageVars() test.SetupFunc {
36+
return func(ctx context.Context, t *testing.T) (context.Context, error) {
37+
cmdutils.ResetGoCmd()
38+
return ctx, nil
39+
}
40+
}
41+
42+
// DryRunCobraCommand Run given cobra.Command in given work dir relative to "testdata"
43+
func DryRunCobraCommand(ctx context.Context, wd string, cmd *cobra.Command, handler TestDryRunHandler, args ...string) error {
44+
// prepare global args
45+
originalArgs := cmdutils.GlobalArgs
46+
defer func() { cmdutils.GlobalArgs = originalArgs }()
47+
48+
cmdutils.GlobalArgs = cmdutils.Global{
49+
WorkingDir: PathRelativeToTestdata(wd),
50+
TmpDir: PathRelativeToTestdata(wd, TestTmpDir),
51+
OutputDir: PathRelativeToTestdata(wd, TestOutputDir),
52+
Verbose: true,
53+
DryRun: true,
54+
}
55+
56+
// setup dry run handler
57+
if handler != nil {
58+
cmdutils.GlobalArgs.DryRunFunc = handler.Handle
59+
} else {
60+
cmdutils.GlobalArgs.DryRunFunc = originalArgs.DryRunFunc
61+
}
62+
63+
// clean up FS
64+
if e := os.RemoveAll(cmdutils.GlobalArgs.OutputDir); e != nil {
65+
return fmt.Errorf("unable to start command with clean FS: %w", e)
66+
}
67+
68+
// run command
69+
if len(args) == 0 {
70+
args = []string{}
71+
}
72+
cmdCopy, _ := CopyCommandChain(cmd, args...)
73+
return cmdCopy.ExecuteContext(ctx)
74+
}
75+
76+
// CopyCommandChain do following things:
77+
// - Make copy of given command and its parents/ancestors.
78+
// - Fix arguments of its ancestors.
79+
// - Attach the given command chain to a copy of predefined test root command (to mimic how main() function works)
80+
// This function returns copied command and a copy of its root command. Two values may be same if they are the root
81+
func CopyCommandChain(cmd *cobra.Command, args...string) (cmdCpy, rootCpy *cobra.Command) {
82+
cmdCpy = copyCmd(cmd)
83+
var cpy, prev *cobra.Command
84+
for cpy, prev = cmdCpy, nil; cpy != nil; cpy = copyCmd(cpy.Parent()) {
85+
if prev != nil {
86+
args = append([]string{prev.Name()}, args...)
87+
cpy.ResetCommands()
88+
cpy.AddCommand(prev)
89+
}
90+
cpy.SetArgs(args)
91+
prev = cpy
92+
}
93+
rootCpy = copyCmd(testRootCmd)
94+
if prev != nil {
95+
args = append([]string{prev.Name()}, args...)
96+
rootCpy.ResetCommands()
97+
rootCpy.AddCommand(prev)
98+
}
99+
rootCpy.SetArgs(args)
100+
return
101+
}
102+
103+
func copyCmd(cmd *cobra.Command) *cobra.Command {
104+
if cmd == nil {
105+
return nil
106+
}
107+
vCopy := *cmd
108+
return &vCopy
109+
}
110+
111+
func PathRelativeToTestdata(pathComponents ...string) string {
112+
if base, e := os.Getwd(); e != nil {
113+
pathComponents = append([]string{"testdata"}, pathComponents...)
114+
} else {
115+
pathComponents = append([]string{base, "testdata"}, pathComponents...)
116+
}
117+
return filepath.Join(pathComponents...)
118+
}

cmd/lanai-cli/cmdtest/dryrun.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package cmdtest
2+
3+
import (
4+
"context"
5+
"github.com/cisco-open/go-lanai/cmd/lanai-cli/cmdutils"
6+
"github.com/cisco-open/go-lanai/pkg/utils/matcher"
7+
"github.com/cisco-open/go-lanai/test"
8+
"mvdan.cc/sh/v3/interp"
9+
"mvdan.cc/sh/v3/syntax"
10+
"testing"
11+
)
12+
13+
func SetupDryRun(handler TestDryRunHandler) test.SetupFunc {
14+
return func(ctx context.Context, t *testing.T) (context.Context, error) {
15+
cmdutils.GlobalArgs.DryRun = true
16+
if handler != nil {
17+
cmdutils.GlobalArgs.DryRunFunc = handler.Handle
18+
}
19+
return ctx, nil
20+
}
21+
}
22+
23+
type TestDryRunHandler map[cmdutils.DryRunCmdType]cmdutils.DryRunFunc
24+
25+
func (h TestDryRunHandler) Handle(ctx context.Context, cmdType cmdutils.DryRunCmdType, args ...interface{}) (interface{}, error) {
26+
if handler, ok := h[cmdType]; ok {
27+
return handler(ctx, cmdType, args...)
28+
}
29+
return nil, cmdutils.ErrDryRunIgnored
30+
}
31+
32+
func DryRunShellWithFilter(ignoreMatched bool, matchers ...matcher.StringMatcher) cmdutils.DryRunFunc {
33+
return func(ctx context.Context, cmdType cmdutils.DryRunCmdType, args ...interface{}) (interface{}, error) {
34+
if cmdType != cmdutils.DryRunTypeShell || len(args) < 4 {
35+
return nil, cmdutils.ErrDryRunIgnored
36+
}
37+
38+
var cmd string
39+
var runner *interp.Runner
40+
var parsedCmd *syntax.File
41+
for _, arg := range args {
42+
switch v := arg.(type) {
43+
case string:
44+
cmd = v
45+
case *interp.Runner:
46+
runner = v
47+
case *syntax.File:
48+
parsedCmd = v
49+
case *cmdutils.ShCmdOption:
50+
// do nothing
51+
default:
52+
return nil, cmdutils.ErrDryRunIgnored
53+
}
54+
}
55+
56+
for _, m := range matchers {
57+
ok, e := m.MatchesWithContext(ctx, cmd)
58+
if e != nil || ignoreMatched && ok || !ignoreMatched && !ok || runner == nil || parsedCmd == nil {
59+
continue
60+
}
61+
if e := runner.Run(ctx, parsedCmd); e != nil {
62+
if status, ok := interp.IsExitStatus(e); ok {
63+
return status, e
64+
}
65+
return uint8(1), e
66+
}
67+
return 0, nil
68+
}
69+
70+
return nil, cmdutils.ErrDryRunIgnored
71+
}
72+
}
73+

cmd/lanai-cli/cmdtest/package.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package cmdtest
2+
3+
import "github.com/cisco-open/go-lanai/test"
4+
5+
func WithDryRun(handler TestDryRunHandler) test.Options {
6+
return test.WithOptions(
7+
test.SubTestSetup(SetupResetPackageVars()),
8+
test.SubTestSetup(SetupDryRun(handler)),
9+
)
10+
}

cmd/lanai-cli/cmdutils/flags.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,11 @@ func parseStructForFlags(v reflect.Value) (ret []*flagMeta, err error) {
9999
}
100100

101101
meta, e := parseStructFieldForFlags(f, fv)
102-
if e != nil || meta == nil {
102+
if e != nil {
103103
return nil, e
104+
} else if meta != nil {
105+
ret = append(ret, meta)
104106
}
105-
ret = append(ret, meta)
106107
}
107108
return
108109
}

cmd/lanai-cli/cmdutils/global.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package cmdutils
1818

1919
import (
20+
"context"
21+
"errors"
2022
"github.com/cisco-open/go-lanai/pkg/log"
2123
"os"
2224
"path"
@@ -42,8 +44,22 @@ type Global struct {
4244
OutputDir string `flag:"output,o" desc:"output directory. All non-absolute paths for output are relative to this directory"`
4345
Verbose bool `flag:"debug" desc:"show debug information"`
4446
DryRun bool `flag:"dry-run" desc:"do not actually execute shell commands"`
47+
// DryRunFunc Used for tests. To provide an alternative behavior when DryRun flag is detected.
48+
// If not set (default case in non-testing execution), the behavior depends on supporting components.
49+
// e.g. for shell command, the command is logged.
50+
DryRunFunc DryRunFunc
4551
}
4652

53+
var ErrDryRunIgnored = errors.New("dry-run: ignored command")
54+
55+
type DryRunCmdType string
56+
57+
// DryRunFunc Used for tests. To provide an alternative behavior when DryRun flag is detected.
58+
// Implementing function could return special error ErrDryRunIgnored to indicate there is no alternative way to perform dry-run,
59+
// and the caller should apply default behavior
60+
type DryRunFunc func(ctx context.Context, cmdType DryRunCmdType, args...interface{}) (interface{}, error)
61+
62+
4763
func (g Global) AbsPath(base, path string) string {
4864
if filepath.IsAbs(path) {
4965
return filepath.Clean(path)

cmd/lanai-cli/cmdutils/go_cmd.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ var (
4141
packageImportPathCacheOnce = sync.Once{}
4242
)
4343

44+
// ResetGoCmd Used for tests. Reset any package-level variables related to go command
45+
func ResetGoCmd() {
46+
targetTmpGoModFile = ""
47+
targetModule = nil
48+
targetModuleOnce = sync.Once{}
49+
packageImportPathCache = nil
50+
packageImportPathCacheOnce = sync.Once{}
51+
}
52+
4453
type GoCmdOptions func(goCmd *string)
4554

4655
func GoCmdModFile(modFile string) GoCmdOptions {

cmd/lanai-cli/cmdutils/shell.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package cmdutils
1818

1919
import (
2020
"context"
21+
"errors"
2122
"io"
2223
"mvdan.cc/sh/v3/expand"
2324
"mvdan.cc/sh/v3/interp"
@@ -42,6 +43,8 @@ type ShCmdOption struct {
4243
Stderr io.Writer
4344
}
4445

46+
var DryRunTypeShell DryRunCmdType = "shell"
47+
4548
// ShellCmd add shell commends
4649
func ShellCmd(cmds ...string) ShCmdOptions {
4750
return func(opt *ShCmdOption) {
@@ -150,6 +153,17 @@ func runSingleCommand(ctx context.Context, cmd string, opt *ShCmdOption) (uint8,
150153
}
151154

152155
if GlobalArgs.DryRun {
156+
if GlobalArgs.DryRunFunc != nil {
157+
switch rs, e := GlobalArgs.DryRunFunc(ctx, DryRunTypeShell, cmd, opt, r, p); {
158+
case e != nil && !errors.Is(e, ErrDryRunIgnored):
159+
if status, ok := rs.(uint8); ok {
160+
return status, e
161+
}
162+
return 1, e
163+
case e == nil:
164+
return 0, nil
165+
}
166+
}
153167
logger.WithContext(ctx).Infof("Run: %s", cmd)
154168
return 0, nil
155169
}

cmd/lanai-cli/initcmd/binaries_test.go

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ package initcmd
22

33
import (
44
"context"
5-
"github.com/cisco-open/go-lanai/cmd/lanai-cli/cmdutils"
5+
"github.com/cisco-open/go-lanai/cmd/lanai-cli/cmdtest"
66
"github.com/cisco-open/go-lanai/test"
77
"github.com/onsi/gomega"
88
"testing"
99
)
1010

1111
func TestInstallBinaries(t *testing.T) {
1212
test.RunTest(context.Background(), t,
13-
test.Setup(SetupGlobalArgs()),
13+
cmdtest.WithDryRun(nil),
1414
test.GomegaSubTest(SubTestWithBinaryOverride(), "WithBinaryOverride"),
1515
)
1616
}
@@ -19,13 +19,6 @@ func TestInstallBinaries(t *testing.T) {
1919
Sub-Test Cases
2020
*************************/
2121

22-
func SetupGlobalArgs() test.SetupFunc {
23-
return func(ctx context.Context, t *testing.T) (context.Context, error) {
24-
cmdutils.GlobalArgs.DryRun = true
25-
return ctx, nil
26-
}
27-
}
28-
2922
func SubTestWithBinaryOverride() test.GomegaSubTestFunc {
3023
return func(ctx context.Context, t *testing.T, g *gomega.WithT) {
3124
Module.Binaries = []*Binary{

0 commit comments

Comments
 (0)