diff --git a/cmd/register.go b/cmd/register.go index 6b179f0..ab0518b 100644 --- a/cmd/register.go +++ b/cmd/register.go @@ -33,7 +33,7 @@ var registerCmd = &cobra.Command{ goc register [flags] `, Run: func(cmd *cobra.Command, args []string) { - s := cover.Service{ + s := cover.ServiceUnderTest{ Name: name, Address: address, } diff --git a/cmd/run.go b/cmd/run.go index bf0bdeb..628293e 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -50,12 +50,16 @@ goc run . [--buildflags] [--exec] [--arguments] gocBuild.GoRunArguments = goRunArguments defer gocBuild.Clean() - // only save services in memory - cover.DefaultStore = cover.NewMemoryStore() + server := cover.NewMemoryBasedServer() // only save services in memory // start goc server var l = newLocalListener() - go cover.GocServer(ioutil.Discard).RunListener(l) + go func() { + err = server.Route(ioutil.Discard).RunListener(l) + if err != nil { + log.Fatalf("Start goc server failed: %v", err) + } + }() gocServer := fmt.Sprintf("http://%s", l.Addr().String()) fmt.Printf("[goc] goc server started: %s \n", gocServer) diff --git a/cmd/server.go b/cmd/server.go index f37db9c..36d5b5e 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -19,6 +19,7 @@ package cmd import ( "github.com/qiniu/goc/pkg/cover" "github.com/spf13/cobra" + "log" ) var serverCmd = &cobra.Command{ @@ -36,13 +37,18 @@ goc server --port=:8080 goc server --port=localhost:8080 `, Run: func(cmd *cobra.Command, args []string) { - cover.Run(port) + server, err := cover.NewFileBasedServer(localPersistence) + if err != nil { + log.Fatalf("New file based server failed, err: %v", err) + } + server.Run(port) }, } -var port string +var port, localPersistence string func init() { serverCmd.Flags().StringVarP(&port, "port", "", ":7777", "listen port to start a coverage host center") + serverCmd.Flags().StringVarP(&localPersistence, "local-persistence", "", "_svrs_address.txt", "the file to save services address information") rootCmd.AddCommand(serverCmd) } diff --git a/go.mod b/go.mod index a4b18ff..7135ab5 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,6 @@ require ( golang.org/x/net v0.0.0-20200625001655-4c5254603344 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d golang.org/x/tools v0.0.0-20200730221956-1ac65761fe2c - k8s.io/kubernetes v1.13.0 + k8s.io/kubernetes v1.13.0 // indirect k8s.io/test-infra v0.0.0-20200511080351-8ac9dbfab055 ) diff --git a/go.sum b/go.sum index 9881f5d..63fe015 100644 --- a/go.sum +++ b/go.sum @@ -464,6 +464,7 @@ github.com/klauspost/pgzip v1.2.1/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= diff --git a/pkg/cover/client.go b/pkg/cover/client.go index c45166f..55f2c08 100644 --- a/pkg/cover/client.go +++ b/pkg/cover/client.go @@ -36,7 +36,7 @@ type Action interface { Clear(param ProfileParam) ([]byte, error) InitSystem() ([]byte, error) ListServices() ([]byte, error) - RegisterService(svr Service) ([]byte, error) + RegisterService(svr ServiceUnderTest) ([]byte, error) } const ( @@ -69,7 +69,7 @@ func NewWorker(host string) Action { } } -func (c *client) RegisterService(srv Service) ([]byte, error) { +func (c *client) RegisterService(srv ServiceUnderTest) ([]byte, error) { if _, err := url.ParseRequestURI(srv.Address); err != nil { return nil, err } diff --git a/pkg/cover/client_test.go b/pkg/cover/client_test.go index 6d812cb..077acba 100644 --- a/pkg/cover/client_test.go +++ b/pkg/cover/client_test.go @@ -28,7 +28,9 @@ import ( func TestClientAction(t *testing.T) { // mock goc server - ts := httptest.NewServer(GocServer(os.Stdout)) + server, err := NewFileBasedServer("_svrs_address.txt") + assert.NoError(t, err) + ts := httptest.NewServer(server.Route(os.Stdout)) defer ts.Close() var client = NewWorker(ts.URL) @@ -46,15 +48,15 @@ func TestClientAction(t *testing.T) { })) defer profileErrMockSvr.Close() - // regsiter service into goc server - var src Service + // register service into goc server + var src ServiceUnderTest src.Name = "serviceSuccess" src.Address = profileSuccessMockSvr.URL res, err := client.RegisterService(src) assert.NoError(t, err) assert.Contains(t, string(res), "success") - // do list and check service + // do list and check server res, err = client.ListServices() assert.NoError(t, err) assert.Contains(t, string(res), src.Address) @@ -63,64 +65,64 @@ func TestClientAction(t *testing.T) { // get profile from goc server tcs := []struct { name string - service Service + service ServiceUnderTest param ProfileParam expected string expectedErr bool }{ { - name: "both service and address existed", - service: Service{Name: "serviceOK", Address: profileSuccessMockSvr.URL}, + name: "both server and address existed", + service: ServiceUnderTest{Name: "serviceOK", Address: profileSuccessMockSvr.URL}, param: ProfileParam{Force: false, Service: []string{"serviceOK"}, Address: []string{profileSuccessMockSvr.URL}}, expectedErr: true, }, { - name: "valid test with no service flag provide", - service: Service{Name: "serviceOK", Address: profileSuccessMockSvr.URL}, + name: "valid test with no server flag provide", + service: ServiceUnderTest{Name: "serviceOK", Address: profileSuccessMockSvr.URL}, param: ProfileParam{}, expected: "mockService/main.go:30.13,48.33 13 1", }, { - name: "valid test with service flag provide", - service: Service{Name: "serviceOK", Address: profileSuccessMockSvr.URL}, + name: "valid test with server flag provide", + service: ServiceUnderTest{Name: "serviceOK", Address: profileSuccessMockSvr.URL}, param: ProfileParam{Service: []string{"serviceOK"}}, expected: "mockService/main.go:30.13,48.33 13 1", }, { name: "valid test with address flag provide", - service: Service{Name: "serviceOK", Address: profileSuccessMockSvr.URL}, + service: ServiceUnderTest{Name: "serviceOK", Address: profileSuccessMockSvr.URL}, param: ProfileParam{Address: []string{profileSuccessMockSvr.URL}}, expected: "mockService/main.go:30.13,48.33 13 1", }, { - name: "invalid test with invalid service flag provide", - service: Service{Name: "serviceOK", Address: profileSuccessMockSvr.URL}, + name: "invalid test with invalid server flag provide", + service: ServiceUnderTest{Name: "serviceOK", Address: profileSuccessMockSvr.URL}, param: ProfileParam{Service: []string{"unknown"}}, - expected: "service [unknown] not found", + expected: "server [unknown] not found", expectedErr: true, }, { - name: "invalid test with invalid profile got by service", - service: Service{Name: "serviceErr", Address: profileErrMockSvr.URL}, + name: "invalid test with invalid profile got by server", + service: ServiceUnderTest{Name: "serviceErr", Address: profileErrMockSvr.URL}, expected: "bad mode line: error", expectedErr: true, }, { - name: "invalid test with disconnected service", - service: Service{Name: "serviceNotExist", Address: "http://172.0.0.2:7777"}, + name: "invalid test with disconnected server", + service: ServiceUnderTest{Name: "serviceNotExist", Address: "http://172.0.0.2:7777"}, expected: "connection refused", expectedErr: true, }, { name: "invalid test with empty profile", - service: Service{Name: "serviceNotExist", Address: "http://172.0.0.2:7777"}, + service: ServiceUnderTest{Name: "serviceNotExist", Address: "http://172.0.0.2:7777"}, param: ProfileParam{Force: true}, expectedErr: true, expected: "no profiles", }, { name: "valid test with coverfile flag provide", - service: Service{Name: "serviceOK", Address: profileSuccessMockSvr.URL}, + service: ServiceUnderTest{Name: "serviceOK", Address: profileSuccessMockSvr.URL}, param: ProfileParam{CoverFilePatterns: []string{"b.go$"}}, expected: "b/b.go", }, @@ -150,7 +152,7 @@ func TestClientAction(t *testing.T) { }) } - // init system and check service again + // init system and check server again _, err = client.InitSystem() assert.NoError(t, err) res, err = client.ListServices() @@ -162,7 +164,7 @@ func TestClientRegisterService(t *testing.T) { c := &client{} // client register with empty address - testService1 := Service{ + testService1 := ServiceUnderTest{ Address: "", Name: "abc", } @@ -170,7 +172,7 @@ func TestClientRegisterService(t *testing.T) { assert.Contains(t, err.Error(), "empty url") // client register with empty name - testService2 := Service{ + testService2 := ServiceUnderTest{ Address: "http://127.0.0.1:444", Name: "", } diff --git a/pkg/cover/server.go b/pkg/cover/server.go index b923d80..0ad07d9 100644 --- a/pkg/cover/server.go +++ b/pkg/cover/server.go @@ -33,18 +33,35 @@ import ( "k8s.io/test-infra/gopherage/pkg/cov" ) -// DefaultStore implements the IPersistence interface -var DefaultStore Store - // LogFile a file to save log. const LogFile = "goc.log" -func init() { - DefaultStore = NewFileStore() +type server struct { + PersistenceFile string + Store Store +} + +// NewFileBasedServer new a file based server with persistenceFile +func NewFileBasedServer(persistenceFile string) (*server, error) { + store, err := NewFileStore(persistenceFile) + if err != nil { + return nil, err + } + return &server{ + PersistenceFile: persistenceFile, + Store: store, + }, nil +} + +// NewMemoryBasedServer new a memory based server without persistenceFile +func NewMemoryBasedServer() *server { + return &server{ + Store: NewMemoryStore(), + } } // Run starts coverage host center -func Run(port string) { +func (s *server) Run(port string) { f, err := os.Create(LogFile) if err != nil { log.Fatalf("failed to create log file %s, err: %v", LogFile, err) @@ -52,34 +69,34 @@ func Run(port string) { // both log to stdout and file by default mw := io.MultiWriter(f, os.Stdout) - r := GocServer(mw) + r := s.Route(mw) log.Fatal(r.Run(port)) } -// GocServer init goc server engine -func GocServer(w io.Writer) *gin.Engine { +// Router init goc server engine +func (s *server) Route(w io.Writer) *gin.Engine { if w != nil { gin.DefaultWriter = w } r := gin.Default() // api to show the registered services - r.StaticFile(PersistenceFile, "./"+PersistenceFile) + r.StaticFile("static", "./"+s.PersistenceFile) v1 := r.Group("/v1") { - v1.POST("/cover/register", registerService) - v1.GET("/cover/profile", profile) - v1.POST("/cover/profile", profile) - v1.POST("/cover/clear", clear) - v1.POST("/cover/init", initSystem) - v1.GET("/cover/list", listServices) + v1.POST("/cover/register", s.registerService) + v1.GET("/cover/profile", s.profile) + v1.POST("/cover/profile", s.profile) + v1.POST("/cover/clear", s.clear) + v1.POST("/cover/init", s.initSystem) + v1.GET("/cover/list", s.listServices) } return r } -// Service is a entry under being tested -type Service struct { +// ServiceUnderTest is a entry under being tested +type ServiceUnderTest struct { Name string `form:"name" json:"name" binding:"required"` Address string `form:"address" json:"address" binding:"required"` } @@ -93,13 +110,13 @@ type ProfileParam struct { } //listServices list all the registered services -func listServices(c *gin.Context) { - services := DefaultStore.GetAll() +func (s *server) listServices(c *gin.Context) { + services := s.Store.GetAll() c.JSON(http.StatusOK, services) } -func registerService(c *gin.Context) { - var service Service +func (s *server) registerService(c *gin.Context) { + var service ServiceUnderTest if err := c.ShouldBind(&service); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return @@ -122,9 +139,9 @@ func registerService(c *gin.Context) { service.Address = fmt.Sprintf("http://%s:%s", realIP, port) } - address := DefaultStore.Get(service.Name) + address := s.Store.Get(service.Name) if !contains(address, service.Address) { - if err := DefaultStore.Add(service); err != nil { + if err := s.Store.Add(service); err != nil && err != ErrServiceAlreadyRegistered { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } @@ -137,14 +154,14 @@ func registerService(c *gin.Context) { // profile API examples: // POST /v1/cover/profile // { "force": "true", "service":["a","b"], "address":["c","d"],"coverfile":["e","f"] } -func profile(c *gin.Context) { +func (s *server) profile(c *gin.Context) { var body ProfileParam if err := c.ShouldBind(&body); err != nil { c.JSON(http.StatusExpectationFailed, gin.H{"error": err.Error()}) return } - allInfos := DefaultStore.GetAll() + allInfos := s.Store.GetAll() filterAddrList, err := filterAddrs(body.Service, body.Address, body.Force, allInfos) if err != nil { c.JSON(http.StatusExpectationFailed, gin.H{"error": err.Error()}) @@ -216,13 +233,13 @@ func filterProfile(coverFile []string, profiles []*cover.Profile) ([]*cover.Prof return out, nil } -func clear(c *gin.Context) { +func (s *server) clear(c *gin.Context) { var body ProfileParam if err := c.ShouldBind(&body); err != nil { c.JSON(http.StatusExpectationFailed, gin.H{"error": err.Error()}) return } - svrsUnderTest := DefaultStore.GetAll() + svrsUnderTest := s.Store.GetAll() filterAddrList, err := filterAddrs(body.Service, body.Address, true, svrsUnderTest) if err != nil { c.JSON(http.StatusExpectationFailed, gin.H{"error": err.Error()}) @@ -239,8 +256,8 @@ func clear(c *gin.Context) { } -func initSystem(c *gin.Context) { - if err := DefaultStore.Init(); err != nil { +func (s *server) initSystem(c *gin.Context) { + if err := s.Store.Init(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } @@ -313,6 +330,6 @@ func filterAddrs(serviceList, addressList []string, force bool, allInfos map[str filterAddrList = addressAll } - // Return all servers when all param is nil + // Return all services when all param is nil return filterAddrList, nil } diff --git a/pkg/cover/server_test.go b/pkg/cover/server_test.go index fb7feb7..c144911 100644 --- a/pkg/cover/server_test.go +++ b/pkg/cover/server_test.go @@ -22,7 +22,7 @@ type MockStore struct { mock.Mock } -func (m *MockStore) Add(s Service) error { +func (m *MockStore) Add(s ServiceUnderTest) error { args := m.Called(s) return args.Error(0) } @@ -113,7 +113,9 @@ func TestFilterAddrs(t *testing.T) { } func TestRegisterService(t *testing.T) { - router := GocServer(os.Stdout) + server, err := NewFileBasedServer("_svrs_address.txt") + assert.NoError(t, err) + router := server.Route(os.Stdout) // register with empty service struct w := httptest.NewRecorder() @@ -147,7 +149,7 @@ func TestRegisterService(t *testing.T) { assert.Contains(t, w.Body.String(), "missing port in address") // register with store failure - expectedS := Service{ + expectedS := ServiceUnderTest{ Name: "foo", Address: "http://:64444", // the real IP is empty in unittest, so server will get a empty one } @@ -155,7 +157,7 @@ func TestRegisterService(t *testing.T) { testObj.On("Get", "foo").Return([]string{"http://127.0.0.1:66666"}) testObj.On("Add", expectedS).Return(fmt.Errorf("lala error")) - DefaultStore = testObj + server.Store = testObj w = httptest.NewRecorder() data.Set("name", expectedS.Name) @@ -169,7 +171,9 @@ func TestRegisterService(t *testing.T) { } func TestProfileService(t *testing.T) { - router := GocServer(os.Stdout) + server, err := NewFileBasedServer("_svrs_address.txt") + assert.NoError(t, err) + router := server.Route(os.Stdout) // get profile with invalid force parameter w := httptest.NewRecorder() @@ -184,9 +188,10 @@ func TestClearService(t *testing.T) { testObj := new(MockStore) testObj.On("GetAll").Return(map[string][]string{"foo": {"http://127.0.0.1:66666"}}) - DefaultStore = testObj - - router := GocServer(os.Stdout) + server := &server{ + Store: testObj, + } + router := server.Route(os.Stdout) // clear profile with non-exist port w := httptest.NewRecorder() @@ -224,9 +229,10 @@ func TestInitService(t *testing.T) { testObj := new(MockStore) testObj.On("Init").Return(fmt.Errorf("lala error")) - DefaultStore = testObj - - router := GocServer(os.Stdout) + server := &server{ + Store: testObj, + } + router := server.Route(os.Stdout) // get profile with invalid force parameter w := httptest.NewRecorder() diff --git a/pkg/cover/store.go b/pkg/cover/store.go index a18aa9a..f36520d 100644 --- a/pkg/cover/store.go +++ b/pkg/cover/store.go @@ -18,18 +18,21 @@ package cover import ( "bufio" + "errors" "fmt" + log "github.com/sirupsen/logrus" "os" + "path/filepath" "strings" "sync" - - log "github.com/sirupsen/logrus" ) +var ErrServiceAlreadyRegistered = errors.New("service already registered") + // Store persistents the registered service information type Store interface { // Add adds the given service to store - Add(s Service) error + Add(s ServiceUnderTest) error // Get returns the registered service information with the given service's name Get(name string) []string @@ -44,9 +47,6 @@ type Store interface { Set(services map[string][]string) } -// PersistenceFile is the file to save services address information -const PersistenceFile = "_svrs_address.txt" - // fileStore holds the registered services into memory and persistent to a local file type fileStore struct { mu sync.RWMutex @@ -56,9 +56,17 @@ type fileStore struct { } // NewFileStore creates a store using local file -func NewFileStore() Store { +func NewFileStore(persistenceFile string) (store Store, err error) { + path, err := filepath.Abs(persistenceFile) + if err != nil { + return nil, err + } + err = os.MkdirAll(filepath.Dir(path), os.ModePerm) + if err != nil { + return nil, err + } l := &fileStore{ - persistentFile: PersistenceFile, + persistentFile: path, memoryStore: NewMemoryStore(), } @@ -66,12 +74,14 @@ func NewFileStore() Store { log.Fatalf("load failed, file: %s, err: %v", l.persistentFile, err) } - return l + return l, nil } // Add adds the given service to file Store -func (l *fileStore) Add(s Service) error { - l.memoryStore.Add(s) +func (l *fileStore) Add(s ServiceUnderTest) error { + if err := l.memoryStore.Add(s); err != nil { + return err + } // persistent to local store l.mu.Lock() @@ -147,7 +157,7 @@ func (l *fileStore) Set(services map[string][]string) { panic("TO BE IMPLEMENTED") } -func (l *fileStore) appendToFile(s Service) error { +func (l *fileStore) appendToFile(s ServiceUnderTest) error { f, err := os.OpenFile(l.persistentFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) if err != nil { return err @@ -163,7 +173,7 @@ func (l *fileStore) appendToFile(s Service) error { return nil } -func format(s Service) string { +func format(s ServiceUnderTest) string { return fmt.Sprintf("%s&%s", s.Name, s.Address) } @@ -185,7 +195,7 @@ func NewMemoryStore() Store { } // Add adds the given service to MemoryStore -func (l *memoryStore) Add(s Service) error { +func (l *memoryStore) Add(s ServiceUnderTest) error { l.mu.Lock() defer l.mu.Unlock() // load to memory @@ -193,7 +203,7 @@ func (l *memoryStore) Add(s Service) error { for _, addr := range addrs { if addr == s.Address { log.Printf("service registered already, name: %s, address: %s", s.Name, s.Address) - return nil + return ErrServiceAlreadyRegistered } } addrs = append(addrs, s.Address) diff --git a/pkg/cover/store_test.go b/pkg/cover/store_test.go index f4bd8aa..6d5d076 100644 --- a/pkg/cover/store_test.go +++ b/pkg/cover/store_test.go @@ -17,31 +17,34 @@ package cover import ( + "github.com/stretchr/testify/assert" "testing" ) func TestLocalStore(t *testing.T) { - localStore := NewFileStore() - var tc1 = Service{ + localStore, err := NewFileStore("_svrs_address.txt") + assert.NoError(t, err) + var tc1 = ServiceUnderTest{ Name: "a", Address: "http://127.0.0.1", } - var tc2 = Service{ + var tc2 = ServiceUnderTest{ Name: "b", Address: "http://127.0.0.2", } - var tc3 = Service{ + var tc3 = ServiceUnderTest{ Name: "c", Address: "http://127.0.0.3", } - var tc4 = Service{ + var tc4 = ServiceUnderTest{ Name: "a", Address: "http://127.0.0.4", } - localStore.Add(tc1) - localStore.Add(tc2) - localStore.Add(tc3) - localStore.Add(tc4) + assert.NoError(t, localStore.Add(tc1)) + assert.Equal(t, localStore.Add(tc1), ErrServiceAlreadyRegistered) + assert.NoError(t, localStore.Add(tc2)) + assert.NoError(t, localStore.Add(tc3)) + assert.NoError(t, localStore.Add(tc4)) addrs := localStore.Get(tc1.Name) if len(addrs) != 2 { t.Error("unexpected result") @@ -57,6 +60,10 @@ func TestLocalStore(t *testing.T) { t.Error("local store check failed") } + localStoreNew, err := NewFileStore("_svrs_address.txt") + assert.NoError(t, err) + assert.Equal(t, localStore.GetAll(), localStoreNew.GetAll()) + localStore.Init() if len(localStore.GetAll()) != 0 { t.Error("local store init failed") diff --git a/tests/server.bats b/tests/server.bats index 371cee5..ed9719e 100755 --- a/tests/server.bats +++ b/tests/server.bats @@ -22,7 +22,7 @@ setup_file() { sleep 2 goc init # run covered goc - gocc server --port=:60001 --debug 3>&- & + gocc server --port=:60001 --local-persistence="persistence/servicesAll.txt" --debug 3>&- & GOCC_PID=$! sleep 2 info "goc gocc server started" @@ -57,4 +57,11 @@ teardown_file() { # connect to covered goc run goc profile --center=http://127.0.0.1:60001 [ "$status" -eq 0 ] + + # verify the persistence file exist + [ -f "$WORKDIR/persistence/servicesAll.txt" ] + # remove goc persistence file + run goc init --center=http://127.0.0.1:60001 + [ "$status" -eq 0 ] + [ ! -f "$WORKDIR/persistence/servicesAll.txt" ] } \ No newline at end of file