• 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.py23 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
41class FakeFont:
42    def __init__(self, glyphs):
43        self.glyphOrder_ = glyphs
44        self.reverseGlyphOrderDict_ = {g: i for i, g in enumerate(glyphs)}
45        self.lazy = False
46        self.tables = {}
47
48    def __getitem__(self, tag):
49        return self.tables[tag]
50
51    def __setitem__(self, tag, table):
52        self.tables[tag] = table
53
54    def get(self, tag, default=None):
55        return self.tables.get(tag, default)
56
57    def getGlyphID(self, name):
58        return self.reverseGlyphOrderDict_[name]
59
60    def getGlyphName(self, glyphID):
61        if glyphID < len(self.glyphOrder_):
62            return self.glyphOrder_[glyphID]
63        else:
64            return "glyph%.5d" % glyphID
65
66    def getGlyphOrder(self):
67        return self.glyphOrder_
68
69    def getReverseGlyphMap(self):
70        return self.reverseGlyphOrderDict_
71
72    def getGlyphNames(self):
73        return sorted(self.getGlyphOrder())
74
75
76class TestXMLReader_(object):
77    def __init__(self):
78        from xml.parsers.expat import ParserCreate
79        self.parser = ParserCreate()
80        self.parser.StartElementHandler = self.startElement_
81        self.parser.EndElementHandler = self.endElement_
82        self.parser.CharacterDataHandler = self.addCharacterData_
83        self.root = None
84        self.stack = []
85
86    def startElement_(self, name, attrs):
87        element = (name, attrs, [])
88        if self.stack:
89            self.stack[-1][2].append(element)
90        else:
91            self.root = element
92        self.stack.append(element)
93
94    def endElement_(self, name):
95        self.stack.pop()
96
97    def addCharacterData_(self, data):
98        self.stack[-1][2].append(data)
99
100
101def makeXMLWriter(newlinestr='\n'):
102    # don't write OS-specific new lines
103    writer = XMLWriter(BytesIO(), newlinestr=newlinestr)
104    # erase XML declaration
105    writer.file.seek(0)
106    writer.file.truncate()
107    return writer
108
109
110def getXML(func, ttFont=None):
111    """Call the passed toXML function and return the written content as a
112    list of lines (unicode strings).
113    Result is stripped of XML declaration and OS-specific newline characters.
114    """
115    writer = makeXMLWriter()
116    func(writer, ttFont)
117    xml = writer.file.getvalue().decode("utf-8")
118    # toXML methods must always end with a writer.newline()
119    assert xml.endswith("\n")
120    return xml.splitlines()
121
122
123class MockFont(object):
124    """A font-like object that automatically adds any looked up glyphname
125    to its glyphOrder."""
126
127    def __init__(self):
128        self._glyphOrder = ['.notdef']
129
130        class AllocatingDict(dict):
131            def __missing__(reverseDict, key):
132                self._glyphOrder.append(key)
133                gid = len(reverseDict)
134                reverseDict[key] = gid
135                return gid
136        self._reverseGlyphOrder = AllocatingDict({'.notdef': 0})
137        self.lazy = False
138
139    def getGlyphID(self, glyph, requireReal=None):
140        gid = self._reverseGlyphOrder[glyph]
141        return gid
142
143    def getReverseGlyphMap(self):
144        return self._reverseGlyphOrder
145
146    def getGlyphName(self, gid):
147        return self._glyphOrder[gid]
148
149    def getGlyphOrder(self):
150        return self._glyphOrder
151
152
153class TestCase(_TestCase):
154
155    def __init__(self, methodName):
156        _TestCase.__init__(self, methodName)
157        # Python 3 renamed assertRaisesRegexp to assertRaisesRegex,
158        # and fires deprecation warnings if a program uses the old name.
159        if not hasattr(self, "assertRaisesRegex"):
160            self.assertRaisesRegex = self.assertRaisesRegexp
161
162
163class DataFilesHandler(TestCase):
164
165    def setUp(self):
166        self.tempdir = None
167        self.num_tempfiles = 0
168
169    def tearDown(self):
170        if self.tempdir:
171            shutil.rmtree(self.tempdir)
172
173    def getpath(self, testfile):
174        folder = os.path.dirname(sys.modules[self.__module__].__file__)
175        return os.path.join(folder, "data", testfile)
176
177    def temp_dir(self):
178        if not self.tempdir:
179            self.tempdir = tempfile.mkdtemp()
180
181    def temp_font(self, font_path, file_name):
182        self.temp_dir()
183        temppath = os.path.join(self.tempdir, file_name)
184        shutil.copy2(font_path, temppath)
185        return temppath
186