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