github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/language/haskell/cabal/cabal_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 cabal_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/haskell/cabal" 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 cabal.Config 41 wantCfg cabal.Config 42 }{ 43 { 44 name: "default", 45 cfg: cabal.DefaultConfig(), 46 wantCfg: cabal.Config{ 47 MaxFileSizeBytes: 30 * units.MiB, 48 }, 49 }, 50 { 51 name: "custom", 52 cfg: cabal.Config{ 53 MaxFileSizeBytes: 10, 54 }, 55 wantCfg: cabal.Config{ 56 MaxFileSizeBytes: 10, 57 }, 58 }, 59 } 60 61 for _, tt := range tests { 62 t.Run(tt.name, func(t *testing.T) { 63 got := cabal.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: "cabal.project.freeze file", 82 path: "software-develop/cabal.project.freeze", 83 wantRequired: true, 84 wantResultMetric: stats.FileRequiredResultOK, 85 }, 86 { 87 name: "cabal.project.freeze file required if file size < max file size", 88 path: "software-develop/cabal.project.freeze", 89 fileSizeBytes: 100 * units.KiB, 90 maxFileSizeBytes: 1000 * units.KiB, 91 wantRequired: true, 92 wantResultMetric: stats.FileRequiredResultOK, 93 }, 94 { 95 name: "cabal.project.freeze file required if file size == max file size", 96 path: "software-develop/cabal.project.freeze", 97 fileSizeBytes: 1000 * units.KiB, 98 maxFileSizeBytes: 1000 * units.KiB, 99 wantRequired: true, 100 wantResultMetric: stats.FileRequiredResultOK, 101 }, 102 { 103 name: "cabal.project.freeze file not required if file size > max file size", 104 path: "software-develop/cabal.project.freeze", 105 fileSizeBytes: 1000 * units.KiB, 106 maxFileSizeBytes: 100 * units.KiB, 107 wantRequired: false, 108 wantResultMetric: stats.FileRequiredResultSizeLimitExceeded, 109 }, 110 { 111 name: "cabal.project.freeze file required if max file size set to 0", 112 path: "software-develop/cabal.project.freeze", 113 fileSizeBytes: 100 * units.KiB, 114 maxFileSizeBytes: 0, 115 wantRequired: true, 116 wantResultMetric: stats.FileRequiredResultOK, 117 }, 118 { 119 name: "not required", 120 path: "software-develop/cabal.project.freeze/foo", 121 wantRequired: false, 122 }, 123 { 124 name: "not required", 125 path: "software-develop/foocabal.project.freeze", 126 wantRequired: false, 127 }, 128 } 129 130 for _, tt := range tests { 131 t.Run(tt.name, func(t *testing.T) { 132 collector := testcollector.New() 133 var e filesystem.Extractor = cabal.New(cabal.Config{ 134 Stats: collector, 135 MaxFileSizeBytes: tt.maxFileSizeBytes, 136 }) 137 138 fileSizeBytes := tt.fileSizeBytes 139 if fileSizeBytes == 0 { 140 fileSizeBytes = 1000 141 } 142 143 isRequired := e.FileRequired(simplefileapi.New(tt.path, fakefs.FakeFileInfo{ 144 FileName: filepath.Base(tt.path), 145 FileMode: fs.ModePerm, 146 FileSize: fileSizeBytes, 147 })) 148 if isRequired != tt.wantRequired { 149 t.Fatalf("FileRequired(%s): got %v, want %v", tt.path, isRequired, tt.wantRequired) 150 } 151 152 gotResultMetric := collector.FileRequiredResult(tt.path) 153 if tt.wantResultMetric != "" && gotResultMetric != tt.wantResultMetric { 154 t.Errorf("FileRequired(%s) recorded result metric %v, want result metric %v", tt.path, gotResultMetric, tt.wantResultMetric) 155 } 156 }) 157 } 158 } 159 160 func TestExtract(t *testing.T) { 161 tests := []extracttest.TestTableEntry{ 162 { 163 Name: "valid stack.yaml.lock file", 164 InputConfig: extracttest.ScanInputMockConfig{ 165 Path: "testdata/valid", 166 }, 167 WantPackages: []*extractor.Package{ 168 { 169 Name: "AC-Angle", 170 Version: "1.0", 171 PURLType: purl.TypeHaskell, 172 Locations: []string{"testdata/valid"}, 173 }, 174 { 175 Name: "ALUT", 176 Version: "2.4.0.3", 177 PURLType: purl.TypeHaskell, 178 Locations: []string{"testdata/valid"}, 179 }, 180 { 181 Name: "ANum", 182 Version: "0.2.0.2", 183 PURLType: purl.TypeHaskell, 184 Locations: []string{"testdata/valid"}, 185 }, 186 { 187 Name: "Agda", 188 Version: "2.6.4.3", 189 PURLType: purl.TypeHaskell, 190 Locations: []string{"testdata/valid"}, 191 }, 192 { 193 Name: "Allure", 194 Version: "0.11.0.0", 195 PURLType: purl.TypeHaskell, 196 Locations: []string{"testdata/valid"}, 197 }, 198 }, 199 }, 200 { 201 Name: "valid stack.yaml.lock file with package problems", 202 InputConfig: extracttest.ScanInputMockConfig{ 203 Path: "testdata/valid_2", 204 }, 205 WantPackages: []*extractor.Package{ 206 { 207 Name: "AC-Angle", 208 Version: "1.0", 209 PURLType: purl.TypeHaskell, 210 Locations: []string{"testdata/valid_2"}, 211 }, 212 { 213 Name: "ANum", 214 Version: "0.2.0.2", 215 PURLType: purl.TypeHaskell, 216 Locations: []string{"testdata/valid_2"}, 217 }, 218 { 219 Name: "Agda", 220 Version: "2.6.4.3", 221 PURLType: purl.TypeHaskell, 222 Locations: []string{"testdata/valid_2"}, 223 }, 224 { 225 Name: "Allure", 226 Version: "0.11.0.0", 227 PURLType: purl.TypeHaskell, 228 Locations: []string{"testdata/valid_2"}, 229 }, 230 }, 231 }, 232 { 233 Name: "invalid", 234 InputConfig: extracttest.ScanInputMockConfig{ 235 Path: "testdata/invalid", 236 }, 237 WantPackages: []*extractor.Package{}, 238 }, 239 } 240 241 for _, tt := range tests { 242 t.Run(tt.Name, func(t *testing.T) { 243 collector := testcollector.New() 244 245 var e filesystem.Extractor = cabal.New(cabal.Config{ 246 Stats: collector, 247 MaxFileSizeBytes: 100, 248 }) 249 250 scanInput := extracttest.GenerateScanInputMock(t, tt.InputConfig) 251 defer extracttest.CloseTestScanInput(t, scanInput) 252 253 got, err := e.Extract(t.Context(), &scanInput) 254 255 if diff := cmp.Diff(tt.WantErr, err, cmpopts.EquateErrors()); diff != "" { 256 t.Errorf("%s.Extract(%q) error diff (-want +got):\n%s", e.Name(), tt.InputConfig.Path, diff) 257 return 258 } 259 260 wantInv := inventory.Inventory{Packages: tt.WantPackages} 261 if diff := cmp.Diff(wantInv, got, cmpopts.SortSlices(extracttest.PackageCmpLess)); diff != "" { 262 t.Errorf("%s.Extract(%q) diff (-want +got):\n%s", e.Name(), tt.InputConfig.Path, diff) 263 } 264 }) 265 } 266 }