Skip to content
quarry Artifact [51f405c4d3]
Login

Artifact [51f405c4d3]

Artifact 51f405c4d349cc880c4ac687174c8381098e878cb6dbc8e393aafc03e021bbf4:


package api

import (
	"archive/tar"
	"bufio"
	"compress/gzip"
	"crypto/sha256"
	"encoding/hex"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"sort"
	"strings"
	"testing"
)

// releaseStub stands in for build-linux.sh: a quarry that prints its
// version, a zit, and every runner download the tree's build script
// lists, with their SHA256SUMS, as the real build lays them out.
const releaseStub = `#!/bin/sh
set -eu
arch=$1 prefix=$2
mkdir -p "$prefix/runner"
printf '#!/bin/sh\necho "quarry %s"\n' "$VERSION" >"$prefix/quarry"
printf '#!/bin/sh\necho "zit 0.0.0 linux-%s"\n' "$arch" >"$prefix/zit"
chmod 0755 "$prefix/quarry" "$prefix/zit"
for t in $(sed -n 's/^RUNNER_TARGETS="\(.*\)"$/\1/p' scripts/build-linux.sh); do
  os=${t%/*}; a=${t#*/}; ext=; [ "$os" = windows ] && ext=.exe
  echo "runner $t $VERSION" >"$prefix/runner/quarry-runner-$os-$a$ext"
done
if command -v sha256sum >/dev/null 2>&1; then s="sha256sum"; else s="shasum -a 256"; fi
(cd "$prefix/runner" && $s quarry-runner-* >SHA256SUMS)
`

// releaseTree is a throwaway quarry-shaped repository at version ver,
// carrying the scripts release.sh runs and tagged version-<ver>; it
// returns the checkout and the tagged check-in.
func releaseTree(t *testing.T, ver string) (string, string) {
	t.Helper()
	for _, bin := range []string{"zit", "sh", "tar", "gzip"} {
		if _, err := exec.LookPath(bin); err != nil {
			t.Skipf("%s not on PATH", bin)
		}
	}
	dir := t.TempDir()
	zitIn(t, dir, "init")
	files := map[string][]byte{
		"VERSION":      []byte(ver + "\n"),
		"CHANGELOG.md": []byte("# Changelog\n\n## " + ver + " — 2026-10-02\n\n- A release.\n\n## 0.1.0\n\n- Before.\n"),
		"LICENSE":      []byte("license\n"),
	}
	for _, s := range []string{"release.sh", "version-stamp.sh", "runner-check.sh", "build-linux.sh"} {
		raw, err := os.ReadFile(filepath.Join("..", "..", "scripts", s))
		if err != nil {
			t.Fatal(err)
		}
		files["scripts/"+s] = raw
	}
	for name, body := range files {
		p := filepath.Join(dir, filepath.FromSlash(name))
		if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(p, body, 0o644); err != nil {
			t.Fatal(err)
		}
		zitIn(t, dir, "add", name)
	}
	zitIn(t, dir, "commit", "-m", "release "+ver)
	checkin := strings.SplitN(zitIn(t, dir, "info", "current"), "\n", 2)[0]
	zitIn(t, dir, "tag", "add", "version-"+ver, checkin)
	return dir, checkin
}

func zitIn(t *testing.T, dir string, args ...string) string {
	t.Helper()
	cmd := exec.Command("zit", args...)
	cmd.Dir = dir
	out, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("zit %v: %v\n%s", args, err, out)
	}
	return string(out)
}

// runRelease runs release.sh --dry-run in dir with the stub builder.
func runRelease(t *testing.T, dir, out string, args ...string) (string, error) {
	t.Helper()
	stub := filepath.Join(t.TempDir(), "build-stub.sh")
	if err := os.WriteFile(stub, []byte(releaseStub), 0o755); err != nil {
		t.Fatal(err)
	}
	cmd := exec.Command("sh", append([]string{"scripts/release.sh", "--dry-run", "--out", out}, args...)...)
	cmd.Dir = dir
	cmd.Env = append(os.Environ(), "RELEASE_BUILD=sh "+stub, "TMPDIR="+t.TempDir(), "QUARRY_TAG=")
	b, err := cmd.CombinedOutput()
	return string(b), err
}

func fileSum(t *testing.T, p string) (string, int64) {
	t.Helper()
	raw, err := os.ReadFile(p)
	if err != nil {
		t.Fatal(err)
	}
	h := sha256.Sum256(raw)
	return hex.EncodeToString(h[:]), int64(len(raw))
}

