|  | #!/usr/bin/env python3 | 
|  | # Copyright 2021 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. | 
|  | """Command-line tool for finding differences in dependency graphs.""" | 
|  |  | 
|  | import argparse | 
|  |  | 
|  | import itertools | 
|  | from typing import List, Set, Tuple | 
|  |  | 
|  | 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_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.') | 
|  | 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 __name__ == '__main__': | 
|  | main() |