From be8ef2c5b4c0fead374f23065768e43fc9af6fa6 Mon Sep 17 00:00:00 2001 From: Patricio Whittingslow Date: Sat, 4 Jul 2026 11:56:36 -0300 Subject: [PATCH] add tests --- dnserror_test.go | 42 +++++++++++++ lookup_test.go | 104 ++++++++++++++++++++++++++++++++ netdev_fake_test.go | 85 ++++++++++++++++++++++++++ tcpsock_test.go | 70 ++++++++++++++++++++++ udpsock_test.go | 143 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 444 insertions(+) create mode 100644 dnserror_test.go create mode 100644 lookup_test.go create mode 100644 netdev_fake_test.go create mode 100644 tcpsock_test.go create mode 100644 udpsock_test.go diff --git a/dnserror_test.go b/dnserror_test.go new file mode 100644 index 0000000..65ed043 --- /dev/null +++ b/dnserror_test.go @@ -0,0 +1,42 @@ +// TINYGO test for DNSError. + +package net + +import ( + "errors" + "testing" +) + +func TestDNSErrorError(t *testing.T) { + tests := []struct { + name string + e *DNSError + want string + }{ + {"nil", nil, ""}, + {"no server", &DNSError{Err: "no such host", Name: "example.com"}, + "lookup example.com: no such host"}, + {"with server", &DNSError{Err: "timeout", Name: "example.com", Server: "8.8.8.8:53"}, + "lookup example.com on 8.8.8.8:53: timeout"}, + } + for _, tt := range tests { + if got := tt.e.Error(); got != tt.want { + t.Errorf("%s: Error() = %q, want %q", tt.name, got, tt.want) + } + } +} + +func TestDNSErrorUnwrap(t *testing.T) { + inner := errors.New("boom") + e := &DNSError{Err: "x", UnwrapErr: inner} + if got := e.Unwrap(); got != inner { + t.Errorf("Unwrap() = %v, want %v", got, inner) + } + if !errors.Is(e, inner) { + t.Errorf("errors.Is(e, inner) = false, want true") + } + + if (&DNSError{Err: "x"}).Unwrap() != nil { + t.Errorf("Unwrap() with no UnwrapErr should be nil") + } +} diff --git a/lookup_test.go b/lookup_test.go new file mode 100644 index 0000000..78c9bc1 --- /dev/null +++ b/lookup_test.go @@ -0,0 +1,104 @@ +// TINYGO tests for the netdev-backed Resolver lookups. + +package net + +import ( + "context" + "errors" + "net/netip" + "testing" +) + +func TestResolverLookupHost(t *testing.T) { + ip := netip.MustParseAddr("1.2.3.4") + withNetdev(t, &fakeNetdev{hostIP: ip}) + + addrs, err := DefaultResolver.LookupHost(context.Background(), "example.com") + if err != nil { + t.Fatalf("LookupHost: unexpected error %v", err) + } + if len(addrs) != 1 || addrs[0] != "1.2.3.4" { + t.Fatalf("LookupHost = %v, want [1.2.3.4]", addrs) + } +} + +func TestResolverLookupHostEmpty(t *testing.T) { + withNetdev(t, &fakeNetdev{}) + + _, err := DefaultResolver.LookupHost(context.Background(), "") + var de *DNSError + if !errors.As(err, &de) || !de.IsNotFound { + t.Fatalf("LookupHost(\"\") = %v, want *DNSError with IsNotFound", err) + } +} + +func TestResolverLookupHostError(t *testing.T) { + withNetdev(t, &fakeNetdev{hostErr: errors.New("dns down")}) + + _, err := DefaultResolver.LookupHost(context.Background(), "example.com") + var de *DNSError + if !errors.As(err, &de) { + t.Fatalf("LookupHost error = %T, want *DNSError", err) + } + if de.Name != "example.com" { + t.Errorf("DNSError.Name = %q, want example.com", de.Name) + } +} + +func TestResolverLookupIPAddr(t *testing.T) { + ip := netip.MustParseAddr("10.0.0.5") + withNetdev(t, &fakeNetdev{hostIP: ip}) + + addrs, err := DefaultResolver.LookupIPAddr(context.Background(), "host") + if err != nil { + t.Fatalf("LookupIPAddr: %v", err) + } + if len(addrs) != 1 || !addrs[0].IP.Equal(IPv4(10, 0, 0, 5)) { + t.Fatalf("LookupIPAddr = %v, want [10.0.0.5]", addrs) + } +} + +func TestResolverLookupNetIP(t *testing.T) { + ip := netip.MustParseAddr("10.0.0.6") + withNetdev(t, &fakeNetdev{hostIP: ip}) + + addrs, err := DefaultResolver.LookupNetIP(context.Background(), "ip", "host") + if err != nil { + t.Fatalf("LookupNetIP: %v", err) + } + if len(addrs) != 1 || addrs[0] != ip { + t.Fatalf("LookupNetIP = %v, want [%v]", addrs, ip) + } +} + +func TestLookupIP(t *testing.T) { + ip := netip.MustParseAddr("192.168.1.1") + withNetdev(t, &fakeNetdev{hostIP: ip}) + + ips, err := LookupIP("host") + if err != nil { + t.Fatalf("LookupIP: %v", err) + } + if len(ips) != 1 || !ips[0].Equal(IPv4(192, 168, 1, 1)) { + t.Fatalf("LookupIP = %v, want [192.168.1.1]", ips) + } +} + +func TestLookupHostPackage(t *testing.T) { + ip := netip.MustParseAddr("172.16.0.9") + withNetdev(t, &fakeNetdev{hostIP: ip}) + + addrs, err := LookupHost("host") + if err != nil { + t.Fatalf("LookupHost: %v", err) + } + if len(addrs) != 1 || addrs[0] != "172.16.0.9" { + t.Fatalf("LookupHost = %v, want [172.16.0.9]", addrs) + } +} + +func TestDefaultResolverNonNil(t *testing.T) { + if DefaultResolver == nil { + t.Fatal("DefaultResolver is nil") + } +} diff --git a/netdev_fake_test.go b/netdev_fake_test.go new file mode 100644 index 0000000..90d7303 --- /dev/null +++ b/netdev_fake_test.go @@ -0,0 +1,85 @@ +// TINYGO test helper: an in-memory fake netdever for exercising the +// netdev-backed net APIs without real hardware/OS sockets. + +package net + +import ( + "net/netip" + "testing" + "time" +) + +// fakeNetdev embeds nopNetdev (whose methods all error) and overrides only +// the calls a given test needs. Recorded fields let tests assert on how the +// net package drove the device. +type fakeNetdev struct { + nopNetdev + + // GetHostByName control. + hostIP netip.Addr + hostErr error + + // Socket/Bind/Connect control + capture. + socketFD int + socketErr error + socketArgs [3]int + bindErr error + bindAddr netip.AddrPort + connectErr error + + // Send/Recv control + capture. + sent []byte // accumulates everything written via Send + sendErr error + recvData []byte // returned (once) by Recv + recvErr error + + closedFDs []int +} + +func (f *fakeNetdev) GetHostByName(name string) (netip.Addr, error) { + return f.hostIP, f.hostErr +} + +func (f *fakeNetdev) Socket(domain, stype, protocol int) (int, error) { + f.socketArgs = [3]int{domain, stype, protocol} + return f.socketFD, f.socketErr +} + +func (f *fakeNetdev) Bind(sockfd int, ip netip.AddrPort) error { + f.bindAddr = ip + return f.bindErr +} + +func (f *fakeNetdev) Connect(sockfd int, host string, ip netip.AddrPort) error { + return f.connectErr +} + +func (f *fakeNetdev) Send(sockfd int, buf []byte, flags int, deadline time.Time) (int, error) { + if f.sendErr != nil { + return -1, f.sendErr + } + f.sent = append(f.sent, buf...) + return len(buf), nil +} + +func (f *fakeNetdev) Recv(sockfd int, buf []byte, flags int, deadline time.Time) (int, error) { + if f.recvErr != nil { + return -1, f.recvErr + } + n := copy(buf, f.recvData) + f.recvData = f.recvData[n:] + return n, nil +} + +func (f *fakeNetdev) Close(sockfd int) error { + f.closedFDs = append(f.closedFDs, sockfd) + return nil +} + +// withNetdev installs d as the package netdev for the duration of the test. +func withNetdev(t *testing.T, d netdever) { + t.Helper() + old := netdev + netdev = d + t.Cleanup(func() { netdev = old }) +} diff --git a/tcpsock_test.go b/tcpsock_test.go new file mode 100644 index 0000000..0621f34 --- /dev/null +++ b/tcpsock_test.go @@ -0,0 +1,70 @@ +// TINYGO tests for TCPConn.ReadFrom and its generic copy helper. + +package net + +import ( + "bytes" + "io" + "strings" + "testing" +) + +func TestGenericReadFrom(t *testing.T) { + var dst bytes.Buffer + src := strings.NewReader("payload bytes") + + n, err := genericReadFrom(&dst, src) + if err != nil { + t.Fatalf("genericReadFrom: %v", err) + } + if n != int64(len("payload bytes")) { + t.Errorf("n = %d, want %d", n, len("payload bytes")) + } + if dst.String() != "payload bytes" { + t.Errorf("copied %q, want %q", dst.String(), "payload bytes") + } +} + +// readerFromSpy is an io.Writer that also implements io.ReaderFrom. If +// io.Copy ever routes through ReadFrom, the spy records it — genericReadFrom +// must hide it (via onlyWriter) to avoid recursing back into TCPConn.ReadFrom. +type readerFromSpy struct { + bytes.Buffer + readFromCalled bool +} + +func (s *readerFromSpy) ReadFrom(r io.Reader) (int64, error) { + s.readFromCalled = true + return s.Buffer.ReadFrom(r) +} + +func TestGenericReadFromHidesReaderFrom(t *testing.T) { + spy := &readerFromSpy{} + if _, err := genericReadFrom(spy, strings.NewReader("abc")); err != nil { + t.Fatalf("genericReadFrom: %v", err) + } + if spy.readFromCalled { + t.Error("onlyWriter failed to hide ReadFrom; io.Copy recursed via ReaderFrom") + } + if spy.String() != "abc" { + t.Errorf("copied %q, want abc", spy.String()) + } +} + +func TestTCPConnReadFrom(t *testing.T) { + f := &fakeNetdev{} + withNetdev(t, f) + + c := &TCPConn{fd: 1, net: "tcp"} + src := strings.NewReader("streamed over tcp") + n, err := c.ReadFrom(src) + if err != nil { + t.Fatalf("ReadFrom: %v", err) + } + if n != int64(len("streamed over tcp")) { + t.Errorf("n = %d, want %d", n, len("streamed over tcp")) + } + if string(f.sent) != "streamed over tcp" { + t.Errorf("netdev received %q, want %q", f.sent, "streamed over tcp") + } +} diff --git a/udpsock_test.go b/udpsock_test.go new file mode 100644 index 0000000..89d21e7 --- /dev/null +++ b/udpsock_test.go @@ -0,0 +1,143 @@ +// TINYGO tests for ListenUDP and the UDPConn addr-carrying I/O methods. + +package net + +import ( + "net/netip" + "testing" +) + +func TestListenUDP(t *testing.T) { + f := &fakeNetdev{socketFD: 7} + withNetdev(t, f) + + uc, err := ListenUDP("udp", &UDPAddr{IP: IPv4(0, 0, 0, 0), Port: 1234}) + if err != nil { + t.Fatalf("ListenUDP: %v", err) + } + if uc.fd != 7 { + t.Errorf("fd = %d, want 7", uc.fd) + } + if uc.net != "udp" { + t.Errorf("net = %q, want udp", uc.net) + } + if uc.laddr.Port != 1234 { + t.Errorf("laddr.Port = %d, want 1234", uc.laddr.Port) + } + if uc.raddr != nil { + t.Errorf("raddr = %v, want nil (no connect)", uc.raddr) + } + if f.socketArgs != [3]int{_AF_INET, _SOCK_DGRAM, _IPPROTO_UDP} { + t.Errorf("Socket args = %v, want AF_INET/SOCK_DGRAM/IPPROTO_UDP", f.socketArgs) + } + if f.bindAddr.Port() != 1234 { + t.Errorf("Bind port = %d, want 1234", f.bindAddr.Port()) + } +} + +func TestListenUDPBadNetwork(t *testing.T) { + withNetdev(t, &fakeNetdev{}) + if _, err := ListenUDP("tcp", nil); err == nil { + t.Fatal("ListenUDP(\"tcp\") = nil error, want error") + } +} + +func TestListenUDPEphemeralPort(t *testing.T) { + withNetdev(t, &fakeNetdev{socketFD: 3}) + uc, err := ListenUDP("udp", nil) + if err != nil { + t.Fatalf("ListenUDP: %v", err) + } + if uc.laddr.Port == 0 { + t.Error("expected an ephemeral port to be assigned, got 0") + } +} + +func TestListenUDPBindErrorClosesSocket(t *testing.T) { + f := &fakeNetdev{socketFD: 9, bindErr: errBind} + withNetdev(t, f) + + if _, err := ListenUDP("udp", &UDPAddr{Port: 5}); err != errBind { + t.Fatalf("ListenUDP err = %v, want %v", err, errBind) + } + if len(f.closedFDs) != 1 || f.closedFDs[0] != 9 { + t.Errorf("closed fds = %v, want [9]", f.closedFDs) + } +} + +var errBind = &DNSError{Err: "bind failed"} // any sentinel error + +func TestUDPConnReadFromUDP(t *testing.T) { + raddr := &UDPAddr{IP: IPv4(5, 6, 7, 8), Port: 99} + f := &fakeNetdev{recvData: []byte("hello")} + withNetdev(t, f) + + c := &UDPConn{fd: 1, net: "udp", raddr: raddr} + buf := make([]byte, 8) + n, addr, err := c.ReadFromUDP(buf) + if err != nil { + t.Fatalf("ReadFromUDP: %v", err) + } + if string(buf[:n]) != "hello" { + t.Errorf("read %q, want hello", buf[:n]) + } + if addr != raddr { + t.Errorf("source addr = %v, want %v (connected remote)", addr, raddr) + } +} + +func TestUDPConnReadFromUDPAddrPort(t *testing.T) { + raddr := &UDPAddr{IP: IPv4(5, 6, 7, 8), Port: 99} + withNetdev(t, &fakeNetdev{recvData: []byte("hi")}) + + c := &UDPConn{fd: 1, net: "udp", raddr: raddr} + n, ap, err := c.ReadFromUDPAddrPort(make([]byte, 8)) + if err != nil { + t.Fatalf("ReadFromUDPAddrPort: %v", err) + } + if n != 2 { + t.Errorf("n = %d, want 2", n) + } + if ap != raddr.AddrPort() { + t.Errorf("addrport = %v, want %v", ap, raddr.AddrPort()) + } +} + +func TestUDPConnWriteToUDP(t *testing.T) { + f := &fakeNetdev{} + withNetdev(t, f) + + c := &UDPConn{fd: 1, net: "udp"} + n, err := c.WriteToUDP([]byte("data"), &UDPAddr{IP: IPv4(1, 1, 1, 1), Port: 53}) + if err != nil { + t.Fatalf("WriteToUDP: %v", err) + } + if n != 4 || string(f.sent) != "data" { + t.Errorf("sent %q (n=%d), want data (n=4)", f.sent, n) + } +} + +func TestUDPConnWriteToUDPAddrPort(t *testing.T) { + f := &fakeNetdev{} + withNetdev(t, f) + + c := &UDPConn{fd: 1, net: "udp"} + ap := netip.AddrPortFrom(netip.MustParseAddr("2.2.2.2"), 53) + n, err := c.WriteToUDPAddrPort([]byte("xyz"), ap) + if err != nil { + t.Fatalf("WriteToUDPAddrPort: %v", err) + } + if n != 3 || string(f.sent) != "xyz" { + t.Errorf("sent %q (n=%d), want xyz (n=3)", f.sent, n) + } +} + +func TestUDPConnSetBuffersNoop(t *testing.T) { + c := &UDPConn{} + if err := c.SetReadBuffer(4096); err != nil { + t.Errorf("SetReadBuffer: %v", err) + } + if err := c.SetWriteBuffer(4096); err != nil { + t.Errorf("SetWriteBuffer: %v", err) + } +}