1#!/usr/bin/env python3
2# coding=utf-8
3
4#
5# Copyright (c) 2022 Huawei Device Co., Ltd.
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10#     http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17#
18
19import os
20from ohos.drivers import *
21from ohos.error import ErrorMessage
22
23__all__ = ["CppTestDriver"]
24LOG = platform_logger("CppTestDriver")
25DEFAULT_TEST_PATH = "/%s/%s/" % ("data", "test")
26
27FAILED_RUN_TEST_ATTEMPTS = 3
28TIME_OUT = 900 * 1000
29
30
31@Plugin(type=Plugin.DRIVER, id=DeviceTestType.cpp_test)
32class CppTestDriver(IDriver):
33    """
34    CppTestDriver is a Test that runs a native test package on given harmony
35    device.
36    """
37
38    def __init__(self):
39        self.result = ""
40        self.error_message = ""
41        self.config = None
42        self.rerun = True
43        self.rerun_all = True
44        self.runner = None
45        # log
46        self.device_log = None
47        self.hilog = None
48        self.log_proc = None
49        self.hilog_proc = None
50
51    def __check_environment__(self, device_options):
52        pass
53
54    def __check_config__(self, config):
55        pass
56
57    def __execute__(self, request):
58        try:
59            LOG.debug("Start execute xdevice extension CppTest")
60
61            self.config = request.config
62            self.config.device = request.config.environment.devices[0]
63
64            config_file = request.root.source.config_file
65            self.result = "%s.xml" % \
66                          os.path.join(request.config.report_path,
67                                       "result", request.root.source.test_name)
68
69            self.device_log = get_device_log_file(
70                request.config.report_path,
71                request.config.device.__get_serial__(),
72                "device_log",
73                module_name=request.get_module_name(),
74                repeat=request.config.repeat,
75                repeat_round=request.get_repeat_round())
76
77            self.hilog = get_device_log_file(
78                request.config.report_path,
79                request.config.device.__get_serial__(),
80                "device_hilog",
81                module_name=request.get_module_name(),
82                repeat=request.config.repeat,
83                repeat_round=request.get_repeat_round())
84
85            device_log_open = os.open(self.device_log, os.O_WRONLY | os.O_CREAT |
86                                      os.O_APPEND, FilePermission.mode_755)
87            hilog_open = os.open(self.hilog, os.O_WRONLY | os.O_CREAT | os.O_APPEND,
88                                 FilePermission.mode_755)
89            self.config.device.device_log_collector.add_log_address(self.device_log, self.hilog)
90            log_level = self.config.device_log.get(ConfigConst.tag_loglevel, "INFO")
91            with os.fdopen(device_log_open, "a") as log_file_pipe, \
92                    os.fdopen(hilog_open, "a") as hilog_file_pipe:
93                self.log_proc, self.hilog_proc = self.config.device.device_log_collector. \
94                    start_catch_device_log(log_file_pipe, hilog_file_pipe, log_level=log_level)
95                self._run_cpp_test(config_file, listeners=request.listeners,
96                                   request=request)
97                log_file_pipe.flush()
98                hilog_file_pipe.flush()
99
100        except Exception as exception:
101            self.error_message = exception
102            if not getattr(exception, "error_no", ""):
103                setattr(exception, "error_no", "03404")
104            LOG.exception(self.error_message, exc_info=False, error_no="03404")
105            raise exception
106
107        finally:
108            self.config.device.device_log_collector.remove_log_address(self.device_log, self.hilog)
109            self.config.device.device_log_collector.stop_catch_device_log(self.log_proc)
110            self.config.device.device_log_collector.stop_catch_device_log(self.hilog_proc)
111            self.result = check_result_report(
112                request.config.report_path, self.result, self.error_message, request=request)
113
114    def _run_cpp_test(self, config_file, listeners=None, request=None):
115        try:
116            if not os.path.exists(config_file):
117                err_msg = ErrorMessage.Common.Code_0301002.format(config_file)
118                LOG.error(err_msg)
119                raise ParamError(err_msg)
120
121            json_config = JsonParser(config_file)
122            kits = get_kit_instances(json_config, self.config.resource_path,
123                                     self.config.testcases_path)
124
125            for listener in listeners:
126                listener.device_sn = self.config.device.device_sn
127
128            self._get_driver_config(json_config)
129            do_module_kit_setup(request, kits)
130            self.runner = RemoteCppTestRunner(self.config)
131            self.runner.suite_name = request.root.source.test_name
132
133            if hasattr(self.config, "history_report_path") and \
134                    self.config.testargs.get("test"):
135                self._do_test_retry(listeners, self.config.testargs)
136            else:
137                gtest_para_parse(self.config.testargs, self.runner, request)
138                self._do_test_run(listeners)
139
140        finally:
141            do_module_kit_teardown(request)
142
143    def _do_test_retry(self, listener, testargs):
144        for test in testargs.get("test"):
145            test_item = test.split("#")
146            if len(test_item) != 2:
147                continue
148            self.runner.add_instrumentation_arg(
149                "gtest_filter", "%s.%s" % (test_item[0], test_item[1]))
150            self.runner.run(listener)
151
152    def _do_test_run(self, listener):
153        test_to_run = self._collect_test_to_run()
154        LOG.info("Collected test count is: %s" % (len(test_to_run)
155                                                  if test_to_run else 0))
156        if not test_to_run:
157            self.runner.run(listener)
158        else:
159            self._run_with_rerun(listener, test_to_run)
160
161    def _collect_test_to_run(self):
162        if self.rerun:
163            self.runner.add_instrumentation_arg("gtest_list_tests", True)
164            run_results = self.runner.dry_run()
165            self.runner.remove_instrumentation_arg("gtest_list_tests")
166            return run_results
167        return None
168
169    def _run_tests(self, listener):
170        test_tracker = CollectingTestListener()
171        listener_copy = listener.copy()
172        listener_copy.append(test_tracker)
173        self.runner.run(listener_copy)
174        test_run = test_tracker.get_current_run_results()
175        return test_run
176
177    def _run_with_rerun(self, listener, expected_tests):
178        LOG.debug("Ready to run with rerun, expect run: %s"
179                  % len(expected_tests))
180        test_run = self._run_tests(listener)
181        LOG.debug("Run with rerun, has run: %s" % len(test_run)
182                  if test_run else 0)
183        if len(test_run) < len(expected_tests):
184            expected_tests = TestDescription.remove_test(expected_tests,
185                                                         test_run)
186            if not expected_tests:
187                LOG.debug("No tests to re-run, all tests executed at least "
188                          "once.")
189            if self.rerun_all:
190                self._rerun_all(expected_tests, listener)
191            else:
192                self._rerun_serially(expected_tests, listener)
193
194    def _rerun_all(self, expected_tests, listener):
195        tests = []
196        for test in expected_tests:
197            tests.append("%s.%s" % (test.class_name, test.test_name))
198        self.runner.add_instrumentation_arg("gtest_filter", ":".join(tests))
199        LOG.debug("Ready to rerun file, expect run: %s" % len(expected_tests))
200        test_run = self._run_tests(listener)
201        LOG.debug("Rerun file, has run: %s" % len(test_run))
202        if len(test_run) < len(expected_tests):
203            expected_tests = TestDescription.remove_test(expected_tests,
204                                                         test_run)
205            if not expected_tests:
206                LOG.debug("Rerun textFile success")
207            self._rerun_serially(expected_tests, listener)
208
209    def _rerun_serially(self, expected_tests, listener):
210        LOG.debug("Rerun serially, expected run: %s" % len(expected_tests))
211        for test in expected_tests:
212            self.runner.add_instrumentation_arg(
213                "gtest_filter", "%s.%s" % (test.class_name, test.test_name))
214            self.runner.rerun(listener, test)
215            self.runner.remove_instrumentation_arg("gtest_filter")
216
217    def _get_driver_config(self, json_config):
218        target_test_path = get_config_value('native-test-device-path',
219                                            json_config.get_driver(), False)
220        if target_test_path:
221            self.config.target_test_path = target_test_path
222        else:
223            self.config.target_test_path = DEFAULT_TEST_PATH
224
225        self.config.module_name = get_config_value(
226            'module-name', json_config.get_driver(), False)
227
228        timeout_config = get_config_value('native-test-timeout',
229                                          json_config.get_driver(), False)
230        if timeout_config:
231            self.config.timeout = int(timeout_config)
232        else:
233            self.config.timeout = TIME_OUT
234
235        rerun = get_config_value('rerun', json_config.get_driver(), False)
236        if isinstance(rerun, bool):
237            self.rerun = rerun
238        elif str(rerun).lower() == "false":
239            self.rerun = False
240        else:
241            self.rerun = True
242
243    def __result__(self):
244        return self.result if os.path.exists(self.result) else ""
245
246
247class RemoteCppTestRunner:
248    def __init__(self, config):
249        self.arg_list = {}
250        self.suite_name = None
251        self.config = config
252        self.rerun_attempt = FAILED_RUN_TEST_ATTEMPTS
253        # 判断半容器
254        self.ohca = check_device_ohca(self.config.device)
255
256    def dry_run(self):
257        parsers = get_plugin(Plugin.PARSER, CommonParserType.cpptest_list)
258        if parsers:
259            parsers = parsers[:1]
260        parser_instances = []
261        for parser in parsers:
262            parser_instance = parser.__class__()
263            parser_instances.append(parser_instance)
264        handler = ShellHandler(parser_instances)
265        handler.add_process_method(_cpp_output_method)
266
267        command = "cd %s; chmod +x *; ./%s %s --gtest_color=no" \
268                  % (self.config.target_test_path, self.config.module_name,
269                     self.get_args_command())
270        if self.ohca:
271            bin_path = "{}/{}".format(self.config.target_test_path,
272                                      self.config.module_name)
273            # 半容器的C++用例需要调整权限
274            command = "ohsh toybox chown 20000000:20000000 {}; ohsh toybox chmod a+x {}; ohsh {} {}". \
275                format(bin_path, bin_path, bin_path, self.get_args_command())
276
277        self.config.device.execute_shell_command(
278            command, timeout=self.config.timeout, receiver=handler, retry=0)
279        return parser_instances[0].tests
280
281    def run(self, listener):
282        handler = self._get_shell_handler(listener)
283        command = "cd %s; chmod +x *; ./%s %s --gtest_color=no" \
284                  % (self.config.target_test_path, self.config.module_name,
285                     self.get_args_command())
286        if self.ohca:
287            bin_path = "{}/{}".format(self.config.target_test_path,
288                                      self.config.module_name)
289            # 半容器的C++用例需要调整权限
290            command = "ohsh toybox chown 20000000:20000000 {}; ohsh toybox chmod a+x {}; ohsh {} {}". \
291                format(bin_path, bin_path, bin_path, self.get_args_command())
292
293        self.config.device.execute_shell_command(
294            command, timeout=self.config.timeout, receiver=handler, retry=0)
295
296    def rerun(self, listener, test):
297        if self.rerun_attempt:
298            test_tracker = CollectingTestListener()
299            listener_copy = listener.copy()
300            listener_copy.append(test_tracker)
301            handler = self._get_shell_handler(listener_copy)
302            try:
303                command = "cd %s; chmod +x *; ./%s %s" \
304                          % (self.config.target_test_path,
305                             self.config.module_name,
306                             self.get_args_command())
307                if self.ohca:
308                    bin_path = "{}/{}".format(self.config.target_test_path,
309                                              self.config.module_name)
310                    command = "ohsh toybox chmod a+x {}; ohsh {} {}".format(
311                        bin_path, bin_path, self.get_args_command())
312
313                self.config.device.execute_shell_command(
314                    command, timeout=self.config.timeout, receiver=handler,
315                    retry=0)
316
317            except ShellCommandUnresponsiveException as _:
318                LOG.debug("Exception: ShellCommandUnresponsiveException")
319            finally:
320                if not test_tracker.get_current_run_results():
321                    LOG.debug("No test case is obtained finally")
322                    self.rerun_attempt -= 1
323                    handler.parsers[0].mark_test_as_blocked(test)
324        else:
325            LOG.debug("Not execute and mark as blocked finally")
326            handler = self._get_shell_handler(listener)
327            handler.parsers[0].mark_test_as_blocked(test)
328
329    def add_instrumentation_arg(self, name, value):
330        if not name or not value:
331            return
332        self.arg_list[name] = value
333
334    def remove_instrumentation_arg(self, name):
335        if not name:
336            return
337        if name in self.arg_list:
338            del self.arg_list[name]
339
340    def get_args_command(self):
341        args_commands = ""
342        for key, value in self.arg_list.items():
343            if key == "gtest_list_tests":
344                args_commands = "%s --%s" % (args_commands, key)
345            else:
346                args_commands = "%s --%s=%s" % (args_commands, key, value)
347        return args_commands
348
349    def _get_shell_handler(self, listener):
350        parsers = get_plugin(Plugin.PARSER, CommonParserType.cpptest)
351        if parsers:
352            parsers = parsers[:1]
353        parser_instances = []
354        for parser in parsers:
355            parser_instance = parser.__class__()
356            parser_instance.suite_name = self.suite_name
357            parser_instance.listeners = listener
358            parser_instances.append(parser_instance)
359        handler = ShellHandler(parser_instances)
360        handler.add_process_method(_cpp_output_method)
361        return handler
362
363
364def _cpp_output_method(handler, output, end_mark="\n"):
365    content = output
366    if handler.unfinished_line:
367        content = "".join((handler.unfinished_line, content))
368        handler.unfinished_line = ""
369    lines = content.split(end_mark)
370    if content.endswith(end_mark):
371        # get rid of the tail element of this list contains empty str
372        return lines[:-1]
373    else:
374        handler.unfinished_line = lines[-1]
375        # not return the tail element of this list contains unfinished str,
376        # so we set position -1
377        return lines[:-1]
378