diff --git a/cmd/build.go b/cmd/build.go index 076c2a5..1791313 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -43,14 +43,18 @@ goc build -- -o /to/this/path goc build -- -ldflags "-extldflags -static" -tags="embed kodo" `, Run: func(cmd *cobra.Command, args []string) { - gocbuild := build.NewBuild(buildFlags, packages, buildOutput) + gocBuild := build.NewBuild(buildFlags, packages, buildOutput) // remove temporary directory if needed - defer gocbuild.RemoveTmpDir() + defer func() { + if !debugGoc { + gocBuild.Clean() + } + }() // doCover with original buildFlags, with new GOPATH( tmp:original ) // in the tmp directory - doCover(buildFlags, gocbuild.NewGOPATH, gocbuild.TmpDir) + doCover(buildFlags, gocBuild.NewGOPATH, gocBuild.TmpDir) // do install in the temporary directory - gocbuild.Build() + gocBuild.Build() return }, } diff --git a/cmd/clear.go b/cmd/clear.go index 8e1bc93..65cfa6e 100644 --- a/cmd/clear.go +++ b/cmd/clear.go @@ -37,7 +37,7 @@ goc clear goc clear --center=http://192.168.1.1:8080 `, Run: func(cmd *cobra.Command, args []string) { - res, err := cover.NewWorker().Clear(center) + res, err := cover.NewWorker(center).Clear() if err != nil { log.Fatalf("call host %v failed, err: %v, response: %v", center, err, string(res)) } diff --git a/cmd/init.go b/cmd/init.go index cd7e0f0..c489f80 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -27,7 +27,7 @@ var initCmd = &cobra.Command{ Use: "init", Short: "Clear the register information in order to start a new round of tests", Run: func(cmd *cobra.Command, args []string) { - if res, err := cover.NewWorker().InitSystem(center); err != nil { + if res, err := cover.NewWorker(center).InitSystem(); err != nil { log.Fatalf("call host %v failed, err: %v, response: %v", center, err, string(res)) } }, diff --git a/cmd/install.go b/cmd/install.go index d30d0a7..08659e2 100644 --- a/cmd/install.go +++ b/cmd/install.go @@ -27,7 +27,7 @@ var installCmd = &cobra.Command{ Long: ` First of all, this install command will copy the project code and its necessary dependencies to a temporary directory, then do cover for the target in this temporary directory, finally go install command will be executed and binaries generated to their original place. -To pass origial go build flags to goc command, place them after "--", see examples below for reference. +To pass original go build flags to goc command, place them after "--", see examples below for reference. `, Example: ` # Install all binaries with cover variables injected. The binary will be installed in $GOPATH/bin or $HOME/go/bin if directory existed. @@ -40,14 +40,18 @@ goc install --center=http://127.0.0.1:7777 goc build --buildflags="-ldflags '-extldflags -static' -tags='embed kodo'" `, Run: func(cmd *cobra.Command, args []string) { - gocbuild := build.NewInstall(buildFlags, packages) + gocBuild := build.NewInstall(buildFlags, packages) // remove temporary directory if needed - defer gocbuild.RemoveTmpDir() + defer func() { + if !debugGoc { + gocBuild.Clean() + } + }() // doCover with original buildFlags, with new GOPATH( tmp:original ) // in the tmp directory - doCover(buildFlags, gocbuild.NewGOPATH, gocbuild.TmpDir) + doCover(buildFlags, gocBuild.NewGOPATH, gocBuild.TmpDir) // do install in the temporary directory - gocbuild.Install() + gocBuild.Install() }, } diff --git a/cmd/list.go b/cmd/list.go new file mode 100644 index 0000000..194d486 --- /dev/null +++ b/cmd/list.go @@ -0,0 +1,47 @@ +/* + Copyright 2020 Qiniu Cloud (qiniu.com) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package cmd + +import ( + "fmt" + "log" + "os" + + "github.com/qiniu/goc/pkg/cover" + "github.com/spf13/cobra" +) + +var listCmd = &cobra.Command{ + Use: "list", + Short: "Lists all the registered services", + Long: "Lists all the registered services", + Example: ` +goc list [flags] +`, + Run: func(cmd *cobra.Command, args []string) { + res, err := cover.NewWorker(center).ListServices() + if err != nil { + log.Fatalf("list failed, err: %v", err) + } + fmt.Fprint(os.Stdout, string(res)) + }, +} + +func init() { + listCmd.Flags().StringVarP(¢er, "center", "", "http://127.0.0.1:7777", "cover profile host center") + rootCmd.AddCommand(listCmd) +} diff --git a/cmd/profile.go b/cmd/profile.go index c1d0918..18ae1c6 100644 --- a/cmd/profile.go +++ b/cmd/profile.go @@ -45,7 +45,7 @@ goc profile --center=http://192.168.1.1:8080 -o ./coverage.cov goc profile --center=http://192.168.1.1:8080 --output=./coverage.cov `, Run: func(cmd *cobra.Command, args []string) { - res, err := cover.NewWorker().Profile(center) + res, err := cover.NewWorker(center).Profile() if err != nil { log.Fatalf("call host %v failed, err: %v, response: %v", center, err, string(res)) } diff --git a/cmd/register.go b/cmd/register.go new file mode 100644 index 0000000..6b179f0 --- /dev/null +++ b/cmd/register.go @@ -0,0 +1,60 @@ +/* + Copyright 2020 Qiniu Cloud (qiniu.com) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package cmd + +import ( + "fmt" + "log" + "os" + + "github.com/qiniu/goc/pkg/cover" + "github.com/spf13/cobra" +) + +var registerCmd = &cobra.Command{ + Use: "register", + Short: "Register a service into service center", + Long: "Register a service into service center", + Example: ` +goc register [flags] +`, + Run: func(cmd *cobra.Command, args []string) { + s := cover.Service{ + Name: name, + Address: address, + } + res, err := cover.NewWorker(center).RegisterService(s) + if err != nil { + log.Fatalf("register service failed, err: %v", err) + } + fmt.Fprint(os.Stdout, string(res)) + }, +} + +var ( + name string + address string +) + +func init() { + registerCmd.Flags().StringVarP(¢er, "center", "", "http://127.0.0.1:7777", "cover profile host center") + registerCmd.Flags().StringVarP(&name, "name", "n", "", "service name") + registerCmd.Flags().StringVarP(&address, "address", "a", "", "service address") + registerCmd.MarkFlagRequired("name") + registerCmd.MarkFlagRequired("address") + rootCmd.AddCommand(registerCmd) +} diff --git a/cmd/root.go b/cmd/root.go index 3974e4c..b0d50d1 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -17,13 +17,14 @@ package cmd import ( - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - "github.com/spf13/viper" "io/ioutil" "path/filepath" "runtime" "strconv" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" ) var rootCmd = &cobra.Command{ @@ -54,8 +55,7 @@ Find more information at: } func init() { - rootCmd.PersistentFlags().BoolVar(&debugGoc, "debuggoc", false, "turn goc into debug mode") - rootCmd.PersistentFlags().MarkHidden("debuggoc") + rootCmd.PersistentFlags().BoolVar(&debugGoc, "debug", false, "run goc in debug mode") viper.BindPFlags(rootCmd.PersistentFlags()) } diff --git a/cmd/server.go b/cmd/server.go index 16e0d33..f37db9c 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -36,7 +36,7 @@ goc server --port=:8080 goc server --port=localhost:8080 `, Run: func(cmd *cobra.Command, args []string) { - cover.StartServer(port) + cover.Run(port) }, } diff --git a/pkg/build/build.go b/pkg/build/build.go index d40ea88..854461b 100644 --- a/pkg/build/build.go +++ b/pkg/build/build.go @@ -87,7 +87,7 @@ func (b *Build) determineOutputDir(outputDir string) string { if err != nil { log.Fatalf("Cannot get current working directory, the err: %v.", err) } - // if + if outputDir == "" { _, last := filepath.Split(curWorkingDir) if b.IsMod { diff --git a/pkg/build/tmpfolder.go b/pkg/build/tmpfolder.go index b4b5a6a..aff30bd 100644 --- a/pkg/build/tmpfolder.go +++ b/pkg/build/tmpfolder.go @@ -19,12 +19,12 @@ package build import ( "crypto/sha256" "fmt" - log "github.com/sirupsen/logrus" - "github.com/spf13/viper" "os" "path/filepath" "strings" + log "github.com/sirupsen/logrus" + "github.com/qiniu/goc/pkg/cover" ) @@ -57,7 +57,7 @@ func (b *Build) MvProjectsToTmp() { func (b *Build) mvProjectsToTmp() { path, err := os.Getwd() if err != nil { - log.Fatalf("Cannot get current working directoy, the error is: %v", err) + log.Fatalf("Cannot get current working directory, the error is: %v", err) } b.TmpDir = filepath.Join(os.TempDir(), TmpFolderName(path)) @@ -70,7 +70,7 @@ func (b *Build) mvProjectsToTmp() { } log.Printf("Tmp project generated in: %v", b.TmpDir) - // set Build.IsMod flag, so we dont have to call checkIfLegacyProject another time + // set Build.IsMod flag, so we don't have to call checkIfLegacyProject another time if b.checkIfLegacyProject() { b.cpLegacyProject() } else { @@ -154,11 +154,7 @@ func (b *Build) findWhereToInstall() string { return filepath.Join(os.Getenv("HOME"), "go", "bin") } -func (b *Build) RemoveTmpDir() { - debuggoc := viper.GetBool("debuggoc") - if debuggoc == false { - if b.TmpDir != "" { - os.RemoveAll(b.TmpDir) - } - } +// Clean clears up the temporary workspace +func (b *Build) Clean() error { + return os.RemoveAll(b.TmpDir) } diff --git a/pkg/cover/client.go b/pkg/cover/client.go index a1415e4..2eb1cf0 100644 --- a/pkg/cover/client.go +++ b/pkg/cover/client.go @@ -20,67 +20,104 @@ import ( "fmt" "io" "io/ioutil" + "log" "net" "net/http" + "net/url" + "strings" ) -// Action provides methods to contact with the coverd service under test +// Action provides methods to contact with the covered service under test type Action interface { - Profile(host string) ([]byte, error) - Clear(host string) ([]byte, error) - InitSystem(host string) ([]byte, error) + Profile() ([]byte, error) + Clear() ([]byte, error) + InitSystem() ([]byte, error) + ListServices() ([]byte, error) + RegisterService(svr Service) ([]byte, error) } -// CoverProfileAPI is provided by the covered service to get profiles -const CoverProfileAPI = "/v1/cover/profile" - -// CoverProfileClearAPI is provided by the covered service to clear profiles -const CoverProfileClearAPI = "/v1/cover/clear" - -// CoverInitSystemAPI prepare a new round of testing -const CoverInitSystemAPI = "/v1/cover/init" +const ( + //CoverInitSystemAPI prepare a new round of testing + CoverInitSystemAPI = "/v1/cover/init" + //CoverProfileAPI is provided by the covered service to get profiles + CoverProfileAPI = "/v1/cover/profile" + //CoverProfileClearAPI is provided by the covered service to clear profiles + CoverProfileClearAPI = "/v1/cover/clear" + //CoverServicesListAPI list all the registered services + CoverServicesListAPI = "/v1/cover/list" + //CoverRegisterServiceAPI register a service into service center + CoverRegisterServiceAPI = "/v1/cover/register" +) type client struct { + Host string client *http.Client } // NewWorker creates a worker to contact with service -func NewWorker() Action { +func NewWorker(host string) Action { + _, err := url.ParseRequestURI(host) + if err != nil { + log.Fatalf("Parse url %s failed, err: %v", host, err) + } return &client{ + Host: host, client: http.DefaultClient, } } -func (w *client) Profile(host string) ([]byte, error) { - u := fmt.Sprintf("%s%s", host, CoverProfileAPI) - profile, err := w.do("GET", u, nil) +func (c *client) RegisterService(srv Service) ([]byte, error) { + if _, err := url.ParseRequestURI(srv.Address); err != nil { + return nil, err + } + if strings.TrimSpace(srv.Name) == "" { + return nil, fmt.Errorf("invalid service name") + } + u := fmt.Sprintf("%s%s?name=%s&address=%s", c.Host, CoverRegisterServiceAPI, srv.Name, srv.Address) + res, err := c.do("POST", u, nil) + return res, err +} + +func (c *client) ListServices() ([]byte, error) { + u := fmt.Sprintf("%s%s", c.Host, CoverServicesListAPI) + services, err := c.do("GET", u, nil) if err != nil && isNetworkError(err) { - profile, err = w.do("GET", u, nil) + services, err = c.do("GET", u, nil) + } + + return services, err +} + +func (c *client) Profile() ([]byte, error) { + u := fmt.Sprintf("%s%s", c.Host, CoverProfileAPI) + profile, err := c.do("GET", u, nil) + if err != nil && isNetworkError(err) { + profile, err = c.do("GET", u, nil) } return profile, err } -func (w *client) Clear(host string) ([]byte, error) { - u := fmt.Sprintf("%s%s", host, CoverProfileClearAPI) - resp, err := w.do("POST", u, nil) +func (c *client) Clear() ([]byte, error) { + u := fmt.Sprintf("%s%s", c.Host, CoverProfileClearAPI) + resp, err := c.do("POST", u, nil) if err != nil && isNetworkError(err) { - resp, err = w.do("POST", u, nil) + resp, err = c.do("POST", u, nil) } return resp, err } -func (w *client) InitSystem(host string) ([]byte, error) { - u := fmt.Sprintf("%s%s", host, CoverInitSystemAPI) - return w.do("POST", u, nil) +func (c *client) InitSystem() ([]byte, error) { + u := fmt.Sprintf("%s%s", c.Host, CoverInitSystemAPI) + return c.do("POST", u, nil) } -func (w *client) do(method, url string, body io.Reader) ([]byte, error) { +func (c *client) do(method, url string, body io.Reader) ([]byte, error) { req, err := http.NewRequest(method, url, body) if err != nil { return nil, err } - res, err := w.client.Do(req) + res, err := c.client.Do(req) if err != nil { return nil, err } diff --git a/pkg/cover/client_test.go b/pkg/cover/client_test.go new file mode 100644 index 0000000..273355f --- /dev/null +++ b/pkg/cover/client_test.go @@ -0,0 +1,59 @@ +/* + Copyright 2020 Qiniu Cloud (qiniu.com) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package cover + +import ( + "net/http/httptest" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestClientAction(t *testing.T) { + ts := httptest.NewServer(GocServer(os.Stdout)) + defer ts.Close() + var client = NewWorker(ts.URL) + + // regsiter service into goc server + var src Service + src.Name = "goc" + src.Address = "http://127.0.0.1:7777" + res, err := client.RegisterService(src) + assert.NoError(t, err) + assert.Contains(t, string(res), "success") + + // do list and check service + res, err = client.ListServices() + assert.NoError(t, err) + assert.Contains(t, string(res), src.Address) + assert.Contains(t, string(res), src.Name) + + // init system and check service again + res, err = client.InitSystem() + assert.NoError(t, err) + res, err = client.ListServices() + assert.NoError(t, err) + assert.Equal(t, "{}", string(res)) +} + +func TestE2E(t *testing.T) { + // FIXME: start goc server + // FIXME: call goc build to cover goc server + // FIXME: do some tests again goc server + // FIXME: goc profile and checkout coverage +} diff --git a/pkg/cover/cover.go b/pkg/cover/cover.go index d06b558..3e8db5b 100644 --- a/pkg/cover/cover.go +++ b/pkg/cover/cover.go @@ -58,7 +58,7 @@ type FileVar struct { } // Package map a package output by go list -// this is subset of pakcage struct in: https://github.com/golang/go/blob/master/src/cmd/go/internal/load/pkg.go#L58 +// this is subset of package struct in: https://github.com/golang/go/blob/master/src/cmd/go/internal/load/pkg.go#L58 type Package struct { Dir string `json:"Dir"` // directory containing package sources ImportPath string `json:"ImportPath"` // import path of package in dir @@ -157,7 +157,7 @@ func AddCounters(pkg *Package, mode, newgopath string) (*PackageCover, error) { cmd := buildCoverCmd(file, coverVar, pkg, mode, newgopath) out, err := cmd.CombinedOutput() if err != nil { - return nil, fmt.Errorf("execuate go tool cover -mode=atomic -var %s -o %s/%s failed, err: %v, out: %s", coverVar.Var, pkg.Dir, file, err, string(out)) + return nil, fmt.Errorf("execute go tool cover -mode=atomic -var %s -o %s/%s failed, err: %v, out: %s", coverVar.Var, pkg.Dir, file, err, string(out)) } } diff --git a/pkg/cover/server.go b/pkg/cover/server.go index 721ee05..93e9038 100644 --- a/pkg/cover/server.go +++ b/pkg/cover/server.go @@ -35,26 +35,31 @@ import ( // LocalStore implements the IPersistence interface var LocalStore Store -// Client implements the Action interface -var Client Action - // LogFile a file to save log. const LogFile = "goc.log" -// StartServer starts coverage host center -func StartServer(port string) { +func init() { LocalStore = NewStore() - Client = NewWorker() +} +// Run starts coverage host center +func Run(port string) { f, err := os.Create(LogFile) if err != nil { log.Fatalf("failed to create log file %s, err: %v", LogFile, err) } + r := GocServer(f) + log.Fatal(r.Run(port)) +} + +// GocServer init goc server engine +func GocServer(w io.Writer) *gin.Engine { + if w != nil && w != os.Stdout { + gin.DefaultWriter = io.MultiWriter(w, os.Stdout) + } - gin.DefaultWriter = io.MultiWriter(f, os.Stdout) r := gin.Default() - - // api to show the registerd services + // api to show the registered services r.StaticFile(PersistenceFile, "./"+PersistenceFile) v1 := r.Group("/v1") @@ -63,9 +68,10 @@ func StartServer(port string) { v1.GET("/cover/profile", profile) v1.POST("/cover/clear", clear) v1.POST("/cover/init", initSystem) + v1.GET("/cover/list", listServices) } - log.Fatal(r.Run(port)) + return r } // Service is a entry under being tested @@ -74,6 +80,12 @@ type Service struct { Address string `form:"address" json:"address" binding:"required"` } +//listServices list all the registered services +func listServices(c *gin.Context) { + services := LocalStore.GetAll() + c.JSON(http.StatusOK, services) +} + func registerService(c *gin.Context) { var service Service if err := c.ShouldBind(&service); err != nil { @@ -102,7 +114,7 @@ func registerService(c *gin.Context) { return } - c.JSON(http.StatusOK, gin.H{"name": service.Name, "address": service.Address}) + c.JSON(http.StatusOK, gin.H{"result": "success"}) } func profile(c *gin.Context) { @@ -110,7 +122,7 @@ func profile(c *gin.Context) { var mergedProfiles = make([][]*cover.Profile, len(svrsUnderTest)) for _, addrs := range svrsUnderTest { for _, addr := range addrs { - pp, err := Client.Profile(addr) + pp, err := NewWorker(addr).Profile() if err != nil { c.JSON(http.StatusExpectationFailed, gin.H{"error": err.Error()}) return @@ -145,7 +157,7 @@ func clear(c *gin.Context) { svrsUnderTest := LocalStore.GetAll() for svc, addrs := range svrsUnderTest { for _, addr := range addrs { - pp, err := Client.Clear(addr) + pp, err := NewWorker(addr).Clear() if err != nil { c.JSON(http.StatusExpectationFailed, gin.H{"error": err.Error()}) return diff --git a/tests/e2e/simple_project_test.go b/tests/e2e/simple_project_test.go index 370838f..139d05a 100644 --- a/tests/e2e/simple_project_test.go +++ b/tests/e2e/simple_project_test.go @@ -20,11 +20,10 @@ import ( "fmt" "os" "os/exec" + "path/filepath" "strings" "time" - "path/filepath" - . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/qiniu/goc/pkg/build" @@ -55,7 +54,7 @@ var _ = Describe("E2E", func() { By("goc build") testProjDir := filepath.Join(TESTS_ROOT, "samples/simple_project") - cmd := exec.Command("goc", "build", "--debuggoc") + cmd := exec.Command("goc", "build", "--debug") cmd.Dir = testProjDir out, err := cmd.CombinedOutput() @@ -63,7 +62,7 @@ var _ = Describe("E2E", func() { By("goc install") testProjDir = filepath.Join(TESTS_ROOT, "samples/simple_project") - cmd = exec.Command("goc", "install", "--debuggoc") + cmd = exec.Command("goc", "install", "--debug") cmd.Dir = testProjDir out, err = cmd.CombinedOutput() @@ -114,7 +113,7 @@ var _ = Describe("E2E", func() { GOPATH = testProjDir By("goc build") - cmd := exec.Command("goc", "build", "--debuggoc") + cmd := exec.Command("goc", "build", "--debug") cmd.Dir = oriWorkingDir // use GOPATH mode to compile project cmd.Env = append(os.Environ(), fmt.Sprintf("GOPATH=%v", GOPATH), "GO111MODULE=off") @@ -125,7 +124,7 @@ var _ = Describe("E2E", func() { By("goc install") testProjDir = filepath.Join(TESTS_ROOT, "samples/simple_gopath_project") - cmd = exec.Command("goc", "install", "--debuggoc") + cmd = exec.Command("goc", "install", "--debug") cmd.Dir = filepath.Join(testProjDir, "src/qiniu.com/simple_gopath_project") // use GOPATH mode to compile project cmd.Env = append(os.Environ(), fmt.Sprintf("GOPATH=%v", testProjDir), "GO111MODULE=off") @@ -157,7 +156,7 @@ var _ = Describe("E2E", func() { Expect(err).To(BeNil(), "the binary cannot be disassembled") cnt := strings.Count(string(out), "GoCover") - Expect(cnt).To(BeNumerically(">", 0), "GoCover varibale should be in the binary") + Expect(cnt).To(BeNumerically(">", 0), "GoCover variable should be in the binary") cnt = strings.Count(string(out), "main.registerSelf") Expect(cnt).To(BeNumerically(">", 0), "main.registerSelf function should be in the binary")