github.com/benhoyt/goawk@v1.8.1/goawk_test.go (about) 1 // GoAWK tests 2 3 package main_test 4 5 import ( 6 "bytes" 7 "flag" 8 "io" 9 "io/ioutil" 10 "os" 11 "os/exec" 12 "path/filepath" 13 "runtime" 14 "sort" 15 "strings" 16 "sync" 17 "testing" 18 19 "github.com/benhoyt/goawk/interp" 20 "github.com/benhoyt/goawk/parser" 21 ) 22 23 var ( 24 testsDir string 25 outputDir string 26 awkExe string 27 goAWKExe string 28 writeAWK bool 29 writeGoAWK bool 30 ) 31 32 func TestMain(m *testing.M) { 33 flag.StringVar(&testsDir, "testsdir", "./testdata", "directory with one-true-awk tests") 34 flag.StringVar(&outputDir, "outputdir", "./testdata/output", "directory for test output") 35 flag.StringVar(&awkExe, "awk", "gawk", "awk executable name") 36 flag.StringVar(&goAWKExe, "goawk", "./goawk", "goawk executable name") 37 flag.BoolVar(&writeAWK, "writeawk", false, "write expected output") 38 flag.BoolVar(&writeGoAWK, "writegoawk", true, "write Go AWK output") 39 flag.Parse() 40 os.Exit(m.Run()) 41 } 42 43 func TestAWK(t *testing.T) { 44 inputByPrefix := map[string]string{ 45 "t": "test.data", 46 "p": "test.countries", 47 } 48 // These programs exit with non-zero status code 49 errorExits := map[string]bool{ 50 "t.exit": true, 51 "t.exit1": true, 52 "t.gsub4": true, 53 "t.split3": true, 54 } 55 // These programs have known different output 56 knownDifferent := map[string]bool{ 57 "t.printf2": true, // because awk is weird here (our behavior is like mawk) 58 } 59 // Can't really diff test rand() tests as we're using a totally 60 // different algorithm for random numbers 61 randTests := map[string]bool{ 62 "p.48b": true, 63 "t.randk": true, 64 } 65 // These tests use "for (x in a)", which iterates in an undefined 66 // order (according to the spec), so sort lines before comparing. 67 sortLines := map[string]bool{ 68 "p.43": true, 69 "t.in1": true, // because "sort" is locale-dependent 70 "t.in2": true, 71 "t.intest2": true, 72 } 73 dontRunOnWindows := map[string]bool{ 74 "p.50": true, // because this pipes to Unix sort "sort -t: +0 -1 +2nr" 75 "t.printf2": true, // TODO: until we fix discrepancies here 76 } 77 78 infos, err := ioutil.ReadDir(testsDir) 79 if err != nil { 80 t.Fatalf("couldn't read test files: %v", err) 81 } 82 for _, info := range infos { 83 if !strings.HasPrefix(info.Name(), "t.") && !strings.HasPrefix(info.Name(), "p.") { 84 continue 85 } 86 if runtime.GOOS == "windows" && dontRunOnWindows[info.Name()] { 87 continue 88 } 89 t.Run(info.Name(), func(t *testing.T) { 90 srcPath := filepath.Join(testsDir, info.Name()) 91 inputPath := filepath.Join(testsDir, inputByPrefix[info.Name()[:1]]) 92 outputPath := filepath.Join(outputDir, info.Name()) 93 94 cmd := exec.Command(awkExe, "-f", srcPath, inputPath) 95 expected, err := cmd.Output() 96 if err != nil && !errorExits[info.Name()] { 97 t.Fatalf("error running %s: %v", awkExe, err) 98 } 99 expected = bytes.Replace(expected, []byte{0}, []byte("<00>"), -1) 100 expected = normalizeNewlines(expected) 101 if sortLines[info.Name()] { 102 expected = sortedLines(expected) 103 } 104 if writeAWK { 105 err := ioutil.WriteFile(outputPath, expected, 0644) 106 if err != nil { 107 t.Fatalf("error writing awk output: %v", err) 108 } 109 } 110 111 prog, err := parseGoAWK(srcPath) 112 if err != nil { 113 t.Fatal(err) 114 } 115 output, err := interpGoAWK(prog, inputPath) 116 if err != nil && !errorExits[info.Name()] { 117 t.Fatal(err) 118 } 119 output = bytes.Replace(output, []byte{0}, []byte("<00>"), -1) 120 output = normalizeNewlines(output) 121 if randTests[info.Name()] || knownDifferent[info.Name()] { 122 // For tests that use rand(), run them to ensure they 123 // parse and interpret, but can't compare the output, 124 // so stop now 125 return 126 } 127 if sortLines[info.Name()] { 128 output = sortedLines(output) 129 } 130 if writeGoAWK { 131 err := ioutil.WriteFile(outputPath, output, 0644) 132 if err != nil { 133 t.Fatalf("error writing goawk output: %v", err) 134 } 135 } 136 if string(output) != string(expected) { 137 t.Fatalf("output differs, run: git diff %s", outputPath) 138 } 139 }) 140 } 141 142 _ = os.Remove("tempbig") 143 _ = os.Remove("tempsmall") 144 } 145 146 func parseGoAWK(srcPath string) (*parser.Program, error) { 147 src, err := ioutil.ReadFile(srcPath) 148 if err != nil { 149 return nil, err 150 } 151 prog, err := parser.ParseProgram(src, nil) 152 if err != nil { 153 return nil, err 154 } 155 return prog, nil 156 } 157 158 func interpGoAWK(prog *parser.Program, inputPath string) ([]byte, error) { 159 outBuf := &bytes.Buffer{} 160 errBuf := &bytes.Buffer{} 161 config := &interp.Config{ 162 Output: outBuf, 163 Error: &concurrentWriter{w: errBuf}, 164 Args: []string{inputPath}, 165 } 166 _, err := interp.ExecProgram(prog, config) 167 result := outBuf.Bytes() 168 result = append(result, errBuf.Bytes()...) 169 return result, err 170 } 171 172 func interpGoAWKStdin(prog *parser.Program, inputPath string) ([]byte, error) { 173 input, _ := ioutil.ReadFile(inputPath) 174 outBuf := &bytes.Buffer{} 175 errBuf := &bytes.Buffer{} 176 config := &interp.Config{ 177 Stdin: &concurrentReader{r: bytes.NewReader(input)}, 178 Output: outBuf, 179 Error: &concurrentWriter{w: errBuf}, 180 // srcdir is for "redfilnm.awk" 181 Vars: []string{"srcdir", filepath.Dir(inputPath)}, 182 } 183 _, err := interp.ExecProgram(prog, config) 184 result := outBuf.Bytes() 185 result = append(result, errBuf.Bytes()...) 186 return result, err 187 } 188 189 // Wraps a Writer but makes Write calls safe for concurrent use. 190 type concurrentWriter struct { 191 w io.Writer 192 mu sync.Mutex 193 } 194 195 func (w *concurrentWriter) Write(p []byte) (int, error) { 196 w.mu.Lock() 197 defer w.mu.Unlock() 198 return w.w.Write(p) 199 } 200 201 // Wraps a Reader but makes Read calls safe for concurrent use. 202 type concurrentReader struct { 203 r io.Reader 204 mu sync.Mutex 205 } 206 207 func (r *concurrentReader) Read(p []byte) (int, error) { 208 r.mu.Lock() 209 defer r.mu.Unlock() 210 return r.r.Read(p) 211 } 212 213 func sortedLines(data []byte) []byte { 214 trimmed := strings.TrimSuffix(string(data), "\n") 215 lines := strings.Split(trimmed, "\n") 216 sort.Strings(lines) 217 return []byte(strings.Join(lines, "\n") + "\n") 218 } 219 220 func TestGAWK(t *testing.T) { 221 skip := map[string]bool{ // TODO: fix these 222 "inputred": true, // getInputScanner errors 223 224 "getline": true, // getline syntax issues (may be okay, see grammar notes at http://pubs.opengroup.org/onlinepubs/007904975/utilities/awk.html#tag_04_06_13_14) 225 "getline3": true, // getline syntax issues (similar to above) 226 "getline5": true, // getline syntax issues (similar to above) 227 228 "gsubtst7": true, // something wrong with gsub or field split/join 229 "splitwht": true, // other awks handle split(s, a, " ") differently from split(s, a, / /) 230 "status-close": true, // hmmm, not sure what's up here 231 "sigpipe1": true, // probable race condition: sometimes fails, sometimes passes 232 233 "parse1": true, // incorrect parsing of $$a++++ (see TODOs in interp_test.go too) 234 235 "nfldstr": true, // invalid handling of '!$0' when $0="0" 236 "zeroe0": true, // difference in handling of numStr typing when setting $0 and $1 237 } 238 239 dontRunOnWindows := map[string]bool{ 240 "delargv": true, // reads from /dev/null 241 "eofsplit": true, // reads from /etc/passwd 242 "iobug1": true, // reads from /dev/null 243 } 244 245 sortLines := map[string]bool{ 246 "arryref2": true, 247 "delargv": true, 248 "delarpm2": true, 249 "forref": true, 250 } 251 252 gawkDir := filepath.Join(testsDir, "gawk") 253 infos, err := ioutil.ReadDir(gawkDir) 254 if err != nil { 255 t.Fatalf("couldn't read test files: %v", err) 256 } 257 for _, info := range infos { 258 if !strings.HasSuffix(info.Name(), ".awk") { 259 continue 260 } 261 testName := info.Name()[:len(info.Name())-4] 262 if skip[testName] { 263 continue 264 } 265 if runtime.GOOS == "windows" && dontRunOnWindows[testName] { 266 continue 267 } 268 t.Run(testName, func(t *testing.T) { 269 srcPath := filepath.Join(gawkDir, info.Name()) 270 inputPath := filepath.Join(gawkDir, testName+".in") 271 okPath := filepath.Join(gawkDir, testName+".ok") 272 273 expected, err := ioutil.ReadFile(okPath) 274 if err != nil { 275 t.Fatal(err) 276 } 277 expected = normalizeNewlines(expected) 278 279 prog, err := parseGoAWK(srcPath) 280 if err != nil { 281 if err.Error() != string(expected) { 282 t.Fatalf("parser error differs, got:\n%s\nexpected:\n%s", err.Error(), expected) 283 } 284 return 285 } 286 output, err := interpGoAWKStdin(prog, inputPath) 287 output = normalizeNewlines(output) 288 if err != nil { 289 errStr := string(output) + err.Error() 290 if errStr != string(expected) { 291 t.Fatalf("interp error differs, got:\n%s\nexpected:\n%s", errStr, expected) 292 } 293 return 294 } 295 296 if sortLines[testName] { 297 output = sortedLines(output) 298 expected = sortedLines(expected) 299 } 300 301 if string(output) != string(expected) { 302 t.Fatalf("output differs, got:\n%s\nexpected:\n%s", output, expected) 303 } 304 }) 305 } 306 307 _ = os.Remove("seq") 308 } 309 310 func TestCommandLine(t *testing.T) { 311 tests := []struct { 312 args []string 313 stdin string 314 output string 315 error string 316 }{ 317 // Load source from stdin 318 {[]string{"-f", "-"}, `BEGIN { print "b" }`, "b\n", ""}, 319 {[]string{"-f", "-", "-f", "-"}, `BEGIN { print "b" }`, "b\n", ""}, 320 {[]string{"-f-", "-f", "-"}, `BEGIN { print "b" }`, "b\n", ""}, 321 322 // Program with no input 323 {[]string{`BEGIN { print "a" }`}, "", "a\n", ""}, 324 325 // Read input from stdin 326 {[]string{`$0`}, "one\n\nthree", "one\nthree\n", ""}, 327 {[]string{`$0`, "-"}, "one\n\nthree", "one\nthree\n", ""}, 328 {[]string{`$0`, "-", "-"}, "one\n\nthree", "one\nthree\n", ""}, 329 330 // Read input from file(s) 331 {[]string{`$0`, "testdata/g.1"}, "", "ONE\n", ""}, 332 {[]string{`$0`, "testdata/g.1", "testdata/g.2"}, "", "ONE\nTWO\n", ""}, 333 {[]string{`{ print FILENAME ":" FNR "/" NR ": " $0 }`, "testdata/g.1", "testdata/g.4"}, "", 334 "testdata/g.1:1/1: ONE\ntestdata/g.4:1/2: FOUR a\ntestdata/g.4:2/3: FOUR b\n", ""}, 335 {[]string{`$0`, "testdata/g.1", "-", "testdata/g.2"}, "STDIN", "ONE\nSTDIN\nTWO\n", ""}, 336 {[]string{`$0`, "testdata/g.1", "-", "testdata/g.2", "-"}, "STDIN", "ONE\nSTDIN\nTWO\n", ""}, 337 {[]string{"-F", " ", "--", "$0", "testdata/g.1"}, "", "ONE\n", ""}, 338 // I've deleted the "-ftest" file for now as it was causing problems with "go install" zip files 339 // {[]string{"--", "$0", "-ftest"}, "", "used in tests; do not delete\n", ""}, // Issue #53 340 // {[]string{"$0", "-ftest"}, "", "used in tests; do not delete\n", ""}, 341 342 // Specifying field separator with -F 343 {[]string{`{ print $1, $3 }`}, "1 2 3\n4 5 6", "1 3\n4 6\n", ""}, 344 {[]string{"-F", ",", `{ print $1, $3 }`}, "1 2 3\n4 5 6", "1 2 3 \n4 5 6 \n", ""}, 345 {[]string{"-F", ",", `{ print $1, $3 }`}, "1,2,3\n4,5,6", "1 3\n4 6\n", ""}, 346 {[]string{"-F", ",", `{ print $1, $3 }`}, "1,2,3\n4,5,6", "1 3\n4 6\n", ""}, 347 {[]string{"-F,", `{ print $1, $3 }`}, "1,2,3\n4,5,6", "1 3\n4 6\n", ""}, 348 349 // Assigning other variables with -v 350 {[]string{"-v", "OFS=.", `{ print $1, $3 }`}, "1 2 3\n4 5 6", "1.3\n4.6\n", ""}, 351 {[]string{"-v", "OFS=.", "-v", "ORS=", `{ print $1, $3 }`}, "1 2 3\n4 5 6", "1.34.6", ""}, 352 {[]string{"-v", "x=42", "-v", "y=foo", `BEGIN { print x, y }`}, "", "42 foo\n", ""}, 353 {[]string{"-v", "RS=;", `$0`}, "a b;c\nd;e", "a b\nc\nd\ne\n", ""}, 354 {[]string{"-vRS=;", `$0`}, "a b;c\nd;e", "a b\nc\nd\ne\n", ""}, 355 356 // ARGV/ARGC handling 357 {[]string{` 358 BEGIN { 359 for (i=1; i<ARGC; i++) { 360 print i, ARGV[i] 361 } 362 }`, "a", "b"}, "", "1 a\n2 b\n", ""}, 363 {[]string{` 364 BEGIN { 365 for (i=1; i<ARGC; i++) { 366 print i, ARGV[i] 367 delete ARGV[i] 368 } 369 } 370 $0`, "a", "b"}, "c\nd", "1 a\n2 b\nc\nd\n", ""}, 371 {[]string{` 372 BEGIN { 373 ARGV[1] = "" 374 } 375 $0`, "testdata/g.1", "-", "testdata/g.2"}, "c\nd", "c\nd\nTWO\n", ""}, 376 {[]string{` 377 BEGIN { 378 ARGC = 3 379 } 380 $0`, "testdata/g.1", "-", "testdata/g.2"}, "c\nd", "ONE\nc\nd\n", ""}, 381 {[]string{"-v", "A=1", "-f", "testdata/g.3", "B=2", "testdata/test.countries"}, "", 382 "A=1, B=0\n\tARGV[1] = B=2\n\tARGV[2] = testdata/test.countries\nA=1, B=2\n", ""}, 383 {[]string{`END { print (x==42) }`, "x=42.0"}, "", "1\n", ""}, 384 {[]string{"-v", "x=42.0", `BEGIN { print (x==42) }`}, "", "1\n", ""}, 385 386 // Error handling 387 {[]string{}, "", "", "usage: goawk [-F fs] [-v var=value] [-f progfile | 'prog'] [file ...]"}, 388 {[]string{"-F"}, "", "", "flag needs an argument: -F"}, 389 {[]string{"-f"}, "", "", "flag needs an argument: -f"}, 390 {[]string{"-v"}, "", "", "flag needs an argument: -v"}, 391 {[]string{"-z"}, "", "", "flag provided but not defined: -z"}, 392 {[]string{"{ print }", "notexist"}, "", "", `file "notexist" not found`}, 393 {[]string{"@"}, "", "", "-----------------------------------\n@\n^\n-----------------------------------\nparse error at 1:1: unexpected char"}, 394 {[]string{"BEGIN { print 1/0 }"}, "", "", "division by zero"}, 395 {[]string{"-v", "foo", "BEGIN {}"}, "", "", "-v flag must be in format name=value"}, 396 {[]string{"--", "{ print $1 }", "-file"}, "", "", `file "-file" not found`}, 397 {[]string{"{ print $1 }", "-file"}, "", "", `file "-file" not found`}, 398 } 399 for _, test := range tests { 400 testName := strings.Join(test.args, " ") 401 t.Run(testName, func(t *testing.T) { 402 cmd := exec.Command(awkExe, test.args...) 403 if test.stdin != "" { 404 cmd.Stdin = bytes.NewReader([]byte(test.stdin)) 405 } 406 errBuf := &bytes.Buffer{} 407 cmd.Stderr = errBuf 408 output, err := cmd.Output() 409 if err != nil { 410 if test.error == "" { 411 t.Fatalf("expected no error, got AWK error: %v (%s)", err, errBuf.String()) 412 } 413 } else { 414 if test.error != "" { 415 t.Fatalf("expected AWK error, got none") 416 } 417 } 418 stdout := string(normalizeNewlines(output)) 419 if stdout != test.output { 420 t.Fatalf("expected AWK to give %q, got %q", test.output, stdout) 421 } 422 423 stdout, stderr, err := runGoAWK(test.args, test.stdin) 424 if err != nil { 425 stderr = strings.TrimSpace(stderr) 426 if stderr != test.error { 427 t.Fatalf("expected GoAWK error %q, got %q", test.error, stderr) 428 } 429 } else { 430 if test.error != "" { 431 t.Fatalf("expected GoAWK error %q, got none", test.error) 432 } 433 } 434 if stdout != test.output { 435 t.Fatalf("expected GoAWK to give %q, got %q", test.output, stdout) 436 } 437 }) 438 } 439 } 440 441 func runGoAWK(args []string, stdin string) (stdout, stderr string, err error) { 442 cmd := exec.Command(goAWKExe, args...) 443 if stdin != "" { 444 cmd.Stdin = bytes.NewReader([]byte(stdin)) 445 } 446 errBuf := &bytes.Buffer{} 447 cmd.Stderr = errBuf 448 output, err := cmd.Output() 449 stdout = string(normalizeNewlines(output)) 450 stderr = string(normalizeNewlines(errBuf.Bytes())) 451 return stdout, stderr, err 452 } 453 454 func TestWildcards(t *testing.T) { 455 if runtime.GOOS != "windows" { 456 // Wildcards shouldn't be expanded on non-Windows systems, and a file 457 // literally named "*.go" doesn't exist, so expect a failure. 458 _, stderr, err := runGoAWK([]string{"FNR==1 { print FILENAME }", "testdata/wildcards/*.txt"}, "") 459 if err == nil { 460 t.Fatal("expected error using wildcards on non-Windows system") 461 } 462 expected := "file \"testdata/wildcards/*.txt\" not found\n" 463 if stderr != expected { 464 t.Fatalf("expected %q, got %q", expected, stderr) 465 } 466 return 467 } 468 469 tests := []struct { 470 args []string 471 output string 472 }{ 473 { 474 []string{"FNR==1 { print FILENAME }", "testdata/wildcards/*.txt"}, 475 "testdata/wildcards/one.txt\ntestdata/wildcards/two.txt\n", 476 }, 477 { 478 []string{"-f", "testdata/wildcards/*.awk", "testdata/wildcards/one.txt"}, 479 "testdata/wildcards/one.txt\nbee\n", 480 }, 481 { 482 []string{"-f", "testdata/wildcards/*.awk", "testdata/wildcards/*.txt"}, 483 "testdata/wildcards/one.txt\nbee\ntestdata/wildcards/two.txt\nbee\n", 484 }, 485 } 486 487 for _, test := range tests { 488 testName := strings.Join(test.args, " ") 489 t.Run(testName, func(t *testing.T) { 490 stdout, stderr, err := runGoAWK(test.args, "") 491 if err != nil { 492 t.Fatalf("expected no error, got %v (%q)", err, stderr) 493 } 494 stdout = strings.Replace(stdout, "\\", "/", -1) 495 if stdout != test.output { 496 t.Fatalf("expected %q, got %q", test.output, stdout) 497 } 498 }) 499 } 500 } 501 502 func normalizeNewlines(b []byte) []byte { 503 return bytes.Replace(b, []byte("\r\n"), []byte{'\n'}, -1) 504 }