• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#
2# Copyright (C) 2018 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the 'License');
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an 'AS IS' BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
16
17import logging
18import os
19import shutil
20import subprocess
21import tempfile
22import threading
23import zipfile
24
25from host_controller.command_processor import base_command_processor
26from host_controller.utils.parser import xml_utils
27from vts.runners.host import utils
28
29
30class CommandTest(base_command_processor.BaseCommandProcessor):
31    """Command processor for test command.
32
33    Attributes:
34        _RESULT_ATTRIBUTES: The attributes of <Result> in the XML report.
35                            After test execution, the attributes are loaded
36                            from report to console's dictionary.
37        _result_dir: the path to the temporary result directory.
38    """
39
40    command = "test"
41    command_detail = "Executes a command on TF."
42    _RESULT_TAG = "Result"
43    _RESULT_ATTRIBUTES = ["suite_plan"]
44
45    # @Override
46    def SetUp(self):
47        """Initializes the parser for test command."""
48        self._result_dir = None
49        self.arg_parser.add_argument(
50            "--suite",
51            default="vts",
52            choices=("vts", "cts", "gts", "sts"),
53            help="To specify the type of a test suite to be run.")
54        self.arg_parser.add_argument(
55            "--serial",
56            "-s",
57            default=None,
58            help="The target device serial to run the command. "
59            "A comma-separate list.")
60        self.arg_parser.add_argument(
61            "--test-exec-mode",
62            default="subprocess",
63            help="The target exec model.")
64        self.arg_parser.add_argument(
65            "--keep-result",
66            action="store_true",
67            help="Keep the path to the result in the console instance.")
68        self.arg_parser.add_argument(
69            "command",
70            metavar="COMMAND",
71            nargs="+",
72            help="The command to be executed. If the command contains "
73            "arguments starting with \"-\", place the command after "
74            "\"--\" at end of line. format: plan -m module -t testcase")
75
76    def _ClearResultDir(self):
77        """Deletes all files in the result directory."""
78        if self._result_dir is None:
79            self._result_dir = tempfile.mkdtemp()
80            return
81
82        for file_name in os.listdir(self._result_dir):
83            shutil.rmtree(os.path.join(self._result_dir, file_name))
84
85    @staticmethod
86    def _GenerateTestSuiteCommand(bin_path, command, serials, result_dir=None):
87        """Generates a *ts-tradefed command.
88
89        Args:
90            bin_path: the path to *ts-tradefed.
91            command: a list of strings, the command arguments.
92            serials: a list of strings, the serial numbers of the devices.
93            result_dir: the path to the temporary directory where the result is
94                        saved.
95
96        Returns:
97            a list of strings, the *ts-tradefed command.
98        """
99        cmd = [bin_path, "run", "commandAndExit"]
100        cmd.extend(str(c) for c in command)
101
102        for serial in serials:
103            cmd.extend(["-s", str(serial)])
104
105        if result_dir:
106            cmd.extend(["--log-file-path", result_dir, "--use-log-saver"])
107
108        return cmd
109
110    @staticmethod
111    def _ExecuteCommand(cmd):
112        """Executes a command and logs output in real time.
113
114        Args:
115            cmd: a list of strings, the command to execute.
116        """
117
118        def LogOutputStream(log_level, stream):
119            try:
120                while True:
121                    line = stream.readline()
122                    if not line:
123                        break
124                    logging.log(log_level, line.rstrip())
125            finally:
126                stream.close()
127
128        proc = subprocess.Popen(
129            cmd,
130            stdin=subprocess.PIPE,
131            stdout=subprocess.PIPE,
132            stderr=subprocess.PIPE)
133
134        out_thread = threading.Thread(
135            target=LogOutputStream, args=(logging.INFO, proc.stdout))
136        err_thread = threading.Thread(
137            target=LogOutputStream, args=(logging.ERROR, proc.stderr))
138        out_thread.daemon = True
139        err_thread.daemon = True
140        out_thread.start()
141        err_thread.start()
142        proc.wait()
143        logging.info("Return code: %d", proc.returncode)
144        proc.stdin.close()
145        out_thread.join()
146        err_thread.join()
147
148    # @Override
149    def Run(self, arg_line):
150        """Executes a command using a *TS-TF instance.
151
152        Args:
153            arg_line: string, line of command arguments.
154        """
155        args = self.arg_parser.ParseLine(arg_line)
156        if args.serial:
157            serials = args.serial.split(",")
158        elif self.console.GetSerials():
159            serials = self.console.GetSerials()
160        else:
161            serials = []
162
163        if args.test_exec_mode == "subprocess":
164            if args.suite not in self.console.test_suite_info:
165                logging.error("test_suite_info doesn't have '%s': %s",
166                              args.suite, self.console.test_suite_info)
167                return
168
169            if args.keep_result:
170                self._ClearResultDir()
171                result_dir = self._result_dir
172            else:
173                result_dir = None
174
175            cmd = self._GenerateTestSuiteCommand(
176                self.console.test_suite_info[args.suite], args.command,
177                serials, result_dir)
178
179            logging.info("Command: %s", cmd)
180            self._ExecuteCommand(cmd)
181
182            if result_dir:
183                result_paths = [
184                    os.path.join(dir_name, file_name)
185                    for dir_name, file_name in utils.iterate_files(result_dir)
186                    if file_name.startswith("log-result")
187                    and file_name.endswith(".zip")
188                ]
189
190                if len(result_paths) != 1:
191                    logging.warning("Unexpected number of results: %s",
192                                    result_paths)
193
194                self.console.test_result.clear()
195                result = {}
196                if len(result_paths) > 0:
197                    with zipfile.ZipFile(
198                            result_paths[0], mode="r") as result_zip:
199                        with result_zip.open(
200                                "log-result.xml", mode="rU") as result_xml:
201                            result = xml_utils.GetAttributes(
202                                result_xml, self._RESULT_TAG,
203                                self._RESULT_ATTRIBUTES)
204                            if not result:
205                                logging.warning("Nothing loaded from report.")
206                    result["result_zip"] = result_paths[0]
207
208                result_paths_full = [
209                    os.path.join(dir_name, file_name)
210                    for dir_name, file_name in utils.iterate_files(result_dir)
211                    if file_name.endswith(".zip")
212                ]
213                result["result_full"] = " ".join(result_paths_full)
214                result["suite_name"] = args.suite
215
216                logging.debug(result)
217                self.console.test_result.update(result)
218        else:
219            logging.error("unsupported exec mode: %s", args.test_exec_mode)
220            return False
221
222    # @Override
223    def TearDown(self):
224        """Deletes the result directory."""
225        if self._result_dir:
226            shutil.rmtree(self._result_dir, ignore_errors=True)
227