summaryrefslogtreecommitdiff
path: root/pystache/tests/test_mustachespec.py
blob: 453c5ea20d057dd49879e5000bd9efe8cae2f671 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# 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.

    """
    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:
        b = common.read(path)
        u = unicode(b, encoding=FILE_ENCODING)
        spec_data = parse(u)
        tests = spec_data['tests']

        for data in tests:
            case = _deserialize_spec_test(data, path)
            cases.append(case)

    # 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 _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 code strings to functions.
    # TODO: make this section of code easier to understand.
    new_context = {}
    for key, val in context.iteritems():
        if isinstance(val, dict) and val.get('__tag__') == 'code':
            val = eval(val['python'])
        new_context[key] = val

    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("%", "%%")

        subs = [repr(test_name), description, os.path.abspath(file_path), template, parser.__version__, str(parser)]
        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\"""

  %%s

  (using version %s of %s)
  """ % subs

        self.assertString(actual, expected, format=message)