1#!/usr/bin/env python3
2#
3# Copyright 2018, The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""
18ATest Integration Test Class.
19
20The purpose is to prevent potential side-effects from breaking ATest at the
21early stage while landing CLs with potential side-effects.
22
23It forks a subprocess with ATest commands to validate if it can pass all the
24finding, running logic of the python code, and waiting for TF to exit properly.
25    - When running with ROBOLECTRIC tests, it runs without TF, and will exit
26    the subprocess with the message "All tests passed"
27    - If FAIL, it means something breaks ATest unexpectedly!
28"""
29
30from __future__ import print_function
31
32import os
33import subprocess
34import sys
35import tempfile
36import time
37import unittest
38
39_TEST_RUN_DIR_PREFIX = 'atest_integration_tests_%s_'
40_LOG_FILE = 'integration_tests.log'
41_FAILED_LINE_LIMIT = 50
42_INTEGRATION_TESTS = 'INTEGRATION_TESTS'
43_EXIT_TEST_FAILED = 1
44
45
46class ATestIntegrationTest(unittest.TestCase):
47    """ATest Integration Test Class."""
48    NAME = 'ATestIntegrationTest'
49    EXECUTABLE = 'atest'
50    OPTIONS = ''
51    _RUN_CMD = '{exe} {options} {test}'
52    _PASSED_CRITERIA = ['will be rescheduled', 'All tests passed']
53
54    def setUp(self):
55        """Set up stuff for testing."""
56        self.full_env_vars = os.environ.copy()
57        self.test_passed = False
58        self.log = []
59
60    def run_test(self, testcase):
61        """Create a subprocess to execute the test command.
62
63        Strategy:
64            Fork a subprocess to wait for TF exit properly, and log the error
65            if the exit code isn't 0.
66
67        Args:
68            testcase: A string of testcase name.
69        """
70        run_cmd_dict = {'exe': self.EXECUTABLE, 'options': self.OPTIONS,
71                        'test': testcase}
72        run_command = self._RUN_CMD.format(**run_cmd_dict)
73        try:
74            subprocess.check_output(run_command,
75                                    stderr=subprocess.PIPE,
76                                    env=self.full_env_vars,
77                                    shell=True)
78        except subprocess.CalledProcessError as e:
79            self.log.append(e.output.decode())
80            return False
81        return True
82
83    def get_failed_log(self):
84        """Get a trimmed failed log.
85
86        Strategy:
87            In order not to show the unnecessary log such as build log,
88            it's better to get a trimmed failed log that contains the
89            most important information.
90
91        Returns:
92            A trimmed failed log.
93        """
94        failed_log = '\n'.join(filter(None, self.log[-_FAILED_LINE_LIMIT:]))
95        return failed_log
96
97
98def create_test_method(testcase, log_path):
99    """Create a test method according to the testcase.
100
101    Args:
102        testcase: A testcase name.
103        log_path: A file path for storing the test result.
104
105    Returns:
106        A created test method, and a test function name.
107    """
108    test_function_name = 'test_%s' % testcase.replace(' ', '_')
109    # pylint: disable=missing-docstring
110    def template_test_method(self):
111        self.test_passed = self.run_test(testcase)
112        open(log_path, 'a').write('\n'.join(self.log))
113        failed_message = 'Running command: %s failed.\n' % testcase
114        failed_message += '' if self.test_passed else self.get_failed_log()
115        self.assertTrue(self.test_passed, failed_message)
116    return test_function_name, template_test_method
117
118
119def create_test_run_dir():
120    """Create the test run directory in tmp.
121
122    Returns:
123        A string of the directory path.
124    """
125    utc_epoch_time = int(time.time())
126    prefix = _TEST_RUN_DIR_PREFIX % utc_epoch_time
127    return tempfile.mkdtemp(prefix=prefix)
128
129
130if __name__ == '__main__':
131    # TODO(b/129029189) Implement detail comparison check for dry-run mode.
132    ARGS = ' '.join(sys.argv[1:])
133    if ARGS:
134        ATestIntegrationTest.OPTIONS = ARGS
135    TEST_PLANS = os.path.join(os.path.dirname(__file__), _INTEGRATION_TESTS)
136    try:
137        LOG_PATH = os.path.join(create_test_run_dir(), _LOG_FILE)
138        with open(TEST_PLANS) as test_plans:
139            for test in test_plans:
140                # Skip test when the line startswith #.
141                if not test.strip() or test.strip().startswith('#'):
142                    continue
143                test_func_name, test_func = create_test_method(
144                    test.strip(), LOG_PATH)
145                setattr(ATestIntegrationTest, test_func_name, test_func)
146        SUITE = unittest.TestLoader().loadTestsFromTestCase(
147            ATestIntegrationTest)
148        RESULTS = unittest.TextTestRunner(verbosity=2).run(SUITE)
149    finally:
150        if RESULTS.failures:
151            print('Full test log is saved to %s' % LOG_PATH)
152            sys.exit(_EXIT_TEST_FAILED)
153        else:
154            os.remove(LOG_PATH)
155