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  }