github.com/anchore/syft@v1.38.2/syft/pkg/cataloger/python/parse_setup_test.go (about)

     1  package python
     2  
     3  import (
     4  	"context"
     5  	"testing"
     6  
     7  	"github.com/stretchr/testify/assert"
     8  
     9  	"github.com/anchore/syft/syft/artifact"
    10  	"github.com/anchore/syft/syft/file"
    11  	"github.com/anchore/syft/syft/pkg"
    12  	"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
    13  )
    14  
    15  func TestParseSetup(t *testing.T) {
    16  	tests := []struct {
    17  		fixture  string
    18  		expected []pkg.Package
    19  	}{
    20  		{
    21  			fixture: "test-fixtures/setup/setup.py",
    22  			expected: []pkg.Package{
    23  				{
    24  					Name:     "pathlib3",
    25  					Version:  "2.2.0",
    26  					PURL:     "pkg:pypi/pathlib3@2.2.0",
    27  					Language: pkg.Python,
    28  					Type:     pkg.PythonPkg,
    29  				},
    30  				{
    31  					Name:     "mypy",
    32  					Version:  "v0.770",
    33  					PURL:     "pkg:pypi/mypy@v0.770",
    34  					Language: pkg.Python,
    35  					Type:     pkg.PythonPkg,
    36  				},
    37  				{
    38  					Name:     "mypy1",
    39  					Version:  "v0.770",
    40  					PURL:     "pkg:pypi/mypy1@v0.770",
    41  					Language: pkg.Python,
    42  					Type:     pkg.PythonPkg,
    43  				},
    44  				{
    45  					Name:     "mypy2",
    46  					Version:  "v0.770",
    47  					PURL:     "pkg:pypi/mypy2@v0.770",
    48  					Language: pkg.Python,
    49  					Type:     pkg.PythonPkg,
    50  				},
    51  				{
    52  					Name:     "mypy3",
    53  					Version:  "v0.770",
    54  					PURL:     "pkg:pypi/mypy3@v0.770",
    55  					Language: pkg.Python,
    56  					Type:     pkg.PythonPkg,
    57  				},
    58  			},
    59  		},
    60  		{
    61  			// regression... ensure we clean packages names and don't find "%s" as the name
    62  			fixture:  "test-fixtures/setup/dynamic-setup.py",
    63  			expected: nil,
    64  		},
    65  		{
    66  			fixture: "test-fixtures/setup/multiline-split-setup.py",
    67  			expected: []pkg.Package{
    68  				{
    69  					Name:     "black",
    70  					Version:  "23.12.1",
    71  					PURL:     "pkg:pypi/black@23.12.1",
    72  					Language: pkg.Python,
    73  					Type:     pkg.PythonPkg,
    74  				},
    75  				{
    76  					Name:     "cairosvg",
    77  					Version:  "2.7.1",
    78  					PURL:     "pkg:pypi/cairosvg@2.7.1",
    79  					Language: pkg.Python,
    80  					Type:     pkg.PythonPkg,
    81  				},
    82  				{
    83  					Name:     "celery",
    84  					Version:  "5.3.4",
    85  					PURL:     "pkg:pypi/celery@5.3.4",
    86  					Language: pkg.Python,
    87  					Type:     pkg.PythonPkg,
    88  				},
    89  				{
    90  					Name:     "django",
    91  					Version:  "4.2.23",
    92  					PURL:     "pkg:pypi/django@4.2.23",
    93  					Language: pkg.Python,
    94  					Type:     pkg.PythonPkg,
    95  				},
    96  				{
    97  					Name:     "mypy",
    98  					Version:  "1.7.1",
    99  					PURL:     "pkg:pypi/mypy@1.7.1",
   100  					Language: pkg.Python,
   101  					Type:     pkg.PythonPkg,
   102  				},
   103  				{
   104  					Name:     "pillow",
   105  					Version:  "11.0.0",
   106  					PURL:     "pkg:pypi/pillow@11.0.0",
   107  					Language: pkg.Python,
   108  					Type:     pkg.PythonPkg,
   109  				},
   110  				{
   111  					Name:     "pytest",
   112  					Version:  "7.4.3",
   113  					PURL:     "pkg:pypi/pytest@7.4.3",
   114  					Language: pkg.Python,
   115  					Type:     pkg.PythonPkg,
   116  				},
   117  				{
   118  					Name:     "requests",
   119  					Version:  "2.31.0",
   120  					PURL:     "pkg:pypi/requests@2.31.0",
   121  					Language: pkg.Python,
   122  					Type:     pkg.PythonPkg,
   123  				},
   124  			},
   125  		},
   126  		{
   127  			// Test mixed quoted and unquoted dependencies - ensure no duplicates
   128  			fixture: "test-fixtures/setup/mixed-format-setup.py",
   129  			expected: []pkg.Package{
   130  				{
   131  					Name:     "requests",
   132  					Version:  "2.31.0",
   133  					PURL:     "pkg:pypi/requests@2.31.0",
   134  					Language: pkg.Python,
   135  					Type:     pkg.PythonPkg,
   136  				},
   137  				{
   138  					Name:     "django",
   139  					Version:  "4.2.23",
   140  					PURL:     "pkg:pypi/django@4.2.23",
   141  					Language: pkg.Python,
   142  					Type:     pkg.PythonPkg,
   143  				},
   144  				{
   145  					Name:     "flask",
   146  					Version:  "3.0.0",
   147  					PURL:     "pkg:pypi/flask@3.0.0",
   148  					Language: pkg.Python,
   149  					Type:     pkg.PythonPkg,
   150  				},
   151  			},
   152  		},
   153  	}
   154  
   155  	for _, tt := range tests {
   156  		t.Run(tt.fixture, func(t *testing.T) {
   157  			locations := file.NewLocationSet(file.NewLocation(tt.fixture))
   158  			for i := range tt.expected {
   159  				tt.expected[i].Locations = locations
   160  			}
   161  			var expectedRelationships []artifact.Relationship
   162  
   163  			setupFileParser := newSetupFileParser(DefaultCatalogerConfig())
   164  			pkgtest.TestFileParser(t, tt.fixture, setupFileParser.parseSetupFile, tt.expected, expectedRelationships)
   165  		})
   166  	}
   167  
   168  }
   169  
   170  func TestParseSetupFileWithLicenseEnrichment(t *testing.T) {
   171  	ctx := context.TODO()
   172  	fixture := "test-fixtures/pypi-remote/setup.py"
   173  	locations := file.NewLocationSet(file.NewLocation(fixture))
   174  	mux, url, teardown := setupPypiRegistry()
   175  	defer teardown()
   176  	tests := []struct {
   177  		name             string
   178  		fixture          string
   179  		config           CatalogerConfig
   180  		requestHandlers  []handlerPath
   181  		expectedPackages []pkg.Package
   182  	}{
   183  		{
   184  			name:   "search remote licenses returns the expected licenses when search is set to true",
   185  			config: CatalogerConfig{SearchRemoteLicenses: true},
   186  			requestHandlers: []handlerPath{
   187  				{
   188  					path:    "/certifi/2025.10.5/json",
   189  					handler: generateMockPypiRegistryHandler("test-fixtures/pypi-remote/registry_response.json"),
   190  				},
   191  			},
   192  			expectedPackages: []pkg.Package{
   193  				{
   194  					Name:      "certifi",
   195  					Version:   "2025.10.5",
   196  					Locations: locations,
   197  					PURL:      "pkg:pypi/certifi@2025.10.5",
   198  					Licenses:  pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, "MPL-2.0")),
   199  					Language:  pkg.Python,
   200  					Type:      pkg.PythonPkg,
   201  				},
   202  			},
   203  		},
   204  	}
   205  	for _, tc := range tests {
   206  		t.Run(tc.name, func(t *testing.T) {
   207  			// set up the mock server
   208  			for _, handler := range tc.requestHandlers {
   209  				mux.HandleFunc(handler.path, handler.handler)
   210  			}
   211  			tc.config.PypiBaseURL = url
   212  			setupFileParser := newSetupFileParser(tc.config)
   213  			pkgtest.TestFileParser(t, fixture, setupFileParser.parseSetupFile, tc.expectedPackages, nil)
   214  		})
   215  	}
   216  }
   217  func Test_hasTemplateDirective(t *testing.T) {
   218  
   219  	tests := []struct {
   220  		input string
   221  		want  bool
   222  	}{
   223  		{
   224  			input: "foo",
   225  			want:  false,
   226  		},
   227  		{
   228  			input: "foo %s",
   229  			want:  true,
   230  		},
   231  		{
   232  			input: "%s",
   233  			want:  true,
   234  		},
   235  		{
   236  			input: "{f_string}",
   237  			want:  true,
   238  		},
   239  		{
   240  			input: "{}", // .format() directive
   241  			want:  true,
   242  		},
   243  	}
   244  	for _, tt := range tests {
   245  		t.Run(tt.input, func(t *testing.T) {
   246  			assert.Equal(t, tt.want, hasTemplateDirective(tt.input))
   247  		})
   248  	}
   249  }