github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/language/python/condameta/condameta_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 condameta_test 16 17 import ( 18 "io/fs" 19 "path/filepath" 20 "testing" 21 22 "github.com/google/go-cmp/cmp" 23 "github.com/google/go-cmp/cmp/cmpopts" 24 "github.com/google/osv-scalibr/extractor" 25 "github.com/google/osv-scalibr/extractor/filesystem" 26 "github.com/google/osv-scalibr/extractor/filesystem/internal/units" 27 "github.com/google/osv-scalibr/extractor/filesystem/language/python/condameta" 28 "github.com/google/osv-scalibr/extractor/filesystem/simplefileapi" 29 "github.com/google/osv-scalibr/inventory" 30 "github.com/google/osv-scalibr/purl" 31 "github.com/google/osv-scalibr/stats" 32 "github.com/google/osv-scalibr/testing/extracttest" 33 "github.com/google/osv-scalibr/testing/fakefs" 34 "github.com/google/osv-scalibr/testing/testcollector" 35 ) 36 37 func TestNew(t *testing.T) { 38 tests := []struct { 39 name string 40 cfg condameta.Config 41 wantCfg condameta.Config 42 }{ 43 { 44 name: "default", 45 cfg: condameta.DefaultConfig(), 46 wantCfg: condameta.Config{ 47 MaxFileSizeBytes: 10 * units.MiB, 48 }, 49 }, 50 { 51 name: "custom", 52 cfg: condameta.Config{ 53 MaxFileSizeBytes: 10, 54 }, 55 wantCfg: condameta.Config{ 56 MaxFileSizeBytes: 10, 57 }, 58 }, 59 } 60 61 for _, tt := range tests { 62 t.Run(tt.name, func(t *testing.T) { 63 got := condameta.New(tt.cfg) 64 if diff := cmp.Diff(tt.wantCfg, got.Config()); diff != "" { 65 t.Errorf("New(%+v).Config(): (-want +got):\n%s", tt.cfg, diff) 66 } 67 }) 68 } 69 } 70 71 func TestFileRequired(t *testing.T) { 72 tests := []struct { 73 name string 74 path string 75 fileSizeBytes int64 76 maxFileSizeBytes int64 77 wantRequired bool 78 wantResultMetric stats.FileRequiredResult 79 }{ 80 { 81 name: "valid path conda json file", 82 path: "envs/data_analysis/conda-meta/numpy-1.21.2-py39h123abcde.json", 83 wantRequired: true, 84 wantResultMetric: stats.FileRequiredResultOK, 85 }, 86 { 87 name: "non-toplevel dirs", 88 path: "path/to/envs/data_analysis/conda-meta/numpy-1.21.2-py39h123abcde.json", 89 wantRequired: true, 90 }, 91 { 92 name: "invalid envs dir", 93 path: "/path/to/fooenvs/conda-meta/numpy-1.21.2-py39h123abcde.json", 94 wantRequired: false, 95 }, 96 { 97 name: "invalid path conda json file", 98 path: "envs/data_analysis/test/numpy-1.21.2-py39h123abcde.json", 99 wantRequired: false, 100 }, 101 { 102 name: "invalid extension", 103 path: "envs/data_analysis/conda-meta/numpy-1.21.2-py39h123abcde.txt", 104 wantRequired: false, 105 }, 106 { 107 name: "conda json file required if file size < max file size", 108 path: "envs/data_analysis/conda-meta/numpy-1.21.2-py39h123abcde.json", 109 fileSizeBytes: 100 * units.KiB, 110 maxFileSizeBytes: 1000 * units.KiB, 111 wantRequired: true, 112 wantResultMetric: stats.FileRequiredResultOK, 113 }, 114 { 115 name: "conda json file required if file size == max file size", 116 path: "envs/data_analysis/conda-meta/numpy-1.21.2-py39h123abcde.json", 117 fileSizeBytes: 1000 * units.KiB, 118 maxFileSizeBytes: 1000 * units.KiB, 119 wantRequired: true, 120 wantResultMetric: stats.FileRequiredResultOK, 121 }, 122 { 123 name: "conda json file not required if file size > max file size", 124 path: "envs/data_analysis/conda-meta/numpy-1.21.2-py39h123abcde.json", 125 fileSizeBytes: 1000 * units.KiB, 126 maxFileSizeBytes: 100 * units.KiB, 127 wantRequired: false, 128 wantResultMetric: stats.FileRequiredResultSizeLimitExceeded, 129 }, 130 { 131 name: "conda json file required if max file size set to 0", 132 path: "envs/data_analysis/conda-meta/numpy-1.21.2-py39h123abcde.json", 133 fileSizeBytes: 100 * units.KiB, 134 maxFileSizeBytes: 0, 135 wantRequired: true, 136 wantResultMetric: stats.FileRequiredResultOK, 137 }, 138 } 139 140 for _, tt := range tests { 141 t.Run(tt.name, func(t *testing.T) { 142 collector := testcollector.New() 143 var e filesystem.Extractor = condameta.New(condameta.Config{ 144 Stats: collector, 145 MaxFileSizeBytes: tt.maxFileSizeBytes, 146 }) 147 148 fileSizeBytes := tt.fileSizeBytes 149 if fileSizeBytes == 0 { 150 fileSizeBytes = 1000 151 } 152 153 isRequired := e.FileRequired(simplefileapi.New(tt.path, fakefs.FakeFileInfo{ 154 FileName: filepath.Base(tt.path), 155 FileMode: fs.ModePerm, 156 FileSize: fileSizeBytes, 157 })) 158 if isRequired != tt.wantRequired { 159 t.Fatalf("FileRequired(%s): got %v, want %v", tt.path, isRequired, tt.wantRequired) 160 } 161 162 gotResultMetric := collector.FileRequiredResult(tt.path) 163 if tt.wantResultMetric != "" && gotResultMetric != tt.wantResultMetric { 164 t.Errorf("FileRequired(%s) recorded result metric %v, want result metric %v", tt.path, gotResultMetric, tt.wantResultMetric) 165 } 166 }) 167 } 168 } 169 170 func TestExtract(t *testing.T) { 171 tests := []extracttest.TestTableEntry{ 172 { 173 Name: "valid conda file", 174 InputConfig: extracttest.ScanInputMockConfig{ 175 Path: "testdata/valid", 176 }, 177 WantPackages: []*extractor.Package{ 178 { 179 Name: "jupyterlab", 180 Version: "3.1.12", 181 PURLType: purl.TypePyPi, 182 Locations: []string{"testdata/valid"}, 183 }, 184 }, 185 }, 186 { 187 Name: "conda file not valid", 188 InputConfig: extracttest.ScanInputMockConfig{ 189 Path: "testdata/invalid", 190 }, 191 WantErr: cmpopts.AnyError, 192 }, 193 { 194 Name: "conda file empty", 195 InputConfig: extracttest.ScanInputMockConfig{ 196 Path: "testdata/empty", 197 }, 198 WantErr: cmpopts.AnyError, 199 }, 200 { 201 Name: "no package name", 202 InputConfig: extracttest.ScanInputMockConfig{ 203 Path: "testdata/noname", 204 }, 205 WantErr: cmpopts.AnyError, 206 }, 207 { 208 Name: "no package version", 209 InputConfig: extracttest.ScanInputMockConfig{ 210 Path: "testdata/noversion", 211 }, 212 WantErr: cmpopts.AnyError, 213 }, 214 } 215 216 for _, tt := range tests { 217 t.Run(tt.Name, func(t *testing.T) { 218 collector := testcollector.New() 219 var e filesystem.Extractor = condameta.New(condameta.Config{ 220 Stats: collector, 221 MaxFileSizeBytes: 100, 222 }) 223 224 scanInput := extracttest.GenerateScanInputMock(t, tt.InputConfig) 225 defer extracttest.CloseTestScanInput(t, scanInput) 226 227 got, err := e.Extract(t.Context(), &scanInput) 228 229 if diff := cmp.Diff(tt.WantErr, err, cmpopts.EquateErrors()); diff != "" { 230 t.Errorf("%s.Extract(%q) error diff (-want +got):\n%s", e.Name(), tt.InputConfig.Path, diff) 231 return 232 } 233 234 wantInv := inventory.Inventory{Packages: tt.WantPackages} 235 if diff := cmp.Diff(wantInv, got, cmpopts.SortSlices(extracttest.PackageCmpLess)); diff != "" { 236 t.Errorf("%s.Extract(%q) diff (-want +got):\n%s", e.Name(), tt.InputConfig.Path, diff) 237 } 238 }) 239 } 240 }