]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - contrib/fuse/runtests.py
050f066f54538e69fa120b0e61a63d0bf3431c71
[tahoe-lafs/tahoe-lafs.git] / contrib / fuse / runtests.py
1 #! /usr/bin/env python
2 '''
3 Unit and system tests for tahoe-fuse.
4 '''
5
6 # Note: It's always a SetupFailure, not a TestFailure if a webapi
7 # operation fails, because this does not indicate a fuse interface
8 # failure.
9
10 # TODO: Unmount after tests regardless of failure or success!
11
12 # TODO: Test mismatches between tahoe and fuse/posix.  What about nodes
13 # with crazy names ('\0', unicode, '/', '..')?  Huuuuge files?
14 # Huuuuge directories...  As tahoe approaches production quality, it'd
15 # be nice if the fuse interface did so also by hardening against such cases.
16
17 # FIXME: Only create / launch necessary nodes.  Do we still need an introducer and three nodes?
18
19 # FIXME: This framework might be replaceable with twisted.trial,
20 # especially the "layer" design, which is a bit cumbersome when
21 # using recursion to manage multiple clients.
22
23 # FIXME: Identify all race conditions (hint: starting clients, versus
24 # using the grid fs).
25
26 import sys, os, shutil, unittest, subprocess
27 import tempfile, re, time, random, httplib, urllib
28 #import traceback
29
30 from twisted.python import usage
31
32 if sys.platform.startswith('darwin'):
33     UNMOUNT_CMD = ['umount']
34 else:
35     # linux, and until we hear otherwise, all other platforms with fuse, by assumption
36     UNMOUNT_CMD = ['fusermount', '-u']
37
38 # Import fuse implementations:
39 #FuseDir = os.path.join('.', 'contrib', 'fuse')
40 #if not os.path.isdir(FuseDir):
41 #    raise SystemExit('''
42 #Could not find directory "%s".  Please run this script from the tahoe
43 #source base directory.
44 #''' % (FuseDir,))
45 FuseDir = '.'
46
47
48 ### Load each implementation
49 sys.path.append(os.path.join(FuseDir, 'impl_a'))
50 import tahoe_fuse as impl_a
51 sys.path.append(os.path.join(FuseDir, 'impl_b'))
52 import pyfuse.tahoe as impl_b
53 sys.path.append(os.path.join(FuseDir, 'impl_c'))
54 import blackmatch as impl_c
55
56 ### config info about each impl, including which make sense to run
57 implementations = {
58     'impl_a': dict(module=impl_a,
59                    mount_args=['--basedir', '%(nodedir)s', '%(mountpath)s', ],
60                    mount_wait=True,
61                    suites=['read', ]),
62     'impl_b': dict(module=impl_b,
63                    todo=True,
64                    mount_args=['--basedir', '%(nodedir)s', '%(mountpath)s', ],
65                    mount_wait=False,
66                    suites=['read', ]),
67     'impl_c': dict(module=impl_c,
68                    mount_args=['--cache-timeout', '0', '--root-uri', '%(root-uri)s',
69                                '--node-directory', '%(nodedir)s', '%(mountpath)s', ],
70                    mount_wait=True,
71                    suites=['read', 'write', ]),
72     'impl_c_no_split': dict(module=impl_c,
73                    mount_args=['--cache-timeout', '0', '--root-uri', '%(root-uri)s',
74                                '--no-split',
75                                '--node-directory', '%(nodedir)s', '%(mountpath)s', ],
76                    mount_wait=True,
77                    suites=['read', 'write', ]),
78     }
79
80 if sys.platform == 'darwin':
81     del implementations['impl_a']
82     del implementations['impl_b']
83
84 default_catch_up_pause = 0
85 if sys.platform == 'linux2':
86     default_catch_up_pause = 2
87
88 class FuseTestsOptions(usage.Options):
89     optParameters = [
90         ["test-type", None, "both",
91          "Type of test to run; unit, system or both"
92          ],
93         ["implementations", None, "all",
94          "Comma separated list of implementations to test, or 'all'"
95          ],
96         ["suites", None, "all",
97          "Comma separated list of test suites to run, or 'all'"
98          ],
99         ["tests", None, None,
100          "Comma separated list of specific tests to run"
101          ],
102         ["path-to-tahoe", None, "../../bin/tahoe",
103          "Which 'tahoe' script to use to create test nodes"],
104         ["tmp-dir", None, "/tmp",
105          "Where the test should create temporary files"],
106          # Note; this is '/tmp' because on leopard, tempfile.mkdtemp creates
107          # directories in a location which leads paths to exceed what macfuse
108          # can handle without leaking un-umount-able fuse processes.
109         ["catch-up-pause", None, str(default_catch_up_pause),
110          "Pause between tahoe operations and fuse tests thereon"],
111         ]
112     optFlags = [
113         ["debug-wait", None,
114          "Causes the test system to pause at various points, to facilitate debugging"],
115         ["web-open", None,
116          "Opens a web browser to the web ui at the start of each impl's tests"],
117         ["no-cleanup", False,
118          "Prevents the cleanup of the working directories, to allow analysis thereof"],
119          ]
120
121     def postOptions(self):
122         if self['suites'] == 'all':
123             self.suites = ['read', 'write']
124             # [ ] todo: deduce this from looking for test_ in dir(self)
125         else:
126             self.suites = map(str.strip, self['suites'].split(','))
127         if self['implementations'] == 'all':
128             self.implementations = implementations.keys()
129         else:
130             self.implementations = map(str.strip, self['implementations'].split(','))
131         if self['tests']:
132             self.tests = map(str.strip, self['tests'].split(','))
133         else:
134             self.tests = None
135         self.catch_up_pause = float(self['catch-up-pause'])
136
137 ### Main flow control:
138 def main(args):
139     config = FuseTestsOptions()
140     config.parseOptions(args[1:])
141
142     target = 'all'
143     if len(args) > 1:
144         target = args.pop(1)
145
146     test_type = config['test-type']
147     if test_type not in ('both', 'unit', 'system'):
148         raise usage.error('test-type %r not supported' % (test_type,))
149
150     if test_type in ('both', 'unit'):
151         run_unit_tests([args[0]])
152
153     if test_type in ('both', 'system'):
154         return run_system_test(config)
155
156
157 def run_unit_tests(argv):
158     print 'Running Unit Tests.'
159     try:
160         unittest.main(argv=argv)
161     except SystemExit, se:
162         pass
163     print 'Unit Tests complete.\n'
164
165
166 def run_system_test(config):
167     return SystemTest(config).run()
168
169 def drepr(obj):
170     r = repr(obj)
171     if len(r) > 200:
172         return '%s ... %s [%d]' % (r[:100], r[-100:], len(r))
173     else:
174         return r
175
176 ### System Testing:
177 class SystemTest (object):
178     def __init__(self, config):
179         self.config = config
180
181         # These members represent test state:
182         self.cliexec = None
183         self.testroot = None
184
185         # This test state is specific to the first client:
186         self.port = None
187         self.clientbase = None
188
189     ## Top-level flow control:
190     # These "*_layer" methods call eachother in a linear fashion, using
191     # exception unwinding to do cleanup properly.  Each "layer" invokes
192     # a deeper layer, and each layer does its own cleanup upon exit.
193
194     def run(self):
195         print '\n*** Setting up system tests.'
196         try:
197             results = self.init_cli_layer()
198             print '\n*** System Tests complete:'
199             total_failures = todo_failures = 0
200             for result in results:
201                 impl_name, failures, total = result
202                 if implementations[impl_name].get('todo'):
203                     todo_failures += failures
204                 else:
205                     total_failures += failures
206                 print 'Implementation %s: %d failed out of %d.' % result           
207             if total_failures:
208                 print '%s total failures, %s todo' % (total_failures, todo_failures)
209                 return 1
210             else:
211                 return 0
212         except SetupFailure, sfail:
213             print
214             print sfail
215             print '\n*** System Tests were not successfully completed.' 
216             return 1
217
218     def maybe_wait(self, msg='waiting', or_if_webopen=False):
219         if self.config['debug-wait'] or or_if_webopen and self.config['web-open']:
220             print msg
221             raw_input()
222
223     def maybe_webopen(self, where=None):
224         if self.config['web-open']:
225             import webbrowser
226             url = self.weburl
227             if where is not None:
228                 url += urllib.quote(where)
229             webbrowser.open(url)
230
231     def maybe_pause(self):
232         time.sleep(self.config.catch_up_pause)
233
234     def init_cli_layer(self):
235         '''This layer finds the appropriate tahoe executable.'''
236         #self.cliexec = os.path.join('.', 'bin', 'tahoe')
237         self.cliexec = self.config['path-to-tahoe']
238         version = self.run_tahoe('--version')
239         print 'Using %r with version:\n%s' % (self.cliexec, version.rstrip())
240
241         return self.create_testroot_layer()
242
243     def create_testroot_layer(self):
244         print 'Creating test base directory.'
245         #self.testroot = tempfile.mkdtemp(prefix='tahoe_fuse_test_')
246         #self.testroot = tempfile.mkdtemp(prefix='tahoe_fuse_test_', dir='/tmp/')
247         tmpdir = self.config['tmp-dir']
248         if tmpdir:
249             self.testroot = tempfile.mkdtemp(prefix='tahoe_fuse_test_', dir=tmpdir)
250         else:
251             self.testroot = tempfile.mkdtemp(prefix='tahoe_fuse_test_')
252         try:
253             return self.launch_introducer_layer()
254         finally:
255             if not self.config['no-cleanup']:
256                 print 'Cleaning up test root directory.'
257                 try:
258                     shutil.rmtree(self.testroot)
259                 except Exception, e:
260                     print 'Exception removing test root directory: %r' % (self.testroot, )
261                     print 'Ignoring cleanup exception: %r' % (e,)
262             else:
263                 print 'Leaving test root directory: %r' % (self.testroot, )
264
265
266     def launch_introducer_layer(self):
267         print 'Launching introducer.'
268         introbase = os.path.join(self.testroot, 'introducer')
269
270         # NOTE: We assume if tahoe exits with non-zero status, no separate
271         # tahoe child process is still running.
272         createoutput = self.run_tahoe('create-introducer', '--basedir', introbase)
273
274         self.check_tahoe_output(createoutput, ExpectedCreationOutput, introbase)
275
276         startoutput = self.run_tahoe('start', '--basedir', introbase)
277         try:
278             self.check_tahoe_output(startoutput, ExpectedStartOutput, introbase)
279
280             return self.launch_clients_layer(introbase)
281
282         finally:
283             print 'Stopping introducer node.'
284             self.stop_node(introbase)
285
286     TotalClientsNeeded = 3
287     def launch_clients_layer(self, introbase, clientnum = 0):
288         if clientnum >= self.TotalClientsNeeded:
289             self.maybe_wait('waiting (launched clients)')
290             ret = self.create_test_dirnode_layer()
291             self.maybe_wait('waiting (ran tests)', or_if_webopen=True)
292             return ret
293
294         tmpl = 'Launching client %d of %d.'
295         print tmpl % (clientnum,
296                       self.TotalClientsNeeded)
297
298         base = os.path.join(self.testroot, 'client_%d' % (clientnum,))
299
300         output = self.run_tahoe('create-client', '--basedir', base)
301         self.check_tahoe_output(output, ExpectedCreationOutput, base)
302
303         webportpath = os.path.join(base, 'webport')
304         if clientnum == 0:
305             # The first client is special:
306             self.clientbase = base
307             self.port = random.randrange(1024, 2**15)
308
309             f = open(webportpath, 'w')
310             f.write('tcp:%d:interface=127.0.0.1\n' % self.port)
311             f.close()
312             self.weburl = "http://127.0.0.1:%d/" % (self.port,)
313             print self.weburl
314         else:
315             if os.path.exists(webportpath):
316                 os.remove(webportpath)
317
318         introfurl = os.path.join(introbase, 'introducer.furl')
319
320         self.polling_operation(lambda : os.path.isfile(introfurl),
321                                'introducer.furl creation')
322         shutil.copy(introfurl, base)
323
324         # NOTE: We assume if tahoe exist with non-zero status, no separate
325         # tahoe child process is still running.
326         startoutput = self.run_tahoe('start', '--basedir', base)
327         try:
328             self.check_tahoe_output(startoutput, ExpectedStartOutput, base)
329
330             return self.launch_clients_layer(introbase, clientnum+1)
331
332         finally:
333             print 'Stopping client node %d.' % (clientnum,)
334             self.stop_node(base)
335
336     def create_test_dirnode_layer(self):
337         print 'Creating test dirnode.'
338
339         cap = self.create_dirnode()
340
341         f = open(os.path.join(self.clientbase, 'private', 'root_dir.cap'), 'w')
342         f.write(cap)
343         f.close()
344
345         return self.mount_fuse_layer(cap)
346
347     def mount_fuse_layer(self, root_uri):
348         mpbase = os.path.join(self.testroot, 'mountpoint')
349         os.mkdir(mpbase)
350         results = []
351
352         if self.config['debug-wait']:
353             ImplProcessManager.debug_wait = True
354
355         #for name, kwargs in implementations.items():
356         for name in self.config.implementations:
357             kwargs = implementations[name]
358             #print 'instantiating %s: %r' % (name, kwargs)
359             implprocmgr = ImplProcessManager(name, **kwargs)
360             print '\n*** Testing impl: %r' % (implprocmgr.name)
361             implprocmgr.configure(self.clientbase, mpbase)
362             implprocmgr.mount()
363             try:
364                 failures, total = self.run_test_layer(root_uri, implprocmgr)
365                 result = (implprocmgr.name, failures, total)
366                 tmpl = '\n*** Test Results implementation %s: %d failed out of %d.'
367                 print tmpl % result
368                 results.append(result)
369             finally:
370                 implprocmgr.umount()
371         return results
372
373     def run_test_layer(self, root_uri, iman):
374         self.maybe_webopen('uri/'+root_uri)
375         failures = 0
376         testnum = 0
377         numtests = 0
378         if self.config.tests:
379             tests = self.config.tests
380         else:
381             tests = list(set(self.config.suites).intersection(set(iman.suites)))
382         self.maybe_wait('waiting (about to run tests)')
383         for test in tests:
384             testnames = [n for n in sorted(dir(self)) if n.startswith('test_'+test)]
385             numtests += len(testnames)
386             print 'running %s %r tests' % (len(testnames), test,)
387             for testname in testnames:
388                 testnum += 1
389                 print '\n*** Running test #%d: %s' % (testnum, testname)
390                 try:
391                     testcap = self.create_dirnode()
392                     dirname = '%s_%s' % (iman.name, testname)
393                     self.attach_node(root_uri, testcap, dirname)
394                     method = getattr(self, testname)
395                     method(testcap, testdir = os.path.join(iman.mountpath, dirname))
396                     print 'Test succeeded.'
397                 except TestFailure, f:
398                     print f
399                     #print traceback.format_exc()
400                     failures += 1
401                 except:
402                     print 'Error in test code...  Cleaning up.'
403                     raise
404         return (failures, numtests)
405
406     # Tests:
407     def test_read_directory_existence(self, testcap, testdir):
408         if not wrap_os_error(os.path.isdir, testdir):
409             raise TestFailure('Attached test directory not found: %r', testdir)
410
411     def test_read_empty_directory_listing(self, testcap, testdir):
412         listing = wrap_os_error(os.listdir, testdir)
413         if listing:
414             raise TestFailure('Expected empty directory, found: %r', listing)
415
416     def test_read_directory_listing(self, testcap, testdir):
417         names = []
418         filesizes = {}
419
420         for i in range(3):
421             fname = 'file_%d' % (i,)
422             names.append(fname)
423             body = 'Hello World #%d!' % (i,)
424             filesizes[fname] = len(body)
425
426             cap = self.webapi_call('PUT', '/uri', body)
427             self.attach_node(testcap, cap, fname)
428
429             dname = 'dir_%d' % (i,)
430             names.append(dname)
431
432             cap = self.create_dirnode()
433             self.attach_node(testcap, cap, dname)
434
435         names.sort()
436
437         listing = wrap_os_error(os.listdir, testdir)
438         listing.sort()
439
440         if listing != names:
441             tmpl = 'Expected directory list containing %r but fuse gave %r'
442             raise TestFailure(tmpl, names, listing)
443
444         for file, size in filesizes.items():
445             st = wrap_os_error(os.stat, os.path.join(testdir, file))
446             if st.st_size != size:
447                 tmpl = 'Expected %r size of %r but fuse returned %r'
448                 raise TestFailure(tmpl, file, size, st.st_size)
449
450     def test_read_file_contents(self, testcap, testdir):
451         name = 'hw.txt'
452         body = 'Hello World!'
453
454         cap = self.webapi_call('PUT', '/uri', body)
455         self.attach_node(testcap, cap, name)
456
457         path = os.path.join(testdir, name)
458         try:
459             found = open(path, 'r').read()
460         except Exception, err:
461             tmpl = 'Could not read file contents of %r: %r'
462             raise TestFailure(tmpl, path, err)
463
464         if found != body:
465             tmpl = 'Expected file contents %r but found %r'
466             raise TestFailure(tmpl, body, found)
467
468     def test_read_in_random_order(self, testcap, testdir):
469         sz = 2**20
470         bs = 2**10
471         assert(sz % bs == 0)
472         name = 'random_read_order'
473         body = os.urandom(sz)
474
475         cap = self.webapi_call('PUT', '/uri', body)
476         self.attach_node(testcap, cap, name)
477
478         # XXX this should also do a test where sz%bs != 0, so that it correctly tests
479         # the edge case where the last read is a 'short' block
480         path = os.path.join(testdir, name)
481         try:
482             fsize = os.path.getsize(path)
483             if fsize != len(body):
484                 tmpl = 'Expected file size %s but found %s'
485                 raise TestFailure(tmpl, len(body), fsize)
486         except Exception, err:
487             tmpl = 'Could not read file size for %r: %r'
488             raise TestFailure(tmpl, path, err)
489
490         try:
491             f = open(path, 'r')
492             posns = range(0,sz,bs)
493             random.shuffle(posns)
494             data = [None] * (sz/bs)
495             for p in posns:
496                 f.seek(p)
497                 data[p/bs] = f.read(bs)
498             found = ''.join(data)
499         except Exception, err:
500             tmpl = 'Could not read file %r: %r'
501             raise TestFailure(tmpl, path, err)
502
503         if found != body:
504             tmpl = 'Expected file contents %s but found %s'
505             raise TestFailure(tmpl, drepr(body), drepr(found))
506
507     def get_file(self, dircap, path):
508         body = self.webapi_call('GET', '/uri/%s/%s' % (dircap, path))
509         return body
510
511     def test_write_tiny_file(self, testcap, testdir):
512         self._write_test_linear(testcap, testdir, name='tiny.junk', bs=2**9, sz=2**9)
513
514     def test_write_linear_small_writes(self, testcap, testdir):
515         self._write_test_linear(testcap, testdir, name='large_linear.junk', bs=2**9, sz=2**20)
516
517     def test_write_linear_large_writes(self, testcap, testdir):
518         # at least on the mac, large io block sizes are reduced to 64k writes through fuse
519         self._write_test_linear(testcap, testdir, name='small_linear.junk', bs=2**18, sz=2**20)
520
521     def _write_test_linear(self, testcap, testdir, name, bs, sz):
522         body = os.urandom(sz)
523         try:
524             path = os.path.join(testdir, name)
525             f = file(path, 'w')
526         except Exception, err:
527             tmpl = 'Could not open file for write at %r: %r'
528             raise TestFailure(tmpl, path, err)
529         try:
530             for posn in range(0,sz,bs):
531                 f.write(body[posn:posn+bs])
532             f.close()
533         except Exception, err:
534             tmpl = 'Could not write to file %r: %r'
535             raise TestFailure(tmpl, path, err)
536
537         self.maybe_pause()
538         self._check_write(testcap, name, body)
539
540     def _check_write(self, testcap, name, expected_body):
541         uploaded_body = self.get_file(testcap, name)
542         if uploaded_body != expected_body:
543             tmpl = 'Expected file contents %s but found %s'
544             raise TestFailure(tmpl, drepr(expected_body), drepr(uploaded_body))
545
546     def test_write_overlapping_small_writes(self, testcap, testdir):
547         self._write_test_overlap(testcap, testdir, name='large_overlap', bs=2**9, sz=2**20)
548
549     def test_write_overlapping_large_writes(self, testcap, testdir):
550         self._write_test_overlap(testcap, testdir, name='small_overlap', bs=2**18, sz=2**20)
551
552     def _write_test_overlap(self, testcap, testdir, name, bs, sz):
553         body = os.urandom(sz)
554         try:
555             path = os.path.join(testdir, name)
556             f = file(path, 'w')
557         except Exception, err:
558             tmpl = 'Could not open file for write at %r: %r'
559             raise TestFailure(tmpl, path, err)
560         try:
561             for posn in range(0,sz,bs):
562                 start = max(0, posn-bs)
563                 end = min(sz, posn+bs)
564                 f.seek(start)
565                 f.write(body[start:end])
566             f.close()
567         except Exception, err:
568             tmpl = 'Could not write to file %r: %r'
569             raise TestFailure(tmpl, path, err)
570
571         self.maybe_pause()
572         self._check_write(testcap, name, body)
573
574
575     def test_write_random_scatter(self, testcap, testdir):
576         sz = 2**20
577         name = 'random_scatter'
578         body = os.urandom(sz)
579
580         def rsize(sz=sz):
581             return min(int(random.paretovariate(.25)), sz/12)
582
583         # first chop up whole file into random sized chunks
584         slices = []
585         posn = 0
586         while posn < sz:
587             size = rsize()
588             slices.append( (posn, body[posn:posn+size]) )
589             posn += size
590         random.shuffle(slices) # and randomise their order
591
592         try:
593             path = os.path.join(testdir, name)
594             f = file(path, 'w')
595         except Exception, err:
596             tmpl = 'Could not open file for write at %r: %r'
597             raise TestFailure(tmpl, path, err)
598         try:
599             # write all slices: we hence know entire file is ultimately written
600             # write random excerpts: this provides for mixed and varied overlaps
601             for posn,slice in slices:
602                 f.seek(posn)
603                 f.write(slice)
604                 rposn = random.randint(0,sz)
605                 f.seek(rposn)
606                 f.write(body[rposn:rposn+rsize()])
607             f.close()
608         except Exception, err:
609             tmpl = 'Could not write to file %r: %r'
610             raise TestFailure(tmpl, path, err)
611
612         self.maybe_pause()
613         self._check_write(testcap, name, body)
614
615     def test_write_partial_overwrite(self, testcap, testdir):
616         name = 'partial_overwrite'
617         body = '_'*132
618         overwrite = '^'*8
619         position = 26
620
621         def write_file(path, mode, contents, position=None):
622             try:
623                 f = file(path, mode)
624                 if position is not None:
625                     f.seek(position)
626                 f.write(contents)
627                 f.close()
628             except Exception, err:
629                 tmpl = 'Could not write to file %r: %r'
630                 raise TestFailure(tmpl, path, err)
631
632         def read_file(path):
633             try:
634                 f = file(path, 'rb')
635                 contents = f.read()
636                 f.close()
637             except Exception, err:
638                 tmpl = 'Could not read file %r: %r'
639                 raise TestFailure(tmpl, path, err)
640             return contents
641
642         path = os.path.join(testdir, name)
643         #write_file(path, 'w', body)
644
645         cap = self.webapi_call('PUT', '/uri', body)
646         self.attach_node(testcap, cap, name)
647         self.maybe_pause()
648
649         contents = read_file(path)
650         if contents != body:
651             raise TestFailure('File contents mismatch (%r) %r v.s. %r', path, contents, body)
652
653         write_file(path, 'r+', overwrite, position)
654         contents = read_file(path)
655         expected = body[:position] + overwrite + body[position+len(overwrite):]
656         if contents != expected:
657             raise TestFailure('File contents mismatch (%r) %r v.s. %r', path, contents, expected)
658
659
660     # Utilities:
661     def run_tahoe(self, *args):
662         realargs = ('tahoe',) + args
663         status, output = gather_output(realargs, executable=self.cliexec)
664         if status != 0:
665             tmpl = 'The tahoe cli exited with nonzero status.\n'
666             tmpl += 'Executable: %r\n'
667             tmpl += 'Command arguments: %r\n'
668             tmpl += 'Exit status: %r\n'
669             tmpl += 'Output:\n%s\n[End of tahoe output.]\n'
670             raise SetupFailure(tmpl,
671                                     self.cliexec,
672                                     realargs,
673                                     status,
674                                     output)
675         return output
676
677     def check_tahoe_output(self, output, expected, expdir):
678         ignorable_lines = map(re.compile, [
679             '.*site-packages/zope\.interface.*\.egg/zope/__init__.py:3: UserWarning: Module twisted was already imported from .*egg is being added to sys.path',
680             '  import pkg_resources',
681             ])
682         def ignore_line(line):
683             for ignorable_line in ignorable_lines:
684                 if ignorable_line.match(line):
685                     return True
686             else:
687                 return False
688         output = '\n'.join( [ line 
689                               for line in output.split('\n')+['']
690                               #if line not in ignorable_lines ] )
691                               if not ignore_line(line) ] )
692         m = re.match(expected, output, re.M)
693         if m is None:
694             tmpl = 'The output of tahoe did not match the expectation:\n'
695             tmpl += 'Expected regex: %s\n'
696             tmpl += 'Actual output: %r\n'
697             self.warn(tmpl, expected, output)
698
699         elif expdir != m.group('path'):
700             tmpl = 'The output of tahoe refers to an unexpected directory:\n'
701             tmpl += 'Expected directory: %r\n'
702             tmpl += 'Actual directory: %r\n'
703             self.warn(tmpl, expdir, m.group(1))
704
705     def stop_node(self, basedir):
706         try:
707             self.run_tahoe('stop', '--basedir', basedir)
708         except Exception, e:
709             print 'Failed to stop tahoe node.'
710             print 'Ignoring cleanup exception:'
711             # Indent the exception description:
712             desc = str(e).rstrip()
713             print '  ' + desc.replace('\n', '\n  ')
714
715     def webapi_call(self, method, path, body=None, **options):
716         if options:
717             path = path + '?' + ('&'.join(['%s=%s' % kv for kv in options.items()]))
718
719         conn = httplib.HTTPConnection('127.0.0.1', self.port)
720         conn.request(method, path, body = body)
721         resp = conn.getresponse()
722
723         if resp.status != 200:
724             tmpl = 'A webapi operation failed.\n'
725             tmpl += 'Request: %r %r\n'
726             tmpl += 'Body:\n%s\n'
727             tmpl += 'Response:\nStatus %r\nBody:\n%s'
728             raise SetupFailure(tmpl,
729                                     method, path,
730                                     body or '',
731                                     resp.status, body)
732
733         return resp.read()
734
735     def create_dirnode(self):
736         return self.webapi_call('PUT', '/uri', t='mkdir').strip()
737
738     def attach_node(self, dircap, childcap, childname):
739         body = self.webapi_call('PUT',
740                                 '/uri/%s/%s' % (dircap, childname),
741                                 body = childcap,
742                                 t = 'uri',
743                                 replace = 'false')
744         assert body.strip() == childcap, `body, dircap, childcap, childname`
745
746     def polling_operation(self, operation, polldesc, timeout = 10.0, pollinterval = 0.2):
747         totaltime = timeout # Fudging for edge-case SetupFailure description...
748
749         totalattempts = int(timeout / pollinterval)
750
751         starttime = time.time()
752         for attempt in range(totalattempts):
753             opstart = time.time()
754
755             try:
756                 result = operation()
757             except KeyboardInterrupt, e:
758                 raise
759             except Exception, e:
760                 result = False
761
762             totaltime = time.time() - starttime
763
764             if result is not False:
765                 #tmpl = '(Polling took over %.2f seconds.)'
766                 #print tmpl % (totaltime,)
767                 return result
768
769             elif totaltime > timeout:
770                 break
771
772             else:
773                 opdelay = time.time() - opstart
774                 realinterval = max(0., pollinterval - opdelay)
775
776                 #tmpl = '(Poll attempt %d failed after %.2f seconds, sleeping %.2f seconds.)'
777                 #print tmpl % (attempt+1, opdelay, realinterval)
778                 time.sleep(realinterval)
779
780
781         tmpl = 'Timeout while polling for: %s\n'
782         tmpl += 'Waited %.2f seconds (%d polls).'
783         raise SetupFailure(tmpl, polldesc, totaltime, attempt+1)
784
785     def warn(self, tmpl, *args):
786         print ('Test Warning: ' + tmpl) % args
787
788
789 # SystemTest Exceptions:
790 class Failure (Exception):
791     def __init__(self, tmpl, *args):
792         msg = self.Prefix + (tmpl % args)
793         Exception.__init__(self, msg)
794
795 class SetupFailure (Failure):
796     Prefix = 'Setup Failure - The test framework encountered an error:\n'
797
798 class TestFailure (Failure):
799     Prefix = 'TestFailure: '
800
801
802 ### Unit Tests:
803 class Impl_A_UnitTests (unittest.TestCase):
804     '''Tests small stand-alone functions.'''
805     def test_canonicalize_cap(self):
806         iopairs = [('http://127.0.0.1:8123/uri/URI:DIR2:yar9nnzsho6czczieeesc65sry:upp1pmypwxits3w9izkszgo1zbdnsyk3nm6h7e19s7os7s6yhh9y',
807                     'URI:DIR2:yar9nnzsho6czczieeesc65sry:upp1pmypwxits3w9izkszgo1zbdnsyk3nm6h7e19s7os7s6yhh9y'),
808                    ('http://127.0.0.1:8123/uri/URI%3ACHK%3Ak7ktp1qr7szmt98s1y3ha61d9w%3A8tiy8drttp65u79pjn7hs31po83e514zifdejidyeo1ee8nsqfyy%3A3%3A12%3A242?filename=welcome.html',
809                     'URI:CHK:k7ktp1qr7szmt98s1y3ha61d9w:8tiy8drttp65u79pjn7hs31po83e514zifdejidyeo1ee8nsqfyy:3:12:242?filename=welcome.html')]
810
811         for input, output in iopairs:
812             result = impl_a.canonicalize_cap(input)
813             self.failUnlessEqual(output, result, 'input == %r' % (input,))
814
815
816
817 ### Misc:
818 class ImplProcessManager(object):
819     debug_wait = False
820
821     def __init__(self, name, module, mount_args, mount_wait, suites, todo=False):
822         self.name = name
823         self.module = module
824         self.script = module.__file__
825         self.mount_args = mount_args
826         self.mount_wait = mount_wait
827         self.suites = suites
828         self.todo = todo
829
830     def maybe_wait(self, msg='waiting'):
831         if self.debug_wait:
832             print msg
833             raw_input()
834
835     def configure(self, client_nodedir, mountpoint):
836         self.client_nodedir = client_nodedir
837         self.mountpath = os.path.join(mountpoint, self.name)
838         os.mkdir(self.mountpath)
839
840     def mount(self):
841         print 'Mounting implementation: %s (%s)' % (self.name, self.script)
842
843         rootdirfile = os.path.join(self.client_nodedir, 'private', 'root_dir.cap')
844         root_uri = file(rootdirfile, 'r').read().strip()
845         fields = {'mountpath': self.mountpath,
846                   'nodedir': self.client_nodedir,
847                   'root-uri': root_uri,
848                  }
849         args = ['python', self.script] + [ arg%fields for arg in self.mount_args ]
850         print ' '.join(args)
851         self.maybe_wait('waiting (about to launch fuse)')
852
853         if self.mount_wait:
854             exitcode, output = gather_output(args)
855             if exitcode != 0 or output:
856                 tmpl = '%r failed to launch:\n'
857                 tmpl += 'Exit Status: %r\n'
858                 tmpl += 'Output:\n%s\n'
859                 raise SetupFailure(tmpl, self.script, exitcode, output)
860         else:
861             self.proc = subprocess.Popen(args)
862
863     def umount(self):
864         print 'Unmounting implementation: %s' % (self.name,)
865         args = UNMOUNT_CMD + [self.mountpath]
866         print args
867         self.maybe_wait('waiting (unmount)')
868         #print os.system('ls -l '+self.mountpath)
869         ec, out = gather_output(args)
870         if ec != 0 or out:
871             tmpl = '%r failed to unmount:\n' % (' '.join(UNMOUNT_CMD),)
872             tmpl += 'Arguments: %r\n'
873             tmpl += 'Exit Status: %r\n'
874             tmpl += 'Output:\n%s\n'
875             raise SetupFailure(tmpl, args, ec, out)
876
877
878 def gather_output(*args, **kwargs):
879     '''
880     This expects the child does not require input and that it closes
881     stdout/err eventually.
882     '''
883     p = subprocess.Popen(stdout = subprocess.PIPE,
884                          stderr = subprocess.STDOUT,
885                          *args,
886                          **kwargs)
887     output = p.stdout.read()
888     exitcode = p.wait()
889     return (exitcode, output)
890
891
892 def wrap_os_error(meth, *args):
893     try:
894         return meth(*args)
895     except os.error, e:
896         raise TestFailure('%s', e)
897
898
899 ExpectedCreationOutput = r'(introducer|client) created in (?P<path>.*?)\n'
900 ExpectedStartOutput = r'STARTING (?P<path>.*?)\n(introducer|client) node probably started'
901
902
903 if __name__ == '__main__':
904     sys.exit(main(sys.argv))