Files
sanderling/cmd/internal-tools/implementation-sweep/serve_test.go
T
pj 146152a3af feat(implementation-sweep): run one campaign against every implementation of a requirement
installs, builds and serves each implementation on its own port, then hands the campaign tool the same seed slice, step budget and generator for all of them, so a difference between implementations is not a difference in exploration. the generator and platform are fixed rather than exposed.
2026-08-16 17:45:41 +05:30

120 lines
2.8 KiB
Go

package main
import (
"context"
"fmt"
"net/http"
"os"
"path/filepath"
"syscall"
"testing"
"time"
)
// bunSpawningAServer serves from a child process and then waits, which is the
// shape of `bun run preview`: the port belongs to something below the process
// the sweep started.
const bunSpawningAServer = `#!/bin/sh
port=""
previous=""
for argument in "$@"; do
if [ "$previous" = "--port" ]; then port="$argument"; fi
previous="$argument"
done
SWEEP_TEST_SERVE_PORT="$port" "%[1]s" &
wait
`
func TestServerStop_TakesTheProcessBelowItWithTheServer(t *testing.T) {
directory := t.TempDir()
testBinary, err := filepath.Abs(os.Args[0])
if err != nil {
t.Fatal(err)
}
bunPath := writeScript(
t,
filepath.Join(directory, "stub-bun"),
fmt.Sprintf(bunSpawningAServer, testBinary),
)
port := freePortRange(t, 1)
target := implementation{Name: "impl-01", Directory: directory, Port: port}
// Background rather than the test context: only stop() may end this
// server, or a leak would be hidden by the context being cancelled.
running, err := startServer(
context.Background(),
config{bunPath: bunPath},
target,
filepath.Join(directory, "serve.log"),
)
if err != nil {
t.Fatal(err)
}
t.Cleanup(
func() { syscall.Kill(-running.command.Process.Pid, syscall.SIGKILL) },
)
if err := running.waitReady(context.Background(), readinessURL(port)); err != nil {
t.Fatal(err)
}
running.stop()
client := &http.Client{Timeout: time.Second}
deadline := time.Now().Add(5 * time.Second)
for time.Now().Before(deadline) {
response, err := client.Get(readinessURL(port))
if err != nil {
return
}
response.Body.Close()
time.Sleep(100 * time.Millisecond)
}
t.Fatalf(
"port %d is still served after stop(): the server below bun outlived the sweep and holds the port",
port,
)
}
func TestServerWaitReady_ReportsAServerThatExited(t *testing.T) {
directory := t.TempDir()
bunPath := writeScript(
t,
filepath.Join(directory, "stub-bun"),
"#!/bin/sh\necho 'port is already in use' >&2\nexit 1\n",
)
port := freePortRange(t, 1)
running, err := startServer(
context.Background(),
config{bunPath: bunPath},
implementation{
Name: "impl-01",
Directory: directory,
Port: port,
},
filepath.Join(directory, "serve.log"),
)
if err != nil {
t.Fatal(err)
}
defer running.stop()
started := time.Now()
err = running.waitReady(context.Background(), readinessURL(port))
if err == nil {
t.Fatal(
"a server that exited should not be waited on until the start timeout",
)
}
if elapsed := time.Since(started); elapsed > 30*time.Second {
t.Errorf("waited %s for a server that had already exited", elapsed)
}
log, err := os.ReadFile(filepath.Join(directory, "serve.log"))
if err != nil {
t.Fatal(err)
}
if string(log) == "" {
t.Error("serve.log did not capture why the server exited")
}
}