Skip to content

Commit ea1f8e4

Browse files
tests/wasitest: add end-to-end wasip1 TCP echo test
Mirrors upstream Go's src/internal/runtime/wasitest/tcpecho_test.go. A host-side go test compiles the tcpecho_wasip1 guest with tinygo build -target=wasip1, spawns wasmtime with -Stcplisten=host:port so the guest sees a pre-opened TCP listener on fd 3, dials in, exchanges a payload, and asserts the echo. Gated behind TINYGO_WASITEST_TCP=1 because the port-probe race is unavoidable: the host picks a port by binding port 0 then closing the listener, then asks wasmtime to re-bind that same port. Another process can win the race in between. Upstream Go gates the same way (GOWASIENABLERACYTEST=1). Skips cleanly when tinygo or wasmtime aren't on PATH. TINYGO env var overrides which tinygo binary is used so CI can point at a freshly-built one. Exercises the cooperative scheduler's poll_oneoff idle-path integration (PR tinygo-org#5386) together with this branch's io.EOF translation fix in internal/poll.FD.sockRecv — without that fix io.Copy(c, c) in the guest spins on (0, nil) reads when the host half-closes.
1 parent a1577a1 commit ea1f8e4

2 files changed

Lines changed: 189 additions & 0 deletions

File tree

