diff --git a/_fixtures/testenv2.go b/_fixtures/testenv2.go new file mode 100644 index 00000000..98cf58b8 --- /dev/null +++ b/_fixtures/testenv2.go @@ -0,0 +1,13 @@ +package main + +import ( + "fmt" + "os" + "runtime" +) + +func main() { + x, y := os.LookupEnv("SOMEVAR") + runtime.Breakpoint() + fmt.Printf("SOMEVAR=%s\n%v", x, y) +} diff --git a/_fixtures/testnextnethttp.go b/_fixtures/testnextnethttp.go index ac95d882..4ce50cc1 100644 --- a/_fixtures/testnextnethttp.go +++ b/_fixtures/testnextnethttp.go @@ -1,10 +1,13 @@ package main import ( + "fmt" + "net" "net/http" + "os" + "path/filepath" "runtime" ) - func main() { http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { runtime.Breakpoint() @@ -17,7 +20,23 @@ func main() { header := w.Header().Get("Content-Type") w.Write([]byte(msg + header)) }) - err := http.ListenAndServe(":9191", nil) + listener, err := net.Listen("tcp", ":0") + if err != nil { + panic(err) + } + port := listener.Addr().(*net.TCPAddr).Port + fmt.Printf("LISTENING:%d\n", port) + + // Also write port to a file for tests that can't capture stdout + // Include PID in filename to avoid conflicts when tests run in parallel + tmpdir := os.TempDir() + portFile := filepath.Join(tmpdir, fmt.Sprintf("testnextnethttp_port_%d", os.Getpid())) + os.WriteFile(portFile, []byte(fmt.Sprintf("%d", port)), 0644) + + // Clean up port file when program exits + defer os.Remove(portFile) + + err = http.Serve(listener, nil) if err != nil { panic(err) } diff --git a/_scripts/test_linux.sh b/_scripts/test_linux.sh index afcf17d4..7a1d1448 100755 --- a/_scripts/test_linux.sh +++ b/_scripts/test_linux.sh @@ -48,7 +48,6 @@ else getgo $version fi - GOPATH=$(pwd)/go export GOPATH export PATH=$PATH:$GOROOT/bin:$GOPATH/bin diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go index 48f29324..38ca5c19 100644 --- a/pkg/proc/proc_test.go +++ b/pkg/proc/proc_test.go @@ -21,6 +21,7 @@ import ( "runtime" "slices" "sort" + "strconv" "strings" "sync" "testing" @@ -526,21 +527,49 @@ func TestNextConcurrentVariant2(t *testing.T) { func TestNextNetHTTP(t *testing.T) { testcases := []nextTest{ - {11, 12}, - {12, 13}, + {14, 15}, + {15, 16}, } withTestProcess("testnextnethttp", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) { + pid := p.Pid() + tmpdir := os.TempDir() + portFile := filepath.Join(tmpdir, fmt.Sprintf("testnextnethttp_port_%d", pid)) + + defer os.Remove(portFile) + go func() { - // Wait for program to start listening. + // Wait for program to write the port to file with timeout + var port int + t0 := time.Now() for { - conn, err := net.Dial("tcp", "127.0.0.1:9191") + if data, err := os.ReadFile(portFile); err == nil { + if parsedPort, err := strconv.Atoi(strings.TrimSpace(string(data))); err == nil { + port = parsedPort + break + } + } + time.Sleep(50 * time.Millisecond) + if time.Since(t0) > 10*time.Second { + t.Errorf("timeout waiting for port file") + return + } + } + + // Wait for program to start listening with timeout + t0 = time.Now() + for { + conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) if err == nil { conn.Close() break } time.Sleep(50 * time.Millisecond) + if time.Since(t0) > 10*time.Second { + t.Errorf("timeout waiting for server to start listening") + return + } } - resp, err := http.Get("http://127.0.0.1:9191") + resp, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d", port)) if err == nil { resp.Body.Close() } @@ -1821,15 +1850,46 @@ func TestCmdLineArgs(t *testing.T) { func TestIssue462(t *testing.T) { skipOn(t, "broken", "windows") // Stacktrace of Goroutine 0 fails with an error withTestProcess("testnextnethttp", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) { + pid := p.Pid() + tmpdir := os.TempDir() + portFile := filepath.Join(tmpdir, fmt.Sprintf("testnextnethttp_port_%d", pid)) + + // Ensure cleanup of port file + defer func() { + os.Remove(portFile) + }() + go func() { - // Wait for program to start listening. + // Wait for program to write the port to file with timeout + var port int + t0 := time.Now() for { - conn, err := net.Dial("tcp", "127.0.0.1:9191") + if data, err := os.ReadFile(portFile); err == nil { + if parsedPort, err := strconv.Atoi(strings.TrimSpace(string(data))); err == nil { + port = parsedPort + break + } + } + time.Sleep(50 * time.Millisecond) + if time.Since(t0) > 10*time.Second { + t.Errorf("timeout waiting for port file") + return + } + } + + // Wait for program to start listening with timeout + t0 = time.Now() + for { + conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) if err == nil { conn.Close() break } time.Sleep(50 * time.Millisecond) + if time.Since(t0) > 10*time.Second { + t.Errorf("timeout waiting for server to start listening") + return + } } grp.RequestManualStop() @@ -2482,17 +2542,46 @@ func TestAttachDetach(t *testing.T) { cmd.Stderr = os.Stderr assertNoError(cmd.Start(), t, "starting fixture") - // wait for testnextnethttp to start listening + // Read port from PID-specific file and wait for testnextnethttp to start listening + var port int + pid := cmd.Process.Pid + tmpdir := os.TempDir() + portFile := filepath.Join(tmpdir, fmt.Sprintf("testnextnethttp_port_%d", pid)) + + // Ensure cleanup of port file + defer func() { + os.Remove(portFile) + if cmd.Process != nil { + cmd.Process.Kill() + } + }() + + // First wait for port file to be written t0 := time.Now() for { - conn, err := net.Dial("tcp", "127.0.0.1:9191") + if data, err := os.ReadFile(portFile); err == nil { + if parsedPort, err := strconv.Atoi(strings.TrimSpace(string(data))); err == nil { + port = parsedPort + break + } + } + time.Sleep(50 * time.Millisecond) + if time.Since(t0) > 10*time.Second { + t.Fatal("fixture did not write port file") + } + } + + // Then wait for server to start listening + t0 = time.Now() + for { + conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) if err == nil { conn.Close() break } time.Sleep(50 * time.Millisecond) if time.Since(t0) > 10*time.Second { - t.Fatal("fixture did not start") + t.Fatal("fixture did not start listening") } } @@ -2515,21 +2604,21 @@ func TestAttachDetach(t *testing.T) { assertNoError(err, t, "Attach") go func() { time.Sleep(1 * time.Second) - resp, err := http.Get("http://127.0.0.1:9191") + resp, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d", port)) if err == nil { resp.Body.Close() } }() assertNoError(p.Continue(), t, "Continue") - assertLineNumber(p.Selected, t, 11, "Did not continue to correct location,") + assertLineNumber(p.Selected, t, 14, "Did not continue to correct location,") assertNoError(p.Detach(false), t, "Detach") if runtime.GOOS != "darwin" { // Debugserver sometimes will leave a zombie process after detaching, this // seems to be a bug with debugserver. - resp, err := http.Get("http://127.0.0.1:9191/nobp") + resp, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d/nobp", port)) assertNoError(err, t, "Page request after detach") bs, err := io.ReadAll(resp.Body) assertNoError(err, t, "Reading /nobp page") diff --git a/service/dap/server_test.go b/service/dap/server_test.go index 427692d8..218e5242 100644 --- a/service/dap/server_test.go +++ b/service/dap/server_test.go @@ -87,6 +87,7 @@ func startDAPServerWithClient(t *testing.T, defaultDebugInfoDirs bool, serverSto // To mock a server created by dap.NewServer(config) or serving dap.NewSession(conn, config, debugger) // set those arg fields manually after the server creation. func startDAPServer(t *testing.T, defaultDebugInfoDirs bool, serverStopped chan struct{}) (server *Server, forceStop chan struct{}) { + t.Helper() // Start the DAP server. listener, err := net.Listen("tcp", ":0") if err != nil { @@ -5421,14 +5422,14 @@ func TestLaunchRequestDefaults(t *testing.T) { runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) { runDebugSession(t, client, "launch", func() { client.LaunchRequestWithArgs(map[string]any{ - "mode": "" /*"debug" by default*/, "program": fixture.Source, "output": "__mybin", + "mode": "" /*"debug" by default*/, "program": fixture.Source, }) }) }) runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) { runDebugSession(t, client, "launch", func() { client.LaunchRequestWithArgs(map[string]any{ - /*"mode":"debug" by default*/ "program": fixture.Source, "output": "__mybin", + /*"mode":"debug" by default*/ "program": fixture.Source, }) }) }) @@ -5504,7 +5505,7 @@ func TestNoDebug_GoodExitStatus(t *testing.T) { runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) { runNoDebugSession(t, client, func() { client.LaunchRequestWithArgs(map[string]any{ - "noDebug": true, "mode": "debug", "program": fixture.Source, "output": "__mybin", + "noDebug": true, "mode": "debug", "program": fixture.Source, }) }, 0) }) @@ -5761,7 +5762,7 @@ func TestLaunchRequestWithBuildFlags(t *testing.T) { // We reuse the harness that builds, but ignore the built binary, // only relying on the source to be built in response to LaunchRequest. client.LaunchRequestWithArgs(map[string]any{ - "mode": "debug", "program": fixture.Source, "output": "__mybin", + "mode": "debug", "program": fixture.Source, "buildFlags": "-ldflags '-X main.Hello=World'", }) }) @@ -5774,7 +5775,7 @@ func TestLaunchRequestWithBuildFlags2(t *testing.T) { // We reuse the harness that builds, but ignore the built binary, // only relying on the source to be built in response to LaunchRequest. client.LaunchRequestWithArgs(map[string]any{ - "mode": "debug", "program": fixture.Source, "output": "__mybin", + "mode": "debug", "program": fixture.Source, "buildFlags": []string{"-ldflags", "-X main.Hello=World"}, }) }) @@ -7844,12 +7845,42 @@ func TestBreakpointAfterDisconnect(t *testing.T) { fixture := protest.BuildFixture(t, "testnextnethttp", protest.AllNonOptimized) cmd := exec.Command(fixture.Path) - cmd.Stdout = os.Stdout + + // Capture stdout to read the port number + stdout, err := cmd.StdoutPipe() + if err != nil { + t.Fatal("failed to create stdout pipe:", err) + } cmd.Stderr = os.Stderr if err := cmd.Start(); err != nil { t.Fatal(err) } + // Read the port from stdout in a goroutine + var port int + portChan := make(chan int, 1) + go func() { + var portLine string + buf := make([]byte, 256) + for { + n, err := stdout.Read(buf) + if err != nil { + return + } + portLine += string(buf[:n]) + if strings.Contains(portLine, "LISTENING:") { + parts := strings.Split(portLine, "LISTENING:") + if len(parts) > 1 { + portStr := strings.TrimSpace(strings.Split(parts[1], "\n")[0]) + if p, err := strconv.Atoi(portStr); err == nil { + portChan <- p + return + } + } + } + } + }() + var server MultiClientCloseServerMock server.stopped = make(chan struct{}) server.impl, server.forceStop = startDAPServer(t, false, server.stopped) @@ -7881,9 +7912,16 @@ func TestBreakpointAfterDisconnect(t *testing.T) { server.impl.session.conn = &connection{ReadWriteCloser: discard{}} // fake a race condition between onDisconnectRequest and the runUntilStopAndNotify goroutine + // Wait for port to be available + select { + case port = <-portChan: + case <-time.After(5 * time.Second): + t.Fatal("timed out waiting for fixture to start listening") + } + httpClient := &http.Client{Timeout: time.Second} - resp, err := httpClient.Get("http://127.0.0.1:9191/nobp") + resp, err := httpClient.Get(fmt.Sprintf("http://127.0.0.1:%d/nobp", port)) if err != nil { t.Fatalf("Page request after disconnect failed: %v", err) } diff --git a/service/test/integration2_test.go b/service/test/integration2_test.go index 1df8b68e..b0f311bf 100644 --- a/service/test/integration2_test.go +++ b/service/test/integration2_test.go @@ -233,7 +233,9 @@ func TestRestart_rebuild(t *testing.T) { // In the original fixture file the env var tested for is SOMEVAR. t.Setenv("SOMEVAR", "bah") - withTestClient2Extended("testenv", t, 0, [3]string{}, nil, func(c service.Client, f protest.Fixture) { + // This test must use `testenv2` and it should be the *only* test that uses it. This is because it will overwrite + // the fixture file with new source. + withTestClient2Extended("testenv2", t, 0, [3]string{}, nil, func(c service.Client, f protest.Fixture) { <-c.Continue() var1, err := c.EvalVariable(api.EvalScope{GoroutineID: -1}, "x", normalLoadConfig) @@ -2498,15 +2500,39 @@ func TestDetachLeaveRunning(t *testing.T) { fixture := protest.BuildFixture(t, "testnextnethttp", buildFlags) cmd := exec.Command(fixture.Path) - cmd.Stdout = os.Stdout + + // Capture stdout to read the port number + stdout, err := cmd.StdoutPipe() + assertNoError(err, t, "creating stdout pipe") cmd.Stderr = os.Stderr assertNoError(cmd.Start(), t, "starting fixture") defer cmd.Process.Kill() + // Read the port from stdout + var port int + var portLine string + buf := make([]byte, 256) + for { + n, err := stdout.Read(buf) + if err != nil { + t.Fatal("failed to read port from fixture stdout:", err) + } + portLine += string(buf[:n]) + if strings.Contains(portLine, "LISTENING:") { + parts := strings.Split(portLine, "LISTENING:") + if len(parts) > 1 { + portStr := strings.TrimSpace(strings.Split(parts[1], "\n")[0]) + port, err = strconv.Atoi(portStr) + assertNoError(err, t, "parsing port number") + break + } + } + } + // wait for testnextnethttp to start listening t0 := time.Now() for { - conn, err := net.Dial("tcp", "127.0.0.1:9191") + conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) if err == nil { conn.Close() break @@ -3185,9 +3211,7 @@ func TestGuessSubstitutePath(t *testing.T) { } guess := func(t *testing.T, goflags string) [][2]string { - oldgoflags := os.Getenv("GOFLAGS") - os.Setenv("GOFLAGS", goflags) - defer os.Setenv("GOFLAGS", oldgoflags) + t.Setenv("GOFLAGS", goflags) dlvbin := protest.GetDlvBinary(t) @@ -3212,11 +3236,11 @@ func TestGuessSubstitutePath(t *testing.T) { switch runtime.GOARCH { case "ppc64le": - os.Setenv("GOFLAGS", "-tags=exp.linuxppc64le") + t.Setenv("GOFLAGS", "-tags=exp.linuxppc64le") case "riscv64": - os.Setenv("GOFLAGS", "-tags=exp.linuxriscv64") + t.Setenv("GOFLAGS", "-tags=exp.linuxriscv64") case "loong64": - os.Setenv("GOFLAGS", "-tags=exp.linuxloong64") + t.Setenv("GOFLAGS", "-tags=exp.linuxloong64") } gsp, err := client.GuessSubstitutePath()