github.com/noqcks/syft@v0.0.0-20230920222752-a9e2c4e288e5/syft/pkg/cataloger/python/parse_requirements_test.go (about)

     1  package python
     2  
     3  import (
     4  	"testing"
     5  
     6  	"github.com/stretchr/testify/assert"
     7  
     8  	"github.com/anchore/syft/syft/artifact"
     9  	"github.com/anchore/syft/syft/file"
    10  	"github.com/anchore/syft/syft/pkg"
    11  	"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
    12  )
    13  
    14  func TestParseRequirementsTxt(t *testing.T) {
    15  	fixture := "test-fixtures/requires/requirements.txt"
    16  	locations := file.NewLocationSet(file.NewLocation(fixture))
    17  
    18  	pinnedPkgs := []pkg.Package{
    19  		{
    20  			Name:         "flask",
    21  			Version:      "4.0.0",
    22  			PURL:         "pkg:pypi/flask@4.0.0",
    23  			Locations:    locations,
    24  			Language:     pkg.Python,
    25  			Type:         pkg.PythonPkg,
    26  			MetadataType: pkg.PythonRequirementsMetadataType,
    27  			Metadata: pkg.PythonRequirementsMetadata{
    28  				Name:              "flask",
    29  				VersionConstraint: "== 4.0.0",
    30  			},
    31  		},
    32  		{
    33  			Name:         "foo",
    34  			Version:      "1.0.0",
    35  			PURL:         "pkg:pypi/foo@1.0.0",
    36  			Locations:    locations,
    37  			Language:     pkg.Python,
    38  			Type:         pkg.PythonPkg,
    39  			MetadataType: pkg.PythonRequirementsMetadataType,
    40  			Metadata: pkg.PythonRequirementsMetadata{
    41  				Name:              "foo",
    42  				VersionConstraint: "== 1.0.0",
    43  			},
    44  		},
    45  		{
    46  			Name:         "SomeProject",
    47  			Version:      "5.4",
    48  			PURL:         "pkg:pypi/SomeProject@5.4",
    49  			Locations:    locations,
    50  			Language:     pkg.Python,
    51  			Type:         pkg.PythonPkg,
    52  			MetadataType: pkg.PythonRequirementsMetadataType,
    53  			Metadata: pkg.PythonRequirementsMetadata{
    54  				Name:              "SomeProject",
    55  				VersionConstraint: "==5.4",
    56  				Markers:           "python_version < '3.8'",
    57  			},
    58  		},
    59  		{
    60  			Name:         "argh",
    61  			Version:      "0.26.2",
    62  			PURL:         "pkg:pypi/argh@0.26.2",
    63  			Locations:    locations,
    64  			Language:     pkg.Python,
    65  			Type:         pkg.PythonPkg,
    66  			MetadataType: pkg.PythonRequirementsMetadataType,
    67  			Metadata: pkg.PythonRequirementsMetadata{
    68  				Name:              "argh",
    69  				VersionConstraint: "==0.26.2",
    70  			},
    71  		},
    72  		{
    73  			Name:         "argh",
    74  			Version:      "0.26.3",
    75  			PURL:         "pkg:pypi/argh@0.26.3",
    76  			Locations:    locations,
    77  			Language:     pkg.Python,
    78  			Type:         pkg.PythonPkg,
    79  			MetadataType: pkg.PythonRequirementsMetadataType,
    80  			Metadata: pkg.PythonRequirementsMetadata{
    81  				Name:              "argh",
    82  				VersionConstraint: "==0.26.3",
    83  			},
    84  		},
    85  		{
    86  			Name:         "celery",
    87  			Version:      "4.4.7",
    88  			PURL:         "pkg:pypi/celery@4.4.7",
    89  			Locations:    locations,
    90  			Language:     pkg.Python,
    91  			Type:         pkg.PythonPkg,
    92  			MetadataType: pkg.PythonRequirementsMetadataType,
    93  			Metadata: pkg.PythonRequirementsMetadata{
    94  				Name:              "celery",
    95  				Extras:            []string{"redis", "pytest"},
    96  				VersionConstraint: "== 4.4.7",
    97  			},
    98  		},
    99  		{
   100  			Name:         "GithubSampleProject",
   101  			Version:      "3.7.1",
   102  			PURL:         "pkg:pypi/GithubSampleProject@3.7.1",
   103  			Locations:    locations,
   104  			Language:     pkg.Python,
   105  			Type:         pkg.PythonPkg,
   106  			MetadataType: pkg.PythonRequirementsMetadataType,
   107  			Metadata: pkg.PythonRequirementsMetadata{
   108  				Name:              "GithubSampleProject",
   109  				VersionConstraint: "== 3.7.1",
   110  				URL:               "git+https://github.com/owner/repo@releases/tag/v3.7.1",
   111  			},
   112  		},
   113  	}
   114  
   115  	var testCases = []struct {
   116  		name                  string
   117  		fixture               string
   118  		cfg                   CatalogerConfig
   119  		expectedPkgs          []pkg.Package
   120  		expectedRelationships []artifact.Relationship
   121  	}{
   122  		{
   123  			name:    "pinned dependencies only",
   124  			fixture: fixture,
   125  			cfg: CatalogerConfig{
   126  				GuessUnpinnedRequirements: false,
   127  			},
   128  			expectedPkgs: pinnedPkgs,
   129  		},
   130  		{
   131  			name:    "guess unpinned requirements (lowest version)",
   132  			fixture: fixture,
   133  			cfg: CatalogerConfig{
   134  				GuessUnpinnedRequirements: true,
   135  			},
   136  			expectedPkgs: append([]pkg.Package{
   137  				{
   138  					Name:         "Mopidy-Dirble",
   139  					Version:      "1.1",
   140  					PURL:         "pkg:pypi/Mopidy-Dirble@1.1",
   141  					Locations:    locations,
   142  					Language:     pkg.Python,
   143  					Type:         pkg.PythonPkg,
   144  					MetadataType: pkg.PythonRequirementsMetadataType,
   145  					Metadata: pkg.PythonRequirementsMetadata{
   146  						Name:              "Mopidy-Dirble",
   147  						VersionConstraint: "~= 1.1",
   148  					},
   149  				},
   150  				{
   151  					Name:         "sqlalchemy",
   152  					Version:      "2.0.0",
   153  					PURL:         "pkg:pypi/sqlalchemy@2.0.0",
   154  					Locations:    locations,
   155  					Language:     pkg.Python,
   156  					Type:         pkg.PythonPkg,
   157  					MetadataType: pkg.PythonRequirementsMetadataType,
   158  					Metadata: pkg.PythonRequirementsMetadata{
   159  						Name:              "sqlalchemy",
   160  						VersionConstraint: ">= 1.0.0, <= 2.0.0, != 3.0.0, <= 3.0.0",
   161  					},
   162  				},
   163  				{
   164  					Name:         "bar",
   165  					Version:      "2.0.0",
   166  					PURL:         "pkg:pypi/bar@2.0.0",
   167  					Locations:    locations,
   168  					Language:     pkg.Python,
   169  					Type:         pkg.PythonPkg,
   170  					MetadataType: pkg.PythonRequirementsMetadataType,
   171  					Metadata: pkg.PythonRequirementsMetadata{
   172  						Name:              "bar",
   173  						VersionConstraint: ">= 1.0.0, <= 2.0.0, != 3.0.0, <= 3.0.0",
   174  					},
   175  				},
   176  				{
   177  					Name:         "numpy",
   178  					Version:      "3.4.1",
   179  					PURL:         "pkg:pypi/numpy@3.4.1",
   180  					Locations:    locations,
   181  					Language:     pkg.Python,
   182  					Type:         pkg.PythonPkg,
   183  					MetadataType: pkg.PythonRequirementsMetadataType,
   184  					Metadata: pkg.PythonRequirementsMetadata{
   185  						Name:              "numpy",
   186  						VersionConstraint: ">= 3.4.1",
   187  						Markers:           `sys_platform == 'win32'`,
   188  					},
   189  				},
   190  				{
   191  					Name:         "requests",
   192  					Version:      "2.8.0",
   193  					PURL:         "pkg:pypi/requests@2.8.0",
   194  					Locations:    locations,
   195  					Language:     pkg.Python,
   196  					Type:         pkg.PythonPkg,
   197  					MetadataType: pkg.PythonRequirementsMetadataType,
   198  					Metadata: pkg.PythonRequirementsMetadata{
   199  						Name:              "requests",
   200  						Extras:            []string{"security"},
   201  						VersionConstraint: "== 2.8.*",
   202  						Markers:           `python_version < "2.7" and sys_platform == "linux"`,
   203  					},
   204  				},
   205  			}, pinnedPkgs...),
   206  		},
   207  	}
   208  
   209  	for _, tc := range testCases {
   210  		t.Run(tc.name, func(t *testing.T) {
   211  			parser := newRequirementsParser(tc.cfg)
   212  			pkgtest.TestFileParser(t, tc.fixture, parser.parseRequirementsTxt, tc.expectedPkgs, tc.expectedRelationships)
   213  		})
   214  	}
   215  }
   216  
   217  func Test_newRequirement(t *testing.T) {
   218  
   219  	tests := []struct {
   220  		name string
   221  		raw  string
   222  		want *unprocessedRequirement
   223  	}{
   224  		{
   225  			name: "simple",
   226  			raw:  "requests==2.8",
   227  			want: &unprocessedRequirement{
   228  				Name:              "requests",
   229  				VersionConstraint: "==2.8",
   230  			},
   231  		},
   232  		{
   233  			name: "comment + constraint",
   234  			raw:  "Mopidy-Dirble ~= 1.1 # Compatible release. Same as >= 1.1, == 1.*",
   235  			want: &unprocessedRequirement{
   236  				Name:              "Mopidy-Dirble",
   237  				VersionConstraint: "~= 1.1",
   238  			},
   239  		},
   240  		{
   241  			name: "hashes",
   242  			raw:  "argh==0.26.3 --hash=sha256:a9b3aaa1904eeb78e32394cd46c6f37ac0fb4af6dc488daa58971bdc7d7fcaf3 --hash=sha256:e9535b8c84dc9571a48999094fda7f33e63c3f1b74f3e5f3ac0105a58405bb65",
   243  			want: &unprocessedRequirement{
   244  				Name:              "argh",
   245  				VersionConstraint: "==0.26.3",
   246  				Hashes:            "--hash=sha256:a9b3aaa1904eeb78e32394cd46c6f37ac0fb4af6dc488daa58971bdc7d7fcaf3 --hash=sha256:e9535b8c84dc9571a48999094fda7f33e63c3f1b74f3e5f3ac0105a58405bb65",
   247  			},
   248  		},
   249  		{
   250  			name: "extras",
   251  			raw:  "celery[redis, pytest] == 4.4.7 # should remove [redis, pytest]",
   252  			want: &unprocessedRequirement{
   253  				Name:              "celery[redis, pytest]",
   254  				VersionConstraint: "== 4.4.7",
   255  			},
   256  		},
   257  		{
   258  			name: "url",
   259  			raw:  "GithubSampleProject == 3.7.1 @ git+https://github.com/owner/repo@releases/tag/v3.7.1",
   260  			want: &unprocessedRequirement{
   261  				Name:              "GithubSampleProject",
   262  				VersionConstraint: "== 3.7.1",
   263  				URL:               "git+https://github.com/owner/repo@releases/tag/v3.7.1",
   264  			},
   265  		},
   266  		{
   267  			name: "markers",
   268  			raw:  "numpy >= 3.4.1 ; sys_platform == 'win32'",
   269  			want: &unprocessedRequirement{
   270  				Name:              "numpy",
   271  				VersionConstraint: ">= 3.4.1",
   272  				Markers:           "sys_platform == 'win32'",
   273  			},
   274  		},
   275  	}
   276  	for _, tt := range tests {
   277  		t.Run(tt.name, func(t *testing.T) {
   278  			assert.Equal(t, tt.want, newRequirement(tt.raw))
   279  		})
   280  	}
   281  }
   282  
   283  // checkout https://www.darius.page/pipdev/ for help here! (github.com/nok/pipdev)
   284  func Test_parseVersion(t *testing.T) {
   285  	tests := []struct {
   286  		name    string
   287  		version string
   288  		guess   bool
   289  		want    string
   290  	}{
   291  		{
   292  			name:    "exact",
   293  			version: "1.0.0",
   294  			want:    "", // we can only parse constraints, not assume that a single version is a pin
   295  		},
   296  		{
   297  			name:    "exact constraint",
   298  			version: " == 1.0.0 ",
   299  			want:    "1.0.0",
   300  		},
   301  		{
   302  			name:    "resolve lowest, simple constraint",
   303  			version: " >= 1.0.0 ",
   304  			guess:   true,
   305  			want:    "1.0.0",
   306  		},
   307  		{
   308  			name:    "resolve lowest, compound constraint",
   309  			version: "  < 2.0.0,  >= 1.0.0, != 1.1.0 ",
   310  			guess:   true,
   311  			want:    "1.0.0",
   312  		},
   313  		{
   314  			name:    "resolve lowest, handle asterisk",
   315  			version: "==2.8.*",
   316  			guess:   true,
   317  			want:    "2.8.0",
   318  		},
   319  		{
   320  			name:    "resolve lowest, handle exceptions",
   321  			version: " !=4.0.2,!=4.1.0,!=4.2.0,>=4.0.1,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0",
   322  			guess:   true,
   323  			want:    "4.0.1",
   324  		},
   325  		{
   326  			name:    "resolve lowest, compatible version constraint",
   327  			version: "~=0.6.10", // equates to >=0.6.10, ==0.6.*
   328  			guess:   true,
   329  			want:    "0.6.10",
   330  		},
   331  		{
   332  			name:    "resolve lowest, with character in version",
   333  			version: "~=1.2b,<=1.3a,!=1.1,!=1.2",
   334  			guess:   true,
   335  			want:    "1.3a0", // note: 1.3a == 1.3a0
   336  		},
   337  	}
   338  	for _, tt := range tests {
   339  		t.Run(tt.name, func(t *testing.T) {
   340  			assert.Equal(t, tt.want, parseVersion(tt.version, tt.guess))
   341  		})
   342  	}
   343  }