go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/exec/execmock/mock_example_test.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_test 16 17 import ( 18 "bytes" 19 "context" 20 "flag" 21 "fmt" 22 "os" 23 "path/filepath" 24 "testing" 25 26 "go.chromium.org/luci/common/errors" 27 "go.chromium.org/luci/common/exec" 28 "go.chromium.org/luci/common/exec/execmock" 29 ) 30 31 func TestMain(m *testing.M) { 32 execmock.Intercept() 33 os.Exit(m.Run()) 34 } 35 36 // CustomInput is data that the TEST wants to communicate to CustomRunner. 37 // 38 // This should typically be things which affect the behavior of the Runner, e.g. 39 // to simulate different outputs, errors, etc., or to change the data that the 40 // Runner responds with. 41 type CustomInput struct { 42 // OutputFile is a string that the test sends to CustomRunner to have it write 43 // out to the `--output` argument. 44 OutputFile []byte 45 46 // CollectInput indicates that the test wants the Runner to collect the data 47 // of the file specified by the `--input` argument. 48 CollectInput bool 49 } 50 51 type CustomOutput struct { 52 // If CollectInput was true, expect and read the input file and return the 53 // data here. 54 InputData []byte 55 } 56 57 var CustomRunner = execmock.Register(func(in *CustomInput) (*CustomOutput, int, error) { 58 input := flag.String("input", "", "") 59 _ = flag.String("random", "", "") 60 output := flag.String("output", "", "") 61 flag.Parse() 62 63 ret := &CustomOutput{} 64 65 if in.CollectInput { 66 if *input == "" { 67 return nil, 1, errors.New("input was expected") 68 } 69 data, err := os.ReadFile(*input) 70 if err != nil { 71 return nil, 1, errors.Annotate(err, "reading input file").Err() 72 } 73 ret.InputData = data 74 } 75 76 if in.OutputFile != nil { 77 if err := os.WriteFile(*output, in.OutputFile, 0777); err != nil { 78 return nil, 1, errors.Annotate(err, "writing output file").Err() 79 } 80 } 81 82 return ret, 0, nil 83 }) 84 85 func must(err error) { 86 if err != nil { 87 panic(err) 88 } 89 } 90 91 func ExampleInit() { 92 // See this file for the definition of `CustomRunner` 93 94 // BEGIN: TEST CODE 95 ctx := execmock.Init(context.Background()) 96 97 outputUses := CustomRunner.WithArgs("--output").Mock(ctx, &CustomInput{ 98 OutputFile: []byte("hello I am Mx. Catopolous"), 99 }) 100 allOtherUses := CustomRunner.Mock(ctx) 101 inputUses := CustomRunner.WithArgs("--input").Mock(ctx, &CustomInput{ 102 CollectInput: true, 103 }) 104 tmpDir, err := os.MkdirTemp("", "") 105 must(errors.Annotate(err, "failed to create tmpDir").Err()) 106 107 defer func() { 108 must(os.RemoveAll(tmpDir)) 109 }() 110 // END: TEST CODE 111 112 // BEGIN: APPLICATION CODE 113 // Your program would then get `ctx` passed to it from the test, and it would 114 // run commands as usual: 115 116 // this should be intercepted by `allOtherUses` 117 must(exec.Command(ctx, "some_prog", "--random", "argument").Run()) 118 fmt.Println("[some_prog --random argument]: OK") 119 120 // this should be intercepted by `inputUses` 121 inputFile := filepath.Join(tmpDir, "input_file") 122 must(os.WriteFile(inputFile, []byte("hello world"), 0777)) 123 must(exec.Command(ctx, "another_program", "--input", inputFile).Run()) 124 fmt.Println("[another_program --input inputFile]: OK") 125 126 // this should be intercepted by `outputUses`. 127 outputFile := filepath.Join(tmpDir, "output_file") 128 outputCall := exec.Command(ctx, "another_program", "--output", outputFile) 129 outputCall.Stdout = os.Stdout 130 outputCall.Stderr = os.Stderr 131 must(outputCall.Run()) 132 133 outputFileData, err := os.ReadFile(outputFile) 134 must(errors.Annotate(err, "our mock failed to write %q", outputFile).Err()) 135 if !bytes.Equal(outputFileData, []byte("hello I am Mx. Catopolous")) { 136 panic(errors.New("our mock failed to write the expected data")) 137 } 138 fmt.Printf("[another_program --output outputFile]: %q\n", outputFileData) 139 // END: APPLICATION CODE 140 141 // BEGIN: TEST CODE 142 // Back in the test after running the application code. Now we can look and 143 // see what our mocks caught. 144 fmt.Printf("allOtherUses: got %d calls\n", len(allOtherUses.Snapshot())) 145 fmt.Printf("outputUses: got %d calls\n", len(outputUses.Snapshot())) 146 147 incallMock, _, err := inputUses.Snapshot()[0].GetOutput(ctx) 148 must(errors.Annotate(err, "could not get output from --input call").Err()) 149 fmt.Printf("incallMock: saw %q written to the mock program\n", incallMock.InputData) 150 // END: TEST CODE 151 152 // Output: 153 // [some_prog --random argument]: OK 154 // [another_program --input inputFile]: OK 155 // [another_program --output outputFile]: "hello I am Mx. Catopolous" 156 // allOtherUses: got 1 calls 157 // outputUses: got 1 calls 158 // incallMock: saw "hello world" written to the mock program 159 }