1"""Helpers for writing unit tests."""
2
3from __future__ import (print_function, division, absolute_import,
4                        unicode_literals)
5try:
6    from collections.abc import Iterable
7except ImportError:  # python < 3.3
8    from collections import Iterable
9import os
10import shutil
11import sys
12import tempfile
13from unittest import TestCase as _TestCase
14from fontTools.misc.py23 import *
15from fontTools.misc.xmlWriter import XMLWriter
16
17
18def parseXML(xmlSnippet):
19    """Parses a snippet of XML.
20
21    Input can be either a single string (unicode or UTF-8 bytes), or a
22    a sequence of strings.
23
24    The result is in the same format that would be returned by
25    XMLReader, but the parser imposes no constraints on the root
26    element so it can be called on small snippets of TTX files.
27    """
28    # To support snippets with multiple elements, we add a fake root.
29    reader = TestXMLReader_()
30    xml = b"<root>"
31    if isinstance(xmlSnippet, bytes):
32        xml += xmlSnippet
33    elif isinstance(xmlSnippet, unicode):
34        xml += tobytes(xmlSnippet, 'utf-8')
35    elif isinstance(xmlSnippet, Iterable):
36        xml += b"".join(tobytes(s, 'utf-8') for s in xmlSnippet)
37    else:
38        raise TypeError("expected string or sequence of strings; found %r"
39                        % type(xmlSnippet).__name__)
40    xml += b"</root>"
41    reader.parser.Parse(xml, 0)
42    return reader.root[2]
43
44
45class FakeFont:
46    def __init__(self, glyphs):
47        self.glyphOrder_ = glyphs
48        self.reverseGlyphOrderDict_ = {g: i for i, g in enumerate(glyphs)}
49        self.lazy = False
50        self.tables = {}
51
52    def __getitem__(self, tag):
53        return self.tables[tag]
54
55    def __setitem__(self, tag, table):
56        self.tables[tag] = table
57
58    def get(self, tag, default=None):
59        return self.tables.get(tag, default)
60
61    def getGlyphID(self, name):
62        return self.reverseGlyphOrderDict_[name]
63
64    def getGlyphName(self, glyphID):
65        if glyphID < len(self.glyphOrder_):
66            return self.glyphOrder_[glyphID]
67        else:
68            return "glyph%.5d" % glyphID
69
70    def getGlyphOrder(self):
71        return self.glyphOrder_
72
73    def getReverseGlyphMap(self):
74        return self.reverseGlyphOrderDict_
75
76
77class TestXMLReader_(object):
78    def __init__(self):
79        from xml.parsers.expat import ParserCreate
80        self.parser = ParserCreate()
81        self.parser.StartElementHandler = self.startElement_
82        self.parser.EndElementHandler = self.endElement_
83        self.parser.CharacterDataHandler = self.addCharacterData_
84        self.root = None
85        self.stack = []
86
87    def startElement_(self, name, attrs):
88        element = (name, attrs, [])
89        if self.stack:
90            self.stack[-1][2].append(element)
91        else:
92            self.root = element
93        self.stack.append(element)
94
95    def endElement_(self, name):
96        self.stack.pop()
97
98    def addCharacterData_(self, data):
99        self.stack[-1][2].append(data)
100
101
102def makeXMLWriter(newlinestr='\n'):
103    # don't write OS-specific new lines
104    writer = XMLWriter(BytesIO(), newlinestr=newlinestr)
105    # erase XML declaration
106    writer.file.seek(0)
107    writer.file.truncate()
108    return writer
109
110
111def getXML(func, ttFont=None):
112    """Call the passed toXML function and return the written content as a
113    list of lines (unicode strings).
114    Result is stripped of XML declaration and OS-specific newline characters.
115    """
116    writer = makeXMLWriter()
117    func(writer, ttFont)
118    xml = writer.file.getvalue().decode("utf-8")
119    # toXML methods must always end with a writer.newline()
120    assert xml.endswith("\n")
121    return xml.splitlines()
122
123
124class MockFont(object):
125    """A font-like object that automatically adds any looked up glyphname
126    to its glyphOrder."""
127
128    def __init__(self):
129        self._glyphOrder = ['.notdef']
130
131        class AllocatingDict(dict):
132            def __missing__(reverseDict, key):
133                self._glyphOrder.append(key)
134                gid = len(reverseDict)
135                reverseDict[key] = gid
136                return gid
137        self._reverseGlyphOrder = AllocatingDict({'.notdef': 0})
138        self.lazy = False
139
140    def getGlyphID(self, glyph, requireReal=None):
141        gid = self._reverseGlyphOrder[glyph]
142        return gid
143
144    def getReverseGlyphMap(self):
145        return self._reverseGlyphOrder
146
147    def getGlyphName(self, gid):
148        return self._glyphOrder[gid]
149
150    def getGlyphOrder(self):
151        return self._glyphOrder
152
153
154class TestCase(_TestCase):
155
156    def __init__(self, methodName):
157        _TestCase.__init__(self, methodName)
158        # Python 3 renamed assertRaisesRegexp to assertRaisesRegex,
159        # and fires deprecation warnings if a program uses the old name.
160        if not hasattr(self, "assertRaisesRegex"):
161            self.assertRaisesRegex = self.assertRaisesRegexp
162
163
164class DataFilesHandler(TestCase):
165
166    def setUp(self):
167        self.tempdir = None
168        self.num_tempfiles = 0
169
170    def tearDown(self):
171        if self.tempdir:
172            shutil.rmtree(self.tempdir)
173
174    def getpath(self, testfile):
175        folder = os.path.dirname(sys.modules[self.__module__].__file__)
176        return os.path.join(folder, "data", testfile)
177
178    def temp_dir(self):
179        if not self.tempdir:
180            self.tempdir = tempfile.mkdtemp()
181
182    def temp_font(self, font_path, file_name):
183        self.temp_dir()
184        temppath = os.path.join(self.tempdir, file_name)
185        shutil.copy2(font_path, temppath)
186        return temppath
187