github.com/stevenmatthewt/agent@v3.5.4+incompatible/bootstrap/integration/bootstrap_tester.go (about) 1 package integration 2 3 import ( 4 "bufio" 5 "bytes" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "log" 10 "os" 11 "os/exec" 12 "path/filepath" 13 "regexp" 14 "runtime" 15 "strings" 16 "sync" 17 "syscall" 18 "testing" 19 20 "github.com/buildkite/bintest" 21 ) 22 23 // BootstrapTester invokes a buildkite-agent bootstrap script with a temporary environment 24 type BootstrapTester struct { 25 Name string 26 Args []string 27 Env []string 28 HomeDir string 29 PathDir string 30 BuildDir string 31 HooksDir string 32 PluginsDir string 33 Repo *gitRepository 34 Output string 35 36 cmd *exec.Cmd 37 cmdLock sync.Mutex 38 hookMock *bintest.Mock 39 mocks []*bintest.Mock 40 } 41 42 func NewBootstrapTester() (*BootstrapTester, error) { 43 homeDir, err := ioutil.TempDir("", "home") 44 if err != nil { 45 return nil, err 46 } 47 48 pathDir, err := ioutil.TempDir("", "bootstrap-path") 49 if err != nil { 50 return nil, err 51 } 52 53 buildDir, err := ioutil.TempDir("", "bootstrap-builds") 54 if err != nil { 55 return nil, err 56 } 57 58 hooksDir, err := ioutil.TempDir("", "bootstrap-hooks") 59 if err != nil { 60 return nil, err 61 } 62 63 pluginsDir, err := ioutil.TempDir("", "bootstrap-plugins") 64 if err != nil { 65 return nil, err 66 } 67 68 repo, err := createTestGitRespository() 69 if err != nil { 70 return nil, err 71 } 72 73 bt := &BootstrapTester{ 74 Name: os.Args[0], 75 Args: []string{"bootstrap"}, 76 Repo: repo, 77 Env: []string{ 78 "HOME=" + homeDir, 79 "BUILDKITE_BIN_PATH=" + pathDir, 80 "BUILDKITE_BUILD_PATH=" + buildDir, 81 "BUILDKITE_HOOKS_PATH=" + hooksDir, 82 "BUILDKITE_PLUGINS_PATH=" + pluginsDir, 83 `BUILDKITE_REPO=` + repo.Path, 84 `BUILDKITE_AGENT_DEBUG=true`, 85 `BUILDKITE_AGENT_NAME=test-agent`, 86 `BUILDKITE_ORGANIZATION_SLUG=test`, 87 `BUILDKITE_PIPELINE_SLUG=test-project`, 88 `BUILDKITE_PULL_REQUEST=`, 89 `BUILDKITE_PIPELINE_PROVIDER=git`, 90 `BUILDKITE_COMMIT=HEAD`, 91 `BUILDKITE_BRANCH=master`, 92 `BUILDKITE_COMMAND_EVAL=true`, 93 `BUILDKITE_ARTIFACT_PATHS=`, 94 `BUILDKITE_COMMAND=true`, 95 `BUILDKITE_JOB_ID=1111-1111-1111-1111`, 96 `BUILDKITE_AGENT_ACCESS_TOKEN=test`, 97 }, 98 PathDir: pathDir, 99 BuildDir: buildDir, 100 HooksDir: hooksDir, 101 PluginsDir: pluginsDir, 102 } 103 104 // Windows requires certain env variables to be present 105 if runtime.GOOS == "windows" { 106 bt.Env = append(bt.Env, 107 "PATH="+pathDir+";"+os.Getenv("PATH"), 108 "SystemRoot="+os.Getenv("SystemRoot"), 109 "WINDIR="+os.Getenv("WINDIR"), 110 "COMSPEC="+os.Getenv("COMSPEC"), 111 "PATHEXT="+os.Getenv("PATHEXT"), 112 "TMP="+os.Getenv("TMP"), 113 "TEMP="+os.Getenv("TEMP"), 114 ) 115 } else { 116 bt.Env = append(bt.Env, 117 "PATH="+pathDir+":"+os.Getenv("PATH"), 118 ) 119 } 120 121 // Create a mock used for hook assertions 122 hook, err := bt.Mock("buildkite-agent-hooks") 123 if err != nil { 124 return nil, err 125 } 126 bt.hookMock = hook 127 128 return bt, nil 129 } 130 131 // Mock creates a mock for a binary using bintest 132 func (b *BootstrapTester) Mock(name string) (*bintest.Mock, error) { 133 mock, err := bintest.NewMock(filepath.Join(b.PathDir, name)) 134 if err != nil { 135 return mock, err 136 } 137 138 b.mocks = append(b.mocks, mock) 139 return mock, err 140 } 141 142 // MustMock will fail the test if creating the mock fails 143 func (b *BootstrapTester) MustMock(t *testing.T, name string) *bintest.Mock { 144 mock, err := b.Mock(name) 145 if err != nil { 146 t.Fatal(err) 147 } 148 return mock 149 } 150 151 // HasMock returns true if a mock has been created by that name 152 func (b *BootstrapTester) HasMock(name string) bool { 153 for _, m := range b.mocks { 154 if strings.TrimSuffix(m.Name, filepath.Ext(m.Name)) == name { 155 return true 156 } 157 } 158 return false 159 } 160 161 // writeHookScript generates a buildkite-agent hook script that calls a mock binary 162 func (b *BootstrapTester) writeHookScript(m *bintest.Mock, name string, dir string, args ...string) (string, error) { 163 hookScript := filepath.Join(dir, name) 164 body := "" 165 166 if runtime.GOOS == "windows" { 167 body = fmt.Sprintf("@\"%s\" %s", m.Path, strings.Join(args, " ")) 168 hookScript += ".bat" 169 } else { 170 body = "#!/bin/sh\n" + strings.Join(append([]string{m.Path}, args...), " ") 171 } 172 173 if err := os.MkdirAll(dir, 0700); err != nil { 174 return "", err 175 } 176 177 return hookScript, ioutil.WriteFile(hookScript, []byte(body), 0600) 178 } 179 180 // ExpectLocalHook creates a mock object and a script in the git repository's buildkite hooks dir 181 // that proxies to the mock. This lets you set up expectations on a local hook 182 func (b *BootstrapTester) ExpectLocalHook(name string) *bintest.Expectation { 183 hooksDir := filepath.Join(b.Repo.Path, ".buildkite", "hooks") 184 185 if err := os.MkdirAll(hooksDir, 0700); err != nil { 186 panic(err) 187 } 188 189 hookPath, err := b.writeHookScript(b.hookMock, name, hooksDir, "local", name) 190 if err != nil { 191 panic(err) 192 } 193 194 if err = b.Repo.Add(hookPath); err != nil { 195 panic(err) 196 } 197 if err = b.Repo.Commit("Added local hook file %s", name); err != nil { 198 panic(err) 199 } 200 201 return b.hookMock.Expect("local", name) 202 } 203 204 // ExpectGlobalHook creates a mock object and a script in the global buildkite hooks dir 205 // that proxies to the mock. This lets you set up expectations on a global hook 206 func (b *BootstrapTester) ExpectGlobalHook(name string) *bintest.Expectation { 207 _, err := b.writeHookScript(b.hookMock, name, b.HooksDir, "global", name) 208 if err != nil { 209 panic(err) 210 } 211 212 return b.hookMock.Expect("global", name) 213 } 214 215 // Run the bootstrap and return any errors 216 func (b *BootstrapTester) Run(t *testing.T, env ...string) error { 217 // Mock out the meta-data calls to the agent after checkout 218 if !b.HasMock("buildkite-agent") { 219 agent := b.MustMock(t, "buildkite-agent") 220 agent. 221 Expect("meta-data", "exists", "buildkite:git:commit"). 222 Optionally(). 223 AndExitWith(0) 224 } 225 226 path, err := exec.LookPath(b.Name) 227 if err != nil { 228 return err 229 } 230 231 b.cmdLock.Lock() 232 b.cmd = exec.Command(path, b.Args...) 233 234 buf := &buffer{} 235 236 if os.Getenv(`DEBUG_BOOTSTRAP`) == "1" { 237 w := newTestLogWriter(t) 238 b.cmd.Stdout = io.MultiWriter(buf, w) 239 b.cmd.Stderr = io.MultiWriter(buf, w) 240 } else { 241 b.cmd.Stdout = buf 242 b.cmd.Stderr = buf 243 } 244 245 b.cmd.Env = append(b.Env, env...) 246 247 err = b.cmd.Start() 248 if err != nil { 249 b.cmdLock.Unlock() 250 return err 251 } 252 253 b.cmdLock.Unlock() 254 255 err = b.cmd.Wait() 256 b.Output = buf.String() 257 return err 258 } 259 260 func (b *BootstrapTester) Cancel() error { 261 b.cmdLock.Lock() 262 defer b.cmdLock.Unlock() 263 log.Printf("Killing pid %d", b.cmd.Process.Pid) 264 return b.cmd.Process.Signal(syscall.SIGINT) 265 } 266 267 func (b *BootstrapTester) CheckMocks(t *testing.T) { 268 for _, mock := range b.mocks { 269 if !mock.Check(t) { 270 return 271 } 272 } 273 } 274 275 func (b *BootstrapTester) CheckoutDir() string { 276 return filepath.Join(b.BuildDir, "test-agent", "test", "test-project") 277 } 278 279 func (b *BootstrapTester) ReadEnvFromOutput(key string) (string, bool) { 280 re := regexp.MustCompile(key + "=(.+)\n") 281 matches := re.FindStringSubmatch(b.Output) 282 if len(matches) == 0 { 283 return "", false 284 } 285 return matches[1], true 286 } 287 288 // Run the bootstrap and then check the mocks 289 func (b *BootstrapTester) RunAndCheck(t *testing.T, env ...string) { 290 if err := b.Run(t, env...); err != nil { 291 t.Logf("Bootstrap output:\n%s", b.Output) 292 t.Fatal(err) 293 } 294 b.CheckMocks(t) 295 } 296 297 // Close the tester, delete all the directories and mocks 298 func (b *BootstrapTester) Close() error { 299 for _, mock := range b.mocks { 300 if err := mock.Close(); err != nil { 301 return err 302 } 303 } 304 if b.Repo != nil { 305 if err := b.Repo.Close(); err != nil { 306 return err 307 } 308 } 309 if err := os.RemoveAll(b.HomeDir); err != nil { 310 return err 311 } 312 if err := os.RemoveAll(b.BuildDir); err != nil { 313 return err 314 } 315 if err := os.RemoveAll(b.HooksDir); err != nil { 316 return err 317 } 318 if err := os.RemoveAll(b.PathDir); err != nil { 319 return err 320 } 321 if err := os.RemoveAll(b.PluginsDir); err != nil { 322 return err 323 } 324 return nil 325 } 326 327 type testLogWriter struct { 328 io.Writer 329 sync.Mutex 330 } 331 332 func newTestLogWriter(t *testing.T) *testLogWriter { 333 r, w := io.Pipe() 334 in := bufio.NewScanner(r) 335 lw := &testLogWriter{Writer: w} 336 337 go func() { 338 for in.Scan() { 339 lw.Lock() 340 t.Logf("%s", in.Text()) 341 lw.Unlock() 342 } 343 344 if err := in.Err(); err != nil { 345 t.Errorf("Error with log writer: %v", err) 346 r.CloseWithError(err) 347 } else { 348 r.Close() 349 } 350 }() 351 352 return lw 353 } 354 355 type buffer struct { 356 b bytes.Buffer 357 m sync.Mutex 358 } 359 360 func (b *buffer) Read(p []byte) (n int, err error) { 361 b.m.Lock() 362 defer b.m.Unlock() 363 return b.b.Read(p) 364 } 365 366 func (b *buffer) Write(p []byte) (n int, err error) { 367 b.m.Lock() 368 defer b.m.Unlock() 369 return b.b.Write(p) 370 } 371 372 func (b *buffer) String() string { 373 b.m.Lock() 374 defer b.m.Unlock() 375 return b.b.String() 376 }