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 }