go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/exec/execmock/mocker.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 allows mocking exec commands using the
    16  // go.chromium.org/luci/common/exec ("luci exec") library, which is nearly
    17  // a drop-in replacement for the "os/exec" stdlib library.
    18  //
    19  // The way this package works is by registering `RunnerFunction`s, and then
    20  // adding references to those inside of the Context ('mocks'). The LUCI exec
    21  // library will then detect these in any constructed Command's and run the
    22  // RunnerFunction in a real sub-process.
    23  //
    24  // The sub-process is always an instance of the test binary itself; You hook
    25  // execution of these RunnerFunctions by calling the Intercept() function in
    26  // your TestMain function. TestMain will then have two modes. For the main test
    27  // execution, the test binary will run an http server to serve and retrieve data
    28  // from 'invocations'. For the mock sub-processes, a special environment
    29  // variable will be set, and the Intercept() function will effectively hijack
    30  // the test execution before the `testing.Main` function. The hijack will reach
    31  // out to the http server and retrieve information about this specific
    32  // invocation, including inputs tot he RunnerFunction, and execute the
    33  // RunnerFunction with the provided input. Once the function is done, its
    34  // output will be POST'd back to the http server, and the process will exit with
    35  // the return code from the runner function.
    36  //
    37  // Running these as real sub-processes has a number of advantages; In
    38  // particular, application code which is written to interact with a real
    39  // sub-process (e.g. passing file descriptors, sending signals, reading/writing
    40  // from Stdio pipes, calling system calls like Wait() on the process, etc.) will
    41  // be interacting with a 100% real process, not an emulation in a goroutine.
    42  // Additionally, the RunnerFunction will get to use all `os` level functions (to
    43  // read environment, working directory, look for parent process ID, etc.) and
    44  // they will work correctly.
    45  //
    46  // As a convenience, this library includes a `Simple` mock which can cover many
    47  // very basic execution scenarios.
    48  //
    49  // By default, if the Intercept() function has been called in the process, ALL
    50  // commands using the luci exec library will need to have a matching mock. You
    51  // can explicitly enable 'passthrough' for some executions (with the Passthrough
    52  // mock).
    53  //
    54  // Finally, you can simulate any error from the Start function using the
    55  // StartError mock; This will not run a sub-process, but will instead produce
    56  // an error from Start and other functions which call Start.
    57  //
    58  // # Debugging Runners
    59  //
    60  // Occassionally you have a Runner which is sufficiently complex that you would
    61  // need to debug it (e.g. with delve). execmock supports this by doing the
    62  // following:
    63  //
    64  // First, run `go test . -execmock.list`, which will print out all registered
    65  // runners at the point that your test calls Intercept().
    66  package execmock
    67  
    68  import (
    69  	"context"
    70  	"reflect"
    71  )
    72  
    73  // A Mocker allows you to create mocks for a RunnerFunction, or to create mocks
    74  // for a 'Start error' (via StartError).
    75  //
    76  // Mockers are immutable, and by default a Mock would apply to ALL commands. You
    77  // can create derivative Mockers using the With methods, which will only apply
    78  // to commands matching that criteria.
    79  //
    80  // Once you have constructed the filter you want by chaining With calls, call
    81  // Mock to actually add a filtered mock into the context, supplying any input
    82  // data your RunnerFunction needs (in the case of a Start error, this would be
    83  // the error that will be returned from Command.Start()).
    84  type Mocker[In any, Out any] interface {
    85  	// WithArgs will return a new Mocker which only applies to commands whose
    86  	// argument list matches `argPattern`.
    87  	//
    88  	// Using this multiple times will require a command to match ALL given patterns.
    89  	//
    90  	// `argPattern` follows the same rules that
    91  	// go.chromium.org/luci/common/data/text/sequence.NewPattern uses, namely:
    92  	//
    93  	// Tokens can be:
    94  	//   - "/a regex/" - A regular expression surrounded by slashes.
    95  	//   - "..." - An Ellipsis which matches any number of sequence entries.
    96  	//   - "^" at index 0 - Zero-width matches at the beginning of the sequence.
    97  	//   - "$" at index -1 - Zero-width matches at the end of the sequence.
    98  	//   - "=string" - Literally match anything after the "=". Allows escaping
    99  	//     special strings, e.g. "=/regex/", "=...", "=^", "=$", "==something".
   100  	//   - "any other string" - Literally match without escaping.
   101  	//
   102  	// Panics if `argPattern` is invalid.
   103  	WithArgs(argPattern ...string) Mocker[In, Out]
   104  
   105  	// WithEnv will return a new Mocker which only applies to commands which
   106  	// include an environment variable `varName` which matches `valuePattern`.
   107  	//
   108  	// Using this multiple times will require a command to match ALL the given
   109  	// restrictions (even within the same `varName`).
   110  	//
   111  	// varName is the environment variable name (which will be matched exactly), and
   112  	// valuePattern is either:
   113  	//   - "/a regex/" - A regular expression surrounded by slashes.
   114  	//   - "!" - An indicator that this envvar should be unset.
   115  	//   - "=string" - Literally match anything after the "=". Allows escaping
   116  	//     regex strings, e.g. "=/regex/", "==something".
   117  	//   - "any other string" - Literally match without escaping.
   118  	WithEnv(varName, valuePattern string) Mocker[In, Out]
   119  
   120  	// WithLimit will restrict the number of times a Mock from this Mocker can
   121  	// be used.
   122  	//
   123  	// After that many usages, the Mock will become inactive and won't match any
   124  	// new executions.
   125  	//
   126  	// Calling WithLimit replaces the current limit.
   127  	// Calling WithLimit(0) will remove the limit.
   128  	WithLimit(limit uint64) Mocker[In, Out]
   129  
   130  	// Mock adds a new mocker in the context, and returns the corresponding
   131  	// Uses struct which will allow your test to see how many times this mock
   132  	// was used, and what outputs it produced.
   133  	//
   134  	// Any execution which matches this Entry will run this Mocker's associated
   135  	// RunnerFunction in a subprocess with the data `indata`.
   136  	//
   137  	// Supplying multiple input values will panic.
   138  	// Supplying no input values will use a default-constructed In (especially
   139  	// useful for None{}).
   140  	//
   141  	// Mocks are ordered based on the filter (i.e. what WithXXX calls in the chain
   142  	// prior to calling Mock) once the first exec.Command/CommandContext is
   143  	// Start()'d. The ordering criteria is as follows:
   144  	//   * Number of LiteralMatchers in WithArgs patterns (more LiteralMatchers
   145  	//     will be tried earlier).
   146  	//   * Number of all matchers in WithArgs patterns (more matchers will be
   147  	//     tried earlier).
   148  	//   * Mocks with lower limits are tried before mocks with higher limits.
   149  	//   * Mocks with more WithEnv entries are tried before mocks with fewer
   150  	//     WithEnv.
   151  	//   * Finally, mocks are tried in the order they are created (i.e. the order
   152  	//     that Mock() here is called.
   153  	//
   154  	// Lastly, once `ctx` has been used to start executing commands (e.g.
   155  	// `exec.Command(ctx, ...)`), the state in that context is `sealed`, and
   156  	// calling Mock on that context again will panic. The state can be unsealed by
   157  	// calling ResetState on the context.
   158  	Mock(ctx context.Context, indata ...In) *Uses[Out]
   159  }
   160  
   161  // getOne is a little helper function for WithArgs implementations.
   162  func getOne[In any](indatas []In) In {
   163  	if len(indatas) > 1 {
   164  		panic("Mock only accepts a single input")
   165  	}
   166  
   167  	var ret In
   168  	inType := reflect.TypeOf(&ret).Elem()
   169  	if inType.Kind() == reflect.Pointer {
   170  		ret = reflect.New(inType.Elem()).Interface().(In)
   171  	}
   172  	if len(indatas) > 0 {
   173  		ret = indatas[0]
   174  	}
   175  	return ret
   176  }