mirror of
https://github.com/tahoe-lafs/tahoe-lafs.git
synced 2024-12-25 15:41:06 +00:00
fe6abac87b
See ticket #536 for details.
905 lines
32 KiB
Python
905 lines
32 KiB
Python
#! /usr/bin/env python
|
|
'''
|
|
Unit and system tests for tahoe-fuse.
|
|
'''
|
|
|
|
# Note: It's always a SetupFailure, not a TestFailure if a webapi
|
|
# operation fails, because this does not indicate a fuse interface
|
|
# failure.
|
|
|
|
# TODO: Unmount after tests regardless of failure or success!
|
|
|
|
# TODO: Test mismatches between tahoe and fuse/posix. What about nodes
|
|
# with crazy names ('\0', unicode, '/', '..')? Huuuuge files?
|
|
# Huuuuge directories... As tahoe approaches production quality, it'd
|
|
# be nice if the fuse interface did so also by hardening against such cases.
|
|
|
|
# FIXME: Only create / launch necessary nodes. Do we still need an introducer and three nodes?
|
|
|
|
# FIXME: This framework might be replaceable with twisted.trial,
|
|
# especially the "layer" design, which is a bit cumbersome when
|
|
# using recursion to manage multiple clients.
|
|
|
|
# FIXME: Identify all race conditions (hint: starting clients, versus
|
|
# using the grid fs).
|
|
|
|
import sys, os, shutil, unittest, subprocess
|
|
import tempfile, re, time, random, httplib, urllib
|
|
#import traceback
|
|
|
|
from twisted.python import usage
|
|
|
|
if sys.platform.startswith('darwin'):
|
|
UNMOUNT_CMD = ['umount']
|
|
else:
|
|
# linux, and until we hear otherwise, all other platforms with fuse, by assumption
|
|
UNMOUNT_CMD = ['fusermount', '-u']
|
|
|
|
# Import fuse implementations:
|
|
#FuseDir = os.path.join('.', 'contrib', 'fuse')
|
|
#if not os.path.isdir(FuseDir):
|
|
# raise SystemExit('''
|
|
#Could not find directory "%s". Please run this script from the tahoe
|
|
#source base directory.
|
|
#''' % (FuseDir,))
|
|
FuseDir = '.'
|
|
|
|
|
|
### Load each implementation
|
|
sys.path.append(os.path.join(FuseDir, 'impl_a'))
|
|
import tahoe_fuse as impl_a
|
|
sys.path.append(os.path.join(FuseDir, 'impl_b'))
|
|
import pyfuse.tahoe as impl_b
|
|
sys.path.append(os.path.join(FuseDir, 'impl_c'))
|
|
import blackmatch as impl_c
|
|
|
|
### config info about each impl, including which make sense to run
|
|
implementations = {
|
|
'impl_a': dict(module=impl_a,
|
|
mount_args=['--basedir', '%(nodedir)s', '%(mountpath)s', ],
|
|
mount_wait=True,
|
|
suites=['read', ]),
|
|
'impl_b': dict(module=impl_b,
|
|
todo=True,
|
|
mount_args=['--basedir', '%(nodedir)s', '%(mountpath)s', ],
|
|
mount_wait=False,
|
|
suites=['read', ]),
|
|
'impl_c': dict(module=impl_c,
|
|
mount_args=['--cache-timeout', '0', '--root-uri', '%(root-uri)s',
|
|
'--node-directory', '%(nodedir)s', '%(mountpath)s', ],
|
|
mount_wait=True,
|
|
suites=['read', 'write', ]),
|
|
'impl_c_no_split': dict(module=impl_c,
|
|
mount_args=['--cache-timeout', '0', '--root-uri', '%(root-uri)s',
|
|
'--no-split',
|
|
'--node-directory', '%(nodedir)s', '%(mountpath)s', ],
|
|
mount_wait=True,
|
|
suites=['read', 'write', ]),
|
|
}
|
|
|
|
if sys.platform == 'darwin':
|
|
del implementations['impl_a']
|
|
del implementations['impl_b']
|
|
|
|
default_catch_up_pause = 0
|
|
if sys.platform == 'linux2':
|
|
default_catch_up_pause = 2
|
|
|
|
class FuseTestsOptions(usage.Options):
|
|
optParameters = [
|
|
["test-type", None, "both",
|
|
"Type of test to run; unit, system or both"
|
|
],
|
|
["implementations", None, "all",
|
|
"Comma separated list of implementations to test, or 'all'"
|
|
],
|
|
["suites", None, "all",
|
|
"Comma separated list of test suites to run, or 'all'"
|
|
],
|
|
["tests", None, None,
|
|
"Comma separated list of specific tests to run"
|
|
],
|
|
["path-to-tahoe", None, "../../bin/tahoe",
|
|
"Which 'tahoe' script to use to create test nodes"],
|
|
["tmp-dir", None, "/tmp",
|
|
"Where the test should create temporary files"],
|
|
# Note; this is '/tmp' because on leopard, tempfile.mkdtemp creates
|
|
# directories in a location which leads paths to exceed what macfuse
|
|
# can handle without leaking un-umount-able fuse processes.
|
|
["catch-up-pause", None, str(default_catch_up_pause),
|
|
"Pause between tahoe operations and fuse tests thereon"],
|
|
]
|
|
optFlags = [
|
|
["debug-wait", None,
|
|
"Causes the test system to pause at various points, to facilitate debugging"],
|
|
["web-open", None,
|
|
"Opens a web browser to the web ui at the start of each impl's tests"],
|
|
["no-cleanup", False,
|
|
"Prevents the cleanup of the working directories, to allow analysis thereof"],
|
|
]
|
|
|
|
def postOptions(self):
|
|
if self['suites'] == 'all':
|
|
self.suites = ['read', 'write']
|
|
# [ ] todo: deduce this from looking for test_ in dir(self)
|
|
else:
|
|
self.suites = map(str.strip, self['suites'].split(','))
|
|
if self['implementations'] == 'all':
|
|
self.implementations = implementations.keys()
|
|
else:
|
|
self.implementations = map(str.strip, self['implementations'].split(','))
|
|
if self['tests']:
|
|
self.tests = map(str.strip, self['tests'].split(','))
|
|
else:
|
|
self.tests = None
|
|
self.catch_up_pause = float(self['catch-up-pause'])
|
|
|
|
### Main flow control:
|
|
def main(args):
|
|
config = FuseTestsOptions()
|
|
config.parseOptions(args[1:])
|
|
|
|
target = 'all'
|
|
if len(args) > 1:
|
|
target = args.pop(1)
|
|
|
|
test_type = config['test-type']
|
|
if test_type not in ('both', 'unit', 'system'):
|
|
raise usage.error('test-type %r not supported' % (test_type,))
|
|
|
|
if test_type in ('both', 'unit'):
|
|
run_unit_tests([args[0]])
|
|
|
|
if test_type in ('both', 'system'):
|
|
return run_system_test(config)
|
|
|
|
|
|
def run_unit_tests(argv):
|
|
print 'Running Unit Tests.'
|
|
try:
|
|
unittest.main(argv=argv)
|
|
except SystemExit, se:
|
|
pass
|
|
print 'Unit Tests complete.\n'
|
|
|
|
|
|
def run_system_test(config):
|
|
return SystemTest(config).run()
|
|
|
|
def drepr(obj):
|
|
r = repr(obj)
|
|
if len(r) > 200:
|
|
return '%s ... %s [%d]' % (r[:100], r[-100:], len(r))
|
|
else:
|
|
return r
|
|
|
|
### System Testing:
|
|
class SystemTest (object):
|
|
def __init__(self, config):
|
|
self.config = config
|
|
|
|
# These members represent test state:
|
|
self.cliexec = None
|
|
self.testroot = None
|
|
|
|
# This test state is specific to the first client:
|
|
self.port = None
|
|
self.clientbase = None
|
|
|
|
## Top-level flow control:
|
|
# These "*_layer" methods call eachother in a linear fashion, using
|
|
# exception unwinding to do cleanup properly. Each "layer" invokes
|
|
# a deeper layer, and each layer does its own cleanup upon exit.
|
|
|
|
def run(self):
|
|
print '\n*** Setting up system tests.'
|
|
try:
|
|
results = self.init_cli_layer()
|
|
print '\n*** System Tests complete:'
|
|
total_failures = todo_failures = 0
|
|
for result in results:
|
|
impl_name, failures, total = result
|
|
if implementations[impl_name].get('todo'):
|
|
todo_failures += failures
|
|
else:
|
|
total_failures += failures
|
|
print 'Implementation %s: %d failed out of %d.' % result
|
|
if total_failures:
|
|
print '%s total failures, %s todo' % (total_failures, todo_failures)
|
|
return 1
|
|
else:
|
|
return 0
|
|
except SetupFailure, sfail:
|
|
print
|
|
print sfail
|
|
print '\n*** System Tests were not successfully completed.'
|
|
return 1
|
|
|
|
def maybe_wait(self, msg='waiting', or_if_webopen=False):
|
|
if self.config['debug-wait'] or or_if_webopen and self.config['web-open']:
|
|
print msg
|
|
raw_input()
|
|
|
|
def maybe_webopen(self, where=None):
|
|
if self.config['web-open']:
|
|
import webbrowser
|
|
url = self.weburl
|
|
if where is not None:
|
|
url += urllib.quote(where)
|
|
webbrowser.open(url)
|
|
|
|
def maybe_pause(self):
|
|
time.sleep(self.config.catch_up_pause)
|
|
|
|
def init_cli_layer(self):
|
|
'''This layer finds the appropriate tahoe executable.'''
|
|
#self.cliexec = os.path.join('.', 'bin', 'tahoe')
|
|
self.cliexec = self.config['path-to-tahoe']
|
|
version = self.run_tahoe('--version')
|
|
print 'Using %r with version:\n%s' % (self.cliexec, version.rstrip())
|
|
|
|
return self.create_testroot_layer()
|
|
|
|
def create_testroot_layer(self):
|
|
print 'Creating test base directory.'
|
|
#self.testroot = tempfile.mkdtemp(prefix='tahoe_fuse_test_')
|
|
#self.testroot = tempfile.mkdtemp(prefix='tahoe_fuse_test_', dir='/tmp/')
|
|
tmpdir = self.config['tmp-dir']
|
|
if tmpdir:
|
|
self.testroot = tempfile.mkdtemp(prefix='tahoe_fuse_test_', dir=tmpdir)
|
|
else:
|
|
self.testroot = tempfile.mkdtemp(prefix='tahoe_fuse_test_')
|
|
try:
|
|
return self.launch_introducer_layer()
|
|
finally:
|
|
if not self.config['no-cleanup']:
|
|
print 'Cleaning up test root directory.'
|
|
try:
|
|
shutil.rmtree(self.testroot)
|
|
except Exception, e:
|
|
print 'Exception removing test root directory: %r' % (self.testroot, )
|
|
print 'Ignoring cleanup exception: %r' % (e,)
|
|
else:
|
|
print 'Leaving test root directory: %r' % (self.testroot, )
|
|
|
|
|
|
def launch_introducer_layer(self):
|
|
print 'Launching introducer.'
|
|
introbase = os.path.join(self.testroot, 'introducer')
|
|
|
|
# NOTE: We assume if tahoe exits with non-zero status, no separate
|
|
# tahoe child process is still running.
|
|
createoutput = self.run_tahoe('create-introducer', '--basedir', introbase)
|
|
|
|
self.check_tahoe_output(createoutput, ExpectedCreationOutput, introbase)
|
|
|
|
startoutput = self.run_tahoe('start', '--basedir', introbase)
|
|
try:
|
|
self.check_tahoe_output(startoutput, ExpectedStartOutput, introbase)
|
|
|
|
return self.launch_clients_layer(introbase)
|
|
|
|
finally:
|
|
print 'Stopping introducer node.'
|
|
self.stop_node(introbase)
|
|
|
|
TotalClientsNeeded = 3
|
|
def launch_clients_layer(self, introbase, clientnum = 0):
|
|
if clientnum >= self.TotalClientsNeeded:
|
|
self.maybe_wait('waiting (launched clients)')
|
|
ret = self.create_test_dirnode_layer()
|
|
self.maybe_wait('waiting (ran tests)', or_if_webopen=True)
|
|
return ret
|
|
|
|
tmpl = 'Launching client %d of %d.'
|
|
print tmpl % (clientnum,
|
|
self.TotalClientsNeeded)
|
|
|
|
base = os.path.join(self.testroot, 'client_%d' % (clientnum,))
|
|
|
|
output = self.run_tahoe('create-client', '--basedir', base)
|
|
self.check_tahoe_output(output, ExpectedCreationOutput, base)
|
|
|
|
webportpath = os.path.join(base, 'webport')
|
|
if clientnum == 0:
|
|
# The first client is special:
|
|
self.clientbase = base
|
|
self.port = random.randrange(1024, 2**15)
|
|
|
|
f = open(webportpath, 'w')
|
|
f.write('tcp:%d:interface=127.0.0.1\n' % self.port)
|
|
f.close()
|
|
self.weburl = "http://127.0.0.1:%d/" % (self.port,)
|
|
print self.weburl
|
|
else:
|
|
if os.path.exists(webportpath):
|
|
os.remove(webportpath)
|
|
|
|
introfurl = os.path.join(introbase, 'introducer.furl')
|
|
|
|
self.polling_operation(lambda : os.path.isfile(introfurl),
|
|
'introducer.furl creation')
|
|
shutil.copy(introfurl, base)
|
|
|
|
# NOTE: We assume if tahoe exist with non-zero status, no separate
|
|
# tahoe child process is still running.
|
|
startoutput = self.run_tahoe('start', '--basedir', base)
|
|
try:
|
|
self.check_tahoe_output(startoutput, ExpectedStartOutput, base)
|
|
|
|
return self.launch_clients_layer(introbase, clientnum+1)
|
|
|
|
finally:
|
|
print 'Stopping client node %d.' % (clientnum,)
|
|
self.stop_node(base)
|
|
|
|
def create_test_dirnode_layer(self):
|
|
print 'Creating test dirnode.'
|
|
|
|
cap = self.create_dirnode()
|
|
|
|
f = open(os.path.join(self.clientbase, 'private', 'root_dir.cap'), 'w')
|
|
f.write(cap)
|
|
f.close()
|
|
|
|
return self.mount_fuse_layer(cap)
|
|
|
|
def mount_fuse_layer(self, root_uri):
|
|
mpbase = os.path.join(self.testroot, 'mountpoint')
|
|
os.mkdir(mpbase)
|
|
results = []
|
|
|
|
if self.config['debug-wait']:
|
|
ImplProcessManager.debug_wait = True
|
|
|
|
#for name, kwargs in implementations.items():
|
|
for name in self.config.implementations:
|
|
kwargs = implementations[name]
|
|
#print 'instantiating %s: %r' % (name, kwargs)
|
|
implprocmgr = ImplProcessManager(name, **kwargs)
|
|
print '\n*** Testing impl: %r' % (implprocmgr.name)
|
|
implprocmgr.configure(self.clientbase, mpbase)
|
|
implprocmgr.mount()
|
|
try:
|
|
failures, total = self.run_test_layer(root_uri, implprocmgr)
|
|
result = (implprocmgr.name, failures, total)
|
|
tmpl = '\n*** Test Results implementation %s: %d failed out of %d.'
|
|
print tmpl % result
|
|
results.append(result)
|
|
finally:
|
|
implprocmgr.umount()
|
|
return results
|
|
|
|
def run_test_layer(self, root_uri, iman):
|
|
self.maybe_webopen('uri/'+root_uri)
|
|
failures = 0
|
|
testnum = 0
|
|
numtests = 0
|
|
if self.config.tests:
|
|
tests = self.config.tests
|
|
else:
|
|
tests = list(set(self.config.suites).intersection(set(iman.suites)))
|
|
self.maybe_wait('waiting (about to run tests)')
|
|
for test in tests:
|
|
testnames = [n for n in sorted(dir(self)) if n.startswith('test_'+test)]
|
|
numtests += len(testnames)
|
|
print 'running %s %r tests' % (len(testnames), test,)
|
|
for testname in testnames:
|
|
testnum += 1
|
|
print '\n*** Running test #%d: %s' % (testnum, testname)
|
|
try:
|
|
testcap = self.create_dirnode()
|
|
dirname = '%s_%s' % (iman.name, testname)
|
|
self.attach_node(root_uri, testcap, dirname)
|
|
method = getattr(self, testname)
|
|
method(testcap, testdir = os.path.join(iman.mountpath, dirname))
|
|
print 'Test succeeded.'
|
|
except TestFailure, f:
|
|
print f
|
|
#print traceback.format_exc()
|
|
failures += 1
|
|
except:
|
|
print 'Error in test code... Cleaning up.'
|
|
raise
|
|
return (failures, numtests)
|
|
|
|
# Tests:
|
|
def test_read_directory_existence(self, testcap, testdir):
|
|
if not wrap_os_error(os.path.isdir, testdir):
|
|
raise TestFailure('Attached test directory not found: %r', testdir)
|
|
|
|
def test_read_empty_directory_listing(self, testcap, testdir):
|
|
listing = wrap_os_error(os.listdir, testdir)
|
|
if listing:
|
|
raise TestFailure('Expected empty directory, found: %r', listing)
|
|
|
|
def test_read_directory_listing(self, testcap, testdir):
|
|
names = []
|
|
filesizes = {}
|
|
|
|
for i in range(3):
|
|
fname = 'file_%d' % (i,)
|
|
names.append(fname)
|
|
body = 'Hello World #%d!' % (i,)
|
|
filesizes[fname] = len(body)
|
|
|
|
cap = self.webapi_call('PUT', '/uri', body)
|
|
self.attach_node(testcap, cap, fname)
|
|
|
|
dname = 'dir_%d' % (i,)
|
|
names.append(dname)
|
|
|
|
cap = self.create_dirnode()
|
|
self.attach_node(testcap, cap, dname)
|
|
|
|
names.sort()
|
|
|
|
listing = wrap_os_error(os.listdir, testdir)
|
|
listing.sort()
|
|
|
|
if listing != names:
|
|
tmpl = 'Expected directory list containing %r but fuse gave %r'
|
|
raise TestFailure(tmpl, names, listing)
|
|
|
|
for file, size in filesizes.items():
|
|
st = wrap_os_error(os.stat, os.path.join(testdir, file))
|
|
if st.st_size != size:
|
|
tmpl = 'Expected %r size of %r but fuse returned %r'
|
|
raise TestFailure(tmpl, file, size, st.st_size)
|
|
|
|
def test_read_file_contents(self, testcap, testdir):
|
|
name = 'hw.txt'
|
|
body = 'Hello World!'
|
|
|
|
cap = self.webapi_call('PUT', '/uri', body)
|
|
self.attach_node(testcap, cap, name)
|
|
|
|
path = os.path.join(testdir, name)
|
|
try:
|
|
found = open(path, 'r').read()
|
|
except Exception, err:
|
|
tmpl = 'Could not read file contents of %r: %r'
|
|
raise TestFailure(tmpl, path, err)
|
|
|
|
if found != body:
|
|
tmpl = 'Expected file contents %r but found %r'
|
|
raise TestFailure(tmpl, body, found)
|
|
|
|
def test_read_in_random_order(self, testcap, testdir):
|
|
sz = 2**20
|
|
bs = 2**10
|
|
assert(sz % bs == 0)
|
|
name = 'random_read_order'
|
|
body = os.urandom(sz)
|
|
|
|
cap = self.webapi_call('PUT', '/uri', body)
|
|
self.attach_node(testcap, cap, name)
|
|
|
|
# XXX this should also do a test where sz%bs != 0, so that it correctly tests
|
|
# the edge case where the last read is a 'short' block
|
|
path = os.path.join(testdir, name)
|
|
try:
|
|
fsize = os.path.getsize(path)
|
|
if fsize != len(body):
|
|
tmpl = 'Expected file size %s but found %s'
|
|
raise TestFailure(tmpl, len(body), fsize)
|
|
except Exception, err:
|
|
tmpl = 'Could not read file size for %r: %r'
|
|
raise TestFailure(tmpl, path, err)
|
|
|
|
try:
|
|
f = open(path, 'r')
|
|
posns = range(0,sz,bs)
|
|
random.shuffle(posns)
|
|
data = [None] * (sz/bs)
|
|
for p in posns:
|
|
f.seek(p)
|
|
data[p/bs] = f.read(bs)
|
|
found = ''.join(data)
|
|
except Exception, err:
|
|
tmpl = 'Could not read file %r: %r'
|
|
raise TestFailure(tmpl, path, err)
|
|
|
|
if found != body:
|
|
tmpl = 'Expected file contents %s but found %s'
|
|
raise TestFailure(tmpl, drepr(body), drepr(found))
|
|
|
|
def get_file(self, dircap, path):
|
|
body = self.webapi_call('GET', '/uri/%s/%s' % (dircap, path))
|
|
return body
|
|
|
|
def test_write_tiny_file(self, testcap, testdir):
|
|
self._write_test_linear(testcap, testdir, name='tiny.junk', bs=2**9, sz=2**9)
|
|
|
|
def test_write_linear_small_writes(self, testcap, testdir):
|
|
self._write_test_linear(testcap, testdir, name='large_linear.junk', bs=2**9, sz=2**20)
|
|
|
|
def test_write_linear_large_writes(self, testcap, testdir):
|
|
# at least on the mac, large io block sizes are reduced to 64k writes through fuse
|
|
self._write_test_linear(testcap, testdir, name='small_linear.junk', bs=2**18, sz=2**20)
|
|
|
|
def _write_test_linear(self, testcap, testdir, name, bs, sz):
|
|
body = os.urandom(sz)
|
|
try:
|
|
path = os.path.join(testdir, name)
|
|
f = file(path, 'w')
|
|
except Exception, err:
|
|
tmpl = 'Could not open file for write at %r: %r'
|
|
raise TestFailure(tmpl, path, err)
|
|
try:
|
|
for posn in range(0,sz,bs):
|
|
f.write(body[posn:posn+bs])
|
|
f.close()
|
|
except Exception, err:
|
|
tmpl = 'Could not write to file %r: %r'
|
|
raise TestFailure(tmpl, path, err)
|
|
|
|
self.maybe_pause()
|
|
self._check_write(testcap, name, body)
|
|
|
|
def _check_write(self, testcap, name, expected_body):
|
|
uploaded_body = self.get_file(testcap, name)
|
|
if uploaded_body != expected_body:
|
|
tmpl = 'Expected file contents %s but found %s'
|
|
raise TestFailure(tmpl, drepr(expected_body), drepr(uploaded_body))
|
|
|
|
def test_write_overlapping_small_writes(self, testcap, testdir):
|
|
self._write_test_overlap(testcap, testdir, name='large_overlap', bs=2**9, sz=2**20)
|
|
|
|
def test_write_overlapping_large_writes(self, testcap, testdir):
|
|
self._write_test_overlap(testcap, testdir, name='small_overlap', bs=2**18, sz=2**20)
|
|
|
|
def _write_test_overlap(self, testcap, testdir, name, bs, sz):
|
|
body = os.urandom(sz)
|
|
try:
|
|
path = os.path.join(testdir, name)
|
|
f = file(path, 'w')
|
|
except Exception, err:
|
|
tmpl = 'Could not open file for write at %r: %r'
|
|
raise TestFailure(tmpl, path, err)
|
|
try:
|
|
for posn in range(0,sz,bs):
|
|
start = max(0, posn-bs)
|
|
end = min(sz, posn+bs)
|
|
f.seek(start)
|
|
f.write(body[start:end])
|
|
f.close()
|
|
except Exception, err:
|
|
tmpl = 'Could not write to file %r: %r'
|
|
raise TestFailure(tmpl, path, err)
|
|
|
|
self.maybe_pause()
|
|
self._check_write(testcap, name, body)
|
|
|
|
|
|
def test_write_random_scatter(self, testcap, testdir):
|
|
sz = 2**20
|
|
name = 'random_scatter'
|
|
body = os.urandom(sz)
|
|
|
|
def rsize(sz=sz):
|
|
return min(int(random.paretovariate(.25)), sz/12)
|
|
|
|
# first chop up whole file into random sized chunks
|
|
slices = []
|
|
posn = 0
|
|
while posn < sz:
|
|
size = rsize()
|
|
slices.append( (posn, body[posn:posn+size]) )
|
|
posn += size
|
|
random.shuffle(slices) # and randomise their order
|
|
|
|
try:
|
|
path = os.path.join(testdir, name)
|
|
f = file(path, 'w')
|
|
except Exception, err:
|
|
tmpl = 'Could not open file for write at %r: %r'
|
|
raise TestFailure(tmpl, path, err)
|
|
try:
|
|
# write all slices: we hence know entire file is ultimately written
|
|
# write random excerpts: this provides for mixed and varied overlaps
|
|
for posn,slice in slices:
|
|
f.seek(posn)
|
|
f.write(slice)
|
|
rposn = random.randint(0,sz)
|
|
f.seek(rposn)
|
|
f.write(body[rposn:rposn+rsize()])
|
|
f.close()
|
|
except Exception, err:
|
|
tmpl = 'Could not write to file %r: %r'
|
|
raise TestFailure(tmpl, path, err)
|
|
|
|
self.maybe_pause()
|
|
self._check_write(testcap, name, body)
|
|
|
|
def test_write_partial_overwrite(self, testcap, testdir):
|
|
name = 'partial_overwrite'
|
|
body = '_'*132
|
|
overwrite = '^'*8
|
|
position = 26
|
|
|
|
def write_file(path, mode, contents, position=None):
|
|
try:
|
|
f = file(path, mode)
|
|
if position is not None:
|
|
f.seek(position)
|
|
f.write(contents)
|
|
f.close()
|
|
except Exception, err:
|
|
tmpl = 'Could not write to file %r: %r'
|
|
raise TestFailure(tmpl, path, err)
|
|
|
|
def read_file(path):
|
|
try:
|
|
f = file(path, 'rb')
|
|
contents = f.read()
|
|
f.close()
|
|
except Exception, err:
|
|
tmpl = 'Could not read file %r: %r'
|
|
raise TestFailure(tmpl, path, err)
|
|
return contents
|
|
|
|
path = os.path.join(testdir, name)
|
|
#write_file(path, 'w', body)
|
|
|
|
cap = self.webapi_call('PUT', '/uri', body)
|
|
self.attach_node(testcap, cap, name)
|
|
self.maybe_pause()
|
|
|
|
contents = read_file(path)
|
|
if contents != body:
|
|
raise TestFailure('File contents mismatch (%r) %r v.s. %r', path, contents, body)
|
|
|
|
write_file(path, 'r+', overwrite, position)
|
|
contents = read_file(path)
|
|
expected = body[:position] + overwrite + body[position+len(overwrite):]
|
|
if contents != expected:
|
|
raise TestFailure('File contents mismatch (%r) %r v.s. %r', path, contents, expected)
|
|
|
|
|
|
# Utilities:
|
|
def run_tahoe(self, *args):
|
|
realargs = ('tahoe',) + args
|
|
status, output = gather_output(realargs, executable=self.cliexec)
|
|
if status != 0:
|
|
tmpl = 'The tahoe cli exited with nonzero status.\n'
|
|
tmpl += 'Executable: %r\n'
|
|
tmpl += 'Command arguments: %r\n'
|
|
tmpl += 'Exit status: %r\n'
|
|
tmpl += 'Output:\n%s\n[End of tahoe output.]\n'
|
|
raise SetupFailure(tmpl,
|
|
self.cliexec,
|
|
realargs,
|
|
status,
|
|
output)
|
|
return output
|
|
|
|
def check_tahoe_output(self, output, expected, expdir):
|
|
ignorable_lines = map(re.compile, [
|
|
'.*site-packages/zope\.interface.*\.egg/zope/__init__.py:3: UserWarning: Module twisted was already imported from .*egg is being added to sys.path',
|
|
' import pkg_resources',
|
|
])
|
|
def ignore_line(line):
|
|
for ignorable_line in ignorable_lines:
|
|
if ignorable_line.match(line):
|
|
return True
|
|
else:
|
|
return False
|
|
output = '\n'.join( [ line
|
|
for line in output.split('\n')+['']
|
|
#if line not in ignorable_lines ] )
|
|
if not ignore_line(line) ] )
|
|
m = re.match(expected, output, re.M)
|
|
if m is None:
|
|
tmpl = 'The output of tahoe did not match the expectation:\n'
|
|
tmpl += 'Expected regex: %s\n'
|
|
tmpl += 'Actual output: %r\n'
|
|
self.warn(tmpl, expected, output)
|
|
|
|
elif expdir != m.group('path'):
|
|
tmpl = 'The output of tahoe refers to an unexpected directory:\n'
|
|
tmpl += 'Expected directory: %r\n'
|
|
tmpl += 'Actual directory: %r\n'
|
|
self.warn(tmpl, expdir, m.group(1))
|
|
|
|
def stop_node(self, basedir):
|
|
try:
|
|
self.run_tahoe('stop', '--basedir', basedir)
|
|
except Exception, e:
|
|
print 'Failed to stop tahoe node.'
|
|
print 'Ignoring cleanup exception:'
|
|
# Indent the exception description:
|
|
desc = str(e).rstrip()
|
|
print ' ' + desc.replace('\n', '\n ')
|
|
|
|
def webapi_call(self, method, path, body=None, **options):
|
|
if options:
|
|
path = path + '?' + ('&'.join(['%s=%s' % kv for kv in options.items()]))
|
|
|
|
conn = httplib.HTTPConnection('127.0.0.1', self.port)
|
|
conn.request(method, path, body = body)
|
|
resp = conn.getresponse()
|
|
|
|
if resp.status != 200:
|
|
tmpl = 'A webapi operation failed.\n'
|
|
tmpl += 'Request: %r %r\n'
|
|
tmpl += 'Body:\n%s\n'
|
|
tmpl += 'Response:\nStatus %r\nBody:\n%s'
|
|
raise SetupFailure(tmpl,
|
|
method, path,
|
|
body or '',
|
|
resp.status, body)
|
|
|
|
return resp.read()
|
|
|
|
def create_dirnode(self):
|
|
return self.webapi_call('PUT', '/uri', t='mkdir').strip()
|
|
|
|
def attach_node(self, dircap, childcap, childname):
|
|
body = self.webapi_call('PUT',
|
|
'/uri/%s/%s' % (dircap, childname),
|
|
body = childcap,
|
|
t = 'uri',
|
|
replace = 'false')
|
|
assert body.strip() == childcap, `body, dircap, childcap, childname`
|
|
|
|
def polling_operation(self, operation, polldesc, timeout = 10.0, pollinterval = 0.2):
|
|
totaltime = timeout # Fudging for edge-case SetupFailure description...
|
|
|
|
totalattempts = int(timeout / pollinterval)
|
|
|
|
starttime = time.time()
|
|
for attempt in range(totalattempts):
|
|
opstart = time.time()
|
|
|
|
try:
|
|
result = operation()
|
|
except KeyboardInterrupt, e:
|
|
raise
|
|
except Exception, e:
|
|
result = False
|
|
|
|
totaltime = time.time() - starttime
|
|
|
|
if result is not False:
|
|
#tmpl = '(Polling took over %.2f seconds.)'
|
|
#print tmpl % (totaltime,)
|
|
return result
|
|
|
|
elif totaltime > timeout:
|
|
break
|
|
|
|
else:
|
|
opdelay = time.time() - opstart
|
|
realinterval = max(0., pollinterval - opdelay)
|
|
|
|
#tmpl = '(Poll attempt %d failed after %.2f seconds, sleeping %.2f seconds.)'
|
|
#print tmpl % (attempt+1, opdelay, realinterval)
|
|
time.sleep(realinterval)
|
|
|
|
|
|
tmpl = 'Timeout while polling for: %s\n'
|
|
tmpl += 'Waited %.2f seconds (%d polls).'
|
|
raise SetupFailure(tmpl, polldesc, totaltime, attempt+1)
|
|
|
|
def warn(self, tmpl, *args):
|
|
print ('Test Warning: ' + tmpl) % args
|
|
|
|
|
|
# SystemTest Exceptions:
|
|
class Failure (Exception):
|
|
def __init__(self, tmpl, *args):
|
|
msg = self.Prefix + (tmpl % args)
|
|
Exception.__init__(self, msg)
|
|
|
|
class SetupFailure (Failure):
|
|
Prefix = 'Setup Failure - The test framework encountered an error:\n'
|
|
|
|
class TestFailure (Failure):
|
|
Prefix = 'TestFailure: '
|
|
|
|
|
|
### Unit Tests:
|
|
class Impl_A_UnitTests (unittest.TestCase):
|
|
'''Tests small stand-alone functions.'''
|
|
def test_canonicalize_cap(self):
|
|
iopairs = [('http://127.0.0.1:3456/uri/URI:DIR2:yar9nnzsho6czczieeesc65sry:upp1pmypwxits3w9izkszgo1zbdnsyk3nm6h7e19s7os7s6yhh9y',
|
|
'URI:DIR2:yar9nnzsho6czczieeesc65sry:upp1pmypwxits3w9izkszgo1zbdnsyk3nm6h7e19s7os7s6yhh9y'),
|
|
('http://127.0.0.1:3456/uri/URI%3ACHK%3Ak7ktp1qr7szmt98s1y3ha61d9w%3A8tiy8drttp65u79pjn7hs31po83e514zifdejidyeo1ee8nsqfyy%3A3%3A12%3A242?filename=welcome.html',
|
|
'URI:CHK:k7ktp1qr7szmt98s1y3ha61d9w:8tiy8drttp65u79pjn7hs31po83e514zifdejidyeo1ee8nsqfyy:3:12:242?filename=welcome.html')]
|
|
|
|
for input, output in iopairs:
|
|
result = impl_a.canonicalize_cap(input)
|
|
self.failUnlessEqual(output, result, 'input == %r' % (input,))
|
|
|
|
|
|
|
|
### Misc:
|
|
class ImplProcessManager(object):
|
|
debug_wait = False
|
|
|
|
def __init__(self, name, module, mount_args, mount_wait, suites, todo=False):
|
|
self.name = name
|
|
self.module = module
|
|
self.script = module.__file__
|
|
self.mount_args = mount_args
|
|
self.mount_wait = mount_wait
|
|
self.suites = suites
|
|
self.todo = todo
|
|
|
|
def maybe_wait(self, msg='waiting'):
|
|
if self.debug_wait:
|
|
print msg
|
|
raw_input()
|
|
|
|
def configure(self, client_nodedir, mountpoint):
|
|
self.client_nodedir = client_nodedir
|
|
self.mountpath = os.path.join(mountpoint, self.name)
|
|
os.mkdir(self.mountpath)
|
|
|
|
def mount(self):
|
|
print 'Mounting implementation: %s (%s)' % (self.name, self.script)
|
|
|
|
rootdirfile = os.path.join(self.client_nodedir, 'private', 'root_dir.cap')
|
|
root_uri = file(rootdirfile, 'r').read().strip()
|
|
fields = {'mountpath': self.mountpath,
|
|
'nodedir': self.client_nodedir,
|
|
'root-uri': root_uri,
|
|
}
|
|
args = ['python', self.script] + [ arg%fields for arg in self.mount_args ]
|
|
print ' '.join(args)
|
|
self.maybe_wait('waiting (about to launch fuse)')
|
|
|
|
if self.mount_wait:
|
|
exitcode, output = gather_output(args)
|
|
if exitcode != 0 or output:
|
|
tmpl = '%r failed to launch:\n'
|
|
tmpl += 'Exit Status: %r\n'
|
|
tmpl += 'Output:\n%s\n'
|
|
raise SetupFailure(tmpl, self.script, exitcode, output)
|
|
else:
|
|
self.proc = subprocess.Popen(args)
|
|
|
|
def umount(self):
|
|
print 'Unmounting implementation: %s' % (self.name,)
|
|
args = UNMOUNT_CMD + [self.mountpath]
|
|
print args
|
|
self.maybe_wait('waiting (unmount)')
|
|
#print os.system('ls -l '+self.mountpath)
|
|
ec, out = gather_output(args)
|
|
if ec != 0 or out:
|
|
tmpl = '%r failed to unmount:\n' % (' '.join(UNMOUNT_CMD),)
|
|
tmpl += 'Arguments: %r\n'
|
|
tmpl += 'Exit Status: %r\n'
|
|
tmpl += 'Output:\n%s\n'
|
|
raise SetupFailure(tmpl, args, ec, out)
|
|
|
|
|
|
def gather_output(*args, **kwargs):
|
|
'''
|
|
This expects the child does not require input and that it closes
|
|
stdout/err eventually.
|
|
'''
|
|
p = subprocess.Popen(stdout = subprocess.PIPE,
|
|
stderr = subprocess.STDOUT,
|
|
*args,
|
|
**kwargs)
|
|
output = p.stdout.read()
|
|
exitcode = p.wait()
|
|
return (exitcode, output)
|
|
|
|
|
|
def wrap_os_error(meth, *args):
|
|
try:
|
|
return meth(*args)
|
|
except os.error, e:
|
|
raise TestFailure('%s', e)
|
|
|
|
|
|
ExpectedCreationOutput = r'(introducer|client) created in (?P<path>.*?)\n'
|
|
ExpectedStartOutput = r'STARTING (?P<path>.*?)\n(introducer|client) node probably started'
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main(sys.argv))
|