forked from canonical/snapd
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathexec.go
232 lines (204 loc) · 6.53 KB
/
exec.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
// -*- 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 (
"bytes"
"fmt"
"io"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"sync"
"gopkg.in/check.v1"
)
var shellcheckPath string
func init() {
if p, err := exec.LookPath("shellcheck"); err == nil {
shellcheckPath = p
}
}
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
}
var pristineEnv = os.Environ()
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
}
c.Logf("using shellcheck: %q", shellcheckPath)
if shellcheckPath == "" {
// no shellcheck, nothing to do
return
}
cmd := exec.Command(shellcheckPath, "-s", "bash", "-")
cmd.Env = pristineEnv
cmd.Stdin = wholeScript
out, err := cmd.CombinedOutput()
c.Check(err, check.IsNil, check.Commentf("shellcheck failed:\n%s", string(out)))
}
// MockCmd allows mocking commands for testing.
type MockCmd struct {
binDir string
exeFile string
logFile string
}
// 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
var scriptTpl = `#!/bin/bash
###LOCK###
printf "%%s" "$(basename "$0")" >> %[1]q
printf '\0' >> %[1]q
for arg in "$@"; do
printf "%%s" "$arg" >> %[1]q
printf '\0' >> %[1]q
done
printf '\0' >> %[1]q
%s
`
// 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`
func mockCommand(c *check.C, basename, script, template string) *MockCmd {
var wholeScript bytes.Buffer
var binDir, exeFile, logFile string
var newpath string
if filepath.IsAbs(basename) {
binDir = filepath.Dir(basename)
err := os.MkdirAll(binDir, 0755)
if err != nil {
panic(fmt.Sprintf("cannot create the directory for mocked command %q: %v", basename, err))
}
exeFile = basename
logFile = basename + ".log"
} else {
binDir = c.MkDir()
exeFile = path.Join(binDir, basename)
logFile = path.Join(binDir, basename+".log")
newpath = binDir + ":" + os.Getenv("PATH")
}
fmt.Fprintf(&wholeScript, template, logFile, script)
err := os.WriteFile(exeFile, wholeScript.Bytes(), 0700)
if err != nil {
panic(err)
}
maybeShellcheck(c, script, &wholeScript)
if newpath != "" {
os.Setenv("PATH", binDir+":"+os.Getenv("PATH"))
}
return &MockCmd{binDir: binDir, exeFile: exeFile, logFile: logFile}
}
// 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))
}
// MockLockedCommand is the same as MockCommand(), but the script uses flock to
// 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))
}
// Also mock this command, using the same bindir and log.
// Useful when you want to check the ordering of things.
func (cmd *MockCmd) Also(basename, script string) *MockCmd {
exeFile := path.Join(cmd.binDir, basename)
err := os.WriteFile(exeFile, []byte(fmt.Sprintf(scriptTpl, cmd.logFile, script)), 0700)
if err != nil {
panic(err)
}
return &MockCmd{binDir: cmd.binDir, exeFile: exeFile, logFile: cmd.logFile}
}
// 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.
// of the form:
//
// [][]string{
// {"cmd", "arg1", "arg2"}, // first invocation of "cmd"
// {"cmd", "arg1", "arg2"}, // second invocation of "cmd"
// }
func (cmd *MockCmd) Calls() [][]string {
raw, err := os.ReadFile(cmd.logFile)
if os.IsNotExist(err) {
return nil
}
if err != nil {
panic(err)
}
logContent := strings.TrimSuffix(string(raw), "\000")
allCalls := [][]string{}
calls := strings.Split(logContent, "\000\000")
for _, call := range calls {
call = strings.TrimSuffix(call, "\000")
allCalls = append(allCalls, strings.Split(call, "\000"))
}
return allCalls
}
// 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)
}
}
// BinDir returns the location of the directory holding overridden commands.
func (cmd *MockCmd) BinDir() string {
return cmd.binDir
}
// Exe return the full path of the mock binary
func (cmd *MockCmd) Exe() string {
return filepath.Join(cmd.exeFile)
}