github.com/google/osv-scalibr@v0.4.1/semantic/compare_test.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package semantic_test 16 17 import ( 18 "bufio" 19 "errors" 20 "os" 21 "strings" 22 "testing" 23 24 "github.com/google/osv-scalibr/semantic" 25 ) 26 27 func expectedResult(t *testing.T, comparator string) int { 28 t.Helper() 29 30 switch comparator { 31 case "<": 32 return -1 33 case "=": 34 return 0 35 case ">": 36 return +1 37 default: 38 t.Fatalf("unknown comparator %s", comparator) 39 40 return -999 41 } 42 } 43 44 func compareWord(t *testing.T, result int) string { 45 t.Helper() 46 47 switch result { 48 case 1: 49 return "greater than" 50 case 0: 51 return "equal to" 52 case -1: 53 return "less than" 54 default: 55 t.Fatalf("Unexpected compare result: %d\n", result) 56 57 return "" 58 } 59 } 60 61 func runAgainstEcosystemFixture(t *testing.T, ecosystem string, filename string) { 62 t.Helper() 63 64 file, err := os.Open("testdata/" + filename) 65 if err != nil { 66 t.Fatalf("Failed to read fixture file: %v", err) 67 } 68 69 defer file.Close() 70 71 scanner := bufio.NewScanner(file) 72 73 total := 0 74 failed := 0 75 76 for scanner.Scan() { 77 line := scanner.Text() 78 79 if line == "" || 80 strings.HasPrefix(line, "# ") || 81 strings.HasPrefix(line, "// ") { 82 continue 83 } 84 85 total++ 86 pieces := strings.Split(line, " ") 87 88 if len(pieces) != 3 { 89 t.Fatalf(`incorrect number of peices in fixture "%s" (got %d)`, line, len(pieces)) 90 } 91 92 result := expectEcosystemCompareResult(t, ecosystem, pieces[0], pieces[1], pieces[2]) 93 94 if !result { 95 failed++ 96 } 97 } 98 99 if failed > 0 { 100 t.Errorf("%d of %d failed", failed, total) 101 } 102 103 if err = scanner.Err(); err != nil { 104 t.Fatal(err) 105 } 106 } 107 108 func parseAsVersion(t *testing.T, str string, ecosystem string) semantic.Version { 109 t.Helper() 110 111 v, err := semantic.Parse(str, ecosystem) 112 113 if err != nil { 114 t.Fatalf("failed to parse version '%s' as ecosystem '%s': %v", str, ecosystem, err) 115 } 116 117 return v 118 } 119 120 func expectCompareResult( 121 t *testing.T, 122 ecosystem string, 123 a string, 124 b string, 125 expectedResult int, 126 ) bool { 127 t.Helper() 128 129 v := parseAsVersion(t, a, ecosystem) 130 131 actualResult, err := v.CompareStr(b) 132 133 if err != nil { 134 t.Fatalf("failed to compare versions: %v", err) 135 } 136 137 if actualResult != expectedResult { 138 t.Errorf( 139 "Expected %s to be %s %s, but it was %s", 140 a, 141 compareWord(t, expectedResult), 142 b, 143 compareWord(t, actualResult), 144 ) 145 146 return false 147 } 148 149 return true 150 } 151 152 func expectEcosystemCompareResult( 153 t *testing.T, 154 ecosystem string, 155 a string, 156 c string, 157 b string, 158 ) (success bool) { 159 t.Helper() 160 161 success = success || expectCompareResult(t, 162 ecosystem, a, b, 163 +expectedResult(t, c), 164 ) 165 166 success = success && expectCompareResult(t, 167 ecosystem, b, a, 168 -expectedResult(t, c), 169 ) 170 171 return success 172 } 173 174 func TestVersion_Compare_Ecosystems(t *testing.T) { 175 tests := []struct { 176 name string 177 file string 178 }{ 179 { 180 name: "npm", 181 file: "semver-versions.txt", 182 }, 183 { 184 name: "crates.io", 185 file: "semver-versions.txt", 186 }, 187 { 188 name: "RubyGems", 189 file: "rubygems-versions.txt", 190 }, 191 { 192 name: "RubyGems", 193 file: "rubygems-versions-generated.txt", 194 }, 195 { 196 name: "NuGet", 197 file: "nuget-versions.txt", 198 }, 199 { 200 name: "Packagist", 201 file: "packagist-versions.txt", 202 }, 203 { 204 name: "Packagist", 205 file: "packagist-versions-generated.txt", 206 }, 207 { 208 name: "Go", 209 file: "semver-versions.txt", 210 }, 211 { 212 name: "Hex", 213 file: "semver-versions.txt", 214 }, 215 { 216 name: "Maven", 217 file: "maven-versions.txt", 218 }, 219 { 220 name: "Maven", 221 file: "maven-versions-generated.txt", 222 }, 223 { 224 name: "PyPI", 225 file: "pypi-versions.txt", 226 }, 227 { 228 name: "PyPI", 229 file: "pypi-versions-generated.txt", 230 }, 231 { 232 name: "Debian", 233 file: "debian-versions.txt", 234 }, 235 { 236 name: "Debian", 237 file: "debian-versions-generated.txt", 238 }, 239 { 240 name: "CRAN", 241 file: "cran-versions.txt", 242 }, 243 { 244 name: "CRAN", 245 file: "cran-versions-generated.txt", 246 }, 247 { 248 name: "Alpine", 249 file: "alpine-versions.txt", 250 }, 251 { 252 name: "Alpine", 253 file: "alpine-versions-generated.txt", 254 }, 255 { 256 name: "Red Hat", 257 file: "redhat-versions.txt", 258 }, 259 { 260 name: "Hackage", 261 file: "hackage-versions.txt", 262 }, 263 { 264 name: "Pub", 265 file: "pub-versions.txt", 266 }, 267 } 268 for _, tt := range tests { 269 t.Run(tt.name, func(t *testing.T) { 270 runAgainstEcosystemFixture(t, tt.name, tt.file) 271 }) 272 } 273 } 274 275 func TestVersion_Compare_Debian_InvalidVersion(t *testing.T) { 276 v := parseAsVersion(t, "1.2.3", "Debian") 277 278 _, err := v.CompareStr("1.2.3-not-a-debian:version!@#$") 279 280 if err == nil { 281 t.Fatalf("expected error comparing invalid version") 282 } 283 284 if !errors.Is(err, semantic.ErrInvalidVersion) { 285 t.Errorf("expected ErrInvalidVersion, got '%v'", err) 286 } 287 }