d PASS, "Looks good!"


@check(
    id = 'com.google.fonts/check/kerning_for_non_ligated_sequences',
    conditions = ['ligatures',
                  'has_kerning_info'],
    rationale = """
        Fonts with ligatures should have kerning on the corresponding non-ligated
        sequences for text where ligatures aren't used
        (eg https://github.com/impallari/Raleway/issues/14).
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/issues/1145'
)
def com_google_fonts_check_kerning_for_non_ligated_sequences(ttFont, ligatures, has_kerning_info):
    """Is there kerning info for non-ligated sequences?"""

    def look_for_nonligated_kern_info(table):
        for pairpos in table.SubTable:
            for i, glyph in enumerate(pairpos.Coverage.glyphs):
                if not hasattr(pairpos, 'PairSet'):
                    continue
                for pairvalue in pairpos.PairSet[i].PairValueRecord:
                    kern_pair = (glyph, pairvalue.SecondGlyph)
                    if kern_pair in ligature_pairs:
                        ligature_pairs.remove(kern_pair)

    def ligatures_str(pairs):
        result = [f"\t- {first} + {second}" for first, second in pairs]
        return "\n".join(result)

    if ligatures == -1:
        yield FAIL,\
              Message("malformed",
                      "Failed to lookup ligatures."
                      " This font file seems to be malformed."
                      " For more info, read:"
                      " https://github.com/googlefonts/fontbakery/issues/1596")
    else:
        ligature_pairs = []
        for first, comp in ligatures.items():
            for components in comp:
                while components:
                    pair = (first, components[0])
                    if pair not in ligature_pairs:
                        ligature_pairs.append(pair)
                    first = components[0]
                    components.pop(0)

        for record in ttFont["GSUB"].table.FeatureList.FeatureRecord:
            if record.FeatureTag == 'kern':
                for index in record.Feature.LookupListIndex:
                    lookup = ttFont["GSUB"].table.LookupList.Lookup[index]
                    look_for_nonligated_kern_info(lookup)

        if ligature_pairs:
            yield WARN,\
                  Message("lacks-kern-info",
                          f"GPOS table lacks kerning info for the following"
                          f" non-ligated sequences:\n"
                          f"{ligatures_str(ligature_pairs)}\n\n  ")
        else:
            yield PASS, ("GPOS table provides kerning info for "
                         "all non-ligated sequences.")


@check(
    id = 'com.google.fonts/check/name/family_and_style_max_length',
    rationale = """
        According to a GlyphsApp tutorial [1], in order to make sure all versions of
        Windows recognize it as a valid font file, we must make sure that the
        concatenated length of the familyname (NameID.FONT_FAMILY_NAME) and
        style (NameID.FONT_SUBFAMILY_NAME) strings in the name table do not
        exceed 20 characters.

        After discussing the problem in more detail at FontBakery issue #2179 [2] we
        decided that allowing up to 27 chars would still be on the safe side, though.

        [1] https://glyphsapp.com/tutorials/multiple-masters-part-3-setting-up-instances
        [2] https://github.com/googlefonts/fontbakery/issues/2179
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/issues/1488',
    misc_metadata = {
        # Somebody with access to Windows should make some experiments
        # and confirm that this is really the case.
        'affects': [('Windows', 'unspecified')],
    }
)
def com_google_fonts_check_name_family_and_style_max_length(ttFont):
    """Combined length of family and style must not exceed 27 characters."""
    from fontbakery.utils import (get_name_entries,
                                  get_name_entry_strings)
    failed = False
    for familyname in get_name_entries(ttFont,
                                       NameID.FONT_FAMILY_NAME):
        # we'll only match family/style name entries with the same platform ID:
        plat = familyname.platformID
        familyname_str = familyname.string.decode(familyname.getEncoding())
        for stylename_str in get_name_entry_strings(ttFont,
                                                    NameID.FONT_SUBFAMILY_NAME,
                                                    platformID=plat):
            if len(familyname_str + stylename_str) > 27:
                failed = True
                yield WARN,\
                      Message("too-long",
                              f"The combined length of family and style"
                              f" exceeds 27 chars in the following"
                              f" '{PlatformID(plat).name}' entries:\n"
                              f" FONT_FAMILY_NAME = '{familyname_str}' /"
                              f" SUBFAMILY_NAME = '{stylename_str}'\n"
                              f"\n"
                              f"Please take a look at the conversation at"
                              f" https://github.com/googlefonts/fontbakery/issues/2179"
                              f" in order to understand the reasoning behind these"
                              f" name table records max-length criteria.")
    if not failed:
        yield PASS, "All name entries are good."


@disable
@check(
    id = 'com.google.fonts/check/glyphs_file/name/family_and_style_max_length',
)
def com_google_fonts_check_glyphs_file_name_family_and_style_max_length(glyphsFile):
    """Combined length of family and style must not exceed 27 characters."""

    too_long = []
    for instance in glyphsFile.instances:
        if len(instance.fullName) > 27:
            too_long.append(instance.fullName)

    if too_long:
        too_long_list = "\n  - " + "\n  - ".join(too_long)
        yield WARN,\
              Message("too-long",
                      f"The fullName length exceeds 27 chars in the"
                      f" following entries:\n"
                      f"{too_long_list}\n"
                      f"\n"
                      f"Please take a look at the conversation at"
                      f" https://github.com/googlefonts/fontbakery/issues/2179"
                      f" in order to understand the reasoning behind these"
                      f" name table records max-length criteria.")
    else:
        yield PASS, "ok"


@check(
    id = 'com.google.fonts/check/name/line_breaks',
    rationale = """
        There are some entries on the name table that may include more than one line
        of text. The Google Fonts team, though, prefers to keep the name table entries
        short and simple without line breaks.

        For instance, some designers like to include the full text of a font license in
        the "copyright notice" entry, but for the GFonts collection this entry should
        only mention year, author and other basic info in a manner enforced by
        com.google.fonts/check/font_copyright
    """,
    proposal = 'legacy:check/057'
)
def com_google_fonts_check_name_line_breaks(ttFont):
    """Name table entries should not contain line-breaks."""
    failed = False
    for name in ttFont["name"].names:
        string = name.string.decode(name.getEncoding())
        if "\n" in string:
            failed = True
            yield FAIL,\
                  Message("line-break",
                          f"Name entry {NameID(name.nameID).name}"
                          f" on platform {PlatformID(name.platformID).name}"
                          f" contains a line-break.")
    if not failed:
        yield PASS, ("Name table entries are all single-line"
                     " (no line-breaks found).")


@check(
    id = 'com.google.fonts/check/name/rfn',
    rationale = """
        Some designers adopt the "Reserved Font Name" clause of the OFL license. This
        means that the original author reserves the rights to the family name and other
        people can only distribute modified versions using a different family name.

        Google Fonts published updates to the fonts in the collection in order to fix
        issues and/or implement further improvements to the fonts. It is important to
        keep the family name so that users of the webfonts can benefit from the updates.
        Since it would forbid such usage scenario, all families in the GFonts collection
        are required to not adopt the RFN clause.

        This check ensures "Reserved Font Name" is not mentioned in the name table.
    """,
    conditions = ['not rfn_exception'],
    proposal = 'https://github.com/googlefonts/fontbakery/issues/1380'
)
def com_google_fonts_check_name_rfn(ttFont):
    """Name table strings must not contain the string 'Reserved Font Name'."""
    failed = False
    for entry in ttFont["name"].names:
        string = entry.toUnicode()
        if "This license is copied below, and is also available with a FAQ" in string:
            # This is the OFL text in a name table entry.
            # It contains the term 'Reserved Font Name' in one of its clauses,
            # so we will ignore this here.
            continue

        if "reserved font name" in string.lower():
            yield FAIL,\
                  Message("rfn",
                          f'Name table entry ("{string}")'
                          f' contains "Reserved Font Name".'
                          f' This is an error except in a few specific rare cases.')
            failed = True
    if not failed:
        yield PASS, 'None of the name table strings contain "Reserved Font Name".'


@check(
    id = 'com.google.fonts/check/family/control_chars',
    conditions = ['are_ttf'],
    rationale = """
        Use of some unacceptable control characters in the U+0000 - U+001F range can
        lead to rendering issues on some platforms.

        Acceptable control characters are defined as .null (U+0000) and
        CR (U+000D) for this test.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/pull/2430'
)
def com_google_fonts_check_family_control_chars(ttFonts):
    """Does font file include unacceptable control character glyphs?"""
    # list of unacceptable control character glyph names
    # definition includes the entire control character Unicode block except:
    #    - .null (U+0000)
    #    - CR (U+000D)
    unacceptable_cc_list = [
        "uni0001",
        "uni0002",
        "uni0003",
        "uni0004",
        "uni0005",
        "uni0006",
        "uni0007",
        "uni0008",
        "uni0009",
        "uni000A",
        "uni000B",
        "uni000C",
        "uni000E",
        "uni000F",
        "uni0010",
        "uni0011",
        "uni0012",
        "uni0013",
        "uni0014",
        "uni0015",
        "uni0016",
        "uni0017",
        "uni0018",
        "uni0019",
        "uni001A",
        "uni001B",
        "uni001C",
        "uni001D",
        "uni001E",
        "uni001F"
    ]

    # a dict with key:value of font path that failed check : list of unacceptable glyph names
    failed_font_dict = {}

    for ttFont in ttFonts:
        font_failed = False
        unacceptable_glyphs_in_set = []  # a list of unacceptable glyph names identified
        glyph_name_set = set(ttFont["glyf"].glyphs.keys())
        fontname = ttFont.reader.file.name

        for unacceptable_glyph_name in unacceptable_cc_list:
            if unacceptable_glyph_name in glyph_name_set:
                font_failed = True
                unacceptable_glyphs_in_set.append(unacceptable_glyph_name)

        if font_failed:
            failed_font_dict[fontname] = unacceptable_glyphs_in_set

    if len(failed_font_dict) > 0:
        msg_unacceptable = "The following unacceptable control characters were identified:\n"
        for fnt in failed_font_dict.keys():
            msg_unacceptable += f" {fnt}: {', '.join(failed_font_dict[fnt])}\n"
        yield FAIL,\
              Message("unacceptable",
                      f"{msg_unacceptable}")
    else:
        yield PASS, ("Unacceptable control characters were not identified.")


@check(
    id = 'com.google.fonts/check/family/italics_have_roman_counterparts',
    rationale = """
        For each font family on Google Fonts, every Italic style must have
        a Roman sibling.

        This kind of problem was first observed at [1] where the Bold style was
        missing but BoldItalic was included.

        [1] https://github.com/google/fonts/pull/1482
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/issues/1733'
)
def com_google_fonts_check_family_italics_have_roman_counterparts(fonts, config):
    """Ensure Italic styles have Roman counterparts."""

    italics = [f for f in fonts if 'Italic' in f]
    missing_roman = []
    for italic in italics:
        if '-' not in os.path.basename(italic) \
            or len(os.path.basename(italic).split('-')[-1].split('.')) != 2:
            yield WARN,\
                  Message('bad-filename',
                          f"Filename seems to be incorrect: '{italic}'")

        style = os.path.basename(italic).split('-')[-1].split('.')[0]
        is_varfont = '[' in style

        # to remove the axes syntax used on variable-font filenames:
        if is_varfont:
            style = style.split('[')[0]

        if style == 'Italic':
            if is_varfont:
                # "Familyname-Italic[wght,wdth].ttf" => "Familyname[wght,wdth].ttf"
                roman_counterpart = italic.replace('-Italic', '')
            else:
                # "Familyname-Italic.ttf" => "Familyname-Regular.ttf"
                roman_counterpart = italic.replace('Italic', 'Regular')
        else:
            # "Familyname-BoldItalic[wght,wdth].ttf" => "Familyname-Bold[wght,wdth].ttf"
            roman_counterpart = italic.replace('Italic', '')

        if roman_counterpart not in fonts:
            missing_roman.append(italic)

    if missing_roman:
        from fontbakery.utils import pretty_print_list
        missing_roman = pretty_print_list(config,
                                          missing_roman)
        yield FAIL,\
              Message('missing-roman',
                      f"Italics missing a Roman counterpart: {missing_roman}")
    else:
        yield PASS, "OK"


@check(
    id = 'com.google.fonts/check/repo/dirname_matches_nameid_1',
    conditions = ['gfonts_repo_structure',
                  'not is_variable_font'],
    proposal = 'https://github.com/googlefonts/fontbakery/issues/2302'
)
def com_google_fonts_check_repo_dirname_match_nameid_1(fonts,
                                                       gfonts_repo_structure):
    """Directory name in GFonts repo structure must
       match NameID 1 of the regular."""
    from fontTools.ttLib import TTFont
    from fontbakery.utils import (get_name_entry_strings,
                                  get_absolute_path,
                                  get_regular)
    regular = get_regular(fonts)
    if not regular:
        yield FAIL,\
              Message("lacks-regular",
                      "The font seems to lack a regular."
                      " If family consists of a single-weight non-Regular style only,"
                      " consider the Google Fonts specs for this case:"
                      " https://github.com/googlefonts/gf-docs/tree/main/Spec#single-weight-families")
        return

    entry = get_name_entry_strings(TTFont(regular), NameID.FONT_FAMILY_NAME)[0]
    expected = entry.lower()
    expected = "".join(expected.split(' '))
    expected = "".join(expected.split('-'))

    license, familypath, filename = get_absolute_path(regular).split(os.path.sep)[-3:]
    if familypath == expected:
        yield PASS, "OK"
    else:
        yield FAIL,\
              Message("mismatch",
                      f"Family name on the name table ('{entry}') does not match"
                      f" directory name in the repo structure ('{familypath}')."
                      f" Expected '{expected}'.")


@check(
    id = 'com.google.fonts/check/repo/vf_has_static_fonts',
    conditions = ['family_directory',
                  'gfonts_repo_structure',
                  'is_variable_font'],
    rationale = """
        Variable font family directories kept in the google/fonts git repo may include
        a static/ subdir containing static fonts.
        
        These files are meant to be served for users that still lack support for
        variable fonts in their web browsers.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/issues/2654'
)
def com_google_fonts_check_repo_vf_has_static_fonts(family_directory):
    """A static fonts directory with at least two fonts must accompany variable fonts"""
    static_dir = os.path.join(family_directory, 'static')
    if os.path.exists(static_dir):
        has_static_fonts = any([f for f in os.listdir(static_dir)
                                if f.endswith('.ttf')])
        if has_static_fonts:
            yield PASS, 'OK'
        else:
            yield FAIL,\
                  Message("empty",
                          'There is a "static" dir but it is empty.'
                          ' Either add static fonts or delete the directory.')
    else:
        yield WARN,\
              Message("missing",
                      'Please consider adding a subdirectory called "static/"'
                      ' and including in it static font files.')


@check(
    id = 'com.google.fonts/check/repo/fb_report',
    conditions = ['family_directory'],
    rationale = """
        A FontBakery report is ephemeral and so should be used for posting issues on a
        bug-tracker instead of being hosted in the font project repository.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/issues/2888'
)
def com_google_fonts_check_repo_fb_report(family_directory):
    """A font repository should not include fontbakery report files"""
    from fontbakery.utils import filenames_ending_in

    has_report_files = any([f for f in filenames_ending_in(".json", family_directory)
                            if '"result"' in open(f).read()])
    if not has_report_files:
        yield PASS, 'OK'
    else:
        yield WARN,\
              Message("fb-report",
                      "There's no need to keep a copy of Font Bakery reports in the"
                      " repository, since they are ephemeral; Font Bakery has"
                      " a 'github markdown' output mode to make it easy to file"
                      " reports as issues.")


@check(
    id = "com.google.fonts/check/repo/upstream_yaml_has_required_fields",
    rationale = """
        If a family has been pushed using the gftools packager, we must check that all
        the required fields in the upstream.yaml file have been populated.
    """,
    conditions = ["upstream_yaml"],
    severity = 10,
    proposal = 'https://github.com/googlefonts/fontbakery/issues/3338'
)
def com_google_fonts_check_repo_upstream_yaml_has_required_fields(upstream_yaml):
    """Check upstream.yaml file contains all required fields"""
    required_fields = set(["branch", "files"])
    upstream_fields = set(upstream_yaml.keys())

    missing_fields = required_fields - upstream_fields
    if missing_fields:
        yield FAIL,\
              Message('missing-fields',
                      f"The upstream.yaml file is missing the following fields:"
                      f" {list(missing_fields)}")
    else:
        yield PASS, "The upstream.yaml file contains all necessary fields"


@check(
    id = 'com.google.fonts/check/repo/zip_files',
    conditions = ['family_directory'],
    rationale = """
        Sometimes people check in ZIPs into their font project repositories. While we
        accept the practice of checking in binaries, we believe that a ZIP is a step
        too far ;)

        Note: a source purist position is that only source files and build scripts
        should be checked in.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/issues/2903'
)
def com_google_fonts_check_repo_zip_files(family_directory, config):
    """A font repository should not include ZIP files"""
    from fontbakery.utils import (filenames_ending_in,
                                  pretty_print_list)

    COMMON_ZIP_EXTENSIONS = [".zip", ".7z", ".rar"]
    zip_files = []
    for ext in COMMON_ZIP_EXTENSIONS:
        zip_files.extend(filenames_ending_in(ext, family_directory))

    if not zip_files:
        yield PASS, 'OK'
    else:
        files_list = pretty_print_list(config,
                                       zip_files,
                                       sep='\n\t* ')
        yield FAIL,\
              Message("zip-files",
                      f"Please do not host ZIP files on the project repository."
                      f" These files were detected:\n"
                      f"\t* {files_list}")


@check(
    id = 'com.google.fonts/check/vertical_metrics_regressions',
    conditions = ['regular_remote_style',
                  'not is_cjk_font'],
    rationale = """
        If the family already exists on Google Fonts, we need to ensure that the
        checked family's vertical metrics are similar. This check will test the
        following schema which was outlined in Fontbakery issue #1162 [1]:

        - The family should visually have the same vertical metrics as the Regular
          style hosted on Google Fonts.
        
        - If the family on Google Fonts has differing hhea and typo metrics, the family
          being checked should use the typo metrics for both the hhea and typo entries.
        
        - If the family on Google Fonts has use typo metrics not enabled and the family
          being checked has it enabled, the hhea and typo metrics should use the family
          on Google Fonts winAscent and winDescent values.
        
        - If the upms differ, the values must be scaled so the visual appearance is
          the same.

        [1] https://github.com/googlefonts/fontbakery/issues/1162
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/issues/1162'
)
def com_google_fonts_check_vertical_metrics_regressions(regular_ttFont, regular_remote_style):
    """Check if the vertical metrics of a family are similar to the same
    family hosted on Google Fonts."""
    import math
    from .shared_conditions import (is_variable_font,
                                    get_instance_axis_value,
                                    typo_metrics_enabled)

    gf_ttFont = regular_remote_style
    ttFont = regular_ttFont

    upm_scale = ttFont['head'].unitsPerEm / gf_ttFont['head'].unitsPerEm

    gf_has_typo_metrics = typo_metrics_enabled(gf_ttFont)
    ttFont_has_typo_metrics = typo_metrics_enabled(ttFont)

    failed = False
    if gf_has_typo_metrics:
        if not ttFont_has_typo_metrics:
            failed = True
            yield FAIL, Message("bad-fsselection-bit7",
                                "fsSelection bit 7 needs to be enabled because "
                                "the family on Google Fonts has it enabled.")
            # faux enable it so we can see which metrics also need changing
            ttFont_has_typo_metrics = True
        expected_ascender = math.ceil(gf_ttFont['OS/2'].sTypoAscender * upm_scale)
        expected_descender = math.ceil(gf_ttFont['OS/2'].sTypoDescender * upm_scale)
    else:
        # if the win metrics have changed, the updated fonts must have bit 7
        # enabled
        if (math.ceil(gf_ttFont['OS/2'].usWinAscent * upm_scale),
            math.ceil(gf_ttFont['OS/2'].usWinDescent * upm_scale)) != \
           (math.ceil(ttFont['OS/2'].usWinAscent),
            math.ceil(ttFont['OS/2'].usWinDescent)):
               if not ttFont_has_typo_metrics:
                   failed = True
                   yield FAIL, Message("bad-fsselection-bit7",
                                       "fsSelection bit 7 needs to be enabled "
                                       "because the win metrics differ from "
                                       "the family on Google Fonts.")
                   ttFont_has_typo_metrics = True
        expected_ascender = math.ceil(gf_ttFont["OS/2"].usWinAscent * upm_scale)
        expected_descender = -math.ceil(gf_ttFont["OS/2"].usWinDescent * upm_scale)

    full_font_name = ttFont['name'].getName(
        NameID.FULL_FONT_NAME,
        PlatformID.WINDOWS,
        UnicodeEncodingID.UNICODE_1_1,
        WindowsLanguageID.ENGLISH_USA
    ).toUnicode()
    typo_ascender = ttFont['OS/2'].sTypoAscender
    typo_descender = ttFont['OS/2'].sTypoDescender
    hhea_ascender = ttFont['hhea'].ascent
    hhea_descender = ttFont['hhea'].descent

    if typo_ascender != expected_ascender:
        failed = True
        yield FAIL,\
              Message("bad-typo-ascender",
                      f"{full_font_name}:"
                      f" OS/2 sTypoAscender is {typo_ascender}"
                      f" when it should be {expected_ascender}")

    if typo_descender != expected_descender:
        failed = True
        yield FAIL,\
              Message("bad-typo-descender",
                      f"{full_font_name}:"
                      f" OS/2 sTypoDescender is {typo_descender}"
                      f" when it should be {expected_descender}")

    if hhea_ascender != expected_ascender:
        failed = True
        yield FAIL,\
              Message("bad-hhea-ascender",
                      f"{full_font_name}:"
                      f" hhea Ascender is {hhea_ascender}"
                      f" when it should be {expected_ascender}")

    if hhea_descender != expected_descender:
        failed = True
        yield FAIL,\
              Message("bad-hhea-descender",
                      f"{full_font_name}:"
                      f" hhea Descender is {hhea_descender}"
                      f" when it should be {expected_descender}")
    if not failed:
        yield PASS, "Vertical metrics have not regressed."


@check(
    id = 'com.google.fonts/check/cjk_vertical_metrics',
    conditions = ['is_cjk_font',
                  'not remote_styles'],
    rationale = """
        CJK fonts have different vertical metrics when compared to Latin fonts.
        We follow the schema developed by dr Ken Lunde for Source Han Sans and
        the Noto CJK fonts.

        Our documentation includes further information:
        https://github.com/googlefonts/gf-docs/tree/main/Spec#cjk-vertical-metrics
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/pull/2797'
)
def com_google_fonts_check_cjk_vertical_metrics(ttFont):
    """Check font follows the Google Fonts CJK vertical metric schema"""
    from .shared_conditions import is_cjk_font, typo_metrics_enabled
    filename = os.path.basename(ttFont.reader.file.name)

    # Check necessary tables are present.
    missing_tables = False
    required = ["OS/2", "hhea", "head"]
    for key in required:
        if key not in ttFont:
            missing_tables = True
            yield FAIL,\
                  Message(f'lacks-{key}',
                          f"{filename} lacks a '{key}' table.")

    if missing_tables:
        return

    font_upm = ttFont['head'].unitsPerEm
    font_metrics = {
        'OS/2.sTypoAscender': ttFont['OS/2'].sTypoAscender,
        'OS/2.sTypoDescender': ttFont['OS/2'].sTypoDescender,
        'OS/2.sTypoLineGap': ttFont['OS/2'].sTypoLineGap,
        'hhea.ascent': ttFont['hhea'].ascent,
        'hhea.descent': ttFont['hhea'].descent,
        'hhea.lineGap': ttFont['hhea'].lineGap,
        'OS/2.usWinAscent': ttFont['OS/2'].usWinAscent,
        'OS/2.usWinDescent': ttFont['OS/2'].usWinDescent
    }
    expected_metrics = {
        'OS/2.sTypoAscender': round(font_upm * 0.88),
        'OS/2.sTypoDescender': round(font_upm * -0.12),
        'OS/2.sTypoLineGap': 0,
        'hhea.lineGap': 0,
    }

    failed = False
    warn = False

    # Check fsSelection bit 7 is not enabled
    if typo_metrics_enabled(ttFont):
        failed = True
        yield FAIL,\
              Message('bad-fselection-bit7',
                      'OS/2 fsSelection bit 7 must be disabled')

    # Check typo metrics and hhea lineGap match our expected values
    for k in expected_metrics:
        if font_metrics[k] != expected_metrics[k]:
            failed = True
            yield FAIL,\
                  Message(f'bad-{k}',
                          f'{k} is "{font_metrics[k]}" it should be {expected_metrics[k]}')

    # Check hhea and win values match
    if font_metrics['hhea.ascent'] != font_metrics['OS/2.usWinAscent']:
        failed = True
        yield FAIL,\
              Message('ascent-mismatch',
                      'hhea.ascent must match OS/2.usWinAscent')

    if abs(font_metrics['hhea.descent']) != font_metrics['OS/2.usWinDescent']:
        failed = True
        yield FAIL,\
              Message('descent-mismatch',
                      'hhea.descent must match absolute value of OS/2.usWinDescent')

    # Check the sum of the hhea metrics is between 1.1-1.5x of the font's upm
    hhea_sum = (font_metrics['hhea.ascent'] +
                abs(font_metrics['hhea.descent']) +
                font_metrics['hhea.lineGap']) / font_upm
    if not failed and not 1.1 < hhea_sum <= 1.5:
        warn = True
        yield WARN,\
              Message('bad-hhea-range',
                      f"We recommend the absolute sum of the hhea metrics should be"
                      f" between 1.1-1.4x of the font's upm. This font has {hhea_sum}x")

    if not failed and not warn:
        yield PASS, 'Vertical metrics are good'


@check(
    id = 'com.google.fonts/check/cjk_vertical_metrics_regressions',
    conditions = ['is_cjk_font',
                  'regular_remote_style'],
    rationale = """
        Check CJK family has the same vertical metrics as the same family
        hosted on Google Fonts.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/pull/3244'
)
def com_google_fonts_check_cjk_vertical_metrics_regressions(regular_ttFont, regular_remote_style):
    """Check if the vertical metrics of a CJK family are similar to the same
    family hosted on Google Fonts."""
    import math
    gf_ttFont = regular_remote_style
    ttFont = regular_ttFont

    upm_scale = ttFont['head'].unitsPerEm / gf_ttFont['head'].unitsPerEm

    failed = False
    for tbl, attrib in [
        ("OS/2", "sTypoAscender"),
        ("OS/2", "sTypoDescender"),
        ("OS/2", "sTypoLineGap"),
        ("OS/2", "usWinAscent"),
        ("OS/2", "usWinDescent"),
        ("hhea", "ascent"),
        ("hhea", "descent"),
        ("hhea", "lineGap"),
        ]:
        gf_val = math.ceil(getattr(gf_ttFont[tbl], attrib) * upm_scale)
        f_val = math.ceil(getattr(ttFont[tbl], attrib))
        if gf_val != f_val:
            failed = True
            yield FAIL,\
                  Message("cjk-metric-regression",
                          f" {tbl} {attrib} is {f_val}"
                          f" when it should be {gf_val}")
    if not failed:
        yield PASS, "CJK vertical metrics are good"


@check(
    id = 'com.google.fonts/check/cjk_not_enough_glyphs',
    conditions = ['is_cjk_font'],
    rationale = """
        Hangul has 40 characters and it's the smallest CJK writing system.

        If a font contains less CJK glyphs than this writing system, we inform the
        user that some glyphs may be encoded incorrectly.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/pull/3214'
)
def com_google_fonts_check_cjk_not_enough_glyphs(ttFont):
    """Does the font contain less than 40 CJK characters?"""
    from .shared_conditions import get_cjk_glyphs
    cjk_glyphs = get_cjk_glyphs(ttFont)
    cjk_glyph_count = len(cjk_glyphs)
    if cjk_glyph_count > 0 and cjk_glyph_count < 40:
        if cjk_glyph_count == 1:
            N_CJK_glyphs = "There is only one CJK glyph"
        else:
            N_CJK_glyphs = f"There are only {cjk_glyph_count} CJK glyphs"

        yield WARN,\
              Message('cjk-not-enough-glyphs',
                      f"{N_CJK_glyphs} when there needs to be at least 40"
                      f" in order to support the smallest CJK writing system, Hangul.\n"
                      f"The following CJK glyphs were found:\n"
                      f"{cjk_glyphs}\n"
                      f"Please check that these glyphs have the correct unicodes.")
    else:
        yield PASS, "Font has the correct quantity of CJK glyphs"


@check(
    id = 'com.google.fonts/check/varfont_instance_coordinates',
    conditions = ['is_variable_font'],
    proposal = 'https://github.com/googlefonts/fontbakery/pull/2520'
)
def com_google_fonts_check_varfont_instance_coordinates(ttFont):
    """Check variable font instances have correct coordinate values"""
    from fontbakery.parse import instance_parse
    from fontbakery.constants import SHOW_GF_DOCS_MSG

    failed = False
    for instance in ttFont['fvar'].instances:
        name = ttFont['name'].getName(
            instance.subfamilyNameID,
            PlatformID.WINDOWS,
            WindowsEncodingID.UNICODE_BMP,
            WindowsLanguageID.ENGLISH_USA
        ).toUnicode()
        expected_instance = instance_parse(name)
        for axis in instance.coordinates:
            if axis in expected_instance.coordinates and \
                instance.coordinates[axis] != expected_instance.coordinates[axis]:
                yield FAIL,\
                      Message("bad-coordinate",
                              f'Instance "{name}" {axis} value '
                              f'is "{instance.coordinates[axis]}". '
                              f'It should be "{expected_instance.coordinates[axis]}"')
                failed = True

    if failed:
        yield FAIL, f"{SHOW_GF_DOCS_MSG}#axes"
    else:
        yield PASS, "Instance coordinates are correct"


@check(
    id = 'com.google.fonts/check/varfont_instance_names',
    conditions = ['is_variable_font'],
    proposal = 'https://github.com/googlefonts/fontbakery/pull/2520'
)
def com_google_fonts_check_varfont_instance_names(ttFont):
    """Check variable font instances have correct names"""
    # This check and the fontbakery.parse module used to be more complicated.
    # On 2020-06-26, we decided to only allow Thin-Black + Italic instances.
    # If we decide to add more particles to instance names, It's worthwhile
    # revisiting our previous implementation which can be found in commits
    # earlier than or equal to ca71d787eb2b8b5a9b111884080dde5d45f5579f
    from fontbakery.parse import instance_parse
    from fontbakery.constants import SHOW_GF_DOCS_MSG

    failed = []
    for instance in ttFont['fvar'].instances:
        name = ttFont['name'].getName(
            instance.subfamilyNameID,
            PlatformID.WINDOWS,
            WindowsEncodingID.UNICODE_BMP,
            WindowsLanguageID.ENGLISH_USA
        ).toUnicode()
        expected_instance = instance_parse(name)

        # Check if name matches predicted name
        if expected_instance.name != name:
            failed.append(name)

    if failed:
        failed_instances = "\n\t- ".join([""] + failed)
        yield FAIL,\
              Message('bad-instance-names',
                      f'Following instances are not supported: {failed_instances}\n'
                      f'\n'
                      f'{SHOW_GF_DOCS_MSG}#fvar-instances')
    else:
        yield PASS, "Instance names are correct"


@check(
    id = 'com.google.fonts/check/varfont_duplicate_instance_names',
    rationale = """
        This check's purpose is to detect duplicate named instances names in a
        given variable font.

        Repeating instance names may be the result of instances for several VF axes
        defined in `fvar`, but since currently only weight+italic tokens are allowed
        in instance names as per GF specs, they ended up repeating.

        Instead, only a base set of fonts for the most default representation of the
        family can be defined through instances in the `fvar` table, all other
        instances will have to be left to access through the `STAT` table.
    """,
    conditions = ['is_variable_font'],
    proposal = 'https://github.com/googlefonts/fontbakery/issues/2986'
)
def com_google_fonts_check_varfont_duplicate_instance_names(ttFont):
    """Check variable font instances don't have duplicate names"""
    from fontbakery.constants import SHOW_GF_DOCS_MSG

    seen = []
    duplicate = []

    for instance in ttFont['fvar'].instances:
        name = ttFont['name'].getName(
            instance.subfamilyNameID,
            PlatformID.WINDOWS,
            WindowsEncodingID.UNICODE_BMP,
            WindowsLanguageID.ENGLISH_USA
        ).toUnicode()

        if name in seen:
            duplicate.append(name)

        if not name in seen:
            seen.append(name)

    if duplicate:
        duplicate_instances = "\n\t- ".join([""] + duplicate)
        yield FAIL,\
              Message('duplicate-instance-names',
                      f'Following instances names are duplicate: {duplicate_instances}\n')
    else:
        yield PASS, "Instance names are unique"


@check(
    id = 'com.google.fonts/check/varfont/unsupported_axes',
    rationale = """
        The 'ital' and 'slnt' axes are not supported yet in Google Chrome.

        For the time being, we need to ensure that VFs do not contain either of
        these axes. Once browser support is better, we can deprecate this check.

        For more info regarding browser support, see:
        https://arrowtype.github.io/vf-slnt-test/
    """,
    conditions = ['is_variable_font'],
    proposal = 'https://github.com/googlefonts/fontbakery/issues/2866'
)
def com_google_fonts_check_varfont_unsupported_axes(ttFont):
    """ Ensure VFs do not contain slnt or ital axes. """
    from fontbakery.profiles.shared_conditions import slnt_axis, ital_axis
    if ital_axis(ttFont):
        yield FAIL,\
              Message("unsupported-ital",
                      'The "ital" axis is not yet well supported on Google Chrome.')
    elif slnt_axis(ttFont):
        yield FAIL,\
              Message("unsupported-slnt",
                      'The "slnt" axis is not yet well supported on Google Chrome.')
    else:
        yield PASS, "Looks good!"


@check(
    id = 'com.google.fonts/check/varfont/grade_reflow',
    rationale = """
        The grade (GRAD) axis should not change any advanceWidth or kerning data
        across its design space. This is because altering the advance width of glyphs
        can cause text reflow.
    """,
    conditions = ['is_variable_font'],
    proposal = 'https://github.com/googlefonts/fontbakery/issues/3187'
)
def com_google_fonts_check_varfont_grade_reflow(ttFont, config):
    """ Ensure VFs with the GRAD axis do not vary horizontal advance. """
    from fontbakery.profiles.shared_conditions import grad_axis
    from fontbakery.utils import (all_kerning,
                                  pretty_print_list)
    if not grad_axis(ttFont):
        yield SKIP,\
              Message("no-grad",
                      "This font has no GRAD axis")
        return

    gvar = ttFont["gvar"]
    bad_glyphs = set()
    for glyph, deltas in gvar.variations.items():
        for delta in deltas:
            if "GRAD" not in delta.axes:
                continue
            if any(c is not None and c != (0, 0)
                   for c in delta.coordinates[-4:]):
                bad_glyphs.add(glyph)

    if bad_glyphs:
        bad_glyphs_list = pretty_print_list(config,
                                            list(bad_glyphs))
        yield FAIL,\
              Message("grad-causes-reflow",
                      f"The following glyphs have variation in horizontal"
                      f" advance due to the GRAD axis: {bad_glyphs_list}")

    # Determine if any kerning rules vary the horizontal advance.
    # This is going to get grubby.
    bad_kerning = False

    if "GDEF" in ttFont and hasattr(ttFont["GDEF"].table, "VarStore"):
        effective_regions = []
        varstore = ttFont["GDEF"].table.VarStore
        regions = varstore.VarRegionList.Region
        grad_index = [x.axisTag == "GRAD"
                      for x in ttFont["fvar"].axes].index(True)
        for ix, region in enumerate(regions):
            axis_tent = region.VarRegionAxis[grad_index]
            effective = (axis_tent.StartCoord != axis_tent.PeakCoord
                         or axis_tent.PeakCoord != axis_tent.EndCoord)
            if effective:
              effective_regions.append(ix)

        # Some regions vary *something* along the GRAD axis. But what?
        if effective_regions:
            kerning = all_kerning(ttFont)
            for left, right, v1, v2 in kerning:
                if v1 and hasattr(v1, "XAdvDevice") and v1.XAdvDevice:
                    variation = [v1.XAdvDevice.StartSize, v1.XAdvDevice.EndSize]
                    regions = varstore.VarData[variation[0]].VarRegionIndex
                    if any(region in effective_regions for region in regions):
                        deltas = varstore.VarData[variation[0]].Item[variation[1]]
                        effective_deltas = [deltas[ix]
                                            for ix, region in enumerate(regions)
                                            if region in effective_regions]
                        if any(x for x in effective_deltas):
                            yield FAIL,\
                                  Message("grad-kern-causes-reflow",
                                          f"Kerning rules cause variation in"
                                          f" horizontal advance on the GRAD axis"
                                          f" (e.g. {left}/{right})")
                            bad_kerning = True
                            break

    # Check kerning here
    if not bad_glyphs and not bad_kerning:
        yield PASS, ("No variations or kern rules vary "
                     "horizontal advance along the GRAD axis")


@check(
    id = 'com.google.fonts/check/metadata/gf-axisregistry_bounds',
    rationale = """
        Each axis range in a METADATA.pb file must be registered, and within the bounds
        of the axis definition in the Google Fonts Axis Registry, available at
        https://github.com/google/fonts/tree/main/axisregistry
    """,
    conditions = ['is_variable_font',
                  'family_metadata',
                  'GFAxisRegistry'],
    proposal = 'https://github.com/googlefonts/fontbakery/issues/3010'
)
def com_google_fonts_check_gf_axisregistry_bounds(family_metadata, GFAxisRegistry):
    """ Validate METADATA.pb axes values are within gf-axisregistry bounds. """
    passed = True
    for axis in family_metadata.axes:
        if axis.tag in GFAxisRegistry.keys():
            expected = GFAxisRegistry[axis.tag]
            if axis.min_value < expected.min_value or axis.max_value > expected.max_value:
                passed = False
                yield FAIL,\
                      Message('bad-axis-range',
                              f"The range in the font variation axis"
                              f" '{axis.tag}' ({expected.display_name}"
                              f" min:{axis.min_value} max:{axis.max_value})"
                              f" does not comply with the expected maximum range,"
                              f" as defined on Google Fonts Axis Registry"
                              f" (min:{expected.min_value} max:{expected.max_value}).")
    if passed:
        yield PASS, "OK"


@check(
    id = 'com.google.fonts/check/metadata/gf-axisregistry_valid_tags',
    rationale = """
        Ensure all axes in a METADATA.pb file are registered in the
        Google Fonts Axis Registry, available at
        https://github.com/google/fonts/tree/main/axisregistry

        Why does Google Fonts have its own Axis Registry?

        We support a superset of the OpenType axis registry axis set, and use
        additional metadata for each axis. Axes present in a font file but not in this
        registry will not function via our API. No variable font is expected to
        support all of the axes here.

        Any font foundry or distributor library that offers variable fonts has a
        implicit, latent, de-facto axis registry, which can be extracted by scanning
        the library for axes' tags, labels, and min/def/max values. While in 2016
        Microsoft originally offered to include more axes in the OpenType 1.8
        specification (github.com/microsoft/OpenTypeDesignVariationAxisTags), as of
        August 2020, this effort has stalled. We hope more foundries and distributors
        will publish documents like this that make their axes explicit, to encourage
        of adoption of variable fonts throughout the industry, and provide source
        material for a future update to the OpenType specification's axis registry.
    """,
    conditions = ['is_variable_font',
                  'family_metadata',
                  'GFAxisRegistry'],
    proposal = 'https://github.com/googlefonts/fontbakery/issues/3022'
)
def com_google_fonts_check_gf_axisregistry_valid_tags(family_metadata, GFAxisRegistry):
    """ Validate METADATA.pb axes tags are defined in gf-axisregistry. """
    passed = True
    for axis in family_metadata.axes:
        if axis.tag not in GFAxisRegistry.keys():
            passed = False
            yield FAIL,\
                  Message('bad-axis-tag',
                          f"The font variation axis '{axis.tag}'"
                          f" is not yet registered on Google Fonts Axis Registry.")

    if passed:
        yield PASS, "OK"


@check(
    id = 'com.google.fonts/check/gf-axisregistry/fvar_axis_defaults',
    rationale = """
        Check that axis defaults have a corresponding fallback name registered at the
        Google Fonts Axis Registry, available at
        https://github.com/google/fonts/tree/main/axisregistry

        This is necessary for the following reasons:

        To get ZIP files downloads on Google Fonts to be accurate — otherwise the
        chosen default font is not generated. The Newsreader family, for instance, has
        a default value on the 'opsz' axis of 16pt. If 16pt was not a registered
        fallback position, then the ZIP file would instead include another position
        as default (such as 14pt).

        For the Variable fonts to display the correct location on the specimen page.

        For VF with no weight axis to be displayed at all. For instance, Ballet, which
        has no weight axis, was not appearing in sandbox because default position on
        'opsz' axis was 16pt, and it was not yet a registered fallback positon.
    """,
    conditions = ['is_variable_font',
                  'GFAxisRegistry'],
    proposal = 'https://github.com/googlefonts/fontbakery/issues/3141'
)
def com_google_fonts_check_gf_axisregistry_fvar_axis_defaults(ttFont, GFAxisRegistry):
    """ Validate defaults on fvar table match registered fallback names in GFAxisRegistry. """

    passed = True
    for axis in ttFont['fvar'].axes:
        if axis.axisTag not in GFAxisRegistry:
            continue
        fallbacks = GFAxisRegistry[axis.axisTag].fallback
        if axis.defaultValue not in [f.value for f in fallbacks]:
            passed = False
            yield FAIL,\
                  Message('not-registered',
                          f"The defaul value {axis.axisTag}:{axis.defaultValue} is not"
                          f" registered as an axis fallback name on the Google Axis Registry.\n"
                          f"\tYou should consider suggesting the addition of this value to the registry"
                          f" or adopted one of the existing fallback names for this axis:\n"
                          f"\t{fallbacks}")
    if passed:
        yield PASS, "OK"


@check(
    id = 'com.google.fonts/check/STAT/gf-axisregistry',
    rationale = """
        Check that particle names and values on STAT table match the fallback names
        in each axis entry at the Google Fonts Axis Registry, available at
        https://github.com/google/fonts/tree/main/axisregistry
    """,
    conditions = ['is_variable_font',
                  'GFAxisRegistry'],
    proposal = 'https://github.com/googlefonts/fontbakery/issues/3022'
)
def com_google_fonts_check_STAT_gf_axisregistry_names(ttFont, GFAxisRegistry):
    """ Validate STAT particle names and values match the fallback names in GFAxisRegistry. """

    def normalize_name(name):
        return ''.join(name.split(' '))

    passed = True
    format4_entries = False
    axis_value_array = ttFont['STAT'].table.AxisValueArray
    if not axis_value_array:
        yield FAIL, Message("missing-axis-values",
                            "STAT table is missing Axis Value Records")
        return

    for axis_value in axis_value_array.AxisValue:
        if axis_value.Format == 4:
            coords = []
            for record in axis_value.AxisValueRecord:
                axis = ttFont['STAT'].table.DesignAxisRecord.Axis[record.AxisIndex]
                coords.append(f"{axis.AxisTag}:{record.Value}")
            coords = ", ".join(coords)

            name_entry = ttFont['name'].getName(axis_value.ValueNameID,
                                                PlatformID.WINDOWS,
                                                WindowsEncodingID.UNICODE_BMP,
                                                WindowsLanguageID.ENGLISH_USA)
            format4_entries = True
            yield INFO,\
                  Message("format-4",
                          f"'{name_entry.toUnicode()}' at ({coords})")
            continue

        axis = ttFont['STAT'].table.DesignAxisRecord.Axis[axis_value.AxisIndex]
        if axis.AxisTag in GFAxisRegistry.keys():
            fallbacks = GFAxisRegistry[axis.AxisTag].fallback
            fallbacks = {f.name: f.value for f in fallbacks}

            # Here we assume that it is enough to check for only the Windows,
            # English USA entry corresponding to a given nameID. It is up to other
            # checks to ensure all different platform/encoding entries
            # with a given nameID are consistent in the name table.
            name_entry = ttFont['name'].getName(axis_value.ValueNameID,
                                                PlatformID.WINDOWS,
                                                WindowsEncodingID.UNICODE_BMP,
                                                WindowsLanguageID.ENGLISH_USA)

            # Here "name_entry" has the user-friendly name of the current AxisValue
            # We want to ensure that this string shows up as a "fallback" name
            # on the GF Axis Registry for this specific variation axis tag.
            name = normalize_name(name_entry.toUnicode())
            expected_names = [normalize_name(n) for n in fallbacks.keys()]
            if hasattr(axis_value, 'Value'): # Format 1 & 3
                is_value = axis_value.Value
            elif hasattr(axis_value, 'NominalValue'): # Format 2
                is_value = axis_value.NominalValue
            if name not in expected_names:
                expected_names = ", ".join(expected_names)
                passed = False
                yield FAIL,\
                      Message('invalid-name',
                              f"On the font variation axis '{axis.AxisTag}', the name '{name_entry.toUnicode()}'"
                              f" is not among the expected ones ({expected_names}) according"
                              f" to the Google Fonts Axis Registry.")
            elif is_value != fallbacks[name_entry.toUnicode()]:
                passed = False
                yield FAIL,\
                      Message("bad-coordinate",
                              (f"Axis Value for '{axis.AxisTag}':'{name_entry.toUnicode()}' is"
                               f" expected to be '{fallbacks[name_entry.toUnicode()]}'"
                               f" but this font has '{name_entry.toUnicode()}'='{axis_value.Value}'."))

    if format4_entries:
        yield INFO,\
              Message("format-4",
                      "The GF Axis Registry does not currently contain fallback names"
                      " for the combination of values for more than a single axis,"
                      " which is what these 'format 4' entries are designed to describe,"
                      " so this check will ignore them for now.")

    if passed:
        yield PASS, "OK"


@check(
    id = 'com.google.fonts/check/metadata/consistent_axis_enumeration',
    rationale = """
        All font variation axes present in the font files must be properly declared
        on METADATA.pb so that they can be served by the GFonts API.
    """,
    conditions = ['is_variable_font',
                  'family_metadata'],
    proposal = 'https://github.com/googlefonts/fontbakery/issues/3051'
)
def com_google_fonts_check_metadata_consistent_axis_enumeration(family_metadata, ttFont, config):
    """ Validate VF axes match the ones declared on METADATA.pb. """
    from fontbakery.utils import pretty_print_list

    passed = True
    md_axes = set(axis.tag for axis in family_metadata.axes)
    fvar_axes = set(axis.axisTag for axis in ttFont['fvar'].axes)
    missing = sorted(fvar_axes - md_axes)
    extra = sorted(md_axes - fvar_axes)

    if missing:
        passed = False
        yield FAIL,\
              Message('missing-axes',
                      f"The font variation axes {pretty_print_list(config, missing)}"
                      f" are present in the font's fvar table but are not"
                      f" declared on the METADATA.pb file.")
    if extra:
        passed = False
        yield FAIL,\
              Message('extra-axes',
                      f"The METADATA.pb file lists font variation axes that"
                      f" are not supported but this family: {pretty_print_list(config, extra)}")
    if passed:
        yield PASS, "OK"


@check(
    id = 'com.google.fonts/check/STAT/axis_order',
    rationale = """
        This is (for now) a merely informative check to detect what's the axis ordering
        declared on the STAT table of fonts in the Google Fonts collection.

        We may later update this to enforce some unified axis ordering scheme,
        yet to be determined.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/issues/3049'
)
def com_google_fonts_check_STAT_axis_order(fonts):
    """ Check axis ordering on the STAT table. """
    from collections import Counter
    from fontTools.ttLib import TTFont

    no_stat = 0
    summary = []
    for font in fonts:
        try:
            ttFont = TTFont(font)
            if 'STAT' in ttFont:
                order = {}
                for axis in ttFont['STAT'].table.DesignAxisRecord.Axis:
                    order[axis.AxisTag] = axis.AxisOrdering

                summary.append('-'.join(sorted(order.keys(), key=order.get)))
            else:
                no_stat += 1
                yield SKIP,\
                      Message('missing-STAT',
                              f"This font does not have a STAT table: {font}")
        except:
            yield INFO,\
                  Message('bad-font',
                          f"Something wrong with {font}")

    report = "\n\t".join(map(str, Counter(summary).most_common()))
    yield INFO,\
          Message('summary',
                  f"From a total of {len(fonts)} font files,"
                  f" {no_stat} of them ({100.0*no_stat/len(fonts):.2f}%)"
                  f" lack a STAT table.\n"
                  f"\n"
                  f"\tAnd these are the most common STAT axis orderings:\n"
                  f"\t{report}")


@check(
    id = 'com.google.fonts/check/metadata/escaped_strings',
    rationale = """
        In some cases we've seen designer names and other fields with escaped strings
        in METADATA files. Nowadays the strings can be full unicode strings and
        do not need escaping.
    """,
    conditions = ["metadata_file"],
    proposal = 'https://github.com/googlefonts/fontbakery/issues/2932'
)
def com_google_fonts_check_metadata_escaped_strings(metadata_file):
    """Ensure METADATA.pb does not use escaped strings."""
    passed = True
    for line in open(metadata_file, "r").readlines():
        for quote_char in ["'", "\""]:
            segments = line.split(quote_char)
            if len(segments) >= 3:
                a_string = segments[1]
                if "\\" in a_string:
                    passed = False
                    yield FAIL,\
                          Message('escaped-strings',
                                  f"Found escaped chars at '{a_string}'."
                                  f" Please use an unicode string instead.")
    if passed:
        yield PASS, "OK"


@check(
    id = 'com.google.fonts/check/metadata/designer_profiles',
    rationale = """
        Google Fonts has a catalog of designers.

        This check ensures that the online entries of the catalog can be found based
        on the designer names listed on the METADATA.pb file.

        It also validates the URLs and file formats are all correctly set.
    """,
    conditions = ['family_metadata'],
    proposal = 'https://github.com/googlefonts/fontbakery/issues/3083'
)
def com_google_fonts_check_metadata_designer_profiles(family_metadata):
    """METADATA.pb: Designers are listed correctly on the Google Fonts catalog?"""
    DESIGNER_INFO_RAW_URL = ("https://raw.githubusercontent.com/google/"
                             "fonts/master/catalog/designers/{}/")
    from fontbakery.utils import get_DesignerInfoProto_Message
    import requests

    # NOTE: See issue #3316
    TRANSLATE = {
        'á': 'a',
        'é': 'e',
        'í': 'i',
        'ó': 'o',
        'ú': 'u',
        'à': 'a',
        'è': 'e',
        'ì': 'i',
        'ò': 'o',
        'ù': 'u',
        'ń': 'n',
        'ø': 'o',
        'ř': 'r',
        'ś': 's',
        'ß': 'ss',
        'ł': 'l',
        'ã': 'a',
        'ı': 'i',
        'ü': 'ue'
    }
    def normalize(name):
        """ Restrict the designer name to lowercase a-z and numbers"""
        import string
        normalized_name = ""
        for c in name.lower():
            if c in string.ascii_letters or c in "0123456789":
                normalized_name += c
            elif c in TRANSLATE.keys():
                normalized_name += TRANSLATE[c]
        return normalized_name

    passed = True
    for designer in family_metadata.designer.split(','):
        designer = designer.strip()
        normalized_name = normalize(designer)
        if normalized_name == "multipledesigners":
            yield FAIL,\
                  Message("multiple-designers",
                          f"Font family {family_metadata.name} does not explicitely"
                          f" mention the names of its designers on its METADATA.pb file.")
            continue

        url = DESIGNER_INFO_RAW_URL.format(normalized_name) + "info.pb"
        response = requests.get(url)
        if response.status_code != requests.codes.OK:
            passed = False
            yield WARN,\
                  Message("profile-not-found",
                          f"It seems that {designer} is still not listed on"
                          f" the designers catalog. Please submit a photo and"
                          f" a link to a webpage where people can learn more"
                          f" about the work of this designer/typefoundry.")
            continue

        info = get_DesignerInfoProto_Message(response.content)
        if info.designer != designer.strip():
            passed = False
            yield FAIL,\
                  Message("mismatch",
                          f"Designer name at METADATA.pb ({designer})"
                          f" is not the same as listed on the designers"
                          f" catalog ({info.designer}) available at {url}")

        if info.link != "":
            passed = False
            yield FAIL,\
                  Message("link-field",
                          "Currently the link field is not used by the GFonts API."
                          " Designer webpage links should, for now, be placed"
                          " directly on the bio.html file.")

        if not info.avatar.file_name:
            passed = False
            yield FAIL,\
                  Message("missing-avatar",
                          f"Designer {designer} still does not have an avatar image. "
                          f"Please provide one.")
        else:
            avatar_url = DESIGNER_INFO_RAW_URL.format(normalized_name) + info.avatar.file_name
            response = requests.get(avatar_url)
            if response.status_code != requests.codes.OK:
                passed = False
                yield FAIL,\
                      Message("bad-avatar-filename",
                              f"The avatar filename provided seems to be incorrect: ({avatar_url})")

    if passed:
        yield PASS, "OK"


@check(
    id = 'com.google.fonts/check/mandatory_avar_table',
    rationale = """
        Most variable fonts should include an avar table to correctly define
        axes progression rates.

        For example, a weight axis from 0% to 100% doesn't map directly to 100 to 1000,
        because a 10% progression from 0% may be too much to define the 200,
        while 90% may be too little to define the 900.

        If the progression rates of axes is linear, this check can be ignored.
        Fontmake will also skip adding an avar table if the progression rates
        are linear. However, we still recommend designers visually proof each
        instance is at the desired weight, width etc.
    """,
    conditions = ["is_variable_font"],
    proposal = 'https://github.com/googlefonts/fontbakery/issues/3100'
)
def com_google_fonts_check_mandatory_avar_table(ttFont):
    """Ensure variable fonts include an avar table."""
    if "avar" not in ttFont:
        yield FAIL,\
              Message('missing-avar',
                      "This variable font does not have an avar table.")
    else:
        yield PASS, "OK"


@check(
    id = 'com.google.fonts/check/description/family_update',
    rationale = """
        We want to ensure that any significant changes to the font family are
        properly mentioned in the DESCRIPTION file.

        In general, it means that the contents of the DESCRIPTION.en_us.html file
        will typically change if when font files are updated. Please treat this check
        as a reminder to do so whenever appropriate!
    """,
    conditions = ["description",
                  "github_gfonts_description"],
    proposal = 'https://github.com/googlefonts/fontbakery/issues/3182'
)
def com_google_fonts_check_description_family_update(description, github_gfonts_description):
    """On a family update, the DESCRIPTION.en_us.html file should ideally also be updated."""
    if github_gfonts_description == description:
        yield WARN,\
              Message('description-not-updated',
                      "The DESCRIPTION.en_us.html file in this family has not changed"
                      " in comparison to the latest font release on the"
                      " google/fonts github repo.\n"
                      "Please consider mentioning note-worthy improvements made"
                      " to the family recently.")
    else:
        yield PASS, "OK"


@check(
    id = 'com.google.fonts/check/missing_small_caps_glyphs',
    rationale = """
        Ensure small caps glyphs are available if
        a font declares smcp or c2sc OT features.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/issues/3154'
)
def com_google_fonts_check_missing_small_caps_glyphs(ttFont):
    """Check small caps glyphs are available."""

    passed = True
    if 'GSUB' in ttFont and ttFont['GSUB'].table.FeatureList is not None:
        llist = ttFont['GSUB'].table.LookupList
        for record in range(ttFont['GSUB'].table.FeatureList.FeatureCount):
            feature = ttFont['GSUB'].table.FeatureList.FeatureRecord[record]
            tag = feature.FeatureTag
            if tag in ['smcp', 'c2sc']:
                for index in feature.Feature.LookupListIndex:
                    subtable = llist.Lookup[index].SubTable[0]
                    if subtable.LookupType == 7:
                        # This is an Extension lookup
                        # used for reaching 32-bit offsets
                        # within the GSUB table.
                        subtable = subtable.ExtSubTable
                    smcp_glyphs = set(subtable.mapping.values())
                    missing = smcp_glyphs - set(ttFont.getGlyphNames())
                    if missing:
                        passed = False
                        missing = "\n\t - " + "\n\t - ".join(missing)
                        yield FAIL,\
                              Message('missing-glyphs',
                                      f"These '{tag}' glyphs are missing:\n"
                                      f"{missing}")
                break

    if passed:
        yield PASS, "OK"


@check(
    id = 'com.google.fonts/check/stylisticset_description',
    rationale = """
        Stylistic sets should provide description text. Programs such as InDesign,
        TextEdit and Inkscape use that info to display to the users so that they know
        what a given stylistic set offers.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/issues/3155'
)
def com_google_fonts_check_stylisticset_description(ttFont):
    """Ensure Stylistic Sets have description."""

    passed = True
    if 'GSUB' in ttFont and ttFont['GSUB'].table.FeatureList is not None:
        for record in range(ttFont['GSUB'].table.FeatureList.FeatureCount):
            feature = ttFont['GSUB'].table.FeatureList.FeatureRecord[record]
            tag = feature.FeatureTag
            SSETS = [f'ss{n+1:02d}' for n in range(20)]
            assert('ss00' not in SSETS)
            assert('ss01' in SSETS)
            assert('ss20' in SSETS)
            assert('ss21' not in SSETS)
            if tag in SSETS:
                if feature.Feature.FeatureParams == None:
                    passed = False
                    yield WARN,\
                          Message('missing-description',
                                  f"The stylistic set {tag} lacks"
                                  f" a description string on the 'name' table.")
                else:
                    pass # TODO: Maybe here we can add code to make sure
                         #       that the referenced nameid does exist
                         #       in the name table.
    if passed:
        yield PASS, "OK"


@check(
    id = "com.google.fonts/check/os2/use_typo_metrics",
    rationale = """
        All fonts on the Google Fonts collection should have OS/2.fsSelection bit 7
        (USE_TYPO_METRICS) set. This requirement is part of the vertical metrics scheme
        established as a Google Fonts policy aiming at a common ground supported by
        all major font rendering environments.

        For more details, read:
        https://github.com/googlefonts/gf-docs/blob/main/VerticalMetrics/README.md

        Below is the portion of that document that is most relevant to this check:

        Use_Typo_Metrics must be enabled. This will force MS Applications to use the
        OS/2 Typo values instead of the Win values. By doing this, we can freely set
        the Win values to avoid clipping and control the line height with the typo
        values. It has the added benefit of future line height compatibility. When
        a new script is added, we simply change the Win values to the new yMin
        and yMax, without needing to worry if the line height have changed.
    """,
    conditions = ['not is_cjk_font'],
    severity = 10,
    proposal = 'https://github.com/googlefonts/fontbakery/issues/3241'
)
def com_google_fonts_check_os2_fsselectionbit7(ttFonts):
    """OS/2.fsSelection bit 7 (USE_TYPO_METRICS) is set in all fonts."""

    bad_fonts = []
    for ttFont in ttFonts:
        if not ttFont["OS/2"].fsSelection & (1 << 7):
            bad_fonts.append(ttFont.reader.file.name)

    if bad_fonts:
        yield FAIL,\
              Message('missing-os2-fsselection-bit7',
                      f"OS/2.fsSelection bit 7 (USE_TYPO_METRICS) was"
                      f"NOT set in the following fonts: {bad_fonts}.")
    else:
        yield PASS, "OK"


@check(
    id = "com.google.fonts/check/meta/script_lang_tags",
    rationale = """
        The OpenType 'meta' table originated at Apple. Microsoft added it to OT with
        just two DataMap records:

        - dlng: comma-separated ScriptLangTags that indicate which scripts,
          or languages and scripts, with possible variants, the font is designed for.

        - slng: comma-separated ScriptLangTags that indicate which scripts,
          or languages and scripts, with possible variants, the font supports.


        The slng structure is intended to describe which languages and scripts the
        font overall supports. For example, a Traditional Chinese font that also
        contains Latin characters, can indicate Hant,Latn, showing that it supports
        Hant, the Traditional Chinese variant of the Hani script, and it also
        supports the Latn script.

        The dlng structure is far more interesting. A font may contain various glyphs,
        but only a particular subset of the glyphs may be truly "leading" in the design,
        while other glyphs may have been included for technical reasons. Such a
        Traditional Chinese font could only list Hant there, showing that it’s designed
        for Traditional Chinese, but the font would omit Latn, because the developers
        don’t think the font is really recommended for purely Latin-script use.

        The tags used in the structures can comprise just script, or also language
        and script. For example, if a font has Bulgarian Cyrillic alternates in the
        locl feature for the cyrl BGR OT languagesystem, it could also indicate in
        dlng explicitly that it supports bul-Cyrl. (Note that the scripts and languages
        in meta use the ISO language and script codes, not the OpenType ones).

        This check ensures that the font has the meta table containing the
        slng and dlng structures.

        All families in the Google Fonts collection should contain the 'meta' table.
        Windows 10 already uses it when deciding on which fonts to fall back to.
        The Google Fonts API and also other environments could use the data for
        smarter filtering. Most importantly, those entries should be added
        to the Noto fonts.

        In the font making process, some environments store this data in external
        files already. But the meta table provides a convenient way to store this
        inside the font file, so some tools may add the data, and unrelated tools
        may read this data. This makes the solution much more portable and universal.
    """,
    severity = 3,
    proposal = 'https://github.com/googlefonts/fontbakery/issues/3349'
)
def com_google_fonts_check_meta_script_lang_tags(ttFont):
    """Ensure fonts have ScriptLangTags declared on the 'meta' table."""

    if "meta" not in ttFont:
        yield WARN,\
              Message('lacks-meta-table',
                      "This font file does not have a 'meta' table.")

    else:
        if 'dlng' not in ttFont['meta'].data:
            yield FAIL,\
                  Message('missing-dlng-tag',
                          "Please specify which languages and scripts"
                          " this font is designed for.")
        else:
            yield INFO,\
                  Message('dlng-tag',
                          f"{ttFont['meta'].data['dlng']}")

        if 'slng' not in ttFont['meta'].data:
            yield FAIL,\
                  Message('missing-slng-tag',
                          "Please specify which languages and scripts"
                          " this font supports.")
        else:
            yield INFO,\
                  Message('slng-tag',
                          f"{ttFont['meta'].data['slng']}")


@check(
    id = "com.google.fonts/check/no_debugging_tables",
    rationale = """
        Tables such as `Debg` are useful in the pre-production stages of font
        development, but add unnecessary bloat to a production font and should
        be removed before release.
    """,
    severity = 6,
    proposal = 'https://github.com/googlefonts/fontbakery/issues/3357',
)
def com_google_fonts_check_no_debugging_tables(ttFont):
    """Ensure fonts do not contain any pre-production tables."""

    DEBUGGING_TABLES = ["Debg", "FFTM"]
    found = [t for t in DEBUGGING_TABLES if t in ttFont]
    if found:
        tables_list = ", ".join(found)
        yield WARN,\
              Message("has-debugging-tables",
                      f"This font file contains the following"
                      f" pre-production tables: {tables_list}")
    else:
        yield PASS, "OK"


@check(
    id = "com.google.fonts/check/metadata/family_directory_name",
    rationale = """
        We want the directory name of a font family to be predictable and directly
        derived from the family name, all lowercased and removing spaces.
    """,
    conditions = ['family_metadata',
                  'family_directory'],
    proposal = 'https://github.com/googlefonts/fontbakery/issues/3421',
)
def com_google_fonts_check_metadata_family_directory_name(family_metadata, family_directory):
    """Check font family directory name."""

    dir_name = os.path.basename(family_directory)
    expected = family_metadata.name.replace(" ", "").lower()
    if expected != dir_name:
        yield FAIL,\
              Message("bad-directory-name",
                      f'Family name on METADATA.pb is "{family_metadata.name}"\n'
                      f'Directory name is "{dir_name}"\n'
                      f'Expected "{expected}"')
    else:
        yield PASS, f'Directory name is "{dir_name}", as expected.'


@check(
    id = "com.google.fonts/check/render_own_name",
    rationale = """
        A base expectation is that a font family's regular/default (400 roman) style
        can render its 'menu name' (nameID 1) in itself.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/issues/3159',
)
def com_google_fonts_check_render_own_name(ttFont):
    """Check font can render its own name."""
    from fontbakery.utils import can_shape

    menu_name = ttFont["name"].getName(
        NameID.FONT_FAMILY_NAME,
        PlatformID.WINDOWS,
        WindowsEncodingID.UNICODE_BMP,
        WindowsLanguageID.ENGLISH_USA
    ).toUnicode()
    if can_shape(ttFont, menu_name):
        yield PASS, f'Font can successfully render its own name ({menu_name})'
    else:
        yield FAIL,\
              Message("render-own-name",
                      f'.notdef glyphs were found when attempting to render {menu_name}')

@check(
    id = "com.google.fonts/check/repo/sample_image",
    rationale = """
        In order to showcase what a font family looks like, the project's README.md
        file should ideally include a sample image and highlight it in the upper
        portion of the document, no more than 10 lines away from the top of the file.
    """,
    conditions = ["readme_contents",
                  "readme_directory"],
    proposal = 'https://github.com/googlefonts/fontbakery/issues/2898',
)
def com_google_fonts_check_repo_sample_image(readme_contents, readme_directory, config):
    """Check README.md has a sample image."""
    import re
    from fontbakery.utils import bullet_list

    image_path = False
    line_number = 0
    for line in readme_contents.split('\n'):
        if line.strip() == "":
            continue

        line_number += 1
        # We're looking for something like this:
        # ![Raleway font sample](documents/raleway-promo.jpg)
        # And we accept both png and jpg files
        result = re.match(r'\!\[.*\]\((.*\.(png|jpg))\)', line)
        if result:
            image_path = result[1]
            break

    local_image_files = []
    for (dirpath, dirnames, filenames) in os.walk(readme_directory):
        local_image_files.extend([os.path.join(dirpath[len(readme_directory)+1:], filename)
                                  for filename in filenames
                                  if filename.endswith('.jpg') or
                                     filename.endswith('.png')])

    if local_image_files:
        sample_tip = local_image_files[0]
    else:
        sample_tip = 'samples/some-sample-image.jpg'
    MARKDOWN_IMAGE_SYNTAX_TIP = (f'You can use something like this:\n\n'
                                 f'\t![font sample]({sample_tip})')

    if image_path:
        if image_path not in local_image_files:
            yield FAIL,\
                  Message("image-missing",
                          f'The referenced sample image could not be found:'
                          f' {os.path.join(readme_directory, image_path)}\n')
        else:
            if line_number < 10:
                yield PASS, "Looks good!"
            else:
                yield WARN,\
                      Message("not-ideal-placement",
                              'Please consider placing the sample image closer'
                              ' to the top of the README document so that it is'
                              ' more immediately viewed by readers.\n')
    else: # if no image reference was found on README.md:
        if local_image_files:
            yield WARN,\
                  Message("image-not-displayed",
                          f'Even though the README.md file does not display'
                          f' a font sample image, a few image files were found:\n\n'
                          f'{bullet_list(config, local_image_files)}\n'
                          f'\n'
                          f'Please consider including one of those images on the README.\n'
                          f'{MARKDOWN_IMAGE_SYNTAX_TIP}\n')
        else:
            yield WARN,\
                  Message("no-sample",
                          f'Please add a font sample image to the README.md file.\n'
                          f'{MARKDOWN_IMAGE_SYNTAX_TIP}\n')


@check(
    id = "com.google.fonts/check/metadata/can_render_samples",
    rationale = """
        In order to prevent tofu from being seen on fonts.google.com, this check
        verifies that all samples provided on METADATA.pb can be properly
        rendered by the font.
    """,
    conditions = ["family_metadata"],
    proposal = ['https://github.com/googlefonts/fontbakery/issues/3419',
                'https://github.com/googlefonts/fontbakery/issues/3605']
)
def com_google_fonts_check_metadata_can_render_samples(ttFont, family_metadata):
    """Check samples can be rendered."""
    from fontbakery.utils import can_shape
    from gflanguages import LoadLanguages

    passed = True
    if not family_metadata.sample_glyphs:
       passed = False
       yield INFO,\
             Message('no-samples',
                     'No sample_glyphs on METADATA.pb')
    else:
        for name, glyphs in family_metadata.sample_glyphs.items():
            if not can_shape(ttFont, glyphs):
                passed = False
                yield FAIL,\
                      Message('sample-glyphs',
                              f"Font can't render the following sample glyphs:\n"
                              f"'{name}': '{glyphs}'")

    languages = LoadLanguages()
    for lang in family_metadata.languages:
        # Note: checking agains all samples often results in
        #       a way too verbose output. That's why I only left
        #       the "tester" string for now.
        SAMPLES = {
            #'styles': languages[lang].sample_text.styles,
            'tester': languages[lang].sample_text.tester,
            #'specimen_16': languages[lang].sample_text.specimen_16,
            #'specimen_21': languages[lang].sample_text.specimen_21,
            #'specimen_32': languages[lang].sample_text.specimen_32,
            #'specimen_36': languages[lang].sample_text.specimen_36,
            #'specimen_48': languages[lang].sample_text.specimen_48
        }
        for sample_type, sample_text in SAMPLES.items():
            if not can_shape(ttFont, sample_text):
                passed = False
                yield FAIL,\
                      Message('sample-text',
                              f'Font can\'t render "{lang}" sample text:\n'
                              f'"{sample_text}"\n')

    if passed:
       yield PASS, "OK."


@check(
    id = "com.google.fonts/check/metadata/category_hints",
    rationale = """
        Sometimes the font familyname contains words that hint at which is the most
        likely correct category to be declared on METADATA.pb
    """,
    conditions = ["family_metadata"],
    proposal = 'https://github.com/googlefonts/fontbakery/issues/3624'
)
def com_google_fonts_check_metadata_category_hint(family_metadata):
    """Check if category on METADATA.pb matches what can be inferred from the family name."""

    HINTS = {
        "SANS_SERIF": ["Sans",
                       "Grotesk",
                       "Grotesque"],
        "SERIF": ["Old Style",
                  "Transitional",
                  "Garamond",
                  "Serif",
                  "Slab"],
        "DISPLAY": ["Display"],
        "HANDWRITING": ["Hand",
                        "Script"]
    }

    inferred_category = None
    for category, hints in HINTS.items():
        for hint in hints:
            if hint in family_metadata.name:
                inferred_category = category
                break

    if (inferred_category is not None and
        not family_metadata.category == inferred_category):
       yield WARN,\
             Message('inferred-category',
                     f'Familyname seems to hint at "{inferred_category}" but'
                     f' METADATA.pb declares it as "{family_metadata.category}".')
    else:
       yield PASS, "OK."


###############################################################################

def is_librebarcode(font):
    font_filenames = [
        "LibreBarcode39-Regular.ttf",
        "LibreBarcode39Text-Regular.ttf",
        "LibreBarcode128-Regular.ttf",
        "LibreBarcode128Text-Regular.ttf",
        "LibreBarcode39Extended-Regular.ttf",
        "LibreBarcode39ExtendedText-Regular.ttf"
    ]
    for font_filename in font_filenames:
        if font_filename in font:
            return True


def check_skip_filter(checkid, font=None, **iterargs):
    if font and is_librebarcode(font) and\
       checkid in (
        # See: https://github.com/graphicore/librebarcode/issues/3
        'com.google.fonts/check/monospace',
        'com.google.fonts/check/gpos_kerning_info',
        'com.google.fonts/check/currency_chars',
        'com.google.fonts/check/whitespace_ink'):
        return False, ('LibreBarcode is blacklisted for this check, see '
                       'https://github.com/graphicore/librebarcode/issues/3')
    return True, None


profile.check_skip_filter = check_skip_filter
profile.auto_register(globals())
profile.test_expected_checks(GOOGLEFONTS_PROFILE_CHECKS, exclusive=True)


# Lib/fontbakery/profiles/googlefonts_conditions.py

import os
import re
import yaml

from fontbakery.callable import condition
from fontbakery.constants import (
    NameID,
    PlatformID,
    UnicodeEncodingID,
    WindowsLanguageID
)

# -------------------------------------------------------------------
# FIXME! Redundant with @condition canonical_stylename(font)?
@condition
def style(font):
    """Determine font style from canonical filename."""
    from fontbakery.constants import STATIC_STYLE_NAMES
    filename = os.path.basename(font)
    if '-' in filename:
        stylename = os.path.splitext(filename)[0].split('-')[1]
        if stylename in [name.replace(' ', '') for name in STATIC_STYLE_NAMES]:
            return stylename
    return None


@condition
def RIBBI_ttFonts(ttFonts):
    from fontbakery.constants import RIBBI_STYLE_NAMES
    return [ttFont for ttFont in ttFonts
            if style(ttFont.reader.file.name) in RIBBI_STYLE_NAMES]


@condition
def glyphsFile(glyphs_file):
    import glyphsLib
    return glyphsLib.load(open(glyphs_file))


@condition
def style_with_spaces(font):
    """Stylename with spaces (derived from a canonical filename)."""
    if style(font):
        return style(font).replace('Italic',
                                   ' Italic').strip()

@condition
def expected_style(ttFont):
    from fontbakery.parse import style_parse
    return style_parse(ttFont)


@condition
def expected_os2_weight(style):
    """The weight name and the expected OS/2 usWeightClass value inferred from
    the style part of the font name

    The Google Font's API which serves the fonts can only serve
    the following weights values with the corresponding subfamily styles:

    250, Thin
    275, ExtraLight
    300, Light
    400, Regular
    500, Medium
    600, SemiBold
    700, Bold
    800, ExtraBold
    900, Black

    Thin is not set to 100 because of legacy Windows GDI issues:
    https://www.adobe.com/devnet/opentype/afdko/topic_font_wt_win.html
    """
    if not style:
        return None
    # Weight name to value mapping:
    GF_API_WEIGHTS = {
        "Thin": 250,
        "ExtraLight": 275,
        "Light": 300,
        "Regular": 400,
        "Medium": 500,
        "SemiBold": 600,
        "Bold": 700,
        "ExtraBold": 800,
        "Black": 900
    }
    if style == "Italic":
        weight_name = "Regular"
    elif style.endswith("Italic"):
        weight_name = style.replace("Italic", "")
    else:
        weight_name = style

    expected = GF_API_WEIGHTS[weight_name]
    return weight_name, expected


@condition
def stylenames_are_canonical(fonts):
    """ Are all font files named canonically ? """
    for font in fonts:
        if not canonical_stylename(font):
            return False
    # otherwise:
    return True


@condition
def canonical_stylename(font):
    """ Returns the canonical stylename of a given font. """
    from fontbakery.utils import suffix
    from fontbakery.constants import (STATIC_STYLE_NAMES,
                                      VARFONT_SUFFIXES)
    from .shared_conditions import is_variable_font
    from fontTools.ttLib import TTFont

    # remove spaces in style names
    valid_style_suffixes = [name.replace(' ', '') for name in STATIC_STYLE_NAMES]

    filename = os.path.basename(font)
    basename = os.path.splitext(filename)[0]
    s = suffix(font)
    varfont = os.path.exists(font) and is_variable_font(TTFont(font))
    if ('-' in basename and
        (s in VARFONT_SUFFIXES and varfont)
        or (s in valid_style_suffixes and not varfont)):
        return s


@condition
def descfile(font):
    """Get the path of the DESCRIPTION file of a given font project."""
    if font:
        directory = os.path.dirname(font)
        descfilepath = os.path.join(directory, "DESCRIPTION.en_us.html")
        if os.path.exists(descfilepath):
            return descfilepath


@condition
def description(descfile):
    """Get the contents of the DESCRIPTION file of a font project."""
    if not descfile:
        return
    import io
    return io.open(descfile, "r", encoding="utf-8").read()


@condition
def readme_directory(readme_md):
    if not readme_md:
        return
    if isinstance(readme_md, list):
        # It makes no sense to deal with more than a single README.md file
        # This here is just a quirk of the way we handle fontbakery inputs nowadays.
        readme_md = readme_md[0]
    return os.path.dirname(readme_md)


@condition
def readme_contents(readme_md):
    """Get the contents of the README.md file of a font project."""
    if not readme_md:
        return
    if isinstance(readme_md, list):
        readme_md = readme_md[0]        # quirk
    return open(readme_md, "r", encoding="utf-8").read()


@condition
def metadata_file(family_directory,
                  metadata_pb=None):
    if metadata_pb:
        if isinstance(metadata_pb, list):
            metadata_pb = metadata_pb[0]        # quirk

        return metadata_pb

    elif family_directory:
        pb_file = os.path.join(family_directory, "METADATA.pb")
        if os.path.exists(pb_file):
            return pb_file


@condition
def family_metadata(metadata_file):
    if not metadata_file:
        return

    from google.protobuf import text_format
    from fontbakery.utils import get_FamilyProto_Message
    try:
        return get_FamilyProto_Message(metadata_file)
    except text_format.ParseError:
        return None


@condition
def registered_vendor_ids():
    """Get a list of vendor IDs from Microsoft's website."""
    from bs4 import BeautifulSoup
    from pkg_resources import resource_filename

    registered_vendor_ids = {}
    CACHED = resource_filename('fontbakery',
                               'data/fontbakery-microsoft-vendorlist.cache')
    content = open(CACHED, encoding='utf-8').read()
    # Strip all <A> HTML tags from the raw HTML. The current page contains a
    # closing </A> for which no opening <A> is present, which causes
    # beautifulsoup to silently stop processing that section from the error
    # onwards. We're not using the href's anyway.
    content = re.sub("<a[^>]*>", "", content, flags=re.IGNORECASE)
    content = re.sub("</a>", "", content, flags=re.IGNORECASE)
    soup = BeautifulSoup(content, 'html.parser')

    IDs = [chr(c + ord('a')) for c in range(ord('z') - ord('a') + 1)]
    IDs.append("0-9-")

    for section_id in IDs:
        section = soup.find('h2', {'id': section_id})
        table = section.find_next_sibling('table')
        if not table: continue

        #print ("table: '{}'".format(table))
        for row in table.findAll('tr'):
            #print("ROW: '{}'".format(row))
            cells = row.findAll('td')
            if not cells:
                continue

            labels = [label for label in cells[1].stripped_strings]

            # pad the code to make sure it is a 4 char string,
            # otherwise eg "CF  " will not be matched to "CF"
            code = cells[0].string.strip()
            code = code + (4 - len(code)) * ' '
            registered_vendor_ids[code] = labels[0]

            # Do the same with NULL-padding:
            code = cells[0].string.strip()
            code = code + (4 - len(code)) * chr(0)
            registered_vendor_ids[code] = labels[0]


    return registered_vendor_ids


def git_rootdir(family_dir):
    if not family_dir:
        return None

    original_dir = os.getcwd()
    root_dir = None
    try:
        import subprocess
        os.chdir(family_dir)
        git_cmd = [
            "git", "rev-parse", "--show-toplevel"
        ]
        git_output = subprocess.check_output(git_cmd, stderr=subprocess.STDOUT)
        root_dir = git_output.decode("utf-8").strip()

    except (OSError, IOError, subprocess.CalledProcessError):
        pass # Not a git repo, or git is not installed.

    os.chdir(original_dir)
    return root_dir


@condition
def licenses(family_directory):
    """Get a list of paths for every license
       file found in a font project."""
    found = []
    search_paths = [family_directory]
    gitroot = git_rootdir(family_directory)
    if gitroot and gitroot not in search_paths:
        search_paths.append(gitroot)

    for directory in search_paths:
        if directory:
            for license in ['OFL.txt', 'LICENSE.txt']:
                license_path = os.path.join(directory, license)
                if os.path.exists(license_path):
                    found.append(license_path)
    return found


@condition
def license_contents(license_path):
    if license_path:
        return open(license_path).read()


@condition
def license_path(licenses):
    """Get license path."""
    # This assumes that a repo can have multiple license files
    # and they're all the same.
    # FIXME: We should have a fontbakery check for that, though!
    if licenses and len(licenses) > 0:
        return licenses[0]


@condition
def license(license_path):
    """Get license filename."""
    if license_path:
        return os.path.basename(license_path)

@condition
def is_ofl(license):
    return license and "OFL" in license


@condition
def familyname(font):
    filename = os.path.basename(font)
    filename_base = os.path.splitext(filename)[0]
    if '-' in filename_base:
        return filename_base.split('-')[0]
    # Handle VFs e.g Inconsolata[wdth,wght] --> Inconsolata
    elif "[" in filename_base:
        return filename_base.split("[")[0]
    else:
        return filename_base


@condition
def hinting_stats(font):
    """
    Return file size differences for a hinted font compared to an dehinted version of same file
    """
    from io import BytesIO
    from dehinter.font import dehint
    from fontTools.ttLib import TTFont
    from fontTools.subset import main as pyftsubset
    from fontbakery.profiles.shared_conditions import (is_ttf,
                                                       is_cff,
                                                       is_cff2)

    hinted_size = os.stat(font).st_size
    ttFont = TTFont(font)

    if is_ttf(ttFont):
        dehinted_buffer = BytesIO()
        dehint(ttFont, verbose=False)
        ttFont.save(dehinted_buffer)
        dehinted_buffer.seek(0)
        dehinted_size = len(dehinted_buffer.read())
    elif is_cff(ttFont) or is_cff2(ttFont):
        ext = os.path.splitext(font)[1]
        tmp = font.replace(ext, "-tmp-dehinted%s" % ext)
        args = [font,
                "--no-hinting",
                "--glyphs=*",
                "--ignore-missing-glyphs",
                "--no-notdef-glyph",
                "--no-recommended-glyphs",
                "--no-layout-closure",
                "--layout-features=*",
                "--no-desubroutinize",
                "--name-languages=*",
                "--glyph-names",
                "--no-prune-unicode-ranges",
                "--output-file=%s" % tmp]
        pyftsubset(args)

        dehinted_size = os.stat(tmp).st_size
        os.remove(tmp)

    else:
        return None

    return {
        "dehinted_size": dehinted_size,
        "hinted_size": hinted_size,
    }


@condition
def listed_on_gfonts_api(familyname):
    if not familyname:
        return False

    from fontbakery.utils import split_camel_case
    # Some families such as "Libre Calson Text" have camelcased filenames
    # ("LibreCalsonText-Regular.ttf") and require us to split in order
    # to find it in the GFonts metadata:
    from_camelcased_name = split_camel_case(familyname)

    for item in production_metadata()["familyMetadataList"]:
        if item["family"] == familyname or \
           item["family"] == from_camelcased_name:
            return True

    return False


@condition
def has_regular_style(family_metadata):
    fonts = family_metadata.fonts if family_metadata else []
    for f in fonts:
        if f.weight == 400 and f.style == "normal":
            return True
    return False


@condition
def font_metadata(family_metadata, font):
    if not family_metadata:
        return

    for f in family_metadata.fonts:
        if font.endswith(f.filename):
            return f


@condition
def font_familynames(ttFont):
    from fontbakery.utils import get_name_entry_strings
    return get_name_entry_strings(ttFont, NameID.FONT_FAMILY_NAME)


@condition
def typographic_familynames(ttFont):
    from fontbakery.utils import get_name_entry_strings
    return get_name_entry_strings(ttFont, NameID.TYPOGRAPHIC_FAMILY_NAME)


@condition
def font_familyname(font_familynames):
    # This assumes that all familyname
    # name table entries are identical.
    # FIX-ME: Maybe we should have a check for that.
    #         Have we ever seen this kind of
    #         problem in the wild, though ?
    return font_familynames[0]


# TODO: Design special case handling mechanism:
# https://github.com/googlefonts/fontbakery/issues/1540
# TODO: Improve camelcase check result explanation and whitelisting process:
#https://github.com/googlefonts/fontbakery/issues/3270
@condition
def camelcased_familyname_exception(familyname):
    '''In general, we would not like to have camel-cased
       familynames but there are a few exceptions to that
       rule, that we keep listed here, for now.
    '''
    for exception in [
        "3D", # seen in "Rock 3D"
        "ABeeZee",
        "BenchNine",
        "BhuTuka", # seen in "BhuTuka Expanded One"
        "DotGothic", # seen in "DotGothic16"
        "FakeFont",
        "JetBrains", # seen in "JetBrains Mono"
        "McLaren",
        "MedievalSharp",
        "RocknRoll", # seen in "RocknRoll One"
        "BIZ UDGothic",
        "BIZ UDPGothic",
        "BIZ UDMincho",
        "BIZ UDPMincho",
        "UnifrakturCook",
        "UnifrakturMaguntia",
        "MonteCarlo",
        "WindSong",
    ]:
        if exception in familyname:
            return True

@condition
def rfn_exception(familyname):
    '''These are the very few font families where we accept usage of
       a Reserved Font Name. These are typically fonts that had already
       been published previously with an RFN, or fonts which benefit from
       an agreement with Google Fonts.
    '''
    RNF_EXCEPTIONS = [] # For now we'll leave this empty because the
                        # actual long list of familynames we had here
                        # still needs to be more carefully reviewed.
                        # See https://github.com/googlefonts/fontbakery/issues/3612

    for exception in RNF_EXCEPTIONS:
        if exception in familyname:
            return True


@condition
def remote_styles(familyname_with_spaces):
    """Get a dictionary of TTFont objects of all font files of
       a given family as currently hosted at Google Fonts.
    """

    def download_family_from_Google_Fonts(familyname):
        """Return a zipfile containing a font family hosted on fonts.google.com"""
        from zipfile import ZipFile
        from fontbakery.utils import download_file
        url_prefix = 'https://fonts.google.com/download?family='
        url = '{}{}'.format(url_prefix, familyname.replace(' ', '+'))
        return ZipFile(download_file(url))  # pylint: disable=R1732


    def fonts_from_zip(zipfile):
        '''return a list of fontTools TTFonts'''
        from fontTools.ttLib import TTFont
        from io import BytesIO
        fonts = []
        for file_name in zipfile.namelist():
            if file_name.lower().endswith(".ttf"):
                file_obj = BytesIO(zipfile.open(file_name).read())
                fonts.append([file_name, TTFont(file_obj)])
        return fonts

    if not listed_on_gfonts_api(familyname_with_spaces):
        return None

    remote_fonts_zip = download_family_from_Google_Fonts(familyname_with_spaces)
    rstyles = {}

    for remote_filename, remote_font in fonts_from_zip(remote_fonts_zip):
        remote_style = os.path.splitext(remote_filename)[0]
        if '-' in remote_style:
            remote_style = remote_style.split('-')[1]
        rstyles[remote_style] = remote_font
    return rstyles


@condition
def regular_remote_style(remote_styles):
    from .shared_conditions import is_variable_font, get_instance_axis_value
    if not remote_styles:
        return None
    if "Regular" in remote_styles:
        return remote_styles["Regular"]

    for style, font in remote_styles.items():
        if is_variable_font(font):
            if get_instance_axis_value(font, "Regular", "wght"):
                return font
    return list(remote_styles.items())[0][1]


@condition
def regular_ttFont(ttFonts):
    from .shared_conditions import is_variable_font, get_instance_axis_value
    for ttFont in ttFonts:
        if "-Regular." in os.path.basename(ttFont.reader.file.name):
            return ttFont
        nametable = ttFont['name']
        sub_family = nametable.getName(NameID.FONT_SUBFAMILY_NAME,
                                       PlatformID.WINDOWS,
                                       UnicodeEncodingID.UNICODE_1_1,
                                       WindowsLanguageID.ENGLISH_USA)
        typo_sub_family = nametable.getName(NameID.TYPOGRAPHIC_SUBFAMILY_NAME,
                                            PlatformID.WINDOWS,
                                            UnicodeEncodingID.UNICODE_1_1,
                                            WindowsLanguageID.ENGLISH_USA)
        if sub_family and sub_family.toUnicode() == "Regular" and not typo_sub_family:
            return ttFont
        if is_variable_font(ttFont):
            if get_instance_axis_value(ttFont, "Regular", "wght"):
                return ttFont
    return None


@condition
def api_gfonts_ttFont(style, remote_styles):
    """Get a TTFont object of a font downloaded from Google Fonts
       corresponding to the given TTFont object of
       a local font being checked.
    """
    if remote_styles and style in remote_styles:
        return remote_styles[style]


@condition
def github_gfonts_ttFont(ttFont, license):
    """Get a TTFont object of a font downloaded
       from Google Fonts git repository.
    """
    if not license:
        return None

    from fontbakery.utils import download_file
    from fontTools.ttLib import TTFont
    from urllib.request import HTTPError
    LICENSE_DIRECTORY = {
        "OFL.txt": "ofl",
        "UFL.txt": "ufl",
        "LICENSE.txt": "apache"
    }
    filename = os.path.basename(ttFont.reader.file.name)
    familyname = filename.split('-')[0].lower()
    url = (f"https://github.com/google/fonts/raw/main"
           f"/{LICENSE_DIRECTORY[license]}/{familyname}/{filename}")
    try:
        fontfile = download_file(url)
        if fontfile:
            return TTFont(fontfile)
    except HTTPError:
        return None


@condition
def github_gfonts_description(ttFont, license):
    """Get the contents of the DESCRIPTION.en_us.html file
       from the google/fonts github repository corresponding
       to a given ttFont.
    """
    if not license:
        return None

    from fontbakery.utils import download_file
    from urllib.request import HTTPError
    LICENSE_DIRECTORY = {
        "OFL.txt": "ofl",
        "UFL.txt": "ufl",
        "LICENSE.txt": "apache"
    }
    filename = os.path.basename(ttFont.reader.file.name)
    familyname = filename.split('-')[0].lower()
    url = (f"https://github.com/google/fonts/raw/main"
           f"/{LICENSE_DIRECTORY[license]}/{familyname}/DESCRIPTION.en_us.html")
    try:
        descfile = download_file(url)
        if descfile:
            return descfile.read().decode('UTF-8')
    except HTTPError:
        return None


@condition
def familyname_with_spaces(familyname):
    """Attempts to build family name from font name.
    For example, HPSimplifiedSans => HP Simplified Sans.
    Args:
      familyname: The name of a font.
    Returns:
      The name of the family that should be in this font.
    """
    # SomethingUpper => Something Upper
    familyname = re.sub('(.)([A-Z][a-z]+)', r'\1 \2', familyname)
    # Font3 => Font 3
    familyname = re.sub('([a-z])([0-9]+)', r'\1 \2', familyname)
    # lookHere => look Here
    familyname = re.sub('([a-z0-9])([A-Z])', r'\1 \2', familyname)

    def of_special_case(s):
        """Special case for family names such as
           MountainsofChristmas which would need to
           have the "of" split apart from "Mountains".

           See also: https://github.com/googlefonts/fontbakery/issues/1489
           "Failure to handle font family with 3 words in it"
        """
        # TODO (M Foley) this case is insane. We should just fix the fonts
        # and drop this function
        if s[-2:] == "of":
            return s[:-2] + " of"
        else:
            return s

    familyname = " ".join(map(of_special_case, familyname.split(" ")))
    return familyname


@condition
def VTT_hinted(ttFont):
    # it seems that VTT is the only program that generates a TSI5 table
    return 'TSI5' in ttFont


@condition
def is_hinted(ttFont):
    return "fpgm" in ttFont


@condition
def gfonts_repo_structure(fonts):
    """ The family at the given font path
        follows the files and directory structure
        typical of a font project hosted on
        the Google Fonts repo on GitHub? """
    from fontbakery.utils import get_absolute_path

    # FIXME: Improve this with more details
    #        about the expected structure.
    abspath = get_absolute_path(fonts[0])
    return abspath.split(os.path.sep)[-3] in ["ufl", "ofl", "apache"]


@condition
def production_metadata():
    """Get the Google Fonts production metadata"""
    import requests
    meta_url = "http://fonts.google.com/metadata/fonts"
    return requests.get(meta_url).json()


@condition
def GFAxisRegistry():
    from axisregistry import AxisRegistry
    return AxisRegistry()


@condition
def upstream_yaml(family_directory):
    fp = os.path.join(family_directory, "upstream.yaml")
    if not os.path.isfile(fp):
        return None
    return yaml.load(open(fp, "r"), yaml.FullLoader)


# Lib/fontbakery/profiles/gpos.py

from fontbakery.callable import check, condition
from fontbakery.status import PASS, WARN
from fontbakery.message import Message

# used to inform get_module_profile whether and how to create a profile
from fontbakery.fonts_profile import profile_factory # NOQA pylint: disable=unused-import


@condition
def has_kerning_info(ttFont):
    """A font has kerning info if it has a GPOS table containing at least one
    Pair Adjustment lookup (either directly or through an extension
    subtable)."""
    if "GPOS" not in ttFont:
        return False

    if not ttFont["GPOS"].table.LookupList:
        return False

    for lookup in ttFont["GPOS"].table.LookupList.Lookup:
        if lookup.LookupType == 2:  # type 2 = Pair Adjustment
            return True
        elif lookup.LookupType == 9:  # type 9 = Extension subtable
            for ext in lookup.SubTable:
                if ext.ExtensionLookupType == 2:  # type 2 = Pair Adjustment
                    return True


@check(
    id = 'com.google.fonts/check/gpos_kerning_info',
    proposal = 'legacy:check/063'
)
def com_google_fonts_check_gpos_kerning_info(ttFont):
    """Does GPOS table have kerning information?
    This check skips monospaced fonts as defined by post.isFixedPitch value
    """
    if ttFont["post"].isFixedPitch == 0 and not has_kerning_info(ttFont):
        yield WARN,\
              Message("lacks-kern-info",
                      "GPOS table lacks kerning information.")
    else:
        yield PASS, "GPOS table check for kerning information passed."


# Lib/fontbakery/profiles/head.py

import fractions
from fontbakery.callable import check
from fontbakery.status import FAIL, PASS, WARN
from fontbakery.message import Message
from fontbakery.constants import NameID

# used to inform get_module_profile whether and how to create a profile
from fontbakery.fonts_profile import profile_factory # NOQA pylint: disable=unused-import

@check(
    id = 'com.google.fonts/check/family/equal_font_versions',
    proposal = 'legacy:check/014'
)
def com_google_fonts_check_family_equal_font_versions(ttFonts):
    """Make sure all font files have the same version value."""
    all_detected_versions = []
    fontfile_versions = {}
    for ttFont in ttFonts:
        v = ttFont['head'].fontRevision
        fontfile_versions[ttFont] = v

        if v not in all_detected_versions:
            all_detected_versions.append(v)
    if len(all_detected_versions) != 1:
        versions_list = ""
        for v in fontfile_versions.keys():
            versions_list += "* {}: {}\n".format(v.reader.file.name,
                                                 fontfile_versions[v])
        yield WARN,\
              Message("mismatch",
                      f"Version info differs among font"
                      f" files of the same font project.\n"
                      f"These were the version values found:\n"
                      f"{versions_list}")
    else:
        yield PASS, "All font files have the same version."


@check(
    id = 'com.google.fonts/check/unitsperem',
    rationale = """
        According to the OpenType spec:

        The value of unitsPerEm at the head table must be a value
        between 16 and 16384. Any value in this range is valid.

        In fonts that have TrueType outlines, a power of 2 is recommended
        as this allows performance optimizations in some rasterizers.

        But 1000 is a commonly used value. And 2000 may become
        increasingly more common on Variable Fonts.
    """,
    proposal = 'legacy:check/043'
)
def com_google_fonts_check_unitsperem(ttFont):
    """Checking unitsPerEm value is reasonable."""
    upem = ttFont['head'].unitsPerEm
    target_upem = [2**i for i in range(4, 15)]
    target_upem.append(1000)
    target_upem.append(2000)
    if upem < 16 or upem > 16384:
        yield FAIL,\
              Message("out-of-range",
                      f"The value of unitsPerEm at the head table"
                      f" must be a value between 16 and 16384."
                      f" Got {upem} instead.")
    elif upem not in target_upem:
        yield WARN,\
              Message("suboptimal",
                      f"In order to optimize performance on some"
                      f" legacy renderers, the value of unitsPerEm"
                      f" at the head table should idealy be"
                      f" a power of between 16 to 16384."
                      f" And values of 1000 and 2000 are also"
                      f" common and may be just fine as well."
                      f" But we got {upem} instead.")
    else:
        yield PASS, (f"The unitsPerEm value ({upem}) on"
                     f" the 'head' table is reasonable.")


def parse_version_string(name: str) -> float:
    """Parse a version string (name ID 5) as a decimal and
    return as fractions.Fraction.

    Example of the expected format: 'Version 01.003; Comments'. Version
    strings like "Version 1.300" will be post-processed into
    Fraction(13, 10).
    The parsed version numbers will therefore match as numbers, but not
    necessarily in string form.
    """
    import re

    # We assume ";" is the universal delimiter here.
    version_entry = name.split(";")[0]

    # Catch both "Version 1.234" and "1.234" but not "1x2.34". Note: search()
    # will return the first match.
    version_string = re.search(r"(?: |^)(\d+\.\d+)", version_entry)

    if version_string is None:
        raise ValueError("The version string didn't contain a"
                         " number of the format `major.minor`.")

    return fractions.Fraction(version_string.group(1))


@check(
    id = 'com.google.fonts/check/font_version',
    proposal = 'legacy:check/044'
)
def com_google_fonts_check_font_version(ttFont):
    """Checking font version fields (head and name table)."""

    # Get font version from the head table as an exact Fraction.
    head_version = fractions.Fraction(ttFont["head"].fontRevision)

    # 1/0x10000 is the best achievable when converting a decimal
    # to 16.16 Fixed point.
    warn_tolerance = 1/0x10000
    # Some tools aren't that accurate and only care to do a
    # conversion to 3 decimal places.
    # 1/2000 is the tolerance for accepting equality to 3 decimal places.
    fail_tolerance = fractions.Fraction(1, 2000)

    # Compare the head version against the name ID 5 strings in all name records.
    name_id_5_records = [
        record for record in ttFont["name"].names
        if record.nameID == NameID.VERSION_STRING
    ]

    failed = False
    if name_id_5_records:
        for record in name_id_5_records:
            try:
                name_version = parse_version_string(record.toUnicode())
                if abs(name_version - head_version) > fail_tolerance:
                    failed = True
                    yield FAIL,\
                          Message("mismatch",
                                  f'head version is "{float(head_version):.5f}"'
                                  f' while name version string (for'
                                  f' platform {record.platformID},'
                                  f' encoding {record.platEncID}) is'
                                  f' "{record.toUnicode()}".')
                elif abs(name_version - head_version) > warn_tolerance:
                    yield WARN,\
                          Message("near-mismatch",
                                  f'head version is "{float(head_version):.5f}"'
                                  f' while name version string (for'
                                  f' platform {record.platformID},'
                                  f' encoding {record.platEncID}) is'
                                  f' "{record.toUnicode()}".'
                                  f' This matches to 3 decimal places, but'
                                  f' is not as accurate as possible.')
            except ValueError:
                failed = True
                yield FAIL,\
                      Message("parse",
                              f'name version string for'
                              f' platform {record.platformID},'
                              f' encoding {record.platEncID}'
                              f' ("{record.toUnicode()}"),'
                              f' could not be parsed.')
    else:
        failed = True
        yield FAIL,\
              Message("missing",
                      "There is no name ID 5 (version string) in the font.")

    if not failed:
        yield PASS, "All font version fields match."


# Lib/fontbakery/profiles/hhea.py

from fontbakery.callable import check
from fontbakery.status import FAIL, PASS, SKIP, WARN
from fontbakery.message import Message
# used to inform get_module_profile whether and how to create a profile
from fontbakery.fonts_profile import profile_factory # NOQA pylint: disable=unused-import

profile_imports = [
    ('.shared_conditions', ('glyph_metrics_stats', 'is_ttf'))
]

@check(
    id = 'com.google.fonts/check/linegaps',
    proposal = 'legacy:check/041'
)
def com_google_fonts_check_linegaps(ttFont):
    """Checking Vertical Metric Linegaps."""
    if ttFont["hhea"].lineGap != 0:
        yield WARN,\
              Message("hhea",
                      "hhea lineGap is not equal to 0.")
    elif ttFont["OS/2"].sTypoLineGap != 0:
        yield WARN,\
              Message("OS/2",
                      "OS/2 sTypoLineGap is not equal to 0.")
    else:
        yield PASS, "OS/2 sTypoLineGap and hhea lineGap are both 0."


@check(
    id = 'com.google.fonts/check/maxadvancewidth',
    proposal = 'legacy:check/073'
)
def com_google_fonts_check_maxadvancewidth(ttFont):
    """MaxAdvanceWidth is consistent with values in the Hmtx and Hhea tables?"""
    hhea_advance_width_max = ttFont['hhea'].advanceWidthMax
    hmtx_advance_width_max = None
    for g in ttFont['hmtx'].metrics.values():
        if hmtx_advance_width_max is None:
            hmtx_advance_width_max = max(0, g[0])
        else:
            hmtx_advance_width_max = max(g[0], hmtx_advance_width_max)

    if hmtx_advance_width_max != hhea_advance_width_max:
        yield FAIL,\
              Message("mismatch",
                      f"AdvanceWidthMax mismatch:"
                      f" expected {hmtx_advance_width_max} (from hmtx);"
                      f" got {hhea_advance_width_max} (from hhea)")
    else:
        yield PASS, ("MaxAdvanceWidth is consistent"
                     " with values in the Hmtx and Hhea tables.")


# Lib/fontbakery/profiles/hmtx.py

from fontbakery.callable import check
from fontbakery.status import FAIL, PASS
from fontbakery.message import Message
# used to inform get_module_profile whether and how to create a profile
from fontbakery.fonts_profile import profile_factory  # NOQA pylint: disable=unused-import

profile_imports = [('.shared_conditions', ('missing_whitespace_chars',))]


@check(
    id = 'com.google.fonts/check/whitespace_widths',
    conditions = ['not missing_whitespace_chars'],
    proposal = 'legacy:check/050'
)
def com_google_fonts_check_whitespace_widths(ttFont):
    """Space and non-breaking space have the same width?"""
    from fontbakery.utils import get_glyph_name

    space_name = get_glyph_name(ttFont, 0x0020)
    nbsp_name = get_glyph_name(ttFont, 0x00A0)

    space_width = ttFont['hmtx'][space_name][0]
    nbsp_width = ttFont['hmtx'][nbsp_name][0]

    if space_width > 0 and space_width == nbsp_width:
        yield PASS, "Space and non-breaking space have the same width."
    else:
        yield FAIL,\
              Message("different-widths",
                      f"Space and non-breaking space have differing width:"
                      f" The space glyph named {space_name}"
                      f" is {space_width} font units wide,"
                      f" non-breaking space named ({nbsp_name})"
                      f" is {nbsp_width} font units wide, and"
                      f" both should be positive and the same."
                      f" GlyphsApp has \"Sidebearing arithmetic\""
                      f" (https://glyphsapp.com/tutorials/spacing)"
                      f" which allows you to set the non-breaking"
                      f" space width to always equal the space width.")


# Lib/fontbakery/profiles/iso15008.py

"""
Checks for suitability for in-car displays (ISO 15008).
"""

from fontbakery.callable import check, condition
from fontbakery.section import Section
from fontbakery.status import PASS, FAIL, WARN
from fontbakery.fonts_profile import profile_factory
from fontbakery.message import Message
from fontTools.pens.boundsPen import BoundsPen
from beziers.path import BezierPath
from beziers.line import Line
from beziers.point import Point
import beziers
import uharfbuzz as hb


profile = profile_factory(default_section=Section("Suitability for In-Car Display"))

DISCLAIMER = """
        (Note that passing this check does not guarantee compliance with ISO-15008.)
"""

CHECKS = [
    "com.google.fonts/check/iso15008_proportions",
    "com.google.fonts/check/iso15008_stem_width",
    "com.google.fonts/check/iso15008_intercharacter_spacing",
    "com.google.fonts/check/iso15008_interword_spacing",
    "com.google.fonts/check/iso15008_interline_spacing",
]


def xheight_intersections(ttFont, glyph):
    glyphset = ttFont.getGlyphSet()
    if glyph not in glyphset:
        return []

    paths = BezierPath.fromFonttoolsGlyph(ttFont, glyph)
    if len(paths) != 1:
        return []
    path = paths[0]

    xheight = ttFont["OS/2"].sxHeight

    bounds = path.bounds()
    bounds.addMargin(10)
    ray = Line(Point(bounds.left, xheight), Point(bounds.right, xheight))
    intersections = []
    for seg in path.asSegments():
        intersections.extend(seg.intersections(ray))
    return sorted(intersections, key=lambda i: i.point.x)


def stem_width(ttFont):
    glyphset = ttFont.getGlyphSet()
    if "l" not in glyphset:
        return None

    intersections = xheight_intersections(ttFont, "l")
    if len(intersections) != 2:
        return None
    (i1, i2) = intersections[0:2]
    return abs(i1.point.x - i2.point.x)


def pair_kerning(font, left, right):
    """The kerning between two glyphs (specified by name), in font units."""
    with open(font, "rb") as fontfile:
        fontdata = fontfile.read()
    face = hb.Face(fontdata)
    font = hb.Font(face)
    scale = face.upem
    font.scale = (scale, scale)
    buf = hb.Buffer()
    buf.add_str(left + right)
    buf.guess_segment_properties()
    hb.shape(font, buf, {"kern": True})
    pos = buf.glyph_positions[0].x_advance
    buf = hb.Buffer()
    buf.add_str(left + right)
    buf.guess_segment_properties()
    hb.shape(font, buf, {"kern": False})
    pos2 = buf.glyph_positions[0].x_advance
    return pos - pos2


@check(
    id = 'com.google.fonts/check/iso15008_proportions',
    rationale = """
        According to ISO 15008, fonts used for in-car displays should not be
        too narrow or too wide.
        
        To ensure legibility of this font on in-car information systems,
        it is recommended that the ratio of H width to H height
        is between 0.65 and 0.80.
    """ + DISCLAIMER,
    proposal = ['https://github.com/googlefonts/fontbakery/issues/1832',
                'https://github.com/googlefonts/fontbakery/issues/3250']
)
def com_google_fonts_check_iso15008_proportions(ttFont):
    """Check if 0.65 => (H width / H height) => 0.80"""
    glyphset = ttFont.getGlyphSet()
    if "H" not in glyphset:
        yield FAIL,\
              Message('glyph-not-present',
                      "There was no 'H' glyph in the font,"
                      " so the proportions could not be tested")

    h_glyph = glyphset["H"]
    pen = BoundsPen(glyphset)
    h_glyph._glyph.draw(pen, ttFont.get("glyf"))
    (xMin, yMin, xMax, yMax) = pen.bounds
    proportion = (xMax - xMin) / (yMax - yMin)
    if 0.65 <= proportion <= 0.80:
        yield PASS, "the letter H is not too narrow or too wide"
    else:
        yield FAIL,\
              Message('invalid-proportion',
                      f"The proportion of H width to H height ({proportion})"
                      f"does not conform to the expected range of 0.65-0.80")


@check(
    id = 'com.google.fonts/check/iso15008_stem_width',
    rationale = """
        According to ISO 15008, fonts used for in-car displays should
        not be too light or too bold.
        
        To ensure legibility of this font on in-car information systems,
        it is recommended that the ratio of stem width to ascender height
        is between 0.10 and 0.20.
    """ + DISCLAIMER,
    proposal = ['https://github.com/googlefonts/fontbakery/issues/1832',
                'https://github.com/googlefonts/fontbakery/issues/3251']
)
def com_google_fonts_check_iso15008_stem_width(ttFont):
    """Check if 0.10 <= (stem width / ascender) <= 0.82"""
    width = stem_width(ttFont)
    if width is None:
        yield FAIL,\
              Message('no-stem-width',
                      "Could not determine stem width")
        return
    ascender = ttFont["hhea"].ascender
    proportion = width / ascender
    if 0.10 <= proportion <= 0.20:
        yield PASS, "the stem width is not too light or too bold"
    else:
        yield FAIL,\
              Message('invalid-proportion',
                      f"The proportion of stem width to ascender ({proportion})"
                      f"does not conform to the expected range of 0.10-0.20")


@check(
    id = 'com.google.fonts/check/iso15008_intercharacter_spacing',
    rationale = """
        According to ISO 15008, fonts used for in-car displays should not
        be too narrow or too wide.
        
        To ensure legibility of this font on in-car information systems,
        it is recommended that the spacing falls within the following values:
        
        * space between vertical strokes (e.g. "ll") should be 150%-240%
          of the stem width.
        
        * space between diagonals and verticals (e.g. "vl") should be
          at least 85% of the stem width.
        
        * diagonal characters should not touch (e.g. "vv").
    """ + DISCLAIMER,
    proposal = ['https://github.com/googlefonts/fontbakery/issues/1832',
                'https://github.com/googlefonts/fontbakery/issues/3252']
)
def com_google_fonts_check_iso15008_intercharacter_spacing(font, ttFont):
    """Check if spacing between characters is adequate for display use"""
    width = stem_width(ttFont)

    # Because an l can have a curly tail, we don't want the *glyph* sidebearings;
    # we want the sidebearings measured using a line at Y=x-height.
    l_intersections = xheight_intersections(ttFont, "l")
    if width is None or len(l_intersections) < 2:
        yield FAIL, Message("no-stem-width", "Could not determine stem width")
        return

    l_lsb = l_intersections[0].point.x
    l_advance = ttFont["hmtx"]["l"][0]
    l_rsb = l_advance - l_intersections[-1].point.x

    l_l = l_rsb + pair_kerning(font, "l", "l") + l_lsb
    if l_l is None:
        yield FAIL,\
              Message('glyph-not-present',
                      "There was no 'l' glyph in the font,"
                      " so the spacing could not be tested")
        return
    if 1.5 <= (l_l / width) <= 2.4:
        yield PASS, "Distance between vertical strokes was adequate"
    else:
        yield FAIL,\
              Message('bad-vertical-vertical-spacing',
                      f"The space between vertical strokes ({l_l})"
                      f" does not conform to the expected"
                      f" range of {width * 1.5}-{width * 2.4}")

    # For v, however, a simple LSB/RSB is adequate.
    glyphset = ttFont.getGlyphSet()
    h_glyph = glyphset["v"]
    pen = BoundsPen(glyphset)
    h_glyph._glyph.draw(pen, ttFont.get("glyf"))
    (xMin, yMin, xMax, yMax) = pen.bounds
    v_advance = ttFont["hmtx"]["v"][0]

    v_lsb = xMin
    v_rsb = v_advance - (v_lsb + xMax - xMin)

    l_v = l_rsb + pair_kerning(font, "l", "v") + v_lsb

    if l_v is None:
        yield FAIL,\
              Message('glyph-not-present',
                      "There was no 'v' glyph in the font,"
                      " so the spacing could not be tested")
        return

    if (l_v / width) > 0.85:
        yield PASS, "Distance between vertical and diagonal strokes was adequate"
    else:
        yield FAIL,\
              Message('bad-vertical-diagonal-spacing',
                      f"The space between vertical and diagonal strokes ({l_v})"
                      f" was less than the expected"
                      f" value of {width * 0.85}")

    v_v = v_rsb + pair_kerning(font, "v", "v") + v_lsb
    if v_v > 0:
        yield PASS, "Distance between diagonal strokes was adequate"
    else:
        yield FAIL,\
              Message('bad-diagonal-diagonal-spacing',
                      "Diagonal strokes (vv) were touching")


@check(
    id = 'com.google.fonts/check/iso15008_interword_spacing',
    rationale = """
        According to ISO 15008, fonts used for in-car displays
        should not be too narrow or too wide.
        
        To ensure legibility of this font on in-car information systems,
        it is recommended that the space character should have advance width
        between 250% and 300% of the space between the letters l and m.
    """ + DISCLAIMER,
    proposal = ['https://github.com/googlefonts/fontbakery/issues/1832',
                'https://github.com/googlefonts/fontbakery/issues/3253']
)
def com_google_fonts_check_iso15008_interword_spacing(font, ttFont):
    """Check if spacing between words is adequate for display use"""
    l_intersections = xheight_intersections(ttFont, "l")
    if len(l_intersections) < 2:
        yield FAIL,\
              Message('glyph-not-present',
                      "There was no 'l' glyph in the font,"
                      " so the spacing could not be tested")
        return

    l_advance = ttFont["hmtx"]["l"][0]
    l_rsb = l_advance - l_intersections[-1].point.x

    glyphset = ttFont.getGlyphSet()
    h_glyph = glyphset["m"]
    pen = BoundsPen(glyphset)
    h_glyph._glyph.draw(pen, ttFont.get("glyf"))
    (xMin, yMin, xMax, yMax) = pen.bounds
    m_advance = ttFont["hmtx"]["m"][0]
    m_lsb = xMin
    m_rsb = m_advance - (m_lsb + xMax - xMin)

    n_lsb = ttFont["hmtx"]["n"][1]

    l_m = l_rsb + pair_kerning(font, "l", "m") + m_lsb
    space_width = ttFont["hmtx"]["space"][0]
    # Add spacing caused by normal sidebearings
    space_width += m_rsb + n_lsb

    if 2.50 <= space_width / l_m <= 3.0:
        yield PASS, "Advance width of interword space was adequate"
    else:
        yield FAIL,\
              Message('bad-interword-spacing',
                      f"The interword space ({space_width}) was"
                      f" outside the recommended range ({l_m*2.5}-{l_m*3.0})")


@check(
    id = 'com.google.fonts/check/iso15008_interline_spacing',
    rationale = """
        According to ISO 15008, fonts used for in-car displays
        should not be too narrow or too wide.
        
        To ensure legibility of this font on in-car information systems,
        it is recommended that the vertical metrics be set to a minimum
        at least one stem width between the bottom of the descender
        and the top of the ascender.
    """ + DISCLAIMER,
    proposal = ['https://github.com/googlefonts/fontbakery/issues/1832',
                'https://github.com/googlefonts/fontbakery/issues/3254']
)
def com_google_fonts_check_iso15008_interline_spacing(ttFont):
    """Check if spacing between lines is adequate for display use"""
    glyphset = ttFont.getGlyphSet()
    if "h" not in glyphset or "g" not in glyphset:
        yield FAIL,\
              Message('glyph-not-present',
                      "There was no 'g'/'h' glyph in the font,"
                      " so the spacing could not be tested")
        return

    h_glyph = glyphset["h"]
    pen = BoundsPen(glyphset)
    h_glyph._glyph.draw(pen, ttFont.get("glyf"))
    (_, _, _, h_yMax) = pen.bounds

    g_glyph = glyphset["g"]
    pen = BoundsPen(glyphset)
    g_glyph._glyph.draw(pen, ttFont.get("glyf"))
    (_, g_yMin, _, _) = pen.bounds

    linegap = (
        (g_yMin - ttFont["OS/2"].sTypoDescender)
        + ttFont["OS/2"].sTypoLineGap
        + (ttFont["OS/2"].sTypoAscender - h_yMax)
    )
    width = stem_width(ttFont)
    if width is None:
        yield FAIL,\
              Message('no-stem-width',
                      "Could not determine stem width")
        return
    if linegap < width:
        yield FAIL,\
              Message('bad-interline-spacing',
                      f"The interline space {linegap} should"
                      f" be more than the stem width {width}")
        return
    yield PASS, "Amount of interline space was adequate"


profile.auto_register(globals())


profile.test_expected_checks(CHECKS, exclusive=True)


# Lib/fontbakery/profiles/kern.py

from fontbakery.callable import check
from fontbakery.status import FAIL, INFO, PASS, WARN
from fontbakery.message import Message
# used to inform get_module_profile whether and how to create a profile
from fontbakery.fonts_profile import profile_factory # NOQA pylint: disable=unused-import

@check(
    id = 'com.google.fonts/check/kern_table',
    rationale = """
        Even though all fonts should have their kerning implemented in the GPOS table,
        there may be kerning info at the kern table as well.

        Some applications such as MS PowerPoint require kerning info on the kern table.
        More specifically, they require a format 0 kern subtable from a kern table
        version 0 with only glyphs defined in the cmap table, which is the only one
        that Windows understands (and which is also the simplest and more limited
        of all the kern subtables).

        Google Fonts ingests fonts made for download and use on desktops, and does
        all web font optimizations in the serving pipeline (using libre libraries
        that anyone can replicate.)

        Ideally, TTFs intended for desktop users (and thus the ones intended for
        Google Fonts) should have both KERN and GPOS tables.

        Given all of the above, we currently treat kerning on a v0 kern table
        as a good-to-have (but optional) feature.
    """,
    proposal = ['legacy:check/066',
                'https://github.com/googlefonts/fontbakery/issues/1675',
                'https://github.com/googlefonts/fontbakery/issues/3148']
)
def com_google_fonts_check_kern_table(ttFont):
    """Is there a usable "kern" table declared in the font?"""

    kern = ttFont.get("kern")
    if kern:
        cmap = set(ttFont.getBestCmap().values())
        nonCharacterGlyphs = set()
        for kernTable in kern.kernTables:
            if kernTable.format == 0:
                for leftGlyph, rightGlyph in kernTable.kernTable.keys():
                    if leftGlyph not in cmap:
                        nonCharacterGlyphs.add(leftGlyph)
                    if rightGlyph not in cmap:
                        nonCharacterGlyphs.add(rightGlyph)
        if all(kernTable.format != 0 for kernTable in kern.kernTables):
          yield WARN,\
                Message("kern-unknown-format",
                        'The "kern" table does not have any format-0 subtable '
                        'and will not work in a few programs that may require '
                        'the table.')
        elif nonCharacterGlyphs:
          yield FAIL,\
                Message("kern-non-character-glyphs",
                        'The following glyphs should not be used in the "kern" '
                        'table because they are not in the "cmap" table: %s'
                        % ', '.join(sorted(nonCharacterGlyphs)))
        else:
          yield INFO,\
                Message("kern-found",
                        'Only a few programs may require the kerning'
                        ' info that this font provides on its "kern" table.')
    else:
        yield PASS, 'Font does not declare an optional "kern" table.'


# Lib/fontbakery/profiles/layout.py

from fontbakery.callable import check
from fontbakery.status import PASS, FAIL
from fontbakery.message import Message
from fontbakery.fonts_profile import profile_factory # NOQA pylint: disable=unused-import


from opentypespec.tags import FEATURE_TAGS, SCRIPT_TAGS, LANGUAGE_TAGS


def feature_tags(ttFont):
    in_this_font = set()
    for table in ["GSUB", "GPOS"]:
        if ttFont.get(table) and ttFont[table].table.FeatureList:
            for fr in ttFont[table].table.FeatureList.FeatureRecord:
                in_this_font.add(fr.FeatureTag)
    return in_this_font


DEPRECATED_TAGS = ["hngl", "opbd", "size"]


@check(
    id = "com.google.fonts/check/layout_valid_feature_tags",
    rationale = """
        Incorrect tags can be indications of typos, leftover debugging code or
        questionable approaches, or user error in the font editor. Such typos can
        cause features and language support to fail to work as intended.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/issues/3355',
    severity = 8
)
def com_google_fonts_check_layout_valid_feature_tags(ttFont):
    """Does the font have any invalid feature tags?"""

    # We'll accept any of the OpenType specified feature tags:
    acceptable_tags = list(FEATURE_TAGS.keys())

    # And whoever is using these clearly know what they're doing:
    acceptable_tags += ['HARF', 'BUZZ']

    bad_tags = set()
    for tag in feature_tags(ttFont):
        if tag not in acceptable_tags:
            bad_tags.add(tag)
    if bad_tags:
        yield FAIL, \
              Message("bad-feature-tags",
                      "The following invalid feature tags were found in the font: "
                      + ", ".join(bad_tags))
    else:
        yield PASS, "No invalid feature tags were found"


def script_tags(ttFont):
    in_this_font = set()
    for table in ["GSUB", "GPOS"]:
        if ttFont.get(table) and ttFont[table].table.ScriptList:
            for fr in ttFont[table].table.ScriptList.ScriptRecord:
                in_this_font.add(fr.ScriptTag)
    return in_this_font


@check(
    id = "com.google.fonts/check/layout_valid_script_tags",
    rationale = """
        Incorrect script tags can be indications of typos, leftover debugging code
        or questionable approaches, or user error in the font editor. Such typos can
        cause features and language support to fail to work as intended.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/issues/3355',
    severity = 8
)
def com_google_fonts_check_layout_valid_script_tags(ttFont):
    """Does the font have any invalid script tags?"""
    bad_tags = set()
    for tag in script_tags(ttFont):
        if tag not in SCRIPT_TAGS.keys():
            bad_tags.add(tag)
    if bad_tags:
        yield FAIL, \
              Message("bad-script-tags",
                      "The following invalid script tags were found in the font: "
                      + ", ".join(bad_tags))
    else:
        yield PASS, "No invalid script tags were found"


def language_tags(ttFont):
    in_this_font = set()
    for table in ["GSUB", "GPOS"]:
        if ttFont.get(table) and ttFont[table].table.ScriptList:
            for fr in ttFont[table].table.ScriptList.ScriptRecord:
                for lsr in fr.Script.LangSysRecord:
                    in_this_font.add(lsr.LangSysTag)
    return in_this_font


@check(
    id = "com.google.fonts/check/layout_valid_language_tags",
    rationale = """
        Incorrect language tags can be indications of typos, leftover debugging code
        or questionable approaches, or user error in the font editor. Such typos can
        cause features and language support to fail to work as intended.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/issues/3355',
    severity = 8
)
def com_google_fonts_check_layout_valid_language_tags(ttFont):
    """Does the font have any invalid language tags?"""
    bad_tags = set()
    for tag in language_tags(ttFont):
        if tag not in LANGUAGE_TAGS.keys():
            bad_tags.add(tag)
    if bad_tags:
        yield FAIL, \
              Message("bad-language-tags",
                      "The following invalid language tags were found in the font: "
                      + ", ".join(bad_tags))
    else:
        yield PASS, "No invalid language tags were found"


# Lib/fontbakery/profiles/loca.py

from fontbakery.callable import check
from fontbakery.status import PASS, FAIL
from fontbakery.message import Message
# used to inform get_module_profile whether and how to create a profile
from fontbakery.fonts_profile import profile_factory # NOQA pylint: disable=unused-import

profile_imports = (
    ('.', ('shared_conditions', )),
)

@check(
    id = 'com.google.fonts/check/loca/maxp_num_glyphs',
    conditions = ['is_ttf'],
    proposal = 'legacy:check/180'
)
def com_google_fonts_check_loca_maxp_num_glyphs(ttFont):
    """Does the number of glyphs in the loca table match the maxp table?"""
    if len(ttFont['loca']) < (ttFont['maxp'].numGlyphs + 1):
        yield FAIL,\
              Message("corrupt",
                      'Corrupt "loca" table or wrong numGlyphs in "maxp" table.')
    else:
        yield PASS, "'loca' table matches numGlyphs in 'maxp' table."


# Lib/fontbakery/profiles/name.py

from fontbakery.callable import check
from fontbakery.status import ERROR, FAIL, PASS, WARN, INFO
from fontbakery.message import Message
from fontbakery.constants import (NameID,
                                  PlatformID,
                                  WindowsEncodingID,
                                  WindowsLanguageID)
# used to inform get_module_profile whether and how to create a profile
from fontbakery.fonts_profile import profile_factory # NOQA pylint: disable=unused-import

profile_imports = [
    ('.shared_conditions', ('glyph_metrics_stats', 'is_ttf', 'is_cff'))
]


@check(
    id = 'com.adobe.fonts/check/name/empty_records',
    rationale = """
        Check the name table for empty records,
        as this can cause problems in Adobe apps.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/pull/2369'
)
def com_adobe_fonts_check_name_empty_records(ttFont):
    """Check name table for empty records."""
    failed = False
    for name_record in ttFont['name'].names:
        name_string = name_record.toUnicode().strip()
        if len(name_string) == 0:
            failed = True
            name_key = tuple([name_record.platformID,
                              name_record.platEncID,
                              name_record.langID,
                              name_record.nameID])
            yield FAIL,\
                  Message("empty-record",
                          f'"name" table record with key={name_key} is'
                          f' empty and should be removed.')
    if not failed:
        yield PASS, ("No empty name table records found.")


@check(
    id = 'com.google.fonts/check/name/no_copyright_on_description',
    proposal = 'legacy:check/031'
)
def com_google_fonts_check_name_no_copyright_on_description(ttFont):
    """Description strings in the name table must not contain copyright info."""
    failed = False
    for name in ttFont['name'].names:
        if 'opyright' in name.string.decode(name.getEncoding())\
           and name.nameID == NameID.DESCRIPTION:
            failed = True

    if failed:
        yield FAIL,\
              Message("copyright-on-description",
                      f"Some namerecords with"
                      f" ID={NameID.DESCRIPTION} (NameID.DESCRIPTION)"
                      f" containing copyright info should be removed"
                      f" (perhaps these were added by a longstanding"
                      f" FontLab Studio 5.x bug that copied"
                      f" copyright notices to them.)")
    else:
        yield PASS, ("Description strings in the name table"
                     " do not contain any copyright string.")


def PANOSE_is_monospaced(panose):
    # https://github.com/googlefonts/fontbakery/issues/2857#issue-608671015
    from fontbakery.constants import (PANOSE_Family_Type,
                                      PANOSE_Proportion,
                                      PANOSE_Spacing)

    if panose.bFamilyType == PANOSE_Family_Type.LATIN_TEXT:
        return panose.bProportion == PANOSE_Proportion.MONOSPACED

    if panose.bFamilyType in [PANOSE_Family_Type.LATIN_HAND_WRITTEN,
                              PANOSE_Family_Type.LATIN_SYMBOL]:
        return panose.bSpacing == PANOSE_Spacing.MONOSPACED

    # otherwise
    return False

def PANOSE_expected(family_type):
    # https://github.com/googlefonts/fontbakery/issues/2857#issue-608671015
    from fontbakery.constants import (PANOSE_Family_Type,
                                      PANOSE_Proportion,
                                      PANOSE_Spacing)

    if family_type == PANOSE_Family_Type.LATIN_TEXT:
        return (f"Please set PANOSE Proportion to"
                f" {PANOSE_Proportion.MONOSPACED} (monospaced)")

    if family_type in [PANOSE_Family_Type.LATIN_HAND_WRITTEN,
                       PANOSE_Family_Type.LATIN_SYMBOL]:
        return f"Please set PANOSE Spacing to {PANOSE_Spacing.MONOSPACED} (monospaced)"

    if family_type == PANOSE_Family_Type.LATIN_SYMBOL:
        return (f"PANOSE Family Type is set to 4 (latin symbol)."
                f" Please set it instead to {PANOSE_Family_Type.LATIN_TEXT} (latin text),"
                f" {PANOSE_Family_Type.LATIN_HAND_WRITTEN} (latin hand written)"
                f" or {PANOSE_Family_Type.LATIN_SYMBOL} (latin symbol)")

    # Otherwise:
    # I can't even suggest what to do
    # if it is that much broken!
    return f"Note: Family Type is set to {family_type}, which does not seem right."

@check(
    id = 'com.google.fonts/check/monospace',
    conditions = ['glyph_metrics_stats',
                  'is_ttf'],
    rationale = """
        There are various metadata in the OpenType spec to specify if a font is
        monospaced or not. If the font is not truly monospaced, then no monospaced
        metadata should be set (as sometimes they mistakenly are...)

        Requirements for monospace fonts:

        * post.isFixedPitch - "Set to 0 if the font is proportionally spaced, 
          non-zero if the font is not proportionally spaced (monospaced)"
          (https://www.microsoft.com/typography/otspec/post.htm)

        * hhea.advanceWidthMax must be correct, meaning no glyph's width value
          is greater. (https://www.microsoft.com/typography/otspec/hhea.htm)

        * OS/2.panose.bProportion must be set to 9 (monospace) on latin text fonts.

        * OS/2.panose.bSpacing must be set to 3 (monospace) on latin hand written
          or latin symbol fonts.

        * Spec says: "The PANOSE definition contains ten digits each of which currently
          describes up to sixteen variations. Windows uses bFamilyType, bSerifStyle
          and bProportion in the font mapper to determine family type. It also uses
          bProportion to determine if the font is monospaced."
          (https://www.microsoft.com/typography/otspec/os2.htm#pan 
           https://monotypecom-test.monotype.de/services/pan2)

        * OS/2.xAvgCharWidth must be set accurately.
          "OS/2.xAvgCharWidth is used when rendering monospaced fonts,
          at least by Windows GDI"
          (http://typedrawers.com/discussion/comment/15397/#Comment_15397)

        Also we should report an error for glyphs not of average width.


        Please also note:
        
        Thomas Phinney told us that a few years ago (as of December 2019), if you gave
        a font a monospace flag in Panose, Microsoft Word would ignore the actual
        advance widths and treat it as monospaced.
        
        Source: https://typedrawers.com/discussion/comment/45140/#Comment_45140
    """,
    proposal = 'legacy:check/033'
)
def com_google_fonts_check_monospace(ttFont, glyph_metrics_stats):
    """Checking correctness of monospaced metadata."""
    from fontbakery.constants import (IsFixedWidth,
                                      PANOSE_Proportion)
    failed = False
    # Note: These values are read from the dict here only to
    # reduce the max line length in the check implementation below:
    seems_monospaced = glyph_metrics_stats["seems_monospaced"]
    most_common_width = glyph_metrics_stats["most_common_width"]
    width_max = glyph_metrics_stats['width_max']

    # Check for missing tables before indexing them
    missing_tables = False
    required = ["glyf", "hhea", "hmtx", "OS/2", "post"]
    for key in required:
        if key not in ttFont:
            missing_tables = True
            yield FAIL,\
                  Message(f'lacks-{key}',
                          f"Font file lacks a '{key}' table.")

    if missing_tables:
        return

    if ttFont['hhea'].advanceWidthMax != width_max:
        failed = True
        yield FAIL,\
              Message("bad-advanceWidthMax",
                      f"Value of hhea.advanceWidthMax"
                      f" should be set to {width_max}"
                      f" but got {ttFont['hhea'].advanceWidthMax} instead.")

    if seems_monospaced:
        if ttFont['post'].isFixedPitch == IsFixedWidth.NOT_MONOSPACED:
            failed = True
            yield FAIL,\
                  Message("mono-bad-post-isFixedPitch",
                          f"On monospaced fonts, the value of post.isFixedPitch"
                          f" must be set to a non-zero value"
                          f" (meaning 'fixed width monospaced'),"
                          f" but got {ttFont['post'].isFixedPitch} instead.")

        if not PANOSE_is_monospaced(ttFont['OS/2'].panose):
            failed = True
            family_type = ttFont['OS/2'].panose.bFamilyType
            yield FAIL,\
                  Message("mono-bad-panose",
                          f"The PANOSE numbers are incorrect for a monospaced font. "
                          f"{PANOSE_expected(family_type)}")

        num_glyphs = len(ttFont['glyf'].glyphs)
        unusually_spaced_glyphs = [
            g for g in ttFont['glyf'].glyphs
            if g not in ['.notdef', '.null', 'NULL'] and
            ttFont['hmtx'].metrics[g][0] != 0 and
            ttFont['hmtx'].metrics[g][0] != most_common_width
        ]
        outliers_ratio = float(len(unusually_spaced_glyphs)) / num_glyphs
        if outliers_ratio > 0:
            failed = True
            yield WARN,\
                  Message("mono-outliers",
                          f"Font is monospaced"
                          f" but {len(unusually_spaced_glyphs)} glyphs"
                          f" ({100.0 * outliers_ratio:.2f}%)"
                          f" have a different width."
                          f" You should check the widths of:"
                          f" {unusually_spaced_glyphs}")
        if not failed:
            yield PASS,\
                  Message("mono-good",
                          "Font is monospaced and all related metadata look good.")
    else:
        # it is a non-monospaced font, so lets make sure
        # that all monospace-related metadata is properly unset.

        if ttFont['post'].isFixedPitch != IsFixedWidth.NOT_MONOSPACED:
            failed = True
            yield FAIL,\
                  Message("bad-post-isFixedPitch",
                          f"On non-monospaced fonts,"
                          f" the post.isFixedPitch value must be set to"
                          f" {IsFixedWidth.NOT_MONOSPACED} (not monospaced),"
                          f" but got {ttFont['post'].isFixedPitch} instead.")

        if ttFont['OS/2'].panose.bProportion == PANOSE_Proportion.MONOSPACED:
            failed = True
            yield FAIL,\
                  Message("bad-panose",
                          "On non-monospaced fonts,"
                          " the OS/2.panose.bProportion value can be set to"
                          " any value except 9 (proportion: monospaced)"
                          " which is the bad value we got in this font.")
        if not failed:
            yield PASS,\
                  Message("good",
                          "Font is not monospaced and"
                          " all related metadata look good.")


@check(
    id = 'com.google.fonts/check/name/match_familyname_fullfont',
    rationale = """
        The FULL_FONT_NAME entry in the ‘name’ table should start with the same string
        as the Family Name (FONT_FAMILY_NAME, TYPOGRAPHIC_FAMILY_NAME or
        WWS_FAMILY_NAME).

        If the Family Name is not included as the first part of the Full Font Name, and
        the user embeds the font in a document using a Microsoft Office app, the app
        will fail to render the font when it opens the document again.
    """,
    proposal = 'legacy:check/068'
)
def com_google_fonts_check_name_match_familyname_fullfont(ttFont):
    """Does full font name begin with the font family name?"""

    name_table = ttFont["name"]
    names_compared = False  # Used for knowing if at least one comparison was attempted
    passed = True

    # Collect all the unique platformIDs, encodingIDs, and languageIDs
    # used in the font's name table.
    platform_ids = set(rec.platformID for rec in name_table.names)
    encoding_ids = set(rec.platEncID for rec in name_table.names)
    language_ids = set(rec.langID for rec in name_table.names)

    # Now iterate over the platform/encoding/languageID sets and compare
    # the full name and family name strings.
    # The full name string is always taken from nameID 4, but
    # the family name string can come from nameIDs 1, 16, or 21.
    # We'll compare all of the ones contained in the font.
    full_name_id = NameID.FULL_FONT_NAME
    for plat_id in sorted(platform_ids):
        for enc_id in sorted(encoding_ids):
            for lang_id in sorted(language_ids):

                # Check if the full name record exists
                if name_table.getName(full_name_id, plat_id, enc_id, lang_id) is None:
                    # The full_name_id wasn't found. No point in going further
                    continue

                # Iterate over all possible family name records
                for family_name_id in (NameID.FONT_FAMILY_NAME,
                                       NameID.TYPOGRAPHIC_FAMILY_NAME,
                                       NameID.WWS_FAMILY_NAME):
                    if name_table.getName(
                            family_name_id, plat_id, enc_id, lang_id) is None:
                        # The family_name_id wasn't found. Move on to the next
                        continue

                    names_compared = True  # Yay! At least one comparison will be made!!

                    decode_error_msg_prefix = (
                        f"On the 'name' table, the name record"
                        f" for platformID {plat_id},"
                        f" encodingID {enc_id},"
                        f" languageID {lang_id}({lang_id:04X}),"
                    )
                    try:
                        family_name = name_table.getName(
                            family_name_id, plat_id, enc_id, lang_id).toUnicode()
                    except UnicodeDecodeError:
                        yield FAIL, Message(
                            f"cannot-decode-nameid-{family_name_id}",
                            f"{decode_error_msg_prefix} and nameID {family_name_id}"
                            " failed to be decoded."
                        )
                        passed = False
                        continue

                    try:
                        full_name = name_table.getName(
                            full_name_id, plat_id, enc_id, lang_id).toUnicode()
                    except UnicodeDecodeError:
                        yield FAIL, Message(
                            f"cannot-decode-nameid-{full_name_id}",
                            f"{decode_error_msg_prefix} and nameID {full_name_id}"
                            " failed to be decoded."
                        )
                        passed = False
                        continue

                    if not full_name.startswith(family_name):
                        yield FAIL, Message(
                            "mismatch-font-names",
                            f"On the 'name' table, the full font name {full_name!r}"
                            f" does not begin with the font family name {family_name!r}"
                            f" in platformID {plat_id},"
                            f" encodingID {enc_id},"
                            f" languageID {lang_id}({lang_id:04X}),"
                            f" and nameID {family_name_id}."
                        )
                        passed = False

    if not names_compared:
        yield FAIL, Message(
            "missing-font-names",
            f"The font's 'name' table lacks a pair of records with"
            f" nameID {NameID.FULL_FONT_NAME} (Full name),"
            f" and at least one of"
            f" nameID {NameID.FONT_FAMILY_NAME} (Font Family name),"
            f" {NameID.TYPOGRAPHIC_FAMILY_NAME} (Typographic Family name),"
            f" or {NameID.WWS_FAMILY_NAME} (WWS Family name)."
        )
    elif passed:
        yield PASS, "Full font name begins with the font family name."


@check(
    id = 'com.google.fonts/check/family_naming_recommendations',
    proposal = 'legacy:check/071'
)
def com_google_fonts_check_family_naming_recommendations(ttFont):
    """Font follows the family naming recommendations?"""
    # See http://forum.fontlab.com/index.php?topic=313.0
    import re
    from fontbakery.utils import get_name_entry_strings
    bad_entries = []

    # <Postscript name> may contain only a-zA-Z0-9
    # and one hyphen
    bad_psname = re.compile("[^A-Za-z0-9-]")
    for string in get_name_entry_strings(ttFont,
                                         NameID.POSTSCRIPT_NAME):
        if bad_psname.search(string):
            bad_entries.append({
                'field': 'PostScript Name',
                'value': string,
                'rec': ('May contain only a-zA-Z0-9'
                        ' characters and an hyphen.')
            })
        if string.count('-') > 1:
            bad_entries.append({
                'field': 'Postscript Name',
                'value': string,
                'rec': ('May contain not more'
                        ' than a single hyphen')
            })

    for string in get_name_entry_strings(ttFont,
                                         NameID.FULL_FONT_NAME):
        if len(string) >= 64:
            bad_entries.append({
                'field': 'Full Font Name',
                'value': string,
                'rec': 'exceeds max length (63)'
            })

    for string in get_name_entry_strings(ttFont,
                                         NameID.POSTSCRIPT_NAME):
        if len(string) >= 64:
            bad_entries.append({
                'field': 'PostScript Name',
                'value': string,
                'rec': 'exceeds max length (63)'
            })

    for string in get_name_entry_strings(ttFont,
                                         NameID.FONT_FAMILY_NAME):
        if len(string) >= 32:
            bad_entries.append({
                'field': 'Family Name',
                'value': string,
                'rec': 'exceeds max length (31)'
            })

    for string in get_name_entry_strings(ttFont,
                                         NameID.FONT_SUBFAMILY_NAME):
        if len(string) >= 32:
            bad_entries.append({
                'field': 'Style Name',
                'value': string,
                'rec': 'exceeds max length (31)'
            })

    for string in get_name_entry_strings(ttFont,
                                         NameID.TYPOGRAPHIC_FAMILY_NAME):
        if len(string) >= 32:
            bad_entries.append({
                'field': 'OT Family Name',
                'value': string,
                'rec': 'exceeds max length (31)'
            })

    for string in get_name_entry_strings(ttFont,
                                         NameID.TYPOGRAPHIC_SUBFAMILY_NAME):
        if len(string) >= 32:
            bad_entries.append({
                'field': 'OT Style Name',
                'value': string,
                'rec': 'exceeds max length (31)'
            })

    if len(bad_entries) > 0:
        table = "| Field | Value | Recommendation |\n"
        table += "|:----- |:----- |:-------------- |\n"
        for bad in bad_entries:
            table += "| {} | {} | {} |\n".format(bad["field"],
                                                 bad["value"],
                                                 bad["rec"])
        yield INFO,\
              Message("bad-entries",
                      f"Font does not follow "
                      f"some family naming recommendations:\n"
                      f"\n"
                      f"{table}")
    else:
        yield PASS, "Font follows the family naming recommendations."


@check(
    id = 'com.adobe.fonts/check/name/postscript_vs_cff',
    conditions = ['is_cff'],
    rationale = """
        The PostScript name entries in the font's 'name' table should match
        the FontName string in the 'CFF ' table.

        The 'CFF ' table has a lot of information that is duplicated in other tables.
        This information should be consistent across tables, because there's
        no guarantee which table an app will get the data from.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/pull/2229'
)
def com_adobe_fonts_check_name_postscript_vs_cff(ttFont):
    """CFF table FontName must match name table ID 6 (PostScript name)."""
    cff_names = ttFont['CFF '].cff.fontNames
    if len(cff_names) != 1:
        yield ERROR, "Unexpected number of font names in CFF table."
        return

    passed = True
    cff_name = cff_names[0]
    for entry in ttFont['name'].names:
        if entry.nameID == NameID.POSTSCRIPT_NAME:
            postscript_name = entry.toUnicode()
            if postscript_name != cff_name:
                passed = False
                yield FAIL,\
                      Message("mismatch",
                              f"Name table PostScript name '{postscript_name}' "
                              f"does not match CFF table FontName '{cff_name}'.")

    if passed:
        yield PASS, ("Name table PostScript name matches CFF table FontName.")


@check(
    id = 'com.adobe.fonts/check/name/postscript_name_consistency',
    conditions = ['not is_cff'],  # e.g. TTF or CFF2
    rationale = """
        The PostScript name entries in the font's 'name' table should be
        consistent across platforms.

        This is the TTF/CFF2 equivalent of the CFF 'name/postscript_vs_cff' check.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/pull/2394'
)
def com_adobe_fonts_check_name_postscript_name_consistency(ttFont):
    """Name table ID 6 (PostScript name) must be consistent across platforms."""
    postscript_names = set()
    for entry in ttFont['name'].names:
        if entry.nameID == NameID.POSTSCRIPT_NAME:
            postscript_name = entry.toUnicode()
            postscript_names.add(postscript_name)

    if len(postscript_names) > 1:
        yield FAIL,\
              Message("inconsistency",
                      f'Entries in the "name" table for ID 6'
                      f' (PostScript name) are not consistent.'
                      f' Names found: {sorted(postscript_names)}.')
    else:
        yield PASS, ('Entries in the "name" table for ID 6 '
                     '(PostScript name) are consistent.')


@check(
    id = 'com.adobe.fonts/check/family/max_4_fonts_per_family_name',
    rationale = """
        Per the OpenType spec:
        
        'The Font Family name [...] should be shared among at most four fonts that
        differ only in weight or style [...]'
    """,
)
def com_adobe_fonts_check_family_max_4_fonts_per_family_name(ttFonts):
    """Verify that each group of fonts with the same nameID 1 has maximum of 4 fonts"""
    from collections import Counter
    from fontbakery.utils import get_name_entry_strings

    family_names = list()
    for ttFont in ttFonts:
        names_list = get_name_entry_strings(ttFont,
                                            NameID.FONT_FAMILY_NAME,
                                            PlatformID.WINDOWS,
                                            WindowsEncodingID.UNICODE_BMP,
                                            WindowsLanguageID.ENGLISH_USA)
        # names_list may contain multiple entries.
        # We use set() below to remove the duplicates and only store
        # the unique family name(s) used for a given font
        names_set = set(names_list)
        family_names.extend(names_set)

    passed = True
    counter = Counter(family_names)
    for family_name, count in counter.items():
        if count > 4:
            passed = False
            yield FAIL,\
                  Message("too-many",
                          f"Family '{family_name}' has {count} fonts"
                          f" (should be 4 or fewer).")
    if passed:
        yield PASS, ("There were no more than 4 fonts per family name.")


# Lib/fontbakery/profiles/notofonts.py

import re

from fontbakery.profiles.googlefonts import GOOGLEFONTS_PROFILE_CHECKS
from fontbakery.section import Section
from fontbakery.status import WARN, PASS, FAIL, SKIP #, INFO, ERROR
from fontbakery.callable import check #, disable
from fontbakery.message import Message
from fontbakery.fonts_profile import profile_factory
from fontbakery.constants import (PlatformID,
                                  WindowsEncodingID,
                                  UnicodeEncodingID,
                                  MacintoshEncodingID)

from .googlefonts_conditions import * # pylint: disable=wildcard-import,unused-wildcard-import
profile_imports = [
    'fontbakery.profiles.googlefonts',
    ('.shared_conditions', ('glyph_metrics_stats', 'preferred_cmap', 'unicoderange'))
]

profile = profile_factory(default_section=Section("Noto Fonts"))

profile.configuration_defaults = {
    "com.google.fonts/check/file_size": {
        "WARN_SIZE": 1 * 1024 * 1024,
        "FAIL_SIZE": 16 * 1024 * 1024
    }
}

NOTOFONTS_PROFILE_CHECKS = \
    GOOGLEFONTS_PROFILE_CHECKS + [
    'com.google.fonts/check/cmap/unexpected_subtables',
    'com.google.fonts/check/unicode_range_bits',
    'com.google.fonts/check/name/noto_manufacturer',
    'com.google.fonts/check/name/noto_designer',
    'com.google.fonts/check/name/noto_trademark',
    'com.google.fonts/check/cmap/format_12',
    'com.google.fonts/check/os2/noto_vendor',
    'com.google.fonts/check/hmtx/encoded_latin_digits',
    'com.google.fonts/check/hmtx/comma_period',
    'com.google.fonts/check/hmtx/whitespace_advances',
    'com.google.fonts/check/cmap/alien_codepoints',
]


# For builds which target Google Fonts, we do want these checks, but as part of
# onboarding we will be running check-googlefonts on such builds.
# On other builds (e.g. targetting Android), we still want most of the Google
# strictures but size is a premium and we will be expecting to deliver a
# "minimal" font, so we accept the fact that there will be no Latin set and no
# hinting information at all.
SKIPPED_CHECKS = [
    'com.google.fonts/check/render_own_name',
    'com.google.fonts/check/glyph_coverage',
    'com.google.fonts/check/smart_dropout',
    'com.google.fonts/check/gasp',
]

NOTOFONTS_PROFILE_CHECKS = list(filter(lambda c: c not in SKIPPED_CHECKS, NOTOFONTS_PROFILE_CHECKS))

TRADEMARK = r"(Noto|Arimo|Tinos) is a trademark of Google (Inc|LLC)"
NOTO_URL = "http://www.google.com/get/noto/" # Vendor URL

MANUFACTURERS_URLS = {
    "Adobe Systems Incorporated": "http://www.adobe.com/type/",
    "Ek Type": "http://www.ektype.in",
    "Monotype Imaging Inc.": "http://www.monotype.com/studio",
    "JamraPatel LLC": "http://www.jamra-patel.com",
    "Danh Hong": "http://www.khmertype.org",
    "Google LLC": "http://www.google.com/get/noto/",
    "Dalton Maag Ltd": "http://www.daltonmaag.com/",
    "Lisa Huang": "http://www.lisahuang.work",
    "Mangu Purty": "",
    "LiuZhao Studio": "",
}

NOTO_DESIGNERS = [
    "Nadine Chahine - Monotype Design Team",
    "Jelle Bosma - Monotype Design Team",
    "Danh Hong and the Monotype Design Team",
    "Indian Type Foundry and the Monotype Design Team",
    "Ben Mitchell and the Monotype Design Team",
    "Vaibhav Singh and the Monotype Design Team",
    "Universal Thirst, Indian Type Foundry and the Monotype Design Team",
    "Monotype Design Team",
    "Ek Type & Mukund Gokhale",
    "Ek Type",
    "JamraPatel",
    "Dalton Maag Ltd",
    "Amélie Bonet and Sol Matas",
    "Ben Nathan",
    "Indian type Foundry, Jelle Bosma, Monotype Design Team",
    "Indian Type Foundry, Tom Grace, and the Monotype Design Team",
    "Jelle Bosma - Monotype Design Team, Universal Thirst",
    "Juan Bruce, Universal Thirst, Indian Type Foundry and the Monotype Design Team.",
    "Lisa Huang",
    "Mangu Purty",
    "Mark Jamra, Neil Patel",
    "Monotype Design Team (Regular), Sérgio L. Martins (other weights)",
    "Monotype Design Team 2013. Revised by David WIlliams 2020",
    "Monotype Design Team and DaltonMaag",
    "Monotype Design Team and Neelakash Kshetrimayum",
    "Monotype Design Team, Akaki Razmadze",
    "Monotype Design Team, Lewis McGuffie",
    "Monotype Design Team, Nadine Chahine and Nizar Qandah",
    "Monotype Design Team, Sérgio Martins",
    "Monotype Design Team. David Williams.",
    "Patrick Giasson and the Monotype Design Team",
    "David Williams",
    "LIU Zhao",
    "Steve Matteson",
    "Juan Bruce",
    "Sérgio Martins",
    "Lewis McGuffie",
    "YANG Xicheng",
]

def _get_advance_width_for_char(ttFont, ch):
    cp = ord(ch)
    cmap = ttFont.getBestCmap()
    if cp not in cmap:
        return None
    return ttFont["hmtx"][cmap[cp]][0]

@check(
    id = 'com.google.fonts/check/cmap/unexpected_subtables',
    rationale = """
        There are just a few typical types of cmap subtables that are used in fonts.
        If anything different is declared in a font, it will be treated as a FAIL.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/issues/2676'
)
def com_google_fonts_check_cmap_unexpected_subtables(ttFont):
    """Ensure all cmap subtables are the typical types expected in a font."""
    from fontbakery.profiles.shared_conditions import is_cjk_font

    passed = True
    # Note:
    #   Format 0 = Byte encoding table
    #   Format 4 = Segment mapping to delta values
    #   Format 6 = Trimmed table mapping
    #   Format 12 = Segmented coverage
    #   Format 14 = Unicode Variation Sequences
    EXPECTED_SUBTABLES = [
        ( 0, PlatformID.MACINTOSH, MacintoshEncodingID.ROMAN),     # 13.7% of GFonts TTFs (389 files)
        #( 4, PlatformID.MACINTOSH, MacintoshEncodingID.ROMAN),     # only the Sansation family has this on GFonts
        ( 6, PlatformID.MACINTOSH, MacintoshEncodingID.ROMAN),     # 38.1% of GFonts TTFs (1.082 files)
        #( 4, PlatformID.UNICODE,   UnicodeEncodingID.UNICODE_1_0), # only the Gentium family has this on GFonts
        #(12, PlatformID.UNICODE, 10), # INVALID? - only the Overpass family and SawarabiGothic-Regular has this on GFonts
        # -----------------------------------------------------------------------
        ( 4, PlatformID.WINDOWS, WindowsEncodingID.UNICODE_BMP),                 # Absolutely all GFonts TTFs have this table :-)
        (12, PlatformID.WINDOWS, WindowsEncodingID.UNICODE_FULL_REPERTOIRE),     #   5.7% of GFonts TTFs (162 files)
        (14, PlatformID.UNICODE, UnicodeEncodingID.UNICODE_VARIATION_SEQUENCES), #   1.1% - Only 4 families (30 TTFs),
                                                                                 #          including SourceCodePro, have this on GFonts
        ( 4, PlatformID.UNICODE, UnicodeEncodingID.UNICODE_2_0_BMP_ONLY),        #  97.0% of GFonts TTFs (only 84 files lack it)
        (12, PlatformID.UNICODE, UnicodeEncodingID.UNICODE_2_0_FULL)             #   2.9% of GFonts TTFs (82 files)
    ]
    if is_cjk_font(ttFont):
        EXPECTED_SUBTABLES.extend([
            # Adobe says historically some programs used these to identify
            # the script in the font.  The encodingID is the quickdraw
            # script manager code.  These are placeholder tables.
            (6, PlatformID.MACINTOSH, MacintoshEncodingID.JAPANESE),
            (6, PlatformID.MACINTOSH, MacintoshEncodingID.CHINESE_TRADITIONAL),
            (6, PlatformID.MACINTOSH, MacintoshEncodingID.KOREAN),
            (6, PlatformID.MACINTOSH, MacintoshEncodingID.CHINESE_SIMPLIFIED)
        ])

    for subtable in ttFont['cmap'].tables:
        if (subtable.format,
            subtable.platformID,
            subtable.platEncID) not in EXPECTED_SUBTABLES:
            passed = False
            yield WARN,\
                  Message("unexpected-subtable",
                          f"'cmap' has a subtable of"
                          f" (format={subtable.format}, platform={subtable.platformID},"
                          f" encoding={subtable.platEncID}), which it shouldn't have.")
    if passed:
        yield PASS, "All cmap subtables look good!"


@check(
    id = 'com.google.fonts/check/unicode_range_bits',
    rationale = """
        When the UnicodeRange bits on the OS/2 table are not properly set,
        some programs running on Windows may not recognize the font and use a
        system fallback font instead. For that reason, this check calculates the
        proper settings by inspecting the glyphs declared on the cmap table and
        then ensures that their corresponding ranges are enabled.
    """,
    conditions = ["unicoderange",
                  "preferred_cmap"],
    proposal = 'https://github.com/googlefonts/fontbakery/issues/2676'
)
def com_google_fonts_check_unicode_range_bits(ttFont, unicoderange, preferred_cmap):
    """Ensure UnicodeRange bits are properly set."""
    from fontbakery.constants import UNICODERANGE_DATA
    from fontbakery.utils import (compute_unicoderange_bits,
                                  unicoderange_bit_name,
                                  chars_in_range)
    expected_unicoderange = compute_unicoderange_bits(ttFont)
    difference = unicoderange ^ expected_unicoderange
    if not difference:
        yield PASS, "Looks good!"
    else:
        for bit in range(128):
            if difference & (1 << bit):
                range_name = unicoderange_bit_name(bit)
                num_chars = len(chars_in_range(ttFont, bit))
                range_size = sum(entry[3] - entry[2] + 1 for entry in UNICODERANGE_DATA[bit])
                set_unset = "1"
                if num_chars == 0:
                    set_unset = "0"
                    num_chars = "none"
                yield WARN, \
                      Message("bad-range-bit",
                              f'UnicodeRange bit {bit} "{range_name}" should be'
                              f' {set_unset} because cmap has {num_chars} of'
                              f' the {range_size} codepoints in this range.')


@check(
    id = 'com.google.fonts/check/name/noto_manufacturer',
    rationale = """
        Noto fonts must contain known manufacturer and manufacturer
        URL entries in the name table.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/pull/3681',
)
def com_google_fonts_check_noto_manufacturer(ttFont):
    """Ensure the manufacturer is a known Noto manufacturer and the URL is correct."""
    from fontbakery.utils import get_name_entry_strings
    bad = False
    manufacturers = get_name_entry_strings(ttFont, NameID.MANUFACTURER_NAME)
    good_manufacturer = None
    if not manufacturers:
        bad = True
        yield FAIL,\
              Message("no-manufacturer",
                      "The font contained no manufacturer name")

    manufacturer_re = "|".join(MANUFACTURERS_URLS.keys())
    for manufacturer in manufacturers:
        m = re.search(manufacturer_re, manufacturer)
        if m:
            good_manufacturer = m[0]
        else:
            bad = True
            yield WARN,\
                  Message("unknown-manufacturer",
                          f"The font's manufacturer name '{manufacturer}' was"
                          f" not a known Noto font manufacturer")

    designer_urls = get_name_entry_strings(ttFont, NameID.DESIGNER_URL)
    if not designer_urls:
        bad = True
        yield WARN,\
              Message("no-designer-urls",
                      "The font contained no designer URL"
                     )
    if good_manufacturer:
        expected_url = MANUFACTURERS_URLS[good_manufacturer]
        for designer_url in designer_urls:
            if designer_url != expected_url:
                yield WARN,\
                      Message("bad-designer-url",
                              f"The font's designer URL was '{designer_url}'"
                              f" but should have been '{expected_url}'")
    if not bad:
        yield PASS, "The manufacturer name and designer URL entries were valid"


@check(
    id = 'com.google.fonts/check/name/noto_designer',
    rationale = """
        Noto fonts must contain known designer entries in the name table.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/pull/3681',
)
def com_google_fonts_check_noto_designer(ttFont):
    """Ensure the designer is a known Noto designer."""
    from fontbakery.utils import get_name_entry_strings
    bad = False
    designers = get_name_entry_strings(ttFont, NameID.DESIGNER)
    if not designers:
        bad = True
        yield FAIL,\
                  Message("no-designer",
                          "The font contained no designer name"
                         )

    for designer in designers:
        if designer not in NOTO_DESIGNERS:
            bad = True
            yield WARN,\
                  Message("unknown-designer",
                          f"The font's designer name '{designer}' was "
                          "not a known Noto font designer"
                        )
    if not bad:
        yield PASS, "The designer name entry was valid"


@check(
    id = 'com.google.fonts/check/name/noto_trademark',
    rationale = """
        Noto fonts must contain the correct trademark entry in the name table.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/pull/3681',
)
def com_google_fonts_check_noto_trademark(ttFont):
    """Ensure the trademark matches the expected string."""
    from fontbakery.utils import get_name_entry_strings

    bad = False
    trademarks = get_name_entry_strings(ttFont, NameID.TRADEMARK)
    if not trademarks:
        bad = True
        yield FAIL,\
                  Message("no-trademark",
                          "The font contained no trademark entry"
                         )
    for trademark in trademarks:
        if not re.match(TRADEMARK, trademark):
            bad = True
            yield FAIL,\
                  Message("bad-trademark",
                          f"The trademark entry should be '{TRADEMARK}' "
                          f"but was actually '{trademark}'"
                         )

    if not bad:
        yield PASS, "The trademark name entry was valid"


@check(
    id = 'com.google.fonts/check/cmap/format_12',
    rationale = """
        If a format 12 cmap table is used to address codepoints beyond the BMP,
        it should actually contain such codepoints. Additionally, it should also
        contain all characters mapped in the format 4 subtable.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/pull/3681',
)
def com_google_fonts_check_cmap_format_12(ttFont, config):
    """Check that format 12 cmap subtables are correctly constituted."""
    bad = False
    skipped = True
    # Find the format 4
    cmap4 = None
    for table in ttFont['cmap'].tables:
        if table.format == 4:
            cmap4 = table
            break

    if not cmap4:
        yield FAIL,\
                  Message("no-cmap-4",
                    "The font did not contain a format 4 cmap table"
                  )
        return

    for subtable in ttFont['cmap'].tables:
        if subtable.format != 12:
            continue
        skipped = False
        codepoints = subtable.cmap.keys()
        if not any(cp > 0x0FFF for cp in codepoints):
            bad = True
            yield FAIL,\
                  Message("pointless-format-12",
                          "A format 12 subtable did not contain"
                          " any codepoints beyond the Basic Multilingual Plane (BMP)")
        unmapped_from_4 = set(cmap4.cmap.keys()) - set(codepoints)
        if unmapped_from_4:
            from fontbakery.utils import pretty_print_list
            yield WARN,\
                  Message("unmapped-from-4",
                          f"A format 12 subtable did not the following codepoints"
                          f" mapped in the format 4 subtable:"
                          f" {pretty_print_list(config, unmapped_from_4)}")

    if skipped:
        yield SKIP, "No format 12 subtables found"
    elif not bad:
        yield PASS, "All format 12 subtables were correctly formed"


@check(
    id = 'com.google.fonts/check/os2/noto_vendor',
    rationale = """
        Vendor ID must be 'GOOG'
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/pull/3681',
)
def com_google_fonts_check_os2_noto_vendor(ttFont):
    """Check OS/2 achVendID is set to GOOG."""

    vendor_id = ttFont['OS/2'].achVendID
    if vendor_id != 'GOOG':
        yield FAIL,\
              Message("bad-vendor-id",
                      f"OS/2 VendorID is '{vendor_id}', but should be 'GOOG'.")
    else:
        yield PASS, f"OS/2 VendorID '{vendor_id}' is correct."


@check(
    id = 'com.google.fonts/check/hmtx/encoded_latin_digits',
    rationale = """
        Encoded Latin digits in Noto fonts should have equal advance widths.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/pull/3681',
)
def com_google_fonts_check_htmx_encoded_latin_digits(ttFont):
    """Check all encoded Latin digits have the same advance width"""
    bad = False
    digits = "0123456789"
    zero_width = _get_advance_width_for_char(ttFont, "0")
    if zero_width is None:
        yield SKIP, "No encoded Latin digits"
        return
    for d in digits:
        actual_width = _get_advance_width_for_char(ttFont, d)
        if actual_width is None:
            bad = True
            yield FAIL, Message("missing-digit", f"Missing Latin digit {d}")
        elif actual_width != zero_width:
            bad = True
            yield FAIL,\
                  Message("bad-digit-width",
                          f"Width of {d} was expected to be "
                          f"{zero_width} but was {actual_width}")
    if not bad:
        yield PASS, "All Latin digits had same advance width"


@check(
    id = 'com.google.fonts/check/hmtx/comma_period',
    rationale = """
        If Latin comma and period are encoded in Noto fonts,
        they should have equal advance widths.
    """
)
def com_google_fonts_check_htmx_comma_period(ttFont):
    """Check comma and period have the same advance width"""
    comma = _get_advance_width_for_char(ttFont, ",")
    period = _get_advance_width_for_char(ttFont, ".")
    if comma is None or period is None:
        yield SKIP, "No comma and/or period"
    elif comma != period:
        yield FAIL,\
              Message("comma-period",
                      f"Advance width of comma ({comma}) != advance width"
                      f" of period {period}")
    else:
        yield PASS, "Comma and period had the same advance width"


@check(
    id = 'com.google.fonts/check/hmtx/whitespace_advances',
    rationale = """
        Encoded whitespace in Noto fonts should have well-defined advance widths.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/pull/3681',
)
def com_google_fonts_check_htmx_whitespace_advances(ttFont, config, glyph_metrics_stats):
    """Check all whitespace glyphs have correct advances"""
    import math

    problems = []
    if glyph_metrics_stats["seems_monospaced"]:
        yield SKIP, "Monospace glyph widths handled in other checks"
        return

    space_width = _get_advance_width_for_char(ttFont, " ")
    period_width = _get_advance_width_for_char(ttFont, ".")
    digit_width = _get_advance_width_for_char(ttFont, "0")
    em_width = ttFont["head"].unitsPerEm
    expectations = {
        0x09: space_width,  # tab
        0xa0: space_width,  # nbsp
        0x2000: em_width / 2,
        0x2001: em_width,
        0x2002: em_width / 2,
        0x2003: em_width,
        0x2004: em_width / 3,
        0x2005: em_width / 4,
        0x2006: em_width / 6,
        0x2007: digit_width,
        0x2008: period_width,
        0x2009: (em_width / 6, em_width / 5),
        0x200A: (em_width / 16, em_width / 10),
        0x200B: 0
    }
    for cp, expected_width in expectations.items():
        got_width = _get_advance_width_for_char(ttFont, chr(cp))
        if got_width is None:
            continue
        if isinstance(expected_width, tuple):
            if (got_width < math.floor(expected_width[0]) or
                got_width > math.ceil(expected_width[1])):
                problems.append(f"0x{cp:02x} (got={got_width},"
                                f" expected={expected_width[0]}...{expected_width[1]}")
        else:
            if got_width != round(expected_width):
                problems.append(f"0x{cp:02x} (got={got_width},"
                                f" expected={expected_width}")

    if problems:
        from fontbakery.utils import pretty_print_list
        formatted_list = "\t* " + pretty_print_list(config, problems, sep="\n\t* ")
        yield FAIL,\
              Message("bad-whitespace-advances",
                      f"The following glyphs had wrong advance widths:\n"
                      f"{formatted_list}")
    else:
        yield PASS, "Whitespace glyphs had correct advance widths"


@check(
    id = 'com.google.fonts/check/cmap/alien_codepoints',
    rationale = """
        Private Use Area codepoints and Surrogate Pairs should not be encoded.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/pull/3681',
)
def com_google_fonts_check_cmap_alien_codepoints(ttFont, config):
    """Check no PUA or Surrogate Pair codepoints encoded"""
    pua = []
    surrogate = []
    for cp in ttFont.getBestCmap().keys():
        if ((0xE000 <= cp <= 0xF8FF) or
            (0xF0000 <= cp <= 0xFFFFD) or
            (0x100000 <= cp <= 0x10FFFD)):
            pua.append(f"0x{cp:02x}")
        if 0xD800 <= cp <= 0xDFFF:
            surrogate.append(f"0x{cp:02x}")
    if not pua and not surrogate:
        yield PASS, "No alien codepoints were encoded"
        return

    from fontbakery.utils import pretty_print_list
    if pua:
        yield FAIL,\
              Message("pua-encoded",
                      "The following private use area codepoints were"
                      " encoded in the font: " + pretty_print_list(config, pua))
    if surrogate:
        yield FAIL,\
              Message("surrogate-encoded",
                      "The following surrogate pair codepoints were"
                      " encoded in the font: " + pretty_print_list(config, surrogate))

profile.auto_register(globals(),
                      filter_func=lambda type, id, _:
                      not (type == 'check' and id not in NOTOFONTS_PROFILE_CHECKS))
profile.test_expected_checks(NOTOFONTS_PROFILE_CHECKS, exclusive=True)


# Lib/fontbakery/profiles/opentype.py

from fontbakery.fonts_profile import profile_factory  # NOQA pylint: disable=unused-import
from fontbakery.section import Section

OPENTYPE_PROFILE_IMPORTS = (
    ".",
    (
        "cff",
        "cmap",
        "head",
        "os2",
        "post",
        "name",
        "loca",
        "hhea",
        "dsig",
        "hmtx",
        "gdef",
        "gpos",
        "kern",
        "glyf",
        "fvar",
        "stat",
        "layout",
        "shared_conditions",
    ),
)
profile_imports = (OPENTYPE_PROFILE_IMPORTS, )
profile = profile_factory(default_section=Section("OpenType Specification Checks"))

OPENTYPE_PROFILE_CHECKS = [
    'com.google.fonts/check/family/underline_thickness',
    'com.google.fonts/check/family/panose_proportion',
    'com.google.fonts/check/family/panose_familytype',
    'com.google.fonts/check/family/equal_unicode_encodings',
    'com.google.fonts/check/family/equal_font_versions',
    'com.adobe.fonts/check/family/bold_italic_unique_for_nameid1',
    'com.adobe.fonts/check/family/max_4_fonts_per_family_name',
    'com.adobe.fonts/check/name/postscript_vs_cff',
    'com.adobe.fonts/check/name/postscript_name_consistency',
    'com.adobe.fonts/check/name/empty_records',
    'com.google.fonts/check/name/no_copyright_on_description',
    'com.google.fonts/check/name/match_familyname_fullfont',
    'com.google.fonts/check/varfont/regular_wght_coord',
    'com.google.fonts/check/varfont/regular_wdth_coord',
    'com.google.fonts/check/varfont/regular_slnt_coord',
    'com.google.fonts/check/varfont/regular_ital_coord',
    'com.google.fonts/check/varfont/regular_opsz_coord',
    'com.google.fonts/check/varfont/bold_wght_coord',
    'com.google.fonts/check/varfont/slnt_range',
    'com.google.fonts/check/varfont/wght_valid_range',
    'com.google.fonts/check/varfont/wdth_valid_range',
    'com.google.fonts/check/varfont/stat_axis_record_for_each_axis',
    'com.google.fonts/check/loca/maxp_num_glyphs',
    'com.adobe.fonts/check/cff2_call_depth',
    'com.adobe.fonts/check/cff_call_depth',
    'com.adobe.fonts/check/cff_deprecated_operators',
    'com.google.fonts/check/font_version',
    'com.google.fonts/check/post_table_version',
    'com.google.fonts/check/monospace',
    'com.google.fonts/check/xavgcharwidth',
    'com.adobe.fonts/check/fsselection_matches_macstyle',
    'com.google.fonts/check/linegaps',
    'com.google.fonts/check/unitsperem',
    'com.google.fonts/check/dsig',
    'com.google.fonts/check/whitespace_widths',
    'com.google.fonts/check/gdef_spacing_marks',
    'com.google.fonts/check/gdef_mark_chars',
    'com.google.fonts/check/gdef_non_mark_chars',
    'com.google.fonts/check/gpos_kerning_info',
    'com.google.fonts/check/kern_table',
    'com.google.fonts/check/glyf_unused_data',
    'com.google.fonts/check/family_naming_recommendations',
    'com.google.fonts/check/maxadvancewidth',
    'com.google.fonts/check/points_out_of_bounds',
    'com.google.fonts/check/glyf_non_transformed_duplicate_components',
    'com.google.fonts/check/all_glyphs_have_codepoints',
    'com.google.fonts/check/code_pages',
    'com.google.fonts/check/layout_valid_feature_tags',
    'com.google.fonts/check/layout_valid_script_tags',
    'com.google.fonts/check/layout_valid_language_tags',
    'com.adobe.fonts/check/varfont/valid_axis_nameid',
    'com.adobe.fonts/check/varfont/valid_subfamily_nameid',
    'com.adobe.fonts/check/varfont/valid_postscript_nameid',
    'com.adobe.fonts/check/varfont/valid_default_instance_nameids',
    'com.adobe.fonts/check/varfont/same_size_instance_records',
    'com.adobe.fonts/check/varfont/distinct_instance_records',
]

profile.auto_register(globals())
profile.test_expected_checks(OPENTYPE_PROFILE_CHECKS, exclusive=True)


# Lib/fontbakery/profiles/os2.py

from fontbakery.callable import check
from fontbakery.status import FAIL, PASS, WARN, INFO
from fontbakery.message import Message
# used to inform get_module_profile whether and how to create a profile
from fontbakery.fonts_profile import profile_factory # NOQA pylint: disable=unused-import

profile_imports = [
    ('.shared_conditions', ('vmetrics', )),
    ('.googlefonts_conditions', ('RIBBI_ttFonts', ))
]

@check(
    id = 'com.google.fonts/check/family/panose_proportion',
    proposal = 'legacy:check/009'
)
def com_google_fonts_check_family_panose_proportion(ttFonts):
    """Fonts have consistent PANOSE proportion?"""
    passed = True
    proportion = None
    missing = False
    for ttFont in ttFonts:
        if "OS/2" not in ttFont:
            missing = True
            passed = False
            continue
        if proportion is None:
            proportion = ttFont['OS/2'].panose.bProportion
        if proportion != ttFont['OS/2'].panose.bProportion:
            passed = False

    if missing:
        yield FAIL,\
              Message("lacks-OS/2",
                      "One or more fonts lack the required OS/2 table.")

    if not passed:
        yield FAIL,\
              Message("inconsistency",
                      "PANOSE proportion is not the same across this family."
                      " In order to fix this, please make sure that"
                      " the panose.bProportion value is the same"
                      " in the OS/2 table of all of this family font files.")
    else:
        yield PASS, "Fonts have consistent PANOSE proportion."


@check(
    id = 'com.google.fonts/check/family/panose_familytype',
    proposal = 'legacy:check/010'
)
def com_google_fonts_check_family_panose_familytype(ttFonts):
    """Fonts have consistent PANOSE family type?"""
    passed = True
    familytype = None
    missing = False

    for ttfont in ttFonts:
        if "OS/2" not in ttfont:
            passed = False
            missing = True
            continue
        if familytype is None:
            familytype = ttfont['OS/2'].panose.bFamilyType
        if familytype != ttfont['OS/2'].panose.bFamilyType:
            passed = False

    if missing:
        yield FAIL,\
              Message("lacks-OS/2",
                      "One or more fonts lack the required OS/2 table.")

    if not passed:
        yield FAIL,\
              Message("inconsistency",
                      "PANOSE family type is not the same across this family."
                      " In order to fix this, please make sure that"
                      " the panose.bFamilyType value is the same"
                      " in the OS/2 table of all of this family font files.")
    else:
        yield PASS, "Fonts have consistent PANOSE family type."


@check(
    id = 'com.google.fonts/check/xavgcharwidth',
    conditions = ['is_ttf'],
    proposal = 'legacy:check/034'
)
def com_google_fonts_check_xavgcharwidth(ttFont):
    """Check if OS/2 xAvgCharWidth is correct."""

    if "OS/2" not in ttFont:
        yield FAIL,\
              Message("lacks-OS/2",
                      "Required OS/2 table is missing.")
        return

    current_value = ttFont['OS/2'].xAvgCharWidth
    ACCEPTABLE_ERROR = 10  # Width deviation tolerance in font units

    # Since version 3, the average is computed using _all_ glyphs in a font.
    if ttFont['OS/2'].version >= 3:
        calculation_rule = "the average of the widths of all glyphs in the font"
        if not ttFont['hmtx'].metrics:  # May contain just '.notdef', which is valid.
            yield FAIL,\
                  Message("missing-glyphs",
                          "CRITICAL: Found no glyph width data in the hmtx table!")
            return

        width_sum = 0
        count = 0
        for glyph_id in ttFont['glyf'].glyphs:  # At least .notdef must be present.
            width = ttFont['hmtx'].metrics[glyph_id][0]
            # The OpenType spec doesn't exclude negative widths, but only positive
            # widths seems to be the assumption in the wild?
            if width > 0:
                count += 1
                width_sum += width

        expected_value = int(round(width_sum / count))
    else:  # Version 2 and below only consider lowercase latin glyphs and space.
        calculation_rule = ("the weighted average of the widths of the latin"
                            " lowercase glyphs in the font")
        weightFactors = {
            'a': 64,
            'b': 14,
            'c': 27,
            'd': 35,
            'e': 100,
            'f': 20,
            'g': 14,
            'h': 42,
            'i': 63,
            'j': 3,
            'k': 6,
            'l': 35,
            'm': 20,
            'n': 56,
            'o': 56,
            'p': 17,
            'q': 4,
            'r': 49,
            's': 56,
            't': 71,
            'u': 31,
            'v': 10,
            'w': 18,
            'x': 3,
            'y': 18,
            'z': 2,
            'space': 166
        }
        glyph_order = ttFont.getGlyphOrder()
        if not all(character in glyph_order for character in weightFactors):
            yield FAIL,\
                  Message("missing-glyphs",
                          "Font is missing the required"
                          " latin lowercase letters and/or space.")
            return

        width_sum = 0
        for glyph_id in weightFactors:
            width = ttFont['hmtx'].metrics[glyph_id][0]
            width_sum += (width * weightFactors[glyph_id])

        expected_value = int(width_sum / 1000.0 + 0.5)  # round to closest int

    difference = abs(current_value - expected_value)

    # We accept matches and off-by-ones due to rounding as correct.
    if current_value == expected_value or difference == 1:
        yield PASS, "OS/2 xAvgCharWidth value is correct."
    elif difference < ACCEPTABLE_ERROR:
        yield INFO, \
              Message("xAvgCharWidth-close",
                      f"OS/2 xAvgCharWidth is {current_value} but it should be"
                      f" {expected_value} which corresponds to {calculation_rule}."
                      f" These are similar values, which"
                      f" may be a symptom of the slightly different"
                      f" calculation of the xAvgCharWidth value in"
                      f" font editors. There's further discussion on"
                      f" this at https://github.com/googlefonts/fontbakery"
                      f"/issues/1622")
    else:
        yield WARN, \
              Message("xAvgCharWidth-wrong",
                      f"OS/2 xAvgCharWidth is {current_value} but it should be"
                      f" {expected_value} which corresponds to {calculation_rule}.")


@check(
    id = 'com.adobe.fonts/check/fsselection_matches_macstyle',
    rationale = """
        The bold and italic bits in OS/2.fsSelection must match the bold and italic
        bits in head.macStyle per the OpenType spec.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/pull/2382'
)
def com_adobe_fonts_check_fsselection_matches_macstyle(ttFont):
    """Check if OS/2 fsSelection matches head macStyle bold and italic bits."""

    # Check both OS/2 and head are present.
    missing_tables = False

    required = ["OS/2", "head"]
    for key in required:
        if key not in ttFont:
            missing_tables = True
            yield FAIL,\
                  Message(f'lacks-{key}',
                          f"The '{key}' table is missing.")
    if missing_tables:
        return

    from fontbakery.constants import FsSelection, MacStyle
    failed = False
    head_bold = (ttFont['head'].macStyle & MacStyle.BOLD) != 0
    os2_bold = (ttFont['OS/2'].fsSelection & FsSelection.BOLD) != 0
    if head_bold != os2_bold:
        failed = True
        yield FAIL, \
              Message("fsselection-macstyle-bold",
                      "The OS/2.fsSelection and head.macStyle " \
                      "bold settings do not match.")
    head_italic = (ttFont['head'].macStyle & MacStyle.ITALIC) != 0
    os2_italic = (ttFont['OS/2'].fsSelection & FsSelection.ITALIC) != 0
    if head_italic != os2_italic:
        failed = True
        yield FAIL, \
              Message("fsselection-macstyle-italic",
                      "The OS/2.fsSelection and head.macStyle " \
                      "italic settings do not match.")
    if not failed:
        yield PASS, ("The OS/2.fsSelection and head.macStyle "
                     "bold and italic settings match.")


@check(
    id = 'com.adobe.fonts/check/family/bold_italic_unique_for_nameid1',
    conditions=['RIBBI_ttFonts'],
    rationale = """
        Per the OpenType spec: name ID 1 'is used in combination with Font Subfamily
        name (name ID 2), and should be shared among at most four fonts that differ
        only in weight or style.

        This four-way distinction should also be reflected in the OS/2.fsSelection
        field, using bits 0 and 5.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/pull/2388'
)
def com_adobe_fonts_check_family_bold_italic_unique_for_nameid1(RIBBI_ttFonts):
    """Check that OS/2.fsSelection bold & italic settings are unique
    for each NameID1"""
    from collections import Counter
    from fontbakery.utils import get_name_entry_strings
    from fontbakery.constants import NameID, FsSelection

    failed = False
    family_name_and_bold_italic = list()
    for ttFont in RIBBI_ttFonts:
        names_list = get_name_entry_strings(ttFont, NameID.FONT_FAMILY_NAME)
        # names_list will likely contain multiple entries, e.g. multiple copies
        # of the same name in the same language for different platforms, but
        # also different names in different languages, we use set() below
        # to remove the duplicates and only store the unique family name(s)
        # used for a given font
        names_set = set(names_list)

        bold = (ttFont['OS/2'].fsSelection & FsSelection.BOLD) != 0
        italic = (ttFont['OS/2'].fsSelection & FsSelection.ITALIC) != 0
        bold_italic = 'Bold=%r, Italic=%r' % (bold, italic)

        for name in names_set:
            family_name_and_bold_italic.append((name, bold_italic,))

    counter = Counter(family_name_and_bold_italic)

    for (family_name, bold_italic), count in counter.items():
        if count > 1:
            failed = True
            yield FAIL, \
                  Message("unique-fsselection",
                          f"Family '{family_name}' has {count} fonts"
                          f" (should be no more than 1) with the"
                          f" same OS/2.fsSelection bold & italic settings:"
                          f" {bold_italic}")
    if not failed:
        yield PASS, ("The OS/2.fsSelection bold & italic settings were unique "
                     "within each compatible family group.")


@check(
    id = 'com.google.fonts/check/code_pages',
    rationale = """
        At least some programs (such as Word and Sublime Text) under Windows 7
        do not recognize fonts unless code page bits are properly set on the
        ulCodePageRange1 (and/or ulCodePageRange2) fields of the OS/2 table.

        More specifically, the fonts are selectable in the font menu, but whichever
        Windows API these applications use considers them unsuitable for any
        character set, so anything set in these fonts is rendered with Arial as a
        fallback font.

        This check currently does not identify which code pages should be set.
        Auto-detecting coverage is not trivial since the OpenType specification
        leaves the interpretation of whether a given code page is "functional"
        or not open to the font developer to decide.

        So here we simply detect as a FAIL when a given font has no code page
        declared at all.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/issues/2474'
)
def com_google_fonts_check_code_pages(ttFont):
    """Check code page character ranges"""

    if "OS/2" not in ttFont:
        yield FAIL,\
              Message("lacks-OS/2",
                      "The required OS/2 table is missing.")
        return

    if not hasattr(ttFont['OS/2'], "ulCodePageRange1") or \
       not hasattr(ttFont['OS/2'], "ulCodePageRange2") or \
       (ttFont['OS/2'].ulCodePageRange1 == 0 and \
        ttFont['OS/2'].ulCodePageRange2 == 0):
        yield FAIL, \
              Message("no-code-pages",
                      "No code pages defined in the OS/2 table"
                      " ulCodePageRange1 and CodePageRange2 fields.")
    else:
        yield PASS, "At least one code page is defined."


# Lib/fontbakery/profiles/outline.py

from beziers.path import BezierPath

from fontbakery.callable import condition, check
from fontbakery.status import FAIL, PASS, WARN
from fontbakery.section import Section
from fontbakery.fonts_profile import profile_factory # NOQA pylint: disable=unused-import
from fontbakery.message import Message
from fontbakery.utils import bullet_list
import math


ALIGNMENT_MISS_EPSILON = 2  # Two point lee-way on alignment misses
SHORT_PATH_EPSILON = 0.006  # <0.6% of total outline length makes a short segment
SHORT_PATH_ABSOLUTE_EPSILON = 3  # 3 units is a small outline
COLINEAR_EPSILON = 0.1  # Radians
JAG_AREA_EPSILON = 0.05  # <5% of total outline area makes a jaggy segment
JAG_ANGLE = 0.25  # Radians
FALSE_POSITIVE_CUTOFF = 100  # More than this and we don't make a report


@condition
def outlines_dict(ttFont):
    cmap = ttFont['cmap'].getBestCmap()
    return {(codepoint, glyphname): BezierPath.fromFonttoolsGlyph(ttFont, glyphname)
            for codepoint, glyphname in cmap.items()}


def close_but_not_on(yExpected, yTrue, tolerance):
    if yExpected == yTrue:
        return False
    if abs(yExpected - yTrue) <= tolerance:
        return True
    return False


@check(
    id = "com.google.fonts/check/outline_alignment_miss",
    rationale = f"""
        This check heuristically looks for on-curve points which are close to, but
        do not sit on, significant boundary coordinates. For example, a point which
        has a Y-coordinate of 1 or -1 might be a misplaced baseline point. As well as
        the baseline, here we also check for points near the x-height (but only for
        lowercase Latin letters), cap-height, ascender and descender Y coordinates.

        Not all such misaligned curve points are a mistake, and sometimes the design
        may call for points in locations near the boundaries. As this check is liable
        to generate significant numbers of false positives, it will pass if there are
        more than {FALSE_POSITIVE_CUTOFF} reported misalignments.
    """,
    conditions = ["outlines_dict"],
    proposal = 'https://github.com/googlefonts/fontbakery/pull/3088'
)
def com_google_fonts_check_outline_alignment_miss(ttFont, outlines_dict, config):
    """Are there any misaligned on-curve points?"""
    alignments = {
        "baseline": 0,
        "x-height": ttFont["OS/2"].sxHeight,
        "cap-height": ttFont["OS/2"].sCapHeight,
        "ascender": ttFont["OS/2"].sTypoAscender,
        "descender": ttFont["OS/2"].sTypoDescender,
    }
    warnings = []
    for glyph, outlines in outlines_dict.items():
        codepoint, glyphname = glyph
        for p in outlines:
            for node in p.asNodelist():
                if node.type == "offcurve":
                    continue
                for line, yExpected in alignments.items():
                    # skip x-height check for caps
                    if line == "x-height" and (
                        len(glyphname) > 1 or glyphname[0].isupper()
                    ):
                        continue
                    if close_but_not_on(yExpected, node.y, ALIGNMENT_MISS_EPSILON):
                        warnings.append(f"{glyphname} (U+{codepoint:04X}):"
                                        f" X={node.x},Y={node.y}"
                                        f" (should be at {line} {yExpected}?)")
        if len(warnings) > FALSE_POSITIVE_CUTOFF:
            # Let's not waste time.
            yield PASS, ("So many Y-coordinates of points were close to"
                         " boundaries that this was probably by design.")
            return

    if warnings:
        formatted_list = bullet_list(config, warnings, bullet="*")
        yield WARN,\
              Message("found-misalignments",
                      f"The following glyphs have on-curve points which"
                      f" have potentially incorrect y coordinates:\n\n"
                      f"{formatted_list}")
    else:
        yield PASS, "Y-coordinates of points fell on appropriate boundaries."


@check(
    id = "com.google.fonts/check/outline_short_segments",
    rationale = f"""
        This check looks for outline segments which seem particularly short (less
        than {SHORT_PATH_EPSILON:.1%} of the overall path length).

        This check is not run for variable fonts, as they may legitimately have
        short segments. As this check is liable to generate significant numbers
        of false positives, it will pass if there are more than
        {FALSE_POSITIVE_CUTOFF} reported short segments.
    """,
    conditions = ["outlines_dict",
                  "is_not_variable_font"],
    proposal = 'https://github.com/googlefonts/fontbakery/pull/3088'
)
def com_google_fonts_check_outline_short_segments(ttFont, outlines_dict, config):
    """Are any segments inordinately short?"""
    warnings = []
    for glyph, outlines in outlines_dict.items():
        codepoint, glyphname = glyph
        for p in outlines:
            outline_length = p.length
            segments = p.asSegments()
            if not segments:
                continue
            prev_was_line = len(segments[-1]) == 2
            for seg in p.asSegments():
                if math.isclose(seg.length, 0):  # That's definitely wrong
                    warnings.append(f"{glyphname} (U+{codepoint:04X})"
                                    f" contains a short segment {seg}")
                elif (
                    seg.length < SHORT_PATH_ABSOLUTE_EPSILON
                    or seg.length < SHORT_PATH_EPSILON * outline_length
                ) and (prev_was_line or len(seg) > 2):
                    warnings.append(f"{glyphname} (U+{codepoint:04X})"
                                    f" contains a short segment {seg}")
                prev_was_line = len(seg) == 2
        if len(warnings) > FALSE_POSITIVE_CUTOFF:
            yield PASS, ("So many short segments were found"
                         " that this was probably by design.")
            return

    if warnings:
        formatted_list = bullet_list(config, warnings, bullet="*")
        yield WARN,\
              Message("found-short-segments",
                      f"The following glyphs have segments which seem very short:\n\n"
                      f"{formatted_list}")
    else:
        yield PASS, "No short segments were found."


@check(
    id = "com.google.fonts/check/outline_colinear_vectors",
    rationale = """
        This check looks for consecutive line segments which have the same angle. This
        normally happens if an outline point has been added by accident.

        This check is not run for variable fonts, as they may legitimately have
        colinear vectors.
    """,
    conditions = ["outlines_dict",
                  "is_not_variable_font"],
    proposal = 'https://github.com/googlefonts/fontbakery/pull/3088'
)
def com_google_fonts_check_outline_colinear_vectors(ttFont, outlines_dict, config):
    """Do any segments have colinear vectors?"""
    warnings = []
    for glyph, outlines in outlines_dict.items():
        codepoint, glyphname = glyph
        for p in outlines:
            segments = p.asSegments()
            if not segments:
                continue
            for i in range(0, len(segments)):
                prev = segments[i - 1]
                this = segments[i]
                if len(prev) == 2 and len(this) == 2:
                    if (
                        abs(prev.tangentAtTime(0).angle - this.tangentAtTime(0).angle)
                        < COLINEAR_EPSILON
                    ):
                        warnings.append(f"{glyphname} (U+{codepoint:04X}):"
                                        f" {prev} -> {this}")
        if len(warnings) > FALSE_POSITIVE_CUTOFF:
            yield PASS, ("So many colinear vectors were found"
                         " that this was probably by design.")
            return

    if warnings:
        formatted_list = bullet_list(config, sorted(set(warnings)), bullet="*")
        yield WARN,\
              Message("found-colinear-vectors",
                      f"The following glyphs have colinear vectors:\n\n"
                      f"{formatted_list}")
    else:
        yield PASS, "No colinear vectors found."


@check(
    id = "com.google.fonts/check/outline_jaggy_segments",
    rationale = """
        This check heuristically detects outline segments which form a particularly
        small angle, indicative of an outline error. This may cause false positives
        in cases such as extreme ink traps, so should be regarded as advisory and
        backed up by manual inspection.
    """,
    conditions = ["outlines_dict",
                  "is_not_variable_font"],
    proposal = 'https://github.com/googlefonts/fontbakery/issues/3064'
)
def com_google_fonts_check_outline_jaggy_segments(ttFont, outlines_dict, config):
    """Do outlines contain any jaggy segments?"""
    warnings = []
    for glyph, outlines in outlines_dict.items():
        codepoint, glyphname = glyph
        for p in outlines:
            segments = p.asSegments()
            if not segments:
                continue
            for i in range(0, len(segments)):
                prev = segments[i - 1]
                this = segments[i]
                in_vector = prev.tangentAtTime(1) * -1
                out_vector = this.tangentAtTime(0)
                if not (in_vector.magnitude * out_vector.magnitude):
                    continue
                angle = (in_vector @ out_vector) / (
                    in_vector.magnitude * out_vector.magnitude
                )
                if not (-1 <= angle <= 1):
                    continue
                jag_angle = math.acos(angle)
                if abs(jag_angle) > JAG_ANGLE or jag_angle == 0:
                    continue
                warnings.append(f"{glyphname} (U+{codepoint:04X}):"
                                f" {prev}/{this} = {math.degrees(jag_angle)}")

    if warnings:
        formatted_list = bullet_list(config, sorted(warnings), bullet="*")
        yield WARN,\
              Message("found-jaggy-segments",
                      f"The following glyphs have jaggy segments:\n\n"
                      f"{formatted_list}")
    else:
        yield PASS, "No jaggy segments found."


@check(
    id = "com.google.fonts/check/outline_semi_vertical",
    rationale = """
        This check detects line segments which are nearly, but not quite, exactly horizontal or vertical. Sometimes such lines are created by design, but often they are indicative of a design error.

        This check is disabled for italic styles, which often contain nearly-upright lines.
    """,
    conditions = ["outlines_dict",
                  "is_not_variable_font",
                  "is_not_italic"],
    proposal = 'https://github.com/googlefonts/fontbakery/pull/3088'
)
def com_google_fonts_check_outline_semi_vertical(ttFont, outlines_dict, config):
    """Do outlines contain any semi-vertical or semi-horizontal lines?"""
    warnings = []
    for glyph, outlines in outlines_dict.items():
        codepoint, glyphname = glyph
        for p in outlines:
            segments = p.asSegments()
            if not segments:
                continue
            for s in segments:
                if len(s) != 2:
                    continue
                angle = math.degrees((s.end - s.start).angle)
                for yExpected in [-180, -90, 0, 90, 180]:
                    if close_but_not_on(angle, yExpected, 0.5):
                        warnings.append(f"{glyphname} (U+{codepoint:04X}): {s}")

    if warnings:
        formatted_list = bullet_list(config, sorted(warnings), bullet="*")
        yield WARN,\
             Message("found-semi-vertical",
                     f"The following glyphs have"
                     f" semi-vertical/semi-horizontal lines:\n"
                     f"\n"
                     f"{formatted_list}")
    else:
        yield PASS, "No semi-horizontal/semi-vertical lines found."


OUTLINE_PROFILE_IMPORTS = (
    ".",
    ("shared_conditions",),
)
profile_imports = (OUTLINE_PROFILE_IMPORTS,)
profile = profile_factory(default_section=Section("Outline Correctness Checks"))
OUTLINE_PROFILE_CHECKS = [
    "com.google.fonts/check/outline_alignment_miss",
    "com.google.fonts/check/outline_short_segments",
    "com.google.fonts/check/outline_colinear_vectors",
    "com.google.fonts/check/outline_jaggy_segments",
    "com.google.fonts/check/outline_semi_vertical",
]

profile.auto_register(globals())
profile.test_expected_checks(OUTLINE_PROFILE_CHECKS, exclusive=True)


# Lib/fontbakery/profiles/post.py

from fontbakery.callable import check
from fontbakery.status import FAIL, PASS, WARN
from fontbakery.message import Message
# used to inform get_module_profile whether and how to create a profile
from fontbakery.fonts_profile import profile_factory # NOQA pylint: disable=unused-import

profile_imports = [
    ('.shared_conditions', ('is_ttf', ))
]

@check(
    id = 'com.google.fonts/check/family/underline_thickness',
    rationale = """
        Dave C Lemon (Adobe Type Team) recommends setting the underline thickness to be consistent across the family.

        If thicknesses are not family consistent, words set on the same line which have different styles look strange.

        See also:
        https://twitter.com/typenerd1/status/690361887926697986
    """,
    proposal = 'legacy:check/008',
    misc_metadata = {
        'affects': [('InDesign', 'unspecified')]
    }
)
def com_google_fonts_check_family_underline_thickness(ttFonts):
    """Fonts have consistent underline thickness?"""
    underTs = {}
    underlineThickness = None
    failed = False
    for ttfont in ttFonts:
        fontname = ttfont.reader.file.name
        # stylename = style(fontname)
        ut = ttfont['post'].underlineThickness
        underTs[fontname] = ut
        if underlineThickness is None:
            underlineThickness = ut
        if ut != underlineThickness:
            failed = True

    if failed:
        msg = ("Thickness of the underline is not"
               " the same across this family. In order to fix this,"
               " please make sure that the underlineThickness value"
               " is the same in the 'post' table of all of this family"
               " font files.\n"
               "Detected underlineThickness values are:\n")
        for style in underTs.keys():
            msg += f"\t{style}: {underTs[style]}\n"
        yield FAIL, Message("inconsistent-underline-thickness", msg)
    else:
        yield PASS, "Fonts have consistent underline thickness."


@check(
    id = 'com.google.fonts/check/post_table_version',
    rationale = """
        Format 2.5 of the 'post' table was deprecated in OpenType 1.3 and
        should not be used.

        According to Thomas Phinney, the possible problem with post format 3
        is that under the right combination of circumstances, one can generate
        PDF from a font with a post format 3 table, and not have accurate backing
        store for any text that has non-default glyphs for a given codepoint.
        
        It will look fine but not be searchable. This can affect Latin text with
        high-end typography, and some complex script writing systems, especially
        with higher-quality fonts. Those circumstances generally involve creating
        a PDF by first printing a PostScript stream to disk, and then creating a
        PDF from that stream without reference to the original source document.
        There are some workflows where this applies,but these are not common
        use cases.

        Apple recommends against use of post format version 4 as "no longer
        necessary and should be avoided". Please see the Apple TrueType reference
        documentation for additional details.
         
        https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6post.html

        Acceptable post format versions are 2 and 3 for TTF and OTF CFF2 builds,
        and post format 3 for CFF builds.
    """,
    proposal = ['legacy:check/015',
                'https://github.com/google/fonts/issues/215',
                'https://github.com/googlefonts/fontbakery/issues/2638',
                'https://github.com/googlefonts/fontbakery/issues/3635']
)
def com_google_fonts_check_post_table_version(ttFont):
    """Font has correct post table version?"""
    formatType = ttFont['post'].formatType
    is_cff = "CFF " in ttFont

    if is_cff and formatType != 3:
        yield FAIL, \
              Message("post-table-version",
                      "CFF fonts must contain post format 3 table.")
    elif not is_cff and formatType == 3:
        yield WARN, \
              Message("post-table-version",
                      "Post table format 3 use has niche use case problems."
                      "Please review the check rationale for additional details.")
    elif formatType == 2.5:
        yield FAIL, \
              Message("post-table-version",
                      "Post format 2.5 was deprecated in OpenType 1.3 and should" 
                      "not be used.")
    elif formatType == 4:
        yield FAIL, \
              Message("post-table-version",
                      "According to Apple documentation, post format 4 tables are" 
                      "no longer necessary and should not be used.")
    else:
        yield PASS, f"Font has an acceptable post format {formatType} table version."


# Lib/fontbakery/profiles/shaping.py

# Copyright 2020 Google Sans Authors
# Copyright 2021 Simon Cozens

# 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 json
import sys
import textwrap
from difflib import ndiff
from pathlib import Path
from fontbakery.callable import check, condition
from fontbakery.checkrunner import FAIL, PASS, SKIP
from fontbakery.fonts_profile import profile_factory
from fontbakery.message import Message
from fontbakery.section import Section
from vharfbuzz import Vharfbuzz, FakeBuffer
from os.path import basename, relpath
from stringbrewer import StringBrewer
from collidoscope import Collidoscope

shaping_basedir = Path("qa", "shaping_tests")

profile_imports = ()
profile = profile_factory(default_section=Section("Shaping Checks"))

SHAPING_PROFILE_CHECKS = [
    "com.google.fonts/check/shaping/regression",
    "com.google.fonts/check/shaping/forbidden",
    "com.google.fonts/check/shaping/collides",
]

STYLESHEET = """
<style type="text/css">
    @font-face {font-family: "TestFont"; src: url(%s);}
    .tf { font-family: "TestFont"; }
    .shaping pre { font-size: 1.2rem; }
    .shaping li {
        font-size: 1.2rem;
        border-top: 1px solid #ddd;
        padding: 12px;
        margin-top: 12px;
    }
    .shaping-svg {
        height: 100px;
        margin: 10px;
        transform: matrix(1, 0, 0, -1, 0, 0);
    }
</style>
"""


def get_stylesheet(vharfbuzz):
    filename = Path(vharfbuzz.filename)
    return STYLESHEET % relpath(filename, shaping_basedir)


def fix_svg(svg):
    return svg.replace('<svg', '<svg class="shaping-svg"')


def create_report_item(vharfbuzz,
                       message,
                       text=None,
                       buf1=None,
                       buf2=None,
                       type="item",
                       note=None,
                       extra_data=None):
    if text:
        message += f': <span class="tf">{text}</span>'

    if type == "item":
        message = f"<li>{message}"
        if note:
            message += f" ({note})"
        message += "</li>\n"
    elif type == "header":
        message = get_stylesheet(vharfbuzz) + f"\n<h4>{message}</h4>\n"

    if extra_data:
        message += f"\n\n<pre>{extra_data}</pre>\n\n"

    serialized_buf1 = None
    serialized_buf2 = None
    if buf2:
        if isinstance(buf2, FakeBuffer):
            try:
                serialized_buf2 = vharfbuzz.serialize_buf(buf2)
            except Exception:
                # This may fail if the glyphs are not found in the font
                serialized_buf2 = None
                buf2 = None  # Don't try to draw it either
        else:
            serialized_buf2 = buf2
        message += f"\n\n<pre>Expected: {serialized_buf2}</pre>\n\n"

    if buf1:
        serialized_buf1 = vharfbuzz.serialize_buf(buf1,
                                                  glyphsonly=(buf2 and
                                                              isinstance(buf2, str)))
        message += f"\n\n<pre>Got     : {serialized_buf1}</pre>\n\n"

    # Report a diff table
    if serialized_buf1 and serialized_buf2:
        diff = list(ndiff([serialized_buf1], [serialized_buf2]))
        if diff and diff[-1][0] == "?":
            message += f"\n\n<pre>         {diff[-1][1:]}</pre>\n\n"

    # Now draw it as SVG
    if buf1:
        message += "\nGot: " + fix_svg(vharfbuzz.buf_to_svg(buf1))

    if buf2 and isinstance(buf2, FakeBuffer):
        try:
            message += " Expected: " + fix_svg(vharfbuzz.buf_to_svg(buf2))
        except KeyError:
            pass

    return f'<div class="shaping">\n\n{message}\n\n</div>'


def get_from_test_with_default(test, configuration, el, default=None):
    defaults = configuration.get("defaults", {})
    return test.get(el, defaults.get(el, default))


def get_shaping_parameters(test, configuration):
    params = {}
    for el in ["script", "language", "direction", "features", "shaper"]:
        params[el] = get_from_test_with_default(test, configuration, el)
    return params


# This is a very generic "do something with shaping" test runner.
# It'll be given concrete meaning later.
def run_a_set_of_shaping_tests(config,
                               ttFont,
                               run_a_test,
                               test_filter,
                               generate_report,
                               preparation=None):
    filename = Path(ttFont.reader.file.name)
    vharfbuzz = Vharfbuzz(filename)
    shaping_file_found = False
    ran_a_test = False
    extra_data = None
    if "com.google.fonts/check/shaping" not in config:
        yield SKIP, "Shaping test directory not defined in configuration file"
        return

    shaping_basedir = config["com.google.fonts/check/shaping"].get("test_directory")
    if not shaping_basedir:
        yield SKIP, "Shaping test directory not defined in configuration file"
        return

    for shaping_file in Path(shaping_basedir).glob("*.json"):
        shaping_file_found = True
        try:
            shaping_input_doc = json.loads(shaping_file.read_text())
        except Exception as e:
            yield FAIL,\
                  Message("shaping-invalid-json",
                          f"{shaping_file}: Invalid JSON: {e}.")
            return

        configuration = shaping_input_doc.get("configuration", {})
        try:
            shaping_tests = shaping_input_doc["tests"]
        except KeyError:
            yield FAIL,\
                  Message("shaping-missing-tests",
                          f"{shaping_file}: JSON file must have a 'tests' key.")
            return

        if preparation:
            extra_data = preparation(ttFont, configuration)

        failed_shaping_tests = []
        for test in shaping_tests:
            if not test_filter(test, configuration):
                continue

            if not "input" in test:
                yield FAIL,\
                      Message("shaping-missing-input",
                              f"{shaping_file}: test is missing an input key.")
                return

            exclude_fonts = test.get("exclude", [])
            if basename(filename) in exclude_fonts:
                continue

            only_fonts = test.get("only")
            if only_fonts and basename(filename) not in only_fonts:
                continue

            run_a_test(filename,
                       vharfbuzz,
                       test,
                       configuration,
                       failed_shaping_tests,
                       extra_data)
            ran_a_test = True

        if ran_a_test:
            if not failed_shaping_tests:
                yield PASS, f"{shaping_file}: No regression detected"
            else:
                yield from generate_report(vharfbuzz,
                                           shaping_file,
                                           failed_shaping_tests)

    if not shaping_file_found:
        yield SKIP, "No test files found."

    if not ran_a_test:
        yield SKIP, "No applicable tests ran."


@check(
    id = "com.google.fonts/check/shaping/regression",
    rationale = """
        Fonts with complex layout rules can benefit from regression tests to ensure
        that the rules are behaving as designed. This checks runs a shaping test
        suite and compares expected shaping against actual shaping, reporting
        any differences.
        
        Shaping test suites should be written by the font engineer and referenced
        in the fontbakery configuration file. For more information about write
        shaping test files and how to configure fontbakery to read the shaping
        test suites, see https://simoncozens.github.io/tdd-for-otl/
    """,
    proposal = "https://github.com/googlefonts/fontbakery/pull/3223"
)
def com_google_fonts_check_shaping_regression(config, ttFont):
    """Check that texts shape as per expectation"""
    yield from run_a_set_of_shaping_tests(
        config,
        ttFont,
        run_shaping_regression,
        lambda test, configuration: "expectation" in test,
        gereate_shaping_regression_report,
    )


def run_shaping_regression(filename,
                           vharfbuzz,
                           test,
                           configuration,
                           failed_shaping_tests,
                           extra_data):
    shaping_text = test["input"]
    parameters = get_shaping_parameters(test, configuration)
    output_buf = vharfbuzz.shape(shaping_text, parameters)
    expectation = test["expectation"]
    if isinstance(expectation, dict):
        expectation = expectation.get(filename.name, expectation["default"])
    output_serialized = vharfbuzz.serialize_buf(output_buf,
                                                glyphsonly="+" not in expectation)

    if output_serialized != expectation:
        failed_shaping_tests.append((test, expectation, output_buf, output_serialized))


def gereate_shaping_regression_report(vharfbuzz, shaping_file, failed_shaping_tests):
    report_items = []
    header = f"{shaping_file}: Expected and actual shaping not matching"
    report_items.append(create_report_item(vharfbuzz, header, type="header"))
    for test, expected, output_buf, output_serialized in failed_shaping_tests:
        extra_data = {
            k: test[k]
            for k in ["script", "language", "direction", "features"]
            if k in test
        }
        # Make HTML report here.
        if "=" in expected:
            buf2 = vharfbuzz.buf_from_string(expected)
        else:
            buf2 = expected

        report_item = create_report_item(vharfbuzz,
                                         "Shaping did not match",
                                         text=test["input"],
                                         buf1=output_buf,
                                         buf2=buf2,
                                         note=test.get("note"),
                                         extra_data=extra_data)
        report_items.append(report_item)

    yield FAIL,\
          Message("shaping-regression",
                  header + "\n" + "\n".join(report_items))


@check(
    id="com.google.fonts/check/shaping/forbidden",
    rationale="""
        Fonts with complex layout rules can benefit from regression tests to ensure
        that the rules are behaving as designed. This checks runs a shaping test
        suite and reports if any glyphs are generated in the shaping which should
        not be produced. (For example, .notdef glyphs, visible viramas, etc.)
        
        Shaping test suites should be written by the font engineer and referenced in
        the Font Bakery configuration file. For more information about write shaping
        test files and how to configure fontbakery to read the shaping test suites,
        see https://simoncozens.github.io/tdd-for-otl/
    """,
    proposal = "https://github.com/googlefonts/fontbakery/pull/3223"
)
def com_google_fonts_check_shaping_forbidden(config, ttFont):
    """Check that no forbidden glyphs are found while shaping"""
    yield from run_a_set_of_shaping_tests(
        config,
        ttFont,
        run_forbidden_glyph_test,
        lambda test, configuration: "forbidden_glyphs" in configuration,
        forbidden_glyph_test_results,
    )


def run_forbidden_glyph_test(
    filename, vharfbuzz, test, configuration, failed_shaping_tests, extra_data
):
    is_stringbrewer = (
        get_from_test_with_default(test, configuration, "input_type", "string")
        == "pattern"
    )
    parameters = get_shaping_parameters(test, configuration)
    forbidden_glyphs = configuration["forbidden_glyphs"]
    if is_stringbrewer:
        sb = StringBrewer(
            recipe=test["input"], ingredients=configuration["ingredients"]
        )
        strings = sb.generate_all()
    else:
        strings = [test["input"]]

    for shaping_text in strings:
        output_buf = vharfbuzz.shape(shaping_text, parameters)
        output_serialized = vharfbuzz.serialize_buf(output_buf, glyphsonly=True)
        glyph_names = output_serialized.split("|")
        for forbidden in forbidden_glyphs:
            if forbidden in glyph_names:
                failed_shaping_tests.append((shaping_text, output_buf, forbidden))


def forbidden_glyph_test_results(vharfbuzz, shaping_file, failed_shaping_tests):
    report_items = []
    msg = f"{shaping_file}: Forbidden glyphs found while shaping"
    report_items.append(create_report_item(vharfbuzz, msg, type="header"))
    for shaping_text, buf, forbidden in failed_shaping_tests:
        msg = f"{shaping_text} produced '{forbidden}'"
        report_items.append(create_report_item(vharfbuzz,
                                               msg,
                                               text=shaping_text,
                                               buf1=buf))

    yield FAIL,\
          Message("shaping-forbidden",
                  msg + ".\n" + "\n".join(report_items))


@check(
    id = "com.google.fonts/check/shaping/collides",
    rationale = """
        Fonts with complex layout rules can benefit from regression tests to ensure
        that the rules are behaving as designed. This checks runs a shaping test
        suite and reports instances where the glyphs collide in unexpected ways.
        
        Shaping test suites should be written by the font engineer and referenced
        in the fontbakery configuration file. For more information about write
        shaping test files and how to configure fontbakery to read the shaping
        test suites, see https://simoncozens.github.io/tdd-for-otl/
    """,
    proposal = "https://github.com/googlefonts/fontbakery/pull/3223"
)
def com_google_fonts_check_shaping_collides(config, ttFont):
    """Check that no collisions are found while shaping"""
    yield from run_a_set_of_shaping_tests(
        config,
        ttFont,
        run_collides_glyph_test,
        lambda test, configuration: "collidoscope" in test
        or "collidoscope" in configuration,
        collides_glyph_test_results,
        setup_glyph_collides,
    )


def setup_glyph_collides(ttFont, configuration):
    filename = Path(ttFont.reader.file.name)
    collidoscope_configuration = configuration.get("collidoscope")
    if not collidoscope_configuration:
        return {
            "bases": True,
            "marks": True,
            "faraway": True,
            "adjacent_clusters": True,
        }
    col = Collidoscope(filename,
                       collidoscope_configuration,
                       direction=configuration.get("direction", "LTR"))
    return {"collidoscope": col}


def run_collides_glyph_test(filename,
                            vharfbuzz,
                            test,
                            configuration,
                            failed_shaping_tests,
                            extra_data):
    col = extra_data["collidoscope"]
    is_stringbrewer = get_from_test_with_default(test,
                                                 configuration,
                                                 "input_type",
                                                 "string") == "pattern"
    parameters = get_shaping_parameters(test, configuration)
    allowed_collisions = get_from_test_with_default(test,
                                                    configuration,
                                                    "allowedcollisions",
                                                    [])
    if is_stringbrewer:
        sb = StringBrewer(recipe=test["input"],
                          ingredients=configuration["ingredients"])
        strings = sb.generate_all()
    else:
        strings = [test["input"]]

    for shaping_text in strings:
        output_buf = vharfbuzz.shape(shaping_text, parameters)
        glyphs = col.get_glyphs(shaping_text, buf=output_buf)
        collisions = col.has_collisions(glyphs)
        bumps = [f"{c.glyph1}/{c.glyph2}" for c in collisions]
        bumps = [b for b in bumps if b not in allowed_collisions]
        if bumps:
            draw = fix_svg(col.draw_overlaps(glyphs, collisions))
            failed_shaping_tests.append((shaping_text, bumps, draw, output_buf))


def collides_glyph_test_results(vharfbuzz, shaping_file, failed_shaping_tests):
    report_items = []
    seen_bumps = {}
    msg = f"{shaping_file}: {len(failed_shaping_tests)} collisions found while shaping"
    report_items.append(create_report_item(vharfbuzz, msg, type="header"))
    for shaping_text, bumps, draw, buf in failed_shaping_tests:
        # Make HTML report here.
        if tuple(bumps) in seen_bumps:
            continue
        seen_bumps[tuple(bumps)] = True
        report_item = create_report_item(vharfbuzz,
                                         f"{',' .join(bumps)} collision found in"
                                         f" e.g. <span class='tf'>{shaping_text}</span>"
                                         f" <div>{draw}</div>",
                                         buf1=buf)
        report_items.append(report_item)
    yield FAIL,\
          Message("shaping-collides",
                  msg + ".\n" + "\n".join(report_items))


profile.auto_register(globals())
profile.test_expected_checks(SHAPING_PROFILE_CHECKS, exclusive=True)


# Lib/fontbakery/profiles/shared_conditions.py

import os
from typing import List
from collections import Counter

from fontbakery.callable import condition
# used to inform get_module_profile whether and how to create a profile
from fontbakery.fonts_profile import profile_factory # NOQA pylint: disable=unused-import,cyclic-import


@condition
def ttFont(font):
    from fontTools.ttLib import TTFont
    return TTFont(font)


@condition
def is_ttf(ttFont):
    return 'glyf' in ttFont


@condition
def are_ttf(ttFonts):
    for f in ttFonts:
        if not is_ttf(f):
            return False
    # otherwise:
    return True


@condition
def is_cff(ttFont):
    return 'CFF ' in ttFont

@condition
def is_cff2(ttFont):
    return 'CFF2' in ttFont


@condition
def has_name_table(ttFont):
    return "name" in ttFont.keys()


@condition
def variable_font_filename(ttFont):
    from fontbakery.utils import get_name_entry_strings
    from fontbakery.constants import (MacStyle,
                                      NameID)
    familynames = get_name_entry_strings(ttFont, NameID.FONT_FAMILY_NAME)
    typo_familynames = get_name_entry_strings(ttFont, NameID.TYPOGRAPHIC_FAMILY_NAME)
    if familynames == []:
        return None

    familyname = typo_familynames[0] if typo_familynames else familynames[0]
    familyname = "".join(familyname.split(' ')) #remove spaces
    if bool(ttFont["head"].macStyle & MacStyle.ITALIC):
        familyname+="-Italic"

    tags = ttFont["fvar"].axes
    tags = list(map(lambda t: t.axisTag, tags))
    tags.sort()
    tags = "[{}]".format(",".join(tags))
    return f"{familyname}{tags}.ttf"


@condition
def designSpace(designspace):
    """
    Given a filepath for a designspace file, parse it
    and return a DesignSpaceDocument, which is
    'an object to read, write and edit
    interpolation systems for typefaces'.
    """
    if designspace:
        from fontTools.designspaceLib import DesignSpaceDocument
        import defcon
        DS = DesignSpaceDocument.fromfile(designspace)
        DS.loadSourceFonts(defcon.Font)
        return DS


@condition
def designspace_sources(designSpace):
    """
    Given a DesignSpaceDocument object,
    return a set of UFO font sources.
    """
    if designSpace:
        import defcon
        return designSpace.loadSourceFonts(defcon.Font)


@condition
def family_directory(font):
    """Get the path of font project directory."""
    if font:
        dirname = os.path.dirname(font)
        if dirname == '':
            dirname = '.'
        return dirname


@condition
def sibling_directories(family_directory):
    """
    Given a directory, this function tries to figure out where else in the filesystem
    other related "sibling" families might be located.
    This is guesswork and may not be able to find font files in other folders not yet
    covered by this routine. We may improve this in the future by adding other
    smarter filesystem lookup procedures or even by letting the user feed explicit
    sibling family paths.

    This function returs a list of paths to directories where related font files were detected.
    """
    SIBLING_SUFFIXES = ["sans",
                        "sc",
                        "narrow",
                        "text",
                        "display",
                        "condensed"]

    base_family_dir = family_directory
    for suffix in SIBLING_SUFFIXES:
        if family_directory.endswith(suffix):
            candidate = family_directory[:-len(suffix)]
            if os.path.isdir(candidate):
                base_family_dir = candidate
                break

    directories = [base_family_dir]
    for suffix in SIBLING_SUFFIXES:
        candidate = base_family_dir + suffix
        if os.path.isdir(candidate):
            directories.append(candidate)

    return directories


@condition
def superfamily(sibling_directories):
    """
    Given a list of directories, this functions looks for font files
    and returs a list of lists of the detected filepaths.
    """
    result = []
    for family_dir in sibling_directories:
        filepaths = []
        for entry in os.listdir(family_dir):
            if entry[-4:] in [".otf", ".ttf"]:
                filepaths.append(os.path.join(family_dir, entry))
        result.append(filepaths)
    return result


@condition
def superfamily_ttFonts(superfamily):
    from fontTools.ttLib import TTFont
    result = []
    for family in superfamily:
        result.append([TTFont(f) for f in family])
    return result


@condition
def ligatures(ttFont):
    from fontTools.ttLib.tables.otTables import LigatureSubst

    all_ligatures = {}
    try:
        if "GSUB" in ttFont and ttFont["GSUB"].table.LookupList:
            for record in ttFont["GSUB"].table.FeatureList.FeatureRecord:
                if record.FeatureTag == 'liga':
                    for index in record.Feature.LookupListIndex:
                        lookup = ttFont["GSUB"].table.LookupList.Lookup[index]
                        for subtable in lookup.SubTable:
                            if isinstance(subtable, LigatureSubst):
                                for firstGlyph in subtable.ligatures.keys():
                                    all_ligatures[firstGlyph] = []
                                    for lig in subtable.ligatures[firstGlyph]:
                                        if lig.Component not in all_ligatures[firstGlyph]:
                                            all_ligatures[firstGlyph].append(lig.Component)
        return all_ligatures
    except:
        return -1 # Indicate fontTools-related crash...


@condition
def ligature_glyphs(ttFont):
    from fontTools.ttLib.tables.otTables import LigatureSubst

    all_ligature_glyphs = []
    try:
        if "GSUB" in ttFont and ttFont["GSUB"].table.LookupList:
            for record in ttFont["GSUB"].table.FeatureList.FeatureRecord:
                if record.FeatureTag == 'liga':
                    for index in record.Feature.LookupListIndex:
                        lookup = ttFont["GSUB"].table.LookupList.Lookup[index]
                        for subtable in lookup.SubTable:
                            if isinstance(subtable, LigatureSubst):
                                for firstGlyph in subtable.ligatures.keys():
                                    for lig in subtable.ligatures[firstGlyph]:
                                        if lig.LigGlyph not in all_ligature_glyphs:
                                            all_ligature_glyphs.append(lig.LigGlyph)
        return all_ligature_glyphs
    except:
        return -1  # Indicate fontTools-related crash...


@condition
def glyph_metrics_stats(ttFont):
    """Returns a dict containing whether the font seems_monospaced,
    what's the maximum glyph width and what's the most common width.

    For a font to be considered monospaced, if at least 80% of ASCII
    characters have glyphs, then at least 80% of those must have the same
    width, otherwise all glyphs of printable characters must have one of
    two widths or be zero-width.
    """
    glyph_metrics = ttFont['hmtx'].metrics
    # NOTE: `range(a, b)` includes `a` and does not include `b`.
    #       Here we don't include 0-31 as well as 127
    #       because these are control characters.
    ascii_glyph_names = [ttFont.getBestCmap()[c] for c in range(32, 127)
                         if c in ttFont.getBestCmap()]

    if len(ascii_glyph_names) > 0.8 * (127 - 32):
        ascii_widths = [adv for name, (adv, lsb) in glyph_metrics.items()
                        if name in ascii_glyph_names and adv != 0]
        ascii_width_count = Counter(ascii_widths)
        ascii_most_common_width = ascii_width_count.most_common(1)[0][1]
        seems_monospaced = ascii_most_common_width >= len(ascii_widths) * 0.8
    else:
        from fontTools import unicodedata
        # Collect relevant glyphs.
        relevant_glyph_names = set()
        # Add character glyphs that are in one of these categories:
        # Letter, Mark, Number, Punctuation, Symbol, Space_Separator.
        # This excludes Line_Separator, Paragraph_Separator and Control.
        for value, name in ttFont.getBestCmap().items():
            if unicodedata.category(chr(value)).startswith(
                ("L", "M", "N", "P", "S", "Zs")
            ):
                relevant_glyph_names.add(name)
        # Remove character glyphs that are mark glyphs.
        gdef = ttFont.get("GDEF")
        if gdef and gdef.table.GlyphClassDef:
            marks = {name
                     for name, c in gdef.table.GlyphClassDef.classDefs.items()
                     if c == 3
                    }
            relevant_glyph_names.difference_update(marks)

        widths = sorted({adv for name, (adv, lsb) in glyph_metrics.items()
                         if name in relevant_glyph_names and adv != 0})
        seems_monospaced = len(widths) <= 2

    width_max = max([adv for k, (adv, lsb) in glyph_metrics.items()])
    most_common_width = Counter([g for g in glyph_metrics.values()
                                 if g[0] != 0]).most_common(1)[0][0][0]
    return {
        "seems_monospaced": seems_monospaced,
        "width_max": width_max,
        "most_common_width": most_common_width,
    }


@condition
def missing_whitespace_chars(ttFont):
    from fontbakery.utils import get_glyph_name
    space = get_glyph_name(ttFont, 0x0020)
    nbsp = get_glyph_name(ttFont, 0x00A0)
    # tab = get_glyph_name(ttFont, 0x0009)

    missing = []
    if space is None: missing.append("0x0020")
    if nbsp is None: missing.append("0x00A0")
    # fonts probably don't need an actual tab char
    # if tab is None: missing.append("0x0009")
    return missing


@condition
def vmetrics(ttFonts):
    from fontbakery.utils import get_bounding_box
    v_metrics = {"ymin": 0, "ymax": 0}
    for ttFont in ttFonts:
        font_ymin, font_ymax = get_bounding_box(ttFont)
        v_metrics["ymin"] = min(font_ymin, v_metrics["ymin"])
        v_metrics["ymax"] = max(font_ymax, v_metrics["ymax"])
    return v_metrics


@condition
def is_variable_font(ttFont):
    return "fvar" in ttFont.keys()

@condition
def is_not_variable_font(ttFont):
    return "fvar" not in ttFont.keys()

@condition
def VFs(ttFonts):
    """Returns a list of font files which are recognized as variable fonts"""
    return [ttFont for ttFont in ttFonts
            if is_variable_font(ttFont)]

@condition
def slnt_axis(ttFont):
    if "fvar" in ttFont:
        for axis in ttFont["fvar"].axes:
            if axis.axisTag == "slnt":
                return axis

@condition
def opsz_axis(ttFont):
    if "fvar" in ttFont:
        for axis in ttFont["fvar"].axes:
            if axis.axisTag == "opsz":
                return axis

@condition
def ital_axis(ttFont):
    if "fvar" in ttFont:
        for axis in ttFont["fvar"].axes:
            if axis.axisTag == "ital":
                return axis

@condition
def grad_axis(ttFont):
    if "fvar" in ttFont:
        for axis in ttFont["fvar"].axes:
            if axis.axisTag == "GRAD":
                return axis

def get_instance_axis_value(ttFont, instance_name, axis_tag):
    if not is_variable_font(ttFont):
        return None

    instance = None
    for i in ttFont["fvar"].instances:
        name = ttFont["name"].getDebugName(i.subfamilyNameID)
        if name == instance_name:
            instance = i
            break

    if instance:
        for axis in ttFont["fvar"].axes:
            if axis.axisTag == axis_tag:
                return instance.coordinates[axis_tag]


@condition
def regular_wght_coord(ttFont):
    return get_instance_axis_value(ttFont, "Regular", "wght")


@condition
def bold_wght_coord(ttFont):
    return get_instance_axis_value(ttFont, "Bold", "wght")


@condition
def regular_wdth_coord(ttFont):
    return get_instance_axis_value(ttFont, "Regular", "wdth")


@condition
def regular_slnt_coord(ttFont):
    return get_instance_axis_value(ttFont, "Regular", "slnt")


@condition
def regular_ital_coord(ttFont):
    return get_instance_axis_value(ttFont, "Regular", "ital")


@condition
def regular_opsz_coord(ttFont):
    return get_instance_axis_value(ttFont, "Regular", "opsz")


@condition
def vtt_talk_sources(ttFont) -> List[str]:
    """Return the tags of VTT source tables found in a font."""
    VTT_SOURCE_TABLES = {'TSI0', 'TSI1', 'TSI2', 'TSI3', 'TSI5'}
    tables_found = [tag for tag in ttFont.keys() if tag in VTT_SOURCE_TABLES]
    return tables_found


@condition
def preferred_cmap(ttFont):
    from fontbakery.utils import get_preferred_cmap
    return get_preferred_cmap(ttFont)


@condition
def unicoderange(ttFont):
    """Get an integer bitmap representing the UnicodeRange fields in the os/2 table."""
    os2 = ttFont['OS/2']
    return (os2.ulUnicodeRange1 |
            os2.ulUnicodeRange2 << 32 |
            os2.ulUnicodeRange3 << 64 |
            os2.ulUnicodeRange4 << 96)

@condition
def is_cjk_font(ttFont):
    """Test font object to confirm that it meets our definition of a CJK font file.

    The definition is met if any of the following conditions are True:
      1. The font has a CJK code page bit set in the OS/2 table
      2. The font has a CJK Unicode range bit set in the OS/2 table
      3. The font has any CJK Unicode code points defined in the cmap table
    """
    from fontbakery.constants import (CJK_CODEPAGE_BITS,
                                      CJK_UNICODE_RANGE_BITS,
                                      CJK_UNICODE_RANGES)
    os2 = ttFont["OS/2"]

    # OS/2 code page checks
    for _, bit in CJK_CODEPAGE_BITS.items():
        if os2.ulCodePageRange1 & (1 << bit):
            return True

    # OS/2 Unicode range checks
    for _, bit in CJK_UNICODE_RANGE_BITS.items():
        if bit in range(0, 32):
            if os2.ulUnicodeRange1 & (1 << bit):
                return True

        elif bit in range(32, 64):
            if os2.ulUnicodeRange2 & (1 << (bit-32)):
                return True

        elif bit in range(64, 96):
            if os2.ulUnicodeRange3 & (1 << (bit-64)):
                return True

    # defined CJK Unicode code point in cmap table checks
    cmap = ttFont.getBestCmap()
    for unicode_range in CJK_UNICODE_RANGES:
        for x in range(unicode_range[0], unicode_range[1]+1):
            if int(x) in cmap:
                return True

    # default, return False if the above checks did not identify a CJK font
    return False


@condition
def get_cjk_glyphs(ttFont):
    """Return all glyphs which belong to a CJK unicode block"""
    from fontbakery.constants import CJK_UNICODE_RANGES
    results = []
    cjk_unicodes = set()
    for start, end in CJK_UNICODE_RANGES:
        cjk_unicodes |= set(u for u in range(start, end+1))
    for uni, glyph_name in ttFont.getBestCmap().items():
        if uni in cjk_unicodes:
            results.append(glyph_name)
    return results


@condition
def typo_metrics_enabled(ttFont):
    return ttFont['OS/2'].fsSelection & 0b10000000 > 0


@condition
def is_indic_font(ttFont):
    INDIC_FONT_DETECTION_CODEPOINTS = [
        0x0988, # Bengali
        0x0908, # Devanagari
        0x0A88, # Gujarati
        0x0A08, # Gurmukhi
        0x0D08, # Kannada
        0x0B08, # Malayalam
        0xABC8, # Meetei Mayek
        0x1C58, # OlChiki
        0x0B08, # Oriya
        0x0B88, # Tamil
        0x0C08, # Telugu
    ]

    font_codepoints = ttFont['cmap'].getBestCmap().keys()
    for codepoint in INDIC_FONT_DETECTION_CODEPOINTS:
        if codepoint in font_codepoints:
            return True

    #otherwise:
    return False


def keyword_in_full_font_name(ttFont, keyword):
    from fontbakery.constants import (MacStyle,
                                      NameID)
    for entry in ttFont["name"].names:
        if entry.nameID == NameID.FULL_FONT_NAME and \
           keyword in entry.string.decode(entry.getEncoding()).lower().split():
            return True
    return False


@condition
def is_italic(ttFont):
    from fontbakery.constants import (FsSelection,
                                      MacStyle)
    return (
        ("OS/2" in ttFont and ttFont["OS/2"].fsSelection & FsSelection.ITALIC)
        or ("head" in ttFont and ttFont["head"].macStyle & MacStyle.ITALIC)
        or keyword_in_full_font_name(ttFont, "italic")
        or ("post" in ttFont and ttFont["post"].italicAngle)
    )


@condition
def is_bold(ttFont):
    from fontbakery.constants import (FsSelection,
                                      MacStyle)
    return (
        ("OS/2" in ttFont and ttFont["OS/2"].fsSelection & FsSelection.BOLD)
        or ("head" in ttFont and ttFont["head"].macStyle & MacStyle.BOLD)
        or keyword_in_full_font_name(ttFont, "bold")
    )


@condition
def is_not_italic(ttFont):
    return not is_italic(ttFont)


@condition
def is_not_bold(ttFont):
    return not is_bold(ttFont)


# Lib/fontbakery/profiles/stat.py

from fontbakery.callable import check
from fontbakery.status import FAIL, PASS
from fontbakery.message import Message
# used to inform get_module_profile whether and how to create a profile
from fontbakery.fonts_profile import profile_factory # NOQA pylint: disable=unused-import

profile_imports = [
    ('.shared_conditions', ('is_variable_font',))
]

@check(
    id = 'com.google.fonts/check/varfont/stat_axis_record_for_each_axis',
    rationale = """
        According to the OpenType spec, there must be an Axis Record
        for every axis defined in the fvar table.

        https://docs.microsoft.com/en-us/typography/opentype/spec/stat#axis-records
    """,
    conditions = ['is_variable_font'],
    proposal = 'https://github.com/googlefonts/fontbakery/pull/3017'
)
def com_google_fonts_check_varfont_stat_axis_record_for_each_axis(ttFont):
    """ All fvar axes have a correspondent Axis Record on STAT table? """
    fvar_axes = set(a.axisTag for a in ttFont['fvar'].axes)
    stat_axes = set(a.AxisTag for a in ttFont['STAT'].table.DesignAxisRecord.Axis)
    missing_axes = fvar_axes - stat_axes
    if len(missing_axes) > 0:
        yield FAIL,\
              Message("missing-axis-records",
                      f"STAT table is missing Axis Records for"
                      f" the following axes: {missing_axes}")
    else:
        yield PASS, "STAT table has all necessary Axis Records"



# Lib/fontbakery/profiles/typenetwork.py

"""
Checks for Font Bureau.
"""

from fontbakery.callable import check
from fontbakery.section import Section
from fontbakery.status import PASS, FAIL, WARN
from fontbakery.fonts_profile import profile_factory
from fontbakery.message import Message
from fontbakery.profiles.universal import UNIVERSAL_PROFILE_CHECKS

profile_imports = ('fontbakery.profiles.universal',)
profile = profile_factory(default_section=Section("Type Network"))

TYPENETWORK_PROFILE_CHECKS = \
    UNIVERSAL_PROFILE_CHECKS + [
        'io.github.abysstypeco/check/ytlc_sanity'
    ]

@check(
    id = 'io.github.abysstypeco/check/ytlc_sanity',
    rationale = """
        This check follows the values of the ytlc axis proposed by Font Bureau.
    """,
    conditions=["is_variable_font"],
    proposal = 'https://github.com/googlefonts/fontbakery/issues/3130'
)
def io_github_abysstypeco_check_ytlc_sanity(ttFont):
    """Check if ytlc values are sane in vf"""
    passed = True

    for axis in ttFont['fvar'].axes:
        if not axis.axisTag == 'ytlc': continue

        if axis.minValue < 0 or axis.maxValue > 1000:
            passed = False
            yield FAIL,\
                  Message("invalid-range",
                          f'The range of ytlc values'
                          f' ({axis.minValue} - {axis.maxValue}) does not conform'
                          f' to the expected range of ytlc which'
                          f' should be min value 0 to max value 1000')
    if passed:
        yield PASS, 'ytlc is sane'


profile.auto_register(globals())


profile.test_expected_checks(TYPENETWORK_PROFILE_CHECKS, exclusive=True)


# Lib/fontbakery/profiles/ufo_sources.py

import os

from fontbakery.callable import check, condition
from fontbakery.callable import FontBakeryExpectedValue as ExpectedValue
from fontbakery.status import ERROR, FAIL, PASS, WARN
from fontbakery.section import Section
from fontbakery.message import Message
from fontbakery.fonts_profile import profile_factory


profile = profile_factory(default_section=Section("UFO Sources"))

UFO_PROFILE_CHECKS = [
    'com.daltonmaag/check/ufolint',
    'com.daltonmaag/check/ufo-required-fields',
    'com.daltonmaag/check/ufo-recommended-fields',
    'com.daltonmaag/check/ufo-unnecessary-fields'
]


@condition
def ufo_font(ufo):
    try:
        import defcon
        return defcon.Font(ufo)
    except:
        return None


@check(
    id = 'com.daltonmaag/check/ufolint',
    proposal = 'https://github.com/googlefonts/fontbakery/pull/1736'
)
def com_daltonmaag_check_ufolint(ufo):
    """Run ufolint on UFO source directory."""
    import subprocess
    ufolint_cmd = ["ufolint", ufo]

    try:
        subprocess.check_output(ufolint_cmd, stderr=subprocess.STDOUT)
    except subprocess.CalledProcessError as e:
        yield FAIL, \
              Message("ufolint-fail",
                      ("ufolint failed the UFO source. Output follows :"
                      "\n\n{}\n").format(e.output.decode()))
    except OSError:
        yield ERROR, \
              Message("ufolint-unavailable",
                      "ufolint is not available!")
    else:
        yield PASS, "ufolint passed the UFO source."


@check(
    id = 'com.daltonmaag/check/ufo-required-fields',
    conditions = ['ufo_font'],
    rationale = """
        ufo2ft requires these info fields to compile a font binary:
        unitsPerEm, ascender, descender, xHeight, capHeight and familyName.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/pull/1736'
)
def com_daltonmaag_check_required_fields(ufo_font):
    """Check that required fields are present in the UFO fontinfo."""
    required_fields = []

    for field in ["unitsPerEm",
                  "ascender",
                  "descender",
                  "xHeight",
                  "capHeight",
                  "familyName"]:
        if ufo_font.info.__dict__.get("_" + field) is None:
            required_fields.append(field)

    if required_fields:
        yield FAIL, \
              Message("missing-required-fields",
                      f"Required field(s) missing: {required_fields}")
    else:
        yield PASS, "Required fields present."


@check(
    id = 'com.daltonmaag/check/ufo-recommended-fields',
    conditions = ['ufo_font'],
    rationale = """
        This includes fields that should be in any production font.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/pull/1736'
)
def com_daltonmaag_check_recommended_fields(ufo_font):
    """Check that recommended fields are present in the UFO fontinfo."""
    recommended_fields = []

    for field in ["postscriptUnderlineThickness",
                  "postscriptUnderlinePosition",
                  "versionMajor",
                  "versionMinor",
                  "styleName",
                  "copyright",
                  "openTypeOS2Panose"]:
        if ufo_font.info.__dict__.get("_" + field) is None:
            recommended_fields.append(field)

    if recommended_fields:
        yield WARN, \
              Message("missing-recommended-fields",
                      f"Recommended field(s) missing: {recommended_fields}")
    else:
        yield PASS, "Recommended fields present."


@check(
    id = 'com.daltonmaag/check/ufo-unnecessary-fields',
    conditions = ['ufo_font'],
    rationale = """
        ufo2ft will generate these.

        openTypeOS2UnicodeRanges and openTypeOS2CodePageRanges are exempted
        because it is useful to toggle a range when not _all_ the glyphs in that
        region are present.

        year is deprecated since UFO v2.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/pull/1736'
)
def com_daltonmaag_check_unnecessary_fields(ufo_font):
    """Check that no unnecessary fields are present in the UFO fontinfo."""
    unnecessary_fields = []

    for field in ["openTypeNameUniqueID",
                  "openTypeNameVersion",
                  "postscriptUniqueID",
                  "year"]:
        if ufo_font.info.__dict__.get("_" + field) is not None:
            unnecessary_fields.append(field)

    if unnecessary_fields:
        yield WARN, \
              Message("unnecessary-fields",
                      f"Unnecessary field(s) present: {unnecessary_fields}")
    else:
        yield PASS, "Unnecessary fields omitted."


# The following fields are always generated empty by defcon:
# guidelines, postscriptBlueValues, postscriptOtherBlues,
# postscriptFamilyBlues, postscriptFamilyOtherBlues,
# postscriptStemSnapH, postscriptStemSnapV -- not sure if checking for that
# is useful.

profile.auto_register(globals())



# Lib/fontbakery/profiles/universal.py

import os

from fontbakery.status import PASS, FAIL, WARN, ERROR, INFO, SKIP
from fontbakery.section import Section
from fontbakery.callable import condition, check, disable
from fontbakery.message import Message
from fontbakery.fonts_profile import profile_factory
from fontbakery.profiles.opentype import OPENTYPE_PROFILE_CHECKS
from fontbakery.profiles.outline import OUTLINE_PROFILE_CHECKS
from fontbakery.profiles.shaping import SHAPING_PROFILE_CHECKS
from fontbakery.profiles.ufo_sources import UFO_PROFILE_CHECKS

profile_imports = ('fontbakery.profiles.opentype',
                   'fontbakery.profiles.outline',
                   'fontbakery.profiles.shaping',
                   'fontbakery.profiles.ufo_sources',
                   '.shared_conditions')
profile = profile_factory(default_section=Section("Universal"))

THIRDPARTY_CHECKS = [
    'com.google.fonts/check/ots',
]

SUPERFAMILY_CHECKS = [
    'com.google.fonts/check/superfamily/list',
    'com.google.fonts/check/superfamily/vertical_metrics',
]

DESIGNSPACE_CHECKS = [
    'com.google.fonts/check/designspace_has_sources',
    'com.google.fonts/check/designspace_has_default_master',
    'com.google.fonts/check/designspace_has_consistent_glyphset',
    'com.google.fonts/check/designspace_has_consistent_codepoints',
]

UNIVERSAL_PROFILE_CHECKS = \
    DESIGNSPACE_CHECKS + \
    OPENTYPE_PROFILE_CHECKS + \
    OUTLINE_PROFILE_CHECKS + \
    SHAPING_PROFILE_CHECKS + \
    SUPERFAMILY_CHECKS + \
    THIRDPARTY_CHECKS + \
    UFO_PROFILE_CHECKS + [
        'com.google.fonts/check/name/trailing_spaces',
        'com.google.fonts/check/family/win_ascent_and_descent',
        'com.google.fonts/check/os2_metrics_match_hhea',
        'com.google.fonts/check/fontbakery_version',
        'com.google.fonts/check/ttx-roundtrip',
        'com.google.fonts/check/family/single_directory',
        'com.google.fonts/check/mandatory_glyphs',
        'com.google.fonts/check/whitespace_glyphs',
        'com.google.fonts/check/whitespace_glyphnames',
        'com.google.fonts/check/whitespace_ink',
        'com.google.fonts/check/required_tables',
        'com.google.fonts/check/unwanted_tables',
        'com.google.fonts/check/valid_glyphnames',
        'com.google.fonts/check/unique_glyphnames',
#       'com.google.fonts/check/glyphnames_max_length',
        'com.google.fonts/check/family/vertical_metrics',
        'com.google.fonts/check/STAT_strings',
        'com.google.fonts/check/rupee',
        'com.google.fonts/check/unreachable_glyphs',
        'com.google.fonts/check/contour_count',
        'com.google.fonts/check/cjk_chws_feature',
        'com.google.fonts/check/transformed_components',
        'com.google.fonts/check/dotted_circle',
        'com.google.fonts/check/gpos7',
        'com.adobe.fonts/check/freetype_rasterizer',
    ]


@check(
    id = 'com.google.fonts/check/name/trailing_spaces',
    proposal = 'https://github.com/googlefonts/fontbakery/issues/2417'
)
def com_google_fonts_check_name_trailing_spaces(ttFont):
    """Name table records must not have trailing spaces."""
    failed = False
    for name_record in ttFont['name'].names:
        name_string = name_record.toUnicode()
        if name_string != name_string.strip():
            failed = True
            name_key = tuple([name_record.platformID,
                              name_record.platEncID,
                              name_record.langID,
                              name_record.nameID])
            shortened_str = name_record.toUnicode()
            if len(shortened_str) > 20:
                shortened_str = shortened_str[:10] + "[...]" + shortened_str[-10:]
            yield FAIL, \
                  Message("trailing-space",
                          f"Name table record with key = {name_key} has"
                          f" trailing spaces that must be removed:"
                          f" '{shortened_str}'")
    if not failed:
        yield PASS, ("No trailing spaces on name table entries.")


@check(
    id = 'com.google.fonts/check/family/win_ascent_and_descent',
    conditions = ['vmetrics',
                  'not is_cjk_font'],
    rationale = """
        A font's winAscent and winDescent values should be greater than or equal to
        the head table's yMax, abs(yMin) values. If they are less than these values,
        clipping can occur on Windows platforms
        (https://github.com/RedHatBrand/Overpass/issues/33).

        If the font includes tall/deep writing systems such as Arabic or Devanagari,
        the winAscent and winDescent can be greater than the yMax and abs(yMin)
        to accommodate vowel marks.

        When the win Metrics are significantly greater than the upm, the linespacing
        can appear too loose. To counteract this, enabling the OS/2 fsSelection
        bit 7 (Use_Typo_Metrics), will force Windows to use the OS/2 typo values
        instead. This means the font developer can control the linespacing with
        the typo values, whilst avoiding clipping by setting the win values to values
        greater than the yMax and abs(yMin).
    """,
    proposal = 'legacy:check/040'
)
def com_google_fonts_check_family_win_ascent_and_descent(ttFont, vmetrics):
    """Checking OS/2 usWinAscent & usWinDescent."""

    if "OS/2" not in ttFont:
        yield FAIL,\
              Message("lacks-OS/2",
                      "Font file lacks OS/2 table")
        return

    failed = False
    os2_table = ttFont['OS/2']
    win_ascent = os2_table.usWinAscent
    win_descent = os2_table.usWinDescent
    y_max = vmetrics['ymax']
    y_min = vmetrics['ymin']

    # OS/2 usWinAscent:
    if win_ascent < y_max:
        failed = True
        yield FAIL,\
              Message("ascent",
                      f"OS/2.usWinAscent value should be"
                      f" equal or greater than {y_max},"
                      f" but got {win_ascent} instead")
    if win_ascent > y_max * 2:
        failed = True
        yield FAIL,\
              Message("ascent",
                      f"OS/2.usWinAscent value"
                      f" {win_ascent} is too large."
                      f" It should be less than double the yMax."
                      f" Current yMax value is {y_max}")
    # OS/2 usWinDescent:
    if win_descent < abs(y_min):
        failed = True
        yield FAIL,\
              Message("descent",
                      f"OS/2.usWinDescent value should be equal or"
                      f" greater than {abs(y_min)}, but got"
                      f" {win_descent} instead.")

    if win_descent > abs(y_min) * 2:
        failed = True
        yield FAIL,\
              Message("descent",
                      f"OS/2.usWinDescent value"
                      f" {win_descent} is too large."
                      f" It should be less than double the yMin."
                      f" Current absolute yMin value is {abs(y_min)}")
    if not failed:
        yield PASS, "OS/2 usWinAscent & usWinDescent values look good!"


@check(
    id = 'com.google.fonts/check/os2_metrics_match_hhea',
    conditions = ['not is_cjk_font'],
    rationale = """
        OS/2 and hhea vertical metric values should match. This will produce the
        same linespacing on Mac, GNU+Linux and Windows.

        - Mac OS X uses the hhea values.⏎
        - Windows uses OS/2 or Win, depending on the OS or fsSelection bit value.

        When OS/2 and hhea vertical metrics match, the same linespacing results on
        macOS, GNU+Linux and Windows. Unfortunately as of 2018, Google Fonts
        has released many fonts with vertical metrics that don't match in this way.
        When we fix this issue in these existing families, we will create a visible
        change in line/paragraph layout for either Windows or macOS users,
        which will upset some of them.

        But we have a duty to fix broken stuff, and inconsistent paragraph layout
        is unacceptably broken when it is possible to avoid it.

        If users complain and prefer the old broken version, they have the freedom
        to take care of their own situation.
    """,
    proposal = 'legacy:check/042'
)
def com_google_fonts_check_os2_metrics_match_hhea(ttFont):
    """Checking OS/2 Metrics match hhea Metrics."""

    filename = os.path.basename(ttFont.reader.file.name)

    # Check both OS/2 and hhea are present.
    missing_tables = False

    required = ["OS/2", "hhea"]
    for key in required:
        if key not in ttFont:
            missing_tables = True
            yield FAIL,\
                  Message(f'lacks-{key}',
                          f"{filename} lacks a '{key}' table.")

    if missing_tables:
        return

    # OS/2 sTypoAscender and sTypoDescender match hhea ascent and descent
    if ttFont["OS/2"].sTypoAscender != ttFont["hhea"].ascent:
        yield FAIL,\
              Message("ascender",
                      f"OS/2 sTypoAscender ({ttFont['OS/2'].sTypoAscender})"
                      f" and hhea ascent ({ttFont['hhea'].ascent})"
                      f" must be equal.")
    elif ttFont["OS/2"].sTypoDescender != ttFont["hhea"].descent:
        yield FAIL,\
              Message("descender",
                      f"OS/2 sTypoDescender ({ttFont['OS/2'].sTypoDescender})"
                      f" and hhea descent ({ttFont['hhea'].descent})"
                      f" must be equal.")
    else:
        yield PASS, ("OS/2.sTypoAscender/Descender values"
                     " match hhea.ascent/descent.")


@check(
    id = 'com.google.fonts/check/family/single_directory',
    rationale = """
        If the set of font files passed in the command line is not all in the
        same directory, then we warn the user since the tool will interpret the
        set of files as belonging to a single family (and it is unlikely that
        the user would store the files from a single family spreaded
        in several separate directories).
    """,
    proposal = 'legacy:check/002'
)
def com_google_fonts_check_family_single_directory(fonts):
    """Checking all files are in the same directory."""

    directories = []
    for target_file in fonts:
        directory = os.path.dirname(target_file)
        if directory not in directories:
            directories.append(directory)

    if len(directories) == 1:
        yield PASS, "All files are in the same directory."
    else:
        yield FAIL,\
              Message("single-directory",
                      f"Not all fonts passed in the command line are in the"
                      f" same directory. This may lead to bad results as the tool"
                      f" will interpret all font files as belonging to a single"
                      f" font family. The detected directories are:"
                      f" {directories}")


@check(
    id = 'com.google.fonts/check/ots',
    proposal = 'legacy:check/036'
)
def com_google_fonts_check_ots(font):
    """Checking with ots-sanitize."""
    import ots

    try:
        process = ots.sanitize(font, check=True, capture_output=True)
    except ots.CalledProcessError as e:
        yield FAIL,\
              Message("ots-sanitize-error",
                      f"ots-sanitize returned an error code ({e.returncode})."
                      f" Output follows:\n"
                      f"\n"
                      f"{e.stderr.decode()}{e.stdout.decode()}")
    else:
        if process.stderr:
            yield WARN,\
                  Message("ots-sanitize-warn",
                          f"ots-sanitize passed this file,"
                          f" however warnings were printed:\n"
                          f"\n"
                          f"{process.stderr.decode()}")
        else:
            yield PASS, "ots-sanitize passed this file"


def is_up_to_date(installed, latest):
    # ignoring the development version suffix is ok
    # and is necessary for the string comparison
    # below to always yield valid results
    has_githash = ".dev" in installed
    installed = installed.split(".dev")[0]

    # Maybe the installed version is even newer than the
    # released one (such as during development on git).
    # That's what we're trying to detect here:
    installed = installed.split('.')
    latest = latest.split('.')
    for i in range(len(installed)):
        if installed[i] > latest[i]:
            return True
        if installed[i] < latest[i]:
            return False

    # Otherwise it should be identical
    # to the latest released version.
    return not has_githash


@check(
    id = 'com.google.fonts/check/fontbakery_version',
    rationale = """
        Running old versions of FontBakery can lead to a poor report which may
        include false WARNs and FAILs due do bugs, as well as outdated
        quality assurance criteria.

        Older versions will also not report problems that are detected by new checks
        added to the tool in more recent updates.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/issues/2093'
)
def com_google_fonts_check_fontbakery_version(font):
    """Do we have the latest version of FontBakery installed?"""
    import requests
    import pip_api

    try:
        response = requests.get('https://pypi.org/pypi/fontbakery/json')

    except requests.exceptions.ConnectionError as err:
        return FAIL, Message(
            "connection-error",
            f"Request to PyPI.org failed with this message:\n{err}",
        )

    status_code = response.status_code
    if status_code != 200:
        return FAIL, Message(
            f"unsuccessful-request-{status_code}",
            f"Request to PyPI.org was not successful:\n{response.content}",
        )

    latest = response.json()["info"]["version"]
    installed = str(pip_api.installed_distributions()["fontbakery"].version)

    if not is_up_to_date(installed, latest):
        return FAIL, Message("outdated-fontbakery",
                             f"Current Font Bakery version is {installed},"
                             f" while a newer {latest} is already available."
                             f" Please upgrade it with 'pip install -U fontbakery'")
    else:
        return PASS, "Font Bakery is up-to-date."


@check(
    id = 'com.google.fonts/check/mandatory_glyphs',
    rationale = """
        The OpenType specification v1.8.2 recommends that the first glyph is the
        '.notdef' glyph without a codepoint assigned and with a drawing.

        https://docs.microsoft.com/en-us/typography/opentype/spec/recom#glyph-0-the-notdef-glyph

        Pre-v1.8, it was recommended that fonts should also contain 'space', 'CR'
        and '.null' glyphs. This might have been relevant for MacOS 9 applications.
    """,
    proposal = 'legacy:check/046'
)
def com_google_fonts_check_mandatory_glyphs(ttFont):
    """Font contains '.notdef' as its first glyph?"""
    from fontbakery.utils import glyph_has_ink

    passed = True
    if ttFont.getGlyphOrder()[0] != ".notdef":
        passed = False
        yield WARN,\
              Message('first-glyph',
                      "Font should contain the .notdef glyph as the first glyph.")

    if ".notdef" in ttFont.getBestCmap().values():
        passed = False
        yield WARN,\
              Message('codepoint',
                      f"Glyph '.notdef' should not have a Unicode"
                      f" codepoint value assigned, but got"
                      f" 0x{ttFont.getBestCmap().values()['.notdef']:04X}.")

    if not glyph_has_ink(ttFont, ".notdef"):
        passed = False
        yield WARN,\
              Message('empty',
                      "Glyph '.notdef' should contain a drawing, but it is empty.")

    if passed:
        yield PASS, "OK"


@check(
    id = 'com.google.fonts/check/whitespace_glyphs',
    proposal = 'legacy:check/047'
)
def com_google_fonts_check_whitespace_glyphs(ttFont, missing_whitespace_chars):
    """Font contains glyphs for whitespace characters?"""
    failed = False
    for wsc in missing_whitespace_chars:
        failed = True
        yield FAIL, Message(f"missing-whitespace-glyph-{wsc}",
                            (f"Whitespace glyph missing for codepoint {wsc}."))

    if not failed:
        yield PASS, "Font contains glyphs for whitespace characters."


@check(
    id = 'com.google.fonts/check/whitespace_glyphnames',
    conditions = ['not missing_whitespace_chars'],
    rationale = """
        This check enforces adherence to recommended whitespace
        (codepoints 0020 and 00A0) glyph names according to the Adobe Glyph List.
    """,
    proposal = 'legacy:check/048'
)
def com_google_fonts_check_whitespace_glyphnames(ttFont):
    """Font has **proper** whitespace glyph names?"""
    from fontbakery.utils import get_glyph_name

    # AGL recommended names, according to Adobe Glyph List for new fonts:
    AGL_RECOMMENDED_0020 = {'space'}
    AGL_RECOMMENDED_00A0 = {"uni00A0", "space"}
    # "space" is in this set because some fonts use the same glyph for
    # U+0020 and U+00A0. Including it here also removes a warning
    # when U+0020 is wrong, but U+00A0 is okay.

    # AGL compliant names, but not recommended for new fonts:
    AGL_COMPLIANT_BUT_NOT_RECOMMENDED_0020 = {'uni0020',
                                              'u0020',
                                              'u00020',
                                              'u000020'}
    AGL_COMPLIANT_BUT_NOT_RECOMMENDED_00A0 = {'nonbreakingspace',
                                              'nbspace',
                                              'u00A0',
                                              'u000A0',
                                              'u0000A0'}

    if ttFont['post'].formatType == 3.0:
        yield SKIP, "Font has version 3 post table."
    else:
        passed = True

        space = get_glyph_name(ttFont, 0x0020)
        if not space:
            passed = False
            yield FAIL,\
                  Message('missing-0020',
                          'Glyph 0x0020 is missing a glyph name!')

        elif space in AGL_RECOMMENDED_0020:
            pass

        elif space in AGL_COMPLIANT_BUT_NOT_RECOMMENDED_0020:
            passed = False
            yield WARN,\
                  Message('not-recommended-0020',
                          f'Glyph 0x0020 is called "{space}":'
                          f' Change to "space"')
        else:
            passed = False
            yield FAIL,\
                  Message('non-compliant-0020',
                          f'Glyph 0x0020 is called "{space}":'
                          f' Change to "space"')


        nbsp = get_glyph_name(ttFont, 0x00A0)
        if not nbsp:
            yield FAIL,\
                  Message('missing-00a0',
                          'Glyph 0x00A0 is missing a glyph name!')

        elif nbsp == space:
            # This is OK.
            # Some fonts use the same glyph for both space and nbsp.
            pass

        elif nbsp in AGL_RECOMMENDED_00A0:
            pass

        elif nbsp in AGL_COMPLIANT_BUT_NOT_RECOMMENDED_00A0:
            passed = False
            yield WARN,\
                  Message('not-recommended-00a0',
                          f'Glyph 0x00A0 is called "{nbsp}":'
                          f' Change to "uni00A0"')
        else:
            passed = False
            yield FAIL,\
                  Message('non-compliant-00a0',
                          f'Glyph 0x00A0 is called "{nbsp}":'
                          f' Change to "uni00A0"')

        if passed:
            yield PASS, "Font has **AGL recommended** names for whitespace glyphs."


@check(
    id = 'com.google.fonts/check/whitespace_ink',
    proposal = 'legacy:check/049'
)
def com_google_fonts_check_whitespace_ink(ttFont):
    """Whitespace glyphs have ink?"""
    from fontbakery.utils import (get_glyph_name,
                                  glyph_has_ink)

    # This checks that certain glyphs are empty.
    # Some, but not all, are Unicode whitespace.

    # code-points for all Unicode whitespace chars
    # (according to Unicode 11.0 property list):
    WHITESPACE_CHARACTERS = {
        0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x0020, 0x0085, 0x00A0, 0x1680,
        0x2000, 0x2001, 0x2002, 0x2003, 0x2004, 0x2005, 0x2006, 0x2007, 0x2008,
        0x2009, 0x200A, 0x2028, 0x2029, 0x202F, 0x205F, 0x3000
    }

    # Code-points that do not have whitespace property, but
    # should not have a drawing.
    EXTRA_NON_DRAWING = {
        0x180E, 0x200B, 0x2060, 0xFEFF
    }

    # Make the set of non drawing characters.
    # OGHAM SPACE MARK U+1680 is removed as it is
    # a whitespace that should have a drawing.
    NON_DRAWING = (WHITESPACE_CHARACTERS | EXTRA_NON_DRAWING) - {0x1680}

    passed = True
    for codepoint in sorted(NON_DRAWING):
        g = get_glyph_name(ttFont, codepoint)
        if g is not None and glyph_has_ink(ttFont, g):
            passed = False
            yield FAIL,\
                  Message('has-ink',
                          f'Glyph "{g}" has ink.'
                          f' It needs to be replaced by an empty glyph.')
    if passed:
        yield PASS, "There is no whitespace glyph with ink."


@check(
    id='com.google.fonts/check/required_tables',
    conditions = ['ttFont'],
    rationale = """
        According to the OpenType spec
        https://docs.microsoft.com/en-us/typography/opentype/spec/otff#required-tables

        Whether TrueType or CFF outlines are used in an OpenType font, the following
        tables are required for the font to function correctly:

        - cmap (Character to glyph mapping)⏎
        - head (Font header)⏎
        - hhea (Horizontal header)⏎
        - hmtx (Horizontal metrics)⏎
        - maxp (Maximum profile)⏎
        - name (Naming table)⏎
        - OS/2 (OS/2 and Windows specific metrics)⏎
        - post (PostScript information)

        The spec also documents that variable fonts require the following table:

        - STAT (Style attributes)

        Depending on the typeface and coverage of a font, certain tables are
        recommended for optimum quality.

        For example:⏎
        - the performance of a non-linear font is improved if the VDMX, LTSH,
          and hdmx tables are present.⏎
        - Non-monospaced Latin fonts should have a kern table.⏎
        - A gasp table is necessary if a designer wants to influence the sizes
          at which grayscaling is used under Windows. Etc.
    """,
    proposal = 'legacy:check/052'
)
def com_google_fonts_check_required_tables(ttFont, config, is_variable_font):
    """Font contains all required tables?"""
    from fontbakery.utils import bullet_list

    REQUIRED_TABLES = ["cmap", "head", "hhea", "hmtx",
                       "maxp", "name", "OS/2", "post"]

    OPTIONAL_TABLES = ["cvt ", "fpgm", "loca", "prep",
                       "VORG", "EBDT", "EBLC", "EBSC",
                       "BASE", "GPOS", "GSUB", "JSTF",
                       "gasp", "hdmx", "LTSH", "PCLT",
                       "VDMX", "vhea", "vmtx", "kern"]

    # See https://github.com/googlefonts/fontbakery/issues/617
    #
    # We should collect the rationale behind the need for each of the
    # required tables above. Perhaps split it into individual checks
    # with the correspondent rationales for each subset of required tables.
    #
    # com.google.fonts/check/kern_table is a good example of a separate
    # check for a specific table providing a detailed description of
    # the rationale behind it.

    font_tables = ttFont.keys()

    optional_tables = [opt for opt in OPTIONAL_TABLES if opt in font_tables]
    if optional_tables:
        yield INFO, \
              Message("optional-tables",
                      f"This font contains the following optional tables:\n\n"
                      f"{bullet_list(config, optional_tables)}")

    if is_variable_font:
        # According to https://github.com/googlefonts/fontbakery/issues/1671
        # STAT table is required on WebKit on MacOS 10.12 for variable fonts.
        REQUIRED_TABLES.append("STAT")

    missing_tables = [req for req in REQUIRED_TABLES
                      if req not in font_tables]

    if ttFont.sfntVersion == "OTTO" and (
        "CFF " not in font_tables and "CFF2" not in font_tables
    ):
        if "fvar" in font_tables:
            missing_tables.append("CFF2")
        else:
            missing_tables.append("CFF ")

    elif ttFont.sfntVersion == "\x00\x01\x00\x00" and "glyf" not in font_tables:
        missing_tables.append("glyf")

    if missing_tables:
        yield FAIL, \
              Message("required-tables",
                      f"This font is missing the following required tables:\n\n"
                      f"{bullet_list(config, missing_tables)}")
    else:
        yield PASS, "Font contains all required tables."


@check(
    id = 'com.google.fonts/check/unwanted_tables',
    rationale = """
        Some font editors store source data in their own SFNT tables, and these
        can sometimes sneak into final release files, which should only have
        OpenType spec tables.
    """,
    proposal = 'legacy:check/053'
)
def com_google_fonts_check_unwanted_tables(ttFont):
    """Are there unwanted tables?"""
    UNWANTED_TABLES = {
        'FFTM': 'Table contains redundant FontForge timestamp info',
        'TTFA': 'Redundant TTFAutohint table',
        'TSI0': 'Table contains data only used in VTT',
        'TSI1': 'Table contains data only used in VTT',
        'TSI2': 'Table contains data only used in VTT',
        'TSI3': 'Table contains data only used in VTT',
        'TSI5': 'Table contains data only used in VTT',
        'prop': ('Table used on AAT, Apple\'s OS X specific technology.'
                 ' Although Harfbuzz now has optional AAT support,'
                 ' new fonts should not be using that.'),
    }
    unwanted_tables_found = []
    for table in ttFont.keys():
        if table in UNWANTED_TABLES.keys():
            info = UNWANTED_TABLES[table]
            unwanted_tables_found.append(f'Table: {table}\nReason: {info}\n')

    if unwanted_tables_found:
        tables = "\n".join(unwanted_tables_found)
        yield FAIL, \
              Message("unwanted-tables",
                      f"The following unwanted font tables were found:\n"
                      f"{tables}\n"
                      f"They can be removed with the fix-unwanted-tables"
                      f" script provided by gftools.")
    else:
        yield PASS, "There are no unwanted tables."


@condition
def STAT_table(ttFont):
    return "STAT" in ttFont


@check(
    id = 'com.google.fonts/check/STAT_strings',
    conditions = ["STAT_table"],
    rationale = """
        On the STAT table, the "Italic" keyword must not be used on AxisValues
        for variation axes other than 'ital'.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/issues/2863'
)
def com_google_fonts_check_STAT_strings(ttFont):
    """ Check correctness of STAT table strings """
    passed = True
    ital_axis_index = None
    for index, axis in enumerate(ttFont["STAT"].table.DesignAxisRecord.Axis):
        if axis.AxisTag == 'ital':
            ital_axis_index = index
            break

    nameIDs = set()
    if ttFont["STAT"].table.AxisValueArray:
        for value in ttFont["STAT"].table.AxisValueArray.AxisValue:
            if hasattr(value, "AxisIndex"):
                if value.AxisIndex != ital_axis_index:
                    nameIDs.add(value.ValueNameID)

            if hasattr(value, "AxisValueRecord"):
                for record in value.AxisValueRecord:
                    if record.AxisIndex != ital_axis_index:
                        nameIDs.add(value.ValueNameID)

    bad_values = set()
    for name in ttFont['name'].names:
        if name.nameID in nameIDs and "italic" in name.toUnicode().lower():
            passed = False
            bad_values.add(f"nameID {name.nameID}: {name.toUnicode()}")

    if bad_values:
        yield FAIL,\
              Message("bad-italic",
                      f'The following AxisValue entries on the STAT table'
                      f' should not contain "Italic":\n'
                      f' {list(sorted(bad_values))}')

    if passed:
        yield PASS, "Looks good!"


@check(
    id = 'com.google.fonts/check/valid_glyphnames',
    rationale = """
        Microsoft's recommendations for OpenType Fonts states the following:

        'NOTE: The PostScript glyph name must be no longer than 31 characters,
        include only uppercase or lowercase English letters, European digits,
        the period or the underscore, i.e. from the set [A-Za-z0-9_.] and
        should start with a letter, except the special glyph name ".notdef"
        which starts with a period.'

        https://docs.microsoft.com/en-us/typography/opentype/spec/recom#post-table


        In practice, though, particularly in modern environments, glyph names
        can be as long as 63 characters.

        According to the "Adobe Glyph List Specification" available at:

        https://github.com/adobe-type-tools/agl-specification
    """,
    proposal = ['legacy:check/058',
                'https://github.com/googlefonts/fontbakery/issues/2832']
                # issue #2832 increased the limit to 63 chars
)
def com_google_fonts_check_valid_glyphnames(ttFont, config):
    """Glyph names are all valid?"""
    from fontbakery.utils import pretty_print_list

    if (ttFont.sfntVersion == b'\x00\x01\x00\x00'
        and ttFont.get("post")
        and ttFont["post"].formatType == 3.0):
        yield SKIP, ("TrueType fonts with a format 3.0 post table"
                     " contain no glyph names.")
    else:
        import re
        bad_names = []
        warn_names = []
        for _, glyphName in enumerate(ttFont.getGlyphOrder()):
            if glyphName in [".null", ".notdef", ".ttfautohint"]:
                # These 2 names are explicit exceptions
                # in the glyph naming rules
                continue
            if not re.match(r'^(?![.0-9])[a-zA-Z._0-9]{1,63}$', glyphName):
                bad_names.append(glyphName)
            if len(glyphName) > 31 and len(glyphName) <= 63:
                warn_names.append(glyphName)

        if len(bad_names) == 0:
            if len(warn_names) == 0:
                yield PASS, "Glyph names are all valid."
            else:
                yield WARN,\
                      Message('legacy-long-names',
                              f"The following glyph names may be too"
                              f" long for some legacy systems which may"
                              f" expect a maximum 31-char length limit:\n"
                              f"{pretty_print_list(config, warn_names)}")
        else:
            bad_names_list = pretty_print_list(config, bad_names)
            yield FAIL,\
                  Message('found-invalid-names',
                          f"The following glyph names do not comply"
                          f" with naming conventions: {bad_names_list}\n"
                          f"\n"
                          f" A glyph name must be entirely comprised of characters"
                          f" from the following set:"
                          f" A-Z a-z 0-9 .(period) _(underscore)."
                          f" A glyph name must not start with a digit or period."
                          f" There are a few exceptions"
                          f" such as the special glyph \".notdef\"."
                          f" The glyph names \"twocents\", \"a1\", and \"_\""
                          f" are all valid, while \"2cents\""
                          f" and \".twocents\" are not.")


@check(
    id = 'com.google.fonts/check/unique_glyphnames',
    rationale = """
        Duplicate glyph names prevent font installation on Mac OS X.
    """,
    proposal = 'legacy:check/059',
    misc_metadata = {
        'affects': [('Mac', 'unspecified')]
    }
)
def com_google_fonts_check_unique_glyphnames(ttFont):
    """Font contains unique glyph names?"""
    if (ttFont.sfntVersion == b'\x00\x01\x00\x00'
        and ttFont.get("post")
        and ttFont["post"].formatType == 3.0):
        yield SKIP, ("TrueType fonts with a format 3.0 post table"
                     " contain no glyph names.")
    else:
        import re
        glyphs = []
        duplicated_glyphIDs = []
        for _, g in enumerate(ttFont.getGlyphOrder()):
            glyphID = re.sub(r'#\w+', '', g)
            if glyphID in glyphs:
                duplicated_glyphIDs.append(glyphID)
            else:
                glyphs.append(glyphID)

        if len(duplicated_glyphIDs) == 0:
            yield PASS, "Font contains unique glyph names."
        else:
            yield FAIL, \
                  Message("duplicated-glyph-names",
                          "The following glyph names occur twice: "
                          f"{duplicated_glyphIDs}")


@disable # until we know the rationale.
@check(
    id = 'com.google.fonts/check/glyphnames_max_length',
    proposal = 'https://github.com/googlefonts/fontbakery/issues/735'
)
def com_google_fonts_check_glyphnames_max_length(ttFont):
    """Check that glyph names do not exceed max length."""
    if (ttFont.sfntVersion == b'\x00\x01\x00\x00'
        and ttFont.get("post")
        and ttFont["post"].formatType == 3.0):
        yield PASS, ("TrueType fonts with a format 3.0 post table"
                     " contain no glyph names.")
    else:
        failed = False
        for name in ttFont.getGlyphOrder():
            if len(name) > 109:
                failed = True
                yield FAIL, \
                    Message("glyphname-too-long",
                            f"Glyph name is too long: '{name}'")
        if not failed:
            yield PASS, "No glyph names exceed max allowed length."


@check(
    id = 'com.google.fonts/check/ttx-roundtrip',
    conditions = ["not vtt_talk_sources"],
    proposal = 'https://github.com/googlefonts/fontbakery/issues/1763'
)
def com_google_fonts_check_ttx_roundtrip(font):
    """Checking with fontTools.ttx"""
    from fontTools import ttx
    import sys
    import tempfile
    ttFont = ttx.TTFont(font)
    failed = False
    fd, xml_file = tempfile.mkstemp()
    os.close(fd)

    class TTXLogger:
        msgs = []

        def __init__(self):
            self.original_stderr = sys.stderr
            self.original_stdout = sys.stdout
            sys.stderr = self
            sys.stdout = self

        def write(self, data):
            if data not in self.msgs:
                self.msgs.append(data)

        def restore(self):
            sys.stderr = self.original_stderr
            sys.stdout = self.original_stdout

    from xml.parsers.expat import ExpatError
    try:
        logger = TTXLogger()
        ttFont.saveXML(xml_file)
        export_error_msgs = logger.msgs

        if len(export_error_msgs):
            failed = True
            yield INFO, ("While converting TTF into an XML file,"
                         " ttx emited the messages listed below.")
            for msg in export_error_msgs:
                yield FAIL, msg.strip()

        f = ttx.TTFont()
        f.importXML(xml_file)
        import_error_msgs = [msg for msg in logger.msgs
                             if msg not in export_error_msgs]

        if len(import_error_msgs):
            failed = True
            yield INFO, ("While importing an XML file and converting"
                         " it back to TTF, ttx emited the messages"
                         " listed below.")
            for msg in import_error_msgs:
                yield FAIL, msg.strip()
        logger.restore()
    except ExpatError as e:
        failed = True
        yield FAIL, ("TTX had some problem parsing the generated XML file."
                     " This most likely mean there's some problem in the font."
                     " Please inspect the output of ttx in order to find more"
                     " on what went wrong. A common problem is the presence of"
                     " control characteres outside the accepted character range"
                     " as defined in the XML spec. FontTools has got a bug which"
                     " causes TTX to generate corrupt XML files in those cases."
                     " So, check the entries of the name table and remove any"
                     " control chars that you find there."
                     " The full ttx error message was:\n"
                     "======\n{}\n======".format(e))

    if not failed:
        yield PASS, "Hey! It all looks good!"

    # and then we need to cleanup our mess...
    if os.path.exists(xml_file):
        os.remove(xml_file)


@check(
    id = 'com.google.fonts/check/family/vertical_metrics',
    rationale = """
        We want all fonts within a family to have the same vertical metrics so
        their line spacing is consistent across the family.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/issues/1487'
)
def com_google_fonts_check_family_vertical_metrics(ttFonts):
    """Each font in a family must have the same set of vertical metrics values."""
    failed = []
    vmetrics = {
        "sTypoAscender": {},
        "sTypoDescender": {},
        "sTypoLineGap": {},
        "usWinAscent": {},
        "usWinDescent": {},
        "ascent": {},
        "descent": {}
    }

    missing_tables = False
    for ttFont in ttFonts:
        filename = os.path.basename(ttFont.reader.file.name)
        if 'OS/2' not in ttFont:
            missing_tables = True
            yield FAIL, \
                  Message('lacks-OS/2',
                          f"{filename} lacks an 'OS/2' table.")
            continue

        if 'hhea' not in ttFont:
            missing_tables = True
            yield FAIL, \
                  Message('lacks-hhea',
                          f"{filename} lacks a 'hhea' table.")
            continue

        full_font_name = ttFont['name'].getName(4, 3, 1, 1033).toUnicode()
        vmetrics['sTypoAscender'][full_font_name] = ttFont['OS/2'].sTypoAscender
        vmetrics['sTypoDescender'][full_font_name] = ttFont['OS/2'].sTypoDescender
        vmetrics['sTypoLineGap'][full_font_name] = ttFont['OS/2'].sTypoLineGap
        vmetrics['usWinAscent'][full_font_name] = ttFont['OS/2'].usWinAscent
        vmetrics['usWinDescent'][full_font_name] = ttFont['OS/2'].usWinDescent
        vmetrics['ascent'][full_font_name] = ttFont['hhea'].ascent
        vmetrics['descent'][full_font_name] = ttFont['hhea'].descent


    if not missing_tables:
        # It is important to first ensure all font files have OS/2 and hhea tables
        # before performing the rest of the check routine.

        for k, v in vmetrics.items():
            metric_vals = set(vmetrics[k].values())
            if len(metric_vals) != 1:
                failed.append(k)

        if failed:
            for k in failed:
                s = ["{}: {}".format(k, v) for k, v in vmetrics[k].items()]
                s = "\n".join(s)
                yield FAIL, \
                      Message(f'{k}-mismatch',
                              f"{k} is not the same across the family:\n"
                              f"{s}")
        else:
            yield PASS, "Vertical metrics are the same across the family."


@check(
    id = 'com.google.fonts/check/superfamily/list',
    rationale = """
        This is a merely informative check that lists all sibling families
        detected by fontbakery.

        Only the fontfiles in these directories will be considered in
        superfamily-level checks.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/issues/1487'
)
def com_google_fonts_check_superfamily_list(superfamily):
    """List all superfamily filepaths"""
    for family in superfamily:
        yield INFO,\
              Message("family-path",
                      os.path.split(family[0])[0])


@check(
    id = 'com.google.fonts/check/superfamily/vertical_metrics',
    rationale = """
        We may want all fonts within a super-family (all sibling families) to have
        the same vertical metrics so their line spacing is consistent
        across the super-family.

        This is an experimental extended version of
        com.google.fonts/check/family/vertical_metrics and for now it will only
        result in WARNs.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/issues/1487'
)
def com_google_fonts_check_superfamily_vertical_metrics(superfamily_ttFonts):
    """Each font in set of sibling families must have the same set of vertical metrics values."""
    if len(superfamily_ttFonts) < 2:
        yield SKIP, "Sibling families were not detected."
        return

    warn = []
    vmetrics = {
        "sTypoAscender": {},
        "sTypoDescender": {},
        "sTypoLineGap": {},
        "usWinAscent": {},
        "usWinDescent": {},
        "ascent": {},
        "descent": {}
    }

    for family_ttFonts in superfamily_ttFonts:
        for ttFont in family_ttFonts:
            full_font_name = ttFont['name'].getName(4, 3, 1, 1033).toUnicode()
            vmetrics['sTypoAscender'][full_font_name] = ttFont['OS/2'].sTypoAscender
            vmetrics['sTypoDescender'][full_font_name] = ttFont['OS/2'].sTypoDescender
            vmetrics['sTypoLineGap'][full_font_name] = ttFont['OS/2'].sTypoLineGap
            vmetrics['usWinAscent'][full_font_name] = ttFont['OS/2'].usWinAscent
            vmetrics['usWinDescent'][full_font_name] = ttFont['OS/2'].usWinDescent
            vmetrics['ascent'][full_font_name] = ttFont['hhea'].ascent
            vmetrics['descent'][full_font_name] = ttFont['hhea'].descent

    for k, v in vmetrics.items():
        metric_vals = set(vmetrics[k].values())
        if len(metric_vals) != 1:
            warn.append(k)

    if warn:
        for k in warn:
            s = ["{}: {}".format(k, v) for k, v in vmetrics[k].items()]
            s = "\n".join(s)
            yield WARN, \
                  Message("superfamily-vertical-metrics",
                          f"{k} is not the same across the super-family:\n"
                          f"{s}")
    else:
        yield PASS, "Vertical metrics are the same across the super-family."


@check(
    id = 'com.google.fonts/check/rupee',
    rationale = """
        Per Bureau of Indian Standards every font supporting one of the
        official Indian languages needs to include Unicode Character
        “₹” (U+20B9) Indian Rupee Sign.
    """,
    conditions = ['is_indic_font'],
    proposal = 'https://github.com/googlefonts/fontbakery/issues/2967'
)
def com_google_fonts_check_rupee(ttFont):
    """ Ensure indic fonts have the Indian Rupee Sign glyph. """
    if 0x20B9 not in ttFont['cmap'].getBestCmap().keys():
        yield FAIL,\
              Message("missing-rupee",
                      'Please add a glyph for'
                      ' Indian Rupee Sign “₹” at codepoint U+20B9.')
    else:
        yield PASS, "Looks good!"


@check(
    id = "com.google.fonts/check/designspace_has_sources",
    rationale = """
        This check parses a designspace file and tries to load the
        source files specified.

        This is meant to ensure that the file is not malformed,
        can be properly parsed and does include valid source file references.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/pull/3168'
)
def com_google_fonts_check_designspace_has_sources(designspace_sources):
    """See if we can actually load the source files."""
    if not designspace_sources:
        yield FAIL, \
              Message("no-sources",
                      "Unable to load source files.")
    else:
        yield PASS, "OK"


@check(
    id = "com.google.fonts/check/designspace_has_default_master",
    rationale = """
        We expect that designspace files declare on of the masters as default.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/pull/3168'
)
def com_google_fonts_check_designspace_has_default_master(designSpace):
    """Ensure a default master is defined."""
    if not designSpace.findDefault():
        yield FAIL, \
              Message("not-found",
                      "Unable to find a default master.")
    else:
        yield PASS, "We located a default master."


@check(
    id = "com.google.fonts/check/designspace_has_consistent_glyphset",
    rationale = """
        This check ensures that non-default masters don't have glyphs
        not present in the default one.
    """,
    conditions = ["designspace_sources"],
    proposal = 'https://github.com/googlefonts/fontbakery/pull/3168'
)
def com_google_fonts_check_designspace_has_consistent_glyphset(designSpace, config):
    """Check consistency of glyphset in a designspace file."""
    from fontbakery.utils import bullet_list

    default_glyphset = set(designSpace.findDefault().font.keys())
    failures = []
    for source in designSpace.sources:
        master_glyphset = set(source.font.keys())
        outliers = master_glyphset - default_glyphset
        if outliers:
            outliers = ", ".join(list(outliers))
            failures.append(f"Source {source.filename} has glyphs not present"
                            f" in the default master: {outliers}")
    if failures:
        yield FAIL,\
              Message("inconsistent-glyphset",
                      f"Glyphsets were not consistent:\n\n"
                      f"{bullet_list(config, failures)}")
    else:
        yield PASS, "Glyphsets were consistent."


@check(
    id = "com.google.fonts/check/designspace_has_consistent_codepoints",
    rationale = """
        This check ensures that Unicode assignments are consistent
        across all sources specified in a designspace file.
    """,
    conditions = ["designspace_sources"],
    proposal = 'https://github.com/googlefonts/fontbakery/pull/3168'
)
def com_google_fonts_check_designspace_has_consistent_codepoints(designSpace, config):
    """Check codepoints consistency in a designspace file."""
    from fontbakery.utils import bullet_list

    default_source = designSpace.findDefault()
    default_unicodes = {g.name: g.unicode for g in default_source.font}
    failures = []
    for source in designSpace.sources:
        for g in source.font:
            if g.name not in default_unicodes:
                # Previous test will cover this
                continue

            if g.unicode != default_unicodes[g.name]:
                failures.append(f"Source {source.filename} has"
                                f" {g.name}={g.unicode};"
                                f" default master has"
                                f" {g.name}={default_unicodes[g.name]}")
    if failures:
        yield FAIL,\
              Message("inconsistent-codepoints",
                      f"Unicode assignments were not consistent:\n\n"
                      f"{bullet_list(config, failures)}")
    else:
        yield PASS, "Unicode assignments were consistent."


@check(
    id = "com.google.fonts/check/unreachable_glyphs",
    rationale = """
        Glyphs are either accessible directly through Unicode codepoints or through
        substitution rules. Any glyphs not accessible by either of these means
        are redundant and serve only to increase the font's file size.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/issues/3160',
)
def com_google_fonts_check_unreachable_glyphs(ttFont, config):
    """Check font contains no unreachable glyphs"""

    def remove_lookup_outputs(all_glyphs, lookup):
        if lookup.LookupType == 1: # Single:
                                   # Replace one glyph with one glyph
            for sub in lookup.SubTable:
                all_glyphs -= set(sub.mapping.values())

        if lookup.LookupType == 2: # Multiple:
                                   # Replace one glyph with more than one glyph
            for sub in lookup.SubTable:
                for slot in sub.mapping.values():
                    all_glyphs -= set(slot)

        if lookup.LookupType == 3: # Alternate:
                                   # Replace one glyph with one of many glyphs
            for sub in lookup.SubTable:
                for slot in sub.alternates.values():
                    all_glyphs -= set(slot)

        if lookup.LookupType == 4: # Ligature:
                                   # Replace multiple glyphs with one glyph
            for sub in lookup.SubTable:
                for ligatures in sub.ligatures.values():
                    all_glyphs -= set(lig.LigGlyph for lig in ligatures)

        if lookup.LookupType in [5, 6]:
            # We do nothing here, because these contextual lookup types don't
            # generate glyphs directly; they only dispatch to other lookups
            # stored elsewhere in the lookup list. As we are examining all
            # lookups in the lookup list, other calls to this function will
            # deal with the lookups that a contextual lookup references.
            pass

        if lookup.LookupType == 7: # Extension Substitution:
                                   # Extension mechanism for other substitutions
            for xt in lookup.SubTable:
                xt.SubTable = [xt.ExtSubTable]
                xt.LookupType = xt.ExtSubTable.LookupType
                remove_lookup_outputs(all_glyphs, xt)

        if lookup.LookupType == 8: # Reverse chaining context single:
                                   # Applied in reverse order,
                                   # replace single glyph in chaining context
            for sub in lookup.SubTable:
                all_glyphs -= set(sub.Substitute)


    all_glyphs = set(ttFont.getGlyphOrder())

    # Exclude cmapped glyphs
    all_glyphs -= set(ttFont.getBestCmap().values())
    all_glyphs.discard(".notdef")

    if "GSUB" in ttFont and ttFont["GSUB"].table.LookupList:
        lookups = ttFont["GSUB"].table.LookupList.Lookup

        for lookup in lookups:
            remove_lookup_outputs(all_glyphs, lookup)

    # Remove components used in TrueType table
    if "glyf" in ttFont:
        for glyph_name in ttFont["glyf"].keys():
            base_glyph = ttFont["glyf"][glyph_name]
            if base_glyph.isComposite():
                all_glyphs -= set(base_glyph.getComponentNames(ttFont["glyf"]))

    if all_glyphs:
        from fontbakery.utils import bullet_list
        yield WARN,\
              Message("unreachable-glyphs",
                      f"The following glyphs could not be reached"
                      f" by codepoint or substitution rules:\n\n"
                      f"{bullet_list(config, list(all_glyphs))}\n")
    else:
        yield PASS, "Font did not contain any unreachable glyphs"


@check(
    id = 'com.google.fonts/check/contour_count',
    conditions = ['is_ttf',
                  'not is_variable_font'],
    rationale = """
        Visually QAing thousands of glyphs by hand is tiring. Most glyphs can only
        be constructured in a handful of ways. This means a glyph's contour count
        will only differ slightly amongst different fonts, e.g a 'g' could either
        be 2 or 3 contours, depending on whether its double story or single story.

        However, a quotedbl should have 2 contours, unless the font belongs
        to a display family.

        This check currently does not cover variable fonts because there's plenty
        of alternative ways of constructing glyphs with multiple outlines for each
        feature in a VarFont. The expected contour count data for this check is
        currently optimized for the typical construction of glyphs in static fonts.
    """,
    proposal = 'legacy:check/153'
)
def com_google_fonts_check_contour_count(ttFont, config):
    """Check if each glyph has the recommended amount of contours.

    This check is useful to assure glyphs aren't incorrectly constructed.

    The desired_glyph_data module contains the 'recommended' countour count
    for encoded glyphs. The contour counts are derived from fonts which were
    chosen for their quality and unique design decisions for particular glyphs.

    In the future, additional glyph data can be included. A good addition would
    be the 'recommended' anchor counts for each glyph.
    """
    from fontbakery.glyphdata import desired_glyph_data as glyph_data
    from fontbakery.constants import (PlatformID,
                                      WindowsEncodingID)
    from fontbakery.utils import (bullet_list,
                                  get_font_glyph_data,
                                  pretty_print_list)

    def in_PUA_range(codepoint):
        """
          In Unicode, a Private Use Area (PUA) is a range of code points that,
          by definition, will not be assigned characters by the Unicode Consortium.
          Three private use areas are defined:
            one in the Basic Multilingual Plane (U+E000–U+F8FF),
            and one each in, and nearly covering, planes 15 and 16
            (U+F0000–U+FFFFD, U+100000–U+10FFFD).
        """
        return (codepoint >= 0xE000 and codepoint <= 0xF8FF) or \
               (codepoint >= 0xF0000 and codepoint <= 0xFFFFD) or \
               (codepoint >= 0x100000 and codepoint <= 0x10FFFD)

    # rearrange data structure:
    desired_glyph_data_by_codepoint = {}
    desired_glyph_data_by_glyphname = {}
    for glyph in glyph_data:
        desired_glyph_data_by_glyphname[glyph['name']] = glyph
        # since the glyph in PUA ranges have unspecified meaning,
        # it doesnt make sense for us to have an expected contour cont for them
        if not in_PUA_range(glyph['unicode']):
            desired_glyph_data_by_codepoint[glyph['unicode']] = glyph

    bad_glyphs = []
    desired_glyph_contours_by_codepoint = {f: desired_glyph_data_by_codepoint[f]['contours']
                                           for f in desired_glyph_data_by_codepoint}
    desired_glyph_contours_by_glyphname = {f: desired_glyph_data_by_glyphname[f]['contours']
                                           for f in desired_glyph_data_by_glyphname}

    font_glyph_data = get_font_glyph_data(ttFont)

    if font_glyph_data is None:
        yield FAIL,\
              Message("lacks-cmap",
                      "This font lacks cmap data.")
    else:
        font_glyph_contours_by_codepoint = {f['unicode']: list(f['contours'])[0]
                                            for f in font_glyph_data}
        font_glyph_contours_by_glyphname = {f['name']: list(f['contours'])[0]
                                            for f in font_glyph_data}

        if 0x00AD in font_glyph_contours_by_codepoint.keys():
            yield WARN,\
                  Message("softhyphen",
                          "This font has a 'Soft Hyphen' character (codepoint 0x00AD)"
                          " which is supposed to be zero-width and invisible, and is"
                          " used to mark a hyphenation possibility within a word"
                          " in the absence of or overriding dictionary hyphenation."
                          " It is mostly an obsolete mechanism now, and the character"
                          " is only included in fonts for legacy codepage coverage.")

        shared_glyphs_by_codepoint = set(desired_glyph_contours_by_codepoint) & \
                                     set(font_glyph_contours_by_codepoint)
        for glyph in sorted(shared_glyphs_by_codepoint):
            if font_glyph_contours_by_codepoint[glyph] not in desired_glyph_contours_by_codepoint[glyph]:
                bad_glyphs.append([glyph,
                                   font_glyph_contours_by_codepoint[glyph],
                                   desired_glyph_contours_by_codepoint[glyph]])

        shared_glyphs_by_glyphname = set(desired_glyph_contours_by_glyphname) & \
                                     set(font_glyph_contours_by_glyphname)
        for glyph in sorted(shared_glyphs_by_glyphname):
            if font_glyph_contours_by_glyphname[glyph] not in desired_glyph_contours_by_glyphname[glyph]:
                bad_glyphs.append([glyph,
                                   font_glyph_contours_by_glyphname[glyph],
                                   desired_glyph_contours_by_glyphname[glyph]])

        if len(bad_glyphs) > 0:
            cmap = ttFont['cmap'].getcmap(PlatformID.WINDOWS,
                                          WindowsEncodingID.UNICODE_BMP).cmap

            def _glyph_name(cmap, name):
                if name in cmap:
                    return cmap[name]
                else:
                    return name

            bad_glyphs_name = [
                f"Glyph name: {_glyph_name(cmap, name)}\t"
                f"Contours detected: {count}\t"
                f"Expected: {pretty_print_list(config, expected, glue='or')}"
                for name, count, expected in bad_glyphs
            ]
            bad_glyphs_name = bullet_list(config, bad_glyphs_name)
            yield WARN,\
                  Message("contour-count",
                          f"This check inspects the glyph outlines and detects the"
                          f" total number of contours in each of them. The expected"
                          f" values are infered from the typical ammounts of"
                          f" contours observed in a large collection of reference"
                          f" font families. The divergences listed below may simply"
                          f" indicate a significantly different design on some of"
                          f" your glyphs. On the other hand, some of these may flag"
                          f" actual bugs in the font such as glyphs mapped to an"
                          f" incorrect codepoint. Please consider reviewing"
                          f" the design and codepoint assignment of these to make"
                          f" sure they are correct.\n"
                          f"\n"
                          f"The following glyphs do not have the recommended"
                          f" number of contours:\n"
                          f"\n"
                          f"{bad_glyphs_name}"
                          f"\n")
        else:
            yield PASS, "All glyphs have the recommended amount of contours"


@check(
    id = 'com.google.fonts/check/cjk_chws_feature',
    conditions = ['is_cjk_font'],
    rationale = """
        The W3C recommends the addition of chws and vchw features to CJK fonts
        to enhance the spacing of glyphs in environments which do not fully support
        JLREQ layout rules.

        The chws_tool utility (https://github.com/googlefonts/chws_tool) can be used
        to add these features automatically.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/issues/3363'
)
def com_google_fonts_check_cjk_chws_feature(ttFont):
    """Does the font contain chws and vchw features?"""
    from fontbakery.profiles.layout import feature_tags
    passed = True
    tags = feature_tags(ttFont)
    FEATURE_NOT_FOUND = ("{} feature not found in font."
                         " Use chws_tool (https://github.com/googlefonts/chws_tool)"
                         " to add it.")
    if "chws" not in tags:
        passed = False
        yield WARN,\
              Message('missing-chws-feature',
                      FEATURE_NOT_FOUND.format("chws"))
    if "vchw" not in tags:
        passed = False
        yield WARN,\
              Message('missing-vchw-feature',
                      FEATURE_NOT_FOUND.format("vchw"))
    if passed:
        yield PASS, "Font contains chws and vchw features"


@check(
    id = 'com.google.fonts/check/transformed_components',
    conditions = ['is_ttf'],
    rationale = """
        Some families have glyphs which have been constructed by using
        transformed components e.g the 'u' being constructed from a flipped 'n'.

        From a designers point of view, this sounds like a win (less work).
        However, such approaches can lead to rasterization issues, such as
        having the 'u' not sitting on the baseline at certain sizes after
        running the font through ttfautohint.

        As of July 2019, Marc Foley observed that ttfautohint assigns cvt values
        to transformed glyphs as if they are not transformed and the result is
        they render very badly, and that vttLib does not support flipped components.

        When building the font with fontmake, the problem can be fixed by adding
        this to the command line:

        --filter DecomposeTransformedComponentsFilter
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/issues/2011',
)
def com_google_fonts_check_transformed_components(ttFont):
    """Ensure component transforms do not perform scaling or rotation."""
    failures = ""
    for glyph_name in ttFont.getGlyphOrder():
        glyf = ttFont["glyf"][glyph_name]
        if not glyf.isComposite():
            continue
        for component in glyf.components:
            comp_name, transform = component.getComponentInfo()
            if transform[0:4] != (1, 0, 0, 1):
                failures += f"* {glyph_name} (component {comp_name})\n"
    if failures:
        yield FAIL,\
              Message("transformed-components",
                      f"The following glyphs had components with scaling or rotation:\n"
                      f"\n"
                      f"{failures}")
    else:
        yield PASS, "No glyphs had components with scaling or rotation"


@check(
    id = 'com.google.fonts/check/dotted_circle',
    conditions = ['is_ttf'],
    severity = 3,
    rationale = """
        The dotted circle character (U+25CC) is inserted by shaping engines before
        mark glyphs which do not have an associated base, especially in the context
        of broken syllabic clusters.

        For fonts containing combining marks, it is recommended that the dotted circle
        character be included so that these isolated marks can be displayed properly;
        for fonts supporting complex scripts, this should be considered mandatory.

        Additionally, when a dotted circle glyph is present, it should be able to
        display all marks correctly, meaning that it should contain anchors for all
        attaching marks.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/issues/3600',
)
def com_google_fonts_check_dotted_circle(ttFont, config):
    """Ensure dotted circle glyph is present and can attach marks."""
    from fontbakery.utils import (bullet_list,
                                  is_complex_shaper_font,
                                  iterate_lookup_list_with_extensions)

    mark_glyphs = []
    if "GDEF" in ttFont and hasattr(ttFont["GDEF"].table, "GlyphClassDef"):
      mark_glyphs = [k for k, v in ttFont["GDEF"].table.GlyphClassDef.classDefs.items()
                     if v == 3]

    # Only check for encoded
    mark_glyphs = set(mark_glyphs) & set(ttFont.getBestCmap().values())
    nonspacing_mark_glyphs = [g for g in mark_glyphs if ttFont["hmtx"][g][0] == 0]

    if not nonspacing_mark_glyphs:
        yield SKIP, "Font has no nonspacing mark glyphs."
        return

    if 0x25CC not in ttFont.getBestCmap():
        # How bad is this?
        if is_complex_shaper_font(ttFont):
            yield FAIL,\
                  Message('missing-dotted-circle-complex',
                          "No dotted circle glyph present"
                          "and font uses a complex shaper")
        else:
            yield WARN,\
                  Message('missing-dotted-circle',
                          "No dotted circle glyph present")
        return

    # Check they all attach to dotted circle
    # if they attach to something else
    dotted_circle = ttFont.getBestCmap()[0x25CC]
    attachments = {dotted_circle: []}
    does_attach = {}
    def find_mark_base(lookup, attachments):
        if lookup.LookupType == 4:
            # Assume all-to-all
            for st in lookup.SubTable:
                for base in st.BaseCoverage.glyphs:
                    for mark in st.MarkCoverage.glyphs:
                        attachments.setdefault(base,[]).append(mark)
                        does_attach[mark] = True

    iterate_lookup_list_with_extensions(ttFont, "GPOS", find_mark_base, attachments)

    unattached = []
    for g in nonspacing_mark_glyphs:
        if g in does_attach and g not in attachments[dotted_circle]:
            unattached.append(g)

    if unattached:
        yield FAIL,\
              Message("unattached-dotted-circle-marks",
                      f"The following glyphs could not be attached"
                      f" to the dotted circle glyph:\n\n"
                      f"{bullet_list(config, unattached)}")
    else:
        yield PASS, "All marks were anchored to dotted circle"


@check(
    id = 'com.google.fonts/check/gpos7',
    conditions = ['ttFont'],
    severity = 9,
    rationale = """
        Versions of fonttools >=4.14.0 (19 August 2020) perform an optimisation on
        chained contextual lookups, expressing GSUB6 as GSUB5 and GPOS8 and GPOS7
        where possible (when there are no suffixes/prefixes for all rules in
        the lookup).

        However, makeotf has never generated these lookup types and they are rare
        in practice. Perhaps before of this, Mac's CoreText shaper does not correctly
        interpret GPOS7, meaning that these lookups will be ignored by the shaper,
        and fonts containing these lookups will have unintended positioning errors.

        To fix this warning, rebuild the font with a recent version of fonttools.
    """,
    proposal = 'https://github.com/googlefonts/fontbakery/issues/3643',
)
def com_google_fonts_check_gpos7(ttFont):
    """Ensure no GPOS7 lookups are present."""
    from fontbakery.utils import iterate_lookup_list_with_extensions

    has_gpos7 = False
    def find_gpos7(lookup):
        nonlocal has_gpos7
        if lookup.LookupType == 7:
            has_gpos7 = True
    iterate_lookup_list_with_extensions(ttFont, "GPOS", find_gpos7)

    if not has_gpos7:
        yield PASS, "Font has no GPOS7 lookups"
        return

    yield WARN,\
          Message('has-gpos7',
                  "Font contains a GPOS7 lookup which is not processed by macOS")


@check(
    id="com.adobe.fonts/check/freetype_rasterizer",
    conditions=['ttFont'],
    severity=10,
    rationale="""
        Malformed fonts can cause FreeType to crash.
    """,
    proposal="https://github.com/googlefonts/fontbakery/issues/3642",
)
def com_adobe_fonts_check_freetype_rasterizer(font):
    """Ensure that the font can be rasterized by FreeType."""
    try:
        import freetype
        from freetype.ft_errors import FT_Exception

        face = freetype.Face(font)
        face.set_char_size(48 * 64)
        face.load_char("✅")  # any character can be used here

    except ImportError:
        return SKIP, Message(
            "freetype-not-installed",
            "FreeType is not available; to install it, invoke the "
            "'freetype' extra when installing Font Bakery.",
        )
    except FT_Exception as err:
        return FAIL, Message(
            "freetype-crash",
            f"Font caused FreeType to crash with this error: {err}",
        )
    else:
        return PASS, "Font can be rasterized by FreeType."


profile.auto_register(globals())
profile.test_expected_checks(UNIVERSAL_PROFILE_CHECKS, exclusive=True)


# Lib/fontbakery/reporters/__init__.py

"""
Separation of Concerns Disclaimer:
While created specifically for checking fonts and font-families this
module has no domain knowledge about fonts. It can be used for any kind
of (document) checking. Please keep it so. It will be valuable for other
domains as well.
Domain specific knowledge should be encoded only in the Profile (Checks,
Conditions) and MAYBE in *customized* reporters e.g. subclasses.
"""
from collections import Counter

from fontbakery.checkrunner import (
              DEBUG
            , INFO
            , WARN
            , ERROR
            , STARTCHECK
            , SKIP
            , PASS
            , FAIL
            , ENDCHECK
            , SECTIONSUMMARY
            , START
            , END
            )
from fontbakery.errors import ProtocolViolationError

class FontbakeryReporter:
    def __init__(self, is_async=False, runner=None, output_file=None, loglevels=None):
        self._started = None
        self._ended = None
        self._order = None
        self._results = [] # ENDCHECK events in order of appearance
        self._indexes = {}
        self._tick = 0
        self._counter = Counter()
        self.loglevels = loglevels

        # Runner should know if it is async!
        self.is_async = is_async
        self.runner = runner

        self._worst_check_status = None
        self.output_file = output_file

    def run(self, order=None):
        """
        self.runner must be present
        """
        for event in self.runner.run(order=order):
            self.receive(event)

    @property
    def order(self):
        return self._order

    def write(self):
        if self.output_file is not None:
            raise NotImplementedError(
                f'{type(self)} does not implement the "write" method, '
                'but it has an "output_file".'
            )
        # reporters without an output file do nothing here

    def _get_key(self, identity):
        section, check, iterargs = identity
        return (str(section) if section else section
              , str(check) if check else check
              , iterargs
              )

    def _get_index(self, identity):
        key = self._get_key(identity)
        try:
            return self._indexes[key]
        except KeyError:
            self._indexes[key] = len(self._indexes)
            return self._indexes[key]

    def _set_order(self, order):
        self._order = tuple(order)
        length = len(self._order)
        self._counter['(not finished)'] = length - len(self._results)
        self._indexes = dict(zip(map(self._get_key, self._order), range(length)))

    def _cleanup(self, event):
        pass

    def _output(self, event):
        pass

    def _register(self, event):
        status, message, identity = event
        self._tick += 1
        if status == START:
            self._set_order(message)
            self._started = event

        if status == END:
            self._ended = event

        if status == ENDCHECK:
            self._results.append(event)
            self._counter[message.name] += 1
            self._counter['(not finished)'] -= 1

    @property
    def worst_check_status(self):
        """ Returns a status or None if there was no check result """
        return self._worst_check_status

    def receive(self, event):
        status, message, identity = event
        if self._started is None and status != START:
            raise ProtocolViolationError(f'Received Event before status START:'
                                         f' {status} {message}.')
        if self._ended:
            status, message, identity = event
            raise ProtocolViolationError(f'Received Event after status END:'
                                         f' {status} {message}.')

        if status is ENDCHECK and (self._worst_check_status is None \
                                   or self._worst_check_status < message):
            # we only record ENDCHECK, because check runner may in the future
            # have tools to upgrade/downgrade the actually worst status
            # this should be future proof.
            self._worst_check_status = message

        self._register(event)
        self._cleanup(event)
        self._output(event)


# Lib/fontbakery/reporters/ghmarkdown.py

import os
from fontbakery.reporters.serialize import SerializeReporter
from fontbakery.utils import html5_collapsible
from fontbakery.checkrunner import Status
from fontbakery import __version__ as version

LOGLEVELS=["ERROR","FAIL","WARN","SKIP","INFO","PASS","DEBUG"]


class GHMarkdownReporter(SerializeReporter):

    def write(self):
        with open(self.output_file, "w") as fh:
            fh.write(self.get_markdown())
        print(f'A report in GitHub Markdown format which can be useful\n'
              f' for posting issues on a GitHub issue tracker has been\n'
              f' saved to "{self.output_file}"')


    def emoticon(self, name):
        return {
            'ERROR': "\U0001F494", # 💔  :broken_heart:
            'FAIL':  "\U0001F525", # 🔥  :fire:
            'WARN':  "\U000026A0", # ⚠️  :warning:
            'INFO':  "\U00002139", # ℹ️  :information_source:
            'SKIP':  "\U0001F4A4", # 💤  :zzz:
            'PASS':  "\U0001F35E", # 🍞  :bread
            'DEBUG': "\U0001F50E", # 🔎 :mag_right:
        }[name]


    def log_md(self, log):
        if not self.omit_loglevel(log["status"]):
            return "* {} **{}** {}\n".format(self.emoticon(log["status"]),
                                             log["status"],
                                             log["message"])
        else:
            return ""


    def render_rationale(self, check, checkid):
        if self.succinct or not "rationale" in check:
            return ""

        from fontbakery.utils import unindent_and_unwrap_rationale
        content = unindent_and_unwrap_rationale(check['rationale'], checkid)
        return "\n".join([ ">" + line for line in content.split("\n")])

    def check_md(self, check):
        checkid = check["key"][1].split(":")[1].split(">")[0]
        profile = check["profile"]

        check["logs"].sort(key=lambda c: c["status"])
        logs = "".join(map(self.log_md, check["logs"]))
        github_search_url = ( "<a href=\"https://font-bakery.readthedocs.io/en/stable"
                             f"/fontbakery/profiles/{profile}.html#{checkid}\">"
                             f"{checkid}</a>" )

        rationale = self.render_rationale(check, checkid)

        return html5_collapsible("{} <b>{}:</b> {} ({})".format(self.emoticon(check["result"]),
                                                                check["result"],
                                                                check["description"],
                                                                github_search_url),
                                      f"\n\n{rationale}\n{logs}")

    def deduce_profile_from_section_name(self, section):
        # This is very hacky!
        # We should have a much better way of doing it...
        if 'Google Fonts' in section: return 'googlefonts'
        if 'Adobe' in section: return 'adobefonts'
        if 'Type Network' in section: return 'typenetwork'
        if 'Universal' in section: return 'universal'
        if 'Basic UFO checks' in section: return 'ufo_sources'
        if 'Checks inherited from Microsoft Font Validator' in section: return 'fontval'
        if 'fontbakery.profiles.' in section: return section.split('fontbakery.profiles.')[1].split('>')[0]
        return section

    def result_is_all_same(self, cluster):
        first_check = cluster[0]
        return all(check["logs"] == first_check["logs"] for check in cluster[1:])


    def get_markdown(self):
        checks = {}
        family_checks = []
        data = self.getdoc()
        num_checks = 0
        for section in data["sections"]:
            for cluster in section["checks"]:
                if not isinstance(cluster, list):
                    cluster = [cluster]
                num_checks += len(cluster)
                if len(cluster) > 1 and self.result_is_all_same(cluster):
                    # Pretend it's a family check
                    cluster = [cluster[0]]
                    del cluster[0]["filename"]
                for check in cluster:
                    if self.omit_loglevel(check["result"]):
                        continue

                    check['profile'] = self.deduce_profile_from_section_name(section["key"][0])
                    if "filename" not in check.keys():
                        # That's a family check!
                        family_checks.append(check)
                    else:
                        key = os.path.basename(check["filename"])
                        if key not in checks:
                            checks[key] = []
                        checks[key].append(check)

        md = (f"## Fontbakery report\n"
              f"\n"
              f"Fontbakery version: {version}\n"
              f"\n")

        if family_checks:
            family_checks.sort(key=lambda c: c["result"])
            md += html5_collapsible("<b>[{}] Family checks</b>".format(len(family_checks)),
                                         "".join(map(self.check_md, family_checks)) + "<br>")

        for filename in checks.keys():
            checks[filename].sort(key=lambda c: LOGLEVELS.index(c["result"]))
            md += html5_collapsible("<b>[{}] {}</b>".format(len(checks[filename]),
                                                                 filename),
                                         "".join(map(self.check_md, checks[filename])) + "<br>")

        if num_checks != 0:
            summary_table = "### Summary\n\n" + \
                ("| {} " + " | {} ".join(LOGLEVELS) + " |\n").format(*[self.emoticon(k)
                                                                       for k in LOGLEVELS]) + \
                ("|:-----:|:----:|:----:|:----:|:----:|:----:|:----:|\n"
                 "| {} | {} | {} | {} | {} | {} | {} |\n"
                 "").format(*[data["result"][k]
                              for k in LOGLEVELS]) + \
                ("| {:.0f}% | {:.0f}% | {:.0f}% | {:.0f}% | {:.0f}% | {:.0f}% | {:.0f}% |\n"
                 "").format(*[100*data["result"][k]/num_checks
                              for k in LOGLEVELS])
            md += "\n" + summary_table

        omitted = [l for l in LOGLEVELS if self.omit_loglevel(l)]
        if omitted:
            md += "\n" + \
                  "**Note:** The following loglevels were omitted in this report:\n" + \
                  "".join(map("* **{}**\n".format, omitted))

        return md


# Lib/fontbakery/reporters/html.py

"""Reporter class that renders report as a HTML document."""

import collections
import html
from typing import List, Dict
import cmarkgfm
from cmarkgfm.cmark import Options as cmarkgfmOptions


import fontbakery.checkrunner
from fontbakery.reporters.serialize import SerializeReporter
from fontbakery.utils import unindent_and_unwrap_rationale, html5_collapsible

LOGLEVELS = ["ERROR", "FAIL", "WARN", "SKIP", "INFO", "PASS", "DEBUG"]
EMOTICON = {
    "ERROR": "💥",
    "FAIL": "🔥",
    "WARN": "⚠️",
    "INFO": "ℹ️",
    "SKIP": "⏩",
    "PASS": "✔️",
    "DEBUG": "🔎"
}
ISSUE_URL = "https://github.com/googlefonts/fontbakery/issues"


class HTMLReporter(SerializeReporter):
    """Renders a report as a HTML document."""

    def write(self):
        with open(self.output_file, "w", encoding="utf-8") as fh:
            fh.write(self.get_html())
        print(f'A report in HTML format has been saved to "{self.output_file}"')


    def get_html(self) -> str:
        """Return complete report as a HTML string."""
        data = self.getdoc()
        num_checks = 0
        body_elements = []

        # Order by section first...
        for section in data["sections"]:
            section_name = html.escape(section["key"][0])
            section_stati_of_note = (
                e for e in section["result"].elements() if e != "PASS"
            )
            if all([self.omit_loglevel(s) for s in section["result"].elements()]):
                continue
            section_stati = "".join(
                EMOTICON[s] for s in sorted(section_stati_of_note, key=LOGLEVELS.index)
            )
            body_elements.append(f"<h2>{section_name} {section_stati}</h2>")

            checks_by_id: Dict[str, List[Dict[str, str]]] = collections.defaultdict(
                list
            )
            # ...and check second.
            for cluster in section["checks"]:
                if not isinstance(cluster, list):
                    cluster = [cluster]
                num_checks += len(cluster)
                for check in cluster:
                    checks_by_id[check["key"][1]].append(check)
            for check, results in checks_by_id.items():
                if all([self.omit_loglevel(result['result']) for result in results]):
                    continue
                check_name = html.escape(check)
                body_elements.append(f"<h3>{results[0]['description']}</h3>")
                body_elements.append(f"<div>Check ID: {check_name}</div>")
                body_elements.append(self.render_rationale(results[0], check))
                for result in results:
                    if self.omit_loglevel(result['result']):
                        continue
                    if "filename" in result:
                        body_elements.append(
                            html5_collapsible(
                                f"{EMOTICON[result['result']]} <strong>{result['filename']}</strong>",
                                self.html_for_check(result),
                            )
                        )
                    else:
                        body_elements.append(
                            html5_collapsible(
                                f"{EMOTICON[result['result']]} <strong>Family check</strong>",
                                self.html_for_check(result),
                            )
                        )

        body_top = [
            "<h1>Fontbakery Technical Report</h1>",
            "<div>If you think a check is flawed or have an idea for a check, please "
            f" file an issue at <a href='{ISSUE_URL}'>{ISSUE_URL}</a> and remember "
            "to include a pointer to the repo and branch you're checking.</div>",
        ]

        if num_checks:
            results_summary = [data["result"][k] for k in LOGLEVELS]
            body_top.append(summary_table(*results_summary, num_checks))

        omitted = [l for l in LOGLEVELS if self.omit_loglevel(l)]
        if omitted:
            body_top.append(
                "<p><strong>Note:</strong>"
                " The following loglevels were omitted in this report:"
                f" {', '.join(omitted)}</p>"
            )

        body_elements[0:0] = body_top
        return html5_document(body_elements)

    def html_for_check(self, check) -> str:
        """Return HTML string for complete single check."""
        check["logs"].sort(key=lambda c: LOGLEVELS.index(c["status"]))
        logs = "<ul>" + "".join([self.log_html(log) for log in check["logs"]]) + "</ul>"
        return logs

    def render_rationale(self, check, checkid) -> str:
        if self.succinct or "rationale" not in check:
            return ""
        content = unindent_and_unwrap_rationale(check['rationale'], checkid)
        return cmarkgfm.markdown_to_html(
                content, options=cmarkgfmOptions.CMARK_OPT_UNSAFE
        )

    def log_html(self, log) -> str:
        """Return single check sub-result string as HTML or not if below log
        level."""
        if not self.omit_loglevel(log["status"]):
            emoticon = EMOTICON[log["status"]]
            status = log["status"]
            message = cmarkgfm.markdown_to_html(
                log["message"], options=cmarkgfmOptions.CMARK_OPT_UNSAFE
            )
            return (
                "<li class='details_item'>"
                f"<span class='details_indicator'>{emoticon} {status}</span>"
                f"<span class='details_text'>{message}</span>"
                "</li>"
            )
        return ""


def html5_document(body_elements) -> str:
    """Return complete HTML5 document string."""

    style = """
            html {
                font-family: sans-serif;
            }

            h2 {
                margin-top: 2em;
            }

            h3 {
                margin-bottom: 1px;
            }

            table {
                border-collapse: collapse;
            }

            th,
            td {
                border: 1px solid #ddd;
                padding: 0.5em
            }

            tr:nth-child(even) {
                background-color: #f2f2f2;
            }

            tr {
                text-align: left;
            }

            ul {
                margin-top: 0;
            }

            .details_item {
                list-style: none;
                display: flex;
                align-items: baseline;
            }

            .details_indicator {
                flex: 0 0 5em;
                font-weight: bold;
                padding-right: 0.5em;
                text-align: right;
            }

            .details_text {
                flex: 1 0;
            }
            """
    body = "\n".join(body_elements)
    return f"""<!DOCTYPE html>
                <html lang="en">
                    <head>
                        <meta charset="utf-8">
                        <title>Fontbakery Check Report</title>
                        <style>
                            {style}
                        </style>
                    </head>
                    <body>
                    {body}
                    </body>
                </html>"""


def summary_table(
    errors: int, fails: int, warns: int, skips: int, infos: int, passes: int, debugs: int, total: int
) -> str:
    """Return summary table with statistics."""

    # DEBUG messages are omitted for now...
    return f"""<h2>Summary</h2>
            <table>
            <tr>
                <th>{EMOTICON['ERROR']} ERROR</th>
                <th>{EMOTICON['FAIL']} FAIL</th>
                <th>{EMOTICON['WARN']} WARN</th>
                <th>{EMOTICON['SKIP']} SKIP</th>
                <th>{EMOTICON['INFO']} INFO</th>
                <th>{EMOTICON['PASS']} PASS</th>
            </tr>
            <tr>
                <td>{errors}</td>
                <td>{fails}</td>
                <td>{warns}</td>
                <td>{skips}</td>
                <td>{infos}</td>
                <td>{passes}</td>
            </tr>
            <tr>
                <td>{round(errors / total * 100)}%</td>
                <td>{round(fails / total * 100)}%</td>
                <td>{round(warns / total * 100)}%</td>
                <td>{round(skips / total * 100)}%</td>
                <td>{round(infos / total * 100)}%</td>
                <td>{round(passes / total * 100)}%</td>
            </tr>
            </table>
            """


# Lib/fontbakery/reporters/serialize.py

"""
Font Bakery reporters/serialize can report the events of the Font Bakery
CheckRunner Protocol to a serializeable document e.g. for usage with `json.dumps`.

Separation of Concerns Disclaimer:
While created specifically for checking fonts and font-families this
module has no domain knowledge about fonts. It can be used for any kind
of (document) checking. Please keep it so. It will be valuable for other
domains as well.
Domain specific knowledge should be encoded only in the Profile (Checks,
Conditions) and MAYBE in *customized* reporters e.g. subclasses.
"""
from fontbakery.checkrunner import (
              DEBUG
            , SECTIONSUMMARY
            , ENDCHECK
            , START
            , END
            )
from fontbakery.reporters import FontbakeryReporter
from fontbakery.checkrunner import Status

class SerializeReporter(FontbakeryReporter):
    """
    usage:
    >> sr = SerializeReporter(runner=runner, collect_results_by='font')
    >> sr.run()
    >> import json
    >> print(json.dumps(sr.getdoc(), sort_keys=True, indent=4))
    """


    def __init__(self, loglevels,
                     succinct=None,
                     collect_results_by=None,
                     **kwd):
        super().__init__(**kwd)
        self.succinct = succinct
        self.loglevels = loglevels
        self._results_by = collect_results_by
        self._items = {}
        self._doc = None

        # used when self._results_by is set
        # this way we minimize our knowledge of the profile
        self._max_cluster_by_index = None
        self._observed_checks = {}

    def _set_metadata(self, identity, item):
        section, check, iterargs = identity
        # If section is None this is the main doc.
        # If check is None this is `section`
        # otherwise this `check`
        pass

    def omit_loglevel(self, msg) -> bool:
        """Determine if message is below log level."""
        return self.loglevels and (
            self.loglevels[0] > Status(msg)
        )

    def _register(self, event):
        super()._register(event)
        status, message, identity = event
        section, check, iterargs = identity
        key = self._get_key(identity)

        # not item == True when item is empty
        item = self._items.get(key, {})
        if not item:
            self._items[key] = item
            # init
            if status in (START, END) and not item:
                item.update(dict(result=None, sections=[]))
                if self._results_by:
                    # give the consumer a clue that/how the sections
                    # are structured differently.
                    item['clusteredBy'] = self._results_by
            if status == SECTIONSUMMARY:
                item.update(dict(key=key, result=None, checks=[]))
            if check:
                item.update(dict(key=key, result=None, logs=[]))
                if self._results_by:
                    if self._results_by == '*check':
                        if check.id not in self._observed_checks:
                            self._observed_checks[check.id] = len(self._observed_checks)
                        index = self._observed_checks[check.id]
                        value = check.id
                    else:
                        index = dict(iterargs).get(self._results_by, None)
                        value = None
                        if self.runner:
                            value = self.runner.get_iterarg(self._results_by, index)

                    if index is not None:
                        if self._max_cluster_by_index is not None:
                            self._max_cluster_by_index = max(index, self._max_cluster_by_index)
                        else:
                            self._max_cluster_by_index = index

                    item['clustered'] = {
                        'name': self._results_by
                      , 'index': index # None if this check did not require self.results_by
                    }
                    if value: # Not set if self.runner was not defined on initialization
                        item['clustered']['value'] = value
            self._set_metadata(identity, item)

        if check:
            item['description'] = check.description
            if check.rationale:
                item['rationale'] = check.rationale
            if check.severity:
                item['severity'] = check.severity
            if item["key"][2] != ():
                item['filename'] = self.runner.get_iterarg(*item["key"][2][0])

        if status == END:
            item['result'] = message # is a Counter
        if status == SECTIONSUMMARY:
            _, item['result'] = message # message[1] is a Counter
        if status == ENDCHECK:
            item['result'] = message.name # is a Status
        if status >= DEBUG:
            item['logs'].append({'status': status.name,
                                 'message': f'{message}',
                                 'traceback': getattr(message, 'traceback', None)
                                })

    def getdoc(self):
        if not self._ended:
            raise Exception('Can\'t create doc before END status was recevived.')
        if self._doc is not None:
            return self._doc
        doc = self._items[self._get_key((None, None, None))]
        seen = set()
        # this puts all in the original order
        for identity in self._order:
            key = self._get_key(identity)
            section, _, _ = identity
            sectionKey = self._get_key((section, None, None))
            sectionDoc = self._items[sectionKey]

            check = self._items[key]
            if self._results_by:
                if not len(sectionDoc['checks']):
                    clusterlen = self._max_cluster_by_index + 1
                    if self._results_by != '*check':
                        # + 1 for rests bucket
                        clusterlen += 1
                    sectionDoc['checks'] = [[] for _ in range(clusterlen)]
                index = check['clustered']['index']
                if index is None:
                    # last element collects unclustered
                    index = -1
                sectionDoc['checks'][index].append(check)
            else:
                sectionDoc['checks'].append(check)
            if sectionKey not in seen:
                seen.add(sectionKey)
                doc['sections'].append(sectionDoc)
        self._doc = doc
        return doc

    def write(self):
        import json
        with open(self.output_file, "w") as fh:
            json.dump(self.getdoc(), fh, sort_keys=True, indent=4)
        print(f'A report in JSON format has been saved to "{self.output_file}"')



# Lib/fontbakery/reporters/terminal.py

"""
Font Bakery reporters/terminal can report the events of the Font Bakery
CheckRunner Protocol to the terminal (or by pipe to files). It understands
both, the synchronous and asynchronous execution model.

Separation of Concerns Disclaimer:
While created specifically for checking fonts and font-families this
module has no domain knowledge about fonts. It can be used for any kind
of (document) checking. Please keep it so. It will be valuable for other
domains as well.
Domain specific knowledge should be encoded only in the Profile (Checks,
Conditions) and MAYBE in *customized* reporters e.g. subclasses.

"""
import sys
import os
import subprocess
from collections import Counter
from functools import partial
import builtins # using this to override print function somewhere
from io import StringIO
from time import time

from fontbakery.reporters import FontbakeryReporter
from fontbakery.checkrunner import (  # NOQA
              INFO
            , WARN
            , ERROR
            , STARTCHECK
            , SKIP
            , PASS
            , FAIL
            , ENDCHECK
            , SECTIONSUMMARY
            , START
            , END
            , DEBUG
            , Status
            )

statuses = (
              INFO
            , WARN
            , ERROR
            , STARTCHECK
            , SKIP
            , PASS
            , FAIL
            , ENDCHECK
            , SECTIONSUMMARY
            , START
            , END
            , DEBUG
            )
# these are displayed in the result counters
check_statuses = [ERROR, FAIL, SKIP, PASS, WARN, INFO]
check_statuses.sort(key=lambda s:s.weight, reverse=True)


def formatStatus(theme, status, text=None):
    # status can be a status or a string
    name = getattr(status, 'name', status)
    if text is None:
        text = name
    try:
        return theme[name](text)
    except KeyError:
        return text

CUPCAKE = r"""
                           ,@.
                         ,@.@@,.
                   ,@@,.@@@.  @.@@@,.
                 ,@@. @@@.     @@. @@,.
         ,@@@.@,.@.              @.  @@@@,.@.@@,.
    ,@@.@.     @@.@@.            @,.    .@’ @’  @@,
  ,@@. @.          .@@.@@@.  @@’                  @,
 ,@.  @@.                                          @,
 @.     @,@@,.     ,                             .@@,
 @,.       .@,@@,.         .@@,.  ,       .@@,  @, @,
 @.                             .@. @ @@,.    ,      @
  @,.@@.     @,.      @@,.      @.           @,.    @’
   @@||@,.  @’@,.       @@,.  @@ @,.        @’@@,  @’
      \\@@@@’  @,.      @’@@@@’   @@,.   @@@’ //@@@’
       |||||||| @@,.  @@’ |||||||  |@@@|@||  ||
        \\\\\\\  ||@@@||  |||||||  |||||||  //
         |||||||  ||||||  ||||||   ||||||  ||
          \\\\\\  ||||||  ||||||  ||||||  //
           ||||||  |||||  |||||   |||||  ||
            \\\\\  |||||  |||||  |||||  //
             |||||  ||||  |||||  ||||  ||
              \\\\  ||||  ||||  ||||  //
               ||||||||||||||||||||||||

         No check is failing. Get a cupcake!
       <<Art by Tony de Marco, July 26, 2018>>
"""

class ThrottledOut:
    def __init__(self, outFile, holdback_time=None, max_ticks=0, draw_progressbar=None):
        """ holdback_time: float, 1.0 = 1 second"""
        self._outFile = outFile
        self._holdback_time = holdback_time
        self._last_flush_time = None
        self._buffer = []
        self._draw_progressbar = draw_progressbar
        self._current_ticks = 0
        self._max_ticks = max_ticks

    def write(self, data):
        """only put to stdout every now and then"""
        self._buffer.append(data)
        self._current_ticks += 1
        # first entry ever will be flushed immediately

        flush=False
        if self._last_flush_time is None or \
                    (self._holdback_time is None and self._max_ticks == 0):
            flush = True
        elif self._max_ticks and self._current_ticks >= self._max_ticks or \
             self._holdback_time and time() - self._holdback_time >= self._last_flush_time:
            flush=True

        if flush:
            self.flush()

    def flush(self, draw_progress=True):
        """call this at the very end, so that we can output the rest"""
        reset_progressbar = None
        if self._draw_progressbar and draw_progress:
            progressbar, reset_progressbar = self._draw_progressbar()
            self._buffer.append(progressbar)

        for line in self._buffer:
            self._outFile.write(line)
        #self._outFile.flush() needed?
        self._buffer = []
        if reset_progressbar:
            # first thing on next flush is to reset the current progressbar
            self._buffer.append(reset_progressbar)
        self._current_ticks = 0
        self._last_flush_time = time()


from fontbakery.constants import LIGHT_THEME

class TerminalProgress(FontbakeryReporter):
    def __init__(self, print_progress=True
                     , stdout=sys.stdout
                     , structure_threshold=None
                     , theme=LIGHT_THEME
                     , succinct=None
                     , cupcake=True
                       # a tuple of structural statuses to be skipped
                       # e.g. (SECTIONSUMMARY, )
                     , skip_status_report=None
                     , **kwd):
        super().__init__(**kwd)

        self.theme = theme
        self.succinct = succinct
        self._print_progress = stdout.isatty() and print_progress

        if self._print_progress:
            self.stdout = ThrottledOut(stdout, holdback_time=1/24, max_ticks=10
                                     , draw_progressbar=self.draw_progressbar)
        else:
            self.stdout = stdout

        self._progressbar = []
        self._cupcake = cupcake
        self._skip_status_report = skip_status_report or tuple()
        if structure_threshold:
            self._structure_threshold = min(START.weight, structure_threshold)
        else:
            self._structure_threshold = START.weight
        if self._structure_threshold % 2:
            # always include the according START status
            self._structure_threshold -= 1

    def _register(self, event):
        super()._register(event)
        status, message, identity = event
        if status == ENDCHECK and self._print_progress:
            self._set_progress_event(event)

    def _output(self, event):
        super()._output(event)
        text = self._render_event(event)
        if text:
            self.stdout.write(text)
        elif self._print_progress:
            # the empty string will change the ticks counter when self.stdout is a ThrottledOut
            self.stdout.write('')
        status, _, _ = event
        if status == END and self._print_progress:
            # this flush is only relevant when self.stdout is a ThrottledOut
            self.stdout.flush(False)

    def _render_event(self, event):
        status, message, (section, check, iterargs) = event
        output = StringIO()
        print = partial(builtins.print, file=output)

        if not status.weight >= self._structure_threshold \
                                          or status in self._skip_status_report:
            return output.getvalue()

        if status == START:
            order = message
            print(f'Start ... running {len(order)} individual check executions.')

        if status == END:
            if self._print_progress:
                print(self._draw_progressbar())#.encode('utf-8'))
            print('')
            if self._cupcake and len(self._order) \
               and self._counter[ERROR.name] + self._counter[FAIL.name] == 0 \
               and self._counter[PASS.name] > 20:
                print(self.theme["cupcake"](CUPCAKE))
            print('DONE!')
        return output.getvalue()

    def _set_order(self, order):
        super()._set_order(order)
        if self._print_progress:
            # set/reset
            self._progressbar = list('.'*len(order))
            for event in self._results:
                self._set_progress_event(event)

    def _set_progress_event(self, event):
        _, status, identity = event
        index = self._get_index(identity)
        self._progressbar[index] = formatStatus(self.theme,
                                                status,
                                                status.name[0])

    def _get_index(self, identity):
        index = super()._get_index(identity)
        if self._print_progress and len(self._indexes) < len(self._progressbar):
            self._progressbar.append('.')
        return index

    def _reset_progress(self, num_linebeaks):
        # BACKSPACE = u'\b'
        TOLEFT = '\u001b[1000D' # Move all the way left (max 1000 steps
        CLEARLINE = '\u001b[2K'    # Clear the line
        UP =  '\u001b[1A' # moves cursor 1 up
        reset = (CLEARLINE + UP) * num_linebeaks + TOLEFT
        return reset

    def _draw_progressbar(self, columns=None, len_prefix=0, right_margin=0):
        """
        if columns is None, don't insert any extra line breaks
        """
        if self._order == None:
            total = len(self._results)
        else:
            total = max(len(self._order), len(self._results))

        percent = int(round(len(self._results)/total*100)) if total else 0

        needs_break = lambda count: columns and count > columns \
                                    and (count % (columns - right_margin))

        # together with unicode_literals `str('status')` seems the best
        # py2 and py3 compatible solution
        status = type(str('status'), (object,), dict(count=0,progressbar=[]))
        def _append(status, item, length=1, separator=''):
            # * assuming a standard item will take one column in the tty
            # * length must not be bigger than columns (=very narrow columns)
            progressbar = status.progressbar
            if needs_break(status.count + length + len(separator)):
                progressbar.append('\n')
                status.count = 0
            else:
                progressbar.append(separator)
            status.count += length + len(separator)
            progressbar.append(item)
        append=partial(_append, status)
        progressbar = status.progressbar

        append('', len_prefix)
        append('[')
        for item in self._progressbar:
            append(item)
        append(']')
        percentstring = f'{percent:3d}%'
        append(percentstring, len(percentstring), ' ')
        return ''.join(progressbar)


    def draw_progressbar(self):
        # tty size
        if sys.platform == "win32":
            mode = subprocess.Popen(
                "mode", shell=True, stdout=subprocess.PIPE).stdout.readlines()
            columns_entry = [s for s in mode if b"Columns" in s]
            columns = int(columns_entry[0].split()[1])
        else:
            columns = int(os.popen('stty size').read().split()[1])
        # this is the amout of space the spinner takes when rendered in the tty
        # NOTE: the color codes are not taking space in the tty, so we can't
        # just take the length of `spinner`.
        # 1 for the spinner + 1 for the separating space
        progressbar = self._draw_progressbar(columns, len_prefix=2)
        counter = _render_results_counter(self.theme, self._counter)

        spinnerstates = ' ░▒▓█▓▒░'
        spinner = spinnerstates[self._tick % len(spinnerstates)]
        spinner = self.theme["spinner"](spinner)

        rendered = f'\n{counter}\n\n{spinner} {progressbar}\n'
        num_linebeaks = rendered.count('\n')
        return rendered, self._reset_progress(num_linebeaks)

def _render_results_counter(theme, counter):
    format = '    {}: {}'.format
    result = []

    seen = set()
    for s in check_statuses:
        name = s.name
        seen.add(name)
        result.append(format(formatStatus(theme, s), counter[name]))

    # there may be custom statuses
    for name in counter:
        if name not in seen:
            seen.add(name)
            result.append(format(formatStatus(theme, name), counter[name]))
    return '\n'.join(result)

class TerminalReporter(TerminalProgress):
    """
    yield for each check
    on endcheck, make a summary of the check and yield that
    """
    def __init__(self, collect_results_by=None
                     , check_threshold=None
                     , log_threshold=None
                     , **kwd):
        super().__init__(**kwd)
        self.results_by = collect_results_by
        self._collected_results = {}
        self._event_buffers = {}

        # logs can occur at any point in the logging protocol
        # especially DEBUG, INFO, WARNING and ERROR
        # FAIL, PASS and SKIP are only expected within checks though
        # Log statuses have weights >= 0
        log_threshold = log_threshold if type(log_threshold) is not Status \
                                      else log_threshold.weight
        self._log_threshold = min(ERROR.weight + 1 , max(0, log_threshold))

        # Use this to silence the output checks in async mode, it also activates
        # async mode if turned off.
        # You can't silence whole checks in sync output, as the events are
        # rendered as soon as they happen, you can however silence some log
        # messages in sync mode, use log_threshold for this.
        # default: no DEBUG output
        check_threshold = check_threshold if type(check_threshold) is not Status \
                                          else check_threshold.weight
        self._check_threshold = min(ERROR.weight + 1, max(PASS.weight, check_threshold))

        # if this is used we must use async rendering, otherwise we can't
        # suppress the output of checks, because we only know the final
        # status after ENDCHECK.
        self._render_async = self.is_async or check_threshold is not None

    def _register(self, event):
        super()._register(event)
        status, message, (section, check, iterargs) = event

        if self.results_by and status == ENDCHECK:

            key = check.id if self.results_by == '*check' \
                           else dict(iterargs).get(self.results_by, None)
            if key not in self._collected_results:
                self._collected_results[key] = Counter()
            self._collected_results[key][message.name] += 1

    def _render_event_sync(self, print, event):
        import re
        status, msg, (section, check, iterargs) = event

        if not status.weight >= self._structure_threshold \
           or status in self._skip_status_report:
            return

        if status == START:
            text = super()._render_event(event)
            if text:
                self.stdout.write(text)

        if status == STARTCHECK:
            if self.runner:
                formatted_iterargs = tuple(
                    ('{}[{}]'.format(*item), self.runner.get_iterarg(*item))
                    for item in iterargs)
            else:
                formatted_iterargs = iterargs

            if self.succinct:
                with_string = "All fonts"
                if formatted_iterargs != ():
                    with_string = os.path.basename(f"{formatted_iterargs[0][1]}")

                print((" >> Check:   {}\n"
                       "    Desc:    {}\n"
                       "    Files:   {}").format(
                    self.theme["check-id"](check.id),
                    self.theme["description"](check.description),
                    with_string))

            else:
                # Omit printing of iterargs when there's none of them:
                with_string = ""
                if formatted_iterargs != ():
                    with_string = f"with {formatted_iterargs[0][1]}"

                print((' >> {}\n'
                       '    {}\n'
                       '    {}\n').format(
                    self.theme["check-id"](check.id),
                    self.theme["description"](check.description),
                    with_string))

                if check.rationale:
                    from fontbakery.utils import text_flow, unindent_and_unwrap_rationale
                    content = unindent_and_unwrap_rationale(check.rationale)
                    print('    ' + self.theme["rationale-title"]("  Rationale:" + " " * 64) + '\n'
                          + text_flow(content,
                                      width=76,
                                      indent=4,
                                      left_margin=2,
                                      right_margin=1,
                                      space_padding=True,
                                      text_color=self.theme["rationale-text"]))

        # Log statuses have weights >= 0
        # log_statuses = (INFO, WARN, PASS, SKIP, FAIL, ERROR, DEBUG)
        if status.weight >= self._log_threshold:
            print('')

            from fontbakery.utils import text_flow
            status_name = getattr(status, 'name', status)

            try:
                message = (f"{msg.message}\n"
                           f"[code: {msg.code}]")
            except:
                message = str(msg)

            if hasattr(msg, 'traceback'):
                message = re.sub(r'(<[^<>]*>)', r'**`\1`**', message, flags=re.MULTILINE)
                traceback = '  \n`' + '`  \n`'.join(msg.traceback.strip().split('\n')) + '`'
                traceback = re.sub(r'\n`\s*(File ".*)`', r'\n  ↳ \1', traceback)
                traceback = re.sub(r'\n`\s*(.*)', r'\n`\1', traceback)
                message += traceback

            formated_msg = '{} {}'.format(formatStatus(self.theme, status), message)
            formated_msg = parse_md(formated_msg)

            print(text_flow(formated_msg,
                            width=76,
                            indent=4,
                            first_line_indent=-1-len(status_name),
                            left_margin=6,
                            space_padding=True))

        if status == ENDCHECK:
            if not self.succinct:
                print('\n')
            print('    Result: {}\n'.format(formatStatus(self.theme, msg)))

        if status == SECTIONSUMMARY:
            order, counter = msg
            print('')
            print('='*8, f'Section results: {section}','='*8)
            print('{} {} in section'.format(len(order)
                                          , len(order) == 1 and 'check' or 'checks' ))
            print('')
            print(_render_results_counter(self.theme, counter))

        if status == END:
            print('')
            if self.results_by:
                print('Collected results by', self.results_by)
                for key in self._collected_results:
                    if self.results_by == '*check':
                        val = key
                    elif key is not None and self.runner:
                        val = self.runner.get_iterarg(self.results_by, key)
                    elif key is not None:
                        val = key
                    else:
                        val = f'(not using "{self.results_by}")'
                    print(f'{self.results_by}: {val}')
                    print(_render_results_counter(self.theme, self._collected_results[key]))
                    print('')

            print('Total:')
            print('')
            print(_render_results_counter(self.theme, msg))
            print('')

            # same end message as parent
            text = super()._render_event(event)
            if text:
                print(text)

            print(f"    {self.theme['header']('Meaning of check results:')}\n"
                  f"\n"
                  f"    An {formatStatus(self.theme, 'ERROR')} is something"
                  f" wrong with FontBakery itself, possibly a bug.\n"
                  f"    A {formatStatus(self.theme, 'FAIL')} is a problem"
                  f" with the font that must be fixed.\n"
                  f"    A {formatStatus(self.theme, 'WARN')} is something"
                  f" that you should consider addressing.\n"
                  f"    An {formatStatus(self.theme, 'INFO')} result simply"
                  f" prints something useful. Typically stats.\n"
                  f"    A {formatStatus(self.theme, 'PASS')} means the font looks good"
                  f" for the given checking routine.\n"
                  f"    And a {formatStatus(self.theme, 'SKIP')} happens when the check"
                  f" does not apply to the given font.\n"
                  f"\n"
                  f"    If you get {formatStatus(self.theme, 'ERROR')}s,"
                  f" please help us improve the tool by reporting them at\n"
                  f"    {self.theme['url']('https://github.com/googlefonts/fontbakery/issues')}\n"
                  f"\n"
                  f"    (but other kinds of bug reports and/or\n"
                  f"     feature requests are also always welcome, of course!)\n")

        if status not in statuses:
            print('-'*8, status , '-'*8)

    def _render_event_async(self, print, event):
        status, message, identity = event
        (section, check, iterargs) = identity
        key = self._get_key(identity)
        logs = self._event_buffers.get(key, None)
        if logs is None:
            self._event_buffers[key] = logs = {
                'start': None
              , 'logs': []
              , 'end': None
            }

        # STARTCHECK
        if status.weight < 0 and status.weight % 2 == 0 :
            logs['start'] = event
        # ENDCHECK, SECTIONSUMMARY
        elif status.weight < 0 and status.weight % 2 == 1 :
            logs['end'] = event
        else:
            logs['logs'].append(event)

        if status == ENDCHECK \
           and message.weight >= self._check_threshold \
           or status == SECTIONSUMMARY:
            for e in [logs['start']] + logs['logs'] + [logs['end']]:
                if e is not None:
                    self._render_event_sync(print, e)

        if not section:
            self._render_event_sync(print, event)

    def _render_event(self, event):
        status, message, (section, check, iterargs) = event
        output = StringIO()
        print = partial(builtins.print, file=output)

        if self._render_async:
            self._render_event_async(print, event)
        else:
            self._render_event_sync(print, event)

        return output.getvalue()

def parse_md(md):
    from rich.console import Console
    from rich.markdown import Markdown
    import re
    table_re = r'(^|[^|]\n)((?:^\|[^\n]*\|(?:\n|$))+)([^|]|$)'
    md = re.sub(r'\n([\r\t ]*\n)+', r'\n\n', md, flags=re.MULTILINE)
    md = re.sub(r'^[\t ]*\|', r'|', md, flags=re.MULTILINE)
    md = re.sub(r'\|[\t ]*$', r'|', md, flags=re.MULTILINE)
    tables = []
    md = re.sub(table_re, lambda match: parse_md_table(match, tables), md,
                flags=re.MULTILINE | re.S)
    console = Console(width=70)
    with console.capture() as capture:
        console.print(Markdown(md))
    formated_text = capture.get()
    for i in range(len(tables)):
        formated_text = re.sub('<#MD-TABLE-'+str(i)+'#>', tables[i], formated_text)
    return re.sub(r'<br/?>', r'\n', formated_text).strip()

def parse_md_table(match, tables_memo):
    from rich.console import Console
    from rich.table import Table
    from rich.style import Style
    from rich.align import Align
    from rich import box
    import re
    [table_header, table_body, columns] = split_md_table(match.group(2))
    b = box.Box('    \n    \n══╪═\n    \n┈┈┼┈\n┈┈┼┈\n    \n    ')
    table = Table(box=b, show_header=False, show_edge=False, border_style=Style(color='#222222'))
    for col_align in columns:
        table.add_column(None, justify=col_align)
    sty_header = Style(bgcolor='blue', color='#000000', bold=True)
    sty_odd = Style(bgcolor="#111111", color='white')
    sty_even = Style(bgcolor="#222222", color='white')
    for row in table_header:
        row = map(Align.center, row)
        table.add_row(*row, style=sty_header)
    num = 0
    for row in table_body:
        num += 1
        style = sty_even if (num % 2 == 0) else sty_odd
        table.add_row(*row, style=style)

    console = Console(width=70)
    with console.capture() as capture:
        console.print(table)
    formated_text = '\n'.join(capture.get().split('\n')[0:-1])
    before = '' if match.group(1) == '\n\n' else '\n'
    after = '  ' if match.group(3) == '\n' else '  \n'
    tables_memo.append(before + formated_text)
    return match.group(1) + \
           '<#MD-TABLE-'+str(len(tables_memo)-1)+'#>' + after + \
           match.group(3)

def map_md_table_align_col(cell):
    import re
    if re.match(r'^\s*:-+:\s*$', cell):
        return 'center'
    if re.match(r'^\s*-+:\s*$', cell):
        return 'right'
    return 'left'

def split_md_table(md_table):
    import re
    md_table = re.sub(r'^\||\|$', '', md_table, flags=re.MULTILINE)
    table_header = []
    table_body = []
    columns = []
    table = list(map(
        lambda row:
            list(map(lambda cell: cell.strip(), row.split('|'))),
        md_table.strip().split('\n')
    ))
    has_header = False
    for row in table:
        if re.match(r'^\s*:?-+:?\s*$', row[0]):
            has_header = True
    in_header = has_header
    for row in table:
        if in_header:
            if re.match(r'^\s*:?-+:?\s*$', row[0]):
                in_header = False
                columns = map(map_md_table_align_col, row)
            else:
                table_header.append(row)
        else:
            table_body.append(row)
    return [table_header, table_body, columns]


# Lib/fontbakery/sphinx_extensions/linkcode.py

"""
    sphinx.ext.linkcode
    ~~~~~~~~~~~~~~~~~~~

    Add external links to module code in Python object descriptions.

    :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS.
    :license: BSD, see LICENSE for details.
"""

# started as a clone of
# https://github.com/sphinx-doc/sphinx/blob/f4a98d8ca0cc88a5a993a6b90a3af0892fdc19c0/sphinx/ext/linkcode.py

from docutils import nodes

import sphinx
from sphinx import addnodes
from sphinx.errors import SphinxError
from sphinx.locale import _

if False: #pylint: disable=using-constant-test
    # For type annotation
    from typing import Any, Dict, Set  # NOQA
    from sphinx.application import Sphinx  # NOQA


class LinkcodeError(SphinxError):
    category = "linkcode error"


def doctree_read(app, doctree):
    # type: (Sphinx, nodes.Node) -> None
    env = app.builder.env

    resolve_target = getattr(env.config, 'linkcode_resolve', None)
    if not callable(env.config.linkcode_resolve):
        raise LinkcodeError(
            "Function `linkcode_resolve` is not given in conf.py")

    domain_keys = {
        'py': ['module', 'fullname'],
        'c': ['names'],
        'cpp': ['names'],
        'js': ['object', 'fullname'],
    }

    for objnode in doctree.traverse(addnodes.desc):
        domain = objnode.get('domain')
        uris = set()  # type: Set[str]
        for signode in objnode:
            if not isinstance(signode, addnodes.desc_signature):
                continue

            # Convert signode to a specified format
            info = {}
            for key in domain_keys.get(domain, []):
                value = signode.get(key)
                if not value:
                    value = ''
                info[key] = value
            if not info:
                continue

            if 'lineno' in signode.attributes:
                info['lineno'] = signode.attributes['lineno']
            # Call user code to resolve the link
            uri = resolve_target(domain, info)
            if not uri:
                # no source
                continue

            if uri in uris or not uri:
                # only one link per name, please
                continue
            uris.add(uri)

            inline = nodes.inline('', _('[source]'), classes=['viewcode-link'])
            onlynode = addnodes.only(expr='html')
            onlynode += nodes.reference('', '', inline, internal=False, refuri=uri)
            signode += onlynode


def setup(app):
    # type: (Sphinx) -> Dict[str, Any]
    app.connect('doctree-read', doctree_read)
    app.add_config_value('linkcode_resolve', None, '')
    return {'version': sphinx.__display_version__, 'parallel_read_safe': True}


# Lib/fontbakery/sphinx_extensions/profile.py

from typing import Any, List, Tuple, Dict #cast

from sphinx.application import Sphinx
# from sphinx.ext.autodoc import Documenter
from sphinx.ext.autodoc import ModuleLevelDocumenter
from sphinx.pycode import ModuleAnalyzer, PycodeError
#from sphinx.domains.python import PythonDomain
from sphinx.locale import __
from sphinx.domains.python import PyObject
from sphinx import addnodes
from sphinx.util.inspect import signature as Signature
from sphinx.util.inspect import stringify_signature

import logging
logger = logging.getLogger(__name__)

# we can get source code first line numbers with this module for object
import inspect

from fontbakery.callable import (
                          FontbakeryCallable
                        , FontBakeryCondition
                        , FontBakeryCheck
                        , Disabled
                        , FontBakeryExpectedValue
                        )

# mute the style checks for unused names
# will be removed eventually
if False: #pylint: disable=using-constant-test
    FontbakeryCallable
    FontBakeryCondition
    FontBakeryCheck
    Disabled
    FontBakeryExpectedValue

__version__ = '0.0.1'


# ModuleLevelDocumenter(Documenter): Specialized Documenter subclass for objects on module level (functions,
# classes, data/constants). Implements: resolve_name
# https://github.com/sphinx-doc/sphinx/blob/master/sphinx/ext/autodoc/__init__.py#L850
# Documenter
class FontBakeryCallableDocumenter(ModuleLevelDocumenter):
    """
    Specialized Documenter subclass for instances of FontBakeryCheck.
    """
    objtype = 'fontbakerycallable'
    can_doc_cls = FontbakeryCallable
    member_order = 30

    @classmethod
    def can_document_member(cls, member, membername, isattr, parent):
        # type: (Any, str, bool, Any) -> bool
        return isinstance(member, cls.can_doc_cls)

    def format_args(self): # pylint: disable=arguments-differ  # I am really not sure what went wrong here...
        # type: () -> str
        # We use the original signature from the wrapped _function
        has_retval = isinstance(self.object, FontBakeryCondition)

        if not hasattr(self.object, '_func'):
            # FIXME! I don't know what's this.
            return None

        sig = Signature(self.object._func, bound_method=False, has_retval=has_retval)
        args = stringify_signature(sig)
        # escape backslashes for reST
        args = args.replace('\\', '\\\\')
        return args

    def format_name(self):
        # I'm using this to inject some new info into the check
        # search for the separator ":::" in this document to see where
        # the info is received. This is not a clean solution!
        #
        # in https://github.com/sphinx-doc/sphinx/blob/master/sphinx/ext/autodoc/__init__.py#L374
        # it says:
        #  > This normally should be something that can be parsed by the generated
        #  > directive, but doesn't need to be (Sphinx will display it unparsed
        #  > then).
        # See below in `handle_signature`
        # where that ipdb debugger is started, usually that eception would be
        # dropped and we drop out of signature building. (RAISED here in `_handle_signature`
        # The ValueError when the regex doesn't match...)
        # seems like the slash (/) Is killing most of the header!
        # Otherwise the ids display fine, the dots are fine.
        # Also, in any case of name change, the [source] view is killed (removed!)
        # the document and also genindex.html anchor works so far (with 7 instead of /)
        #
        res = super().format_name()
        if self.objtype == 'fontbakerycheck':
            # A bit hackish, splitting somwhere else by ::: to retrieve the checkid
            # we can get the source file first line number of self.object:
            lineno = inspect.getsourcelines(self.object)[1]
            res =  self.object.id + ':::' + f'{lineno}' + ':::' + res#.replace('/', '7')
        # else:
        #   res = super().format_name()
        # print('formatted name:', res)
        # > formatted name: com.google.fonts/check/xavgcharwidth:::59:::com_google_fonts_check_xavgcharwidth
        # > formatted name: bold_wght_coord

        return res

        # handle_signature: com_google_fonts_check_post_table_version(ttFont, is_ttf) <desc_signature first="False"/>
        # sig signature: com_google_fonts_check_post_table_version(ttFont, is_ttf)
        # result: ('com_google_fonts_check_post_table_version', None) signode: <desc_signature class="" first="False" fullname="com_google_fonts_check_post_table_version" module="fontbakery.profiles.post"><desc_annotation xml:space="preserve">FontBakeryCheck </desc_annotation><desc_addname xml:space="preserve">fontbakery.profiles.post.</desc_addname><desc_name xml:space="preserve">com_google_fonts_check_post_table_version</desc_name><desc_parameterlist xml:space="preserve"><desc_parameter xml:space="preserve">ttFont</desc_parameter><desc_parameter xml:space="preserve">is_ttf</desc_parameter></desc_parameterlist></desc_signature>

    def generate(self, more_content=None, real_modname=None,
                       check_module=False, all_members=False):
        # type: (Any, str, bool, bool) -> None
        """Generate reST for the object given by *self.name*, and possibly for
        its members.

        If *more_content* is given, include that content. If *real_modname* is
        given, use that module name to find attribute docs. If *check_module* is
        True, only generate if the object is defined in the module name it is
        imported from. If *all_members* is True, document all members.
        """
        # print('generate', more_content, real_modname, check_module, all_members)
        # print(self.name)
        # print('---------------------')
        # > generate None fontbakery.profiles.post True True
        # > fontbakery.profiles.post::com_google_fonts_check_post_table_version
        # > ---------------------
        #
        # > generate None fontbakery.profiles.shared_conditions True True
        # > fontbakery.profiles.shared_conditions::glyph_metrics_stats
        # > ---------------------
        if not self.parse_name():
            # need a module to import
            logger.warning(
                __('don\'t know which module to import for autodocumenting '
                   '%r (try placing a "module" or "currentmodule" directive '
                   'in the document, or giving an explicit module name)') %
                   self.name, type='autodoc')
            return


        # now, import the module and get object to document
        if not self.import_object():
            return

        # doesn't do anything!
        # if self.objtype == 'fontbakerycheck':
        #   self.name = self.object.id


        # If there is no real module defined, figure out which to use.
        # The real module is used in the module analyzer to look up the module
        # where the attribute documentation would actually be found in.
        # This is used for situations where you have a module that collects the
        # functions and classes of internal submodules.
        self.real_modname = real_modname or self.get_real_modname()  # type: str

        # try to also get a source code analyzer for attribute docs
        try:
            self.analyzer = ModuleAnalyzer.for_module(self.real_modname)
            # parse right now, to get PycodeErrors on parsing (results will
            # be cached anyway)
            self.analyzer.find_attr_docs()
        except PycodeError as err:
            logger.debug('[autodoc] module analyzer failed: %s', err)
            # no source file -- e.g. for builtin and C modules
            self.analyzer = None
            # at least add the module.__file__ as a dependency
            if hasattr(self.module, '__file__') and self.module.__file__:
                self.directive.filename_set.add(self.module.__file__)
        else:
            self.directive.filename_set.add(self.analyzer.srcname)


        # check __module__ of object (for members not given explicitly)
        if check_module:
            if not self.check_module():
                return


        sourcename = self.get_sourcename()


        # make sure that the result starts with an empty line.  This is
        # necessary for some situations where another directive preprocesses
        # reST and no starting newline is present
        self.add_line('', sourcename)


        # format the object's signature, if any
        sig = self.format_signature()

        # generate the directive header and options, if applicable
        self.add_directive_header(sig)
        self.add_line('', sourcename)


        # e.g. the module directive doesn't have content
        self.indent += self.content_indent


        # add all content (from docstrings, attribute docs etc.)
        self.add_content(more_content)


        # document members, if possible
        self.document_members(all_members)


class FontBakeryCheckDocumenter(FontBakeryCallableDocumenter):
    objtype = 'fontbakerycheck'
    can_doc_cls = FontBakeryCheck


class FontBakeryConditionDocumenter(FontBakeryCallableDocumenter):
    objtype = 'fontbakerycondition'
    can_doc_cls = FontBakeryCondition

from sphinx.domains.python import _pseudo_parse_arglist

import re
# REs for Python signatures
py_sig_re = re.compile(
    r'''^ ([\w.]*\.)?            # class name(s)
          (\w+)  \s*             # thing name
          (?: \(\s*(.*)\s*\)     # optional: arguments
           (?:\s* -> \s* (.*))?  #           return annotation
          )? $                   # and nothing more
     ''', re.VERBOSE)

# PyObject: https://github.com/sphinx-doc/sphinx/blob/master/sphinx/domains/python.py#L189
# PyObject is a subclass of sphinx.directives.ObjectDescription
# ObjectDescription is a sphinx.util.docutils.SphinxDirective
# SphinxDirective is a docutils.parsers.rst.Directive
class PyFontBakeryObject(PyObject):
    """
    Description of a class-like object (classes, interfaces, exceptions).
    """

    allow_nesting = True

    @property
    def pretty_objtype(self):
        if self.objtype.startswith('fontbakery'):
            suffix = self.objtype[len('fontbakery'):]
            return 'FontBakery' + suffix[0].upper() + suffix[1:]
        return self.objtype

    def get_signature_prefix(self, sig):
        # type: (str) -> str
        # import ipdb
        # ipdb.set_trace()
        # print('sig signature:', sig)
        # > sig signature: com_google_fonts_check_all_glyphs_have_codepoints(ttFont)

        return self.pretty_objtype + ' '

    # this is bullshit, returns two values but manipulates
    # signode massively, which is undocumented.
    # signode is an instance of <class 'sphinx.addnodes.desc_signature'>
    # from https://github.com/sphinx-doc/sphinx/blob/master/sphinx/domains/python.py#L237
    def _handle_signature(self, cid, lineno, sig, signode):
        # type: (str, addnodes.desc_signature) -> Tuple[str, str]
        """Transform a Python signature into RST nodes.

        Return (fully qualified name of the thing, classname if any).

        If inside a class, the current class name is handled intelligently:
        * it is stripped from the displayed name if present
        * it is added to the full name (return value) if not present

        This is the xml string result of signode, whitespace is not
        equivalent for readability.
        <desc_signature
            class=""
            first="False"
            fullname="com.google.fonts/check/all_glyphs_have_codepoints"
            module="fontbakery.profiles.cmap"
            >
                <desc_annotation
                    xml:space="preserve">FontBakeryCheck </desc_annotation>
                <desc_addname
                    xml:space="preserve">fontbakery.profiles.cmap.</desc_addname>
                <desc_name
                    xml:space="preserve">com_google_fonts_check_all_glyphs_have_codepoints</desc_name>
                <desc_parameterlist
                    xml:space="preserve">
                        <desc_parameter xml:space="preserve">ttFont</desc_parameter>
                </desc_parameterlist>
          </desc_signature>

        """
        m = py_sig_re.match(sig)
        if m is None:
            # this is the immediate fail!!!
            raise ValueError
        prefix, name, arglist, retann = m.groups()
        # print('prefix, name, arglist, retann =', prefix, name, arglist, retann)
        # > prefix, name, arglist, retann = None com_google_fonts_check_all_glyphs_have_codepoints ttFont None

        # determine module and class name (if applicable), as well as full name
        modname = self.options.get('module', self.env.ref_context.get('py:module'))
        classname = self.env.ref_context.get('py:class')
        if classname:
            add_module = False
            if prefix and (prefix == classname or
               prefix.startswith(classname + ".")):
                fullname = prefix + name
                # class name is given again in the signature
                prefix = prefix[len(classname):].lstrip('.')
            elif prefix:
                # class name is given in the signature, but different
                # (shouldn't happen)
                fullname = classname + '.' + prefix + name
            else:
                # class name is not given in the signature
                fullname = classname + '.' + name
        else:
            add_module = True
            if prefix:
                classname = prefix.rstrip('.')
                fullname = prefix + name
            else:
                classname = ''
                fullname = name


        signode['module'] = modname
        signode['class'] = classname
        signode['fullname'] = fullname
        signode.attributes['lineno'] = lineno


        #sig_prefix = self.get_signature_prefix(sig)
        #if sig_prefix:
        #  signode += addnodes.desc_annotation(sig_prefix, sig_prefix)


        if prefix:
            signode += addnodes.desc_addname(prefix, prefix)
        elif add_module and self.env.config.add_module_names:
            if modname and modname != 'exceptions':
                # exceptions are a special case, since they are documented in the
                # 'exceptions' module.
                #nodetext = modname + ' ID: '
                #signode += addnodes.desc_addname(nodetext, nodetext)
                pass


        signode += addnodes.desc_name(name, cid)
        if arglist:
            _pseudo_parse_arglist(signode, arglist)
        else:
            if self.needs_arglist():
                # for callables, add an empty parameter list
                signode += addnodes.desc_parameterlist()


        if retann:
            signode += addnodes.desc_returns(retann, retann)


        anno = self.options.get('annotation')
        if anno:
            signode += addnodes.desc_annotation(' ' + anno, ' ' + anno)


        return cid, prefix

    def handle_signature(self, sig, signode):
        # print('>>>>>>>>>>>>>>>>>handle_signature:', sig, signode)
        # > >>>>>>>>>>>>>>>>>handle_signature: com.google.fonts/check/all_glyphs_have_codepoints:::36:::com_google_fonts_check_all_glyphs_have_codepoints(ttFont) <desc_signature first="False"/>

        cid = None
        if ':::' in sig:
            cid, lineno, sig = sig.split(':::')
            # print('GOT id:', cid, lineno, 'for:', sig)
            # > GOT id: com.google.fonts/check/all_glyphs_have_codepoints 36 for: com_google_fonts_check_all_glyphs_have_codepoints(ttFont)

        res = '(NONE!)'
        try:
            res = self._handle_signature(cid, lineno, sig, signode) if cid is not None\
                            else super().handle_signature(sig, signode)
        except Exception as e:
            print('!!!', e)
            raise e

        return res

    # This ends in: path-to-docs/html/genindex.html
    def get_index_text(self, modname, name):
        # type: (str, Tuple[str, str]) -> str

        return f'{name[0]} ({self.pretty_objtype} in {modname})'
        # fontbakerycheck
        # modname: fontbakery.profiles.cmap
        # name_cls:('com_google_fonts_check_all_glyphs_have_codepoints', None)
        # return f' {self.objtype} modname: {modname} name_cls:{name_cls}'

    def add_target_and_index(self, name_cls, sig, signode):
        # type: (Tuple[str, str], str, addnodes.desc_signature) -> None
        modname = self.options.get('module', self.env.ref_context.get('py:module'))
        # fullname = (modname and modname + '.' or '') + name_cls[0]
        fullname = name_cls[0]
        # note target
        if fullname not in self.state.document.ids:
            signode['names'].append(fullname)
            signode['ids'].append(fullname)
            signode['first'] = (not self.names)
            self.state.document.note_explicit_target(signode)

            # note, there will be a change to this in a future release
            # https://github.com/sphinx-doc/sphinx/commit/259be8716ad4b2332aa4d7693d73400eb06fa7d7
            ## in the past (now)
            objects = self.env.domaindata['py']['objects']
            if fullname in objects:
                self.state_machine.reporter.warning(
                    'duplicate object description of %s, ' % fullname +
                    'other instance in ' +
                    self.env.doc2path(objects[fullname][0]) +
                    ', use :noindex: for one of them',
                    line=self.lineno)
                objects[fullname] = (self.env.docname, self.objtype)
            ## in the future
            # domain = cast(PythonDomain, self.env.get_domain('py'))
            # domain.note_object(fullname, self.objtype)

        indextext = self.get_index_text(modname, name_cls)
        if indextext:
            self.indexnode['entries'].append(('single',
                                              indextext,
                                              fullname,
                                              '',
                                              None))

# Copied a lot from napoleon extension:
# https://github.com/sphinx-doc/sphinx/blob/master/sphinx/ext/napoleon/__init__.py
# To get started, hooking into autodoc seems the way to go, hence that was
# a good fit.

def setup(app):
    # type: (Sphinx) -> Dict[str, Any]
    """Sphinx extension setup function.
    When the extension is loaded, Sphinx imports this module and executes
    the ``setup()`` function, which in turn notifies Sphinx of everything
    the extension offers.
    Parameters
    ----------
    app : sphinx.application.Sphinx
        Application object representing the Sphinx process
    See Also
    --------
    `The Sphinx documentation on Extensions
    <http://sphinx-doc.org/extensions.html>`_
    `The Extension Tutorial <http://sphinx-doc.org/extdev/tutorial.html>`_
    `The Extension API <http://sphinx-doc.org/extdev/appapi.html>`_
    """
    if not isinstance(app, Sphinx):
        # probably called by tests
        return {'version': __version__, 'parallel_read_safe': True}

    # _patch_python_domain()

    #=> this:
    app.add_autodocumenter(FontBakeryCallableDocumenter)
    app.add_autodocumenter(FontBakeryCheckDocumenter)
    app.add_autodocumenter(FontBakeryConditionDocumenter)

    # https://github.com/sphinx-doc/sphinx/blob/master/sphinx/domains/python.py
    app.add_directive_to_domain('py', 'fontbakerycallable', PyFontBakeryObject, override=False)
    app.add_directive_to_domain('py', 'fontbakerycheck', PyFontBakeryObject, override=False)
    app.add_directive_to_domain('py', 'fontbakerycondition', PyFontBakeryObject, override=False)

    # => see e.g.: https://github.com/sphinx-doc/sphinx/blob/master/sphinx/ext/autodoc/__init__.py#L984


    app.setup_extension('sphinx.ext.autodoc')
    app.connect('autodoc-process-docstring', _process_docstring)
    app.connect('autodoc-skip-member', _skip_member)

    #for name, (default, rebuild) in Config._config_values.items():
    #    app.add_config_value(name, default, rebuild)
    return {'version': __version__, 'parallel_read_safe': True}

def _skip_member(app, what, name, obj, skip, options):
    # type: (Sphinx, str, str, Any, bool, Any) -> bool
    """Determine if private and special class members are included in docs.
    The following settings in conf.py determine if private and special class
    members or init methods are included in the generated documentation:
    * ``napoleon_include_init_with_doc`` --
      include init methods if they have docstrings
    * ``napoleon_include_private_with_doc`` --
      include private members if they have docstrings
    * ``napoleon_include_special_with_doc`` --
      include special members if they have docstrings
    Parameters
    ----------
    app : sphinx.application.Sphinx
        Application object representing the Sphinx process
    what : str
        A string specifying the type of the object to which the member
        belongs. Valid values: "module", "class", "exception", "function",
        "method", "attribute".
    name : str
        The name of the member.
    obj : module, class, exception, function, method, or attribute.
        For example, if the member is the __init__ method of class A, then
        `obj` will be `A.__init__`.
    skip : bool
        A boolean indicating if autodoc will skip this member if `_skip_member`
        does not override the decision
    options : sphinx.ext.autodoc.Options
        The options given to the directive: an object with attributes
        inherited_members, undoc_members, show_inheritance and noindex that
        are True if the flag option of same name was given to the auto
        directive.
    Returns
    -------
    bool
        True if the member should be skipped during creation of the docs,
        False if it should be included in the docs.
    """
    if name in ['check_skip_filter',
                'conditions',
                'configs',
                'description',
                'documentation',
                'force',
                'id',
                'is_librebarcode',
                'name',
                'proposal',
                'rationale',
                'severity']:
        return True
    else:
        return None


def _process_docstring(app, what, name, obj, options, lines):
    # type: (Sphinx, str, str, Any, Any, List[str]) -> None
    """Process the docstring for a given python object.
    Called when autodoc has read and processed a docstring. `lines` is a list
    of docstring lines that `_process_docstring` modifies in place to change
    what Sphinx outputs.
    The following settings in conf.py control what styles of docstrings will
    be parsed:
    * ``napoleon_google_docstring`` -- parse Google style docstrings
    * ``napoleon_numpy_docstring`` -- parse NumPy style docstrings
    Parameters
    ----------
    app : sphinx.application.Sphinx
        Application object representing the Sphinx process.
    what : str
        A string specifying the type of the object to which the docstring
        belongs. Valid values: "module", "class", "exception", "function",
        "method", "attribute".
    name : str
        The fully qualified name of the object.
    obj : module, class, exception, function, method, or attribute
        The object to which the docstring belongs.
    options : sphinx.ext.autodoc.Options
        The options given to the directive: an object with attributes
        inherited_members, undoc_members, show_inheritance and noindex that
        are True if the flag option of same name was given to the auto
        directive.
    lines : list of str
        The lines of the docstring, see above.
        .. note:: `lines` is modified *in place*
    """

    if hasattr(obj, 'rationale') and obj.rationale:
        lines.append("**Rationale:**")
        for line in obj.rationale.split('\n'):
            lines.append(line)

    if hasattr(obj, 'proposal') and obj.proposal:
        proposal = obj.proposal
        if not isinstance(obj.proposal, list):
            proposal = [obj.proposal]

        proposals = [p for p in proposal if "legacy:" not in p]
        legacy_name = [p.split('legacy:')[1] for p in proposal if "legacy:" in p]

        if legacy_name:
            lines.append(f"**Legacy check** originally simply called '{legacy_name[0]}'."
                         f" We used to lack richer metadata back in 2015. We're open to"
                         f" further improvements to this description.")
        else:
            if proposals:
                lines.append(f"**Originally proposed at** {proposals.pop(0)}")

        if proposals:
            proposals = ' / '.join(proposals)
            lines.append(f"**Some additional changes** were proposed at {proposals}")



# Lib/fontbakery/__init__.py

try:
    from ._version import version as __version__
except ImportError:
    __version__ = "0.0.0+unknown"


# Lib/fontbakery/__main__.py

import fontbakery.cli

if __name__ == "__main__":
    fontbakery.cli.main()


# Lib/fontbakery/axes_pb2.py

# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler.  DO NOT EDIT!
# source: axes.proto
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import message as _message
from google.protobuf import reflection as _reflection
from google.protobuf import symbol_database as _symbol_database
# @@protoc_insertion_point(imports)

_sym_db = _symbol_database.Default()




DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\naxes.proto\"\xcc\x01\n\tAxisProto\x12\x0b\n\x03tag\x18\x01 \x01(\t\x12\x14\n\x0c\x64isplay_name\x18\x07 \x01(\t\x12\x11\n\tmin_value\x18\x02 \x01(\x02\x12\x15\n\rdefault_value\x18\x03 \x01(\x02\x12\x11\n\tmax_value\x18\x04 \x01(\x02\x12\x11\n\tprecision\x18\x05 \x01(\x05\x12 \n\x08\x66\x61llback\x18\x06 \x03(\x0b\x32\x0e.FallbackProto\x12\x13\n\x0b\x64\x65scription\x18\x08 \x01(\t\x12\x15\n\rfallback_only\x18\t \x01(\x08\"B\n\rFallbackProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x02\x12\x14\n\x0c\x64isplay_name\x18\x03 \x01(\t')



_AXISPROTO = DESCRIPTOR.message_types_by_name['AxisProto']
_FALLBACKPROTO = DESCRIPTOR.message_types_by_name['FallbackProto']
AxisProto = _reflection.GeneratedProtocolMessageType('AxisProto', (_message.Message,), {
  'DESCRIPTOR' : _AXISPROTO,
  '__module__' : 'axes_pb2'
  # @@protoc_insertion_point(class_scope:AxisProto)
  })
_sym_db.RegisterMessage(AxisProto)

FallbackProto = _reflection.GeneratedProtocolMessageType('FallbackProto', (_message.Message,), {
  'DESCRIPTOR' : _FALLBACKPROTO,
  '__module__' : 'axes_pb2'
  # @@protoc_insertion_point(class_scope:FallbackProto)
  })
_sym_db.RegisterMessage(FallbackProto)

if _descriptor._USE_C_DESCRIPTORS == False:

  DESCRIPTOR._options = None
  _AXISPROTO._serialized_start=15
  _AXISPROTO._serialized_end=219
  _FALLBACKPROTO._serialized_start=221
  _FALLBACKPROTO._serialized_end=287
# @@protoc_insertion_point(module_scope)


# Lib/fontbakery/callable.py

"""
Font Bakery callable is the wrapper for your custom check code.


Separation of Concerns Disclaimer:
While created specifically for running checks on fonts and font-families
this module has no domain knowledge about fonts. It can be used for any
kind of (document) checking. Please keep it so. It will be valuable for
other domains as well.
Domain specific knowledge should be encoded only in the Profile (Checks,
Conditions) and MAYBE in *customized* reporters e.g. subclasses.

"""
import inspect

from functools import wraps, update_wrapper

def cached_getter(func):
    """Decorate a property by executing it at instatiation time and cache the
    result on the instance object."""
    @wraps(func)
    def wrapper(self):
        attribute = f'_{func.__name__}'
        value = getattr(self, attribute, None)
        if value is None:
            value = func(self)
            setattr(self, attribute, value)
        return value
    return wrapper

class FontbakeryCallable:
    def __init__(self, func):
        self._args = None
        self._mandatoryArgs = None
        self._optionalArgs = None
        # must be set by sub class

        # this is set by update_wrapper
        # self.__wrapped__ = func

        # https://docs.python.org/2/library/functools.html#functools.update_wrapper
        # Update a wrapper function to look like the wrapped function.
        # ... assigns to the wrapper function’s __name__, __module__ and __doc__
        update_wrapper(self, func)

    def __repr__(self):
        return'<{}:{}>'.format(type(self).__name__,
                               getattr(self, 'id',
                                       getattr(self, 'name',
                                               super().__repr__() ))) # pylint: disable=consider-using-f-string

    @property
    @cached_getter
    def args(self):
        return self.mandatoryArgs + self.optionalArgs

    @property
    @cached_getter
    def mandatoryArgs(self):
        args = list()
        # make follow_wrapped=True explicit, even though it is the default!
        sig = inspect.signature(self, follow_wrapped=True)
        for name, param in sig.parameters.items():
            if param.default is not inspect.Parameter.empty \
               or param.kind not in (inspect.Parameter.POSITIONAL_OR_KEYWORD,
                                     inspect.Parameter.POSITIONAL_ONLY):
                # has a default i.e. not mandatory or not positional of any kind

                ## Debugging message:
                #print(f'[{param}]'
                #      f' {param.default is inspect.Parameter.empty}'
                #      f' param.kind: {param.kind}'
                #      f' param.default: {param.default}'
                #      f' BREAK')
                break
            args.append(name)
        return tuple(args)

    @property
    @cached_getter
    def optionalArgs(self):
        args = list()
        # make follow_wrapped=True explicit, even though it is the default!
        sig = inspect.signature(self, follow_wrapped=True)
        for name, param in sig.parameters.items():
            if param.default is inspect.Parameter.empty:
                # is a mandatory
                continue

            if param.kind not in (inspect.Parameter.POSITIONAL_OR_KEYWORD,
                                  inspect.Parameter.POSITIONAL_ONLY):
                # no more positional of any kind

                ## Debugging message:
                #print(f'[{param}]'
                #      f'{param.default is inspect.Parameter.empty}'
                #      f' param.kind: {param.kind}'
                #      f' param.default: {param.default}'
                #      f' BREAK')
                break
            args.append(name)
        return tuple(args)

    def __call__(self, *args, **kwds):
        """ Each call to __call__ with the same arguments must return
        the same result.
        """
        return self.__wrapped__(*args, **kwds)

    def inject_globals(self, new_globals):
        self.__wrapped__.__globals__.update(new_globals)


def get_doc_desc(func, description, documentation):
    doc = inspect.getdoc(func) or ""

    doclines = doc.split('\n')

    if not description:
        description = []
        while len(doclines) and doclines[0]:
            # consume until first empty line
            description.append(doclines[0])
            doclines = doclines[1:]
        # This removes line breaks
        description = ' '.join(description)

    # remove preceding empty lines
    while len(doclines) and not doclines[0]:
        doclines = doclines[1:]

    if not documentation and len(doclines):
        documentation = '\n'.join(doclines) or None

    return description, documentation

class FontBakeryCondition(FontbakeryCallable):
    def __init__(self,
                 func,
                 # id,
                 name = None, # very short text
                 description = None, # short text
                 documentation=None, # long text, markdown?
                 force=False):
        super().__init__(func)
        # self.id = id
        self.name = func.__name__ if name is None else name
        self.description, self.documentation = get_doc_desc(func,
                                                            description,
                                                            documentation)
        self.force = force

class FontBakeryCheck(FontbakeryCallable):
    def __init__(self,
                 checkfunc,
                 id,
                 description=None, # short text, this is mandatory
                 documentation=None,
                 name=None, # very short text
                 conditions=None,
                 rationale=None, # long text explaining why this check is needed.
                                 # Using markdown, perhaps?
                 proposal=None, # An URL to the original proposal for this check.
                                # This is typically a github issue or pull request.
                 severity=None, # numeric value from 1=min to 10=max, denoting check severity
                 configs=None, # items from config[self.id] to inject into the check's namespace.
                 misc_metadata=None, # Miscelaneous free-form metadata fields
                                     # Some of them may be promoted to 1st-class metadata fields
                                     # if they start being used by the check-runner.
                                     # Below are a few candidates for that:

                 # affects=None, # A list of tuples each indicating Browser/OS/Application
                 #               # and the affected versions range.
                 # example_failures=None, # A reference to some font or family that
                 #                        # originally failed due to the problems
                 #                        # that this check tries to detect and report.
                ):
        """This is the base class for all checks. It will usually
        not be used directly to create check instances, rather
        decorators which are factories will init this class.

        Arguments:
        checkfunc: callable, the check implementation itself.

        id: use reverse domain name notation as a namespace and a
        unique identifier (numbers or anything) but make sure that
        it **never** **ever** changes, that it is **unique until
        eternity**. This is meant to provide a way to track
        burn-down or regressions in a project over time and maybe
        to identify changed/updated check implementations for partial
        profile re-evaluation (in contrast to full profile evaluation)
        if the profile/check changed but not the font.

        description: text, used as one line short description
        read by humans

        name: text, used as a short label read by humans, defaults
        to checkfunc.__name__

        conditions: a list of condition names that must be all true
        in order for this check to be executed. conditions are similar
        to checks, because they also inspect the check subject and they
        also belong to the profile. However, they do not get reported
        directly (there could be checks that report the result of a
        condition). Conditions are registered and referenced by name
        (like "isVariableFont").
        We may accept a python function for combining or negating a condition.
        It receives the condition values as arguments, queried by name
        via inspection, and returns True or False.

        documentation: text used as a detailed documentation to
        be read by humans (in markdown format).

        configs: a list of variable names. Their values are looked up the
        check-specific config (i.e. ``config[self.id]``), and assigned to
        global variables within the check's namespace. e.g. in a check called
        ``example.com/mytest``, setting ``configs = [ "hello" ]`` will create
        a variable called ``hello` and fill it with the value of
        ``config["example.com/mytest"]["hello"]``.
        """
        super().__init__(checkfunc)
        self.id = id
        self.name = checkfunc.__name__ if name is None else name
        self.conditions = conditions or []
        self.rationale = rationale
        self.description, self.documentation = get_doc_desc(checkfunc,
                                                            description,
                                                            documentation)
        self.configs = configs
        self.proposal = proposal
        self.severity = severity
        if not self.description:
            raise TypeError('{} needs a description.'.format(type(self).__name__))

    # This was problematic. See: https://github.com/googlefonts/fontbakery/issues/2194
    # def __str__(self):
    #  return self.id

def condition(*args, **kwds):
    """Check wrapper, a factory for FontBakeryCondition

    Requires all arguments of FontBakeryCondition but not `func`
    which is passed via the decorator syntax.
    """
    if len(args) == 1 and len(kwds) == 0 and callable(args[0]):
        # used as `@decorator`
        func = args[0]
        return FontBakeryCondition(func)
    else:
        # used as `@decorator()` maybe with args
        def wrapper(func):
            return FontBakeryCondition(func, *args, **kwds)
    return wrapper

def check(*args, **kwds):
    """Check wrapper, a factory for FontBakeryCheck

    Requires all arguments of FontBakeryCheck but not `checkfunc`
    which is passed via the decorator syntax.
    """
    def wrapper(checkfunc):
        return FontBakeryCheck(checkfunc, *args, **kwds)
    return wrapper

# ExpectedValue is not a callable, but it belongs next to check and condition
_NOT_SET = object() # used as a marker
class FontBakeryExpectedValue:
    def __init__(self,
                 name, # unique name in global namespace
                 description=None, # short text, this is mandatory
                 documentation=None, # markdown?
                 default=_NOT_SET, # because None can be a valid default
                 validator=None, # function, see the docstring of `def validate`
                 force=False
                ):
        self.name = name
        self.description = description
        self.documentation = documentation
        self._default = (True, default) if default is not _NOT_SET else (False, None)
        self._validator = validator
        self.force = force

    def __repr__(self):
        return'<{}:{}>'.format(type(self).__name__, self.name)

    @property
    def has_default(self):
        return self._default[0]

    @property
    def default(self):
        has_default, value = self._default
        if not has_default:
            raise AttributeError(f'{self} has no default value')
        return value

    def validate(self, value):
        """
          returns (bool valid, string|None message)
          If valid is True, message is None or can be ignored.
          If valid is False, message should be a string describing what
          is wrong with value.
        """
        return self._validator(value) if self._validator else (True, None)


class Disabled:
    def __init__(self, func):
        self.func = func


def disable(func):
    return Disabled(func)


# Lib/fontbakery/checkrunner.py

"""
Font Bakery CheckRunner is the driver of a font bakery suite of checks.


Separation of Concerns Disclaimer:
While created specifically for checking fonts and font-families this
module has no domain knowledge about fonts. It can be used for any kind
of (document) checking. Please keep it so. It will be valuable for other
domains as well.
Domain specific knowledge should be encoded only in the Profile (Checks,
Conditions) and MAYBE in *customized* reporters e.g. subclasses.

"""
import os
import types
from collections import OrderedDict, Counter
import importlib
import inspect
from typing import Dict, Any

from fontbakery.callable import (
    FontbakeryCallable,
    FontBakeryCheck,
    FontBakeryCondition,
    FontBakeryExpectedValue,
)
from fontbakery.message import Message
from fontbakery.profile import Profile, get_module_profile
from fontbakery.utils import is_negated
from fontbakery.errors import (
    APIViolationError,
    CircularDependencyError,
    FailedCheckError,
    FailedConditionError,
    FailedDependenciesError,
    MissingConditionError,
    SetupError,
    MissingValueError,
    CircularAliasError,
    NamespaceError,
    ValueValidationError,
)
from fontbakery.section import Section
from fontbakery.status import (
    Status,
    DEBUG,
    PASS,
    SKIP,
    INFO,
    WARN,
    FAIL,
    ERROR,
    START,
    STARTCHECK,
    ENDCHECK,
    SECTIONSUMMARY,
    END,
)


class CheckRunner:
    def __init__(
        self,
        profile,
        values,
        config,
        values_can_override_profile_names=True,
        use_cache=True,
    ):
        # TODO: transform all iterables that are list like to tuples
        # to make sure that they won't change anymore.
        # Also remove duplicates from list like iterables

        # Add the profile's config values "underneath" the user's config
        self.config = profile.merge_default_config(config)
        self._custom_order = config["custom_order"]
        self._explicit_checks = config["explicit_checks"]
        self._exclude_checks = config["exclude_checks"]
        self._iterargs = OrderedDict()
        for singular, plural in profile.iterargs.items():
            if plural in values:
                values[plural] = tuple(values[plural])
                self._iterargs[singular] = len(values[plural])

        if not values_can_override_profile_names:
            for name in values:
                if profile.has(name) and profile.get_type(name) != "expected_values":
                    # Of course values can override
                    # expected_values, that's their purpose!
                    raise SetupError(
                        f'Values entry "{name}" collides with profile'
                        f" namespace as a {profile.get_type(name)}"
                    )

        self._profile = profile
        self._profile.test_dependencies()
        valid, message = self._profile.validate_values(values)
        if not valid:
            raise ValueValidationError(
                f"Validation of expected values failed:\n" f"{message}"
            )
        self._values = values

        self.use_cache = use_cache
        self._cache = {"conditions": {}, "order": None}

    def clearCache(self):
        # no need to clear 'order' cache IMO
        self._cache["conditions"] = {}

    @property
    def iterargs(self):
        """ uses the singular name as key """
        iterargs = OrderedDict()
        for name in self._iterargs:
            plural = self._profile.iterargs[name]
            iterargs[name] = tuple(self._values[plural])
        return iterargs

    @property
    def profile(self):
        return self._profile

    def _check_result(self, result):
        """Check that the check returned a well formed result:
        A tuple (<Status>, message)

        A boolean Status is allowd and will be transformed to:
        True <Status: PASS>, False <Status: FAIL>

        Checks will be implemented by other parties. This is to
        help implementors creating good checks, to spot erroneous
        implementations early and to make it easier to handle
        the results tuple.
        """
        if not isinstance(result, tuple):
            msg = f"Result must be a tuple but it is {type(result)}."
            return (FAIL, APIViolationError(msg, result))

        if len(result) != 2:
            msg = f"Result must have 2 items, but it has {len(result)}."
            return (FAIL, APIViolationError(msg, result))

        status, message = result
        # Allow booleans, but there's no way to issue a WARNING
        if isinstance(status, bool):
            # normalize
            status = PASS if status else FAIL
            result = (status, message)

        if not isinstance(status, Status):
            msg = (
                f"Result item `status` must be an instance of Status,"
                f" but it is {status} and its type is {type(status)}."
            )
            return (FAIL, APIViolationError(msg, result))

        return result

    def _exec_check(self, check: FontbakeryCallable, args: Dict[str, Any]):
        """Yields check sub results.

        Each check result is a tuple of: (<Status>, mixed message)
        `status`: must be an instance of Status.
              If one of the `status` entries in one of the results
              is FAIL, the whole check is considered failed.
              WARN is most likely a PASS in a non strict mode and a
              FAIL in a strict mode.
        `message`:
          * If it is an `Exception` type we expect `status`
            not to be PASS
          * If it is a `string` it's a description of what passed
            or failed.
          * we'll think of an AdvancedMessageType as well, so that
            we can connect the check result with more in depth
            knowledge from the check definition.
        """
        if check.configs:
            new_globals = {
                varname: self.config.get(check.id, {}).get(varname)
                for varname in check.configs
            }
            check.inject_globals(new_globals)
        try:
            # A check can be either a normal function that returns one Status or a
            # generator that yields one or more. The latter will return a generator
            # object that we can detect with types.GeneratorType.
            result = check(**args)  # Might raise.

            if isinstance(result, types.GeneratorType):
                # Iterate over sub-results one-by-one, list(result) would abort on
                # encountering the first exception.
                for sub_result in result:  # Might raise.
                    yield self._override_status(self._check_result(sub_result), check)
                return  # Do not fall through to rest of method.
        except Exception as e:
            error = FailedCheckError(e)
            result = (ERROR, error)

        yield self._override_status(self._check_result(result), check)

    def _evaluate_condition(self, name, iterargs, path=None):
        if path is None:
            # top level call
            path = []
        if name in path:
            dependencies = " -> ".join(path)
            msg = f'Condition "{name}" is' f" a circular dependency in {dependencies}"
            raise CircularDependencyError(msg)

        path.append(name)

        try:
            condition = self._profile.conditions[name]
        except KeyError as err:
            error = MissingConditionError(name, err)
            return error, None

        try:
            args = self._get_args(condition, iterargs, path)
        except Exception as err:
            error = FailedConditionError(condition, err)
            return error, None

        path.pop()
        try:
            return None, condition(**args)
        except Exception as err:
            error = FailedConditionError(condition, err)
            return error, None

    def _filter_condition_used_iterargs(self, name, iterargs):
        allArgs = set()
        names = list(self._profile.conditions[name].args)
        while names:
            name = names.pop()
            if name in allArgs:
                continue
            allArgs.add(name)
            if name in self._profile.conditions:
                names += self._profile.conditions[name].args
        return tuple((name, value) for name, value in iterargs if name in allArgs)

    def _get_condition(self, name, iterargs, path=None):
        # conditions are evaluated lazily
        used_iterargs = self._filter_condition_used_iterargs(name, iterargs)
        key = (name, used_iterargs)
        if not self.use_cache or key not in self._cache["conditions"]:
            err, val = self._evaluate_condition(name, used_iterargs, path)
            if self.use_cache:
                self._cache["conditions"][key] = err, val
        else:
            err, val = self._cache["conditions"][key]
        return err, val

    def get(self, key, iterargs, *args):
        return self._get(key, iterargs, None, *args)

    def get_iterarg(self, name, index):
        """ Used by e.g. reporters """
        plural = self._profile.iterargs[name]
        return self._values[plural][index]

    def _generate_iterargs(self, requirements):
        if not requirements:
            yield tuple()
            return
        name, length = requirements[0]
        for index in range(length):
            current = (name, index)
            for tail in self._generate_iterargs(requirements[1:]):
                yield (current,) + tail

    def _derive_iterable_condition(self, name, simple=False, path=None):
        # returns a generator, which is better for memory critical situations
        # than a list containing all results of the used conditions
        condition = self._profile.conditions[name]
        iterargs = self._profile.get_iterargs(condition)

        if not iterargs:
            # without this, we would return just an empty tuple
            raise TypeError(f'Condition "{name}" uses no iterargs.')

        # like [('font', 10), ('other', 22)]
        requirements = [(singular, self._iterargs[singular]) for singular in iterargs]
        for iterargs in self._generate_iterargs(requirements):
            error, value = self._get_condition(name, iterargs, path)
            if error:
                raise error
            if simple:
                yield value
            else:
                yield (iterargs, value)

    def _get(self, name, iterargs, path, *args):
        iterargsDict = dict(iterargs)
        has_fallback = bool(len(args))
        if has_fallback:
            fallback = args[0]

        # try this once before resolving aliases and once after
        if name in self._values:
            return self._values[name]

        original_name = name
        name = self._profile.resolve_alias(name)
        if name != original_name and name in self._values:
            return self._values[name]

        nametype = self._profile.get_type(name, None)

        if name in self._values:
            return self._values[name]

        if nametype == "expected_values":
            # No need to validate
            expected_value = self._profile.get(name)
            if expected_value.has_default:
                # has no default: fallback or MissingValueError
                return expected_value.default

        if nametype == "iterargs" and name in iterargsDict:
            index = iterargsDict[name]
            plural = self._profile.get(name)
            return self._values[plural][index]

        if nametype == "conditions":
            error, value = self._get_condition(name, iterargs, path)
            if error:
                raise error
            return value

        if nametype == "derived_iterables":
            condition_name, simple = self._profile.get(name)
            return self._derive_iterable_condition(condition_name, simple, path)

        if nametype == "config":
            return self.config

        if has_fallback:
            return fallback

        if original_name != name:
            report_name = f'"{original_name}" as "{name}"'
        else:
            report_name = f'"{name}"'
        raise MissingValueError(f"Value {report_name} is undefined.")

    def _get_args(self, item, iterargs, path=None):
        # iterargs can't be optional arguments yet, we wouldn't generate
        # an execution with an empty list. I don't know if that would be even
        # feasible, so I don't add this complication for the sake of clarity.
        # If this is needed for anything useful, we'll have to figure this out.
        args = {}
        for name in item.args:
            if name in args:
                continue
            try:
                args[name] = self._get(name, iterargs, path)
            except MissingValueError:
                if name not in item.optionalArgs:
                    raise
        return args

    def _get_check_dependencies(self, check, iterargs):
        unfulfilled_conditions = []
        for condition in check.conditions:
            negate, name = is_negated(condition)
            if name in self._values:
                # this is a handy way to set flags from the outside
                err, val = None, self._values[name]
            else:
                err, val = self._get_condition(name, iterargs)
            if negate:
                val = not val
            if err:
                status = (ERROR, err)
                return (status, None)

            # An annoying FutureWarning here (Python 3.8.3) on stderr:
            #     "FutureWarning: The behavior of this method will change in future
            #      versions. Use specific 'len(elem)' or 'elem is not None' test instead."
            # Actually not shure how to tackle this, since val is very unspecific
            # here intentionally. Where is the documentation for the change?
            if val is None:
                bool_val = False
            else:
                try:
                    _len = len(val)
                    bool_val = not _len == 0
                except TypeError:
                    # TypeError: object of type 'bool' has no len()
                    bool_val = bool(val)

            if not bool_val:
                unfulfilled_conditions.append(condition)
        if unfulfilled_conditions:
            # This will make the check neither pass nor fail
            comma_separated = ", ".join(unfulfilled_conditions)
            status = (SKIP, f"Unfulfilled Conditions: {comma_separated}")
            return (status, None)

        try:
            args = self._get_args(check, iterargs)
            # Run the generators now, so we can test if they're empty
            for k,v in args.items():
                if inspect.isgenerator(v) or inspect.isgeneratorfunction(v):
                    args[k] = list(v)

            if all(not x for x in args.values()):
                status = (SKIP, "No applicable arguments")
                return (status, None)
            return None, args
        except Exception as error:
            status = (ERROR, FailedDependenciesError(check, error))
            return (status, None)

    def _run_check(self, check, iterargs):
        summary_status = None
        # A check is more than just a function, it carries
        # a lot of meta-data for us, in this case we can use
        # meta-data to learn how to call the check (via
        # configuration or inspection, where inspection would be
        # the default and configuration could be used to override
        # inspection results).

        skipped = None
        if self._profile.check_skip_filter:
            iterargsDict = {
                key: self.get_iterarg(key, index) for key, index in iterargs
            }
            accepted, message = self._profile.check_skip_filter(
                check.id, **iterargsDict
            )
            if not accepted:
                skipped = (SKIP, "Filtered: {}".format(message or "(no message)"))

        if not skipped:
            skipped, args = self._get_check_dependencies(check, iterargs)

        # FIXME: check is not a message
        # so, to use it as a message, it should have a "message-interface"
        # TODO: describe generic "message-interface"
        yield STARTCHECK, None
        if skipped is not None:
            summary_status = skipped[0]
            # `skipped` is a normal result tuple (status, message)
            # where `status` is either FAIL for unmet dependencies
            # or SKIP for unmet conditions or ERROR. A status of SKIP is
            # never a failed check.
            # ERROR is either a missing dependency or a condition that raised
            # an exception. This shouldn't happen when everyting is set up
            # correctly.
            yield skipped
        else:
            for sub_result in self._exec_check(check, args):
                status, _ = sub_result
                if summary_status is None or status >= summary_status:
                    summary_status = status
                yield sub_result
            # The only reason to yield this is to make it testable
            # that a check ran to its end, or, if we start to allow
            # nestable subchecks. Otherwise, a STARTCHECK would end the
            # previous check implicitly.
            # We can also use it to display status updates to the user.
        if summary_status is None:
            summary_status = ERROR
            yield ERROR, (f"The check {check} did not yield any status")
        elif summary_status < PASS:
            summary_status = ERROR
            # got to yield it,so we can see it in the report
            yield ERROR, (
                f"The most significant status of {check}"
                f" was only {summary_status} but"
                f" the minimum is {PASS}"
            )

        yield ENDCHECK, summary_status

    @property
    def order(self):
        order = self._cache.get("order", None)
        if order is None:
            order = []
            # section, check, iterargs = identity
            for identity in self._profile.execution_order(
                self._iterargs,
                custom_order=self._custom_order,
                explicit_checks=self._explicit_checks,
                exclude_checks=self._exclude_checks,
            ):
                order.append(identity)
            self._cache["order"] = order = tuple(order)
        return order

    def check_order(self, order):
        """
        order must be a subset of self.order
        """
        own_order = self.order
        for item in order:
            if item not in own_order:
                raise ValueError(f"Order item {item} not found.")
        return order

    def _check_protocol_generator(self, next_check_identity):
        section, check, iterargs = next_check_identity
        for status, message in self._run_check(check, iterargs):
            yield status, message, (section, check, iterargs)

    def session_protocol_generator(self, order=None):
        order = order if order is not None else self.order
        yield from session_protocol_generator(self._check_protocol_generator, order)

    def run(self, order=None):
        order = order if order is not None else self.order
        session_gen = self.session_protocol_generator(order)
        next_check_gen = iter(order)
        yield from drive_session_protocol(session_gen, next_check_gen)

    def run_externally_controlled(self, receive_result_fn, next_check_gen, order=None):
        order = order if order is not None else self.order
        session_gen = self.session_protocol_generator(order)
        for result in drive_session_protocol(session_gen, next_check_gen):
            receive_result_fn(result)

    def _override_status(self, result, check):
        # Potentially override the status based on the config file.
        # Replaces the status with config["overrides"][check.id][message.code]
        status, message = result
        status_overrides = self.config.get("overrides", {}).get(check.id)
        if (
            not status_overrides
            or not isinstance(message, Message)
            or message.code not in status_overrides
        ):
            return result
        return Status(status_overrides[message.code]), message


def drive_session_protocol(session_gen, next_check_gen):
    # can't send anything but None on first iteration
    value = None
    try:
        while True:
            result = session_gen.send(value)
            yield result
            value = None
            if result[0] in (START, ENDCHECK):
                # get the next check, if None protocol will wrap up
                value = next(next_check_gen, None)
    except StopIteration:
        pass


def session_protocol_generator(check_protocol_generator, order):
    # init

    # Could use self.order, but the truth is, we don't know.
    # However, self.order still should contain each check_identity
    # just to make sure we can actually run the check!
    # Let's just assume that _run_check will fail otherwise...
    sections = OrderedDict()
    next_check_identity = yield START, order, (None, None, None)
    while next_check_identity:

        for event in check_protocol_generator(next_check_identity):
            # send(check_identity) always after ENDCHECK
            next_check_identity = yield event
        # after _run_check the last status must be ENDCHECK
        status, message, (section, check, iterargs) = event
        event = None
        assert status == ENDCHECK

        # message is the summary_status of the check when status is ENDCHECK
        section_key = str(section)
        if section_key not in sections:
            sections[section_key] = ([], Counter(), section)
        section_order, section_summary, _ = sections[section_key]
        section_order.append((check, iterargs))
        # message is the summary_status of the check when status is ENDCHECK
        section_summary[message.name] += 1

    checkrun_summary = Counter()
    for _, (section_order, section_summary, section) in sections.items():
        yield SECTIONSUMMARY, (section_order, section_summary), (section, None, None)
        checkrun_summary.update(section_summary)
    yield END, checkrun_summary, (None, None, None)


def distribute_generator(gen, targets_callbacks):
    for item in gen:
        for target in targets_callbacks:
            target(item)


FILE_MODULE_NAME_PREFIX = "."


def get_module_from_file(filename):
    # filename = 'my/path/to/file.py'
    # module_name = 'file_module.file_py'
    module_name = f"{FILE_MODULE_NAME_PREFIX}{format(os.path.basename(filename).replace('.', '_'))}"
    module_spec = importlib.util.spec_from_file_location(module_name, filename)
    module = importlib.util.module_from_spec(module_spec)
    module_spec.loader.exec_module(module)
    # assert module.__file__ == filename
    return module


def _get_module_from_locator(module_locator):
    if module_locator["name"].startswith(FILE_MODULE_NAME_PREFIX):
        return get_module_from_file(module_locator["origin"])
    # Fails with an appropriate ImportError.
    return importlib.import_module(module_locator["name"], package=None)


def get_profile_from_module_locator(module_locator):
    module = _get_module_from_locator(module_locator)
    return get_module_profile(module)


# Lib/fontbakery/cli.py

import argparse
import pkgutil
import runpy
import signal
import sys
from importlib import import_module

import fontbakery.commands
from fontbakery.commands.check_profile import main as check_profile_main

CLI_PROFILES = [
    "adobefonts",
    "fontval",
    "fontwerk",
    "googlefonts",
    "iso15008",
    "notofonts",
    "opentype",
    "typenetwork",
    "ufo_sources",
    "universal",
]


def run_profile_check(profilename):
    module = import_module(f"fontbakery.profiles.{profilename}")
    sys.exit(check_profile_main(module.profile))


def signal_handler(sig, frame):
    print('\nCancelled by user')
    sys.exit(-1)


def main():
    signal.signal(signal.SIGINT, signal_handler)

    subcommands = [
        pkg[1] for pkg in pkgutil.walk_packages(fontbakery.commands.__path__)
    ] + ["check_" + prof for prof in CLI_PROFILES]

    subcommands = [command.replace("_", "-") for command in sorted(subcommands)]

    if len(sys.argv) >= 2 and sys.argv[1] in subcommands:
        # Relay to subcommand.
        subcommand = sys.argv[1]
        subcommand_module = subcommand.replace("-", "_")
        sys.argv[0] += " " + subcommand
        del sys.argv[1]  # Make this indirection less visible for subcommands.
        if (
            subcommand_module.startswith("check_")
            and subcommand_module[6:] in CLI_PROFILES
        ):
            run_profile_check(subcommand_module[6:])
        else:
            runpy.run_module(
                "fontbakery.commands." + subcommand_module, run_name="__main__"
            )
    else:
        description = (
            "Run fontbakery subcommands. Subcommands have their own help "
            "messages. These are usually accessible with the -h/--help flag "
            "positioned after the subcommand, i.e.: fontbakery subcommand -h")

        parser = argparse.ArgumentParser(description=description)
        parser.add_argument(
            'subcommand',
            help="The subcommand to execute",
            nargs="?",
            choices=subcommands)
        parser.add_argument(
            '--list-subcommands',
            action='store_true',
            help='print the list of subcommands '
            'to stdout, separated by a space character. This is '
            'usually only used to generate the shell completion code.')
        parser.add_argument(
            '--version',
            action='version',
            version=fontbakery.__version__)
        args = parser.parse_args()

        if args.list_subcommands:
            print(' '.join(subcommands))
        else:
            parser.print_help()


# Lib/fontbakery/codetesting.py

#!/usr/bin/env python3
# Copyright 2020 The Fontbakery Authors
#
# 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.
#

from fontbakery.checkrunner import CheckRunner
from fontbakery.configuration import Configuration
from fontbakery.profile import Profile, get_module_profile
import defcon


class CheckTester:
    """
    This class offers a bit of automation to aid in the implementation of
    code-tests to validade the proper behaviour of FontBakery checks.

    !!!CAUTION: this uses a lot of "private" methods and properties
    of CheckRunner, in order to make unit testing different cases simpler.

    This is not intended to run in production. However, if that is desired
    we may or may not find inspiration here on how to implement a proper
    public CheckRunner API.

    Not built for performance either!

    The idea is that we can let this class take care of computing
    the dependencies of a check for us. And we can also optionaly "fake"
    some of them in order to create useful testing scenarios for the checks.

    An initial run can be with unaltered arguments, as CheckRunner would
    produce them by itself. And subsequent calls can reuse some of them.
    """
    def __init__(self, module_or_profile, check_id):
        self.profile = module_or_profile \
                       if isinstance(module_or_profile, Profile) \
                       else get_module_profile(module_or_profile)
        self.check_id = check_id
        self.check_identity = None
        self.check_section = None
        self.check = None
        self.check_iterargs = None
        self._args = None

    def _get_args(self, condition_overrides=None):
        if condition_overrides is not None:
            for name_key, value in condition_overrides.items():
                if isinstance(name_key, str):
                    # this is a simplified form of a cache key:
                    # write the conditions directly to the iterargs of the check identity
                    used_iterargs = self.runner._filter_condition_used_iterargs(name_key, self.check_iterargs)
                    key = (name_key, used_iterargs)
                else:
                    # Full control for the caller, who has to inspect how
                    # the desired key needs to be set up.
                    key = name_key
                #                                      error, value
                self.runner._cache['conditions'][key] = None, value
        args = self.runner._get_args(self.check, self.check_iterargs)
        # args that are derived iterables are generators that must be
        # converted to lists, otherwise we end up with exhausted
        # generators after their first consumption.
        for k in args.keys():
            if self.profile.get_type(k, None) == 'derived_iterables':
                args[k] = list(args[k])
        return args

    def __getitem__(self, key):
        if key in self._args:
            return self._args[key]

        used_iterargs = self.runner._filter_condition_used_iterargs(key, self.check_iterargs)
        key = (key, used_iterargs)
        if key in self.runner._cache['conditions']:
            return self.runner._cache['conditions'][key][1]

    def __call__(self, values, condition_overrides={}):
        from fontTools.ttLib import TTFont
        from fontbakery.profiles.googlefonts_conditions import family_metadata
        from glyphsLib import GSFont
        import os

        if isinstance(values, str):
            if values.endswith('README.md'):
                values = {'readme_md': values}
            elif values.endswith('.ufo'):
                values = {'ufo': values}
            elif values.endswith('.designspace'):
                values = {'designspace': values}
            elif values.endswith('METADATA.pb'):
                fonts = [os.path.join(os.path.dirname(values), f.filename)
                         for f in family_metadata(values).fonts]
                values = {'metadata_pb': values,
                          'font': fonts[0], #FIXME!
                          'fonts': fonts}
            else:
                values = {'font': values,
                          'fonts': [values]}
        elif isinstance(values, TTFont):
            values = {'font': values.reader.file.name,
                      'fonts': [values.reader.file.name],
                      'ttFont': values,
                      'ttFonts': [values]}
        elif isinstance(values, GSFont):
            values = {'glyphs_file': values.filepath,
                      'glyphs_files': [values.filepath],
                      'glyphsFile': values,
                      'glyphsFiles': [values]}
        elif isinstance(values, defcon.Font):
            values = {'ufo_font': values}
        elif isinstance(values, list):
            if values:
                if isinstance(values[0], str):
                    values = {'fonts': values}
                elif isinstance(values[0], TTFont):
                    values = {'fonts': [v.reader.file.name for v in values],
                              'ttFonts': values}
                elif isinstance(values[0], GSFont):
                    values = {'glyphs_files': [v.filepath for v in values],
                              'glyphsFiles': values}
            else:
                values = {}

        self.runner = CheckRunner(self.profile,
                                  values,
                                  Configuration(explicit_checks=[self.check_id], full_lists=True))
        for check_identity in self.runner.order:
            _, check, _ = check_identity
            if check.id != self.check_id:
                continue
            self.check_identity = check_identity
            self.check_section, self.check, self.check_iterargs = check_identity
            break
        if self.check_identity is None:
            raise KeyError(f'Check with id "{self.check_id}" not found.')

        self._args = self._get_args(condition_overrides)
        return list(self.runner._exec_check(self.check, self._args))


def portable_path(p):
    import os
    return os.path.join(*p.split('/'))


def TEST_FILE(f):
    return portable_path("data/test/" + f)


def GLYPHSAPP_TEST_FILE(f):
    import glyphsLib
    the_file = portable_path("data/test/glyphs_files/" + f)
    return glyphsLib.load(open(the_file))


def assert_PASS(check_results, reason="with a good font...", ignore_error=None):
    from fontbakery.checkrunner import PASS, ERROR
    print(f"Test PASS {reason}")
    status, message = list(check_results)[-1]
    if ignore_error and status == ERROR:
        print(ignore_error)
        return None
    else:
        assert status == PASS
        return str(message)


def assert_SKIP(check_results, reason=""):
    from fontbakery.checkrunner import SKIP
    print(f"Test SKIP {reason}")
    status, message = list(check_results)[-1]
    assert status == SKIP
    return str(message)


def assert_results_contain(check_results,
                           expected_status,
                           expected_msgcode=None,
                           reason=None,
                           ignore_error=None):
    """
    This helper function is useful when we want to make sure that
    a certain log message is emited by a check but it can be in any
    order among other log messages.
    """
    from fontbakery.message import Message
    from fontbakery.checkrunner import PASS, DEBUG, ERROR
    if not reason:
        reason = f"[{expected_msgcode}]"
    if not expected_msgcode:
        raise Exception(
            "Test must provide the expected message code")

    print(f"Test {expected_status} {reason}")
    check_results = list(check_results)

    for status, _ in check_results:
        if ignore_error and status == ERROR:
            print(ignore_error)
            return None

    for status, msg in check_results:
        if status not in [PASS, DEBUG] and not isinstance(msg, Message):
            raise Exception(
                f"Bare Python strings are no longer supported in result values.\n"
                f"Please use the Message class to wrap strings and to give them"
                f" a keyword useful for identifying them (in bug reports as well as"
                f" in the implementation of reliable unit tests).\n"
                f"(Bare string: {msg!r})")

        if status == expected_status and (
            (isinstance(msg, str) and msg == expected_msgcode)
            or (isinstance(msg, Message) and msg.code == expected_msgcode)
        ):
            if isinstance(msg, Message):
                return msg.message
            else:
                return msg  # It is probably a plain python string

    # if no match was found
    raise Exception(f"Expected to find {expected_status}, [code: {expected_msgcode}]\n"
                    f"But did not find it in:\n"
                    f"{check_results}")


# Lib/fontbakery/configuration.py

import toml
import yaml


class Configuration(dict):
    def __init__(self, **kwargs):
        super().__init__(kwargs)
        for required_arg in ["custom_order",
                             "explicit_checks",
                             "exclude_checks",
                             "full_lists"]:
            if required_arg not in self:
                self[required_arg] = None

    @classmethod
    def from_config_file(cls, filename):
        try:
            config = toml.load(filename)
        except toml.TomlDecodeError:
            # Try yaml
            config = yaml.safe_load(open(filename))
        if not isinstance(config, dict):
            raise Exception(f"Can't understand config file {filename}.")
        return cls(**config)

    def maybe_override(self, other):
        for key, value in other.items():
            if value is not None:
                self[key] = value



# Lib/fontbakery/constants.py

#!/usr/bin/env python3
# Copyright 2016 The Fontbakery Authors
#
# 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 enum

# =====================================
# GLOBAL CONSTANTS DEFINITIONS

# These variable font naming rules will soon change.
# For more detail, see:
# https://github.com/googlefonts/fontbakery/issues/2396#issuecomment-473250089
VARFONT_SUFFIXES = [
    "VF",
    "Italic-VF",
    "Roman-VF"]

STATIC_STYLE_NAMES = [
    "Thin",
    "ExtraLight",
    "Light",
    "Regular",
    "Medium",
    "SemiBold",
    "Bold",
    "ExtraBold",
    "Black",
    "Thin Italic",
    "ExtraLight Italic",
    "Light Italic",
    "Italic",
    "Medium Italic",
    "SemiBold Italic",
    "Bold Italic",
    "ExtraBold Italic",
    "Black Italic"]

RIBBI_STYLE_NAMES = [
    "Regular",
    "Italic",
    "Bold",
    "BoldItalic",
    "Bold Italic"]  # <-- Do we really need this one?

PLACEHOLDER_LICENSING_TEXT = {
    'UFL.txt': 'Licensed under the Ubuntu Font Licence 1.0.',
    'OFL.txt': 'This Font Software is licensed under the SIL Open Font '
               'License, Version 1.1. This license is available with a FAQ '
               'at: https://scripts.sil.org/OFL',
    'LICENSE.txt': 'Licensed under the Apache License, Version 2.0'
}

SHOW_GF_DOCS_MSG = (
    "Further info can be found in our spec "
    "https://github.com/googlefonts/gf-docs/tree/main/Spec"
)

# ANSI color codes for the helper logging class:
def color(bg, fg, bold=False):
    bold_bit = 0
    if bold:
        bold_bit = 1
    return ('\033[{};{};{}m'.format(bold_bit, bg, fg+10) + '{}\033[0m').format

def no_color(s):
    return s

BLACK = 30
RED = 31
GREEN = 32
YELLOW = 33
BLUE = 34
MAGENTA = 35
CYAN = 36
WHITE = 37
BRIGHT_BLACK = 90
BRIGHT_RED = 91
BRIGHT_GREEN = 92
BRIGHT_YELLOW = 93
BRIGHT_BLUE = 94
BRIGHT_MAGENTA = 95
BRIGHT_CYAN = 96
BRIGHT_WHITE = 97

NO_COLORS_THEME = {
                     "header": no_color,
                        "url": no_color,
                   "check-id": no_color,
                "description": no_color,
            "rationale-title": no_color,
             "rationale-text": no_color,
                       "INFO": no_color,
                       "WARN": no_color,
                      "ERROR": no_color,
                       "SKIP": no_color,
                       "PASS": no_color,
                       "FAIL": no_color,
                    "cupcake": no_color,
                    "spinner": no_color,
       "list-checks: section": no_color,
      "list-checks: check-id": no_color,
    "list-checks: description": no_color
}

DARK_THEME = {                  #     Foreground    Background
                      "header": color(WHITE,        BLACK,        bold=True),
                         "url": color(CYAN,         BLACK,        bold=True),
                    "check-id": color(CYAN,         BLACK,        bold=True),
                 "description": color(MAGENTA,      BLACK),
             "rationale-title": color(BRIGHT_CYAN,  BRIGHT_BLACK, bold=True),
              "rationale-text": color(WHITE,        BLACK),
                        "INFO": color(CYAN,         BLACK),
                        "WARN": color(YELLOW,       BLACK),
                       "ERROR": color(BRIGHT_WHITE, RED),
                        "SKIP": color(BLUE,         BLACK),
                        "PASS": color(GREEN,        BLACK),
                        "FAIL": color(RED,          BLACK),
                     "cupcake": color(MAGENTA,      BLACK),
                     "spinner": color(GREEN,        BLACK),
        "list-checks: section": color(WHITE,        BLACK),
       "list-checks: check-id": color(CYAN,         BLACK),
    "list-checks: description": color(BLUE,         BLACK)
}

LIGHT_THEME = {                 #     Foreground     Background
                      "header": color(BLACK,         BRIGHT_WHITE,  bold=True),
                         "url": color(CYAN,          BRIGHT_WHITE,  bold=True),
                    "check-id": color(MAGENTA,       BRIGHT_WHITE,  bold=True),
                 "description": color(CYAN,          BRIGHT_WHITE),
             "rationale-title": color(MAGENTA,       BRIGHT_WHITE,  bold=True),
              "rationale-text": color(BLACK,         BRIGHT_WHITE),
                        "INFO": color(CYAN,          BRIGHT_WHITE),
                        "WARN": color(BLACK,         BRIGHT_YELLOW, bold=True),
                       "ERROR": color(BRIGHT_WHITE,  BRIGHT_RED,    bold=True),
                        "SKIP": color(BLUE,          BRIGHT_WHITE),
                        "PASS": color(GREEN,         BRIGHT_WHITE),
                        "FAIL": color(BRIGHT_RED,    BRIGHT_WHITE,  bold=True),
                     "cupcake": color(MAGENTA,       BRIGHT_WHITE),
                     "spinner": color(GREEN,         BRIGHT_WHITE),
        "list-checks: section": color(WHITE,         BRIGHT_WHITE,  bold=True),
       "list-checks: check-id": color(CYAN,          BRIGHT_WHITE,  bold=True),
    "list-checks: description": color(BLUE,          BRIGHT_WHITE)
}


class NameID(enum.IntEnum):
    """ nameID definitions for the name table """
    COPYRIGHT_NOTICE = 0
    FONT_FAMILY_NAME = 1
    FONT_SUBFAMILY_NAME = 2
    UNIQUE_FONT_IDENTIFIER = 3
    FULL_FONT_NAME = 4
    VERSION_STRING = 5
    POSTSCRIPT_NAME = 6
    TRADEMARK = 7
    MANUFACTURER_NAME = 8
    DESIGNER = 9
    DESCRIPTION = 10
    VENDOR_URL = 11
    DESIGNER_URL = 12
    LICENSE_DESCRIPTION = 13
    LICENSE_INFO_URL = 14
    # Name ID 15 is RESERVED
    TYPOGRAPHIC_FAMILY_NAME = 16
    TYPOGRAPHIC_SUBFAMILY_NAME = 17
    COMPATIBLE_FULL_MACONLY = 18
    SAMPLE_TEXT = 19
    POSTSCRIPT_CID_NAME = 20
    WWS_FAMILY_NAME = 21
    WWS_SUBFAMILY_NAME = 22
    LIGHT_BACKGROUND_PALETTE = 23
    DARK_BACKGROUD_PALETTE = 24

class GlyphClass(enum.IntEnum):
    BASE = 1
    LIGATURE = 2
    MARK = 3
    COMPONENT = 4

class FsSelection(enum.IntEnum):
    ITALIC         = (1 << 0)
    UNDERSCORE     = (1 << 1)
    NEGATIVE       = (1 << 2)
    OUTLINED       = (1 << 3)
    STRIKEOUT      = (1 << 4)
    BOLD           = (1 << 5)
    REGULAR        = (1 << 6)
    USETYPOMETRICS = (1 << 7)
    WWS            = (1 << 8)
    OBLIQUE        = (1 << 9)

class MacStyle(enum.IntEnum):
    BOLD   = (1 << 0)
    ITALIC = (1 << 1)

class PANOSE_Family_Type(enum.IntEnum):
    ANY = 0
    NO_FIT = 1
    LATIN_TEXT = 2
    LATIN_HAND_WRITTEN = 3
    LATIN_DECORATIVE = 4
    LATIN_SYMBOL = 5 # aka LATIN_PICTURE

class PANOSE_Proportion(enum.IntEnum):
    ANY = 0
    NO_FIT = 1
    OLD_STYLE = 2
    MODERN = 3
    EVEN_WIDTH = 4
    EXTENDED = 5
    CONDENSED = 6
    VERY_EXTENDED = 7
    VERY_CONDENSED = 8
    MONOSPACED = 9

class PANOSE_Spacing(enum.IntEnum):
    ANY = 0
    NO_FIT = 1
    PROPORTIONAL = 2
    MONOSPACED = 3

class IsFixedWidth(enum.IntEnum):
    """ 'post' table / isFixedWidth definitions """
    NOT_MONOSPACED = 0
    # Do NOT use `MONOSPACED = 1` because *any* non-zero value means monospaced.
    # I've commented it out because we were incorrectly testing against it. - CJC

class PlatformID(enum.IntEnum):
    UNICODE = 0
    MACINTOSH = 1
    ISO = 2
    WINDOWS = 3
    CUSTOM = 4

class UnicodeEncodingID(enum.IntEnum):
    """ Unicode platform-specific encoding IDs
        (when platID == 0)
    """
    UNICODE_1_0 = 0
    UNICODE_1_1 = 1
    ISO_IEC_10646 = 2
    UNICODE_2_0_BMP_ONLY = 3 # Basic Multilingual Plane
    UNICODE_2_0_FULL = 4
    UNICODE_VARIATION_SEQUENCES = 5
    UNICODE_FULL = 6

class MacintoshEncodingID(enum.IntEnum):
    """ Encoding IDs defined for use
        with the Macintosh platform
        (when platID = 1)
    """
    ROMAN = 0
    JAPANESE = 1
    CHINESE_TRADITIONAL = 2
    KOREAN = 3
    ARABIC = 4
    HEBREW = 5
    GREEK = 6
    RUSSIAN = 7
    RSYMBOL = 8
    DEVANAGARI = 9
    GURMUKHI = 10
    GUJARATI = 11
    ORIYA = 12
    BENGALI = 13
    TAMIL = 14
    TELUGU = 15
    KANNADA = 16
    MALAYALAM = 17
    SINHALESE = 18
    BURMESE = 19
    KHMER = 20
    THAI = 21
    LAOTIAN = 22
    GEORGIAN = 23
    ARMENIAN = 24
    CHINESE_SIMPLIFIED = 25
    TIBETAN = 26
    MONGOLIAN = 27
    GEEZ = 28
    SLAVIC = 29
    VIETNAMESE = 30
    SINDHI = 31
    UNINTERPRETED = 32

class WindowsEncodingID(enum.IntEnum):
    """ Windows platform-specific encoding IDs
        (when platID == 3)
    """
    SYMBOL = 0
    UNICODE_BMP = 1 # Basic Multilingual Plane
    SHIFTJIS = 2
    PRC = 3
    BIG5 = 4
    WANSUNG = 5
    JOHAB = 6
    # IDs 7, 8 and 9 are reserved.
    UNICODE_FULL_REPERTOIRE = 10

class MacintoshLanguageID(enum.IntEnum):
    """ Platform-specific Language IDs
        assigned by Apple
    """
    ENGLISH = 0

class WindowsLanguageID(enum.IntEnum):
    """ Platform-specific Language IDs
        assigned by Microsoft
    """
    ENGLISH_USA = 0x0409


# https://docs.microsoft.com/en-us/typography/opentype/spec/os2
UNICODERANGE_DATA = [
    [(0, "Basic Latin",                               0x00000, 0x0007F)],
    [(1, "Latin-1 Supplement",                        0x00080, 0x000FF)],
    [(2, "Latin Extended-A",                          0x00100, 0x0017F)],
    [(3, "Latin Extended-B",                          0x00180, 0x0024F)],

    [(4, "IPA Extensions",                           0x00250, 0x002AF),
     (4, "Phonetic Extensions",                      0x01D00, 0x01D7F),
     (4, "Phonetic Extensions Supplement",           0x01D80, 0x01DBF)],

    [(5, "Spacing Modifier Letters",                 0x002B0, 0x002FF),
     (5, "Modifier Tone Letters",                    0x0A700, 0x0A71F)],

    [(6, "Combining Diacritical Marks",              0x00300, 0x0036F),
     (6, "Combining Diacritical Marks Supplement",   0x01DC0, 0x01DFF)],

    [(7, "Greek and Coptic",                          0x00370, 0x003FF)],
    [(8, "Coptic",                                    0x02C80, 0x02CFF)],

    [(9, "Cyrillic",                                 0x00400, 0x004FF),
     (9, "Cyrillic Supplement",                      0x00500, 0x0052F),
     (9, "Cyrillic Extended-A",                      0x02DE0, 0x02DFF),
     (9, "Cyrillic Extended-B",                      0x0A640, 0x0A69F)],

    [(10, "Armenian",                                 0x00530, 0x0058F)],
    [(11, "Hebrew",                                   0x00590, 0x005FF)],
    [(12, "Vai",                                      0x0A500, 0x0A63F)],

    [(13, "Arabic",                                  0x00600, 0x006FF),
     (13, "Arabic Supplement",                       0x00750, 0x0077F)],

    [(14, "NKo",                                      0x007C0, 0x007FF)],
    [(15, "Devanagari",                               0x00900, 0x0097F)],
    [(16, "Bengali",                                  0x00980, 0x009FF)],
    [(17, "Gurmukhi",                                 0x00A00, 0x00A7F)],
    [(18, "Gujarati",                                 0x00A80, 0x00AFF)],
    [(19, "Oriya",                                    0x00B00, 0x00B7F)],
    [(20, "Tamil",                                    0x00B80, 0x00BFF)],
    [(21, "Telugu",                                   0x00C00, 0x00C7F)],
    [(22, "Kannada",                                  0x00C80, 0x00CFF)],
    [(23, "Malayalam",                                0x00D00, 0x00D7F)],
    [(24, "Thai",                                     0x00E00, 0x00E7F)],
    [(25, "Lao",                                      0x00E80, 0x00EFF)],

    [(26, "Georgian",                                0x010A0, 0x010FF),
     (26, "Georgian Supplement",                     0x02D00, 0x02D2F)],

    [(27, "Balinese",                                 0x01B00, 0x01B7F)],
    [(28, "Hangul Jamo",                              0x01100, 0x011FF)],

    [(29, "Latin Extended Additional",               0x01E00, 0x01EFF),
     (29, "Latin Extended-C",                        0x02C60, 0x02C7F),
     (29, "Latin Extended-D",                        0x0A720, 0x0A7FF)],

    [(30, "Greek Extended",                           0x01F00, 0x01FFF)],

    [(31, "General Punctuation",                     0x02000, 0x0206F),
     (31, "Supplemental Punctuation",                0x02E00, 0x02E7F)],

    [(32, "Superscripts And Subscripts",              0x02070, 0x0209F)],
    [(33, "Currency Symbols",                         0x020A0, 0x020CF)],
    [(34, "Combining Diacritical Marks For Symbols",  0x020D0, 0x020FF)],
    [(35, "Letterlike Symbols",                       0x02100, 0x0214F)],
    [(36, "Number Forms",                             0x02150, 0x0218F)],

    [(37, "Arrows",                                  0x02190, 0x021FF),
     (37, "Supplemental Arrows-A",                   0x027F0, 0x027FF),
     (37, "Supplemental Arrows-B",                   0x02900, 0x0297F),
     (37, "Miscellaneous Symbols and Arrows",        0x02B00, 0x02BFF)],

    [(38, "Mathematical Operators",                  0x02200, 0x022FF),
     (38, "Supplemental Mathematical Operators",     0x02A00, 0x02AFF),
     (38, "Miscellaneous Mathematical Symbols-A",    0x027C0, 0x027EF),
     (38, "Miscellaneous Mathematical Symbols-B",    0x02980, 0x029FF)],

    [(39, "Miscellaneous Technical",                  0x02300, 0x023FF)],
    [(40, "Control Pictures",                         0x02400, 0x0243F)],
    [(41, "Optical Character Recognition",            0x02440, 0x0245F)],
    [(42, "Enclosed Alphanumerics",                   0x02460, 0x024FF)],
    [(43, "Box Drawing",                              0x02500, 0x0257F)],
    [(44, "Block Elements",                           0x02580, 0x0259F)],
    [(45, "Geometric Shapes",                         0x025A0, 0x025FF)],
    [(46, "Miscellaneous Symbols",                    0x02600, 0x026FF)],
    [(47, "Dingbats",                                 0x02700, 0x027BF)],
    [(48, "CJK Symbols And Punctuation",              0x03000, 0x0303F)],
    [(49, "Hiragana",                                 0x03040, 0x0309F)],

    [(50, "Katakana",                                0x030A0, 0x030FF),
     (50, "Katakana Phonetic Extensions",            0x031F0, 0x031FF)],

    [(51, "Bopomofo",                                0x03100, 0x0312F),
     (51, "Bopomofo Extended",                       0x031A0, 0x031BF)],

    [(52, "Hangul Compatibility Jamo",                0x03130, 0x0318F)],
    [(53, "Phags-pa",                                 0x0A840, 0x0A87F)],
    [(54, "Enclosed CJK Letters And Months",          0x03200, 0x032FF)],
    [(55, "CJK Compatibility",                        0x03300, 0x033FF)],
    [(56, "Hangul Syllables",                         0x0AC00, 0x0D7AF)],
    [(57, "Non-Plane 0 *",                            0x10000, 0x10FFFF)],
    [(58, "Phoenician",                               0x10900, 0x1091F)],

    [(59, "CJK Unified Ideographs",                  0x04E00, 0x09FFF),
     (59, "CJK Radicals Supplement",                 0x02E80, 0x02EFF),
     (59, "Kangxi Radicals",                         0x02F00, 0x02FDF),
     (59, "Ideographic Description Characters",      0x02FF0, 0x02FFF),
     (59, "CJK Unified Ideographs Extension A",      0x03400, 0x04DBF),
     (59, "CJK Unified Ideographs Extension B",      0x20000, 0x2A6DF),
     (59, "Kanbun",                                  0x03190, 0x0319F)],

    [(60, "Private Use Area (plane 0)",               0x0E000, 0x0F8FF)],

    [(61, "CJK Strokes",                             0x031C0, 0x031EF),
     (61, "CJK Compatibility Ideographs",            0x0F900, 0x0FAFF),
     (61, "CJK Compatibility Ideographs Supplement", 0x2F800, 0x2FA1F)],

    [(62, "Alphabetic Presentation Forms",            0x0FB00, 0x0FB4F)],
    [(63, "Arabic Presentation Forms-A",              0x0FB50, 0x0FDFF)],
    [(64, "Combining Half Marks",                     0x0FE20, 0x0FE2F)],

    [(65, "Vertical Forms",                          0x0FE10, 0x0FE1F),
     (65, "CJK Compatibility Forms",                 0x0FE30, 0x0FE4F)],

    [(66, "Small Form Variants",                      0x0FE50, 0x0FE6F)],
    [(67, "Arabic Presentation Forms-B",              0x0FE70, 0x0FEFF)],
    [(68, "Halfwidth And Fullwidth Forms",            0x0FF00, 0x0FFEF)],
    [(69, "Specials",                                 0x0FFF0, 0x0FFFF)],
    [(70, "Tibetan",                                  0x00F00, 0x00FFF)],
    [(71, "Syriac",                                   0x00700, 0x0074F)],
    [(72, "Thaana",                                   0x00780, 0x007BF)],
    [(73, "Sinhala",                                  0x00D80, 0x00DFF)],
    [(74, "Myanmar",                                  0x01000, 0x0109F)],

    [(75, "Ethiopic",                                0x01200, 0x0137F),
     (75, "Ethiopic Supplement",                     0x01380, 0x0139F),
     (75, "Ethiopic Extended",                       0x02D80, 0x02DDF)],

    [(76, "Cherokee",                                 0x013A0, 0x013FF)],
    [(77, "Unified Canadian Aboriginal Syllabics",    0x01400, 0x0167F)],
    [(78, "Ogham",                                    0x01680, 0x0169F)],
    [(79, "Runic",                                    0x016A0, 0x016FF)],

    [(80, "Khmer",                                   0x01780, 0x017FF),
     (80, "Khmer Symbols",                           0x019E0, 0x019FF)],

    [(81, "Mongolian",                                0x01800, 0x018AF)],
    [(82, "Braille Patterns",                         0x02800, 0x028FF)],

    [(83, "Yi Syllables",                            0x0A000, 0x0A48F),
     (83, "Yi Radicals",                             0x0A490, 0x0A4CF)],

    [(84, "Tagalog",                                 0x01700, 0x0171F),
     (84, "Hanunoo",                                 0x01720, 0x0173F),
     (84, "Buhid",                                   0x01740, 0x0175F),
     (84, "Tagbanwa",                                0x01760, 0x0177F)],

    [(85, "Old Italic",                               0x10300, 0x1032F)],
    [(86, "Gothic",                                   0x10330, 0x1034F)],
    [(87, "Deseret",                                  0x10400, 0x1044F)],

    [(88, "Byzantine Musical Symbols",               0x1D000, 0x1D0FF),
     (88, "Musical Symbols",                         0x1D100, 0x1D1FF),
     (88, "Ancient Greek Musical Notation",          0x1D200, 0x1D24F)],

    [(89, "Mathematical Alphanumeric Symbols",        0x1D400, 0x1D7FF)],

    [(90, "Private Use (plane 15)",                  0xFF000, 0xFFFFD),
     (90, "Private Use (plane 16)",                 0x100000, 0x10FFFD)],

    [(91, "Variation Selectors",                     0x0FE00, 0x0FE0F),
     (91, "Variation Selectors Supplement",          0xE0100, 0xE01EF)],

    [(92, "Tags",                                     0xE0000, 0xE007F)],
    [(93, "Limbu",                                    0x01900, 0x0194F)],
    [(94, "Tai Le",                                   0x01950, 0x0197F)],
    [(95, "New Tai Lue",                              0x01980, 0x019DF)],
    [(96, "Buginese",                                 0x01A00, 0x01A1F)],
    [(97, "Glagolitic",                               0x02C00, 0x02C5F)],
    [(98, "Tifinagh",                                 0x02D30, 0x02D7F)],
    [(99, "Yijing Hexagram Symbols",                  0x04DC0, 0x04DFF)],
    [(100, "Syloti Nagri",                            0x0A800, 0x0A82F)],

    [(101, "Linear B Syllabary",                     0x10000, 0x1007F),
     (101, "Linear B Ideograms",                     0x10080, 0x100FF),
     (101, "Aegean Numbers",                         0x10100, 0x1013F)],

    [(102, "Ancient Greek Numbers",                   0x10140, 0x1018F)],
    [(103, "Ugaritic",                                0x10380, 0x1039F)],
    [(104, "Old Persian",                             0x103A0, 0x103DF)],
    [(105, "Shavian",                                 0x10450, 0x1047F)],
    [(106, "Osmanya",                                 0x10480, 0x104AF)],
    [(107, "Cypriot Syllabary",                       0x10800, 0x1083F)],
    [(108, "Kharoshthi",                              0x10A00, 0x10A5F)],
    [(109, "Tai Xuan Jing Symbols",                   0x1D300, 0x1D35F)],

    [(110, "Cuneiform",                              0x12000, 0x123FF),
     (110, "Cuneiform Numbers and Punctuation",      0x12400, 0x1247F)],

    [(111, "Counting Rod Numerals",                   0x1D360, 0x1D37F)],
    [(112, "Sundanese",                               0x01B80, 0x01BBF)],
    [(113, "Lepcha",                                  0x01C00, 0x01C4F)],
    [(114, "Ol Chiki",                                0x01C50, 0x01C7F)],
    [(115, "Saurashtra",                              0x0A880, 0x0A8DF)],
    [(116, "Kayah Li",                                0x0A900, 0x0A92F)],
    [(117, "Rejang",                                  0x0A930, 0x0A95F)],
    [(118, "Cham",                                    0x0AA00, 0x0AA5F)],
    [(119, "Ancient Symbols",                         0x10190, 0x101CF)],
    [(120, "Phaistos Disc",                           0x101D0, 0x101FF)],

    [(121, "Carian",                                 0x102A0, 0x102DF),
     (121, "Lycian",                                 0x10280, 0x1029F),
     (121, "Lydian",                                 0x10920, 0x1093F)],

    [(122, "Domino Tiles",                           0x1F030, 0x1F09F),
     (122, "Mahjong Tiles",                          0x1F000, 0x1F02F)]
]


CJK_CODEPAGE_BITS = {
    "JIS/Japan": 17,
    "Chinese: Simplified chars—PRC and Singapore": 18,
    "Korean Wansung": 19,
    "Chinese: Traditional chars—Taiwan and Hong Kong": 20,
    "Korean Johab": 21
}


# FIXME: This is a bit redundant with UNICODERANGE_DATA:
CJK_UNICODE_RANGE_BITS = {
    'Hangul Jamo': 28,
 #  'CJK Symbols And Punctuation': 48,
    'Hiragana': 49,
    'Katakana': 50,
    'Bopomofo': 51,
    'Hangul Compatibility Jamo': 52,
    'Enclosed CJK Letters And Months': 54,
    'CJK Compatibility': 55,
    'Hangul Syllables': 56,
    'CJK Unified Ideographs': 59,
    'CJK Strokes': 61,
    'Yi Syllables': 83
}


# FIXME: This is a bit redundant with UNICODERANGE_DATA:
CJK_UNICODE_RANGES = [
    [0x1100, 0x11FF],  # Hangul Jamo
   #[0x3000, 0x303F],  # CJK Symbols and Punctuation
    [0x3040, 0x309F],  # Hiragana
    [0x30A0, 0x30FF],  # Katakana
    [0x31F0, 0x31FF],  # Katakana Phonetic Extensions
    [0x3100, 0x312F],  # Bopomofo
    [0x31A0, 0x31BF],  # Bopomofo Extended (Bopomofo)
    [0x3130, 0x318F],  # Hangul Compatibility Jamo
    [0x3200, 0x32FF],  # Enclosed CJK Letters and Months
    [0x3300, 0x33FF],  # CJK Compatibility
    [0xAC00, 0xD7AF],  # Hangul Syllables
    [0x4E00, 0x9FFF],  # CJK Unified Ideographs
    [0x2E80, 0x2EFF],  # CJK Radicals Supplement (CJK Unified Ideographs)
    [0x2F00, 0x2FDF],  # Kangxi Radicals (CJK Unified Ideographs)
    [0x2FF0, 0x2FFF],  # Ideographic Description Characters (CJK Unified Ideographs)
    [0x3400, 0x4DBF],  # CJK Unified Ideographs Extension A (CJK Unified Ideographs)
    [0x20000, 0x2A6DF],  # CJK Unified Ideographs Extension B (CJK Unified Ideographs)
    [0x3190, 0x319F],  # Kanbun (CJK Unified Ideographs)
    [0x31C0, 0x31EF],  # CJK Strokes
    [0xF900, 0xFAFF],  # CJK Compatibility Ideographs (CJK Strokes)
    [0x2F800, 0x2FA1F],  # CJK Compatibility Ideographs Supplement (CJK Strokes)
    [0xA000, 0xA48F],  # Yi Syllables
    [0xA490, 0xA4CF],  # Yi Radicals
]

OFL_BODY_TEXT = \
"""

This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL


-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------

PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.

The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded, 
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.

DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.

"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).

"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).

"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.

"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.

PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:

1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.

2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.

3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.

4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.

5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.

TERMINATION
This license becomes null and void if any of the above conditions are
not met.

DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
"""

LATEST_TTFAUTOHINT_VERSION = "1.8.4"


# Set of 2,350 modern Korean hangul syllables from KS X 1001 encoding.
# The file linked below provides the mapping between KS X 1001 and Unicode.
# http://unicode.org/Public/MAPPINGS/OBSOLETE/EASTASIA/KSC/KSX1001.TXT
MODERN_HANGUL_SYLLABLES_CODEPOINTS = {
    0xAC00, 0xAC01, 0xAC04, 0xAC07, 0xAC08, 0xAC09, 0xAC0A, 0xAC10, 0xAC11, 0xAC12,
    0xAC13, 0xAC14, 0xAC15, 0xAC16, 0xAC17, 0xAC19, 0xAC1A, 0xAC1B, 0xAC1C, 0xAC1D,
    0xAC20, 0xAC24, 0xAC2C, 0xAC2D, 0xAC2F, 0xAC30, 0xAC31, 0xAC38, 0xAC39, 0xAC3C,
    0xAC40, 0xAC4B, 0xAC4D, 0xAC54, 0xAC58, 0xAC5C, 0xAC70, 0xAC71, 0xAC74, 0xAC77,
    0xAC78, 0xAC7A, 0xAC80, 0xAC81, 0xAC83, 0xAC84, 0xAC85, 0xAC86, 0xAC89, 0xAC8A,
    0xAC8B, 0xAC8C, 0xAC90, 0xAC94, 0xAC9C, 0xAC9D, 0xAC9F, 0xACA0, 0xACA1, 0xACA8,
    0xACA9, 0xACAA, 0xACAC, 0xACAF, 0xACB0, 0xACB8, 0xACB9, 0xACBB, 0xACBC, 0xACBD,
    0xACC1, 0xACC4, 0xACC8, 0xACCC, 0xACD5, 0xACD7, 0xACE0, 0xACE1, 0xACE4, 0xACE7,
    0xACE8, 0xACEA, 0xACEC, 0xACEF, 0xACF0, 0xACF1, 0xACF3, 0xACF5, 0xACF6, 0xACFC,
    0xACFD, 0xAD00, 0xAD04, 0xAD06, 0xAD0C, 0xAD0D, 0xAD0F, 0xAD11, 0xAD18, 0xAD1C,
    0xAD20, 0xAD29, 0xAD2C, 0xAD2D, 0xAD34, 0xAD35, 0xAD38, 0xAD3C, 0xAD44, 0xAD45,
    0xAD47, 0xAD49, 0xAD50, 0xAD54, 0xAD58, 0xAD61, 0xAD63, 0xAD6C, 0xAD6D, 0xAD70,
    0xAD73, 0xAD74, 0xAD75, 0xAD76, 0xAD7B, 0xAD7C, 0xAD7D, 0xAD7F, 0xAD81, 0xAD82,
    0xAD88, 0xAD89, 0xAD8C, 0xAD90, 0xAD9C, 0xAD9D, 0xADA4, 0xADB7, 0xADC0, 0xADC1,
    0xADC4, 0xADC8, 0xADD0, 0xADD1, 0xADD3, 0xADDC, 0xADE0, 0xADE4, 0xADF8, 0xADF9,
    0xADFC, 0xADFF, 0xAE00, 0xAE01, 0xAE08, 0xAE09, 0xAE0B, 0xAE0D, 0xAE14, 0xAE30,
    0xAE31, 0xAE34, 0xAE37, 0xAE38, 0xAE3A, 0xAE40, 0xAE41, 0xAE43, 0xAE45, 0xAE46,
    0xAE4A, 0xAE4C, 0xAE4D, 0xAE4E, 0xAE50, 0xAE54, 0xAE56, 0xAE5C, 0xAE5D, 0xAE5F,
    0xAE60, 0xAE61, 0xAE65, 0xAE68, 0xAE69, 0xAE6C, 0xAE70, 0xAE78, 0xAE79, 0xAE7B,
    0xAE7C, 0xAE7D, 0xAE84, 0xAE85, 0xAE8C, 0xAEBC, 0xAEBD, 0xAEBE, 0xAEC0, 0xAEC4,
    0xAECC, 0xAECD, 0xAECF, 0xAED0, 0xAED1, 0xAED8, 0xAED9, 0xAEDC, 0xAEE8, 0xAEEB,
    0xAEED, 0xAEF4, 0xAEF8, 0xAEFC, 0xAF07, 0xAF08, 0xAF0D, 0xAF10, 0xAF2C, 0xAF2D,
    0xAF30, 0xAF32, 0xAF34, 0xAF3C, 0xAF3D, 0xAF3F, 0xAF41, 0xAF42, 0xAF43, 0xAF48,
    0xAF49, 0xAF50, 0xAF5C, 0xAF5D, 0xAF64, 0xAF65, 0xAF79, 0xAF80, 0xAF84, 0xAF88,
    0xAF90, 0xAF91, 0xAF95, 0xAF9C, 0xAFB8, 0xAFB9, 0xAFBC, 0xAFC0, 0xAFC7, 0xAFC8,
    0xAFC9, 0xAFCB, 0xAFCD, 0xAFCE, 0xAFD4, 0xAFDC, 0xAFE8, 0xAFE9, 0xAFF0, 0xAFF1,
    0xAFF4, 0xAFF8, 0xB000, 0xB001, 0xB004, 0xB00C, 0xB010, 0xB014, 0xB01C, 0xB01D,
    0xB028, 0xB044, 0xB045, 0xB048, 0xB04A, 0xB04C, 0xB04E, 0xB053, 0xB054, 0xB055,
    0xB057, 0xB059, 0xB05D, 0xB07C, 0xB07D, 0xB080, 0xB084, 0xB08C, 0xB08D, 0xB08F,
    0xB091, 0xB098, 0xB099, 0xB09A, 0xB09C, 0xB09F, 0xB0A0, 0xB0A1, 0xB0A2, 0xB0A8,
    0xB0A9, 0xB0AB, 0xB0AC, 0xB0AD, 0xB0AE, 0xB0AF, 0xB0B1, 0xB0B3, 0xB0B4, 0xB0B5,
    0xB0B8, 0xB0BC, 0xB0C4, 0xB0C5, 0xB0C7, 0xB0C8, 0xB0C9, 0xB0D0, 0xB0D1, 0xB0D4,
    0xB0D8, 0xB0E0, 0xB0E5, 0xB108, 0xB109, 0xB10B, 0xB10C, 0xB110, 0xB112, 0xB113,
    0xB118, 0xB119, 0xB11B, 0xB11C, 0xB11D, 0xB123, 0xB124, 0xB125, 0xB128, 0xB12C,
    0xB134, 0xB135, 0xB137, 0xB138, 0xB139, 0xB140, 0xB141, 0xB144, 0xB148, 0xB150,
    0xB151, 0xB154, 0xB155, 0xB158, 0xB15C, 0xB160, 0xB178, 0xB179, 0xB17C, 0xB180,
    0xB182, 0xB188, 0xB189, 0xB18B, 0xB18D, 0xB192, 0xB193, 0xB194, 0xB198, 0xB19C,
    0xB1A8, 0xB1CC, 0xB1D0, 0xB1D4, 0xB1DC, 0xB1DD, 0xB1DF, 0xB1E8, 0xB1E9, 0xB1EC,
    0xB1F0, 0xB1F9, 0xB1FB, 0xB1FD, 0xB204, 0xB205, 0xB208, 0xB20B, 0xB20C, 0xB214,
    0xB215, 0xB217, 0xB219, 0xB220, 0xB234, 0xB23C, 0xB258, 0xB25C, 0xB260, 0xB268,
    0xB269, 0xB274, 0xB275, 0xB27C, 0xB284, 0xB285, 0xB289, 0xB290, 0xB291, 0xB294,
    0xB298, 0xB299, 0xB29A, 0xB2A0, 0xB2A1, 0xB2A3, 0xB2A5, 0xB2A6, 0xB2AA, 0xB2AC,
    0xB2B0, 0xB2B4, 0xB2C8, 0xB2C9, 0xB2CC, 0xB2D0, 0xB2D2, 0xB2D8, 0xB2D9, 0xB2DB,
    0xB2DD, 0xB2E2, 0xB2E4, 0xB2E5, 0xB2E6, 0xB2E8, 0xB2EB, 0xB2EC, 0xB2ED, 0xB2EE,
    0xB2EF, 0xB2F3, 0xB2F4, 0xB2F5, 0xB2F7, 0xB2F8, 0xB2F9, 0xB2FA, 0xB2FB, 0xB2FF,
    0xB300, 0xB301, 0xB304, 0xB308, 0xB310, 0xB311, 0xB313, 0xB314, 0xB315, 0xB31C,
    0xB354, 0xB355, 0xB356, 0xB358, 0xB35B, 0xB35C, 0xB35E, 0xB35F, 0xB364, 0xB365,
    0xB367, 0xB369, 0xB36B, 0xB36E, 0xB370, 0xB371, 0xB374, 0xB378, 0xB380, 0xB381,
    0xB383, 0xB384, 0xB385, 0xB38C, 0xB390, 0xB394, 0xB3A0, 0xB3A1, 0xB3A8, 0xB3AC,
    0xB3C4, 0xB3C5, 0xB3C8, 0xB3CB, 0xB3CC, 0xB3CE, 0xB3D0, 0xB3D4, 0xB3D5, 0xB3D7,
    0xB3D9, 0xB3DB, 0xB3DD, 0xB3E0, 0xB3E4, 0xB3E8, 0xB3FC, 0xB410, 0xB418, 0xB41C,
    0xB420, 0xB428, 0xB429, 0xB42B, 0xB434, 0xB450, 0xB451, 0xB454, 0xB458, 0xB460,
    0xB461, 0xB463, 0xB465, 0xB46C, 0xB480, 0xB488, 0xB49D, 0xB4A4, 0xB4A8, 0xB4AC,
    0xB4B5, 0xB4B7, 0xB4B9, 0xB4C0, 0xB4C4, 0xB4C8, 0xB4D0, 0xB4D5, 0xB4DC, 0xB4DD,
    0xB4E0, 0xB4E3, 0xB4E4, 0xB4E6, 0xB4EC, 0xB4ED, 0xB4EF, 0xB4F1, 0xB4F8, 0xB514,
    0xB515, 0xB518, 0xB51B, 0xB51C, 0xB524, 0xB525, 0xB527, 0xB528, 0xB529, 0xB52A,
    0xB530, 0xB531, 0xB534, 0xB538, 0xB540, 0xB541, 0xB543, 0xB544, 0xB545, 0xB54B,
    0xB54C, 0xB54D, 0xB550, 0xB554, 0xB55C, 0xB55D, 0xB55F, 0xB560, 0xB561, 0xB5A0,
    0xB5A1, 0xB5A4, 0xB5A8, 0xB5AA, 0xB5AB, 0xB5B0, 0xB5B1, 0xB5B3, 0xB5B4, 0xB5B5,
    0xB5BB, 0xB5BC, 0xB5BD, 0xB5C0, 0xB5C4, 0xB5CC, 0xB5CD, 0xB5CF, 0xB5D0, 0xB5D1,
    0xB5D8, 0xB5EC, 0xB610, 0xB611, 0xB614, 0xB618, 0xB625, 0xB62C, 0xB634, 0xB648,
    0xB664, 0xB668, 0xB69C, 0xB69D, 0xB6A0, 0xB6A4, 0xB6AB, 0xB6AC, 0xB6B1, 0xB6D4,
    0xB6F0, 0xB6F4, 0xB6F8, 0xB700, 0xB701, 0xB705, 0xB728, 0xB729, 0xB72C, 0xB72F,
    0xB730, 0xB738, 0xB739, 0xB73B, 0xB744, 0xB748, 0xB74C, 0xB754, 0xB755, 0xB760,
    0xB764, 0xB768, 0xB770, 0xB771, 0xB773, 0xB775, 0xB77C, 0xB77D, 0xB780, 0xB784,
    0xB78C, 0xB78D, 0xB78F, 0xB790, 0xB791, 0xB792, 0xB796, 0xB797, 0xB798, 0xB799,
    0xB79C, 0xB7A0, 0xB7A8, 0xB7A9, 0xB7AB, 0xB7AC, 0xB7AD, 0xB7B4, 0xB7B5, 0xB7B8,
    0xB7C7, 0xB7C9, 0xB7EC, 0xB7ED, 0xB7F0, 0xB7F4, 0xB7FC, 0xB7FD, 0xB7FF, 0xB800,
    0xB801, 0xB807, 0xB808, 0xB809, 0xB80C, 0xB810, 0xB818, 0xB819, 0xB81B, 0xB81D,
    0xB824, 0xB825, 0xB828, 0xB82C, 0xB834, 0xB835, 0xB837, 0xB838, 0xB839, 0xB840,
    0xB844, 0xB851, 0xB853, 0xB85C, 0xB85D, 0xB860, 0xB864, 0xB86C, 0xB86D, 0xB86F,
    0xB871, 0xB878, 0xB87C, 0xB88D, 0xB8A8, 0xB8B0, 0xB8B4, 0xB8B8, 0xB8C0, 0xB8C1,
    0xB8C3, 0xB8C5, 0xB8CC, 0xB8D0, 0xB8D4, 0xB8DD, 0xB8DF, 0xB8E1, 0xB8E8, 0xB8E9,
    0xB8EC, 0xB8F0, 0xB8F8, 0xB8F9, 0xB8FB, 0xB8FD, 0xB904, 0xB918, 0xB920, 0xB93C,
    0xB93D, 0xB940, 0xB944, 0xB94C, 0xB94F, 0xB951, 0xB958, 0xB959, 0xB95C, 0xB960,
    0xB968, 0xB969, 0xB96B, 0xB96D, 0xB974, 0xB975, 0xB978, 0xB97C, 0xB984, 0xB985,
    0xB987, 0xB989, 0xB98A, 0xB98D, 0xB98E, 0xB9AC, 0xB9AD, 0xB9B0, 0xB9B4, 0xB9BC,
    0xB9BD, 0xB9BF, 0xB9C1, 0xB9C8, 0xB9C9, 0xB9CC, 0xB9CE, 0xB9CF, 0xB9D0, 0xB9D1,
    0xB9D2, 0xB9D8, 0xB9D9, 0xB9DB, 0xB9DD, 0xB9DE, 0xB9E1, 0xB9E3, 0xB9E4, 0xB9E5,
    0xB9E8, 0xB9EC, 0xB9F4, 0xB9F5, 0xB9F7, 0xB9F8, 0xB9F9, 0xB9FA, 0xBA00, 0xBA01,
    0xBA08, 0xBA15, 0xBA38, 0xBA39, 0xBA3C, 0xBA40, 0xBA42, 0xBA48, 0xBA49, 0xBA4B,
    0xBA4D, 0xBA4E, 0xBA53, 0xBA54, 0xBA55, 0xBA58, 0xBA5C, 0xBA64, 0xBA65, 0xBA67,
    0xBA68, 0xBA69, 0xBA70, 0xBA71, 0xBA74, 0xBA78, 0xBA83, 0xBA84, 0xBA85, 0xBA87,
    0xBA8C, 0xBAA8, 0xBAA9, 0xBAAB, 0xBAAC, 0xBAB0, 0xBAB2, 0xBAB8, 0xBAB9, 0xBABB,
    0xBABD, 0xBAC4, 0xBAC8, 0xBAD8, 0xBAD9, 0xBAFC, 0xBB00, 0xBB04, 0xBB0D, 0xBB0F,
    0xBB11, 0xBB18, 0xBB1C, 0xBB20, 0xBB29, 0xBB2B, 0xBB34, 0xBB35, 0xBB36, 0xBB38,
    0xBB3B, 0xBB3C, 0xBB3D, 0xBB3E, 0xBB44, 0xBB45, 0xBB47, 0xBB49, 0xBB4D, 0xBB4F,
    0xBB50, 0xBB54, 0xBB58, 0xBB61, 0xBB63, 0xBB6C, 0xBB88, 0xBB8C, 0xBB90, 0xBBA4,
    0xBBA8, 0xBBAC, 0xBBB4, 0xBBB7, 0xBBC0, 0xBBC4, 0xBBC8, 0xBBD0, 0xBBD3, 0xBBF8,
    0xBBF9, 0xBBFC, 0xBBFF, 0xBC00, 0xBC02, 0xBC08, 0xBC09, 0xBC0B, 0xBC0C, 0xBC0D,
    0xBC0F, 0xBC11, 0xBC14, 0xBC15, 0xBC16, 0xBC17, 0xBC18, 0xBC1B, 0xBC1C, 0xBC1D,
    0xBC1E, 0xBC1F, 0xBC24, 0xBC25, 0xBC27, 0xBC29, 0xBC2D, 0xBC30, 0xBC31, 0xBC34,
    0xBC38, 0xBC40, 0xBC41, 0xBC43, 0xBC44, 0xBC45, 0xBC49, 0xBC4C, 0xBC4D, 0xBC50,
    0xBC5D, 0xBC84, 0xBC85, 0xBC88, 0xBC8B, 0xBC8C, 0xBC8E, 0xBC94, 0xBC95, 0xBC97,
    0xBC99, 0xBC9A, 0xBCA0, 0xBCA1, 0xBCA4, 0xBCA7, 0xBCA8, 0xBCB0, 0xBCB1, 0xBCB3,
    0xBCB4, 0xBCB5, 0xBCBC, 0xBCBD, 0xBCC0, 0xBCC4, 0xBCCD, 0xBCCF, 0xBCD0, 0xBCD1,
    0xBCD5, 0xBCD8, 0xBCDC, 0xBCF4, 0xBCF5, 0xBCF6, 0xBCF8, 0xBCFC, 0xBD04, 0xBD05,
    0xBD07, 0xBD09, 0xBD10, 0xBD14, 0xBD24, 0xBD2C, 0xBD40, 0xBD48, 0xBD49, 0xBD4C,
    0xBD50, 0xBD58, 0xBD59, 0xBD64, 0xBD68, 0xBD80, 0xBD81, 0xBD84, 0xBD87, 0xBD88,
    0xBD89, 0xBD8A, 0xBD90, 0xBD91, 0xBD93, 0xBD95, 0xBD99, 0xBD9A, 0xBD9C, 0xBDA4,
    0xBDB0, 0xBDB8, 0xBDD4, 0xBDD5, 0xBDD8, 0xBDDC, 0xBDE9, 0xBDF0, 0xBDF4, 0xBDF8,
    0xBE00, 0xBE03, 0xBE05, 0xBE0C, 0xBE0D, 0xBE10, 0xBE14, 0xBE1C, 0xBE1D, 0xBE1F,
    0xBE44, 0xBE45, 0xBE48, 0xBE4C, 0xBE4E, 0xBE54, 0xBE55, 0xBE57, 0xBE59, 0xBE5A,
    0xBE5B, 0xBE60, 0xBE61, 0xBE64, 0xBE68, 0xBE6A, 0xBE70, 0xBE71, 0xBE73, 0xBE74,
    0xBE75, 0xBE7B, 0xBE7C, 0xBE7D, 0xBE80, 0xBE84, 0xBE8C, 0xBE8D, 0xBE8F, 0xBE90,
    0xBE91, 0xBE98, 0xBE99, 0xBEA8, 0xBED0, 0xBED1, 0xBED4, 0xBED7, 0xBED8, 0xBEE0,
    0xBEE3, 0xBEE4, 0xBEE5, 0xBEEC, 0xBF01, 0xBF08, 0xBF09, 0xBF18, 0xBF19, 0xBF1B,
    0xBF1C, 0xBF1D, 0xBF40, 0xBF41, 0xBF44, 0xBF48, 0xBF50, 0xBF51, 0xBF55, 0xBF94,
    0xBFB0, 0xBFC5, 0xBFCC, 0xBFCD, 0xBFD0, 0xBFD4, 0xBFDC, 0xBFDF, 0xBFE1, 0xC03C,
    0xC051, 0xC058, 0xC05C, 0xC060, 0xC068, 0xC069, 0xC090, 0xC091, 0xC094, 0xC098,
    0xC0A0, 0xC0A1, 0xC0A3, 0xC0A5, 0xC0AC, 0xC0AD, 0xC0AF, 0xC0B0, 0xC0B3, 0xC0B4,
    0xC0B5, 0xC0B6, 0xC0BC, 0xC0BD, 0xC0BF, 0xC0C0, 0xC0C1, 0xC0C5, 0xC0C8, 0xC0C9,
    0xC0CC, 0xC0D0, 0xC0D8, 0xC0D9, 0xC0DB, 0xC0DC, 0xC0DD, 0xC0E4, 0xC0E5, 0xC0E8,
    0xC0EC, 0xC0F4, 0xC0F5, 0xC0F7, 0xC0F9, 0xC100, 0xC104, 0xC108, 0xC110, 0xC115,
    0xC11C, 0xC11D, 0xC11E, 0xC11F, 0xC120, 0xC123, 0xC124, 0xC126, 0xC127, 0xC12C,
    0xC12D, 0xC12F, 0xC130, 0xC131, 0xC136, 0xC138, 0xC139, 0xC13C, 0xC140, 0xC148,
    0xC149, 0xC14B, 0xC14C, 0xC14D, 0xC154, 0xC155, 0xC158, 0xC15C, 0xC164, 0xC165,
    0xC167, 0xC168, 0xC169, 0xC170, 0xC174, 0xC178, 0xC185, 0xC18C, 0xC18D, 0xC18E,
    0xC190, 0xC194, 0xC196, 0xC19C, 0xC19D, 0xC19F, 0xC1A1, 0xC1A5, 0xC1A8, 0xC1A9,
    0xC1AC, 0xC1B0, 0xC1BD, 0xC1C4, 0xC1C8, 0xC1CC, 0xC1D4, 0xC1D7, 0xC1D8, 0xC1E0,
    0xC1E4, 0xC1E8, 0xC1F0, 0xC1F1, 0xC1F3, 0xC1FC, 0xC1FD, 0xC200, 0xC204, 0xC20C,
    0xC20D, 0xC20F, 0xC211, 0xC218, 0xC219, 0xC21C, 0xC21F, 0xC220, 0xC228, 0xC229,
    0xC22B, 0xC22D, 0xC22F, 0xC231, 0xC232, 0xC234, 0xC248, 0xC250, 0xC251, 0xC254,
    0xC258, 0xC260, 0xC265, 0xC26C, 0xC26D, 0xC270, 0xC274, 0xC27C, 0xC27D, 0xC27F,
    0xC281, 0xC288, 0xC289, 0xC290, 0xC298, 0xC29B, 0xC29D, 0xC2A4, 0xC2A5, 0xC2A8,
    0xC2AC, 0xC2AD, 0xC2B4, 0xC2B5, 0xC2B7, 0xC2B9, 0xC2DC, 0xC2DD, 0xC2E0, 0xC2E3,
    0xC2E4, 0xC2EB, 0xC2EC, 0xC2ED, 0xC2EF, 0xC2F1, 0xC2F6, 0xC2F8, 0xC2F9, 0xC2FB,
    0xC2FC, 0xC300, 0xC308, 0xC309, 0xC30C, 0xC30D, 0xC313, 0xC314, 0xC315, 0xC318,
    0xC31C, 0xC324, 0xC325, 0xC328, 0xC329, 0xC345, 0xC368, 0xC369, 0xC36C, 0xC370,
    0xC372, 0xC378, 0xC379, 0xC37C, 0xC37D, 0xC384, 0xC388, 0xC38C, 0xC3C0, 0xC3D8,
    0xC3D9, 0xC3DC, 0xC3DF, 0xC3E0, 0xC3E2, 0xC3E8, 0xC3E9, 0xC3ED, 0xC3F4, 0xC3F5,
    0xC3F8, 0xC408, 0xC410, 0xC424, 0xC42C, 0xC430, 0xC434, 0xC43C, 0xC43D, 0xC448,
    0xC464, 0xC465, 0xC468, 0xC46C, 0xC474, 0xC475, 0xC479, 0xC480, 0xC494, 0xC49C,
    0xC4B8, 0xC4BC, 0xC4E9, 0xC4F0, 0xC4F1, 0xC4F4, 0xC4F8, 0xC4FA, 0xC4FF, 0xC500,
    0xC501, 0xC50C, 0xC510, 0xC514, 0xC51C, 0xC528, 0xC529, 0xC52C, 0xC530, 0xC538,
    0xC539, 0xC53B, 0xC53D, 0xC544, 0xC545, 0xC548, 0xC549, 0xC54A, 0xC54C, 0xC54D,
    0xC54E, 0xC553, 0xC554, 0xC555, 0xC557, 0xC558, 0xC559, 0xC55D, 0xC55E, 0xC560,
    0xC561, 0xC564, 0xC568, 0xC570, 0xC571, 0xC573, 0xC574, 0xC575, 0xC57C, 0xC57D,
    0xC580, 0xC584, 0xC587, 0xC58C, 0xC58D, 0xC58F, 0xC591, 0xC595, 0xC597, 0xC598,
    0xC59C, 0xC5A0, 0xC5A9, 0xC5B4, 0xC5B5, 0xC5B8, 0xC5B9, 0xC5BB, 0xC5BC, 0xC5BD,
    0xC5BE, 0xC5C4, 0xC5C5, 0xC5C6, 0xC5C7, 0xC5C8, 0xC5C9, 0xC5CA, 0xC5CC, 0xC5CE,
    0xC5D0, 0xC5D1, 0xC5D4, 0xC5D8, 0xC5E0, 0xC5E1, 0xC5E3, 0xC5E5, 0xC5EC, 0xC5ED,
    0xC5EE, 0xC5F0, 0xC5F4, 0xC5F6, 0xC5F7, 0xC5FC, 0xC5FD, 0xC5FE, 0xC5FF, 0xC600,
    0xC601, 0xC605, 0xC606, 0xC607, 0xC608, 0xC60C, 0xC610, 0xC618, 0xC619, 0xC61B,
    0xC61C, 0xC624, 0xC625, 0xC628, 0xC62C, 0xC62D, 0xC62E, 0xC630, 0xC633, 0xC634,
    0xC635, 0xC637, 0xC639, 0xC63B, 0xC640, 0xC641, 0xC644, 0xC648, 0xC650, 0xC651,
    0xC653, 0xC654, 0xC655, 0xC65C, 0xC65D, 0xC660, 0xC66C, 0xC66F, 0xC671, 0xC678,
    0xC679, 0xC67C, 0xC680, 0xC688, 0xC689, 0xC68B, 0xC68D, 0xC694, 0xC695, 0xC698,
    0xC69C, 0xC6A4, 0xC6A5, 0xC6A7, 0xC6A9, 0xC6B0, 0xC6B1, 0xC6B4, 0xC6B8, 0xC6B9,
    0xC6BA, 0xC6C0, 0xC6C1, 0xC6C3, 0xC6C5, 0xC6CC, 0xC6CD, 0xC6D0, 0xC6D4, 0xC6DC,
    0xC6DD, 0xC6E0, 0xC6E1, 0xC6E8, 0xC6E9, 0xC6EC, 0xC6F0, 0xC6F8, 0xC6F9, 0xC6FD,
    0xC704, 0xC705, 0xC708, 0xC70C, 0xC714, 0xC715, 0xC717, 0xC719, 0xC720, 0xC721,
    0xC724, 0xC728, 0xC730, 0xC731, 0xC733, 0xC735, 0xC737, 0xC73C, 0xC73D, 0xC740,
    0xC744, 0xC74A, 0xC74C, 0xC74D, 0xC74F, 0xC751, 0xC752, 0xC753, 0xC754, 0xC755,
    0xC756, 0xC757, 0xC758, 0xC75C, 0xC760, 0xC768, 0xC76B, 0xC774, 0xC775, 0xC778,
    0xC77C, 0xC77D, 0xC77E, 0xC783, 0xC784, 0xC785, 0xC787, 0xC788, 0xC789, 0xC78A,
    0xC78E, 0xC790, 0xC791, 0xC794, 0xC796, 0xC797, 0xC798, 0xC79A, 0xC7A0, 0xC7A1,
    0xC7A3, 0xC7A4, 0xC7A5, 0xC7A6, 0xC7AC, 0xC7AD, 0xC7B0, 0xC7B4, 0xC7BC, 0xC7BD,
    0xC7BF, 0xC7C0, 0xC7C1, 0xC7C8, 0xC7C9, 0xC7CC, 0xC7CE, 0xC7D0, 0xC7D8, 0xC7DD,
    0xC7E4, 0xC7E8, 0xC7EC, 0xC800, 0xC801, 0xC804, 0xC808, 0xC80A, 0xC810, 0xC811,
    0xC813, 0xC815, 0xC816, 0xC81C, 0xC81D, 0xC820, 0xC824, 0xC82C, 0xC82D, 0xC82F,
    0xC831, 0xC838, 0xC83C, 0xC840, 0xC848, 0xC849, 0xC84C, 0xC84D, 0xC854, 0xC870,
    0xC871, 0xC874, 0xC878, 0xC87A, 0xC880, 0xC881, 0xC883, 0xC885, 0xC886, 0xC887,
    0xC88B, 0xC88C, 0xC88D, 0xC894, 0xC89D, 0xC89F, 0xC8A1, 0xC8A8, 0xC8BC, 0xC8BD,
    0xC8C4, 0xC8C8, 0xC8CC, 0xC8D4, 0xC8D5, 0xC8D7, 0xC8D9, 0xC8E0, 0xC8E1, 0xC8E4,
    0xC8F5, 0xC8FC, 0xC8FD, 0xC900, 0xC904, 0xC905, 0xC906, 0xC90C, 0xC90D, 0xC90F,
    0xC911, 0xC918, 0xC92C, 0xC934, 0xC950, 0xC951, 0xC954, 0xC958, 0xC960, 0xC961,
    0xC963, 0xC96C, 0xC970, 0xC974, 0xC97C, 0xC988, 0xC989, 0xC98C, 0xC990, 0xC998,
    0xC999, 0xC99B, 0xC99D, 0xC9C0, 0xC9C1, 0xC9C4, 0xC9C7, 0xC9C8, 0xC9CA, 0xC9D0,
    0xC9D1, 0xC9D3, 0xC9D5, 0xC9D6, 0xC9D9, 0xC9DA, 0xC9DC, 0xC9DD, 0xC9E0, 0xC9E2,
    0xC9E4, 0xC9E7, 0xC9EC, 0xC9ED, 0xC9EF, 0xC9F0, 0xC9F1, 0xC9F8, 0xC9F9, 0xC9FC,
    0xCA00, 0xCA08, 0xCA09, 0xCA0B, 0xCA0C, 0xCA0D, 0xCA14, 0xCA18, 0xCA29, 0xCA4C,
    0xCA4D, 0xCA50, 0xCA54, 0xCA5C, 0xCA5D, 0xCA5F, 0xCA60, 0xCA61, 0xCA68, 0xCA7D,
    0xCA84, 0xCA98, 0xCABC, 0xCABD, 0xCAC0, 0xCAC4, 0xCACC, 0xCACD, 0xCACF, 0xCAD1,
    0xCAD3, 0xCAD8, 0xCAD9, 0xCAE0, 0xCAEC, 0xCAF4, 0xCB08, 0xCB10, 0xCB14, 0xCB18,
    0xCB20, 0xCB21, 0xCB41, 0xCB48, 0xCB49, 0xCB4C, 0xCB50, 0xCB58, 0xCB59, 0xCB5D,
    0xCB64, 0xCB78, 0xCB79, 0xCB9C, 0xCBB8, 0xCBD4, 0xCBE4, 0xCBE7, 0xCBE9, 0xCC0C,
    0xCC0D, 0xCC10, 0xCC14, 0xCC1C, 0xCC1D, 0xCC21, 0xCC22, 0xCC27, 0xCC28, 0xCC29,
    0xCC2C, 0xCC2E, 0xCC30, 0xCC38, 0xCC39, 0xCC3B, 0xCC3C, 0xCC3D, 0xCC3E, 0xCC44,
    0xCC45, 0xCC48, 0xCC4C, 0xCC54, 0xCC55, 0xCC57, 0xCC58, 0xCC59, 0xCC60, 0xCC64,
    0xCC66, 0xCC68, 0xCC70, 0xCC75, 0xCC98, 0xCC99, 0xCC9C, 0xCCA0, 0xCCA8, 0xCCA9,
    0xCCAB, 0xCCAC, 0xCCAD, 0xCCB4, 0xCCB5, 0xCCB8, 0xCCBC, 0xCCC4, 0xCCC5, 0xCCC7,
    0xCCC9, 0xCCD0, 0xCCD4, 0xCCE4, 0xCCEC, 0xCCF0, 0xCD01, 0xCD08, 0xCD09, 0xCD0C,
    0xCD10, 0xCD18, 0xCD19, 0xCD1B, 0xCD1D, 0xCD24, 0xCD28, 0xCD2C, 0xCD39, 0xCD5C,
    0xCD60, 0xCD64, 0xCD6C, 0xCD6D, 0xCD6F, 0xCD71, 0xCD78, 0xCD88, 0xCD94, 0xCD95,
    0xCD98, 0xCD9C, 0xCDA4, 0xCDA5, 0xCDA7, 0xCDA9, 0xCDB0, 0xCDC4, 0xCDCC, 0xCDD0,
    0xCDE8, 0xCDEC, 0xCDF0, 0xCDF8, 0xCDF9, 0xCDFB, 0xCDFD, 0xCE04, 0xCE08, 0xCE0C,
    0xCE14, 0xCE19, 0xCE20, 0xCE21, 0xCE24, 0xCE28, 0xCE30, 0xCE31, 0xCE33, 0xCE35,
    0xCE58, 0xCE59, 0xCE5C, 0xCE5F, 0xCE60, 0xCE61, 0xCE68, 0xCE69, 0xCE6B, 0xCE6D,
    0xCE74, 0xCE75, 0xCE78, 0xCE7C, 0xCE84, 0xCE85, 0xCE87, 0xCE89, 0xCE90, 0xCE91,
    0xCE94, 0xCE98, 0xCEA0, 0xCEA1, 0xCEA3, 0xCEA4, 0xCEA5, 0xCEAC, 0xCEAD, 0xCEC1,
    0xCEE4, 0xCEE5, 0xCEE8, 0xCEEB, 0xCEEC, 0xCEF4, 0xCEF5, 0xCEF7, 0xCEF8, 0xCEF9,
    0xCF00, 0xCF01, 0xCF04, 0xCF08, 0xCF10, 0xCF11, 0xCF13, 0xCF15, 0xCF1C, 0xCF20,
    0xCF24, 0xCF2C, 0xCF2D, 0xCF2F, 0xCF30, 0xCF31, 0xCF38, 0xCF54, 0xCF55, 0xCF58,
    0xCF5C, 0xCF64, 0xCF65, 0xCF67, 0xCF69, 0xCF70, 0xCF71, 0xCF74, 0xCF78, 0xCF80,
    0xCF85, 0xCF8C, 0xCFA1, 0xCFA8, 0xCFB0, 0xCFC4, 0xCFE0, 0xCFE1, 0xCFE4, 0xCFE8,
    0xCFF0, 0xCFF1, 0xCFF3, 0xCFF5, 0xCFFC, 0xD000, 0xD004, 0xD011, 0xD018, 0xD02D,
    0xD034, 0xD035, 0xD038, 0xD03C, 0xD044, 0xD045, 0xD047, 0xD049, 0xD050, 0xD054,
    0xD058, 0xD060, 0xD06C, 0xD06D, 0xD070, 0xD074, 0xD07C, 0xD07D, 0xD081, 0xD0A4,
    0xD0A5, 0xD0A8, 0xD0AC, 0xD0B4, 0xD0B5, 0xD0B7, 0xD0B9, 0xD0C0, 0xD0C1, 0xD0C4,
    0xD0C8, 0xD0C9, 0xD0D0, 0xD0D1, 0xD0D3, 0xD0D4, 0xD0D5, 0xD0DC, 0xD0DD, 0xD0E0,
    0xD0E4, 0xD0EC, 0xD0ED, 0xD0EF, 0xD0F0, 0xD0F1, 0xD0F8, 0xD10D, 0xD130, 0xD131,
    0xD134, 0xD138, 0xD13A, 0xD140, 0xD141, 0xD143, 0xD144, 0xD145, 0xD14C, 0xD14D,
    0xD150, 0xD154, 0xD15C, 0xD15D, 0xD15F, 0xD161, 0xD168, 0xD16C, 0xD17C, 0xD184,
    0xD188, 0xD1A0, 0xD1A1, 0xD1A4, 0xD1A8, 0xD1B0, 0xD1B1, 0xD1B3, 0xD1B5, 0xD1BA,
    0xD1BC, 0xD1C0, 0xD1D8, 0xD1F4, 0xD1F8, 0xD207, 0xD209, 0xD210, 0xD22C, 0xD22D,
    0xD230, 0xD234, 0xD23C, 0xD23D, 0xD23F, 0xD241, 0xD248, 0xD25C, 0xD264, 0xD280,
    0xD281, 0xD284, 0xD288, 0xD290, 0xD291, 0xD295, 0xD29C, 0xD2A0, 0xD2A4, 0xD2AC,
    0xD2B1, 0xD2B8, 0xD2B9, 0xD2BC, 0xD2BF, 0xD2C0, 0xD2C2, 0xD2C8, 0xD2C9, 0xD2CB,
    0xD2D4, 0xD2D8, 0xD2DC, 0xD2E4, 0xD2E5, 0xD2F0, 0xD2F1, 0xD2F4, 0xD2F8, 0xD300,
    0xD301, 0xD303, 0xD305, 0xD30C, 0xD30D, 0xD30E, 0xD310, 0xD314, 0xD316, 0xD31C,
    0xD31D, 0xD31F, 0xD320, 0xD321, 0xD325, 0xD328, 0xD329, 0xD32C, 0xD330, 0xD338,
    0xD339, 0xD33B, 0xD33C, 0xD33D, 0xD344, 0xD345, 0xD37C, 0xD37D, 0xD380, 0xD384,
    0xD38C, 0xD38D, 0xD38F, 0xD390, 0xD391, 0xD398, 0xD399, 0xD39C, 0xD3A0, 0xD3A8,
    0xD3A9, 0xD3AB, 0xD3AD, 0xD3B4, 0xD3B8, 0xD3BC, 0xD3C4, 0xD3C5, 0xD3C8, 0xD3C9,
    0xD3D0, 0xD3D8, 0xD3E1, 0xD3E3, 0xD3EC, 0xD3ED, 0xD3F0, 0xD3F4, 0xD3FC, 0xD3FD,
    0xD3FF, 0xD401, 0xD408, 0xD41D, 0xD440, 0xD444, 0xD45C, 0xD460, 0xD464, 0xD46D,
    0xD46F, 0xD478, 0xD479, 0xD47C, 0xD47F, 0xD480, 0xD482, 0xD488, 0xD489, 0xD48B,
    0xD48D, 0xD494, 0xD4A9, 0xD4CC, 0xD4D0, 0xD4D4, 0xD4DC, 0xD4DF, 0xD4E8, 0xD4EC,
    0xD4F0, 0xD4F8, 0xD4FB, 0xD4FD, 0xD504, 0xD508, 0xD50C, 0xD514, 0xD515, 0xD517,
    0xD53C, 0xD53D, 0xD540, 0xD544, 0xD54C, 0xD54D, 0xD54F, 0xD551, 0xD558, 0xD559,
    0xD55C, 0xD560, 0xD565, 0xD568, 0xD569, 0xD56B, 0xD56D, 0xD574, 0xD575, 0xD578,
    0xD57C, 0xD584, 0xD585, 0xD587, 0xD588, 0xD589, 0xD590, 0xD5A5, 0xD5C8, 0xD5C9,
    0xD5CC, 0xD5D0, 0xD5D2, 0xD5D8, 0xD5D9, 0xD5DB, 0xD5DD, 0xD5E4, 0xD5E5, 0xD5E8,
    0xD5EC, 0xD5F4, 0xD5F5, 0xD5F7, 0xD5F9, 0xD600, 0xD601, 0xD604, 0xD608, 0xD610,
    0xD611, 0xD613, 0xD614, 0xD615, 0xD61C, 0xD620, 0xD624, 0xD62D, 0xD638, 0xD639,
    0xD63C, 0xD640, 0xD645, 0xD648, 0xD649, 0xD64B, 0xD64D, 0xD651, 0xD654, 0xD655,
    0xD658, 0xD65C, 0xD667, 0xD669, 0xD670, 0xD671, 0xD674, 0xD683, 0xD685, 0xD68C,
    0xD68D, 0xD690, 0xD694, 0xD69D, 0xD69F, 0xD6A1, 0xD6A8, 0xD6AC, 0xD6B0, 0xD6B9,
    0xD6BB, 0xD6C4, 0xD6C5, 0xD6C8, 0xD6CC, 0xD6D1, 0xD6D4, 0xD6D7, 0xD6D9, 0xD6E0,
    0xD6E4, 0xD6E8, 0xD6F0, 0xD6F5, 0xD6FC, 0xD6FD, 0xD700, 0xD704, 0xD711, 0xD718,
    0xD719, 0xD71C, 0xD720, 0xD728, 0xD729, 0xD72B, 0xD72D, 0xD734, 0xD735, 0xD738,
    0xD73C, 0xD744, 0xD747, 0xD749, 0xD750, 0xD751, 0xD754, 0xD756, 0xD757, 0xD758,
    0xD759, 0xD760, 0xD761, 0xD763, 0xD765, 0xD769, 0xD76C, 0xD770, 0xD774, 0xD77C,
    0xD77D, 0xD781, 0xD788, 0xD789, 0xD78C, 0xD790, 0xD798, 0xD799, 0xD79B, 0xD79D,
}

# Set based on the character set of the Hahmlet Fonts Project (Version 1.002)
# https://github.com/hyper-type/hahmlet#hahmlet-fonts-project
OTHER_COMMON_HANGUL_SYLLABLES_CODEPOINTS = {
    0xAC0B, 0xAC23, 0xAC65, 0xAC82, 0xAC8D, 0xACB7, 0xACBF, 0xACD8, 0xACF9, 0xAD10,
    0xAD12, 0xAD22, 0xAD60, 0xAD65, 0xAD83, 0xADB8, 0xADD5, 0xADEC, 0xADED, 0xAE02,
    0xAE07, 0xAE0F, 0xAE11, 0xAE13, 0xAE18, 0xAE29, 0xAE44, 0xAE6F, 0xAE86, 0xAE8D,
    0xAEA0, 0xAEA4, 0xAED3, 0xAED5, 0xAF09, 0xAF33, 0xAF45, 0xAF4C, 0xAF68, 0xAF78,
    0xAFCF, 0xAFD8, 0xB030, 0xB03C, 0xB060, 0xB064, 0xB090, 0xB0BB, 0xB0D7, 0xB0E1,
    0xB0E3, 0xB0EC, 0xB10F, 0xB111, 0xB122, 0xB12B, 0xB13E, 0xB147, 0xB171, 0xB181,
    0xB191, 0xB1A5, 0xB1B0, 0xB1C3, 0xB1C4, 0xB1CD, 0xB1E1, 0xB1F8, 0xB20D, 0xB21D,
    0xB24F, 0xB27B, 0xB297, 0xB2A7, 0xB2AB, 0xB2BC, 0xB2BF, 0xB2C1, 0xB2CF, 0xB2DE,
    0xB2E0, 0xB2FD, 0xB316, 0xB320, 0xB331, 0xB36F, 0xB40F, 0xB42C, 0xB42D, 0xB457,
    0xB45A, 0xB499, 0xB4B8, 0xB4E5, 0xB4E7, 0xB4FC, 0xB500, 0xB52B, 0xB52E, 0xB532,
    0xB537, 0xB54E, 0xB568, 0xB584, 0xB588, 0xB594, 0xB599, 0xB620, 0xB621, 0xB623,
    0xB62D, 0xB630, 0xB647, 0xB65C, 0xB6A7, 0xB6AF, 0xB6B8, 0xB704, 0xB70C, 0xB733,
    0xB73D, 0xB73E, 0xB743, 0xB761, 0xB783, 0xB79F, 0xB7B0, 0xB7B2, 0xB7C5, 0xB7D0,
    0xB7D4, 0xB7F2, 0xB7F3, 0xB81C, 0xB821, 0xB82B, 0xB863, 0xB879, 0xB894, 0xB908,
    0xB924, 0xB98F, 0xB990, 0xB994, 0xB9C0, 0xB9C6, 0xB9D4, 0xB9DC, 0xB9DF, 0xB9E2,
    0xB9EB, 0xB9FB, 0xB9FD, 0xBA04, 0xBA10, 0xBA4C, 0xBA5B, 0xBA6B, 0xBA77, 0xBA90,
    0xBAAF, 0xBAB1, 0xBAE0, 0xBAF4, 0xBB0C, 0xBB64, 0xBB65, 0xBB7F, 0xBB8A, 0xBB99,
    0xBB9B, 0xBBB9, 0xBBC1, 0xBBD1, 0xBBD5, 0xBBDC, 0xBBE0, 0xBBED, 0xBBF1, 0xBC23,
    0xBC28, 0xBC2B, 0xBC32, 0xBC37, 0xBC5C, 0xBC5F, 0xBC61, 0xBC98, 0xBC9D, 0xBC9F,
    0xBCCC, 0xBCD3, 0xBCFB, 0xBCFD, 0xBCFE, 0xBCFF, 0xBD23, 0xBD25, 0xBD5C, 0xBD74,
    0xBD94, 0xBD9D, 0xBDA0, 0xBDAD, 0xBDB4, 0xBDC1, 0xBDE5, 0xBE01, 0xBE15, 0xBE21,
    0xBE28, 0xBE29, 0xBE30, 0xBE3D, 0xBE4B, 0xBE58, 0xBE67, 0xBE69, 0xBE7E, 0xBE9C,
    0xBED9, 0xBEF0, 0xBF0C, 0xBF53, 0xBFB1, 0xBFD5, 0xBFDD, 0xC020, 0xC06D, 0xC0C6,
    0xC0CF, 0xC0FE, 0xC101, 0xC13F, 0xC151, 0xC171, 0xC180, 0xC181, 0xC193, 0xC1F4,
    0xC1F5, 0xC216, 0xC28C, 0xC2AB, 0xC2B2, 0xC2C0, 0xC2C1, 0xC2E5, 0xC2F0, 0xC2F3,
    0xC2FF, 0xC330, 0xC331, 0xC382, 0xC385, 0xC394, 0xC3A0, 0xC3A4, 0xC3B5, 0xC3BC,
    0xC3DA, 0xC3FC, 0xC40B, 0xC440, 0xC45D, 0xC490, 0xC4A0, 0xC4AC, 0xC4D4, 0xC503,
    0xC52B, 0xC53C, 0xC53F, 0xC54B, 0xC54F, 0xC55C, 0xC590, 0xC5AC, 0xC5AD, 0xC5BA,
    0xC5CF, 0xC5E4, 0xC60F, 0xC61D, 0xC626, 0xC62B, 0xC62F, 0xC64E, 0xC658, 0xC66D,
    0xC670, 0xC68C, 0xC6B7, 0xC6C2, 0xC6C7, 0xC6DF, 0xC6FB, 0xC718, 0xC73E, 0xC743,
    0xC747, 0xC74E, 0xC769, 0xC76C, 0xC76D, 0xC77B, 0xC78C, 0xC78D, 0xC7F5, 0xC802,
    0xC807, 0xC809, 0xC80B, 0xC814, 0xC839, 0xC842, 0xC84B, 0xC890, 0xC8A6, 0xC8AC,
    0xC8C5, 0xC96D, 0xC981, 0xC992, 0xC9A4, 0xC9D4, 0xC9D7, 0xC9E3, 0xC9F2, 0xCA24,
    0xCA30, 0xCA70, 0xCAC3, 0xCAD2, 0xCADC, 0xCB27, 0xCB2C, 0xCB2D, 0xCB32, 0xCB5B,
    0xCB80, 0xCB93, 0xCBCD, 0xCBD5, 0xCC13, 0xCC1F, 0xCC26, 0xCC3F, 0xCCCA, 0xCCE1,
    0xCCE5, 0xCD23, 0xCD25, 0xCD35, 0xCD40, 0xCDCD, 0xCDD4, 0xCE05, 0xCE29, 0xCE3C,
    0xCE62, 0xCE6C, 0xCE6E, 0xCE70, 0xCE7B, 0xCEA8, 0xCEB0, 0xCEC4, 0xCEFD, 0xCF14,
    0xCF18, 0xCF19, 0xCF5B, 0xCF83, 0xCFC8, 0xCFDC, 0xCFFD, 0xD00C, 0xD01C, 0xD020,
    0xD072, 0xD084, 0xD088, 0xD0B8, 0xD0C7, 0xD114, 0xD13B, 0xD166, 0xD170, 0xD17B,
    0xD1A7, 0xD1FB, 0xD225, 0xD236, 0xD277, 0xD2CD, 0xD2D1, 0xD311, 0xD313, 0xD324,
    0xD32F, 0xD35D, 0xD396, 0xD3B5, 0xD3D4, 0xD405, 0xD409, 0xD519, 0xD520, 0xD535,
    0xD550, 0xD564, 0xD570, 0xD571, 0xD573, 0xD57B, 0xD58B, 0xD58F, 0xD594, 0xD5A3,
    0xD5AC, 0xD5C0, 0xD5D7, 0xD5E0, 0xD5E1, 0xD5E3, 0xD5F8, 0xD5FF, 0xD665, 0xD668,
    0xD6BD, 0xD6CD, 0xD6D0, 0xD6D3, 0xD6D5, 0xD75D, 0xD79C, 0xD7A3,
}

# "Hangul Syllables" Unicode v2.0 block (Full range: U+AC00..U+D7AF)
# https://en.wikipedia.org/wiki/Hangul_Syllables
ALL_HANGUL_SYLLABLES_CODEPOINTS = set(range(0xAC00, 0xD7A3 + 1))

LESS_COMMON_HANGUL_SYLLABLES_CODEPOINTS = (
    ALL_HANGUL_SYLLABLES_CODEPOINTS
    - MODERN_HANGUL_SYLLABLES_CODEPOINTS
    - OTHER_COMMON_HANGUL_SYLLABLES_CODEPOINTS
)

assert len(ALL_HANGUL_SYLLABLES_CODEPOINTS) == 11172
assert len(MODERN_HANGUL_SYLLABLES_CODEPOINTS) == 2350
assert len(OTHER_COMMON_HANGUL_SYLLABLES_CODEPOINTS) == 438  # 438 + 2350 = 2788
assert len(LESS_COMMON_HANGUL_SYLLABLES_CODEPOINTS) == 8384


# Lib/fontbakery/designers_pb2.py

# Generated by the protocol buffer compiler.  DO NOT EDIT!
# source: designers.proto

import sys
_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from google.protobuf import reflection as _reflection
from google.protobuf import symbol_database as _symbol_database
# @@protoc_insertion_point(imports)

_sym_db = _symbol_database.Default()




DESCRIPTOR = _descriptor.FileDescriptor(
  name='designers.proto',
  package='',
  syntax='proto2',
  serialized_options=None,
  serialized_pb=_b('\n\x0f\x64\x65signers.proto\"Q\n\x11\x44\x65signerInfoProto\x12\x10\n\x08\x64\x65signer\x18\x01 \x01(\t\x12\x0c\n\x04link\x18\x02 \x01(\t\x12\x1c\n\x06\x61vatar\x18\x03 \x01(\x0b\x32\x0c.AvatarProto\" \n\x0b\x41vatarProto\x12\x11\n\tfile_name\x18\x01 \x01(\t')
)




_DESIGNERINFOPROTO = _descriptor.Descriptor(
  name='DesignerInfoProto',
  full_name='DesignerInfoProto',
  filename=None,
  file=DESCRIPTOR,
  containing_type=None,
  fields=[
    _descriptor.FieldDescriptor(
      name='designer', full_name='DesignerInfoProto.designer', index=0,
      number=1, type=9, cpp_type=9, label=1,
      has_default_value=False, default_value=_b("").decode('utf-8'),
      message_type=None, enum_type=None, containing_type=None,
      is_extension=False, extension_scope=None,
      serialized_options=None, file=DESCRIPTOR),
    _descriptor.FieldDescriptor(
      name='link', full_name='DesignerInfoProto.link', index=1,
      number=2, type=9, cpp_type=9, label=1,
      has_default_value=False, default_value=_b("").decode('utf-8'),
      message_type=None, enum_type=None, containing_type=None,
      is_extension=False, extension_scope=None,
      serialized_options=None, file=DESCRIPTOR),
    _descriptor.FieldDescriptor(
      name='avatar', full_name='DesignerInfoProto.avatar', index=2,
      number=3, type=11, cpp_type=10, label=1,
      has_default_value=False, default_value=None,
      message_type=None, enum_type=None, containing_type=None,
      is_extension=False, extension_scope=None,
      serialized_options=None, file=DESCRIPTOR),
  ],
  extensions=[
  ],
  nested_types=[],
  enum_types=[
  ],
  serialized_options=None,
  is_extendable=False,
  syntax='proto2',
  extension_ranges=[],
  oneofs=[
  ],
  serialized_start=19,
  serialized_end=100,
)


_AVATARPROTO = _descriptor.Descriptor(
  name='AvatarProto',
  full_name='AvatarProto',
  filename=None,
  file=DESCRIPTOR,
  containing_type=None,
  fields=[
    _descriptor.FieldDescriptor(
      name='file_name', full_name='AvatarProto.file_name', index=0,
      number=1, type=9, cpp_type=9, label=1,
      has_default_value=False, default_value=_b("").decode('utf-8'),
      message_type=None, enum_type=None, containing_type=None,
      is_extension=False, extension_scope=None,
      serialized_options=None, file=DESCRIPTOR),
  ],
  extensions=[
  ],
  nested_types=[],
  enum_types=[
  ],
  serialized_options=None,
  is_extendable=False,
  syntax='proto2',
  extension_ranges=[],
  oneofs=[
  ],
  serialized_start=102,
  serialized_end=134,
)

_DESIGNERINFOPROTO.fields_by_name['avatar'].message_type = _AVATARPROTO
DESCRIPTOR.message_types_by_name['DesignerInfoProto'] = _DESIGNERINFOPROTO
DESCRIPTOR.message_types_by_name['AvatarProto'] = _AVATARPROTO
_sym_db.RegisterFileDescriptor(DESCRIPTOR)

DesignerInfoProto = _reflection.GeneratedProtocolMessageType('DesignerInfoProto', (_message.Message,), dict(
  DESCRIPTOR = _DESIGNERINFOPROTO,
  __module__ = 'designers_pb2'
  # @@protoc_insertion_point(class_scope:DesignerInfoProto)
  ))
_sym_db.RegisterMessage(DesignerInfoProto)

AvatarProto = _reflection.GeneratedProtocolMessageType('AvatarProto', (_message.Message,), dict(
  DESCRIPTOR = _AVATARPROTO,
  __module__ = 'designers_pb2'
  # @@protoc_insertion_point(class_scope:AvatarProto)
  ))
_sym_db.RegisterMessage(AvatarProto)


# @@protoc_insertion_point(module_scope)


# Lib/fontbakery/errors.py

import traceback


class FontBakeryRunnerError(Exception):
    pass


class CircularDependencyError(FontBakeryRunnerError):
    pass


class APIViolationError(FontBakeryRunnerError):
    def __init__(self, message, result, *args):
        self.message = message
        self.result = result
        super().__init__(message, *args)


class ProtocolViolationError(FontBakeryRunnerError):
    def __init__(self, message, *args):
        self.message = message
        super().__init__(message, *args)


class FailedCheckError(FontBakeryRunnerError):
    def __init__(self, error, *args):
        message = f"Failed with {type(error).__name__}: {error}"
        self.error = error
        self.traceback = "".join(traceback.format_tb(error.__traceback__))
        super().__init__(message, *args)


class FailedConditionError(FontBakeryRunnerError):
    """This is a serious problem with the check suite profile
    and it must be solved.
    """

    def __init__(self, condition, error, *args):
        message = (
            f"The condition {condition} had an error:"
            f" {type(error).__name__}: {error}"
        )
        self.condition = condition
        self.error = error
        self.traceback = "".join(traceback.format_tb(error.__traceback__))
        super().__init__(message, *args)


class MissingConditionError(FontBakeryRunnerError):
    """This is a serious problem with the check suite profile
    and it must be solved, most probably a typo.
    """

    def __init__(self, condition_name, error, *args):
        message = (
            f"The condition named {condition_name} is missing:"
            f" {type(error).__name__}: {error}"
        )
        self.error = error
        self.traceback = "".join(traceback.format_tb(error.__traceback__))
        super().__init__(message, *args)


class FailedDependenciesError(FontBakeryRunnerError):
    def __init__(self, check, error, *args):
        message = f"The check {check} had an error:" f" {type(error).__name__}: {error}"
        self.check = check
        self.error = error
        self.traceback = "".join(traceback.format_tb(error.__traceback__))
        super().__init__(message, *args)


class SetupError(FontBakeryRunnerError):
    pass


class MissingValueError(FontBakeryRunnerError):
    pass


class CircularAliasError(FontBakeryRunnerError):
    pass


class NamespaceError(FontBakeryRunnerError):
    pass


class ValueValidationError(FontBakeryRunnerError):
    pass


# Lib/fontbakery/fonts_profile.py

"""
Font Bakery CheckRunner is the driver of a font bakery suite of checks.
"""
import glob
import logging
import argparse
from dataclasses import dataclass

from fontbakery.callable import FontBakeryExpectedValue as ExpectedValue
from fontbakery.profile import Profile
from fontbakery.errors import ValueValidationError


@dataclass
class FileDescription:
    name: str
    extensions: list
    singular: str
    description: str


class FontsProfile(Profile):
    accepted_files = [
        FileDescription(name="fonts",
                        singular="font",
                        extensions=[".otf",".ttf"],
                        description="OpenType binary"),
        FileDescription(name="ufos",
                        singular="ufo",
                        extensions=[".ufo"],
                        description="UFO source"),
        FileDescription(name="designspaces",
                        singular="designspace",
                        extensions=[".designspace"],
                        description="Designspace"),
        FileDescription(name="glyphs_files",
                        singular="glyphs_file",
                        extensions=[".glyphs"],
                        description="Glyphs source"),
        FileDescription(name="readme_md",
                        singular="readme_md",
                        extensions=["README.md"],
                        description="Project's README markdown file"),
        FileDescription(name="metadata_pb",
                        singular="metadata_pb",
                        extensions=["METADATA.pb"],
                        description="Project's METADATA protobuf file"),
    ]

    def setup_argparse(self, argument_parser):
        """
        Set up custom arguments needed for this profile.
        """
        profile = self

        def get_files(pattern):
            files_to_check = []
            # use glob.glob to accept *.ttf
            # but perform a hacky fixup to workaround the square-brackets naming scheme
            # currently in use for varfonts in google fonts...
            if '].ttf' in pattern:
                pattern = "*.ttf".join(pattern.split('].ttf'))

            # Everything goes in for now, gets sorted in the Merge
            for fullpath in glob.glob(pattern):
                files_to_check.append(fullpath)
            return files_to_check


        class MergeAction(argparse.Action):
            def __call__(self, parser, namespace, values, option_string=None):
                for file_description in profile.accepted_files:
                    setattr(namespace, file_description.name, [])
                target = [item for l in values for item in l]
                any_accepted = False
                for file in target:
                    accepted = False
                    for file_description in profile.accepted_files:
                        if any([file.endswith(extension) for extension in file_description.extensions]):
                          setattr(namespace, file_description.name, getattr(namespace, file_description.name) + [file])
                          accepted = True
                          any_accepted = True
                    if not accepted:
                        logging.info(f"Skipping '{file}' as it does not"
                                     f" seem to be accepted by this profile.")
                if not any_accepted:
                    raise ValueValidationError('No applicable files found')

        argument_parser.add_argument(
            'files',
            # To allow optional commands like "-L" to work without other input
            # files:
            nargs='*',
            type=get_files,
            action=MergeAction,
            help='file path(s) to check. Wildcards like *.ttf are allowed.')

        return tuple(x.name for x in self.accepted_files)

    def get_family_checks(self):
        family_checks = self.get_checks_by_dependencies('fonts')
        family_checks.extend(self.get_checks_by_dependencies('ttFonts'))
        return family_checks

    @classmethod
    def _expected_values(cls):
        return { val.name:
          ExpectedValue(val.name,
                        default = [],
                        description = f"A list of the {val.description} file paths to check",
                        force=True
                        )
          for val in cls.accepted_files
        }

    @classmethod
    def _iterargs(cls):
        return { val.singular: val.name for val in cls.accepted_files }


def profile_factory(**kwds):
    from fontbakery.profiles.shared_conditions import ttFont
    profile = FontsProfile(
        iterargs=FontsProfile._iterargs()
      , conditions={ttFont.name: ttFont}
      , derived_iterables={'ttFonts': ('ttFont', True)}
      , expected_values=FontsProfile._expected_values()
      , **kwds
    )
    return profile


# Lib/fontbakery/message.py

# These constants are merely meant to be used
# so that the check_override declarations are more readable:
KEEP_ORIGINAL_STATUS = None
KEEP_ORIGINAL_MESSAGE = None


class Message:
    """Status messages to be yielded by FontBakeryCheck"""
    def __init__(self, code, message):
        """
          code: (string|number) a check internal, unique code to describe a
                specific failure condition. A short string is preferred, it
                makes a better experience to read the code.
                Never use the same code at two different yields, this is meant
                for unit testing to identify a specific condition of the check.
                A yield within a loop would (usually) have the same code for
                each iteration, unless there's a good reason not to do so.

          message: (string) human readable message.

          In the future, this class could be extended to hold even more
          information if useful, e.g. a hint how to fix a specific condition.
        """
        self.code = code
        self.message = message

    def __repr__(self):
        return f'{self.message} [code: {self.code}]'

    def getData(self):
        """ return a dictionary with data suitable for serialization,
            i.e. only stuff that is allowed in JSON.
        """
        return {
            'code': self.code
          , 'message': self.message
        }


# Lib/fontbakery/multiproc.py

"""
Separation of Concerns Disclaimer:
While created specifically for checking fonts and font-families this
module has no domain knowledge about fonts. It can be used for any kind
of (document) checking. Please keep it so. It will be valuable for other
domains as well.

Domain specific knowledge should be encoded only in the Profile (Checks,
Conditions) and MAYBE in *customized* reporters e.g. subclasses.
"""

from collections import OrderedDict
from contextlib import contextmanager
from functools import partial
from multiprocessing import Process, Queue
import queue

from fontbakery.reporters import FontbakeryReporter
from fontbakery.checkrunner import (  # NOQA
              INFO
            , WARN
            , ERROR
            , STARTCHECK
            , SKIP
            , PASS
            , FAIL
            , ENDCHECK
            , END
            , DEBUG
            , CheckRunner
            , session_protocol_generator
            , drive_session_protocol
            , get_profile_from_module_locator
            )
from fontbakery.message import Message


################
# WORKER/CHILD #
################

name2status = {status.name:status for status in \
               (DEBUG, PASS, SKIP, INFO, WARN, FAIL, ERROR)}

# Similar to DashbordWorkerReporter of Font Bakery Dashboard.
class WorkerToQueueReporter(FontbakeryReporter):
    def __init__(self, queue, profile, ticks_to_flush = None, **kwd):
        super().__init__(**kwd)
        self._queue = queue
        self._profile = profile
        self.ticks_to_flush = ticks_to_flush or 1

        self._current = None
        self._collectedChecks = None

    def _register(self, event):
        super()._register(event)
        status, message, identity = event
        _section, check, _iterargs = identity
        if status == END:
            self.flush()
            return

        if not check:
            return

        key = self._profile.serialize_identity(identity)

        if status == STARTCHECK:
            self._current = {
                'statuses': []
            }

        if status == ENDCHECK:
            # Do more? Anything more would make access easier but also be a
            # derivative of the actual data, i.e. not SSOT. Calculating (and
            # thus interpreting) results for the checks is probably not too
            # expensive to do it on the fly.
            self._current['result'] = message.name
            self._save_result(key, self._current)
            self._current = None

        if status >= DEBUG:
            # message can be a lot here, currently we know about:
            #    string, an Exception, a Message. Probably we should leave it
            #    like this. Message should be the ultimate answer if it's not
            #    an Exception or a string.
            # turn everything in a fontbakery/Message like object
            # `code` may be used for overwriting special failing statuses
            # otherwise, code must be none
            #
            # Optional keys are:
            #  "code": used to explicitly overwrite specific (FAIL) statuses
            #  "traceback": only provided if message is an Excepion and likely
            #               if status is "ERROR"
            log = {'status': status.name}

            if hasattr(message, 'traceback'):
                # message is likely a FontbakeryError if this is not None
                log['traceback'] = message.traceback
            if isinstance(message, Message):
                # Ducktyping could be a valid option here.
                # in that case, a FontbakeryError could also provide a `code` attribute
                # which would allow to skip that error explicitly. However
                # ERROR statuses should never be skiped explicitly, the cause
                # of the error must be repaired!
                log.update(message.getData())
            else:
                log['message'] = f'{message}'
            self._current['statuses'].append(log)

    def _save_result(self, key, check_result):
        """ send check_result to the queue"""
        if self._collectedChecks is None:
            # order is not really relevant in multiprocessing, however, we keep
            # it as long as possible, could be good for debugging.
            self._collectedChecks = OrderedDict()
        self._collectedChecks[key] = check_result
        if len(self._collectedChecks) >= self.ticks_to_flush:
            self.flush()

    def flush(self):
        if self._collectedChecks:
            self._queue.put(tuple(self._collectedChecks.items()))
        self._collectedChecks = None

#This is the inverse of the serialization in WorkerToQueueReporter
def check_protocol_from_worker_data(profile, key_check_data):
    key, check_data = key_check_data
    identity = profile.deserialize_identity(key)
    yield STARTCHECK, None, identity  ## = event

    for log in check_data['statuses']:
        status = name2status[log['status']]
        if 'code' in log:
            message = Message(log['code'], log['message'])
        elif 'traceback' in log:
            # not to happy with this generic exception, let's se how it plays out
            message = Exception(log['message'])
            setattr(message, 'traceback', log['traceback'])
        else:
            message = log['message']
        yield status, message, identity ### = event

    status = name2status[check_data['result']]
    yield ENDCHECK, status, identity ## = event

def _worker_jobs_generator(jobs_queue, profile, reporter):
    while True:
        try:
            job = jobs_queue.get(False)
        except queue.Empty:
            # This removes a race condition.
            # The queue looks empty, apparently that must not be the actual case,
            # but since the parent process is counting results to decide when
            # it's done, we flush here, besides the value of ticks_to_flush.
            # before.
            reporter.flush()
            # No held back results anymore, so parent process has complete control
            # and we won't block it in blocking waiting mode.
            job = jobs_queue.get(True)
        yield profile.deserialize_identity(job)

def multiprocessing_worker(jobs_queue, results_queue, profile_module_locator
                         , runner_kwds):
    profile = get_profile_from_module_locator(profile_module_locator)
    runner = CheckRunner(profile, **runner_kwds)
    reporter = WorkerToQueueReporter( results_queue
                                    , profile=profile
                                    , runner=runner
                                    , ticks_to_flush=5
                                    )

    next_check_gen = _worker_jobs_generator(jobs_queue, profile, reporter)
    runner.run_externally_controlled(reporter.receive, next_check_gen)

#####################
# DISPATCHER/PARENT #
#####################

def _results_generator(results_queue, len_results):
    count_results = 0
    while True:
        result_list = results_queue.get()
        for result in result_list:
            yield result
            count_results += 1
        if count_results >= len_results:
            return

@contextmanager
def _multiprocessing_checkrunner(jobs, process_count, *args):
    jobs_queue = Queue()
    results_queue = Queue()
    for job in jobs:
        jobs_queue.put(job)
    len_jobs = len(jobs)

    try:
        processes = []
        for _ in range(process_count):
            p = Process(target=multiprocessing_worker,
                        # NOTE: stuff is pickled here, but that
                        # seems to be no problem despite of
                        # e.g. pickling a profile (see #2982),
                        # which was fixed by using profile.module_locator
                        # instead. The other arguments seem easier
                        # to pickle.
                        args=(jobs_queue, results_queue, *args))
            processes.append(p)
            p.start()
        yield _results_generator(results_queue, len_jobs) # next_check_gen
    finally:
        for p in processes:
            p.terminate()
            p.join()

def multiprocessing_runner(process_count, runner, runner_kwds):
    # process_count is a positive int, never 0 at this point
    assert process_count > 0
    profile = runner.profile
    joblist = list(profile.serialize_order(runner.order))

    session_gen = session_protocol_generator(
                      partial(check_protocol_from_worker_data, profile),
                      runner.order)
    with _multiprocessing_checkrunner(joblist,
                                      process_count,
                                      profile.module_locator,
                                      runner_kwds) as next_check_gen:
        yield from drive_session_protocol(session_gen, next_check_gen)


# Lib/fontbakery/parse.py

"""Parse GF specific style properties for font styles or font instances."""
import re
from collections import namedtuple


__all__ = ["style_parse", "instance_parse"]


RIBBI_STYLES = ["Regular", "Italic", "Bold", "Bold Italic"] 

_WEIGHT_NAMES = {
    r"Th?i?n|wt100": "Thin",
    r"Ext?r?a?Li?g?h?t|wt200": "ExtraLight",
    r"Li?g?h?t|wt300": "Light",
    r"Re?gu?l?a?r?|wt400": "Regular",
    r"Me?di?u?m?|wt500":"Medium",
    r"Se?m?i?Bo?l?d|wt600": "SemiBold",
    r"Bo?l?d|wt700": "Bold",
    r"Ext?r?a?Bo?l?d|wt800": "ExtraBold",
    r"Bla?c?k|wt900": "Black",
    r"Ext?r?a?Bla?c?k|wt1000": "ExtraBlack"
}
_ITALIC_NAMES = {
    r"Ita?l?i?c?|Obli?q?u?e?": "Italic" 
}
_WEIGHT_VALUES = {
    "Thin": {"usWeightClass": 100, "fvar": 100.0},
    "ExtraLight": {"usWeightClass": 200, "fvar": 200.0},
    "Light": {"usWeightClass": 300, "fvar": 300.0},
    "Regular": {"usWeightClass": 400, "fvar": 400.0},
    "": {"usWeightClass": 400, "fvar": 400.0},
    "Medium": {"usWeightClass": 500, "fvar": 500.0},
    "SemiBold": {"usWeightClass": 600, "fvar": 600.0},
    "Bold": {"usWeightClass": 700, "fvar": 700.0},
    "ExtraBold": {"usWeightClass": 800, "fvar": 800.0},
    "Black": {"usWeightClass": 900, "fvar": 900.0},
    "ExtraBlack": {"usWeightClass": 1000, "fvar": 1000.0},
}

def _re_string_tokenizer(string, mapping):
    found = []
    for k, v in mapping.items():
        result = re.search(k, string)
        if result:
            found.append(v)
    if not found:
        return None
    return sorted(found, key=len, reverse=True)[0]


def _style_tokens(string):
    string = re.sub(r"\W", "", string)
    wght = _re_string_tokenizer(string, _WEIGHT_NAMES) or ""
    ital = _re_string_tokenizer(string, _ITALIC_NAMES) or ""
    return wght, ital


def _parse_name(wght, ital):
    if wght == "Regular" and ital == "Italic":
        wght = ""
    result = f"{wght} {ital}".lstrip().rstrip()
    # replace multiple whitespace characters with a single space"
    return re.sub(r"\W+", " ", result)


def _win_style_name(string):
    if "Italic" in string:
        if string in ["Italic", "Bold Italic"]:
            return string
        return "Italic"
    elif string not in ["Regular", "Bold"]:
        return "Regular"
    return string


def _typo_style_name(string):
    if string not in RIBBI_STYLES:
        return string
    return None


def _fsSelection(string):
    result = 0b0
    if string == "Bold":
        result |= 0b100000
    if "Italic" in string:
        result |= 0b1
    if result == 0b0:
        result |= 0b1000000
    return result


def _macStyle(string):
    result = 0b0
    if string == "Bold":
        result |= 0b1
    if "Italic" in string:
        result |= 0b10
    return result


def _style_parse(string):
    """Parse a string into a GF font style object. All parsed properties
    conform to the Google Fonts specification.
    """
    _GFStyle = namedtuple("GFStyle",
                          """name
                          usWeightClass
                          win_style_name
                          mac_style_name
                          typo_style_name
                          fsSelection
                          macStyle
                          is_ribbi
                          filename""")
    wght, ital = _style_tokens(string)
    name = _parse_name(wght, ital)
    return _GFStyle(name=name,
                    usWeightClass=_WEIGHT_VALUES[wght]['usWeightClass'],
                    win_style_name=_win_style_name(name),
                    mac_style_name=name,
                    typo_style_name=_typo_style_name(name),
                    fsSelection=_fsSelection(wght),
                    macStyle=_macStyle(wght),
                    is_ribbi=name in RIBBI_STYLES,
                    filename=name.replace(" ", ""))


def style_parse(ttFont):
    """Get style properites from a TTFont object. If the font is a static font,
    use the filename to get style info, else if the font is a variable font,
    use the default instance's subfamilyNameID record"""
    if 'fvar' in ttFont:
        dflt_instance_coords = {a.axisTag: a.defaultValue for a in ttFont['fvar'].axes}
        for instance in ttFont['fvar'].instances:
            if instance.coordinates == dflt_instance_coords:
                name = ttFont['name'].getName(instance.subfamilyNameID, 3, 1, 1033).toUnicode()
                return _style_parse(name)
    import os
    filename = os.path.basename(ttFont.reader.file.name)
    if len(filename.split("-")) != 2:
        # Google Fonts policy on font file naming scheme
        # requires that only a single dash is used
        # to separate family name from style.
        return None
    else:
        style = filename.split("-")[1].split(".")[0]
        return _style_parse(style)


def instance_parse(string):
    """Derive instance properties from a string."""
    _GFInstance = namedtuple("GFStyle",
                             """name
                             coordinates
                             unparsable_tokens
                             raw_token_order
                             expected_token_order""")
    wght, ital = _style_tokens(string)
    if not wght:
        wght = "Regular"
    name = _parse_name(wght, ital)
    coords = {"wght": _WEIGHT_VALUES[wght]['fvar']}
    return _GFInstance(
        name=name,
        coordinates=coords,
        unparsable_tokens=_unparsable_tokens(string),
        raw_token_order=_token_order(string),
        expected_token_order=_token_order(name))


def _unparsable_tokens(string):
    string = string.split()
    results = []
    for word in string:
        if not any(_style_tokens(word)):
            results.append(word)
    return results


def _token_order(string):
    tokens = string.split()
    results = []
    for token in tokens:
        if _re_string_tokenizer(token, _WEIGHT_NAMES):
            results.append('wght')
        if "Italic" == token:
            results.append('ital')
    return results



# Lib/fontbakery/profile.py

from typing import Iterable
from collections import OrderedDict
from functools import wraps, partial
from itertools import chain
import re
import importlib
import inspect
import types
import json
import logging

from fontbakery.errors import NamespaceError, SetupError, CircularAliasError
from fontbakery.callable import (
    FontbakeryCallable,
    FontBakeryCheck,
    FontBakeryCondition,
    FontBakeryExpectedValue,
)
from fontbakery.configuration import Configuration
from fontbakery.message import Message
from fontbakery.section import Section
from fontbakery.utils import is_negated
from fontbakery.status import DEBUG


def get_module_profile(module, name=None):
    """
    Get or create a profile from a module and return it.

    If the name `module.profile` is present the value of that is returned.
    Otherwise, if the name `module.profile_factory` is present, a new profile
    is created using `module.profile_factory` and then `profile.auto_register`
    is called with the module namespace.
    If neither name is defined, the module is not considered a profile-module
    and None is returned.

    TODO: describe the `name` argument and better define the signature of `profile_factory`.

    The `module` argument is expected to behave like a python module.
    The optional `name` argument is used when `profile_factory` is called to
    give a name to the default section of the new profile. If name is not
    present `module.__name__` is the fallback.

    `profile_factory` is called like this:
        `profile = module.profile_factory(default_section=default_section)`

    """
    try:
        # if profile is defined we just use it
        return module.profile
    except AttributeError:  # > 'module' object has no attribute 'profile'
        # try to create one on the fly.
        # e.g. module.__name__ == "fontbakery.profiles.cmap"
        if "profile_factory" not in module.__dict__:
            return None
        default_section = Section(name or module.__name__)
        profile = module.profile_factory(
            default_section=default_section, module_spec=module.__spec__
        )
        profile.auto_register(module.__dict__)
        return profile


class Profile:
    """
    Profiles may specify default configuration values (used to parameterize
    checks), which are then overridden by values in the user's configuration
    file.
    """
    configuration_defaults = {}

    def __init__(
        self,
        sections=None,
        iterargs=None,
        derived_iterables=None,
        conditions=None,
        aliases=None,
        expected_values=None,
        default_section=None,
        check_skip_filter=None,
        profile_tag=None,
        module_spec=None,
    ):
        """
          sections: a list of sections, which are ideally ordered sets of
              individual checks.
              It makes no sense to have checks repeatedly, they yield the same
              results anyway, thus we don't allow this.
          iterargs: maping 'singular' variable names to the iterable in values
              e.g.: `{'font': 'fonts'}` in this case fonts must be iterable AND
              'font' may not be a value NOR a condition name.
          derived_iterables: a dictionary {"plural": ("singular", bool simple)}
              where singular points to a condition, that consumes directly or indirectly
              iterargs. plural will be a list of all values the condition produces
              with all combination of it's iterargs.
              If simple is False, the result returns tuples of: (iterars, value)
              where iterargs is a tuple of ('iterargname', number index)
              Especially for cases where only one iterarg is involved, simple
              can be set to True and the result list will just contain the values.
              Example:

              @condition
              def ttFont(font):
                  return TTFont(font)

              values={'fonts': ['font_0', 'font_1']}
              iterargs={'font': 'fonts'}

              derived_iterables={'ttFonts': ('ttFont', True)}
              # Then:
              ttfons = (
                  <TTFont object from font_0>
                , <TTFont object from font_1>
              )

              # However
              derived_iterables={'ttFonts': ('ttFont', False)}
              ttfons = [
                  ((('font', 0), ), <TTFont object from font_0>)
                , ((('font', 1), ), <TTFont object from font_1>)
              ]

        We will:
          a) get all needed values/variable names from here
          b) add some validation, so that we know the values match
             our expectations! These values must be treated as user input!
        """
        self._namespace = {
            "config": "config"  # Filled in by checkrunner
        }

        self.iterargs = {}
        if iterargs:
            self._add_dict_to_namespace("iterargs", iterargs)

        self.derived_iterables = {}
        if derived_iterables:
            self._add_dict_to_namespace("derived_iterables", derived_iterables)

        self.aliases = {}
        if aliases:
            self._add_dict_to_namespace("aliases", aliases)

        self.conditions = {}
        if conditions:
            self._add_dict_to_namespace("conditions", conditions)

        self.expected_values = {}
        if expected_values:
            self._add_dict_to_namespace("expected_values", expected_values)

        self._check_registry = {}
        self._sections = OrderedDict()
        if sections:
            for section in sections:
                self.add_section(section)

        if not default_section:
            default_section = (
                sections[0] if sections and len(sections) else Section("Default")
            )
        self._default_section = default_section
        self.add_section(self._default_section)

        # currently only used for new check ids in self.check_log_override
        # only a-z everything else is deleted
        self.profile_tag = re.sub(
            r"[^a-z]", "", (profile_tag or self._default_section.name).lower()
        )

        self._check_skip_filter = check_skip_filter

        # Used in multiprocessing because pickling the profiles fail on
        # Mac and Windows. See: googlefonts/fontbakery#2982
        # module_locator can actually a module.__spec__ but also just a dict
        # self.module_locator will always be just a dict
        if module_spec is None:
            # This is a bit of a hack, but the idea is to reduce boilerplate
            # when writing modules that directly define a profile.
            try:
                frame = inspect.currentframe().f_back
                while frame:
                    # Note, if __spec__ is a local variable we shpuld be at a
                    # module top level. It should also be the correct ModuleSpec
                    # according to how we do this "usually" (as documented and
                    # practiced as far as I'm aware of), e.g. the profile module
                    # defines a profile object directly by calling a Profile constructor
                    # (e.g. profies.ufo_sources) or indirectly via a profile_factory
                    # (e.g. profiles.google_fonts). Otherwise, if this fails
                    # or finds a wrong ModuleSpec, there's still the option to
                    # pass module_spec as an argument (module.__spec__), which is
                    # actually demonstrated in get_module_profile.
                    if "__spec__" in frame.f_locals:
                        module_spec = frame.f_locals["__spec__"]
                        if module_spec and isinstance(
                            module_spec, importlib.machinery.ModuleSpec
                        ):
                            break
                        module_spec = None  # reset
                    frame = frame.f_back
            finally:
                del frame

        # If not module_spec: this is only a problem in multiprocessing, in
        # that case we'll be failing to access this with an AttributeError.
        if module_spec is not None:
            self.module_locator = dict(name=module_spec.name, origin=module_spec.origin)

    _valid_namespace_types = {
        "iterargs": "iterarg",
        "derived_iterables": "derived_iterable",
        "aliases": "alias",
        "conditions": "condition",
        "expected_values": "expected_value",
    }

    @property
    def sections(self):
        return self._sections.values()

    def _add_dict_to_namespace(self, type, data):
        for key, value in data.items():
            self.add_to_namespace(
                type, key, value, force=getattr(value, "force", False)
            )

    def add_to_namespace(self, type, name, value, force=False):
        if type not in self._valid_namespace_types:
            valid_types = ", ".join(self._valid_namespace_types)
            raise TypeError(f'Unknow type "{type}"' f" Valid types are: {valid_types}")

        if name in self._namespace:
            registered_type = self._namespace[name]
            registered_value = getattr(self, registered_type)[name]
            if type == registered_type and registered_value == value:
                # if the registered equals: skip silently. Registering the same
                # value multiple times is allowed, so we can easily expand profiles
                # that define (partly) the same entries
                return

            if not force:
                msg = (
                    f'Name "{name}" is already registered'
                    f' in "{registered_type}" (value: {registered_value}).'
                    f' Requested registering in "{type}" (value: {value}).'
                )
                raise NamespaceError(msg)
            else:
                # clean the old type up
                del getattr(self, registered_type)[name]

        self._namespace[name] = type
        target = getattr(self, type)
        target[name] = value

    def test_dependencies(self):
        """Raises SetupError if profile uses any names that are not declared
        in the its namespace.
        """
        seen = set()
        failed = []
        # make this simple, collect all used names
        for section_name, section in self._sections.items():
            for check in section.checks:
                dependencies = list(check.args)
                if hasattr(check, "conditions"):
                    dependencies += [
                        name for negated, name in map(is_negated, check.conditions)
                    ]

                while dependencies:
                    name = dependencies.pop()
                    if name in seen:
                        continue
                    seen.add(name)
                    if name not in self._namespace:
                        failed.append(name)
                        continue
                    # if this is a condition, expand its dependencies
                    condition = self.conditions.get(name, None)
                    if condition is not None:
                        dependencies += condition.args
        if len(failed):
            comma_separated = ", ".join(failed)
            raise SetupError(
                f"Profile uses names that are not declared"
                f" in its namespace: {comma_separated}."
            )

    def test_expected_checks(self, expected_check_ids, exclusive=False):
        """Self-test to make a sure profile maintainer is aware of changes in
        the profile.
        Raises SetupError if expected check ids are missing in the profile (removed)
        If `exclusive=True` also raises SetupError if check ids are in the
        profile that are not in expected_check_ids (newly added).

        This is handy if `profile.auto_register` is used and the profile maintainer
        is looking for a high level of control over the profile contents,
        especially for a warning when the profile contents have changed after an
        update.
        """
        s = set()
        duplicates = set(x for x in expected_check_ids if x in s or s.add(x))
        if len(duplicates):
            raise SetupError(
                "Profile has duplicated entries in its list"
                " of expected check IDs:\n" + "\n".join(duplicates)
            )

        expected_check_ids = set(expected_check_ids)
        registered_checks = set(self._check_registry.keys())
        missing_checks = expected_check_ids - registered_checks
        unexpected_checks = None
        if exclusive:
            unexpected_checks = registered_checks - expected_check_ids
        message = []
        if missing_checks:
            message.append("missing checks: {};".format(", ".join(missing_checks)))
        if unexpected_checks:
            message.append(
                "unexpected checks: {};".format(", ".join(unexpected_checks))
            )
        if message:
            raise SetupError(
                "Profile fails expected checks test:\n" + "\n".join(message)
            )

        def is_numerical_id(checkid):
            try:
                int(checkid.split("/")[-1])
                return True
            except:
                return False

        numerical_check_ids = [c for c in registered_checks if is_numerical_id(c)]
        if numerical_check_ids:
            list_of_checks = "\t- " + "\n\t- ".join(numerical_check_ids)
            raise SetupError(
                f"\n"
                f"\n"
                f"Numerical check IDs must be renamed to keyword-based IDs:\n"
                f"{list_of_checks}\n"
                f"\n"
                f"See also: https://github.com/googlefonts/fontbakery/issues/2238\n"
                f"\n"
            )

    def resolve_alias(self, original_name):
        name = original_name
        seen = set()
        path = []
        while name in self.aliases:
            if name in seen:
                names = " -> ".join(path)
                raise CircularAliasError(
                    f'Alias for "{original_name}" has'
                    f" a circular reference in {names}"
                )
            seen.add(name)
            path.append(name)
            name = self.aliases[name]
        return name

    def validate_values(self, values):
        """
        Validate values if they are registered as expected_values and present.

        * If they are not registered they shouldn't be used anywhere at all
          because profile can self check (profile.check_dependencies) for
          missing/undefined dependencies.

        * If they are not present in values but registered as expected_values
          either the expected value has a default value OR a request for that
          name will raise a KeyError on runtime. We don't know if all expected
          values are actually needed/used, thus this fails late.
        """
        messages = []
        for name, value in values.items():
            if name not in self.expected_values:
                continue
            valid, message = self.expected_values[name].validate(value)
            if valid:
                continue
            messages.append(f"{name}: {message} (value: {value})")
        if len(messages):
            return False, "\n".join(messages)
        return True, None

    def get_type(self, name, *args):
        has_fallback = bool(args)
        if has_fallback:
            fallback = args[0]

        if not name in self._namespace:
            if has_fallback:
                return fallback
            raise KeyError(name)

        return self._namespace[name]

    def get(self, name, *args):
        has_fallback = bool(args)
        if has_fallback:
            fallback = args[0]

        try:
            target_type = self.get_type(name)
        except KeyError:
            if not has_fallback:
                raise
            return fallback

        target = getattr(self, target_type)
        if name not in target:
            if has_fallback:
                return fallback
            raise KeyError(name)
        return target[name]

    def has(self, name):
        marker_fallback = object()
        val = self.get(name, marker_fallback)
        return val is not marker_fallback

    def _get_aggregate_args(self, item, key):
        """
        Get all arguments or mandatory arguments of the item.

        Item is a check or a condition, which means it can be dependent on
        more conditions, this climbs down all the way.
        """
        if not key in ("args", "mandatoryArgs"):
            raise TypeError(f'key must be "args" or "mandatoryArgs", got {key}')
        dependencies = list(getattr(item, key))
        if hasattr(item, "conditions"):
            dependencies += [name for negated, name in map(is_negated, item.conditions)]
        args = set()
        while dependencies:
            name = dependencies.pop()
            if name in args:
                continue
            args.add(name)
            # if this is a condition, expand its dependencies
            c = self.conditions.get(name, None)
            if c is None:
                continue
            dependencies += [
                dependency for dependency in getattr(c, key) if dependency not in args
            ]
        return args

    def get_iterargs(self, item):
        """ Returns a tuple of all iterags for item, sorted by name."""
        # iterargs should always be mandatory, unless there's a good reason
        # not to, which I can't think of right now.

        args = self._get_aggregate_args(item, "mandatoryArgs")
        return tuple(sorted([arg for arg in args if arg in self.iterargs]))

    def _analyze_checks(self, all_args, checks):
        args = list(all_args)
        args.reverse()
        # (check, signature, scope)
        scopes = [(check, tuple(), tuple()) for check in checks]
        aggregatedArgs = {
            "args": {
                check.name: self._get_aggregate_args(check, "args") for check in checks
            },
            "mandatoryArgs": {
                check.name: self._get_aggregate_args(check, "mandatoryArgs")
                for check in checks
            },
        }
        saturated = []
        while args:
            new_scopes = []
            # args_set must contain all current args, hence it's before the pop
            args_set = set(args)
            arg = args.pop()
            for check, signature, scope in scopes:
                if not len(aggregatedArgs["args"][check.name] & args_set):
                    # there's no args no more or no arguments of check are
                    # in args
                    target = saturated
                elif (
                    arg == "*check"
                    or arg in aggregatedArgs["mandatoryArgs"][check.name]
                ):
                    signature += (1,)
                    scope += (arg,)
                    target = new_scopes
                else:
                    # there's still a tail of args and check requires one of the
                    # args in tail but not the current arg
                    signature += (0,)
                    target = new_scopes
                target.append((check, signature, scope))
            scopes = new_scopes
        return saturated + scopes

    def _execute_section(self, iterargs, section, items):
        if section is None:
            # base case: terminate recursion
            for check, signature, scope in items:
                yield check, []
        elif not section[0]:
            # no sectioning on this level
            for item in self._execute_scopes(iterargs, items):
                yield item
        elif section[1] == "*check":
            # enforce sectioning by check
            for section_item in items:
                for item in self._execute_scopes(iterargs, [section_item]):
                    yield item
        else:
            # section by gen_arg, i.e. ammend with changing arg.
            _, gen_arg = section
            for index in range(iterargs[gen_arg]):
                for check, args in self._execute_scopes(iterargs, items):
                    yield check, [(gen_arg, index)] + args

    def _execute_scopes(self, iterargs, scopes):
        generators = []
        items = []
        current_section = None
        last_section = None
        seen = set()
        for check, signature, scope in scopes:
            if len(signature):
                # items are left
                if signature[0]:
                    gen_arg = scope[0]
                    scope = scope[1:]
                    current_section = True, gen_arg
                else:
                    current_section = False, None
                signature = signature[1:]
            else:
                current_section = None

            assert (
                current_section not in seen
            ), f"Scopes are badly sorted. {current_section} in {seen}"

            if current_section != last_section:
                if len(items):
                    # flush items
                    generators.append(
                        self._execute_section(iterargs, last_section, items)
                    )
                    items = []
                    seen.add(last_section)
                last_section = current_section
            items.append((check, signature, scope))
        # clean up left overs
        if len(items):
            generators.append(self._execute_section(iterargs, current_section, items))

        for item in chain(*generators):
            yield item

    def _section_execution_order(
        self,
        section,
        iterargs,
        reverse=False,
        custom_order=None,
        explicit_checks: Iterable = None,
        exclude_checks: Iterable = None,
    ):
        """
        order must:
          a) contain all variable args (we're appending missing ones)
          b) not contian duplictates (we're removing repeated items)

        order may contain *iterargs otherwise it is appended
        to the end

        order may contain "*check" otherwise, it is like *check is appended
        to the end (Not done explicitly though).
        """
        stack = list(custom_order) if custom_order is not None else list(section.order)
        if "*iterargs" not in stack:
            stack.append("*iterargs")
        stack.reverse()

        full_order = []
        seen = set()
        while len(stack):
            item = stack.pop()
            if item in seen:
                continue
            seen.add(item)
            if item == "*iterargs":
                all_iterargs = list(iterargs.keys())
                # assuming there is a meaningful order
                all_iterargs.reverse()
                stack += all_iterargs
                continue
            full_order.append(item)

        # Filter down checks. Checks to exclude are filtered for last as the user
        # might e.g. want to include all tests with "kerning" in the ID, except for
        # "kerning_something". explicit_checks could then be ["kerning"] and
        # exclude_checks ["something"].
        checks = section.checks
        if explicit_checks:
            checks = [
                check
                for check in checks
                if any(include_string in check.id for include_string in explicit_checks)
            ]
        if exclude_checks:
            checks = [
                check
                for check in checks
                if not any(
                    exclude_string in check.id for exclude_string in exclude_checks
                )
            ]

        scopes = self._analyze_checks(full_order, checks)
        key = lambda item: item[1]  # check, signature, scope = item
        scopes.sort(key=key, reverse=reverse)

        for check, args in self._execute_scopes(iterargs, scopes):
            # this is the iterargs tuple that will be used as a key for caching
            # and so on. we could sort it, to ensure it yields in the same
            # cache locations always, but then again, it is already in a well
            # defined order, by clustering.
            yield check, tuple(args)

    def execution_order(
        self, iterargs, custom_order=None, explicit_checks=None, exclude_checks=None
    ):
        # TODO: a custom_order per section may become necessary one day
        explicit_checks = set() if not explicit_checks else set(explicit_checks)
        for _, section in self._sections.items():
            for check, section_iterargs in self._section_execution_order(
                section,
                iterargs,
                custom_order=custom_order,
                explicit_checks=explicit_checks,
                exclude_checks=exclude_checks,
            ):
                yield (section, check, section_iterargs)

    def _register_check(self, section, func):
        other_section = self._check_registry.get(func.id, None)
        if other_section:
            other_check = other_section.get_check(func.id)
            if other_check is func:
                if other_section is not section:
                    logging.debug(
                        "Check {} is already registered in {}, skipping "
                        "register in {}.".format(func, other_section, section)
                    )
                return False  # skipped
            else:
                raise SetupError(
                    f'Check id "{func}" is not unique!'
                    f" It is already registered in {other_section} and"
                    f" registration for that id is now requested in {section}."
                    f" BUT the current check is a different object than"
                    f" the registered check."
                )
        self._check_registry[func.id] = section
        return True

    def _unregister_check(self, section, check_id):
        assert (
            section == self._check_registry[check_id]
        ), "Registered section must match"
        del self._check_registry[check_id]
        return True

    def remove_check(self, check_id):
        section = self._check_registry[check_id]
        section.remove_check(check_id)

    def check_log_override(
        self,
        override_check_id
        # see def check_log_override
        ,
        *args,
        **kwds,
    ):
        new_id = f"{override_check_id}:{self.profile_tag}"
        old_check, section = self.get_check(override_check_id)
        new_check = check_log_override(old_check, new_id, *args, **kwds)
        section.replace_check(override_check_id, new_check)
        return new_check

    def get_check(self, check_id):
        section = self._check_registry[check_id]
        return section.get_check(check_id), section

    def add_section(self, section):
        key = str(section)
        if key in self._sections:
            # the string representation of a section must be unique.
            # string representations of section and check will be used as unique keys
            if self._sections[key] is not section:
                raise SetupError(f"A section with key {section} is already registered")
            return
        self._sections[key] = section
        section.on_add_check(self._register_check)
        section.on_remove_check(self._unregister_check)

        for check in section.checks:
            self._register_check(section, check)

    def _get_section(self, key):
        return self._sections[key]

    def _add_check(self, section, func):
        self.add_section(section)
        section.add_check(func)
        return func

    def register_check(self, section=None, *args, **kwds):
        """
        Usage:
        # register in default section
        @profile.register_check
        @check(id='com.example.fontbakery/check/0')
        def my_check():
          yield PASS, 'example'

        # register in `special_section` also register that section in the profile
        @profile.register_check(special_section)
        @check(id='com.example.fontbakery/check/0')
        def my_check():
          yield PASS, 'example'

        """
        if section and len(kwds) == 0 and callable(section):
            func = section
            section = self._default_section
            return self._add_check(section, func)
        else:
            return partial(self._add_check, section)

    def _add_condition(self, condition, name=None):
        self.add_to_namespace(
            "conditions", name or condition.name, condition, force=condition.force
        )
        return condition

    def register_condition(self, *args, **kwds):
        """
        Usage:

        @profile.register_condition
        @condition
        def myCondition():
          return 123

        #or

        @profile.register_condition(name='my_condition')
        @condition
        def myCondition():
          return 123
        """
        if len(args) == 1 and len(kwds) == 0 and callable(args[0]):
            return self._add_condition(args[0])
        else:
            return partial(self._add_condition, *args, **kwds)

    def register_expected_value(self, expected_value, name=None):
        name = name or expected_value.name
        self.add_to_namespace(
            "expected_values", name, expected_value, force=expected_value.force
        )
        return True

    def _get_package(self, symbol_table):
        package = symbol_table.get("__package__", None)
        if package is not None:
            return package
        name = symbol_table.get("__name__", None)
        if name is None or not "." in name:
            return None
        return name.rpartition(".")[0]

    def _load_profile_imports(self, symbol_table):
        """
        profile_imports is a list of module names or tuples
        of (module_name, names to import)
        in the form of ('.', names) it behaces like:
        from . import name1, name2, name3
        or similarly
        import .name1, .name2, .name3

        i.e. "name" in names becomes ".name"
        """
        results = []
        if "profile_imports" not in symbol_table:
            return results

        package = self._get_package(symbol_table)
        profile_imports = symbol_table["profile_imports"]

        for item in profile_imports:
            if isinstance(item, str):
                # import the whole module
                module_name, names = (item, None)
            else:
                # expecting a 2 items tuple or list
                # import only the names from the module
                module_name, names = item

            if "." in module_name and len(set(module_name)) == 1 and names is not None:
                # if you execute `from . import mod` from a module in the pkg package
                # then you will end up importing pkg.mod
                module_names = [f"{module_name}{name}" for name in names]
                names = None
            else:
                module_names = [module_name]

            for module_name in module_names:
                module = importlib.import_module(module_name, package=package)
                if names is None:
                    results.append(module)
                else:
                    #  1. check if the imported module has an attribute by that name
                    #  2. if not, attempt to import a submodule with that name
                    #  3. if the attribute is not found, ImportError is raised.
                    #  …
                    for name in names:
                        try:
                            results.append(getattr(module, name))
                        except AttributeError:
                            # attempt to import a submodule with that name
                            sub_module_name = ".".join([module_name, name])
                            sub_module = importlib.import_module(
                                sub_module_name, package=package
                            )
                            results.append(sub_module)
        return results

    def auto_register(self, symbol_table, filter_func=None, profile_imports=None):
        """Register items from `symbol_table` in the profile.

        Get all items from `symbol_table` dict and from `symbol_table.profile_imports`
        if it is present. If they an item is an instance of FontBakeryCheck,
        FontBakeryCondition or FontBakeryExpectedValue and register it in
        the default section.
        If an item is a python module, try to get a profile using `get_module_profile(item)`
        and then using `merge_profile`;
        If the profile_imports kwarg is given, it is used instead of the one taken from
        the module namespace.

        To register the current module use explicitly:
          `profile.auto_register(globals())`
          OR maybe: `profile.auto_register(sys.modules[__name__].__dict__)`
        To register an imported module explicitly:
          `profile.auto_register(module.__dict__)`

        if filter_func is defined it is called like:
        filter_func(type, name_or_id, item)
        where
        type: one of "check", "module", "condition", "expected_value", "iterarg",
              "derived_iterable", "alias"
        name_or_id: the name at which the item will be registered.
              if type == 'check': the check.id
              if type == 'module': the module name (module.__name__)
        item: the item to be registered
        if filter_func returns a falsy value for an item, the item will
        not be registered.
        """
        if profile_imports:
            symbol_table = symbol_table.copy()  # Avoid messing with original table
            symbol_table["profile_imports"] = profile_imports

        all_items = list(symbol_table.values()) + self._load_profile_imports(
            symbol_table
        )
        namespace_types = (FontBakeryCondition, FontBakeryExpectedValue)
        namespace_items = []

        for item in all_items:
            if isinstance(item, namespace_types):
                # register these after all modules have been registered. That way,
                # "local" items can optionally force override items registered
                # previously by modules.
                namespace_items.append(item)
            elif isinstance(item, FontBakeryCheck):
                if filter_func and not filter_func("check", item.id, item):
                    continue
                self.register_check(item)
            elif isinstance(item, types.ModuleType):
                if filter_func and not filter_func("module", item.__name__, item):
                    continue
                profile = get_module_profile(item)
                if profile:
                    self.merge_profile(profile, filter_func=filter_func)

        for item in namespace_items:
            if isinstance(item, FontBakeryCondition):
                if filter_func and not filter_func("condition", item.name, item):
                    continue
                self.register_condition(item)
            elif isinstance(item, FontBakeryExpectedValue):
                if filter_func and not filter_func("expected_value", item.name, item):
                    continue
                self.register_expected_value(item)

    def merge_profile(self, profile, filter_func=None):
        """Copy all namespace items from profile to self.

        Namespace items are: 'iterargs', 'derived_iterables', 'aliases',
                             'conditions', 'expected_values'

        Don't change any contents of profile ever!
        That means sections are cloned not used directly

        filter_func: see description in auto_register
        """
        # 'iterargs', 'derived_iterables', 'aliases', 'conditions', 'expected_values'
        for ns_type in self._valid_namespace_types:
            # this will raise a NamespaceError if an item of profile.{ns_type}
            # is already registered.
            ns_dict = getattr(profile, ns_type)
            if filter_func:
                ns_type_singular = self._valid_namespace_types[ns_type]
                ns_dict = {
                    name: item
                    for name, item in ns_dict.items()
                    if filter_func(ns_type_singular, name, item)
                }
            self._add_dict_to_namespace(ns_type, ns_dict)

        check_filter_func = (
            None
            if not filter_func
            else lambda check: filter_func("check", check.id, check)
        )
        for section in profile.sections:
            my_section = self._sections.get(str(section), None)
            if not len(section.checks):
                continue
            if my_section is None:
                # create a new section: don't change other module/profile contents
                my_section = section.clone(check_filter_func)
                self.add_section(my_section)
            else:
                # order, description are not updated
                my_section.merge_section(section, check_filter_func)

    @property
    def check_skip_filter(self):
        """ return the current check_skip_filter function or None """
        return self._check_skip_filter

    @check_skip_filter.setter
    def check_skip_filter(self, check_skip_filter):
        """ Set a check_skip_filter function.

        A check_skip_filter has a signature like:

        ```
        def check_skip_filter(check_id: str, **iterargsDict : dict) \
                                    -> Tuple[accepted: bool, message: str]
          …
        ```

        If present, this function is called just before a check
        with `check_id` is executed. `iterargsDict` is a dictionary
        containing key:value pairs of the "iterable arguments" that will be
        applied for that check execution.

        If the returned `accepted` is falsy, the check will be SKIPed using
        `message` for reporting.

        There's no full resolution of all check arguments at this point,
        That can be achieved with the `conditions=[]` argument of the
        check constructor/decorator. This is for more general filtering.
        """
        self._check_skip_filter = check_skip_filter

    def serialize_identity(self, identity):
        """Return a json string that can also  be used as a key.

        The JSON is explicitly unambiguous in the item order
        entries (dictionaries are not ordered usually)
        Otherwise it is valid JSON
        """
        section, check, iterargs = identity
        values = map(
            # separators are without space, which is the default in JavaScript;
            # just in case we need to make these keys in JS.
            partial(json.dumps, separators=(",", ":"))
            # iterargs are sorted, because it doesn't matter for the result
            # but it gives more predictable keys.
            # Though, arguably, the order generated by the profile is also good
            # and conveys insights on how the order came to be (clustering of
            # iterargs). `sorted(iterargs)` however is more robust over time,
            # the keys will be the same, even if the sorting order changes.
            ,
            [str(section), check.id, sorted(iterargs)],
        )
        return '{{"section":{},"check":{},"iterargs":{}}}'.format(*values)

    def deserialize_identity(self, key):
        item = json.loads(key)
        section = self._get_section(item["section"])
        check, _ = self.get_check(item["check"])
        # tuple of tuples instead list of lists
        iterargs = tuple(tuple(item) for item in item["iterargs"])
        return section, check, iterargs

    def serialize_order(self, order):
        return map(self.serialize_identity, order)

    def deserialize_order(self, serialized_order):
        return tuple(self.deserialize_identity(item) for item in serialized_order)

    def setup_argparse(self, argument_parser):
        """
        Set up custom arguments needed for this profile.
        Return a list of keys that will be set to the `values` dictonary
        """
        pass

    def get_deep_check_dependencies(self, check):
        seen = set()
        dependencies = list(check.args)
        if hasattr(check, "conditions"):
            dependencies += [
                name for negated, name in map(is_negated, check.conditions)
            ]
        while dependencies:
            name = dependencies.pop()
            if name in seen:
                continue
            seen.add(name)
            condition = self.conditions.get(name, None)
            if condition is not None:
                dependencies += condition.args
        return seen

    @property
    def checks(self):
        for section in self.sections:
            for check in section.checks:
                yield check

    def get_checks_by_dependencies(self, *dependencies, subset=False):
        deps = set(dependencies)  # faster membership checking
        result = []
        for check in self.checks:
            check_deps = self.get_deep_check_dependencies(check)
            if (subset and deps.issubset(check_deps)) or (
                not subset and len(deps.intersection(check_deps))
            ):
                result.append(check)
        return result

    def merge_default_config(self, user_config):
        """
        Forms a configuration object based on defaults provided by the profile,
        overridden by values in the user's configuration file.
        """
        copy = Configuration(**self.configuration_defaults)
        copy.update(user_config)
        return copy


def _check_log_override(overrides, status, message):
    # These constants are merely meant to be used
    # so that the check_override declarations are more readable:
    from fontbakery.message import (KEEP_ORIGINAL_STATUS,
                                    KEEP_ORIGINAL_MESSAGE)
    result_status = status
    result_message = message
    override = False
    for override_target, new_status, new_message_string in overrides:
        # Override is only possible by matching message.code
        if (
            not hasattr(result_message, "code")
            or result_message.code != override_target
        ):
            continue
        override = True
        if new_status is not KEEP_ORIGINAL_STATUS:
            result_status = new_status
        if new_message_string is not KEEP_ORIGINAL_MESSAGE:
            # If it looks like an instance of Message we reuse the code,
            # as it is the same condition this makes totally sense.
            result_message = Message(result_message.code, new_message_string)
        # Break the for loop, we had a successful override.
        break
    return override, result_status, result_message


def check_log_override(check, new_id, overrides, reason=None):
    """Returns a new FontBakeryCheck that is decorating (wrapping) check,
    but with overrides applied to returned statuses when they match.

    The new FontBakeryCheck is always a generator check, even if the old
    check is just a normal function that returns (instead of yields)
    its result. Also, the new check yields an INFO Status for each
    overridden original status.

    Arguments:

    check: the FontBakeryCheck to be decorated
    new_id: string, must be unique of course and should not(!) be check.id
            as we essentially create a new, different check.
    overrides: a tuple of override triple-tuples
               ((override_target, new_status, new_message_string), ...)
               override_target: string, specific Message.code
               new_status: Status or None, keep old status
               new_message_string: string or None, keep old message
    """

    @wraps(check)  # defines __wrapped__
    def override_wrapper(*args, **kwds):
        # A check can be either a normal function that returns one Status or a
        # generator that yields one or more. The latter will return a generator
        # object that we can detect with types.GeneratorType.
        result = check(*args, **kwds)  # Might raise.
        if not isinstance(result, types.GeneratorType):
            # Now it iterates
            # make these always iterators, it's nicer to handle
            # also we can mix-in new status messages
            result = (result,)
        # Iterate over sub-results one-by-one, list(result) would abort on
        # encountering the first exception.
        for (status, message) in result:  # Might raise.
            overriden, result_status, result_message = _check_log_override(
                overrides, status, message
            )
            if overriden:
                # nothing changed (despite of a match in override rules)
                if result_status == status and result_message == message:
                    yield DEBUG, (
                        "A check status override rule matched but"
                        " did not change the resulting status."
                    )
                # Both changed
                elif result_status != status and result_message != message:
                    yield DEBUG, (
                        f"Overridden check status and message,"
                        f" original: {status} {message}"
                    )
                # Only status changed
                elif result_status != status and result_message == message:
                    yield DEBUG, f"Overridden check status, original: {status}"
                # Only message changed
                elif result_status == status and result_message != message:
                    yield DEBUG, f"Overridden check message, original: {message}"

            yield result_status, result_message

    # Make the callable here and return that.
    new_check = FontBakeryCheck(
        override_wrapper,
        new_id
        # Untouched, the reason for this checks existence stays the same!
        ,
        rationale=check.rationale
        # the "Derived ..." part should be prominent, so we always see it
        ,
        description=f"{check.description} (derived from {check.id})"
        # ONLY if there's a reason for derivation, otherwise will take
        # the documentation from the __doc__ string of check.
        ,
        documentation=(f"{reason}\n" f"\n" f"{check.documentation}")
        if reason and check.documentation
        else (reason or check.documentation or None),
    )

    # reconstruct a proper doc string from the changes we made.
    # This is really backwards! But, it's so fundamental how python doc
    # strings work, that I think it's solid enough.
    new_check.__doc__ = f"{new_check.description}\n" f"\n" f"{new_check.documentation}"
    return new_check


# Lib/fontbakery/section.py

from fontbakery.errors import SetupError


class Section:
    """ An ordered set of checks.

    Used to structure checks in a profile. A profile consists
    of one or more sections.
    """
    def __init__(self, name, checks=None, order=None, description=None):
        self.name = name
        self.description = description
        self._add_check_callback = None
        self._remove_check_callback = None
        self._checks = [] if checks is None else list(checks)
        self._checkid2index = {check.id:i for i, check in enumerate(self._checks)}
        # a list of iterarg-names
        self._order = order or []

    def clone(self, filter_func=None):
        checks = self.checks if not filter_func else filter(filter_func, self.checks)
        return Section(self.name,
                       checks=checks,
                       order=self.order,
                       description=self.description)

    def __repr__(self):
        return f'<Section: {self.name}>'

    # This was problematic. See: https://github.com/googlefonts/fontbakery/issues/2194
    # def __str__(self):
    #     return self.name

    def __eq__(self, other):
        """ True if other.checks has the same checks in the same order"""
        if hasattr(other, "checks"):
            return self._checks == other.checks
        else:
            return False

    @property
    def order(self):
        return self._order[:]

    @property
    def checks(self):
        return self._checks

    def on_add_check(self, callback):
        if self._add_check_callback is not None:
            # allow only one, otherwise, skipping registration in
            # add_check becomes problematic, can't skip just for some
            # callbacks.
            raise Exception(f'{self} already has an on_add_check callback')
        self._add_check_callback = callback

    def on_remove_check(self, callback):
        if self._remove_check_callback is not None:
            # allow only one, otherwise, skipping un-registration in
            # remove_check becomes problematic, can't skip just for some
            # callbacks.
            raise Exception(f'{self} already has an on_add_check callback')
        self._remove_check_callback = callback

    def add_check(self, check):
        """
        Please use rather `register_check` as a decorator.
        """
        if self._add_check_callback is not None:
            if not self._add_check_callback(self, check):
                # rejected, skip!
                return False

        self._checkid2index[check.id] = len(self._checks)
        self._checks.append(check)
        return True

    def remove_check(self, check_id):
        index = self._checkid2index[check_id]
        if self._remove_check_callback is not None:
            if not self._remove_check_callback(self, check_id):
                # rejected, skip!
                return False
        del self._checks[index]
        # Fixing the index, maybe an ordered dict would work here the same
        # but simpler. Could also rebuild the entire index.
        del self._checkid2index[check_id]
        for cid, idx in self._checkid2index.items():
            if idx > index:
                self._checkid2index[cid] = idx - 1
        return True

    def replace_check(self, override_check_id, new_check):
        index = self._checkid2index[override_check_id]
        if self._remove_check_callback is not None:
            if not self._remove_check_callback(self, override_check_id):
                # rejected, skip!
                return False

        if self._add_check_callback is not None:
            if not self._add_check_callback(self, new_check):
                # rejected, skip!
                # But first restore the old check registration.
                # Maybe a resource manager would be nice here.
                # Also, raising and failing could be an option.
                self._add_check_callback(self, self.get_check(override_check_id))
                return False

        del self._checkid2index[override_check_id]
        self._checkid2index[new_check.id] = index
        self._checks[index] = new_check
        return True

    def merge_section(self, section, filter_func=None):
        """
        Add section.checks to self, if not skipped by self._add_check_callback.
        order, description, etc. are not updated.
        """
        for check in section.checks:
            if filter_func and not filter_func(check):
                continue
            self.add_check(check)

    def get_check(self, check_id):
        index = self._checkid2index[check_id]
        return self._checks[index]

    def register_check(self, func):
        """
        # register in `special_section`
        @my_section.register_check
        @check(id='com.example.fontbakery/check/0')
        def my_check():
          yield PASS, 'example'
        """
        if not self.add_check(func):
            raise SetupError(f'Can\'t add check {func} to section {self}.')
        return func


# Lib/fontbakery/status.py

class Status:
    """ If you create a custom Status symbol, please keep in mind that
    all statuses are registered globally and that can cause name collisions.

    However, it's an intended use case for your checks to be able to yield
    custom statuses. Interpreters of the check protocol will have to skip
    statuses unknown to them or treat them in an otherwise non-fatal fashion.
    """
    def __new__(cls, name, weight=0):
        """ Don't create two instances with same name.

        >>> a = Status('PASS')
        >>> a
        <Status hello>
        >>> b = Status('PASS')
        >>> b
        <Status hello>
        >>> b is a
        True
        >>> b == a
        True
        """
        instance = cls.__instances.get(name, None)
        if instance is None:
            instance = cls.__instances[name] = super(Status, cls).__new__(cls)
            setattr(instance, '_Status__name', name)
            setattr(instance, '_Status__weight', weight)
        return instance

    __instances = {}

    def __str__(self):
        return f'<Status {self.__name}>'

    @property
    def name(self):
        return self.__name

    @property
    def weight(self):
        return self.__weight

    def __gt__(self, other):
        return self.weight > other.weight

    def __ge__(self, other):
        return self.weight >= other.weight

    def __lt__(self, other):
        return self.weight < other.weight

    def __le__(self, other):
        return self.weight <= other.weight

    __repr__ = __str__

# Status messages of the check runner protocol

# Structuring statuses
#  * begin with "START" and "END"
#  * have weights < 0
#  * START statuses have even weight, corresponding END statuses have odd
#    weights, such that START.weight + 1 == END.weight
#  * the bigger the weight the bigger is the structure, structuring on a macro-level
#  * different structures can have the same weights, if they occur on the same level
#  * ENDCHECK is the biggest structuring status
#
# Log statuses
#  * have weights >= 0
#  * the more important the status the bigger the weight
#  * ERROR has the biggest weight
#  * PASS is the lowest status a check can have,
#    i.e.: a check run must at least yield one log that is >= PASS
#
# From all the statuses that can occur within a check, the "worst" one
# is defining for the check overall status:
# ERROR > FAIL > WARN > INFO > SKIP > PASS > DEBUG
# Anything from WARN to PASS does not make a check fail.
# A result < PASS creates an ERROR. That means, DEBUG is not a valid
# result of a check, nor is any of the structuring statuses.
# A check with SKIP can't (MUST NOT) create any other event.

# Log statuses
# only between STARTCHECK and ENDCHECK:
DEBUG = Status('DEBUG', 0) # Silent by default
PASS = Status('PASS', 1)
SKIP = Status('SKIP', 2) # SKIP is heavier than PASS because it's likely more interesting to
                         # see what got skipped, to reveal blind spots.
INFO = Status('INFO', 3)
WARN = Status('WARN', 4) # A check that results in WARN may indicate a problem, but also may be OK.
FAIL = Status('FAIL', 5) # A FAIL is a problem detected in the font or family.
ERROR = Status('ERROR', 6) # Something a programmer must fix. It will make a check fail as well.

# Start of the suite of checks. Must be always the first message, even in async mode.
# Message is the full execution order of the whole profile
START = Status('START', -6)
# Only between START and before the first SECTIONSUMMARY and END
# Message is None.
STARTCHECK = Status('STARTCHECK', -2)
# Ends the last check started by STARTCHECK.
# Message the the result status of the whole check, one of PASS, SKIP, FAIL, ERROR.
ENDCHECK = Status('ENDCHECK', -1)
# After the last ENDCHECK one SECTIONSUMMARY for each section before END.
# Message is a tuple of:
#   * the actual execution order of the section in the check runner session
#     as reported. Especially in async mode, the order can differ significantly
#     from the actual order of checks in the session.
#   * a Counter dictionary where the keys are Status.name of
#     the ENDCHECK message. If serialized, some existing statuses may not be
#     in the counter because they never occurred in the section.
SECTIONSUMMARY = Status('SECTIONSUMMARY', -3)
# End of the suite of checks. Must be always the last message, even in async mode.
# Message is a counter as described in SECTIONSUMMARY, but with the collected
# results of all checks in all sections.
END = Status('END', -5)


# Lib/fontbakery/utils.py

#!/usr/bin/env python3
# Copyright 2016 The Fontbakery Authors
#
# 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 os
import subprocess
import sys

from fontTools.ttLib import TTFont
from fontTools.unicodedata import ot_tag_to_script
from typing import Text, Optional
from fontbakery.constants import NO_COLORS_THEME, DARK_THEME, LIGHT_THEME
from ufo2ft.constants import INDIC_SCRIPTS, USE_SCRIPTS
from vharfbuzz import Vharfbuzz



# TODO: this should be part of FontBakeryCheck and check.conditions
# should be a tuple (negated, name)
def is_negated(name):
    stripped = name.strip()
    if stripped.startswith('not '):
        return True, stripped[4:].strip()
    if stripped.startswith('!'):
        return True, stripped[1:].strip()
    return False, stripped


def colorless_len(str):
    import re
    return len(re.sub('\x1b(\\[[0-9;]+|\\].+)m', '', str))


def text_flow(content, width=80, indent=0, left_margin=0, right_margin=0,
              first_line_indent=0, space_padding=False,
              text_color="{}".format):  # pylint: disable=consider-using-f-string
    result = []
    line_num = 0
    for line in content.split("\n"):
        _indent = indent
        _width = width - right_margin

        if line.strip() == "":
            if space_padding:
                result.append(" " * _indent + text_color(" " * width))
            continue

        words = line.split(" ")
        while words:
            line_num += 1
            if line_num == 1:
                if left_margin > -first_line_indent:
                    inside_indent = " " * (left_margin + first_line_indent)
                else:
                    inside_indent = ""
            else:
                inside_indent = " " * left_margin
            this_line = inside_indent + words.pop(0)

            if colorless_len(this_line) > _width:
                # let's see what we can do to make it fit
                if "/" in this_line:
                    # here we feed-back chunks of a URL
                    # into words if it overflows the block
                    chunks = this_line.split('/')
                    new_line = chunks.pop(0)
                    while chunks:
                        next_len = colorless_len(new_line) + 1 + colorless_len(chunks[0])
                        if next_len >= _width:
                            break
                        new_line += "/" + chunks.pop(0)
                    this_line = new_line
                    words.insert(0, "/" + "/".join(chunks))
                else:
                    # not sure what else to do,
                    # so we'll simply cut the long word
                    words.insert(0, this_line[_width:])  # word overflow chunk
                    this_line = this_line[:_width]  # line with chopped word leftover

            while words and (colorless_len(this_line) + 1 + colorless_len(words[0]) <= _width):
                this_line += " " + words.pop(0)

            if space_padding:
                # pad the line with spaces to fit the block width:
                this_line += " " * (width - colorless_len(this_line))
            result.append(" " * _indent + text_color(this_line))
    return "\n".join(result)


def get_apple_terminal_bg_color():
    """Runs an AppleScript snippet that returns the RGB values of the
    background color of the active Apple Terminal window."""
    line_1 = 'tell application "Terminal"'
    line_2 = '    get background color of selected tab of window 1'
    line_3 = 'end tell'
    output = subprocess.check_output(
        ["osascript", "-e", line_1, "-e", line_2, "-e", line_3], text=True)
    return output.strip()


def apple_terminal_bg_is_white():
    """Returns a boolean indicating if the background color
    of Apple's Terminal is white."""
    is_apple_terminal = os.getenv("TERM_PROGRAM") == "Apple_Terminal"
    if is_apple_terminal:
        bg_color = get_apple_terminal_bg_color()
        if bg_color == "65535, 65535, 65535":
            return True
    return False


def get_theme(args):
    if args.no_colors:
        return NO_COLORS_THEME
    if args.light_theme:
        return LIGHT_THEME
    if args.dark_theme:
        return DARK_THEME
    if sys.platform == "darwin":
        # Apple's Terminal default profile is called 'Basic' and has a white background.
        # But the user may have switched to a different profile, or even to a different
        # terminal app. Default to the light-theme only if we're sure that the terminal
        # app is Apple's Terminal and its background color is indeed white.
        return LIGHT_THEME if apple_terminal_bg_is_white() else DARK_THEME
    # For orther systems like GNU+Linux and Windows, a dark terminal seems to be more common.
    return DARK_THEME


def unindent_and_unwrap_rationale(rationale, checkid=None):
    """Takes the 'rationale' docstring of a check and removes indents and hard line
    breaks that were added to long lines."""
    content = ""

    for line in rationale.split("\n"):
        soft_return = line.endswith("⏎")  # U+23CE
        stripped_line = line.strip()
        new_paragraph = len(stripped_line) == 0

        if new_paragraph:
            content = content.rstrip()
            content += "\n\n"

        else:
            content += stripped_line

            if soft_return:
                content = f"{content[:-1]}\n"
            else:
                content += " "

    return f"\n{content.strip()}\n"


def html5_collapsible(summary, details) -> str:
    """Return nestable, collapsible <detail> tag for check grouping and sub-
    results."""

    return f"<details><summary>{summary}</summary><div>{details}</div></details>"


def split_camel_case(camelcase):
    result = []
    word = ""
    for char in camelcase:
        if char.isupper():
            if word != "":
                result.append(word)
            word = char
        else:
            word += char

    if word != "":
        result.append(word)
    return " ".join(result)


def suffix(font):
    filename = os.path.basename(font)
    basename = os.path.splitext(filename)[0]
    s = basename.split('-')
    s.pop(0)
    return '-'.join(s)


def pretty_print_list(config, values, shorten=10, sep=", ", glue="and"):
    if len(values) == 1:
        return str(values[0])

    if config.get('full_lists'):
        shorten = None

    if shorten and len(values) > shorten + 2:
        joined_items_str = sep.join(map(str, values[:shorten]))
        return (f"{joined_items_str} {glue} {len(values) - shorten} more.\n"
                f"\n"
                f"Use -F or --full-lists to disable shortening of long lists.")
    else:
        joined_items_str = sep.join(map(str, values[:-1]))
        return f"{joined_items_str} {glue} {str(values[-1])}"


def bullet_list(config, items, bullet="-", indentation="\t"):
    return f"{indentation}{bullet} " +\
           pretty_print_list(config,
                             items,
                             sep=f"\n\n{indentation}{bullet} ",
                             glue=f"\n\n{indentation}{bullet} And")


def get_regular(fonts):
    # TODO: Maybe also support getting a regular instance from a variable font?
    for font in fonts:
        if "-Regular.ttf" in font:
            return font


def get_absolute_path(p):
    if os.path.isabs(p):
        abspath = p
    else:
        abspath = os.path.join(os.path.abspath('.'), p)
    return abspath


def filesize_formatting(s):
    if s < 1024:
        return f"{s} bytes"
    elif s < 1024*1024:
        return f"{s/1024:.1f}kb"
    else:
        return f"{s/(1024*1024):.1f}Mb"


def get_bounding_box(font):
    """ Returns max and min bbox of given truetype font """
    ymin = 0
    ymax = 0
    if font.sfntVersion == 'OTTO':
        ymin = font['head'].yMin
        ymax = font['head'].yMax
    else:
        for g in font['glyf'].glyphs:
            char = font['glyf'][g]
            if hasattr(char, 'yMin') and ymin > char.yMin:
                ymin = char.yMin
            if hasattr(char, 'yMax') and ymax < char.yMax:
                ymax = char.yMax
    return ymin, ymax


def get_name_entries(font,
                     nameID,
                     platformID=None,
                     encodingID=None,
                     langID=None):
    results = []
    for entry in font['name'].names:
        if entry.nameID == nameID and \
           (platformID is None or entry.platformID == platformID) and \
           (encodingID is None or entry.platEncID == encodingID) and \
           (langID is None or entry.langID == langID):
            results.append(entry)
    return results


def get_name_entry_strings(font,
                           nameID,
                           platformID=None,
                           encodingID=None,
                           langID=None):
    entries = get_name_entries(font, nameID, platformID, encodingID, langID)
    return list(map(lambda e: e.string.decode(e.getEncoding()), entries))


def name_entry_id(name):
    from fontbakery.constants import (NameID,
                                      PlatformID)
    return "[{}({}):{}({})]".format(NameID(name.nameID).name,
                                    name.nameID,
                                    PlatformID(name.platformID).name,
                                    name.platformID) # pylint: disable=consider-using-f-string


def get_glyph_name(font: TTFont, codepoint: int) -> Optional[str]:
    next_best_cmap = font.getBestCmap()

    if codepoint in next_best_cmap:
        return next_best_cmap[codepoint]

    return None


def glyph_contour_count(font, name):
    """Contour count for specified glyph.
    This implementation will also return contour count for
    composite glyphs.
    """
    contour_count = 0
    items = [font['glyf'][name]]

    while items:
        g = items.pop(0)
        if g.isComposite():
            for comp in g.components:
                if comp.glyphName != ".ttfautohint":
                    items.append(font['glyf'][comp.glyphName])
        if g.numberOfContours != -1:
            contour_count += g.numberOfContours
    return contour_count


def get_font_glyph_data(font):
    """Return information for each glyph in a font"""
    from fontbakery.constants import (PlatformID,
                                      WindowsEncodingID)
    font_data = []

    try:
        subtable = font['cmap'].getcmap(PlatformID.WINDOWS,
                                        WindowsEncodingID.UNICODE_BMP)
        if not subtable:
                    # Well... Give it a chance here...
                    # It may be using a different Encoding_ID value
            subtable = font['cmap'].tables[0]

        cmap = subtable.cmap
    except:
        return None

    cmap_reversed = dict(zip(cmap.values(), cmap.keys()))

    for glyph_name in font.getGlyphSet().keys():
        if glyph_name in cmap_reversed:
            uni_glyph = cmap_reversed[glyph_name]
            contours = glyph_contour_count(font, glyph_name)
            font_data.append({
                'unicode': uni_glyph,
                'name': glyph_name,
                'contours': {contours}
            })
    return font_data

def get_Protobuf_Message(klass, path):
    from google.protobuf import text_format
    message = klass()
    text_data = open(path, "rb").read()
    text_format.Merge(text_data, message)
    return message

def get_FamilyProto_Message(path):
    from fontbakery.fonts_public_pb2 import FamilyProto
    return get_Protobuf_Message(FamilyProto, path)

def get_DesignerInfoProto_Message(text_data):
    from fontbakery.designers_pb2 import DesignerInfoProto
    from google.protobuf import text_format
    message = DesignerInfoProto()
    text_format.Merge(text_data, message)
    return message

def check_bit_entry(ttFont, table, attr, expected, bitmask, bitname):
    from fontbakery.message import Message
    from fontbakery.status import (PASS, FAIL)
    value = getattr(ttFont[table], attr)
    name_str = f"{table} {attr} {bitname} bit"
    if bool(value & bitmask) == expected:
        return PASS, f"{name_str} is properly set."
    else:
        if expected:
            expected_str = "set"
        else:
            expected_str = "unset"
        return FAIL,\
               Message(f"bad-{bitname}",
                       f"{name_str} should be {expected_str}.")


class BadCertificateSetupException(Exception):
    pass


def download_file(url):
    from urllib.request import urlopen
    from urllib.error import URLError
    from io import BytesIO
    try:
        return BytesIO(urlopen(url).read())
    except URLError as e:
        if "CERTIFICATE_VERIFY_FAILED" in str(e.reason):
            raise BadCertificateSetupException(
                "You probably installed official"
                " Mac python from python.org but forgot to also install"
                " the certificates. There is a note in the installer"
                " Readme about that. Check the Python folder in the"
                " Applications directory, you should find a shell script"
                " to install the certificates.")


def cff_glyph_has_ink(font: TTFont, glyph_name: Text) -> bool:
    if 'CFF2' in font:
        top_dict = font['CFF2'].cff.topDictIndex[0]
    else:
        top_dict = font['CFF '].cff.topDictIndex[0]
    char_strings = top_dict.CharStrings
    char_string = char_strings[glyph_name]
    bounds = char_string.calcBounds(char_strings)
    if bounds is not None:
        return True

    return False


def ttf_glyph_has_ink(font: TTFont, name: Text) -> bool:
    glyph = font['glyf'].glyphs[name]
    glyph.expand(font['glyf'])

    if not glyph.isComposite():
        if glyph.numberOfContours == 0:
            return False
        (coords, _, _) = glyph.getCoordinates(font['glyf'])
        # you need at least 3 points to draw
        return len(coords) > 2

    # Check for ink in each sub-component.
    for glyph_name in glyph.getComponentNames(glyph.components):
        if glyph_has_ink(font, glyph_name):
            return True

    return False


def unicoderange_bit_name(bit):
    from fontbakery.constants import UNICODERANGE_DATA
    return UNICODERANGE_DATA[bit][0][1]


def get_preferred_cmap(ttFont):
    cmaps = {}
    for table in ttFont["cmap"].tables:
        cmaps[table.format] = table.cmap

    if 12 in cmaps:
        return cmaps[12]

    elif 4 in cmaps:
        return cmaps[4]

    else:
        return None


def chars_in_range(ttFont, bit):
    from fontbakery.constants import UNICODERANGE_DATA
    cmap = get_preferred_cmap(ttFont)
    chars = []
    for c in sorted(cmap):
        for entry in UNICODERANGE_DATA[bit]:
            if c >= entry[2] and c <= entry[3]:
                chars.append(c)
    return chars


def compute_unicoderange_bits(ttFont):
    from fontbakery.constants import UNICODERANGE_DATA
    cmap = get_preferred_cmap(ttFont)
    result = 0
    for c in sorted(cmap):
        for bit in range(len(UNICODERANGE_DATA)):
            for entry in UNICODERANGE_DATA[bit]:
                bit = entry[0]
                if c >= entry[2] and c <= entry[3]:
                    result |= (1 << bit)
    return result


def glyph_has_ink(font: TTFont, name: Text) -> bool:
    """Checks if specified glyph has any ink.

    That is, that it has at least one defined contour associated.
    Composites are considered to have ink if any of their components have ink.
    Args:
        font:       the font
        glyph_name: The name of the glyph to check for ink.
    Returns:
        True if the font has at least one contour associated with it.
    """
    if 'glyf' in font:
        return ttf_glyph_has_ink(font, name)
    elif ('CFF ' in font) or ('CFF2' in font):
        return cff_glyph_has_ink(font, name)
    else:
        raise Exception("Could not find 'glyf', 'CFF ', or 'CFF2' table.")


def filenames_ending_in(suffix, root):
    '''
    Returns a list of the filenames of all files in a given directory subtree
    that have the given filename suffix. Example: List all ".json" files.
    '''
    filenames = []
    for f in os.listdir(root):
        fullpath = os.path.join(root, f)
        if f.endswith(suffix):
            filenames.append(fullpath)
        if os.path.isdir(fullpath):
            filenames.extend(filenames_ending_in(suffix, fullpath))
    return filenames


def add_check_overrides(checkids, profile_tag, overrides):
    '''
    Overridden checkids have a suffix identifying the specific
    profile that customize their behaviour.

    This helper function adds them to the list of checks and
    ensures the original check is not redundantly listed.
    '''

    # First we add the overridden check ids:
    checkids += [f'{checkid}:{profile_tag}'
                 for checkid in overrides]

    # But then we also remove the original check ids that
    # may have also been included from the original profile:
    checkids[:] = [checkid for checkid in checkids
                   if checkid not in overrides]
    return checkids


def can_shape(ttFont, text, parameters=None):
    '''
    Returns true if the font can render a text string without any
    .notdef characters.
    '''
    filename = ttFont.reader.file.name
    vharfbuzz = Vharfbuzz(filename)
    buf = vharfbuzz.shape(text, parameters)
    return all(g.codepoint != 0 for g in buf.glyph_infos)


def all_kerning(ttFont):
    if "GPOS" not in ttFont:
        return []
    rules = []

    def _invertClassDef(a, font):
        classes = {}
        for glyph, klass in a.items():
            if klass not in classes:
                classes[klass] = []
            classes[klass].append(glyph)
        glyphset = set(font.getGlyphOrder())
        classes[0] = glyphset - set(a.keys())
        return classes

    kern_subtables = []
    for lu in ttFont["GPOS"].table.LookupList.Lookup:
        for subtable in lu.SubTable:
            if subtable.LookupType == 9:
                subtable = subtable.ExtSubTable
            if subtable.LookupType == 2:
                kern_subtables.append(subtable)

    for subtable in kern_subtables:
        if subtable.Format == 1:
            for g, pair in zip(subtable.Coverage.glyphs, subtable.PairSet):
                for vr in pair.PairValueRecord:
                    rules.append((g, vr.SecondGlyph, vr.Value1, vr.Value2))
        else:
            class1 = _invertClassDef(subtable.ClassDef1.classDefs, ttFont)
            class2 = _invertClassDef(subtable.ClassDef2.classDefs, ttFont)
            for ix1, c1 in enumerate(subtable.Class1Record):
                if ix1 not in class1:
                    continue
                for ix2, c2 in enumerate(c1.Class2Record):
                    if ix2 not in class2:
                        continue
                    firstClass = list(
                        set(class1[ix1]) & set(subtable.Coverage.glyphs)
                    )
                    for left in firstClass:
                        for right in class2[ix2]:
                            rules.append((left, right, c2.Value1, c2.Value2))
    return rules


def is_complex_shaper_font(ttFont):
    for table in ["GSUB", "GPOS"]:
        if table not in ttFont:
            continue
        if not ttFont[table].table.ScriptList:
            continue
        for rec in ttFont[table].table.ScriptList.ScriptRecord:
            script = ot_tag_to_script(rec.ScriptTag)
            if script in USE_SCRIPTS or script in INDIC_SCRIPTS:
                return True
            if script in ["Khmr", "Mymr", "Hang"]:
                return True
    return False


def iterate_lookup_list_with_extensions(ttFont, table, callback, *args):
    """Iterates over the lookup list of a font's GSUB/GPOS table, calling
    the callback with the lookup and the provided arguments, but descending
    into Extension subtables."""
    if table not in ttFont or not ttFont[table].table.LookupList:
        return

    extension_type = 9 if table == "GPOS" else 7

    for lookup in ttFont[table].table.LookupList.Lookup:
        if lookup.LookupType == extension_type:
            for xt in lookup.SubTable:
                xt.SubTable = [xt.ExtSubTable]
                xt.LookupType = xt.ExtSubTable.LookupType
                callback(xt, *args)
        else:
            callback(lookup, *args)





# docs/source/conf.py

# Configuration file for the Sphinx documentation builder.
#
# This file does only contain a selection of the most common options. For a
# full list see the documentation:
# http://www.sphinx-doc.org/en/master/config

# -- Path setup --------------------------------------------------------------

# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import sys

import recommonmark.parser

sys.path.insert(0, os.path.abspath("../Lib"))


# -- Project information -----------------------------------------------------

project = "Font Bakery"
copyright = "2021 The Font Bakery Authors"
author = "The Font Bakery Authors"

# The short X.Y version
version = "0.8"
# The full version, including alpha/beta/rc tags
release = "0.8.8"


# -- General configuration ---------------------------------------------------

# If your documentation needs a minimal Sphinx version, state it here.
#
needs_sphinx = "4.3"

# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ["sphinx.ext.autodoc"
            , "sphinx.ext.viewcode"
            , "fontbakery.sphinx_extensions.linkcode" #was "sphinx.ext.linkcode"
            , "fontbakery.sphinx_extensions.profile"
            , "sphinx.ext.napoleon"
            , "recommonmark"
            ]

# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]

source_parsers = {".md": recommonmark.parser.CommonMarkParser}

# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
source_suffix = [".rst", ".md"]

# The main toctree document.
main_doc = "index"

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path .
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]

# The name of the Pygments (syntax highlighting) style to use.
pygments_style = "sphinx"


# -- Options for HTML output -------------------------------------------------

# The theme to use for HTML and HTML Help pages.  See the documentation for
# a list of builtin themes.
#
html_theme = "sphinx_rtd_theme"

# Theme options are theme-specific and customize the look and feel of a theme
# further.  For a list of options available for each theme, see the
# documentation.
#
html_theme_options = {"display_version": False}

# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]

# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# The default sidebars (for documents that don't match any pattern) are
# defined by theme itself.  Builtin themes are using these templates by
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
# 'searchbox.html']``.
#
# html_sidebars = {}


# -- Options for HTMLHelp output ---------------------------------------------

# Output file base name for HTML help builder.
htmlhelp_basename = "FontBakerydoc"


# -- Options for LaTeX output ------------------------------------------------

latex_elements = {
    # The paper size ('letterpaper' or 'a4paper').
    #
    # 'papersize': 'letterpaper',
    # The font size ('10pt', '11pt' or '12pt').
    #
    # 'pointsize': '10pt',
    # Additional stuff for the LaTeX preamble.
    #
    # 'preamble': '',
    # Latex figure (float) alignment
    #
    # 'figure_align': 'htbp',
}

# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
#  author, documentclass [howto, manual, or own class]).
latex_documents = [
    (main_doc,
     "FontBakery.tex",
     "Font Bakery Documentation",
     "The Font Bakery Authors",
     "manual")
]


# -- Options for manual page output ------------------------------------------

# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
    (main_doc,
     "fontbakery",
     "Font Bakery Documentation",
     [author],
     1)
]

# -- Options for Texinfo output ----------------------------------------------

# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
#  dir menu entry, description, category)
texinfo_documents = [
    (main_doc,
     "FontBakery",
     "Font Bakery Documentation",
     author,
     "FontBakery",
     "Well designed Font QA tool, written in Python 3.",
     "Miscellaneous")
]


# -- Extension configuration -------------------------------------------------

def linkcode_resolve(domain, info):
    # hmmm: related https://github.com/sphinx-doc/sphinx/issues/1556
    # ipdb> info
    # {'module': 'fontbakery.callable', 'fullname': 'Disabled'}
    # ipdb> domain
    # 'py'

    if domain != 'py':
        return None
    if not info['module']:
        return None
    filename = info['module'].replace('.', '/')
    # We must get the "tree" part dynamically, best would be the release
    # tag, if it is the same as the version that we are building at the
    # moment. Second best probably a hash. The branch HEAD is only of
    # limited usefulnes as the documentation will become out of sync.
    #
    # On GitHub
    # The release tagged "v0.7.2" is at:
    # short: bffe109
    # long:  bffe10976dc641d63b0622f09627bd0136157f1e
    # we can link to HEAD of a branch by branchname (here 'main'):
    # https://github.com/googlefonts/fontbakery/tree/main/Lib/fontbakery/profiles
    # We can link to a commit hash tree
    # short: https://github.com/googlefonts/fontbakery/tree/bffe109/Lib/fontbakery/profiles
    # long: https://github.com/googlefonts/fontbakery/tree/bffe10976dc641d63b0622f09627bd0136157f1e/Lib/fontbakery/profiles
    #
    # AND We can link to a tag i.e. a release tag. This is awesome:
    # tag is: "v0.7.2"
    # https://github.com/googlefonts/fontbakery/tree/v0.7.2/Lib/fontbakery/profiles
    tree = 'v0.8.8'
    # It's not planned for us to get the line number :-(
    # I had to hammer this data into the info.
    if 'lineno' in info:
        lineno = '#L{}'.format(info["lineno"])
    else:
        lineno = ''
    #if 'com_google' in info['fullname']:
    #    import ipdb
    #    ipdb.set_trace()
    return 'https://github.com/googlefonts/fontbakery/tree/{}/Lib/{}.py{}'.format(tree, filename, lineno)


# snippets/check-custom.py

import os

from fontbakery.checkrunner import Section, PASS, FAIL, WARN, ERROR, INFO, SKIP
from fontbakery.callable import condition, check, disable
from fontbakery.constants import PriorityLevel
from fontbakery.message import Message
from fontbakery.fonts_profile import profile_factory
from fontbakery.profiles.universal import UNIVERSAL_PROFILE_CHECKS

profile_imports = ("fontbakery.profiles.universal",)
profile = profile_factory(default_section=Section("Custom Checks"))

# ================================================
#
# Custom check list
#
# ================================================
# define new custom checks that are implemented in
# this source file by check ID.  Format these as a
# Python list.
# The check ID here is an example that demonstrates
# the example pass check in this module.  It is safe
# to remove the check ID when you remove the example
# check from this module
CUSTOM_PROFILE_CHECKS = UNIVERSAL_PROFILE_CHECKS + [
    "org.example/check/valid/testpass",
]

# ================================================
#
# Fontbakery check exclusion list
#
# ================================================
# define check ID's in the upstream `universal` profile
# that should be excluded as a Python tuple
excluded_check_ids = (
    # "com.google.fonts/check/dsig",
)

# ================================================
#
# Example test implementation
#
# ================================================

# The following are example implementations of
# pass and fail checks.  They are commented out
# and are not executed.

# # Test failure template
# @check(
#     id="org.example/check/valid/testfail",
#     rationale="""
#         This is the test failure rationale.
#     """,
# )
# def org_example_check_valid_test_fail():
#     """A test failure example."""
#     yield FAIL, "test failure message"


# # Test pass template
# @check(
#     id="org.example/check/valid/testpass",
#     rationale="""
#         This is the test pass rationale.
#     """,
# )
# def org_example_check_valid_test_pass():
#     """A test pass example."""
#     yield PASS, "test pass message"

# ================================================
#
# Begin check definitions
#
# ================================================

# TEST PASS EXAMPLE
# This check is a live example that is executed when
# you run this custom profile.  It is safe to remove
# this code when you implement your own checks.
@check(
    id="org.example/check/valid/testpass",
    rationale="""
        This is a test pass rationale.
    """,
)
def org_example_check_valid_test_pass():
    """A test pass example."""
    yield PASS, "test pass message"


# ================================================
#
# End check definitions
#
# ================================================

# This function identifies the skipped checks that are
# defined in the `excluded_check_ids` tuple at the head
# of this module.  Do not remove this function unless
# you do not intend to filter checks imported from the
# fontbakery universal profile test suite
def check_skip_filter(checkid, font=None, **iterargs):
    if font and checkid in excluded_check_ids:
        return False, ("Check skipped in Valid project profile")
    return True, None


# You should not need to edit the following block of code
profile.check_skip_filter = check_skip_filter
profile.auto_register(globals())
profile.test_expected_checks(CUSTOM_PROFILE_CHECKS, exclusive=True)


# snippets/dump-all-the-checks.py

import pkgutil
import fontbakery
from fontbakery.utils import get_theme
from importlib import import_module
from fontbakery.profile import get_module_profile
import json
import cmarkgfm
from cmarkgfm.cmark import Options as cmarkgfmOptions
import re


def md2html(x):
    return cmarkgfm.markdown_to_html_with_extensions(x, options=cmarkgfmOptions.CMARK_OPT_UNSAFE, extensions=["autolink"])


checks = {}
profiles_modules = [
    x.name
    for x in pkgutil.walk_packages(fontbakery.__path__, "fontbakery.")
    if x.name.startswith("fontbakery.profiles")
]
for profile_name in profiles_modules:
    imported = import_module(profile_name, package=None)
    profile = get_module_profile(imported)
    if not profile:
        continue
    profile_name = profile_name[20:]
    for section in profile._sections.values():
        for check in section._checks:
            if check.id not in checks:
                checks[check.id] = {
                    "sections": set(),
                    "profiles": set(),
                }
            checks[check.id]["sections"].add(section.name)
            checks[check.id]["profiles"].add(profile_name)
            for attr in ["proposal", "rationale", "severity", "description"]:
                if getattr(check, attr):
                    md = getattr(check, attr)
                    if attr == "rationale":
                        md = re.sub(r"(?m)^\s+", "", md)
                        checks[check.id][attr] = md2html(md)
                    else:
                        checks[check.id][attr] = md

for ck in checks.values():
    ck["sections"] = list(ck["sections"])
    ck["profiles"] = list(ck["profiles"])
print("window.fbchecks=" + json.dumps(checks, indent=4, sort_keys=True))


# snippets/fontbakery-check-upstream.py

"""Run Fontbakery on an upstream repo using the googlefonts profile.

In order to post an issue back to the upstream repo, you will need to add
a github access token.

Example:

python snippets/fontbakery-check-upstream.py \
    https://www.github.com/googlefonts/comfortaa /fonts/TTF \
    --post_issue --gh_token $MY_GH_TOKEN \
    --footnote "Please ignore checkXX"
"""
import requests
import argparse
import subprocess
import tempfile
from urllib.parse import urlparse
import os
import json
import shutil
import logging


logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


def convert_gh_url_to_api_url(url):
    return url.replace('https://github.com/',
                       'https://api.github.com/repos/')


def download_file(url, dst_path):
    """Download a file from a url"""
    request = requests.get(url, stream=True)
    with open(dst_path, 'wb') as downloaded_file:
        request.raw.decode_content = True
        shutil.copyfileobj(request.raw, downloaded_file)


def download_fonts(gh_url, dst):
    """Download fonts from a github dir"""
    font_paths = []
    r = requests.get(gh_url)
    for item in r.json():
        if item['name'].endswith(".ttf"):
            f = item['download_url']
            dl_path = os.path.join(dst, os.path.basename(f))
            download_file(f, dl_path)
            font_paths.append(dl_path)
    return font_paths


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("repo_url")
    parser.add_argument("font_dir", help="path to ttfs e.g /fonts/ttf")
    parser.add_argument("--post_issue", action="store_true", default=False,
                        help="Post FontBakery report to repo")
    parser.add_argument("-gh", "--gh_token", help="Your github access token")
    parser.add_argument("-ft", "--footnote", help="text to include after report")
    args = parser.parse_args()

    tmp = tempfile.mkdtemp()
    gh_report = os.path.join(tmp, 'report.md')

    logger.info("Downloading fonts from repo {}".format(args.repo_url))
    fonts_dir_url = args.repo_url + "/contents" + args.font_dir
    fonts_dir_url = convert_gh_url_to_api_url(fonts_dir_url)
    fonts = download_fonts(fonts_dir_url, tmp)

    logger.info("Running fonts through FontBakery")
    # Ignore check 28 since this is an upstream repo
    cmd = ['fontbakery', 'check-googlefonts'] + fonts + \
          ['--ghmarkdown', gh_report] + \
          ['-x', 'com.google.fonts/check/has_license'] + \
          ['-l', 'FAIL']

    subprocess.call(cmd)

    if args.post_issue and args.gh_token:
        issue_url = convert_gh_url_to_api_url(args.repo_url)
        issue_url += "/issues"
        logger.info(f"Posting issue to {issue_url}")
        headers = {'Authorization': f"token {args.gh_token}"}
        body = open(gh_report, "r").read()
        if args.footnote:
            body += (f"\n"
                     f"\n"
                     f"{args.footnote}")
        payload = {"title": "FontBakery Report", "body": body}
        r = requests.post(issue_url, json.dumps(payload), headers=headers)
        logger.info("Issue posted {}".format(r.json()["html_url"]))
    elif args.post_issue and not args.gh_token:
        logger.info("Failed to post issue!\n"
                    "\n"
                    "Include your Github api token.")

    shutil.rmtree(tmp)


if __name__ == "__main__":
    main()


# snippets/test_nametable-from-filename.py

#!/usr/bin/env python
# Copyright 2013,2016 The Font Bakery Authors.
#
# 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.
#
# See AUTHORS.txt for the list of Authors and LICENSE.txt for the License.
import unittest
import os
from fontTools.ttLib import TTFont
script = __import__("fontbakery-nametable-from-filename")


class NameTableFromTTFName(unittest.TestCase):
    def _font_renaming(self, f_path):
        """The test fonts have been generated from Glyphsapp and conform
        to the googlefonts nametable spec. The test should pass if the new
        nametable matches the test font's name table."""
        fonts_paths = [os.path.join(f_path, f) for f in os.listdir(f_path)
                       if '.ttf' in f]

        for font_path in fonts_paths:
            font = TTFont(font_path)
            old_nametable = font['name']
            new_nametable = script.nametable_from_filename(font_path)

            for field in script.REQUIRED_FIELDS:
                if old_nametable.getName(*field):
                    enc = old_nametable.getName(*field).getEncoding()
                    self.assertEqual(
                        str(old_nametable.getName(*field)).decode(enc),
                        str(new_nametable.getName(*field)).decode(enc),
                    )

    def test_nunito_renaming(self):
        """Nunito Chosen because it has another family Nunito Heavy and a lot
        of weights"""
        f_path = os.path.join('data', 'test', 'nunito')
        self._font_renaming(f_path)

    def test_cabin_renaming(self):
        """Cabin chosen because it has a seperate Condensed family"""
        f_path = os.path.join('data', 'test', 'cabin')
        self._font_renaming(f_path)

    def test_glyphsapp_family_sans_export(self):
        """The ultimate test. Can this naming tool repoduce Google Font's
        Naming schema.
        Source repo here: https://github.com/davelab6/glyphs-export"""
        f_path = os.path.join('data', 'test', 'familysans')
        self._font_renaming(f_path)


if __name__ == '__main__':
    unittest.main()


# tests/commands/test_usage.py

import os
import subprocess
import sys
import re
import pytest
import tempfile


def test_list_subcommands_has_all_scripts():
    """Tests if the output from running `fontbakery --list-subcommands` matches
      the fontbakery scripts within the bin folder and the promoted profiles."""
    import fontbakery.commands
    from fontbakery.cli import CLI_PROFILES
    commands_dir = os.path.dirname(fontbakery.commands.__file__)

    scripts = [
        f.rstrip(".py").replace("_", "-")
        for f in os.listdir(commands_dir)
        if (f.endswith(".py") and not f.startswith('_'))
    ]
    scripts = scripts + [ ("check-"+i).replace("_", "-") for i in CLI_PROFILES ]
    subcommands = subprocess.check_output(
        ['fontbakery', '--list-subcommands']).decode().split()
    assert sorted(scripts) == sorted(subcommands)


def test_command_check_googlefonts():
    """Test if `fontbakery check-googlefonts` can run successfully`."""
    subprocess.check_output(["fontbakery", "check-googlefonts", "-h"])

    test_font = os.path.join("data", "test", "nunito", "Nunito-Regular.ttf")

    subprocess.check_output([
        "fontbakery", "check-googlefonts", "-c", "com.google.fonts/check/canonical_filename",
        test_font
    ])

    with pytest.raises(subprocess.CalledProcessError):
        subprocess.check_output(["fontbakery", "check-googlefonts"])


#@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher")
@pytest.mark.xfail(strict=True) # This test is too much prone to failing whenever we update
                                # the text-output formatting or the actual log messages in that fontbakery check
                                # I would like to have this test refactored to be in a good state for much longer.
                                # Please, only remove the xfail mark once the test is more robust / future proof.
def test_status_log_is_indented():
    """Test if statuses are printed in a limited boundary."""
    test_font = os.path.join("data", "test", "nunito", "Nunito-Regular.ttf")

    result = subprocess.run(["fontbakery", "check-googlefonts",
        "-c", "old_ttfautohint", "-c", "font_copyright",
        test_font], capture_output=True)

    p = re.compile('([^][a-zA-Z0-9@:,.()\'"\n ])', re.I | re.M)
    stdout = p.sub('#', result.stdout.decode()).split('\n')
    assert '\n'.join(stdout[24:30]) == '\n'.join([
    '     #[0#31#40mFAIL#[0m Name Table entry: Copyright notices should match a      ',
    '          pattern similar to: "Copyright 2019 The Familyname Project Authors    ',
    '          (git url)"                                                            ',
    '          But instead we have got:                                              ',
    '          "Copyright 2014 The Nunito Project Authors (contact@sansoxygen.com)"  ',
    '          [code: bad#notice#format]                                             '
    ])
    assert '\n'.join(stdout[10:15]) == '\n'.join([
    '     #[0#36#40mINFO#[0m Could not detect which version of ttfautohint was used  ',
    '          in this font. It is typically specified as a comment in the font      ',
    "          version entries of the 'name' table. Such font version strings are    ",
    "          currently: ['Version 3.000', 'Version 3.000'] [code:                  ",
    '          version#not#detected]                                                 '
    ])


def test_command_check_profile():
    """Test if `fontbakery check-profile` can run successfully`."""
    subprocess.check_output(["fontbakery", "check-profile", "-h"])

    with pytest.raises(subprocess.CalledProcessError):
        subprocess.check_output(["fontbakery", "check-profile"])


def test_command_check_opentype():
    """Test if `fontbakery check-opentype` can run successfully`."""
    subprocess.check_output(["fontbakery", "check-opentype", "-h"])

    with pytest.raises(subprocess.CalledProcessError):
        subprocess.check_output(["fontbakery", "check-opentype"])


def test_command_check_ufo_sources():
    """Test if `fontbakery check-ufo-sources` can run successfully`."""
    subprocess.check_output(["fontbakery", "check-ufo-sources", "-h"])

    with pytest.raises(subprocess.CalledProcessError):
        subprocess.check_output(["fontbakery", "check-ufo-sources"])

def test_command_config_file():
    """Test if we can set checks using a config file."""
    config = tempfile.NamedTemporaryFile(delete=False)
    config.write(b"explicit_checks = ['com.adobe.fonts/check/name/empty_records']")
    config.close()
    test_font = os.path.join("data", "test", "nunito", "Nunito-Regular.ttf")
    result = subprocess.run(["fontbakery", "check-googlefonts",
        "--config", config.name,
        test_font], stdout=subprocess.PIPE)
    stdout = result.stdout.decode()
    assert "running 1 individual check" in stdout
    os.unlink(config.name)


def test_command_config_file_injection():
    """Test if we can inject a config variable into a check."""
    config = tempfile.NamedTemporaryFile(delete=False)
    config.write(b"""
[a_test_profile]
OK = 123
""")
    config.close()
    test_font = os.path.join("data", "test", "nunito", "Nunito-Regular.ttf")
    test_profile = os.path.join("tests", "profiles", "a_test_profile.py")
    result = subprocess.run(["fontbakery", "check-profile",
        "-C",
        "--config", config.name,
        test_profile,
        test_font], stdout=subprocess.PIPE)
    stdout = result.stdout.decode()
    assert "FAIL: 0" in stdout
    os.unlink(config.name)


def test_config_override():
    """Test we can override check statuses in the configuration file"""
    config = tempfile.NamedTemporaryFile(delete=False)
    config.write(b"""
overrides:
  com.google.fonts/check/file_size:
    large-font: FAIL
explicit_checks:
  - com.google.fonts/check/file_size
""")
    config.close()
    test_font = os.path.join("data", "test", "varfont", "inter", "Inter[slnt,wght].ttf")
    result = subprocess.run(["fontbakery", "check-googlefonts",
        "-C",
        "--config", config.name,
        test_font], stdout=subprocess.PIPE)
    stdout = result.stdout.decode()
    # This font has a WARN here, so should now FAIL
    assert "FAIL: 1" in stdout
    os.unlink(config.name)


# tests/profiles/a_test_profile.py

from fontbakery.callable import check
from fontbakery.section import Section
from fontbakery.status import PASS, FAIL, WARN
from fontbakery.fonts_profile import profile_factory
from fontbakery.message import Message

profile = profile_factory(default_section=Section("Just a Test"))

@check(
    id='com.google.fonts/check_for_testing/configuration',
    rationale="""
        Check that we can inject the configuration object and read it.
    """,
)
def com_google_fonts_check_for_testing_configuration(config):
    """Check if we can inject a config file"""
    if config and "a_test_profile" in config and config["a_test_profile"]["OK"] == 123:
        yield PASS, 'we have injected a config'
    else:
        yield FAIL, "config variable didn't look like we expected"


profile.auto_register(globals())


# tests/profiles/adobefonts_test.py

"""
Unit tests for Adobe Fonts profile.
"""
import os
from unittest.mock import patch

from fontTools.ttLib import TTFont
from requests.exceptions import ConnectionError

from fontbakery.checkrunner import WARN, FAIL, ERROR, PASS, SKIP
from fontbakery.codetesting import (
    assert_PASS,
    assert_results_contain,
    CheckTester,
    portable_path,
    TEST_FILE,
)
from fontbakery.profiles import adobefonts as adobefonts_profile
from fontbakery.profiles.adobefonts import (
    ADOBEFONTS_PROFILE_CHECKS,
    OVERRIDDEN_CHECKS,
    profile,
    SET_EXPLICIT_CHECKS,
)
from fontbakery.profiles.shared_conditions import vmetrics

OVERRIDE_SUFFIX = ":adobefonts"


def test_get_family_checks():
    """Validate the set of family checks."""
    family_checks = profile.get_family_checks()
    family_check_ids = {check.id for check in family_checks}
    expected_family_check_ids = {
        "com.adobe.fonts/check/family/bold_italic_unique_for_nameid1",
        "com.adobe.fonts/check/family/consistent_upm",
        "com.adobe.fonts/check/family/max_4_fonts_per_family_name",
        "com.google.fonts/check/family/underline_thickness",
        "com.google.fonts/check/family/panose_proportion",
        "com.google.fonts/check/family/panose_familytype",
        "com.google.fonts/check/family/equal_unicode_encodings",
        "com.google.fonts/check/family/equal_font_versions",
        f"com.google.fonts/check/family/win_ascent_and_descent{OVERRIDE_SUFFIX}",
        "com.google.fonts/check/family/vertical_metrics",
        "com.google.fonts/check/family/single_directory",
        # should it be included here? or should we have
        # a get_superfamily_checks() method?
        # 'com.google.fonts/check/superfamily/vertical_metrics',
    }
    assert family_check_ids == expected_family_check_ids


def test_profile_check_set():
    """Confirm that the profile has the correct number of checks and the correct
    set of check IDs."""
    assert len(SET_EXPLICIT_CHECKS) == 79
    explicit_with_overrides = sorted(
        f"{check_id}{OVERRIDE_SUFFIX}" if check_id in OVERRIDDEN_CHECKS else check_id
        for check_id in SET_EXPLICIT_CHECKS
    )
    assert explicit_with_overrides == sorted(ADOBEFONTS_PROFILE_CHECKS)


def test_check_family_consistent_upm():
    """A group of fonts designed & produced as a family should have consistent
    units per em."""
    check = CheckTester(
        adobefonts_profile, "com.adobe.fonts/check/family/consistent_upm"
    )

    # these fonts have a consistent unitsPerEm of 1000:
    filenames = [
        "SourceSansPro-Regular.otf",
        "SourceSansPro-Bold.otf",
        "SourceSansPro-Italic.otf",
    ]
    fonts = [
        os.path.join(portable_path("data/test/source-sans-pro/OTF"), filename)
        for filename in filenames
    ]

    # try fonts with consistent UPM (i.e. 1000)
    assert_PASS(check(fonts))

    ttFonts = check["ttFonts"]
    # now try with one font with a different UPM (i.e. 2048)
    ttFonts[1]["head"].unitsPerEm = 2048
    assert_results_contain(check(ttFonts), FAIL, "inconsistent-upem")


def test_check_find_empty_letters():
    """Validate that empty glyphs are found."""
    check = CheckTester(adobefonts_profile, "com.adobe.fonts/check/find_empty_letters")

    PASS_MSG = "No empty glyphs for letters found."

    # this OT-CFF font has inked glyphs for all letters
    ttFont = TTFont(TEST_FILE("source-sans-pro/OTF/SourceSansPro-Regular.otf"))
    assert assert_PASS(check(ttFont)) == PASS_MSG

    # this OT-CFF2 font has inked glyphs for all letters
    ttFont = TTFont(TEST_FILE("source-sans-pro/VAR/SourceSansVariable-Italic.otf"))
    assert assert_PASS(check(ttFont)) == PASS_MSG

    # this TrueType font has inked glyphs for all letters
    ttFont = TTFont(TEST_FILE("source-sans-pro/TTF/SourceSansPro-Bold.ttf"))
    assert assert_PASS(check(ttFont)) == PASS_MSG

    # Add 2 Korean hangul syllable characters to cmap table mapped to the 'space' glyph.
    # These characters are part of the set whose glyphs are allowed to be blank.
    # The check should only yield a WARN.
    for cmap_table in ttFont["cmap"].tables:
        if cmap_table.format != 4:
            cmap_table.cmap[0xB646] = "space"
            cmap_table.cmap[0xD7A0] = "space"
    msg = assert_results_contain(check(ttFont), WARN, "empty-hangul-letter")
    assert msg == "Found 2 empty hangul glyph(s)."

    # this font has empty glyphs for several letters,
    # the first of which is 'B' (U+0042)
    ttFont = TTFont(TEST_FILE("familysans/FamilySans-Regular.ttf"))
    msg = assert_results_contain(check(ttFont), FAIL, "empty-letter")
    assert msg == "U+0042 should be visible, but its glyph ('B') is empty."


def _get_nameid_1_win_eng_record(name_table):
    """Helper method that returns the Windows nameID 1 US-English record"""
    for rec in name_table.names:
        if (rec.nameID, rec.platformID, rec.platEncID, rec.langID) == (1, 3, 1, 0x409):
            return rec
    return None


def test_check_nameid_1_win_english():
    """Validate that font has a good nameID 1, Windows/Unicode/US-English
    `name` table record."""
    check = CheckTester(
        adobefonts_profile, "com.adobe.fonts/check/nameid_1_win_english"
    )

    ttFont = TTFont(TEST_FILE("source-sans-pro/OTF/SourceSansPro-Regular.otf"))
    msg = assert_PASS(check(ttFont))
    assert msg == "Font contains a good Windows nameID 1 US-English record."

    name_table = ttFont["name"]
    nameid_1_win_eng_rec = _get_nameid_1_win_eng_record(name_table)

    # Replace the nameID 1 string with an empty string
    nameid_1_win_eng_rec.string = ""
    msg = assert_results_contain(check(ttFont), FAIL, "nameid-1-empty")
    assert msg == "Windows nameID 1 US-English record is empty."

    # Replace the nameID 1 string with data that can't be 'utf_16_be'-decoded
    nameid_1_win_eng_rec.string = "\xff".encode("utf_7")
    msg = assert_results_contain(check(ttFont), ERROR, "nameid-1-decoding-error")
    assert msg == "Windows nameID 1 US-English record could not be decoded."

    # Delete all 'name' table records
    name_table.names = []
    msg = assert_results_contain(check(ttFont), FAIL, "nameid-1-not-found")
    assert msg == "Windows nameID 1 US-English record not found."

    # Delete 'name' table
    del ttFont["name"]
    msg = assert_results_contain(check(ttFont), FAIL, "name-table-not-found")
    assert msg == "Font has no 'name' table."


def test_check_override_whitespace_glyphs():
    """Check that overridden test for nbsp yields WARN rather than FAIL."""
    check = CheckTester(
        adobefonts_profile, f"com.google.fonts/check/whitespace_glyphs{OVERRIDE_SUFFIX}"
    )

    ttFont = TTFont(TEST_FILE("source-sans-pro/OTF/SourceSansPro-Regular.otf"))
    assert_PASS(check(ttFont))

    # remove U+00A0, status should be WARN (standard check would be FAIL)
    for subtable in ttFont["cmap"].tables:
        subtable.cmap.pop(0x00A0, None)
    assert_results_contain(check(ttFont), WARN, "missing-whitespace-glyph-0x00A0")


def test_check_override_valid_glyphnames():
    """Check that overridden test yields WARN rather than FAIL."""
    check = CheckTester(
        adobefonts_profile, f"com.google.fonts/check/valid_glyphnames{OVERRIDE_SUFFIX}"
    )

    ttFont = TTFont(TEST_FILE("nunito/Nunito-Regular.ttf"))
    assert_PASS(check(ttFont))

    good_name = "b" * 63
    bad_name1 = "a" * 64
    bad_name2 = "3cents"
    bad_name3 = ".threecents"
    ttFont.glyphOrder[2] = bad_name1
    ttFont.glyphOrder[3] = bad_name2
    ttFont.glyphOrder[4] = bad_name3
    ttFont.glyphOrder[5] = good_name
    message = assert_results_contain(check(ttFont), WARN, "found-invalid-names")
    assert good_name not in message
    assert bad_name1 in message
    assert bad_name2 in message
    assert bad_name3 in message


def test_check_override_family_win_ascent_and_descent():
    """Check that overridden test yields WARN rather than FAIL."""
    check = CheckTester(
        adobefonts_profile,
        f"com.google.fonts/check/family/win_ascent_and_descent{OVERRIDE_SUFFIX}",
    )

    # Our reference Mada Regular is know to FAIL the original check.
    # The overridden check should just WARN.
    ttFont = TTFont(TEST_FILE("mada/Mada-Regular.ttf"))
    msg = assert_results_contain(check(ttFont), WARN, "ascent")
    assert msg == (
        "OS/2.usWinAscent value should be equal or greater than 880,"
        " but got 776 instead"
    )

    vm = vmetrics([ttFont])
    y_max = vm["ymax"]
    y_min = vm["ymin"]
    os2_table = ttFont["OS/2"]

    # Now change 'OS/2.usWinAscent' to be more than double 'head.yMax'.
    # The overridden check should just WARN.
    os2_table.usWinAscent = y_max * 2 + 1
    msg = assert_results_contain(check(ttFont), WARN, "ascent")
    assert msg == (
        "OS/2.usWinAscent value 1761 is too large."
        " It should be less than double the yMax. Current yMax value is 880"
    )

    # Now fix the value of 'OS/2.usWinAscent'. The overridden check should PASS.
    os2_table.usWinAscent = y_max
    msg = assert_PASS(check(ttFont), PASS)
    assert msg == "OS/2 usWinAscent & usWinDescent values look good!"

    # Now mess up the 'OS/2.usWinDescent' value. The overridden check should just WARN.
    os2_table.usWinDescent = abs(y_min) - 10
    msg = assert_results_contain(check(ttFont), WARN, "descent")
    assert msg == (
        "OS/2.usWinDescent value should be equal or greater than 292,"
        " but got 282 instead."
    )

    # Now change 'OS/2.usWinDescent' to be more than double 'head.yMin'.
    # The overridden check should just WARN.
    os2_table.usWinDescent = abs(y_min) * 2 + 1
    msg = assert_results_contain(check(ttFont), WARN, "descent")
    assert msg == (
        "OS/2.usWinDescent value 585 is too large."
        " It should be less than double the yMin. Current absolute yMin value is 292"
    )


def test_check_override_os2_metrics_match_hhea():
    """Check that overridden test yields WARN rather than FAIL."""
    check = CheckTester(
        adobefonts_profile,
        f"com.google.fonts/check/os2_metrics_match_hhea{OVERRIDE_SUFFIX}",
    )

    # Our reference Mada Regular is know to be good here.
    ttFont = TTFont(TEST_FILE("mada/Mada-Regular.ttf"))

    msg = assert_PASS(check(ttFont), PASS)
    assert msg == "OS/2.sTypoAscender/Descender values match hhea.ascent/descent."

    os2_table = ttFont["OS/2"]
    hhea_table = ttFont["hhea"]
    ascent = hhea_table.ascent
    descent = hhea_table.descent

    # Now we change sTypoAscender to be bad. The overridden check should just WARN.
    os2_table.sTypoAscender = ascent - 100
    msg = assert_results_contain(check(ttFont), WARN, "ascender")
    assert msg == "OS/2 sTypoAscender (800) and hhea ascent (900) must be equal."

    # Restore 'sTypoAscender' to a good value.
    os2_table.sTypoAscender = ascent

    # And break the font again, now changing the sTypoDescender value.
    # The overridden check should just WARN.
    os2_table.sTypoDescender = descent + 100
    msg = assert_results_contain(check(ttFont), WARN, "descender")
    assert msg == "OS/2 sTypoDescender (-200) and hhea descent (-300) must be equal."


@patch("freetype.Face", side_effect=ImportError)
def test_check_override_freetype_rasterizer(mock_import_error):
    """Check that overridden test yields ERROR rather than SKIP."""
    check = CheckTester(
        adobefonts_profile,
        f"com.adobe.fonts/check/freetype_rasterizer{OVERRIDE_SUFFIX}",
    )

    font = TEST_FILE("cabin/Cabin-Regular.ttf")
    msg = assert_results_contain(check(font), ERROR, "freetype-not-installed")
    assert "FreeType is not available" in msg


@patch("requests.get", side_effect=ConnectionError)
def test_check_override_fontbakery_version(mock_get):
    """Check that overridden test yields SKIP rather than FAIL."""
    check = CheckTester(
        adobefonts_profile,
        f"com.google.fonts/check/fontbakery_version{OVERRIDE_SUFFIX}",
    )

    font = TEST_FILE("cabin/Cabin-Regular.ttf")
    msg = assert_results_contain(check(font), SKIP, "connection-error")
    assert "Request to PyPI.org failed with this message" in msg


# tests/profiles/cff_test.py

from fontTools.ttLib import TTFont

from fontbakery.codetesting import (assert_PASS,
                                    assert_results_contain,
                                    CheckTester,
                                    TEST_FILE)
from fontbakery.checkrunner import (DEBUG, INFO, WARN, ERROR, SKIP, PASS, FAIL)
from fontbakery.profiles import cff as cff_profile

check_statuses = (ERROR, FAIL, SKIP, PASS, WARN, INFO, DEBUG)


def test_check_cff_call_depth():
    check = CheckTester(cff_profile,
                        "com.adobe.fonts/check/cff_call_depth")

    # this font's CFF subr call depths should all be <= 10:
    font = TEST_FILE('source-sans-pro/OTF/SourceSansPro-Regular.otf')
    assert_PASS(check(font))

    # in this font, glyphs D & E exceed max call depth,
    # and glyph F calls a subroutine that calls itself
    font = TEST_FILE('subr_test_fonts/subr_test_font_infinite_recursion.otf')

    assert_results_contain(check(font),
                           FAIL, 'max-depth',
                           '- Subroutine call depth exceeded'
                           ' maximum of 10 for glyph "D".')

    assert_results_contain(check(font),
                           FAIL, 'max-depth',
                           '- Subroutine call depth exceeded'
                           ' maximum of 10 for glyph "E".')

    assert_results_contain(check(font),
                           FAIL, 'recursion-error',
                           '- Recursion error while decompiling glyph "F".')


def test_check_cff2_call_depth():
    check = CheckTester(cff_profile,
                        "com.adobe.fonts/check/cff2_call_depth")

    # this font's CFF subr call depths should all be <= 10:
    font = TEST_FILE('source-sans-pro/VAR/SourceSansVariable-Roman.otf')
    assert_PASS(check(font))

    # in this font, glyphs D & E exceed max call depth,
    # and glyph F calls a subroutine that calls itself
    font = TEST_FILE('subr_test_fonts/var_subr_test_font_infinite_recursion.otf')

    assert_results_contain(check(font),
                           FAIL, 'max-depth',
                           'Subroutine call depth exceeded'
                           ' maximum of 10 for glyph "D".')

    assert_results_contain(check(font),
                           FAIL, 'max-depth',
                           'Subroutine call depth exceeded'
                           ' maximum of 10 for glyph "E".')

    assert_results_contain(check(font),
                           FAIL, 'recursion-error',
                           'Recursion error while decompiling glyph "F".')


def test_check_cff_deprecated_operators():
    check = CheckTester(cff_profile,
                        "com.adobe.fonts/check/cff_deprecated_operators")

    # this font uses the deprecated 'dotsection' operator
    font = TEST_FILE('deprecated_operators/cff1_dotsection.otf')
    assert_results_contain(check(font),
                           WARN, 'deprecated-operator-dotsection',
                           'Glyph "i" uses deprecated "dotsection" operator.')

    # this font uses the 'endchar' operator in a manner that is deprecated ("seac")
    font = TEST_FILE('deprecated_operators/cff1_endchar_seac.otf')
    assert_results_contain(check(font),
                           FAIL, 'deprecated-operation-endchar-seac',
                           'Glyph "Agrave" has deprecated use of "endchar" operator to build accented characters (seac).')


# tests/profiles/cmap_test.py

import pytest
from fontTools.ttLib import TTFont

from fontbakery.codetesting import (assert_PASS,
                                    assert_results_contain,
                                    CheckTester,
                                    TEST_FILE)
from fontbakery.checkrunner import (DEBUG, INFO, WARN, ERROR, SKIP, PASS, FAIL)
from fontbakery.profiles import opentype as opentype_profile

check_statuses = (ERROR, FAIL, SKIP, PASS, WARN, INFO, DEBUG)

mada_fonts = [
    TEST_FILE("mada/Mada-Black.ttf"),
    TEST_FILE("mada/Mada-ExtraLight.ttf"),
    TEST_FILE("mada/Mada-Medium.ttf"),
    TEST_FILE("mada/Mada-SemiBold.ttf"),
    TEST_FILE("mada/Mada-Bold.ttf"),
    TEST_FILE("mada/Mada-Light.ttf"),
    TEST_FILE("mada/Mada-Regular.ttf")
]

@pytest.fixture
def mada_ttFonts():
    return [TTFont(path) for path in mada_fonts]


def test_check_family_equal_unicode_encodings(mada_ttFonts):
    """ Fonts have equal unicode encodings ? """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/family/equal_unicode_encodings")

    from fontbakery.constants import WindowsEncodingID

    # our reference Mada family is know to be good here.
    assert_PASS(check(mada_ttFonts),
                'with good family.')

    bad_ttFonts = mada_ttFonts
    # introduce mismatching encodings into the first 2 font files:
    for i, encoding in enumerate([WindowsEncodingID.SYMBOL,
                                  WindowsEncodingID.UNICODE_BMP]):
        for table in bad_ttFonts[i]['cmap'].tables:
            if table.format == 4:
                table.platEncID = encoding

    assert_results_contain(check(bad_ttFonts),
                           FAIL, 'mismatch',
                           'with fonts that diverge on unicode encoding.')


# Note: I am not aware of any real-case of a font that FAILs this check.
def test_check_all_glyphs_have_codepoints():
    """ Check all glyphs have codepoints assigned. """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/all_glyphs_have_codepoints")

    # our reference Mada SemiBold is know to be good here.
    ttFont = TTFont(TEST_FILE("mada/Mada-SemiBold.ttf"))
    assert_PASS(check(ttFont),
                'with a good font.')

    # This is a silly way to break the font.
    # A much better test would rather use a real font file that has the problem.
    ttFont['cmap'].tables[0].cmap[None] = "foo"

    assert_results_contain(check(ttFont),
                           FAIL, 'glyph-lacks-codepoint',
                           'with a font in which a glyph'
                           ' lacks a unicode codepoint assignment.')



# tests/profiles/dsig_test.py

from fontbakery.codetesting import (assert_PASS,
                                    assert_results_contain,
                                    CheckTester,
                                    TEST_FILE)
from fontbakery.checkrunner import WARN
from fontbakery.profiles import opentype as opentype_profile

from fontTools.ttLib import TTFont


def test_check_dsig():
    """ Does the font have a DSIG table ? """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/dsig")

    # Our reference Cabin Regular font is bad (theres a DSIG table declared):
    ttFont = TTFont(TEST_FILE("cabin/Cabin-Regular.ttf"))
    assert_results_contain(check(ttFont),
                           WARN, 'found-DSIG',
                           'with a font containing a DSIG table...')

    # Then we remove the DSIG table and it should now PASS the check:
    del ttFont['DSIG']
    assert_PASS(check(ttFont),
                'with a good font...')


# tests/profiles/external_profile_test.py

from fontbakery.section import Section
from fontbakery.fonts_profile import profile_factory


def check_filter(item_type, item_id, item):
    # Filter out external tool checks for testing purposes.
    if item_type == "check" and item_id in (
        "com.google.fonts/check/ots",
        "com.google.fonts/check/fontvalidator",
    ):
        return False

    return True


def test_external_profile():
    """Test the creation of external profiles."""
    profile = profile_factory(default_section=Section("Dalton Maag OpenType"))
    profile.auto_register(
        globals(),
        profile_imports=["fontbakery.profiles.opentype"],
        filter_func=check_filter)

    # Probe some tests
    expected_tests = ["com.google.fonts/check/family/panose_proportion",
                      "com.google.fonts/check/varfont/regular_opsz_coord"]
    profile.test_expected_checks(expected_tests)

    # Probe tests we don't want
    assert "com.google.fonts/check/fontvalidator" not in profile._check_registry.keys()

    assert len(profile.sections) > 1

def test_profile_imports():
    """
      When a names array in profile_imports contained sub module names, the import
      would fail.

      https://github.com/googlefonts/fontbakery/issues/1886
    """
    def _test(profile_imports, expected_tests,expected_conditions=tuple()):
        profile = profile_factory(default_section=Section("Testing"))
        profile.auto_register({}, profile_imports=profile_imports)
        profile.test_expected_checks(expected_tests)
        if expected_conditions:
            registered_conditions = profile.conditions.keys()
            for name in expected_conditions:
                assert name in registered_conditions, \
                       f'"{name}" is expected to be registered as a condition.'

    # this is in docs/writing profiles
    profile_imports = [
        ['fontbakery.profiles', ['cmap', 'head']]
    ]
    # Probe some tests
    expected_tests = [
        "com.google.fonts/check/all_glyphs_have_codepoints", # in cmap
        "com.google.fonts/check/unitsperem"  # in head
    ]
    _test(profile_imports, expected_tests)


    # the example from issue #1886
    profile_imports = (
        (
            "fontbakery.profiles",
            (
                "cmap",
                "head",
                "os2",
                "post",
                "name",
                "hhea",
                "dsig",
                "hmtx",
                "gpos",
                "kern",
                "glyf",
                "fvar",
                "shared_conditions",
            ),
        ),
    )
    # Probe some tests
    expected_tests = [
        "com.google.fonts/check/all_glyphs_have_codepoints", # in cmap
        "com.google.fonts/check/unitsperem"  # in head
    ]
    _test(profile_imports, expected_tests)


    # make sure the suggested workaround still works:
    # https://github.com/googlefonts/fontbakery/issues/1886#issuecomment-392535435
    profile_imports = (
        "fontbakery.profiles.cmap",
        "fontbakery.profiles.head",
        "fontbakery.profiles.os2",
        "fontbakery.profiles.post",
        "fontbakery.profiles.name",
        "fontbakery.profiles.hhea",
        "fontbakery.profiles.dsig",
        "fontbakery.profiles.hmtx",
        "fontbakery.profiles.gpos",
        "fontbakery.profiles.kern",
        "fontbakery.profiles.glyf",
        "fontbakery.profiles.fvar",
        "fontbakery.profiles.shared_conditions"
    )
    # Probe some tests
    expected_tests = [
        "com.google.fonts/check/all_glyphs_have_codepoints", # in cmap
        "com.google.fonts/check/unitsperem"  # in head
    ]
    _test(profile_imports, expected_tests)


    # cherry pick attributes from a module (instead of getting submodules)
    # also from this is in docs/writing profiles
    # Import just certain attributes from modules.
    # Also, using absolute import module names:
    profile_imports = [
        # like we do in fontbakery.profiles.fvar
        ('fontbakery.profiles.shared_conditions', ('is_variable_font',
            'regular_wght_coord', 'regular_wdth_coord', 'regular_slnt_coord',
            'regular_ital_coord', 'regular_opsz_coord', 'bold_wght_coord')),
        # just as an example: import a check and a dependency/condition of
        # that check from the googlefonts specific profile:
        ('fontbakery.profiles.googlefonts', (
            # "License URL matches License text on name table?"
            'com_google_fonts_check_name_license_url',
            # This condition is a dependency of the check above:
            'familyname',
        ))
    ]
    # Probe some tests
    expected_tests = [
        "com.google.fonts/check/name/license_url"  # in googlefonts
    ]
    expected_conditions = ('is_variable_font', 'regular_wght_coord',
        'regular_wdth_coord', 'regular_slnt_coord', 'regular_ital_coord',
        'regular_opsz_coord', 'bold_wght_coord', 'familyname')
    _test(profile_imports, expected_tests, expected_conditions)


def test_opentype_checks_load():
    profile_imports = ("fontbakery.profiles.opentype", )
    profile = profile_factory(default_section=Section("OpenType Testing"))
    profile.auto_register({}, profile_imports=profile_imports)
    profile.test_dependencies()


def test_googlefonts_checks_load():
    profile_imports = ("fontbakery.profiles.googlefonts", )
    profile = profile_factory(default_section=Section("Google Fonts Testing"))
    profile.auto_register({}, profile_imports=profile_imports)
    profile.test_dependencies()


def test_in_and_exclude_checks():
    profile_imports = ("fontbakery.profiles.opentype", )
    profile = profile_factory(default_section=Section("OpenType Testing"))
    profile.auto_register({}, profile_imports=profile_imports)
    profile.test_dependencies()
    explicit_checks = ["06", "07"]  # "06" or "07" in check ID
    exclude_checks = ["065", "079"]  # "065" or "079" in check ID
    iterargs = {"font": 1}
    check_names = {
        c[1].id for c in \
            profile.execution_order(iterargs,
                                    explicit_checks=explicit_checks,
                                    exclude_checks=exclude_checks)
    }
    check_names_expected = set()
    for section in profile.sections:
        for check in section.checks:
            if any(i in check.id
                   for i in explicit_checks) and \
               not any(x in check.id
                       for x in exclude_checks):
                check_names_expected.add(check.id)
    assert check_names == check_names_expected


def test_in_and_exclude_checks_default():
    profile_imports = ("fontbakery.profiles.opentype",)
    profile = profile_factory(default_section=Section("OpenType Testing"))
    profile.auto_register({}, profile_imports=profile_imports)
    profile.test_dependencies()
    explicit_checks = None  # "All checks aboard"
    exclude_checks = None  # "No checks left behind"
    iterargs = {"font": 1}
    check_names = {
        c[1].id for c in \
            profile.execution_order(iterargs,
                                    explicit_checks=explicit_checks,
                                    exclude_checks=exclude_checks)
    }
    check_names_expected = set()
    for section in profile.sections:
        for check in section.checks:
            check_names_expected.add(check.id)
    assert check_names == check_names_expected


# tests/profiles/fontval_test.py

import pytest
import shutil

from fontbakery.codetesting import (TEST_FILE,
                                    assert_results_contain)
from fontbakery.checkrunner import ERROR
from fontbakery.profiles import fontval as fontval_profile

@pytest.mark.skipif(not shutil.which("FontValidator"),
  reason="FontValidator is not installed on your system")
def test_check_fontvalidator():
    """ MS Font Validator checks """
    # check = CheckTester(fontval_profile,
    #                     "com.google.fonts/check/fontvalidator")
    check = fontval_profile.com_google_fonts_check_fontvalidator

    font = TEST_FILE("mada/Mada-Regular.ttf")

    # Then we make sure that there wasn't an ERROR
    # which would mean FontValidator is not properly installed:
    for status, message in check(font):
        assert status != ERROR

    # Simulate FontVal missing.
    import os
    old_path = os.environ["PATH"]
    os.environ["PATH"] = ""
    with pytest.raises(OSError) as _:
        assert_results_contain(check(font),
                               ERROR, "fontval-not-available")
    os.environ["PATH"] = old_path



# tests/profiles/fontwerk_test.py

from fontTools.ttLib import TTFont
from fontbakery.checkrunner import FAIL
from fontbakery.codetesting import (assert_PASS,
                                    assert_results_contain,
                                    CheckTester,
                                    TEST_FILE)
from fontbakery.profiles import fontwerk as fontwerk_profile


def test_check_name_no_mac_entries():
    check = CheckTester(fontwerk_profile,
                        'com.fontwerk/check/no_mac_entries')

    font = TEST_FILE('abeezee/ABeeZee-Italic.ttf')
    assert_results_contain(check(font),
                           FAIL, 'mac-names',
                           'with a font containing Mac names')

    font = TEST_FILE('source-sans-pro/OTF/SourceSansPro-Regular.otf')
    assert_PASS(check(font),
                'with a font without Mac names')


def test_check_vendor_id():
    check = CheckTester(fontwerk_profile,
                        'com.fontwerk/check/vendor_id')

    ttFont = TTFont(TEST_FILE('abeezee/ABeeZee-Italic.ttf'))
    assert_results_contain(check(ttFont),
                           FAIL, 'bad-vendor-id',
                           ", but should be 'WERK'.")

    ttFont['OS/2'].achVendID = 'WERK'
    assert_PASS(check(ttFont),
                "'WERK' is correct.")


def test_check_weight_class_fvar():
    check = CheckTester(fontwerk_profile,
                        'com.fontwerk/check/weight_class_fvar')

    ttFont = TTFont(TEST_FILE('varfont/Oswald-VF.ttf'))
    assert_PASS(check(ttFont),
                "matches fvar default value.")

    ttFont['OS/2'].usWeightClass = 333
    assert_results_contain(check(ttFont),
                           FAIL, 'bad-weight-class',
                           "but should match fvar default value.")


def test_check_inconsistencies_between_fvar_stat():
    check = CheckTester(fontwerk_profile,
                        'com.fontwerk/check/inconsistencies_between_fvar_stat')

    ttFont = TTFont(TEST_FILE("cabinvf/Cabin[wdth,wght].ttf"))
    msg = assert_PASS(check(ttFont), "with a good varfont...")
    assert msg == "STAT and fvar axis records are consistent."

    ttFont = TTFont(TEST_FILE("bad_fonts/fvar_stat_differences/AxisLocationVAR.ttf"))
    ttFont['name'].removeNames(nameID=277)
    assert_results_contain(check(ttFont),
                           FAIL, 'missing-name-id',
                           'fvar instance is missing in the name table')

    # add name with wrong order of name parts
    ttFont['name'].setName('Medium Text', 277, 3, 1, 0x409)
    assert_results_contain(check(ttFont),
                           FAIL, 'missing-fvar-instance-axis-value',
                           'missing in STAT table')


def test_check_style_linking():
    check = CheckTester(fontwerk_profile,
                        'com.fontwerk/check/style_linking')

    font = TEST_FILE("bad_fonts/style_linking_issues/NotoSans-BoldItalic.ttf")
    assert_results_contain(check(font),
                           FAIL, 'style-linking-issue')

    font = TEST_FILE("bad_fonts/style_linking_issues/NotoSans-Bold.ttf")
    assert_results_contain(check(font),
                           FAIL, 'style-linking-issue')

    font = TEST_FILE("bad_fonts/style_linking_issues/NotoSans-MediumItalic.ttf")
    assert_PASS(check(font),
                "Style linking looks good.")


# tests/profiles/fvar_test.py

from fontTools.ttLib import TTFont
from fontTools.ttLib.tables._f_v_a_r import Axis

from fontbakery.checkrunner import (FAIL, WARN)
from fontbakery.codetesting import (assert_PASS,
                                    assert_results_contain,
                                    CheckTester)
from fontbakery.profiles import opentype as opentype_profile


def test_check_varfont_regular_wght_coord():
    """ The variable font 'wght' (Weight) axis coordinate
        must be 400 on the 'Regular' instance. """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/varfont/regular_wght_coord")

    # Our reference varfont CabinVFBeta.ttf
    # has a good Regular:wght coordinate
    ttFont = TTFont("data/test/cabinvfbeta/CabinVFBeta.ttf")
    assert_PASS(check(ttFont),
                'with a good Regular:wght coordinate...')

    # We then ensure the check detects it when we
    # introduce the problem by setting a bad value:
    ttFont["fvar"].instances[0].coordinates["wght"] = 500
    assert_results_contain(check(ttFont),
                           FAIL, 'not-400',
                           'with a bad Regular:wght coordinate (500)...')


def test_check_varfont_regular_wdth_coord():
    """ The variable font 'wdth' (Width) axis coordinate
        must be 100 on the 'Regular' instance. """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/varfont/regular_wdth_coord")

    # Our reference varfont CabinVFBeta.ttf
    # has a good Regular:wdth coordinate
    ttFont = TTFont("data/test/cabinvfbeta/CabinVFBeta.ttf")
    assert_PASS(check(ttFont),
                'with a good Regular:wdth coordinate...')

    # We then ensure the check detects it when we
    # introduce the problem by setting a bad value:
    ttFont["fvar"].instances[0].coordinates["wdth"] = 0
    assert_results_contain(check(ttFont),
                           FAIL, 'not-100',
                           'with a bad Regular:wdth coordinate (0)...')


def test_check_varfont_regular_slnt_coord():
    """ The variable font 'slnt' (Slant) axis coordinate
        must be zero on the 'Regular' instance. """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/varfont/regular_slnt_coord")

    # Our reference varfont, CabinVFBeta.ttf, lacks a 'slnt' variation axis.
    ttFont = TTFont("data/test/cabinvfbeta/CabinVFBeta.ttf")

    # So we add one:
    new_axis = Axis()
    new_axis.axisTag = "slnt"
    ttFont["fvar"].axes.append(new_axis)

    # and specify a bad coordinate for the Regular:
    ttFont["fvar"].instances[0].coordinates["slnt"] = 12
    # Note: I know the correct instance index for this hotfix because
    # I inspected our reference CabinVF using ttx

    # And with this the check must detect the problem:
    assert_results_contain(check(ttFont),
                           FAIL, 'non-zero',
                           'with a bad Regular:slnt coordinate (12)...')

    # We then fix the Regular:slnt coordinate value in order to PASS:
    assert_PASS(check(ttFont, {"regular_slnt_coord": 0}),
                'with a good Regular:slnt coordinate (zero)...')


def test_check_varfont_regular_ital_coord():
    """ The variable font 'ital' (Italic) axis coordinate
        must be zero on the 'Regular' instance. """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/varfont/regular_ital_coord")

    # Our reference varfont, CabinVFBeta.ttf, lacks an 'ital' variation axis.
    ttFont = TTFont("data/test/cabinvfbeta/CabinVFBeta.ttf")

    # So we add one:
    new_axis = Axis()
    new_axis.axisTag = "ital"
    ttFont["fvar"].axes.append(new_axis)

    # and specify a bad coordinate for the Regular:
    ttFont["fvar"].instances[0].coordinates["ital"] = 123
    # Note: I know the correct instance index for this hotfix because
    # I inspected the our reference CabinVF using ttx

    # And with this the check must detect the problem:
    assert_results_contain(check(ttFont),
                           FAIL, 'non-zero',
                           'with a bad Regular:ital coordinate (123)...')

    # but with zero it must PASS the check:
    assert_PASS(check(ttFont, {"regular_ital_coord": 0}),
                'with a good Regular:ital coordinate (zero)...')


def test_check_varfont_regular_opsz_coord():
    """ The variable font 'opsz' (Optical Size) axis coordinate
        should be between 10 and 16 on the 'Regular' instance. """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/varfont/regular_opsz_coord")

    # Our reference varfont, CabinVFBeta.ttf, lacks an 'opsz' variation axis.
    ttFont = TTFont("data/test/cabinvfbeta/CabinVFBeta.ttf")

    # So we add one:
    new_axis = Axis()
    new_axis.axisTag = "opsz"
    ttFont["fvar"].axes.append(new_axis)

    # and specify a bad coordinate for the Regular:
    ttFont["fvar"].instances[0].coordinates["opsz"] = 9
    # Note: I know the correct instance index for this hotfix because
    # I inspected the our reference CabinVF using ttx

    # Then we ensure the problem is detected:
    assert_results_contain(check(ttFont),
                           WARN, 'out-of-range',
                           'with a bad Regular:opsz coordinate (9)...')

    # We try yet another bad value
    # and the check should detect the problem:
    assert_results_contain(check(ttFont, {"regular_opsz_coord": 17}),
                           WARN, 'out-of-range',
                           'with another bad Regular:opsz value (17)...')

    # We then test with good default opsz values:
    for value in [10, 11, 12, 13, 14, 15, 16]:
        assert_PASS(check(ttFont, {"regular_opsz_coord": value}),
                    f'with a good Regular:opsz coordinate ({value})...')


def test_check_varfont_bold_wght_coord():
    """ The variable font 'wght' (Weight) axis coordinate
        must be 700 on the 'Bold' instance. """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/varfont/bold_wght_coord")

    # Our reference varfont CabinVFBeta.ttf
    # has a good Bold:wght coordinate
    ttFont = TTFont("data/test/cabinvfbeta/CabinVFBeta.ttf")
    assert_PASS(check(ttFont),
                'with a good Bold:wght coordinate...')

    # We then change the value to ensure the problem is properly detected by the check:
    ttFont["fvar"].instances[3].coordinates["wght"] = 600
    assert_results_contain(check(ttFont),
                           FAIL, 'not-700',
                           'with a bad Bold:wght coordinage (600)...')


def test_check_varfont_wght_valid_range():
    """ The variable font 'wght' (Weight) axis coordinate
        must be within spec range of 1 to 1000 on all instances. """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/varfont/wght_valid_range")

    # Our reference varfont CabinVFBeta.ttf
    # has all instances within the 1-1000 range
    ttFont = TTFont("data/test/cabinvfbeta/CabinVFBeta.ttf")
    assert_PASS(check(ttFont),
                'with a good varfont...')

    # We then introduce the problem by setting a bad value:
    ttFont["fvar"].instances[0].coordinates["wght"] = 0
    assert_results_contain(check(ttFont),
                           FAIL, 'out-of-range',
                           'with wght=0...')

    # And yet another bad value:
    ttFont["fvar"].instances[0].coordinates["wght"] = 1001
    assert_results_contain(check(ttFont),
                           FAIL, 'out-of-range',
                           'with wght=1001...')


def test_check_varfont_wdth_valid_range():
    """ The variable font 'wdth' (Width) axis coordinate
        must be within spec range of 1 to 1000 on all instances. """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/varfont/wdth_valid_range")

    # Our reference varfont CabinVFBeta.ttf
    # has all instances within the 1-1000 range
    ttFont = TTFont("data/test/cabinvfbeta/CabinVFBeta.ttf")
    assert_PASS(check(ttFont),
                'with a good varfont...')

    # We then introduce the problem by setting a bad value:
    ttFont["fvar"].instances[0].coordinates["wdth"] = 0
    assert_results_contain(check(ttFont),
                           FAIL, 'out-of-range',
                           'with wght=0...')

    # And yet another bad value:
    ttFont["fvar"].instances[0].coordinates["wdth"] = 1001
    assert_results_contain(check(ttFont),
                           FAIL, 'out-of-range',
                           'with wght=1001...')


def test_check_varfont_slnt_range():
    """ The variable font 'slnt' (Slant) axis coordinate
        specifies positive values in its range? """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/varfont/slnt_range")

    # Our reference Inter varfont has a bad slnt range
    ttFont = TTFont("data/test/varfont/inter/Inter[slnt,wght].ttf")
    assert_results_contain(check(ttFont),
                           WARN, 'unusual-range',
                           'with a varfont that has an unusual "slnt" range.')

    # We then fix the font-bug by flipping the slnt axis range:
    for i, axis in enumerate(ttFont["fvar"].axes):
        if axis.axisTag == "slnt":
            minValue, maxValue = axis.minValue, axis.maxValue
            ttFont["fvar"].axes[i].minValue = -maxValue
            ttFont["fvar"].axes[i].maxValue = -minValue

    # And it must now be good ;-)
    assert_PASS(check(ttFont))


def test_check_varfont_valid_axis_nameid():
    """The value of axisNameID used by each VariationAxisRecord must
    be greater than 255 and less than 32768."""
    check = CheckTester(
        opentype_profile, "com.adobe.fonts/check/varfont/valid_axis_nameid"
    )

    # The axisNameID values in the reference varfont are all valid
    ttFont = TTFont("data/test/cabinvf/Cabin[wdth,wght].ttf")
    msg = assert_PASS(check(ttFont), "with a good varfont...")
    assert msg == "All axisNameID values are valid."

    fvar_table = ttFont["fvar"]
    wght_axis = fvar_table.axes[0]
    wdth_axis = fvar_table.axes[1]

    # Change the axes' axisNameID to the maximum and minimum allowed values
    wght_axis.axisNameID = 32767
    wdth_axis.axisNameID = 256
    msg = assert_PASS(check(ttFont), "with a good varfont...")
    assert msg == "All axisNameID values are valid."

    # Change the axes' axisNameID to invalid values
    # (32768 is greater than the maximum, and 255 is less than the minimum)
    wght_axis.axisNameID = 32768
    wdth_axis.axisNameID = 255
    assert_results_contain(check(ttFont), FAIL, "invalid-axis-nameid:32768")
    msg = assert_results_contain(check(ttFont), FAIL, "invalid-axis-nameid:255")
    assert msg == (
        "'Unnamed' instance has an axisNameID value that"
        " is not greater than 255 and less than 32768."
    )

    # Another set of invalid values
    wght_axis.axisNameID = 128
    wdth_axis.axisNameID = 36000
    assert_results_contain(check(ttFont), FAIL, "invalid-axis-nameid:128")
    msg = assert_results_contain(check(ttFont), FAIL, "invalid-axis-nameid:36000")
    assert msg == (
        "'Unnamed' instance has an axisNameID value that"
        " is not greater than 255 and less than 32768."
    )


def test_check_varfont_valid_subfamily_nameid():
    """The value of subfamilyNameID used by each InstanceRecord must
    be 2, 17, or greater than 255 and less than 32768."""
    check = CheckTester(
        opentype_profile, "com.adobe.fonts/check/varfont/valid_subfamily_nameid"
    )

    # The subfamilyNameID values in the reference varfont are all valid
    ttFont = TTFont("data/test/cabinvf/Cabin[wdth,wght].ttf")
    msg = assert_PASS(check(ttFont), "with a good varfont...")
    assert msg == "All subfamilyNameID values are valid."

    fvar_table = ttFont["fvar"]
    inst_1 = fvar_table.instances[0]
    inst_2 = fvar_table.instances[1]
    inst_3 = fvar_table.instances[2]
    inst_4 = fvar_table.instances[3]

    # Change the instances' subfamilyNameID to
    # 2, 17 and to the maximum and minimum allowed values
    inst_1.subfamilyNameID = 2
    inst_2.subfamilyNameID = 17
    inst_3.subfamilyNameID = 256
    inst_4.subfamilyNameID = 32767
    msg = assert_PASS(check(ttFont), "with a good varfont...")
    assert msg == "All subfamilyNameID values are valid."

    # Change two instances' subfamilyNameID to invalid values
    # (32768 is greater than the maximum, and 255 is less than the minimum)
    inst_3.subfamilyNameID = 255
    inst_4.subfamilyNameID = 32768
    assert_results_contain(check(ttFont), FAIL, "invalid-subfamily-nameid:255")
    msg = assert_results_contain(check(ttFont), FAIL, "invalid-subfamily-nameid:32768")
    assert msg == (
        "'Unnamed' instance has a subfamilyNameID value that"
        " is neither 2, 17, or greater than 255 and less than 32768."
    )

    # Reset two subfamilyNameID to valid values,
    # then set two other subfamilyNameID to invalid values
    inst_3.subfamilyNameID = 256  # valid
    inst_4.subfamilyNameID = 32767  # valid
    inst_1.subfamilyNameID = 3
    inst_2.subfamilyNameID = 18
    assert_results_contain(check(ttFont), FAIL, "invalid-subfamily-nameid:3")
    msg = assert_results_contain(check(ttFont), FAIL, "invalid-subfamily-nameid:18")
    assert msg == (
        "'Unnamed' instance has a subfamilyNameID value that"
        " is neither 2, 17, or greater than 255 and less than 32768."
    )


def test_check_varfont_valid_postscript_nameid():
    """The value of postScriptNameID used by each InstanceRecord must
    be 6, 0xFFFF, or greater than 255 and less than 32768."""
    check = CheckTester(
        opentype_profile, "com.adobe.fonts/check/varfont/valid_postscript_nameid"
    )

    # The postScriptNameID values in the reference varfont are all valid
    ttFont = TTFont("data/test/cabinvf/Cabin[wdth,wght].ttf")
    msg = assert_PASS(check(ttFont), "with a good varfont...")
    assert msg == "All postScriptNameID values are valid."

    fvar_table = ttFont["fvar"]
    inst_1 = fvar_table.instances[0]
    inst_2 = fvar_table.instances[1]
    inst_3 = fvar_table.instances[2]
    inst_4 = fvar_table.instances[3]

    # Change the instances' postScriptNameID to
    # 6, 0xFFFF and to the maximum and minimum allowed values
    inst_1.postscriptNameID = 6
    inst_2.postscriptNameID = 0xFFFF
    inst_3.postscriptNameID = 256
    inst_4.postscriptNameID = 32767
    msg = assert_PASS(check(ttFont), "with a good varfont...")
    assert msg == "All postScriptNameID values are valid."

    # Change two instances' postScriptNameID to invalid values
    # (32768 is greater than the maximum, and 255 is less than the minimum)
    inst_3.postscriptNameID = 255
    inst_4.postscriptNameID = 32768
    assert_results_contain(check(ttFont), FAIL, "invalid-postscript-nameid:255")
    msg = assert_results_contain(check(ttFont), FAIL, "invalid-postscript-nameid:32768")
    assert msg == (
        "'Unnamed' instance has a postScriptNameID value that"
        " is neither 6, 0xFFFF, or greater than 255 and less than 32768."
    )

    # Reset two postScriptNameID to valid values,
    # then set two other postScriptNameID to invalid values
    inst_3.postscriptNameID = 256  # valid
    inst_4.postscriptNameID = 32767  # valid
    inst_1.postscriptNameID = 3
    inst_2.postscriptNameID = 18
    assert_results_contain(check(ttFont), FAIL, "invalid-postscript-nameid:3")
    msg = assert_results_contain(check(ttFont), FAIL, "invalid-postscript-nameid:18")
    assert msg == (
        "'Unnamed' instance has a postScriptNameID value that"
        " is neither 6, 0xFFFF, or greater than 255 and less than 32768."
    )


def test_check_varfont_valid_default_instance_nameids():
    """If an instance record is included for the default instance, then the
    subfamilyNameID value should be set to either 2 or 17, and the postScriptNameID
    value should be set to 6."""
    check = CheckTester(
        opentype_profile, "com.adobe.fonts/check/varfont/valid_default_instance_nameids"
    )

    # The 'Regular' instance record in the reference varfont has the same coordinates
    # as the default instance, but it doesn't use the correct nameID.
    ttFont = TTFont("data/test/cabinvf/Cabin[wdth,wght].ttf")
    msg = assert_results_contain(
        check(ttFont), FAIL, "invalid-default-instance-subfamily-nameid:258"
    )
    assert msg == (
        "'Regular' instance has the same coordinates as the default instance;"
        " its subfamilyNameID should be either 2 or 17, instead of 258."
    )

    # Correct the subfamilyNameID value of the default instance
    fvar_table = ttFont["fvar"]
    dflt_inst = fvar_table.instances[0]
    dflt_inst.subfamilyNameID = 2
    msg = assert_PASS(check(ttFont), "with a good varfont...")
    assert msg == "All default instance nameID values are valid."

    # Change subfamilyNameID value of the default instance to another valid value
    dflt_inst.subfamilyNameID = 17
    msg = assert_PASS(check(ttFont), "with a good varfont...")
    assert msg == "All default instance nameID values are valid."

    # The value of postScriptNameID is 0xFFFF for all the instance records in the
    # reference varfont. Change one of them, to make the check validate the
    # postScriptNameID value of the default instance (which is currently 0xFFFF).
    inst_2 = fvar_table.instances[1]
    inst_2.postscriptNameID = 256
    msg = assert_results_contain(
        check(ttFont), FAIL, "invalid-default-instance-postscript-nameid:0xFFFF"
    )
    assert msg == (
        "'Instance #1' instance has the same coordinates as the default instance;"
        " its postScriptNameID should be 6, instead of 0xFFFF."
    )

    # Change postScriptNameID value of the default instance to a valid value
    dflt_inst.postscriptNameID = 6
    msg = assert_PASS(check(ttFont), "with a good varfont...")
    assert msg == "All default instance nameID values are valid."

    # Change postScriptNameID value of the default instance to an invalid value,
    # and the postScriptNameID value of the second instance record back to 0xFFFF.
    dflt_inst.postscriptNameID = 8
    inst_2.postscriptNameID = 0xFFFF
    msg = assert_results_contain(
        check(ttFont), FAIL, "invalid-default-instance-postscript-nameid:8"
    )
    assert msg == (
        "'Instance #1' instance has the same coordinates as the default instance;"
        " its postScriptNameID should be 6, instead of 8."
    )


def test_check_varfont_same_size_instance_records():
    """All of the instance records in a given font must have the same size,
    with all either including or omitting the postScriptNameID field. If the value
    is 0xFFFF it means that no PostScript name is provided for the instance."""
    check = CheckTester(
        opentype_profile, "com.adobe.fonts/check/varfont/same_size_instance_records"
    )

    # The value of postScriptNameID is 0xFFFF for all the instance records in the
    # reference varfont
    ttFont = TTFont("data/test/cabinvf/Cabin[wdth,wght].ttf")
    msg = assert_PASS(check(ttFont), "with a good varfont...")
    assert msg == "All instance records have the same size."

    fvar_table = ttFont["fvar"]
    inst_1 = fvar_table.instances[0]
    inst_2 = fvar_table.instances[1]
    inst_3 = fvar_table.instances[2]
    inst_4 = fvar_table.instances[3]

    # Change the postScriptNameID of one instance record
    inst_1.postscriptNameID = 256
    msg = assert_results_contain(check(ttFont), FAIL, "different-size-instance-records")
    assert msg == "Instance records don't all have the same size."

    # Change the postScriptNameID of the remaining instance records
    inst_2.postscriptNameID = 356
    inst_3.postscriptNameID = 456
    inst_4.postscriptNameID = 556
    msg = assert_PASS(check(ttFont), "with a good varfont...")
    assert msg == "All instance records have the same size."

    # Change the postScriptNameID of two instance records
    inst_1.postscriptNameID = 0xFFFF
    inst_3.postscriptNameID = 0xFFFF
    msg = assert_results_contain(check(ttFont), FAIL, "different-size-instance-records")
    assert msg == "Instance records don't all have the same size."


def test_check_varfont_distinct_instance_records():
    """All of the instance records in a font should have distinct coordinates
    and distinct subfamilyNameID and postScriptName ID values."""
    check = CheckTester(
        opentype_profile, "com.adobe.fonts/check/varfont/distinct_instance_records"
    )

    # All of the instance records in the reference varfont are unique
    ttFont = TTFont("data/test/cabinvf/Cabin[wdth,wght].ttf")
    msg = assert_PASS(check(ttFont), "with a good varfont...")
    assert msg == "All instance records are distinct."

    fvar_table = ttFont["fvar"]
    inst_1 = fvar_table.instances[0]
    inst_2 = fvar_table.instances[1]
    inst_3 = fvar_table.instances[2]
    inst_4 = fvar_table.instances[3]

    # Make instance 2 the same as instance 1
    inst_2.subfamilyNameID = inst_1.subfamilyNameID
    inst_2.coordinates["wght"] = inst_1.coordinates["wght"]
    msg = assert_results_contain(
        check(ttFont), WARN, "repeated-instance-record:Regular")
    assert msg == "'Regular' is a repeated instance record."

    # Make instance 4 the same as instance 3
    inst_4.subfamilyNameID = inst_3.subfamilyNameID
    inst_4.coordinates["wght"] = inst_3.coordinates["wght"]
    msg = assert_results_contain(
        check(ttFont), WARN, "repeated-instance-record:SemiBold")
    assert msg == "'SemiBold' is a repeated instance record."


# tests/profiles/gdef_test.py

from fontTools.ttLib import TTFont, newTable
from fontTools.ttLib.tables import otTables

from fontbakery.checkrunner import WARN
from fontbakery.codetesting import (assert_PASS,
                                    assert_SKIP,
                                    assert_results_contain,
                                    CheckTester,
                                    TEST_FILE)
from fontbakery.profiles import opentype as opentype_profile


def get_test_font():
    import defcon
    import ufo2ft
    test_ufo = defcon.Font(TEST_FILE("test.ufo"))
    glyph = test_ufo.newGlyph("acute")
    glyph.unicode = 0x00B4
    glyph = test_ufo.newGlyph("acutecomb")
    glyph.unicode = 0x0301
    test_ttf = ufo2ft.compileTTF(test_ufo)

    # Make the CheckTester class happy... :-P
    test_ttf.reader.file.name = "in-memory-data.ttf"
    return test_ttf


def add_gdef_table(font, class_defs):
    font["GDEF"] = gdef = newTable("GDEF")
    class_def_table = otTables.GlyphClassDef()
    class_def_table.classDefs = class_defs
    gdef.table = otTables.GDEF()
    gdef.table.GlyphClassDef = class_def_table


def test_check_gdef_spacing_marks():
    """ Are some spacing glyphs in GDEF mark glyph class? """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/gdef_spacing_marks")

    ttFont = get_test_font()
    assert_SKIP(check(ttFont),
                'if a font lacks a GDEF table...')

    add_gdef_table(ttFont, {})
    assert_PASS(check(ttFont),
                'with an empty GDEF table...')

    # Add a table with 'A' defined as a mark glyph:
    add_gdef_table(ttFont, {'A': 3})
    assert_results_contain(check(ttFont),
                           WARN, 'spacing-mark-glyphs',
                           'if a mark glyph has non-zero width...')


def test_check_gdef_mark_chars():
    """ Are some mark characters not in in GDEF mark glyph class? """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/gdef_mark_chars")

    ttFont = get_test_font()
    assert_SKIP(check(ttFont),
                'if a font lacks a GDEF table...')

    # Add a GDEF table not including `acutecomb` (U+0301) as a mark char:
    add_gdef_table(ttFont, {})
    message = assert_results_contain(check(ttFont),
                                     WARN, 'mark-chars',
                                     'if a mark-char is not listed...')
    assert 'U+0301' in message

    # Include it in the table to see the check PASS:
    add_gdef_table(ttFont, {'acutecomb': 3})
    assert_PASS(check(ttFont),
                'when properly declared...')


def test_check_gdef_non_mark_chars():
    """ Are some non-mark characters in GDEF mark glyph class spacing? """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/gdef_non_mark_chars")

    ttFont = get_test_font()
    assert_SKIP(check(ttFont),
                'if a font lacks a GDEF table...')

    add_gdef_table(ttFont, {})
    assert_PASS(check(ttFont),
                'with an empty GDEF table.')

    add_gdef_table(ttFont, {'acutecomb': 3})
    assert_PASS(check(ttFont),
                'with an GDEF with only properly declared mark chars.')

    add_gdef_table(ttFont, {'acute': 3, 'acutecomb': 3})
    message = assert_results_contain(check(ttFont),
                                     WARN, 'non-mark-chars',
                                     'with an GDEF with a non-mark char (U+00B4, "acute") misdeclared')
    assert 'U+00B4' in message



# tests/profiles/glyf_test.py

import io

from fontTools.ttLib import TTFont

from fontbakery.checkrunner import (WARN, FAIL)
from fontbakery.codetesting import (assert_PASS,
                                    assert_results_contain,
                                    CheckTester,
                                    TEST_FILE)
from fontbakery.profiles import opentype as opentype_profile


def test_check_glyf_unused_data():
    """ Is there any unused data at the end of the glyf table? """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/glyf_unused_data")

    font = TEST_FILE("nunito/Nunito-Regular.ttf")
    ttFont = TTFont(font)
    assert_PASS(check(ttFont))

    # Always start with a fresh copy, as fT works lazily. Accessing certain data
    # can prevent the test from working because we rely on uninitialized
    # behavior.
    ttFont = TTFont(font)
    ttFont["loca"].locations.pop()
    _file = io.BytesIO()
    ttFont.save(_file)
    ttFont = TTFont(_file)
    ttFont.reader.file.name = font
    assert_results_contain(check(ttFont),
                           FAIL, 'unreachable-data')

    ttFont = TTFont(font)
    ttFont["loca"].locations.append(50000)
    _file = io.BytesIO()
    ttFont.save(_file)
    ttFont = TTFont(_file)
    ttFont.reader.file.name = font
    assert_results_contain(check(ttFont),
                           FAIL, 'missing-data')


def test_check_points_out_of_bounds():
    """ Check for points out of bounds. """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/points_out_of_bounds")

    ttFont = TTFont(TEST_FILE("nunito/Nunito-Regular.ttf"))
    assert_results_contain(check(ttFont),
                           WARN, 'points-out-of-bounds')

    ttFont = TTFont(TEST_FILE("familysans/FamilySans-Regular.ttf"))
    assert_PASS(check(ttFont))


def test_check_glyf_non_transformed_duplicate_components():
    """Check glyphs do not have duplicate components which have the same x,y coordinates."""
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/glyf_non_transformed_duplicate_components")

    ttFont = TTFont(TEST_FILE("nunito/Nunito-Regular.ttf"))
    assert_PASS(check(ttFont))

    # Set qutodbl's components to have the same x,y values
    ttFont['glyf']['quotedbl'].components[0].x = 0
    ttFont['glyf']['quotedbl'].components[1].x = 0
    ttFont['glyf']['quotedbl'].components[0].y = 0
    ttFont['glyf']['quotedbl'].components[1].y = 0
    assert_results_contain(check(ttFont),
                           FAIL, 'found-duplicates')


# tests/profiles/googlefonts_test.py

import pytest
import os
from fontTools.ttLib import TTFont

from fontbakery.checkrunner import (DEBUG, INFO, WARN, ERROR,
                                    SKIP, PASS, FAIL, ENDCHECK)
from fontbakery.codetesting import (assert_results_contain,
                                    assert_PASS,
                                    assert_SKIP,
                                    portable_path,
                                    TEST_FILE,
                                    GLYPHSAPP_TEST_FILE,
                                    CheckTester)
from fontbakery.configuration import Configuration
from fontbakery.constants import (NameID,
                                  PlatformID,
                                  WindowsEncodingID,
                                  WindowsLanguageID,
                                  MacintoshEncodingID,
                                  MacintoshLanguageID,
                                  OFL_BODY_TEXT)
from fontbakery.profiles import googlefonts as googlefonts_profile
import math

check_statuses = (ERROR, FAIL, SKIP, PASS, WARN, INFO, DEBUG)

mada_fonts = [
    TEST_FILE("mada/Mada-Black.ttf"),
    TEST_FILE("mada/Mada-ExtraLight.ttf"),
    TEST_FILE("mada/Mada-Medium.ttf"),
    TEST_FILE("mada/Mada-SemiBold.ttf"),
    TEST_FILE("mada/Mada-Bold.ttf"),
    TEST_FILE("mada/Mada-Light.ttf"),
    TEST_FILE("mada/Mada-Regular.ttf"),
]

@pytest.fixture
def mada_ttFonts():
    return [TTFont(path) for path in mada_fonts]

cabin_fonts = [
    TEST_FILE("cabin/Cabin-BoldItalic.ttf"),
    TEST_FILE("cabin/Cabin-Bold.ttf"),
    TEST_FILE("cabin/Cabin-Italic.ttf"),
    TEST_FILE("cabin/Cabin-MediumItalic.ttf"),
    TEST_FILE("cabin/Cabin-Medium.ttf"),
    TEST_FILE("cabin/Cabin-Regular.ttf"),
    TEST_FILE("cabin/Cabin-SemiBoldItalic.ttf"),
    TEST_FILE("cabin/Cabin-SemiBold.ttf")
]

cabin_condensed_fonts = [
    TEST_FILE("cabincondensed/CabinCondensed-Regular.ttf"),
    TEST_FILE("cabincondensed/CabinCondensed-Medium.ttf"),
    TEST_FILE("cabincondensed/CabinCondensed-Bold.ttf"),
    TEST_FILE("cabincondensed/CabinCondensed-SemiBold.ttf")
]

montserrat_fonts = [
    TEST_FILE("montserrat/Montserrat-Black.ttf"),
    TEST_FILE("montserrat/Montserrat-BlackItalic.ttf"),
    TEST_FILE("montserrat/Montserrat-Bold.ttf"),
    TEST_FILE("montserrat/Montserrat-BoldItalic.ttf"),
    TEST_FILE("montserrat/Montserrat-ExtraBold.ttf"),
    TEST_FILE("montserrat/Montserrat-ExtraBoldItalic.ttf"),
    TEST_FILE("montserrat/Montserrat-ExtraLight.ttf"),
    TEST_FILE("montserrat/Montserrat-ExtraLightItalic.ttf"),
    TEST_FILE("montserrat/Montserrat-Italic.ttf"),
    TEST_FILE("montserrat/Montserrat-Light.ttf"),
    TEST_FILE("montserrat/Montserrat-LightItalic.ttf"),
    TEST_FILE("montserrat/Montserrat-Medium.ttf"),
    TEST_FILE("montserrat/Montserrat-MediumItalic.ttf"),
    TEST_FILE("montserrat/Montserrat-Regular.ttf"),
    TEST_FILE("montserrat/Montserrat-SemiBold.ttf"),
    TEST_FILE("montserrat/Montserrat-SemiBoldItalic.ttf"),
    TEST_FILE("montserrat/Montserrat-Thin.ttf"),
    TEST_FILE("montserrat/Montserrat-ThinItalic.ttf")
]

cjk_font = TEST_FILE("cjk/SourceHanSans-Regular.otf")


@pytest.fixture
def montserrat_ttFonts():
    return [TTFont(path) for path in montserrat_fonts]

@pytest.fixture
def cabin_ttFonts():
    return [TTFont(path) for path in cabin_fonts]

@pytest.fixture
def vf_ttFont():
    path = TEST_FILE("varfont/Oswald-VF.ttf")
    return TTFont(path)


def change_name_table_id(ttFont, nameID, newEntryString, platEncID=0):
    for i, nameRecord in enumerate(ttFont['name'].names):
        if nameRecord.nameID == nameID and nameRecord.platEncID == platEncID:
            ttFont['name'].names[i].string = newEntryString

def delete_name_table_id(ttFont, nameID):
    delete = []
    for i, nameRecord in enumerate(ttFont['name'].names):
        if nameRecord.nameID == nameID:
            delete.append(i)
    for i in sorted(delete, reverse=True):
        del(ttFont['name'].names[i])

@pytest.fixture
def cabin_regular_path():
    return portable_path('data/test/cabin/Cabin-Regular.ttf')


def test_example_checkrunner_based(cabin_regular_path):
    """ This is just an example test. We'll probably need something like
        this setup in a checkrunner_test.py testsuite.
        Leave it here for the moment until we implemented a real case.

        This test is run via the checkRunner and demonstrate how to get
        (mutable) objects from the conditions cache and change them.

        NOTE: the actual fontbakery checks of conditions should never
        change a condition object.
    """
    from fontbakery.checkrunner import CheckRunner
    from fontbakery.profiles.googlefonts import profile
    values = dict(fonts=[cabin_regular_path])
    runner = CheckRunner(profile, values, Configuration(explicit_checks=['com.google.fonts/check/vendor_id']))

    # we could also reuse the `iterargs` that was assigned in the previous
    # for loop, but this here is more explicit
    iterargs = (('font', 0),)
    ttFont = runner.get('ttFont', iterargs)

    print('Test PASS ...')
    # prepare
    ttFont['OS/2'].achVendID = "APPL"
    # run
    for status, message, _ in runner.run():
        if status in check_statuses:
            last_check_message = message
        if status == ENDCHECK:
            assert message == PASS
            break

    print('Test WARN ...')
    # prepare
    ttFont['OS/2'].achVendID = "????"
    # run
    for status, message, _ in runner.run():
        if status in check_statuses:
            last_check_message = message
        if status == ENDCHECK:
            assert message == WARN and last_check_message.code == 'unknown'
            break


def test_check_canonical_filename():
    """ Files are named canonically. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/canonical_filename")

    static_canonical_names = [
        TEST_FILE("montserrat/Montserrat-Thin.ttf"),
        TEST_FILE("montserrat/Montserrat-ExtraLight.ttf"),
        TEST_FILE("montserrat/Montserrat-Light.ttf"),
        TEST_FILE("montserrat/Montserrat-Regular.ttf"),
        TEST_FILE("montserrat/Montserrat-Medium.ttf"),
        TEST_FILE("montserrat/Montserrat-SemiBold.ttf"),
        TEST_FILE("montserrat/Montserrat-Bold.ttf"),
        TEST_FILE("montserrat/Montserrat-ExtraBold.ttf"),
        TEST_FILE("montserrat/Montserrat-Black.ttf"),
        TEST_FILE("montserrat/Montserrat-ThinItalic.ttf"),
        TEST_FILE("montserrat/Montserrat-ExtraLightItalic.ttf"),
        TEST_FILE("montserrat/Montserrat-LightItalic.ttf"),
        TEST_FILE("montserrat/Montserrat-Italic.ttf"),
        TEST_FILE("montserrat/Montserrat-MediumItalic.ttf"),
        TEST_FILE("montserrat/Montserrat-SemiBoldItalic.ttf"),
        TEST_FILE("montserrat/Montserrat-BoldItalic.ttf"),
        TEST_FILE("montserrat/Montserrat-ExtraBoldItalic.ttf"),
        TEST_FILE("montserrat/Montserrat-BlackItalic.ttf"),
    ]

    varfont_canonical_names = [
        TEST_FILE("cabinvfbeta/CabinVFBeta-Italic[wght].ttf"),
        TEST_FILE("cabinvfbeta/CabinVFBeta[wdth,wght].ttf"), # axis tags are sorted
    ]

    non_canonical_names = [
        TEST_FILE("cabinvfbeta/CabinVFBeta.ttf"),
        TEST_FILE("cabinvfbeta/Cabin-Italic.ttf"),
        TEST_FILE("cabinvfbeta/Cabin-Roman.ttf"),
        TEST_FILE("cabinvfbeta/Cabin-Italic-VF.ttf"),
        TEST_FILE("cabinvfbeta/Cabin-Roman-VF.ttf"),
        TEST_FILE("cabinvfbeta/Cabin-VF.ttf"),
        TEST_FILE("cabinvfbeta/CabinVFBeta[wght,wdth].ttf"), # axis tags are NOT sorted here
    ]

    for canonical in static_canonical_names + varfont_canonical_names:
        assert_PASS(check(canonical),
                    f'with "{canonical}" ...')

    for non_canonical in non_canonical_names:
        assert_results_contain(check(non_canonical),
                               FAIL, 'bad-varfont-filename',
                               f'with "{non_canonical}" ...')

    assert_results_contain(check(TEST_FILE("Bad_Name.ttf")),
                           FAIL, 'invalid-char',
                           'with filename containing an underscore...')

    assert_results_contain(check(TEST_FILE("mutatorsans-vf/MutatorSans-VF.ttf")),
                           FAIL, 'unknown-name',
                           'with a variable font that lacks some important name table entries...')

    # TODO: FAIL, 'bad-static-filename'
    # TODO: FAIL, 'varfont-with-static-filename'


def test_check_description_broken_links():
    """ Does DESCRIPTION file contain broken links ? """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/description/broken_links")

    font = TEST_FILE("cabin/Cabin-Regular.ttf")
    assert_PASS(check(font),
                'with description file that has no links...')

    good_desc = check['description']
    good_desc += ("<a href='http://example.com'>Good Link</a>"
                  "<a href='http://fonts.google.com'>Another Good One</a>")
    assert_PASS(check(font, {"description": good_desc}),
                'with description file that has good links...')

    good_desc += "<a href='mailto:juca@members.fsf.org'>An example mailto link</a>"
    assert_results_contain(check(font, {"description": good_desc}),
                           INFO, "email",
                           'with a description file containing "mailto" links...')

    assert_PASS(check(font, {"description": good_desc}),
                      'with a description file containing "mailto" links...')

    bad_desc = good_desc + "<a href='http://thisisanexampleofabrokenurl.com/'>This is a Bad Link</a>"
    assert_results_contain(check(font, {"description": bad_desc}),
                           FAIL, 'broken-links',
                           'with a description file containing a known-bad URL...')

    #TODO: WARN, 'timeout'


def test_check_description_git_url():
    """ Does DESCRIPTION file contain an upstream Git repo URL? """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/description/git_url")

    # TODO: test INFO 'url-found'

    font = TEST_FILE("cabin/Cabin-Regular.ttf")
    assert_results_contain(check(font),
                           FAIL, 'lacks-git-url',
                           'with description file that has no git repo URLs...')

    good_desc = ("<a href='https://github.com/uswds/public-sans'>Good URL</a>"
                 "<a href='https://gitlab.com/smc/fonts/uroob'>Another Good One</a>")
    assert_PASS(check(font, {"description": good_desc}),
                'with description file that has good links...')

    bad_desc = "<a href='https://v2.designsystem.digital.gov'>Bad URL</a>"
    assert_results_contain(check(font, {"description": bad_desc}),
                           FAIL, 'lacks-git-url',
                           'with description file that has false git in URL...')


def test_check_description_valid_html():
    """ DESCRIPTION file is a propper HTML snippet ? """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/description/valid_html")

    font = TEST_FILE("nunito/Nunito-Regular.ttf")
    assert_PASS(check(font),
                'with description file that contains a good HTML snippet...')

    bad_desc = open(TEST_FILE("cabin/FONTLOG.txt"), "r").read() # :-)
    assert_results_contain(check(font, {"description": bad_desc}),
                           FAIL, 'lacks-paragraph',
                           'with a known-bad file (without HTML paragraph tags)...')

    bad_desc = "<html>foo</html>"
    assert_results_contain(check(font, {"description": bad_desc}),
                           FAIL, 'html-tag',
                           'with description file that contains the <html> tag...')

    bad_desc = ("<p>This example has the & caracter,"
                " but does not escape it with an HTML entity code."
                " It should use &amp; instead."
                "</p>")
    assert_results_contain(check(font, {"description": bad_desc}),
                           FAIL, 'malformed-snippet',
                           'with a known-bad file (not using HTML entity syntax)...')


def test_check_description_min_length():
    """ DESCRIPTION.en_us.html must have more than 200 bytes. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/description/min_length")

    font = TEST_FILE("nunito/Nunito-Regular.ttf")

    bad_length = 'a' * 199
    assert_results_contain(check(font, {"description": bad_length}),
                           FAIL, 'too-short',
                           'with 199-byte buffer...')

    bad_length = 'a' * 200
    assert_results_contain(check(font, {"description": bad_length}),
                           FAIL, 'too-short',
                           'with 200-byte buffer...')

    good_length = 'a' * 201
    assert_PASS(check(font, {"description": good_length}),
                'with 201-byte buffer...')


def test_check_description_max_length():
    """ DESCRIPTION.en_us.html must have less than 2000 bytes. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/description/max_length")

    font = TEST_FILE("nunito/Nunito-Regular.ttf")

    bad_length = 'a' * 2001
    assert_results_contain(check(font, {"description": bad_length}),
                           FAIL, "too-long",
                           'with 2001-byte buffer...')

    bad_length = 'a' * 2000
    assert_results_contain(check(font, {"description": bad_length}),
                           FAIL, "too-long",
                           'with 2000-byte buffer...')

    good_length = 'a' * 1999
    assert_PASS(check(font, {"description": good_length}),
                'with 1999-byte buffer...')


def test_check_description_eof_linebreak():
    """ DESCRIPTION.en_us.html should end in a linebreak. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/description/eof_linebreak")

    font = TEST_FILE("nunito/Nunito-Regular.ttf")

    bad = ("We want to avoid description files\n"
           "without an end-of-file linebreak\n"
           "like this one.")
    assert_results_contain(check(font, {"description": bad}),
                           WARN, "missing-eof-linebreak",
                           'when we lack an end-of-file linebreak...')

    good = ("On the other hand, this one\n"
            "is good enough.\n")
    assert_PASS(check(font, {"description": good}),
                'when we add one...')


def test_check_name_family_and_style_max_length():
    """ Combined length of family and style must not exceed 27 characters. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/name/family_and_style_max_length")

    # Our reference Cabin Regular is known to be good 
    ttFont = TTFont(TEST_FILE("cabin/Cabin-Regular.ttf"))

    # So it must PASS the check:
    assert_PASS(check(ttFont),
                'with a good font...')

    # Then we emit a WARNing with long family/style names
    # Originaly these were based on the example on the glyphs tutorial
    # (at https://glyphsapp.com/tutorials/multiple-masters-part-3-setting-up-instances)
    # but later we increased a bit the max allowed length.

    # First we expect a WARN with a bad FAMILY NAME
    for index, name in enumerate(ttFont["name"].names):
        if name.nameID == NameID.FONT_FAMILY_NAME:
            # This has 28 chars, while the max currently allowed is 27.
            bad = "AnAbsurdlyLongFamilyNameFont"
            assert len(bad) == 28
            ttFont["name"].names[index].string = bad.encode(name.getEncoding())
            break
    assert_results_contain(check(ttFont),
                           WARN, 'too-long',
                           'with a bad font...')

    # Now let's restore the good Cabin Regular...
    ttFont = TTFont(TEST_FILE("cabin/Cabin-Regular.ttf"))

    # ...and break the check again with a bad SUBFAMILY NAME:
    for index, name in enumerate(ttFont["name"].names):
        if name.nameID == NameID.FONT_SUBFAMILY_NAME:
            bad = "WithAVeryLongAndBadStyleName"
            assert len(bad) == 28
            ttFont["name"].names[index].string = bad.encode(name.getEncoding())
            break
    assert_results_contain(check(ttFont),
                           WARN, 'too-long',
                           'with a bad font...')


def DISABLED_test_check_glyphs_file_name_family_and_style_max_length():
    """ Combined length of family and style must not exceed 27 characters. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/glyphs_file/name/family_and_style_max_length")

    # Our reference Comfortaa.glyphs is known to be good
    glyphsFile = GLYPHSAPP_TEST_FILE("Comfortaa.glyphs")

    # So it must PASS the check:
    assert_PASS(check(glyphsFile),
                'with a good font...')

    # Then we emit a WARNing with long family/style names
    # Originaly these were based on the example on the glyphs tutorial
    # (at https://glyphsapp.com/tutorials/multiple-masters-part-3-setting-up-instances)
    # but later we increased a bit the max allowed length.

    # First we expect a WARN with a bad FAMILY NAME
    # This has 28 chars, while the max currently allowed is 27.
    bad = "AnAbsurdlyLongFamilyNameFont"
    assert len(bad) == 28
    glyphsFile.familyName = bad
    assert_results_contain(check(glyphsFile),
                           WARN, 'too-long',
                           'with a too long font familyname...')

    for i in range(len(glyphsFile.instances)):
        # Restore the good glyphs file...
        glyphsFile = GLYPHSAPP_TEST_FILE("Comfortaa.glyphs")

        # ...and break the check again with a long SUBFAMILY NAME
        # on one of its instances:
        bad_stylename = "WithAVeryLongAndBadStyleName"
        assert len(bad_stylename) == 28
        glyphsFile.instances[i].fullName = f"{glyphsFile.familyName} {bad_stylename}"
        assert_results_contain(check(glyphsFile),
                               WARN, 'too-long',
                               'with a too long stylename...')


def test_check_name_line_breaks():
    """ Name table entries should not contain line-breaks. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/name/line_breaks")

    # Our reference Mada Regular font is good here:
    ttFont = TTFont(TEST_FILE("mada/Mada-Regular.ttf"))

    # So it must PASS the check:
    assert_PASS(check(ttFont),
                'with a good font...')

    num_entries = len(ttFont["name"].names)
    for i in range(num_entries):
        ttFont = TTFont(TEST_FILE("mada/Mada-Regular.ttf"))
        encoding = ttFont["name"].names[i].getEncoding()
        ttFont["name"].names[i].string = "bad\nstring".encode(encoding)
        assert_results_contain(check(ttFont),
                               FAIL, 'line-break',
                               f'with name entries containing a linebreak ({i}/{num_entries})...')


def test_check_name_rfn():
    """ Name table strings must not contain 'Reserved Font Name'. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/name/rfn")

    ttFont = TTFont(TEST_FILE("nunito/Nunito-Regular.ttf"))
    assert_PASS(check(ttFont))

    # The OFL text contains the term 'Reserved Font Name',
    # which should to cause a FAIL:
    ttFont["name"].setName(OFL_BODY_TEXT,
                           NameID.LICENSE_DESCRIPTION,
                           PlatformID.WINDOWS, WindowsEncodingID.UNICODE_BMP,
                           WindowsLanguageID.ENGLISH_USA)
    assert_PASS(check(ttFont),
                "with the OFL full text...")

    ttFont["name"].setName("Bla Reserved Font Name",
                           NameID.VERSION_STRING,
                           PlatformID.WINDOWS, WindowsEncodingID.UNICODE_BMP,
                           WindowsLanguageID.ENGLISH_USA)
    assert_results_contain(check(ttFont),
                           FAIL, 'rfn',
                           'with "Reserved Font Name" on a name table entry...')


def test_check_metadata_parses():
    """ Check METADATA.pb parse correctly. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/parses")

    good = TEST_FILE("merriweather/Merriweather-Regular.ttf")
    assert_PASS(check(good),
                'with a good METADATA.pb file...')

    skip = TEST_FILE("slabo/Slabo-Regular.ttf")
    assert_results_contain(check(skip),
                           SKIP, 'file-not-found',
                           'with a missing METADATA.pb file...')

    bad = TEST_FILE("broken_metadata/foo.ttf")
    assert_results_contain(check(bad),
                           FAIL, 'parsing-error',
                           'with a bad METADATA.pb file...')


def test_check_metadata_unknown_designer():
    """ Font designer field in METADATA.pb must not be 'unknown'. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/unknown_designer")

    font = TEST_FILE("merriweather/Merriweather.ttf")
    assert_PASS(check(font),
                'with a good METADATA.pb file...')

    md = check["family_metadata"]
    md.designer = "unknown"
    assert_results_contain(check(font, {"family_metadata": md}),
                           FAIL, 'unknown-designer',
                           'with a bad METADATA.pb file...')


def test_check_metadata_designer_values():
    """ Multiple values in font designer field in
        METADATA.pb must be separated by commas. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/designer_values")

    font = TEST_FILE("merriweather/Merriweather.ttf")
    assert_PASS(check(font),
                'with a good METADATA.pb file...')

    md = check["family_metadata"]
    md.designer = "Pentagram, MCKL"
    assert_PASS(check(font, {"family_metadata": md}),
                'with a good multiple-designers string...')

    md.designer = "Pentagram / MCKL" # This actually happened on an
                                     # early version of the Red Hat Text family
    assert_results_contain(check(font, {"family_metadata": md}),
                           FAIL, 'slash',
                           'with a bad multiple-designers string'
                           ' (names separated by a slash char)...')


def test_check_metadata_broken_links():
    """ Does DESCRIPTION file contain broken links? """
    #check = CheckTester(googlefonts_profile,
    #                    "com.google.fonts/check/metadata/broken_links")
    # TODO: Implement-me!
    # INFO, "email"
    # WARN, "timeout"
    # FAIL, "broken-links"


def test_check_metadata_undeclared_fonts():
    """ Ensure METADATA.pb lists all font binaries. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/undeclared_fonts")

    # Our reference Nunito family is know to be good here.
    font = TEST_FILE("nunito/Nunito-Regular.ttf")
    assert_PASS(check(font))

    # Our reference Cabin family has files that are not declared in its METADATA.pb:
    # - CabinCondensed-Medium.ttf
    # - CabinCondensed-SemiBold.ttf
    # - CabinCondensed-Regular.ttf
    # - CabinCondensed-Bold.ttf
    font = TEST_FILE("cabin/Cabin-Regular.ttf")
    assert_results_contain(check(font),
                           FAIL, 'file-not-declared')

    # We placed an additional file on a subdirectory of our reference
    # OverpassMono family with the name "another_directory/ThisShouldNotBeHere.otf"
    font = TEST_FILE("overpassmono/OverpassMono-Regular.ttf")
    assert_results_contain(check(font),
                           WARN, 'font-on-subdir')

    # We do accept statics folder though!
    # Jura is an example:
    font = TEST_FILE("varfont/jura/Jura.ttf")
    assert_PASS(check(font))


@pytest.mark.skip(reason="re-enable after addressing issue #1998")
def test_check_family_equal_numbers_of_glyphs(mada_ttFonts, cabin_ttFonts):
    """ Fonts have equal numbers of glyphs? """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/family/equal_numbers_of_glyphs")

    # our reference Cabin family is know to be good here.
    assert_PASS(check(cabin_ttFonts),
                'with a good family.')

    # our reference Mada family is bad here with 407 glyphs on most font files
    # except the Black and the Medium, that both have 408 glyphs.
    assert_results_contain(check(mada_ttFonts),
                           FAIL, 'glyph-count-diverges',
                           'with fonts that diverge on number of glyphs.')


@pytest.mark.skip(reason="re-enable after addressing issue #1998")
def test_check_family_equal_glyph_names(mada_ttFonts, cabin_ttFonts):
    """ Fonts have equal glyph names? """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/family/equal_glyph_names")

    # our reference Cabin family is know to be good here.
    assert_PASS(check(cabin_ttFonts),
                'with a good family.')

    # our reference Mada family is bad here with 407 glyphs on most font files
    # except the Black and the Medium, that both have 408 glyphs (that extra glyph
    # causes the check to fail).
    assert_results_contain(check(mada_ttFonts),
                           FAIL, 'missing-glyph',
                           'with fonts that diverge on number of glyphs.')


def test_check_fstype():
    """ Checking OS/2 fsType """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/fstype")

    # our reference Cabin family is know to be good here.
    ttFont = TTFont(TEST_FILE("cabin/Cabin-Regular.ttf"))
    assert_PASS(check(ttFont),
                'with a good font without DRM.')

    # modify the OS/2 fsType value to something different than zero:
    ttFont['OS/2'].fsType = 1

    assert_results_contain(check(ttFont),
                           FAIL, 'drm',
                           'with fonts that enable DRM restrictions via non-zero fsType bits.')


def test_condition__registered_vendor_ids():
    """ Get a list of vendor IDs from Microsoft's website. """
    from fontbakery.profiles.googlefonts import registered_vendor_ids
    registered_ids = registered_vendor_ids()

    print('As of July 2018, "MLAG": "Michael LaGattuta" must show up in the list...')
    assert "MLAG" in registered_ids # Michael LaGattuta

    print('As of December 2020, "GOOG": "Google" must show up in the list...')
    assert "GOOG" in registered_ids # Google

    print('"CFA ": "Computer Fonts Australia" is a good vendor id, lacking a URL')
    assert "CFA " in registered_ids # Computer Fonts Australia

    print('"GNU ": "Free Software Foundation, Inc." is a good vendor id with 3 letters and a space.')
    assert "GNU " in registered_ids # Free Software Foundation, Inc. / http://www.gnu.org/

    print('"GNU" without the right-padding space must not be on the list!')
    assert "GNU" not in registered_ids # All vendor ids must be 4 chars long!

    print('"ADBE": "Adobe" is a good 4-letter vendor id.')
    assert "ADBE" in registered_ids # Adobe

    print('"B&H ": "Bigelow & Holmes" is a valid vendor id that contains an ampersand.')
    assert "B&H " in registered_ids # Bigelow & Holmes

    print('"MS  ": "Microsoft Corp." is a good vendor id with 2 letters and padded with spaces.')
    assert "MS  " in registered_ids # Microsoft Corp.

    print('"TT\0\0": we also accept vendor-IDs containing NULL-padding.')
    assert "TT\0\0" in registered_ids # constains NULL bytes

    print('All vendor ids must be 4 chars long!')
    assert "GNU" not in registered_ids # 3 chars long is bad
    assert "MS" not in registered_ids # 2 chars long is bad
    assert "H" not in registered_ids # 1 char long is bad

    print('"H   ": "Hurme Design" is a good vendor id with a single letter padded with spaces.')
    assert "H   " in registered_ids # Hurme Design

    print('"   H": But not padded on the left, please!')
    assert "   H" not in registered_ids # a bad vendor id (presumably for "Hurme Design"
                                        # but with a vendor id parsing bug)

    print('"????" is an unknown vendor id.')
    assert "????" not in registered_ids


def test_check_vendor_id():
    """ Checking OS/2 achVendID """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/vendor_id")

    # Let's start with our reference Merriweather Regular
    ttFont = TTFont(TEST_FILE("merriweather/Merriweather-Regular.ttf"))

    bad_vids = ['UKWN', 'ukwn', 'PfEd']
    for bad_vid in bad_vids:
        ttFont['OS/2'].achVendID = bad_vid
        assert_results_contain(check(ttFont),
                               WARN, 'bad',
                               f'with bad vid "{bad_vid}".')

    ttFont['OS/2'].achVendID = None
    assert_results_contain(check(ttFont),
                           WARN, 'not-set',
                           'with font missing vendor id info.')

    ttFont['OS/2'].achVendID = "????"
    assert_results_contain(check(ttFont),
                           WARN, 'unknown',
                           'with unknwon vendor id.')

    # we now change the fields into a known good vendor id:
    ttFont['OS/2'].achVendID = "APPL"
    assert_PASS(check(ttFont),
                'with a good font.')

    # And let's also make sure it works here:
    ttFont['OS/2'].achVendID = "GOOG"
    assert_PASS(check(ttFont),
                'with a good font.')


def NOT_IMPLEMENTED__test_check_glyph_coverage():
    """ Check glyph coverage. """
    #check = CheckTester(googlefonts_profile,
    #                    "com.google.fonts/check/glyph_coverage")
    #TODO: Implement-me!

    ## Our reference Mada Regular is know to be bad here.
    #ttFont = TTFont(TEST_FILE("mada/Mada-Regular.ttf"))
    #assert_results_contain(check(ttFont),
    #                       FAIL, 'missing-codepoints',
    #                       'with a bad font...')

    ## Our reference Cabin Regular is know to be good here.
    #ttFont = TTFont(TEST_FILE("cabin/Cabin-Regular.ttf"))
    #assert_PASS(check(ttFont),
    #            'with a good font...')


def test_check_name_unwanted_chars():
    """ Substitute copyright, registered and trademark
        symbols in name table entries. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/name/unwanted_chars")

    # Our reference Mada Regular is know to be bad here.
    font = TEST_FILE("mada/Mada-Regular.ttf")
    assert_results_contain(check(font),
                           FAIL, 'unwanted-chars',
                           'with a bad font...')

    # Our reference Cabin Regular is know to be good here.
    font = TEST_FILE("cabin/Cabin-Regular.ttf")
    assert_PASS(check(font),
                'with a good font...')


def test_check_usweightclass():
    """ Checking OS/2 usWeightClass. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/usweightclass")

    # Our reference Mada Regular is know to be bad here.
    font = TEST_FILE("mada/Mada-Regular.ttf")
    ttFont = TTFont(font)
    assert_results_contain(check(ttFont),
                           FAIL, 'bad-value',
                           f'with bad font "{font}" ...')

    # All fonts in our reference Cabin family are know to be good here.
    for font in cabin_fonts:
        ttFont = TTFont(font)
        assert_PASS(check(ttFont),
                    f'with good font "{font}"...')

    # Check otf Thin == 250 and ExtraLight == 275
    font = TEST_FILE("rokkitt/Rokkitt-Thin.otf")
    ttFont = TTFont(font)
    assert_results_contain(check(ttFont),
                           FAIL, "bad-value",
                           f'with bad font "{font}"...')

    ttFont['OS/2'].usWeightClass = 250
    assert_PASS(check(ttFont),
                f'with good font "{font}" (usWeightClass = 250) ...')

    font = TEST_FILE("rokkitt/Rokkitt-ExtraLight.otf")
    ttFont = TTFont(font)
    assert_results_contain(check(ttFont),
                           FAIL, "bad-value",
                           f'with bad font "{font}" ...')

    ttFont['OS/2'].usWeightClass = 275
    assert_PASS(check(ttFont),
                f'with good font "{font}" (usWeightClass = 275) ...')

    # TODO: test italic variants to ensure we do not get regressions of
    #       this bug: https://github.com/googlefonts/fontbakery/issues/2650


def test_family_directory_condition():
    from fontbakery.profiles.shared_conditions import family_directory
    assert family_directory("some_directory/Foo.ttf") == "some_directory"
    assert family_directory("some_directory/subdir/Foo.ttf") == "some_directory/subdir"
    assert family_directory("Foo.ttf") == "." # This is meant to ensure license files
                                              # are correctly detected on the current
                                              # working directory.

def test_check_family_has_license():
    """ Check font project has a license. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/family/has_license")

    from fontbakery.profiles.googlefonts import licenses

    # The lines maked with 'hack' below are meant to
    # not let fontbakery's own license to mess up
    # this code test.
    detected_licenses = licenses(portable_path("data/test/028/multiple"))
    detected_licenses.pop(-1) # hack
    assert_results_contain(check([], {"licenses": detected_licenses}),
                           FAIL, 'multiple',
                           'with multiple licenses...')

    detected_licenses = licenses(portable_path("data/test/028/none"))
    detected_licenses.pop(-1) # hack
    assert_results_contain(check([], {"licenses": detected_licenses}),
                           FAIL, 'no-license',
                           'with no license...')

    detected_licenses = licenses(portable_path("data/test/028/pass_ofl"))
    detected_licenses.pop(-1) # hack
    assert_PASS(check([], {"licenses": detected_licenses}),
                'with a single OFL license...')

    detected_licenses = licenses(portable_path("data/test/028/pass_apache"))
    detected_licenses.pop(-1) # hack
    assert_PASS(check([], {"licenses": detected_licenses}),
                'with a single Apache license...')


def test_check_license_ofl_copyright():
    """Check license file has good copyright string."""
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/license/OFL_copyright")


    # And Mada has a bad copyright string format:
    font = TEST_FILE("mada/Mada-Regular.ttf")
    ttFont = TTFont(font)
    assert_results_contain(check(ttFont),
                           FAIL, "bad-format",
                           "with bad string formatting.")

    # so we fix it:
    SOME_GOOD_TEXT = "Copyright 2019 The Montserrat Project Authors (https://github.com/julietaula/montserrat)"
    assert_PASS(check(ttFont, {"license_contents": SOME_GOOD_TEXT}),
                'with good license contents.')


def test_check_license_ofl_body_text():
    """Check OFL.txt contains correct body text."""
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/license/OFL_body_text")

    # Our reference Montserrat family is know to have
    # a proper OFL.txt license file.
    # NOTE: This is currently considered good
    #       even though it uses an "http://" URL
    font = TEST_FILE("montserrat/Montserrat-Regular.ttf")
    ttFont = TTFont(font)

    assert_PASS(check(ttFont),
                'with a good OFL.txt license with "http://" url.')


    # using "https://" is also considered good:
    good_license = check["license_contents"].replace("http://", "https://")
    assert_PASS(check(ttFont, {'license_contents': good_license}),
                'with a good OFL.txt license with "https://" url.')


    # modify a tiny bit of the license text, to trigger the FAIL:
    bad_license = check["license_contents"].replace("SIL OPEN FONT LICENSE Version 1.1",
                                                    "SOMETHING ELSE :-P Version Foo")
    assert_results_contain(check(ttFont, {'license_contents': bad_license}),
                           FAIL, "incorrect-ofl-body-text",
                           "with incorrect ofl body text")


def test_check_name_license(mada_ttFonts):
    """ Check copyright namerecords match license file. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/name/license")

    # Our reference Mada family has its copyright name records properly set
    # identifying it as being licensed under the Open Font License
    for ttFont in mada_ttFonts:
        assert_PASS(check(ttFont),
                    'with good fonts ...')

    for ttFont in mada_ttFonts:
        assert_results_contain(check(ttFont, {"license": "LICENSE.txt"}), # Apache
                               FAIL, 'wrong',
                               'with wrong entry values ...')

    for ttFont in mada_ttFonts:
        delete_name_table_id(ttFont, NameID.LICENSE_DESCRIPTION)
        assert_results_contain(check(ttFont),
                               FAIL, 'missing',
                               'with missing copyright namerecords ...')

    # TODO:
    # WARN, "http" / "http-in-description"


def NOT_IMPLEMENTED_test_check_name_license_url():
    """ License URL matches License text on name table? """
    # check = CheckTester(googlefonts_profile,
    #                     "com.google.fonts/check/name/license_url")
    # TODO: Implement-me!
    #
    # code-paths:
    # - FAIL, code="ufl"
    # - FAIL, code="licensing-inconsistency"
    # - FAIL, code="no-license-found"
    # - FAIL, code="bad-entries"
    # - WARN, code="http-in-description"
    # - WARN, code="http"
    # - PASS


def test_check_name_description_max_length():
    """ Description strings in the name table
        must not exceed 200 characters.
    """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/name/description_max_length")

    # Our reference Mada Regular is know to be good here.
    ttFont = TTFont(TEST_FILE("mada/Mada-Regular.ttf"))
    assert_PASS(check(ttFont),
                'with a good font...')

    # Here we add strings to NameID.DESCRIPTION with exactly 100 chars,
    # so it should still PASS:
    for i, name in enumerate(ttFont['name'].names):
        if name.nameID == NameID.DESCRIPTION:
            ttFont['name'].names[i].string = ('a' * 200).encode(name.getEncoding())
    assert_PASS(check(ttFont),
                'with a 200 char string...')

    # And here we make the strings longer than 200 chars
    # in order to make the check emit a WARN:
    for i, name in enumerate(ttFont['name'].names):
        if name.nameID == NameID.DESCRIPTION:
            ttFont['name'].names[i].string = ('a' * 201).encode(name.getEncoding())
    assert_results_contain(check(ttFont),
                           WARN, 'too-long',
                           'with a too long description string...')


def test_check_hinting_impact():
    """ Show hinting filesize impact. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/hinting_impact")

    font = TEST_FILE("mada/Mada-Regular.ttf")
    assert_results_contain(check(font),
                           INFO, 'size-impact',
                           'this check always emits an INFO result...')
    # TODO: test the CFF code-path


def test_check_file_size():
    """Ensure files are not too large."""
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/file_size")

    assert_PASS(check(TEST_FILE("mada/Mada-Regular.ttf")))

    assert_results_contain(check(TEST_FILE("varfont/inter/Inter[slnt,wght].ttf")),
                           WARN, 'large-font',
                           'with quite a big font...')

    assert_results_contain(check(TEST_FILE("cjk/SourceHanSans-Regular.otf")),
                           FAIL, 'massive-font',
                           'with a very big font...')

def test_check_name_version_format():
    """ Version format is correct in 'name' table ? """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/name/version_format")

    # Our reference Mada Regular font is good here:
    ttFont = TTFont(TEST_FILE("mada/Mada-Regular.ttf"))

    # So it must PASS the check:
    assert_PASS(check(ttFont),
                'with a good font...')

    # then we introduce bad strings in all version-string entries:
    for i, name in enumerate(ttFont["name"].names):
        if name.nameID == NameID.VERSION_STRING:
            invalid = "invalid-version-string".encode(name.getEncoding())
            ttFont["name"].names[i].string = invalid
    assert_results_contain(check(ttFont),
                           FAIL, 'bad-version-strings',
                           'with bad version format in name table...')

    # and finally we remove all version-string entries:
    for i, name in enumerate(ttFont["name"].names):
        if name.nameID == NameID.VERSION_STRING:
            del ttFont["name"].names[i]
    assert_results_contain(check(ttFont),
                           FAIL, 'no-version-string',
                           'with font lacking version string entries in name table...')


def NOT_IMPLEMENTED_test_check_old_ttfautohint():
    """ Font has old ttfautohint applied? """
    # check = CheckTester(googlefonts_profile,
    #                     "com.google.fonts/check/old_ttfautohint")
    # TODO: Implement-me!
    #
    # code-paths:
    # - FAIL, code="lacks-version-strings"
    # - INFO, code="version-not-detected"  "Could not detect which version of ttfautohint
    #                                       was used in this font."
    # - WARN, code="old-ttfa"  "detected an old ttfa version"
    # - PASS
    # - FAIL, code="parse-error"


@pytest.mark.parametrize("expected_status,expected_keyword,reason,font",[
    (FAIL, "lacks-ttfa-params",
     'with a font lacking ttfautohint params on its version strings on the name table.',
     TEST_FILE("coveredbyyourgrace/CoveredByYourGrace.ttf")),

    (SKIP, "not-hinted",
     'with a font which appears to our heuristic as not hinted using ttfautohint.',
     TEST_FILE("mada/Mada-Regular.ttf")),

    (PASS, "ok",
     'with a font that has ttfautohint params'
     ' (-l 6 -r 36 -G 0 -x 10 -H 350 -D latn -f cyrl -w "" -X "")',
     TEST_FILE("merriweather/Merriweather-Regular.ttf"))
])
def test_check_has_ttfautohint_params(expected_status, expected_keyword, reason, font):
    """ Font has ttfautohint params? """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/has_ttfautohint_params")

    assert_results_contain(check(font),
                           expected_status, expected_keyword,
                           reason)


def test_check_epar():
    """ EPAR table present in font? """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/epar")

    # Our reference Mada Regular lacks an EPAR table:
    ttFont = TTFont(TEST_FILE("mada/Mada-Regular.ttf"))

    # So it must emit an INFO message inviting the designers
    # to learn more about it:
    assert_results_contain(check(ttFont),
                           INFO, 'lacks-EPAR',
                           'with a font lacking an EPAR table...')

    # add a fake EPAR table to validate the PASS code-path:
    ttFont["EPAR"] = "foo"
    assert_PASS(check(ttFont),
                'with a good font...')


def NOT_IMPLEMENTED_test_check_gasp():
    """ Is GASP table correctly set? """
    # check = CheckTester(googlefonts_profile,
    #                     "com.google.fonts/check/gasp")
    # TODO: Implement-me!
    #
    # code-paths:
    # - FAIL, "lacks-gasp"        "Font is missing the gasp table."
    # - FAIL, "empty"             "The gasp table has no values."
    # - FAIL, "lacks-ffff-range"  "The gasp table does not have a 0xFFFF gasp range."
    # - INFO, "ranges"            "These are the ppm ranges declared on the gasp table:"
    # - WARN, "non-ffff-range"    "The gasp table has a range that may be unneccessary."
    # - WARN, "unset-flags"       "All flags in gasp range 0xFFFF (i.e. all font sizes) must be set to 1"
    # - PASS                      "The gasp table is correctly set."


def test_check_name_familyname_first_char():
    """ Make sure family name does not begin with a digit. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/name/familyname_first_char")

    # Our reference Mada Regular is known to be good
    ttFont = TTFont(TEST_FILE("mada/Mada-Regular.ttf"))

    # So it must PASS the check:
    assert_PASS(check(ttFont),
                'with a good font...')

    # alter the family-name prepending a digit:
    for i, name in enumerate(ttFont["name"].names):
        if name.nameID == NameID.FONT_FAMILY_NAME:
            ttFont["name"].names[i].string = "1badname".encode(name.getEncoding())

    # and make sure the check FAILs:
    assert_results_contain(check(ttFont),
                           FAIL, 'begins-with-digit',
                           'with a font in which the family name begins with a digit...')


def test_check_name_ascii_only_entries():
    """ Are there non-ASCII characters in ASCII-only NAME table entries? """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/name/ascii_only_entries")

    # Our reference Merriweather Regular is known to be good
    ttFont = TTFont(TEST_FILE("merriweather/Merriweather-Regular.ttf"))

    # So it must PASS the check:
    assert_PASS(check(ttFont),
                'with a good font...')

    #  The OpenType spec requires ASCII for the POSTSCRIPT_NAME (nameID 6).
    #  For COPYRIGHT_NOTICE (nameID 0) ASCII is required because that
    #  string should be the same in CFF fonts which also have this
    #  requirement in the OpenType spec.

    # Let's check detection of both. First nameId 6:
    for i, name in enumerate(ttFont['name'].names):
        if name.nameID == NameID.POSTSCRIPT_NAME:
            ttFont['name'].names[i].string = "Infração".encode(encoding="utf-8")

    assert_results_contain(check(ttFont),
                           FAIL, 'bad-string',
                           'with non-ascii on nameID 6 entry (Postscript name)...')

    assert_results_contain(check(ttFont),
                           FAIL, 'non-ascii-strings',
                           'with non-ascii on nameID 6 entry (Postscript name)...')

    # Then reload the good font
    ttFont = TTFont(TEST_FILE("merriweather/Merriweather-Regular.ttf"))

    # And check detection of a problem on nameId 0:
    for i, name in enumerate(ttFont['name'].names):
        if name.nameID == NameID.COPYRIGHT_NOTICE:
            ttFont['name'].names[i].string = "Infração".encode(encoding="utf-8")

    assert_results_contain(check(ttFont),
                           FAIL, 'bad-string',
                           'with non-ascii on nameID 0 entry (Copyright notice)...')

    assert_results_contain(check(ttFont),
                           FAIL, 'non-ascii-strings',
                           'with non-ascii on nameID 0 entry (Copyright notice)...')

    # Reload the good font once more:
    ttFont = TTFont(TEST_FILE("merriweather/Merriweather-Regular.ttf"))

    #  Note:
    #  A common place where we find non-ASCII strings is on name table
    #  entries with NameID > 18, which are expressly for localising
    #  the ASCII-only IDs into Hindi / Arabic / etc.

    # Let's check a good case of a non-ascii on the name table then!
    # Choose an arbitrary name entry to mess up with:
    index = 5

    ttFont['name'].names[index].nameID = 19
    ttFont['name'].names[index].string = "Fantástico!".encode(encoding="utf-8")
    assert_PASS(check(ttFont),
                'with non-ascii on entries with nameId > 18...')


def test_split_camel_case_condition():
    from fontbakery.utils import split_camel_case
    assert split_camel_case("Lobster") == "Lobster"
    assert split_camel_case("LibreCaslonText") == "Libre Caslon Text"


def test_check_metadata_listed_on_gfonts():
    """ METADATA.pb: Fontfamily is listed on Google Fonts API? """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/listed_on_gfonts")

    font = TEST_FILE("familysans/FamilySans-Regular.ttf")
    # Our reference FamilySans family is a just a generic example
    # and thus is not really hosted (nor will ever be hosted) at Google Fonts servers:
    assert_results_contain(check(font),
                           WARN, 'not-found',
                           f'with "{font}", from a family that\'s'
                           f' not listed on GFonts...')

    font = TEST_FILE("merriweather/Merriweather-Regular.ttf")
    # Our reference Merriweather family is available on the Google Fonts collection:
    assert_PASS(check(font),
                f'with "{font}", from a family that is'
                f' listed on Google Fonts API...')

    font = TEST_FILE("abeezee/ABeeZee-Regular.ttf")
    # This is to ensure the code handles well camel-cased familynames.
    assert_PASS(check(font),
                f'with "{font}", listed and with a camel-cased name...')

    font = TEST_FILE("librecaslontext/LibreCaslonText[wght].ttf")
    # And the check should also properly handle space-separated multi-word familynames.
    assert_PASS(check(font),
                f'with "{font}", available and with a space-separated family name...')


def test_check_metadata_unique_full_name_values():
    """ METADATA.pb: check if fonts field only has unique "full_name" values. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/unique_full_name_values")

    # Our reference FamilySans family is good:
    font = TEST_FILE("familysans/FamilySans-Regular.ttf")
    assert_PASS(check(font),
                'with a good family...')

    # then duplicate a full_name entry to make it FAIL:
    md = check["family_metadata"]
    md.fonts[0].full_name = md.fonts[1].full_name
    assert_results_contain(check(font, {"family_metadata": md}),
                           FAIL, 'duplicated',
                           'with a duplicated full_name entry.')


def test_check_metadata_unique_weight_style_pairs():
    """ METADATA.pb: check if fonts field only contains unique style:weight pairs. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/unique_weight_style_pairs")

    # Our reference FamilySans family is good:
    font = TEST_FILE("familysans/FamilySans-Regular.ttf")
    assert_PASS(check(font),
                'with a good family...')

    # then duplicate a pair of style & weight entries to make it FAIL:
    md = check["family_metadata"]
    md.fonts[0].style = md.fonts[1].style
    md.fonts[0].weight = md.fonts[1].weight
    assert_results_contain(check(font, {"family_metadata": md}),
                           FAIL, 'duplicated',
                           'with a duplicated pair of style & weight entries')


def test_check_metadata_license():
    """ METADATA.pb license is "APACHE2", "UFL" or "OFL"? """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/license")

    # Let's start with our reference FamilySans family:
    font = TEST_FILE("familysans/FamilySans-Regular.ttf")

    good_licenses = ["APACHE2", "UFL", "OFL"]
    some_bad_values = ["APACHE", "Apache", "Ufl", "Ofl", "Open Font License"]

    check(font)
    md = check["family_metadata"]
    for good in good_licenses:
        md.license = good
        assert_PASS(check(font, {"family_metadata": md}),
                    f': {good}')

    for bad in some_bad_values:
        md.license = bad
        assert_results_contain(check(font, {"family_metadata": md}),
                               FAIL, 'bad-license',
                               f': {bad}')


def test_check_metadata_menu_and_latin():
    """ METADATA.pb should contain at least "menu" and "latin" subsets. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/menu_and_latin")

    # Let's start with our reference FamilySans family:
    fonts = TEST_FILE("familysans/FamilySans-Regular.ttf")

    good_cases = [
        ["menu", "latin"],
        ["menu", "cyrillic", "latin"],
    ]

    bad_cases = [
        ["menu"],
        ["latin"],
        [""],
        ["latin", "cyrillyc"],
        ["khmer"]
    ]

    check(fonts)
    md = check["family_metadata"]
    for good in good_cases:
        del md.subsets[:]
        md.subsets.extend(good)
        assert_PASS(check(fonts, {"family_metadata": md}),
                    f'with subsets = {good}')

    for bad in bad_cases:
        del md.subsets[:]
        md.subsets.extend(bad)
        assert_results_contain(check(fonts, {"family_metadata": md}),
                               FAIL, 'missing',
                               f'with subsets = {bad}')


def test_check_metadata_subsets_order():
    """ METADATA.pb subsets should be alphabetically ordered. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/subsets_order")

    # Let's start with our reference FamilySans family:
    fonts = TEST_FILE("familysans/FamilySans-Regular.ttf")

    good_cases = [
        ["latin", "menu"],
        ["cyrillic", "latin", "menu"],
        ["cyrillic", "khmer", "latin", "menu"],
    ]

    bad_cases = [
        ["menu", "latin"],
        ["latin", "cyrillic", "menu"],
        ["cyrillic", "menu", "khmer", "latin"],
    ]

    check(fonts)
    md = check["family_metadata"]
    for good in good_cases:
        del md.subsets[:]
        md.subsets.extend(good)
        assert_PASS(check(fonts, {"family_metadata": md}),
                    f'with subsets = {good}')

    md = check["family_metadata"]
    for bad in bad_cases:
        del md.subsets[:]
        md.subsets.extend(bad)
        assert_results_contain(check(fonts, {"family_metadata": md}),
                               FAIL, 'not-sorted',
                               f'with subsets = {bad}')


def test_check_metadata_includes_production_subsets():
    """Check METADATA.pb has production subsets."""
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/includes_production_subsets")

    # We need to use a family that is already in production
    # Our reference Cabin is known to be good
    fonts = cabin_fonts

    # So it must PASS the check:
    assert_PASS(check(fonts),
                "with a good METADATA.pb for this family...")

    # Then we induce the problem by removing a subset:
    md = check["family_metadata"]
    md.subsets.pop()
    assert_results_contain(check(fonts, {"family_metadata": md}),
                           FAIL, 'missing-subsets',
                           'with a bad METADATA.pb (last subset has been removed)...')


def test_check_metadata_copyright():
    """ METADATA.pb: Copyright notice is the same in all fonts? """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/copyright")

    # Let's start with our reference FamilySans family:
    font = TEST_FILE("familysans/FamilySans-Regular.ttf")

    # We know its copyright notices are consistent:
    assert_PASS(check(font),
                'with consistent copyright notices on FamilySans...')

    # Now we make them diverge:
    md = check["family_metadata"]
    md.fonts[1].copyright = md.fonts[0].copyright + " arbitrary suffix!" # to make it different

    # To ensure the problem is detected:
    assert_results_contain(check(font, {"family_metadata": md}),
                           FAIL, 'inconsistency',
                           'with diverging copyright notice strings...')


def test_check_metadata_familyname():
    """ Check that METADATA.pb family values are all the same. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/familyname")

    # Let's start with our reference FamilySans family:
    font = TEST_FILE("familysans/FamilySans-Regular.ttf")

    # We know its family name entries on METADATA.pb are consistent:
    assert_PASS(check(font),
                'with consistent family name...')

    # Now we make them diverge:
    md = check["family_metadata"]
    md.fonts[1].name = md.fonts[0].name + " arbitrary suffix!" # to make it different

    # To ensure the problem is detected:
    assert_results_contain(check(font, {"family_metadata": md}),
                           FAIL, 'inconsistency',
                           'With diverging Family name metadata entries...')


def test_check_metadata_has_regular():
    """ METADATA.pb: According Google Fonts standards, families should have a Regular style. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/has_regular")

    # Let's start with our reference FamilySans family:
    font = TEST_FILE("familysans/FamilySans-Regular.ttf")

    # We know that Family Sans has got a regular declares in its METADATA.pb file:
    assert_PASS(check(font),
                'with Family Sans, a family with a regular style...')

    # We remove the regular:
    md = check["family_metadata"]
    for i in range(len(md.fonts)):
        if md.fonts[i].filename == "FamilySans-Regular.ttf":
            del md.fonts[i]
            break

    # and make sure the check now FAILs:
    assert_results_contain(check(font, {"family_metadata": md}),
                           FAIL, 'lacks-regular',
                           'with a METADATA.pb file without a regular...')


def test_check_metadata_regular_is_400():
    """ METADATA.pb: Regular should be 400. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/regular_is_400")

    # Let's start with the METADATA.pb file from our reference FamilySans family:
    font = TEST_FILE("familysans/FamilySans-Regular.ttf")

    # We know that Family Sans' Regular has a weight value equal to 400, so the check should PASS:
    assert_PASS(check(font),
                'with Family Sans, a family with regular=400...')

    md = check["family_metadata"]
    # The we change the value for its regular:
    for i in range(len(md.fonts)):
        if md.fonts[i].filename == "FamilySans-Regular.ttf":
            md.fonts[i].weight = 500

    # and make sure the check now FAILs:
    assert_results_contain(check(font, {"family_metadata": md}),
                           FAIL, 'not-400',
                           'with METADATA.pb with regular=500...')


def test_check_metadata_nameid_family_name():
    """ Checks METADATA.pb font.name field matches
        family name declared on the name table. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/nameid/family_name")

    # Let's start with the METADATA.pb file from our reference FamilySans family:
    font = TEST_FILE("familysans/FamilySans-Regular.ttf")

    # We know that Family Sans Regular is good here:
    assert_PASS(check(font))

    # Then cause it to fail:
    md = check["font_metadata"]
    md.name = "Foo"
    assert_results_contain(check(font, {"font_metadata": md}),
                           FAIL, "mismatch")

    # TODO: the failure-mode below seems more generic than the scope
    #       of this individual check. This could become a check by itself!
    #
    # code-paths:
    # - FAIL code="missing", "Font lacks a FONT_FAMILY_NAME entry"


def test_check_metadata_nameid_post_script_name():
    """ Checks METADATA.pb font.post_script_name matches
        postscript name declared on the name table. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/nameid/post_script_name")

    # Let's start with the METADATA.pb file from our reference FamilySans family:
    font = TEST_FILE("familysans/FamilySans-Regular.ttf")

    # We know that Family Sans Regular is good here:
    assert_PASS(check(font))

    # Then cause it to fail:
    md = check["font_metadata"]
    md.post_script_name = "Foo"
    assert_results_contain(check(font, {'font_metadata': md}),
                           FAIL, 'mismatch')

    # TODO: the failure-mode below seems more generic than the scope
    #       of this individual check. This could become a check by itself!
    #
    # code-paths:
    # - FAIL code="missing", "Font lacks a POSTSCRIPT_NAME"


def test_check_metadata_nameid_full_name():
    """ METADATA.pb font.fullname value matches fullname declared on the name table ? """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/nameid/full_name")

    font = TEST_FILE("merriweather/Merriweather-Regular.ttf")

    assert_PASS(check(font),
                'with a good font...')

    # here we change the font.fullname on the METADATA.pb
    # to introduce a "mismatch" error condition:
    font_metadata = check['font_metadata']
    good = font_metadata.full_name
    font_metadata.full_name = good + "bad-suffix"

    assert_results_contain(check(font, {"font_metadata": font_metadata}),
                           FAIL, 'mismatch',
                           'with mismatching fullname values...')

    # and restore the good value prior to the next test case:
    font_metadata.full_name = good

    # And here we remove all FULL_FONT_NAME entries
    # in order to get a "lacks-entry" error condition:
    ttFont = check['ttFont']
    for i, name in enumerate(ttFont["name"].names):
        if name.nameID == NameID.FULL_FONT_NAME:
            del ttFont["name"].names[i]
    assert_results_contain(check(ttFont),
                           FAIL, 'lacks-entry',
                           'when a font lacks FULL_FONT_NAME entries in its name table...')


def test_check_metadata_nameid_font_name():
    """ METADATA.pb font.name value should be same as the family name declared on the name table. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/nameid/font_name")

    # Our reference Merriweather-Regular is know to have good fullname metadata
    font = TEST_FILE("merriweather/Merriweather-Regular.ttf")
    ttFont = TTFont(font)
    assert_PASS(check(ttFont),
                'with a good font...')

    for i, name in enumerate(ttFont["name"].names):
        if name.nameID == NameID.FONT_FAMILY_NAME:
            good = name.string.decode(name.getEncoding()) # keep a copy of the good value
            ttFont["name"].names[i].string = (good + "bad-suffix").encode(name.getEncoding())
            assert_results_contain(check(ttFont),
                                   FAIL, 'mismatch',
                                   f'with a bad FULL_FONT_NAME entry ({i})...')
            ttFont["name"].names[i].string = good # restore good value

    # TODO:
    # FAIL, "lacks-entry"


def test_check_metadata_match_fullname_postscript():
    """ METADATA.pb family.full_name and family.post_script_name
        fields have equivalent values ? """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/match_fullname_postscript")

    regular_font = TEST_FILE("merriweather/Merriweather-Regular.ttf")
    lightitalic_font = TEST_FILE("merriweather/Merriweather-LightItalic.ttf")

    assert_PASS(check(lightitalic_font),
                'with good entries (Merriweather-LightItalic)...')
    #            post_script_name: "Merriweather-LightItalic"
    #            full_name:        "Merriweather Light Italic"

    # TODO: Verify why/whether "Regular" cannot be omited on font.full_name
    #       There's some relevant info at:
    #       https://github.com/googlefonts/fontbakery/issues/1517
    #
    # FIXME: com.google.fonts/check/metadata/nameid/full_name
    #        ties the full_name values from the METADATA.pb file and the
    #        internal name table entry (FULL_FONT_NAME)
    #        to be strictly identical. So it seems that the test below is
    #        actually wrong (as well as the current implementation):
    #
    assert_results_contain(check(regular_font),
                           FAIL, 'mismatch',
                           'with bad entries (Merriweather-Regular)...')
    #                       post_script_name: "Merriweather-Regular"
    #                       full_name:        "Merriweather"

    # fix the regular metadata:
    md = check['font_metadata']
    md.full_name = "Merriweather Regular"

    assert_PASS(check(regular_font, {"font_metadata": md}),
                'with good entries (Merriweather-Regular after full_name fix)...')
    #            post_script_name: "Merriweather-Regular"
    #            full_name:        "Merriweather Regular"


    # introduce an error in the metadata:
    md.full_name = "MistakenFont Regular"

    assert_results_contain(check(regular_font, {"font_metadata": md}),
                           FAIL, 'mismatch',
                           'with a mismatch...')
    #                       post_script_name: "Merriweather-Regular"
    #                       full_name:        "MistakenFont Regular"


def NOT_IMPLEMENTED_test_check_match_filename_postscript():
    """ METADATA.pb family.filename and family.post_script_name
        fields have equivalent values? """
    # check = CheckTester(googlefonts_profile,
    #                     "com.google.fonts/check/match_filename_postscript")
    # TODO: Implement-me!
    #
    # code-paths:
    # - FAIL, "mismatch"		"METADATA.pb filename does not match post_script_name"
    # - PASS


MONTSERRAT_RIBBI = [
    TEST_FILE("montserrat/Montserrat-Regular.ttf"),
    TEST_FILE("montserrat/Montserrat-Italic.ttf"),
    TEST_FILE("montserrat/Montserrat-Bold.ttf"),
    TEST_FILE("montserrat/Montserrat-BoldItalic.ttf")
]
MONTSERRAT_NON_RIBBI = [
    TEST_FILE("montserrat/Montserrat-BlackItalic.ttf"),
    TEST_FILE("montserrat/Montserrat-Black.ttf"),
    TEST_FILE("montserrat/Montserrat-ExtraBoldItalic.ttf"),
    TEST_FILE("montserrat/Montserrat-ExtraBold.ttf"),
    TEST_FILE("montserrat/Montserrat-ExtraLightItalic.ttf"),
    TEST_FILE("montserrat/Montserrat-ExtraLight.ttf"),
    TEST_FILE("montserrat/Montserrat-LightItalic.ttf"),
    TEST_FILE("montserrat/Montserrat-Light.ttf"),
    TEST_FILE("montserrat/Montserrat-MediumItalic.ttf"),
    TEST_FILE("montserrat/Montserrat-Medium.ttf"),
    TEST_FILE("montserrat/Montserrat-SemiBoldItalic.ttf"),
    TEST_FILE("montserrat/Montserrat-SemiBold.ttf"),
    TEST_FILE("montserrat/Montserrat-ThinItalic.ttf"),
    TEST_FILE("montserrat/Montserrat-Thin.ttf")
]

def test_check_metadata_valid_name_values():
    """ METADATA.pb font.name field contains font name in right format? """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/valid_name_values")

    # Our reference Montserrat family is a good 18-styles family:
    for font in MONTSERRAT_RIBBI:
        # So it must PASS the check:
        assert_PASS(check(font),
                    f'with a good RIBBI font ({font})...')

        # And fail if it finds a bad font_familyname:
        assert_results_contain(check(font, {"font_familynames": ["WrongFamilyName"]}),
                               FAIL, 'mismatch',
                               f'with a bad RIBBI font ({font})...')

    # We do the same for NON-RIBBI styles:
    for font in MONTSERRAT_NON_RIBBI:

        # So it must PASS the check:
        assert_PASS(check(font),
                    'with a good NON-RIBBI font ({fontfile})...')

        # And fail if it finds a bad font_familyname:
        assert_results_contain(check(font, {"typographic_familynames": ["WrongFamilyName"]}),
                               FAIL, 'mismatch',
                               f'with a bad NON-RIBBI font ({font})...')


def test_check_metadata_valid_full_name_values():
    """ METADATA.pb font.full_name field contains font name in right format? """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/valid_full_name_values")

    # Our reference Montserrat family is a good 18-styles family:
    # properly described in its METADATA.pb file:
    for font in MONTSERRAT_RIBBI:

        # So it must PASS the check:
        assert_PASS(check(font),
                    'with a good RIBBI font ({fontfile})...')

        # And fail if the full familyname in METADATA.pb diverges
        # from the name inferred from the name table:
        assert_results_contain(check(font, {"font_familynames": ["WrongFamilyName"]}),
                               FAIL, 'mismatch',
                               f'with a bad RIBBI font ({font})...')

    # We do the same for NON-RIBBI styles:
    for font in MONTSERRAT_NON_RIBBI:

        # So it must PASS the check:
        assert_PASS(check(font),
                    f'with a good NON-RIBBI font ({font})...')

        # Unless when not matching typographic familyname from the name table:
        assert_results_contain(check(font, {"typographic_familynames": ["WrongFamilyName"]}),
                               FAIL, 'mismatch',
                               f'with a bad NON-RIBBI font ({font})...')


def test_check_metadata_valid_filename_values():
    """ METADATA.pb font.filename field contains font name in right format? """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/valid_filename_values")

    # Our reference Montserrat family is a good 18-styles family:
    for font in MONTSERRAT_RIBBI + MONTSERRAT_NON_RIBBI:
        # So it must PASS the check:
        assert_PASS(check(font),
                    f"with a good font ({font})...")

        # And fail if it finds a bad filename:
        meta = check["family_metadata"]
        for i in range(len(meta.fonts)):
            meta.fonts[i].filename = "WrongFileName"
        assert_results_contain(check(font, {"family_metadata": meta}),
                               FAIL, 'bad-field',
                               f'with bad filename metadata ("WrongFileName")'
                               f' for fontfile "{font}"...')


def test_check_metadata_valid_post_script_name_values():
    """ METADATA.pb font.post_script_name field contains font name in right format? """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/valid_post_script_name_values")

    # Our reference Montserrat family is a good 18-styles family:
    for fontfile in MONTSERRAT_RIBBI + MONTSERRAT_NON_RIBBI:
        # So it must PASS the check:
        assert_PASS(check(fontfile),
                    f"with a good font ({fontfile})...")

        # And fail if it finds a bad filename:
        md = check["font_metadata"]
        md.post_script_name = "WrongPSName"
        assert_results_contain(check(fontfile, {"font_metadata": md}),
                               FAIL, 'mismatch',
                               f'with a bad font ({fontfile})...')


def test_check_metadata_valid_copyright():
    """ Copyright notice on METADATA.pb matches canonical pattern ? """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/valid_copyright")

    # Our reference Cabin Regular is known to be bad
    # Since it provides an email instead of a git URL:
    font = TEST_FILE("cabin/Cabin-Regular.ttf")
    assert_results_contain(check(font),
                           FAIL, 'bad-notice-format',
                           'with a bad copyright notice string...')

    # Then we change it into a good string (example extracted from Archivo Black):
    # note: the check does not actually verify that the project name is correct.
    #       It only focuses on the string format.
    good_string = ("Copyright 2017 The Archivo Black Project Authors"
                   " (https://github.com/Omnibus-Type/ArchivoBlack)")
    md = check["font_metadata"]
    md.copyright = good_string
    assert_PASS(check(font, {"font_metadata": md}),
                'with a good copyright notice string...')

    # We also ignore case, so these should also PASS:
    md.copyright = good_string.upper()
    assert_PASS(check(font, {"font_metadata": md}),
                'with all uppercase...')

    md.copyright = good_string.lower()
    assert_PASS(check(font, {"font_metadata": md}),
                'with all lowercase...')


def test_check_font_copyright():
    """Copyright notices match canonical pattern in fonts"""
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/font_copyright")

    # Our reference Cabin Regular is known to be bad
    # Since it provides an email instead of a git URL:
    ttFont = TTFont(TEST_FILE("cabin/Cabin-Regular.ttf"))
    assert_results_contain(check(ttFont),
                           FAIL, 'bad-notice-format',
                           'with a bad copyright notice string...')

    # We change it into a good string (example extracted from Archivo Black):
    # note: the check does not actually verify that the project name is correct.
    #       It only focuses on the string format.
    good_string = ("Copyright 2017 The Archivo Black Project Authors"
                   " (https://github.com/Omnibus-Type/ArchivoBlack)")
    for i, entry in enumerate(ttFont['name'].names):
        if entry.nameID == NameID.COPYRIGHT_NOTICE:
            ttFont['name'].names[i].string = good_string.encode(entry.getEncoding())
    assert_PASS(check(ttFont),
                'with good strings...')


def DISABLE_test_check_glyphs_file_font_copyright():
    """Copyright notices match canonical pattern in fonts"""
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/glyphs_file/font_copyright")

    glyphsFile = GLYPHSAPP_TEST_FILE("Comfortaa.glyphs")
    # note: the check does not actually verify that the project name is correct.
    #       It only focuses on the string format.

    # Use an email instead of a git URL:
    bad_string = ("Copyright 2017 The Archivo Black Project Authors"
                  " (contact-us@fake-address.com)")
    glyphsFile.copyright = bad_string
    assert_results_contain(check(glyphsFile),
                           FAIL, 'bad-notice-format',
                           'with a bad copyright notice string...')

    # We change it into a good string (example extracted from Archivo Black):
    good_string = ("Copyright 2017 The Archivo Black Project Authors"
                   " (https://github.com/Omnibus-Type/ArchivoBlack)")
    glyphsFile.copyright = good_string
    assert_PASS(check(glyphsFile),
                'with a good coopyright string...')


def test_check_metadata_reserved_font_name():
    """ Copyright notice on METADATA.pb should not contain Reserved Font Name. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/reserved_font_name")

    font = TEST_FILE("cabin/Cabin-Regular.ttf")
    assert_PASS(check(font),
                'with a good copyright notice string...')

    # Then we make it bad:
    md = check["font_metadata"]
    md.copyright += "Reserved Font Name"
    assert_results_contain(check(font, {"font_metadata": md}),
                           WARN, 'rfn',
                           'with a notice containing "Reserved Font Name"...')


def test_check_metadata_copyright_max_length():
    """ METADATA.pb: Copyright notice shouldn't exceed 500 chars. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/copyright_max_length")

    font = TEST_FILE("cabin/Cabin-Regular.ttf")
    check(font)
    md = check["font_metadata"]

    md.copyright = 500 * "x"
    assert_PASS(check(font, {"font_metadata": md}),
                'with a 500-char copyright notice string...')

    md.copyright = 501 * "x"
    assert_results_contain(check(font, {"font_metadata": md}),
                           FAIL, 'max-length',
                           'with a 501-char copyright notice string...')


def test_check_metadata_filenames():
    """ METADATA.pb: Font filenames match font.filename entries? """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/filenames")

    assert_PASS(check(montserrat_fonts),
                'with matching list of font files...')

    # make sure missing files are detected by the check:
    fonts = montserrat_fonts
    original_name = fonts[0]
    # rename one font file in order to trigger the FAIL
    os.rename(original_name, "font.tmp")
    assert_results_contain(check(fonts),
                           FAIL, 'file-not-found',
                           'with missing font files...')
    os.rename("font.tmp", original_name) # restore filename


    # From all TTFs in Cabin's directory, the condensed ones are not
    # listed on METADATA.pb, so the check must FAIL, even if we do not
    # explicitely include them in the set of files to be checked:
    assert_results_contain(check(cabin_fonts),
                           FAIL, 'file-not-declared',
                           'with some font files not declared...')


def test_check_metadata_italic_style():
    """ METADATA.pb font.style "italic" matches font internals ? """
    from fontbakery.constants import MacStyle
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/italic_style")

    # Our reference Merriweather Italic is known to good
    ttFont = TTFont(TEST_FILE("merriweather/Merriweather-Italic.ttf"))
    assert_PASS(check(ttFont),
                'with a good font...')

    # now let's introduce issues on the FULL_FONT_NAME entries
    # to test the "bad-fullfont-name" codepath:
    for i, name in enumerate(ttFont['name'].names):
        if name.nameID == NameID.FULL_FONT_NAME:
            backup = name.string
            ttFont['name'].names[i].string = "BAD VALUE".encode(name.getEncoding())
            assert_results_contain(check(ttFont),
                                   FAIL, 'bad-fullfont-name',
                                   'with a bad NameID.FULL_FONT_NAME entry...')
            # and restore the good value:
            ttFont['name'].names[i].string = backup

    # And, finally, let's flip off that italic bit
    # and get a "bad-macstyle" FAIL (so much fun!):
    ttFont['head'].macStyle &= ~MacStyle.ITALIC
    assert_results_contain(check(ttFont),
                           FAIL, 'bad-macstyle',
                           'with bad macstyle bit value...')


def test_check_metadata_normal_style():
    """ METADATA.pb font.style "normal" matches font internals ? """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/normal_style")
    from fontbakery.constants import MacStyle

    # This one is pretty similar to check/metadata/italic_style
    # You may want to take a quick look above...

    # Our reference Merriweather Regular is known to be good here.
    ttFont = TTFont(TEST_FILE("merriweather/Merriweather-Regular.ttf"))
    assert_PASS(check(ttFont),
                'with a good font...')

    # now we sadically insert brokenness into
    # each occurrence of the FONT_FAMILY_NAME nameid:
    for i, name in enumerate(ttFont['name'].names):
        if name.nameID == NameID.FONT_FAMILY_NAME:
            backup = name.string
            ttFont['name'].names[i].string = "Merriweather-Italic".encode(name.getEncoding())
            assert_results_contain(check(ttFont),
                                   FAIL, 'familyname-italic',
                                   'with a non-italic font that has a "-Italic" in FONT_FAMILY_NAME...')
            # and restore the good value:
            ttFont['name'].names[i].string = backup

    # now let's do the same with
    # occurrences of the FULL_FONT_NAME nameid:
    for i, name in enumerate(ttFont['name'].names):
        if name.nameID == NameID.FULL_FONT_NAME:
            backup = name.string
            ttFont['name'].names[i].string = "Merriweather-Italic".encode(name.getEncoding())
            assert_results_contain(check(ttFont),
                                   FAIL, 'fullfont-italic',
                                   'with a non-italic font that has a "-Italic" in FULL_FONT_NAME...')
            # and restore the good value:
            ttFont['name'].names[i].string = backup

    # And, finally, again, we flip a bit and...
    #
    # Note: This time the boolean logic is the quite opposite in comparison
    # to the test for com.google.fonts/check/metadata/italic_style above.
    # Here we have to set the bit back to 1 to get a wrongful "this font is an italic" setting:
    ttFont['head'].macStyle |= MacStyle.ITALIC
    assert_results_contain(check(ttFont),
                           FAIL, 'bad-macstyle',
                           'with bad macstyle bit value...')


def test_check_metadata_nameid_family_and_full_names():
    """ METADATA.pb font.name and font.full_name fields match the values declared on the name table? """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/nameid/family_and_full_names")

    # Our reference Merriweather Regular is known to be good here.
    ttFont = TTFont(TEST_FILE("merriweather/Merriweather-Regular.ttf"))
    assert_PASS(check(ttFont),
                'with a good font...')

    # There we go again!
    # Breaking FULL_FONT_NAME entries one by one:
    for i, name in enumerate(ttFont['name'].names):
        if name.nameID == NameID.FULL_FONT_NAME:
            backup = name.string
            ttFont['name'].names[i].string = "This is utterly wrong!".encode(name.getEncoding())
            assert_results_contain(check(ttFont),
                                   FAIL, 'fullname-mismatch',
                                   'with a METADATA.pb / FULL_FONT_NAME mismatch...')
            # and restore the good value:
            ttFont['name'].names[i].string = backup

    # And then we do the same with FONT_FAMILY_NAME entries:
    for i, name in enumerate(ttFont['name'].names):
        if name.nameID == NameID.FONT_FAMILY_NAME:
            backup = name.string
            ttFont['name'].names[i].string = ("I'm listening to"
                                              " The Players with Hiromasa Suzuki - Galaxy (1979)").encode(name.getEncoding())
            assert_results_contain(check(ttFont),
                                   FAIL, 'familyname-mismatch',
                                   'with a METADATA.pb / FONT_FAMILY_NAME mismatch...')
            # and restore the good value:
            ttFont['name'].names[i].string = backup


def test_check_metadata_fontname_not_camel_cased():
    """ METADATA.pb: Check if fontname is not camel cased. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/fontname_not_camel_cased")

    # Our reference Cabin Regular is known to be good
    font = TEST_FILE("cabin/Cabin-Regular.ttf")
    assert_PASS(check(font),
                'with a good font...')

    # Then we FAIL with a CamelCased name:
    md = check["font_metadata"]
    md.name = "GollyGhost"
    assert_results_contain(check(font, {"font_metadata": md}),
                           FAIL, 'camelcase',
                           'with a bad font name (CamelCased)...')

    # And we also make sure the check PASSes with a few known good names:
    for good_name in ["VT323",
                      "PT Sans",
                      "Amatic SC"]:
        md.name = good_name
        assert_PASS(check(font, {"font_metadata": md}),
                    f'with a good font name "{good_name}"...')


def test_check_metadata_match_name_familyname():
    """ METADATA.pb: Check font name is the same as family name. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/match_name_familyname")

    # Our reference Cabin Regular is known to be good
    font = TEST_FILE("cabin/Cabin-Regular.ttf")
    assert_PASS(check(font),
                'with a good font...')

    # Then we FAIL with mismatching names:
    family_md = check["family_metadata"]
    font_md = check["font_metadata"]
    family_md.name = "Some Fontname"
    font_md.name = "Something Else"
    assert_results_contain(check(font, {"family_metadata": family_md,
                                        "font_metadata": font_md}),
                           FAIL, 'mismatch',
                           'with bad font/family name metadata...')


def test_check_check_metadata_canonical_weight_value():
    """ METADATA.pb: Check that font weight has a canonical value. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/canonical_weight_value")

    font = TEST_FILE("cabin/Cabin-Regular.ttf")
    check(font)
    md = check["font_metadata"]

    for w in [100, 200, 300, 400, 500, 600, 700, 800, 900]:
        md.weight = w
        assert_PASS(check(font, {"font_metadata": md}),
                    f'with a good weight value ({w})...')

    for w in [150, 250, 350, 450, 550, 650, 750, 850]:
        md.weight = w
        assert_results_contain(check(font, {"font_metadata": md}),
                               FAIL, 'bad-weight',
                               'with a bad weight value ({w})...')


def test_check_metadata_os2_weightclass():
    """ Checking OS/2 usWeightClass matches weight specified at METADATA.pb """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/os2_weightclass")

    # === test cases for Variable Fonts ===
    # Our reference Jura is known to be good
    ttFont = TTFont(TEST_FILE("varfont/jura/Jura[wght].ttf"))
    assert_PASS(check(ttFont),
                f'with a good metadata...')

    # Should report if a bad weight value is ifound though:
    md = check["font_metadata"]
    good_value = md.weight
    bad_value = good_value + 100
    md.weight = bad_value
    assert_results_contain(check(ttFont, {"font_metadata": md}),
                           FAIL, 'mismatch',
                           f'with a bad metadata...')

    ttFont = TTFont(TEST_FILE("leaguegothic-vf/LeagueGothic[wdth].ttf"))
    assert_PASS(check(ttFont),
                f'with a good VF that lacks a "wght" axis....')
                # See: https://github.com/googlefonts/fontbakery/issues/3529

    # === test cases for Static Fonts ===
    # Our reference Montserrat family is a good 18-styles family:
    for fontfile in MONTSERRAT_RIBBI + MONTSERRAT_NON_RIBBI:
        ttFont = TTFont(fontfile)
        assert_PASS(check(ttFont),
                    f'with a good font ({fontfile})...')

        # but should report bad weight values:
        md = check["font_metadata"]
        good_value = md.weight
        bad_value = good_value + 50
        md.weight = bad_value
        assert_results_contain(check(ttFont, {"font_metadata": md}),
                               FAIL, 'mismatch',
                               f'with bad metadata for {fontfile}...')

        # If font is Thin or ExtraLight, ensure that this check can
        # accept both 100, 250 for Thin and 200, 275 for ExtraLight
        if "Thin" in fontfile:
            ttFont["OS/2"].usWeightClass = 100
            assert_PASS(check(ttFont),
                        f'with weightclass 100 on ({fontfile})...')

            ttFont["OS/2"].usWeightClass = 250
            assert_PASS(check(ttFont),
                        f'with weightclass 250 on ({fontfile})...')

        if "ExtraLight" in fontfile:
            ttFont["OS/2"].usWeightClass = 200
            assert_PASS(check(ttFont),
                        f'with weightClass 200 on ({fontfile})...')

            ttFont["OS/2"].usWeightClass = 275
            assert_PASS(check(ttFont),
                        f'with weightClass 275 on ({fontfile})...')


def NOT_IMPLEMENTED_test_check_metadata_match_weight_postscript():
    """ METADATA.pb: Metadata weight matches postScriptName. """
    # check = CheckTester(googlefonts_profile,
    #                     "com.google.fonts/check/metadata/match_weight_postscript")
    # TODO: Implement-me!
    #
    # code-paths:
    # - FAIL, "METADATA.pb: Font weight value is invalid."
    # - FAIL, "METADATA.pb: Mismatch between postScriptName and weight value."
    # - PASS


def NOT_IMPLEMENTED_test_check_metadata_canonical_style_names():
    """ METADATA.pb: Font styles are named canonically? """
    # check = CheckTester(googlefonts_profile,
    #                     "com.google.fonts/check/metadata/canonical_style_names")
    # TODO: Implement-me!
    #
    # code-paths:
    # - SKIP		"Applicable only to font styles declared as 'italic' or 'normal' on METADATA.pb."
    # - FAIL, "italic"	"Font style should be italic."
    # - FAIL, "normal"	"Font style should be normal."
    # - PASS		"Font styles are named canonically."


def test_check_unitsperem_strict():
    """ Stricter unitsPerEm criteria for Google Fonts. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/unitsperem_strict")

    ttFont = TTFont(TEST_FILE("cabin/Cabin-Regular.ttf"))

    PASS_VALUES = [16, 32, 64, 128, 256, 512, 1024] # Good for better performance on legacy renderers
    PASS_VALUES.extend([500, 1000]) # or common typical values
    PASS_VALUES.extend([2000, 2048]) # not so common, but still ok

    WARN_LARGE_VALUES = [2500, 4000, 4096] # uncommon and large,
                                           # but we've seen legitimate cases such as the
                                           # Big Shoulders Family which uses 4000 since
                                           # it needs more details.

    # and finally the bad ones, including:
    FAIL_VALUES = [0, 1, 2, 4, 8, 15, 16385] # simply invalid
    FAIL_VALUES.extend([100, 1500, 5000]) # suboptimal (uncommon and not power of two)
    FAIL_VALUES.extend([8192, 16384]) # and valid ones suggested by the opentype spec,
                                      # but too large, causing undesireable filesize bloat.


    for pass_value in PASS_VALUES:
        ttFont["head"].unitsPerEm = pass_value
        assert_PASS(check(ttFont),
                    f'with unitsPerEm = {pass_value}...')


    for warn_value in WARN_LARGE_VALUES:
        ttFont["head"].unitsPerEm = warn_value
        assert_results_contain(check(ttFont),
                               WARN, 'large-value',
                               f'with unitsPerEm = {warn_value}...')

    for fail_value in FAIL_VALUES:
        ttFont["head"].unitsPerEm = fail_value
        assert_results_contain(check(ttFont),
                               FAIL, 'bad-value',
                               f'with unitsPerEm = {fail_value}...')


def NOT_IMPLEMENTED_test_check_version_bump():
    """ Version number has increased since previous release on Google Fonts? """
    # check = CheckTester(googlefonts_profile,
    #                     "com.google.fonts/check/version_bump")
    # TODO: Implement-me!
    #
    # code-paths:
    # - FAIL, "Version number is equal to version on Google Fonts."
    # - FAIL, "Version number is less than version on Google Fonts."
    # - FAIL, "Version number is equal to version on Google Fonts GitHub repo."
    # - FAIL, "Version number is less than version on Google Fonts GitHub repo."
    # - PASS


def NOT_IMPLEMENTED_test_check_production_glyphs_similarity():
    """ Glyphs are similiar to Google Fonts version? """
    # check = CheckTester(googlefonts_profile,
    #                     "com.google.fonts/check/production_glyphs_similarity")
    # TODO: Implement-me!
    #
    # code-paths:
    # - WARN, "Following glyphs differ greatly from Google Fonts version"
    # - PASS, "Glyphs are similar"


def NOT_IMPLEMENTED_test_check_fsselection():
    """ Checking OS/2 fsSelection value. """
    # check = CheckTester(googlefonts_profile,
    #                     "com.google.fonts/check/fsselection")
    # TODO: Implement-me!
    #
    # code-paths:
    # ...


def test_check_italic_angle():
    """ Checking post.italicAngle value. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/italic_angle")

    ttFont = TTFont(TEST_FILE("cabin/Cabin-Regular.ttf"))

    # italic-angle, style, fail_message
    test_cases = [
        [1, "Italic", FAIL, "positive"],
        [0, "Regular", PASS, None], # This must PASS as it is a non-italic
        [-21, "ThinItalic", WARN, "over-minus20-degrees"],
        [-30, "ThinItalic", WARN, "over-minus20-degrees"],
        [-31, "ThinItalic", FAIL, "over-minus30-degrees"],
        [0, "Italic", FAIL, "zero-italic"],
        [-1,"ExtraBold", FAIL, "non-zero-normal"]
    ]

    for value, style, expected_result, expected_msg in test_cases:
        ttFont["post"].italicAngle = value

        if expected_result != PASS:
            assert_results_contain(check(ttFont, {"style": style}),
                                   expected_result,
                                   expected_msg,
                                   f"with italic-angle:{value} style:{style}...")
        else:
            assert_PASS(check(ttFont, {"style": style}),
                        f'with italic-angle:{value} style:{style}...')


def test_check_mac_style():
    """ Checking head.macStyle value. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/mac_style")
    from fontbakery.constants import MacStyle

    ttFont = TTFont(TEST_FILE("cabin/Cabin-Regular.ttf"))

    # macStyle-value, style, expected
    test_cases = [
        [0, "Thin", PASS],
        [0, "Bold", "bad-BOLD"],
        [0, "Italic", "bad-ITALIC"],
        [MacStyle.ITALIC, "Italic", PASS],
        [MacStyle.ITALIC, "Thin", "bad-ITALIC"],
        [MacStyle.BOLD, "Bold", PASS],
        [MacStyle.BOLD, "Thin", "bad-BOLD"],
        [MacStyle.BOLD | MacStyle.ITALIC, "BoldItalic", PASS]
    ]

    for macStyle_value, style, expected in test_cases:
        ttFont["head"].macStyle = macStyle_value

        if expected == PASS:
            assert_PASS(check(ttFont, {"style": style}),
                        'with macStyle:{macStyle_value} style:{style}...')
        else:
            assert_results_contain(check(ttFont, {"style": style}),
                                   FAIL, expected,
                                   f"with macStyle:{macStyle_value} style:{style}...")


# FIXME!
# GFonts hosted Cabin files seem to have changed in ways
# that break some of the assumptions in the code-test below.
# More info at https://github.com/googlefonts/fontbakery/issues/2581
@pytest.mark.xfail(strict=True)
def test_check_production_encoded_glyphs(cabin_ttFonts):
    """Check glyphs are not missing when compared to version on fonts.google.com"""
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/production_encoded_glyphs")

    for font in cabin_fonts:
        # Cabin font hosted on fonts.google.com contains
        # all the glyphs for the font in data/test/cabin
        assert_PASS(check(font),
                    f"with '{font}'")

        ttFont = check["ttFont"]
        # Take A glyph out of font
        ttFont['cmap'].getcmap(3, 1).cmap.pop(ord('A'))
        ttFont['glyf'].glyphs.pop('A')
        assert_results_contain(check(ttFont),
                               FAIL, 'lost-glyphs')


def test_check_metadata_nameid_copyright():
    """ Copyright field for this font on METADATA.pb matches
        all copyright notice entries on the name table? """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/nameid/copyright")
    from fontbakery.utils import get_name_entry_strings

    # Our reference Cabin Regular is known to be good
    font = TEST_FILE("cabin/Cabin-Regular.ttf")
    assert_PASS(check(font),
                "with a good METADATA.pb for this font...")

    # But the check must report when mismatching names are found:
    good_value = get_name_entry_strings(check["ttFont"],
                                        NameID.COPYRIGHT_NOTICE)[0]
    md = check["font_metadata"]
    md.copyright = good_value + "something bad"
    assert_results_contain(check(font, {"font_metadata": md}),
                           FAIL, 'mismatch',
                           'with a bad METADATA.pb'
                           ' (with a copyright string not matching this font)...')


def test_check_metadata_category():
    """ Category field for this font on METADATA.pb is valid? """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/category")

    # Our reference Cabin family...
    font = TEST_FILE("cabin/Cabin-Regular.ttf")
    check(font)
    md = check["family_metadata"]
    assert md.category == "SANS_SERIF" # ...is known to be good ;-)
    assert_PASS(check(font),
                "with a good METADATA.pb...")

    # We then report a problem with this sample of bad values:
    for bad_value in ["SAN_SERIF",
                      "MONO_SPACE",
                      "sans_serif",
                      "monospace"]:
        md.category = bad_value
        assert_results_contain(check(font, {"family_metadata": md}),
                               FAIL, 'bad-value',
                               f'with a bad category "{bad_value}"...')

    # And we accept the good ones:
    for good_value in ["MONOSPACE",
                       "SANS_SERIF",
                       "SERIF",
                       "DISPLAY",
                       "HANDWRITING"]:
        md.category = good_value
        assert_PASS(check(font, {"family_metadata": md}),
                    f'with "{good_value}"...')


def test_check_name_mandatory_entries():
    """ Font has all mandatory 'name' table entries ? """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/name/mandatory_entries")

    # We'll check both RIBBI and non-RIBBI fonts
    # so that we cover both cases for FAIL/PASS scenarios

    # === First with a RIBBI font: ===
    # Our reference Cabin Regular is known to be good
    ttFont = TTFont(TEST_FILE("cabin/Cabin-Regular.ttf"))
    assert_PASS(check(ttFont),
                "with a good RIBBI font...")

    mandatory_entries = [NameID.FONT_FAMILY_NAME,
                         NameID.FONT_SUBFAMILY_NAME,
                         NameID.FULL_FONT_NAME,
                         NameID.POSTSCRIPT_NAME]

    # then we "remove" each mandatory entry one by one:
    for mandatory in mandatory_entries:
        ttFont = TTFont(TEST_FILE("cabin/Cabin-Regular.ttf"))
        for i, name in enumerate(ttFont['name'].names):
            if name.nameID == mandatory:
                ttFont['name'].names[i].nameID = 0 # not really removing it, but replacing it
                                                   # by something else completely irrelevant
                                                   # for the purposes of this specific check
        assert_results_contain(check(ttFont),
                               FAIL, 'missing-entry',
                               f'with a missing madatory (RIBBI) name entry (id={mandatory})...')

    # === And now a non-RIBBI font: ===
    # Our reference Merriweather Black is known to be good
    ttFont = TTFont(TEST_FILE("merriweather/Merriweather-Black.ttf"))
    assert_PASS(check(ttFont),
                "with a good non-RIBBI font...")

    mandatory_entries = [NameID.FONT_FAMILY_NAME,
                         NameID.FONT_SUBFAMILY_NAME,
                         NameID.FULL_FONT_NAME,
                         NameID.POSTSCRIPT_NAME,
                         NameID.TYPOGRAPHIC_FAMILY_NAME,
                         NameID.TYPOGRAPHIC_SUBFAMILY_NAME]

    # then we (again) "remove" each mandatory entry one by one:
    for mandatory in mandatory_entries:
        ttFont = TTFont(TEST_FILE("merriweather/Merriweather-Black.ttf"))
        for i, name in enumerate(ttFont['name'].names):
            if name.nameID in mandatory_entries:
                ttFont['name'].names[i].nameID = 0 # not really removing it, but replacing it
                                                   # by something else completely irrelevant
                                                   # for the purposes of this specific check
        assert_results_contain(check(ttFont),
                               FAIL, 'missing-entry',
                               'with a missing madatory (non-RIBBI) name entry (id={mandatory})...')


def test_condition_familyname_with_spaces():
    from fontbakery.profiles.googlefonts_conditions import familyname_with_spaces
    assert familyname_with_spaces("OverpassMono") == "Overpass Mono"
    assert familyname_with_spaces("BodoniModa11") == "Bodoni Moda 11"


def test_check_name_familyname():
    """ Check name table: FONT_FAMILY_NAME entries. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/name/familyname")

    # TODO: FAIL, "lacks-name"

    test_cases = [
        #expect                      filename                                      mac_value        win_value
        (PASS, "ok",       TEST_FILE("cabin/Cabin-Regular.ttf"),                   "Cabin",         "Cabin"),
        (FAIL, "mismatch", TEST_FILE("cabin/Cabin-Regular.ttf"),                   "Wrong",         "Cabin"),
        (PASS, "ok",       TEST_FILE("overpassmono/OverpassMono-Regular.ttf"),     "Overpass Mono", "Overpass Mono"),
        (PASS, "ok",       TEST_FILE("overpassmono/OverpassMono-Bold.ttf"),        "Overpass Mono", "Overpass Mono"),
        (FAIL, "mismatch", TEST_FILE("overpassmono/OverpassMono-Regular.ttf"),     "Overpass Mono", "Foo"),
        (PASS, "ok",       TEST_FILE("merriweather/Merriweather-Black.ttf"),       "Merriweather",  "Merriweather Black"),
        (PASS, "ok",       TEST_FILE("merriweather/Merriweather-LightItalic.ttf"), "Merriweather",  "Merriweather Light"),
        (FAIL, "mismatch", TEST_FILE("merriweather/Merriweather-LightItalic.ttf"), "Merriweather",  "Merriweather Light Italic"),
        (PASS, "ok",       TEST_FILE("abeezee/ABeeZee-Regular.ttf"),               "ABeeZee",       "ABeeZee"),
        # Note: ABeeZee is a good camel-cased name exception.
    ]

    for expected, keyword, filename, mac_value, win_value in test_cases:
        ttFont = TTFont(filename)
        for i, name in enumerate(ttFont['name'].names):
            if name.platformID == PlatformID.MACINTOSH:
                value = mac_value
            if name.platformID == PlatformID.WINDOWS:
                value = win_value
            assert value

            if name.nameID == NameID.FONT_FAMILY_NAME:
                ttFont['name'].names[i].string = value.encode(name.getEncoding())
        assert_results_contain(check(ttFont),
                                     expected, keyword,
                                     f'with filename="{filename}",'
                                     f' value="{value}", style="{check["style"]}"...')


def test_check_name_subfamilyname():
    """ Check name table: FONT_SUBFAMILY_NAME entries. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/name/subfamilyname")

    PASS_test_cases = [
        #          filename                                       mac_value             win_value
        (TEST_FILE("overpassmono/OverpassMono-Regular.ttf"),      "Regular",            "Regular"),
        (TEST_FILE("overpassmono/OverpassMono-Bold.ttf"),         "Bold",               "Bold"),
        (TEST_FILE("merriweather/Merriweather-Black.ttf"),        "Black",              "Regular"),
        (TEST_FILE("merriweather/Merriweather-LightItalic.ttf"),  "Light Italic",       "Italic"),
        (TEST_FILE("montserrat/Montserrat-BlackItalic.ttf"),      "Black Italic",       "Italic"),
        (TEST_FILE("montserrat/Montserrat-Black.ttf"),            "Black",              "Regular"),
        (TEST_FILE("montserrat/Montserrat-BoldItalic.ttf"),       "Bold Italic",        "Bold Italic"),
        (TEST_FILE("montserrat/Montserrat-Bold.ttf"),             "Bold",               "Bold"),
        (TEST_FILE("montserrat/Montserrat-ExtraBoldItalic.ttf"),  "ExtraBold Italic",   "Italic"),
        (TEST_FILE("montserrat/Montserrat-ExtraBold.ttf"),        "ExtraBold",          "Regular"),
        (TEST_FILE("montserrat/Montserrat-ExtraLightItalic.ttf"), "ExtraLight Italic",  "Italic"),
        (TEST_FILE("montserrat/Montserrat-ExtraLight.ttf"),       "ExtraLight",         "Regular"),
        (TEST_FILE("montserrat/Montserrat-Italic.ttf"),           "Italic",             "Italic"),
        (TEST_FILE("montserrat/Montserrat-LightItalic.ttf"),      "Light Italic",       "Italic"),
        (TEST_FILE("montserrat/Montserrat-Light.ttf"),            "Light",              "Regular"),
        (TEST_FILE("montserrat/Montserrat-MediumItalic.ttf"),     "Medium Italic",      "Italic"),
        (TEST_FILE("montserrat/Montserrat-Medium.ttf"),           "Medium",             "Regular"),
        (TEST_FILE("montserrat/Montserrat-Regular.ttf"),          "Regular",            "Regular"),
        (TEST_FILE("montserrat/Montserrat-SemiBoldItalic.ttf"),   "SemiBold Italic",    "Italic"),
        (TEST_FILE("montserrat/Montserrat-SemiBold.ttf"),         "SemiBold",           "Regular"),
        (TEST_FILE("montserrat/Montserrat-ThinItalic.ttf"),       "Thin Italic",        "Italic"),
        (TEST_FILE("montserrat/Montserrat-Thin.ttf"),             "Thin",               "Regular")
    ]

    for filename, mac_value, win_value in PASS_test_cases:
        ttFont = TTFont(filename)
        for i, name in enumerate(ttFont['name'].names):
            if name.platformID == PlatformID.MACINTOSH:
                value = mac_value
            if name.platformID == PlatformID.WINDOWS:
                value = win_value
            assert value

            if name.nameID == NameID.FONT_SUBFAMILY_NAME:
                ttFont['name'].names[i].string = value.encode(name.getEncoding())

        results = check(ttFont)
        style = check["expected_style"]
        assert_PASS(results,
                    f"with filename='{filename}', value='{value}', "
                    f"style_win='{style.win_style_name}', "
                    f"style_mac='{style.mac_style_name}'...")

    # - FAIL, "bad-familyname" - "Bad familyname value on a FONT_SUBFAMILY_NAME entry."
    filename = TEST_FILE("montserrat/Montserrat-ThinItalic.ttf")
    ttFont = TTFont(filename)
    # We setup a bad entry:
    ttFont["name"].setName("Not a proper style",
                           NameID.FONT_SUBFAMILY_NAME,
                           PlatformID.MACINTOSH,
                           MacintoshEncodingID.ROMAN,
                           MacintoshLanguageID.ENGLISH)

    # And this should now FAIL:
    assert_results_contain(check(ttFont),
                           FAIL, 'bad-familyname')

    # Repeat this for a Win subfamily name
    ttFont = TTFont(filename)
    ttFont["name"].setName("Not a proper style",
                           NameID.FONT_SUBFAMILY_NAME,
                           PlatformID.WINDOWS,
                           WindowsEncodingID.UNICODE_BMP,
                           WindowsLanguageID.ENGLISH_USA)
    assert_results_contain(check(ttFont),
                           FAIL, 'bad-familyname')


def test_check_name_fullfontname():
    """ Check name table: FULL_FONT_NAME entries. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/name/fullfontname")

    # Our reference Cabin Regular is known to be good
    ttFont = TTFont(TEST_FILE("cabin/Cabin-Regular.ttf"))
    assert_PASS(check(ttFont),
                "with a good Regular font...")

    # Let's now test the Regular exception
    # ('Regular' can be optionally ommited on the FULL_FONT_NAME entry):
    for index, name in enumerate(ttFont["name"].names):
        if name.nameID == NameID.FULL_FONT_NAME:
            backup = name.string
            ttFont["name"].names[index].string = "Cabin".encode(name.getEncoding())
            assert_results_contain(check(ttFont),
                                   WARN, 'lacks-regular',
                                   'with a good Regular font that omits "Regular" on FULL_FONT_NAME...')
            # restore it:
            ttFont["name"].names[index].string = backup

    # Let's also make sure our good reference Cabin BoldItalic PASSes the check.
    # This also tests the splitting of filename infered style with a space char
    ttFont = TTFont(TEST_FILE("cabin/Cabin-BoldItalic.ttf"))
    assert_PASS(check(ttFont),
                "with a good Bold Italic font...")

    # And here we test the FAIL codepath:
    for index, name in enumerate(ttFont["name"].names):
        if name.nameID == NameID.FULL_FONT_NAME:
            backup = name.string
            ttFont["name"].names[index].string = "MAKE IT FAIL".encode(name.getEncoding())
            assert_results_contain(check(ttFont),
                                   FAIL, 'bad-entry',
                                   'with a bad FULL_FONT_NAME entry...')
            # restore it:
            ttFont["name"].names[index].string = backup

    # And we should also accept a few camel-cased familyname exceptions,
    # so this one should also be fine:
    ttFont = TTFont(TEST_FILE("abeezee/ABeeZee-Regular.ttf"))
    assert_PASS(check(ttFont),
                "with a good camel-cased fontname...")


def NOT_IMPLEMENTED_test_check_name_postscriptname():
    """ Check name table: POSTSCRIPT_NAME entries. """
    # check = CheckTester(googlefonts_profile,
    #                     "com.google.fonts/check/name/postscriptname")
    # TODO: Implement-me!
    #
    # code-paths:
    # - FAIL, "bad-entry"
    # - PASS


def test_check_name_typographicfamilyname():
    """ Check name table: TYPOGRAPHIC_FAMILY_NAME entries. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/name/typographicfamilyname")

    # RIBBI fonts must not have a TYPOGRAPHIC_FAMILY_NAME entry
    ttFont = TTFont(TEST_FILE("montserrat/Montserrat-BoldItalic.ttf"))
    assert_PASS(check(ttFont),
                f"with a RIBBI without nameid={NameID.TYPOGRAPHIC_FAMILY_NAME} entry...")

    # so we add one and make sure the check reports the problem:
    ttFont['name'].names[5].nameID = NameID.TYPOGRAPHIC_FAMILY_NAME # 5 is arbitrary here
    assert_results_contain(check(ttFont),
                           FAIL, 'ribbi',
                           f'with a RIBBI that has got a nameid={NameID.TYPOGRAPHIC_FAMILY_NAME} entry...')

    # non-RIBBI fonts must have a TYPOGRAPHIC_FAMILY_NAME entry
    ttFont = TTFont(TEST_FILE("montserrat/Montserrat-ExtraLight.ttf"))
    assert_PASS(check(ttFont),
                f"with a non-RIBBI containing a nameid={NameID.TYPOGRAPHIC_FAMILY_NAME} entry...")

    # set bad values on all TYPOGRAPHIC_FAMILY_NAME entries:
    for i, name in enumerate(ttFont['name'].names):
        if name.nameID == NameID.TYPOGRAPHIC_FAMILY_NAME:
            ttFont['name'].names[i].string = "foo".encode(name.getEncoding())

    assert_results_contain(check(ttFont),
                           FAIL, 'non-ribbi-bad-value',
                           'with a non-RIBBI with bad nameid={NameID.TYPOGRAPHIC_FAMILY_NAME} entries...')

    # remove all TYPOGRAPHIC_FAMILY_NAME entries
    # by changing their nameid to something else:
    for i, name in enumerate(ttFont['name'].names):
        if name.nameID == NameID.TYPOGRAPHIC_FAMILY_NAME:
            ttFont['name'].names[i].nameID = 255 # blah! :-)

    assert_results_contain(check(ttFont),
                           FAIL, 'non-ribbi-lacks-entry',
                           f'with a non-RIBBI lacking a nameid={NameID.TYPOGRAPHIC_FAMILY_NAME} entry...')


def test_check_name_typographicsubfamilyname():
    """ Check name table: TYPOGRAPHIC_SUBFAMILY_NAME entries. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/name/typographicsubfamilyname")

    RIBBI = "montserrat/Montserrat-BoldItalic.ttf"
    NON_RIBBI = "montserrat/Montserrat-ExtraLight.ttf"

    # Add incorrect TYPOGRAPHIC_SUBFAMILY_NAME entries to a RIBBI font
    ttFont = TTFont(TEST_FILE(RIBBI))
    ttFont['name'].setName("FOO",
                           NameID.TYPOGRAPHIC_SUBFAMILY_NAME,
                           PlatformID.WINDOWS,
                           WindowsEncodingID.UNICODE_BMP,
                           WindowsLanguageID.ENGLISH_USA)
    ttFont['name'].setName("BAR",
                           NameID.TYPOGRAPHIC_SUBFAMILY_NAME,
                           PlatformID.MACINTOSH,
                           MacintoshEncodingID.ROMAN,
                           MacintoshLanguageID.ENGLISH)
    assert_results_contain(check(ttFont),
                           FAIL, 'mismatch',
                           f'with a RIBBI that has got incorrect'
                           f' nameid={NameID.TYPOGRAPHIC_SUBFAMILY_NAME} entries...')
    assert_results_contain(check(ttFont),
                           FAIL, 'bad-win-name')
    assert_results_contain(check(ttFont),
                           FAIL, 'bad-mac-name')

    # non-RIBBI fonts must have a TYPOGRAPHIC_SUBFAMILY_NAME entry
    ttFont = TTFont(TEST_FILE(NON_RIBBI))
    assert_PASS(check(ttFont),
                f'with a non-RIBBI containing a nameid={NameID.TYPOGRAPHIC_SUBFAMILY_NAME} entry...')

    # set bad values on the win TYPOGRAPHIC_SUBFAMILY_NAME entry:
    ttFont = TTFont(TEST_FILE(NON_RIBBI))
    ttFont['name'].setName("Generic subfamily name",
                           NameID.TYPOGRAPHIC_SUBFAMILY_NAME,
                           PlatformID.WINDOWS,
                           WindowsEncodingID.UNICODE_BMP,
                           WindowsLanguageID.ENGLISH_USA)
    assert_results_contain(check(ttFont),
                           FAIL, 'bad-typo-win',
                           f'with a non-RIBBI with bad nameid={NameID.TYPOGRAPHIC_SUBFAMILY_NAME} entries...')

    # set bad values on the mac TYPOGRAPHIC_SUBFAMILY_NAME entry:
    ttFont = TTFont(TEST_FILE(NON_RIBBI))
    ttFont['name'].setName("Generic subfamily name",
                           NameID.TYPOGRAPHIC_SUBFAMILY_NAME,
                           PlatformID.MACINTOSH,
                           MacintoshEncodingID.ROMAN,
                           MacintoshLanguageID.ENGLISH)
    assert_results_contain(check(ttFont),
                           FAIL, 'bad-typo-mac',
                           f'with a non-RIBBI with bad nameid={NameID.TYPOGRAPHIC_SUBFAMILY_NAME} entries...')


    # remove all TYPOGRAPHIC_SUBFAMILY_NAME entries
    ttFont = TTFont(TEST_FILE(NON_RIBBI))
    win_name = ttFont['name'].getName(NameID.TYPOGRAPHIC_SUBFAMILY_NAME,
                                      PlatformID.WINDOWS,
                                      WindowsEncodingID.UNICODE_BMP,
                                      WindowsLanguageID.ENGLISH_USA)
    mac_name = ttFont['name'].getName(NameID.TYPOGRAPHIC_SUBFAMILY_NAME,
                                      PlatformID.MACINTOSH,
                                      MacintoshEncodingID.ROMAN,
                                      MacintoshLanguageID.ENGLISH)
    win_name.nameID = 254
    if mac_name:
        mac_name.nameID = 255
    assert_results_contain(check(ttFont),
                           FAIL, 'missing-typo-win',
                           f'with a non-RIBBI lacking a nameid={NameID.TYPOGRAPHIC_SUBFAMILY_NAME} entry...')
                           # note: the check must not complain
                           #       about the lack of a mac entry!


def test_check_name_copyright_length():
    """ Length of copyright notice must not exceed 500 characters. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/name/copyright_length")

    ttFont = TTFont(TEST_FILE("cabin/Cabin-Regular.ttf"))

    good_entry = 'a' * 499
    for i, entry in enumerate(ttFont['name'].names):
        if entry.nameID == NameID.COPYRIGHT_NOTICE:
            ttFont['name'].names[i].string = good_entry.encode(entry.getEncoding())
    assert_PASS(check(ttFont),
                'with 499-byte copyright notice string...')

    good_entry = 'a' * 500
    for i, entry in enumerate(ttFont['name'].names):
        if entry.nameID == NameID.COPYRIGHT_NOTICE:
            ttFont['name'].names[i].string = good_entry.encode(entry.getEncoding())
    assert_PASS(check(ttFont),
                'with 500-byte copyright notice string...')

    bad_entry = 'a' * 501
    for i, entry in enumerate(ttFont['name'].names):
        if entry.nameID == NameID.COPYRIGHT_NOTICE:
            ttFont['name'].names[i].string = bad_entry.encode(entry.getEncoding())
    assert_results_contain(check(ttFont),
                           FAIL, 'too-long',
                           'with 501-byte copyright notice string...')


# TODO: Maybe skip this code-test if the service is offline?
# we could use pytest.mak.skipif here together with a piece of code that
# verifies whether or not the namecheck.fontdata.com website is online at the moment
def test_check_fontdata_namecheck():
    """ Familyname is unique according to namecheck.fontdata.com """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/fontdata_namecheck")

    TIMEOUT_MSG = ("Sometimes namecheck.fontdata.com times out"
                   " and we don't want to stop running all the other"
                   " code tests. Unless you touched this portion of"
                   " the code, it is generaly safe to ignore this glitch.")
    # We dont FAIL because this is meant as a merely informative check
    # There may be frequent cases when fonts are being updated and thus
    # already have a public family name registered on the
    # namecheck.fontdata.com database.
    font = TEST_FILE("cabin/Cabin-Regular.ttf")
    assert_results_contain(check(font),
                           INFO, 'name-collision',
                           'with an already used name...',
                           ignore_error=TIMEOUT_MSG)

    # Here we know that FamilySans has not been (and will not be)
    # registered as a real family.
    font = TEST_FILE("familysans/FamilySans-Regular.ttf")
    assert_PASS(check(font),
                'with a unique family name...',
                ignore_error=TIMEOUT_MSG)


def test_check_fontv():
    """ Check for font-v versioning """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/fontv")

    ttFont = TTFont(TEST_FILE("cabin/Cabin-Regular.ttf"))
    assert_results_contain(check(ttFont),
                           INFO, 'bad-format',
                           'with a font that does not follow'
                           ' the suggested font-v versioning scheme ...')

    from fontv.libfv import FontVersion
    fv = FontVersion(ttFont)
    fv.set_state_git_commit_sha1(development=True)
    version_string = fv.get_name_id5_version_string()
    for record in ttFont['name'].names:
        if record.nameID == NameID.VERSION_STRING:
            record.string = version_string
    assert_PASS(check(ttFont),
                'with one that follows the suggested scheme ...')


def test_check_glyf_nested_components():
    """Check glyphs do not have nested components."""
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/glyf_nested_components")

    ttFont = TTFont(TEST_FILE("nunito/Nunito-Regular.ttf"))
    assert_PASS(check(ttFont))

    # We need to create a nested component. "second" has components, so setting
    # one of "quotedbl"'s components to "second" should do it.
    ttFont['glyf']['quotedbl'].components[0].glyphName = "second"

    assert_results_contain(check(ttFont),
                           FAIL, 'found-nested-components')


# Temporarily disabling this code-test since check/negative_advance_width itself
# is disabled waiting for an implementation targetting the
# actual root cause of the issue.
#
# See also comments at googlefons.py as well as at
# https://github.com/googlefonts/fontbakery/issues/1727
def disabled_test_check_negative_advance_width():
    """ Check that advance widths cannot be inferred as negative. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/negative_advance_width")

    # Our reference Cabin Regular is good
    ttFont = TTFont(TEST_FILE("cabin/Cabin-Regular.ttf"))

    # So it must PASS
    assert_PASS(check(ttFont),
                'with a good font...')

    # We then change values in an arbitrary glyph
    # in the glyf table in order to cause the problem:
    glyphName = "J"
    coords = ttFont["glyf"].glyphs[glyphName].coordinates

# FIXME:
# Note: I thought this was the proper way to induce the
# issue, but now I think I'll need to look more
# carefully at sample files providedby MarcFoley
# to see what's really at play here and how the relevant
# data is encoded into the affected OpenType files.
    rightSideX = coords[-3][0]
    # leftSideX: (make right minus left a negative number)
    coords[-4][0] = rightSideX + 1

    ttFont["glyf"].glyphs[glyphName].coordinates = coords

    # and now this should FAIL:
    assert_results_contain(check(ttFont),
                           FAIL, 'bad-coordinates',
                           'with bad coordinates on the glyf table...')


def test_check_varfont_generate_static():
    """ Check a static ttf can be generated from a variable font. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/varfont/generate_static")

    ttFont = TTFont(TEST_FILE("cabinvfbeta/CabinVFBeta.ttf"))
    assert_PASS(check(ttFont))

    # Removing a table to deliberately break variable font
    del ttFont['fvar']
    assert_results_contain(check(ttFont),
                           FAIL, 'varlib-mutator')


def test_check_varfont_has_HVAR():
    """ Check that variable fonts have an HVAR table. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/varfont/has_HVAR")

    # Our reference Cabin Variable Font contains an HVAR table.
    ttFont = TTFont(TEST_FILE("cabinvfbeta/CabinVFBeta.ttf"))

    # So the check must PASS.
    assert_PASS(check(ttFont))

    # Introduce the problem by removing the HVAR table:
    del ttFont['HVAR']
    assert_results_contain(check(ttFont),
                           FAIL, 'lacks-HVAR')


def test_check_smart_dropout():
    """ Font enables smart dropout control in "prep" table instructions? """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/smart_dropout")

    ttFont = TTFont(TEST_FILE("nunito/Nunito-Regular.ttf"))

    # "Program at 'prep' table contains
    #  instructions enabling smart dropout control."
    assert_PASS(check(ttFont))

    # "Font does not contain TrueType instructions enabling
    #  smart dropout control in the 'prep' table program."
    import array
    ttFont["prep"].program.bytecode = array.array('B', [0])
    assert_results_contain(check(ttFont),
                           FAIL, 'lacks-smart-dropout')


def test_check_vttclean():
    """ There must not be VTT Talk sources in the font. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/vttclean")

    good_font = TEST_FILE("mada/Mada-Regular.ttf")
    assert_PASS(check(good_font))

    bad_font = TEST_FILE("hinting/Roboto-VF.ttf")
    assert_results_contain(check(bad_font),
                           FAIL, 'has-vtt-sources')


def test_check_aat():
    """ Are there unwanted Apple tables ? """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/aat")

    unwanted_tables = [
        'EBSC', 'Zaph', 'acnt', 'ankr', 'bdat', 'bhed', 'bloc',
        'bmap', 'bsln', 'fdsc', 'feat', 'fond', 'gcid', 'just',
        'kerx', 'lcar', 'ltag', 'mort', 'morx', 'opbd', 'prop',
        'trak', 'xref'
    ]
    # Our reference Mada Regular font is good here:
    ttFont = TTFont(TEST_FILE("mada/Mada-Regular.ttf"))

    # So it must PASS the check:
    assert_PASS(check(ttFont),
                'with a good font...')

    # We now add unwanted tables one-by-one to validate the FAIL code-path:
    for unwanted in unwanted_tables:
        ttFont = TTFont(TEST_FILE("mada/Mada-Regular.ttf"))
        ttFont.reader.tables[unwanted] = "foo"
        assert_results_contain(check(ttFont),
                               FAIL, 'has-unwanted-tables',
                               f'with unwanted table {unwanted} ...')


def test_check_fvar_name_entries():
    """ All name entries referenced by fvar instances exist on the name table? """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/fvar_name_entries")

    # This broken version of the Expletus variable font, was where this kind of problem was first observed:
    ttFont = TTFont(TEST_FILE("broken_expletus_vf/ExpletusSansBeta-VF.ttf"))

    # So it must FAIL the check:
    assert_results_contain(check(ttFont),
                           FAIL, 'missing-name',
                           'with a bad font...')

    # If we add the name entry with id=265 (which was the one missing)
    # then the check must now PASS:
    from fontTools.ttLib.tables._n_a_m_e import makeName
    ttFont["name"].names.append(makeName("Foo", 265, 1, 0, 0))

    assert_PASS(check(ttFont),
                'with a good font...')


def test_check_varfont_has_instances():
    """ A variable font must have named instances. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/varfont_has_instances")

    # ExpletusVF does have instances.
    # Note: The "broken" in the path name refers to something else.
    #       (See test_check_fvar_name_entries)
    ttFont = TTFont(TEST_FILE("broken_expletus_vf/ExpletusSansBeta-VF.ttf"))

    # So it must PASS the check:
    assert_PASS(check(ttFont),
                'with a good font...')

    # If we delete all instances, then it must FAIL:
    while len(ttFont["fvar"].instances):
        del ttFont["fvar"].instances[0]

    assert_results_contain(check(ttFont),
                           FAIL, 'lacks-named-instances',
                           'with a bad font...')


def test_check_varfont_weight_instances():
    """ Variable font weight coordinates must be multiples of 100. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/varfont_weight_instances")

    # This copy of Markazi Text has an instance with
    # a 491 'wght' coordinate instead of 500.
    ttFont = TTFont(TEST_FILE("broken_markazitext/MarkaziText-VF.ttf"))

    # So it must FAIL the check:
    assert_results_contain(check(ttFont),
                           FAIL, 'bad-coordinate',
                           'with a bad font...')

    # Let's then change the weight coordinates to make it PASS the check:
    for i, instance in enumerate(ttFont["fvar"].instances):
        ttFont["fvar"].instances[i].coordinates['wght'] -= instance.coordinates['wght'] % 100

    assert_PASS(check(ttFont),
                'with a good font...')


def NOT_IMPLEMENTED_test_check_family_tnum_horizontal_metrics():
    """ All tabular figures must have the same width across the RIBBI-family. """
    # check = CheckTester(googlefonts_profile,
    #                     "com.google.fonts/check/family/tnum_horizontal_metrics")
    # TODO: Implement-me!
    #
    # code-paths:
    # - FAIL, "inconsistent-widths"
    # - PASS


def test_check_integer_ppem_if_hinted():
    """ PPEM must be an integer on hinted fonts. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/integer_ppem_if_hinted")

    # Our reference Merriweather Regular is hinted, but does not set
    # the "rounded PPEM" flag (bit 3 on the head table flags) as
    # described at https://docs.microsoft.com/en-us/typography/opentype/spec/head
    ttFont = TTFont(TEST_FILE("merriweather/Merriweather-Regular.ttf"))

    # So it must FAIL the check:
    assert_results_contain(check(ttFont),
                           FAIL, 'bad-flags',
                           'with a bad font...')

    # hotfixing it should make it PASS:
    ttFont["head"].flags |= (1 << 3)

    assert_PASS(check(ttFont),
                'with a good font...')


def test_check_ligature_carets():
    """ Is there a caret position declared for every ligature? """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/ligature_carets")

    # Our reference Mada Medium is known to be bad
    ttFont = TTFont(TEST_FILE("mada/Mada-Medium.ttf"))
    assert_results_contain(check(ttFont),
                           WARN, 'lacks-caret-pos',
                           'with a bad font...')

    # And FamilySans Regular is also bad
    ttFont = TTFont("data/test/familysans/FamilySans-Regular.ttf")
    assert_results_contain(check(ttFont),
                           WARN, 'GDEF-missing',
                           'with a bad font...')

    # TODO: test the following code-paths:
    # - WARN "incomplete-caret-pos-data"
    # - FAIL "malformed"
    # - PASS (We currently lack a reference family that PASSes this check!)


def test_check_kerning_for_non_ligated_sequences():
    """ Is there kerning info for non-ligated sequences ? """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/kerning_for_non_ligated_sequences")

    # Our reference Mada Medium is known to be good
    ttFont = TTFont(TEST_FILE("mada/Mada-Medium.ttf"))
    assert_PASS(check(ttFont),
                'with a good font...')

    # And Merriweather Regular is known to be bad
    ttFont = TTFont(TEST_FILE("merriweather/Merriweather-Regular.ttf"))
    assert_results_contain(check(ttFont),
                           WARN, 'lacks-kern-info',
                           'with a bad font...')


def test_check_family_control_chars():
    """Are any unacceptable control characters present in font files?"""
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/family/control_chars")

    good_font = TEST_FILE("bad_character_set/control_chars/"
                          "FontbakeryTesterCCGood-Regular.ttf")
    onebad_cc_font = TEST_FILE("bad_character_set/control_chars/"
                               "FontbakeryTesterCCOneBad-Regular.ttf")
    multibad_cc_font = TEST_FILE("bad_character_set/control_chars/"
                                 "FontbakeryTesterCCMultiBad-Regular.ttf")

    # No unacceptable control characters should pass with one file
    fonts = [good_font]
    assert_PASS(check(fonts),
                'with one good font...')

    # No unacceptable control characters should pass with multiple good files
    fonts = [good_font,
             good_font]
    assert_PASS(check(fonts),
                'with multiple good fonts...')

    # Unacceptable control chars should fail with one file x one bad char in font
    fonts = [onebad_cc_font]
    assert_results_contain(check(fonts),
                           FAIL, 'unacceptable',
                           'with one bad font that has one bad char...')

    # Unacceptable control chars should fail with one file x multiple bad char in font
    fonts = [multibad_cc_font]
    assert_results_contain(check(fonts),
                           FAIL, 'unacceptable',
                           'with one bad font that has multiple bad char...')

    # Unacceptable control chars should fail with multiple files x multiple bad chars in fonts
    fonts = [onebad_cc_font,
             multibad_cc_font]
    assert_results_contain(check(fonts),
                           FAIL, 'unacceptable',
                           'with multiple bad fonts that have multiple bad chars...')


def test_check_family_italics_have_roman_counterparts():
    """Ensure Italic styles have Roman counterparts."""
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/family/italics_have_roman_counterparts")

    # The path used here, "some-crazy.path/", is meant to ensure
    # that the parsing code does not get lost when trying to
    # extract the style of a font file.
    fonts = ['some-crazy.path/merriweather/Merriweather-BlackItalic.ttf',
             'some-crazy.path/merriweather/Merriweather-Black.ttf',
             'some-crazy.path/merriweather/Merriweather-BoldItalic.ttf',
             'some-crazy.path/merriweather/Merriweather-Bold.ttf',
             'some-crazy.path/merriweather/Merriweather-Italic.ttf',
             'some-crazy.path/merriweather/Merriweather-LightItalic.ttf',
             'some-crazy.path/merriweather/Merriweather-Light.ttf',
             'some-crazy.path/merriweather/Merriweather-Regular.ttf']

    assert_PASS(check(fonts),
                'with a good family...')

    fonts.pop(-1) # remove the last one, which is the Regular
    assert 'some-crazy.path/merriweather/Merriweather-Regular.ttf' not in fonts
    assert 'some-crazy.path/merriweather/Merriweather-Italic.ttf' in fonts
    assert_results_contain(check(fonts),
                           FAIL, 'missing-roman',
                           'with a family that has an Italic but lacks a Regular.')

    fonts.append('some-crazy.path/merriweather/MerriweatherItalic.ttf')
    assert_results_contain(check(fonts),
                           WARN, 'bad-filename',
                           'with a family that has a non-canonical italic filename.')

    # This check must also be able to deal with variable fonts!
    fonts = ["cabinvfbeta/CabinVFBeta-Italic[wdth,wght].ttf",
             "cabinvfbeta/CabinVFBeta[wdth,wght].ttf"]
    assert_PASS(check(fonts),
                'with a good set of varfonts...')

    fonts = ["cabinvfbeta/CabinVFBeta-Italic[wdth,wght].ttf"]
    assert_results_contain(check(fonts),
                           FAIL, 'missing-roman',
                           'with an Italic varfont that lacks a Roman counterpart.')


def NOT_IMPLEMENTED__test_com_google_fonts_check_repo_dirname_match_nameid_1():
    """Are any unacceptable control characters present in font files?"""
    # check = CheckTester(googlefonts_profile,
    #                     "com.google.fonts/check/repo_dirname_match_nameid_1")
    # TODO: Implement-me!
    #
    # PASS
    # FAIL, "lacks-regular"
    # FAIL, "mismatch"
    #
    #  passing_file = TEST_FILE(".../.ttf")
    #  fonts = [passing_file]
    #  assert_PASS(check(fonts),
    #              'with one good font...')


def test_check_repo_vf_has_static_fonts():
    """Check VF family dirs in google/fonts contain static fonts"""
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/repo/vf_has_static_fonts")
    import tempfile
    import shutil
    # in order for this check to work, we need to
    # mimic the folder structure of the Google Fonts repository
    with tempfile.TemporaryDirectory() as tmp_gf_dir:
        family_dir = portable_path(tmp_gf_dir + "/ofl/testfamily")
        src_family = portable_path("data/test/varfont")
        shutil.copytree(src_family, family_dir)

        assert_results_contain(check([], {"family_directory": family_dir}),
                               WARN, 'missing',
                               'for a VF family which does not has a static dir.')

        static_dir = portable_path(family_dir + "/static")
        os.mkdir(static_dir)
        assert_results_contain(check([], {"family_directory": family_dir}),
                               FAIL, 'empty',
                               'for a VF family which has a static dir'
                               ' but no fonts in the static dir.')

        static_fonts = portable_path("data/test/cabin")
        shutil.rmtree(static_dir)
        shutil.copytree(static_fonts, static_dir)
        assert_PASS(check([], {"family_directory": family_dir}),
                    'for a VF family which has a static dir and static fonts')


def test_check_repo_upstream_yaml_has_required_fields():
    """Check upstream.yaml has all required fields"""
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/repo/upstream_yaml_has_required_fields")
    upstream_yaml = {
        "branch": "main",
        "files": {"TestFamily-Regular.ttf": "TestFamily-Regular.ttf"}
    }
    # Pass if upstream.yaml file contains all fields
    assert_PASS(check([], {"upstream_yaml": upstream_yaml}),
                'for an upstream.yaml which contains all fields')

    # Fail if it doesn't
    upstream_yaml.pop("files")
    assert_results_contain(check([], {"upstream_yaml": upstream_yaml}),
                           FAIL, "missing-fields",
                           "for an upsream.yaml which doesn't contain all fields")


def test_check_repo_fb_report():
    """ A font repository should not include fontbakery report files """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/repo/fb_report")
    import tempfile
    import shutil
    with tempfile.TemporaryDirectory() as tmp_dir:
        family_dir = portable_path(tmp_dir)
        src_family = portable_path("data/test/varfont")
        shutil.copytree(src_family, family_dir)

        assert_PASS(check([], {"family_directory": family_dir}),
                    'for a repo without Font Bakery report files.')

        assert_PASS(check([], {"family_directory": family_dir}),
                    'with a json file that is not a Font Bakery report.')

        # Add a json file that is not a FB report
        open(os.path.join(family_dir,
                          "something_else.json"), "w+").write("this is not a FB report")

        FB_REPORT_SNIPPET = """
{
    "result": {
        "INFO": 8,
        "PASS": 81,
        "SKIP": 74,
        "WARN": 4
    },
    "sections": [
    """
        # Report files must be detected even if placed on subdirectories
        # and the check code shuld not rely only on filename (such as "Jura-Regular.fb-report.json")
        # but should instead inspect the contents of the file:
        open(os.path.join(family_dir,
                          "jura",
                          "static",
                          "my_fontfamily_name.json"), "w+").write(FB_REPORT_SNIPPET)
        assert_results_contain(check([], {"family_directory": family_dir}),
                               WARN, 'fb-report',
                               'with an actual snippet of a report.')


def test_check_repo_zip_files():
    """ A font repository should not include ZIP files """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/repo/zip_files")
    import tempfile
    import shutil
    with tempfile.TemporaryDirectory() as tmp_dir:
        family_dir = portable_path(tmp_dir)
        src_family = portable_path("data/test/varfont")
        shutil.copytree(src_family, family_dir)

        assert_PASS(check([], {"family_directory": family_dir}),
                    'for a repo without ZIP files.')

        for ext in ["zip", "rar", "7z"]:
            # ZIP files must be detected even if placed on subdirectories:
            filepath = os.path.join(family_dir,
                                    f"jura",
                                    f"static",
                                    f"fonts-release.{ext}")
            #create an empty file. The check won't care about the contents:
            open(filepath, "w+")
            assert_results_contain(check([], {"family_directory": family_dir}),
                                   FAIL, 'zip-files',
                                   f"when a {ext} file is found.")
            # remove the file before testing the next one ;-)
            os.remove(filepath)


def test_check_vertical_metrics_regressions(cabin_ttFonts):
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/vertical_metrics_regressions")
    from fontbakery.profiles.shared_conditions import family_directory

    ttFonts = [TTFont(f) for f in cabin_fonts]

    # Cabin test family should match by default
    assert_PASS(check(ttFonts),
                'with a good family...')

    # FAIL with changed vertical metric values
    local_regular = check["regular_ttFont"]
    local_regular['OS/2'].sTypoAscender = 0
    assert_results_contain(check(ttFonts, {"regular_ttFont": local_regular}),
                           FAIL, 'bad-typo-ascender',
                           'with a family which has an incorrect typoAscender...')

    local_regular['OS/2'].sTypoDescender = 0
    assert_results_contain(check(ttFonts, {"regular_ttFont": local_regular}),
                           FAIL, 'bad-typo-descender',
                           'with a family which has an incorrect typoDescender...')

    local_regular['hhea'].ascent = 0
    assert_results_contain(check(ttFonts, {"regular_ttFont": local_regular}),
                           FAIL, 'bad-hhea-ascender',
                           'with a family which has an incorrect hhea ascender...')

    local_regular['hhea'].descent = 0
    assert_results_contain(check(ttFonts, {"regular_ttFont": local_regular}),
                           FAIL, 'bad-hhea-descender',
                           'with a family which has an incorrect hhea descender...')

    # Fail if family on Google Fonts has fsSelection bit 7 enabled but checked fonts don't
    local_regular["OS/2"].fsSelection &= ~(1 << 7)
    assert_results_contain(check(ttFonts, {"regular_ttFont": local_regular}),
                           FAIL, "bad-fsselection-bit7",
                           "with a remote family which has typo metrics "
                           "enabled and the fonts being checked don't.")

    if 0: # FIXME:
        # Pass if family on Google Fonts doesn't have fsSelection bit 7 enabled
        # but checked fonts have taken this into consideration
        check(ttFonts)
        remote_regular = check["regular_remote_style"]
        local_regular = check["regular_ttFont"]

        remote_regular["OS/2"].fsSelection &= ~(1 << 7)
        local_regular["OS/2"].sTypoAscender = remote_regular["OS/2"].usWinAscent
        local_regular["OS/2"].sTypoDescender = -remote_regular["OS/2"].usWinDescent
        local_regular["hhea"].ascent = remote_regular["OS/2"].usWinAscent
        local_regular["hhea"].descent = -remote_regular["OS/2"].usWinDescent
        assert_PASS(check(ttFonts, {"regular_remote_style": remote_regular,
                                    "regular_ttFont": local_regular}),
                    'with a remote family which does not have typo metrics'
                    ' enabled but the checked fonts vertical metrics have been'
                    ' set so its typo and hhea metrics match the remote'
                    ' fonts win metrics.')

    if 0: # FIXME:
        # Same as previous check but using a remote font which has a different upm
        check(ttFonts)
        remote_regular = check["regular_remote_style"]
        local_regular = check["regular_ttFont"]

        remote_regular["OS/2"].fsSelection &= ~(1 << 7)
        remote_regular["head"].unitsPerEm = 2000
        # divide by 2 since we've doubled the upm
        local_regular["OS/2"].sTypoAscender = math.ceil(remote_regular["OS/2"].usWinAscent / 2)
        local_regular["OS/2"].sTypoDescender = math.ceil(-remote_regular["OS/2"].usWinDescent / 2)
        local_regular["hhea"].ascent = math.ceil(remote_regular["OS/2"].usWinAscent / 2)
        local_regular["hhea"].descent = math.ceil(-remote_regular["OS/2"].usWinDescent / 2)
        assert_PASS(check(ttFonts, {"regular_remote_style": remote_regular,
                                    "regular_ttFont": local_regular}),
                    'with a remote family which does not have typo metrics '
                    'enabled but the checked fonts vertical metrics have been '
                    'set so its typo and hhea metrics match the remote '
                    'fonts win metrics.')


    check(ttFonts)
    remote_regular = check["regular_remote_style"]
    local_regular = check["regular_ttFont"]
    local_regular['OS/2'].fsSelection &= ~(1 << 7)
    assert_results_contain(check(ttFonts, {"regular_remote_style": remote_regular,
                                           "regular_ttFont": local_regular}),
                           FAIL, "bad-fsselection-bit7",
                           'OS/2 fsSelection bit 7 must be enabled.')


    # Disable bit 7 in both fonts but change win metrics of ttFont
    check(ttFonts)
    remote_regular = check["regular_remote_style"]
    local_regular = check["regular_ttFont"]

    remote_regular["OS/2"].fsSelection &= ~(1 << 7)
    local_regular["OS/2"].fsSelection &= ~(1 << 7)
    local_regular["OS/2"].usWinAscent = 2500
    assert_results_contain(check(ttFonts, {"regular_remote_style": remote_regular,
                                           "regular_ttFont": local_regular}),
                           FAIL, "bad-fsselection-bit7",
                           'OS/2 fsSelection bit 7 must be enabled.')


def test_check_cjk_vertical_metrics():
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/cjk_vertical_metrics")

    ttFont = TTFont(cjk_font)
    assert_PASS(check(ttFont),
                'for Source Han Sans')

    ttFont = TTFont(cjk_font)
    ttFont['OS/2'].fsSelection |= (1 << 7)
    assert_results_contain(check(ttFont),
                           FAIL, 'bad-fselection-bit7',
                           'for font where OS/2 fsSelection bit 7 is enabled')

    ttFont = TTFont(cjk_font)
    ttFont['OS/2'].sTypoAscender = float('inf')
    assert_results_contain(check(ttFont),
                           FAIL, 'bad-OS/2.sTypoAscender',
                           'for font with bad OS/2.sTypoAscender')

    ttFont = TTFont(cjk_font)
    ttFont['OS/2'].sTypoDescender = float('inf')
    assert_results_contain(check(ttFont),
                           FAIL, 'bad-OS/2.sTypoDescender',
                           'for font with bad OS/2.sTypoDescender')

    ttFont = TTFont(cjk_font)
    ttFont['OS/2'].sTypoLineGap = float('inf')
    assert_results_contain(check(ttFont),
                           FAIL, 'bad-OS/2.sTypoLineGap',
                           'for font where linegaps have been set (OS/2 table)')

    ttFont = TTFont(cjk_font)
    ttFont['hhea'].lineGap = float('inf')
    assert_results_contain(check(ttFont),
                           FAIL, 'bad-hhea.lineGap',
                           'for font where linegaps have been set (hhea table)')

    ttFont = TTFont(cjk_font)
    ttFont['OS/2'].usWinAscent = float('inf')
    assert_results_contain(check(ttFont),
                           FAIL, 'ascent-mismatch',
                           'for a font where typo ascender != 0.88 * upm')

    ttFont = TTFont(cjk_font)
    ttFont['OS/2'].usWinDescent = -float('inf')
    assert_results_contain(check(ttFont),
                           FAIL, 'descent-mismatch',
                           'for a font where typo descender != 0.12 * upm')

    ttFont = TTFont(cjk_font)
    ttFont['OS/2'].usWinAscent = float('inf')
    ttFont['hhea'].ascent = float('inf')
    assert_results_contain(check(ttFont),
                           WARN, 'bad-hhea-range',
                           'if font hhea and win metrics are greater than 1.5 * upm')


def test_check_cjk_vertical_metrics_regressions():
    # TODO: try to remove deepcopy usage
    from copy import deepcopy

    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/cjk_vertical_metrics_regressions")

    ttFont = TTFont(cjk_font)
    regular_remote_style = deepcopy(ttFont)

    # Check on duplicate
    regular_remote_style = deepcopy(ttFont)
    assert_PASS(check(ttFont, {"regular_remote_style": regular_remote_style}),
                'for Source Han Sans')

    # Change a single metric
    ttFont2 = deepcopy(ttFont)
    ttFont2['hhea'].ascent = 0
    assert_results_contain(check(ttFont2, {"regular_remote_style": regular_remote_style}),
                           FAIL, "cjk-metric-regression",
                           'hhea ascent is 0 when it should be 880')

    # Change upm of font being checked
    ttFont3 = deepcopy(ttFont)
    ttFont3['head'].unitsPerEm = 2000
    assert_results_contain(check(ttFont3, {"regular_remote_style": regular_remote_style}),
                           FAIL, "cjk-metric-regression",
                           'upm is 2000 and vert metrics values are not updated')

    # Change upm of checked font and update vert metrics
    ttFont4 = deepcopy(ttFont)
    ttFont4['head'].unitsPerEm = 2000
    for tbl, attrib in [("OS/2", "sTypoAscender"),
                        ("OS/2", "sTypoDescender"),
                        ("OS/2", "sTypoLineGap"),
                        ("OS/2", "usWinAscent"),
                        ("OS/2", "usWinDescent"),
                        ("hhea", "ascent"),
                        ("hhea", "descent"),
                        ("hhea", "lineGap")]:
        current_val = getattr(ttFont4[tbl], attrib)
        setattr(ttFont4[tbl], attrib, current_val * 2)
    assert_PASS(check(ttFont4, {"regular_remote_style": regular_remote_style}),
                'for Source Han Sans with doubled upm and doubled vert metrics')


def test_check_cjk_not_enough_glyphs():
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/cjk_not_enough_glyphs")
    ttFont = TTFont(cjk_font)
    assert_PASS(check(ttFont),
                'for Source Han Sans')

    ttFont = TTFont(TEST_FILE("montserrat/Montserrat-Regular.ttf"))
    assert_PASS(check(ttFont),
                'for Montserrat')

    # Let's modify Montserrat's cmap so there's a cjk glyph
    cmap = ttFont['cmap'].getcmap(3,1)
    # Add first character of the CJK unified Ideographs
    cmap.cmap[0x4E00] = "A"
    assert_results_contain(check(ttFont),
                           WARN, "cjk-not-enough-glyphs",
                           "There is only 1 CJK glyphs")
    # Add second character of the CJK unified Ideographs
    cmap.cmap[0x4E01] = "B"
    assert_results_contain(check(ttFont),
                           WARN, "cjk-not-enough-glyphs",
                           "There are only 2 CJK glyphs")


def test_check_varfont_instance_coordinates(vf_ttFont):
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/varfont_instance_coordinates")

    # OpenSans-Roman-VF is correct
    assert_PASS(check(vf_ttFont),
                'with a variable font which has correct instance coordinates.')

    from copy import copy
    vf_ttFont2 = copy(vf_ttFont)
    for instance in vf_ttFont2['fvar'].instances:
        for axis in instance.coordinates.keys():
            instance.coordinates[axis] = 0
    assert_results_contain(check(vf_ttFont2),
                           FAIL, "bad-coordinate",
                           'with a variable font which does not have'
                           ' correct instance coordinates.')


def test_check_varfont_instance_names(vf_ttFont):
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/varfont_instance_names")

    assert_PASS(check(vf_ttFont),
                'with a variable font which has correct instance names.')

    from copy import copy
    vf_ttFont2 = copy(vf_ttFont)
    for instance in vf_ttFont2['fvar'].instances:
        instance.subfamilyNameID = 300
    broken_name ="ExtraBlack Condensed 300pt"
    vf_ttFont2['name'].setName(broken_name,
                               300,
                               PlatformID.MACINTOSH,
                               MacintoshEncodingID.ROMAN,
                               MacintoshLanguageID.ENGLISH)
    vf_ttFont2['name'].setName(broken_name,
                               300,
                               PlatformID.WINDOWS,
                               WindowsEncodingID.UNICODE_BMP,
                               WindowsLanguageID.ENGLISH_USA)
    assert_results_contain(check(vf_ttFont2),
                           FAIL, 'bad-instance-names',
                           'with a variable font which does not have correct instance names.')


def test_check_varfont_duplicate_instance_names(vf_ttFont):
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/varfont_duplicate_instance_names")

    assert_PASS(check(vf_ttFont),
                'with a variable font which has unique instance names.')

    from copy import copy
    vf_ttFont2 = copy(vf_ttFont)
    duplicate_instance_name = vf_ttFont2['name'].getName(
        vf_ttFont2['fvar'].instances[0].subfamilyNameID,
        PlatformID.WINDOWS,
        WindowsEncodingID.UNICODE_BMP,
        WindowsLanguageID.ENGLISH_USA
    ).toUnicode()
    vf_ttFont2['name'].setName(string=duplicate_instance_name,
                               nameID=vf_ttFont2['fvar'].instances[1].subfamilyNameID,
                               platformID=PlatformID.WINDOWS,
                               platEncID=WindowsEncodingID.UNICODE_BMP,
                               langID=WindowsLanguageID.ENGLISH_USA)
    assert_results_contain(check(vf_ttFont2),
                           FAIL, 'duplicate-instance-names')


def test_check_varfont_unsupported_axes():
    """Ensure VFs do not contain opsz or ital axes."""
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/varfont/unsupported_axes")

    # Our reference varfont, CabinVFBeta.ttf, lacks 'ital' and 'slnt' variation axes.
    # So, should pass the check:
    ttFont = TTFont(TEST_FILE("cabinvfbeta/CabinVFBeta.ttf"))
    assert_PASS(check(ttFont))

    # If we add 'ital' it must FAIL:
    from fontTools.ttLib.tables._f_v_a_r import Axis
    new_axis = Axis()
    new_axis.axisTag = "ital"
    ttFont["fvar"].axes.append(new_axis)
    assert_results_contain(check(ttFont),
                           FAIL, 'unsupported-ital')

    # Then we reload the font and add 'opsz'
    # so it must also FAIL:
    ttFont = TTFont("data/test/cabinvfbeta/CabinVFBeta.ttf")
    new_axis = Axis()
    new_axis.axisTag = "slnt"
    ttFont["fvar"].axes.append(new_axis)
    assert_results_contain(check(ttFont),
                           FAIL, 'unsupported-slnt')


def test_check_varfont_grade_reflow():
    """ Ensure VFs with the GRAD axis do not vary horizontal advance. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/varfont/grade_reflow")

    ttFont = TTFont(TEST_FILE("BadGrades/BadGrades-VF.ttf"))
    assert_results_contain(check(ttFont),
                           FAIL, 'grad-causes-reflow')

    # Zero out the horizontal advances
    gvar = ttFont["gvar"]
    for glyph, deltas in gvar.variations.items():
        for delta in deltas:
            if "GRAD" not in delta.axes:
                continue
            if delta.coordinates:
                delta.coordinates = delta.coordinates[:-4] + [(0,0)] * 4

    # But the kern rules should still be a problem
    assert_results_contain(check(ttFont),
                           FAIL, 'grad-kern-causes-reflow')

    ttFont["GPOS"].table.LookupList.Lookup = []
    assert_PASS(check(ttFont))


def test_check_gfaxisregistry_bounds():
    """Validate METADATA.pb axes values are within gf-axisregistry bounds."""
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/gf-axisregistry_bounds")

    # Our reference varfont, CabinVF, has good axes bounds:
    ttFont = TTFont(TEST_FILE("cabinvf/Cabin[wdth,wght].ttf"))
    assert_PASS(check(ttFont))

    # The first axis declared in this family is 'wdth' (Width)
    # And the GF Axis Registry expects this axis to have a range
    # not broader than min: 25 / max: 200
    # So...
    md = check["family_metadata"]
    md.axes[0].min_value = 20
    assert_results_contain(check(ttFont, {"family_metadata": md}),
                           FAIL, "bad-axis-range")

    md.axes[0].min_value = 25
    md.axes[0].max_value = 250
    assert_results_contain(check(ttFont, {"family_metadata": md}),
                           FAIL, "bad-axis-range")


def test_check_gf_axisregistry_valid_tags():
    """Validate METADATA.pb axes tags are defined in gf-axisregistry."""
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/gf-axisregistry_valid_tags")

    # The axis tags in our reference varfont, CabinVF,
    # are properly defined in the registry:
    ttFont = TTFont(TEST_FILE("cabinvf/Cabin[wdth,wght].ttf"))
    assert_PASS(check(ttFont))

    md = check["family_metadata"]
    md.axes[0].tag = "crap" # I'm pretty sure this one wont ever be included in the registry
    assert_results_contain(check(ttFont, {"family_metadata": md}),
                           FAIL, "bad-axis-tag")


def test_check_gf_axisregistry_fvar_axis_defaults():
    """Validate METADATA.pb axes tags are defined in gf-axisregistry."""
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/gf-axisregistry/fvar_axis_defaults")

    # The default value for the axes in this reference varfont
    # are properly registered in the registry:
    ttFont = TTFont(TEST_FILE("cabinvf/Cabin[wdth,wght].ttf"))
    assert_PASS(check(ttFont))

    # And this value surely doen't map to a fallback name in the registry
    ttFont['fvar'].axes[0].defaultValue = 123
    assert_results_contain(check(ttFont),
                           FAIL, "not-registered")


def test_check_STAT_gf_axisregistry():
    """Validate STAT particle names and values match the fallback names in GFAxisRegistry."""
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/STAT/gf-axisregistry")

    # Our reference varfont, CabinVF,
    # has "Regular", instead of "Roman" in its 'ital' axis on the STAT table:
    ttFont = TTFont(TEST_FILE("cabinvf/Cabin[wdth,wght].ttf"))
    assert_results_contain(check(ttFont),
                           FAIL, "invalid-name")

    # LibreCaslonText is good though:
    ttFont = TTFont(TEST_FILE("librecaslontext/LibreCaslonText[wght].ttf"))
    assert_PASS(check(ttFont))

    # Let's break it by setting an invalid coordinate for "Bold":
    assert ttFont['STAT'].table.AxisValueArray.AxisValue[3].ValueNameID == ttFont['name'].names[4].nameID
    assert ttFont['name'].names[4].toUnicode() == "Bold"
    ttFont['STAT'].table.AxisValueArray.AxisValue[3].Value = 800 # instead of the expected 700
    # Note: I know it is AxisValue[3] and names[4] because I inspected the font using ttx.
    assert_results_contain(check(ttFont),
                           FAIL, "bad-coordinate")

    # Let's remove all Axis Values. This will fail since we Google Fonts
    # requires them.
    ttFont['STAT'].table.AxisValueArray = None
    assert_results_contain(check(ttFont),
                           FAIL, "missing-axis-values")


def test_check_metadata_consistent_axis_enumeration():
    """Validate VF axes match the ones declared on METADATA.pb."""
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/consistent_axis_enumeration")

    # The axis tags of CabinVF,
    # are properly declared on its METADATA.pb:
    ttFont = TTFont(TEST_FILE("cabinvf/Cabin[wdth,wght].ttf"))
    assert_PASS(check(ttFont))

    md = check["family_metadata"]
    md.axes[1].tag = "wdth" # this effectively removes the "wght" axis while not adding an extra one
    assert_results_contain(check(ttFont, {"family_metadata": md}),
                           FAIL, "missing-axes")

    md.axes[1].tag = "ouch" # and this is an unwanted extra axis
    assert_results_contain(check(ttFont, {"family_metadata": md}),
                           FAIL, "extra-axes")


def test_check_STAT_axis_order():
    """Check axis ordering on the STAT table."""
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/STAT/axis_order")

    fonts = [TEST_FILE("cabinvf/Cabin[wdth,wght].ttf")]
    assert_results_contain(check(fonts),
                           INFO, "summary")

    fonts = [TEST_FILE("merriweather/Merriweather-Regular.ttf")]
    assert_results_contain(check(fonts),
                           SKIP, "missing-STAT")

    # A real-world case here would be a corrupted TTF file.
    # This clearly is not a TTF, but is good enough for testing:
    fonts = [TEST_FILE("merriweather/METADATA.pb")]
    assert_results_contain(check(fonts),
                           INFO, "bad-font")


def test_check_metadata_escaped_strings():
    """Ensure METADATA.pb does not use escaped strings."""
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/escaped_strings")

    good = TEST_FILE("issue_2932/good/SomeFont-Regular.ttf")
    assert_PASS(check(good))

    bad = TEST_FILE("issue_2932/bad/SomeFont-Regular.ttf")
    assert_results_contain(check(bad),
                           FAIL, "escaped-strings")


def test_check_metadata_designer_profiles():
    """METADATA.pb: Designer is listed with the correct name on
       the Google Fonts catalog of designers?"""
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/designer_profiles")

    # Delve Withrington is still not listed on the designers catalog.
    # Note: Once it is listed, this code-test will start failing and will need to be updated.
    font = TEST_FILE("overpassmono/OverpassMono-Regular.ttf")
    assert_results_contain(check(font),
                           WARN, "profile-not-found")

    # Cousine lists designers: "Multiple Designers"
    font = TEST_FILE("cousine/Cousine-Regular.ttf")
    assert_results_contain(check(font),
                           FAIL, "multiple-designers")

    # This reference Merriweather font family lists "Sorkin Type" in its METADATA.pb file.
    # And this foundry has a good profile on the catalog.
    font = TEST_FILE("merriweather/Merriweather-Regular.ttf")
    assert_PASS(check(font))

    # TODO: FAIL, "mismatch"
    # TODO: FAIL, "link-field"
    # TODO: FAIL, "missing-avatar"
    # TODO: FAIL, "bad-avatar-filename"


def test_check_mandatory_avar_table():
    """Ensure variable fonts include an avar table."""
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/mandatory_avar_table")

    ttFont = TTFont(TEST_FILE("ibmplexsans-vf/IBMPlexSansVar-Roman.ttf"))
    assert_PASS(check(ttFont))

    del ttFont["avar"]
    assert_results_contain(check(ttFont),
                           FAIL, "missing-avar")


def test_check_description_family_update():
    """On a family update, the DESCRIPTION.en_us.html file should ideally also be updated."""
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/description/family_update")

    font = TEST_FILE("abeezee/ABeeZee-Regular.ttf")
    ABEEZEE_DESC = ('https://raw.githubusercontent.com/google/fonts/'
                    'main/ofl/abeezee/DESCRIPTION.en_us.html')
    import requests
    desc = requests.get(ABEEZEE_DESC).text
    assert_results_contain(check(font, {'description': desc}),
                           WARN, "description-not-updated")

    assert_PASS(check(font, {'description': desc + '\nSomething else...'}))


def test_check_os2_use_typo_metrics():
    """All non-CJK fonts checked with the googlefonts profile
    should have OS/2.fsSelection bit 7 (USE TYPO METRICS) set."""
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/os2/use_typo_metrics")

    ttFont = TTFont(TEST_FILE("abeezee/ABeeZee-Regular.ttf"))
    fsel = ttFont["OS/2"].fsSelection

    # set bit 7
    ttFont["OS/2"].fsSelection = fsel | (1 << 7)
    assert_PASS(check(ttFont))

    # clear bit 7
    ttFont["OS/2"].fsSelection = fsel & ~(1 << 7)
    assert_results_contain(check(ttFont),
                           FAIL, 'missing-os2-fsselection-bit7')


# TODO: If I recall correctly, there was something wrong with
#       code-tests that try to ensure a check skips.
#       I will have to review this one at some point to verify
#       if that's the reason for this test not working properly.
#
#              -- Felipe Sanches (May 31, 2021)
def TODO_test_check_os2_use_typo_metrics_with_cjk():
    """All CJK fonts checked with the googlefonts profile should skip this check"""
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/os2/use_typo_metrics")

    tt_pass_clear = TTFont(TEST_FILE("cjk/SourceHanSans-Regular.otf"))
    tt_pass_set = TTFont(TEST_FILE("cjk/SourceHanSans-Regular.otf"))

    fs_selection = 0

    # test skip with font that contains cleared bit
    tt_pass_clear["OS/2"].fsSelection = fs_selection
    # test skip with font that contains set bit
    tt_pass_set["OS/2"].fsSelection = fs_selection | (1 << 7)

    assert_SKIP(check(tt_pass_clear))
    assert_SKIP(check(tt_pass_set))


def test_check_missing_small_caps_glyphs():
    """Check small caps glyphs are available."""
    #check = CheckTester(googlefonts_profile,
    #                    "com.google.fonts/check/missing_small_caps_glyphs")
    # TODO: Implement-me!


def test_check_stylisticset_description():
    """Ensure Stylistic Sets have description."""
    #check = CheckTester(googlefonts_profile,
    #                    "com.google.fonts/check/stylisticset_description")
    # TODO: Implement-me!


def test_check_meta_script_lang_tags():
    """Ensure font has ScriptLangTags in the 'meta' table."""
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/meta/script_lang_tags")

    # This sample font from the Noto project declares
    # the script/lang tags in the meta table correctly:
    ttFont = TTFont(TEST_FILE("meta_tag/NotoSansPhagsPa-Regular-with-meta.ttf"))
    assert_results_contain(check(ttFont), INFO, 'dlng-tag')
    assert_results_contain(check(ttFont), INFO, 'slng-tag')

    del ttFont["meta"].data['dlng']
    assert_results_contain(check(ttFont),
                           FAIL, 'missing-dlng-tag')

    del ttFont["meta"].data['slng']
    assert_results_contain(check(ttFont),
                           FAIL, 'missing-slng-tag')

    del ttFont["meta"]
    assert_results_contain(check(ttFont),
                           WARN, 'lacks-meta-table')


def test_check_no_debugging_tables():
    """Ensure fonts do not contain any preproduction tables."""
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/no_debugging_tables")

    ttFont = TTFont(TEST_FILE("overpassmono/OverpassMono-Regular.ttf"))
    assert_results_contain(check(ttFont),
                           WARN, 'has-debugging-tables')

    del ttFont["FFTM"]
    assert_PASS(check(ttFont))


def test_check_metadata_family_directory_name():
    """Check family directory name."""
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/family_directory_name")

    ttFont = TEST_FILE("overpassmono/OverpassMono-Regular.ttf")
    assert_PASS(check(ttFont))

    # Note:
    # Here I explicitly pass 'family_metadata' to avoid it being recomputed
    # after I make the family_directory wrong:
    assert_results_contain(check(ttFont, {'family_metadata': check['family_metadata'],
                                          'family_directory': 'overpass'}),
                           FAIL, 'bad-directory-name')


def test_check_render_own_name():
    """Check family directory name."""
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/render_own_name")

    ttFont = TEST_FILE("overpassmono/OverpassMono-Regular.ttf")
    assert_PASS(check(ttFont))

    ttFont = TEST_FILE("noto_sans_tamil_supplement/NotoSansTamilSupplement-Regular.ttf")
    assert_results_contain(check(ttFont),
                           FAIL, 'render-own-name')


def test_check_repo_sample_image():
    """Check README.md has a sample image."""
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/repo/sample_image")

    # That's what we'd like to see:
    # README.md including a sample image and highlighting it in the
    # upper portion of the document (no more than 10 lines from the top).
    readme = TEST_FILE("issue_2898/good/README.md")
    assert_PASS(check(readme))

    # This one is still good, but places the sample image too late in the page:
    readme = TEST_FILE("issue_2898/not-ideal-placement/README.md")
    assert_results_contain(check(readme),
                           WARN, 'not-ideal-placement')

    # Here's a README.md in a project completely lacking such sample image.
    # This will likely become a FAIL in the future:
    readme = TEST_FILE("issue_2898/no-sample/README.md")
    assert_results_contain(check(readme),
                           WARN, 'no-sample') # FIXME: Make this a FAIL!

    # This is really broken, as it references an image that is not available:
    readme = TEST_FILE("issue_2898/image-missing/README.md")
    assert_results_contain(check(readme),
                           FAIL, 'image-missing')

    # An here a README.md that does not include any sample image,
    # while an image file can be found within the project's directory tree.
    # This image could potentially be a font sample, so we let the user know
    # that it might be the case:
    readme = TEST_FILE("issue_2898/image-not-displayed/README.md")
    assert_results_contain(check(readme),
                           WARN, 'image-not-displayed')


def test_check_metadata_can_render_samples():
    """Check README.md has a sample image."""
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/can_render_samples")

    # Cabin's METADATA.pb does not have sample_glyphs entry
    metadata_file = TEST_FILE("cabin/METADATA.pb")
    assert_results_contain(check(metadata_file),
                           INFO, 'no-samples')

    # We add a small set of latin glyphs
    # that we're sure Cabin supports:
    md = check["family_metadata"]
    md.sample_glyphs["Letters"] = "abcdefghijklmnopqrstuvyz0123456789"
    assert_PASS(check(metadata_file, {"family_metadata": md}))

    # And now with Tamil glyphs that Cabin lacks:
    md.sample_glyphs["Numbers"] = "𑿀 𑿁 𑿂 𑿃 𑿄 𑿅 𑿆 𑿇 𑿈 𑿉 𑿊 𑿋 𑿌 𑿍 𑿎 𑿏 𑿐 𑿑 𑿒 𑿓 𑿔"
    assert_results_contain(check(metadata_file, {"family_metadata": md}),
                           FAIL, 'sample-glyphs')

    # TODO: expand the check to also validate sample_text fields


def test_check_description_urls():
    """URLs on DESCRIPTION file must not display http(s) prefix."""
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/description/urls")

    font = TEST_FILE("librecaslontext/LibreCaslonText[wght].ttf")
    assert_PASS(check(font))

    font = TEST_FILE("cabinvfbeta/CabinVFBeta.ttf")
    assert_results_contain(check(font),
                           WARN, 'prefix-found')

    good_desc = check["description"].replace(">https://", ">")
    assert_PASS(check(font, {"description": good_desc}))


def test_check_metadata_unsupported_subsets():
    """Check for METADATA subsets with zero support."""
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/unsupported_subsets")

    font = TEST_FILE("librecaslontext/LibreCaslonText[wght].ttf")
    assert_PASS(check(font))

    md = check["family_metadata"]
    md.subsets.extend(["foo"])
    assert_results_contain(check(font, {"family_metadata": md}),
                           FAIL, 'unknown-subset')

    del md.subsets[:]
    md.subsets.extend(["cyrillic"])
    assert_results_contain(check(font, {"family_metadata": md}),
                           FAIL, 'unsupported-subset')


def test_check_metadata_category_hints():
    """ Check if category on METADATA.pb matches what can be inferred from the family name. """
    check = CheckTester(googlefonts_profile,
                        "com.google.fonts/check/metadata/category_hints")

    font = TEST_FILE("cabin/Cabin-Regular.ttf")
    assert_PASS(check(font),
                "with a familyname without any of the keyword hints...")

    md = check["family_metadata"]
    md.name = "Seaweed Script"
    md.category = "DISPLAY"
    assert_results_contain(check(font, {"family_metadata": md}),
                           WARN, 'inferred-category',
                           f'with a bad category "{md.category}" for familyname "{md.name}"...')

    md.name = "Red Hat Display"
    md.category = "SANS_SERIF"
    assert_results_contain(check(font, {"family_metadata": md}),
                           WARN, 'inferred-category',
                           f'with a bad category "{md.category}" for familyname "{md.name}"...')

    md.name = "Seaweed Script"
    md.category = "HANDWRITING"
    assert_PASS(check(font, {"family_metadata": md}),
                f'with a good category "{md.category}" for familyname "{md.name}"...')

    md.name = "Red Hat Display"
    md.category = "DISPLAY"
    assert_PASS(check(font, {"family_metadata": md}),
                f'with a good category "{md.category}" for familyname "{md.name}"...')


# tests/profiles/gpos_test.py

from fontTools.ttLib import TTFont

from fontbakery.codetesting import (TEST_FILE,
                                    assert_PASS,
                                    assert_results_contain,
                                    CheckTester)
from fontbakery.checkrunner import WARN
from fontbakery.profiles import opentype as opentype_profile


def test_check_gpos_kerning_info():
    """ Does GPOS table have kerning information? """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/gpos_kerning_info")

    # Our reference Mada Regular is known to have kerning-info
    # exclusively on an extension subtable
    # (lookup type = 9 / ext-type = 2):
    ttFont = TTFont(TEST_FILE("mada/Mada-Regular.ttf"))

    # So it must PASS the check:
    assert_PASS(check(ttFont),
                'with a font that has got kerning info...')

    # delete all Pair Adjustment lookups:
    while True:
        found = False
        for l, lookup in enumerate(ttFont["GPOS"].table.LookupList.Lookup):
            #if lookup.LookupType == 2:  # type 2 = Pair Adjustment
            #  del ttFont["GPOS"].table.LookupList.Lookup[l]
            #  found = True
            if lookup.LookupType == 9:  # type 9 = Extension subtable
                for e, ext in enumerate(lookup.SubTable):
                    if ext.ExtensionLookupType == 2:  # type 2 = Pair Adjustment
                        del ttFont["GPOS"].table.LookupList.Lookup[l].SubTable[e]
                        found = True
        if not found:
            break

    assert_results_contain(check(ttFont),
                           WARN, 'lacks-kern-info',
                           'with a font lacking kerning info...')

    # setup a fake type=2 Pair Adjustment lookup
    ttFont["GPOS"].table.LookupList.Lookup[0].LookupType = 2
    # and make sure the check emits a PASS result:
    assert_PASS(check(ttFont),
                'with kerning info on a type=2 lookup...')

    # remove the GPOS table and make sure to get a WARN:
    del ttFont["GPOS"]
    assert_results_contain(check(ttFont),
                           WARN, 'lacks-kern-info',
                           'with a font lacking a GPOS table...')



def test_check_gpos_kerning_info_monospaced_font():
    """ Does GPOS table have kerning information in a monospaced font? """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/gpos_kerning_info")
    # Even though our reference Overpass Mono lacks kerning info
    # it is a monospaced font and this is expected:
    font = TEST_FILE("overpassmono/OverpassMono-Regular-post-edit.subset.ttf")
    assert_PASS(check(font),
                "with a font that is a monospaced font without kerning info...")


# tests/profiles/head_test.py

import io
import os

import pytest
from fontTools.ttLib import TTFont

from fontbakery.checkrunner import (WARN, FAIL)
from fontbakery.codetesting import (assert_PASS,
                                    assert_results_contain,
                                    CheckTester,
                                    TEST_FILE)
from fontbakery.profiles import opentype as opentype_profile


mada_fonts = [
    TEST_FILE("mada/Mada-Black.ttf"),
    TEST_FILE("mada/Mada-ExtraLight.ttf"),
    TEST_FILE("mada/Mada-Medium.ttf"),
    TEST_FILE("mada/Mada-SemiBold.ttf"),
    TEST_FILE("mada/Mada-Bold.ttf"),
    TEST_FILE("mada/Mada-Light.ttf"),
    TEST_FILE("mada/Mada-Regular.ttf")
]

@pytest.fixture
def mada_ttFonts():
    return [TTFont(path) for path in mada_fonts]


def test_check_family_equal_font_versions(mada_ttFonts):
    """ Make sure all font files have the same version value. """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/family/equal_font_versions")

    # our reference Mada family is know to be good here.
    assert_PASS(check(mada_ttFonts),
                'with good family.')

    bad_ttFonts = mada_ttFonts
    # introduce a mismatching version value into the second font file:
    version = bad_ttFonts[0]['head'].fontRevision
    bad_ttFonts[1]['head'].fontRevision = version + 1

    assert_results_contain(check(bad_ttFonts),
                           WARN, 'mismatch',
                           'with fonts that diverge on the fontRevision field value.')


def test_check_unitsperem():
    """ Checking unitsPerEm value is reasonable. """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/unitsperem")

    # In this test we'll forge several known-good and known-bad values.
    # We'll use Mada Regular to start with:
    ttFont = TTFont(TEST_FILE("mada/Mada-Regular.ttf"))

    for good_value in [16, 32, 64, 128, 256, 512, 1000,
                       1024, 2000, 2048, 4096, 8192, 16384]:
        ttFont['head'].unitsPerEm = good_value
        assert_PASS(check(ttFont),
                    f'with a good value of unitsPerEm = {good_value} ...')

    for warn_value in [20, 50, 100, 500, 4000]:
        ttFont['head'].unitsPerEm = warn_value
        assert_results_contain(check(ttFont),
                               WARN, 'suboptimal',
                               f'with a value of unitsPerEm = {warn_value} ...')

    # These are arbitrarily chosen bad values:
    for bad_value in [0, 1, 2, 4, 8, 10, 15, 16385, 32768]:
        ttFont['head'].unitsPerEm = bad_value
        assert_results_contain(check(ttFont),
                               FAIL, 'out-of-range',
                               f'with a bad value of unitsPerEm = {bad_value} ...')


def test_parse_version_string():
    """ Checking font version fields. """
    from fontbakery.profiles.head import parse_version_string
    import fractions

    version_tests_good = {
        "Version 01.234": fractions.Fraction("1.234"),
        "1.234": fractions.Fraction("1.234"),
        "01.234; afjidfkdf 5.678": fractions.Fraction("1.234"),
        "1.3": fractions.Fraction("1.300"),
        "1.003": fractions.Fraction("1.003"),
        "1.0": fractions.Fraction(1),
        "1.000": fractions.Fraction(1),
        "3.000;NeWT;Nunito-Regular": fractions.Fraction("3"),
        "Something Regular Italic Version 1.234": fractions.Fraction("1.234")
    }

    version_tests_bad = ["Version 0x.234",
                         "x",
                         "212122;asdf 01.234"]

    for string, version in version_tests_good.items():
        assert parse_version_string(string) == version

    for string in version_tests_bad:
        with pytest.raises(ValueError):
            parse_version_string(string)


def test_check_font_version():
    """ Checking font version fields. """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/font_version")

    test_font_path = TEST_FILE("nunito/Nunito-Regular.ttf")
    test_font = TTFont(test_font_path)
    assert_PASS(check(test_font))

    # 1.00099 is only a mis-interpretation of a valid float value (1.001)
    # See more detailed discussion at:
    # https://github.com/googlefonts/fontbakery/issues/2006
    test_font = TTFont(test_font_path)
    test_font["head"].fontRevision = 1.00098
    test_font["name"].setName("Version 1.001", 5, 1, 0, 0x0)
    test_font["name"].setName("Version 1.001", 5, 3, 1, 0x409)

    # There should be at least one WARN...
    assert_results_contain(check(test_font),
                           WARN, "near-mismatch")

    # But final result is a PASS:
    assert_PASS(check(test_font))

    # Test that having more than 3 decimal places in the version
    # in the Name table is acceptable.
    # See https://github.com/googlefonts/fontbakery/issues/2928
    test_font = TTFont(test_font_path)
    # This is the nearest multiple of 1/65536 to 2020.0613
    test_font["head"].fontRevision = 2020.061294555664
    test_font["name"].setName("Version 2020.0613", 5, 1, 0, 0x0)
    test_font["name"].setName("Version 2020.0613", 5, 3, 1, 0x409)
    assert_PASS(check(test_font))

    test_font = TTFont(test_font_path)
    test_font["head"].fontRevision = 3.1
    test_font["name"].setName("Version 3.000", 5, 1, 0, 0x0)
    test_font["name"].setName("Version 3.000", 5, 3, 1, 0x409)
    assert_results_contain(check(test_font),
                           FAIL, 'mismatch')

    test_font = TTFont(test_font_path)
    test_font["head"].fontRevision = 3.0
    test_font["name"].setName("Version 1.000", 5, 3, 1, 0x409)
    assert_results_contain(check(test_font),
                           FAIL, 'mismatch')

    test_font = TTFont(test_font_path)
    test_font["name"].setName("Version x.000", 5, 3, 1, 0x409)
    assert_results_contain(check(test_font),
                           FAIL, 'parse')

    test_font = TTFont(test_font_path)
    v1 = test_font["name"].getName(5, 3, 1)
    v2 = test_font["name"].getName(5, 1, 0)
    test_font["name"].names.remove(v1)
    test_font["name"].names.remove(v2)
    assert_results_contain(check(test_font),
                           FAIL, 'missing')



# tests/profiles/hhea_test.py

from fontTools.ttLib import TTFont

from fontbakery.checkrunner import WARN, FAIL
from fontbakery.codetesting import (assert_PASS,
                                    assert_results_contain,
                                    CheckTester,
                                    TEST_FILE)
from fontbakery.profiles import opentype as opentype_profile


def test_check_linegaps():
    """ Checking Vertical Metric Linegaps. """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/linegaps")

    # Our reference Mada Regular is know to be bad here.
    ttFont = TTFont(TEST_FILE("mada/Mada-Regular.ttf"))

    # But just to be sure, we first explicitely set
    # the values we're checking for:
    ttFont['hhea'].lineGap = 1
    ttFont['OS/2'].sTypoLineGap = 0
    assert_results_contain(check(ttFont),
                           WARN, 'hhea',
                           'with non-zero hhea.lineGap...')

    # Then we run the check with a non-zero OS/2.sTypoLineGap:
    ttFont['hhea'].lineGap = 0
    ttFont['OS/2'].sTypoLineGap = 1
    assert_results_contain(check(ttFont),
                           WARN, 'OS/2',
                           'with non-zero OS/2.sTypoLineGap...')

    # And finaly we fix it by making both values equal to zero:
    ttFont['hhea'].lineGap = 0
    ttFont['OS/2'].sTypoLineGap = 0
    assert_PASS(check(ttFont))


def test_check_maxadvancewidth():
    """ MaxAdvanceWidth is consistent with values in the Hmtx and Hhea tables? """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/maxadvancewidth")

    ttFont = TTFont(TEST_FILE("familysans/FamilySans-Regular.ttf"))
    assert_PASS(check(ttFont))

    ttFont["hmtx"].metrics["A"] = (1234567, 1234567)
    assert_results_contain(check(ttFont),
                           FAIL, 'mismatch')



# tests/profiles/hmtx_test.py

from fontTools.ttLib import TTFont

from fontbakery.checkrunner import FAIL
from fontbakery.codetesting import (TEST_FILE,
                                    assert_PASS,
                                    assert_results_contain,
                                    CheckTester)
from fontbakery.profiles import opentype as opentype_profile


def test_check_whitespace_widths():
    """ Whitespace glyphs have coherent widths? """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/whitespace_widths")

    ttFont = TTFont(TEST_FILE("nunito/Nunito-Regular.ttf"))
    assert_PASS(check(ttFont))

    ttFont["hmtx"].metrics["space"] = (0, 1)
    assert_results_contain(check(ttFont),
                           FAIL, 'different-widths')



# tests/profiles/iso15008_test.py

from fontbakery.checkrunner import FAIL
from fontbakery.codetesting import (assert_PASS,
                                    assert_results_contain,
                                    CheckTester,
                                    TEST_FILE)
from fontbakery.profiles import iso15008 as iso15008_profile


def test_check_iso15008_proportions():
    """Check if 0.65 => (H width / H height) => 0.80"""
    check = CheckTester(iso15008_profile,
                        "com.google.fonts/check/iso15008_proportions")

    # Cabin has a proportion of 0.7, so that's good.
    font = TEST_FILE("cabin/Cabin-Regular.ttf")
    assert_PASS(check(font),
                "with a good font...")

    # Wonky Paths doesn't have an H
    font = TEST_FILE("wonky_paths/WonkySourceSansPro-Regular.otf")
    assert_results_contain(check(font),
                           FAIL, "glyph-not-present",
                           "with a font that does not have an 'H' glyph...")

    # Covered By Your Grace is really tall (proportion 0.39)
    font = TEST_FILE("coveredbyyourgrace/CoveredByYourGrace.ttf")
    assert_results_contain(check(font),
                           FAIL, "invalid-proportion",
                           "with a very tall font (proportion of 'H' width to 'H' height)...")


def test_check_iso15008_stem_width():
    """Check if 0.10 <= (stem width / ascender) <= 0.82"""
    check = CheckTester(iso15008_profile,
                        "com.google.fonts/check/iso15008_stem_width")

    font = TEST_FILE("cabin/Cabin-SemiBold.ttf")
    assert_PASS(check(font),
                "with a good font...")

    # Wonky Paths doesn't have an 'l'
    font = TEST_FILE("wonky_paths/WonkySourceSansPro-Regular.otf")
    assert_results_contain(check(font),
                           FAIL, "no-stem-width",
                           "with a font lacking an 'l' glyph...")

    # Cabin Regular is actually slightly too thin for displays
    font = TEST_FILE("cabin/Cabin-Regular.ttf")
    assert_results_contain(check(font),
                           FAIL, "invalid-proportion",
                           "with a too thin font (proportion of stem width to ascender)...")


def test_check_iso15008_intercharacter_spacing():
    """Check if spacing between characters is adequate for display use"""
    check = CheckTester(iso15008_profile,
                        "com.google.fonts/check/iso15008_intercharacter_spacing")

    font = TEST_FILE("cabin/Cabin-Regular.ttf")
    assert_PASS(check(font),
                "with a good font...")

    font = TEST_FILE("cabin/Cabin-SemiBold.ttf")
    # l stem width is 111, LSB at x-height is 59, RSB at x-Height is 83
    # 142 / 111 = 128%, so this font is too tight.
    assert_results_contain(check(font),
                           FAIL, "bad-vertical-vertical-spacing",
                           "with a too tight font (space between vertical strokes)...")

    # v LSB is 5, lv kern is -6 (!) so lv distance is 83+5-6 = 82
    # 82 / 111 = 0.73%, so that fails too.
    assert_results_contain(check(font),
                           FAIL, "bad-vertical-diagonal-spacing",
                           "with bad spacing between vertical and diagonal strokes...")

    font = TEST_FILE("montserrat/Montserrat-Black.ttf")
    # vv touches
    assert_results_contain(check(font),
                           FAIL, "bad-diagonal-diagonal-spacing",
                           "with diagonal strokes (vv) that are touching...")


def test_check_iso15008_interword_spacing():
    """Check if spacing between words is adequate for display use"""
    check = CheckTester(iso15008_profile,
                        "com.google.fonts/check/iso15008_interword_spacing")

    font = TEST_FILE("cabin/CabinCondensed-Bold.ttf")
    # lm space is 112; m+space+l space is 286; 286/112 = 255%
    assert_PASS(check(font),
                "with a good font...")

    font = TEST_FILE("cabin/Cabin-Regular.ttf")
    # lm space is 147; m+space+l space is 341; 341/147 = 232%
    assert_results_contain(check(font),
                           FAIL, "bad-interword-spacing",
                           "with bad interword space...")


def test_check_iso15008_interline_spacing():
    """Check if spacing between lines is adequate for display use"""
    check = CheckTester(iso15008_profile,
                        "com.google.fonts/check/iso15008_interline_spacing")

    font = TEST_FILE("cabin/Cabin-Regular.ttf")
    assert_PASS(check(font),
                "with a good font...")

    font = TEST_FILE("source-sans-pro/TTF/SourceSansPro-Bold.ttf")
    # 39 units at bottom of g + 49 units at top of h + no typolinegap = 88
    # stem width = 147
    assert_results_contain(check(font),
                           FAIL, "bad-interline-spacing",
                           "with bad interline space...")



# tests/profiles/kern_test.py

from fontTools.ttLib import TTFont, newTable
from fontTools.ttLib.tables._k_e_r_n  import KernTable_format_0, KernTable_format_unkown

from fontbakery.checkrunner import INFO, FAIL, WARN
from fontbakery.codetesting import (assert_PASS,
                                    assert_results_contain,
                                    CheckTester,
                                    TEST_FILE)

from fontbakery.profiles import opentype as opentype_profile


def test_check_kern_table():
    """ Is there a "kern" table declared in the font? """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/kern_table")

    # Our reference Mada Regular is known to be good
    # (does not have a 'kern' table):
    ttFont = TTFont(TEST_FILE("mada/Mada-Regular.ttf"))

    # So it must PASS the check:
    assert_PASS(check(ttFont),
                'with a font without a "kern" table...')

    # add a basic 'kern' table:
    kern = ttFont["kern"] = newTable("kern")
    kern.version = 0
    subtable = KernTable_format_0()
    subtable.coverage = 1
    subtable.version = 0
    subtable.kernTable = {("A", "V"): -50}
    kern.kernTables = [subtable]

    # and make sure the check emits an INFO message:
    assert_results_contain(check(ttFont),
                           INFO, 'kern-found',
                           'with a font containing a "kern" table...')

    # and a FAIL message when a non-character glyph is used.
    subtable.kernTable.update({("A", "four.dnom"): -50})
    assert_results_contain(check(ttFont),
                           FAIL, 'kern-non-character-glyphs',
                           'The following glyphs should not be used...')

    # and a WARN message when a non-character glyph is used.
    subtable = KernTable_format_unkown(2)
    kern.kernTables = [subtable]
    assert_results_contain(check(ttFont),
                           WARN, 'kern-unknown-format',
                           'The "kern" table does not have any format-0 subtable...')


# tests/profiles/layout_test.py

from fontTools.ttLib import TTFont

from fontbakery.checkrunner import FAIL
from fontbakery.codetesting import (assert_PASS,
                                    assert_results_contain,
                                    CheckTester,
                                    TEST_FILE)
from fontbakery.profiles import layout as layout_profile


def test_check_layout_valid_feature_tags():
    """Does the font have any invalid feature tags?"""
    check = CheckTester(layout_profile,
                        "com.google.fonts/check/layout_valid_feature_tags")

    font = TEST_FILE("nunito/Nunito-Regular.ttf")
    assert_PASS(check(font))


    font = TEST_FILE("rosarivo/Rosarivo-Regular.ttf")
    assert_results_contain(check(font),
                           FAIL, 'bad-feature-tags')


def test_check_layout_valid_script_tags():
    """Does the font have any invalid script tags?"""
    check = CheckTester(layout_profile,
                        "com.google.fonts/check/layout_valid_script_tags")

    font = TEST_FILE("nunito/Nunito-Regular.ttf")
    assert_PASS(check(font))


    font = TEST_FILE("rosarivo/Rosarivo-Regular.ttf")
    assert_results_contain(check(font),
                           FAIL, 'bad-script-tags')


def test_check_layout_valid_language_tags():
    """Does the font have any invalid language tags?"""
    check = CheckTester(layout_profile,
                        "com.google.fonts/check/layout_valid_language_tags")

    font = TEST_FILE("nunito/Nunito-Regular.ttf")
    assert_PASS(check(font))


    font = TEST_FILE("rosarivo/Rosarivo-Regular.ttf")
    assert_results_contain(check(font),
                           FAIL, 'bad-language-tags')


# tests/profiles/loca_test.py

import io
from fontTools.ttLib import TTFont

from fontbakery.checkrunner import FAIL
from fontbakery.codetesting import (assert_PASS,
                                    assert_results_contain,
                                    CheckTester,
                                    TEST_FILE)
from fontbakery.profiles import opentype as opentype_profile


def test_check_loca_maxp_num_glyphs():
    """Does the number of glyphs in the loca table match the maxp table?"""
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/loca/maxp_num_glyphs")

    ttFont = TTFont(TEST_FILE("nunito/Nunito-Regular.ttf"))
    assert_PASS(check(ttFont))

    ttFont["loca"].locations.pop()
    _file = io.BytesIO()
    ttFont.save(_file)
    ttFont = TTFont(_file)
    ttFont.reader.file.name = "foo" # Make CheckTester class happy... :-P
    assert_results_contain(check(ttFont),
                           FAIL, 'corrupt')



# tests/profiles/name_test.py

import os
from types import SimpleNamespace

import fontTools.ttLib
from fontTools.ttLib import TTFont

from fontbakery.constants import (NameID,
                                  PlatformID,
                                  WindowsEncodingID,
                                  WindowsLanguageID,
                                  MacintoshEncodingID,
                                  MacintoshLanguageID)
from fontbakery.checkrunner import (INFO, WARN, PASS, FAIL)
from fontbakery.codetesting import (assert_PASS,
                                    assert_results_contain,
                                    CheckTester,
                                    portable_path,
                                    TEST_FILE)
from fontbakery.profiles import opentype as opentype_profile


def test_check_name_empty_records():
    check = CheckTester(opentype_profile,
                        "com.adobe.fonts/check/name/empty_records")

    font_path = TEST_FILE("source-sans-pro/OTF/SourceSansPro-Regular.otf")
    test_font = TTFont(font_path)

    assert_PASS(check(test_font),
                'with a font with fully populated name records.')

    test_font['name'].names[3].string = b''
    assert_results_contain(check(test_font),
                           FAIL, 'empty-record',
                           'with a completely empty string.')

    test_font['name'].names[3].string = b' '
    assert_results_contain(check(test_font),
                           FAIL, 'empty-record',
                           'with a string that only has whitespace.')


def test_check_name_no_copyright_on_description():
    """ Description strings in the name table
        must not contain copyright info.
    """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/name/no_copyright_on_description")

    # Our reference Mada Regular is know to be good here.
    ttFont = TTFont(TEST_FILE("mada/Mada-Regular.ttf"))
    assert_PASS(check(ttFont),
                'with a good font...')

    # here we add a "Copyright" string to a NameID.DESCRIPTION
    for i, name in enumerate(ttFont['name'].names):
        if name.nameID == NameID.DESCRIPTION:
            ttFont['name'].names[i].string = "Copyright".encode(name.getEncoding())

    assert_results_contain(check(ttFont),
                           FAIL, 'copyright-on-description',
                           'with a bad font...')


def test_check_monospace():
    """ Checking correctness of monospaced metadata. """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/monospace")
    import string
    from fontbakery.constants import (PANOSE_Proportion,
                                      IsFixedWidth)

    # This check has a large number of code-paths
    # We'll make sure to test them all here.
    #
    # --------------------------------------------
    # Starting with non-monospaced code-paths:
    # --------------------------------------------

    # Our reference Mada Regular is a non-monospace font
    # know to have good metadata for this check.
    ttFont = TTFont(TEST_FILE("mada/Mada-Regular.ttf"))
    assert_results_contain(check(ttFont),
                           PASS, "good",
                           'with a good non-monospace font...')

    # We'll mark it as monospaced on the post table and make sure it fails:
    ttFont["post"].isFixedPitch = 42 # *any* non-zero value means monospaced
    assert_results_contain(check(ttFont),
                           FAIL, 'bad-post-isFixedPitch',
                           'with a non-monospaced font with bad'
                           ' post.isFixedPitch value ...')

    # restore good value:
    ttFont["post"].isFixedPitch = IsFixedWidth.NOT_MONOSPACED

    # Now we mark it as monospaced on the OS/2 table and it should also fail:
    original_proportion = ttFont["OS/2"].panose.bProportion
    ttFont["OS/2"].panose.bProportion = PANOSE_Proportion.MONOSPACED
    assert_results_contain(check(ttFont),
                           FAIL, 'bad-panose',
                           'with a non-monospaced font with bad'
                           ' OS/2.panose.bProportion value (MONOSPACED) ...')

    # restore good value
    ttFont["OS/2"].panose.bProportion = original_proportion

    # Now we try with very little ASCII characters in the font
    cmap = ttFont["cmap"]
    for subtable in list(cmap.tables):
        # Remove A-Z, a-z from cmap
        for code in list(map(ord, string.ascii_letters)):
            if subtable.cmap.get(code):
                del subtable.cmap[code]
    assert_results_contain(check(ttFont),
                           PASS, "good",
                           'with a good non-monospace font...')

    # --------------------------------------------
    # And now we test the monospaced code-paths:
    # --------------------------------------------

    print('Test PASS with a good monospaced font...')
    # Our reference OverpassMono Regular is know to be
    # a monospaced font with good metadata here.
    ttFont = TTFont(TEST_FILE("overpassmono/OverpassMono-Regular.ttf"))

    status, message = check(ttFont)[-1]
    # WARN is emitted when there's at least one outlier.
    # I don't see a good reason to be picky and also test that one separately here...
    assert (status == WARN and message.code == "mono-outliers") or \
           (status == PASS and message.code == "mono-good")

    # Let's incorrectly mark it as a non-monospaced on the post table and it should fail:
    ttFont["post"].isFixedPitch = IsFixedWidth.NOT_MONOSPACED
    # here we search for the expected FAIL among all results
    # instead of simply looking at the last one
    # because we may also get an outliers WARN in some cases:
    assert_results_contain(check(ttFont),
                           FAIL, 'mono-bad-post-isFixedPitch',
                           'with a monospaced font with'
                           ' bad post.isFixedPitch value ...')

    # There are several bad panose proportion values for a monospaced font.
    # Only PANOSE_Proportion.MONOSPACED would be valid.
    # So we'll try all the bad ones here to make sure all of them emit a FAIL:
    bad_monospaced_panose_values = [
        PANOSE_Proportion.ANY,
        PANOSE_Proportion.NO_FIT,
        PANOSE_Proportion.OLD_STYLE,
        PANOSE_Proportion.MODERN,
        PANOSE_Proportion.EVEN_WIDTH,
        PANOSE_Proportion.EXTENDED,
        PANOSE_Proportion.CONDENSED,
        PANOSE_Proportion.VERY_EXTENDED,
        PANOSE_Proportion.VERY_CONDENSED,
    ]
    for bad_value in bad_monospaced_panose_values:
        ttFont["OS/2"].panose.bProportion = bad_value
        # again, we search the expected FAIL because we may algo get an outliers WARN here:
        assert_results_contain(check(ttFont),
                               FAIL, 'mono-bad-panose',
                               f'Test FAIL with a monospaced font with bad'
                               f' OS/2.panose.bProportion value ({bad_value}) ...')

    # restore good values
    ttFont["post"].isFixedPitch = 1
    ttFont["OS/2"].panose.bProportion = PANOSE_Proportion.MONOSPACED

    # Now we try with very little ASCII characters in the font
    cmap = ttFont["cmap"]
    for subtable in list(cmap.tables):
        # Remove A-Z, a-z from cmap
        for code in list(map(ord, string.ascii_letters)):
            if subtable.cmap.get(code):
                del subtable.cmap[code]

    status, message = check(ttFont)[-1]
    # WARN is emitted when there's at least one outlier.
    # I don't see a good reason to be picky and also test that one separately here...
    assert (status == WARN and message.code == "mono-outliers") or \
           (status == PASS and message.code == "mono-good")


def test_check_name_match_familyname_fullfont():
    """ Does full font name begin with the font family name? """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/name/match_familyname_fullfont")

    # Our reference Mada Regular is known to be good
    ttFont = TTFont(TEST_FILE("mada/Mada-Regular.ttf"))

    # So it must PASS the check:
    msg = assert_PASS(check(ttFont))
    assert msg == "Full font name begins with the font family name."

    EXPECTED_NAME_STRING = "Mada"
    BAD_PREFIX = "bad-prefix"
    name_table = ttFont["name"]
    platform_id = 3
    encoding_id = 1
    language_id = 0x0409
    family_name_id = NameID.FONT_FAMILY_NAME
    full_name_id = NameID.FULL_FONT_NAME

    # Alter the font's full_name string and re-run the check.
    # 1. Retrieve the existing name strings and assert that they're the expected ones.
    family_name = name_table.getName(
        family_name_id, platform_id, encoding_id, language_id).toUnicode()
    assert family_name == EXPECTED_NAME_STRING

    full_name_before = name_table.getName(
        full_name_id, platform_id, encoding_id, language_id).toUnicode()
    assert full_name_before == EXPECTED_NAME_STRING

    # 2. Prefix the full_name string, and update the font's name record.
    name_table.setName(f"{BAD_PREFIX}{full_name_before}", full_name_id,
                       platform_id, encoding_id, language_id)

    # 3. Retrieve the updated name string, and assert that it's the expected one.
    full_name_after = name_table.getName(
        full_name_id, platform_id, encoding_id, language_id).toUnicode()
    assert full_name_after != family_name
    assert full_name_after == f"{BAD_PREFIX}{EXPECTED_NAME_STRING}"
    assert full_name_after.startswith(BAD_PREFIX)

    # 4. Now re-run the check. It should yield FAIL because the full_name string
    # no longer starts with the family_name string.
    msg = assert_results_contain(check(ttFont), FAIL, "mismatch-font-names")
    assert msg == (f"On the 'name' table, the full font name {full_name_after!r}"
                   f" does not begin with the font family name {family_name!r}"
                   f" in platformID {platform_id},"
                   f" encodingID {encoding_id},"
                   f" languageID {language_id}({language_id:04X}),"
                   f" and nameID {family_name_id}.")

    # Remove the modified full name record and re-run the check.
    # It should yield FAIL because the name table won't contain a full name string
    # to compare with the family name string.
    name_table.removeNames(full_name_id, platform_id, encoding_id, language_id)
    msg = assert_results_contain(check(ttFont), FAIL, "missing-font-names")
    assert msg == (
        f"The font's 'name' table lacks a pair of records with"
        f" nameID {NameID.FULL_FONT_NAME} (Full name),"
        f" and at least one of"
        f" nameID {NameID.FONT_FAMILY_NAME} (Font Family name),"
        f" {NameID.TYPOGRAPHIC_FAMILY_NAME} (Typographic Family name),"
        f" or {NameID.WWS_FAMILY_NAME} (WWS Family name)."
    )

    # Run the check on a CJK font. The font's 'name' table contains
    # English-US (1033/0x0409) and Japanese (1041/0x0411) records. It should PASS.
    ttFont = TTFont(TEST_FILE("cjk/SourceHanSans-Regular.otf"))
    msg = assert_PASS(check(ttFont))
    assert msg == "Full font name begins with the font family name."

    name_table = ttFont["name"]
    decode_error_msg_prefix = (
        f"On the 'name' table, the name record"
        f" for platformID {platform_id},"
        f" encodingID {encoding_id},"
        f" languageID {language_id}({language_id:04X}),"
    )

    # Replace the English full name string with data that can't be 'utf_16_be'-decoded.
    # This will cause a UnicodeDecodeError which will yield a FAIL.
    name_table.setName("\xff".encode("utf_7"),
                       full_name_id, platform_id, encoding_id, language_id)
    msg = assert_results_contain(
        check(ttFont), FAIL, f"cannot-decode-nameid-{full_name_id}")
    assert msg == (
        f"{decode_error_msg_prefix} and nameID {full_name_id} failed to be decoded."
    )

    # This time replace the family name string instead.
    # This should still trigger a UnicodeDecodeError.
    name_table = ttFont["name"]
    name_table.setName("\xff".encode("utf_7"),
                       family_name_id, platform_id, encoding_id, language_id)
    msg = assert_results_contain(
        check(ttFont), FAIL, f"cannot-decode-nameid-{family_name_id}")
    assert msg == (
        f"{decode_error_msg_prefix} and nameID {family_name_id} failed to be decoded."
    )


def assert_name_table_check_result(ttFont, index, name, check, value, expected_result, expected_keyword=None):
    backup = name.string
    # set value
    ttFont["name"].names[index].string = value.encode(name.getEncoding())
    # run check
    status, message = list(check(ttFont))[-1]
    # restore value
    ttFont["name"].names[index].string = backup
    assert status == expected_result
    if expected_keyword:
        assert message.code == expected_keyword


def test_check_family_naming_recommendations():
    """ Font follows the family naming recommendations ? """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/family_naming_recommendations")

    # Our reference Mada Medium is known to be good
    ttFont = TTFont(TEST_FILE("mada/Mada-Medium.ttf"))

    # So it must PASS the check:
    assert_PASS(check(ttFont),
                'with a good font...')

    # We'll test rule violations in all entries one-by-one
    for index, name in enumerate(ttFont["name"].names):
        # and we'll test all INFO/PASS code-paths for each of the rules:
        def name_test(value, expected, keyword=None):
            assert_name_table_check_result(ttFont, index, name, check, value, expected, keyword) #pylint: disable=cell-var-from-loop

        if name.nameID == NameID.POSTSCRIPT_NAME:
            print ("== NameID.POST_SCRIPT_NAME ==")

            print ("Test INFO: May contain only a-zA-Z0-9 characters and an hyphen...")
            # The '@' and '!' chars here are the expected rule violations:
            name_test("B@zinga!", INFO, "bad-entries")

            print ("Test PASS: A name with a single hyphen is OK...")
            # A single hypen in the name is OK:
            name_test("Big-Bang", PASS)

            print ("Test INFO: May not contain more than a single hyphen...")
            # The second hyphen char here is the expected rule violation:
            name_test("Big-Bang-Theory", INFO, "bad-entries")

            print ("Test INFO: Exceeds max length (63)...")
            name_test("A"*64, INFO, "bad-entries")

            print ("Test PASS: Does not exceed max length...")
            name_test("A"*63, PASS)

        elif name.nameID == NameID.FULL_FONT_NAME:
            print ("== NameID.FULL_FONT_NAME ==")

            print ("Test INFO: Exceeds max length (63)...")
            name_test("A"*64, INFO, "bad-entries")

            print ("Test PASS: Does not exceeds max length...")
            name_test("A"*63, PASS)

        elif name.nameID == NameID.FONT_FAMILY_NAME:
            print ("== NameID.FONT_FAMILY_NAME ==")

            print ("Test INFO: Exceeds max length (31)...")
            name_test("A"*32, INFO, "bad-entries")

            print ("Test PASS: Does not exceeds max length...")
            name_test("A"*31, PASS)

        elif name.nameID == NameID.FONT_SUBFAMILY_NAME:
            print ("== NameID.FONT_SUBFAMILY_NAME ==")

            print ("Test INFO: Exceeds max length (31)...")
            name_test("A"*32, INFO, "bad-entries")

            print ("Test PASS: Does not exceeds max length...")
            name_test("A"*31, PASS)

        elif name.nameID == NameID.TYPOGRAPHIC_FAMILY_NAME:
            print ("== NameID.TYPOGRAPHIC_FAMILY_NAME ==")

            print ("Test INFO: Exceeds max length (31)...")
            name_test("A"*32, INFO, "bad-entries")

            print ("Test PASS: Does not exceeds max length...")
            name_test("A"*31, PASS)

        elif name.nameID == NameID.TYPOGRAPHIC_SUBFAMILY_NAME:
            print ("== NameID.FONT_TYPOGRAPHIC_SUBFAMILY_NAME ==")

            print ("Test INFO: Exceeds max length (31)...")
            name_test("A"*32, INFO, "bad-entries")

            print ("Test PASS: Does not exceeds max length...")
            name_test("A"*31, PASS)


def test_check_name_postscript_vs_cff():
    check = CheckTester(opentype_profile,
                        "com.adobe.fonts/check/name/postscript_vs_cff")
    test_font = TTFont()
    test_font['CFF '] = fontTools.ttLib.newTable('CFF ')
    test_font['CFF '].cff.fontNames = ['SomeFontName']
    test_font['name'] = fontTools.ttLib.newTable('name')

    test_font['name'].setName(
        'SomeOtherFontName',
        NameID.POSTSCRIPT_NAME,
        PlatformID.WINDOWS,
        WindowsEncodingID.UNICODE_BMP,
        WindowsLanguageID.ENGLISH_USA
    )

    test_font.reader = SimpleNamespace(file=SimpleNamespace(name="whatever"))
    assert_results_contain(check(test_font),
                           FAIL, 'mismatch')

    test_font['name'].setName(
        'SomeFontName',
        NameID.POSTSCRIPT_NAME,
        PlatformID.WINDOWS,
        WindowsEncodingID.UNICODE_BMP,
        WindowsLanguageID.ENGLISH_USA
    )
    assert_PASS(check(test_font))


def test_check_name_postscript_name_consistency():
    check = CheckTester(opentype_profile,
                        "com.adobe.fonts/check/name/postscript_name_consistency")

    base_path = portable_path("data/test/source-sans-pro/TTF")
    font_path = os.path.join(base_path, 'SourceSansPro-Regular.ttf')
    test_font = TTFont(font_path)

    # SourceSansPro-Regular only has one name ID 6 entry (for Windows),
    # let's add another one for Mac that matches the Windows entry:
    test_font['name'].setName(
        'SourceSansPro-Regular',
        NameID.POSTSCRIPT_NAME,
        PlatformID.MACINTOSH,
        MacintoshEncodingID.ROMAN,
        MacintoshLanguageID.ENGLISH
    )
    assert_PASS(check(test_font))

    # ...now let's change the Mac name ID 6 entry to something else:
    test_font['name'].setName(
        'YetAnotherFontName',
        NameID.POSTSCRIPT_NAME,
        PlatformID.MACINTOSH,
        MacintoshEncodingID.ROMAN,
        MacintoshLanguageID.ENGLISH
    )
    assert_results_contain(check(test_font),
                           FAIL, 'inconsistency')


def test_check_family_max_4_fonts_per_family_name():
    check = CheckTester(opentype_profile,
                        "com.adobe.fonts/check/family/max_4_fonts_per_family_name")

    base_path = portable_path("data/test/source-sans-pro/OTF")

    font_names = [
        'SourceSansPro-Black.otf',
        'SourceSansPro-BlackItalic.otf',
        'SourceSansPro-Bold.otf',
        'SourceSansPro-BoldItalic.otf',
        'SourceSansPro-ExtraLight.otf',
        'SourceSansPro-ExtraLightItalic.otf',
        'SourceSansPro-Italic.otf',
        'SourceSansPro-Light.otf',
        'SourceSansPro-LightItalic.otf',
        'SourceSansPro-Regular.otf',
        'SourceSansPro-Semibold.otf',
        'SourceSansPro-SemiboldItalic.otf']

    font_paths = [os.path.join(base_path, n) for n in font_names]

    test_fonts = [TTFont(x) for x in font_paths]

    # try fonts with correct family name grouping
    assert_PASS(check(test_fonts))

    # now set 5 of the fonts to have the same family name
    for font in test_fonts[:5]:
        name_records = font['name'].names
        for name_record in name_records:
            if name_record.nameID == 1:
                # print(repr(name_record.string))
                name_record.string = 'foobar'.encode('utf-16be')

    assert_results_contain(check(test_fonts),
                           FAIL, 'too-many')



# tests/profiles/os2_test.py

import io
import os
import pytest

import fontTools.ttLib
from fontTools.ttLib import TTFont
import fontTools.subset

from fontbakery.checkrunner import (INFO, WARN, FAIL)
from fontbakery.codetesting import (assert_PASS,
                                    assert_results_contain,
                                    CheckTester,
                                    portable_path,
                                    TEST_FILE)
from fontbakery.profiles import opentype as opentype_profile


mada_fonts = [
    TEST_FILE("mada/Mada-Black.ttf"),
    TEST_FILE("mada/Mada-ExtraLight.ttf"),
    TEST_FILE("mada/Mada-Medium.ttf"),
    TEST_FILE("mada/Mada-SemiBold.ttf"),
    TEST_FILE("mada/Mada-Bold.ttf"),
    TEST_FILE("mada/Mada-Light.ttf"),
    TEST_FILE("mada/Mada-Regular.ttf")
]

@pytest.fixture
def mada_ttFonts():
    return [TTFont(path) for path in mada_fonts]

cabin_fonts = [
    TEST_FILE("cabin/Cabin-BoldItalic.ttf"),
    TEST_FILE("cabin/Cabin-Bold.ttf"),
    TEST_FILE("cabin/Cabin-Italic.ttf"),
    TEST_FILE("cabin/Cabin-MediumItalic.ttf"),
    TEST_FILE("cabin/Cabin-Medium.ttf"),
    TEST_FILE("cabin/Cabin-Regular.ttf"),
    TEST_FILE("cabin/Cabin-SemiBoldItalic.ttf"),
    TEST_FILE("cabin/Cabin-SemiBold.ttf")
]


def test_check_family_panose_proportion(mada_ttFonts):
    """ Fonts have consistent PANOSE proportion ? """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/family/panose_proportion")

    assert_PASS(check(mada_ttFonts),
                'with good family.')

    # introduce a wrong value in one of the font files:
    value = mada_ttFonts[0]['OS/2'].panose.bProportion
    incorrect_value = value + 1
    mada_ttFonts[0]['OS/2'].panose.bProportion = incorrect_value

    assert_results_contain(check(mada_ttFonts),
                           FAIL, 'inconsistency',
                           'with inconsistent family.')


def test_check_family_panose_familytype(mada_ttFonts):
    """ Fonts have consistent PANOSE family type ? """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/family/panose_familytype")

    assert_PASS(check(mada_ttFonts),
                'with good family.')

    # introduce a wrong value in one of the font files:
    value = mada_ttFonts[0]['OS/2'].panose.bFamilyType
    incorrect_value = value + 1
    mada_ttFonts[0]['OS/2'].panose.bFamilyType = incorrect_value

    assert_results_contain(check(mada_ttFonts),
                           FAIL, 'inconsistency',
                           'with inconsistent family.')


def test_check_xavgcharwidth():
    """ Check if OS/2 xAvgCharWidth is correct. """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/xavgcharwidth")

    test_font_path = TEST_FILE("nunito/Nunito-Regular.ttf")

    test_font = TTFont(test_font_path)
    assert_PASS(check(test_font))

    test_font['OS/2'].xAvgCharWidth = 556
    assert_results_contain(check(test_font),
                           INFO, "xAvgCharWidth-close")

    test_font['OS/2'].xAvgCharWidth = 500
    assert_results_contain(check(test_font),
                           WARN, "xAvgCharWidth-wrong") # FIXME: This needs a message keyword

    del test_font['OS/2']
    del test_font['glyf']
    del test_font['hmtx']
    test_font['OS/2'] = fontTools.ttLib.newTable('OS/2')
    test_font['OS/2'].version = 4
    test_font['OS/2'].xAvgCharWidth = 1000
    test_font['glyf'] = fontTools.ttLib.newTable('glyf')
    test_font['glyf'].glyphs = {}
    test_font['hmtx'] = fontTools.ttLib.newTable('hmtx')
    test_font['hmtx'].metrics = {}
    assert_results_contain(check(test_font),
                           FAIL, 'missing-glyphs')

    test_font = TTFont(test_font_path)
    subsetter = fontTools.subset.Subsetter()
    subsetter.populate(glyphs=['a', 'b', 'c', 'd', 'e', 'f', 'g',
                               'h', 'i', 'j', 'k', 'l', 'm', 'n',
                               'o', 'p', 'q', 'r', 's', 't', 'u',
                               'v', 'w', 'x', 'y', 'z', 'space'])
    subsetter.subset(test_font)
    test_font['OS/2'].xAvgCharWidth = 447
    test_font['OS/2'].version = 2
    temp_file = io.BytesIO()
    test_font.save(temp_file)
    test_font = TTFont(temp_file)
    test_font.reader.file.name = "foo.ttf"
    assert_PASS(check(test_font))

    test_font['OS/2'].xAvgCharWidth = 450
    assert_results_contain(check(test_font),
                           INFO, "xAvgCharWidth-close")

    test_font['OS/2'].xAvgCharWidth = 500
    assert_results_contain(check(test_font),
                           WARN, "xAvgCharWidth-wrong")

    test_font = TTFont(temp_file)
    test_font.reader.file.name = "foo.ttf"
    subsetter = fontTools.subset.Subsetter()
    subsetter.populate(glyphs=['b', 'c', 'd', 'e', 'f', 'g', 'h',
                               'i', 'j', 'k', 'l', 'm', 'n', 'o',
                               'p', 'q', 'r', 's', 't', 'u', 'v',
                               'w', 'x', 'y', 'z', 'space'])
    subsetter.subset(test_font)
    assert_results_contain(check(test_font),
                           FAIL, 'missing-glyphs')


def test_check_fsselection_matches_macstyle():
    """Check if OS/2 fsSelection matches head macStyle bold and italic bits."""
    check = CheckTester(opentype_profile,
                        "com.adobe.fonts/check/fsselection_matches_macstyle")
    from fontbakery.constants import FsSelection

    test_font_path = TEST_FILE("nunito/Nunito-Regular.ttf")

    # try a regular (not bold, not italic) font
    test_font = TTFont(test_font_path)
    assert_PASS(check(test_font))

    # now turn on bold in OS/2.fsSelection, but not in head.macStyle
    test_font['OS/2'].fsSelection |= FsSelection.BOLD
    message = assert_results_contain(check(test_font),
                                     FAIL, "fsselection-macstyle-bold")
    assert 'bold' in message

    # now turn off bold in OS/2.fsSelection so we can focus on italic
    test_font['OS/2'].fsSelection &= ~FsSelection.BOLD

    # now turn on italic in OS/2.fsSelection, but not in head.macStyle
    test_font['OS/2'].fsSelection |= FsSelection.ITALIC
    message = assert_results_contain(check(test_font),
                                     FAIL, "fsselection-macstyle-italic")
    assert 'italic' in message


def test_check_family_bold_italic_unique_for_nameid1():
    """Check that OS/2.fsSelection bold/italic settings are unique within each
    Compatible Family group (i.e. group of up to 4 with same NameID1)"""
    check = CheckTester(opentype_profile,
                        "com.adobe.fonts/check/family/bold_italic_unique_for_nameid1")
    from fontbakery.constants import FsSelection

    base_path = portable_path("data/test/source-sans-pro/OTF")

    # these fonts have the same NameID1
    font_names = ['SourceSansPro-Regular.otf',
                  'SourceSansPro-Bold.otf',
                  'SourceSansPro-Italic.otf',
                  'SourceSansPro-BoldItalic.otf']
    font_paths = [os.path.join(base_path, n) for n in font_names]
    ttFonts = [TTFont(x) for x in font_paths]

    # the family should be correctly constructed
    assert_PASS(check(ttFonts))

    # now hack the italic font to also have the bold bit set
    ttFonts[2]['OS/2'].fsSelection |= FsSelection.BOLD

    # we should get a failure due to two fonts with both bold & italic set
    message = assert_results_contain(check(ttFonts),
                                     FAIL, "unique-fsselection")
    assert message == ("Family 'Source Sans Pro' has 2 fonts (should be no"
                       " more than 1) with the same OS/2.fsSelection"
                       " bold & italic settings: Bold=True, Italic=True")


def test_check_code_pages():
    """ Check code page character ranges """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/code_pages")

    ttFont = TTFont(TEST_FILE("merriweather/Merriweather-Regular.ttf"))
    assert(ttFont['OS/2'].ulCodePageRange1 != 0 or
           ttFont['OS/2'].ulCodePageRange2 != 0) # It has got at least 1 code page range declared
    assert_PASS(check(ttFont),
                'with good font.')

    ttFont['OS/2'].ulCodePageRange1 = 0 # remove all code pages to make the check FAIL
    ttFont['OS/2'].ulCodePageRange2 = 0
    assert_results_contain(check(ttFont),
                           FAIL, "no-code-pages",
                           'with a font with no code page declared.')



# tests/profiles/outline_test.py

from fontbakery.checkrunner import WARN
from fontbakery.codetesting import (assert_results_contain,
                                    CheckTester,
                                    TEST_FILE,
                                    assert_PASS)
from fontbakery.profiles import outline as outline_profile


def test_check_outline_alignment_miss():
    """ Check for misaligned points. """
    check = CheckTester(outline_profile,
                        "com.google.fonts/check/outline_alignment_miss")

    filename = TEST_FILE("wonky_paths/WonkySourceSansPro-Regular.otf")
    results = check(filename)
    assert_results_contain(results,
                           WARN, 'found-misalignments')
    messages = "".join([m[1].message for m in results])
    assert "A (U+0041): X=3.0,Y=-2.0 (should be at baseline 0?)" in messages

    # TODO: PASS


def test_check_outline_short_segments():
    """ Check for short segments. """
    check = CheckTester(outline_profile,
                        "com.google.fonts/check/outline_short_segments")

    filename = TEST_FILE("wonky_paths/WonkySourceSansPro-Regular.otf")
    results = check(filename)
    assert_results_contain(results,
                           WARN, 'found-short-segments')
    messages = "".join([m[1].message for m in results])
    assert "D (U+0044) contains a short segment L<<180.0,68.0>--<173.0,71.0>>" in messages

    # TODO: PASS


def test_check_outline_colinear_vectors():
    """ Check for colinear line segments. """
    check = CheckTester(outline_profile,
                        "com.google.fonts/check/outline_colinear_vectors")

    filename = TEST_FILE("wonky_paths/WonkySourceSansPro-Regular.otf")
    results = check(filename)
    assert_results_contain(results,
                           WARN, 'found-colinear-vectors')
    messages = "".join([m[1].message for m in results])
    assert "A (U+0041)" not in messages
    assert "B (U+0042)" not in messages
    assert "C (U+0043)" in messages
    assert "E (U+0045)" in messages
    assert ".notdef" not in messages

    # TODO: PASS


def test_check_outline_jaggy_segments():
    """ Check for jaggy segments. """
    check = CheckTester(outline_profile,
                        "com.google.fonts/check/outline_jaggy_segments")

    filename = TEST_FILE("wonky_paths/WonkySourceSansPro-Regular.otf")
    results = check(filename)
    assert_results_contain(results,
                           WARN, 'found-jaggy-segments')
    messages = "".join([m[1].message for m in results])
    assert "* E (U+0045)" in messages

    filename = TEST_FILE("familysans/FamilySans-Regular.ttf")
    assert_PASS(check(filename))

    filename = TEST_FILE("source-sans-pro/OTF/SourceSansPro-LightItalic.otf")
    assert_PASS(check(filename))


def test_check_outline_semi_vertical():
    """ Check for semi-vertical/semi-horizontal lines. """
    check = CheckTester(outline_profile,
                        "com.google.fonts/check/outline_semi_vertical")

    filename = TEST_FILE("wonky_paths/WonkySourceSansPro-Regular.otf")
    results = check(filename)
    assert_results_contain(results,
                           WARN, 'found-semi-vertical')
    messages = "".join([m[1].message for m in results])
    assert "* B (U+0042)" in messages
    assert "* E (U+0045)" not in messages

    # TODO: PASS


# tests/profiles/post_test.py

import pytest

from fontTools.ttLib import TTFont

from fontbakery.checkrunner import WARN, FAIL
from fontbakery.codetesting import (assert_PASS,
                                    assert_results_contain,
                                    CheckTester,
                                    TEST_FILE)
from fontbakery.profiles import opentype as opentype_profile


mada_fonts = [
    TEST_FILE("mada/Mada-Black.ttf"),
    TEST_FILE("mada/Mada-ExtraLight.ttf"),
    TEST_FILE("mada/Mada-Medium.ttf"),
    TEST_FILE("mada/Mada-SemiBold.ttf"),
    TEST_FILE("mada/Mada-Bold.ttf"),
    TEST_FILE("mada/Mada-Light.ttf"),
    TEST_FILE("mada/Mada-Regular.ttf")
]

@pytest.fixture
def mada_ttFonts():
    return [TTFont(path) for path in mada_fonts]


def test_check_family_underline_thickness(mada_ttFonts):
    """ Fonts have consistent underline thickness ? """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/family/underline_thickness")

    # We start with our reference Mada font family,
    # which we know has the same value of post.underlineThickness
    # across all of its font files, based on our inspection
    # of the file contents using TTX.
    #
    # So the check should PASS in this case:
    assert_PASS(check(mada_ttFonts),
                'with a good family.')

    # Then we introduce the issue by setting a
    # different underlineThickness value in just
    # one of the font files:
    value = mada_ttFonts[0]['post'].underlineThickness
    incorrect_value = value + 1
    mada_ttFonts[0]['post'].underlineThickness = incorrect_value

    # And now re-running the check on the modified
    # family should result in a FAIL:
    assert_results_contain(check(mada_ttFonts),
                           FAIL, "inconsistent-underline-thickness",
                           'with an inconsistent family.')


def test_check_post_table_version():
    """ Font has acceptable post format version table? """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/post_table_version")

    # create mock fonts for post format testing

    base_tt_font = TTFont(TEST_FILE("mada/Mada-Regular.ttf"))

    #
    # post format 2 mock font test
    #
    mock_post_2 = base_tt_font

    mock_post_2["post"].formatType = 2
    mock_post_2.reader.file.name = "post 2 mock font"

    assert_PASS(check(mock_post_2), reason="with a post 2 mock font")

    #
    # post format 2.5 mock font test
    #
    mock_post_2_5 = base_tt_font

    mock_post_2_5["post"].formatType = 2.5
    mock_post_2_5.reader.file.name = "post 2.5 mock font"

    assert_results_contain(check(mock_post_2_5),
                           FAIL, "post-table-version",
                           'with a font that has post format 2.5 table')

    #
    # post format 3 mock font test
    #
    mock_post_3 = base_tt_font

    mock_post_3["post"].formatType = 3
    mock_post_3.reader.file.name = "post 3  mock font"

    assert_results_contain(check(mock_post_3),
                           WARN, "post-table-version",
                           'with a font that has post format 3 table')

    #
    # post format 4 mock font test
    #
    mock_post_4 = base_tt_font

    mock_post_4["post"].formatType = 4
    mock_post_4.reader.file.name = "post 4 mock font"

    assert_results_contain(check(mock_post_4),
                           FAIL, "post-table-version",
                           'with a font that has post format 4 table')


    #
    # post format 2/3 OTF CFF mock font test
    #
    mock_cff_post_2 = TTFont(TEST_FILE("source-sans-pro/OTF/SourceSansPro-Regular.otf"))

    mock_cff_post_2["post"].formatType = 2
    assert("CFF " in mock_cff_post_2)
    assert("CFF2" not in mock_cff_post_2)
    mock_cff_post_2.reader.file.name = "post 2 CFF mock font"

    assert_results_contain(check(mock_cff_post_2),
                           FAIL, "post-table-version",
                           'with a CFF font that has post format 2 table')

    mock_cff_post_3 = mock_cff_post_2
    mock_cff_post_3["post"].formatType = 3

    assert_PASS(check(mock_cff_post_3), reason="with a post 3 CFF mock font.")



# tests/profiles/shaping_test.py

from fontTools.ttLib import TTFont
import json
import os
import tempfile

from fontbakery.checkrunner import FAIL
from fontbakery.codetesting import (assert_PASS,
                                    assert_results_contain,
                                    CheckTester,
                                    TEST_FILE)
from fontbakery.profiles import universal as universal_profile


def wrap_args(config, font):
    ttFont = TTFont(font)
    return {
        "config": config,
        "font": font,
        "fonts": [font],
        "ttFont": ttFont,
        "ttFonts": [ttFont],
    }


def test_check_shaping_regression():
    """ Check that we can test shaping against expectations. """
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/shaping/regression")

    shaping_test = {
        "configuration": {},
        "tests": [{"input": "AV",
                   "expectation": "A=0+664|V=1+691"}],
    }

    with tempfile.TemporaryDirectory() as tmp_gf_dir:
        json.dump(shaping_test, open(os.path.join(tmp_gf_dir, "test.json"), "w"))

        config = {"com.google.fonts/check/shaping": {"test_directory": tmp_gf_dir}}

        font = TEST_FILE("nunito/Nunito-Regular.ttf")
        assert_PASS(check(wrap_args(config, font)),
                    "Nunito: A=664,V=691")

        font = TEST_FILE("slabo/Slabo13px.ttf")
        assert_results_contain(check(wrap_args(config, font)),
                               FAIL, "shaping-regression",
                               "Slabo: A!=664,V!=691")


def test_check_shaping_forbidden():
    """ Check that we can test for forbidden glyphs in output. """
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/shaping/forbidden")

    shaping_test = {
        "configuration": {"forbidden_glyphs": [".notdef"]},
        "tests": [{"input": "日"}],
    }

    with tempfile.TemporaryDirectory() as tmp_gf_dir:
        json.dump(shaping_test, open(os.path.join(tmp_gf_dir, "test.json"), "w"))

        config = {"com.google.fonts/check/shaping": {"test_directory": tmp_gf_dir}}

        font = TEST_FILE("cjk/SourceHanSans-Regular.otf")
        assert_PASS(check(wrap_args(config, font)),
                    "Source Han contains CJK")

        font = TEST_FILE("slabo/Slabo13px.ttf")
        assert_results_contain(check(wrap_args(config, font)),
                               FAIL, "shaping-forbidden",
                               "Slabo shapes .notdef for CJK")


def test_check_shaping_collides():
    """ Check that we can test for colliding glyphs in output. """
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/shaping/collides")

    shaping_test = {
        "configuration": {"collidoscope": {"area": 0,
                                           "bases": True,
                                           "marks": True}},
        "tests": [{"input": "ïï"}],
    }

    with tempfile.TemporaryDirectory() as tmp_gf_dir:
        json.dump(shaping_test, open(os.path.join(tmp_gf_dir, "test.json"), "w"))

        config = {"com.google.fonts/check/shaping": {"test_directory": tmp_gf_dir}}

        font = TEST_FILE("cousine/Cousine-Regular.ttf")
        assert_PASS(check(wrap_args(config, font)),
                    "ïï doesn't collide in Cousine")

        font = TEST_FILE("nunito/Nunito-Black.ttf")
        assert_results_contain(check(wrap_args(config, font)),
                               FAIL, "shaping-collides",
                               "ïï collides in Nunito")


# tests/profiles/stat_test.py

from fontTools.ttLib import TTFont

from fontbakery.checkrunner import FAIL
from fontbakery.codetesting import (assert_PASS,
                                    assert_results_contain,
                                    CheckTester,
                                    TEST_FILE)
from fontbakery.profiles import opentype as opentype_profile


def test_check_varfont_stat_axis_record_for_each_axis():
    """ Check the STAT table has an Axis Record for every axis in the font. """
    check = CheckTester(opentype_profile,
                        "com.google.fonts/check/varfont/stat_axis_record_for_each_axis")

    # Our reference Cabin[wdth,wght].ttf variable font
    # has all necessary Axis Records
    ttFont = TTFont(TEST_FILE("cabinvf/Cabin[wdth,wght].ttf"))

    # So the check must PASS
    assert_PASS(check(ttFont),
                'with all necessary Axis Records...')

    # We then remove its first Axis Record (`wdth`):
    ttFont['STAT'].table.DesignAxisRecord.Axis.pop(0)

    # And now the problem should be detected::
    assert_results_contain(check(ttFont),
                           FAIL, 'missing-axis-records',
                           'with a missing Axis Record: (wght)...')


# tests/profiles/ufo_sources_test.py

import os

import defcon
import pytest

from fontbakery.checkrunner import (DEBUG, ERROR, FAIL, INFO, PASS, SKIP, WARN)
from fontbakery.codetesting import (assert_PASS,
                                    assert_results_contain,
                                    CheckTester)
from fontbakery.profiles.ufo_sources import profile as ufo_sources_profile

check_statuses = (ERROR, FAIL, SKIP, PASS, WARN, INFO, DEBUG)


@pytest.fixture
def empty_ufo_font(tmpdir):
    ufo = defcon.Font()
    ufo_path = str(tmpdir.join("empty_font.ufo"))
    ufo.save(ufo_path)
    return (ufo, ufo_path)


def test_check_ufolint(empty_ufo_font):
    check = CheckTester(ufo_sources_profile,
                        "com.daltonmaag/check/ufolint")

    _, ufo_path = empty_ufo_font

    assert_PASS(check(ufo_path),
                'with an empty UFO.')

    os.remove(os.path.join(ufo_path, "metainfo.plist"))
    assert_results_contain(check(ufo_path),
                           FAIL, "ufolint-fail",
                           'with maimed UFO.')


def test_check_required_fields(empty_ufo_font):
    check = CheckTester(ufo_sources_profile,
                        "com.daltonmaag/check/ufo-required-fields")

    ufo, _ = empty_ufo_font

    assert_results_contain(check(ufo),
                           FAIL, "missing-required-fields",
                           'with an empty UFO.')

    ufo.info.unitsPerEm = 1000
    ufo.info.ascender = 800
    ufo.info.descender = -200
    ufo.info.xHeight = 500
    ufo.info.capHeight = 700
    ufo.info.familyName = "Test"

    assert_PASS(check(ufo),
                'with an almost empty UFO.')


def test_check_recommended_fields(empty_ufo_font):
    check = CheckTester(ufo_sources_profile,
                        "com.daltonmaag/check/ufo-recommended-fields")

    ufo, _ = empty_ufo_font

    assert_results_contain(check(ufo),
                           WARN, "missing-recommended-fields",
                           'with an empty UFO.')

    ufo.info.postscriptUnderlineThickness = 1000
    ufo.info.postscriptUnderlinePosition = 800
    ufo.info.versionMajor = -200
    ufo.info.versionMinor = 500
    ufo.info.styleName = "700"
    ufo.info.copyright = "Test"
    ufo.info.openTypeOS2Panose = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

    assert_PASS(check(ufo),
                'with an almost empty UFO.')


def test_check_unnecessary_fields(empty_ufo_font):
    check = CheckTester(ufo_sources_profile,
                        "com.daltonmaag/check/ufo-unnecessary-fields")

    ufo, _ = empty_ufo_font

    assert_PASS(check(ufo),
                'with an empty UFO.')

    ufo.info.openTypeNameUniqueID = "aaa"
    ufo.info.openTypeNameVersion = "1.000"
    ufo.info.postscriptUniqueID = -1
    ufo.info.year = 2018

    assert_results_contain(check(ufo),
                           WARN, "unnecessary-fields",
                           'with unnecessary fields in UFO.')



# tests/profiles/universal_test.py

import io
from unittest.mock import patch, MagicMock

from fontTools.ttLib import TTFont
import pytest
from requests.exceptions import ConnectionError

from fontbakery.checkrunner import INFO, WARN, FAIL, PASS, SKIP
from fontbakery.codetesting import (assert_PASS,
                                    assert_SKIP,
                                    assert_results_contain,
                                    CheckTester,
                                    TEST_FILE)
from fontbakery.profiles import universal as universal_profile
from fontbakery.profiles.universal import is_up_to_date


@pytest.fixture
def montserrat_ttFonts():
    paths = [
        TEST_FILE("montserrat/Montserrat-Black.ttf"),
        TEST_FILE("montserrat/Montserrat-BlackItalic.ttf"),
        TEST_FILE("montserrat/Montserrat-Bold.ttf"),
        TEST_FILE("montserrat/Montserrat-BoldItalic.ttf"),
        TEST_FILE("montserrat/Montserrat-ExtraBold.ttf"),
        TEST_FILE("montserrat/Montserrat-ExtraBoldItalic.ttf"),
        TEST_FILE("montserrat/Montserrat-ExtraLight.ttf"),
        TEST_FILE("montserrat/Montserrat-ExtraLightItalic.ttf"),
        TEST_FILE("montserrat/Montserrat-Italic.ttf"),
        TEST_FILE("montserrat/Montserrat-Light.ttf"),
        TEST_FILE("montserrat/Montserrat-LightItalic.ttf"),
        TEST_FILE("montserrat/Montserrat-Medium.ttf"),
        TEST_FILE("montserrat/Montserrat-MediumItalic.ttf"),
        TEST_FILE("montserrat/Montserrat-Regular.ttf"),
        TEST_FILE("montserrat/Montserrat-SemiBold.ttf"),
        TEST_FILE("montserrat/Montserrat-SemiBoldItalic.ttf"),
        TEST_FILE("montserrat/Montserrat-Thin.ttf"),
        TEST_FILE("montserrat/Montserrat-ThinItalic.ttf")
    ]
    return [TTFont(path) for path in paths]


cabin_fonts = [
    TEST_FILE("cabin/Cabin-BoldItalic.ttf"),
    TEST_FILE("cabin/Cabin-Bold.ttf"),
    TEST_FILE("cabin/Cabin-Italic.ttf"),
    TEST_FILE("cabin/Cabin-MediumItalic.ttf"),
    TEST_FILE("cabin/Cabin-Medium.ttf"),
    TEST_FILE("cabin/Cabin-Regular.ttf"),
    TEST_FILE("cabin/Cabin-SemiBoldItalic.ttf"),
    TEST_FILE("cabin/Cabin-SemiBold.ttf")
]

cabin_condensed_fonts = [
    TEST_FILE("cabincondensed/CabinCondensed-Regular.ttf"),
    TEST_FILE("cabincondensed/CabinCondensed-Medium.ttf"),
    TEST_FILE("cabincondensed/CabinCondensed-Bold.ttf"),
    TEST_FILE("cabincondensed/CabinCondensed-SemiBold.ttf")
]

@pytest.fixture
def cabin_ttFonts():
    return [TTFont(path) for path in cabin_fonts]

@pytest.fixture
def cabin_condensed_ttFonts():
    return [TTFont(path) for path in cabin_condensed_fonts]


def test_check_valid_glyphnames():
    """ Glyph names are all valid? """
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/valid_glyphnames")

    # We start with a good font file:
    ttFont = TTFont(TEST_FILE("nunito/Nunito-Regular.ttf"))
    assert_PASS(check(ttFont))

    # There used to be a 31 char max-length limit.
    # This was considered good:
    ttFont.glyphOrder[2] = "a" * 31
    assert_PASS(check(ttFont))

    # And this was considered bad:
    legacy_too_long = "a" * 32
    ttFont.glyphOrder[2] = legacy_too_long
    message = assert_results_contain(check(ttFont),
                                     WARN, 'legacy-long-names')
    assert legacy_too_long in message

    # Nowadays, it seems most implementations can deal with
    # up to 63 char glyph names:
    good_name = "b" * 63
    bad_name1 = "a" * 64
    bad_name2 = "3cents"
    bad_name3 = ".threecents"
    ttFont.glyphOrder[2] = bad_name1
    ttFont.glyphOrder[3] = bad_name2
    ttFont.glyphOrder[4] = bad_name3
    ttFont.glyphOrder[5] = good_name
    message = assert_results_contain(check(ttFont),
                                     FAIL, 'found-invalid-names')
    assert good_name not in message
    assert bad_name1 in message
    assert bad_name2 in message
    assert bad_name3 in message

    # TrueType fonts with a format 3.0 post table contain
    # no glyph names, so the check must be SKIP'd in that case.
    #
    # Upgrade to post format 3.0 and roundtrip data to update TTF object.
    ttFont = TTFont(TEST_FILE("nunito/Nunito-Regular.ttf"))
    ttFont["post"].formatType = 3.0
    _file = io.BytesIO()
    _file.name = ttFont.reader.file.name
    ttFont.save(_file)
    ttFont = TTFont(_file)
    assert_SKIP(check(ttFont))


def test_check_unique_glyphnames():
    """ Font contains unique glyph names? """
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/unique_glyphnames")

    ttFont = TTFont(TEST_FILE("nunito/Nunito-Regular.ttf"))
    assert_PASS(check(ttFont))

    # Fonttools renames duplicate glyphs with #1, #2, ... on load.
    # Code snippet from https://github.com/fonttools/fonttools/issues/149.
    glyph_names = ttFont.getGlyphOrder()
    glyph_names[2] = glyph_names[3]

    # Load again, we changed the font directly.
    ttFont = TTFont(TEST_FILE("nunito/Nunito-Regular.ttf"))
    ttFont.setGlyphOrder(glyph_names)
    ttFont['post']  # Just access the data to make fonttools generate it.
    _file = io.BytesIO()
    _file.name = ttFont.reader.file.name
    ttFont.save(_file)
    ttFont = TTFont(_file)
    message = assert_results_contain(check(ttFont),
                                     FAIL, "duplicated-glyph-names")
    assert "space" in message

    # Upgrade to post format 3.0 and roundtrip data to update TTF object.
    ttFont = TTFont(TEST_FILE("nunito/Nunito-Regular.ttf"))
    ttFont.setGlyphOrder(glyph_names)
    ttFont["post"].formatType = 3.0
    _file = io.BytesIO()
    _file.name = ttFont.reader.file.name
    ttFont.save(_file)
    ttFont = TTFont(_file)
    assert_SKIP(check(ttFont))


def DISABLED_test_check_glyphnames_max_length():
    """ Check that glyph names do not exceed max length. """
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/glyphnames_max_length")
    import defcon
    import ufo2ft

    # TTF
    test_font = defcon.Font(TEST_FILE("test.ufo"))
    ttFont = ufo2ft.compileTTF(test_font)
    assert_PASS(check(ttFont))

    test_glyph = defcon.Glyph()
    test_glyph.unicode = 0x1234
    test_glyph.name = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
                       "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
                       "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
    test_font.insertGlyph(test_glyph)
    ttFont = ufo2ft.compileTTF(test_font, useProductionNames=False)
    assert_results_contain(check(ttFont),
                           FAIL, "glyphname-too-long")


    ttFont = ufo2ft.compileTTF(test_font, useProductionNames=True)
    assert_PASS(check(ttFont))


    # Upgrade to post format 3.0 and roundtrip data to update TTF object.
    ttFont["post"].formatType = 3.0
    _file = io.BytesIO()
    _file.name = ttFont.reader.file.name
    ttFont.save(_file)
    ttFont = TTFont(_file)
    message = assert_PASS(check(ttFont))
    assert "format 3.0" in message

    del test_font, ttFont, _file  # Prevent copypasta errors.

    # CFF
    test_font = defcon.Font(TEST_FILE("test.ufo"))
    ttFont = ufo2ft.compileOTF(test_font)
    assert_PASS(check(ttFont))

    test_font.insertGlyph(test_glyph)
    ttFont = ufo2ft.compileOTF(test_font, useProductionNames=False)
    assert_results_contain(check(ttFont),
                           FAIL, "glyphname-too-long")

    ttFont = ufo2ft.compileOTF(test_font, useProductionNames=True)
    assert_PASS(check(ttFont))


def test_check_ttx_roundtrip():
    """ Checking with fontTools.ttx """
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/ttx-roundtrip")

    font = TEST_FILE("mada/Mada-Regular.ttf")
    assert_PASS(check(font))

    # TODO: Can anyone show us a font file that fails ttx roundtripping?!
    #
    # font = TEST_FILE("...")
    # assert_results_contain(check(font),
    #                        FAIL, None) # FIXME: This needs a message keyword


def test_check_name_trailing_spaces():
    """ Name table entries must not have trailing spaces. """
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/name/trailing_spaces")

    # Our reference Cabin Regular is known to be good:
    ttFont = TTFont(TEST_FILE("cabin/Cabin-Regular.ttf"))
    assert_PASS(check(ttFont),
                'with a good font...')

    for i, entry in enumerate(ttFont['name'].names):
        good_string = ttFont['name'].names[i].toUnicode()
        bad_string = good_string + " "
        ttFont['name'].names[i].string = bad_string.encode(entry.getEncoding())
        assert_results_contain(check(ttFont),
                               FAIL, "trailing-space",
                               f'with a bad name table entry ({i}: "{bad_string}")...')

        #restore good entry before moving to the next one:
        ttFont['name'].names[i].string = good_string.encode(entry.getEncoding())


def test_check_family_single_directory():
    """ Fonts are all in the same directory. """
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/family/single_directory")
    same_dir = [
        TEST_FILE("cabin/Cabin-Thin.ttf"),
        TEST_FILE("cabin/Cabin-ExtraLight.ttf")
    ]
    multiple_dirs = [
        TEST_FILE("mada/Mada-Regular.ttf"),
        TEST_FILE("cabin/Cabin-ExtraLight.ttf")
    ]

    assert_PASS(check(same_dir),
                f'with same dir: {same_dir}')

    assert_results_contain(check(multiple_dirs),
                           FAIL, "single-directory",
                           f'with multiple dirs: {multiple_dirs}')


def test_check_ots():
    """ Checking with ots-sanitize. """
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/ots")

    sanitary_font = TEST_FILE("cabin/Cabin-Regular.ttf")
    assert_PASS(check(sanitary_font))

    bogus_font = TEST_FILE("README.txt")
    message = assert_results_contain(check(bogus_font),
                                     FAIL, "ots-sanitize-error")
    assert "invalid sfntVersion" in message
    assert "Failed to sanitize file!" in message


@pytest.mark.parametrize('installed, latest, result', [
    ("0.5.0",
     "0.5.0", True),
    ("0.5.1",
     "0.5.0", True),
    ("0.4.1",
     "0.5.0", False),
    ("0.3.4",
     "0.3.5", False),
    ("1.0.0",
     "1.0.1", False),
    ("2.0.0",
     "1.5.1", True),
    ("0.5.2.dev73+g8c9ebc0.d20181023",
     "0.5.1", True),
    ("0.5.2.dev73+g8c9ebc0.d20181023",
     "0.5.2", False),
    ("0.5.2.dev73+g8c9ebc0.d20181023",
     "0.5.3", False),
])
def test_is_up_to_date(installed, latest, result):
    assert is_up_to_date(installed, latest) is result


class MockDistribution:
    """Helper class to mock pip-api's Distribution class."""

    def __init__(self, version: str):
        self.name = "fontbakery"
        self.version = version

    def __repr__(self):
        return f"<Distribution(name='{self.name}', version='{self.version}')>"


# We don't want to make an actual GET request to PyPI.org, so we'll mock it.
# We'll also mock pip-api's 'installed_distributions' method.
@patch("pip_api.installed_distributions")
@patch("requests.get")
def test_check_fontbakery_version(mock_get, mock_installed):
    """Check if Font Bakery is up-to-date"""
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/fontbakery_version")

    # Any of the test fonts can be used here.
    # The check requires a 'font' argument but it doesn't do anything with it.
    font = TEST_FILE("nunito/Nunito-Regular.ttf")

    mock_response = MagicMock()
    mock_response.status_code = 200

    # Test the case of installed version being the same as PyPI's version.
    latest_ver = installed_ver = "0.1.0"
    mock_response.json.return_value = {"info": {"version": latest_ver}}
    mock_get.return_value = mock_response
    mock_installed.return_value = {"fontbakery": MockDistribution(installed_ver)}
    msg = assert_PASS(check(font), PASS)
    assert msg == "Font Bakery is up-to-date."

    # Test the case of installed version being newer than PyPI's version.
    installed_ver = "0.1.1"
    mock_installed.return_value = {"fontbakery": MockDistribution(installed_ver)}
    msg = assert_PASS(check(font), PASS)
    assert msg == "Font Bakery is up-to-date."

    # Test the case of installed version being older than PyPI's version.
    installed_ver = "0.0.1"
    mock_installed.return_value = {"fontbakery": MockDistribution(installed_ver)}
    msg = assert_results_contain(check(font), FAIL, "outdated-fontbakery")
    assert (f"Current Font Bakery version is {installed_ver},"
            f" while a newer {latest_ver} is already available.") in msg

    # Test the case of an unsuccessful response to the GET request.
    mock_response.status_code = 500
    mock_response.content = "500 Internal Server Error"
    msg = assert_results_contain(check(font), FAIL, "unsuccessful-request-500")
    assert "Request to PyPI.org was not successful" in msg

    # Test the case of the GET request failing due to a connection error.
    mock_get.side_effect = ConnectionError
    msg = assert_results_contain(check(font), FAIL, "connection-error")
    assert "Request to PyPI.org failed with this message" in msg


def test_check_fontbakery_version_live_apis():
    """Check if Font Bakery is up-to-date. (No API-mocking edition)"""
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/fontbakery_version")

    # Any of the test fonts can be used here.
    # The check requires a 'font' argument but it doesn't do anything with it.
    font = TEST_FILE("nunito/Nunito-Regular.ttf")

    # The check will make an actual request to PyPI.org,
    # and will query 'pip' to determine which version of 'fontbakery' is installed.
    # The check should PASS.
    msg = assert_PASS(check(font), PASS)
    assert msg == "Font Bakery is up-to-date."


def test_check_mandatory_glyphs():
    """ Font contains the first few mandatory glyphs (.null or NULL, CR and space)? """
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/mandatory_glyphs")

    ttFont = TTFont(TEST_FILE("nunito/Nunito-Regular.ttf"))
    assert_PASS(check(ttFont))

    import fontTools.subset
    subsetter = fontTools.subset.Subsetter()
    subsetter.populate(glyphs="n")  # Arbitrarily remove everything except n.
    subsetter.subset(ttFont)
    # It seems that fontTools automatically adds '.notdef' as the 1st glyph
    # but as an empty one, so the check should complain about it:
    assert_results_contain(check(ttFont),
                           WARN, 'empty')
    # FIXME: The assumption above should be double-checked!


    # TODO: assert_results_contain(check(ttFont),
    #                              WARN, 'codepoint')

    # TODO: assert_results_contain(check(ttFont),
    #                              WARN, 'first-glyph')


def test_check_whitespace_glyphs():
    """ Font contains glyphs for whitespace characters? """
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/whitespace_glyphs")

    # Our reference Mada Regular font is good here:
    ttFont = TTFont(TEST_FILE("mada/Mada-Regular.ttf"))
    assert_PASS(check(ttFont),
                'with a good font...')

    # We remove the nbsp char (0x00A0)
    for table in ttFont['cmap'].tables:
        if 0x00A0 in table.cmap:
            del table.cmap[0x00A0]

    # And make sure the problem is detected:
    assert_results_contain(check(ttFont),
                           FAIL, 'missing-whitespace-glyph-0x00A0',
                           'with a font lacking a nbsp (0x00A0)...')

    # restore original Mada Regular font:
    ttFont = TTFont(TEST_FILE("mada/Mada-Regular.ttf"))

    # And finally do the same with the space character (0x0020):
    for table in ttFont['cmap'].tables:
        if 0x0020 in table.cmap:
            del table.cmap[0x0020]
    assert_results_contain(check(ttFont),
                           FAIL, 'missing-whitespace-glyph-0x0020',
                           'with a font lacking a space (0x0020)...')


def test_check_whitespace_glyphnames():
    """ Font has **proper** whitespace glyph names? """
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/whitespace_glyphnames")

    def deleteGlyphEncodings(font, cp):
        """ This routine is used on to introduce errors
            in a given font by removing specific entries
            in the cmap tables.
        """
        for subtable in font['cmap'].tables:
            if subtable.isUnicode():
                subtable.cmap = {
                    codepoint: name for codepoint, name in subtable.cmap.items()
                    if codepoint != cp
                }

    def editCmap(font, cp, name):
        """ Corrupt the cmap by changing the glyph name
            for a given code point.
        """
        for subtable in font['cmap'].tables:
            if subtable.isUnicode():
                # Copy the map
                subtable.cmap = subtable.cmap.copy()
                # edit it
                subtable.cmap[cp] = name

    # Our reference Mada Regular font is good here:
    ttFont = TTFont(TEST_FILE("mada/Mada-Regular.ttf"))
    assert_PASS(check(ttFont),
                'with a good font...')


    value = ttFont["post"].formatType
    ttFont["post"].formatType = 3.0
    assert_SKIP(check(ttFont),
                'with post.formatType == 3.0 ...')

    # restore good value:
    ttFont["post"].formatType = value


    deleteGlyphEncodings(ttFont, 0x0020)
    assert_results_contain(check(ttFont),
                           FAIL, 'missing-0020',
                           'with missing glyph name for char 0x0020 ...')


    # restore the original font object in preparation for the next test-case:
    ttFont = TTFont(TEST_FILE("mada/Mada-Regular.ttf"))

    deleteGlyphEncodings(ttFont, 0x00A0)
    assert_results_contain(check(ttFont),
                           FAIL, 'missing-00a0',
                           'with missing glyph name for char 0x00A0 ...')


    # restore the original font object in preparation for the next test-case:
    ttFont = TTFont(TEST_FILE("mada/Mada-Regular.ttf"))

    # See https://github.com/googlefonts/fontbakery/issues/2624
    # nbsp is not Adobe Glyph List compliant.
    editCmap(ttFont, 0x00A0, "nbsp")
    assert_results_contain(check(ttFont),
                           FAIL, 'non-compliant-00a0',
                           'with not AGL-compliant glyph name "nbsp" for char 0x00A0...')


    editCmap(ttFont, 0x00A0, "nbspace")
    assert_results_contain(check(ttFont),
                           WARN, 'not-recommended-00a0',
                           'for naming 0x00A0 "nbspace"...')


    editCmap(ttFont, 0x0020, "foo")
    assert_results_contain(check(ttFont),
                           FAIL, 'non-compliant-0020',
                           'with not AGL-compliant glyph name "foo" for char 0x0020...')


    editCmap(ttFont, 0x0020, "uni0020")
    assert_results_contain(check(ttFont),
                           WARN, 'not-recommended-0020',
                           'for naming 0x0020 "uni0020"...')


    editCmap(ttFont, 0x0020, "space")
    editCmap(ttFont, 0x00A0, "uni00A0")
    assert_PASS(check(ttFont))


def test_check_whitespace_ink():
    """ Whitespace glyphs have ink? """
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/whitespace_ink")

    test_font = TTFont(TEST_FILE("nunito/Nunito-Regular.ttf"))
    assert_PASS(check(test_font))

    test_font["cmap"].tables[0].cmap[0x1680] = "a"
    assert_PASS(check(test_font),
                'because Ogham space mark does have ink.')

    test_font["cmap"].tables[0].cmap[0x0020] = "uni1E17"
    assert_results_contain(check(test_font),
                           FAIL, 'has-ink',
                           'for whitespace character having composites (with ink).')

    test_font["cmap"].tables[0].cmap[0x0020] = "scedilla"
    assert_results_contain(check(test_font),
                           FAIL, 'has-ink',
                           'for whitespace character having outlines (with ink).')

    import fontTools.pens.ttGlyphPen
    pen = fontTools.pens.ttGlyphPen.TTGlyphPen(test_font.getGlyphSet())
    pen.addComponent("space", (1, 0, 0, 1, 0, 0))
    test_font["glyf"].glyphs["uni200B"] = pen.glyph()
    assert_results_contain(check(test_font),
                           FAIL, 'has-ink', # should we give is a separate keyword? This looks wrong.
                           'for whitespace character having composites (without ink).')


def test_check_required_tables():
    """ Font contains all required tables ? """
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/required_tables")

    REQUIRED_TABLES = ["cmap", "head", "hhea", "hmtx",
                       "maxp", "name", "OS/2", "post"]

    OPTIONAL_TABLES = ["cvt ", "fpgm", "loca", "prep",
                       "VORG", "EBDT", "EBLC", "EBSC",
                       "BASE", "GPOS", "GSUB", "JSTF",
                       "gasp", "hdmx", "LTSH", "PCLT",
                       "VDMX", "vhea", "vmtx", "kern"]

    # Valid reference fonts, one for each format.
    # TrueType: Mada Regular
    # OpenType-CFF: SourceSansPro-Black
    # OpenType-CFF2: SourceSansVariable-Italic
    ttFont = TTFont(TEST_FILE("mada/Mada-Regular.ttf"))
    cff_font = TTFont(TEST_FILE("source-sans-pro/OTF/SourceSansPro-Black.otf"))
    cff2_font = TTFont(TEST_FILE("source-sans-pro/VAR/SourceSansVariable-Italic.otf"))

    # The TrueType font contains all required tables, so it must PASS the check.
    msg = assert_PASS(check(ttFont), 'with a good font...')
    assert msg == "Font contains all required tables."

    # Here we confirm that the check also yields INFO with
    # a list of table tags specific to the font.
    msg = assert_results_contain(check(ttFont), INFO, "optional-tables")
    for tag in {"loca", "GPOS", "GSUB"}:
        assert tag in msg

    # The OpenType-CFF font contains all required tables, so it must PASS the check.
    msg = assert_PASS(check(cff_font), 'with a good font...')
    assert msg == "Font contains all required tables."

    # Here we confirm that the check also yields INFO with
    # a list of table tags specific to the OpenType-CFF font.
    msg = assert_results_contain(check(cff_font), INFO, "optional-tables")
    for tag in {"BASE", "GPOS", "GSUB"}:
        assert tag in msg

    # The font must also contain the table that holds the outlines, "CFF " in this case.
    del cff_font.reader.tables["CFF "]
    msg = assert_results_contain(check(cff_font), FAIL, "required-tables")
    assert "CFF " in msg

    # The OpenType-CFF2 font contains all required tables, so it must PASS the check.
    msg = assert_PASS(check(cff2_font), 'with a good font...')
    assert msg == "Font contains all required tables."

    # Here we confirm that the check also yields INFO with
    # a list of table tags specific to the OpenType-CFF2 font.
    msg = assert_results_contain(check(cff2_font), INFO, "optional-tables")
    for tag in {"BASE", "GPOS", "GSUB"}:
        assert tag in msg

    # The font must also contain the table that holds the outlines, "CFF2" in this case.
    del cff2_font.reader.tables["CFF2"]
    msg = assert_results_contain(check(cff2_font), FAIL, "required-tables")
    assert "CFF2" in msg

    # The OT-CFF2 font is variable, so a "STAT" table is also required.
    # Here we confirm that the check fails when the "STAT" table is removed.
    del cff2_font.reader.tables["STAT"]
    msg = assert_results_contain(check(cff2_font), FAIL, "required-tables")
    assert "STAT" in msg

    # Here we also remove the "fvar" table from the OT-CFF2 font.
    # Without an "fvar" table the font is validated as if it were a stactic font,
    # leading the check to FAIL with a message about the lack of a "CFF " table.
    del cff2_font.reader.tables["fvar"]
    msg = assert_results_contain(check(cff2_font), FAIL, "required-tables")
    assert "CFF " in msg

    # Now we test the special cases for variable fonts:
    #
    # Note: A TTF with an fvar table but no STAT table
    #       is probably a GX font. For now we're OK with
    #       rejecting those by emitting a FAIL in this case.
    #
    # TODO: Maybe we could someday emit a more explicit
    #       message to the users regarding that...
    ttFont.reader.tables["fvar"] = "foo"
    msg = assert_results_contain(check(ttFont),
                                 FAIL, "required-tables",
                                 'with fvar but no STAT...')
    assert "STAT" in msg

    del ttFont.reader.tables["fvar"]
    ttFont.reader.tables["STAT"] = "foo"
    msg = assert_PASS(check(ttFont),
                      'with STAT on a non-variable font...')
    assert msg == "Font contains all required tables."

    # and finally remove what we've just added:
    del ttFont.reader.tables["STAT"]

    # Now we remove required tables one-by-one to validate the FAIL code-path:
    # The font must also contain the table that holds the outlines, "glyf" in this case.
    for required in REQUIRED_TABLES + ["glyf"]:
        ttFont = TTFont(TEST_FILE("mada/Mada-Regular.ttf"))
        if required in ttFont.reader.tables:
            del ttFont.reader.tables[required]
        msg = assert_results_contain(check(ttFont),
                                     FAIL, "required-tables",
                                     f'with missing mandatory table {required} ...')
        assert required in msg

    # Then, in preparation for the next step, we make sure
    # there's no optional table (by removing them all):
    for optional in OPTIONAL_TABLES:
        if optional in ttFont.reader.tables:
            del ttFont.reader.tables[optional]

    # Then re-insert them one by one to validate the INFO code-path:
    for optional in OPTIONAL_TABLES:
        ttFont.reader.tables[optional] = "foo"
        # and ensure that the second to last logged message is an
        # INFO status informing the user about it:
        msg = assert_results_contain(check(ttFont),
                                     INFO, "optional-tables",
                                     f'with optional table {required} ...')
        assert optional in msg

        # remove the one we've just inserted before trying the next one:
        del ttFont.reader.tables[optional]


def test_check_unwanted_tables():
    """ Are there unwanted tables ? """
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/unwanted_tables")

    unwanted_tables = [
        "FFTM", # FontForge
        "TTFA", # TTFAutohint
        "TSI0", # TSI* = VTT
        "TSI1",
        "TSI2",
        "TSI3",
        "TSI5",
        "prop", # FIXME: Why is this one unwanted?
    ]
    # Our reference Mada Regular font is good here:
    ttFont = TTFont(TEST_FILE("mada/Mada-Regular.ttf"))

    # So it must PASS the check:
    assert_PASS(check(ttFont),
                'with a good font...')

    # We now add unwanted tables one-by-one to validate the FAIL code-path:
    for unwanted in unwanted_tables:
        ttFont = TTFont(TEST_FILE("mada/Mada-Regular.ttf"))
        ttFont.reader.tables[unwanted] = "foo"
        assert_results_contain(check(ttFont),
                               FAIL, "unwanted-tables",
                               f'with unwanted table {unwanted} ...')


def test_glyph_has_ink():
    from fontbakery.utils import glyph_has_ink
    from fontTools.ttLib import TTFont

    print()  # so next line doesn't start with '.....'

    cff_test_font = TTFont(TEST_FILE("source-sans-pro/OTF/SourceSansPro-Regular.otf"))
    print('Test if CFF glyph with ink has ink')
    assert(glyph_has_ink(cff_test_font, '.notdef') is True)
    print('Test if CFF glyph without ink has ink')
    assert(glyph_has_ink(cff_test_font, 'space') is False)

    ttf_test_font = TTFont(TEST_FILE("source-sans-pro/TTF/SourceSansPro-Regular.ttf"))
    print('Test if TTF glyph with ink has ink')
    assert(glyph_has_ink(ttf_test_font, '.notdef') is True)
    print('Test if TTF glyph without ink has ink')
    assert(glyph_has_ink(ttf_test_font, 'space') is False)

    cff2_test_font = TTFont(TEST_FILE("source-sans-pro/VAR/SourceSansVariable-Roman.otf"))
    print('Test if CFF2 glyph with ink has ink')
    assert(glyph_has_ink(cff2_test_font, '.notdef') is True)
    print('Test if CFF2 glyph without ink has ink')
    assert(glyph_has_ink(cff2_test_font, 'space') is False)


mada_fonts = [
    TEST_FILE("mada/Mada-Black.ttf"),
    TEST_FILE("mada/Mada-ExtraLight.ttf"),
    TEST_FILE("mada/Mada-Medium.ttf"),
    TEST_FILE("mada/Mada-SemiBold.ttf"),
    TEST_FILE("mada/Mada-Bold.ttf"),
    TEST_FILE("mada/Mada-Light.ttf"),
    TEST_FILE("mada/Mada-Regular.ttf"),
]

@pytest.fixture
def mada_ttFonts():
    return [TTFont(path) for path in mada_fonts]


def test_check_family_win_ascent_and_descent(mada_ttFonts):
    """ Checking OS/2 usWinAscent & usWinDescent. """
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/family/win_ascent_and_descent")
    from fontbakery.profiles.shared_conditions import vmetrics

    # Our reference Mada Regular is know to be bad here.
    ttFont = TTFont(TEST_FILE("mada/Mada-Regular.ttf"))

    # But we fix it first to test the PASS code-path:
    vm = vmetrics(mada_ttFonts)
    ttFont['OS/2'].usWinAscent = vm['ymax']
    ttFont['OS/2'].usWinDescent = abs(vm['ymin'])
    assert_PASS(check(ttFont),
                'with a good font...')

    # Then we break it:
    ttFont['OS/2'].usWinAscent = 0 # FIXME: this should be bad as well: vm['ymax'] - 1
    ttFont['OS/2'].usWinDescent = abs(vm['ymin'])
    assert_results_contain(check(ttFont),
                           FAIL, 'ascent',
                           'with a bad OS/2.usWinAscent...')

    # and also this other way of breaking it:
    ttFont['OS/2'].usWinAscent = vm['ymax']
    ttFont['OS/2'].usWinDescent = 0 # FIXME: this should be bad as well: abs(vm['ymin']) - 1
    assert_results_contain(check(ttFont),
                           FAIL, 'descent',
                           'with a bad OS/2.usWinDescent...')


def test_check_os2_metrics_match_hhea():
    """ Checking OS/2 Metrics match hhea Metrics. """
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/os2_metrics_match_hhea")

    # Our reference Mada Regular is know to be good here.
    ttFont = TTFont(TEST_FILE("mada/Mada-Regular.ttf"))

    assert_PASS(check(ttFont),
                'with a good font...')

    # Now we break it:
    correct = ttFont['hhea'].ascent
    ttFont['OS/2'].sTypoAscender = correct + 1
    assert_results_contain(check(ttFont),
                           FAIL, 'ascender',
                           'with a bad OS/2.sTypoAscender font...')

    # Restore good value:
    ttFont['OS/2'].sTypoAscender = correct

    # And break it again, now on sTypoDescender value:
    correct = ttFont['hhea'].descent
    ttFont['OS/2'].sTypoDescender = correct + 1
    assert_results_contain(check(ttFont),
                           FAIL, 'descender',
                           'with a bad OS/2.sTypoDescender font...')


def test_check_family_vertical_metrics(montserrat_ttFonts):
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/family/vertical_metrics")

    assert_PASS(check(montserrat_ttFonts),
                'with multiple good fonts...')

    montserrat_ttFonts[0]['OS/2'].usWinAscent = 4000
    assert_results_contain(check(montserrat_ttFonts),
                           FAIL, 'usWinAscent-mismatch',
                           'with one bad font that has one different vertical metric val...')

    # TODO: Also test these code-paths:
    # FAIL, 'mismatch-<other values>'
    # FAIL, 'lacks-OS/2'
    # FAIL, 'lacks-hhea'


def test_check_superfamily_vertical_metrics(montserrat_ttFonts, cabin_ttFonts, cabin_condensed_ttFonts):
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/superfamily/vertical_metrics")

    assert_PASS(check([], {"superfamily_ttFonts": [cabin_ttFonts,
                                                   cabin_condensed_ttFonts]}),
                'with multiple good families...')

    assert_results_contain(check([], {"superfamily_ttFonts": [cabin_ttFonts,
                                                              montserrat_ttFonts]}),
                           WARN, "superfamily-vertical-metrics",
                           'with families that diverge on vertical metric values...')


def test_check_STAT_strings():
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/STAT_strings")

    good = TTFont(TEST_FILE("ibmplexsans-vf/IBMPlexSansVar-Roman.ttf"))
    assert_PASS(check(good))

    bad = TTFont(TEST_FILE("ibmplexsans-vf/IBMPlexSansVar-Italic.ttf"))
    assert_results_contain(check(bad),
                           FAIL, "bad-italic")


def test_check_rupee():
    """ Ensure indic fonts have the Indian Rupee Sign glyph. """
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/rupee")

    # FIXME: This should be possible:
    #          The `assert_SKIP` helper should be able to detect when a
    #          check skips due to unmet @conditions.
    #          For now it only detects explicit SKIP messages instead.
    #
    # non_indic = TTFont(TEST_FILE("mada/Mada-Regular.ttf"))
    # assert_SKIP(check(non_indic),
    #             "with a non-indic font.")
    #
    #        But for now we have to do this:
    #
    from fontbakery.profiles.shared_conditions import is_indic_font
    print("Ensure the check will SKIP when dealing with a non-indic font...")
    non_indic = TTFont(TEST_FILE("mada/Mada-Regular.ttf"))
    assert is_indic_font(non_indic) == False


    # This one is good:
    ttFont = TTFont(TEST_FILE("indic-font-with-rupee-sign/NotoSerifDevanagari-Regular.ttf"))
    assert_PASS(check(ttFont),
                "with a sample font that has the Indian Rupee Sign.")

    # But this one lacks the glyph:
    ttFont = TTFont(TEST_FILE("indic-font-without-rupee-sign/NotoSansOlChiki-Regular.ttf"))
    assert_results_contain(check(ttFont),
                           FAIL, "missing-rupee",
                           "with a sample font missing it.")


def test_check_unreachable_glyphs():
    """Check font contains no unreachable glyphs."""
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/unreachable_glyphs")

    font = TEST_FILE("noto_sans_tamil_supplement/NotoSansTamilSupplement-Regular.ttf")
    assert_PASS(check(font))

    font = TEST_FILE("merriweather/Merriweather-Regular.ttf")
    message = assert_results_contain(check(font), WARN, "unreachable-glyphs")
    for glyph in [
        "Gtilde",
        "eight.dnom",
        "four.dnom",
        "three.dnom",
        "two.dnom",
        "i.dot",
        "five.numr",
        "seven.numr",
        "bullet.cap",
        "periodcentered.cap",
        "ampersand.sc",
        "I.uc",
    ]:
        assert glyph in message

    for glyph in [
        "caronvertical",
        "acute.cap",
        "breve.cap",
        "caron.cap",
        "circumflex.cap",
        "dotaccent.cap",
        "dieresis.cap",
        "grave.cap",
        "hungarumlaut.cap",
        "macron.cap",
        "ring.cap",
        "tilde.cap",
        "breve.r",
        "breve.rcap",
    ]:
        assert glyph not in message


def test_check_contour_count(montserrat_ttFonts):
    """Check glyphs contain the recommended contour count"""
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/contour_count")
    # TODO: FAIL, "lacks-cmap"

    for ttFont in montserrat_ttFonts:
        # Montserrat which was used to assemble the glyph data,
        # so, it should be good, except for that softhyphen...
        # TODO: how can we test PASS then?
        assert_results_contain(check(ttFont),
                               WARN, 'softhyphen')

    # Lets swap the glyf 'a' (2 contours) with glyf 'c' (1 contour)
    for ttFont in montserrat_ttFonts:
        ttFont['glyf']['a'] = ttFont['glyf']['c']
        assert_results_contain(check(ttFont),
                               WARN, 'contour-count')


def test_check_cjk_chws_feature():
    """Does the font contain chws and vchw features?"""
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/cjk_chws_feature")

    cjk_font = TEST_FILE("cjk/SourceHanSans-Regular.otf")
    ttFont = TTFont(cjk_font)
    assert_results_contain(check(ttFont),
                           WARN, "missing-chws-feature",
                           'for Source Han Sans')

    assert_results_contain(check(ttFont),
                           WARN, "missing-vchw-feature",
                           'for Source Han Sans')

    # Insert them.
    from fontTools.ttLib.tables.otTables import FeatureRecord
    chws = FeatureRecord()
    chws.FeatureTag = "chws"
    vchw = FeatureRecord()
    vchw.FeatureTag = "vchw"
    ttFont["GPOS"].table.FeatureList.FeatureRecord.extend([chws, vchw])

    assert_PASS(check(ttFont))


def test_check_designspace_has_sources():
    """See if we can actually load the source files."""
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/designspace_has_sources")

    designspace = TEST_FILE("stupidfont/Stupid Font.designspace")
    assert_PASS(check(designspace))

    # TODO: FAIL, 'no-sources'


def test_check_designspace_has_default_master():
    """Ensure a default master is defined."""
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/designspace_has_default_master")

    designspace = TEST_FILE("stupidfont/Stupid Font.designspace")
    assert_PASS(check(designspace))

    # TODO: FAIL, 'not-found'


def test_check_designspace_has_consistent_glyphset():
    """Check consistency of glyphset in a designspace file."""
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/designspace_has_consistent_glyphset")

    designspace = TEST_FILE("stupidfont/Stupid Font.designspace")
    assert_results_contain(check(designspace),
                           FAIL, 'inconsistent-glyphset')

    # TODO: Fix it and ensure it passes the check


def test_check_designspace_has_consistent_codepoints():
    """Check codepoints consistency in a designspace file."""
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/designspace_has_consistent_codepoints")

    designspace = TEST_FILE("stupidfont/Stupid Font.designspace")
    assert_results_contain(check(designspace),
                           FAIL, 'inconsistent-codepoints')

    # TODO: Fix it and ensure it passes the check


def test_check_transformed_components():
    """Ensure component transforms do not perform scaling or rotation."""
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/transformed_components")

    font = TEST_FILE("cabin/Cabin-Regular.ttf")
    assert_PASS(check(font),
                "with a good font...")

    # DM Sans v1.100 had some transformed components
    font = TEST_FILE("dm-sans-v1.100/DMSans-Regular.ttf")
    assert_results_contain(check(font),
                           FAIL, 'transformed-components')


def test_check_dotted_circle():
    """Ensure dotted circle glyph is present and can attach marks."""
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/dotted_circle")

    font = TEST_FILE("mada/Mada-Regular.ttf")
    assert_PASS(check(font),
                "with a good font...")

    font = TEST_FILE("cabin/Cabin-Regular.ttf")
    assert_results_contain(check(font),
                           WARN, "missing-dotted-circle")

    font = TEST_FILE("broken_markazitext/MarkaziText-VF.ttf")
    assert_results_contain(check(font),
                           FAIL, "unattached-dotted-circle-marks")


def test_check_gpos7():
    """Check if font contains any GPOS 7 lookups
    which are not widely supported."""
    check = CheckTester(universal_profile,
                        "com.google.fonts/check/gpos7")

    font = TEST_FILE("mada/Mada-Regular.ttf")
    assert_PASS(check(font),
                "with a good font...")

    font = TEST_FILE("notosanskhudawadi/NotoSansKhudawadi-Regular.ttf")
    assert_results_contain(check(font),
                           WARN, "has-gpos7")


def test_freetype_rasterizer():
    """Ensure that the font can be rasterized by FreeType."""
    check = CheckTester(universal_profile, "com.adobe.fonts/check/freetype_rasterizer")

    font = TEST_FILE("cabin/Cabin-Regular.ttf")
    msg = assert_PASS(check(font), "with a good font...")
    assert msg == "Font can be rasterized by FreeType."

    font = TEST_FILE("ancho/AnchoGX.ttf")
    msg = assert_results_contain(check(font), FAIL, "freetype-crash")
    assert "FT_Exception:  (too many function definitions)" in msg

    font = TEST_FILE("rubik/Rubik-Italic.ttf")
    msg = assert_results_contain(check(font), FAIL, "freetype-crash")
    assert "FT_Exception:  (stack overflow)" in msg


# setup.py

# Copyright 2017 The Font Bakery Authors. 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.
#
# See AUTHORS.txt for the list of Authors and LICENSE.txt for the License.
import os
from setuptools import setup

try:
    readme = open('README.md').read()
except IOError:
    readme = ''

setup(
    name="fontbakery",
    use_scm_version={"write_to": "Lib/fontbakery/_version.py"},
    url='https://github.com/googlefonts/fontbakery/',
    description='Well designed Font QA tool, written in Python 3',
    long_description=readme,
    long_description_content_type='text/markdown',
    author=('Font Bakery authors and contributors:'
            ' Dave Crossland,'
            ' Felipe Sanches,'
            ' Lasse Fister,'
            ' Marc Foley,'
            ' Nikolaus Waxweiler,'
            ' Chris Simpkins,'
            ' Jens Kutilek,'
            ' Vitaly Volkov'),
    author_email='dave@lab6.com',
    package_dir={'': 'Lib'},
    packages=['fontbakery',
              'fontbakery.reporters',
              'fontbakery.profiles',
              'fontbakery.commands',
              'fontbakery.sphinx_extensions'
              ],
    package_data={'fontbakery': ['data/*.cache']},
    classifiers=[
        'Environment :: Console',
        'Intended Audience :: Developers',
        'License :: OSI Approved :: Apache Software License',
        'Operating System :: OS Independent',
        'Programming Language :: Python :: 3',
        'Programming Language :: Python :: 3.7',
        'Programming Language :: Python :: 3 :: Only',
    ],
    python_requires='>=3.7',
    setup_requires=['setuptools_scm'],
    install_requires=[
        'axisregistry',
        'beautifulsoup4',
        'beziers',
        'cmarkgfm',
        'collidoscope>=0.4.1', # 0.4.0 had a bug that failed to detect
                               # an ïï collision on Nunito Black.
                               # (see https://github.com/googlefonts/fontbakery/issues/3554)
        'defcon',
        'dehinter>=3.1.0', # 3.1.0 added dehinter.font.hint function
        'fontTools[ufo,lxml,unicode]>=3.34',  # 3.34 fixed some CFF2 issues, including calcBounds
        'font-v',
        'gflanguages>=0.3.0', # there was an api simplification/update on v0.3.0 (see https://github.com/googlefonts/gflanguages/pull/7)
        'glyphsets>=0.5.0',
        'lxml',
        'opentype-sanitizer>=7.1.9',  # 7.1.9 fixes caret value format = 3 bug
                                      # (see https://github.com/khaledhosny/ots/pull/182)
        'opentypespec',
        'pip-api',
        'protobuf>=3.7.0',  # 3.7.0 fixed a bug on parsing some METADATA.pb files
                            # (see https://github.com/googlefonts/fontbakery/issues/2200)
        'PyYAML',
        'requests',
        'rich',
        'stringbrewer',
        'toml',
        'ufolint',
        'ufo2ft>=2.25.2',  # 2.25.2 updated the script lists for Unicode 14.0
        'unicodedata2',
        'vharfbuzz',
    ],
    extras_require={
        'docs': [
            'recommonmark',
            'sphinx >= 1.4',
            'sphinx_rtd_theme',
        ],
        'freetype': [
            'freetype-py',
        ],
    },
    entry_points={
        'console_scripts': ['fontbakery=fontbakery.cli:main'],
    },
# TODO: review this and make it cross-platform:
#    data_files=[
#        ('/etc/bash_completion.d', ['snippets/fontbakery.bash-completion']),
#    ]
)


# tests/test_parse.py

import pytest
from fontbakery.parse import _style_parse, instance_parse


def test_name():
    style = _style_parse("Extra-Bold")
    assert style.name == "ExtraBold"

    style = _style_parse("Extra Bold")
    assert style.name == "ExtraBold"

    style = _style_parse("Extra Bold Italic")
    assert style.name == "ExtraBold Italic"

    style = _style_parse("wt400 It")
    assert style.name == "Italic"

    style = _style_parse("Regular Italic")
    assert style.name == "Italic"

    style = _style_parse("Thn It")
    assert style.name == "Thin Italic"

    style = _style_parse("Bold Oblique")
    assert style.name == "Bold Italic"


def test_fvar_coordinates():
    style = instance_parse("Regular")
    assert style.coordinates == {"wght": 400.0}

    style = instance_parse("Black Italic")
    assert style.coordinates == {"wght": 900.0}

    style = instance_parse("Regular")
    assert style.coordinates == {"wght": 400.0}

    style = instance_parse("Italic")
    assert style.coordinates == {"wght": 400.0}


def test_unparsable_tokens():
    # https://github.com/googlefonts/fontbakery/issues/2701#issuecomment-585870359
    style = instance_parse("144 G100 Thin")
    assert style.unparsable_tokens == ["144", "G100"]

    style = instance_parse("Loud Thin Italic")
    assert style.unparsable_tokens == ["Loud"]


def test_token_order():
    style = instance_parse("Italic Black")
    assert style.raw_token_order == ["ital", "wght"]
    assert style.expected_token_order == ["wght", "ital"]


# tests/test_utils.py

import pytest

from fontbakery.utils import (
    bullet_list,
    can_shape,
    pretty_print_list,
    text_flow,
    unindent_and_unwrap_rationale,
)
from fontTools.ttLib import TTFont
from fontbakery.codetesting import portable_path


def test_text_flow():
    assert text_flow("") == ""

    assert text_flow("Testing") == "Testing"

    assert text_flow("One Two Three") == "One Two Three"

    assert text_flow("One Two Three",
                     width=5) == ("One\n"
                                  "Two\n"
                                  "Three")

    assert text_flow("One Two Three",
                     width=6,
                     space_padding=True) == ("One   \n"
                                             "Two   \n"
                                             "Three ")

    assert text_flow("One Two Three",
                     width=7,
                     space_padding=True) == ("One Two\n"
                                             "Three  ")

    assert text_flow("One Two Three",
                     width=9,
                     left_margin=2,
                     space_padding=True) == ("  One Two\n"
                                             "  Three  ")

    assert text_flow("One Two Three",
                     width=7,
                     left_margin=1,
                     space_padding=True) == (" One   \n"
                                             " Two   \n"
                                             " Three ")

    assert text_flow("One Two Three",
                     width=9,
                     left_margin=1,
                     right_margin=1,
                     space_padding=True) == (" One Two \n"
                                             " Three   ")

    assert text_flow("One Two Three",
                     width=8,
                     left_margin=1,
                     right_margin=1,
                     space_padding=True) == (" One    \n"
                                             " Two    \n"
                                             " Three  ")

    assert text_flow("One Two Three Four",
                     width=7,
                     left_margin=1,
                     right_margin=1,
                     space_padding=True) == (" One   \n"
                                             " Two   \n"
                                             " Three \n"
                                             " Four  ")

    assert text_flow("One Two Three Four",
                     width=6,
                     left_margin=1,
                     right_margin=1,
                     space_padding=True) == (" One  \n"
                                             " Two  \n"
                                             " Thre \n"
                                             " e    \n"
                                             " Four ")

# FIXME!
#    assert text_flow("One Two Three",
#                     width=12,
#                     left_margin=6,
#                     first_line_indent=-5,
#                     space_padding=True) == (     " One   \n"
#                                             "      Two   \n"
#                                             "      Three ")


def test_can_shape():
    font = TTFont(portable_path(
        "data/test/source-sans-pro/OTF/SourceSansPro-Regular.otf"
    ))
    assert can_shape(font, "ABC")
    assert not can_shape(font, "こんにちは")


def test_unindent_and_unwrap_rationale():
    rationale = """
        This is a line that is very long, so long in fact that it must be hard wrapped
        because it is longer than 88 lines, including the two 4-space indents.

        This is a new paragraph. This paragraph is also too long to fit within the
        maximum width, so it must be hard wrapped.
        This is a new line that was NOT soft-wrapped, so it will end up appendend to
        the previous line.⏎
        This is yet another line, but this one was soft-wrapped (Shift+Return), which
        means that it will not be appendend to the end of the previous line.

        This is the last paragraph.
    """
    expected_rationale = (
        "\n"
        "This is a line that is very long, so long in fact that it must be hard wrapped"
        " because it is longer than 88 lines, including the two 4-space indents.\n"
        "\n"
        "This is a new paragraph. This paragraph is also too long to fit within the"
        " maximum width, so it must be hard wrapped."
        " This is a new line that was NOT soft-wrapped, so it will end up appendend to"
        " the previous line.\n"
        "This is yet another line, but this one was soft-wrapped (Shift+Return), which"
        " means that it will not be appendend to the end of the previous line.\n"
        "\n"
        "This is the last paragraph."
        "\n"
    )
    assert unindent_and_unwrap_rationale(rationale) == expected_rationale


def _make_values(count: int) -> list:
    """Helper method that returns a list with 'count' number of items"""
    return [f"item {i}" for i in range(1, count + 1)]


@pytest.mark.parametrize('values, expected_str', [
    # (_make_values(0), ""),  # causes IndexError
    (_make_values(1), "item 1"),
    (_make_values(2), "item 1 and item 2"),
    (_make_values(3), "item 1, item 2 and item 3"),
    (_make_values(4), "item 1, item 2, item 3 and item 4"),
    (_make_values(12), ("item 1, item 2, item 3, item 4, item 5, item 6, item 7,"
                        " item 8, item 9, item 10, item 11 and item 12")),
])
def test_pretty_print_list_full(values, expected_str):
    config = {'full_lists': True}
    assert pretty_print_list(config, values) == expected_str
    assert pretty_print_list(config, values, shorten=3) == expected_str
    assert pretty_print_list(
        config, values, sep=" + ") == expected_str.replace(", ", " + ")
    assert pretty_print_list(
        config, values, glue="&") == expected_str.replace("and", "&")


MORE_MSG = "\n\nUse -F or --full-lists to disable shortening of long lists."


@pytest.mark.parametrize('values, shorten, expected_str', [
    (_make_values(1), None, "item 1"),
    (_make_values(1), 0, "item 1"),
    (_make_values(1), 1, "item 1"),
    (_make_values(1), 2, "item 1"),
    (_make_values(2), None, "item 1 and item 2"),
    (_make_values(2), 0, "item 1 and item 2"),
    (_make_values(2), 1, "item 1 and item 2"),
    (_make_values(2), 2, "item 1 and item 2"),
    (_make_values(2), 3, "item 1 and item 2"),
    (_make_values(3), None, "item 1, item 2 and item 3"),
    (_make_values(3), 1, "item 1, item 2 and item 3"),
    (_make_values(3), 2, "item 1, item 2 and item 3"),
    (_make_values(3), 3, "item 1, item 2 and item 3"),
    (_make_values(3), 4, "item 1, item 2 and item 3"),
    (_make_values(4), None, "item 1, item 2, item 3 and item 4"),
    (_make_values(4), 0, "item 1, item 2, item 3 and item 4"),
    (_make_values(4), 1, f"item 1 and 3 more.{MORE_MSG}"),
    (_make_values(4), 2, "item 1, item 2, item 3 and item 4"),
    (_make_values(4), 3, "item 1, item 2, item 3 and item 4"),
    (_make_values(4), 4, "item 1, item 2, item 3 and item 4"),
    (_make_values(4), 5, "item 1, item 2, item 3 and item 4"),
    (_make_values(5), None, "item 1, item 2, item 3, item 4 and item 5"),
    (_make_values(5), 0, "item 1, item 2, item 3, item 4 and item 5"),
    (_make_values(5), 1, f"item 1 and 4 more.{MORE_MSG}"),
    (_make_values(5), 2, f"item 1, item 2 and 3 more.{MORE_MSG}"),
    (_make_values(5), 3, "item 1, item 2, item 3, item 4 and item 5"),
    (_make_values(5), 4, "item 1, item 2, item 3, item 4 and item 5"),
    (_make_values(5), 5, "item 1, item 2, item 3, item 4 and item 5"),
    (_make_values(5), 6, "item 1, item 2, item 3, item 4 and item 5"),
    (_make_values(12), None, ("item 1, item 2, item 3, item 4, item 5, item 6, item 7,"
                              " item 8, item 9, item 10, item 11 and item 12")),
    (_make_values(12), 0, ("item 1, item 2, item 3, item 4, item 5, item 6, item 7,"
                           " item 8, item 9, item 10, item 11 and item 12")),
    (_make_values(12), 1, f"item 1 and 11 more.{MORE_MSG}"),
    (_make_values(12), 6, ("item 1, item 2, item 3, item 4, item 5, item 6"
                           f" and 6 more.{MORE_MSG}")),
    (_make_values(13), None, ("item 1, item 2, item 3, item 4, item 5, item 6, item 7,"
                              f" item 8, item 9, item 10 and 3 more.{MORE_MSG}")),
    (_make_values(13), 0, ("item 1, item 2, item 3, item 4, item 5, item 6, item 7,"
                           " item 8, item 9, item 10, item 11, item 12 and item 13")),
    (_make_values(13), 1, f"item 1 and 12 more.{MORE_MSG}"),
    (_make_values(13), 6, ("item 1, item 2, item 3, item 4, item 5, item 6"
                           f" and 7 more.{MORE_MSG}")),
])
def test_pretty_print_list_shorten(values, shorten, expected_str):
    config = {}
    if shorten is not None:
        assert pretty_print_list(config, values, shorten=shorten) == expected_str
    else:
        assert pretty_print_list(config, values) == expected_str


@pytest.mark.parametrize('values, expected_str', [
    (_make_values(1), "\t- item 1"),
    (_make_values(2), "\t- item 1 \n\n\t- And item 2"),
    (_make_values(3), "\t- item 1\n\n\t- item 2 \n\n\t- And item 3"),
    (_make_values(4), "\t- item 1\n\n\t- item 2\n\n\t- item 3 \n\n\t- And item 4"),
])
def test_bullet_list(values, expected_str):
    config = {'full_lists': True}
    assert bullet_list(config, values) == expected_str
    assert bullet_list(config, values, bullet="*") == expected_str.replace("-", "*")
    assert bullet_list(config, values, indentation="") == expected_str.replace("\t", "")


# tests/test_codetesting.py
import os

from glyphsLib import GSFont
import pytest

from fontbakery.codetesting import (
    assert_PASS,
    assert_results_contain,
    assert_SKIP,
    GLYPHSAPP_TEST_FILE,
    PATH_TEST_DATA,
    portable_path,
    TEST_FILE,
)
from fontbakery.message import Message
from fontbakery.status import PASS, FAIL, WARN, ERROR, INFO, SKIP, DEBUG


def test_portable_path():
    test_path = "dir/subdir/file"
    assert portable_path(test_path) == f"{os.sep}".join(test_path.split("/"))


def test_TEST_FILE():
    file_path = "dir/file"
    assert TEST_FILE(file_path) == f"{PATH_TEST_DATA}{file_path}"


def test_GLYPHSAPP_TEST_FILE():
    glyphs_filename = "Comfortaa.glyphs"
    gfile = GLYPHSAPP_TEST_FILE(glyphs_filename)
    assert isinstance(gfile, GSFont)


def test_assert_SKIP_success(capsys):
    skip_msg = "SKIP message"
    skip_reason = "SKIP reason"
    results = [
        (PASS,),
        (SKIP, skip_msg),
    ]
    assert assert_SKIP(results, skip_reason) == skip_msg

    captured = capsys.readouterr()
    assert captured.out == f"Test SKIP {skip_reason}\n"


def test_assert_SKIP_failure(capsys):
    pass_msg = "PASS message"
    skip_reason = "SKIP reason"
    results = [
        (SKIP,),
        (PASS, pass_msg),
    ]
    with pytest.raises(AssertionError):
        assert_SKIP(results, skip_reason)

    captured = capsys.readouterr()
    assert captured.out == f"Test SKIP {skip_reason}\n"


def test_assert_PASS_success(capsys):
    pass_msg = "PASS message"
    pass_reason = "with a good font..."
    results = [
        (SKIP,),
        (PASS, pass_msg),
    ]
    assert assert_PASS(results) == pass_msg

    captured = capsys.readouterr()
    assert captured.out == f"Test PASS {pass_reason}\n"


def test_assert_PASS_failure(capsys):
    skip_msg = "SKIP message"
    pass_reason = "with a good font..."
    results = [
        (PASS,),
        (SKIP, skip_msg),
    ]
    with pytest.raises(AssertionError):
        assert_PASS(results)

    captured = capsys.readouterr()
    assert captured.out == f"Test PASS {pass_reason}\n"


def test_assert_PASS_ignore_error_true(capsys):
    error_msg = "ERROR message"
    pass_reason = "with a good font..."
    ignore = "an error"
    results = [
        (PASS,),
        (ERROR, error_msg),
    ]
    assert assert_PASS(results, ignore_error=ignore) is None

    captured = capsys.readouterr()
    assert captured.out == f"Test PASS {pass_reason}\n{ignore}\n"


def test_assert_PASS_ignore_error_false(capsys):
    error_msg = "ERROR message"
    pass_reason = "with a good font..."
    results = [
        (PASS,),
        (ERROR, error_msg),
    ]
    with pytest.raises(AssertionError):
        assert_PASS(results)

    captured = capsys.readouterr()
    assert captured.out == f"Test PASS {pass_reason}\n"


def test_assert_results_contain_no_expected_msgcode():
    with pytest.raises(Exception) as err:
        assert_results_contain([], PASS)
    assert str(err.value) == "Test must provide the expected message code"


def test_assert_results_contain_ignore_error_true(capsys):
    msg_code = "a message code"
    ignore = "an error"
    expected_status = PASS
    results = [
        (ERROR, ""),
        (FAIL, ""),
    ]
    assert (
        assert_results_contain(results, expected_status, msg_code, ignore_error=ignore)
        is None
    )

    captured = capsys.readouterr()
    assert captured.out == f"Test {expected_status} [{msg_code}]\n{ignore}\n"


def test_assert_results_contain_bare_string(capsys):
    msg_code = "a message code"
    bare_str = "just a string"
    expected_status = PASS
    results = [
        (WARN, bare_str),
        (INFO, bare_str),
    ]
    with pytest.raises(Exception) as err:
        assert_results_contain(results, expected_status, msg_code)
    assert f"(Bare string: {bare_str!r})" in str(err.value)

    captured = capsys.readouterr()
    assert captured.out == f"Test {expected_status} [{msg_code}]\n"


def test_assert_results_contain_success_string_msg(capsys):
    msg_code = "a message code"
    expected_status = PASS
    results = [
        (PASS, msg_code),
    ]
    assert assert_results_contain(results, expected_status, msg_code) == msg_code

    captured = capsys.readouterr()
    assert captured.out == f"Test {expected_status} [{msg_code}]\n"


def test_assert_results_contain_failure_string_msg(capsys):
    msg_code = "a message code"
    expected_status = PASS
    results = [
        (DEBUG, msg_code),
    ]
    exception_message = (
        f"Expected to find {expected_status}, [code: {msg_code}]\n"
        f"But did not find it in:\n"
        f"{results}"
    )

    with pytest.raises(Exception) as err:
        assert_results_contain(results, expected_status, msg_code)
    assert str(err.value) == exception_message

    captured = capsys.readouterr()
    assert captured.out == f"Test {expected_status} [{msg_code}]\n"


def test_assert_results_contain_success_message_msg(capsys):
    msg_code = "a message code"
    msg_human = "human readable message"
    message = Message(msg_code, msg_human)
    expected_status = FAIL
    results = [
        (FAIL, message),
    ]
    assert assert_results_contain(results, expected_status, msg_code) == msg_human

    captured = capsys.readouterr()
    assert captured.out == f"Test {expected_status} [{msg_code}]\n"


def test_assert_results_contain_failure_message_msg(capsys):
    msg_code = "a message code"
    msg_human = "human readable message"
    message = Message(msg_code, msg_human)
    expected_status = FAIL
    results = [
        (ERROR, message),
    ]
    exception_message = (
        f"Expected to find {expected_status}, [code: {msg_code}]\n"
        f"But did not find it in:\n"
        f"{results}"
    )

    with pytest.raises(Exception) as err:
        assert_results_contain(results, expected_status, msg_code)
    assert str(err.value) == exception_message

    captured = capsys.readouterr()
    assert captured.out == f"Test {expected_status} [{msg_code}]\n"
