1## $Id$ 2 3# testbase.py 4# 5# A set of classes for testing BOINC. 6# These classes let you create multiple projects and multiple hosts 7# (all running on a single machine), add applications and work units, 8# run the system, and verify that the results are correct. 9# 10# See doc/test.php for details 11 12# TODO: make sure things work if build_dir != src_dir 13 14import boinc_path_config 15from Boinc import version 16from Boinc.setup_project import * 17import atexit, traceback, signal 18import cgiserver 19 20options.have_init_t = False 21options.echo_overwrite = False 22options.client_bin_filename = version.CLIENT_BIN_FILENAME 23 24def test_init(): 25 if options.have_init_t: return 26 options.have_init_t = True 27 28 if not os.path.exists('testbase.py'): 29 # automake sets the srcdir env. variable if srcdir != builddir 30 os.chdir(os.path.join(os.getenv('srcdir'),'test')) 31 if not os.path.exists('testbase.py'): 32 raise SystemExit('Could not find testbase.py anywhere') 33 34 #options.program_path = os.path.realpath(os.path.dirname(sys.argv[0])) 35 options.program_path = os.getcwd() 36 37 options.auto_setup = int(get_env_var("BOINC_TEST_AUTO_SETUP",1))####### 38 options.user_name = get_env_var("BOINC_TEST_USER_NAME", '') or get_env_var("USER") 39 options.db_user = options.user_name 40 options.db_passwd = '' 41 options.db_host = '' 42 options.delete_testbed = get_env_var("BOINC_TEST_DELETE", 'if-successful').lower() 43# options.install_method = get_env_var("BOINC_TEST_INSTALL_METHOD", 'symlink').lower() 44 options.install_method = 'copy' 45 options.echo_verbose = int(get_env_var("BOINC_TEST_VERBOSE", '1')) 46 options.proxy_port = 16000 + (os.getpid() % 1000) 47 options.drop_db_first = True 48 49 if options.auto_setup: 50 cgiserver.setup_php(program_path = options.program_path) 51 52 options.auto_setup_basedir = 'run-%d'%os.getpid() 53 verbose_echo(0, "Creating testbed in %s"%options.auto_setup_basedir) 54 os.mkdir(options.auto_setup_basedir) 55 try: 56 os.unlink('run') 57 except OSError: 58 pass 59 try: 60 os.symlink(options.auto_setup_basedir, 'run') 61 except OSError: 62 pass 63 options.cgiserver_basedir = os.path.join(os.getcwd(), options.auto_setup_basedir) 64 options.cgiserver_port = 15000 + (os.getpid() % 1000) 65 options.cgiserver_baseurl = 'http://localhost:%d/' % options.cgiserver_port 66 CgiServer = AsynchCGIServer() 67 CgiServer.serve(base_dir=options.auto_setup_basedir, port=options.cgiserver_port) 68 options.projects_dir = os.path.join(options.cgiserver_basedir, 'projects') 69 options.cgi_dir = os.path.join(options.cgiserver_basedir, 'cgi-bin') 70 options.html_dir = os.path.join(options.cgiserver_basedir, 'html') 71 options.hosts_dir = os.path.join(options.cgiserver_basedir, 'hosts') 72 options.cgi_url = os.path.join(options.cgiserver_baseurl, 'cgi-bin') 73 options.html_url = os.path.join(options.cgiserver_baseurl, 'html') 74 options.port = options.cgiserver_port 75 map(os.mkdir, [options.projects_dir, options.cgi_dir, options.html_dir, options.hosts_dir]) 76 else: 77 options.key_dir = get_env_var("BOINC_TEST_KEY_DIR") 78 options.projects_dir = get_env_var("BOINC_TEST_PROJECTS_DIR") 79 options.cgi_dir = get_env_var("BOINC_TEST_CGI_DIR") 80 options.html_dir = get_env_var("BOINC_TEST_HTML_DIR") 81 options.hosts_dir = get_env_var("BOINC_TEST_HOSTS_DIR") 82 options.cgi_url = get_env_var("BOINC_TEST_CGI_URL") 83 options.html_url = get_env_var("BOINC_TEST_HTML_URL") 84 m = re.compile('http://[^/]+:(\d+)/').match(options.html_url) 85 options.port = m and m.group(1) or 80 86 87 init() 88 89def proxerize(url, t=True): 90 if t: 91 r = re.compile('http://[^/]*/') 92 return r.sub('http://localhost:%d/'%options.proxy_port, url) 93 else: 94 return url 95 96def use_cgi_proxy(): 97 test_init() 98 options.cgi_url = proxerize(options.cgi_url) 99def use_html_proxy(): 100 test_init() 101 options.html_url = proxerize(options.html_url) 102 103def check_exists(file): 104 if not os.path.isfile(file): 105 error("file doesn't exist: " + file) 106 return 1 107 return 0 108def check_deleted(file): 109 if os.path.isfile(file): 110 error("file wasn't deleted: " + file) 111 return 1 112 return 0 113def check_files_match(file, correct, descr=''): 114 if not os.path.isfile(file): 115 error("file doesn't exist: %s (needs to match %s)" % (file,correct)) 116 return 1 117 if os.system("diff %s %s" % (file, correct)): 118 error("File mismatch%s: %s %s" % (descr, file, correct)) 119 return 1 120 else: 121 verbose_echo(2, "Files match%s: %s %s" % (descr, file, correct)) 122 return 0 123 124def _check_vars(dict, **names): 125 for key in names: 126 value = names[key] 127 if not key in dict: 128 if value == None: 129 raise SystemExit('error in test script: required parameter "%s" not specified'%key) 130 dict[key] = value 131 for key in dict: 132 if not key in names: 133 raise SystemExit('error in test script: extraneous parameter "%s" unknown'%key) 134 135class STARTS_WITH(str): 136 pass 137 138class MATCH_REGEXPS(list): 139 def match(self, text): 140 '''Returns True iff each regexp in self is in text''' 141 for r in self: 142 R = re.compile(r) 143 if not R.search(text): 144 return False 145 return True 146 pass 147 148def dict_match(dic, resultdic): 149 '''match values in DIC against RESULTDIC''' 150 if not isinstance(dic, dict): 151 dic = dic.__dict__ 152 for key in dic.keys(): 153 expected = dic[key] 154 try: 155 found = resultdic[key] 156 except KeyError: 157 error("Database query result didn't have key '%s'!" % key) 158 continue 159 if isinstance(expected,STARTS_WITH): 160 match = found.startswith(expected) 161 elif isinstance(expected,MATCH_REGEXPS): 162 match = expected.match(found) 163 else: 164 match = found == expected 165 if not match: 166 id = resultdic.get('id', '?') 167 if str(found).count('\n') or str(expected).count('\n'): 168 format = """result %s: unexpected %s: 169 170%s 171 172(expected:) 173 174%s""" 175 else: 176 format = "result %s: unexpected %s '%s' (expected '%s')" 177 error( format % (id, key, found, expected)) 178 179class ExpectedResult: 180 def __init__(self): 181 self.server_state = RESULT_SERVER_STATE_OVER 182 self.client_state = RESULT_FILES_UPLOADED 183 self.outcome = RESULT_OUTCOME_SUCCESS 184 185class ExpectedResultComputeError: 186 def __init__(self): 187 self.server_state = RESULT_SERVER_STATE_OVER 188 self.client_state = RESULT_COMPUTE_DONE 189 self.outcome = RESULT_OUTCOME_CLIENT_ERROR 190 191# TODO: figure out a better way to change settings for the progress meter than 192# kill and refork 193class ProjectList(list): 194 def __init__(self): 195 list.__init__(self) 196 self.state = '<error>' 197 self.pm_started = False 198 def install(self): 199 self.state = '<error>' 200 for i in self: 201 i.install() 202 def run(self): 203 self.set_progress_state('INIT ') 204 for i in self: 205 i.run() 206 def run_init_wait(self): 207 for i in self: 208 i.run_init_wait() 209 self.set_progress_state('RUNNING ') 210 def run_finish_wait(self): 211 self.set_progress_state('FINISH ') 212 for i in self: 213 i.run_finish_wait() 214 self.state='DONE ' 215 def check(self): 216 for i in self: 217 i.check() 218 def stop(self): 219 for i in self: 220 i.maybe_stop() 221 def get_progress(self): 222 progress = [] 223 for project in self: 224 progress.append(project.short_name + ": " + project.progress_meter_status()) 225 return self.state + " " + ' | '.join(progress) 226 def start_progress_meter(self): 227 for project in self: 228 project.progress_meter_ctor() 229 self.rm = ResultMeter(self.get_progress) 230 self.pm_started = True 231 def stop_progress_meter(self): 232 if self.pm_started: 233 self.rm.stop() 234 for project in self: 235 project.progress_meter_dtor() 236 self.pm_started = False 237 def restart_progress_meter(self): 238 if self.pm_started: 239 self.stop_progress_meter() 240 self.start_progress_meter() 241 def set_progress_state(self, state): 242 self.state = state 243 self.restart_progress_meter() 244all_projects = ProjectList() 245 246DEFAULT_NUM_WU = 10 247DEFAULT_REDUNDANCY = 5 248 249def get_redundancy_args(num_wu = None, redundancy = None): 250 num_wu = num_wu or sys.argv[1:] and get_int(sys.argv[1]) or DEFAULT_NUM_WU 251 redundancy = redundancy or sys.argv[2:] and get_int(sys.argv[2]) or DEFAULT_REDUNDANCY 252 return (num_wu, redundancy) 253 254class TestProject(Project): 255 def __init__(self, works, expected_result, appname=None, 256 num_wu=None, redundancy=None, 257 users=None, hosts=None, 258 add_to_list=True, 259 apps=None, app_versions=None, 260 resource_share=None, 261 **kwargs): 262 test_init() 263 if add_to_list: 264 all_projects.append(self) 265 266 kwargs['short_name'] = kwargs.get('short_name') or 'test_'+appname 267 kwargs['long_name'] = kwargs.get('long_name') or 'Project ' + kwargs['short_name'].replace('_',' ').capitalize() 268 apply(Project.__init__, [self], kwargs) 269 270 (num_wu, redundancy) = get_redundancy_args(num_wu, redundancy) 271 self.resource_share = resource_share or 1 272 self.num_wu = num_wu 273 self.redundancy = redundancy 274 self.expected_result = expected_result 275 self.works = works 276 self.users = users or [User()] 277 self.hosts = hosts or [Host()] 278 279 self.platforms = [Platform()] 280 self.app_versions = app_versions or [ 281 AppVersion(App(appname), self.platforms[0], appname)] 282 self.apps = apps or unique(map(lambda av: av.app, self.app_versions)) 283 # convenience vars: 284 self.app_version = self.app_versions[0] 285 self.app = self.apps[0] 286 287 # convenience vars: 288 self.work = self.works[0] 289 self.user = self.users[0] 290 self.host = self.hosts[0] 291 self.started = False 292 293 def init_install(self): 294 if not options.auto_setup: 295 verbose_echo(1, "Deleting previous test runs") 296 rmtree(self.dir()) 297 # self.drop_db_if_exists() 298 299 def query_create_keys(self): 300 '''Overrides Project::query_create_keys() to always return true''' 301 return True 302 303 def install_works(self): 304 for work in self.works: 305 work.install(self) 306 307 def install_hosts(self): 308 for host in self.hosts: 309 for user in self.users: 310 host.add_user(user, self) 311 host.install() 312 313 def install_platforms_versions(self): 314 def commit(list): 315 for item in list: item.commit() 316 317 self.platforms = unique(map(lambda a: a.platform, self.app_versions)) 318 verbose_echo(1, "Setting up database: adding %d platform(s)" % len(self.platforms)) 319 commit(self.platforms) 320 321 verbose_echo(1, "Setting up database: adding %d apps(s)" % len(self.apps)) 322 commit(self.apps) 323 324 verbose_echo(1, "Setting up database: adding %d app version(s)" % len(self.app_versions)) 325 commit(self.app_versions) 326 327 verbose_echo(1, "Setting up database: adding %d user(s)" % len(self.users)) 328 commit(self.users) 329 330 def install(self): 331 self.init_install() 332 self.install_project() 333 self.install_platforms_versions() 334 self.install_works() 335 self.install_hosts() 336 337 def run(self): 338 self.sched_install('make_work', max_wus = self.num_wu) 339 self.sched_install('sample_dummy_assimilator') 340 self.sched_install('file_deleter') 341 self.sched_install('sample_bitwise_validator') 342 self.sched_install('feeder') 343 self.sched_install('transitioner') 344 self.start_servers() 345 346 def run_init_wait(self): 347 time.sleep(8) 348 def run_finish_wait(self): 349 '''Sleep until all workunits are assimilated and no workunits need to transition. 350 351 If more than X seconds have passed than just assume something is broken and return.''' 352 353 timeout = time.time() + 3*60 354 while (num_wus_assimilated() < self.num_wu) or num_wus_to_transition(): 355 time.sleep(.5) 356 if time.time() > timeout: 357 error("run_finish_wait(): timed out waiting for workunits to assimilate/transition") 358 break 359 360 def check(self): 361 # verbose_sleep("Sleeping to allow server daemons to finish", 5) 362 # TODO: self.check_outputs(output_expected, ZZZ, YYY) 363 self.check_results(self.expected_result, self.num_wu*self.redundancy) 364 self.sched_run('file_deleter') 365 # self.check_deleted("download/input") 366 # TODO: use generator/iterator whatever to check all files deleted 367 # self.check_deleted("upload/uc_wu_%d_0", count=self.num_wu) 368 369 def progress_meter_ctor(self): 370 pass 371 def progress_meter_status(self): 372 return "WUs: [A:%d T:%d] Results: [U:%d,IP:%d,O:%d T:%d]" % ( 373 num_wus_assimilated(), 374 #num_wus(), 375 self.num_wu, 376 num_results_unsent(), 377 num_results_in_progress(), 378 num_results_over(), 379 num_results()) 380 def progress_meter_dtor(self): 381 pass 382 383 def _disable(self, *path): 384 '''Temporarily disable a file to test exponential backoff''' 385 path = apply(self.dir, path) 386 os.rename(path, path+'.disabled') 387 def _reenable(self, *path): 388 path = apply(self.dir, path) 389 os.rename(path+'.disabled', path) 390 391 def disable_masterindex(self): 392 self._disable('html_user/index.php') 393 def reenable_masterindex(self): 394 self._reenable('html_user/index.php') 395 def disable_scheduler(self, num = ''): 396 self._disable('cgi-bin/cgi'+str(num)) 397 def reenable_scheduler(self, num = ''): 398 self._reenable('cgi-bin/cgi'+str(num)) 399 def disable_downloaddir(self, num = ''): 400 self._disable('download'+str(num)) 401 def reenable_downloaddir(self, num = ''): 402 self._reenable('download'+str(num)) 403 def disable_file_upload_handler(self, num = ''): 404 self._disable('cgi-bin/file_upload_handler'+str(num)) 405 def reenable_file_upload_handler(self, num = ''): 406 self._reenable('cgi-bin/file_upload_handler'+str(num)) 407 408 def check_results(self, matchresult, expected_count=None): 409 '''MATCHRESULT should be a dictionary of columns to check, such as: 410 411 server_state 412 stderr_out 413 exit_status 414 ''' 415 expected_count = expected_count or self.redundancy 416 results = database.Results.find() 417 for result in results: 418 dict_match(matchresult, result.__dict__) 419 if len(results) != expected_count: 420 error("expected %d results, but found %d" % (expected_count, len(results))) 421 422 def check_files_match(self, result, correct, count=None): 423 '''if COUNT is specified then [0,COUNT) is mapped onto the %d in RESULT''' 424 if count != None: 425 errs = 0 426 for i in range(count): 427 errs += self.check_files_match(result%i, correct) 428 return errs 429 return check_files_match(self.dir(result), 430 correct, " for project '%s'"%self.short_name) 431 def check_deleted(self, file, count=None): 432 if count != None: 433 errs = 0 434 for i in range(count): 435 errs += self.check_deleted(file%i) 436 return errs 437 return check_deleted(self.dir(file)) 438 def check_exists(self, file, count=None): 439 if count != None: 440 errs = 0 441 for i in range(count): 442 errs += self.check_exists(file%i) 443 return errs 444 return check_exists(self.dir(file)) 445 446class Platform(database.Platform): 447 def __init__(self, name=None, user_friendly_name=None): 448 database.Platform.__init__(self) 449 self.name = name or version.PLATFORM 450 self.user_friendly_name = user_friendly_name or name 451 452class User(database.User): 453 def __init__(self): 454 database.User.__init__(self,id=None) 455 self.name = 'John' 456 self.email_addr = 'john@boinc.org' 457 self.authenticator = "3f7b90793a0175ad0bda68684e8bd136" 458 459class App(database.App): 460 def __init__(self, name): 461 database.App.__init__(self,id=None) 462 self.name = name 463 self.min_version = 1 464 465class AppVersion(database.AppVersion): 466 def __init__(self, app, platform, exec_file): 467 database.AppVersion.__init__(self,id=None) 468 self.app = app 469 self.version_num = version.BOINC_MAJOR_VERSION * 100 470 self.platform = platform 471 self.min_core_version = 1 472 self.max_core_version = 999 473 self._exec_file=exec_file 474 def commit(self): 475 self.xml_doc = tools.process_app_version( 476 self.app, self.version_num, 477 [os.path.join(boinc_path_config.TOP_BUILD_DIR,'apps',self._exec_file)], 478 quiet=True) 479 database.AppVersion.commit(self) 480 481class HostList(list): 482 def run(self, asynch=False): map(lambda i: i.run(asynch=asynch), self) 483 def stop(self): map(lambda i: i.stop(), self) 484 485all_hosts = HostList() 486 487class Host: 488 def __init__(self, add_to_list=True): 489 if add_to_list: 490 all_hosts.append(self) 491 self.name = 'Commodore64' 492 self.users = [] 493 self.projects = [] 494 self.global_prefs = None 495 self.cc_config = 'cc_config.xml' 496 self.host_dir = os.path.join(options.hosts_dir, self.name) 497 self.defargs = "-exit_when_idle -skip_cpu_benchmarks -return_results_immediately" 498 # self.defargs = "-exit_when_idle -skip_cpu_benchmarks -sched_retry_delay_bin 1" 499 500 def add_user(self, user, project): 501 self.users.append(user) 502 self.projects.append(project) 503 504 def dir(self, *dirs): 505 return apply(os.path.join,(self.host_dir,)+dirs) 506 507 def install(self): 508 rmtree(self.dir()) 509 os.mkdir(self.dir()) 510 511 verbose_echo(1, "Setting up host '%s': creating account files" % self.name); 512 for (user,project) in map(None,self.users,self.projects): 513 filename = self.dir(account_file_name(project.config.config.master_url)) 514 verbose_echo(2, "Setting up host '%s': writing %s" % (self.name, filename)) 515 516 f = open(filename, "w") 517 print >>f, "<account>" 518 print >>f, map_xml(project.config.config, ['master_url']) 519 print >>f, map_xml(user, ['authenticator']) 520 if user.project_prefs: 521 print >>f, user.project_prefs 522 print >>f, "</account>" 523 f.close() 524 525 # copy log flags and global prefs, if any 526 if self.cc_config: 527 shutil.copy(self.cc_config, self.dir('cc_config.xml')) 528 # if self.global_prefs: 529 # shell_call("cp %s %s" % (self.global_prefs, self.dir('global_prefs.xml'))) 530 # # shutil.copy(self.global_prefs, self.dir('global_prefs.xml')) 531 532 def run(self, args='', asynch=False): 533 if asynch: 534 verbose_echo(1, "Running core client asynchronously") 535 pid = os.fork() 536 if pid: 537 self.pid = pid 538 return 539 else: 540 verbose_echo(1, "Running core client") 541 verbose_shell_call("cd %s && %s %s %s > client.out 2> client.err" % ( 542 self.dir(), builddir('client', options.client_bin_filename), 543 self.defargs, args)) 544 if asynch: os._exit(0) 545 def stop(self): 546 if self.pid: 547 verbose_echo(1, "Stopping core client") 548 try: 549 os.kill(self.pid, 2) 550 except OSError: 551 verbose_echo(0, "Couldn't kill pid %d" % self.pid) 552 self.pid = 0 553 554 def read_cpu_time_file(filename): 555 try: 556 return float(open(self.dir(filename)).readline()) 557 except: 558 return 0 559 560 def check_file_present(self, project, filename): 561 check_exists(self.dir('projects', 562 _url_to_filename(project.master_url), 563 filename)) 564 565 # TODO: do this in Python 566class Work: 567 def __init__(self, redundancy, **kwargs): 568 self.input_files = [] 569 self.rsc_fpops_est = 1e10 570 self.rsc_fpops_bound = 4e10 571 self.rsc_memory_bound = 1e7 572 self.rsc_disk_bound = 1e7 573 self.delay_bound = 86400 574 if not isinstance(redundancy, int): 575 raise TypeError 576 self.min_quorum = redundancy 577 self.target_nresults = redundancy 578 self.max_error_results = redundancy * 2 579 self.max_total_results = redundancy * 4 580 self.max_success_results = redundancy * 2 581 self.app = None 582 self.__dict__.update(kwargs) 583 584 def install(self, project): 585 verbose_echo(1, "Installing work <%s> in project '%s'" %( 586 self.wu_template, project.short_name)) 587 if not self.app: 588 self.app = project.app_versions[0].app 589 for input_file in unique(self.input_files): 590 install(os.path.realpath(input_file), 591 os.path.join(project.config.config.download_dir, 592 os.path.basename(input_file))) 593 594 # simulate multiple data servers by making symbolic links to the 595 # download directory 596 r = re.compile('<download_url/>([^<]+)<', re.IGNORECASE) 597 for line in open(self.wu_template): 598 match = r.search(line) 599 if match: 600 newdir = project.download_dir+match.group(1) 601 verbose_echo(2, "Linking "+newdir) 602 os.symlink(project.download_dir, newdir) 603 604 # simulate multiple data servers by making copies of the file upload 605 # handler 606 r = re.compile('<upload_url/>([^<]+)<', re.IGNORECASE) 607 for line in open(self.wu_template): 608 match = r.search(line) 609 if match: 610 handler = project.srcdir('sched', 'file_upload_handler') 611 newhandler = handler + match.group(1) 612 verbose_echo(2, "Linking "+newhandler) 613 os.symlink(handler, newhandler) 614 615 shutil.copy(self.result_template, project.dir()); 616 cmd = build_command_line("create_work", 617 config_dir = project.dir(), 618 appname = self.app.name, 619 rsc_fpops_est = self.rsc_fpops_est, 620 rsc_fpops_bound = self.rsc_fpops_bound, 621 rsc_disk_bound = self.rsc_disk_bound, 622 rsc_memory_bound = self.rsc_memory_bound, 623 wu_template = self.wu_template, 624 result_template = self.result_template, 625 min_quorum = self.min_quorum, 626 target_nresults = self.target_nresults, 627 max_error_results = self.max_error_results, 628 max_total_results = self.max_total_results, 629 max_success_results = self.max_success_results, 630 wu_name = self.wu_template, 631 delay_bound = self.delay_bound) 632 633 for input_file in self.input_files: 634 cmd += ' ' + input_file 635 636 run_tool(cmd) 637 638RESULT_METER_DELAY = 0.5 639# Note: if test script errors with: 640# _mysql_exceptions.OperationalError: 2013, 'Lost connection to MySQL server 641# during query' 642# Then your mysql server can't handle the load -- increase the RESULT_METER_DELAY 643 644class ResultMeter: 645 def __init__(self, func, args=[], delay=RESULT_METER_DELAY): 646 '''Forks to print a progress meter''' 647 self.pid = os.fork() 648 if self.pid: 649 atexit.register(self.stop) 650 return 651 # re-open database 652 from Boinc import db_base, database 653 db_base.dbconnection = None 654 database.connect() 655 prev_s = None 656 while True: 657 s = apply(func, args) 658 if s != prev_s: 659 verbose_echo(1, s) 660 prev_s = s 661 time.sleep(delay) 662 def stop(self): 663 if self.pid: 664 os.kill(self.pid, 9) 665 self.pid = 0 666 667def run_check_all(): 668 '''Run all projects, run all hosts, check all projects, stop all projects.''' 669 atexit.register(all_projects.stop) 670 all_projects.install() 671 all_projects.run() 672 all_projects.start_progress_meter() 673 all_projects.run_init_wait() 674 if os.environ.get('TEST_STOP_BEFORE_HOST_RUN'): 675 verbose_echo(1, 'stopped') 676 # wait instead of killing backend procs. 677 # (Is there a better way to do this?) 678 while (1): 679 time.sleep(1) 680 raise SystemExit, 'Stopped due to $TEST_STOP_BEFORE_HOST_RUN' 681 # all_hosts.run(asynch=True) 682 all_hosts.run() 683 all_projects.run_finish_wait() 684 all_projects.stop_progress_meter() 685 print 686 # all_hosts.stop() 687 time.sleep(12) 688 all_projects.stop() 689 all_projects.check() 690 691def delete_test(): 692 '''Delete all test data''' 693 if options.auto_setup and hasattr(options,'auto_setup_basedir'): 694 verbose_echo(1, "Deleting testbed %s."%options.auto_setup_basedir) 695 try: 696 shutil.rmtree(options.auto_setup_basedir) 697 except e: 698 verbose_echo(0, "Couldn't delete testbed %s: %s"%(options.auto_setup_basedir,e)) 699 700class Proxy: 701 def __init__(self, code, cgi=0, html=0, start=1): 702 self.pid = 0 703 self.code = code 704 if cgi: use_cgi_proxy() 705 if html: use_html_proxy() 706 if start: self.start() 707 def start(self): 708 self.pid = os.fork() 709 if not self.pid: 710 verbose_shell_call( 711 "exec ./testproxy %d localhost:%d '%s' 2>testproxy.log" % ( 712 options.proxy_port, options.port, self.code), 713 doexec=True) 714 verbose_sleep("Starting proxy server", 1) 715 # check if child process died 716 (pid,status) = os.waitpid(self.pid, os.WNOHANG) 717 if pid: 718 fatal_error("testproxy failed; see testproxy.log for details") 719 self.pid = 0 720 else: 721 atexit.register(self.stop) 722 def stop(self): 723 if self.pid: 724 verbose_echo(1, "Stopping proxy server") 725 try: 726 os.kill(self.pid, 2) 727 except OSError: 728 verbose_echo(0, "Couldn't kill pid %d" % self.pid) 729 self.pid = 0 730 731 732 733class AsynchCGIServer: 734 def __init__(self): 735 self.pid = None 736 def serve(self, port, base_dir): 737 verbose_echo(0,"Running CGI server on localhost:%d"%port) 738 self.pid = os.fork() 739 if self.pid: 740 atexit.register(self.kill) 741 return 742 ## child 743 os.chdir(base_dir) 744 sys.stderr = open('cgiserver.log', 'w', 0) 745 cgiserver.serve(port=port) 746 os._exit(1) 747 def kill(self): 748 if self.pid: 749 verbose_echo(1,"Killing cgiserver") 750 try: 751 os.kill(self.pid, 9) 752 except Exception: 753 verbose_echo(0, "Couldn't kill cgiserver pid %d" %self.pid) 754 self.pid = None 755 756def test_msg(msg): 757 print 758 print "-- Testing", msg, '-'*(66-len(msg)) 759 test_init() 760 761def test_done(): 762 test_init() 763 if sys.__dict__.get('last_traceback'): 764 if sys.last_type == KeyboardInterrupt: 765 errors.count += 0.1 766 sys.stderr.write("\nTest canceled by user\n") 767 else: 768 errors.count += 1 769 sys.stderr.write("\nException thrown - bug in test scripts?\n") 770 if errors.count: 771 verbose_echo(0, "ERRORS.COUNT: %d" % errors.count) 772 if options.delete_testbed == 'always': 773 delete_test() 774 return int(errors.count) 775 else: 776 verbose_echo(1, "Passed test!") 777 if options.echo_overwrite: 778 print 779 if options.delete_testbed == 'if-successful' or options.delete_testbed == 'always': 780 delete_test() 781 if options.echo_overwrite: 782 print 783 return 0 784 785# Note: this bypasses other cleanup functions - if something goes wrong during 786# exit, this may be the culprit. 787def osexit_test_done(): 788 os._exit(test_done()) 789 790atexit.register(osexit_test_done) 791