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 }