. "$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-odd # # 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 -- pretend is a # # stubbing library and every fixture it needs is an object # # the probe constructs in memory. That is why this is 1200 # # where python3-vcversioner's is 1800: that suite spawns a # # dozen /bin/sh children and this one spawns none. # # # # 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-pretend 1.0.9 (poky meta, # recipes-devtools/python/python3-pretend_1.0.9.bb, `inherit pypi setuptools3', # PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE DOES, AND WHAT THAT MAKES THE SUITE ASSERT. # ============================================================================ # # One module, 3040 bytes, four public names. `stub' builds an object whose # attributes are exactly the keyword arguments given; `call_recorder' wraps a # callable so every invocation is recorded as a `call' for later comparison; # `raiser' turns an exception into a callable that raises it. It exists so a # test can supply a collaborator of the right shape without a mocking # framework. # # The package is small, so this suite is EXHAUSTIVE rather than # representative -- every public name, every branch of the magic-method # dispatcher, and both sides of every comparison. # # ---------------------------------------------------------------------------- # THE PROPERTY THE PORT MUST NOT LOSE. # ---------------------------------------------------------------------------- # # An attribute that was NOT declared raises AttributeError rather than being # auto-created (`stub-undeclared', and `stub-undeclared-dunder' for the magic # case). That is the whole difference between this library and a permissive # mock: a mock answers anything, so a test that misspells the collaborator's # method passes over a bug. A port that lost this would still pass every other # case below. # # ---------------------------------------------------------------------------- # EVERY STUB IS AN INSTANCE OF A GENERATED SUBCLASS, NEVER OF `stub'. # ---------------------------------------------------------------------------- # # This is the thing about the package a reader is most likely to get wrong. # `stub.__new__' builds a subclass carrying dispatchers for whichever magic # methods the call declared, and caches it on `stub._classes_cache' keyed by # that frozenset. So `type(s) is pretend.stub' is FALSE even for a stub with no # magic methods at all, while `isinstance(s, pretend.stub)' is true -- # `stub-type-is-stub' and `stub-isinstance' assert the pair, because either # alone reads as the other. The cache is keyed by the MAGIC METHODS and by # nothing else, which `stub-cache-shared-type' (different ordinary attributes, # one class) and `stub-cache-magic-type-differs' assert from both sides. # # ---------------------------------------------------------------------------- # THE DISPATCHER PASSES NO SELF, AND THE TABLE IS A WHITELIST. # ---------------------------------------------------------------------------- # # `_build_magic_dispatcher' forwards `self.__dict__[method](*args, **kwargs)', # so a supplied callable sees the operator's arguments and nothing else: # `__len__=lambda: 3' works and `__len__=lambda self: 3' is a TypeError # (`magic-no-self'). And MAGIC_METHODS is a whitelist of 31 names -- a dunder # outside it, `__str__' being the case asserted, is stored as an ordinary # attribute that NOTHING consults, so `str()' falls back to the default # `__repr__' (`magic-not-in-table-str'). A stub that silently ignored the # method a test supplied is the failure those two catch. # # Every magic case goes through the OPERATOR or the builtin -- `len(s)', # `s[k]', `with s', `next(s)' -- rather than calling the attribute, because # calling the attribute would pass with no dispatcher generated at all. # # ---------------------------------------------------------------------------- # THE CALL IS RECORDED BEFORE THE WRAPPED CALLABLE RUNS. # ---------------------------------------------------------------------------- # # `call_recorder''s inner appends to `.calls' and THEN calls through, so a # callable that raises propagates its exception AND leaves the call recorded # (`recorder-raises' asserts all three: the exception type, the ledger length # and the recorded value). That ordering is what lets a test assert what a # failing collaborator was asked for, and it is the other thing about this # library a reader gets backwards. # # ---------------------------------------------------------------------------- # WHERE THE EXPECTATIONS COME FROM. # ---------------------------------------------------------------------------- # # Every behavioural value asserted below was MEASURED on an x86-64 control at # the same version -- pretend 1.0.9 from the sdist whose sha256 the recipe pins, # under CPython 3.12.3, the same 3.12 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 `pretend.py' is # byte-identical to the sdist's (3040 bytes, `cmp' clean). So a red here is this # arm, not a version skew and not a repackaging. # # Two values are deliberately NOT asserted as literals because they are not # reproducible between runs: the fallback `str()' of a stub carrying an # undispatched `__str__' embeds the stored lambda's address, so the probe # compares it against that stub's OWN repr instead; and nothing here asserts a # dict's iteration order except where the source guarantees it (`__repr__' # walks `self.__dict__', whose insertion order is a language guarantee since # 3.7, which is what `stub-repr-two' and `stub-repr-two-reversed' assert as a # PAIR rather than one standing for both). # # ---------------------------------------------------------------------------- # 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 one # rather than a discovery: this package is 3040 bytes of pure python over # `functools' and `sys', with no ext_modules, no file access, no subprocess 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 -- is one this package does not # touch. What is left is `type()' at runtime, dict ordering, the descriptor # protocol and the comparison dunders, all of which are the interpreter's own # and are exercised by python3-core's own suite before this one runs. # # 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. # # ============================================================================ P=python3-pretend # ---------------------------------------------------------------------------- # 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 MOD=$SITE/pretend.py DI=$SITE/pretend-1.0.9.dist-info PKGLIST=/var/lib/dpkg/info/python3-pretend.list PROBE=$BST_DATA/python3-pretend-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-pretend.$$ # 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/module-present test -s "$MOD" # A SINGLE MODULE, NOT A PACKAGE. setup.py declares `py_modules=["pretend"]' # rather than `packages=', so the payload lands as site-packages/pretend.py with # its bytecode in SITE-PACKAGES' OWN __pycache__, and there is no `pretend/' # directory. A release that became a package would relocate the whole payload # while every behavioural case below still passed. check $P/not-a-package test ! -d "$SITE/pretend" # 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 3040 \ "$(wc -c < "$MOD" 2>/dev/null | tr -d ' ')" # BST-REDIRECT-OK: forked -- inside $( ) check $P/pycache-present test -s "$SITE/__pycache__/pretend.cpython-312.pyc" # 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 "$SITE" -maxdepth 1 -name 'pretend*.so' 2>/dev/null)" check $P/no-bin-script test ! -e /usr/bin/pretend # ---------------------------------------------------------------------------- # 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: an entry appearing or # disappearing is the signal, and a per-file case cannot report an addition. check_eq $P/dist-info-file-set \ "LICENSE.rst,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. It is # the one file python3-vcversioner's dist-info has that this one does not, and # 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 "pretend" \ "$(cat "$DI/top_level.txt" 2>/dev/null | tr -d ' \r\n')" # THE LICENSE FILE IS SHIPPED, unlike python3-vcversioner's, because this sdist # carries a LICENSE.rst that MANIFEST.in includes -- which is also what the # recipe's LIC_FILES_CHKSUM checksums. check $P/license-file-present test -s "$DI/LICENSE.rst" check $P/license-file-text grep -q 'Alex Gaynor' "$DI/LICENSE.rst" check $P/metadata-license grep -q '^License: BSD$' "$DI/METADATA" check $P/metadata-version grep -q '^Version: 1\.0\.9$' "$DI/METADATA" check $P/metadata-name grep -q '^Name: pretend$' "$DI/METADATA" # A UNIVERSAL, PURE-PYTHON WHEEL. setup.cfg carries `[wheel] universal = 1', so # the built wheel is tagged for both py2 and py3 and `Root-Is-Purelib' is true. # Asserted because it is the metadata statement that there is no arch-specific # payload -- the same fact `no-so' asserts from the tree side. 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" # ---------------------------------------------------------------------------- # 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" # ---------------------------------------------------------------------------- # 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 pretend' if [ ! -s "$PROBE" ]; then bst_notok $P/probe-present "probe missing at $PROBE" 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 "139" "$(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 and thirty # 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="pretend did not import: $(pv import_error)" for _c in \ module-name is-package public-names has-version-attr has-all \ stub-is-class call-is-class recorder-callable raiser-callable \ py3k magic-count magic-is-frozenset magic-methods \ magic-has-next magic-has-bool magic-has-div magic-has-nonzero \ methods-leaked stub-a stub-b stub-c stub-c-is-same-object \ stub-undeclared stub-undeclared-dunder stub-dict stub-dict-len \ stub-setattr stub-callable-attr stub-callable-attr-args \ stub-callable-not-bound stub-empty-repr stub-repr \ stub-repr-two stub-repr-two-reversed stub-repr-uses-repr \ stub-type-name stub-type-module stub-type-is-stub \ stub-isinstance stub-type-base-is-stub stub-cache-shared-type \ stub-cache-magic-type-differs stub-cache-same-magic-shared \ stub-cache-is-dict stub-cache-key-frozenset \ stub-cache-has-empty-key magic-len magic-getitem magic-setitem \ magic-delitem magic-iter magic-contains magic-call magic-bool \ magic-next magic-add magic-mul magic-sub magic-lt magic-eq \ magic-ne magic-repr magic-context magic-exit-arity \ magic-no-self magic-dispatcher-name magic-attr-is-raw \ magic-not-in-table-str magic-str-in-table magic-hash-in-table \ raiser-class raiser-instance raiser-ignores-args \ raiser-rejects-int raiser-rejects-str raiser-rejects-object \ raiser-rejects-none raiser-accepts-baseexception \ raiser-accepts-systemexit raiser-reject-msg \ raiser-same-instance raiser-in-stub call-args call-kwargs \ call-args-is-tuple call-kwargs-is-dict call-empty-args \ call-empty-kwargs call-eq-positional call-eq-keyword \ call-eq-both call-eq-none call-eq-kwargs-order \ call-ne-arg-order call-ne-value call-ne-arity \ call-ne-kwarg-name call-ne-pos-vs-kw call-eq-not-ne \ call-ne-not-eq call-eq-other call-ne-other \ call-eq-other-reflected call-eq-returns-notimplemented \ call-eq-none-object call-hash-equal call-hash-kwargs-order \ call-in-set call-set-dedup call-hash-unhashable-arg \ call-hash-unhashable-kwarg call-eq-unhashable call-repr-empty \ call-repr-args call-repr-kwargs call-repr-both \ call-repr-uses-repr recorder-basic recorder-calls-is-list \ recorder-calls-initially-empty recorder-records-positional \ recorder-records-keyword recorder-records-both \ recorder-records-none recorder-records-repr recorder-order \ recorder-raises recorder-independent recorder-name \ recorder-doc recorder-wrapped-is recorder-returns \ recorder-identity recorder-on-stub recorder-builtin module-file; 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. Four public names and two imported stdlib ones -- and # two ABSENCES, `__version__' and `__all__', which are asserted because a # library whose subject is standing in for other objects is exactly the one a # reader assumes declares its own exports, and it does not. # ---------------------------------------------------------------------------- check_eq $P/module-name "pretend" "$(pv module_name)" check_eq $P/is-package "no" "$(pv is_package)" check_eq $P/public-names "MAGIC_METHODS,PY3K,call,call_recorder,functools,raiser,stub,sys" \ "$(pv public_names)" check_eq $P/has-version-attr "no" "$(pv has_version_attr)" check_eq $P/has-all "no" "$(pv has_all)" check_eq $P/stub-is-class "yes" "$(pv stub_is_class)" check_eq $P/call-is-class "yes" "$(pv call_is_class)" check_eq $P/recorder-callable "yes" "$(pv recorder_callable)" check_eq $P/raiser-callable "yes" "$(pv raiser_callable)" # ---------------------------------------------------------------------------- # THE MAGIC-METHOD TABLE. The one piece of data in this library, and a # WHITELIST: a kwarg whose name is not in it is stored as an ordinary attribute # and is never dispatched. Asserted as the whole sorted membership rather than # by sampling, because what matters is that nothing has been added or dropped. # # `PY3K' selects which two names are in it -- `__next__' and `__bool__' in, the # py2 spellings `__div__' and `__nonzero__' out -- so all four are asserted, two # as presences and two as absences. `methods-leaked' asserts that the # module-level set the table was built from was `del'ed, which keeps the # module's namespace the audited one above. # ---------------------------------------------------------------------------- check_eq $P/py3k "yes" "$(pv py3k)" check_eq $P/magic-count "31" "$(pv magic_count)" check_eq $P/magic-is-frozenset "yes" "$(pv magic_is_frozenset)" check_eq $P/magic-methods "__add__,__and__,__bool__,__call__,__contains__,__delitem__,__divmod__,__enter__,__eq__,__exit__,__floordiv__,__ge__,__getitem__,__gt__,__iter__,__le__,__len__,__lshift__,__lt__,__mod__,__mul__,__ne__,__next__,__or__,__pow__,__repr__,__rshift__,__setitem__,__sub__,__truediv__,__xor__" \ "$(pv magic_methods)" check_eq $P/magic-has-next "yes" "$(pv magic_has_next)" check_eq $P/magic-has-bool "yes" "$(pv magic_has_bool)" check_eq $P/magic-has-div "no" "$(pv magic_has_div)" check_eq $P/magic-has-nonzero "no" "$(pv magic_has_nonzero)" check_eq $P/methods-leaked "no" "$(pv methods_leaked)" # ---------------------------------------------------------------------------- # stub: THE CENTRAL PROPERTY. The attributes are exactly the kwargs, of the # values given -- by identity for a mutable value, not a copy -- and an # UNDECLARED attribute raises AttributeError rather than being auto-created. # `stub-undeclared' and `stub-undeclared-dunder' are the two cases a permissive # mock would fail and this library must not. # # `stub-setattr' bounds that claim from the other side: a stub is an ordinary # object with no __slots__, so the refusal is about what was DECLARED at # construction, not a freeze. # ---------------------------------------------------------------------------- check_eq $P/stub-a "1" "$(pv stub_a)" check_eq $P/stub-b "x" "$(pv stub_b)" check_eq $P/stub-c "[1, 2]" "$(pv stub_c)" check_eq $P/stub-c-is-same-object "yes" "$(pv stub_c_is_same_object)" check_eq $P/stub-undeclared "AttributeError" "$(pv stub_undeclared)" check_eq $P/stub-undeclared-dunder "AttributeError" \ "$(pv stub_undeclared_dunder)" check_eq $P/stub-dict "a=1,b='x',c=[1, 2]" "$(pv stub_dict)" check_eq $P/stub-dict-len "3" "$(pv stub_dict_len)" check_eq $P/stub-setattr "2" "$(pv stub_setattr)" # A CALLABLE ATTRIBUTE IS CALLED WITHOUT A SELF. It lives in the instance # __dict__, so the descriptor protocol never runs on it and it is never bound -- # a plain lambda of the arity the test wrote is what is called, and one written # `lambda self: ...' is a TypeError. check_eq $P/stub-callable-attr "7" "$(pv stub_callable_attr)" check_eq $P/stub-callable-attr-args "5" "$(pv stub_callable_attr_args)" check_eq $P/stub-callable-not-bound "TypeError" "$(pv stub_callable_not_bound)" # THE repr IS A LITERAL A TEST CAN COMPARE AGAINST, and it walks `self.__dict__' # -- so it is INSERTION ORDER, not sorted order. The two-attribute case is # asserted in both orders as a PAIR: one alone is equally consistent with a sort. check_eq $P/stub-empty-repr "" "$(pv stub_empty_repr)" check_eq $P/stub-repr "" "$(pv stub_repr)" check_eq $P/stub-repr-two "" "$(pv stub_repr_two)" check_eq $P/stub-repr-two-reversed "" \ "$(pv stub_repr_two_reversed)" check_eq $P/stub-repr-uses-repr "" "$(pv stub_repr_uses_repr)" # ---------------------------------------------------------------------------- # stub: EVERY INSTANCE IS OF A GENERATED SUBCLASS, NEVER OF `stub' ITSELF -- # true even with no magic methods at all. `stub-type-is-stub' (no) and # `stub-isinstance' (yes) are asserted as a pair because either alone reads as # the other, and an isinstance check in somebody's test rests on the second. # # The subclass cache is keyed by the MAGIC-METHOD SET and by nothing else, which # is what the three `stub-cache-*' type cases assert from both directions: two # stubs with different ordinary attributes share one class, two with different # magic sets do not, and two with the same magic set share again. # ---------------------------------------------------------------------------- check_eq $P/stub-type-name "stub" "$(pv stub_type_name)" check_eq $P/stub-type-module "pretend" "$(pv stub_type_module)" check_eq $P/stub-type-is-stub "no" "$(pv stub_type_is_stub)" check_eq $P/stub-isinstance "yes" "$(pv stub_isinstance)" check_eq $P/stub-type-base-is-stub "yes" "$(pv stub_type_base_is_stub)" check_eq $P/stub-cache-shared-type "yes" "$(pv stub_cache_shared_type)" check_eq $P/stub-cache-magic-type-differs "no" \ "$(pv stub_cache_magic_type_differs)" check_eq $P/stub-cache-same-magic-shared "yes" \ "$(pv stub_cache_same_magic_shared)" check_eq $P/stub-cache-is-dict "yes" "$(pv stub_cache_is_dict)" check_eq $P/stub-cache-key-frozenset "yes" "$(pv stub_cache_key_frozenset)" check_eq $P/stub-cache-has-empty-key "yes" "$(pv stub_cache_has_empty_key)" # ---------------------------------------------------------------------------- # stub: THE DISPATCHERS. Each case goes through the OPERATOR or the builtin -- # `len(s)', `s[k]', `del s[k]', `with s', `next(s)', `s + 1' -- and never by # calling the attribute, which would pass with no dispatcher generated at all. # ---------------------------------------------------------------------------- check_eq $P/magic-len "3" "$(pv magic_len)" check_eq $P/magic-getitem "42" "$(pv magic_getitem)" check_eq $P/magic-setitem "k=v" "$(pv magic_setitem)" check_eq $P/magic-delitem "k" "$(pv magic_delitem)" check_eq $P/magic-iter "1,2,3" "$(pv magic_iter)" check_eq $P/magic-contains "yes" "$(pv magic_contains)" check_eq $P/magic-call "10" "$(pv magic_call)" check_eq $P/magic-bool "no" "$(pv magic_bool)" check_eq $P/magic-next "42" "$(pv magic_next)" check_eq $P/magic-add "add:1" "$(pv magic_add)" check_eq $P/magic-mul "12" "$(pv magic_mul)" check_eq $P/magic-sub "6" "$(pv magic_sub)" check_eq $P/magic-lt "yes" "$(pv magic_lt)" check_eq $P/magic-eq "yes" "$(pv magic_eq)" check_eq $P/magic-ne "yes" "$(pv magic_ne)" check_eq $P/magic-repr "" "$(pv magic_repr)" # The context-manager pair through the `with' statement itself. `__exit__' is # handed THREE arguments even on the clean path, which `magic-context' asserts # as the count and `magic-exit-arity' asserts from the failing side: a stub # whose __exit__ takes none raises TypeError at the END of the block, not its # start. That is the arity mistake this library's users make. check_eq $P/magic-context "resource,3" "$(pv magic_context)" check_eq $P/magic-exit-arity "TypeError" "$(pv magic_exit_arity)" # THE DISPATCHER PASSES NO SELF, asserted from the failing side too. The # dispatcher is renamed to the method it serves (so a traceback through a stub # is readable) and lives on the TYPE, while the supplied callable stays in the # instance __dict__ -- so reading the attribute gives back the raw callable. check_eq $P/magic-no-self "TypeError" "$(pv magic_no_self)" check_eq $P/magic-dispatcher-name "__len__" "$(pv magic_dispatcher_name)" check_eq $P/magic-attr-is-raw "yes" "$(pv magic_attr_is_raw)" # A DUNDER OUTSIDE THE TABLE IS NOT DISPATCHED. `__str__' is not a member, so # supplying it stores an attribute nothing consults and `str()' falls back to # `__repr__', which IS a member and is the default one here. The value is a # triple rather than the fallback text itself: that text embeds the stored # lambda's address and is not reproducible between runs. check_eq $P/magic-not-in-table-str "yes,yes,yes" "$(pv magic_not_in_table_str)" check_eq $P/magic-str-in-table "no" "$(pv magic_str_in_table)" check_eq $P/magic-hash-in-table "no" "$(pv magic_hash_in_table)" # ---------------------------------------------------------------------------- # raiser. Turns an exception into a callable that raises it, so a stub's # attribute can model a collaborator that fails. It takes a CLASS or an # INSTANCE and refuses anything else AT CONSTRUCTION -- four rejections are # asserted, plus the message, because catching the mistake where it was made # rather than at call time is the whole value of the check. # # The bound is BaseException, not Exception, so KeyboardInterrupt and SystemExit # are accepted -- which is what lets a test model a signal, and is why both are # asserted rather than assumed from the Exception case. # ---------------------------------------------------------------------------- check_eq $P/raiser-class "ValueError" "$(pv raiser_class)" check_eq $P/raiser-instance "KeyError" "$(pv raiser_instance)" check_eq $P/raiser-ignores-args "ValueError" "$(pv raiser_ignores_args)" check_eq $P/raiser-rejects-int "TypeError" "$(pv raiser_rejects_int)" check_eq $P/raiser-rejects-str "TypeError" "$(pv raiser_rejects_str)" check_eq $P/raiser-rejects-object "TypeError" "$(pv raiser_rejects_object)" check_eq $P/raiser-rejects-none "TypeError" "$(pv raiser_rejects_none)" check_eq $P/raiser-accepts-baseexception "KeyboardInterrupt" \ "$(pv raiser_accepts_baseexception)" check_eq $P/raiser-accepts-systemexit "SystemExit" \ "$(pv raiser_accepts_systemexit)" check_eq $P/raiser-reject-msg "exc must be either an exception instance or class." \ "$(pv raiser_reject_msg)" check_eq $P/raiser-same-instance "yes" "$(pv raiser_same_instance)" check_eq $P/raiser-in-stub "ZeroDivisionError" "$(pv raiser_in_stub)" # ---------------------------------------------------------------------------- # call. A recorded invocation, compared against a literal a test writes -- the # whole library is used through that comparison, so equality and inequality are # asserted in both directions and against a non-`call' object. # ---------------------------------------------------------------------------- check_eq $P/call-args "(1, 2)" "$(pv call_args)" check_eq $P/call-kwargs "[('a', 3)]" "$(pv call_kwargs)" check_eq $P/call-args-is-tuple "yes" "$(pv call_args_is_tuple)" check_eq $P/call-kwargs-is-dict "yes" "$(pv call_kwargs_is_dict)" check_eq $P/call-empty-args "()" "$(pv call_empty_args)" check_eq $P/call-empty-kwargs "{}" "$(pv call_empty_kwargs)" # EQUALITY over the four argument shapes, then the ways two calls DIFFER. # Keyword order does not matter (the kwargs are a dict) and positional order # does; a positional argument is NOT equal to a keyword one of the same value, # because `call' compares what was WRITTEN and not what a callee would bind. # # `__ne__' is written by hand as `not (self == other)' rather than inherited, so # `call-eq-not-ne' and `call-ne-not-eq' are separate measurements and not # consequences of the cases above them. check_eq $P/call-eq-positional "yes" "$(pv call_eq_positional)" check_eq $P/call-eq-keyword "yes" "$(pv call_eq_keyword)" check_eq $P/call-eq-both "yes" "$(pv call_eq_both)" check_eq $P/call-eq-none "yes" "$(pv call_eq_none)" check_eq $P/call-eq-kwargs-order "yes" "$(pv call_eq_kwargs_order)" check_eq $P/call-ne-arg-order "yes" "$(pv call_ne_arg_order)" check_eq $P/call-ne-value "yes" "$(pv call_ne_value)" check_eq $P/call-ne-arity "yes" "$(pv call_ne_arity)" check_eq $P/call-ne-kwarg-name "yes" "$(pv call_ne_kwarg_name)" check_eq $P/call-ne-pos-vs-kw "yes" "$(pv call_ne_pos_vs_kw)" check_eq $P/call-eq-not-ne "yes" "$(pv call_eq_not_ne)" check_eq $P/call-ne-not-eq "yes" "$(pv call_ne_not_eq)" # AGAINST A NON-`call' OBJECT. `__eq__' returns NotImplemented, python falls # back to identity, and the answers are False and True -- not a TypeError and # not an accidental match. Asserted through the operators AND, for # NotImplemented itself, through the dunder directly, since the operator can # never show it. check_eq $P/call-eq-other "no" "$(pv call_eq_other)" check_eq $P/call-ne-other "yes" "$(pv call_ne_other)" check_eq $P/call-eq-other-reflected "no" "$(pv call_eq_other_reflected)" check_eq $P/call-eq-returns-notimplemented "yes" \ "$(pv call_eq_returns_notimplemented)" check_eq $P/call-eq-none-object "no" "$(pv call_eq_none_object)" # HASHABLE, so recorded calls go into a set or a dict key -- but only as far as # their arguments are. An unhashable argument makes the call unhashable on # either side, and that is a TypeError at HASH time rather than at construction, # while EQUALITY over the same argument still works because it compares rather # than hashes. check_eq $P/call-hash-equal "yes" "$(pv call_hash_equal)" check_eq $P/call-hash-kwargs-order "yes" "$(pv call_hash_kwargs_order)" check_eq $P/call-in-set "yes" "$(pv call_in_set)" check_eq $P/call-set-dedup "2" "$(pv call_set_dedup)" check_eq $P/call-hash-unhashable-arg "TypeError" \ "$(pv call_hash_unhashable_arg)" check_eq $P/call-hash-unhashable-kwarg "TypeError" \ "$(pv call_hash_unhashable_kwarg)" check_eq $P/call-eq-unhashable "yes" "$(pv call_eq_unhashable)" # THE repr, in all four shapes. The comma between the positional and keyword # groups is CONDITIONAL, which is why each shape is asserted rather than one # standing for the rest. check_eq $P/call-repr-empty "" "$(pv call_repr_empty)" check_eq $P/call-repr-args "" "$(pv call_repr_args)" check_eq $P/call-repr-kwargs "" "$(pv call_repr_kwargs)" check_eq $P/call-repr-both "" "$(pv call_repr_both)" check_eq $P/call-repr-uses-repr "" \ "$(pv call_repr_uses_repr)" # ---------------------------------------------------------------------------- # call_recorder. Records every invocation as a `call' on `.calls' and then does # what the wrapped callable did. Both halves matter: a recorder that swallowed # the return value would be a mock, and the point of this library is that it is # not one. # # `recorder-basic' is the composite -- two returns, the ledger length, and the # ledger compared against `call' literals -- and the `recorder-records-*' cases # take the four argument shapes one at a time so a single shape failing is # legible on its own. # ---------------------------------------------------------------------------- check_eq $P/recorder-basic "3,7,2,yes" "$(pv recorder_basic)" check_eq $P/recorder-calls-is-list "yes" "$(pv recorder_calls_is_list)" check_eq $P/recorder-calls-initially-empty "0" \ "$(pv recorder_calls_initially_empty)" check_eq $P/recorder-records-positional "yes" \ "$(pv recorder_records_positional)" check_eq $P/recorder-records-keyword "yes" "$(pv recorder_records_keyword)" check_eq $P/recorder-records-both "yes" "$(pv recorder_records_both)" check_eq $P/recorder-records-none "yes" "$(pv recorder_records_none)" check_eq $P/recorder-records-repr "" \ "$(pv recorder_records_repr)" check_eq $P/recorder-order "yes" "$(pv recorder_order)" # THE CALL IS RECORDED BEFORE THE WRAPPED CALLABLE RUNS, so an exception # PROPAGATES and the call is STILL recorded. Asserted as a triple -- exception # type, ledger length, recorded value -- because the ledger being right matters # as much as the exception arriving. check_eq $P/recorder-raises "ValueError,1,yes" "$(pv recorder_raises)" check_eq $P/recorder-independent "1,0" "$(pv recorder_independent)" # functools.wraps IS APPLIED, so the wrapper keeps the wrapped callable's # identity for anything that introspects it -- a test runner naming the function # in a failure, or a decorator further out. check_eq $P/recorder-name "_wrapped" "$(pv recorder_name)" check_eq $P/recorder-doc "docstring." "$(pv recorder_doc)" check_eq $P/recorder-wrapped-is "yes" "$(pv recorder_wrapped_is)" check_eq $P/recorder-returns "r" "$(pv recorder_returns)" # ARGUMENTS PASS THROUGH BY IDENTITY, not by copy: the recorder holds the same # object the caller passed, so a test that mutates a recorded argument afterwards # sees the mutation in `.calls'. A genuine sharp edge, asserted rather than # hidden -- the third field of the value is the list AFTER the mutation. # # `recorder-on-stub' is the shape the library is actually used in, with the two # halves together; `recorder-builtin' wraps `len', which is an ordinary thing to # do in a test suite and which works because a builtin has the `__name__' and # `__doc__' functools.wraps copies. check_eq $P/recorder-identity "yes,yes,[1, 2]" "$(pv recorder_identity)" check_eq $P/recorder-on-stub "value:k,yes" "$(pv recorder_on_stub)" check_eq $P/recorder-builtin "3,len,yes" "$(pv recorder_builtin)" rm -rf "$W" exit 0