1 #!/usr/bin/python 2 # 3 # Copyright 2010 Google Inc. All Rights Reserved. 4 """Tool script for auto dejagnu.""" 5 6 __author__ = 'shenhan (at] google.com (Han Shen)' 7 8 import getpass 9 import optparse 10 import os 11 from os import path 12 import re 13 import shutil 14 import stat 15 import sys 16 import tempfile 17 import time 18 19 import lock_machine 20 import tc_enter_chroot 21 from cros_utils import command_executer 22 from cros_utils import constants 23 from cros_utils import logger 24 from cros_utils import misc 25 26 27 def 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 109 class 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 367 def 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 383 def 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 416 if __name__ == '__main__': 417 retval = Main(sys.argv)[0] 418 sys.exit(retval) 419