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