<reponame>hung135/pythoscope
from pythoscope.astvisitor import descend, ASTVisitor
from pythoscope.astbuilder import parse_fragment, EmptyCode
from pythoscope.logger import log
from pythoscope.generator.adder import add_test_case_to_project
from pythoscope.generator.assertions import assertions_for_interaction
from pythoscope.generator.builder import UnittestTemplate, NoseTemplate,\
    generate_test_contents
from pythoscope.generator.case_namer import call2testname, name2testname,\
    userobject2testname
from pythoscope.generator.cleaner import remove_objects_unworthy_of_naming
from pythoscope.generator.objects_namer import name_objects_on_timeline
from pythoscope.generator.optimizer import optimize
from pythoscope.generator.selector import testable_objects, testable_calls
from pythoscope.store import Class, Function, TestClass, TestMethod,\
    ModuleNotFound
from pythoscope.compat import all, sorted
from pythoscope.util import camelize, pluralize, underscore


# :: Call | UserObject | Method | Function -> CodeString
def generate_test_case(testable_interaction, template):
    """This functions binds all other functions from generator submodules
    together (assertions, cleaner, optimizer, objects_namer and builder),
    implementing full test generation process, from a testable interaction
    object to a test case string.

    Call|UserObject|Method|Function -> assertions_for_interaction ->
      [Event] -> remove_objects_unworthy_of_naming ->
        [Event] -> optimize ->
          [Event] -> name_objects_on_timeline ->
            [Event] -> generate_test_contents ->
              CodeString
    """
    return \
        generate_test_contents(
            name_objects_on_timeline(
                optimize(
                    remove_objects_unworthy_of_naming(
                        assertions_for_interaction(testable_interaction)))),
            template)

# :: [TestMethodDescription] -> [TestMethodDescription]
def sorted_test_method_descriptions(descriptions):
    return sorted(descriptions, key=lambda md: md.name)

# :: [TestMethodDescription] -> [TestMethodDescription]
def resolve_name_duplicates(descriptions):
    # We abuse the fact that descriptions has been sorted by name before being
    # passed into this function.
    last_name = ''
    num = 2
    for description in descriptions:
        if last_name != description.name:
            last_name = description.name
            num = 2
        else:
            description.name = "%s_case_%d" % (description.name, num)
            num += 1
    return descriptions

def should_ignore_method(method):
    return method.is_private()

class UnknownTemplate(Exception):
    def __init__(self, template):
        Exception.__init__(self, "Couldn't find template %r." % template)
        self.template = template

def find_method_code(code, method_name):
    """Return part of the code tree that corresponds to the given method
    definition.
    """
    class LocalizeMethodVisitor(ASTVisitor):
        def __init__(self):
            ASTVisitor.__init__(self)
            self.method_body = None
        def visit_function(self, name, args, body):
            if name == method_name:
                self.method_body = body

    return descend(code.children, LocalizeMethodVisitor).method_body

# :: str, str -> str
def indented_setup(setup, indentation):
    """Indent each line of setup with given amount of indentation.

    >>> indented_setup("x = 1\\n", "  ")
    '  x = 1\\n'
    >>> indented_setup("x = 1\\ny = 2\\n", "    ")
    '    x = 1\\n    y = 2\\n'
    """
    return ''.join([indentation + line for line in setup.splitlines(True)])

class TestMethodDescription(object):
    def __init__(self, name, code=""):
        self.name = name
        self.code = code
    def contains_code(self):
        return not all([(line.strip().startswith("#") or line.strip() == '') for line in self.code.splitlines()])

