github.com/petermattis/pebble@v0.0.0-20190905164901-ab51a2166067/internal/datadriven/datadriven.go (about)

     1  // Copyright 2018 The Cockroach Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
    12  // implied. See the License for the specific language governing
    13  // permissions and limitations under the License.
    14  
    15  package datadriven // import "github.com/petermattis/pebble/internal/datadriven"
    16  
    17  import (
    18  	"bufio"
    19  	"flag"
    20  	"fmt"
    21  	"io"
    22  	"io/ioutil"
    23  	"os"
    24  	"path/filepath"
    25  	"strconv"
    26  	"strings"
    27  	"testing"
    28  )
    29  
    30  var (
    31  	rewriteTestFiles = flag.Bool(
    32  		"rewrite", false,
    33  		"ignore the expected results and rewrite the test files with the actual results from this "+
    34  			"run. Used to update tests when a change affects many cases; please verify the testfile "+
    35  			"diffs carefully!",
    36  	)
    37  )
    38  
    39  // RunTest invokes a data-driven test. The test cases are contained in a
    40  // separate test file and are dynamically loaded, parsed, and executed by this
    41  // testing framework. By convention, test files are typically located in a
    42  // sub-directory called "testdata". Each test file has the following format:
    43  //
    44  //   <command>[,<command>...] [arg | arg=val | arg=(val1, val2, ...)]...
    45  //   <input to the command>
    46  //   ----
    47  //   <expected results>
    48  //
    49  // The command input can contain blank lines. However, by default, the expected
    50  // results cannot contain blank lines. This alternate syntax allows the use of
    51  // blank lines:
    52  //
    53  //   <command>[,<command>...] [arg | arg=val | arg=(val1, val2, ...)]...
    54  //   <input to the command>
    55  //   ----
    56  //   ----
    57  //   <expected results>
    58  //
    59  //   <more expected results>
    60  //   ----
    61  //   ----
    62  //
    63  // To execute data-driven tests, pass the path of the test file as well as a
    64  // function which can interpret and execute whatever commands are present in
    65  // the test file. The framework invokes the function, passing it information
    66  // about the test case in a TestData struct. The function then returns the
    67  // actual results of the case, which this function compares with the expected
    68  // results, and either succeeds or fails the test.
    69  func RunTest(t *testing.T, path string, f func(d *TestData) string) {
    70  	t.Helper()
    71  	file, err := os.OpenFile(path, os.O_RDWR, 0644 /* irrelevant */)
    72  	if err != nil {
    73  		t.Fatal(err)
    74  	}
    75  	defer func() {
    76  		_ = file.Close()
    77  	}()
    78  
    79  	runTestInternal(t, path, file, f, *rewriteTestFiles)
    80  }
    81  
    82  // RunTestFromString is a version of RunTest which takes the contents of a test
    83  // directly.
    84  func RunTestFromString(t *testing.T, input string, f func(d *TestData) string) {
    85  	t.Helper()
    86  	runTestInternal(t, "<string>" /* optionalPath */, strings.NewReader(input), f, *rewriteTestFiles)
    87  }
    88  
    89  func runTestInternal(
    90  	t *testing.T, sourceName string, reader io.Reader, f func(d *TestData) string, rewrite bool,
    91  ) {
    92  	t.Helper()
    93  
    94  	r := newTestDataReader(t, sourceName, reader, rewrite)
    95  	for r.Next(t) {
    96  		d := &r.data
    97  		actual := func() string {
    98  			defer func() {
    99  				if r := recover(); r != nil {
   100  					fmt.Printf("\npanic during %s:\n%s\n", d.Pos, d.Input)
   101  					panic(r)
   102  				}
   103  			}()
   104  			s := f(d)
   105  			if n := len(s); n > 0 && s[n-1] != '\n' {
   106  				s += "\n"
   107  			}
   108  			return s
   109  		}()
   110  
   111  		if r.rewrite != nil {
   112  			r.emit("----")
   113  			if hasBlankLine(actual) {
   114  				r.emit("----")
   115  				r.rewrite.WriteString(actual)
   116  				r.emit("----")
   117  				r.emit("----")
   118  			} else {
   119  				r.emit(actual)
   120  			}
   121  		} else if d.Expected != actual {
   122  			t.Fatalf("\n%s: %s\nexpected:\n%s\nfound:\n%s", d.Pos, d.Input, d.Expected, actual)
   123  		} else if testing.Verbose() {
   124  			input := d.Input
   125  			if input == "" {
   126  				input = "<no input to command>"
   127  			}
   128  			// TODO(tbg): it's awkward to reproduce the args, but it would be helpful.
   129  			fmt.Printf("\n%s:\n%s [%d args]\n%s\n----\n%s", d.Pos, d.Cmd, len(d.CmdArgs), input, actual)
   130  		}
   131  	}
   132  
   133  	if r.rewrite != nil {
   134  		data := r.rewrite.Bytes()
   135  		if l := len(data); l > 2 && data[l-1] == '\n' && data[l-2] == '\n' {
   136  			data = data[:l-1]
   137  		}
   138  		if dest, ok := reader.(*os.File); ok {
   139  			if _, err := dest.WriteAt(data, 0); err != nil {
   140  				t.Fatal(err)
   141  			}
   142  			if err := dest.Truncate(int64(len(data))); err != nil {
   143  				t.Fatal(err)
   144  			}
   145  			if err := dest.Sync(); err != nil {
   146  				t.Fatal(err)
   147  			}
   148  		} else {
   149  			t.Logf("input is not a file; rewritten output is:\n%s", data)
   150  		}
   151  	}
   152  }
   153  
   154  // Walk goes through all the files in a subdirectory, creating subtests to match
   155  // the file hierarchy; for each "leaf" file, the given function is called.
   156  //
   157  // This can be used in conjunction with RunTest. For example:
   158  //
   159  //    datadriven.Walk(t, path, func (t *testing.T, path string) {
   160  //      // initialize per-test state
   161  //      datadriven.RunTest(t, path, func (d *datadriven.TestData) {
   162  //       // ...
   163  //      }
   164  //    }
   165  //
   166  //   Files:
   167  //     testdata/typing
   168  //     testdata/logprops/scan
   169  //     testdata/logprops/select
   170  //
   171  //   If path is "testdata/typing", the function is called once and no subtests
   172  //   care created.
   173  //
   174  //   If path is "testdata/logprops", the function is called two times, in
   175  //   separate subtests /scan, /select.
   176  //
   177  //   If path is "testdata", the function is called three times, in subtest
   178  //   hierarchy /typing, /logprops/scan, /logprops/select.
   179  //
   180  func Walk(t *testing.T, path string, f func(t *testing.T, path string)) {
   181  	finfo, err := os.Stat(path)
   182  	if err != nil {
   183  		t.Fatal(err)
   184  	}
   185  	if !finfo.IsDir() {
   186  		f(t, path)
   187  		return
   188  	}
   189  	files, err := ioutil.ReadDir(path)
   190  	if err != nil {
   191  		t.Fatal(err)
   192  	}
   193  	for _, file := range files {
   194  		t.Run(file.Name(), func(t *testing.T) {
   195  			Walk(t, filepath.Join(path, file.Name()), f)
   196  		})
   197  	}
   198  }
   199  
   200  // TestData contains information about one data-driven test case that was
   201  // parsed from the test file.
   202  type TestData struct {
   203  	Pos string // reader and line number
   204  
   205  	// Cmd is the first string on the directive line (up to the first whitespace).
   206  	Cmd string
   207  
   208  	CmdArgs []CmdArg
   209  
   210  	Input    string
   211  	Expected string
   212  }
   213  
   214  // ScanArgs looks up the first CmdArg matching the given key and scans it into
   215  // the given destinations in order. If the arg does not exist, the number of
   216  // destinations does not match that of the arguments, or a destination can not
   217  // be populated from its matching value, a fatal error results.
   218  //
   219  // For example, for a TestData originating from
   220  //
   221  // cmd arg1=50 arg2=yoruba arg3=(50, 50, 50)
   222  //
   223  // the following would be valid:
   224  //
   225  // var i1, i2, i3, i4 int
   226  // var s string
   227  // td.ScanArgs(t, "arg1", &i1)
   228  // td.ScanArgs(t, "arg2", &s)
   229  // td.ScanArgs(t, "arg3", &i2, &i3, &i4)
   230  func (td *TestData) ScanArgs(t *testing.T, key string, dests ...interface{}) {
   231  	t.Helper()
   232  	var arg CmdArg
   233  	for i := range td.CmdArgs {
   234  		if td.CmdArgs[i].Key == key {
   235  			arg = td.CmdArgs[i]
   236  			break
   237  		}
   238  	}
   239  	if arg.Key == "" {
   240  		t.Fatalf("missing argument: %s", key)
   241  	}
   242  	if len(dests) != len(arg.Vals) {
   243  		t.Fatalf("%s: got %d destinations, but %d values", arg.Key, len(dests), len(arg.Vals))
   244  	}
   245  
   246  	for i := range dests {
   247  		val := arg.Vals[i]
   248  		switch dest := dests[i].(type) {
   249  		case *string:
   250  			*dest = val
   251  		case *int:
   252  			n, err := strconv.ParseInt(val, 10, 64)
   253  			if err != nil {
   254  				t.Fatal(err)
   255  			}
   256  			*dest = int(n) // assume 64bit ints
   257  		case *uint64:
   258  			n, err := strconv.ParseUint(val, 10, 64)
   259  			if err != nil {
   260  				t.Fatal(err)
   261  			}
   262  			*dest = n
   263  		case *bool:
   264  			b, err := strconv.ParseBool(val)
   265  			if err != nil {
   266  				t.Fatal(err)
   267  			}
   268  			*dest = b
   269  		default:
   270  			t.Fatalf("unsupported type %T for destination #%d (might be easy to add it)", dest, i+1)
   271  		}
   272  	}
   273  }
   274  
   275  // CmdArg contains information about an argument on the directive line. An
   276  // argument is specified in one of the following forms:
   277  //  - argument
   278  //  - argument=value
   279  //  - argument=(values, ...)
   280  type CmdArg struct {
   281  	Key  string
   282  	Vals []string
   283  }
   284  
   285  func (arg CmdArg) String() string {
   286  	switch len(arg.Vals) {
   287  	case 0:
   288  		return arg.Key
   289  
   290  	case 1:
   291  		return fmt.Sprintf("%s=%s", arg.Key, arg.Vals[0])
   292  
   293  	default:
   294  		return fmt.Sprintf("%s=(%s)", arg.Key, strings.Join(arg.Vals, ", "))
   295  	}
   296  }
   297  
   298  // Fatalf wraps a fatal testing error with test file position information, so
   299  // that it's easy to locate the source of the error.
   300  func (td TestData) Fatalf(tb testing.TB, format string, args ...interface{}) {
   301  	tb.Helper()
   302  	tb.Fatalf("%s: %s", td.Pos, fmt.Sprintf(format, args...))
   303  }
   304  
   305  func hasBlankLine(s string) bool {
   306  	scanner := bufio.NewScanner(strings.NewReader(s))
   307  	for scanner.Scan() {
   308  		if strings.TrimSpace(scanner.Text()) == "" {
   309  			return true
   310  		}
   311  	}
   312  	return false
   313  }