. "$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 two hundred and six measurements. # # # # The work inside each start is a parse of a config fixture # # a few lines long -- the largest is nine settings -- plus # # six small files written under $TMPDIR for the `@include' # # and `load()' cases. Microseconds beside an interpreter # # start on this arm, which is tens of seconds before the # # first line of the program runs. # # # # 1200 is 400s per start, the same per-start budget # # 99-python3-imagesize and 99-python3-iso8601 each set for # # their three, and roughly six times the ~60s a python3 # # start has measured at here. Nothing in this suite is # # quadratic in anything, so the start is the only thing the # # number has to cover. # # # # 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-libconf 2.0.1 (meta-openembedded/meta-python # recipes-devtools/python/python3-libconf_2.0.1.bb, `inherit pypi setuptools3', # PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE DOES. # ============================================================================ # # A reader and writer for the libconfig configuration format, in one 756-line # pure-Python module with no C behind it and no runtime dependency outside the # interpreter. `load'/`loads' return an `AttrDict' -- an `OrderedDict' subclass # whose string keys are also attributes -- and `dump'/`dumps' serialise one back. # The reader is a hand-written tokenizer plus a recursive-descent parser; there # is no configure step, no compiled extension and no `.so' anywhere in it. # # THE PACKAGE IS A FLAT MODULE, not a directory: `setup.py' declares # `py_modules=["libconf"]', so site-packages holds `libconf.py' and nothing else # of the library's. The upstream `test/' tree in the sdist is NOT installed. # # ============================================================================ # THE SUITE'S FOUR FINDINGS, AND WHY EACH IS ASSERTED SEPARATELY. # ============================================================================ # # ATTRIBUTE ACCESS AND ITEM ACCESS ARE NOT THE SAME LOOKUP, and the difference # is reachable from an ordinary config file. `AttrDict.__getattr__' runs only # after NORMAL attribute lookup has failed, so a setting whose name collides with # a `dict' member -- `keys', `items', `values', `get' -- is returned by item # access and SHADOWED by the inherited method under attribute access. Nothing is # raised. `shadow-attr-ne-item' states it as the inequality it is; the four # `shadow-attr-*-is-method' cases name each colliding word, because a future # `AttrDict' that fixed one and not the others would otherwise read as fixed. # For every name that does NOT collide the two are the same object, which # `attr-is-item' asserts so the finding is bounded rather than general. # # THE SETTING TERMINATOR IS OPTIONAL. `Parser.setting()' ends on # `accept(";", ",")' -- an ACCEPT, not an EXPECT -- so `a = 1 b = 2' parses # clean and a dropped semicolon is silent. libconfig's own grammar requires the # terminator, so this reader accepts files a C `libconfig' would refuse, which # is the direction that matters for a config written against this library and # then read by a C program. `trailing-semicolons' is the same latitude from the # other side and is the shape nobody predicts: `a = 1;;;' IS an error, because # the extra `;' is not a setting and the parse then fails on unconsumed input. # So the reader forgives the missing terminator and refuses the surplus one. # # THE READER ACCEPTS ARRAYS THE WRITER REFUSES. `array()' parses a # comma-separated list of scalars with no type check, so `a = [1, "s"];' loads # happily; `get_array_value_dtype' then refuses it on the way out. A round trip # of a file that a C `libconfig' would itself have rejected therefore dies at # the `dumps' -- with a `ConfigSerializeError', which derives from `TypeError' # and NOT from `ConfigParseError' (`serialize-error-is-parse-error' asserts the # two are unrelated). An `except ConfigParseError' wrapped round a round trip # catches the reader's half and none of the writer's. # # AN ERROR MESSAGE CARRIES A POSITION ONLY SOMETIMES, AND THE COMMON MALFORMED # FILE IS THE CASE THAT DOES NOT. Three code paths raise `ConfigParseError': # an unexpected token reports `row R, column C' through `Token.__str__'; a byte # the tokenizer cannot match at all reports a row and column of its own; and # running out of input takes `TokenStream.error()'s `finished()' branch, which # formats NO position because there is no token left to take one from. An # unterminated group is that third path -- so `g = { a = 1;' is refused with # `Unexpected end of input', naming neither line nor column. `row-eof' asserts # that absence directly rather than leaving it to be inferred. # # ...and the row it does report can be wrong. The tokenizer's string regex is # `"([^"\\]|\\.)*"', and `[^"\\]' matches a NEWLINE, so a string literal may # span lines -- while `row' is advanced only by the whitespace-and-comment # skipper, never by the text of a token. `row-after-multiline-string' puts a real # error on line 4 behind a string that swallowed two newlines and asserts the # reported row is 2. This is an upstream defect, it is ours to report rather # than to fix, and it is asserted at the WRONG value so a release that repairs # it turns this case red. # # ============================================================================ # WHAT IS ASSERTED AT A WRONG VALUE, AND WHAT AT A RIGHT ONE. # ============================================================================ # # Only `row-after-multiline-string' is a defect asserted at its wrong value. # Everything else that surprises is a documented-by-omission divergence rather # than a bug: the optional terminator, the shadowed attribute, the mixed array, # the missing position. Each is asserted at what the code does, with the header # above saying why the answer is not the expected one. # # The hex spelling is the fourth of that kind and the one most likely to bite a # caller: `IntToken' computes an `is_hex' flag and discards it, so `0x10' comes # back as the integer 16 and `dumps' writes `16'. A round trip rewrites every # hex constant in a file to decimal. `dumps-hex-roundtrip' and # `roundtrip-hex-decimal' assert it from both ends. # # The `L' suffix, by contrast, IS preserved -- `IntToken' wraps an `L'-suffixed # literal in `LibconfInt64', which `dump_int' recognises -- and it is emitted on # a second, independent ground: any plain `int' outside the signed 32-bit range. # The four `dumps-int32-*' cases walk that boundary, because the suffix is what # makes the output readable by a C `libconfig' at all. # # ============================================================================ # THE FIXTURES ARE WRITTEN BY THE PROBE, NOT SHIPPED. # ============================================================================ # # libconfig is a text format and every fixture is a string literal a few lines # long, so building them in the probe keeps them beside the case that asserts # them and lets a case vary ONE construct at a time -- which is what the # terminator, the shadowed name and the multi-line string each need and what a # fixed corpus cannot give. Six small files under $TMPDIR are the exception: an # `@include' is resolved by path and a `load()' takes a stream, so neither can # be expressed in memory. # # The suite reaches no device, no peer and no terminal, and needs no manual # browser pass. # # ============================================================================ P=python3-libconf # 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 MOD=$SITE/libconf.py DI=$SITE/libconf-2.0.1.dist-info PKGLIST=/var/lib/dpkg/info/python3-libconf.list PROBE=$BST_DATA/python3-libconf-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-libconf.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ---------------------------------------------------------------- layout ----- # IT IS A MODULE, NOT A PACKAGE -- `py_modules=["libconf"]' in setup.py. A # release that grew the library into a directory would still import and still # pass every behavioural case below, so the shape is asserted on its own. check $P/site-packages-dir test -d "$SITE" check $P/flat-module test -s "$MOD" check $P/no-package-dir test ! -d "$SITE/libconf" # The sdist ships a `test/' tree; the wheel does not install it. Asserted by the # file rather than by the directory, so this stays a statement about THIS port # and not about whatever else may put a `test' dir in site-packages. check $P/no-test-e2e test ! -e "$SITE/test/test_e2e.py" check $P/no-test-tokenize test ! -e "$SITE/test/test_tokenize.py" check $P/no-py-typed test ! -e "$SITE/libconf.pyi" check $P/pycache test -s "$SITE/__pycache__/libconf.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. # libconf 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: a data file or a script appearing here would be a packaging change # nobody asked for. _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: 2.0.1" cat "$DI/METADATA" check_out $P/dist-info-name "Name: libconf" cat "$DI/METADATA" # THE LICENCE FILE IS `LICENSE', WITH NO EXTENSION -- which is what the recipe's # LIC_FILES_CHKSUM md5 is taken over. A repackaging that renamed it would break # the recipe's checksum, and this says so here first. check $P/dist-info-license test -s "$DI/LICENSE" 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. The # `py2-none-any' tag is upstream's `universal = 1' in setup.cfg -- a package # whose last release still declares a Python 2 wheel tag, which is worth pinning # so that a repackaging shows up here rather than silently. 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-tag-py2 "Tag: py2-none-any" cat "$DI/WHEEL" check_out $P/dist-info-top-level "libconf" cat "$DI/top_level.txt" sed -n '/^Package: python3-libconf$/,/^$/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: 2.0.1-r0" cat "$W/st" # THE ONLY RDEPEND IS `python3-core', a sub-package of the ported python3 # (BRINTOS-1476). The module's imports are sys, os, codecs, collections, io and # re -- all of them in core -- so the line is asserted exactly: a lean image # would say so here once rather than present as a broken library, and a release # that reached outside the standard library would say so here too. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" check_eq $P/pkg-depends-exact "Depends: python3-core" "$(cat "$W/dep")" # ---------------------------------------------------------------- import ----- check $P/import python3 -c 'import libconf' # A DeprecationWarning or a stray print on import would be invisible to the case # above, which scores only the exit status. `from __future__ import division' at # the top of a 2026 module is exactly the shape that starts emitting one. check_eq $P/import-quiet "" \ "$(python3 -c 'import libconf' 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" 900 python3 "$PROBE" 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 "206" "$(pv emitted_count)" # -------------------------------------------------------------- identity ----- # WHICH COPY ANSWERED. Every behavioural case below is a statement about the # module named here. check_eq $P/module-answered "$MOD" "$(pv module_file)" check_eq $P/is-not-package "no" "$(pv is_package)" # NO `__version__' AND NO `__all__'. The first is why `dist-info-version' above # reads the version out of the wheel metadata; the second is why a star-import # rebinds the caller's `os' and `re' -- the module's own imports are public # names in the absence of an `__all__', and three cases say so. check_eq $P/no-version-attr "no" "$(pv has_version_attr)" check_eq $P/no-all "no" "$(pv has_all)" check_eq $P/star-exports-os "yes" "$(pv star_exports_os)" check_eq $P/star-exports-re "yes" "$(pv star_exports_re)" check_eq $P/star-exports-loads "yes" "$(pv star_exports_loads)" # The exported surface, name by name. check_eq $P/has-load "yes" "$(pv has_load)" check_eq $P/has-loads "yes" "$(pv has_loads)" check_eq $P/has-dump "yes" "$(pv has_dump)" check_eq $P/has-dumps "yes" "$(pv has_dumps)" check_eq $P/has-attrdict "yes" "$(pv has_AttrDict)" check_eq $P/has-parse-error "yes" "$(pv has_ConfigParseError)" check_eq $P/has-serialize-error "yes" "$(pv has_ConfigSerializeError)" check_eq $P/has-libconflist "yes" "$(pv has_LibconfList)" check_eq $P/has-libconfarray "yes" "$(pv has_LibconfArray)" check_eq $P/has-libconfint64 "yes" "$(pv has_LibconfInt64)" check_eq $P/has-tokenizer "yes" "$(pv has_Tokenizer)" check_eq $P/has-tokenstream "yes" "$(pv has_TokenStream)" check_eq $P/has-parser "yes" "$(pv has_Parser)" # THE TWO EXCEPTION CLASSES ARE UNRELATED -- see the header. This is the finding # `mixed-array-dumps' below depends on, stated as a shape. check_eq $P/parse-error-base "RuntimeError" "$(pv parse_error_base)" check_eq $P/serialize-error-base "TypeError" "$(pv serialize_error_base)" check_eq $P/serialize-error-is-not-parse-error "no" \ "$(pv serialize_error_is_parse_error)" check_eq $P/attrdict-base "OrderedDict" "$(pv attrdict_base)" # --------------------------------------------------------- load(): scalars --- check_eq $P/int "7" "$(pv int)" check_eq $P/int-type "int" "$(pv int_type)" check_eq $P/int-negative "-7" "$(pv int_negative)" check_eq $P/int-plus "7" "$(pv int_plus)" # THE `L' SUFFIX SURVIVES THE PARSE AS A TYPE. `LibconfInt64' is an `int' # subclass, so the value compares and arithmetics as an ordinary int and is # distinguishable only by `type()' -- which is the whole of how `dumps' knows to # write the suffix back out. check_eq $P/int64 "7" "$(pv int64)" check_eq $P/int64-type "LibconfInt64" "$(pv int64_type)" check_eq $P/int64-ll "7" "$(pv int64_ll)" check_eq $P/int64-ll-type "LibconfInt64" "$(pv int64_ll_type)" check_eq $P/int64-is-int "yes" "$(pv int64_is_int)" check_eq $P/int64-equals-int "yes" "$(pv int64_equals_int)" check_eq $P/float "1.5" "$(pv float)" check_eq $P/float-type "float" "$(pv float_type)" check_eq $P/float-trailing-dot "1.0" "$(pv float_trailing_dot)" check_eq $P/float-exponent "1000.0" "$(pv float_exponent)" check_eq $P/float-leading-dot "0.5" "$(pv float_leading_dot)" check_eq $P/bool-true "True" "$(pv bool_true)" check_eq $P/bool-false "False" "$(pv bool_false)" check_eq $P/bool-type "bool" "$(pv bool_type)" check_eq $P/bool-mixed-case "True" "$(pv bool_mixed_case)" check_eq $P/string "x" "$(pv string)" check_eq $P/string-type "str" "$(pv string_type)" check_eq $P/string-empty "" "$(pv string_empty)" # A HEX LITERAL IS AN ORDINARY `int' AFTER THE PARSE -- the spelling is not # recorded, which is what `dumps-hex-roundtrip' below is about. check_eq $P/hex "16" "$(pv hex)" check_eq $P/hex-type "int" "$(pv hex_type)" check_eq $P/hex-upper-x "16" "$(pv hex_upper_x)" check_eq $P/hex64 "16" "$(pv hex64)" check_eq $P/hex64-type "LibconfInt64" "$(pv hex64_type)" # THERE IS NO NEGATIVE HEX LITERAL. The hex regex carries no sign, so `-0x10' # tokenizes as the integer `-0' followed by the NAME `x10' and the parse dies on # the name. The message is asserted with it, because what it names is a token # the parser did not expect and NOT a bad number -- a caller reading it goes # looking for the wrong thing. check_eq $P/hex-negative "!ConfigParseError" "$(pv hex_negative)" check_eq $P/hex-negative-msg \ "ConfigParseError: Unexpected token ';' in '', row 1, column 10; expected: (':', '=')" \ "$(pv hex_negative_msg)" # ----------------------------------------------------- load(): collections --- # THE THREE BRACKETS MAP ONTO THREE PYTHON TYPES and that mapping is the whole # of the reader's type system: `{}' an AttrDict, `[]' a plain `list', `()' a # plain `tuple'. NOT the LibconfArray/LibconfList subclasses -- those exist only # so a caller can force the WRITER's hand, and the two cases below say the # reader never produces them. check_eq $P/group-type "AttrDict" "$(pv group_type)" check_eq $P/array-type "list" "$(pv array_type)" check_eq $P/list-type "tuple" "$(pv list_type)" check_eq $P/array-is-not-libconfarray "no" "$(pv array_is_libconfarray)" check_eq $P/list-is-not-libconflist "no" "$(pv list_is_libconflist)" check_eq $P/group-value "2" "$(pv group_value)" check_eq $P/array-value "20" "$(pv array_value)" check_eq $P/list-value "30" "$(pv list_value)" check_eq $P/array-empty "[]" "$(pv array_empty)" check_eq $P/list-empty "()" "$(pv list_empty)" check_eq $P/group-empty "AttrDict()" "$(pv group_empty)" # NESTING, EACH CONSTRUCT INSIDE EACH OTHER -- and the two that do not nest. An # array holds SCALARS ONLY (`array()' calls `scalar_value_list_or_empty'), so an # array of groups is not a refused array but a setting that never found a value, # and the two cases below record it as the parse error it actually is. check_eq $P/group-in-group "1" "$(pv group_in_group)" check_eq $P/list-in-group "2" "$(pv list_in_group)" check_eq $P/array-in-group "1" "$(pv array_in_group)" check_eq $P/group-in-list "1" "$(pv group_in_list)" check_eq $P/list-in-list "2" "$(pv list_in_list)" check_eq $P/array-in-list "1" "$(pv array_in_list)" check_eq $P/list-mixed-types "s" "$(pv list_mixed_types)" check_eq $P/array-of-groups "!ConfigParseError" "$(pv array_of_groups)" check_eq $P/array-of-arrays "!ConfigParseError" "$(pv array_of_arrays)" # ORDER IS THE INPUT'S ORDER -- `AttrDict' derives from `OrderedDict', which is # what makes a round trip of a hand-written config file readable afterwards. check_eq $P/key-order "c,a,b" "$(pv key_order)" check_eq $P/group-key-order "z,y" "$(pv group_key_order)" # ------------------------------------------------------- attribute access ---- check_eq $P/attr-access "libconfig example" "$(pv attr_access)" check_eq $P/attr-is-item "yes" "$(pv attr_is_item)" # A MISSING NAME RAISES `AttributeError', NOT the `KeyError' the `__getitem__' # underneath would. That translation is what makes `hasattr()' and # `copy.deepcopy()' work on an AttrDict at all, and both sides are asserted. check_eq $P/attr-missing "!AttributeError" "$(pv attr_missing)" check_eq $P/item-missing "!KeyError" "$(pv item_missing)" check_eq $P/hasattr-missing "no" "$(pv hasattr_missing)" check_eq $P/hasattr-present "yes" "$(pv hasattr_present)" # THE HEADLINE FINDING -- see the header. Four colliding words, each named, plus # the inequality itself and a length case saying all four settings ARE there. check_eq $P/shadow-item-keys "mine" "$(pv shadow_item_keys)" check_eq $P/shadow-attr-ne-item "yes" "$(pv shadow_attr_ne_item)" check_eq $P/shadow-attr-keys-is-method "yes" "$(pv shadow_attr_keys_is_method)" check_eq $P/shadow-attr-items-is-method "yes" "$(pv shadow_attr_items_is_method)" check_eq $P/shadow-attr-values-is-method "yes" "$(pv shadow_attr_values_is_method)" check_eq $P/shadow-attr-get-is-method "yes" "$(pv shadow_attr_get_is_method)" check_eq $P/shadow-len "4" "$(pv shadow_len)" # A NAME THAT IS NOT A PYTHON IDENTIFIER IS ITEM-ONLY, and libconfig's grammar # admits two of them: `-' and `*' are both name characters there. Nothing is # raised; the setting simply cannot be spelled as an attribute. check_eq $P/name-with-hyphen "1" "$(pv name_with_hyphen)" check_eq $P/name-with-star "1" "$(pv name_with_star)" check_eq $P/name-leading-star "1" "$(pv name_leading_star)" check_eq $P/name-with-underscore-attr "1" "$(pv name_with_underscore_attr)" check_eq $P/name-leading-digit "!ConfigParseError" "$(pv name_leading_digit)" # ---------------------------------------------------------- the separators --- # `:' AND `=' ARE ONE OPERATOR. `setting()' does `expect(":", "=")' and keeps no # record of which it saw, so the two spellings are indistinguishable after the # parse and `dumps' always writes `=' (`roundtrip-no-colon'). check_eq $P/sep-equals "1" "$(pv sep_equals)" check_eq $P/sep-colon "1" "$(pv sep_colon)" check_eq $P/sep-mixed "1" "$(pv sep_mixed)" # THE TERMINATOR IS OPTIONAL AND THE SURPLUS ONE IS AN ERROR -- see the header. check_eq $P/missing-semicolon "2" "$(pv missing_semicolon)" check_eq $P/missing-semicolon-first "1" "$(pv missing_semicolon_first)" check_eq $P/no-semicolons-at-all "3" "$(pv no_semicolons_at_all)" check_eq $P/comma-terminator "2" "$(pv comma_terminator)" check_eq $P/trailing-semicolons "!ConfigParseError" "$(pv trailing_semicolons)" # An empty configuration is a legal one, not a refusal. check_eq $P/empty-input "ok:AttrDict()" "$(pv empty_input)" check_eq $P/whitespace-only-input "ok:AttrDict()" "$(pv whitespace_only_input)" # ------------------------------------------------------------- comments ------ # All three spellings libconfig defines, including the block comment's # multi-line form -- which is also the one that must advance the row counter, as # `row-after-block-comment' checks below. check_eq $P/comment-hash "1" "$(pv comment_hash)" check_eq $P/comment-slash "1" "$(pv comment_slash)" check_eq $P/comment-block "1" "$(pv comment_block)" check_eq $P/comment-block-multiline "1" "$(pv comment_block_multiline)" check_eq $P/comment-trailing "1" "$(pv comment_trailing)" check_eq $P/comment-inside-group "1" "$(pv comment_inside_group)" # -------------------------------------------------------------- strings ------ # ADJACENT LITERALS CONCATENATE with no operator, across newlines and across a # comment -- `Parser.string()' loops on `accept("string")' until it stops # matching, so anything the skipper eats is invisible to the join. check_eq $P/string-concat "xy" "$(pv string_concat)" check_eq $P/string-concat-three "xyz" "$(pv string_concat_three)" check_eq $P/string-concat-newline "xy" "$(pv string_concat_newline)" check_eq $P/string-concat-comment-between "xy" "$(pv string_concat_comment_between)" # The escapes `decode_escapes' knows: the single-character set and `\xNN'. # Asserted through `repr' so that a decoded control character stays on one line. check_eq $P/escape-newline "'x\\ny'" "$(pv escape_newline)" check_eq $P/escape-tab "'x\\ty'" "$(pv escape_tab)" check_eq $P/escape-quote "'x\"y'" "$(pv escape_quote)" check_eq $P/escape-backslash "'x\\\\y'" "$(pv escape_backslash)" check_eq $P/escape-hex "A" "$(pv escape_hex)" check_eq $P/escape-bell-len "1" "$(pv escape_bell_len)" # ...AND THE ONES IT DOES NOT. `\q' is not in `ESCAPE_SEQUENCE_RE', so the # BACKSLASH SURVIVES INTO THE VALUE rather than being consumed or refused -- the # tokenizer took it because its string regex accepts `\\.' unconditionally. # `A' is the same hole in the form a caller is far likelier to write: six # characters out, not one. check_eq $P/escape-unknown "'x\\\\qy'" "$(pv escape_unknown)" check_eq $P/escape-unicode-u "'\\\\u0041'" "$(pv escape_unicode_u)" check_eq $P/escape-unicode-u-len "6" "$(pv escape_unicode_u_len)" # Non-ASCII survives as text, asserted by codepoint so the value stays ASCII and # the answer does not depend on the encoding stdout happens to be in. check_eq $P/utf8-codepoint "228" "$(pv utf8_codepoint)" check_eq $P/utf8-len "1" "$(pv utf8_len)" # --------------------------------------------------------------- errors ------ check_eq $P/unterminated-group "!ConfigParseError" "$(pv unterminated_group)" check_eq $P/unterminated-list "!ConfigParseError" "$(pv unterminated_list)" check_eq $P/unterminated-array "!ConfigParseError" "$(pv unterminated_array)" check_eq $P/unterminated-string "!ConfigParseError" "$(pv unterminated_string)" check_eq $P/value-missing "!ConfigParseError" "$(pv value_missing)" check_eq $P/stray-close-brace "!ConfigParseError" "$(pv stray_close_brace)" check_eq $P/trailing-garbage "!ConfigParseError" "$(pv trailing_garbage)" # A BAD HEX LITERAL IS NOT DIAGNOSED AS ONE. `0xZZ' tokenizes as the integer `0' # followed by the name `xZZ', so the refusal that reaches the caller names an # unexpected `;' where a `:' or `=' was wanted -- three tokens away from the # thing that is actually wrong. The message is asserted verbatim for that # reason. check_eq $P/bad-hex "!ConfigParseError" "$(pv bad_hex)" check_eq $P/bad-hex-msg \ "ConfigParseError: Unexpected token ';' in '', row 1, column 9; expected: (':', '=')" \ "$(pv bad_hex_msg)" # `load()' TAKES TEXT, NOT BYTES, and says so with a `TypeError' of its own # before any tokenizing -- `f.read(0)' is probed for its type first. So the # wrong file mode is a clear refusal rather than a confusing parse error. check_eq $P/load-bytes-file "!TypeError" "$(pv load_bytes_file)" check_eq $P/loads-bytes "!TypeError" "$(pv loads_bytes)" check_eq $P/load-text-file "1" "$(pv load_text_file)" check_eq $P/load-error-names-file "yes" "$(pv load_error_names_file)" # ------------------------------------------------------- error POSITIONS ----- # See the header: three raising paths, and only two of them carry a position. check_eq $P/row-line1 "1" "$(pv row_line1)" check_eq $P/row-line3 "3" "$(pv row_line3)" check_eq $P/row-line3-unmatched-byte "3" "$(pv row_line3_unmatched_byte)" check_eq $P/row-after-block-comment "3" "$(pv row_after_block_comment)" # THE COMMON MALFORMED FILE IS THE ONE WITH NO POSITION AT ALL. check_eq $P/row-eof "no-row" "$(pv row_eof)" check_eq $P/unterminated-group-msg \ "ConfigParseError: Unexpected end of input; expected: ('}',)" \ "$(pv unterminated_group_msg)" # AND THE ROW IT DOES REPORT CAN BE WRONG. A string literal may contain a raw # newline -- `[^"\\]' matches one -- and the row counter is advanced only by the # skipper, never by a token's text. The real error here is on line 4 and the # message says row 2. Asserted at the WRONG value: a release that repairs the # counter turns this case red, which is the intent. check_eq $P/row-after-multiline-string "2" "$(pv row_after_multiline_string)" check_eq $P/multiline-string-len "7" "$(pv multiline_string_len)" check_eq $P/multiline-string-has-newline "yes" "$(pv multiline_string_has_newline)" # ------------------------------------------------------------- includes ------ # `@include' IS RESOLVED BY `TokenStream.from_file', WHICH `loads()' ALSO # REACHES -- `loads' wraps the string in a StringIO and hands it to `load' -- so # an include directive works on a string that never came from a file. The # directive must be the whole of its line: the regex is anchored at both ends # after `strip()', so the trailing form is a parse error rather than an include. check_eq $P/include-from-string "42" "$(pv include_from_string)" check_eq $P/include-sibling-setting "1" "$(pv include_sibling_setting)" check_eq $P/include-missing "!ConfigParseError" "$(pv include_missing)" check_eq $P/include-not-alone-on-line "!ConfigParseError" \ "$(pv include_not_alone_on_line)" # A FILE THAT INCLUDES ITSELF IS REFUSED RATHER THAN LOOPED -- the guard is # keyed on the filename, and a suite that got this wrong would HANG rather than # fail, which is why it is asserted rather than assumed. check_eq $P/include-circular "!ConfigParseError" "$(pv include_circular)" # ---------------------------------------------------------------- dump ------- # The writer's type map is the reader's read backwards. Each `dumps' answer is # compared whole, with the newlines the probe collapsed to a literal `\n'. check_eq $P/dumps-int 'a = 1;\n' "$(pv dumps_int)" check_eq $P/dumps-negative 'a = -1;\n' "$(pv dumps_negative)" check_eq $P/dumps-float 'a = 1.5;\n' "$(pv dumps_float)" check_eq $P/dumps-bool-true 'a = True;\n' "$(pv dumps_bool_true)" check_eq $P/dumps-bool-false 'a = False;\n' "$(pv dumps_bool_false)" check_eq $P/dumps-string 'a = "x";\n' "$(pv dumps_string)" check_eq $P/dumps-dict 'g =\n{\n a = 1;\n};\n' "$(pv dumps_dict)" check_eq $P/dumps-list-as-array 'a =\n[\n 1,\n 2\n];\n' \ "$(pv dumps_list_as_array)" check_eq $P/dumps-tuple-as-list 'a =\n(\n 1,\n 2\n);\n' \ "$(pv dumps_tuple_as_list)" check_eq $P/dumps-empty-dict 'g =\n{\n};\n' "$(pv dumps_empty_dict)" # `bool' IS TESTED BEFORE `int' in `get_dump_type', which it has to be -- # `isinstance(True, int)' is true in Python. What goes out is Python's # `True'/`False'; libconfig's own lexer is case-insensitive so a C reader takes # them, and the capital is pinned here so a change to lowercase is visible. check_eq $P/dumps-bool-is-capitalised "yes" "$(pv dumps_bool_is_capitalised)" # THE `L' SUFFIX IS EMITTED ON TWO INDEPENDENT GROUNDS -- a `LibconfInt64' # however small, and a plain `int' outside the signed 32-bit range however it # arrived. The boundary is walked exactly, because the suffix is what makes the # output readable by a C `libconfig'. check_eq $P/dumps-int64-roundtrip 'a = 7L;\n' "$(pv dumps_int64_roundtrip)" check_eq $P/dumps-int64-explicit 'a = 7L;\n' "$(pv dumps_int64_explicit)" check_eq $P/dumps-big-int 'a = 2147483648L;\n' "$(pv dumps_big_int)" check_eq $P/dumps-int32-max 'a = 2147483647;\n' "$(pv dumps_int32_max)" check_eq $P/dumps-int32-min 'a = -2147483648;\n' "$(pv dumps_int32_min)" check_eq $P/dumps-below-int32-min 'a = -2147483649L;\n' \ "$(pv dumps_below_int32_min)" # ...AND THE HEX SPELLING IS NOT PRESERVED, because the reader keeps no record # of it. A round trip rewrites every hex constant in a file to decimal. check_eq $P/dumps-hex-roundtrip 'a = 16;\n' "$(pv dumps_hex_roundtrip)" check_eq $P/dumps-hex64-roundtrip 'a = 16L;\n' "$(pv dumps_hex64_roundtrip)" # `dump_string' NAMES ONLY `\f \n \r \t' plus the backslash and the quote; # everything else unprintable leaves as `\xNN'. So `\a' is DECODED on the way in # and re-encoded on the way out in a different spelling -- exact round trip, # different text, which is asserted as such. check_eq $P/dumps-escape-newline 'a = "x\ny";\n' "$(pv dumps_escape_newline)" check_eq $P/dumps-escape-tab 'a = "x\ty";\n' "$(pv dumps_escape_tab)" check_eq $P/dumps-escape-quote 'a = "x\"y";\n' "$(pv dumps_escape_quote)" check_eq $P/dumps-escape-backslash 'a = "x\\y";\n' "$(pv dumps_escape_backslash)" check_eq $P/dumps-escape-control 'a = "x\x01y";\n' "$(pv dumps_escape_control)" check_eq $P/dumps-escape-del 'a = "x\x7fy";\n' "$(pv dumps_escape_del)" check_eq $P/dumps-escape-bell 'a = "\x07";\n' "$(pv dumps_escape_bell)" check_eq $P/dumps-escape-formfeed 'a = "\f";\n' "$(pv dumps_escape_formfeed)" # ------------------------------------------------------- what dump refuses --- # THE READER ACCEPTS ARRAYS THE WRITER WILL NOT TAKE BACK -- the header's third # finding. The load succeeds and the dump raises, and what it raises is not a # `ConfigParseError'. check_eq $P/mixed-array-loads "s" "$(pv mixed_array_loads)" check_eq $P/mixed-array-dumps "!ConfigSerializeError" "$(pv mixed_array_dumps)" # `bool' AND `int' ARE DIFFERENT ARRAY TYPES here even though Python makes one a # subclass of the other -- `get_dump_type' splits them, so `[True, 1]' is a # mixed array. `int' and `int64' are the one pair that DOES merge, and the merge # promotes every element to the suffixed form. check_eq $P/array-bool-int-refused "!ConfigSerializeError" "$(pv mixed_array_bool_int)" check_eq $P/array-int-and-int64 'a =\n[\n 1L,\n 2L\n];\n' \ "$(pv array_int_and_int64)" # An array may hold scalars only, on the way out as well as on the way in. check_eq $P/array-of-dicts-dumps "!ConfigSerializeError" "$(pv array_of_dicts_dumps)" check_eq $P/array-of-lists-dumps "!ConfigSerializeError" "$(pv array_of_lists_dumps)" check_eq $P/dumps-nonstring-key "!ConfigSerializeError" "$(pv dumps_nonstring_key)" check_eq $P/dumps-unsupported-value "!ConfigSerializeError" "$(pv dumps_unsupported_value)" check_eq $P/dumps-top-level-list "!ConfigSerializeError" "$(pv dumps_top_level_list)" check_eq $P/dumps-top-level-string "!ConfigSerializeError" "$(pv dumps_top_level_string)" # `LibconfList' IS THE CALLER'S OVERRIDE and the only way to say "write this # Python sequence as a libconfig LIST" -- a plain `list' always becomes an array # and therefore always inherits the scalar-only rule. Wrapped, the same nested # value serialises. check_eq $P/libconflist-of-dicts 'a =\n(\n {\n x = 1;\n }\n);\n' \ "$(pv libconflist_of_dicts)" check_eq $P/libconfarray-type 'a =\n[\n 1,\n 2\n];\n' "$(pv libconfarray_type)" # ----------------------------------------------------------- the round trip -- # `dumps(loads(text))' REPARSES TO AN EQUAL STRUCTURE for a fixture carrying # every construct the format defines. Equality is on the parsed objects, not on # the text: the text differs by design -- indentation, `=' for `:', decimal for # hex -- and asserting it would be asserting the formatter. check_eq $P/roundtrip-equal "yes" "$(pv roundtrip_equal)" check_eq $P/roundtrip-keys "name,n,big,f,flag,h,grp,arr,lst" "$(pv roundtrip_keys)" # A SECOND DUMP OF THE REPARSED OBJECT IS BYTE-IDENTICAL TO THE FIRST, which is # the property a caller who rewrites a config file in place depends on: the # first pass may reformat, the second must not. check_eq $P/roundtrip-stable "yes" "$(pv roundtrip_stable)" check_eq $P/roundtrip-int64-suffix "yes" "$(pv roundtrip_int64_suffix)" check_eq $P/roundtrip-int64-type "LibconfInt64" "$(pv roundtrip_int64_type)" check_eq $P/roundtrip-hex-decimal "yes" "$(pv roundtrip_hex_decimal)" check_eq $P/roundtrip-array-brackets "yes" "$(pv roundtrip_array_brackets)" check_eq $P/roundtrip-list-parens "yes" "$(pv roundtrip_list_parens)" check_eq $P/roundtrip-no-colon "yes" "$(pv roundtrip_no_colon)" check_eq $P/roundtrip-concat-joined "ab" "$(pv roundtrip_concat_joined)" check_eq $P/roundtrip-nested-deep "1" "$(pv roundtrip_nested_deep)" check_eq $P/roundtrip-list-group "3" "$(pv roundtrip_list_group)" check_eq $P/roundtrip-list-array "5" "$(pv roundtrip_list_array)" # `dump()' WRITES TO A STREAM and `dumps()' is that with a StringIO in front, so # the two must agree exactly -- a caller writing a config file takes the first. check_eq $P/dump-equals-dumps "yes" "$(pv dump_equals_dumps)" check_eq $P/dump-ends-with-newline "yes" "$(pv dump_ends_with_newline)" check_eq $P/dump-line-count "32" "$(pv dump_line_count)" rm -rf "$W"