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 }