From 9726df2bed6c1c2f0e3a73d3bbda519304bf65e2 2007-03-02 06:38:35 From: fperez Date: 2007-03-02 06:38:35 Subject: [PATCH] Tests for prefiltering, contributed by Dan Milstein. --- diff --git a/test/test_prefilter.py b/test/test_prefilter.py new file mode 100644 index 0000000..3dab83a --- /dev/null +++ b/test/test_prefilter.py @@ -0,0 +1,381 @@ +""" +Test which prefilter transformations get called for various input lines. +Note that this does *not* test the transformations themselves -- it's just +verifying that a particular combination of, e.g. config options and escape +chars trigger the proper handle_X transform of the input line. + +Usage: run from the command line with *normal* python, not ipython: +> python test_prefilter.py + +Fairly quiet output by default. Pass in -v to get everyone's favorite dots. +""" + +# The prefilter always ends in a call to some self.handle_X method. We swap +# all of those out so that we can capture which one was called. + +import sys +import IPython +import IPython.ipapi +import sys + +verbose = False +if len(sys.argv) > 1: + if sys.argv[1] == '-v': + sys.argv = sys.argv[:-1] # IPython is confused by -v, apparently + verbose = True + +IPython.Shell.start() + +ip = IPython.ipapi.get() + +# Collect failed tests + stats and print them at the end +failures = [] +num_tests = 0 + +# Store the results in module vars as we go +last_line = None +handler_called = None +def install_mock_handler(name): + """Swap out one of the IP.handle_x methods with a function which can + record which handler was called and what line was produced. The mock + handler func always returns '', which causes ipython to cease handling + the string immediately. That way, that it doesn't echo output, raise + exceptions, etc. But do note that testing multiline strings thus gets + a bit hard.""" + def mock_handler(self, line, continue_prompt=None, + pre=None,iFun=None,theRest=None, + obj=None): + #print "Inside %s with '%s'" % (name, line) + global last_line, handler_called + last_line = line + handler_called = name + return '' + mock_handler.name = name + setattr(IPython.iplib.InteractiveShell, name, mock_handler) + +install_mock_handler('handle_normal') +install_mock_handler('handle_auto') +install_mock_handler('handle_magic') +install_mock_handler('handle_help') +install_mock_handler('handle_shell_escape') +install_mock_handler('handle_alias') +install_mock_handler('handle_emacs') + + +def reset_esc_handlers(): + """The escape handlers are stored in a hash (as an attribute of the + InteractiveShell *instance*), so we have to rebuild that hash to get our + new handlers in there.""" + s = ip.IP + s.esc_handlers = {s.ESC_PAREN : s.handle_auto, + s.ESC_QUOTE : s.handle_auto, + s.ESC_QUOTE2 : s.handle_auto, + s.ESC_MAGIC : s.handle_magic, + s.ESC_HELP : s.handle_help, + s.ESC_SHELL : s.handle_shell_escape, + } +reset_esc_handlers() + +# This is so I don't have to quote over and over. Gotta be a better way. +handle_normal = 'handle_normal' +handle_auto = 'handle_auto' +handle_magic = 'handle_magic' +handle_help = 'handle_help' +handle_shell_escape = 'handle_shell_escape' +handle_alias = 'handle_alias' +handle_emacs = 'handle_emacs' + +def check(assertion, failure_msg): + """Check a boolean assertion and fail with a message if necessary. Store + an error essage in module-level failures list in case of failure. Print + '.' or 'F' if module var Verbose is true. + """ + global num_tests + num_tests += 1 + if assertion: + if verbose: + sys.stdout.write('.') + sys.stdout.flush() + else: + if verbose: + sys.stdout.write('F') + sys.stdout.flush() + failures.append(failure_msg) + + +def check_handler(expected_handler, line): + """Verify that the expected hander was called (for the given line, + passed in for failure reporting). + + Pulled out to its own function so that tests which don't use + run_handler_tests can still take advantage of it.""" + check(handler_called == expected_handler, + "Expected %s to be called for %s, " + "instead %s called" % (expected_handler, + repr(line), + handler_called)) + + +def run_handler_tests(h_tests): + """Loop through a series of (input_line, handler_name) pairs, verifying + that, for each ip calls the given handler for the given line. + + The verbose complaint includes the line passed in, so if that line can + include enough info to find the error, the tests are modestly + self-documenting. + """ + for ln, expected_handler in h_tests: + global handler_called + handler_called = None + ip.runlines(ln) + check_handler(expected_handler, ln) + +def run_one_test(ln, expected_handler): + run_handler_tests([(ln, expected_handler)]) + + +# ========================================= +# Tests +# ========================================= + + +# Fundamental escape characters + whitespace & misc +# ================================================= +esc_handler_tests = [ + ( '?thing', handle_help, ), + ( 'thing?', handle_help ), # '?' can trail... + ( 'thing!', handle_normal), # but only '?' can trail + ( '!thing?', handle_help), # trailing '?' wins if more than one + ( ' ?thing', handle_help), # ignore leading whitespace + ( '!ls', handle_shell_escape ), + ( '%magic', handle_magic), + # Possibly, add test for /,; once those are unhooked from %autocall + ( 'emacs_mode # PYTHON-MODE', handle_emacs ), + ( ' ', handle_normal), + ] +run_handler_tests(esc_handler_tests) + + + +# Shell Escapes in Multi-line statements +# ====================================== +# +# We can't test this via runlines, since the hacked over-handlers all +# return None, so continue_prompt never becomes true. Instead we drop +# into prefilter directly and pass in continue_prompt. + +old_mls = ip.options.multi_line_specials +ln = '!ls $f multi_line_specials on' +ignore = ip.IP.prefilter(ln, continue_prompt=True) +check_handler(handle_shell_escape, ln) + +ip.options.multi_line_specials = 0 +ln = '!ls $f multi_line_specials off' +ignore = ip.IP.prefilter(ln, continue_prompt=True) +check_handler(handle_normal, ln) + +ip.options.multi_line_specials = old_mls + + +# Automagic +# ========= + +# Pick one magic fun and one non_magic fun, make sure both exist +assert hasattr(ip.IP, "magic_cpaste") +assert not hasattr(ip.IP, "magic_does_not_exist") +ip.options.automagic = 0 +run_handler_tests([ + # Without automagic, only shows up with explicit escape + ( 'cpaste', handle_normal), + ( '%cpaste', handle_magic), + ( '%does_not_exist', handle_magic) + ]) +ip.options.automagic = 1 +run_handler_tests([ + ( 'cpaste', handle_magic), + ( '%cpaste', handle_magic), + ( 'does_not_exist', handle_normal), + ( '%does_not_exist', handle_magic)]) + +# If next elt starts with anything that could be an assignment, func call, +# etc, we don't call the magic func, unless explicitly escaped to do so. +magic_killing_tests = [] +for c in list('!=()<>,'): + magic_killing_tests.append(('cpaste %s killed_automagic' % c, handle_normal)) + magic_killing_tests.append(('%%cpaste %s escaped_magic' % c, handle_magic)) +run_handler_tests(magic_killing_tests) + +# magic on indented continuation lines -- on iff multi_line_specials == 1 +ip.options.multi_line_specials = 0 +ln = 'cpaste multi_line off kills magic' +ignore = ip.IP.prefilter(ln, continue_prompt=True) +check_handler(handle_normal, ln) + +ip.options.multi_line_specials = 1 +ln = 'cpaste multi_line on enables magic' +ignore = ip.IP.prefilter(ln, continue_prompt=True) +check_handler(handle_magic, ln) + +# user namespace shadows the magic one unless shell escaped +ip.user_ns['cpaste'] = 'user_ns' +run_handler_tests([ + ( 'cpaste', handle_normal), + ( '%cpaste', handle_magic)]) +del ip.user_ns['cpaste'] + + +# Check for !=() turning off .ofind +# ================================= +class AttributeMutator(object): + """A class which will be modified on attribute access, to test ofind""" + def __init__(self): + self.called = False + + def getFoo(self): self.called = True + foo = property(getFoo) + +attr_mutator = AttributeMutator() +ip.to_user_ns('attr_mutator') + +ip.options.autocall = 1 + +run_one_test('attr_mutator.foo should mutate', handle_normal) +check(attr_mutator.called, 'ofind should be called in absence of assign characters') + +for c in list('!=()'): # XXX What about <> -- they *are* important above + attr_mutator.called = False + run_one_test('attr_mutator.foo %s should *not* mutate' % c, handle_normal) + check(not attr_mutator.called, + 'ofind should not be called near character %s' % c) + + + +# Alias expansion +# =============== + +# With autocall on or off, aliases should be shadowed by user, internal and +# __builtin__ namespaces +# +# XXX Can aliases have '.' in their name? With autocall off, that works, +# with autocall on, it doesn't. Hmmm. +import __builtin__ +for ac_state in [0,1]: + ip.options.autocall = ac_state + ip.IP.alias_table['alias_cmd'] = 'alias_result' + ip.IP.alias_table['alias_head.with_dot'] = 'alias_result' + run_handler_tests([ + ("alias_cmd", handle_alias), + # XXX See note above + #("alias_head.with_dot unshadowed, autocall=%s" % ac_state, handle_alias), + ("alias_cmd.something aliases must match whole expr", handle_normal), + ]) + + for ns in [ip.user_ns, ip.IP.internal_ns, __builtin__.__dict__ ]: + ns['alias_cmd'] = 'a user value' + ns['alias_head'] = 'a user value' + run_handler_tests([ + ("alias_cmd", handle_normal), + ("alias_head.with_dot", handle_normal)]) + del ns['alias_cmd'] + del ns['alias_head'] + +ip.options.autocall = 1 + + + + +# Autocall +# ======== + +# First, with autocalling fully off +ip.options.autocall = 0 +run_handler_tests( [ + # Since len is callable, these *should* get auto-called + + # XXX Except, at the moment, they're *not*, because the code is wrong + # XXX So I'm commenting 'em out to keep the tests quiet + + #( '/len autocall_0', handle_auto), + #( ',len autocall_0 b0', handle_auto), + #( ';len autocall_0 b0', handle_auto), + + # But these, since fun is not a callable, should *not* get auto-called + ( '/fun autocall_0', handle_normal), + ( ',fun autocall_0 b0', handle_normal), + ( ';fun autocall_0 b0', handle_normal), + + # With no escapes, no autocalling should happen, callable or not + ( 'len autocall_0', handle_normal), + ( 'fun autocall_0', handle_normal), + ]) + + +# Now, with autocall in default, 'smart' mode +ip.options.autocall = 1 +run_handler_tests( [ + # Since len is callable, these *do* get auto-called + ( '/len a1', handle_auto), + ( ',len a1 b1', handle_auto), + ( ';len a1 b1', handle_auto), + # But these, since fun is not a callable, should *not* get auto-called + ( '/fun a1', handle_normal), + ( ',fun a1 b1', handle_normal), + ( ';fun a1 b1', handle_normal), + # Autocalls without escapes + ( 'len a1', handle_auto), + ( 'fun a1', handle_normal), # Not callable -> no add + # Autocalls only happen on things which look like funcs, even if + # explicitly requested. Which, in this case means they look like a + # sequence of identifiers and . attribute references. So the second + # test should pass, but it's not at the moment (meaning, IPython is + # attempting to run an autocall). Though it does blow up in ipython + # later (because of how lines are split, I think). + ( '"abc".join range(4)', handle_normal), + # XXX ( '/"abc".join range(4)', handle_normal), + ]) + + +# No tests for autocall = 2, since the extra magic there happens inside the +# handle_auto function, which our test doesn't examine. + +# Note that we leave autocall in default, 1, 'smart' mode + + +# Autocall / Binary operators +# ========================== + +# Even with autocall on, 'len in thing' won't transform. +# But ';len in thing' will + +# Note, the tests below don't check for multi-char ops. It could. + +# XXX % is a binary op and should be in the list, too, but fails +bin_ops = list(r'<>,&^|*/+-') + 'is not in and or'.split() +bin_tests = [] +for b in bin_ops: + bin_tests.append(('len %s binop_autocall' % b, handle_normal)) + bin_tests.append((';len %s binop_autocall' % b, handle_auto)) + bin_tests.append((',len %s binop_autocall' % b, handle_auto)) + bin_tests.append(('/len %s binop_autocall' % b, handle_auto)) + +# Who loves auto-generating tests? +run_handler_tests(bin_tests) + + +# Possibly add tests for namespace shadowing (really ofind's business?). +# +# user > ipython internal > python builtin > alias > magic + + +# ============ +# Test Summary +# ============ +num_f = len(failures) +if verbose: + print +print "%s tests run, %s failure%s" % (num_tests, + num_f, + num_f != 1 and "s" or "") +for f in failures: + print f +