github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/language/ruby/gemspec/gemspec_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 gemspec_test 16 17 import ( 18 "io/fs" 19 "os" 20 "path/filepath" 21 "testing" 22 23 "github.com/google/go-cmp/cmp" 24 "github.com/google/go-cmp/cmp/cmpopts" 25 "github.com/google/osv-scalibr/extractor" 26 "github.com/google/osv-scalibr/extractor/filesystem" 27 "github.com/google/osv-scalibr/extractor/filesystem/internal/units" 28 "github.com/google/osv-scalibr/extractor/filesystem/language/ruby/gemspec" 29 "github.com/google/osv-scalibr/extractor/filesystem/simplefileapi" 30 scalibrfs "github.com/google/osv-scalibr/fs" 31 "github.com/google/osv-scalibr/inventory" 32 "github.com/google/osv-scalibr/purl" 33 "github.com/google/osv-scalibr/stats" 34 "github.com/google/osv-scalibr/testing/fakefs" 35 "github.com/google/osv-scalibr/testing/testcollector" 36 ) 37 38 func TestFileRequired(t *testing.T) { 39 tests := []struct { 40 name string 41 path string 42 fileSizeBytes int64 43 maxFileSizeBytes int64 44 wantRequired bool 45 wantResultMetric stats.FileRequiredResult 46 }{ 47 { 48 name: "yaml gemspec", 49 path: "testdata/yaml-0.2.1.gemspec", 50 wantRequired: true, 51 wantResultMetric: stats.FileRequiredResultOK, 52 }, 53 { 54 name: "ruby file", 55 path: "testdata/test.rb", 56 wantRequired: false, 57 }, 58 { 59 name: "yaml gemspec required if file size < max file size", 60 path: "testdata/yaml-0.2.1.gemspec", 61 fileSizeBytes: 100 * units.KiB, 62 maxFileSizeBytes: 1000 * units.KiB, 63 wantRequired: true, 64 wantResultMetric: stats.FileRequiredResultOK, 65 }, 66 { 67 name: "yaml gemspec required if file size == max file size", 68 path: "testdata/yaml-0.2.1.gemspec", 69 fileSizeBytes: 1000 * units.KiB, 70 maxFileSizeBytes: 1000 * units.KiB, 71 wantRequired: true, 72 wantResultMetric: stats.FileRequiredResultOK, 73 }, 74 { 75 name: "yaml gemspec not required if file size > max file size", 76 path: "testdata/yaml-0.2.1.gemspec", 77 fileSizeBytes: 1000 * units.KiB, 78 maxFileSizeBytes: 100 * units.KiB, 79 wantRequired: false, 80 wantResultMetric: stats.FileRequiredResultSizeLimitExceeded, 81 }, 82 { 83 name: "yaml gemspec required if max file size set to 0", 84 path: "testdata/yaml-0.2.1.gemspec", 85 fileSizeBytes: 1000 * units.KiB, 86 maxFileSizeBytes: 0, 87 wantRequired: true, 88 wantResultMetric: stats.FileRequiredResultOK, 89 }, 90 } 91 92 for _, test := range tests { 93 t.Run(test.name, func(t *testing.T) { 94 collector := testcollector.New() 95 var e filesystem.Extractor = gemspec.New( 96 gemspec.Config{ 97 Stats: collector, 98 MaxFileSizeBytes: test.maxFileSizeBytes, 99 }, 100 ) 101 102 // Set default size if not provided. 103 fileSizeBytes := test.fileSizeBytes 104 if fileSizeBytes == 0 { 105 fileSizeBytes = 100 * units.KiB 106 } 107 108 isRequired := e.FileRequired(simplefileapi.New(test.path, fakefs.FakeFileInfo{ 109 FileName: filepath.Base(test.path), 110 FileMode: fs.ModePerm, 111 FileSize: fileSizeBytes, 112 })) 113 if isRequired != test.wantRequired { 114 t.Fatalf("FileRequired(%s): got %v, want %v", test.path, isRequired, test.wantRequired) 115 } 116 117 gotResultMetric := collector.FileRequiredResult(test.path) 118 if gotResultMetric != test.wantResultMetric { 119 t.Errorf("FileRequired(%s) recorded result metric %v, want result metric %v", test.path, gotResultMetric, test.wantResultMetric) 120 } 121 }) 122 } 123 } 124 125 func TestExtract(t *testing.T) { 126 tests := []struct { 127 name string 128 path string 129 wantPackages []*extractor.Package 130 wantErr error 131 wantResultMetric stats.FileExtractedResult 132 }{ 133 { 134 name: "yaml_gemspec", 135 path: "testdata/yaml-0.2.1.gemspec", 136 wantPackages: []*extractor.Package{ 137 { 138 Name: "yaml", 139 Version: "0.2.1", 140 PURLType: purl.TypeGem, 141 Locations: []string{"testdata/yaml-0.2.1.gemspec"}, 142 }, 143 }, 144 wantResultMetric: stats.FileExtractedResultSuccess, 145 }, 146 { 147 name: "rss_gemspec", 148 path: "testdata/rss-0.2.9.gemspec", 149 wantPackages: []*extractor.Package{ 150 { 151 Name: "rss", 152 Version: "0.2.9", 153 PURLType: purl.TypeGem, 154 Locations: []string{"testdata/rss-0.2.9.gemspec"}, 155 }, 156 }, 157 wantResultMetric: stats.FileExtractedResultSuccess, 158 }, 159 { 160 name: "version constant gemspec", 161 path: "testdata/version_constant/version_constant.gemspec", 162 wantPackages: []*extractor.Package{ 163 { 164 Name: "example_app", 165 Version: "1.2.3", 166 PURLType: purl.TypeGem, 167 Locations: []string{"testdata/version_constant/version_constant.gemspec"}, 168 }, 169 }, 170 wantResultMetric: stats.FileExtractedResultSuccess, 171 }, 172 { 173 name: "version constant with freeze", 174 path: "testdata/version_constant_freeze/version_constant_freeze.gemspec", 175 wantPackages: []*extractor.Package{ 176 { 177 Name: "example_app_freeze", 178 Version: "2.3.4", 179 PURLType: purl.TypeGem, 180 Locations: []string{"testdata/version_constant_freeze/version_constant_freeze.gemspec"}, 181 }, 182 }, 183 wantResultMetric: stats.FileExtractedResultSuccess, 184 }, 185 { 186 name: "version inline constant", 187 path: "testdata/version_inline.gemspec", 188 wantPackages: []*extractor.Package{ 189 { 190 Name: "example_inline", 191 Version: "3.0.0", 192 PURLType: purl.TypeGem, 193 Locations: []string{"testdata/version_inline.gemspec"}, 194 }, 195 }, 196 wantResultMetric: stats.FileExtractedResultSuccess, 197 }, 198 { 199 name: "version constant via File.join", 200 path: "testdata/version_constant_join/version_constant_join.gemspec", 201 wantPackages: []*extractor.Package{ 202 { 203 Name: "example_app_join", 204 Version: "4.5.6", 205 PURLType: purl.TypeGem, 206 Locations: []string{"testdata/version_constant_join/version_constant_join.gemspec"}, 207 }, 208 }, 209 wantResultMetric: stats.FileExtractedResultSuccess, 210 }, 211 { 212 name: "version constant via File.join multiline", 213 path: "testdata/version_constant_join_multiline/version_constant_join_multiline.gemspec", 214 wantPackages: []*extractor.Package{ 215 { 216 Name: "example_app_join_multiline", 217 Version: "7.8.9", 218 PURLType: purl.TypeGem, 219 Locations: []string{"testdata/version_constant_join_multiline/version_constant_join_multiline.gemspec"}, 220 }, 221 }, 222 wantResultMetric: stats.FileExtractedResultSuccess, 223 }, 224 { 225 name: "version constant via File.expand_path", 226 path: "testdata/version_constant_expand/version_constant_expand.gemspec", 227 wantPackages: []*extractor.Package{ 228 { 229 Name: "example_app_expand", 230 Version: "5.6.7", 231 PURLType: purl.TypeGem, 232 Locations: []string{"testdata/version_constant_expand/version_constant_expand.gemspec"}, 233 }, 234 }, 235 wantResultMetric: stats.FileExtractedResultSuccess, 236 }, 237 { 238 name: "version constant via File.dirname", 239 path: "testdata/version_constant_dirname/version_constant_dirname.gemspec", 240 wantPackages: []*extractor.Package{ 241 { 242 Name: "example_app_dirname", 243 Version: "8.9.0", 244 PURLType: purl.TypeGem, 245 Locations: []string{"testdata/version_constant_dirname/version_constant_dirname.gemspec"}, 246 }, 247 }, 248 wantResultMetric: stats.FileExtractedResultSuccess, 249 }, 250 { 251 name: "version constant via require", 252 path: "testdata/version_constant_require/version_constant_require.gemspec", 253 wantPackages: []*extractor.Package{ 254 { 255 Name: "example_app_require", 256 Version: "0.9.9", 257 PURLType: purl.TypeGem, 258 Locations: []string{"testdata/version_constant_require/version_constant_require.gemspec"}, 259 }, 260 }, 261 wantResultMetric: stats.FileExtractedResultSuccess, 262 }, 263 { 264 name: "version constant via conditional require", 265 path: "testdata/version_constant_conditional/version_constant_conditional.gemspec", 266 wantPackages: []*extractor.Package{ 267 { 268 Name: "example_app_conditional", 269 Version: "9.0.1", 270 PURLType: purl.TypeGem, 271 Locations: []string{"testdata/version_constant_conditional/version_constant_conditional.gemspec"}, 272 }, 273 }, 274 wantResultMetric: stats.FileExtractedResultSuccess, 275 }, 276 { 277 name: "version constant via nested File.expand_path and File.join", 278 path: "testdata/version_constant_expand_join/version_constant_expand_join.gemspec", 279 wantPackages: []*extractor.Package{ 280 { 281 Name: "example_app_expand_join", 282 Version: "6.7.8", 283 PURLType: purl.TypeGem, 284 Locations: []string{"testdata/version_constant_expand_join/version_constant_expand_join.gemspec"}, 285 }, 286 }, 287 wantResultMetric: stats.FileExtractedResultSuccess, 288 }, 289 { 290 name: "invalid gemspec", 291 path: "testdata/invalid.gemspec", 292 wantErr: cmpopts.AnyError, 293 wantResultMetric: stats.FileExtractedResultErrorUnknown, 294 }, 295 { 296 name: "version constant missing definition", 297 path: "testdata/version_constant_missing/version_constant_missing.gemspec", 298 wantErr: cmpopts.AnyError, 299 wantResultMetric: stats.FileExtractedResultErrorUnknown, 300 }, 301 { 302 name: "empty gemspec", 303 path: "testdata/empty.gemspec", 304 wantPackages: nil, 305 wantResultMetric: stats.FileExtractedResultSuccess, 306 }, 307 { 308 name: "bad definition gemspec", 309 path: "testdata/badspec.gemspec", 310 wantPackages: nil, 311 wantResultMetric: stats.FileExtractedResultSuccess, 312 }, 313 { 314 name: "version constant class", 315 path: "testdata/version_constant_class/version_constant_class.gemspec", 316 wantPackages: []*extractor.Package{ 317 { 318 Name: "example_app", 319 Version: "3.0.0", 320 PURLType: purl.TypeGem, 321 Locations: []string{"testdata/version_constant_class/version_constant_class.gemspec"}, 322 }, 323 }, 324 wantResultMetric: stats.FileExtractedResultSuccess, 325 }, 326 { 327 name: "version constant different casing", 328 path: "testdata/version_constant_different_casing/version_constant_different_casing.gemspec", 329 wantPackages: []*extractor.Package{ 330 { 331 Name: "example_app", 332 Version: "4.0.0", 333 PURLType: purl.TypeGem, 334 Locations: []string{"testdata/version_constant_different_casing/version_constant_different_casing.gemspec"}, 335 }, 336 }, 337 wantResultMetric: stats.FileExtractedResultSuccess, 338 }, 339 { 340 name: "version method not supported", 341 path: "testdata/version_method/version_method.gemspec", 342 wantErr: cmpopts.AnyError, 343 wantResultMetric: stats.FileExtractedResultErrorUnknown, 344 }, 345 { 346 name: "load path not supported", 347 path: "testdata/version_load_path/version_load_path.gemspec", 348 wantErr: cmpopts.AnyError, 349 wantResultMetric: stats.FileExtractedResultErrorUnknown, 350 }, 351 } 352 353 for _, test := range tests { 354 t.Run(test.name, func(t *testing.T) { 355 collector := testcollector.New() 356 var e filesystem.Extractor = gemspec.New(gemspec.Config{Stats: collector}) 357 358 r, err := os.Open(test.path) 359 defer func() { 360 if err = r.Close(); err != nil { 361 t.Errorf("Close(): %v", err) 362 } 363 }() 364 if err != nil { 365 t.Fatal(err) 366 } 367 368 info, err := os.Stat(test.path) 369 if err != nil { 370 t.Fatalf("Failed to stat test file: %v", err) 371 } 372 373 input := &filesystem.ScanInput{FS: scalibrfs.DirFS("."), Path: test.path, Reader: r, Info: info} 374 got, err := e.Extract(t.Context(), input) 375 if !cmp.Equal(err, test.wantErr, cmpopts.EquateErrors()) { 376 t.Fatalf("Extract(%+v) error: got %v, want %v\n", test.name, err, test.wantErr) 377 } 378 379 var want inventory.Inventory 380 if test.wantPackages != nil { 381 want = inventory.Inventory{Packages: test.wantPackages} 382 } 383 384 if diff := cmp.Diff(want, got); diff != "" { 385 t.Errorf("Extract(%+v) diff (-want +got):\n%s", test.name, diff) 386 } 387 388 gotResultMetric := collector.FileExtractedResult(test.path) 389 if gotResultMetric != test.wantResultMetric { 390 t.Errorf("Extract(%s) recorded result metric %v, want result metric %v", test.path, gotResultMetric, test.wantResultMetric) 391 } 392 393 gotFileSizeMetric := collector.FileExtractedFileSize(test.path) 394 if gotFileSizeMetric != info.Size() { 395 t.Errorf("Extract(%s) recorded file size %v, want file size %v", test.path, gotFileSizeMetric, info.Size()) 396 } 397 }) 398 } 399 }