go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/luciexe/invoke/subprocess_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  	"flag"
    20  	"fmt"
    21  	"io"
    22  	"os"
    23  	"os/signal"
    24  	"path"
    25  	"testing"
    26  	"time"
    27  
    28  	"google.golang.org/protobuf/proto"
    29  	"google.golang.org/protobuf/types/known/timestamppb"
    30  
    31  	bbpb "go.chromium.org/luci/buildbucket/proto"
    32  	"go.chromium.org/luci/common/clock"
    33  	"go.chromium.org/luci/common/clock/testclock"
    34  	"go.chromium.org/luci/common/system/signals"
    35  	"go.chromium.org/luci/lucictx"
    36  
    37  	. "github.com/smartystreets/goconvey/convey"
    38  
    39  	. "go.chromium.org/luci/common/testing/assertions"
    40  )
    41  
    42  const (
    43  	selfTestEnvvar          = "LUCIEXE_INVOKE_TEST"
    44  	terminateExitCode       = 71
    45  	unexpectedErrorExitCode = 97
    46  )
    47  
    48  func TestMain(m *testing.M) {
    49  	switch os.Getenv(selfTestEnvvar) {
    50  	case "":
    51  		m.Run()
    52  	case "exiterr":
    53  		os.Exit(unexpectedErrorExitCode)
    54  	case "hang":
    55  		<-time.After(time.Minute)
    56  		fmt.Fprintln(os.Stderr, "ERROR: TIMER ENDED")
    57  		os.Exit(1)
    58  	case "signal":
    59  		fmt.Fprintf(os.Stderr, "signal subprocess started\n")
    60  		signalCh := make(chan os.Signal, 1)
    61  		signal.Notify(signalCh, signals.Interrupts()...)
    62  		touch := func(name string) error {
    63  			f, err := os.OpenFile(name, os.O_RDONLY|os.O_CREATE, 0644)
    64  			if err != nil {
    65  				return err
    66  			}
    67  			return f.Close()
    68  		}
    69  		if err := touch(os.Args[1]); err != nil {
    70  			fmt.Fprintf(os.Stderr, "ERROR: creating file %s\n", err)
    71  			os.Exit(unexpectedErrorExitCode)
    72  		}
    73  		fmt.Fprintf(os.Stderr, "touched %s\n", os.Args[1])
    74  		select {
    75  		case <-signalCh:
    76  			os.Exit(terminateExitCode)
    77  		case <-time.After(time.Minute):
    78  			fmt.Fprintln(os.Stderr, "ERROR: Timeout waiting for Signal")
    79  			os.Exit(unexpectedErrorExitCode)
    80  		}
    81  	default:
    82  		out := flag.String("output", "", "write the output here")
    83  		flag.Parse()
    84  
    85  		data, err := io.ReadAll(os.Stdin)
    86  		if err != nil {
    87  			panic(err)
    88  		}
    89  
    90  		in := &bbpb.Build{}
    91  		if err := proto.Unmarshal(data, in); err != nil {
    92  			panic(err)
    93  		}
    94  		in.SummaryMarkdown = "hi"
    95  
    96  		if *out != "" {
    97  			outData, err := proto.Marshal(in)
    98  			if err != nil {
    99  				panic(err)
   100  			}
   101  			if err := os.WriteFile(*out, outData, 0666); err != nil {
   102  				panic(err)
   103  			}
   104  		}
   105  
   106  		os.Exit(0)
   107  	}
   108  }
   109  
   110  func TestSubprocess(t *testing.T) {
   111  	Convey(`Subprocess`, t, func() {
   112  		ctx, o, tdir, closer := commonOptions()
   113  		defer closer()
   114  
   115  		o.Env.Set(selfTestEnvvar, "1")
   116  
   117  		selfArgs := []string{os.Args[0]}
   118  
   119  		Convey(`defaults`, func() {
   120  			sp, err := Start(ctx, selfArgs, &bbpb.Build{Id: 1}, o)
   121  			So(err, ShouldBeNil)
   122  			So(sp.Step, ShouldBeNil)
   123  			build, err := sp.Wait()
   124  			So(err, ShouldBeNil)
   125  			So(build, ShouldResembleProto, &bbpb.Build{})
   126  		})
   127  
   128  		Convey(`exiterr`, func() {
   129  			o.Env.Set(selfTestEnvvar, "exiterr")
   130  			sp, err := Start(ctx, selfArgs, &bbpb.Build{Id: 1}, o)
   131  			So(err, ShouldBeNil)
   132  			So(sp.Step, ShouldBeNil)
   133  			build, err := sp.Wait()
   134  			So(err, ShouldErrLike, "exit status 97")
   135  			So(build, ShouldResembleProto, &bbpb.Build{})
   136  		})
   137  
   138  		Convey(`collect`, func() {
   139  			o.CollectOutput = true
   140  			sp, err := Start(ctx, selfArgs, &bbpb.Build{Id: 1}, o)
   141  			So(err, ShouldBeNil)
   142  			So(sp.Step, ShouldBeNil)
   143  			build, err := sp.Wait()
   144  			So(err, ShouldBeNil)
   145  			So(build, ShouldNotBeNil)
   146  			So(build.SummaryMarkdown, ShouldEqual, "hi")
   147  		})
   148  
   149  		Convey(`clear fields in initial build`, func() {
   150  			o.CollectOutput = true
   151  			initialBuildTime := time.Date(2020, time.January, 2, 3, 4, 5, 6, time.UTC)
   152  			ctx, _ := testclock.UseTime(ctx, initialBuildTime)
   153  
   154  			inputBuild := &bbpb.Build{
   155  				Id:              11,
   156  				Status:          bbpb.Status_CANCELED,
   157  				StatusDetails:   &bbpb.StatusDetails{Timeout: &bbpb.StatusDetails_Timeout{}},
   158  				SummaryMarkdown: "Heyo!",
   159  				EndTime:         timestamppb.New(time.Date(2020, time.January, 2, 3, 4, 5, 10, time.UTC)),
   160  				UpdateTime:      timestamppb.New(time.Date(2020, time.January, 2, 3, 4, 5, 11, time.UTC)),
   161  				Steps:           []*bbpb.Step{{Name: "Step cool"}},
   162  				Tags:            []*bbpb.StringPair{{Key: "foo", Value: "bar"}},
   163  				Output: &bbpb.Build_Output{
   164  					Logs: []*bbpb.Log{{Name: "stdout"}},
   165  				},
   166  			}
   167  			sp, err := Start(ctx, selfArgs, inputBuild, o)
   168  			So(err, ShouldBeNil)
   169  			build, err := sp.Wait()
   170  			So(err, ShouldBeNil)
   171  			So(build, ShouldResembleProto, &bbpb.Build{
   172  				Id:              11,
   173  				Status:          bbpb.Status_STARTED,
   174  				SummaryMarkdown: "hi",
   175  				CreateTime:      timestamppb.New(initialBuildTime),
   176  				StartTime:       timestamppb.New(initialBuildTime),
   177  				Tags:            []*bbpb.StringPair{{Key: "foo", Value: "bar"}},
   178  			})
   179  		})
   180  
   181  		Convey(`cancel context`, func() {
   182  			ctx, cancel := context.WithCancel(ctx)
   183  			defer cancel()
   184  
   185  			start := time.Now()
   186  
   187  			o.Env.Set(selfTestEnvvar, "hang")
   188  			sp, err := Start(ctx, selfArgs, &bbpb.Build{Id: 1}, o)
   189  			So(err, ShouldBeNil)
   190  			cancel()
   191  			_, err = sp.Wait()
   192  			So(err, ShouldErrLike, "waiting for luciexe")
   193  
   194  			So(time.Now(), ShouldHappenWithin, time.Second, start)
   195  		})
   196  
   197  		Convey(`cancel context before Start`, func() {
   198  			ctx, cancel := context.WithCancel(ctx)
   199  			cancel()
   200  			_, err := Start(ctx, selfArgs, &bbpb.Build{Id: 1}, o)
   201  			So(err, ShouldErrLike, "prior to starting subprocess: context canceled")
   202  		})
   203  
   204  		Convey(`deadline`, func() {
   205  			o.Env.Set(selfTestEnvvar, "signal")
   206  
   207  			ctx, tc := testclock.UseTime(ctx, testclock.TestRecentTimeUTC)
   208  			ctx, cancel := clock.WithTimeout(ctx, 130*time.Second)
   209  
   210  			ctx, shutdown := lucictx.TrackSoftDeadline(ctx, 0)
   211  			defer shutdown()
   212  
   213  			readyFile := path.Join(tdir, "readyToCatchSignal")
   214  			sp, err := Start(ctx, append(selfArgs, readyFile), &bbpb.Build{Id: 1}, o)
   215  			So(err, ShouldBeNil)
   216  			timer := time.After(time.Minute)
   217  			for {
   218  				select {
   219  				case <-timer:
   220  					panic("subprocess is never ready to catch signal")
   221  				default:
   222  					_, err = os.Stat(readyFile)
   223  				}
   224  				if err == nil {
   225  					break
   226  				} else {
   227  					time.Sleep(time.Second)
   228  				}
   229  			}
   230  			defer os.Remove(readyFile)
   231  
   232  			Convey(`interrupt`, func() {
   233  				shutdown()
   234  
   235  				bld, err := sp.Wait()
   236  				So(err, ShouldContainErr, "luciexe process is interrupted")
   237  				So(sp.cmd.ProcessState.ExitCode(), ShouldEqual, terminateExitCode)
   238  				So(bld, ShouldResembleProto, &bbpb.Build{})
   239  			})
   240  
   241  			Convey(`timeout`, func() {
   242  				tc.Add(100 * time.Second) // hits soft deadline
   243  
   244  				bld, err := sp.Wait()
   245  				So(err, ShouldContainErr, "luciexe process timed out")
   246  				So(sp.cmd.ProcessState.ExitCode(), ShouldEqual, terminateExitCode)
   247  				So(bld, ShouldResembleProto, &bbpb.Build{
   248  					StatusDetails: &bbpb.StatusDetails{Timeout: &bbpb.StatusDetails_Timeout{}},
   249  				})
   250  			})
   251  
   252  			Convey(`closure`, func() {
   253  				cancel()
   254  
   255  				bld, err := sp.Wait()
   256  				So(err, ShouldContainErr, "luciexe process's context is cancelled")
   257  				// The exit code for killed process varies on different platform.
   258  				So(sp.cmd.ProcessState.ExitCode(), ShouldNotEqual, unexpectedErrorExitCode)
   259  				So(bld, ShouldResembleProto, &bbpb.Build{})
   260  			})
   261  		})
   262  	})
   263  }