xref: /aosp_15_r20/external/fonttools/Tests/feaLib/lexer_test.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1*e1fe3e4aSElliott Hughesfrom fontTools.feaLib.error import FeatureLibError, IncludedFeaNotFound
2*e1fe3e4aSElliott Hughesfrom fontTools.feaLib.lexer import IncludingLexer, Lexer
3*e1fe3e4aSElliott Hughesfrom fontTools.misc.textTools import tobytes
4*e1fe3e4aSElliott Hughesfrom io import StringIO
5*e1fe3e4aSElliott Hughesimport os
6*e1fe3e4aSElliott Hughesimport shutil
7*e1fe3e4aSElliott Hughesimport tempfile
8*e1fe3e4aSElliott Hughesimport unittest
9*e1fe3e4aSElliott Hughes
10*e1fe3e4aSElliott Hughes
11*e1fe3e4aSElliott Hughesdef lex(s):
12*e1fe3e4aSElliott Hughes    return [(typ, tok) for (typ, tok, _) in Lexer(s, "test.fea")]
13*e1fe3e4aSElliott Hughes
14*e1fe3e4aSElliott Hughes
15*e1fe3e4aSElliott Hughesclass LexerTest(unittest.TestCase):
16*e1fe3e4aSElliott Hughes    def __init__(self, methodName):
17*e1fe3e4aSElliott Hughes        unittest.TestCase.__init__(self, methodName)
18*e1fe3e4aSElliott Hughes        # Python 3 renamed assertRaisesRegexp to assertRaisesRegex,
19*e1fe3e4aSElliott Hughes        # and fires deprecation warnings if a program uses the old name.
20*e1fe3e4aSElliott Hughes        if not hasattr(self, "assertRaisesRegex"):
21*e1fe3e4aSElliott Hughes            self.assertRaisesRegex = self.assertRaisesRegexp
22*e1fe3e4aSElliott Hughes
23*e1fe3e4aSElliott Hughes    def test_empty(self):
24*e1fe3e4aSElliott Hughes        self.assertEqual(lex(""), [])
25*e1fe3e4aSElliott Hughes        self.assertEqual(lex(" \t "), [])
26*e1fe3e4aSElliott Hughes
27*e1fe3e4aSElliott Hughes    def test_name(self):
28*e1fe3e4aSElliott Hughes        self.assertEqual(lex("a17"), [(Lexer.NAME, "a17")])
29*e1fe3e4aSElliott Hughes        self.assertEqual(lex(".notdef"), [(Lexer.NAME, ".notdef")])
30*e1fe3e4aSElliott Hughes        self.assertEqual(lex("two.oldstyle"), [(Lexer.NAME, "two.oldstyle")])
31*e1fe3e4aSElliott Hughes        self.assertEqual(lex("_"), [(Lexer.NAME, "_")])
32*e1fe3e4aSElliott Hughes        self.assertEqual(lex("\\table"), [(Lexer.NAME, "\\table")])
33*e1fe3e4aSElliott Hughes        self.assertEqual(lex("a+*:^~!"), [(Lexer.NAME, "a+*:^~!")])
34*e1fe3e4aSElliott Hughes        self.assertEqual(lex("with-dash"), [(Lexer.NAME, "with-dash")])
35*e1fe3e4aSElliott Hughes
36*e1fe3e4aSElliott Hughes    def test_cid(self):
37*e1fe3e4aSElliott Hughes        self.assertEqual(lex("\\0 \\987"), [(Lexer.CID, 0), (Lexer.CID, 987)])
38*e1fe3e4aSElliott Hughes
39*e1fe3e4aSElliott Hughes    def test_glyphclass(self):
40*e1fe3e4aSElliott Hughes        self.assertEqual(lex("@Vowel.sc"), [(Lexer.GLYPHCLASS, "Vowel.sc")])
41*e1fe3e4aSElliott Hughes        self.assertEqual(lex("@Vowel-sc"), [(Lexer.GLYPHCLASS, "Vowel-sc")])
42*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(FeatureLibError, "Expected glyph class", lex, "@(a)")
43*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(FeatureLibError, "Expected glyph class", lex, "@ A")
44*e1fe3e4aSElliott Hughes        self.assertEqual(lex("@" + ("A" * 600)), [(Lexer.GLYPHCLASS, "A" * 600)])
45*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
46*e1fe3e4aSElliott Hughes            FeatureLibError, "Glyph class names must consist of", lex, "@Ab:c"
47*e1fe3e4aSElliott Hughes        )
48*e1fe3e4aSElliott Hughes
49*e1fe3e4aSElliott Hughes    def test_include(self):
50*e1fe3e4aSElliott Hughes        self.assertEqual(
51*e1fe3e4aSElliott Hughes            lex("include (~/foo/bar baz.fea);"),
52*e1fe3e4aSElliott Hughes            [
53*e1fe3e4aSElliott Hughes                (Lexer.NAME, "include"),
54*e1fe3e4aSElliott Hughes                (Lexer.FILENAME, "~/foo/bar baz.fea"),
55*e1fe3e4aSElliott Hughes                (Lexer.SYMBOL, ";"),
56*e1fe3e4aSElliott Hughes            ],
57*e1fe3e4aSElliott Hughes        )
58*e1fe3e4aSElliott Hughes        self.assertEqual(
59*e1fe3e4aSElliott Hughes            lex("include # Comment\n    (foo) \n;"),
60*e1fe3e4aSElliott Hughes            [
61*e1fe3e4aSElliott Hughes                (Lexer.NAME, "include"),
62*e1fe3e4aSElliott Hughes                (Lexer.COMMENT, "# Comment"),
63*e1fe3e4aSElliott Hughes                (Lexer.FILENAME, "foo"),
64*e1fe3e4aSElliott Hughes                (Lexer.SYMBOL, ";"),
65*e1fe3e4aSElliott Hughes            ],
66*e1fe3e4aSElliott Hughes        )
67*e1fe3e4aSElliott Hughes        self.assertRaises(FeatureLibError, lex, "include blah")
68*e1fe3e4aSElliott Hughes        self.assertRaises(FeatureLibError, lex, "include (blah")
69*e1fe3e4aSElliott Hughes
70*e1fe3e4aSElliott Hughes    def test_number(self):
71*e1fe3e4aSElliott Hughes        self.assertEqual(lex("123 -456"), [(Lexer.NUMBER, 123), (Lexer.NUMBER, -456)])
72*e1fe3e4aSElliott Hughes        self.assertEqual(lex("0xCAFED00D"), [(Lexer.HEXADECIMAL, 0xCAFED00D)])
73*e1fe3e4aSElliott Hughes        self.assertEqual(lex("0xcafed00d"), [(Lexer.HEXADECIMAL, 0xCAFED00D)])
74*e1fe3e4aSElliott Hughes        self.assertEqual(lex("010"), [(Lexer.OCTAL, 0o10)])
75*e1fe3e4aSElliott Hughes
76*e1fe3e4aSElliott Hughes    def test_float(self):
77*e1fe3e4aSElliott Hughes        self.assertEqual(lex("1.23 -4.5"), [(Lexer.FLOAT, 1.23), (Lexer.FLOAT, -4.5)])
78*e1fe3e4aSElliott Hughes
79*e1fe3e4aSElliott Hughes    def test_symbol(self):
80*e1fe3e4aSElliott Hughes        self.assertEqual(lex("a'"), [(Lexer.NAME, "a"), (Lexer.SYMBOL, "'")])
81*e1fe3e4aSElliott Hughes        self.assertEqual(lex("-A-B"), [(Lexer.SYMBOL, "-"), (Lexer.NAME, "A-B")])
82*e1fe3e4aSElliott Hughes        self.assertEqual(
83*e1fe3e4aSElliott Hughes            lex("foo - -2"),
84*e1fe3e4aSElliott Hughes            [(Lexer.NAME, "foo"), (Lexer.SYMBOL, "-"), (Lexer.NUMBER, -2)],
85*e1fe3e4aSElliott Hughes        )
86*e1fe3e4aSElliott Hughes
87*e1fe3e4aSElliott Hughes    def test_comment(self):
88*e1fe3e4aSElliott Hughes        self.assertEqual(
89*e1fe3e4aSElliott Hughes            lex("# Comment\n#"), [(Lexer.COMMENT, "# Comment"), (Lexer.COMMENT, "#")]
90*e1fe3e4aSElliott Hughes        )
91*e1fe3e4aSElliott Hughes
92*e1fe3e4aSElliott Hughes    def test_string(self):
93*e1fe3e4aSElliott Hughes        self.assertEqual(
94*e1fe3e4aSElliott Hughes            lex('"foo" "bar"'), [(Lexer.STRING, "foo"), (Lexer.STRING, "bar")]
95*e1fe3e4aSElliott Hughes        )
96*e1fe3e4aSElliott Hughes        self.assertEqual(
97*e1fe3e4aSElliott Hughes            lex('"foo \nbar\r baz \r\nqux\n\n "'), [(Lexer.STRING, "foo bar baz qux ")]
98*e1fe3e4aSElliott Hughes        )
99*e1fe3e4aSElliott Hughes        # The lexer should preserve escape sequences because they have
100*e1fe3e4aSElliott Hughes        # different interpretations depending on context. For better
101*e1fe3e4aSElliott Hughes        # or for worse, that is how the OpenType Feature File Syntax
102*e1fe3e4aSElliott Hughes        # has been specified; see section 9.e (name table) for examples.
103*e1fe3e4aSElliott Hughes        self.assertEqual(
104*e1fe3e4aSElliott Hughes            lex(r'"M\00fcller-Lanc\00e9"'),  # 'nameid 9'
105*e1fe3e4aSElliott Hughes            [(Lexer.STRING, r"M\00fcller-Lanc\00e9")],
106*e1fe3e4aSElliott Hughes        )
107*e1fe3e4aSElliott Hughes        self.assertEqual(
108*e1fe3e4aSElliott Hughes            lex(r'"M\9fller-Lanc\8e"'),  # 'nameid 9 1'
109*e1fe3e4aSElliott Hughes            [(Lexer.STRING, r"M\9fller-Lanc\8e")],
110*e1fe3e4aSElliott Hughes        )
111*e1fe3e4aSElliott Hughes        self.assertRaises(FeatureLibError, lex, '"foo\n bar')
112*e1fe3e4aSElliott Hughes
113*e1fe3e4aSElliott Hughes    def test_bad_character(self):
114*e1fe3e4aSElliott Hughes        self.assertRaises(FeatureLibError, lambda: lex("123 \u0001"))
115*e1fe3e4aSElliott Hughes
116*e1fe3e4aSElliott Hughes    def test_newline(self):
117*e1fe3e4aSElliott Hughes        def lines(s):
118*e1fe3e4aSElliott Hughes            return [loc.line for (_, _, loc) in Lexer(s, "test.fea")]
119*e1fe3e4aSElliott Hughes
120*e1fe3e4aSElliott Hughes        self.assertEqual(lines("FOO\n\nBAR\nBAZ"), [1, 3, 4])  # Unix
121*e1fe3e4aSElliott Hughes        self.assertEqual(lines("FOO\r\rBAR\rBAZ"), [1, 3, 4])  # Macintosh
122*e1fe3e4aSElliott Hughes        self.assertEqual(lines("FOO\r\n\r\n BAR\r\nBAZ"), [1, 3, 4])  # Windows
123*e1fe3e4aSElliott Hughes        self.assertEqual(lines("FOO\n\rBAR\r\nBAZ"), [1, 3, 4])  # mixed
124*e1fe3e4aSElliott Hughes
125*e1fe3e4aSElliott Hughes    def test_location(self):
126*e1fe3e4aSElliott Hughes        def locs(s):
127*e1fe3e4aSElliott Hughes            return [str(loc) for (_, _, loc) in Lexer(s, "test.fea")]
128*e1fe3e4aSElliott Hughes
129*e1fe3e4aSElliott Hughes        self.assertEqual(
130*e1fe3e4aSElliott Hughes            locs("a b # Comment\n12 @x"),
131*e1fe3e4aSElliott Hughes            [
132*e1fe3e4aSElliott Hughes                "test.fea:1:1",
133*e1fe3e4aSElliott Hughes                "test.fea:1:3",
134*e1fe3e4aSElliott Hughes                "test.fea:1:5",
135*e1fe3e4aSElliott Hughes                "test.fea:2:1",
136*e1fe3e4aSElliott Hughes                "test.fea:2:4",
137*e1fe3e4aSElliott Hughes            ],
138*e1fe3e4aSElliott Hughes        )
139*e1fe3e4aSElliott Hughes
140*e1fe3e4aSElliott Hughes    def test_scan_over_(self):
141*e1fe3e4aSElliott Hughes        lexer = Lexer("abbacabba12", "test.fea")
142*e1fe3e4aSElliott Hughes        self.assertEqual(lexer.pos_, 0)
143*e1fe3e4aSElliott Hughes        lexer.scan_over_("xyz")
144*e1fe3e4aSElliott Hughes        self.assertEqual(lexer.pos_, 0)
145*e1fe3e4aSElliott Hughes        lexer.scan_over_("abc")
146*e1fe3e4aSElliott Hughes        self.assertEqual(lexer.pos_, 9)
147*e1fe3e4aSElliott Hughes        lexer.scan_over_("abc")
148*e1fe3e4aSElliott Hughes        self.assertEqual(lexer.pos_, 9)
149*e1fe3e4aSElliott Hughes        lexer.scan_over_("0123456789")
150*e1fe3e4aSElliott Hughes        self.assertEqual(lexer.pos_, 11)
151*e1fe3e4aSElliott Hughes
152*e1fe3e4aSElliott Hughes    def test_scan_until_(self):
153*e1fe3e4aSElliott Hughes        lexer = Lexer("foo'bar", "test.fea")
154*e1fe3e4aSElliott Hughes        self.assertEqual(lexer.pos_, 0)
155*e1fe3e4aSElliott Hughes        lexer.scan_until_("'")
156*e1fe3e4aSElliott Hughes        self.assertEqual(lexer.pos_, 3)
157*e1fe3e4aSElliott Hughes        lexer.scan_until_("'")
158*e1fe3e4aSElliott Hughes        self.assertEqual(lexer.pos_, 3)
159*e1fe3e4aSElliott Hughes
160*e1fe3e4aSElliott Hughes
161*e1fe3e4aSElliott Hughesclass IncludingLexerTest(unittest.TestCase):
162*e1fe3e4aSElliott Hughes    @staticmethod
163*e1fe3e4aSElliott Hughes    def getpath(filename):
164*e1fe3e4aSElliott Hughes        path, _ = os.path.split(__file__)
165*e1fe3e4aSElliott Hughes        return os.path.join(path, "data", filename)
166*e1fe3e4aSElliott Hughes
167*e1fe3e4aSElliott Hughes    def test_include(self):
168*e1fe3e4aSElliott Hughes        lexer = IncludingLexer(self.getpath("include/include4.fea"))
169*e1fe3e4aSElliott Hughes        result = [
170*e1fe3e4aSElliott Hughes            "%s %s:%d" % (token, os.path.split(loc.file)[1], loc.line)
171*e1fe3e4aSElliott Hughes            for _, token, loc in lexer
172*e1fe3e4aSElliott Hughes        ]
173*e1fe3e4aSElliott Hughes        self.assertEqual(
174*e1fe3e4aSElliott Hughes            result,
175*e1fe3e4aSElliott Hughes            [
176*e1fe3e4aSElliott Hughes                "I4a include4.fea:1",
177*e1fe3e4aSElliott Hughes                "I3a include3.fea:1",
178*e1fe3e4aSElliott Hughes                "I2a include2.fea:1",
179*e1fe3e4aSElliott Hughes                "I1a include1.fea:1",
180*e1fe3e4aSElliott Hughes                "I0 include0.fea:1",
181*e1fe3e4aSElliott Hughes                "I1b include1.fea:3",
182*e1fe3e4aSElliott Hughes                "; include2.fea:2",
183*e1fe3e4aSElliott Hughes                "I2b include2.fea:3",
184*e1fe3e4aSElliott Hughes                "; include3.fea:2",
185*e1fe3e4aSElliott Hughes                "I3b include3.fea:3",
186*e1fe3e4aSElliott Hughes                "; include4.fea:2",
187*e1fe3e4aSElliott Hughes                "I4b include4.fea:3",
188*e1fe3e4aSElliott Hughes            ],
189*e1fe3e4aSElliott Hughes        )
190*e1fe3e4aSElliott Hughes
191*e1fe3e4aSElliott Hughes    def test_include_limit(self):
192*e1fe3e4aSElliott Hughes        lexer = IncludingLexer(self.getpath("include/include6.fea"))
193*e1fe3e4aSElliott Hughes        self.assertRaises(FeatureLibError, lambda: list(lexer))
194*e1fe3e4aSElliott Hughes
195*e1fe3e4aSElliott Hughes    def test_include_self(self):
196*e1fe3e4aSElliott Hughes        lexer = IncludingLexer(self.getpath("include/includeself.fea"))
197*e1fe3e4aSElliott Hughes        self.assertRaises(FeatureLibError, lambda: list(lexer))
198*e1fe3e4aSElliott Hughes
199*e1fe3e4aSElliott Hughes    def test_include_missing_file(self):
200*e1fe3e4aSElliott Hughes        lexer = IncludingLexer(self.getpath("include/includemissingfile.fea"))
201*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
202*e1fe3e4aSElliott Hughes            IncludedFeaNotFound,
203*e1fe3e4aSElliott Hughes            "includemissingfile.fea:1:8: The following feature file "
204*e1fe3e4aSElliott Hughes            "should be included but cannot be found: "
205*e1fe3e4aSElliott Hughes            "missingfile.fea",
206*e1fe3e4aSElliott Hughes            lambda: list(lexer),
207*e1fe3e4aSElliott Hughes        )
208*e1fe3e4aSElliott Hughes
209*e1fe3e4aSElliott Hughes    def test_featurefilepath_None(self):
210*e1fe3e4aSElliott Hughes        lexer = IncludingLexer(StringIO("# foobar"))
211*e1fe3e4aSElliott Hughes        self.assertIsNone(lexer.featurefilepath)
212*e1fe3e4aSElliott Hughes        files = set(loc.file for _, _, loc in lexer)
213*e1fe3e4aSElliott Hughes        self.assertIn("<features>", files)
214*e1fe3e4aSElliott Hughes
215*e1fe3e4aSElliott Hughes    def test_include_absolute_path(self):
216*e1fe3e4aSElliott Hughes        with tempfile.NamedTemporaryFile(delete=False) as included:
217*e1fe3e4aSElliott Hughes            included.write(
218*e1fe3e4aSElliott Hughes                tobytes(
219*e1fe3e4aSElliott Hughes                    """
220*e1fe3e4aSElliott Hughes                feature kern {
221*e1fe3e4aSElliott Hughes                    pos A B -40;
222*e1fe3e4aSElliott Hughes                } kern;
223*e1fe3e4aSElliott Hughes                """,
224*e1fe3e4aSElliott Hughes                    encoding="utf-8",
225*e1fe3e4aSElliott Hughes                )
226*e1fe3e4aSElliott Hughes            )
227*e1fe3e4aSElliott Hughes        including = StringIO("include(%s);" % included.name)
228*e1fe3e4aSElliott Hughes        try:
229*e1fe3e4aSElliott Hughes            lexer = IncludingLexer(including)
230*e1fe3e4aSElliott Hughes            files = set(loc.file for _, _, loc in lexer)
231*e1fe3e4aSElliott Hughes            self.assertIn(included.name, files)
232*e1fe3e4aSElliott Hughes        finally:
233*e1fe3e4aSElliott Hughes            os.remove(included.name)
234*e1fe3e4aSElliott Hughes
235*e1fe3e4aSElliott Hughes    def test_include_relative_to_cwd(self):
236*e1fe3e4aSElliott Hughes        # save current working directory, to be restored later
237*e1fe3e4aSElliott Hughes        cwd = os.getcwd()
238*e1fe3e4aSElliott Hughes        tmpdir = tempfile.mkdtemp()
239*e1fe3e4aSElliott Hughes        try:
240*e1fe3e4aSElliott Hughes            # create new feature file in a temporary directory
241*e1fe3e4aSElliott Hughes            with open(
242*e1fe3e4aSElliott Hughes                os.path.join(tmpdir, "included.fea"), "w", encoding="utf-8"
243*e1fe3e4aSElliott Hughes            ) as included:
244*e1fe3e4aSElliott Hughes                included.write(
245*e1fe3e4aSElliott Hughes                    """
246*e1fe3e4aSElliott Hughes                    feature kern {
247*e1fe3e4aSElliott Hughes                        pos A B -40;
248*e1fe3e4aSElliott Hughes                    } kern;
249*e1fe3e4aSElliott Hughes                    """
250*e1fe3e4aSElliott Hughes                )
251*e1fe3e4aSElliott Hughes            # change current folder to the temporary dir
252*e1fe3e4aSElliott Hughes            os.chdir(tmpdir)
253*e1fe3e4aSElliott Hughes            # instantiate a new lexer that includes the above file
254*e1fe3e4aSElliott Hughes            # using a relative path; the IncludingLexer does not
255*e1fe3e4aSElliott Hughes            # itself have a path, because it was initialized from
256*e1fe3e4aSElliott Hughes            # an in-memory stream, so it will use the current working
257*e1fe3e4aSElliott Hughes            # directory to resolve relative include statements
258*e1fe3e4aSElliott Hughes            lexer = IncludingLexer(StringIO("include(included.fea);"))
259*e1fe3e4aSElliott Hughes            files = set(os.path.realpath(loc.file) for _, _, loc in lexer)
260*e1fe3e4aSElliott Hughes            expected = os.path.realpath(included.name)
261*e1fe3e4aSElliott Hughes            self.assertIn(expected, files)
262*e1fe3e4aSElliott Hughes        finally:
263*e1fe3e4aSElliott Hughes            # remove temporary folder and restore previous working directory
264*e1fe3e4aSElliott Hughes            os.chdir(cwd)
265*e1fe3e4aSElliott Hughes            shutil.rmtree(tmpdir)
266*e1fe3e4aSElliott Hughes
267*e1fe3e4aSElliott Hughes
268*e1fe3e4aSElliott Hughesif __name__ == "__main__":
269*e1fe3e4aSElliott Hughes    import sys
270*e1fe3e4aSElliott Hughes
271*e1fe3e4aSElliott Hughes    sys.exit(unittest.main())
272