go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/exec/internal/execmockctx/global.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 execmockctx provides the minimum interface that the 16 // `go.chromium.org/luci/common/exec` library needs to hook into the mocking 17 // system provided by `go.chromium.org/luci/common/exec/execmock` without 18 // needing to actually link the execmock code (including it's registration of 19 // the Simple runner, and the implementation of the http test server) 20 // into non-test binaries. 21 package execmockctx 22 23 import ( 24 "context" 25 "os" 26 "os/exec" 27 "sync" 28 29 "go.chromium.org/luci/common/errors" 30 "go.chromium.org/luci/common/system/environ" 31 ) 32 33 // MockCriteria is what execmock uses to look up Entries. 34 type MockCriteria struct { 35 Args []string 36 Env environ.Env 37 } 38 39 // NewMockCriteria is a convenience function to return a new MockCriteria from 40 // a Cmd. 41 func NewMockCriteria(cmd *exec.Cmd) *MockCriteria { 42 return &MockCriteria{ 43 Args: cmd.Args, 44 Env: environ.New(cmd.Env), 45 } 46 } 47 48 var ErrNoMatchingMock = errors.New("execmock: mocking enabled but no mock matches") 49 50 type MockInvocation struct { 51 // Unique ID of this invocation. 52 ID uint64 53 54 // An environment variable ("KEY=Value") which exec should add to 55 // the command during invocation. 56 // 57 // This is "generic" in the sense that it doesn't tie to the specific 58 // key/value format that `execmockserver` actually uses, but: 59 // 1) this package is internal, so only exec & execmock can use it. 60 // 2) this package is separate from execmock specifically to decouple the 61 // need to pull in heavy stuff like "net/http" and "testing" into uses 62 // of "exec". 63 // 64 // Practically speaking this will always look like 65 // "LUCI_EXECMOCK_CTX=localhost:port|invocationID". 66 EnvVar string 67 68 // After Wait()'ing for the process, the Cmd runner can call this to get an 69 // error (if any) which the Runner returned, as well as the panic stack (if 70 // any) from the runner. 71 GetErrorOutput func() (panicStack string, err error) 72 } 73 74 // CreateMockInvocation encapsulates the entire functionality which 75 // "common/exec" needs to call, and which "common/exec/execmock" needs to 76 // implement. 77 // 78 // This function, if set, should evaluate `mc` against state stored by execmock 79 // in `ctx`, and return a MockInvocation if `mc` matches a mock. Note that this 80 // will return an error wrapping ErrNoMatchingMock if the context doesn't define 81 // a matching mock (which could be due to the user forgetting to Init the 82 // context at all). 83 // 84 // `proc` should point to the underlying Cmd.Process field; This will be used to 85 // expose the Process to the test via Usage.GetProcess(). 86 // 87 // If this returns (nil, nil) it means that this invocation should be passed 88 // through (run as normal). 89 type CreateMockInvocation func(mc *MockCriteria, proc **os.Process) (*MockInvocation, error) 90 91 var mockCreator func(context.Context) (mocker CreateMockInvocation, chatty bool) 92 var mockCreatorOnce sync.Once 93 94 // EnableMockingForThisProcess is called from execmock to install the mocker service here. 95 func EnableMockingForThisProcess(mcFactory func(context.Context) (mocker CreateMockInvocation, chatty bool)) { 96 alreadySet := true 97 mockCreatorOnce.Do(func() { 98 alreadySet = false 99 mockCreator = mcFactory 100 }) 101 102 if mockCreator == nil { 103 panic("EnableMockingForThisProcess called after MockingEnabled()") 104 } 105 106 if alreadySet { 107 panic("EnableMockingForThisProcess called twice") 108 } 109 } 110 111 // GetMockCreator returns the process wide implementation of 112 // CreateMockInvocation, or nil, if mocking is not enabled for this process. 113 // 114 // Once this function has been called (i.e. after the first call of 115 // ".../common/exec.CommandContext()"), EnableMockingForThisProcess will panic. 116 // 117 // If no mocking is configured for this process, returns (nil, false). 118 func GetMockCreator(ctx context.Context) (mocker CreateMockInvocation, chatty bool) { 119 mockCreatorOnce.Do(func() { 120 mockCreator = nil 121 }) 122 if mockCreator != nil { 123 mocker, chatty = mockCreator(ctx) 124 } 125 return 126 }