service,proc: fix tests to enable parallel runs (#4135)

* *: randomize testnextnethttp.go listen port

* service/test: prefer t.Setenv

* *: cleanup port pid files

* service: fix final parallelization bugs

* address review feedback

* pkg/proc: fix test on windows

* fix finding port file on TestIssue462
This commit is contained in:
Derek Parker
2025-09-29 11:13:03 -04:00
committed by GitHub
parent 7c9e79be5c
commit fd4fc92c74
6 changed files with 214 additions and 32 deletions

13
_fixtures/testenv2.go Normal file
View File

@ -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)
}

View File

@ -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)
}

View File

@ -48,7 +48,6 @@ else
getgo $version
fi
GOPATH=$(pwd)/go
export GOPATH
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin

View File

@ -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")

View File

@ -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)
}

View File

@ -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()