go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/luciexe/invoke/options_test.go (about)

     1  // Copyright 2019 The LUCI Authors.
     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 invoke
    16  
    17  import (
    18  	"context"
    19  	"io/ioutil"
    20  	"os"
    21  	"path/filepath"
    22  	"runtime"
    23  	"testing"
    24  	"time"
    25  
    26  	"github.com/golang/protobuf/jsonpb"
    27  	"google.golang.org/protobuf/proto"
    28  	"google.golang.org/protobuf/types/known/timestamppb"
    29  
    30  	bbpb "go.chromium.org/luci/buildbucket/proto"
    31  	"go.chromium.org/luci/common/clock"
    32  	"go.chromium.org/luci/common/clock/testclock"
    33  	"go.chromium.org/luci/common/data/stringset"
    34  	"go.chromium.org/luci/common/system/environ"
    35  	"go.chromium.org/luci/logdog/client/butlerlib/bootstrap"
    36  	"go.chromium.org/luci/lucictx"
    37  	"go.chromium.org/luci/luciexe"
    38  
    39  	. "github.com/smartystreets/goconvey/convey"
    40  
    41  	. "go.chromium.org/luci/common/testing/assertions"
    42  )
    43  
    44  var tempEnvVar string
    45  
    46  func init() {
    47  	if runtime.GOOS == "windows" {
    48  		tempEnvVar = "TMP"
    49  	} else {
    50  		tempEnvVar = "TMPDIR"
    51  	}
    52  }
    53  
    54  var nullLogdogEnv = environ.New([]string{
    55  	bootstrap.EnvStreamServerPath + "=null",
    56  	bootstrap.EnvStreamProject + "=testing",
    57  	bootstrap.EnvStreamPrefix + "=prefix",
    58  	bootstrap.EnvCoordinatorHost + "=test.example.com",
    59  })
    60  
    61  func commonOptions() (ctx context.Context, o *Options, tdir string, closer func()) {
    62  	ctx, _ = testclock.UseTime(context.Background(), testclock.TestRecentTimeUTC)
    63  	ctx = lucictx.SetDeadline(ctx, nil)
    64  
    65  	// luciexe protocol requires the 'host' application to manage the tempdir.
    66  	// In this context the test binary is the host. It's more
    67  	// convenient+accurate to have a non-hermetic test than to mock this out.
    68  	oldTemp := os.Getenv(tempEnvVar)
    69  	closer = func() {
    70  		if err := os.Setenv(tempEnvVar, oldTemp); err != nil {
    71  			panic(err)
    72  		}
    73  		if tdir != "" {
    74  			So(os.RemoveAll(tdir), ShouldBeNil)
    75  		}
    76  	}
    77  
    78  	var err error
    79  	if tdir, err = ioutil.TempDir("", "luciexe_test"); err != nil {
    80  		closer() // want to do cleanup if ioutil.TempDir failed
    81  		So(err, ShouldBeNil)
    82  	}
    83  	if err := os.Setenv(tempEnvVar, tdir); err != nil {
    84  		panic(err)
    85  	}
    86  
    87  	o = &Options{
    88  		Env: nullLogdogEnv.Clone(),
    89  	}
    90  
    91  	return
    92  }
    93  
    94  func TestOptionsGeneral(t *testing.T) {
    95  	Convey(`test Options (general)`, t, func() {
    96  		ctx, _ := testclock.UseTime(context.Background(), testclock.TestRecentTimeUTC)
    97  
    98  		Convey(`works with nil`, func() {
    99  			// TODO(iannucci): really gotta put all these envvars in LUCI_CONTEXT...
   100  			oldVals := map[string]string{}
   101  			for k, v := range nullLogdogEnv.Map() {
   102  				oldVals[k] = os.Getenv(k)
   103  				if err := os.Setenv(k, v); err != nil {
   104  					panic(err)
   105  				}
   106  			}
   107  			defer func() {
   108  				for k, v := range oldVals {
   109  					if err := os.Setenv(k, v); err != nil {
   110  						panic(err)
   111  					}
   112  				}
   113  			}()
   114  			expected := stringset.NewFromSlice(luciexe.TempDirEnvVars...)
   115  			for key := range environ.System().Map() {
   116  				expected.Add(key)
   117  			}
   118  			expected.Add(lucictx.EnvKey)
   119  
   120  			lo, _, err := ((*Options)(nil)).rationalize(ctx)
   121  			So(err, ShouldBeNil)
   122  
   123  			envKeys := stringset.New(expected.Len())
   124  			lo.env.Iter(func(k, _ string) error {
   125  				envKeys.Add(k)
   126  				return nil
   127  			})
   128  			So(envKeys, ShouldResemble, expected)
   129  		})
   130  	})
   131  }
   132  
   133  func TestOptionsNamespace(t *testing.T) {
   134  	Convey(`test Options.Namespace`, t, func() {
   135  		ctx, o, _, closer := commonOptions()
   136  		defer closer()
   137  
   138  		nowP := timestamppb.New(clock.Now(ctx))
   139  
   140  		Convey(`default`, func() {
   141  			lo, _, err := o.rationalize(ctx)
   142  			So(err, ShouldBeNil)
   143  			So(lo.env.Get(bootstrap.EnvNamespace), ShouldResemble, "")
   144  			So(lo.step, ShouldBeNil)
   145  		})
   146  
   147  		Convey(`errors`, func() {
   148  			Convey(`bad clock`, func() {
   149  				o.Namespace = "yarp"
   150  				ctx, _ := testclock.UseTime(ctx, time.Unix(-100000000000, 0))
   151  
   152  				_, _, err := o.rationalize(ctx)
   153  				So(err, ShouldErrLike, "preparing namespace: invalid StartTime")
   154  			})
   155  		})
   156  
   157  		Convey(`toplevel`, func() {
   158  			o.Namespace = "u"
   159  			lo, _, err := o.rationalize(ctx)
   160  			So(err, ShouldBeNil)
   161  			So(lo.env.Get(bootstrap.EnvNamespace), ShouldResemble, "u")
   162  			So(lo.step, ShouldResembleProto, &bbpb.Step{
   163  				Name:      "u",
   164  				StartTime: nowP,
   165  				Status:    bbpb.Status_STARTED,
   166  				Logs: []*bbpb.Log{
   167  					{Name: "stdout", Url: "u/stdout"},
   168  					{Name: "stderr", Url: "u/stderr"},
   169  				},
   170  				MergeBuild: &bbpb.Step_MergeBuild{
   171  					FromLogdogStream: "u/build.proto",
   172  				},
   173  			})
   174  		})
   175  
   176  		Convey(`nested`, func() {
   177  			o.Env.Set(bootstrap.EnvNamespace, "u/bar")
   178  			o.Namespace = "sub"
   179  			lo, _, err := o.rationalize(ctx)
   180  			So(err, ShouldBeNil)
   181  			So(lo.env.Get(bootstrap.EnvNamespace), ShouldResemble, "u/bar/sub")
   182  			So(lo.step, ShouldResembleProto, &bbpb.Step{
   183  				Name:      "sub", // host application will swizzle this
   184  				StartTime: nowP,
   185  				Status:    bbpb.Status_STARTED,
   186  				Logs: []*bbpb.Log{
   187  					{Name: "stdout", Url: "sub/stdout"},
   188  					{Name: "stderr", Url: "sub/stderr"},
   189  				},
   190  				MergeBuild: &bbpb.Step_MergeBuild{
   191  					FromLogdogStream: "sub/build.proto",
   192  				},
   193  			})
   194  		})
   195  
   196  		Convey(`deeply nested`, func() {
   197  			o.Env.Set(bootstrap.EnvNamespace, "u")
   198  			o.Namespace = "step|!!cool!!|sub"
   199  			lo, _, err := o.rationalize(ctx)
   200  			So(err, ShouldBeNil)
   201  			So(lo.env.Get(bootstrap.EnvNamespace),
   202  				ShouldResemble, "u/step/s___cool__/sub")
   203  			So(lo.step, ShouldResembleProto, &bbpb.Step{
   204  				Name:      "step|!!cool!!|sub", // host application will swizzle this
   205  				StartTime: nowP,
   206  				Status:    bbpb.Status_STARTED,
   207  				Logs: []*bbpb.Log{
   208  					{Name: "stdout", Url: "step/s___cool__/sub/stdout"},
   209  					{Name: "stderr", Url: "step/s___cool__/sub/stderr"},
   210  				},
   211  				MergeBuild: &bbpb.Step_MergeBuild{
   212  					FromLogdogStream: "step/s___cool__/sub/build.proto",
   213  				},
   214  			})
   215  		})
   216  	})
   217  }
   218  
   219  func TestOptionsCacheDir(t *testing.T) {
   220  	Convey(`Options.CacheDir`, t, func() {
   221  		ctx, o, tdir, closer := commonOptions()
   222  		defer closer()
   223  
   224  		Convey(`default`, func() {
   225  			_, ctx, err := o.rationalize(ctx)
   226  			So(err, ShouldBeNil)
   227  			lexe := lucictx.GetLUCIExe(ctx)
   228  			So(lexe, ShouldNotBeNil)
   229  			So(lexe.CacheDir, ShouldStartWith, tdir)
   230  		})
   231  
   232  		Convey(`override`, func() {
   233  			o.CacheDir = filepath.Join(tdir, "cache")
   234  			So(os.Mkdir(o.CacheDir, 0777), ShouldBeNil)
   235  			_, ctx, err := o.rationalize(ctx)
   236  			So(err, ShouldBeNil)
   237  			lexe := lucictx.GetLUCIExe(ctx)
   238  			So(lexe, ShouldNotBeNil)
   239  			So(lexe.CacheDir, ShouldEqual, o.CacheDir)
   240  		})
   241  
   242  		Convey(`errors`, func() {
   243  			Convey(`empty cache dir set`, func() {
   244  				ctx := lucictx.SetLUCIExe(ctx, &lucictx.LUCIExe{})
   245  				_, _, err := o.rationalize(ctx)
   246  				So(err, ShouldErrLike, `"cache_dir" is empty`)
   247  			})
   248  
   249  			Convey(`bad override (doesn't exist)`, func() {
   250  				o.CacheDir = filepath.Join(tdir, "cache")
   251  				_, _, err := o.rationalize(ctx)
   252  				So(err, ShouldErrLike, "checking CacheDir: dir does not exist")
   253  			})
   254  
   255  			Convey(`bad override (not a dir)`, func() {
   256  				o.CacheDir = filepath.Join(tdir, "cache")
   257  				So(os.WriteFile(o.CacheDir, []byte("not a dir"), 0666), ShouldBeNil)
   258  				_, _, err := o.rationalize(ctx)
   259  				So(err, ShouldErrLike, "checking CacheDir: path is not a directory")
   260  			})
   261  		})
   262  	})
   263  }
   264  
   265  func TestOptionsCollectOutput(t *testing.T) {
   266  	Convey(`Options.CollectOutput`, t, func() {
   267  		ctx, o, tdir, closer := commonOptions()
   268  		defer closer()
   269  
   270  		Convey(`default`, func() {
   271  			lo, _, err := o.rationalize(ctx)
   272  			So(err, ShouldBeNil)
   273  			So(lo.args, ShouldBeEmpty)
   274  			out, err := luciexe.ReadBuildFile(lo.collectPath)
   275  			So(err, ShouldBeNil)
   276  			So(out, ShouldBeNil)
   277  		})
   278  
   279  		Convey(`errors`, func() {
   280  			Convey(`bad extension`, func() {
   281  				o.CollectOutputPath = filepath.Join(tdir, "output.fleem")
   282  				_, _, err := o.rationalize(ctx)
   283  				So(err, ShouldErrLike, "bad extension for build proto file path")
   284  			})
   285  
   286  			Convey(`already exists`, func() {
   287  				outPath := filepath.Join(tdir, "output.pb")
   288  				o.CollectOutputPath = outPath
   289  				So(os.WriteFile(outPath, nil, 0666), ShouldBeNil)
   290  				_, _, err := o.rationalize(ctx)
   291  				So(err, ShouldErrLike, "CollectOutputPath points to an existing file")
   292  			})
   293  
   294  			Convey(`parent is not a dir`, func() {
   295  				parDir := filepath.Join(tdir, "parent")
   296  				So(os.WriteFile(parDir, nil, 0666), ShouldBeNil)
   297  				o.CollectOutputPath = filepath.Join(parDir, "out.pb")
   298  
   299  				_, _, err := o.rationalize(ctx)
   300  				So(err, ShouldErrLike, "checking CollectOutputPath's parent: path is not a directory")
   301  			})
   302  
   303  			Convey(`no parent folder`, func() {
   304  				o.CollectOutputPath = filepath.Join(tdir, "extra", "output.fleem")
   305  				_, _, err := o.rationalize(ctx)
   306  				So(err, ShouldErrLike, "checking CollectOutputPath's parent: dir does not exist")
   307  			})
   308  		})
   309  
   310  		Convey(`parseOutput`, func() {
   311  			expected := &bbpb.Build{SummaryMarkdown: "I'm a summary."}
   312  			testParseOutput := func(expectedData []byte, checkFilename func(string)) {
   313  				lo, _, err := o.rationalize(ctx)
   314  				So(err, ShouldBeNil)
   315  				So(lo.args, ShouldHaveLength, 2)
   316  				So(lo.args[0], ShouldEqual, luciexe.OutputCLIArg)
   317  				checkFilename(lo.args[1])
   318  
   319  				_, err = luciexe.ReadBuildFile(lo.collectPath)
   320  				So(err, ShouldErrLike, "opening build file")
   321  
   322  				So(os.WriteFile(lo.args[1], expectedData, 0666), ShouldBeNil)
   323  
   324  				build, err := luciexe.ReadBuildFile(lo.collectPath)
   325  				So(err, ShouldBeNil)
   326  				So(build, ShouldResembleProto, expected)
   327  			}
   328  
   329  			Convey(`collect but no specific file`, func() {
   330  				o.CollectOutput = true
   331  				data, err := proto.Marshal(expected)
   332  				So(err, ShouldBeNil)
   333  				testParseOutput(data, func(filename string) {
   334  					So(filename, ShouldStartWith, tdir)
   335  					So(filename, ShouldEndWith, luciexe.BuildFileCodecBinary.FileExtension())
   336  				})
   337  			})
   338  
   339  			Convey(`collect from a binary file`, func() {
   340  				o.CollectOutput = true
   341  				outPath := filepath.Join(tdir, "output.pb")
   342  				o.CollectOutputPath = outPath
   343  
   344  				data, err := proto.Marshal(expected)
   345  				So(err, ShouldBeNil)
   346  
   347  				testParseOutput(data, func(filename string) {
   348  					So(filename, ShouldEqual, outPath)
   349  				})
   350  			})
   351  
   352  			Convey(`collect from a json file`, func() {
   353  				o.CollectOutput = true
   354  				outPath := filepath.Join(tdir, "output.json")
   355  				o.CollectOutputPath = outPath
   356  
   357  				data, err := (&jsonpb.Marshaler{OrigName: true}).MarshalToString(expected)
   358  				So(err, ShouldBeNil)
   359  
   360  				testParseOutput([]byte(data), func(filename string) {
   361  					So(filename, ShouldEqual, outPath)
   362  				})
   363  			})
   364  
   365  			Convey(`collect from a textpb file`, func() {
   366  				o.CollectOutput = true
   367  				outPath := filepath.Join(tdir, "output.textpb")
   368  				o.CollectOutputPath = outPath
   369  
   370  				testParseOutput([]byte(expected.String()), func(filename string) {
   371  					So(filename, ShouldEqual, outPath)
   372  				})
   373  			})
   374  		})
   375  	})
   376  }
   377  
   378  func TestOptionsEnv(t *testing.T) {
   379  	Convey(`Env`, t, func() {
   380  		ctx, o, _, closer := commonOptions()
   381  		defer closer()
   382  
   383  		Convey(`default`, func() {
   384  			lo, _, err := o.rationalize(ctx)
   385  			So(err, ShouldBeNil)
   386  
   387  			expected := stringset.NewFromSlice(luciexe.TempDirEnvVars...)
   388  			for key := range o.Env.Map() {
   389  				expected.Add(key)
   390  			}
   391  			expected.Add(lucictx.EnvKey)
   392  
   393  			actual := stringset.New(expected.Len())
   394  			for key := range lo.env.Map() {
   395  				actual.Add(key)
   396  			}
   397  
   398  			So(actual, ShouldResemble, expected)
   399  		})
   400  	})
   401  }
   402  
   403  func TestOptionsStdio(t *testing.T) {
   404  	Convey(`stdio`, t, func() {
   405  		ctx, o, _, closer := commonOptions()
   406  		defer closer()
   407  
   408  		Convey(`default`, func() {
   409  			lo, _, err := o.rationalize(ctx)
   410  			So(err, ShouldBeNil)
   411  			So(lo.stderr, ShouldNotBeNil)
   412  			So(lo.stdout, ShouldNotBeNil)
   413  		})
   414  
   415  		Convey(`errors`, func() {
   416  			Convey(`bad bootstrap (missing)`, func() {
   417  				o.Env.Remove(bootstrap.EnvStreamServerPath)
   418  				_, _, err := o.rationalize(ctx)
   419  				So(err, ShouldErrLike, "Logdog Butler environment required")
   420  			})
   421  
   422  			Convey(`bad bootstrap (malformed)`, func() {
   423  				o.Env.Set(bootstrap.EnvStreamPrefix, "!!!!")
   424  				_, _, err := o.rationalize(ctx)
   425  				So(err, ShouldErrLike, `failed to validate prefix "!!!!"`)
   426  			})
   427  		})
   428  	})
   429  }
   430  
   431  func TestOptionsExtraDirs(t *testing.T) {
   432  	Convey(`tempDir+workDir`, t, func() {
   433  		ctx, o, tdir, closer := commonOptions()
   434  		defer closer()
   435  
   436  		Convey(`provided BaseDir`, func() {
   437  			o.BaseDir = filepath.Join(tdir, "base")
   438  			So(os.Mkdir(o.BaseDir, 0777), ShouldBeNil)
   439  			lo, _, err := o.rationalize(ctx)
   440  			So(err, ShouldBeNil)
   441  			So(lo.env.Get("TMP"), ShouldStartWith, o.BaseDir)
   442  			So(lo.workDir, ShouldStartWith, o.BaseDir)
   443  		})
   444  
   445  		Convey(`provided BaseDir does not exist`, func() {
   446  			o.BaseDir = filepath.Join(tdir, "base")
   447  			_, _, err := o.rationalize(ctx)
   448  			So(err, ShouldErrLike, "checking BaseDir: dir does not exist")
   449  		})
   450  
   451  		Convey(`provided BaseDir is not a directory`, func() {
   452  			o.BaseDir = filepath.Join(tdir, "base")
   453  			So(os.WriteFile(o.BaseDir, []byte("not a dir"), 0666), ShouldBeNil)
   454  			_, _, err := o.rationalize(ctx)
   455  			So(err, ShouldErrLike, "checking BaseDir: path is not a directory")
   456  		})
   457  
   458  		Convey(`fallback to temp`, func() {
   459  			lo, _, err := o.rationalize(ctx)
   460  			So(err, ShouldBeNil)
   461  			So(lo.env.Get("TMP"), ShouldStartWith, tdir)
   462  			So(lo.workDir, ShouldStartWith, tdir)
   463  		})
   464  	})
   465  }