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