普通文本  |  152行  |  5.13 KB

#!/usr/bin/env python
#
# Copyright 2018, 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 subprocess
import sys
import tempfile
import time
import unittest

_TEST_RUN_DIR_PREFIX = 'atest_integration_tests_%s_'
_LOG_FILE = 'integration_tests.log'
_FAILED_LINE_LIMIT = 50
_INTEGRATION_TESTS = 'INTEGRATION_TESTS'


class ATestIntegrationTest(unittest.TestCase):
    """ATest Integration Test Class."""
    NAME = 'ATestIntegrationTest'
    EXECUTABLE = 'atest'
    OPTIONS = ''
    _RUN_CMD = '{exe} {options} {test}'
    _PASSED_CRITERIAS = ['will be rescheduled', 'All tests passed']

    def setUp(self):
        """Set up stuff for testing."""
        self.full_env_vars = os.environ.copy()
        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,
                                    stderr=subprocess.PIPE,
                                    env=self.full_env_vars,
                                    shell=True)
        except subprocess.CalledProcessError as e:
            self.log.append(e.output)
            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)
        open(log_path, 'a').write('\n'.join(self.log))
        failed_message = 'Running command: %s failed.\n' % testcase
        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)


if __name__ == '__main__':
    # TODO(b/129029189) Implement detail comparison check for dry-run mode.
    ARGS = ' '.join(sys.argv[1:])
    if ARGS:
        ATestIntegrationTest.OPTIONS = ARGS
    TEST_PLANS = os.path.join(os.path.dirname(__file__), _INTEGRATION_TESTS)
    try:
        LOG_PATH = os.path.join(create_test_run_dir(), _LOG_FILE)
        with open(TEST_PLANS) 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)
        RESULTS = unittest.TextTestRunner(verbosity=2).run(SUITE)
    finally:
        if RESULTS.failures:
            print('Full test log is saved to %s' % LOG_PATH)
        else:
            os.remove(LOG_PATH)