run_dejagnu.py revision e4b6f220f3cc7d3277daca7bca25f89d648dae8e
1#!/usr/bin/python
2#
3# Copyright 2010 Google Inc. All Rights Reserved.
4
5"""Tool script for auto dejagnu."""
6
7__author__ = 'shenhan@google.com (Han Shen)'
8
9import getpass
10import optparse
11import os
12from os import path
13import re
14import shutil
15import stat
16import sys
17import tempfile
18import time
19
20import lock_machine
21import tc_enter_chroot
22from utils import command_executer
23from utils import constants
24from utils import logger
25from utils import misc
26
27
28def ProcessArguments(argv):
29  """Processing/validating script arguments."""
30  parser = optparse.OptionParser(description=(
31      'Launches gcc dejagnu test in chroot for chromeos toolchain, compares '
32      'the test result with a repository baseline and prints out the result.'),
33                                 usage='run_dejagnu options')
34  parser.add_option('-c', '--chromeos_root', dest='chromeos_root',
35                    help='Required. Specify chromeos root')
36  parser.add_option('-m', '--mount', dest='mount',
37                    help=('Specify gcc source to mount instead of "auto". '
38                          'Under "auto" mode, which is the default - gcc is '
39                          'checked out and built automatically at default '
40                          'directories. Under "mount" mode '
41                          '- the gcc_source is set to "$chromeos_'
42                          'root/chroot/usr/local/toolchain_root/gcc", which is '
43                          'the mount point for this option value, the '
44                          'gcc-build-dir then is computed as '
45                          '"${gcc_source_dir}-build-${ctarget}". In this mode, '
46                          'a complete gcc build must be performed in the '
47                          'computed gcc-build-dir beforehand.'))
48  parser.add_option('-b', '--board', dest='board',
49                    help=('Required. Specify board.'))
50  parser.add_option('-r', '--remote', dest='remote',
51                    help=('Required. Specify addresses/names of the board, '
52                          'seperate each address/name using comma(\',\').'))
53  parser.add_option('-f', '--flags', dest='flags',
54                    help='Optional. Extra run test flags to pass to dejagnu.')
55  parser.add_option('-k', '--keep', dest='keep_intermediate_files',
56                    action='store_true', default=False,
57                    help=('Optional. Default to false. Do not remove dejagnu '
58                          'intermediate files after test run.'))
59  parser.add_option('--cleanup', dest='cleanup', default=None,
60                    help=('Optional. Values to this option could be '
61                          '\'mount\' (unmount gcc source and '
62                          'directory directory, '
63                          'only valid when --mount is given), '
64                          '\'chroot\' (delete chroot) and '
65                          '\'chromeos\' (delete the whole chromeos tree).'))
66  parser.add_option('-t', '--tools', dest='tools', default='gcc,g++',
67                    help=('Optional. Specify which tools to check, using '
68                          '","(comma) as separator. A typical value would be '
69                          '"g++" so that only g++ tests are performed. '
70                          'Defaults to "gcc,g++".'))
71
72  options, args = parser.parse_args(argv)
73
74  if not options.chromeos_root:
75    raise Exception('Missing argument for --chromeos_root.')
76  if not options.remote:
77    raise Exception('Missing argument for --remote.')
78  if not options.board:
79    raise Exception('Missing argument for --board.')
80  if options.cleanup == 'mount' and not options.mount:
81    raise Exception('--cleanup=\'mount\' not valid unless --mount is given.')
82  if options.cleanup and not (
83    options.cleanup == 'mount' or \
84      options.cleanup == 'chroot' or options.cleanup == 'chromeos'):
85    raise Exception('Invalid option value for --cleanup')
86  if options.cleanup and options.keep_intermediate_files:
87    raise Exception('Only one of --keep and --cleanup could be given.')
88
89  return options
90
91
92class DejagnuExecuter(object):
93  """The class wrapper for dejagnu test executer."""
94
95  def __init__(self, base_dir, mount, chromeos_root, remote, board,
96               flags, keep_intermediate_files, tools, cleanup):
97    self._l = logger.GetLogger()
98    self._chromeos_root = chromeos_root
99    self._chromeos_chroot = path.join(chromeos_root, 'chroot')
100    if mount:
101      self._gcc_source_dir_to_mount = mount
102      self._gcc_source_dir = path.join(constants.MOUNTED_TOOLCHAIN_ROOT, 'gcc')
103    else:
104      self._gcc_source_dir = None
105
106    self._remote = remote
107    self._board = board
108    ## Compute target from board
109    self._target = misc.GetCtargetFromBoard(board, chromeos_root)
110    if not self._target:
111      raise Exception('Unsupported board "%s"' % board)
112    self._executer = command_executer.GetCommandExecuter()
113    self._flags = flags or ''
114    self._base_dir = base_dir
115    self._tmp_abs = None
116    self._keep_intermediate_files = keep_intermediate_files
117    self._tools = tools.split(',')
118    self._cleanup = cleanup
119
120  def SetupTestingDir(self):
121    self._tmp_abs = tempfile.mkdtemp(prefix='dejagnu_', dir=path.join(
122        self._chromeos_chroot, 'tmp'))
123    self._tmp = self._tmp_abs[len(self._chromeos_chroot):]
124    self._tmp_testing_rsa = path.join(self._tmp, 'testing_rsa')
125    self._tmp_testing_rsa_abs = path.join(self._tmp_abs, 'testing_rsa')
126
127  def MakeCheckString(self):
128    return ' '.join(['check-{0}'.format(t) for t in self._tools if t])
129
130  def CleanupIntermediateFiles(self):
131    if self._tmp_abs and path.isdir(self._tmp_abs):
132      if self._keep_intermediate_files:
133        self._l.LogOutput(
134          'Your intermediate dejagnu files are kept, you can re-run '
135          'inside chroot the command:')
136        self._l.LogOutput(
137          ' DEJAGNU={0} make -C {1} {2} RUNTESTFLAGS="--target_board={3} {4}"' \
138            .format(path.join(self._tmp, 'site.exp'), self._gcc_build_dir,
139                    self.MakeCheckString(), self._board, self._flags))
140      else:
141        self._l.LogOutput(
142          '[Cleanup] - Removing temp dir - {0}'.format(self._tmp_abs))
143        shutil.rmtree(self._tmp_abs)
144
145  def Cleanup(self):
146    if not self._cleanup:
147      return
148
149    # Optionally cleanup mounted diretory, chroot and chromeos tree.
150    if self._cleanup == 'mount' or self._cleanup == 'chroot' or \
151          self._cleanup == 'chromeos':
152      # No exceptions are allowed from this method.
153      try:
154        self._l.LogOutput('[Cleanup] - Unmounting directories ...')
155        self.MountGccSourceAndBuildDir(unmount=True)
156      except:
157        print 'Warning: failed to unmount gcc source/build directory.'
158
159    if self._cleanup == 'chroot' or self._cleanup == 'chromeos':
160      self._l.LogOutput('[Cleanup]: Deleting chroot inside \'{0}\''.format(
161        self._chromeos_root))
162      command = "cd %s; cros_sdk --delete" % self._chromeos_root
163      rv = self._executer.RunCommand(command)
164      if rv:
165        self._l.LogWarning('Warning - failed to delete chroot.')
166      # Delete .cache - crosbug.com/34956
167      command = "sudo rm -fr %s" % os.path.join(self._chromeos_root, ".cache")
168      rv = self._executer.RunCommand(command)
169      if rv:
170        self._l.LogWarning('Warning - failed to delete \'.cache\'.')
171
172    if self._cleanup == 'chromeos':
173      self._l.LogOutput('[Cleanup]: Deleting chromeos tree \'{0}\' ...'.format(
174        self._chromeos_root))
175      command = 'rm -fr {0}'.format(self._chromeos_root)
176      rv = self._executer.RunCommand(command)
177      if rv:
178        self._l.LogWarning('Warning - failed to remove chromeos tree.')
179
180  def PrepareTestingRsaKeys(self):
181    if not path.isfile(self._tmp_testing_rsa_abs):
182      shutil.copy(path.join(
183          self._chromeos_root,
184          'src/scripts/mod_for_test_scripts/ssh_keys/testing_rsa'),
185                  self._tmp_testing_rsa_abs)
186      os.chmod(self._tmp_testing_rsa_abs, stat.S_IRUSR)
187
188  def PrepareTestFiles(self):
189    """Prepare site.exp and board exp files."""
190    # Create the boards directory.
191    os.mkdir('%s/boards' % self._tmp_abs)
192
193    # Generate the chromeos.exp file.
194    with open('%s/chromeos.exp.in' % self._base_dir, 'r') as template_file:
195      content = template_file.read()
196    substitutions = dict({
197        '__boardname__': self._board,
198        '__board_hostname__': self._remote,
199        '__tmp_testing_rsa__': self._tmp_testing_rsa,
200        '__tmp_dir__': self._tmp})
201    for pat, sub in substitutions.items():
202      content = content.replace(pat, sub)
203
204    board_file_name = '%s/boards/%s.exp' % (self._tmp_abs, self._board)
205    with open(board_file_name, 'w') as board_file:
206      board_file.write(content)
207
208    # Generate the site file
209    with open('%s/site.exp' % self._tmp_abs, 'w') as site_file:
210      site_file.write('set target_list "%s"\n' % self._board)
211
212  def PrepareGcc(self):
213    if self._gcc_source_dir:
214      self.PrepareGccFromCustomizedPath()
215    else:
216      self.PrepareGccDefault()
217    self._l.LogOutput('Gcc source dir - {0}'.format(self._gcc_source_dir))
218    self._l.LogOutput('Gcc build dir - {0}'.format(self._gcc_top_build_dir))
219
220  def PrepareGccFromCustomizedPath(self):
221    """Prepare gcc source, build directory from mounted source."""
222    # We have these source directories -
223    #   _gcc_source_dir
224    #     e.g. '/usr/local/toolchain_root/gcc'
225    #   _gcc_source_dir_abs
226    #     e.g. '/somewhere/chromeos.live/chroot/usr/local/toolchain_root/gcc'
227    #   _gcc_source_dir_to_mount
228    #     e.g. '/somewhere/gcc'
229    self._gcc_source_dir_abs = path.join(self._chromeos_chroot,
230                                         self._gcc_source_dir.lstrip('/'))
231    if not path.isdir(self._gcc_source_dir_abs) and \
232          self._executer.RunCommand(
233      'sudo mkdir -p {0}'.format(self._gcc_source_dir_abs)):
234      raise Exception("Failed to create \'{0}\' inside chroot.".format(
235          self._gcc_source_dir))
236    if not (path.isdir(self._gcc_source_dir_to_mount) and path.isdir(
237      path.join(self._gcc_source_dir_to_mount, 'gcc'))):
238      raise Exception("{0} is not a valid gcc source tree.".format(
239          self._gcc_source_dir_to_mount))
240
241    # We have these build directories -
242    #   _gcc_top_build_dir
243    #     e.g. '/usr/local/toolchain_root/gcc-build-x86_64-cros-linux-gnu'
244    #   _gcc_top_build_dir_abs
245    #     e.g. '/somewhere/chromeos.live/chroo/tusr/local/toolchain_root/
246    #                 gcc-build-x86_64-cros-linux-gnu'
247    #   _gcc_build_dir
248    #     e.g. '/usr/local/toolchain_root/gcc-build-x86_64-cros-linux-gnu/gcc'
249    #   _gcc_build_dir_to_mount
250    #     e.g. '/somewhere/gcc-build-x86_64-cros-linux-gnu'
251    self._gcc_top_build_dir = '{0}-build-{1}'.format(
252        self._gcc_source_dir.rstrip('/'), self._target)
253    self._gcc_build_dir = path.join(self._gcc_top_build_dir, 'gcc')
254    self._gcc_build_dir_to_mount = '{0}-build-{1}'.format(
255        self._gcc_source_dir_to_mount, self._target)
256    self._gcc_top_build_dir_abs = path.join(self._chromeos_chroot,
257                                      self._gcc_top_build_dir.lstrip('/'))
258    if not path.isdir(self._gcc_top_build_dir_abs) and \
259          self._executer.RunCommand(
260      'sudo mkdir -p {0}'.format(self._gcc_top_build_dir_abs)):
261      raise Exception('Failed to create \'{0}\' inside chroot.'.format(
262          self._gcc_top_build_dir))
263    if not (path.isdir(self._gcc_build_dir_to_mount) and path.join(
264        self._gcc_build_dir_to_mount, 'gcc')):
265      raise Exception('{0} is not a valid gcc build tree.'.format(
266          self._gcc_build_dir_to_mount))
267
268    # All check passed. Now mount gcc source and build directories.
269    self.MountGccSourceAndBuildDir()
270
271  def PrepareGccDefault(self):
272    """Auto emerging gcc for building purpose only."""
273    ret = self._executer.ChrootRunCommand(
274        self._chromeos_root,
275        'equery w cross-%s/gcc' % self._target, return_output=True)[1]
276    ret = path.basename(ret.strip())
277    # ret is expected to be something like 'gcc-4.6.2-r11.ebuild' or
278    # 'gcc-9999.ebuild' parse it.
279    matcher = re.match('((.*)-r\d+).ebuild', ret)
280    if matcher:
281      gccrevision, gccversion = matcher.group(1, 2)
282    elif ret == 'gcc-9999.ebuild':
283      gccrevision = 'gcc-9999'
284      gccversion = 'gcc-9999'
285    else:
286      raise Exception('Failed to get gcc version.')
287
288    gcc_portage_dir = '/var/tmp/portage/cross-%s/%s/work' % (
289        self._target, gccrevision)
290    self._gcc_source_dir = path.join(gcc_portage_dir, gccversion)
291    self._gcc_top_build_dir = (gcc_portage_dir + '/%s-build-%s') % (
292        gccversion, self._target)
293    self._gcc_build_dir = path.join(self._gcc_top_build_dir, 'gcc')
294    gcc_build_dir_abs = path.join(
295        self._chromeos_root, 'chroot', self._gcc_build_dir.lstrip('/'))
296    if not path.isdir(gcc_build_dir_abs):
297      ret = self._executer.ChrootRunCommand(
298        self._chromeos_root,
299        ('ebuild $(equery w cross-%s/gcc) clean prepare compile' % (
300            self._target)))
301      if ret:
302        raise Exception('ebuild gcc failed.')
303
304  def MakeCheck(self):
305    self.MountGccSourceAndBuildDir()
306    cmd = ('cd %s ; '
307           'DEJAGNU=%s make %s RUNTESTFLAGS="--target_board=%s %s"' %
308           (self._gcc_build_dir, path.join(self._tmp, 'site.exp'),
309            self.MakeCheckString(), self._board, self._flags))
310    self._executer.ChrootRunCommand(self._chromeos_root, cmd)
311
312  def ValidateFailures(self):
313    validate_failures_py = path.join(
314        self._gcc_source_dir,
315        'contrib/testsuite-management/validate_failures.py')
316    cmd = 'cd {0} ; {1} --build_dir={0}'.format(
317        self._gcc_top_build_dir, validate_failures_py)
318    self.MountGccSourceAndBuildDir()
319    ret = self._executer.ChrootRunCommand(
320      self._chromeos_root, cmd, return_output=True)
321    if ret[0] != 0:
322      self._l.LogWarning('*** validate_failures.py exited with non-zero code,'
323                         'please run it manually inside chroot - \n'
324                         '    ' + cmd)
325    return ret
326
327  # This method ensures necessary mount points before executing chroot comamnd.
328  def MountGccSourceAndBuildDir(self, unmount=False):
329    mount_points = [tc_enter_chroot.MountPoint(
330          self._gcc_source_dir_to_mount, self._gcc_source_dir_abs,
331          getpass.getuser(), "ro"),
332                    tc_enter_chroot.MountPoint(
333          self._gcc_build_dir_to_mount, self._gcc_top_build_dir_abs,
334          getpass.getuser(), "rw"),]
335    for mp in mount_points:
336      if unmount:
337        if mp.UnMount():
338          raise Exception('Failed to unmount {0}'.format(mp.mount_dir))
339        else:
340          self._l.LogOutput('{0} unmounted successfully.'.format(mp.mount_dir))
341      elif mp.DoMount():
342        raise Exception('Failed to mount {0} onto {1}'.format(
343          mp.external_dir, mp.mount_dir))
344      else:
345        self._l.LogOutput('{0} mounted successfully.'.format(mp.mount_dir))
346
347# The end of class DejagnuExecuter
348
349def TryAcquireMachine(remotes):
350  available_machine = None
351  for r in remotes.split(','):
352    machine = lock_machine.Machine(r)
353    if machine.TryLock(timeout=300, exclusive=True):
354      available_machine = machine
355      break
356    else:
357      logger.GetLogger().LogWarning(
358        '*** Failed to lock machine \'{0}\'.'.format(r))
359  if not available_machine:
360    raise Exception(
361        "Failed to acquire one machine from \'{0}\'.".format(remotes))
362  return available_machine
363
364def Main(argv):
365  opts = ProcessArguments(argv)
366  available_machine = TryAcquireMachine(opts.remote)
367  executer = DejagnuExecuter(misc.GetRoot(argv[0])[0],
368                             opts.mount, opts.chromeos_root,
369                             available_machine._name,
370                             opts.board, opts.flags,
371                             opts.keep_intermediate_files, opts.tools,
372                             opts.cleanup)
373  # Return value is a 3- or 4-element tuple
374  #   element#1 - exit code
375  #   element#2 - stdout
376  #   element#3 - stderr
377  #   element#4 - exception infor
378  # Some other scripts need these detailed information.
379  ret = (1, '', '')
380  try:
381    executer.SetupTestingDir()
382    executer.PrepareTestingRsaKeys()
383    executer.PrepareTestFiles()
384    executer.PrepareGcc()
385    executer.MakeCheck()
386    ret = executer.ValidateFailures()
387  except Exception as e:
388    # At least log the exception on console.
389    print e
390    # The #4 element encodes the runtime exception.
391    ret = (1, '', '', 'Exception happened during execution: \n' + str(e))
392  finally:
393    available_machine.Unlock(exclusive=True)
394    executer.CleanupIntermediateFiles()
395    executer.Cleanup()
396    return ret
397
398if __name__ == '__main__':
399  retval = Main(sys.argv)[0]
400  sys.exit(retval)
401