blob: f49dc460d27ad6cbf35a749c1ddde01936a8bf7b [file] [log] [blame]
#!/usr/bin/env python
import argparse
from datetime import datetime
import os
from pathlib import Path
import shutil
import subprocess
import sys
import xml.etree.ElementTree as ET
JAVA_UNIT_TESTS = 'test/mts/tools/mts-tradefed/res/config/mts-bluetooth-tests-list-shard-01.xml'
NATIVE_UNIT_TESTS = 'test/mts/tools/mts-tradefed/res/config/mts-bluetooth-tests-list-shard-02.xml'
DO_NOT_RETRY_TESTS = {
'CtsBluetoothTestCases',
'GoogleBluetoothInstrumentationTests',
}
MAX_TRIES = 3
def run_pts_bot(logs_out):
run_pts_bot_cmd = [
# atest command with verbose mode.
'atest',
'-d',
'-v',
'pts-bot',
# Coverage tool chains and specify that coverage should be flush to the
# disk between each tests.
'--',
'--coverage',
'--coverage-toolchain JACOCO',
'--coverage-toolchain CLANG',
'--coverage-flush',
]
with open(f'{logs_out}/pts_bot.txt', 'w') as f:
subprocess.run(run_pts_bot_cmd, stdout=f, stderr=subprocess.STDOUT)
def list_unit_tests():
android_build_top = os.getenv('ANDROID_BUILD_TOP')
unit_tests = []
java_unit_xml = ET.parse(f'{android_build_top}/{JAVA_UNIT_TESTS}')
for child in java_unit_xml.getroot():
value = child.attrib['value']
if 'enable:true' in value:
test = value.replace(':enable:true', '')
unit_tests.append(test)
native_unit_xml = ET.parse(f'{android_build_top}/{NATIVE_UNIT_TESTS}')
for child in native_unit_xml.getroot():
value = child.attrib['value']
if 'enable:true' in value:
test = value.replace(':enable:true', '')
unit_tests.append(test)
return unit_tests
def run_unit_test(test, logs_out):
print(f'Test started: {test}')
# Env variables necessary for native unit tests.
env = os.environ.copy()
env['CLANG_COVERAGE_CONTINUOUS_MODE'] = 'true'
env['CLANG_COVERAGE'] = 'true'
env['NATIVE_COVERAGE_PATHS'] = 'packages/modules/Bluetooth'
run_test_cmd = [
# atest command with verbose mode.
'atest',
'-d',
'-v',
test,
# Coverage tool chains and specify that coverage should be flush to the
# disk between each tests.
'--',
'--coverage',
'--coverage-toolchain JACOCO',
'--coverage-toolchain CLANG',
'--coverage-flush',
# Allows tests to use hidden APIs.
'--test-arg ',
'com.android.compatibility.testtype.LibcoreTest:hidden-api-checks:false',
'--test-arg ',
'com.android.tradefed.testtype.AndroidJUnitTest:hidden-api-checks:false',
'--test-arg ',
'com.android.tradefed.testtype.InstrumentationTest:hidden-api-checks:false',
'--skip-system-status-check ',
'com.android.tradefed.suite.checker.ShellStatusChecker',
]
try_count = 1
while (try_count == 1 or test not in DO_NOT_RETRY_TESTS) and try_count <= MAX_TRIES:
with open(f'{logs_out}/{test}_{try_count}.txt', 'w') as f:
if try_count > 1: print(f'Retrying {test}: count = {try_count}')
returncode = subprocess.run(
run_test_cmd, env=env, stdout=f, stderr=subprocess.STDOUT).returncode
if returncode == 0: break
try_count += 1
print(
f'Test ended [{"Success" if returncode == 0 else "Failed"}]: {test}')
def pull_and_rename_trace_for_test(test, trace):
date = datetime.now().strftime("%Y%m%d")
temp_trace = Path('temp_trace')
subprocess.run(['adb', 'pull', '/data/misc/trace', temp_trace])
for child in temp_trace.iterdir():
child = child.rename(f'{child.parent}/{date}_{test}_{child.name}')
shutil.copy(child, trace)
shutil.rmtree(temp_trace, ignore_errors=True)
def generate_java_coverage(bt_apex_name, trace_path, coverage_out):
out = os.getenv('OUT')
android_host_out = os.getenv('ANDROID_HOST_OUT')
java_coverage_out = Path(f'{coverage_out}/java')
temp_path = Path(f'{coverage_out}/temp')
if temp_path.exists():
shutil.rmtree(temp_path, ignore_errors=True)
temp_path.mkdir()
framework_jar_path = Path(
f'{out}/obj/PACKAGING/jacoco_intermediates/JAVA_LIBRARIES/framework-bluetooth.{bt_apex_name}_intermediates'
)
service_jar_path = Path(
f'{out}/obj/PACKAGING/jacoco_intermediates/JAVA_LIBRARIES/service-bluetooth.{bt_apex_name}_intermediates'
)
app_jar_path = Path(
f'{out}/obj/PACKAGING/jacoco_intermediates/ETC/Bluetooth{"Google" if "com.google" in bt_apex_name else ""}.{bt_apex_name}_intermediates'
)
# From google3/configs/wireless/android/testing/atp/prod/mainline-engprod/templates/modules/bluetooth.gcl.
framework_exclude_classes = [
# Exclude statically linked & jarjar'ed classes.
'**/com/android/bluetooth/x/**/*.class',
# Exclude AIDL generated interfaces.
'**/android/bluetooth/I*$Default.class',
'**/android/bluetooth/**/I*$Default.class',
'**/android/bluetooth/I*$Stub.class',
'**/android/bluetooth/**/I*$Stub.class',
'**/android/bluetooth/I*$Stub$Proxy.class',
'**/android/bluetooth/**/I*$Stub$Proxy.class',
# Exclude annotations.
'**/android/bluetooth/annotation/**/*.class',
]
service_exclude_classes = [
# Exclude statically linked & jarjar'ed classes.
'**/android/support/**/*.class',
'**/androidx/**/*.class',
'**/com/android/bluetooth/x/**/*.class',
'**/com/android/internal/**/*.class',
'**/com/google/**/*.class',
'**/kotlin/**/*.class',
'**/kotlinx/**/*.class',
'**/org/**/*.class',
]
app_exclude_classes = [
# Exclude statically linked & jarjar'ed classes.
'**/android/hardware/**/*.class',
'**/android/hidl/**/*.class',
'**/android/net/**/*.class',
'**/android/support/**/*.class',
'**/androidx/**/*.class',
'**/com/android/bluetooth/x/**/*.class',
'**/com/android/internal/**/*.class',
'**/com/android/obex/**/*.class',
'**/com/android/vcard/**/*.class',
'**/com/google/**/*.class',
'**/kotlin/**/*.class',
'**/kotlinx/**/*.class',
'**/javax/**/*.class',
'**/org/**/*.class',
# Exclude SIM Access Profile (SAP) which is being deprecated.
'**/com/android/bluetooth/sap/*.class',
# Added for local runs.
'**/com/android/bluetooth/**/BluetoothMetrics*.class',
'**/com/android/bluetooth/**/R*.class',
]
# Merged ec files.
merged_ec_path = Path(f'{temp_path}/merged.ec')
subprocess.run((
f'java -jar {android_host_out}/framework/jacoco-cli.jar merge {trace_path.absolute()}/*.ec '
f'--destfile {merged_ec_path.absolute()}'),
shell=True)
# Copy and extract jar files.
framework_temp_path = Path(f'{temp_path}/{framework_jar_path.name}')
service_temp_path = Path(f'{temp_path}/{service_jar_path.name}')
app_temp_path = Path(f'{temp_path}/{app_jar_path.name}')
shutil.copytree(framework_jar_path, framework_temp_path)
shutil.copytree(service_jar_path, service_temp_path)
shutil.copytree(app_jar_path, app_temp_path)
current_dir_path = Path.cwd()
for p in [framework_temp_path, service_temp_path, app_temp_path]:
os.chdir(p.absolute())
os.system('jar xf jacoco-report-classes.jar')
os.chdir(current_dir_path)
os.remove(f'{framework_temp_path}/jacoco-report-classes.jar')
os.remove(f'{service_temp_path}/jacoco-report-classes.jar')
os.remove(f'{app_temp_path}/jacoco-report-classes.jar')
# Generate coverage report.
exclude_classes = []
for glob in framework_exclude_classes:
exclude_classes.extend(list(framework_temp_path.glob(glob)))
for glob in service_exclude_classes:
exclude_classes.extend(list(service_temp_path.glob(glob)))
for glob in app_exclude_classes:
exclude_classes.extend(list(app_temp_path.glob(glob)))
for c in exclude_classes:
if c.exists():
os.remove(c.absolute())
gen_java_cov_report_cmd = [
f'java',
f'-jar',
f'{android_host_out}/framework/jacoco-cli.jar',
f'report',
f'{merged_ec_path.absolute()}',
f'--classfiles',
f'{temp_path.absolute()}',
f'--html',
f'{java_coverage_out.absolute()}',
f'--name',
f'{java_coverage_out.absolute()}.html',
]
subprocess.run(gen_java_cov_report_cmd)
# Cleanup.
shutil.rmtree(temp_path, ignore_errors=True)
def generate_native_coverage(bt_apex_name, trace_path, coverage_out):
out = os.getenv('OUT')
android_build_top = os.getenv('ANDROID_BUILD_TOP')
native_coverage_out = Path(f'{coverage_out}/native')
temp_path = Path(f'{coverage_out}/temp')
if temp_path.exists():
shutil.rmtree(temp_path, ignore_errors=True)
temp_path.mkdir()
# From google3/configs/wireless/android/testing/atp/prod/mainline-engprod/templates/modules/bluetooth.gcl.
exclude_files = {
'android/',
# Exclude AIDLs definition and generated interfaces.
'system/.*_aidl.*',
'system/binder/',
# Exclude tests.
'system/.*_test.*',
'system/.*_mock.*',
'system/.*_unittest.*',
'system/blueberry/',
'system/test/',
# Exclude config and doc.
'system/build/',
'system/conf/',
'system/doc/',
# Exclude (currently) unused GD code.
'system/gd/att/',
'system/gd/l2cap/',
'system/gd/neighbor/',
'system/gd/rust/',
'system/gd/security/',
# Exclude legacy AVRCP implementation (to be removed, current AVRCP
# implementation is in packages/modules/Bluetooth/system/profile/avrcp)
'system/stack/avrc/',
# Exclude audio HIDL since AIDL is used instead today (in
# packages/modules/Bluetooth/system/audio_hal_interface/aidl)
'system/audio_hal_interface/hidl/',
}
# Merge profdata files.
profdata_path = Path(f'{temp_path}/coverage.profdata')
subprocess.run(
f'llvm-profdata merge --sparse -o {profdata_path.absolute()} {trace_path.absolute()}/*.profraw',
shell=True)
gen_native_cov_report_cmd = [
f'llvm-cov',
f'show',
f'-format=html',
f'-output-dir={native_coverage_out.absolute()}',
f'-instr-profile={profdata_path.absolute()}',
f'{out}/symbols/apex/{bt_apex_name}/lib64/libbluetooth_jni.so',
f'-path-equivalence=/proc/self/cwd,{android_build_top}',
f'/proc/self/cwd/packages/modules/Bluetooth',
]
for f in exclude_files:
gen_native_cov_report_cmd.append(f'-ignore-filename-regex={f}')
subprocess.run(gen_native_cov_report_cmd, cwd=android_build_top)
# Cleanup.
shutil.rmtree(temp_path, ignore_errors=True)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument(
'--apex-name',
default='com.android.btservices',
help='bluetooth apex name. Default: com.android.btservices')
parser.add_argument(
'--java', action='store_true', help='generate Java coverage')
parser.add_argument(
'--native', action='store_true', help='generate native coverage')
parser.add_argument(
'--out',
type=str,
default='out_coverage',
help='out directory for coverage reports. Default: ./out_coverage')
parser.add_argument(
'--trace',
type=str,
default='trace',
help='trace directory with .ec and .profraw files. Default: ./trace')
parser.add_argument(
'--full-report',
action='store_true',
help='run all tests and compute coverage report')
args = parser.parse_args()
coverage_out = Path(args.out)
shutil.rmtree(coverage_out, ignore_errors=True)
coverage_out.mkdir()
if not args.full_report:
trace_path = Path(args.trace)
if (not trace_path.exists() or not trace_path.is_dir()):
sys.exit('Trace directory does not exist')
if (args.java):
generate_java_coverage(args.apex_name, trace_path, coverage_out)
if (args.native):
generate_native_coverage(args.apex_name, trace_path, coverage_out)
else:
# Output logs directory
logs_out = Path('logs_bt_tests')
logs_out.mkdir(exist_ok=True)
# Compute Pandora tests coverage
coverage_out_pandora = Path(f'{coverage_out}/pandora')
coverage_out_pandora.mkdir()
trace_pandora = Path('trace_pandora')
shutil.rmtree(trace_pandora, ignore_errors=True)
trace_pandora.mkdir()
subprocess.run(['adb', 'shell', 'rm', '/data/misc/trace/*'])
run_pts_bot(logs_out)
pull_and_rename_trace_for_test('pts_bot', trace_pandora)
generate_java_coverage(args.apex_name, trace_pandora, coverage_out_pandora)
generate_native_coverage(args.apex_name, trace_pandora, coverage_out_pandora)
# Compute unit tests coverage
coverage_out_unit = Path(f'{coverage_out}/unit')
coverage_out_unit.mkdir()
trace_unit = Path('trace_unit')
shutil.rmtree(trace_unit, ignore_errors=True)
trace_unit.mkdir()
unit_tests = list_unit_tests()
for test in unit_tests:
subprocess.run(['adb', 'shell', 'rm', '/data/misc/trace/*'])
run_unit_test(test, logs_out)
pull_and_rename_trace_for_test(test, trace_unit)
generate_java_coverage(args.apex_name, trace_unit, coverage_out_unit)
generate_native_coverage(args.apex_name, trace_unit, coverage_out_unit)
# Compute all tests coverage
coverage_out_mainline = Path(f'{coverage_out}/mainline')
coverage_out_mainline.mkdir()
trace_mainline = Path('trace_mainline')
shutil.rmtree(trace_mainline, ignore_errors=True)
trace_mainline.mkdir()
for child in trace_pandora.iterdir():
shutil.copy(child, trace_mainline)
for child in trace_unit.iterdir():
shutil.copy(child, trace_mainline)
generate_java_coverage(args.apex_name, trace_mainline, coverage_out_mainline)
generate_native_coverage(args.apex_name, trace_mainline, coverage_out_mainline)