-
Notifications
You must be signed in to change notification settings - Fork 128
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add script that calculates test targets based on gn deps
b/382508397
- Loading branch information
Showing
5 changed files
with
305 additions
and
13 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
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,110 @@ | ||
#!/usr/bin/env python3 | ||
# Copyright 2024 The Cobalt Authors. All Rights Reserved. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the 'License'); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an 'AS IS' BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
"""Calculates test targets based on a root package from GN dependencies.""" | ||
|
||
import argparse | ||
import json | ||
import os | ||
import typing | ||
|
||
import networkx as nx | ||
|
||
_UNRELATED_TARGETS_PACKAGES = {'components', 'third_party', 'ui'} | ||
|
||
|
||
def _is_in_same_root_package(node1, node2) -> bool: | ||
"""Checks if two nodes are in the same root package.""" | ||
# Remove the target name and split the path into its segments. | ||
node1_path = node1.split(':')[0].split('/') | ||
node2_path = node2.split(':')[0].split('/') | ||
common = os.path.commonprefix([node1_path, node2_path]) | ||
if len(common) == 2: | ||
# Only the initial // matches. | ||
return False | ||
if len(common) == 3: | ||
# The two nodes are adjacent in a path that contains unrelated components. | ||
return common[2] not in _UNRELATED_TARGETS_PACKAGES | ||
return True | ||
|
||
|
||
def _is_test_target(g, node) -> bool: | ||
# First check that the node is a testonly executable. | ||
if not (g.nodes[node]['is_testonly'] and g.nodes[node]['is_executable']): | ||
return False | ||
# Test targets get a runner target added to its deps. On linux the name of | ||
# this target is ${target_name}__runner, on android it's | ||
# ${target_name}__test_runner_script. | ||
linux_runner = f'{node}__runner' | ||
android_runner = f'{node}__test_runner_script' | ||
successors = set(g.successors(node)) | ||
return linux_runner in successors or android_runner in successors | ||
|
||
|
||
def _load_gn_data(filename) -> dict: | ||
"""Load the JSON file generated by gn desc.""" | ||
with open(filename, mode='r', encoding='utf-8') as f: | ||
return json.load(f) | ||
|
||
|
||
def _create_graph(data) -> nx.DiGraph: | ||
"""Creates a directed graph from 'gn desc' json output.""" | ||
g = nx.DiGraph() | ||
# Each key is a target and 'deps' is a list of dependencies | ||
for target_name, attributes in data.items(): | ||
# Only save the attributes we need to deduce which are the test targets. | ||
node_attributes = { | ||
'is_testonly': attributes.get('testonly', False), | ||
'is_executable': attributes.get('type') == 'executable' | ||
} | ||
g.add_node(target_name, **node_attributes) | ||
g.add_edges_from((target_name, dep) for dep in attributes['deps']) | ||
return g | ||
|
||
|
||
def get_relevant_test_targets(root_target, gn_json_path) -> typing.List[str]: | ||
"""Main logic.""" | ||
g = _create_graph(_load_gn_data(gn_json_path)) | ||
|
||
descendants = nx.descendants(g, root_target) | {root_target} | ||
all_test_targets = list( | ||
filter(lambda n: _is_test_target(g, n), | ||
nx.ancestors(g, '//testing/gtest:gtest'))) | ||
tests_to_run = [] | ||
for target in all_test_targets: | ||
for descendant in descendants: | ||
if _is_in_same_root_package(descendant, target) \ | ||
and nx.has_path(g, target, descendant): | ||
tests_to_run.append(target) | ||
# Break the inner loop, no need to add the test target more than once. | ||
break | ||
return tests_to_run | ||
|
||
|
||
if __name__ == '__main__': | ||
parser = argparse.ArgumentParser() | ||
parser.add_argument( | ||
'--gn_json', | ||
type=str, | ||
nargs='?', | ||
help='Path to the json output of \'gn desc *\'.') | ||
parser.add_argument( | ||
'--root_target', | ||
type=str, | ||
nargs='?', | ||
help='The root target for which to find test targets.') | ||
args = parser.parse_args() | ||
for test_target in sorted( | ||
get_relevant_test_targets(args.root_target, args.gn_json)): | ||
print(test_target) |
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,159 @@ | ||
#!/usr/bin/env python3 | ||
# Copyright 2024 The Cobalt Authors. All Rights Reserved. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the 'License'); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an 'AS IS' BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
"""Unit tests for test_targets.py""" | ||
import unittest | ||
|
||
import networkx as nx | ||
|
||
from unittest.mock import patch, mock_open | ||
from cobalt.build.test_targets import ( | ||
_UNRELATED_TARGETS_PACKAGES, | ||
_is_in_same_root_package, | ||
_is_test_target, | ||
_load_gn_data, | ||
_create_graph, | ||
get_relevant_test_targets, | ||
) | ||
|
||
|
||
class TestTestTargets(unittest.TestCase): | ||
"""Test class for the test target calculator.""" | ||
|
||
def test_same_root_package(self): | ||
self.assertTrue( | ||
_is_in_same_root_package('//foo:target', '//foo:test_target')) | ||
|
||
def test_different_packages_in_same_root_package(self): | ||
self.assertTrue( | ||
_is_in_same_root_package('//foo/bar:target', '//foo/qux:test_target')) | ||
|
||
def test_only_initial_slash_matches(self): | ||
self.assertFalse( | ||
_is_in_same_root_package('//foo:target', '//bar:test_target')) | ||
|
||
def test_adjacent_unrelated_components(self): | ||
for component in _UNRELATED_TARGETS_PACKAGES: | ||
with self.subTest(component): | ||
self.assertFalse( | ||
_is_in_same_root_package(f'//{component}/bar:target', | ||
f'//{component}/qux:test_target')) | ||
self.assertFalse( | ||
_is_in_same_root_package(f'//{component}/bar:target', | ||
f'//{component}/qux/bar:test_target')) | ||
|
||
def test_adjacent_related_targets(self): | ||
for component in _UNRELATED_TARGETS_PACKAGES: | ||
with self.subTest(component): | ||
self.assertTrue( | ||
_is_in_same_root_package(f'//{component}/bar:target', | ||
f'//{component}/bar:test_target')) | ||
self.assertTrue( | ||
_is_in_same_root_package(f'//{component}/bar:target', | ||
f'//{component}/bar/baz:test_target')) | ||
|
||
def test_adjacent_related_components(self): | ||
self.assertTrue( | ||
_is_in_same_root_package('//foo/bar/baz:target', | ||
'//foo/bar/qux:test_target')) | ||
|
||
def test_not_testonly(self): | ||
self.g = nx.DiGraph() | ||
self.g.add_node('//foo:bar', is_testonly=False, is_executable=True) | ||
self.assertFalse(_is_test_target(self.g, '//foo:bar')) | ||
|
||
def test_not_executable(self): | ||
self.g = nx.DiGraph() | ||
self.g.add_node('//foo:bar', is_testonly=True, is_executable=False) | ||
self.assertFalse(_is_test_target(self.g, '//foo:bar')) | ||
|
||
def test_no_runner_target(self): | ||
self.g = nx.DiGraph() | ||
self.g.add_node('//foo:bar', is_testonly=True, is_executable=True) | ||
self.assertFalse(_is_test_target(self.g, '//foo:bar')) | ||
|
||
def test_linux_runner_target(self): | ||
self.g = nx.DiGraph() | ||
self.g.add_node('//foo:bar', is_testonly=True, is_executable=True) | ||
self.g.add_edge('//foo:bar', '//foo:bar__runner') | ||
self.assertTrue(_is_test_target(self.g, '//foo:bar')) | ||
|
||
def test_android_runner_target(self): | ||
self.g = nx.DiGraph() | ||
self.g.add_node('//foo:bar', is_testonly=True, is_executable=True) | ||
self.g.add_edge('//foo:bar', '//foo:bar__test_runner_script') | ||
self.assertTrue(_is_test_target(self.g, '//foo:bar')) | ||
|
||
@patch( | ||
'builtins.open', | ||
new_callable=mock_open, | ||
read_data='{"//foo:bar": {"deps": []}}') | ||
def test_load_gn_data(self, _): | ||
data = _load_gn_data('gn_all.json') | ||
self.assertEqual(data, {'//foo:bar': {'deps': []}}) | ||
|
||
def test_create_graph(self): | ||
self.g = nx.DiGraph() | ||
data = { | ||
'//foo:bar': { | ||
'testonly': True, | ||
'type': 'executable', | ||
'deps': ['//baz:qux'] | ||
} | ||
} | ||
g = _create_graph(data) | ||
self.assertTrue(g.has_node('//foo:bar')) | ||
self.assertEqual(g.nodes['//foo:bar']['is_testonly'], True) | ||
self.assertEqual(g.nodes['//foo:bar']['is_executable'], True) | ||
self.assertTrue(g.has_edge('//foo:bar', '//baz:qux')) | ||
|
||
@patch('cobalt.build.test_targets._load_gn_data') | ||
def test_get_relevant_test_targets(self, mock_load_gn_data): | ||
mock_load_gn_data.return_value = { | ||
'//cobalt:cobalt': { | ||
'testonly': False, | ||
'type': 'executable', | ||
'deps': ['//foo:bar'] | ||
}, | ||
'//foo:bar': { | ||
'testonly': False, | ||
'type': 'static_library', | ||
'deps': ['//baz:qux'] | ||
}, | ||
'//foo:bar_tests': { | ||
'testonly': | ||
True, | ||
'type': | ||
'executable', | ||
'deps': [ | ||
'//foo:bar', '//foo:bar_tests__runner', '//testing/gtest:gtest' | ||
] | ||
}, | ||
'//foo:bar__runner': { | ||
'testonly': False, | ||
'type': 'executable', | ||
'deps': [] | ||
}, | ||
'//testing/gtest:gtest': { | ||
'testonly': False, | ||
'type': 'static_library', | ||
'deps': [] | ||
}, | ||
} | ||
tests_to_run = get_relevant_test_targets() | ||
self.assertEqual(tests_to_run, ['//foo:bar_tests']) | ||
|
||
|
||
if __name__ == '__main__': | ||
unittest.main() |