Skip to content

Commit

Permalink
feat(gexec) Add CompileTest functions. Close #410 (#411)
Browse files Browse the repository at this point in the history
* feat(gexec) Add CompileTest functions. Close #410

Adds following methods:
- CompileTest(packagePath string, args ...string) (compiledPath string, err error)
- CompileTestWithEnvironment(packagePath string, env []string, args ...string) (compiledPath string, err error)
- CompileTestIn(gopath string, packagePath string, args ...string) (compiledPath string, err error)

Signed-off-by: Pierre Péronnet <pierre.peronnet@ovhcloud.com>

* fix(gexec) Binaries for tests are built when needed

This solve the issue in test where suite scoped firefly binary
was removed by CleanupBuildArtifacts() in build_test.go

Signed-off-by: Pierre Péronnet <pierre.peronnet@ovhcloud.com>

* feat(gexec) Add methods to get and compile tests

Adds following methods:
- GetAndCompileTest(packagePath string, args ...string) (compiledPath string, err error)
- GetAndCompileTestWithEnvironment(packagePath string, env []string, args ...string) (compiledPath string, err error)
- GetAndCompileTestIn(gopath string, packagePath string, args ...string) (compiledPath string, err error)

Signed-off-by: Pierre Péronnet <pierre.peronnet@ovhcloud.com>
  • Loading branch information
holyhope committed Feb 25, 2021
1 parent d732d5e commit 47c613f
Show file tree
Hide file tree
Showing 6 changed files with 604 additions and 241 deletions.
7 changes: 7 additions & 0 deletions gexec/_fixture/firefly/main_test.go
@@ -0,0 +1,7 @@
package main_test

import "testing"

func Test(t *testing.T) {
t.Log("Hum, it seems okay.")
}
150 changes: 136 additions & 14 deletions gexec/build.go
Expand Up @@ -3,6 +3,8 @@
package gexec

