|
|
package capsule_test
|
|
|
|
|
|
import (
|
|
|
"bytes"
|
|
|
"context"
|
|
|
"encoding/binary"
|
|
|
"errors"
|
|
|
"io"
|
|
|
"runtime"
|
|
|
"testing"
|
|
|
|
|
|
"filippo.io/age"
|
|
|
|
|
|
datekeys "g.activething.com/go/DateKeys"
|
|
|
"g.activething.com/go/DateKeys/capsule"
|
|
|
"g.activething.com/go/DateKeys/extension"
|
|
|
"g.activething.com/go/DateKeys/internal/testkit"
|
|
|
)
|
|
|
|
|
|
// The tests of this file cover the reader of format 3 (spec v0.10): step 17,
|
|
|
// its substeps and their precedence, and step 18 (spec §29.2 to §29.7, §63).
|
|
|
|
|
|
// capsule3 describes a format 3 capsule of round 1000 to build, with edits
|
|
|
// at each level: the head, HEAD_CBOR, BODY, the plaintext with its padding,
|
|
|
// and PAYLOAD_AGE.
|
|
|
type capsule3 struct {
|
|
|
paths []string
|
|
|
contents [][]byte
|
|
|
comment, author string
|
|
|
security []byte // nil: the empty security that writers write
|
|
|
head func(h *capsule.Head)
|
|
|
headCBOR func(b []byte) []byte
|
|
|
body func(b []byte) []byte
|
|
|
plain func(p []byte) []byte
|
|
|
payload func(t *testing.T, p []byte) []byte
|
|
|
structure capsule.Policy
|
|
|
recipients []age.Recipient
|
|
|
}
|
|
|
|
|
|
// build returns the .dkc and its BODY, before any edit of the plaintext.
|
|
|
func (c capsule3) build(t *testing.T) ([]byte, []byte) {
|
|
|
t.Helper()
|
|
|
h := testkit.Head3(c.comment, c.author, c.paths, c.contents)
|
|
|
if c.head != nil {
|
|
|
c.head(h)
|
|
|
}
|
|
|
hb, err := capsule.EncodeHead(h)
|
|
|
if err != nil {
|
|
|
t.Fatal(err)
|
|
|
}
|
|
|
if c.headCBOR != nil {
|
|
|
hb = c.headCBOR(hb)
|
|
|
}
|
|
|
sec := c.security
|
|
|
if sec == nil {
|
|
|
sec = capsule.EncodeSecurity()
|
|
|
}
|
|
|
body := testkit.Body3(capsule.AreaLen, sec, hb, c.contents...)
|
|
|
if c.body != nil {
|
|
|
body = c.body(body)
|
|
|
}
|
|
|
b, err := testkit.Build{Format: capsule.Format3, Declared: c.structure, Structure: c.structure,
|
|
|
AccessRecipients: c.recipients, Plaintext: body, EditPlaintext: c.plain}.Make()
|
|
|
if err != nil {
|
|
|
t.Fatal(err)
|
|
|
}
|
|
|
if c.payload == nil {
|
|
|
return b.DKC, body
|
|
|
}
|
|
|
return testkit.Join(b.Prelude[:], b.PublicHeader, b.Sealed, c.payload(t, bytes.Clone(b.Payload))), body
|
|
|
}
|
|
|
|
|
|
type result3 struct {
|
|
|
opened *capsule.Opened
|
|
|
sink *testkit.MemorySink
|
|
|
err error
|
|
|
unread int // bytes of the .dkc that Open did not read
|
|
|
}
|
|
|
|
|
|
// open3 opens a capsule of round 1000 into sink.
|
|
|
func open3(t *testing.T, dkc []byte, sink capsule.Sink, ids ...age.Identity) result3 {
|
|
|
t.Helper()
|
|
|
o := defaultOpen(1000)
|
|
|
o.Identities, o.Sink = ids, sink
|
|
|
r := bytes.NewReader(dkc)
|
|
|
opened, err := capsule.Open(context.Background(), nil, r, o)
|
|
|
res := result3{opened: opened, err: err, unread: r.Len()}
|
|
|
if m, ok := sink.(*testkit.MemorySink); ok {
|
|
|
res.sink = m
|
|
|
}
|
|
|
return res
|
|
|
}
|
|
|
|
|
|
// chunk returns the offset in the age file p of its STREAM chunk i: after
|
|
|
// the header come a 16-byte nonce and chunks of 64 KiB, each with a 16-byte
|
|
|
// tag (C2SP age.md).
|
|
|
func chunk(t *testing.T, p []byte, i int) int {
|
|
|
t.Helper()
|
|
|
n, err := testkit.HeaderLen(p)
|
|
|
if err != nil {
|
|
|
t.Fatal(err)
|
|
|
}
|
|
|
return n + 16 + i*(64<<10+16)
|
|
|
}
|
|
|
|
|
|
// Spec §29.2 to §29.7, §63 steps 17 and 18: a format 3 capsule opens, its
|
|
|
// files reach the Sink in the order of the head, and Commit comes last.
|
|
|
func TestOpen3(t *testing.T) {
|
|
|
photo := bytes.Repeat([]byte("playa"), 30000) // three STREAM chunks
|
|
|
c := capsule3{
|
|
|
paths: []string{"fotos/playa.jpg", "nota.txt", "vacío.txt"},
|
|
|
contents: [][]byte{photo, []byte("Hola.\n"), {}},
|
|
|
comment: "Para ti ❤️", author: "Ana López",
|
|
|
}
|
|
|
dkc, body := c.build(t)
|
|
|
|
|
|
// Steps 1 to 8 need no Sink.
|
|
|
in, err := capsule.Inspect(bytes.NewReader(dkc), capsule.InspectOptions{Registry: testkit.Registry()})
|
|
|
if err != nil || in.Prelude.Format != capsule.Format3 {
|
|
|
t.Fatalf("Inspect: format %d, %v", in.Prelude.Format, err)
|
|
|
}
|
|
|
|
|
|
res := open3(t, dkc, &testkit.MemorySink{})
|
|
|
if res.err != nil {
|
|
|
t.Fatal(res.err)
|
|
|
}
|
|
|
o, s := res.opened, res.sink
|
|
|
if !s.Committed || s.Aborted {
|
|
|
t.Errorf("committed %v, aborted %v", s.Committed, s.Aborted)
|
|
|
}
|
|
|
for i, want := range c.contents {
|
|
|
if !bytes.Equal(s.Files[i], want) {
|
|
|
t.Errorf("file %d: %d bytes, want %d", i+1, len(s.Files[i]), len(want))
|
|
|
}
|
|
|
}
|
|
|
p, _ := capsule.PaddedLength(uint64(len(body)), capsule.Reforzado)
|
|
|
switch {
|
|
|
case o.Format != capsule.Format3 || o.AreaLen != capsule.AreaLen:
|
|
|
t.Errorf("format %d, area %d", o.Format, o.AreaLen)
|
|
|
case o.PayloadLength != uint64(len(body)) || o.PaddedLength != p || o.Padding != capsule.Reforzado:
|
|
|
t.Errorf("L = %d, P = %d, padding %s; want %d, %d", o.PayloadLength, o.PaddedLength, o.Padding, len(body), p)
|
|
|
case o.Head == nil || o.Head != s.Head || o.Head.Comment != c.comment || o.Head.Author != c.author || len(o.Head.Files) != 3:
|
|
|
t.Errorf("head %+v", o.Head)
|
|
|
case o.Verdicts != capsule.Verdicts{Signature: capsule.VerdictNoSignature, Seal: capsule.VerdictNoSeal}:
|
|
|
t.Errorf("verdicts %+v", o.Verdicts)
|
|
|
}
|
|
|
checks := o.Inspection.Checks
|
|
|
if n := len(checks); n < 2 || checks[n-2].Step != 17 || !checks[n-2].OK || checks[n-1].Step != 18 || !checks[n-1].OK {
|
|
|
t.Errorf("last checks %+v", checks[len(checks)-2:])
|
|
|
}
|
|
|
|
|
|
// The examples of spec §29.2: L and P of three small capsules.
|
|
|
for _, tc := range []struct {
|
|
|
name string
|
|
|
c capsule3
|
|
|
l, p uint64
|
|
|
}{
|
|
|
{"no files and no comment", capsule3{}, 577, 768},
|
|
|
{"a comment of one byte", capsule3{comment: "a"}, 580, 768},
|
|
|
{"nota.txt of 1000 bytes, with mtime", capsule3{paths: []string{"nota.txt"}, contents: [][]byte{make([]byte, 1000)},
|
|
|
head: func(h *capsule.Head) { h.Files[0].MTime, h.Files[0].HasMTime = 1790000000, true }}, 1641, 1792},
|
|
|
} {
|
|
|
dkc, _ := tc.c.build(t)
|
|
|
res := open3(t, dkc, &testkit.MemorySink{})
|
|
|
if res.err != nil || res.opened.PayloadLength != tc.l || res.opened.PaddedLength != tc.p || !res.sink.Committed {
|
|
|
t.Errorf("%s: L = %d, P = %d, %v; want %d, %d", tc.name, res.opened.PayloadLength, res.opened.PaddedLength, res.err, tc.l, tc.p)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// time_and_key: format 3 has the 16 stanzas of format 2 (spec §29.1, §39).
|
|
|
id, _ := age.GenerateX25519Identity()
|
|
|
dkc, _ = capsule3{paths: []string{"a"}, contents: [][]byte{[]byte("x")},
|
|
|
structure: capsule.TimeAndKey, recipients: []age.Recipient{id.Recipient()}}.build(t)
|
|
|
if res := open3(t, dkc, &testkit.MemorySink{}, id); res.err != nil || string(res.sink.Files[0]) != "x" {
|
|
|
t.Errorf("time_and_key: %v", res.err)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Spec §63 step 17: a failure of age, or a plaintext whose length is not P,
|
|
|
// prevails; otherwise the first substep that fails decides. A code other
|
|
|
// than ERR_INTEGRITY is reported only after reading PAYLOAD_AGE to its end.
|
|
|
// Nothing reaches Commit, and a Sink that got Begin gets Abort.
|
|
|
func TestOpen3Substeps(t *testing.T) {
|
|
|
two := capsule3{paths: []string{"a.txt", "b.txt"}, contents: [][]byte{[]byte("uno"), []byte("dos")}}
|
|
|
// With a big file the head is in STREAM chunk 0, and two chunks follow.
|
|
|
big := capsule3{paths: []string{"a.bin"}, contents: [][]byte{bytes.Repeat([]byte{0x5a}, 100000)}}
|
|
|
with := func(c capsule3, edit func(c *capsule3)) capsule3 { edit(&c); return c }
|
|
|
u32 := func(at int, v uint32) func(b []byte) []byte {
|
|
|
return func(b []byte) []byte { binary.BigEndian.PutUint32(b[at:], v); return b }
|
|
|
}
|
|
|
dotdot := func(h *capsule.Head) { h.Files[0].Path = ".." }
|
|
|
lastPadding := func(p []byte) []byte { p[len(p)-1] = 1; return p }
|
|
|
short := func(p []byte) []byte { return p[:len(p)-1] }
|
|
|
long := func(p []byte) []byte { return append(p, make([]byte, 256)...) }
|
|
|
areaByte := func(b []byte) []byte { b[capsule.BodyFrameSize+capsule.AreaLen-1] = 1; return b }
|
|
|
// HEAD_CBOR starts with the map, key 0, the text header and the 13 bytes
|
|
|
// of "datekeys-head", then key 1 and the version.
|
|
|
const headTag, headVersion = 3, 17
|
|
|
|
|
|
for _, tc := range []struct {
|
|
|
name string
|
|
|
c capsule3
|
|
|
want error // nil: opens
|
|
|
begun bool // the Sink got Begin, and so Abort
|
|
|
eof bool // Open read the .dkc to its end
|
|
|
}{
|
|
|
// 17.2: the frame and the area.
|
|
|
{"AREA_LEN 511", with(two, func(c *capsule3) { c.body = u32(0, 511) }), datekeys.ErrIntegrity, false, false},
|
|
|
{"AREA_LEN 513", with(two, func(c *capsule3) { c.body = u32(0, 513) }), datekeys.ErrIntegrity, false, false},
|
|
|
{"AREA_LEN 66048", with(two, func(c *capsule3) { c.body = u32(0, 66048) }), datekeys.ErrIntegrity, false, false},
|
|
|
{"SECURITY_LEN 0", with(two, func(c *capsule3) { c.body = u32(4, 0) }), datekeys.ErrIntegrity, false, false},
|
|
|
{"SECURITY_LEN 513", with(two, func(c *capsule3) { c.body = u32(4, 513) }), datekeys.ErrIntegrity, false, false},
|
|
|
{"HEAD_LEN 0", with(two, func(c *capsule3) { c.body = u32(8, 0) }), datekeys.ErrIntegrity, false, false},
|
|
|
{"HEAD_LEN 2^24 + 1", with(two, func(c *capsule3) { c.body = u32(8, 1<<24+1) }), datekeys.ErrIntegrity, false, false},
|
|
|
{"12 + AREA_LEN + HEAD_LEN = L + 1", with(capsule3{}, func(c *capsule3) { c.body = short }), datekeys.ErrIntegrity, false, false},
|
|
|
{"L < 12", with(two, func(c *capsule3) { c.body = func(b []byte) []byte { return b[:11] } }), datekeys.ErrIntegrity, false, false},
|
|
|
{"a byte of the area not zero", with(two, func(c *capsule3) { c.body = areaByte }), datekeys.ErrIntegrity, false, false},
|
|
|
{"a byte of the area not zero, and path ..", with(two, func(c *capsule3) { c.head, c.body = dotdot, areaByte }), datekeys.ErrIntegrity, false, false},
|
|
|
|
|
|
// 17.3 and 17.6: security never fails.
|
|
|
{"security unreadable", with(two, func(c *capsule3) { c.security = []byte{0xa0} }), nil, false, true},
|
|
|
|
|
|
// 17.4: the head, whose codes wait for the end of PAYLOAD_AGE.
|
|
|
{"head of version 2", with(two, func(c *capsule3) {
|
|
|
c.headCBOR = func(b []byte) []byte { b[headVersion] = 2; return b }
|
|
|
}), datekeys.ErrUnsupportedVersion, false, true},
|
|
|
{"head of another type tag", with(two, func(c *capsule3) {
|
|
|
c.headCBOR = func(b []byte) []byte { b[headTag] = 'D'; return b }
|
|
|
}), datekeys.ErrNonCanonicalCBOR, false, true},
|
|
|
{"head with one byte more within HEAD_LEN", with(two, func(c *capsule3) {
|
|
|
c.headCBOR = func(b []byte) []byte { return append(b, 0) }
|
|
|
}), datekeys.ErrNonCanonicalCBOR, false, true},
|
|
|
{"paths b and a, in that order", with(two, func(c *capsule3) { c.paths = []string{"b.txt", "a.txt"} }), datekeys.ErrNonCanonicalCBOR, false, true},
|
|
|
{"path ..", with(two, func(c *capsule3) { c.head = dotdot }), datekeys.ErrHeadInvalid, false, true},
|
|
|
{"paths A.txt and a.txt", with(two, func(c *capsule3) { c.paths = []string{"A.txt", "a.txt"} }), datekeys.ErrHeadInvalid, false, true},
|
|
|
{"comment with U+202E", with(two, func(c *capsule3) { c.comment = "ab" }), datekeys.ErrHeadInvalid, false, true},
|
|
|
{"start of an entry not the end of the one before", with(two, func(c *capsule3) {
|
|
|
c.head = func(h *capsule.Head) { h.Files[1].Start, h.Files[1].End = 2, 5 }
|
|
|
}), datekeys.ErrHeadInvalid, false, true},
|
|
|
{"an unknown critical extension", with(two, func(c *capsule3) {
|
|
|
c.head = func(h *capsule.Head) { h.Critical = []extension.Extension{{ID: "x.head", Version: 1}} }
|
|
|
}), datekeys.ErrExtensionCriticalUnknown, false, true},
|
|
|
|
|
|
// 17.5: the files fill CONTENT.
|
|
|
{"end of the last file not C", with(two, func(c *capsule3) {
|
|
|
c.body = func(b []byte) []byte { return append(b, 'x') }
|
|
|
}), datekeys.ErrIntegrity, false, false},
|
|
|
|
|
|
// 17.7: the SHA-256 of each file, and 17.8: the padding, up to P.
|
|
|
{"a byte of a file changed", with(two, func(c *capsule3) {
|
|
|
c.body = func(b []byte) []byte { b[len(b)-1] ^= 1; return b }
|
|
|
}), datekeys.ErrIntegrity, true, false},
|
|
|
{"a padding byte not zero", with(two, func(c *capsule3) { c.plain = lastPadding }), datekeys.ErrIntegrity, true, false},
|
|
|
{"plaintext of P - 1 bytes", with(two, func(c *capsule3) { c.plain = short }), datekeys.ErrIntegrity, true, true},
|
|
|
{"plaintext of P + 256 bytes", with(two, func(c *capsule3) { c.plain = long }), datekeys.ErrIntegrity, true, false},
|
|
|
|
|
|
// Precedence: the head fails first, and what follows decides only when
|
|
|
// it is a failure of age or of the length.
|
|
|
{"path .. and a padding byte not zero", with(two, func(c *capsule3) { c.head, c.plain = dotdot, lastPadding }), datekeys.ErrHeadInvalid, false, true},
|
|
|
{"path .. and a plaintext of P - 1 bytes", with(two, func(c *capsule3) { c.head, c.plain = dotdot, short }), datekeys.ErrIntegrity, false, true},
|
|
|
{"path .. and a plaintext of P + 256 bytes", with(two, func(c *capsule3) { c.head, c.plain = dotdot, long }), datekeys.ErrIntegrity, false, true},
|
|
|
{"path .. and the next STREAM chunk corrupt", with(big, func(c *capsule3) {
|
|
|
c.head = dotdot
|
|
|
c.payload = func(t *testing.T, p []byte) []byte { p[chunk(t, p, 1)+100] ^= 1; return p }
|
|
|
}), datekeys.ErrIntegrity, false, false},
|
|
|
{"path .. and a cut right after its chunk", with(big, func(c *capsule3) {
|
|
|
c.head = dotdot
|
|
|
c.payload = func(t *testing.T, p []byte) []byte { return p[:chunk(t, p, 1)] }
|
|
|
}), datekeys.ErrIntegrity, false, true},
|
|
|
{"path .. and a big file", with(big, func(c *capsule3) { c.head = dotdot }), datekeys.ErrHeadInvalid, false, true},
|
|
|
} {
|
|
|
dkc, _ := tc.c.build(t)
|
|
|
res := open3(t, dkc, &testkit.MemorySink{})
|
|
|
o, s := res.opened, res.sink
|
|
|
if tc.want == nil {
|
|
|
if res.err != nil || !s.Committed || o.Verdicts.Signature != capsule.VerdictUnreadable || len(o.Verdicts.Lines()) != 1 {
|
|
|
t.Errorf("%s: %v, verdicts %+v", tc.name, res.err, o.Verdicts)
|
|
|
}
|
|
|
continue
|
|
|
}
|
|
|
step := failedStep(t, o.Inspection.Checks, res.err)
|
|
|
expectStep(t, tc.name, step, res.err, tc.want, 17)
|
|
|
switch {
|
|
|
case s.Committed || o.Head != nil:
|
|
|
t.Errorf("%s: committed %v, head %v", tc.name, s.Committed, o.Head)
|
|
|
case (s.Head != nil) != tc.begun || s.Aborted != tc.begun:
|
|
|
t.Errorf("%s: begun %v, aborted %v, want %v", tc.name, s.Head != nil, s.Aborted, tc.begun)
|
|
|
case tc.eof && res.unread != 0:
|
|
|
t.Errorf("%s: %d bytes of the .dkc not read", tc.name, res.unread)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// failSink fails at one point: "begin", "create", "write", "close" or
|
|
|
// "commit", at file 2 for "create", "write" and "close".
|
|
|
type failSink struct {
|
|
|
testkit.MemorySink
|
|
|
at string
|
|
|
aborts int
|
|
|
}
|
|
|
|
|
|
var errDiskFull = errors.New("disk full")
|
|
|
|
|
|
func (s *failSink) Begin(h *capsule.Head) error {
|
|
|
if s.at == "begin" {
|
|
|
return errDiskFull
|
|
|
}
|
|
|
return s.MemorySink.Begin(h)
|
|
|
}
|
|
|
|
|
|
func (s *failSink) Create(i int) (io.WriteCloser, error) {
|
|
|
if s.at == "create" && i == 1 {
|
|
|
return nil, errDiskFull
|
|
|
}
|
|
|
w, err := s.MemorySink.Create(i)
|
|
|
return &failWriter{WriteCloser: w, fail: i == 1, at: s.at}, err
|
|
|
}
|
|
|
|
|
|
func (s *failSink) Commit() error {
|
|
|
if s.at == "commit" {
|
|
|
return errDiskFull
|
|
|
}
|
|
|
return s.MemorySink.Commit()
|
|
|
}
|
|
|
|
|
|
func (s *failSink) Abort() { s.aborts++; s.MemorySink.Abort() }
|
|
|
|
|
|
type failWriter struct {
|
|
|
io.WriteCloser
|
|
|
fail bool
|
|
|
at string
|
|
|
}
|
|
|
|
|
|
func (w *failWriter) Write(b []byte) (int, error) {
|
|
|
if w.fail && w.at == "write" {
|
|
|
return 0, errDiskFull
|
|
|
}
|
|
|
return w.WriteCloser.Write(b)
|
|
|
}
|
|
|
|
|
|
func (w *failWriter) Close() error {
|
|
|
if w.fail && w.at == "close" {
|
|
|
return errDiskFull
|
|
|
}
|
|
|
return w.WriteCloser.Close()
|
|
|
}
|
|
|
|
|
|
// A failure of the Sink is the caller's own error with ERR_INTEGRITY, as a
|
|
|
// failure of dst is in formats 1 and 2, and after Begin it gets Abort, once.
|
|
|
func TestOpen3SinkFailures(t *testing.T) {
|
|
|
dkc, _ := capsule3{paths: []string{"a.txt", "b.txt"}, contents: [][]byte{[]byte("uno"), []byte("dos")}}.build(t)
|
|
|
for _, at := range []string{"begin", "create", "write", "close", "commit"} {
|
|
|
s := &failSink{at: at}
|
|
|
res := open3(t, dkc, s)
|
|
|
wantAborts := 1
|
|
|
if at == "begin" {
|
|
|
wantAborts = 0
|
|
|
}
|
|
|
switch {
|
|
|
case !errors.Is(res.err, errDiskFull) || datekeys.Code(res.err) != datekeys.Code(datekeys.ErrIntegrity):
|
|
|
t.Errorf("%s: %v", at, res.err)
|
|
|
case s.aborts != wantAborts || s.Committed || res.opened.Head != nil:
|
|
|
t.Errorf("%s: %d aborts, committed %v", at, s.aborts, s.Committed)
|
|
|
}
|
|
|
}
|
|
|
if res := open3(t, dkc, &failSink{}); res.err != nil {
|
|
|
t.Fatal(res.err)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Spec §57: a reader reserves no memory by what BODY declares before it
|
|
|
// receives the bytes. Each capsule declares 16 MiB, in HEAD_LEN or in the
|
|
|
// size of a file, and holds a plaintext of 2000 bytes; Open fails, and
|
|
|
// allocates about what a small capsule makes it allocate.
|
|
|
func TestOpen3DeclaredLengths(t *testing.T) {
|
|
|
const declared = 16 << 20
|
|
|
cut := func(p []byte) []byte { return p[:2000] }
|
|
|
for _, tc := range []struct {
|
|
|
name string
|
|
|
c capsule3
|
|
|
}{
|
|
|
{"HEAD_LEN", capsule3{plain: cut, body: func(b []byte) []byte {
|
|
|
binary.BigEndian.PutUint32(b[8:], declared)
|
|
|
return append(b, make([]byte, declared)...)
|
|
|
}}},
|
|
|
{"size", capsule3{plain: cut, paths: []string{"a.bin"}, contents: [][]byte{make([]byte, declared)}}},
|
|
|
} {
|
|
|
dkc, _ := tc.c.build(t)
|
|
|
var before, after runtime.MemStats
|
|
|
runtime.GC()
|
|
|
runtime.ReadMemStats(&before)
|
|
|
res := open3(t, dkc, &testkit.MemorySink{})
|
|
|
runtime.ReadMemStats(&after)
|
|
|
if n := after.TotalAlloc - before.TotalAlloc; n > 4<<20 || !errors.Is(res.err, datekeys.ErrIntegrity) {
|
|
|
t.Errorf("%s: %d bytes allocated, %v", tc.name, n, res.err)
|
|
|
}
|
|
|
}
|
|
|
}
|