. "$BST_LIB" # BST-TIMEOUT: 1200 # THREE python3 starts, and they are the whole cost. Every # # other assertion below reads the filesystem or dpkg with # # shell builtins, or greps a line out of the transcript the # # third start left behind. # # # # The three are: a bare `import', a second one whose STDERR # # is the assertion (`import-quiet'), and the probe, which # # takes ONE start for 93 measurements. # # # # The work inside the probe is ~40 small XML documents, # # none over 90 bytes, each serialised by xml.etree and most # # of them read straight back. There is no loop over # # anything that grows and no input but the fixtures the # # probe writes itself, so the interpreter start is the only # # term the number has to cover. # # # # 1200 is 400s per start, the same per-start budget # # 99-python3-appdirs sets for its three, and roughly six # # times the ~60s a python3 start has measured at here. # # # # A suite-timeout red is a batch-wide cost, so if the three # # starts prove slower than that 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-et-xmlfile 1.1.0 (meta-openembedded meta-python/recipes-devtools/ # python, python3-et-xmlfile_1.1.0.bb, `inherit setuptools3 pypi', PyPI sdist) # on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE ACTUALLY IS, AND THEREFORE WHAT HAS TO BE TESTED. # ============================================================================ # # et_xmlfile IS 104 LINES OF PYTHON THAT WRAP `xml.etree.ElementTree'. It # exports one name, `xmlfile', a context manager whose `__enter__' hands back a # writer with two methods: `element(tag, attrib, nsmap, **extra)', itself a # context manager, and `write(arg)', which takes either a string or an Element. # It exists so that code written against `lxml.etree.xmlfile' runs unchanged # where lxml is not available; it is the writer openpyxl is built on. # # It has NO C, no configure step and no compile step, so this port carries NO # PATCH, NO BBAPPEND AND NO SITE ANSWER -- the recipe is meta-openembedded's # taken as written, and the port is the IMAGE_INSTALL line, this suite and its # probe. There is nothing on this arm for an override to reach. # # So the subject is a SERIALISATION RULE, and its output is BYTES. Every # byte-level expectation below is carried as `repr(bytes)' -- one line of pure # ASCII whatever the document held -- and every one of them was MEASURED, first # against the unpacked sdist and then again against the module extracted from # the built .deb. # # ============================================================================ # THE "LOW MEMORY" CLAIM IS TRUE ONLY OF A DOCUMENT THAT DOES NOT PARSE. # ============================================================================ # # This is the finding the suite is built around, and it is three cases, not one. # # `element()'s exit path serialises through `_write_element' ONLY when the # element stack has emptied -- that is, only for a TOP-LEVEL element. So: # # * `oneroot-sizes-during' opens ONE root, writes three children inside it, # and flushes the file after each. The file is `0,0,0' bytes. The whole # document is in memory until the root closes. That is the only shape that # yields well-formed XML, and it buys no incremental writing at all. # # * `incr-after-first' / `incr-after-second' open two SIBLING top-level # elements. Each one IS flushed as it closes -- the claim holds -- and # `incr-parses' then measures what that produced: `!ParseError', because a # document with two roots is not XML. # # A caller who wants both properties cannot have them from this library. That # is upstream's design and not a defect of this port, which is why these are # recorded as measurements rather than filed; but a caller reading the package # summary ("a low memory library for creating large XML files") will get it # wrong, and this suite is where that is written down. # # ============================================================================ # THE MISSING-ROOT CHECK IS DEAD CODE UNDER THE DOCUMENTED IDIOM. # ============================================================================ # # `_FakeIncrementalFileWriter.__exit__' carries `if not self._have_root: raise # LxmlSyntaxError()'. It never runs. `with xmlfile(f) as xf' binds # `xmlfile.__exit__', not the writer's -- `xmlfile.__enter__' RETURNS a writer # rather than being one -- so the check is unreachable from the only spelling # the package documents. # # Four cases state that as a pair of opposites rather than as a claim: # `no-root-documented-idiom' is `' and leaves an EMPTY FILE, while # `no-root-writer-exit' and `no-root-nested-with' reach the same line directly # and do raise. `writer-exit-is-xmlfile-exit' is `no', which is why. # # ============================================================================ # `_have_root' IS SET BY `element()' AND NEVER BY `write()'. # ============================================================================ # # The guard that is supposed to stop a second root only sees one of the two ways # to make one. `two-prebuilt-bytes' writes two pre-built Elements at the top # level and gets `b''' with no error; `prebuilt-then-element' does the # same across the two APIs and is also silent. Only `element-after-root' -- the # one ordering `_have_root' covers -- raises. The asymmetry is the point, so the # cases sit together. # # ============================================================================ # THREE MORE THINGS A CALLER WILL GET WRONG. # ============================================================================ # # `encoding' AND `buffered' ARE ACCEPTED AND DISCARDED. # Both are in `xmlfile.__init__''s signature and neither is # ever stored -- `instance-attrs' is `_close,_file' and # nothing else. The writer calls `tostring(element)' with no # encoding, so a caller who passes `encoding="utf-8"' gets # NO XML DECLARATION (`no-declaration') and a non-ASCII # character as a NUMERIC CHARACTER REFERENCE # (`nonascii-bytes'), not as UTF-8 bytes. `nsmap' on # `element()' is discarded the same way: `nsmap-bytes' has no # xmlns on it. # # MIXING `write(Element)' AND `write(str)' INSIDE ONE ELEMENT RAISES TypeError. # A trailing string goes to `self._top_element[-1].tail', and # a tail is `None' on any Element the CALLER built while # `element()' initialises its own to the empty string. So # `text-after-prebuilt-child' is `!TypeError' out of # `str.__add__' -- not `LxmlSyntaxError', and not anything # et_xmlfile says -- while `text-after-element-child', the # same shape one API over, works. # # `close' IS COMPARED WITH `== True', NOT FOR TRUTHINESS. # `close=1' closes, because `1 == True'; `close="yes"' does # NOT, and the file is left open with no indication. # `close-int-one-closes' and `close-truthy-str-closes' are # that pair. # # ============================================================================ P=python3-et-xmlfile # The canary is first so that a suite which dies before its own first assertion # is distinguishable from one that never started. bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKG=$SITE/et_xmlfile DI=$SITE/et_xmlfile-1.1.0.dist-info PKGLIST=/var/lib/dpkg/info/python3-et-xmlfile.list PROBE=$BST_DATA/python3-et-xmlfile-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-et-xmlfile.$$ mkdir -p "$W/fixtures" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ---------------------------------------------------------------- layout ----- # A PACKAGE, NOT A FLAT MODULE -- two modules under one directory, unlike # appdirs next door. `is-package' below measures the same thing from inside the # interpreter. check $P/site-packages-dir test -d "$SITE" check $P/package-dir test -d "$PKG" check $P/package-init test -s "$PKG/__init__.py" check $P/package-xmlfile test -s "$PKG/xmlfile.py" check $P/not-a-flat-module test ! -e "$SITE/et_xmlfile.py" # Upstream's tests are not packaged, and neither is the `tests' subpackage that # sits beside the two modules in the sdist. check $P/no-tests-dir test ! -d "$PKG/tests" check $P/no-py-typed test ! -e "$PKG/py.typed" check $P/pycache-init test -s "$PKG/__pycache__/__init__.cpython-312.pyc" check $P/pycache-xmlfile test -s "$PKG/__pycache__/xmlfile.cpython-312.pyc" # ------------------------------------------------------------- the .deb ------ cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" # PURE PYTHON, AND NOT MERELY "NO .so OF ITS OWN". This image's python3 is ONE # STATIC BINARY with an empty lib-dynload and no dynamic loader (BRINTOS-1476), # so a module carrying a compiled extension could not be imported here at all. # et_xmlfile ships no C -- no .c, no .pyx, no ext_modules in setup.py -- so this # is a REGRESSION TRIPWIRE on a version bump rather than a statement about # today. check_eq $P/pkg-no-so "" \ "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" # ...and nothing but Python in the payload, which is the same claim from the # other side. setup.py declares no `entry_points' and no `scripts', so the # package installs NO executable and there is nothing to reach through a shebang # -- which is why this suite has no CLI cases at all. _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" 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.1.0" cat "$DI/METADATA" # Underscore, not the PEP 503 hyphen: setup.py declares name="et_xmlfile" and # this bdist_wheel copies it into METADATA verbatim without normalising. check_out $P/dist-info-name "Name: et_xmlfile" cat "$DI/METADATA" check $P/dist-info-record test -s "$DI/RECORD" # `setuptools3' BUILT THIS, which is visible in the wheel metadata and is the # half of the recipe's `inherit' line that leaves a trace on the rootfs. check_out $P/dist-info-generator "Generator: bdist_wheel" cat "$DI/WHEEL" check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" check_out $P/dist-info-tag-py3 "Tag: py3-none-any" cat "$DI/WHEEL" check_out $P/dist-info-top-level "et_xmlfile" cat "$DI/top_level.txt" sed -n '/^Package: python3-et-xmlfile$/,/^$/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" check_out $P/pkg-version "Version: 1.1.0-r0" cat "$W/st" # THE RDEPENDS IS ASSERTED EXACTLY, and it is the one part of this recipe that # is not simply upstream's default. The module imports `contextlib' and # `xml.etree.ElementTree'; the recipe declares five splits by hand # (python3-compression, python3-io, python3-pprint, python3-shell, python3-xml) # on top of the python3-core that setuptools3-base brings. A lean image that # dropped one of those would say so HERE, once, rather than present as a library # that raises ImportError at the first `import'. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" for _d in python3-compression python3-io python3-pprint python3-shell \ python3-xml python3-core; do check_out $P/pkg-depends-$_d "$_d" cat "$W/dep" done # ...and each declared split is actually installed, which is what the Depends # line is a claim about. for _d in python3-compression python3-io python3-pprint python3-shell \ python3-xml; do check $P/split-installed-$_d test -s "/var/lib/dpkg/info/$_d.list" done # ---------------------------------------------------------------- import ----- check $P/import python3 -c 'import et_xmlfile' # A DeprecationWarning or a stray print on import would be invisible to the case # above, which scores only the exit status. `from __future__ import # absolute_import' at the top of both modules is a Python-2 relic and is the # most likely source of one on a future interpreter. check_eq $P/import-quiet "" \ "$(python3 -c 'import et_xmlfile' 2>&1)" # ----------------------------------------------------------------- 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 bst_probe $P/probe-run "$W/probe" 400 python3 "$PROBE" "$W/fixtures" OUT="$W/probe" pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } 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 # The probe's own two bookends. `probe-complete' says it reached its last line # and `probe-count' says it emitted every key it meant to: a probe that died # two-thirds of the way through would otherwise leave the missing cases scoring # against an empty string, which `check_eq' would read as a wrong answer rather # than as an absent one. check_eq $P/probe-complete 'yes' "$(pv probe_done)" check_eq $P/probe-count '93' "$(pv emitted_count)" # -------------------------------------------------------------- identity ----- # WHICH COPY ANSWERED. Every behavioural case below is a statement about the # package named here. check_eq $P/module-answered "$PKG/__init__.py" "$(pv module_file)" check_eq $P/is-package 'yes' "$(pv is_package)" check_eq $P/version-attr '1.1.0' "$(pv version_attr)" check_eq $P/license-attr 'MIT' "$(pv license_attr)" check_eq $P/author-attr 'See ATUHORS.txt' "$(pv author_attr)" check_eq $P/url-attr 'https://foss.heptapod.net/openpyxl/et_xmlfile' "$(pv url_attr)" # NO `__all__', so a star-import takes whatever the package binds -- which is # `xmlfile' and the four dunder strings above, and nothing else. check_eq $P/no-dunder-all 'no' "$(pv has___all__)" # `from .xmlfile import xmlfile' REBINDS THE SUBMODULE NAME TO THE CLASS. # After the package is imported, `et_xmlfile.xmlfile' is the CLASS and not the # module it came from -- so `from et_xmlfile.xmlfile import LxmlSyntaxError' # still works (the import system reads sys.modules, not the attribute) while # `et_xmlfile.xmlfile.LxmlSyntaxError' raises AttributeError. A caller who # reaches for the exception through the package gets nothing: it is not # re-exported either. check_eq $P/xmlfile-attr-kind 'type' "$(pv xmlfile_attr_kind)" check_eq $P/xmlfile-is-class 'yes' "$(pv xmlfile_is_class)" check_eq $P/submodule-in-sys-modules 'yes' "$(pv submodule_in_sys_modules)" check_eq $P/pkg-no-lxmlsyntaxerror 'no' "$(pv pkg_has_LxmlSyntaxError)" check_eq $P/attr-via-pkg-raises '!AttributeError' "$(pv attr_via_pkg_raises)" check_eq $P/writer-class '_FakeIncrementalFileWriter' "$(pv writer_class)" check_eq $P/lxmlsyntaxerror-base 'Exception' "$(pv lxmlsyntaxerror_base)" # THE TWO SIGNATURES, because that is where the ignored parameters live and a # release that finally honoured one of them would change these lines first. check_eq $P/xmlfile-signature '(self, output_file, buffered=False, encoding=None, close=False)' "$(pv xmlfile_signature)" check_eq $P/element-signature '(self, tag, attrib=None, nsmap=None, **_extra)' "$(pv element_signature)" check_eq $P/writer-methods '_write_element,element,write' "$(pv writer_methods)" # --------------------------------------------------------- serialisation ---- # THE SIMPLE SHAPES, byte for byte. An empty element self-closes with a SPACE # before the slash -- `' -- which is xml.etree's spelling and not the # one most hand-written comparators expect. check_eq $P/one-bytes 'b'"'"''"'"'' "$(pv one_bytes)" check_eq $P/text-bytes 'b'"'"'hello'"'"'' "$(pv text_bytes)" check_eq $P/nested-bytes 'b'"'"'inner'"'"'' "$(pv nested_bytes)" check_eq $P/siblings-bytes 'b'"'"'12'"'"'' "$(pv siblings_bytes)" check_eq $P/attrib-bytes 'b'"'"''"'"'' "$(pv attrib_bytes)" # ATTRIBUTES ARRIVE TWO WAYS and both reach `Element()': the `attrib' mapping # and `**_extra'. They are merged, and the serialised order is the merged # insertion order rather than anything sorted. check_eq $P/extra-bytes 'b'"'"''"'"'' "$(pv extra_bytes)" # `nsmap' IS ACCEPTED AND DISCARDED. It is in the signature for lxml source # compatibility and is never read, so a caller who passes it gets no xmlns and # no error -- see `ns-bytes' below for the spelling that does work. check_eq $P/nsmap-bytes 'b'"'"''"'"'' "$(pv nsmap_bytes)" # ------------------------------------------------------------- escaping ----- # `&' and `<' ARE ESCAPED IN TEXT and `>' is too; `"' and `'' are NOT. In an # ATTRIBUTE value `"' is escaped as well, because it is the delimiter. This is # xml.etree's table, inherited whole, and the two cases sit together so the # difference is legible as one rule with two contexts. check_eq $P/esc-text-bytes 'b'"'"'a & b < c > d " e \'"'"' f'"'"'' "$(pv esc_text_bytes)" check_eq $P/esc-attr-bytes 'b'"'"''"'"'' "$(pv esc_attr_bytes)" # NON-ASCII BECOMES A NUMERIC CHARACTER REFERENCE, NOT A UTF-8 BYTE. # `_write_element' calls `tostring(element)' with no encoding, and that # default is us-ascii-with-charrefs. So the file is PURE ASCII whatever the # document held -- it decodes as UTF-8 trivially, which is a much weaker # statement than it looks and is why both are asserted -- and the characters # come back only through a parser. check_eq $P/nonascii-bytes 'b'"'"'café € 😀'"'"'' "$(pv nonascii_bytes)" check_eq $P/nonascii-is-ascii 'yes' "$(pv nonascii_is_ascii)" check_eq $P/nonascii-utf8-decodes 'yes' "$(pv nonascii_utf8_decodes)" check_eq $P/nonascii-roundtrip-codepoints '99,97,102,233,32,8364,32,128512' "$(pv nonascii_roundtrip_codepoints)" # ...AND THERE IS NO XML DECLARATION, EVER. `tostring` emits one only when it # is given an encoding, and it never is. check_eq $P/no-declaration 'yes' "$(pv no_declaration)" # --------------------------------------- `encoding' AND `buffered' IGNORED --- # Neither is stored on the instance and neither is read. `instance-attrs' is # the whole of what `__init__` sets, and it is two names. check_eq $P/encoding-arg-bytes 'b'"'"'café'"'"'' "$(pv encoding_arg_bytes)" check_eq $P/buffered-arg-bytes 'b'"'"'x'"'"'' "$(pv buffered_arg_bytes)" check_eq $P/encoding-stored 'no' "$(pv encoding_stored)" check_eq $P/buffered-stored 'no' "$(pv buffered_stored)" check_eq $P/instance-attrs '_close,_file' "$(pv instance_attrs)" # ----------------------------------------------------------- namespaces ----- # A JAMES-CLARK TAG IS THE SPELLING THAT WORKS. xml.etree invents the `ns0' # prefix and declares it on the outermost element carrying it. check_eq $P/ns-bytes 'b'"'"'v'"'"'' "$(pv ns_bytes)" check_eq $P/ns-roundtrip-tag '{http://example.com/ns}root' "$(pv ns_roundtrip_tag)" # ------------------------------------------------- a pre-built Element ------ # `write(Element)' TAKES A WHOLE SUBTREE, text, attributes, children and all. # At the top level it becomes the document; inside an open element it becomes # a child. check_eq $P/prebuilt-bytes 'b'"'"'
bodydeep
'"'"'' "$(pv prebuilt_bytes)" check_eq $P/prebuilt-child-bytes 'b'"'"'k'"'"'' "$(pv prebuilt_child_bytes)" # ------------------------------------------ THE INCREMENTAL CLAIM, MEASURED -- # See the header. Two sibling top-level elements ARE written as they close -- # and the document that produces has two roots and does not parse. check_eq $P/incr-after-first 'b'"'"'1'"'"'' "$(pv incr_after_first)" check_eq $P/incr-inside-second 'b'"'"'1'"'"'' "$(pv incr_inside_second)" check_eq $P/incr-after-second 'b'"'"'12'"'"'' "$(pv incr_after_second)" check_eq $P/incr-final 'b'"'"'12'"'"'' "$(pv incr_final)" check_eq $P/incr-parses '!ParseError' "$(pv incr_parses)" # ...while the ONE shape that does parse writes nothing until the root closes. # `oneroot-sizes-during' is the file's size after each of three children was # closed and the handle flushed. It is zeroes. This is the header's central # claim, measured rather than read off the source. check_eq $P/oneroot-sizes-during '0,0,0' "$(pv oneroot_sizes_during)" check_eq $P/oneroot-after-root 'b'"'"'012'"'"'' "$(pv oneroot_after_root)" check_eq $P/oneroot-parses '' "$(pv oneroot_parses)" # ----------------------------------------------------------- error paths ---- # A STRING OUTSIDE ANY ELEMENT RAISES, both before the first element and after # the last one. This is the one guard that works from either side. check_eq $P/str-before-any '!LxmlSyntaxError' "$(pv str_before_any)" check_eq $P/str-after-root '!LxmlSyntaxError' "$(pv str_after_root)" # THE MISSING-ROOT CHECK IS DEAD CODE UNDER THE DOCUMENTED IDIOM -- see the # header. The first two cases are the idiom and produce no error and an empty # file; the next two reach the same line directly and do raise; # `writer-exit-is-xmlfile-exit' is why. check_eq $P/no-root-documented-idiom '' "$(pv no_root_documented_idiom)" check_eq $P/no-root-bytes 'b'"'"''"'"'' "$(pv no_root_bytes)" check_eq $P/no-root-writer-exit '!LxmlSyntaxError' "$(pv no_root_writer_exit)" check_eq $P/no-root-nested-with '!LxmlSyntaxError' "$(pv no_root_nested_with)" check_eq $P/writer-exit-is-xmlfile-exit 'no' "$(pv writer_exit_is_xmlfile_exit)" # BOTH `__enter__'s ON THE WRITER SIDE YIELD None, so `with xf.element("a") as # e' binds None rather than an element -- there is no handle to reach the # element being built, by design. check_eq $P/writer-enter-returns 'None' "$(pv writer_enter_returns)" check_eq $P/element-yields 'None' "$(pv element_yields)" # ------------------------------------------- MORE THAN ONE ROOT, SILENTLY ---- # `_have_root' is set by `element()' and never by `write()' -- see the header. # Two pre-built Elements at the top level produce a two-root document with no # error; only the one ordering the flag covers raises. check_eq $P/two-prebuilt-raises 'no' "$(pv two_prebuilt_raises)" check_eq $P/two-prebuilt-bytes 'b'"'"''"'"'' "$(pv two_prebuilt_bytes)" check_eq $P/two-prebuilt-parses '!ParseError' "$(pv two_prebuilt_parses)" check_eq $P/prebuilt-then-element '' "$(pv prebuilt_then_element)" check_eq $P/prebuilt-then-element-bytes 'b'"'"''"'"'' "$(pv prebuilt_then_element_bytes)" check_eq $P/element-after-root '!LxmlSyntaxError' "$(pv element_after_root)" # -------------------------------------------- MIXING THE TWO WRITE APIS ------ # A trailing string goes to `[-1].tail', which is None on a caller-built # Element and the empty string on one `element()' made -- see the header. One # API apart, opposite outcomes. check_eq $P/text-after-prebuilt-child '!TypeError' "$(pv text_after_prebuilt_child)" check_eq $P/text-after-element-child 'b'"'"'tail'"'"'' "$(pv text_after_element_child)" # A NON-STRING, NON-ELEMENT ARGUMENT IS NOT VALIDATED. It goes down the `else' # branch, is appended to the tree, and the raise comes out of ElementTree's # serialiser at the end rather than out of the call that was wrong. A bytes # object is the likely mistake here and is not special-cased. check_eq $P/write-int '!TypeError' "$(pv write_int)" check_eq $P/write-bytes '!TypeError' "$(pv write_bytes)" # A TEXT-MODE FILE IS NOT USABLE: `_write_element' hands `file.write' the # bytes `tostring()' returned. There is no check and no message; it is a # TypeError from the file object. check_eq $P/text-mode-file '!TypeError' "$(pv text_mode_file)" # ---------------------------------------------------------- file handling --- # A STRING ARGUMENT IS OPENED 'wb' AND CLOSED; a file OBJECT is left open # unless `close=True'. `_close' is the only thing `__init__' branches on. check_eq $P/str-arg-close-flag 'yes' "$(pv str_arg_close_flag)" check_eq $P/str-arg-mode 'wb' "$(pv str_arg_mode)" check_eq $P/obj-arg-close-flag 'no' "$(pv obj_arg_close_flag)" check_eq $P/obj-arg-close-true 'yes' "$(pv obj_arg_close_true)" check_eq $P/obj-left-open 'yes' "$(pv obj_left_open)" check_eq $P/obj-closed-on-exit 'yes' "$(pv obj_closed_on_exit)" # `close' IS COMPARED WITH `== True', NOT FOR TRUTHINESS -- see the header. check_eq $P/close-int-one-closes 'yes' "$(pv close_int_one_closes)" check_eq $P/close-truthy-str-closes 'no' "$(pv close_truthy_str_closes)" # AN os.PathLike IS NOT A str, so `isinstance(output_file, str)' is False and # a Path is treated as an already-open file object. The failure is an # AttributeError on `.write' at the first element, not anything about paths. check_eq $P/pathlike-arg '!AttributeError' "$(pv pathlike_arg)" # ------------------------------------------------------------ round trips --- # EVERY WELL-FORMED DOCUMENT ABOVE PARSES BACK TO THE STRUCTURE THAT WAS ASKED # FOR. The byte-level cases say what was written; these say it means what the # caller intended. `incr.xml` is deliberately absent -- it has two roots and # does not parse, which `incr-parses' is the measurement of. check_eq $P/rt-nested-tag 'root' "$(pv rt_nested_tag)" check_eq $P/rt-nested-children 'child' "$(pv rt_nested_children)" check_eq $P/rt-nested-child-text 'inner' "$(pv rt_nested_child_text)" check_eq $P/rt-siblings-children 'a,b' "$(pv rt_siblings_children)" check_eq $P/rt-siblings-texts '1,2' "$(pv rt_siblings_texts)" check_eq $P/rt-esc-attr-value ''"'"'a & b < c > d " e \'"'"' f'"'"'' "$(pv rt_esc_attr_value)" check_eq $P/rt-esc-text ''"'"'a & b < c > d " e \'"'"' f'"'"'' "$(pv rt_esc_text)" check_eq $P/rt-prebuilt-tag 'pre' "$(pv rt_prebuilt_tag)" check_eq $P/rt-prebuilt-attr 'v' "$(pv rt_prebuilt_attr)" check_eq $P/rt-prebuilt-text 'body' "$(pv rt_prebuilt_text)" check_eq $P/rt-prebuilt-child 'sub=deep' "$(pv rt_prebuilt_child)" check_eq $P/rt-oneroot-children 'row,row,row' "$(pv rt_oneroot_children)" check_eq $P/rt-oneroot-texts '0,1,2' "$(pv rt_oneroot_texts)" check_eq $P/rt-extra-attrs '[('"'"'a'"'"', '"'"'1'"'"'), ('"'"'b'"'"', '"'"'2'"'"')]' "$(pv rt_extra_attrs)" # ------------------------------------------------------ nothing else wrote --- # THE PROBE WROTE EXACTLY THESE FILES AND NO OTHERS, into a workdir this suite # made for it. The package opens files, so unlike a path-composition library # this cannot be asserted by pointing it at a directory that does not exist -- # it is asserted by listing what appeared. check_eq $P/workdir-files 'attrib.xml,auto-closed.xml,buf.xml,closes.xml,enc.xml,esc-attr.xml,esc-text.xml,extra.xml,incr.xml,kept-open.xml,nested.xml,nonascii.xml,ns.xml,nsmap.xml,one.xml,oneroot.xml,prebuilt-child.xml,prebuilt.xml,siblings.xml,tail-ok.xml,text.xml,textmode.xml,truthy-int.xml,truthy-str.xml' "$(pv workdir_files)" check_eq $P/cwd-unchanged 'yes' "$(pv cwd_unchanged)" # ...and nothing outside it. The fixture names are generic enough that a # package writing to a relative path would land in the runner's cwd, so the # rootfs root is checked for the two most likely. check $P/no-stray-root-one test ! -e /one.xml check $P/no-stray-root-incr test ! -e /incr.xml rm -rf "$W"