Repository: goplus/gop
Pull request #968 (2021-11-29 07:44:08)

Goproot


Patch:
diff --git a/README.md b/README.md
index f05d2e60..448280a2 100644
--- a/README.md
+++ b/README.md
@@ -20,12 +20,9 @@ For now, we suggest you install Go+ from source code.
 ```bash
 git clone https://github.com/goplus/gop.git
 cd gop
-./all.bash
+go run cmd/install.go
 ```
 
-Actually, `all.bash` script will use `go run cmd/install.go` underneath.
-
-
 ## Code style (important)
 
 * https://tutorial.goplus.org/hello-world
diff --git a/README_zh.md b/README_zh.md
index 03734118..03c31a39 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -20,7 +20,7 @@
 ```bash
 git clone https://github.com/goplus/gop.git
 cd gop
-./all.bash
+go run cmd/install.go
 ```
 
 ## 代码风格（很重要）
diff --git a/all.bash b/all.bash
index 0cea3680..b8867cf4 100755
--- a/all.bash
+++ b/all.bash
@@ -18,4 +18,4 @@
 
 set -ex
 
-go run cmd/install.go --install --autoproxy
+go run cmd/install.go --install
diff --git a/cmd/install.go b/cmd/install.go
index 6b987934..dd67b30c 100644
--- a/cmd/install.go
+++ b/cmd/install.go
@@ -42,22 +42,19 @@ func getGopLocalLink() string {
 	return filepath.Join(path, "gop")
 }
 
+var gopRoot = getcwd()
+var gopLocalLink = getGopLocalLink()
+
 func checkPathExist(path string) bool {
 	_, err := os.Stat(path)
 	return !os.IsNotExist(err)
 }
 
-var gopRoot = getcwd()
-var gopLocalLink = getGopLocalLink()
-var initCommandExecuteEnv = os.Environ()
-var commandExecuteEnv = initCommandExecuteEnv
-
 func execCommand(command string, arg ...string) (string, string, error) {
 	var stdout, stderr bytes.Buffer
 	cmd := exec.Command(command, arg...)
 	cmd.Stdout = &stdout
 	cmd.Stderr = &stderr
-	cmd.Env = commandExecuteEnv
 	err := cmd.Run()
 	return stdout.String(), stderr.String(), err
 }
