Skip to content

Commit e7481fa

Browse files
authored
Merge pull request #35 from sw360/rework-project-bom-code
Rework some project and bom methods
2 parents 53e1b37 + 2a2c2bf commit e7481fa

8 files changed

Lines changed: 117 additions & 89 deletions

File tree

capycli/bom/download_sources.py

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import requests
1919
from cyclonedx.model import ExternalReference, ExternalReferenceType, HashAlgorithm, HashType
2020
from cyclonedx.model.bom import Bom
21-
from cyclonedx.model.component import Component
2221

2322
import capycli.common.json_support
2423
import capycli.common.script_base
@@ -122,35 +121,20 @@ def download_sources(self, sbom: Bom, source_folder: str) -> None:
122121
if new:
123122
component.external_references.add(ext_ref)
124123

125-
def have_relative_source_file_path(self, component: Component, bompath: str):
126-
ext_ref = CycloneDxSupport.get_ext_ref(
127-
component, ExternalReferenceType.DISTRIBUTION, CaPyCliBom.SOURCE_FILE_COMMENT)
128-
if not ext_ref:
129-
return
130-
131-
bip = pathlib.PurePath(ext_ref.url)
132-
try:
133-
CycloneDxSupport.update_or_set_property(
134-
component,
135-
CycloneDxSupport.CDX_PROP_FILENAME,
136-
bip.name)
137-
file = bip.as_posix()
138-
if os.path.isfile(file):
139-
CycloneDxSupport.update_or_set_ext_ref(
140-
component,
141-
ExternalReferenceType.DISTRIBUTION,
142-
CaPyCliBom.SOURCE_FILE_COMMENT,
143-
"file://" + bip.relative_to(bompath).as_posix())
144-
except ValueError:
145-
print_yellow(
146-
" SBOM file is not relative to source file " + ext_ref.url)
147-
# .relative_to
148-
pass
149-
150124
def update_local_path(self, sbom: Bom, bomfile: str):
151125
bompath = pathlib.Path(bomfile).parent
152126
for component in sbom.components:
153-
self.have_relative_source_file_path(component, bompath)
127+
ext_ref = CycloneDxSupport.get_ext_ref(
128+
component, ExternalReferenceType.DISTRIBUTION, CaPyCliBom.SOURCE_FILE_COMMENT)
129+
if ext_ref:
130+
try:
131+
name = CycloneDxSupport.have_relative_ext_ref_path(ext_ref, bompath)
132+
CycloneDxSupport.update_or_set_property(
133+
component,
134+
CycloneDxSupport.CDX_PROP_FILENAME,
135+
name)
136+
except ValueError:
137+
print_yellow(" SBOM file is not relative to source file " + ext_ref.url)
154138

155139
def run(self, args):
156140
"""Main method

capycli/common/capycli_bom_support.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import json
1010
import os
1111
import tempfile
12+
import pathlib
1213
import uuid
1314
from datetime import datetime
1415
from enum import Enum
@@ -370,6 +371,14 @@ def update_or_set_ext_ref(comp: Component, type: ExternalReferenceType, comment:
370371
else:
371372
CycloneDxSupport.set_ext_ref(comp, type, comment, value)
372373

374+
@staticmethod
375+
def have_relative_ext_ref_path(ext_ref: ExternalReference, rel_to: str):
376+
bip = pathlib.PurePath(ext_ref.url)
377+
file = bip.as_posix()
378+
if os.path.isfile(file):
379+
ext_ref.url = "file://" + bip.relative_to(rel_to).as_posix()
380+
return bip.name
381+
373382
@staticmethod
374383
def get_ext_ref_by_comment(comp: Component, comment: str) -> Any:
375384
for ext_ref in comp.external_references:

capycli/common/script_base.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
Base class for python scripts.
1111
"""
1212

13+
from typing import List, Tuple
14+
1315
import json
1416
import os
1517
import sys
@@ -111,6 +113,32 @@ def get_error_message(self, swex: sw360.sw360_api.SW360Error) -> str:
111113
str(jcontent["status"]) + "): " + jcontent["message"]
112114
return text
113115

