github.com/noqcks/syft@v0.0.0-20230920222752-a9e2c4e288e5/syft/pkg/cataloger/python/parse_requirements_test.go (about) 1 package python 2 3 import ( 4 "testing" 5 6 "github.com/stretchr/testify/assert" 7 8 "github.com/anchore/syft/syft/artifact" 9 "github.com/anchore/syft/syft/file" 10 "github.com/anchore/syft/syft/pkg" 11 "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" 12 ) 13 14 func TestParseRequirementsTxt(t *testing.T) { 15 fixture := "test-fixtures/requires/requirements.txt" 16 locations := file.NewLocationSet(file.NewLocation(fixture)) 17 18 pinnedPkgs := []pkg.Package{ 19 { 20 Name: "flask", 21 Version: "4.0.0", 22 PURL: "pkg:pypi/flask@4.0.0", 23 Locations: locations, 24 Language: pkg.Python, 25 Type: pkg.PythonPkg, 26 MetadataType: pkg.PythonRequirementsMetadataType, 27 Metadata: pkg.PythonRequirementsMetadata{ 28 Name: "flask", 29 VersionConstraint: "== 4.0.0", 30 }, 31 }, 32 { 33 Name: "foo", 34 Version: "1.0.0", 35 PURL: "pkg:pypi/foo@1.0.0", 36 Locations: locations, 37 Language: pkg.Python, 38 Type: pkg.PythonPkg, 39 MetadataType: pkg.PythonRequirementsMetadataType, 40 Metadata: pkg.PythonRequirementsMetadata{ 41 Name: "foo", 42 VersionConstraint: "== 1.0.0", 43 }, 44 }, 45 { 46 Name: "SomeProject", 47 Version: "5.4", 48 PURL: "pkg:pypi/SomeProject@5.4", 49 Locations: locations, 50 Language: pkg.Python, 51 Type: pkg.PythonPkg, 52 MetadataType: pkg.PythonRequirementsMetadataType, 53 Metadata: pkg.PythonRequirementsMetadata{ 54 Name: "SomeProject", 55 VersionConstraint: "==5.4", 56 Markers: "python_version < '3.8'", 57 }, 58 }, 59 { 60 Name: "argh", 61 Version: "0.26.2", 62 PURL: "pkg:pypi/argh@0.26.2", 63 Locations: locations, 64 Language: pkg.Python, 65 Type: pkg.PythonPkg, 66 MetadataType: pkg.PythonRequirementsMetadataType, 67 Metadata: pkg.PythonRequirementsMetadata{ 68 Name: "argh", 69 VersionConstraint: "==0.26.2", 70 }, 71 }, 72 { 73 Name: "argh", 74 Version: "0.26.3", 75 PURL: "pkg:pypi/argh@0.26.3", 76 Locations: locations, 77 Language: pkg.Python, 78 Type: pkg.PythonPkg, 79 MetadataType: pkg.PythonRequirementsMetadataType, 80 Metadata: pkg.PythonRequirementsMetadata{ 81 Name: "argh", 82 VersionConstraint: "==0.26.3", 83 }, 84 }, 85 { 86 Name: "celery", 87 Version: "4.4.7", 88 PURL: "pkg:pypi/celery@4.4.7", 89 Locations: locations, 90 Language: pkg.Python, 91 Type: pkg.PythonPkg, 92 MetadataType: pkg.PythonRequirementsMetadataType, 93 Metadata: pkg.PythonRequirementsMetadata{ 94 Name: "celery", 95 Extras: []string{"redis", "pytest"}, 96 VersionConstraint: "== 4.4.7", 97 }, 98 }, 99 { 100 Name: "GithubSampleProject", 101 Version: "3.7.1", 102 PURL: "pkg:pypi/GithubSampleProject@3.7.1", 103 Locations: locations, 104 Language: pkg.Python, 105 Type: pkg.PythonPkg, 106 MetadataType: pkg.PythonRequirementsMetadataType, 107 Metadata: pkg.PythonRequirementsMetadata{ 108 Name: "GithubSampleProject", 109 VersionConstraint: "== 3.7.1", 110 URL: "git+https://github.com/owner/repo@releases/tag/v3.7.1", 111 }, 112 }, 113 } 114 115 var testCases = []struct { 116 name string 117 fixture string 118 cfg CatalogerConfig 119 expectedPkgs []pkg.Package 120 expectedRelationships []artifact.Relationship 121 }{ 122 { 123 name: "pinned dependencies only", 124 fixture: fixture, 125 cfg: CatalogerConfig{ 126 GuessUnpinnedRequirements: false, 127 }, 128 expectedPkgs: pinnedPkgs, 129 }, 130 { 131 name: "guess unpinned requirements (lowest version)", 132 fixture: fixture, 133 cfg: CatalogerConfig{ 134 GuessUnpinnedRequirements: true, 135 }, 136 expectedPkgs: append([]pkg.Package{ 137 { 138 Name: "Mopidy-Dirble", 139 Version: "1.1", 140 PURL: "pkg:pypi/Mopidy-Dirble@1.1", 141 Locations: locations, 142 Language: pkg.Python, 143 Type: pkg.PythonPkg, 144 MetadataType: pkg.PythonRequirementsMetadataType, 145 Metadata: pkg.PythonRequirementsMetadata{ 146 Name: "Mopidy-Dirble", 147 VersionConstraint: "~= 1.1", 148 }, 149 }, 150 { 151 Name: "sqlalchemy", 152 Version: "2.0.0", 153 PURL: "pkg:pypi/sqlalchemy@2.0.0", 154 Locations: locations, 155 Language: pkg.Python, 156 Type: pkg.PythonPkg, 157 MetadataType: pkg.PythonRequirementsMetadataType, 158 Metadata: pkg.PythonRequirementsMetadata{ 159 Name: "sqlalchemy", 160 VersionConstraint: ">= 1.0.0, <= 2.0.0, != 3.0.0, <= 3.0.0", 161 }, 162 }, 163 { 164 Name: "bar", 165 Version: "2.0.0", 166 PURL: "pkg:pypi/bar@2.0.0", 167 Locations: locations, 168 Language: pkg.Python, 169 Type: pkg.PythonPkg, 170 MetadataType: pkg.PythonRequirementsMetadataType, 171 Metadata: pkg.PythonRequirementsMetadata{ 172 Name: "bar", 173 VersionConstraint: ">= 1.0.0, <= 2.0.0, != 3.0.0, <= 3.0.0", 174 }, 175 }, 176 { 177 Name: "numpy", 178 Version: "3.4.1", 179 PURL: "pkg:pypi/numpy@3.4.1", 180 Locations: locations, 181 Language: pkg.Python, 182 Type: pkg.PythonPkg, 183 MetadataType: pkg.PythonRequirementsMetadataType, 184 Metadata: pkg.PythonRequirementsMetadata{ 185 Name: "numpy", 186 VersionConstraint: ">= 3.4.1", 187 Markers: `sys_platform == 'win32'`, 188 }, 189 }, 190 { 191 Name: "requests", 192 Version: "2.8.0", 193 PURL: "pkg:pypi/requests@2.8.0", 194 Locations: locations, 195 Language: pkg.Python, 196 Type: pkg.PythonPkg, 197 MetadataType: pkg.PythonRequirementsMetadataType, 198 Metadata: pkg.PythonRequirementsMetadata{ 199 Name: "requests", 200 Extras: []string{"security"}, 201 VersionConstraint: "== 2.8.*", 202 Markers: `python_version < "2.7" and sys_platform == "linux"`, 203 }, 204 }, 205 }, pinnedPkgs...), 206 }, 207 } 208 209 for _, tc := range testCases { 210 t.Run(tc.name, func(t *testing.T) { 211 parser := newRequirementsParser(tc.cfg) 212 pkgtest.TestFileParser(t, tc.fixture, parser.parseRequirementsTxt, tc.expectedPkgs, tc.expectedRelationships) 213 }) 214 } 215 } 216 217 func Test_newRequirement(t *testing.T) { 218 219 tests := []struct { 220 name string 221 raw string 222 want *unprocessedRequirement 223 }{ 224 { 225 name: "simple", 226 raw: "requests==2.8", 227 want: &unprocessedRequirement{ 228 Name: "requests", 229 VersionConstraint: "==2.8", 230 }, 231 }, 232 { 233 name: "comment + constraint", 234 raw: "Mopidy-Dirble ~= 1.1 # Compatible release. Same as >= 1.1, == 1.*", 235 want: &unprocessedRequirement{ 236 Name: "Mopidy-Dirble", 237 VersionConstraint: "~= 1.1", 238 }, 239 }, 240 { 241 name: "hashes", 242 raw: "argh==0.26.3 --hash=sha256:a9b3aaa1904eeb78e32394cd46c6f37ac0fb4af6dc488daa58971bdc7d7fcaf3 --hash=sha256:e9535b8c84dc9571a48999094fda7f33e63c3f1b74f3e5f3ac0105a58405bb65", 243 want: &unprocessedRequirement{ 244 Name: "argh", 245 VersionConstraint: "==0.26.3", 246 Hashes: "--hash=sha256:a9b3aaa1904eeb78e32394cd46c6f37ac0fb4af6dc488daa58971bdc7d7fcaf3 --hash=sha256:e9535b8c84dc9571a48999094fda7f33e63c3f1b74f3e5f3ac0105a58405bb65", 247 }, 248 }, 249 { 250 name: "extras", 251 raw: "celery[redis, pytest] == 4.4.7 # should remove [redis, pytest]", 252 want: &unprocessedRequirement{ 253 Name: "celery[redis, pytest]", 254 VersionConstraint: "== 4.4.7", 255 }, 256 }, 257 { 258 name: "url", 259 raw: "GithubSampleProject == 3.7.1 @ git+https://github.com/owner/repo@releases/tag/v3.7.1", 260 want: &unprocessedRequirement{ 261 Name: "GithubSampleProject", 262 VersionConstraint: "== 3.7.1", 263 URL: "git+https://github.com/owner/repo@releases/tag/v3.7.1", 264 }, 265 }, 266 { 267 name: "markers", 268 raw: "numpy >= 3.4.1 ; sys_platform == 'win32'", 269 want: &unprocessedRequirement{ 270 Name: "numpy", 271 VersionConstraint: ">= 3.4.1", 272 Markers: "sys_platform == 'win32'", 273 }, 274 }, 275 } 276 for _, tt := range tests { 277 t.Run(tt.name, func(t *testing.T) { 278 assert.Equal(t, tt.want, newRequirement(tt.raw)) 279 }) 280 } 281 } 282 283 // checkout https://www.darius.page/pipdev/ for help here! (github.com/nok/pipdev) 284 func Test_parseVersion(t *testing.T) { 285 tests := []struct { 286 name string 287 version string 288 guess bool 289 want string 290 }{ 291 { 292 name: "exact", 293 version: "1.0.0", 294 want: "", // we can only parse constraints, not assume that a single version is a pin 295 }, 296 { 297 name: "exact constraint", 298 version: " == 1.0.0 ", 299 want: "1.0.0", 300 }, 301 { 302 name: "resolve lowest, simple constraint", 303 version: " >= 1.0.0 ", 304 guess: true, 305 want: "1.0.0", 306 }, 307 { 308 name: "resolve lowest, compound constraint", 309 version: " < 2.0.0, >= 1.0.0, != 1.1.0 ", 310 guess: true, 311 want: "1.0.0", 312 }, 313 { 314 name: "resolve lowest, handle asterisk", 315 version: "==2.8.*", 316 guess: true, 317 want: "2.8.0", 318 }, 319 { 320 name: "resolve lowest, handle exceptions", 321 version: " !=4.0.2,!=4.1.0,!=4.2.0,>=4.0.1,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0", 322 guess: true, 323 want: "4.0.1", 324 }, 325 { 326 name: "resolve lowest, compatible version constraint", 327 version: "~=0.6.10", // equates to >=0.6.10, ==0.6.* 328 guess: true, 329 want: "0.6.10", 330 }, 331 { 332 name: "resolve lowest, with character in version", 333 version: "~=1.2b,<=1.3a,!=1.1,!=1.2", 334 guess: true, 335 want: "1.3a0", // note: 1.3a == 1.3a0 336 }, 337 } 338 for _, tt := range tests { 339 t.Run(tt.name, func(t *testing.T) { 340 assert.Equal(t, tt.want, parseVersion(tt.version, tt.guess)) 341 }) 342 } 343 }