github.com/kolbycrouch/elvish@v0.14.1-0.20210614162631-215b9ac1c423/pkg/prog/progtest/progtest.go (about)

     1  // Package progtest provides utilities for testing subprograms.
     2  //
     3  // This package intentionally has no test file; it is excluded from test
     4  // coverage.
     5  package progtest
     6  
     7  import (
     8  	"io/ioutil"
     9  	"os"
    10  	"strings"
    11  	"testing"
    12  
    13  	"src.elv.sh/pkg/testutil"
    14  )
    15  
    16  // Fixture is a test fixture suitable for testing programs.
    17  type Fixture struct {
    18  	pipes      [3]*pipe
    19  	dirCleanup func()
    20  }
    21  
    22  func captureOutput(p *pipe) {
    23  	b, err := ioutil.ReadAll(p.r)
    24  	if err != nil {
    25  		panic(err)
    26  	}
    27  	p.output <- b
    28  }
    29  
    30  // Setup sets up a test fixture. The caller is responsible for calling the
    31  // Cleanup method of the returned Fixture.
    32  func Setup() *Fixture {
    33  	_, dirCleanup := testutil.InTestDir()
    34  	pipes := [3]*pipe{makePipe(false), makePipe(true), makePipe(true)}
    35  	return &Fixture{pipes, dirCleanup}
    36  }
    37  
    38  // Cleanup cleans up the test fixture.
    39  func (f *Fixture) Cleanup() {
    40  	f.pipes[0].close()
    41  	f.pipes[1].close()
    42  	f.pipes[2].close()
    43  	f.dirCleanup()
    44  }
    45  
    46  // Fds returns the file descriptors in the fixture.
    47  func (f *Fixture) Fds() [3]*os.File {
    48  	return [3]*os.File{f.pipes[0].r, f.pipes[1].w, f.pipes[2].w}
    49  }
    50  
    51  // FeedIn feeds input to the standard input.
    52  func (f *Fixture) FeedIn(s string) {
    53  	_, err := f.pipes[0].w.WriteString(s)
    54  	if err != nil {
    55  		panic(err)
    56  	}
    57  	f.pipes[0].w.Close()
    58  	f.pipes[0].wClosed = true
    59  }
    60  
    61  // TestOut tests that the output on the given FD matches the given text.
    62  func (f *Fixture) TestOut(t *testing.T, fd int, wantOut string) {
    63  	t.Helper()
    64  	if out := f.pipes[fd].get(); out != wantOut {
    65  		t.Errorf("got out %q, want %q", out, wantOut)
    66  	}
    67  }
    68  
    69  // TestOutSnippet tests that the output on the given FD contains the given text.
    70  func (f *Fixture) TestOutSnippet(t *testing.T, fd int, wantOutSnippet string) {
    71  	t.Helper()
    72  	if err := f.pipes[fd].get(); !strings.Contains(err, wantOutSnippet) {
    73  		t.Errorf("got out %q, want string containing %q", err, wantOutSnippet)
    74  	}
    75  }
    76  
    77  type pipe struct {
    78  	r, w             *os.File
    79  	rClosed, wClosed bool
    80  	saved            string
    81  	output           chan []byte
    82  }
    83  
    84  func makePipe(capture bool) *pipe {
    85  	r, w, err := os.Pipe()
    86  	if err != nil {
    87  		panic(err)
    88  	}
    89  	if !capture {
    90  		return &pipe{r: r, w: w}
    91  	}
    92  	output := make(chan []byte, 1)
    93  	p := pipe{r: r, w: w, output: output}
    94  	go captureOutput(&p)
    95  	return &p
    96  }
    97  
    98  func (p *pipe) get() string {
    99  	if !p.wClosed {
   100  		// Close the write side so captureOutput goroutine sees EOF and
   101  		// terminates allowing us to capture and cache the output.
   102  		p.w.Close()
   103  		p.wClosed = true
   104  		if p.output != nil {
   105  			p.saved = string(<-p.output)
   106  		}
   107  	}
   108  	return p.saved
   109  }
   110  
   111  func (p *pipe) close() {
   112  	if !p.wClosed {
   113  		p.w.Close()
   114  		p.wClosed = true
   115  		if p.output != nil {
   116  			p.saved = string(<-p.output)
   117  		}
   118  	}
   119  	if !p.rClosed {
   120  		p.r.Close()
   121  		p.rClosed = true
   122  	}
   123  	if p.output != nil {
   124  		close(p.output)
   125  		p.output = nil
   126  	}
   127  }
   128  
   129  // MustWriteFile writes a file with the given name and content. It panics if the
   130  // write fails.
   131  func MustWriteFile(name, content string) {
   132  	err := ioutil.WriteFile(name, []byte(content), 0600)
   133  	if err != nil {
   134  		panic(err)
   135  	}
   136  }
   137  
   138  // Elvish returns an argument slice starting with "elvish".
   139  func Elvish(args ...string) []string {
   140  	return append([]string{"elvish"}, args...)
   141  }
   142  
   143  // TestError tests the error result of a program.
   144  func TestError(t *testing.T, f *Fixture, exit int, wantErrSnippet string) {
   145  	t.Helper()
   146  	if exit != 2 {
   147  		t.Errorf("got exit %v, want 2", exit)
   148  	}
   149  	f.TestOutSnippet(t, 2, wantErrSnippet)
   150  }