. "$BST_LIB" require perl libconfig-general-perl/present # Config::General 2.65 (meta-oe recipes-devtools/perl) -- an Apache-style # configuration-file parser and writer, and the eleventh Perl module on this # image under BRINTOS-999. # # Pure Perl, so it ports the way liburi-perl established -- stock recipe, one # line in the image install list, no patch. What it needed that no earlier Perl # port here did is a bbappend for its RDEPENDS: General.pm opens with # `use English '-no_match_vars'` (line 17) and `use Carp::Heavy` (line 29), both # at FILE SCOPE, and the meta-oe recipe declares no RDEPENDS at all. Neither # perl-module-english nor perl-module-carp-heavy was on this image. Both splits # exist and are built; they simply were not installed, so without the bbappend # this port would have shipped a module that cannot be loaded at all -- measured # on the host control, hiding either one alone aborts the load at General.pm # line 17 / line 29 respectively. # # Four layers, each honest about what it can see: # 1. filesystem facts -- what the port SHIPS, and the two dependency modules # the bbappend exists to pull in. Green on target today; asserted before # anything starts perl, so they hold even on a boot where the interpreter # cannot load a module at all. # 2. can perl LOAD it on this boot, classified on the diagnostic. # 3. the battery, driven by the payload -- parse, round trip, error handling # and the documented quirks, 50-odd cases at VALUES. # 4. the saved config read back by the image's own shell tools, which is the # one thing a perl-only test cannot state: that what the writer put on disk # is the structure it claims by an independent reckoning. # Every expectation in 3 and 4 was measured against a native x86-64 perl running # the identical distribution (sha256 4d6d5754...fc58f, the tarball the recipe # fetches), so they are demonstrated rather than guessed. V=$(ls -d /usr/lib/perl5/vendor_perl/* 2>/dev/null | head -1) CORE=$(ls -d /usr/lib/perl5/5.* 2>/dev/null | head -1) # ------------------------- 1. what the port SHIPS: filesystem facts ---------- check libconfig-general-perl/vendor-dir test -n "$V" check libconfig-general-perl/module-installed test -f "$V/Config/General.pm" check libconfig-general-perl/extended-installed test -f "$V/Config/General/Extended.pm" check libconfig-general-perl/interpolated-installed test -f "$V/Config/General/Interpolated.pm" check libconfig-general-perl/payload-installed \ test -f "$BST_DATA/libconfig-general-perl-selftest.pl" # THE TWO SPLITS THE BBAPPEND EXISTS FOR. These are the regression guard on that # change and the reason it is a change at all: both are `use`d at file scope in # General.pm, so without them the module cannot be loaded -- and the failure # would otherwise look exactly like the #154 abort in section 2, which is the # misattribution this pair prevents. Asserted as ordinary reds rather than as a # `require`: they are a documented part of this port's closure now, so their # absence is a regression to report, not a coverage prompt to skip on. check libconfig-general-perl/dep-english test -f "$CORE/English.pm" check libconfig-general-perl/dep-carp-heavy test -f "$CORE/Carp/Heavy.pm" # ------------------ 2. can perl LOAD this module on this boot at all? -------- # BRINTOS-WORKAROUND: linux-in-the-browser#154 remove -- perl aborts in glibc's # allocator while COMPILING a module, so the battery below cannot run on an # image that meets it; delete this guard and the battery runs unconditionally # again once #154 closes. # # The guard classifies on the DIAGNOSTIC and says which block it met, because # there are now two ways this load can fail and attributing a red to the wrong # one is exactly the error a later reader cannot detect: #154's allocator abort, # and a missing dependency split (section 1's pair), which says so in perl's own # words and is OURS, not a platform defect. pout=$(perl -MConfig::General -e 'print "CG-OK\n"' 2>&1); prc=$? case "$pout" in *CG-OK*) bst_ok libconfig-general-perl/import-load ;; *) # The interpreter's locale warning is a four-line preamble on this image and # is NOT the failure: it precedes the real diagnostic and would otherwise # fill the 100-char detail field, leaving the red reading "Setting locale # failed" -- the wrong diagnosis in the one field a later reader attributes by. pdiag=$(printf '%s\n' "$pout" | awk '/^perl: warning/ { skip=1; next } /^[ \t]/ && skip { next } { skip=0; print }') [ -n "$pdiag" ] || pdiag=$pout case "$pdiag" in *"Can't locate English.pm"*|*"Can't locate Carp/Heavy.pm"*) why="OURS -- the RDEPENDS bbappend did not take: perl-module-english / perl-module-carp-heavy is not on this image" ;; *mremap_chunk*|*munmap_chunk*|*"corrupted size"*|*"corrupted double-linked list"*|\ *"double free or corruption"*|*"free(): "*|*"malloc(): "*|*"realloc(): "*) # The pattern set is WIDE on purpose and was widened by measurement in # BRINTOS-1011, not by guesswork: glibc has a dozen spellings for one # abort, and missing one turns an attributed red into a false alarm in # the single field a later reader attributes by. why="linux-in-the-browser#154 -- perl aborts in glibc's allocator while compiling a module" ;; *"Can't locate"*) why="OURS -- a module in Config::General's closure is missing from this image; read the name in the diagnostic" ;; *) if [ "$prc" = 134 ]; then why="unclassified SIGABRT -- rc=134 with no allocator diagnostic; probably #154 with a spelling this guard has not seen, but not asserted as one" else why="unclassified -- neither #154's allocator abort nor a missing-module diagnostic" fi ;; esac bst_notok libconfig-general-perl/import-load "rc=$prc $(_bst_1line "$pdiag") -- $why" # Named rather than silent. The ticket's headline criteria are the parse of # nested blocks / repeated keys / continuations / <>, the write-out # and re-parse to the same structure, and a broken file being reported rather # than silently half-parsed -- and those are exactly what could not be # measured. A suite that merely stopped here would report seven greens and # read as a pass. bst_notok libconfig-general-perl/parse-unverified \ "not run: perl cannot load Config::General ($why) -- the nested-block/repeated-key/continuation/include parse, the save-and-reparse round trip and the broken-file refusal are unverified on wasm32-hwjs" exit 0 ;; esac # ------------------------- 3. the parser's own answers ----------------------- # The payload writes its config files into $BST_CG_DIR and prints one # `RNN-tag value` line per measurement; section 4 then drives the image's own # tools against those same artifacts, which is why the directory is created here # and not inside the payload. CGD=${TMPDIR:-/tmp}/bst-configgeneral.$$ mkdir -p "$CGD" || CGD=/tmp export BST_CG_DIR="$CGD" out=$(perl "$BST_DATA/libconfig-general-perl-selftest.pl" 2>&1); rc=$? case "$out" in *ALL-CONFIG-GENERAL-OK*) all=1 ;; *) all=0 ;; esac if [ "$rc" -eq 0 ] && [ "$all" -eq 1 ]; then bst_ok libconfig-general-perl/all-ok else bst_notok libconfig-general-perl/all-ok "rc=$rc $(_bst_1line "$out")"; fi # Deliberately NOT the sibling suites' `$1=""; sub(/^ /,"")` idiom. Assigning to # $1 makes awk rebuild $0 with OFS, which COLLAPSES every run of spaces inside # the value -- and this suite asserts values that carry double spaces ("a b", # R76). Measured: the rebuild idiom turns that into "a b" and the case reds with # a diff a reader cannot see. substr() returns the remainder verbatim. val() { printf '%s\n' "$out" | awk -v t="$1" 'substr($0,1,length(t)+1)==t" "{ print substr($0,length(t)+2); exit }'; } # --- the module loads, from the image rather than a stray @INC dir ---------- check_eq libconfig-general-perl/version 2.65 "$(val R1-version)" case "$(val R2-inc-path)" in /usr/*) bst_ok libconfig-general-perl/installed-path ;; *) bst_notok libconfig-general-perl/installed-path "resolved to $(val R2-inc-path)" ;; esac # --- WHAT THE TICKET ASKS (1/3): the parse ----------------------------------- # Top level: four keys and no more. The count is an assertion in its own right -- # an include merged as a nested key instead of into the parent would leave five. check_eq libconfig-general-perl/parse-name brintos "$(val R10-name)" check_eq libconfig-general-perl/parse-key-count 4 "$(val R11-scalar-keys)" check_eq libconfig-general-perl/parse-keys listen,motd,name,server "$(val R12-keys)" # REPEATED KEYS collapse to an array ref in file order, and the include's value # is the THIRD element -- one assertion covering both the repeated-key rule and # the include merging into the same key rather than shadowing it. check_eq libconfig-general-perl/repeated-reftype ARRAY "$(val R13-listen-reftype)" check_eq libconfig-general-perl/repeated-count 3 "$(val R14-listen-count)" check_eq libconfig-general-perl/repeated-order '10.0.0.1|10.0.0.2|10.0.0.9' "$(val R15-listen-joined)" # LINE CONTINUATION: the backslash-newline is joined and the continued line's # leading indent collapses to ONE space. Both halves are in this single value -- # a parser that joined without collapsing yields 'welcome to the machine'. check_eq libconfig-general-perl/continuation 'welcome to the machine' "$(val R16-motd)" # NESTED BLOCKS, three levels, reached by name at every level check_eq libconfig-general-perl/nested-reftype HASH "$(val R20-server-reftype)" check_eq libconfig-general-perl/nested-names backup,primary "$(val R21-server-names)" check_eq libconfig-general-perl/nested-host a.example.net "$(val R22-primary-host)" check_eq libconfig-general-perl/nested-port 8080 "$(val R23-primary-port)" check_eq libconfig-general-perl/nested-depth3-max 100 "$(val R24-limits-max)" check_eq libconfig-general-perl/nested-depth3-min 1 "$(val R25-limits-min)" check_eq libconfig-general-perl/nested-depth3-type HASH "$(val R26-limits-reftype)" # <>: the included block lands as a SIBLING of the one in main.conf, # and files() names both files -- the include being READ, not the values merely # happening to be present. check_eq libconfig-general-perl/include-host backup.example.net "$(val R27-backup-host)" check_eq libconfig-general-perl/include-port 8081 "$(val R28-backup-port)" check_eq libconfig-general-perl/include-files-count 2 "$(val R29-files-count)" # SORTED by the payload: files() ends `return keys %{...}`, so its order is # perl's hash order and flips run to run (21/19 across 40 fresh interpreters). check_eq libconfig-general-perl/include-files extra.conf,main.conf "$(val R30-files-base)" # --- WHAT THE TICKET ASKS (2/3): write out, re-parse, same structure --------- check_eq libconfig-general-perl/saved-len 354 "$(val R40-saved-len)" check_eq libconfig-general-perl/roundtrip-identical 1 "$(val R41-roundtrip-identical)" check_eq libconfig-general-perl/roundtrip-canon-len 192 "$(val R42-canon-len)" # spot values THROUGH the round trip, so a canonicaliser that returned '' on both # sides could not make the comparison above pass check_eq libconfig-general-perl/roundtrip-deep-value 100 "$(val R43-rt-limits-max)" check_eq libconfig-general-perl/roundtrip-array 3 "$(val R44-rt-listen-count)" # THE WRITER'S ORDERING QUIRK, asserted only where it is DETERMINISTIC. # save_string emits in hash-iteration order. Saving the same hash twice is # therefore repeatable (100/100 runs) and is pinned. Saving a RE-PARSED hash is # not: %h2 is a different hash carrying the same content, and the two saves come # out byte-equal purely by luck -- measured 18 times in 200 runs. A case pinning # that either way would red about one boot in eleven, which on this image would # read as platform noise beside the #154 reds and be diagnosed as nothing. So the # raw cross-parse comparison is deliberately absent; what is pinned is the # invariant that does hold, and is what "re-parses to the same structure" needs. check_eq libconfig-general-perl/save-repeatable 1 "$(val R45-save-same-hash-stable)" check_eq libconfig-general-perl/save-sorted-stable 1 "$(val R45b-save-sorted-stable)" check_eq libconfig-general-perl/save-len-stable 1 "$(val R45c-save-len-stable)" # the file-writing half of the same API, and the two functional entry points check_eq libconfig-general-perl/save-file-exists 1 "$(val R46-save-file-exists)" check_eq libconfig-general-perl/save-file-identical 1 "$(val R47-save-file-identical)" check_eq libconfig-general-perl/parseconfig-identical 1 "$(val R48-parseconfig-identical)" check_eq libconfig-general-perl/saveconfigstring-eq 1 "$(val R49-saveconfigstring-eq)" # --- WHAT THE TICKET ASKS (3/3): broken input is REPORTED -------------------- # Each case asserts that it died AND what it said: a die carrying a different # message would mean the parser noticed a different problem than the one planted. check_eq libconfig-general-perl/err-unclosed-died 1 "$(val R60-unclosed-died)" check_eq libconfig-general-perl/err-unclosed-msg \ 'Config::General: Block "" has no EndBlock statement (level: 2, chunk 2)!' \ "$(val R61-unclosed-msg)" check_eq libconfig-general-perl/err-orphan-close-died 1 "$(val R62-orphan-close-died)" check_eq libconfig-general-perl/err-orphan-close-msg \ 'Config::General: EndBlock "" has no StartBlock statement (level: 1, chunk 1)!' \ "$(val R63-orphan-close-msg)" check_eq libconfig-general-perl/err-dup-strict-died 1 "$(val R64-dup-strict-died)" check_eq libconfig-general-perl/err-dup-strict-msg \ 'Config::General: Option "x" occurs more than once (level: 1, chunk 2)!' \ "$(val R65-dup-strict-msg)" check_eq libconfig-general-perl/err-missing-include 1 "$(val R66-missing-inc-died)" # the broken file ON DISK, which is the ticket's own phrasing... check_eq libconfig-general-perl/err-broken-file-died 1 "$(val R67-broken-file-died)" # ...and the "rather than silently half-parsed" half, which is the stronger claim: # NOTHING comes back, not a partial hash carrying the keys read before the fault. check_eq libconfig-general-perl/err-no-partial-hash 1 "$(val R68-broken-no-partial)" # the parser is not left broken by a die -- a good file still parses afterwards check_eq libconfig-general-perl/err-recovers 1 "$(val R69-recovers)" # --- documented behaviour worth PINNING, measured not assumed ---------------- # THE ONE PLACE THE TICKET'S PREMISE DOES NOT HOLD. ` ... ` is ACCEPTED as # a block named `a`: the parser tracks nesting DEPTH, not tag names, and its POD # says so outright. Pinned as measured upstream behaviour rather than left as an # unstated gap in "broken files are reported" -- and pinned in the direction that # is true today, so if upstream ever tightens it this case reds and says so. check_eq libconfig-general-perl/mismatched-close-accepted 1 "$(val R70-mismatched-close-accepted)" check_eq libconfig-general-perl/dup-block-reftype ARRAY "$(val R71-dup-block-reftype)" check_eq libconfig-general-perl/dup-block-count 2 "$(val R72-dup-block-count)" check_eq libconfig-general-perl/merge-dup-blocks '{x=1,y=2}' "$(val R73-merged-block)" # an option with no value is undef, not '' -- the distinction a caller testing # `defined` depends on check_eq libconfig-general-perl/empty-value-undef 1 "$(val R74-empty-value-undef)" # `#` starts a comment even mid-line, and NOT even double quotes protect it -- # the closing quote is eaten with the comment and the opening one is kept. The # working escape is the backslash. This is the trap a caller writing a URL # fragment or a password falls into, so all three forms are pinned. check_eq libconfig-general-perl/hash-ends-value a "$(val R75-hash-ends-value)" check_eq libconfig-general-perl/quoted-spaces 'a b' "$(val R76-quoted-spaces)" check_eq libconfig-general-perl/quote-no-hash-escape '"a b' "$(val R76b-quote-no-hash-escape)" check_eq libconfig-general-perl/backslash-escapes-hash 'a#b' "$(val R76c-backslash-escapes-hash)" check_eq libconfig-general-perl/heredoc line1/line2 "$(val R77-heredoc)" check_eq libconfig-general-perl/equals-split 1 "$(val R78-equals-split)" # -LowerCaseNames lowercases NAMES only, never values check_eq libconfig-general-perl/lowercase-names '{server={hostname=A}}' "$(val R79-lowercase)" check_eq libconfig-general-perl/no-warnings 0 "$(val R99-warnings)" # ---- 4. the file on disk, by an independent reckoning ----------------------- # Everything above is the module marking its own homework: a writer that emitted # a private format and a reader that understood it would satisfy all of it. These # read the SAVED config with the image's own shell tools, which is the one claim # a perl-only test cannot make -- that what save_string() put on disk really is # the nested Apache-style structure it says it is. check libconfig-general-perl/saved-file-written test -s "$CGD/saved.conf" # the three-level nesting, as text check libconfig-general-perl/saved-has-server grep -q '^' "$CGD/saved.conf" check libconfig-general-perl/saved-has-primary grep -q '^ *' "$CGD/saved.conf" check libconfig-general-perl/saved-has-limits grep -q '^ *' "$CGD/saved.conf" check libconfig-general-perl/saved-closes-limits grep -q '^ *' "$CGD/saved.conf" check libconfig-general-perl/saved-closes-server grep -q '^' "$CGD/saved.conf" # every block that opens closes, counted by an implementation that is not perl check_eq libconfig-general-perl/saved-block-balance \ "$(grep -c '^ *<[a-z]' "$CGD/saved.conf" 2>/dev/null)" \ "$(grep -c '^ */dev/null)" # the repeated key really is three separate lines in the output, not one joined check_eq libconfig-general-perl/saved-repeated-lines 3 \ "$(grep -c '^listen' "$CGD/saved.conf" 2>/dev/null)" # ...and the include's value is among them, so the include survived the write check libconfig-general-perl/saved-carries-include grep -q '^listen *10\.0\.0\.9$' "$CGD/saved.conf" # the ORIGINAL include file is untouched by the round trip -- save_file must not # write back through the include, which would silently rewrite a user's tree check_eq libconfig-general-perl/include-file-untouched 2 \ "$(grep -c 'backup' "$CGD/extra.conf" 2>/dev/null)" rm -rf "$CGD"