diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-14 14:10:28 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-14 14:20:25 -0300 |
| commit | 5f24c4a2284a0af84fb9d116f7a39f6a58e42ae9 (patch) | |
| tree | 8679763439492361fe99ea01f5190e5204fa590c /test/differential | |
| download | cloud9-5f24c4a2284a0af84fb9d116f7a39f6a58e42ae9.tar.gz cloud9-5f24c4a2284a0af84fb9d116f7a39f6a58e42ae9.zip | |
Implement base 9P2000 sessions, shared transports, and conformance probes
Diffstat (limited to 'test/differential')
| -rw-r--r-- | test/differential/go.mod | 14 | ||||
| -rw-r--r-- | test/differential/go.sum | 58 | ||||
| -rw-r--r-- | test/differential/main.go | 172 | ||||
| -rw-r--r-- | test/differential/probe.zig | 59 | ||||
| -rw-r--r-- | test/differential/session.go | 45 | ||||
| -rw-r--r-- | test/differential/session.zig | 82 |
6 files changed, 430 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 }); +} |
