evaltest.go 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. // Package evaltest provides a framework for testing Elvish script.
  2. //
  3. // The entry point for the framework is the Test function, which accepts a
  4. // *testing.T and any number of test cases.
  5. //
  6. // Test cases are constructed using the That function, followed by method calls
  7. // that add additional information to it.
  8. //
  9. // Example:
  10. //
  11. // Test(t,
  12. // That("put x").Puts("x"),
  13. // That("echo x").Prints("x\n"))
  14. //
  15. // If some setup is needed, use the TestWithSetup function instead.
  16. package evaltest
  17. import (
  18. "bytes"
  19. "os"
  20. "reflect"
  21. "strings"
  22. "testing"
  23. "github.com/google/go-cmp/cmp"
  24. "src.elv.sh/pkg/eval"
  25. "src.elv.sh/pkg/eval/vals"
  26. "src.elv.sh/pkg/parse"
  27. "src.elv.sh/pkg/testutil"
  28. "src.elv.sh/pkg/tt"
  29. )
  30. // Case is a test case that can be used in Test.
  31. type Case struct {
  32. codes []string
  33. setup func(ev *eval.Evaler)
  34. verify func(t *testing.T)
  35. want result
  36. }
  37. type result struct {
  38. ValueOut []interface{}
  39. BytesOut []byte
  40. StderrOut []byte
  41. CompilationError error
  42. Exception error
  43. }
  44. // That returns a new Case with the specified source code. Multiple arguments
  45. // are joined with newlines. To specify multiple pieces of code that are
  46. // executed separately, use the Then method to append code pieces.
  47. //
  48. // When combined with subsequent method calls, a test case reads like English.
  49. // For example, a test for the fact that "put x" puts "x" reads:
  50. //
  51. // That("put x").Puts("x")
  52. func That(lines ...string) Case {
  53. return Case{codes: []string{strings.Join(lines, "\n")}}
  54. }
  55. // Then returns a new Case that executes the given code in addition. Multiple
  56. // arguments are joined with newlines.
  57. func (c Case) Then(lines ...string) Case {
  58. c.codes = append(c.codes, strings.Join(lines, "\n"))
  59. return c
  60. }
  61. // Then returns a new Case with the given setup function executed on the Evaler
  62. // before the code is executed.
  63. func (c Case) WithSetup(f func(*eval.Evaler)) Case {
  64. c.setup = f
  65. return c
  66. }
  67. // DoesNothing returns t unchanged. It is useful to mark tests that don't have
  68. // any side effects, for example:
  69. //
  70. // That("nop").DoesNothing()
  71. func (c Case) DoesNothing() Case {
  72. return c
  73. }
  74. // Puts returns an altered Case that runs an additional verification function.
  75. func (c Case) Passes(f func(t *testing.T)) Case {
  76. c.verify = f
  77. return c
  78. }
  79. // Puts returns an altered Case that requires the source code to produce the
  80. // specified values in the value channel when evaluated.
  81. func (c Case) Puts(vs ...interface{}) Case {
  82. c.want.ValueOut = vs
  83. return c
  84. }
  85. // Prints returns an altered Case that requires the source code to produce the
  86. // specified output in the byte pipe when evaluated.
  87. func (c Case) Prints(s string) Case {
  88. c.want.BytesOut = []byte(s)
  89. return c
  90. }
  91. // PrintsStderrWith returns an altered Case that requires the stderr output to
  92. // contain the given text.
  93. func (c Case) PrintsStderrWith(s string) Case {
  94. c.want.StderrOut = []byte(s)
  95. return c
  96. }
  97. // Throws returns an altered Case that requires the source code to throw an
  98. // exception with the given reason. The reason supports special matcher values
  99. // constructed by functions like ErrorWithMessage.
  100. //
  101. // If at least one stacktrace string is given, the exception must also have a
  102. // stacktrace matching the given source fragments, frame by frame (innermost
  103. // frame first). If no stacktrace string is given, the stack trace of the
  104. // exception is not checked.
  105. func (c Case) Throws(reason error, stacks ...string) Case {
  106. c.want.Exception = exc{reason, stacks}
  107. return c
  108. }
  109. // DoesNotCompile returns an altered Case that requires the source code to fail
  110. // compilation.
  111. func (c Case) DoesNotCompile() Case {
  112. c.want.CompilationError = anyError{}
  113. return c
  114. }
  115. // Test runs test cases. For each test case, a new Evaler is created with
  116. // NewEvaler.
  117. func Test(t *testing.T, tests ...Case) {
  118. t.Helper()
  119. TestWithSetup(t, func(*eval.Evaler) {}, tests...)
  120. }
  121. // TestWithSetup runs test cases. For each test case, a new Evaler is created
  122. // with NewEvaler and passed to the setup function.
  123. func TestWithSetup(t *testing.T, setup func(*eval.Evaler), tests ...Case) {
  124. t.Helper()
  125. for _, tc := range tests {
  126. t.Run(strings.Join(tc.codes, "\n"), func(t *testing.T) {
  127. t.Helper()
  128. ev := eval.NewEvaler()
  129. setup(ev)
  130. if tc.setup != nil {
  131. tc.setup(ev)
  132. }
  133. r := evalAndCollect(t, ev, tc.codes)
  134. if tc.verify != nil {
  135. tc.verify(t)
  136. }
  137. if !matchOut(tc.want.ValueOut, r.ValueOut) {
  138. t.Errorf("got value out (-want +got):\n%s",
  139. cmp.Diff(r.ValueOut, tc.want.ValueOut, tt.CommonCmpOpt))
  140. }
  141. if !bytes.Equal(tc.want.BytesOut, r.BytesOut) {
  142. t.Errorf("got bytes out %q, want %q", r.BytesOut, tc.want.BytesOut)
  143. }
  144. if !bytes.Contains(r.StderrOut, tc.want.StderrOut) {
  145. t.Errorf("got stderr out %q, want %q", r.StderrOut, tc.want.StderrOut)
  146. }
  147. if !matchErr(tc.want.CompilationError, r.CompilationError) {
  148. t.Errorf("got compilation error %v, want %v",
  149. r.CompilationError, tc.want.CompilationError)
  150. }
  151. if !matchErr(tc.want.Exception, r.Exception) {
  152. t.Errorf("unexpected exception")
  153. if exc, ok := r.Exception.(eval.Exception); ok {
  154. // For an eval.Exception report the type of the underlying error.
  155. t.Logf("got: %T: %v", exc.Reason(), exc)
  156. t.Logf("stack trace: %#v", getStackTexts(exc.StackTrace()))
  157. } else {
  158. t.Logf("got: %T: %v", r.Exception, r.Exception)
  159. }
  160. t.Errorf("want: %v", tc.want.Exception)
  161. }
  162. })
  163. }
  164. }
  165. func evalAndCollect(t *testing.T, ev *eval.Evaler, texts []string) result {
  166. var r result
  167. port1, collect1 := capturePort()
  168. port2, collect2 := capturePort()
  169. ports := []*eval.Port{eval.DummyInputPort, port1, port2}
  170. for _, text := range texts {
  171. err := ev.Eval(parse.Source{Name: "[test]", Code: text},
  172. eval.EvalCfg{Ports: ports, Interrupt: eval.ListenInterrupts})
  173. if parse.GetError(err) != nil {
  174. t.Fatalf("Parse(%q) error: %s", text, err)
  175. } else if eval.GetCompilationError(err) != nil {
  176. // NOTE: If multiple code pieces have compilation errors, only the
  177. // last one compilation error is saved.
  178. r.CompilationError = err
  179. } else if err != nil {
  180. // NOTE: If multiple code pieces throw exceptions, only the last one
  181. // is saved.
  182. r.Exception = err
  183. }
  184. }
  185. r.ValueOut, r.BytesOut = collect1()
  186. _, r.StderrOut = collect2()
  187. return r
  188. }
  189. // Like eval.CapturePort, but captures values and bytes separately. Also panics
  190. // if it cannot create a pipe.
  191. func capturePort() (*eval.Port, func() ([]interface{}, []byte)) {
  192. var values []interface{}
  193. var bytes []byte
  194. port, done, err := eval.PipePort(
  195. func(ch <-chan interface{}) {
  196. for v := range ch {
  197. values = append(values, v)
  198. }
  199. },
  200. func(r *os.File) {
  201. bytes = testutil.MustReadAllAndClose(r)
  202. })
  203. if err != nil {
  204. panic(err)
  205. }
  206. return port, func() ([]interface{}, []byte) {
  207. done()
  208. return values, bytes
  209. }
  210. }
  211. func matchOut(want, got []interface{}) bool {
  212. if len(got) != len(want) {
  213. return false
  214. }
  215. for i := range got {
  216. if !match(got[i], want[i]) {
  217. return false
  218. }
  219. }
  220. return true
  221. }
  222. func match(got, want interface{}) bool {
  223. switch got := got.(type) {
  224. case float64:
  225. // Special-case float64 to correctly handle NaN and support
  226. // approximate comparison.
  227. switch want := want.(type) {
  228. case float64:
  229. return matchFloat64(got, want, 0)
  230. case Approximately:
  231. return matchFloat64(got, want.F, ApproximatelyThreshold)
  232. }
  233. case string:
  234. switch want := want.(type) {
  235. case MatchingRegexp:
  236. return matchRegexp(want.Pattern, got)
  237. }
  238. }
  239. return vals.Equal(got, want)
  240. }
  241. func matchErr(want, got error) bool {
  242. if want == nil {
  243. return got == nil
  244. }
  245. if matcher, ok := want.(errorMatcher); ok {
  246. return matcher.matchError(got)
  247. }
  248. return reflect.DeepEqual(want, got)
  249. }