package zitcli
import (
"context"
"errors"
"strings"
"testing"
"time"
)
// The point of a bounded read is that it does NOT read the rest. A
// child that would go on writing for thirty seconds must be cut off
// once the prefix is in hand, not waited for — the old whole-blob read
// is exactly the cost this replaces.
func TestArtifactHeadStopsAtTheBound(t *testing.T) {
fakeZit(t, `printf '0123456789'; sleep 30`)
c := &Client{repo: "tcl"}
done := make(chan struct{})
var got []byte
var err error
go func() {
got, err = c.ArtifactHead(context.Background(), "abcd", 4)
close(done)
}()
select {
case <-done:
case <-time.After(10 * time.Second):
t.Fatal("FAIL: the bounded read waited for the child instead of cutting it off")
}
if err != nil {
t.Fatalf("FAIL: %v", err)
}
if string(got) != "0123" {
t.Fatalf("FAIL: read %q, want the first four bytes", got)
}
t.Log("ok: the head read takes its prefix and leaves the child behind")
}
// A blob shorter than the bound arrives whole, and the child's own exit
// is waited for rather than killed out from under it.
func TestArtifactHeadReturnsShortContentWhole(t *testing.T) {
fakeZit(t, `printf 'hi\n'`)
c := &Client{repo: "tcl"}
got, err := c.ArtifactHead(context.Background(), "abcd", 4096)
if err != nil {
t.Fatalf("FAIL: %v", err)
}
if string(got) != "hi\n" {
t.Fatalf("FAIL: read %q", got)
}
t.Log("ok: a blob smaller than the bound comes back entire")
}
// A hash the store does not hold is ErrNoEntry, the same answer the
// whole-blob read gives — the bound must not hide a real failure.
func TestArtifactHeadReportsAMissingArtifact(t *testing.T) {
fakeZit(t, `echo "no artifact matches" >&2; exit 1`)
c := &Client{repo: "tcl"}
if _, err := c.ArtifactHead(context.Background(), "abcd", 4096); !errors.Is(err, ErrNoEntry) {
t.Fatalf("FAIL: missing artifact answered %v, want ErrNoEntry", err)
}
t.Log("ok: a missing artifact is still ErrNoEntry under a bounded read")
}
// The size comes from the SUMMARY, which names it in one line, so
// learning how big a file is costs no bytes of the file.
func TestArtifactSizeReadsTheSummary(t *testing.T) {
fakeZit(t, `printf 'aaaa\ntype: file\nsize: 18349022 bytes\nbinary: yes\n'`)
c := &Client{repo: "tcl"}
n, err := c.ArtifactSize(context.Background(), "aaaa")
if err != nil {
t.Fatalf("FAIL: %v", err)
}
if n != 18349022 {
t.Fatalf("FAIL: size %d", n)
}
t.Log("ok: the size is parsed from the summary without reading the blob")
}
// A manifest's summary runs to thousands of F-card lines. The size is
// on the third; the reader must not sit through the rest.
func TestArtifactSizeStopsAtTheSizeLine(t *testing.T) {
fakeZit(t, `printf 'aaaa\ntype: manifest\nsize: 50800 bytes\n'; sleep 30`)
c := &Client{repo: "tcl"}
done := make(chan struct{})
var n int
var err error
go func() {
n, err = c.ArtifactSize(context.Background(), "aaaa")
close(done)
}()
select {
case <-done:
case <-time.After(10 * time.Second):
t.Fatal("FAIL: the size read waited for the whole summary")
}
if err != nil || n != 50800 {
t.Fatalf("FAIL: size %d, err %v", n, err)
}
t.Log("ok: the size read stops at the line it came for")
}
func TestArtifactSizeReportsAMissingArtifact(t *testing.T) {
fakeZit(t, `echo "no artifact matches" >&2; exit 1`)
c := &Client{repo: "tcl"}
_, err := c.ArtifactSize(context.Background(), "abcd")
if !errors.Is(err, ErrNoEntry) {
t.Fatalf("FAIL: %v", err)
}
// A summary with no size line is a contract change, not a zero.
fakeZit(t, `printf 'aaaa\ntype: file\n'`)
if _, err := c.ArtifactSize(context.Background(), "aaaa"); err == nil ||
!strings.Contains(err.Error(), "size") {
t.Fatalf("FAIL: a summary with no size answered %v", err)
}
t.Log("ok: a missing artifact and a sizeless summary are both errors")
}