• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# Copyright 2020 The ChromiumOS Authors
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""The unified package/object bisecting tool."""
8
9
10import abc
11import argparse
12from argparse import RawTextHelpFormatter
13import os
14import shlex
15import sys
16
17from binary_search_tool import binary_search_state
18from binary_search_tool import common
19from cros_utils import command_executer
20from cros_utils import logger
21
22
23class Bisector(object, metaclass=abc.ABCMeta):
24    """The abstract base class for Bisectors."""
25
26    def __init__(self, options, overrides=None):
27        """Constructor for Bisector abstract base class
28
29        Args:
30          options: positional arguments for specific mode (board, remote, etc.)
31          overrides: optional dict of overrides for argument defaults
32        """
33        self.options = options
34        self.overrides = overrides
35        if not overrides:
36            self.overrides = {}
37        self.logger = logger.GetLogger()
38        self.ce = command_executer.GetCommandExecuter()
39
40    def _PrettyPrintArgs(self, args, overrides):
41        """Output arguments in a nice, human readable format
42
43        Will print and log all arguments for the bisecting tool and make note of
44        which arguments have been overridden.
45
46        Example output:
47          ./run_bisect.py package daisy 172.17.211.184 -I "" -t cros_pkg/my_test.sh
48          Performing ChromeOS Package bisection
49          Method Config:
50            board : daisy
51           remote : 172.17.211.184
52
53          Bisection Config: (* = overridden)
54             get_initial_items : cros_pkg/get_initial_items.sh
55                switch_to_good : cros_pkg/switch_to_good.sh
56                 switch_to_bad : cros_pkg/switch_to_bad.sh
57           * test_setup_script :
58           *       test_script : cros_pkg/my_test.sh
59                         prune : True
60                 noincremental : False
61                     file_args : True
62
63        Args:
64          args: The args to be given to binary_search_state.Run. This represents
65                how the bisection tool will run (with overridden arguments already
66                added in).
67          overrides: The dict of overriden arguments provided by the user. This is
68                     provided so the user can be told which arguments were
69                     overriden and with what value.
70        """
71        # Output method config (board, remote, etc.)
72        options = vars(self.options)
73        out = "\nPerforming %s bisection\n" % self.method_name
74        out += "Method Config:\n"
75        max_key_len = max([len(str(x)) for x in options.keys()])
76        for key in sorted(options):
77            val = options[key]
78            key_str = str(key).rjust(max_key_len)
79            val_str = str(val)
80            out += " %s : %s\n" % (key_str, val_str)
81
82        # Output bisection config (scripts, prune, etc.)
83        out += "\nBisection Config: (* = overridden)\n"
84        max_key_len = max([len(str(x)) for x in args.keys()])
85        # Print args in common._ArgsDict order
86        args_order = [x["dest"] for x in common.GetArgsDict().values()]
87        for key in sorted(args, key=args_order.index):
88            val = args[key]
89            key_str = str(key).rjust(max_key_len)
90            val_str = str(val)
91            changed_str = "*" if key in overrides else " "
92
93            out += " %s %s : %s\n" % (changed_str, key_str, val_str)
94
95        out += "\n"
96        self.logger.LogOutput(out)
97
98    def ArgOverride(self, args, overrides, pretty_print=True):
99        """Override arguments based on given overrides and provide nice output
100
101        Args:
102          args: dict of arguments to be passed to binary_search_state.Run (runs
103                dict.update, causing args to be mutated).
104          overrides: dict of arguments to update args with
105          pretty_print: if True print out args/overrides to user in pretty format
106        """
107        args.update(overrides)
108        if pretty_print:
109            self._PrettyPrintArgs(args, overrides)
110
111    @abc.abstractmethod
112    def PreRun(self):
113        pass
114
115    @abc.abstractmethod
116    def Run(self):
117        pass
118
119    @abc.abstractmethod
120    def PostRun(self):
121        pass
122
123
124class BisectPackage(Bisector):
125    """The class for package bisection steps."""
126
127    cros_pkg_setup = "cros_pkg/setup.sh"
128    cros_pkg_cleanup = "cros_pkg/%s_cleanup.sh"
129
130    def __init__(self, options, overrides):
131        super(BisectPackage, self).__init__(options, overrides)
132        self.method_name = "ChromeOS Package"
133        self.default_kwargs = {
134            "get_initial_items": "cros_pkg/get_initial_items.sh",
135            "switch_to_good": "cros_pkg/switch_to_good.sh",
136            "switch_to_bad": "cros_pkg/switch_to_bad.sh",
137            "test_setup_script": "cros_pkg/test_setup.sh",
138            "test_script": "cros_pkg/interactive_test.sh",
139            "noincremental": False,
140            "prune": True,
141            "file_args": True,
142        }
143        self.setup_cmd = " ".join(
144            (self.cros_pkg_setup, self.options.board, self.options.remote)
145        )
146        self.ArgOverride(self.default_kwargs, self.overrides)
147
148    def PreRun(self):
149        ret, _, _ = self.ce.RunCommandWExceptionCleanup(
150            self.setup_cmd, print_to_console=True
151        )
152        if ret:
153            self.logger.LogError(
154                "Package bisector setup failed w/ error %d" % ret
155            )
156            return 1
157        return 0
158
159    def Run(self):
160        return binary_search_state.Run(**self.default_kwargs)
161
162    def PostRun(self):
163        cmd = self.cros_pkg_cleanup % self.options.board
164        ret, _, _ = self.ce.RunCommandWExceptionCleanup(
165            cmd, print_to_console=True
166        )
167        if ret:
168            self.logger.LogError(
169                "Package bisector cleanup failed w/ error %d" % ret
170            )
171            return 1
172
173        self.logger.LogOutput(
174            (
175                "Cleanup successful! To restore the bisection "
176                "environment run the following:\n"
177                "  cd %s; %s"
178            )
179            % (os.getcwd(), self.setup_cmd)
180        )
181        return 0
182
183
184class BisectObject(Bisector):
185    """The class for object bisection steps."""
186
187    sysroot_wrapper_setup = "sysroot_wrapper/setup.sh"
188    sysroot_wrapper_cleanup = "sysroot_wrapper/cleanup.sh"
189
190    def __init__(self, options, overrides):
191        super(BisectObject, self).__init__(options, overrides)
192        self.method_name = "ChromeOS Object"
193        self.default_kwargs = {
194            "get_initial_items": "sysroot_wrapper/get_initial_items.sh",
195            "switch_to_good": "sysroot_wrapper/switch_to_good.sh",
196            "switch_to_bad": "sysroot_wrapper/switch_to_bad.sh",
197            "test_setup_script": "sysroot_wrapper/test_setup.sh",
198            "test_script": "sysroot_wrapper/interactive_test.sh",
199            "noincremental": False,
200            "prune": True,
201            "file_args": True,
202        }
203        self.options = options
204        if options.dir:
205            os.environ["BISECT_DIR"] = options.dir
206        self.options.dir = os.environ.get("BISECT_DIR", "/tmp/sysroot_bisect")
207        self.setup_cmd = " ".join(
208            (
209                self.sysroot_wrapper_setup,
210                self.options.board,
211                self.options.remote,
212                self.options.package,
213                str(self.options.reboot).lower(),
214                shlex.quote(self.options.use_flags),
215            )
216        )
217
218        self.ArgOverride(self.default_kwargs, overrides)
219
220    def PreRun(self):
221        ret, _, _ = self.ce.RunCommandWExceptionCleanup(
222            self.setup_cmd, print_to_console=True
223        )
224        if ret:
225            self.logger.LogError(
226                "Object bisector setup failed w/ error %d" % ret
227            )
228            return 1
229
230        os.environ["BISECT_STAGE"] = "TRIAGE"
231        return 0
232
233    def Run(self):
234        return binary_search_state.Run(**self.default_kwargs)
235
236    def PostRun(self):
237        cmd = self.sysroot_wrapper_cleanup
238        ret, _, _ = self.ce.RunCommandWExceptionCleanup(
239            cmd, print_to_console=True
240        )
241        if ret:
242            self.logger.LogError(
243                "Object bisector cleanup failed w/ error %d" % ret
244            )
245            return 1
246        self.logger.LogOutput(
247            (
248                "Cleanup successful! To restore the bisection "
249                "environment run the following:\n"
250                "  cd %s; %s"
251            )
252            % (os.getcwd(), self.setup_cmd)
253        )
254        return 0
255
256
257class BisectAndroid(Bisector):
258    """The class for Android bisection steps."""
259
260    android_setup = "android/setup.sh"
261    android_cleanup = "android/cleanup.sh"
262    default_dir = os.path.expanduser("~/ANDROID_BISECT")
263
264    def __init__(self, options, overrides):
265        super(BisectAndroid, self).__init__(options, overrides)
266        self.method_name = "Android"
267        self.default_kwargs = {
268            "get_initial_items": "android/get_initial_items.sh",
269            "switch_to_good": "android/switch_to_good.sh",
270            "switch_to_bad": "android/switch_to_bad.sh",
271            "test_setup_script": "android/test_setup.sh",
272            "test_script": "android/interactive_test.sh",
273            "prune": True,
274            "file_args": True,
275            "noincremental": False,
276        }
277        self.options = options
278        if options.dir:
279            os.environ["BISECT_DIR"] = options.dir
280        self.options.dir = os.environ.get("BISECT_DIR", self.default_dir)
281
282        num_jobs = "NUM_JOBS='%s'" % self.options.num_jobs
283        device_id = ""
284        if self.options.device_id:
285            device_id = "ANDROID_SERIAL='%s'" % self.options.device_id
286
287        self.setup_cmd = " ".join(
288            (num_jobs, device_id, self.android_setup, self.options.android_src)
289        )
290
291        self.ArgOverride(self.default_kwargs, overrides)
292
293    def PreRun(self):
294        ret, _, _ = self.ce.RunCommandWExceptionCleanup(
295            self.setup_cmd, print_to_console=True
296        )
297        if ret:
298            self.logger.LogError(
299                "Android bisector setup failed w/ error %d" % ret
300            )
301            return 1
302
303        os.environ["BISECT_STAGE"] = "TRIAGE"
304        return 0
305
306    def Run(self):
307        return binary_search_state.Run(**self.default_kwargs)
308
309    def PostRun(self):
310        cmd = self.android_cleanup
311        ret, _, _ = self.ce.RunCommandWExceptionCleanup(
312            cmd, print_to_console=True
313        )
314        if ret:
315            self.logger.LogError(
316                "Android bisector cleanup failed w/ error %d" % ret
317            )
318            return 1
319        self.logger.LogOutput(
320            (
321                "Cleanup successful! To restore the bisection "
322                "environment run the following:\n"
323                "  cd %s; %s"
324            )
325            % (os.getcwd(), self.setup_cmd)
326        )
327        return 0
328
329
330def Run(bisector):
331    log = logger.GetLogger()
332
333    log.LogOutput("Setting up Bisection tool")
334    ret = bisector.PreRun()
335    if ret:
336        return ret
337
338    log.LogOutput("Running Bisection tool")
339    ret = bisector.Run()
340    if ret:
341        return ret
342
343    log.LogOutput("Cleaning up Bisection tool")
344    ret = bisector.PostRun()
345    if ret:
346        return ret
347
348    return 0
349
350
351_HELP_EPILOG = """
352Run ./run_bisect.py {method} --help for individual method help/args
353
354------------------
355
356See README.bisect for examples on argument overriding
357
358See below for full override argument reference:
359"""
360
361
362def Main(argv):
363    override_parser = argparse.ArgumentParser(
364        add_help=False,
365        argument_default=argparse.SUPPRESS,
366        usage="run_bisect.py {mode} [options]",
367    )
368    common.BuildArgParser(override_parser, override=True)
369
370    epilog = _HELP_EPILOG + override_parser.format_help()
371    parser = argparse.ArgumentParser(
372        epilog=epilog, formatter_class=RawTextHelpFormatter
373    )
374    subparsers = parser.add_subparsers(
375        title="Bisect mode",
376        description=(
377            "Which bisection method to "
378            "use. Each method has "
379            "specific setup and "
380            "arguments. Please consult "
381            "the README for more "
382            "information."
383        ),
384    )
385
386    parser_package = subparsers.add_parser("package")
387    parser_package.add_argument("board", help="Board to target")
388    parser_package.add_argument("remote", help="Remote machine to test on")
389    parser_package.set_defaults(handler=BisectPackage)
390
391    parser_object = subparsers.add_parser("object")
392    parser_object.add_argument("board", help="Board to target")
393    parser_object.add_argument("remote", help="Remote machine to test on")
394    parser_object.add_argument("package", help="Package to emerge and test")
395    parser_object.add_argument(
396        "--use_flags",
397        required=False,
398        default="",
399        help="Use flags passed to emerge",
400    )
401    parser_object.add_argument(
402        "--noreboot",
403        action="store_false",
404        dest="reboot",
405        help="Do not reboot after updating the package (default: False)",
406    )
407    parser_object.add_argument(
408        "--dir",
409        help=(
410            "Bisection directory to use, sets "
411            "$BISECT_DIR if provided. Defaults to "
412            "current value of $BISECT_DIR (or "
413            "/tmp/sysroot_bisect if $BISECT_DIR is "
414            "empty)."
415        ),
416    )
417    parser_object.set_defaults(handler=BisectObject)
418
419    parser_android = subparsers.add_parser("android")
420    parser_android.add_argument(
421        "android_src", help="Path to android source tree"
422    )
423    parser_android.add_argument(
424        "--dir",
425        help=(
426            "Bisection directory to use, sets "
427            "$BISECT_DIR if provided. Defaults to "
428            "current value of $BISECT_DIR (or "
429            "~/ANDROID_BISECT/ if $BISECT_DIR is "
430            "empty)."
431        ),
432    )
433    parser_android.add_argument(
434        "-j",
435        "--num_jobs",
436        type=int,
437        default=1,
438        help=(
439            "Number of jobs that make and various "
440            "scripts for bisector can spawn. Setting "
441            "this value too high can freeze up your "
442            "machine!"
443        ),
444    )
445    parser_android.add_argument(
446        "--device_id",
447        default="",
448        help=(
449            "Device id for device used for testing. "
450            "Use this if you have multiple Android "
451            "devices plugged into your machine."
452        ),
453    )
454    parser_android.set_defaults(handler=BisectAndroid)
455
456    options, remaining = parser.parse_known_args(argv)
457    if remaining:
458        overrides = override_parser.parse_args(remaining)
459        overrides = vars(overrides)
460    else:
461        overrides = {}
462
463    subcmd = options.handler
464    del options.handler
465
466    bisector = subcmd(options, overrides)
467    return Run(bisector)
468
469
470if __name__ == "__main__":
471    os.chdir(os.path.dirname(__file__))
472    sys.exit(Main(sys.argv[1:]))
473