feat: add a service commnd (#39)

* feat: add a service commnd

The following command will install atest as a Linux service. `atest service --action install`.

Set the default service port to be 7070 to avoid confliction

* add unit tests

* add more unit tests
This commit is contained in:
Rick 2023-04-14 09:32:24 +08:00 committed by GitHub
parent 9ee38ffc17
commit e08c2046d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 248 additions and 36 deletions

View File

@ -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
}
}

View File

@ -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)

View File

@ -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
}

View File

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

View File

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

View File

@ -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)

View File

@ -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{}) {
}

View File

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

76
cmd/service.go Normal file
View File

@ -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
`

69
cmd/service_test.go Normal file
View File

@ -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())
})
}
}

1
go.mod
View File

@ -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

2
go.sum
View File

@ -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=

View File

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

View File

@ -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
}
}

View File

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

View File

@ -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