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