4 Run various tests, as a client.
7 from __future__ import print_function
11 import ConfigParser as configparser
26 LocalError = p9conn.LocalError
27 RemoteError = p9conn.RemoteError
28 TEError = p9conn.TEError
30 class TestState(object):
43 def ccc(self, cid=None):
45 Connect or reconnect as client (ccc = check and connect client).
47 If caller provides a cid (client ID) we check that specific
48 client. Otherwise the default ID ('base') is used.
49 In any case we return the now-connected client, plus the
50 attachment (session info) if any.
54 pair = self.clnt_tab.get(cid)
56 clnt = self.mkclient()
58 self.clnt_tab[cid] = pair
61 if not clnt.is_connected():
65 def dcc(self, cid=None):
67 Disconnect client (disconnect checked client). If no specific
68 client ID is provided, this disconnects ALL checked clients!
71 for cid in list(self.clnt_tab.keys()):
73 pair = self.clnt_tab.get(cid)
76 if clnt.is_connected():
78 del self.clnt_tab[cid]
80 def ccs(self, cid=None):
82 Like ccc, but establish a session as well, by setting up
85 Return the client instance (only).
90 # No session yet - establish one. Note, this may fail.
91 section = None if cid is None else ('client-' + cid)
92 aname = getconf(self.config, section, 'aname', '')
93 uname = getconf(self.config, section, 'uname', '')
94 if clnt.proto > protocol.plain:
95 n_uname = getint(self.config, section, 'n_uname', 1001)
98 clnt.attach(afid=None, aname=aname, uname=uname, n_uname=n_uname)
99 pair[1] = (aname, uname, n_uname)
102 def getconf(conf, section, name, default=None, rtype=str):
104 Get configuration item for given section, or for "client" if
105 there is no entry for that particular section (or if section
108 This lets us get specific values for specific tests or
109 groups ([foo] name=value), falling back to general values
110 ([client] name=value).
112 The type of the returned value <rtype> can be str, int, bool,
113 or float. The default is str (and see getconfint, getconfbool,
116 A default value may be supplied; if it is, that's the default
117 return value (this default should have the right type). If
118 no default is supplied, a missing value is an error.
121 # note: conf.get(None, 'foo') raises NoSectionError
123 result = conf.get(where, name)
124 except (configparser.NoSectionError, configparser.NoOptionError):
127 result = conf.get(where, name)
128 except configparser.NoSectionError:
129 sys.exit('no [{0}] section in configuration!'.format(where))
130 except configparser.NoOptionError:
131 if default is not None:
133 if section is not None:
134 where = '[{0}] or [{1}]'.format(section, where)
136 where = '[{0}]'.format(where)
137 raise LocalError('need {0}=value in {1}'.format(name, where))
138 where = '[{0}]'.format(where)
146 if result.lower() in ('1', 't', 'true', 'y', 'yes'):
148 if result.lower() in ('0', 'f', 'false', 'n', 'no'):
150 raise ValueError('{0} {1}={2}: invalid boolean'.format(where, name,
152 raise ValueError('{0} {1}={2}: internal error: bad result type '
153 '{3!r}'.format(where, name, result, rtype))
155 def getint(conf, section, name, default=None):
156 "get integer config item"
157 return getconf(conf, section, name, default, int)
159 def getfloat(conf, section, name, default=None):
160 "get float config item"
161 return getconf(conf, section, name, default, float)
163 def getbool(conf, section, name, default=None):
164 "get boolean config item"
165 return getconf(conf, section, name, default, bool)
167 def pluralize(n, singular, plural):
168 "return singular or plural based on value of n"
169 return plural if n != 1 else singular
171 class TCDone(Exception):
172 "used in succ/fail/skip - skips rest of testcase with"
175 class TestCase(object):
177 Start a test case. Most callers must then do a ccs() to connect.
179 A failed test will generally disconnect from the server; a
180 new ccs() will reconnect, if the server is still alive.
182 def __init__(self, name, tstate):
187 self._shutdown = None
188 self._autoclunk = None
191 def auto_disconnect(self, conn):
192 self._shutdown = conn
194 def succ(self, detail=None):
200 def fail(self, detail):
206 def skip(self, detail=None):
212 def autoclunk(self, fid):
213 "mark fid to be closed/clunked on test exit"
214 if self._acconn is None:
215 raise ValueError('autoclunk: no _acconn')
216 self._autoclunk.append(fid)
218 def trace(self, msg, *args, **kwargs):
219 "add tracing info to log-file output"
220 level = kwargs.pop('level', logging.INFO)
221 self.tstate.logger.log(level, ' ' + msg, *args, **kwargs)
224 "call tstate ccs, turn socket.error connect failure into test fail"
226 self.detail = 'connecting'
227 ret = self.tstate.ccs()
231 except socket.error as err:
235 self.tstate.logger.log(logging.DEBUG, 'ENTER: %s', self.name)
239 def __exit__(self, exc_type, exc_val, exc_tb):
243 if exc_type is TCDone:
244 # we exited with succ, fail, or skip
247 if exc_type is not None:
248 if self.status is None:
251 self.status += ' EXC'
252 if exc_type == TEError:
253 # timeout/eof - best guess is that we crashed the server!
255 tb_detail = ['timeout or EOF']
256 elif exc_type in (socket.error, RemoteError, LocalError):
258 tb_detail = traceback.format_exception(exc_type, exc_val,
260 level = logging.ERROR
262 tstate.exceptions += 1
264 if self.status is None:
266 if self.status == 'SUCC':
268 tstate.successes += 1
269 elif self.status == 'SKIP':
273 level = logging.ERROR
275 tstate.logger.log(level, '%s: %s', self.status, self.name)
277 tstate.logger.log(level, ' detail: %s', self.detail)
279 for line in tb_detail:
280 tstate.logger.log(level, ' %s', line.rstrip())
281 for fid in self._autoclunk:
282 self._acconn.clunk(fid, ignore_error=True)
284 self._shutdown.shutdown()
289 parser = argparse.ArgumentParser(description='run tests against a server')
291 parser.add_argument('-c', '--config',
293 help='specify additional file(s) to read (beyond testconf.ini)')
295 args = parser.parse_args()
296 config = configparser.SafeConfigParser()
297 # use case sensitive keys
298 config.optionxform = str
301 with open('testconf.ini', 'r') as stream:
302 config.readfp(stream)
303 except (OSError, IOError) as err:
306 ok = config.read(args.config)
307 failed = set(ok) - set(args.config)
309 nfailed = len(failed)
310 word = 'files' if nfailed > 1 else 'file'
311 failed = ', '.join(failed)
312 print('failed to read {0} {1}: {2}'.format(nfailed, word, failed))
315 logging.basicConfig(level=config.get('client', 'loglevel').upper())
316 logger = logging.getLogger(__name__)
318 tstate.logger = logger
319 tstate.config = config
321 server = config.get('client', 'server')
322 port = config.getint('client', 'port')
323 proto = config.get('client', 'protocol')
324 may_downgrade = config.getboolean('client', 'may_downgrade')
325 timeout = config.getfloat('client', 'timeout')
327 tstate.stop = True # unless overwritten below
328 with TestCase('send bad packet', tstate) as tc:
329 tc.detail = 'connecting to {0}:{1}'.format(server, port)
331 conn = p9conn.P9SockIO(logger, server=server, port=port)
332 except socket.error as err:
333 tc.fail('cannot connect at all (server down?)')
334 tc.auto_disconnect(conn)
336 pkt = struct.pack('<I', 256);
338 # ignore reply if any, we're just trying to trip the server
343 tstate.mkclient = functools.partial(p9conn.P9Client, logger,
344 timeout, proto, may_downgrade,
345 server=server, port=port)
347 with TestCase('send bad Tversion', tstate) as tc:
349 clnt = tstate.mkclient()
350 except socket.error as err:
351 tc.fail('can no longer connect, did bad pkt crash server?')
352 tc.auto_disconnect(clnt)
353 clnt.set_monkey('version', b'wrongo, fishbreath!')
354 tc.detail = 'connecting'
357 except RemoteError as err:
360 tc.fail('server accepted a bad Tversion')
363 # All NUL characters in strings are invalid.
364 with TestCase('send illegal NUL in Tversion', tstate) as tc:
365 clnt = tstate.mkclient()
366 tc.auto_disconnect(clnt)
367 clnt.set_monkey('version', b'9P2000\0')
368 # Forcibly allow downgrade so that Tversion
369 # succeeds if they ignore the \0.
370 clnt.may_downgrade = True
371 tc.detail = 'connecting'
374 except (TEError, RemoteError) as err:
376 tc.fail('server accepted NUL in Tversion')
379 with TestCase('connect normally', tstate) as tc:
380 tc.detail = 'connecting'
383 except RemoteError as err:
384 # can't test any further, but this might be success
386 if 'they only support version' in err.args[0]:
392 with TestCase('attach with bad afid', tstate) as tc:
393 clnt = tstate.ccc()[0]
394 section = 'attach-with-bad-afid'
395 aname = getconf(tstate.config, section, 'aname', '')
396 uname = getconf(tstate.config, section, 'uname', '')
397 if clnt.proto > protocol.plain:
398 n_uname = getint(tstate.config, section, 'n_uname', 1001)
402 clnt.attach(afid=42, aname=aname, uname=uname, n_uname=n_uname)
403 except RemoteError as err:
406 tc.fail('bad attach afid not rejected')
410 # Various Linux tests need gids. Just get them for everyone.
411 tstate.gid = getint(tstate.config, 'client', 'gid', 0)
412 more_test_cases(tstate)
416 n_tests = tstate.successes + tstate.failures
419 print('{0}/{1} tests succeeded'.format(tstate.successes, n_tests))
421 print('{0}/{1} tests failed'.format(tstate.failures, n_tests))
423 print('{0} {1} skipped'.format(tstate.skips,
424 pluralize(tstate.skips,
426 if tstate.exceptions:
427 print('{0} {1} occurred'.format(tstate.exceptions,
428 pluralize(tstate.exceptions,
429 'exception', 'exceptions')))
431 print('tests stopped early')
432 return 1 if tstate.stop or tstate.exceptions or tstate.failures else 0
434 def more_test_cases(tstate):
435 "run cases that can only proceed if connecting works at all"
436 with TestCase('attach normally', tstate) as tc:
442 # Empty string is not technically illegal. It's not clear
443 # whether it should be accepted or rejected. However, it
444 # used to crash the server entirely, so it's a desirable
446 with TestCase('empty string in Twalk request', tstate) as tc:
449 fid, qid = clnt.lookup(clnt.rootfid, [b''])
450 except RemoteError as err:
453 tc.succ('note: empty Twalk component name not rejected')
455 # Name components may not contain /
456 with TestCase('embedded / in lookup component name', tstate) as tc:
459 fid, qid = clnt.lookup(clnt.rootfid, [b'/'])
461 except RemoteError as err:
463 tc.fail('/ in lookup component name not rejected')
465 # Proceed from a clean tree. As a side effect, this also tests
466 # either the old style readdir (read() on a directory fid) or
467 # the dot-L readdir().
469 # The test case will fail if we don't have permission to remove
471 with TestCase('clean up tree (readdir+remove)', tstate) as tc:
473 fset = clnt.uxreaddir(b'/')
474 fset = [i for i in fset if i != '.' and i != '..']
475 tc.trace("what's there initially: {0!r}".format(fset))
477 clnt.uxremove(b'/', force=False, recurse=True)
478 except RemoteError as err:
479 tc.trace('failed to read or clean up tree', level=logging.ERROR)
480 tc.trace('this might be a permissions error', level=logging.ERROR)
483 fset = clnt.uxreaddir(b'/')
484 fset = [i for i in fset if i != '.' and i != '..']
485 tc.trace("what's left after removing everything: {0!r}".format(fset))
488 tc.trace('note: could be a permissions error', level=logging.ERROR)
489 tc.fail('/ not empty after removing all: {0!r}'.format(fset))
494 # Name supplied to create, mkdir, etc, may not contain /.
495 # Note that this test may fail for the wrong reason if /dir
496 # itself does not already exist, so first let's make /dir.
497 only_dotl = getbool(tstate.config, 'client', 'only_dotl', False)
498 with TestCase('mkdir', tstate) as tc:
500 if only_dotl and not clnt.supports(protocol.td.Tmkdir):
501 tc.skip('cannot test dot-L mkdir on {0}'.format(clnt.proto))
503 fid, qid = clnt.uxlookup(b'/dir', None)
506 tc.fail('found existing /dir after cleaning tree')
507 except RemoteError as err:
508 # we'll just assume it's "no such file or directory"
511 qid = clnt.mkdir(clnt.rootfid, b'dir', 0o777, tstate.gid)
513 qid, _ = clnt.create(clnt.rootfid, b'dir',
514 protocol.td.DMDIR | 0o777,
516 if qid.type != protocol.td.QTDIR:
518 tc.fail('creating /dir: result is not a directory')
519 tc.trace('now attempting to create /dir/sub the wrong way')
522 qid = clnt.mkdir(clnt.rootfid, b'dir/sub', 0o777, tstate.gid)
524 qid, _ = clnt.create(clnt.rootfid, b'dir/sub',
525 protocol.td.DMDIR | 0o777,
527 # it's not clear what happened on the server at this point!
528 tc.trace("creating dir/sub (with embedded '/') should have "
529 'failed but did not')
531 fset = clnt.uxreaddir(b'/dir')
533 tc.trace('(found our dir/sub detritus)')
534 clnt.uxremove(b'dir/sub', force=True)
535 fset = clnt.uxreaddir(b'/dir')
536 if 'sub' not in fset:
537 tc.trace('(successfully removed our dir/sub detritus)')
539 tc.fail('created dir/sub as single directory with embedded slash')
540 except RemoteError as err:
541 # we'll just assume it's the right kind of error
542 tc.trace('invalid path dir/sub failed with: %s', str(err))
543 tc.succ('embedded slash in mkdir correctly refused')
547 with TestCase('getattr/setattr', tstate) as tc:
548 # This test is not really thorough enough, need to test
549 # all combinations of settings. Should also test that
550 # old values are restored on failure, although it is not
551 # clear how to trigger failures.
553 if not clnt.supports(protocol.td.Tgetattr):
554 tc.skip('%s does not support Tgetattr', clnt)
555 fid, _, _, _ = clnt.uxopen(b'/dir/file', os.O_CREAT | os.O_RDWR, 0o666,
558 written = clnt.write(fid, 0, 'bytes\n')
560 tc.trace('expected to write 6 bytes, actually wrote %d', written,
562 attrs = clnt.Tgetattr(fid)
563 #tc.trace('getattr: after write, before setattr: got %s', attrs)
564 if attrs.size != written:
565 tc.fail('getattr: expected size=%d, got size=%d',
567 # now truncate, set mtime to (3,14), and check result
568 set_time_to = p9conn.Timespec(sec=0, nsec=140000000)
569 clnt.Tsetattr(fid, size=0, mtime=set_time_to)
570 attrs = clnt.Tgetattr(fid)
571 #tc.trace('getattr: after setattr: got %s', attrs)
572 if attrs.mtime.sec != set_time_to.sec or attrs.size != 0:
573 tc.fail('setattr: expected to get back mtime.sec={0}, size=0; '
574 'got mtime.sec={1}, size='
575 '{1}'.format(set_time_to.sec, attrs.mtime.sec, attrs.size))
576 # nsec is not as stable but let's check
577 if attrs.mtime.nsec != set_time_to.nsec:
578 tc.trace('setattr: expected to get back mtime_nsec=%d; '
579 'got %d', set_time_to.nsec, mtime_nsec)
580 tc.succ('able to set and see size and mtime')
582 # this test should be much later, but we know the current
583 # server is broken...
584 with TestCase('rename adjusts other fids', tstate) as tc:
586 dirfid, _ = clnt.uxlookup(b'/dir')
588 clnt.uxmkdir(b'd1', 0o777, tstate.gid, startdir=dirfid)
589 clnt.uxmkdir(b'd1/sub', 0o777, tstate.gid, startdir=dirfid)
590 d1fid, _ = clnt.uxlookup(b'd1', dirfid)
592 subfid, _ = clnt.uxlookup(b'sub', d1fid)
594 fid, _, _, _ = clnt.uxopen(b'file', os.O_CREAT | os.O_RDWR,
595 0o666, startdir=subfid, gid=tstate.gid)
597 written = clnt.write(fid, 0, 'filedata\n')
599 tc.trace('expected to write 9 bytes, actually wrote %d', written,
601 # Now if we rename /dir/d1 to /dir/d2, the fids for both
602 # sub/file and sub itself should still be usable. This
603 # holds for both Trename (Linux only) and Twstat based
606 # Note that some servers may cache some number of files and/or
607 # diretories held open, so we should open many fids to wipe
608 # out the cache (XXX notyet).
609 if clnt.supports(protocol.td.Trename):
610 clnt.rename(d1fid, dirfid, name=b'd2')
612 clnt.wstat(d1fid, name=b'd2')
614 rofid, _, _, _ = clnt.uxopen(b'file', os.O_RDONLY, startdir=subfid)
616 except RemoteError as err:
617 tc.fail('open file in renamed dir/d2/sub: {0}'.format(err))
620 # Even if xattrwalk is supported by the protocol, it's optional
622 with TestCase('xattrwalk', tstate) as tc:
624 if not clnt.supports(protocol.td.Txattrwalk):
625 tc.skip('{0} does not support Txattrwalk'.format(clnt))
626 dirfid, _ = clnt.uxlookup(b'/dir')
629 # need better tests...
630 attrfid, size = clnt.xattrwalk(dirfid)
631 tc.autoclunk(attrfid)
632 data = clnt.read(attrfid, 0, size)
633 tc.trace('xattrwalk with no name: data=%r', data)
634 tc.succ('xattrwalk size={0} datalen={1}'.format(size, len(data)))
635 except RemoteError as err:
636 tc.trace('xattrwalk on /dir: {0}'.format(err))
637 tc.succ('xattrwalk apparently not implemented')
639 if __name__ == '__main__':
642 except KeyboardInterrupt:
643 sys.exit('\nInterrupted')