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