-
Notifications
You must be signed in to change notification settings - Fork 135
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[issue-558] add optional feature to generate a relationship graph
Signed-off-by: Meret Behrens <meret.behrens@tngtech.com>
- Loading branch information
Showing
8 changed files
with
295 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
# SPDX-FileCopyrightText: 2023 spdx contributors | ||
# | ||
# SPDX-License-Identifier: Apache-2.0 | ||
from typing import Dict, List, Union | ||
|
||
from spdx.model.file import File | ||
from spdx.model.package import Package | ||
from spdx.model.snippet import Snippet | ||
|
||
try: | ||
from networkx import DiGraph | ||
except ImportError: | ||
DiGraph = None | ||
from spdx.document_utils import get_contained_spdx_elements | ||
from spdx.model.document import Document | ||
from spdx.model.relationship import Relationship | ||
|
||
|
||
def export_graph_from_document(document: Document, file_name: str) -> None: | ||
from networkx.drawing import nx_agraph | ||
|
||
graph = generate_relationship_graph_from_spdx(document) | ||
_color_nodes(graph) | ||
attributes_graph = nx_agraph.to_agraph(graph) # convert to a pygraphviz graph | ||
attributes_graph.draw(file_name, prog="dot") | ||
|
||
|
||
def generate_relationship_graph_from_spdx(document: Document) -> DiGraph: | ||
from networkx import DiGraph | ||
|
||
graph = DiGraph() | ||
graph.add_node(document.creation_info.spdx_id, element=document.creation_info) | ||
|
||
contained_elements: Dict[str, Union[Package, File, Snippet]] = get_contained_spdx_elements(document) | ||
contained_element_nodes = [(spdx_id, {"element": element}) for spdx_id, element in contained_elements.items()] | ||
graph.add_nodes_from(contained_element_nodes) | ||
|
||
relationships_by_spdx_id: Dict[str, List[Relationship]] = dict() | ||
for relationship in document.relationships: | ||
relationships_by_spdx_id.setdefault(relationship.spdx_element_id, []).append(relationship) | ||
|
||
for spdx_id, relationships in relationships_by_spdx_id.items(): | ||
if spdx_id not in graph.nodes(): | ||
# this will add any external spdx_id to the graph where we have no further information about the element, | ||
# to indicate that this node represents an element we add the attribute "element" | ||
graph.add_node(spdx_id, element=None) | ||
for relationship in relationships: | ||
relationship_node_key = relationship.spdx_element_id + "_" + relationship.relationship_type.name | ||
graph.add_node(relationship_node_key, comment=relationship.comment) | ||
graph.add_edge(relationship.spdx_element_id, relationship_node_key) | ||
# if the related spdx element is SpdxNone or SpdxNoAssertion we need a type conversion | ||
related_spdx_element_id = str(relationship.related_spdx_element_id) | ||
|
||
if related_spdx_element_id not in graph.nodes(): | ||
# this will add any external spdx_id to the graph where we have no further information about | ||
# the element, to indicate that this node represents an element we add the attribute "element" | ||
graph.add_node( | ||
related_spdx_element_id, | ||
element=None, | ||
) | ||
graph.add_edge(relationship_node_key, related_spdx_element_id) | ||
|
||
return graph | ||
|
||
|
||
def _color_nodes(graph: DiGraph) -> None: | ||
for node in graph.nodes(): | ||
if "_" in node: | ||
# nodes representing a RelationshipType are concatenated with the spdx_element_id, | ||
# to only see the RelationshipType when rendering the graph to a picture we add | ||
# a label to these nodes | ||
graph.add_node(node, color="lightgreen", label=node.split("_", 1)[-1]) | ||
elif node == "SPDXRef-DOCUMENT": | ||
graph.add_node(node, color="indianred2") | ||
else: | ||
graph.add_node(node, color="lightskyblue") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
# SPDX-FileCopyrightText: 2023 spdx contributors | ||
# | ||
# SPDX-License-Identifier: Apache-2.0 | ||
from pathlib import Path | ||
from typing import List | ||
from unittest import TestCase | ||
|
||
import pytest | ||
|
||
from spdx.graph_generation import generate_relationship_graph_from_spdx | ||
from spdx.model.document import Document | ||
from spdx.model.relationship import Relationship, RelationshipType | ||
from spdx.parser.parse_anything import parse_file | ||
from tests.spdx.fixtures import document_fixture, file_fixture, package_fixture | ||
|
||
try: | ||
import networkx # noqa: F401 | ||
import pygraphviz # noqa: F401 | ||
except ImportError: | ||
pytest.skip("Skip this module as the tests need optional dependencies to run.", allow_module_level=True) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"file_name, nodes_count, edges_count, relationship_node_keys", | ||
[ | ||
( | ||
"SPDXJSONExample-v2.3.spdx.json", | ||
22, | ||
22, | ||
["SPDXRef-Package_DYNAMIC_LINK", "SPDXRef-JenaLib_CONTAINS"], | ||
), | ||
( | ||
"SPDXJSONExample-v2.2.spdx.json", | ||
20, | ||
19, | ||
["SPDXRef-Package_DYNAMIC_LINK", "SPDXRef-JenaLib_CONTAINS"], | ||
), | ||
( | ||
"SPDXRdfExample-v2.3.spdx.rdf.xml", | ||
22, | ||
22, | ||
["SPDXRef-Package_DYNAMIC_LINK", "SPDXRef-JenaLib_CONTAINS"], | ||
), | ||
( | ||
"SPDXRdfExample-v2.2.spdx.rdf.xml", | ||
20, | ||
17, | ||
["SPDXRef-Package_DYNAMIC_LINK", "SPDXRef-JenaLib_CONTAINS"], | ||
), | ||
( | ||
"SPDXTagExample-v2.3.spdx", | ||
22, | ||
22, | ||
["SPDXRef-Package_DYNAMIC_LINK", "SPDXRef-JenaLib_CONTAINS"], | ||
), | ||
], | ||
) | ||
def test_generate_graph_from_spdx( | ||
file_name: str, | ||
nodes_count: int, | ||
edges_count: int, | ||
relationship_node_keys: List[str], | ||
) -> None: | ||
document = parse_file(str(Path(__file__).resolve().parent.parent / "spdx" / "data" / "formats" / file_name)) | ||
graph = generate_relationship_graph_from_spdx(document) | ||
|
||
assert document.creation_info.spdx_id in graph.nodes() | ||
assert graph.number_of_nodes() == nodes_count | ||
assert graph.number_of_edges() == edges_count | ||
assert "SPDXRef-DOCUMENT_DESCRIBES" in graph.nodes() | ||
for relationship_node_key in relationship_node_keys: | ||
assert relationship_node_key in graph.nodes() | ||
|
||
|
||
def test_complete_connected_graph() -> None: | ||
document = _create_minimal_document() | ||
|
||
graph = generate_relationship_graph_from_spdx(document) | ||
|
||
TestCase().assertCountEqual( | ||
graph.nodes(), | ||
[ | ||
"SPDXRef-DOCUMENT", | ||
"SPDXRef-Package-A", | ||
"SPDXRef-Package-B", | ||
"SPDXRef-File", | ||
"SPDXRef-DOCUMENT_DESCRIBES", | ||
"SPDXRef-Package-A_CONTAINS", | ||
"SPDXRef-Package-B_CONTAINS", | ||
], | ||
) | ||
TestCase().assertCountEqual( | ||
graph.edges(), | ||
[ | ||
("SPDXRef-DOCUMENT", "SPDXRef-DOCUMENT_DESCRIBES"), | ||
("SPDXRef-DOCUMENT_DESCRIBES", "SPDXRef-Package-A"), | ||
("SPDXRef-DOCUMENT_DESCRIBES", "SPDXRef-Package-B"), | ||
("SPDXRef-Package-A", "SPDXRef-Package-A_CONTAINS"), | ||
("SPDXRef-Package-A_CONTAINS", "SPDXRef-File"), | ||
("SPDXRef-Package-B", "SPDXRef-Package-B_CONTAINS"), | ||
("SPDXRef-Package-B_CONTAINS", "SPDXRef-File"), | ||
], | ||
) | ||
|
||
|
||
def test_complete_unconnected_graph() -> None: | ||
document = _create_minimal_document() | ||
document.packages += [package_fixture(spdx_id="SPDXRef-Package-C", name="Package without connection to document")] | ||
|
||
graph = generate_relationship_graph_from_spdx(document) | ||
|
||
TestCase().assertCountEqual( | ||
graph.nodes(), | ||
[ | ||
"SPDXRef-DOCUMENT", | ||
"SPDXRef-Package-A", | ||
"SPDXRef-Package-B", | ||
"SPDXRef-File", | ||
"SPDXRef-DOCUMENT_DESCRIBES", | ||
"SPDXRef-Package-A_CONTAINS", | ||
"SPDXRef-Package-B_CONTAINS", | ||
"SPDXRef-Package-C", | ||
], | ||
) | ||
TestCase().assertCountEqual( | ||
graph.edges(), | ||
[ | ||
("SPDXRef-DOCUMENT", "SPDXRef-DOCUMENT_DESCRIBES"), | ||
("SPDXRef-DOCUMENT_DESCRIBES", "SPDXRef-Package-A"), | ||
("SPDXRef-DOCUMENT_DESCRIBES", "SPDXRef-Package-B"), | ||
("SPDXRef-Package-A", "SPDXRef-Package-A_CONTAINS"), | ||
("SPDXRef-Package-A_CONTAINS", "SPDXRef-File"), | ||
("SPDXRef-Package-B", "SPDXRef-Package-B_CONTAINS"), | ||
("SPDXRef-Package-B_CONTAINS", "SPDXRef-File"), | ||
], | ||
) | ||
|
||
|
||
def _create_minimal_document() -> Document: | ||
packages = [ | ||
package_fixture(spdx_id="SPDXRef-Package-A", name="Package-A"), | ||
package_fixture(spdx_id="SPDXRef-Package-B", name="Package-B"), | ||
] | ||
files = [ | ||
file_fixture(spdx_id="SPDXRef-File", name="File"), | ||
] | ||
relationships = [ | ||
Relationship("SPDXRef-DOCUMENT", RelationshipType.DESCRIBES, "SPDXRef-Package-A"), | ||
Relationship("SPDXRef-DOCUMENT", RelationshipType.DESCRIBES, "SPDXRef-Package-B"), | ||
Relationship("SPDXRef-Package-A", RelationshipType.CONTAINS, "SPDXRef-File"), | ||
Relationship("SPDXRef-Package-B", RelationshipType.CONTAINS, "SPDXRef-File"), | ||
] | ||
document = document_fixture(packages=packages, files=files, relationships=relationships, snippets=[]) | ||
|
||
return document |