. "$BST_LIB" # BST-TIMEOUT: 1200 # THREE python3 starts, and they are the whole cost. A # # bare `import', a second one whose STDERR is the # # assertion (`import-quiet'), and the probe, which takes # # ONE start for a hundred and sixty measurements. # # # # Everything else in this file is a shell builtin over a # # tmpfs copy of a dpkg list, or a grep of the probe's # # output. The work inside each start is dictionary lookups # # and `%02x' formatting over strings under 20 bytes -- # # microseconds beside an interpreter start on this arm, # # which is tens of seconds before the first line of the # # program runs. # # # # 1200 is 400s per start, the same budget 99-python3-wcwidth # # sets for its three, and roughly seven times the ~60s a # # python3 start has measured at here. The two sweeps the # # probe runs are the only loops in the file: 256 and 101 # # iterations of three dictionary lookups each, which is not # # a term in this budget. # # # # A suite-timeout red is a batch-wide cost, so if the three # # starts prove slower than this on target the RIGHT fix is # # this number, not fewer cases. # # # # NOT MEASURED ON TARGET YET: this port's first on-target # # run is the batch verify, and the measured elapsed replaces # # this note in the test plan rather than in this comment, # # which would cost an image build to keep the shipped suite # # byte-identical. # # python3-webcolors 1.13 (poky/meta # recipes-devtools/python/python3-webcolors_1.13.bb, `inherit pypi # python_setuptools_build_meta ptest', PyPI sdist) on the wasm32-hwjs glibc # image. # # ============================================================================ # WHAT THE PACKAGE IS: THREE TABLES, TWELVE CONVERSIONS AND THREE ALGORITHMS. # ============================================================================ # # webcolors converts between the colour formats the HTML and CSS specifications # define -- named colours, three- and six-digit hex, integer `rgb()' triplets # and percentage `rgb()' triplets -- in every direction. Four names, twelve # conversions, and the whole of the implementation is a dictionary lookup and a # `%02x'. # # What it actually SHIPS, and what a red here is most likely about, is the # TABLES: 16 names for HTML 4, 16 for CSS 2, 17 for CSS 2.1 and 147 for CSS 3, # each with a reverse map. `conform-*' below sweeps all 147 rather than spot # checking, because a truncated or corrupted constants.py is the failure a spot # check passes. # # A third thing sits beside those: `html5.py', literal translations of the # HTML5 colour algorithms, which are NOT the CSS ones and behave differently # enough to need their own group -- see below. # # ============================================================================ # THE SPECIFICATION ARGUMENT IS THE POINT OF THE PACKAGE. # ============================================================================ # # Every lookup takes `spec=', defaulting to CSS3. That argument is the entire # reason the four tables are separate, so it is asserted as a PAIR rather than # as a feature: `goldenrod' is defined in CSS 3 and is not one of HTML 4's # sixteen, so `name_to_hex("goldenrod")' answers and # `name_to_hex("goldenrod", spec=HTML4)' raises. A suite that only ever took the # default would pass on an implementation that ignored the argument. # # `spec-orange-*' is the narrowest form of the same statement: `orange' is the # single name CSS 2.1 added to CSS 2, so that pair says the argument selects # between ADJACENT tables and not merely between the big one and the small one. # # ============================================================================ # TWO MEASUREMENTS CORRECTED THE EXPECTATION THIS SUITE WAS WRITTEN WITH. # ============================================================================ # # BOTH are recorded as what was measured, because both are the kind of thing a # reader arrives with the opposite belief about. # # FIRST: THE PERCENTAGE ROUND TRIP IS EXACT, NOT LOSSY -- in the direction that # is usually asked about. `rgb_to_rgb_percent' rounds to two decimal places and # its docstring warns about precision, which reads as a warning that an integer # may not survive the trip out and back. Measured over ALL 256 values of a # component: every one survives (`pct-roundtrip-survivors' is 256). Two decimal # places resolve 1/255 = 0.392% unambiguously. # # The loss is in the OTHER direction, and it is severe: a percentage that is not # itself the image of an integer is snapped to the nearest one and does not come # back. Of the 101 whole percentages 0%..100%, exactly FOUR survive # (`pct-int-roundtrip-survivors'), and `1%' becomes 3 becomes `1.18%'. So the # percentage form is safe as a way of re-expressing a colour and unsafe as a # storage format for arbitrary percentages. Asserting either half alone would # leave a reader with the wrong rule, so both are asserted. # # SECOND: AN OUT-OF-RANGE COMPONENT IS CLIPPED, NOT REJECTED. 270 becomes 255 # and -20 becomes 0, silently, and `rgb_to_hex((300, 300, 300))' is `#ffffff'. # A caller validating user input by catching ValueError from these functions # catches nothing at all. Where an out-of-range triplet DOES raise, the cause is # something else -- it was clipped first and then had no name -- and # `fail-rgb-clipped-named' is the case that separates the two by showing an # impossible colour being NAMED rather than refused. # # ============================================================================ # NINE HEX VALUES CARRY TWO NAMES, AND ONLY SEVEN OF THEM ARE HANDLED. # ============================================================================ # # CSS 3 has 147 names and 138 distinct values, so the reverse map is nine # entries shorter than the forward one. The nine are not all of one kind: # # 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. Upstream documents that guarantee, and # `grey-all-gray' asserts it over the whole set rather than on one example -- # a claim about a table is not evidenced by one row of it. # # 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'. `aqua' and `fuchsia' are two of HTML 4's own sixteen # names and `cyan' and `magenta' are not in HTML 4 at all, so a CSS3 round # trip RENAMES two HTML 4 colours. Under `spec=HTML4' the same two values # answer `aqua' and `fuchsia', which is what says the rename belongs to the # CSS3 table rather than to `hex_to_name'. # # That asymmetry is upstream 1.13's, it is undocumented, and `collision-*' # records it as what it does rather than as a defect of this port. # # ============================================================================ # THE HTML5 ALGORITHMS ARE STRICTER AND MORE FORGIVING THAN THE CSS ONES, EACH # IN A DIFFERENT PLACE. # ============================================================================ # # `html5_parse_simple_color' is STRICTER than `normalize_hex': exactly seven # characters, so the three-digit shorthand every conversion above accepts is a # ValueError here. `html5_parse_legacy_color' is the opposite -- it exists to # reproduce what browsers do with malformed markup, so `chucknorris' parses to # (192, 0, 0) and `Window' to (0, 13, 0), and its only two refusals are the # empty string and `transparent'. The pair is asserted together because either # alone reads as an inconsistency. # # ============================================================================ # WHY IT PORTS: PURE PYTHON, AND python3-stringold IS THE WHOLE DEPENDENCY. # ============================================================================ # # This image's python3 is ONE STATIC BINARY with an EMPTY lib-dynload and no # dynamic loader (BRINTOS-1476; 99-python3 asserts both directly), so a module # carrying a compiled extension could not be imported here -- not "would be # slow", could not be imported. # # webcolors ships no C: `Root-Is-Purelib: true' in the wheel, no ext_modules, # and the only imports outside the package are `re', `typing' and `string'. That # last one is why the recipe's target RDEPENDS names `python3-stringold' -- OE # splits the stdlib `string' module into its own sub-package of the ported # python3 recipe, and `html5.py' uses `string.hexdigits'. MEASURED off the built # python3-webcolors_1.13-r0_wasm32.deb: the control stanza reads # `Depends: python3-core, python3-stringold' and nothing else, both of which are # sub-packages of a package already in this image, so this port waits on # nothing. # # It is a PACKAGE, NOT A MODULE -- a directory of six files -- which is the # opposite shape from 99-python3-smartypants' single file. `mod-*' asserts all # six answered from the installed path, because the public API is re-exported # through `__init__.py' and a build that lost a submodule's source while keeping # its bytecode would still import. # # ============================================================================ # THE UPSTREAM ptest SUITE IS NOT SHIPPED, AND THE SUBSTANCE OF IT IS HERE. # ============================================================================ # # The recipe does `inherit ptest' and its run-ptest calls `pytest --automake', # so "run upstream's own tests" is the obvious reading of this port. Two facts # compound against it, and both are measured rather than assumed. # # First, this distro sets DISTRO_FEATURES without "ptest" # (conf/distro/brintos.conf), so ptest.bbclass never adds ${PN}-ptest to # PACKAGES at all -- the recipe-scope build produced python3-webcolors, # python3-webcolors-dev and python3-webcolors-dbg and NO -ptest deb. That is # the same mechanism already recorded for attr, acl and python3-wcwidth in the # image recipe. # # Second, forcing one would put its RDEPENDS on the image: python3-pytest and # python3-unittest-automake-output, and pytest's own RDEPENDS names sixteen # packages of which seven are not here and are not this ticket's. Landing seven # unrelated packages as a side effect of a pure-Python port is what acl-dev's # RRECOMMENDS on attr was refused for. # # What upstream's tests/test_conformance.py actually does is compare every table # entry against the specification's own text, and that comparison needs no test # framework. It is reproduced in the probe as three sweeps over the whole table # -- `conform-css3' (147 entries through three separate conversion routes), # `conform-html4' (16 entries round-tripped under their own spec) and # `conform-normalised' (every value already in normalised form) -- which is the # part a truncated constants.py would fail. `pkg-no-ptest' asserts no -ptest # package is installed, so a future image that grew one is told to run it rather # than ship an untested duplicate. # # ============================================================================ # WHERE THE EXPECTED VALUES CAME FROM. # ============================================================================ # # Every literal below was measured on x86-64 against the bytes unpacked from the # built python3-webcolors_1.13-r0_wasm32.deb -- not against the sdist -- so what # was measured is what this image installs. The control interpreter is CPython # 3.12.3 and the image ships 3.12, so unlike 99-python3-smartypants there is no # minor-version gap to reason across. The package is dictionary lookups and # `%02x' over ASCII with no version-dependent construct in it. # # The on-target result is not yet measured -- the batch verify is this port's # first run on wasm32-hwjs -- and the test plan records the expectation as the # prediction it is. P=python3-webcolors # THE CANARY, which asserts nothing about python3-webcolors. # # A suite's FIRST BST result line is lost between the guest's printf and the # host's read -- per-suite, deterministic, and NOT a defect in the host tool # (BRINTOS-1149 established that and filed it as linux-in-the-browser#177). The # harness computes its pass count as total-minus-failures, so THE DROPPED LINE # IS COUNTED AS A PASS. A nothing-case therefore stands in that position and the # eaten line costs nothing. It sits ahead of the `require' guards deliberately, # because a `require' that skips would emit its skip as the first line and be # eaten too. # # Remove this case when linux-in-the-browser#177 is fixed. bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKG=$SITE/webcolors DI=$SITE/webcolors-1.13.dist-info PKGLIST=/var/lib/dpkg/info/python3-webcolors.list PROBE=$BST_DATA/python3-webcolors-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-webcolors.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------- the payload --- # Where the package has to land. `import webcolors' further down is the # functional statement; these are the ones that say WHICH copy answered, so a # package that imported out of some other path on sys.path is distinguishable # from one that imported out of what this image installed. check $P/site-packages-dir test -d "$SITE" # A DIRECTORY, NOT A FILE. See the header: this is the opposite shape from # 99-python3-smartypants' `py_modules' single file, and it is the case that # fails a release which collapsed the six modules into one. check $P/package-dir test -d "$PKG" check $P/no-module-file test ! -f "$SITE/webcolors.py" # ALL SIX SOURCE FILES. The API is re-exported through __init__.py, so a build # that lost one of the other five would still import; each is named because # "the package is there" is not the same statement as "the package is whole". for _m in __init__ constants conversion html5 normalization types; do check $P/source-$_m test -s "$PKG/$_m.py" done # ...AND ALL SIX PRECOMPILED. This is what `import-quiet' below rests on: OE # writes these with checked-hash invalidation, so the source is hashed and never # recompiled. A missing one is a module compiled from source on every import. for _m in __init__ constants conversion html5 normalization types; do check $P/pycache-$_m test -s "$PKG/__pycache__/$_m.cpython-312.pyc" done # NO COMPILED EXTENSION ANYWHERE THE PACKAGE PUT ANYTHING. See the header: on # this image a .so could not be loaded at all, so this is the case that fails a # version bump which grew one -- at install rather than at `import'. # # The list is copied to tmpfs first and never redirected from the rootfs: a # top-level stdin redirect on the virtiofs root makes dup2(2) return 513 and ash # EXITS, ending the suite silently (brintos/linux#73). # BRINTOS-WORKAROUND: brintos/linux#73 remove cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" check_eq $P/pkg-no-so "" \ "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" # EVERY REGULAR FILE THE PACKAGE SHIPS IS PYTHON OR ITS METADATA. The case above # names the failure that is expected; this one catches the shape nobody # predicted -- a binary, a .pyd, a stray data file -- without having to # enumerate what it might be. This package installs no script and no data, so # unlike 99-python3-smartypants there is no exemption at all. # # A dpkg .list names DIRECTORIES too, and without a trailing slash, so a # name-only filter would report every directory in the package as an unexpected # shape; each entry is therefore tested for being a regular file first. _bad="" while IFS= read -r _f; do [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # ...AND IT INSTALLS NO EXECUTABLE. webcolors is a library: no console_scripts # entry point, no `scripts', nothing in ${bindir}. Stated directly, because a # release that grew a CLI would need a group of cases this file does not have. check_eq $P/pkg-no-bindir "" \ "$(grep -E '^/usr/(bin|sbin)/' "$W/pkgf" 2>/dev/null)" # ----------------------------------------------------------- the dist-info --- # The installed metadata. This package carries its own `__version__', so the # dist-info is the SECOND of two independent statements of the version rather # than the only one, and `version-agrees' below is what compares them. check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-version "Version: 1.13" cat "$DI/METADATA" # top_level.txt names the ONE importable this package installs. It is what an # `import webcolors' has to match, and a wheel that renamed the package would # change it here first. check_out $P/dist-info-top-level "webcolors" cat "$DI/top_level.txt" # PURE PYTHON, AS THE WHEEL ITSELF DECLARES IT. `Root-Is-Purelib: true' is the # packaging-side statement of the header's claim -- no platform tag, no # extension module, nothing that could need the absent dynamic loader. The # `py3-none-any' tag says the same thing from the other end. check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" check_out $P/dist-info-tag "Tag: py3-none-any" cat "$DI/WHEEL" # The upstream licence text ships beside the metadata, which is where this # recipe's LIC_FILES_CHKSUM points. Note the NAME: this package ships LICENSE, # not the COPYING that 99-python3-smartypants asserts. check $P/dist-info-license test -s "$DI/LICENSE" check_out $P/dist-info-license-id "License: BSD-3-Clause" cat "$DI/METADATA" # ------------------------------------------------------------- the package --- # dpkg KNOWS the package. Every case above would look identical if some other # package had installed the same files, so this is the one that says # python3-webcolors is a real package this image installed rather than an # IMAGE_INSTALL line that resolved to nothing. # # The stanza is read from a tmpfs copy for the same reason as the file list, and # the anchor is exact -- an unanchored match would also take the -dev or -dbg # stanza of the same family. # BRINTOS-WORKAROUND: brintos/linux#73 remove sed -n '/^Package: python3-webcolors$/,/^$/p' /var/lib/dpkg/status > "$W/st" 2>/dev/null check $P/pkg-known test -s "$W/st" check_out $P/pkg-installed "Status: install ok installed" cat "$W/st" # THE RUNTIME DEPENDENCY THE RECIPE DECLARES, as dpkg recorded it -- and here it # is the WHOLE of it. See the header: `python3-stringold' is the OE split-out of # the stdlib `string' module, which html5.py needs for `string.hexdigits'. Both # names are sub-packages of the already-ported python3, so this port waits on # nothing; these cases are what would say so if a release grew a third-party # requirement. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" check_out $P/pkg-depends-core "python3-core" cat "$W/dep" check_out $P/pkg-depends-stringold "python3-stringold" cat "$W/dep" check_eq $P/pkg-depends-exact "Depends: python3-core, python3-stringold" \ "$(cat "$W/dep")" # NO -ptest PACKAGE IS INSTALLED. See the header: the recipe does `inherit # ptest' and this distro's DISTRO_FEATURES carries no "ptest", so ptest.bbclass # never produces one -- measured, the recipe-scope build emitted three debs and # no -ptest. This case is what tells a future image that grew one to RUN it # rather than go on shipping the reproduction in the probe as if it were the # only option. check_eq $P/pkg-no-ptest "" \ "$(sed -n '/^Package: python3-webcolors-ptest$/p' /var/lib/dpkg/status 2>/dev/null)" # --------------------------------------------------------- the interpreter --- # THE IMPORT, ON ITS OWN, BEFORE THE PROBE. If webcolors cannot be imported then # every probe case below reds at once and each names a different symptom; this # case is what says the cause is the import. It is also the cheapest possible # statement of the deliverable. check $P/import python3 -c 'import webcolors' # ...AND THE IMPORT SAYS NOTHING WHILE DOING IT. This is a statement about the # shipped bytecode rather than about style. OE writes the .pyc files with # CHECKED-HASH invalidation (measured off the built .deb: pyc header flags = 3 # for all six, i.e. hash-based with check_source set), so the sources are hashed # and never recompiled. A red here means a shipped .pyc was REJECTED -- a stale # hash, a python3 minor bump that changed the magic, or a missing __pycache__ -- # and the package is being compiled from source on every import. check_eq $P/import-quiet "" \ "$(python3 -c 'import webcolors' 2>&1)" # ---------------------------------------------------------------- the probe --- if [ ! -f "$PROBE" ]; then bst_notok $P/probe-present "$PROBE did not ship" rm -rf "$W" exit 0 fi bst_ok $P/probe-present # ONE interpreter start for everything that follows. bst_probe bounds it and # keeps BOTH streams: a probe that dies early otherwise leaves every case below # reporting got:'' and naming a symptom each, with the actual traceback nowhere. # The bound is generous because a python3 start on this arm is tens of seconds # before any work begins. bst_probe $P/probe-run "$W/probe" 600 python3 "$PROBE" OUT="$W/probe" # Read a single key. The probe emits each key at most once, so head -n 1 is # defensive rather than load-bearing; the `^' and `=' anchors keep `names_css2' # distinguishable from `names_css21', which is the pair this suite most needs to # keep apart. pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } # THE EVIDENCE PLANE. If the probe emitted nothing, every case below is reading # an absent file: each would red with got:'' and the set would read exactly like # a broken library, when what actually happened is that no measurement was # taken. The word `unscorable' is PARSED -- brintos_local_verify re-measures a # suite that prints it on a fresh guest rather than trusting a shared boot's # numbers (BRINTOS-1644) -- so it goes in this structural check, not a comment. # # The cases below still print their reds: withdraw, do not hide. This case is # what says what those reds measure. if [ -s "$OUT" ]; then bst_ok $P/probe-output else bst_notok $P/probe-output "the probe emitted nothing -- python3 did not start, or died before its first line; every CASE below is unscorable" fi # ...and it ran to the END. Every key is emitted in file order, so a probe that # died two thirds of the way through would leave the earlier cases green and the # later ones reading got:'' -- which looks like a partial library rather than a # partial run. This is the case that tells those apart. check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "164" "$(pv emitted_count)" # ------------------------------------------------------------- the identity --- # The package answered out of the directory this image installed, not from a # stray copy on sys.path. This is what makes the payload cases above and the # behaviour cases below statements about the SAME webcolors. check_eq $P/package-answered "$PKG/__init__.py" "$(pv package_file)" check_eq $P/is-package "yes" "$(pv is_package)" # ...AND SO DID EACH OF THE FIVE SUBMODULES. See the header: the API is # re-exported through __init__.py, so this is the group that says the package is # whole rather than merely importable. for _m in constants conversion html5 normalization types; do check_eq $P/mod-$_m "$PKG/$_m.py" "$(pv mod_$_m)" done # THE VERSION, FROM BOTH PLACES, AND THEN AGAINST EACH OTHER. The package's own # `__version__' and the installed dist-info read through importlib.metadata are # independent statements; `version-agrees' is the one that catches a package # that landed beside a dist-info describing some other release, which either # case alone would pass. check_eq $P/version-attr "1.13" "$(pv version_attr)" check_eq $P/dist-version "1.13" "$(pv dist_version)" check_eq $P/version-agrees "$(pv version_attr)" "$(pv dist_version)" # The size of `__all__', which is the package's own statement of its public # surface: 12 conversions, 3 normalisers, 3 HTML5 algorithms, 4 specification # names, 12 tables and 5 types less the overlap upstream chose to export. check_eq $P/all-count "35" "$(pv all_count)" # THE PUBLIC API, BY NAME. Each of these is documented and 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; do check_eq $P/api-$(echo "$_n" | tr _ -) "yes" "$(pv has_$_n)" done # THE THREE NAMED TUPLES, BY FIELD NAME. A function returning a plain 3-tuple # would compare EQUAL to every expected value in this file -- Python tuple # equality ignores the class -- so the type is asserted apart from the value or # it is not asserted at all. check_eq $P/type-integer-rgb "red,green,blue" "$(pv type_IntegerRGB)" check_eq $P/type-percent-rgb "red,green,blue" "$(pv type_PercentRGB)" check_eq $P/type-html5-simple "red,green,blue" "$(pv type_HTML5SimpleColor)" # -------------------------------------------------------- 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. check_eq $P/spec-html4 "html4" "$(pv spec_html4)" check_eq $P/spec-css2 "css2" "$(pv spec_css2)" check_eq $P/spec-css21 "css21" "$(pv spec_css21)" check_eq $P/spec-css3 "css3" "$(pv spec_css3)" check_eq $P/spec-supported "html4,css2,css21,css3" "$(pv spec_supported)" # EVERY TABLE'S SIZE, FORWARD AND REVERSE. See the header: the tables ARE the # package, and this is the group a truncated constants.py fails at once. check_eq $P/names-html4 "16" "$(pv names_html4)" check_eq $P/hexes-html4 "16" "$(pv hexes_html4)" check_eq $P/names-css2 "16" "$(pv names_css2)" check_eq $P/hexes-css2 "16" "$(pv hexes_css2)" check_eq $P/names-css21 "17" "$(pv names_css21)" check_eq $P/hexes-css21 "17" "$(pv hexes_css21)" check_eq $P/names-css3 "147" "$(pv names_css3)" # ...and the reverse map is NINE SHORTER, which is the collision count the # `collision-*' group below accounts for name by name. check_eq $P/hexes-css3 "138" "$(pv hexes_css3)" check_eq $P/css3-collisions "9" "$(pv css3_collisions)" # HTML 4's SIXTEEN NAMES, IN FULL. Sixteen is a number a truncated table could # still hit; the membership is what cannot be faked. check_eq $P/html4-names \ "aqua,black,blue,fuchsia,gray,green,lime,maroon,navy,olive,purple,red,silver,teal,white,yellow" \ "$(pv html4_names)" # THE RELATIONSHIPS BETWEEN THE SPECIFICATIONS, as set comparisons rather than # as three more length checks -- because that is the actual structure `spec=' # selects between. CSS 2 is HTML 4 exactly; CSS 2.1 adds `orange' and nothing # else; CSS 3 loses nothing CSS 2.1 had. check_eq $P/css2-equals-html4 "yes" "$(pv css2_equals_html4)" check_eq $P/css21-adds-orange "orange" "$(pv css21_minus_css2)" check_eq $P/css2-subset-css21 "" "$(pv css2_minus_css21)" check_eq $P/css21-subset-css3 "" "$(pv css21_minus_css3)" # ---------------------------------------------------------- the ONE fixture --- # ONE COLOUR THROUGH ALL TWELVE CONVERSIONS. goldenrod is upstream's own worked # example and it is the right choice for a reason: its 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. check_eq $P/fixture-name "goldenrod" "$(pv fixture_name)" check_eq $P/fixture-hex "#daa520" "$(pv fixture_hex)" check_eq $P/fixture-rgb "218,165,32" "$(pv fixture_rgb)" check_eq $P/fixture-pct "85.49%,64.71%,12.5%" "$(pv fixture_pct)" # FROM A NAME. check_eq $P/name-to-hex "#daa520" "$(pv name_to_hex)" check_eq $P/name-to-rgb "218,165,32" "$(pv name_to_rgb)" check_eq $P/name-to-rgb-percent "85.49%,64.71%,12.5%" "$(pv name_to_rgb_percent)" # FROM A HEX VALUE. check_eq $P/hex-to-name "goldenrod" "$(pv hex_to_name)" check_eq $P/hex-to-rgb "218,165,32" "$(pv hex_to_rgb)" check_eq $P/hex-to-rgb-percent "85.49%,64.71%,12.5%" "$(pv hex_to_rgb_percent)" # FROM AN INTEGER TRIPLET. check_eq $P/rgb-to-name "goldenrod" "$(pv rgb_to_name)" check_eq $P/rgb-to-hex "#daa520" "$(pv rgb_to_hex)" check_eq $P/rgb-to-rgb-percent "85.49%,64.71%,12.5%" "$(pv rgb_to_rgb_percent)" # FROM A PERCENTAGE TRIPLET -- the three inverses, which are the direction the # ticket names and the direction the round-trip note below is about. check_eq $P/rgb-percent-to-name "goldenrod" "$(pv rgb_percent_to_name)" check_eq $P/rgb-percent-to-hex "#daa520" "$(pv rgb_percent_to_hex)" check_eq $P/rgb-percent-to-rgb "218,165,32" "$(pv rgb_percent_to_rgb)" # THE RETURN TYPES, ONCE EACH, AND THE FIELDS BY NAME. See `type-*' above: the # values would compare equal on a plain tuple, and a NamedTuple's whole point is # the attribute access a plain tuple cannot offer. check_eq $P/name-to-rgb-type "IntegerRGB" "$(pv name_to_rgb_type)" check_eq $P/name-to-rgb-percent-type "PercentRGB" "$(pv name_to_rgb_percent_type)" check_eq $P/rgb-percent-to-rgb-type "IntegerRGB" "$(pv rgb_percent_to_rgb_type)" check_eq $P/field-red "218" "$(pv name_to_rgb_red)" check_eq $P/field-green "165" "$(pv name_to_rgb_green)" check_eq $P/field-blue "32" "$(pv name_to_rgb_blue)" # ------------------------------------------------------------- round trips --- # WHERE THE SPECIFICATIONS MAKE THE TRIP EXACT, IT IS ASSERTED. A name, a hex # value and an integer triplet are three spellings of the same 24 bits, so every # cycle among them must land back on the input byte for byte. check_eq $P/rt-name-hex-name "goldenrod" "$(pv rt_name_hex_name)" check_eq $P/rt-name-rgb-name "goldenrod" "$(pv rt_name_rgb_name)" check_eq $P/rt-hex-rgb-hex "#daa520" "$(pv rt_hex_rgb_hex)" check_eq $P/rt-rgb-hex-rgb "218,165,32" "$(pv rt_rgb_hex_rgb)" # THE PERCENTAGE FORM, WHERE THE ANSWER IS NOT THE ONE THE DOCSTRING SUGGESTS. # See the header. Integer -> percent -> integer is EXACT -- for goldenrod, for # (1, 2, 3), and measured across all 256 values of a component -- because two # decimal places resolve 1/255 unambiguously. check_eq $P/rt-rgb-pct-rgb "218,165,32" "$(pv rt_rgb_pct_rgb_fixture)" check_eq $P/rt-rgb-pct-small "0.39%,0.78%,1.18%" "$(pv rt_rgb_pct_small_pct)" check_eq $P/rt-rgb-pct-small-back "1,2,3" "$(pv rt_rgb_pct_small_out)" check_eq $P/pct-roundtrip-survivors "256" "$(pv pct_roundtrip_survivors)" check_eq $P/pct-roundtrip-first-lost "" "$(pv pct_roundtrip_first_lost)" # ...AND THE DIRECTION THAT IS LOSSY, WHICH IS THE OTHER ONE. A percentage no # integer produces is snapped to the nearest and does not come back: `1%' is 3 # is `1.18%'. Of the 101 whole percentages, exactly FOUR survive -- 0%, 25%, 50% # and 100%, the whole-numbered members of the six special cases. This is the # pair the ticket's "the percentage forms are lossy" is actually about, and # asserting only the exact half above would leave a reader with the wrong rule. check_eq $P/rt-pct-int-pct-lossy "1.18%" "$(pv rt_pct_int_pct_1)" check_eq $P/rt-pct-int-pct-exact "50%" "$(pv rt_pct_int_pct_50)" check_eq $P/pct-int-roundtrip-survivors "4" "$(pv pct_int_roundtrip_survivors)" check_eq $P/pct-int-roundtrip-first-lost "1%->3->1.18%" "$(pv pct_int_roundtrip_first_lost)" # THE SIX SPECIAL-CASED VALUES, which are the exact half of that function and # which upstream states as a precision guarantee rather than as an optimisation. check_eq $P/pct-specials "100%,50%,25%,12.5%,6.25%,0%" "$(pv pct_specials)" # --------------------------------------------------- the specification argument --- # THE WHOLE REASON `spec=' EXISTS, AS ONE PAIR AND THEN AS THREE MORE ROUTES TO # IT. `goldenrod' is defined in CSS 3 and is not one of HTML 4's sixteen, so it # must resolve under the one and raise under the other -- through the name # lookup, through the hex reverse lookup and through the triplet reverse lookup # alike, since all three take the argument. check_eq $P/spec-goldenrod-css3 "#daa520" "$(pv spec_goldenrod_css3)" check_eq $P/spec-goldenrod-html4 "" "$(pv spec_goldenrod_html4)" check_eq $P/spec-goldenrod-hex-html4 "" "$(pv spec_goldenrod_hex_html4)" check_eq $P/spec-goldenrod-rgb-html4 "" "$(pv spec_goldenrod_rgb_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. check_eq $P/spec-orange-css21 "#ffa500" "$(pv spec_orange_css21)" check_eq $P/spec-orange-css2 "" "$(pv spec_orange_css2)" # A NAME EVERY SPECIFICATION DEFINES, under all four, as the control: without it # the cases above would pass on an implementation whose HTML 4 table was simply # empty. check_eq $P/spec-white-all "#ffffff,#ffffff,#ffffff,#ffffff" "$(pv spec_white_all)" # 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. check_eq $P/spec-default-is-css3 "yes" "$(pv spec_default_is_css3)" # 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. The MESSAGE is asserted here, unlike everywhere else in # this file, because it names the supported set: a caller who typoed is told # what to type, and that is a contract rather than wording. check_eq $P/spec-unsupported "" "$(pv spec_unsupported)" check_eq $P/spec-unsupported-lists "yes" "$(pv spec_unsupported_names_css3)" # ------------------------------------------------------------ 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. check_eq $P/norm-name-upper "#daa520" "$(pv norm_name_upper)" check_eq $P/norm-name-mixed "#daa520" "$(pv norm_name_mixed)" # HEX VALUES ARE LOWER-CASED AND EXPANDED. Three digits become six by doubling # each -- the CSS shorthand -- and the output is always the six-digit lower-case # form HTML5 calls a "valid lowercase simple color". All four inputs land on the # same string, which is the assertion. check_eq $P/norm-hex-lower "#0099cc" "$(pv norm_hex_lower)" check_eq $P/norm-hex-short "#0099cc" "$(pv norm_hex_short)" check_eq $P/norm-hex-short-upper "#0099cc" "$(pv norm_hex_short_upper)" check_eq $P/norm-hex-already "#0099cc" "$(pv norm_hex_already)" # 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 through the normaliser and through a # conversion, which is the path a caller actually takes. check_eq $P/norm-hex-no-hash "" "$(pv norm_hex_no_hash)" check_eq $P/norm-hex-no-hash-convert "" "$(pv norm_hex_no_hash_convert)" # THE SHORT FORM REACHES THE CONVERSIONS TOO, so `#fff' is white everywhere and # not only under `normalize_hex'. check_eq $P/norm-short-to-rgb "255,255,255" "$(pv norm_short_to_rgb)" check_eq $P/norm-short-to-name "white" "$(pv norm_short_to_name)" # INTEGER COMPONENTS ARE CLIPPED, NOT REJECTED. See the header: this is the # port's most useful correction to a reasonable expectation. 270 becomes 255 and # -20 becomes 0, silently, and nothing raises -- so a caller validating input by # catching ValueError from these functions catches nothing. check_eq $P/norm-int-clip-high "255,0,128" "$(pv norm_int_clip_high)" check_eq $P/norm-int-clip-all "255,0,0" "$(pv norm_int_clip_all)" check_eq $P/norm-int-no-raise "255,0,0" "$(pv norm_int_no_raise)" # ...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. check_eq $P/norm-int-clip-via-hex "#ffffff" "$(pv norm_int_clip_via_hex)" # PERCENTAGES ARE CLIPPED THE SAME WAY, to 0%..100%, with `-0%' normalised to # `0%' -- the one case where the string form has to do something the integer # form does not -- and a fractional percentage surviving, which it must, since # the package's own output contains them. check_eq $P/norm-pct-clip "0%,100%,50%" "$(pv norm_pct_clip)" check_eq $P/norm-pct-negative-zero "0%,0%,0%" "$(pv norm_pct_negative_zero)" check_eq $P/norm-pct-fraction "85.49%,64.71%,12.5%" "$(pv norm_pct_fraction)" # -------------------------------------------------------- the failure path --- # EACH DOCUMENTED FAILURE AS ITS OWN CASE. These are a deliverable: a caller # validating user input relies on which of them raise. The exception TYPE is # asserted and the message is not -- upstream's wording is not part of any # specification, and a release that reworded it would red every case here while # the contract was intact. # # AN UNKNOWN NAME, and the same raise reached by a different route: a name that # is only unknown in the specification asked for. check_eq $P/fail-unknown-name "" "$(pv fail_unknown_name)" check_eq $P/fail-name-wrong-spec "" "$(pv fail_name_wrong_spec)" # A MALFORMED HEX VALUE, in all four shapes upstream's own tests name: a non-hex # digit, too few digits, too many, and a missing `#'. check_eq $P/fail-hex-nonhex "" "$(pv fail_hex_nonhex)" check_eq $P/fail-hex-short "" "$(pv fail_hex_short)" check_eq $P/fail-hex-long "" "$(pv fail_hex_long)" check_eq $P/fail-hex-bare "" "$(pv fail_hex_bare)" # A COLOUR WITH NO NAME, by all three routes. This is where an "out of range" # input actually does raise, and the route matters: the triplet is CLIPPED first # and then looked up, so what raises is the absence of a name, not the range. check_eq $P/fail-rgb-unnamed "" "$(pv fail_rgb_unnamed)" check_eq $P/fail-hex-unnamed "" "$(pv fail_hex_unnamed)" check_eq $P/fail-pct-unnamed "" "$(pv fail_pct_unnamed)" # ...AND THE CASE THAT SEPARATES THE TWO. (300, 300, 300) is not a colour, and # it is not refused: it clips to white and is NAMED. Without this case the three # above would read as "out-of-range raises", which is exactly the wrong lesson. check_eq $P/fail-rgb-clipped-named "white" "$(pv fail_rgb_clipped_named)" # ------------------------------------------------------ the two-name values --- # THE SEVEN gray/grey PAIRS. Both spellings resolve forward to one value, and # the reverse map answers with the `gray' spelling -- which constants.py # guarantees by overwriting those seven entries by hand after reversing. check_eq $P/grey-forward-gray "#d3d3d3" "$(pv grey_forward_gray)" check_eq $P/grey-forward-grey "#d3d3d3" "$(pv grey_forward_grey)" check_eq $P/grey-reverse "lightgray" "$(pv grey_reverse)" check_eq $P/grey-names \ "darkgrey,darkslategrey,dimgrey,grey,lightgrey,lightslategrey,slategrey" \ "$(pv css3_grey_names)" check_eq $P/grey-count "7" "$(pv css3_grey_count)" # ...OVER ALL SEVEN, because "always the gray spelling" is a claim about the # whole table and one example would pass on a table that got the other six # wrong. The probe reports the first disagreement by name, so a red says which. check_eq $P/grey-all-gray "yes" "$(pv grey_all_gray)" check_eq $P/grey-pairs-checked "7" "$(pv grey_pairs_checked)" # THE OTHER TWO COLLISIONS, WHICH GET NO FIXUP AND ANSWER THE OTHER WAY. See the # header: `aqua' and `fuchsia' are two of HTML 4's own sixteen names, `cyan' and # `magenta' are not in HTML 4 at all, and it is the latter two the CSS3 reverse # map keeps -- so a CSS3 round trip RENAMES two HTML 4 colours. Undocumented, # upstream 1.13's, and recorded here as what it does. check_eq $P/collision-aqua-forward "#00ffff" "$(pv collision_aqua_forward)" check_eq $P/collision-cyan-forward "#00ffff" "$(pv collision_cyan_forward)" check_eq $P/collision-aqua-reverse "cyan" "$(pv collision_aqua_reverse)" check_eq $P/collision-fuchsia-forward "#ff00ff" "$(pv collision_fuchsia_forward)" check_eq $P/collision-magenta-forward "#ff00ff" "$(pv collision_magenta_forward)" check_eq $P/collision-fuchsia-reverse "magenta" "$(pv collision_fuchsia_reverse)" # ...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 belongs to the CSS3 TABLE and not to `hex_to_name'. check_eq $P/collision-aqua-html4 "aqua" "$(pv collision_aqua_html4)" check_eq $P/collision-fuchsia-html4 "fuchsia" "$(pv collision_fuchsia_html4)" # ----------------------------------------------------- the HTML5 algorithms --- # THE THIRD THING IN THE PACKAGE, beside the tables and the conversions. See the # header: 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 -- and that difference is the reason both exist. check_eq $P/html5-simple "255,255,255" "$(pv html5_simple)" check_eq $P/html5-simple-short "" "$(pv html5_simple_short)" check_eq $P/html5-simple-nonstring "" "$(pv html5_simple_nonstring)" check_eq $P/html5-serialize "#000080" "$(pv html5_serialize)" check_eq $P/html5-simple-type "HTML5SimpleColor" "$(pv html5_simple_type)" # THE LEGACY PARSER IS THE OPPOSITE: it exists to reproduce what browsers do # with malformed markup, so it is unable to fail on junk. Upstream's own three # examples, including the two that surprise everyone -- `chucknorris' is a red # and `Window' is a very dark green. check_eq $P/html5-legacy-name "0,0,0" "$(pv html5_legacy_name)" check_eq $P/html5-legacy-junk "192,0,0" "$(pv html5_legacy_junk)" check_eq $P/html5-legacy-window "0,13,0" "$(pv html5_legacy_window)" check_eq $P/html5-legacy-short "255,0,255" "$(pv html5_legacy_short)" # ...and its only two refusals, which are the whole of what it will not take. check_eq $P/html5-legacy-empty "" "$(pv html5_legacy_empty)" check_eq $P/html5-legacy-transparent "" "$(pv html5_legacy_transparent)" # ---------------------------------------------------------- the conformance --- # THE SUBSTANCE OF UPSTREAM'S tests/test_conformance.py, WHICH THIS IMAGE CANNOT # RUN. See the header for why there is no -ptest package and why forcing one was # refused. What those tests do is compare every table entry against the # specification's own text, which needs no test framework -- so the probe sweeps # the whole table three ways and reports the first disagreement BY NAME, and a # red here says which colour rather than only that something is wrong. # # All 147 CSS 3 entries, each through three separate conversion routes: the # direct name lookup, the integer triplet, and the percentage triplet. The third # is the one that would catch a rounding change, since it goes out to # percentages and back. check_eq $P/conform-css3-checked "147" "$(pv conform_css3_checked)" check_eq $P/conform-css3 "" "$(pv conform_css3_bad)" # All 16 HTML 4 entries round-tripped under their OWN specification, which is # the table `spec=' exists to reach and the one every case above uses only as a # foil. check_eq $P/conform-html4-checked "16" "$(pv conform_html4_checked)" check_eq $P/conform-html4 "" "$(pv conform_html4_bad)" # EVERY VALUE IN THE TABLE IS ALREADY NORMALISED -- `#' plus six lower-case hex # digits. The reverse 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. check_eq $P/conform-normalised "" "$(pv conform_normalised_bad)" # THE FOUR CORNERS, 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. It is the same value under CSS 3, which is what says # the surprise is the specification's rather than the specification argument's. check_eq $P/corner-black "#000000" "$(pv corner_black)" check_eq $P/corner-white "#ffffff" "$(pv corner_white)" check_eq $P/corner-green "#008000" "$(pv corner_green)" check_eq $P/corner-lime "#00ff00" "$(pv corner_lime)" check_eq $P/corner-green-html4 "#008000" "$(pv corner_green_html4)" check_eq $P/corner-black-pct "0%,0%,0%" "$(pv corner_black_pct)" check_eq $P/corner-white-pct "100%,100%,100%" "$(pv corner_white_pct)" rm -rf "$W"