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