package ci
import (
"encoding/json"
"strings"
"testing"
)
// The bug: a pipeline was registered under its file's stem alone, so
// two repositories with the same file name shared one definition on the
// engine — and quarry re-registers immediately before every run, so the
// last writer won and a run could execute the other's pipeline.
func TestTwoRepositoriesDoNotShareAPipelineName(t *testing.T) {
alpha := ScopedName("alpha", "build")
beta := ScopedName("beta", "build")
if alpha == beta {
t.Fatalf("FAIL: %q == %q — the collision this exists to remove", alpha, beta)
}
t.Logf("ok: alpha/build and beta/build register apart (%s, %s)", alpha, beta)
}
// The ambiguity a plain dotted join leaves behind, which is the same one
// dagnats documents for its own version keys. Repository names may
// contain dots and so may file stems, so the join alone is not enough.
func TestADottedNameCannotBeConfusedWithAnother(t *testing.T) {
a := ScopedName("a.b", "c")
b := ScopedName("a", "b.c")
if a == b {
t.Fatalf("FAIL: %q == %q — a fixed-length digest is what separates these", a, b)
}
if !strings.HasPrefix(a, "a.b.c.") || !strings.HasPrefix(b, "a.b.c.") {
t.Fatalf("both should stay readable as a.b.c.<digest>, got %q and %q", a, b)
}
t.Logf("ok: %s and %s differ only where they must", a, b)
}
// Stable across calls: the name is recomputed before every run, and a
// name that moved would register a second definition each time.
func TestTheNameIsStable(t *testing.T) {
if ScopedName("quarry", "quarry") != ScopedName("quarry", "quarry") {
t.Fatal("FAIL: the name is not stable")
}
t.Log("ok: the same pair always names the same workflow")
}
// The readable half is kept, because this is what shows in the engine's
// console and a name nobody can read is a name nobody can debug.
func TestTheNameStaysReadable(t *testing.T) {
got := ScopedName("quarry", "build")
if !strings.HasPrefix(got, "quarry.build.") {
t.Fatalf("FAIL: %q does not lead with the repository and the pipeline", got)
}
t.Logf("ok: %s", got)
}
// A JSON pipeline carries its own name, so registering one under
// quarry's name must replace that field and keep everything else — the
// struct knows only what quarry reads, and most of a pipeline is not it.
func TestRenamingAJSONPipelineKeepsTheRest(t *testing.T) {
def := []byte(`{"name":"build","steps":[{"task":"shell"}],"unknown_field":{"kept":true}}`)
var raw map[string]any
if err := json.Unmarshal(def, &raw); err != nil {
t.Fatal(err)
}
raw["name"] = ScopedName("alpha", "build")
out, err := json.Marshal(raw)
if err != nil {
t.Fatal(err)
}
var back map[string]any
if err := json.Unmarshal(out, &back); err != nil {
t.Fatal(err)
}
if back["unknown_field"] == nil {
t.Fatal("FAIL: a field quarry does not read was dropped by the rename")
}
if back["name"] == "build" {
t.Fatal("FAIL: the name was not replaced")
}
t.Log("ok: the rename keeps fields quarry never reads")
}
// A cancelled run is not a verdict. quarry cancels runs itself when a
// newer check-in lands on the same branch, so recording one as a
// failure marks a check-in that was never finished being tested — in a
// tag artifact that clones with the repository.
func TestACancelledRunIsNeitherPassedNorAFailure(t *testing.T) {
for _, tc := range []struct {
status string
terminal, passed bool
cancelled, verdict bool
why string
}{
{"completed", true, true, false, true, "a finished run earns ci-pass"},
{"failed", true, false, false, true, "a run that failed earns ci-fail"},
{"error", true, false, false, true, "an errored run earns ci-fail"},
{"cancelled", true, false, true, false, "a superseded run earns nothing"},
{"canceled", true, false, true, false, "the other spelling too"},
{"running", false, false, false, false, "an unfinished run earns nothing yet"},
} {
r := Run{Status: tc.status}
if r.Terminal() != tc.terminal || r.Passed() != tc.passed || r.Cancelled() != tc.cancelled {
t.Errorf("FAIL: %q: terminal=%v passed=%v cancelled=%v", tc.status,
r.Terminal(), r.Passed(), r.Cancelled())
continue
}
// The rule the caller applies: a verdict is written for a
// terminal run that was not cancelled.
if got := r.Terminal() && !r.Cancelled(); got != tc.verdict {
t.Errorf("FAIL: %q: writes a verdict = %v, want %v — %s", tc.status, got, tc.verdict, tc.why)
continue
}
t.Logf("ok: %s", tc.why)
}
}