|
1 | 1 | package interp_test
|
2 | 2 |
|
3 | 3 | import (
|
| 4 | + "bufio" |
4 | 5 | "bytes"
|
5 | 6 | "context"
|
6 | 7 | "fmt"
|
@@ -917,6 +918,199 @@ func TestImportPathIsKey(t *testing.T) {
|
917 | 918 | }
|
918 | 919 | }
|
919 | 920 |
|
| 921 | +// The code in hello1.go and hello2.go spawns a "long-running" goroutine, which |
| 922 | +// means each call to EvalPath actually terminates before the evaled code is done |
| 923 | +// running. So this test demonstrates: |
| 924 | +// 1) That two sequential calls to EvalPath don't see their "compilation phases" |
| 925 | +// collide (no data race on the fields of the interpreter), which is somewhat |
| 926 | +// obvious since the calls (and hence the "compilation phases") are sequential too. |
| 927 | +// 2) That two concurrent goroutine runs spawned by the same interpreter do not |
| 928 | +// collide either. |
| 929 | +func TestConcurrentEvals(t *testing.T) { |
| 930 | + if testing.Short() { |
| 931 | + return |
| 932 | + } |
| 933 | + pin, pout := io.Pipe() |
| 934 | + defer func() { |
| 935 | + _ = pin.Close() |
| 936 | + _ = pout.Close() |
| 937 | + }() |
| 938 | + interpr := interp.New(interp.Options{Stdout: pout}) |
| 939 | + interpr.Use(stdlib.Symbols) |
| 940 | + |
| 941 | + if _, err := interpr.EvalPath("testdata/concurrent/hello1.go"); err != nil { |
| 942 | + t.Fatal(err) |
| 943 | + } |
| 944 | + if _, err := interpr.EvalPath("testdata/concurrent/hello2.go"); err != nil { |
| 945 | + t.Fatal(err) |
| 946 | + } |
| 947 | + |
| 948 | + c := make(chan error) |
| 949 | + go func() { |
| 950 | + hello1, hello2 := false, false |
| 951 | + sc := bufio.NewScanner(pin) |
| 952 | + for sc.Scan() { |
| 953 | + l := sc.Text() |
| 954 | + switch l { |
| 955 | + case "hello world1": |
| 956 | + hello1 = true |
| 957 | + case "hello world2": |
| 958 | + hello2 = true |
| 959 | + default: |
| 960 | + c <- fmt.Errorf("unexpected output: %v", l) |
| 961 | + return |
| 962 | + } |
| 963 | + if hello1 && hello2 { |
| 964 | + break |
| 965 | + } |
| 966 | + } |
| 967 | + c <- nil |
| 968 | + }() |
| 969 | + |
| 970 | + timeout := time.NewTimer(5 * time.Second) |
| 971 | + select { |
| 972 | + case <-timeout.C: |
| 973 | + t.Fatal("timeout") |
| 974 | + case err := <-c: |
| 975 | + if err != nil { |
| 976 | + t.Fatal(err) |
| 977 | + } |
| 978 | + } |
| 979 | +} |
| 980 | + |
| 981 | +// TestConcurrentEvals2 shows that even though EvalWithContext calls Eval in a |
| 982 | +// goroutine, it indeed waits for Eval to terminate, and that therefore the code |
| 983 | +// called by EvalWithContext is sequential. And that there is no data race for the |
| 984 | +// interp package global vars or the interpreter fields in this case. |
| 985 | +func TestConcurrentEvals2(t *testing.T) { |
| 986 | + pin, pout := io.Pipe() |
| 987 | + defer func() { |
| 988 | + _ = pin.Close() |
| 989 | + _ = pout.Close() |
| 990 | + }() |
| 991 | + interpr := interp.New(interp.Options{Stdout: pout}) |
| 992 | + interpr.Use(stdlib.Symbols) |
| 993 | + |
| 994 | + done := make(chan error) |
| 995 | + go func() { |
| 996 | + hello1 := false |
| 997 | + sc := bufio.NewScanner(pin) |
| 998 | + for sc.Scan() { |
| 999 | + l := sc.Text() |
| 1000 | + if hello1 { |
| 1001 | + if l == "hello world2" { |
| 1002 | + break |
| 1003 | + } else { |
| 1004 | + done <- fmt.Errorf("unexpected output: %v", l) |
| 1005 | + return |
| 1006 | + } |
| 1007 | + } |
| 1008 | + if l == "hello world1" { |
| 1009 | + hello1 = true |
| 1010 | + } else { |
| 1011 | + done <- fmt.Errorf("unexpected output: %v", l) |
| 1012 | + return |
| 1013 | + } |
| 1014 | + } |
| 1015 | + done <- nil |
| 1016 | + }() |
| 1017 | + |
| 1018 | + ctx := context.Background() |
| 1019 | + if _, err := interpr.EvalWithContext(ctx, `import "time"`); err != nil { |
| 1020 | + t.Fatal(err) |
| 1021 | + } |
| 1022 | + if _, err := interpr.EvalWithContext(ctx, `time.Sleep(time.Second); println("hello world1")`); err != nil { |
| 1023 | + t.Fatal(err) |
| 1024 | + } |
| 1025 | + if _, err := interpr.EvalWithContext(ctx, `time.Sleep(time.Second); println("hello world2")`); err != nil { |
| 1026 | + t.Fatal(err) |
| 1027 | + } |
| 1028 | + |
| 1029 | + timeout := time.NewTimer(5 * time.Second) |
| 1030 | + select { |
| 1031 | + case <-timeout.C: |
| 1032 | + t.Fatal("timeout") |
| 1033 | + case err := <-done: |
| 1034 | + if err != nil { |
| 1035 | + t.Fatal(err) |
| 1036 | + } |
| 1037 | + } |
| 1038 | +} |
| 1039 | + |
| 1040 | +// TestConcurrentEvals3 makes sure that we don't regress into data races at the package level, i.e from: |
| 1041 | +// - global vars, which should obviously not be mutated. |
| 1042 | +// - when calling Interpreter.Use, the symbols given as argument should be |
| 1043 | +// copied when being inserted into interp.binPkg, and not directly used as-is. |
| 1044 | +func TestConcurrentEvals3(t *testing.T) { |
| 1045 | + allDone := make(chan bool) |
| 1046 | + runREPL := func() { |
| 1047 | + done := make(chan error) |
| 1048 | + pinin, poutin := io.Pipe() |
| 1049 | + pinout, poutout := io.Pipe() |
| 1050 | + i := interp.New(interp.Options{Stdin: pinin, Stdout: poutout}) |
| 1051 | + i.Use(stdlib.Symbols) |
| 1052 | + |
| 1053 | + go func() { |
| 1054 | + _, _ = i.REPL() |
| 1055 | + }() |
| 1056 | + |
| 1057 | + input := []string{ |
| 1058 | + `hello one`, |
| 1059 | + `hello two`, |
| 1060 | + `hello three`, |
| 1061 | + } |
| 1062 | + |
| 1063 | + go func() { |
| 1064 | + sc := bufio.NewScanner(pinout) |
| 1065 | + k := 0 |
| 1066 | + for sc.Scan() { |
| 1067 | + l := sc.Text() |
| 1068 | + if l != input[k] { |
| 1069 | + done <- fmt.Errorf("unexpected output, want %q, got %q", input[k], l) |
| 1070 | + return |
| 1071 | + } |
| 1072 | + k++ |
| 1073 | + if k > 2 { |
| 1074 | + break |
| 1075 | + } |
| 1076 | + } |
| 1077 | + done <- nil |
| 1078 | + }() |
| 1079 | + |
| 1080 | + for _, v := range input { |
| 1081 | + in := strings.NewReader(fmt.Sprintf("println(\"%s\")\n", v)) |
| 1082 | + if _, err := io.Copy(poutin, in); err != nil { |
| 1083 | + t.Fatal(err) |
| 1084 | + } |
| 1085 | + time.Sleep(time.Second) |
| 1086 | + } |
| 1087 | + |
| 1088 | + if err := <-done; err != nil { |
| 1089 | + t.Fatal(err) |
| 1090 | + } |
| 1091 | + _ = pinin.Close() |
| 1092 | + _ = poutin.Close() |
| 1093 | + _ = pinout.Close() |
| 1094 | + _ = poutout.Close() |
| 1095 | + allDone <- true |
| 1096 | + } |
| 1097 | + |
| 1098 | + for i := 0; i < 2; i++ { |
| 1099 | + go func() { |
| 1100 | + runREPL() |
| 1101 | + }() |
| 1102 | + } |
| 1103 | + |
| 1104 | + timeout := time.NewTimer(10 * time.Second) |
| 1105 | + for i := 0; i < 2; i++ { |
| 1106 | + select { |
| 1107 | + case <-allDone: |
| 1108 | + case <-timeout.C: |
| 1109 | + t.Fatal("timeout") |
| 1110 | + } |
| 1111 | + } |
| 1112 | +} |
| 1113 | + |
920 | 1114 | func TestEvalScanner(t *testing.T) {
|
921 | 1115 | type testCase struct {
|
922 | 1116 | desc string
|
@@ -1005,6 +1199,7 @@ func TestEvalScanner(t *testing.T) {
|
1005 | 1199 | }
|
1006 | 1200 |
|
1007 | 1201 | runREPL := func(t *testing.T, test testCase) {
|
| 1202 | + // TODO(mpl): use a pipe for the output as well, just as in TestConcurrentEvals5 |
1008 | 1203 | var stdout bytes.Buffer
|
1009 | 1204 | safeStdout := &safeBuffer{buf: &stdout}
|
1010 | 1205 | var stderr bytes.Buffer
|
|
0 commit comments