package moves import ( "errors" "testing" "github.com/carlosframework/platform/internal/genselect" "github.com/carlosframework/platform/internal/hostclaim" ) func startClaim(gen string, adm ...hostclaim.AdmissionRecord) *hostclaim.Claim { return &hostclaim.Claim{Holder: &hostclaim.Holder{ Incarnation: "inc-1", Location: "loc/", Generation: gen, Admissions: adm, }} } func startMarker(op string) *genselect.Marker { return &genselect.Marker{Op: op, Incarnation: "inc-1"} } func TestStartCheckOriginEverywhere(t *testing.T) { if err := StartCheck(startClaim(""), nil, nil, ""); err != nil { t.Fatalf("origin everywhere: %v", err) } } func TestStartCheckMarkerNamesTheHoldersGeneration(t *testing.T) { c := startClaim("g2", hostclaim.AdmissionRecord{Generation: "g2", Predecessor: "g1"}) fg := &FilesGen{Op: "g2", Incarnation: "inc-1"} if err := StartCheck(c, startMarker("g2"), fg, "g2"); err != nil { t.Fatalf("all three name the holder's generation: %v", err) } } func TestStartCheckLaggingMarker(t *testing.T) { // The latest admission replaced g1 with g2 and has published nothing. c := startClaim("g2", hostclaim.AdmissionRecord{Generation: "g2", Predecessor: "g1"}) if err := StartCheck(c, startMarker("g1"), nil, ""); err != nil { t.Fatalf("marker naming the latest admission's predecessor: %v", err) } // A restarted probe: the fresh generation F restored from the prepared M; // the files still name M. c = startClaim("F", hostclaim.AdmissionRecord{Generation: "M", Predecessor: "g0"}, hostclaim.AdmissionRecord{Generation: "F", Predecessor: "M"}) fg := &FilesGen{Op: "M", Incarnation: "inc-1"} if err := StartCheck(c, startMarker("M"), fg, "F"); err != nil { t.Fatalf("restarted attempt: %v", err) } } func TestStartCheckChainOfAdmissionsWithoutMarkers(t *testing.T) { c := startClaim("g3", hostclaim.AdmissionRecord{Generation: "g2", Predecessor: "g1"}, hostclaim.AdmissionRecord{Generation: "g3", Predecessor: "g2"}) for _, op := range []string{"g3", "g2", "g1"} { if err := StartCheck(c, startMarker(op), nil, ""); err != nil { t.Errorf("marker %s through the chain: %v", op, err) } } if err := StartCheck(c, startMarker("g0"), nil, ""); !errors.Is(err, ErrStartMarker) { t.Errorf("marker beyond the chain = %v, want ErrStartMarker", err) } // The chain is by predecessor link, not by position: an admission whose // generation the chain never reaches adds nothing. c = startClaim("g3", hostclaim.AdmissionRecord{Generation: "x2", Predecessor: "x1"}, hostclaim.AdmissionRecord{Generation: "g3", Predecessor: "g2"}) if err := StartCheck(c, startMarker("x1"), nil, ""); !errors.Is(err, ErrStartMarker) { t.Errorf("marker off the chain = %v, want ErrStartMarker", err) } } func TestStartCheckChainTerminatesOnACycle(t *testing.T) { // A damaged record whose predecessors loop must still end. c := startClaim("g2", hostclaim.AdmissionRecord{Generation: "g1", Predecessor: "g2"}, hostclaim.AdmissionRecord{Generation: "g2", Predecessor: "g1"}) if err := StartCheck(c, startMarker("g1"), nil, ""); err != nil { t.Errorf("marker in the cycle: %v", err) } if err := StartCheck(c, startMarker("g9"), nil, ""); !errors.Is(err, ErrStartMarker) { t.Errorf("marker outside the cycle = %v, want ErrStartMarker", err) } } func TestStartCheckRejectsFilesBeforePreparedGeneration(t *testing.T) { // M was prepared over S; F is the restarted attempt restored from M. // Files from before the preparation are stale, even though S is an // ancestor of the holder's generation. c := startClaim("F", hostclaim.AdmissionRecord{Generation: "M", Predecessor: "S", Mode: "cold"}, hostclaim.AdmissionRecord{Generation: "F", Predecessor: "M", Mode: "cold"}) if err := StartCheck(c, startMarker("M"), &FilesGen{Op: "S", Incarnation: "inc-1"}, "F"); !errors.Is(err, ErrStartFiles) { t.Errorf("files naming S = %v, want ErrStartFiles", err) } if err := StartCheck(c, startMarker("M"), &FilesGen{Op: "M", Incarnation: "inc-1"}, "F"); err != nil { t.Errorf("files naming M: %v", err) } if err := StartCheck(c, startMarker("M"), &FilesGen{Op: "F", Incarnation: "inc-1"}, "F"); err != nil { t.Errorf("files naming F: %v", err) } } func TestStartCheckLaggingFilesAfterWarmAdmission(t *testing.T) { // Cold F from M, then a warm re-admission of F (F from F): the files // still name M. c := startClaim("F", hostclaim.AdmissionRecord{Generation: "F", Predecessor: "M", Mode: "cold"}, hostclaim.AdmissionRecord{Generation: "F", Predecessor: "F", Mode: "warm"}) if err := StartCheck(c, startMarker("F"), &FilesGen{Op: "M", Incarnation: "inc-1"}, "F"); err != nil { t.Errorf("files naming M after a warm admission: %v", err) } if err := StartCheck(c, startMarker("F"), &FilesGen{Op: "other", Incarnation: "inc-1"}, "F"); !errors.Is(err, ErrStartFiles) { t.Errorf("files naming another generation = %v, want ErrStartFiles", err) } } func TestStartCheckLaggingMarkerAfterWarmAdmission(t *testing.T) { c := startClaim("F", hostclaim.AdmissionRecord{Generation: "F", Predecessor: "M", Mode: "cold"}, hostclaim.AdmissionRecord{Generation: "F", Predecessor: "F", Mode: "warm"}) if err := StartCheck(c, startMarker("M"), nil, ""); err != nil { t.Errorf("marker naming M after a warm admission: %v", err) } } func TestStartCheckOriginAfterAFirstAdmission(t *testing.T) { // The first admission left the origin: its predecessor is the origin, so // an absent marker is lagging, not wrong. c := startClaim("g1", hostclaim.AdmissionRecord{Generation: "g1"}) if err := StartCheck(c, nil, nil, ""); err != nil { t.Fatalf("absent marker, predecessor the origin: %v", err) } // With no admission reaching the origin, absent is refused. c = startClaim("g2", hostclaim.AdmissionRecord{Generation: "g2", Predecessor: "g1"}) if err := StartCheck(c, nil, nil, ""); !errors.Is(err, ErrStartMarker) { t.Errorf("absent marker with a published generation = %v, want ErrStartMarker", err) } } func TestStartCheckMarkerRefusals(t *testing.T) { c := startClaim("g2", hostclaim.AdmissionRecord{Generation: "g2", Predecessor: "g1"}) for name, m := range map[string]*genselect.Marker{ "unrelated generation": startMarker("other"), "another incarnation": {Op: "g2", Incarnation: "inc-2"}, "empty op": {Incarnation: "inc-1"}, "root as an op": startMarker(hostclaim.RootGeneration), } { if err := StartCheck(c, m, nil, ""); !errors.Is(err, ErrStartMarker) { t.Errorf("%s = %v, want ErrStartMarker", name, err) } } if err := StartCheck(&hostclaim.Claim{}, nil, nil, ""); !errors.Is(err, ErrStartMarker) { t.Errorf("no holder = %v, want ErrStartMarker", err) } if err := StartCheck(nil, nil, nil, ""); !errors.Is(err, ErrStartMarker) { t.Errorf("nil claim = %v, want ErrStartMarker", err) } } func TestStartCheckFilesGen(t *testing.T) { c := startClaim("g2", hostclaim.AdmissionRecord{Generation: "g2", Predecessor: "g1"}) m := startMarker("g2") for name, fg := range map[string]*FilesGen{ "another op": {Op: "other", Incarnation: "inc-1"}, "another incarnation": {Op: "g2", Incarnation: "inc-2"}, "empty": {}, } { if err := StartCheck(c, m, fg, ""); !errors.Is(err, ErrStartFiles) { t.Errorf("%s = %v, want ErrStartFiles", name, err) } } // The origin's own files carry no .files-gen, and a stray one is refused. if err := StartCheck(startClaim(""), nil, &FilesGen{Op: "g1", Incarnation: "inc-1"}, ""); !errors.Is(err, ErrStartFiles) { t.Errorf("files-gen on an origin claim = %v, want ErrStartFiles", err) } } func TestStartCheckDBGen(t *testing.T) { c := startClaim("g2", hostclaim.AdmissionRecord{Generation: "g2", Predecessor: "g1"}) m := startMarker("g2") // The .gen is plain and exact: a lagging name is a warm database from an // earlier stay, even though the chain would admit it elsewhere. for _, g := range []string{"other", "g1", hostclaim.RootGeneration} { if err := StartCheck(c, m, nil, g); !errors.Is(err, ErrStartDB) { t.Errorf(".gen %q = %v, want ErrStartDB", g, err) } } if err := StartCheck(c, m, nil, "g2"); err != nil { t.Errorf(".gen g2: %v", err) } if err := StartCheck(startClaim(""), nil, nil, "g1"); !errors.Is(err, ErrStartDB) { t.Errorf(".gen on an origin claim = %v, want ErrStartDB", err) } } func TestStartCheckReportsTheFirstFailingMarker(t *testing.T) { c := startClaim("g2") err := StartCheck(c, startMarker("other"), &FilesGen{Op: "other", Incarnation: "inc-1"}, "other") if !errors.Is(err, ErrStartMarker) || errors.Is(err, ErrStartFiles) || errors.Is(err, ErrStartDB) { t.Errorf("err = %v, want only ErrStartMarker", err) } } func TestPreparedCheck(t *testing.T) { h := hostclaim.Holder{Incarnation: "inc-1", Location: "loc/", Generation: "g1"} ok := &FilesGen{Op: "mv-1", Incarnation: "inc-1"} if err := PreparedCheck("mv-1", h, ok, "mv-1"); err != nil { t.Fatalf("both name the prepared generation: %v", err) } cases := []struct { name string prepared string fg *FilesGen db string want error }{ {"files-gen absent", "mv-1", nil, "mv-1", ErrStartFiles}, {"files-gen another op", "mv-1", &FilesGen{Op: "mv-2", Incarnation: "inc-1"}, "mv-1", ErrStartFiles}, {"files-gen another incarnation", "mv-1", &FilesGen{Op: "mv-1", Incarnation: "inc-2"}, "mv-1", ErrStartFiles}, {"gen absent", "mv-1", ok, "", ErrStartDB}, {"gen another op", "mv-1", ok, "mv-2", ErrStartDB}, {"gen the holder's generation", "mv-1", ok, "g1", ErrStartDB}, {"no prepared generation", "", &FilesGen{}, "", ErrStartFiles}, {"root as prepared", hostclaim.RootGeneration, &FilesGen{Op: hostclaim.RootGeneration, Incarnation: "inc-1"}, hostclaim.RootGeneration, ErrStartFiles}, } for _, tc := range cases { if err := PreparedCheck(tc.prepared, h, tc.fg, tc.db); !errors.Is(err, tc.want) { t.Errorf("%s = %v, want %v", tc.name, err, tc.want) } } }