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