/
githubmirror
/
terraform
Обзор
Документация
Войти
/
githubmirror
/
terraform
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
internal/terminal/testing.go
198 строк
6 KB
Sarah French
test: Mark some test-related functions as helpers, so error messages are easier to reason about.
21 июл 2026, 18:12
21 июл 2026, 18:12
d525915
Код
Авторство
О чём код?
// Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: BUSL-1.1 package terminal import ( "fmt" "io" "os" "strings" "sync" "testing" ) // StreamsForTesting is a helper for test code that is aiming to test functions // that interact with the input and output streams. // // This particular function is for the simple case of a function that only // produces output: the returned input stream is connected to the system's // "null device", as if a user had run Terraform with I/O redirection like // </dev/null on Unix. It also configures the output as a pipe rather than // as a terminal, and so can't be used to test whether code is able to adapt // to different terminal widths. // // The return values are a Streams object ready to pass into a function under // test, and a callback function for the test itself to call afterwards // in order to obtain any characters that were written to the streams. Once // you call the close function, the Streams object becomes invalid and must // not be used anymore. Any caller of this function _must_ call close before // its test concludes, even if it doesn't intend to check the output, or else // it will leak resources. // // Since this function is for testing only, for convenience it will react to // any setup errors by logging a message to the given testing.T object and // then failing the test, preventing any later code from running. func StreamsForTesting(t *testing.T) (streams *Streams, close func(*testing.T) *TestOutput) { t.Helper() stdinR, err := os.Open(os.DevNull) if err != nil { t.Fatalf("failed to open /dev/null to represent stdin: %s", err) } // (Although we only have StreamsForTesting right now, it seems plausible // that we'll want some other similar helpers for more complicated // situations, such as codepaths that need to read from Stdin or // tests for whether a function responds properly to terminal width. // In that case, we'd probably want to factor out the core guts of this // which set up the pipe *os.File values and the goroutines, but then // let each caller produce its own Streams wrapping around those. For // now though, it's simpler to just have this whole implementation together // in one function.) // Our idea of streams is only a very thin wrapper around OS-level file // descriptors, so in order to produce a realistic implementation for // the code under test while still allowing us to capture the output // we'll OS-level pipes and concurrently copy anything we read from // them into the output object. outp := &TestOutput{} var lock sync.Mutex // hold while appending to outp stdoutR, stdoutW, err := os.Pipe() if err != nil { t.Fatalf("failed to create stdout pipe: %s", err) } stderrR, stderrW, err := os.Pipe() if err != nil { t.Fatalf("failed to create stderr pipe: %s", err) } var wg sync.WaitGroup // for waiting until our goroutines have exited // We need an extra goroutine for each of the pipes so we can block // on reading both of them alongside the caller hopefully writing to // the write sides. wg.Add(2) consume := func(r *os.File, isErr bool) { var buf [1024]byte for { n, err := r.Read(buf[:]) if err != nil { if err != io.EOF { // We aren't allowed to write to the testing.T from // a different goroutine than it was created on, but // encountering other errors would be weird here anyway // so we'll just panic. (If we were to just ignore this // and then drop out of the loop then we might deadlock // anyone still trying to write to the write end.) panic(fmt.Sprintf("failed to read from pipe: %s", err)) } break } lock.Lock() outp.parts = append(outp.parts, testOutputPart{ isErr: isErr, bytes: append(([]byte)(nil), buf[:n]...), // copy so we can reuse the buffer }) lock.Unlock() } wg.Done() } go consume(stdoutR, false) go consume(stderrR, true) close = func(t *testing.T) *TestOutput { t.Helper() err := stdinR.Close() if err != nil { t.Errorf("failed to close stdin handle: %s", err) } // We'll close both of the writer streams now, which should in turn // cause both of the "consume" goroutines above to terminate by // encountering io.EOF. err = stdoutW.Close() if err != nil { t.Errorf("failed to close stdout pipe: %s", err) } err = stderrW.Close() if err != nil { t.Errorf("failed to close stderr pipe: %s", err) } // The above error cases still allow this to complete and thus // potentially allow the test to report its own result, but will // ensure that the test doesn't pass while also leaking resources. // Wait for the stream-copying goroutines to finish anything they // are working on before we return, or else we might miss some // late-arriving writes. wg.Wait() return outp } return &Streams{ Stdout: &OutputStream{ File: stdoutW, }, Stderr: &OutputStream{ File: stderrW, }, Stdin: &InputStream{ File: stdinR, }, }, close } // TestOutput is a type used to return the results from the various stream // testing helpers. It encapsulates any captured writes to the output and // error streams, and has methods to consume that data in some different ways // to allow for a few different styles of testing. type TestOutput struct { parts []testOutputPart } type testOutputPart struct { // isErr is true if this part was written to the error stream, or false // if it was written to the output stream. isErr bool // bytes are the raw bytes that were written bytes []byte } // All returns the output written to both the Stdout and Stderr streams, // interleaved together in the order of writing in a single string. func (o TestOutput) All() string { buf := &strings.Builder{} for _, part := range o.parts { buf.Write(part.bytes) } return buf.String() } // Stdout returns the output written to just the Stdout stream, ignoring // anything that was written to the Stderr stream. func (o TestOutput) Stdout() string { buf := &strings.Builder{} for _, part := range o.parts { if part.isErr { continue } buf.Write(part.bytes) } return buf.String() } // Stderr returns the output written to just the Stderr stream, ignoring // anything that was written to the Stdout stream. func (o TestOutput) Stderr() string { buf := &strings.Builder{} for _, part := range o.parts { if !part.isErr { continue } buf.Write(part.bytes) } return buf.String() }