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().__init__(results_dir, **kwargs)
60        # TODO: Rollback when found a solution to b/183335046.
61        if not os.getenv(test_runner_base.OLD_OUTPUT_ENV_VAR):
62            self.is_verbose = True
63        else:
64            self.is_verbose = logging.getLogger().isEnabledFor(logging.DEBUG)
65
66    def run_tests(self, test_infos, extra_args, reporter):
67        """Run the list of test_infos. See base class for more.
68
69        Args:
70            test_infos: A list of TestInfos.
71            extra_args: Dict of extra args to add to test run.
72            reporter: An instance of result_report.ResultReporter.
73
74        Returns:
75            0 if tests succeed, non-zero otherwise.
76        """
77        # TODO: Rollback when found a solution to b/183335046.
78        if os.getenv(test_runner_base.OLD_OUTPUT_ENV_VAR):
79            return self.run_tests_pretty(test_infos, extra_args, reporter)
80        return self.run_tests_raw(test_infos, extra_args, reporter)
81
82    def run_tests_raw(self, test_infos, extra_args, reporter):
83        """Run the list of test_infos with raw output.
84
85        Args:
86            test_infos: List of TestInfo.
87            extra_args: Dict of extra args to add to test run.
88            reporter: A ResultReporter Instance.
89
90        Returns:
91            0 if tests succeed, non-zero otherwise.
92        """
93        reporter.register_unsupported_runner(self.NAME)
94        ret_code = constants.EXIT_CODE_SUCCESS
95        for test_info in test_infos:
96            full_env_vars = self._get_full_build_environ(test_info,
97                                                         extra_args)
98            run_cmd = self.generate_run_commands([test_info], extra_args)[0]
99            subproc = self.run(run_cmd,
100                               output_to_stdout=self.is_verbose,
101                               env_vars=full_env_vars)
102            ret_code |= self.wait_for_subprocess(subproc)
103        return ret_code
104
105    def run_tests_pretty(self, test_infos, extra_args, reporter):
106        """Run the list of test_infos with pretty output mode.
107
108        Args:
109            test_infos: List of TestInfo.
110            extra_args: Dict of extra args to add to test run.
111            reporter: A ResultReporter Instance.
112
113        Returns:
114            0 if tests succeed, non-zero otherwise.
115        """
116        ret_code = constants.EXIT_CODE_SUCCESS
117        for test_info in test_infos:
118            # Create a temp communication file.
119            with tempfile.NamedTemporaryFile(dir=self.results_dir) as event_file:
120                # Prepare build environment parameter.
121                full_env_vars = self._get_full_build_environ(test_info,
122                                                             extra_args,
123                                                             event_file)
124                run_cmd = self.generate_run_commands([test_info], extra_args)[0]
125                subproc = self.run(run_cmd,
126                                   output_to_stdout=self.is_verbose,
127                                   env_vars=full_env_vars)
128                event_handler = EventHandler(reporter, self.NAME)
129                # Start polling.
130                self.handle_subprocess(subproc,
131                                       partial(self._exec_with_robo_polling,
132                                               event_file,
133                                               subproc,
134                                               event_handler))
135                ret_code |= self.wait_for_subprocess(subproc)
136        return ret_code
137
138    def _get_full_build_environ(self, test_info=None, extra_args=None,
139                                event_file=None):
140        """Helper to get full build environment.
141
142       Args:
143           test_info: TestInfo object.
144           extra_args: Dict of extra args to add to test run.
145           event_file: A file-like object that can be used as a temporary
146                       storage area.
147       """
148        full_env_vars = os.environ.copy()
149        env_vars = self.generate_env_vars(test_info,
150                                          extra_args,
151                                          event_file)
152        full_env_vars.update(env_vars)
153        return full_env_vars
154
155    def _exec_with_robo_polling(self, communication_file, robo_proc,
156                                event_handler):
157        """Polling data from communication file
158
159        Polling data from communication file. Exit when communication file
160        is empty and subprocess ended.
161
162        Args:
163            communication_file: A monitored communication file.
164            robo_proc: The build process.
165            event_handler: A file-like object storing the events of robolectric tests.
166        """
167        buf = ''
168        while True:
169            # Make sure that ATest gets content from current position.
170            communication_file.seek(0, 1)
171            data = communication_file.read()
172            if isinstance(data, bytes):
173                data = data.decode()
174            buf += data
175            reg = re.compile(r'(.|\n)*}\n\n')
176            if not reg.match(buf) or data == '':
177                if robo_proc.poll() is not None:
178                    logging.debug('Build process exited early')
179                    return
180                time.sleep(POLL_FREQ_SECS)
181            else:
182                # Read all new data and handle it at one time.
183                for event in re.split(r'\n\n', buf):
184                    match = EVENT_RE.match(event)
185                    if match:
186                        try:
187                            event_data = json.loads(match.group('json_data'),
188                                                    strict=False)
189                        except ValueError:
190                            # Parse event fail, continue to parse next one.
191                            logging.debug('"%s" is not valid json format.',
192                                          match.group('json_data'))
193                            continue
194                        event_name = match.group('event_name')
195                        event_handler.process_event(event_name, event_data)
196                buf = ''
197
198    @staticmethod
199    def generate_env_vars(test_info, extra_args, event_file=None):
200        """Turn the args into env vars.
201
202        Robolectric tests specify args through env vars, so look for class
203        filters and debug args to apply to the env.
204
205        Args:
206            test_info: TestInfo class that holds the class filter info.
207            extra_args: Dict of extra args to apply for test run.
208            event_file: A file-like object storing the events of robolectric
209            tests.
210
211        Returns:
212            Dict of env vars to pass into invocation.
213        """
214        env_var = {}
215        for arg in extra_args:
216            if constants.WAIT_FOR_DEBUGGER == arg:
217                env_var['DEBUG_ROBOLECTRIC'] = 'true'
218                continue
219        filters = test_info.data.get(constants.TI_FILTER)
220        if filters:
221            robo_filter = next(iter(filters))
222            env_var['ROBOTEST_FILTER'] = robo_filter.class_name
223            if robo_filter.methods:
224                logging.debug('method filtering not supported for robolectric '
225                              'tests yet.')
226        if event_file:
227            env_var['EVENT_FILE_ROBOLECTRIC'] = event_file.name
228        return env_var
229
230    # pylint: disable=unnecessary-pass
231    # Please keep above disable flag to ensure host_env_check is overriden.
232    def host_env_check(self):
233        """Check that host env has everything we need.
234
235        We actually can assume the host env is fine because we have the same
236        requirements that atest has. Update this to check for android env vars
237        if that changes.
238        """
239        pass
240
241    def get_test_runner_build_reqs(self):
242        """Return the build requirements.
243
244        Returns:
245            Set of build targets.
246        """
247        return set()
248
249    # pylint: disable=unused-argument
250    def generate_run_commands(self, test_infos, extra_args, port=None):
251        """Generate a list of run commands from TestInfos.
252
253        Args:
254            test_infos: A set of TestInfo instances.
255            extra_args: A Dict of extra args to append.
256            port: Optional. An int of the port number to send events to.
257                  Subprocess reporter in TF won't try to connect if it's None.
258
259        Returns:
260            A list of run commands to run the tests.
261        """
262        run_cmds = []
263        for test_info in test_infos:
264            robo_command = atest_utils.get_build_cmd() + [str(test_info.test_name)]
265            run_cmd = ' '.join(x for x in robo_command)
266            if constants.DRY_RUN in extra_args:
267                run_cmd = run_cmd.replace(
268                    os.environ.get(constants.ANDROID_BUILD_TOP) + os.sep, '')
269            run_cmds.append(run_cmd)
270        return run_cmds
271