. "$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 ten measurements. # # # # The work inside each start is `re.match' over TOML # # documents under 200 bytes. The largest document in the # # suite is four lines. 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-pytoml 0.1.21 (meta-openembedded/meta-python # recipes-devtools/python/python3-pytoml_0.1.21.bb, `inherit pypi setuptools3 # ptest', PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE DOES. # ============================================================================ # # A TOML **0.4.0** parser and writer. `loads(s)' / `load(fin)' return a plain # dict; `dumps(obj)' / `dump(obj, fout)' write one back out. A syntax error # raises `TomlError', which carries `.message', `.line', `.col' and `.filename' # and formats as `(, ): '. # # Five modules, 606 lines of Python between them, no C, no configure step and # no compiled extension. That is why it ports: this image's python3 is ONE # STATIC BINARY with an empty lib-dynload and no dynamic loader (BRINTOS-1476), # so a module carrying an extension could not be imported here at all. # # It predates the stdlib `tomllib' (3.11) and predates TOML 1.0 by years. The # version it implements is not a detail -- half the cases below exist because # 0.4.0 is a SMALLER language than the TOML most readers have in mind, and this # parser is faithful to the small one. # # ============================================================================ # WHAT 0.4.0 DOES NOT HAVE, AND THIS PARSER THEREFORE REFUSES. # ============================================================================ # # Each of these is a well-formed TOML 1.0 document that this library rejects. # They are asserted as refusals so that a version bump which added them would # turn the case naming it red rather than passing unremarked: # # key-dotted `a.b = 1' -- dotted keys are 0.5.0 # err-local-datetime `1979-05-27T07:32:00' with no offset # err-date-only `1979-05-27' # err-time-only `07:32:00' # err-bare-inf/-nan `inf' / `nan' are 1.0 # inline-trailing-comma `{x = 1,}' # inline-newline-inside an inline table spanning two lines # # The offset one is worth singling out because the TICKET'S OWN acceptance text # expects local datetimes to parse. They do not: `rfc3339_re' makes the offset # MANDATORY, so `1979-05-27T07:32:00' never reaches the datetime branch at all # -- `1979' matches the integer rule, and the parse dies on the `-' after it. # `err-local-datetime' asserts the refusal, at column 9, which is where that # integer ended. # # ============================================================================ # THE ERROR MESSAGE IS THE LITERAL STRING `msg' FOR EVERY SYNTAX ERROR. # ============================================================================ # # `_Source._expect' raises `TomlError('msg', ...)' -- a placeholder that was # never filled in. Exactly FIVE errors carry a message a caller could branch # on, and all five are SEMANTIC rather than syntactic, raised by `loads' after # the parse rather than by the parser: # # duplicate_keys. Key "" was used more than once. # duplicate_tables # table_type_mismatch # key_table_conflict # array-type-mismatch # # Everything else -- an unterminated string, a bad escape, a lone surrogate, a # control character, a leading zero, a trailing dot, a missing value -- is # `msg'. The `err-*' cases assert the message text as well as the position, so # the line between the two families is stated rather than implied. # # ============================================================================ # AND THE COLUMN IS NOT THE ERROR SITE. # ============================================================================ # # `_p_toml' parses the whole statement list inside a backtracking `with s:', # and `_Source.__exit__' SWALLOWS a TomlError and restores the position to the # last committed statement. The error a caller finally sees comes from the # `expect_eof()' AFTER that block, so its position is where the last GOOD # statement ended -- not where the document went wrong. # # In practice that means column 1 nearly always, and the line number is the # line the failing statement STARTS on: # # err-on-line-1 / -3 / -4 the same broken value, moved down the file, # reported at (1,1), (3,1) and (4,1) # err-leading-zero (1,6) -- `a = 0' committed, `1' did not # err-local-datetime (1,9) -- `a = 1979' committed # # So the position is real information, but it is "how far the parser got", not # "where the mistake is". A caller pointing a user at line:col will point at # the start of the broken line and column 1 of it. # # ============================================================================ # THE ROUND TRIP IS NOT TOTAL. FOUR WAYS `loads(dumps(x)) != x'. # ============================================================================ # # The `rt-*' family asserts the property a caller actually relies on, which no # amount of asserting `dumps' output text would cover. Twenty-one shapes round # trip exactly -- scalars, non-ASCII, control characters, nested tables, arrays # of tables with sub-tables, big ints, floats at both exponent extremes, and # every datetime form the language has. Four do not, and each is asserted at # what actually happens, so a release that repairs one turns the case naming it # red: # # rt-none `{"a": None}' is written as a COMMENT # (`#a = null # To use: ...') and comes back as `{}'. # The key is silently gone -- no exception, no warning. # rt-mixed-list `{"a": [1, "x"]}' is WRITTEN, and will not re-parse: # `dumps' has no type check and `loads' enforces 0.4.0's # homogeneous-array rule. Same for `[1, 1.0]', which is # heterogeneous to this parser because `int' and `float' # are different kinds. # rt-empty-key `{"": 1}' is written as a bare ` = 1', which is not a # document. `_escape_id' returns '' unquoted because # `any(...)' over no characters is False. # rt-naive-datetime a datetime with NO tzinfo is written with a `Z' suffix # and comes back claiming UTC. `format_rfc3339' reads # `utcoffset()' as None and substitutes zero, so the # writer FABRICATES a timezone the value never had. # # ============================================================================ # THE tzname BUG. # ============================================================================ # # A parsed datetime carries `pytoml.utils._TimeZone', not `datetime.timezone', # and `_TimeZone.tzname' formats its hours and minutes -- both FLOATS, from # `//' on `total_seconds()' -- with `'{}{:.02}{:.02}''. In Python's format mini # language `.02' on a float is a PRECISION of two significant digits, not a # width of two. So: # # Z -> '+0.00.0' # -07:00 -> '-7.00.0' # +05:30 -> '+5.03e+01' <- 30 minutes, in scientific notation # # The datetimes themselves are correct: `utcoffset()' is right for all three, # `dst()' is None, and comparison against a stdlib UTC datetime succeeds # (`dt-equals-stdlib-utc'). Only the NAME is wrong -- which is what a caller # formatting a timestamp for a human would print. # # ============================================================================ # TYPES THE WRITER REFUSES, AND THE ONE IT CRASHES ON. # ============================================================================ # # `inf' and `nan' raise ValueError naming the value, which is deliberate: 0.4.0 # has no syntax for either. `datetime.date', `datetime.time', a tuple and a set # fall off the end of `_format_value' into a bare `RuntimeError' carrying only # the value -- no message, no type name. A non-dict at the top level raises # AttributeError from `.keys()'. # # `bytes' is the interesting one. `_format_value' HAS a bytes branch -- it is # tested for beside `unicode' -- but it sends bytes to `_escape_string', which # indexes them into INTS and evaluates `c in '"\\\n\r\t\b\f'', so Python 3 # raises TypeError before any escaping happens. The branch is Python 2 residue # and cannot work on this image at all; `dumps-bytes-value' asserts the # TypeError rather than the RuntimeError the else-branch would have produced. # # ============================================================================ # THE FIXTURES ARE WRITTEN, NOT SHIPPED. # ============================================================================ # # Upstream's own test corpus is the top-level `test/' directory, and it is NOT # packaged -- `setup.py' declares `packages=["pytoml"]', so only the package # itself installs. `no-test-dir' asserts that. Every document the probe parses # is a string literal a few lines long, so no fixture file is needed and none # is shipped; the two cases that DO need a real file -- `load' reads `fin.name' # to fill in `TomlError.filename' -- write into a private directory under # $TMPDIR and remove it again. # # `pytoml/test.py' IS packaged, because it is inside the package rather than # beside it. It is not a test suite: it is `translate_to_test', the helper that # renders a parsed document into the typed JSON form the toml-test harness # compares against. The `tt-*' cases exercise it as the exported API it is. # # ============================================================================ # THE RDEPENDS IS CORRECTLY DECLARED. # ============================================================================ # # The recipe declares `RDEPENDS:${PN} += "python3-datetime python3-stringold"', # and the built control file reads `python3-core, python3-datetime, # python3-stringold' with `python3-core' added by setuptools3-base. All three # are sub-packages of the ported python3 (BRINTOS-1476) and all three are # already on this image. `datetime' is used by `utils.py' and `writer.py'; # `string' is used by `writer.py' for `_key_chars'. `pkg-depends-exact' asserts # the line whole, so a lean image that dropped one would say so here once # rather than present as a broken library. # # The `-ptest' RDEPENDS (python3-pytest, python3-unittest-automake-output) is # NOT ours: `ptest' is not in DISTRO_FEATURES for this image, so no ptest # package is produced and nothing pulls pytest in. `no-ptest-dir' asserts it. # # ============================================================================ P=python3-pytoml # 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/pytoml DI=$SITE/pytoml-0.1.21.dist-info PKGLIST=/var/lib/dpkg/info/python3-pytoml.list PROBE=$BST_DATA/python3-pytoml-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-pytoml.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ---------------------------------------------------------------- layout ----- # IT IS A PACKAGE OF FIVE MODULES, NOT A MODULE. A release that flattened it # 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/package-dir test -d "$PKG" check $P/no-flat-module test ! -f "$SITE/pytoml.py" check $P/init-file test -s "$PKG/__init__.py" check $P/mod-core test -s "$PKG/core.py" check $P/mod-parser test -s "$PKG/parser.py" check $P/mod-utils test -s "$PKG/utils.py" check $P/mod-writer test -s "$PKG/writer.py" # Inside the package, so packaged -- see the header. This is `translate_to_test', # not a test suite. check $P/mod-test test -s "$PKG/test.py" # Upstream's corpus lives in a TOP-LEVEL `test/' that setup.py does not declare, # so it must not be here. `ptest' is not in DISTRO_FEATURES either. check $P/no-test-dir test ! -d "$SITE/test" check $P/no-ptest-dir test ! -d "/usr/lib/python3-pytoml" 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-core test -s "$PKG/__pycache__/core.cpython-312.pyc" check $P/pycache-parser test -s "$PKG/__pycache__/parser.cpython-312.pyc" check $P/pycache-utils test -s "$PKG/__pycache__/utils.cpython-312.pyc" check $P/pycache-writer test -s "$PKG/__pycache__/writer.cpython-312.pyc" check $P/pycache-test test -s "$PKG/__pycache__/test.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" -- see the header. 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. setup.py has no `entry_points', so the package # installs NO executable and there is nothing to reach through a shebang. _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_eq $P/pkg-no-bindir "" \ "$(grep -E '^/usr/bin/' "$W/pkgf" 2>/dev/null)" check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-version "Version: 0.1.21" cat "$DI/METADATA" check_out $P/dist-info-name "Name: pytoml" cat "$DI/METADATA" check_out $P/dist-info-summary "Summary: A parser for TOML-0.4.0" cat "$DI/METADATA" 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 Python 2 # wheel tag on a package this image can only run under Python 3, worth pinning # so 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 "pytoml" cat "$DI/top_level.txt" sed -n '/^Package: python3-pytoml$/,/^$/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: 0.1.21-r0" cat "$W/st" # ALL THREE RDEPENDS ARE SUB-PACKAGES OF THE PORTED python3 (BRINTOS-1476) and # all three are already on this image -- see the header for what uses each. 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, python3-datetime, python3-stringold" \ "$(cat "$W/dep")" # ---------------------------------------------------------------- import ----- check $P/import python3 -c 'import pytoml' # A DeprecationWarning or a stray print on import would be invisible to the case # above, which scores only the exit status. `writer.py' does a guarded # `from pathlib import PurePath', which is the one import here that could warn. check_eq $P/import-quiet "" \ "$(python3 -c 'import pytoml' 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 "210" "$(pv emitted_count)" # -------------------------------------------------------------- identity ----- # WHICH COPY ANSWERED. Every behavioural case below is a statement about the # package named here. check_eq $P/package-answered "$PKG/__init__.py" "$(pv package_file)" check_eq $P/is-package "yes" "$(pv is_package)" check_eq $P/submodules-answered \ "core.py,parser.py,utils.py,writer.py,test.py" "$(pv submodule_files)" check_eq $P/dist-version "0.1.21" "$(pv dist_version)" check_eq $P/dist-name "pytoml" "$(pv dist_name)" # THE EXPORTED SURFACE. `__init__.py' re-exports six names and Python adds the # five submodules to the same namespace, so `from pytoml import *' would bring # `parser' and `test' with it. There is NO `__all__' to stop that and NO # `__version__' at all -- a caller wanting the version must read the dist-info, # which is what `dist-version' above does. check_eq $P/public-names \ "TomlError,core,dump,dumps,load,loads,parser,test,translate_to_test,utils,writer" \ "$(pv public_names)" check_eq $P/no-dunder-version "no" "$(pv has___version__)" check_eq $P/no-dunder-all "no" "$(pv has___all__)" # `TomlError' SUBCLASSES RuntimeError, not ValueError -- so a caller writing # `except ValueError' around a parse catches nothing. All four fields are passed # to RuntimeError.__init__ as well as set as attributes, so they appear in # `.args' too. check_eq $P/tomlerror-base "RuntimeError" "$(pv tomlerror_base)" check_eq $P/tomlerror-args "('msg', 1, 1, '')" "$(pv tomlerror_args)" check_eq $P/tomlerror-str "(1, 1): msg" "$(pv tomlerror_str)" check_eq $P/tomlerror-repr "TomlError('msg', 1, 1, '')" "$(pv tomlerror_repr)" # THIS ARM'S FLOAT IS float64, which every float case below assumes. On a build # whose Python used a narrower float the float literals would all be wrong # together, and this case says which of the two happened. check_eq $P/float-is-double "53" "$(pv float_mant_dig)" check_eq $P/float-max-10-exp "308" "$(pv float_max_10_exp)" # ---------------------------------------------------------------- strings ---- check_eq $P/str-basic '{a=s:hi}' "$(pv str_basic)" check_eq $P/str-empty '{a=s:}' "$(pv str_empty)" # A LITERAL STRING PROCESSES NO ESCAPES -- the two backslashes survive. check_eq $P/str-literal '{a=s:C:\\Users\\n}' "$(pv str_literal)" # A MULTI-LINE STRING DROPS THE NEWLINE IMMEDIATELY AFTER ITS OPENER, and only # that one: a second newline is content. check_eq $P/str-ml-basic '{a=s:line1\nline2}' "$(pv str_ml_basic)" check_eq $P/str-ml-basic-keepnl '{a=s:\nfirst}' "$(pv str_ml_basic_keepnl)" check_eq $P/str-ml-literal '{a=s:x\\y}' "$(pv str_ml_literal)" check_eq $P/str-ml-quote-inside '{a=s:say "hi" ok}' "$(pv str_ml_quote_inside)" check_eq $P/str-ml-apos-inside "{a=s:it's}" "$(pv str_ml_lit_apos_inside)" check_eq $P/str-escapes '{a=s:\t\n\r\u000c\u0008\\"}' "$(pv str_escapes)" # A TRAILING `\' BEFORE A NEWLINE EATS THE NEWLINE AND THE INDENT AFTER IT. check_eq $P/str-line-continuation '{a=s:abcd}' "$(pv str_line_continuation)" check_eq $P/str-esc-short-uni '{a=s:\u00e9}' "$(pv str_esc_short_uni)" check_eq $P/str-esc-short-uni-upper '{a=s:\u00e9}' "$(pv str_esc_short_uni_upper)" check_eq $P/str-esc-long-uni '{a=s:\U0001f600}' "$(pv str_esc_long_uni)" # Raw non-ASCII in the SOURCE, rather than escaped -- this is the case that # would move if the image's default encoding were not UTF-8. check_eq $P/str-raw-non-ascii '{a=s:h\u00e9llo \U0001f600}' "$(pv str_raw_non_ascii)" # --------------------------------------------------------------- numbers ----- check_eq $P/int-plain '{a=i:42}' "$(pv int_plain)" check_eq $P/int-zero '{a=i:0}' "$(pv int_zero)" check_eq $P/int-plus '{a=i:99}' "$(pv int_plus)" check_eq $P/int-minus '{a=i:-17}' "$(pv int_minus)" check_eq $P/int-underscores '{a=i:1000000}' "$(pv int_underscores)" # ARBITRARY PRECISION -- 2**70 is not truncated to a machine word on this arm. check_eq $P/int-big '{a=i:1180591620717411303424}' "$(pv int_big)" check_eq $P/float-frac '{a=f:3.14}' "$(pv float_frac)" check_eq $P/float-neg '{a=f:-0.01}' "$(pv float_neg)" check_eq $P/float-exp-plus '{a=f:5e+22}' "$(pv float_exp_plus)" # `1e6' HAS NO DECIMAL POINT AND IS STILL A FLOAT: the int/float decision is # made on the SOURCE TEXT containing `.', `e' or `E', not on the value. check_eq $P/float-exp-bare '{a=f:1000000.0}' "$(pv float_exp_bare)" check_eq $P/float-exp-neg '{a=f:6.626e-34}' "$(pv float_exp_neg)" # Underscores are stripped before `float()', and the result is the nearest # double -- the last three digits of the literal do not survive, which is the # language's business rather than this parser's. check_eq $P/float-underscores '{a=f:9224617.445991227}' "$(pv float_underscores)" check_eq $P/bool-true '{a=true}' "$(pv bool_true)" check_eq $P/bool-false '{a=false}' "$(pv bool_false)" # ------------------------------------------------------------- datetimes ----- check_eq $P/dt-zulu '{a=dt:1979-05-27T07:32:00+00:00}' "$(pv dt_zulu)" check_eq $P/dt-offset-neg '{a=dt:1979-05-27T00:32:00-07:00}' "$(pv dt_offset_neg)" check_eq $P/dt-offset-pos '{a=dt:1979-05-27T00:32:00+05:30}' "$(pv dt_offset_pos)" check_eq $P/dt-fractional '{a=dt:1979-05-27T00:32:00.999999-07:00}' "$(pv dt_fractional)" # THE tzinfo IS THE LIBRARY'S OWN CLASS, not `datetime.timezone' -- so an # identity test against `timezone.utc' fails while an equality test on the # datetimes succeeds. Both are asserted, because a caller can reasonably write # either and only one of them works. check_eq $P/dt-tzinfo-type "_TimeZone" "$(pv dt_tzinfo_type)" check_eq $P/dt-tzinfo-not-stdlib "no" "$(pv dt_tzinfo_is_stdlib_utc)" check_eq $P/dt-utcoffset-zulu "0:00:00" "$(pv dt_utcoffset_zulu)" check_eq $P/dt-utcoffset-neg "-1 day, 17:00:00" "$(pv dt_utcoffset_neg)" check_eq $P/dt-utcoffset-pos "5:30:00" "$(pv dt_utcoffset_pos)" check_eq $P/dt-dst-is-none "yes" "$(pv dt_dst_is_none)" check_eq $P/dt-same-instant-equal "yes" "$(pv dt_same_instant_equal)" check_eq $P/dt-equals-stdlib-utc "yes" "$(pv dt_equals_stdlib_utc)" # THE tzname BUG -- see the header. Asserted at its WRONG values, so a release # that repairs the format string turns these three red. check_eq $P/tzname-zulu '+0.00.0' "$(pv tzname_zulu)" check_eq $P/tzname-neg '-7.00.0' "$(pv tzname_neg)" check_eq $P/tzname-half-hour '+5.03e+01' "$(pv tzname_half_hour)" # ---------------------------------------------------------------- arrays ----- check_eq $P/arr-int '{a=[i:1,i:2,i:3]}' "$(pv arr_int)" check_eq $P/arr-str '{a=[s:x,s:y]}' "$(pv arr_str)" check_eq $P/arr-empty '{a=[]}' "$(pv arr_empty)" check_eq $P/arr-nested '{a=[[i:1,i:2],[i:3]]}' "$(pv arr_nested)" check_eq $P/arr-deep '{a=[[[i:1]]]}' "$(pv arr_deep)" check_eq $P/arr-trailing-comma '{a=[i:1,i:2]}' "$(pv arr_trailing_comma)" check_eq $P/arr-multiline-comment '{a=[i:1,i:2]}' "$(pv arr_multiline_comment)" # 0.4.0 REQUIRES A HOMOGENEOUS ARRAY, and this parser enforces it -- by # comparing its own KIND strings, at the TOP level of the array only. So # `[1, 1.0]' is a refusal (int and float are different kinds) while # `[[1], ["x"]]' is accepted (both elements are kind `array', and nothing looks # inside them). The third case is the hole, and it is asserted as one. check_eq $P/arr-mixed-types '!TomlError(1,1,array-type-mismatch)' "$(pv arr_mixed_types)" check_eq $P/arr-int-and-float '!TomlError(1,1,array-type-mismatch)' "$(pv arr_int_and_float)" check_eq $P/arr-nested-heterogeneous '{a=[[i:1],[s:x]]}' "$(pv arr_nested_heterogeneous)" # --------------------------------------------------------------- tables ------ check_eq $P/inline-table '{a={x=i:1,y=s:z}}' "$(pv inline_table)" check_eq $P/inline-empty '{a={}}' "$(pv inline_empty)" check_eq $P/inline-nested '{a={x={y=i:1}}}' "$(pv inline_nested)" check_eq $P/inline-with-array '{a={x=[i:1,i:2]}}' "$(pv inline_with_array)" # BOTH OF THESE ARE VALID TOML 1.0 AND INVALID 0.4.0 -- see the header. check_eq $P/inline-trailing-comma '!TomlError(1,1,msg)' "$(pv inline_trailing_comma)" check_eq $P/inline-newline-inside '!TomlError(1,1,msg)' "$(pv inline_newline_inside)" check_eq $P/table-simple '{a={x=i:1}}' "$(pv table_simple)" check_eq $P/table-nested '{a={b={y=i:2},x=i:1}}' "$(pv table_nested)" check_eq $P/table-dotted-name '{a={b={c={x=i:1}}}}' "$(pv table_dotted_name)" # A SUPER-TABLE IS CREATED IMPLICITLY by `[a.b]' with no `[a]' before it... check_eq $P/table-implicit-super '{a={b={x=i:1}}}' "$(pv table_implicit_super)" # ...and may then be DEFINED AFTERWARDS, which is legal because the implicit # creation left its scope unset. This is the case `duplicate_tables' does not # fire on, and the asymmetry is why it is asserted beside `err-dup-table'. check_eq $P/table-out-of-order '{a={b={x=i:1},y=i:2}}' "$(pv table_out_of_order)" # A quoted table name containing a dot is ONE key, not a path. check_eq $P/table-quoted-name '{a.b={x=i:1}}' "$(pv table_quoted_name)" check_eq $P/aot-simple '{a=[{x=i:1},{x=i:2}]}' "$(pv aot_simple)" # A SUB-TABLE UNDER AN ARRAY-OF-TABLES ATTACHES TO THE LAST ELEMENT, which is # the rule that makes `[a.b]' mean different things on either side of a `[[a]]'. check_eq $P/aot-with-subtable \ '{a=[{b={y=i:2},x=i:1},{b={y=i:3},x=i:2}]}' "$(pv aot_with_subtable)" check_eq $P/aot-nested-aot '{a=[{b=[{x=i:1},{x=i:2}]}]}' "$(pv aot_nested_aot)" check_eq $P/key-bare '{a-b_c1=i:1}' "$(pv key_bare)" check_eq $P/key-quoted-dot '{a.b=i:1}' "$(pv key_quoted_dot)" check_eq $P/key-literal-space '{a b=i:1}' "$(pv key_literal_space)" # DOTTED KEYS ARE 0.5.0 -- see the header. check_eq $P/key-dotted '!TomlError(1,1,msg)' "$(pv key_dotted)" # ------------------------------------------------------------- documents ----- check_eq $P/doc-empty '{}' "$(pv doc_empty)" check_eq $P/doc-comment-only '{}' "$(pv doc_comment_only)" # CRLF IS NORMALISED before the parse, so a document written on Windows parses. check_eq $P/doc-crlf '{a=i:1,b=i:2}' "$(pv doc_crlf)" # `loads' DECODES bytes AS UTF-8 ITSELF, so a caller may hand it either. check_eq $P/doc-bytes-input '{a=i:1}' "$(pv doc_bytes_input)" # A UTF-8 BOM IS NOT STRIPPED, so a document saved by an editor that writes one # fails at (1,1) with the placeholder message and no hint as to why. check_eq $P/doc-bom '!TomlError(1,1,msg)' "$(pv doc_bom)" # ---------------------------------------------------- errors: the five ------- # The only five with a message a caller can branch on -- see the header. Each # is raised by `loads' after the parse, which is why its position is the # STATEMENT's, and is therefore the one place in this library where line:col # points at the thing that is wrong. check_eq $P/err-dup-key \ '!TomlError(2,1,duplicate_keys. Key "a" was used more than once.)' \ "$(pv err_dup_key)" check_eq $P/err-dup-key-later \ '!TomlError(4,1,duplicate_keys. Key "a" was used more than once.)' \ "$(pv err_dup_key_later)" check_eq $P/err-dup-table '!TomlError(3,1,duplicate_tables)' "$(pv err_dup_table)" check_eq $P/err-table-then-aot '!TomlError(3,1,table_type_mismatch)' "$(pv err_table_then_aot)" check_eq $P/err-aot-then-table '!TomlError(3,1,table_type_mismatch)' "$(pv err_aot_then_table)" check_eq $P/err-key-table-conflict '!TomlError(3,1,key_table_conflict)' "$(pv err_key_table_conflict)" # ...except this one. `key_table_conflict' is raised from `merge_tables', which # runs AFTER the statement loop has ended, so the `pos' its `error()' closes # over is the LAST statement in the document rather than the conflicting one. # Here the conflict is `a' on line 1 against `[a]' on line 4, and the report is # line 5. check_eq $P/err-key-table-conflict-late '!TomlError(5,1,key_table_conflict)' \ "$(pv err_key_table_conflict_late)" check_eq $P/err-array-type-mismatch '!TomlError(1,1,array-type-mismatch)' \ "$(pv err_array_type_mismatch)" # ------------------------------------------- errors: the placeholder --------- # Every one of these reports the literal message `msg' -- see the header. check_eq $P/err-unterminated-basic '!TomlError(1,1,msg)' "$(pv err_unterminated_basic)" check_eq $P/err-unterminated-literal '!TomlError(1,1,msg)' "$(pv err_unterminated_literal)" check_eq $P/err-unterminated-ml '!TomlError(1,1,msg)' "$(pv err_unterminated_ml)" check_eq $P/err-bad-escape '!TomlError(1,1,msg)' "$(pv err_bad_escape)" # `\/' IS VALID JSON AND NOT VALID TOML, which is the escape a caller coming # from JSON reaches for first. check_eq $P/err-escaped-solidus '!TomlError(1,1,msg)' "$(pv err_escaped_solidus)" # A LONE SURROGATE IS REFUSED EXPLICITLY -- `_p_basicstr_content' tests the # 0xd800..0xdfff range rather than letting `chr' build an unpaired surrogate. check_eq $P/err-lone-surrogate '!TomlError(1,1,msg)' "$(pv err_lone_surrogate)" check_eq $P/err-control-in-string '!TomlError(1,1,msg)' "$(pv err_control_in_string)" check_eq $P/err-bare-inf '!TomlError(1,1,msg)' "$(pv err_bare_inf)" check_eq $P/err-bare-nan '!TomlError(1,1,msg)' "$(pv err_bare_nan)" check_eq $P/err-missing-value '!TomlError(1,1,msg)' "$(pv err_missing_value)" # THE TWO THAT DO NOT REPORT COLUMN 1, and they are the proof of the backtrack # rule: `a = 0' and `a = 1' each parse as a complete statement, so the commit # point is column 6 and the leftover digit or dot is what the document dies on. check_eq $P/err-leading-zero '!TomlError(1,6,msg)' "$(pv err_leading_zero)" check_eq $P/err-trailing-dot '!TomlError(1,6,msg)' "$(pv err_trailing_dot)" # THE SAME BROKEN VALUE, MOVED DOWN THE FILE. The line tracks the failing # statement; the column stays 1. check_eq $P/err-on-line-1 '!TomlError(1,1,msg)' "$(pv err_on_line_1)" check_eq $P/err-on-line-3 '!TomlError(3,1,msg)' "$(pv err_on_line_3)" check_eq $P/err-on-line-4 '!TomlError(4,1,msg)' "$(pv err_on_line_4)" # ------------------------------------------ errors: NOT a TomlError ---------- # A DATE OUT OF RANGE ESCAPES THE PARSER AS A ValueError -- with no line, no # column and no filename. `rfc3339_re' matches on SHAPE alone, so an impossible # day, month or hour reaches `datetime.datetime()' and its own exception # propagates. A caller catching `TomlError' around a parse does not catch this, # which makes it the sharpest single finding in this suite: the ticket's # acceptance text expected a bad datetime to raise `TomlError'. check_eq $P/err-bad-datetime-day '!ValueError' "$(pv err_bad_datetime_day)" check_eq $P/err-bad-datetime-month '!ValueError' "$(pv err_bad_datetime_month)" check_eq $P/err-bad-datetime-hour '!ValueError' "$(pv err_bad_datetime_hour)" # 0.4.0 HAS NO OFFSETLESS DATETIME -- see the header. The columns are where the # integer that swallowed the leading digits ended. check_eq $P/err-local-datetime '!TomlError(1,9,msg)' "$(pv err_local_datetime)" check_eq $P/err-date-only '!TomlError(1,9,msg)' "$(pv err_date_only)" check_eq $P/err-time-only '!TomlError(1,6,msg)' "$(pv err_time_only)" # ------------------------------------------------- filename plumbing --------- # The three ways a filename reaches the message, which is the ONE field of # `TomlError' a caller can rely on being useful. check_eq $P/err-filename-arg 'cfg.toml(1, 1): msg' "$(pv err_filename_arg)" check_eq $P/err-filename-default '(1, 1): msg' "$(pv err_filename_default)" check_eq $P/err-load-filename 'bad.toml' "$(pv err_load_filename)" # A file object with no `.name' -- `load' falls back to `repr(fin)', so the # filename in the message is a Python object repr rather than a path. check_eq $P/err-load-noname-is-repr 'yes' "$(pv err_load_noname_is_repr)" check_eq $P/load-file '{a=i:1}' "$(pv load_file)" check_eq $P/load-stringio '{a=i:1}' "$(pv load_stringio)" # `load' ACCEPTS A BINARY STREAM TOO, because `loads' decodes bytes itself. check_eq $P/load-bytesio '{a=i:1}' "$(pv load_bytesio)" # ---------------------------------------------------------------- writer ----- check_eq $P/dumps-scalars 'i = 1\nf = 1.5\nb = true\ns = "hi"\n' "$(pv dumps_scalars)" check_eq $P/dumps-empty-dict '' "$(pv dumps_empty_dict)" # SCALARS ARE WRITTEN BEFORE SUB-TABLES whatever order the dict is in, which is # what makes the output re-parse: a key written after a `[section]' header would # land inside that section. check_eq $P/dumps-nested 'c = 2\n\n[a]\nb = 1\n' "$(pv dumps_nested)" # A TABLE WITH NO SCALARS STILL GETS ITS HEADER, so `[a]' appears empty above # `[a.b]'. Harmless, and it is what makes `dumps-empty-table' round trip. check_eq $P/dumps-deep '[a]\n\n[a.b]\nc = 1\n' "$(pv dumps_deep)" check_eq $P/dumps-empty-table '[a]\n' "$(pv dumps_empty_table)" check_eq $P/dumps-aot '[[a]]\nx = 1\n\n[[a]]\nx = 2\n' "$(pv dumps_aot)" check_eq $P/dumps-aot-subtable \ '[[a]]\nx = 1\n\n[a.s]\np = 1\n\n[[a]]\nx = 2\n\n[a.s]\np = 2\n' \ "$(pv dumps_aot_subtable)" check_eq $P/dumps-list 'a = [1, 2]\n' "$(pv dumps_list)" check_eq $P/dumps-empty-list 'a = []\n' "$(pv dumps_empty_list)" # A KEY OUTSIDE `[0-9A-Za-z-_]' IS QUOTED as a basic string, which is what keeps # a dot or a space in a key from becoming a path or a syntax error. check_eq $P/dumps-key-escape-dot '"a.b" = 1\n' "$(pv dumps_key_escape_dot)" check_eq $P/dumps-key-escape-space '"a b" = 1\n' "$(pv dumps_key_escape_space)" check_eq $P/dumps-key-non-ascii '"\u00e9" = 1\n' "$(pv dumps_key_non_ascii)" # CONTROL CHARACTERS BECOME `\uXXXX' and the six with short forms use those. check_eq $P/dumps-str-escapes 'a = "x\\ty\\nz\\\\\\"q\\u0001"\n' "$(pv dumps_str_escapes)" # FLOATS GO THROUGH `repr', so the shortest round-tripping form is written. check_eq $P/dumps-float 'a = 0.1\nb = 1e+300\nc = 1e-300\n' "$(pv dumps_float)" check_eq $P/dumps-int-big 'a = 1180591620717411303424\n' "$(pv dumps_int_big)" # `sort_keys' IS THE ONLY KNOB `dumps' HAS, and without it insertion order is # preserved -- both are asserted, because a caller diffing generated config # depends on which. check_eq $P/dumps-sort-keys 'a = 3\nm = 2\nz = 1\n' "$(pv dumps_sort_keys)" check_eq $P/dumps-no-sort-keys 'z = 1\nm = 2\na = 3\n' "$(pv dumps_no_sort_keys)" # A ZERO OFFSET IS WRITTEN `Z', not `+00:00'. check_eq $P/dumps-datetime 'a = 1979-05-27T07:32:00Z\n' "$(pv dumps_datetime)" check_eq $P/dumps-datetime-offset 'a = 1979-05-27T00:32:00-07:00\n' "$(pv dumps_datetime_offset)" check_eq $P/dumps-to-stream 'a = 1\n' "$(pv dumps_to_stream)" # ------------------------------------------------------------ round trip ----- # `loads(dumps(x)) == x' -- the property a caller relies on, asserted directly # rather than inferred from the output text. Twenty-one shapes hold. check_eq $P/rt-scalars 'yes' "$(pv rt_scalars)" check_eq $P/rt-non-ascii 'yes' "$(pv rt_non_ascii)" check_eq $P/rt-control-chars 'yes' "$(pv rt_control_chars)" check_eq $P/rt-nested 'yes' "$(pv rt_nested)" check_eq $P/rt-deep 'yes' "$(pv rt_deep)" check_eq $P/rt-empty-table 'yes' "$(pv rt_empty_table)" check_eq $P/rt-aot 'yes' "$(pv rt_aot)" check_eq $P/rt-aot-subtable 'yes' "$(pv rt_aot_subtable)" check_eq $P/rt-mixed-tables 'yes' "$(pv rt_mixed_tables)" check_eq $P/rt-list 'yes' "$(pv rt_list)" check_eq $P/rt-empty-list 'yes' "$(pv rt_empty_list)" check_eq $P/rt-nested-list 'yes' "$(pv rt_nested_list)" check_eq $P/rt-dict-in-list 'yes' "$(pv rt_dict_in_list)" check_eq $P/rt-key-dot 'yes' "$(pv rt_key_dot)" check_eq $P/rt-key-space 'yes' "$(pv rt_key_space)" check_eq $P/rt-key-non-ascii 'yes' "$(pv rt_key_non_ascii)" check_eq $P/rt-float 'yes' "$(pv rt_float)" check_eq $P/rt-int-big 'yes' "$(pv rt_int_big)" check_eq $P/rt-datetime 'yes' "$(pv rt_datetime)" check_eq $P/rt-datetime-offset 'yes' "$(pv rt_datetime_offset)" check_eq $P/rt-datetime-fractional 'yes' "$(pv rt_datetime_fractional)" # ...and four do not. Each is asserted at what actually happens -- see the # header -- so a release that repairs one turns the case naming it red. # # 1. `None' IS WRITTEN AS A COMMENT AND THE KEY IS SILENTLY LOST. check_eq $P/rt-none 'no:{}' "$(pv rt_none)" check_eq $P/dumps-none '#a = null # To use: uncomment and replace null with value\n' \ "$(pv dumps_none)" # 2. A HETEROGENEOUS LIST IS WRITTEN AND WILL NOT RE-PARSE. `dumps' has no type # check; `loads' enforces 0.4.0's homogeneous-array rule. Note that `[1, 1.0]' # is heterogeneous here, which surprises even a caller who knew the rule. check_eq $P/rt-mixed-list '!load-TomlError(array-type-mismatch)' "$(pv rt_mixed_list)" check_eq $P/dumps-mixed-list 'a = [1, "x"]\n' "$(pv dumps_mixed_list)" check_eq $P/rt-int-float-list '!load-TomlError(array-type-mismatch)' "$(pv rt_int_float_list)" check_eq $P/rt-dict-in-mixed-list '!load-TomlError(array-type-mismatch)' "$(pv rt_dict_in_mixed_list)" check_eq $P/dumps-dict-in-mixed-list 'a = [1, {x = 1}]\n' "$(pv dumps_dict_in_mixed_list)" # 3. AN EMPTY KEY IS WRITTEN UNQUOTED, producing ` = 1', which is not a # document. `_escape_id' returns it as-is because `any(...)' over no # characters is False. check_eq $P/rt-empty-key '!load-TomlError(msg)' "$(pv rt_empty_key)" check_eq $P/dumps-empty-key ' = 1\n' "$(pv dumps_empty_key)" # 4. A NAIVE DATETIME COMES BACK CLAIMING UTC. The writer FABRICATES a timezone # the value never had, and the round trip is unequal because a naive and an # aware datetime are never equal. check_eq $P/rt-naive-datetime 'no:{a=dt:1979-05-27T07:32:00+00:00}' "$(pv rt_naive_datetime)" check_eq $P/dumps-naive-datetime 'a = 1979-05-27T07:32:00Z\n' "$(pv dumps_naive_datetime)" # ------------------------------------------------- writer: what it refuses --- # `inf' and `nan' get a ValueError naming the value -- deliberate, since 0.4.0 # has no syntax for either. The rest fall off the end of `_format_value' into a # bare RuntimeError carrying only the value: no message, no type name, and # nothing to tell a caller which key was at fault. check_eq $P/dumps-inf '!ValueError' "$(pv dumps_inf)" check_eq $P/dumps-nan '!ValueError' "$(pv dumps_nan)" check_eq $P/dumps-date-object '!RuntimeError' "$(pv dumps_date_object)" check_eq $P/dumps-time-object '!RuntimeError' "$(pv dumps_time_object)" check_eq $P/dumps-tuple '!RuntimeError' "$(pv dumps_tuple)" check_eq $P/dumps-set '!RuntimeError' "$(pv dumps_set)" # A NON-DICT AT THE TOP LEVEL dies in `.keys()' rather than being reported. check_eq $P/dumps-list-toplevel '!AttributeError' "$(pv dumps_list_toplevel)" check_eq $P/dumps-none-toplevel '!AttributeError' "$(pv dumps_none_toplevel)" # THE DEAD `bytes' BRANCH -- see the header. TypeError, not RuntimeError. check_eq $P/dumps-bytes-value '!TypeError' "$(pv dumps_bytes_value)" # ------------------------------------------------------------------ utils ---- check_eq $P/parse-rfc3339-good '1979-05-27 07:32:00+00:00' "$(pv parse_rfc3339_good)" # `parse_rfc3339' RETURNS None RATHER THAN RAISING for anything it does not # match -- including a valid local datetime, and including a good timestamp with # one character of trailing junk, which the `m.group(0) != v' guard catches. check_eq $P/parse-rfc3339-local 'None' "$(pv parse_rfc3339_local)" check_eq $P/parse-rfc3339-junk 'None' "$(pv parse_rfc3339_junk)" check_eq $P/parse-rfc3339-trailing 'None' "$(pv parse_rfc3339_trailing)" # `format_rfc3339' ON A NAIVE DATETIME IS THE FABRICATION `rt-naive-datetime' # reports, seen directly at its source. check_eq $P/format-rfc3339-naive '1979-05-27T07:32:00Z' "$(pv format_rfc3339_naive)" check_eq $P/format-rfc3339-micro '1979-05-27T07:32:00.000005Z' "$(pv format_rfc3339_micro)" check_eq $P/format-rfc3339-offset '1979-05-27T00:32:00-07:00' "$(pv format_rfc3339_offset)" # The half-hour offset that `tzname' mangles is written CORRECTLY here -- the # bug is in `tzname' alone, not in the offset arithmetic. check_eq $P/format-rfc3339-half-hour '1979-05-27T00:32:00+05:30' "$(pv format_rfc3339_half_hour)" check_eq $P/rfc3339-re-compiled 'yes' "$(pv rfc3339_re_is_compiled)" check_eq $P/timezone-base 'tzinfo' "$(pv timezone_class)" # ------------------------------------------------------ translate_to_test ---- # The exported helper that renders a parsed document into the typed form the # toml-test harness compares against. A scalar becomes a `{type, value}' pair # with the value as a STRING; a table and an array-of-tables stay bare, which # is what lets the two be told apart from an array of scalars. check_eq $P/tt-int "[('type', 'integer'), ('value', '1')]" "$(pv tt_int)" check_eq $P/tt-float "[('type', 'float'), ('value', '1.5')]" "$(pv tt_float)" check_eq $P/tt-bool "[('type', 'bool'), ('value', 'true')]" "$(pv tt_bool)" check_eq $P/tt-string "[('type', 'string'), ('value', 'x')]" "$(pv tt_string)" check_eq $P/tt-datetime "[('type', 'datetime'), ('value', '1979-05-27T07:32:00Z')]" \ "$(pv tt_datetime)" check_eq $P/tt-array-type 'array' "$(pv tt_array_type)" check_eq $P/tt-array-len '2' "$(pv tt_array_len)" check_eq $P/tt-table-is-bare 'yes' "$(pv tt_table_is_bare)" check_eq $P/tt-aot-is-bare-list 'yes' "$(pv tt_aot_is_bare_list)" # It has no `None' case, so a document containing one -- which only `dumps' can # produce, and only as a comment -- raises the same bare RuntimeError the # writer does. check_eq $P/tt-none 'RuntimeError' "$(pv tt_none)" # ------------------------------------------------------------------ hooks ---- # `translate' SEES THE PARSER'S OWN (kind, source-text, value) TRIPLE, which is # the ONLY route to a number's original spelling -- `1_0' has already become # `10' by the time the value exists. `int' and `float' are the parser's kind # names, and they are what the array-type rule compares. check_eq $P/hook-translate \ "[('a', ('int', '1_0')), ('b', ('str', 'x')), ('c', ('float', '1.5'))]" \ "$(pv hook_translate)" # `object_pairs_hook' REACHES EVERY TABLE, not just the top level, so a caller # can have ordered output throughout. check_eq $P/hook-object-pairs 'OrderedDict' "$(pv hook_object_pairs)" rm -rf "$W"