# coding=UTF-8
#
# Copyright 2015 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


import glob
from os import path
import unittest

from fontTools import ttLib
import freetype

from nototools import coverage
from nototools import font_data
from nototools import noto_fonts
from nototools import unicode_data
from nototools.glyph_area_pen import GlyphAreaPen
from nototools.unittests import layout
from nototools.py23 import unichr


def get_rendered_char_height(font_filename, font_size, char, target="mono"):
    if target == "mono":
        render_params = freetype.FT_LOAD_TARGET_MONO
    elif target == "lcd":
        render_params = freetype.FT_LOAD_TARGET_LCD
    render_params |= freetype.FT_LOAD_RENDER

    face = freetype.Face(font_filename)
    face.set_char_size(font_size * 64)
    face.load_char(char, render_params)
    return face.glyph.bitmap.rows


def load_fonts(patterns, expected_count=None, font_class=None):
    """Load all fonts specified in the patterns.

    Also assert that the number of the fonts found is exactly the same as
    expected_count."""

    if font_class is None:
        font_class = ttLib.TTFont

    all_font_files = []
    for pattern in patterns:
        all_font_files += glob.glob(pattern)
    all_fonts = [font_class(font) for font in all_font_files]
    if expected_count:
        assert len(all_font_files) == expected_count, "got %d fonts, expected %d." % (
            len(all_font_files),
            expected_count,
        )
    return all_font_files, all_fonts


class FontTest(unittest.TestCase):
    """Parent class for all font tests."""

    loaded_fonts = None


class TestItalicAngle(FontTest):
    """Test the italic angle of fonts."""

    def setUp(self):
        _, self.fonts = self.loaded_fonts

    def test_italic_angle(self):
        """Tests the italic angle of fonts to be correct."""
        for font in self.fonts:
            post_table = font["post"]
            if "Italic" in font_data.font_name(font):
                expected_angle = self.expected_italic_angle
            else:
                expected_angle = 0.0
            self.assertEqual(post_table.italicAngle, expected_angle)


class TestMetaInfo(FontTest):
    """Test various meta information."""

    mark_heavier_as_bold = False
    mark_italic_as_oblique = False

    def setUp(self):
        _, self.fonts = self.loaded_fonts

    def parse_metadata(self, font):
        """Parse font name to infer weight and slope."""

        font_name = font_data.font_name(font)
        bold = ("Bold" in font_name) or (
            self.mark_heavier_as_bold and "Black" in font_name
        )
        italic = "Italic" in font_name
        return bold, italic

    def test_mac_style(self):
        """Tests the macStyle of the fonts to be correct."""

        for font in self.fonts:
            bold, italic = self.parse_metadata(font)
            expected_mac_style = (italic << 1) | bold
            self.assertEqual(font["head"].macStyle, expected_mac_style)

    def test_fs_selection(self):
        """Tests the fsSelection to be correct."""

        for font in self.fonts:
            bold, italic = self.parse_metadata(font)
            expected_fs_type = ((bold << 5) | italic) or (1 << 6)
            if italic and self.mark_italic_as_oblique:
                expected_fs_type |= 1 << 9
            self.assertEqual(font["OS/2"].fsSelection, expected_fs_type)

    def test_fs_type(self):
        """Tests the fsType of the fonts."""

        for font in self.fonts:
            self.assertEqual(font["OS/2"].fsType, self.expected_os2_fsType)

    def test_vendor_id(self):
        """Tests the vendor ID of the fonts."""
        for font in self.fonts:
            self.assertEqual(font["OS/2"].achVendID, self.expected_os2_achVendID)

    def test_us_weight(self):
        "Tests the usWeight of the fonts to be correct." ""
        for font in self.fonts:
            weight = noto_fonts.parse_weight(font_data.font_name(font))
            expected_numeric_weight = noto_fonts.WEIGHTS[weight]
            # hack for windows GDI
            expected_numeric_weight = max(expected_numeric_weight, 250)
            self.assertEqual(font["OS/2"].usWeightClass, expected_numeric_weight)

    def test_version_numbers(self):
        "Tests the two version numbers of the font to be correct." ""
        for font in self.fonts:
            version = font_data.font_version(font)
            usable_part_of_version = version.split(";")[0]
            self.assertEqual(usable_part_of_version, "Version " + self.expected_version)

            revision = font_data.printable_font_revision(font, accuracy=3)
            self.assertEqual(revision, self.expected_version)


