Files
sausage/userland/goawk/source/goawk_test.go
T

1040 lines
33 KiB
Go

// 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<ARGC; i++) {
print i, ARGV[i]
}
}`, "a", "b"}, "", "1 a\n2 b\n", ""},
{[]string{`
BEGIN {
for (i=1; i<ARGC; i++) {
print i, ARGV[i]
delete ARGV[i]
}
}
$0`, "a", "b"}, "c\nd", "1 a\n2 b\nc\nd\n", ""},
{[]string{`
BEGIN {
ARGV[1] = ""
}
$0`, "testdata/g.1", "-", "testdata/g.2"}, "c\nd", "c\nd\nTWO\n", ""},
{[]string{`
BEGIN {
ARGC = 3
}
$0`, "testdata/g.1", "-", "testdata/g.2"}, "c\nd", "ONE\nc\nd\n", ""},
{[]string{"-v", "A=1", "-f", "testdata/g.3", "B=2", "testdata/test.countries"}, "",
"A=1, B=0\n\tARGV[1] = B=2\n\tARGV[2] = testdata/test.countries\nA=1, B=2\n", ""},
{[]string{`END { print (x==42) }`, "x=42.0"}, "", "1\n", ""},
{[]string{`END { printf X }`, `X=a\tb`}, "", "a\tb", ""},
{[]string{"-v", "x=42.0", `BEGIN { print (x==42) }`}, "", "1\n", ""},
{[]string{`BEGIN { print(ARGV[1]<2, ARGV[2]<2); ARGV[1]="10"; ARGV[2]="10x"; print(ARGV[1]<2, ARGV[2]<2) }`,
"10", "10x"}, "", "0 1\n1 1\n", ""},
// -E argument
{[]string{"-E", "testdata/awc.awk", "-w", "testdata/awc.awk"}, "", "95 \n", ""},
{[]string{"-Etestdata/awc.awk", "-w", "testdata/awc.awk"}, "", "95 \n", ""},
{[]string{"-E", "testdata/awc.awk", "-w", "--", "testdata/awc.awk"}, "", "95 \n", ""},
{[]string{"-E", "testdata/awc.awk", "--", "-l"}, "", "", `file "-l" not found`},
{[]string{"-E", "testdata/awc.awk", "foo=bar"}, "", "", `file "foo=bar" not found`},
// Error handling
{[]string{}, "", "", "usage: goawk [-F fs] [-v var=value] [-f progfile | 'prog'] [file ...]"},
{[]string{"-F"}, "", "", "flag needs an argument: -F"},
{[]string{"-f"}, "", "", "flag needs an argument: -f"},
{[]string{"-v"}, "", "", "flag needs an argument: -v"},
{[]string{"-z"}, "", "", "flag provided but not defined: -z"},
{[]string{"{ print }", "notexist"}, "", "", `file "notexist" not found`},
{[]string{"BEGIN { print 1/0 }"}, "", "", "division by zero"},
{[]string{"-v", "foo", "BEGIN {}"}, "", "", "-v flag must be in format name=value"},
{[]string{"--", "{ print $1 }", "-file"}, "", "", `file "-file" not found`},
{[]string{"{ print $1 }", "-file"}, "", "", `file "-file" not found`},
// Output synchronization
{[]string{`BEGIN { print "1"; print "2"|"cat" }`}, "", "1\n2\n", ""},
{[]string{`BEGIN { print "1"; "echo 2" | getline x; print x }`}, "", "1\n2\n", ""},
// The "nextfile" statement
{[]string{`{ print FILENAME ":" FNR "/" NR ": " $0 } FNR==2 { nextfile }`,
"testdata/g.5", "-", "testdata/g.6"},
"a\nb\nc\n", `
testdata/g.5:1/1: one
testdata/g.5:2/2: two 2
-:1/3: a
-:2/4: b
testdata/g.6:1/5: Uno
testdata/g.6:2/6: Duo
`[1:], ""},
// Parse error formatting
{[]string{"`"}, "", "", "<cmdline>:1:1: unexpected char\n`\n^"},
{[]string{"BEGIN {\n\tx*;\n}"}, "", "", "<cmdline>:2:4: expected expression instead of ;\n x*;\n ^"},
{[]string{"BEGIN {\n\tx*\r\n}"}, "", "", "<cmdline>:2:4: expected expression instead of <newline>\n x*\n ^"},
{[]string{"-f", "-"}, "\n $", "", "<stdin>:2:3: expected expression instead of <newline>\n $\n ^"},
{[]string{"-f", "testdata/parseerror/good.awk", "-f", "testdata/parseerror/bad.awk"},
"", "", "testdata/parseerror/bad.awk:2:3: expected expression instead of <newline>\nx*\n ^"},
{[]string{"-f", "testdata/parseerror/bad.awk", "-f", "testdata/parseerror/good.awk"},
"", "", "testdata/parseerror/bad.awk:2:3: expected expression instead of <newline>\nx*\n ^"},
{[]string{"-f", "testdata/parseerror/good.awk", "-f", "-", "-f", "testdata/parseerror/bad.awk"},
"`", "", "<stdin>: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")
}