# coding: utf-8 """ Exposes a get_spec_tests() function for the project's test harness. Creates a unittest.TestCase for the tests defined in the mustache spec. """ # TODO: this module can be cleaned up somewhat. # TODO: move all of this code to pystache/tests/spectesting.py and # have it expose a get_spec_tests(spec_test_dir) function. FILE_ENCODING = 'utf-8' # the encoding of the spec test files. yaml = None try: # We try yaml first since it is more convenient when adding and modifying # test cases by hand (since the YAML is human-readable and is the master # from which the JSON format is generated). import yaml except ImportError: try: import json except: # The module json is not available prior to Python 2.6, whereas # simplejson is. The simplejson package dropped support for Python 2.4 # in simplejson v2.1.0, so Python 2.4 requires a simplejson install # older than the most recent version. try: import simplejson as json except ImportError: # Raise an error with a type different from ImportError as a hack around # this issue: # http://bugs.python.org/issue7559 from sys import exc_info ex_type, ex_value, tb = exc_info() new_ex = Exception("%s: %s" % (ex_type.__name__, ex_value)) raise new_ex.__class__, new_ex, tb file_extension = 'json' parser = json else: file_extension = 'yml' parser = yaml import codecs import glob import os.path import unittest import pystache from pystache import common from pystache.renderer import Renderer from pystache.tests.common import AssertStringMixin def get_spec_tests(spec_test_dir): """ Return a list of unittest.TestCase instances. """ # TODO: use logging module instead. print "pystache: spec tests: using %s" % _get_parser_info() cases = [] # Make this absolute for easier diagnosis in case of error. spec_test_dir = os.path.abspath(spec_test_dir) spec_paths = glob.glob(os.path.join(spec_test_dir, '*.%s' % file_extension)) for path in spec_paths: new_cases = _read_spec_tests(path) cases.extend(new_cases) # Store this as a value so that CheckSpecTestsFound is not checking # a reference to cases that contains itself. spec_test_count = len(cases) # This test case lets us alert the user that spec tests are missing. class CheckSpecTestsFound(unittest.TestCase): def runTest(self): if spec_test_count > 0: return raise Exception("Spec tests not found--\n in %s\n" " Consult the README file on how to add the Mustache spec tests." % repr(spec_test_dir)) case = CheckSpecTestsFound() cases.append(case) return cases def _get_parser_info(): return "%s (version %s)" % (parser.__name__, parser.__version__) def _read_spec_tests(path): """ Return a list of unittest.TestCase instances. """ b = common.read(path) u = unicode(b, encoding=FILE_ENCODING) spec_data = parse(u) tests = spec_data['tests'] cases = [] for data in tests: case = _deserialize_spec_test(data, path) cases.append(case) return cases # TODO: simplify the implementation of this function. def _convert_children(node): """ Recursively convert to functions all "code strings" below the node. This function is needed only for the json format. """ if not isinstance(node, (list, dict)): # Then there is nothing to iterate over and recurse. return if isinstance(node, list): for child in node: _convert_children(child) return # Otherwise, node is a dict, so attempt the conversion. for key in node.keys(): val = node[key] if not isinstance(val, dict) or val.get('__tag__') != 'code': _convert_children(val) continue # Otherwise, we are at a "leaf" node. val = eval(val['python']) node[key] = val continue def _deserialize_spec_test(data, file_path): """ Return a unittest.TestCase instance representing a spec test. Arguments: data: the dictionary of attributes for a single test. """ context = data['data'] description = data['desc'] # PyYAML seems to leave ASCII strings as byte strings. expected = unicode(data['expected']) # TODO: switch to using dict.get(). partials = data.has_key('partials') and data['partials'] or {} template = data['template'] test_name = data['name'] _convert_children(context) test_case = _make_spec_test(expected, template, context, partials, description, test_name, file_path) return test_case def _make_spec_test(expected, template, context, partials, description, test_name, file_path): """ Return a unittest.TestCase instance representing a spec test. """ file_name = os.path.basename(file_path) test_method_name = "Mustache spec (%s): %s" % (file_name, repr(test_name)) # We subclass SpecTestBase in order to control the test method name (for # the purposes of improved reporting). class SpecTest(SpecTestBase): pass def run_test(self): self._runTest() # TODO: should we restore this logic somewhere? # If we don't convert unicode to str, we get the following error: # "TypeError: __name__ must be set to a string object" # test.__name__ = str(name) setattr(SpecTest, test_method_name, run_test) case = SpecTest(test_method_name) case._context = context case._description = description case._expected = expected case._file_path = file_path case._partials = partials case._template = template case._test_name = test_name return case def parse(u): """ Parse the contents of a spec test file, and return a dict. Arguments: u: a unicode string. """ # TODO: find a cleaner mechanism for choosing between the two. if yaml is None: # Then use json. # The only way to get the simplejson module to return unicode strings # is to pass it unicode. See, for example-- # # http://code.google.com/p/simplejson/issues/detail?id=40 # # and the documentation of simplejson.loads(): # # "If s is a str then decoded JSON strings that contain only ASCII # characters may be parsed as str for performance and memory reasons. # If your code expects only unicode the appropriate solution is # decode s to unicode prior to calling loads." # return json.loads(u) # Otherwise, yaml. def code_constructor(loader, node): value = loader.construct_mapping(node) return eval(value['python'], {}) yaml.add_constructor(u'!code', code_constructor) return yaml.load(u) class SpecTestBase(unittest.TestCase, AssertStringMixin): def _runTest(self): context = self._context description = self._description expected = self._expected file_path = self._file_path partials = self._partials template = self._template test_name = self._test_name renderer = Renderer(partials=partials) actual = renderer.render(template, context) # We need to escape the strings that occur in our format string because # they can contain % symbols, for example (in delimiters.yml)-- # # "template: '{{=<% %>=}}(<%text%>)'" # def escape(s): return s.replace("%", "%%") parser_info = _get_parser_info() subs = [repr(test_name), description, os.path.abspath(file_path), template, repr(context), parser_info] subs = tuple([escape(sub) for sub in subs]) # We include the parsing module version info to help with troubleshooting # yaml/json/simplejson issues. message = """%s: %s File: %s Template: \"""%s\""" Context: %s %%s [using %s] """ % subs self.assertString(actual, expected, format=message)