// Tests for the New...Execute API. package interp_test import ( "bytes" "context" "errors" "math" "runtime" "strings" "testing" "time" "github.com/benhoyt/goawk/interp" "github.com/benhoyt/goawk/parser" ) // This definitely doesn't test that everything was reset, but it's a good start. func TestNewExecute(t *testing.T) { source := `{ print NR, OFMT, x, y, a["k"], $1, $3; OFMT="%g"; x++; y++; a["k"]++ }` interpreter := newInterp(t, source) // First execution. var output bytes.Buffer status, err := interpreter.Execute(&interp.Config{ Stdin: strings.NewReader("one two three\nfour five six\n"), Output: &output, }) if err != nil { t.Fatalf("error executing: %v", err) } if status != 0 { t.Fatalf("expected status 0, got %d", status) } normalized := normalizeNewlines(output.String()) expected := "1 %.6g one three\n2 %g 1 1 1 four six\n" if normalized != expected { t.Fatalf("expected %q, got %q", expected, normalized) } // Second execution, with ResetVars. output.Reset() interpreter.ResetVars() status, err = interpreter.Execute(&interp.Config{ Stdin: strings.NewReader("ONE TWO THREE\nFOUR FIVE SIX\n"), Output: &output, Vars: []string{"x", "10"}, }) if err != nil { t.Fatalf("error executing: %v", err) } if status != 0 { t.Fatalf("expected status 0, got %d", status) } normalized = normalizeNewlines(output.String()) expected = "1 %.6g 10 ONE THREE\n2 %g 11 1 1 FOUR SIX\n" if normalized != expected { t.Fatalf("expected %q, got %q", expected, normalized) } // Third execution, without ResetVars. output.Reset() status, err = interpreter.Execute(&interp.Config{ Stdin: strings.NewReader("1 2 3\n4 5 6\n"), Output: &output, Vars: []string{"x", "100"}, }) if err != nil { t.Fatalf("error executing: %v", err) } if status != 0 { t.Fatalf("expected status 0, got %d", status) } normalized = normalizeNewlines(output.String()) expected = "1 %g 100 2 2 1 3\n2 %g 101 3 3 4 6\n" if normalized != expected { t.Fatalf("expected %q, got %q", expected, normalized) } } func TestResetRand(t *testing.T) { source := `BEGIN { print rand(), rand(), rand() }` interpreter := newInterp(t, source) var output bytes.Buffer _, err := interpreter.Execute(&interp.Config{Output: &output}) if err != nil { t.Fatalf("error executing: %v", err) } original := output.String() output.Reset() _, err = interpreter.Execute(&interp.Config{Output: &output}) if err != nil { t.Fatalf("error executing: %v", err) } noResetRand := output.String() if original == noResetRand { t.Fatalf("expected different random numbers, got %q both times", original) } output.Reset() interpreter.ResetRand() _, err = interpreter.Execute(&interp.Config{Output: &output}) if err != nil { t.Fatalf("error executing: %v", err) } withResetRand := output.String() if original != withResetRand { t.Fatalf("expected same random numbers (%q) as original (%q)", withResetRand, original) } } func TestGetArrayValue(t *testing.T) { interpreter := newInterp(t, ` BEGIN { Arr["key"]; f(); g(Arr) } function f() { Arr["hello"]="world" } function g(arr) { arr["a"]=1.23 }`) _, err := interpreter.Execute(nil) if err != nil { t.Fatalf("error executing: %v", err) } arr := interpreter.Array("Arr") if len(arr) != 3 { t.Errorf("expected length 3, got %d", len(arr)) } if arr["key"] != "" { t.Errorf("expected value \"\", got %q", arr["key"]) } if arr["hello"] != "world" { t.Errorf("expected value \"world\", got %q", arr["hello"]) } if math.Abs(arr["a"].(float64)-1.23) > 1e-9 { t.Errorf("expected value 1.23, got %f", arr["a"]) } if interpreter.Array("NonExistent") != nil { t.Errorf("non existent name must resolve to nil") } } func TestExecuteContextNoError(t *testing.T) { interpreter := newInterp(t, `BEGIN {}`) _, err := interpreter.ExecuteContext(context.Background(), nil) if err != nil { t.Fatalf("execute error: %v", err) } } func TestExecuteContextTimeout(t *testing.T) { interpreter := newInterp(t, `BEGIN { for (i=0; i<100000000; i++) s+=i }`) // would take about 4s ctx, cancel := context.WithTimeout(context.Background(), 5*time.Millisecond) defer cancel() _, err := interpreter.ExecuteContext(ctx, nil) if !errors.Is(err, context.DeadlineExceeded) { t.Fatalf("expected DeadlineExceeded error, got: %v", err) } } func TestExecuteContextCancel(t *testing.T) { interpreter := newInterp(t, `BEGIN { for (i=0; i<100000000; i++) s+=i }`) // would take about 4s ctx, cancel := context.WithCancel(context.Background()) cancel() // cancel it right away _, err := interpreter.ExecuteContext(ctx, nil) if !errors.Is(err, context.Canceled) { t.Fatalf("expected Canceled error, got: %v", err) } } func TestExecuteContextSystemTimeout(t *testing.T) { start := time.Now() interpreter := newInterp(t, `BEGIN { print system("sleep 1") }`) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Millisecond) defer cancel() _, err := interpreter.ExecuteContext(ctx, nil) switch runtime.GOOS { case "windows", "darwin": // For some reason on Windows and macOS the returned error is nil, rather // than DeadlineExceeded. Don't check it on those platforms. default: if !errors.Is(err, context.DeadlineExceeded) { t.Fatalf("expected DeadlineExceeded error, got: %v", err) } } elapsed := time.Since(start) if elapsed > 500*time.Millisecond { t.Fatalf("should have taken ~5ms, took %v", elapsed) } } func newInterp(t *testing.T, src string) *interp.Interpreter { t.Helper() prog, err := parser.ParseProgram([]byte(src), nil) if err != nil { t.Fatalf("parse error: %v", err) } interpreter, err := interp.New(prog) if err != nil { t.Fatalf("interp.New error: %v", err) } return interpreter }