| // Copyright 2023 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package os_test | |
| import ( | |
| "bytes" | |
| "internal/poll" | |
| "io" | |
| "math/rand" | |
| "net" | |
| . "os" | |
| "strconv" | |
| "syscall" | |
| "testing" | |
| "time" | |
| ) | |
| func TestSendFile(t *testing.T) { | |
| sizes := []int{ | |
| 1, | |
| 42, | |
| 1025, | |
| syscall.Getpagesize() + 1, | |
| 32769, | |
| } | |
| t.Run("sendfile-to-unix", func(t *testing.T) { | |
| for _, size := range sizes { | |
| t.Run(strconv.Itoa(size), func(t *testing.T) { | |
| testSendFile(t, "unix", int64(size)) | |
| }) | |
| } | |
| }) | |
| t.Run("sendfile-to-tcp", func(t *testing.T) { | |
| for _, size := range sizes { | |
| t.Run(strconv.Itoa(size), func(t *testing.T) { | |
| testSendFile(t, "tcp", int64(size)) | |
| }) | |
| } | |
| }) | |
| } | |
| func testSendFile(t *testing.T, proto string, size int64) { | |
| dst, src, recv, data, hook := newSendFileTest(t, proto, size) | |
| // Now call WriteTo (through io.Copy), which will hopefully call poll.SendFile | |
| n, err := io.Copy(dst, src) | |
| if err != nil { | |
| t.Fatalf("io.Copy error: %v", err) | |
| } | |
| // We should have called poll.Splice with the right file descriptor arguments. | |
| if n > 0 && !hook.called { | |
| t.Fatal("expected to called poll.SendFile") | |
| } | |
| if hook.called && hook.srcfd != int(src.Fd()) { | |
| t.Fatalf("wrong source file descriptor: got %d, want %d", hook.srcfd, src.Fd()) | |
| } | |
| sc, ok := dst.(syscall.Conn) | |
| if !ok { | |
| t.Fatalf("destination is not a syscall.Conn") | |
| } | |
| rc, err := sc.SyscallConn() | |
| if err != nil { | |
| t.Fatalf("destination SyscallConn error: %v", err) | |
| } | |
| if err = rc.Control(func(fd uintptr) { | |
| if hook.called && hook.dstfd != int(fd) { | |
| t.Fatalf("wrong destination file descriptor: got %d, want %d", hook.dstfd, int(fd)) | |
| } | |
| }); err != nil { | |
| t.Fatalf("destination Conn Control error: %v", err) | |
| } | |
| // Verify the data size and content. | |
| dataSize := len(data) | |
| dstData := make([]byte, dataSize) | |
| m, err := io.ReadFull(recv, dstData) | |
| if err != nil { | |
| t.Fatalf("server Conn Read error: %v", err) | |
| } | |
| if n != int64(dataSize) { | |
| t.Fatalf("data length mismatch for io.Copy, got %d, want %d", n, dataSize) | |
| } | |
| if m != dataSize { | |
| t.Fatalf("data length mismatch for net.Conn.Read, got %d, want %d", m, dataSize) | |
| } | |
| if !bytes.Equal(dstData, data) { | |
| t.Errorf("data mismatch, got %s, want %s", dstData, data) | |
| } | |
| } | |
| // newSendFileTest initializes a new test for sendfile. | |
| // | |
| // It creates source file and destination sockets, and populates the source file | |
| // with random data of the specified size. It also hooks package os' call | |
| // to poll.Sendfile and returns the hook so it can be inspected. | |
| func newSendFileTest(t *testing.T, proto string, size int64) (net.Conn, *File, net.Conn, []byte, *sendFileHook) { | |
| t.Helper() | |
| hook := hookSendFile(t) | |
| client, server := createSocketPair(t, proto) | |
| tempFile, data := createTempFile(t, size) | |
| return client, tempFile, server, data, hook | |
| } | |
| func hookSendFile(t *testing.T) *sendFileHook { | |
| h := new(sendFileHook) | |
| h.install() | |
| t.Cleanup(h.uninstall) | |
| return h | |
| } | |
| type sendFileHook struct { | |
| called bool | |
| dstfd int | |
| srcfd int | |
| remain int64 | |
| written int64 | |
| handled bool | |
| err error | |
| original func(dst *poll.FD, src int, remain int64) (int64, error, bool) | |
| } | |
| func (h *sendFileHook) install() { | |
| h.original = *PollSendFile | |
| *PollSendFile = func(dst *poll.FD, src int, remain int64) (int64, error, bool) { | |
| h.called = true | |
| h.dstfd = dst.Sysfd | |
| h.srcfd = src | |
| h.remain = remain | |
| h.written, h.err, h.handled = h.original(dst, src, remain) | |
| return h.written, h.err, h.handled | |
| } | |
| } | |
| func (h *sendFileHook) uninstall() { | |
| *PollSendFile = h.original | |
| } | |
| func createTempFile(t *testing.T, size int64) (*File, []byte) { | |
| f, err := CreateTemp(t.TempDir(), "writeto-sendfile-to-socket") | |
| if err != nil { | |
| t.Fatalf("failed to create temporary file: %v", err) | |
| } | |
| t.Cleanup(func() { | |
| f.Close() | |
| }) | |
| randSeed := time.Now().Unix() | |
| t.Logf("random data seed: %d\n", randSeed) | |
| prng := rand.New(rand.NewSource(randSeed)) | |
| data := make([]byte, size) | |
| prng.Read(data) | |
| if _, err := f.Write(data); err != nil { | |
| t.Fatalf("failed to create and feed the file: %v", err) | |
| } | |
| if err := f.Sync(); err != nil { | |
| t.Fatalf("failed to save the file: %v", err) | |
| } | |
| if _, err := f.Seek(0, io.SeekStart); err != nil { | |
| t.Fatalf("failed to rewind the file: %v", err) | |
| } | |
| return f, data | |
| } |