testTools.py 6.8 KB

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