xref: /aosp_15_r20/external/toolchain-utils/crosperf/suite_runner.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
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