diff --git a/.sdkharness/README.md b/.sdkharness/README.md index 0bfe3de1..00e0405a 100644 --- a/.sdkharness/README.md +++ b/.sdkharness/README.md @@ -21,7 +21,9 @@ only the fixed simulator credential `test-key-id` / `test-key`, and the health check refuses any other `B2_TEST_APPLICATION_KEY*` pair before it reaches the SDK. The individual conformance and resilience check files refuse to run at all unless they are given a loopback simulator URL, so run them through the -dispatchers below, never with real `B2_*` values in the environment. +dispatchers below, never with real `B2_*` values in the environment. The +dispatchers also drop `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY` (any case) and pin +`NO_PROXY` to `127.0.0.1`, so a proxy configured on the machine cannot change a result. Conformance owns 33 capability checks and resilience owns 16 injected-fault checks. Their small dispatchers validate the invocation, run the selected @@ -83,5 +85,12 @@ HEALTHCHECK_REALM_URL=$SDKHARNESS_SIMULATOR_URL B2_TEST_APPLICATION_KEY_ID=test- When you are done, stop the simulator with `kill "$SIM_PID"`. Each prints one `SDKHARNESS_RESULT` line. Scenario ids are in `tests.tsv`. -A `FAIL` for `api.retry_after_503`, `upload.retry_408` is a known SDK finding, not a setup problem. - +`api.retry_after_503` and `upload.retry_408` used to FAIL as known SDK findings; +both pass now, so there are no standing known findings listed here. `upload.stall` +reports `SKIP` because b2sdk documents no request-timeout option. A `FAIL` is a +real SDK or harness defect (or a simulator older than the checks expect), not a +setup problem. `SKIP` is reserved for a question that genuinely cannot be asked +(`no-realm-option`, `missing-runtime` when `b2sdk` is not installed at all, +`no-client-option`, `not-claimed`). An unreachable simulator, an authorization +error, an access-denied error, a connection error, or an SDK `ImportError` is +always a `FAIL`. diff --git a/.sdkharness/tests/conformance/bucket.cors b/.sdkharness/tests/conformance/bucket.cors index d370b27d..1b8b1a29 100755 --- a/.sdkharness/tests/conformance/bucket.cors +++ b/.sdkharness/tests/conformance/bucket.cors @@ -7,12 +7,12 @@ claims it): Action set a CORS rule set, then read it back Assert field-for-field equality - The card answers `partial` for this cell and states the boundary: CORS + The capability record answers `partial` for this cell and states the boundary: CORS round-trips as an UNTYPED dict with no typed model and no dedicated accessor, unlike lifecycle and notification rules. That boundary is the most falsifiable claim in the matrix, so it is asserted here too -- a typed CorsRule or a get/set_cors_rules accessor appearing upstream FAILS this - check, because the card would then be stale. + check, because the record would then be stale. EXPECTED VALUES -- B2's published contract, reviewed 2026-09-23. Every field sent here is a documented rule field (corsRuleName, allowedOrigins, @@ -24,13 +24,12 @@ b2_list_buckets returns corsRules. https://www.backblaze.com/docs/cloud-storage-cross-origin-resource-sharing-rules https://www.backblaze.com/apidocs/b2-update-bucket https://www.backblaze.com/apidocs/b2-list-buckets -The `partial` boundary leg tests the card's claim about b2sdk's own surface; +The `partial` boundary leg tests the capability's recorded claim about b2sdk's own surface; B2's docs do not speak to it. -Written from bin/conformance/b2-sdk-python/files.upload, the reference check, -and bin/conformance/README.md, the contract. It exercises b2sdk from this exact repository -checkout; the API was taken from -docs/sdks/b2-sdk-python/card.md and the published b2sdk docs; the assertion now lives with the SDK code it exercises. +Written from files.upload in this directory, the reference check. It exercises +b2sdk from this exact repository checkout; the API usage is taken from the +published b2sdk documentation. """ import os @@ -38,7 +37,7 @@ import sys import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' CAPABILITY = 'bucket.cors' @@ -51,8 +50,10 @@ RULE = { 'maxAgeSeconds': 3600, } -# The closed reason set, mirrored from bin/conformance/README.md. The runner -# enforces it; naming one outside this tuple is a FAIL it manufactures. +# The closed reason set. `lib/contract.py` only turns `no-realm-option`, +# `missing-runtime` and `not-claimed` into a SKIP. `unreachable` and +# `unauthorized` are never reported at the simulator: the runner supplies both +# the server and the credential, so those are FAIL, not amber. REASONS = ( 'no-realm-option', 'unreachable', @@ -63,7 +64,7 @@ REASONS = ( ) # A requested target maps to a realm or to amber. `simulator` is the harness's -# own server (bin/simulator/serve.mjs), whose URL the runner exports as +# own loopback server, whose URL the dispatcher exports as # CONFORMANCE_SIMULATOR_URL; unset, there is nothing to reach: no-realm-option. REALMS = { 'simulator': os.environ.get('CONFORMANCE_SIMULATOR_URL') or None, @@ -92,7 +93,7 @@ class Amber(Exception): class Failure(Exception): - """A golden-path step that did not do what the card claims.""" + """A golden-path step that did not do what the capability requires.""" def __init__(self, step: str, detail: str) -> None: self.step = step @@ -116,8 +117,10 @@ def note(text: str) -> None: def step(name, action): """Run one golden-path step, converting the environment's own errors. - A credential NEVER reaches the detail: only the exception class name and, - for a classified network/auth error, the class that classified it. + A credential NEVER reaches the detail: only the exception class name. + Every error here is a FAIL: the runner supplies both the simulator and the + credential, so an auth error, access denied, connection error or SDK error + is a defect, never an amber (COULD-NOT-RUN) result. """ try: return action() @@ -125,10 +128,6 @@ def step(name, action): raise except Exception as error: # noqa: BLE001 -- classified below, never echoed name_of = type(error).__name__ - if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f"{name_of} at step '{name}'") from error - if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'): - raise Amber('unreachable', f"{name_of} at step '{name}'") from error raise Failure(name, name_of) from error @@ -173,6 +172,9 @@ def prepare(target): try: import b2sdk.v3 as b2 except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error return b2, realm @@ -280,18 +282,18 @@ def run(target: str) -> None: if hasattr(b2, 'CorsRule'): raise Failure( 'boundary', - "b2sdk.v3 now exports a typed CorsRule; the card's `partial` answer is stale", + "b2sdk.v3 now exports a typed CorsRule; the capability's `partial` answer is stale", ) for accessor in ('get_cors_rules', 'set_cors_rules'): if hasattr(b2.Bucket, accessor): raise Failure( 'boundary', - f"Bucket.{accessor} now exists; the card's `partial` answer is stale", + f"Bucket.{accessor} now exists; the capability's `partial` answer is stale", ) if type(rule) is not dict: raise Failure( 'boundary', - f'the CORS rule is a {type(rule).__name__}, not the untyped dict the card records', + f'the CORS rule is a {type(rule).__name__}, not the untyped dict the record names', ) note('boundary held: CORS round-trips as a plain dict, with no typed model and no accessor') diff --git a/.sdkharness/tests/conformance/bucket.crud b/.sdkharness/tests/conformance/bucket.crud index de5cd114..0a687443 100755 --- a/.sdkharness/tests/conformance/bucket.crud +++ b/.sdkharness/tests/conformance/bucket.crud @@ -20,10 +20,9 @@ https://www.backblaze.com/apidocs/b2-update-bucket https://www.backblaze.com/apidocs/b2-delete-bucket https://www.backblaze.com/apidocs/b2-list-buckets -Written from bin/conformance/b2-sdk-python/files.upload, the reference check, -and bin/conformance/README.md, the contract. It exercises b2sdk from this exact repository -checkout; the API was taken from -docs/sdks/b2-sdk-python/card.md and the published b2sdk docs; the assertion now lives with the SDK code it exercises. +Written from files.upload in this directory, the reference check. It exercises +b2sdk from this exact repository checkout; the API usage is taken from the +published b2sdk documentation. """ import os @@ -31,13 +30,15 @@ import sys import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' CAPABILITY = 'bucket.crud' -# The closed reason set, mirrored from bin/conformance/README.md. The runner -# enforces it; naming one outside this tuple is a FAIL it manufactures. +# The closed reason set. `lib/contract.py` only turns `no-realm-option`, +# `missing-runtime` and `not-claimed` into a SKIP. `unreachable` and +# `unauthorized` are never reported at the simulator: the runner supplies both +# the server and the credential, so those are FAIL, not amber. REASONS = ( 'no-realm-option', 'unreachable', @@ -48,7 +49,7 @@ REASONS = ( ) # A requested target maps to a realm or to amber. `simulator` is the harness's -# own server (bin/simulator/serve.mjs), whose URL the runner exports as +# own loopback server, whose URL the dispatcher exports as # CONFORMANCE_SIMULATOR_URL; unset, there is nothing to reach: no-realm-option. REALMS = { 'simulator': os.environ.get('CONFORMANCE_SIMULATOR_URL') or None, @@ -77,7 +78,7 @@ class Amber(Exception): class Failure(Exception): - """A golden-path step that did not do what the card claims.""" + """A golden-path step that did not do what the capability requires.""" def __init__(self, step: str, detail: str) -> None: self.step = step @@ -101,8 +102,10 @@ def note(text: str) -> None: def step(name, action): """Run one golden-path step, converting the environment's own errors. - A credential NEVER reaches the detail: only the exception class name and, - for a classified network/auth error, the class that classified it. + A credential NEVER reaches the detail: only the exception class name. + Every error here is a FAIL: the runner supplies both the simulator and the + credential, so an auth error, access denied, connection error or SDK error + is a defect, never an amber (COULD-NOT-RUN) result. """ try: return action() @@ -110,10 +113,6 @@ def step(name, action): raise except Exception as error: # noqa: BLE001 -- classified below, never echoed name_of = type(error).__name__ - if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f"{name_of} at step '{name}'") from error - if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'): - raise Amber('unreachable', f"{name_of} at step '{name}'") from error raise Failure(name, name_of) from error @@ -158,6 +157,9 @@ def prepare(target): try: import b2sdk.v3 as b2 except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error return b2, realm diff --git a/.sdkharness/tests/conformance/bucket.lifecycle b/.sdkharness/tests/conformance/bucket.lifecycle index a8246e43..5013e4d7 100755 --- a/.sdkharness/tests/conformance/bucket.lifecycle +++ b/.sdkharness/tests/conformance/bucket.lifecycle @@ -18,10 +18,9 @@ list." https://www.backblaze.com/docs/cloud-storage-lifecycle-rules https://www.backblaze.com/apidocs/b2-update-bucket -Written from bin/conformance/b2-sdk-python/files.upload, the reference check, -and bin/conformance/README.md, the contract. It exercises b2sdk from this exact repository -checkout; the API was taken from -docs/sdks/b2-sdk-python/card.md and the published b2sdk docs; the assertion now lives with the SDK code it exercises. +Written from files.upload in this directory, the reference check. It exercises +b2sdk from this exact repository checkout; the API usage is taken from the +published b2sdk documentation. """ import os @@ -29,15 +28,17 @@ import sys import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' CAPABILITY = 'bucket.lifecycle' RULE = {'fileNamePrefix': 'st/', 'daysFromHidingToDeleting': 1} OTHER_FIELDS = ('daysFromUploadingToHiding', 'daysFromStartingToCancelingUnfinishedLargeFiles') -# The closed reason set, mirrored from bin/conformance/README.md. The runner -# enforces it; naming one outside this tuple is a FAIL it manufactures. +# The closed reason set. `lib/contract.py` only turns `no-realm-option`, +# `missing-runtime` and `not-claimed` into a SKIP. `unreachable` and +# `unauthorized` are never reported at the simulator: the runner supplies both +# the server and the credential, so those are FAIL, not amber. REASONS = ( 'no-realm-option', 'unreachable', @@ -48,7 +49,7 @@ REASONS = ( ) # A requested target maps to a realm or to amber. `simulator` is the harness's -# own server (bin/simulator/serve.mjs), whose URL the runner exports as +# own loopback server, whose URL the dispatcher exports as # CONFORMANCE_SIMULATOR_URL; unset, there is nothing to reach: no-realm-option. REALMS = { 'simulator': os.environ.get('CONFORMANCE_SIMULATOR_URL') or None, @@ -77,7 +78,7 @@ class Amber(Exception): class Failure(Exception): - """A golden-path step that did not do what the card claims.""" + """A golden-path step that did not do what the capability requires.""" def __init__(self, step: str, detail: str) -> None: self.step = step @@ -101,8 +102,10 @@ def note(text: str) -> None: def step(name, action): """Run one golden-path step, converting the environment's own errors. - A credential NEVER reaches the detail: only the exception class name and, - for a classified network/auth error, the class that classified it. + A credential NEVER reaches the detail: only the exception class name. + Every error here is a FAIL: the runner supplies both the simulator and the + credential, so an auth error, access denied, connection error or SDK error + is a defect, never an amber (COULD-NOT-RUN) result. """ try: return action() @@ -110,10 +113,6 @@ def step(name, action): raise except Exception as error: # noqa: BLE001 -- classified below, never echoed name_of = type(error).__name__ - if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f"{name_of} at step '{name}'") from error - if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'): - raise Amber('unreachable', f"{name_of} at step '{name}'") from error raise Failure(name, name_of) from error @@ -158,6 +157,9 @@ def prepare(target): try: import b2sdk.v3 as b2 except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error return b2, realm diff --git a/.sdkharness/tests/conformance/bucket.notification_rules b/.sdkharness/tests/conformance/bucket.notification_rules index 62f69093..f0b2da92 100755 --- a/.sdkharness/tests/conformance/bucket.notification_rules +++ b/.sdkharness/tests/conformance/bucket.notification_rules @@ -16,10 +16,9 @@ https://www.backblaze.com/apidocs/b2-get-bucket-notification-rules The 2026-09-22 row asserted the secret is NOT echoed, which contradicts the docs. The secret is generated here and compared, never printed. -Written from bin/conformance/b2-sdk-python/files.upload, the reference check, -and bin/conformance/README.md, the contract. It exercises b2sdk from this exact repository -checkout; the API was taken from -docs/sdks/b2-sdk-python/card.md and the published b2sdk docs; the assertion now lives with the SDK code it exercises. +Written from files.upload in this directory, the reference check. It exercises +b2sdk from this exact repository checkout; the API usage is taken from the +published b2sdk documentation. """ import os @@ -29,7 +28,7 @@ import sys import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' CAPABILITY = 'bucket.notification_rules' @@ -37,8 +36,10 @@ RULE_NAME = 'sdkharness-conf' EVENT_TYPES = ['b2:ObjectCreated:*'] WEBHOOK_URL = 'https://example.com/sdkharness-conf' -# The closed reason set, mirrored from bin/conformance/README.md. The runner -# enforces it; naming one outside this tuple is a FAIL it manufactures. +# The closed reason set. `lib/contract.py` only turns `no-realm-option`, +# `missing-runtime` and `not-claimed` into a SKIP. `unreachable` and +# `unauthorized` are never reported at the simulator: the runner supplies both +# the server and the credential, so those are FAIL, not amber. REASONS = ( 'no-realm-option', 'unreachable', @@ -49,7 +50,7 @@ REASONS = ( ) # A requested target maps to a realm or to amber. `simulator` is the harness's -# own server (bin/simulator/serve.mjs), whose URL the runner exports as +# own loopback server, whose URL the dispatcher exports as # CONFORMANCE_SIMULATOR_URL; unset, there is nothing to reach: no-realm-option. REALMS = { 'simulator': os.environ.get('CONFORMANCE_SIMULATOR_URL') or None, @@ -78,7 +79,7 @@ class Amber(Exception): class Failure(Exception): - """A golden-path step that did not do what the card claims.""" + """A golden-path step that did not do what the capability requires.""" def __init__(self, step: str, detail: str) -> None: self.step = step @@ -102,8 +103,10 @@ def note(text: str) -> None: def step(name, action): """Run one golden-path step, converting the environment's own errors. - A credential NEVER reaches the detail: only the exception class name and, - for a classified network/auth error, the class that classified it. + A credential NEVER reaches the detail: only the exception class name. + Every error here is a FAIL: the runner supplies both the simulator and the + credential, so an auth error, access denied, connection error or SDK error + is a defect, never an amber (COULD-NOT-RUN) result. """ try: return action() @@ -111,10 +114,6 @@ def step(name, action): raise except Exception as error: # noqa: BLE001 -- classified below, never echoed name_of = type(error).__name__ - if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f"{name_of} at step '{name}'") from error - if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'): - raise Amber('unreachable', f"{name_of} at step '{name}'") from error raise Failure(name, name_of) from error @@ -159,6 +158,9 @@ def prepare(target): try: import b2sdk.v3 as b2 except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error return b2, realm diff --git a/.sdkharness/tests/conformance/bucket.replication_config b/.sdkharness/tests/conformance/bucket.replication_config index 5d5d95c8..19d7d009 100755 --- a/.sdkharness/tests/conformance/bucket.replication_config +++ b/.sdkharness/tests/conformance/bucket.replication_config @@ -18,10 +18,9 @@ rules associated with one or more buckets." https://www.backblaze.com/apidocs/b2-update-bucket https://www.backblaze.com/docs/cloud-storage-create-a-cloud-replication-rule-with-the-native-api -Written from bin/conformance/b2-sdk-python/files.upload, the reference check, -and bin/conformance/README.md, the contract. It exercises b2sdk from this exact repository -checkout; the API was taken from -docs/sdks/b2-sdk-python/card.md and the published b2sdk docs; the assertion now lives with the SDK code it exercises. +Written from files.upload in this directory, the reference check. It exercises +b2sdk from this exact repository checkout; the API usage is taken from the +published b2sdk documentation. """ import os @@ -29,7 +28,7 @@ import sys import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' CAPABILITY = 'bucket.replication_config' @@ -48,8 +47,10 @@ SOURCE_CAPABILITIES = [ 'writeBucketReplications', ] -# The closed reason set, mirrored from bin/conformance/README.md. The runner -# enforces it; naming one outside this tuple is a FAIL it manufactures. +# The closed reason set. `lib/contract.py` only turns `no-realm-option`, +# `missing-runtime` and `not-claimed` into a SKIP. `unreachable` and +# `unauthorized` are never reported at the simulator: the runner supplies both +# the server and the credential, so those are FAIL, not amber. REASONS = ( 'no-realm-option', 'unreachable', @@ -60,7 +61,7 @@ REASONS = ( ) # A requested target maps to a realm or to amber. `simulator` is the harness's -# own server (bin/simulator/serve.mjs), whose URL the runner exports as +# own loopback server, whose URL the dispatcher exports as # CONFORMANCE_SIMULATOR_URL; unset, there is nothing to reach: no-realm-option. REALMS = { 'simulator': os.environ.get('CONFORMANCE_SIMULATOR_URL') or None, @@ -89,7 +90,7 @@ class Amber(Exception): class Failure(Exception): - """A golden-path step that did not do what the card claims.""" + """A golden-path step that did not do what the capability requires.""" def __init__(self, step: str, detail: str) -> None: self.step = step @@ -113,8 +114,10 @@ def note(text: str) -> None: def step(name, action): """Run one golden-path step, converting the environment's own errors. - A credential NEVER reaches the detail: only the exception class name and, - for a classified network/auth error, the class that classified it. + A credential NEVER reaches the detail: only the exception class name. + Every error here is a FAIL: the runner supplies both the simulator and the + credential, so an auth error, access denied, connection error or SDK error + is a defect, never an amber (COULD-NOT-RUN) result. """ try: return action() @@ -122,10 +125,6 @@ def step(name, action): raise except Exception as error: # noqa: BLE001 -- classified below, never echoed name_of = type(error).__name__ - if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f"{name_of} at step '{name}'") from error - if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'): - raise Amber('unreachable', f"{name_of} at step '{name}'") from error raise Failure(name, name_of) from error @@ -170,6 +169,9 @@ def prepare(target): try: import b2sdk.v3 as b2 except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error return b2, realm diff --git a/.sdkharness/tests/conformance/bucket.replication_helper b/.sdkharness/tests/conformance/bucket.replication_helper index 34483db0..ddb9a690 100755 --- a/.sdkharness/tests/conformance/bucket.replication_helper +++ b/.sdkharness/tests/conformance/bucket.replication_helper @@ -22,10 +22,9 @@ ReplicationMonitor entry; counter_by_status appears only under b2sdk._internal search for the helper names. So "the status reports the expected counts" has no documented expected value; it is asserted only as at least one file counted. -Written from bin/conformance/b2-sdk-python/files.upload, the reference check, -and bin/conformance/README.md, the contract. It exercises b2sdk from this exact repository -checkout; the API was taken from -docs/sdks/b2-sdk-python/card.md and the published b2sdk docs; the assertion now lives with the SDK code it exercises. +Written from files.upload in this directory, the reference check. It exercises +b2sdk from this exact repository checkout; the API usage is taken from the +published b2sdk documentation. """ import io @@ -34,7 +33,7 @@ import sys import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' CAPABILITY = 'bucket.replication_helper' @@ -43,8 +42,10 @@ PREFIX = 'st/' OBJECT_NAME = 'st/replicated.txt' PAYLOAD_SIZE = 256 -# The closed reason set, mirrored from bin/conformance/README.md. The runner -# enforces it; naming one outside this tuple is a FAIL it manufactures. +# The closed reason set. `lib/contract.py` only turns `no-realm-option`, +# `missing-runtime` and `not-claimed` into a SKIP. `unreachable` and +# `unauthorized` are never reported at the simulator: the runner supplies both +# the server and the credential, so those are FAIL, not amber. REASONS = ( 'no-realm-option', 'unreachable', @@ -55,7 +56,7 @@ REASONS = ( ) # A requested target maps to a realm or to amber. `simulator` is the harness's -# own server (bin/simulator/serve.mjs), whose URL the runner exports as +# own loopback server, whose URL the dispatcher exports as # CONFORMANCE_SIMULATOR_URL; unset, there is nothing to reach: no-realm-option. REALMS = { 'simulator': os.environ.get('CONFORMANCE_SIMULATOR_URL') or None, @@ -84,7 +85,7 @@ class Amber(Exception): class Failure(Exception): - """A golden-path step that did not do what the card claims.""" + """A golden-path step that did not do what the capability requires.""" def __init__(self, step: str, detail: str) -> None: self.step = step @@ -108,8 +109,10 @@ def note(text: str) -> None: def step(name, action): """Run one golden-path step, converting the environment's own errors. - A credential NEVER reaches the detail: only the exception class name and, - for a classified network/auth error, the class that classified it. + A credential NEVER reaches the detail: only the exception class name. + Every error here is a FAIL: the runner supplies both the simulator and the + credential, so an auth error, access denied, connection error or SDK error + is a defect, never an amber (COULD-NOT-RUN) result. """ try: return action() @@ -117,10 +120,6 @@ def step(name, action): raise except Exception as error: # noqa: BLE001 -- classified below, never echoed name_of = type(error).__name__ - if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f"{name_of} at step '{name}'") from error - if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'): - raise Amber('unreachable', f"{name_of} at step '{name}'") from error raise Failure(name, name_of) from error @@ -165,6 +164,9 @@ def prepare(target): try: import b2sdk.v3 as b2 except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error return b2, realm diff --git a/.sdkharness/tests/conformance/client.auth_persistence b/.sdkharness/tests/conformance/client.auth_persistence index 46fb5b40..65e835c6 100755 --- a/.sdkharness/tests/conformance/client.auth_persistence +++ b/.sdkharness/tests/conformance/client.auth_persistence @@ -21,10 +21,9 @@ cache. The nearest documented fact bounds, but does not promise, the reuse: b2_authorize_account's token "is valid for at most 24 hours." https://www.backblaze.com/apidocs/b2-authorize-account -Written from bin/conformance/b2-sdk-python/files.upload, the reference check, -and bin/conformance/README.md, the contract. It exercises b2sdk from this exact repository -checkout; the API was taken from -docs/sdks/b2-sdk-python/card.md and the published b2sdk docs; the assertion now lives with the SDK code it exercises. +Written from files.upload in this directory, the reference check. It exercises +b2sdk from this exact repository checkout; the API usage is taken from the +published b2sdk documentation. """ import os @@ -35,13 +34,15 @@ import tempfile import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' CAPABILITY = 'client.auth_persistence' -# The closed reason set, mirrored from bin/conformance/README.md. The runner -# enforces it; naming one outside this tuple is a FAIL it manufactures. +# The closed reason set. `lib/contract.py` only turns `no-realm-option`, +# `missing-runtime` and `not-claimed` into a SKIP. `unreachable` and +# `unauthorized` are never reported at the simulator: the runner supplies both +# the server and the credential, so those are FAIL, not amber. REASONS = ( 'no-realm-option', 'unreachable', @@ -52,7 +53,7 @@ REASONS = ( ) # A requested target maps to a realm or to amber. `simulator` is the harness's -# own server (bin/simulator/serve.mjs), whose URL the runner exports as +# own loopback server, whose URL the dispatcher exports as # CONFORMANCE_SIMULATOR_URL; unset, there is nothing to reach: no-realm-option. REALMS = { 'simulator': os.environ.get('CONFORMANCE_SIMULATOR_URL') or None, @@ -81,7 +82,7 @@ class Amber(Exception): class Failure(Exception): - """A golden-path step that did not do what the card claims.""" + """A golden-path step that did not do what the capability requires.""" def __init__(self, step: str, detail: str) -> None: self.step = step @@ -105,8 +106,10 @@ def note(text: str) -> None: def step(name, action): """Run one golden-path step, converting the environment's own errors. - A credential NEVER reaches the detail: only the exception class name and, - for a classified network/auth error, the class that classified it. + A credential NEVER reaches the detail: only the exception class name. + Every error here is a FAIL: the runner supplies both the simulator and the + credential, so an auth error, access denied, connection error or SDK error + is a defect, never an amber (COULD-NOT-RUN) result. """ try: return action() @@ -114,10 +117,6 @@ def step(name, action): raise except Exception as error: # noqa: BLE001 -- classified below, never echoed name_of = type(error).__name__ - if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f"{name_of} at step '{name}'") from error - if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'): - raise Amber('unreachable', f"{name_of} at step '{name}'") from error raise Failure(name, name_of) from error @@ -162,6 +161,9 @@ def prepare(target): try: import b2sdk.v3 as b2 except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error return b2, realm @@ -304,18 +306,6 @@ def run(target: str) -> None: if marker.startswith('CHILD error '): failed_with = marker[len('CHILD error ') :].strip() - if failed_with in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f'{failed_with} in process 2') - if failed_with in ( - 'gaierror', - 'ConnectionError', - 'ServiceError', - 'B2ConnectionError', - 'UnknownHost', - ): - raise Amber('unreachable', f'{failed_with} in process 2') - if failed_with in ('ImportError', 'ModuleNotFoundError'): - raise Amber('missing-runtime', 'b2sdk is not installed for the second process') raise Failure('process 2', f'the second process failed with {failed_with}') fields = dict(part.split('=', 1) for part in marker.split() if '=' in part) diff --git a/.sdkharness/tests/conformance/client.progress b/.sdkharness/tests/conformance/client.progress index 8c39d335..b085a074 100755 --- a/.sdkharness/tests/conformance/client.progress +++ b/.sdkharness/tests/conformance/client.progress @@ -18,10 +18,9 @@ Progress reporting is client-side ergonomics; the B2 API has no progress surface, so the expected values (fires, non-decreasing, ends at the byte count) come from the scenario, not a Backblaze doc. -Written from bin/conformance/b2-sdk-python/files.upload, the reference check, -and bin/conformance/README.md, the contract. It exercises b2sdk from this exact repository -checkout; the API was taken from -docs/sdks/b2-sdk-python/card.md and the published b2sdk docs; the assertion now lives with the SDK code it exercises. +Written from files.upload in this directory, the reference check. It exercises +b2sdk from this exact repository checkout; the API usage is taken from the +published b2sdk documentation. """ import os @@ -30,15 +29,17 @@ import threading import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' CAPABILITY = 'client.progress' OBJECT_NAME = 'st/progress.bin' PARTS_WANTED = 3 -# The closed reason set, mirrored from bin/conformance/README.md. The runner -# enforces it; naming one outside this tuple is a FAIL it manufactures. +# The closed reason set. `lib/contract.py` only turns `no-realm-option`, +# `missing-runtime` and `not-claimed` into a SKIP. `unreachable` and +# `unauthorized` are never reported at the simulator: the runner supplies both +# the server and the credential, so those are FAIL, not amber. REASONS = ( 'no-realm-option', 'unreachable', @@ -49,7 +50,7 @@ REASONS = ( ) # A requested target maps to a realm or to amber. `simulator` is the harness's -# own server (bin/simulator/serve.mjs), whose URL the runner exports as +# own loopback server, whose URL the dispatcher exports as # CONFORMANCE_SIMULATOR_URL; unset, there is nothing to reach: no-realm-option. REALMS = { 'simulator': os.environ.get('CONFORMANCE_SIMULATOR_URL') or None, @@ -78,7 +79,7 @@ class Amber(Exception): class Failure(Exception): - """A golden-path step that did not do what the card claims.""" + """A golden-path step that did not do what the capability requires.""" def __init__(self, step: str, detail: str) -> None: self.step = step @@ -102,8 +103,10 @@ def note(text: str) -> None: def step(name, action): """Run one golden-path step, converting the environment's own errors. - A credential NEVER reaches the detail: only the exception class name and, - for a classified network/auth error, the class that classified it. + A credential NEVER reaches the detail: only the exception class name. + Every error here is a FAIL: the runner supplies both the simulator and the + credential, so an auth error, access denied, connection error or SDK error + is a defect, never an amber (COULD-NOT-RUN) result. """ try: return action() @@ -111,10 +114,6 @@ def step(name, action): raise except Exception as error: # noqa: BLE001 -- classified below, never echoed name_of = type(error).__name__ - if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f"{name_of} at step '{name}'") from error - if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'): - raise Amber('unreachable', f"{name_of} at step '{name}'") from error raise Failure(name, name_of) from error @@ -159,6 +158,9 @@ def prepare(target): try: import b2sdk.v3 as b2 except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error return b2, realm diff --git a/.sdkharness/tests/conformance/client.simulator b/.sdkharness/tests/conformance/client.simulator index 1936bfa1..6c15a402 100755 --- a/.sdkharness/tests/conformance/client.simulator +++ b/.sdkharness/tests/conformance/client.simulator @@ -20,10 +20,9 @@ Shipping an in-process fake is packaging; no Backblaze doc page covers it. The fake models the same endpoints the API docs describe, but that is the tool's claim, so a PASS here says the tool and its own fake agree, nothing about B2. -Written from bin/conformance/b2-sdk-python/files.upload, the reference check, -and bin/conformance/README.md, the contract. It exercises b2sdk from this exact repository -checkout; the API was taken from -docs/sdks/b2-sdk-python/card.md and the published b2sdk docs; the assertion now lives with the SDK code it exercises. +Written from files.upload in this directory, the reference check. It exercises +b2sdk from this exact repository checkout; the API usage is taken from the +published b2sdk documentation. """ import io @@ -33,15 +32,17 @@ import sys import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' CAPABILITY = 'client.simulator' OBJECT_NAME = 'st/simulator.txt' PAYLOAD_SIZE = 1024 -# The closed reason set, mirrored from bin/conformance/README.md. The runner -# enforces it; naming one outside this tuple is a FAIL it manufactures. +# The closed reason set. `lib/contract.py` only turns `no-realm-option`, +# `missing-runtime` and `not-claimed` into a SKIP. `unreachable` and +# `unauthorized` are never reported at the simulator: the runner supplies both +# the server and the credential, so those are FAIL, not amber. REASONS = ( 'no-realm-option', 'unreachable', @@ -65,7 +66,7 @@ class Amber(Exception): class Failure(Exception): - """A golden-path step that did not do what the card claims.""" + """A golden-path step that did not do what the capability requires.""" def __init__(self, step: str, detail: str) -> None: self.step = step @@ -89,8 +90,10 @@ def note(text: str) -> None: def step(name, action): """Run one golden-path step, converting the environment's own errors. - A credential NEVER reaches the detail: only the exception class name and, - for a classified network/auth error, the class that classified it. + A credential NEVER reaches the detail: only the exception class name. + Every error here is a FAIL: the runner supplies both the simulator and the + credential, so an auth error, access denied, connection error or SDK error + is a defect, never an amber (COULD-NOT-RUN) result. """ try: return action() @@ -98,10 +101,6 @@ def step(name, action): raise except Exception as error: # noqa: BLE001 -- classified below, never echoed name_of = type(error).__name__ - if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f"{name_of} at step '{name}'") from error - if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'): - raise Amber('unreachable', f"{name_of} at step '{name}'") from error raise Failure(name, name_of) from error @@ -197,6 +196,9 @@ def run(target: str) -> None: try: import b2sdk.v3 as b2 except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error if not hasattr(b2, 'RawSimulator'): diff --git a/.sdkharness/tests/conformance/client.sync b/.sdkharness/tests/conformance/client.sync index 126049e9..8032ff5e 100755 --- a/.sdkharness/tests/conformance/client.sync +++ b/.sdkharness/tests/conformance/client.sync @@ -26,10 +26,9 @@ https://www.backblaze.com/apidocs/b2-delete-file-version Which of hide/delete a delete policy issues, and the compare rule that decides a file changed, are the tool's semantics, not B2's. -Written from bin/conformance/b2-sdk-python/files.upload, the reference check, -and bin/conformance/README.md, the contract. It exercises b2sdk from this exact repository -checkout; the API was taken from -docs/sdks/b2-sdk-python/card.md and the published b2sdk docs; the assertion now lives with the SDK code it exercises. +Written from files.upload in this directory, the reference check. It exercises +b2sdk from this exact repository checkout; the API usage is taken from the +published b2sdk documentation. """ import io @@ -41,15 +40,17 @@ import time import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' CAPABILITY = 'client.sync' REMOTE_PREFIX = 'st/sync' FILES = ('a.txt', 'b.txt', 'c.txt') -# The closed reason set, mirrored from bin/conformance/README.md. The runner -# enforces it; naming one outside this tuple is a FAIL it manufactures. +# The closed reason set. `lib/contract.py` only turns `no-realm-option`, +# `missing-runtime` and `not-claimed` into a SKIP. `unreachable` and +# `unauthorized` are never reported at the simulator: the runner supplies both +# the server and the credential, so those are FAIL, not amber. REASONS = ( 'no-realm-option', 'unreachable', @@ -60,7 +61,7 @@ REASONS = ( ) # A requested target maps to a realm or to amber. `simulator` is the harness's -# own server (bin/simulator/serve.mjs), whose URL the runner exports as +# own loopback server, whose URL the dispatcher exports as # CONFORMANCE_SIMULATOR_URL; unset, there is nothing to reach: no-realm-option. REALMS = { 'simulator': os.environ.get('CONFORMANCE_SIMULATOR_URL') or None, @@ -89,7 +90,7 @@ class Amber(Exception): class Failure(Exception): - """A golden-path step that did not do what the card claims.""" + """A golden-path step that did not do what the capability requires.""" def __init__(self, step: str, detail: str) -> None: self.step = step @@ -113,8 +114,10 @@ def note(text: str) -> None: def step(name, action): """Run one golden-path step, converting the environment's own errors. - A credential NEVER reaches the detail: only the exception class name and, - for a classified network/auth error, the class that classified it. + A credential NEVER reaches the detail: only the exception class name. + Every error here is a FAIL: the runner supplies both the simulator and the + credential, so an auth error, access denied, connection error or SDK error + is a defect, never an amber (COULD-NOT-RUN) result. """ try: return action() @@ -122,10 +125,6 @@ def step(name, action): raise except Exception as error: # noqa: BLE001 -- classified below, never echoed name_of = type(error).__name__ - if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f"{name_of} at step '{name}'") from error - if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'): - raise Amber('unreachable', f"{name_of} at step '{name}'") from error raise Failure(name, name_of) from error @@ -170,6 +169,9 @@ def prepare(target): try: import b2sdk.v3 as b2 except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error return b2, realm diff --git a/.sdkharness/tests/conformance/enc.sse_b2 b/.sdkharness/tests/conformance/enc.sse_b2 index 8450614a..92341bba 100755 --- a/.sdkharness/tests/conformance/enc.sse_b2 +++ b/.sdkharness/tests/conformance/enc.sse_b2 @@ -24,10 +24,9 @@ for new files uploaded to this bucket", example {"mode": "SSE-B2", "algorithm": "AES256"}: https://www.backblaze.com/apidocs/b2-create-bucket -Written from bin/conformance/b2-sdk-python/files.upload, the reference check, -and bin/conformance/README.md, the contract. It exercises b2sdk from this exact repository -checkout; the API was taken from -docs/sdks/b2-sdk-python/card.md and the published b2sdk docs; the assertion now lives with the SDK code it exercises. +Written from files.upload in this directory, the reference check. It exercises +b2sdk from this exact repository checkout; the API usage is taken from the +published b2sdk documentation. """ import hashlib @@ -37,15 +36,17 @@ import sys import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' CAPABILITY = 'enc.sse_b2' OBJECT_NAME = 'st/sse-b2.txt' PAYLOAD_SIZE = 1024 -# The closed reason set, mirrored from bin/conformance/README.md. The runner -# enforces it; naming one outside this tuple is a FAIL it manufactures. +# The closed reason set. `lib/contract.py` only turns `no-realm-option`, +# `missing-runtime` and `not-claimed` into a SKIP. `unreachable` and +# `unauthorized` are never reported at the simulator: the runner supplies both +# the server and the credential, so those are FAIL, not amber. REASONS = ( 'no-realm-option', 'unreachable', @@ -56,7 +57,7 @@ REASONS = ( ) # A requested target maps to a realm or to amber. `simulator` is the harness's -# own server (bin/simulator/serve.mjs), whose URL the runner exports as +# own loopback server, whose URL the dispatcher exports as # CONFORMANCE_SIMULATOR_URL; unset, there is nothing to reach: no-realm-option. REALMS = { 'simulator': os.environ.get('CONFORMANCE_SIMULATOR_URL') or None, @@ -85,7 +86,7 @@ class Amber(Exception): class Failure(Exception): - """A golden-path step that did not do what the card claims.""" + """A golden-path step that did not do what the capability requires.""" def __init__(self, step: str, detail: str) -> None: self.step = step @@ -109,8 +110,10 @@ def note(text: str) -> None: def step(name, action): """Run one golden-path step, converting the environment's own errors. - A credential NEVER reaches the detail: only the exception class name and, - for a classified network/auth error, the class that classified it. + A credential NEVER reaches the detail: only the exception class name. + Every error here is a FAIL: the runner supplies both the simulator and the + credential, so an auth error, access denied, connection error or SDK error + is a defect, never an amber (COULD-NOT-RUN) result. """ try: return action() @@ -118,10 +121,6 @@ def step(name, action): raise except Exception as error: # noqa: BLE001 -- classified below, never echoed name_of = type(error).__name__ - if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f"{name_of} at step '{name}'") from error - if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'): - raise Amber('unreachable', f"{name_of} at step '{name}'") from error raise Failure(name, name_of) from error @@ -166,6 +165,9 @@ def prepare(target): try: import b2sdk.v3 as b2 except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error return b2, realm diff --git a/.sdkharness/tests/conformance/enc.sse_c b/.sdkharness/tests/conformance/enc.sse_c index faf10bf3..6b0e23a0 100755 --- a/.sdkharness/tests/conformance/enc.sse_c +++ b/.sdkharness/tests/conformance/enc.sse_c @@ -41,10 +41,9 @@ cloud-storage-enable-server-side-encryption-with-the-native-api. The `sse_c_key_id` fileInfo entry is a b2sdk / CLI convention, not a B2 promise; the identity B2 documents is the key md5 on the download response (above). -Written from bin/conformance/b2-sdk-python/files.upload, the reference check, -and bin/conformance/README.md, the contract. It exercises b2sdk from this exact repository -checkout; the API was taken from -docs/sdks/b2-sdk-python/card.md and the published b2sdk docs; the assertion now lives with the SDK code it exercises. +Written from files.upload in this directory, the reference check. It exercises +b2sdk from this exact repository checkout; the API usage is taken from the +published b2sdk documentation. """ import base64 @@ -55,7 +54,7 @@ import sys import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' CAPABILITY = 'enc.sse_c' @@ -63,8 +62,10 @@ OBJECT_NAME = 'st/sse-c.txt' PAYLOAD_SIZE = 1024 KEY_ID = 'sdkharness-conf' -# The closed reason set, mirrored from bin/conformance/README.md. The runner -# enforces it; naming one outside this tuple is a FAIL it manufactures. +# The closed reason set. `lib/contract.py` only turns `no-realm-option`, +# `missing-runtime` and `not-claimed` into a SKIP. `unreachable` and +# `unauthorized` are never reported at the simulator: the runner supplies both +# the server and the credential, so those are FAIL, not amber. REASONS = ( 'no-realm-option', 'unreachable', @@ -75,7 +76,7 @@ REASONS = ( ) # A requested target maps to a realm or to amber. `simulator` is the harness's -# own server (bin/simulator/serve.mjs), whose URL the runner exports as +# own loopback server, whose URL the dispatcher exports as # CONFORMANCE_SIMULATOR_URL; unset, there is nothing to reach: no-realm-option. REALMS = { 'simulator': os.environ.get('CONFORMANCE_SIMULATOR_URL') or None, @@ -104,7 +105,7 @@ class Amber(Exception): class Failure(Exception): - """A golden-path step that did not do what the card claims.""" + """A golden-path step that did not do what the capability requires.""" def __init__(self, step: str, detail: str) -> None: self.step = step @@ -128,8 +129,10 @@ def note(text: str) -> None: def step(name, action): """Run one golden-path step, converting the environment's own errors. - A credential NEVER reaches the detail: only the exception class name and, - for a classified network/auth error, the class that classified it. + A credential NEVER reaches the detail: only the exception class name. + Every error here is a FAIL: the runner supplies both the simulator and the + credential, so an auth error, access denied, connection error or SDK error + is a defect, never an amber (COULD-NOT-RUN) result. """ try: return action() @@ -137,10 +140,6 @@ def step(name, action): raise except Exception as error: # noqa: BLE001 -- classified below, never echoed name_of = type(error).__name__ - if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f"{name_of} at step '{name}'") from error - if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'): - raise Amber('unreachable', f"{name_of} at step '{name}'") from error raise Failure(name, name_of) from error @@ -185,6 +184,9 @@ def prepare(target): try: import b2sdk.v3 as b2 except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error return b2, realm diff --git a/.sdkharness/tests/conformance/files.delete_version b/.sdkharness/tests/conformance/files.delete_version index 17a9f536..73d2313c 100755 --- a/.sdkharness/tests/conformance/files.delete_version +++ b/.sdkharness/tests/conformance/files.delete_version @@ -16,10 +16,9 @@ will become the current version, and be the one that you'll get when downloading by name." https://www.backblaze.com/apidocs/b2-delete-file-version -Written from bin/conformance/b2-sdk-python/files.upload, the reference check, -and bin/conformance/README.md, the contract. It exercises b2sdk from this exact repository -checkout; the API was taken from -docs/sdks/b2-sdk-python/card.md and the published b2sdk docs; the assertion now lives with the SDK code it exercises. +Written from files.upload in this directory, the reference check. It exercises +b2sdk from this exact repository checkout; the API usage is taken from the +published b2sdk documentation. """ import io @@ -28,7 +27,7 @@ import sys import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' CAPABILITY = 'files.delete_version' @@ -36,8 +35,10 @@ PREFIX = 'st/' OBJECT_NAME = 'st/delete-version.txt' PAYLOAD_SIZE = 1024 -# The closed reason set, mirrored from bin/conformance/README.md. The runner -# enforces it; naming one outside this tuple is a FAIL it manufactures. +# The closed reason set. `lib/contract.py` only turns `no-realm-option`, +# `missing-runtime` and `not-claimed` into a SKIP. `unreachable` and +# `unauthorized` are never reported at the simulator: the runner supplies both +# the server and the credential, so those are FAIL, not amber. REASONS = ( 'no-realm-option', 'unreachable', @@ -48,7 +49,7 @@ REASONS = ( ) # A requested target maps to a realm or to amber. `simulator` is the harness's -# own server (bin/simulator/serve.mjs), whose URL the runner exports as +# own loopback server, whose URL the dispatcher exports as # CONFORMANCE_SIMULATOR_URL; unset, there is nothing to reach: no-realm-option. REALMS = { 'simulator': os.environ.get('CONFORMANCE_SIMULATOR_URL') or None, @@ -77,7 +78,7 @@ class Amber(Exception): class Failure(Exception): - """A golden-path step that did not do what the card claims.""" + """A golden-path step that did not do what the capability requires.""" def __init__(self, step: str, detail: str) -> None: self.step = step @@ -101,8 +102,10 @@ def note(text: str) -> None: def step(name, action): """Run one golden-path step, converting the environment's own errors. - A credential NEVER reaches the detail: only the exception class name and, - for a classified network/auth error, the class that classified it. + A credential NEVER reaches the detail: only the exception class name. + Every error here is a FAIL: the runner supplies both the simulator and the + credential, so an auth error, access denied, connection error or SDK error + is a defect, never an amber (COULD-NOT-RUN) result. """ try: return action() @@ -110,10 +113,6 @@ def step(name, action): raise except Exception as error: # noqa: BLE001 -- classified below, never echoed name_of = type(error).__name__ - if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f"{name_of} at step '{name}'") from error - if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'): - raise Amber('unreachable', f"{name_of} at step '{name}'") from error raise Failure(name, name_of) from error @@ -158,6 +157,9 @@ def prepare(target): try: import b2sdk.v3 as b2 except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error return b2, realm diff --git a/.sdkharness/tests/conformance/files.download_by_id b/.sdkharness/tests/conformance/files.download_by_id index ddc2ab60..16d2f6c4 100755 --- a/.sdkharness/tests/conformance/files.download_by_id +++ b/.sdkharness/tests/conformance/files.download_by_id @@ -16,10 +16,9 @@ the most recent version of the file." https://www.backblaze.com/docs/cloud-storage-file-versions https://www.backblaze.com/apidocs/b2-download-file-by-id -Written from bin/conformance/b2-sdk-python/files.upload, the reference check, -and bin/conformance/README.md, the contract. It exercises b2sdk from this exact repository -checkout; the API was taken from -docs/sdks/b2-sdk-python/card.md and the published b2sdk docs; the assertion now lives with the SDK code it exercises. +Written from files.upload in this directory, the reference check. It exercises +b2sdk from this exact repository checkout; the API usage is taken from the +published b2sdk documentation. """ import io @@ -28,15 +27,17 @@ import sys import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' CAPABILITY = 'files.download_by_id' OBJECT_NAME = 'st/by-id.txt' PAYLOAD_SIZE = 1024 -# The closed reason set, mirrored from bin/conformance/README.md. The runner -# enforces it; naming one outside this tuple is a FAIL it manufactures. +# The closed reason set. `lib/contract.py` only turns `no-realm-option`, +# `missing-runtime` and `not-claimed` into a SKIP. `unreachable` and +# `unauthorized` are never reported at the simulator: the runner supplies both +# the server and the credential, so those are FAIL, not amber. REASONS = ( 'no-realm-option', 'unreachable', @@ -47,7 +48,7 @@ REASONS = ( ) # A requested target maps to a realm or to amber. `simulator` is the harness's -# own server (bin/simulator/serve.mjs), whose URL the runner exports as +# own loopback server, whose URL the dispatcher exports as # CONFORMANCE_SIMULATOR_URL; unset, there is nothing to reach: no-realm-option. REALMS = { 'simulator': os.environ.get('CONFORMANCE_SIMULATOR_URL') or None, @@ -76,7 +77,7 @@ class Amber(Exception): class Failure(Exception): - """A golden-path step that did not do what the card claims.""" + """A golden-path step that did not do what the capability requires.""" def __init__(self, step: str, detail: str) -> None: self.step = step @@ -100,8 +101,10 @@ def note(text: str) -> None: def step(name, action): """Run one golden-path step, converting the environment's own errors. - A credential NEVER reaches the detail: only the exception class name and, - for a classified network/auth error, the class that classified it. + A credential NEVER reaches the detail: only the exception class name. + Every error here is a FAIL: the runner supplies both the simulator and the + credential, so an auth error, access denied, connection error or SDK error + is a defect, never an amber (COULD-NOT-RUN) result. """ try: return action() @@ -109,10 +112,6 @@ def step(name, action): raise except Exception as error: # noqa: BLE001 -- classified below, never echoed name_of = type(error).__name__ - if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f"{name_of} at step '{name}'") from error - if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'): - raise Amber('unreachable', f"{name_of} at step '{name}'") from error raise Failure(name, name_of) from error @@ -157,6 +156,9 @@ def prepare(target): try: import b2sdk.v3 as b2 except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error return b2, realm diff --git a/.sdkharness/tests/conformance/files.download_content b/.sdkharness/tests/conformance/files.download_content index eb98001a..67ee991e 100755 --- a/.sdkharness/tests/conformance/files.download_content +++ b/.sdkharness/tests/conformance/files.download_content @@ -15,10 +15,9 @@ headers, Content-Length and X-Bz-Content-Sha1 among them, "contain the same information they did when the file was uploaded". https://www.backblaze.com/apidocs/b2-download-file-by-name -Written from bin/conformance/b2-sdk-python/files.upload, the reference check, -and bin/conformance/README.md, the contract. It exercises b2sdk from this exact repository -checkout; the API was taken from -docs/sdks/b2-sdk-python/card.md and the published b2sdk docs; the assertion now lives with the SDK code it exercises. +Written from files.upload in this directory, the reference check. It exercises +b2sdk from this exact repository checkout; the API usage is taken from the +published b2sdk documentation. """ import hashlib @@ -28,15 +27,17 @@ import sys import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' CAPABILITY = 'files.download_content' OBJECT_NAME = 'st/download.txt' PAYLOAD_SIZE = 1024 -# The closed reason set, mirrored from bin/conformance/README.md. The runner -# enforces it; naming one outside this tuple is a FAIL it manufactures. +# The closed reason set. `lib/contract.py` only turns `no-realm-option`, +# `missing-runtime` and `not-claimed` into a SKIP. `unreachable` and +# `unauthorized` are never reported at the simulator: the runner supplies both +# the server and the credential, so those are FAIL, not amber. REASONS = ( 'no-realm-option', 'unreachable', @@ -47,7 +48,7 @@ REASONS = ( ) # A requested target maps to a realm or to amber. `simulator` is the harness's -# own server (bin/simulator/serve.mjs), whose URL the runner exports as +# own loopback server, whose URL the dispatcher exports as # CONFORMANCE_SIMULATOR_URL; unset, there is nothing to reach: no-realm-option. REALMS = { 'simulator': os.environ.get('CONFORMANCE_SIMULATOR_URL') or None, @@ -76,7 +77,7 @@ class Amber(Exception): class Failure(Exception): - """A golden-path step that did not do what the card claims.""" + """A golden-path step that did not do what the capability requires.""" def __init__(self, step: str, detail: str) -> None: self.step = step @@ -100,8 +101,10 @@ def note(text: str) -> None: def step(name, action): """Run one golden-path step, converting the environment's own errors. - A credential NEVER reaches the detail: only the exception class name and, - for a classified network/auth error, the class that classified it. + A credential NEVER reaches the detail: only the exception class name. + Every error here is a FAIL: the runner supplies both the simulator and the + credential, so an auth error, access denied, connection error or SDK error + is a defect, never an amber (COULD-NOT-RUN) result. """ try: return action() @@ -109,10 +112,6 @@ def step(name, action): raise except Exception as error: # noqa: BLE001 -- classified below, never echoed name_of = type(error).__name__ - if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f"{name_of} at step '{name}'") from error - if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'): - raise Amber('unreachable', f"{name_of} at step '{name}'") from error raise Failure(name, name_of) from error @@ -157,6 +156,9 @@ def prepare(target): try: import b2sdk.v3 as b2 except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error return b2, realm diff --git a/.sdkharness/tests/conformance/files.hide b/.sdkharness/tests/conformance/files.hide index 171cf366..138ae8d8 100755 --- a/.sdkharness/tests/conformance/files.hide +++ b/.sdkharness/tests/conformance/files.hide @@ -22,10 +22,9 @@ The original stays reachable because "b2_download_file_by_id This operation downloads a specific version of a file by the given file ID." https://www.backblaze.com/docs/cloud-storage-file-versions -Written from bin/conformance/b2-sdk-python/files.upload, the reference check, -and bin/conformance/README.md, the contract. It exercises b2sdk from this exact repository -checkout; the API was taken from -docs/sdks/b2-sdk-python/card.md and the published b2sdk docs; the assertion now lives with the SDK code it exercises. +Written from files.upload in this directory, the reference check. It exercises +b2sdk from this exact repository checkout; the API usage is taken from the +published b2sdk documentation. """ import io @@ -34,7 +33,7 @@ import sys import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' CAPABILITY = 'files.hide' @@ -42,8 +41,10 @@ PREFIX = 'st/' OBJECT_NAME = 'st/hide.txt' PAYLOAD_SIZE = 1024 -# The closed reason set, mirrored from bin/conformance/README.md. The runner -# enforces it; naming one outside this tuple is a FAIL it manufactures. +# The closed reason set. `lib/contract.py` only turns `no-realm-option`, +# `missing-runtime` and `not-claimed` into a SKIP. `unreachable` and +# `unauthorized` are never reported at the simulator: the runner supplies both +# the server and the credential, so those are FAIL, not amber. REASONS = ( 'no-realm-option', 'unreachable', @@ -54,7 +55,7 @@ REASONS = ( ) # A requested target maps to a realm or to amber. `simulator` is the harness's -# own server (bin/simulator/serve.mjs), whose URL the runner exports as +# own loopback server, whose URL the dispatcher exports as # CONFORMANCE_SIMULATOR_URL; unset, there is nothing to reach: no-realm-option. REALMS = { 'simulator': os.environ.get('CONFORMANCE_SIMULATOR_URL') or None, @@ -83,7 +84,7 @@ class Amber(Exception): class Failure(Exception): - """A golden-path step that did not do what the card claims.""" + """A golden-path step that did not do what the capability requires.""" def __init__(self, step: str, detail: str) -> None: self.step = step @@ -107,8 +108,10 @@ def note(text: str) -> None: def step(name, action): """Run one golden-path step, converting the environment's own errors. - A credential NEVER reaches the detail: only the exception class name and, - for a classified network/auth error, the class that classified it. + A credential NEVER reaches the detail: only the exception class name. + Every error here is a FAIL: the runner supplies both the simulator and the + credential, so an auth error, access denied, connection error or SDK error + is a defect, never an amber (COULD-NOT-RUN) result. """ try: return action() @@ -116,10 +119,6 @@ def step(name, action): raise except Exception as error: # noqa: BLE001 -- classified below, never echoed name_of = type(error).__name__ - if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f"{name_of} at step '{name}'") from error - if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'): - raise Amber('unreachable', f"{name_of} at step '{name}'") from error raise Failure(name, name_of) from error @@ -164,6 +163,9 @@ def prepare(target): try: import b2sdk.v3 as b2 except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error return b2, realm diff --git a/.sdkharness/tests/conformance/files.list b/.sdkharness/tests/conformance/files.list index 2bcb8a32..2b4b1211 100755 --- a/.sdkharness/tests/conformance/files.list +++ b/.sdkharness/tests/conformance/files.list @@ -18,10 +18,9 @@ b2_list_file_versions "Lists all versions of all files contained in one bucket" and supports the same prefix option. https://www.backblaze.com/apidocs/b2-list-file-versions -Written from bin/conformance/b2-sdk-python/files.upload, the reference check, -and bin/conformance/README.md, the contract. It exercises b2sdk from this exact repository -checkout; the API was taken from -docs/sdks/b2-sdk-python/card.md and the published b2sdk docs; the assertion now lives with the SDK code it exercises. +Written from files.upload in this directory, the reference check. It exercises +b2sdk from this exact repository checkout; the API usage is taken from the +published b2sdk documentation. """ import collections @@ -30,7 +29,7 @@ import sys import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' CAPABILITY = 'files.list' @@ -39,8 +38,10 @@ INSIDE = ('st/list/a.txt', 'st/list/b.txt', 'st/list/c.txt') OUTSIDE = 'st/outside.txt' PAYLOAD_SIZE = 256 -# The closed reason set, mirrored from bin/conformance/README.md. The runner -# enforces it; naming one outside this tuple is a FAIL it manufactures. +# The closed reason set. `lib/contract.py` only turns `no-realm-option`, +# `missing-runtime` and `not-claimed` into a SKIP. `unreachable` and +# `unauthorized` are never reported at the simulator: the runner supplies both +# the server and the credential, so those are FAIL, not amber. REASONS = ( 'no-realm-option', 'unreachable', @@ -51,7 +52,7 @@ REASONS = ( ) # A requested target maps to a realm or to amber. `simulator` is the harness's -# own server (bin/simulator/serve.mjs), whose URL the runner exports as +# own loopback server, whose URL the dispatcher exports as # CONFORMANCE_SIMULATOR_URL; unset, there is nothing to reach: no-realm-option. REALMS = { 'simulator': os.environ.get('CONFORMANCE_SIMULATOR_URL') or None, @@ -80,7 +81,7 @@ class Amber(Exception): class Failure(Exception): - """A golden-path step that did not do what the card claims.""" + """A golden-path step that did not do what the capability requires.""" def __init__(self, step: str, detail: str) -> None: self.step = step @@ -104,8 +105,10 @@ def note(text: str) -> None: def step(name, action): """Run one golden-path step, converting the environment's own errors. - A credential NEVER reaches the detail: only the exception class name and, - for a classified network/auth error, the class that classified it. + A credential NEVER reaches the detail: only the exception class name. + Every error here is a FAIL: the runner supplies both the simulator and the + credential, so an auth error, access denied, connection error or SDK error + is a defect, never an amber (COULD-NOT-RUN) result. """ try: return action() @@ -113,10 +116,6 @@ def step(name, action): raise except Exception as error: # noqa: BLE001 -- classified below, never echoed name_of = type(error).__name__ - if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f"{name_of} at step '{name}'") from error - if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'): - raise Amber('unreachable', f"{name_of} at step '{name}'") from error raise Failure(name, name_of) from error @@ -161,6 +160,9 @@ def prepare(target): try: import b2sdk.v3 as b2 except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error return b2, realm diff --git a/.sdkharness/tests/conformance/files.metadata b/.sdkharness/tests/conformance/files.metadata index 050e00f7..d42dd5ef 100755 --- a/.sdkharness/tests/conformance/files.metadata +++ b/.sdkharness/tests/conformance/files.metadata @@ -22,10 +22,9 @@ A custom field name "is converted to lower case when it is stored and returned in the fileInfo object" -- hence the lower-case key. https://www.backblaze.com/apidocs/b2-upload-file -Written from bin/conformance/b2-sdk-python/files.upload, the reference check, -and bin/conformance/README.md, the contract. It exercises b2sdk from this exact repository -checkout; the API was taken from -docs/sdks/b2-sdk-python/card.md and the published b2sdk docs; the assertion now lives with the SDK code it exercises. +Written from files.upload in this directory, the reference check. It exercises +b2sdk from this exact repository checkout; the API usage is taken from the +published b2sdk documentation. """ import hashlib @@ -34,7 +33,7 @@ import sys import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' CAPABILITY = 'files.metadata' @@ -44,8 +43,10 @@ CONTENT_TYPE = 'text/x-sdkharness' INFO_KEY = 'sdkharness-conf' INFO_VALUE = 'fixture' -# The closed reason set, mirrored from bin/conformance/README.md. The runner -# enforces it; naming one outside this tuple is a FAIL it manufactures. +# The closed reason set. `lib/contract.py` only turns `no-realm-option`, +# `missing-runtime` and `not-claimed` into a SKIP. `unreachable` and +# `unauthorized` are never reported at the simulator: the runner supplies both +# the server and the credential, so those are FAIL, not amber. REASONS = ( 'no-realm-option', 'unreachable', @@ -56,7 +57,7 @@ REASONS = ( ) # A requested target maps to a realm or to amber. `simulator` is the harness's -# own server (bin/simulator/serve.mjs), whose URL the runner exports as +# own loopback server, whose URL the dispatcher exports as # CONFORMANCE_SIMULATOR_URL; unset, there is nothing to reach: no-realm-option. REALMS = { 'simulator': os.environ.get('CONFORMANCE_SIMULATOR_URL') or None, @@ -85,7 +86,7 @@ class Amber(Exception): class Failure(Exception): - """A golden-path step that did not do what the card claims.""" + """A golden-path step that did not do what the capability requires.""" def __init__(self, step: str, detail: str) -> None: self.step = step @@ -109,8 +110,10 @@ def note(text: str) -> None: def step(name, action): """Run one golden-path step, converting the environment's own errors. - A credential NEVER reaches the detail: only the exception class name and, - for a classified network/auth error, the class that classified it. + A credential NEVER reaches the detail: only the exception class name. + Every error here is a FAIL: the runner supplies both the simulator and the + credential, so an auth error, access denied, connection error or SDK error + is a defect, never an amber (COULD-NOT-RUN) result. """ try: return action() @@ -118,10 +121,6 @@ def step(name, action): raise except Exception as error: # noqa: BLE001 -- classified below, never echoed name_of = type(error).__name__ - if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f"{name_of} at step '{name}'") from error - if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'): - raise Amber('unreachable', f"{name_of} at step '{name}'") from error raise Failure(name, name_of) from error @@ -166,6 +165,9 @@ def prepare(target): try: import b2sdk.v3 as b2 except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error return b2, realm diff --git a/.sdkharness/tests/conformance/files.server_side_copy b/.sdkharness/tests/conformance/files.server_side_copy index 8e9d1509..2d207639 100755 --- a/.sdkharness/tests/conformance/files.server_side_copy +++ b/.sdkharness/tests/conformance/files.server_side_copy @@ -17,10 +17,9 @@ of this file" and contentSha1 is "The SHA1 of the bytes stored in the file", so an entire copy reports the source's sha1 under a new fileId. https://www.backblaze.com/apidocs/b2-copy-file -Written from bin/conformance/b2-sdk-python/files.upload, the reference check, -and bin/conformance/README.md, the contract. It exercises b2sdk from this exact repository -checkout; the API was taken from -docs/sdks/b2-sdk-python/card.md and the published b2sdk docs; the assertion now lives with the SDK code it exercises. +Written from files.upload in this directory, the reference check. It exercises +b2sdk from this exact repository checkout; the API usage is taken from the +published b2sdk documentation. """ import hashlib @@ -29,21 +28,23 @@ import sys import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' CAPABILITY = 'files.server_side_copy' SOURCE_NAME = 'st/copy-source.bin' DEST_NAME = 'st/copy-dest.bin' # Single-part on purpose: the harness simulator reports a 5,000-byte -# recommendedPartSize (bin/simulator/serve.mjs, PROFILE); above it b2sdk uploads +# recommendedPartSize (the simulator's PROFILE); above it b2sdk uploads # the fixture as a large file, whose contentSha1 is "none", not the whole-file # hash this scenario asserts (https://www.backblaze.com/apidocs/b2-get-file-info). # Mirrors the TypeScript check. PAYLOAD_SIZE = 512 -# The closed reason set, mirrored from bin/conformance/README.md. The runner -# enforces it; naming one outside this tuple is a FAIL it manufactures. +# The closed reason set. `lib/contract.py` only turns `no-realm-option`, +# `missing-runtime` and `not-claimed` into a SKIP. `unreachable` and +# `unauthorized` are never reported at the simulator: the runner supplies both +# the server and the credential, so those are FAIL, not amber. REASONS = ( 'no-realm-option', 'unreachable', @@ -54,7 +55,7 @@ REASONS = ( ) # A requested target maps to a realm or to amber. `simulator` is the harness's -# own server (bin/simulator/serve.mjs), whose URL the runner exports as +# own loopback server, whose URL the dispatcher exports as # CONFORMANCE_SIMULATOR_URL; unset, there is nothing to reach: no-realm-option. REALMS = { 'simulator': os.environ.get('CONFORMANCE_SIMULATOR_URL') or None, @@ -83,7 +84,7 @@ class Amber(Exception): class Failure(Exception): - """A golden-path step that did not do what the card claims.""" + """A golden-path step that did not do what the capability requires.""" def __init__(self, step: str, detail: str) -> None: self.step = step @@ -107,8 +108,10 @@ def note(text: str) -> None: def step(name, action): """Run one golden-path step, converting the environment's own errors. - A credential NEVER reaches the detail: only the exception class name and, - for a classified network/auth error, the class that classified it. + A credential NEVER reaches the detail: only the exception class name. + Every error here is a FAIL: the runner supplies both the simulator and the + credential, so an auth error, access denied, connection error or SDK error + is a defect, never an amber (COULD-NOT-RUN) result. """ try: return action() @@ -116,10 +119,6 @@ def step(name, action): raise except Exception as error: # noqa: BLE001 -- classified below, never echoed name_of = type(error).__name__ - if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f"{name_of} at step '{name}'") from error - if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'): - raise Amber('unreachable', f"{name_of} at step '{name}'") from error raise Failure(name, name_of) from error @@ -164,6 +163,9 @@ def prepare(target): try: import b2sdk.v3 as b2 except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error return b2, realm diff --git a/.sdkharness/tests/conformance/files.upload b/.sdkharness/tests/conformance/files.upload index 574451bc..7ba36f29 100755 --- a/.sdkharness/tests/conformance/files.upload +++ b/.sdkharness/tests/conformance/files.upload @@ -1,11 +1,10 @@ #!/usr/bin/env python3 -"""THE REFERENCE CHECK — b2-sdk-python x files.upload. +"""Reference conformance check: b2-sdk-python x files.upload. -The other 134 checks are written from this one. Read bin/conformance/README.md -for the contract; this file is what obeying it looks like. +The other conformance checks in this directory follow the same structure; this +file is the simplest example of it. -Scenario (approved 2026-09-22, one per capability, identical on every tool that -claims it): +Scenario (one per capability, identical on every tool that claims it): Setup an ephemeral bucket and a 1 KiB payload of known sha1 Action upload it to st/upload.txt @@ -23,8 +22,8 @@ The round trip holds because b2_download_file_by_name returns headers that "contain the same information they did when the file was uploaded". https://www.backblaze.com/apidocs/b2-download-file-by-name -It exercises b2sdk from this exact repository checkout. The API was originally the API was taken from docs/sdks/b2-sdk-python/card.md and the -published b2sdk docs, which is what every check author does. +It exercises b2sdk from this exact repository checkout. The API usage is taken +from the published b2sdk documentation. """ import hashlib @@ -34,15 +33,17 @@ import sys import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' CAPABILITY = 'files.upload' OBJECT_NAME = 'st/upload.txt' PAYLOAD_SIZE = 1024 -# The closed reason set, mirrored from bin/conformance/README.md. The runner -# enforces it; naming one outside this tuple is a FAIL it manufactures. +# The closed reason set. `lib/contract.py` only turns `no-realm-option`, +# `missing-runtime` and `not-claimed` into a SKIP. `unreachable` and +# `unauthorized` are never reported at the simulator: the runner supplies both +# the server and the credential, so those are FAIL, not amber. REASONS = ( 'no-realm-option', 'unreachable', @@ -53,7 +54,7 @@ REASONS = ( ) # A requested target maps to a realm or to amber. `simulator` is the harness's -# own server (bin/simulator/serve.mjs), whose URL the runner exports as +# own loopback server, whose URL the dispatcher exports as # CONFORMANCE_SIMULATOR_URL; unset, there is nothing to reach: no-realm-option. REALMS = { 'simulator': os.environ.get('CONFORMANCE_SIMULATOR_URL') or None, @@ -75,7 +76,7 @@ class Amber(Exception): class Failure(Exception): - """A golden-path step that did not do what the card claims.""" + """A golden-path step that did not do what the capability requires.""" def __init__(self, step: str, detail: str) -> None: self.step = step @@ -89,8 +90,10 @@ def say(target: str, verdict: str) -> None: def step(name: str, action): """Run one golden-path step, converting the environment's own errors. - A credential NEVER reaches the detail: only the exception class name and, - for a classified network/auth error, the class that classified it. + A credential NEVER reaches the detail: only the exception class name. + Every error here is a FAIL: the runner supplies both the simulator and the + credential, so an auth error, access denied, connection error or SDK error + is a defect, never an amber (COULD-NOT-RUN) result. """ try: return action() @@ -98,10 +101,6 @@ def step(name: str, action): raise except Exception as error: # noqa: BLE001 -- classified below, never echoed name_of = type(error).__name__ - if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f"{name_of} at step '{name}'") from error - if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'): - raise Amber('unreachable', f"{name_of} at step '{name}'") from error raise Failure(name, name_of) from error @@ -120,6 +119,9 @@ def run(target: str) -> None: try: from b2sdk.v3 import B2Api, InMemoryAccountInfo except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error api = B2Api(InMemoryAccountInfo()) diff --git a/.sdkharness/tests/conformance/keys.crud b/.sdkharness/tests/conformance/keys.crud index 69a510d9..3ca012fc 100755 --- a/.sdkharness/tests/conformance/keys.crud +++ b/.sdkharness/tests/conformance/keys.crud @@ -20,10 +20,9 @@ https://www.backblaze.com/apidocs/b2-create-key https://www.backblaze.com/apidocs/b2-list-keys https://www.backblaze.com/apidocs/b2-delete-key -Written from bin/conformance/b2-sdk-python/files.upload, the reference check, -and bin/conformance/README.md, the contract. It exercises b2sdk from this exact repository -checkout; the API was taken from -docs/sdks/b2-sdk-python/card.md and the published b2sdk docs; the assertion now lives with the SDK code it exercises. +Written from files.upload in this directory, the reference check. It exercises +b2sdk from this exact repository checkout; the API usage is taken from the +published b2sdk documentation. """ import os @@ -31,14 +30,16 @@ import sys import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' CAPABILITY = 'keys.crud' CAPABILITIES = ['listBuckets', 'listFiles', 'readFiles'] -# The closed reason set, mirrored from bin/conformance/README.md. The runner -# enforces it; naming one outside this tuple is a FAIL it manufactures. +# The closed reason set. `lib/contract.py` only turns `no-realm-option`, +# `missing-runtime` and `not-claimed` into a SKIP. `unreachable` and +# `unauthorized` are never reported at the simulator: the runner supplies both +# the server and the credential, so those are FAIL, not amber. REASONS = ( 'no-realm-option', 'unreachable', @@ -49,7 +50,7 @@ REASONS = ( ) # A requested target maps to a realm or to amber. `simulator` is the harness's -# own server (bin/simulator/serve.mjs), whose URL the runner exports as +# own loopback server, whose URL the dispatcher exports as # CONFORMANCE_SIMULATOR_URL; unset, there is nothing to reach: no-realm-option. REALMS = { 'simulator': os.environ.get('CONFORMANCE_SIMULATOR_URL') or None, @@ -78,7 +79,7 @@ class Amber(Exception): class Failure(Exception): - """A golden-path step that did not do what the card claims.""" + """A golden-path step that did not do what the capability requires.""" def __init__(self, step: str, detail: str) -> None: self.step = step @@ -102,8 +103,10 @@ def note(text: str) -> None: def step(name, action): """Run one golden-path step, converting the environment's own errors. - A credential NEVER reaches the detail: only the exception class name and, - for a classified network/auth error, the class that classified it. + A credential NEVER reaches the detail: only the exception class name. + Every error here is a FAIL: the runner supplies both the simulator and the + credential, so an auth error, access denied, connection error or SDK error + is a defect, never an amber (COULD-NOT-RUN) result. """ try: return action() @@ -111,10 +114,6 @@ def step(name, action): raise except Exception as error: # noqa: BLE001 -- classified below, never echoed name_of = type(error).__name__ - if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f"{name_of} at step '{name}'") from error - if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'): - raise Amber('unreachable', f"{name_of} at step '{name}'") from error raise Failure(name, name_of) from error @@ -159,6 +158,9 @@ def prepare(target): try: import b2sdk.v3 as b2 except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error return b2, realm diff --git a/.sdkharness/tests/conformance/keys.multi_bucket b/.sdkharness/tests/conformance/keys.multi_bucket index fe34963c..773c6e01 100755 --- a/.sdkharness/tests/conformance/keys.multi_bucket +++ b/.sdkharness/tests/conformance/keys.multi_bucket @@ -19,10 +19,9 @@ https://www.backblaze.com/apidocs/b2-create-key https://www.backblaze.com/apidocs/b2-authorize-account https://www.backblaze.com/apidocs/b2-list-buckets -Written from bin/conformance/b2-sdk-python/files.upload, the reference check, -and bin/conformance/README.md, the contract. It exercises b2sdk from this exact repository -checkout; the API was taken from -docs/sdks/b2-sdk-python/card.md and the published b2sdk docs; the assertion now lives with the SDK code it exercises. +Written from files.upload in this directory, the reference check. It exercises +b2sdk from this exact repository checkout; the API usage is taken from the +published b2sdk documentation. """ import os @@ -30,14 +29,16 @@ import sys import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' CAPABILITY = 'keys.multi_bucket' CAPABILITIES = ['listBuckets', 'listFiles', 'readFiles'] -# The closed reason set, mirrored from bin/conformance/README.md. The runner -# enforces it; naming one outside this tuple is a FAIL it manufactures. +# The closed reason set. `lib/contract.py` only turns `no-realm-option`, +# `missing-runtime` and `not-claimed` into a SKIP. `unreachable` and +# `unauthorized` are never reported at the simulator: the runner supplies both +# the server and the credential, so those are FAIL, not amber. REASONS = ( 'no-realm-option', 'unreachable', @@ -48,7 +49,7 @@ REASONS = ( ) # A requested target maps to a realm or to amber. `simulator` is the harness's -# own server (bin/simulator/serve.mjs), whose URL the runner exports as +# own loopback server, whose URL the dispatcher exports as # CONFORMANCE_SIMULATOR_URL; unset, there is nothing to reach: no-realm-option. REALMS = { 'simulator': os.environ.get('CONFORMANCE_SIMULATOR_URL') or None, @@ -77,7 +78,7 @@ class Amber(Exception): class Failure(Exception): - """A golden-path step that did not do what the card claims.""" + """A golden-path step that did not do what the capability requires.""" def __init__(self, step: str, detail: str) -> None: self.step = step @@ -101,8 +102,10 @@ def note(text: str) -> None: def step(name, action): """Run one golden-path step, converting the environment's own errors. - A credential NEVER reaches the detail: only the exception class name and, - for a classified network/auth error, the class that classified it. + A credential NEVER reaches the detail: only the exception class name. + Every error here is a FAIL: the runner supplies both the simulator and the + credential, so an auth error, access denied, connection error or SDK error + is a defect, never an amber (COULD-NOT-RUN) result. """ try: return action() @@ -110,10 +113,6 @@ def step(name, action): raise except Exception as error: # noqa: BLE001 -- classified below, never echoed name_of = type(error).__name__ - if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f"{name_of} at step '{name}'") from error - if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'): - raise Amber('unreachable', f"{name_of} at step '{name}'") from error raise Failure(name, name_of) from error @@ -158,6 +157,9 @@ def prepare(target): try: import b2sdk.v3 as b2 except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error return b2, realm diff --git a/.sdkharness/tests/conformance/large.compose_mixed b/.sdkharness/tests/conformance/large.compose_mixed index e09fb88d..8315ca64 100755 --- a/.sdkharness/tests/conformance/large.compose_mixed +++ b/.sdkharness/tests/conformance/large.compose_mixed @@ -22,10 +22,9 @@ will be copied." https://www.backblaze.com/apidocs/b2-copy-part Part sizes: "the minimum part size is 5 MB except for the last part in a file which has a minimum size of 1 byte." https://www.backblaze.com/docs/cloud-storage-create-large-files-with-the-native-api -Written from bin/conformance/b2-sdk-python/files.upload, the reference check, -and bin/conformance/README.md, the contract. It exercises b2sdk from this exact repository -checkout; the API was taken from -docs/sdks/b2-sdk-python/card.md and the published b2sdk docs; the assertion now lives with the SDK code it exercises. +Written from files.upload in this directory, the reference check. It exercises +b2sdk from this exact repository checkout; the API usage is taken from the +published b2sdk documentation. """ import hashlib @@ -36,7 +35,7 @@ import tempfile import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' CAPABILITY = 'large.compose_mixed' @@ -45,8 +44,10 @@ STORED_A = 'st/compose-a.bin' STORED_B = 'st/compose-b.bin' COMPOSED = 'st/composed.bin' -# The closed reason set, mirrored from bin/conformance/README.md. The runner -# enforces it; naming one outside this tuple is a FAIL it manufactures. +# The closed reason set. `lib/contract.py` only turns `no-realm-option`, +# `missing-runtime` and `not-claimed` into a SKIP. `unreachable` and +# `unauthorized` are never reported at the simulator: the runner supplies both +# the server and the credential, so those are FAIL, not amber. REASONS = ( 'no-realm-option', 'unreachable', @@ -57,7 +58,7 @@ REASONS = ( ) # A requested target maps to a realm or to amber. `simulator` is the harness's -# own server (bin/simulator/serve.mjs), whose URL the runner exports as +# own loopback server, whose URL the dispatcher exports as # CONFORMANCE_SIMULATOR_URL; unset, there is nothing to reach: no-realm-option. REALMS = { 'simulator': os.environ.get('CONFORMANCE_SIMULATOR_URL') or None, @@ -86,7 +87,7 @@ class Amber(Exception): class Failure(Exception): - """A golden-path step that did not do what the card claims.""" + """A golden-path step that did not do what the capability requires.""" def __init__(self, step: str, detail: str) -> None: self.step = step @@ -110,8 +111,10 @@ def note(text: str) -> None: def step(name, action): """Run one golden-path step, converting the environment's own errors. - A credential NEVER reaches the detail: only the exception class name and, - for a classified network/auth error, the class that classified it. + A credential NEVER reaches the detail: only the exception class name. + Every error here is a FAIL: the runner supplies both the simulator and the + credential, so an auth error, access denied, connection error or SDK error + is a defect, never an amber (COULD-NOT-RUN) result. """ try: return action() @@ -119,10 +122,6 @@ def step(name, action): raise except Exception as error: # noqa: BLE001 -- classified below, never echoed name_of = type(error).__name__ - if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f"{name_of} at step '{name}'") from error - if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'): - raise Amber('unreachable', f"{name_of} at step '{name}'") from error raise Failure(name, name_of) from error @@ -167,6 +166,9 @@ def prepare(target): try: import b2sdk.v3 as b2 except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error return b2, realm diff --git a/.sdkharness/tests/conformance/large.concurrent_parts b/.sdkharness/tests/conformance/large.concurrent_parts index b0bce90d..8062a4c0 100755 --- a/.sdkharness/tests/conformance/large.concurrent_parts +++ b/.sdkharness/tests/conformance/large.concurrent_parts @@ -26,10 +26,9 @@ PERMISSION ONLY: no B2 document requires a client to upload parts concurrently, so the ">1 in flight at 4" expected value traces to the tool's own concurrency setting, not to B2. -Written from bin/conformance/b2-sdk-python/files.upload, the reference check, -and bin/conformance/README.md, the contract. It exercises b2sdk from this exact repository -checkout; the API was taken from -docs/sdks/b2-sdk-python/card.md and the published b2sdk docs; the assertion now lives with the SDK code it exercises. +Written from files.upload in this directory, the reference check. It exercises +b2sdk from this exact repository checkout; the API usage is taken from the +published b2sdk documentation. """ import os @@ -39,7 +38,7 @@ import time import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' CAPABILITY = 'large.concurrent_parts' @@ -49,8 +48,10 @@ PARTS_WANTED = 4 WORKERS = 4 OBSERVATION_DWELL_SECONDS = 0.1 -# The closed reason set, mirrored from bin/conformance/README.md. The runner -# enforces it; naming one outside this tuple is a FAIL it manufactures. +# The closed reason set. `lib/contract.py` only turns `no-realm-option`, +# `missing-runtime` and `not-claimed` into a SKIP. `unreachable` and +# `unauthorized` are never reported at the simulator: the runner supplies both +# the server and the credential, so those are FAIL, not amber. REASONS = ( 'no-realm-option', 'unreachable', @@ -61,7 +62,7 @@ REASONS = ( ) # A requested target maps to a realm or to amber. `simulator` is the harness's -# own server (bin/simulator/serve.mjs), whose URL the runner exports as +# own loopback server, whose URL the dispatcher exports as # CONFORMANCE_SIMULATOR_URL; unset, there is nothing to reach: no-realm-option. REALMS = { 'simulator': os.environ.get('CONFORMANCE_SIMULATOR_URL') or None, @@ -90,7 +91,7 @@ class Amber(Exception): class Failure(Exception): - """A golden-path step that did not do what the card claims.""" + """A golden-path step that did not do what the capability requires.""" def __init__(self, step: str, detail: str) -> None: self.step = step @@ -114,8 +115,10 @@ def note(text: str) -> None: def step(name, action): """Run one golden-path step, converting the environment's own errors. - A credential NEVER reaches the detail: only the exception class name and, - for a classified network/auth error, the class that classified it. + A credential NEVER reaches the detail: only the exception class name. + Every error here is a FAIL: the runner supplies both the simulator and the + credential, so an auth error, access denied, connection error or SDK error + is a defect, never an amber (COULD-NOT-RUN) result. """ try: return action() @@ -123,10 +126,6 @@ def step(name, action): raise except Exception as error: # noqa: BLE001 -- classified below, never echoed name_of = type(error).__name__ - if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f"{name_of} at step '{name}'") from error - if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'): - raise Amber('unreachable', f"{name_of} at step '{name}'") from error raise Failure(name, name_of) from error @@ -171,6 +170,9 @@ def prepare(target): try: import b2sdk.v3 as b2 except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error return b2, realm diff --git a/.sdkharness/tests/conformance/large.multipart b/.sdkharness/tests/conformance/large.multipart index 2ee9bf3e..6034da97 100755 --- a/.sdkharness/tests/conformance/large.multipart +++ b/.sdkharness/tests/conformance/large.multipart @@ -23,10 +23,9 @@ DOCS -- the acceptance criteria; what the tool does is the result: NEEDS REVIEW: docs say large_file_sha1 is RECOMMENDED, not required; the assertion FAILs a large file that lacks it. -Written from bin/conformance/b2-sdk-python/files.upload, the reference check, -and bin/conformance/README.md, the contract. It exercises b2sdk from this exact repository -checkout; the API was taken from -docs/sdks/b2-sdk-python/card.md and the published b2sdk docs; the assertion now lives with the SDK code it exercises. +Written from files.upload in this directory, the reference check. It exercises +b2sdk from this exact repository checkout; the API usage is taken from the +published b2sdk documentation. """ import collections @@ -37,15 +36,17 @@ import sys import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' CAPABILITY = 'large.multipart' OBJECT_NAME = 'st/multipart.bin' PARTS_WANTED = 3 -# The closed reason set, mirrored from bin/conformance/README.md. The runner -# enforces it; naming one outside this tuple is a FAIL it manufactures. +# The closed reason set. `lib/contract.py` only turns `no-realm-option`, +# `missing-runtime` and `not-claimed` into a SKIP. `unreachable` and +# `unauthorized` are never reported at the simulator: the runner supplies both +# the server and the credential, so those are FAIL, not amber. REASONS = ( 'no-realm-option', 'unreachable', @@ -56,7 +57,7 @@ REASONS = ( ) # A requested target maps to a realm or to amber. `simulator` is the harness's -# own server (bin/simulator/serve.mjs), whose URL the runner exports as +# own loopback server, whose URL the dispatcher exports as # CONFORMANCE_SIMULATOR_URL; unset, there is nothing to reach: no-realm-option. REALMS = { 'simulator': os.environ.get('CONFORMANCE_SIMULATOR_URL') or None, @@ -85,7 +86,7 @@ class Amber(Exception): class Failure(Exception): - """A golden-path step that did not do what the card claims.""" + """A golden-path step that did not do what the capability requires.""" def __init__(self, step: str, detail: str) -> None: self.step = step @@ -109,8 +110,10 @@ def note(text: str) -> None: def step(name, action): """Run one golden-path step, converting the environment's own errors. - A credential NEVER reaches the detail: only the exception class name and, - for a classified network/auth error, the class that classified it. + A credential NEVER reaches the detail: only the exception class name. + Every error here is a FAIL: the runner supplies both the simulator and the + credential, so an auth error, access denied, connection error or SDK error + is a defect, never an amber (COULD-NOT-RUN) result. """ try: return action() @@ -118,10 +121,6 @@ def step(name, action): raise except Exception as error: # noqa: BLE001 -- classified below, never echoed name_of = type(error).__name__ - if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f"{name_of} at step '{name}'") from error - if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'): - raise Amber('unreachable', f"{name_of} at step '{name}'") from error raise Failure(name, name_of) from error @@ -166,6 +165,9 @@ def prepare(target): try: import b2sdk.v3 as b2 except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error return b2, realm diff --git a/.sdkharness/tests/conformance/large.parallel_download b/.sdkharness/tests/conformance/large.parallel_download index 1df48e88..5c0a8a8e 100755 --- a/.sdkharness/tests/conformance/large.parallel_download +++ b/.sdkharness/tests/conformance/large.parallel_download @@ -30,10 +30,9 @@ PERMISSION ONLY: no B2 document requires a client to download in parallel, so the ">1 concurrent ranged GET" expected value traces to the tool's stream setting, not to B2. -Written from bin/conformance/b2-sdk-python/files.upload, the reference check, -and bin/conformance/README.md, the contract. It exercises b2sdk from this exact repository -checkout; the API was taken from -docs/sdks/b2-sdk-python/card.md and the published b2sdk docs; the assertion now lives with the SDK code it exercises. +Written from files.upload in this directory, the reference check. It exercises +b2sdk from this exact repository checkout; the API usage is taken from the +published b2sdk documentation. """ import io @@ -44,7 +43,7 @@ import time import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' CAPABILITY = 'large.parallel_download' @@ -53,8 +52,10 @@ PARTS_WANTED = 4 STREAMS = 4 OBSERVATION_DWELL_SECONDS = 0.1 -# The closed reason set, mirrored from bin/conformance/README.md. The runner -# enforces it; naming one outside this tuple is a FAIL it manufactures. +# The closed reason set. `lib/contract.py` only turns `no-realm-option`, +# `missing-runtime` and `not-claimed` into a SKIP. `unreachable` and +# `unauthorized` are never reported at the simulator: the runner supplies both +# the server and the credential, so those are FAIL, not amber. REASONS = ( 'no-realm-option', 'unreachable', @@ -65,7 +66,7 @@ REASONS = ( ) # A requested target maps to a realm or to amber. `simulator` is the harness's -# own server (bin/simulator/serve.mjs), whose URL the runner exports as +# own loopback server, whose URL the dispatcher exports as # CONFORMANCE_SIMULATOR_URL; unset, there is nothing to reach: no-realm-option. REALMS = { 'simulator': os.environ.get('CONFORMANCE_SIMULATOR_URL') or None, @@ -94,7 +95,7 @@ class Amber(Exception): class Failure(Exception): - """A golden-path step that did not do what the card claims.""" + """A golden-path step that did not do what the capability requires.""" def __init__(self, step: str, detail: str) -> None: self.step = step @@ -118,8 +119,10 @@ def note(text: str) -> None: def step(name, action): """Run one golden-path step, converting the environment's own errors. - A credential NEVER reaches the detail: only the exception class name and, - for a classified network/auth error, the class that classified it. + A credential NEVER reaches the detail: only the exception class name. + Every error here is a FAIL: the runner supplies both the simulator and the + credential, so an auth error, access denied, connection error or SDK error + is a defect, never an amber (COULD-NOT-RUN) result. """ try: return action() @@ -127,10 +130,6 @@ def step(name, action): raise except Exception as error: # noqa: BLE001 -- classified below, never echoed name_of = type(error).__name__ - if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f"{name_of} at step '{name}'") from error - if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'): - raise Amber('unreachable', f"{name_of} at step '{name}'") from error raise Failure(name, name_of) from error @@ -175,6 +174,9 @@ def prepare(target): try: import b2sdk.v3 as b2 except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error return b2, realm diff --git a/.sdkharness/tests/conformance/large.resume b/.sdkharness/tests/conformance/large.resume index 3ea0559e..7c80d05e 100755 --- a/.sdkharness/tests/conformance/large.resume +++ b/.sdkharness/tests/conformance/large.resume @@ -25,10 +25,9 @@ B2 makes reusing the unfinished fileId POSSIBLE; that a rerun reuses it instead of calling b2_start_large_file is the capability under test, not a B2 promise. -Written from bin/conformance/b2-sdk-python/files.upload, the reference check, -and bin/conformance/README.md, the contract. It exercises b2sdk from this exact repository -checkout; the API was taken from -docs/sdks/b2-sdk-python/card.md and the published b2sdk docs; the assertion now lives with the SDK code it exercises. +Written from files.upload in this directory, the reference check. It exercises +b2sdk from this exact repository checkout; the API usage is taken from the +published b2sdk documentation. """ import collections @@ -40,7 +39,7 @@ import tempfile import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' CAPABILITY = 'large.resume' @@ -48,8 +47,10 @@ OBJECT_NAME = 'st/resume.bin' PARTS_WANTED = 4 PARTS_BEFORE_INTERRUPT = 1 -# The closed reason set, mirrored from bin/conformance/README.md. The runner -# enforces it; naming one outside this tuple is a FAIL it manufactures. +# The closed reason set. `lib/contract.py` only turns `no-realm-option`, +# `missing-runtime` and `not-claimed` into a SKIP. `unreachable` and +# `unauthorized` are never reported at the simulator: the runner supplies both +# the server and the credential, so those are FAIL, not amber. REASONS = ( 'no-realm-option', 'unreachable', @@ -60,7 +61,7 @@ REASONS = ( ) # A requested target maps to a realm or to amber. `simulator` is the harness's -# own server (bin/simulator/serve.mjs), whose URL the runner exports as +# own loopback server, whose URL the dispatcher exports as # CONFORMANCE_SIMULATOR_URL; unset, there is nothing to reach: no-realm-option. REALMS = { 'simulator': os.environ.get('CONFORMANCE_SIMULATOR_URL') or None, @@ -89,7 +90,7 @@ class Amber(Exception): class Failure(Exception): - """A golden-path step that did not do what the card claims.""" + """A golden-path step that did not do what the capability requires.""" def __init__(self, step: str, detail: str) -> None: self.step = step @@ -113,8 +114,10 @@ def note(text: str) -> None: def step(name, action): """Run one golden-path step, converting the environment's own errors. - A credential NEVER reaches the detail: only the exception class name and, - for a classified network/auth error, the class that classified it. + A credential NEVER reaches the detail: only the exception class name. + Every error here is a FAIL: the runner supplies both the simulator and the + credential, so an auth error, access denied, connection error or SDK error + is a defect, never an amber (COULD-NOT-RUN) result. """ try: return action() @@ -122,10 +125,6 @@ def step(name, action): raise except Exception as error: # noqa: BLE001 -- classified below, never echoed name_of = type(error).__name__ - if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f"{name_of} at step '{name}'") from error - if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'): - raise Amber('unreachable', f"{name_of} at step '{name}'") from error raise Failure(name, name_of) from error @@ -170,6 +169,9 @@ def prepare(target): try: import b2sdk.v3 as b2 except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error return b2, realm diff --git a/.sdkharness/tests/conformance/large.unbound_incremental b/.sdkharness/tests/conformance/large.unbound_incremental index 32fcaa5a..f57c331c 100755 --- a/.sdkharness/tests/conformance/large.unbound_incremental +++ b/.sdkharness/tests/conformance/large.unbound_incremental @@ -32,10 +32,9 @@ which b2_start_large_file recommends only "If the caller knows the SHA1 of the entire large file being uploaded" (https://www.backblaze.com/apidocs/b2-start-large-file) -- an unbound stream's uploader cannot know it when the file is started. -Written from bin/conformance/b2-sdk-python/files.upload, the reference check, -and bin/conformance/README.md, the contract. It exercises b2sdk from this exact repository -checkout; the API was taken from -docs/sdks/b2-sdk-python/card.md and the published b2sdk docs; the assertion now lives with the SDK code it exercises. +Written from files.upload in this directory, the reference check. It exercises +b2sdk from this exact repository checkout; the API usage is taken from the +published b2sdk documentation. """ import collections @@ -47,7 +46,7 @@ import tempfile import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' CAPABILITY = 'large.unbound_incremental' @@ -56,8 +55,10 @@ INCREMENTAL_NAME = 'st/incremental.bin' UNBOUND_SIZE = 4096 APPENDED_SIZE = 1024 -# The closed reason set, mirrored from bin/conformance/README.md. The runner -# enforces it; naming one outside this tuple is a FAIL it manufactures. +# The closed reason set. `lib/contract.py` only turns `no-realm-option`, +# `missing-runtime` and `not-claimed` into a SKIP. `unreachable` and +# `unauthorized` are never reported at the simulator: the runner supplies both +# the server and the credential, so those are FAIL, not amber. REASONS = ( 'no-realm-option', 'unreachable', @@ -68,7 +69,7 @@ REASONS = ( ) # A requested target maps to a realm or to amber. `simulator` is the harness's -# own server (bin/simulator/serve.mjs), whose URL the runner exports as +# own loopback server, whose URL the dispatcher exports as # CONFORMANCE_SIMULATOR_URL; unset, there is nothing to reach: no-realm-option. REALMS = { 'simulator': os.environ.get('CONFORMANCE_SIMULATOR_URL') or None, @@ -97,7 +98,7 @@ class Amber(Exception): class Failure(Exception): - """A golden-path step that did not do what the card claims.""" + """A golden-path step that did not do what the capability requires.""" def __init__(self, step: str, detail: str) -> None: self.step = step @@ -121,8 +122,10 @@ def note(text: str) -> None: def step(name, action): """Run one golden-path step, converting the environment's own errors. - A credential NEVER reaches the detail: only the exception class name and, - for a classified network/auth error, the class that classified it. + A credential NEVER reaches the detail: only the exception class name. + Every error here is a FAIL: the runner supplies both the simulator and the + credential, so an auth error, access denied, connection error or SDK error + is a defect, never an amber (COULD-NOT-RUN) result. """ try: return action() @@ -130,10 +133,6 @@ def step(name, action): raise except Exception as error: # noqa: BLE001 -- classified below, never echoed name_of = type(error).__name__ - if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f"{name_of} at step '{name}'") from error - if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'): - raise Amber('unreachable', f"{name_of} at step '{name}'") from error raise Failure(name, name_of) from error @@ -178,6 +177,9 @@ def prepare(target): try: import b2sdk.v3 as b2 except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error return b2, realm diff --git a/.sdkharness/tests/conformance/lock.bucket_default b/.sdkharness/tests/conformance/lock.bucket_default index facb2590..976a39f4 100755 --- a/.sdkharness/tests/conformance/lock.bucket_default +++ b/.sdkharness/tests/conformance/lock.bucket_default @@ -18,10 +18,9 @@ The read-back shape -- defaultRetention {mode, period {duration, unit}} -- is the native API's: https://www.backblaze.com/docs/cloud-storage-enable-object-lock-with-the-native-api -Written from bin/conformance/b2-sdk-python/files.upload, the reference check, -and bin/conformance/README.md, the contract. It exercises b2sdk from this exact repository -checkout; the API was taken from -docs/sdks/b2-sdk-python/card.md and the published b2sdk docs; the assertion now lives with the SDK code it exercises. +Written from files.upload in this directory, the reference check. It exercises +b2sdk from this exact repository checkout; the API usage is taken from the +published b2sdk documentation. """ import os @@ -30,7 +29,7 @@ import time import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' CAPABILITY = 'lock.bucket_default' @@ -39,8 +38,10 @@ PAYLOAD_SIZE = 256 DAY_MILLIS = 24 * 60 * 60 * 1000 TOLERANCE_MILLIS = 30 * 60 * 1000 -# The closed reason set, mirrored from bin/conformance/README.md. The runner -# enforces it; naming one outside this tuple is a FAIL it manufactures. +# The closed reason set. `lib/contract.py` only turns `no-realm-option`, +# `missing-runtime` and `not-claimed` into a SKIP. `unreachable` and +# `unauthorized` are never reported at the simulator: the runner supplies both +# the server and the credential, so those are FAIL, not amber. REASONS = ( 'no-realm-option', 'unreachable', @@ -51,7 +52,7 @@ REASONS = ( ) # A requested target maps to a realm or to amber. `simulator` is the harness's -# own server (bin/simulator/serve.mjs), whose URL the runner exports as +# own loopback server, whose URL the dispatcher exports as # CONFORMANCE_SIMULATOR_URL; unset, there is nothing to reach: no-realm-option. REALMS = { 'simulator': os.environ.get('CONFORMANCE_SIMULATOR_URL') or None, @@ -80,7 +81,7 @@ class Amber(Exception): class Failure(Exception): - """A golden-path step that did not do what the card claims.""" + """A golden-path step that did not do what the capability requires.""" def __init__(self, step: str, detail: str) -> None: self.step = step @@ -104,8 +105,10 @@ def note(text: str) -> None: def step(name, action): """Run one golden-path step, converting the environment's own errors. - A credential NEVER reaches the detail: only the exception class name and, - for a classified network/auth error, the class that classified it. + A credential NEVER reaches the detail: only the exception class name. + Every error here is a FAIL: the runner supplies both the simulator and the + credential, so an auth error, access denied, connection error or SDK error + is a defect, never an amber (COULD-NOT-RUN) result. """ try: return action() @@ -113,10 +116,6 @@ def step(name, action): raise except Exception as error: # noqa: BLE001 -- classified below, never echoed name_of = type(error).__name__ - if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f"{name_of} at step '{name}'") from error - if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'): - raise Amber('unreachable', f"{name_of} at step '{name}'") from error raise Failure(name, name_of) from error @@ -161,6 +160,9 @@ def prepare(target): try: import b2sdk.v3 as b2 except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error return b2, realm diff --git a/.sdkharness/tests/conformance/lock.bypass_governance b/.sdkharness/tests/conformance/lock.bypass_governance index 570f778b..a914aaaa 100755 --- a/.sdkharness/tests/conformance/lock.bypass_governance +++ b/.sdkharness/tests/conformance/lock.bypass_governance @@ -28,10 +28,9 @@ InvalidAuthToken (bad or expired token), any other Unauthorized code, a 5xx or a network error does not count. The exact error code and message are not verified against real B2 (UNVERIFIED in the simulator ledger, docs/FIDELITY.md). -Written from bin/conformance/b2-sdk-python/files.upload, the reference check, -and bin/conformance/README.md, the contract. It exercises b2sdk from this exact repository -checkout; the API was taken from -docs/sdks/b2-sdk-python/card.md and the published b2sdk docs; the assertion now lives with the SDK code it exercises. +Written from files.upload in this directory, the reference check. It exercises +b2sdk from this exact repository checkout; the API usage is taken from the +published b2sdk documentation. """ import os @@ -40,7 +39,7 @@ import time import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' CAPABILITY = 'lock.bypass_governance' @@ -48,8 +47,10 @@ OBJECT_NAME = 'st/bypass-governance.txt' PAYLOAD_SIZE = 256 DAY_MILLIS = 24 * 60 * 60 * 1000 -# The closed reason set, mirrored from bin/conformance/README.md. The runner -# enforces it; naming one outside this tuple is a FAIL it manufactures. +# The closed reason set. `lib/contract.py` only turns `no-realm-option`, +# `missing-runtime` and `not-claimed` into a SKIP. `unreachable` and +# `unauthorized` are never reported at the simulator: the runner supplies both +# the server and the credential, so those are FAIL, not amber. REASONS = ( 'no-realm-option', 'unreachable', @@ -60,7 +61,7 @@ REASONS = ( ) # A requested target maps to a realm or to amber. `simulator` is the harness's -# own server (bin/simulator/serve.mjs), whose URL the runner exports as +# own loopback server, whose URL the dispatcher exports as # CONFORMANCE_SIMULATOR_URL; unset, there is nothing to reach: no-realm-option. REALMS = { 'simulator': os.environ.get('CONFORMANCE_SIMULATOR_URL') or None, @@ -89,7 +90,7 @@ class Amber(Exception): class Failure(Exception): - """A golden-path step that did not do what the card claims.""" + """A golden-path step that did not do what the capability requires.""" def __init__(self, step: str, detail: str) -> None: self.step = step @@ -113,8 +114,10 @@ def note(text: str) -> None: def step(name, action): """Run one golden-path step, converting the environment's own errors. - A credential NEVER reaches the detail: only the exception class name and, - for a classified network/auth error, the class that classified it. + A credential NEVER reaches the detail: only the exception class name. + Every error here is a FAIL: the runner supplies both the simulator and the + credential, so an auth error, access denied, connection error or SDK error + is a defect, never an amber (COULD-NOT-RUN) result. """ try: return action() @@ -122,10 +125,6 @@ def step(name, action): raise except Exception as error: # noqa: BLE001 -- classified below, never echoed name_of = type(error).__name__ - if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f"{name_of} at step '{name}'") from error - if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'): - raise Amber('unreachable', f"{name_of} at step '{name}'") from error raise Failure(name, name_of) from error @@ -180,6 +179,9 @@ def prepare(target): try: import b2sdk.v3 as b2 except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error return b2, realm diff --git a/.sdkharness/tests/conformance/lock.legal_hold b/.sdkharness/tests/conformance/lock.legal_hold index af39e7fc..d2009cff 100755 --- a/.sdkharness/tests/conformance/lock.legal_hold +++ b/.sdkharness/tests/conformance/lock.legal_hold @@ -18,10 +18,9 @@ https://www.backblaze.com/apidocs/b2-update-file-legal-hold given file, then that file is protected from being deleted." https://www.backblaze.com/docs/cloud-storage-object-lock -Written from bin/conformance/b2-sdk-python/files.upload, the reference check, -and bin/conformance/README.md, the contract. It exercises b2sdk from this exact repository -checkout; the API was taken from -docs/sdks/b2-sdk-python/card.md and the published b2sdk docs; the assertion now lives with the SDK code it exercises. +Written from files.upload in this directory, the reference check. It exercises +b2sdk from this exact repository checkout; the API usage is taken from the +published b2sdk documentation. """ import os @@ -29,15 +28,17 @@ import sys import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' CAPABILITY = 'lock.legal_hold' OBJECT_NAME = 'st/legal-hold.txt' PAYLOAD_SIZE = 256 -# The closed reason set, mirrored from bin/conformance/README.md. The runner -# enforces it; naming one outside this tuple is a FAIL it manufactures. +# The closed reason set. `lib/contract.py` only turns `no-realm-option`, +# `missing-runtime` and `not-claimed` into a SKIP. `unreachable` and +# `unauthorized` are never reported at the simulator: the runner supplies both +# the server and the credential, so those are FAIL, not amber. REASONS = ( 'no-realm-option', 'unreachable', @@ -48,7 +49,7 @@ REASONS = ( ) # A requested target maps to a realm or to amber. `simulator` is the harness's -# own server (bin/simulator/serve.mjs), whose URL the runner exports as +# own loopback server, whose URL the dispatcher exports as # CONFORMANCE_SIMULATOR_URL; unset, there is nothing to reach: no-realm-option. REALMS = { 'simulator': os.environ.get('CONFORMANCE_SIMULATOR_URL') or None, @@ -77,7 +78,7 @@ class Amber(Exception): class Failure(Exception): - """A golden-path step that did not do what the card claims.""" + """A golden-path step that did not do what the capability requires.""" def __init__(self, step: str, detail: str) -> None: self.step = step @@ -101,8 +102,10 @@ def note(text: str) -> None: def step(name, action): """Run one golden-path step, converting the environment's own errors. - A credential NEVER reaches the detail: only the exception class name and, - for a classified network/auth error, the class that classified it. + A credential NEVER reaches the detail: only the exception class name. + Every error here is a FAIL: the runner supplies both the simulator and the + credential, so an auth error, access denied, connection error or SDK error + is a defect, never an amber (COULD-NOT-RUN) result. """ try: return action() @@ -110,10 +113,6 @@ def step(name, action): raise except Exception as error: # noqa: BLE001 -- classified below, never echoed name_of = type(error).__name__ - if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f"{name_of} at step '{name}'") from error - if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'): - raise Amber('unreachable', f"{name_of} at step '{name}'") from error raise Failure(name, name_of) from error @@ -158,6 +157,9 @@ def prepare(target): try: import b2sdk.v3 as b2 except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error return b2, realm diff --git a/.sdkharness/tests/conformance/lock.per_file_retention b/.sdkharness/tests/conformance/lock.per_file_retention index 93f28b82..b4af79d9 100755 --- a/.sdkharness/tests/conformance/lock.per_file_retention +++ b/.sdkharness/tests/conformance/lock.per_file_retention @@ -30,10 +30,9 @@ InvalidAuthToken (bad or expired token), any other Unauthorized code, a 5xx or a network error does not count. The exact error code and message are not verified against real B2 (UNVERIFIED in the simulator ledger, docs/FIDELITY.md). -Written from bin/conformance/b2-sdk-python/files.upload, the reference check, -and bin/conformance/README.md, the contract. It exercises b2sdk from this exact repository -checkout; the API was taken from -docs/sdks/b2-sdk-python/card.md and the published b2sdk docs; the assertion now lives with the SDK code it exercises. +Written from files.upload in this directory, the reference check. It exercises +b2sdk from this exact repository checkout; the API usage is taken from the +published b2sdk documentation. """ import os @@ -42,7 +41,7 @@ import time import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' CAPABILITY = 'lock.per_file_retention' @@ -50,8 +49,10 @@ OBJECT_NAME = 'st/per-file-retention.txt' PAYLOAD_SIZE = 256 TWO_DAYS_MILLIS = 2 * 24 * 60 * 60 * 1000 -# The closed reason set, mirrored from bin/conformance/README.md. The runner -# enforces it; naming one outside this tuple is a FAIL it manufactures. +# The closed reason set. `lib/contract.py` only turns `no-realm-option`, +# `missing-runtime` and `not-claimed` into a SKIP. `unreachable` and +# `unauthorized` are never reported at the simulator: the runner supplies both +# the server and the credential, so those are FAIL, not amber. REASONS = ( 'no-realm-option', 'unreachable', @@ -62,7 +63,7 @@ REASONS = ( ) # A requested target maps to a realm or to amber. `simulator` is the harness's -# own server (bin/simulator/serve.mjs), whose URL the runner exports as +# own loopback server, whose URL the dispatcher exports as # CONFORMANCE_SIMULATOR_URL; unset, there is nothing to reach: no-realm-option. REALMS = { 'simulator': os.environ.get('CONFORMANCE_SIMULATOR_URL') or None, @@ -91,7 +92,7 @@ class Amber(Exception): class Failure(Exception): - """A golden-path step that did not do what the card claims.""" + """A golden-path step that did not do what the capability requires.""" def __init__(self, step: str, detail: str) -> None: self.step = step @@ -115,8 +116,10 @@ def note(text: str) -> None: def step(name, action): """Run one golden-path step, converting the environment's own errors. - A credential NEVER reaches the detail: only the exception class name and, - for a classified network/auth error, the class that classified it. + A credential NEVER reaches the detail: only the exception class name. + Every error here is a FAIL: the runner supplies both the simulator and the + credential, so an auth error, access denied, connection error or SDK error + is a defect, never an amber (COULD-NOT-RUN) result. """ try: return action() @@ -124,10 +127,6 @@ def step(name, action): raise except Exception as error: # noqa: BLE001 -- classified below, never echoed name_of = type(error).__name__ - if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f"{name_of} at step '{name}'") from error - if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'): - raise Amber('unreachable', f"{name_of} at step '{name}'") from error raise Failure(name, name_of) from error @@ -182,6 +181,9 @@ def prepare(target): try: import b2sdk.v3 as b2 except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error return b2, realm diff --git a/.sdkharness/tests/conformance/urls.native_download b/.sdkharness/tests/conformance/urls.native_download index 282d2a02..8af211ee 100755 --- a/.sdkharness/tests/conformance/urls.native_download +++ b/.sdkharness/tests/conformance/urls.native_download @@ -10,7 +10,7 @@ claims it): Assert 200 with the correct bytes, then 401 The share token MUST be the B2-NATIVE shape: b2sdk has no S3 presigning at - all (the card records that), so a SigV4 query string appearing here would + all (the capability record says so), so a SigV4 query string appearing here would mean this check is testing something other than what the cell claims. DOCS. The expected values are B2's published NATIVE contract, not SigV4. @@ -26,10 +26,9 @@ https://www.backblaze.com/apidocs/b2-download-file-by-name The assertion below matches using the header form: a /file/ URL with no query string, the token sent as Authorization, no X-Amz-* material, 401. -Written from bin/conformance/b2-sdk-python/files.upload, the reference check, -and bin/conformance/README.md, the contract. It exercises b2sdk from this exact repository -checkout; the API was taken from -docs/sdks/b2-sdk-python/card.md and the published b2sdk docs; the assertion now lives with the SDK code it exercises. +Written from files.upload in this directory, the reference check. It exercises +b2sdk from this exact repository checkout; the API usage is taken from the +published b2sdk documentation. """ import hashlib @@ -41,7 +40,7 @@ import urllib.request import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' CAPABILITY = 'urls.native_download' @@ -51,8 +50,10 @@ PAYLOAD_SIZE = 1024 TTL_SECONDS = 1 EXPIRY_SLACK_SECONDS = 4 -# The closed reason set, mirrored from bin/conformance/README.md. The runner -# enforces it; naming one outside this tuple is a FAIL it manufactures. +# The closed reason set. `lib/contract.py` only turns `no-realm-option`, +# `missing-runtime` and `not-claimed` into a SKIP. `unreachable` and +# `unauthorized` are never reported at the simulator: the runner supplies both +# the server and the credential, so those are FAIL, not amber. REASONS = ( 'no-realm-option', 'unreachable', @@ -63,7 +64,7 @@ REASONS = ( ) # A requested target maps to a realm or to amber. `simulator` is the harness's -# own server (bin/simulator/serve.mjs), whose URL the runner exports as +# own loopback server, whose URL the dispatcher exports as # CONFORMANCE_SIMULATOR_URL; unset, there is nothing to reach: no-realm-option. REALMS = { 'simulator': os.environ.get('CONFORMANCE_SIMULATOR_URL') or None, @@ -92,7 +93,7 @@ class Amber(Exception): class Failure(Exception): - """A golden-path step that did not do what the card claims.""" + """A golden-path step that did not do what the capability requires.""" def __init__(self, step: str, detail: str) -> None: self.step = step @@ -116,8 +117,10 @@ def note(text: str) -> None: def step(name, action): """Run one golden-path step, converting the environment's own errors. - A credential NEVER reaches the detail: only the exception class name and, - for a classified network/auth error, the class that classified it. + A credential NEVER reaches the detail: only the exception class name. + Every error here is a FAIL: the runner supplies both the simulator and the + credential, so an auth error, access denied, connection error or SDK error + is a defect, never an amber (COULD-NOT-RUN) result. """ try: return action() @@ -125,10 +128,6 @@ def step(name, action): raise except Exception as error: # noqa: BLE001 -- classified below, never echoed name_of = type(error).__name__ - if name_of in ('Unauthorized', 'InvalidAuthToken', 'AccessDenied'): - raise Amber('unauthorized', f"{name_of} at step '{name}'") from error - if name_of in ('gaierror', 'ConnectionError', 'ServiceError', 'B2ConnectionError'): - raise Amber('unreachable', f"{name_of} at step '{name}'") from error raise Failure(name, name_of) from error @@ -173,6 +172,9 @@ def prepare(target): try: import b2sdk.v3 as b2 except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error return b2, realm @@ -295,8 +297,8 @@ def run(target: str) -> None: except urllib.error.HTTPError as error: raise Failure('fetch', f'the authorized fetch returned HTTP {error.code}') from error except urllib.error.URLError as error: - raise Amber( - 'unreachable', f'{type(error.reason).__name__} fetching the download url' + raise Failure( + 'fetch', f'{type(error.reason).__name__} fetching the download url' ) from error if status != 200: @@ -316,8 +318,8 @@ def run(target: str) -> None: 'expiry', f'the expired token returned HTTP {error.code}, expected 401' ) from error except urllib.error.URLError as error: - raise Amber( - 'unreachable', f'{type(error.reason).__name__} re-fetching after expiry' + raise Failure( + 'expiry', f'{type(error.reason).__name__} re-fetching after expiry' ) from error else: raise Failure('expiry', f'the expired token still returned HTTP {expired_status}') diff --git a/.sdkharness/tests/health-golden-path b/.sdkharness/tests/health-golden-path index 9d150599..a96931f1 100755 --- a/.sdkharness/tests/health-golden-path +++ b/.sdkharness/tests/health-golden-path @@ -16,6 +16,9 @@ SCENARIO = 'golden-path' SIMULATOR_CREDENTIAL = ('test-key-id', 'test-key') REPOSITORY_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(Path(__file__).resolve().parent / 'lib')) +from loopback_guard import scrub_proxy_environment # noqa: E402 + class CheckFailure(Exception): """A failed lifecycle step with a credential-safe reason.""" @@ -144,6 +147,7 @@ def run_health( def main() -> int: + scrub_proxy_environment(os.environ) try: run_health(os.environ) except CheckFailure as error: diff --git a/.sdkharness/tests/lib/contract.py b/.sdkharness/tests/lib/contract.py index 86da62df..8d53f477 100644 --- a/.sdkharness/tests/lib/contract.py +++ b/.sdkharness/tests/lib/contract.py @@ -10,9 +10,17 @@ from pathlib import Path from urllib.parse import urlsplit +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from loopback_guard import scrub_proxy_environment # noqa: E402 + SLUG = 'b2-sdk-python' REPOSITORY_ROOT = Path(__file__).resolve().parents[3] SCENARIO_ROOT = Path(__file__).resolve().parents[1] +# The only COULD-NOT-RUN reasons that may become SKIP: the question genuinely +# could not be asked. At a simulator the harness supplies the server and the +# credential, so `unreachable`, `unauthorized` and an SDK import error are never +# legitimate ambers: any other reason is a FAIL (the CLI's SKIP policy). +SKIP_REASONS = frozenset({'no-realm-option', 'missing-runtime', 'no-client-option', 'not-claimed'}) SCENARIO_RE = re.compile(r'^[A-Za-z0-9_.-]+$') @@ -61,6 +69,7 @@ def validate_environment(level: str, environment: Mapping[str, str]) -> tuple[st def child_environment(level: str, environment: Mapping[str, str]) -> dict[str, str]: child = {name: value for name, value in environment.items() if not name.startswith('B2_')} + scrub_proxy_environment(child) child['PYTHONPATH'] = str(REPOSITORY_ROOT) child['B2_APPLICATION_KEY_ID'] = 'test-key-id' child['B2_APPLICATION_KEY'] = 'test-key' @@ -96,7 +105,10 @@ def translate(level: str, scenario: str, returncode: int, output: str) -> tuple[ if verdict.startswith('COULD-NOT-RUN (') and verdict.endswith(')'): if returncode != 0: return 'FAIL', f'contract: SKIP exited {returncode}', 1 - return 'SKIP', verdict[15:-1], 0 + reason = verdict[15:-1] + if reason.split(' -- ', 1)[0] not in SKIP_REASONS: + return 'FAIL', f'contract: {reason}', 1 + return 'SKIP', reason, 0 return 'FAIL', 'contract: malformed standing verdict', 1 diff --git a/.sdkharness/tests/lib/loopback_guard.py b/.sdkharness/tests/lib/loopback_guard.py index 2bd38bab..ea32a35d 100644 --- a/.sdkharness/tests/lib/loopback_guard.py +++ b/.sdkharness/tests/lib/loopback_guard.py @@ -10,8 +10,40 @@ from __future__ import annotations import os +from collections.abc import MutableMapping from urllib.parse import urlsplit +PROXY_VARIABLES = frozenset( + {'http_proxy', 'https_proxy', 'all_proxy', 'ftp_proxy', 'no_proxy', 'request_method'} +) + + +def scrub_proxy_environment(environment: MutableMapping[str, str]) -> None: + """Remove proxy settings so a stray proxy cannot change a loopback result. + + `requests` and `urllib` honor `HTTP_PROXY`, `HTTPS_PROXY` and `ALL_PROXY` + (any case) even for 127.0.0.1, which turns a passing scenario into an + "unreachable" one on a machine that has a proxy configured. The proxy + variables are dropped and `NO_PROXY` is pinned to the loopback address, so + nothing but the simulator is ever reached directly and nothing is proxied. + `REQUEST_METHOD` (a CGI marker that makes `requests` ignore `HTTP_PROXY`) + is dropped too so the behavior does not depend on it. + """ + for name in list(environment): + if name.lower() in PROXY_VARIABLES: + del environment[name] + environment['NO_PROXY'] = '127.0.0.1' + environment['no_proxy'] = '127.0.0.1' + + +def missing_runtime(error: ImportError) -> bool: + """True only when `b2sdk` itself is not installed (no such top-level module). + + Any other ImportError means b2sdk is installed but broken (a failed or + circular import inside the SDK): that is an SDK regression and a FAIL. + """ + return isinstance(error, ModuleNotFoundError) and error.name == 'b2sdk' + def _is_loopback_origin(value: str, scheme: str) -> bool: try: @@ -38,6 +70,7 @@ def refusal(level: str) -> str | None: dispatcher builds: `_TARGET`, `_SIMULATOR_URL`, optionally `_SIMULATOR_HTTPS_URL`, and `RESILIENCE_CONTROL_URL` for resilience. """ + scrub_proxy_environment(os.environ) prefix = level.upper() target = os.environ.get(f'{prefix}_TARGET', 'simulator') if target != 'simulator': diff --git a/.sdkharness/tests/resilience/api.backoff_503 b/.sdkharness/tests/resilience/api.backoff_503 index 680ae01f..59f915dc 100755 --- a/.sdkharness/tests/resilience/api.backoff_503 +++ b/.sdkharness/tests/resilience/api.backoff_503 @@ -1,9 +1,8 @@ #!/usr/bin/env python3 """b2-sdk-python x api.backoff_503. -Written from the reference check, bin/resilience/b2-sdk-python/upload.retry_503. -The contract is bin/resilience/README.md; the question is the api.backoff_503 row of -bin/resilience/scenarios.tsv. +Written from the reference check, upload.retry_503 in this directory. +The question is the api.backoff_503 scenario of the central sdkharness registry. Fault fault b2_list_file_names 503 service_unavailable, NO Retry-After (count 1), armed after setup Assert the end state: the listing names exactly the fixture; the recovery path, @@ -18,7 +17,7 @@ not present, the developer should retry using an exponential backoff starting wi https://www.backblaze.com/docs/cloud-storage-integration-checklist It exercises b2sdk from this exact repository checkout; -the API is taken from docs/sdks/b2-sdk-python/card.md. +the API is taken from the published b2sdk documentation. """ import json @@ -29,7 +28,7 @@ import urllib.request import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' SCENARIO = 'api.backoff_503' @@ -44,7 +43,7 @@ def is_faulted(e): FLOOR_S = 1.0 -# The closed reason set, mirrored from bin/resilience/README.md. This +# The closed reason set, fixed by the sdkharness resilience contract. This # scenario's `needs` is `-`, so no-client-option is not available to it. REASONS = ('missing-runtime',) @@ -118,6 +117,9 @@ def authorized_bucket(**api_kwargs): try: from b2sdk.v3 import B2Api, InMemoryAccountInfo except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error api = B2Api(InMemoryAccountInfo(), **api_kwargs) step('authenticate', lambda: api.authorize_account('test-key-id', 'test-key', realm=realm)) diff --git a/.sdkharness/tests/resilience/api.retry_after_429 b/.sdkharness/tests/resilience/api.retry_after_429 index 2bad7d67..6468e8c9 100755 --- a/.sdkharness/tests/resilience/api.retry_after_429 +++ b/.sdkharness/tests/resilience/api.retry_after_429 @@ -1,9 +1,8 @@ #!/usr/bin/env python3 """b2-sdk-python x api.retry_after_429. -Written from the reference check, bin/resilience/b2-sdk-python/upload.retry_503. -The contract is bin/resilience/README.md; the question is the api.retry_after_429 row of -bin/resilience/scenarios.tsv. +Written from the reference check, upload.retry_503 in this directory. +The question is the api.retry_after_429 scenario of the central sdkharness registry. Fault fault b2_list_file_names 429 too_many_requests retryAfter=2 (count 1), armed after setup Assert the end state: the listing names exactly the fixture; the recovery path, @@ -17,7 +16,7 @@ The Integration Checklist, Error Handling, 429: "the response may also include a https://www.backblaze.com/docs/cloud-storage-integration-checklist It exercises b2sdk from this exact repository checkout; -the API is taken from docs/sdks/b2-sdk-python/card.md. +the API is taken from the published b2sdk documentation. """ import json @@ -28,7 +27,7 @@ import urllib.request import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' SCENARIO = 'api.retry_after_429' @@ -49,7 +48,7 @@ def is_faulted(e): FLOOR_S = 2.0 -# The closed reason set, mirrored from bin/resilience/README.md. This +# The closed reason set, fixed by the sdkharness resilience contract. This # scenario's `needs` is `-`, so no-client-option is not available to it. REASONS = ('missing-runtime',) @@ -123,6 +122,9 @@ def authorized_bucket(**api_kwargs): try: from b2sdk.v3 import B2Api, InMemoryAccountInfo except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error api = B2Api(InMemoryAccountInfo(), **api_kwargs) step('authenticate', lambda: api.authorize_account('test-key-id', 'test-key', realm=realm)) diff --git a/.sdkharness/tests/resilience/api.retry_after_503 b/.sdkharness/tests/resilience/api.retry_after_503 index 79e5037b..527b41e3 100755 --- a/.sdkharness/tests/resilience/api.retry_after_503 +++ b/.sdkharness/tests/resilience/api.retry_after_503 @@ -1,9 +1,8 @@ #!/usr/bin/env python3 """b2-sdk-python x api.retry_after_503. -Written from the reference check, bin/resilience/b2-sdk-python/upload.retry_503. -The contract is bin/resilience/README.md; the question is the api.retry_after_503 row of -bin/resilience/scenarios.tsv. +Written from the reference check, upload.retry_503 in this directory. +The question is the api.retry_after_503 scenario of the central sdkharness registry. Fault fault b2_list_file_names 503 service_unavailable retryAfter=2 (count 1), armed after setup Assert the end state: the listing names exactly the fixture; the recovery path, @@ -18,7 +17,7 @@ before re-issuing the command." https://www.backblaze.com/docs/cloud-storage-integration-checklist It exercises b2sdk from this exact repository checkout; -the API is taken from docs/sdks/b2-sdk-python/card.md. +the API is taken from the published b2sdk documentation. """ import json @@ -29,7 +28,7 @@ import urllib.request import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' SCENARIO = 'api.retry_after_503' @@ -50,7 +49,7 @@ def is_faulted(e): FLOOR_S = 2.0 -# The closed reason set, mirrored from bin/resilience/README.md. This +# The closed reason set, fixed by the sdkharness resilience contract. This # scenario's `needs` is `-`, so no-client-option is not available to it. REASONS = ('missing-runtime',) @@ -124,6 +123,9 @@ def authorized_bucket(**api_kwargs): try: from b2sdk.v3 import B2Api, InMemoryAccountInfo except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error api = B2Api(InMemoryAccountInfo(), **api_kwargs) step('authenticate', lambda: api.authorize_account('test-key-id', 'test-key', realm=realm)) diff --git a/.sdkharness/tests/resilience/auth.clock_expiry b/.sdkharness/tests/resilience/auth.clock_expiry index 542035af..8e935c20 100755 --- a/.sdkharness/tests/resilience/auth.clock_expiry +++ b/.sdkharness/tests/resilience/auth.clock_expiry @@ -1,9 +1,8 @@ #!/usr/bin/env python3 """b2-sdk-python x auth.clock_expiry. -Written from the reference check, bin/resilience/b2-sdk-python/upload.retry_503. -The contract is bin/resilience/README.md; the question is the auth.clock_expiry row of -bin/resilience/scenarios.tsv. +Written from the reference check, upload.retry_503 in this directory. +The question is the auth.clock_expiry scenario of the central sdkharness registry. Fault clock 86400001 -- POST /clock {advanceMs: 86400001}, after setup, so the account token the one live B2Api session holds is past its 24 h life. @@ -20,12 +19,12 @@ or expired_auth_token you should call b2_authorize_account again to get a new au https://www.backblaze.com/docs/cloud-storage-integration-checklist `needs: long-lived-session` is met: one B2Api over an InMemoryAccountInfo is -one session that outlives the virtual advance, and the card records the token -as "cached in an AccountInfo and auto-refreshed on expiry" (card.md, Auth model: -b2sdk/_internal/api.py:206, session.py:107, session.py:92). +one session that outlives the virtual advance, and the token is cached in the +AccountInfo and auto-refreshed on expiry (b2sdk/_internal/api.py, +b2sdk/_internal/session.py). It exercises b2sdk from this exact repository checkout; -the API is taken from docs/sdks/b2-sdk-python/card.md. +the API is taken from the published b2sdk documentation. """ import json @@ -35,7 +34,7 @@ import urllib.request import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' SCENARIO = 'auth.clock_expiry' @@ -48,8 +47,8 @@ def is_faulted(e): return e['status'] == 401 and e['fault'] is None -# The closed reason set, mirrored from bin/resilience/README.md. This -# scenario's `needs` is `long-lived-session`; the card shows b2sdk meets it, +# The closed reason set, fixed by the sdkharness resilience contract. This +# scenario's `needs` is `long-lived-session`; b2sdk meets it, # so this check never emits no-client-option. REASONS = ('missing-runtime', 'no-client-option') @@ -123,6 +122,9 @@ def authorized_bucket(**api_kwargs): try: from b2sdk.v3 import B2Api, InMemoryAccountInfo except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error api = B2Api(InMemoryAccountInfo(), **api_kwargs) step('authenticate', lambda: api.authorize_account('test-key-id', 'test-key', realm=realm)) diff --git a/.sdkharness/tests/resilience/auth.expired_401 b/.sdkharness/tests/resilience/auth.expired_401 index a3af2919..dafee56c 100755 --- a/.sdkharness/tests/resilience/auth.expired_401 +++ b/.sdkharness/tests/resilience/auth.expired_401 @@ -1,9 +1,8 @@ #!/usr/bin/env python3 """b2-sdk-python x auth.expired_401. -Written from the reference check, bin/resilience/b2-sdk-python/upload.retry_503. -The contract is bin/resilience/README.md; the question is the auth.expired_401 row of -bin/resilience/scenarios.tsv. +Written from the reference check, upload.retry_503 in this directory. +The question is the auth.expired_401 scenario of the central sdkharness registry. Fault fault b2_list_file_names 401 expired_auth_token (count 1), armed after setup Assert the recovery path, from GET /journal: exactly one injected 401 on @@ -20,7 +19,7 @@ or expired_auth_token you should call b2_authorize_account again to get a new au https://www.backblaze.com/docs/cloud-storage-integration-checklist It exercises b2sdk from this exact repository checkout; -the API is taken from docs/sdks/b2-sdk-python/card.md. +the API is taken from the published b2sdk documentation. """ import json @@ -30,7 +29,7 @@ import urllib.request import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' SCENARIO = 'auth.expired_401' @@ -43,7 +42,7 @@ def is_faulted(e): return e['endpoint'] == 'b2_list_file_names' and e['fault'] == 'injected' and e['status'] == 401 -# The closed reason set, mirrored from bin/resilience/README.md. This +# The closed reason set, fixed by the sdkharness resilience contract. This # scenario's `needs` is `-`, so no-client-option is not available to it. REASONS = ('missing-runtime',) @@ -117,6 +116,9 @@ def authorized_bucket(**api_kwargs): try: from b2sdk.v3 import B2Api, InMemoryAccountInfo except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error api = B2Api(InMemoryAccountInfo(), **api_kwargs) step('authenticate', lambda: api.authorize_account('test-key-id', 'test-key', realm=realm)) diff --git a/.sdkharness/tests/resilience/download.retry_503 b/.sdkharness/tests/resilience/download.retry_503 index eac3982a..b8b24866 100755 --- a/.sdkharness/tests/resilience/download.retry_503 +++ b/.sdkharness/tests/resilience/download.retry_503 @@ -1,9 +1,8 @@ #!/usr/bin/env python3 """b2-sdk-python x download.retry_503. -Written from the reference check, bin/resilience/b2-sdk-python/upload.retry_503. -The contract is bin/resilience/README.md; the question is the download.retry_503 row of -bin/resilience/scenarios.tsv. +Written from the reference check, upload.retry_503 in this directory. +The question is the download.retry_503 scenario of the central sdkharness registry. Fault fault b2_download_file_by_id 503 service_unavailable (count 1), armed after the fixture is uploaded @@ -18,7 +17,7 @@ using an exponential backoff starting with 1 second." https://www.backblaze.com/docs/cloud-storage-integration-checklist It exercises b2sdk from this exact repository checkout; -the API is taken from docs/sdks/b2-sdk-python/card.md. +the API is taken from the published b2sdk documentation. """ import io @@ -29,7 +28,7 @@ import urllib.request import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' SCENARIO = 'download.retry_503' @@ -46,7 +45,7 @@ def is_faulted(e): ) -# The closed reason set, mirrored from bin/resilience/README.md. This +# The closed reason set, fixed by the sdkharness resilience contract. This # scenario's `needs` is `-`, so no-client-option is not available to it. REASONS = ('missing-runtime',) @@ -120,6 +119,9 @@ def authorized_bucket(**api_kwargs): try: from b2sdk.v3 import B2Api, InMemoryAccountInfo except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error api = B2Api(InMemoryAccountInfo(), **api_kwargs) step('authenticate', lambda: api.authorize_account('test-key-id', 'test-key', realm=realm)) diff --git a/.sdkharness/tests/resilience/part.retry_503 b/.sdkharness/tests/resilience/part.retry_503 index d8cdbe47..b534a21d 100755 --- a/.sdkharness/tests/resilience/part.retry_503 +++ b/.sdkharness/tests/resilience/part.retry_503 @@ -1,9 +1,8 @@ #!/usr/bin/env python3 """b2-sdk-python x part.retry_503. -Written from the reference check, bin/resilience/b2-sdk-python/upload.retry_503. -The contract is bin/resilience/README.md; the question is the part.retry_503 row of -bin/resilience/scenarios.tsv. +Written from the reference check, upload.retry_503 in this directory. +The question is the part.retry_503 scenario of the central sdkharness registry. Fault fault b2_upload_part 503 service_unavailable (count 1), armed after setup, so the first part of a three-part large file gets it (one upload worker) @@ -23,7 +22,7 @@ upload using the new URL and auth key pair"; among them: https://www.backblaze.com/docs/cloud-storage-integration-checklist It exercises b2sdk from this exact repository checkout; -the API is taken from docs/sdks/b2-sdk-python/card.md. +the API is taken from the published b2sdk documentation. """ import io @@ -34,7 +33,7 @@ import urllib.request import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' SCENARIO = 'part.retry_503' @@ -49,7 +48,7 @@ def is_faulted(e): PARTS = 3 -# The closed reason set, mirrored from bin/resilience/README.md. This +# The closed reason set, fixed by the sdkharness resilience contract. This # scenario's `needs` is `-`, so no-client-option is not available to it. REASONS = ('missing-runtime',) @@ -123,6 +122,9 @@ def authorized_bucket(**api_kwargs): try: from b2sdk.v3 import B2Api, InMemoryAccountInfo except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error api = B2Api(InMemoryAccountInfo(), **api_kwargs) step('authenticate', lambda: api.authorize_account('test-key-id', 'test-key', realm=realm)) diff --git a/.sdkharness/tests/resilience/upload.cap_exceeded_403 b/.sdkharness/tests/resilience/upload.cap_exceeded_403 index 2f0dd353..161f4f39 100755 --- a/.sdkharness/tests/resilience/upload.cap_exceeded_403 +++ b/.sdkharness/tests/resilience/upload.cap_exceeded_403 @@ -1,9 +1,8 @@ #!/usr/bin/env python3 """b2-sdk-python x upload.cap_exceeded_403. -Written from the reference check, bin/resilience/b2-sdk-python/upload.retry_503. -The contract is bin/resilience/README.md; the question is the upload.cap_exceeded_403 row of -bin/resilience/scenarios.tsv. +Written from the reference check, upload.retry_503 in this directory. +The question is the upload.cap_exceeded_403 scenario of the central sdkharness registry. Fault fault b2_upload_file 403 cap_exceeded (count 1), armed after setup Assert the end state: the upload RAISES a b2sdk error to the caller; and the @@ -18,7 +17,7 @@ inform the user they need to log in and review their B2 Caps." https://www.backblaze.com/docs/cloud-storage-integration-checklist It exercises b2sdk from this exact repository checkout; -the API is taken from docs/sdks/b2-sdk-python/card.md. +the API is taken from the published b2sdk documentation. """ import json @@ -28,7 +27,7 @@ import urllib.request import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' SCENARIO = 'upload.cap_exceeded_403' @@ -41,7 +40,7 @@ def is_faulted(e): return e['endpoint'] == 'b2_upload_file' and e['fault'] == 'injected' and e['status'] == 403 -# The closed reason set, mirrored from bin/resilience/README.md. This +# The closed reason set, fixed by the sdkharness resilience contract. This # scenario's `needs` is `-`, so no-client-option is not available to it. REASONS = ('missing-runtime',) @@ -115,6 +114,9 @@ def authorized_bucket(**api_kwargs): try: from b2sdk.v3 import B2Api, InMemoryAccountInfo except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error api = B2Api(InMemoryAccountInfo(), **api_kwargs) step('authenticate', lambda: api.authorize_account('test-key-id', 'test-key', realm=realm)) diff --git a/.sdkharness/tests/resilience/upload.expired_token_401 b/.sdkharness/tests/resilience/upload.expired_token_401 index a5dc9bb7..32c58f0c 100755 --- a/.sdkharness/tests/resilience/upload.expired_token_401 +++ b/.sdkharness/tests/resilience/upload.expired_token_401 @@ -1,9 +1,8 @@ #!/usr/bin/env python3 """b2-sdk-python x upload.expired_token_401. -Written from the reference check, bin/resilience/b2-sdk-python/upload.retry_503. -The contract is bin/resilience/README.md; the question is the upload.expired_token_401 row of -bin/resilience/scenarios.tsv. +Written from the reference check, upload.retry_503 in this directory. +The question is the upload.expired_token_401 scenario of the central sdkharness registry. Fault fault b2_upload_file 401 expired_auth_token (count 1), armed after setup Assert the recovery path, from GET /journal: exactly one faulted b2_upload_file, @@ -20,7 +19,7 @@ and auth key pair". https://www.backblaze.com/docs/cloud-storage-integration-checklist It exercises b2sdk from this exact repository checkout; -the API is taken from docs/sdks/b2-sdk-python/card.md. +the API is taken from the published b2sdk documentation. """ import hashlib @@ -32,7 +31,7 @@ import urllib.request import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' SCENARIO = 'upload.expired_token_401' @@ -45,7 +44,7 @@ def is_faulted(e): return e['endpoint'] == 'b2_upload_file' and e['fault'] == 'injected' and e['status'] == 401 -# The closed reason set, mirrored from bin/resilience/README.md. This +# The closed reason set, fixed by the sdkharness resilience contract. This # scenario's `needs` is `-`, so no-client-option is not available to it. REASONS = ('missing-runtime',) @@ -119,6 +118,9 @@ def authorized_bucket(**api_kwargs): try: from b2sdk.v3 import B2Api, InMemoryAccountInfo except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error api = B2Api(InMemoryAccountInfo(), **api_kwargs) step('authenticate', lambda: api.authorize_account('test-key-id', 'test-key', realm=realm)) diff --git a/.sdkharness/tests/resilience/upload.get_url_503 b/.sdkharness/tests/resilience/upload.get_url_503 index abebbb3b..89ba80e7 100755 --- a/.sdkharness/tests/resilience/upload.get_url_503 +++ b/.sdkharness/tests/resilience/upload.get_url_503 @@ -1,9 +1,8 @@ #!/usr/bin/env python3 """b2-sdk-python x upload.get_url_503. -Written from the reference check, bin/resilience/b2-sdk-python/upload.retry_503. -The contract is bin/resilience/README.md; the question is the upload.get_url_503 row of -bin/resilience/scenarios.tsv. +Written from the reference check, upload.retry_503 in this directory. +The question is the upload.get_url_503 scenario of the central sdkharness registry. Fault fault b2_get_upload_url 503 service_unavailable (count 1), armed after setup, so the first attempt to obtain an upload URL gets it @@ -19,7 +18,7 @@ using an exponential backoff starting with 1 second." https://www.backblaze.com/docs/cloud-storage-integration-checklist It exercises b2sdk from this exact repository checkout; -the API is taken from docs/sdks/b2-sdk-python/card.md. +the API is taken from the published b2sdk documentation. """ import hashlib @@ -31,7 +30,7 @@ import urllib.request import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' SCENARIO = 'upload.get_url_503' @@ -44,7 +43,7 @@ def is_faulted(e): return e['endpoint'] == 'b2_get_upload_url' and e['fault'] == 'injected' and e['status'] == 503 -# The closed reason set, mirrored from bin/resilience/README.md. This +# The closed reason set, fixed by the sdkharness resilience contract. This # scenario's `needs` is `-`, so no-client-option is not available to it. REASONS = ('missing-runtime',) @@ -118,6 +117,9 @@ def authorized_bucket(**api_kwargs): try: from b2sdk.v3 import B2Api, InMemoryAccountInfo except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error api = B2Api(InMemoryAccountInfo(), **api_kwargs) step('authenticate', lambda: api.authorize_account('test-key-id', 'test-key', realm=realm)) diff --git a/.sdkharness/tests/resilience/upload.reset_before_response b/.sdkharness/tests/resilience/upload.reset_before_response index 1903e153..77d3267e 100755 --- a/.sdkharness/tests/resilience/upload.reset_before_response +++ b/.sdkharness/tests/resilience/upload.reset_before_response @@ -1,9 +1,8 @@ #!/usr/bin/env python3 """b2-sdk-python x upload.reset_before_response. -Written from the reference check, bin/resilience/b2-sdk-python/upload.retry_503. -The contract is bin/resilience/README.md; the question is the upload.reset_before_response row of -bin/resilience/scenarios.tsv. +Written from the reference check, upload.retry_503 in this directory. +The question is the upload.reset_before_response scenario of the central sdkharness registry. Fault wire b2_upload_file reset-before-response (count 1), armed after setup: the upload IS dispatched (state changed), then the connection drops with no @@ -20,7 +19,7 @@ upload using the new URL and auth key pair"; among them: https://www.backblaze.com/docs/cloud-storage-integration-checklist It exercises b2sdk from this exact repository checkout; -the API is taken from docs/sdks/b2-sdk-python/card.md. +the API is taken from the published b2sdk documentation. """ import hashlib @@ -32,7 +31,7 @@ import urllib.request import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' SCENARIO = 'upload.reset_before_response' @@ -45,7 +44,7 @@ def is_faulted(e): return e['endpoint'] == 'b2_upload_file' and e['fault'] == 'reset-before-response' -# The closed reason set, mirrored from bin/resilience/README.md. This +# The closed reason set, fixed by the sdkharness resilience contract. This # scenario's `needs` is `-`, so no-client-option is not available to it. REASONS = ('missing-runtime',) @@ -119,6 +118,9 @@ def authorized_bucket(**api_kwargs): try: from b2sdk.v3 import B2Api, InMemoryAccountInfo except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error api = B2Api(InMemoryAccountInfo(), **api_kwargs) step('authenticate', lambda: api.authorize_account('test-key-id', 'test-key', realm=realm)) diff --git a/.sdkharness/tests/resilience/upload.reset_mid_request b/.sdkharness/tests/resilience/upload.reset_mid_request index a4e11642..a78d35ed 100755 --- a/.sdkharness/tests/resilience/upload.reset_mid_request +++ b/.sdkharness/tests/resilience/upload.reset_mid_request @@ -1,9 +1,8 @@ #!/usr/bin/env python3 """b2-sdk-python x upload.reset_mid_request. -Written from the reference check, bin/resilience/b2-sdk-python/upload.retry_503. -The contract is bin/resilience/README.md; the question is the upload.reset_mid_request row of -bin/resilience/scenarios.tsv. +Written from the reference check, upload.retry_503 in this directory. +The question is the upload.reset_mid_request scenario of the central sdkharness registry. Fault wire b2_upload_file reset-mid-request (count 1), armed after setup: the connection drops after the first request-body chunk, never dispatched @@ -19,7 +18,7 @@ upload using the new URL and auth key pair"; among them: https://www.backblaze.com/docs/cloud-storage-integration-checklist It exercises b2sdk from this exact repository checkout; -the API is taken from docs/sdks/b2-sdk-python/card.md. +the API is taken from the published b2sdk documentation. """ import hashlib @@ -31,7 +30,7 @@ import urllib.request import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' SCENARIO = 'upload.reset_mid_request' @@ -44,7 +43,7 @@ def is_faulted(e): return e['endpoint'] == 'b2_upload_file' and e['fault'] == 'reset-mid-request' -# The closed reason set, mirrored from bin/resilience/README.md. This +# The closed reason set, fixed by the sdkharness resilience contract. This # scenario's `needs` is `-`, so no-client-option is not available to it. REASONS = ('missing-runtime',) @@ -118,6 +117,9 @@ def authorized_bucket(**api_kwargs): try: from b2sdk.v3 import B2Api, InMemoryAccountInfo except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error api = B2Api(InMemoryAccountInfo(), **api_kwargs) step('authenticate', lambda: api.authorize_account('test-key-id', 'test-key', realm=realm)) diff --git a/.sdkharness/tests/resilience/upload.retry_408 b/.sdkharness/tests/resilience/upload.retry_408 index 2399a255..999835f8 100755 --- a/.sdkharness/tests/resilience/upload.retry_408 +++ b/.sdkharness/tests/resilience/upload.retry_408 @@ -1,9 +1,8 @@ #!/usr/bin/env python3 """b2-sdk-python x upload.retry_408. -Written from the reference check, bin/resilience/b2-sdk-python/upload.retry_503. -The contract is bin/resilience/README.md; the question is the upload.retry_408 row of -bin/resilience/scenarios.tsv. +Written from the reference check, upload.retry_503 in this directory. +The question is the upload.retry_408 scenario of the central sdkharness registry. Fault fault b2_upload_file 408 request_timeout (count 1), armed after setup Assert the recovery path, from GET /journal: exactly one faulted b2_upload_file, @@ -20,7 +19,7 @@ upload using the new URL and auth key pair"; among them: https://www.backblaze.com/docs/cloud-storage-integration-checklist It exercises b2sdk from this exact repository checkout; -the API is taken from docs/sdks/b2-sdk-python/card.md. +the API is taken from the published b2sdk documentation. """ import hashlib @@ -32,7 +31,7 @@ import urllib.request import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' SCENARIO = 'upload.retry_408' @@ -45,7 +44,7 @@ def is_faulted(e): return e['endpoint'] == 'b2_upload_file' and e['fault'] == 'injected' and e['status'] == 408 -# The closed reason set, mirrored from bin/resilience/README.md. This +# The closed reason set, fixed by the sdkharness resilience contract. This # scenario's `needs` is `-`, so no-client-option is not available to it. REASONS = ('missing-runtime',) @@ -119,6 +118,9 @@ def authorized_bucket(**api_kwargs): try: from b2sdk.v3 import B2Api, InMemoryAccountInfo except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error api = B2Api(InMemoryAccountInfo(), **api_kwargs) step('authenticate', lambda: api.authorize_account('test-key-id', 'test-key', realm=realm)) diff --git a/.sdkharness/tests/resilience/upload.retry_500 b/.sdkharness/tests/resilience/upload.retry_500 index 84a476a7..189d0068 100755 --- a/.sdkharness/tests/resilience/upload.retry_500 +++ b/.sdkharness/tests/resilience/upload.retry_500 @@ -1,9 +1,8 @@ #!/usr/bin/env python3 """b2-sdk-python x upload.retry_500. -Written from the reference check, bin/resilience/b2-sdk-python/upload.retry_503. -The contract is bin/resilience/README.md; the question is the upload.retry_500 row of -bin/resilience/scenarios.tsv. +Written from the reference check, upload.retry_503 in this directory. +The question is the upload.retry_500 scenario of the central sdkharness registry. Fault fault b2_upload_file 500 internal_error (count 1), armed after setup; asserted on the 500 STATUS only -- the code string is not doc-sourced (PROVENANCE.md) @@ -19,7 +18,7 @@ upload using the new URL and auth key pair"; among them: https://www.backblaze.com/docs/cloud-storage-integration-checklist It exercises b2sdk from this exact repository checkout; -the API is taken from docs/sdks/b2-sdk-python/card.md. +the API is taken from the published b2sdk documentation. """ import hashlib @@ -31,7 +30,7 @@ import urllib.request import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' SCENARIO = 'upload.retry_500' @@ -44,7 +43,7 @@ def is_faulted(e): return e['endpoint'] == 'b2_upload_file' and e['fault'] == 'injected' and e['status'] == 500 -# The closed reason set, mirrored from bin/resilience/README.md. This +# The closed reason set, fixed by the sdkharness resilience contract. This # scenario's `needs` is `-`, so no-client-option is not available to it. REASONS = ('missing-runtime',) @@ -118,6 +117,9 @@ def authorized_bucket(**api_kwargs): try: from b2sdk.v3 import B2Api, InMemoryAccountInfo except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error api = B2Api(InMemoryAccountInfo(), **api_kwargs) step('authenticate', lambda: api.authorize_account('test-key-id', 'test-key', realm=realm)) diff --git a/.sdkharness/tests/resilience/upload.retry_503 b/.sdkharness/tests/resilience/upload.retry_503 index 65708c51..7d04a5cd 100755 --- a/.sdkharness/tests/resilience/upload.retry_503 +++ b/.sdkharness/tests/resilience/upload.retry_503 @@ -2,9 +2,8 @@ """THE REFERENCE CHECK -- b2-sdk-python x upload.retry_503. Every other resilience check is written from this one, the way -bin/conformance/b2-sdk-python/files.upload is for conformance. The contract is -bin/resilience/README.md; the question is the upload.retry_503 row of -bin/resilience/scenarios.tsv. +conformance/files.upload is for conformance. The question is the upload.retry_503 +scenario of the central sdkharness registry. Fault fault b2_upload_file 503 service_unavailable (count 1), armed after setup, so the first upload attempt is the one that gets it @@ -23,7 +22,7 @@ new URL and auth key pair". https://www.backblaze.com/docs/cloud-storage-integration-checklist It exercises b2sdk from this exact repository checkout; -the API is taken from docs/sdks/b2-sdk-python/card.md. +the API is taken from the published b2sdk documentation. """ import hashlib @@ -35,14 +34,14 @@ import urllib.request import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' SCENARIO = 'upload.retry_503' OBJECT_NAME = 'res/retry_503.txt' FAULT = {'on': 'b2_upload_file', 'status': 503, 'code': 'service_unavailable', 'count': 1} -# The closed reason set, mirrored from bin/resilience/README.md. This +# The closed reason set, fixed by the sdkharness resilience contract. This # scenario's `needs` is `-`, so no-client-option is not available to it. REASONS = ('missing-runtime',) @@ -97,6 +96,9 @@ def run() -> None: try: from b2sdk.v3 import B2Api, InMemoryAccountInfo except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error api = B2Api(InMemoryAccountInfo()) diff --git a/.sdkharness/tests/resilience/upload.stall b/.sdkharness/tests/resilience/upload.stall index 7c1949a5..28eeceed 100755 --- a/.sdkharness/tests/resilience/upload.stall +++ b/.sdkharness/tests/resilience/upload.stall @@ -1,13 +1,12 @@ #!/usr/bin/env python3 """b2-sdk-python x upload.stall. -Written from the reference check, bin/resilience/b2-sdk-python/upload.retry_503. -The contract is bin/resilience/README.md; the question is the upload.stall row of -bin/resilience/scenarios.tsv. +Written from the reference check, upload.retry_503 in this directory. +The question is the upload.stall scenario of the central sdkharness registry. Fault wire b2_upload_file stall ms=60000 -- NOT ARMED. The scenario needs a client request timeout short enough to beat a 60 s stall (`needs: request-timeout`) -Assert nothing: docs/sdks/b2-sdk-python/card.md cites no request-timeout option, so +Assert nothing: b2sdk documents no request-timeout option, so the check reports no-client-option instead of guessing at one The expected behaviour is B2's, not the tool's. @@ -18,7 +17,7 @@ upload using the new URL and auth key pair"; among them: https://www.backblaze.com/docs/cloud-storage-integration-checklist It exercises b2sdk from this exact repository checkout; -the API is taken from docs/sdks/b2-sdk-python/card.md. +the API is taken from the published b2sdk documentation. """ import json @@ -28,7 +27,7 @@ import urllib.request import uuid sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib')) -from loopback_guard import refusal # noqa: E402 +from loopback_guard import missing_runtime, refusal # noqa: E402 SLUG = 'b2-sdk-python' SCENARIO = 'upload.stall' @@ -41,7 +40,7 @@ def is_faulted(e): return e['endpoint'] == 'b2_upload_file' and e['fault'] == 'stall' -# The closed reason set, mirrored from bin/resilience/README.md. This +# The closed reason set, fixed by the sdkharness resilience contract. This # scenario's `needs` is `request-timeout`, so no-client-option is available to it. REASONS = ('missing-runtime', 'no-client-option') @@ -115,6 +114,9 @@ def authorized_bucket(**api_kwargs): try: from b2sdk.v3 import B2Api, InMemoryAccountInfo except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error api = B2Api(InMemoryAccountInfo(), **api_kwargs) step('authenticate', lambda: api.authorize_account('test-key-id', 'test-key', realm=realm)) @@ -125,11 +127,11 @@ def authorized_bucket(**api_kwargs): return api, bucket -# What the card was searched for, and why it does not settle the precondition. -# Only sdk-inspector reads the clone, so this check never looks past the card. +# Why this scenario cannot be asked. The published b2sdk documentation names no +# request-timeout option, so the precondition cannot be settled from outside. UNSETTLED = ( - "docs/sdks/b2-sdk-python/card.md cites no request-timeout option: B2HttpApiConfig's " - 'cited parameters are http_session_factory and _raw_api_class, and no citation names ' + "b2sdk documents no request-timeout option: B2HttpApiConfig's " + 'documented parameters are http_session_factory and _raw_api_class, and nothing names ' 'the timeout B2Http passes to requests or a way to shorten it' ) @@ -138,11 +140,14 @@ def run() -> None: try: import b2sdk.v3 # noqa: F401 -- only presence is asked except ImportError as error: + if not missing_runtime(error): + # b2sdk is installed but broken: an SDK regression, not a missing runtime + raise Failure('import', type(error).__name__) from error raise Amber('missing-runtime', 'b2sdk is not installed') from error note( 'unconfirmed, not absent: an http_session_factory returning a Session that forces a ' - 'short timeout MIGHT meet the need, but the card marks session-level behaviour as ' - 'inferred; an sdk-inspector re-inspection asking what timeout b2http.py passes to ' + 'short timeout MIGHT meet the need, but session-level behaviour is not documented; ' + 'reading what timeout b2http.py passes to ' 'Session.request, and whether it is configurable, would settle it' ) raise Amber('no-client-option', UNSETTLED) diff --git a/changelog.d/+sdkharness-fail-not-skip.infrastructure.md b/changelog.d/+sdkharness-fail-not-skip.infrastructure.md new file mode 100644 index 00000000..9d77740a --- /dev/null +++ b/changelog.d/+sdkharness-fail-not-skip.infrastructure.md @@ -0,0 +1 @@ +Repository-owned sdkharness conformance and resilience checks now report an authorization error, access denied, a connection error against the simulator, or an SDK `ImportError` as `FAIL` instead of `COULD-NOT-RUN`/`SKIP`, strip proxy environment variables so a stray proxy cannot change a result, and no longer refer to files that exist only in the central harness. diff --git a/test/unit/test_sdkharness_fail_not_skip.py b/test/unit/test_sdkharness_fail_not_skip.py new file mode 100644 index 00000000..79b1fc9f --- /dev/null +++ b/test/unit/test_sdkharness_fail_not_skip.py @@ -0,0 +1,176 @@ +###################################################################### +# +# File: test/unit/test_sdkharness_fail_not_skip.py +# +# Copyright 2026 Backblaze Inc. All Rights Reserved. +# +# License https://www.backblaze.com/using_b2_code.html +# +###################################################################### +from __future__ import annotations + +import os +import socket +import subprocess +import sys +from importlib.util import module_from_spec, spec_from_file_location +from pathlib import Path + +import pytest + +ROOT = Path(__file__).parents[2] +LIB = ROOT / '.sdkharness' / 'tests' / 'lib' +CONFORMANCE = ROOT / '.sdkharness' / 'tests' / 'conformance' +LEAVES = ('files.upload', 'files.hide', 'keys.multi_bucket') + + +def load(name): + spec = spec_from_file_location(f'sdkharness_{name}', LIB / f'{name}.py') + module = module_from_spec(spec) + sys.path.insert(0, str(LIB)) + try: + spec.loader.exec_module(module) + finally: + sys.path.remove(str(LIB)) + return module + + +def dead_loopback_url(): + with socket.socket() as sock: + sock.bind(('127.0.0.1', 0)) + port = sock.getsockname()[1] + return f'http://127.0.0.1:{port}' # the socket is closed again: nothing listens there + + +def run_leaf(leaf, simulator_url, extra_path=None): + env = { + name: value + for name, value in os.environ.items() + if not name.startswith(('B2_', 'CONFORMANCE_', 'RESILIENCE_', 'SDKHARNESS_')) + } + python_path = [str(ROOT)] + if extra_path: + python_path.insert(0, str(extra_path)) + env.update( + PYTHONPATH=os.pathsep.join(python_path), + CONFORMANCE_TARGET='simulator', + CONFORMANCE_SIMULATOR_URL=simulator_url, + ) + return subprocess.run( + [sys.executable, str(CONFORMANCE / leaf)], + env=env, + stdin=subprocess.DEVNULL, + capture_output=True, + text=True, + timeout=120, + check=False, + ) + + +def dispatcher_verdict(contract, leaf, completed): + return contract.translate('conformance', leaf, completed.returncode, completed.stdout) + + +@pytest.mark.parametrize('leaf', LEAVES) +def test_dead_simulator_port_is_a_fail_not_a_skip(leaf): + contract = load('contract') + completed = run_leaf(leaf, dead_loopback_url()) + assert completed.returncode == 1, completed.stdout + assert 'COULD-NOT-RUN' not in completed.stdout + outcome, _reason, code = dispatcher_verdict(contract, leaf, completed) + assert (outcome, code) == ('FAIL', 1) + + +@pytest.mark.parametrize('leaf', LEAVES) +def test_sdk_import_error_is_a_fail_not_a_skip(leaf, tmp_path): + broken = tmp_path / 'b2sdk' + broken.mkdir() + (broken / '__init__.py').write_text('raise ImportError("circular import inside the SDK")\n') + contract = load('contract') + completed = run_leaf(leaf, dead_loopback_url(), extra_path=tmp_path) + assert completed.returncode == 1, completed.stdout + assert 'COULD-NOT-RUN' not in completed.stdout + assert 'ImportError' in completed.stdout + outcome, _reason, code = dispatcher_verdict(contract, leaf, completed) + assert (outcome, code) == ('FAIL', 1) + + +@pytest.mark.parametrize('leaf', LEAVES) +def test_a_missing_b2sdk_module_is_still_could_not_run(leaf, tmp_path): + # `import b2sdk` raising ModuleNotFoundError(name='b2sdk') is a genuinely missing runtime. + missing = tmp_path / 'b2sdk' + missing.mkdir() + (missing / '__init__.py').write_text( + "raise ModuleNotFoundError(\"No module named 'b2sdk'\", name='b2sdk')\n" + ) + contract = load('contract') + completed = run_leaf(leaf, dead_loopback_url(), extra_path=tmp_path) + assert completed.returncode == 0, completed.stdout + outcome, reason, code = dispatcher_verdict(contract, leaf, completed) + assert (outcome, code) == ('SKIP', 0) + assert reason.startswith('missing-runtime') + + +def test_missing_runtime_is_only_b2sdk_itself_not_found(): + guard = load('loopback_guard') + assert guard.missing_runtime(ModuleNotFoundError('x', name='b2sdk')) + assert not guard.missing_runtime(ModuleNotFoundError('x', name='b2sdk.v3.exception')) + assert not guard.missing_runtime(ModuleNotFoundError('x', name='requests')) + assert not guard.missing_runtime(ImportError('cannot import name')) + + +@pytest.mark.parametrize( + 'reason', + [ + 'unreachable -- B2ConnectionError at step authenticate', + 'unauthorized -- Unauthorized at step authenticate', + 'unauthorized -- AccessDenied at step create bucket', + 'unreachable -- ServiceError at step upload', + 'missing-runtime-ish -- unknown reason', + ], +) +def test_dispatcher_turns_non_genuine_could_not_run_reasons_into_fail(reason): + contract = load('contract') + line = f'CONFORMANCE b2-sdk-python files.upload @simulator: COULD-NOT-RUN ({reason})\n' + outcome, _reason, code = contract.translate('conformance', 'files.upload', 0, line) + assert (outcome, code) == ('FAIL', 1) + + +@pytest.mark.parametrize('reason', ['no-realm-option', 'missing-runtime', 'no-client-option']) +def test_dispatcher_keeps_genuine_could_not_run_reasons_as_skip(reason): + contract = load('contract') + line = f'CONFORMANCE b2-sdk-python files.upload @simulator: COULD-NOT-RUN ({reason} -- why)\n' + assert contract.translate('conformance', 'files.upload', 0, line) == ( + 'SKIP', + f'{reason} -- why', + 0, + ) + + +def test_child_environment_drops_proxies_and_pins_no_proxy(): + contract = load('contract') + parent = { + 'SDKHARNESS_SIMULATOR_URL': 'http://127.0.0.1:8180', + 'HTTP_PROXY': 'http://127.0.0.1:9', + 'https_proxy': 'http://127.0.0.1:9', + 'ALL_PROXY': 'socks5://127.0.0.1:9', + 'NO_PROXY': 'example.invalid', + } + child = contract.child_environment('conformance', parent) + assert not { + name for name in child if name.lower().endswith('_proxy') and 'no' not in name.lower() + } + assert child['NO_PROXY'] == child['no_proxy'] == '127.0.0.1' + assert parent['HTTP_PROXY'] == 'http://127.0.0.1:9' # the caller's mapping is untouched + + +def test_leaf_refusal_scrubs_the_proxy_environment(monkeypatch): + guard = load('loopback_guard') + monkeypatch.setenv('HTTP_PROXY', 'http://127.0.0.1:9') + monkeypatch.setenv('https_proxy', 'http://127.0.0.1:9') + monkeypatch.setenv('NO_PROXY', 'example.invalid') + monkeypatch.delenv('CONFORMANCE_TARGET', raising=False) + monkeypatch.setenv('CONFORMANCE_SIMULATOR_URL', 'http://127.0.0.1:8180') + assert guard.refusal('conformance') is None + assert 'HTTP_PROXY' not in os.environ and 'https_proxy' not in os.environ + assert os.environ['NO_PROXY'] == '127.0.0.1'