Commit a2e696c2 authored by Henrique Nakashima's avatar Henrique Nakashima Committed by Commit Bot

[Lorenz] Class deps audit script accepts multiple classes

Often we want to audit dependencies on a set of classes and it is
tedious to run the script once for each class.

Also change the matching logic not to match partial class names, as
it gets in the way of, for example, ChromeTabbedActivity, because of
MultiInstanceChromeTabbedActivity.

Change-Id: Ib75b8bdd1361e446f61b7a9d979f0f3403882edf
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2391610
Commit-Queue: Henrique Nakashima <hnakashima@chromium.org>
Reviewed-by: default avatarMohamed Heikal <mheikal@chromium.org>
Cr-Commit-Position: refs/heads/master@{#804273}
parent 37668738
......@@ -38,32 +38,26 @@ required arguments:
```
#### Class Dependency Audit
Given a JSON dependency graph, output the class-level dependencies for a given
class.
```
usage: print_class_dependencies.py [-h] -f FILE -c CLASS_NAME
list of classes.
required arguments:
-f FILE, --file FILE Path to the JSON file containing the dependency graph.
See the README on how to generate this file.
-c CLASS_NAME, --class CLASS_NAME
Case-insensitive name of the class to print
dependencies for. Matches names of the form ...input,
for example `apphooks` matches
`org.chromium.browser.AppHooks`.
An example is given at the end of this page. To see the options:
```
tools/android/dependency_analysis/print_class_dependencies.py -h
```
#### Package Dependency Audit
Given a JSON dependency graph, output the package-level dependencies for a
given package and the class dependencies comprising those dependencies.
An example is given at the end of this page. To see the options:
```
usage: print_package_dependencies.py [-h] -f FILE -p PACKAGE
tools/android/dependency_analysis/print_package_dependencies.py -h
```
#### Package Cycle Counting
Given a JSON dependency graph, counts package cycles up to a given size.
required arguments:
-f FILE, --file FILE Path to the JSON file containing the dependency graph.
See the README on how to generate this file.
-p PACKAGE, --package PACKAGE
Case-insensitive name of the package to print
dependencies for. Matches names of the form ...input,
for example `browser` matches `org.chromium.browser`.
To see the options:
```
tools/android/dependency_analysis/count_cycles.py -h
```
## Example Usage
......@@ -72,20 +66,20 @@ and that Chromium has been built as per the instructions
[here](https://chromium.googlesource.com/chromium/src/+/master/docs/linux/build_instructions.md),
although the only things these assumptions affect are the file paths.
```
$ tools/android/dependency_analysis/generate_json_dependency_graph.py -C out/Debug -o ~/json_graph.txt
$ tools/android/dependency_analysis/generate_json_dependency_graph.py -C out/Debug -o ~/graph.json
>>> Running jdeps and parsing output...
>>> Parsed class-level dependency graph, got 3239 nodes and 19272 edges.
>>> Created package-level dependency graph, got 500 nodes and 4954 edges.
>>> Dumping JSON representation to ./json_graph.txt.
>>> Dumping JSON representation to ~/graph.json.
./print_class_dependencies.py --file ./json_graph.txt --class apphooks
tools/android/dependency_analysis/print_class_dependencies.py -f ~/graph.json -c AppHooks
>>> Printing class dependencies for org.chromium.chrome.browser.AppHooks:
>>> 35 inbound dependency(ies) for org.chromium.chrome.browser.AppHooks:
>>> org.chromium.chrome.browser.AppHooksImpl
>>> org.chromium.chrome.browser.ChromeActivity
>>> ...
./print_package_dependencies.py --file ./json_graph.txt --package chrome.browser
tools/android/dependency_analysis/print_package_dependencies.py -f ~/graph.json -p chrome.browser
>>> Printing package dependencies for org.chromium.chrome.browser:
>>> 121 inbound dependency(ies) for org.chromium.chrome.browser:
>>> org.chromium.chrome.browser.about_settings -> org.chromium.chrome.browser
......
......@@ -25,10 +25,10 @@ def print_class_dependencies_for_key(class_graph, key):
def main():
"""Prints class-level dependencies for an input class."""
"""Prints class-level dependencies for one or more input classes."""
arg_parser = argparse.ArgumentParser(
description='Given a JSON dependency graph, output '
'the class-level dependencies for a given class.')
'the class-level dependencies for a given list of classes.')
required_arg_group = arg_parser.add_argument_group('required arguments')
required_arg_group.add_argument(
'-f',
......@@ -38,30 +38,39 @@ def main():
'See the README on how to generate this file.')
required_arg_group.add_argument(
'-c',
'--class',
'--classes',
required=True,
dest='class_name',
help='Case-insensitive name of the class to print dependencies for. '
'Matches names of the form ...input, for example '
'`apphooks` matches `org.chromium.browser.AppHooks`.')
dest='class_names',
help='Case-sensitive name of the classes to print dependencies for. '
'Matches either the simple class name without package or the fully '
'qualified class name. For example, `AppHooks` matches '
'`org.chromium.browser.AppHooks`. Specify multiple classes with a '
'comma-separated list, for example '
'`ChromeActivity,ChromeTabbedActivity`')
arguments = arg_parser.parse_args()
class_graph = serialization.load_class_graph_from_file(arguments.file)
class_graph_keys = [node.name for node in class_graph.nodes]
valid_keys = print_dependencies_helper.get_valid_keys_matching_input(
class_graph_keys, arguments.class_name)
if len(valid_keys) == 0:
print(f'No class found by the name {arguments.class_name}.')
elif len(valid_keys) > 1:
print(
f'Multiple valid keys found for the name {arguments.class_name}, '
'please disambiguate between one of the following options:')
for valid_key in valid_keys:
print(f'\t{valid_key}')
else:
print(f'Printing class dependencies for {valid_keys[0]}:')
print_class_dependencies_for_key(class_graph, valid_keys[0])
class_names = arguments.class_names.split(',')
for i, class_name in enumerate(class_names):
valid_keys = print_dependencies_helper.get_valid_class_keys_matching(
class_graph_keys, class_name)
if i > 0:
print()
if len(valid_keys) == 0:
print(f'No class found by the name {class_name}.')
elif len(valid_keys) > 1:
print(f'Multiple valid keys found for the name {class_name}, '
'please disambiguate between one of the following options:')
for valid_key in valid_keys:
print(f'\t{valid_key}')
else:
print(f'Printing class dependencies for {valid_keys[0]}:')
print_class_dependencies_for_key(class_graph, valid_keys[0])
if __name__ == '__main__':
......
# Lint as: 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.
......@@ -6,12 +7,30 @@
from typing import List
def get_valid_keys_matching_input(all_keys: List, input_key: str) -> List[str]:
"""Return a list of valid keys into the graph's nodes based on an input.
def get_valid_package_keys_matching(all_keys: List,
input_key: str) -> List[str]:
"""Return a list of keys of graph nodes that match a package input.
For our use case (matching user input to graph nodes),
For our use case (matching user input to package nodes),
a valid key is one that ends with the input, case insensitive.
For example, 'apphooks' matches 'org.chromium.browser.AppHooks'.
"""
input_key_lower = input_key.lower()
return [key for key in all_keys if key.lower().endswith(input_key_lower)]
def get_valid_class_keys_matching(all_keys: List, input_key: str) -> List[str]:
"""Return a list of keys of graph nodes that match a class input.
For our use case (matching user input to class nodes),
a valid key is one that matches fully the input either fully qualified or
ignoring package, case sensitive.
For example, the inputs 'org.chromium.browser.AppHooks' and 'AppHooks'
match the node 'org.chromium.browser.AppHooks' but 'Hooks' does not.
"""
if '.' in input_key:
# Match full name with package only.
return [input_key] if input_key in all_keys else []
else:
# Match class name in any package.
return [key for key in all_keys if key.endswith(f'.{input_key}')]
......@@ -10,17 +10,56 @@ import print_dependencies_helper
class TestHelperFunctions(unittest.TestCase):
"""Unit tests for the helper functions in the module."""
def test_get_valid_keys_matching_input(self):
def test_package_multiple_matches(self):
"""Tests getting all valid keys for the given input."""
test_keys = ['o.c.another.test', 'o.c.test', 'o.c.testing']
valid_keys = print_dependencies_helper.get_valid_package_keys_matching(
test_keys, 'test')
self.assertEqual(valid_keys, ['o.c.another.test', 'o.c.test'])
def test_package_no_match(self):
"""Tests getting no valid keys when there is no matching key."""
test_keys = ['o.c.another.test', 'o.c.test', 'o.c.testing']
valid_keys = print_dependencies_helper.get_valid_package_keys_matching(
test_keys, 'nomatch')
self.assertEqual(valid_keys, [])
def test_class_multiple_matches(self):
"""Tests getting multiple valid keys that match the given input."""
test_keys = ['o.c.test.Test', 'o.c.testing.Test', 'o.c.test.Wrong']
valid_keys = print_dependencies_helper.get_valid_class_keys_matching(
test_keys, 'Test')
self.assertEqual(valid_keys, ['o.c.test.Test', 'o.c.testing.Test'])
def test_class_full_match(self):
"""Tests getting a valid key when there is an exact fully-qualified
match."""
test_keys = ['o.c.test.Test', 'o.c.testing.Test', 'o.c.test.Wrong']
valid_keys = print_dependencies_helper.get_valid_keys_matching_input(
valid_keys = print_dependencies_helper.get_valid_class_keys_matching(
test_keys, 'o.c.test.Test')
self.assertEqual(valid_keys, ['o.c.test.Test'])
def test_class_no_match_lower_case(self):
"""Tests getting no valid keys when there would only be a match if the
input was case-insensitive."""
test_keys = ['o.c.test.Test', 'o.c.testing.Test', 'o.c.test.Wrong']
valid_keys = print_dependencies_helper.get_valid_class_keys_matching(
test_keys, 'test')
self.assertEqual(valid_keys,
sorted(['o.c.test.Test', 'o.c.testing.Test']))
self.assertEqual(valid_keys, [])
def test_get_valid_keys_matching_input_no_match(self):
"""Tests getting all valid keys when there is no matching key."""
def test_class_no_match_partial(self):
"""Tests getting no valid keys when the match is only partial in the
class name."""
test_keys = ['o.c.test.Test', 'o.c.testing.Test', 'o.c.test.Wrong']
valid_keys = print_dependencies_helper.get_valid_keys_matching_input(
test_keys, 'nomatch')
valid_keys = print_dependencies_helper.get_valid_class_keys_matching(
test_keys, 'est')
self.assertEqual(valid_keys, [])
def test_class_no_match_partial_qualified(self):
"""Tests getting no valid keys when the match is only partial in the
fully qualified name."""
test_keys = ['o.c.test.Test', 'o.c.testing.Test', 'o.c.test.Wrong']
valid_keys = print_dependencies_helper.get_valid_class_keys_matching(
test_keys, '.test.Test')
self.assertEqual(valid_keys, [])
......@@ -84,7 +84,7 @@ def main():
_, package_graph = serialization.load_class_and_package_graphs_from_file(
arguments.file)
package_graph_keys = [node.name for node in package_graph.nodes]
valid_keys = print_dependencies_helper.get_valid_keys_matching_input(
valid_keys = print_dependencies_helper.get_valid_package_keys_matching(
package_graph_keys, arguments.package)
if len(valid_keys) == 0:
......
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