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