. "$BST_LIB" # BST-TIMEOUT: 900 # ONE python3 start, and that is the whole cost. Every # # assertion below either reads the filesystem/dpkg with # # shell builtins or greps the single probe run's output. # # 99-python3-trove-classifiers carries the same 900 for the # # same shape; the one start here parses a 17-line fixture and # # six malformed ones, which is nothing beside the start. # # 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-iniconfig 2.0.0 (poky meta/recipes-devtools/python, # python3-iniconfig_2.0.0.bb, `inherit pypi python_hatchling`) -- A SMALL # INI-FILE PARSER WITH A LINE-PRECISE ERROR REPORT, on the wasm32-hwjs glibc # image. # # ============================================================================ # WHAT THIS PACKAGE IS, AND WHAT THE ASSERTIONS ARE ABOUT. # ============================================================================ # # iniconfig is one module in three files: a parser, an `IniConfig` mapping over # what it parsed, and a `ParseError` carrying the line the fault is on. It is the # config reader `pytest` reaches for first. So the port is correct exactly when # the PARSE is right -- the sections and their key/value pairs, the line numbers, # and the two documented ways it refuses input. # # The cases divide into four groups, and each answers a different question: # # - THE PAYLOAD (`site-packages-dir` .. `pkg-depends-core`) -- did the right # files land, and does dpkg know the package that put them there. # - THE PARSE (`sections` .. `beta-items`) -- does it read the fixture the way # the control read it, value by value. # - THE REFUSALS (`keyerror-*`, `bad-*`) -- does it reject what it documents as # invalid, at the LINE the fault is on. # - THE NEGATIVES (`module-no-so`, `has-version-attr`, `iscomment-value`) -- # facts that are "no" on a correct package, pinned so a later reader does not # turn them into reds by "fixing" them. # # ============================================================================ # WHY THIS PORT IS POSSIBLE AT ALL, AND WHAT THE .so CASES ARE REALLY SAYING. # ============================================================================ # # This image's python3 is ONE STATIC BINARY with an EMPTY lib-dynload and no # dynamic loader (BRINTOS-1476; 99-python3 asserts both directly). A module # carrying a compiled extension could therefore not be imported here -- not # "would be slower", could not be imported. iniconfig is pure Python, which is # the entire reason it ports. # # So `module-no-so` and `pkg-no-so` are not boilerplate. They are a REGRESSION # TRIPWIRE on a version bump: this release grows no extension, and the day one # adds a compiled tokenizer these go red at INSTALL time rather than at `import # iniconfig` on somebody else's ticket three rounds later. The assertion is made # from BOTH sides because the two fail differently -- a find catches a file the # package does not declare, the dpkg list catches a declared file that is not # Python -- and a third time from inside the interpreter (`module-is-py`). # # ============================================================================ # EVERY EXPECTATION BELOW WAS MEASURED, NOT PREDICTED. # ============================================================================ # # data/python3-iniconfig-probe.py was run on x86-64 TWICE, on 2026-08-31: against # iniconfig unpacked from the same iniconfig-2.0.0.tar.gz this recipe builds, AND # against the iniconfig extracted from this build's own # python3-iniconfig_2.0.0-r0_wasm32.deb. The two outputs agree on every key but # two, and both differences are understood: # # - `module_file`, which is the path each tree was read from; and # - `version`, which is read through importlib.metadata and so comes from the # INSTALLED dist-info. The sdist carries none, so the sdist run reported the # BUILD HOST's own iniconfig (1.1.1) -- the .deb run reported 2.0.0, and that # is the number `version` below asserts. This is exactly why the control was # run against the .deb as well as the tarball. # # What makes the control transferable is that all of it is pure-Python and # content-determined: parsed strings, line numbers and exception fields over # ASCII text. Nothing here depends on pointer width, byte order or a locale, and # nothing arch-dependent is asserted. # # THE ONE THING THE CONTROL CANNOT SETTLE is whether this arm's static python3 # starts and imports the module inside the bound above. That is what the batch # verify measures, and it is the only genuine unknown in this file. # # ============================================================================ # FOUR MEASURED FACTS THAT WOULD EACH MAKE A CASE RED ON A CORRECT PACKAGE. # ============================================================================ # # 1. THE PACKAGE CARRIES NO `__version__`. `_version.py` IS shipped and does # hold '2.0.0', but `__init__.py` never imports it -- so `iniconfig.__version__` # does not exist and the version can only be read from the dist-info. # `has-version-attr` pins that "no"; asserting the "yes" the shipped # `_version.py` suggests would be red on a correct package. # 2. `ParseError.lineno` IS 0-BASED while `__str__` RENDERS 1-BASED, and # `IniConfig.lineof()` returns 1-based. All three are asserted separately # (`bad-*-lineno` against `bad-*-str-lineno` against `lineof-*`), because a # consumer that read `.lineno` as 1-based would be off by one on every report # and no single rendering would show it. # 3. `py.typed` IS A ZERO-BYTE FILE -- measured. Its existence is the PEP 561 # marker and its content is nothing, so it is asserted with `test -f`; # `test -s` would be red on a correct package. # 4. THE dist-info CARRIES NO `top_level.txt`, and its LICENSE is one level # down in `licenses/`. This is a hatchling-built wheel, not a setuptools one # -- so the dist-info file list differs from # 99-python3-trove-classifiers', and copying that suite's cases here would # be red on a correct package. # # ============================================================================ # WHAT IS NOT COVERED, ON PURPOSE. # ============================================================================ # # - python3-iniconfig-dev and -dbg. Both are EMPTY on this build (measured: the # data archive of each carries no regular file), so the image names only the # main package and this suite says nothing about them. # - The upstream pytest suite. Running it would drag python3-pytest into the # image for one package's sake; the probe drives the same surface directly -- # the parse, the mapping, and each documented raise. # - Encodings. `IniConfig` takes an `encoding` argument and the fixture is # ASCII, so nothing here exercises it. It is a real gap and a deliberate one: # a non-ASCII fixture would make the control depend on the target's locale, # which is the one thing that would stop the x86-64 numbers transferring. # # THIS PORT CLOSES ON THIS SUITE: non-interactive, no manual browser pass, no # follow-on ticket under BRINTOS-780. P=python3-iniconfig # THE CANARY, which asserts nothing about python3-iniconfig. # # A suite's FIRST BST result line is lost between the guest's printf and the # host's read -- per-suite, deterministic, and NOT a defect in the host tool # (BRINTOS-1149 established that and filed it as linux-in-the-browser#177). The # harness computes its pass count as total-minus-failures, so THE DROPPED LINE # IS COUNTED AS A PASS. A nothing-case therefore stands in that position and the # eaten line costs nothing. It sits ahead of the `require` guard deliberately, # because a `require` that skips would emit its skip as the first line and be # eaten too. # # Remove this case when linux-in-the-browser#177 is fixed. bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages IC=$SITE/iniconfig DI=$SITE/iniconfig-2.0.0.dist-info PKGLIST=/var/lib/dpkg/info/python3-iniconfig.list D=$BST_DATA PROBE=$D/python3-iniconfig-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-iniconfig.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------ the payload --- # Where the module has to land. `python3 -c 'import iniconfig'` further down is # the functional statement; these are the ones that say WHICH copy answered, so # a module that imported out of some other path is distinguishable from one that # imported out of the package this image installed. check $P/site-packages-dir test -d "$SITE" check $P/module-dir test -d "$IC" # THE THREE SOURCE FILES, named individually because they are three different # things and a missing one fails differently: the package surface, the parser it # delegates to, and the exception type both raise. check $P/module-init test -f "$IC/__init__.py" check $P/module-init-nonempty test -s "$IC/__init__.py" check $P/module-parse test -f "$IC/_parse.py" check $P/module-exceptions test -f "$IC/exceptions.py" # `_version.py` IS SHIPPED -- and is NOT imported by `__init__.py`. Asserted # present because it is a file the package ships and a later reader will look for # it; `has-version-attr` below is the other half of the statement, that shipping # it does not put `__version__` on the module. check $P/module-version-file test -f "$IC/_version.py" # py.typed is a SHIPPED FILE, not build residue: it is what marks the package # PEP 561 typed. It is also ZERO BYTES -- measured -- so `test -s` would be red # on a correct package, and it is the one regular file under the module that is # neither .py nor metadata, which is why `pkg-only-python` below has to allow it. # Named here so that allowance is anchored on something asserted. check $P/module-py-typed test -f "$IC/py.typed" # THE DIST-INFO IS WHERE THE VERSION PHYSICALLY LIVES, the module carrying no # __version__ of its own (see the header). The directory NAME and the METADATA # CONTENT are two separate statements of it, and `version` below reads the # CONTENT -- measured: renaming this directory to iniconfig-1.9.9.dist-info # leaves `version` reporting 2.0.0 and reds only the cases here. So the two # halves are asserted in two places on purpose; neither covers the other. # # THIS IS A HATCHLING-BUILT WHEEL. Its dist-info carries no `top_level.txt` and # puts LICENSE one level down under `licenses/` -- both measured off the built # .deb. A setuptools-built package's dist-info looks different, which is why # these cases are not the ones 99-python3-trove-classifiers carries. check $P/dist-info-dir test -d "$DI" check $P/dist-info-record test -f "$DI/RECORD" check $P/dist-info-metadata test -f "$DI/METADATA" check $P/dist-info-wheel test -f "$DI/WHEEL" check $P/dist-info-license test -f "$DI/licenses/LICENSE" # NO COMPILED EXTENSION, ANYWHERE UNDER THE MODULE. See the header: on this image # a .so could not be loaded at all, so this is the case that fails a version bump # which grew one -- loudly, here, instead of at `import` later. check_eq $P/module-no-so "" \ "$(find "$IC" -name '*.so' -o -name '*.so.*' 2>/dev/null)" # ...and the same assertion from the PACKAGE's side rather than the filesystem's. # # The list is copied to tmpfs first and never redirected from the rootfs: a # top-level stdin redirect on the virtiofs root makes dup2(2) return 513 and ash # EXITS, ending the suite silently (brintos/linux#73). cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" check_eq $P/pkg-no-so "" \ "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" # EVERY REGULAR FILE THE PACKAGE SHIPS UNDER site-packages IS PYTHON OR ITS # METADATA. The two cases above name the failure that is expected; this one # catches the shape nobody predicted -- a binary, a .pyd, a shared archive -- # without having to enumerate what it might be. # # THE `-f` TEST IS LOAD-BEARING AND WAS MEASURED OFF THE BUILT PACKAGE. A dpkg # .list names DIRECTORIES too, and without a trailing slash, so a name-only # filter reports every directory in the package as an unexpected shape -- and # this case would be red on a CORRECT package. # # The `.pyc` arm is not decoration either: this package ships a __pycache__ with # four compiled files, `_version.cpython-312.pyc` among them. `py.typed` is # allowed by the *.typed arm and asserted present above, so the allowance cannot # quietly cover its disappearance. The dist-info arm covers METADATA, RECORD, # WHEEL and licenses/LICENSE, each of which is named individually above for the # same reason. _bad="" while IFS= read -r _f; do case "$_f" in "$SITE"/*) ;; *) continue ;; esac [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc|*.typed) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # --------------------------------------------------------- the package ------ # dpkg KNOWS the package. Every case above would look identical if some other # package had installed the same files, so this is the one that says # python3-iniconfig is a real package this image installed rather than an # IMAGE_INSTALL line that resolved to nothing. # # The stanza is read from a tmpfs copy for the same reason as the file list. The # anchor is exact: `python3-iniconfig` is a PREFIX of `python3-iniconfig-dev`, # and an unanchored match would report one stanza as the other. sed -n '/^Package: python3-iniconfig$/,/^$/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 "2.0.0-r0" cat "$W/st" # THE ONLY RUNTIME DEPENDENCY THIS PACKAGE CARRIES -- measured off the built # .deb, whose control stanza reads `Depends: python3-core` and nothing else. Pure # Python over the interpreter core: no third-party module, not even a stdlib # split, which is why nothing about this port waited on another member of # BRINTOS-1826. Every DEPENDS the recipe carries -- python3-hatch-vcs-native # among them -- is `-native` and resolved on the build host. check_out $P/pkg-depends-core "python3-core" cat "$W/st" # ---------------------------------------------------------- the interpreter -- # THE IMPORT, ON ITS OWN, BEFORE THE PROBE. If iniconfig cannot be imported then # every probe case below reds at once and each names a different symptom; this # case is what says the cause is the import. It is also the cheapest possible # statement of the deliverable. check $P/import python3 -c 'import iniconfig' check $P/import-names python3 -c 'from iniconfig import IniConfig, ParseError, COMMENTCHARS, iscommentline' # ------------------------------------------------------------- the 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 # ONE interpreter start for everything that follows. bst_probe bounds it and # keeps BOTH streams: a probe that dies early otherwise leaves every case below # reporting got:'' and naming a symptom each, with the actual traceback nowhere. # The bound is generous because a python3 start on this arm is tens of seconds # before any work begins. # # $W is passed as the scratch directory the probe writes its fixtures into. It is # on tmpfs: the fixtures must be WRITTEN and then read back by path, and the # rootfs is read-only here. bst_probe $P/probe-run "$W/probe" 600 python3 "$PROBE" "$W" OUT="$W/probe" # Read a single key. The probe emits each key at most once, so head -n 1 is # defensive rather than load-bearing; the anchors keep `lineof_key1` and # `lineof_kev` -- and `bad_no_section` from `bad_no_section_lineno` -- apart. pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } # THE EVIDENCE PLANE. If the probe emitted nothing, every case below is reading # an absent file: each would red with got:'' and the set would read exactly like # a parser that got every answer wrong, when what actually happened is that no # measurement was taken. # # The word `unscorable` is PARSED -- brintos_local_verify re-measures a suite that # prints it on a fresh guest rather than trusting a shared boot's numbers # (BRINTOS-1644) -- so it goes in this structural check, not a comment. # # The cases below still print their reds: withdraw, do not hide. This case is # what says what those reds measure. 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 # A traceback the probe caught itself, surfaced as its own case rather than as # sixty separate got:'' reds. check_eq $P/probe-no-fatal "" "$(pv fatal)" # THE VERSION THE RECIPE PINS, read through importlib.metadata out of the # METADATA file above -- not out of the dist-info directory's name, which is # asserted separately by `dist-info-dir`. The module carries no __version__, so # a red here is either a version drift or a METADATA the interpreter could not # read, and `dist-info-metadata` above is what separates the two. check_eq $P/version "2.0.0" "$(pv version)" # The negative, pinned. See the header: `_version.py` is shipped and holds the # string, but `__init__.py` never imports it, so asserting a `__version__` would # be red on a correct package and this case is what stops a later reader adding # one. check_eq $P/has-version-attr "no" "$(pv has_version_attr)" # The module answered out of the package this image installed, not from a stray # copy on sys.path. This is what makes the payload cases above and the parse # cases below statements about the SAME iniconfig. check_eq $P/module-file "$IC/__init__.py" "$(pv module_file)" # ...and it answered out of a .py, not an extension. The third statement of the # .so negative, this one from inside the interpreter. check_eq $P/module-is-py "yes" "$(pv module_is_py)" # ------------------------------------------------------------- the API ------- # `__all__` IS THE PACKAGE'S OWN STATEMENT of what it exports, asserted whole and # in order so a name added or dropped is named rather than merely counted. check_eq $P/all-names "IniConfig|ParseError|COMMENTCHARS|iscommentline" "$(pv all_names)" # ...and each name actually resolves. `__all__` is a list of strings and would # still read correctly if the object behind one of them had gone. check_eq $P/api-iniconfig "yes" "$(pv api_iniconfig)" check_eq $P/api-parseerror "yes" "$(pv api_parseerror)" check_eq $P/api-commentchars "yes" "$(pv api_commentchars)" check_eq $P/api-iscommentline "yes" "$(pv api_iscommentline)" # SectionWrapper is NOT in `__all__` -- it is reached only by indexing a config -- # but it is the type `IniConfig.__getitem__` returns, so a consumer that annotates # one breaks on its absence and no case above would see it. check_eq $P/api-sectionwrapper "yes" "$(pv api_sectionwrapper)" # THE COMMENT SYNTAX, which is the one piece of the grammar that is a constant # rather than code. Both characters, in order. check_eq $P/commentchars "#;" "$(pv commentchars)" # `iscommentline` tests the FIRST NON-BLANK character. The indented case is what # says it lstrips, and `iscomment-value` is the pinned NEGATIVE: a `#` that comes # after a value does NOT make the line a comment, so iniconfig keeps `1 # x` as # the value. Asserting the "yes" there would be red on a correct package. check_eq $P/iscomment-hash "yes" "$(pv iscomment_hash)" check_eq $P/iscomment-semi "yes" "$(pv iscomment_semi)" check_eq $P/iscomment-indented "yes" "$(pv iscomment_indented)" check_eq $P/iscomment-value "no" "$(pv iscomment_value)" # An empty line is a comment line as far as this predicate is concerned -- `""[:1]` # is `""`, which is `in "#;"`. A surprising "yes", pinned so it is not "fixed". check_eq $P/iscomment-blank "yes" "$(pv iscomment_blank)" # ------------------------------------------------------------- the parse ----- # THE CONFIG KNOWS WHICH FILE IT READ. Every ParseError below carries this path, # so a report is traceable to a fixture rather than merely to a line number. check_eq $P/path-basename "good.ini" "$(pv path_basename)" # THE SECTIONS, twice: alphabetically, and in the order they were DECLARED. # The fixture declares `beta` BEFORE `alpha` on purpose -- with the file in # alphabetical order these two keys agree and neither could tell an ordering # failure from a sorting one. check_eq $P/sections "alpha|beta|gamma" "$(pv sections)" check_eq $P/iter-sections "beta|alpha|gamma" "$(pv iter_sections)" check_eq $P/count-sections "3" "$(pv count_sections)" # `__contains__`, both ways round. The negative is what says a missing section is # reported missing rather than created on access. check_eq $P/contains-alpha "yes" "$(pv contains_alpha)" check_eq $P/contains-missing "no" "$(pv contains_missing)" # THE KEY/VALUE PAIRS, COMPARED AGAINST LITERALS -- the deliverable. The key sets # are asserted whole so a pair that vanished is caught, and the values one at a # time so a red names WHICH pair the parser got wrong. check_eq $P/alpha-keys "empty|key1|key2|multi|spaced" "$(pv alpha_keys)" # ...and the wrapper's own iteration, which is in LINE order, not sorted. The two # differ here, which is what makes this a real assertion. check_eq $P/alpha-iter-keys "key1|key2|empty|spaced|multi" "$(pv alpha_iter_keys)" # `key1 = value1` and `key2: value2` take DIFFERENT arms of the parser -- the `:` # arm is reached only when the `=` split raises -- so both separators are driven. check_eq $P/key1 "value1" "$(pv key1)" check_eq $P/key2 "value2" "$(pv key2)" # AN EMPTY VALUE IS A VALUE. `empty =` yields '', not a missing key: the two read # alike through `.get()` and are different facts, which is why the repr and the # equality are asserted separately. check_eq $P/empty-value "''" "$(pv empty_value)" check_eq $P/empty-is-str "yes" "$(pv empty_is_str)" # WHITESPACE IS STRIPPED from both sides of both name and value. The fixture line # carries trailing spaces deliberately; an editor that stripped them would # silently weaken this case rather than break it. check_eq $P/spaced-value "trimmed" "$(pv spaced_value)" check_eq $P/spaced-key-present "yes" "$(pv spaced_key_present)" # THE CONTINUATION -- a leading-whitespace line joined onto the value above it # with a newline. Asserted as the joined text (with the newlines made visible, # since a key=value line cannot carry a real one) AND as a line count, because a # join that lost a line and a join that used the wrong separator fail differently. check_eq $P/multi-value "first\\nsecond\\nthird" "$(pv multi_value)" check_eq $P/multi-lines "3" "$(pv multi_lines)" # THE COMMENT AFTER A SECTION HEADER is stripped from the section NAME. The # fixture's `[beta] # ...` is why `sections` above reads `beta` and not # `beta] # a comment...`. check_eq $P/beta-keys "one|two" "$(pv beta_keys)" check_eq $P/beta-one "1" "$(pv beta_one)" # A SECTION THAT DECLARES NO KEYS IS A SECTION. `[gamma]` is present and its # mapping is empty -- distinct from absent, which is what `contains-missing` # above measures. check_eq $P/gamma-present "yes" "$(pv gamma_present)" check_eq $P/gamma-empty "yes" "$(pv gamma_empty)" # ----------------------------------------------------------------- get() ----- # THE ARM THAT DOES NOT RAISE. `.get()` returns a default where `[...]` raises, # and that difference is the documented one -- asserted here and, from the other # side, by the `keyerror-*` cases below. check_eq $P/get-present "value1" "$(pv get_present)" check_eq $P/get-missing-default "None" "$(pv get_missing_default)" check_eq $P/get-missing-explicit "fallback" "$(pv get_missing_explicit)" # A missing SECTION also returns the default rather than raising -- a different # code path from a missing key, and the same answer. check_eq $P/get-missing-section "None" "$(pv get_missing_section)" # THE `convert` CALLABLE, applied only when the key is present. The value is # `int('1') + 41`, so a `convert` that was ignored would yield the string and a # red naming it rather than a silently-passing 1. check_eq $P/get-convert "42" "$(pv get_convert)" # ...and NOT applied to the default, which would be `int(None)` and a TypeError. check_eq $P/get-convert-missing "None" "$(pv get_convert_missing)" # The same two through the SectionWrapper, which forwards into the above. A # wrapper that forwarded the wrong section would pass every case that reads # `cfg.sections` directly. check_eq $P/wrapper-get "2" "$(pv wrapper_get)" check_eq $P/wrapper-get-default "dflt" "$(pv wrapper_get_default)" check_eq $P/wrapper-name "alpha" "$(pv wrapper_name)" # ------------------------------------------------------- the line numbers ---- # 1-BASED, which is what a human reads in an editor and the whole point of the # package. `lineof` adds one to the parser's own 0-based count; the `bad-*` # cases below assert the 0-based `.lineno` alongside its 1-based rendering, and # the three together are what pin the convention at each boundary. check_eq $P/lineof-beta "4" "$(pv lineof_beta)" check_eq $P/lineof-alpha "8" "$(pv lineof_alpha)" check_eq $P/lineof-key1 "9" "$(pv lineof_key1)" check_eq $P/lineof-multi "13" "$(pv lineof_multi)" check_eq $P/lineof-gamma "17" "$(pv lineof_gamma)" # A name it never saw reports None -- rather than raising, or guessing a 0 that a # consumer would render as line 1. check_eq $P/lineof-missing "None" "$(pv lineof_missing)" check_eq $P/wrapper-lineof "10" "$(pv wrapper_lineof)" # `.items()` on the wrapper, in line order -- the iteration a consumer walks. check_eq $P/beta-items "one=1|two=2" "$(pv beta_items)" # THE STRING CONSTRUCTOR ARM, where the path is used only for error reports. This # is the arm pytest uses, and it must agree with the file arm exactly -- asserted # as an equality of the whole parse, not just of the section names. check_eq $P/data-arm-sections "alpha|beta|gamma" "$(pv data_arm_sections)" check_eq $P/data-arm-agrees "yes" "$(pv data_arm_agrees)" # ------------------------------------------------------- the KeyError arm ---- # THE DOCUMENTED RAISE the ticket names. `[...]` raises where `.get()` returns a # default, and the two are asserted together -- above and here -- so a release # that made indexing forgiving is caught rather than silently changing what a # consumer's `except KeyError` guards. # # A missing SECTION and a missing KEY raise from different `__getitem__`s -- # IniConfig's own, and SectionWrapper's forward into the mapping -- so both are # driven, and the argument is asserted so the exception names WHAT was missing. check_eq $P/keyerror-section "KeyError" "$(pv keyerror_section)" check_eq $P/keyerror-section-arg "nosuch" "$(pv keyerror_section_arg)" check_eq $P/keyerror-key "KeyError" "$(pv keyerror_key)" check_eq $P/keyerror-key-arg "nokey" "$(pv keyerror_key_arg)" # ----------------------------------------------------- the ParseError arm ---- # THE LINE-PRECISE ERROR REPORT, which is what this package is for. Six malformed # fixtures, one per documented `raise`, each the SMALLEST input that reaches its # own raise site -- so a red names one arm of the parser rather than a fixture # that could have failed several ways. # # For each: the exception type, its 0-BASED `.lineno`, and the whole rendered # report (`{lineno+1}: {msg}`, path aside). The two line numbers DIFFER BY DESIGN # and are asserted separately -- see the header. `-render` carries the message # too, so a parser that found the right line for the wrong reason is caught. # A line that is neither a section, nor `name=value`, nor `name:value`, nor a # continuation. The parser's own raise, and the only message that carries the # offending text -- which is why its rendering has TWO colons in it, and why the # probe cuts the line number against the known path rather than by splitting. check_eq $P/bad-unexpected-line "ParseError" "$(pv bad_unexpected_line)" check_eq $P/bad-unexpected-line-lineno "1" "$(pv bad_unexpected_line_lineno)" check_eq $P/bad-unexpected-line-render "2: unexpected line: 'justaword'" "$(pv bad_unexpected_line_render)" check_eq $P/bad-unexpected-line-path "unexpected_line.ini" "$(pv bad_unexpected_line_path)" # A value before any section header. The parse itself succeeds; IniConfig's own # loop rejects it -- a different raise site from every other case here. check_eq $P/bad-no-section "ParseError" "$(pv bad_no_section)" check_eq $P/bad-no-section-lineno "0" "$(pv bad_no_section_lineno)" check_eq $P/bad-no-section-render "1: no section header defined" "$(pv bad_no_section_render)" # `[]` -- the header parses and the empty name is rejected. check_eq $P/bad-empty-section-name "ParseError" "$(pv bad_empty_section_name)" check_eq $P/bad-empty-section-name-lineno "0" "$(pv bad_empty_section_name_lineno)" check_eq $P/bad-empty-section-name-render "1: empty section name" "$(pv bad_empty_section_name_render)" # A leading-whitespace line with nothing above it to continue. check_eq $P/bad-continuation-first "ParseError" "$(pv bad_continuation_first)" check_eq $P/bad-continuation-first-lineno "0" "$(pv bad_continuation_first_lineno)" check_eq $P/bad-continuation-first-render "1: unexpected value continuation" "$(pv bad_continuation_first_render)" # The same section header twice. The line reported is the SECOND one -- the # duplicate, not the original -- which is the useful end and is what the number # pins. check_eq $P/bad-duplicate-section "ParseError" "$(pv bad_duplicate_section)" check_eq $P/bad-duplicate-section-lineno "1" "$(pv bad_duplicate_section_lineno)" check_eq $P/bad-duplicate-section-render "2: duplicate section 's'" "$(pv bad_duplicate_section_render)" # The same key twice in one section; again the second occurrence is the one # reported, and the message names the key. check_eq $P/bad-duplicate-name "ParseError" "$(pv bad_duplicate_name)" check_eq $P/bad-duplicate-name-lineno "2" "$(pv bad_duplicate_name_lineno)" check_eq $P/bad-duplicate-name-render "3: duplicate name 'a'" "$(pv bad_duplicate_name_render)" # ParseError is an ordinary Exception, so a consumer's bare `except` catches it. # Stated because the class is exported for consumers to catch and subclass. check_eq $P/parseerror-is-exception "yes" "$(pv parseerror_is_exception)" # The probe ran to the end. Every case above greps a key, so a probe that died # halfway would leave the remaining keys absent and each case would red with # got:'' -- this case is the one that says the cause was a truncated run. check_eq $P/probe-complete "yes" "$(pv done)" rm -rf "$W"