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  }