116+
@staticmethod
117+
def get_release_attachments(release_details: dict, att_types: Tuple[str] = None) -> List[dict]:
118+
"""Returns the attachments with the given types from a release. Use empty att_types
119+
to get all attachments."""
120+
if "_embedded" not in release_details:
121+
return None
122+
123+
if "sw360:attachments" not in release_details["_embedded"]:
124+
return None
125+
126+
found = []
127+
attachments = release_details["_embedded"]["sw360:attachments"]
128+
if not att_types:
129+
return attachments
130+
131+
for attachment in attachments:
132+
if attachment["attachmentType"] in att_types:
133+
found.append(attachment)
134+
135+
return found
136+
137+
def release_web_url(self, release_id) -> str:
138+
"""Returns the HTML URL for a given release_id."""
139+
return (self.sw360_url + "group/guest/components/-/component/release/detailRelease/"
140+
+ release_id)
141+
114142
def find_project(self, name: str, version: str, show_results: bool = False) -> str:
115143
"""Find the project with the matching name and version on SW360"""
116144
print_text(" Searching for project...")

capycli/project/create_bom.py

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
import logging
1010
import sys
11-
from typing import List, Tuple
1211

1312
import sw360
1413
from cyclonedx.model import ExternalReferenceType, HashAlgorithm
@@ -34,22 +33,6 @@ def get_external_id(self, name: str, release_details: dict):
3433

3534
return release_details["externalIds"].get(name, "")
3635

37-
def get_attachments(self, att_types: Tuple[str], release_details: dict) -> List[dict]:
38-
"""Returns the attachments with the given types or empty list."""
39-
if "_embedded" not in release_details:
40-
return None
41-
42-
if "sw360:attachments" not in release_details["_embedded"]:
43-
return None
44-
45-
found = []
46-
attachments = release_details["_embedded"]["sw360:attachments"]
47-
for attachment in attachments:
48-
if attachment["attachmentType"] in att_types:
49-
found.append(attachment)
50-
51-
return found
52-
5336
def get_clearing_state(self, proj, href) -> str:
5437
"""Returns the clearing state of the given component/release"""
5538
rel = proj["linkedReleases"]
@@ -103,7 +86,7 @@ def create_project_bom(self, project) -> list:
10386

10487
for at_type, comment in (("SOURCE", CaPyCliBom.SOURCE_FILE_COMMENT),
10588
("BINARY", CaPyCliBom.BINARY_FILE_COMMENT)):
106-
attachments = self.get_attachments((at_type, at_type + "_SELF"), release_details)
89+
attachments = self.get_release_attachments(release_details, (at_type, at_type + "_SELF"))
10790
for attachment in attachments:
10891
CycloneDxSupport.set_ext_ref(rel_item, ExternalReferenceType.DISTRIBUTION,
10992
comment, attachment["filename"],
@@ -123,9 +106,7 @@ def create_project_bom(self, project) -> list:
123106
CycloneDxSupport.set_property(
124107
rel_item,
125108
CycloneDxSupport.CDX_PROP_SW360_URL,
126-
self.sw360_url
127-
+ "group/guest/components/-/component/release/detailRelease/"
128-
+ sw360_id)
109+
self.release_web_url(sw360_id))
129110

130111
bom.append(rel_item)
131112

capycli/project/show_ecc.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,8 @@ def get_project_status(self, project_id: str):
114114
rel_item["Id"] = self.client.get_id_from_href(href)
115115
rel_item["S360Id"] = rel_item["Id"]
116116
rel_item["Href"] = href
117-
rel_item["Url"] = (
118-
self.sw360_url
119-
+ "group/guest/components/-/component/release/detailRelease/"
120-
+ self.client.get_id_from_href(href))
117+
rel_item["Url"] = self.release_web_url(
118+
self.client.get_id_from_href(href))
121119

122120
try:
123121
release_details = self.client.get_release_by_url(href)

tests/test_base.py

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -84,16 +84,13 @@ def dump_textfile(text: str, filename: str) -> None:
8484
outfile.write(text)
8585

8686
@staticmethod
87-
def capture_stderr(func: Any, args: Any = None) -> str:
87+
def capture_stderr(func: Any, *args, **kwargs) -> str:
8888
"""Capture stderr for the given function and return result as string"""
8989
# setup the environment
9090
old_stderr = sys.stderr
9191
sys.stderr = TextIOWrapper(BytesIO(), sys.stderr.encoding)
9292

