test_toolchains.py revision 364131220999a99b408c5ef848da17844028c604
1#!/usr/bin/python
2
3# Script to test different toolchains against ChromeOS benchmarks.
4import datetime
5import optparse
6import os
7import sys
8import build_chromeos
9import setup_chromeos
10import time
11from utils import command_executer
12from utils import misc
13from utils import logger
14
15
16WEEKLY_REPORTS_ROOT="/usr/local/google/crostc/weekly_test_data"
17AFDO_BOARDS = ["butterfly", "lumpy", "stumpy", "stout", "parrot", "parrot_ivb"]
18MAIL_PROGRAM = "~/var/bin/mail-sheriff"
19
20class GCCConfig(object):
21  def __init__(self, githash):
22    self.githash = githash
23
24
25class ToolchainConfig:
26  def __init__(self, gcc_config=None, binutils_config=None):
27    self.gcc_config = gcc_config
28
29
30class ChromeOSCheckout(object):
31  def __init__(self, board, chromeos_root):
32    self._board = board
33    self._chromeos_root = chromeos_root
34    self._ce = command_executer.GetCommandExecuter()
35    self._l = logger.GetLogger()
36    self._build_num = None
37
38  def _DeleteChroot(self):
39    command = "cd %s; cros_sdk --delete" % self._chromeos_root
40    return self._ce.RunCommand(command)
41
42  def _DeleteCcahe(self):
43    # crosbug.com/34956
44    command = "sudo rm -rf %s" % os.path.join(self._chromeos_root, ".cache")
45    return self._ce.RunCommand(command)
46
47  def _GetBuildNumber(self):
48    """ This function assumes a ChromeOS image has been built in the chroot.
49    It translates the 'latest' symlink in the
50    <chroot>/src/build/images/<board> directory, to find the actual
51    ChromeOS build number for the image that was built.  For example, if
52    src/build/image/lumpy/latest ->  R37-5982.0.2014_06_23_0454-a1, then
53    This function would parse it out and assign 'R37-5982' to self._build_num.
54    This is used to determine the official, vanilla build to use for
55    comparison tests.
56    """
57    # Get the path to 'latest'
58    sym_path = os.path.join (misc.GetImageDir(self._chromeos_root,
59                                              self._board),
60                             "latest")
61    # Translate the symlink to its 'real' path.
62    real_path = os.path.realpath(sym_path)
63    # Break up the path and get the last piece
64    # (e.g. 'R37-5982.0.2014_06_23_0454-a1"
65    path_pieces = real_path.split("/")
66    last_piece = path_pieces[-1]
67    # Break this piece into the image number + other pieces, and get the
68    # image number [ 'R37-5982', '0', '2014_06_23_0454-a1']
69    image_parts = last_piece.split(".")
70    self._build_num = image_parts[0]
71
72  def _BuildAndImage(self, label=""):
73    if (not label or
74        not misc.DoesLabelExist(self._chromeos_root, self._board, label)):
75      build_chromeos_args = [build_chromeos.__file__,
76                             "--chromeos_root=%s" % self._chromeos_root,
77                             "--board=%s" % self._board,
78                             "--rebuild"]
79      if self._public:
80        build_chromeos_args.append("--env=USE=-chrome_internal")
81
82      if self._board in AFDO_BOARDS:
83        build_chromeos_args.append("--env=USE=afdo_use")
84
85      ret = build_chromeos.Main(build_chromeos_args)
86      if ret != 0:
87        raise RuntimeError("Couldn't build ChromeOS!")
88
89      if not  self._build_num:
90        self._GetBuildNumber()
91      # Check to see if we need to create the symbolic link for the vanilla
92      # image, and do so if appropriate.
93      if not misc.DoesLabelExist(self._chromeos_root, self._board, "vanilla"):
94        build_name = "%s-release/%s.0.0" % (self._board, self._build_num)
95        full_vanilla_path = os.path.join (os.getcwd(), self._chromeos_root,
96                                          'chroot/tmp', build_name)
97        misc.LabelLatestImage(self._chromeos_root, self._board, label,
98                              full_vanilla_path)
99      else:
100        misc.LabelLatestImage(self._chromeos_root, self._board, label)
101    return label
102
103  def _SetupBoard(self, env_dict, usepkg_flag, clobber_flag):
104    env_string = misc.GetEnvStringFromDict(env_dict)
105    command = ("%s %s" %
106               (env_string,
107                misc.GetSetupBoardCommand(self._board,
108                                          usepkg=usepkg_flag,
109                                          force=clobber_flag)))
110    ret = self._ce.ChrootRunCommand(self._chromeos_root,
111                                    command)
112    error_str = "Could not setup board: '%s'" % command
113    assert ret == 0, error_str
114
115  def _UnInstallToolchain(self):
116    command = ("sudo CLEAN_DELAY=0 emerge -C cross-%s/gcc" %
117               misc.GetCtargetFromBoard(self._board,
118                                   self._chromeos_root))
119    ret = self._ce.ChrootRunCommand(self._chromeos_root,
120                                    command)
121    if ret != 0:
122      raise RuntimeError("Couldn't uninstall the toolchain!")
123
124  def _CheckoutChromeOS(self):
125    # TODO(asharif): Setup a fixed ChromeOS version (quarterly snapshot).
126    if not os.path.exists(self._chromeos_root):
127      setup_chromeos_args = [setup_chromeos.__file__,
128                             "--dir=%s" % self._chromeos_root]
129      if self._public:
130        setup_chromeos_args.append("--public")
131      ret = setup_chromeos.Main(setup_chromeos_args)
132      if ret != 0:
133        raise RuntimeError("Couldn't run setup_chromeos!")
134
135
136  def _BuildToolchain(self, config):
137    # Call setup_board for basic, vanilla setup.
138    self._SetupBoard({}, usepkg_flag=True, clobber_flag=False)
139    # Now uninstall the vanilla compiler and setup/build our custom
140    # compiler.
141    self._UnInstallToolchain()
142    envdict = {"USE": "git_gcc",
143               "GCC_GITHASH": config.gcc_config.githash,
144               "EMERGE_DEFAULT_OPTS": "--exclude=gcc"}
145    self._SetupBoard(envdict, usepkg_flag=False, clobber_flag=False)
146
147
148class ToolchainComparator(ChromeOSCheckout):
149  def __init__(self, board, remotes, configs, clean,
150               public, force_mismatch, schedv2=False):
151    self._board = board
152    self._remotes = remotes
153    self._chromeos_root = "chromeos"
154    self._configs = configs
155    self._clean = clean
156    self._public = public
157    self._force_mismatch = force_mismatch
158    self._ce = command_executer.GetCommandExecuter()
159    self._l = logger.GetLogger()
160    timestamp = datetime.datetime.strftime(datetime.datetime.now(),
161                                           "%Y-%m-%d_%H:%M:%S")
162    self._reports_dir = os.path.join(
163        os.path.expanduser("~/nightly_test_reports"),
164        "%s.%s" % (timestamp, board),
165        )
166    self._schedv2 = schedv2
167    ChromeOSCheckout.__init__(self, board, self._chromeos_root)
168
169
170  def _FinishSetup(self):
171    # Get correct .boto file
172    current_dir = os.getcwd()
173    src = "/home/mobiletc-prebuild/.boto"
174    dest = os.path.join(current_dir, self._chromeos_root,
175                        "src/private-overlays/chromeos-overlay/"
176                        "googlestorage_account.boto")
177    # Copy the file to the correct place
178    copy_cmd = "cp %s %s" % (src, dest)
179    retval = self._ce.RunCommand(copy_cmd)
180    if retval != 0:
181      raise RuntimeError("Couldn't copy .boto file for google storage.")
182
183    # Fix protections on ssh key
184    command = ("chmod 600 /var/cache/chromeos-cache/distfiles/target"
185               "/chrome-src-internal/src/third_party/chromite/ssh_keys"
186               "/testing_rsa")
187    retval = self._ce.ChrootRunCommand(self._chromeos_root, command)
188    if retval != 0:
189      raise RuntimeError("chmod for testing_rsa failed")
190
191  def _TestLabels(self, labels):
192    experiment_file = "toolchain_experiment.txt"
193    image_args = ""
194    if self._force_mismatch:
195      image_args = "--force-mismatch"
196    experiment_header = """
197    board: %s
198    remote: %s
199    retries: 1
200    """ % (self._board, self._remotes)
201    experiment_tests = """
202    benchmark: all_toolchain_perf {
203      suite: telemetry_Crosperf
204      iterations: 3
205    }
206    """
207    with open(experiment_file, "w") as f:
208      print >>f, experiment_header
209      print >>f, experiment_tests
210      for label in labels:
211        # TODO(asharif): Fix crosperf so it accepts labels with symbols
212        crosperf_label = label
213        crosperf_label = crosperf_label.replace("-", "minus")
214        crosperf_label = crosperf_label.replace("+", "plus")
215        crosperf_label = crosperf_label.replace(".", "")
216
217        # Use the official build instead of building vanilla ourselves.
218        if label == "vanilla":
219          build_name = '%s-release/%s.0.0' % (self._board, self._build_num)
220
221          # Now add 'official build' to test file.
222          official_image = """
223          official_image {
224            chromeos_root: %s
225            build: %s
226          }
227          """ % (self._chromeos_root, build_name)
228          print >>f, official_image
229
230        else:
231          experiment_image = """
232          %s {
233            chromeos_image: %s
234            image_args: %s
235          }
236          """ % (crosperf_label,
237                 os.path.join(misc.GetImageDir(self._chromeos_root,
238                                               self._board),
239                              label, "chromiumos_test_image.bin"),
240                 image_args)
241          print >>f, experiment_image
242
243    crosperf = os.path.join(os.path.dirname(__file__),
244                            "crosperf",
245                            "crosperf")
246    schedv2_opts = '--schedv2 --logging_level=verbose' if self._schedv2 else ''
247    command = ("{crosperf} --no_email=True --results_dir={r_dir} "
248               "{schedv2_opts} {exp_file}").format(
249                crosperf=crosperf,
250                r_dir=self._reports_dir,
251                schedv2_opts=schedv2_opts,
252                exp_file=experiment_file)
253
254    ret = self._ce.RunCommand(command)
255    if ret != 0:
256      raise RuntimeError("Couldn't run crosperf!")
257    return
258
259
260  def _CopyWeeklyReportFiles(self, labels):
261    """Create tar files of the custom and official images and copy them
262    to the weekly reports directory, so they exist when the weekly report
263    gets generated.  IMPORTANT NOTE: This function must run *after*
264    crosperf has been run; otherwise the vanilla images will not be there.
265    """
266    images_path = os.path.join(os.path.realpath(self._chromeos_root),
267                               "src/build/images", self._board)
268    weekday = time.strftime("%a")
269    data_dir = os.path.join(WEEKLY_REPORTS_ROOT, self._board)
270    dest_dir = os.path.join (data_dir, weekday)
271    if not os.path.exists(dest_dir):
272      os.makedirs(dest_dir)
273    # Make sure dest_dir is empty (clean out last week's data).
274    cmd = "cd %s; rm -Rf %s_*_image*" % (dest_dir, weekday)
275    self._ce.RunCommand(cmd)
276    # Now create new tar files and copy them over.
277    for l in labels:
278      test_path = os.path.join(images_path, l)
279      if os.path.exists(test_path):
280        if l != "vanilla":
281          label_name = "test"
282        else:
283          label_name = "vanilla"
284        tar_file_name = "%s_%s_image.tar" % (weekday, label_name)
285        cmd = ("cd %s; tar -cvf %s %s/chromiumos_test_image.bin; "
286               "cp %s %s/.") % (images_path,
287                                tar_file_name,
288                                l, tar_file_name,
289                                dest_dir)
290        tar_ret = self._ce.RunCommand(cmd)
291        if tar_ret != 0:
292          self._l.LogOutput("Error while creating/copying test tar file(%s)."
293                            % tar_file_name)
294
295  def _SendEmail(self):
296    """Find email msesage generated by crosperf and send it."""
297    filename = os.path.join(self._reports_dir, "msg_body.html")
298    if (os.path.exists(filename) and
299        os.path.exists(os.path.expanduser(MAIL_PROGRAM))):
300      command = ('cat %s | %s -s "Nightly test results, %s" -team -html'
301                 % (filename, MAIL_PROGRAM, self._board))
302      self._ce.RunCommand(command)
303
304  def DoAll(self):
305    self._CheckoutChromeOS()
306    labels = []
307    labels.append("vanilla")
308    for config in self._configs:
309      label = misc.GetFilenameFromString(config.gcc_config.githash)
310      if (not misc.DoesLabelExist(self._chromeos_root,
311                                  self._board,
312                                  label)):
313        self._BuildToolchain(config)
314        label = self._BuildAndImage(label)
315      labels.append(label)
316    self._FinishSetup()
317    self._TestLabels(labels)
318    self._SendEmail()
319    # Only try to copy the image files if the test runs ran successfully.
320    self._CopyWeeklyReportFiles(labels)
321    if self._clean:
322      ret = self._DeleteChroot()
323      if ret != 0:
324        return ret
325      ret = self._DeleteCcahe()
326      if ret != 0:
327        return ret
328    return 0
329
330
331def Main(argv):
332  """The main function."""
333  # Common initializations
334###  command_executer.InitCommandExecuter(True)
335  command_executer.InitCommandExecuter()
336  parser = optparse.OptionParser()
337  parser.add_option("--remote",
338                    dest="remote",
339                    help="Remote machines to run tests on.")
340  parser.add_option("--board",
341                    dest="board",
342                    default="x86-zgb",
343                    help="The target board.")
344  parser.add_option("--githashes",
345                    dest="githashes",
346                    default="master",
347                    help="The gcc githashes to test.")
348  parser.add_option("--clean",
349                    dest="clean",
350                    default=False,
351                    action="store_true",
352                    help="Clean the chroot after testing.")
353  parser.add_option("--public",
354                    dest="public",
355                    default=False,
356                    action="store_true",
357                    help="Use the public checkout/build.")
358  parser.add_option("--force-mismatch",
359                    dest="force_mismatch",
360                    default="",
361                    help="Force the image regardless of board mismatch")
362  parser.add_option("--schedv2",
363                    dest="schedv2",
364                    action="store_true",
365                    default=False,
366                    help="Pass --schedv2 to crosperf.")
367  options, _ = parser.parse_args(argv)
368  if not options.board:
369    print "Please give a board."
370    return 1
371  if not options.remote:
372    print "Please give at least one remote machine."
373    return 1
374  toolchain_configs = []
375  for githash in options.githashes.split(","):
376    gcc_config = GCCConfig(githash=githash)
377    toolchain_config = ToolchainConfig(gcc_config=gcc_config)
378    toolchain_configs.append(toolchain_config)
379  fc = ToolchainComparator(options.board, options.remote, toolchain_configs,
380                           options.clean, options.public,
381                           options.force_mismatch,
382                           options.schedv2)
383  return fc.DoAll()
384
385
386if __name__ == "__main__":
387  retval = Main(sys.argv)
388  sys.exit(retval)
389