1414currentdir = os .path .dirname (os .path .realpath (__file__ ))
1515sys .path .insert (1 , os .path .dirname (currentdir ))
1616
17- from api .api_utils import is_http_url , parse_comma_separated_list # noqa E402
17+ from api .api_utils import ( # noqa E402
18+ get_test_run_artifacts_dir ,
19+ is_http_url ,
20+ list_test_run_artifacts ,
21+ parse_comma_separated_list ,
22+ )
1823from db .db_orm import DbInterface # noqa E402
1924from db .models .api import ApiModel # noqa E402
2025from db .models .api_document import ApiDocumentModel # noqa E402
@@ -76,6 +81,9 @@ class SPDXExternalIdentifier:
7681 ``identifier`` and, when the value is an http(s) URL, also set
7782 ``identifierLocator``.
7883
84+ Test Run artifacts use:
85+ basil:test_runs:<id>:artifact:<filename>
86+
7987 Example:
8088 identifier = "basil:sw_requirements:42"
8189 comment = "BASIL Software Requirement 'My Title' with ID 42"
@@ -1135,6 +1143,7 @@ def addJustification(self, justification: JustificationModel = None, dbsession=N
11351143 def addTestRuns (self , spdx_tc : SPDXFile = None , mapping_to : str = "" , mapping_id : int = 0 , dbsession = None ):
11361144 if not self .include_test_runs :
11371145 logger .warning ("Skip Test Runs as per export configuration" )
1146+ return
11381147
11391148 added_test_runs = []
11401149 test_runs_query = (
@@ -1199,6 +1208,7 @@ def addTestRun(self, test_run: TestRunModel = None, dbsession=None):
11991208 self .add_to_sbom (tr_file )
12001209 self .add_to_sbom (tr_annotation )
12011210 self .addTestRunBugAndFixOutputs (spdx_tr = tr_file , test_run = test_run , creation_info = creation_info )
1211+ self .addTestRunArtifacts (spdx_tr = tr_file , test_run = test_run , creation_info = creation_info )
12021212 return tr_file
12031213
12041214 def addTestRunBugOrFix (
@@ -1275,6 +1285,101 @@ def addTestRunBugAndFixOutputs(
12751285 relationship_type = SpdxRelationshipType .HAS_OUTPUT ,
12761286 )
12771287
1288+ def addTestRunArtifact (
1289+ self ,
1290+ test_run : TestRunModel = None ,
1291+ artifact_name : str = "" ,
1292+ index : int = 1 ,
1293+ creation_info : SPDXCreationInfo = None ,
1294+ ):
1295+ """Create an SPDX File for one Test Run artifact on disk.
1296+
1297+ Artifacts live under ``TEST_RUNS_BASE_DIR/<uid>/api/tmt-plan/data/``.
1298+ ``verifiedUsing`` prefers an MD5 of the file contents when readable;
1299+ otherwise a metadata hash is used.
1300+ """
1301+ artifacts_dir = get_test_run_artifacts_dir (test_run .uid )
1302+ artifact_path = os .path .join (artifacts_dir , artifact_name )
1303+ spdx_id = f"spdx:file:basil:test-run:{ test_run .id } :artifact:{ index } "
1304+
1305+ content_hash = None
1306+ if os .path .isfile (artifact_path ):
1307+ try :
1308+ dhash = hashlib .md5 ()
1309+ with open (artifact_path , "rb" ) as artifact_file :
1310+ for chunk in iter (lambda : artifact_file .read (1024 * 1024 ), b"" ):
1311+ dhash .update (chunk )
1312+ content_hash = SPDXMD5Hash (hash_value = dhash .hexdigest ())
1313+ except OSError as e :
1314+ logger .warning (f"Unable to hash artifact { artifact_path } : { e } " )
1315+
1316+ if content_hash is None :
1317+ content_hash = self .make_hash_object (
1318+ data_dict = {
1319+ "artifact_name" : artifact_name ,
1320+ "test_run_id" : test_run .id ,
1321+ "index" : index ,
1322+ }
1323+ )
1324+
1325+ artifact_file = SPDXFile (
1326+ spdx_id = spdx_id ,
1327+ name = artifact_name ,
1328+ comment = "BASIL Artifact" ,
1329+ description = f"Artifact '{ artifact_name } ' from BASIL Test Run { test_run .id } " ,
1330+ purpose = "evidence" ,
1331+ copyright_text = "" ,
1332+ verified_using = [content_hash ],
1333+ external_identifiers = [
1334+ SPDXExternalIdentifier (
1335+ identifier = f"basil:{ test_run .__tablename__ } :{ test_run .id } :artifact:{ artifact_name } " ,
1336+ comment = f"BASIL Test Run { test_run .id } Artifact '{ artifact_name } '" ,
1337+ )
1338+ ],
1339+ creation_info = creation_info ,
1340+ )
1341+ self .add_to_sbom (artifact_file )
1342+ return artifact_file
1343+
1344+ def addTestRunArtifacts (
1345+ self ,
1346+ spdx_tr : SPDXFile = None ,
1347+ test_run : TestRunModel = None ,
1348+ creation_info : SPDXCreationInfo = None ,
1349+ ):
1350+ """Emit SPDX Files for Test Run artifacts and link them 1-to-many.
1351+
1352+ Each on-disk artifact becomes an SPDX File (purpose ``evidence``).
1353+ The Test Run is linked to all artifacts with both ``hasOutput`` and
1354+ ``hasEvidence`` relationships (one relationship of each type covering
1355+ the full artifact list).
1356+ """
1357+ artifact_names = list_test_run_artifacts (test_run .uid )
1358+ if not artifact_names :
1359+ return
1360+
1361+ added_artifacts = []
1362+ for index , artifact_name in enumerate (artifact_names , start = 1 ):
1363+ added_artifacts .append (
1364+ self .addTestRunArtifact (
1365+ test_run = test_run ,
1366+ artifact_name = artifact_name ,
1367+ index = index ,
1368+ creation_info = creation_info ,
1369+ )
1370+ )
1371+
1372+ self .addRelationship (
1373+ from_element = spdx_tr ,
1374+ to = added_artifacts ,
1375+ relationship_type = SpdxRelationshipType .HAS_OUTPUT ,
1376+ )
1377+ self .addRelationship (
1378+ from_element = spdx_tr ,
1379+ to = added_artifacts ,
1380+ relationship_type = SpdxRelationshipType .HAS_EVIDENCE ,
1381+ )
1382+
12781383 def addDocumentsNestedElements (
12791384 self ,
12801385 api : ApiModel = None ,
@@ -1629,6 +1734,7 @@ def get_file_node_color(node):
16291734 "test run" : "purple" ,
16301735 "bug" : "salmon" ,
16311736 "fix" : "olivedrab" ,
1737+ "artifact" : "khaki" ,
16321738 }
16331739
16341740 if hasattr (node , "comment" ):
@@ -1744,6 +1850,7 @@ def add_edge(src, dst, label):
17441850 legend .node ("test_run" , label = "Test Run" , shape = "box" , style = "filled" , fillcolor = "purple" )
17451851 legend .node ("bug" , label = "Bug" , shape = "box" , style = "filled" , fillcolor = "salmon" )
17461852 legend .node ("fix" , label = "Fix" , shape = "box" , style = "filled" , fillcolor = "olivedrab" )
1853+ legend .node ("artifact" , label = "Artifact" , shape = "box" , style = "filled" , fillcolor = "khaki" )
17471854
17481855 # Stack legend nodes vertically
17491856 legend .edge ("library" , "software_component" , style = "invis" , weight = "100" )
@@ -1757,6 +1864,7 @@ def add_edge(src, dst, label):
17571864 legend .edge ("test_case" , "test_run" , style = "invis" , weight = "100" )
17581865 legend .edge ("test_run" , "bug" , style = "invis" , weight = "100" )
17591866 legend .edge ("bug" , "fix" , style = "invis" , weight = "100" )
1867+ legend .edge ("fix" , "artifact" , style = "invis" , weight = "100" )
17601868
17611869 dot .subgraph (legend )
17621870
0 commit comments