// SPDX-FileCopyrightText: 2026 Echolot contributors // SPDX-License-Identifier: GPL-3.0-or-later package compat import "testing" func TestParseAcceptsTheFormsThatActuallyReachUs(t *testing.T) { cases := map[string]Version{ "1.2.3": {Major: 1, Minor: 2, Patch: 3}, "v1.2.3": {Major: 1, Minor: 2, Patch: 3}, "server-v0.4.2": {Minor: 4, Patch: 2}, " 0.2.0 ": {Minor: 2}, "1.0.0-rc1": {Major: 1, Pre: "rc1"}, "1.0.0+build.7": {Major: 1}, "1.0.0-rc1+meta": {Major: 1, Pre: "rc1"}, } for in, want := range cases { got, ok := Parse(in) if !ok || got != want { t.Errorf("Parse(%q) = %v,%v; want %v", in, got, ok, want) } } // A pre-release identifier containing a "v" must not be mistaken for a tag prefix. if got, ok := Parse("1.2.3-rcv1"); !ok || got.Pre != "rcv1" || got.Major != 1 { t.Errorf("Parse(1.2.3-rcv1) = %v,%v", got, ok) } for _, bad := range []string{"", "dev", "1.2", "1.2.3.4", "x.y.z", "-1.0.0", "1.2.beta"} { if _, ok := Parse(bad); ok { t.Errorf("Parse(%q) should have failed", bad) } } } func TestCompareOrdersPreReleasesBelowTheirRelease(t *testing.T) { lt := func(a, b string) { t.Helper() x, _ := Parse(a) y, _ := Parse(b) if x.Compare(y) != -1 || y.Compare(x) != 1 { t.Errorf("expected %s < %s", a, b) } } lt("0.9.9", "1.0.0") lt("1.0.0", "1.0.1") lt("1.0.0", "1.1.0") lt("1.0.0-rc1", "1.0.0") lt("1.0.0-rc1", "1.0.0-rc2") a, _ := Parse("1.2.3") b, _ := Parse("v1.2.3") if a.Compare(b) != 0 { t.Error("the same version written two ways must compare equal") } } // Below 1.0.0 the minor is the breaking axis. Treating 1.0.0 as the next break for a 0.x build // would let a 0.5 server accept a 0.4 client it cannot actually talk to. func TestNextBreakingUsesTheMinorBelowOne(t *testing.T) { cases := map[string]string{ "0.4.2": "0.5.0", "0.0.9": "0.1.0", "1.2.3": "2.0.0", "2.0.0": "3.0.0", } for in, want := range cases { v, _ := Parse(in) if got := v.NextBreaking().String(); got != want { t.Errorf("NextBreaking(%s) = %s, want %s", in, got, want) } } } func TestRangeIsMinInclusiveMaxExclusive(t *testing.T) { r, err := ParseRange("0.2.0", "1.0.0") if err != nil { t.Fatal(err) } in := []string{"0.2.0", "0.2.1", "0.9.9", "1.0.0-rc1"} out := []string{"0.1.9", "1.0.0", "1.0.1", "2.0.0"} for _, s := range in { v, _ := Parse(s) if !r.Contains(v) { t.Errorf("%s should be inside %s", s, r) } } for _, s := range out { v, _ := Parse(s) if r.Contains(v) { t.Errorf("%s should be outside %s", s, r) } } } func TestUnboundedRangeHasNoCeiling(t *testing.T) { r, err := ParseRange("0.2.0", "") if err != nil { t.Fatal(err) } v, _ := Parse("99.0.0") if !r.Contains(v) { t.Error("an empty maximum must mean unbounded") } } // A typo in an operator's config must not silently disable the restriction it was meant to set. func TestMalformedBoundsAreErrorsNotSilentPermissiveness(t *testing.T) { for _, c := range [][2]string{ {"nonsense", "1.0.0"}, {"0.2.0", "nonsense"}, {"1.0.0", "0.9.0"}, // max below min {"1.0.0", "1.0.0"}, // empty window: nothing could ever satisfy it } { if _, err := ParseRange(c[0], c[1]); err == nil { t.Errorf("ParseRange(%q, %q) should have failed", c[0], c[1]) } } } func TestCheckExplainsItself(t *testing.T) { r, _ := ParseRange("0.2.0", "1.0.0") if v, msg := Check("0.5.0", r, "app"); v != OK || msg != "" { t.Errorf("in-range check should pass silently: %v %q", v, msg) } v, msg := Check("0.1.0", r, "app") if v != TooOld { t.Fatalf("want TooOld, got %v", v) } for _, want := range []string{"0.1.0", "0.2.0", "Update"} { if !contains(msg, want) { t.Errorf("the refusal must name %q so the user can act on it: %q", want, msg) } } if v, _ := Check("1.4.0", r, "app"); v != TooNew { t.Errorf("want TooNew, got %v", v) } // A development build reports "dev". Locking developers out of their own server would be a // poor trade for a check that exists to prevent confusing failures. if v, msg := Check("dev", r, "server"); v != Unknown || msg == "" { t.Errorf("unparseable version should be Unknown with an explanation, got %v %q", v, msg) } } func contains(h, n string) bool { return len(h) >= len(n) && (h == n || len(n) == 0 || indexOf(h, n) >= 0) } func indexOf(h, n string) int { for i := 0; i+len(n) <= len(h); i++ { if h[i:i+len(n)] == n { return i } } return -1 }