. "$BST_LIB" # BST-TIMEOUT: 1200 # TWO python3 starts, and they are the whole of the cost. # # Every other case here reads the filesystem or dpkg with # # shell builtins, or greps the second start's output. # # # # The two are: a bare `import' (one case, so an import # # failure is legible without reading a transcript) and the # # probe, which takes ONE start for a hundred measurements. # # # # THE ORDINARY BUDGET, NOT AN ENLARGED ONE. Nothing here # # forks a child, opens a file, sleeps, starts a thread or # # reaches a device, a peer or a terminal -- cycler composes # # dictionaries, and every fixture it needs is a list the # # probe writes out. # # # # 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-cycler 0.12.1 (meta-openembedded/meta-python # recipes-devtools/python/python3-cycler_0.12.1.bb, `inherit pypi # python_setuptools_build_meta', PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE DOES, AND WHAT THAT MAKES THE SUITE ASSERT. # ============================================================================ # # One module, 16709 bytes, three public names. `cycler()' builds a finite cycle # of property values; `+' zips two cycles together, `*' takes their outer # product, and `concat' chains them. matplotlib uses it to cycle colours, line # styles and markers as one composed unit rather than three parallel lists. # # A CYCLE'S WHOLE DEFINITION IS AN ENUMERATION THAT CAN BE WRITTEN OUT BY HAND, # which is what makes this port testable to the bottom rather than by sampling. # A cycler of three colours by two widths is six dicts in one documented order; # either it is those six in that order or the port is wrong. So nearly every # behavioural case below is a literal comparison against a list typed from the # documentation, not a property check and not a spot check. # # ---------------------------------------------------------------------------- # THE PROPERTY THE PORT MUST NOT LOSE. # ---------------------------------------------------------------------------- # # `+' REFUSES two cycles of unequal length (`add-unequal') rather than zipping # to the shorter. A bare `zip' would truncate silently, and a caller who added a # six-colour cycle to a four-marker one would get four artists styled and two # unstyled with no diagnostic anywhere. That refusal, and the refusal of two # cycles that share a key (`add-overlapping'), are the two cases a plausible # wrong port passes everything else while failing. # # ---------------------------------------------------------------------------- # `*' ORDER: THE LEFT OPERAND VARIES SLOWEST. # ---------------------------------------------------------------------------- # # `__mul__' is `itertools.product(left, right)', so `colour * width' walks both # widths of the first colour before reaching the second colour. This is the one # thing about the package most likely to be implemented backwards while looking # right, so the full six-entry product is written out (`mul'), the reversed # operand order is written out beside it (`mul-reversed'), and the pair is # pinned from both sides: same CONTENT (`mul-same-content'), different ORDER # (`mul-order-differs'). Either literal alone is consistent with a port that had # normalised the order away. # # ---------------------------------------------------------------------------- # FOUR VALUES ARE NORMALISED IN THE PROBE, AND THAT IS NOT A WEAKENING. # ---------------------------------------------------------------------------- # # A composed cycler builds each yielded dict by walking `self.keys', which is a # SET -- so the KEY ORDER of a multi-key result, and the operand order of # `simplify()', follow the string hash, which PYTHONHASHSEED randomises at every # interpreter start. Measured across twelve seeds on an x86-64 control: four of # the probe's values came back in two different forms (`slice-of-product', # `concat-two-keys', the `simplify' repr, and the disjoint set inside `concat''s # key-mismatch message); the other ninety-six were byte-identical every time. # # The probe sorts those four before emitting them, and the suite asserts the # sorted form. Dict equality ignores key order, so nothing is given up -- what # it buys is that a correct port is not an INTERMITTENT red, which is the worst # result a suite can produce because it is read as flakiness in the harness. # # ---------------------------------------------------------------------------- # WHERE THE EXPECTATIONS COME FROM. # ---------------------------------------------------------------------------- # # Every behavioural value asserted below was MEASURED on an x86-64 control at # the same version -- cycler 0.12.1 from the sdist whose sha256 the recipe pins # (88bb128f...), under CPython 3.12.3, the same 3.12 line this image ships. The # control was then re-run against THIS BUILD's own .deb payload and produced a # transcript IDENTICAL apart from the module's path, and that payload's # `cycler/__init__.py' is byte-identical to the sdist's (16709 bytes, `cmp' # clean). So a red here is this arm, not a version skew and not a repackaging. # # ---------------------------------------------------------------------------- # WHAT IS PREDICTED RATHER THAN MEASURED, AND SO MIGHT BE RED. # ---------------------------------------------------------------------------- # # EXPECTED, NOT YET MEASURED -- this is a prediction and is written as one. The # recipe phase cannot build the image and so cannot run this suite; its first # on-target run is the batch verify. # # THE PREDICTION IS THAT NOTHING HERE IS ARM-SENSITIVE, and it is a weak claim # rather than a discovery: this package is 16709 bytes of pure Python over # `copy', `functools', `itertools', `operator' and `typing', with no # ext_modules, no file access, no subprocess, no thread, no signal and no # syscall of its own. Every platform surface a port of this shape usually leans # on -- BRINTOS-1476's static interpreter for a compiled extension, an autoconf # answer, process creation, a socket, a timer -- is one this package does not # touch. # # So the honest statement of the exposure is: if anything here is red, the # expected shape is a WHOLESALE failure -- `import' red and every behavioural # case skipped by the block below -- rather than a scatter of individual reds, # because there is no case here that depends on a platform facility the others # do not. A scattered result would itself be the finding, and the design doc # records what the verify measured rather than this paragraph. # # ============================================================================ P=python3-cycler # ---------------------------------------------------------------------------- # The suite reached its first line. Distinguishes "ran and found nothing" from # "never started", which the runner cannot tell apart on its own. # ---------------------------------------------------------------------------- bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKG=$SITE/cycler MOD=$PKG/__init__.py DI=$SITE/cycler-0.12.1.dist-info PKGLIST=/var/lib/dpkg/info/python3-cycler.list PROBE=$BST_DATA/python3-cycler-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-cycler.$$ # The probe transcript is written HERE, under $TMPDIR, and never onto the # virtiofs root: a redirect whose source is the rootfs makes dup2 return 513 # and ends the suite in silence (brintos/linux#73). mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ---------------------------------------------------------------------------- # THE TREE. What the .deb actually installed. # ---------------------------------------------------------------------------- check $P/site-packages-dir test -d "$SITE" check $P/package-dir test -d "$PKG" check $P/module-present test -s "$MOD" # A PACKAGE, NOT A SINGLE MODULE. pyproject declares `packages = ["cycler"]', # so the payload lands as `cycler/__init__.py' with its bytecode in the # PACKAGE'S OWN __pycache__ -- the opposite shape from a `py_modules=' # distribution, whose module would sit at site-packages/cycler.py with its # bytecode one level up. A release that changed shape would relocate the whole # payload while every behavioural case below still passed. check $P/not-a-flat-module test ! -e "$SITE/cycler.py" # The exact byte count of the shipped module, verified with `cmp' against the # sdist's own copy at recipe time. A repackaging that rewrote the file -- a # 2to3 pass, a line-ending conversion -- moves this number. check_eq $P/module-size 16709 \ "$(wc -c < "$MOD" 2>/dev/null | tr -d ' ')" # BST-REDIRECT-OK: forked -- inside $( ) check $P/pycache-present test -s "$PKG/__pycache__/__init__.cpython-312.pyc" # THE PEP 561 MARKER IS SHIPPED, AND IT IS EMPTY. `py.typed' is how a type # checker is told this package's inline annotations are to be trusted; it is # declared in `[tool.setuptools.package-data]' and carries no bytes by design. # Asserted as an existing-but-empty file, which `test -s' would get backwards. check $P/py-typed-present test -f "$PKG/py.typed" check_eq $P/py-typed-empty 0 \ "$(wc -c < "$PKG/py.typed" 2>/dev/null | tr -d ' ')" # BST-REDIRECT-OK: forked -- inside $( ) # The package directory holds exactly these three entries and nothing else. # One case rather than three: an entry APPEARING is the signal a per-file case # cannot report. check_eq $P/package-file-set "__init__.py,__pycache__,py.typed," \ "$(LC_ALL=C ls -1 "$PKG" 2>/dev/null | LC_ALL=C sort | tr '\n' ',')" # NO COMPILED EXTENSION and NO CONSOLE SCRIPT. The package declares neither, so # both are asserted as absences: a .so here would mean the recipe had grown an # ext_module and would put this port on BRINTOS-1476's static-interpreter path, # and a /usr/bin entry would mean it had grown a console_scripts entry point. check_eq $P/no-so "" "$(find "$PKG" -name '*.so' 2>/dev/null)" check $P/no-bin-script test ! -e /usr/bin/cycler # ---------------------------------------------------------------------------- # THE dist-info. The install's own record of what it installed. # ---------------------------------------------------------------------------- check $P/dist-info-dir test -d "$DI" check $P/dist-info-record test -s "$DI/RECORD" check $P/dist-info-metadata test -s "$DI/METADATA" check $P/dist-info-wheel test -s "$DI/WHEEL" # The whole file set, as one case rather than five. Note what is NOT here: # `INSTALLER' and `REQUESTED', which a `pip install' of the same wheel does # leave behind. Their absence is what says this tree was staged by the recipe's # own PEP 517 install rather than by pip on the target. check_eq $P/dist-info-file-set \ "LICENSE,METADATA,RECORD,WHEEL,top_level.txt," \ "$(LC_ALL=C ls -1 "$DI" 2>/dev/null | LC_ALL=C sort | tr '\n' ',')" # NO entry_points.txt, and that is not an omission: the package declares no # console script and no setup keyword, so it has no entry points at all. The # absence is what says this package is a library and nothing else. check $P/no-entry-points test ! -e "$DI/entry_points.txt" check_eq $P/dist-info-top-level "cycler" \ "$(cat "$DI/top_level.txt" 2>/dev/null | tr -d ' \r\n')" # A PURE-PYTHON WHEEL. `Root-Is-Purelib: true' and a `py3-none-any' tag are the # metadata statement that there is no arch-specific payload -- the same fact # `no-so' asserts from the tree side, and the reason this port needed no # toolchain work at all. check $P/wheel-purelib grep -q '^Root-Is-Purelib: true$' "$DI/WHEEL" check $P/wheel-tag-py3 grep -q '^Tag: py3-none-any$' "$DI/WHEEL" check $P/metadata-name grep -q '^Name: cycler$' "$DI/METADATA" check $P/metadata-version grep -q '^Version: 0\.12\.1$' "$DI/METADATA" check $P/metadata-summary grep -q '^Summary: Composable style cycles$' "$DI/METADATA" check $P/metadata-license grep -q '^Classifier: License :: OSI Approved :: BSD License$' "$DI/METADATA" # THE LICENSE FILE IS SHIPPED -- `license = {file = "LICENSE"}' in pyproject, # which is also what the recipe's LIC_FILES_CHKSUM checksums. check $P/license-file-present test -s "$DI/LICENSE" check $P/license-file-text grep -q 'matplotlib project' "$DI/LICENSE" # RECORD lists the module at its measured size. This is the install's own # statement about the payload, cross-checked against `module-size' above -- two # independent readings of one fact, so a repackaging cannot move one silently. check $P/record-lists-module \ grep -q '^cycler/__init__\.py,sha256=.*,16709$' "$DI/RECORD" check $P/record-lists-py-typed \ grep -q '^cycler/py\.typed,sha256=.*,0$' "$DI/RECORD" # ---------------------------------------------------------------------------- # dpkg's own record. The package is registered, not merely present on disk. # ---------------------------------------------------------------------------- check $P/dpkg-list test -s "$PKGLIST" check $P/dpkg-lists-module grep -q "^$MOD\$" "$PKGLIST" check $P/dpkg-lists-pytyped grep -q "^$PKG/py\.typed\$" "$PKGLIST" # ---------------------------------------------------------------------------- # THE Depends LINE, WHICH IS THE ONE THING THIS PORT CHANGED. # # meta-python declares `RDEPENDS:${PN} += "python3-core python3-six"'. cycler # 0.12.1 imports `six' NOWHERE -- the string does not occur anywhere in the # sdist, egg-info included, and the sdist's own `requires.txt' has an EMPTY base # section -- so the entry is a Python-2-era leftover upstream dropped and the # recipe did not. The bbappend removes it, and these two cases assert the result # from BOTH sides: what must be there, and what must not. # # The removal changes no rootfs: python3-six is named directly on this image's # IMAGE_INSTALL (BRINTOS-1744), so it is present whatever cycler says about it. # What it fixes is the statement the PACKAGE makes about what it needs. # ---------------------------------------------------------------------------- sed -n '/^Package: python3-cycler$/,/^$/p' /var/lib/dpkg/status > "$W/st" 2>/dev/null check $P/deb-known test -s "$W/st" check_out $P/deb-installed "Status: install ok installed" cat "$W/st" check_out $P/deb-version "Version: 0.12.1-r0" cat "$W/st" # THE Depends LINE, EXACTLY -- the case that proves the bbappend did what it # says. With the stock recipe this reads "Depends: python3-core, python3-six". grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/deb-depends-line test -s "$W/dep" check_eq $P/deb-depends-exact "Depends: python3-core" "$(cat "$W/dep")" # ...and the same fact from the other side, as its own case. `deb-depends-exact' # already fails if six is still named, but it fails identically for ANY drift in # that line, and this port is specifically about one entry: a red here says # "the removal did not take", which is a different report from "the line moved". if grep -q 'python3-six' "$W/dep" 2>/dev/null; then bst_notok $P/deb-depends-no-six "python3-six still on the Depends line: $(cat "$W/dep")" else bst_ok $P/deb-depends-no-six fi # ---------------------------------------------------------------------------- # THE IMPORT, as its own case, so an import failure is legible from the summary # without reading a transcript. # ---------------------------------------------------------------------------- check $P/import python3 -c 'import cycler' if [ ! -s "$PROBE" ]; then bst_notok $P/probe-present "probe missing at $PROBE" rm -rf "$W" exit 0 fi bst_ok $P/probe-present # ---------------------------------------------------------------------------- # THE BEHAVIOUR. One python3 start for all of it: the probe emits `key=value' # lines and every case below reads one by key. # ---------------------------------------------------------------------------- bst_probe $P/probe-run "$W/probe" 900 python3 "$PROBE" OUT="$W/probe" # One reader for the whole transcript. `head -n 1' so a key the probe emitted # twice cannot make a case read two lines, and `cut -d= -f2-' so a VALUE # carrying `=' survives whole. pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } # The probe ran to its last line, and emitted exactly the number of values this # suite was written against. Together these two catch a probe that died halfway # -- every case after the death would otherwise read the empty string and report # a value mismatch, burying one fact under a hundred. check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "100" "$(pv emitted_count)" _imp=$(pv import_ok) check_eq $P/import-ok "yes" "$_imp" if [ "$_imp" != "yes" ]; then # The module did not import. Skip -- not red -- every behavioural case, each # with the reason attached: they are all one fact, and a hundred reds would # bury it rather than report it. `import' and `import-ok' above are the two # reds that carry the verdict, and the design doc attributes them. _why="cycler did not import: $(pv import_error)" for _c in \ module-file module-name is-package version has-all public-names \ cycler-callable Cycler-is-class concat-callable concat-is-method \ single single-len single-keys kwarg-single kwarg-eq-positional \ add add-len add-keys kwarg-pair mul mul-len mul-reversed \ mul-same-content mul-order-differs mul-int rmul-int \ mul-int-eq-rmul mul-int-repr by-key-single by-key-add by-key-mul \ by-key-roundtrip transpose-alias simplify-same-cycle simplify-op \ simplify-operands simplify-repr-differs infinite-seven \ infinite-wraps infinite-independent eq-same eq-diff-values \ eq-diff-key eq-diff-len eq-non-cycler eq-zip-vs-rebuilt \ hash-is-none hash-raises repr-base repr-add repr-mul len-base \ len-mul-3x2 len-mul-int contains-yes contains-no keys-is-copy \ slice-step slice-tail slice-reverse slice-of-product slice-empty \ concat-free concat-method concat-len concat-two-keys change-key \ change-key-composed change-key-same iadd imul imul-int iadd-int \ copy-ctor copy-entries-independent relabel int-key int-key-repr \ tuple-key unhashable-key int-key-composes add-unequal \ add-overlapping mul-overlapping ctor-one-non-cycler \ ctor-three-args ctor-args-and-kwargs ctor-no-args getitem-int \ getitem-str mul-float mul-str concat-key-mismatch \ change-key-exists change-key-missing relabel-multi repr-html; do bst_skip $P/$_c "$_why" done rm -rf "$W" exit 0 fi # The module the probe imported is the one the .deb installed -- not a copy on # the path from somewhere else, which would make every case below a statement # about a file this port did not ship. check_eq $P/module-file "$MOD" "$(pv module_file)" # ---------------------------------------------------------------------------- # THE MODULE'S SURFACE. Three public names, and the stdlib names it imports at # module level -- which are reachable as `cycler.copy', `cycler.product' and so # on, and a reader should be able to see that from the suite. The `typing' # aliases are in there too because the module imports them by name; the set is # asserted WHOLE rather than sampled, so an addition or a removal is the signal. # ---------------------------------------------------------------------------- check_eq $P/module-name "cycler" "$(pv module_name)" check_eq $P/is-package "yes" "$(pv is_package)" check_eq $P/version "0.12.1" "$(pv version)" # NO `__all__'. Asserted because a library with exactly three names to export is # the one a reader assumes declares them, and it does not -- so `from cycler # import *' takes the stdlib re-exports with it. check_eq $P/has-all "no" "$(pv has_all)" check_eq $P/public-names \ "Any,Callable,Cycler,Dict,Generator,Generic,Hashable,Iterable,K,L,List,TypeVar,U,Union,V,add,annotations,cast,concat,copy,cycle,cycler,mul,overload,product,reduce" \ "$(pv public_names)" check_eq $P/cycler-callable "yes" "$(pv cycler_callable)" check_eq $P/Cycler-is-class "yes" "$(pv Cycler_is_class)" check_eq $P/concat-callable "yes" "$(pv concat_callable)" # `concat' is BOTH a free function and a class attribute -- `concat = concat' at # class scope makes `c.concat(d)' and `concat(c, d)' the same object. A port # that lost the class attribute would leave every free-function case green. check_eq $P/concat-is-method "yes" "$(pv concat_is_method)" # ---------------------------------------------------------------------------- # THE ENUMERATION. The assertion this port is about: each of these is a complete # cycle, written out here by hand from the documentation, not pasted from a run. # ---------------------------------------------------------------------------- check_eq $P/single "[{'color': 'r'}, {'color': 'g'}, {'color': 'b'}]" \ "$(pv single)" check_eq $P/single-len "3" "$(pv single_len)" check_eq $P/single-keys "'color'" "$(pv single_keys)" # The keyword form of one key is the same cycle as the two-argument form. # Asserted as an equality as well as a literal, so the two constructors cannot # drift apart while each stays self-consistent. check_eq $P/kwarg-single "[{'color': 'r'}, {'color': 'g'}, {'color': 'b'}]" \ "$(pv kwarg_single)" check_eq $P/kwarg-eq-positional "yes" "$(pv kwarg_eq_positional)" # `+' IS A ZIP: index by index, both keys, the length of either. check_eq $P/add "[{'color': 'r', 'lw': 1}, {'color': 'g', 'lw': 2}]" "$(pv add)" check_eq $P/add-len "2" "$(pv add_len)" check_eq $P/add-keys "'color','lw'" "$(pv add_keys)" # The multi-kwarg keyword form IS that zip -- `reduce(add, ...)' over the kwargs # -- rather than a second code path with an ordering of its own. check_eq $P/kwarg-pair "[{'color': 'r', 'lw': 1}, {'color': 'g', 'lw': 2}]" \ "$(pv kwarg_pair)" # `*' IS AN OUTER PRODUCT AND THE LEFT OPERAND VARIES SLOWEST. Written out in # full, then pinned from the other side by the reversed operand order: same six # dicts, different sequence. See the header -- either literal alone is # consistent with a port that had normalised the order away. check_eq $P/mul \ "[{'color': 'r', 'lw': 1}, {'color': 'r', 'lw': 2}, {'color': 'g', 'lw': 1}, {'color': 'g', 'lw': 2}, {'color': 'b', 'lw': 1}, {'color': 'b', 'lw': 2}]" \ "$(pv mul)" check_eq $P/mul-len "6" "$(pv mul_len)" check_eq $P/mul-reversed \ "[{'lw': 1, 'color': 'r'}, {'lw': 1, 'color': 'g'}, {'lw': 1, 'color': 'b'}, {'lw': 2, 'color': 'r'}, {'lw': 2, 'color': 'g'}, {'lw': 2, 'color': 'b'}]" \ "$(pv mul_reversed)" check_eq $P/mul-same-content "yes" "$(pv mul_same_content)" check_eq $P/mul-order-differs "yes" "$(pv mul_order_differs)" # `* int' REPEATS THE VALUE LIST, not the cycle object: it goes through # `by_key()' and rebuilds, so the result is a BASE cycler again -- which # `mul-int-repr' is what shows. `__rmul__' delegates, so `3 * c' is the same. check_eq $P/mul-int \ "[{'a': 1}, {'a': 2}, {'a': 1}, {'a': 2}, {'a': 1}, {'a': 2}]" \ "$(pv mul_int)" check_eq $P/rmul-int \ "[{'a': 1}, {'a': 2}, {'a': 1}, {'a': 2}, {'a': 1}, {'a': 2}]" \ "$(pv rmul_int)" check_eq $P/mul-int-eq-rmul "yes" "$(pv mul_int_eq_rmul)" check_eq $P/mul-int-repr "cycler('a', [1, 2, 1, 2, 1, 2])" "$(pv mul_int_repr)" # ---------------------------------------------------------------------------- # by_key(): THE INVERSE OF THE ENUMERATION. The docstring promises the transpose # can rebuild an equivalent cycler using only `+', and `by-key-roundtrip' is # that claim asserted rather than the transpose alone. # ---------------------------------------------------------------------------- check_eq $P/by-key-single "{'color': ['r', 'g', 'b']}" "$(pv by_key_single)" check_eq $P/by-key-add "{'color': ['r', 'g'], 'lw': [1, 2]}" "$(pv by_key_add)" check_eq $P/by-key-mul \ "{'color': ['r', 'r', 'g', 'g', 'b', 'b'], 'lw': [1, 2, 1, 2, 1, 2]}" \ "$(pv by_key_mul)" check_eq $P/by-key-roundtrip "yes" "$(pv by_key_roundtrip)" # `_transpose' is kept for backwards compatibility and is the SAME function # object, not a wrapper that could drift. check_eq $P/transpose-alias "yes" "$(pv transpose_alias)" # `simplify' is `by_key' plus that rebuild, so a PRODUCT simplifies to a ZIP of # fully-enumerated lists: same enumeration, different internal shape. The repr # is asserted structurally rather than verbatim -- see the header's note on the # four normalised values. check_eq $P/simplify-same-cycle "yes" "$(pv simplify_same_cycle)" check_eq $P/simplify-op "+" "$(pv simplify_op)" check_eq $P/simplify-operands \ "cycler('color', ['r', 'r', 'g', 'g', 'b', 'b']),cycler('lw', [1, 2, 1, 2, 1, 2])" \ "$(pv simplify_operands)" check_eq $P/simplify-repr-differs "yes" "$(pv simplify_repr_differs)" # ---------------------------------------------------------------------------- # THE INFINITE CONSUMER. `c()' is `itertools.cycle(self)' -- the form matplotlib # actually consumes, an unbounded iterator that REPEATS FROM THE START once the # finite cycle is exhausted. Seven pulls from a three-entry cycle shows the wrap # twice over; `infinite-wraps' is the assertion that it repeats rather than # merely not stopping, and `infinite-independent' that two consumers do not # share a position -- which is what lets one cycler drive several artists. # ---------------------------------------------------------------------------- check_eq $P/infinite-seven \ "[{'color': 'r'}, {'color': 'g'}, {'color': 'b'}, {'color': 'r'}, {'color': 'g'}, {'color': 'b'}, {'color': 'r'}]" \ "$(pv infinite_seven)" check_eq $P/infinite-wraps "yes" "$(pv infinite_wraps)" check_eq $P/infinite-independent "yes" "$(pv infinite_independent)" # ---------------------------------------------------------------------------- # EQUALITY, len, repr, keys. # # `__eq__' compares length, key set and every entry in order, and returns False # for a non-Cycler rather than raising -- so all four ways of differing are # asserted separately. `eq-zip-vs-rebuilt' is the one that says equality is # about the ENUMERATION and not the construction: `a + b' and `b + a' are two # different object graphs yielding the same dicts, and they compare equal. # ---------------------------------------------------------------------------- check_eq $P/eq-same "yes" "$(pv eq_same)" check_eq $P/eq-diff-values "no" "$(pv eq_diff_values)" check_eq $P/eq-diff-key "no" "$(pv eq_diff_key)" check_eq $P/eq-diff-len "no" "$(pv eq_diff_len)" check_eq $P/eq-non-cycler "no" "$(pv eq_non_cycler)" check_eq $P/eq-zip-vs-rebuilt "yes" "$(pv eq_zip_vs_rebuilt)" # UNHASHABLE ON PURPOSE. `__hash__ = None' at class scope, because `__eq__' is # defined and a cycler is mutable (`change_key', `+='). Asserted from both # sides, because it is the kind of one-line declaration a repackaging drops # without any enumeration case noticing. check_eq $P/hash-is-none "yes" "$(pv hash_is_none)" check_eq $P/hash-raises "TypeError" "$(pv hash_raises)" # THE repr IS A LITERAL A CALLER CAN READ BACK: a base cycler reprs as its own # constructor call, and a composition as the two operands around its operator. check_eq $P/repr-base "cycler('color', ['r', 'g', 'b'])" "$(pv repr_base)" check_eq $P/repr-add \ "(cycler('color', ['r', 'g']) + cycler('lw', [1, 2]))" "$(pv repr_add)" check_eq $P/repr-mul \ "(cycler('color', ['r', 'g']) * cycler('lw', [1, 2]))" "$(pv repr_mul)" # `len' of a composition is the OPERATOR's own arithmetic -- `min' for a zip, # `mul' for a product -- not the left operand's length. check_eq $P/len-base "3" "$(pv len_base)" check_eq $P/len-mul-3x2 "6" "$(pv len_mul_3x2)" check_eq $P/len-mul-int "8" "$(pv len_mul_int)" check_eq $P/contains-yes "yes" "$(pv contains_yes)" check_eq $P/contains-no "no" "$(pv contains_no)" # `.keys' hands back a COPY. A port that returned the live set would pass every # other case here and let a caller corrupt the cycler by reading it. check_eq $P/keys-is-copy "yes" "$(pv keys_is_copy)" # ---------------------------------------------------------------------------- # SLICING. `[]' takes a SLICE ONLY, applied per key through `by_key', so the # result is a rebuilt cycler rather than a view -- which is why a slice of a # PRODUCT comes back as an ordinary two-key cycle. # ---------------------------------------------------------------------------- check_eq $P/slice-step "[{'color': 'r'}, {'color': 'b'}]" "$(pv slice_step)" check_eq $P/slice-tail "[{'color': 'g'}, {'color': 'b'}]" "$(pv slice_tail)" check_eq $P/slice-reverse \ "[{'color': 'b'}, {'color': 'g'}, {'color': 'r'}]" "$(pv slice_reverse)" check_eq $P/slice-of-product \ "[{'color': 'r', 'lw': 1}, {'color': 'r', 'lw': 2}]" "$(pv slice_of_product)" check_eq $P/slice-empty "[]" "$(pv slice_empty)" # ---------------------------------------------------------------------------- # concat: THE ONE COMPOSITION THAT LENGTHENS THE CYCLE rather than widening or # multiplying it. The keys must match EXACTLY, which `concat-key-mismatch' # below asserts from the refusing side. # ---------------------------------------------------------------------------- check_eq $P/concat-free \ "[{'a': 0}, {'a': 1}, {'a': 'x'}, {'a': 'y'}]" "$(pv concat_free)" check_eq $P/concat-method \ "[{'a': 0}, {'a': 1}, {'a': 'x'}, {'a': 'y'}]" "$(pv concat_method)" check_eq $P/concat-len "3" "$(pv concat_len)" check_eq $P/concat-two-keys \ "[{'a': 0, 'b': 1}, {'a': 2, 'b': 3}]" "$(pv concat_two_keys)" # ---------------------------------------------------------------------------- # change_key: AN IN-PLACE RENAME, which recurses into a composition so the # entries the cycler yields carry the new name too. `change-key-same' pins the # early return: renaming a key to itself is a no-op rather than the `already a # key' ValueError it would otherwise walk into. # ---------------------------------------------------------------------------- check_eq $P/change-key "[{'b': 1}, {'b': 2}]" "$(pv change_key)" check_eq $P/change-key-composed \ "[[('a', 1), ('c', 3)], [('a', 2), ('c', 4)]]" "$(pv change_key_composed)" check_eq $P/change-key-same "[{'a': 1}, {'a': 2}],a" "$(pv change_key_same)" # ---------------------------------------------------------------------------- # IN-PLACE `+=' AND `*=' -- and the asymmetry worth knowing about. # # `c * 3' is documented integer multiplication and works. `c *= 3' is a # TypeError: `__imul__' accepts a Cycler and nothing else. So the augmented form # of an expression that worked raises, and that is upstream's behaviour rather # than this arm's -- asserted with the message, so a reader meeting it in a # traceback can find it here. # ---------------------------------------------------------------------------- check_eq $P/iadd "[{'a': 1, 'b': 3}, {'a': 2, 'b': 4}]" "$(pv iadd)" check_eq $P/imul \ "[{'a': 1, 'b': 3}, {'a': 1, 'b': 4}, {'a': 2, 'b': 3}, {'a': 2, 'b': 4}]" \ "$(pv imul)" check_eq $P/imul-int "TypeError:Cannot *= with a non-Cycler object" \ "$(pv imul_int)" check_eq $P/iadd-int "TypeError:Cannot += with a non-Cycler object" \ "$(pv iadd_int)" # ---------------------------------------------------------------------------- # THE COPY CONSTRUCTOR. One positional Cycler copies it: a different object that # compares equal. It is documented as SHALLOW, and `copy-entries-independent' is # the half a reader assumes is false -- the per-entry dicts ARE copied # (`[copy.copy(v) for v in left]'), so mutating one cannot reach the original. # ---------------------------------------------------------------------------- check_eq $P/copy-ctor "yes,yes,[{'a': 1}, {'a': 2}]" "$(pv copy_ctor)" check_eq $P/copy-entries-independent "[{'a': 1}, {'a': 2}]" \ "$(pv copy_entries_independent)" # A single-property cycler may be the ITERABLE of the two-arg form, which is a # key rename by shallow copy rather than a nested cycler. check_eq $P/relabel "[{'b': 1}, {'b': 2}]" "$(pv relabel)" # ---------------------------------------------------------------------------- # NON-STRING KEYS: THE TICKET'S CLAUSE, NARROWED AGAINST MEASUREMENT. # # The ticket asks for "constructing one with a non-string key" to raise the # documented exception. IT DOES NOT, and the suite asserts what the package # does. The label of the two-argument form is documented as ANY HASHABLE, and an # int key and a tuple key both build, enumerate, repr and compose correctly. # What cannot take a non-identifier key is the KEYWORD form, and that is # Python's own call syntax rather than a check of cycler's. # # The construction that DOES fail is an UNHASHABLE label, and it fails inside # `set()' with a TypeError rather than through any guard of the package's own -- # which is worth having measured, because it is the difference between a # documented refusal and an incidental one. # ---------------------------------------------------------------------------- check_eq $P/int-key "[{1: 'x'}, {1: 'y'}]" "$(pv int_key)" check_eq $P/int-key-repr "cycler(1, ['x', 'y'])" "$(pv int_key_repr)" check_eq $P/tuple-key "[{('a', 'b'): 1}]" "$(pv tuple_key)" check_eq $P/unhashable-key "TypeError" "$(pv unhashable_key)" check_eq $P/int-key-composes "[{1: 'x', 2: 'y'}]" "$(pv int_key_composes)" # ---------------------------------------------------------------------------- # THE ERROR PATHS. Every one of these is a composition with no correct answer, # and the failure mode a port must not have is producing a SILENTLY TRUNCATED # cycle instead of raising. Each is asserted WITH ITS MESSAGE, because the # message is what a caller reads and because an exception of the right type # raised at the wrong place would otherwise pass. # ---------------------------------------------------------------------------- check_eq $P/add-unequal \ "ValueError:Can only add equal length cycles, not 3 and 2" \ "$(pv add_unequal)" check_eq $P/add-overlapping \ "ValueError:Can not compose overlapping cycles" "$(pv add_overlapping)" check_eq $P/mul-overlapping \ "ValueError:Can not compose overlapping cycles" "$(pv mul_overlapping)" check_eq $P/ctor-one-non-cycler \ "TypeError:If only one positional argument given, it must be a Cycler instance." \ "$(pv ctor_one_non_cycler)" check_eq $P/ctor-three-args \ "TypeError:Only a single Cycler can be accepted as the lone positional argument. Use keyword arguments instead." \ "$(pv ctor_three_args)" check_eq $P/ctor-args-and-kwargs \ "TypeError:cycler() can only accept positional OR keyword arguments -- not both." \ "$(pv ctor_args_and_kwargs)" check_eq $P/ctor-no-args \ "TypeError:Must have at least a positional OR keyword arguments" \ "$(pv ctor_no_args)" # `[]' IS SLICE-ONLY, and both an int and a str are refused by the same guard -- # so a caller reaching for `c[0]' to get the first entry gets a diagnostic # rather than a wrong answer. check_eq $P/getitem-int \ "ValueError:Can only use slices with Cycler.__getitem__" "$(pv getitem_int)" check_eq $P/getitem-str \ "ValueError:Can only use slices with Cycler.__getitem__" "$(pv getitem_str)" # `__mul__' returns NotImplemented for anything that is neither a Cycler nor an # int, which the interpreter then turns into a TypeError. check_eq $P/mul-float "TypeError" "$(pv mul_float)" check_eq $P/mul-str "TypeError" "$(pv mul_str)" # concat's message embeds two SET reprs, so the probe reads it back in pieces # and sorts the disjoint names -- see the header's note on the normalised # values. The empty intersection is the whole reason this pair is rejected: the # keys do not OVERLAP, they simply are not the same set. check_eq $P/concat-key-mismatch \ "ValueError|Keys do not match:|Intersection: set()|'a','b'" \ "$(pv concat_key_mismatch)" check_eq $P/change-key-exists \ "ValueError:Can't replace a with b, b is already a key" \ "$(pv change_key_exists)" # A KeyError, not a ValueError -- and its `str()' carries the message's own # quotes, which is KeyError's repr rather than a quirk of this package. check_eq $P/change-key-missing \ "KeyError:\"Can't replace z with b, z is not a key\"" \ "$(pv change_key_missing)" check_eq $P/relabel-multi \ "ValueError:Can not create Cycler from a multi-property Cycler" \ "$(pv relabel_multi)" # ---------------------------------------------------------------------------- # THE HTML VIEW. `_repr_html_' is what a notebook renders, and the only output # of this package a reader inspects by eye rather than compares. Keys are sorted # by `repr' and there is one row per entry. # ---------------------------------------------------------------------------- check_eq $P/repr-html \ "
| 'a' |
|---|
| 1 |
| 2 |