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