class TestNames(FontTest):
    """Tests various strings in the name table."""

    mark_heavier_as_bold = False

    def setUp(self):
        font_files, self.fonts = self.loaded_fonts
        self.font_files = [path.basename(f) for f in font_files]
        self.condensed_family_name = self.family_name + " Condensed"
        self.names = []
        for font in self.fonts:
            self.names.append(font_data.get_name_records(font))

    def test_copyright(self):
        """Tests the copyright message."""
        for records in self.names:
            self.assertEqual(records[0], self.expected_copyright)

    def parse_filename(self, filename):
        """Parse expected name attributes from filename."""

        name_nosp = self.family_name.replace(" ", "")
        condensed_name_nosp = self.condensed_family_name.replace(" ", "")
        family_names = "%s|%s" % (condensed_name_nosp, name_nosp)

        filename_match = noto_fonts.match_filename(filename, family_names)
        family, _, _, _, _, _, _, _, weight, slope, _ = filename_match.groups()

        if family == condensed_name_nosp:
            family = self.condensed_family_name
        else:  # family == name_nosp
            family = self.family_name
        if not weight:
            weight = "Regular"
        return family, weight, slope

    def build_style(self, weight, slope):
        """Build style (typographic subfamily) out of weight and slope."""

        style = weight
        if slope:
            if style == "Regular":
                style = "Italic"
            else:
                style += " " + slope
        return style

    def test_family_name(self):
        """Tests the family name."""

        for font_file, records in zip(self.font_files, self.names):

            family, weight, _ = self.parse_filename(font_file)

            # check that family name includes weight, if not regular or bold
            if weight not in ["Regular", "Bold"]:
                self.assertEqual(records[1], "%s %s" % (family, weight))

                # check typographic name, if present
                self.assertIn(16, records)
                self.assertEqual(records[16], family)

            # check that family name does not include weight, if regular or bold
            else:
                self.assertEqual(records[1], family)

    def test_subfamily_name(self):
        """Tests the subfamily name."""

        for font_file, records in zip(self.font_files, self.names):
            _, weight, slope = self.parse_filename(font_file)
            subfam = records[2]

            # check that subfamily is just a combination of bold and italic
            self.assertIn(subfam, ["Regular", "Bold", "Italic", "Bold Italic"])

            # check that subfamily weight/slope are consistent with filename
            bold = (weight == "Bold") or (
                self.mark_heavier_as_bold
                and noto_fonts.WEIGHTS[weight] > noto_fonts.WEIGHTS["Bold"]
            )
            self.assertEqual(bold, subfam.startswith("Bold"))
            self.assertEqual(slope == "Italic", subfam.endswith("Italic"))

            # check typographic name, if present
            if weight not in ["Regular", "Bold"]:
                self.assertIn(17, records)
                self.assertEqual(records[17], self.build_style(weight, slope))

    def test_unique_identifier_and_full_name(self):
        """Tests the unique identifier and full name."""
        for font_file, records in zip(self.font_files, self.names):
            family, weight, slope = self.parse_filename(font_file)
            style = self.build_style(weight, slope)
            expected_name = family
            if style != "Regular":
                expected_name += " " + style
            self.assertEqual(records[3], self.expected_unique_id(family, style))
            self.assertEqual(records[4], expected_name)
            self.assertFalse(records.has_key(18))

    def test_postscript_name(self):
        """Tests the postscript name."""
        for font_file, records in zip(self.font_files, self.names):
            family, weight, slope = self.parse_filename(font_file)
            style = self.build_style(weight, slope)
            expected_name = (family + "-" + style).replace(" ", "")
            self.assertEqual(records[6], expected_name)

    def test_postscript_name_for_spaces(self):
        """Tests that there are no spaces in PostScript names."""
        for records in self.names:
            self.assertFalse(" " in records[6])


