Skip to content
quarry Artifact [a6948c843f]
Login

Artifact [a6948c843f]

Artifact a6948c843fce168acf81843406a4215b57813cb3312c6c35ed703977e1d4e342:


package api

import (
	"errors"
	"path/filepath"
	"sync"
	"testing"

	"craftdesign.group/quarry/internal/oltp"
	"craftdesign.group/quarry/internal/ops"
)

func TestTwoPromotionsOfOneLocalTicketAtOnceWriteOneTicketArtifact(t *testing.T) {
	ix, z, _ := attachStore(t)
	db, err := oltp.Open(filepath.Join(t.TempDir(), "oltp.db"))
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	local, err := db.LocalTicketNew("test", "lou", "", map[string]string{"title": "promote me once", "status": "Open"})
	if err != nil {
		t.Fatal(err)
	}
	r := ops.NewRegistry()
	if err := RegisterAll(r, Deps{Version: "v", Repos: NewRepos(map[string]*Repo{"test": {Index: ix, Zit: z}}), Local: db}); err != nil {
		t.Fatal(err)
	}

	const n = 2
	var start, done sync.WaitGroup
	start.Add(1)
	ids := make([]string, n)
	errs := make([]error, n)
	for i := 0; i < n; i++ {
		done.Add(1)
		go func(i int) {
			defer done.Done()
			start.Wait()
			out, err := r.Invoke(as("adm", "a"), "ticket.promote", ticketPromoteIn{Repo: "test", ID: local[:12]})
			if err == nil {
				ids[i] = out.(ticketPromoteOut).ID
			}
			errs[i] = err
		}(i)
	}
	start.Done()
	done.Wait()

	written := map[string]bool{}
	var refused []error
	for i := range ids {
		if errs[i] == nil {
			written[ids[i]] = true
		} else {
			refused = append(refused, errs[i])
		}
	}
	if len(written) != 1 || len(refused) != n-1 {
		t.Fatalf("FAIL: %d concurrent promotions wrote ticket artifacts %v and refused %v; want one artifact and one refusal", n, written, refused)
	}
	t.Logf("ok: one promotion wrote the ticket artifact")
	// Which refusal the loser gets depends on where the winner was when
	// it looked — mid-promotion or already tombstoned — and both are
	// coded; never a bare not-found.
	var ref *ops.Refusal
	if !errors.As(refused[0], &ref) || !errors.Is(refused[0], ops.ErrInvalid) {
		t.Fatalf("FAIL: the losing promotion failed with %v, want a coded refusal", refused[0])
	}
	switch ref.Code {
	case RefusalBeingPromoted:
	case RefusalAlreadyPromoted:
		if !written[ref.Fields["to"]] {
			t.Fatalf("FAIL: the refusal's to field %q is not the written artifact %v", ref.Fields["to"], written)
		}
	default:
		t.Fatalf("FAIL: the losing promotion's refusal has code %q", ref.Code)
	}
	t.Logf("ok: the other was refused with code %s", ref.Code)
	for id := range written {
		if to, ok := db.LocalTicketRedirect("test", local[:12]); !ok || to != id {
			t.Fatalf("FAIL: the local id redirects to %q (%v), want the one artifact %s", to, ok, id)
		}
	}
	t.Logf("ok: the local id redirects to the one artifact")
}

// staleProbe is the local store as a losing promotion sees it: its
// redirect probe ran before the winner's tombstone, so it answers "not
// promoted" until the ticket itself has been read, by which time the
// tombstone has landed and the ticket folds to nothing.
type staleProbe struct {
	*oltp.DB
	read bool
}

func (s *staleProbe) LocalTicketRedirect(repo, prefix string) (string, bool) {
	if !s.read {
		return "", false
	}
	return s.DB.LocalTicketRedirect(repo, prefix)
}

func (s *staleProbe) LocalTicketByPrefix(repo, prefix string) (oltp.LocalTicket, bool, error) {
	s.read = true
	return s.DB.LocalTicketByPrefix(repo, prefix)
}

func TestAPromotionThatLosesAfterTheWinnersTombstoneIsToldWhereItWent(t *testing.T) {
	db, err := oltp.Open(filepath.Join(t.TempDir(), "oltp.db"))
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	local, err := db.LocalTicketNew("test", "lou", "", map[string]string{"title": "promote me once"})
	if err != nil {
		t.Fatal(err)
	}
	// The winner: claimed, written, tombstoned.
	claim, _, err := db.LocalTicketClaim("test", local)
	if err != nil {
		t.Fatal(err)
	}
	if err := db.LocalTicketPromoted("test", local, "f0551171f055", claim); err != nil {
		t.Fatal(err)
	}
	r := ops.NewRegistry()
	if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(wikiIndex(t)), Local: &staleProbe{DB: db}}); err != nil {
		t.Fatal(err)
	}
	_, err = r.Invoke(as("adm", "a"), "ticket.promote", ticketPromoteIn{Repo: "test", ID: local[:12]})
	var ref *ops.Refusal
	if errors.Is(err, ops.ErrNotFound) || !errors.Is(err, ops.ErrInvalid) || !errors.As(err, &ref) {
		t.Fatalf("FAIL: the losing promotion failed with %v, want the already-promoted refusal", err)
	}
	if ref.Code != RefusalAlreadyPromoted || ref.Fields["to"] != "f0551171f055" {
		t.Fatalf("FAIL: refusal code %q fields %v, want %s to f0551171f055", ref.Code, ref.Fields, RefusalAlreadyPromoted)
	}
	t.Logf("ok: the loser is refused with code %s and where the ticket went", ref.Code)
}

// The other window: the winner holds the claim and has not tombstoned
// yet. There is no promoted id to name, so the refusal says the ticket
// is being promoted.
func TestAPromotionWhileAnotherHoldsTheClaimIsRefusedAsBeingPromoted(t *testing.T) {
	ix, z, _ := attachStore(t)
	db, err := oltp.Open(filepath.Join(t.TempDir(), "oltp.db"))
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	local, err := db.LocalTicketNew("test", "lou", "", map[string]string{"title": "held"})
	if err != nil {
		t.Fatal(err)
	}
	// The winner, stopped between its claim and its tombstone.
	if _, ok, err := db.LocalTicketClaim("test", local); err != nil || !ok {
		t.Fatalf("claim: %v %v", ok, err)
	}
	r := ops.NewRegistry()
	if err := RegisterAll(r, Deps{Version: "v", Repos: NewRepos(map[string]*Repo{"test": {Index: ix, Zit: z}}), Local: db}); err != nil {
		t.Fatal(err)
	}
	_, err = r.Invoke(as("adm", "a"), "ticket.promote", ticketPromoteIn{Repo: "test", ID: local[:12]})
	var ref *ops.Refusal
	if !errors.As(err, &ref) || !errors.Is(err, ops.ErrInvalid) || ref.Code != RefusalBeingPromoted {
		t.Fatalf("FAIL: promote while another holds the claim = %v, want code %s", err, RefusalBeingPromoted)
	}
	if _, named := ref.Fields["to"]; named {
		t.Fatalf("FAIL: a being-promoted refusal names a ticket that does not exist yet: %v", ref.Fields)
	}
	t.Logf("ok: refused with code %s", ref.Code)
}