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  }