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