xref: /aosp_15_r20/external/fonttools/Lib/fontTools/misc/testTools.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
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