class TestDigitWidths(FontTest):
    """Tests the width of digits."""

    def setUp(self):
        self.font_files, self.fonts = self.loaded_fonts
        self.digits = [
            "zero",
            "one",
            "two",
            "three",
            "four",
            "five",
            "six",
            "seven",
            "eight",
            "nine",
        ]

    def test_digit_widths(self):
        """Tests all decimal digits to make sure they have the same width."""
        for font in self.fonts:
            hmtx_table = font["hmtx"]
            widths = [hmtx_table[digit][0] for digit in self.digits]
            self.assertEqual(len(set(widths)), 1)

    def test_superscript_digits(self):
        """Tests that 'numr' features maps digits to Unicode superscripts."""
        ascii_digits = "0123456789"
        superscript_digits = "⁰¹²³⁴⁵⁶⁷⁸⁹"
        for font_file in self.font_files:
            numr_glyphs = layout.get_advances(
                ascii_digits, font_file, "--features=numr"
            )
            superscript_glyphs = layout.get_advances(superscript_digits, font_file)
            self.assertEqual(superscript_glyphs, numr_glyphs)


class TestCharacterCoverage(FontTest):
    """Tests character coverage."""

    def setUp(self):
        _, self.fonts = self.loaded_fonts

    def test_inclusion(self):
        """Tests for characters which should be included."""

        for font in self.fonts:
            charset = coverage.character_set(font)
            for char in self.include:
                self.assertIn(char, charset)

    def test_exclusion(self):
        """Tests that characters which should be excluded are absent."""

        for font in self.fonts:
            charset = coverage.character_set(font)
            for char in self.exclude:
                self.assertNotIn(char, charset)

    def test_format_4_cmap_preset(self):
        """Test that fonts contain a MS Unicode BMP encoded cmap, as recommended
        by the spec: https://www.microsoft.com/typography/otspec/cmap.htm
        """

        for font in self.fonts:
            found = False
            for cmap in font["cmap"].tables:
                if (cmap.format, cmap.platformID, cmap.platEncID) == (4, 3, 1):
                    found = True
            self.assertTrue(found)


class TestLigatures(FontTest):
    """Tests formation or lack of formation of ligatures.

    Must have the following attributes:
        active: List of (parameters, (sequence, sequence, ...)) tuples, for
            which each sequence should form a ligature when shaped with the
            given parameters.
        inactive: Similar list of tuples, for parameter-sequence cases which
            should not ligate.
    """

    def setUp(self):
        self.fontfiles, _ = self.loaded_fonts

    def test_active_ligatures(self):
        """Tests that some ligatures are formed by certain features."""

        self._run_ligature_test(self.active, True)

    def test_inactive_ligatures(self):
        """Tests that some ligatures are not formed by certain features."""

        self._run_ligature_test(self.inactive, False)

    def _run_ligature_test(self, sequences_with_params, active):
        for fontfile in self.fontfiles:
            for params, sequences in sequences_with_params:
                for sequence in sequences:
                    if params:
                        output = layout.get_glyphs(sequence, fontfile, params)
                    else:
                        output = layout.get_glyphs(sequence, fontfile)
                    if active:  # should ligate with parameters applied
                        expected = 1
                        err_msg = "%s not ligated in %s with parameters: %s"
                    else:  # should not ligate with parameters applied
                        expected = len(sequence)
                        err_msg = "%s ligated in %s with parameters: %s"
                    self.assertEqual(
                        len(output), expected, err_msg % (sequence, fontfile, params)
                    )


