summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-14 14:10:28 -0300
committerGabriel Schneider <[email protected]>2026-09-14 14:20:25 -0300
commit5f24c4a2284a0af84fb9d116f7a39f6a58e42ae9 (patch)
tree8679763439492361fe99ea01f5190e5204fa590c /test
downloadcloud9-5f24c4a2284a0af84fb9d116f7a39f6a58e42ae9.tar.gz
cloud9-5f24c4a2284a0af84fb9d116f7a39f6a58e42ae9.zip
Implement base 9P2000 sessions, shared transports, and conformance probes
Diffstat (limited to 'test')
-rw-r--r--test/differential/go.mod14
-rw-r--r--test/differential/go.sum58
-rw-r--r--test/differential/main.go172
-rw-r--r--test/differential/probe.zig59
-rw-r--r--test/differential/session.go45
-rw-r--r--test/differential/session.zig82
-rw-r--r--test/fuzz.zig70
-rw-r--r--test/quic.zig268
-rw-r--r--test/transport.zig82
9 files changed, 850 insertions, 0 deletions
diff --git a/test/differential/go.mod b/test/differential/go.mod
new file mode 100644
index 0000000..43f6ef2
--- /dev/null
+++ b/test/differential/go.mod
@@ -0,0 +1,14 @@
+module cloud9-differential
+
+go 1.26.5
+
+require (
+ 9fans.net/go v0.0.7
+ github.com/knusbaum/go9p v1.18.0
+)
+
+require (
+ github.com/Plan9-Archive/libauth v0.0.0-20180917063427-d1ca9e94969d // indirect
+ github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect
+ github.com/fhs/mux9p v0.3.1 // indirect
+)
diff --git a/test/differential/go.sum b/test/differential/go.sum
new file mode 100644
index 0000000..20c7aea
--- /dev/null
+++ b/test/differential/go.sum
@@ -0,0 +1,58 @@
+9fans.net/go v0.0.2/go.mod h1:lfPdxjq9v8pVQXUMBCx5EO5oLXWQFlKRQgs1kEkjoIM=
+9fans.net/go v0.0.7 h1:H5CsYJTf99C8EYAQr+uSoEJnLP/iZU8RmDuhyk30iSM=
+9fans.net/go v0.0.7/go.mod h1:Rxvbbc1e+1TyGMjAvLthGTyO97t+6JMQ6ly+Lcs9Uf0=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/Plan9-Archive/libauth v0.0.0-20180917063427-d1ca9e94969d h1:xH/U6K+HYxh1480TkQYRqRO8F2RJsg+R6wFiVJzdldg=
+github.com/Plan9-Archive/libauth v0.0.0-20180917063427-d1ca9e94969d/go.mod h1:UKp8dv9aeaZoQFWin7eQXtz89iHly1YAFZNn3MCutmQ=
+github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
+github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
+github.com/fhs/mux9p v0.3.1 h1:x1UswUWZoA9vrA02jfisndCq3xQm+wrQUxUt5N99E08=
+github.com/fhs/mux9p v0.3.1/go.mod h1:F4hwdenmit0WDoNVT2VMWlLJrBVCp/8UhzJa7scfjEQ=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok=
+github.com/hanwen/go-fuse/v2 v2.0.3/go.mod h1:0EQM6aH2ctVpvZ6a+onrQ/vaykxh2GH7hy3e13vzTUY=
+github.com/knusbaum/go9p v1.18.0 h1:/Y67RNvNKX1ZV1IOdnO1lIetiF0X+CumOyvEc0011GI=
+github.com/knusbaum/go9p v1.18.0/go.mod h1:HtMoJKqZUe1Oqag5uJqG5RKQ9gWPSP+wolsnLLv44r8=
+github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
+golang.org/x/exp v0.0.0-20210405174845-4513512abef3/go.mod h1:I6l2HNBLBZEcrOoCpyKLdY2lHoRZ8lI4x60KMCQDft4=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20201217150744-e6ae53a27f4f/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=
+golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201020230747-6e5568b54d1a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210415045647-66c3f260301c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/test/differential/main.go b/test/differential/main.go
new file mode 100644
index 0000000..7a2538d
--- /dev/null
+++ b/test/differential/main.go
@@ -0,0 +1,172 @@
+// Valid-traffic differential conformance and measurement runner. No network targets.
+package main
+
+import (
+ p9 "9fans.net/go/plan9"
+ "bytes"
+ "encoding/binary"
+ "encoding/json"
+ "flag"
+ "fmt"
+ other "github.com/knusbaum/go9p/proto"
+ "io"
+ "math/rand"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "time"
+)
+
+type reader struct {
+ data []byte
+ chunk, calls int
+}
+
+func (r *reader) Read(p []byte) (int, error) {
+ r.calls++
+ if len(r.data) == 0 {
+ return 0, io.EOF
+ }
+ n := min(len(p), len(r.data), r.chunk)
+ copy(p, r.data[:n])
+ r.data = r.data[n:]
+ return n, nil
+}
+func check(err error) {
+ if err != nil {
+ panic(err)
+ }
+}
+func marshal(v any) []byte { b, e := json.MarshalIndent(v, "", " "); check(e); return append(b, '\n') }
+func corpus(seed int64, rounds int) [][]byte {
+ r := rand.New(rand.NewSource(seed))
+ frames := [][]byte{}
+ names := []string{"a", "file", "directory", "日本語", "ação"}
+ for i := 0; i < rounds; i++ {
+ q := p9.Qid{Type: 0, Vers: r.Uint32(), Path: r.Uint64()}
+ d := p9.Dir{Qid: q, Mode: 0600, Atime: r.Uint32(), Mtime: r.Uint32(), Length: r.Uint64(), Name: names[i%len(names)], Uid: "user", Gid: "group", Muid: "user"}
+ stat, e := d.Bytes()
+ check(e)
+ if i%2 == 1 {
+ d.Null()
+ stat, e = d.Bytes()
+ check(e)
+ }
+ data := make([]byte, []int{0, 1, 7, 127, 1024, 8192}[i%6])
+ _, e = r.Read(data)
+ check(e)
+ wn := make([]string, i%17)
+ wq := make([]p9.Qid, i%17)
+ for j := range wn {
+ wn[j] = names[r.Intn(len(names))]
+ wq[j] = q
+ }
+ for typ := uint8(100); typ <= 127; typ++ {
+ if typ == 106 {
+ continue
+ }
+ f := p9.Fcall{Type: typ, Tag: uint16(r.Intn(65535)), Fid: r.Uint32(), Newfid: r.Uint32(), Afid: p9.NOFID, Uname: "user", Aname: "", Version: "9P2000", Msize: 65536, Oldtag: uint16(r.Intn(65535)), Ename: "permission denied", Qid: q, Aqid: p9.Qid{Type: p9.QTAUTH, Vers: q.Vers, Path: q.Path}, Iounit: 0, Name: d.Name, Perm: 0600, Mode: uint8(i % 4), Offset: r.Uint64(), Count: uint32(len(data)), Data: data, Wname: wn, Wqid: wq, Stat: stat}
+ if typ == 100 || typ == 101 {
+ f.Tag = p9.NOTAG
+ }
+ if typ == 114 {
+ f.Name = names[i%len(names)]
+ }
+ b, e := f.Bytes()
+ check(e)
+ frames = append(frames, b)
+ }
+ }
+ return frames
+}
+
+type metric struct {
+ Implementation string `json:"implementation"`
+ Operations uint64 `json:"operations"`
+ CodecNS int64 `json:"codec_ns"`
+ Allocations uint64 `json:"codec_allocations"`
+ AllocatedBytes uint64 `json:"allocated_bytes"`
+ ReadCalls uint64 `json:"read_calls"`
+ Bytes uint64 `json:"bytes"`
+}
+
+func measure(name string, frames [][]byte, repeats, chunk int) metric {
+ m := metric{Implementation: name}
+ var before, after runtime.MemStats
+ runtime.GC()
+ runtime.ReadMemStats(&before)
+ for index, frame := range frames {
+ start := time.Now()
+ for j := 0; j < repeats; j++ {
+ rd := reader{data: frame, chunk: chunk}
+ var out []byte
+ if name == "9fans v0.0.7" {
+ f, e := p9.ReadFcall(&rd)
+ check(e)
+ out, e = f.Bytes()
+ check(e)
+ } else {
+ f, e := other.ParseCall(&rd)
+ check(e)
+ out = f.Compose()
+ }
+ if !bytes.Equal(frame, out) {
+ panic(fmt.Sprintf("%s frame %d type %d differs", name, index, frame[4]))
+ }
+ m.ReadCalls += uint64(rd.calls)
+ m.Bytes += uint64(len(frame))
+ m.Operations++
+ }
+ m.CodecNS += time.Since(start).Nanoseconds()
+ }
+ runtime.ReadMemStats(&after)
+ m.Allocations = after.Mallocs - before.Mallocs
+ m.AllocatedBytes = after.TotalAlloc - before.TotalAlloc
+ return m
+}
+func main() {
+ seed := flag.Int64("seed", 4200, "deterministic valid-traffic seed")
+ rounds := flag.Int("rounds", 100, "27 messages per round")
+ repeats := flag.Int("repeats", 20, "codec repetitions")
+ chunk := flag.Int("chunk", 65536, "maximum read/write chunk; use 1 to fragment every byte")
+ session := flag.String("session-probe", "", "run live sessions against go9p")
+ probe := flag.String("probe", "../../zig-out/bin/cloud9-probe", "cloud9 executable")
+ output := flag.String("output", "results", "artifact directory")
+ flag.Parse()
+ if *rounds < 1 || *rounds > 10000 || *repeats < 1 || *repeats > 10000 || *chunk < 1 {
+ panic("invalid bounds")
+ }
+ frames := corpus(*seed, *rounds)
+ all := bytes.Join(frames, nil)
+ check(os.MkdirAll(*output, 0755))
+ check(os.WriteFile(filepath.Join(*output, "corpus.9p"), all, 0644))
+ config := map[string]any{"seed": *seed, "rounds": *rounds, "repeats": *repeats, "chunk": *chunk, "frames": len(frames), "go": runtime.Version(), "platform": runtime.GOOS + "/" + runtime.GOARCH}
+ check(os.WriteFile(filepath.Join(*output, "config.json"), marshal(config), 0644))
+ metrics := []metric{measure("9fans v0.0.7", frames, *repeats, *chunk), measure("go9p v1.18.0", frames, *repeats, *chunk)}
+ var input bytes.Buffer
+ for _, v := range []uint32{uint32(len(frames)), uint32(*repeats), uint32(*chunk)} {
+ check(binary.Write(&input, binary.LittleEndian, v))
+ }
+ input.Write(all)
+ command := exec.Command(*probe)
+ command.Stdin = &input
+ var stderr bytes.Buffer
+ command.Stderr = &stderr
+ out, e := command.Output()
+ if e != nil {
+ panic(fmt.Sprintf("cloud9: %v: %s", e, stderr.String()))
+ }
+ if !bytes.Equal(all, out) {
+ panic("cloud9 round-trip differs; replay corpus.9p with saved configuration")
+ }
+ var cloud map[string]any
+ check(json.Unmarshal(stderr.Bytes(), &cloud))
+ report := map[string]any{"configuration": config, "correctness": "all generated base-9P2000 frames agree byte-for-byte", "cloud9": cloud, "references": metrics, "measurement_notes": []string{"Cloud9 codec timer excludes pipe I/O; Go timers include in-memory stream parsing and byte comparison. Do not treat these as equivalent throughput benchmarks.", "Go allocations use runtime.MemStats deltas on a single-process run; cloud9 codec has no allocation API.", "Cloud9 I/O counters are actual stdin/stdout syscalls; Go read counters are io.Reader calls, not syscalls.", "These are wire-conformance probes, not differential filesystem-server semantics."}}
+ if *session != "" {
+ report["session"] = sessionProbe(*session, *seed, *rounds)
+ }
+ b := marshal(report)
+ check(os.WriteFile(filepath.Join(*output, "report.json"), b, 0644))
+ os.Stdout.Write(b)
+}
diff --git a/test/differential/probe.zig b/test/differential/probe.zig
new file mode 100644
index 0000000..ec7c88e
--- /dev/null
+++ b/test/differential/probe.zig
@@ -0,0 +1,59 @@
+const std = @import("std");
+const c9 = @import("cloud9");
+const libc = std.c;
+var reads: u64 = 0;
+var writes: u64 = 0;
+var bytes_in: u64 = 0;
+var bytes_out: u64 = 0;
+
+fn readAll(bytes: []u8, chunk: usize) !void {
+ var offset: usize = 0;
+ while (offset < bytes.len) {
+ reads += 1;
+ const n = libc.read(0, bytes[offset..].ptr, @min(chunk, bytes.len - offset));
+ if (n <= 0) return error.Input;
+ offset += @intCast(n);
+ bytes_in += @intCast(n);
+ }
+}
+fn writeAll(bytes: []const u8, chunk: usize) !void {
+ var offset: usize = 0;
+ while (offset < bytes.len) {
+ writes += 1;
+ const n = libc.write(1, bytes[offset..].ptr, @min(chunk, bytes.len - offset));
+ if (n <= 0) return error.Output;
+ offset += @intCast(n);
+ bytes_out += @intCast(n);
+ }
+}
+fn nowNs() u64 {
+ var ts: libc.timespec = undefined;
+ std.debug.assert(libc.clock_gettime(.MONOTONIC, &ts) == 0);
+ return @as(u64, @intCast(ts.sec)) * std.time.ns_per_s + @as(u64, @intCast(ts.nsec));
+}
+pub fn main() !void {
+ var header: [12]u8 = undefined;
+ try readAll(&header, header.len);
+ const count = std.mem.readInt(u32, header[0..4], .little);
+ const repeats = std.mem.readInt(u32, header[4..8], .little);
+ const chunk = std.mem.readInt(u32, header[8..12], .little);
+ if (count > 1_000_000 or repeats == 0 or repeats > 10000 or chunk == 0) return error.Options;
+ var input: [65536]u8 = undefined;
+ var output: [65536]u8 = undefined;
+ var elapsed_ns: u64 = 0;
+ for (0..count) |_| {
+ try readAll(input[0..4], chunk);
+ const len = c9.frameLen(input[0..4]).?;
+ if (len < c9.header_len or len > input.len) return error.Frame;
+ try readAll(input[4..len], chunk);
+ const start = nowNs();
+ for (0..repeats) |_| {
+ const decoded = try c9.decode(input[0..len]);
+ const encoded = try c9.encode(decoded.msg, decoded.tag, &output);
+ std.mem.doNotOptimizeAway(encoded);
+ }
+ elapsed_ns += nowNs() - start;
+ try writeAll(output[0..len], chunk);
+ }
+ std.debug.print("{{\"implementation\":\"cloud9\",\"codec_ns\":{d},\"operations\":{d},\"read_calls\":{d},\"write_calls\":{d},\"bytes_in\":{d},\"bytes_out\":{d},\"codec_allocations\":0,\"allocation_evidence\":\"no allocator or allocation calls in codec\"}}\n", .{ elapsed_ns, @as(u64, count) * repeats, reads, writes, bytes_in - header.len, bytes_out });
+}
diff --git a/test/differential/session.go b/test/differential/session.go
new file mode 100644
index 0000000..3fda214
--- /dev/null
+++ b/test/differential/session.go
@@ -0,0 +1,45 @@
+package main
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "github.com/knusbaum/go9p"
+ "github.com/knusbaum/go9p/fs"
+ "os/exec"
+ "strconv"
+ "time"
+)
+
+func sessionProbe(binary string, seed int64, rounds int) map[string]any {
+ tree, root := fs.NewFS("user", "user", 0755)
+ check(root.AddChild(fs.NewStaticFile(tree.NewStat("file", "user", "user", 0600), nil)))
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+ command := exec.CommandContext(ctx, binary, strconv.FormatInt(seed, 10), strconv.Itoa(rounds))
+ input, e := command.StdinPipe()
+ check(e)
+ output, e := command.StdoutPipe()
+ check(e)
+ var stderr bytes.Buffer
+ command.Stderr = &stderr
+ check(command.Start())
+ done := make(chan error, 1)
+ go func() { done <- go9p.ServeReadWriter(output, input, tree.Server()) }()
+ e = command.Wait()
+ input.Close()
+ if e != nil {
+ panic(fmt.Sprintf("session probe: %v: %s", e, stderr.String()))
+ }
+ select {
+ case <-done:
+ case <-time.After(time.Second):
+ panic("reference server did not stop")
+ }
+ var result map[string]any
+ check(json.Unmarshal(stderr.Bytes(), &result))
+ result["server"] = "go9p v1.18.0 StaticFile"
+ result["checks"] = "walk/open/write/read/stat/EOF/clunk, content oracle, fragmented requests and replies"
+ return result
+}
diff --git a/test/differential/session.zig b/test/differential/session.zig
new file mode 100644
index 0000000..3c5d2de
--- /dev/null
+++ b/test/differential/session.zig
@@ -0,0 +1,82 @@
+//! Local interoperability client. The parent connects stdio to a reference server.
+const std = @import("std");
+const c9 = @import("cloud9");
+const libc = std.c;
+const Session = struct {
+ client: c9.Client,
+ requests: usize = 0,
+ bytes_read: usize = 0,
+ bytes_written: usize = 0,
+ read_calls: usize = 0,
+ write_calls: usize = 0,
+
+ fn ask(s: *Session, request: c9.Client.Request) !c9.Client.Result {
+ const tag = try s.client.submit(request);
+ s.requests += 1;
+ while (s.client.output().len != 0) {
+ const output = s.client.output();
+ s.write_calls += 1;
+ // Fragment requests across their header and body fields.
+ const n = libc.write(1, output.ptr, @min(output.len, 3));
+ if (n <= 0) return error.Write;
+ s.bytes_written += @intCast(n);
+ s.client.wrote(@intCast(n));
+ }
+ while (true) {
+ if (s.client.take()) |done| {
+ if (done.tag != tag) return error.Tag;
+ if (done.result == .fail) {
+ std.debug.print("remote error: {s}\n", .{done.result.fail});
+ return error.Remote;
+ }
+ return done.result;
+ }
+ if (s.client.dead) return error.Protocol;
+ var buffer: [7]u8 = undefined;
+ s.read_calls += 1;
+ const n = libc.read(0, &buffer, buffer.len);
+ if (n <= 0) return error.Read;
+ s.bytes_read += @intCast(n);
+ if (s.client.push(buffer[0..@intCast(n)]) != n) return error.InputFull;
+ }
+ }
+};
+
+pub fn main(init: std.process.Init) !void {
+ const args = try init.minimal.args.toSlice(init.arena.allocator());
+ if (args.len != 3) return error.Arguments;
+ const seed = try std.fmt.parseInt(u64, args[1], 10);
+ const rounds = try std.fmt.parseInt(usize, args[2], 10);
+ if (rounds == 0 or rounds > 10000) return error.Arguments;
+ var random: std.Random.DefaultPrng = .init(seed);
+ var input: [8192]u8 = undefined;
+ var output: [8192]u8 = undefined;
+ var session: Session = .{ .client = .init(.{ .in = &input, .out = &output }) };
+ const version = (try session.ask(.{ .version = .{} })).version;
+ if (version.msize > input.len or !std.mem.eql(u8, version.version, "9P2000")) return error.Version;
+ _ = try session.ask(.{ .attach = .{ .fid = 0, .uname = "user" } });
+ var expected: [4096]u8 = @splat(0);
+ var length: usize = 0;
+ for (0..rounds) |_| {
+ const walk = (try session.ask(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &.{"file"} } })).walk;
+ if (walk.nwqid != 1 or walk.wqid[0].type & c9.qtdir != 0) return error.Walk;
+ _ = try session.ask(.{ .open = .{ .fid = 1, .mode = c9.ordwr } });
+ const offset = random.random().uintLessThan(usize, 2048);
+ const count = random.random().uintLessThan(usize, 1024) + 1;
+ var data: [1024]u8 = undefined;
+ random.random().bytes(data[0..count]);
+ const written = (try session.ask(.{ .write = .{ .fid = 1, .offset = offset, .data = data[0..count] } })).write;
+ if (written != count) return error.WriteCount;
+ @memcpy(expected[offset..][0..count], data[0..count]);
+ length = @max(length, offset + count);
+ const result = (try session.ask(.{ .read = .{ .fid = 1, .offset = 0, .count = expected.len } })).read;
+ if (!std.mem.eql(u8, result, expected[0..length])) return error.Contents;
+ const stat = (try session.ask(.{ .stat = .{ .fid = 1 } })).stat;
+ if (stat.length != length or !std.mem.eql(u8, stat.name, "file")) return error.Stat;
+ const eof = (try session.ask(.{ .read = .{ .fid = 1, .offset = length, .count = 1 } })).read;
+ if (eof.len != 0) return error.Eof;
+ _ = try session.ask(.{ .clunk = .{ .fid = 1 } });
+ }
+ _ = try session.ask(.{ .clunk = .{ .fid = 0 } });
+ std.debug.print("{{\"requests\":{d},\"bytes_read\":{d},\"bytes_written\":{d},\"read_calls\":{d},\"write_calls\":{d},\"seed\":{d},\"rounds\":{d}}}\n", .{ session.requests, session.bytes_read, session.bytes_written, session.read_calls, session.write_calls, seed, rounds });
+}
diff --git a/test/fuzz.zig b/test/fuzz.zig
new file mode 100644
index 0000000..b7d9f21
--- /dev/null
+++ b/test/fuzz.zig
@@ -0,0 +1,70 @@
+//! Deterministic local robustness probes; reference implementations see valid traffic only.
+const std = @import("std");
+const c9 = @import("cloud9");
+pub fn main(init: std.process.Init) !void {
+ const args = try init.minimal.args.toSlice(init.arena.allocator());
+ const seed = if (args.len > 1) try std.fmt.parseInt(u64, args[1], 10) else 4200;
+ const iterations = if (args.len > 2) try std.fmt.parseInt(u32, args[2], 10) else 100000;
+ if (args.len > 3 or iterations > 10000000) return error.Arguments;
+ var prng: std.Random.DefaultPrng = .init(seed);
+ const random = prng.random();
+ var accepted: usize = 0;
+ var rejected: usize = 0;
+ var buffer: [8192]u8 = undefined;
+ var encoded: [8192]u8 = undefined;
+ var payload: [4096]u8 = undefined;
+ var server_in: [8192]u8 = undefined;
+ var server_out: [8192]u8 = undefined;
+ for (0..iterations) |_| {
+ random.bytes(&payload);
+ const data = payload[0..random.uintLessThan(usize, payload.len)];
+ const qid: c9.Qid = .{ .type = random.int(u8), .version = random.int(u32), .path = random.int(u64) };
+ const stat: c9.Stat = .{
+ .type = 0,
+ .dev = 0,
+ .qid = qid,
+ .mode = random.int(u32),
+ .atime = 0,
+ .mtime = 0,
+ .length = random.int(u64),
+ .name = "file",
+ .uid = "user",
+ .gid = "group",
+ .muid = "",
+ };
+ const messages = [_]c9.Msg{
+ .{ .tversion = .{ .msize = 8192, .version = "9P2000" } },
+ .{ .twalk = .{ .fid = 0, .newfid = 1, .nwname = 16, .wname = @splat("dir") } },
+ .{ .rwalk = .{ .nwqid = 16, .wqid = @splat(qid) } },
+ .{ .twrite = .{ .fid = random.int(u32), .offset = random.int(u64), .data = data } },
+ .{ .rread = .{ .data = data } },
+ .{ .rstat = .{ .stat = stat } },
+ .{ .twstat = .{ .fid = 1, .stat = stat } },
+ .{ .tflush = .{ .oldtag = random.int(u16) } },
+ .{ .tread = .{ .fid = 1, .offset = random.int(u64), .count = random.int(u32) } },
+ };
+ const msg = messages[random.uintLessThan(usize, messages.len)];
+ var frame: []u8 = try c9.encode(msg, random.int(u16), &buffer);
+ // Leave some valid seeds unchanged; probe truncation and altered fields locally.
+ switch (random.uintLessThan(u8, 4)) {
+ 0 => {},
+ 1 => frame = frame[0..random.uintLessThan(usize, frame.len)],
+ 2 => frame[random.uintLessThan(usize, frame.len)] ^= random.int(u8),
+ 3 => std.mem.writeInt(u32, frame[0..4], random.int(u32), .little),
+ else => unreachable,
+ }
+ if (c9.decode(frame)) |decoded| {
+ const output = try c9.encode(decoded.msg, decoded.tag, &encoded);
+ if (!std.mem.eql(u8, frame, output)) return error.RoundTrip;
+ accepted += 1;
+ } else |_| rejected += 1;
+ var server: c9.Server = .init(.{ .in = &server_in, .out = &server_out });
+ _ = server.push(frame);
+ if (server.receive() catch null) |request| {
+ if (request.msg != .tversion) return error.BeforeVersion;
+ server.negotiate(request.msg.tversion.msize, request.msg.tversion.version) catch {};
+ server.release();
+ }
+ }
+ std.debug.print("seed={d} iterations={d} accepted={d} rejected={d}\n", .{ seed, iterations, accepted, rejected });
+}
diff --git a/test/quic.zig b/test/quic.zig
new file mode 100644
index 0000000..48ff839
--- /dev/null
+++ b/test/quic.zig
@@ -0,0 +1,268 @@
+const std = @import("std");
+const libc = std.c;
+const ssl = @import("openssl");
+const Quic = @import("cloud9").Quic(ssl, "cloud9-test");
+const Listener = Quic.Listener;
+const Connection = Quic.Connection;
+const TestPair = struct {
+ listener: *Listener,
+ client: Connection,
+ server: ?Connection = null,
+
+ fn init(listener: *Listener) !TestPair {
+ var p: TestPair = .{ .listener = listener, .client = try .dial(listener.address) };
+ errdefer p.deinit();
+ const deadline = testNow() + 3000;
+ while (true) {
+ try listener.events();
+ try p.client.events();
+ if (p.server == null) p.server = try listener.accept();
+ const connected = try p.client.handshake();
+ if (p.server) |*server| if (connected and try server.handshake()) return p;
+ try p.wait(deadline);
+ }
+ }
+
+ fn wait(p: *TestPair, deadline: i64) !void {
+ const remaining = deadline - testNow();
+ if (remaining <= 0) return error.Deadline;
+ var timeout: i32 = @intCast(@min(remaining, std.math.maxInt(i32)));
+ if (p.listener.nextDue()) |ms| timeout = @min(timeout, ms);
+ if (p.client.nextDue()) |ms| timeout = @min(timeout, ms);
+ var fds = [_]libc.pollfd{ p.listener.poll(), p.client.poll().? };
+ const rc = libc.poll(&fds, fds.len, timeout);
+ if (rc < 0 and libc.errno(rc) != .INTR) return error.Poll;
+ if (testNow() >= deadline) return error.Deadline;
+ try p.listener.events();
+ try p.client.events();
+ }
+
+ fn transfer(p: *TestPair, from_client: bool, bytes: []const u8, fragment: usize) !void {
+ const writer = if (from_client) &p.client else &p.server.?;
+ const reader = if (from_client) &p.server.? else &p.client;
+ var sent: usize = 0;
+ var received: usize = 0;
+ var buffer: [8192]u8 = undefined;
+ const deadline = testNow() + 3000;
+ while (received < bytes.len) {
+ const written = if (sent != bytes.len) try writer.write(bytes[sent..][0..@min(fragment, bytes.len - sent)]) else 0;
+ sent += written;
+ const count = try reader.read(buffer[0..@min(fragment, buffer.len)]);
+ if (count) |n| {
+ try std.testing.expect(n > 0 and n <= bytes.len - received);
+ try std.testing.expectEqualSlices(u8, bytes[received..][0..n], buffer[0..n]);
+ received += n;
+ }
+ if (testNow() >= deadline) return error.Deadline;
+ if (received != bytes.len and written == 0 and count == null) {
+ try p.wait(deadline);
+ } else {
+ try p.listener.events();
+ try p.client.events();
+ }
+ }
+ try std.testing.expectEqual(bytes.len, sent);
+ }
+
+ fn finish(p: *TestPair) !void {
+ try p.client.conclude();
+ try p.server.?.conclude();
+ var buffer: [16]u8 = undefined;
+ var a = false;
+ var b = false;
+ const deadline = testNow() + 3000;
+ while (!a or !b) {
+ if (!a) if (try p.client.read(&buffer)) |n| {
+ try std.testing.expectEqual(@as(usize, 0), n);
+ a = true;
+ };
+ if (!b) if (try p.server.?.read(&buffer)) |n| {
+ try std.testing.expectEqual(@as(usize, 0), n);
+ b = true;
+ };
+ if (!a or !b) try p.wait(deadline);
+ }
+ }
+
+ fn deinit(p: *TestPair) void {
+ if (p.server) |*server| server.deinit();
+ p.client.deinit();
+ }
+};
+
+fn testNow() i64 {
+ var ts: libc.timespec = undefined;
+ std.debug.assert(libc.clock_gettime(.MONOTONIC, &ts) == 0);
+ return @as(i64, @intCast(ts.sec)) * 1000 + @divFloor(@as(i64, @intCast(ts.nsec)), 1_000_000);
+}
+
+test "QUIC fragmented 9P frames reconnect and stream EOF over IPv4 and IPv6" {
+ const request = "\x13\x00\x00\x00\x64\xff\xff\x00\x20\x00\x00\x06\x00" ++ "9P2000";
+ const response = "\x13\x00\x00\x00\x65\xff\xff\x00\x20\x00\x00\x06\x00" ++ "9P2000";
+ const read_request = "\x17\x00\x00\x00\x74\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x00\x00";
+ const read_response = "\x16\x00\x00\x00\x75\x01\x00\x0b\x00\x00\x00" ++ "hello ninep";
+ for ([_][]const u8{ "127.0.0.1", "::1" }) |host| {
+ var listener = try Listener.init(try .parse(host, 0));
+ defer listener.deinit();
+ try std.testing.expect(listener.address.getPort() != 0);
+ if (listener.address == .ip6) {
+ var enabled: c_int = 0;
+ var len: libc.socklen_t = @sizeOf(c_int);
+ const v6only = if (@import("builtin").os.tag.isDarwin()) 27 else libc.IPV6.V6ONLY;
+ try std.testing.expectEqual(@as(c_int, 0), libc.getsockopt(listener.fd, libc.IPPROTO.IPV6, v6only, @ptrCast(&enabled), &len));
+ try std.testing.expectEqual(@as(c_int, 1), enabled);
+ }
+ for (0..3) |_| {
+ var pair = try TestPair.init(&listener);
+ defer pair.deinit();
+ try std.testing.expect(pair.server.?.poll() == null);
+ try std.testing.expect(pair.server.?.nextDue() == null);
+ for ([_]usize{ 1, 2, 7, 64 }) |fragment| {
+ try pair.transfer(true, request, fragment);
+ try pair.transfer(false, response, fragment);
+ try pair.transfer(true, read_request, fragment);
+ try pair.transfer(false, read_response, fragment);
+ }
+ try pair.finish();
+ }
+ }
+}
+
+test "QUIC backpressure retries a moved prefix while new replies are appended" {
+ var listener = try Listener.init(try .parse("127.0.0.1", 0));
+ defer listener.deinit();
+ var pair = try TestPair.init(&listener);
+ defer pair.deinit();
+ try pair.transfer(true, "hello", 1);
+ const bytes: [8192]u8 = @splat(0x5a);
+ var total: usize = 0;
+ const deadline = testNow() + 3000;
+ while (total < 64 * 1024 * 1024) {
+ const written = try pair.client.write(&bytes);
+ total += written;
+ if (written == 0) break;
+ try pair.listener.events();
+ try pair.client.events();
+ if (testNow() >= deadline) return error.Deadline;
+ }
+ try std.testing.expect(total > 0 and total < 64 * 1024 * 1024);
+ try std.testing.expectEqual(bytes.len, pair.client.pending_write_len);
+ try std.testing.expectError(error.InvalidWrite, pair.client.write(bytes[0..1]));
+ var buffer: [8192]u8 = undefined;
+ var received: usize = 0;
+ // A pending SSL write may already have delivered a prefix before it
+ // reports completion. Leave that prefix for the retry check below.
+ while (received < total) {
+ if (try pair.server.?.read(buffer[0..@min(buffer.len, total - received)])) |n| {
+ try std.testing.expect(n > 0);
+ try std.testing.expect(std.mem.allEqual(u8, buffer[0..n], 0x5a));
+ received += n;
+ } else try pair.wait(deadline);
+ }
+ try std.testing.expectEqual(total, received);
+ var moved: [8192 + 7]u8 = undefined;
+ @memcpy(moved[0..bytes.len], &bytes);
+ @memset(moved[bytes.len..], 0x6b);
+ while (true) {
+ const n = try pair.client.write(&moved);
+ if (n != 0) {
+ try std.testing.expectEqual(bytes.len, n);
+ break;
+ }
+ try pair.wait(deadline);
+ }
+ received = 0;
+ while (received < bytes.len) {
+ if (try pair.server.?.read(&buffer)) |n| {
+ try std.testing.expect(n > 0);
+ try std.testing.expect(std.mem.allEqual(u8, buffer[0..n], 0x5a));
+ received += n;
+ } else try pair.wait(deadline);
+ }
+ try std.testing.expectEqual(bytes.len, received);
+ try pair.transfer(true, moved[bytes.len..], 7);
+ try pair.finish();
+}
+
+test "QUIC owner can enforce handshake and read deadlines without busy polling" {
+ var listener = try Listener.init(try .parse("127.0.0.1", 0));
+ defer listener.deinit();
+ var client = try Connection.dial(listener.address);
+ defer client.deinit();
+ const handshake_deadline = testNow() + 100;
+ var turns: usize = 0;
+ while (testNow() < handshake_deadline) {
+ try std.testing.expect(!try client.handshake());
+ try client.events();
+ var timeout: i32 = @intCast(@max(0, handshake_deadline - testNow()));
+ if (client.nextDue()) |ms| timeout = @min(timeout, ms);
+ var fds = [_]libc.pollfd{client.poll().?};
+ const rc = libc.poll(&fds, fds.len, timeout);
+ if (rc < 0 and libc.errno(rc) != .INTR) return error.Poll;
+ turns += 1;
+ }
+ try std.testing.expect(turns < 100);
+ var serving = try Listener.init(try .parse("127.0.0.1", 0));
+ defer serving.deinit();
+ var pair = try TestPair.init(&serving);
+ defer pair.deinit();
+ try pair.transfer(true, "request", 2);
+ var buffer: [32]u8 = undefined;
+ const read_deadline = testNow() + 100;
+ turns = 0;
+ while (true) {
+ try std.testing.expect(try pair.client.read(&buffer) == null);
+ pair.wait(read_deadline) catch |err| {
+ try std.testing.expectEqual(error.Deadline, err);
+ break;
+ };
+ turns += 1;
+ }
+ try std.testing.expect(turns < 100);
+}
+
+test "QUIC bind failure leaves the existing listener usable" {
+ var listener = try Listener.init(try .parse("127.0.0.1", 0));
+ defer listener.deinit();
+ for (0..8) |_| try std.testing.expectError(error.Bind, Listener.init(listener.address));
+ var pair = try TestPair.init(&listener);
+ defer pair.deinit();
+ try pair.transfer(true, "still listening", 3);
+ try pair.transfer(false, "still serving", 2);
+ try pair.finish();
+}
+
+test "QUIC pending reports buffered bytes and EOF without UDP readiness" {
+ var listener = try Listener.init(try .parse("127.0.0.1", 0));
+ defer listener.deinit();
+ var pair = try TestPair.init(&listener);
+ defer pair.deinit();
+ try std.testing.expect(!pair.server.?.pending());
+ try std.testing.expectEqual(@as(usize, 3), try pair.client.write("abc"));
+ const deadline = testNow() + 3000;
+ while (!pair.server.?.pending()) try pair.wait(deadline);
+ var buffer: [3]u8 = undefined;
+ try std.testing.expectEqual(@as(?usize, 1), try pair.server.?.read(buffer[0..1]));
+ try std.testing.expectEqual(@as(u8, 'a'), buffer[0]);
+ try listener.events();
+ try pair.client.events();
+ try std.testing.expect(pair.server.?.pending());
+ try std.testing.expectEqual(@as(?usize, 2), try pair.server.?.read(buffer[1..]));
+ try std.testing.expectEqualStrings("abc", &buffer);
+ try std.testing.expect(!pair.server.?.pending());
+ try pair.client.conclude();
+ while (!pair.server.?.pending()) try pair.wait(deadline);
+ try std.testing.expectEqual(@as(?usize, 0), try pair.server.?.read(&buffer));
+}
+
+test "QUIC moved listener retains its in-memory identity" {
+ var original = try Listener.init(try .parse("127.0.0.1", 0));
+ var listener = original;
+ original = undefined;
+ defer listener.deinit();
+ var pair = try TestPair.init(&listener);
+ defer pair.deinit();
+ try pair.transfer(true, "moved listener", 2);
+ try pair.transfer(false, "same identity", 3);
+ try pair.finish();
+}
diff --git a/test/transport.zig b/test/transport.zig
new file mode 100644
index 0000000..3d43ddb
--- /dev/null
+++ b/test/transport.zig
@@ -0,0 +1,82 @@
+const std = @import("std");
+const c9 = @import("cloud9");
+const t = c9.transport;
+const testing = std.testing;
+
+test "standard readers and writers preserve frame boundaries" {
+ var bytes: [128]u8 = undefined;
+ const frame = try c9.encode(.{ .tversion = .{ .msize = 4096, .version = "9P2000" } }, c9.notag, &bytes);
+ var source: testing.Reader = .init(&.{}, &.{.{ .buffer = frame }});
+ source.artificial_limit = .limited(1);
+ var target: [128]u8 = undefined;
+ const got = try t.readFrame(&source.interface, &target, 4096);
+ try testing.expectEqualSlices(u8, frame, got);
+ var output: [128]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&output);
+ try t.writeFrame(&writer, got, 4096);
+ try testing.expectEqualSlices(u8, frame, writer.buffered());
+}
+
+test "POSIX TCP and Unix streams handle retry and EOF" {
+ var dir = testing.tmpDir(.{});
+ defer dir.cleanup();
+ const io = testing.io;
+ var path_buffer: [std.fs.max_path_bytes]u8 = undefined;
+ const parent_len = try dir.dir.realPath(io, &path_buffer);
+ const parent = path_buffer[0..parent_len];
+ var unix_buffer: [t.sun_path_len]u8 = undefined;
+ const unix = try std.fmt.bufPrintSentinel(&unix_buffer, "{s}/9p", .{parent}, 0);
+ for ([_]t.Address{ .{ .tcp = .{ .ip4 = .loopback(0) } }, .{ .unix = unix } }) |address| {
+ const listener = try t.listenFd(address, 4);
+ defer t.close(listener);
+ var destination = address;
+ if (address == .tcp) {
+ var actual: std.c.sockaddr.in = undefined;
+ var len: std.c.socklen_t = @sizeOf(@TypeOf(actual));
+ try testing.expectEqual(@as(c_int, 0), std.c.getsockname(listener, @ptrCast(&actual), &len));
+ destination.tcp.setPort(std.mem.bigToNative(u16, actual.port));
+ }
+ const client = try t.connectFd(destination, t.nowMs() + 1000);
+ defer t.close(client);
+ const server = (try t.acceptFd(listener, address == .tcp)) orelse return error.NoConnection;
+ var buffer: [32]u8 = undefined;
+ try testing.expectEqual(@as(?usize, null), try t.read(server, &buffer));
+ try testing.expectEqual(@as(?usize, 3), try t.write(client, "abc"));
+ try t.wait(server, @intCast(std.c.POLL.IN), t.nowMs() + 1000);
+ try testing.expectEqual(@as(?usize, 3), try t.read(server, &buffer));
+ try testing.expectEqualStrings("abc", buffer[0..3]);
+ t.close(server);
+ try t.wait(client, @intCast(std.c.POLL.IN), t.nowMs() + 1000);
+ try testing.expectEqual(@as(?usize, 0), try t.read(client, &buffer));
+ }
+}
+
+test "std.Io TCP and Unix listener and client adapters" {
+ var dir = testing.tmpDir(.{});
+ defer dir.cleanup();
+ const io = testing.io;
+ var path_buffer: [std.fs.max_path_bytes]u8 = undefined;
+ const parent_len = try dir.dir.realPath(io, &path_buffer);
+ const parent = path_buffer[0..parent_len];
+ var unix_buffer: [t.sun_path_len]u8 = undefined;
+ const unix = try std.fmt.bufPrintSentinel(&unix_buffer, "{s}/9p", .{parent}, 0);
+ for ([_]t.Address{ .{ .tcp = .{ .ip4 = .loopback(0) } }, .{ .unix = unix } }) |address| {
+ var listener = try t.listen(io, address, 4);
+ defer listener.deinit(io);
+ const destination: t.Address = if (address == .tcp) .{ .tcp = listener.socket.address } else address;
+ const client = try t.connect(io, destination);
+ defer client.close(io);
+ const server = try listener.accept(io);
+ defer server.close(io);
+ var output: [128]u8 = undefined;
+ var writer = client.writer(io, &output);
+ var bytes: [128]u8 = undefined;
+ const frame = try c9.encode(.rflush, 1, &bytes);
+ try t.writeFrame(&writer.interface, frame, 4096);
+ try writer.interface.flush();
+ var input: [128]u8 = undefined;
+ var reader = server.reader(io, &input);
+ var target: [128]u8 = undefined;
+ try testing.expectEqualSlices(u8, frame, try t.readFrame(&reader.interface, &target, 4096));
+ }
+}