run-tests.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594
  1. #!/usr/bin/env python2
  2. #
  3. # Test case executor
  4. # Copyright (c) 2013-2015, 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 termios
  16. import logging
  17. logger = logging.getLogger()
  18. try:
  19. import sqlite3
  20. sqlite3_imported = True
  21. except ImportError:
  22. sqlite3_imported = False
  23. scriptsdir = os.path.dirname(os.path.realpath(sys.modules[__name__].__file__))
  24. sys.path.append(os.path.join(scriptsdir, '..', '..', 'wpaspy'))
  25. from wpasupplicant import WpaSupplicant
  26. from hostapd import HostapdGlobal
  27. from check_kernel import check_kernel
  28. from wlantest import Wlantest
  29. from utils import HwsimSkip
  30. def set_term_echo(fd, enabled):
  31. [iflag, oflag, cflag, lflag, ispeed, ospeed, cc] = termios.tcgetattr(fd)
  32. if enabled:
  33. lflag |= termios.ECHO
  34. else:
  35. lflag &= ~termios.ECHO
  36. termios.tcsetattr(fd, termios.TCSANOW,
  37. [iflag, oflag, cflag, lflag, ispeed, ospeed, cc])
  38. def reset_devs(dev, apdev):
  39. ok = True
  40. for d in dev:
  41. try:
  42. d.reset()
  43. except Exception, e:
  44. logger.info("Failed to reset device " + d.ifname)
  45. print str(e)
  46. ok = False
  47. wpas = None
  48. try:
  49. wpas = WpaSupplicant(global_iface='/tmp/wpas-wlan5', monitor=False)
  50. ifaces = wpas.global_request("INTERFACES").splitlines()
  51. for iface in ifaces:
  52. if iface.startswith("wlan"):
  53. wpas.interface_remove(iface)
  54. except Exception, e:
  55. pass
  56. if wpas:
  57. wpas.close_ctrl()
  58. try:
  59. hapd = HostapdGlobal()
  60. hapd.flush()
  61. hapd.remove('wlan3-6')
  62. hapd.remove('wlan3-5')
  63. hapd.remove('wlan3-4')
  64. hapd.remove('wlan3-3')
  65. hapd.remove('wlan3-2')
  66. for ap in apdev:
  67. hapd.remove(ap['ifname'])
  68. except Exception, e:
  69. logger.info("Failed to remove hostapd interface")
  70. print str(e)
  71. ok = False
  72. return ok
  73. def add_log_file(conn, test, run, type, path):
  74. if not os.path.exists(path):
  75. return
  76. contents = None
  77. with open(path, 'r') as f:
  78. contents = f.read()
  79. if contents is None:
  80. return
  81. sql = "INSERT INTO logs(test,run,type,contents) VALUES(?, ?, ?, ?)"
  82. params = (test, run, type, sqlite3.Binary(contents))
  83. try:
  84. conn.execute(sql, params)
  85. conn.commit()
  86. except Exception, e:
  87. print "sqlite: " + str(e)
  88. print "sql: %r" % (params, )
  89. def report(conn, prefill, build, commit, run, test, result, duration, logdir,
  90. sql_commit=True):
  91. if conn:
  92. if not build:
  93. build = ''
  94. if not commit:
  95. commit = ''
  96. if prefill:
  97. conn.execute('DELETE FROM results WHERE test=? AND run=? AND result=?', (test, run, 'NOTRUN'))
  98. sql = "INSERT INTO results(test,result,run,time,duration,build,commitid) VALUES(?, ?, ?, ?, ?, ?, ?)"
  99. params = (test, result, run, time.time(), duration, build, commit)
  100. try:
  101. conn.execute(sql, params)
  102. if sql_commit:
  103. conn.commit()
  104. except Exception, e:
  105. print "sqlite: " + str(e)
  106. print "sql: %r" % (params, )
  107. if result == "FAIL":
  108. for log in [ "log", "log0", "log1", "log2", "log3", "log5",
  109. "hostapd", "dmesg", "hwsim0", "hwsim0.pcapng" ]:
  110. add_log_file(conn, test, run, log,
  111. logdir + "/" + test + "." + log)
  112. class DataCollector(object):
  113. def __init__(self, logdir, testname, args):
  114. self._logdir = logdir
  115. self._testname = testname
  116. self._tracing = args.tracing
  117. self._dmesg = args.dmesg
  118. self._dbus = args.dbus
  119. def __enter__(self):
  120. if self._tracing:
  121. output = os.path.abspath(os.path.join(self._logdir, '%s.dat' % (self._testname, )))
  122. self._trace_cmd = subprocess.Popen(['trace-cmd', 'record', '-o', output, '-e', 'mac80211', '-e', 'cfg80211', '-e', 'printk', 'sh', '-c', 'echo STARTED ; read l'],
  123. stdin=subprocess.PIPE,
  124. stdout=subprocess.PIPE,
  125. stderr=open('/dev/null', 'w'),
  126. cwd=self._logdir)
  127. l = self._trace_cmd.stdout.read(7)
  128. while self._trace_cmd.poll() is None and not 'STARTED' in l:
  129. l += self._trace_cmd.stdout.read(1)
  130. res = self._trace_cmd.returncode
  131. if res:
  132. print "Failed calling trace-cmd: returned exit status %d" % res
  133. sys.exit(1)
  134. if self._dbus:
  135. output = os.path.abspath(os.path.join(self._logdir, '%s.dbus' % (self._testname, )))
  136. self._dbus_cmd = subprocess.Popen(['dbus-monitor', '--system'],
  137. stdout=open(output, 'w'),
  138. stderr=open('/dev/null', 'w'),
  139. cwd=self._logdir)
  140. res = self._dbus_cmd.returncode
  141. if res:
  142. print "Failed calling dbus-monitor: returned exit status %d" % res
  143. sys.exit(1)
  144. def __exit__(self, type, value, traceback):
  145. if self._tracing:
  146. self._trace_cmd.stdin.write('DONE\n')
  147. self._trace_cmd.wait()
  148. if self._dmesg:
  149. output = os.path.join(self._logdir, '%s.dmesg' % (self._testname, ))
  150. num = 0
  151. while os.path.exists(output):
  152. output = os.path.join(self._logdir, '%s.dmesg-%d' % (self._testname, num))
  153. num += 1
  154. subprocess.call(['dmesg', '-c'], stdout=open(output, 'w'))
  155. def rename_log(logdir, basename, testname, dev):
  156. try:
  157. import getpass
  158. srcname = os.path.join(logdir, basename)
  159. dstname = os.path.join(logdir, testname + '.' + basename)
  160. num = 0
  161. while os.path.exists(dstname):
  162. dstname = os.path.join(logdir,
  163. testname + '.' + basename + '-' + str(num))
  164. num = num + 1
  165. os.rename(srcname, dstname)
  166. if dev:
  167. dev.relog()
  168. subprocess.call(['chown', '-f', getpass.getuser(), srcname])
  169. except Exception, e:
  170. logger.info("Failed to rename log files")
  171. logger.info(e)
  172. def main():
  173. tests = []
  174. test_modules = []
  175. files = os.listdir(scriptsdir)
  176. for t in files:
  177. m = re.match(r'(test_.*)\.py$', t)
  178. if m:
  179. logger.debug("Import test cases from " + t)
  180. mod = __import__(m.group(1))
  181. test_modules.append(mod.__name__.replace('test_', '', 1))
  182. for key,val in mod.__dict__.iteritems():
  183. if key.startswith("test_"):
  184. tests.append(val)
  185. test_names = list(set([t.__name__.replace('test_', '', 1) for t in tests]))
  186. run = None
  187. parser = argparse.ArgumentParser(description='hwsim test runner')
  188. parser.add_argument('--logdir', metavar='<directory>',
  189. help='log output directory for all other options, ' +
  190. 'must be given if other log options are used')
  191. group = parser.add_mutually_exclusive_group()
  192. group.add_argument('-d', const=logging.DEBUG, action='store_const',
  193. dest='loglevel', default=logging.INFO,
  194. help="verbose debug output")
  195. group.add_argument('-q', const=logging.WARNING, action='store_const',
  196. dest='loglevel', help="be quiet")
  197. parser.add_argument('-S', metavar='<sqlite3 db>', dest='database',
  198. help='database to write results to')
  199. parser.add_argument('--prefill-tests', action='store_true', dest='prefill',
  200. help='prefill test database with NOTRUN before all tests')
  201. parser.add_argument('--commit', metavar='<commit id>',
  202. help='commit ID, only for database')
  203. parser.add_argument('-b', metavar='<build>', dest='build', help='build ID')
  204. parser.add_argument('-L', action='store_true', dest='update_tests_db',
  205. help='List tests (and update descriptions in DB)')
  206. parser.add_argument('-T', action='store_true', dest='tracing',
  207. help='collect tracing per test case (in log directory)')
  208. parser.add_argument('-D', action='store_true', dest='dmesg',
  209. help='collect dmesg per test case (in log directory)')
  210. parser.add_argument('--dbus', action='store_true', dest='dbus',
  211. help='collect dbus per test case (in log directory)')
  212. parser.add_argument('--shuffle-tests', action='store_true',
  213. dest='shuffle_tests',
  214. help='Shuffle test cases to randomize order')
  215. parser.add_argument('--split', help='split tests for parallel execution (<server number>/<total servers>)')
  216. parser.add_argument('--no-reset', action='store_true', dest='no_reset',
  217. help='Do not reset devices at the end of the test')
  218. parser.add_argument('--long', action='store_true',
  219. help='Include test cases that take long time')
  220. parser.add_argument('-f', dest='testmodules', metavar='<test module>',
  221. help='execute only tests from these test modules',
  222. type=str, choices=[[]] + test_modules, nargs='+')
  223. parser.add_argument('-l', metavar='<modules file>', dest='mfile',
  224. help='test modules file name')
  225. parser.add_argument('-i', action='store_true', dest='stdin_ctrl',
  226. help='stdin-controlled test case execution')
  227. parser.add_argument('tests', metavar='<test>', nargs='*', type=str,
  228. help='tests to run (only valid without -f)',
  229. choices=[[]] + test_names)
  230. args = parser.parse_args()
  231. if (args.tests and args.testmodules) or (args.tests and args.mfile) or (args.testmodules and args.mfile):
  232. print 'Invalid arguments - only one of (test, test modules, modules file) can be given.'
  233. sys.exit(2)
  234. if args.database:
  235. if not sqlite3_imported:
  236. print "No sqlite3 module found"
  237. sys.exit(2)
  238. conn = sqlite3.connect(args.database)
  239. conn.execute('CREATE TABLE IF NOT EXISTS results (test,result,run,time,duration,build,commitid)')
  240. conn.execute('CREATE TABLE IF NOT EXISTS tests (test,description)')
  241. conn.execute('CREATE TABLE IF NOT EXISTS logs (test,run,type,contents)')
  242. else:
  243. conn = None
  244. if conn:
  245. run = int(time.time())
  246. # read the modules from the modules file
  247. if args.mfile:
  248. args.testmodules = []
  249. with open(args.mfile) as f:
  250. for line in f.readlines():
  251. line = line.strip()
  252. if not line or line.startswith('#'):
  253. continue
  254. args.testmodules.append(line)
  255. tests_to_run = []
  256. if args.tests:
  257. for selected in args.tests:
  258. for t in tests:
  259. name = t.__name__.replace('test_', '', 1)
  260. if name == selected:
  261. tests_to_run.append(t)
  262. else:
  263. for t in tests:
  264. name = t.__name__.replace('test_', '', 1)
  265. if args.testmodules:
  266. if not t.__module__.replace('test_', '', 1) in args.testmodules:
  267. continue
  268. tests_to_run.append(t)
  269. if args.update_tests_db:
  270. for t in tests_to_run:
  271. name = t.__name__.replace('test_', '', 1)
  272. if t.__doc__ is None:
  273. print name + " - MISSING DESCRIPTION"
  274. else:
  275. print name + " - " + t.__doc__
  276. if conn:
  277. sql = 'INSERT OR REPLACE INTO tests(test,description) VALUES (?, ?)'
  278. params = (name, t.__doc__)
  279. try:
  280. conn.execute(sql, params)
  281. except Exception, e:
  282. print "sqlite: " + str(e)
  283. print "sql: %r" % (params,)
  284. if conn:
  285. conn.commit()
  286. conn.close()
  287. sys.exit(0)
  288. if not args.logdir:
  289. if os.path.exists('logs/current'):
  290. args.logdir = 'logs/current'
  291. else:
  292. args.logdir = 'logs'
  293. # Write debug level log to a file and configurable verbosity to stdout
  294. logger.setLevel(logging.DEBUG)
  295. stdout_handler = logging.StreamHandler()
  296. stdout_handler.setLevel(args.loglevel)
  297. logger.addHandler(stdout_handler)
  298. file_name = os.path.join(args.logdir, 'run-tests.log')
  299. log_handler = logging.FileHandler(file_name)
  300. log_handler.setLevel(logging.DEBUG)
  301. fmt = "%(asctime)s %(levelname)s %(message)s"
  302. log_formatter = logging.Formatter(fmt)
  303. log_handler.setFormatter(log_formatter)
  304. logger.addHandler(log_handler)
  305. dev0 = WpaSupplicant('wlan0', '/tmp/wpas-wlan0')
  306. dev1 = WpaSupplicant('wlan1', '/tmp/wpas-wlan1')
  307. dev2 = WpaSupplicant('wlan2', '/tmp/wpas-wlan2')
  308. dev = [ dev0, dev1, dev2 ]
  309. apdev = [ ]
  310. apdev.append({"ifname": 'wlan3', "bssid": "02:00:00:00:03:00"})
  311. apdev.append({"ifname": 'wlan4', "bssid": "02:00:00:00:04:00"})
  312. for d in dev:
  313. if not d.ping():
  314. logger.info(d.ifname + ": No response from wpa_supplicant")
  315. return
  316. logger.info("DEV: " + d.ifname + ": " + d.p2p_dev_addr())
  317. for ap in apdev:
  318. logger.info("APDEV: " + ap['ifname'])
  319. passed = []
  320. skipped = []
  321. failed = []
  322. # make sure nothing is left over from previous runs
  323. # (if there were any other manual runs or we crashed)
  324. if not reset_devs(dev, apdev):
  325. if conn:
  326. conn.close()
  327. conn = None
  328. sys.exit(1)
  329. if args.dmesg:
  330. subprocess.call(['dmesg', '-c'], stdout=open('/dev/null', 'w'))
  331. if conn and args.prefill:
  332. for t in tests_to_run:
  333. name = t.__name__.replace('test_', '', 1)
  334. report(conn, False, args.build, args.commit, run, name, 'NOTRUN', 0,
  335. args.logdir, sql_commit=False)
  336. conn.commit()
  337. if args.split:
  338. vals = args.split.split('/')
  339. split_server = int(vals[0])
  340. split_total = int(vals[1])
  341. logger.info("Parallel execution - %d/%d" % (split_server, split_total))
  342. split_server -= 1
  343. tests_to_run.sort(key=lambda t: t.__name__)
  344. tests_to_run = [x for i,x in enumerate(tests_to_run) if i % split_total == split_server]
  345. if args.shuffle_tests:
  346. from random import shuffle
  347. shuffle(tests_to_run)
  348. count = 0
  349. if args.stdin_ctrl:
  350. print "READY"
  351. sys.stdout.flush()
  352. num_tests = 0
  353. else:
  354. num_tests = len(tests_to_run)
  355. if args.stdin_ctrl:
  356. set_term_echo(sys.stdin.fileno(), False)
  357. while True:
  358. if args.stdin_ctrl:
  359. test = sys.stdin.readline()
  360. if not test:
  361. break
  362. test = test.splitlines()[0]
  363. if test == '':
  364. break
  365. t = None
  366. for tt in tests:
  367. name = tt.__name__.replace('test_', '', 1)
  368. if name == test:
  369. t = tt
  370. break
  371. if not t:
  372. print "NOT-FOUND"
  373. sys.stdout.flush()
  374. continue
  375. else:
  376. if len(tests_to_run) == 0:
  377. break
  378. t = tests_to_run.pop(0)
  379. name = t.__name__.replace('test_', '', 1)
  380. open('/dev/kmsg', 'w').write('running hwsim test case %s\n' % name)
  381. if log_handler:
  382. log_handler.stream.close()
  383. logger.removeHandler(log_handler)
  384. file_name = os.path.join(args.logdir, name + '.log')
  385. log_handler = logging.FileHandler(file_name)
  386. log_handler.setLevel(logging.DEBUG)
  387. log_handler.setFormatter(log_formatter)
  388. logger.addHandler(log_handler)
  389. reset_ok = True
  390. with DataCollector(args.logdir, name, args):
  391. count = count + 1
  392. msg = "START {} {}/{}".format(name, count, num_tests)
  393. logger.info(msg)
  394. if args.loglevel == logging.WARNING:
  395. print msg
  396. sys.stdout.flush()
  397. if t.__doc__:
  398. logger.info("Test: " + t.__doc__)
  399. start = datetime.now()
  400. open('/dev/kmsg', 'w').write('TEST-START %s @%.6f\n' % (name, time.time()))
  401. for d in dev:
  402. try:
  403. d.dump_monitor()
  404. if not d.ping():
  405. raise Exception("PING failed for {}".format(d.ifname))
  406. if not d.global_ping():
  407. raise Exception("Global PING failed for {}".format(d.ifname))
  408. d.request("NOTE TEST-START " + name)
  409. except Exception, e:
  410. logger.info("Failed to issue TEST-START before " + name + " for " + d.ifname)
  411. logger.info(e)
  412. print "FAIL " + name + " - could not start test"
  413. if conn:
  414. conn.close()
  415. conn = None
  416. if args.stdin_ctrl:
  417. set_term_echo(sys.stdin.fileno(), True)
  418. sys.exit(1)
  419. try:
  420. if t.func_code.co_argcount > 2:
  421. params = {}
  422. params['logdir'] = args.logdir
  423. params['long'] = args.long
  424. t(dev, apdev, params)
  425. elif t.func_code.co_argcount > 1:
  426. t(dev, apdev)
  427. else:
  428. t(dev)
  429. result = "PASS"
  430. except HwsimSkip, e:
  431. logger.info("Skip test case: %s" % e)
  432. result = "SKIP"
  433. except NameError, e:
  434. import traceback
  435. logger.info(e)
  436. traceback.print_exc()
  437. result = "FAIL"
  438. except Exception, e:
  439. import traceback
  440. logger.info(e)
  441. traceback.print_exc()
  442. if args.loglevel == logging.WARNING:
  443. print "Exception: " + str(e)
  444. result = "FAIL"
  445. open('/dev/kmsg', 'w').write('TEST-STOP %s @%.6f\n' % (name, time.time()))
  446. for d in dev:
  447. try:
  448. d.dump_monitor()
  449. d.request("NOTE TEST-STOP " + name)
  450. except Exception, e:
  451. logger.info("Failed to issue TEST-STOP after {} for {}".format(name, d.ifname))
  452. logger.info(e)
  453. result = "FAIL"
  454. if args.no_reset:
  455. print "Leaving devices in current state"
  456. else:
  457. reset_ok = reset_devs(dev, apdev)
  458. wpas = None
  459. try:
  460. wpas = WpaSupplicant(global_iface="/tmp/wpas-wlan5")
  461. rename_log(args.logdir, 'log5', name, wpas)
  462. if not args.no_reset:
  463. wpas.remove_ifname()
  464. except Exception, e:
  465. pass
  466. if wpas:
  467. wpas.close_ctrl()
  468. for i in range(0, 3):
  469. rename_log(args.logdir, 'log' + str(i), name, dev[i])
  470. try:
  471. hapd = HostapdGlobal()
  472. except Exception, e:
  473. print "Failed to connect to hostapd interface"
  474. print str(e)
  475. reset_ok = False
  476. result = "FAIL"
  477. hapd = None
  478. rename_log(args.logdir, 'hostapd', name, hapd)
  479. if hapd:
  480. del hapd
  481. hapd = None
  482. # Use None here since this instance of Wlantest() will never be
  483. # used for remote host hwsim tests on real hardware.
  484. Wlantest.setup(None)
  485. wt = Wlantest()
  486. rename_log(args.logdir, 'hwsim0.pcapng', name, wt)
  487. rename_log(args.logdir, 'hwsim0', name, wt)
  488. if os.path.exists(os.path.join(args.logdir, 'fst-wpa_supplicant')):
  489. rename_log(args.logdir, 'fst-wpa_supplicant', name, None)
  490. if os.path.exists(os.path.join(args.logdir, 'fst-hostapd')):
  491. rename_log(args.logdir, 'fst-hostapd', name, None)
  492. if os.path.exists(os.path.join(args.logdir, 'wmediumd.log')):
  493. rename_log(args.logdir, 'wmediumd.log', name, None)
  494. end = datetime.now()
  495. diff = end - start
  496. if result == 'PASS' and args.dmesg:
  497. if not check_kernel(os.path.join(args.logdir, name + '.dmesg')):
  498. logger.info("Kernel issue found in dmesg - mark test failed")
  499. result = 'FAIL'
  500. if result == 'PASS':
  501. passed.append(name)
  502. elif result == 'SKIP':
  503. skipped.append(name)
  504. else:
  505. failed.append(name)
  506. report(conn, args.prefill, args.build, args.commit, run, name, result,
  507. diff.total_seconds(), args.logdir)
  508. result = "{} {} {} {}".format(result, name, diff.total_seconds(), end)
  509. logger.info(result)
  510. if args.loglevel == logging.WARNING:
  511. print result
  512. sys.stdout.flush()
  513. if not reset_ok:
  514. print "Terminating early due to device reset failure"
  515. break
  516. if args.stdin_ctrl:
  517. set_term_echo(sys.stdin.fileno(), True)
  518. if log_handler:
  519. log_handler.stream.close()
  520. logger.removeHandler(log_handler)
  521. file_name = os.path.join(args.logdir, 'run-tests.log')
  522. log_handler = logging.FileHandler(file_name)
  523. log_handler.setLevel(logging.DEBUG)
  524. log_handler.setFormatter(log_formatter)
  525. logger.addHandler(log_handler)
  526. if conn:
  527. conn.close()
  528. if len(failed):
  529. logger.info("passed {} test case(s)".format(len(passed)))
  530. logger.info("skipped {} test case(s)".format(len(skipped)))
  531. logger.info("failed tests: " + ' '.join(failed))
  532. if args.loglevel == logging.WARNING:
  533. print "failed tests: " + ' '.join(failed)
  534. sys.exit(1)
  535. logger.info("passed all {} test case(s)".format(len(passed)))
  536. if len(skipped):
  537. logger.info("skipped {} test case(s)".format(len(skipped)))
  538. if args.loglevel == logging.WARNING:
  539. print "passed all {} test case(s)".format(len(passed))
  540. if len(skipped):
  541. print "skipped {} test case(s)".format(len(skipped))
  542. if __name__ == "__main__":
  543. main()