. "$BST_LIB" # BST-TIMEOUT: 1200 # THREE python3 starts, and they are the whole cost. Every # # measurement in this suite is dict construction -- no # # exec, no I/O, no device -- so unlike 99-python3-editor # # next door the interpreter start IS the run. # # # # The three are: a bare `import', a second one whose STDERR # # is the assertion (`import-quiet'), and the probe, which # # takes ONE start for 111 measurements. # # # # 1200 is 400s per start, the per-start budget the # # neighbouring pure-Python python3 suites use. # # # # 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-apispec 6.4.0 (meta-openembedded meta-python/recipes-devtools/python, # python3-apispec_6.4.0.bb, `inherit pypi setuptools3', PyPI sdist) on the # wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE ACTUALLY IS, AND THEREFORE WHAT HAS TO BE TESTED. # ============================================================================ # # `apispec' ACCUMULATES METADATA AND EMITS A DOCUMENT. An `APISpec' object # collects paths, operations, schemas, parameters, responses, tags and security # schemes; `to_dict()' renders them into the layout the OpenAPI Specification # requires -- a DIFFERENT layout for the 2.0 and the 3.x families -- and # plugins supply framework- and serializer-specific translation at documented # hooks. # # There is no I/O, no network, no subprocess, no terminal and no device in any # of it. So the output IS the document, and the assertion is the document: # every structural case below compares `json.dumps(..., sort_keys=True)' of a # real `to_dict()' result against a literal. That is what makes this port close # on its suite with no BRINTOS-780 follow-on. # # ============================================================================ # THE PORT IS AN IMAGE_INSTALL LINE, A SUITE AND A PROBE. NOTHING ELSE. # ============================================================================ # # NO PATCH, NO BBAPPEND AND NO SITE ANSWER. meta-openembedded's recipe is used # exactly as written: there is no configure step, no compile step and no C, so # there is nothing on this arm for an override to reach. MEASURED rather than # read off the recipe -- all eleven installed `.py' files are BYTE-IDENTICAL # (`cmp') to their counterparts in the sdist the SRC_URI sha256 pins. # Recipe-scope build: 1641 of 1641 tasks, all succeeded, zero ERROR: lines. # # THE DECLARED RDEPENDS IS RIGHT AND IS LEFT ALONE. `python3-packaging' is # load-bearing rather than decorative: core.py line 10 is `from # packaging.version import Version', at MODULE scope, so without it `import # apispec' fails outright. The built .deb's line is exactly # "Depends: python3-core, python3-packaging", and `pkg-depends-exact' pins it. # # PURE PYTHON, WHICH IS WHY IT PORTS. This image's python3 is ONE STATIC BINARY # with an empty lib-dynload and no dynamic loader (BRINTOS-1476), so a module # carrying a compiled extension could not be imported here at all. apispec # ships no C -- no `.c', no `ext_modules' in setup.py -- so `pkg-no-so' is a # REGRESSION TRIPWIRE on a version bump rather than a statement about today. # setup.py declares no `entry_points' and no `scripts', so NOTHING lands in # /usr/bin; there is no CLI to test and none is claimed. # # ============================================================================ # NO YAML ON THIS IMAGE. THIS IS THE ONE THING A CALLER WILL TRIP OVER. # ============================================================================ # # `to_yaml()' does not work here, and the suite says so in four places rather # than leaving it to be rediscovered. # # PyYAML is an OPTIONAL EXTRA upstream -- `extras_require["yaml"]', not # `install_requires' -- so the recipe is RIGHT not to carry it, and unlike the # python3-configargparse port next door there is no `PACKAGECONFIG' pulling it # in and therefore no bbappend needed to push it back out. poky's # python3-pyyaml is a CYTHON EXTENSION against libyaml, which this # interpreter could not load however libyaml were linked (BRINTOS-1476); it is # not ported and is not on this image. # # What saves the port is that the import is LAZY: `to_yaml()' does # `from .yaml_utils import dict_to_yaml' INSIDE the method body, so the package # imports cleanly, `to_dict()' works, and only this one call path fails -- # with CPython's own ModuleNotFoundError rather than anything of apispec's. # `yaml-absent', `yaml-utils-absent', `to-yaml-raises' and # `to-yaml-still-callable' are the four sides of that. # # THE TICKET ASKED FOR A to_yaml() ROUND-TRIP, AND THAT CANNOT HOLD HERE. There # is no YAML parser in the standard library either, so the round-trip is run # through the parser this image DOES have -- `json' -- which answers the same # question the ticket was really asking: that `to_dict()' returns a plain, # fully-serialisable tree with no library objects left in it. A stray `Version' # or a tuple would fail `json-roundtrip'. This is a SUBSTITUTION, stated as one. # # `apispec.ext.marshmallow' IS SHIPPED AND CANNOT IMPORT, for the same ordinary # reason: marshmallow is also an extra and is also not on this image. The files # are on the rootfs, so a caller reaching for them gets an import error rather # than a missing path -- one named case (`ext-marshmallow-absent') so it is not # rediscovered as a packaging fault. # # ============================================================================ # THE VERSION DISPATCH, WHICH IS THE POINT OF THE PACKAGE. # ============================================================================ # # The probe builds THE SAME SPEC TWICE from ONE function that takes only the # version -- so every difference between the two documents is one the LIBRARY # introduced. Three differences are asserted from both sides: # # THE VERSION MARKER IS A DIFFERENT KEY, not a different value: `swagger' at # 2.0 and `openapi' at 3.x. `v2-swagger'/`v3-openapi' assert # the value and `v2-no-openapi-key'/`v3-no-swagger-key' # assert that neither document carries the other's. # # COMPONENTS MOVE BY NESTING, not by renaming alone. At 2.0 they are TOP-LEVEL # keys (`definitions', `securityDefinitions'); at 3.x they are # all under one `components' key (`schemas', # `securitySchemes'). `root-only-in-v2' and `root-only-in-v3' # state the difference as a set, once, rather than key by key. # # THE `$ref' STRINGS DIFFER, and they are compared EXACTLY. The same # `schema: "Pet"' becomes `#/definitions/Pet' at 2.0 and # `#/components/schemas/Pet' at 3.x -- the `components/' # segment is present at 3.x only. This is the case that # catches a version-dispatch mistake. # # TWO THINGS THE TICKET EXPECTED THAT ARE NOT TRUE, AND THE SUITE RECORDS WHAT # IS ACTUALLY THERE. # # `basePath' VERSUS `servers' IS NOT A VERSION DISPATCH AT ALL. apispec # neither generates nor translates either key: both arrive # through `**options' and are merged verbatim by # `deepupdate'. So passing `basePath' to a 3.x spec puts # `basePath' in a 3.x document -- not a valid OpenAPI 3 field # -- and NOTHING REJECTS IT. `v3-basepath-passes-through' is # that measurement; the ticket's expectation that the two # swap is wrong. # # AN UNKNOWN OpenAPI VERSION HAS TWO DIFFERENT FAILURE MODES. A number OUT OF # RANGE is apispec's own `APISpecError'. A string that is not # a version AT ALL never reaches apispec's check -- # `packaging.version.Version' rejects it first, with # `InvalidVersion', which is NOT an APISpecError and which a # caller catching APISpecError alone will not catch. # `version-not-a-version' and `version-empty' are the two that # say so. # # THE HEADLINE STRUCTURAL FINDING: THE SAME RESPONSE RESOLVES AT 3.x AND DOES # NOT AT 2.0. `_resolve_refs_in_response' looks at `response["schema"]' when the # major version is < 3 and at `response["content"][...]["schema"]' when it is # not -- correctly, because an OpenAPI 2 response has no `content'. The # consequence is that a caller who writes ONE v3-shaped operation and renders it # at both versions gets a 2.0 document containing the BARE STRING "Pet" where a # `$ref' belongs, and no error saying so. Measured from both sides: # `v2-response-unresolved' (the v3-shaped response) and # `v2-response-v2shape-resolved' (a properly v2-shaped one, which does resolve). # `v2-requestbody-present' is the same defect from the other end -- `requestBody' # is a 3.x-only construct that the resolver handles with NO version check, so it # survives into a 2.0 document with a correctly-built `#/definitions/' ref # inside it. apispec validates neither the shape nor the version it belongs to. # # ============================================================================ # HOW THE VALUES ARE COMPARED. # ============================================================================ # # Every structural expectation is `json.dumps(..., sort_keys=True)' with compact # separators. json rather than repr because repr's dict order is INSERTION # order, which would make the expectation depend on the order the probe happens # to call things in rather than on what the library produced; sort_keys makes # the comparison about CONTENT. It is also single-line and ASCII, which are the # runner's two rules. # # Every expectation below is a LITERAL, and every literal was MEASURED by # running this probe against the built .deb's own site-packages tree rather than # read off the source. # # The probe builds nothing on the filesystem -- it has no fixtures to write -- # so unlike its neighbours there is no workdir to clean up beyond the # transcript's own. # # ============================================================================ P=python3-apispec # The canary is first so that a suite which dies before its own first assertion # is distinguishable from one that never started. bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKG=$SITE/apispec DI=$SITE/apispec-6.4.0.dist-info PKGLIST=/var/lib/dpkg/info/python3-apispec.list PROBE=$BST_DATA/python3-apispec-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-apispec-suite.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ---------------------------------------------------------------- layout ----- # A PACKAGE, NOT A FLAT MODULE -- and one with a subpackage, `ext.marshmallow', # that ships whether or not it can import. check $P/site-packages-dir test -d "$SITE" check $P/package-dir test -d "$PKG" check $P/package-init test -s "$PKG/__init__.py" check $P/not-a-flat-module test ! -e "$SITE/apispec.py" check $P/mod-core test -s "$PKG/core.py" check $P/mod-utils test -s "$PKG/utils.py" check $P/mod-plugin test -s "$PKG/plugin.py" check $P/mod-exceptions test -s "$PKG/exceptions.py" check $P/mod-yaml-utils test -s "$PKG/yaml_utils.py" check $P/ext-dir test -d "$PKG/ext/marshmallow" check $P/ext-openapi test -s "$PKG/ext/marshmallow/openapi.py" # `py.typed' IS SHIPPED AND IS ZERO BYTES. PEP 561 makes an EMPTY file the # marker, so `test -s' would fail on a correct one -- `test -f' is the right # assertion here and the size is pinned separately. check $P/py-typed-present test -f "$PKG/py.typed" check_eq $P/py-typed-empty "0" "$(wc -c < "$PKG/py.typed" 2>/dev/null | tr -d ' ')" check $P/pycache-core test -s "$PKG/__pycache__/core.cpython-312.pyc" check $P/pycache-init test -s "$PKG/__pycache__/__init__.cpython-312.pyc" # ------------------------------------------------------------- the .deb ------ cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" # UPSTREAM'S tests/ ARE NOT PACKAGED -- setup.py uses find_packages("src") and # the tests live outside src/. Asserted against THIS package's own dpkg list # rather than against the filesystem: a leaked tests/ would land as a TOP-LEVEL # `tests' in site-packages, and that path is not ours to speak for -- # python3-reedsolo's stale egg-info already creates it (BRINTOS-2283), which # made the filesystem form of this case red for a reason having nothing to do # with apispec. check_eq $P/no-tests-dir "" \ "$(grep -E '^/usr/lib/python3\.12/site-packages/tests(/|$)' "$W/pkgf" 2>/dev/null)" # PURE PYTHON, AND NOT MERELY "NO .so OF ITS OWN" -- see the header. check_eq $P/pkg-no-so "" \ "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" # NOTHING LANDS IN /usr/bin: no `entry_points' and no `scripts' in setup.py, so # there is no CLI on this image and none is claimed. check_eq $P/pkg-no-bin "" \ "$(grep -E '^/usr/(s?bin)/' "$W/pkgf" 2>/dev/null)" # ...and nothing but Python in the payload, with `py.typed' as the ONE # documented exception. A data file appearing here would be a packaging change # nobody asked for. _bad="" while IFS= read -r _f; do [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; */apispec/py.typed) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # THE DIST-INFO IS NAMED FOR THE DISTRIBUTION, and here -- unlike the # python3-editor port next door -- the distribution, the module and the import # name are all `apispec'. check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-name "Name: apispec" cat "$DI/METADATA" check_out $P/dist-info-version "Version: 6.4.0" cat "$DI/METADATA" check $P/dist-info-record test -s "$DI/RECORD" check $P/dist-info-license test -s "$DI/LICENSE" # `setuptools3' BUILT THIS, WHICH IS VISIBLE IN THE WHEEL METADATA: one tag, and # it is py3 -- there is no `universal = 1' in setup.cfg. check_out $P/dist-info-generator "Generator: bdist_wheel" cat "$DI/WHEEL" check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" check_out $P/dist-info-tag-py3 "Tag: py3-none-any" cat "$DI/WHEEL" check_eq $P/dist-info-no-py2-tag "" \ "$(grep '^Tag: py2' "$DI/WHEEL" 2>/dev/null)" check_out $P/dist-info-top-level "apispec" cat "$DI/top_level.txt" # THE OPTIONAL EXTRAS ARE DECLARED IN THE METADATA AND NOT INSTALLED, which is # the packaging-side statement of the whole yaml/marshmallow story above. check_out $P/metadata-extra-yaml "Provides-Extra: yaml" cat "$DI/METADATA" check_out $P/metadata-requires-packaging "Requires-Dist: packaging" cat "$DI/METADATA" sed -n '/^Package: python3-apispec$/,/^$/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: 6.4.0-r0" cat "$W/st" # THE RDEPENDS IS ASSERTED EXACTLY -- see the header. A lean image that dropped # python3-packaging would say so HERE, once, rather than present as a library # that will not import. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" check_eq $P/pkg-depends-exact \ "Depends: python3-core, python3-packaging" \ "$(cat "$W/dep")" # THE TWO OPTIONAL EXTRAS ARE NOT ON THE IMAGE, asserted from the rootfs rather # than inferred from the Depends line -- these are what `to_yaml()' and # `apispec.ext.marshmallow' need, and their absence is deliberate. check $P/no-pyyaml-installed test ! -d "$SITE/yaml" check $P/no-marshmallow-installed test ! -d "$SITE/marshmallow" # ---------------------------------------------------------------- import ----- check $P/import python3 -c 'import apispec' # A DeprecationWarning, or a stray print on import, would be invisible to the # case above, which scores only the exit status. The expectation is nothing at # all. check_eq $P/import-quiet "" \ "$(python3 -c 'import apispec' 2>&1)" # ----------------------------------------------------------------- probe ----- if [ ! -f "$PROBE" ]; then bst_notok $P/probe-present "$PROBE did not ship" rm -rf "$W" exit 0 fi bst_ok $P/probe-present bst_probe $P/probe-run "$W/probe" 900 python3 "$PROBE" OUT="$W/probe" pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } if [ -s "$OUT" ]; then bst_ok $P/probe-output else bst_notok $P/probe-output "the probe emitted nothing -- python3 did not start, or died before its first line; every CASE below is unscorable" fi # The probe's own two bookends. `probe-complete' says it reached its last line # and `probe-count' says it emitted every key it meant to: a probe that died # two-thirds of the way through would otherwise leave the missing cases scoring # against an empty string, which `check_eq' would read as a wrong answer rather # than as an absent one. check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "111" "$(pv emitted_count)" # -------------------------------------------------------------- identity ----- # WHICH COPY ANSWERED. Every behavioural case below is a statement about the # package named here. check_eq $P/module-answered "$PKG/__init__.py" "$(pv module_file)" check_eq $P/is-package "yes" "$(pv is_package)" check_eq $P/version-attr "6.4.0" "$(pv version_attr)" # `__all__' EXISTS, and it is exactly the two public names. check_eq $P/dunder-all "APISpec,BasePlugin" "$(pv dunder_all)" # THE ONE RUNTIME DEPENDENCY, AND THAT IT IS THE ONE BEING USED. Not just that # `packaging' imports, but that the `Version' apispec compares with IS # packaging's -- which is what makes `pkg-depends-exact' above load-bearing. check_eq $P/packaging-importable "yes" "$(pv packaging_importable)" check_eq $P/version-type-is-packaging "yes" "$(pv version_type_is_packaging)" # The bounds the constructor enforces, read off the module rather than retyped. check_eq $P/min-version "2.0" "$(pv min_version)" check_eq $P/max-version "4.0" "$(pv max_version)" # `trace' IS THE ONLY METHOD 3.x ADDS. check_eq $P/valid-methods-v2 "get,post,put,patch,delete,head,options" \ "$(pv valid_methods_v2)" check_eq $P/valid-methods-v3 "get,post,put,patch,delete,head,options,trace" \ "$(pv valid_methods_v3)" # THE SUBSECTION TABLE IS THE WHOLE OF THE VERSION DIFFERENCE, so it is asserted # whole: v2 has FOUR component kinds and v3 has SIX -- headers and examples do # not exist as components in the 2.0 layout at all. check_eq $P/subsections-v2 \ '{"parameter":"parameters","response":"responses","schema":"definitions","security_scheme":"securityDefinitions"}' \ "$(pv subsections_v2)" check_eq $P/subsections-v3 \ '{"example":"examples","header":"headers","parameter":"parameters","response":"responses","schema":"schemas","security_scheme":"securitySchemes"}' \ "$(pv subsections_v3)" # ==================================================== THE DOCUMENT, WHOLE ==== # The ticket's spec -- title, version, one path with two operations, path and # query parameters, a request body, two responses each, a component schema # referenced from both, a tag and a security scheme -- rendered at each version # and compared against the literal nested structure the specification requires. # These two cases are the deliverable; everything after them names one part of # them so that a red says WHICH part. check_eq $P/v3-document \ '{"components":{"schemas":{"Pet":{"properties":{"id":{"format":"int64","type":"integer"},"name":{"type":"string"}},"required":["id"],"type":"object"}},"securitySchemes":{"apiKey":{"in":"header","name":"X-API-Key","type":"apiKey"}}},"info":{"title":"Pet Store","version":"1.2.3"},"openapi":"3.0.2","paths":{"/pets/{petId}":{"get":{"operationId":"getPet","parameters":[{"in":"path","name":"petId","required":true,"schema":{"type":"integer"}},{"in":"query","name":"verbose","schema":{"type":"boolean"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pet"}}},"description":"ok"},"404":{"description":"absent"}},"summary":"Get a pet","tags":["pets"]},"put":{"operationId":"putPet","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pet"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pet"}}},"description":"ok"},"400":{"description":"bad"}},"summary":"Replace a pet"}}},"tags":[{"description":"Everything about pets","name":"pets"}]}' \ "$(pv v3_doc)" check_eq $P/v2-document \ '{"definitions":{"Pet":{"properties":{"id":{"format":"int64","type":"integer"},"name":{"type":"string"}},"required":["id"],"type":"object"}},"info":{"title":"Pet Store","version":"1.2.3"},"paths":{"/pets/{petId}":{"get":{"operationId":"getPet","parameters":[{"in":"path","name":"petId","required":true,"schema":{"type":"integer"}},{"in":"query","name":"verbose","schema":{"type":"boolean"}}],"responses":{"200":{"content":{"application/json":{"schema":"Pet"}},"description":"ok"},"404":{"description":"absent"}},"summary":"Get a pet","tags":["pets"]},"put":{"operationId":"putPet","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/definitions/Pet"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":"Pet"}},"description":"ok"},"400":{"description":"bad"}},"summary":"Replace a pet"}}},"securityDefinitions":{"apiKey":{"in":"header","name":"X-API-Key","type":"apiKey"}},"swagger":"2.0","tags":[{"description":"Everything about pets","name":"pets"}]}' \ "$(pv v2_doc)" # ---------------------------------------------- the version difference, named -- check_eq $P/v3-root-keys "components,info,openapi,paths,tags" "$(pv v3_root_keys)" check_eq $P/v2-root-keys "definitions,info,paths,securityDefinitions,swagger,tags" \ "$(pv v2_root_keys)" # The difference stated as a set, in both directions, once. check_eq $P/root-only-in-v2 "definitions,securityDefinitions,swagger" \ "$(pv v2_v3_root_only_in_v2)" check_eq $P/root-only-in-v3 "components,openapi" "$(pv v2_v3_root_only_in_v3)" # A DIFFERENT KEY, NOT A DIFFERENT VALUE -- asserted from both sides so that a # document carrying BOTH markers (which no version of the spec permits) is a red # rather than a pass. check_eq $P/v2-swagger "2.0" "$(pv v2_swagger)" check_eq $P/v3-openapi "3.0.2" "$(pv v3_openapi)" check_eq $P/v2-no-openapi-key "no" "$(pv v2_has_openapi_key)" check_eq $P/v3-no-swagger-key "no" "$(pv v3_has_swagger_key)" # COMPONENTS MOVE BY NESTING. The schema itself is byte-for-byte the same object # in both documents -- only where it hangs differs, which is exactly the claim. check_eq $P/v2-definitions \ '{"Pet":{"properties":{"id":{"format":"int64","type":"integer"},"name":{"type":"string"}},"required":["id"],"type":"object"}}' \ "$(pv v2_definitions)" check_eq $P/v3-components-schemas \ '{"Pet":{"properties":{"id":{"format":"int64","type":"integer"},"name":{"type":"string"}},"required":["id"],"type":"object"}}' \ "$(pv v3_components_schemas)" check_eq $P/v2-no-components-key "no" "$(pv v2_has_components_key)" check_eq $P/v3-no-definitions-key "no" "$(pv v3_has_definitions_key)" check_eq $P/v2-security-definitions \ '{"apiKey":{"in":"header","name":"X-API-Key","type":"apiKey"}}' \ "$(pv v2_security_definitions)" check_eq $P/v3-security-schemes \ '{"apiKey":{"in":"header","name":"X-API-Key","type":"apiKey"}}' \ "$(pv v3_security_schemes)" # ------------------------------------------------- the $ref strings, exactly -- # THE CASE THAT CATCHES A VERSION-DISPATCH MISTAKE. The `components/' segment is # present at 3.x only, and the subsection name differs on top of that. check_eq $P/v3-ref-get-response "#/components/schemas/Pet" "$(pv v3_ref_get_response)" check_eq $P/v3-ref-request-body "#/components/schemas/Pet" "$(pv v3_ref_request_body)" check_eq $P/ref-build-v2 '{"$ref":"#/definitions/Pet"}' "$(pv ref_build_v2)" check_eq $P/ref-build-v3 '{"$ref":"#/components/schemas/Pet"}' "$(pv ref_build_v3)" check_eq $P/ref-build-v2-secscheme '{"$ref":"#/securityDefinitions/apiKey"}' \ "$(pv ref_build_v2_secscheme)" check_eq $P/ref-build-v3-secscheme '{"$ref":"#/components/securitySchemes/apiKey"}' \ "$(pv ref_build_v3_secscheme)" # THE HEADLINE STRUCTURAL FINDING -- see the header. The SAME response dict # resolves at 3.x and does NOT at 2.0, because a 2.0 response has no `content' # and the resolver correctly looks elsewhere. The consequence is a 2.0 document # with a bare component NAME where a `$ref' belongs, and no error saying so. check_eq $P/v2-response-unresolved '"Pet"' "$(pv v2_response_schema_unresolved)" check_eq $P/v2-response-v2shape-resolved '{"$ref":"#/definitions/Pet"}' \ "$(pv v2_response_v2shape_resolved)" # ...and the same defect from the other end: `requestBody' is a 3.x-only # construct, the resolver handles it with NO version check, so it survives into # a 2.0 document -- with a correctly-built `#/definitions/' ref inside it. check_eq $P/v2-requestbody-present "yes" "$(pv v2_requestbody_present)" check_eq $P/v2-requestbody-ref "#/definitions/Pet" "$(pv v2_requestbody_ref)" # --------------------------------------------------------------- parameters -- # A `path' PARAMETER IS FORCED REQUIRED whatever the caller wrote -- the # specification demands it and `_clean_parameters' enforces it. A `query' # parameter is left exactly as given, with no `required' key AT ALL, which is # the half that would silently regress if the enforcement were made # unconditional. check_eq $P/path-param-forced-required \ '{"in":"path","name":"petId","required":true,"schema":{"type":"integer"}}' \ "$(pv v3_path_param)" check_eq $P/query-param-untouched \ '{"in":"query","name":"verbose","schema":{"type":"boolean"}}' \ "$(pv v3_query_param)" check_eq $P/query-param-no-required-key "no" "$(pv v3_query_param_has_required)" # RESPONSE CODES ARE STRINGIFIED. The builder passed the INTS 200/404, and what # comes back are strings -- `_clean_operations' runs every code through `int()' # and then `str()', which is also how an `http.HTTPStatus' IntEnum normalises. check_eq $P/response-codes "200,404" "$(pv v3_get_response_codes)" check_eq $P/response-code-type "str" "$(pv v3_response_code_types)" # ============================================ basePath VERSUS servers ======== # NOT A VERSION DISPATCH -- see the header. Both keys are pass-through options # and apispec translates neither. check_eq $P/v2-basepath "/v1" "$(pv v2_basepath)" check_eq $P/v3-servers '[{"url":"https://example.test/v1"}]' "$(pv v3_servers)" check_eq $P/v2-no-servers-invented "no" "$(pv v2_has_servers)" # THE TICKET'S EXPECTATION, MEASURED AND WRONG: `basePath' handed to a 3.x spec # lands in the 3.x document verbatim. Nothing converts it and nothing rejects it. check_eq $P/v3-basepath-passes-through "/v1" "$(pv v3_has_basepath_when_given)" # `deepupdate' MERGES INTO SUBDICTS rather than replacing them, so an `info' # option is COMBINED with the constructor's title/version -- and wins on a # collision, which is how a caller overrides what they passed positionally. check_eq $P/info-option-merged '{"description":"d","title":"t","version":"1"}' \ "$(pv info_merged)" check_eq $P/info-option-overrides '{"title":"from-option","version":"1"}' \ "$(pv info_option_overrides)" # ==================================================================== YAML ==== # ABSENT ON THIS IMAGE, AND MEASURED RATHER THAN ASSUMED -- see the header. check_eq $P/yaml-absent "!ModuleNotFoundError" "$(pv yaml_importable)" check_eq $P/yaml-utils-absent "!ModuleNotFoundError" "$(pv yaml_utils_importable)" check_eq $P/to-yaml-raises "!ModuleNotFoundError" "$(pv to_yaml_result)" # THE FAILURE IS AT CALL TIME, NOT A MISSING ATTRIBUTE -- which is the # difference between "this build left it out" and "the optional extra is not # installed". The lazy import is what keeps the rest of the package working. check_eq $P/to-yaml-still-callable "yes" "$(pv to_yaml_is_callable)" # THE ROUND-TRIP, THROUGH THE PARSER THIS IMAGE HAS. A SUBSTITUTION for the # ticket's to_yaml() round-trip, stated as one in the header: it asserts that # `to_dict()' returns a plain, fully-serialisable tree with no library objects # left in it. A stray `Version' or a tuple fails here. check_eq $P/json-roundtrip "yes" "$(pv json_roundtrip_equal)" check_eq $P/json-roundtrip-v2 "yes" "$(pv json_roundtrip_equal_v2)" # `apispec.ext.marshmallow' SHIPS AND CANNOT IMPORT -- the files are on the # rootfs, so this is an absent EXTRA rather than a packaging fault. `apispec.ext' # itself imports fine, which locates the failure precisely at marshmallow. check_eq $P/marshmallow-absent "!ModuleNotFoundError" "$(pv marshmallow_importable)" check_eq $P/ext-marshmallow-absent "!ModuleNotFoundError" "$(pv ext_marshmallow_importable)" check_eq $P/ext-imports "ok" "$(pv ext_importable)" # ================================================================= PLUGINS ==== # EVERY DOCUMENTED HOOK, AND THE ORDER THEY FIRE IN. `init_spec' at construction; # then one hook per component kind as it is registered; then `path_helper' # BEFORE `operation_helper', which is the order a framework plugin depends on -- # the path must be resolved before the operations hanging off it are rewritten. check_eq $P/plugin-init-spec "yes" "$(pv plugin_init_spec_called)" check_eq $P/plugin-hooks \ "init_spec,schema_helper:Thing,response_helper,parameter_helper:tid,header_helper,path_helper:/things,operation_helper:/things" \ "$(pv plugin_hooks)" check_eq $P/plugin-hook-count "7" "$(pv plugin_hook_count)" # THE HOOKS' RETURN VALUES REACH THE DOCUMENT -- the call alone would not prove # the plugin can actually contribute anything. check_eq $P/plugin-schema-effect '{"type":"object","x-from-schema-helper":"Thing"}' \ "$(pv plugin_schema_effect)" check_eq $P/plugin-response-effect '{"description":"e","x-from-response-helper":true}' \ "$(pv plugin_response_effect)" check_eq $P/plugin-parameter-effect \ '{"in":"path","name":"tid","required":true,"schema":{"type":"string"},"x-from-parameter-helper":true}' \ "$(pv plugin_parameter_effect)" check_eq $P/plugin-header-effect '{"schema":{"type":"integer"},"x-from-header-helper":true}' \ "$(pv plugin_header_effect)" # A `path_helper' RETURNING A STRING SETS THE PATH: the documented way a # framework plugin turns a view object into a URL. The original path is GONE. check_eq $P/plugin-path-rewrite "/rewritten" "$(pv plugin_path_rewrite)" # A PLUGIN THAT IMPLEMENTS NOTHING IS NOT AN ERROR. `BasePlugin' raises # PluginMethodNotImplementedError from every hook and the core catches EXACTLY # that and continues -- which is the mechanism that makes the hooks optional, # and it is asserted from both ends. check_eq $P/bare-plugin-harmless '{"type":"object"}' "$(pv bare_plugin_ok)" check_eq $P/bare-hook-raises "!PluginMethodNotImplementedError" "$(pv bare_hook_raises)" check_eq $P/plugin-error-is-notimplementederror "yes" \ "$(pv plugin_error_is_notimplemented)" # ============================================================= ERROR PATHS ==== # A DUPLICATE COMPONENT NAME, for every kind that guards against one. `example' # and `security_scheme' are included because they take a DIFFERENT code path -- # neither runs plugin helpers -- so a regression could land on them alone. check_eq $P/dup-schema "!DuplicateComponentNameError" "$(pv dup_schema)" check_eq $P/dup-response "!DuplicateComponentNameError" "$(pv dup_response)" check_eq $P/dup-parameter "!DuplicateComponentNameError" "$(pv dup_parameter)" check_eq $P/dup-header "!DuplicateComponentNameError" "$(pv dup_header)" check_eq $P/dup-example "!DuplicateComponentNameError" "$(pv dup_example)" check_eq $P/dup-security-scheme "!DuplicateComponentNameError" "$(pv dup_security_scheme)" check_eq $P/dup-error-is-apispecerror "yes" "$(pv dup_error_is_apispecerror)" # AN UNKNOWN OpenAPI VERSION FAILS TWO DIFFERENT WAYS -- see the header. Out of # range is apispec's; not-a-version is packaging's, and is NOT an APISpecError. check_eq $P/version-too-low "!APISpecError" "$(pv version_too_low)" check_eq $P/version-too-high "!APISpecError" "$(pv version_too_high)" check_eq $P/version-above-max "!APISpecError" "$(pv version_4_1)" check_eq $P/version-not-a-version "!InvalidVersion" "$(pv version_not_a_version)" check_eq $P/version-empty "!InvalidVersion" "$(pv version_empty)" # The bounds are INCLUSIVE at 2.0 and EXCLUSIVE at 4.0, so both edges are pinned. check_eq $P/version-2-0-accepted "2.0" "$(pv version_2_0_boundary)" check_eq $P/version-3-9-accepted "3.9.9" "$(pv version_3_9_accepted)" # A MALFORMED OPERATION. An HTTP method the version does not define is an # APISpecError -- and `trace' makes it a VERSION question rather than a spelling # one: accepted at 3.x, refused at 2.0, from the same input. check_eq $P/bad-method "!APISpecError" "$(pv bad_method)" check_eq $P/trace-ok-v3 "trace" "$(pv trace_ok_v3)" check_eq $P/trace-bad-v2 "!APISpecError" "$(pv trace_bad_v2)" # AN `x-' EXTENSION KEY IS NOT A METHOD AND IS NOT REJECTED -- the documented # escape hatch, and the reason `bad-method' above had to use a name that does # not begin `x-'. check_eq $P/x-extension-allowed "get,x-mine" "$(pv x_extension_allowed)" # NO PATH AT ALL: with no plugin to supply one, `path()' refuses rather than # emitting a document with an empty key. check_eq $P/no-path "!APISpecError" "$(pv no_path)" # PARAMETER FAULTS ARE THREE DISTINCT TYPES, all under APISpecError. Missing # keys and a duplicate are different mistakes and report differently. check_eq $P/param-missing-in "!InvalidParameterError" "$(pv param_missing_keys)" check_eq $P/param-missing-both "!InvalidParameterError" "$(pv param_missing_both)" check_eq $P/param-duplicate "!DuplicateParameterError" "$(pv param_duplicate)" # UNIQUENESS IS THE (name, in) PAIR, which is what the specification says -- so # the same name in a different location is legal and BOTH survive. check_eq $P/param-same-name-other-location "2" "$(pv param_same_name_other_location)" check_eq $P/param-errors-are-apispecerror "yes" "$(pv param_errors_are_apispecerror)" # A NON-INTEGER RESPONSE CODE UNDER 2.0 WARNS RATHER THAN RAISING, and the # document KEEPS the code as written -- so a typo becomes a silently malformed # document rather than a failure. `default' is exempt: it is a legal OpenAPI 2 # code and must not warn. check_eq $P/noninteger-code-warns "yes" "$(pv noninteger_code_warned)" check_eq $P/noninteger-code-category "UserWarning" "$(pv noninteger_code_category)" check_eq $P/noninteger-code-kept "nope" "$(pv noninteger_code_kept)" check_eq $P/default-code-no-warning "yes" "$(pv default_code_not_warned)" # ========================================================= LAZY COMPONENTS ==== # `lazy=True' REGISTERS ONLY WHEN SOMETHING REFERS TO THE COMPONENT, which is # how a plugin can offer a large catalogue without putting all of it in every # document. Asserted from both sides: the referenced one is there and the # unreferenced one is not. check_eq $P/lazy-registered "Used" "$(pv lazy_registered)" check_eq $P/lazy-unused-absent "yes" "$(pv lazy_unused_absent)" # ============================================================ THE DOCUMENT ==== # AN EMPTY SPEC OMITS `components' ENTIRELY at 3.x rather than emitting an empty # dict -- which matters to a strict validator, and is a one-line change away # from regressing. check_eq $P/empty-document '{"info":{"title":"e","version":"1"},"openapi":"3.0.2","paths":{}}' \ "$(pv empty_doc)" check_eq $P/empty-no-components-key "no" "$(pv empty_has_components)" check_eq $P/empty-document-v2 '{"info":{"title":"e","version":"1"},"paths":{},"swagger":"2.0"}' \ "$(pv empty_v2_doc)" # `to_dict()' IS RECOMPUTED, NOT CACHED: a path added after a first call appears # in the second. A caller who renders early and then keeps building depends on # this. check_eq $P/to-dict-recomputed "yes" "$(pv to_dict_recomputed)" check_eq $P/to-dict-sees-late-path "/late" "$(pv to_dict_not_cached)" # CALLING `path()' TWICE ON ONE PATH MERGES the operations rather than replacing # them -- `setdefault(...).update(...)' -- which is what lets two decorators on # two views contribute to the same path item. check_eq $P/path-called-twice-merges "get,post" "$(pv path_twice_merges)" # THE FLUENT API RETURNS THE OBJECT ITSELF. `components.schema()' returns the # COMPONENTS object, not the spec -- so a chain that mixes the two levels does # not work, and this is what says which is which. check_eq $P/tag-returns-self "yes" "$(pv tag_returns_self)" check_eq $P/path-returns-self "yes" "$(pv path_returns_self)" check_eq $P/schema-returns-components "yes" "$(pv schema_returns_components)" # THE CALLER'S OWN DICTS ARE NOT MUTATED. `path()' and `schema()' deepcopy their # input, so the int response codes the caller passed are still ints in the # caller's dict after the spec has stringified its own copy -- which is what # makes it safe to reuse one operations dict across two specs, as the two # `build()' calls above do. check_eq $P/caller-ops-unmutated "200:int" "$(pv caller_ops_unmutated)" check_eq $P/caller-schema-unmutated '{"properties":{"a":{"type":"string"}},"type":"object"}' \ "$(pv caller_schema_unmutated)" rm -rf "$W"