testein.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. #!/usr/bin/env python
  2. """
  3. Run EIN test suite
  4. """
  5. import sys
  6. import os
  7. import glob
  8. from subprocess import Popen, PIPE, STDOUT
  9. import itertools
  10. EIN_ROOT = os.path.normpath(
  11. os.path.join(os.path.dirname(__file__), os.path.pardir))
  12. def has_library(emacs, library):
  13. """
  14. Return True when `emacs` has build-in `library`.
  15. """
  16. with open(os.devnull, 'w') as devnull:
  17. proc = Popen(
  18. [emacs, '-Q', '-batch', '-l', 'cl',
  19. '--eval', '(assert (locate-library "{0}"))'.format(library)],
  20. stdout=devnull, stderr=devnull)
  21. return proc.wait() == 0
  22. def eindir(*path):
  23. return os.path.join(EIN_ROOT, *path)
  24. def einlispdir(*path):
  25. return eindir('lisp', *path)
  26. def eintestdir(*path):
  27. return eindir('tests', *path)
  28. def einlibdir(*path):
  29. return eindir('lib', *path)
  30. def show_nonprinting(string, stream=sys.stdout):
  31. """Emulate ``cat -v`` (``--show-nonprinting``)."""
  32. stream.writelines(itertools.imap(chr, convert_nonprinting(string)))
  33. def convert_nonprinting(string):
  34. """
  35. Convert non-printing characters in `string`.
  36. Output is iterable of int. So for Python 2, you need to
  37. convert it into string using `chr`.
  38. Adapted from: http://stackoverflow.com/a/437542/727827
  39. """
  40. for b in itertools.imap(ord, string):
  41. assert 0 <= b < 0x100
  42. if b in (0x09, 0x0a): # '\t\n'
  43. yield b
  44. continue
  45. if b > 0x7f: # not ascii
  46. yield 0x4d # 'M'
  47. yield 0x2d # '-'
  48. b &= 0x7f
  49. if b < 0x20: # control char
  50. yield 0x5e # '^'
  51. b |= 0x40
  52. elif b == 0x7f:
  53. yield 0x5e # '^'
  54. yield 0x3f # '?'
  55. continue
  56. yield b
  57. class BaseRunner(object):
  58. def __init__(self, **kwds):
  59. self.__dict__.update(kwds)
  60. self.batch = self.batch and not self.debug_on_error
  61. def logpath(self, name, ext='log'):
  62. return os.path.join(
  63. self.log_dir,
  64. "{testname}_{logname}_{modename}_{emacsname}.{ext}".format(
  65. ext=ext,
  66. logname=name,
  67. emacsname=os.path.basename(self.emacs),
  68. testname=os.path.splitext(self.testfile)[0],
  69. modename='batch' if self.batch else 'interactive',
  70. ))
  71. @property
  72. def command(self):
  73. raise NotImplementedError
  74. def do_run(self):
  75. raise NotImplementedError
  76. def run(self):
  77. if self.dry_run:
  78. command = self.command
  79. if isinstance(command, basestring):
  80. print command
  81. else:
  82. print construct_command(command)
  83. return 0
  84. else:
  85. mkdirp(self.log_dir)
  86. return self.do_run()
  87. class TestRunner(BaseRunner):
  88. def __init__(self, **kwds):
  89. super(TestRunner, self).__init__(**kwds)
  90. fmtdata = self.__dict__.copy()
  91. fmtdata.update(
  92. emacsname=os.path.basename(self.emacs),
  93. testname=os.path.splitext(self.testfile)[0],
  94. modename='batch' if self.batch else 'interactive',
  95. )
  96. quote = '"{0}"'.format
  97. self.logpath_log = self.logpath('log')
  98. self.logpath_messages = self.logpath('messages')
  99. self.lispvars = {
  100. 'ein:testing-dump-file-log': quote(self.logpath_log),
  101. 'ein:testing-dump-file-messages': quote(self.logpath_messages),
  102. 'ein:log-level': self.ein_log_level,
  103. 'ein:log-message-level': self.ein_message_level,
  104. }
  105. if self.ein_debug:
  106. self.lispvars['ein:debug'] = "'t"
  107. def setq(self, sym, val):
  108. self.lispvars[sym] = val
  109. def bind_lispvars(self):
  110. command = []
  111. for (k, v) in self.lispvars.iteritems():
  112. if v is not None:
  113. command.extend([
  114. '--eval', '(setq {0} {1})'.format(k, v)])
  115. return command
  116. @property
  117. def base_command(self):
  118. command = [self.emacs, '-Q'] + self.bind_lispvars()
  119. if self.batch:
  120. command.append('-batch')
  121. if self.debug_on_error:
  122. command.extend(['-f', 'toggle-debug-on-error'])
  123. # load modules
  124. if self.need_ert():
  125. ertdir = einlibdir('ert', 'lisp', 'emacs-lisp')
  126. command.extend([
  127. '-L', ertdir,
  128. # Load `ert-run-tests-batch-and-exit`:
  129. '-l', os.path.join(ertdir, 'ert-batch.el'),
  130. # Load `ert-run-tests-interactively`:
  131. '-l', os.path.join(ertdir, 'ert-ui.el'),
  132. ])
  133. for path in self.load_path:
  134. command.extend(['-L', path])
  135. for path in self.load:
  136. command.extend(['-l', path])
  137. command.extend(['-L', einlispdir(),
  138. '-L', einlibdir('websocket'),
  139. '-L', einlibdir('request'),
  140. '-L', einlibdir('auto-complete'),
  141. '-L', einlibdir('popup'),
  142. '-L', eintestdir(),
  143. '-l', eintestdir(self.testfile)])
  144. return command
  145. @property
  146. def command(self):
  147. command = self.base_command[:]
  148. if self.batch:
  149. command.extend(['-f', 'ert-run-tests-batch-and-exit'])
  150. else:
  151. command.extend(['--eval', "(ert 't)"])
  152. return command
  153. def show_sys_info(self):
  154. print "*" * 50
  155. command = self.base_command + [
  156. '-batch', '-l', 'ein-dev', '-f', 'ein:dev-print-sys-info']
  157. proc = Popen(command, stderr=PIPE)
  158. err = proc.stderr.read()
  159. proc.wait()
  160. if proc.returncode != 0:
  161. print "Error with return code {0} while running {1}".format(
  162. proc.returncode, command)
  163. print err
  164. pass
  165. print "*" * 50
  166. def need_ert(self):
  167. if self.load_ert:
  168. return True
  169. if self.auto_ert:
  170. if has_library(self.emacs, 'ert'):
  171. print "{0} has ERT module.".format(self.emacs)
  172. return False
  173. else:
  174. print "{0} has no ERT module.".format(self.emacs),
  175. print "ERT is going to be loaded from git submodule."
  176. return True
  177. return False
  178. def make_process(self):
  179. print "Start test {0}".format(self.testfile)
  180. self.proc = Popen(self.command, stdout=PIPE, stderr=STDOUT)
  181. return self.proc
  182. def report(self):
  183. (stdout, _) = self.proc.communicate()
  184. self.stdout = stdout
  185. self.failed = self.proc.returncode != 0
  186. if self.failed:
  187. print "*" * 50
  188. print "Showing {0}:".format(self.logpath_log)
  189. print open(self.logpath_log).read()
  190. print
  191. print "*" * 50
  192. print "Showing STDOUT/STDERR:"
  193. show_nonprinting(stdout)
  194. print
  195. print "{0} failed".format(self.testfile)
  196. else:
  197. print "{0} OK".format(self.testfile)
  198. for line in reversed(stdout.splitlines()):
  199. if line.startswith('Ran'):
  200. print line
  201. break
  202. return int(self.failed)
  203. def do_run(self):
  204. self.show_sys_info()
  205. self.make_process()
  206. return self.report()
  207. def is_known_failure(self):
  208. """
  209. Check if failures are known, based on STDOUT from ERT.
  210. """
  211. import re
  212. lines = iter(self.stdout.splitlines())
  213. for l in lines:
  214. if re.match("[0-9]+ unexpected results:.*", l):
  215. break
  216. else:
  217. return True # no failure
  218. # Check "FAILED <test-name>" lines
  219. for l in lines:
  220. if not l:
  221. break # end with an empty line
  222. for f in self.known_failures:
  223. if re.search(f, l):
  224. break
  225. else:
  226. return False
  227. return True
  228. known_failures = [
  229. "ein:notebook-execute-current-cell-pyout-image$",
  230. ]
  231. """
  232. A list of regexp which matches to test that is known to fail (sometimes).
  233. This is a workaround for ##74.
  234. """
  235. def mkdirp(path):
  236. """Do ``mkdir -p {path}``"""
  237. if not os.path.isdir(path):
  238. os.makedirs(path)
  239. def remove_elc():
  240. files = glob.glob(einlispdir("*.elc")) + glob.glob(eintestdir("*.elc"))
  241. map(os.remove, files)
  242. print "Removed {0} elc files".format(len(files))
  243. class ServerRunner(BaseRunner):
  244. port = None
  245. notebook_dir = os.path.join(EIN_ROOT, "tests", "notebook")
  246. def __enter__(self):
  247. self.run()
  248. return self.port
  249. def __exit__(self, type, value, traceback):
  250. self.stop()
  251. def do_run(self):
  252. self.clear_notebook_dir()
  253. self.start()
  254. self.get_port()
  255. print "Server running at", self.port
  256. def clear_notebook_dir(self):
  257. files = glob.glob(os.path.join(self.notebook_dir, '*.ipynb'))
  258. map(os.remove, files)
  259. print "Removed {0} ipynb files".format(len(files))
  260. @staticmethod
  261. def _parse_port_line(line):
  262. return line.strip().rsplit(':', 1)[-1].strip('/')
  263. def get_port(self):
  264. if self.port is None:
  265. self.port = self._parse_port_line(self.proc.stdout.readline())
  266. return self.port
  267. def start(self):
  268. from subprocess import Popen, PIPE, STDOUT
  269. self.proc = Popen(
  270. self.command, stdout=PIPE, stderr=STDOUT, stdin=PIPE,
  271. shell=True)
  272. # Answer "y" to the prompt: Shutdown Notebook Server (y/[n])?
  273. self.proc.stdin.write('y\n')
  274. def stop(self):
  275. print "Stopping server", self.port
  276. returncode = self.proc.poll()
  277. if returncode is not None:
  278. logpath = self.logpath('server')
  279. print "Server process was already dead by exit code", returncode
  280. print "*" * 50
  281. print "Showing {0}:".format(logpath)
  282. print open(logpath).read()
  283. print
  284. return
  285. if not self.dry_run:
  286. try:
  287. kill_subprocesses(self.proc.pid, lambda x: 'ipython' in x)
  288. finally:
  289. self.proc.terminate()
  290. @property
  291. def command(self):
  292. fmtdata = dict(
  293. notebook_dir=self.notebook_dir,
  294. ipython=self.ipython,
  295. server_log=self.logpath('server'),
  296. )
  297. return self.command_template.format(**fmtdata)
  298. command_template = r"""
  299. {ipython} notebook \
  300. --notebook-dir {notebook_dir} \
  301. --debug \
  302. --no-browser 2>&1 \
  303. | tee {server_log} \
  304. | grep --line-buffered 'The IPython Notebook is running at' \
  305. | head -n1
  306. """
  307. def kill_subprocesses(pid, include=lambda x: True):
  308. from subprocess import Popen, PIPE
  309. import signal
  310. command = ['ps', '-e', '-o', 'ppid,pid,command']
  311. proc = Popen(command, stdout=PIPE, stderr=PIPE)
  312. (stdout, stderr) = proc.communicate()
  313. if proc.returncode != 0:
  314. raise RuntimeError(
  315. 'Command {0} failed with code {1} and following error message:\n'
  316. '{2}'.format(command, proc.returncode, stderr))
  317. for line in map(str.strip, stdout.splitlines()):
  318. (cmd_ppid, cmd_pid, cmd) = line.split(None, 2)
  319. if cmd_ppid == str(pid) and include(cmd):
  320. print "Killing PID={0} COMMAND={1}".format(cmd_pid, cmd)
  321. os.kill(int(cmd_pid), signal.SIGINT)
  322. def construct_command(args):
  323. """
  324. Construct command as a string given a list of arguments.
  325. """
  326. command = []
  327. escapes = set(' ()')
  328. for a in args:
  329. if set(a) & escapes:
  330. command.append(repr(str(a))) # hackish way to escape
  331. else:
  332. command.append(a)
  333. return " ".join(command)
  334. def run_ein_test(unit_test, func_test, func_test_max_retries,
  335. no_skip, clean_elc, **kwds):
  336. if clean_elc and not kwds['dry_run']:
  337. remove_elc()
  338. if unit_test:
  339. unit_test_runner = TestRunner(testfile='test-load.el', **kwds)
  340. if unit_test_runner.run() != 0:
  341. return 1
  342. if func_test:
  343. for i in range(func_test_max_retries + 1):
  344. func_test_runner = TestRunner(testfile='func-test.el', **kwds)
  345. with ServerRunner(testfile='func-test.el', **kwds) as port:
  346. func_test_runner.setq('ein:testing-port', port)
  347. if func_test_runner.run() == 0:
  348. print "Functional test succeeded after {0} retries." \
  349. .format(i)
  350. return 0
  351. if not no_skip and func_test_runner.is_known_failure():
  352. print "All failures are known. Ending functional test."
  353. return 0
  354. print "Functional test failed after {0} retries.".format(i)
  355. return 1
  356. return 0
  357. def main():
  358. import sys
  359. from argparse import ArgumentParser
  360. parser = ArgumentParser(description=__doc__.splitlines()[1])
  361. parser.add_argument('--emacs', '-e', default='emacs',
  362. help='Emacs executable.')
  363. parser.add_argument('--load-path', '-L', default=[], action='append',
  364. help="add a directory to load-path. "
  365. "can be specified multiple times.")
  366. parser.add_argument('--load', '-l', default=[], action='append',
  367. help="load lisp file before tests. "
  368. "can be specified multiple times.")
  369. parser.add_argument('--load-ert', default=False, action='store_true',
  370. help="load ERT from git submodule. "
  371. "you need to update git submodule manually "
  372. "if ert/ directory does not exist yet.")
  373. parser.add_argument('--no-auto-ert', default=True,
  374. dest='auto_ert', action='store_false',
  375. help="load ERT from git submodule. "
  376. "if this Emacs has no build-in ERT module.")
  377. parser.add_argument('--no-batch', '-B', default=True,
  378. dest='batch', action='store_false',
  379. help="start interactive session.")
  380. parser.add_argument('--debug-on-error', '-d', default=False,
  381. action='store_true',
  382. help="set debug-on-error to t and start "
  383. "interactive session.")
  384. parser.add_argument('--func-test-max-retries', default=4, type=int,
  385. help="""
  386. Specify number of retries for functional test
  387. before failing with error. This is workaround
  388. for the issue #74.
  389. """)
  390. parser.add_argument('--no-skip', default=False, action='store_true',
  391. help="""
  392. Do no skip known failures. Known failures
  393. are implemented as another workaround for the
  394. issue #74.
  395. """)
  396. parser.add_argument('--no-func-test', '-F', default=True,
  397. dest='func_test', action='store_false',
  398. help="do not run functional test.")
  399. parser.add_argument('--no-unit-test', '-U', default=True,
  400. dest='unit_test', action='store_false',
  401. help="do not run unit test.")
  402. parser.add_argument('--clean-elc', '-c', default=False,
  403. action='store_true',
  404. help="remove *.elc files in ein/lisp and "
  405. "ein/tests directories.")
  406. parser.add_argument('--dry-run', default=False,
  407. action='store_true',
  408. help="Print commands to be executed.")
  409. parser.add_argument('--ipython', default='ipython',
  410. help="""
  411. ipython executable to use to run notebook server.
  412. """)
  413. parser.add_argument('--ein-log-level', default=40)
  414. parser.add_argument('--ein-message-level', default=30)
  415. parser.add_argument('--ein-debug', default=False, action='store_true',
  416. help="(setq ein:debug t) when given.")
  417. parser.add_argument('--log-dir', default="log",
  418. help="Directory to store log (default: %(default)s)")
  419. args = parser.parse_args()
  420. sys.exit(run_ein_test(**vars(args)))
  421. if __name__ == '__main__':
  422. main()