Commit 510a69f3 authored by James Long's avatar James Long Committed by Commit Bot

Parse jdeps class graphs into package graphs

Added a new class that takes a JavaClassDependencyGraph as input and
groups the classes into their packages, forming a less granular
package-level dependency graph. Unit tests are included.

To allow for this change, functionality was added to the Graph class
to fetch a list of nodes and edges. Unit tests are included for this as
well.

Bug: 1081889
Change-Id: I6ff39a68945d7907afbbf6d58a6cb885fafcaba4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2213408
Commit-Queue: James Long <yjlong@google.com>
Reviewed-by: default avatarHenrique Nakashima <hnakashima@chromium.org>
Reviewed-by: default avatarMohamed Heikal <mheikal@chromium.org>
Cr-Commit-Position: refs/heads/master@{#772368}
parent 2877fb92
...@@ -10,13 +10,13 @@ for more details on the presubmit API built into depot_tools. ...@@ -10,13 +10,13 @@ for more details on the presubmit API built into depot_tools.
def CommonChecks(input_api, output_api): def CommonChecks(input_api, output_api):
checks = input_api.canned_checks.GetUnitTestsRecursively( checks = input_api.canned_checks.GetUnitTestsRecursively(
input_api, input_api,
output_api, output_api,
input_api.PresubmitLocalPath(), input_api.PresubmitLocalPath(),
whitelist=[r'.+_unittest\.py$'], whitelist=[r'.+_unittest\.py$'],
blacklist=[], blacklist=[],
run_on_python2=False, run_on_python2=False,
run_on_python3=True) run_on_python3=True)
return input_api.RunTests(checks, False) return input_api.RunTests(checks, False)
......
...@@ -33,9 +33,6 @@ class JavaClass(graph.Node): ...@@ -33,9 +33,6 @@ class JavaClass(graph.Node):
Some classes may have nested classes (eg. explicitly, or Some classes may have nested classes (eg. explicitly, or
implicitly through lambdas). We treat these nested classes as part of implicitly through lambdas). We treat these nested classes as part of
the outer class, storing only their names as metadata. the outer class, storing only their names as metadata.
Attributes:
nested_classes: A set of nested classes contained within this class.
""" """
def __init__(self, package: str, class_name: str): def __init__(self, package: str, class_name: str):
"""Initializes a new Java class structure. """Initializes a new Java class structure.
...@@ -54,6 +51,19 @@ class JavaClass(graph.Node): ...@@ -54,6 +51,19 @@ class JavaClass(graph.Node):
self._nested_classes = set() self._nested_classes = set()
@property
def package(self):
"""The package the class belongs to."""
return self._package
@property
def class_name(self):
"""The name of the class.
For nested classes, this is the name of the class that contains them.
"""
return self._class_name
@property @property
def nested_classes(self): def nested_classes(self):
"""A set of nested classes contained within this class.""" """A set of nested classes contained within this class."""
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
# found in the LICENSE file. # found in the LICENSE file.
"""Unit tests for dependency_analysis.class_dependency.""" """Unit tests for dependency_analysis.class_dependency."""
import unittest import unittest.mock
import class_dependency import class_dependency
...@@ -16,29 +16,31 @@ class TestHelperFunctions(unittest.TestCase): ...@@ -16,29 +16,31 @@ class TestHelperFunctions(unittest.TestCase):
self.assertEqual(result, 'pkg.name.class') self.assertEqual(result, 'pkg.name.class')
def test_split_nested_class_from_key(self): def test_split_nested_class_from_key(self):
"""Tests that the helper correctly splits out a nested class""" """Tests that the helper correctly splits out a nested class."""
part1, part2 = class_dependency.split_nested_class_from_key( part1, part2 = class_dependency.split_nested_class_from_key(
'pkg.name.class$nested') 'pkg.name.class$nested')
self.assertEqual(part1, 'pkg.name.class') self.assertEqual(part1, 'pkg.name.class')
self.assertEqual(part2, 'nested') self.assertEqual(part2, 'nested')
def test_split_nested_class_from_key_no_nested(self): def test_split_nested_class_from_key_no_nested(self):
"""Tests that the helper works when there is no nested class""" """Tests that the helper works when there is no nested class."""
part1, part2 = class_dependency.split_nested_class_from_key( part1, part2 = class_dependency.split_nested_class_from_key(
'pkg.name.class') 'pkg.name.class')
self.assertEqual(part1, 'pkg.name.class') self.assertEqual(part1, 'pkg.name.class')
self.assertIsNone(part2) self.assertIsNone(part2)
def test_split_nested_class_from_key_lambda(self): def test_split_nested_class_from_key_lambda(self):
"""Tests that the helper works for common jdeps output (lambdas)""" """Tests that the helper works for jdeps' formatting of lambdas."""
part1, part2 = class_dependency.split_nested_class_from_key( part1, part2 = class_dependency.split_nested_class_from_key(
'pkg.name.class$$Lambda$1') 'pkg.name.class$$Lambda$1')
self.assertEqual(part1, 'pkg.name.class') self.assertEqual(part1, 'pkg.name.class')
self.assertEqual(part2, '$Lambda$1') self.assertEqual(part2, '$Lambda$1')
def test_split_nested_class_from_key_numeric(self): def test_split_nested_class_from_key_numeric(self):
"""Tests that the helper works for common jdeps output """Tests that the helper works for jdeps' formatting of nested classes.
(numeric class name, used for private nested classes)"""
Specifically, jdeps uses a numeric name for private nested classes.
"""
part1, part2 = class_dependency.split_nested_class_from_key( part1, part2 = class_dependency.split_nested_class_from_key(
'pkg.name.class$1') 'pkg.name.class$1')
self.assertEqual(part1, 'pkg.name.class') self.assertEqual(part1, 'pkg.name.class')
...@@ -53,9 +55,11 @@ class TestJavaClass(unittest.TestCase): ...@@ -53,9 +55,11 @@ class TestJavaClass(unittest.TestCase):
UNIQUE_KEY_2 = 'def' UNIQUE_KEY_2 = 'def'
def test_initialization(self): def test_initialization(self):
"""Tests that the JavaClass's unique_key was initialized correctly.""" """Tests that JavaClass is initialized correctly."""
test_node = class_dependency.JavaClass(self.TEST_PKG, self.TEST_CLS) test_node = class_dependency.JavaClass(self.TEST_PKG, self.TEST_CLS)
self.assertEqual(test_node.name, f'{self.TEST_PKG}.{self.TEST_CLS}') self.assertEqual(test_node.name, f'{self.TEST_PKG}.{self.TEST_CLS}')
self.assertEqual(test_node.package, self.TEST_PKG)
self.assertEqual(test_node.class_name, self.TEST_CLS)
def test_equality(self): def test_equality(self):
"""Tests that two JavaClasses with the same package+class are equal.""" """Tests that two JavaClasses with the same package+class are equal."""
...@@ -86,8 +90,9 @@ class TestJavaClass(unittest.TestCase): ...@@ -86,8 +90,9 @@ class TestJavaClass(unittest.TestCase):
class TestJavaClassDependencyGraph(unittest.TestCase): class TestJavaClassDependencyGraph(unittest.TestCase):
"""Unit tests for """Unit tests for JavaClassDependencyGraph.
dependency_analysis.class_dependency.JavaClassDependencyGraph.
Full name: dependency_analysis.class_dependency.JavaClassDependencyGraph.
""" """
def setUp(self): def setUp(self):
"""Sets up a new JavaClassDependencyGraph.""" """Sets up a new JavaClassDependencyGraph."""
...@@ -103,11 +108,11 @@ class TestJavaClassDependencyGraph(unittest.TestCase): ...@@ -103,11 +108,11 @@ class TestJavaClassDependencyGraph(unittest.TestCase):
def test_add_nested_class_to_key(self): def test_add_nested_class_to_key(self):
"""Tests adding a nested class to an existing node.""" """Tests adding a nested class to an existing node."""
self.test_graph.add_node_if_new('package.class') added = self.test_graph.add_node_if_new('package.class')
added.add_nested_class = unittest.mock.Mock()
self.test_graph.add_nested_class_to_key('package.class', 'nested') self.test_graph.add_nested_class_to_key('package.class', 'nested')
added = self.test_graph.get_node_by_key('package.class') added.add_nested_class.assert_called_once_with('nested')
self.assertEqual(len(added.nested_classes), 1)
self.assertIn('nested', added.nested_classes)
if __name__ == '__main__': if __name__ == '__main__':
......
...@@ -3,15 +3,23 @@ ...@@ -3,15 +3,23 @@
# found in the LICENSE file. # found in the LICENSE file.
"""Utility classes (and functions, in the future) for graph operations.""" """Utility classes (and functions, in the future) for graph operations."""
from typing import List, Tuple
class Node(object): # pylint: disable=useless-object-inheritance
"""A node/vertex in a directed graph.
Attributes: def sorted_nodes_by_name(nodes):
name: A unique string representation of the node. """Sorts a list of Nodes by their name."""
inbound: A set of Nodes that have a directed edge into this Node. return sorted(nodes, key=lambda node: node.name)
outbound: A set of Nodes that this Node has a directed edge into.
"""
def sorted_edges_by_name(edges):
"""Sorts a list of edges (tuples) by their names.
Prioritizes sorting by the first node in an edge."""
return sorted(edges, key=lambda edge: (edge[0].name, edge[1].name))
class Node(object): # pylint: disable=useless-object-inheritance
"""A node/vertex in a directed graph."""
def __init__(self, unique_key: str): def __init__(self, unique_key: str):
"""Initializes a new node with the given key. """Initializes a new node with the given key.
...@@ -57,21 +65,30 @@ class Graph(object): # pylint: disable=useless-object-inheritance ...@@ -57,21 +65,30 @@ class Graph(object): # pylint: disable=useless-object-inheritance
Maintains an internal Dict[str, Node] _key_to_node Maintains an internal Dict[str, Node] _key_to_node
mapping the unique key of nodes to their Node objects. mapping the unique key of nodes to their Node objects.
Attributes:
num_nodes: The number of nodes in the graph.
num_edges: The number of edges in the graph.
""" """
def __init__(self): # pylint: disable=missing-function-docstring def __init__(self): # pylint: disable=missing-function-docstring
self._key_to_node = {} self._key_to_node = {}
self._edges = []
@property
def num_nodes(self) -> int:
"""The number of nodes in the graph."""
return len(self.nodes)
@property @property
def num_nodes(self): # pylint: disable=missing-function-docstring def num_edges(self) -> int:
return len(self._key_to_node) """The number of edges in the graph."""
return len(self.edges)
@property @property
def num_edges(self): # pylint: disable=missing-function-docstring def nodes(self) -> List:
return sum(len(node.outbound) for node in self._key_to_node.values()) """A list of Nodes in the graph."""
return list(self._key_to_node.values())
@property
def edges(self) -> List[Tuple]:
"""A list of tuples (begin, end) representing directed edges."""
return self._edges
def get_node_by_key(self, key: str): # pylint: disable=missing-function-docstring def get_node_by_key(self, key: str): # pylint: disable=missing-function-docstring
return self._key_to_node.get(key) return self._key_to_node.get(key)
...@@ -83,19 +100,25 @@ class Graph(object): # pylint: disable=useless-object-inheritance ...@@ -83,19 +100,25 @@ class Graph(object): # pylint: disable=useless-object-inheritance
""" """
return Node(key) return Node(key)
def add_node_if_new(self, key: str): def add_node_if_new(self, key: str) -> Node:
"""Adds a Node to the graph. """Adds a Node to the graph.
A new Node object is constructed from the given key and added. A new Node object is constructed from the given key and added.
If the key already exists in the graph, this is a no-op. If the key already exists in the graph, no change is made to the graph.
Args: Args:
key: A unique key to create the new Node from. key: A unique key to create the new Node from.
Returns:
The Node with the given key in the graph.
""" """
if key not in self._key_to_node: node = self._key_to_node.get(key)
self._key_to_node[key] = self.create_node_from_key(key) if node is None:
node = self.create_node_from_key(key)
self._key_to_node[key] = node
return node
def add_edge_if_new(self, src: str, dest: str): def add_edge_if_new(self, src: str, dest: str) -> bool:
"""Adds a directed edge to the graph. """Adds a directed edge to the graph.
The source and destination nodes are created and added if they The source and destination nodes are created and added if they
...@@ -105,10 +128,17 @@ class Graph(object): # pylint: disable=useless-object-inheritance ...@@ -105,10 +128,17 @@ class Graph(object): # pylint: disable=useless-object-inheritance
Args: Args:
src: A unique key representing the source node. src: A unique key representing the source node.
dest: A unique key representing the destination node. dest: A unique key representing the destination node.
Returns:
True if the edge was added (did not already exist), else False
""" """
self.add_node_if_new(src) src_node = self.add_node_if_new(src)
self.add_node_if_new(dest) dest_node = self.add_node_if_new(dest)
src_node = self.get_node_by_key(src)
dest_node = self.get_node_by_key(dest) # The following check is much faster than `if (src, dest) not in _edges`
src_node.add_outbound(dest_node) if dest_node not in src_node.outbound:
dest_node.add_inbound(src_node) src_node.add_outbound(dest_node)
dest_node.add_inbound(src_node)
self._edges.append((src_node, dest_node))
return True
return False
...@@ -82,6 +82,8 @@ class TestGraph(unittest.TestCase): ...@@ -82,6 +82,8 @@ class TestGraph(unittest.TestCase):
"""Tests that the graph was initialized correctly.""" """Tests that the graph was initialized correctly."""
self.assertEqual(self.test_graph.num_nodes, 0) self.assertEqual(self.test_graph.num_nodes, 0)
self.assertEqual(self.test_graph.num_edges, 0) self.assertEqual(self.test_graph.num_edges, 0)
self.assertEqual(self.test_graph.nodes, [])
self.assertEqual(self.test_graph.edges, [])
def test_get_node_exists(self): def test_get_node_exists(self):
"""Tests getting a node that we know exists in the graph.""" """Tests getting a node that we know exists in the graph."""
...@@ -95,48 +97,59 @@ class TestGraph(unittest.TestCase): ...@@ -95,48 +97,59 @@ class TestGraph(unittest.TestCase):
def test_add_nodes(self): def test_add_nodes(self):
"""Tests adding two different nodes to the graph.""" """Tests adding two different nodes to the graph."""
self.test_graph.add_node_if_new(self.UNIQUE_KEY_1) node1 = self.test_graph.add_node_if_new(self.UNIQUE_KEY_1)
self.test_graph.add_node_if_new(self.UNIQUE_KEY_2) node2 = self.test_graph.add_node_if_new(self.UNIQUE_KEY_2)
self.assertEqual(self.test_graph.num_nodes, 2) self.assertEqual(self.test_graph.num_nodes, 2)
self.assertEqual(graph.sorted_nodes_by_name(self.test_graph.nodes),
graph.sorted_nodes_by_name([node1, node2]))
def test_add_nodes_duplicate(self): def test_add_nodes_duplicate(self):
"""Tests adding the same node twice to the graph.""" """Tests adding the same node twice to the graph."""
self.test_graph.add_node_if_new(self.UNIQUE_KEY_1) self.test_graph.add_node_if_new(self.UNIQUE_KEY_1)
self.test_graph.add_node_if_new(self.UNIQUE_KEY_1) node = self.test_graph.add_node_if_new(self.UNIQUE_KEY_1)
self.assertEqual(self.test_graph.num_nodes, 1) self.assertEqual(self.test_graph.num_nodes, 1)
self.assertEqual(self.test_graph.nodes, [node])
def test_add_edge(self): def test_add_edge(self):
"""Tests adding a new edge to the graph.""" """Tests adding a new edge to the graph."""
self.test_graph.add_node_if_new(self.UNIQUE_KEY_1) node1 = self.test_graph.add_node_if_new(self.UNIQUE_KEY_1)
self.test_graph.add_node_if_new(self.UNIQUE_KEY_2) node2 = self.test_graph.add_node_if_new(self.UNIQUE_KEY_2)
self.test_graph.add_edge_if_new(self.UNIQUE_KEY_1, self.UNIQUE_KEY_2) self.test_graph.add_edge_if_new(self.UNIQUE_KEY_1, self.UNIQUE_KEY_2)
self.assertEqual(self.test_graph.num_edges, 1) self.assertEqual(self.test_graph.num_edges, 1)
node1 = self.test_graph.get_node_by_key(self.UNIQUE_KEY_1)
node2 = self.test_graph.get_node_by_key(self.UNIQUE_KEY_2)
self.assertEqual(node2.inbound, {node1}) self.assertEqual(node2.inbound, {node1})
self.assertEqual(node1.outbound, {node2}) self.assertEqual(node1.outbound, {node2})
self.assertEqual(self.test_graph.edges, [(node1, node2)])
def test_add_edge_double_sided(self): def test_add_edge_double_sided(self):
"""Tests adding a bidirectional edge to the graph.""" """Tests adding a bidirectional edge to the graph."""
self.test_graph.add_node_if_new(self.UNIQUE_KEY_1) node1 = self.test_graph.add_node_if_new(self.UNIQUE_KEY_1)
self.test_graph.add_node_if_new(self.UNIQUE_KEY_2) node2 = self.test_graph.add_node_if_new(self.UNIQUE_KEY_2)
self.test_graph.add_edge_if_new(self.UNIQUE_KEY_1, self.UNIQUE_KEY_2) self.test_graph.add_edge_if_new(self.UNIQUE_KEY_1, self.UNIQUE_KEY_2)
self.test_graph.add_edge_if_new(self.UNIQUE_KEY_2, self.UNIQUE_KEY_1) self.test_graph.add_edge_if_new(self.UNIQUE_KEY_2, self.UNIQUE_KEY_1)
self.assertEqual(self.test_graph.num_edges, 2) self.assertEqual(self.test_graph.num_edges, 2)
node1 = self.test_graph.get_node_by_key(self.UNIQUE_KEY_1)
node2 = self.test_graph.get_node_by_key(self.UNIQUE_KEY_2)
self.assertEqual(node1.inbound, {node2}) self.assertEqual(node1.inbound, {node2})
self.assertEqual(node1.outbound, {node2}) self.assertEqual(node1.outbound, {node2})
self.assertEqual(node2.inbound, {node1}) self.assertEqual(node2.inbound, {node1})
self.assertEqual(node2.outbound, {node1}) self.assertEqual(node2.outbound, {node1})
self.assertEqual(
graph.sorted_edges_by_name(self.test_graph.edges),
graph.sorted_edges_by_name([(node1, node2), (node2, node1)]))
def test_add_edge_duplicate(self): def test_add_edge_duplicate(self):
"""Tests adding a duplicate edge to the graph.""" """Tests adding a duplicate edge to the graph."""
self.test_graph.add_node_if_new(self.UNIQUE_KEY_1) node1 = self.test_graph.add_node_if_new(self.UNIQUE_KEY_1)
self.test_graph.add_node_if_new(self.UNIQUE_KEY_2) node2 = self.test_graph.add_node_if_new(self.UNIQUE_KEY_2)
self.test_graph.add_edge_if_new(self.UNIQUE_KEY_1, self.UNIQUE_KEY_2) edge_added_first = self.test_graph.add_edge_if_new(
self.test_graph.add_edge_if_new(self.UNIQUE_KEY_1, self.UNIQUE_KEY_2) self.UNIQUE_KEY_1, self.UNIQUE_KEY_2)
edge_added_second = self.test_graph.add_edge_if_new(
self.UNIQUE_KEY_1, self.UNIQUE_KEY_2)
self.assertEqual(self.test_graph.num_edges, 1) self.assertEqual(self.test_graph.num_edges, 1)
self.assertTrue(edge_added_first)
self.assertFalse(edge_added_second)
self.assertEqual(self.test_graph.edges, [(node1, node2)])
def test_add_edge_nodes_do_not_exist(self): def test_add_edge_nodes_do_not_exist(self):
"""Tests adding a new edge to a graph without the edge's nodes.""" """Tests adding a new edge to a graph without the edge's nodes."""
......
# Copyright 2020 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Implementation of the graph module for a [Java package] dependency graph."""
import collections
from typing import Set, Tuple
import class_dependency
import graph
class JavaPackage(graph.Node):
"""A representation of a Java package."""
def __init__(self, package_name: str):
"""Initializes a new Java package structure.
Args:
package_name: The name of the package.
"""
super().__init__(package_name)
self._classes = {}
self._class_dependency_edges = collections.defaultdict(set)
@property
def classes(self):
"""A map { name -> JavaClass } of classes within this package."""
return self._classes
def add_class(self, java_class: class_dependency.JavaClass):
"""Adds a JavaClass to the package, if its key doesn't already exist.
Notably, this does /not/ automatically update the inbound/outbound
dependencies of the package with the dependencies of the class.
"""
if java_class.name not in self._classes:
self._classes[java_class.name] = java_class
def add_class_dependency_edge(self, end_package: 'JavaPackage',
begin_class: class_dependency.JavaClass,
end_class: class_dependency.JavaClass):
"""Adds a class dependency edge as part of a package dependency.
Each package dependency is comprised of one or more class dependencies,
we manually update the nodes with this info when parsing class graphs.
Args:
end_package: the end node of the package dependency edge
which starts from this node.
begin_class: the start node of the class dependency edge.
end_class: the end node of the class dependency edge.
"""
class_edge = (begin_class, end_class)
if class_edge not in self._class_dependency_edges[end_package]:
self._class_dependency_edges[end_package].add(class_edge)
def get_class_dependencies_in_outbound_edge(
self, end_node: 'JavaPackage') -> Set[Tuple]:
"""Breaks down a package dependency edge into class dependencies.
For package A to depend on another package B, there must exist
at least one class in A depending on a class in B. This method, given
a package dependency edge A -> B, returns a set of class
dependency edges satisfying (class in A) -> (class in B).
Args:
end_node: The destination node. This method will return the class
dependencies forming the edge from the current node to end_node.
Returns:
A set of tuples of `JavaClass` nodes, where a tuple (a, b)
indicates a class dependency a -> b. If there are no relevant
class dependencies, returns an empty set.
"""
return self._class_dependency_edges[end_node]
class JavaPackageDependencyGraph(graph.Graph):
"""A graph representation of the dependencies between Java packages.
A directed edge A -> B indicates that A depends on B.
"""
def __init__(self, class_graph: class_dependency.JavaClassDependencyGraph):
"""Initializes a new package-level dependency graph
by "collapsing" a class-level dependency graph into its packages.
Args:
class_graph: A class-level graph to collapse to a package-level one.
"""
super().__init__()
# Create list of all packages using class nodes
# so we don't miss packages with no dependencies (edges).
for class_node in class_graph.nodes:
self.add_node_if_new(class_node.package)
for begin_class, end_class in class_graph.edges:
begin_package = begin_class.package
end_package = end_class.package
self.add_edge_if_new(begin_package, end_package)
begin_package_node = self.get_node_by_key(begin_package)
end_package_node = self.get_node_by_key(end_package)
begin_package_node.add_class(begin_class)
end_package_node.add_class(end_class)
begin_package_node.add_class_dependency_edge(
end_package_node, begin_class, end_class)
def create_node_from_key(self, package_name: str):
"""Create a JavaPackage node from the given package name."""
return JavaPackage(package_name)
#!/usr/bin/env python3
# Copyright 2020 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Unit tests for dependency_analysis.package_dependency."""
import unittest.mock
import graph
import package_dependency
def create_mock_java_class(pkg='package', cls='class'):
"""Returns a Mock of JavaClass.
The fields `class_name`, `package`, and `name` will be initialized.
"""
mock_class = unittest.mock.Mock()
mock_class.class_name = cls
mock_class.package = pkg
mock_class.name = f'{pkg}.{cls}'
return mock_class
class TestJavaPackage(unittest.TestCase):
"""Unit tests for dependency_analysis.class_dependency.JavaPackage."""
TEST_PKG_1 = 'package1'
TEST_PKG_2 = 'package2'
TEST_CLS_1 = 'class1'
TEST_CLS_2 = 'class2'
TEST_CLS_3 = 'class3'
def test_initialization(self):
"""Tests that JavaPackage is initialized correctly."""
test_node = package_dependency.JavaPackage(self.TEST_PKG_1)
self.assertEqual(test_node.name, self.TEST_PKG_1)
self.assertEqual(test_node.classes, {})
def test_add_class(self):
"""Tests adding a single class to this package."""
test_node = package_dependency.JavaPackage(self.TEST_PKG_1)
mock_class_node = create_mock_java_class()
test_node.add_class(mock_class_node)
self.assertEqual(test_node.classes,
{mock_class_node.name: mock_class_node})
def test_add_class_multiple(self):
"""Tests adding multiple classes to this package."""
test_node = package_dependency.JavaPackage(self.TEST_PKG_1)
mock_class_node_1 = create_mock_java_class(cls=self.TEST_CLS_1)
mock_class_node_2 = create_mock_java_class(cls=self.TEST_CLS_2)
test_node.add_class(mock_class_node_1)
test_node.add_class(mock_class_node_2)
self.assertEqual(
test_node.classes, {
mock_class_node_1.name: mock_class_node_1,
mock_class_node_2.name: mock_class_node_2
})
def test_add_class_duplicate(self):
"""Tests that adding the same class twice will not dupe."""
test_node = package_dependency.JavaPackage(self.TEST_PKG_1)
mock_class_node = create_mock_java_class()
test_node.add_class(mock_class_node)
test_node.add_class(mock_class_node)
self.assertEqual(test_node.classes,
{mock_class_node.name: mock_class_node})
def test_get_class_dependencies_in_outbound_edge(self):
"""Tests adding/getting class dependency edges for a package edge."""
start_node = package_dependency.JavaPackage(self.TEST_PKG_1)
end_node = package_dependency.JavaPackage(self.TEST_PKG_2)
# Create three class nodes (1, 2, 3)
mock_class_node_1 = create_mock_java_class(cls=self.TEST_CLS_1)
mock_class_node_2 = create_mock_java_class(cls=self.TEST_CLS_2)
mock_class_node_3 = create_mock_java_class(cls=self.TEST_CLS_3)
# Add the class dependencies (1 -> 2), (1 -> 2) (duplicate), (1 -> 3)
start_node.add_class_dependency_edge(end_node, mock_class_node_1,
mock_class_node_2)
start_node.add_class_dependency_edge(end_node, mock_class_node_1,
mock_class_node_2)
start_node.add_class_dependency_edge(end_node, mock_class_node_1,
mock_class_node_3)
# Expected output: the two deduped dependencies (1 -> 2), (1 -> 3)
# making up the edge (start_node, end_node).
deps = start_node.get_class_dependencies_in_outbound_edge(end_node)
self.assertEqual(len(deps), 2)
self.assertEqual(
deps, {(mock_class_node_1, mock_class_node_2),
(mock_class_node_1, mock_class_node_3)})
def test_get_class_dependencies_in_outbound_edge_not_outbound(self):
"""Tests getting dependencies for a non-outbound edge."""
test_node_1 = package_dependency.JavaPackage(self.TEST_PKG_1)
test_node_2 = package_dependency.JavaPackage(self.TEST_PKG_2)
# Expected output: an empty set, since there are no class dependencies
# comprising a package dependency edge that doesn't exist.
deps = test_node_1.get_class_dependencies_in_outbound_edge(test_node_2)
self.assertEqual(deps, set())
class TestJavaPackageDependencyGraph(unittest.TestCase):
"""Unit tests for JavaPackageDependencyGraph.
Full name: dependency_analysis.class_dependency.JavaPackageDependencyGraph.
"""
TEST_PKG_1 = 'package1'
TEST_PKG_2 = 'package2'
TEST_CLS = 'class'
def test_initialization(self):
"""Tests that initialization collapses a class dependency graph."""
# Create three class nodes (1, 2, 3) in two packages: [1, 2] and [3].
mock_class_node_1 = create_mock_java_class(pkg=self.TEST_PKG_1)
mock_class_node_2 = create_mock_java_class(pkg=self.TEST_PKG_1)
mock_class_node_3 = create_mock_java_class(pkg=self.TEST_PKG_2)
# Create dependencies (1 -> 3) and (3 -> 2).
mock_class_graph = unittest.mock.Mock()
mock_class_graph.nodes = [
mock_class_node_1, mock_class_node_2, mock_class_node_3
]
mock_class_graph.edges = [(mock_class_node_1, mock_class_node_3),
(mock_class_node_3, mock_class_node_2)]
test_graph = package_dependency.JavaPackageDependencyGraph(
mock_class_graph)
# Expected output: two-node package graph with a bidirectional edge.
self.assertEqual(test_graph.num_nodes, 2)
self.assertEqual(test_graph.num_edges, 2)
self.assertIsNotNone(test_graph.get_node_by_key(self.TEST_PKG_1))
self.assertIsNotNone(test_graph.get_node_by_key(self.TEST_PKG_2))
# Ensure there is a bidirectional edge.
(edge_1_start, edge_1_end), (edge_2_start,
edge_2_end) = test_graph.edges
self.assertEqual(edge_1_start, edge_2_end)
self.assertEqual(edge_2_start, edge_1_end)
def test_initialization_no_dependencies(self):
"""Tests that a package with no external dependencies is included."""
# Create one class node (1) in one package: [1].
mock_class_node = create_mock_java_class(pkg=self.TEST_PKG_1)
# Do not create any dependencies.
mock_class_graph = unittest.mock.Mock()
mock_class_graph.nodes = [mock_class_node]
mock_class_graph.edges = []
test_graph = package_dependency.JavaPackageDependencyGraph(
mock_class_graph)
# Expected output: one-node package graph with no edges.
self.assertEqual(test_graph.num_nodes, 1)
self.assertEqual(test_graph.num_edges, 0)
self.assertIsNotNone(test_graph.get_node_by_key(self.TEST_PKG_1))
def test_initialization_internal_dependencies(self):
"""Tests that a package with only internal dependencies is included."""
# Create two class nodes (1, 2) in one package: [1, 2].
mock_class_node_1 = create_mock_java_class(pkg=self.TEST_PKG_1)
mock_class_node_2 = create_mock_java_class(pkg=self.TEST_PKG_1)
# Create a dependency (1 -> 2).
mock_class_graph = unittest.mock.Mock()
mock_class_graph.nodes = [mock_class_node_1, mock_class_node_2]
mock_class_graph.edges = [(mock_class_node_1, mock_class_node_2)]
test_graph = package_dependency.JavaPackageDependencyGraph(
mock_class_graph)
# Expected output: one-node package graph with a self-edge.
self.assertEqual(test_graph.num_nodes, 1)
self.assertEqual(test_graph.num_edges, 1)
self.assertIsNotNone(test_graph.get_node_by_key(self.TEST_PKG_1))
# Ensure there is a self-edge.
[(edge_start, edge_end)] = test_graph.edges
self.assertEqual(edge_start, edge_end)
def test_create_node_from_key(self):
"""Tests that a JavaPackage is correctly generated."""
mock_class_graph = unittest.mock.Mock()
mock_class_graph.nodes = []
mock_class_graph.edges = []
test_graph = package_dependency.JavaPackageDependencyGraph(
mock_class_graph)
created_node = test_graph.create_node_from_key(self.TEST_PKG_1)
self.assertEqual(created_node.name, self.TEST_PKG_1)
if __name__ == '__main__':
unittest.main()
...@@ -8,6 +8,7 @@ import pathlib ...@@ -8,6 +8,7 @@ import pathlib
import subprocess import subprocess
import class_dependency import class_dependency
import package_dependency
SRC_PATH = pathlib.Path(__file__).resolve().parents[3] # src/ SRC_PATH = pathlib.Path(__file__).resolve().parents[3] # src/
JDEPS_PATH = SRC_PATH.joinpath('third_party/jdk/current/bin/jdeps') JDEPS_PATH = SRC_PATH.joinpath('third_party/jdk/current/bin/jdeps')
...@@ -21,12 +22,7 @@ def class_is_interesting(name: str): ...@@ -21,12 +22,7 @@ def class_is_interesting(name: str):
class JavaClassJdepsParser(object): # pylint: disable=useless-object-inheritance class JavaClassJdepsParser(object): # pylint: disable=useless-object-inheritance
"""A parser for jdeps class-level dependency output. """A parser for jdeps class-level dependency output."""
Attributes:
graph: The dependency graph of the jdeps output. Initialized as empty
and updated using parse_raw_jdeps_output.
"""
def __init__(self): # pylint: disable=missing-function-docstring def __init__(self): # pylint: disable=missing-function-docstring
self._graph = class_dependency.JavaClassDependencyGraph() self._graph = class_dependency.JavaClassDependencyGraph()
...@@ -102,10 +98,18 @@ def main(): ...@@ -102,10 +98,18 @@ def main():
raw_jdeps_output = run_jdeps(arguments.filepath) raw_jdeps_output = run_jdeps(arguments.filepath)
jdeps_parser = JavaClassJdepsParser() jdeps_parser = JavaClassJdepsParser()
jdeps_parser.parse_raw_jdeps_output(raw_jdeps_output) jdeps_parser.parse_raw_jdeps_output(raw_jdeps_output)
graph = jdeps_parser.graph
print(f'Parsed graph, got {graph.num_nodes} nodes ' class_graph = jdeps_parser.graph
f'and {graph.num_edges} edges.')
print(f"Parsed class-level dependency graph, "
f"got {class_graph.num_nodes} nodes "
f"and {class_graph.num_edges} edges.")
package_graph = package_dependency.JavaPackageDependencyGraph(class_graph)
print(f"Created package-level dependency graph, "
f"got {package_graph.num_nodes} nodes "
f"and {package_graph.num_edges} edges.")
if __name__ == '__main__': if __name__ == '__main__':
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment