mirror of
https://github.com/soypat/lneto.git
synced 2026-08-08 08:53:40 +00:00
document all the things; improve req Query semantics; add Form.EnableBufferGrowth
This commit is contained in:
@@ -69,7 +69,7 @@ func BenchmarkHandle(b *testing.B) {
|
||||
handler: func(ex *Exchange) {
|
||||
ex.StageHeader("Content-Type", "text/plain")
|
||||
ex.StageHeaderInt("Content-Length", int64(len(benchBody)), 10)
|
||||
data, present := ex.AppendQuery(buf[:0], "abc", true)
|
||||
data, present := ex.RequestQueryAppend(buf[:0], "abc", true)
|
||||
if !present || !internal.BytesEqual(data, expect) {
|
||||
panic("invalid result")
|
||||
}
|
||||
|
||||
@@ -62,15 +62,19 @@ func ExampleMuxSlice_query_forms_multipart() {
|
||||
|
||||
mux.Handle("/users/{id}", func(ex *httphi.Exchange) {
|
||||
userID := ex.PathValue("id")
|
||||
fmt.Printf("someone requested data for user %s", userID)
|
||||
fmt.Printf("someone requested data for user %s\n", userID)
|
||||
})
|
||||
|
||||
mux.Handle("/query", func(ex *httphi.Exchange) {
|
||||
// query parameter in URL.
|
||||
const decodeQuery = true
|
||||
const queryKey = "search"
|
||||
queryValue, present := ex.AppendQuery(nil, queryKey, decodeQuery)
|
||||
fmt.Printf("got query=%v %s=%s", present, queryKey, queryValue)
|
||||
valueRaw, present := ex.RequestQueryValue(queryKey)
|
||||
if !present {
|
||||
return
|
||||
}
|
||||
valueDecoded, present := ex.RequestQueryAppend(nil, queryKey, decodeQuery)
|
||||
fmt.Printf("got query=%v %s=%s (raw:%s)\n", present, queryKey, valueDecoded, valueRaw)
|
||||
})
|
||||
|
||||
mux.Handle("GET /form", func(ex *httphi.Exchange) {
|
||||
|
||||
+56
-39
@@ -49,11 +49,24 @@ type Exchange struct {
|
||||
readErr error
|
||||
}
|
||||
|
||||
// ExchangeConfig is the memory an [Exchange] is fixed to for the rest of its
|
||||
// life by [Exchange.Configure]. A [Router] derives one per exchange from its
|
||||
// [RouterConfig], which is what bounds the router's memory.
|
||||
type ExchangeConfig struct {
|
||||
RawBuf []byte
|
||||
RequestBufferLim int
|
||||
NumHeaderKVCap int
|
||||
// RawBuf is the single buffer holding the request header, the response
|
||||
// header and any surplus body. See [Exchange.UnsafeRawBuffer].
|
||||
RawBuf []byte
|
||||
// RequestBufferLim reserves the first bytes of RawBuf for the request
|
||||
// header, the rest being the response. Configure panics if it exceeds RawBuf.
|
||||
RequestBufferLim int
|
||||
// NumHeaderKVCap is how many request header fields may be parsed. A request
|
||||
// carrying more is answered 431, see [httpraw.ErrHeaderTooMany].
|
||||
NumHeaderKVCap int
|
||||
// NormalizeOutgoingKeys normalizes staged response header keys as they are
|
||||
// written, i.e: "content-type" becomes "Content-Type".
|
||||
NormalizeOutgoingKeys bool
|
||||
// NoRequestBufferGrowth holds the request header to RequestBufferLim rather
|
||||
// than growing it. A header outgrowing it is answered 431, see [httpraw.ErrBufferExhausted].
|
||||
NoRequestBufferGrowth bool
|
||||
// MaxPathValues is how many wildcards a single pattern may bind, read back with
|
||||
// [Exchange.PathValue]. A pattern with more never matches, see [SetPathValues].
|
||||
@@ -443,7 +456,7 @@ func (exch *Exchange) RequestContentLength() (_ int64, present bool, _ error) {
|
||||
// left as they arrived, call [httpraw.Form.Decode] to decode them in place.
|
||||
//
|
||||
// Unlike http.Request.ParseForm the query string is not folded in, reach it with
|
||||
// [Exchange.RequestQuery] or [Exchange.AppendQuery]. The body is consumed, so
|
||||
// [Exchange.RequestQuery] or [Exchange.RequestQueryAppend]. The body is consumed, so
|
||||
// call this before [Exchange.ReadBody].
|
||||
//
|
||||
// A request with no Content-Length has no body, RFC 9112 6.3, and yields an
|
||||
@@ -615,47 +628,51 @@ func (exch *Exchange) RequestQuery() []byte {
|
||||
return exch.RequestHeaderRaw().RequestQuery()
|
||||
}
|
||||
|
||||
// AppendQuery appends the value of the first query parameter matching key to
|
||||
// dst and reports whether the parameter was present. A parameter with no value
|
||||
// ("?debug") and one with an empty value ("?debug=") are both present with
|
||||
// nothing appended.
|
||||
// RequestQueryValue returns an undecoded view of the first query parameter
|
||||
// matching key and reports whether it was present. Keys are matched decoded, so
|
||||
// key "a b" finds "a%20b" and "a+b"; a parameter whose key is a malformed
|
||||
// escape is skipped. A parameter with no value ("?debug") and one with an empty
|
||||
// value ("?debug=") are both present with a zero length view.
|
||||
//
|
||||
// Keys are matched decoded, so key "a b" finds "a%20b" and "a+b". Values are
|
||||
// appended raw unless decoded is set, in which case percent escapes and '+' are
|
||||
// decoded. A parameter whose value fails to decode is reported absent, and a
|
||||
// parameter whose key fails to decode is skipped.
|
||||
//
|
||||
// dst doubles as scratch space for decoding candidate keys, so AppendQuery only
|
||||
// allocates when dst lacks the capacity to hold the longest key it inspects.
|
||||
func (exch *Exchange) AppendQuery(dst []byte, key string, decoded bool) (valueAppended []byte, present bool) {
|
||||
// The view aliases the request buffer, so copy it to outlive the handler or use
|
||||
// [Exchange.RequestQueryAppend] to decode it out.
|
||||
func (exch *Exchange) RequestQueryValue(key string) (rawValue []byte, present bool) {
|
||||
const plusAsSpace = true // Query strings are form encoded, unlike paths.
|
||||
base := len(dst)
|
||||
rawkey, rawval, rest := httpraw.NextQueryPair(exch.RequestQuery())
|
||||
for ; rawkey != nil; rawkey, rawval, rest = httpraw.NextQueryPair(rest) {
|
||||
if b2s(rawkey) != key {
|
||||
// Key may be encoded: decode it over dst's free space and compare.
|
||||
// A decoded key cannot appear raw, so this cannot alias a real key.
|
||||
dst = slices.Grow(dst, len(rawkey))
|
||||
scratch := dst[base : base+len(rawkey)]
|
||||
n, err := httpraw.CopyDecodedPercentURL(scratch, rawkey, plusAsSpace)
|
||||
if err != nil || b2s(scratch[:n]) != key {
|
||||
continue // Malformed or different key, keep looking.
|
||||
}
|
||||
// Compare raw first: a key needing no decoding is the common case, and
|
||||
// the decoding compare walks the key an escape at a time.
|
||||
if b2s(rawkey) == key || httpraw.EqualDecodedPercentURL(rawkey, key, plusAsSpace) {
|
||||
return rawval, true
|
||||
}
|
||||
if len(rawval) == 0 {
|
||||
return dst[:base], true // Flag or empty value, nothing to append.
|
||||
}
|
||||
dst = slices.Grow(dst, len(rawval))
|
||||
if !decoded {
|
||||
return append(dst[:base], rawval...), true
|
||||
}
|
||||
n, err := httpraw.CopyDecodedPercentURL(dst[base:base+len(rawval)], rawval, plusAsSpace)
|
||||
if err != nil {
|
||||
return dst[:base], false // Do not hand back half a decode.
|
||||
}
|
||||
return dst[:base+n], true
|
||||
}
|
||||
return dst[:base], false
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// RequestQueryAppend appends the value of the first query parameter matching key to
|
||||
// dst and reports whether the parameter was present, matching keys as
|
||||
// [Exchange.RequestQueryValue] does. A parameter with no value ("?debug") and
|
||||
// one with an empty value ("?debug=") are both present with nothing appended.
|
||||
//
|
||||
// Values are appended raw unless decoded is set, in which case percent escapes
|
||||
// and '+' are decoded. A parameter whose value fails to decode is reported
|
||||
// absent, dst being left as it was rather than holding half a decode.
|
||||
func (exch *Exchange) RequestQueryAppend(dst []byte, key string, decoded bool) (valueAppended []byte, present bool) {
|
||||
const plusAsSpace = true // Query strings are form encoded, unlike paths.
|
||||
rawval, present := exch.RequestQueryValue(key)
|
||||
if !present || len(rawval) == 0 {
|
||||
return dst, present
|
||||
}
|
||||
if !decoded {
|
||||
return append(dst, rawval...), true
|
||||
}
|
||||
base := len(dst)
|
||||
dst = slices.Grow(dst, len(rawval))
|
||||
n, err := httpraw.CopyDecodedPercentURL(dst[base:base+len(rawval)], rawval, plusAsSpace)
|
||||
if err != nil {
|
||||
return dst[:base], false // Do not hand back half a decode.
|
||||
}
|
||||
return dst[:base+n], true
|
||||
}
|
||||
|
||||
// RequestMethod returns the request line's method, i.e: "GET". See
|
||||
|
||||
@@ -671,7 +671,7 @@ func TestExchangeAppendQuery(t *testing.T) {
|
||||
var present bool
|
||||
sm.Handle("/x", func(ex *Exchange) {
|
||||
var value []byte
|
||||
value, present = ex.AppendQuery(nil, test.key, test.decoded)
|
||||
value, present = ex.RequestQueryAppend(nil, test.key, test.decoded)
|
||||
got = string(value)
|
||||
})
|
||||
serve(t, "GET "+test.uri+" HTTP/1.1\r\nHost: h\r\n\r\n", &sm)
|
||||
@@ -696,7 +696,7 @@ func TestExchangeAppendQueryReusesBuffer(t *testing.T) {
|
||||
sm.Handle("/x", func(ex *Exchange) {
|
||||
var value []byte
|
||||
allocs = testing.AllocsPerRun(50, func() {
|
||||
value, _ = ex.AppendQuery(dst[:len("prefix:")], "q", true)
|
||||
value, _ = ex.RequestQueryAppend(dst[:len("prefix:")], "q", true)
|
||||
})
|
||||
got = string(value) // Conversion allocates, keep it out of the measurement.
|
||||
})
|
||||
|
||||
@@ -242,7 +242,7 @@ func FuzzQueryAndForm(f *testing.F) {
|
||||
if n > len(key) {
|
||||
t.Fatalf("decoding key %q grew it to %d bytes", key, n)
|
||||
}
|
||||
if _, present := exch.AppendQuery(nil, string(dec[:n]), decoded); !present {
|
||||
if _, present := exch.RequestQueryAppend(nil, string(dec[:n]), decoded); !present {
|
||||
t.Fatalf("query pair %q absent from AppendQuery", key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,12 @@ type Cookie struct {
|
||||
kv KVBuffer
|
||||
}
|
||||
|
||||
// EnableBufferGrowth allows the cookie's buffer to grow past what [Cookie.Reset] was
|
||||
// handed. See [KVBuffer.EnableBufferGrowth].
|
||||
func (c *Cookie) EnableBufferGrowth(enableBufferGrowth bool) {
|
||||
c.kv.EnableBufferGrowth(enableBufferGrowth)
|
||||
}
|
||||
|
||||
// Reset functions very similarly to [Header.Reset]. Can be used for in-place cookie parsing.
|
||||
func (c *Cookie) Reset(buf []byte, capKV int) { c.kv.Reset(buf, capKV) }
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ type Form struct {
|
||||
kv KVBuffer
|
||||
}
|
||||
|
||||
// EnableBufferGrowth allows the form's buffer to grow past what [Form.Reset] was
|
||||
// handed. See [KVBuffer.EnableBufferGrowth].
|
||||
func (f *Form) EnableBufferGrowth(enableGrowth bool) { f.kv.EnableBufferGrowth(enableGrowth) }
|
||||
|
||||
// Reset discards parsed pairs and sets the buffer to parse in place.
|
||||
|
||||
@@ -634,6 +634,42 @@ func CopyDecodedPercentURL(dst, value []byte, plusAsSpace bool) (n int, err erro
|
||||
}
|
||||
}
|
||||
|
||||
// EqualDecodedPercentURL reports whether value, once decoded, equals want. It
|
||||
// decodes as it compares so it needs no scratch buffer, and reports false on a
|
||||
// malformed escape just as [CopyDecodedPercentURL] errors on one.
|
||||
// plusAsSpace decodes '+' to ' ', correct for query and form-encoded data but
|
||||
// NOT for path segments.
|
||||
func EqualDecodedPercentURL(value []byte, want string, plusAsSpace bool) bool {
|
||||
w := 0
|
||||
for i := 0; i < len(value); {
|
||||
var c byte
|
||||
switch {
|
||||
case value[i] == '%':
|
||||
if i+2 >= len(value) {
|
||||
return false // Truncated escape at end of value.
|
||||
}
|
||||
hi, okhi := unhexdigit(value[i+1])
|
||||
lo, oklo := unhexdigit(value[i+2])
|
||||
if !okhi || !oklo {
|
||||
return false
|
||||
}
|
||||
c = hi<<4 | lo
|
||||
i += 3
|
||||
case plusAsSpace && value[i] == '+':
|
||||
c = ' '
|
||||
i++
|
||||
default:
|
||||
c = value[i]
|
||||
i++
|
||||
}
|
||||
if w >= len(want) || want[w] != c {
|
||||
return false
|
||||
}
|
||||
w++
|
||||
}
|
||||
return w == len(want)
|
||||
}
|
||||
|
||||
// copyPlusDecoded copies src to dst replacing '+' with ' ' if plusAsSpace set.
|
||||
func copyPlusDecoded(dst, src []byte, plusAsSpace bool) int {
|
||||
n := copy(dst, src)
|
||||
|
||||
@@ -653,3 +653,51 @@ func TestHeader_FieldTableFullIsReported(t *testing.T) {
|
||||
t.Fatalf("want ErrHeaderFieldsTooLarge, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// EqualDecodedPercentURL must agree with CopyDecodedPercentURL on every input:
|
||||
// same decoded bytes, and false wherever the copying decoder reports an error.
|
||||
func TestEqualDecodedPercentURL(t *testing.T) {
|
||||
for _, value := range []string{
|
||||
"", "plain", "a+b", "a%20b", "%41%42", "100%25", "a%2Fb", "+", "%2b",
|
||||
"trailing%", "trailing%4", "%zz", "a%2", "%%", "a+b%20c", "%00",
|
||||
} {
|
||||
for _, plusAsSpace := range []bool{false, true} {
|
||||
dst := make([]byte, len(value))
|
||||
n, err := CopyDecodedPercentURL(dst, []byte(value), plusAsSpace)
|
||||
// The copying decoder is the reference: whatever it produces is what
|
||||
// an equal comparison must accept, and only that.
|
||||
want := ""
|
||||
if err == nil {
|
||||
want = string(dst[:n])
|
||||
}
|
||||
got := EqualDecodedPercentURL([]byte(value), want, plusAsSpace)
|
||||
if err != nil {
|
||||
if got {
|
||||
t.Errorf("%q plus=%v: malformed escape must not compare equal", value, plusAsSpace)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !got {
|
||||
t.Errorf("%q plus=%v: want equal to its own decoding %q", value, plusAsSpace, want)
|
||||
}
|
||||
if EqualDecodedPercentURL([]byte(value), want+"x", plusAsSpace) {
|
||||
t.Errorf("%q plus=%v: must not equal a longer want", value, plusAsSpace)
|
||||
}
|
||||
if want != "" && EqualDecodedPercentURL([]byte(value), want[:len(want)-1], plusAsSpace) {
|
||||
t.Errorf("%q plus=%v: must not equal a shorter want", value, plusAsSpace)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The comparison must not allocate: it is the reason a query lookup can return
|
||||
// a view without scratch space.
|
||||
func TestEqualDecodedPercentURLNoAlloc(t *testing.T) {
|
||||
value := []byte("a%20long%2Dish+key")
|
||||
allocs := testing.AllocsPerRun(100, func() {
|
||||
EqualDecodedPercentURL(value, "a long-ish key", true)
|
||||
})
|
||||
if allocs != 0 {
|
||||
t.Fatalf("EqualDecodedPercentURL allocated %v times, want 0", allocs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/soypat/lneto/internal"
|
||||
)
|
||||
|
||||
// KVBuffer is a common key-value store engine for Cookie, Form, and other HTTP abstractions that need
|
||||
// KVBuffer is a common key-value store engine for Cookie, Form, Header and other HTTP abstractions that need
|
||||
// a key-value store with underlying buffer memory.
|
||||
type KVBuffer struct {
|
||||
buf []byte
|
||||
@@ -18,8 +18,12 @@ type KVBuffer struct {
|
||||
|
||||
func (mb *KVBuffer) free() int { return cap(mb.buf) - len(mb.buf) }
|
||||
|
||||
// BufferRaw returns the underlying buffer, its length being the portion in use.
|
||||
// Stored pairs alias it, so writing to it mangles them.
|
||||
func (mb *KVBuffer) BufferRaw() []byte { return mb.buf }
|
||||
|
||||
// EnableBufferGrowth allows the buffer to grow past the memory [KVBuffer.Reset]
|
||||
// was handed. The setting outlives Reset; with growth off callers get [ErrBufferExhausted].
|
||||
func (mb *KVBuffer) EnableBufferGrowth(enableGrowth bool) {
|
||||
if enableGrowth {
|
||||
mb.flags &^= flagNoBufferGrow
|
||||
@@ -30,8 +34,11 @@ func (mb *KVBuffer) EnableBufferGrowth(enableGrowth bool) {
|
||||
|
||||
func (mb *KVBuffer) discardKVs() { mb.kvs = mb.kvs[:0] }
|
||||
|
||||
// BufferGrowthEnabled reports whether the buffer may grow, see [KVBuffer.EnableBufferGrowth].
|
||||
func (mb *KVBuffer) BufferGrowthEnabled() bool { return !mb.flags.HasAny(flagNoBufferGrow) }
|
||||
|
||||
// ReadFromBytes appends buf to the underlying buffer, accumulating data to parse.
|
||||
// Returns [ErrBufferExhausted] when buf does not fit and growth is disabled.
|
||||
func (mb *KVBuffer) ReadFromBytes(buf []byte) error {
|
||||
if len(buf) == 0 {
|
||||
return io.ErrNoProgress // Nothing handed over, not a buffer problem.
|
||||
@@ -48,6 +55,8 @@ func (mb *KVBuffer) ReadFromBytes(buf []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadLimited appends at most limit bytes read from r to the underlying buffer.
|
||||
// A read returning data alongside [io.EOF] reports a nil error, later ones io.EOF.
|
||||
func (mb *KVBuffer) ReadLimited(r io.Reader, limit int) (int, error) {
|
||||
free := mb.free()
|
||||
growthEnabled := mb.BufferGrowthEnabled()
|
||||
@@ -72,6 +81,8 @@ func (mb *KVBuffer) ReadLimited(r io.Reader, limit int) (int, error) {
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Reset discards all pairs and takes buf as the buffer to parse in place, nil
|
||||
// reusing the current one. kvCap sizes the pair table. Only the growth setting survives.
|
||||
func (mb *KVBuffer) Reset(buf []byte, kvCap int) {
|
||||
if buf == nil {
|
||||
mb.buf = mb.buf[:0]
|
||||
@@ -82,6 +93,8 @@ func (mb *KVBuffer) Reset(buf []byte, kvCap int) {
|
||||
mb.flags = mb.flags & flagNoBufferGrow // Only flag persisted is buffer grow config.
|
||||
}
|
||||
|
||||
// CopyFrom replaces the receiver's contents with a copy of src, sharing no
|
||||
// memory with it afterwards.
|
||||
func (mb *KVBuffer) CopyFrom(src *KVBuffer) {
|
||||
mb.buf = append(mb.buf[:0], src.buf...)
|
||||
mb.kvs = append(mb.kvs[:0], src.kvs...)
|
||||
@@ -124,6 +137,9 @@ func (mb *KVBuffer) HasKeyValue(key, value string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Add appends a pair, keeping any already sharing the key: use [KVBuffer.Set]
|
||||
// to replace instead. Reports false if the buffer could not hold it.
|
||||
func (mb *KVBuffer) Add(key, value string) (enoughSpace bool) {
|
||||
mb.appendPair(key, value)
|
||||
return mb.getIdx(key) >= 0
|
||||
@@ -215,7 +231,11 @@ func (mb *KVBuffer) setInternal(key, value []byte) (enoughSpace bool) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Len returns the number of slots stored, counting those [KVBuffer.Set] invalidated.
|
||||
func (mb *KVBuffer) Len() int { return len(mb.kvs) }
|
||||
|
||||
// At returns the i'th pair in wire order. value is nil for a pair holding none,
|
||||
// which is what tells a form's "ok" from "ok=".
|
||||
func (mb *KVBuffer) At(i int) (key, value []byte) {
|
||||
kv := mb.kvs[i]
|
||||
if !kv.HasValue() {
|
||||
@@ -233,7 +253,10 @@ func (mb *KVBuffer) setAt(i int, k, v []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
// AtKey is [KVBuffer.At] limited to the i'th key.
|
||||
func (mb *KVBuffer) AtKey(i int) (key []byte) { return mb.musttoken(mb.kvs[i].key) }
|
||||
|
||||
// AtValue is [KVBuffer.At] limited to the i'th value, nil when the pair holds none.
|
||||
func (mb *KVBuffer) AtValue(i int) (key []byte) {
|
||||
if !mb.kvs[i].HasValue() {
|
||||
return nil
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
//go:build !tinygo && linux
|
||||
|
||||
package rawsock
|
||||
|
||||
import (
|
||||
"net"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestAcceptConnDoesNotAllocate pins down what AcceptConn exists for. A server
|
||||
// that owns its connection storage still pays an allocation per connection if
|
||||
// accepting one allocates, which is what syscall.Accept does with the peer
|
||||
// address it returns.
|
||||
func TestAcceptConnDoesNotAllocate(t *testing.T) {
|
||||
var ln Listener
|
||||
err := ln.Listen(0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
addr := ln.Addr().String()
|
||||
|
||||
const n = 32
|
||||
dialed := make([]net.Conn, 0, n)
|
||||
defer func() {
|
||||
for _, c := range dialed {
|
||||
c.Close()
|
||||
}
|
||||
}()
|
||||
for range n {
|
||||
c, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dialed = append(dialed, c)
|
||||
}
|
||||
|
||||
var conn Conn
|
||||
// The first accept warms whatever the runtime wants to warm, and is where
|
||||
// the peer address is checked: reading it hands a value to a net.Addr
|
||||
// interface, which allocates whatever the accept did.
|
||||
if err = ln.AcceptConn(&conn); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if conn.RemoteAddr().String() != dialed[0].LocalAddr().String() {
|
||||
t.Errorf("accepted peer %s, dialer says %s", conn.RemoteAddr(), dialed[0].LocalAddr())
|
||||
}
|
||||
conn.Close()
|
||||
|
||||
var before, after runtime.MemStats
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&before)
|
||||
for i := 1; i < n; i++ {
|
||||
if err = ln.AcceptConn(&conn); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
conn.Close()
|
||||
}
|
||||
runtime.ReadMemStats(&after)
|
||||
if allocs := after.Mallocs - before.Mallocs; allocs != 0 {
|
||||
t.Errorf("AcceptConn allocated %d times over %d accepts, want 0", allocs, n-1)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user