tests/wasitest/tcpecho_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package wasitest
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"math/rand"
7+
"net"
8+
"os"
9+
"os/exec"
10+
"path/filepath"
11+
"testing"
12+
"time"
13+
)
14+
15+
// TestTCPEchoWasip1 spawns a TinyGo-compiled wasip1 module that accepts a
16+
// single TCP connection on a host pre-opened socket and echoes the payload
17+
// back. The runtime is wasmtime, invoked with -Stcplisten=host:port.
18+
//
19+
// Like the upstream Go src/internal/runtime/wasitest/tcpecho_test.go, the
20+
// port-probe is racy (the host releases the listener, then asks the
21+
// runtime to re-bind the same port; another process could win in
22+
// between). Test is gated behind TINYGO_WASITEST_TCP=1 so normal CI runs
23+
// don't see flakes.
24+
func TestTCPEchoWasip1(t *testing.T) {
25+
if os.Getenv("TINYGO_WASITEST_TCP") != "1" {
26+
t.Skip("set TINYGO_WASITEST_TCP=1 to run wasip1 TCP echo test (racy port probe)")
27+
}
28+
29+
tinygo := os.Getenv("TINYGO")
30+
if tinygo == "" {
31+
if p, err := exec.LookPath("tinygo"); err == nil {
32+
tinygo = p
33+
} else {
34+
t.Skip("tinygo not found in PATH; set TINYGO to override")
35+
}
36+
}
37+
wasmtime, err := exec.LookPath("wasmtime")
38+
if err != nil {
39+
t.Skip("wasmtime not found in PATH")
40+
}
41+
42+
tmp := t.TempDir()
43+
wasm := filepath.Join(tmp, "tcpecho_wasip1.wasm")
44+
src := filepath.Join("testdata", "tcpecho_wasip1.go")
45+
46+
build := exec.Command(tinygo, "build", "-target=wasip1", "-o", wasm, src)
47+
build.Stderr = os.Stderr
48+
build.Stdout = os.Stderr
49+
if err := build.Run(); err != nil {
50+
t.Fatalf("tinygo build: %v", err)
51+
}
52+
53+
host, err := probeFreePort()
54+
if err != nil {
55+
t.Fatalf("probe port: %v", err)
56+
}
57+
58+
run := exec.Command(wasmtime, "run", "-Spreview2=false", "-Stcplisten="+host, wasm)
59+
var out bytes.Buffer
60+
run.Stdout = &out
61+
run.Stderr = &out
62+
if err := run.Start(); err != nil {
63+
t.Fatalf("wasmtime start: %v", err)
64+
}
65+
defer func() {
66+
_ = run.Process.Kill()
67+
_ = run.Wait()
68+
if t.Failed() {
69+
t.Logf("wasmtime output:\n%s", out.String())
70+
}
71+
}()
72+
73+
var conn net.Conn
74+
deadline := time.Now().Add(10 * time.Second)
75+
for {
76+
conn, err = net.Dial("tcp", host)
77+
if err == nil {
78+
break
79+
}
80+
if time.Now().After(deadline) {
81+
t.Fatalf("dial %s: %v\nwasmtime output:\n%s", host, err, out.String())
82+
}
83+
time.Sleep(100 * time.Millisecond)
84+
}
85+
defer conn.Close()
86+
87+
if err := conn.SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
88+
t.Fatal(err)
89+
}
90+
91+
payload := []byte("foobar")
92+
if _, err := conn.Write(payload); err != nil {
93+
t.Fatalf("write: %v", err)
94+
}
95+
if err := conn.(*net.TCPConn).CloseWrite(); err != nil {
96+
t.Fatalf("CloseWrite: %v", err)
97+
}
98+
var got bytes.Buffer
99+
if _, err := got.ReadFrom(conn); err != nil {
100+
t.Fatalf("read: %v", err)
101+
}
102+
if !bytes.Equal(got.Bytes(), payload) {
103+
t.Errorf("payload mismatch: sent %q, got %q", payload, got.Bytes())
104+
}
105+
}
106+
107+
func probeFreePort() (string, error) {
108+
port := rand.Intn(10000) + 40000
109+
for range 20 {
110+
host := fmt.Sprintf("127.0.0.1:%d", port)
111+
l, err := net.Listen("tcp", host)
112+
if err == nil {
113+
l.Close()
114+
return host, nil
115+
}
116+
port++
117+
}
118+
return "", fmt.Errorf("could not find free port")
119+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
//go:build wasip1
2+
3+
// tcpecho_wasip1 is a guest program for TestTCPEchoWasip1. It accepts a
4+
// single TCP connection on a wasi-pre-opened socket fd, echoes the
5+
// payload back, half-closes its write side, and exits.
6+
package main
7+
8+
import (
9+
"errors"
10+
"net"
11+
"os"
12+
"syscall"
13+
)
14+
15+
func main() {
16+
if err := run(); err != nil {
17+
println(err.Error())
18+
os.Exit(1)
19+
}
20+
}
21+
22+
func run() error {
23+
l, err := findListener()
24+
if err != nil {
25+
return err
26+
}
27+
if l == nil {
28+
return errors.New("no pre-opened sockets available")
29+
}
30+
defer l.Close()
31+
32+
c, err := l.Accept()
33+
if err != nil {
34+
return err
35+
}
36+
defer c.Close()
37+
38+
var buf [256]byte
39+
n, err := c.Read(buf[:])
40+
if err != nil {
41+
return err
42+
}
43+
if _, err := c.Write(buf[:n]); err != nil {
44+
return err
45+
}
46+
return c.(*net.TCPConn).CloseWrite()
47+
}
48+
49+
// findListener walks pre-opened fds starting at 3 (0/1/2 are stdio),
50+
// returning a TCPListener for the first one that is a socket. wasip1
51+
// host runtimes hand pre-opened TCP listeners as raw fds with no
52+
// in-band metadata, so probing via net.FileListener is the only path.
53+
func findListener() (net.Listener, error) {
54+
for preopenFd := uintptr(3); ; preopenFd++ {
55+
f := os.NewFile(preopenFd, "")
56+
l, err := net.FileListener(f)
57+
f.Close()
58+
59+
var se syscall.Errno
60+
if errors.As(err, &se) {
61+
switch se {
62+
case syscall.ENOTSOCK:
63+
continue
64+
case syscall.EBADF:
65+
return nil, nil
66+
}
67+
}
68+
return l, err
69+
}
70+
}

0 commit comments

Comments
 (0)