class TestFeatures(FontTest):
    """Tests typographic features."""

    def setUp(self):
        self.fontfiles, self.fonts = self.loaded_fonts

    def test_num_features(self):
        """Tests that lnum/onum/pnum/tnum features behave appropriately."""

        def run_test(
            glyph_order, glyph_set, fontfile, tags, proportional=False, oldstyle=False
        ):

            num = glyph_order[layout.get_glyphs("1", fontfile)[0]]
            styled = glyph_order[
                layout.get_glyphs("1", fontfile, "--features=" + ",".join(tags))[0]
            ]

            if not (proportional or oldstyle):
                self.assertEqual(num, styled)

            else:
                num, styled = glyph_set[num], glyph_set[styled]
                if proportional:
                    self.assertNotEqual(num.width, styled.width)
                if oldstyle:
                    self.assertNotEqual(num._glyph.yMax, styled._glyph.yMax)

        for fontfile, font in zip(self.fontfiles, self.fonts):
            go = font.getGlyphOrder()
            gs = font.getGlyphSet()

            # test individual tags
            run_test(go, gs, fontfile, ["lnum"])
            run_test(go, gs, fontfile, ["tnum"])
            run_test(go, gs, fontfile, ["pnum"], proportional=True)
            run_test(go, gs, fontfile, ["onum"], oldstyle=True)

            # test standard combinations
            run_test(go, gs, fontfile, ["lnum", "tnum"])
            run_test(go, gs, fontfile, ["lnum", "pnum"], proportional=True)
            run_test(go, gs, fontfile, ["onum", "tnum"], oldstyle=True)
            run_test(
                go, gs, fontfile, ["onum", "pnum"], oldstyle=True, proportional=True
            )

            # test that defaults take precedence
            run_test(go, gs, fontfile, ["lnum", "onum"])
            run_test(go, gs, fontfile, ["pnum", "tnum"])
            run_test(go, gs, fontfile, ["lnum", "onum", "tnum"])
            run_test(go, gs, fontfile, ["lnum", "onum", "pnum"], proportional=True)
            run_test(go, gs, fontfile, ["lnum", "pnum", "tnum"])
            run_test(go, gs, fontfile, ["onum", "pnum", "tnum"], oldstyle=True)
            run_test(go, gs, fontfile, ["lnum", "onum", "pnum", "tnum"])

    def run_sub_coverage_test(self, feature, reqs_path):
        """Tests that a substitution feature is supported for a required set."""

        with open(reqs_path) as reqs_file:
            reqs_list = []
            for line in reqs_file.readlines():
                input_cp, output_name = line[: line.index(" #")].split()
                reqs_list.append((unichr(int(input_cp, 16)), output_name))

        for fontfile, font in zip(self.fontfiles, self.fonts):
            glyph_order = font.getGlyphOrder()
            chars_with_no_sub = []
            for char, expected_name in reqs_list:
                sub = layout.get_glyphs(char, fontfile, "--features=%s" % feature)
                if glyph_order[sub[0]] != expected_name:
                    chars_with_no_sub.append(char)
            self.assertEqual(
                chars_with_no_sub,
                [],
                (
                    "%s feature is not applied correctly to '%s'"
                    % (feature, "".join(chars_with_no_sub).encode("UTF-8"))
                ),
            )

    def test_smcp_coverage(self):
        """Tests that smcp is supported for a required set."""

        self.run_sub_coverage_test("smcp", self.smcp_reqs_path)

    def test_c2sc_coverage(self):
        """Tests that c2sc is supported for a required set."""

        self.run_sub_coverage_test("c2sc", self.c2sc_reqs_path)

    def test_unic_coverage(self):
        """Tests that unic is supported for a required set."""

        self.run_sub_coverage_test("unic", self.unic_reqs_path)


