. "$BST_LIB" # BST-TIMEOUT: 1200 # TWO python3 starts, and they are the whole of the cost. # # Every other case here reads the filesystem or dpkg with # # shell builtins, or greps the second start's output. # # # # The two are: a bare `import' (one case, so an import # # failure is legible without reading a transcript) and the # # probe, which takes ONE start for a hundred and forty # # measurements. # # # # THE ORDINARY BUDGET, NOT AN ENLARGED ONE. Nothing here # # forks a child, opens a file, sleeps, starts a thread or # # reaches a device, a peer or a terminal -- polyline is # # 3101 bytes of integer and float arithmetic over an # # io.StringIO, and every fixture it needs is a coordinate # # list the probe writes out. Its largest single case is a # # 500-point synthetic track: 3873 output characters. # # # # 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-polyline 2.0.2 (meta-openembedded/meta-python # recipes-devtools/python/python3-polyline_2.0.2.bb, `inherit pypi # python_setuptools_build_meta ptest') on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE DOES, AND WHAT THAT MAKES THE SUITE ASSERT. # ============================================================================ # # Google's Encoded Polyline Algorithm Format compresses a list of (lat, lon) # coordinates into an ASCII string. Each coordinate is scaled to an integer at a # fixed precision, differenced against its predecessor, ZIG-ZAG encoded so a # negative delta becomes a small non-negative integer, then emitted five bits at # a time with a continuation bit and a +63 offset that lands every byte in # printable ASCII. # # ---------------------------------------------------------------------------- # THE ASSERTION IS AGAINST A LITERAL FROM OUTSIDE THE LIBRARY, NOT A ROUND TRIP. # ---------------------------------------------------------------------------- # # The format's own documentation publishes a worked example: the three points # (38.5, -120.2), (40.7, -120.95), (43.252, -126.453) encode to # # _p~iF~ps|U_ulLnnqC_mqNvxq`@ # # and that string is what this suite scores the port against, character for # character. It has to be, because a round trip is SYMMETRIC UNDER A WRONG # SHIFT: an encoder that shifted a negative delta the wrong way and a decoder # that unshifted it the same wrong way agree with each other perfectly, produce # a track no other implementation on earth can read, and pass every # encode-then-decode case that could be written. The round trips below are # corroboration; the reference string is the assertion. # # WHY A FRESH ARM IS AT RISK HERE SPECIFICALLY. The whole of the encoding is # `coord <<= 1', `coord if coord >= 0 else ~coord', `coord & 0x1f', `coord >>= 5' # and the mirror-image `~(result >> 1) if comp else (result >> 1)' -- shifts and # complements ON SIGNED VALUES, at a five-bit stride, over integers scaled by # 1e5. It fails as a WRONG CHARACTER, not as a crash: nothing raises, nothing # logs, and the caller gets a plausible-looking track through the wrong part of # the world. So the cases that matter are the ones that reach the negative # branch -- negative latitudes, negative longitudes, tracks crossing zero, and # deltas of a single 1e-5 unit in each direction -- and they are enumerated # rather than sampled. # # `_py2_round' is the second such place. The format rounds half AWAY FROM ZERO, # which is Python 2's rule and not Python 3's: `round(0.5)' is 0 and # `round(2.5)' is 2 under the builtin's round-half-to-even. The module # implements the old rule with copysign/floor, and a port that quietly reached # for the builtin would be wrong on exactly the half-unit boundaries and right # everywhere else -- which is the hardest kind of wrong to notice. # # ---------------------------------------------------------------------------- # WHAT THE LIBRARY DOES **NOT** DO, WHICH IS AS MUCH OF THE CONTRACT. # ---------------------------------------------------------------------------- # # polyline 2.0.2 has no validation layer of any kind. It does not check that a # latitude is in range, that a string is inside the alphabet, or that a string # is not truncated. So the edge cases below are MEASURED and asserted as what # they are, not as what a reader might expect: # # * a latitude of 91 or a longitude of 181 encodes happily; # * a 3-tuple's third component is dropped in SILENCE, so an altitude a # caller carried along disappears; # * a precision-6 string decoded at the default precision 5 comes back a # factor of ten too large, with nothing to say so -- the precision is not # carried in the string, and this is the single most likely way to misuse # the library; # * a truncated string or a byte below the alphabet raises IndexError from # the raw `value[index]', not a diagnostic of its own. # # Writing those down is the point. A future upstream that ADDS validation # changes each of them, and this suite is what would report it. # # ============================================================================ # THE OTHER HALF: WHAT "PORTED" MEANS. # ============================================================================ # # The .deb installs on target, the files the recipe promises land where the # packaging says, and the installed distribution reports the version the recipe # pins. Every literal in that half was read out of THIS PORT'S OWN # python3-polyline_2.0.2-r0_wasm32.deb rather than predicted from the sdist. # # THE Depends LINE IS ONE OF THE ASSERTIONS. meta-python's recipe declares # `RDEPENDS:${PN} += "python3-six"' and polyline imports six nowhere -- see # recipes-devtools/python/python3-polyline_%.bbappend, which removes it. The # case below is what would report the bbappend having stopped taking effect. P=python3-polyline bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKG=$SITE/polyline INIT=$PKG/__init__.py MOD=$PKG/polyline.py DI=$SITE/polyline-2.0.2.dist-info PKGLIST=/var/lib/dpkg/info/python3-polyline.list PROBE=$BST_DATA/python3-polyline-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-polyline.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # --------------------------------------------------------------- the tree --- # src-layout: `polyline/' is a PACKAGE with the implementation in a submodule of # the same name, not a flat `polyline.py'. Both shapes import as `polyline', so # only the tree can tell them apart, and the wrong one would mean the wheel was # built from a different layout than the recipe's sdist. check $P/site-packages-dir test -d "$SITE" check $P/package-dir test -d "$PKG" check $P/init-present test -s "$INIT" check $P/module-present test -s "$MOD" check $P/not-a-flat-module test ! -e "$SITE/polyline.py" # Both sizes are the sdist's own, byte for byte: a pure-Python package is # COPIED into the wheel, so any difference here is a patch nobody declared. check_eq $P/init-size 113 \ "$(wc -c "$INIT" 2>/dev/null | awk '{print $1}')" check_eq $P/module-size 3101 \ "$(wc -c "$MOD" 2>/dev/null | awk '{print $1}')" check $P/pycache-init test -s "$PKG/__pycache__/__init__.cpython-312.pyc" check $P/pycache-mod test -s "$PKG/__pycache__/polyline.cpython-312.pyc" # NO py.typed, unlike several of its neighbours: polyline annotates its two # public functions but ships no marker, so a type checker treats the package as # untyped. Asserted so that an upstream adding one is visible. check $P/no-py-typed test ! -e "$PKG/py.typed" check_eq $P/package-file-set "__init__.py,__pycache__,polyline.py," \ "$(LC_ALL=C ls -1 "$PKG" 2>/dev/null | LC_ALL=C sort | tr '\n' ',')" check_eq $P/no-so "" "$(find "$PKG" -name '*.so' 2>/dev/null)" check $P/no-bin-script test ! -e /usr/bin/polyline # ------------------------------------------------------------- dist-info --- check $P/dist-info-dir test -d "$DI" check $P/dist-info-record test -s "$DI/RECORD" check $P/dist-info-metadata test -s "$DI/METADATA" check $P/dist-info-wheel test -s "$DI/WHEEL" check_eq $P/dist-info-file-set \ "LICENSE,METADATA,RECORD,WHEEL,top_level.txt," \ "$(LC_ALL=C ls -1 "$DI" 2>/dev/null | LC_ALL=C sort | tr '\n' ',')" check $P/no-entry-points test ! -e "$DI/entry_points.txt" check_eq $P/dist-info-top-level "polyline" \ "$(cat "$DI/top_level.txt" 2>/dev/null | tr -d ' \r\n')" check $P/wheel-purelib grep -q '^Root-Is-Purelib: true$' "$DI/WHEEL" check $P/wheel-tag-py3 grep -q '^Tag: py3-none-any$' "$DI/WHEEL" check $P/metadata-name grep -q '^Name: polyline$' "$DI/METADATA" check $P/metadata-version grep -q '^Version: 2\.0\.2$' "$DI/METADATA" check $P/metadata-summary \ grep -q "^Summary: A Python implementation of Google's Encoded Polyline Algorithm Format\.\$" "$DI/METADATA" check $P/metadata-requires-python grep -q '^Requires-Python: >=3\.7$' "$DI/METADATA" # THE BASE REQUIREMENT SET IS EMPTY. Every Requires-Dist line the wheel carries # is marked `extra ==', so an installer acting on this metadata would install # nothing alongside -- which is the packaging half of the six question the # bbappend answers. check_eq $P/metadata-requires-dist-count 8 \ "$(grep -c '^Requires-Dist:' "$DI/METADATA")" check_eq $P/metadata-requires-dist-unconditional 0 \ "$(grep '^Requires-Dist:' "$DI/METADATA" | grep -cv "extra ==")" check_eq $P/metadata-no-six 0 "$(grep -ci 'six' "$DI/METADATA")" check $P/license-file-present test -s "$DI/LICENSE" check $P/license-file-text grep -q 'Frederick Jansen' "$DI/LICENSE" # RECORD carries each shipped file's size beside its hash, so it is a second, # independent statement of the two sizes asserted above. check $P/record-lists-init \ grep -q '^polyline/__init__\.py,sha256=.*,113$' "$DI/RECORD" check $P/record-lists-module \ grep -q '^polyline/polyline\.py,sha256=.*,3101$' "$DI/RECORD" # ------------------------------------------------------------------ dpkg --- check $P/dpkg-list test -s "$PKGLIST" check $P/dpkg-lists-init grep -q "^$INIT\$" "$PKGLIST" check $P/dpkg-lists-module grep -q "^$MOD\$" "$PKGLIST" sed -n '/^Package: python3-polyline$/,/^$/p' /var/lib/dpkg/status > "$W/st" 2>/dev/null check $P/deb-known test -s "$W/st" check_out $P/deb-installed "Status: install ok installed" cat "$W/st" check_out $P/deb-version "Version: 2.0.2-r0" cat "$W/st" grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/deb-depends-line test -s "$W/dep" check_eq $P/deb-depends-exact "Depends: python3-core" "$(cat "$W/dep")" if grep -q 'python3-six' "$W/dep" 2>/dev/null; then bst_notok $P/deb-depends-no-six "python3-six still on the Depends line: $(cat "$W/dep")" else bst_ok $P/deb-depends-no-six fi # ----------------------------------------------------------- the module --- check $P/import python3 -c 'import polyline' if [ ! -s "$PROBE" ]; then bst_notok $P/probe-present "probe missing at $PROBE" 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-; } check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "145" "$(pv emitted_count)" _imp=$(pv import_ok) check_eq $P/import-ok "yes" "$_imp" if [ "$_imp" != "yes" ]; then _why="polyline did not import: $(pv import_error)" for _c in module-file dist-version \ module-name is-package submodule-name submodule-is-file \ decode-is-submodule encode-is-submodule has-all all-names \ public-names submodule-public-names has-dunder-version \ dist-name dist-requires-count dist-requires-base-empty \ dist-requires-no-six dist-extras dist-requires-python \ ast-imports six-imported decode-signature encode-signature \ decode-doc-first encode-doc-first ref-documented-string \ ref-encode ref-encode-matches ref-encode-len ref-decode \ ref-decode-len ref-decode-maxerr ref-decode-within-1e5 \ ref-reencode-stable ref-decode-tuple-types ref-decode-widths \ ref-decode-value-types ref-decode-first ref-decode-last \ ref-result-is-list geojson-encode geojson-encode-matches \ geojson-decode geojson-decode-is-swapped geojson-decode-first \ geojson-default-is-false geojson-truthy-one-ignored p6-encode \ p6-differs-from-p5 p6-decode p6-decode-maxerr \ p6-decode-within-1e6 p6-encode-len p6-decoded-at-p5-first \ p6-at5-factor-of-ten tbl-origin-enc tbl-origin-rt \ tbl-origin-exact tbl-origin-pair-enc tbl-origin-pair-rt \ tbl-origin-pair-exact tbl-repeated-point-enc \ tbl-repeated-point-rt tbl-repeated-point-exact \ tbl-one-step-lat-enc tbl-one-step-lat-rt tbl-one-step-lat-exact \ tbl-one-step-lng-enc tbl-one-step-lng-rt tbl-one-step-lng-exact \ tbl-negative-lat-enc tbl-negative-lat-rt tbl-negative-lat-exact \ tbl-negative-both-enc tbl-negative-both-rt \ tbl-negative-both-exact tbl-cross-zero-lat-enc \ tbl-cross-zero-lat-rt tbl-cross-zero-lat-exact \ tbl-cross-zero-lng-enc tbl-cross-zero-lng-rt \ tbl-cross-zero-lng-exact tbl-cross-zero-both-enc \ tbl-cross-zero-both-rt tbl-cross-zero-both-exact \ tbl-smallest-unit-enc tbl-smallest-unit-rt \ tbl-smallest-unit-exact tbl-smallest-neg-enc \ tbl-smallest-neg-rt tbl-smallest-neg-exact tbl-poles-enc \ tbl-poles-rt tbl-poles-exact tbl-descending-enc \ tbl-descending-rt tbl-descending-exact tbl-zigzag-track-enc \ tbl-zigzag-track-rt tbl-zigzag-track-exact repeated-tail \ repeated-len descending-differs descending-rt-exact \ long-rt-exact long-enc-len long-rt-len py2-round-half-up \ py2-round-half-down py2-round-differs-from-builtin \ py2-round-below-half py2-round-returns-int trans-ref-lat \ trans-ref-lng trans-zero trans-negative-one pcitr-pairs \ pcitr-single pcitr-empty decode-empty decode-truncated \ decode-one-char decode-below-alphabet decode-two-below-alphabet \ decode-high-char \ decode-non-string decode-list-arg decode-negative-precision \ encode-empty encode-lat-out-of-range encode-lng-out-of-range \ encode-huge-value encode-short-tuple encode-long-tuple \ encode-string-coord encode-none-arg encode-precision-zero \ encode-int-coords encode-nan encode-inf encode-generator \ encode-long-tuple-drops-third; do bst_skip $P/$_c "$_why" done rm -rf "$W" exit 0 fi check_eq $P/module-file "$INIT" "$(pv module_file)" # The version the RECIPE pins, read back through importlib.metadata -- the # package defines no `__version__' of its own, so this is the only thing on the # image that can answer it. check_eq $P/dist-version "2.0.2" "$(pv dist_version)" # ------------------------------------------------- shape of the module --- check_eq $P/module-name 'polyline' \ "$(pv module_name)" check_eq $P/is-package 'yes' \ "$(pv is_package)" check_eq $P/submodule-name 'polyline.polyline' \ "$(pv submodule_name)" check_eq $P/submodule-is-file 'yes' \ "$(pv submodule_is_file)" check_eq $P/decode-is-submodule 'yes' \ "$(pv decode_is_submodule)" check_eq $P/encode-is-submodule 'yes' \ "$(pv encode_is_submodule)" check_eq $P/has-all 'yes' \ "$(pv has_all)" check_eq $P/all-names 'decode,encode' \ "$(pv all_names)" check_eq $P/public-names 'decode,encode,polyline' \ "$(pv public_names)" check_eq $P/submodule-public-names \ 'List,Tuple,decode,encode,io,itertools,math' \ "$(pv submodule_public_names)" # ---------------------------------------------------- the metadata half --- # The package defines no `__version__'. Asserted as an ABSENCE so that an # upstream adding one is a visible change, with the real version question # answered by importlib.metadata above. check_eq $P/has-dunder-version 'no' \ "$(pv has_dunder_version)" check_eq $P/dist-name 'polyline' \ "$(pv dist_name)" check_eq $P/dist-requires-count '8' \ "$(pv dist_requires_count)" check_eq $P/dist-requires-base-empty 'yes' \ "$(pv dist_requires_base_empty)" check_eq $P/dist-requires-no-six 'yes' \ "$(pv dist_requires_no_six)" check_eq $P/dist-extras 'dev,publish' \ "$(pv dist_extras)" check_eq $P/dist-requires-python '>=3.7' \ "$(pv dist_requires_python)" # ------------------------------------------------------ the six question --- # Scored from the AST of both shipped files rather than grepped, so a mention # inside a string or a comment cannot pass for an import. `polyline' is the # re-export in `__init__.py'; the other four are stdlib, and every one of them # resolves to python3-core on this image -- io.py and typing.py as files, # itertools and math as extension modules this arm links into the interpreter. # `six_imported' reads sys.modules after the import, which is the same claim # made a second way: if six were reached at import time it would be there. check_eq $P/ast-imports 'io,itertools,math,polyline,typing' \ "$(pv ast_imports)" check_eq $P/six-imported 'no' \ "$(pv six_imported)" # ------------------------------------------------------- the public API --- # The two defaults are part of the contract: precision 5 (Google Maps) and # geojson False. A port that changed either would break every caller silently. check_eq $P/decode-signature \ '(expression: str, precision: int = 5, geojson: bool = False) -> List[Tuple[float, float]]' \ "$(pv decode_signature)" check_eq $P/encode-signature \ '(coordinates: List[Tuple[float, float]], precision: int = 5, geojson: bool = False) -> str' \ "$(pv encode_signature)" check_eq $P/decode-doc-first \ 'Decode a polyline string into a set of coordinates.' \ "$(pv decode_doc_first)" check_eq $P/encode-doc-first \ 'Encode a set of coordinates in a polyline string.' \ "$(pv encode_doc_first)" # ========================================================================== # THE REFERENCE VECTOR -- the case this whole suite exists for. # ========================================================================== # The three points and the string below are published TOGETHER by the format's # documentation. Neither is derived from the other by this library, so this is # the one case here that a wrong-but-self-consistent implementation cannot # pass. `ref-encode-matches' is the port's verdict in one word. check_eq $P/ref-documented-string '_p~iF~ps|U_ulLnnqC_mqNvxq`@' \ "$(pv ref_documented_string)" check_eq $P/ref-encode '_p~iF~ps|U_ulLnnqC_mqNvxq`@' \ "$(pv ref_encode)" check_eq $P/ref-encode-matches 'yes' \ "$(pv ref_encode_matches)" check_eq $P/ref-encode-len '27' \ "$(pv ref_encode_len)" check_eq $P/ref-decode \ '38.50000,-120.20000;40.70000,-120.95000;43.25200,-126.45300' \ "$(pv ref_decode)" check_eq $P/ref-decode-len '3' \ "$(pv ref_decode_len)" check_eq $P/ref-decode-maxerr '0.000000000' \ "$(pv ref_decode_maxerr)" check_eq $P/ref-decode-within-1e5 'yes' \ "$(pv ref_decode_within_1e5)" check_eq $P/ref-reencode-stable 'yes' \ "$(pv ref_reencode_stable)" # The decoder's output SHAPE: a list of genuine 2-tuples of floats in # (lat, lon) order. A caller unpacks these positionally, so the widths and the # order are as much of the contract as the values. check_eq $P/ref-decode-tuple-types 'tuple' \ "$(pv ref_decode_tuple_types)" check_eq $P/ref-decode-widths '2,2,2' \ "$(pv ref_decode_widths)" check_eq $P/ref-decode-value-types 'float' \ "$(pv ref_decode_value_types)" check_eq $P/ref-decode-first '38.50000,-120.20000' \ "$(pv ref_decode_first)" check_eq $P/ref-decode-last '43.25200,-126.45300' \ "$(pv ref_decode_last)" check_eq $P/ref-result-is-list 'yes' \ "$(pv ref_result_is_list)" # --------------------------------------------------------------- geojson --- # `geojson=True' reverses each tuple -- the caller speaks (lon, lat) and the # WIRE FORMAT IS UNCHANGED. So the same reference string must come back out of # the same reference points with their components swapped, which is a far # stronger statement than `the option reorders something'. check_eq $P/geojson-encode '_p~iF~ps|U_ulLnnqC_mqNvxq`@' \ "$(pv geojson_encode)" check_eq $P/geojson-encode-matches 'yes' \ "$(pv geojson_encode_matches)" check_eq $P/geojson-decode \ '-120.20000,38.50000;-120.95000,40.70000;-126.45300,43.25200' \ "$(pv geojson_decode)" check_eq $P/geojson-decode-is-swapped 'yes' \ "$(pv geojson_decode_is_swapped)" check_eq $P/geojson-decode-first '-120.20000,38.50000' \ "$(pv geojson_decode_first)" check_eq $P/geojson-default-is-false 'yes' \ "$(pv geojson_default_is_false)" # The option is tested with `is True', so a truthy non-True value is IGNORED. # `geojson=1' does nothing at all -- measured, not assumed, and exactly the # kind of thing a caller gets wrong once. check_eq $P/geojson-truthy-one-ignored 'yes' \ "$(pv geojson_truthy_one_ignored)" # ------------------------------------------------------------- precision --- # OpenStreetMap uses 6. The scale factor is 10**precision, so the same points # produce a different, longer string and round-trip at the finer grid. check_eq $P/p6-encode '_izlhA~rlgdF_{geC~ywl@_kwzCn`{nI' \ "$(pv p6_encode)" check_eq $P/p6-differs-from-p5 'yes' \ "$(pv p6_differs_from_p5)" check_eq $P/p6-decode \ '38.500000,-120.200000;40.700000,-120.950000;43.252000,-126.453000' \ "$(pv p6_decode)" check_eq $P/p6-decode-maxerr '0.000000000' \ "$(pv p6_decode_maxerr)" check_eq $P/p6-decode-within-1e6 'yes' \ "$(pv p6_decode_within_1e6)" check_eq $P/p6-encode-len '32' \ "$(pv p6_encode_len)" # PRECISION IS NOT CARRIED IN THE STRING. A precision-6 string decoded at the # default 5 comes back a factor of ten too large -- no exception, no marker, a # track ten times the size of the world. It is the most likely way to misuse # this library, so it is written down as the observed behaviour rather than # left to be discovered by whoever ships it. check_eq $P/p6-decoded-at-p5-first '385.00000,-1202.00000' \ "$(pv p6_decoded_at_p5_first)" check_eq $P/p6-at5-factor-of-ten 'yes' \ "$(pv p6_at5_factor_of_ten)" # ========================================================================== # THE TABLE -- where the zig-zag is actually exercised. # ========================================================================== # A track of increasing positive values never takes the negative branch of # `_write' and never sets the sign bit `_trans' reads back, so it cannot tell a # correct implementation from one that dropped the zig-zag entirely. Every row # below is chosen for the branch it reaches: negative latitude, negative # longitude, a crossing of zero in each component and in both at once, a delta # of one 1e-5 unit in each direction, the poles, a descending track, and an # alternating track that changes sign on every single delta. # # Each row is asserted three ways: the exact encoded string, the round trip # rendered at the format's own precision, and whether that round trip equals # the input. The STRING is the assertion that a round trip cannot make. check_eq $P/tbl-origin-enc '??' \ "$(pv tbl_origin_enc)" check_eq $P/tbl-origin-rt '0.00000,0.00000' \ "$(pv tbl_origin_rt)" check_eq $P/tbl-origin-exact 'yes' \ "$(pv tbl_origin_exact)" check_eq $P/tbl-origin-pair-enc '????' \ "$(pv tbl_origin_pair_enc)" check_eq $P/tbl-origin-pair-rt '0.00000,0.00000;0.00000,0.00000' \ "$(pv tbl_origin_pair_rt)" check_eq $P/tbl-origin-pair-exact 'yes' \ "$(pv tbl_origin_pair_exact)" check_eq $P/tbl-repeated-point-enc '_p~iF~ps|U????' \ "$(pv tbl_repeated_point_enc)" check_eq $P/tbl-repeated-point-rt \ '38.50000,-120.20000;38.50000,-120.20000;38.50000,-120.20000' \ "$(pv tbl_repeated_point_rt)" check_eq $P/tbl-repeated-point-exact 'yes' \ "$(pv tbl_repeated_point_exact)" check_eq $P/tbl-one-step-lat-enc '??_ibE?' \ "$(pv tbl_one_step_lat_enc)" check_eq $P/tbl-one-step-lat-rt '0.00000,0.00000;1.00000,0.00000' \ "$(pv tbl_one_step_lat_rt)" check_eq $P/tbl-one-step-lat-exact 'yes' \ "$(pv tbl_one_step_lat_exact)" check_eq $P/tbl-one-step-lng-enc '???_ibE' \ "$(pv tbl_one_step_lng_enc)" check_eq $P/tbl-one-step-lng-rt '0.00000,0.00000;0.00000,1.00000' \ "$(pv tbl_one_step_lng_rt)" check_eq $P/tbl-one-step-lng-exact 'yes' \ "$(pv tbl_one_step_lng_exact)" check_eq $P/tbl-negative-lat-enc '~o~iF_qs|U' \ "$(pv tbl_negative_lat_enc)" check_eq $P/tbl-negative-lat-rt '-38.50000,120.20000' \ "$(pv tbl_negative_lat_rt)" check_eq $P/tbl-negative-lat-exact 'yes' \ "$(pv tbl_negative_lat_exact)" check_eq $P/tbl-negative-both-enc '~o~iF~ps|U' \ "$(pv tbl_negative_both_enc)" check_eq $P/tbl-negative-both-rt '-38.50000,-120.20000' \ "$(pv tbl_negative_both_rt)" check_eq $P/tbl-negative-both-exact 'yes' \ "$(pv tbl_negative_both_exact)" check_eq $P/tbl-cross-zero-lat-enc '~hbE?_seK?' \ "$(pv tbl_cross_zero_lat_enc)" check_eq $P/tbl-cross-zero-lat-rt '-1.00000,0.00000;1.00000,0.00000' \ "$(pv tbl_cross_zero_lat_rt)" check_eq $P/tbl-cross-zero-lat-exact 'yes' \ "$(pv tbl_cross_zero_lat_exact)" check_eq $P/tbl-cross-zero-lng-enc '?~hbE?_seK' \ "$(pv tbl_cross_zero_lng_enc)" check_eq $P/tbl-cross-zero-lng-rt '0.00000,-1.00000;0.00000,1.00000' \ "$(pv tbl_cross_zero_lng_rt)" check_eq $P/tbl-cross-zero-lng-exact 'yes' \ "$(pv tbl_cross_zero_lng_exact)" check_eq $P/tbl-cross-zero-both-enc '~s`B~s`B_ibE_ibE' \ "$(pv tbl_cross_zero_both_enc)" check_eq $P/tbl-cross-zero-both-rt '-0.50000,-0.50000;0.50000,0.50000' \ "$(pv tbl_cross_zero_both_rt)" check_eq $P/tbl-cross-zero-both-exact 'yes' \ "$(pv tbl_cross_zero_both_exact)" check_eq $P/tbl-smallest-unit-enc '??AA' \ "$(pv tbl_smallest_unit_enc)" check_eq $P/tbl-smallest-unit-rt '0.00000,0.00000;0.00001,0.00001' \ "$(pv tbl_smallest_unit_rt)" check_eq $P/tbl-smallest-unit-exact 'yes' \ "$(pv tbl_smallest_unit_exact)" check_eq $P/tbl-smallest-neg-enc '??@@' \ "$(pv tbl_smallest_neg_enc)" check_eq $P/tbl-smallest-neg-rt '0.00000,0.00000;-0.00001,-0.00001' \ "$(pv tbl_smallest_neg_rt)" check_eq $P/tbl-smallest-neg-exact 'yes' \ "$(pv tbl_smallest_neg_exact)" check_eq $P/tbl-poles-enc '~bidP~fsia@_gsia@_ogtcA' \ "$(pv tbl_poles_enc)" check_eq $P/tbl-poles-rt '-90.00000,-180.00000;90.00000,180.00000' \ "$(pv tbl_poles_rt)" check_eq $P/tbl-poles-exact 'yes' \ "$(pv tbl_poles_exact)" check_eq $P/tbl-descending-enc '_t~fGfzxbW~lqNwxq`@~tlLonqC' \ "$(pv tbl_descending_enc)" check_eq $P/tbl-descending-rt \ '43.25200,-126.45300;40.70000,-120.95000;38.50000,-120.20000' \ "$(pv tbl_descending_rt)" check_eq $P/tbl-descending-exact 'yes' \ "$(pv tbl_descending_exact)" check_eq $P/tbl-zigzag-track-enc '_ibE_ibE~reK~reK_seK_seK~reK~reK' \ "$(pv tbl_zigzag_track_enc)" check_eq $P/tbl-zigzag-track-rt \ '1.00000,1.00000;-1.00000,-1.00000;1.00000,1.00000;-1.00000,-1.00000' \ "$(pv tbl_zigzag_track_rt)" check_eq $P/tbl-zigzag-track-exact 'yes' \ "$(pv tbl_zigzag_track_exact)" # A REPEATED POINT IS A ZERO DELTA, and a zero delta encodes as chr(0 + 63) -- # the single character `?'. Three identical points are therefore the first # point's two values followed by four `?'. Worth naming on its own, because a # delta encoder that emitted NOTHING for a zero delta would round-trip to a # shorter track and pass a length check that only counted characters. check_eq $P/repeated-tail '????' \ "$(pv repeated_tail)" check_eq $P/repeated-len '3' \ "$(pv repeated_len)" check_eq $P/descending-differs 'yes' \ "$(pv descending_differs)" check_eq $P/descending-rt-exact 'yes' \ "$(pv descending_rt_exact)" # A 500-POINT SYNTHETIC TRACK crossing zero in both components, so the sign bit # alternates the whole way down and an accumulator that lost a carry diverges # visibly rather than in the last place. 3873 characters out. check_eq $P/long-rt-exact 'yes' \ "$(pv long_rt_exact)" check_eq $P/long-enc-len '3873' \ "$(pv long_enc_len)" check_eq $P/long-rt-len '500' \ "$(pv long_rt_len)" # ========================================================================== # THE PRIVATE ARITHMETIC -- the two functions that carry the format's rules. # ========================================================================== # `_py2_round' rounds half AWAY FROM ZERO, through copysign/floor. Python 3's # builtin rounds half to EVEN: round(0.5) is 0 and round(2.5) is 2. Every other # implementation of this format uses the old rule, so a port that reached for # the builtin would be wrong on exactly the half-unit boundaries -- 1,2,3,4 # would read 0,2,2,4 -- and right everywhere else. check_eq $P/py2-round-half-up '1,2,3,4' \ "$(pv py2_round_half_up)" check_eq $P/py2-round-half-down '-1,-2,-3,-4' \ "$(pv py2_round_half_down)" check_eq $P/py2-round-differs-from-builtin 'yes' \ "$(pv py2_round_differs_from_builtin)" check_eq $P/py2-round-below-half '0,0' \ "$(pv py2_round_below_half)" check_eq $P/py2-round-returns-int 'yes' \ "$(pv py2_round_returns_int)" # `_trans' reads one value out of the string and hands back the next index. The # sign is the LOW bit of the assembled result and the value is the rest shifted # right -- `~(result >> 1)' for a negative, which is the shift-on-a-signed-value # this arm is at risk of getting wrong. The two reference values are asserted # as the scaled INTEGERS, before any division has hidden a wrong bit in a # float. `?' is the zero character and `@' is minus one. check_eq $P/trans-ref-lat '3850000,5' \ "$(pv trans_ref_lat)" check_eq $P/trans-ref-lng '-12020000,10' \ "$(pv trans_ref_lng)" check_eq $P/trans-zero '0,1' \ "$(pv trans_zero)" check_eq $P/trans-negative-one '-1,1' \ "$(pv trans_negative_one)" # `_pcitr' is the pairwise iterator the delta loop walks. A single point and an # empty list both yield NOTHING, which is why `encode' writes the first point # separately -- and why a one-point track is not an empty string. check_eq $P/pcitr-pairs '[(1, 2), (2, 3), (3, 4)]' \ "$(pv pcitr_pairs)" check_eq $P/pcitr-single '[]' \ "$(pv pcitr_single)" check_eq $P/pcitr-empty '[]' \ "$(pv pcitr_empty)" # ========================================================================== # THE EDGE PATHS -- measured, and several of them are not refusals. # ========================================================================== # polyline 2.0.2 validates nothing: no range check, no alphabet check, no # length check. So `NONE:' below is a RESULT, not a gap in the suite -- # it records that the library returned a value where a caller would expect an # exception. A future upstream adding validation changes these cases, and this # is what would report it. check_eq $P/decode-empty '[]' \ "$(pv decode_empty)" check_eq $P/decode-truncated 'IndexError' \ "$(pv decode_truncated)" check_eq $P/decode-one-char 'IndexError' \ "$(pv decode_one_char)" # THERE IS NO ALPHABET CHECK, and this pair is the proof rather than the case # above it. ONE byte below the alphabet raises IndexError -- but only because a # lone value has no longitude beside it, which is a length failure wearing an # alphabet failure's clothes. TWO of them decode CLEANLY to a coordinate: chr(32) # is 31 below the +63 offset, `-31 & 0x1f' is 1, the low bit reads as the sign, # and the caller gets (-1e-05, -1e-05) back with nothing at all to say the input # was never a polyline. check_eq $P/decode-below-alphabet 'IndexError' \ "$(pv decode_below_alphabet)" check_eq $P/decode-two-below-alphabet 'NONE:[(-1e-05, -1e-05)]' \ "$(pv decode_two_below_alphabet)" check_eq $P/decode-high-char 'IndexError' \ "$(pv decode_high_char)" check_eq $P/decode-non-string 'TypeError' \ "$(pv decode_non_string)" check_eq $P/decode-list-arg 'NONE:[(0.0, 0.0)]' \ "$(pv decode_list_arg)" check_eq $P/decode-negative-precision \ 'NONE:[(38500000.0, -120200000.0), (40700000.0, -120950000.0), (43252000.0, -126453000.0)]' \ "$(pv decode_negative_precision)" # `encode' indexes `coordinates[0]' before it loops, so an empty list is an # IndexError from the raw index rather than a diagnostic of its own. check_eq $P/encode-empty 'IndexError' \ "$(pv encode_empty)" # NO RANGE CHECK ANYWHERE. A latitude of 91 and a longitude of 181 encode # happily, and 1e9 encodes to a string that decodes back to 1e9. The format has # no opinion about the Earth. check_eq $P/encode-lat-out-of-range "NONE:'_mljP?'" \ "$(pv encode_lat_out_of_range)" check_eq $P/encode-lng-out-of-range "NONE:'?_qvoa@'" \ "$(pv encode_lng_out_of_range)" check_eq $P/encode-huge-value "NONE:'___hnow{tD___hnow{tD'" \ "$(pv encode_huge_value)" check_eq $P/encode-short-tuple 'IndexError' \ "$(pv encode_short_tuple)" check_eq $P/encode-long-tuple "NONE:'_ibE_seK'" \ "$(pv encode_long_tuple)" check_eq $P/encode-string-coord 'TypeError' \ "$(pv encode_string_coord)" check_eq $P/encode-none-arg 'TypeError' \ "$(pv encode_none_arg)" check_eq $P/encode-precision-zero "NONE:'mAnFC@CH'" \ "$(pv encode_precision_zero)" check_eq $P/encode-int-coords "NONE:'??_ibE?'" \ "$(pv encode_int_coords)" check_eq $P/encode-nan 'ValueError' \ "$(pv encode_nan)" check_eq $P/encode-inf 'OverflowError' \ "$(pv encode_inf)" # A generator is refused, because `coordinates[0]' subscripts. Worth a case: # passing a generator to something that takes a list is an ordinary mistake, # and here it fails loudly rather than encoding a prefix. check_eq $P/encode-generator 'TypeError' \ "$(pv encode_generator)" # A 3-TUPLE'S THIRD COMPONENT IS DROPPED IN SILENCE. `_write' indexes [0] and # [1] and never asks how long the tuple is, so an altitude a caller carried # along disappears and the encoding is identical to the 2-tuple's. check_eq $P/encode-long-tuple-drops-third 'yes' \ "$(pv encode_long_tuple_drops_third)" rm -rf "$W" exit 0