class TestGenerator(object):
    main_snippet = EmptyCode()
    template = None

    def from_template(cls, template):
        if template == 'unittest':
            return UnittestTestGenerator()
        elif template == 'nose':
            return NoseTestGenerator()
        else:
            raise UnknownTemplate(template)
    from_template = classmethod(from_template)

    def __init__(self):
        self.imports = []

    def test_class_header(self, name):
        raise NotImplementedError("Method test_class_header() not defined.")

    def ensure_import(self, import_):
        if import_ is not None and import_ not in self.imports:
            self.imports.append(import_)

    def ensure_imports(self, imports):
        for import_ in imports:
            self.ensure_import(import_)

    def add_tests_to_project(self, project, modnames, force=False):
        for modname in modnames:
            try:
                module = project.find_module_by_full_path(modname)
                if not module.has_errors():
                    self._add_tests_for_module(module, project, force)
            except ModuleNotFound:
                log.warning("Failed to inspect module %s, skipping test generation." % modname)

    def _add_tests_for_module(self, module, project, force):
        log.info("Generating tests for module %s." % module.subpath)
        for test_case in self._generate_test_cases(module):
            add_test_case_to_project(project, test_case, self.main_snippet, force)

    def _generate_test_cases(self, module):
        for object in testable_objects(module):
            test_case = self._generate_test_case(object, module)
            if test_case:
                yield test_case

    def _generate_test_case(self, object, module):
        class_name = name2testname(camelize(object.name))
        method_descriptions = resolve_name_duplicates(sorted_test_method_descriptions(self._generate_test_method_descriptions(object, module)))

        # Don't generate empty test classes.
        if method_descriptions:
            body = self._generate_test_class_code(class_name, method_descriptions)
            return self._generate_test_class(class_name, method_descriptions, module, body)

    def _generate_test_class_code(self, class_name, method_descriptions):
        result = "%s\n" % (self.test_class_header(class_name))
        for method_description in method_descriptions:
            result += "    def %s(self):\n" % method_description.name
            result += indented_setup(method_description.code, "        ")
            self.ensure_imports(method_description.code.imports)
            # We need at least one statement in a method to be syntatically correct.
            if not method_description.contains_code():
                result += "        pass\n"
            result += "\n"
        return result

    def _generate_test_class(self, class_name, method_descriptions, module, body):
        code = parse_fragment(body)
        def methoddesc2testmethod(method_description):
            name = method_description.name
            return TestMethod(name=name, code=find_method_code(code, name))
        return TestClass(name=class_name,
                         code=code,
                         test_cases=map(methoddesc2testmethod, method_descriptions),
                         imports=self.imports,
                         associated_modules=[module])

    def _generate_test_method_descriptions(self, object, module):
        if isinstance(object, Function):
            return self._generate_test_method_descriptions_for_function(object, module)
        elif isinstance(object, Class):
            return self._generate_test_method_descriptions_for_class(object, module)
        else:
            raise TypeError("Don't know how to generate test method descriptions for %s" % object)

    def _generate_test_method_descriptions_for_function(self, function, module):
        if testable_calls(function.calls):
            log.debug("Detected %s in function %s." % \
                          (pluralize("testable call", len(testable_calls(function.calls))),
                           function.name))

            # We're calling the function, so we have to make sure it will
            # be imported in the test
            self.ensure_import((module.locator, function.name))

            # We have at least one call registered, so use it.
            return self._method_descriptions_from_function(function)
        else:
            # No calls were traced, so we'll go for a single test stub.
            log.debug("Detected _no_ testable calls in function %s." % function.name)
            name = name2testname(underscore(function.name))
            return [TestMethodDescription(name, generate_test_case(function, self.template))]

    def _generate_test_method_descriptions_for_class(self, klass, module):
        if klass.user_objects:
            # We're calling the method, so we have to make sure its class
            # will be imported in the test.
            self.ensure_import((module.locator, klass.name))

        for user_object in klass.user_objects:
            yield self._method_description_from_user_object(user_object)

        # No calls were traced for those methods, so we'll go for simple test stubs.
        for method in klass.get_untraced_methods():
            if not should_ignore_method(method):
                yield self._generate_test_method_description_for_method(method)

    def _generate_test_method_description_for_method(self, method):
        test_name = name2testname(method.name)
        return TestMethodDescription(test_name, generate_test_case(method, self.template))

    def _method_descriptions_from_function(self, function):
        for call in testable_calls(function.get_unique_calls()):
            name = call2testname(call, function.name)
            yield TestMethodDescription(name, generate_test_case(call, self.template))

    def _method_description_from_user_object(self, user_object):
        return TestMethodDescription(userobject2testname(user_object),
                                     generate_test_case(user_object, self.template))

class UnittestTestGenerator(TestGenerator):
    main_snippet = parse_fragment("if __name__ == '__main__':\n    unittest.main()\n")
    template = UnittestTemplate()

    def test_class_header(self, name):
        self.ensure_import('unittest')
        return "class %s(unittest.TestCase):" % name

class NoseTestGenerator(TestGenerator):
    template = NoseTemplate()

    def test_class_header(self, name):
        return "class %s:" % name

def add_tests_to_project(project, modnames, template, force=False):
    generator = TestGenerator.from_template(template)
    generator.add_tests_to_project(project, modnames, force)

from pythoscope.astvisitor import descend, ASTVisitor
from pythoscope.astbuilder import parse_fragment, EmptyCode
from pythoscope.logger import log
from pythoscope.generator.adder import add_test_case_to_project
from pythoscope.generator.assertions import assertions_for_interaction
from pythoscope.generator.builder import UnittestTemplate, NoseTemplate,\
    generate_test_contents
from pythoscope.generator.case_namer import call2testname, name2testname,\
    userobject2testname
from pythoscope.generator.cleaner import remove_objects_unworthy_of_naming
from pythoscope.generator.objects_namer import name_objects_on_timeline
from pythoscope.generator.optimizer import optimize
from pythoscope.generator.selector import testable_objects, testable_calls
from pythoscope.store import Class, Function, TestClass, TestMethod,\
    ModuleNotFound
