#!/usr/bin/env python3 """One interpreter start for the whole of 99-python3-webcolors. Emits `key=value' lines, one per line, in file order; the suite reads them with grep. A python3 start on this arm is tens of seconds and every case in the suite is a dictionary lookup or a `%02x' format costing microseconds, so the start is the entire cost and taking it once is the difference between a half-minute suite and a half-hour one. THREE RULES THE VALUES OBEY. The runner parses its input line by line, so no value may contain a newline; the tuples below are joined on commas and the conformance sweeps report counts and the first disagreement rather than a table. No value may depend on the encoding stdout happens to be in. That is free here -- every string this package produces is ASCII: hex digits, `#', `%', decimal digits and lower-case colour names. Nothing needs the code-point treatment 99-python3-smartypants had to give its `u' mode. And an exception is reported as `' or `' rather than allowed to end the run. Half of this package's contract IS its failure path -- an unknown name, a malformed hex value and an unsupported specification each raise ValueError -- so a raise is a MEASUREMENT here, and the suite asserts the type by name. """ import sys emitted = [] def emit(key, value): emitted.append(key) print('%s=%s' % (key, value)) def call(key, fn, *args, **kwargs): """Emit fn(*args) or, where it raises, the exception TYPE by name. The failure path is a deliverable of this package rather than an accident of it, so a raise is recorded as `' and asserted like any other value. The message is deliberately NOT included: upstream's wording is not part of any specification and a release that reworded it would red every such case while the contract -- which exception -- was intact. The two places the message IS the contract are emitted separately and by hand. """ try: emit(key, fn(*args, **kwargs)) except Exception as exc: emit(key, '<%s>' % type(exc).__name__) def tup(value): """A NamedTuple as `a,b,c' -- see the header on newlines.""" return ','.join(str(v) for v in value) try: import webcolors as w except Exception as exc: # pragma: no cover print('import_error=%s: %s' % (type(exc).__name__, exc)) sys.exit(1) # ---------------------------------------------------------------- identity --- # WHICH COPY ANSWERED. Every case below is a statement about the package named # here; without this one they would be statements about whatever was first on # sys.path. emit('package_file', getattr(w, '__file__', '')) # ...and it IS a package -- a DIRECTORY of six modules, not one file. That is # the opposite shape from 99-python3-smartypants' `py_modules' single file, and # a release that collapsed the six into one would still import and still pass # every behavioural case below. emit('is_package', 'yes' if hasattr(w, '__path__') else 'no') emit('version_attr', getattr(w, '__version__', '')) emit('all_count', len(getattr(w, '__all__', ()))) try: from importlib.metadata import version as _dist_version emit('dist_version', _dist_version('webcolors')) except Exception as exc: emit('dist_version', '<%s>' % type(exc).__name__) # THE SIX SUBMODULES, EACH IMPORTED BY NAME. The public API is re-exported # from `webcolors/__init__.py', so every behavioural case below would pass on a # build that shipped the __init__ and lost a submodule's SOURCE while keeping # its bytecode -- or the reverse. These say the six files this image installed # are the six that answered. for _n in ('constants', 'conversion', 'html5', 'normalization', 'types'): try: _m = __import__('webcolors.' + _n, fromlist=[_n]) emit('mod_' + _n, getattr(_m, '__file__', '')) except Exception as exc: emit('mod_' + _n, '<%s>' % type(exc).__name__) # THE PUBLIC API, BY NAME. Each is reached by at least one behavioural case # below; this group is what says a red further down is about a conversion # rather than about a function that is not there. for _n in ('name_to_hex', 'name_to_rgb', 'name_to_rgb_percent', 'hex_to_name', 'hex_to_rgb', 'hex_to_rgb_percent', 'rgb_to_name', 'rgb_to_hex', 'rgb_to_rgb_percent', 'rgb_percent_to_name', 'rgb_percent_to_hex', 'rgb_percent_to_rgb', 'normalize_hex', 'normalize_integer_triplet', 'normalize_percent_triplet', 'html5_parse_simple_color', 'html5_serialize_simple_color', 'html5_parse_legacy_color'): emit('has_' + _n, 'yes' if callable(getattr(w, _n, None)) else 'no') # THE THREE NAMED TUPLES the conversions return. A function that returned a # plain 3-tuple would compare EQUAL to the expected value in every case below # -- Python tuple equality ignores the class -- so the type is asserted on its # own or it is not asserted at all. for _n in ('IntegerRGB', 'PercentRGB', 'HTML5SimpleColor'): _t = getattr(w, _n, None) emit('type_' + _n, ','.join(getattr(_t, '_fields', ())) if _t else '') # ------------------------------------------------------ the specifications --- # THE FOUR SPECIFICATION IDENTIFIERS. These are the values a caller passes as # `spec=', so they are the argument's vocabulary rather than an internal: a # release that spelled one differently would break every caller while every # default-argument case below still passed. emit('spec_html4', w.HTML4) emit('spec_css2', w.CSS2) emit('spec_css21', w.CSS21) emit('spec_css3', w.CSS3) from webcolors import constants as _c emit('spec_supported', ','.join(_c.SUPPORTED_SPECIFICATIONS)) # THE SIZE OF EVERY TABLE, FORWARD AND REVERSE. The tables ARE the package -- # everything else is `%02x' and a dictionary lookup -- so a build that shipped # a truncated constants.py would pass every conversion case that happened to # use a surviving colour. These are what fail it at once. # # The forward and reverse counts differ for CSS3 and that is not an error: NINE # hex values carry two names each, so reversing 147 names yields 138 entries. # MEASURED, and the nine are not all of one kind -- seven are the gray/grey # spelling pairs and the other two are `aqua'/`cyan' and `fuchsia'/`magenta', # which is a distinction the reverse map treats quite differently. See # `collision_*' below. for _s in ('HTML4', 'CSS2', 'CSS21', 'CSS3'): emit('names_' + _s.lower(), len(getattr(w, _s + '_NAMES_TO_HEX'))) emit('hexes_' + _s.lower(), len(getattr(w, _s + '_HEX_TO_NAMES'))) # HTML 4's SIXTEEN NAMES, IN FULL AND IN ORDER. Sixteen is a number a # truncated table could still hit; the membership is what cannot be faked. emit('html4_names', ','.join(sorted(w.HTML4_NAMES_TO_HEX))) # CSS 2 IS HTML 4's LIST EXACTLY -- same names, same values -- and CSS 2.1 adds # `orange' and nothing else. Both are asserted as SET COMPARISONS rather than # as two more length checks, because that is the actual relationship between # the specifications and it is what `spec=' selects between. emit('css2_equals_html4', 'yes' if w.CSS2_NAMES_TO_HEX == w.HTML4_NAMES_TO_HEX else 'no') emit('css21_minus_css2', ','.join(sorted(set(w.CSS21_NAMES_TO_HEX) - set(w.CSS2_NAMES_TO_HEX)))) emit('css2_minus_css21', ','.join(sorted(set(w.CSS2_NAMES_TO_HEX) - set(w.CSS21_NAMES_TO_HEX)))) # ...and CSS 3 is a superset of CSS 2.1, so nothing an older specification # defines is lost by asking for the newest. emit('css21_minus_css3', ','.join(sorted(set(w.CSS21_NAMES_TO_HEX) - set(w.CSS3_NAMES_TO_HEX)))) # THE NINE COLLIDING VALUES, WHICH ARE WHY THE REVERSE MAP IS SMALLER. Named # in full, and split by kind, because the two kinds behave differently and only # one of them is documented. # # SEVEN are the gray/grey spelling pairs. constants.py reverses the table and # then OVERWRITES those seven entries by hand, so `hex_to_name' is guaranteed to # answer with the `gray' spelling -- a guarantee upstream states in the # docstring, asserted over the whole set at `grey-all-gray' below. # # TWO are `aqua'/`cyan' and `fuchsia'/`magenta', which get NO such fixup. Their # reverse entry is whatever `_reversedict' left, which is the LATER key of the # alphabetically-ordered table -- so `hex_to_name("#00ffff")' is `cyan', not # `aqua', even though `aqua' is the name HTML 4 defines and `cyan' is not in # HTML 4 at all. That asymmetry is upstream 1.13's, it is undocumented, and it # is the answer a caller round-tripping HTML 4's own sixteen names through CSS3 # gets wrong. `collision-aqua' and `collision-fuchsia' record it. emit('css3_grey_names', ','.join(sorted(n for n in w.CSS3_NAMES_TO_HEX if 'grey' in n))) emit('css3_grey_count', len([n for n in w.CSS3_NAMES_TO_HEX if 'grey' in n])) emit('css3_collisions', len(w.CSS3_NAMES_TO_HEX) - len(w.CSS3_HEX_TO_NAMES)) # --------------------------------------------------------- the ONE fixture --- # ONE COLOUR CARRIED THROUGH ALL TWELVE CONVERSIONS. goldenrod is upstream's # own worked example and it is the right choice for a reason: its three # components are 218, 165 and 32, of which 32 is one of the six values # `rgb_to_rgb_percent' special-cases to an exact percentage and the other two # are not. So a single fixture exercises both halves of that function. NAME = 'goldenrod' HEX = '#daa520' RGB = (218, 165, 32) PCT = ('85.49%', '64.71%', '12.5%') emit('fixture_name', NAME) emit('fixture_hex', HEX) emit('fixture_rgb', tup(RGB)) emit('fixture_pct', tup(PCT)) # FROM A NAME. call('name_to_hex', w.name_to_hex, NAME) emit('name_to_rgb', tup(w.name_to_rgb(NAME))) emit('name_to_rgb_percent', tup(w.name_to_rgb_percent(NAME))) # FROM A HEX VALUE. call('hex_to_name', w.hex_to_name, HEX) emit('hex_to_rgb', tup(w.hex_to_rgb(HEX))) emit('hex_to_rgb_percent', tup(w.hex_to_rgb_percent(HEX))) # FROM AN INTEGER TRIPLET. call('rgb_to_name', w.rgb_to_name, RGB) call('rgb_to_hex', w.rgb_to_hex, RGB) emit('rgb_to_rgb_percent', tup(w.rgb_to_rgb_percent(RGB))) # FROM A PERCENTAGE TRIPLET -- the three inverses. call('rgb_percent_to_name', w.rgb_percent_to_name, PCT) call('rgb_percent_to_hex', w.rgb_percent_to_hex, PCT) emit('rgb_percent_to_rgb', tup(w.rgb_percent_to_rgb(PCT))) # THE RETURN TYPES, ONCE EACH. See `type_*' above: a plain 3-tuple would be # equal to every expected value in this file, so the class is asserted apart # from the value. emit('name_to_rgb_type', type(w.name_to_rgb(NAME)).__name__) emit('name_to_rgb_percent_type', type(w.name_to_rgb_percent(NAME)).__name__) emit('rgb_percent_to_rgb_type', type(w.rgb_percent_to_rgb(PCT)).__name__) # ...and the fields are reachable BY NAME, which is the whole point of a # NamedTuple and the thing a plain tuple cannot do. emit('name_to_rgb_red', w.name_to_rgb(NAME).red) emit('name_to_rgb_green', w.name_to_rgb(NAME).green) emit('name_to_rgb_blue', w.name_to_rgb(NAME).blue) # ------------------------------------------------------------ round trips --- # WHERE THE SPECIFICATIONS MAKE THE TRIP EXACT, IT IS ASSERTED. Names, hex # values and integer triplets are three spellings of the same 24 bits, so every # cycle among them must land back on the input byte for byte. emit('rt_name_hex_name', w.hex_to_name(w.name_to_hex(NAME))) emit('rt_name_rgb_name', w.rgb_to_name(w.name_to_rgb(NAME))) emit('rt_hex_rgb_hex', w.rgb_to_hex(w.hex_to_rgb(HEX))) emit('rt_rgb_hex_rgb', tup(w.hex_to_rgb(w.rgb_to_hex(RGB)))) # THE PERCENTAGE FORM IS LOSSY -- BUT NOT IN THE DIRECTION THE DOCSTRING'S # WARNING LEADS YOU TO EXPECT, AND THAT IS WORTH MEASURING RATHER THAN # ASSUMING. `rgb_to_rgb_percent' rounds to two decimal places for every value # outside the six it special-cases, which reads like a warning that an integer # may not survive the trip out and back. # # MEASURED, over all 256 values of a component: EVERY ONE SURVIVES. Two # decimal places resolve 1/255 (0.392%) unambiguously, so integer -> percent -> # integer is exact across the whole range, and `pct-roundtrip-survivors' pins # that as the number it is rather than leaving it as a claim. # # THE LOSS IS IN THE OTHER DIRECTION. A percentage that is not itself the image # of an integer is snapped to the nearest one and does not come back: `1%' maps # to 3, and 3 maps to `1.18%'. So a caller may round-trip a colour through the # percentage form safely and may NOT use the percentage form as a storage format # for arbitrary percentages. Both halves are asserted, because asserting either # alone gives a reader the wrong rule. emit('rt_rgb_pct_rgb_fixture', tup(w.rgb_percent_to_rgb(w.rgb_to_rgb_percent(RGB)))) emit('rt_rgb_pct_small_in', tup((1, 2, 3))) emit('rt_rgb_pct_small_pct', tup(w.rgb_to_rgb_percent((1, 2, 3)))) emit('rt_rgb_pct_small_out', tup(w.rgb_percent_to_rgb(w.rgb_to_rgb_percent((1, 2, 3))))) # THE SWEEP. One example proves nothing about a range; this checks all 256 and # names the first that does not survive, so a release that changed the rounding # moves a number here rather than going unnoticed. _survive = 0 _first_lost = '' for _i in range(256): _back = w.rgb_percent_to_rgb(w.rgb_to_rgb_percent((_i, _i, _i))) if _back == (_i, _i, _i): _survive += 1 elif not _first_lost: _first_lost = '%d->%s->%d' % (_i, w.rgb_to_rgb_percent((_i, _i, _i))[0], _back[0]) emit('pct_roundtrip_survivors', _survive) emit('pct_roundtrip_first_lost', _first_lost or '') # ...AND THE TRIP THAT IS NOT EXACT, stated as its own pair: a percentage that # no integer produces is snapped, and the sweep below counts how many of a # hundred whole percentages come back unchanged. emit('rt_pct_int_pct_1', w.rgb_to_rgb_percent(w.rgb_percent_to_rgb(('1%',) * 3))[0]) emit('rt_pct_int_pct_50', w.rgb_to_rgb_percent(w.rgb_percent_to_rgb(('50%',) * 3))[0]) _pct_survive = 0 _pct_first_lost = '' for _i in range(101): _p = '%d%%' % _i _back = w.rgb_to_rgb_percent(w.rgb_percent_to_rgb((_p,) * 3))[0] if _back == _p: _pct_survive += 1 elif not _pct_first_lost: _pct_first_lost = '%s->%d->%s' % (_p, w.rgb_percent_to_rgb((_p,) * 3)[0], _back) emit('pct_int_roundtrip_survivors', _pct_survive) emit('pct_int_roundtrip_first_lost', _pct_first_lost or '') # THE SIX SPECIAL-CASED VALUES, which are the exact half of that function. # They are named in the docstring as a precision guarantee, so they are a # contract rather than an optimisation. emit('pct_specials', ','.join(w.rgb_to_rgb_percent((_v, 0, 0))[0] for _v in (255, 128, 64, 32, 16, 0))) # ------------------------------------------------- the specification argument --- # THE WHOLE REASON `spec=' EXISTS, AS ONE PAIR. `goldenrod' is defined in CSS3 # and is NOT one of HTML 4's sixteen, so it must resolve under the one and raise # under the other. A suite that only ever took the default would pass on an # implementation that ignored the argument entirely. call('spec_goldenrod_css3', w.name_to_hex, NAME, spec=w.CSS3) call('spec_goldenrod_html4', w.name_to_hex, NAME, spec=w.HTML4) call('spec_goldenrod_hex_html4', w.hex_to_name, HEX, spec=w.HTML4) call('spec_goldenrod_rgb_html4', w.rgb_to_name, RGB, spec=w.HTML4) # ...AND THE SAME PAIR ONE SPECIFICATION APART. `orange' is the single name # CSS 2.1 added to CSS 2, so this is the narrowest possible statement that the # argument selects between ADJACENT tables rather than merely between "the big # one" and "the small one". call('spec_orange_css21', w.name_to_hex, 'orange', spec=w.CSS21) call('spec_orange_css2', w.name_to_hex, 'orange', spec=w.CSS2) # A NAME EVERY SPECIFICATION DEFINES, under all four, as the control: without # it the four cases above would pass on an implementation whose HTML4 table was # simply empty. emit('spec_white_all', ','.join(w.name_to_hex('white', spec=_s) for _s in (w.HTML4, w.CSS2, w.CSS21, w.CSS3))) # THE DEFAULT IS CSS3, stated as an equality between a defaulted call and an # explicit one rather than as two identical literals, so a release that moved # the default says so HERE instead of everywhere at once. emit('spec_default_is_css3', 'yes' if w.name_to_hex(NAME) == w.name_to_hex(NAME, spec=w.CSS3) else 'no') # AN UNSUPPORTED SPECIFICATION IS AN ERROR, not a silent fall back to the # default -- which is the failure mode that would matter, since it would return # a plausible answer. This is one of the two places the MESSAGE is the # contract: it names the supported set, so a caller who typoed is told what to # type. See `call' on why every other message is left unasserted. try: w.name_to_hex(NAME, spec='css4') emit('spec_unsupported', '') emit('spec_unsupported_names_css3', 'no') except ValueError as exc: emit('spec_unsupported', '') emit('spec_unsupported_names_css3', 'yes' if 'css3' in str(exc) else 'no') except Exception as exc: emit('spec_unsupported', '<%s>' % type(exc).__name__) emit('spec_unsupported_names_css3', 'no') # ------------------------------------------------------------ normalisation --- # NAMES ARE LOWER-CASED BEFORE LOOKUP. The tables are keyed in lower case, so # without this every caller passing a name as it appears in a stylesheet would # get ValueError. emit('norm_name_upper', w.name_to_hex('GOLDENROD')) emit('norm_name_mixed', w.name_to_hex('GoldenRod')) # HEX VALUES ARE LOWER-CASED AND EXPANDED. Three digits become six by doubling # each, which is the CSS shorthand, and the output is always the six-digit # lower-case form HTML5 calls a "valid lowercase simple color". emit('norm_hex_lower', w.normalize_hex('#0099CC')) emit('norm_hex_short', w.normalize_hex('#09c')) emit('norm_hex_short_upper', w.normalize_hex('#09C')) emit('norm_hex_already', w.normalize_hex('#0099cc')) # THE LEADING `#' IS REQUIRED, NOT OPTIONAL, and that is the single most likely # way to misuse this package: `0099cc' is a perfectly good hex colour to a human # and a ValueError here. Asserted as a raise rather than left to a reader. call('norm_hex_no_hash', w.normalize_hex, '0099cc') # ...and the same requirement one level up, through a conversion rather than # through the normaliser directly -- which is the path a caller actually takes. call('norm_hex_no_hash_convert', w.hex_to_rgb, '0099cc') # THE SHORT FORM REACHES THE CONVERSIONS TOO, so `#fff' is white everywhere and # not only under `normalize_hex'. emit('norm_short_to_rgb', tup(w.hex_to_rgb('#fff'))) emit('norm_short_to_name', w.hex_to_name('#fff')) # INTEGER COMPONENTS ARE CLIPPED, NOT REJECTED, and this is the port's most # useful correction to a reasonable expectation: an out-of-range component does # NOT raise. 270 becomes 255 and -20 becomes 0, silently. A caller validating # input by catching ValueError from these functions catches nothing. emit('norm_int_clip_high', tup(w.normalize_integer_triplet((270, -20, 128)))) emit('norm_int_clip_all', tup(w.normalize_integer_triplet((999, -999, 0)))) emit('norm_int_no_raise', tup(w.normalize_integer_triplet((270, 0, 0)))) # ...and the clipping is reached THROUGH a conversion as well, so the surprise # is not confined to the normaliser: an impossible colour comes back as white. emit('norm_int_clip_via_hex', w.rgb_to_hex((300, 300, 300))) # PERCENTAGES ARE CLIPPED THE SAME WAY, to 0%..100%. emit('norm_pct_clip', tup(w.normalize_percent_triplet(('-10%', '250%', '50%')))) # ...and `-0%' is normalised to `0%', which is the one case where the string # form and the integer form differ in what they have to do. emit('norm_pct_negative_zero', tup(w.normalize_percent_triplet(('-0%', '-0%', '-0%')))) # A FRACTIONAL PERCENTAGE SURVIVES, because the package's own output contains # them -- `12.5%' is what 32 converts to -- so normalising must not truncate. emit('norm_pct_fraction', tup(w.normalize_percent_triplet(('85.49%', '64.71%', '12.5%')))) # ------------------------------------------------------- the failure path --- # EACH OF THE THREE DOCUMENTED FAILURES, AS ITS OWN CASE. These are a # deliverable: a caller validating user input relies on which of them raise. # # AN UNKNOWN NAME. call('fail_unknown_name', w.name_to_hex, 'notacolour') # ...INCLUDING ONE THAT IS ONLY UNKNOWN IN THE SPECIFICATION ASKED FOR, which # is the same raise reached by a different route. call('fail_name_wrong_spec', w.name_to_hex, 'goldenrod', spec=w.HTML4) # A MALFORMED HEX VALUE, in all four shapes upstream's own tests name: a # non-hex digit, a missing `#', too few digits and too many. call('fail_hex_nonhex', w.normalize_hex, '#0000gg') call('fail_hex_short', w.normalize_hex, '#0000') call('fail_hex_long', w.normalize_hex, '#00000000') call('fail_hex_bare', w.normalize_hex, '0099cc') # A COLOUR WITH NO NAME. This is where an "out of range" input actually does # raise, and the route is worth stating: the triplet is clipped first and THEN # looked up, so what raises is the absence of a name, not the range. call('fail_rgb_unnamed', w.rgb_to_name, (1, 2, 3)) call('fail_hex_unnamed', w.hex_to_name, '#010203') call('fail_pct_unnamed', w.rgb_percent_to_name, ('1%', '2%', '3%')) # ...and the clipped-then-looked-up route, shown directly: (300, 300, 300) is # not a colour, but it clips to white and is NAMED rather than rejected. call('fail_rgb_clipped_named', w.rgb_to_name, (300, 300, 300)) # ------------------------------------------------- the gray/grey variants --- # TWO NAMES, ONE VALUE, AND A DOCUMENTED CHOICE OF WHICH COMES BACK. Both # spellings resolve forward; the reverse map keeps only the `gray' one, which # upstream states as a guarantee rather than as an accident of dict ordering. emit('grey_forward_gray', w.name_to_hex('lightgray')) emit('grey_forward_grey', w.name_to_hex('lightgrey')) emit('grey_reverse', w.hex_to_name('#d3d3d3')) # ...over ALL NINE pairs, because "always the gray spelling" is a claim about # the whole table and one example would pass on a table that got the other # eight wrong. _grey_bad = [] for _n in sorted(w.CSS3_NAMES_TO_HEX): if 'grey' not in _n: continue _gray = _n.replace('grey', 'gray') if w.CSS3_NAMES_TO_HEX.get(_gray) != w.CSS3_NAMES_TO_HEX[_n]: _grey_bad.append(_n + ':forward') elif w.hex_to_name(w.CSS3_NAMES_TO_HEX[_n]) != _gray: _grey_bad.append(_n + ':reverse') emit('grey_all_gray', ','.join(_grey_bad) or 'yes') emit('grey_pairs_checked', len([n for n in w.CSS3_NAMES_TO_HEX if 'grey' in n])) # THE TWO COLLISIONS THAT GET NO FIXUP, and therefore answer the OTHER way. # See `css3_collisions' above: `aqua' and `fuchsia' are HTML 4 names, `cyan' and # `magenta' are not in HTML 4 at all, and it is the latter two that come back -- # so a CSS3 round trip through the reverse map RENAMES two of HTML 4's own # sixteen colours. Both directions are emitted, because the forward lookup # still accepts all four names and only the reverse picks. emit('collision_aqua_forward', w.name_to_hex('aqua')) emit('collision_cyan_forward', w.name_to_hex('cyan')) emit('collision_aqua_reverse', w.hex_to_name('#00ffff')) emit('collision_fuchsia_forward', w.name_to_hex('fuchsia')) emit('collision_magenta_forward', w.name_to_hex('magenta')) emit('collision_fuchsia_reverse', w.hex_to_name('#ff00ff')) # ...and under HTML 4, where `cyan' and `magenta' do not exist, the same two # values answer with the names HTML 4 defines. This is the case that says the # rename above is a property of the CSS3 TABLE and not of `hex_to_name'. emit('collision_aqua_html4', w.hex_to_name('#00ffff', spec=w.HTML4)) emit('collision_fuchsia_html4', w.hex_to_name('#ff00ff', spec=w.HTML4)) # ------------------------------------------------------ the HTML5 algorithms --- # THE THIRD THING IN THE PACKAGE, beside the tables and the conversions: literal # translations of the HTML5 colour algorithms, which are NOT the CSS ones. # # The simple-colour parser is STRICTER than `normalize_hex' -- exactly seven # characters, so the three-digit shorthand every conversion above accepts is an # error here. That difference is the reason both exist and it is asserted as a # pair. emit('html5_simple', tup(w.html5_parse_simple_color('#ffffff'))) call('html5_simple_short', w.html5_parse_simple_color, '#fff') call('html5_simple_nonstring', w.html5_parse_simple_color, 0xffffff) emit('html5_serialize', w.html5_serialize_simple_color((0, 0, 128))) emit('html5_simple_type', type(w.html5_parse_simple_color('#ffffff')).__name__) # THE LEGACY PARSER, which is the opposite: it is deliberately unable to fail on # junk, because it exists to reproduce what browsers do with malformed markup. # Upstream's own three examples, including the two that surprise everyone. emit('html5_legacy_name', tup(w.html5_parse_legacy_color('black'))) emit('html5_legacy_junk', tup(w.html5_parse_legacy_color('chucknorris'))) emit('html5_legacy_window', tup(w.html5_parse_legacy_color('Window'))) emit('html5_legacy_short', tup(w.html5_parse_legacy_color('#f0f'))) # ...and its only two errors, which are the whole of what it refuses. call('html5_legacy_empty', w.html5_parse_legacy_color, '') call('html5_legacy_transparent', w.html5_parse_legacy_color, 'transparent') # ---------------------------------------------------------- the conformance --- # THE SUBSTANCE OF UPSTREAM'S OWN tests/test_conformance.py, WHICH THIS IMAGE # CANNOT RUN. The recipe does `inherit ptest', but this distro sets # DISTRO_FEATURES = "" so ptest.bbclass never adds a ${PN}-ptest package at all # -- MEASURED: the recipe-scope build produced python3-webcolors, -dev and -dbg # and no -ptest deb. Forcing one would put python3-pytest on the image and, # through it, seven packages nothing here asked for. # # What upstream's conformance tests do is compare every table entry against the # specification's own text. That comparison needs no test framework, so it is # reproduced here as three sweeps over the whole table rather than as a spot # check -- which is the part of the suite a truncated or corrupted constants.py # would actually fail. # # Each sweep reports the number of entries it checked AND the first disagreement # it found, so a red says which colour rather than only that something is wrong. _bad = [] _n_checked = 0 for _name, _hexv in w.CSS3_NAMES_TO_HEX.items(): _n_checked += 1 if w.name_to_hex(_name) != _hexv: _bad.append(_name + ':name_to_hex') elif w.rgb_to_hex(w.name_to_rgb(_name)) != _hexv: _bad.append(_name + ':rgb') elif w.rgb_percent_to_hex(w.name_to_rgb_percent(_name)) != _hexv: _bad.append(_name + ':percent') if _bad: break emit('conform_css3_checked', _n_checked) emit('conform_css3_bad', _bad[0] if _bad else '') # THE HTML 4 TABLE, ROUND-TRIPPED THROUGH ITS OWN SPECIFICATION. Sixteen # entries, each of which must survive name -> hex -> name under `spec=HTML4'. _bad4 = [] for _name, _hexv in w.HTML4_NAMES_TO_HEX.items(): if w.name_to_hex(_name, spec=w.HTML4) != _hexv: _bad4.append(_name + ':forward') elif w.hex_to_name(_hexv, spec=w.HTML4) != _name: _bad4.append(_name + ':reverse') if _bad4: break emit('conform_html4_checked', len(w.HTML4_NAMES_TO_HEX)) emit('conform_html4_bad', _bad4[0] if _bad4 else '') # EVERY VALUE IN EVERY TABLE IS ALREADY NORMALISED -- `#' plus six lower-case # hex digits. The lookups are dictionary hits on the normalised form, so a # single upper-case or three-digit entry would be unreachable through # `hex_to_name' while `name_to_hex' still returned it happily. _badn = [] for _name, _hexv in w.CSS3_NAMES_TO_HEX.items(): if _hexv != w.normalize_hex(_hexv): _badn.append(_name) break emit('conform_normalised_bad', _badn[0] if _badn else '') # THE FOUR CORNERS OF THE COLOUR CUBE, plus the two the tables disagree about. # `green' is HTML 4's #008000 and NOT #00ff00, which every specification calls # `lime' -- the single most common wrong assumption about these tables, and the # one a reader is most likely to arrive with. emit('corner_black', w.name_to_hex('black')) emit('corner_white', w.name_to_hex('white')) emit('corner_green', w.name_to_hex('green')) emit('corner_lime', w.name_to_hex('lime')) emit('corner_green_html4', w.name_to_hex('green', spec=w.HTML4)) emit('corner_black_pct', tup(w.name_to_rgb_percent('black'))) emit('corner_white_pct', tup(w.name_to_rgb_percent('white'))) emit('probe_done', 'yes') emit('emitted_count', len(emitted) + 1)