// RELEASE.SH --DRY-RUN WRITES A RELEASE THE FORGE AND DEPLOY.SH READ
// (RFC 0020 §releases): the manifest parses as the Releases page parses
// it, every sha256 in it is SHA256SUMS' and the bytes', and the tarballs
// are the one-directory layout deploy.sh --release unpacks.
func TestReleaseScriptDryRunWritesWhatTheForgeReads(t *testing.T) {
	const ver = "9.8.7"
	dir, checkin := releaseTree(t, ver)
	out := filepath.Join(t.TempDir(), "out")
	if log, err := runRelease(t, dir, out, ver); err != nil {
		t.Fatalf("FAIL: release.sh --dry-run: %v\n%s", err, log)
	}

	// SHA256SUMS: one sum and one bare name per line, as deploy.sh
	// insists, and a line for every file but itself and the manifest.
	sums := map[string]string{}
	f, err := os.Open(filepath.Join(out, "SHA256SUMS"))
	if err != nil {
		t.Fatal(err)
	}
	line := regexp.MustCompile(`^([0-9a-f]{64}) [ *]([A-Za-z0-9_][A-Za-z0-9._-]*)$`)
	sc := bufio.NewScanner(f)
	for sc.Scan() {
		m := line.FindStringSubmatch(sc.Text())
		if m == nil {
			t.Errorf("FAIL: SHA256SUMS line %q is not a sum and a bare name", sc.Text())
			continue
		}
		sums[m[2]] = m[1]
	}
	f.Close()
	entries, err := os.ReadDir(out)
	if err != nil {
		t.Fatal(err)
	}
	var names []string
	for _, e := range entries {
		n := e.Name()
		names = append(names, n)
		if n == "SHA256SUMS" || n == "release-manifest.json" {
			continue
		}
		got, _ := fileSum(t, filepath.Join(out, n))
		if sums[n] != got {
			t.Errorf("FAIL: SHA256SUMS says %s is %q; its bytes are %s", n, sums[n], got)
		}
	}
	sort.Strings(names)
	want := []string{
		"SHA256SUMS",
		"quarry-9.8.7-linux-amd64.tar.gz", "quarry-9.8.7-linux-arm64.tar.gz", "quarry-9.8.7-src.tar.gz",
		"quarry-runner-darwin-amd64", "quarry-runner-darwin-arm64", "quarry-runner-linux-amd64",
		"quarry-runner-linux-arm64", "quarry-runner-windows-amd64.exe",
		"release-manifest.json",
	}
	if strings.Join(names, " ") != strings.Join(want, " ") {
		t.Errorf("FAIL: the release holds\n  %v\nwant\n  %v", names, want)
	}
	if len(sums) != len(want)-2 {
		t.Errorf("FAIL: SHA256SUMS has %d lines; want %d", len(sums), len(want)-2)
	}

	// The manifest, through the Releases page's own parser.
	raw, err := os.ReadFile(filepath.Join(out, "release-manifest.json"))
	if err != nil {
		t.Fatal(err)
	}
	m, err := parseReleaseManifest(raw, ver)
	if err != nil {
		t.Fatalf("FAIL: the forge refuses the manifest: %v\n%s", err, raw)
	}
	if m.Version != ver || m.Tag != "version-"+ver || m.Checkin != checkin || m.Date == "" {
		t.Errorf("FAIL: manifest release = %q %q %q %q; want %s, version-%s, %s and a date", m.Version, m.Tag, m.Checkin, m.Date, ver, ver, checkin)
	}
	described := append([]releaseArtifact{}, m.Artifacts...)
	if m.Source == nil {
		t.Error("FAIL: the manifest names no source archive")
	} else {
		described = append(described, *m.Source)
	}
	seen := map[string]bool{}
	for _, a := range described {
		seen[a.Name] = true
		got, size := fileSum(t, filepath.Join(out, a.Name))
		if a.SHA256 != got || a.SHA256 != sums[a.Name] || a.Size != size {
			t.Errorf("FAIL: manifest says %s is %s, %d bytes; its bytes are %s, %d; SHA256SUMS says %s", a.Name, a.SHA256, a.Size, got, size, sums[a.Name])
		}
		if a.Target == "" || a.Qualification == "" {
			t.Errorf("FAIL: %s has target %q and qualification %q; want both", a.Name, a.Target, a.Qualification)
		}
	}
	for n := range sums {
		if !seen[n] {
			t.Errorf("FAIL: %s is published and the manifest does not describe it", n)
		}
	}
	omitted := map[string]bool{}
	for _, o := range m.Omitted {
		omitted[o.Target] = o.Reason != ""
	}
	for _, t2 := range []string{"darwin-arm64", "darwin-amd64", "windows-amd64"} {
		if !omitted[t2] {
			t.Errorf("FAIL: the manifest does not name %s as omitted, with a reason: %+v", t2, m.Omitted)
		}
	}
	if len(m.Qualification) == 0 {
		t.Error("FAIL: the manifest carries no qualification notes")
	}

	// The layout deploy.sh --release unpacks: one directory, holding
	// quarry, zit, CHANGELOG.md and LICENSE, and nothing else.
	for _, arch := range []string{"amd64", "arm64"} {
		top := "quarry-" + ver + "-linux-" + arch
		members := tarMembers(t, filepath.Join(out, top+".tar.gz"))
		wantM := []string{top + "/", top + "/CHANGELOG.md", top + "/LICENSE", top + "/quarry", top + "/zit"}
		if strings.Join(members, " ") != strings.Join(wantM, " ") {
			t.Errorf("FAIL: %s.tar.gz holds %v; want %v", top, members, wantM)
		}
	}
	t.Log("ok: the dry run's manifest parses, its sums are SHA256SUMS' and the bytes', the tarballs are deploy's layout")
}

