github.com/joshdk/godel@v0.0.0-20170529232908-862138a45aee/apps/distgo/cmd/build/build_test.go (about)

     1  // Copyright 2016 Palantir Technologies, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package build_test
    16  
    17  import (
    18  	"bytes"
    19  	"fmt"
    20  	"io/ioutil"
    21  	"os"
    22  	"os/exec"
    23  	"path"
    24  	"reflect"
    25  	"regexp"
    26  	"strings"
    27  	"testing"
    28  
    29  	"github.com/nmiyake/pkg/dirs"
    30  	"github.com/palantir/pkg/pkgpath"
    31  	"github.com/stretchr/testify/assert"
    32  	"github.com/stretchr/testify/require"
    33  
    34  	"github.com/palantir/godel/apps/distgo/cmd"
    35  	"github.com/palantir/godel/apps/distgo/cmd/build"
    36  	"github.com/palantir/godel/apps/distgo/params"
    37  	"github.com/palantir/godel/apps/distgo/pkg/binspec"
    38  	"github.com/palantir/godel/apps/distgo/pkg/git"
    39  	"github.com/palantir/godel/apps/distgo/pkg/git/gittest"
    40  	"github.com/palantir/godel/apps/distgo/pkg/osarch"
    41  )
    42  
    43  const (
    44  	testMain = `package main
    45  
    46  import "fmt"
    47  
    48  var testVersionVar = "defaultVersion"
    49  
    50  func main() {
    51  	fmt.Println(testVersionVar)
    52  }
    53  `
    54  	testCMain = `package main
    55  
    56  import "C"
    57  import "fmt"
    58  
    59  func main() {
    60  	fmt.Println("C")
    61  }`
    62  	testVersionValue = "1.0.1"
    63  	testBuildScript  = `package main
    64  
    65  import (
    66  	"fmt"
    67  	"./dependency" // written by the build script
    68  )
    69  
    70  func main() {
    71  	fmt.Println(dependency.V)
    72  }
    73  `
    74  	longCompileMain = `package main
    75  
    76  import (
    77  	"encoding/json"
    78  	"net/http"
    79  )
    80  
    81  func main() {
    82  	http.Get("")
    83  	json.Marshal("")
    84  }
    85  `
    86  )
    87  
    88  func TestBuildAll(t *testing.T) {
    89  	tmp, cleanup, err := dirs.TempDir("", "")
    90  	defer cleanup()
    91  	require.NoError(t, err)
    92  
    93  	for i, currCase := range []struct {
    94  		productName     string
    95  		mainFileContent string
    96  		mainFilePath    string
    97  		params          params.Product
    98  		wantError       bool
    99  		runExecutable   bool
   100  		wantOutput      string
   101  	}{
   102  		{
   103  			productName:     "randomProduct",
   104  			mainFileContent: testMain,
   105  			mainFilePath:    "main.go",
   106  			params: params.Product{
   107  				Build: params.Build{
   108  					MainPkg:    "./.",
   109  					VersionVar: "main.testVersionVar",
   110  					OSArchs: []osarch.OSArch{
   111  						osarch.Current(),
   112  					},
   113  				},
   114  			},
   115  			runExecutable: true,
   116  			wantOutput:    testVersionValue + ".dirty",
   117  		},
   118  		// building project that requires CGo succeeds if "CGO_ENABLED" environment variable is set to 1
   119  		{
   120  			productName:     "CProduct",
   121  			mainFileContent: testCMain,
   122  			mainFilePath:    "main.go",
   123  			params: params.Product{
   124  				Build: params.Build{
   125  					MainPkg: "./.",
   126  					Environment: map[string]string{
   127  						"CGO_ENABLED": "1",
   128  					},
   129  					OSArchs: []osarch.OSArch{
   130  						osarch.Current(),
   131  					},
   132  				},
   133  			},
   134  			runExecutable: true,
   135  			wantOutput:    "C",
   136  		},
   137  		// building project that requires CGo fails if "CGO_ENABLED" environment variable is set to 0
   138  		{
   139  			productName:     "CProduct",
   140  			mainFileContent: testCMain,
   141  			mainFilePath:    "main.go",
   142  			params: params.Product{
   143  				Build: params.Build{
   144  					MainPkg: "./.",
   145  					Environment: map[string]string{
   146  						"CGO_ENABLED": "0",
   147  					},
   148  					OSArchs: []osarch.OSArch{
   149  						osarch.Current(),
   150  					},
   151  				},
   152  			},
   153  			wantError: true,
   154  		},
   155  		{
   156  			productName:     "preBuildScript",
   157  			mainFileContent: testBuildScript,
   158  			mainFilePath:    "main.go",
   159  			params: params.Product{
   160  				Build: params.Build{
   161  					Script: "" +
   162  						"mkdir dependency\n" +
   163  						"echo 'package dependency\n\nvar V = `success`\n' > dependency/lib.go\n",
   164  					MainPkg: "./.",
   165  					OSArchs: []osarch.OSArch{
   166  						osarch.Current(),
   167  					},
   168  				},
   169  			},
   170  			wantOutput: "success",
   171  		},
   172  		{
   173  			productName:     "customBuildScriptProduct",
   174  			mainFileContent: testMain,
   175  			mainFilePath:    "main.go",
   176  			params: params.Product{
   177  				Build: params.Build{
   178  					MainPkg: "./.",
   179  					BuildArgsScript: `set -eu pipefail
   180  VALUE="foo bar"
   181  echo "-ldflags"
   182  echo "-X \"main.testVersionVar=$VALUE\""`,
   183  					OSArchs: []osarch.OSArch{
   184  						osarch.Current(),
   185  					},
   186  				},
   187  			},
   188  			runExecutable: true,
   189  			wantOutput:    "foo bar",
   190  		},
   191  		{
   192  			productName:     "foo",
   193  			mainFileContent: testMain,
   194  			mainFilePath:    "foo/main.go",
   195  			params: params.Product{
   196  				Build: params.Build{
   197  					MainPkg: "./foo",
   198  					OSArchs: []osarch.OSArch{
   199  						{
   200  							OS:   "darwin",
   201  							Arch: "amd64",
   202  						},
   203  						{
   204  							OS:   "linux",
   205  							Arch: "amd64",
   206  						},
   207  						{
   208  							OS:   "windows",
   209  							Arch: "amd64",
   210  						},
   211  					},
   212  				},
   213  			},
   214  			wantOutput: "defaultVersion",
   215  		},
   216  	} {
   217  		currTmpDir, err := ioutil.TempDir(tmp, "")
   218  		require.NoError(t, err)
   219  
   220  		gittest.InitGitDir(t, currTmpDir)
   221  		gittest.CreateGitTag(t, currTmpDir, testVersionValue)
   222  
   223  		mainFilePath := path.Join(currTmpDir, currCase.mainFilePath)
   224  
   225  		err = os.MkdirAll(path.Dir(mainFilePath), 0755)
   226  		require.NoError(t, err)
   227  
   228  		err = ioutil.WriteFile(mainFilePath, []byte(currCase.mainFileContent), 0644)
   229  		require.NoError(t, err)
   230  
   231  		binDir := path.Join(currTmpDir, "bin")
   232  		err = os.Mkdir(binDir, 0755)
   233  		require.NoError(t, err)
   234  
   235  		pkgPath, err := pkgpath.NewAbsPkgPath(path.Dir(mainFilePath)).Rel(currTmpDir)
   236  		require.NoError(t, err)
   237  
   238  		spec := binspec.New(currCase.params.Build.OSArchs, path.Base(pkgPath))
   239  		err = spec.CreateDirectoryStructure(binDir, nil, false)
   240  		require.NoError(t, err)
   241  
   242  		gitProductInfo, err := git.NewProjectInfo(currTmpDir)
   243  		require.NoError(t, err)
   244  
   245  		buildSpec := params.NewProductBuildSpec(
   246  			currTmpDir,
   247  			currCase.productName,
   248  			gitProductInfo,
   249  			currCase.params,
   250  			params.Project{
   251  				BuildOutputDir: "bin",
   252  			},
   253  		)
   254  
   255  		foundExecForCurrOsArch := false
   256  
   257  		err = build.Run([]params.ProductBuildSpec{buildSpec}, nil, build.Context{
   258  			Parallel: false,
   259  		}, ioutil.Discard)
   260  
   261  		if currCase.wantError {
   262  			assert.Error(t, err, fmt.Sprintf("Case %d", i))
   263  		} else {
   264  			assert.NoError(t, err, "Case %d", i)
   265  
   266  			artifactPaths := build.ArtifactPaths(buildSpec)
   267  			for _, currOSArch := range currCase.params.Build.OSArchs {
   268  				pathToCurrExecutable, ok := artifactPaths[currOSArch]
   269  				require.True(t, ok, "Case %d: could not find path for %s for %s", buildSpec.ProductName, currOSArch.String())
   270  				fileInfo, err := os.Stat(pathToCurrExecutable)
   271  				require.NoError(t, err, "Case %d", i)
   272  				assert.False(t, fileInfo.IsDir())
   273  
   274  				if reflect.DeepEqual(currOSArch, osarch.Current()) {
   275  					foundExecForCurrOsArch = true
   276  					output, err := exec.Command(pathToCurrExecutable).Output()
   277  					require.NoError(t, err)
   278  					assert.Equal(t, currCase.wantOutput, strings.TrimSpace(string(output)), "Case %d", i)
   279  				}
   280  			}
   281  
   282  			if currCase.runExecutable {
   283  				assert.True(t, foundExecForCurrOsArch, "Case %d: executable for current os/arch (%v) not found in %v", osarch.Current(), currCase.params.Build.OSArchs)
   284  			}
   285  		}
   286  	}
   287  }
   288  
   289  func TestBuildOnlyDistinctSpecs(t *testing.T) {
   290  	tmp, cleanup, err := dirs.TempDir("", "")
   291  	defer cleanup()
   292  	require.NoError(t, err)
   293  
   294  	mainFilePath := path.Join(tmp, "foo/main.go")
   295  	err = os.MkdirAll(path.Dir(mainFilePath), 0755)
   296  	require.NoError(t, err)
   297  	err = ioutil.WriteFile(mainFilePath, []byte(testMain), 0644)
   298  	require.NoError(t, err)
   299  
   300  	buildSpec := params.NewProductBuildSpec(
   301  		tmp,
   302  		"foo",
   303  		git.ProjectInfo{},
   304  		params.Product{
   305  			Build: params.Build{
   306  				MainPkg: "./foo",
   307  			},
   308  		},
   309  		params.Project{
   310  			BuildOutputDir: "bin",
   311  		},
   312  	)
   313  
   314  	buf := &bytes.Buffer{}
   315  	err = build.Run([]params.ProductBuildSpec{buildSpec, buildSpec}, nil, build.Context{
   316  		Parallel: false,
   317  	}, buf)
   318  	require.NoError(t, err)
   319  
   320  	assert.Equal(t, 1, strings.Count(buf.String(), "Finished building foo"))
   321  }
   322  
   323  func TestBuildOnlySpecifiedOSArchs(t *testing.T) {
   324  	tmp, cleanup, err := dirs.TempDir("", "")
   325  	defer cleanup()
   326  	require.NoError(t, err)
   327  
   328  	mainFilePath := path.Join(tmp, "foo/main.go")
   329  	err = os.MkdirAll(path.Dir(mainFilePath), 0755)
   330  	require.NoError(t, err)
   331  	err = ioutil.WriteFile(mainFilePath, []byte(testMain), 0644)
   332  	require.NoError(t, err)
   333  
   334  	for i, currCase := range []struct {
   335  		specOSArchs []osarch.OSArch
   336  		osArchs     []osarch.OSArch
   337  		want        []string
   338  		notWant     []string
   339  	}{
   340  		// empty value for osArchs filter builds all
   341  		{
   342  			specOSArchs: []osarch.OSArch{{OS: "darwin", Arch: "amd64"}, {OS: "linux", Arch: "386"}},
   343  			osArchs:     nil,
   344  			want: []string{
   345  				"Finished building foo for darwin-amd64",
   346  				"Finished building foo for linux-386",
   347  			},
   348  		},
   349  		// if non-empty filter is provided, only values matching filter are built
   350  		{
   351  			specOSArchs: []osarch.OSArch{{OS: "darwin", Arch: "amd64"}, {OS: "linux", Arch: "386"}},
   352  			osArchs:     []osarch.OSArch{{OS: "linux", Arch: "386"}},
   353  			want: []string{
   354  				"Finished building foo for linux-386",
   355  			},
   356  			notWant: []string{
   357  				"Finished building foo for darwin-amd64",
   358  			},
   359  		},
   360  		// if no OS/arch values match filter, nothing is built
   361  		{
   362  			specOSArchs: []osarch.OSArch{{OS: "darwin", Arch: "amd64"}, {OS: "linux", Arch: "386"}},
   363  			osArchs:     []osarch.OSArch{{OS: "windows", Arch: "386"}},
   364  			want: []string{
   365  				"$^",
   366  			},
   367  		},
   368  	} {
   369  		buildSpec := params.NewProductBuildSpec(
   370  			tmp,
   371  			"foo",
   372  			git.ProjectInfo{},
   373  			params.Product{
   374  				Build: params.Build{
   375  					MainPkg: "./foo",
   376  					OSArchs: currCase.specOSArchs,
   377  				},
   378  			},
   379  			params.Project{
   380  				BuildOutputDir: "bin",
   381  			},
   382  		)
   383  
   384  		buf := &bytes.Buffer{}
   385  		err = build.Run([]params.ProductBuildSpec{buildSpec}, cmd.OSArchFilter(currCase.osArchs), build.Context{
   386  			Parallel: false,
   387  		}, buf)
   388  		require.NoError(t, err)
   389  
   390  		for _, want := range currCase.want {
   391  			assert.Regexp(t, regexp.MustCompile(want), buf.String(), "Case %d", i)
   392  		}
   393  
   394  		for _, notWant := range currCase.notWant {
   395  			assert.NotRegexp(t, regexp.MustCompile(notWant), buf.String(), "Case %d", i)
   396  		}
   397  	}
   398  }
   399  
   400  func TestBuildErrorMessage(t *testing.T) {
   401  	tmp, cleanup, err := dirs.TempDir(".", "")
   402  	defer cleanup()
   403  	require.NoError(t, err)
   404  
   405  	mainFilePath := path.Join(tmp, "foo/main.go")
   406  	err = os.MkdirAll(path.Dir(mainFilePath), 0755)
   407  	require.NoError(t, err)
   408  	err = ioutil.WriteFile(mainFilePath, []byte(`package main; asdfa`), 0644)
   409  	require.NoError(t, err)
   410  
   411  	buildSpec := params.NewProductBuildSpec(
   412  		tmp,
   413  		"foo",
   414  		git.ProjectInfo{},
   415  		params.Product{
   416  			Build: params.Build{
   417  				MainPkg: "./foo",
   418  			},
   419  		},
   420  		params.Project{
   421  			BuildOutputDir: "bin",
   422  		},
   423  	)
   424  
   425  	want := `(?s)^go install failed: build command \[.+go install ./foo\] run with additional environment variables \[GOOS=.+ GOARCH=.+\] failed with output:.+foo/main.go:1: syntax error: non-declaration statement outside function body$`
   426  
   427  	buf := &bytes.Buffer{}
   428  	err = build.Run([]params.ProductBuildSpec{buildSpec, buildSpec}, nil, build.Context{
   429  		Install:  true,
   430  		Parallel: false,
   431  	}, buf)
   432  	assert.Regexp(t, want, err.Error())
   433  }
   434  
   435  func TestBuildInstallErrorMessage(t *testing.T) {
   436  	tmp, cleanup, err := dirs.TempDir(".", "")
   437  	defer cleanup()
   438  	require.NoError(t, err)
   439  
   440  	goRoot, err := dirs.GoRoot()
   441  	require.NoError(t, err)
   442  	_, err = os.Stat(goRoot)
   443  	require.NoError(t, err)
   444  
   445  	pkgDir := path.Join(goRoot, "pkg")
   446  	_, err = os.Stat(pkgDir)
   447  	require.NoError(t, err)
   448  
   449  	osArchPkgDir := path.Join(pkgDir, "dragonfly_amd64")
   450  	_, err = os.Stat(osArchPkgDir)
   451  	if os.IsNotExist(err) {
   452  		// if directory does not exist, attempt to create it (and clean up afterwards)
   453  		if err := os.Mkdir(osArchPkgDir, 0444); err == nil {
   454  			defer func() {
   455  				if err := os.RemoveAll(osArchPkgDir); err != nil {
   456  					fmt.Printf("Failed to remove directory %v: %v\n", osArchPkgDir, err)
   457  				}
   458  			}()
   459  		}
   460  		// if creation failed, assume that write permissions do not exist, which is sufficient for the test
   461  	}
   462  
   463  	mainFilePath := path.Join(tmp, "foo/main.go")
   464  	err = os.MkdirAll(path.Dir(mainFilePath), 0755)
   465  	require.NoError(t, err)
   466  	err = ioutil.WriteFile(mainFilePath, []byte(`package main`), 0644)
   467  	require.NoError(t, err)
   468  
   469  	buildSpec := params.NewProductBuildSpec(
   470  		tmp,
   471  		"foo",
   472  		git.ProjectInfo{},
   473  		params.Product{
   474  			Build: params.Build{
   475  				MainPkg: "./foo",
   476  				OSArchs: []osarch.OSArch{
   477  					{
   478  						OS:   "dragonfly",
   479  						Arch: "amd64",
   480  					},
   481  				},
   482  			},
   483  		},
   484  		params.Project{
   485  			BuildOutputDir: "bin",
   486  		},
   487  	)
   488  
   489  	goBinary := "go"
   490  	if output, err := exec.Command("command", "-v", "go").CombinedOutput(); err == nil {
   491  		goBinary = strings.TrimSpace(string(output))
   492  	}
   493  
   494  	want := `(?s)go install failed: failed to install a Go standard library package due to insufficient permissions to create directory.\n` +
   495  		`This typically means that the standard library for the OS/architectecture combination have not been installed locally and the current user does not have write permissions to GOROOT/pkg.\n` +
   496  		fmt.Sprintf("Run \"sudo env GOOS=dragonfly GOARCH=amd64 %s install std\" to install the standard packages for this combination as root and then try again.\n", goBinary) +
   497  		`Full error: build command \[.+/go install ./foo\] run with additional environment variables \[GOOS=dragonfly GOARCH=amd64\] failed with output:\n` +
   498  		`go install .+: mkdir .+: permission denied$`
   499  
   500  	buf := &bytes.Buffer{}
   501  	err = build.Run([]params.ProductBuildSpec{buildSpec, buildSpec}, nil, build.Context{
   502  		Install:  true,
   503  		Parallel: false,
   504  	}, buf)
   505  	assert.Regexp(t, want, err.Error())
   506  }
   507  
   508  func TestBuildAllParallel(t *testing.T) {
   509  	tmp, cleanup, err := dirs.TempDir("", "")
   510  	defer cleanup()
   511  	require.NoError(t, err)
   512  
   513  	for i, currCase := range []struct {
   514  		mainFiles map[string]string
   515  		specs     []params.ProductBuildSpec
   516  	}{
   517  		{
   518  			mainFiles: map[string]string{
   519  				"foo/main.go": longCompileMain,
   520  				"bar/main.go": longCompileMain,
   521  			},
   522  			specs: []params.ProductBuildSpec{
   523  				{
   524  					ProductName: "foo",
   525  					Product: params.Product{
   526  						Build: params.Build{
   527  							MainPkg: "./foo",
   528  							OSArchs: []osarch.OSArch{
   529  								{
   530  									OS:   "darwin",
   531  									Arch: "amd64",
   532  								},
   533  								{
   534  									OS:   "linux",
   535  									Arch: "amd64",
   536  								},
   537  							},
   538  							OutputDir: "build",
   539  						},
   540  					},
   541  					VersionInfo: git.ProjectInfo{
   542  						Version: "0.1.0",
   543  					},
   544  				},
   545  				{
   546  					ProductName: "bar",
   547  					Product: params.Product{
   548  						Build: params.Build{
   549  							MainPkg: "./bar",
   550  							OSArchs: []osarch.OSArch{
   551  								{
   552  									OS:   "darwin",
   553  									Arch: "amd64",
   554  								},
   555  								{
   556  									OS:   "linux",
   557  									Arch: "amd64",
   558  								},
   559  							},
   560  							OutputDir: "build",
   561  						},
   562  					},
   563  					VersionInfo: git.ProjectInfo{
   564  						Version: "0.1.0",
   565  					},
   566  				},
   567  			},
   568  		},
   569  	} {
   570  		currTmpDir, err := ioutil.TempDir(tmp, "")
   571  		require.NoError(t, err)
   572  
   573  		for file, content := range currCase.mainFiles {
   574  			err := os.MkdirAll(path.Join(currTmpDir, path.Dir(file)), 0755)
   575  			require.NoError(t, err)
   576  			err = ioutil.WriteFile(path.Join(currTmpDir, file), []byte(content), 0644)
   577  			require.NoError(t, err)
   578  		}
   579  
   580  		for i := range currCase.specs {
   581  			currCase.specs[i].ProjectDir = currTmpDir
   582  		}
   583  
   584  		err = build.Run(currCase.specs, nil, build.Context{
   585  			Parallel: true,
   586  		}, ioutil.Discard)
   587  		assert.NoError(t, err, "Case %d", i)
   588  	}
   589  }