@@ -108,7 +105,8 @@ func findTag(commit string) string {
 }
 
 func getGopBuildFlags() string {
-	buildFlags := fmt.Sprintf("-X github.com/goplus/gop/env.buildDate=%s", getBuildDateTime())
+	buildFlags := fmt.Sprintf("-X github.com/goplus/gop/env.defaultGopRoot=%s", gopRoot)
+	buildFlags = fmt.Sprintf("-X github.com/goplus/gop/env.buildDate=%s", getBuildDateTime())
 	if commit, ok := getGitInfo(); ok {
 		buildFlags += fmt.Sprintf(" -X github.com/goplus/gop/env.buildCommit=%s", commit)
 		if branch := getBuildBranch(); branch != "" {
@@ -136,7 +134,7 @@ func detectGoBinPath() string {
 	return filepath.Join(homeDir, "go", "bin")
 }
 
-func buildGoplusTools(useGoProxy bool) {
+func buildGoplusTools() {
 	commandsDir := filepath.Join(gopRoot, "cmd")
 	if !checkPathExist(commandsDir) {
 		println("Error: This script should be run at the root directory of gop repository.")
@@ -145,13 +143,6 @@ func buildGoplusTools(useGoProxy bool) {
 
 	buildFlags := getGopBuildFlags()
 
-	if useGoProxy {
-		println("Info: we will use goproxy.cn as a Go proxy to accelerate installing process.")
-		commandExecuteEnv = append(commandExecuteEnv,
-			"GOPROXY=https://goproxy.cn,direct",
-		)
-	}
-
 	println("Installing Go+ tools...")
 	os.Chdir(commandsDir)
 	buildOutput, buildErr, err := execCommand("go", "install", "-v", "-ldflags", buildFlags, "./...")
@@ -185,19 +176,24 @@ func runTestcases() {
 
 	path, _ := os.LookupEnv("PATH")
 	path = fmt.Sprintf("%s:", detectGoBinPath()) + path
-	commandExecuteEnv = append(commandExecuteEnv, "PATH="+path)
 
-	coverage := "-coverprofile=coverage.txt"
-	testOutput, testErr, err := execCommand("gop", "test", "-v", coverage, "-covermode=atomic", "./...")
-	println(testOutput)
-	println(testErr)
+	cmd := exec.Command("gop", "test", "-v", "-coverprofile=coverage.txt", "-covermode=atomic", "./...")
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	cmd.Env = append(os.Environ(), "PATH="+path)
+	err := cmd.Run()
 	if err != nil {
 		println(err.Error())
 	}
-
 	println("End running testcases.")
 }
 
+func localInstall() {
+	buildGoplusTools()
+	linkGoplusToLocal()
+	println("Go+ is now installed.")
+}
+
 func localUninstall() {
 	println("Uninstalling Go+ and related tools.")
 
@@ -221,41 +217,18 @@ func localUninstall() {
 	println("Go+ and related tools uninstalled successfully.")
 }
 
-func isInChina() bool {
-	const prefix = "LANG=\""
-	out, errMsg, err := execCommand("locale")
-	if err != nil || errMsg != "" {
-		return false
-	}
-	if strings.HasPrefix(out, prefix) {
-		out = out[len(prefix):]
-		return strings.HasPrefix(out, "zh_CN") || strings.HasPrefix(out, "zh_HK")
-	}
-	return false
-}
-
 func main() {
 	isInstall := flag.Bool("install", false, "Install Go+")
 	isBuild := flag.Bool("build", false, "Build the Go+")
 	isTest := flag.Bool("test", false, "Run testcases")
 	isUninstall := flag.Bool("uninstall", false, "Uninstall Go+")
-	isGoProxy := flag.Bool("proxy", false, "Set GOPROXY for people in China")
-	isAutoProxy := flag.Bool("autoproxy", false, "Check to set GOPROXY automatically")
 
 	flag.Parse()
 
-	useGoProxy := *isGoProxy
-	if !useGoProxy && *isAutoProxy {
-		useGoProxy = isInChina()
-	}
 	flagActionMap := map[*bool]func(){
-		isInstall: func() {
-			buildGoplusTools(useGoProxy)
-			linkGoplusToLocal()
-			println("Go+ is now installed.")
-		},
+		isInstall:   localInstall,
 		isTest:      runTestcases,
-		isBuild:     func() { buildGoplusTools(useGoProxy) },
+		isBuild:     buildGoplusTools,
 		isUninstall: localUninstall,
 	}
 
diff --git a/cmd/internal/build/build.go b/cmd/internal/build/build.go
index d8b4dbbb..cc72ba4b 100644
--- a/cmd/internal/build/build.go
+++ b/cmd/internal/build/build.go
@@ -44,7 +44,7 @@ var (
 )
 
 func init() {
-	flag.StringVar(&flagBuildOutput, "o", "", "gop build output file")
+	flag.StringVar(&flagBuildOutput, "o", "", "go build output file")
 	Cmd.Run = runCmd
 }
 
diff --git a/cmd/internal/help/help.go b/cmd/internal/help/help.go
index 037d81cd..52468310 100644
--- a/cmd/internal/help/help.go
+++ b/cmd/internal/help/help.go
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-// Package help implements the ``gop help'' command.
+// Package help implements the ``go help'' command.
 package help
 
 import (
@@ -27,9 +27,8 @@ import (
 	"unicode"
 	"unicode/utf8"
 
-	"github.com/qiniu/x/log"
-
 	"github.com/goplus/gop/cmd/internal/base"
+	"github.com/qiniu/x/log"
 )
 
 // Help implements the 'help' command.
diff --git a/cmd/internal/mod/init.go b/cmd/internal/mod/init.go
index a3f14e85..8ba3448b 100644
--- a/cmd/internal/mod/init.go
+++ b/cmd/internal/mod/init.go
@@ -26,7 +26,7 @@ import (
 )
 
 var cmdInit = &base.Command{
-	UsageLine: "gop mod init [module]",
+	UsageLine: "go mod init [module]",
 	Short:     "initialize new module in current directory",
 }
 
diff --git a/cmd/internal/modload/modload.go b/cmd/internal/modload/modload.go
index 936e5e6a..3c428c8b 100644
--- a/cmd/internal/modload/modload.go
+++ b/cmd/internal/modload/modload.go
@@ -174,10 +174,10 @@ func findModulePath(dir string) (string, error) {
 	msg := `cannot determine module path for source directory %s (%s)
 
 Example usage:
-	'gop mod init example.com/m' to initialize a v0 or v1 module
-	'gop mod init example.com/m/v2' to initialize a v2 module
+	'go mod init example.com/m' to initialize a v0 or v1 module
+	'go mod init example.com/m/v2' to initialize a v2 module
 
-Run 'gop help mod init' for more information.
+Run 'go help mod init' for more information.
 `
 	return "", fmt.Errorf(msg, dir, reason)
 }
diff --git a/cmd/internal/run/run.go b/cmd/internal/run/run.go
index f226c4b0..3edc5072 100644
--- a/cmd/internal/run/run.go
+++ b/cmd/internal/run/run.go
@@ -33,6 +33,7 @@ import (
 	"github.com/goplus/gop/cmd/gengo"
 	"github.com/goplus/gop/cmd/internal/base"
 	"github.com/goplus/gop/cmd/internal/modload"
+	"github.com/goplus/gop/env"
 	"github.com/goplus/gop/parser"
 	"github.com/goplus/gop/scanner"
 	"github.com/goplus/gop/token"
@@ -78,16 +79,10 @@ func saveGoFile(gofile string, pkg *gox.Package) error {
 
 func findGoModFile(dir string) (modfile string, noCacheFile bool, err error) {
 	modfile, err = cl.FindGoModFile(dir)
+
 	if err != nil {
-		home := os.Getenv("HOME")
-		modfile = home + "/gop/go.mod"
-		if fi, e := os.Lstat(modfile); e == nil && !fi.IsDir() {
-			return modfile, true, nil
-		}
-		modfile = home + "/goplus/go.mod"
-		if fi, e := os.Lstat(modfile); e == nil && !fi.IsDir() {
-			return modfile, true, nil
-		}
+		modfile = filepath.Join(env.GOPROOT(), "go.mod")
+		return modfile, true, nil
 	}
 	return
 }
diff --git a/env/path.go b/env/path.go
new file mode 100644
index 00000000..f3919a1e
--- /dev/null
+++ b/env/path.go
@@ -0,0 +1,121 @@
+/*
+ * Copyright (c) 2021 The GoPlus Authors (goplus.org). All rights reserved.
+ *
+ * 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 env
+
+import (
+	"log"
+	"os"
+	"path/filepath"
+	"syscall"
+)
+
+var (
+	// This is set by the linker.
+	defaultGopRoot string
+)
+
+func GOPROOT() string {
+	gopRoot, err := findGopRoot()
+	if err != nil {
+		log.Panicln("GOPROOT not found:", err)
+	}
+	return gopRoot
+}
+
+const (
+	errNotFound = syscall.ENOENT
+)
+
+const (
+	envGOPROOT = "GOPROOT"
+	envHOME    = "HOME"
+)
+
+func findGopRoot() (string, error) {
+	envGopRoot := os.Getenv(envGOPROOT)
+	if envGopRoot != "" {
+		// GOPROOT must valid
+		if isValidGopRoot(envGopRoot) {
+			return envGopRoot, nil
+		}
+		log.Panicf("\n%s (%s) is not valid\n", envGOPROOT, envGopRoot)
+	}
+
+	// if parent directory is a valid gop root, use it
+	exePath, err := executableRealPath()
+	if err == nil {
+		dir := filepath.Dir(exePath)
+		parentDir := filepath.Dir(dir)
+		if parentDir != dir && isValidGopRoot(parentDir) {
+			return parentDir, nil
+		}
+	}
+
+	// check build.GopRoot, if it is valid, use it
+	if defaultGopRoot != "" && isValidGopRoot(defaultGopRoot) {
+		return defaultGopRoot, nil
+	}
+
+	// Compatible with old GOPROOT
+	if home := os.Getenv(envHOME); home != "" {
+		gopRoot := filepath.Join(home, "gop")
+		if isValidGopRoot(gopRoot) {
+			return gopRoot, nil
+		}
+		goplusRoot := filepath.Join(home, "goplus")
+		if isValidGopRoot(goplusRoot) {
+			return goplusRoot, nil
+		}
+	}
+	return "", errNotFound
+}
+
+// Mockable for testing.
+var executable = func() (string, error) {
+	return os.Executable()
+}
+
+func executableRealPath() (path string, err error) {
+	path, err = executable()
+	if err == nil {
+		path, err = filepath.EvalSymlinks(path)
+		if err == nil {
+			path, err = filepath.Abs(path)
+		}
+	}
+	return
+}
+
+func isFileExists(path string) bool {
+	st, err := os.Stat(path)
+	return err == nil && !st.IsDir()
+}
+
+func isDirExists(path string) bool {
+	st, err := os.Stat(path)
+	return err == nil && st.IsDir()
+}
+
+// A valid gop root has go.mod, go.sum, builtin/
+func isValidGopRoot(path string) bool {
+	if !isFileExists(filepath.Join(path, "go.mod")) ||
+		!isFileExists(filepath.Join(path, "go.sum")) ||
+		!isDirExists(filepath.Join(path, "builtin")) {
+		return false
+	}
+	return true
+}
diff --git a/go.mod b/go.mod
index a9214761..9dd541e4 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/goplus/gop
 go 1.16
 
 require (
-	github.com/goplus/gox v1.8.0
+	github.com/goplus/gox v1.7.20
 	github.com/qiniu/x v1.11.5
 	golang.org/x/mod v0.5.1
 	golang.org/x/tools v0.1.7
diff --git a/go.sum b/go.sum
index 5d556051..e5acaaf2 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,5 @@
-github.com/goplus/gox v1.8.0 h1:54OTOoFpAqxU55ZI4EBs8EzDzGw0yzPHSOxgr+U5iT0=
-github.com/goplus/gox v1.8.0/go.mod h1:fUMG5YrEvPjSfGqPnP1gfrZvWlVuA1/9lZy3s/5+Aes=
+github.com/goplus/gox v1.7.20 h1:lzF8X52vTRYTV3tTLWFmP+BtU+VnXHNdqPpLjpNqg24=
+github.com/goplus/gox v1.7.20/go.mod h1:fUMG5YrEvPjSfGqPnP1gfrZvWlVuA1/9lZy3s/5+Aes=
 github.com/qiniu/x v1.11.5 h1:TYr5cl4g2yoHAZeDK4MTjKF6CMoG+IHlCDvvM5qym6U=
 github.com/qiniu/x v1.11.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs=
 github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
diff --git a/x/mod/modfile/rule.go b/x/mod/modfile/rule.go
index 8c5ead70..fa490712 100644
--- a/x/mod/modfile/rule.go
+++ b/x/mod/modfile/rule.go
@@ -395,7 +395,7 @@ func (f *File) add(errs *ErrorList, block *LineBlock, line *Line, verb string, a
 			errorf("gop directive expects exactly one argument")
 			return
 		} else if !GopVersionRE.MatchString(args[0]) {
-			errorf("invalid gop version '%s': must match format 1.23", args[0])
+			errorf("invalid go version '%s': must match format 1.23", args[0])
 			return
 		}
 


Test patch:
diff --git a/env/path_test.go b/env/path_test.go
new file mode 100644
index 00000000..f11d3bb6
--- /dev/null
+++ b/env/path_test.go
@@ -0,0 +1,378 @@
+/*
+ * Copyright (c) 2021 The GoPlus Authors (goplus.org). All rights reserved.
+ *
+ * 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 env
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/goplus/gop/cl"
+)
+
+func findGoModFile(dir string) (modfile string, noCacheFile bool, err error) {
+	modfile, err = cl.FindGoModFile(dir)
+
+	if err != nil {
+		gopRoot, err := findGopRoot()
+		if err == nil {
+			modfile = filepath.Join(gopRoot, "go.mod")
+			return modfile, true, nil
+		}
+	}
+	return
+}
+
+// Common testing directory structure:
+// testing_root/
+//   src/
+//	   subdir/
+//   valid_goproot/
+//	   go.mod
+//	   go.sum
+//     builtin/
+func makeTestDir(t *testing.T) (root string, src string, gopRoot string) {
+	root, _ = filepath.EvalSymlinks(t.TempDir())
+	src = filepath.Join(root, "src")
+	gopRoot = filepath.Join(root, "valid_goproot")
+	makeValidGopRoot(gopRoot)
+	os.Mkdir(src, 0755)
+	return
+}
+
+func makeValidGopRoot(root string) {
+	os.Mkdir(root, 0755)
+	os.Mkdir(filepath.Join(root, "builtin"), 0755)
+	ioutil.WriteFile(filepath.Join(root, "go.mod"), []byte(""), 0644)
+	ioutil.WriteFile(filepath.Join(root, "go.sum"), []byte(""), 0644)
+}
+
+func writeDummyFile(path string) {
+	ioutil.WriteFile(path, []byte(""), 0644)
+}
+
+func cleanup() {
+	os.Setenv("GOPROOT", "")
+	os.Setenv("HOME", "")
+	defaultGopRoot = ""
+}
+
+func TestBasic(t *testing.T) {
+	if GOPROOT() == "" {
+		t.Fatal("TestBasic failed")
+	}
+}
+
+func TestFindGoModFileInGoModDir(t *testing.T) {
+	cleanup()
+
+	t.Run("the src/ is a valid mod dir", func(tt *testing.T) {
+		tt.Cleanup(cleanup)
+		_, src, _ := makeTestDir(tt)
+		subdir := filepath.Join(src, "subdir")
+		writeDummyFile(filepath.Join(src, "go.mod"))
+		os.Mkdir(subdir, 0755)
+
+		{
+			modfile, noCacheFile, err := findGoModFile(src)
+
+			if err != nil || modfile != filepath.Join(src, "go.mod") || noCacheFile {
+				tt.Fatal("got:", modfile, noCacheFile, err)
+			}
+		}
+
+		{
+			// Should found go.mod in parent dir
+			modfile, noCacheFile, err := findGoModFile(subdir)
+
+			if err != nil || modfile != filepath.Join(src, "go.mod") || noCacheFile {
+				tt.Fatal("got:", modfile, noCacheFile, err)
+			}
+		}
+	})
+
+	t.Run("the src/ is not a valid mod dir", func(tt *testing.T) {
+		tt.Cleanup(cleanup)
+		_, src, _ := makeTestDir(tt)
+
+		modfile, noCacheFile, err := findGoModFile(src)
+
+		if err == nil {
+			tt.Fatal("should not found the mod file, but got:", modfile, noCacheFile)
+		}
+	})
+}
+
+func TestFindGoModFileInGopRoot(t *testing.T) {
+	origiExecutable := executable
+	t.Cleanup(func() {
+		executable = origiExecutable
+	})
+
+	home := t.TempDir()
+	bin := filepath.Join(home, "bin")
+
+	// Don't find go.mod in gop source dir when testing
+	os.Chdir(home)
+
+	cleanupAll := func() {
+		cleanup()
+		executable = func() (string, error) {
+			return filepath.Join(bin, "run"), nil
+		}
+	}
+	cleanupAll()
+
+	t.Run("without gop root", func(tt *testing.T) {
+		tt.Cleanup(cleanupAll)
+		root, _, _ := makeTestDir(tt)
+
+		modfile, noCacheFile, err := findGoModFile(root)
+
+		if err == nil || noCacheFile || modfile != "" {
+			tt.Fatal("should not found go.mod without gop root, got:", modfile, noCacheFile, err)
+		}
+	})
+
+	t.Run("set GOPROOT to a valid goproot path", func(tt *testing.T) {
+		tt.Cleanup(cleanupAll)
+		_, src, gopRoot := makeTestDir(tt)
+
+		os.Setenv("GOPROOT", gopRoot)
+		modfile, noCacheFile, err := findGoModFile(src)
+
+		if err != nil || modfile != filepath.Join(gopRoot, "go.mod") || !noCacheFile {
+			tt.Fatal("should found mod file in GOPROOT, got:", modfile, noCacheFile, err)
+		}
+	})
+
+	t.Run("set GOPROOT to an invalid goproot path", func(tt *testing.T) {
+		tt.Cleanup(cleanupAll)
+		root, src, _ := makeTestDir(tt)
+		invalidGopRoot := filepath.Join(root, "invalid_goproot")
+
+		defer func() {
+			r := recover()
+			if r == nil {
+				tt.Fatal("should panic, but not")
+			}
+		}()
+
+		os.Setenv("GOPROOT", invalidGopRoot)
+		findGoModFile(src)
+	})
+
+	t.Run("set build.GopRoot to a valid goproot path", func(tt *testing.T) {
+		tt.Cleanup(cleanupAll)
+		_, src, gopRoot := makeTestDir(tt)
+
+		defaultGopRoot = gopRoot
+		modfile, noCacheFile, err := findGoModFile(src)
+
+		if err != nil || modfile != filepath.Join(gopRoot, "go.mod") || !noCacheFile {
+			tt.Fatal("should found go.mod in the dir of build.GopRoot, got:", modfile, noCacheFile, err)
+		}
+	})
+
+	t.Run("set build.GopRoot to an invalid path", func(tt *testing.T) {
+		tt.Cleanup(cleanupAll)
+		root, src, _ := makeTestDir(tt)
+		invalidGopRoot := filepath.Join(root, "invalid_goproot")
+
+		defaultGopRoot = invalidGopRoot
+		{
+			modfile, noCacheFile, err := findGoModFile(src)
+
+			if err == nil || noCacheFile || modfile != "" {
+				tt.Fatal("should not found go.mod when build.GopRoot isn't exists, got:", modfile, noCacheFile, err)
+			}
+		}
+
+		{
+			os.Mkdir(invalidGopRoot, 0755)
+			modfile, noCacheFile, err := findGoModFile(src)
+
+			if err == nil || noCacheFile || modfile != "" {
+				tt.Fatal("should not found go.mod when build.GopRoot isn't an valid gop root dir, got:", modfile, noCacheFile, err)
+			}
+		}
+	})
+
+	t.Run("use $HOME/gop or $HOME/goplus", func(tt *testing.T) {
+		tt.Cleanup(cleanupAll)
+		root, src, _ := makeTestDir(tt)
+		home := filepath.Join(root, "home")
+		os.Mkdir(home, 0755)
+		os.Setenv("HOME", home)
+
+		{
+			gopRoot := filepath.Join(home, "goplus")
+			makeValidGopRoot(gopRoot)
+
+			modfile, noCacheFile, err := findGoModFile(src)
+
+			if err != nil || !noCacheFile || modfile != filepath.Join(gopRoot, "go.mod") {
+				tt.Fatal("should found go.mod in $HOME/goplus, but got:", modfile, noCacheFile, err)
+			}
+		}
+
+		{
+			gopRoot := filepath.Join(home, "gop")
+			makeValidGopRoot(gopRoot)
+
+			modfile, noCacheFile, err := findGoModFile(src)
+
+			if err != nil || !noCacheFile || modfile != filepath.Join(gopRoot, "go.mod") {
+				tt.Fatal("should found go.mod in $HOME/gop, but got:", modfile, noCacheFile, err)
+			}
+		}
+	})
+
+	t.Run("check if parent dir of the executable is valid gop root", func(tt *testing.T) {
+		tt.Cleanup(cleanupAll)
+		_, src, gopRoot := makeTestDir(tt)
+		bin := filepath.Join(gopRoot, "bin")
+		exePath := filepath.Join(bin, "run")
+		os.Mkdir(bin, 0755)
+		writeDummyFile(exePath)
+
+		// Mock executable location
+		executable = func() (string, error) {
+			return exePath, nil
+		}
+
+		modfile, noCacheFile, err := findGoModFile(src)
+
+		if err != nil || !noCacheFile || modfile != filepath.Join(gopRoot, "go.mod") {
+			tt.Fatal("should found go.mod in gopRoot, but got:", modfile, noCacheFile, err)
+		}
+	})
+
+	t.Run("test gop root priority", func(tt *testing.T) {
+		tt.Cleanup(cleanupAll)
+		root, src, _ := makeTestDir(tt)
+
+		tt.Run("without gop root", func(tt *testing.T) {
+			modfile, noCacheFile, err := findGoModFile(src)
+
+			if err == nil || noCacheFile || modfile != "" {
+				tt.Fatal("should not found go.mod without gop root, got:", modfile, noCacheFile, err)
+			}
+		})
+
+		tt.Run("set HOME but hasn't $HOME/gop/ and $HOME/goplus/", func(tt *testing.T) {
+			os.Setenv("HOME", root)
+
+			modfile, noCacheFile, err := findGoModFile(src)
+
+			if err == nil || noCacheFile || modfile != "" {
+				tt.Fatal("should not found go.mod without gop root, got:", modfile, noCacheFile, err)
+			}
+		})
+
+		tt.Run("set HOME, and has valid $HOME/goplus/", func(tt *testing.T) {
+			gopRoot := filepath.Join(root, "goplus")
+			makeValidGopRoot(gopRoot)
+
+			modfile, noCacheFile, err := findGoModFile(src)
+
+			if err != nil || !noCacheFile || modfile != filepath.Join(gopRoot, "go.mod") {
+				tt.Fatal("should found go.mod in $HOME/goplus, but got:", modfile, noCacheFile, err)
+			}
+		})
+
+		tt.Run("set HOME, and has valid $HOME/gop/", func(tt *testing.T) {
+			gopRoot := filepath.Join(root, "gop")
+			makeValidGopRoot(gopRoot)
+
+			modfile, noCacheFile, err := findGoModFile(src)
+
+			if err != nil || !noCacheFile || modfile != filepath.Join(gopRoot, "go.mod") {
+				tt.Fatal("should found go.mod in $HOME/gop, but got:", modfile, noCacheFile, err)
+			}
+		})
+
+		tt.Run("set build.GopRoot to an invalid gop root dir", func(tt *testing.T) {
+			gopRoot := filepath.Join(root, "gop")
+
+			defaultGopRoot = filepath.Join(root, "invalid_goproot")
+			modfile, noCacheFile, err := findGoModFile(src)
+
+			if err != nil || !noCacheFile || modfile != filepath.Join(gopRoot, "go.mod") {
+				tt.Fatal("should found go.mod in $HOME/gop, but got:", modfile, noCacheFile, err)
+			}
+		})
+
+		tt.Run("set build.GopRoot to a valid gop root dir", func(tt *testing.T) {
+			newGopRoot := filepath.Join(root, "new_gop_root")
+			makeValidGopRoot(newGopRoot)
+
+			defaultGopRoot = newGopRoot
+			modfile, noCacheFile, err := findGoModFile(src)
+
+			if err != nil || !noCacheFile || modfile != filepath.Join(newGopRoot, "go.mod") {
+				tt.Fatal("should found go.mod in new_gop_root/, but got:", modfile, noCacheFile, err)
+			}
+		})
+
+		tt.Run("the executable's parent dir is a valid gop root dir", func(tt *testing.T) {
+			newGopRoot2 := filepath.Join(root, "new_gop_root2")
+			makeValidGopRoot(newGopRoot2)
+			bin := filepath.Join(newGopRoot2, "bin")
+			exePath := filepath.Join(bin, "run")
+			os.Mkdir(bin, 0755)
+			writeDummyFile(exePath)
+			// Mock executable location
+			executable = func() (string, error) {
+				return exePath, nil
+			}
+
+			modfile, noCacheFile, err := findGoModFile(src)
+
+			if err != nil || !noCacheFile || modfile != filepath.Join(newGopRoot2, "go.mod") {
+				tt.Fatal("should found go.mod in new_gop_root2/, but got:", modfile, noCacheFile, err)
+			}
+		})
+
+		tt.Run("set GOPROOT to an invalid gop root dir", func(tt *testing.T) {
+			newGopRoot3 := filepath.Join(root, "new_gop_root3")
+
+			defer func() {
+				r := recover()
+				if r == nil {
+					tt.Fatal("should panic, but not")
+				}
+			}()
+
+			os.Setenv("GOPROOT", newGopRoot3)
+			findGoModFile(src)
+		})
+
+		tt.Run("set GOPROOT to a valid gop root dir", func(tt *testing.T) {
+			newGopRoot4 := filepath.Join(root, "new_gop_root4")
+			makeValidGopRoot(newGopRoot4)
+
+			os.Setenv("GOPROOT", newGopRoot4)
+			modfile, noCacheFile, err := findGoModFile(src)
+
+			if err != nil || !noCacheFile || modfile != filepath.Join(newGopRoot4, "go.mod") {
+				tt.Fatal("should found go.mod in new_gop_root3/, but got:", modfile, noCacheFile, err)
+			}
+		})
+	})
+}
diff --git a/env/version_test.go b/env/version_test.go
new file mode 100644
index 00000000..e4f77726
--- /dev/null
+++ b/env/version_test.go
@@ -0,0 +1,23 @@
+package env
+
+import "testing"
+
+func TestVersion(t *testing.T) {
+	if Version() != "v"+MainVersion+".x" {
+		t.Fatal("TestVersion failed:", Version())
+	}
+	buildVersion = "v1.0.0-beta1"
+	if Version() != buildVersion {
+		t.Fatal("TestVersion failed:", Version())
+	}
+	buildVersion = ""
+}
+
+func TestBuild(t *testing.T) {
+	if BuildCommit() != "" {
+		t.Fatal("BuildCommit failed:", BuildCommit())
+	}
+	if BuildInfo() != "() " {
+		t.Fatal("BuildInfo failed:", BuildInfo())
+	}
+}