github.com/elves/Elvish@v0.12.0/eval/testutils.go (about)

     1  // Framework for testing Elvish script. This file does not file a _test.go
     2  // suffix so that it can be used from other packages that also want to test the
     3  // modules they implement (e.g. edit: and re:).
     4  //
     5  // The entry point for the framework is the Test function, which accepts a
     6  // *testing.T and a variadic number of test cases. Test cases are constructed
     7  // using the That function followed by methods that add constraints on the test
     8  // case. Overall, a test looks like:
     9  //
    10  //     Test(t,
    11  //         That("put x").Puts("x"),
    12  //         That("echo x").Prints("x\n"))
    13  //
    14  // If some setup is needed, use the TestWithSetup function instead.
    15  
    16  package eval
    17  
    18  import (
    19  	"bytes"
    20  	"errors"
    21  	"fmt"
    22  	"io/ioutil"
    23  	"os"
    24  	"reflect"
    25  	"testing"
    26  
    27  	"github.com/elves/elvish/eval/vals"
    28  	"github.com/elves/elvish/parse"
    29  	"github.com/elves/elvish/util"
    30  )
    31  
    32  // TestCase is a test case for Test.
    33  type TestCase struct {
    34  	text string
    35  	want
    36  }
    37  
    38  type want struct {
    39  	out      []interface{}
    40  	bytesOut []byte
    41  	err      error
    42  }
    43  
    44  // A special value for want.err to indicate that any error, as long as not nil,
    45  // is OK
    46  var errAny = errors.New("any error")
    47  
    48  // The following functions and methods are used to build Test structs. They are
    49  // supposed to read like English, so a test that "put x" should put "x" reads:
    50  //
    51  // That("put x").Puts("x")
    52  
    53  // That returns a new Test with the specified source code.
    54  func That(text string) TestCase {
    55  	return TestCase{text: text}
    56  }
    57  
    58  // DoesNothing returns t unchanged. It is used to mark that a piece of code
    59  // should simply does nothing. In particular, it shouldn't have any output and
    60  // does not error.
    61  func (t TestCase) DoesNothing() TestCase {
    62  	return t
    63  }
    64  
    65  // Puts returns an altered Test that requires the source code to produce the
    66  // specified values in the value channel when evaluated.
    67  func (t TestCase) Puts(vs ...interface{}) TestCase {
    68  	t.want.out = vs
    69  	return t
    70  }
    71  
    72  // Puts returns an altered Test that requires the source code to produce the
    73  // specified strings in the value channel when evaluated.
    74  func (t TestCase) PutsStrings(ss []string) TestCase {
    75  	t.want.out = make([]interface{}, len(ss))
    76  	for i, s := range ss {
    77  		t.want.out[i] = s
    78  	}
    79  	return t
    80  }
    81  
    82  // Prints returns an altered test that requires the source code to produce
    83  // the specified output in the byte pipe when evaluated.
    84  func (t TestCase) Prints(s string) TestCase {
    85  	t.want.bytesOut = []byte(s)
    86  	return t
    87  }
    88  
    89  // ErrorsWith returns an altered Test that requires the source code to result in
    90  // the specified error when evaluted.
    91  func (t TestCase) ErrorsWith(err error) TestCase {
    92  	t.want.err = err
    93  	return t
    94  }
    95  
    96  // Errors returns an altered Test that requires the source code to result in any
    97  // error when evaluated.
    98  func (t TestCase) Errors() TestCase {
    99  	return t.ErrorsWith(errAny)
   100  }
   101  
   102  // Test runs test cases. For each test case, a new Evaler is created with
   103  // NewEvaler.
   104  func Test(t *testing.T, tests ...TestCase) {
   105  	TestWithSetup(t, func(*Evaler) {}, tests...)
   106  }
   107  
   108  // Test runs test cases. For each test case, a new Evaler is created with
   109  // NewEvaler and passed to the setup function.
   110  func TestWithSetup(t *testing.T, setup func(*Evaler), tests ...TestCase) {
   111  	for _, tt := range tests {
   112  		ev := NewEvaler()
   113  		setup(ev)
   114  		out, bytesOut, err := evalAndCollect(t, ev, []string{tt.text}, len(tt.want.out))
   115  
   116  		first := true
   117  		errorf := func(format string, args ...interface{}) {
   118  			if first {
   119  				first = false
   120  				t.Errorf("eval(%q) fails:", tt.text)
   121  			}
   122  			t.Errorf("  "+format, args...)
   123  		}
   124  
   125  		if !matchOut(tt.want.out, out) {
   126  			errorf("got out=%v, want %v", out, tt.want.out)
   127  		}
   128  		if !bytes.Equal(tt.want.bytesOut, bytesOut) {
   129  			errorf("got bytesOut=%q, want %q", bytesOut, tt.want.bytesOut)
   130  		}
   131  		if !matchErr(tt.want.err, err) {
   132  			errorf("got err=%v, want %v", err, tt.want.err)
   133  		}
   134  
   135  		ev.Close()
   136  	}
   137  }
   138  
   139  func evalAndCollect(t *testing.T, ev *Evaler, texts []string, chsize int) ([]interface{}, []byte, error) {
   140  	// Collect byte output
   141  	bytesOut := []byte{}
   142  	pr, pw, _ := os.Pipe()
   143  	bytesDone := make(chan struct{})
   144  	go func() {
   145  		for {
   146  			var buf [64]byte
   147  			nr, err := pr.Read(buf[:])
   148  			bytesOut = append(bytesOut, buf[:nr]...)
   149  			if err != nil {
   150  				break
   151  			}
   152  		}
   153  		close(bytesDone)
   154  	}()
   155  
   156  	// Channel output
   157  	outs := []interface{}{}
   158  
   159  	// Eval error. Only that of the last text is saved.
   160  	var ex error
   161  
   162  	for i, text := range texts {
   163  		name := fmt.Sprintf("test%d.elv", i)
   164  		src := NewScriptSource(name, name, text)
   165  
   166  		op := mustParseAndCompile(t, ev, src)
   167  
   168  		outCh := make(chan interface{}, chsize)
   169  		outDone := make(chan struct{})
   170  		go func() {
   171  			for v := range outCh {
   172  				outs = append(outs, v)
   173  			}
   174  			close(outDone)
   175  		}()
   176  
   177  		ports := []*Port{
   178  			{File: os.Stdin, Chan: ClosedChan},
   179  			{File: pw, Chan: outCh},
   180  			{File: os.Stderr, Chan: BlackholeChan},
   181  		}
   182  
   183  		ex = ev.eval(op, ports, src)
   184  		close(outCh)
   185  		<-outDone
   186  	}
   187  
   188  	pw.Close()
   189  	<-bytesDone
   190  	pr.Close()
   191  
   192  	return outs, bytesOut, ex
   193  }
   194  
   195  func mustParseAndCompile(t *testing.T, ev *Evaler, src *Source) Op {
   196  	n, err := parse.Parse(src.name, src.code)
   197  	if err != nil {
   198  		t.Fatalf("Parse(%q) error: %s", src.code, err)
   199  	}
   200  	op, err := ev.Compile(n, src)
   201  	if err != nil {
   202  		t.Fatalf("Compile(Parse(%q)) error: %s", src.code, err)
   203  	}
   204  	return op
   205  }
   206  
   207  func matchOut(want, got []interface{}) bool {
   208  	if len(got) == 0 && len(want) == 0 {
   209  		return true
   210  	}
   211  	if len(got) != len(want) {
   212  		return false
   213  	}
   214  	for i := range got {
   215  		if !vals.Equal(got[i], want[i]) {
   216  			return false
   217  		}
   218  	}
   219  	return true
   220  }
   221  
   222  func matchErr(want, got error) bool {
   223  	if got == nil {
   224  		return want == nil
   225  	}
   226  	return want == errAny || reflect.DeepEqual(got.(*Exception).Cause, want)
   227  }
   228  
   229  // MustMkdirAll calls os.MkdirAll and panics if an error is returned. It is
   230  // mainly useful in tests.
   231  func MustMkdirAll(name string, perm os.FileMode) {
   232  	err := os.MkdirAll(name, perm)
   233  	if err != nil {
   234  		panic(err)
   235  	}
   236  }
   237  
   238  // MustCreateEmpty creates an empty file, and panics if an error occurs. It is
   239  // mainly useful in tests.
   240  func MustCreateEmpty(name string) {
   241  	file, err := os.Create(name)
   242  	if err != nil {
   243  		panic(err)
   244  	}
   245  	file.Close()
   246  }
   247  
   248  // MustWriteFile calls ioutil.WriteFile and panics if an error occurs. It is
   249  // mainly useful in tests.
   250  func MustWriteFile(filename string, data []byte, perm os.FileMode) {
   251  	err := ioutil.WriteFile(filename, data, perm)
   252  	if err != nil {
   253  		panic(err)
   254  	}
   255  }
   256  
   257  // InTempHome is like util.InTempDir, but it also sets HOME to the temporary
   258  // directory when f is called.
   259  func InTempHome(f func(string)) {
   260  	util.InTempDir(func(tmpHome string) {
   261  		oldHome := os.Getenv("HOME")
   262  		os.Setenv("HOME", tmpHome)
   263  		f(tmpHome)
   264  		os.Setenv("HOME", oldHome)
   265  	})
   266  }