github.com/opentofu/opentofu@v1.7.1/internal/terminal/testing.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package terminal 7 8 import ( 9 "fmt" 10 "io" 11 "os" 12 "strings" 13 "sync" 14 "testing" 15 ) 16 17 // StreamsForTesting is a helper for test code that is aiming to test functions 18 // that interact with the input and output streams. 19 // 20 // This particular function is for the simple case of a function that only 21 // produces output: the returned input stream is connected to the system's 22 // "null device", as if a user had run OpenTofu with I/O redirection like 23 // </dev/null on Unix. It also configures the output as a pipe rather than 24 // as a terminal, and so can't be used to test whether code is able to adapt 25 // to different terminal widths. 26 // 27 // The return values are a Streams object ready to pass into a function under 28 // test, and a callback function for the test itself to call afterwards 29 // in order to obtain any characters that were written to the streams. Once 30 // you call the close function, the Streams object becomes invalid and must 31 // not be used anymore. Any caller of this function _must_ call close before 32 // its test concludes, even if it doesn't intend to check the output, or else 33 // it will leak resources. 34 // 35 // Since this function is for testing only, for convenience it will react to 36 // any setup errors by logging a message to the given testing.T object and 37 // then failing the test, preventing any later code from running. 38 func StreamsForTesting(t *testing.T) (streams *Streams, close func(*testing.T) *TestOutput) { 39 stdinR, err := os.Open(os.DevNull) 40 if err != nil { 41 t.Fatalf("failed to open /dev/null to represent stdin: %s", err) 42 } 43 44 // (Although we only have StreamsForTesting right now, it seems plausible 45 // that we'll want some other similar helpers for more complicated 46 // situations, such as codepaths that need to read from Stdin or 47 // tests for whether a function responds properly to terminal width. 48 // In that case, we'd probably want to factor out the core guts of this 49 // which set up the pipe *os.File values and the goroutines, but then 50 // let each caller produce its own Streams wrapping around those. For 51 // now though, it's simpler to just have this whole implementation together 52 // in one function.) 53 54 // Our idea of streams is only a very thin wrapper around OS-level file 55 // descriptors, so in order to produce a realistic implementation for 56 // the code under test while still allowing us to capture the output 57 // we'll OS-level pipes and concurrently copy anything we read from 58 // them into the output object. 59 outp := &TestOutput{} 60 var lock sync.Mutex // hold while appending to outp 61 stdoutR, stdoutW, err := os.Pipe() 62 if err != nil { 63 t.Fatalf("failed to create stdout pipe: %s", err) 64 } 65 stderrR, stderrW, err := os.Pipe() 66 if err != nil { 67 t.Fatalf("failed to create stderr pipe: %s", err) 68 } 69 var wg sync.WaitGroup // for waiting until our goroutines have exited 70 71 // We need an extra goroutine for each of the pipes so we can block 72 // on reading both of them alongside the caller hopefully writing to 73 // the write sides. 74 wg.Add(2) 75 consume := func(r *os.File, isErr bool) { 76 var buf [1024]byte 77 for { 78 n, err := r.Read(buf[:]) 79 if err != nil { 80 if err != io.EOF { 81 // We aren't allowed to write to the testing.T from 82 // a different goroutine than it was created on, but 83 // encountering other errors would be weird here anyway 84 // so we'll just panic. (If we were to just ignore this 85 // and then drop out of the loop then we might deadlock 86 // anyone still trying to write to the write end.) 87 panic(fmt.Sprintf("failed to read from pipe: %s", err)) 88 } 89 break 90 } 91 lock.Lock() 92 outp.parts = append(outp.parts, testOutputPart{ 93 isErr: isErr, 94 bytes: append(([]byte)(nil), buf[:n]...), // copy so we can reuse the buffer 95 }) 96 lock.Unlock() 97 } 98 wg.Done() 99 } 100 go consume(stdoutR, false) 101 go consume(stderrR, true) 102 103 close = func(t *testing.T) *TestOutput { 104 err := stdinR.Close() 105 if err != nil { 106 t.Errorf("failed to close stdin handle: %s", err) 107 } 108 109 // We'll close both of the writer streams now, which should in turn 110 // cause both of the "consume" goroutines above to terminate by 111 // encountering io.EOF. 112 err = stdoutW.Close() 113 if err != nil { 114 t.Errorf("failed to close stdout pipe: %s", err) 115 } 116 err = stderrW.Close() 117 if err != nil { 118 t.Errorf("failed to close stderr pipe: %s", err) 119 } 120 121 // The above error cases still allow this to complete and thus 122 // potentially allow the test to report its own result, but will 123 // ensure that the test doesn't pass while also leaking resources. 124 125 // Wait for the stream-copying goroutines to finish anything they 126 // are working on before we return, or else we might miss some 127 // late-arriving writes. 128 wg.Wait() 129 return outp 130 } 131 132 return &Streams{ 133 Stdout: &OutputStream{ 134 File: stdoutW, 135 }, 136 Stderr: &OutputStream{ 137 File: stderrW, 138 }, 139 Stdin: &InputStream{ 140 File: stdinR, 141 }, 142 }, close 143 } 144 145 // TestOutput is a type used to return the results from the various stream 146 // testing helpers. It encapsulates any captured writes to the output and 147 // error streams, and has methods to consume that data in some different ways 148 // to allow for a few different styles of testing. 149 type TestOutput struct { 150 parts []testOutputPart 151 } 152 153 type testOutputPart struct { 154 // isErr is true if this part was written to the error stream, or false 155 // if it was written to the output stream. 156 isErr bool 157 158 // bytes are the raw bytes that were written 159 bytes []byte 160 } 161 162 // All returns the output written to both the Stdout and Stderr streams, 163 // interleaved together in the order of writing in a single string. 164 func (o TestOutput) All() string { 165 buf := &strings.Builder{} 166 for _, part := range o.parts { 167 buf.Write(part.bytes) 168 } 169 return buf.String() 170 } 171 172 // Stdout returns the output written to just the Stdout stream, ignoring 173 // anything that was written to the Stderr stream. 174 func (o TestOutput) Stdout() string { 175 buf := &strings.Builder{} 176 for _, part := range o.parts { 177 if part.isErr { 178 continue 179 } 180 buf.Write(part.bytes) 181 } 182 return buf.String() 183 } 184 185 // Stderr returns the output written to just the Stderr stream, ignoring 186 // anything that was written to the Stdout stream. 187 func (o TestOutput) Stderr() string { 188 buf := &strings.Builder{} 189 for _, part := range o.parts { 190 if !part.isErr { 191 continue 192 } 193 buf.Write(part.bytes) 194 } 195 return buf.String() 196 }