. "$BST_LIB" # BST-TIMEOUT: 1500 # ONE python3 start, and that is very nearly the whole cost. # # Every assertion below either reads the filesystem/dpkg # # with shell builtins or greps the single probe run's # # output; the probe itself builds ~30 small objects, parses # # a 543-byte document and serialises it back, and calls # # generate_random four times. That is nothing beside the # # interpreter load on this arm. It sits above the 1200 that # # 118 sibling python3 suites use because this probe emits # # 166 keys rather than the usual few dozen, not because any # # single case is slow. # # 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-geojson 3.1.0 (meta-openembedded meta-python # recipes-devtools/python/python3-geojson_3.1.0.bb, plus this layer's # python3-geojson_%.bbappend) on the wasm32-hwjs glibc image -- GeoJSON # (RFC 7946) AS PYTHON OBJECTS. The geometry, feature and collection types, and # their JSON encoding. # # ============================================================================ # THE REFERENCE ANSWERS DO NOT COME FROM THE LIBRARY. # ============================================================================ # # RFC 7946 fixes both the STRUCTURE and the TEXT, so every serialisation case # below compares against a literal written out from the RFC's own examples # (§1.5, §3.1.x, §3.3), and the FeatureCollection case reads a committed # document and asserts the dump equals the input byte for byte. Nothing here is # "whatever the library produced, recorded once" -- a case that pinned the # library's own output would pass a release that changed the encoding and would # be evidence of nothing. # # ============================================================================ # WHY THIS PORT IS POSSIBLE AT ALL. # ============================================================================ # # 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 not be imported here -- not "would be # slow", could not be imported. geojson is eight .py files and ships no # extension of its own, which is the entire reason it ports. `pkg-no-so' is not # boilerplate but a REGRESSION TRIPWIRE: the day a release grows a compiled # speedup, this case reds at install time rather than at `import geojson' on # somebody else's ticket three rounds later. # # ============================================================================ # THE DEPENDENCY THIS PORT ADDS, AND WHY IT IS ASSERTED BY NAME. # ============================================================================ # # meta-python's recipe declares `RDEPENDS:${PN} += "python3-simplejson # python3-math"'. `geojson/geometry.py' opens with `from decimal import Decimal' # and `from numbers import Number, Real', both at MODULE scope, and # `geojson/__init__.py' imports geometry -- so both run on `import geojson'. # poky's python3-manifest.json puts decimal.py, _pydecimal.py and numbers.py in # the `numbers' split, which NEITHER declared dependency reaches transitively. # The built .deb read `Depends: python3-core, python3-math, python3-simplejson' # and nothing else, and the bbappend adds python3-numbers. # # `pkg-depends-numbers' is therefore the case that says the fix is in the image # rather than only in the layer. It is asserted ONE NAME PER CASE against the # extracted Depends line rather than as one whole-line string: the recipe's # RDEPENDS order is not dpkg's to preserve and the separator spacing is dpkg's # own, so an exact-string case would be pinning a FORMAT. Split, a red also # names WHICH dependency went missing. # # `import' below would still pass on an image that had python3-numbers for a # neighbour's sake with the declaration missing -- which is exactly the accident # the bbappend exists to stop the port resting on. The Depends case is the one # that cannot pass by accident. # # ============================================================================ # THREE THINGS UPSTREAM DOES THAT ARE ASSERTED RATHER THAN CORRECTED. # ============================================================================ # # 1. AN UNKNOWN `type' DOES NOT RAISE. `geojson.loads' on {"type": # "Hyperboloid"} returns a PLAIN dict -- not a GeoJSON object, and no # exception. Only the documented `strict' path raises. The ticket's premise # that an unknown type raises is wrong for this library, and the suite pins # the real behaviour (`unknown-type-no-raise', `unknown-type-class') so that # a release which STARTED raising is visible. # # 2. `__all__' IS A LIST OF OBJECTS, NOT NAMES. geojson/__init__.py line 10 # builds it from the imported callables and classes themselves, so # `from geojson import *' RAISES TypeError -- the star-import machinery # requires str. Measured, not inferred. It is upstream's shape and what this # image ships; a release that fixed it would change the public surface, and # `star-import-typeerror' is what would say so. # # 3. `__repr__' SORTS KEYS AND `dumps' DOES NOT. repr(obj) is # `dumps(obj, sort_keys=True)', so printing an object spells the same # document with `coordinates' before `type', while dumps() emits insertion # order. Both are asserted, because a suite that compared one against the # other would be red for a reason that is not a defect. # # ============================================================================ # WHAT IS *NOT* HERE: UPSTREAM'S OWN TEST SUITE, BY TWO INDEPENDENT ROUTES. # ============================================================================ # # The recipe carries `inherit ptest' and a do_install_ptest that copies # ${S}/tests. Neither route reaches this image: ptest.bbclass creates ${PN}-ptest # only when `ptest' is in DISTRO_FEATURES, and this distro sets # `DISTRO_FEATURES = "ipv4 x11 wayland"' (BRINTOS-2246); and setup.py declares # `packages=["geojson"]', so tests/ -- which sits at the sdist root beside the # package -- is not part of the installed distribution either. `no-ptest-package', # `no-test-dir-in-site' and `pkg-no-tests-path' assert both absences rather than # leaving them to be discovered. The hand-written cases stand in their place. # # BOTH ARE ASSERTED OVER WHAT THIS PACKAGE OWNS, NEVER OVER site-packages AS A # WHOLE, and that distinction is a measurement rather than a precaution. This # suite first asserted `absent' for a top-level `$SITE/tests' too, and the batch # verify measured it `present': the directory is there, and it is # python3-reedsolo's -- `usr/lib/python3.12/site-packages/tests/' with # `test_reedsolo.py', `test_creedsolo.py' and `perf.py' in it, named in # python3-reedsolo's pkgdata FILES and in no other package's. python3-geojson's # own file list carries nothing matching `test' at all. So the old case was a # statement about EVERY python package in the image, which one unrelated port # falsifies and which this port can neither cause nor fix; the two cases below # say the thing that is geojson's -- its own package directory, and its own dpkg # file list. P=python3-geojson # THE CANARY, which asserts nothing about python3-geojson. # # 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' guards 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 MOD=$SITE/geojson DI=$SITE/geojson-3.1.0.dist-info PKGLIST=/var/lib/dpkg/info/python3-geojson.list PROBE=$BST_DATA/python3-geojson-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-geojson.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------- the payload --- # Where the package has to land. `import geojson' further down is the functional # statement; these say WHICH copy answered, so a module that imported out of some # other path is distinguishable from one that imported out of what this image # installed. check $P/site-packages-dir test -d "$SITE" check $P/module-dir test -d "$MOD" check $P/module-init test -f "$MOD/__init__.py" # IT IS A PACKAGE, NOT A MODULE -- setup.py declares `packages=["geojson"]', so a # bare `geojson.py' here would mean the upstream shape changed under a recipe # that still says otherwise, invisible to every behavioural case below. check_eq $P/no-flat-module "absent" \ "$(if [ -f "$SITE/geojson.py" ]; then echo present; else echo absent; fi)" # The eight modules the package is made of, each asserted by name: a release that # merged or dropped one still imports and still serves most cases below. for _m in base codec factory feature geometry mapping utils _version; do check $P/module-$_m test -f "$MOD/$_m.py" done # UPSTREAM'S tests/ IS NOT IN THE INSTALLED DISTRIBUTION. See the header -- this # is the second of the two closed routes, asserted from the filesystem side, and # scoped to the package's OWN directory: a top-level `$SITE/tests' exists on this # image and belongs to python3-reedsolo, so testing for it here would report # another port's packaging as this one's. check_eq $P/no-test-dir-in-site "absent" \ "$(if [ -d "$MOD/tests" ]; then echo present; else echo absent; fi)" # NO COMPILED EXTENSION ANYWHERE THE PACKAGE PUT ANYTHING. 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. # # 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" # THE OWNERSHIP HALF OF `no-test-dir-in-site'. The case above reads the # filesystem, which cannot tell a path geojson installed from a path another # package installed; this one reads geojson's own dpkg file list, so it stays # true whatever else the image grows. It is the direct statement of "tests/ is # not part of the installed distribution". check_eq $P/pkg-no-tests-path "" \ "$(grep -E '(^|/)tests(/|$)' "$W/pkgf" 2>/dev/null)" check_eq $P/pkg-no-so "" \ "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" # EVERY REGULAR FILE THE PACKAGE SHIPS IS PYTHON OR ITS METADATA. The case above # names the failure that is expected; this one catches the shape nobody predicted # -- a binary, a .pyd, a stray README -- without having to enumerate what it # might be. # # A dpkg .list names DIRECTORIES too, and without a trailing slash, so a # name-only filter would report every directory in the package as an unexpected # shape; each entry is therefore tested for being a regular file first. _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" # NOTHING LANDS IN ${bindir}. geojson declares no console_scripts and no # `scripts=', so a file in /usr/bin would be a packaging change this suite should # see. (Contrast python3-jmespath beside it, which does ship one.) check_eq $P/pkg-no-bindir "" \ "$(grep -E '^(\./)?usr/bin/.' "$W/pkgf" 2>/dev/null)" # ----------------------------------------------------------- the dist-info --- # The installed metadata, the second and independent statement of the version -- # the directory name carries it a third time in $DI itself. A package whose # module landed and whose metadata did not would import fine and serve every # behavioural case while reporting no installed version at all. check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-version "Version: 3.1.0" cat "$DI/METADATA" # The licence the recipe's LIC_FILES_CHKSUM pins, as installed. check $P/dist-info-license test -f "$DI/LICENSE.rst" # --------------------------------------------------------------- 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-geojson 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, and # the anchor is exact -- an unanchored match would also take the -dev or -dbg # stanza of the same family. sed -n '/^Package: python3-geojson$/,/^$/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: 3.1.0" cat "$W/st" # THE ptest SPLIT IS NEVER CREATED on this distro. First of the two closed routes # from the header, asserted from dpkg's side. check_eq $P/no-ptest-package "absent" \ "$(if grep -q '^Package: python3-geojson-ptest$' /var/lib/dpkg/status 2>/dev/null; \ then echo present; else echo absent; fi)" # THE RUNTIME DEPENDENCIES, as dpkg recorded them -- one case per name. See the # header for why python3-numbers is the one this port adds and why it is asserted # separately from `import'. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" check_out $P/pkg-depends-core "python3-core" cat "$W/dep" check_out $P/pkg-depends-math "python3-math" cat "$W/dep" check_out $P/pkg-depends-simplejson "python3-simplejson" cat "$W/dep" check_out $P/pkg-depends-numbers "python3-numbers" cat "$W/dep" # ---------------------------------------------------------- the interpreter --- # THE IMPORT, ON ITS OWN, BEFORE THE PROBE. If geojson 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 geojson' # ...and the two stdlib splits it rests on, separately and ahead of it in the # failure order that matters. geojson imports both at module scope, so an image # missing either presents AS a broken geojson. These are what tell them apart. check $P/decimal-import python3 -c 'from decimal import Decimal' check $P/numbers-import python3 -c 'from numbers import Number, Real' check $P/simplejson-import python3 -c 'import simplejson' # ---------------------------------------------------------------- the fixture -- # The committed documents the probe drives, pinned by size, so every case that # reads them is known to be about the documents this file describes rather than # about an edited fixture. check $P/fixture-fc test -f "$BST_DATA/python3-geojson-fc.geojson" check $P/fixture-truncated test -f "$BST_DATA/python3-geojson-truncated.geojson" check $P/fixture-unknown test -f "$BST_DATA/python3-geojson-unknown-type.geojson" # ---------------------------------------------------------------- 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. bst_probe $P/probe-run "$W/probe" 1200 python3 "$PROBE" 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 `version' distinguishable # from `version_info', and `point_valid' from `point4d_valid'. 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 broken library, 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 in 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 # THE TRANSCRIPT IS WHOLE. The probe prints `probe_done=yes' last and its own # key count second-to-last, so a truncated run is ONE failing case here rather # than a wall of empty-string mismatches below. check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-key-count "166" "$(pv emitted_count)" # 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 behaviour # cases below statements about the SAME geojson. check_eq $P/module-answered "$MOD/__init__.py" "$(pv module_file)" check_eq $P/is-package "yes" "$(pv is_package)" # THE VERSION THE RECIPE PINS, from the module's own `__version__' and from # `__version_info__' beside it. The dist-info carries it a third time, asserted # from the filesystem above. check_eq $P/version "3.1.0" "$(pv version)" check_eq $P/version-info "3.1.0" "$(pv version_info)" check_eq $P/version-info-type "tuple" "$(pv version_info_type)" # ------------------------------------------------------------- the backend --- # WHICH JSON CODEC ANSWERED, BY NAME. codec.py and mapping.py both open with # `try: import simplejson as json / except ImportError: import json', so the # stdlib would serve -- but NOT identically: simplejson rejects NaN and Infinity # on the way IN, where stdlib json accepts both by default. An image that lost # python3-simplejson would therefore keep working while silently changing the # library's error behaviour. These cases are what make that visible. check_eq $P/codec-backend "simplejson" "$(pv codec_backend)" check_eq $P/codec-is-simplejson "yes" "$(pv codec_is_simplejson)" check_eq $P/mapping-backend "simplejson" "$(pv mapping_backend)" check_eq $P/simplejson-version "3.19.2" "$(pv simplejson_version)" # ------------------------------------------------------------ the geometries -- # Each type constructed, then dumped and compared against text written out from # RFC 7946. `type' and `coordinates' are asserted separately from the dump so # that a red says whether the OBJECT or its ENCODING moved. check_eq $P/point-type "Point" "$(pv point_type)" check_eq $P/point-coords "[100.0, 0.0]" "$(pv point_coords)" check_eq $P/point-dumps '{"type": "Point", "coordinates": [100.0, 0.0]}' "$(pv point_dumps)" check_eq $P/point-valid "yes" "$(pv point_valid)" # A geometry is a dict AND a GeoJSON: the library's whole model is a dict # subclass, and code that treats one as either must keep working. check_eq $P/point-is-dict "yes" "$(pv point_is_dict)" check_eq $P/point-is-geojson "yes" "$(pv point_is_geojson)" # `obj.type' and `obj["type"]' are the same thing -- attribute access is proxied # to the mapping. A release that dropped __getattr__ would keep every dumps case # green and break every caller. check_eq $P/point-attr-type "Point" "$(pv point_attr_type)" check_eq $P/linestring-type "LineString" "$(pv linestring_type)" check_eq $P/linestring-coords "[[100.0, 0.0], [101.0, 1.0]]" "$(pv linestring_coords)" check_eq $P/linestring-dumps '{"type": "LineString", "coordinates": [[100.0, 0.0], [101.0, 1.0]]}' "$(pv linestring_dumps)" check_eq $P/linestring-valid "yes" "$(pv linestring_valid)" check_eq $P/polygon-type "Polygon" "$(pv polygon_type)" check_eq $P/polygon-coords "[[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]]]" "$(pv polygon_coords)" check_eq $P/polygon-dumps '{"type": "Polygon", "coordinates": [[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]]]}' "$(pv polygon_dumps)" check_eq $P/polygon-valid "yes" "$(pv polygon_valid)" check_eq $P/multipoint-type "MultiPoint" "$(pv multipoint_type)" check_eq $P/multipoint-coords "[[100.0, 0.0], [101.0, 1.0]]" "$(pv multipoint_coords)" check_eq $P/multipoint-dumps '{"type": "MultiPoint", "coordinates": [[100.0, 0.0], [101.0, 1.0]]}' "$(pv multipoint_dumps)" check_eq $P/multipoint-valid "yes" "$(pv multipoint_valid)" check_eq $P/multilinestring-type "MultiLineString" "$(pv multilinestring_type)" check_eq $P/multilinestring-coords "[[[100.0, 0.0], [101.0, 1.0]], [[102.0, 2.0], [103.0, 3.0]]]" "$(pv multilinestring_coords)" check_eq $P/multilinestring-dumps '{"type": "MultiLineString", "coordinates": [[[100.0, 0.0], [101.0, 1.0]], [[102.0, 2.0], [103.0, 3.0]]]}' "$(pv multilinestring_dumps)" check_eq $P/multilinestring-valid "yes" "$(pv multilinestring_valid)" check_eq $P/multipolygon-type "MultiPolygon" "$(pv multipolygon_type)" check_eq $P/multipolygon-dumps '{"type": "MultiPolygon", "coordinates": [[[[102.0, 2.0], [103.0, 2.0], [103.0, 3.0], [102.0, 3.0], [102.0, 2.0]]], [[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]]]]}' "$(pv multipolygon_dumps)" check_eq $P/multipolygon-valid "yes" "$(pv multipolygon_valid)" # GeometryCollection IS THE ONE THAT IS SHAPED DIFFERENTLY, and the ticket's # blanket "assert type and coordinates" does not apply to it. RFC 7946 §3.1.8 # gives it a `geometries' member and NO `coordinates', so the two cases below # assert the presence of one and the ABSENCE of the other -- a collection that # grew a `coordinates' key would still dump and still validate. check_eq $P/geometrycollection-type "GeometryCollection" "$(pv geometrycollection_type)" check_eq $P/gc-has-geometries "yes" "$(pv gc_has_geometries)" check_eq $P/gc-has-no-coordinates "no" "$(pv gc_has_coordinates)" check_eq $P/gc-len "2" "$(pv geometrycollection_len)" check_eq $P/gc-member-type "Point" "$(pv gc_index_0_type)" check_eq $P/gc-key-type "GeometryCollection" "$(pv gc_key_type)" check_eq $P/gc-dumps '{"type": "GeometryCollection", "geometries": [{"type": "Point", "coordinates": [100.0, 0.0]}, {"type": "LineString", "coordinates": [[101.0, 0.0], [102.0, 1.0]]}]}' "$(pv geometrycollection_dumps)" check_eq $P/gc-valid "yes" "$(pv geometrycollection_valid)" # ------------------------------------------------------ the FeatureCollection -- # THE COMMITTED DOCUMENT, loaded and asserted feature by feature, then dumped # back and asserted equal to the input. This is the case the ticket is really # about: the reference is a file in this repository, not a value the library # produced. check_eq $P/fc-file-bytes "543" "$(pv fc_file_bytes)" check_eq $P/fc-type "FeatureCollection" "$(pv fc_type)" check_eq $P/fc-class "FeatureCollection" "$(pv fc_class)" check_eq $P/fc-len "3" "$(pv fc_len)" check_eq $P/fc-valid "yes" "$(pv fc_valid)" # THE ROUND TRIP IS BYTE-FOR-BYTE, and it can be here because the fixture is # written in the spelling geojson emits: insertion order, `: ' and `, ' # separators, floats as floats. That is a property of the FIXTURE, chosen so this # case can be exact; it is not a claim that geojson normalises arbitrary input. check_eq $P/fc-roundtrip "yes" "$(pv fc_roundtrip)" check_eq $P/fc-dumps-len "543" "$(pv fc_dumps_len)" # `load' (file) and `loads' (string) agree, and `dump' (file) and `dumps' # (string) agree. Four entry points, two equalities -- a release that fixed one # pair and not the other is visible. check_eq $P/fc-load-equals-loads "yes" "$(pv fc_loads_equals_load)" check_eq $P/fc-dump-equals-dumps "yes" "$(pv fc_dump_matches_dumps)" # Feature 0 -- an integer id, a Point, two properties. check_eq $P/fc-f0-class "Feature" "$(pv fc_f0_class)" check_eq $P/fc-f0-type "Feature" "$(pv fc_f0_type)" check_eq $P/fc-f0-id "1" "$(pv fc_f0_id)" check_eq $P/fc-f0-geom-type "Point" "$(pv fc_f0_geom_type)" # The geometry came back as a Point INSTANCE, not as a bare dict -- that is the # decoding this library exists for, and a plain-dict result would still serve # every ["type"] case above. check_eq $P/fc-f0-geom-class "Point" "$(pv fc_f0_geom_class)" check_eq $P/fc-f0-coords "[13.404954, 52.520008]" "$(pv fc_f0_coords)" check_eq $P/fc-f0-props '{"capital": true, "name": "Berlin"}' "$(pv fc_f0_props)" check_eq $P/fc-f0-valid "yes" "$(pv fc_f0_valid)" # Feature 1 -- a STRING id, a LineString. RFC 7946 §3.2 allows either a string or # a number, and the two are asserted on different features deliberately. check_eq $P/fc-f1-id "'route-1'" "$(pv fc_f1_id)" check_eq $P/fc-f1-geom-type "LineString" "$(pv fc_f1_geom_type)" check_eq $P/fc-f1-geom-class "LineString" "$(pv fc_f1_geom_class)" check_eq $P/fc-f1-coords "[[13.404954, 52.520008], [2.352222, 48.856614]]" "$(pv fc_f1_coords)" check_eq $P/fc-f1-props '{"km": 878, "name": "Berlin-Paris"}' "$(pv fc_f1_props)" check_eq $P/fc-f1-valid "yes" "$(pv fc_f1_valid)" # Feature 2 -- a Polygon and EMPTY properties, which RFC 7946 §3.2 requires to be # an object rather than absent. check_eq $P/fc-f2-id "3" "$(pv fc_f2_id)" check_eq $P/fc-f2-geom-type "Polygon" "$(pv fc_f2_geom_type)" check_eq $P/fc-f2-geom-class "Polygon" "$(pv fc_f2_geom_class)" check_eq $P/fc-f2-coords "[[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]]]" "$(pv fc_f2_coords)" check_eq $P/fc-f2-props "{}" "$(pv fc_f2_props)" check_eq $P/fc-f2-valid "yes" "$(pv fc_f2_valid)" # Indexing a FeatureCollection reaches its features. check_eq $P/fc-index "1" "$(pv fc_index_0_id)" # --------------------------------------------------------------- validation --- # `is_valid' TRUE on the good cases above and FALSE on the documented invalid # ones -- and the ERROR TEXT asserted beside each, because a validator that # rejected everything would pass a bare False case. check_eq $P/invalid-unclosed-ring "no" "$(pv unclosed_valid)" check_eq $P/invalid-unclosed-ring-why "Each linear ring must end where it started" "$(pv unclosed_errors)" check_eq $P/invalid-short-position "no" "$(pv shortpos_valid)" check_eq $P/invalid-short-position-why "a position must have exactly 2 or 3 values" "$(pv shortpos_errors)" check_eq $P/invalid-short-ring "no" "$(pv shortring_valid)" check_eq $P/invalid-short-ring-why "Each linear ring must contain at least 4 positions" "$(pv shortring_errors)" check_eq $P/invalid-4d-position "no" "$(pv point4d_valid)" check_eq $P/invalid-4d-position-why "a position must have exactly 2 or 3 values" "$(pv point4d_errors)" check_eq $P/invalid-single-point-line "no" "$(pv line1_valid)" check_eq $P/invalid-single-point-line-why 'the "coordinates" member must be an array of two or more positions' "$(pv line1_errors)" # THE VALIDATOR HAS TEETH: the minimal closed ring and the 3D position -- both # legal under RFC 7946 §3.1.6 and §3.1.1 -- are accepted. Without these, every # case above would pass on a validator that returned False unconditionally. check_eq $P/valid-minimal-closed-ring "yes" "$(pv closed4_valid)" check_eq $P/valid-3d-position "yes" "$(pv point3d_valid)" # The constructor's `validate=True' keyword RAISES rather than reporting, and # raises ValueError specifically. check_eq $P/validate-kw-raises "ValueError" "$(pv validate_kw_raises)" check_eq $P/validate-kw-accepts-valid "yes" "$(pv validate_kw_ok)" # ------------------------------------------------------- __geo_interface__ ---- # THE PROTOCOL, which is how geojson interoperates with shapely and the rest of # the ecosystem: any object exposing __geo_interface__ can be encoded, and the # result decodes back to an equal object. check_eq $P/geo-interface-dumps '{"type": "Point", "coordinates": [100.0, 0.0]}' "$(pv geo_iface_dumps)" check_eq $P/geo-interface-roundtrip-class "Point" "$(pv geo_iface_roundtrip_class)" check_eq $P/geo-interface-roundtrip-equal "yes" "$(pv geo_iface_roundtrip_equal)" # A geojson object IS its own interface -- it returns self rather than a copy. check_eq $P/geo-interface-is-self "yes" "$(pv point_geo_iface_is_self)" # ...and the base class exposes None, which is what makes the protocol optional. check_eq $P/geo-interface-base-none "None" "$(pv base_geo_iface)" # An arbitrary object wrapped in a Feature encodes through the same path. check_eq $P/geo-interface-feature '{"type": "Feature", "geometry": {"type": "Point", "coordinates": [100.0, 0.0]}, "properties": {}}' "$(pv geo_iface_feature)" # ---------------------------------------------------------------- precision --- # THE DOCUMENTED DEFAULT IS 6 DECIMAL PLACES, applied at CONSTRUCTION -- the # coordinates are already rounded before anything is serialised, which is why # `precision-default-coords' reads the object and not just the text. check_eq $P/precision-default "6" "$(pv default_precision)" check_eq $P/precision-default-coords "[1.234568, 2.345679]" "$(pv precision_default_coords)" check_eq $P/precision-default-dumps '{"type": "Point", "coordinates": [1.234568, 2.345679]}' "$(pv precision_default_dumps)" # The explicit `precision' argument, at three settings including 0. check_eq $P/precision-2 '{"type": "Point", "coordinates": [1.23, 2.35]}' "$(pv precision_2)" check_eq $P/precision-0 '{"type": "Point", "coordinates": [1.0, 2.0]}' "$(pv precision_0)" check_eq $P/precision-10 '{"type": "Point", "coordinates": [1.23456789, 2.34567891]}' "$(pv precision_10)" # ROUNDING IS BANKER'S, NOT HALF-UP -- it goes through Python's round(), so 0.125 # becomes 0.12 and 0.135 becomes 0.14. Surprising enough to pin: a reimplementation # that used half-up would differ only on exact halves and only in the last place. check_eq $P/precision-half-even "[0.12, 0.14]" "$(pv precision_halfeven)" # INTEGER COORDINATES STAY INTEGERS. Rounding does not coerce to float, so the # document keeps the JSON number type it was given. check_eq $P/precision-keeps-int '{"type": "Point", "coordinates": [100, 0]}' "$(pv int_coords_dumps)" # Precision reaches NESTED coordinate arrays, not just the top level. check_eq $P/precision-nested '{"type": "Polygon", "coordinates": [[[0.123, 0.0], [1.0, 0.0], [1.0, 1.0], [0.123, 0.0]]]}' "$(pv precision_nested)" # ------------------------------------------------------------- the utilities -- # `coords' walks any object and yields every position; `map_coords' rebuilds one # with a function applied. Both are part of the documented surface. check_eq $P/utils-coords-point "[(100.0, 0.0)]" "$(pv coords_point)" check_eq $P/utils-coords-fc "[(13.404954, 52.520008), (13.404954, 52.520008), (2.352222, 48.856614), (0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (0.0, 0.0)]" "$(pv coords_fc)" check_eq $P/utils-map-coords "{'type': 'Point', 'coordinates': (200.0, 0.0)}" "$(pv map_coords_point)" # `generate_random' IS THE ONLY ARM THAT NEEDS python3-math -- utils.py imports # `random' and `math' inside it and nowhere else. These cases are what make the # recipe's python3-math declaration a measured dependency rather than a leftover. # The output is random by construction, so only its SHAPE is asserted; a # coordinate literal here would be pinning the PRNG. check_eq $P/generate-random-point-type "Point" "$(pv gen_random_point_type)" check_eq $P/generate-random-point-valid "yes" "$(pv gen_random_point_valid)" check_eq $P/generate-random-point-ncoords "2" "$(pv gen_random_point_ncoords)" check_eq $P/generate-random-line-type "LineString" "$(pv gen_random_line_type)" check_eq $P/generate-random-line-valid "yes" "$(pv gen_random_line_valid)" check_eq $P/generate-random-line-nvert "5" "$(pv gen_random_line_nvert)" check_eq $P/generate-random-poly-type "Polygon" "$(pv gen_random_poly_type)" check_eq $P/generate-random-poly-valid "yes" "$(pv gen_random_poly_valid)" # The bounding box is respected -- the one property of the output that is not # random and is worth asserting. check_eq $P/generate-random-bbox "yes" "$(pv gen_random_bbox_respected)" check_eq $P/generate-random-unknown-raises "ValueError" "$(pv gen_random_unknown_exc)" # ------------------------------------------------------------- the error paths -- # A TRUNCATED DOCUMENT RAISES, and the exception is a ValueError subclass -- so # `except ValueError' catches it whichever codec answered. Both halves asserted: # the class name pins simplejson's spelling, the subclass case pins the contract # that survives a fallback to stdlib json. check_eq $P/error-truncated "JSONDecodeError" "$(pv truncated_exc)" check_eq $P/error-truncated-is-valueerror "yes" "$(pv truncated_is_valueerror)" # AN UNKNOWN `type' DOES NOT RAISE -- see the header. This is the ticket's one # wrong premise, and these four cases pin what actually happens: a plain dict # comes back, it is NOT a GeoJSON instance, and the unknown type survives in it. check_eq $P/unknown-type-no-raise "no-raise" "$(pv unknown_type_exc)" check_eq $P/unknown-type-class "dict" "$(pv unknown_type_class)" check_eq $P/unknown-type-member "Hyperboloid" "$(pv unknown_type_member)" check_eq $P/unknown-type-not-geojson "no" "$(pv unknown_type_is_geojson)" # ...and the documented `strict' path is the one that DOES raise. Without this, # the four cases above would read as "the library cannot detect it at all". check_eq $P/unknown-type-strict-raises "ValueError" "$(pv unknown_type_strict_exc)" # A GEOMETRY BUILT FROM THE WRONG PYTHON TYPE. Which exception depends on WHAT # the wrong type is, so both are asserted rather than one being taken for the # rule: a str is iterable and reaches the coordinate walker, which rejects its # characters as non-JSON numbers (ValueError); an int is not iterable at all and # fails earlier (TypeError). check_eq $P/error-polygon-from-str "ValueError: 'n' is not a JSON compliant number" "$(pv polygon_from_str_exc)" check_eq $P/error-polygon-from-int "TypeError" "$(pv polygon_from_int_exc)" check_eq $P/error-point-from-str "ValueError: 'x' is not a JSON compliant number" "$(pv point_from_str_exc)" # NaN AND Infinity ARE REJECTED IN BOTH DIRECTIONS. RFC 7946 has no spelling for # them, and this is the behaviour that would change silently if the image lost # python3-simplejson: stdlib json ACCEPTS both by default. The `codec-backend' # case above and these three are the same statement made two ways. check_eq $P/reject-nan-dumps "ValueError" "$(pv nan_dumps_exc)" check_eq $P/reject-inf-dumps "ValueError" "$(pv inf_dumps_exc)" check_eq $P/reject-nan-loads "ValueError: Number 'NaN' is not JSON compliant" "$(pv nan_loads_exc)" # --------------------------------------------------------------- the Feature -- # The Feature type's own documented edges, each of which a naive implementation # gets wrong in a different way. # # A Feature MAY have a null geometry (RFC 7946 §3.2) and is still valid. check_eq $P/feature-null-geometry '{"type": "Feature", "id": 7, "geometry": null, "properties": {}}' "$(pv feature_nogeom_dumps)" check_eq $P/feature-null-geometry-valid "yes" "$(pv feature_nogeom_valid)" # `id' is OMITTED when absent rather than emitted as null. check_eq $P/feature-no-id '{"type": "Feature", "geometry": {"type": "Point", "coordinates": [100.0, 0.0]}, "properties": {}}' "$(pv feature_no_id_dumps)" # ...but an id of 0 is KEPT: the constructor tests `is not None', not truthiness. # This is the falsy-id bug that every reimplementation has once. check_eq $P/feature-id-zero '{"type": "Feature", "id": 0, "geometry": {"type": "Point", "coordinates": [100.0, 0.0]}, "properties": {}}' "$(pv feature_id_zero)" # `properties' defaults to an empty object, not to null. check_eq $P/feature-properties-default "{}" "$(pv feature_props_default)" # A Feature reports its GEOMETRY'S invalidity as its own. check_eq $P/feature-bad-geometry-invalid "no" "$(pv feature_bad_geom_valid)" # An empty FeatureCollection is legal and keeps its `features' member. check_eq $P/empty-feature-collection '{"type": "FeatureCollection", "features": []}' "$(pv empty_fc_dumps)" # NON-ASCII PROPERTY VALUES SURVIVE, unescaped -- the encoder does not force # ensure_ascii, so UTF-8 goes through as itself. On this image that is also a # statement about the interpreter's default encoding. check_eq $P/non-ascii '{"type": "Feature", "geometry": {"type": "Point", "coordinates": [100.0, 0.0]}, "properties": {"name": "Köln"}}' "$(pv non_ascii_dumps)" # ----------------------------------------------------------------- __all__ ---- # UPSTREAM SHIPS `__all__' AS OBJECTS, NOT NAMES -- see the header. `from geojson # import *' therefore RAISES TypeError. Asserted, not corrected: it is what this # image ships, and a release that fixed it would change the public surface. check_eq $P/all-length "19" "$(pv all_len)" check_eq $P/all-holds-objects "function" "$(pv all_elem_type)" check_eq $P/all-not-strings "no" "$(pv all_all_str)" check_eq $P/star-import-typeerror "TypeError" "$(pv star_import_exc)" # -------------------------------------------------------------------- repr ---- # `__repr__' IS A SERIALISATION, AND A SORTED ONE -- it is # `dumps(self, sort_keys=True)', so it spells `coordinates' before `type' while # dumps() emits insertion order. Both asserted; see the header for why comparing # one against the other would be a red that is not a defect. check_eq $P/repr-is-sorted-json '{"coordinates": [100.0, 0.0], "type": "Point"}' "$(pv point_repr)" check_eq $P/str-equals-repr '{"coordinates": [100.0, 0.0], "type": "Point"}' "$(pv point_str)" rm -rf "$W"