]> CyberLeo.Net >> Repos - FreeBSD/FreeBSD.git/blob - contrib/lib9p/pytest/client.py
Optionally bind ktls threads to NUMA domains
[FreeBSD/FreeBSD.git] / contrib / lib9p / pytest / client.py
1 #! /usr/bin/env python
2
3 """
4 Run various tests, as a client.
5 """
6
7 from __future__ import print_function
8
9 import argparse
10 try:
11     import ConfigParser as configparser
12 except ImportError:
13     import configparser
14 import functools
15 import logging
16 import os
17 import socket
18 import struct
19 import sys
20 import time
21 import traceback
22
23 import p9conn
24 import protocol
25
26 LocalError = p9conn.LocalError
27 RemoteError = p9conn.RemoteError
28 TEError = p9conn.TEError
29
30 class TestState(object):
31     def __init__(self):
32         self.config = None
33         self.logger = None
34         self.successes = 0
35         self.skips = 0
36         self.failures = 0
37         self.exceptions = 0
38         self.clnt_tab = {}
39         self.mkclient = None
40         self.stop = False
41         self.gid = 0
42
43     def ccc(self, cid=None):
44         """
45         Connect or reconnect as client (ccc = check and connect client).
46
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.
51         """
52         if cid is None:
53             cid = 'base'
54         pair = self.clnt_tab.get(cid)
55         if pair is None:
56             clnt = self.mkclient()
57             pair = [clnt, None]
58             self.clnt_tab[cid] = pair
59         else:
60             clnt = pair[0]
61         if not clnt.is_connected():
62             clnt.connect()
63         return pair
64
65     def dcc(self, cid=None):
66         """
67         Disconnect client (disconnect checked client).  If no specific
68         client ID is provided, this disconnects ALL checked clients!
69         """
70         if cid is None:
71             for cid in list(self.clnt_tab.keys()):
72                 self.dcc(cid)
73         pair = self.clnt_tab.get(cid)
74         if pair is not None:
75             clnt = pair[0]
76             if clnt.is_connected():
77                 clnt.shutdown()
78             del self.clnt_tab[cid]
79
80     def ccs(self, cid=None):
81         """
82         Like ccc, but establish a session as well, by setting up
83         the uname/n_uname.
84
85         Return the client instance (only).
86         """
87         pair = self.ccc(cid)
88         clnt = pair[0]
89         if pair[1] is None:
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)
96             else:
97                 n_uname = None
98             clnt.attach(afid=None, aname=aname, uname=uname, n_uname=n_uname)
99             pair[1] = (aname, uname, n_uname)
100         return clnt
101
102 def getconf(conf, section, name, default=None, rtype=str):
103     """
104     Get configuration item for given section, or for "client" if
105     there is no entry for that particular section (or if section
106     is None).
107
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).
111
112     The type of the returned value <rtype> can be str, int, bool,
113     or float.  The default is str (and see getconfint, getconfbool,
114     getconffloat below).
115
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.
119     """
120     try:
121         # note: conf.get(None, 'foo') raises NoSectionError
122         where = section
123         result = conf.get(where, name)
124     except (configparser.NoSectionError, configparser.NoOptionError):
125         try:
126             where = 'client'
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:
132                 return default
133             if section is not None:
134                 where = '[{0}] or [{1}]'.format(section, where)
135             else:
136                 where = '[{0}]'.format(where)
137             raise LocalError('need {0}=value in {1}'.format(name, where))
138     where = '[{0}]'.format(where)
139     if rtype is str:
140         return result
141     if rtype is int:
142         return int(result)
143     if rtype is float:
144         return float(result)
145     if rtype is bool:
146         if result.lower() in ('1', 't', 'true', 'y', 'yes'):
147             return True
148         if result.lower() in ('0', 'f', 'false', 'n', 'no'):
149             return False
150         raise ValueError('{0} {1}={2}: invalid boolean'.format(where, name,
151                                                               result))
152     raise ValueError('{0} {1}={2}: internal error: bad result type '
153                      '{3!r}'.format(where, name, result, rtype))
154
155 def getint(conf, section, name, default=None):
156     "get integer config item"
157     return getconf(conf, section, name, default, int)
158
159 def getfloat(conf, section, name, default=None):
160     "get float config item"
161     return getconf(conf, section, name, default, float)
162
163 def getbool(conf, section, name, default=None):
164     "get boolean config item"
165     return getconf(conf, section, name, default, bool)
166
167 def pluralize(n, singular, plural):
168     "return singular or plural based on value of n"
169     return plural if n != 1 else singular
170
171 class TCDone(Exception):
172     "used in succ/fail/skip - skips rest of testcase with"
173     pass
174
175 class TestCase(object):
176     """
177     Start a test case.  Most callers must then do a ccs() to connect.
178
179     A failed test will generally disconnect from the server; a
180     new ccs() will reconnect, if the server is still alive.
181     """
182     def __init__(self, name, tstate):
183         self.name = name
184         self.status = None
185         self.detail = None
186         self.tstate = tstate
187         self._shutdown = None
188         self._autoclunk = None
189         self._acconn = None
190
191     def auto_disconnect(self, conn):
192         self._shutdown = conn
193
194     def succ(self, detail=None):
195         "set success status"
196         self.status = 'SUCC'
197         self.detail = detail
198         raise TCDone()
199
200     def fail(self, detail):
201         "set failure status"
202         self.status = 'FAIL'
203         self.detail = detail
204         raise TCDone()
205
206     def skip(self, detail=None):
207         "set skip status"
208         self.status = 'SKIP'
209         self.detail = detail
210         raise TCDone()
211
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)
217
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)
222
223     def ccs(self):
224         "call tstate ccs, turn socket.error connect failure into test fail"
225         try:
226             self.detail = 'connecting'
227             ret = self.tstate.ccs()
228             self.detail = None
229             self._acconn = ret
230             return ret
231         except socket.error as err:
232             self.fail(str(err))
233
234     def __enter__(self):
235         self.tstate.logger.log(logging.DEBUG, 'ENTER: %s', self.name)
236         self._autoclunk = []
237         return self
238
239     def __exit__(self, exc_type, exc_val, exc_tb):
240         tstate = self.tstate
241         eat_exc = False
242         tb_detail = None
243         if exc_type is TCDone:
244             # we exited with succ, fail, or skip
245             eat_exc = True
246             exc_type = None
247         if exc_type is not None:
248             if self.status is None:
249                 self.status = 'EXCP'
250             else:
251                 self.status += ' EXC'
252             if exc_type == TEError:
253                 # timeout/eof - best guess is that we crashed the server!
254                 eat_exc = True
255                 tb_detail = ['timeout or EOF']
256             elif exc_type in (socket.error, RemoteError, LocalError):
257                 eat_exc = True
258                 tb_detail = traceback.format_exception(exc_type, exc_val,
259                                                        exc_tb)
260             level = logging.ERROR
261             tstate.failures += 1
262             tstate.exceptions += 1
263         else:
264             if self.status is None:
265                 self.status = 'SUCC'
266             if self.status == 'SUCC':
267                 level = logging.INFO
268                 tstate.successes += 1
269             elif self.status == 'SKIP':
270                 level = logging.INFO
271                 tstate.skips += 1
272             else:
273                 level = logging.ERROR
274                 tstate.failures += 1
275         tstate.logger.log(level, '%s: %s', self.status, self.name)
276         if self.detail:
277             tstate.logger.log(level, '      detail: %s', self.detail)
278         if tb_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)
283         if self._shutdown:
284             self._shutdown.shutdown()
285         return eat_exc
286
287 def main():
288     "the usual main"
289     parser = argparse.ArgumentParser(description='run tests against a server')
290
291     parser.add_argument('-c', '--config',
292         action='append',
293         help='specify additional file(s) to read (beyond testconf.ini)')
294
295     args = parser.parse_args()
296     config = configparser.SafeConfigParser()
297     # use case sensitive keys
298     config.optionxform = str
299
300     try:
301         with open('testconf.ini', 'r') as stream:
302             config.readfp(stream)
303     except (OSError, IOError) as err:
304         sys.exit(str(err))
305     if args.config:
306         ok = config.read(args.config)
307         failed = set(ok) - set(args.config)
308         if len(failed):
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))
313             sys.exit(1)
314
315     logging.basicConfig(level=config.get('client', 'loglevel').upper())
316     logger = logging.getLogger(__name__)
317     tstate = TestState()
318     tstate.logger = logger
319     tstate.config = config
320
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')
326
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)
330         try:
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)
335         tc.detail = None
336         pkt = struct.pack('<I', 256);
337         conn.write(pkt)
338         # ignore reply if any, we're just trying to trip the server
339         tstate.stop = False
340         tc.succ()
341
342     if not tstate.stop:
343         tstate.mkclient = functools.partial(p9conn.P9Client, logger,
344                                            timeout, proto, may_downgrade,
345                                            server=server, port=port)
346         tstate.stop = True
347         with TestCase('send bad Tversion', tstate) as tc:
348             try:
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'
355             try:
356                 clnt.connect()
357             except RemoteError as err:
358                 tstate.stop = False
359                 tc.succ(err.args[0])
360             tc.fail('server accepted a bad Tversion')
361
362     if not tstate.stop:
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'
372             try:
373                 clnt.connect()
374             except (TEError, RemoteError) as err:
375                 tc.succ(err.args[0])
376             tc.fail('server accepted NUL in Tversion')
377
378     if not tstate.stop:
379         with TestCase('connect normally', tstate) as tc:
380             tc.detail = 'connecting'
381             try:
382                 tstate.ccc()
383             except RemoteError as err:
384                 # can't test any further, but this might be success
385                 tstate.stop = True
386                 if 'they only support version' in err.args[0]:
387                     tc.succ(err.args[0])
388                 tc.fail(err.args[0])
389             tc.succ()
390
391     if not tstate.stop:
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)
399             else:
400                 n_uname = None
401             try:
402                 clnt.attach(afid=42, aname=aname, uname=uname, n_uname=n_uname)
403             except RemoteError as err:
404                 tc.succ(err.args[0])
405             tc.dcc()
406             tc.fail('bad attach afid not rejected')
407
408     try:
409         if not tstate.stop:
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)
413     finally:
414         tstate.dcc()
415
416     n_tests = tstate.successes + tstate.failures
417     print('summary:')
418     if tstate.successes:
419         print('{0}/{1} tests succeeded'.format(tstate.successes, n_tests))
420     if tstate.failures:
421         print('{0}/{1} tests failed'.format(tstate.failures, n_tests))
422     if tstate.skips:
423         print('{0} {1} skipped'.format(tstate.skips,
424                                        pluralize(tstate.skips,
425                                                  'test', 'tests')))
426     if tstate.exceptions:
427         print('{0} {1} occurred'.format(tstate.exceptions,
428                                        pluralize(tstate.exceptions,
429                                                  'exception', 'exceptions')))
430     if tstate.stop:
431         print('tests stopped early')
432     return 1 if tstate.stop or tstate.exceptions or tstate.failures else 0
433
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:
437         tc.ccs()
438         tc.succ()
439     if tstate.stop:
440         return
441
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
445     # test case.
446     with TestCase('empty string in Twalk request', tstate) as tc:
447         clnt = tc.ccs()
448         try:
449             fid, qid = clnt.lookup(clnt.rootfid, [b''])
450         except RemoteError as err:
451             tc.succ(err.args[0])
452         clnt.clunk(fid)
453         tc.succ('note: empty Twalk component name not rejected')
454
455     # Name components may not contain /
456     with TestCase('embedded / in lookup component name', tstate) as tc:
457         clnt = tc.ccs()
458         try:
459             fid, qid = clnt.lookup(clnt.rootfid, [b'/'])
460             tc.autoclunk(fid)
461         except RemoteError as err:
462             tc.succ(err.args[0])
463         tc.fail('/ in lookup component name not rejected')
464
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().
468     #
469     # The test case will fail if we don't have permission to remove
470     # some file(s).
471     with TestCase('clean up tree (readdir+remove)', tstate) as tc:
472         clnt = tc.ccs()
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))
476         try:
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)
481             tstate.stop = True
482             tc.fail(str(err))
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))
486         if fset:
487             tstate.stop = True
488             tc.trace('note: could be a permissions error', level=logging.ERROR)
489             tc.fail('/ not empty after removing all: {0!r}'.format(fset))
490         tc.succ()
491     if tstate.stop:
492         return
493
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:
499         clnt = tc.ccs()
500         if only_dotl and not clnt.supports(protocol.td.Tmkdir):
501             tc.skip('cannot test dot-L mkdir on {0}'.format(clnt.proto))
502         try:
503             fid, qid = clnt.uxlookup(b'/dir', None)
504             tc.autoclunk(fid)
505             tstate.stop = True
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"
509             pass
510         if only_dotl:
511             qid = clnt.mkdir(clnt.rootfid, b'dir', 0o777, tstate.gid)
512         else:
513             qid, _ = clnt.create(clnt.rootfid, b'dir',
514                                  protocol.td.DMDIR | 0o777,
515                                  protocol.td.OREAD)
516         if qid.type != protocol.td.QTDIR:
517             tstate.stop = True
518             tc.fail('creating /dir: result is not a directory')
519         tc.trace('now attempting to create /dir/sub the wrong way')
520         try:
521             if only_dotl:
522                 qid = clnt.mkdir(clnt.rootfid, b'dir/sub', 0o777, tstate.gid)
523             else:
524                 qid, _ = clnt.create(clnt.rootfid, b'dir/sub',
525                                      protocol.td.DMDIR | 0o777,
526                                      protocol.td.OREAD)
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')
530             tstate.stop = True
531             fset = clnt.uxreaddir(b'/dir')
532             if 'sub' in fset:
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)')
538                     tstate.stop = False
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')
544     if tstate.stop:
545         return
546
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.
552         clnt = tc.ccs()
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,
556             gid=tstate.gid)
557         tc.autoclunk(fid)
558         written = clnt.write(fid, 0, 'bytes\n')
559         if written != 6:
560             tc.trace('expected to write 6 bytes, actually wrote %d', written,
561                      level=logging.WARN)
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',
566                     written, attrs.size)
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')
581
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:
585         clnt = tc.ccs()
586         dirfid, _ = clnt.uxlookup(b'/dir')
587         tc.autoclunk(dirfid)
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)
591         tc.autoclunk(d1fid)
592         subfid, _ = clnt.uxlookup(b'sub', d1fid)
593         tc.autoclunk(subfid)
594         fid, _, _, _ = clnt.uxopen(b'file', os.O_CREAT | os.O_RDWR,
595                                    0o666, startdir=subfid, gid=tstate.gid)
596         tc.autoclunk(fid)
597         written = clnt.write(fid, 0, 'filedata\n')
598         if written != 9:
599             tc.trace('expected to write 9 bytes, actually wrote %d', written,
600                      level=logging.WARN)
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
604         # rename ops.
605         #
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')
611         else:
612             clnt.wstat(d1fid, name=b'd2')
613         try:
614             rofid, _, _, _ = clnt.uxopen(b'file', os.O_RDONLY, startdir=subfid)
615             clnt.clunk(rofid)
616         except RemoteError as err:
617             tc.fail('open file in renamed dir/d2/sub: {0}'.format(err))
618         tc.succ()
619
620     # Even if xattrwalk is supported by the protocol, it's optional
621     # on the server.
622     with TestCase('xattrwalk', tstate) as tc:
623         clnt = tc.ccs()
624         if not clnt.supports(protocol.td.Txattrwalk):
625             tc.skip('{0} does not support Txattrwalk'.format(clnt))
626         dirfid, _ = clnt.uxlookup(b'/dir')
627         tc.autoclunk(dirfid)
628         try:
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')
638
639 if __name__ == '__main__':
640     try:
641         sys.exit(main())
642     except KeyboardInterrupt:
643         sys.exit('\nInterrupted')