Skip to content
quarry Artifact [41fdf4b140]
Login

Artifact [41fdf4b140]

Artifact 41fdf4b1401077529b5d47e5ecd4934e0965eb9a4086a3b909e6abe3e208a2ce:


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")
}