from pythoscope.compat import all, sorted
from pythoscope.util import camelize, pluralize, underscore


# :: Call | UserObject | Method | Function -> CodeString
def generate_test_case(testable_interaction, template):
    """This functions binds all other functions from generator submodules
    together (assertions, cleaner, optimizer, objects_namer and builder),
    implementing full test generation process, from a testable interaction
    object to a test case string.

    Call|UserObject|Method|Function -> assertions_for_interaction ->
      [Event] -> remove_objects_unworthy_of_naming ->
        [Event] -> optimize ->
          [Event] -> name_objects_on_timeline ->
            [Event] -> generate_test_contents ->
              CodeString
    """
    return \
        generate_test_contents(
            name_objects_on_timeline(
                optimize(
                    remove_objects_unworthy_of_naming(
                        assertions_for_interaction(testable_interaction)))),
            template)

# :: [TestMethodDescription] -> [TestMethodDescription]
def sorted_test_method_descriptions(descriptions):
    return sorted(descriptions, key=lambda md: md.name)

# :: [TestMethodDescription] -> [TestMethodDescription]
def resolve_name_duplicates(descriptions):
    # We abuse the fact that descriptions has been sorted by name before being
    # passed into this function.
    last_name = ''
    num = 2
    for description in descriptions:
        if last_name != description.name:
            last_name = description.name
            num = 2
        else:
            description.name = "%s_case_%d" % (description.name, num)
            num += 1
    return descriptions

def should_ignore_method(method):
    return method.is_private()

class UnknownTemplate(Exception):
    def __init__(self, template):
        Exception.__init__(self, "Couldn't find template %r." % template)
        self.template = template

def find_method_code(code, method_name):
    """Return part of the code tree that corresponds to the given method
    definition.
    """
    class LocalizeMethodVisitor(ASTVisitor):
        def __init__(self):
            ASTVisitor.__init__(self)
            self.method_body = None
        def visit_function(self, name, args, body):
            if name == method_name:
                self.method_body = body

    return descend(code.children, LocalizeMethodVisitor).method_body

# :: str, str -> str
def indented_setup(setup, indentation):
    """Indent each line of setup with given amount of indentation.

    >>> indented_setup("x = 1\\n", "  ")
    '  x = 1\\n'
    >>> indented_setup("x = 1\\ny = 2\\n", "    ")
    '    x = 1\\n    y = 2\\n'
    """
    return ''.join([indentation + line for line in setup.splitlines(True)])

class TestMethodDescription(object):
    def __init__(self, name, code=""):
        self.name = name
        self.code = code
    def contains_code(self):
        return not all([(line.strip().startswith("#") or line.strip() == '') for line in self.code.splitlines()])