93-
if args:
94-
func(args)
95-
else:
96-
func()
93+
func(*args, **kwargs)
9794

9895
# get output
9996
sys.stderr.seek(0) # jump to the start
@@ -106,33 +103,13 @@ def capture_stderr(func: Any, args: Any = None) -> str:
106103
return out
107104

108105
@staticmethod
109-
def capture_stdout(func: Any, args: Any = None) -> str:
106+
def capture_stdout(func: Any, *args, **kwargs) -> str:
110107
"""Capture stdout for the given function and return result as string"""
111108
# setup the environment
112109
old_stdout = sys.stdout
113110
sys.stdout = TextIOWrapper(BytesIO(), sys.stdout.encoding)
114111

115-
func(args)
116-
117-
# get output
118-
sys.stdout.seek(0) # jump to the start
119-
out = sys.stdout.read() # read output
120-
121-
# restore stdout
122-
sys.stdout.close()
123-
sys.stdout = old_stdout
124-
125-
return out
126-
127-
@staticmethod
128-
def capture_stdout_no_args(func: Any) -> str:
129-
"""Capture stdout for the given function and return result as string"""
130-
# setup the environment
131-
old_stdout = sys.stdout
132-
sys.stdout = TextIOWrapper(BytesIO(), sys.stdout.encoding)
133-
134-
func()
135-
112+
func(*args, **kwargs)
136113
# get output
137114
sys.stdout.seek(0) # jump to the start
138115
out = sys.stdout.read() # read output

tests/test_bom_downloadsources.py

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@
1111

1212
import responses
1313

14+
from cyclonedx.model import ExternalReferenceType
15+
16+
from capycli.common.capycli_bom_support import CaPyCliBom, CycloneDxSupport
1417
from capycli.bom.download_sources import BomDownloadSources
1518
from capycli.main.result_codes import ResultCode
1619
from tests.test_base import AppArguments, TestBase
1720

1821

19-
class TestShowBom(TestBase):
22+
class TestBomDownloadsources(TestBase):
2023
INPUTFILE = "sbom_for_download.json"
2124
INPUTERROR = "plaintext.txt"
2225
OUTPUTFILE = "output.json"
@@ -74,7 +77,7 @@ def test_error_loading_file(self) -> None:
7477
args.command = []
7578
args.command.append("bom")
7679
args.command.append("downloadsources")
77-
args.inputfile = os.path.join(os.path.dirname(__file__), "fixtures", TestShowBom.INPUTERROR)
80+
args.inputfile = os.path.join(os.path.dirname(__file__), "fixtures", TestBomDownloadsources.INPUTERROR)
7881

7982
sut.run(args)
8083
self.assertTrue(False, "Failed to report invalid file")
@@ -90,7 +93,7 @@ def test_source_folder_does_not_exist(self) -> None:
9093
args.command = []
9194
args.command.append("bom")
9295
args.command.append("downloadsources")
93-
args.inputfile = os.path.join(os.path.dirname(__file__), "fixtures", TestShowBom.INPUTFILE)
96+
args.inputfile = os.path.join(os.path.dirname(__file__), "fixtures", TestBomDownloadsources.INPUTFILE)
9497
args.source = "XXX"
9598

9699
sut.run(args)
@@ -107,8 +110,8 @@ def test_simple_bom(self) -> None:
107110
args.command = []
108111
args.command.append("bom")
109112
args.command.append("downloadsources")
110-
args.inputfile = os.path.join(os.path.dirname(__file__), "fixtures", TestShowBom.INPUTFILE)
111-
args.outputfile = TestShowBom.OUTPUTFILE
113+
args.inputfile = os.path.join(os.path.dirname(__file__), "fixtures", TestBomDownloadsources.INPUTFILE)
114+
args.outputfile = TestBomDownloadsources.OUTPUTFILE
112115

113116
with tempfile.TemporaryDirectory() as tmpdirname:
114117
args.source = tmpdirname
@@ -126,20 +129,68 @@ def test_simple_bom(self) -> None:
126129

