From 6267554b74a8cfe7512d016191b534713ccad1e0 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 30 Nov 2025 02:03:35 +0000 Subject: [PATCH] Add unit tests for version utility functions Test coverage for pure functions in internal/updates/version.go: - Version.String() - 6 test cases - Version.Compare() - 14 test cases (major/minor/patch/prerelease) - Version.IsNewerThan() - 4 test cases - Version.IsPrerelease() - 4 test cases - compareInts() - 7 test cases - extractRCNumber() - 12 test cases - envBool() - 17 test cases - sanitizePrereleaseIdentifier() - 14 test cases Coverage: 35.2% -> 35.9% --- internal/updates/version_test.go | 377 +++++++++++++++++++++++++++++++ 1 file changed, 377 insertions(+) diff --git a/internal/updates/version_test.go b/internal/updates/version_test.go index d1b9f1d..c50f09b 100644 --- a/internal/updates/version_test.go +++ b/internal/updates/version_test.go @@ -146,3 +146,380 @@ func TestDetectChannelFromVersion(t *testing.T) { t.Fatalf("detectChannelFromVersion stable = %s, expected stable", got) } } + +func TestVersionString(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + version Version + expected string + }{ + { + name: "basic version", + version: Version{Major: 4, Minor: 24, Patch: 0}, + expected: "4.24.0", + }, + { + name: "with prerelease", + version: Version{Major: 4, Minor: 24, Patch: 0, Prerelease: "rc.3"}, + expected: "4.24.0-rc.3", + }, + { + name: "with build metadata", + version: Version{Major: 4, Minor: 24, Patch: 0, Build: "build.123"}, + expected: "4.24.0+build.123", + }, + { + name: "with prerelease and build", + version: Version{Major: 4, Minor: 24, Patch: 0, Prerelease: "rc.3", Build: "build.123"}, + expected: "4.24.0-rc.3+build.123", + }, + { + name: "zero version", + version: Version{Major: 0, Minor: 0, Patch: 0}, + expected: "0.0.0", + }, + { + name: "large numbers", + version: Version{Major: 100, Minor: 200, Patch: 300}, + expected: "100.200.300", + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if got := tc.version.String(); got != tc.expected { + t.Fatalf("Version.String() = %q, expected %q", got, tc.expected) + } + }) + } +} + +func TestVersionCompare(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + v1 Version + v2 Version + expected int + }{ + { + name: "equal versions", + v1: Version{Major: 4, Minor: 24, Patch: 0}, + v2: Version{Major: 4, Minor: 24, Patch: 0}, + expected: 0, + }, + { + name: "v1 major greater", + v1: Version{Major: 5, Minor: 0, Patch: 0}, + v2: Version{Major: 4, Minor: 24, Patch: 0}, + expected: 1, + }, + { + name: "v1 major less", + v1: Version{Major: 3, Minor: 99, Patch: 99}, + v2: Version{Major: 4, Minor: 0, Patch: 0}, + expected: -1, + }, + { + name: "v1 minor greater", + v1: Version{Major: 4, Minor: 25, Patch: 0}, + v2: Version{Major: 4, Minor: 24, Patch: 0}, + expected: 1, + }, + { + name: "v1 minor less", + v1: Version{Major: 4, Minor: 23, Patch: 99}, + v2: Version{Major: 4, Minor: 24, Patch: 0}, + expected: -1, + }, + { + name: "v1 patch greater", + v1: Version{Major: 4, Minor: 24, Patch: 1}, + v2: Version{Major: 4, Minor: 24, Patch: 0}, + expected: 1, + }, + { + name: "v1 patch less", + v1: Version{Major: 4, Minor: 24, Patch: 0}, + v2: Version{Major: 4, Minor: 24, Patch: 1}, + expected: -1, + }, + { + name: "release > prerelease", + v1: Version{Major: 4, Minor: 24, Patch: 0}, + v2: Version{Major: 4, Minor: 24, Patch: 0, Prerelease: "rc.1"}, + expected: 1, + }, + { + name: "prerelease < release", + v1: Version{Major: 4, Minor: 24, Patch: 0, Prerelease: "rc.1"}, + v2: Version{Major: 4, Minor: 24, Patch: 0}, + expected: -1, + }, + { + name: "RC comparison - rc.9 > rc.1", + v1: Version{Major: 4, Minor: 24, Patch: 0, Prerelease: "rc.9"}, + v2: Version{Major: 4, Minor: 24, Patch: 0, Prerelease: "rc.1"}, + expected: 1, + }, + { + name: "RC comparison - rc.1 < rc.9", + v1: Version{Major: 4, Minor: 24, Patch: 0, Prerelease: "rc.1"}, + v2: Version{Major: 4, Minor: 24, Patch: 0, Prerelease: "rc.9"}, + expected: -1, + }, + { + name: "RC comparison - rc9 format", + v1: Version{Major: 4, Minor: 24, Patch: 0, Prerelease: "rc9"}, + v2: Version{Major: 4, Minor: 24, Patch: 0, Prerelease: "rc1"}, + expected: 1, + }, + { + name: "equal prereleases", + v1: Version{Major: 4, Minor: 24, Patch: 0, Prerelease: "rc.3"}, + v2: Version{Major: 4, Minor: 24, Patch: 0, Prerelease: "rc.3"}, + expected: 0, + }, + { + name: "string prerelease comparison", + v1: Version{Major: 4, Minor: 24, Patch: 0, Prerelease: "beta"}, + v2: Version{Major: 4, Minor: 24, Patch: 0, Prerelease: "alpha"}, + expected: 1, // "beta" > "alpha" lexically + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if got := tc.v1.Compare(&tc.v2); got != tc.expected { + t.Fatalf("(%v).Compare(%v) = %d, expected %d", tc.v1, tc.v2, got, tc.expected) + } + }) + } +} + +func TestVersionIsNewerThan(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + v1 Version + v2 Version + expected bool + }{ + { + name: "newer version", + v1: Version{Major: 5, Minor: 0, Patch: 0}, + v2: Version{Major: 4, Minor: 24, Patch: 0}, + expected: true, + }, + { + name: "older version", + v1: Version{Major: 4, Minor: 0, Patch: 0}, + v2: Version{Major: 4, Minor: 24, Patch: 0}, + expected: false, + }, + { + name: "equal versions", + v1: Version{Major: 4, Minor: 24, Patch: 0}, + v2: Version{Major: 4, Minor: 24, Patch: 0}, + expected: false, + }, + { + name: "release newer than prerelease", + v1: Version{Major: 4, Minor: 24, Patch: 0}, + v2: Version{Major: 4, Minor: 24, Patch: 0, Prerelease: "rc.1"}, + expected: true, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if got := tc.v1.IsNewerThan(&tc.v2); got != tc.expected { + t.Fatalf("(%v).IsNewerThan(%v) = %v, expected %v", tc.v1, tc.v2, got, tc.expected) + } + }) + } +} + +func TestVersionIsPrerelease(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + version Version + expected bool + }{ + { + name: "release version", + version: Version{Major: 4, Minor: 24, Patch: 0}, + expected: false, + }, + { + name: "prerelease version", + version: Version{Major: 4, Minor: 24, Patch: 0, Prerelease: "rc.1"}, + expected: true, + }, + { + name: "alpha prerelease", + version: Version{Major: 1, Minor: 0, Patch: 0, Prerelease: "alpha"}, + expected: true, + }, + { + name: "build metadata only is not prerelease", + version: Version{Major: 4, Minor: 24, Patch: 0, Build: "build.123"}, + expected: false, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if got := tc.version.IsPrerelease(); got != tc.expected { + t.Fatalf("(%v).IsPrerelease() = %v, expected %v", tc.version, got, tc.expected) + } + }) + } +} + +func TestCompareInts(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + a int + b int + expected int + }{ + {name: "a < b", a: 1, b: 2, expected: -1}, + {name: "a > b", a: 2, b: 1, expected: 1}, + {name: "a == b", a: 1, b: 1, expected: 0}, + {name: "negative numbers a < b", a: -2, b: -1, expected: -1}, + {name: "negative numbers a > b", a: -1, b: -2, expected: 1}, + {name: "zero comparisons", a: 0, b: 1, expected: -1}, + {name: "large numbers", a: 1000000, b: 1000001, expected: -1}, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if got := compareInts(tc.a, tc.b); got != tc.expected { + t.Fatalf("compareInts(%d, %d) = %d, expected %d", tc.a, tc.b, got, tc.expected) + } + }) + } +} + +func TestExtractRCNumber(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + prerelease string + expected int + }{ + {name: "rc.1", prerelease: "rc.1", expected: 1}, + {name: "rc.9", prerelease: "rc.9", expected: 9}, + {name: "rc1", prerelease: "rc1", expected: 1}, + {name: "rc9", prerelease: "rc9", expected: 9}, + {name: "RC.3 uppercase", prerelease: "RC.3", expected: 3}, + {name: "RC5 uppercase no dot", prerelease: "RC5", expected: 5}, + {name: "rc.10 double digit", prerelease: "rc.10", expected: 10}, + {name: "rc.123 triple digit", prerelease: "rc.123", expected: 123}, + {name: "alpha no RC", prerelease: "alpha", expected: -1}, + {name: "beta.1 no RC", prerelease: "beta.1", expected: -1}, + {name: "empty string", prerelease: "", expected: -1}, + {name: "just rc no number", prerelease: "rc", expected: -1}, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if got := extractRCNumber(tc.prerelease); got != tc.expected { + t.Fatalf("extractRCNumber(%q) = %d, expected %d", tc.prerelease, got, tc.expected) + } + }) + } +} + +func TestEnvBool(t *testing.T) { + tests := []struct { + name string + value string + expected bool + }{ + {name: "1 is true", value: "1", expected: true}, + {name: "true is true", value: "true", expected: true}, + {name: "TRUE is true", value: "TRUE", expected: true}, + {name: "True is true", value: "True", expected: true}, + {name: "yes is true", value: "yes", expected: true}, + {name: "YES is true", value: "YES", expected: true}, + {name: "on is true", value: "on", expected: true}, + {name: "ON is true", value: "ON", expected: true}, + {name: "0 is false", value: "0", expected: false}, + {name: "false is false", value: "false", expected: false}, + {name: "FALSE is false", value: "FALSE", expected: false}, + {name: "no is false", value: "no", expected: false}, + {name: "off is false", value: "off", expected: false}, + {name: "empty is false", value: "", expected: false}, + {name: "random string is false", value: "random", expected: false}, + {name: "whitespace true", value: " true ", expected: true}, + {name: "whitespace 1", value: " 1 ", expected: true}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + key := "TEST_ENV_BOOL_" + tc.name + t.Setenv(key, tc.value) + if got := envBool(key); got != tc.expected { + t.Fatalf("envBool(%q) with value %q = %v, expected %v", key, tc.value, got, tc.expected) + } + }) + } +} + +func TestSanitizePrereleaseIdentifier(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + expected string + }{ + {name: "simple branch", input: "feature-x", expected: "feature-x"}, + {name: "branch with slash", input: "feature/new-api", expected: "feature-new-api"}, + {name: "branch with multiple slashes", input: "user/feature/test", expected: "user-feature-test"}, + {name: "uppercase converts to lowercase", input: "FEATURE-X", expected: "feature-x"}, + {name: "mixed case", input: "Feature-X", expected: "feature-x"}, + {name: "numbers preserved", input: "issue-123", expected: "issue-123"}, + {name: "dots preserved", input: "v1.2.3", expected: "v1.2.3"}, + {name: "special chars replaced", input: "feat@test#123", expected: "feat-test-123"}, + {name: "consecutive separators collapsed", input: "feature//test", expected: "feature-test"}, + {name: "leading separator trimmed", input: "-feature", expected: "feature"}, + {name: "trailing separator trimmed", input: "feature-", expected: "feature"}, + {name: "empty string", input: "", expected: "dev"}, + {name: "whitespace only", input: " ", expected: "dev"}, + {name: "only special chars", input: "@#$%", expected: "dev"}, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if got := sanitizePrereleaseIdentifier(tc.input); got != tc.expected { + t.Fatalf("sanitizePrereleaseIdentifier(%q) = %q, expected %q", tc.input, got, tc.expected) + } + }) + } +}