"""Support classes and functions for the elpy test code.
|
|
|
|
Elpy uses a bit of a peculiar test setup to avoid redundancy. For the
|
|
tests of the two backends, we provide generic test cases for generic
|
|
tests and for specific callback tests.
|
|
|
|
These mixins can be included in the actual test classes. We can't add
|
|
these tests to a BackendTestCase subclass directly because the test
|
|
discovery would find them there and try to run them, which would fail.
|
|
|
|
"""
|
|
|
|
import os
|
|
import shutil
|
|
import tempfile
|
|
import unittest
|
|
|
|
from elpy.tests import compat
|
|
|
|
|
|
class BackendTestCase(unittest.TestCase):
|
|
"""Base class for backend tests.
|
|
|
|
This class sets up a project root directory and provides an easy
|
|
way to create files within the project root.
|
|
|
|
"""
|
|
|
|
def setUp(self):
|
|
"""Create the project root and make sure it gets cleaned up."""
|
|
super(BackendTestCase, self).setUp()
|
|
self.project_root = tempfile.mkdtemp(prefix="elpy-test")
|
|
self.addCleanup(shutil.rmtree, self.project_root, True)
|
|
|
|
def project_file(self, relname, contents):
|
|
"""Create a file named relname within the project root.
|
|
|
|
Write contents into that file.
|
|
|
|
"""
|
|
full_name = os.path.join(self.project_root, relname)
|
|
try:
|
|
os.makedirs(os.path.dirname(full_name))
|
|
except OSError:
|
|
pass
|
|
with open(full_name, "w") as f:
|
|
f.write(contents)
|
|
return full_name
|
|
|
|
|
|
class GenericRPCTests(object):
|
|
"""Generic RPC test methods.
|
|
|
|
This is a mixin to add tests that should be run for all RPC
|
|
methods that follow the generic (filename, source, offset) calling
|
|
conventions.
|
|
|
|
"""
|
|
METHOD = None
|
|
|
|
def rpc(self, filename, source, offset):
|
|
method = getattr(self.backend, self.METHOD)
|
|
return method(filename, source, offset)
|
|
|
|
def test_should_not_fail_on_inexisting_file(self):
|
|
filename = self.project_root + "/doesnotexist.py"
|
|
self.rpc(filename, "", 0)
|
|
|
|
def test_should_not_fail_on_empty_file(self):
|
|
filename = self.project_file("test.py", "")
|
|
self.rpc(filename, "", 0)
|
|
|
|
def test_should_not_fail_if_file_is_none(self):
|
|
self.rpc(None, "", 0)
|
|
|
|
def test_should_not_fail_for_module_syntax_errors(self):
|
|
source, offset = source_and_offset(
|
|
"class Foo(object):\n"
|
|
" def bar(self):\n"
|
|
" foo(_|_"
|
|
" bar("
|
|
"\n"
|
|
" def a(self):\n"
|
|
" pass\n"
|
|
"\n"
|
|
" def b(self):\n"
|
|
" pass\n"
|
|
"\n"
|
|
" def b(self):\n"
|
|
" pass\n"
|
|
"\n"
|
|
" def b(self):\n"
|
|
" pass\n"
|
|
"\n"
|
|
" def b(self):\n"
|
|
" pass\n"
|
|
"\n"
|
|
" def b(self):\n"
|
|
" pass\n"
|
|
)
|
|
filename = self.project_file("test.py", source)
|
|
|
|
self.rpc(filename, source, offset)
|
|
|
|
def test_should_not_fail_for_bad_indentation(self):
|
|
source, offset = source_and_offset(
|
|
"def foo():\n"
|
|
" print 23_|_\n"
|
|
" print 17\n")
|
|
filename = self.project_file("test.py", source)
|
|
|
|
self.rpc(filename, source, offset)
|
|
|
|
def test_should_not_fail_on_keyword(self):
|
|
source, offset = source_and_offset(
|
|
"_|_try:\n"
|
|
" pass\n"
|
|
"except:\n"
|
|
" pass\n")
|
|
filename = self.project_file("test.py", source)
|
|
|
|
self.rpc(filename, source, offset)
|
|
|
|
def test_should_not_fail_with_bad_encoding(self):
|
|
source = u'# coding: utf-8X\n'
|
|
filename = self.project_file("test.py", source)
|
|
|
|
self.rpc(filename, source, 16)
|
|
|
|
def test_should_not_fail_with_form_feed_characters(self):
|
|
# Bug in Jedi: jedi#424
|
|
source, offset = source_and_offset(
|
|
"\f\n"
|
|
"class Test(object):_|_\n"
|
|
" pass"
|
|
)
|
|
filename = self.project_file("test.py", source)
|
|
|
|
self.rpc(filename, source, offset)
|
|
|
|
def test_should_not_fail_for_dictionaries_in_weird_places(self):
|
|
# Bug in Jedi: jedi#417
|
|
source, offset = source_and_offset(
|
|
"import json\n"
|
|
"\n"
|
|
"def foo():\n"
|
|
" json.loads(_|_\n"
|
|
"\n"
|
|
" json.load.return_value = {'foo': [],\n"
|
|
" 'bar': True}\n"
|
|
"\n"
|
|
" c = Foo()\n"
|
|
)
|
|
filename = self.project_file("test.py", source)
|
|
|
|
self.rpc(filename, source, offset)
|
|
|
|
def test_should_not_break_with_binary_characters_in_docstring(self):
|
|
# Bug in Jedi: jedi#427
|
|
template = '''\
|
|
class Foo(object):
|
|
def __init__(self):
|
|
"""
|
|
COMMUNITY instance that this conversion belongs to.
|
|
DISPERSY_VERSION is the dispersy conversion identifier (on the wire version; must be one byte).
|
|
COMMUNIY_VERSION is the community conversion identifier (on the wire version; must be one byte).
|
|
|
|
COMMUNIY_VERSION may not be '\\x00' or '\\xff'. '\\x00' is used by the DefaultConversion until
|
|
a proper conversion instance can be made for the Community. '\\xff' is reserved for when
|
|
more than one byte is needed as a version indicator.
|
|
"""
|
|
pass
|
|
|
|
|
|
x = Foo()
|
|
x._|_
|
|
'''
|
|
source, offset = source_and_offset(template)
|
|
filename = self.project_file("test.py", source)
|
|
|
|
self.rpc(filename, source, offset)
|
|
|
|
def test_should_not_fail_for_def_without_name(self):
|
|
# Bug jedi#429
|
|
source, offset = source_and_offset(
|
|
"def_|_():\n"
|
|
" if True:\n"
|
|
" return True\n"
|
|
" else:\n"
|
|
" return False\n"
|
|
)
|
|
filename = self.project_file("project.py", source)
|
|
|
|
self.rpc(filename, source, offset)
|
|
|
|
def test_should_not_fail_on_lambda(self):
|
|
# Bug #272 / jedi#431
|
|
source, offset = source_and_offset(
|
|
"map(lambda_|_"
|
|
)
|
|
filename = self.project_file("project.py", source)
|
|
|
|
self.rpc(filename, source, offset)
|
|
|
|
|
|
class RPCGetCompletionsTests(GenericRPCTests):
|
|
METHOD = "rpc_get_completions"
|
|
|
|
def test_should_complete_builtin(self):
|
|
source, offset = source_and_offset("o_|_")
|
|
|
|
expected = ["object", "oct", "open", "or", "ord"]
|
|
actual = [cand['name'] for cand in
|
|
self.backend.rpc_get_completions("test.py",
|
|
source, offset)]
|
|
|
|
for candidate in expected:
|
|
self.assertIn(candidate, actual)
|
|
|
|
def test_should_complete_imports(self):
|
|
source, offset = source_and_offset("import json\n"
|
|
"json.J_|_")
|
|
filename = self.project_file("test.py", source)
|
|
completions = self.backend.rpc_get_completions(filename,
|
|
source,
|
|
offset)
|
|
self.assertEqual(
|
|
sorted([cand['suffix'] for cand in completions]),
|
|
sorted(["SONDecoder", "SONEncoder"]))
|
|
|
|
def test_should_complete_top_level_modules_for_import(self):
|
|
source, offset = source_and_offset("import multi_|_")
|
|
filename = self.project_file("test.py", source)
|
|
completions = self.backend.rpc_get_completions(filename,
|
|
source,
|
|
offset)
|
|
if compat.PYTHON3:
|
|
expected = ["processing"]
|
|
else:
|
|
expected = ["file", "processing"]
|
|
self.assertEqual(sorted([cand['suffix'] for cand in completions]),
|
|
sorted(expected))
|
|
|
|
def test_should_complete_packages_for_import(self):
|
|
source, offset = source_and_offset("import elpy.tes_|_")
|
|
filename = self.project_file("test.py", source)
|
|
completions = self.backend.rpc_get_completions(filename,
|
|
source,
|
|
offset)
|
|
self.assertEqual([cand['suffix'] for cand in completions],
|
|
["ts"])
|
|
|
|
def test_should_not_complete_for_import(self):
|
|
source, offset = source_and_offset("import foo.Conf_|_")
|
|
filename = self.project_file("test.py", source)
|
|
completions = self.backend.rpc_get_completions(filename,
|
|
source,
|
|
offset)
|
|
self.assertEqual([cand['suffix'] for cand in completions],
|
|
[])
|
|
|
|
def test_should_not_fail_for_short_module(self):
|
|
source, offset = source_and_offset("from .. import foo_|_")
|
|
filename = self.project_file("test.py", source)
|
|
completions = self.backend.rpc_get_completions(filename,
|
|
source,
|
|
offset)
|
|
self.assertIsNotNone(completions)
|
|
|
|
def test_should_complete_sys(self):
|
|
source, offset = source_and_offset("import sys\nsys._|_")
|
|
filename = self.project_file("test.py", source)
|
|
completions = self.backend.rpc_get_completions(filename,
|
|
source,
|
|
offset)
|
|
self.assertIn('path', [cand['suffix'] for cand in completions])
|
|
|
|
def test_should_find_with_trailing_text(self):
|
|
source, offset = source_and_offset(
|
|
"import threading\nthreading.T_|_mumble mumble")
|
|
|
|
expected = ["Thread", "ThreadError", "Timer"]
|
|
actual = [cand['name'] for cand in
|
|
self.backend.rpc_get_completions("test.py", source, offset)]
|
|
|
|
for candidate in expected:
|
|
self.assertIn(candidate, actual)
|
|
|
|
def test_should_find_completion_different_package(self):
|
|
# See issue #74
|
|
self.project_file("project/__init__.py", "")
|
|
source1 = ("class Add:\n"
|
|
" def add(self, a, b):\n"
|
|
" return a + b\n")
|
|
self.project_file("project/add.py", source1)
|
|
source2, offset = source_and_offset(
|
|
"from project.add import Add\n"
|
|
"class Calculator:\n"
|
|
" def add(self, a, b):\n"
|
|
" c = Add()\n"
|
|
" c.ad_|_\n")
|
|
file2 = self.project_file("project/calculator.py", source2)
|
|
proposals = self.backend.rpc_get_completions(file2,
|
|
source2,
|
|
offset)
|
|
self.assertEqual(["add"],
|
|
[proposal["name"] for proposal in proposals])
|
|
|
|
|
|
class RPCGetCompletionDocstringTests(object):
|
|
def test_should_return_docstring(self):
|
|
source, offset = source_and_offset("import json\n"
|
|
"json.J_|_")
|
|
filename = self.project_file("test.py", source)
|
|
completions = self.backend.rpc_get_completions(filename,
|
|
source,
|
|
offset)
|
|
completions.sort(key=lambda p: p["name"])
|
|
prop = completions[0]
|
|
self.assertEqual(prop["name"], "JSONDecoder")
|
|
|
|
docs = self.backend.rpc_get_completion_docstring("JSONDecoder")
|
|
|
|
self.assertIn("Simple JSON", docs)
|
|
|
|
def test_should_return_none_if_unknown(self):
|
|
docs = self.backend.rpc_get_completion_docstring("Foo")
|
|
|
|
self.assertIsNone(docs)
|
|
|
|
|
|
class RPCGetCompletionLocationTests(object):
|
|
def test_should_return_location(self):
|
|
source, offset = source_and_offset("donaudampfschiff = 1\n"
|
|
"donau_|_")
|
|
filename = self.project_file("test.py", source)
|
|
completions = self.backend.rpc_get_completions(filename,
|
|
source,
|
|
offset)
|
|
prop = completions[0]
|
|
self.assertEqual(prop["name"], "donaudampfschiff")
|
|
|
|
loc = self.backend.rpc_get_completion_location("donaudampfschiff")
|
|
|
|
self.assertEqual((filename, 1), loc)
|
|
|
|
def test_should_return_none_if_unknown(self):
|
|
docs = self.backend.rpc_get_completion_location("Foo")
|
|
|
|
self.assertIsNone(docs)
|
|
|
|
|
|
class RPCGetDefinitionTests(GenericRPCTests):
|
|
METHOD = "rpc_get_definition"
|
|
|
|
def test_should_return_definition_location_same_file(self):
|
|
source, offset = source_and_offset("import threading\n"
|
|
"def test_function(a, b):\n"
|
|
" return a + b\n"
|
|
"\n"
|
|
"test_func_|_tion(\n")
|
|
filename = self.project_file("test.py", source)
|
|
|
|
location = self.backend.rpc_get_definition(filename,
|
|
source,
|
|
offset)
|
|
|
|
self.assertEqual(location[0], filename)
|
|
# On def or on the function name
|
|
self.assertIn(location[1], (17, 21))
|
|
|
|
def test_should_return_location_in_same_file_if_not_saved(self):
|
|
source, offset = source_and_offset(
|
|
"import threading\n"
|
|
"\n"
|
|
"\n"
|
|
"def other_function():\n"
|
|
" test_f_|_unction(1, 2)\n"
|
|
"\n"
|
|
"\n"
|
|
"def test_function(a, b):\n"
|
|
" return a + b\n")
|
|
filename = self.project_file("test.py", "")
|
|
|
|
location = self.backend.rpc_get_definition(filename,
|
|
source,
|
|
offset)
|
|
|
|
self.assertEqual(location[0], filename)
|
|
# def or function name
|
|
self.assertIn(location[1], (67, 71))
|
|
|
|
def test_should_return_location_in_different_file(self):
|
|
source1 = ("def test_function(a, b):\n"
|
|
" return a + b\n")
|
|
file1 = self.project_file("test1.py", source1)
|
|
source2, offset = source_and_offset("from test1 import test_function\n"
|
|
"test_funct_|_ion(1, 2)\n")
|
|
file2 = self.project_file("test2.py", source2)
|
|
|
|
definition = self.backend.rpc_get_definition(file2,
|
|
source2,
|
|
offset)
|
|
|
|
self.assertEqual(definition[0], file1)
|
|
# Either on the def or on the function name
|
|
self.assertIn(definition[1], (0, 4))
|
|
|
|
def test_should_return_none_if_location_not_found(self):
|
|
source, offset = source_and_offset("test_f_|_unction()\n")
|
|
filename = self.project_file("test.py", source)
|
|
|
|
definition = self.backend.rpc_get_definition(filename,
|
|
source,
|
|
offset)
|
|
|
|
self.assertIsNone(definition)
|
|
|
|
def test_should_return_none_if_outside_of_symbol(self):
|
|
source, offset = source_and_offset("test_function(_|_)\n")
|
|
filename = self.project_file("test.py", source)
|
|
|
|
definition = self.backend.rpc_get_definition(filename,
|
|
source,
|
|
offset)
|
|
|
|
self.assertIsNone(definition)
|
|
|
|
def test_should_return_definition_location_different_package(self):
|
|
# See issue #74
|
|
self.project_file("project/__init__.py", "")
|
|
source1 = ("class Add:\n"
|
|
" def add(self, a, b):\n"
|
|
" return a + b\n")
|
|
file1 = self.project_file("project/add.py", source1)
|
|
source2, offset = source_and_offset(
|
|
"from project.add import Add\n"
|
|
"class Calculator:\n"
|
|
" def add(self, a, b):\n"
|
|
" return Add_|_().add(a, b)\n")
|
|
file2 = self.project_file("project/calculator.py", source2)
|
|
|
|
location = self.backend.rpc_get_definition(file2,
|
|
source2,
|
|
offset)
|
|
|
|
self.assertEqual(location[0], file1)
|
|
# class or class name
|
|
self.assertIn(location[1], (0, 6))
|
|
|
|
def test_should_find_variable_definition(self):
|
|
source, offset = source_and_offset("SOME_VALUE = 1\n"
|
|
"\n"
|
|
"variable = _|_SOME_VALUE\n")
|
|
filename = self.project_file("test.py", source)
|
|
self.assertEqual(self.backend.rpc_get_definition(filename,
|
|
source,
|
|
offset),
|
|
(filename, 0))
|
|
|
|
|
|
class RPCGetCalltipTests(GenericRPCTests):
|
|
METHOD = "rpc_get_calltip"
|
|
|
|
def test_should_get_calltip(self):
|
|
source, offset = source_and_offset(
|
|
"import threading\nthreading.Thread(_|_")
|
|
filename = self.project_file("test.py", source)
|
|
calltip = self.backend.rpc_get_calltip(filename,
|
|
source,
|
|
offset)
|
|
|
|
expected = self.THREAD_CALLTIP
|
|
|
|
self.assertEqual(calltip, expected)
|
|
|
|
def test_should_get_calltip_even_after_parens(self):
|
|
source, offset = source_and_offset(
|
|
"import threading\nthreading.Thread(foo()_|_")
|
|
filename = self.project_file("test.py", source)
|
|
|
|
actual = self.backend.rpc_get_calltip(filename,
|
|
source,
|
|
offset)
|
|
|
|
self.assertEqual(self.THREAD_CALLTIP, actual)
|
|
|
|
def test_should_get_calltip_at_closing_paren(self):
|
|
source, offset = source_and_offset(
|
|
"import threading\nthreading.Thread(_|_)")
|
|
filename = self.project_file("test.py", source)
|
|
|
|
actual = self.backend.rpc_get_calltip(filename,
|
|
source,
|
|
offset)
|
|
|
|
self.assertEqual(self.THREAD_CALLTIP, actual)
|
|
|
|
def test_should_return_none_for_bad_identifier(self):
|
|
source, offset = source_and_offset(
|
|
"froblgoo(_|_")
|
|
filename = self.project_file("test.py", source)
|
|
calltip = self.backend.rpc_get_calltip(filename,
|
|
source,
|
|
offset)
|
|
self.assertIsNone(calltip)
|
|
|
|
def test_should_remove_self_argument(self):
|
|
source, offset = source_and_offset(
|
|
"d = dict()\n"
|
|
"d.keys(_|_")
|
|
filename = self.project_file("test.py", source)
|
|
|
|
actual = self.backend.rpc_get_calltip(filename,
|
|
source,
|
|
offset)
|
|
|
|
self.assertEqual(self.KEYS_CALLTIP, actual)
|
|
|
|
def test_should_remove_package_prefix(self):
|
|
source, offset = source_and_offset(
|
|
"import decimal\n"
|
|
"d = decimal.Decimal('1.5')\n"
|
|
"d.radix(_|_")
|
|
filename = self.project_file("test.py", source)
|
|
|
|
actual = self.backend.rpc_get_calltip(filename,
|
|
source,
|
|
offset)
|
|
|
|
self.assertEqual(self.RADIX_CALLTIP, actual)
|
|
|
|
def test_should_return_none_outside_of_all(self):
|
|
filename = self.project_file("test.py", "")
|
|
source, offset = source_and_offset("import thr_|_eading\n")
|
|
calltip = self.backend.rpc_get_calltip(filename,
|
|
source, offset)
|
|
self.assertIsNone(calltip)
|
|
|
|
def test_should_find_calltip_different_package(self):
|
|
# See issue #74
|
|
self.project_file("project/__init__.py", "")
|
|
source1 = ("class Add:\n"
|
|
" def add(self, a, b):\n"
|
|
" return a + b\n")
|
|
self.project_file("project/add.py", source1)
|
|
source2, offset = source_and_offset(
|
|
"from project.add import Add\n"
|
|
"class Calculator:\n"
|
|
" def add(self, a, b):\n"
|
|
" c = Add()\n"
|
|
" c.add(_|_\n")
|
|
file2 = self.project_file("project/calculator.py", source2)
|
|
|
|
actual = self.backend.rpc_get_calltip(file2,
|
|
source2,
|
|
offset)
|
|
|
|
self.assertEqual(self.ADD_CALLTIP, actual)
|
|
|
|
|
|
class RPCGetDocstringTests(GenericRPCTests):
|
|
METHOD = "rpc_get_docstring"
|
|
|
|
def test_should_get_docstring(self):
|
|
source, offset = source_and_offset(
|
|
"import threading\nthreading.Thread.join_|_(")
|
|
filename = self.project_file("test.py", source)
|
|
docstring = self.backend.rpc_get_docstring(filename,
|
|
source,
|
|
offset)
|
|
|
|
def first_line(s):
|
|
return s[:s.index("\n")]
|
|
|
|
self.assertEqual(first_line(docstring),
|
|
'Thread.join(self, timeout=None):')
|
|
|
|
def test_should_return_none_for_bad_identifier(self):
|
|
source, offset = source_and_offset(
|
|
"froblgoo_|_(\n")
|
|
filename = self.project_file("test.py", source)
|
|
docstring = self.backend.rpc_get_docstring(filename,
|
|
source,
|
|
offset)
|
|
self.assertIsNone(docstring)
|
|
|
|
|
|
class RPCGetUsagesTests(GenericRPCTests):
|
|
METHOD = "rpc_get_usages"
|
|
|
|
def test_should_return_uses_in_same_file(self):
|
|
filename = self.project_file("test.py", "")
|
|
source, offset = source_and_offset(
|
|
"def foo(x):\n"
|
|
" return _|_x + x\n")
|
|
|
|
usages = self.backend.rpc_get_usages(filename,
|
|
source,
|
|
offset)
|
|
|
|
self.assertEqual(usages,
|
|
[{'name': 'x',
|
|
'offset': 8,
|
|
'filename': filename},
|
|
{'name': 'x',
|
|
'filename': filename,
|
|
'offset': 23},
|
|
{'name': u'x',
|
|
'filename': filename,
|
|
'offset': 27}])
|
|
|
|
def test_should_return_uses_in_other_file(self):
|
|
file1 = self.project_file("file1.py", "")
|
|
file2 = self.project_file("file2.py", "\n\n\n\n\nx = 5")
|
|
source, offset = source_and_offset(
|
|
"import file2\n"
|
|
"file2._|_x\n")
|
|
|
|
usages = self.backend.rpc_get_usages(file1,
|
|
source,
|
|
offset)
|
|
|
|
self.assertEqual(usages,
|
|
[{'name': 'x',
|
|
'filename': file1,
|
|
'offset': 19},
|
|
{'name': 'x',
|
|
'filename': file2,
|
|
'offset': 5}])
|
|
|
|
def test_should_not_fail_without_symbol(self):
|
|
filename = self.project_file("file.py", "")
|
|
|
|
usages = self.backend.rpc_get_usages(filename,
|
|
"",
|
|
0)
|
|
|
|
self.assertEqual(usages, [])
|
|
|
|
|
|
def source_and_offset(source):
|
|
"""Return a source and offset from a source description.
|
|
|
|
>>> source_and_offset("hello, _|_world")
|
|
("hello, world", 7)
|
|
>>> source_and_offset("_|_hello, world")
|
|
("hello, world", 0)
|
|
>>> source_and_offset("hello, world_|_")
|
|
("hello, world", 12)
|
|
"""
|
|
offset = source.index("_|_")
|
|
return source[:offset] + source[offset + 3:], offset
|