diff --git a/cmd/init.go b/cmd/init.go index d7fe906..96f4f3b 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -1,19 +1,20 @@ package cmd import ( - "github.com/linuxsuren/api-testing/pkg/exec" + fakeruntime "github.com/linuxsuren/go-fake-runtime" "github.com/spf13/cobra" ) type initOption struct { + execer fakeruntime.Execer kustomization string waitNamespace string waitResource string } // createInitCommand returns the init command -func createInitCommand() (cmd *cobra.Command) { - opt := &initOption{} +func createInitCommand(execer fakeruntime.Execer) (cmd *cobra.Command) { + opt := &initOption{execer: execer} cmd = &cobra.Command{ Use: "init", Long: "Support to init Kubernetes cluster with kustomization, and wait it with command: kubectl wait", @@ -30,13 +31,13 @@ func createInitCommand() (cmd *cobra.Command) { func (o *initOption) runE(cmd *cobra.Command, args []string) (err error) { if o.kustomization != "" { - if err = exec.RunCommand("kubectl", "apply", "-k", o.kustomization, "--wait=true"); err != nil { + if err = o.execer.RunCommand("kubectl", "apply", "-k", o.kustomization, "--wait=true"); err != nil { return } } if o.waitNamespace != "" && o.waitResource != "" { - if err = exec.RunCommand("kubectl", "wait", "-n", o.waitNamespace, o.waitResource, "--for", "condition=Available=True", "--timeout=900s"); err != nil { + if err = o.execer.RunCommand("kubectl", "wait", "-n", o.waitNamespace, o.waitResource, "--for", "condition=Available=True", "--timeout=900s"); err != nil { return } } diff --git a/cmd/jsonschema_test.go b/cmd/jsonschema_test.go index 03ff263..830d675 100644 --- a/cmd/jsonschema_test.go +++ b/cmd/jsonschema_test.go @@ -6,11 +6,12 @@ import ( "testing" "github.com/linuxsuren/api-testing/cmd" + fakeruntime "github.com/linuxsuren/go-fake-runtime" "github.com/stretchr/testify/assert" ) func TestJSONSchemaCmd(t *testing.T) { - c := cmd.NewRootCmd() + c := cmd.NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}) buf := new(bytes.Buffer) c.SetOut(buf) diff --git a/cmd/root.go b/cmd/root.go index 17710a3..7f01221 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,19 +4,23 @@ import ( "os" "github.com/linuxsuren/api-testing/pkg/version" + fakeruntime "github.com/linuxsuren/go-fake-runtime" "github.com/spf13/cobra" + "google.golang.org/grpc" ) // NewRootCmd creates the root command -func NewRootCmd() (c *cobra.Command) { +func NewRootCmd(execer fakeruntime.Execer) (c *cobra.Command) { c = &cobra.Command{ Use: "atest", Short: "API testing tool", } c.SetOut(os.Stdout) c.Version = version.GetVersion() - c.AddCommand(createInitCommand(), + gRPCServer := grpc.NewServer() + c.AddCommand(createInitCommand(execer), createRunCommand(), createSampleCmd(), - createServerCmd(), createJSONSchemaCmd()) + createServerCmd(gRPCServer), createJSONSchemaCmd(), + createServiceCommand(execer)) return } diff --git a/cmd/root_test.go b/cmd/root_test.go index c8851b4..87f7afe 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/assert" atesting "github.com/linuxsuren/api-testing/pkg/testing" + exec "github.com/linuxsuren/go-fake-runtime" ) func Test_setRelativeDir(t *testing.T) { @@ -43,10 +44,15 @@ func TestCreateRunCommand(t *testing.T) { cmd := createRunCommand() assert.Equal(t, "run", cmd.Use) - init := createInitCommand() + init := createInitCommand(exec.FakeExecer{}) assert.Equal(t, "init", init.Use) - server := createServerCmd() + server := createServerCmd(&fakeGRPCServer{}) assert.NotNil(t, server) assert.Equal(t, "server", server.Use) + + root := NewRootCmd(exec.FakeExecer{}) + root.SetArgs([]string{"init", "-k=demo.yaml", "--wait-namespace", "demo", "--wait-resource", "demo"}) + err := root.Execute() + assert.Nil(t, err) } diff --git a/cmd/run_test.go b/cmd/run_test.go index d20f739..3dac0d7 100644 --- a/cmd/run_test.go +++ b/cmd/run_test.go @@ -8,6 +8,7 @@ import ( "github.com/h2non/gock" "github.com/linuxsuren/api-testing/pkg/limit" + fakeruntime "github.com/linuxsuren/go-fake-runtime" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" ) @@ -103,7 +104,7 @@ func TestRunCommand(t *testing.T) { } func TestRootCmd(t *testing.T) { - c := NewRootCmd() + c := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}) assert.NotNil(t, c) assert.Equal(t, "atest", c.Use) } diff --git a/cmd/sample_test.go b/cmd/sample_test.go index 8f0a345..eb81957 100644 --- a/cmd/sample_test.go +++ b/cmd/sample_test.go @@ -6,11 +6,12 @@ import ( "github.com/linuxsuren/api-testing/cmd" "github.com/linuxsuren/api-testing/sample" + fakeruntime "github.com/linuxsuren/go-fake-runtime" "github.com/stretchr/testify/assert" ) func TestSampleCmd(t *testing.T) { - c := cmd.NewRootCmd() + c := cmd.NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}) buf := new(bytes.Buffer) c.SetOut(buf) diff --git a/cmd/server.go b/cmd/server.go index b27409e..f770902 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -11,20 +11,21 @@ import ( "google.golang.org/grpc" ) -func createServerCmd() (c *cobra.Command) { - opt := &serverOption{} +func createServerCmd(gRPCServer gRPCServer) (c *cobra.Command) { + opt := &serverOption{gRPCServer: gRPCServer} c = &cobra.Command{ Use: "server", Short: "Run as a server mode", RunE: opt.runE, } flags := c.Flags() - flags.IntVarP(&opt.port, "port", "p", 9090, "The RPC server port") + flags.IntVarP(&opt.port, "port", "p", 7070, "The RPC server port") flags.BoolVarP(&opt.printProto, "print-proto", "", false, "Print the proto content and exit") return } type serverOption struct { + gRPCServer gRPCServer port int printProto bool } @@ -43,9 +44,26 @@ func (o *serverOption) runE(cmd *cobra.Command, args []string) (err error) { return } - s := grpc.NewServer() + s := o.gRPCServer server.RegisterRunnerServer(s, server.NewRemoteServer()) log.Printf("server listening at %v", lis.Addr()) s.Serve(lis) return } + +type gRPCServer interface { + Serve(lis net.Listener) error + grpc.ServiceRegistrar +} + +type fakeGRPCServer struct { +} + +// Serve is a fake method +func (s *fakeGRPCServer) Serve(net.Listener) error { + return nil +} + +// RegisterService is a fake method +func (s *fakeGRPCServer) RegisterService(desc *grpc.ServiceDesc, impl interface{}) { +} diff --git a/cmd/server_test.go b/cmd/server_test.go index 8a13e0b..f941fe8 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -5,6 +5,7 @@ import ( "strings" "testing" + fakeruntime "github.com/linuxsuren/go-fake-runtime" "github.com/stretchr/testify/assert" ) @@ -30,11 +31,15 @@ func TestPrintProto(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { buf := new(bytes.Buffer) - root := NewRootCmd() + root := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}) root.SetOut(buf) root.SetArgs(tt.args) err := root.Execute() tt.verify(t, buf, err) }) } + + server := createServerCmd(&fakeGRPCServer{}) + err := server.Execute() + assert.Nil(t, err) } diff --git a/cmd/service.go b/cmd/service.go new file mode 100644 index 0000000..d1d319f --- /dev/null +++ b/cmd/service.go @@ -0,0 +1,76 @@ +// Package cmd provides a service command +package cmd + +import ( + "fmt" + "os" + + fakeruntime "github.com/linuxsuren/go-fake-runtime" + "github.com/spf13/cobra" +) + +func createServiceCommand(execer fakeruntime.Execer) (c *cobra.Command) { + opt := &serviceOption{ + Execer: execer, + } + c = &cobra.Command{ + Use: "service", + Aliases: []string{"s"}, + Short: "Install atest as a Linux service", + PreRunE: opt.preRunE, + RunE: opt.runE, + } + flags := c.Flags() + flags.StringVarP(&opt.action, "action", "a", "", "The action of service, support actions: install, start, stop, restart, status") + flags.StringVarP(&opt.scriptPath, "script-path", "", "/lib/systemd/system/atest.service", "The service script file path") + return +} + +type serviceOption struct { + action string + scriptPath string + fakeruntime.Execer +} + +func (o *serviceOption) preRunE(c *cobra.Command, args []string) (err error) { + if o.Execer.OS() != "linux" { + err = fmt.Errorf("only support on Linux") + } + if o.action == "" && len(args) > 0 { + o.action = args[0] + } + return +} + +func (o *serviceOption) runE(c *cobra.Command, args []string) (err error) { + var output string + switch o.action { + case "install", "i": + err = os.WriteFile(o.scriptPath, []byte(script), os.ModeAppend) + case "start": + output, err = o.Execer.RunCommandAndReturn("systemctl", "", "start", "atest") + case "stop": + output, err = o.Execer.RunCommandAndReturn("systemctl", "", "stop", "atest") + case "restart": + output, err = o.Execer.RunCommandAndReturn("systemctl", "", "restart", "atest") + case "status": + output, err = o.Execer.RunCommandAndReturn("systemctl", "", "status", "atest") + default: + err = fmt.Errorf("not support action: '%s'", o.action) + } + + if output != "" { + c.Println(output) + } + return +} + +var script = `[Unit] +Description=API Testing + +[Service] +ExecStart=atest server + +[Install] +WantedBy=multi-user.target +` diff --git a/cmd/service_test.go b/cmd/service_test.go new file mode 100644 index 0000000..d70e0bb --- /dev/null +++ b/cmd/service_test.go @@ -0,0 +1,69 @@ +package cmd + +import ( + "bytes" + "os" + "testing" + + fakeruntime "github.com/linuxsuren/go-fake-runtime" + "github.com/stretchr/testify/assert" +) + +func TestService(t *testing.T) { + root := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}) + root.SetArgs([]string{"service", "fake"}) + err := root.Execute() + assert.NotNil(t, err) + + notLinux := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "fake"}) + notLinux.SetArgs([]string{"service", "--action", "install"}) + err = notLinux.Execute() + assert.NotNil(t, err) + + tmpFile, err := os.CreateTemp(os.TempDir(), "service") + assert.Nil(t, err) + defer func() { + os.RemoveAll(tmpFile.Name()) + }() + + targetScript := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}) + targetScript.SetArgs([]string{"service", "--action", "install", "--script-path", tmpFile.Name()}) + err = targetScript.Execute() + assert.Nil(t, err) + data, err := os.ReadFile(tmpFile.Name()) + assert.Nil(t, err) + assert.Equal(t, script, string(data)) + + tests := []struct { + name string + action string + expectOutput string + }{{ + name: "action: start", + action: "start", + expectOutput: "output1", + }, { + name: "action: stop", + action: "stop", + expectOutput: "output2", + }, { + name: "action: restart", + action: "restart", + expectOutput: "output3", + }, { + name: "action: status", + action: "status", + expectOutput: "output4", + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buf := new(bytes.Buffer) + normalRoot := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux", ExpectOutput: tt.expectOutput}) + normalRoot.SetOut(buf) + normalRoot.SetArgs([]string{"service", "--action", tt.action}) + err = normalRoot.Execute() + assert.Nil(t, err) + assert.Equal(t, tt.expectOutput+"\n", buf.String()) + }) + } +} diff --git a/go.mod b/go.mod index 9eace11..55bb91e 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( github.com/imdario/mergo v0.3.11 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/invopop/jsonschema v0.7.0 // indirect + github.com/linuxsuren/go-fake-runtime v0.0.0-20230413085645-15e77ab55dbd // indirect github.com/mitchellh/copystructure v1.0.0 // indirect github.com/mitchellh/reflectwalk v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/go.sum b/go.sum index 239cc20..65f6807 100644 --- a/go.sum +++ b/go.sum @@ -563,6 +563,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/linuxsuren/go-fake-runtime v0.0.0-20230413085645-15e77ab55dbd h1:2Avir30WOgcDqG3sA4hlW4bC4c/tgseAUntPhf5JQ6E= +github.com/linuxsuren/go-fake-runtime v0.0.0-20230413085645-15e77ab55dbd/go.mod h1:zmh6J78hSnWZo68faMA2eKOdaEp8eFbERHi3ZB9xHCQ= github.com/linuxsuren/unstructured v0.0.1 h1:ilUA8MUYbR6l9ebo/YPV2bKqlf62bzQursDSE+j00iU= github.com/linuxsuren/unstructured v0.0.1/go.mod h1:KH6aTj+FegzGBzc1vS6mzZx3/duhTUTEVyW5sO7p4as= github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= diff --git a/main.go b/main.go index b28e95d..8a5db53 100644 --- a/main.go +++ b/main.go @@ -4,10 +4,11 @@ import ( "os" "github.com/linuxsuren/api-testing/cmd" + exec "github.com/linuxsuren/go-fake-runtime" ) func main() { - c := cmd.NewRootCmd() + c := cmd.NewRootCmd(exec.DefaultExecer{}) if err := c.Execute(); err != nil { os.Exit(1) } diff --git a/pkg/runner/simple.go b/pkg/runner/simple.go index 3d874ef..efd9e64 100644 --- a/pkg/runner/simple.go +++ b/pkg/runner/simple.go @@ -17,8 +17,8 @@ import ( "github.com/andreyvit/diff" "github.com/antonmedv/expr" "github.com/antonmedv/expr/vm" - "github.com/linuxsuren/api-testing/pkg/exec" "github.com/linuxsuren/api-testing/pkg/testing" + fakeruntime "github.com/linuxsuren/go-fake-runtime" unstructured "github.com/linuxsuren/unstructured/pkg" "github.com/xeipuuv/gojsonschema" ) @@ -78,6 +78,7 @@ type TestCaseRunner interface { WithOutputWriter(io.Writer) TestCaseRunner WithWriteLevel(level string) TestCaseRunner WithTestReporter(TestReporter) TestCaseRunner + WithExecer(fakeruntime.Execer) TestCaseRunner } // ReportRecord represents the raw data of a HTTP request @@ -157,6 +158,7 @@ type simpleTestCaseRunner struct { testReporter TestReporter writer io.Writer log LevelWriter + execer fakeruntime.Execer } // NewSimpleTestCaseRunner creates the instance of the simple test case runner @@ -164,7 +166,8 @@ func NewSimpleTestCaseRunner() TestCaseRunner { runner := &simpleTestCaseRunner{} return runner.WithOutputWriter(io.Discard). WithWriteLevel("info"). - WithTestReporter(NewDiscardTestReporter()) + WithTestReporter(NewDiscardTestReporter()). + WithExecer(fakeruntime.DefaultExecer{}) } // RunTestCase is the main entry point of a test case @@ -179,16 +182,14 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte r.testReporter.PutRecord(rr) }(record) - if err = doPrepare(testcase); err != nil { + if err = r.doPrepare(testcase); err != nil { err = fmt.Errorf("failed to prepare, error: %v", err) return } defer func() { if testcase.Clean.CleanPrepare { - if err = doCleanPrepare(testcase); err != nil { - return - } + err = r.doCleanPrepare(testcase) } }() @@ -363,29 +364,29 @@ func (r *simpleTestCaseRunner) WithTestReporter(reporter TestReporter) TestCaseR return r } -// Deprecated -// RunTestCase runs the test case. -func RunTestCase(testcase *testing.TestCase, dataContext interface{}, ctx context.Context) (output interface{}, err error) { - return NewSimpleTestCaseRunner().WithOutputWriter(os.Stdout).RunTestCase(testcase, dataContext, ctx) +// WithExecer sets the execer +func (r *simpleTestCaseRunner) WithExecer(execer fakeruntime.Execer) TestCaseRunner { + r.execer = execer + return r } -func doPrepare(testcase *testing.TestCase) (err error) { +func (r *simpleTestCaseRunner) doPrepare(testcase *testing.TestCase) (err error) { for i := range testcase.Prepare.Kubernetes { item := testcase.Prepare.Kubernetes[i] - if err = exec.RunCommand("kubectl", "apply", "-f", item); err != nil { + if err = r.execer.RunCommand("kubectl", "apply", "-f", item); err != nil { return } } return } -func doCleanPrepare(testcase *testing.TestCase) (err error) { +func (r *simpleTestCaseRunner) doCleanPrepare(testcase *testing.TestCase) (err error) { count := len(testcase.Prepare.Kubernetes) for i := count - 1; i >= 0; i-- { item := testcase.Prepare.Kubernetes[i] - if err = exec.RunCommand("kubectl", "delete", "-f", item); err != nil { + if err = r.execer.RunCommand("kubectl", "delete", "-f", item); err != nil { return } } diff --git a/pkg/runner/simple_test.go b/pkg/runner/simple_test.go index 0eec2ca..cbfb282 100644 --- a/pkg/runner/simple_test.go +++ b/pkg/runner/simple_test.go @@ -5,23 +5,37 @@ import ( "context" "errors" "net/http" + "os" "testing" _ "embed" "github.com/h2non/gock" atest "github.com/linuxsuren/api-testing/pkg/testing" + fakeruntime "github.com/linuxsuren/go-fake-runtime" "github.com/stretchr/testify/assert" ) func TestTestCase(t *testing.T) { tests := []struct { name string + execer fakeruntime.Execer testCase *atest.TestCase ctx interface{} prepare func() verify func(t *testing.T, output interface{}, err error) }{{ + name: "failed during the prepare stage", + testCase: &atest.TestCase{ + Prepare: atest.Prepare{ + Kubernetes: []string{"demo.yaml"}, + }, + }, + execer: fakeruntime.FakeExecer{ExpectError: errors.New("fake")}, + verify: func(t *testing.T, output interface{}, err error) { + assert.NotNil(t, err) + }, + }, { name: "normal, response is map", testCase: &atest.TestCase{ Request: atest.Request{ @@ -44,7 +58,14 @@ func TestTestCase(t *testing.T) { `data.name == "linuxsuren"`, }, }, + Prepare: atest.Prepare{ + Kubernetes: []string{"demo.yaml"}, + }, + Clean: atest.Clean{ + CleanPrepare: true, + }, }, + execer: fakeruntime.FakeExecer{}, prepare: func() { gock.New("http://localhost"). Get("/foo"). @@ -370,7 +391,11 @@ func TestTestCase(t *testing.T) { if tt.prepare != nil { tt.prepare() } - output, err := RunTestCase(tt.testCase, tt.ctx, context.TODO()) + runner := NewSimpleTestCaseRunner().WithOutputWriter(os.Stdout) + if tt.execer != nil { + runner.WithExecer(tt.execer) + } + output, err := runner.RunTestCase(tt.testCase, tt.ctx, context.TODO()) tt.verify(t, output, err) }) } diff --git a/sample/manifest.yaml b/sample/manifest.yaml index c72ad47..3c4d3c8 100644 --- a/sample/manifest.yaml +++ b/sample/manifest.yaml @@ -37,9 +37,9 @@ metadata: spec: ports: - name: server - port: 9090 + port: 7070 protocol: TCP - targetPort: 9090 + targetPort: 7070 selector: app: api-testing sessionAffinity: None