test_toolchains.py revision d1f03b83a267caa1e92d8d04c483c5ec0a8c5355
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, public, force_mismatch):
150    self._board = board
151    self._remotes = remotes
152    self._chromeos_root = "chromeos"
153    self._configs = configs
154    self._clean = clean
155    self._public = public
156    self._force_mismatch = force_mismatch
157    self._ce = command_executer.GetCommandExecuter()
158    self._l = logger.GetLogger()
159    timestamp = datetime.datetime.strftime(datetime.datetime.now(),
160                                           "%Y-%m-%d_%H:%M:%S")
161    self._reports_dir = os.path.join(
162        os.path.expanduser("~/nightly_test_reports"),
163        "%s.%s" % (timestamp, board),
164        )
165    ChromeOSCheckout.__init__(self, board, self._chromeos_root)
166
167
168  def _FinishSetup(self):
169    # Get correct .boto file
170    current_dir = os.getcwd()
171    src = "/home/mobiletc-prebuild/.boto"
172    dest = os.path.join(current_dir, self._chromeos_root,
173                        "src/private-overlays/chromeos-overlay/"
174                        "googlestorage_account.boto")
175    # Copy the file to the correct place
176    copy_cmd = "cp %s %s" % (src, dest)
177    retval = self._ce.RunCommand(copy_cmd)
178    if retval != 0:
179      raise RuntimeError("Couldn't copy .boto file for google storage.")
180
181    # Fix protections on ssh key
182    command = ("chmod 600 /var/cache/chromeos-cache/distfiles/target"
183               "/chrome-src-internal/src/third_party/chromite/ssh_keys"
184               "/testing_rsa")
185    retval = self._ce.ChrootRunCommand(self._chromeos_root, command)
186    if retval != 0:
187      raise RuntimeError("chmod for testing_rsa failed")
188
189  def _TestLabels(self, labels):
190    experiment_file = "toolchain_experiment.txt"
191    image_args = ""
192    if self._force_mismatch:
193      image_args = "--force-mismatch"
194    experiment_header = """
195    board: %s
196    remote: %s
197    retries: 1
198    """ % (self._board, self._remotes)
199    experiment_tests = """
200    benchmark: all_toolchain_perf {
201      suite: telemetry_Crosperf
202      iterations: 3
203    }
204    """
205    with open(experiment_file, "w") as f:
206      print >>f, experiment_header
207      print >>f, experiment_tests
208      for label in labels:
209        # TODO(asharif): Fix crosperf so it accepts labels with symbols
210        crosperf_label = label
211        crosperf_label = crosperf_label.replace("-", "minus")
212        crosperf_label = crosperf_label.replace("+", "plus")
213        crosperf_label = crosperf_label.replace(".", "")
214
215        # Use the official build instead of building vanilla ourselves.
216        if label == "vanilla":
217          build_name = '%s-release/%s.0.0' % (self._board, self._build_num)
218
219          # Now add 'official build' to test file.
220          official_image = """
221          official_image {
222            chromeos_root: %s
223            build: %s
224          }
225          """ % (self._chromeos_root, build_name)
226          print >>f, official_image
227
228        else:
229          experiment_image = """
230          %s {
231            chromeos_image: %s
232            image_args: %s
233          }
234          """ % (crosperf_label,
235                 os.path.join(misc.GetImageDir(self._chromeos_root,
236                                               self._board),
237                              label, "chromiumos_test_image.bin"),
238                 image_args)
239          print >>f, experiment_image
240
241    crosperf = os.path.join(os.path.dirname(__file__),
242                            "crosperf",
243                            "crosperf")
244    command = ("%s --no_email=True --results_dir=%s %s" %
245               (crosperf, self._reports_dir, experiment_file))
246
247    ret = self._ce.RunCommand(command)
248    if ret != 0:
249      raise RuntimeError("Couldn't run crosperf!")
250    return
251
252
253  def _CopyWeeklyReportFiles(self, labels):
254    """Create tar files of the custom and official images and copy them
255    to the weekly reports directory, so they exist when the weekly report
256    gets generated.  IMPORTANT NOTE: This function must run *after*
257    crosperf has been run; otherwise the vanilla images will not be there.
258    """
259    images_path = os.path.join(os.path.realpath(self._chromeos_root),
260                               "src/build/images", self._board)
261    weekday = time.strftime("%a")
262    data_dir = os.path.join(WEEKLY_REPORTS_ROOT, self._board)
263    dest_dir = os.path.join (data_dir, weekday)
264    if not os.path.exists(dest_dir):
265      os.makedirs(dest_dir)
266    # Make sure dest_dir is empty (clean out last week's data).
267    cmd = "cd %s; rm -Rf %s_*_image*" % (dest_dir, weekday)
268    self._ce.RunCommand(cmd)
269    # Now create new tar files and copy them over.
270    for l in labels:
271      test_path = os.path.join(images_path, l)
272      if os.path.exists(test_path):
273        if l != "vanilla":
274          label_name = "test"
275        else:
276          label_name = "vanilla"
277        tar_file_name = "%s_%s_image.tar" % (weekday, label_name)
278        cmd = ("cd %s; tar -cvf %s %s/chromiumos_test_image.bin; "
279               "cp %s %s/.") % (images_path,
280                                tar_file_name,
281                                l, tar_file_name,
282                                dest_dir)
283        tar_ret = self._ce.RunCommand(cmd)
284        if tar_ret != 0:
285          self._l.LogOutput("Error while creating/copying test tar file(%s)."
286                            % tar_file_name)
287
288  def _SendEmail(self):
289    """Find email msesage generated by crosperf and send it."""
290    filename = os.path.join(self._reports_dir, "msg_body.html")
291    if (os.path.exists(filename) and
292        os.path.exists(os.path.expanduser(MAIL_PROGRAM))):
293      command = ('cat %s | %s -s "Nightly test results, %s" -team -html'
294                 % (filename, MAIL_PROGRAM, self._board))
295      self._ce.RunCommand(command)
296
297  def DoAll(self):
298    self._CheckoutChromeOS()
299    labels = []
300    labels.append("vanilla")
301    for config in self._configs:
302      label = misc.GetFilenameFromString(config.gcc_config.githash)
303      if (not misc.DoesLabelExist(self._chromeos_root,
304                                  self._board,
305                                  label)):
306        self._BuildToolchain(config)
307        label = self._BuildAndImage(label)
308      labels.append(label)
309    self._FinishSetup()
310    self._TestLabels(labels)
311    self._SendEmail()
312    # Only try to copy the image files if the test runs ran successfully.
313    self._CopyWeeklyReportFiles(labels)
314    if self._clean:
315      ret = self._DeleteChroot()
316      if ret != 0:
317        return ret
318      ret = self._DeleteCcahe()
319      if ret != 0:
320        return ret
321    return 0
322
323
324def Main(argv):
325  """The main function."""
326  # Common initializations
327###  command_executer.InitCommandExecuter(True)
328  command_executer.InitCommandExecuter()
329  parser = optparse.OptionParser()
330  parser.add_option("--remote",
331                    dest="remote",
332                    help="Remote machines to run tests on.")
333  parser.add_option("--board",
334                    dest="board",
335                    default="x86-zgb",
336                    help="The target board.")
337  parser.add_option("--githashes",
338                    dest="githashes",
339                    default="master",
340                    help="The gcc githashes to test.")
341  parser.add_option("--clean",
342                    dest="clean",
343                    default=False,
344                    action="store_true",
345                    help="Clean the chroot after testing.")
346  parser.add_option("--public",
347                    dest="public",
348                    default=False,
349                    action="store_true",
350                    help="Use the public checkout/build.")
351  parser.add_option("--force-mismatch",
352                    dest="force_mismatch",
353                    default="",
354                    help="Force the image regardless of board mismatch")
355  options, _ = parser.parse_args(argv)
356  if not options.board:
357    print "Please give a board."
358    return 1
359  if not options.remote:
360    print "Please give at least one remote machine."
361    return 1
362  toolchain_configs = []
363  for githash in options.githashes.split(","):
364    gcc_config = GCCConfig(githash=githash)
365    toolchain_config = ToolchainConfig(gcc_config=gcc_config)
366    toolchain_configs.append(toolchain_config)
367  fc = ToolchainComparator(options.board, options.remote, toolchain_configs,
368                           options.clean, options.public,
369                           options.force_mismatch)
370  return fc.DoAll()
371
372
373if __name__ == "__main__":
374  retval = Main(sys.argv)
375  sys.exit(retval)
376