| #!/usr/bin/env python3 |
| # Copyright 2021 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Command-line tool for finding differences in dependency graphs.""" |
| |
| import argparse |
| |
| import itertools |
| from typing import List, Set, Tuple, Callable |
| |
| import chrome_names |
| import count_cycles |
| import graph |
| import serialization |
| |
| |
| def _print_diff_num_nodes(graph1: graph.Graph, graph2: graph.Graph, |
| label: str): |
| before: int = graph1.num_nodes |
| after: int = graph2.num_nodes |
| _print_diff_metric(before, after, label) |
| |
| |
| def _print_diff_num_edges(graph1: graph.Graph, graph2: graph.Graph, |
| label: str): |
| before: int = graph1.num_edges |
| after: int = graph2.num_edges |
| _print_diff_metric(before, after, label) |
| |
| |
| def _print_diff_metric(before: int, after: int, label: str): |
| diff: int = after - before |
| print(f'{label}: {diff:+} ({before} -> {after})') |
| |
| |
| def _print_diff_node_list(graph1: graph.Graph, graph2: graph.Graph, |
| label: str): |
| before_nodes: Set[str] = set(node.name for node in graph1.nodes) |
| after_nodes: Set[str] = set(node.name for node in graph2.nodes) |
| _print_set_diff(before_nodes, after_nodes, label) |
| |
| |
| def _print_diff_node_list_filtered(graph1: graph.Graph, graph2: graph.Graph, |
| label: str, |
| filter_fn: Callable[[graph.Node], bool]): |
| |
| before_nodes: Set[str] = set(node.name for node in graph1.nodes |
| if filter_fn(node)) |
| after_nodes: Set[str] = set(node.name for node in graph2.nodes |
| if filter_fn(node)) |
| _print_diff_metric(len(before_nodes), len(after_nodes), label) |
| _print_set_diff(before_nodes, after_nodes, label) |
| |
| |
| def _print_diff_edge_list(graph1: graph.Graph, graph2: graph.Graph, |
| label: str): |
| before_edges: Set[str] = set(_edge_str(edge) for edge in graph1.edges) |
| after_edges: Set[str] = set(_edge_str(edge) for edge in graph2.edges) |
| _print_set_diff(before_edges, after_edges, label) |
| |
| |
| def _edge_str(edge: Tuple[graph.Node, graph.Node]) -> str: |
| return f'{edge[0]} -> {edge[1]}' |
| |
| |
| def _print_diff_cycle_list(cycles1: Set[count_cycles.Cycle], |
| cycles2: Set[count_cycles.Cycle], label: str): |
| before_cycles: Set[str] = set(_cycle_str(cycle) for cycle in cycles1) |
| after_cycles: Set[str] = set(_cycle_str(cycle) for cycle in cycles2) |
| _print_set_diff(before_cycles, after_cycles, label) |
| |
| |
| def _cycle_str(cycle: count_cycles.Cycle) -> str: |
| return ' > '.join(chrome_names.shorten_class(node.name) for node in cycle) |
| |
| |
| def _print_set_diff(before_set: Set[str], after_set: Set[str], label: str): |
| all_added: List[str] = sorted(after_set - before_set) |
| all_removed: List[str] = sorted(before_set - after_set) |
| if not all_added and not all_removed: |
| print(f'{label} - no changes') |
| return |
| |
| print(f'{label} added (+) and removed (-):') |
| for i, added in enumerate(all_added, start=1): |
| print(f'+ [{i:4}] {added}') |
| for i, removed in enumerate(all_removed, start=1): |
| print(f'- [{i:4}] {removed}') |
| |
| |
| def _cycle_set(graph: graph.Graph, |
| max_cycle_size: int) -> Set[count_cycles.Cycle]: |
| all_cycles_by_size = count_cycles.find_cycles(graph, max_cycle_size) |
| return set(itertools.chain(*all_cycles_by_size)) |
| |
| |
| def main(): |
| arg_parser = argparse.ArgumentParser( |
| description='Given two JSON dependency graphs, output the differences ' |
| 'between them. By default, outputs the differences in the sets of ' |
| 'class and package nodes.') |
| required_arg_group = arg_parser.add_argument_group('required arguments') |
| required_arg_group.add_argument( |
| '-b', |
| '--before', |
| required=True, |
| help='Path to the JSON file containing the "before" dependency graph. ' |
| 'See the README on how to generate this file.') |
| required_arg_group.add_argument( |
| '-a', |
| '--after', |
| required=True, |
| help='Path to the JSON file containing the "after" dependency graph.') |
| arg_parser.add_argument('-e', |
| '--edges', |
| action='store_true', |
| help='Also diff the set of graph edges.') |
| arg_parser.add_argument( |
| '--package-cycles', |
| type=int, |
| help='Also diff the set of package cycles up to the specified size.') |
| arg_parser.add_argument( |
| '--build-target', |
| type=str, |
| help='Also diff the set of class nodes in the given build target, e.g. ' |
| '"//chrome/android:chrome_java"') |
| arguments = arg_parser.parse_args() |
| |
| class_graph_before, package_graph_before, _ = \ |
| serialization.load_class_and_package_graphs_from_file(arguments.before) |
| class_graph_after, package_graph_after, _ = \ |
| serialization.load_class_and_package_graphs_from_file(arguments.after) |
| _print_diff_num_nodes(class_graph_before, class_graph_after, |
| 'Total Java class count') |
| _print_diff_num_nodes(package_graph_before, package_graph_after, |
| 'Total Java package count') |
| |
| print() |
| _print_diff_node_list(class_graph_before, class_graph_after, |
| 'Java classes') |
| print() |
| _print_diff_node_list(package_graph_before, package_graph_after, |
| 'Java packages') |
| |
| if arguments.edges: |
| print() |
| _print_diff_num_edges(class_graph_before, class_graph_after, |
| 'Total Java class edge count') |
| _print_diff_num_edges(package_graph_before, package_graph_after, |
| 'Total Java package edge count') |
| |
| print() |
| _print_diff_edge_list(class_graph_before, class_graph_after, |
| 'Java class edges') |
| print() |
| _print_diff_edge_list(package_graph_before, package_graph_after, |
| 'Java package edges') |
| |
| if arguments.package_cycles: |
| cycles_before = _cycle_set(package_graph_before, |
| arguments.package_cycles) |
| cycles_after = _cycle_set(package_graph_after, |
| arguments.package_cycles) |
| print() |
| _print_diff_metric( |
| len(cycles_before), len(cycles_after), |
| 'Total Java package cycle count (up to size ' |
| f'{arguments.package_cycles})') |
| |
| print() |
| _print_diff_cycle_list( |
| cycles_before, cycles_after, |
| f'Java package cycles (up to size {arguments.package_cycles})') |
| |
| if arguments.build_target: |
| |
| def is_in_build_target(node): |
| return arguments.build_target in node.build_targets |
| |
| print() |
| _print_diff_node_list_filtered( |
| class_graph_before, class_graph_after, |
| f'Java classes in {arguments.build_target}', is_in_build_target) |
| |
| |
| if __name__ == '__main__': |
| main() |