import (
"crypto/md5"
"encoding/hex"
"errors"
"fmt"
"go/build"
Expand Down Expand Up @@ -46,6 +48,135 @@ func BuildIn(gopath string, packagePath string, args ...string) (compiledPath st
return doBuild(gopath, packagePath, nil, args...)
}

func doBuild(gopath, packagePath string, env []string, args ...string) (compiledPath string, err error) {
executable, err := newExecutablePath(gopath, packagePath)
if err != nil {
return "", err
}

cmdArgs := append([]string{"build"}, args...)
cmdArgs = append(cmdArgs, "-o", executable, packagePath)

build := exec.Command("go", cmdArgs...)
build.Env = replaceGoPath(os.Environ(), gopath)
build.Env = append(build.Env, env...)

output, err := build.CombinedOutput()
if err != nil {
return "", fmt.Errorf("Failed to build %s:\n\nError:\n%s\n\nOutput:\n%s", packagePath, err, string(output))
}

return executable, nil
}

/*
CompileTest uses go test to compile the test package at packagePath. The resulting binary is saved off in a temporary directory.
A path pointing to this binary is returned.
CompileTest uses the $GOPATH set in your environment. If $GOPATH is not set and you are using Go 1.8+,
it will use the default GOPATH instead. It passes the variadic args on to `go test`.
*/
func CompileTest(packagePath string, args ...string) (compiledPath string, err error) {
return doCompileTest(build.Default.GOPATH, packagePath, nil, args...)
}

/*
GetAndCompileTest is identical to CompileTest but `go get` the package before compiling tests.
*/
func GetAndCompileTest(packagePath string, args ...string) (compiledPath string, err error) {
if err := getForTest(build.Default.GOPATH, packagePath, nil); err != nil {
return "", err
}

return doCompileTest(build.Default.GOPATH, packagePath, nil, args...)
}

/*
CompileTestWithEnvironment is identical to CompileTest but allows you to specify env vars to be set at build time.
*/
func CompileTestWithEnvironment(packagePath string, env []string, args ...string) (compiledPath string, err error) {
return doCompileTest(build.Default.GOPATH, packagePath, env, args...)
}

/*
GetAndCompileTestWithEnvironment is identical to GetAndCompileTest but allows you to specify env vars to be set at build time.
*/
func GetAndCompileTestWithEnvironment(packagePath string, env []string, args ...string) (compiledPath string, err error) {
if err := getForTest(build.Default.GOPATH, packagePath, env); err != nil {
return "", err
}

return doCompileTest(build.Default.GOPATH, packagePath, env, args...)
}

/*
CompileTestIn is identical to CompileTest but allows you to specify a custom $GOPATH (the first argument).
*/
func CompileTestIn(gopath string, packagePath string, args ...string) (compiledPath string, err error) {
return doCompileTest(gopath, packagePath, nil, args...)
}

/*
GetAndCompileTestIn is identical to GetAndCompileTest but allows you to specify a custom $GOPATH (the first argument).
*/
func GetAndCompileTestIn(gopath string, packagePath string, args ...string) (compiledPath string, err error) {
if err := getForTest(gopath, packagePath, nil); err != nil {
return "", err
}

return doCompileTest(gopath, packagePath, nil, args...)
}

func isLocalPackage(packagePath string) bool {
return strings.HasPrefix(packagePath, ".")
}

func getForTest(gopath, packagePath string, env []string) error {
if isLocalPackage(packagePath) {
return nil
}

return doGet(gopath, packagePath, env, "-t")
}

func doGet(gopath, packagePath string, env []string, args ...string) error {
args = append(args, packagePath)
args = append([]string{"get"}, args...)

goGet := exec.Command("go", args...)
goGet.Dir = gopath
goGet.Env = replaceGoPath(os.Environ(), gopath)
goGet.Env = append(goGet.Env, env...)

output, err := goGet.CombinedOutput()
if err != nil {
return fmt.Errorf("Failed to get %s:\n\nError:\n%s\n\nOutput:\n%s", packagePath, err, string(output))
}

return nil
}

func doCompileTest(gopath, packagePath string, env []string, args ...string) (compiledPath string, err error) {
executable, err := newExecutablePath(gopath, packagePath, ".test")
if err != nil {
return "", err
}

cmdArgs := append([]string{"test", "-c"}, args...)
cmdArgs = append(cmdArgs, "-o", executable, packagePath)

build := exec.Command("go", cmdArgs...)
build.Env = replaceGoPath(os.Environ(), gopath)
build.Env = append(build.Env, env...)

output, err := build.CombinedOutput()
if err != nil {
return "", fmt.Errorf("Failed to build %s:\n\nError:\n%s\n\nOutput:\n%s", packagePath, err, string(output))
}

return executable, nil
}

func replaceGoPath(environ []string, newGoPath string) []string {
newEnviron := []string{}
for _, v := range environ {
Expand All @@ -56,7 +187,7 @@ func replaceGoPath(environ []string, newGoPath string) []string {
return append(newEnviron, "GOPATH="+newGoPath)
}

func doBuild(gopath, packagePath string, env []string, args ...string) (compiledPath string, err error) {
func newExecutablePath(gopath, packagePath string, suffixes ...string) (string, error) {
tmpDir, err := temporaryDirectory()
if err != nil {
return "", err
Expand All @@ -66,23 +197,14 @@ func doBuild(gopath, packagePath string, env []string, args ...string) (compiled
return "", errors.New("$GOPATH not provided when building " + packagePath)
}

executable := filepath.Join(tmpDir, path.Base(packagePath))
hash := md5.Sum([]byte(packagePath))
filename := fmt.Sprintf("%s-%x%s", path.Base(packagePath), hex.EncodeToString(hash[:]), strings.Join(suffixes, ""))
executable := filepath.Join(tmpDir, filename)

if runtime.GOOS == "windows" {
executable += ".exe"
}

cmdArgs := append([]string{"build"}, args...)
cmdArgs = append(cmdArgs, "-o", executable, packagePath)

build := exec.Command("go", cmdArgs...)
build.Env = replaceGoPath(os.Environ(), gopath)
build.Env = append(build.Env, env...)

output, err := build.CombinedOutput()
if err != nil {
return "", fmt.Errorf("Failed to build %s:\n\nError:\n%s\n\nOutput:\n%s", packagePath, err, string(output))
}

return executable, nil
}

Expand Down
177 changes: 174 additions & 3 deletions gexec/build_test.go
Expand Up @@ -14,6 +14,31 @@ import (
var packagePath = "./_fixture/firefly"

var _ = Describe(".Build", func() {
When("there have been previous calls to CompileTest", func() {
BeforeEach(func() {
_, err := gexec.CompileTest(packagePath)
Expect(err).ShouldNot(HaveOccurred())
})

It("compiles the specified package", func() {
compiledPath, err := gexec.Build(packagePath)
Expect(err).ShouldNot(HaveOccurred())
Expect(compiledPath).Should(BeAnExistingFile())
})

Context("and CleanupBuildArtifacts has been called", func() {
BeforeEach(func() {
gexec.CleanupBuildArtifacts()
})

It("compiles the specified package", func() {
fireflyPath, err := gexec.Build(packagePath)
Expect(err).ShouldNot(HaveOccurred())
Expect(fireflyPath).Should(BeAnExistingFile())
})
})
})

When("there have been previous calls to Build", func() {
BeforeEach(func() {
_, err := gexec.Build(packagePath)
Expand All @@ -32,8 +57,7 @@ var _ = Describe(".Build", func() {
})

It("compiles the specified package", func() {
var err error
fireflyPath, err = gexec.Build(packagePath)
fireflyPath, err := gexec.Build(packagePath)
Expect(err).ShouldNot(HaveOccurred())
Expect(fireflyPath).Should(BeAnExistingFile())
})
Expand Down Expand Up @@ -93,8 +117,9 @@ var _ = Describe(".BuildIn", func() {
})

It("appends the gopath env var", func() {
_, err := gexec.BuildIn(gopath, target)
compiledPath, err := gexec.BuildIn(gopath, target)
Expect(err).NotTo(HaveOccurred())
Expect(compiledPath).Should(BeAnExistingFile())
})

It("resets GOPATH to its original value", func() {
Expand All @@ -104,6 +129,152 @@ var _ = Describe(".BuildIn", func() {
})
})

var _ = Describe(".CompileTest", func() {
Context("a remote package", func() {
const remotePackage = "github.com/onsi/ginkgo/types"

It("compiles the specified test package", func() {
compiledPath, err := gexec.GetAndCompileTest(remotePackage)
Expect(err).ShouldNot(HaveOccurred())
Expect(compiledPath).Should(BeAnExistingFile())
})
})

When("there have been previous calls to CompileTest", func() {
BeforeEach(func() {
_, err := gexec.CompileTest(packagePath)
Expect(err).ShouldNot(HaveOccurred())
})

It("compiles the specified test package", func() {
compiledPath, err := gexec.CompileTest(packagePath)
Expect(err).ShouldNot(HaveOccurred())
Expect(compiledPath).Should(BeAnExistingFile())
})

Context("and CleanupBuildArtifacts has been called", func() {
BeforeEach(func() {
gexec.CleanupBuildArtifacts()
})

It("compiles the specified test package", func() {
fireflyTestPath, err := gexec.CompileTest(packagePath)
Expect(err).ShouldNot(HaveOccurred())
Expect(fireflyTestPath).Should(BeAnExistingFile())
})
})
})

When("there have been previous calls to Build", func() {
BeforeEach(func() {
_, err := gexec.Build(packagePath)
Expect(err).ShouldNot(HaveOccurred())
})

It("compiles the specified test package", func() {
compiledPath, err := gexec.CompileTest(packagePath)
Expect(err).ShouldNot(HaveOccurred())
Expect(compiledPath).Should(BeAnExistingFile())
})

Context("and CleanupBuildArtifacts has been called", func() {
BeforeEach(func() {
gexec.CleanupBuildArtifacts()
})

It("compiles the specified test package", func() {
fireflyTestPath, err := gexec.CompileTest(packagePath)
Expect(err).ShouldNot(HaveOccurred())
Expect(fireflyTestPath).Should(BeAnExistingFile())
})
})
})
})

var _ = Describe(".CompileTestWithEnvironment", func() {
var err error
env := []string{
"GOOS=linux",
"GOARCH=amd64",
}

Context("a remote package", func() {
const remotePackage = "github.com/onsi/ginkgo/types"

It("compiles the specified test package with the specified env vars", func() {
compiledPath, err := gexec.GetAndCompileTestWithEnvironment(remotePackage, env)
Expect(err).ShouldNot(HaveOccurred())
Expect(compiledPath).Should(BeAnExistingFile())
})
})

It("compiles the specified test package with the specified env vars", func() {
compiledPath, err := gexec.CompileTestWithEnvironment(packagePath, env)
Expect(err).ShouldNot(HaveOccurred())
Expect(compiledPath).Should(BeAnExistingFile())
})

It("returns the environment to a good state", func() {
_, err = gexec.CompileTestWithEnvironment(packagePath, env)
Expect(err).ShouldNot(HaveOccurred())
Expect(os.Environ()).ShouldNot(ContainElement("GOOS=linux"))
})
})

var _ = Describe(".CompiledTestIn", func() {
const (
target = "github.com/onsi/gomega/gexec/_fixture/firefly"
)

var (
original string
gopath string
)

BeforeEach(func() {
var err error
original = os.Getenv("GOPATH")
gopath, err = ioutil.TempDir("", "")
Expect(err).NotTo(HaveOccurred())
copyFile(filepath.Join("_fixture", "firefly", "main.go"), filepath.Join(gopath, "src", target), "main.go")
Expect(os.Setenv("GOPATH", filepath.Join(os.TempDir(), "emptyFakeGopath"))).To(Succeed())
Expect(os.Environ()).To(ContainElement(fmt.Sprintf("GOPATH=%s", filepath.Join(os.TempDir(), "emptyFakeGopath"))))
})

AfterEach(func() {
if original == "" {
Expect(os.Unsetenv("GOPATH")).To(Succeed())
} else {
Expect(os.Setenv("GOPATH", original)).To(Succeed())
}
if gopath != "" {
os.RemoveAll(gopath)
}
})

Context("a remote package", func() {
const remotePackage = "github.com/onsi/ginkgo/types"

It("compiles the specified test package", func() {
compiledPath, err := gexec.GetAndCompileTestIn(gopath, remotePackage)
Expect(err).ShouldNot(HaveOccurred())
Expect(compiledPath).Should(BeAnExistingFile())
})
})

It("appends the gopath env var", func() {
compiledPath, err := gexec.CompileTestIn(gopath, target)
Expect(err).NotTo(HaveOccurred())
Expect(compiledPath).Should(BeAnExistingFile())
})

It("resets GOPATH to its original value", func() {
_, err := gexec.CompileTestIn(gopath, target)
Expect(err).NotTo(HaveOccurred())
Expect(os.Getenv("GOPATH")).To(Equal(filepath.Join(os.TempDir(), "emptyFakeGopath")))
})
})

func copyFile(source, directory, basename string) {
Expect(os.MkdirAll(directory, 0755)).To(Succeed())
content, err := ioutil.ReadFile(source)
Expand Down

0 comments on commit 47c613f

Please sign in to comment.