2016-03-10 18:07:46 +01:00
|
|
|
// -*- Mode: Go; indent-tabs-mode: t -*-
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Copyright (C) 2016 Canonical Ltd
|
|
|
|
|
*
|
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
|
* it under the terms of the GNU General Public License version 3 as
|
|
|
|
|
* published by the Free Software Foundation.
|
|
|
|
|
*
|
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
|
*
|
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
*
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
package testutil
|
|
|
|
|
|
|
|
|
|
import (
|
2019-04-03 12:53:02 +02:00
|
|
|
"bytes"
|
2016-03-10 18:07:46 +01:00
|
|
|
"fmt"
|
2019-04-03 12:53:02 +02:00
|
|
|
"io"
|
2016-03-10 18:07:46 +01:00
|
|
|
"os"
|
2019-03-28 08:30:08 +01:00
|
|
|
"os/exec"
|
2016-03-10 18:07:46 +01:00
|
|
|
"path"
|
2017-03-07 23:50:22 +01:00
|
|
|
"path/filepath"
|
2016-03-10 18:07:46 +01:00
|
|
|
"strings"
|
2019-05-04 11:11:13 +02:00
|
|
|
"sync"
|
2016-03-10 18:07:46 +01:00
|
|
|
|
2016-03-10 19:10:41 +01:00
|
|
|
"gopkg.in/check.v1"
|
2016-03-10 18:07:46 +01:00
|
|
|
)
|
|
|
|
|
|
2019-03-28 08:30:08 +01:00
|
|
|
var shellcheckPath string
|
|
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
|
if p, err := exec.LookPath("shellcheck"); err == nil {
|
|
|
|
|
shellcheckPath = p
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-04 11:11:13 +02:00
|
|
|
var (
|
|
|
|
|
shellchecked = make(map[string]bool, 16)
|
|
|
|
|
shellcheckedMu sync.Mutex
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func shellcheckSeenAlready(script string) bool {
|
|
|
|
|
shellcheckedMu.Lock()
|
|
|
|
|
defer shellcheckedMu.Unlock()
|
|
|
|
|
if shellchecked[script] {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
shellchecked[script] = true
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-11 14:41:14 +01:00
|
|
|
var pristineEnv = os.Environ()
|
|
|
|
|
|
2019-05-04 11:11:13 +02:00
|
|
|
func maybeShellcheck(c *check.C, script string, wholeScript io.Reader) {
|
|
|
|
|
// MockCommand is used sometimes in SetUptTest, so it adds up
|
|
|
|
|
// even for the empty script, don't recheck the essentially same
|
|
|
|
|
// thing again and again!
|
|
|
|
|
if shellcheckSeenAlready(script) {
|
|
|
|
|
return
|
|
|
|
|
}
|
2019-03-28 08:30:08 +01:00
|
|
|
c.Logf("using shellcheck: %q", shellcheckPath)
|
|
|
|
|
if shellcheckPath == "" {
|
|
|
|
|
// no shellcheck, nothing to do
|
|
|
|
|
return
|
|
|
|
|
}
|
2019-04-03 12:53:02 +02:00
|
|
|
cmd := exec.Command(shellcheckPath, "-s", "bash", "-")
|
2020-12-11 14:41:14 +01:00
|
|
|
cmd.Env = pristineEnv
|
2019-05-04 11:11:13 +02:00
|
|
|
cmd.Stdin = wholeScript
|
2019-03-28 08:30:08 +01:00
|
|
|
out, err := cmd.CombinedOutput()
|
|
|
|
|
c.Check(err, check.IsNil, check.Commentf("shellcheck failed:\n%s", string(out)))
|
|
|
|
|
}
|
|
|
|
|
|
2016-03-10 19:10:41 +01:00
|
|
|
// MockCmd allows mocking commands for testing.
|
|
|
|
|
type MockCmd struct {
|
2016-03-14 22:53:03 +01:00
|
|
|
binDir string
|
|
|
|
|
exeFile string
|
|
|
|
|
logFile string
|
2016-03-10 18:07:46 +01:00
|
|
|
}
|
|
|
|
|
|
2018-01-29 11:34:48 +01:00
|
|
|
// The top of the script generate the output to capture the
|
|
|
|
|
// command that was run and the arguments used. To support
|
|
|
|
|
// mocking commands that need "\n" in their args (like zenity)
|
|
|
|
|
// we use the following convention:
|
|
|
|
|
// - generate \0 to separate args
|
|
|
|
|
// - generate \0\0 to separate commands
|
2017-12-01 16:12:13 +01:00
|
|
|
var scriptTpl = `#!/bin/bash
|
2019-11-28 14:12:36 +01:00
|
|
|
###LOCK###
|
2018-01-29 10:28:39 +01:00
|
|
|
printf "%%s" "$(basename "$0")" >> %[1]q
|
2019-04-03 21:19:36 +02:00
|
|
|
printf '\0' >> %[1]q
|
2017-12-01 16:12:13 +01:00
|
|
|
|
2016-09-06 17:03:07 +01:00
|
|
|
for arg in "$@"; do
|
2018-01-29 10:28:39 +01:00
|
|
|
printf "%%s" "$arg" >> %[1]q
|
2019-04-03 21:19:36 +02:00
|
|
|
printf '\0' >> %[1]q
|
2016-09-06 17:03:07 +01:00
|
|
|
done
|
2017-12-01 16:12:13 +01:00
|
|
|
|
2019-04-03 21:19:36 +02:00
|
|
|
printf '\0' >> %[1]q
|
2016-09-06 17:03:07 +01:00
|
|
|
%s
|
|
|
|
|
`
|
|
|
|
|
|
testutil: workaround 14.04 weirdness in synchronized mock command
On 14.04, the script file is apparently kept open for writingby flock(1) and
subsequent attempt to execve() it fails with ETXTBSY. Workaround that and place
the lock on the parent directory instead.
$ strace -e flock,execve -f /tmp/check-737788169571723474/0/usr/lib/snapd/snap-seccomp
...
flock(3, LOCK_EX) = 0
Process 18827 attached
[pid 18827] execve("/tmp/check-737788169571723474/0/usr/lib/snapd/snap-seccomp", ["/tmp/check-737788169571723474/0/"...], [/* 75 vars */]) = -1 ETXTBSY (Text file busy)
flock: /tmp/check-737788169571723474/0/usr/lib/snapd/snap-seccomp: Text file busy
[pid 18827] +++ exited with 69 +++
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=18827, si_status=69, si_utime=0, si_stime=0} ---
+++ exited with 69 +++
Signed-off-by: Maciej Borzecki <maciej.zenon.borzecki@canonical.com>
2019-12-03 14:09:52 +01:00
|
|
|
// Wrap the script in flock to serialize the calls to the script and prevent the
|
|
|
|
|
// call log from getting corrupted. Workaround 14.04 flock(1) weirdness, that
|
|
|
|
|
// keeps the script file open for writing and execve() fails with ETXTBSY.
|
|
|
|
|
var selfLock = `if [ "${FLOCKER}" != "$0" ]; then exec env FLOCKER="$0" flock -e "$(dirname "$0")" "$0" "$@" ; fi`
|
2019-11-28 14:12:36 +01:00
|
|
|
|
|
|
|
|
func mockCommand(c *check.C, basename, script, template string) *MockCmd {
|
2019-04-03 12:53:02 +02:00
|
|
|
var wholeScript bytes.Buffer
|
2017-03-07 23:50:22 +01:00
|
|
|
var binDir, exeFile, logFile string
|
2020-05-25 11:59:17 -05:00
|
|
|
var newpath string
|
2017-03-07 23:50:22 +01:00
|
|
|
if filepath.IsAbs(basename) {
|
|
|
|
|
binDir = filepath.Dir(basename)
|
2019-11-22 11:04:19 +01:00
|
|
|
err := os.MkdirAll(binDir, 0755)
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(fmt.Sprintf("cannot create the directory for mocked command %q: %v", basename, err))
|
|
|
|
|
}
|
2017-03-07 23:50:22 +01:00
|
|
|
exeFile = basename
|
|
|
|
|
logFile = basename + ".log"
|
|
|
|
|
} else {
|
|
|
|
|
binDir = c.MkDir()
|
|
|
|
|
exeFile = path.Join(binDir, basename)
|
|
|
|
|
logFile = path.Join(binDir, basename+".log")
|
2020-05-25 11:59:17 -05:00
|
|
|
newpath = binDir + ":" + os.Getenv("PATH")
|
2017-03-07 23:50:22 +01:00
|
|
|
}
|
2019-11-28 14:12:36 +01:00
|
|
|
fmt.Fprintf(&wholeScript, template, logFile, script)
|
2023-09-26 11:38:46 +01:00
|
|
|
err := os.WriteFile(exeFile, wholeScript.Bytes(), 0700)
|
2016-03-10 19:22:53 +01:00
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
2017-03-07 23:50:22 +01:00
|
|
|
|
2019-05-04 11:11:13 +02:00
|
|
|
maybeShellcheck(c, script, &wholeScript)
|
2019-03-28 08:30:08 +01:00
|
|
|
|
2020-05-25 11:59:17 -05:00
|
|
|
if newpath != "" {
|
|
|
|
|
os.Setenv("PATH", binDir+":"+os.Getenv("PATH"))
|
|
|
|
|
}
|
|
|
|
|
|
2016-03-14 22:53:03 +01:00
|
|
|
return &MockCmd{binDir: binDir, exeFile: exeFile, logFile: logFile}
|
2016-03-10 18:07:46 +01:00
|
|
|
}
|
|
|
|
|
|
2019-11-28 14:12:36 +01:00
|
|
|
// MockCommand adds a mocked command. If the basename argument is a command it
|
|
|
|
|
// is added to PATH. If it is an absolute path it is just created there, along
|
|
|
|
|
// with the full prefix. The caller is responsible for the cleanup in this case.
|
|
|
|
|
//
|
|
|
|
|
// The command logs all invocations to a dedicated log file. If script is
|
|
|
|
|
// non-empty then it is used as is and the caller is responsible for how the
|
|
|
|
|
// script behaves (exit code and any extra behavior). If script is empty then
|
|
|
|
|
// the command exits successfully without any other side-effect.
|
|
|
|
|
func MockCommand(c *check.C, basename, script string) *MockCmd {
|
|
|
|
|
return mockCommand(c, basename, script, strings.Replace(scriptTpl, "###LOCK###", "", 1))
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-25 11:54:40 -05:00
|
|
|
// MockLockedCommand is the same as MockCommand(), but the script uses flock to
|
2019-11-28 14:12:36 +01:00
|
|
|
// enforce exclusive locking, preventing the call tracking from being corrupted.
|
|
|
|
|
// Thus it is safe to be called in parallel.
|
|
|
|
|
func MockLockedCommand(c *check.C, basename, script string) *MockCmd {
|
|
|
|
|
return mockCommand(c, basename, script, strings.Replace(scriptTpl, "###LOCK###", selfLock, 1))
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-01 16:12:13 +01:00
|
|
|
// Also mock this command, using the same bindir and log.
|
2016-09-06 17:03:07 +01:00
|
|
|
// Useful when you want to check the ordering of things.
|
|
|
|
|
func (cmd *MockCmd) Also(basename, script string) *MockCmd {
|
|
|
|
|
exeFile := path.Join(cmd.binDir, basename)
|
2023-09-26 11:38:46 +01:00
|
|
|
err := os.WriteFile(exeFile, []byte(fmt.Sprintf(scriptTpl, cmd.logFile, script)), 0700)
|
2016-09-06 17:03:07 +01:00
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
return &MockCmd{binDir: cmd.binDir, exeFile: exeFile, logFile: cmd.logFile}
|
|
|
|
|
}
|
|
|
|
|
|
2016-03-10 19:10:41 +01:00
|
|
|
// Restore removes the mocked command from PATH
|
|
|
|
|
func (cmd *MockCmd) Restore() {
|
|
|
|
|
entries := strings.Split(os.Getenv("PATH"), ":")
|
|
|
|
|
for i, entry := range entries {
|
|
|
|
|
if entry == cmd.binDir {
|
|
|
|
|
entries = append(entries[:i], entries[i+1:]...)
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
os.Setenv("PATH", strings.Join(entries, ":"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calls returns a list of calls that were made to the mock command.
|
2016-06-03 16:14:05 +02:00
|
|
|
// of the form:
|
2023-01-09 14:39:58 +00:00
|
|
|
//
|
|
|
|
|
// [][]string{
|
|
|
|
|
// {"cmd", "arg1", "arg2"}, // first invocation of "cmd"
|
|
|
|
|
// {"cmd", "arg1", "arg2"}, // second invocation of "cmd"
|
|
|
|
|
// }
|
2016-06-03 16:14:05 +02:00
|
|
|
func (cmd *MockCmd) Calls() [][]string {
|
2024-04-03 22:23:24 +01:00
|
|
|
raw, err := os.ReadFile(cmd.logFile)
|
2016-03-23 23:12:21 +01:00
|
|
|
if os.IsNotExist(err) {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2016-03-10 19:10:41 +01:00
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
2016-03-10 18:07:46 +01:00
|
|
|
}
|
2017-12-01 16:12:13 +01:00
|
|
|
logContent := strings.TrimSuffix(string(raw), "\000")
|
2016-06-03 16:14:05 +02:00
|
|
|
|
|
|
|
|
allCalls := [][]string{}
|
2017-12-01 16:12:13 +01:00
|
|
|
calls := strings.Split(logContent, "\000\000")
|
2016-06-03 16:14:05 +02:00
|
|
|
for _, call := range calls {
|
2017-12-01 16:12:13 +01:00
|
|
|
call = strings.TrimSuffix(call, "\000")
|
|
|
|
|
allCalls = append(allCalls, strings.Split(call, "\000"))
|
2016-06-03 16:14:05 +02:00
|
|
|
}
|
|
|
|
|
return allCalls
|
2016-03-10 18:07:46 +01:00
|
|
|
}
|
2016-03-23 23:11:55 +01:00
|
|
|
|
|
|
|
|
// ForgetCalls purges the list of calls made so far
|
|
|
|
|
func (cmd *MockCmd) ForgetCalls() {
|
|
|
|
|
err := os.Remove(cmd.logFile)
|
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-09-22 13:27:56 +02:00
|
|
|
|
|
|
|
|
// BinDir returns the location of the directory holding overridden commands.
|
|
|
|
|
func (cmd *MockCmd) BinDir() string {
|
|
|
|
|
return cmd.binDir
|
|
|
|
|
}
|
2018-12-03 15:26:08 +01:00
|
|
|
|
|
|
|
|
// Exe return the full path of the mock binary
|
|
|
|
|
func (cmd *MockCmd) Exe() string {
|
|
|
|
|
return filepath.Join(cmd.exeFile)
|
|
|
|
|
}
|