1# Copyright 2018, The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""
16Robolectric test runner class.
17
18This test runner will be short lived, once robolectric support v2 is in, then
19robolectric tests will be invoked through AtestTFTestRunner.
20"""
21
22# pylint: disable=line-too-long
23
24import json
25import logging
26import os
27import re
28import tempfile
29import time
30
31from functools import partial
32
33import atest_utils
34import constants
35
36from test_runners import test_runner_base
37from .event_handler import EventHandler
38
39POLL_FREQ_SECS = 0.1
40# A pattern to match event like below
41#TEST_FAILED {'className':'SomeClass', 'testName':'SomeTestName',
42#            'trace':'{"trace":"AssertionError: <true> is equal to <false>\n
43#               at FailureStrategy.fail(FailureStrategy.java:24)\n
44#               at FailureStrategy.fail(FailureStrategy.java:20)\n"}\n\n
45EVENT_RE = re.compile(r'^(?P<event_name>[A-Z_]+) (?P<json_data>{(.\r*|\n)*})(?:\n|$)')
46
47
48class RobolectricTestRunner(test_runner_base.TestRunnerBase):
49    """Robolectric Test Runner class."""
50    NAME = 'RobolectricTestRunner'
51    # We don't actually use EXECUTABLE because we're going to use
52    # atest_utils.build to kick off the test but if we don't set it, the base
53    # class will raise an exception.
54    EXECUTABLE = 'make'
55
56    # pylint: disable=useless-super-delegation
57    def __init__(self, results_dir, **kwargs):
58        """Init stuff for robolectric runner class."""
59        super(RobolectricTestRunner, self).__init__(results_dir, **kwargs)
60        self.is_verbose = logging.getLogger().isEnabledFor(logging.DEBUG)
61
62    def run_tests(self, test_infos, extra_args, reporter):
63        """Run the list of test_infos. See base class for more.
64
65        Args:
66            test_infos: A list of TestInfos.
67            extra_args: Dict of extra args to add to test run.
68            reporter: An instance of result_report.ResultReporter.
69
70        Returns:
71            0 if tests succeed, non-zero otherwise.
72        """
73        if os.getenv(test_runner_base.OLD_OUTPUT_ENV_VAR):
74            return self.run_tests_raw(test_infos, extra_args, reporter)
75        return self.run_tests_pretty(test_infos, extra_args, reporter)
76
77    def run_tests_raw(self, test_infos, extra_args, reporter):
78        """Run the list of test_infos with raw output.
79
80        Args:
81            test_infos: List of TestInfo.
82            extra_args: Dict of extra args to add to test run.
83            reporter: A ResultReporter Instance.
84
85        Returns:
86            0 if tests succeed, non-zero otherwise.
87        """
88        reporter.register_unsupported_runner(self.NAME)
89        ret_code = constants.EXIT_CODE_SUCCESS
90        for test_info in test_infos:
91            full_env_vars = self._get_full_build_environ(test_info,
92                                                         extra_args)
93            run_cmd = self.generate_run_commands([test_info], extra_args)[0]
94            subproc = self.run(run_cmd,
95                               output_to_stdout=self.is_verbose,
96                               env_vars=full_env_vars)
97            ret_code |= self.wait_for_subprocess(subproc)
98        return ret_code
99
100    def run_tests_pretty(self, test_infos, extra_args, reporter):
101        """Run the list of test_infos with pretty output mode.
102
103        Args:
104            test_infos: List of TestInfo.
105            extra_args: Dict of extra args to add to test run.
106            reporter: A ResultReporter Instance.
107
108        Returns:
109            0 if tests succeed, non-zero otherwise.
110        """
111        ret_code = constants.EXIT_CODE_SUCCESS
112        for test_info in test_infos:
113            # Create a temp communication file.
114            with tempfile.NamedTemporaryFile(dir=self.results_dir) as event_file:
115                # Prepare build environment parameter.
116                full_env_vars = self._get_full_build_environ(test_info,
117                                                             extra_args,
118                                                             event_file)
119                run_cmd = self.generate_run_commands([test_info], extra_args)[0]
120                subproc = self.run(run_cmd,
121                                   output_to_stdout=self.is_verbose,
122                                   env_vars=full_env_vars)
123                event_handler = EventHandler(reporter, self.NAME)
124                # Start polling.
125                self.handle_subprocess(subproc,
126                                       partial(self._exec_with_robo_polling,
127                                               event_file,
128                                               subproc,
129                                               event_handler))
130                ret_code |= self.wait_for_subprocess(subproc)
131        return ret_code
132
133    def _get_full_build_environ(self, test_info=None, extra_args=None,
134                                event_file=None):
135        """Helper to get full build environment.
136
137       Args:
138           test_info: TestInfo object.
139           extra_args: Dict of extra args to add to test run.
140           event_file: A file-like object that can be used as a temporary
141                       storage area.
142       """
143        full_env_vars = os.environ.copy()
144        env_vars = self.generate_env_vars(test_info,
145                                          extra_args,
146                                          event_file)
147        full_env_vars.update(env_vars)
148        return full_env_vars
149
150    def _exec_with_robo_polling(self, communication_file, robo_proc,
151                                event_handler):
152        """Polling data from communication file
153
154        Polling data from communication file. Exit when communication file
155        is empty and subprocess ended.
156
157        Args:
158            communication_file: A monitored communication file.
159            robo_proc: The build process.
160            event_handler: A file-like object storing the events of robolectric tests.
161        """
162        buf = ''
163        while True:
164            # Make sure that ATest gets content from current position.
165            communication_file.seek(0, 1)
166            data = communication_file.read()
167            if isinstance(data, bytes):
168                data = data.decode()
169            buf += data
170            reg = re.compile(r'(.|\n)*}\n\n')
171            if not reg.match(buf) or data == '':
172                if robo_proc.poll() is not None:
173                    logging.debug('Build process exited early')
174                    return
175                time.sleep(POLL_FREQ_SECS)
176            else:
177                # Read all new data and handle it at one time.
178                for event in re.split(r'\n\n', buf):
179                    match = EVENT_RE.match(event)
180                    if match:
181                        try:
182                            event_data = json.loads(match.group('json_data'),
183                                                    strict=False)
184                        except ValueError:
185                            # Parse event fail, continue to parse next one.
186                            logging.debug('"%s" is not valid json format.',
187                                          match.group('json_data'))
188                            continue
189                        event_name = match.group('event_name')
190                        event_handler.process_event(event_name, event_data)
191                buf = ''
192
193    @staticmethod
194    def generate_env_vars(test_info, extra_args, event_file=None):
195        """Turn the args into env vars.
196
197        Robolectric tests specify args through env vars, so look for class
198        filters and debug args to apply to the env.
199
200        Args:
201            test_info: TestInfo class that holds the class filter info.
202            extra_args: Dict of extra args to apply for test run.
203            event_file: A file-like object storing the events of robolectric
204            tests.
205
206        Returns:
207            Dict of env vars to pass into invocation.
208        """
209        env_var = {}
210        for arg in extra_args:
211            if constants.WAIT_FOR_DEBUGGER == arg:
212                env_var['DEBUG_ROBOLECTRIC'] = 'true'
213                continue
214        filters = test_info.data.get(constants.TI_FILTER)
215        if filters:
216            robo_filter = next(iter(filters))
217            env_var['ROBOTEST_FILTER'] = robo_filter.class_name
218            if robo_filter.methods:
219                logging.debug('method filtering not supported for robolectric '
220                              'tests yet.')
221        if event_file:
222            env_var['EVENT_FILE_ROBOLECTRIC'] = event_file.name
223        return env_var
224
225    # pylint: disable=unnecessary-pass
226    # Please keep above disable flag to ensure host_env_check is overriden.
227    def host_env_check(self):
228        """Check that host env has everything we need.
229
230        We actually can assume the host env is fine because we have the same
231        requirements that atest has. Update this to check for android env vars
232        if that changes.
233        """
234        pass
235
236    def get_test_runner_build_reqs(self):
237        """Return the build requirements.
238
239        Returns:
240            Set of build targets.
241        """
242        return set()
243
244    # pylint: disable=unused-argument
245    def generate_run_commands(self, test_infos, extra_args, port=None):
246        """Generate a list of run commands from TestInfos.
247
248        Args:
249            test_infos: A set of TestInfo instances.
250            extra_args: A Dict of extra args to append.
251            port: Optional. An int of the port number to send events to.
252                  Subprocess reporter in TF won't try to connect if it's None.
253
254        Returns:
255            A list of run commands to run the tests.
256        """
257        run_cmds = []
258        for test_info in test_infos:
259            robo_command = atest_utils.get_build_cmd() + [str(test_info.test_name)]
260            run_cmd = ' '.join(x for x in robo_command)
261            if constants.DRY_RUN in extra_args:
262                run_cmd = run_cmd.replace(
263                    os.environ.get(constants.ANDROID_BUILD_TOP) + os.sep, '')
264            run_cmds.append(run_cmd)
265        return run_cmds
266