github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/language/python/setup/setup_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 // Copyright 2024 Google LLC 16 // 17 // Licensed under the Apache License, Version 2.0 (the "License"); 18 // you may not use this file except in compliance with the License. 19 // You may obtain a copy of the License at 20 // 21 // http://www.apache.org/licenses/LICENSE-2.0 22 // 23 // Unless required by applicable law or agreed to in writing, software 24 // distributed under the License is distributed on an "AS IS" BASIS, 25 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 26 // See the License for the specific language governing permissions and 27 // limitations under the License. 28 29 package setup_test 30 31 import ( 32 "io/fs" 33 "path/filepath" 34 "testing" 35 36 "github.com/google/go-cmp/cmp" 37 "github.com/google/go-cmp/cmp/cmpopts" 38 "github.com/google/osv-scalibr/extractor" 39 "github.com/google/osv-scalibr/extractor/filesystem" 40 "github.com/google/osv-scalibr/extractor/filesystem/internal/units" 41 "github.com/google/osv-scalibr/extractor/filesystem/language/python/setup" 42 "github.com/google/osv-scalibr/extractor/filesystem/simplefileapi" 43 "github.com/google/osv-scalibr/inventory" 44 "github.com/google/osv-scalibr/purl" 45 "github.com/google/osv-scalibr/stats" 46 "github.com/google/osv-scalibr/testing/extracttest" 47 "github.com/google/osv-scalibr/testing/fakefs" 48 "github.com/google/osv-scalibr/testing/testcollector" 49 ) 50 51 func TestNew(t *testing.T) { 52 tests := []struct { 53 name string 54 cfg setup.Config 55 wantCfg setup.Config 56 }{ 57 { 58 name: "default", 59 cfg: setup.DefaultConfig(), 60 wantCfg: setup.Config{ 61 MaxFileSizeBytes: 10 * units.MiB, 62 }, 63 }, 64 { 65 name: "custom", 66 cfg: setup.Config{ 67 MaxFileSizeBytes: 10, 68 }, 69 wantCfg: setup.Config{ 70 MaxFileSizeBytes: 10, 71 }, 72 }, 73 } 74 75 for _, tt := range tests { 76 t.Run(tt.name, func(t *testing.T) { 77 got := setup.New(tt.cfg) 78 if diff := cmp.Diff(tt.wantCfg, got.Config()); diff != "" { 79 t.Errorf("New(%+v).Config(): (-want +got):\n%s", tt.cfg, diff) 80 } 81 }) 82 } 83 } 84 85 func TestFileRequired(t *testing.T) { 86 tests := []struct { 87 name string 88 path string 89 fileSizeBytes int64 90 maxFileSizeBytes int64 91 wantRequired bool 92 wantResultMetric stats.FileRequiredResult 93 }{ 94 { 95 name: "setup.py file", 96 path: "software-develop/setup.py", 97 wantRequired: true, 98 wantResultMetric: stats.FileRequiredResultOK, 99 }, 100 { 101 name: "setup.py file required if file size < max file size", 102 path: "software-develop/setup.py", 103 fileSizeBytes: 100 * units.KiB, 104 maxFileSizeBytes: 1000 * units.KiB, 105 wantRequired: true, 106 wantResultMetric: stats.FileRequiredResultOK, 107 }, 108 { 109 name: "setup.py file required if file size == max file size", 110 path: "software-develop/setup.py", 111 fileSizeBytes: 1000 * units.KiB, 112 maxFileSizeBytes: 1000 * units.KiB, 113 wantRequired: true, 114 wantResultMetric: stats.FileRequiredResultOK, 115 }, 116 { 117 name: "setup.py file not required if file size > max file size", 118 path: "software-develop/setup.py", 119 fileSizeBytes: 1000 * units.KiB, 120 maxFileSizeBytes: 100 * units.KiB, 121 wantRequired: false, 122 wantResultMetric: stats.FileRequiredResultSizeLimitExceeded, 123 }, 124 { 125 name: "setup.py file required if max file size set to 0", 126 path: "software-develop/setup.py", 127 fileSizeBytes: 100 * units.KiB, 128 maxFileSizeBytes: 0, 129 wantRequired: true, 130 wantResultMetric: stats.FileRequiredResultOK, 131 }, 132 { 133 name: "invalid", 134 path: "software-develop/setup.py/foo", 135 wantRequired: false, 136 }, 137 { 138 name: "invalid", 139 path: "software-develop/foo/foosetup.py", 140 wantRequired: false, 141 }, 142 } 143 144 for _, tt := range tests { 145 t.Run(tt.name, func(t *testing.T) { 146 collector := testcollector.New() 147 var e filesystem.Extractor = setup.New(setup.Config{ 148 Stats: collector, 149 MaxFileSizeBytes: tt.maxFileSizeBytes, 150 }) 151 152 fileSizeBytes := tt.fileSizeBytes 153 if fileSizeBytes == 0 { 154 fileSizeBytes = 1000 155 } 156 157 isRequired := e.FileRequired(simplefileapi.New(tt.path, fakefs.FakeFileInfo{ 158 FileName: filepath.Base(tt.path), 159 FileMode: fs.ModePerm, 160 FileSize: fileSizeBytes, 161 })) 162 if isRequired != tt.wantRequired { 163 t.Fatalf("FileRequired(%s): got %v, want %v", tt.path, isRequired, tt.wantRequired) 164 } 165 166 gotResultMetric := collector.FileRequiredResult(tt.path) 167 if tt.wantResultMetric != "" && gotResultMetric != tt.wantResultMetric { 168 t.Errorf("FileRequired(%s) recorded result metric %v, want result metric %v", tt.path, gotResultMetric, tt.wantResultMetric) 169 } 170 }) 171 } 172 } 173 174 func TestExtract(t *testing.T) { 175 tests := []extracttest.TestTableEntry{ 176 { 177 Name: "valid setup.py file", 178 InputConfig: extracttest.ScanInputMockConfig{ 179 Path: "testdata/valid", 180 }, 181 WantPackages: []*extractor.Package{ 182 { 183 Name: "pysaml2", 184 Version: "6.5.1", 185 PURLType: purl.TypePyPi, 186 Locations: []string{"testdata/valid"}, 187 Metadata: &setup.Metadata{VersionComparator: "=="}, 188 }, 189 { 190 Name: "xmlschema", 191 Version: "1.7.1", 192 PURLType: purl.TypePyPi, 193 Locations: []string{"testdata/valid"}, 194 Metadata: &setup.Metadata{VersionComparator: "=="}, 195 }, 196 { 197 Name: "requests", 198 Version: "2.25.1", 199 PURLType: purl.TypePyPi, 200 Locations: []string{"testdata/valid"}, 201 Metadata: &setup.Metadata{VersionComparator: "=="}, 202 }, 203 { 204 Name: "lxml", 205 Version: "4.6.2", 206 PURLType: purl.TypePyPi, 207 Locations: []string{"testdata/valid"}, 208 Metadata: &setup.Metadata{VersionComparator: ">="}, 209 }, 210 { 211 Name: "Jinja2", 212 Version: "2.11.3", 213 PURLType: purl.TypePyPi, 214 Locations: []string{"testdata/valid"}, 215 Metadata: &setup.Metadata{VersionComparator: "=="}, 216 }, 217 { 218 Name: "pkg1", 219 Version: "0.1.1", 220 PURLType: purl.TypePyPi, 221 Locations: []string{"testdata/valid"}, 222 Metadata: &setup.Metadata{VersionComparator: "=="}, 223 }, 224 { 225 Name: "pkg2", 226 Version: "0.1.2", 227 PURLType: purl.TypePyPi, 228 Locations: []string{"testdata/valid"}, 229 Metadata: &setup.Metadata{VersionComparator: "=="}, 230 }, 231 { 232 Name: "foo", 233 Version: "2.20", 234 PURLType: purl.TypePyPi, 235 Locations: []string{"testdata/valid"}, 236 Metadata: &setup.Metadata{VersionComparator: ">="}, 237 }, 238 { 239 Name: "pydantic", 240 Version: "1.8.2", 241 PURLType: purl.TypePyPi, 242 Locations: []string{"testdata/valid"}, 243 Metadata: &setup.Metadata{VersionComparator: ">="}, 244 }, 245 { 246 Name: "certifi", 247 Version: "2017.4.17", 248 PURLType: purl.TypePyPi, 249 Locations: []string{"testdata/valid"}, 250 Metadata: &setup.Metadata{VersionComparator: ">="}, 251 }, 252 { 253 Name: "pkg3", 254 Version: "1.2.3", 255 PURLType: purl.TypePyPi, 256 Locations: []string{"testdata/valid"}, 257 Metadata: &setup.Metadata{VersionComparator: "<="}, 258 }, 259 }, 260 }, 261 { 262 Name: "valid setup.py file 2", 263 InputConfig: extracttest.ScanInputMockConfig{ 264 Path: "testdata/valid_2", 265 }, 266 WantPackages: []*extractor.Package{ 267 { 268 Name: "accelerate", 269 Version: "0.26.1", 270 PURLType: purl.TypePyPi, 271 Locations: []string{"testdata/valid_2"}, 272 Metadata: &setup.Metadata{VersionComparator: "=="}, 273 }, 274 { 275 Name: "transformers", 276 Version: "4.37.2", 277 PURLType: purl.TypePyPi, 278 Locations: []string{"testdata/valid_2"}, 279 Metadata: &setup.Metadata{VersionComparator: "=="}, 280 }, 281 { 282 Name: "datasets", 283 Version: "2.16.1", 284 PURLType: purl.TypePyPi, 285 Locations: []string{"testdata/valid_2"}, 286 Metadata: &setup.Metadata{VersionComparator: "=="}, 287 }, 288 { 289 Name: "mteb", 290 Version: "1.4.0", 291 PURLType: purl.TypePyPi, 292 Locations: []string{"testdata/valid_2"}, 293 Metadata: &setup.Metadata{VersionComparator: ">="}, 294 }, 295 }, 296 }, 297 { 298 Name: "valid setup.py file 3", 299 InputConfig: extracttest.ScanInputMockConfig{ 300 Path: "testdata/valid_3", 301 }, 302 WantPackages: []*extractor.Package{ 303 { 304 Name: "nanoplotter", 305 Version: "0.13.1", 306 PURLType: purl.TypePyPi, 307 Locations: []string{"testdata/valid_3"}, 308 Metadata: &setup.Metadata{VersionComparator: ">="}, 309 }, 310 { 311 Name: "nanoget", 312 Version: "0.11.0", 313 PURLType: purl.TypePyPi, 314 Locations: []string{"testdata/valid_3"}, 315 Metadata: &setup.Metadata{VersionComparator: ">="}, 316 }, 317 { 318 Name: "nanomath", 319 Version: "0.12.0", 320 PURLType: purl.TypePyPi, 321 Locations: []string{"testdata/valid_3"}, 322 Metadata: &setup.Metadata{VersionComparator: ">="}, 323 }, 324 }, 325 }, 326 { 327 Name: "template setup.py file", 328 InputConfig: extracttest.ScanInputMockConfig{ 329 Path: "testdata/template", 330 }, 331 WantPackages: []*extractor.Package{ 332 { 333 Name: "requests", 334 Version: "2.25.1", 335 PURLType: purl.TypePyPi, 336 Locations: []string{"testdata/template"}, 337 Metadata: &setup.Metadata{VersionComparator: "=="}, 338 }, 339 { 340 Name: "lxml", 341 Version: "4.6.2", 342 PURLType: purl.TypePyPi, 343 Locations: []string{"testdata/template"}, 344 Metadata: &setup.Metadata{VersionComparator: ">="}, 345 }, 346 { 347 Name: "Jinja2", 348 Version: "2.11.3", 349 PURLType: purl.TypePyPi, 350 Locations: []string{"testdata/template"}, 351 Metadata: &setup.Metadata{VersionComparator: "=="}, 352 }, 353 }, 354 }, 355 { 356 Name: "empty package setup.py file", 357 InputConfig: extracttest.ScanInputMockConfig{ 358 Path: "testdata/empty", 359 }, 360 WantPackages: []*extractor.Package{}, 361 }, 362 { 363 Name: "empty file", 364 InputConfig: extracttest.ScanInputMockConfig{ 365 Path: "testdata/empty_2", 366 }, 367 WantPackages: []*extractor.Package{}, 368 }, 369 } 370 371 for _, tt := range tests { 372 t.Run(tt.Name, func(t *testing.T) { 373 collector := testcollector.New() 374 375 var e filesystem.Extractor = setup.New(setup.Config{ 376 Stats: collector, 377 MaxFileSizeBytes: 30, 378 }) 379 380 scanInput := extracttest.GenerateScanInputMock(t, tt.InputConfig) 381 defer extracttest.CloseTestScanInput(t, scanInput) 382 383 got, err := e.Extract(t.Context(), &scanInput) 384 385 if diff := cmp.Diff(tt.WantErr, err, cmpopts.EquateErrors()); diff != "" { 386 t.Errorf("%s.Extract(%q) error diff (-want +got):\n%s", e.Name(), tt.InputConfig.Path, diff) 387 return 388 } 389 390 wantInv := inventory.Inventory{Packages: tt.WantPackages} 391 if diff := cmp.Diff(wantInv, got, cmpopts.SortSlices(extracttest.PackageCmpLess)); diff != "" { 392 t.Errorf("%s.Extract(%q) diff (-want +got):\n%s", e.Name(), tt.InputConfig.Path, diff) 393 } 394 }) 395 } 396 }