Skip to content

Commit

Permalink
Add script that calculates test targets based on gn deps
Browse files Browse the repository at this point in the history
b/382508397
  • Loading branch information
oxve committed Dec 26, 2024
1 parent b7b2d76 commit fab059c
Show file tree
Hide file tree
Showing 5 changed files with 305 additions and 13 deletions.
35 changes: 35 additions & 0 deletions .github/actions/build/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ inputs:
targets:
description: "List of ninja targets for Cobalt build."
required: true
test_on_host:
description: "Indicates if on-host test artifacts should be uploaded."
required: true
test_on_device:
description: "Indicates if on-device test artifacts should be uploaded."
required: true
test_artifacts_key:
description: "Artifact key used to store on-host test artifacts."
required: true
runs:
using: "composite"
steps:
Expand Down Expand Up @@ -38,6 +47,25 @@ runs:
cd src
gn args --list --short --overrides-only out/${{ matrix.platform }}_${{ matrix.config }}
shell: bash
- name: Calculate test targets
if: inputs.test_on_host == 'true' || inputs.test_on_device == 'true'
run: |
cd src
gn desc out/${{ matrix.platform }}_${{ matrix.config }}/ "*" --format=json > gn_desc.json
# TODO(oxv): Figure out how to upload test artifacts here, or share list with upload step.
# For now upload step will look for this file
vpython3 cobalt/build/test_targets.py --root_target=//cobalt:cobalt --gn_json=gn_desc.json >> test_targets.txt
echo "TEST_TARGETS=$(cat test_targets.txt)" >> $GITHUB_ENV
shell: bash
- name: Ninja build test targets
if: inputs.test_on_host == 'true' || inputs.test_on_device == 'true'
env:
TARGETS: ${{ inputs.targets }}
run: |
set -x
cd src
ninja -C out/${{ matrix.platform }}_${{ matrix.config }} $(echo "${TARGETS}" | tr -d '"')
shell: bash
- name: Ninja build
env:
TARGETS: ${{ inputs.targets }}
Expand All @@ -58,3 +86,10 @@ runs:
src/out/${{ matrix.platform }}_qa/apks/*.apk
src/out/${{ matrix.platform }}_qa/*_apk/*.apk
src/out/${{ matrix.platform }}_qa/gen/build_info.json
- name: Upload Test Artifacts
if: inputs.test_on_host == 'true' || inputs.test_on_device == 'true'
uses: ./src/.github/actions/upload_test_artifacts
with:
test_artifacts_key: ${{ inputs.test_artifacts_key }}
on_host: ${{ inputs.test_on_host }}
on_device: ${{ inputs.test_on_device }}
10 changes: 1 addition & 9 deletions .github/config/linux.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,8 @@
"linux-x64x11"
],
"targets": [
"base_unittests",
"cobalt:gn_all",
"content_shell",
"gin_unittests",
"gpu_unittests",
"ipc_tests",
"media_unittests",
"mojo_unittests",
"sql_unittests",
"url_unittests"
"content_shell"
],
"includes": [
{
Expand Down
4 changes: 0 additions & 4 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,6 @@ jobs:
if: ${{ ! (contains(matrix.platform, 'android') && matrix.config == 'debug') }}
with:
targets: ${{ needs.initialize.outputs.targets }}
- name: Upload Test Artifacts
if: matrix.config == 'devel'
uses: ./src/.github/actions/upload_test_artifacts
with:
test_artifacts_key: ${{ env.TEST_ARTIFACTS_KEY }}
on_host: ${{ needs.initialize.outputs.test_on_host }}
on_device: ${{ needs.initialize.outputs.test_on_device }}
Expand Down
110 changes: 110 additions & 0 deletions cobalt/build/test_targets.py
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)
159 changes: 159 additions & 0 deletions cobalt/build/test_targets_test.py
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()

0 comments on commit fab059c

Please sign in to comment.