Skip to content
quarry Artifact [20c2c23d25]
Login

Artifact [20c2c23d25]

Artifact 20c2c23d25b0e174b190e29c12fb6350006efe8447eb1c72d7e8d78089ba0fbd:


package evt

import "testing"

// A subscription naming a noun selects every action on it, including ones
// added after the subscription was written. That is the entire argument for
// noun-and-action over Gitea's one-name-per-verb, so it is the thing to pin.
func TestANounSelectsEveryActionOnIt(t *testing.T) {
	if !TicketClosed.Matches("ticket") {
		t.Error(`"ticket" does not select ticket.closed`)
	}
	if !TicketOpened.Matches("ticket") {
		t.Error(`"ticket" does not select ticket.opened`)
	}
	if !TicketClosed.Matches("ticket.closed") {
		t.Error("an exact kind does not select itself")
	}
	// Precision is available where it is wanted.
	if TicketOpened.Matches("ticket.closed") {
		t.Error("ticket.closed selected ticket.opened")
	}
	// A noun does not reach across nouns, however similar the names.
	if LandingOpened.Matches("ticket") {
		t.Error("a noun selected another noun's event")
	}
	// A pattern naming a noun that does not exist selects nothing rather
	// than everything — the failure of a typo must be silence, not a
	// firehose.
	if TicketClosed.Matches("tickets") || TicketClosed.Matches("") {
		t.Error("a bad pattern matched")
	}
}

// The bus is a one-hour window, so what happens to a missed event depends on
// whether it can be re-derived from the store. Getting this classification
// wrong is how an operational fact — a failed mirror pull — silently does not
// happen, so each noun is asserted rather than the rule restated.
func TestDerivedEventsAreExactlyTheOnesTheStoreHolds(t *testing.T) {
	for _, k := range []Kind{CheckinCreated, BranchMoved, TagCreated,
		TicketClosed, WikiEdited, TechnoteCreated, ForumPosted, LandingAccepted} {
		if !k.Derived() {
			t.Errorf("%s is a reading of stored artifacts and should be derived", k)
		}
	}
	// None of these is in the DAG: missing one loses the fact, so each
	// must be recorded in OLTP as it is raised.
	for _, k := range []Kind{CICompleted, MirrorFailed, RepoCreated,
		AccountCaps, CloneCloned} {
		if k.Derived() {
			t.Errorf("%s is not in the store and must not be treated as re-derivable", k)
		}
	}
}

// An unknown name is refused where the subscription is written. A pipeline
// that never runs and a pipeline that runs and fails look nothing alike to
// the person waiting for it.
func TestUnknownKindsAreRefused(t *testing.T) {
	for _, bad := range []Kind{"ticket.exploded", "issue.opened", "checkin", ""} {
		if bad.Valid() {
			t.Errorf("%q was accepted into the vocabulary", bad)
		}
	}
	for _, k := range All {
		if !k.Valid() {
			t.Errorf("%s is in All but not Valid", k)
		}
		if k.Noun() == "" || k.Action() == "" {
			t.Errorf("%s is not noun.action", k)
		}
	}
}

// The vocabulary is a published interface: a name in someone else's pipeline
// definition. This pins the set so a rename is a deliberate act with a
// failing test attached, not a refactor that quietly breaks every store.
func TestTheVocabularyIsFixed(t *testing.T) {
	if got, want := len(All), 38; got != want {
		t.Errorf("the vocabulary changed size: %d, was %d — if deliberate, say so in RFC 0005", got, want)
	}
	want := []string{"checkin", "branch", "tag", "ticket", "wiki", "technote",
		"forum", "landing", "ci", "mirror", "repo", "account", "praise", "clone",
		"error"}
	got := Nouns()
	if len(got) != len(want) {
		t.Fatalf("nouns = %v, want %v", got, want)
	}
	for i := range want {
		if got[i] != want[i] {
			t.Fatalf("nouns = %v, want %v", got, want)
		}
	}
}

// The subject is the subscription interface: dagnats triggers take a NATS
// subject filter, so the vocabulary has to survive being taken apart by
// wildcards. A noun or action containing a dot would silently break every
// filter that assumed the shape.
func TestEverySubjectHasExactlyFiveTokens(t *testing.T) {
	for _, k := range All {
		got := k.Subject("quarry")
		if n := len(splitDots(got)); n != 5 {
			t.Errorf("%s -> %s has %d tokens, want 5 (quarry.evt.repo.noun.action)", k, got, n)
		}
	}
	if got, want := LandingOpened.Subject("quarry"), "quarry.evt.quarry.landing.opened"; got != want {
		t.Errorf("subject = %q, want %q", got, want)
	}
}

func splitDots(s string) []string {
	var out []string
	start := 0
	for i := 0; i < len(s); i++ {
		if s[i] == '.' {
			out = append(out, s[start:i])
			start = i + 1
		}
	}
	return append(out, s[start:])
}

// The vocabulary must stay off zit's DAG-transport root. The ingest
// pipeline's fold consumer subscribes to `zit.evt.<repo>.>` and parses
// everything it receives as an artifact, so an event published under that
// root is not merely sharing a stream — it is handed to a consumer that
// will try to fold it. This test is the tripwire on moving it back.
func TestVocabularyStaysOffTheIngestRoot(t *testing.T) {
	for _, k := range All {
		if s := k.Subject("quarry"); len(s) < 7 || s[:7] != "quarry." {
			t.Fatalf("%s publishes to %q; the vocabulary root must be quarry.", k, s)
		}
	}
}