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