Permalink
Cannot retrieve contributors at this time
Fetching contributors…
| // Copyright 2013, 2014 Canonical Ltd. | |
| // Licensed under the LGPLv3, see LICENCE file for details. | |
| package testing | |
| import ( | |
| "fmt" | |
| "io/ioutil" | |
| "os" | |
| "os/exec" | |
| "path/filepath" | |
| "runtime" | |
| "strconv" | |
| "strings" | |
| "github.com/juju/utils" | |
| gc "gopkg.in/check.v1" | |
| jc "github.com/juju/testing/checkers" | |
| ) | |
| var HookChannelSize = 10 | |
| // HookCommandOutput intercepts CommandOutput to a function that passes the | |
| // actual command and it's output back via a channel, and returns the error | |
| // passed into this function. It also returns a cleanup function so you can | |
| // restore the original function | |
| func HookCommandOutput( | |
| outputFunc *func(cmd *exec.Cmd) ([]byte, error), output []byte, err error) (<-chan *exec.Cmd, func()) { | |
| cmdChan := make(chan *exec.Cmd, HookChannelSize) | |
| origCommandOutput := *outputFunc | |
| cleanup := func() { | |
| close(cmdChan) | |
| *outputFunc = origCommandOutput | |
| } | |
| *outputFunc = func(cmd *exec.Cmd) ([]byte, error) { | |
| cmdChan <- cmd | |
| return output, err | |
| } | |
| return cmdChan, cleanup | |
| } | |
| const ( | |
| // EchoQuotedArgs is a simple bash script that prints out the | |
| // basename of the command followed by the args as quoted strings. | |
| // If a ; separated list of exit codes is provided in $name.exitcodes | |
| // then it will return them in turn over multiple calls. If | |
| // $name.exitcodes does not exist, or the list runs out, return 0. | |
| EchoQuotedArgsUnix = `#!/bin/bash --norc | |
| name=` + "`basename $0`" + ` | |
| argfile="$0.out" | |
| exitcodesfile="$0.exitcodes" | |
| printf "%s" $name | tee -a $argfile | |
| for arg in "$@"; do | |
| printf " '%s'" "$arg" | tee -a $argfile | |
| done | |
| printf "\n" | tee -a $argfile | |
| if [ -f $exitcodesfile ] | |
| then | |
| exitcodes=$(cat $exitcodesfile) | |
| arr=(${exitcodes/;/ }) | |
| echo ${arr[1]} | tee $exitcodesfile | |
| exit ${arr[0]} | |
| fi | |
| ` | |
| EchoQuotedArgsWindows = `@echo off | |
| setlocal enabledelayedexpansion | |
| set list=%0 | |
| set argCount=0 | |
| set argfile=%~f0.out | |
| set exitcodesfile=%~f0.exitcodes | |
| for %%x in (%*) do ( | |
| set /A argCount+=1 | |
| set "argVec[!argCount!]=%%~x" | |
| ) | |
| for /L %%i in (1,1,%argCount%) do set list=!list! '!argVec[%%i]!' | |
| IF exist %exitcodesfile% ( | |
| FOR /F "tokens=1* delims=;" %%i IN (%exitcodesfile%) DO ( | |
| set exitcode=%%i | |
| IF NOT [%%j]==[] ( | |
| echo %%j > %exitcodesfile% | |
| ) ELSE ( | |
| del %exitcodesfile% | |
| ) | |
| ) | |
| ) | |
| echo %list%>> %argfile% | |
| exit /B %exitcode% | |
| ` | |
| ) | |
| // EnvironmentPatcher is an interface that requires just one method: | |
| // PatchEnvironment. | |
| type EnvironmentPatcher interface { | |
| PatchEnvironment(name, value string) | |
| } | |
| // PatchExecutable creates an executable called 'execName' in a new test | |
| // directory and that directory is added to the path. | |
| func PatchExecutable(c *gc.C, patcher EnvironmentPatcher, execName, script string, exitCodes ...int) { | |
| dir := c.MkDir() | |
| patcher.PatchEnvironment("PATH", joinPathLists(dir, os.Getenv("PATH"))) | |
| var filename string | |
| switch runtime.GOOS { | |
| case "windows": | |
| filename = filepath.Join(dir, execName+".bat") | |
| default: | |
| filename = filepath.Join(dir, execName) | |
| } | |
| err := ioutil.WriteFile(filename, []byte(script), 0755) | |
| c.Assert(err, gc.IsNil) | |
| if len(exitCodes) > 0 { | |
| filename := filename + ".exitcodes" | |
| codes := make([]string, len(exitCodes)) | |
| for i, code := range exitCodes { | |
| codes[i] = strconv.Itoa(code) | |
| } | |
| s := strings.Join(codes, ";") + ";" | |
| err = ioutil.WriteFile(filename, []byte(s), 0644) | |
| c.Assert(err, gc.IsNil) | |
| } | |
| } | |
| // PatchExecutableThrowError is needed to test cases in which we expect exit | |
| // codes from executables called from the system path | |
| func PatchExecutableThrowError(c *gc.C, patcher EnvironmentPatcher, execName string, exitCode int) { | |
| switch runtime.GOOS { | |
| case "windows": | |
| script := fmt.Sprintf(`@echo off | |
| setlocal enabledelayedexpansion | |
| echo failing | |
| exit /b %d | |
| REM see %%ERRORLEVEL%% for last exit code like $? on linux | |
| `, exitCode) | |
| PatchExecutable(c, patcher, execName, script) | |
| default: | |
| script := fmt.Sprintf(`#!/bin/bash --norc | |
| echo failing | |
| exit %d | |
| `, exitCode) | |
| PatchExecutable(c, patcher, execName, script) | |
| } | |
| } | |
| // PatchExecutableAsEchoArgs creates an executable called 'execName' in a new | |
| // test directory and that directory is added to the path. The content of the | |
| // script is 'EchoQuotedArgs', and the args file is removed using a cleanup | |
| // function. | |
| func PatchExecutableAsEchoArgs(c *gc.C, patcher EnvironmentPatcher, execName string, exitCodes ...int) { | |
| switch runtime.GOOS { | |
| case "windows": | |
| PatchExecutable(c, patcher, execName, EchoQuotedArgsWindows, exitCodes...) | |
| default: | |
| PatchExecutable(c, patcher, execName, EchoQuotedArgsUnix, exitCodes...) | |
| } | |
| } | |
| // AssertEchoArgs is used to check the args from an execution of a command | |
| // that has been patched using PatchExecutable containing EchoQuotedArgs. | |
| func AssertEchoArgs(c *gc.C, execName string, args ...string) { | |
| // Create expected output string | |
| expected := execName | |
| for _, arg := range args { | |
| expected = fmt.Sprintf("%s %s", expected, utils.ShQuote(arg)) | |
| } | |
| actual := ReadEchoArgs(c, execName) | |
| c.Assert(actual, gc.Equals, expected) | |
| } | |
| // ReadEchoArgs is used to read the args from an execution of a command | |
| // that has been patched using PatchExecutable containing EchoQuotedArgs. | |
| func ReadEchoArgs(c *gc.C, execName string) string { | |
| execPath, err := exec.LookPath(execName) | |
| c.Assert(err, jc.ErrorIsNil) | |
| // Read in entire argument log file | |
| content, err := ioutil.ReadFile(execPath + ".out") | |
| c.Assert(err, jc.ErrorIsNil) | |
| lines := strings.Split(string(content), "\n") | |
| actual := strings.TrimSuffix(lines[0], "\r") | |
| // Write out the remaining lines for the next check | |
| content = []byte(strings.Join(lines[1:], "\n")) | |
| err = ioutil.WriteFile(execPath+".out", content, 0644) // or just call this filename somewhere, once. | |
| return actual | |
| } | |
| // PatchExecHelper is a type that helps you patch out calls to executables by | |
| // patching out the exec.Command function that creates the exec.Cmd to call | |
| // them. This is very similar to PatchExecutable above, except it works on | |
| // windows exe files, is a lot easier to control stderr and stdout, doesn't | |
| // require arcane bash and batch scripting, and lets you control both the output | |
| // *and* test the arguments, all without requiring writing any garbage files to | |
| // disk. | |
| // | |
| // PatchExecHelper *must* be embedded in your test suite in order to function. | |
| // It adds a test to your testsuite which by default simply does nothing. When | |
| // the patched exec.Command function is called (returned by GetExecCommand), | |
| // instead of running the requested executable, we call the test executable with | |
| // -check.f to rnu only TestExecSuiteHelperProcess, which acts as a configurable | |
| // main function. | |
| type PatchExecHelper struct{} | |
| // PatchExecConfig holds the arguments for PatchExecHelper.GetExecCommand. | |
| type PatchExecConfig struct { | |
| // Stderr is the value you'd like written to stderr. | |
| Stderr string | |
| // Stdout is the value you'd like written to stdout. | |
| Stdout string | |
| // ExitCode controls the exit code of the patched executable. | |
| ExitCode int | |
| // Args is a channel that will be sent the args passed to the patched | |
| // execCommand function. It should be a channel with a buffer equal to the | |
| // number of executions you expect to be run (often just 1). Do not use an | |
| // unbuffered channel unless you're reading the channel from another | |
| // goroutine, or you will almost certainly block your tests indefinitely. | |
| Args chan<- []string | |
| } | |
| // GetExecCommand returns a function that can be used to patch out a use of | |
| // exec.Command. See PatchExecConfig for details about the arguments. | |
| func (PatchExecHelper) GetExecCommand(cfg PatchExecConfig) func(string, ...string) *exec.Cmd { | |
| // This method doesn't technically need to be a method on PatchExecHelper, | |
| // but serves as a reminder to embed PatchExecHelper. | |
| return func(command string, args ...string) *exec.Cmd { | |
| // We redirect the command to call the test executable, telling it to | |
| // run the TestExecSuiteHelperProcess test that got embedded into the | |
| // test suite, and pass the original args at the end of our args. | |
| // | |
| // Note that we don't need to include the suite name in check.f, because | |
| // even if you have more than one suite embedding PatchExecHelper, all | |
| // the tests have the same imlpementation, and the first instance of the | |
| // test to run calls os.Exit, and therefore none of the other tests will | |
| // run. | |
| cs := []string{"-check.f=TestExecSuiteHelperProcess", "--", command} | |
| cs = append(cs, args...) | |
| cmd := exec.Command(os.Args[0], cs...) | |
| cmd.Env = append( | |
| // We must preserve os.Environ() on Windows, | |
| // or the subprocess will fail in weird and | |
| // wonderful ways. | |
| os.Environ(), | |
| "JUJU_WANT_HELPER_PROCESS=1", | |
| "JUJU_HELPER_PROCESS_STDERR="+cfg.Stderr, | |
| "JUJU_HELPER_PROCESS_STDOUT="+cfg.Stdout, | |
| fmt.Sprintf("JUJU_HELPER_PROCESS_EXITCODE=%d", cfg.ExitCode), | |
| ) | |
| // Pass the args back on the arg channel. This is why the channel needs | |
| // to be buffered, so this won't block. | |
| if cfg.Args != nil { | |
| cfg.Args <- append([]string{command}, args...) | |
| } | |
| return cmd | |
| } | |
| } | |
| // TestExecSuiteHelperProcess is a fake test which is added to your test suite | |
| // (because you remembered to embed PatchExecHelper in your suite, right?). It | |
| // allows us to use the test executable as a helper process to get expected | |
| // output for tests. When run normally during tests, this test simply does | |
| // nothing (and passes). The above patched exec.Command runs the test | |
| // executable with -check.f, it runs this test and enables the configurable | |
| // behavior. Because the test exits with os.Exit, no additional test output is | |
| // written. | |
| func (PatchExecHelper) TestExecSuiteHelperProcess(c *gc.C) { | |
| if os.Getenv("JUJU_WANT_HELPER_PROCESS") == "" { | |
| return | |
| } | |
| if stderr := os.Getenv("JUJU_HELPER_PROCESS_STDERR"); stderr != "" { | |
| fmt.Fprintln(os.Stderr, stderr) | |
| } | |
| if stdout := os.Getenv("JUJU_HELPER_PROCESS_STDOUT"); stdout != "" { | |
| fmt.Fprintln(os.Stdout, stdout) | |
| } | |
| code := os.Getenv("JUJU_HELPER_PROCESS_EXITCODE") | |
| if code == "" { | |
| os.Exit(0) | |
| } | |
| exit, err := strconv.Atoi(code) | |
| if err != nil { | |
| // This should be impossible, since we set this with an int above. | |
| panic(err) | |
| } | |
| os.Exit(exit) | |
| } | |
| // CaptureOutput runs the given function and captures anything written | |
| // to Stderr or Stdout during f's execution. | |
| func CaptureOutput(c *gc.C, f func()) (stdout []byte, stderr []byte) { | |
| dir := c.MkDir() | |
| stderrf, err := os.OpenFile(filepath.Join(dir, "stderr"), os.O_RDWR|os.O_CREATE, 0600) | |
| c.Assert(err, jc.ErrorIsNil) | |
| defer stderrf.Close() | |
| stdoutf, err := os.OpenFile(filepath.Join(dir, "stdout"), os.O_RDWR|os.O_CREATE, 0600) | |
| c.Assert(err, jc.ErrorIsNil) | |
| defer stdoutf.Close() | |
| // make a sub-functions so those defers go off ASAP. | |
| func() { | |
| origErr := os.Stderr | |
| defer func() { os.Stderr = origErr }() | |
| origOut := os.Stdout | |
| defer func() { os.Stdout = origOut }() | |
| os.Stderr = stderrf | |
| os.Stdout = stdoutf | |
| f() | |
| }() | |
| _, err = stderrf.Seek(0, 0) | |
| c.Assert(err, jc.ErrorIsNil) | |
| stderr, err = ioutil.ReadAll(stderrf) | |
| c.Assert(err, jc.ErrorIsNil) | |
| _, err = stdoutf.Seek(0, 0) | |
| c.Assert(err, jc.ErrorIsNil) | |
| stdout, err = ioutil.ReadAll(stdoutf) | |
| c.Assert(err, jc.ErrorIsNil) | |
| return stdout, stderr | |
| } |