diff --git a/Tests/ttx/ttx_test.py b/Tests/ttx/ttx_test.py index 41c5c2896..827715cda 100644 --- a/Tests/ttx/ttx_test.py +++ b/Tests/ttx/ttx_test.py @@ -1,14 +1,17 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * from fontTools.misc.testTools import parseXML +from fontTools.misc.timeTools import timestampSinceEpoch +from fontTools.ttLib import TTFont, TTLibError from fontTools import ttx import getopt +import logging import os import shutil -import sys import tempfile import unittest +import pytest class TTXTest(unittest.TestCase): def __init__(self, methodName): @@ -263,5 +266,565 @@ class TTXTest(unittest.TestCase): self.assertIsNone(ttx.guessFileType(font_path)) -if __name__ == "__main__": - sys.exit(unittest.main()) +# ----------------------- +# ttx.Options class tests +# ----------------------- + + +def test_ttx_options_class_defaults(): + tto = ttx.Options([(None, None)], 1) + assert tto.listTables is False + assert tto.outputDir is None + assert tto.outputFile is None + assert tto.overWrite is False + assert tto.verbose is False + assert tto.quiet is False + assert tto.splitTables is False + assert tto.splitGlyphs is False + assert tto.disassembleInstructions is True + assert tto.mergeFile is None + assert tto.recalcBBoxes is True + assert tto.allowVID is False + assert tto.ignoreDecompileErrors is True + assert tto.bitmapGlyphDataFormat == "raw" + assert tto.unicodedata is None + assert tto.newlinestr is None + assert tto.recalcTimestamp is False + assert tto.flavor is None + assert tto.useZopfli is False + assert tto.onlyTables == [] + assert tto.skipTables == [] + assert tto.fontNumber == -1 + assert tto.logLevel == logging.INFO + + +def test_ttx_options_class_flag_h(capsys): + with pytest.raises(SystemExit): + ttx.Options([("-h", None)], 1) + + out, err = capsys.readouterr() + assert "TTX -- From OpenType To XML And Back" in out + + +def test_ttx_options_class_flag_version(capsys): + with pytest.raises(SystemExit): + ttx.Options([("--version", None)], 1) + + out, err = capsys.readouterr() + version_list = out.split(".") + assert len(version_list) >= 3 + assert version_list[0].isdigit() + assert version_list[1].isdigit() + assert version_list[2].isdigit() + + +def test_ttx_options_class_flag_d_goodpath(tmpdir): + temp_dir_path = str(tmpdir) + tto = ttx.Options([("-d", temp_dir_path)], 1) + assert tto.outputDir == temp_dir_path + + +def test_ttx_options_class_flag_d_badpath(): + with pytest.raises(getopt.GetoptError): + ttx.Options([("-d", "bogusdir")], 1) + + +def test_ttx_options_class_flag_o(): + tto = ttx.Options([("-o", "testfile.ttx")], 1) + assert tto.outputFile == "testfile.ttx" + + +def test_ttx_options_class_flag_f(): + tto = ttx.Options([("-f", "")], 1) + assert tto.overWrite is True + + +def test_ttx_options_class_flag_v(): + tto = ttx.Options([("-v", "")], 1) + assert tto.verbose is True + assert tto.logLevel == logging.DEBUG + + +def test_ttx_options_class_flag_q(): + tto = ttx.Options([("-q", "")], 1) + assert tto.quiet is True + assert tto.logLevel == logging.WARNING + + +def test_ttx_options_class_flag_l(): + tto = ttx.Options([("-l", "")], 1) + assert tto.listTables is True + + +def test_ttx_options_class_flag_t_nopadding(): + tto = ttx.Options([("-t", "CFF2")], 1) + assert len(tto.onlyTables) == 1 + assert tto.onlyTables[0] == "CFF2" + + +def test_ttx_options_class_flag_t_withpadding(): + tto = ttx.Options([("-t", "CFF")], 1) + assert len(tto.onlyTables) == 1 + assert tto.onlyTables[0] == "CFF " + + +def test_ttx_options_class_flag_s(): + tto = ttx.Options([("-s", "")], 1) + assert tto.splitTables is True + assert tto.splitGlyphs is False + + +def test_ttx_options_class_flag_g(): + tto = ttx.Options([("-g", "")], 1) + assert tto.splitGlyphs is True + assert tto.splitTables is True + + +def test_ttx_options_class_flag_i(): + tto = ttx.Options([("-i", "")], 1) + assert tto.disassembleInstructions is False + + +def test_ttx_options_class_flag_z_validoptions(): + valid_options = ('raw', 'row', 'bitwise', 'extfile') + for option in valid_options: + tto = ttx.Options([("-z", option)], 1) + assert tto.bitmapGlyphDataFormat == option + + +def test_ttx_options_class_flag_z_invalidoption(): + with pytest.raises(getopt.GetoptError): + ttx.Options([("-z", "bogus")], 1) + + +def test_ttx_options_class_flag_y_validvalue(): + tto = ttx.Options([("-y", "1")], 1) + assert tto.fontNumber == 1 + + +# TODO: fix this with a check in the ttx module: str.isdigit() +def test_ttx_options_class_flag_y_invalidvalue(): + with pytest.raises(ValueError): + ttx.Options([("-y", "A")], 1) + + +def test_ttx_options_class_flag_m(): + tto = ttx.Options([("-m", "testfont.ttf")], 1) + assert tto.mergeFile == "testfont.ttf" + + +def test_ttx_options_class_flag_b(): + tto = ttx.Options([("-b", "")], 1) + assert tto.recalcBBoxes is False + + +def test_ttx_options_class_flag_a(): + tto = ttx.Options([("-a", "")], 1) + assert tto.allowVID is True + + +def test_ttx_options_class_flag_e(): + tto = ttx.Options([("-e", "")], 1) + assert tto.ignoreDecompileErrors is False + + +def test_ttx_options_class_flag_unicodedata(): + tto = ttx.Options([("--unicodedata", "UnicodeData.txt")], 1) + assert tto.unicodedata == "UnicodeData.txt" + + +def test_ttx_options_class_flag_newline_lf(): + tto = ttx.Options([("--newline", "LF")], 1) + assert tto.newlinestr == "\n" + + +def test_ttx_options_class_flag_newline_cr(): + tto = ttx.Options([("--newline", "CR")], 1) + assert tto.newlinestr == "\r" + + +def test_ttx_options_class_flag_newline_crlf(): + tto = ttx.Options([("--newline", "CRLF")], 1) + assert tto.newlinestr == "\r\n" + + +def test_ttx_options_class_flag_newline_invalid(): + with pytest.raises(getopt.GetoptError): + ttx.Options([("--newline", "BOGUS")], 1) + + +def test_ttx_options_class_flag_recalc_timestamp(): + tto = ttx.Options([("--recalc-timestamp", "")], 1) + assert tto.recalcTimestamp is True + + +def test_ttx_options_class_flag_flavor(): + tto = ttx.Options([("--flavor", "woff")], 1) + assert tto.flavor == "woff" + + +def test_ttx_options_class_flag_with_zopfli(): + tto = ttx.Options([("--with-zopfli", ""), ("--flavor", "woff")], 1) + assert tto.useZopfli is True + + +def test_ttx_options_class_flag_with_zopfli_fails_without_woff_flavor(): + with pytest.raises(getopt.GetoptError): + ttx.Options([("--with-zopfli", "")], 1) + + +def test_ttx_options_class_flag_quiet_and_verbose_shouldfail(): + with pytest.raises(getopt.GetoptError): + ttx.Options([("-q", ""), ("-v", "")], 1) + + +def test_ttx_options_class_flag_mergefile_and_flavor_shouldfail(): + with pytest.raises(getopt.GetoptError): + ttx.Options([("-m", "testfont.ttf"), ("--flavor", "woff")], 1) + + +def test_ttx_options_class_flag_onlytables_and_skiptables_shouldfail(): + with pytest.raises(getopt.GetoptError): + ttx.Options([("-t", "CFF"), ("-x", "CFF2")], 1) + + +def test_ttx_options_class_flag_mergefile_and_multiplefiles_shouldfail(): + with pytest.raises(getopt.GetoptError): + ttx.Options([("-m", "testfont.ttf")], 2) + + +def test_ttx_options_class_flag_woff2_and_zopfli_shouldfail(): + with pytest.raises(getopt.GetoptError): + ttx.Options([("--with-zopfli", ""), ("--flavor", "woff2")], 1) + +# ---------------------------- +# ttx.ttCompile function tests +# ---------------------------- + + +def test_ttx_ttcompile_otf_compile_default(tmpdir): + inttx = os.path.join("Tests", "ttx", "data", "TestOTF.ttx") + outotf = os.path.join(str(tmpdir), "TestOTF.otf") + default_options = ttx.Options([("", "")], 1) + ttx.ttCompile(inttx, outotf, default_options) + # confirm that font was built + assert os.path.isfile(outotf) is True + # confirm that it is valid OTF file, can instantiate a TTFont, has expected OpenType tables + ttf = TTFont(outotf) + expected_tables = ("head", "hhea", "maxp", "OS/2", "name", "cmap", "post", "CFF ", "hmtx", "DSIG") + for table in expected_tables: + assert table in ttf + + +def test_ttx_ttcompile_otf_to_woff_without_zopfli(tmpdir): + inttx = os.path.join("Tests", "ttx", "data", "TestOTF.ttx") + outwoff = os.path.join(str(tmpdir), "TestOTF.woff") + options = ttx.Options([("", "")], 1) + options.flavor = "woff" + ttx.ttCompile(inttx, outwoff, options) + # confirm that font was built + assert os.path.isfile(outwoff) is True + # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables + ttf = TTFont(outwoff) + expected_tables = ("head", "hhea", "maxp", "OS/2", "name", "cmap", "post", "CFF ", "hmtx", "DSIG") + for table in expected_tables: + assert table in ttf + + +def test_ttx_ttcompile_otf_to_woff_with_zopfli(tmpdir): + inttx = os.path.join("Tests", "ttx", "data", "TestOTF.ttx") + outwoff = os.path.join(str(tmpdir), "TestOTF.woff") + options = ttx.Options([("", "")], 1) + options.flavor = "woff" + options.useZopfli = True + ttx.ttCompile(inttx, outwoff, options) + # confirm that font was built + assert os.path.isfile(outwoff) is True + # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables + ttf = TTFont(outwoff) + expected_tables = ("head", "hhea", "maxp", "OS/2", "name", "cmap", "post", "CFF ", "hmtx", "DSIG") + for table in expected_tables: + assert table in ttf + + +def test_ttx_ttcompile_otf_to_woff2(tmpdir): + inttx = os.path.join("Tests", "ttx", "data", "TestOTF.ttx") + outwoff2 = os.path.join(str(tmpdir), "TestTTF.woff2") + options = ttx.Options([("", "")], 1) + options.flavor = "woff2" + ttx.ttCompile(inttx, outwoff2, options) + # confirm that font was built + assert os.path.isfile(outwoff2) is True + # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables + ttf = TTFont(outwoff2) + # DSIG should not be included from original ttx as per woff2 spec (https://dev.w3.org/webfonts/WOFF2/spec/) + assert "DSIG" not in ttf + expected_tables = ("head", "hhea", "maxp", "OS/2", "name", "cmap", "post", "CFF ", "hmtx") + for table in expected_tables: + assert table in ttf + + +def test_ttx_ttcompile_ttf_compile_default(tmpdir): + inttx = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") + outttf = os.path.join(str(tmpdir), "TestTTF.ttf") + default_options = ttx.Options([("", "")], 1) + ttx.ttCompile(inttx, outttf, default_options) + # confirm that font was built + assert os.path.isfile(outttf) is True + # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables + ttf = TTFont(outttf) + expected_tables = ("head", "hhea", "maxp", "OS/2", "name", "cmap", "hmtx", "fpgm", "prep", "cvt ", "loca", "glyf", "post", "gasp", "DSIG") + for table in expected_tables: + assert table in ttf + + +def test_ttx_ttcompile_ttf_to_woff_without_zopfli(tmpdir): + inttx = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") + outwoff = os.path.join(str(tmpdir), "TestTTF.woff") + options = ttx.Options([("", "")], 1) + options.flavor = "woff" + ttx.ttCompile(inttx, outwoff, options) + # confirm that font was built + assert os.path.isfile(outwoff) is True + # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables + ttf = TTFont(outwoff) + expected_tables = ("head", "hhea", "maxp", "OS/2", "name", "cmap", "hmtx", "fpgm", "prep", "cvt ", "loca", "glyf", "post", "gasp", "DSIG") + for table in expected_tables: + assert table in ttf + + +def test_ttx_ttcompile_ttf_to_woff_with_zopfli(tmpdir): + inttx = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") + outwoff = os.path.join(str(tmpdir), "TestTTF.woff") + options = ttx.Options([("", "")], 1) + options.flavor = "woff" + options.useZopfli = True + ttx.ttCompile(inttx, outwoff, options) + # confirm that font was built + assert os.path.isfile(outwoff) is True + # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables + ttf = TTFont(outwoff) + expected_tables = ("head", "hhea", "maxp", "OS/2", "name", "cmap", "hmtx", "fpgm", "prep", "cvt ", "loca", "glyf", "post", "gasp", "DSIG") + for table in expected_tables: + assert table in ttf + + +def test_ttx_ttcompile_ttf_to_woff2(tmpdir): + inttx = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") + outwoff2 = os.path.join(str(tmpdir), "TestTTF.woff2") + options = ttx.Options([("", "")], 1) + options.flavor = "woff2" + ttx.ttCompile(inttx, outwoff2, options) + # confirm that font was built + assert os.path.isfile(outwoff2) is True + # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables + ttf = TTFont(outwoff2) + # DSIG should not be included from original ttx as per woff2 spec (https://dev.w3.org/webfonts/WOFF2/spec/) + assert "DSIG" not in ttf + expected_tables = ("head", "hhea", "maxp", "OS/2", "name", "cmap", "hmtx", "fpgm", "prep", "cvt ", "loca", "glyf", "post", "gasp") + for table in expected_tables: + assert table in ttf + + +def test_ttx_ttcompile_ttf_timestamp_calcs(tmpdir): + inttx = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") + outttf1 = os.path.join(str(tmpdir), "TestTTF1.ttf") + outttf2 = os.path.join(str(tmpdir), "TestTTF2.ttf") + options = ttx.Options([("", "")], 1) + # build with default options = do not recalculate timestamp + ttx.ttCompile(inttx, outttf1, options) + # confirm that font was built + assert os.path.isfile(outttf1) is True + # confirm that timestamp is same as modified time on ttx file + mtime = os.path.getmtime(inttx) + epochtime = timestampSinceEpoch(mtime) + ttf = TTFont(outttf1) + assert ttf['head'].modified == epochtime + + # reset options to recalculate the timestamp and compile new font + options.recalcTimestamp = True + ttx.ttCompile(inttx, outttf2, options) + # confirm that font was built + assert os.path.isfile(outttf2) is True + # confirm that timestamp is more recent than modified time on ttx file + mtime = os.path.getmtime(inttx) + epochtime = timestampSinceEpoch(mtime) + ttf = TTFont(outttf2) + assert ttf['head'].modified > epochtime + + +def test_ttx_ttcompile_otf_timestamp_calcs(tmpdir): + inttx = os.path.join("Tests", "ttx", "data", "TestOTF.ttx") + outotf1 = os.path.join(str(tmpdir), "TestOTF1.ttf") + outotf2 = os.path.join(str(tmpdir), "TestOTF2.ttf") + options = ttx.Options([("", "")], 1) + # build with default options = do not recalculate timestamp + ttx.ttCompile(inttx, outotf1, options) + # confirm that font was built + assert os.path.isfile(outotf1) is True + # confirm that timestamp is same as modified time on ttx file + mtime = os.path.getmtime(inttx) + epochtime = timestampSinceEpoch(mtime) + ttf = TTFont(outotf1) + assert ttf['head'].modified == epochtime + + # reset options to recalculate the timestamp and compile new font + options.recalcTimestamp = True + ttx.ttCompile(inttx, outotf2, options) + # confirm that font was built + assert os.path.isfile(outotf2) is True + # confirm that timestamp is more recent than modified time on ttx file + mtime = os.path.getmtime(inttx) + epochtime = timestampSinceEpoch(mtime) + ttf = TTFont(outotf2) + assert ttf['head'].modified > epochtime + + +# ------------------------- +# ttx.ttList function tests +# ------------------------- + +def test_ttx_ttlist_ttf(capsys, tmpdir): + inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttf") + fakeoutpath = os.path.join(str(tmpdir), "TestTTF.ttx") + options = ttx.Options([("", "")], 1) + options.listTables = True + ttx.ttList(inpath, fakeoutpath, options) + out, err = capsys.readouterr() + expected_tables = ("head", "hhea", "maxp", "OS/2", "name", "cmap", "hmtx", "fpgm", "prep", "cvt ", "loca", "glyf", "post", "gasp", "DSIG") + # confirm that expected tables are printed to stdout + for table in expected_tables: + assert table in out + # test for one of the expected tag/checksum/length/offset strings + assert "OS/2 0x67230FF8 96 376" in out + + +def test_ttx_ttlist_otf(capsys, tmpdir): + inpath = os.path.join("Tests", "ttx", "data", "TestOTF.Otf") + fakeoutpath = os.path.join(str(tmpdir), "TestOTF.ttx") + options = ttx.Options([("", "")], 1) + options.listTables = True + ttx.ttList(inpath, fakeoutpath, options) + out, err = capsys.readouterr() + expected_tables = ("head", "hhea", "maxp", "OS/2", "name", "cmap", "post", "CFF ", "hmtx", "DSIG") + # confirm that expected tables are printed to stdout + for table in expected_tables: + assert table in out + # test for one of the expected tag/checksum/length/offset strings + assert "OS/2 0x67230FF8 96 272" in out + + +def test_ttx_ttlist_woff(capsys, tmpdir): + inpath = os.path.join("Tests", "ttx", "data", "TestWOFF.woff") + fakeoutpath = os.path.join(str(tmpdir), "TestWOFF.ttx") + options = ttx.Options([("", "")], 1) + options.listTables = True + options.flavor = "woff" + ttx.ttList(inpath, fakeoutpath, options) + out, err = capsys.readouterr() + expected_tables = ("head", "hhea", "maxp", "OS/2", "name", "cmap", "post", "CFF ", "hmtx", "DSIG") + # confirm that expected tables are printed to stdout + for table in expected_tables: + assert table in out + # test for one of the expected tag/checksum/length/offset strings + assert "OS/2 0x67230FF8 84 340" in out + + +def test_ttx_ttlist_woff2(capsys, tmpdir): + inpath = os.path.join("Tests", "ttx", "data", "TestWOFF2.woff2") + fakeoutpath = os.path.join(str(tmpdir), "TestWOFF2.ttx") + options = ttx.Options([("", "")], 1) + options.listTables = True + options.flavor = "woff2" + ttx.ttList(inpath, fakeoutpath, options) + out, err = capsys.readouterr() + expected_tables = ("head", "hhea", "maxp", "OS/2", "name", "cmap", "hmtx", "fpgm", "prep", "cvt ", "loca", "glyf", "post", "gasp") + # confirm that expected tables are printed to stdout + for table in expected_tables: + assert table in out + # test for one of the expected tag/checksum/length/offset strings + assert "OS/2 0x67230FF8 96 0" in out + +# ------------------- +# main function tests +# ------------------- + + +def test_ttx_main_default_ttf_dump_to_ttx(tmpdir): + inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttf") + outpath = os.path.join(str(tmpdir), "TestTTF.ttx") + args = ["-o", outpath, inpath] + ttx.main(args) + assert os.path.isfile(outpath) + + +def test_ttx_main_default_ttx_compile_to_ttf(tmpdir): + inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") + outpath = os.path.join(str(tmpdir), "TestTTF.ttf") + args = ["-o", outpath, inpath] + ttx.main(args) + assert os.path.isfile(outpath) + + +def test_ttx_main_getopterror_missing_directory(): + with pytest.raises(SystemExit): + with pytest.raises(getopt.GetoptError): + inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttf") + args = ["-d", "bogusdir", inpath] + ttx.main(args) + + +def test_ttx_main_keyboard_interrupt(tmpdir, monkeypatch, capsys): + with pytest.raises(SystemExit): + inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") + outpath = os.path.join(str(tmpdir), "TestTTF.ttf") + args = ["-o", outpath, inpath] + monkeypatch.setattr(ttx, 'process', (lambda x, y: raise_exception(KeyboardInterrupt))) + ttx.main(args) + + out, err = capsys.readouterr() + assert "(Cancelled.)" in err + + +def test_ttx_main_system_exit(tmpdir, monkeypatch): + with pytest.raises(SystemExit): + inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") + outpath = os.path.join(str(tmpdir), "TestTTF.ttf") + args = ["-o", outpath, inpath] + monkeypatch.setattr(ttx, 'process', (lambda x, y: raise_exception(SystemExit))) + ttx.main(args) + + +def test_ttx_main_ttlib_error(tmpdir, monkeypatch, capsys): + with pytest.raises(SystemExit): + inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") + outpath = os.path.join(str(tmpdir), "TestTTF.ttf") + args = ["-o", outpath, inpath] + monkeypatch.setattr(ttx, 'process', (lambda x, y: raise_exception(TTLibError("Test error")))) + ttx.main(args) + + out, err = capsys.readouterr() + assert "Test error" in err + + +def test_ttx_main_base_exception(tmpdir, monkeypatch, capsys): + with pytest.raises(SystemExit): + inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") + outpath = os.path.join(str(tmpdir), "TestTTF.ttf") + args = ["-o", outpath, inpath] + monkeypatch.setattr(ttx, 'process', (lambda x, y: raise_exception(Exception("Test error")))) + ttx.main(args) + + out, err = capsys.readouterr() + assert "Unhandled exception has occurred" in err + +# --------------------------- +# support functions for tests +# --------------------------- + + +def raise_exception(exception): + raise exception + + +