go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/exec/execmock/uses.go (about)

     1  // Copyright 2023 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 execmock
    16  
    17  import (
    18  	"context"
    19  	"os"
    20  	"sync"
    21  
    22  	"go.chromium.org/luci/common/errors"
    23  	"go.chromium.org/luci/common/exec/internal/execmockctx"
    24  	"go.chromium.org/luci/common/system/environ"
    25  )
    26  
    27  type usage interface {
    28  	setOutput(any, string, error)
    29  	getErrorOutput() (string, error)
    30  }
    31  
    32  type uses interface {
    33  	len() int
    34  	addUsage(*execmockctx.MockCriteria, **os.Process) usage
    35  }
    36  
    37  // Uses is used to collect uses of a particular mock (Runner + input data).
    38  //
    39  // Your test code can interrogate this object after running your code-under-test
    40  // to determine how many times the corresponding mock entry was used, what
    41  // sub-processes were actually launched (and handles to those processes for your
    42  // test to signal/read/etc.), and, if those sub-processes finished, what `Out`
    43  // data did they return.
    44  type Uses[Out any] struct {
    45  	mu sync.Mutex
    46  
    47  	uses []*Usage[Out]
    48  }
    49  
    50  func (u *Uses[Out]) len() int {
    51  	u.mu.Lock()
    52  	defer u.mu.Unlock()
    53  	return len(u.uses)
    54  }
    55  
    56  func (u *Uses[Out]) addUsage(mc *execmockctx.MockCriteria, proc **os.Process) usage {
    57  	u.mu.Lock()
    58  	defer u.mu.Unlock()
    59  	ret := &Usage[Out]{
    60  		Args: mc.Args,
    61  		Env:  mc.Env,
    62  
    63  		proc:          proc,
    64  		outputWritten: make(chan struct{}),
    65  	}
    66  	u.uses = append(u.uses, ret)
    67  	return ret
    68  }
    69  
    70  // Snapshot retrieves a snapshot of the current Usages.
    71  func (u *Uses[Out]) Snapshot() []*Usage[Out] {
    72  	u.mu.Lock()
    73  	defer u.mu.Unlock()
    74  	ret := make([]*Usage[Out], len(u.uses))
    75  	copy(ret, u.uses)
    76  	return ret
    77  }
    78  
    79  var ErrProcessNotStarted = errors.New("sub-process did not start yet")
    80  
    81  // Usage represents a single `hit` for a given mock.
    82  type Usage[Out any] struct {
    83  	// Args and Env are the exact Args and Env of the Cmd which matched this mock.
    84  	Args []string
    85  	Env  environ.Env
    86  
    87  	proc **os.Process
    88  
    89  	mu            sync.Mutex
    90  	outputWritten chan struct{}
    91  	output        Out
    92  	panicStack    string
    93  	err           error
    94  }
    95  
    96  // GetPID returns the process ID associated with this usage (i.e. the mock
    97  // Process)
    98  //
    99  // NOTE: This is NOT thread-safe; Due to the way that the stdlib exec library
   100  // works with regard to populating Cmd.Process, you must ensure that you only
   101  // call this from a thread which was sequential with the thread which called
   102  // Cmd.Start() (or Run() or CombinedOutput()).
   103  //
   104  // If this Usage is from a MockError invocation, this will always return nil.
   105  //
   106  // Returns 0 if the process is not started.
   107  func (u *Usage[Out]) GetPID() int {
   108  	p := *u.proc
   109  	if p != nil {
   110  		return p.Pid
   111  	}
   112  	return 0
   113  }
   114  
   115  // Signal sends a signal to the mock process.
   116  //
   117  // NOTE: This is NOT thread-safe; Due to the way that the stdlib exec library
   118  // works with regard to populating Cmd.Process, you must ensure that you only
   119  // call this from a thread which was sequential with the thread which called
   120  // Cmd.Start() (or Run() or CombinedOutput()).
   121  //
   122  // If this Usage is from a MockError invocation, this will always return an
   123  // error.
   124  func (u *Usage[Out]) Signal(sig os.Signal) error {
   125  	p := *u.proc
   126  	if p != nil {
   127  		return p.Signal(sig)
   128  	}
   129  	return ErrProcessNotStarted
   130  }
   131  
   132  // Kill kills mock process (usually by sending SIGKILL).
   133  //
   134  // NOTE: This is NOT thread-safe; Due to the way that the stdlib exec library
   135  // works with regard to populating Cmd.Process, you must ensure that you only
   136  // call this from a thread which was sequential with the thread which called
   137  // Cmd.Start() (or Run() or CombinedOutput()).
   138  //
   139  // If this Usage is from a MockError invocation, this will always return an
   140  // error.
   141  func (u *Usage[Out]) Kill() error {
   142  	p := *u.proc
   143  	if p != nil {
   144  		return p.Kill()
   145  	}
   146  	return ErrProcessNotStarted
   147  }
   148  
   149  func (u *Usage[Out]) setOutput(data any, panicStack string, err error) {
   150  	u.mu.Lock()
   151  	defer u.mu.Unlock()
   152  
   153  	u.output = data.(Out)
   154  	u.panicStack = panicStack
   155  	u.err = err
   156  	close(u.outputWritten)
   157  }
   158  
   159  // GetOutput will block until the process writes output data (or until the
   160  // provided context ends), and return the Out value written by the sub-process
   161  // (if any).
   162  //
   163  // If this is Usage[None] then this returns (nil, nil)
   164  //
   165  // Possible errors:
   166  //   - ctx.Err() if `ctx` is Done.
   167  //   - errors which occured when reading the output from the sub-process.
   168  //   - errors which the mock itself (i.e. the RunnerFunction) returned.
   169  func (u *Usage[Out]) GetOutput(ctx context.Context) (value Out, panicStack string, err error) {
   170  	select {
   171  	case <-ctx.Done():
   172  		err = ctx.Err()
   173  		return
   174  	case <-u.outputWritten:
   175  	}
   176  
   177  	u.mu.Lock()
   178  	defer u.mu.Unlock()
   179  	return u.output, u.panicStack, u.err
   180  }
   181  
   182  func (u *Usage[Out]) getErrorOutput() (string, error) {
   183  	u.mu.Lock()
   184  	defer u.mu.Unlock()
   185  	return u.panicStack, u.err
   186  }