class TestVerticalMetrics(FontTest):
    """Test the vertical metrics of fonts."""

    def setUp(self):
        self.font_files, self.fonts = self.loaded_fonts

    def test_ymin_ymax(self):
        """Tests yMin and yMax to be equal to expected values."""

        for font in self.fonts:
            head_table = font["head"]
            self.assertEqual(head_table.yMin, self.expected_head_yMin)
            self.assertEqual(head_table.yMax, self.expected_head_yMax)

    def test_glyphs_ymin_ymax(self):
        """Tests yMin and yMax of all glyphs to not go outside the range."""

        out_of_bounds = []
        for font_file, font in zip(self.font_files, self.fonts):
            glyf_table = font["glyf"]
            for glyph_name in glyf_table.glyphOrder:
                try:
                    y_min = glyf_table[glyph_name].yMin
                    y_max = glyf_table[glyph_name].yMax
                except AttributeError:
                    continue

                if y_min < self.expected_head_yMin or y_max > self.expected_head_yMax:
                    out_of_bounds.append((font_file, glyph_name, y_min, y_max))

        self.assertFalse(
            out_of_bounds,
            "Vertical metrics exceed the acceptable range "
            "yMin=%d, yMax=%d:%s"
            % (
                self.expected_head_yMin,
                self.expected_head_yMax,
                "".join("\n%s %s yMin=%d yMax=%d" % g for g in out_of_bounds),
            ),
        )

    def test_hhea_table_metrics(self):
        """Tests ascent, descent, and lineGap to be equal to expected values."""

        for font in self.fonts:
            hhea_table = font["hhea"]
            self.assertEqual(hhea_table.descent, self.expected_hhea_descent)
            self.assertEqual(hhea_table.ascent, self.expected_hhea_ascent)
            self.assertEqual(hhea_table.lineGap, self.expected_hhea_lineGap)

    def test_os2_metrics(self):
        """Tests OS/2 vertical metrics to be equal to expected values."""

        for font in self.fonts:
            os2_table = font["OS/2"]
            self.assertEqual(os2_table.sTypoDescender, self.expected_os2_sTypoDescender)
            self.assertEqual(os2_table.sTypoAscender, self.expected_os2_sTypoAscender)
            self.assertEqual(os2_table.sTypoLineGap, self.expected_os2_sTypoLineGap)
            self.assertEqual(os2_table.usWinDescent, self.expected_os2_usWinDescent)
            self.assertEqual(os2_table.usWinAscent, self.expected_os2_usWinAscent)


class TestHints(FontTest):
    """Tests hints."""

    def setUp(self):
        self.fontfiles, self.fonts = self.loaded_fonts

    def test_existance_of_hints(self):
        """Tests all glyphs and makes sure non-composite ones have hints."""
        missing_hints = []
        for font in self.fonts:
            glyf_table = font["glyf"]
            for glyph_name in font.getGlyphOrder():
                glyph = glyf_table[glyph_name]
                if glyph.numberOfContours <= 0:  # composite or empty glyph
                    continue
                if len(glyph.program.bytecode) <= 0:
                    missing_hints.append((glyph_name, font_data.font_name(font)))

        self.assertTrue(missing_hints == [])

    def test_height_of_lowercase_o(self):
        """Tests the height of the lowercase o in low resolutions."""
        for fontfile in self.fontfiles:
            for size in range(8, 30):  # Kind of arbitrary
                o_height = get_rendered_char_height(fontfile, size, "o")
                n_height = get_rendered_char_height(fontfile, size, "n")
                self.assertEqual(o_height, n_height)


