Skip to content
quarry Artifact [5f5ffc3c16]
Login

Artifact [5f5ffc3c16]

Artifact 5f5ffc3c163da66fb89bbb38d30ff8b1d74ab2add88afdf1ad859cd01b18e3d2:


package ingest

import (
	"encoding/json"
	"testing"
	"time"

	"github.com/nats-io/nats.go"

	"craftdesign.group/quarry/internal/artifact"
	"craftdesign.group/quarry/internal/evt"
)

func kinds(rs []raised) []evt.Kind {
	out := make([]evt.Kind, 0, len(rs))
	for _, r := range rs {
		out = append(out, r.Kind)
	}
	return out
}

func TestDeriveReadsTheTagOperator(t *testing.T) {
	for _, tc := range []struct {
		name string
		art  *artifact.Artifact
		want []evt.Kind
	}{
		// A check-in's own sym- tag raises nothing extra: branch.moved
		// for that check-in comes from the computed branch after the
		// fold, and raising it here too would double it.
		{"a check-in on a branch", &artifact.Artifact{
			Type: artifact.Manifest,
			Tags: []artifact.Tag{{Name: "*sym-trunk"}},
		}, []evt.Kind{evt.CheckinCreated}},

		{"a check-in with a plain tag", &artifact.Artifact{
			Type: artifact.Manifest,
			Tags: []artifact.Tag{{Name: "*sym-trunk"}, {Name: "+release"}},
		}, []evt.Kind{evt.CheckinCreated, evt.TagCreated}},

		{"a control artifact naming a branch", &artifact.Artifact{
			Type: artifact.Control,
			Tags: []artifact.Tag{{Name: "*sym-feature", Target: "abc"}},
		}, []evt.Kind{evt.BranchCreated}},

		{"a control artifact cancelling a tag", &artifact.Artifact{
			Type: artifact.Control,
			Tags: []artifact.Tag{{Name: "-release", Target: "abc"}},
		}, []evt.Kind{evt.TagRemoved}},

		// Cancelling a branch tag is not closing the branch, and nothing
		// in the vocabulary names it — so it raises nothing rather than
		// something approximate.
		{"a cancelled branch tag", &artifact.Artifact{
			Type: artifact.Control,
			Tags: []artifact.Tag{{Name: "-sym-old", Target: "abc"}},
		}, nil},

		// Everything with an op behind it is raised at that op, which
		// knows the previous state the artifact does not carry.
		{"a wiki page", &artifact.Artifact{Type: artifact.Wiki}, nil},
		{"a ticket change", &artifact.Artifact{Type: artifact.Ticket}, nil},

		{"a malformed tag name", &artifact.Artifact{
			Type: artifact.Control,
			Tags: []artifact.Tag{{Name: "+"}},
		}, nil},
	} {
		got := kinds(derive("h1", tc.art))
		if len(got) != len(tc.want) {
			t.Errorf("%s: got %v, want %v", tc.name, got, tc.want)
			continue
		}
		for i := range got {
			if got[i] != tc.want[i] {
				t.Errorf("%s: got %v, want %v", tc.name, got, tc.want)
				break
			}
		}
	}
}

func TestDeriveCarriesTheBranchAndTarget(t *testing.T) {
	rs := derive("h1", &artifact.Artifact{
		Type: artifact.Manifest, User: "dan", Comment: "fix it\n\nmore",
		Tags: []artifact.Tag{{Name: "*sym-feature"}},
	})
	if got := rs[0].Payload["branch"]; got != "feature" {
		t.Errorf("checkin branch = %v, want feature", got)
	}
	if got := rs[0].Payload["summary"]; got != "fix it" {
		t.Errorf("summary = %v, want the first line only", got)
	}
	rs = derive("ctl", &artifact.Artifact{
		Type: artifact.Control,
		Tags: []artifact.Tag{{Name: "+release", Target: "target-hash"}},
	})
	if got := rs[0].Payload["target"]; got != "target-hash" {
		t.Errorf("target = %v, want the artifact the tag applies to", got)
	}
}

// The pipeline end to end: a real store, a real fold, and the events a
// dagnats subject trigger would actually see.
//
// The bootstrap round is drained FIRST and deliberately unasserted. A
// first sync rebuilds from the store's census rather than the bus, and a
// rebuild is not news — `checkin.created` for a history that already
// existed would be the forge telling every subscriber that five years of
// commits just landed.
func TestFoldRaisesDerivedEventsOnce(t *testing.T) {
	pipe, _, repo := testPipeline(t)
	// Twice: the first round rebuilds from the census (no cursor), and
	// the second settles the once-per-process tip check. Only after that
	// does a round take the bus, which is the path under test.
	for i := 0; i < 2; i++ {
		if _, err := pipe.Sync(); err != nil {
			t.Fatal(err)
		}
	}

	sub, err := pipe.ev.JS().SubscribeSync("quarry.evt.test.>", nats.DeliverNew())
	if err != nil {
		t.Fatal(err)
	}
	defer sub.Unsubscribe()

	commitFile(t, repo, "c.txt", "gamma\n", "add c")
	commitFile(t, repo, "d.txt", "delta\n", "add d")
	if _, err := pipe.Sync(); err != nil {
		t.Fatal(err)
	}
	drain := func() map[evt.Kind]int {
		seen := map[evt.Kind]int{}
		for {
			m, err := sub.NextMsg(400 * time.Millisecond)
			if err != nil {
				return seen
			}
			var e struct {
				Kind evt.Kind
				Repo string
			}
			if err := json.Unmarshal(m.Data, &e); err != nil {
				t.Fatalf("body: %v", err)
			}
			if e.Repo != "test" {
				t.Errorf("event carried repo %q", e.Repo)
			}
			seen[e.Kind]++
		}
	}
	first := drain()
	if first[evt.CheckinCreated] != 2 {
		t.Fatalf("two new check-ins produced %d checkin.created (%v)", first[evt.CheckinCreated], first)
	}
	if first[evt.BranchMoved] == 0 {
		t.Errorf("no branch.moved for check-ins on trunk (%v)", first)
	}

	// Folding again must raise NOTHING. Folds are idempotent; raising is
	// not, and at-least-once redelivery would otherwise tell every
	// subscriber the history had just happened again.
	if _, err := pipe.Sync(); err != nil {
		t.Fatal(err)
	}
	if again := drain(); len(again) != 0 {
		t.Errorf("a refold re-raised %v", again)
	}

	// A genuinely new check-in still raises.
	commitFile(t, repo, "e.txt", "epsilon\n", "add e")
	if _, err := pipe.Sync(); err != nil {
		t.Fatal(err)
	}
	if third := drain(); third[evt.CheckinCreated] != 1 {
		t.Errorf("a new check-in raised %v", third)
	}
}