func/testing/testing.go

247 lines
6.3 KiB
Go

// package testing includes minor testing helpers.
//
// These helpers include extensions to the testing nomenclature which exist to
// ease the development of tests for functions. It is mostly just syntactic
// sugar and closures for creating an removing test directories etc.
// It was originally included in each of the requisite testing packages, but
// since we use both private-access enabled tests (in the function package),
// as well as closed-box tests (in function_test package), and they are gradually
// increasing in size and complexity, the choice was made to choose a small
// dependency over a small amount of copying.
//
// Another reason for including these in a separate locaiton is that they will
// have no tags such that no combination of tags can cause them to either be
// missing or interfere with eachother (a problem encountered with knative
// tooling which by default runs tests with all tags enabled simultaneously)
package testing
import (
"fmt"
"io/ioutil"
"net"
"net/http"
"net/http/cgi"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
)
// Using the given path, create it as a new directory and return a deferrable
// which will remove it.
// usage:
// defer using(t, "testdata/example.com/someExampleTest")()
func Using(t *testing.T, root string) func() {
t.Helper()
mkdir(t, root)
return func() {
rm(t, root)
}
}
// mkdir creates a directory as a test helper, failing the test on error.
func mkdir(t *testing.T, dir string) {
t.Helper()
if err := os.MkdirAll(dir, 0700); err != nil {
t.Fatal(err)
}
}
// rm a directory as a test helper, failing the test on error.
func rm(t *testing.T, dir string) {
t.Helper()
if err := os.RemoveAll(dir); err != nil {
t.Fatal(err)
}
}
// Within the given root creates the directory, CDs to it, and rturns a
// closure that when executed (intended in a defer) removes the given dirctory
// and returns the caller to the initial working directory.
// usage:
// defer within(t, "somedir")()
func Within(t *testing.T, root string) func() {
t.Helper()
cwd := pwd(t)
mkdir(t, root)
cd(t, root)
return func() {
cd(t, cwd)
rm(t, root)
}
}
// Mktemp creates a temporary directory, CDs the current processes (test) to
// said directory, and returns the path to said directory.
// Usage:
// path, rm := Mktemp(t)
// defer rm()
// CWD is now 'path'
// errors encountererd fail the current test.
func Mktemp(t *testing.T) (string, func()) {
t.Helper()
tmp := tempdir(t)
owd := pwd(t)
cd(t, tmp)
return tmp, func() {
os.RemoveAll(tmp)
cd(t, owd)
}
}
// Fromtemp is like Mktemp, but does not bother returing the temp path.
func Fromtemp(t *testing.T) func() {
_, done := Mktemp(t)
return done
}
// tempdir creates a new temporary directory and returns its path.
// errors fail the current test.
func tempdir(t *testing.T) string {
// NOTE: Not using t.TempDir() because it is sometimes helpful during
// debugging to skip running the returned deferred cleanup function
// and manually inspect the contents of the test's temp directory.
d, err := ioutil.TempDir("", "dir")
if err != nil {
t.Fatal(err)
}
return d
}
// pwd prints the current working directory.
// errors fail the test.
func pwd(t *testing.T) string {
t.Helper()
d, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
return d
}
// cd changes directory to the given directory.
// errors fail the given test.
func cd(t *testing.T, dir string) {
if err := os.Chdir(dir); err != nil {
t.Fatal(err)
}
}
// TestRepoURI starts serving HTTP git server with GIT_PROJECT_ROOT=$(pwd)/testdata
// and returns URL for project named `name` under the git root.
//
//
// For example TestRepoURI("my-repo", t) returns string that could look like:
// http://localhost:4242/my-repo.git
func TestRepoURI(name string, t *testing.T) string {
t.Helper()
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
gitRoot := filepath.Join(wd, "testdata")
hostPort := RunGitServer(t, gitRoot)
return fmt.Sprintf(`http://%s/%s.git`, hostPort, name)
}
// WithEnvVar sets an environment variable
// and returns deferrable function that restores previous value of the environment variable.
// TODO: replace with t.Setenv when we upgrade to go.1.17
func WithEnvVar(t *testing.T, name, value string) func() {
t.Helper()
oldDh, hadDh := os.LookupEnv(name)
err := os.Setenv(name, value)
if err != nil {
t.Fatal(err)
}
return func() {
if hadDh {
_ = os.Setenv(name, oldDh)
} else {
_ = os.Unsetenv(name)
}
}
}
// WithExecutable creates an executable of the given name and source in a temp
// directory which is then added to PATH. Returned is a deferrable which will
// clean up both the script and PATH.
func WithExecutable(t *testing.T, name, goSrc string) func() {
var err error
binDir := t.TempDir()
newPath := binDir + string(os.PathListSeparator) + os.Getenv("PATH")
cleanUpPath := WithEnvVar(t, "PATH", newPath)
goSrcPath := filepath.Join(binDir, fmt.Sprintf("%s.go", name))
err = ioutil.WriteFile(goSrcPath,
[]byte(goSrc),
0400)
if err != nil {
t.Fatal(err)
}
runnerScriptName := name
if runtime.GOOS == "windows" {
runnerScriptName = runnerScriptName + ".bat"
}
runnerScriptSrc := `#!/bin/sh
exec go run GO_SCRIPT_PATH $@;
`
if runtime.GOOS == "windows" {
runnerScriptSrc = `@echo off
go.exe run GO_SCRIPT_PATH %*
`
}
runnerScriptPath := filepath.Join(binDir, runnerScriptName)
runnerScriptSrc = strings.ReplaceAll(runnerScriptSrc, "GO_SCRIPT_PATH", goSrcPath)
err = ioutil.WriteFile(runnerScriptPath, []byte(runnerScriptSrc), 0700)
if err != nil {
t.Fatal(err)
}
return func() {
cleanUpPath()
}
}
// RunGitServer starts serving git HTTP server and returns its address including port
func RunGitServer(t *testing.T, gitRoot string) (hostPort string) {
l, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatal(err)
}
hostPort = l.Addr().String()
cmd := exec.Command("git", "--exec-path")
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatal(err)
}
server := &http.Server{
Handler: &cgi.Handler{
Path: filepath.Join(strings.Trim(string(out), "\n"), "git-http-backend"),
Env: []string{"GIT_HTTP_EXPORT_ALL=true", fmt.Sprintf("GIT_PROJECT_ROOT=%s", gitRoot)},
},
}
go func() {
err = server.Serve(l)
if err != nil && !strings.Contains(err.Error(), "Server closed") {
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
}
}()
t.Cleanup(func() {
server.Close()
})
return hostPort
}