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  }