127130
try:
128131
out = self.capture_stdout(sut.run, args)
132+
out_bom = CaPyCliBom.read_sbom(args.outputfile)
129133
# capycli.common.json_support.write_json_to_file(out, "STDOUT.TXT")
130134
self.assertTrue("Loading SBOM file" in out)
131135
self.assertTrue("sbom_for_download.json" in out) # path may vary
136+
self.assertIn("SBOM file is not relative to", out)
132137
self.assertTrue("Downloading source files to folder" in out)
133138
self.assertTrue("Downloading file certifi-2022.12.7.tar.gz" in out)
134139

135140
resultfile = os.path.join(tmpdirname, "certifi-2022.12.7.tar.gz")
136141
self.assertTrue(os.path.isfile(resultfile))
137142

143+
ext_ref = CycloneDxSupport.get_ext_ref(
144+
out_bom.components[0], ExternalReferenceType.DISTRIBUTION, CaPyCliBom.SOURCE_FILE_COMMENT)
145+
self.assertEqual(ext_ref.url, resultfile)
146+
138147
self.delete_file(args.outputfile)
139148
return
140-
except: # noqa
149+
except Exception as e: # noqa
141150
# catch all exception to let Python cleanup the temp folder
142-
pass
151+
print(e)
152+
153+
self.assertTrue(False, "Error: we must never arrive here")
154+
155+
@responses.activate
156+
def test_simple_bom_relative_path(self) -> None:
157+
sut = BomDownloadSources()
158+
159+
# create argparse command line argument object
160+
args = AppArguments()
161+
args.command = []
162+
args.command.append("bom")
163+
args.command.append("downloadsources")
164+
args.inputfile = os.path.join(os.path.dirname(__file__), "fixtures", TestBomDownloadsources.INPUTFILE)
165+
166+
with tempfile.TemporaryDirectory() as tmpdirname:
167+
args.source = tmpdirname
168+
args.outputfile = os.path.join(tmpdirname, TestBomDownloadsources.OUTPUTFILE)
169+
170+
# fake file content
171+
responses.add(
172+
responses.GET,
173+
url="https://files.pythonhosted.org/packages/37/f7/2b1b/certifi-2022.12.7.tar.gz",
174+
body="""
175+
SOME DUMMY DATA
176+
""",
177+
status=200,
178+
content_type="application/json",
179+
)
180+
181+
try:
182+
sut.run(args)
183+
out_bom = CaPyCliBom.read_sbom(args.outputfile)
184+
185+
ext_ref = CycloneDxSupport.get_ext_ref(
186+
out_bom.components[0], ExternalReferenceType.DISTRIBUTION, CaPyCliBom.SOURCE_FILE_COMMENT)
187+
self.assertEqual(ext_ref.url, "file://certifi-2022.12.7.tar.gz")
188+
189+
self.delete_file(args.outputfile)
190+
return
191+
except Exception as e: # noqa
192+
# catch all exception to let Python cleanup the temp folder
193+
print(e)
143194

144195
self.assertTrue(False, "Error: we must never arrive here")
145196

@@ -152,8 +203,8 @@ def test_simple_bom_error_download(self) -> None:
152203
args.command = []
153204
args.command.append("bom")
154205
args.command.append("downloadsources")
155-
args.inputfile = os.path.join(os.path.dirname(__file__), "fixtures", TestShowBom.INPUTFILE)
156-
args.outputfile = os.path.join(os.path.dirname(__file__), "fixtures", TestShowBom.OUTPUTFILE)
206+
args.inputfile = os.path.join(os.path.dirname(__file__), "fixtures", TestBomDownloadsources.INPUTFILE)
207+
args.outputfile = os.path.join(os.path.dirname(__file__), "fixtures", TestBomDownloadsources.OUTPUTFILE)
157208

158209
with tempfile.TemporaryDirectory() as tmpdirname:
159210
args.source = tmpdirname

tests/test_logging.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def test_error_critical(self) -> None:
3535
self.assertFalse(self.DEBUG_MSG in out)
3636

3737
def test_info_debug(self) -> None:
38-
out = self.capture_stdout_no_args(self.create_output)
38+
out = self.capture_stdout(self.create_output)
3939
# self.dump_textfile(out, "dump.txt")
4040
self.assertFalse(self.CRITICAL_MSG in out)
4141
self.assertFalse(self.ERROR_MSG in out)

0 commit comments

Comments
 (0)