github.com/openshift/source-to-image@v1.4.1-0.20240516041539-bf52fc02204e/pkg/build/strategies/sti/sti_test.go (about)

     1  package sti
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"path/filepath"
     8  	"reflect"
     9  	"regexp/syntax"
    10  	"strings"
    11  	"testing"
    12  
    13  	"github.com/openshift/source-to-image/pkg/api"
    14  	"github.com/openshift/source-to-image/pkg/api/constants"
    15  	"github.com/openshift/source-to-image/pkg/build"
    16  	"github.com/openshift/source-to-image/pkg/docker"
    17  	s2ierr "github.com/openshift/source-to-image/pkg/errors"
    18  	"github.com/openshift/source-to-image/pkg/ignore"
    19  	"github.com/openshift/source-to-image/pkg/scm/downloaders/empty"
    20  	"github.com/openshift/source-to-image/pkg/scm/downloaders/file"
    21  	gitdownloader "github.com/openshift/source-to-image/pkg/scm/downloaders/git"
    22  	"github.com/openshift/source-to-image/pkg/scm/git"
    23  	"github.com/openshift/source-to-image/pkg/test"
    24  	testfs "github.com/openshift/source-to-image/pkg/test/fs"
    25  	"github.com/openshift/source-to-image/pkg/util/fs"
    26  )
    27  
    28  type FakeSTI struct {
    29  	CleanupCalled          bool
    30  	PrepareCalled          bool
    31  	SetupRequired          []string
    32  	SetupOptional          []string
    33  	SetupError             error
    34  	ExistsCalled           bool
    35  	ExistsError            error
    36  	BuildRequest           *api.Config
    37  	BuildResult            *api.Result
    38  	DownloadError          error
    39  	SaveArtifactsCalled    bool
    40  	SaveArtifactsError     error
    41  	FetchSourceCalled      bool
    42  	FetchSourceError       error
    43  	ExecuteCommand         string
    44  	ExecuteUser            string
    45  	ExecuteError           error
    46  	ExpectedError          bool
    47  	LayeredBuildCalled     bool
    48  	LayeredBuildError      error
    49  	PostExecuteDestination string
    50  	PostExecuteContainerID string
    51  	PostExecuteError       error
    52  }
    53  
    54  func newFakeBaseSTI() *STI {
    55  	return &STI{
    56  		config:    &api.Config{},
    57  		result:    &api.Result{},
    58  		docker:    &docker.FakeDocker{},
    59  		installer: &test.FakeInstaller{},
    60  		git:       &test.FakeGit{},
    61  		fs:        &testfs.FakeFileSystem{},
    62  		tar:       &test.FakeTar{},
    63  	}
    64  }
    65  
    66  func newFakeSTI(f *FakeSTI) *STI {
    67  	s := &STI{
    68  		config:        &api.Config{},
    69  		result:        &api.Result{},
    70  		docker:        &docker.FakeDocker{},
    71  		runtimeDocker: &docker.FakeDocker{},
    72  		installer:     &test.FakeInstaller{},
    73  		git:           &test.FakeGit{},
    74  		fs:            &testfs.FakeFileSystem{},
    75  		tar:           &test.FakeTar{},
    76  		preparer:      f,
    77  		ignorer:       &ignore.DockerIgnorer{},
    78  		artifacts:     f,
    79  		scripts:       f,
    80  		garbage:       f,
    81  		layered:       &FakeDockerBuild{f},
    82  	}
    83  	s.source = &gitdownloader.Clone{Git: s.git, FileSystem: s.fs}
    84  	return s
    85  }
    86  
    87  func (f *FakeSTI) Cleanup(*api.Config) {
    88  	f.CleanupCalled = true
    89  }
    90  
    91  func (f *FakeSTI) Prepare(*api.Config) error {
    92  	f.PrepareCalled = true
    93  	f.SetupRequired = []string{constants.Assemble, constants.Run}
    94  	f.SetupOptional = []string{constants.SaveArtifacts}
    95  	return nil
    96  }
    97  
    98  func (f *FakeSTI) Exists(*api.Config) bool {
    99  	f.ExistsCalled = true
   100  	return true
   101  }
   102  
   103  func (f *FakeSTI) Request() *api.Config {
   104  	return f.BuildRequest
   105  }
   106  
   107  func (f *FakeSTI) Result() *api.Result {
   108  	return f.BuildResult
   109  }
   110  
   111  func (f *FakeSTI) Save(*api.Config) error {
   112  	f.SaveArtifactsCalled = true
   113  	return f.SaveArtifactsError
   114  }
   115  
   116  func (f *FakeSTI) fetchSource() error {
   117  	return f.FetchSourceError
   118  }
   119  
   120  func (f *FakeSTI) Download(*api.Config) (*git.SourceInfo, error) {
   121  	return nil, f.DownloadError
   122  }
   123  
   124  func (f *FakeSTI) Execute(command string, user string, r *api.Config) error {
   125  	f.ExecuteCommand = command
   126  	f.ExecuteUser = user
   127  	return f.ExecuteError
   128  }
   129  
   130  func (f *FakeSTI) wasExpectedError(text string) bool {
   131  	return f.ExpectedError
   132  }
   133  
   134  func (f *FakeSTI) PostExecute(id, destination string) error {
   135  	f.PostExecuteContainerID = id
   136  	f.PostExecuteDestination = destination
   137  	return f.PostExecuteError
   138  }
   139  
   140  type FakeDockerBuild struct {
   141  	*FakeSTI
   142  }
   143  
   144  func (f *FakeDockerBuild) Build(*api.Config) (*api.Result, error) {
   145  	f.LayeredBuildCalled = true
   146  	return &api.Result{}, f.LayeredBuildError
   147  }
   148  
   149  func TestDefaultSource(t *testing.T) {
   150  	config := &api.Config{
   151  		Source:       git.MustParse("."),
   152  		DockerConfig: &api.DockerConfig{Endpoint: "unix:///var/run/docker.sock"},
   153  	}
   154  	client, err := docker.NewEngineAPIClient(config.DockerConfig)
   155  	if err != nil {
   156  		t.Fatal(err)
   157  	}
   158  	sti, err := New(client, config, fs.NewFileSystem(), build.Overrides{})
   159  	if err != nil {
   160  		t.Fatal(err)
   161  	}
   162  	if config.Source == nil {
   163  		t.Errorf("Config.Source not set: %v", config.Source)
   164  	}
   165  	if _, ok := sti.source.(*file.File); !ok || sti.source == nil {
   166  		t.Errorf("Source interface not set: %#v", sti.source)
   167  	}
   168  }
   169  
   170  func TestEmptySource(t *testing.T) {
   171  	config := &api.Config{
   172  		Source:       nil,
   173  		DockerConfig: &api.DockerConfig{Endpoint: "unix:///var/run/docker.sock"},
   174  	}
   175  	client, err := docker.NewEngineAPIClient(config.DockerConfig)
   176  	if err != nil {
   177  		t.Fatal(err)
   178  	}
   179  	sti, err := New(client, config, fs.NewFileSystem(), build.Overrides{})
   180  	if err != nil {
   181  		t.Fatal(err)
   182  	}
   183  	if config.Source != nil {
   184  		t.Errorf("Config.Source unexpectantly changed: %v", config.Source)
   185  	}
   186  	if _, ok := sti.source.(*empty.Noop); !ok || sti.source == nil {
   187  		t.Errorf("Source interface not set: %#v", sti.source)
   188  	}
   189  }
   190  
   191  func TestScriptInstallerWithLabels(t *testing.T) {
   192  	config := &api.Config{
   193  		BuilderImageLabels: map[string]string{
   194  			constants.ScriptsURLLabel: "image:///usr/some/dir",
   195  		},
   196  		DockerConfig: &api.DockerConfig{Endpoint: "unix:///var/run/docker.sock"},
   197  	}
   198  	client, err := docker.NewEngineAPIClient(config.DockerConfig)
   199  	if err != nil {
   200  		t.Fatal(err)
   201  	}
   202  	sti, err := New(client, config, fs.NewFileSystem(), build.Overrides{})
   203  	if err != nil {
   204  		t.Fatal(err)
   205  	}
   206  	if config.BuilderImageLabels == nil {
   207  		t.Errorf("Config.BuilderImageLabels unexpectantly changed: %v", config.BuilderImageLabels)
   208  	}
   209  	if sti.installer == nil {
   210  		t.Errorf("sti installer not set")
   211  	}
   212  }
   213  
   214  func TestOverrides(t *testing.T) {
   215  	fd := &FakeSTI{}
   216  	client, err := docker.NewEngineAPIClient(&api.DockerConfig{Endpoint: "unix:///var/run/docker.sock"})
   217  	if err != nil {
   218  		t.Fatal(err)
   219  	}
   220  	sti, err := New(client,
   221  		&api.Config{
   222  			DockerConfig: &api.DockerConfig{Endpoint: "unix:///var/run/docker.sock"},
   223  		},
   224  		fs.NewFileSystem(),
   225  		build.Overrides{
   226  			Downloader: fd,
   227  		},
   228  	)
   229  	if err != nil {
   230  		t.Fatal(err)
   231  	}
   232  	if sti.source != fd {
   233  		t.Errorf("Override of downloader not set: %#v", sti)
   234  	}
   235  }
   236  
   237  func TestBuild(t *testing.T) {
   238  	incrementalTest := []bool{false, true}
   239  	for _, incremental := range incrementalTest {
   240  		fh := &FakeSTI{
   241  			BuildRequest: &api.Config{Incremental: incremental},
   242  			BuildResult:  &api.Result{},
   243  		}
   244  
   245  		builder := newFakeSTI(fh)
   246  		builder.Build(&api.Config{Incremental: incremental})
   247  
   248  		// Verify the right scripts were configed
   249  		if !reflect.DeepEqual(fh.SetupRequired, []string{constants.Assemble, constants.Run}) {
   250  			t.Errorf("Unexpected required scripts configured: %#v", fh.SetupRequired)
   251  		}
   252  		if !reflect.DeepEqual(fh.SetupOptional, []string{constants.SaveArtifacts}) {
   253  			t.Errorf("Unexpected optional scripts configured: %#v", fh.SetupOptional)
   254  		}
   255  
   256  		// Verify that Exists was called
   257  		if !fh.ExistsCalled {
   258  			t.Errorf("Exists was not called.")
   259  		}
   260  
   261  		// Verify that Save was called for an incremental build
   262  		if incremental && !fh.SaveArtifactsCalled {
   263  			t.Errorf("Save artifacts was not called for an incremental build")
   264  		}
   265  
   266  		// Verify that Execute was called with the right script
   267  		if fh.ExecuteCommand != constants.Assemble {
   268  			t.Errorf("Unexpected execute command: %s", fh.ExecuteCommand)
   269  		}
   270  	}
   271  }
   272  
   273  func TestLayeredBuild(t *testing.T) {
   274  	fh := &FakeSTI{
   275  		BuildRequest: &api.Config{
   276  			BuilderImage: "testimage",
   277  		},
   278  		BuildResult: &api.Result{
   279  			BuildInfo: api.BuildInfo{
   280  				Stages: []api.StageInfo{},
   281  			},
   282  		},
   283  		ExecuteError:  errMissingRequirements,
   284  		ExpectedError: true,
   285  	}
   286  	builder := newFakeSTI(fh)
   287  	builder.Build(&api.Config{BuilderImage: "testimage"})
   288  	// Verify layered build
   289  	if !fh.LayeredBuildCalled {
   290  		t.Errorf("Layered build was not called.")
   291  	}
   292  }
   293  
   294  func TestBuildErrorExecute(t *testing.T) {
   295  	fh := &FakeSTI{
   296  		BuildRequest: &api.Config{
   297  			BuilderImage: "testimage",
   298  		},
   299  		BuildResult:   &api.Result{},
   300  		ExecuteError:  errors.New("ExecuteError"),
   301  		ExpectedError: false,
   302  	}
   303  	builder := newFakeSTI(fh)
   304  	_, err := builder.Build(&api.Config{BuilderImage: "testimage"})
   305  	if err == nil || err.Error() != "ExecuteError" {
   306  		t.Errorf("An error was expected, but got different %v", err)
   307  	}
   308  }
   309  
   310  func TestWasExpectedError(t *testing.T) {
   311  	type expErr struct {
   312  		text     string
   313  		expected bool
   314  	}
   315  
   316  	tests := []expErr{
   317  		{ // 0 - tar error
   318  			text:     `/bin/sh: tar: not found`,
   319  			expected: true,
   320  		},
   321  		{ // 1 - tar error
   322  			text:     `/bin/sh: tar: command not found`,
   323  			expected: true,
   324  		},
   325  		{ // 2 - /bin/sh error
   326  			text:     `exec: "/bin/sh": stat /bin/sh: no such file or directory`,
   327  			expected: true,
   328  		},
   329  		{ // 3 - non container error
   330  			text:     "other error",
   331  			expected: false,
   332  		},
   333  	}
   334  
   335  	for i, ti := range tests {
   336  		result := isMissingRequirements(ti.text)
   337  		if result != ti.expected {
   338  			t.Errorf("(%d) Unexpected result: %v. Expected: %v", i, result, ti.expected)
   339  		}
   340  	}
   341  }
   342  
   343  func testBuildHandler() *STI {
   344  	s := &STI{
   345  		docker:            &docker.FakeDocker{},
   346  		incrementalDocker: &docker.FakeDocker{},
   347  		installer:         &test.FakeInstaller{},
   348  		git:               &test.FakeGit{},
   349  		fs:                &testfs.FakeFileSystem{ExistsResult: map[string]bool{filepath.FromSlash("a-repo-source"): true}},
   350  		tar:               &test.FakeTar{},
   351  		config:            &api.Config{},
   352  		result:            &api.Result{},
   353  		callbackInvoker:   &test.FakeCallbackInvoker{},
   354  	}
   355  	s.source = &gitdownloader.Clone{Git: s.git, FileSystem: s.fs}
   356  	s.initPostExecutorSteps()
   357  	return s
   358  }
   359  
   360  func TestPostExecute(t *testing.T) {
   361  	type postExecuteTest struct {
   362  		tag              string
   363  		incremental      bool
   364  		previousImageID  string
   365  		scriptsFromImage bool
   366  	}
   367  	testCases := []postExecuteTest{
   368  		// 0: tagged, incremental, without previous image
   369  		{"test/tag", true, "", true},
   370  		// 1: tagged, incremental, with previous image
   371  		{"test/tag", true, "test-image", true},
   372  		// 2: tagged, no incremental, without previous image
   373  		{"test/tag", false, "", true},
   374  		// 3: tagged, no incremental, with previous image
   375  		{"test/tag", false, "test-image", true},
   376  
   377  		// 4: no tag, incremental, without previous image
   378  		{"", true, "", false},
   379  		// 5: no tag, incremental, with previous image
   380  		{"", true, "test-image", false},
   381  		// 6: no tag, no incremental, without previous image
   382  		{"", false, "", false},
   383  		// 7: no tag, no incremental, with previous image
   384  		{"", false, "test-image", false},
   385  	}
   386  
   387  	for i, tc := range testCases {
   388  		bh := testBuildHandler()
   389  		containerID := "test-container-id"
   390  		bh.result.Messages = []string{"one", "two"}
   391  		bh.config.Tag = tc.tag
   392  		bh.config.Incremental = tc.incremental
   393  		dh := bh.docker.(*docker.FakeDocker)
   394  		if tc.previousImageID != "" {
   395  			bh.config.RemovePreviousImage = true
   396  			bh.incremental = tc.incremental
   397  			bh.docker.(*docker.FakeDocker).GetImageIDResult = tc.previousImageID
   398  		}
   399  		if tc.scriptsFromImage {
   400  			bh.scriptsURL = map[string]string{constants.Run: "image:///usr/libexec/s2i/run"}
   401  		}
   402  		err := bh.PostExecute(containerID, "cmd1")
   403  		if err != nil {
   404  			t.Errorf("(%d) Unexpected error from postExecute: %v", i, err)
   405  		}
   406  		// Ensure CommitContainer was called with the right parameters
   407  		expectedCmd := []string{"cmd1/scripts/" + constants.Run}
   408  		if tc.scriptsFromImage {
   409  			expectedCmd = []string{"/usr/libexec/s2i/" + constants.Run}
   410  		}
   411  		if !reflect.DeepEqual(dh.CommitContainerOpts.Command, expectedCmd) {
   412  			t.Errorf("(%d) Unexpected commit container command: %#v, expected %q", i, dh.CommitContainerOpts.Command, expectedCmd)
   413  		}
   414  		if dh.CommitContainerOpts.Repository != tc.tag {
   415  			t.Errorf("(%d) Unexpected tag committed, expected %q, got %q", i, tc.tag, dh.CommitContainerOpts.Repository)
   416  		}
   417  		// Ensure image removal when incremental and previousImageID present
   418  		if tc.incremental && tc.previousImageID != "" {
   419  			if dh.RemoveImageName != "test-image" {
   420  				t.Errorf("(%d) Previous image was not removed: %q", i, dh.RemoveImageName)
   421  			}
   422  		} else {
   423  			if dh.RemoveImageName != "" {
   424  				t.Errorf("(%d) Unexpected image removed: %s", i, dh.RemoveImageName)
   425  			}
   426  		}
   427  	}
   428  }
   429  
   430  func TestExists(t *testing.T) {
   431  	type incrementalTest struct {
   432  		// incremental flag was passed
   433  		incremental bool
   434  		// previous image existence
   435  		previousImage bool
   436  		// script installed
   437  		scriptInstalled bool
   438  		// expected result
   439  		expected bool
   440  	}
   441  
   442  	tests := []incrementalTest{
   443  		// 0-1: incremental, no image, no matter what with scripts
   444  		{true, false, false, false},
   445  		{true, false, true, false},
   446  
   447  		// 2: incremental, previous image, no scripts
   448  		{true, true, false, false},
   449  		// 3: incremental, previous image, scripts installed
   450  		{true, true, true, true},
   451  
   452  		// 4-7: no incremental build - should always return false no matter what other flags are
   453  		{false, false, false, false},
   454  		{false, false, true, false},
   455  		{false, true, false, false},
   456  		{false, true, true, false},
   457  	}
   458  
   459  	for i, ti := range tests {
   460  		bh := testBuildHandler()
   461  		bh.config.WorkingDir = "/working-dir"
   462  		bh.config.Incremental = ti.incremental
   463  		bh.config.BuilderPullPolicy = api.PullAlways
   464  		bh.installedScripts = map[string]bool{constants.SaveArtifacts: ti.scriptInstalled}
   465  		bh.incrementalDocker.(*docker.FakeDocker).PullResult = ti.previousImage
   466  		bh.config.DockerConfig = &api.DockerConfig{Endpoint: "http://localhost:4243"}
   467  		incremental := bh.Exists(bh.config)
   468  		if incremental != ti.expected {
   469  			t.Errorf("(%d) Unexpected incremental result: %v. Expected: %v",
   470  				i, incremental, ti.expected)
   471  		}
   472  		if ti.incremental && ti.previousImage && ti.scriptInstalled {
   473  			if len(bh.fs.(*testfs.FakeFileSystem).ExistsFile) == 0 {
   474  				continue
   475  			}
   476  			scriptChecked := bh.fs.(*testfs.FakeFileSystem).ExistsFile[0]
   477  			expectedScript := "/working-dir/upload/scripts/save-artifacts"
   478  			if scriptChecked != expectedScript {
   479  				t.Errorf("(%d) Unexpected script checked. Actual: %s. Expected: %s",
   480  					i, scriptChecked, expectedScript)
   481  			}
   482  		}
   483  	}
   484  }
   485  
   486  func TestSaveArtifacts(t *testing.T) {
   487  	bh := testBuildHandler()
   488  	bh.config.WorkingDir = "/working-dir"
   489  	bh.config.Tag = "image/tag"
   490  	fakeFS := bh.fs.(*testfs.FakeFileSystem)
   491  	fd := bh.docker.(*docker.FakeDocker)
   492  	th := bh.tar.(*test.FakeTar)
   493  	err := bh.Save(bh.config)
   494  	if err != nil {
   495  		t.Errorf("Unexpected error when saving artifacts: %v", err)
   496  	}
   497  	expectedArtifactDir := "/working-dir/upload/artifacts"
   498  	if filepath.ToSlash(fakeFS.MkdirDir) != expectedArtifactDir {
   499  		t.Errorf("Mkdir was not called with the expected directory: %s",
   500  			fakeFS.MkdirDir)
   501  	}
   502  	if fd.RunContainerOpts.Image != bh.config.Tag {
   503  		t.Errorf("Unexpected image sent to RunContainer: %s",
   504  			fd.RunContainerOpts.Image)
   505  	}
   506  	if filepath.ToSlash(th.ExtractTarDir) != expectedArtifactDir || th.ExtractTarReader == nil {
   507  		t.Errorf("ExtractTar was not called with the expected parameters.")
   508  	}
   509  }
   510  
   511  func TestSaveArtifactsCustomTag(t *testing.T) {
   512  	bh := testBuildHandler()
   513  	bh.config.WorkingDir = "/working-dir"
   514  	bh.config.IncrementalFromTag = "custom/tag"
   515  	bh.config.Tag = "image/tag"
   516  	fakeFS := bh.fs.(*testfs.FakeFileSystem)
   517  	fd := bh.docker.(*docker.FakeDocker)
   518  	th := bh.tar.(*test.FakeTar)
   519  	err := bh.Save(bh.config)
   520  	if err != nil {
   521  		t.Errorf("Unexpected error when saving artifacts: %v", err)
   522  	}
   523  	expectedArtifactDir := "/working-dir/upload/artifacts"
   524  	if filepath.ToSlash(fakeFS.MkdirDir) != expectedArtifactDir {
   525  		t.Errorf("Mkdir was not called with the expected directory: %s",
   526  			fakeFS.MkdirDir)
   527  	}
   528  	if fd.RunContainerOpts.Image != bh.config.IncrementalFromTag {
   529  		t.Errorf("Unexpected image sent to RunContainer: %s",
   530  			fd.RunContainerOpts.Image)
   531  	}
   532  	if filepath.ToSlash(th.ExtractTarDir) != expectedArtifactDir || th.ExtractTarReader == nil {
   533  		t.Errorf("ExtractTar was not called with the expected parameters.")
   534  	}
   535  }
   536  
   537  func TestSaveArtifactsRunError(t *testing.T) {
   538  	tests := []error{
   539  		fmt.Errorf("Run error"),
   540  		s2ierr.NewContainerError("", -1, ""),
   541  	}
   542  	expected := []error{
   543  		tests[0],
   544  		s2ierr.NewSaveArtifactsError("", "", tests[1]),
   545  	}
   546  	// test with tar extract error or not
   547  	tarError := []bool{true, false}
   548  	for i := range tests {
   549  		for _, te := range tarError {
   550  			bh := testBuildHandler()
   551  			fd := bh.docker.(*docker.FakeDocker)
   552  			th := bh.tar.(*test.FakeTar)
   553  			fd.RunContainerError = tests[i]
   554  			if te {
   555  				th.ExtractTarError = fmt.Errorf("tar error")
   556  			}
   557  			err := bh.Save(bh.config)
   558  			if !te && err != expected[i] {
   559  				t.Errorf("Unexpected error returned from saveArtifacts: %v", err)
   560  			} else if te && err != th.ExtractTarError {
   561  				t.Errorf("Expected tar error. Got %v", err)
   562  			}
   563  		}
   564  	}
   565  }
   566  
   567  func TestSaveArtifactsErrorBeforeStart(t *testing.T) {
   568  	bh := testBuildHandler()
   569  	fd := bh.docker.(*docker.FakeDocker)
   570  	expected := fmt.Errorf("run error")
   571  	fd.RunContainerError = expected
   572  	fd.RunContainerErrorBeforeStart = true
   573  	err := bh.Save(bh.config)
   574  	if err != expected {
   575  		t.Errorf("Unexpected error returned from saveArtifacts: %v", err)
   576  	}
   577  }
   578  
   579  func TestSaveArtifactsExtractError(t *testing.T) {
   580  	bh := testBuildHandler()
   581  	th := bh.tar.(*test.FakeTar)
   582  	expected := fmt.Errorf("extract error")
   583  	th.ExtractTarError = expected
   584  	err := bh.Save(bh.config)
   585  	if err != expected {
   586  		t.Errorf("Unexpected error returned from saveArtifacts: %v", err)
   587  	}
   588  }
   589  
   590  func TestFetchSource(t *testing.T) {
   591  	type fetchTest struct {
   592  		refSpecified     bool
   593  		checkoutExpected bool
   594  	}
   595  
   596  	tests := []fetchTest{
   597  		// 0
   598  		{
   599  			refSpecified:     false,
   600  			checkoutExpected: false,
   601  		},
   602  		// 1
   603  		{
   604  			refSpecified:     true,
   605  			checkoutExpected: true,
   606  		},
   607  	}
   608  
   609  	for testNum, ft := range tests {
   610  		bh := testBuildHandler()
   611  		gh := bh.git.(*test.FakeGit)
   612  
   613  		bh.config.WorkingDir = "/working-dir"
   614  		bh.config.Source = git.MustParse("a-repo-source")
   615  		if ft.refSpecified {
   616  			bh.config.Source.URL.Fragment = "a-branch"
   617  		}
   618  
   619  		expectedTargetDir := "/working-dir/upload/src"
   620  		_, e := bh.source.Download(bh.config)
   621  		if e != nil {
   622  			t.Errorf("Unexpected error %v [%d]", e, testNum)
   623  		}
   624  		if gh.CloneSource.StringNoFragment() != "a-repo-source" {
   625  			t.Errorf("Clone was not called with the expected source. Got %s, expected %s [%d]", gh.CloneSource, "a-source-repo-source", testNum)
   626  		}
   627  		if filepath.ToSlash(gh.CloneTarget) != expectedTargetDir {
   628  			t.Errorf("Unexpected target directory for clone operation. Got %s, expected %s [%d]", gh.CloneTarget, expectedTargetDir, testNum)
   629  		}
   630  		if ft.checkoutExpected {
   631  			if gh.CheckoutRef != "a-branch" {
   632  				t.Errorf("Checkout was not called with the expected branch. Got %s, expected %s [%d]", gh.CheckoutRef, "a-branch", testNum)
   633  			}
   634  			if filepath.ToSlash(gh.CheckoutRepo) != expectedTargetDir {
   635  				t.Errorf("Unexpected target repository for checkout operation. Got %s, expected %s [%d]", gh.CheckoutRepo, expectedTargetDir, testNum)
   636  			}
   637  		}
   638  	}
   639  }
   640  
   641  func TestPrepareOK(t *testing.T) {
   642  	rh := newFakeSTI(&FakeSTI{})
   643  	rh.SetScripts([]string{constants.Assemble, constants.Run}, []string{constants.SaveArtifacts})
   644  	rh.fs.(*testfs.FakeFileSystem).WorkingDirResult = "/working-dir"
   645  	err := rh.Prepare(rh.config)
   646  	if err != nil {
   647  		t.Errorf("An error occurred setting up the config handler: %v", err)
   648  	}
   649  	if !rh.fs.(*testfs.FakeFileSystem).WorkingDirCalled {
   650  		t.Errorf("Working directory was not created.")
   651  	}
   652  	var expected []string
   653  	for _, dir := range workingDirs {
   654  		expected = append(expected, filepath.FromSlash("/working-dir/"+dir))
   655  	}
   656  	mkdirs := rh.fs.(*testfs.FakeFileSystem).MkdirAllDir
   657  	if !reflect.DeepEqual(mkdirs, expected) {
   658  		t.Errorf("Unexpected set of MkdirAll calls: %#v", mkdirs)
   659  	}
   660  	scripts := rh.installer.(*test.FakeInstaller).Scripts
   661  	if !reflect.DeepEqual(scripts[0], []string{constants.Assemble, constants.Run}) {
   662  		t.Errorf("Unexpected set of required scripts: %#v", scripts[0])
   663  	}
   664  	if !reflect.DeepEqual(scripts[1], []string{constants.SaveArtifacts}) {
   665  		t.Errorf("Unexpected set of optional scripts: %#v", scripts[1])
   666  	}
   667  }
   668  
   669  func TestPrepareErrorCreatingWorkingDir(t *testing.T) {
   670  	rh := newFakeSTI(&FakeSTI{})
   671  	rh.fs.(*testfs.FakeFileSystem).WorkingDirError = errors.New("WorkingDirError")
   672  	err := rh.Prepare(rh.config)
   673  	if err == nil || err.Error() != "WorkingDirError" {
   674  		t.Errorf("An error was expected for WorkingDir, but got different: %v", err)
   675  	}
   676  }
   677  
   678  func TestPrepareErrorMkdirAll(t *testing.T) {
   679  	rh := newFakeSTI(&FakeSTI{})
   680  	rh.fs.(*testfs.FakeFileSystem).MkdirAllError = errors.New("MkdirAllError")
   681  	err := rh.Prepare(rh.config)
   682  	if err == nil || err.Error() != "MkdirAllError" {
   683  		t.Errorf("An error was expected for MkdirAll, but got different: %v", err)
   684  	}
   685  }
   686  
   687  func TestPrepareErrorRequiredDownloadAndInstall(t *testing.T) {
   688  	rh := newFakeSTI(&FakeSTI{})
   689  	rh.SetScripts([]string{constants.Assemble, constants.Run}, []string{constants.SaveArtifacts})
   690  	rh.installer.(*test.FakeInstaller).Error = fmt.Errorf("%v", constants.Assemble)
   691  	err := rh.Prepare(rh.config)
   692  	if err == nil || err.Error() != constants.Assemble {
   693  		t.Errorf("An error was expected for required DownloadAndInstall, but got different: %v", err)
   694  	}
   695  }
   696  
   697  func TestPrepareErrorOptionalDownloadAndInstall(t *testing.T) {
   698  	rh := newFakeSTI(&FakeSTI{})
   699  	rh.SetScripts([]string{constants.Assemble, constants.Run}, []string{constants.SaveArtifacts})
   700  	err := rh.Prepare(rh.config)
   701  	if err != nil {
   702  		t.Errorf("Unexpected error when downloading optional scripts: %v", err)
   703  	}
   704  }
   705  
   706  func TestPrepareUseCustomRuntimeArtifacts(t *testing.T) {
   707  	expectedMapping := filepath.FromSlash("/src") + ":dst"
   708  
   709  	builder := newFakeSTI(&FakeSTI{})
   710  
   711  	config := builder.config
   712  	config.RuntimeImage = "my-app"
   713  	config.RuntimeArtifacts.Set(expectedMapping)
   714  
   715  	if err := builder.Prepare(config); err != nil {
   716  		t.Fatalf("Prepare() unexpectedly failed with error: %v", err)
   717  	}
   718  
   719  	if actualMapping := config.RuntimeArtifacts.String(); actualMapping != expectedMapping {
   720  		t.Errorf("Prepare() shouldn't change mapping, but it was modified from %v to %v", expectedMapping, actualMapping)
   721  	}
   722  }
   723  
   724  func TestPrepareFailForEmptyRuntimeArtifacts(t *testing.T) {
   725  	builder := newFakeSTI(&FakeSTI{})
   726  
   727  	fakeDocker := builder.docker.(*docker.FakeDocker)
   728  	fakeDocker.AssembleInputFilesResult = ""
   729  
   730  	config := builder.config
   731  	config.RuntimeImage = "my-app"
   732  
   733  	if len(config.RuntimeArtifacts) > 0 {
   734  		t.Fatalf("RuntimeArtifacts must be empty by default")
   735  	}
   736  
   737  	err := builder.Prepare(config)
   738  	if err == nil {
   739  		t.Errorf("Prepare() should fail but it didn't")
   740  
   741  	} else if expectedError := "no runtime artifacts to copy"; !strings.Contains(err.Error(), expectedError) {
   742  		t.Errorf("Prepare() should fail with error that contains text %q but failed with error: %q", expectedError, err)
   743  	}
   744  }
   745  
   746  func TestPrepareRuntimeArtifactsValidation(t *testing.T) {
   747  	testCases := []struct {
   748  		mapping       string
   749  		expectedError string
   750  	}{
   751  		{
   752  			mapping:       "src:dst",
   753  			expectedError: "source must be an absolute path",
   754  		},
   755  		{
   756  			mapping:       "/src:/dst",
   757  			expectedError: "destination must be a relative path",
   758  		},
   759  		{
   760  			mapping:       "/src:../dst",
   761  			expectedError: "destination cannot start with '..'",
   762  		},
   763  	}
   764  
   765  	for _, testCase := range testCases {
   766  		for _, mappingFromUser := range []bool{true, false} {
   767  			builder := newFakeSTI(&FakeSTI{})
   768  
   769  			config := builder.config
   770  			config.RuntimeImage = "my-app"
   771  
   772  			if mappingFromUser {
   773  				config.RuntimeArtifacts.Set(testCase.mapping)
   774  			} else {
   775  				fakeDocker := builder.docker.(*docker.FakeDocker)
   776  				fakeDocker.AssembleInputFilesResult = testCase.mapping
   777  			}
   778  
   779  			err := builder.Prepare(config)
   780  			if err == nil {
   781  				t.Errorf("Prepare() should fail but it didn't")
   782  
   783  			} else if !strings.Contains(err.Error(), testCase.expectedError) {
   784  				t.Errorf("Prepare() should fail to validate mapping %q with error that contains text %q but failed with error: %q", testCase.mapping, testCase.expectedError, err)
   785  			}
   786  		}
   787  	}
   788  }
   789  
   790  func TestPrepareSetRuntimeArtifacts(t *testing.T) {
   791  	for _, mapping := range []string{filepath.FromSlash("/src") + ":dst", filepath.FromSlash("/src1") + ":dst1;" + filepath.FromSlash("/src1") + ":dst1"} {
   792  		expectedMapping := strings.Replace(mapping, ";", ",", -1)
   793  
   794  		builder := newFakeSTI(&FakeSTI{})
   795  
   796  		fakeDocker := builder.docker.(*docker.FakeDocker)
   797  		fakeDocker.AssembleInputFilesResult = mapping
   798  
   799  		config := builder.config
   800  		config.RuntimeImage = "my-app"
   801  
   802  		if len(config.RuntimeArtifacts) > 0 {
   803  			t.Fatalf("RuntimeArtifacts must be empty by default")
   804  		}
   805  
   806  		if err := builder.Prepare(config); err != nil {
   807  			t.Fatalf("Prepare() unexpectedly failed with error: %v", err)
   808  		}
   809  
   810  		if actualMapping := config.RuntimeArtifacts.String(); actualMapping != expectedMapping {
   811  			t.Errorf("Prepare() shouldn't change mapping, but it was modified from %v to %v", expectedMapping, actualMapping)
   812  		}
   813  	}
   814  }
   815  
   816  func TestPrepareDownloadAssembleRuntime(t *testing.T) {
   817  	installer := &test.FakeInstaller{}
   818  
   819  	builder := newFakeSTI(&FakeSTI{})
   820  	builder.runtimeInstaller = installer
   821  	builder.optionalRuntimeScripts = []string{constants.AssembleRuntime}
   822  
   823  	config := builder.config
   824  	config.RuntimeImage = "my-app"
   825  	config.RuntimeArtifacts.Set("/src:dst")
   826  
   827  	if err := builder.Prepare(config); err != nil {
   828  		t.Fatalf("Prepare() unexpectedly failed with error: %v", err)
   829  	}
   830  
   831  	if len(installer.Scripts) != 1 || installer.Scripts[0][0] != constants.AssembleRuntime {
   832  		t.Errorf("Prepare() should download %q script but it downloaded %v", constants.AssembleRuntime, installer.Scripts)
   833  	}
   834  }
   835  
   836  func TestExecuteOK(t *testing.T) {
   837  	rh := newFakeBaseSTI()
   838  	pe := &FakeSTI{}
   839  	rh.postExecutor = pe
   840  	rh.config.WorkingDir = "/working-dir"
   841  	rh.config.BuilderImage = "test/image"
   842  	rh.config.BuilderPullPolicy = api.PullAlways
   843  	rh.config.Environment = api.EnvironmentList{
   844  		api.EnvironmentSpec{
   845  			Name:  "Key1",
   846  			Value: "Value1",
   847  		},
   848  		api.EnvironmentSpec{
   849  			Name:  "Key2",
   850  			Value: "Value2",
   851  		},
   852  	}
   853  	expectedEnv := []string{"Key1=Value1", "Key2=Value2"}
   854  	th := rh.tar.(*test.FakeTar)
   855  	th.CreateTarResult = "/working-dir/test.tar"
   856  	fd := rh.docker.(*docker.FakeDocker)
   857  	fd.RunContainerContainerID = "1234"
   858  	fd.RunContainerCmd = []string{"one", "two"}
   859  
   860  	err := rh.Execute("test-command", "foo", rh.config)
   861  	if err != nil {
   862  		t.Errorf("Unexpected error returned: %v", err)
   863  	}
   864  	th = rh.tar.(*test.FakeTar).Copy()
   865  	if th.CreateTarBase != "" {
   866  		t.Errorf("Unexpected tar base directory: %s", th.CreateTarBase)
   867  	}
   868  	if filepath.ToSlash(th.CreateTarDir) != "/working-dir/upload" {
   869  		t.Errorf("Unexpected tar directory: %s", th.CreateTarDir)
   870  	}
   871  	fh, ok := rh.fs.(*testfs.FakeFileSystem)
   872  	if !ok {
   873  		t.Fatalf("Unable to convert %v to FakeFilesystem", rh.fs)
   874  	}
   875  	if fh.OpenFile != "" {
   876  		t.Fatalf("Unexpected file opened: %s", fh.OpenFile)
   877  	}
   878  	if fh.OpenFileResult != nil {
   879  		t.Errorf("Tar file was opened.")
   880  	}
   881  	ro := fd.RunContainerOpts
   882  
   883  	if ro.User != "foo" {
   884  		t.Errorf("Expected user to be foo, got %q", ro.User)
   885  	}
   886  
   887  	if ro.Image != rh.config.BuilderImage {
   888  		t.Errorf("Unexpected Image passed to RunContainer")
   889  	}
   890  	if _, ok := ro.Stdin.(*io.PipeReader); !ok {
   891  		t.Errorf("Unexpected input stream: %#v", ro.Stdin)
   892  	}
   893  	if ro.PullImage {
   894  		t.Errorf("PullImage is true for RunContainer, should be false")
   895  	}
   896  	if ro.Command != "test-command" {
   897  		t.Errorf("Unexpected command passed to RunContainer: %s",
   898  			ro.Command)
   899  	}
   900  	if pe.PostExecuteContainerID != "1234" {
   901  		t.Errorf("PostExecutor not called with expected ID: %s",
   902  			pe.PostExecuteContainerID)
   903  	}
   904  	if !reflect.DeepEqual(ro.Env, expectedEnv) {
   905  		t.Errorf("Unexpected container environment passed to RunContainer: %v, should be %v", ro.Env, expectedEnv)
   906  	}
   907  	if !reflect.DeepEqual(pe.PostExecuteDestination, "test-command") {
   908  		t.Errorf("PostExecutor not called with expected command: %s", pe.PostExecuteDestination)
   909  	}
   910  }
   911  
   912  func TestExecuteRunContainerError(t *testing.T) {
   913  	rh := newFakeSTI(&FakeSTI{})
   914  	fd := rh.docker.(*docker.FakeDocker)
   915  	runContainerError := fmt.Errorf("an error")
   916  	fd.RunContainerError = runContainerError
   917  	err := rh.Execute("test-command", "", rh.config)
   918  	if err != runContainerError {
   919  		t.Errorf("Did not get expected error, got %v", err)
   920  	}
   921  }
   922  
   923  func TestExecuteErrorCreateTarFile(t *testing.T) {
   924  	rh := newFakeSTI(&FakeSTI{})
   925  	rh.tar.(*test.FakeTar).CreateTarError = errors.New("CreateTarError")
   926  	err := rh.Execute("test-command", "", rh.config)
   927  	if err == nil || err.Error() != "CreateTarError" {
   928  		t.Errorf("An error was expected for CreateTarFile, but got different: %#v", err)
   929  	}
   930  }
   931  
   932  func TestCleanup(t *testing.T) {
   933  	rh := newFakeBaseSTI()
   934  
   935  	rh.config.WorkingDir = "/working-dir"
   936  	preserve := []bool{false, true}
   937  	for _, p := range preserve {
   938  		rh.config.PreserveWorkingDir = p
   939  		rh.fs = &testfs.FakeFileSystem{}
   940  		rh.garbage = build.NewDefaultCleaner(rh.fs, rh.docker)
   941  		rh.garbage.Cleanup(rh.config)
   942  		removedDir := rh.fs.(*testfs.FakeFileSystem).RemoveDirName
   943  		if p && removedDir != "" {
   944  			t.Errorf("Expected working directory to be preserved, but it was removed.")
   945  		} else if !p && removedDir == "" {
   946  			t.Errorf("Expected working directory to be removed, but it was preserved.")
   947  		}
   948  	}
   949  }
   950  
   951  func TestNewWithInvalidExcludeRegExp(t *testing.T) {
   952  	_, err := New(nil, &api.Config{
   953  		DockerConfig:  docker.GetDefaultDockerConfig(),
   954  		ExcludeRegExp: "(",
   955  	}, nil, build.Overrides{})
   956  	if syntaxErr, ok := err.(*syntax.Error); ok && syntaxErr.Code != syntax.ErrMissingParen {
   957  		t.Errorf("expected regexp compilation error, got %v", err)
   958  	}
   959  }