Changeset View
Changeset View
Standalone View
Standalone View
contrib/lib9p/pytest/client.py
- This file was added.
Property | Old Value | New Value |
---|---|---|
File Mode | null | 100755 |
#! /usr/bin/env python | |||||
""" | |||||
Run various tests, as a client. | |||||
""" | |||||
from __future__ import print_function | |||||
import argparse | |||||
try: | |||||
import ConfigParser as configparser | |||||
except ImportError: | |||||
import configparser | |||||
import functools | |||||
import logging | |||||
import os | |||||
import socket | |||||
import struct | |||||
import sys | |||||
import time | |||||
import traceback | |||||
import p9conn | |||||
import protocol | |||||
LocalError = p9conn.LocalError | |||||
RemoteError = p9conn.RemoteError | |||||
TEError = p9conn.TEError | |||||
class TestState(object): | |||||
def __init__(self): | |||||
self.config = None | |||||
self.logger = None | |||||
self.successes = 0 | |||||
self.skips = 0 | |||||
self.failures = 0 | |||||
self.exceptions = 0 | |||||
self.clnt_tab = {} | |||||
self.mkclient = None | |||||
self.stop = False | |||||
self.gid = 0 | |||||
def ccc(self, cid=None): | |||||
""" | |||||
Connect or reconnect as client (ccc = check and connect client). | |||||
If caller provides a cid (client ID) we check that specific | |||||
client. Otherwise the default ID ('base') is used. | |||||
In any case we return the now-connected client, plus the | |||||
attachment (session info) if any. | |||||
""" | |||||
if cid is None: | |||||
cid = 'base' | |||||
pair = self.clnt_tab.get(cid) | |||||
if pair is None: | |||||
clnt = self.mkclient() | |||||
pair = [clnt, None] | |||||
self.clnt_tab[cid] = pair | |||||
else: | |||||
clnt = pair[0] | |||||
if not clnt.is_connected(): | |||||
clnt.connect() | |||||
return pair | |||||
def dcc(self, cid=None): | |||||
""" | |||||
Disconnect client (disconnect checked client). If no specific | |||||
client ID is provided, this disconnects ALL checked clients! | |||||
""" | |||||
if cid is None: | |||||
for cid in list(self.clnt_tab.keys()): | |||||
self.dcc(cid) | |||||
pair = self.clnt_tab.get(cid) | |||||
if pair is not None: | |||||
clnt = pair[0] | |||||
if clnt.is_connected(): | |||||
clnt.shutdown() | |||||
del self.clnt_tab[cid] | |||||
def ccs(self, cid=None): | |||||
""" | |||||
Like ccc, but establish a session as well, by setting up | |||||
the uname/n_uname. | |||||
Return the client instance (only). | |||||
""" | |||||
pair = self.ccc(cid) | |||||
clnt = pair[0] | |||||
if pair[1] is None: | |||||
# No session yet - establish one. Note, this may fail. | |||||
section = None if cid is None else ('client-' + cid) | |||||
aname = getconf(self.config, section, 'aname', '') | |||||
uname = getconf(self.config, section, 'uname', '') | |||||
if clnt.proto > protocol.plain: | |||||
n_uname = getint(self.config, section, 'n_uname', 1001) | |||||
else: | |||||
n_uname = None | |||||
clnt.attach(afid=None, aname=aname, uname=uname, n_uname=n_uname) | |||||
pair[1] = (aname, uname, n_uname) | |||||
return clnt | |||||
def getconf(conf, section, name, default=None, rtype=str): | |||||
""" | |||||
Get configuration item for given section, or for "client" if | |||||
there is no entry for that particular section (or if section | |||||
is None). | |||||
This lets us get specific values for specific tests or | |||||
groups ([foo] name=value), falling back to general values | |||||
([client] name=value). | |||||
The type of the returned value <rtype> can be str, int, bool, | |||||
or float. The default is str (and see getconfint, getconfbool, | |||||
getconffloat below). | |||||
A default value may be supplied; if it is, that's the default | |||||
return value (this default should have the right type). If | |||||
no default is supplied, a missing value is an error. | |||||
""" | |||||
try: | |||||
# note: conf.get(None, 'foo') raises NoSectionError | |||||
where = section | |||||
result = conf.get(where, name) | |||||
except (configparser.NoSectionError, configparser.NoOptionError): | |||||
try: | |||||
where = 'client' | |||||
result = conf.get(where, name) | |||||
except configparser.NoSectionError: | |||||
sys.exit('no [{0}] section in configuration!'.format(where)) | |||||
except configparser.NoOptionError: | |||||
if default is not None: | |||||
return default | |||||
if section is not None: | |||||
where = '[{0}] or [{1}]'.format(section, where) | |||||
else: | |||||
where = '[{0}]'.format(where) | |||||
raise LocalError('need {0}=value in {1}'.format(name, where)) | |||||
where = '[{0}]'.format(where) | |||||
if rtype is str: | |||||
return result | |||||
if rtype is int: | |||||
return int(result) | |||||
if rtype is float: | |||||
return float(result) | |||||
if rtype is bool: | |||||
if result.lower() in ('1', 't', 'true', 'y', 'yes'): | |||||
return True | |||||
if result.lower() in ('0', 'f', 'false', 'n', 'no'): | |||||
return False | |||||
raise ValueError('{0} {1}={2}: invalid boolean'.format(where, name, | |||||
result)) | |||||
raise ValueError('{0} {1}={2}: internal error: bad result type ' | |||||
'{3!r}'.format(where, name, result, rtype)) | |||||
def getint(conf, section, name, default=None): | |||||
"get integer config item" | |||||
return getconf(conf, section, name, default, int) | |||||
def getfloat(conf, section, name, default=None): | |||||
"get float config item" | |||||
return getconf(conf, section, name, default, float) | |||||
def getbool(conf, section, name, default=None): | |||||
"get boolean config item" | |||||
return getconf(conf, section, name, default, bool) | |||||
def pluralize(n, singular, plural): | |||||
"return singular or plural based on value of n" | |||||
return plural if n != 1 else singular | |||||
class TCDone(Exception): | |||||
"used in succ/fail/skip - skips rest of testcase with" | |||||
pass | |||||
class TestCase(object): | |||||
""" | |||||
Start a test case. Most callers must then do a ccs() to connect. | |||||
A failed test will generally disconnect from the server; a | |||||
new ccs() will reconnect, if the server is still alive. | |||||
""" | |||||
def __init__(self, name, tstate): | |||||
self.name = name | |||||
self.status = None | |||||
self.detail = None | |||||
self.tstate = tstate | |||||
self._shutdown = None | |||||
self._autoclunk = None | |||||
self._acconn = None | |||||
def auto_disconnect(self, conn): | |||||
self._shutdown = conn | |||||
def succ(self, detail=None): | |||||
"set success status" | |||||
self.status = 'SUCC' | |||||
self.detail = detail | |||||
raise TCDone() | |||||
def fail(self, detail): | |||||
"set failure status" | |||||
self.status = 'FAIL' | |||||
self.detail = detail | |||||
raise TCDone() | |||||
def skip(self, detail=None): | |||||
"set skip status" | |||||
self.status = 'SKIP' | |||||
self.detail = detail | |||||
raise TCDone() | |||||
def autoclunk(self, fid): | |||||
"mark fid to be closed/clunked on test exit" | |||||
if self._acconn is None: | |||||
raise ValueError('autoclunk: no _acconn') | |||||
self._autoclunk.append(fid) | |||||
def trace(self, msg, *args, **kwargs): | |||||
"add tracing info to log-file output" | |||||
level = kwargs.pop('level', logging.INFO) | |||||
self.tstate.logger.log(level, ' ' + msg, *args, **kwargs) | |||||
def ccs(self): | |||||
"call tstate ccs, turn socket.error connect failure into test fail" | |||||
try: | |||||
self.detail = 'connecting' | |||||
ret = self.tstate.ccs() | |||||
self.detail = None | |||||
self._acconn = ret | |||||
return ret | |||||
except socket.error as err: | |||||
self.fail(str(err)) | |||||
def __enter__(self): | |||||
self.tstate.logger.log(logging.DEBUG, 'ENTER: %s', self.name) | |||||
self._autoclunk = [] | |||||
return self | |||||
def __exit__(self, exc_type, exc_val, exc_tb): | |||||
tstate = self.tstate | |||||
eat_exc = False | |||||
tb_detail = None | |||||
if exc_type is TCDone: | |||||
# we exited with succ, fail, or skip | |||||
eat_exc = True | |||||
exc_type = None | |||||
if exc_type is not None: | |||||
if self.status is None: | |||||
self.status = 'EXCP' | |||||
else: | |||||
self.status += ' EXC' | |||||
if exc_type == TEError: | |||||
# timeout/eof - best guess is that we crashed the server! | |||||
eat_exc = True | |||||
tb_detail = ['timeout or EOF'] | |||||
elif exc_type in (socket.error, RemoteError, LocalError): | |||||
eat_exc = True | |||||
tb_detail = traceback.format_exception(exc_type, exc_val, | |||||
exc_tb) | |||||
level = logging.ERROR | |||||
tstate.failures += 1 | |||||
tstate.exceptions += 1 | |||||
else: | |||||
if self.status is None: | |||||
self.status = 'SUCC' | |||||
if self.status == 'SUCC': | |||||
level = logging.INFO | |||||
tstate.successes += 1 | |||||
elif self.status == 'SKIP': | |||||
level = logging.INFO | |||||
tstate.skips += 1 | |||||
else: | |||||
level = logging.ERROR | |||||
tstate.failures += 1 | |||||
tstate.logger.log(level, '%s: %s', self.status, self.name) | |||||
if self.detail: | |||||
tstate.logger.log(level, ' detail: %s', self.detail) | |||||
if tb_detail: | |||||
for line in tb_detail: | |||||
tstate.logger.log(level, ' %s', line.rstrip()) | |||||
for fid in self._autoclunk: | |||||
self._acconn.clunk(fid, ignore_error=True) | |||||
if self._shutdown: | |||||
self._shutdown.shutdown() | |||||
return eat_exc | |||||
def main(): | |||||
"the usual main" | |||||
parser = argparse.ArgumentParser(description='run tests against a server') | |||||
parser.add_argument('-c', '--config', | |||||
action='append', | |||||
help='specify additional file(s) to read (beyond testconf.ini)') | |||||
args = parser.parse_args() | |||||
config = configparser.SafeConfigParser() | |||||
# use case sensitive keys | |||||
config.optionxform = str | |||||
try: | |||||
with open('testconf.ini', 'r') as stream: | |||||
config.readfp(stream) | |||||
except (OSError, IOError) as err: | |||||
sys.exit(str(err)) | |||||
if args.config: | |||||
ok = config.read(args.config) | |||||
failed = set(ok) - set(args.config) | |||||
if len(failed): | |||||
nfailed = len(failed) | |||||
word = 'files' if nfailed > 1 else 'file' | |||||
failed = ', '.join(failed) | |||||
print('failed to read {0} {1}: {2}'.format(nfailed, word, failed)) | |||||
sys.exit(1) | |||||
logging.basicConfig(level=config.get('client', 'loglevel').upper()) | |||||
logger = logging.getLogger(__name__) | |||||
tstate = TestState() | |||||
tstate.logger = logger | |||||
tstate.config = config | |||||
server = config.get('client', 'server') | |||||
port = config.getint('client', 'port') | |||||
proto = config.get('client', 'protocol') | |||||
may_downgrade = config.getboolean('client', 'may_downgrade') | |||||
timeout = config.getfloat('client', 'timeout') | |||||
tstate.stop = True # unless overwritten below | |||||
with TestCase('send bad packet', tstate) as tc: | |||||
tc.detail = 'connecting to {0}:{1}'.format(server, port) | |||||
try: | |||||
conn = p9conn.P9SockIO(logger, server=server, port=port) | |||||
except socket.error as err: | |||||
tc.fail('cannot connect at all (server down?)') | |||||
tc.auto_disconnect(conn) | |||||
tc.detail = None | |||||
pkt = struct.pack('<I', 256); | |||||
conn.write(pkt) | |||||
# ignore reply if any, we're just trying to trip the server | |||||
tstate.stop = False | |||||
tc.succ() | |||||
if not tstate.stop: | |||||
tstate.mkclient = functools.partial(p9conn.P9Client, logger, | |||||
timeout, proto, may_downgrade, | |||||
server=server, port=port) | |||||
tstate.stop = True | |||||
with TestCase('send bad Tversion', tstate) as tc: | |||||
try: | |||||
clnt = tstate.mkclient() | |||||
except socket.error as err: | |||||
tc.fail('can no longer connect, did bad pkt crash server?') | |||||
tc.auto_disconnect(clnt) | |||||
clnt.set_monkey('version', b'wrongo, fishbreath!') | |||||
tc.detail = 'connecting' | |||||
try: | |||||
clnt.connect() | |||||
except RemoteError as err: | |||||
tstate.stop = False | |||||
tc.succ(err.args[0]) | |||||
tc.fail('server accepted a bad Tversion') | |||||
if not tstate.stop: | |||||
# All NUL characters in strings are invalid. | |||||
with TestCase('send illegal NUL in Tversion', tstate) as tc: | |||||
clnt = tstate.mkclient() | |||||
tc.auto_disconnect(clnt) | |||||
clnt.set_monkey('version', b'9P2000\0') | |||||
# Forcibly allow downgrade so that Tversion | |||||
# succeeds if they ignore the \0. | |||||
clnt.may_downgrade = True | |||||
tc.detail = 'connecting' | |||||
try: | |||||
clnt.connect() | |||||
except (TEError, RemoteError) as err: | |||||
tc.succ(err.args[0]) | |||||
tc.fail('server accepted NUL in Tversion') | |||||
if not tstate.stop: | |||||
with TestCase('connect normally', tstate) as tc: | |||||
tc.detail = 'connecting' | |||||
try: | |||||
tstate.ccc() | |||||
except RemoteError as err: | |||||
# can't test any further, but this might be success | |||||
tstate.stop = True | |||||
if 'they only support version' in err.args[0]: | |||||
tc.succ(err.args[0]) | |||||
tc.fail(err.args[0]) | |||||
tc.succ() | |||||
if not tstate.stop: | |||||
with TestCase('attach with bad afid', tstate) as tc: | |||||
clnt = tstate.ccc()[0] | |||||
section = 'attach-with-bad-afid' | |||||
aname = getconf(tstate.config, section, 'aname', '') | |||||
uname = getconf(tstate.config, section, 'uname', '') | |||||
if clnt.proto > protocol.plain: | |||||
n_uname = getint(tstate.config, section, 'n_uname', 1001) | |||||
else: | |||||
n_uname = None | |||||
try: | |||||
clnt.attach(afid=42, aname=aname, uname=uname, n_uname=n_uname) | |||||
except RemoteError as err: | |||||
tc.succ(err.args[0]) | |||||
tc.dcc() | |||||
tc.fail('bad attach afid not rejected') | |||||
try: | |||||
if not tstate.stop: | |||||
# Various Linux tests need gids. Just get them for everyone. | |||||
tstate.gid = getint(tstate.config, 'client', 'gid', 0) | |||||
more_test_cases(tstate) | |||||
finally: | |||||
tstate.dcc() | |||||
n_tests = tstate.successes + tstate.failures | |||||
print('summary:') | |||||
if tstate.successes: | |||||
print('{0}/{1} tests succeeded'.format(tstate.successes, n_tests)) | |||||
if tstate.failures: | |||||
print('{0}/{1} tests failed'.format(tstate.failures, n_tests)) | |||||
if tstate.skips: | |||||
print('{0} {1} skipped'.format(tstate.skips, | |||||
pluralize(tstate.skips, | |||||
'test', 'tests'))) | |||||
if tstate.exceptions: | |||||
print('{0} {1} occurred'.format(tstate.exceptions, | |||||
pluralize(tstate.exceptions, | |||||
'exception', 'exceptions'))) | |||||
if tstate.stop: | |||||
print('tests stopped early') | |||||
return 1 if tstate.stop or tstate.exceptions or tstate.failures else 0 | |||||
def more_test_cases(tstate): | |||||
"run cases that can only proceed if connecting works at all" | |||||
with TestCase('attach normally', tstate) as tc: | |||||
tc.ccs() | |||||
tc.succ() | |||||
if tstate.stop: | |||||
return | |||||
# Empty string is not technically illegal. It's not clear | |||||
# whether it should be accepted or rejected. However, it | |||||
# used to crash the server entirely, so it's a desirable | |||||
# test case. | |||||
with TestCase('empty string in Twalk request', tstate) as tc: | |||||
clnt = tc.ccs() | |||||
try: | |||||
fid, qid = clnt.lookup(clnt.rootfid, [b'']) | |||||
except RemoteError as err: | |||||
tc.succ(err.args[0]) | |||||
clnt.clunk(fid) | |||||
tc.succ('note: empty Twalk component name not rejected') | |||||
# Name components may not contain / | |||||
with TestCase('embedded / in lookup component name', tstate) as tc: | |||||
clnt = tc.ccs() | |||||
try: | |||||
fid, qid = clnt.lookup(clnt.rootfid, [b'/']) | |||||
tc.autoclunk(fid) | |||||
except RemoteError as err: | |||||
tc.succ(err.args[0]) | |||||
tc.fail('/ in lookup component name not rejected') | |||||
# Proceed from a clean tree. As a side effect, this also tests | |||||
# either the old style readdir (read() on a directory fid) or | |||||
# the dot-L readdir(). | |||||
# | |||||
# The test case will fail if we don't have permission to remove | |||||
# some file(s). | |||||
with TestCase('clean up tree (readdir+remove)', tstate) as tc: | |||||
clnt = tc.ccs() | |||||
fset = clnt.uxreaddir(b'/') | |||||
fset = [i for i in fset if i != '.' and i != '..'] | |||||
tc.trace("what's there initially: {0!r}".format(fset)) | |||||
try: | |||||
clnt.uxremove(b'/', force=False, recurse=True) | |||||
except RemoteError as err: | |||||
tc.trace('failed to read or clean up tree', level=logging.ERROR) | |||||
tc.trace('this might be a permissions error', level=logging.ERROR) | |||||
tstate.stop = True | |||||
tc.fail(str(err)) | |||||
fset = clnt.uxreaddir(b'/') | |||||
fset = [i for i in fset if i != '.' and i != '..'] | |||||
tc.trace("what's left after removing everything: {0!r}".format(fset)) | |||||
if fset: | |||||
tstate.stop = True | |||||
tc.trace('note: could be a permissions error', level=logging.ERROR) | |||||
tc.fail('/ not empty after removing all: {0!r}'.format(fset)) | |||||
tc.succ() | |||||
if tstate.stop: | |||||
return | |||||
# Name supplied to create, mkdir, etc, may not contain /. | |||||
# Note that this test may fail for the wrong reason if /dir | |||||
# itself does not already exist, so first let's make /dir. | |||||
only_dotl = getbool(tstate.config, 'client', 'only_dotl', False) | |||||
with TestCase('mkdir', tstate) as tc: | |||||
clnt = tc.ccs() | |||||
if only_dotl and not clnt.supports(protocol.td.Tmkdir): | |||||
tc.skip('cannot test dot-L mkdir on {0}'.format(clnt.proto)) | |||||
try: | |||||
fid, qid = clnt.uxlookup(b'/dir', None) | |||||
tc.autoclunk(fid) | |||||
tstate.stop = True | |||||
tc.fail('found existing /dir after cleaning tree') | |||||
except RemoteError as err: | |||||
# we'll just assume it's "no such file or directory" | |||||
pass | |||||
if only_dotl: | |||||
qid = clnt.mkdir(clnt.rootfid, b'dir', 0o777, tstate.gid) | |||||
else: | |||||
qid, _ = clnt.create(clnt.rootfid, b'dir', | |||||
protocol.td.DMDIR | 0o777, | |||||
protocol.td.OREAD) | |||||
if qid.type != protocol.td.QTDIR: | |||||
tstate.stop = True | |||||
tc.fail('creating /dir: result is not a directory') | |||||
tc.trace('now attempting to create /dir/sub the wrong way') | |||||
try: | |||||
if only_dotl: | |||||
qid = clnt.mkdir(clnt.rootfid, b'dir/sub', 0o777, tstate.gid) | |||||
else: | |||||
qid, _ = clnt.create(clnt.rootfid, b'dir/sub', | |||||
protocol.td.DMDIR | 0o777, | |||||
protocol.td.OREAD) | |||||
# it's not clear what happened on the server at this point! | |||||
tc.trace("creating dir/sub (with embedded '/') should have " | |||||
'failed but did not') | |||||
tstate.stop = True | |||||
fset = clnt.uxreaddir(b'/dir') | |||||
if 'sub' in fset: | |||||
tc.trace('(found our dir/sub detritus)') | |||||
clnt.uxremove(b'dir/sub', force=True) | |||||
fset = clnt.uxreaddir(b'/dir') | |||||
if 'sub' not in fset: | |||||
tc.trace('(successfully removed our dir/sub detritus)') | |||||
tstate.stop = False | |||||
tc.fail('created dir/sub as single directory with embedded slash') | |||||
except RemoteError as err: | |||||
# we'll just assume it's the right kind of error | |||||
tc.trace('invalid path dir/sub failed with: %s', str(err)) | |||||
tc.succ('embedded slash in mkdir correctly refused') | |||||
if tstate.stop: | |||||
return | |||||
with TestCase('getattr/setattr', tstate) as tc: | |||||
# This test is not really thorough enough, need to test | |||||
# all combinations of settings. Should also test that | |||||
# old values are restored on failure, although it is not | |||||
# clear how to trigger failures. | |||||
clnt = tc.ccs() | |||||
if not clnt.supports(protocol.td.Tgetattr): | |||||
tc.skip('%s does not support Tgetattr', clnt) | |||||
fid, _, _, _ = clnt.uxopen(b'/dir/file', os.O_CREAT | os.O_RDWR, 0o666, | |||||
gid=tstate.gid) | |||||
tc.autoclunk(fid) | |||||
written = clnt.write(fid, 0, 'bytes\n') | |||||
if written != 6: | |||||
tc.trace('expected to write 6 bytes, actually wrote %d', written, | |||||
level=logging.WARN) | |||||
attrs = clnt.Tgetattr(fid) | |||||
#tc.trace('getattr: after write, before setattr: got %s', attrs) | |||||
if attrs.size != written: | |||||
tc.fail('getattr: expected size=%d, got size=%d', | |||||
written, attrs.size) | |||||
# now truncate, set mtime to (3,14), and check result | |||||
set_time_to = p9conn.Timespec(sec=0, nsec=140000000) | |||||
clnt.Tsetattr(fid, size=0, mtime=set_time_to) | |||||
attrs = clnt.Tgetattr(fid) | |||||
#tc.trace('getattr: after setattr: got %s', attrs) | |||||
if attrs.mtime.sec != set_time_to.sec or attrs.size != 0: | |||||
tc.fail('setattr: expected to get back mtime.sec={0}, size=0; ' | |||||
'got mtime.sec={1}, size=' | |||||
'{1}'.format(set_time_to.sec, attrs.mtime.sec, attrs.size)) | |||||
# nsec is not as stable but let's check | |||||
if attrs.mtime.nsec != set_time_to.nsec: | |||||
tc.trace('setattr: expected to get back mtime_nsec=%d; ' | |||||
'got %d', set_time_to.nsec, mtime_nsec) | |||||
tc.succ('able to set and see size and mtime') | |||||
# this test should be much later, but we know the current | |||||
# server is broken... | |||||
with TestCase('rename adjusts other fids', tstate) as tc: | |||||
clnt = tc.ccs() | |||||
dirfid, _ = clnt.uxlookup(b'/dir') | |||||
tc.autoclunk(dirfid) | |||||
clnt.uxmkdir(b'd1', 0o777, tstate.gid, startdir=dirfid) | |||||
clnt.uxmkdir(b'd1/sub', 0o777, tstate.gid, startdir=dirfid) | |||||
d1fid, _ = clnt.uxlookup(b'd1', dirfid) | |||||
tc.autoclunk(d1fid) | |||||
subfid, _ = clnt.uxlookup(b'sub', d1fid) | |||||
tc.autoclunk(subfid) | |||||
fid, _, _, _ = clnt.uxopen(b'file', os.O_CREAT | os.O_RDWR, | |||||
0o666, startdir=subfid, gid=tstate.gid) | |||||
tc.autoclunk(fid) | |||||
written = clnt.write(fid, 0, 'filedata\n') | |||||
if written != 9: | |||||
tc.trace('expected to write 9 bytes, actually wrote %d', written, | |||||
level=logging.WARN) | |||||
# Now if we rename /dir/d1 to /dir/d2, the fids for both | |||||
# sub/file and sub itself should still be usable. This | |||||
# holds for both Trename (Linux only) and Twstat based | |||||
# rename ops. | |||||
# | |||||
# Note that some servers may cache some number of files and/or | |||||
# diretories held open, so we should open many fids to wipe | |||||
# out the cache (XXX notyet). | |||||
if clnt.supports(protocol.td.Trename): | |||||
clnt.rename(d1fid, dirfid, name=b'd2') | |||||
else: | |||||
clnt.wstat(d1fid, name=b'd2') | |||||
try: | |||||
rofid, _, _, _ = clnt.uxopen(b'file', os.O_RDONLY, startdir=subfid) | |||||
clnt.clunk(rofid) | |||||
except RemoteError as err: | |||||
tc.fail('open file in renamed dir/d2/sub: {0}'.format(err)) | |||||
tc.succ() | |||||
# Even if xattrwalk is supported by the protocol, it's optional | |||||
# on the server. | |||||
with TestCase('xattrwalk', tstate) as tc: | |||||
clnt = tc.ccs() | |||||
if not clnt.supports(protocol.td.Txattrwalk): | |||||
tc.skip('{0} does not support Txattrwalk'.format(clnt)) | |||||
dirfid, _ = clnt.uxlookup(b'/dir') | |||||
tc.autoclunk(dirfid) | |||||
try: | |||||
# need better tests... | |||||
attrfid, size = clnt.xattrwalk(dirfid) | |||||
tc.autoclunk(attrfid) | |||||
data = clnt.read(attrfid, 0, size) | |||||
tc.trace('xattrwalk with no name: data=%r', data) | |||||
tc.succ('xattrwalk size={0} datalen={1}'.format(size, len(data))) | |||||
except RemoteError as err: | |||||
tc.trace('xattrwalk on /dir: {0}'.format(err)) | |||||
tc.succ('xattrwalk apparently not implemented') | |||||
if __name__ == '__main__': | |||||
try: | |||||
sys.exit(main()) | |||||
except KeyboardInterrupt: | |||||
sys.exit('\nInterrupted') |