Repository: qiniu/goc
Pull request #8 (2020-05-20 02:02:54)

Migrate qbox/goc to qiniu/goc
1. migrate code to public repo
2. add GitHub Action as CI
3. fix some bugs

qiniu-bot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/qiniu/goc/pull/8#" title="Author self-approved">lyyyuna</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=qiniu%2Fgoc).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/qiniu/goc/blob/master/OWNERS)~~ [lyyyuna]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
lyyyuna: /assign @CarlJi 
e06084: lgtm
CarlJi: /lgtm

Patch:
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(&center, "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(&center, "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(&center, "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/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/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
+}


Test patch:
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/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_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/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.")
+}