from __future__ import print_function, unicode_literals import os import sys import shutil import tempfile import unittest import six from pubs import pubs_cmd, color, content, uis, p3, events from pubs.config import conf from pubs.p3 import _fake_stdio, _get_fake_stdio_ucontent # makes the tests very noisy PRINT_OUTPUT = False CAPTURE_OUTPUT = True original_exception_handler = uis.InputUI.handle_exception class FakeSystemExit(Exception): """\ SystemExit exceptions are replaced by FakeSystemExit in the execute_cmds() function, so they can be catched by ExpectedFailure tests in Python 2.x. If a code is expected to raise SystemExit, catch FakeSystemExit instead. Added explicit __init__ so SystemExit.code functionality could be emulated. Taking form from https://stackoverflow.com/a/26938914/1634191 """ def __init__(self, code=None, *args): self.code = code super(FakeSystemExit, self).__init__( "Exited with code: {}.".format(self.code), *args) # capture output def capture(f, verbose=False): """Capture the stdout and stderr output. Useful for comparing the output with the expected one during tests. :param f: The function to capture output from. :param verbose: If True, print call will still display their outputs. If False, they will be silenced. """ def newf(*args, **kwargs): old_stderr, old_stdout = sys.stderr, sys.stdout sys.stdout = _fake_stdio(additional_out=old_stderr if verbose else None) sys.stderr = _fake_stdio(additional_out=old_stderr if False else None) try: return f(*args, **kwargs), _get_fake_stdio_ucontent(sys.stdout), _get_fake_stdio_ucontent(sys.stderr) finally: sys.stderr, sys.stdout = old_stderr, old_stdout return newf # scriptable input class FakeInput(): """ Replace the input() command, and mock user input during tests Instanciate as : input = FakeInput(['yes', 'no']) then replace the input command in every module of the package : input.as_global() Then : input() returns 'yes' input() returns 'no' input() raises IndexError """ class UnexpectedInput(Exception): pass def __init__(self, inputs, module_list=tuple()): self.inputs = list(inputs) or [] self.module_list = module_list self._cursor = 0 def as_global(self): for md in self.module_list: md.input = self if md.__name__ == 'pubs.uis': md.InputUI.editor_input = self md.InputUI.edit_file = self.input_to_file def handler(ui, exc): if isinstance(exc, self.UnexpectedInput): raise else: original_exception_handler(ui, exc) md.InputUI.handle_exception = handler def input_to_file(self, path_to_file, temporary=True): content.write_file(path_to_file, self()) def add_input(self, inp): self.inputs.append(inp) def __call__(self, *args, **kwargs): try: inp = self.inputs[self._cursor] self._cursor += 1 return inp except IndexError: raise self.UnexpectedInput('Unexpected user input in test.') class SandboxedCommandTestCase(unittest.TestCase): maxDiff = 1000000 def setUp(self): super(SandboxedCommandTestCase, self).setUp() self.temp_dir = tempfile.mkdtemp() self.default_pubs_dir = os.path.join(self.temp_dir, 'pubs') self.default_conf_path = os.path.join(self.temp_dir, 'pubsrc') os.chdir(os.path.dirname(__file__)) @staticmethod def _normalize(s): """Normalize a string for robust comparisons.""" s = color.undye(s) try: s = s.decode('utf-8') except AttributeError: pass return s def _compare_output(self, s1, s2): if s1 is not None and s2 is not None: return self.assertEqual(self._normalize(s1), self._normalize(s2)) def _preprocess_cmd(self, cmd): """Sandbox the pubs command into a temporary directory""" cmd_chunks = cmd.split(' ') assert cmd_chunks[0] == 'pubs' prefix = ['pubs', '-c', self.default_conf_path] if cmd_chunks[1] == 'init': return ' '.join(prefix + ['init', '-p', self.default_pubs_dir] + cmd_chunks[2:]) else: return ' '.join(prefix + cmd_chunks[1:]) def execute_cmds(self, cmds, capture_output=CAPTURE_OUTPUT): """ Execute a list of commands, and capture their output A command can be a string, or a tuple of size 2, 3 or 4. In the latter case, the command is : 1. a string reprensenting the command to execute 2. the user inputs to feed to the command during execution 3. the expected output on stdout, verified with assertEqual. 4. the expected output on stderr, verified with assertEqual. (this does not work yet) """ try: outs = [] for cmd in cmds: inputs = [] expected_out, expected_err = None, None assert isinstance(cmd, tuple) actual_cmd = cmd[0] if len(cmd) >= 2 and cmd[1] is not None: # Inputs provided inputs = cmd[1] if len(cmd) >= 3: # Expected output provided capture_output = True if cmd[2] is not None: expected_out = color.undye(cmd[2]) if len(cmd) >= 4 and cmd[3] is not None: # Expected error output provided expected_err = color.undye(cmd[3]) actual_cmd = self._preprocess_cmd(actual_cmd) # Always set fake input: test should not ask unexpected user input input = FakeInput(inputs, [content, uis, p3]) input.as_global() try: if capture_output: execute_captured = capture(pubs_cmd.execute, verbose=PRINT_OUTPUT) _, stdout, stderr = execute_captured(actual_cmd.split()) self._compare_output(stdout, expected_out) self._compare_output(stderr, expected_err) outs.append(self._normalize(stdout)) else: pubs_cmd.execute(actual_cmd.split()) except FakeInput.UnexpectedInput: self.fail('Unexpected input asked by command: {}.'.format(actual_cmd)) return outs except SystemExit as exc: exc_class, exc, tb = sys.exc_info() if sys.version_info.major == 2: # using six to avoid a SyntaxError in Python 3.x six.reraise(FakeSystemExit, FakeSystemExit(*exc.args), tb) else: raise FakeSystemExit(*exc.args).with_traceback(tb) def tearDown(self): shutil.rmtree(self.temp_dir, ignore_errors=True) ## Testing the test environments class TestInput(unittest.TestCase): """Test that the fake input mechanisms work correctly in the tests""" def test_input(self): input = FakeInput(['yes', 'no']) self.assertEqual(input(), 'yes') self.assertEqual(input(), 'no') with self.assertRaises(FakeInput.UnexpectedInput): input() def test_input2(self): other_input = FakeInput(['yes', 'no'], module_list=[color]) other_input.as_global() self.assertEqual(color.input(), 'yes') self.assertEqual(color.input(), 'no') with self.assertRaises(FakeInput.UnexpectedInput): color.input() def test_editor_input(self): sample_conf = conf.load_default_conf() ui = uis.InputUI(sample_conf) other_input = FakeInput(['yes', 'no'], module_list=[uis]) other_input.as_global() self.assertEqual(ui.editor_input('fake_editor'), 'yes') self.assertEqual(ui.editor_input('fake_editor'), 'no') with self.assertRaises(FakeInput.UnexpectedInput): ui.editor_input() class TestSandboxedCommandTestCase(SandboxedCommandTestCase): def test_init_add(self): """Simple init and add example""" correct = ("added to pubs:\n" "[Page99] Page, Lawrence et al. \"The PageRank Citation Ranking: Bringing Order to the Web.\" (1999) \n") cmds = [('pubs init',), ('pubs add data/pagerank.bib', [], correct), #('pubs add abc', [], '', 'error: File does not exist: /Users/self/Volumes/ResearchSync/projects/pubs/abc\n') ] self.execute_cmds(cmds) if __name__ == '__main__': unittest.main(verbosity=2)