. "$BST_LIB" # BST-TIMEOUT: 1200 # TWO python3 starts, and they are the whole cost. Every # # other assertion below reads the filesystem or dpkg with # # shell builtins, or greps the output of one of those two. # # # # The two are: a bare `import gpsd' at the shell -- what a # # CALLER on this image actually gets, which cannot be folded # # into the probe for that reason -- and the probe, which # # takes ONE start for a hundred and fifty measurements. # # # # There is no third. The distribution declares no # # console_scripts and installs no executable, so there is # # nothing to reach through a shebang; `no-bin-script' # # asserts that absence rather than leaving it unstated. # # # # The work inside the probe is dict lookups, json.loads over # # short literals, and exactly TWO socket(2)+connect(2) calls # # against ports with nothing listening. Nothing sleeps and # # nothing retries. The largest allocation is a five-element # # satellite list. On the x86-64 control the whole probe is # # under 20 ms of work beside an interpreter start that is # # tens of seconds on this arm. # # # # 1200 is 600s per start, matching 99-python3-spidev's and # # 99-python3-crc32c's suite budgets and ten times the ~60s a # # python3 start has measured at on this arm. # # # # A suite-timeout red is a batch-wide cost, so if the two # # 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-gpsd-py3 0.3.0 (meta-openembedded/meta-python # recipes-devtools/python/python3-gpsd-py3_0.3.0.bb, `inherit pypi setuptools3', # PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE DOES. # ============================================================================ # # A client for `gpsd', the GPS multiplexer daemon. It opens a TCP connection to # gpsd (127.0.0.1:2947 by default), speaks gpsd's line-oriented JSON protocol -- # a VERSION banner, a `?WATCH' subscription, then `?POLL;' requests answered by # POLL reports carrying TPV (time-position-velocity) and SKY (satellite view) # objects -- and presents the result as a `GpsResponse' whose methods return # position, altitude, movement, precision and an OpenStreetMap URL. # # IT IS PURE PYTHON, and that is the single most important fact about this port. # The distribution is ONE file, gpsd/__init__.py (9823 bytes), importing nothing # outside the standard library: socket, json, logging, datetime. There is no C # extension, no compiled object and no architecture-specific content of any kind. # # SO BRINTOS-2046 DOES NOT REACH IT. Every C-extension port in this Epic -- # python3-spidev, python3-crc32c, python3-bitarray, python3-psutil -- is dead on # arrival on this image, because /usr/bin/python3.12 is one static binary # exporting 35 symbols of which not one is `Py*', so a side module's env.Py* # imports have nothing to bind against. There is no side module here. `import # gpsd' is EXPECTED TO SUCCEED, and the behavioural half of this suite is # expected to be GREEN rather than skipped -- which is why the cases below assert # values rather than gating on a known platform failure. # # The import is still a VALUE and not an assumption: if it ever fails, the # behavioural cases SKIP with the error attached rather than reding one by one. # That costs nothing when the expectation holds and keeps one platform fact from # arriving as a hundred and fifty unexplained reds if it does not. # # ============================================================================ # THERE IS NO gpsd DAEMON AND NO GPS RECEIVER BEHIND THIS MACHINE. # ============================================================================ # # Both halves of that are ASSERTED below rather than assumed -- `no-gpsd-binary' # and `nothing-on-2947' -- because every refusal this suite measures is # attributed to that absence, and a machine that had grown a listener would make # those expectations wrong while they all still "passed" for the wrong reason. # # The absence splits the package cleanly, and the split is this suite's design: # # THE PROTOCOL LAYER is a PURE FUNCTION of the bytes gpsd would have sent. # Parsing TPV, SKY, VERSION, DEVICES and WATCH reports into objects, and every # derived helper over them, needs no daemon at all -- only the bytes. It is # driven from fixtures committed in data/python3-gpsd-py3-probe.py and, where # a stream is required, through an injected fake that replays them. This is # the large half and it is fully measurable here. # # THE CONNECTION LAYER has ONE documented behaviour with nothing listening, # and the thing worth asserting is that it produces it PROMPTLY -- no retry # loop, no wedge on a socket. That is `connect-refused' plus its elapsed # bound, and it is the case this entry most exists to catch. # # WHY A FAKE STREAM RATHER THAN A REAL LISTENER. Standing up a gpsd-speaking # server would need a thread or a fork plus a bind(2). `gpsd.connect' reaches # its socket through the module-global `gpsd.socket' and `gpsd.get_current' # reaches its stream through `gpsd.gpsd_stream'; both are rebindable from # outside, so the whole protocol layer runs in-process with no listener, no bind # and no second task. The bytes replayed are real gpsd wire format, so what is # asserted is the package's parsing rather than the fake. # # ============================================================================ # EVERY BEHAVIOURAL EXPECTATION IS A MEASUREMENT, NOT A READING OF THE SOURCE. # ============================================================================ # # Each value below is the OUTPUT of an x86-64 control running the exact bytes # this port ships -- the module is pure Python, so the file extracted from this # port's own .deb IS the control, with no recompilation and nothing to diverge. # Python 3.12.3 on the control against python3.12 on the image. # # That caught four upstream behaviours the source does not read like, and the # ticket's own description predicted one of them the other way: # # * `get_current()' BEFORE ANY CONNECTION raises AttributeError: 'NoneType' # object has no attribute 'write' -- NOT one of the package's documented # errors. The module-global stream is None until `connect' assigns it, and # nothing checks it. The ticket expected "the documented error"; there is # none, and `no-connection-raises-attributeerror' asserts what happens. The # REQUIREMENT it was standing in for -- that no hollow object is returned -- # holds, and is asserted separately. # # * AN INACTIVE POLL REPORT RAISES `UserWarning'. Upstream raises a Warning # subclass as a control-flow exception, which is unusual enough that an # expectation written from the docstring would have named NoFixError. # # * `speed()' AND `speed_vertical()' FILTER AGAINST THE ERROR ESTIMATE and # return a bare int 0 when the reading is inside it. The 2D fixture's speed # is 0.25 m/s against an eps of 1.0, so `speed()' is 0 and not 0.25; the 3D # fixture's climb is 0.75 against an epc of 1.5, so `speed_vertical()' is 0 # while `movement()["climb"]' -- which does NOT filter -- is 0.75. Both are # asserted, because the pair is what makes it a statement about the filter. # # * `position_precision()' RETURNS max(epx, epy), NOT a pair of them. The 3D # fixture's epx is 4.0 and its epy 6.0, so the horizontal figure is 6.0. # # `t1_*' asserts the no-fix path, where every one of the eight derived helpers # refuses with NoFixError -- including `get_time()', which the docstrings do not # group with the rest. P=python3-gpsd-py3 # `suite-canary' is emitted FIRST and unconditionally. A suite that dies before # its first result line is indistinguishable from one that was never scheduled, # so this one line is what separates "ran and found nothing" from "did not run". bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages MOD=$SITE/gpsd/__init__.py DI=$SITE/gpsd_py3-0.3.0.dist-info PKGLIST=/var/lib/dpkg/info/python3-gpsd-py3.list PROBE=$BST_DATA/python3-gpsd-py3-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-gpsdpy3.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ---------------------------------------------------------------- packaging -- # WHERE THE PACKAGING SAYS THE MODULE LANDS. These run before any import, so a # red here says the .deb is wrong rather than that the module misbehaves. check $P/site-packages-dir test -d "$SITE" check $P/package-dir test -d "$SITE/gpsd" check $P/module-present test -f "$MOD" check $P/module-nonempty test -s "$MOD" # IT IS PURE PYTHON, asserted on the artifact. A .so anywhere under the package # directory would mean a release that grew a C accelerator -- which would change # what this port is, would put it behind BRINTOS-2046, and wants noticing. check_eq $P/no-shared-object "0" \ "$(find "$SITE/gpsd" -name '*.so' 2>/dev/null | wc -l)" # THE MODULE IS THE WHOLE DISTRIBUTION: one .py, plus its cached bytecode. check_eq $P/one-py-file "1" \ "$(find "$SITE/gpsd" -maxdepth 1 -name '*.py' 2>/dev/null | wc -l)" check $P/pyc-present test -f "$SITE/gpsd/__pycache__/__init__.cpython-312.pyc" # The four standard-library imports the module opens with, read off the shipped # file. If a release grew a third-party dependency, the recipe's RDEPENDS would # have to grow with it and this is where that is noticed. check_out $P/imports-socket "import socket" head -n 4 "$MOD" check_out $P/imports-json "import json" head -n 4 "$MOD" check_out $P/imports-logging "import logging" head -n 4 "$MOD" check_out $P/imports-datetime "import datetime" head -n 4 "$MOD" # NO EXECUTABLE. setup.py declares no console_scripts, so nothing lands in # /usr/bin. Asserted so that a future release which started shipping one is # noticed rather than acquiring an untested surface in silence. check $P/no-bin-script test ! -e /usr/bin/gpsd-py3 # --------------------------------------------------------------- dist-info --- check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-name "Name: gpsd-py3" cat "$DI/METADATA" check_out $P/dist-info-version "Version: 0.3.0" cat "$DI/METADATA" check_out $P/dist-info-top-level "gpsd" cat "$DI/top_level.txt" check $P/dist-info-record test -s "$DI/RECORD" # PURELIB, stated by the packaging itself -- the exact opposite of what # python3-spidev's `dist-info-not-purelib' asserts, and the one-line summary of # why this port is not behind BRINTOS-2046. check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" # AND THE WHEEL TAG IS THE PURE ONE. python3-spidev's `dist-info-tag-is-host' # pins the OPPOSITE case -- bdist_wheel computes a tag from the BUILD HOST, so a # cross-built extension ends up claiming `cp312-cp312-linux_x86_64' for a wasm32 # module. A pure-Python distribution has no such trap: the tag is correct on any # host, and asserting it here is what says this port does not carry that defect. check_out $P/dist-info-tag-pure "Tag: py3-none-any" cat "$DI/WHEEL" # ------------------------------------------------------------ the .deb file --- # The installed file list, read once into $W. The copy is what the loop below # redirects from: a stdin redirect the top-level shell performs on the virtiofs # root makes dup2(2) return 513 and ENDS the suite silently (brintos/linux#73), # and $W is on tmpfs. cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" # Every payload path is under site-packages and is the module, its bytecode, or # a dist-info file. A .deb that had grown anything else is a port that shipped # something nobody measured. _bad="" while IFS= read -r _f; do case "$_f" in ''|/usr/lib/python3.12/site-packages) continue ;; */) continue ;; esac [ -d "$_f" ] && continue case "$_f" in */gpsd/__init__.py) ;; *.pyc) ;; */METADATA|*/WHEEL|*/RECORD|*/INSTALLER|*/LICENSE|*/top_level.txt) ;; *) _bad="$_bad $_f" ;; esac done < "$W/pkgf" check_eq $P/pkg-only-expected "" "$_bad" # ------------------------------------------------------------- dpkg status --- sed -n '/^Package: python3-gpsd-py3$/,/^$/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" # THE DEPENDENCY SET IS THE PORT'S CLAIM THAT IT WAITS ON NOTHING BUT PYTHON. # The recipe carries no DEPENDS and RDEPENDS only `python3'; the .deb's control # stanza reads `Depends: python3, python3-core'. Asserted by name, because a # stanza that had lost python3-core would surface as an ImportError looking like # this port's fault. 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-python3 "python3" cat "$W/dep" check_out $P/pkg-depends-python3-core "python3-core" cat "$W/dep" # A PURE-PYTHON PACKAGE STILL CARRIES THIS ARM'S ARCHITECTURE, because the OE # packaging derives it from the recipe rather than from the payload. Asserted so # the fact is on the record beside `dist-info-purelib', which says the opposite # about the wheel. check_out $P/pkg-arch "Architecture: wasm32" cat "$W/st" # --------------------------------------------- the absence of gpsd, asserted -- # EVERY REFUSAL THIS SUITE MEASURES IS ATTRIBUTED TO THIS ABSENCE. A machine # that had grown a gpsd would make `connect-refused' and its elapsed bound wrong # while both still reported a pass, so the absence is measured and not assumed. check $P/no-gpsd-binary test ! -x /usr/sbin/gpsd check $P/no-gpsd-client test ! -x /usr/bin/gpspipe # Nothing is listening on gpsd's IANA port. Read from /proc/net/tcp, which needs # no tool: the local-address column holds `0B83' for port 2947, and state `0A' # is TCP_LISTEN. A grep that matches nothing exits 1, which is the pass here. check_rc $P/nothing-on-2947 1 grep -qi '^ *[0-9]*: [0-9A-F]*:0B83 .* 0A ' /proc/net/tcp # ------------------------------------------------- the import, from a shell -- # WHAT A CALLER ON THIS IMAGE ACTUALLY GETS, and the first of two python3 starts. # Unlike this Epic's C-extension ports this is EXPECTED TO PASS: the package is # pure Python and has no side module to load. check $P/import python3 -c 'import gpsd' # The module resolves to the file this port shipped, and not to some other gpsd # on the path -- `gpsd' is a common enough name that this is worth pinning. check_out $P/import-resolves "$MOD" \ python3 -c 'import gpsd; print(gpsd.__file__)' # ------------------------------------------------------------------- probe --- if [ ! -f "$PROBE" ]; then bst_notok $P/probe-present "probe missing at $PROBE" rm -rf "$W" exit 0 fi bst_ok $P/probe-present # THE SECOND START, and the one that carries the suite. Bounded at 900s inside # the 1200s suite budget so a hung probe is reported as a probe overrun with the # last line it printed, rather than as a suite timeout that says nothing about # where it stopped. # # The probe EXITS 0 even when the import fails, so this case passing means "the # probe ran", not "the module works" -- `import-ok' below is what carries that. bst_probe $P/probe-run "$W/probe" 900 python3 "$PROBE" OUT="$W/probe" # pv : the value the probe emitted for , or empty. `head -n 1' because # a key is emitted once and a duplicate would otherwise concatenate; `cut -f2-' # because a value may legitimately contain `='. 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 ran to its last line. Without this a probe that died halfway would # leave every later key empty, and each would red as a wrong VALUE rather than # as a truncated run. check_eq $P/probe-complete "yes" "$(pv probe_done)" # ---------------------------------------------------- the behavioural gate --- # One decision point for the whole behavioural half. It is expected to be `yes'; # if it is not, the cases below SKIP with the load error attached rather than # reding one by one, so a platform fact costs two results that name it instead # of a hundred and fifty that bury it. IMPORT_OK=$(pv import_ok) check_eq $P/import-ok "yes" "$IMPORT_OK" if [ "$IMPORT_OK" != "yes" ]; then bst_skip $P/behaviour "import failed: $(pv import_error) -- every behavioural case below is unreachable" rm -rf "$W" exit 0 fi # eq : assert the probe's value for . eq() { check_eq "$P/$1" "$2" "$(pv "$3")"; } # ------------------------------------------------------------ the API shape -- # The names the package documents, each asserted present. A release that dropped # one would otherwise show up only as whichever behavioural case reached it. for _n in connect get_current device GpsResponse NoFixError gpsTimeFormat state logger; do eq "has-$_n" "yes" "has_$_n" done eq time-format "%Y-%m-%dT%H:%M:%S.%fZ" timefmt eq nofix-is-exception yes nofix_is_exception # THE DISTRIBUTION DECLARES NO `__version__'. Asserted as an absence so that the # version case below is understood to read the dist-info and not the module. eq no-dunder-version no has_dunder_version eq dist-version 0.3.0 dist_version # --------------------------------------------------- the 3D fix, parsed ------ # The fixture is a POLL report carrying one TPV at mode 3 and one SKY with five # satellites, three of them `used'. Every literal here is that fixture's. eq t3-mode 3 t3_mode eq t3-lat 52.370216 t3_lat eq t3-lon 4.895168 t3_lon eq t3-alt 12.5 t3_alt eq t3-track 47.5 t3_track eq t3-hspeed 3.25 t3_hspeed eq t3-climb 0.75 t3_climb eq t3-time 2026-09-08T12:00:00.000Z t3_time # SATELLITE COUNTING IS THE SKY OBJECT'S WHOLE CONTRIBUTION: five in view, # three with `used': true. The pair is asserted together because a parser that # ignored `used' would still get `sats' right. eq t3-sats 5 t3_sats eq t3-sats-valid 3 t3_sats_valid # The error block, field by field. `c' and `v' are populated only on the mode>=3 # path, which is what distinguishes this from the 2D fixture below. eq t3-err-x 4.0 t3_err_x eq t3-err-y 6.0 t3_err_y eq t3-err-v 9.5 t3_err_v eq t3-err-t 0.005 t3_err_t eq t3-err-s 0.5 t3_err_s eq t3-err-c 1.5 t3_err_c # The derived helpers, against the values those literals imply. eq t3-position "(52.370216, 4.895168)" t3_position eq t3-altitude 12.5 t3_altitude eq t3-movement "{'speed': 3.25, 'track': 47.5, 'climb': 0.75}" t3_movement # THE FILTER, both ways round. hspeed 3.25 is ABOVE eps 0.5 so `speed()' passes # it through; climb 0.75 is BELOW epc 1.5 so `speed_vertical()' returns a bare # int 0 -- while `movement()' above, which does not filter, still reports 0.75. eq t3-speed 3.25 t3_speed eq t3-speed-vertical 0 t3_speed_vertical # max(epx 4.0, epy 6.0) = 6.0, paired with epv. NOT a pair of epx and epy. eq t3-precision "(6.0, 9.5)" t3_precision eq t3-map-url "http://www.openstreetmap.org/?mlat=52.370216&mlon=4.895168&zoom=15" t3_map_url eq t3-repr "" t3_repr # `get_time()' parses the TPV timestamp through gpsTimeFormat. The result is a # NAIVE datetime -- the trailing Z is consumed as a literal, not as a zone. eq t3-get-time 2026-09-08T12:00:00 t3_get_time # --------------------------------------------------- the 2D fix, parsed ------ # mode 2: the mode>=3 block must NOT run, so altitude and climb keep their # constructor defaults and the error block's `c' and `v' stay 0. eq t2-mode 2 t2_mode eq t2-lat 48.85837 t2_lat eq t2-lon 2.294481 t2_lon eq t2-alt 0.0 t2_alt eq t2-climb 0 t2_climb eq t2-sats 1 t2_sats eq t2-sats-valid 1 t2_sats_valid eq t2-err-v 0 t2_err_v eq t2-err-c 0 t2_err_c eq t2-position "(48.85837, 2.294481)" t2_position eq t2-precision "(7.5, 0)" t2_precision eq t2-map-url "http://www.openstreetmap.org/?mlat=48.85837&mlon=2.294481&zoom=15" t2_map_url eq t2-repr "" t2_repr # A 2D FIX IS NOT A 3D ONE, and the two helpers that need altitude say so by # name even though a fix exists. This is the case that separates "no fix" from # "not enough fix". eq t2-altitude-refused "NoFixError: Needs at least 3D fix" t2_altitude_exc eq t2-movement-refused "NoFixError: Needs at least 3D fix" t2_movement_exc # speed 0.25 is BELOW eps 1.0, so the filter returns a bare int 0. eq t2-speed-filtered 0 t2_speed # --------------------------------------------------- the no-fix report ------- # mode 1. Nothing was populated, and every derived helper refuses. eq t1-mode 1 t1_mode eq t1-lat 0.0 t1_lat eq t1-lon 0.0 t1_lon eq t1-sats 0 t1_sats eq t1-error-empty "{}" t1_error_dict eq t1-repr "" t1_repr # ALL EIGHT REFUSE, and each with NoFixError rather than an AttributeError off a # field that was never set -- which is the failure mode a hollow object would # have. `get_time' is included: the docstrings do not group it with the rest, # and it guards on mode exactly as the others do. for _m in position altitude movement speed speed_vertical position_precision map_url get_time; do eq "t1-$_m-type" NoFixError "t1_${_m}_type" done # The two thresholds, named. 2D is enough for five of them; three need 3D. eq t1-position-msg "NoFixError: Needs at least 2D fix" t1_position_exc eq t1-altitude-msg "NoFixError: Needs at least 3D fix" t1_altitude_exc # A SKY WITH NO `satellites' KEY is the documented else-branch, and it must not # raise: the counts are 0 while the fix itself still parses at mode 3. eq tn-sats 0 tn_sats eq tn-sats-valid 0 tn_sats_valid eq tn-mode 3 tn_mode # --------------------------------------------- a report that claims no GPS --- # `active': 0. UPSTREAM RAISES A `Warning' SUBCLASS AS CONTROL FLOW, which is # unusual enough that an expectation written from the docstring would have said # NoFixError. Asserted as measured, so a release that regularised it is noticed. eq inactive-type UserWarning inactive_type eq inactive-msg "UserWarning: GPS not active" inactive_exc # ------------------------------------------- a default-constructed response -- # It claims NOTHING: mode 0, no position, an empty time and an empty error dict, # and it refuses to hand out a position. This is the shape a "hollow object" # would have, and it is reachable only by constructing one deliberately. eq default-mode 0 default_mode eq default-lat 0.0 default_lat eq default-lon 0.0 default_lon eq default-sats 0 default_sats eq default-time "''" default_time eq default-error "{}" default_error eq default-repr "" default_repr eq default-position-refused "NoFixError: Needs at least 2D fix" default_position_exc # --------------------------------------------------------- malformed input --- # EACH MUST RAISE, never yield a partly-populated fix. That is the requirement; # the exception TYPE is what was measured, and several are the plain KeyError of # an unguarded lookup rather than one of the package's own errors. eq bad-json-type JSONDecodeError bad_json_exc eq bad-json-is-value yes bad_json_is_valueerror # A truncated report: valid JSON, valid envelope, no `tpv' list. eq trunc-no-tpv-type KeyError trunc_no_tpv_type eq trunc-no-tpv-msg "KeyError: 'tpv'" trunc_no_tpv_exc # An EMPTY tpv list -- valid keys, nothing to index. `[-1]' is what fails. eq empty-tpv-type IndexError empty_tpv_type # A required field missing from an otherwise well-formed TPV. eq no-mode-type KeyError no_mode_type eq no-mode-msg "KeyError: 'mode'" no_mode_exc # `active' is the FIRST key the parser reads, so its absence fails earliest. eq no-active-type KeyError no_active_type # A satellite entry with no `used' key, inside an otherwise valid SKY. eq sat-no-used-type KeyError sat_no_used_type # --------------------------------------------- the connect-time state machine - # `_parse_state_packet' is what `connect' drives over the two reports gpsd sends # after a ?WATCH. DEVICES and WATCH are stored; anything else is refused by name. eq state-devices-ok no-raise state_devices_exc eq state-devices-stored yes state_has_devices eq state-watch-ok no-raise state_watch_exc eq state-watch-stored yes state_has_watch # `device()' renames the wire's fields -- bps becomes `speed'. eq device-info "{'path': '/dev/ttyS0', 'speed': 9600, 'driver': 'NMEA0183'}" device_info eq state-bogus-type Exception state_bogus_type eq state-bogus-msg "Exception: Unexpected message received from gps: TPV" state_bogus_exc # AN EMPTY DEVICE LIST IS ACCEPTED AND MERELY LOGGED. gpsd sends this when it is # running with no receiver attached -- exactly this machine's situation had a # daemon been present -- so it must not raise. It is stored, and `device()' over # it is then an IndexError off the empty list rather than anything typed. eq state-empty-devices-ok no-raise state_empty_devices_exc eq state-empty-devices-stored yes state_empty_devices_stored eq device-empty-type IndexError device_empty_type # `device()' before any connection: the state dict is empty, so it is a KeyError. eq device-nostate-type KeyError device_nostate_type # ------------------------------------------------- the stream layer, replayed - # `get_current()' over a POLL report: the whole read path, minus the socket. eq poll-type GpsResponse poll_type eq poll-mode 3 poll_mode eq poll-lat 52.370216 poll_lat eq poll-lon 4.895168 poll_lon eq poll-alt 12.5 poll_alt eq poll-sats 5 poll_sats eq poll-sats-valid 3 poll_sats_valid eq poll-map-url "http://www.openstreetmap.org/?mlat=52.370216&mlon=4.895168&zoom=15" poll_map_url # THE REQUEST IT PUT ON THE WIRE is itself an assertion: gpsd's poll command is # `?POLL;' terminated by a newline, and a client that sent anything else would # get no answer from a real daemon while every parsing case here still passed. eq poll-request "'?POLL;\\n'" poll_wrote # An answer of the wrong class is refused by name rather than parsed anyway. eq poll-wrongclass-type Exception poll_wrongclass_type eq poll-wrongclass-msg "Exception: Unexpected message received from gps: VERSION" poll_wrongclass_exc # A malformed or short line from the daemon raises rather than yielding a fix. eq poll-badjson-type JSONDecodeError poll_badjson_type eq poll-trunc-type KeyError poll_trunc_type # ------------------------------------- get_current() before any connection --- # THE TICKET'S OWN QUESTION, and the answer is not the one it predicted. There # is no documented error here: the module-global stream is None until `connect' # assigns it and nothing checks it, so the failure is an attribute lookup on # None. Asserted as measured. eq no-connection-raises-attributeerror AttributeError noconn_get_current_type eq no-connection-msg "AttributeError: 'NoneType' object has no attribute 'write'" noconn_get_current_exc # THE REQUIREMENT THE TICKET WAS ACTUALLY AFTER still holds, and is asserted # separately from the spelling of the error: it RAISES rather than returning a # hollow GpsResponse that would read as a fix at 0N 0E. eq no-connection-returns-nothing no noconn_returned_object eq no-connection-device-type KeyError noconn_device_type # ------------------------------------------ connect(), through a fake socket -- # The welcome-banner contract, with no listener: VERSION, then ?WATCH, then the # two state reports. This exercises everything `connect' does except the # socket(2) itself. eq fakeconn-ok no-raise fakeconn_exc eq fakeconn-default-addr "('127.0.0.1', 2947)" fakeconn_addr eq fakeconn-subscription "'?WATCH={\"enable\":true}\\n'" fakeconn_wrote eq fakeconn-state-devices yes fakeconn_state_devices eq fakeconn-state-watch yes fakeconn_state_watch eq fakeconn-device "{'path': '/dev/ttyS0', 'speed': 9600, 'driver': 'NMEA0183'}" fakeconn_device # The host and port arguments reach the socket, so the default above is a # default and not the only thing it can do. eq fakeconn-custom-addr "('192.0.2.7', 9999)" fakeconn_custom_addr # A SERVER THAT DOES NOT OPEN WITH `VERSION' IS REFUSED, by name. This is the # guard that would catch a non-gpsd listener on 2947. eq fakeconn-noversion-type Exception fakeconn_noversion_type eq fakeconn-noversion-msg \ "Exception: Unexpected data received as welcome. Is the server a gpsd 3 server?" \ fakeconn_noversion_exc # ============================================================================ # THE ONE REAL SOCKET: connect() with nothing listening. # ============================================================================ # # THIS IS THE CASE THE TICKET EXISTS TO CATCH. The connection layer's single # documented behaviour on a machine with no gpsd is ConnectionRefusedError, and # what matters is that it arrives PROMPTLY -- no retry loop, no wedge on a # socket. `nothing-on-2947' above established that the port is closed. # # ECONNREFUSED on loopback is measured-working on this platform by several # landed suites -- 84-iperf3 `connect-refused', 61-netcat `refused', 99-mbpoll # `conn-refused', 99-netcat-openbsd `scan-closed-out' -- so a hang or a wrong # errno here is this package's behaviour rather than the stack's. eq connect-refused-type ConnectionRefusedError refused_type eq connect-refused-msg "ConnectionRefusedError: [Errno 111] Connection refused" refused_exc eq connect-refused-is-oserror yes refused_is_oserror # ConnectionRefusedError's identity, so the two cases above assert a TYPE and # not merely a spelling: an OSError subclass, and ECONNREFUSED is 111 on this # arm as it is on the control. eq connrefused-subclasses-oserror yes connrefused_is_oserror eq econnrefused-is-111 111 econnrefused # THE ANTI-WEDGE BOUND. On the x86-64 control both refusals are under 1 ms. A # loopback RST needs no timeout on any stack, so 10s is enormous headroom and # still catches the failure this case is for: a connect that retries or hangs. # A red here with a large number is a WEDGE, not a slow machine. _ms=$(pv refused_elapsed_ms) case "$_ms" in ''|*[!0-9]*) bst_notok $P/connect-refused-prompt "no elapsed measurement: got '$_ms'" ;; *) if [ "$_ms" -le 10000 ]; then bst_ok $P/connect-refused-prompt else bst_notok $P/connect-refused-prompt "connect() to a closed port took ${_ms}ms -- expected an immediate ECONNREFUSED, not a retry or a hang"; fi ;; esac # The same against a second closed port, so the result is a property of a closed # port rather than of port 2947 in particular. eq connect-refused-high-type ConnectionRefusedError refused_high_type _ms2=$(pv refused_high_elapsed_ms) case "$_ms2" in ''|*[!0-9]*) bst_notok $P/connect-refused-high-prompt "no elapsed measurement: got '$_ms2'" ;; *) if [ "$_ms2" -le 10000 ]; then bst_ok $P/connect-refused-high-prompt else bst_notok $P/connect-refused-high-prompt "connect() to a closed high port took ${_ms2}ms"; fi ;; esac rm -rf "$W"