diff --git a/.github/actions/build/action.yaml b/.github/actions/build/action.yaml index ee615729983..e71e4b9a5a8 100644 --- a/.github/actions/build/action.yaml +++ b/.github/actions/build/action.yaml @@ -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: @@ -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 }} @@ -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 }} diff --git a/.github/config/linux.json b/.github/config/linux.json index dfacac9eadd..059984dca42 100644 --- a/.github/config/linux.json +++ b/.github/config/linux.json @@ -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": [ { diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 45e7ef8e073..99385abccf9 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -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 }} diff --git a/cobalt/build/test_targets.py b/cobalt/build/test_targets.py new file mode 100644 index 00000000000..583edd2fca1 --- /dev/null +++ b/cobalt/build/test_targets.py @@ -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) diff --git a/cobalt/build/test_targets_test.py b/cobalt/build/test_targets_test.py new file mode 100644 index 00000000000..698b9597838 --- /dev/null +++ b/cobalt/build/test_targets_test.py @@ -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()