class TestGlyphBounds(FontTest):
    """Tests that certain glyphs do or don't go outside of given bounds.

    Glyphs must be designated with two class attribute lists "should_exceed" and
    "should_not_exceed", both containing tuples each with the following four
    elements:
     - names: a list containing glyph names as strings.
     - bounds: a list (xmin, ymin, xmax, ymax), values can be None.
     - multiplier: a value which, when multiplied by a font's weight class
         value minus 400 (regular), gives a bound adjustment for that font.
         Adjustment is outward (e.g. left for xMin and right for xMax) when
         positive and inward when negative.
     - errmsg: a string to print alongside the standard error message.
    """

    def setUp(self):
        self.fontfiles, self.fonts = self.loaded_fonts

    def test_should_not_exceed(self):
        for names, bounds, multiplier, errmsg in self.should_not_exceed:
            self._run_test(names, bounds, multiplier, errmsg, False)

    def test_should_exceed(self):
        for names, bounds, multiplier, errmsg in self.should_exceed:
            self._run_test(names, bounds, multiplier, errmsg, True)

    def _run_test(self, names, bounds, multiplier, errmsg, should_exceed):
        errmsg_format = "%%s's %%s %%s %s %%d (%s)" % (
            "does not exceed" if should_exceed else "exceeds",
            errmsg,
        )

        for fontfile, font in zip(self.fontfiles, self.fonts):
            glyph_set = font.getGlyphSet()

            # offset applied to each bound, allowing for per-weight bounds
            offset = (font["OS/2"].usWeightClass - 400) * multiplier
            xmin, ymin, xmax, ymax = bounds
            for name in names:
                glyph = glyph_set[name]._glyph
                for attr, expected in (
                    ("xMin", xmin),
                    ("yMin", ymin),
                    ("xMax", xmax),
                    ("yMax", ymax),
                ):
                    if expected is None:
                        continue
                    is_min = attr.endswith("Min")
                    actual = getattr(glyph, attr)

                    # try to always apply offset away (if positive) or towards
                    # (if negative) a central value
                    expected = expected + (-offset if is_min else offset)
                    exceeds = actual < expected if is_min else actual > expected
                    self.assertEqual(
                        exceeds,
                        should_exceed,
                        errmsg_format % (fontfile, name, attr, expected),
                    )


class TestGlyphAreas(unittest.TestCase):
    """Tests that glyph areas between weights have the right ratios.

    Must have the following attributes:
        master/instance_glyph_sets: Tuples each containing a list of font
            filenames and a list of glyph sets corresponding to those fonts.
        master/instance_glyphs_to_test: Lists of glyph names.
        master/instance_weights_to_test: Lists of weights as strings, should
            have exactly two master weights and >1 instance weight.
        exclude: List containing strings which if found in a font filename will
            exclude that font from testing, e.g. "Condensed", "Italic".
        whitelist: List containing names of glyphs to not test.

    Glyph sets are used instead of font objects so that either TTFs or UFOs can
    be tested. `exclude` should be used carefully so that only one
    master/instance per weight is included from the given glyph sets.
    """

    def setUp(self):
        """Determine which glyphs are intentionally unchanged."""

        self.unchanged = set()
        master_a, master_b = self.getGlyphSets(
            self.master_glyph_sets, self.master_weights_to_test
        )

        pen_a = GlyphAreaPen(master_a)
        pen_b = GlyphAreaPen(master_b)
        for name in self.master_glyphs_to_test:
            if name in self.whitelist:
                continue
            master_a[name].draw(pen_a)
            area_a = pen_a.pop()
            master_b[name].draw(pen_b)
            area_b = pen_b.pop()
            if area_a == area_b:
                if area_a:
                    self.unchanged.add(name)
            else:
                assert area_a and area_b

    def getGlyphSets(self, glyph_sets, weights):
        """Filter glyph sets to only those with certain weights."""

        glyph_set_map = {
            noto_fonts.parse_weight(font_file): glyph_set
            for font_file, glyph_set in zip(*glyph_sets)
            if all(style not in font_file for style in self.exclude)
        }
        return [glyph_set_map[w] for w in weights]

    def test_output(self):
        """Test that empty or intentionally unchanged glyphs are unchanged, and
        everything else is changed.
        """

        glyph_sets = self.getGlyphSets(
            self.instance_glyph_sets, self.instance_weights_to_test
        )

        standard = glyph_sets[0]
        pen = GlyphAreaPen(standard)
        areas = {}
        for name in self.instance_glyphs_to_test:
            standard[name].draw(pen)
            areas[name] = pen.pop()

        errors = []
        for other in glyph_sets[1:]:
            other_pen = GlyphAreaPen(other)
            for name, area in areas.items():
                if name in self.whitelist:
                    continue
                other[name].draw(other_pen)
                other_area = other_pen.pop()
                if name in self.unchanged or not area:
                    if area != other_area:
                        errors.append(
                            "%s has changed, but should not have: %s vs. %s."
                            % (name, area, other_area)
                        )
                else:
                    if area == other_area:
                        errors.append(
                            "%s has not changed, but should have: %s vs. %s."
                            % (name, area, other_area)
                        )
        self.assertFalse(errors, "\n".join([""] + errors))


