blob: f8ae02ca114df9cc6632d4dff213add33ad87226 [file] [log] [blame]
#!/usr/bin/env python3
#
# Copyright 2022, 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.
"""
ATest Integration Test Class.
The purpose is to prevent potential side-effects from breaking ATest at the
early stage while landing CLs with potential side-effects.
It forks a subprocess with ATest commands to validate if it can pass all the
finding, running logic of the python code, and waiting for TF to exit properly.
- When running with ROBOLECTRIC tests, it runs without TF, and will exit
the subprocess with the message "All tests passed"
- If FAIL, it means something breaks ATest unexpectedly!
"""
from __future__ import print_function
import os
import shutil
import subprocess
import sys
import tempfile
import time
import unittest
import zipfile
_TEST_RUN_DIR_PREFIX = 'atest_integration_tests_%s_'
_LOG_FILE = 'integration_tests.log'
_FAILED_LINE_LIMIT = 50
_EXIT_TEST_FAILED = 1
_EXIT_MISSING_ZIP = 2
class IntegrationConstants:
"""ATest Integration Class for constants definition."""
FAKE_SRC_ZIP = os.path.join(os.path.dirname(__file__),
'atest_integration_fake_src.zip')
FAKE_SRC_ROOT = ''
INTEGRATION_TESTS = [
os.path.join(os.path.dirname(__file__), 'INTEGRATION_TESTS')]
def __init__(self):
pass
class ATestIntegrationTest(unittest.TestCase):
"""ATest Integration Test Class."""
NAME = 'ATestIntegrationTest'
EXECUTABLE = os.path.join(os.path.dirname(__file__), 'atest-py3')
OPTIONS = ' -cy --no-bazel-mode '
EXTRA_ENV = {}
_RUN_CMD = '{exe} {options} {test}'
_PASSED_CRITERIA = ['will be rescheduled', 'All tests passed']
def setUp(self):
"""Set up stuff for testing."""
self.full_env_vars = os.environ.copy()
if self.EXTRA_ENV:
self.full_env_vars.update(self.EXTRA_ENV)
self.test_passed = False
self.log = []
def run_test(self, testcase):
"""Create a subprocess to execute the test command.
Strategy:
Fork a subprocess to wait for TF exit properly, and log the error
if the exit code isn't 0.
Args:
testcase: A string of testcase name.
"""
run_cmd_dict = {'exe': self.EXECUTABLE, 'options': self.OPTIONS,
'test': testcase}
run_command = self._RUN_CMD.format(**run_cmd_dict)
try:
subprocess.check_output(run_command,
cwd=self.full_env_vars['ANDROID_BUILD_TOP'],
stderr=subprocess.PIPE,
env=self.full_env_vars,
shell=True)
except subprocess.CalledProcessError as e:
self.log.append(e.output.decode())
return False
return True
def get_failed_log(self):
"""Get a trimmed failed log.
Strategy:
In order not to show the unnecessary log such as build log,
it's better to get a trimmed failed log that contains the
most important information.
Returns:
A trimmed failed log.
"""
failed_log = '\n'.join(filter(None, self.log[-_FAILED_LINE_LIMIT:]))
return failed_log
def create_test_method(testcase, log_path):
"""Create a test method according to the testcase.
Args:
testcase: A testcase name.
log_path: A file path for storing the test result.
Returns:
A created test method, and a test function name.
"""
test_function_name = 'test_%s' % testcase.replace(' ', '_')
# pylint: disable=missing-docstring
def template_test_method(self):
self.test_passed = self.run_test(testcase)
with open(log_path, 'a', encoding='utf-8') as log_file:
log_file.write('\n'.join(self.log))
failed_message = f'Running command: {testcase} failed.\n'
failed_message += '' if self.test_passed else self.get_failed_log()
self.assertTrue(self.test_passed, failed_message)
return test_function_name, template_test_method
def create_test_run_dir():
"""Create the test run directory in tmp.
Returns:
A string of the directory path.
"""
utc_epoch_time = int(time.time())
prefix = _TEST_RUN_DIR_PREFIX % utc_epoch_time
return tempfile.mkdtemp(prefix=prefix)
def init_test_env():
"""Initialize the environment to run the integration test."""
# Prepare test environment.
if not os.path.isfile(IntegrationConstants.FAKE_SRC_ZIP):
print(f'{IntegrationConstants.FAKE_SRC_ZIP} does not exist.')
sys.exit(_EXIT_MISSING_ZIP)
# Extract fake src tree and make soong_ui.bash as executable.
IntegrationConstants.FAKE_SRC_ROOT = tempfile.mkdtemp()
if os.path.exists(IntegrationConstants.FAKE_SRC_ROOT):
shutil.rmtree(IntegrationConstants.FAKE_SRC_ROOT)
os.mkdir(IntegrationConstants.FAKE_SRC_ROOT)
with zipfile.ZipFile(IntegrationConstants.FAKE_SRC_ZIP, 'r') as zip_ref:
print(f'Extract {IntegrationConstants.FAKE_SRC_ZIP} to '
f'{IntegrationConstants.FAKE_SRC_ROOT}')
zip_ref.extractall(IntegrationConstants.FAKE_SRC_ROOT)
IntegrationConstants.FAKE_SRC_ROOT = os.path.join(
IntegrationConstants.FAKE_SRC_ROOT, 'fake_android_src')
soong_ui = os.path.join(IntegrationConstants.FAKE_SRC_ROOT,
'build/soong/soong_ui.bash')
os.chmod(soong_ui, 0o755)
os.chdir(IntegrationConstants.FAKE_SRC_ROOT)
# Copy atest-py3
dst = os.path.join(IntegrationConstants.FAKE_SRC_ROOT, 'atest')
shutil.copyfile(ATestIntegrationTest.EXECUTABLE, dst)
os.chmod(dst, 0o755)
ATestIntegrationTest.EXECUTABLE = dst
# Setup env
ATestIntegrationTest.EXTRA_ENV[
'ANDROID_BUILD_TOP'] = IntegrationConstants.FAKE_SRC_ROOT
ATestIntegrationTest.EXTRA_ENV['OUT'] = os.path.join(
IntegrationConstants.FAKE_SRC_ROOT, 'out')
ATestIntegrationTest.EXTRA_ENV[
'ANDROID_HOST_OUT'] = os.path.join(
IntegrationConstants.FAKE_SRC_ROOT, 'out/host')
ATestIntegrationTest.EXTRA_ENV[
'ANDROID_PRODUCT_OUT'] = os.path.join(
IntegrationConstants.FAKE_SRC_ROOT, 'out/target/product/vsoc_x86_64')
ATestIntegrationTest.EXTRA_ENV[
'ANDROID_TARGET_OUT_TESTCASES'] = os.path.join(
IntegrationConstants.FAKE_SRC_ROOT,
'out/target/product/vsoc_x86_64/testcase')
ATestIntegrationTest.EXTRA_ENV['ANDROID_SERIAL'] = ''
if __name__ == '__main__':
# Init test
init_test_env()
print(f'Running tests with {ATestIntegrationTest.EXECUTABLE}\n')
RESULT = None
try:
LOG_PATH = os.path.join(create_test_run_dir(), _LOG_FILE)
for TEST_PLANS in IntegrationConstants.INTEGRATION_TESTS:
with open(TEST_PLANS, encoding='utf-8') as test_plans:
for test in test_plans:
# Skip test when the line startswith #.
if not test.strip() or test.strip().startswith('#'):
continue
test_func_name, test_func = create_test_method(
test.strip(), LOG_PATH)
setattr(ATestIntegrationTest, test_func_name, test_func)
SUITE = unittest.TestLoader().loadTestsFromTestCase(
ATestIntegrationTest)
RESULT = unittest.TextTestRunner(verbosity=2).run(SUITE)
finally:
shutil.rmtree(IntegrationConstants.FAKE_SRC_ROOT)
if RESULT.failures:
print('Full test log is saved to %s' % LOG_PATH)
sys.exit(_EXIT_TEST_FAILED)
else:
os.remove(LOG_PATH)