// GoAWK tests package main_test import ( "bufio" "bytes" "flag" "fmt" "io" "os" "os/exec" "path/filepath" "runtime" "sort" "strings" "sync" "testing" "github.com/benhoyt/goawk/interp" "github.com/benhoyt/goawk/parser" ) var ( goExe string testsDir string outputDir string awkExe string goAWKExe string writeAWK bool writeGoAWK bool ) func TestMain(m *testing.M) { flag.StringVar(&goExe, "goexe", "go", "set to override Go executable used to build goawk") flag.StringVar(&testsDir, "testsdir", "./testdata", "directory with one-true-awk tests") flag.StringVar(&outputDir, "outputdir", "./testdata/output", "directory for test output") flag.StringVar(&awkExe, "awk", "gawk", "awk executable name") flag.StringVar(&goAWKExe, "goawk", "./goawk", "goawk executable name") flag.BoolVar(&writeAWK, "writeawk", false, "write expected output") flag.BoolVar(&writeGoAWK, "writegoawk", true, "write Go AWK output") flag.Parse() cmd := exec.Command(goExe, "build", "-ldflags=-w") stderr, err := cmd.CombinedOutput() if err != nil { fmt.Fprintf(os.Stderr, "error building goawk: %v\n%s\n", err, stderr) os.Exit(1) } os.Exit(m.Run()) } func TestAWK(t *testing.T) { inputByPrefix := map[string]string{ "t": "test.data", "p": "test.countries", } // These programs exit with non-zero status code errorExits := map[string]bool{ "t.exit": true, "t.exit1": true, "t.gsub4": true, "t.split3": true, } // These programs have known different output knownDifferent := map[string]bool{ "t.printf2": true, // because awk is weird here (our behavior is like mawk) } // Can't really diff test rand() tests as we're using a totally // different algorithm for random numbers randTests := map[string]bool{ "p.48b": true, "t.randk": true, } // These tests use "for (x in a)", which iterates in an undefined // order (according to the spec), so sort lines before comparing. sortLines := map[string]bool{ "p.43": true, "t.in1": true, // because "sort" is locale-dependent "t.in2": true, "t.intest2": true, } dontRunOnWindows := map[string]bool{ "p.50": true, // because this pipes to Unix sort "sort -t: +0 -1 +2nr" "t.match1": true, // TODO: remove once https://lists.gnu.org/archive/html/bug-gawk/2025-10/msg00072.html is fixed and in choco (presumably 5.3.3) } infos, err := os.ReadDir(testsDir) if err != nil { t.Fatalf("couldn't read test files: %v", err) } for _, info := range infos { if !strings.HasPrefix(info.Name(), "t.") && !strings.HasPrefix(info.Name(), "p.") { continue } if runtime.GOOS == "windows" && dontRunOnWindows[info.Name()] { continue } t.Run(info.Name(), func(t *testing.T) { srcPath := filepath.Join(testsDir, info.Name()) inputPath := filepath.Join(testsDir, inputByPrefix[info.Name()[:1]]) outputPath := filepath.Join(outputDir, info.Name()) cmd := exec.Command(awkExe, "-f", srcPath, inputPath) expected, err := cmd.Output() if err != nil && !errorExits[info.Name()] { t.Fatalf("error running %s: %v", awkExe, err) } expected = bytes.ReplaceAll(expected, []byte{0}, []byte("<00>")) expected = normalizeNewlines(expected) if sortLines[info.Name()] { expected = sortedLines(expected) } if writeAWK { err := os.WriteFile(outputPath, expected, 0644) if err != nil { t.Fatalf("error writing awk output: %v", err) } } prog, err := parseGoAWK(srcPath) if err != nil { t.Fatal(err) } output, err := interpGoAWK(prog, inputPath) if err != nil && !errorExits[info.Name()] { t.Fatal(err) } output = bytes.ReplaceAll(output, []byte{0}, []byte("<00>")) output = normalizeNewlines(output) if randTests[info.Name()] || knownDifferent[info.Name()] { // For tests that use rand(), run them to ensure they // parse and interpret, but can't compare the output, // so stop now return } if sortLines[info.Name()] { output = sortedLines(output) } if writeGoAWK { err := os.WriteFile(outputPath, output, 0644) if err != nil { t.Fatalf("error writing goawk output: %v", err) } } if string(output) != string(expected) { t.Fatalf("output differs, run: git diff %s", outputPath) } }) } _ = os.Remove("tempbig") _ = os.Remove("tempsmall") } func parseGoAWK(srcPath string) (*parser.Program, error) { src, err := os.ReadFile(srcPath) if err != nil { return nil, err } prog, err := parser.ParseProgram(src, nil) if err != nil { return nil, err } return prog, nil } func interpGoAWK(prog *parser.Program, inputPath string) ([]byte, error) { outBuf := &bytes.Buffer{} errBuf := &bytes.Buffer{} config := &interp.Config{ Output: outBuf, Error: &concurrentWriter{w: errBuf}, Args: []string{inputPath}, } _, err := interp.ExecProgram(prog, config) result := outBuf.Bytes() result = append(result, errBuf.Bytes()...) return result, err } func interpGoAWKStdin(prog *parser.Program, inputPath string) ([]byte, error) { input, _ := os.ReadFile(inputPath) outBuf := &bytes.Buffer{} errBuf := &bytes.Buffer{} config := &interp.Config{ Stdin: &concurrentReader{r: bytes.NewReader(input)}, Output: outBuf, Error: &concurrentWriter{w: errBuf}, // srcdir is for "redfilnm.awk" Vars: []string{"srcdir", filepath.Dir(inputPath)}, } _, err := interp.ExecProgram(prog, config) result := outBuf.Bytes() result = append(result, errBuf.Bytes()...) return result, err } // Wraps a Writer but makes Write calls safe for concurrent use. type concurrentWriter struct { w io.Writer mu sync.Mutex } func (w *concurrentWriter) Write(p []byte) (int, error) { w.mu.Lock() defer w.mu.Unlock() return w.w.Write(p) } // Wraps a Reader but makes Read calls safe for concurrent use. type concurrentReader struct { r io.Reader mu sync.Mutex } func (r *concurrentReader) Read(p []byte) (int, error) { r.mu.Lock() defer r.mu.Unlock() return r.r.Read(p) } func sortedLines(data []byte) []byte { trimmed := strings.TrimSuffix(string(data), "\n") lines := strings.Split(trimmed, "\n") sort.Strings(lines) return []byte(strings.Join(lines, "\n") + "\n") } func TestGAWK(t *testing.T) { skip := map[string]bool{ // TODO: fix these (at least the ones that are bugs) "gsubtst7": true, // something wrong with gsub or field split/join "splitwht": true, // other awks handle split(s, a, " ") differently from split(s, a, / /) "rscompat": true, // GoAWK allows multi-char RS by default "rsstart2": true, // GoAWK ^ and $ anchors match beginning and end of line, not file (unlike Gawk) "hex2": true, // GoAWK allows hex numbers / floating point (per POSIX) "strtod": true, // GoAWK allows hex numbers / floating point (per POSIX) } dontRunOnWindows := map[string]bool{ "delargv": true, // reads from /dev/null "eofsplit": true, // reads from /etc/passwd "getline5": true, // removes a file while it's open "iobug1": true, // reads from /dev/null "sigpipe1": true, // requires POSIX signals } sortLines := map[string]bool{ "arryref2": true, "delargv": true, "delarpm2": true, "forref": true, } gawkDir := filepath.Join(testsDir, "gawk") infos, err := os.ReadDir(gawkDir) if err != nil { t.Fatalf("couldn't read test files: %v", err) } for _, info := range infos { if !strings.HasSuffix(info.Name(), ".awk") { continue } testName := info.Name()[:len(info.Name())-4] if skip[testName] { continue } if runtime.GOOS == "windows" && dontRunOnWindows[testName] { continue } t.Run(testName, func(t *testing.T) { srcPath := filepath.Join(gawkDir, info.Name()) inputPath := filepath.Join(gawkDir, testName+".in") okPath := filepath.Join(gawkDir, testName+".ok") expected, err := os.ReadFile(okPath) if err != nil { t.Fatal(err) } expected = normalizeNewlines(expected) prog, err := parseGoAWK(srcPath) if err != nil { if err.Error() != string(expected) { t.Fatalf("parser error differs, got:\n%s\nexpected:\n%s", err.Error(), expected) } return } output, err := interpGoAWKStdin(prog, inputPath) output = normalizeNewlines(output) if err != nil { errStr := string(output) + err.Error() if errStr != string(expected) { t.Fatalf("interp error differs, got:\n%s\nexpected:\n%s", errStr, expected) } return } if sortLines[testName] { output = sortedLines(output) expected = sortedLines(expected) } if string(output) != string(expected) { t.Fatalf("output differs, got:\n%s\nexpected:\n%s", output, expected) } }) } _ = os.Remove("seq") } func TestCommandLine(t *testing.T) { tests := []struct { args []string stdin string output string error string }{ // Load source from stdin {[]string{"-f", "-"}, `BEGIN { print "b" }`, "b\n", ""}, {[]string{"-f", "-", "-f", "-"}, `BEGIN { print "b" }`, "b\n", ""}, {[]string{"-f-", "-f", "-"}, `BEGIN { print "b" }`, "b\n", ""}, // Program with no input {[]string{`BEGIN { print "a" }`}, "", "a\n", ""}, // Read input from stdin {[]string{`$0`}, "one\n\nthree", "one\nthree\n", ""}, {[]string{`$0`, "-"}, "one\n\nthree", "one\nthree\n", ""}, {[]string{`$0`, "-", "-"}, "one\n\nthree", "one\nthree\n", ""}, {[]string{"-f", "testdata/t.0", "-"}, "one\ntwo\n", "one\ntwo\n", ""}, {[]string{"{ print FILENAME }"}, "a", "-\n", ""}, {[]string{"{ print FILENAME }", "-"}, "a", "-\n", ""}, // Read input from file(s) {[]string{`$0`, "testdata/g.1"}, "", "ONE\n", ""}, {[]string{`$0`, "testdata/g.1", "testdata/g.2"}, "", "ONE\nTWO\n", ""}, {[]string{`{ print FILENAME ":" FNR "/" NR ": " $0 }`, "testdata/g.1", "testdata/g.4"}, "", "testdata/g.1:1/1: ONE\ntestdata/g.4:1/2: FOUR a\ntestdata/g.4:2/3: FOUR b\n", ""}, {[]string{`$0`, "testdata/g.1", "-", "testdata/g.2"}, "STDIN", "ONE\nSTDIN\nTWO\n", ""}, {[]string{`$0`, "testdata/g.1", "-", "testdata/g.2", "-"}, "STDIN", "ONE\nSTDIN\nTWO\n", ""}, {[]string{"-F", " ", "--", "$0", "testdata/g.1"}, "", "ONE\n", ""}, {[]string{"{ print NR, FNR } END { print NR, FNR }", "-"}, "a\nb\nc\n", "1 1\n2 2\n3 3\n3 3\n", ""}, // I've deleted the "-ftest" file for now as it was causing problems with "go install" zip files // {[]string{"--", "$0", "-ftest"}, "", "used in tests; do not delete\n", ""}, // Issue #53 // {[]string{"$0", "-ftest"}, "", "used in tests; do not delete\n", ""}, // Specifying field separator with -F {[]string{`{ print $1, $3 }`}, "1 2 3\n4 5 6", "1 3\n4 6\n", ""}, {[]string{"-F", ",", `{ print $1, $3 }`}, "1 2 3\n4 5 6", "1 2 3 \n4 5 6 \n", ""}, {[]string{"-F", ",", `{ print $1, $3 }`}, "1,2,3\n4,5,6", "1 3\n4 6\n", ""}, {[]string{"-F", ",", `{ print $1, $3 }`}, "1,2,3\n4,5,6", "1 3\n4 6\n", ""}, {[]string{"-F,", `{ print $1, $3 }`}, "1,2,3\n4,5,6", "1 3\n4 6\n", ""}, // Assigning other variables with -v {[]string{"-v", "OFS=.", `{ print $1, $3 }`}, "1 2 3\n4 5 6", "1.3\n4.6\n", ""}, {[]string{"-v", "OFS=.", "-v", "ORS=", `{ print $1, $3 }`}, "1 2 3\n4 5 6", "1.34.6", ""}, {[]string{"-v", "x=42", "-v", "y=foo", `BEGIN { print x, y }`}, "", "42 foo\n", ""}, {[]string{"-v", "RS=;", `$0`}, "a b;c\nd;e", "a b\nc\nd\ne\n", ""}, {[]string{"-vRS=;", `$0`}, "a b;c\nd;e", "a b\nc\nd\ne\n", ""}, {[]string{"-v", `X=x\ty`, `BEGIN { printf X }`}, "", "x\ty", ""}, // ARGV/ARGC handling {[]string{` BEGIN { for (i=1; i:1:1: unexpected char\n`\n^"}, {[]string{"BEGIN {\n\tx*;\n}"}, "", "", ":2:4: expected expression instead of ;\n x*;\n ^"}, {[]string{"BEGIN {\n\tx*\r\n}"}, "", "", ":2:4: expected expression instead of \n x*\n ^"}, {[]string{"-f", "-"}, "\n $", "", ":2:3: expected expression instead of \n $\n ^"}, {[]string{"-f", "testdata/parseerror/good.awk", "-f", "testdata/parseerror/bad.awk"}, "", "", "testdata/parseerror/bad.awk:2:3: expected expression instead of \nx*\n ^"}, {[]string{"-f", "testdata/parseerror/bad.awk", "-f", "testdata/parseerror/good.awk"}, "", "", "testdata/parseerror/bad.awk:2:3: expected expression instead of \nx*\n ^"}, {[]string{"-f", "testdata/parseerror/good.awk", "-f", "-", "-f", "testdata/parseerror/bad.awk"}, "`", "", ":1:1: unexpected char\n`\n^"}, // Other programs {[]string{"-f", "testdata/other/gron.awk"}, `{"foo":42}`, "json={}\njson.foo=42\n", ""}, } for _, test := range tests { testName := strings.Join(test.args, " ") t.Run(testName, func(t *testing.T) { runAWKs(t, test.args, test.stdin, test.output, test.error) }) } } func TestDevStdout(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("/dev/stdout not present on Windows") } runAWKs(t, []string{`BEGIN { print "1"; print "2">"/dev/stdout" }`}, "", "1\n2\n", "") } func runGoAWKRaw(args []string, stdin string) (stdout, stderr string, err error) { cmd := exec.Command(goAWKExe, args...) if stdin != "" { cmd.Stdin = strings.NewReader(stdin) } errBuf := &bytes.Buffer{} cmd.Stderr = errBuf output, err := cmd.Output() stdout = string(output) stderr = errBuf.String() return stdout, stderr, err } func runGoAWK(args []string, stdin string) (stdout, stderr string, err error) { stdout, stderr, err = runGoAWKRaw(args, stdin) stdout = string(normalizeNewlines([]byte(stdout))) stderr = string(normalizeNewlines([]byte(stderr))) return stdout, stderr, err } func runAWKs(t *testing.T, testArgs []string, testStdin, testOutput, testError string) { t.Helper() t.Run("awk", func(t *testing.T) { t.Helper() cmd := exec.Command(awkExe, testArgs...) if runtime.GOOS == "darwin" { cmd.Env = []string{"LC_ALL=en_US.UTF-8"} } else if runtime.GOOS != "windows" { cmd.Env = []string{"LC_ALL=C.UTF-8"} } if testStdin != "" { cmd.Stdin = strings.NewReader(testStdin) } errBuf := &bytes.Buffer{} cmd.Stderr = errBuf output, err := cmd.Output() if err != nil { if testError == "" { t.Fatalf("expected no error, got AWK error: %v (%s)", err, errBuf.String()) } } else { if testError != "" { t.Fatalf("expected AWK error, got none") } } stdout := string(normalizeNewlines(output)) if stdout != testOutput { t.Fatalf("AWK expected/got:\n%q\n%q", testOutput, stdout) } }) t.Run("goawk", func(t *testing.T) { t.Helper() stdout, stderr, err := runGoAWK(testArgs, testStdin) if err != nil { stderr = strings.TrimSpace(stderr) if stderr != testError { t.Fatalf("expected GoAWK error %q, got %q", testError, stderr) } } else { if testError != "" { t.Fatalf("expected GoAWK error %q, got none", testError) } } if stdout != testOutput { t.Fatalf("GoAWK expected/got:\n%q\n%q", testOutput, stdout) } }) } func TestWildcards(t *testing.T) { if runtime.GOOS != "windows" { // Wildcards shouldn't be expanded on non-Windows systems, and a file // literally named "*.go" doesn't exist, so expect a failure. _, stderr, err := runGoAWK([]string{"FNR==1 { print FILENAME }", "testdata/wildcards/*.txt"}, "") if err == nil { t.Fatal("expected error using wildcards on non-Windows system") } expected := "file \"testdata/wildcards/*.txt\" not found\n" if stderr != expected { t.Fatalf("expected %q, got %q", expected, stderr) } return } tests := []struct { args []string output string }{ { []string{"FNR==1 { print FILENAME }", "testdata/wildcards/*.txt"}, "testdata/wildcards/one.txt\ntestdata/wildcards/two.txt\n", }, { []string{"-f", "testdata/wildcards/*.awk", "testdata/wildcards/one.txt"}, "testdata/wildcards/one.txt\nbee\n", }, { []string{"-f", "testdata/wildcards/*.awk", "testdata/wildcards/*.txt"}, "testdata/wildcards/one.txt\nbee\ntestdata/wildcards/two.txt\nbee\n", }, } for _, test := range tests { testName := strings.Join(test.args, " ") t.Run(testName, func(t *testing.T) { stdout, stderr, err := runGoAWK(test.args, "") if err != nil { t.Fatalf("expected no error, got %v (%q)", err, stderr) } stdout = strings.ReplaceAll(stdout, "\\", "/") if stdout != test.output { t.Fatalf("expected %q, got %q", test.output, stdout) } }) } } func TestFILENAME(t *testing.T) { origGoAWKExe := goAWKExe goAWKExe = "../../" + goAWKExe defer func() { goAWKExe = origGoAWKExe }() origDir, err := os.Getwd() if err != nil { t.Fatal(err) } err = os.Chdir("testdata/filename") if err != nil { t.Fatal(err) } defer os.Chdir(origDir) src := ` BEGIN { FILENAME = "10"; print(FILENAME, FILENAME<2) } BEGIN { FILENAME = 10; print(FILENAME, FILENAME<2) } { print(FILENAME, FILENAME<2) } ` runAWKs(t, []string{src, "10", "10x"}, "", "10 1\n10 0\n10 0\n10x 1\n", "") } func normalizeNewlines(b []byte) []byte { return bytes.ReplaceAll(b, []byte("\r\n"), []byte{'\n'}) } func TestGoAWKSpecificOptions(t *testing.T) { tests := []struct { args []string input string output string error string }{ // CSV-related options {[]string{"-icsv", "-H", `{ print @"age", @"name" }`}, "name,age\nBob,42\nJane,37", "42 Bob\n37 Jane\n", ""}, {[]string{"-i", "csv", "-H", `{ print @"age", @"name" }`}, "name,age\nBob,42\nJane,37", "42 Bob\n37 Jane\n", ""}, {[]string{"-icsv", `{ print $2, $1 }`}, "Bob,42\nJane,37", "42 Bob\n37 Jane\n", ""}, {[]string{"-i", "csv", `{ print $2, $1 }`}, "Bob,42\nJane,37", "42 Bob\n37 Jane\n", ""}, {[]string{"-icsv", "-H", "-ocsv", `{ print @"age", @"name" }`}, "name,age\n\"Bo,ba\",42\nJane,37", "42,\"Bo,ba\"\n37,Jane\n", ""}, {[]string{"-csv", `{ print $2, $1 }`}, "Bob,42\nJane,37", "42 Bob\n37 Jane\n", ""}, {[]string{"--csv", `{ print $2, $1 }`}, "Bob,42\nJane,37", "42 Bob\n37 Jane\n", ""}, {[]string{"-o", "csv", `BEGIN { print "foo,bar", 3.14, "baz" }`}, "", "\"foo,bar\",3.14,baz\n", ""}, {[]string{"-iabc", `{}`}, "", "", "invalid input mode \"abc\"\n"}, {[]string{"-oxyz", `{}`}, "", "", "invalid output mode \"xyz\"\n"}, {[]string{"-H", `{}`}, "", "", "-H only allowed together with -i\n"}, // Chars mode (vs bytes) {[]string{"-c", `BEGIN { printf "%c", 4660 }`}, "", "\u1234", ""}, {[]string{`BEGIN { printf "%c", 4660 }`}, "", "4", ""}, {[]string{"-c", `{ print length }`}, "絵\n", "1\n", ""}, {[]string{`{ print length }`}, "絵\n", "3\n", ""}, // Debug options {[]string{"-dt", ` BEGIN { x=42; a[1]=x; print f(a, 1) } function f(b, y, z) { return b[y+z] } function hi(x) { print "hi " x } `}, "", ` globals ARGV: array 0 ENVIRON: array 1 FIELDS: array 2 a: array 3 x: scalar 0 function f(b, y, z) # index 0 b: array 0 y: scalar 0 z: scalar 1 function hi(x) # index 1 x: scalar 0 `[1:], ""}, {[]string{"-d", `$1 { print 1+1 }`}, "", ` $1 { print 1 + 1 } `[1:], ""}, {[]string{"-da", `$1 { print 1+1 }`}, "", ` // pattern 0000 FieldInt 1 // { body } 0000 Num 1 (0) 0002 Num 1 (0) 0004 Add 0005 Print 1 `[1:], ""}, {[]string{"-d", "-da", `$1 { print 1+1 }`}, "", ` $1 { print 1 + 1 } // pattern 0000 FieldInt 1 // { body } 0000 Num 1 (0) 0002 Num 1 (0) 0004 Add 0005 Print 1 `[1:], ""}, } for _, test := range tests { testName := strings.Join(test.args, " ") t.Run(testName, func(t *testing.T) { stdout, stderr, err := runGoAWK(test.args, test.input) if err != nil { if test.error == "" { t.Fatalf("expected no error, got %v (%q)", err, stderr) } else if stderr != test.error { t.Fatalf("expected error message %q, got %q", test.error, stderr) } } if stdout != test.output { t.Fatalf("output differs, got:\n%s\nexpected:\n%s", stdout, test.output) } }) } } func TestNewlineOutputMode(t *testing.T) { nl := "\n" if runtime.GOOS == "windows" { nl = "\r\n" } tests := []struct { args []string input string output string error string }{ {[]string{`BEGIN { printf 'foo\nbar\n' }`}, "", "foo" + nl + "bar" + nl, ""}, {[]string{"-N", "smart", `BEGIN { printf 'foo\nbar\n' }`}, "", "foo" + nl + "bar" + nl, ""}, {[]string{"-Nsmart", `BEGIN { printf 'foo\nbar\n' }`}, "", "foo" + nl + "bar" + nl, ""}, {[]string{"-N", "raw", `BEGIN { printf 'foo\nbar\n' }`}, "", "foo\nbar\n", ""}, {[]string{"-Nraw", `BEGIN { printf 'foo\nbar\n' }`}, "", "foo\nbar\n", ""}, {[]string{"-N", "crlf", `BEGIN { printf 'foo\nbar\n' }`}, "", "foo\r\nbar\r\n", ""}, {[]string{"-Ncrlf", `BEGIN { printf 'foo\nbar\n' }`}, "", "foo\r\nbar\r\n", ""}, {[]string{"-N", "INVALID", `BEGIN { printf 'foo\nbar\n' }`}, "", "", "-N arg can only be one of: smart, raw, crlf\n"}, {[]string{"-NINVALID", `BEGIN { printf 'foo\nbar\n' }`}, "", "", "-N arg can only be one of: smart, raw, crlf\n"}, {[]string{"-o", "csv", "-Nraw", `BEGIN { print 1, " 2 " }`}, "", "1,\" 2 \"\n", ""}, {[]string{"-o", "csv", "-Ncrlf", `BEGIN { print 1, " 2 " }`}, "", "1,\" 2 \"\r\n", ""}, } for _, test := range tests { testName := strings.Join(test.args, " ") t.Run(testName, func(t *testing.T) { stdout, stderr, err := runGoAWKRaw(test.args, test.input) if err != nil { if test.error == "" { t.Fatalf("expected no error, got %v (%q)", err, stderr) } else if stderr != test.error { t.Fatalf("expected error message %q, got %q", test.error, stderr) } } if stdout != test.output { t.Fatalf("output differs, got:\n%s\nexpected:\n%s", stdout, test.output) } }) } } func TestMultipleCSVFiles(t *testing.T) { // Ensure CSV handling works across multiple files with different headers (field names). src := ` { for (i=1; i in FIELDS; i++) { if (i>1) printf ","; printf "%s", FIELDS[i] } printf " " } { print @"name", @"age" } ` stdout, stderr, err := runGoAWK([]string{"-i", "csv", "-H", src, "testdata/csv/1.csv", "testdata/csv/2.csv"}, "") if err != nil { t.Fatalf("expected no error, got %v (%q)", err, stderr) } expected := ` name,age Bob 42 name,age Jill 37 age,email,name Sarah 25 `[1:] if stdout != expected { t.Fatalf("expected %q, got %q", expected, stdout) } } func TestCSVDocExamples(t *testing.T) { f, err := os.Open("docs/csv.md") if err != nil { t.Fatalf("error opening examples file: %v", err) } defer f.Close() var ( command string output string truncated bool n = 1 ) runTest := func() { t.Run(fmt.Sprintf("Example%d", n), func(t *testing.T) { shell := "/bin/sh" if runtime.GOOS == "windows" { shell = "sh" } cmd := exec.Command(shell, "-c", command) gotBytes, err := cmd.CombinedOutput() if err != nil { t.Fatalf("error running %q: %v\n%s", command, err, gotBytes) } got := string(gotBytes) if truncated { numLines := strings.Count(output, "\n") got = strings.Join(strings.Split(got, "\n")[:numLines], "\n") + "\n" } got = string(normalizeNewlines([]byte(got))) if got != output { t.Fatalf("error running %q\ngot:\n%s\nexpected:\n%s", command, got, output) } }) n++ } scanner := bufio.NewScanner(f) inTest := false for scanner.Scan() { line := scanner.Text() if strings.HasPrefix(line, "$ goawk") { if inTest { runTest() } inTest = true command = "./" + line[2:] output = "" truncated = false } else if inTest { switch line { case "```", "": runTest() inTest = false case "...": truncated = true runTest() inTest = false default: output += line + "\n" } } } if scanner.Err() != nil { t.Errorf("error reading input: %v", scanner.Err()) } if inTest { t.Error("unexpectedly in test at end of file") } } func TestMandelbrot(t *testing.T) { stdout, stderr, err := runGoAWK([]string{"-v", "width=80", "-v", "height=25", "-f", "testdata/tt.x1_mandelbrot"}, "") if err != nil { t.Fatalf("expected no error, got %v (%q)", err, stderr) } expected := ` ................................................................................ ......................................................--+-----.................. ....................................................-----+*+-++-................ .................................................--------+* *+-----............. ..............................................--------+# #%*-------......... .........................................------------++$ +-----------..... ...................................---------* # +* # *+++++%+--... ............................----------------++ @ *----.. .......................-+----------------+$ %+----.. ..................-------*++%++**+++---++ #+--. ...............----------+* #*++* %*---. .............-------+++++* # #----. ....------+-------++**@ @ ------. ....------+-------++**@ @ ------. .............-------+++++* # #----. ...............----------+* #*++* %*---. ..................-------*++%++**+++---++ #+--. .......................-+----------------+$ %+----.. ............................----------------++ @ *----.. ...................................---------* # +* # *+++++%+--... .........................................------------++$ +-----------..... ..............................................--------+# #%*-------......... .................................................--------+* *+-----............. ....................................................-----+*+-++-................ ......................................................--+-----.................. `[1:] if stdout != expected { t.Fatalf("expected:\n%s\ngot:\n%s", expected, stdout) } } func TestCoverPrintAnnotatedSource(t *testing.T) { tests := []struct { sourceFiles []string mode string expectedResultFile string }{ {[]string{"a1.awk"}, "set", "a1_covermode_set.awk"}, {[]string{"a2.awk"}, "count", "a2_covermode_count.awk"}, {[]string{"a1.awk", "a2.awk"}, "count", "a1_a2_covermode_count.awk"}, {[]string{"a3.awk"}, "set", "a3_covermode_set.awk"}, } for _, test := range tests { t.Run(test.expectedResultFile, func(t *testing.T) { args := []string{"-d", "-covermode", test.mode} for _, file := range test.sourceFiles { args = append(args, "-f", "testdata/cover/"+file) } stdout, stderr, err := runGoAWK(args, "") if err != nil { t.Fatalf("expected no error, got %v (%q)", err, stderr) } expected, err := os.ReadFile("testdata/cover/" + test.expectedResultFile) if err != nil { t.Fatal(err) } if normalizeAwkSource(stdout) != normalizeAwkSource(string(expected)) { t.Fatalf("output differs, got:\n%s\nexpected:\n%s", stdout, expected) } }) } } func normalizeAwkSource(source string) string { p, err := parser.ParseProgram([]byte(source), nil) if err != nil { panic(err) } return p.String() } func TestCoverInvalidArgs(t *testing.T) { tests := []struct { args []string expectedError string }{ {[]string{"-coverprofile"}, "flag needs an argument: -coverprofile"}, {[]string{"-covermode"}, "flag needs an argument: -covermode"}, {[]string{"-covermode", "wrong"}, "-covermode can only be one of: set, count"}, {[]string{"-covermode=wrong"}, "-covermode can only be one of: set, count"}, } for _, test := range tests { t.Run(test.expectedError, func(t *testing.T) { _, stderr, err := runGoAWK(test.args, "") if err == nil { t.Fatalf("expected error") } stderr = strings.TrimSpace(stderr) if stderr != test.expectedError { t.Fatalf("error differs, got:\n%s\nexpected:\n%s", stderr, test.expectedError) } }) } } func TestCover(t *testing.T) { tests := []struct { mode string coverAppend bool runs [][]string expectedCoverReport string }{ {"set", true, [][]string{{"a1.awk"}}, "test_set.cov"}, {"count", true, [][]string{{"a1.awk"}}, "test_count.cov"}, {"set", true, [][]string{{"a2.awk", "a1.awk"}}, "test_a2a1_set.cov"}, {"count", true, [][]string{{"a2.awk", "a1.awk"}}, "test_a2a1_count.cov"}, {"set", true, [][]string{{"a1.awk"}, {"a1.awk"}}, "test_1file2runs_set.cov"}, {"count", true, [][]string{{"a2.awk", "a1.awk"}, {"a2.awk", "a1.awk"}}, "test_2file2runs_count.cov"}, {"set", false, [][]string{{"a1.awk"}, {"a1.awk"}}, "test_1file2runs_set_truncated.cov"}, {"count", false, [][]string{{"a2.awk", "a1.awk"}, {"a2.awk", "a1.awk"}}, "test_2file2runs_count_truncated.cov"}, } for _, test := range tests { t.Run(test.expectedCoverReport, func(t *testing.T) { tempFile, err := os.CreateTemp("", "testCov*.txt") if err != nil { t.Fatalf("%v", err) } err = tempFile.Close() if err != nil { t.Fatalf("%v", err) } coverProfile := tempFile.Name() defer os.Remove(coverProfile) // make sure file doesn't exist - delete the temp file first, but use its name err = os.Remove(coverProfile) if err != nil { t.Fatalf("%v", err) } for _, run := range test.runs { var args []string for _, file := range run { args = append(args, "-f", "testdata/cover/"+file) } args = append(args, "-coverprofile", coverProfile) args = append(args, "-covermode", test.mode) if test.coverAppend { args = append(args, "-coverappend") } _, _, err := runGoAWK(args, "") if err != nil { t.Fatalf("%v", err) } } result, err := os.ReadFile(coverProfile) if err != nil { t.Fatalf("%v", err) } resultStr := string(normalizeNewlines(result)) resultStr = strings.TrimSpace(convertPathsToFilenames(t, resultStr)) expected, err := os.ReadFile("testdata/cover/" + test.expectedCoverReport) if err != nil { t.Fatalf("%v", err) } expectedStr := strings.TrimSpace(string(normalizeNewlines(expected))) if resultStr != expectedStr { t.Fatalf("wrong coverage report, expected:\n\n%s\n\nactual:\n\n%s", expectedStr, resultStr) } }) } } func convertPathsToFilenames(t *testing.T, str string) string { lines := strings.Split(str, "\n") for i, line := range lines { if i == 0 { continue // skip mode line } if line == "" { continue // skip empty line } parts := strings.Split(line, ":") // on win line can be `D:\a\goawk\goawk\testdata\cover\a1.awk:2.3,6.30 5 1` path := strings.Join(parts[:len(parts)-1], ":") data := parts[len(parts)-1] if !filepath.IsAbs(path) { t.Fatalf("must be absolute path in coverage report: %s", line) } // leave only the part with name lines[i] = filepath.Base(path) + ":" + data } return strings.Join(lines, "\n") }