Snap for 11959661 from 5370e3ad9f95f67009da8ccef5acd876769288e5 to 24Q3-release
Change-Id: Id067d3cc9da647c6685bcafc244c422c7a983566
diff --git a/atest/arg_parser.py b/atest/arg_parser.py
index 8884d3f..c698f58 100644
--- a/atest/arg_parser.py
+++ b/atest/arg_parser.py
@@ -146,6 +146,17 @@
action='store_true',
help='Disable test teardown and cleanup.',
)
+
+ parser.add_argument(
+ '--code-under-test',
+ type=lambda value: set(value.split(',')),
+ help=(
+ 'Comma-separated list of modules whose sources should be included in'
+ ' the code coverage report. The dependencies of these modules are not'
+ ' included. For use with the --experimental-coverage flag.'
+ ),
+ )
+
parser.add_argument(
'--experimental-coverage',
action='store_true',
diff --git a/atest/atest_main.py b/atest/atest_main.py
index d0f994131..2eb5956 100755
--- a/atest/atest_main.py
+++ b/atest/atest_main.py
@@ -1168,6 +1168,7 @@
test_infos,
mod_info,
extra_args.get(constants.HOST, False),
+ args.code_under_test,
)
metrics.RunTestsFinishEvent(
diff --git a/atest/coverage/coverage.py b/atest/coverage/coverage.py
index e4e89cf..2aa4e7e 100644
--- a/atest/coverage/coverage.py
+++ b/atest/coverage/coverage.py
@@ -87,72 +87,93 @@
test_infos: List[test_info.TestInfo],
mod_info: module_info.ModuleInfo,
is_host_enabled: bool,
+ code_under_test: Set[str],
):
- """Generates HTML code coverage reports based on the test info."""
+ """Generates HTML code coverage reports based on the test info.
- soong_intermediates = atest_utils.get_build_out_dir('soong/.intermediates')
+ Args:
+ results_dir: The directory containing the test results
+ test_infos: The TestInfo objects for this invocation
+ mod_info: The ModuleInfo object containing all build module information
+ is_host_enabled: True if --host was specified
+ code_under_test: The set of modules to include in the coverage report
+ """
+ if not code_under_test:
+ # No code-under-test was specified on the command line. Deduce the values
+ # from module-info or from the test.
+ code_under_test = _deduce_code_under_test(test_infos, mod_info)
- # Collect dependency and source file information for the tests and any
- # Mainline modules.
- dep_modules = _get_test_deps(test_infos, mod_info)
- src_paths = _get_all_src_paths(dep_modules, mod_info)
+ logging.debug(f'Code-under-test: {code_under_test}')
- # Collect JaCoCo class jars from the build for coverage report generation.
- jacoco_report_jars = {}
- unstripped_native_binaries = set()
- for module in dep_modules:
- for path in mod_info.get_paths(module):
- module_dir = soong_intermediates.joinpath(path, module)
- # Check for uninstrumented Java class files to report coverage.
- classfiles = list(module_dir.rglob('jacoco-report-classes/*.jar'))
- if classfiles:
- jacoco_report_jars[module] = classfiles
-
- # Check for unstripped native binaries to report coverage.
- unstripped_native_binaries.update(_find_native_binaries(module_dir))
-
- # For host tests, use the test itself in the report generation.
- for test_info in test_infos:
- info = mod_info.get_module_info(test_info.raw_test_name)
- if not info or (not is_host_enabled and mod_info.requires_device(info)):
- continue
-
- installed = mod_info.get_installed_paths(test_info.raw_test_name)
- jars = [f for f in installed if f.suffix == '.jar']
- if jars:
- jacoco_report_jars[test_info.raw_test_name] = jars
- elif constants.MODULE_CLASS_NATIVE_TESTS in test_info.module_class:
- unstripped_native_binaries.update(installed)
+ # Collect coverage metadata files from the build for coverage report generation.
+ jacoco_report_jars = _collect_java_report_jars(
+ code_under_test, mod_info, is_host_enabled
+ )
+ unstripped_native_binaries = _collect_native_report_binaries(
+ code_under_test, mod_info, is_host_enabled
+ )
if jacoco_report_jars:
_generate_java_coverage_report(
- jacoco_report_jars, src_paths, results_dir, mod_info
+ jacoco_report_jars,
+ _get_all_src_paths(code_under_test, mod_info),
+ results_dir,
+ mod_info,
)
if unstripped_native_binaries:
_generate_native_coverage_report(unstripped_native_binaries, results_dir)
-def _get_test_deps(test_infos, mod_info):
+def _deduce_code_under_test(
+ test_infos: List[test_info.TestInfo],
+ mod_info: module_info.ModuleInfo,
+) -> Set[str]:
+ """Deduces the code-under-test from the test info and module info.
+ If the test info contains code-under-test information, that is used.
+ Otherwise, the dependencies of the test are used.
+
+ Args:
+ test_infos: The TestInfo objects for this invocation
+ mod_info: The ModuleInfo object containing all build module information
+
+ Returns:
+ The set of modules to include in the coverage report
+ """
+ code_under_test = set()
+
+ for test_info in test_infos:
+ code_under_test.update(
+ mod_info.get_code_under_test(test_info.raw_test_name)
+ )
+
+ if code_under_test:
+ return code_under_test
+
+ # No code-under-test was specified in ModuleInfo, default to using dependency
+ # information of the test.
+ for test_info in test_infos:
+ code_under_test.update(_get_test_deps(test_info, mod_info))
+
+ return code_under_test
+
+
+def _get_test_deps(test_info, mod_info):
"""Gets all dependencies of the TestInfo, including Mainline modules."""
deps = set()
- for info in test_infos:
- deps.add(info.raw_test_name)
+ deps.add(test_info.raw_test_name)
+ deps |= _get_transitive_module_deps(
+ mod_info.get_module_info(test_info.raw_test_name), mod_info, deps
+ )
+
+ # Include dependencies of any Mainline modules specified as well.
+ for mainline_module in test_info.mainline_modules:
+ deps.add(mainline_module)
deps |= _get_transitive_module_deps(
- mod_info.get_module_info(info.raw_test_name), mod_info, deps
+ mod_info.get_module_info(mainline_module), mod_info, deps
)
- # Include dependencies of any Mainline modules specified as well.
- if not info.mainline_modules:
- continue
-
- for mainline_module in info.mainline_modules:
- deps.add(mainline_module)
- deps |= _get_transitive_module_deps(
- mod_info.get_module_info(mainline_module), mod_info, deps
- )
-
return deps
@@ -189,6 +210,61 @@
return deps
+def _collect_java_report_jars(code_under_test, mod_info, is_host_enabled):
+ soong_intermediates = atest_utils.get_build_out_dir('soong/.intermediates')
+ report_jars = {}
+
+ for module in code_under_test:
+ for path in mod_info.get_paths(module):
+ if not path:
+ continue
+ module_dir = soong_intermediates.joinpath(path, module)
+ # Check for uninstrumented Java class files to report coverage.
+ classfiles = list(module_dir.rglob('jacoco-report-classes/*.jar'))
+ if classfiles:
+ report_jars[module] = classfiles
+
+ # Host tests use the test itself to generate the coverage report.
+ info = mod_info.get_module_info(module)
+ if not info:
+ continue
+ if is_host_enabled or not mod_info.requires_device(info):
+ installed = mod_info.get_installed_paths(module)
+ installed_jars = [str(f) for f in installed if f.suffix == '.jar']
+ if installed_jars:
+ report_jars[module] = installed_jars
+
+ return report_jars
+
+
+def _collect_native_report_binaries(code_under_test, mod_info, is_host_enabled):
+ soong_intermediates = atest_utils.get_build_out_dir('soong/.intermediates')
+ report_binaries = set()
+
+ for module in code_under_test:
+ for path in mod_info.get_paths(module):
+ if not path:
+ continue
+ module_dir = soong_intermediates.joinpath(path, module)
+ # Check for unstripped binaries to report coverage.
+ report_binaries.update(_find_native_binaries(module_dir))
+
+ # Host tests use the test itself to generate the coverage report.
+ info = mod_info.get_module_info(module)
+ if not info:
+ continue
+ if constants.MODULE_CLASS_NATIVE_TESTS not in info.get(
+ constants.MODULE_CLASS, []
+ ):
+ continue
+ if is_host_enabled or not mod_info.requires_device(info):
+ report_binaries.update(
+ str(f) for f in mod_info.get_installed_paths(module)
+ )
+
+ return report_binaries
+
+
def _find_native_binaries(module_dir):
files = module_dir.glob('*cov*/**/unstripped/*')
@@ -198,7 +274,7 @@
# Exclude .d and .d.raw files. These are Rust dependency files and are also
# stored in the unstripped directory.
return [
- file
+ str(file)
for file in files
if '.rsp' not in file.suffixes and '.d' not in file.suffixes
]
diff --git a/atest/coverage/coverage_unittest.py b/atest/coverage/coverage_unittest.py
new file mode 100755
index 0000000..f68b368
--- /dev/null
+++ b/atest/coverage/coverage_unittest.py
@@ -0,0 +1,335 @@
+#!/usr/bin/env python3
+#
+# Copyright 2024, The Android Open Source Project
+#
+# 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 coverage."""
+
+# pylint: disable=invalid-name
+
+from pathlib import PosixPath
+import unittest
+from unittest import mock
+from atest import atest_utils
+from atest import constants
+from atest import module_info
+from atest.coverage import coverage
+from atest.test_finders import test_info
+
+
+class DeduceCodeUnderTestUnittests(unittest.TestCase):
+ """Tests for _deduce_code_under_test."""
+
+ def test_code_under_test_is_defined_return_modules_in_code_under_test(self):
+ mod_info = create_module_info([
+ module(
+ name='test1',
+ dependencies=['dep1', 'dep2'],
+ code_under_test=['dep1'],
+ ),
+ module(name='dep1', dependencies=['dep1dep1', 'dep1dep2']),
+ module(name='dep1dep1'),
+ module(name='dep1dep2', dependencies=['dep1dep2dep']),
+ module(name='dep1dep2dep'),
+ module(name='dep2'),
+ ])
+
+ self.assertEqual(
+ coverage._deduce_code_under_test([create_test_info('test1')], mod_info),
+ {'dep1'},
+ )
+
+ def test_code_under_test_not_defined_return_all_modules_from_one_test(self):
+ mod_info = create_module_info([
+ module(name='test1', dependencies=['dep1', 'dep2']),
+ module(name='dep1', dependencies=['dep1dep1', 'dep1dep2']),
+ module(name='dep1dep1'),
+ module(name='dep1dep2', dependencies=['dep1dep2dep']),
+ module(name='dep1dep2dep'),
+ module(name='dep2'),
+ module(name='shouldnotappear'),
+ ])
+
+ self.assertEqual(
+ coverage._deduce_code_under_test([create_test_info('test1')], mod_info),
+ {
+ 'test1',
+ 'dep1',
+ 'dep2',
+ 'dep1dep1',
+ 'dep1dep2',
+ 'dep1dep2dep',
+ },
+ )
+
+ def test_code_under_test_not_defined_return_all_modules_from_all_tests(self):
+ mod_info = create_module_info([
+ module(name='test1', dependencies=['testlib', 'test1dep']),
+ module(name='test2', dependencies=['testlib', 'test2dep']),
+ module(name='testlib', dependencies=['testlibdep']),
+ module(name='testlibdep'),
+ module(name='test1dep'),
+ module(name='test2dep'),
+ module(name='shouldnotappear'),
+ ])
+
+ self.assertEqual(
+ coverage._deduce_code_under_test(
+ [create_test_info('test1'), create_test_info('test2')], mod_info
+ ),
+ {'test1', 'test2', 'testlib', 'testlibdep', 'test1dep', 'test2dep'},
+ )
+
+
+class CollectJavaReportJarsUnittests(unittest.TestCase):
+ """Test cases for _collect_java_report_jars."""
+
+ @mock.patch.object(
+ atest_utils,
+ 'get_build_out_dir',
+ return_value=PosixPath('/out/soong/.intermediates'),
+ )
+ @mock.patch.object(
+ PosixPath,
+ 'rglob',
+ return_value=[
+ '/out/soong/.intermediates/path/to/java_lib/variant-name/jacoco-report-classes/java_lib.jar'
+ ],
+ )
+ def test_java_lib(self, _rglob, _get_build_out_dir):
+ code_under_test = {'java_lib'}
+ mod_info = create_module_info([
+ module(name='java_lib', path='path/to'),
+ ])
+
+ self.assertEqual(
+ coverage._collect_java_report_jars(code_under_test, mod_info, False),
+ {
+ 'java_lib': [
+ '/out/soong/.intermediates/path/to/java_lib/variant-name/jacoco-report-classes/java_lib.jar'
+ ]
+ },
+ )
+
+ def test_host_test_includes_installed(self):
+ code_under_test = {'java_host_test'}
+ mod_info = create_module_info([
+ module(
+ name='java_host_test',
+ installed=[
+ '/path/to/out/host/java_host_test.jar',
+ '/path/to/out/host/java_host_test.config',
+ ],
+ ),
+ ])
+
+ self.assertEqual(
+ coverage._collect_java_report_jars(code_under_test, mod_info, True),
+ {'java_host_test': ['/path/to/out/host/java_host_test.jar']},
+ )
+
+
+class CollectNativeReportBinariesUnittests(unittest.TestCase):
+ """Test cases for _collect_native_report_binaries."""
+
+ @mock.patch.object(
+ atest_utils,
+ 'get_build_out_dir',
+ return_value=PosixPath('/out/soong/.intermediates'),
+ )
+ @mock.patch.object(PosixPath, 'glob')
+ def test_native_binary(self, _glob, _get_build_out_dir):
+ _glob.return_value = [
+ PosixPath(
+ '/out/soong/.intermediates/path/to/native_bin/variant-name-cov/unstripped/native_bin'
+ )
+ ]
+ code_under_test = {'native_bin'}
+ mod_info = create_module_info([
+ module(name='native_bin', path='path/to'),
+ ])
+
+ self.assertEqual(
+ coverage._collect_native_report_binaries(
+ code_under_test, mod_info, False
+ ),
+ {
+ '/out/soong/.intermediates/path/to/native_bin/variant-name-cov/unstripped/native_bin'
+ },
+ )
+
+ @mock.patch.object(
+ atest_utils,
+ 'get_build_out_dir',
+ return_value=PosixPath('/out/soong/.intermediates'),
+ )
+ @mock.patch.object(PosixPath, 'glob')
+ def test_skip_rsp_and_d_files(self, _glob, _get_build_out_dir):
+ _glob.return_value = [
+ PosixPath(
+ '/out/soong/.intermediates/path/to/native_bin/variant-name-cov/unstripped/native_bin'
+ ),
+ PosixPath(
+ '/out/soong/.intermediates/path/to/native_bin/variant-name-cov/unstripped/native_bin.rsp'
+ ),
+ PosixPath(
+ '/out/soong/.intermediates/path/to/native_bin/variant-name-cov/unstripped/native_bin.d'
+ ),
+ ]
+ code_under_test = {'native_bin'}
+ mod_info = create_module_info([
+ module(name='native_bin', path='path/to'),
+ ])
+
+ self.assertEqual(
+ coverage._collect_native_report_binaries(
+ code_under_test, mod_info, False
+ ),
+ {
+ '/out/soong/.intermediates/path/to/native_bin/variant-name-cov/unstripped/native_bin'
+ },
+ )
+
+ def test_host_test_includes_installed(self):
+ code_under_test = {'native_host_test'}
+ mod_info = create_module_info([
+ module(
+ name='native_host_test',
+ installed=['/out/host/nativetests/native_host_test'],
+ classes=[constants.MODULE_CLASS_NATIVE_TESTS],
+ ),
+ ])
+
+ self.assertEqual(
+ coverage._collect_native_report_binaries(
+ code_under_test, mod_info, True
+ ),
+ {'/out/host/nativetests/native_host_test'},
+ )
+
+
+class GenerateCoverageReportUnittests(unittest.TestCase):
+ """Tests for the code-under-test feature."""
+
+ @mock.patch.object(coverage, '_collect_java_report_jars', return_value={})
+ @mock.patch.object(
+ coverage, '_collect_native_report_binaries', return_value=set()
+ )
+ def test_generate_report_for_code_under_test_passed_in_from_atest(
+ self, _collect_native, _collect_java
+ ):
+ test_infos = [create_test_info('test')]
+ mod_info = create_module_info([
+ module(name='test', dependencies=['lib1', 'lib2']),
+ module(name='lib1'),
+ module(name='lib2', dependencies=['lib2dep']),
+ module(name='lib2dep'),
+ ])
+ code_under_test = ['lib1', 'lib2']
+
+ coverage.generate_coverage_report(
+ '/tmp/results_dir', test_infos, mod_info, True, code_under_test
+ )
+
+ _collect_java.assert_called_with(code_under_test, mod_info, True)
+ _collect_native.assert_called_with(code_under_test, mod_info, True)
+
+ @mock.patch.object(coverage, '_collect_java_report_jars', return_value={})
+ @mock.patch.object(
+ coverage, '_collect_native_report_binaries', return_value=set()
+ )
+ def test_generate_report_for_modules_get_from_deduce_code_under_test(
+ self, _collect_native, _collect_java
+ ):
+ test_infos = [create_test_info('test')]
+ mod_info = create_module_info([
+ module(name='test', dependencies=['lib1', 'lib2']),
+ module(name='lib1'),
+ module(name='lib2', dependencies=['lib2dep']),
+ module(name='lib2dep'),
+ module(name='not_a_dep'),
+ ])
+
+ coverage.generate_coverage_report(
+ '/tmp/results_dir', test_infos, mod_info, False, []
+ )
+
+ expected_code_under_test = {'test', 'lib1', 'lib2', 'lib2dep'}
+ _collect_java.assert_called_with(expected_code_under_test, mod_info, False)
+ _collect_native.assert_called_with(
+ expected_code_under_test, mod_info, False
+ )
+
+
+def create_module_info(modules=None):
+ """Wrapper function for creating module_info.ModuleInfo."""
+ name_to_module_info = {}
+ modules = modules or []
+
+ for m in modules:
+ name_to_module_info[m['module_name']] = m
+
+ return module_info.load_from_dict(name_to_module_info)
+
+
+# pylint: disable=too-many-arguments
+def module(
+ name=None,
+ path=None,
+ installed=None,
+ classes=None,
+ auto_test_config=None,
+ test_config=None,
+ shared_libs=None,
+ dependencies=None,
+ runtime_dependencies=None,
+ data=None,
+ data_dependencies=None,
+ compatibility_suites=None,
+ host_dependencies=None,
+ srcs=None,
+ supported_variants=None,
+ code_under_test=None,
+):
+ name = name or 'libhello'
+
+ m = {}
+
+ m['module_name'] = name
+ m['class'] = classes or []
+ m['path'] = [path or '']
+ m['installed'] = installed or []
+ m['is_unit_test'] = 'false'
+ m['auto_test_config'] = auto_test_config or []
+ m['test_config'] = test_config or []
+ m['shared_libs'] = shared_libs or []
+ m['runtime_dependencies'] = runtime_dependencies or []
+ m['dependencies'] = dependencies or []
+ m['data'] = data or []
+ m['data_dependencies'] = data_dependencies or []
+ m['compatibility_suites'] = compatibility_suites or []
+ m['host_dependencies'] = host_dependencies or []
+ m['srcs'] = srcs or []
+ m['supported_variants'] = supported_variants or []
+ m['code_under_test'] = code_under_test or []
+ return m
+
+
+def create_test_info(name='HelloWorldTest'):
+ """Helper function for creating test_info.TestInfo."""
+ return test_info.TestInfo(name, 'AtestTradefedRunner', set())
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/atest/module_info.py b/atest/module_info.py
index 2587dc2..d440e42 100644
--- a/atest/module_info.py
+++ b/atest/module_info.py
@@ -1166,6 +1166,19 @@
return [_to_abs_path(p) for p in mod_info.get('installed', [])]
+ def get_code_under_test(self, module_name: str) -> List[str]:
+ """Return code under test from module info."""
+ mod_info = self.get_module_info(module_name)
+ if not mod_info:
+ atest_utils.colorful_print(
+ '\nmodule %s cannot be found in module info, skip generating'
+ ' coverage for it.' % module_name,
+ constants.YELLOW,
+ )
+ return []
+
+ return mod_info.get('code_under_test', [])
+
def build_variants(self, info: Dict[str, Any]) -> List[str]:
return info.get(constants.MODULE_SUPPORTED_VARIANTS, [])
diff --git a/atest/module_info_unittest.py b/atest/module_info_unittest.py
index 5dc7d17..9f87244 100755
--- a/atest/module_info_unittest.py
+++ b/atest/module_info_unittest.py
@@ -779,6 +779,40 @@
[Path('/mocked/build_top/a/b/c/d')],
)
+ def test_get_code_under_test_module_name_is_not_found_in_module_info(self):
+ mod_info = create_module_info([
+ module(
+ name='my_module',
+ code_under_test='code_under_test_module',
+ )
+ ])
+
+ # module_that_is_not_in_module_info is not found in mod_info.
+ self.assertEqual(
+ mod_info.get_code_under_test('module_that_is_not_in_module_info'), [],
+ )
+
+ def test_get_code_under_test_code_under_test_is_not_defined_in_module_info(self):
+ mod_info = create_module_info([module(name='my_module')])
+
+ # my_module is found in mod_info but code_under_test is not defined.
+ self.assertEqual(
+ mod_info.get_code_under_test('my_module'), [],
+ )
+
+ def test_get_code_under_test_code_under_test_is_defined_in_module_info(self):
+ mod_info = create_module_info([
+ module(
+ name='my_module',
+ code_under_test='code_under_test_module',
+ )
+ ])
+
+ self.assertEqual(
+ mod_info.get_code_under_test('my_module'),
+ 'code_under_test_module',
+ )
+
class ModuleInfoTestFixture(fake_filesystem_unittest.TestCase):
"""Fixture for ModuleInfo tests."""
@@ -1262,6 +1296,7 @@
host_dependencies=None,
srcs=None,
supported_variants=None,
+ code_under_test=None,
):
name = name or 'libhello'
@@ -1283,6 +1318,7 @@
m['host_dependencies'] = host_dependencies or []
m['srcs'] = srcs or []
m['supported_variants'] = supported_variants or []
+ m['code_under_test'] = code_under_test or []
return m