1*760c253cSXin Li# -*- coding: utf-8 -*- 2*760c253cSXin Li# Copyright 2013 The ChromiumOS Authors 3*760c253cSXin Li# Use of this source code is governed by a BSD-style license that can be 4*760c253cSXin Li# found in the LICENSE file. 5*760c253cSXin Li 6*760c253cSXin Li"""SuiteRunner defines the interface from crosperf to test script.""" 7*760c253cSXin Li 8*760c253cSXin Li 9*760c253cSXin Liimport contextlib 10*760c253cSXin Liimport json 11*760c253cSXin Liimport os 12*760c253cSXin Lifrom pathlib import Path 13*760c253cSXin Liimport pipes 14*760c253cSXin Liimport random 15*760c253cSXin Liimport shlex 16*760c253cSXin Liimport subprocess 17*760c253cSXin Liimport time 18*760c253cSXin Li 19*760c253cSXin Lifrom cros_utils import command_executer 20*760c253cSXin Lifrom cros_utils import misc 21*760c253cSXin Li 22*760c253cSXin Li 23*760c253cSXin Li# sshwatcher path, relative to ChromiumOS source root. 24*760c253cSXin LiSSHWATCHER = "src/platform/dev/contrib/sshwatcher/sshwatcher.go" 25*760c253cSXin LiTEST_THAT_PATH = "/usr/bin/test_that" 26*760c253cSXin LiTAST_PATH = "/usr/bin/tast" 27*760c253cSXin LiCROSFLEET_PATH = "crosfleet" 28*760c253cSXin LiGS_UTIL = "src/chromium/depot_tools/gsutil.py" 29*760c253cSXin LiAUTOTEST_DIR = "/mnt/host/source/src/third_party/autotest/files" 30*760c253cSXin LiCHROME_MOUNT_DIR = "/tmp/chrome_root" 31*760c253cSXin Li 32*760c253cSXin Li 33*760c253cSXin Lidef GetProfilerArgs(profiler_args): 34*760c253cSXin Li # Remove "--" from in front of profiler args. 35*760c253cSXin Li args_list = shlex.split(profiler_args) 36*760c253cSXin Li new_list = [] 37*760c253cSXin Li for arg in args_list: 38*760c253cSXin Li if arg[0:2] == "--": 39*760c253cSXin Li arg = arg[2:] 40*760c253cSXin Li new_list.append(arg) 41*760c253cSXin Li args_list = new_list 42*760c253cSXin Li 43*760c253cSXin Li # Remove "perf_options=" from middle of profiler args. 44*760c253cSXin Li new_list = [] 45*760c253cSXin Li for arg in args_list: 46*760c253cSXin Li idx = arg.find("perf_options=") 47*760c253cSXin Li if idx != -1: 48*760c253cSXin Li prefix = arg[0:idx] 49*760c253cSXin Li suffix = arg[idx + len("perf_options=") + 1 : -1] 50*760c253cSXin Li new_arg = prefix + "'" + suffix + "'" 51*760c253cSXin Li new_list.append(new_arg) 52*760c253cSXin Li else: 53*760c253cSXin Li new_list.append(arg) 54*760c253cSXin Li args_list = new_list 55*760c253cSXin Li 56*760c253cSXin Li return " ".join(args_list) 57*760c253cSXin Li 58*760c253cSXin Li 59*760c253cSXin Lidef GetDutConfigArgs(dut_config): 60*760c253cSXin Li return f"dut_config={pipes.quote(json.dumps(dut_config))}" 61*760c253cSXin Li 62*760c253cSXin Li 63*760c253cSXin Li@contextlib.contextmanager 64*760c253cSXin Lidef ssh_tunnel(sshwatcher: "os.PathLike", machinename: str) -> str: 65*760c253cSXin Li """Context manager that forwards a TCP port over SSH while active. 66*760c253cSXin Li 67*760c253cSXin Li This class is used to set up port forwarding before entering the 68*760c253cSXin Li chroot, so that the forwarded port can be used from inside 69*760c253cSXin Li the chroot. 70*760c253cSXin Li 71*760c253cSXin Li Args: 72*760c253cSXin Li sshwatcher: Path to sshwatcher.go 73*760c253cSXin Li machinename: Hostname of the machine to connect to. 74*760c253cSXin Li 75*760c253cSXin Li Returns: 76*760c253cSXin Li host:port string that can be passed to tast 77*760c253cSXin Li """ 78*760c253cSXin Li # We have to tell sshwatcher which port we want to use. 79*760c253cSXin Li # We pick a port that is likely to be available. 80*760c253cSXin Li port = random.randrange(4096, 32768) 81*760c253cSXin Li cmd = ["go", "run", str(sshwatcher), machinename, str(port)] 82*760c253cSXin Li # Pylint wants us to use subprocess.Popen as a context manager, 83*760c253cSXin Li # but we don't, so that we can ask sshwatcher to terminate and 84*760c253cSXin Li # limit the time we wait for it to do so. 85*760c253cSXin Li # pylint: disable=consider-using-with 86*760c253cSXin Li proc = subprocess.Popen(cmd, stdout=subprocess.PIPE) 87*760c253cSXin Li try: 88*760c253cSXin Li # sshwatcher takes a few seconds before it binds to the port, 89*760c253cSXin Li # presumably due to SSH handshaking taking a while. 90*760c253cSXin Li # Give it 12 seconds before we ask the client to connect. 91*760c253cSXin Li time.sleep(12) 92*760c253cSXin Li yield f"localhost:{port}" 93*760c253cSXin Li finally: 94*760c253cSXin Li proc.terminate() 95*760c253cSXin Li proc.wait(timeout=5) 96*760c253cSXin Li 97*760c253cSXin Li 98*760c253cSXin Liclass SuiteRunner(object): 99*760c253cSXin Li """This defines the interface from crosperf to test script.""" 100*760c253cSXin Li 101*760c253cSXin Li def __init__( 102*760c253cSXin Li self, 103*760c253cSXin Li dut_config, 104*760c253cSXin Li logger_to_use=None, 105*760c253cSXin Li log_level="verbose", 106*760c253cSXin Li cmd_exec=None, 107*760c253cSXin Li cmd_term=None, 108*760c253cSXin Li ): 109*760c253cSXin Li self.logger = logger_to_use 110*760c253cSXin Li self.log_level = log_level 111*760c253cSXin Li self._ce = cmd_exec or command_executer.GetCommandExecuter( 112*760c253cSXin Li self.logger, log_level=self.log_level 113*760c253cSXin Li ) 114*760c253cSXin Li # DUT command executer. 115*760c253cSXin Li # Will be initialized and used within Run. 116*760c253cSXin Li self._ct = cmd_term or command_executer.CommandTerminator() 117*760c253cSXin Li self.dut_config = dut_config 118*760c253cSXin Li 119*760c253cSXin Li def Run(self, cros_machine, label, benchmark, test_args, profiler_args): 120*760c253cSXin Li machine_name = cros_machine.name 121*760c253cSXin Li for i in range(0, benchmark.retries + 1): 122*760c253cSXin Li if label.crosfleet: 123*760c253cSXin Li ret_tup = self.Crosfleet_Run( 124*760c253cSXin Li label, benchmark, test_args, profiler_args 125*760c253cSXin Li ) 126*760c253cSXin Li else: 127*760c253cSXin Li if benchmark.suite == "tast": 128*760c253cSXin Li with ssh_tunnel( 129*760c253cSXin Li Path(label.chromeos_root, SSHWATCHER), machine_name 130*760c253cSXin Li ) as hostport: 131*760c253cSXin Li ret_tup = self.Tast_Run(hostport, label, benchmark) 132*760c253cSXin Li else: 133*760c253cSXin Li ret_tup = self.Test_That_Run( 134*760c253cSXin Li machine_name, label, benchmark, test_args, profiler_args 135*760c253cSXin Li ) 136*760c253cSXin Li if ret_tup[0] != 0: 137*760c253cSXin Li self.logger.LogOutput( 138*760c253cSXin Li "benchmark %s failed. Retries left: %s" 139*760c253cSXin Li % (benchmark.name, benchmark.retries - i) 140*760c253cSXin Li ) 141*760c253cSXin Li elif i > 0: 142*760c253cSXin Li self.logger.LogOutput( 143*760c253cSXin Li "benchmark %s succeded after %s retries" 144*760c253cSXin Li % (benchmark.name, i) 145*760c253cSXin Li ) 146*760c253cSXin Li break 147*760c253cSXin Li else: 148*760c253cSXin Li self.logger.LogOutput( 149*760c253cSXin Li "benchmark %s succeded on first try" % benchmark.name 150*760c253cSXin Li ) 151*760c253cSXin Li break 152*760c253cSXin Li return ret_tup 153*760c253cSXin Li 154*760c253cSXin Li def RemoveTelemetryTempFile(self, machine, chromeos_root): 155*760c253cSXin Li filename = "telemetry@%s" % machine 156*760c253cSXin Li fullname = misc.GetOutsideChrootPath( 157*760c253cSXin Li chromeos_root, os.path.join("/tmp", filename) 158*760c253cSXin Li ) 159*760c253cSXin Li if os.path.exists(fullname): 160*760c253cSXin Li os.remove(fullname) 161*760c253cSXin Li 162*760c253cSXin Li def GenTestArgs(self, benchmark, test_args, profiler_args): 163*760c253cSXin Li args_list = [] 164*760c253cSXin Li 165*760c253cSXin Li if benchmark.suite != "telemetry_Crosperf" and profiler_args: 166*760c253cSXin Li self.logger.LogFatal( 167*760c253cSXin Li "Tests other than telemetry_Crosperf do not " 168*760c253cSXin Li "support profiler." 169*760c253cSXin Li ) 170*760c253cSXin Li 171*760c253cSXin Li if test_args: 172*760c253cSXin Li # Strip double quotes off args (so we can wrap them in single 173*760c253cSXin Li # quotes, to pass through to Telemetry). 174*760c253cSXin Li if test_args[0] == '"' and test_args[-1] == '"': 175*760c253cSXin Li test_args = test_args[1:-1] 176*760c253cSXin Li args_list.append("test_args='%s'" % test_args) 177*760c253cSXin Li 178*760c253cSXin Li args_list.append(GetDutConfigArgs(self.dut_config)) 179*760c253cSXin Li 180*760c253cSXin Li if not ( 181*760c253cSXin Li benchmark.suite == "telemetry_Crosperf" 182*760c253cSXin Li or benchmark.suite == "crosperf_Wrapper" 183*760c253cSXin Li ): 184*760c253cSXin Li self.logger.LogWarning( 185*760c253cSXin Li "Please make sure the server test has stage for " 186*760c253cSXin Li "device setup.\n" 187*760c253cSXin Li ) 188*760c253cSXin Li else: 189*760c253cSXin Li args_list.append("test=%s" % benchmark.test_name) 190*760c253cSXin Li if benchmark.suite == "telemetry_Crosperf": 191*760c253cSXin Li args_list.append("run_local=%s" % benchmark.run_local) 192*760c253cSXin Li args_list.append(GetProfilerArgs(profiler_args)) 193*760c253cSXin Li 194*760c253cSXin Li return args_list 195*760c253cSXin Li 196*760c253cSXin Li # TODO(zhizhouy): Currently do not support passing arguments or running 197*760c253cSXin Li # customized tast tests, as we do not have such requirements. 198*760c253cSXin Li def Tast_Run(self, machine, label, benchmark): 199*760c253cSXin Li # Remove existing tast results 200*760c253cSXin Li command = "rm -rf /usr/local/autotest/results/*" 201*760c253cSXin Li self._ce.CrosRunCommand( 202*760c253cSXin Li command, machine=machine, chromeos_root=label.chromeos_root 203*760c253cSXin Li ) 204*760c253cSXin Li 205*760c253cSXin Li command = " ".join( 206*760c253cSXin Li [TAST_PATH, "run", "-build=False", machine, benchmark.test_name] 207*760c253cSXin Li ) 208*760c253cSXin Li 209*760c253cSXin Li if self.log_level != "verbose": 210*760c253cSXin Li self.logger.LogOutput("Running test.") 211*760c253cSXin Li self.logger.LogOutput("CMD: %s" % command) 212*760c253cSXin Li 213*760c253cSXin Li return self._ce.ChrootRunCommandWOutput( 214*760c253cSXin Li label.chromeos_root, command, command_terminator=self._ct 215*760c253cSXin Li ) 216*760c253cSXin Li 217*760c253cSXin Li def Test_That_Run( 218*760c253cSXin Li self, machine, label, benchmark, test_args, profiler_args 219*760c253cSXin Li ): 220*760c253cSXin Li """Run the test_that test..""" 221*760c253cSXin Li 222*760c253cSXin Li # Remove existing test_that results 223*760c253cSXin Li command = "rm -rf /usr/local/autotest/results/*" 224*760c253cSXin Li self._ce.CrosRunCommand( 225*760c253cSXin Li command, machine=machine, chromeos_root=label.chromeos_root 226*760c253cSXin Li ) 227*760c253cSXin Li 228*760c253cSXin Li if benchmark.suite == "telemetry_Crosperf": 229*760c253cSXin Li if not os.path.isdir(label.chrome_src): 230*760c253cSXin Li self.logger.LogFatal( 231*760c253cSXin Li "Cannot find chrome src dir to " 232*760c253cSXin Li "run telemetry: %s" % label.chrome_src 233*760c253cSXin Li ) 234*760c253cSXin Li # Check for and remove temporary file that may have been left by 235*760c253cSXin Li # previous telemetry runs (and which might prevent this run from 236*760c253cSXin Li # working). 237*760c253cSXin Li self.RemoveTelemetryTempFile(machine, label.chromeos_root) 238*760c253cSXin Li 239*760c253cSXin Li # --autotest_dir specifies which autotest directory to use. 240*760c253cSXin Li autotest_dir_arg = "--autotest_dir=%s" % ( 241*760c253cSXin Li label.autotest_path if label.autotest_path else AUTOTEST_DIR 242*760c253cSXin Li ) 243*760c253cSXin Li 244*760c253cSXin Li # --fast avoids unnecessary copies of syslogs. 245*760c253cSXin Li fast_arg = "--fast" 246*760c253cSXin Li board_arg = "--board=%s" % label.board 247*760c253cSXin Li 248*760c253cSXin Li args_list = self.GenTestArgs(benchmark, test_args, profiler_args) 249*760c253cSXin Li args_arg = "--args=%s" % pipes.quote(" ".join(args_list)) 250*760c253cSXin Li 251*760c253cSXin Li command = " ".join( 252*760c253cSXin Li [ 253*760c253cSXin Li TEST_THAT_PATH, 254*760c253cSXin Li autotest_dir_arg, 255*760c253cSXin Li fast_arg, 256*760c253cSXin Li board_arg, 257*760c253cSXin Li args_arg, 258*760c253cSXin Li machine, 259*760c253cSXin Li benchmark.suite 260*760c253cSXin Li if ( 261*760c253cSXin Li benchmark.suite == "telemetry_Crosperf" 262*760c253cSXin Li or benchmark.suite == "crosperf_Wrapper" 263*760c253cSXin Li ) 264*760c253cSXin Li else benchmark.test_name, 265*760c253cSXin Li ] 266*760c253cSXin Li ) 267*760c253cSXin Li 268*760c253cSXin Li # Use --no-ns-pid so that cros_sdk does not create a different 269*760c253cSXin Li # process namespace and we can kill process created easily by their 270*760c253cSXin Li # process group. 271*760c253cSXin Li chrome_root_options = ( 272*760c253cSXin Li f"--no-ns-pid " 273*760c253cSXin Li f"--chrome_root={label.chrome_src} --chrome_root_mount={CHROME_MOUNT_DIR} " 274*760c253cSXin Li f'FEATURES="-usersandbox" ' 275*760c253cSXin Li f"CHROME_ROOT={CHROME_MOUNT_DIR}" 276*760c253cSXin Li ) 277*760c253cSXin Li 278*760c253cSXin Li if self.log_level != "verbose": 279*760c253cSXin Li self.logger.LogOutput("Running test.") 280*760c253cSXin Li self.logger.LogOutput("CMD: %s" % command) 281*760c253cSXin Li 282*760c253cSXin Li return self._ce.ChrootRunCommandWOutput( 283*760c253cSXin Li label.chromeos_root, 284*760c253cSXin Li command, 285*760c253cSXin Li command_terminator=self._ct, 286*760c253cSXin Li cros_sdk_options=chrome_root_options, 287*760c253cSXin Li ) 288*760c253cSXin Li 289*760c253cSXin Li def DownloadResult(self, label, task_id): 290*760c253cSXin Li gsutil_cmd = os.path.join(label.chromeos_root, GS_UTIL) 291*760c253cSXin Li result_dir = "gs://chromeos-autotest-results/swarming-%s" % task_id 292*760c253cSXin Li download_path = misc.GetOutsideChrootPath(label.chromeos_root, "/tmp") 293*760c253cSXin Li ls_command = "%s ls %s" % ( 294*760c253cSXin Li gsutil_cmd, 295*760c253cSXin Li os.path.join(result_dir, "autoserv_test"), 296*760c253cSXin Li ) 297*760c253cSXin Li cp_command = "%s -mq cp -r %s %s" % ( 298*760c253cSXin Li gsutil_cmd, 299*760c253cSXin Li result_dir, 300*760c253cSXin Li download_path, 301*760c253cSXin Li ) 302*760c253cSXin Li 303*760c253cSXin Li # Server sometimes will not be able to generate the result directory right 304*760c253cSXin Li # after the test. Will try to access this gs location every 60s for 305*760c253cSXin Li # RETRY_LIMIT mins. 306*760c253cSXin Li t = 0 307*760c253cSXin Li RETRY_LIMIT = 10 308*760c253cSXin Li while t < RETRY_LIMIT: 309*760c253cSXin Li t += 1 310*760c253cSXin Li status = self._ce.RunCommand(ls_command, print_to_console=False) 311*760c253cSXin Li if status == 0: 312*760c253cSXin Li break 313*760c253cSXin Li if t < RETRY_LIMIT: 314*760c253cSXin Li self.logger.LogOutput( 315*760c253cSXin Li "Result directory not generated yet, " 316*760c253cSXin Li "retry (%d) in 60s." % t 317*760c253cSXin Li ) 318*760c253cSXin Li time.sleep(60) 319*760c253cSXin Li else: 320*760c253cSXin Li self.logger.LogOutput( 321*760c253cSXin Li "No result directory for task %s" % task_id 322*760c253cSXin Li ) 323*760c253cSXin Li return status 324*760c253cSXin Li 325*760c253cSXin Li # Wait for 60s to make sure server finished writing to gs location. 326*760c253cSXin Li time.sleep(60) 327*760c253cSXin Li 328*760c253cSXin Li status = self._ce.RunCommand(cp_command) 329*760c253cSXin Li if status != 0: 330*760c253cSXin Li self.logger.LogOutput( 331*760c253cSXin Li "Cannot download results from task %s" % task_id 332*760c253cSXin Li ) 333*760c253cSXin Li else: 334*760c253cSXin Li self.logger.LogOutput("Result downloaded for task %s" % task_id) 335*760c253cSXin Li return status 336*760c253cSXin Li 337*760c253cSXin Li def Crosfleet_Run(self, label, benchmark, test_args, profiler_args): 338*760c253cSXin Li """Run the test via crosfleet..""" 339*760c253cSXin Li options = [] 340*760c253cSXin Li if label.board: 341*760c253cSXin Li options.append("-board=%s" % label.board) 342*760c253cSXin Li if label.build: 343*760c253cSXin Li options.append("-image=%s" % label.build) 344*760c253cSXin Li # TODO: now only put toolchain pool here, user need to be able to specify 345*760c253cSXin Li # which pool to use. Need to request feature to not use this option at all. 346*760c253cSXin Li options.append("-pool=toolchain") 347*760c253cSXin Li 348*760c253cSXin Li args_list = self.GenTestArgs(benchmark, test_args, profiler_args) 349*760c253cSXin Li options.append("-test-args=%s" % pipes.quote(" ".join(args_list))) 350*760c253cSXin Li 351*760c253cSXin Li dimensions = [] 352*760c253cSXin Li for dut in label.remote: 353*760c253cSXin Li dimensions.append("-dim dut_name:%s" % dut.rstrip(".cros")) 354*760c253cSXin Li 355*760c253cSXin Li command = ("%s create-test %s %s %s") % ( 356*760c253cSXin Li CROSFLEET_PATH, 357*760c253cSXin Li " ".join(dimensions), 358*760c253cSXin Li " ".join(options), 359*760c253cSXin Li benchmark.suite 360*760c253cSXin Li if ( 361*760c253cSXin Li benchmark.suite == "telemetry_Crosperf" 362*760c253cSXin Li or benchmark.suite == "crosperf_Wrapper" 363*760c253cSXin Li ) 364*760c253cSXin Li else benchmark.test_name, 365*760c253cSXin Li ) 366*760c253cSXin Li 367*760c253cSXin Li if self.log_level != "verbose": 368*760c253cSXin Li self.logger.LogOutput("Starting crosfleet test.") 369*760c253cSXin Li self.logger.LogOutput("CMD: %s" % command) 370*760c253cSXin Li ret_tup = self._ce.RunCommandWOutput( 371*760c253cSXin Li command, command_terminator=self._ct 372*760c253cSXin Li ) 373*760c253cSXin Li 374*760c253cSXin Li if ret_tup[0] != 0: 375*760c253cSXin Li self.logger.LogOutput("Crosfleet test not created successfully.") 376*760c253cSXin Li return ret_tup 377*760c253cSXin Li 378*760c253cSXin Li # Std output of the command will look like: 379*760c253cSXin Li # Created request at https://ci.chromium.org/../cros_test_platform/b12345 380*760c253cSXin Li # We want to parse it and get the id number of the task, which is the 381*760c253cSXin Li # number in the very end of the link address. 382*760c253cSXin Li task_id = ret_tup[1].strip().split("b")[-1] 383*760c253cSXin Li 384*760c253cSXin Li command = "crosfleet wait-task %s" % task_id 385*760c253cSXin Li if self.log_level != "verbose": 386*760c253cSXin Li self.logger.LogOutput("Waiting for crosfleet test to finish.") 387*760c253cSXin Li self.logger.LogOutput("CMD: %s" % command) 388*760c253cSXin Li 389*760c253cSXin Li ret_tup = self._ce.RunCommandWOutput( 390*760c253cSXin Li command, command_terminator=self._ct 391*760c253cSXin Li ) 392*760c253cSXin Li 393*760c253cSXin Li # The output of `wait-task` command will be a combination of verbose and a 394*760c253cSXin Li # json format result in the end. The json result looks like this: 395*760c253cSXin Li # {"task-result": 396*760c253cSXin Li # {"name":"Test Platform Invocation", 397*760c253cSXin Li # "state":"", "failure":false, "success":true, 398*760c253cSXin Li # "task-run-id":"12345", 399*760c253cSXin Li # "task-run-url":"https://ci.chromium.org/.../cros_test_platform/b12345", 400*760c253cSXin Li # "task-logs-url":"" 401*760c253cSXin Li # }, 402*760c253cSXin Li # "stdout":"", 403*760c253cSXin Li # "child-results": 404*760c253cSXin Li # [{"name":"graphics_WebGLAquarium", 405*760c253cSXin Li # "state":"", "failure":false, "success":true, "task-run-id":"", 406*760c253cSXin Li # "task-run-url":"https://chromeos-swarming.appspot.com/task?id=1234", 407*760c253cSXin Li # "task-logs-url":"https://stainless.corp.google.com/1234/"} 408*760c253cSXin Li # ] 409*760c253cSXin Li # } 410*760c253cSXin Li # We need the task id of the child-results to download result. 411*760c253cSXin Li output = json.loads(ret_tup[1].split("\n")[-1]) 412*760c253cSXin Li output = output["child-results"][0] 413*760c253cSXin Li if output["success"]: 414*760c253cSXin Li task_id = output["task-run-url"].split("=")[-1] 415*760c253cSXin Li if self.DownloadResult(label, task_id) == 0: 416*760c253cSXin Li result_dir = "\nResults placed in tmp/swarming-%s\n" % task_id 417*760c253cSXin Li return (ret_tup[0], result_dir, ret_tup[2]) 418*760c253cSXin Li return ret_tup 419*760c253cSXin Li 420*760c253cSXin Li def CommandTerminator(self): 421*760c253cSXin Li return self._ct 422*760c253cSXin Li 423*760c253cSXin Li def Terminate(self): 424*760c253cSXin Li self._ct.Terminate() 425*760c253cSXin Li 426*760c253cSXin Li 427*760c253cSXin Liclass MockSuiteRunner(object): 428*760c253cSXin Li """Mock suite runner for test.""" 429*760c253cSXin Li 430*760c253cSXin Li def __init__(self): 431*760c253cSXin Li self._true = True 432*760c253cSXin Li 433*760c253cSXin Li def Run(self, *_args): 434*760c253cSXin Li if self._true: 435*760c253cSXin Li return [0, "", ""] 436*760c253cSXin Li else: 437*760c253cSXin Li return [0, "", ""] 438