func tarMembers(t *testing.T, p string) []string {
	t.Helper()
	f, err := os.Open(p)
	if err != nil {
		t.Fatal(err)
	}
	defer f.Close()
	gz, err := gzip.NewReader(f)
	if err != nil {
		t.Fatal(err)
	}
	tr := tar.NewReader(gz)
	var out []string
	for {
		h, err := tr.Next()
		if err == io.EOF {
			break
		}
		if err != nil {
			t.Fatal(err)
		}
		name := strings.TrimPrefix(h.Name, "./")
		if h.Typeflag == tar.TypeDir && !strings.HasSuffix(name, "/") {
			name += "/"
		}
		out = append(out, name)
	}
	sort.Strings(out)
	return out
}

// RELEASE.SH REFUSES WHAT IS NOT RELEASE X.Y.Z: another version than
// VERSION, a check-in that does not carry the tag itself, an untracked
// file, a changed one, a changelog with no section for it, and a run
// started by another tag.
func TestReleaseScriptRefusesWhatIsNotTheRelease(t *testing.T) {
	const ver = "9.8.7"
	dir, _ := releaseTree(t, ver)
	out := filepath.Join(t.TempDir(), "out")
	refused := func(why, want string, args ...string) {
		t.Helper()
		log, err := runRelease(t, dir, out, args...)
		if err == nil || !strings.Contains(log, want) {
			t.Errorf("FAIL: %s: err %v, output\n%s\nwant a refusal naming %q", why, err, log, want)
		}
	}
	refused("another version", "VERSION holds 9.8.7, not 9.8.8", "9.8.8")

	if err := os.WriteFile(filepath.Join(dir, "stray.txt"), []byte("x\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	refused("an untracked file", "untracked files", ver)
	os.Remove(filepath.Join(dir, "stray.txt"))

	if err := os.WriteFile(filepath.Join(dir, "LICENSE"), []byte("changed\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	refused("a changed file", "has changes", ver)
	zitIn(t, dir, "revert")

	if err := os.WriteFile(filepath.Join(dir, "CHANGELOG.md"), []byte("# Changelog\n\n## 9.8.70\n\n## 0.1.0-9.8.7\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	refused("no changelog heading", "no \"## 9.8.7\" heading", ver)
	zitIn(t, dir, "revert")

	cmd := exec.Command("sh", "scripts/release.sh", "--dry-run", ver)
	cmd.Dir = dir
	cmd.Env = append(os.Environ(), "QUARRY_TAG=version-9.9.9")
	if b, err := cmd.CombinedOutput(); err == nil || !strings.Contains(string(b), "started by the tag version-9.9.9") {
		t.Errorf("FAIL: a run started by another tag: err %v, output\n%s", err, b)
	}

	// The next check-in inherits nothing: the tag is its parent's.
	if err := os.WriteFile(filepath.Join(dir, "LICENSE"), []byte("next\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	zitIn(t, dir, "commit", "-m", "after the release")
	refused("an untagged check-in", "does not carry version-9.8.7", ver)
	t.Log("ok: a wrong version, a dirty tree, no changelog section, another tag and an untagged check-in are refused")
}