|
8 | 8 | "encoding/hex" |
9 | 9 | "os" |
10 | 10 | "path/filepath" |
| 11 | + "sort" |
11 | 12 | "strings" |
12 | 13 | "testing" |
13 | 14 | ) |
@@ -123,6 +124,176 @@ func TestFindViaAPI_ReleaseSorting(t *testing.T) { |
123 | 124 | }) |
124 | 125 | } |
125 | 126 |
|
| 127 | +// ============================================================================= |
| 128 | +// Test 5: Release Version Semantic Sort Bug |
| 129 | +// ============================================================================= |
| 130 | + |
| 131 | +// TestReleaseVersionSemanticSortBug demonstrates the bug where lexicographic |
| 132 | +// sorting picks the wrong version when patch numbers have different digit counts. |
| 133 | +// Example: lib-8.0.1.10 should be > lib-8.0.1.2 semantically, but |
| 134 | +// lexicographically lib-8.0.1.10 < lib-8.0.1.2 (string "10" < "2") |
| 135 | +func TestReleaseVersionSemanticSortBug(t *testing.T) { |
| 136 | + t.Run("lexicographic_sort_picks_wrong_version", func(t *testing.T) { |
| 137 | + // Releases with double-digit patch number |
| 138 | + releases := []string{ |
| 139 | + "lib-8.0.1.2", |
| 140 | + "lib-8.0.1.10", |
| 141 | + } |
| 142 | + |
| 143 | + // Use sort.Strings (current implementation in fetch.go line 169) |
| 144 | + sorted := make([]string, len(releases)) |
| 145 | + copy(sorted, releases) |
| 146 | + sort.Strings(sorted) |
| 147 | + |
| 148 | + // With lexicographic sort, "lib-8.0.1.10" comes before "lib-8.0.1.2" |
| 149 | + // because '1' < '2' when comparing character by character |
| 150 | + if sorted[0] != "lib-8.0.1.10" { |
| 151 | + t.Errorf("Expected lib-8.0.1.10 to be first (lexicographically), got %s", sorted[0]) |
| 152 | + } |
| 153 | + if sorted[1] != "lib-8.0.1.2" { |
| 154 | + t.Errorf("Expected lib-8.0.1.2 to be last (lexicographically), got %s", sorted[1]) |
| 155 | + } |
| 156 | + |
| 157 | + // The bug: last element is selected as "highest" version |
| 158 | + selectedVersion := sorted[len(sorted)-1] |
| 159 | + |
| 160 | + // BUG: This selects lib-8.0.1.2 instead of lib-8.0.1.10 |
| 161 | + if selectedVersion != "lib-8.0.1.2" { |
| 162 | + t.Errorf("Expected bug to select lib-8.0.1.2 (lexicographically last), got %s", selectedVersion) |
| 163 | + } |
| 164 | + |
| 165 | + t.Logf("BUG: Lexicographic sort selected %s instead of semantically correct lib-8.0.1.10", selectedVersion) |
| 166 | + }) |
| 167 | + |
| 168 | + t.Run("demonstrates_bug_with_realistic_release_sequence", func(t *testing.T) { |
| 169 | + // Realistic scenario: multiple patch releases |
| 170 | + releases := []string{ |
| 171 | + "lib-8.0.1.0", |
| 172 | + "lib-8.0.1.1", |
| 173 | + "lib-8.0.1.2", |
| 174 | + "lib-8.0.1.3", |
| 175 | + "lib-8.0.1.10", // Latest release (semantic version 8.0.1.10) |
| 176 | + } |
| 177 | + |
| 178 | + sorted := make([]string, len(releases)) |
| 179 | + copy(sorted, releases) |
| 180 | + sort.Strings(sorted) |
| 181 | + |
| 182 | + // Lexicographic sort order: 0, 1, 10, 2, 3 |
| 183 | + expectedLexOrder := []string{ |
| 184 | + "lib-8.0.1.0", |
| 185 | + "lib-8.0.1.1", |
| 186 | + "lib-8.0.1.10", // BUG: 10 comes before 2 lexicographically |
| 187 | + "lib-8.0.1.2", |
| 188 | + "lib-8.0.1.3", |
| 189 | + } |
| 190 | + |
| 191 | + for i, expected := range expectedLexOrder { |
| 192 | + if sorted[i] != expected { |
| 193 | + t.Errorf("Position %d: expected %s, got %s", i, expected, sorted[i]) |
| 194 | + } |
| 195 | + } |
| 196 | + |
| 197 | + // The bug: selects lib-8.0.1.3 instead of lib-8.0.1.10 |
| 198 | + selectedVersion := sorted[len(sorted)-1] |
| 199 | + if selectedVersion != "lib-8.0.1.3" { |
| 200 | + t.Errorf("Expected bug to select lib-8.0.1.3, got %s", selectedVersion) |
| 201 | + } |
| 202 | + |
| 203 | + t.Logf("BUG: Selected %s instead of latest release lib-8.0.1.10", selectedVersion) |
| 204 | + t.Logf("Lexicographic order: %v", sorted) |
| 205 | + }) |
| 206 | + |
| 207 | + t.Run("bug_affects_all_double_digit_versions", func(t *testing.T) { |
| 208 | + // Test that any double-digit component causes the issue |
| 209 | + testCases := []struct { |
| 210 | + name string |
| 211 | + releases []string |
| 212 | + wrongSelection string // What gets selected (lexicographically last) |
| 213 | + correctSelection string // What should be selected (semantically latest) |
| 214 | + }{ |
| 215 | + { |
| 216 | + name: "patch_version_10_vs_9", |
| 217 | + releases: []string{"lib-8.0.1.9", "lib-8.0.1.10"}, |
| 218 | + wrongSelection: "lib-8.0.1.9", |
| 219 | + correctSelection: "lib-8.0.1.10", |
| 220 | + }, |
| 221 | + { |
| 222 | + name: "patch_version_19_vs_100", |
| 223 | + releases: []string{"lib-8.0.1.19", "lib-8.0.1.100"}, |
| 224 | + wrongSelection: "lib-8.0.1.19", |
| 225 | + correctSelection: "lib-8.0.1.100", |
| 226 | + }, |
| 227 | + { |
| 228 | + name: "patch_version_2_vs_12", |
| 229 | + releases: []string{"lib-8.0.1.2", "lib-8.0.1.12"}, |
| 230 | + wrongSelection: "lib-8.0.1.2", |
| 231 | + correctSelection: "lib-8.0.1.12", |
| 232 | + }, |
| 233 | + } |
| 234 | + |
| 235 | + for _, tc := range testCases { |
| 236 | + t.Run(tc.name, func(t *testing.T) { |
| 237 | + sorted := make([]string, len(tc.releases)) |
| 238 | + copy(sorted, tc.releases) |
| 239 | + sort.Strings(sorted) |
| 240 | + |
| 241 | + selectedVersion := sorted[len(sorted)-1] |
| 242 | + |
| 243 | + // Verify the bug: lexicographic sort picks wrong version |
| 244 | + if selectedVersion != tc.wrongSelection { |
| 245 | + t.Errorf("Expected bug to select %s (lexicographically), got %s", tc.wrongSelection, selectedVersion) |
| 246 | + } |
| 247 | + |
| 248 | + // Document what should be selected semantically |
| 249 | + if selectedVersion == tc.correctSelection { |
| 250 | + t.Logf("Note: This case happens to work correctly") |
| 251 | + } else { |
| 252 | + t.Logf("BUG: Selected %s instead of semantically correct %s", selectedVersion, tc.correctSelection) |
| 253 | + } |
| 254 | + }) |
| 255 | + } |
| 256 | + }) |
| 257 | + |
| 258 | + t.Run("documents_correct_semantic_version_comparison", func(t *testing.T) { |
| 259 | + // This test documents how semantic versioning should work |
| 260 | + // to prevent the bug in future implementations |
| 261 | + |
| 262 | + type semver struct { |
| 263 | + prefix string |
| 264 | + major int |
| 265 | + minor int |
| 266 | + patch int |
| 267 | + build int |
| 268 | + } |
| 269 | + |
| 270 | + releases := []semver{ |
| 271 | + {prefix: "lib", major: 8, minor: 0, patch: 1, build: 0}, |
| 272 | + {prefix: "lib", major: 8, minor: 0, patch: 1, build: 2}, |
| 273 | + {prefix: "lib", major: 8, minor: 0, patch: 1, build: 10}, |
| 274 | + } |
| 275 | + |
| 276 | + // Find semantically highest version |
| 277 | + highest := releases[0] |
| 278 | + for _, r := range releases { |
| 279 | + if r.major > highest.major || |
| 280 | + (r.major == highest.major && r.minor > highest.minor) || |
| 281 | + (r.major == highest.major && r.minor == highest.minor && r.patch > highest.patch) || |
| 282 | + (r.major == highest.major && r.minor == highest.minor && r.patch == highest.patch && r.build > highest.build) { |
| 283 | + highest = r |
| 284 | + } |
| 285 | + } |
| 286 | + |
| 287 | + // Semantically, lib-8.0.1.10 should be selected |
| 288 | + if highest.build != 10 { |
| 289 | + t.Errorf("Semantic version comparison failed: expected build 10, got %d", highest.build) |
| 290 | + } |
| 291 | + |
| 292 | + t.Logf("Correct semantic selection: %s-%d.%d.%d.%d", |
| 293 | + highest.prefix, highest.major, highest.minor, highest.patch, highest.build) |
| 294 | + }) |
| 295 | +} |
| 296 | + |
126 | 297 | // ============================================================================= |
127 | 298 | // Test 1.2: Checksum Verification Robustness |
128 | 299 | // ============================================================================= |
|
0 commit comments