Skip to content

Commit 1705bb3

Browse files
authored
Enhance resolve_about_packages to handle filename and checksum values (#656)
- Split the pipes unit tests into their own related submodule Signed-off-by: Thomas Druez <tdruez@nexb.com>
1 parent e17c742 commit 1705bb3

17 files changed

Lines changed: 1545 additions & 1263 deletions

CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ v33.0.0 (unreleased)
1111
- Remove the admin app and views.
1212
https://github.com/nexB/scancode.io/issues/645
1313

14+
- Enhance the ``resolve_about_packages`` pipe to handle filename and checksum values.
15+
16+
- Split the pipes unit tests into their own related submodule.
17+
1418
v32.1.0 (2023-03-23)
1519
--------------------
1620

scanpipe/pipes/resolve.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
from scanpipe.models import DiscoveredPackage
3939

4040
"""
41-
Utilities to resolve packages from manifest, lockfile, and SBOM.
41+
Resolve packages from manifest, lockfile, and SBOM.
4242
"""
4343

4444

@@ -61,14 +61,22 @@ def resolve_about_packages(input_location):
6161
"""Resolve the packages from the `input_location` .ABOUT file."""
6262
about = About(location=input_location)
6363
about_data = about.as_dict()
64+
package_data = about_data.copy()
6465

6566
if package_url := about_data.get("package_url"):
6667
package_url_data = PackageURL.from_string(package_url).to_dict(encode=True)
6768
for field_name, value in package_url_data.items():
6869
if value:
69-
about_data[field_name] = value
70+
package_data[field_name] = value
7071

71-
package_data = DiscoveredPackage.clean_data(about_data)
72+
if about_resource := about_data.get("about_resource"):
73+
package_data["filename"] = list(about_resource.keys())[0]
74+
75+
for field_name, value in about_data.items():
76+
if field_name.startswith("checksum_"):
77+
package_data[field_name.replace("checksum_", "")] = value
78+
79+
package_data = DiscoveredPackage.clean_data(package_data)
7280
return [package_data]
7381

7482

@@ -177,11 +185,15 @@ def get_default_package_type(input_location):
177185
Return the package type associated with the provided `input_location`.
178186
This type is used to get the related handler that knows how process the input.
179187
"""
188+
input_location = str(input_location)
189+
180190
for handler in APPLICATION_PACKAGE_DATAFILE_HANDLERS:
181191
if handler.is_datafile(input_location):
182192
return handler.default_package_type
193+
183194
if input_location.endswith((".spdx", ".spdx.json")):
184195
return "spdx"
196+
185197
if input_location.endswith((".bom.json", ".cdx.json")):
186198
return "cyclonedx"
187199

scanpipe/tests/data/Django-4.0.8-py3-none-any.whl.ABOUT

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
about_resource: Django-4.0.8-py3-none-any.whl
22
name: django
33
version: 4.0.8
4-
download_url: https://files.pythonhosted.org/packages/e1/d0/d90528978da16288d470bb423abad307ed7ae724090132ff6bf67d6a5579/Django-4.0.8-py3-none-any.whl
4+
download_url: https://python.org/Django-4.0.8-py3-none-any.whl
55
package_url: pkg:pypi/django@4.0.8
66
license_expression: bsd-new
77
attribute: yes
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
name: project
2+
version:
3+
license_expression:

scanpipe/tests/pipes/__init__.py

Whitespace-only changes.
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
#
3+
# http://nexb.com and https://github.com/nexB/scancode.io
4+
# The ScanCode.io software is licensed under the Apache License version 2.0.
5+
# Data generated with ScanCode.io is provided as-is without warranties.
6+
# ScanCode is a trademark of nexB Inc.
7+
#
8+
# You may not use this software except in compliance with the License.
9+
# You may obtain a copy of the License at: http://apache.org/licenses/LICENSE-2.0
10+
# Unless required by applicable law or agreed to in writing, software distributed
11+
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
12+
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
13+
# specific language governing permissions and limitations under the License.
14+
#
15+
# Data Generated with ScanCode.io is provided on an "AS IS" BASIS, WITHOUT WARRANTIES
16+
# OR CONDITIONS OF ANY KIND, either express or implied. No content created from
17+
# ScanCode.io should be considered or used as legal advice. Consult an Attorney
18+
# for any legal advice.
19+
#
20+
# ScanCode.io is a free software code scanning tool from nexB Inc. and others.
21+
# Visit https://github.com/nexB/scancode.io for support and download.
22+
23+
import json
24+
import sys
25+
from pathlib import Path
26+
from unittest import skipIf
27+
28+
from django.core.management import call_command
29+
from django.test import TestCase
30+
31+
from scanpipe.models import Project
32+
from scanpipe.pipes import codebase
33+
from scanpipe.pipes import scancode
34+
35+
36+
class ScanPipeCodebasePipesTest(TestCase):
37+
data_location = Path(__file__).parent.parent / "data"
38+
39+
def test_scanpipe_pipes_codebase_get_codebase_tree(self):
40+
def _replace_path(virtual_tree_children):
41+
"""
42+
Given a list `virtual_tree_children` of mappings, remove instances
43+
of "virtual_root/" from the paths of mappings and their children,
44+
recursively.
45+
"""
46+
for res in virtual_tree_children:
47+
path = res["path"]
48+
path = path.replace("virtual_root/", "")
49+
res["path"] = path
50+
_replace_path(res.get("children", []))
51+
52+
fixtures = self.data_location / "asgiref-3.3.0_fixtures.json"
53+
call_command("loaddata", fixtures, **{"verbosity": 0})
54+
project = Project.objects.get(name="asgiref")
55+
56+
scan_results = self.data_location / "asgiref-3.3.0_scanpipe_output.json"
57+
virtual_codebase = scancode.get_virtual_codebase(project, scan_results)
58+
project_codebase = codebase.ProjectCodebase(project)
59+
60+
fields = ["name", "path"]
61+
62+
virtual_tree = codebase.get_codebase_tree(virtual_codebase, fields)
63+
project_tree = codebase.get_codebase_tree(project_codebase, fields)
64+
65+
with open(self.data_location / "asgiref-3.3.0_tree.json") as f:
66+
expected = json.loads(f.read())
67+
68+
self.assertEqual(expected, project_tree)
69+
70+
virtual_tree_children = virtual_tree["children"][0]["children"]
71+
_replace_path(virtual_tree_children)
72+
73+
self.assertEqual(expected["children"], virtual_tree_children)
74+
75+
def test_scanpipe_pipes_codebase_project_codebase_class_no_resources(self):
76+
project = Project.objects.create(name="project")
77+
project_codebase = codebase.ProjectCodebase(project)
78+
79+
self.assertEqual([], list(project_codebase.root_resources))
80+
self.assertEqual([], list(project_codebase.resources))
81+
self.assertEqual([], list(project_codebase.walk()))
82+
self.assertEqual(dict(children=[]), project_codebase.get_tree())
83+
84+
def test_scanpipe_pipes_codebase_project_codebase_class_with_resources(self):
85+
fixtures = self.data_location / "asgiref-3.3.0_fixtures.json"
86+
call_command("loaddata", fixtures, **{"verbosity": 0})
87+
88+
project = Project.objects.get(name="asgiref")
89+
project_codebase = codebase.ProjectCodebase(project)
90+
91+
expected_root_resources = project.codebaseresources.exclude(path__contains="/")
92+
expected_root_resources = list(expected_root_resources)
93+
self.assertEqual(expected_root_resources, list(project_codebase.root_resources))
94+
95+
self.assertEqual(18, len(project_codebase.resources))
96+
97+
walk_gen = project_codebase.walk()
98+
self.assertEqual(next(iter(expected_root_resources)), next(walk_gen))
99+
expected = "asgiref-3.3.0-py3-none-any.whl-extract"
100+
self.assertEqual(expected, next(walk_gen).path)
101+
102+
tree = project_codebase.get_tree()
103+
with open(self.data_location / "asgiref-3.3.0_tree.json") as f:
104+
expected = json.loads(f.read())
105+
106+
self.assertEqual(expected, tree)
107+
108+
@skipIf(sys.platform != "linux", "Ordering differs on macOS.")
109+
def test_scanpipe_pipes_codebase_project_codebase_class_walk(self):
110+
fixtures = self.data_location / "asgiref-3.3.0_fixtures.json"
111+
call_command("loaddata", fixtures, **{"verbosity": 0})
112+
113+
project = Project.objects.get(name="asgiref")
114+
project_codebase = codebase.ProjectCodebase(project)
115+
116+
topdown_paths = list(r.path for r in project_codebase.walk(topdown=True))
117+
expected_topdown_paths = [
118+
"asgiref-3.3.0-py3-none-any.whl",
119+
"asgiref-3.3.0-py3-none-any.whl-extract",
120+
"asgiref-3.3.0-py3-none-any.whl-extract/asgiref",
121+
"asgiref-3.3.0-py3-none-any.whl-extract/asgiref/compatibility.py",
122+
"asgiref-3.3.0-py3-none-any.whl-extract/asgiref/current_thread_executor.py",
123+
"asgiref-3.3.0-py3-none-any.whl-extract/asgiref/__init__.py",
124+
"asgiref-3.3.0-py3-none-any.whl-extract/asgiref/local.py",
125+
"asgiref-3.3.0-py3-none-any.whl-extract/asgiref/server.py",
126+
"asgiref-3.3.0-py3-none-any.whl-extract/asgiref/sync.py",
127+
"asgiref-3.3.0-py3-none-any.whl-extract/asgiref/testing.py",
128+
"asgiref-3.3.0-py3-none-any.whl-extract/asgiref/timeout.py",
129+
"asgiref-3.3.0-py3-none-any.whl-extract/asgiref/wsgi.py",
130+
"asgiref-3.3.0-py3-none-any.whl-extract/asgiref-3.3.0.dist-info",
131+
"asgiref-3.3.0-py3-none-any.whl-extract/asgiref-3.3.0.dist-info/LICENSE",
132+
"asgiref-3.3.0-py3-none-any.whl-extract/asgiref-3.3.0.dist-info/METADATA",
133+
"asgiref-3.3.0-py3-none-any.whl-extract/asgiref-3.3.0.dist-info/RECORD",
134+
"asgiref-3.3.0-py3-none-any.whl-extract/asgiref-3.3.0.dist-info/"
135+
"top_level.txt",
136+
"asgiref-3.3.0-py3-none-any.whl-extract/asgiref-3.3.0.dist-info/WHEEL",
137+
]
138+
self.assertEqual(expected_topdown_paths, topdown_paths)
139+
140+
bottom_up_paths = list(r.path for r in project_codebase.walk(topdown=False))
141+
expected_bottom_up_paths = [
142+
"asgiref-3.3.0-py3-none-any.whl",
143+
"asgiref-3.3.0-py3-none-any.whl-extract/asgiref/compatibility.py",
144+
"asgiref-3.3.0-py3-none-any.whl-extract/asgiref/current_thread_executor.py",
145+
"asgiref-3.3.0-py3-none-any.whl-extract/asgiref/__init__.py",
146+
"asgiref-3.3.0-py3-none-any.whl-extract/asgiref/local.py",
147+
"asgiref-3.3.0-py3-none-any.whl-extract/asgiref/server.py",
148+
"asgiref-3.3.0-py3-none-any.whl-extract/asgiref/sync.py",
149+
"asgiref-3.3.0-py3-none-any.whl-extract/asgiref/testing.py",
150+
"asgiref-3.3.0-py3-none-any.whl-extract/asgiref/timeout.py",
151+
"asgiref-3.3.0-py3-none-any.whl-extract/asgiref/wsgi.py",
152+
"asgiref-3.3.0-py3-none-any.whl-extract/asgiref",
153+
"asgiref-3.3.0-py3-none-any.whl-extract/asgiref-3.3.0.dist-info/LICENSE",
154+
"asgiref-3.3.0-py3-none-any.whl-extract/asgiref-3.3.0.dist-info/METADATA",
155+
"asgiref-3.3.0-py3-none-any.whl-extract/asgiref-3.3.0.dist-info/RECORD",
156+
"asgiref-3.3.0-py3-none-any.whl-extract/asgiref-3.3.0.dist-info/"
157+
"top_level.txt",
158+
"asgiref-3.3.0-py3-none-any.whl-extract/asgiref-3.3.0.dist-info/WHEEL",
159+
"asgiref-3.3.0-py3-none-any.whl-extract/asgiref-3.3.0.dist-info",
160+
"asgiref-3.3.0-py3-none-any.whl-extract",
161+
]
162+
self.assertEqual(expected_bottom_up_paths, bottom_up_paths)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737

3838

3939
class ScanPipeDockerPipesTest(TestCase):
40-
data_path = Path(__file__).parent / "data"
40+
data_path = Path(__file__).parent.parent / "data"
4141
maxDiff = None
4242

4343
def assertResultsEqual(self, expected_file, results, regen=FIXTURES_REGEN):

scanpipe/tests/pipes/test_fetch.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
#
3+
# http://nexb.com and https://github.com/nexB/scancode.io
4+
# The ScanCode.io software is licensed under the Apache License version 2.0.
5+
# Data generated with ScanCode.io is provided as-is without warranties.
6+
# ScanCode is a trademark of nexB Inc.
7+
#
8+
# You may not use this software except in compliance with the License.
9+
# You may obtain a copy of the License at: http://apache.org/licenses/LICENSE-2.0
10+
# Unless required by applicable law or agreed to in writing, software distributed
11+
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
12+
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
13+
# specific language governing permissions and limitations under the License.
14+
#
15+
# Data Generated with ScanCode.io is provided on an "AS IS" BASIS, WITHOUT WARRANTIES
16+
# OR CONDITIONS OF ANY KIND, either express or implied. No content created from
17+
# ScanCode.io should be considered or used as legal advice. Consult an Attorney
18+
# for any legal advice.
19+
#
20+
# ScanCode.io is a free software code scanning tool from nexB Inc. and others.
21+
# Visit https://github.com/nexB/scancode.io for support and download.
22+
23+
from pathlib import Path
24+
from unittest import mock
25+
26+
from django.test import TestCase
27+
28+
from scanpipe.pipes import fetch
29+
30+
31+
class ScanPipeFetchPipesTest(TestCase):
32+
data_location = Path(__file__).parent.parent / "data"
33+
34+
@mock.patch("requests.get")
35+
def test_scanpipe_pipes_fetch_http(self, mock_get):
36+
url = "https://example.com/filename.zip"
37+
38+
mock_get.return_value = mock.Mock(
39+
content=b"\x00", headers={}, status_code=200, url=url
40+
)
41+
downloaded_file = fetch.fetch_http(url)
42+
self.assertTrue(Path(downloaded_file.directory, "filename.zip").exists())
43+
44+
redirect_url = "https://example.com/redirect.zip"
45+
mock_get.return_value = mock.Mock(
46+
content=b"\x00", headers={}, status_code=200, url=redirect_url
47+
)
48+
downloaded_file = fetch.fetch_http(url)
49+
self.assertTrue(Path(downloaded_file.directory, "redirect.zip").exists())
50+
51+
headers = {
52+
"content-disposition": 'attachment; filename="another_name.zip"',
53+
}
54+
mock_get.return_value = mock.Mock(
55+
content=b"\x00", headers=headers, status_code=200, url=url
56+
)
57+
downloaded_file = fetch.fetch_http(url)
58+
self.assertTrue(Path(downloaded_file.directory, "another_name.zip").exists())
59+
60+
@mock.patch("scanpipe.pipes.fetch.get_docker_image_platform")
61+
@mock.patch("scanpipe.pipes.fetch._get_skopeo_location")
62+
@mock.patch("scanpipe.pipes.run_command")
63+
def test_scanpipe_pipes_fetch_docker_image(
64+
self, mock_run_command, mock_skopeo, mock_platform
65+
):
66+
url = "docker://debian:10.9"
67+
68+
mock_platform.return_value = "linux", "amd64", ""
69+
mock_skopeo.return_value = "skopeo"
70+
mock_run_command.return_value = 1, "error"
71+
72+
with self.assertRaises(fetch.FetchDockerImageError):
73+
fetch.fetch_docker_image(url)
74+
75+
mock_run_command.assert_called_once()
76+
cmd = mock_run_command.call_args[0][0]
77+
self.assertTrue(cmd.startswith("skopeo copy --insecure-policy"))
78+
self.assertIn("docker://debian:10.9 docker-archive:/", cmd)
79+
self.assertIn("--override-os=linux --override-arch=amd64", cmd)
80+
self.assertTrue(cmd.endswith("debian_10_9.tar"))
81+
82+
@mock.patch("requests.get")
83+
def test_scanpipe_pipes_fetch_fetch_urls(self, mock_get):
84+
urls = [
85+
"https://example.com/filename.zip",
86+
"https://example.com/archive.tar.gz",
87+
]
88+
89+
mock_get.return_value = mock.Mock(
90+
content=b"\x00", headers={}, status_code=200, url="mocked_url"
91+
)
92+
downloads, errors = fetch.fetch_urls(urls)
93+
self.assertEqual(2, len(downloads))
94+
self.assertEqual(urls[0], downloads[0].uri)
95+
self.assertEqual(urls[1], downloads[1].uri)
96+
self.assertEqual(0, len(errors))
97+
98+
mock_get.side_effect = Exception
99+
downloads, errors = fetch.fetch_urls(urls)
100+
self.assertEqual(0, len(downloads))
101+
self.assertEqual(2, len(errors))
102+
self.assertEqual(urls, errors)

0 commit comments

Comments
 (0)