class TestGenerator(object):
    main_snippet = EmptyCode()
    template = None

    def from_template(cls, template):
        if template == 'unittest':
            return UnittestTestGenerator()
        elif template == 'nose':
            return NoseTestGenerator()
        else:
            raise UnknownTemplate(template)
    from_template = classmethod(from_template)

    def __init__(self):
        self.imports = []

    def test_class_header(self, name):
        raise NotImplementedError("Method test_class_header() not defined.")

    def ensure_import(self, import_):
        if import_ is not None and import_ not in self.imports:
            self.imports.append(import_)

    def ensure_imports(self, imports):
        for import_ in imports:
            self.ensure_import(import_)

    def add_tests_to_project(self, project, modnames, force=False):
        for modname in modnames:
            try:
                module = project.find_module_by_full_path(modname)
                if not module.has_errors():
                    self._add_tests_for_module(module, project, force)
            except ModuleNotFound:
                log.warning("Failed to inspect module %s, skipping test generation." % modname)

    def _add_tests_for_module(self, module, project, force):
        log.info("Generating tests for module %s." % module.subpath)
        for test_case in self._generate_test_cases(module):
            add_test_case_to_project(project, test_case, self.main_snippet, force)

    def _generate_test_cases(self, module):
        for object in testable_objects(module):
            test_case = self._generate_test_case(object, module)
            if test_case:
                yield test_case

    def _generate_test_case(self, object, module):
        class_name = name2testname(camelize(object.name))
        method_descriptions = resolve_name_duplicates(sorted_test_method_descriptions(self._generate_test_method_descriptions(object, module)))

        # Don't generate empty test classes.
        if method_descriptions:
            body = self._generate_test_class_code(class_name, method_descriptions)
            return self._generate_test_class(class_name, method_descriptions, module, body)

    def _generate_test_class_code(self, class_name, method_descriptions):
        result = "%s\n" % (self.test_class_header(class_name))
        for method_description in method_descriptions:
            result += "    def %s(self):\n" % method_description.name
            result += indented_setup(method_description.code, "        ")
            self.ensure_imports(method_description.code.imports)
            # We need at least one statement in a method to be syntatically correct.
            if not method_description.contains_code():
                result += "        pass\n"
            result += "\n"
        return result

    def _generate_test_class(self, class_name, method_descriptions, module, body):
        code = parse_fragment(body)
        def methoddesc2testmethod(method_description):
            name = method_description.name
            return TestMethod(name=name, code=find_method_code(code, name))
        return TestClass(name=class_name,
                         code=code,
                         test_cases=map(methoddesc2testmethod, method_descriptions),
                         imports=self.imports,
                         associated_modules=[module])

    def _generate_test_method_descriptions(self, object, module):
        if isinstance(object, Function):
            return self._generate_test_method_descriptions_for_function(object, module)
        elif isinstance(object, Class):
            return self._generate_test_method_descriptions_for_class(object, module)
        else:
            raise TypeError("Don't know how to generate test method descriptions for %s" % object)

    def _generate_test_method_descriptions_for_function(self, function, module):
        if testable_calls(function.calls):
            log.debug("Detected %s in function %s." % \
                          (pluralize("testable call", len(testable_calls(function.calls))),
                           function.name))

            # We're calling the function, so we have to make sure it will
            # be imported in the test
            self.ensure_import((module.locator, function.name))

            # We have at least one call registered, so use it.
            return self._method_descriptions_from_function(function)
        else:
            # No calls were traced, so we'll go for a single test stub.
            log.debug("Detected _no_ testable calls in function %s." % function.name)
            name = name2testname(underscore(function.name))
            return [TestMethodDescription(name, generate_test_case(function, self.template))]

    def _generate_test_method_descriptions_for_class(self, klass, module):
        if klass.user_objects:
            # We're calling the method, so we have to make sure its class
            # will be imported in the test.
            self.ensure_import((module.locator, klass.name))

        for user_object in klass.user_objects:
            yield self._method_description_from_user_object(user_object)

        # No calls were traced for those methods, so we'll go for simple test stubs.
        for method in klass.get_untraced_methods():
            if not should_ignore_method(method):
                yield self._generate_test_method_description_for_method(method)

    def _generate_test_method_description_for_method(self, method):
        test_name = name2testname(method.name)
        return TestMethodDescription(test_name, generate_test_case(method, self.template))

    def _method_descriptions_from_function(self, function):
        for call in testable_calls(function.get_unique_calls()):
            name = call2testname(call, function.name)
            yield TestMethodDescription(name, generate_test_case(call, self.template))

    def _method_description_from_user_object(self, user_object):
        return TestMethodDescription(userobject2testname(user_object),
                                     generate_test_case(user_object, self.template))

class UnittestTestGenerator(TestGenerator):
    main_snippet = parse_fragment("if __name__ == '__main__':\n    unittest.main()\n")
    template = UnittestTemplate()

    def test_class_header(self, name):
        self.ensure_import('unittest')
        return "class %s(unittest.TestCase):" % name

class NoseTestGenerator(TestGenerator):
    template = NoseTemplate()

    def test_class_header(self, name):
        return "class %s:" % name

def add_tests_to_project(project, modnames, template, force=False):
    generator = TestGenerator.from_template(template)
    generator.add_tests_to_project(project, modnames, force)
# :: Call | UserObject | Method | Function -> CodeString This functions binds all other functions from generator submodules     together (assertions, cleaner, optimizer, objects_namer and builder),     implementing full test generation process, from a testable interaction     object to a test case string.      Call|UserObject|Method|Function -> assertions_for_interaction ->       [Event] -> remove_objects_unworthy_of_naming ->         [Event] -> optimize ->           [Event] -> name_objects_on_timeline ->             [Event] -> generate_test_contents ->               CodeString # :: [TestMethodDescription] -> [TestMethodDescription] # :: [TestMethodDescription] -> [TestMethodDescription] # We abuse the fact that descriptions has been sorted by name before being # passed into this function. Return part of the code tree that corresponds to the given method     definition. # :: str, str -> str Indent each line of setup with given amount of indentation.      >>> indented_setup("x = 1\\n", "  ")     '  x = 1\\n'     >>> indented_setup("x = 1\\ny = 2\\n", "    ")     '    x = 1\\n    y = 2\\n' # Don't generate empty test classes. # We need at least one statement in a method to be syntatically correct. # We're calling the function, so we have to make sure it will # be imported in the test # We have at least one call registered, so use it. # No calls were traced, so we'll go for a single test stub. # We're calling the method, so we have to make sure its class # will be imported in the test. # No calls were traced for those methods, so we'll go for simple test stubs.