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