diff --git a/.github/workflows/e2e_test_check.yml b/.github/workflows/e2e_test_check.yml new file mode 100644 index 0000000..f60964b --- /dev/null +++ b/.github/workflows/e2e_test_check.yml @@ -0,0 +1,63 @@ +name: e2e test +on: + # Trigger the workflow on push or pull request, + # but only for the master branch + push: + paths-ignore: + - '**.md' + pull_request: + paths-ignore: + - '**.md' +jobs: + job_1: + name: Build goc binary + runs-on: ubuntu-latest + steps: + - name: Install Go + uses: actions/setup-go@v2 + with: + go-version: 1.14.x + - name: Checkout code + uses: actions/checkout@v2 + - name: Go build + run: | + cd cmd/goc + go build + - name: Go build test binary + run: | + cd tests/e2e + go get -u github.com/onsi/ginkgo/ginkgo + ginkgo build ./... + - name: Upload binary result for job 1 + uses: actions/upload-artifact@v2 + with: + name: goc + path: cmd/goc/goc + - name: Upload binary result for job 1 + uses: actions/upload-artifact@v2 + with: + name: e2e.test + path: tests/e2e/e2e.test + + job_2: + name: E2E test + needs: job_1 + strategy: + matrix: + go-version: [1.11.x, 1.12.x, 1.13.x, 1.14.x] + runs-on: ubuntu-latest + steps: + - name: Install Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + - name: Checkout code + uses: actions/checkout@v2 + - name: Download built binary + uses: actions/download-artifact@v2 + with: + path: /home/runner/tools + - name: Do test + run: | + cd tests + ./run-ci-actions.sh \ No newline at end of file diff --git a/.github/workflows/style_check.yml b/.github/workflows/style_check.yml index ff73988..ebc98ba 100644 --- a/.github/workflows/style_check.yml +++ b/.github/workflows/style_check.yml @@ -3,8 +3,6 @@ on: # Trigger the workflow on push or pull request, # but only for the master branch push: - branches: - - master paths-ignore: - '**.md' pull_request: diff --git a/.github/workflows/ut_check.yml b/.github/workflows/ut_check.yml index ae5d8ee..577f2f5 100644 --- a/.github/workflows/ut_check.yml +++ b/.github/workflows/ut_check.yml @@ -3,8 +3,6 @@ on: # Trigger the workflow on push or pull request, # but only for the master branch push: - branches: - - master paths-ignore: - '**.md' pull_request: @@ -27,4 +25,5 @@ jobs: uses: actions/checkout@v2 - name: Go test run: | - go test ./... + export DEFAULT_EXCEPT_PKGS=e2e + go test -p 1 -cover $(go list ./... | grep -v -E $DEFAULT_EXCEPT_PKGS) diff --git a/README.md b/README.md index a0bf106..75112b5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ ![](https://github.com/qiniu/goc/workflows/ut-check/badge.svg) ![](https://github.com/qiniu/goc/workflows/style-check/badge.svg) +![](https://github.com/qiniu/goc/workflows/e2e%20test/badge.svg) # goc A Comprehensive Coverage Testing System for The Go Programming Language diff --git a/cmd/goc/app/build.go b/cmd/goc/app/build.go new file mode 100644 index 0000000..d8b3dee --- /dev/null +++ b/cmd/goc/app/build.go @@ -0,0 +1,105 @@ +/* + Copyright 2020 Qiniu Cloud (七牛云) + + 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 app + +import ( + "flag" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/qiniu/goc/pkg/build" + "github.com/spf13/cobra" +) + +var buildCmd = &cobra.Command{ + Use: "build", + Short: "Do cover for all go files and execute go build command", + Long: `This build command is a little different from the official one, for instance: +* 'goc build' is equal to 'goc cover && go build' +* 'goc build --center=http://127.0.0.1:7777 -- -static app/kodo' is equal to 'goc cover --center=http://127.0.0.1:7777 && go build -static app/kodo' +* 'goc build -- -o output' is equal to 'goc cover && go build -output, both relative/absolute output paths are supported'`, + Run: func(cmd *cobra.Command, args []string) { + newgopath, newwd, tmpdir, pkgs := build.MvProjectsToTmp(target, args) + doCover(cmd, args, newgopath, tmpdir) + newArgs, modified := modifyOutputArg(args) + doBuild(newArgs, newgopath, newwd) + + // if not modified + // find the binary in temp build dir + // and copy them into original dir + if false == modified { + build.MvBinaryToOri(pkgs, tmpdir) + } + }, +} + +func init() { + buildCmd.Flags().StringVarP(¢er, "center", "", "http://127.0.0.1:7777", "cover profile host center") + + rootCmd.AddCommand(buildCmd) +} + +func doBuild(args []string, newgopath string, newworkingdir string) { + log.Println("Go building in temp...") + newArgs := []string{"build"} + newArgs = append(newArgs, args...) + cmd := exec.Command("go", newArgs...) + cmd.Dir = newworkingdir + + if newgopath != "" { + // Change to temp GOPATH for go install command + cmd.Env = append(os.Environ(), fmt.Sprintf("GOPATH=%v", newgopath)) + } + + out, err := cmd.CombinedOutput() + if err != nil { + log.Fatalf("Fail to execute: go build %v. The error is: %v, the stdout/stderr is: %v", strings.Join(args, " "), err, string(out)) + } + log.Println("Go build exit successful.") +} + +// As we build in the temp build dir, we have to modify the "-o output", +// if output is a relative path, transform it to abspath +func modifyOutputArg(args []string) (newArgs []string, modified bool) { + var output string + fs := flag.NewFlagSet("goc-build", flag.PanicOnError) + fs.StringVar(&output, "o", "", "output dir") + + // parse the go args after "--" + fs.Parse(args) + + // skip if output is not present + if output == "" { + modified = false + newArgs = args + return + } + + abs, err := filepath.Abs(output) + if err != nil { + log.Fatalf("Fail to transform the path: %v to absolute path, the error is: %v", output, err) + } + + // the second -o arg will overwrite the first one + newArgs = append(args, "-o", abs) + modified = true + return +} diff --git a/cmd/goc/app/cover.go b/cmd/goc/app/cover.go new file mode 100644 index 0000000..d2f9551 --- /dev/null +++ b/cmd/goc/app/cover.go @@ -0,0 +1,221 @@ +/* + Copyright 2020 Qiniu Cloud (七牛云) + + 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 app + +import ( + "fmt" + "log" + "os" + "strings" + + "github.com/qiniu/goc/pkg/cover" + "github.com/spf13/cobra" +) + +var coverCmd = &cobra.Command{ + Use: "cover", + Short: "do cover for the target source ", + Run: func(cmd *cobra.Command, args []string) { + doCover(cmd, args, "", "") + }, +} + +var ( + target string + center string +) + +func init() { + coverCmd.Flags().StringVarP(¢er, "center", "", "http://127.0.0.1:7777", "cover profile host center") + coverCmd.Flags().StringVarP(&target, "target", "", ".", "target folder to cover") + + rootCmd.AddCommand(coverCmd) + log.SetFlags(log.LstdFlags | log.Lshortfile) +} + +func doCover(cmd *cobra.Command, args []string, newgopath string, newtarget string) { + if newtarget != "" { + target = newtarget + } + if !isDirExist(target) { + log.Fatalf("target directory %s not exist", target) + } + + // + // + + // + listArgs := []string{"list", "-json"} + if len(args) != 0 { + listArgs = append(listArgs, args...) + } + listArgs = append(listArgs, "./...") + pkgs := cover.ListPackages(target, listArgs, newgopath) + + // + // + var seen = make(map[string]*cover.PackageCover) + var seenCache = make(map[string]*cover.PackageCover) + for _, pkg := range pkgs { + // + if pkg.Name == "main" { + log.Printf("handle package: %v", pkg.ImportPath) + // inject the main package + mainCover, err := cover.AddCounters(pkg, newgopath) + if err != nil { + log.Fatalf("failed to add counters for pkg %s, err: %v", pkg.ImportPath, err) + } + + // new a testcover for this service + tc := cover.TestCover{ + Mode: "atomic", + Center: center, + MainPkgCover: mainCover, + } + + // handle its dependency + var internalPkgCache = make(map[string][]*cover.PackageCover) + tc.CacheCover = make(map[string]*cover.PackageCover) + for _, dep := range pkg.Deps { + + if packageCover, ok := seen[dep]; ok { + tc.DepsCover = append(tc.DepsCover, packageCover) + continue + } + + //only focus package neither standard Go library nor dependency library + if depPkg, ok := pkgs[dep]; ok { + + if findInternal(dep) { + + //scan exist cache cover to tc.CacheCover + if cache, ok := seenCache[dep]; ok { + log.Printf("cache cover exist: %s", cache.Package.ImportPath) + tc.CacheCover[cache.Package.Dir] = cache + continue + } + + // add counter for internal package + inPkgCover, err := cover.AddCounters(depPkg, newgopath) + if err != nil { + log.Fatalf("failed to add counters for internal pkg %s, err: %v", depPkg.ImportPath, err) + } + parentDir := getInternalParent(depPkg.Dir) + parentImportPath := getInternalParent(depPkg.ImportPath) + + //if internal parent dir or import is root path, ignore the dep. the dep is Go library nor dependency library + if parentDir == "" { + continue + } + if parentImportPath == "" { + continue + } + + pkg := &cover.Package{ + ImportPath: parentImportPath, + Dir: parentDir, + } + + // Some internal package have same parent dir or import path + // Cache all vars by internal parent dir for all child internal counter vars + cacheCover := cover.AddCacheCover(pkg, inPkgCover) + if v, ok := tc.CacheCover[cacheCover.Package.Dir]; ok { + for cVar, val := range v.Vars { + cacheCover.Vars[cVar] = val + } + tc.CacheCover[cacheCover.Package.Dir] = cacheCover + } else { + tc.CacheCover[cacheCover.Package.Dir] = cacheCover + } + + // Cache all internal vars to internal parent package + inCover := cover.CacheInternalCover(inPkgCover) + if v, ok := internalPkgCache[cacheCover.Package.Dir]; ok { + v = append(v, inCover) + internalPkgCache[cacheCover.Package.Dir] = v + } else { + var covers []*cover.PackageCover + covers = append(covers, inCover) + internalPkgCache[cacheCover.Package.Dir] = covers + } + seenCache[dep] = cacheCover + continue + } + + packageCover, err := cover.AddCounters(depPkg, newgopath) + if err != nil { + log.Fatalf("failed to add counters for pkg %s, err: %v", depPkg.ImportPath, err) + } + tc.DepsCover = append(tc.DepsCover, packageCover) + seen[dep] = packageCover + } + } + + if errs := cover.InjectCacheCounters(internalPkgCache, tc.CacheCover); len(errs) > 0 { + log.Fatalf("failed to inject cache counters for package: %s, err: %v", pkg.ImportPath, errs) + } + + // inject Http Cover APIs + var httpCoverApis = fmt.Sprintf("%s/http_cover_apis_auto_generated.go", pkg.Dir) + if err := cover.InjectCountersHandlers(tc, httpCoverApis); err != nil { + log.Fatalf("failed to inject counters for package: %s, err: %v", pkg.ImportPath, err) + } + } + } +} + +func isDirExist(path string) bool { + s, err := os.Stat(path) + if err != nil { + return false + } + return s.IsDir() +} + +// Refer: https://github.com/golang/go/blob/master/src/cmd/go/internal/load/pkg.go#L1334:6 +// findInternal looks for the final "internal" path element in the given import path. +// If there isn't one, findInternal returns ok=false. +// Otherwise, findInternal returns ok=true and the index of the "internal". +func findInternal(path string) bool { + // Three cases, depending on internal at start/end of string or not. + // The order matters: we must return the index of the final element, + // because the final one produces the most restrictive requirement + // on the importer. + switch { + case strings.HasSuffix(path, "/internal"): + return true + case strings.Contains(path, "/internal/"): + return true + case path == "internal", strings.HasPrefix(path, "internal/"): + return true + } + return false +} + +func getInternalParent(path string) string { + switch { + case strings.HasSuffix(path, "/internal"): + return strings.Split(path, "/internal")[0] + case strings.Contains(path, "/internal/"): + return strings.Split(path, "/internal/")[0] + case path == "internal": + return "" + case strings.HasPrefix(path, "internal/"): + return strings.Split(path, "internal/")[0] + } + return "" +} diff --git a/cmd/goc/app/install.go b/cmd/goc/app/install.go new file mode 100644 index 0000000..1c00982 --- /dev/null +++ b/cmd/goc/app/install.go @@ -0,0 +1,69 @@ +/* + Copyright 2020 Qiniu Cloud (七牛云) + + 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 app + +import ( + "fmt" + "log" + "os" + "os/exec" + "strings" + + "github.com/qiniu/goc/pkg/build" + "github.com/qiniu/goc/pkg/cover" + "github.com/spf13/cobra" +) + +var installCmd = &cobra.Command{ + Use: "install", + Short: "Do cover for all go files and execute go install command", + Long: `This install command is a little different from the official one, for instance: +* 'goc install -- ./...' is equal to 'goc cover && go install ./...' +* 'goc install --center=http://127.0.0.1:7777 -- -static ./...' is equal to 'goc cover --center=http://127.0.0.1:7777 && go install -static ./...'`, + Run: func(cmd *cobra.Command, args []string) { + newgopath, newwd, tmpdir, pkgs := build.MvProjectsToTmp(target, args) + doCover(cmd, args, newgopath, tmpdir) + doInstall(args, newgopath, newwd, pkgs) + }, +} + +func init() { + installCmd.Flags().StringVarP(¢er, "center", "", "http://127.0.0.1:7777", "cover profile host center") + + rootCmd.AddCommand(installCmd) +} + +func doInstall(args []string, newgopath string, newworkingdir string, pkgs map[string]*cover.Package) { + log.Println("Go building in temp...") + newArgs := []string{"install"} + newArgs = append(newArgs, args...) + cmd := exec.Command("go", newArgs...) + cmd.Dir = newworkingdir + + // Change the temp GOBIN, to force binary install to original place + cmd.Env = append(os.Environ(), fmt.Sprintf("GOBIN=%v", build.FindWhereToInstall(pkgs))) + if newgopath != "" { + // Change to temp GOPATH for go install command + cmd.Env = append(cmd.Env, fmt.Sprintf("GOPATH=%v", newgopath)) + } + + out, err := cmd.CombinedOutput() + if err != nil { + log.Fatalf("Fail to execute: go install %v. The error is: %v, the stdout/stderr is: %v", strings.Join(args, " "), err, string(out)) + } + log.Printf("Go install successful. Binary installed in: %v", build.FindWhereToInstall(pkgs)) +} diff --git a/go.mod b/go.mod index cf0bf34..7e1d955 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,11 @@ go 1.13 require ( github.com/gin-gonic/gin v1.6.3 + github.com/onsi/ginkgo v1.11.0 + github.com/onsi/gomega v1.8.1 + github.com/otiai10/copy v1.0.2 github.com/spf13/cobra v1.0.0 + github.com/stretchr/testify v1.5.1 golang.org/x/tools v0.0.0-20200303214625-2b0b585e22fe k8s.io/test-infra v0.0.0-20200511080351-8ac9dbfab055 ) diff --git a/go.sum b/go.sum index d37b08d..ae2819f 100644 --- a/go.sum +++ b/go.sum @@ -401,6 +401,7 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= @@ -522,11 +523,13 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw= github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.8.1 h1:C5Dqfs/LeauYDX0jJXIe2SWmwCbGzx9yF8C8xy3Lh34= github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= @@ -539,6 +542,7 @@ github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.m github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/openzipkin/zipkin-go v0.2.0/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/otiai10/copy v1.0.2 h1:DDNipYy6RkIkjMwy+AWzgKiNTyj2RUI9yEMeETEpVyc= github.com/otiai10/copy v1.0.2/go.mod h1:c7RpqBkwMom4bYTSkLSym4VSJz/XtncWRAj/J4PEIMY= github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= @@ -795,6 +799,7 @@ golang.org/x/net v0.0.0-20190912160710-24e19bdeb0f2/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -849,6 +854,7 @@ golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -897,6 +903,7 @@ golang.org/x/tools v0.0.0-20200303214625-2b0b585e22fe h1:Kh3iY7o/2bMfQXZdwLdL9jD golang.org/x/tools v0.0.0-20200303214625-2b0b585e22fe/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.0.1/go.mod h1:IhYNNY4jnS53ZnfE4PAmpKtDpTCj1JFXc+3mwe7XcUU= gomodules.xyz/jsonpatch/v2 v2.1.0/go.mod h1:IhYNNY4jnS53ZnfE4PAmpKtDpTCj1JFXc+3mwe7XcUU= @@ -957,6 +964,7 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gcfg.v1 v1.2.0/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= @@ -977,6 +985,7 @@ gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76 gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.1/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= diff --git a/pkg/build/binarymove.go b/pkg/build/binarymove.go new file mode 100644 index 0000000..7b7f6ec --- /dev/null +++ b/pkg/build/binarymove.go @@ -0,0 +1,77 @@ +/* + Copyright 2020 Qiniu Cloud (七牛云) + + 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 build + +import ( + "log" + "os" + "path/filepath" + + "github.com/otiai10/copy" + "github.com/qiniu/goc/pkg/cover" +) + +func MvBinaryToOri(pkgs map[string]*cover.Package, newgopath string) { + for _, pkg := range pkgs { + if pkg.Name == "main" { + _, binaryTarget := filepath.Split(pkg.Target) + + binaryTmpPath := filepath.Join(getTmpwd(newgopath, pkgs, !checkIfLegacyProject(pkgs)), binaryTarget) + + if false == checkIfFileExist(binaryTmpPath) { + continue + } + + curwd, err := os.Getwd() + if err != nil { + log.Fatalf("Cannot get current working directoy, the error is: %v", err) + } + binaryOriPath := filepath.Join(curwd, binaryTarget) + + if checkIfFileExist(binaryOriPath) { + // if we have file in the original place with same name, + // but this file is not a binary, + // then we skip it + if false == checkIfExecutable(binaryOriPath) { + log.Printf("Skipping binary: %v, as we find a file in the original place with same name but not executable.", binaryOriPath) + continue + } + } + + log.Printf("Generating binary: %v", binaryOriPath) + if err = copy.Copy(binaryTmpPath, binaryOriPath); err != nil { + log.Println(err) + } + } + } +} + +func checkIfExecutable(path string) bool { + fileInfo, err := os.Lstat(path) + if err != nil { + return false + } + return fileInfo.Mode()&0100 != 0 +} + +func checkIfFileExist(path string) bool { + fileInfo, err := os.Stat(path) + if os.IsNotExist(err) { + return false + } + return !fileInfo.IsDir() +} diff --git a/pkg/build/gomodules.go b/pkg/build/gomodules.go new file mode 100644 index 0000000..d89a3bd --- /dev/null +++ b/pkg/build/gomodules.go @@ -0,0 +1,40 @@ +/* + Copyright 2020 Qiniu Cloud (七牛云) + + 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 build + +import ( + "log" + + "github.com/otiai10/copy" + "github.com/qiniu/goc/pkg/cover" +) + +func cpGoModulesProject(tmpBuildDir string, pkgs map[string]*cover.Package) { + for _, v := range pkgs { + if v.Name == "main" { + dst := tmpBuildDir + src := v.Module.Dir + + if err := copy.Copy(src, dst); err != nil { + log.Printf("Failed to Copy the folder from %v to %v, the error is: %v ", src, dst, err) + } + break + } else { + continue + } + } +} diff --git a/pkg/build/legacy.go b/pkg/build/legacy.go new file mode 100644 index 0000000..4e4b82d --- /dev/null +++ b/pkg/build/legacy.go @@ -0,0 +1,83 @@ +/* + Copyright 2020 Qiniu Cloud (七牛云) + + 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 build + +import ( + "log" + "os" + "path/filepath" + + "github.com/otiai10/copy" + "github.com/qiniu/goc/pkg/cover" +) + +func cpLegacyProject(tmpBuildDir string, pkgs map[string]*cover.Package) { + + visited := make(map[string]bool) + + for k, v := range pkgs { + dst := filepath.Join(tmpBuildDir, "src", k) + src := v.Dir + + if _, ok := visited[src]; ok { + // Skip if already copied + continue + } + + if err := copy.Copy(src, dst); err != nil { + log.Printf("Failed to Copy the folder from %v to %v, the error is: %v ", src, dst, err) + } + + visited[src] = true + + cpDepPackages(tmpBuildDir, v, visited) + } +} + +// only cp dependency in root(current gopath), +// skip deps in other GOPATHs +func cpDepPackages(tmpBuildDir string, pkg *cover.Package, visited map[string]bool) { + /* + oriGOPATH := os.Getenv("GOPATH") + if oriGOPATH == "" { + oriGOPATH = filepath.Join(os.Getenv("HOME"), "go") + } + gopaths := strings.Split(oriGOPATH, ":") + */ + gopath := pkg.Root + for _, dep := range pkg.Deps { + src := filepath.Join(gopath, "src", dep) + // Check if copied + if _, ok := visited[src]; ok { + // Skip if already copied + continue + } + // Check if we can found in the root gopath + _, err := os.Stat(src) + if err != nil { + continue + } + + dst := filepath.Join(tmpBuildDir, "src", dep) + + if err := copy.Copy(src, dst); err != nil { + log.Printf("Failed to Copy the folder from %v to %v, the error is: %v ", src, dst, err) + } + + visited[src] = true + } +} diff --git a/pkg/build/tmpfolder.go b/pkg/build/tmpfolder.go new file mode 100644 index 0000000..de7a772 --- /dev/null +++ b/pkg/build/tmpfolder.go @@ -0,0 +1,149 @@ +/* + Copyright 2020 Qiniu Cloud (七牛云) + + 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 build + +import ( + "crypto/sha256" + "fmt" + "log" + "os" + "path/filepath" + "strings" + + "github.com/qiniu/goc/pkg/cover" +) + +func MvProjectsToTmp(target string, args []string) (newgopath string, newWorkingDir string, tmpBuildDir string, pkgs map[string]*cover.Package) { + listArgs := []string{"list", "-json"} + if len(args) != 0 { + listArgs = append(listArgs, args...) + } + listArgs = append(listArgs, "./...") + pkgs = cover.ListPackages(target, listArgs, "") + + tmpBuildDir, newWorkingDir, isMod := mvProjectsToTmp(pkgs) + origopath := os.Getenv("GOPATH") + if isMod == true { + newgopath = "" + } else if origopath == "" { + newgopath = tmpBuildDir + } else { + newgopath = fmt.Sprintf("%v:%v", tmpBuildDir, origopath) + } + log.Printf("New GOPATH: %v", newgopath) + return +} + +func mvProjectsToTmp(pkgs map[string]*cover.Package) (string, string, bool) { + path, err := os.Getwd() + if err != nil { + log.Fatalf("Cannot get current working directoy, the error is: %v", err) + } + tmpBuildDir := filepath.Join(os.TempDir(), TmpFolderName(path)) + + // Delete previous tmp folder and its content + os.RemoveAll(tmpBuildDir) + // Create a new tmp folder + err = os.MkdirAll(filepath.Join(tmpBuildDir, "src"), os.ModePerm) + if err != nil { + log.Fatalf("Fail to create the temporary build directory. The err is: %v", err) + } + log.Printf("Temp project generated in: %v", tmpBuildDir) + + isMod := false + var tmpWorkingDir string + if checkIfLegacyProject(pkgs) { + cpLegacyProject(tmpBuildDir, pkgs) + tmpWorkingDir = getTmpwd(tmpBuildDir, pkgs, false) + } else { + cpGoModulesProject(tmpBuildDir, pkgs) + tmpWorkingDir = getTmpwd(tmpBuildDir, pkgs, true) + isMod = true + } + + log.Printf("New working/building directory in: %v", tmpWorkingDir) + return tmpBuildDir, tmpWorkingDir, isMod +} + +func TmpFolderName(path string) string { + sum := sha256.Sum256([]byte(path)) + h := fmt.Sprintf("%x", sum[:6]) + + return "goc-" + h +} + +// Check if it is go module project +// true legacy +// flase go mod +func checkIfLegacyProject(pkgs map[string]*cover.Package) bool { + for _, v := range pkgs { + + if v.Module == nil { + return true + } + return false + } + log.Fatalln("Should never be reached....") + return false +} + +func getTmpwd(tmpBuildDir string, pkgs map[string]*cover.Package, isMod bool) string { + for _, pkg := range pkgs { + path, err := os.Getwd() + if err != nil { + log.Fatalf("Cannot get current working directoy, the error is: %v", err) + } + + index := -1 + var parentPath string + if isMod == false { + index = strings.Index(path, pkg.Root) + parentPath = pkg.Root + } else { + index = strings.Index(path, pkg.Module.Dir) + parentPath = pkg.Module.Dir + } + + if index == -1 { + log.Fatalf("goc install not executed in project directory.") + } + tmpwd := filepath.Join(tmpBuildDir, path[len(parentPath):]) + // log.Printf("New building directory in: %v", tmpwd) + return tmpwd + } + + log.Fatalln("Should never be reached....") + return "" +} + +func FindWhereToInstall(pkgs map[string]*cover.Package) string { + if GOBIN := os.Getenv("GOBIN"); GOBIN != "" { + return GOBIN + } + + // old GOPATH dir + GOPATH := os.Getenv("GOPATH") + if true == checkIfLegacyProject(pkgs) { + for _, v := range pkgs { + return filepath.Join(v.Root, "bin") + } + } + if GOPATH != "" { + return filepath.Join(strings.Split(GOPATH, ":")[0], "bin") + } + return filepath.Join(os.Getenv("HOME"), "go", "bin") +} diff --git a/pkg/build/tmpfolder_test.go b/pkg/build/tmpfolder_test.go new file mode 100644 index 0000000..735d2a9 --- /dev/null +++ b/pkg/build/tmpfolder_test.go @@ -0,0 +1,133 @@ +/* + Copyright 2020 Qiniu Cloud (七牛云) + + 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 build + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/qiniu/goc/pkg/cover" +) + +const TEST_GO_LIST_LEGACY = `{ + "Dir": "/Users/lyyyuna/gitup/linking/src/qiniu.com/linking/api/linking.v1", + "ImportPath": "qiniu.com/linking/api/linking.v1", + "Name": "linking", + "Target": "/Users/lyyyuna/gitup/linking/pkg/darwin_amd64/qiniu.com/linking/api/linking.v1.a", + "Root": "/Users/lyyyuna/gitup/linking", + "Match": [ + "./..." + ], + "Stale": true, + "StaleReason": "stale dependency: vendor/github.com/modern-go/concurrent", + "GoFiles": [ + "client.go" + ], + "Imports": [ + "vendor/github.com/json-iterator/go", + "github.com/qiniu/rpc.v2", + "vendor/github.com/qiniu/xlog.v1", + "vendor/qiniu.com/auth/qiniumac.v1" + ], + "ImportMap": { + "github.com/json-iterator/go": "vendor/github.com/json-iterator/go", + "github.com/qiniu/xlog.v1": "vendor/github.com/qiniu/xlog.v1", + "qiniu.com/auth/qiniumac.v1": "vendor/qiniu.com/auth/qiniumac.v1" + }, + "Deps": [ + "bufio" + ] +}` + +const TEST_GO_LIST_MOD = `{ + "Dir": "/Users/lyyyuna/gitup/tonghu-chat", + "ImportPath": "github.com/lyyyuna/tonghu-chat", + "Name": "main", + "Target": "/Users/lyyyuna/go/bin/tonghu-chat", + "Root": "/Users/lyyyuna/gitup/tonghu-chat", + "Module": { + "Path": "github.com/lyyyuna/tonghu-chat", + "Main": true, + "Dir": "/Users/lyyyuna/gitup/tonghu-chat", + "GoMod": "/Users/lyyyuna/gitup/tonghu-chat/go.mod", + "GoVersion": "1.14" + }, + "Match": [ + "./..." + ], + "Stale": true, + "StaleReason": "not installed but available in build cache", + "GoFiles": [ + "main.go" + ], + "Imports": [ + "github.com/gin-gonic/gin", + "github.com/gorilla/websocket" + ], + "Deps": [ + "bufio" + ] +}` + +func constructPkg(raw string) *cover.Package { + var pkg cover.Package + if err := json.Unmarshal([]byte(raw), &pkg); err != nil { + panic(err) + } + return &pkg +} + +func TestLegacyProjectJudgement(t *testing.T) { + pkgs := make(map[string]*cover.Package) + pkg := constructPkg(TEST_GO_LIST_LEGACY) + pkgs[pkg.ImportPath] = pkg + if expect, got := true, checkIfLegacyProject(pkgs); expect != got { + t.Fatalf("Expected %v, but got %v.", expect, got) + } +} + +func TestModProjectJudgement(t *testing.T) { + pkgs := make(map[string]*cover.Package) + pkg := constructPkg(TEST_GO_LIST_MOD) + pkgs[pkg.ImportPath] = pkg + if expect, got := false, checkIfLegacyProject(pkgs); expect != got { + t.Fatalf("Expected %v, but got %v.", expect, got) + } +} + +func TestNewDirParseInLegacyProject(t *testing.T) { + workingDir := "../../tests/samples/simple_gopath_project/src/qiniu.com/simple_gopath_project" + gopath, _ := filepath.Abs("../../tests/samples/simple_gopath_project") + + os.Chdir(workingDir) + fmt.Println(gopath) + os.Setenv("GOPATH", gopath) + os.Setenv("GO111MODULE", "off") + + newgopath, newwd, tmpdir, _ := MvProjectsToTmp(".", nil) + if -1 == strings.Index(newwd, tmpdir) { + t.Fatalf("Directory parse error. newwd: %v, tmpdir: %v", newwd, tmpdir) + } + + if -1 == strings.Index(newgopath, ":") || -1 == strings.Index(newgopath, tmpdir) { + t.Fatalf("The New GOPATH is wrong. newgopath: %v, tmpdir: %v", newgopath, tmpdir) + } +} diff --git a/pkg/cover/cover.go b/pkg/cover/cover.go new file mode 100644 index 0000000..8febf36 --- /dev/null +++ b/pkg/cover/cover.go @@ -0,0 +1,384 @@ +/* + Copyright 2020 Qiniu Cloud (七牛云) + + 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 ( + "bufio" + "bytes" + "crypto/sha256" + "encoding/json" + "fmt" + "io" + "log" + "os" + "os/exec" + "path" + "strconv" + "strings" + "time" +) + +// TestCover is a collection of all counters +type TestCover struct { + Mode string + Center string // cover profile host center + MainPkgCover *PackageCover + DepsCover []*PackageCover + CacheCover map[string]*PackageCover +} + +// PackageCover holds all the generate coverage variables of a package +type PackageCover struct { + Package *Package + Vars map[string]*FileVar +} + +// FileVar holds the name of the generated coverage variables targeting the named file. +type FileVar struct { + File string + Var string +} + +// 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 +type Package struct { + Dir string `json:"Dir"` // directory containing package sources + ImportPath string `json:"ImportPath"` // import path of package in dir + Name string `json:"Name"` // package name + Target string `json:",omitempty"` // installed target for this package (may be executable) + Root string `json:",omitempty"` // Go root, Go path dir, or module root dir containing this package + + Module *ModulePublic `json:",omitempty"` // info about package's module, if any + Goroot bool `json:"Goroot,omitempty"` // is this package in the Go root? + Standard bool `json:"Standard,omitempty"` // is this package part of the standard Go library? + DepOnly bool `json:"DepOnly,omitempty"` // package is only a dependency, not explicitly listed + + // Source files + GoFiles []string `json:"GoFiles,omitempty"` // .go source files (excluding CgoFiles, TestGoFiles, XTestGoFiles) + CgoFiles []string `json:"CgoFiles,omitempty"` // .go source files that import "C" + + // Dependency information + Deps []string `json:"Deps,omitempty"` // all (recursively) imported dependencies + Imports []string `json:",omitempty"` // import paths used by this package + ImportMap map[string]string `json:",omitempty"` // map from source import to ImportPath (identity entries omitted) + + // Error information + Incomplete bool `json:"Incomplete,omitempty"` // this package or a dependency has an error + Error *PackageError `json:"Error,omitempty"` // error loading package + DepsErrors []*PackageError `json:"DepsErrors,omitempty"` // errors loading dependencies +} + +type ModulePublic struct { + Path string `json:",omitempty"` // module path + Version string `json:",omitempty"` // module version + Versions []string `json:",omitempty"` // available module versions + Replace *ModulePublic `json:",omitempty"` // replaced by this module + Time *time.Time `json:",omitempty"` // time version was created + Update *ModulePublic `json:",omitempty"` // available update (with -u) + Main bool `json:",omitempty"` // is this the main module? + Indirect bool `json:",omitempty"` // module is only indirectly needed by main module + Dir string `json:",omitempty"` // directory holding local copy of files, if any + GoMod string `json:",omitempty"` // path to go.mod file describing module, if any + GoVersion string `json:",omitempty"` // go version used in module + Error *ModuleError `json:",omitempty"` // error loading module +} + +type ModuleError struct { + Err string // error text +} + +// PackageError is the error info for a package when list failed +type PackageError struct { + ImportStack []string // shortest path from package named on command line to this one + Pos string // position of error (if present, file:line:col) + Err string // the error itself +} + +// ListPackages list all packages under specific via go list command +func ListPackages(dir string, args []string, newgopath string) map[string]*Package { + cmd := exec.Command("go", args...) + log.Printf("go list cmd is: %v", cmd.Args) + cmd.Dir = dir + if newgopath != "" { + cmd.Env = append(os.Environ(), fmt.Sprintf("GOPATH=%v", newgopath)) + } + out, _ := cmd.Output() + // if err != nil { + // log.Fatalf("excute `go list -json ./...` command failed, err: %v, out: %v", err, string(out)) + // } + + dec := json.NewDecoder(bytes.NewReader(out)) + pkgs := make(map[string]*Package, 0) + for { + var pkg Package + if err := dec.Decode(&pkg); err != nil { + if err == io.EOF { + break + } + log.Fatalf("reading go list output: %v", err) + } + if pkg.Error != nil { + log.Fatalf("list package %s failed with output: %v", pkg.ImportPath, pkg.Error) + } + + // for _, err := range pkg.DepsErrors { + // log.Fatalf("dependency package list failed, err: %v", err) + // } + + pkgs[pkg.ImportPath] = &pkg + } + return pkgs +} + +// AddCounters add counters for all go files under the package +func AddCounters(pkg *Package, newgopath string) (*PackageCover, error) { + coverVarMap := declareCoverVars(pkg) + + // to construct: go tool cover -mode=atomic -o dest src (note: dest==src) + var args = []string{"tool", "cover", "-mode=atomic"} + for file, coverVar := range coverVarMap { + var newArgs = args + newArgs = append(newArgs, "-var", coverVar.Var) + longPath := path.Join(pkg.Dir, file) + newArgs = append(newArgs, "-o", longPath, longPath) + cmd := exec.Command("go", newArgs...) + if newgopath != "" { + cmd.Env = append(os.Environ(), fmt.Sprintf("GOPATH=%v", 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, longPath, longPath, err, string(out)) + } + } + + return &PackageCover{ + Package: pkg, + Vars: coverVarMap, + }, nil +} + +// declareCoverVars attaches the required cover variables names +// to the files, to be used when annotating the files. +func declareCoverVars(p *Package) map[string]*FileVar { + coverVars := make(map[string]*FileVar) + coverIndex := 0 + // We create the cover counters as new top-level variables in the package. + // We need to avoid collisions with user variables (GoCover_0 is unlikely but still) + // and more importantly with dot imports of other covered packages, + // so we append 12 hex digits from the SHA-256 of the import path. + // The point is only to avoid accidents, not to defeat users determined to + // break things. + sum := sha256.Sum256([]byte(p.ImportPath)) + h := fmt.Sprintf("%x", sum[:6]) + for _, file := range p.GoFiles { + // These names appear in the cmd/cover HTML interface. + var longFile = path.Join(p.ImportPath, file) + coverVars[file] = &FileVar{ + File: longFile, + Var: fmt.Sprintf("GoCover_%d_%x", coverIndex, h), + } + coverIndex++ + } + + for _, file := range p.CgoFiles { + // These names appear in the cmd/cover HTML interface. + var longFile = path.Join(p.ImportPath, file) + coverVars[file] = &FileVar{ + File: longFile, + Var: fmt.Sprintf("GoCover_%d_%x", coverIndex, h), + } + coverIndex++ + } + + return coverVars +} + +func declareCacheVars(in *PackageCover) map[string]*FileVar { + sum := sha256.Sum256([]byte(in.Package.ImportPath)) + h := fmt.Sprintf("%x", sum[:5]) + + vars := make(map[string]*FileVar) + coverIndex := 0 + for _, v := range in.Vars { + cacheVar := fmt.Sprintf("GoCacheCover_%d_%x", coverIndex, h) + vars[cacheVar] = v + coverIndex++ + } + return vars +} + +func CacheInternalCover(in *PackageCover) *PackageCover { + c := &PackageCover{} + vars := declareCacheVars(in) + c.Package = in.Package + c.Vars = vars + return c +} + +func AddCacheCover(pkg *Package, in *PackageCover) *PackageCover { + c := &PackageCover{} + sum := sha256.Sum256([]byte(pkg.ImportPath)) + h := fmt.Sprintf("%x", sum[:6]) + goFile := fmt.Sprintf("cache_vars_auto_generated_%x.go", h) + p := &Package{ + Dir: fmt.Sprintf("%s/cache_%x", pkg.Dir, h), + ImportPath: fmt.Sprintf("%s/cache_%x", pkg.ImportPath, h), + Name: fmt.Sprintf("cache_%x", h), + } + p.GoFiles = append(p.GoFiles, goFile) + c.Package = p + c.Vars = declareCacheVars(in) + return c +} + +// CoverageList is a collection and summary over multiple file Coverage objects +type CoverageList struct { + *Coverage + Groups []Coverage + ConcernedFiles map[string]bool + CovThresholdInt int +} + +// Coverage stores test coverage summary data for one file +type Coverage struct { + FileName string + NCoveredStmts int + NAllStmts int + LineCovLink string +} + +type codeBlock struct { + fileName string // the file the code block is in + numStatements int // number of statements in the code block + coverageCount int // number of times the block is covered +} + +func CovList(f io.Reader) (g *CoverageList, err error) { + scanner := bufio.NewScanner(f) + scanner.Scan() // discard first line + g = NewCoverageList("", map[string]bool{}, 0) + + for scanner.Scan() { + row := scanner.Text() + blk, err := toBlock(row) + if err != nil { + return nil, err + } + blk.addToGroupCov(g) + } + return +} + +// NewCoverageList constructs new (file) group Coverage +func NewCoverageList(name string, concernedFiles map[string]bool, covThresholdInt int) *CoverageList { + return &CoverageList{ + Coverage: newCoverage(name), + Groups: []Coverage{}, + ConcernedFiles: concernedFiles, + CovThresholdInt: covThresholdInt, + } +} + +func newCoverage(name string) *Coverage { + return &Coverage{name, 0, 0, ""} +} + +// convert a line in profile file to a codeBlock struct +func toBlock(line string) (res *codeBlock, err error) { + slice := strings.Split(line, " ") + if len(slice) != 3 { + return nil, fmt.Errorf("the profile line %s is not expected", line) + } + blockName := slice[0] + nStmts, _ := strconv.Atoi(slice[1]) + coverageCount, _ := strconv.Atoi(slice[2]) + return &codeBlock{ + fileName: blockName[:strings.Index(blockName, ":")], + numStatements: nStmts, + coverageCount: coverageCount, + }, nil +} + +// add blk Coverage to file group Coverage +func (blk *codeBlock) addToGroupCov(g *CoverageList) { + if g.size() == 0 || g.lastElement().Name() != blk.fileName { + // when a new file name is processed + coverage := newCoverage(blk.fileName) + g.append(coverage) + } + cov := g.lastElement() + cov.NAllStmts += blk.numStatements + if blk.coverageCount > 0 { + cov.NCoveredStmts += blk.numStatements + } +} + +func (g *CoverageList) size() int { + return len(g.Groups) +} + +func (g *CoverageList) lastElement() *Coverage { + return &g.Groups[g.size()-1] +} + +func (g *CoverageList) append(c *Coverage) { + g.Groups = append(g.Groups, *c) +} + +// Group returns the collection of file Coverage objects +func (g *CoverageList) Group() *[]Coverage { + return &g.Groups +} + +// Map returns maps the file name to its coverage for faster retrieval +// & membership check +func (g *CoverageList) Map() map[string]Coverage { + m := make(map[string]Coverage) + for _, c := range g.Groups { + m[c.Name()] = c + } + return m +} + +// Name returns the file name +func (c *Coverage) Name() string { + return c.FileName +} + +// Percentage returns the percentage of statements covered +func (c *Coverage) Percentage() string { + ratio, err := c.Ratio() + if err == nil { + return PercentStr(ratio) + } + + return "N/A" +} + +func (c *Coverage) Ratio() (ratio float32, err error) { + if c.NAllStmts == 0 { + err = fmt.Errorf("[%s] has 0 statement", c.Name()) + } else { + ratio = float32(c.NCoveredStmts) / float32(c.NAllStmts) + } + return +} + +// PercentStr converts a fraction number to percentage string representation +func PercentStr(f float32) string { + return fmt.Sprintf("%.1f%%", f*100) +} diff --git a/pkg/cover/cover_test.go b/pkg/cover/cover_test.go new file mode 100644 index 0000000..dcebca6 --- /dev/null +++ b/pkg/cover/cover_test.go @@ -0,0 +1,97 @@ +/* + Copyright 2020 Qiniu Cloud (七牛云) + + 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 ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func testCoverage() (c *Coverage) { + return &Coverage{FileName: "fake-coverage", NCoveredStmts: 200, NAllStmts: 300} +} + +func TestCoverageRatio(t *testing.T) { + c := testCoverage() + actualRatio, _ := c.Ratio() + assert.Equal(t, float32(c.NCoveredStmts)/float32(c.NAllStmts), actualRatio) +} + +func TestRatioErr(t *testing.T) { + c := &Coverage{FileName: "fake-coverage", NCoveredStmts: 200, NAllStmts: 0} + _, err := c.Ratio() + assert.NotNil(t, err) +} + +func TestPercentageNA(t *testing.T) { + c := &Coverage{FileName: "fake-coverage", NCoveredStmts: 200, NAllStmts: 0} + assert.Equal(t, "N/A", c.Percentage()) +} + +func TestGenLocalCoverDiffReport(t *testing.T) { + //coverage increase + newList := &CoverageList{Groups: []Coverage{{FileName: "fake-coverage", NCoveredStmts: 15, NAllStmts: 20}}} + baseList := &CoverageList{Groups: []Coverage{{FileName: "fake-coverage", NCoveredStmts: 10, NAllStmts: 20}}} + rows := GenLocalCoverDiffReport(newList, baseList) + assert.Equal(t, 1, len(rows)) + assert.Equal(t, []string{"fake-coverage", "50.0%", "75.0%", "25.0%"}, rows[0]) + + //coverage decrease + baseList = &CoverageList{Groups: []Coverage{{FileName: "fake-coverage", NCoveredStmts: 20, NAllStmts: 20}}} + rows = GenLocalCoverDiffReport(newList, baseList) + assert.Equal(t, []string{"fake-coverage", "100.0%", "75.0%", "-25.0%"}, rows[0]) + + //diff file + baseList = &CoverageList{Groups: []Coverage{{FileName: "fake-coverage-v1", NCoveredStmts: 10, NAllStmts: 20}}} + rows = GenLocalCoverDiffReport(newList, baseList) + assert.Equal(t, []string{"fake-coverage", "None", "75.0%", "75.0%"}, rows[0]) +} + +func TestCovList(t *testing.T) { + fileName := "qiniu.com/kodo/apiserver/server/main.go" + + // percentage is 100% + p := strings.NewReader("mode: atomic\n" + + fileName + ":32.49,33.13 1 30\n") + covL, err := CovList(p) + covF := covL.Map()[fileName] + assert.Nil(t, err) + assert.Equal(t, "100.0%", covF.Percentage()) + + // percentage is 50% + p = strings.NewReader("mode: atomic\n" + + fileName + ":32.49,33.13 1 30\n" + + fileName + ":42.49,43.13 1 0\n") + covL, err = CovList(p) + covF = covL.Map()[fileName] + assert.Nil(t, err) + assert.Equal(t, "50.0%", covF.Percentage()) + + // two files + fileName1 := "qiniu.com/kodo/apiserver/server/svr.go" + p = strings.NewReader("mode: atomic\n" + + fileName + ":32.49,33.13 1 30\n" + + fileName1 + ":42.49,43.13 1 0\n") + covL, err = CovList(p) + covF = covL.Map()[fileName] + covF1 := covL.Map()[fileName1] + assert.Nil(t, err) + assert.Equal(t, "100.0%", covF.Percentage()) + assert.Equal(t, "0.0%", covF1.Percentage()) +} diff --git a/pkg/cover/delta.go b/pkg/cover/delta.go new file mode 100644 index 0000000..867bcbb --- /dev/null +++ b/pkg/cover/delta.go @@ -0,0 +1,56 @@ +/* + Copyright 2020 Qiniu Cloud (七牛云) + + 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 + +type GroupChanges struct { + Added []Coverage + Deleted []Coverage + Unchanged []Coverage + Changed []Incremental + BaseGroup *CoverageList + NewGroup *CoverageList +} + +type Incremental struct { + base Coverage + new Coverage +} + +func GenLocalCoverDiffReport(newList *CoverageList, baseList *CoverageList) [][]string { + var rows [][]string + basePMap := baseList.Map() + + for _, l := range newList.Groups { + baseCov, ok := basePMap[l.Name()] + if !ok { + rows = append(rows, []string{l.FileName, "None", l.Percentage(), PercentStr(Delta(l, baseCov))}) + continue + } + if l.Percentage() == baseCov.Percentage() { + continue + } + rows = append(rows, []string{l.FileName, baseCov.Percentage(), l.Percentage(), PercentStr(Delta(l, baseCov))}) + } + + return rows +} + +func Delta(new Coverage, base Coverage) float32 { + baseRatio, _ := base.Ratio() + newRatio, _ := new.Ratio() + return newRatio - baseRatio +} diff --git a/pkg/cover/instrument.go b/pkg/cover/instrument.go new file mode 100644 index 0000000..4bafccb --- /dev/null +++ b/pkg/cover/instrument.go @@ -0,0 +1,368 @@ +/* + Copyright 2020 Qiniu Cloud (七牛云) + + 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 ( + "fmt" + "os" + "path" + "text/template" +) + +// InjectCountersHandlers generate a file _cover_http_apis.go besides the main.go file +func InjectCountersHandlers(tc TestCover, dest string) error { + f, err := os.Create(dest) + if err != nil { + return err + } + if err := coverMainTmpl.Execute(f, tc); err != nil { + return err + } + return nil +} + +var coverMainTmpl = template.Must(template.New("coverMain").Parse(coverMain)) + +const coverMain = ` +// Code generated by goc system. DO NOT EDIT. + +package main + +import ( + "bufio" + "fmt" + "io" + "io/ioutil" + "log" + "net" + "net/http" + "os" + "strings" + "sync/atomic" + "testing" + + {{range $i, $pkgCover := .DepsCover}} + _cover{{$i}} {{$pkgCover.Package.ImportPath | printf "%q"}} + {{end}} + + {{range $k, $pkgCover := .CacheCover}} + {{$pkgCover.Package.ImportPath | printf "%q"}} + {{end}} + +) + +func init() { + go registerHandlers() +} + +func loadValues() (map[string][]uint32, map[string][]testing.CoverBlock) { + var ( + coverCounters = make(map[string][]uint32) + coverBlocks = make(map[string][]testing.CoverBlock) + ) + + {{range $i, $pkgCover := .DepsCover}} + {{range $file, $cover := $pkgCover.Vars}} + loadFileCover(coverCounters, coverBlocks, {{printf "%q" $cover.File}}, _cover{{$i}}.{{$cover.Var}}.Count[:], _cover{{$i}}.{{$cover.Var}}.Pos[:], _cover{{$i}}.{{$cover.Var}}.NumStmt[:]) + {{end}} + {{end}} + + {{range $file, $cover := .MainPkgCover.Vars}} + loadFileCover(coverCounters, coverBlocks, {{printf "%q" $cover.File}}, {{$cover.Var}}.Count[:], {{$cover.Var}}.Pos[:], {{$cover.Var}}.NumStmt[:]) + {{end}} + + {{range $k, $pkgCover := .CacheCover}} + {{range $v, $cover := $pkgCover.Vars}} + loadFileCover(coverCounters, coverBlocks, {{printf "%q" $cover.File}}, {{$pkgCover.Package.Name}}.{{$v}}.Count[:], {{$pkgCover.Package.Name}}.{{$v}}.Pos[:], {{$pkgCover.Package.Name}}.{{$v}}.NumStmt[:]) + {{end}} + {{end}} + + return coverCounters, coverBlocks +} + +func loadFileCover(coverCounters map[string][]uint32, coverBlocks map[string][]testing.CoverBlock, fileName string, counter []uint32, pos []uint32, numStmts []uint16) { + if 3*len(counter) != len(pos) || len(counter) != len(numStmts) { + panic("coverage: mismatched sizes") + } + if coverCounters[fileName] != nil { + // Already registered. + return + } + coverCounters[fileName] = counter + block := make([]testing.CoverBlock, len(counter)) + for i := range counter { + block[i] = testing.CoverBlock{ + Line0: pos[3*i+0], + Col0: uint16(pos[3*i+2]), + Line1: pos[3*i+1], + Col1: uint16(pos[3*i+2] >> 16), + Stmts: numStmts[i], + } + } + coverBlocks[fileName] = block +} + +func registerHandlers() { + ln, host, err := listen() + if err != nil { + log.Fatalf("profile listen failed, err:%v", err) + } + log.Println("profile listen on", host) + profileAddr := "http://" + host + if resp, err := registerSelf(profileAddr); err != nil { + log.Fatalf("register address %v failed, err: %v, response: %v", profileAddr, err, string(resp)) + } + go genProfileAddr(host) + + mux := http.NewServeMux() + // Coverage reports the current code coverage as a fraction in the range [0, 1]. + // If coverage is not enabled, Coverage returns 0. + mux.HandleFunc("/v1/cover/coverage", func(w http.ResponseWriter, r *http.Request) { + counters, _ := loadValues() + + var n, d int64 + for _, counter := range counters { + for i := range counter { + if atomic.LoadUint32(&counter[i]) > 0 { + n++ + } + d++ + } + } + if d == 0 { + fmt.Fprint(w, 0) + return + } + fmt.Fprintf(w, "%f", float64(n)/float64(d)) + }) + + // coverprofile reports a coverage profile with the coverage percentage + mux.HandleFunc("/v1/cover/profile", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "mode: atomic\n") + counters, blocks := loadValues() + + var active, total int64 + var count uint32 + for name, counts := range counters { + block := blocks[name] + for i := range counts { + stmts := int64(block[i].Stmts) + total += stmts + count = atomic.LoadUint32(&counts[i]) // For -mode=atomic. + if count > 0 { + active += stmts + } + _, err := fmt.Fprintf(w, "%s:%d.%d,%d.%d %d %d\n", name, + block[i].Line0, block[i].Col0, + block[i].Line1, block[i].Col1, + stmts, + count) + if err != nil { + fmt.Fprintf(w, "invalid block format, err: %v", err) + return + } + } + } + }) + + mux.HandleFunc("/v1/cover/clear", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "TO BE IMPLEMENTED!\n") + }) + + log.Fatal(http.Serve(ln, mux)) +} + +func registerSelf(address string) ([]byte, error) { + req, err := http.NewRequest("POST", fmt.Sprintf("%s/v1/cover/register?name=%s&address=%s", {{.Center | printf "%q"}}, os.Args[0], address), nil) + if err != nil { + log.Fatalf("http.NewRequest failed: %v", err) + return nil, err + } + + resp, err := http.DefaultClient.Do(req) + if err != nil && isNetworkError(err) { + log.Printf("[WARN]error occured:%v, try again", err) + resp, err = http.DefaultClient.Do(req) + } + defer resp.Body.Close() + + if err != nil { + return nil, fmt.Errorf("registed faile, err:%v", err) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body, err:%v", err) + } + + if resp.StatusCode != 200 { + err = fmt.Errorf("registed failed, response code %d", resp.StatusCode) + } + + return body, err +} + +func isNetworkError(err error) bool { + if err == io.EOF { + return true + } + _, ok := err.(net.Error) + return ok +} + +func listen() (ln net.Listener, host string, err error) { + // 获取上次使用的监听地址 + if previousAddr := getPreviousAddr(); previousAddr != "" { + ss := strings.Split(previousAddr, ":") + // listen on all network interface + ln, err = net.Listen("tcp4", ":"+ss[len(ss)-1]) + if err == nil { + host = previousAddr + return + } + } + + ln, err = net.Listen("tcp4", ":0") + if err != nil { + return + } + + adds, err := net.InterfaceAddrs() + if err != nil { + return + } + + var localIPV4 string + var nonLocalIPV4 string + for _, addr := range adds { + if ipNet, ok := addr.(*net.IPNet); ok && ipNet.IP.To4() != nil { + if ipNet.IP.IsLoopback() { + localIPV4 = ipNet.IP.String() + } else { + nonLocalIPV4 = ipNet.IP.String() + } + } + } + if nonLocalIPV4 != "" { + host = fmt.Sprintf("%s:%d", nonLocalIPV4, ln.Addr().(*net.TCPAddr).Port) + } else { + host = fmt.Sprintf("%s:%d", localIPV4, ln.Addr().(*net.TCPAddr).Port) + } + return +} + +func getPreviousAddr() string { + file, err := os.Open(os.Args[0] + "_profile_listen_addr") + if err != nil { + return "" + } + defer file.Close() + + reader := bufio.NewReader(file) + addr, _, _ := reader.ReadLine() + return string(addr) +} + +func genProfileAddr(profileAddr string) { + fn := os.Args[0] + "_profile_listen_addr" + f, err := os.OpenFile(fn, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + log.Println(err) + return + } + defer f.Close() + + fmt.Fprintf(f, strings.TrimPrefix(profileAddr, "http://")) +} +` + +var coverParentFileTmpl = template.Must(template.New("coverParentFileTmpl").Parse(coverParentFile)) + +const coverParentFile = ` +// Code generated by goc system. DO NOT EDIT. + +package {{.}} + +` + +var coverParentVarsTmpl = template.Must(template.New("coverParentVarsTmpl").Parse(coverParentVars)) + +const coverParentVars = ` + +import ( + + {{range $i, $pkgCover := .}} + _cover{{$i}} {{$pkgCover.Package.ImportPath | printf "%q"}} + {{end}} + +) + +{{range $i, $pkgCover := .}} +{{range $v, $cover := $pkgCover.Vars}} +var {{$v}} = &_cover{{$i}}.{{$cover.Var}} +{{end}} +{{end}} + +` + +func InjectCacheCounters(covers map[string][]*PackageCover, cache map[string]*PackageCover) []error { + var errs []error + for k, v := range covers { + if pkg, ok := cache[k]; ok { + err := checkCacheDir(pkg.Package.Dir) + if err != nil { + errs = append(errs, err) + continue + } + _, pkgName := path.Split(k) + err = injectCache(v, pkgName, fmt.Sprintf("%s/%s", pkg.Package.Dir, pkg.Package.GoFiles[0])) + if err != nil { + errs = append(errs, err) + continue + } + } + } + return errs +} + +// InjectCacheCounters generate a file _cover_http_apis.go besides the main.go file +func injectCache(covers []*PackageCover, pkg, dest string) error { + f, err := os.Create(dest) + if err != nil { + return err + } + + if err := coverParentFileTmpl.Execute(f, pkg); err != nil { + return err + } + + if err := coverParentVarsTmpl.Execute(f, covers); err != nil { + return err + } + return nil +} + +func checkCacheDir(p string) error { + _, err := os.Stat(p) + if os.IsNotExist(err) { + err := os.Mkdir(p, 0755) + if err != nil { + return err + } + } + return nil +} diff --git a/tests/e2e/e2e_suite_test.go b/tests/e2e/e2e_suite_test.go new file mode 100644 index 0000000..c83e602 --- /dev/null +++ b/tests/e2e/e2e_suite_test.go @@ -0,0 +1,29 @@ +/* + Copyright 2020 Qiniu Cloud (七牛云) + + 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 e2e_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestE2e(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "E2E goc Suite") +} diff --git a/tests/e2e/simple_project_test.go b/tests/e2e/simple_project_test.go new file mode 100644 index 0000000..ae11c6c --- /dev/null +++ b/tests/e2e/simple_project_test.go @@ -0,0 +1,167 @@ +/* + Copyright 2020 Qiniu Cloud (七牛云) + + 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 e2e_test + +import ( + "fmt" + "os" + "os/exec" + "strings" + "time" + + "path/filepath" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/qiniu/goc/pkg/build" +) + +var TESTS_ROOT string + +var _ = BeforeSuite(func() { + TESTS_ROOT, _ = os.Getwd() + By("Current working directory: " + TESTS_ROOT) + TESTS_ROOT = filepath.Join(TESTS_ROOT, "..") +}) + +var _ = Describe("E2E", func() { + var GOPATH string + + BeforeEach(func() { + GOPATH = os.Getenv("GOPATH") + // in GitHub Action, this value is empty + if GOPATH == "" { + GOPATH = filepath.Join(os.Getenv("HOME"), "go") + } + }) + + Context("Go module", func() { + It("Simple project", func() { + startTime := time.Now() + + By("goc build") + testProjDir := filepath.Join(TESTS_ROOT, "samples/simple_project") + cmd := exec.Command("goc", "build") + cmd.Dir = testProjDir + + out, err := cmd.CombinedOutput() + Expect(err).To(BeNil(), "goc build on this project should be successful", string(out)) + + By("goc install") + testProjDir = filepath.Join(TESTS_ROOT, "samples/simple_project") + cmd = exec.Command("goc", "install", "./...") + cmd.Dir = testProjDir + + out, err = cmd.CombinedOutput() + Expect(err).To(BeNil(), "goc install on this project should be successful", string(out)) + + By("check files in generated temporary directory") + tempDir := filepath.Join(os.TempDir(), build.TmpFolderName(testProjDir)) + _, err = os.Lstat(tempDir) + Expect(err).To(BeNil(), "projects should be copied to temporary directory") + + By("check if cover variables are injected") + _, err = os.Lstat(filepath.Join(tempDir, "http_cover_apis_auto_generated.go")) + Expect(err).To(BeNil(), "a http server file should be generated") + + By("check generated binary") + objects := []string{GOPATH + "/bin", testProjDir} + for _, dir := range objects { + obj := filepath.Join(dir, "simple-project") + fInfo, err := os.Lstat(obj) + Expect(err).To(BeNil()) + Expect(startTime.Before(fInfo.ModTime())).To(Equal(true), "new binary should be generated, not the old one") + + cmd := exec.Command("go", "tool", "objdump", "simple-project") + cmd.Dir = dir + out, err = cmd.CombinedOutput() + 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") + + cnt = strings.Count(string(out), "main.registerSelf") + Expect(cnt).To(BeNumerically(">", 0), "main.registerSelf function should be in the binary") + } + }) + }) + + Context("GOPATH", func() { + var GOPATH string + + BeforeEach(func() { + GOPATH = os.Getenv("GOPATH") + }) + + It("Simple GOPATH project", func() { + startTime := time.Now() + testProjDir := filepath.Join(TESTS_ROOT, "samples/simple_gopath_project") + oriWorkingDir := filepath.Join(testProjDir, "src/qiniu.com/simple_gopath_project") + GOPATH = testProjDir + + By("goc build") + cmd := exec.Command("goc", "build") + cmd.Dir = oriWorkingDir + // use GOPATH mode to compile project + cmd.Env = append(os.Environ(), fmt.Sprintf("GOPATH=%v", GOPATH), "GO111MODULE=off") + + out, err := cmd.CombinedOutput() + Expect(err).To(BeNil(), "goc build on this project should be successful", string(out), cmd.Dir) + + By("goc install") + testProjDir = filepath.Join(TESTS_ROOT, "samples/simple_gopath_project") + cmd = exec.Command("goc", "install", "./...") + 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") + + out, err = cmd.CombinedOutput() + Expect(err).To(BeNil(), "goc install on this project should be successful", string(out)) + + By("check files in generated temporary directory") + tempDir := filepath.Join(os.TempDir(), build.TmpFolderName(oriWorkingDir)) + _, err = os.Lstat(tempDir) + Expect(err).To(BeNil(), "projects should be copied to temporary directory") + + By("check if cover variables are injected") + newWorkingDir := filepath.Join(tempDir, "src/qiniu.com/simple_gopath_project") + _, err = os.Lstat(filepath.Join(newWorkingDir, "http_cover_apis_auto_generated.go")) + Expect(err).To(BeNil(), "a http server file should be generated") + + By("check generated binary") + objects := []string{GOPATH + "/bin", oriWorkingDir} + for _, dir := range objects { + obj := filepath.Join(dir, "simple_gopath_project") + fInfo, err := os.Lstat(obj) + Expect(err).To(BeNil()) + Expect(startTime.Before(fInfo.ModTime())).To(Equal(true), "new binary should be generated, not the old one") + + cmd := exec.Command("go", "tool", "objdump", "simple_gopath_project") + cmd.Dir = dir + out, err = cmd.CombinedOutput() + 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") + + cnt = strings.Count(string(out), "main.registerSelf") + Expect(cnt).To(BeNumerically(">", 0), "main.registerSelf function should be in the binary") + } + + }) + }) +}) diff --git a/tests/run-ci-actions.sh b/tests/run-ci-actions.sh new file mode 100755 index 0000000..62d0085 --- /dev/null +++ b/tests/run-ci-actions.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# Copyright 2020 Qiniu Cloud (七牛云) +# +# 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. + +set -ex + +chmod +x /home/runner/tools/goc/goc +export PATH=/home/runner/tools/goc:$PATH + +chmod +x /home/runner/tools/e2e.test/e2e.test +export PATH=/home/runner/tools/e2e.test:$PATH + +cd e2e +e2e.test ./... \ No newline at end of file diff --git a/tests/samples/simple_gopath_project/go.mod b/tests/samples/simple_gopath_project/go.mod new file mode 100644 index 0000000..e69de29 diff --git a/tests/samples/simple_gopath_project/src/qiniu.com/simple_gopath_project/main.go b/tests/samples/simple_gopath_project/src/qiniu.com/simple_gopath_project/main.go new file mode 100644 index 0000000..01a2bca --- /dev/null +++ b/tests/samples/simple_gopath_project/src/qiniu.com/simple_gopath_project/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "fmt" +) + +func main() { + fmt.Println("hello, world.") +} diff --git a/tests/samples/simple_project/go.mod b/tests/samples/simple_project/go.mod new file mode 100644 index 0000000..610643a --- /dev/null +++ b/tests/samples/simple_project/go.mod @@ -0,0 +1,3 @@ +module example.com/simple-project + +go 1.11 diff --git a/tests/samples/simple_project/main.go b/tests/samples/simple_project/main.go new file mode 100644 index 0000000..01a2bca --- /dev/null +++ b/tests/samples/simple_project/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "fmt" +) + +func main() { + fmt.Println("hello, world.") +}