class TestSpacingMarks(FontTest):
    """Tests that spacing marks are indeed spacing."""

    def setUp(self):
        self.font_files, _ = self.loaded_fonts
        charset = coverage.character_set(self.font_files[0])
        self.marks_to_test = [
            char for char in charset if unicode_data.category(char) in ["Lm", "Sk"]
        ]
        self.advance_cache = {}

    def test_individual_spacing_marks(self):
        """Tests that spacing marks are spacing by themselves."""
        for font in self.font_files:
            print("Testing %s for stand-alone spacing marks..." % font)
            for mark in self.marks_to_test:
                mark = unichr(mark)
                advances = layout.get_advances(mark, font)
                assert len(advances) == 1
                self.assertNotEqual(advances[0], 0)

    def test_spacing_marks_in_combination(self):
        """Tests that spacing marks do not combine with base letters."""
        for font in self.font_files:
            print("Testing %s for spacing marks in combination..." % font)
            for base_letter in (
                "A\u00C6BCDEFGHIJKLMNO\u00D8\u01A0PRST"
                "U\u01AFVWXYZ"
                "a\u00E6bcdefghi\u0131j\u0237klmn"
                "o\u00F8\u01A1prs\u017Ftu\u01B0vwxyz"
                "\u03D2"
            ):
                print("Testing %s combinations" % base_letter)
                for mark in self.marks_to_test:
                    if mark == 0x02DE:
                        # Skip rhotic hook, as it's perhaps OK for it to form
                        # ligatures
                        continue
                    mark = unichr(mark)
                    advances = layout.get_advances(base_letter + mark, font)
                    self.assertEqual(
                        len(advances),
                        2,
                        "The sequence <%04X, %04X> combines, "
                        "but it should not" % (ord(base_letter), ord(mark)),
                    )


class TestSoftDottedChars(FontTest):
    """Tests that soft-dotted characters lose their dots."""

    def setUp(self):
        self.font_files, _ = self.loaded_fonts
        charset = coverage.character_set(self.font_files[0])
        self.marks_to_test = [
            char for char in charset if unicode_data.combining(char) == 230
        ]
        self.advance_cache = {}

    def test_combinations(self):
        """Tests that soft-dotted characters lose their dots when combined."""

        for font in self.font_files:
            print("Testing %s for soft-dotted combinations..." % font)

            # TODO: replace the following list with actual derivation based on
            # Unicode's soft-dotted property
            for base_letter in (
                "ij\u012F\u0249\u0268\u029D\u02B2\u03F3\u0456"
                "\u0458\u1D62\u1D96\u1DA4\u1DA8\u1E2D\u1ECB"
                "\u2071\u2C7C"
            ):
                print("Testing %s combinations" % base_letter.encode("UTF-8"))
                for mark in self.marks_to_test:
                    mark = unichr(mark)
                    letter_only = layout.get_glyphs(base_letter, font)
                    combination = layout.get_glyphs(base_letter + mark, font)
                    self.assertNotEqual(
                        combination[0],
                        letter_only[0],
                        "The sequence <%04X, %04X> doesn't lose its dot, "
                        "but it should" % (ord(base_letter), ord(mark)),
                    )
