run-tests.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. #!/usr/bin/python
  2. #
  3. # AP tests
  4. # Copyright (c) 2013-2014, Jouni Malinen <j@w1.fi>
  5. #
  6. # This software may be distributed under the terms of the BSD license.
  7. # See README for more details.
  8. import os
  9. import re
  10. import sys
  11. import time
  12. from datetime import datetime
  13. import argparse
  14. import subprocess
  15. import logging
  16. logger = logging.getLogger()
  17. sys.path.append('../../wpaspy')
  18. from wpasupplicant import WpaSupplicant
  19. from hostapd import HostapdGlobal
  20. from check_kernel import check_kernel
  21. from wlantest import Wlantest
  22. def reset_devs(dev, apdev):
  23. ok = True
  24. for d in dev:
  25. try:
  26. d.reset()
  27. except Exception, e:
  28. logger.info("Failed to reset device " + d.ifname)
  29. print str(e)
  30. ok = False
  31. try:
  32. wpas = WpaSupplicant(global_iface='/tmp/wpas-wlan5')
  33. wpas.interface_remove("wlan5")
  34. except Exception, e:
  35. pass
  36. try:
  37. hapd = HostapdGlobal()
  38. hapd.remove('wlan3-3')
  39. hapd.remove('wlan3-2')
  40. for ap in apdev:
  41. hapd.remove(ap['ifname'])
  42. except Exception, e:
  43. logger.info("Failed to remove hostapd interface")
  44. print str(e)
  45. ok = False
  46. return ok
  47. def add_log_file(conn, test, run, type, path):
  48. if not os.path.exists(path):
  49. return
  50. contents = None
  51. with open(path, 'r') as f:
  52. contents = f.read()
  53. if contents is None:
  54. return
  55. sql = "INSERT INTO logs(test,run,type,contents) VALUES(?, ?, ?, ?)"
  56. params = (test, run, type, contents)
  57. try:
  58. conn.execute(sql, params)
  59. conn.commit()
  60. except Exception, e:
  61. print "sqlite: " + str(e)
  62. print "sql: %r" % (params, )
  63. def report(conn, prefill, build, commit, run, test, result, duration, logdir):
  64. if conn:
  65. if not build:
  66. build = ''
  67. if not commit:
  68. commit = ''
  69. if prefill:
  70. conn.execute('DELETE FROM results WHERE test=? AND run=? AND result=?', (test, run, 'NOTRUN'))
  71. sql = "INSERT INTO results(test,result,run,time,duration,build,commitid) VALUES(?, ?, ?, ?, ?, ?, ?)"
  72. params = (test, result, run, time.time(), duration, build, commit)
  73. try:
  74. conn.execute(sql, params)
  75. conn.commit()
  76. except Exception, e:
  77. print "sqlite: " + str(e)
  78. print "sql: %r" % (params, )
  79. if result == "FAIL":
  80. for log in [ "log", "log0", "log1", "log2", "log3", "log5",
  81. "hostapd", "dmesg", "hwsim0", "hwsim0.pcapng" ]:
  82. add_log_file(conn, test, run, log,
  83. logdir + "/" + test + "." + log)
  84. class DataCollector(object):
  85. def __init__(self, logdir, testname, tracing, dmesg):
  86. self._logdir = logdir
  87. self._testname = testname
  88. self._tracing = tracing
  89. self._dmesg = dmesg
  90. def __enter__(self):
  91. if self._tracing:
  92. output = os.path.join(self._logdir, '%s.dat' % (self._testname, ))
  93. self._trace_cmd = subprocess.Popen(['sudo', 'trace-cmd', 'record', '-o', output, '-e', 'mac80211', '-e', 'cfg80211', 'sh', '-c', 'echo STARTED ; read l'],
  94. stdin=subprocess.PIPE,
  95. stdout=subprocess.PIPE,
  96. stderr=open('/dev/null', 'w'),
  97. cwd=self._logdir)
  98. l = self._trace_cmd.stdout.read(7)
  99. while not 'STARTED' in l:
  100. l += self._trace_cmd.stdout.read(1)
  101. def __exit__(self, type, value, traceback):
  102. if self._tracing:
  103. self._trace_cmd.stdin.write('DONE\n')
  104. self._trace_cmd.wait()
  105. if self._dmesg:
  106. output = os.path.join(self._logdir, '%s.dmesg' % (self._testname, ))
  107. subprocess.call(['sudo', 'dmesg', '-c'], stdout=open(output, 'w'))
  108. def rename_log(logdir, basename, testname, dev):
  109. try:
  110. import getpass
  111. srcname = os.path.join(logdir, basename)
  112. dstname = os.path.join(logdir, testname + '.' + basename)
  113. num = 0
  114. while os.path.exists(dstname):
  115. dstname = os.path.join(logdir,
  116. testname + '.' + basename + '-' + str(num))
  117. num = num + 1
  118. os.rename(srcname, dstname)
  119. if dev:
  120. dev.relog()
  121. subprocess.call(['sudo', 'chown', '-f', getpass.getuser(), srcname])
  122. except Exception, e:
  123. logger.info("Failed to rename log files")
  124. logger.info(e)
  125. def main():
  126. tests = []
  127. test_modules = []
  128. for t in os.listdir("."):
  129. m = re.match(r'(test_.*)\.py$', t)
  130. if m:
  131. logger.debug("Import test cases from " + t)
  132. mod = __import__(m.group(1))
  133. test_modules.append(mod.__name__.replace('test_', '', 1))
  134. for s in dir(mod):
  135. if s.startswith("test_"):
  136. func = mod.__dict__.get(s)
  137. tests.append(func)
  138. test_names = list(set([t.__name__.replace('test_', '', 1) for t in tests]))
  139. run = None
  140. parser = argparse.ArgumentParser(description='hwsim test runner')
  141. parser.add_argument('--logdir', metavar='<directory>',
  142. help='log output directory for all other options, ' +
  143. 'must be given if other log options are used')
  144. group = parser.add_mutually_exclusive_group()
  145. group.add_argument('-d', const=logging.DEBUG, action='store_const',
  146. dest='loglevel', default=logging.INFO,
  147. help="verbose debug output")
  148. group.add_argument('-q', const=logging.WARNING, action='store_const',
  149. dest='loglevel', help="be quiet")
  150. parser.add_argument('-S', metavar='<sqlite3 db>', dest='database',
  151. help='database to write results to')
  152. parser.add_argument('--prefill-tests', action='store_true', dest='prefill',
  153. help='prefill test database with NOTRUN before all tests')
  154. parser.add_argument('--commit', metavar='<commit id>',
  155. help='commit ID, only for database')
  156. parser.add_argument('-b', metavar='<build>', dest='build', help='build ID')
  157. parser.add_argument('-L', action='store_true', dest='update_tests_db',
  158. help='List tests (and update descriptions in DB)')
  159. parser.add_argument('-T', action='store_true', dest='tracing',
  160. help='collect tracing per test case (in log directory)')
  161. parser.add_argument('-D', action='store_true', dest='dmesg',
  162. help='collect dmesg per test case (in log directory)')
  163. parser.add_argument('--shuffle-tests', action='store_true',
  164. dest='shuffle_tests',
  165. help='Shuffle test cases to randomize order')
  166. parser.add_argument('--no-reset', action='store_true', dest='no_reset',
  167. help='Do not reset devices at the end of the test')
  168. parser.add_argument('-f', dest='testmodules', metavar='<test module>',
  169. help='execute only tests from these test modules',
  170. type=str, choices=[[]] + test_modules, nargs='+')
  171. parser.add_argument('tests', metavar='<test>', nargs='*', type=str,
  172. help='tests to run (only valid without -f)',
  173. choices=[[]] + test_names)
  174. args = parser.parse_args()
  175. if args.tests and args.testmodules:
  176. print 'Invalid arguments - both test module and tests given'
  177. sys.exit(2)
  178. if not args.logdir:
  179. if os.path.exists('logs/current'):
  180. args.logdir = 'logs/current'
  181. else:
  182. args.logdir = 'logs'
  183. # Write debug level log to a file and configurable verbosity to stdout
  184. logger.setLevel(logging.DEBUG)
  185. stdout_handler = logging.StreamHandler()
  186. stdout_handler.setLevel(args.loglevel)
  187. logger.addHandler(stdout_handler)
  188. file_name = os.path.join(args.logdir, 'run-tests.log')
  189. log_handler = logging.FileHandler(file_name)
  190. log_handler.setLevel(logging.DEBUG)
  191. fmt = "%(asctime)s %(levelname)s %(message)s"
  192. log_formatter = logging.Formatter(fmt)
  193. log_handler.setFormatter(log_formatter)
  194. logger.addHandler(log_handler)
  195. if args.database:
  196. import sqlite3
  197. conn = sqlite3.connect(args.database)
  198. conn.execute('CREATE TABLE IF NOT EXISTS results (test,result,run,time,duration,build,commitid)')
  199. conn.execute('CREATE TABLE IF NOT EXISTS tests (test,description)')
  200. conn.execute('CREATE TABLE IF NOT EXISTS logs (test,run,type,contents)')
  201. else:
  202. conn = None
  203. if conn:
  204. run = int(time.time())
  205. if args.update_tests_db:
  206. for t in tests:
  207. name = t.__name__.replace('test_', '', 1)
  208. if t.__doc__ is None:
  209. print name + " - MISSING DESCRIPTION"
  210. else:
  211. print name + " - " + t.__doc__
  212. if conn:
  213. sql = 'INSERT OR REPLACE INTO tests(test,description) VALUES (?, ?)'
  214. params = (name, t.__doc__)
  215. try:
  216. conn.execute(sql, params)
  217. except Exception, e:
  218. print "sqlite: " + str(e)
  219. print "sql: %r" % (params,)
  220. if conn:
  221. conn.commit()
  222. conn.close()
  223. sys.exit(0)
  224. dev0 = WpaSupplicant('wlan0', '/tmp/wpas-wlan0')
  225. dev1 = WpaSupplicant('wlan1', '/tmp/wpas-wlan1')
  226. dev2 = WpaSupplicant('wlan2', '/tmp/wpas-wlan2')
  227. dev = [ dev0, dev1, dev2 ]
  228. apdev = [ ]
  229. apdev.append({"ifname": 'wlan3', "bssid": "02:00:00:00:03:00"})
  230. apdev.append({"ifname": 'wlan4', "bssid": "02:00:00:00:04:00"})
  231. for d in dev:
  232. if not d.ping():
  233. logger.info(d.ifname + ": No response from wpa_supplicant")
  234. return
  235. logger.info("DEV: " + d.ifname + ": " + d.p2p_dev_addr())
  236. for ap in apdev:
  237. logger.info("APDEV: " + ap['ifname'])
  238. passed = []
  239. skipped = []
  240. failed = []
  241. # make sure nothing is left over from previous runs
  242. # (if there were any other manual runs or we crashed)
  243. if not reset_devs(dev, apdev):
  244. if conn:
  245. conn.close()
  246. conn = None
  247. sys.exit(1)
  248. if args.dmesg:
  249. subprocess.call(['sudo', 'dmesg', '-c'], stdout=open('/dev/null', 'w'))
  250. tests_to_run = []
  251. for t in tests:
  252. name = t.__name__.replace('test_', '', 1)
  253. if args.tests:
  254. if not name in args.tests:
  255. continue
  256. if args.testmodules:
  257. if not t.__module__.replace('test_', '', 1) in args.testmodules:
  258. continue
  259. tests_to_run.append(t)
  260. if conn and args.prefill:
  261. for t in tests_to_run:
  262. name = t.__name__.replace('test_', '', 1)
  263. report(conn, False, args.build, args.commit, run, name, 'NOTRUN', 0,
  264. args.logdir)
  265. if args.shuffle_tests:
  266. from random import shuffle
  267. shuffle(tests_to_run)
  268. count = 0
  269. for t in tests_to_run:
  270. name = t.__name__.replace('test_', '', 1)
  271. if log_handler:
  272. log_handler.stream.close()
  273. logger.removeHandler(log_handler)
  274. file_name = os.path.join(args.logdir, name + '.log')
  275. log_handler = logging.FileHandler(file_name)
  276. log_handler.setLevel(logging.DEBUG)
  277. log_handler.setFormatter(log_formatter)
  278. logger.addHandler(log_handler)
  279. reset_ok = True
  280. with DataCollector(args.logdir, name, args.tracing, args.dmesg):
  281. count = count + 1
  282. msg = "START {} {}/{}".format(name, count, len(tests_to_run))
  283. logger.info(msg)
  284. if args.loglevel == logging.WARNING:
  285. print msg
  286. sys.stdout.flush()
  287. if t.__doc__:
  288. logger.info("Test: " + t.__doc__)
  289. start = datetime.now()
  290. for d in dev:
  291. try:
  292. d.request("NOTE TEST-START " + name)
  293. except Exception, e:
  294. logger.info("Failed to issue TEST-START before " + name + " for " + d.ifname)
  295. logger.info(e)
  296. print "FAIL " + name + " - could not start test"
  297. if conn:
  298. conn.close()
  299. conn = None
  300. sys.exit(1)
  301. try:
  302. if t.func_code.co_argcount > 1:
  303. res = t(dev, apdev)
  304. else:
  305. res = t(dev)
  306. if res == "skip":
  307. result = "SKIP"
  308. else:
  309. result = "PASS"
  310. except Exception, e:
  311. logger.info(e)
  312. result = "FAIL"
  313. for d in dev:
  314. try:
  315. d.request("NOTE TEST-STOP " + name)
  316. except Exception, e:
  317. logger.info("Failed to issue TEST-STOP after {} for {}".format(name, d.ifname))
  318. logger.info(e)
  319. result = "FAIL"
  320. try:
  321. wpas = WpaSupplicant("wlan5", "/tmp/wpas-wlan5")
  322. rename_log(args.logdir, 'log5', name, wpas)
  323. if not args.no_reset:
  324. wpas.remove_ifname()
  325. except Exception, e:
  326. pass
  327. if args.no_reset:
  328. print "Leaving devices in current state"
  329. else:
  330. reset_ok = reset_devs(dev, apdev)
  331. for i in range(0, 3):
  332. rename_log(args.logdir, 'log' + str(i), name, dev[i])
  333. try:
  334. hapd = HostapdGlobal()
  335. except Exception, e:
  336. print "Failed to connect to hostapd interface"
  337. print str(e)
  338. reset_ok = False
  339. result = "FAIL"
  340. hapd = None
  341. rename_log(args.logdir, 'hostapd', name, hapd)
  342. wt = Wlantest()
  343. rename_log(args.logdir, 'hwsim0.pcapng', name, wt)
  344. rename_log(args.logdir, 'hwsim0', name, wt)
  345. end = datetime.now()
  346. diff = end - start
  347. if result == 'PASS' and args.dmesg:
  348. if not check_kernel(os.path.join(args.logdir, name + '.dmesg')):
  349. logger.info("Kernel issue found in dmesg - mark test failed")
  350. result = 'FAIL'
  351. if result == 'PASS':
  352. passed.append(name)
  353. elif result == 'SKIP':
  354. skipped.append(name)
  355. else:
  356. failed.append(name)
  357. report(conn, args.prefill, args.build, args.commit, run, name, result,
  358. diff.total_seconds(), args.logdir)
  359. result = "{} {} {} {}".format(result, name, diff.total_seconds(), end)
  360. logger.info(result)
  361. if args.loglevel == logging.WARNING:
  362. print result
  363. sys.stdout.flush()
  364. if not reset_ok:
  365. print "Terminating early due to device reset failure"
  366. break
  367. if log_handler:
  368. log_handler.stream.close()
  369. logger.removeHandler(log_handler)
  370. file_name = os.path.join(args.logdir, 'run-tests.log')
  371. log_handler = logging.FileHandler(file_name)
  372. log_handler.setLevel(logging.DEBUG)
  373. log_handler.setFormatter(log_formatter)
  374. logger.addHandler(log_handler)
  375. if conn:
  376. conn.close()
  377. if len(failed):
  378. logger.info("passed {} test case(s)".format(len(passed)))
  379. logger.info("skipped {} test case(s)".format(len(skipped)))
  380. logger.info("failed tests: " + ' '.join(failed))
  381. if args.loglevel == logging.WARNING:
  382. print "failed tests: " + ' '.join(failed)
  383. sys.exit(1)
  384. logger.info("passed all {} test case(s)".format(len(passed)))
  385. if len(skipped):
  386. logger.info("skipped {} test case(s)".format(len(skipped)))
  387. if args.loglevel == logging.WARNING:
  388. print "passed all {} test case(s)".format(len(passed))
  389. if len(skipped):
  390. print "skipped {} test case(s)".format(len(skipped))
  391. if __name__ == "__main__":
  392. main()