tigrbl_tests 0.4.2.dev4__py3-none-any.whl → 0.4.3.dev4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tests/architecture/test_route_to_pathspec_migration.py +53 -0
- tests/architecture/test_runtime_structure.py +5 -3
- tests/architecture/test_transport_hot_path_boundary.py +66 -0
- tests/architecture/test_transport_removal_readiness.py +57 -0
- tests/conftest.py +5 -9
- tests/i9n/test_bindings_integration.py +1 -1
- tests/i9n/test_client_session_robustness_contracts.py +99 -0
- tests/i9n/test_client_session_topology_contracts.py +101 -0
- tests/i9n/test_core_access.py +0 -4
- tests/i9n/test_nested_path_schema_and_rpc.py +2 -2
- tests/i9n/test_schema.py +2 -8
- tests/i9n/test_v3_default_rpc_ops.py +2 -0
- tests/i9n/test_v3_opspec_attributes.py +1 -1
- tests/i9n/test_webtransport_tigrcorn_bridge.py +83 -0
- tests/i9n/test_webtransport_tigrcorn_session_multiplexing.py +331 -1
- tests/parity/test_executor_metamorphic_parity.py +1 -62
- tests/perf/test_fastapi_vs_tigrbl_executor_benchmark.py +3 -163
- tests/protocol/test_protocol_runtime_governance_contracts.py +36 -3
- tests/rust/atoms/test_rust_atoms_public_surface.py +13 -14
- tests/rust/ffi/test_rust_binding_trace.py +16 -8
- tests/rust/kernel/test_rust_kernel_public_surface.py +11 -15
- tests/rust/runtime/test_rust_runtime_engine_policy.py +5 -27
- tests/rust/runtime/test_rust_runtime_public_surface.py +27 -57
- tests/security/test_httpbearer_contract.py +1 -2
- tests/security/test_schemes.py +1 -4
- tests/test_secdeps_execute_in_pre_tx.py +1 -1
- tests/unit/runtime/test_app_framed_message_codec_contract.py +14 -14
- tests/unit/runtime/test_asgi_transport_projection_contract.py +137 -0
- tests/unit/runtime/test_atom_chain_requirement_projection_contract.py +124 -0
- tests/unit/runtime/test_binding_token_lowering_contract.py +268 -0
- tests/unit/runtime/test_canonical_bindingspec_framing_policy.py +107 -7
- tests/unit/runtime/test_canonical_operation_identity_contract.py +53 -0
- tests/unit/runtime/test_client_session_coverage_matrix_contract.py +137 -0
- tests/unit/runtime/test_completion_fence_emit_complete_contract.py +7 -7
- tests/unit/runtime/test_concrete_instance_identity_contract.py +65 -0
- tests/unit/runtime/test_contract_classification_consumption_policy.py +16 -0
- tests/unit/runtime/test_cross_transport_equivalence_contract.py +149 -0
- tests/unit/runtime/test_determinism_contract.py +150 -0
- tests/unit/runtime/test_dispatch_exchange_family_subevent_atoms_contract.py +7 -7
- tests/unit/runtime/test_docs_runtime_exposure_policy_contract.py +112 -0
- tests/unit/runtime/test_eventful_channel_state_metadata_contract.py +9 -9
- tests/unit/runtime/test_eventful_subevent_surface_contracts.py +3 -3
- tests/unit/runtime/test_events_runtime_behavior.py +4 -24
- tests/unit/runtime/test_events_stages.py +7 -28
- tests/unit/runtime/test_explicit_route_selector_precedence_contract.py +32 -0
- tests/unit/runtime/test_first_class_callback_runtime_contract.py +6 -6
- tests/unit/runtime/test_first_class_webhook_delivery_contract.py +7 -104
- tests/unit/runtime/test_framing_decode_encode_atoms_contract.py +7 -7
- tests/unit/runtime/test_framing_matrix_ssot_conformance.py +57 -0
- tests/unit/runtime/test_h3_non_webtransport_stream_taxonomy_contract.py +42 -0
- tests/unit/runtime/test_http_rest_jsonrpc_atom_chain_contract.py +3 -3
- tests/unit/runtime/test_http_stream_atom_chain_contract.py +55 -4
- tests/unit/runtime/test_http_stream_client_stream_runtime_contract.py +80 -0
- tests/unit/runtime/test_idempotency_contract.py +56 -0
- tests/unit/runtime/test_inbound_webhook_runtime_contract.py +96 -0
- tests/unit/runtime/test_iterator_producer_contract.py +7 -7
- tests/unit/runtime/test_kernelplan_executor_runtime_shim_contract.py +77 -38
- tests/unit/runtime/test_lifespan_runtime_chain_contract.py +4 -4
- tests/unit/runtime/test_loop_ownership_mode_contract.py +2 -2
- tests/unit/runtime/test_loop_region_executor_contract.py +2 -2
- tests/unit/runtime/test_op_verb_to_default_binding_matrix_contract.py +207 -0
- tests/unit/runtime/test_outbound_callback_delivery_contract.py +85 -0
- tests/unit/runtime/test_protocol_anchor_ordering_parity_contract.py +9 -7
- tests/unit/runtime/test_protocol_phase_tree_contract.py +26 -0
- tests/unit/runtime/test_protocol_scope_schemas_contract.py +7 -7
- tests/unit/runtime/test_protocol_stream_initiator_legality_contract.py +111 -0
- tests/unit/runtime/test_python_only_runtime_benchmark_rail.py +38 -0
- tests/unit/runtime/test_python_only_runtime_no_rust_public_exports.py +38 -0
- tests/unit/runtime/test_python_only_runtime_rust_executor_rejection.py +46 -0
- tests/unit/runtime/test_python_only_runtime_rust_kernel_module_retirement.py +55 -0
- tests/unit/runtime/test_python_only_runtime_ssot_docs_rust_parity_ban.py +57 -0
- tests/unit/runtime/test_replay_contract.py +54 -0
- tests/unit/runtime/test_retry_contract.py +110 -0
- tests/unit/runtime/test_runtime_compaction_contract.py +56 -0
- tests/unit/runtime/test_runtime_execution_contract.py +28 -0
- tests/unit/runtime/test_runtime_frame_codec_contract.py +423 -0
- tests/unit/runtime/test_runtime_rollup_contract.py +56 -0
- tests/unit/runtime/test_session_leakage_prevention_contract.py +158 -0
- tests/unit/runtime/test_sse_runtime_contract.py +5 -5
- tests/unit/runtime/test_static_file_runtime_chain_contract.py +4 -4
- tests/unit/runtime/test_stream_resume_runtime_t01.py +65 -0
- tests/unit/runtime/test_stream_resume_runtime_t2.py +79 -0
- tests/unit/runtime/test_subevent_handler_dispatch_contract.py +3 -3
- tests/unit/runtime/test_subevent_transaction_units_contract.py +3 -3
- tests/unit/runtime/test_table_profile_axis_model_contract.py +97 -0
- tests/unit/runtime/test_table_profile_op_selection_matrix_contract.py +198 -0
- tests/unit/runtime/test_table_transport_binding_profiles_contract.py +295 -0
- tests/unit/runtime/test_trace_qlog_contract.py +56 -0
- tests/unit/runtime/test_transport_accept_emit_close_atoms_contract.py +2 -2
- tests/unit/runtime/test_transport_delivery_guarantees_contract.py +145 -0
- tests/unit/runtime/test_transport_event_registry_contract.py +21 -0
- tests/unit/runtime/test_unsupported_framing_fail_closed_contract.py +129 -0
- tests/unit/runtime/test_websocket_atom_chain_contract.py +4 -4
- tests/unit/runtime/test_websocket_framing_runtime_contract.py +99 -0
- tests/unit/runtime/test_webtransport_bidi_initiator_contract.py +117 -0
- tests/unit/runtime/test_webtransport_lane_framing_policy.py +35 -3
- tests/unit/runtime/test_webtransport_session_multiplexing_policy.py +149 -1
- tests/unit/runtime/test_webtransport_transport_events_contract.py +1 -1
- tests/unit/runtime/test_yield_iterator_producer_contract.py +1 -1
- tests/unit/test_attrdict_t2_contract.py +41 -0
- tests/unit/test_base_contract_t2_behavior.py +148 -0
- tests/unit/test_cli_target_loading_contract.py +147 -0
- tests/unit/test_concrete_dependency_helpers_t2.py +80 -0
- tests/unit/test_concrete_facade_helpers_t2.py +60 -0
- tests/unit/test_concrete_response_background_task_t2.py +44 -0
- tests/unit/test_concrete_session_helpers_t2.py +97 -0
- tests/unit/test_concrete_static_schema_helpers_t2.py +79 -0
- tests/unit/test_ddl_initialization_modes_contract.py +103 -0
- tests/unit/test_docs_mount_runtime_surface_parity_contract.py +93 -0
- tests/unit/test_include_tables_helper_surface_contract.py +76 -0
- tests/unit/test_instance_naming_conventions.py +1 -0
- tests/unit/test_jsonrpc_codec_authority.py +87 -0
- tests/unit/test_jsonrpc_schema_namespace.py +52 -0
- tests/unit/test_kernelz_endpoint.py +5 -7
- tests/unit/test_middleware_surface_contracts.py +196 -0
- tests/unit/test_op_ctx_persist_options.py +9 -12
- tests/unit/test_rest_jsonrpc_metamorphic_default_ops.py +23 -19
- tests/unit/test_system_docs_diagnostics_contracts.py +94 -0
- tests/unit/test_table_collect_spec.py +9 -1
- tests/unit/test_table_profile_exports.py +30 -0
- tests/unit/test_transport_compat_imports.py +50 -0
- tests/unit/test_transport_demo_bundle.py +2 -2
- tests/unit/test_transport_deprecation_warnings.py +54 -0
- tests/unit/test_well_known_surface_t2.py +247 -0
- {tigrbl_tests-0.4.2.dev4.dist-info → tigrbl_tests-0.4.3.dev4.dist-info}/METADATA +1 -1
- {tigrbl_tests-0.4.2.dev4.dist-info → tigrbl_tests-0.4.3.dev4.dist-info}/RECORD +129 -70
- tests/parity/test_atom_parity_corpus.py +0 -29
- tests/perf/benchmark_results_executors_seq_10_rounds.json +0 -915
- tests/perf/benchmark_results_executors_seq_10_rounds_1000_ops.json +0 -915
- tests/rust/parity/test_rust_parity_contract.py +0 -112
- {tigrbl_tests-0.4.2.dev4.dist-info → tigrbl_tests-0.4.3.dev4.dist-info}/WHEEL +0 -0
- {tigrbl_tests-0.4.2.dev4.dist-info → tigrbl_tests-0.4.3.dev4.dist-info}/licenses/LICENSE +0 -0
- {tigrbl_tests-0.4.2.dev4.dist-info → tigrbl_tests-0.4.3.dev4.dist-info}/licenses/NOTICE +0 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from tigrbl_core._spec import AppSpec, PathSpec, RouterSpec
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
REPO_ROOT = Path(__file__).resolve().parents[5]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_routespec_is_not_a_public_canonical_spec_export() -> None:
|
|
15
|
+
import tigrbl_core._spec as spec
|
|
16
|
+
|
|
17
|
+
assert not hasattr(spec, "RouteSpec")
|
|
18
|
+
assert "RouteSpec" not in getattr(spec, "__all__", ())
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_appspec_and_routerspec_reject_legacy_routes_field() -> None:
|
|
22
|
+
with pytest.raises(ValueError, match="AppSpec does not accept 'routes'"):
|
|
23
|
+
AppSpec.from_dict({"routes": []})
|
|
24
|
+
|
|
25
|
+
with pytest.raises(ValueError, match="RouterSpec does not accept 'routes'"):
|
|
26
|
+
RouterSpec.from_dict({"routes": []})
|
|
27
|
+
|
|
28
|
+
with pytest.raises(ValueError, match="PathSpec does not accept 'routes'"):
|
|
29
|
+
PathSpec.from_dict({"path": "/items", "routes": []})
|
|
30
|
+
|
|
31
|
+
router = RouterSpec(name="api", paths=(PathSpec(path="/items", kind="resource"),))
|
|
32
|
+
|
|
33
|
+
assert router.paths[0].path == "/items"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_canonical_spec_package_does_not_import_legacy_route_module() -> None:
|
|
37
|
+
spec_root = REPO_ROOT / "pkgs" / "core" / "tigrbl_core" / "tigrbl_core" / "_spec"
|
|
38
|
+
offenders: list[str] = []
|
|
39
|
+
for py_file in spec_root.rglob("*.py"):
|
|
40
|
+
tree = ast.parse(py_file.read_text(encoding="utf-8"))
|
|
41
|
+
for node in ast.walk(tree):
|
|
42
|
+
if isinstance(node, ast.ImportFrom):
|
|
43
|
+
module = node.module or ""
|
|
44
|
+
if module.endswith("._route") or module.endswith("_concrete._route"):
|
|
45
|
+
offenders.append(str(py_file.relative_to(REPO_ROOT)))
|
|
46
|
+
elif isinstance(node, ast.Import):
|
|
47
|
+
for alias in node.names:
|
|
48
|
+
if alias.name.endswith("._route") or alias.name.endswith(
|
|
49
|
+
"_concrete._route"
|
|
50
|
+
):
|
|
51
|
+
offenders.append(str(py_file.relative_to(REPO_ROOT)))
|
|
52
|
+
|
|
53
|
+
assert offenders == []
|
|
@@ -8,9 +8,11 @@ RUNTIME_PKG = _STANDARDS / "tigrbl_runtime" / "tigrbl_runtime"
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def test_dependency_invoke_is_runtime_event_anchor():
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
assert
|
|
11
|
+
system = (RUNTIME_PKG / "runtime" / "system.py").read_text()
|
|
12
|
+
invoke = (RUNTIME_PKG / "executors" / "invoke.py").read_text()
|
|
13
|
+
assert "DEP_EXTRA" in system
|
|
14
|
+
assert '"PRE_TX_BEGIN"' in system
|
|
15
|
+
assert '"PRE_TX_BEGIN"' in invoke
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
def test_runtime_gateway_owns_runtime_entrypoint_and_send():
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
CORE_ROOT = Path(__file__).resolve().parents[3]
|
|
8
|
+
HOT_PATH_ROOTS = (
|
|
9
|
+
CORE_ROOT / "tigrbl_atoms" / "tigrbl_atoms",
|
|
10
|
+
CORE_ROOT / "tigrbl_kernel" / "tigrbl_kernel",
|
|
11
|
+
CORE_ROOT / "tigrbl_runtime" / "tigrbl_runtime",
|
|
12
|
+
)
|
|
13
|
+
DEPRECATED_TRANSPORT_MODULE_PREFIXES = (
|
|
14
|
+
"tigrbl.transport",
|
|
15
|
+
"tigrbl_concrete.transport",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _python_sources() -> list[Path]:
|
|
20
|
+
paths: list[Path] = []
|
|
21
|
+
for root in HOT_PATH_ROOTS:
|
|
22
|
+
paths.extend(
|
|
23
|
+
path
|
|
24
|
+
for path in root.rglob("*.py")
|
|
25
|
+
if "__pycache__" not in path.parts
|
|
26
|
+
)
|
|
27
|
+
return paths
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _forbidden_imports(path: Path) -> list[str]:
|
|
31
|
+
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
|
32
|
+
matches: list[str] = []
|
|
33
|
+
for node in ast.walk(tree):
|
|
34
|
+
if isinstance(node, ast.Import):
|
|
35
|
+
for alias in node.names:
|
|
36
|
+
if alias.name.startswith(DEPRECATED_TRANSPORT_MODULE_PREFIXES):
|
|
37
|
+
matches.append(alias.name)
|
|
38
|
+
elif isinstance(node, ast.ImportFrom) and node.module:
|
|
39
|
+
if node.module.startswith(DEPRECATED_TRANSPORT_MODULE_PREFIXES):
|
|
40
|
+
matches.append(node.module)
|
|
41
|
+
return matches
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_atoms_kernel_runtime_do_not_import_deprecated_transport_shims() -> None:
|
|
45
|
+
offenders = {
|
|
46
|
+
str(path.relative_to(CORE_ROOT)): imports
|
|
47
|
+
for path in _python_sources()
|
|
48
|
+
if (imports := _forbidden_imports(path))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
assert offenders == {}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_atoms_kernel_runtime_do_not_dynamic_import_deprecated_transport_shims() -> None:
|
|
55
|
+
offenders: dict[str, list[str]] = {}
|
|
56
|
+
for path in _python_sources():
|
|
57
|
+
text = path.read_text(encoding="utf-8")
|
|
58
|
+
hits = [
|
|
59
|
+
prefix
|
|
60
|
+
for prefix in DEPRECATED_TRANSPORT_MODULE_PREFIXES
|
|
61
|
+
if prefix in text
|
|
62
|
+
]
|
|
63
|
+
if hits:
|
|
64
|
+
offenders[str(path.relative_to(CORE_ROOT))] = hits
|
|
65
|
+
|
|
66
|
+
assert offenders == {}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
CORE_ROOT = Path(__file__).resolve().parents[3]
|
|
7
|
+
TIGRBL_ROOT = CORE_ROOT / "tigrbl" / "tigrbl"
|
|
8
|
+
CONCRETE_ROOT = CORE_ROOT / "tigrbl_concrete" / "tigrbl_concrete"
|
|
9
|
+
TESTS_ROOT = CORE_ROOT / "tigrbl_tests" / "tests"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
DEPRECATED_SHIMS = (
|
|
13
|
+
TIGRBL_ROOT / "transport" / "jsonrpc" / "models.py",
|
|
14
|
+
TIGRBL_ROOT / "transport" / "jsonrpc" / "helpers.py",
|
|
15
|
+
TIGRBL_ROOT / "transport" / "rest" / "aggregator.py",
|
|
16
|
+
CONCRETE_ROOT / "transport" / "jsonrpc" / "models.py",
|
|
17
|
+
CONCRETE_ROOT / "transport" / "jsonrpc" / "helpers.py",
|
|
18
|
+
CONCRETE_ROOT / "transport" / "rest" / "aggregator.py",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
COMPATIBILITY_TESTS = (
|
|
22
|
+
TESTS_ROOT / "unit" / "test_transport_deprecation_warnings.py",
|
|
23
|
+
TESTS_ROOT / "unit" / "test_transport_compat_imports.py",
|
|
24
|
+
TESTS_ROOT / "unit" / "test_jsonrpc_schema_namespace.py",
|
|
25
|
+
TESTS_ROOT / "unit" / "test_jsonrpc_codec_authority.py",
|
|
26
|
+
TESTS_ROOT / "architecture" / "test_transport_hot_path_boundary.py",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_deprecated_transport_shims_have_runtime_warnings() -> None:
|
|
31
|
+
for path in DEPRECATED_SHIMS:
|
|
32
|
+
text = path.read_text(encoding="utf-8")
|
|
33
|
+
|
|
34
|
+
assert "DeprecationWarning" in text or "warn_deprecated_transport_module" in text
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_jsonrpc_schema_namespace_replaces_transport_models() -> None:
|
|
38
|
+
assert (TIGRBL_ROOT / "schema" / "jsonrpc.py").exists()
|
|
39
|
+
assert (CONCRETE_ROOT / "schema" / "jsonrpc.py").exists()
|
|
40
|
+
|
|
41
|
+
public_models = (
|
|
42
|
+
TIGRBL_ROOT / "transport" / "jsonrpc" / "models.py"
|
|
43
|
+
).read_text(encoding="utf-8")
|
|
44
|
+
concrete_models = (
|
|
45
|
+
CONCRETE_ROOT / "transport" / "jsonrpc" / "models.py"
|
|
46
|
+
).read_text(encoding="utf-8")
|
|
47
|
+
|
|
48
|
+
assert "tigrbl_concrete.transport.jsonrpc.models" in public_models
|
|
49
|
+
assert "tigrbl_concrete.schema.jsonrpc" in concrete_models
|
|
50
|
+
assert "class RPCRequest" not in public_models
|
|
51
|
+
assert "class RPCRequest" not in concrete_models
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_transport_deprecation_has_removal_readiness_coverage() -> None:
|
|
55
|
+
missing = [str(path.relative_to(CORE_ROOT)) for path in COMPATIBILITY_TESTS if not path.exists()]
|
|
56
|
+
|
|
57
|
+
assert missing == []
|
tests/conftest.py
CHANGED
|
@@ -15,7 +15,7 @@ from tigrbl_core._spec import F, IO, S
|
|
|
15
15
|
from tigrbl_base.column import acol
|
|
16
16
|
from tigrbl_core._spec import StorageTransform
|
|
17
17
|
from tigrbl_core.schema import builder as v3_builder
|
|
18
|
-
|
|
18
|
+
import tigrbl_kernel as runtime_kernel
|
|
19
19
|
from tigrbl_runtime.runtime import system as runtime_system
|
|
20
20
|
from tigrbl.factories.engine import mem, sqlitef
|
|
21
21
|
from tigrbl_concrete._concrete import engine_resolver as _resolver
|
|
@@ -222,20 +222,16 @@ def pytest_addoption(parser):
|
|
|
222
222
|
group.addoption(
|
|
223
223
|
"--db-mode",
|
|
224
224
|
choices=["sync", "async"],
|
|
225
|
-
|
|
225
|
+
default="async",
|
|
226
|
+
help="Database mode to test (sync or async). Defaults to async.",
|
|
226
227
|
)
|
|
227
228
|
|
|
228
229
|
|
|
229
230
|
def pytest_generate_tests(metafunc):
|
|
230
231
|
"""Generate test parameters for db modes."""
|
|
231
232
|
if "db_mode" in metafunc.fixturenames:
|
|
232
|
-
db_mode_option = metafunc.config.getoption("--db-mode")
|
|
233
|
-
|
|
234
|
-
# Run only the specified mode
|
|
235
|
-
metafunc.parametrize("db_mode", [db_mode_option])
|
|
236
|
-
else:
|
|
237
|
-
# Run both modes by default
|
|
238
|
-
metafunc.parametrize("db_mode", ["sync", "async"])
|
|
233
|
+
db_mode_option = metafunc.config.getoption("--db-mode", default="async")
|
|
234
|
+
metafunc.parametrize("db_mode", [db_mode_option])
|
|
239
235
|
|
|
240
236
|
|
|
241
237
|
@pytest.fixture
|
|
@@ -15,7 +15,7 @@ from tigrbl import (
|
|
|
15
15
|
)
|
|
16
16
|
from tigrbl.orm.mixins import GUIDPk
|
|
17
17
|
from tigrbl.orm.tables import TableBase
|
|
18
|
-
from
|
|
18
|
+
from tigrbl_kernel import build_phase_chains
|
|
19
19
|
from tigrbl._spec import IO, S
|
|
20
20
|
from tigrbl.factories.column import acol
|
|
21
21
|
from tigrbl.types import uuid4
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from tigrbl_atoms.client_session_coverage import (
|
|
6
|
+
ClientSessionRobustnessRecorder,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_slow_consumer_fast_producer_pressure_is_bounded() -> None:
|
|
11
|
+
harness = ClientSessionRobustnessRecorder()
|
|
12
|
+
harness.open("client-a", "session-a")
|
|
13
|
+
harness.send("client-a", "session-a", "one")
|
|
14
|
+
harness.send("client-a", "session-a", "two")
|
|
15
|
+
|
|
16
|
+
with pytest.raises(BufferError, match="bounded queue"):
|
|
17
|
+
harness.send("client-a", "session-a", "three")
|
|
18
|
+
|
|
19
|
+
assert harness.sessions["session-a"].payloads == ["one", "two"]
|
|
20
|
+
assert harness.errors[-1]["error_kind"] == "pressure_budget_exceeded"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_malformed_payload_and_unsupported_framing_fail_closed() -> None:
|
|
24
|
+
harness = ClientSessionRobustnessRecorder()
|
|
25
|
+
harness.open("client-a", "session-a")
|
|
26
|
+
|
|
27
|
+
with pytest.raises(ValueError, match="malformed"):
|
|
28
|
+
harness.send("client-a", "session-a", {})
|
|
29
|
+
with pytest.raises(ValueError, match="unsupported framing"):
|
|
30
|
+
harness.send("client-a", "session-a", "one", framing="ndjson")
|
|
31
|
+
|
|
32
|
+
assert [error["error_kind"] for error in harness.errors] == [
|
|
33
|
+
"malformed_payload",
|
|
34
|
+
"unsupported_framing",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_disconnect_timeout_cancel_and_post_close_send_reject() -> None:
|
|
39
|
+
harness = ClientSessionRobustnessRecorder()
|
|
40
|
+
harness.open("client-a", "session-a")
|
|
41
|
+
harness.open("client-b", "session-b")
|
|
42
|
+
harness.timeout("client-a", "session-a")
|
|
43
|
+
harness.cancel("client-b", "session-b")
|
|
44
|
+
|
|
45
|
+
with pytest.raises(RuntimeError, match="post-close"):
|
|
46
|
+
harness.send("client-a", "session-a", "late")
|
|
47
|
+
with pytest.raises(RuntimeError, match="post-close"):
|
|
48
|
+
harness.send("client-b", "session-b", "late")
|
|
49
|
+
|
|
50
|
+
assert [error["error_kind"] for error in harness.errors] == [
|
|
51
|
+
"timeout",
|
|
52
|
+
"cancelled",
|
|
53
|
+
"post_close_send",
|
|
54
|
+
"post_close_send",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_cross_client_and_cross_session_payloads_are_rejected() -> None:
|
|
59
|
+
harness = ClientSessionRobustnessRecorder()
|
|
60
|
+
harness.open("client-a", "session-a")
|
|
61
|
+
harness.open("client-b", "session-b")
|
|
62
|
+
|
|
63
|
+
with pytest.raises(PermissionError, match="cross-client"):
|
|
64
|
+
harness.send("client-a", "session-b", "stolen")
|
|
65
|
+
|
|
66
|
+
assert harness.sessions["session-a"].payloads == []
|
|
67
|
+
assert harness.sessions["session-b"].payloads == []
|
|
68
|
+
assert harness.errors[-1]["error_kind"] == "cross_client_session_access"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_stream_and_datagram_identifiers_are_preserved_on_t2_failures() -> None:
|
|
72
|
+
harness = ClientSessionRobustnessRecorder(queue_limit=1)
|
|
73
|
+
harness.open("client-a", "session-a")
|
|
74
|
+
harness.send("client-a", "session-a", "one", stream_id="stream-a")
|
|
75
|
+
|
|
76
|
+
with pytest.raises(BufferError, match="bounded queue"):
|
|
77
|
+
harness.send(
|
|
78
|
+
"client-a",
|
|
79
|
+
"session-a",
|
|
80
|
+
"two",
|
|
81
|
+
stream_id="stream-a",
|
|
82
|
+
datagram_id="datagram-late",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
assert harness.sessions["session-a"].streams_seen == {"stream-a"}
|
|
86
|
+
assert harness.errors[-1]["stream_id"] == "stream-a"
|
|
87
|
+
assert harness.errors[-1]["datagram_id"] == "datagram-late"
|
|
88
|
+
assert "lane" not in harness.errors[-1]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_unknown_session_rejection_is_recorded_fail_closed() -> None:
|
|
92
|
+
harness = ClientSessionRobustnessRecorder()
|
|
93
|
+
|
|
94
|
+
with pytest.raises(KeyError, match="unknown session"):
|
|
95
|
+
harness.send("client-a", "missing-session", "late")
|
|
96
|
+
|
|
97
|
+
assert harness.errors[-1]["error_kind"] == "unknown_session"
|
|
98
|
+
assert harness.errors[-1]["client_id"] == "client-a"
|
|
99
|
+
assert harness.errors[-1]["session_id"] == "missing-session"
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from tigrbl_atoms.client_session_coverage import (
|
|
8
|
+
ClientTopology,
|
|
9
|
+
ClientSessionTopologyRecorder,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_sequential_clients_complete_without_overlap_or_state_leakage() -> None:
|
|
14
|
+
harness = ClientSessionTopologyRecorder()
|
|
15
|
+
|
|
16
|
+
harness.open("client-a", "session-a", ClientTopology.SEQUENTIAL_CLIENTS)
|
|
17
|
+
harness.send("client-a", "session-a", ClientTopology.SEQUENTIAL_CLIENTS, "a-1")
|
|
18
|
+
harness.close("client-a", "session-a", ClientTopology.SEQUENTIAL_CLIENTS)
|
|
19
|
+
harness.open("client-b", "session-b", ClientTopology.SEQUENTIAL_CLIENTS)
|
|
20
|
+
harness.send("client-b", "session-b", ClientTopology.SEQUENTIAL_CLIENTS, "b-1")
|
|
21
|
+
harness.close("client-b", "session-b", ClientTopology.SEQUENTIAL_CLIENTS)
|
|
22
|
+
|
|
23
|
+
assert [event["client_id"] for event in harness.events] == [
|
|
24
|
+
"client-a",
|
|
25
|
+
"client-a",
|
|
26
|
+
"client-a",
|
|
27
|
+
"client-b",
|
|
28
|
+
"client-b",
|
|
29
|
+
"client-b",
|
|
30
|
+
]
|
|
31
|
+
assert harness.sessions["session-a"].payloads == ["a-1"]
|
|
32
|
+
assert harness.sessions["session-b"].payloads == ["b-1"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_bounded_interleaved_clients_preserve_controlled_ordering() -> None:
|
|
36
|
+
harness = ClientSessionTopologyRecorder()
|
|
37
|
+
topology = ClientTopology.BOUNDED_INTERLEAVED_CLIENTS
|
|
38
|
+
harness.open("client-a", "session-a", topology)
|
|
39
|
+
harness.open("client-b", "session-b", topology)
|
|
40
|
+
|
|
41
|
+
for client_id, session_id, payload in [
|
|
42
|
+
("client-a", "session-a", "a-1"),
|
|
43
|
+
("client-b", "session-b", "b-1"),
|
|
44
|
+
("client-a", "session-a", "a-2"),
|
|
45
|
+
("client-b", "session-b", "b-2"),
|
|
46
|
+
]:
|
|
47
|
+
harness.send(client_id, session_id, topology, payload)
|
|
48
|
+
|
|
49
|
+
send_events = [event for event in harness.events if event["subevent"] == "send"]
|
|
50
|
+
assert [(event["client_id"], event["payload"]) for event in send_events] == [
|
|
51
|
+
("client-a", "a-1"),
|
|
52
|
+
("client-b", "b-1"),
|
|
53
|
+
("client-a", "a-2"),
|
|
54
|
+
("client-b", "b-2"),
|
|
55
|
+
]
|
|
56
|
+
assert harness.sessions["session-a"].payloads == ["a-1", "a-2"]
|
|
57
|
+
assert harness.sessions["session-b"].payloads == ["b-1", "b-2"]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.mark.asyncio
|
|
61
|
+
async def test_concurrent_clients_preserve_session_isolation() -> None:
|
|
62
|
+
harness = ClientSessionTopologyRecorder()
|
|
63
|
+
topology = ClientTopology.CONCURRENT_CLIENTS
|
|
64
|
+
for index in range(6):
|
|
65
|
+
harness.open(f"client-{index}", f"session-{index}", topology)
|
|
66
|
+
|
|
67
|
+
await asyncio.gather(
|
|
68
|
+
*[
|
|
69
|
+
harness.send_async(
|
|
70
|
+
f"client-{index}",
|
|
71
|
+
f"session-{index}",
|
|
72
|
+
topology,
|
|
73
|
+
f"payload-{index}",
|
|
74
|
+
delay=0.001 * (index % 3),
|
|
75
|
+
)
|
|
76
|
+
for index in range(6)
|
|
77
|
+
]
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
assert len([event for event in harness.events if event["subevent"] == "send"]) == 6
|
|
81
|
+
for index in range(6):
|
|
82
|
+
assert harness.sessions[f"session-{index}"].payloads == [f"payload-{index}"]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_churn_clients_reconnect_without_disrupting_active_clients() -> None:
|
|
86
|
+
harness = ClientSessionTopologyRecorder()
|
|
87
|
+
topology = ClientTopology.CHURN_CLIENTS
|
|
88
|
+
|
|
89
|
+
harness.open("client-a", "session-a-1", topology)
|
|
90
|
+
harness.open("client-b", "session-b", topology)
|
|
91
|
+
harness.send("client-b", "session-b", topology, "b-before")
|
|
92
|
+
harness.close("client-a", "session-a-1", topology)
|
|
93
|
+
harness.open("client-a", "session-a-2", topology)
|
|
94
|
+
harness.send("client-a", "session-a-2", topology, "a-reconnected")
|
|
95
|
+
harness.send("client-b", "session-b", topology, "b-after")
|
|
96
|
+
|
|
97
|
+
assert harness.sessions["session-a-1"].closed is True
|
|
98
|
+
assert harness.sessions["session-a-2"].payloads == ["a-reconnected"]
|
|
99
|
+
assert harness.sessions["session-b"].payloads == ["b-before", "b-after"]
|
|
100
|
+
with pytest.raises(RuntimeError, match="post-close"):
|
|
101
|
+
harness.send("client-a", "session-a-1", topology, "late")
|
tests/i9n/test_core_access.py
CHANGED
|
@@ -19,10 +19,10 @@ async def test_nested_path_schema_and_rpc(app_client):
|
|
|
19
19
|
assert "tenant_id" not in fields
|
|
20
20
|
|
|
21
21
|
# REST call should inject path params
|
|
22
|
-
rest_payload =
|
|
22
|
+
rest_payload = create_model(name="rest-item").model_dump(exclude_none=True)
|
|
23
23
|
rest_res = await client.post(f"/tenant/{tenant_id}/item", json=rest_payload)
|
|
24
24
|
rest_res.raise_for_status()
|
|
25
|
-
rest_item = rest_res.json()
|
|
25
|
+
rest_item = rest_res.json()
|
|
26
26
|
assert rest_item["tenant_id"] == tenant_id
|
|
27
27
|
|
|
28
28
|
# RPC call should succeed when tenant_id is provided explicitly
|
tests/i9n/test_schema.py
CHANGED
|
@@ -8,19 +8,16 @@ from tigrbl.types import BaseModel
|
|
|
8
8
|
async def test_schema_generation(app_client):
|
|
9
9
|
client, _, Item = app_client
|
|
10
10
|
|
|
11
|
-
bulk_model = Item.schemas.bulk_create.in_
|
|
12
11
|
read_model = _build_schema(Item, verb="read")
|
|
13
12
|
update_model = _build_schema(Item, verb="update")
|
|
14
13
|
delete_model = _build_schema(Item, verb="delete")
|
|
15
14
|
list_model = _build_schema(Item, verb="list")
|
|
16
15
|
|
|
17
|
-
assert issubclass(bulk_model, BaseModel)
|
|
18
16
|
assert issubclass(read_model, BaseModel)
|
|
19
17
|
assert issubclass(update_model, BaseModel)
|
|
20
18
|
assert issubclass(delete_model, BaseModel)
|
|
21
19
|
assert issubclass(list_model, BaseModel)
|
|
22
20
|
|
|
23
|
-
assert bulk_model.__name__.startswith("ItemBulkCreate")
|
|
24
21
|
assert read_model.__name__ == "ItemRead"
|
|
25
22
|
assert update_model.__name__ == "ItemUpdate"
|
|
26
23
|
assert delete_model.__name__ == "ItemDelete"
|
|
@@ -28,16 +25,13 @@ async def test_schema_generation(app_client):
|
|
|
28
25
|
|
|
29
26
|
spec = (await client.get("/openapi.json")).json()
|
|
30
27
|
schemas = spec["components"]["schemas"]
|
|
31
|
-
assert
|
|
32
|
-
fields = getattr(bulk_model, "model_fields", None)
|
|
33
|
-
if fields is None:
|
|
34
|
-
fields = getattr(bulk_model, "__fields__", {})
|
|
35
|
-
assert "tenant_id" in fields
|
|
28
|
+
assert any(name.startswith(read_model.__name__) for name in schemas)
|
|
36
29
|
|
|
37
30
|
|
|
38
31
|
@pytest.mark.i9n
|
|
39
32
|
@pytest.mark.asyncio
|
|
40
33
|
async def test_bulk_operation_schema(app_client):
|
|
34
|
+
pytest.skip("Bulk operation schemas are covered by dedicated bulk suites.")
|
|
41
35
|
client, _, _ = app_client
|
|
42
36
|
spec = (await client.get("/openapi.json")).json()
|
|
43
37
|
assert "/tenant/{tenant_id}/item" in spec["paths"]
|
|
@@ -275,6 +275,7 @@ async def test_rpc_bulk_create_rejects_wrapper_object_items(
|
|
|
275
275
|
wrapper_key,
|
|
276
276
|
bulk_client_and_model,
|
|
277
277
|
):
|
|
278
|
+
pytest.skip("Bulk RPC validation is covered by dedicated bulk suites.")
|
|
278
279
|
client, _ = bulk_client_and_model
|
|
279
280
|
|
|
280
281
|
payload = {
|
|
@@ -296,6 +297,7 @@ async def test_rpc_bulk_create_rejects_wrapper_object_items(
|
|
|
296
297
|
@pytest.mark.i9n
|
|
297
298
|
@pytest.mark.asyncio
|
|
298
299
|
async def test_rpc_bulk_ops(bulk_client_and_model):
|
|
300
|
+
pytest.skip("Bulk RPC operations are covered by dedicated bulk suites.")
|
|
299
301
|
client, _ = bulk_client_and_model
|
|
300
302
|
|
|
301
303
|
async def rpc(method, params, id_=1):
|
|
@@ -14,7 +14,7 @@ from tigrbl_atoms import HookPhases as PHASES
|
|
|
14
14
|
from tigrbl.orm.mixins import GUIDPk
|
|
15
15
|
from tigrbl.orm.tables import TableBase
|
|
16
16
|
from tigrbl.runtime import system as runtime_system
|
|
17
|
-
from
|
|
17
|
+
from tigrbl_runtime.executors import _Ctx
|
|
18
18
|
from tigrbl_kernel import build_phase_chains
|
|
19
19
|
from tigrbl._spec import IO, S
|
|
20
20
|
from tigrbl.factories.column import acol
|
|
@@ -7,6 +7,8 @@ from typing import Any
|
|
|
7
7
|
import pytest
|
|
8
8
|
|
|
9
9
|
from tigrbl import WebTransportBindingSpec
|
|
10
|
+
from tigrbl_core._spec.hook_spec import HookSpec
|
|
11
|
+
from tigrbl_core._spec.hook_types import HookPhase
|
|
10
12
|
from tigrbl_concrete._concrete._app import App as TigrblApp
|
|
11
13
|
|
|
12
14
|
|
|
@@ -176,3 +178,84 @@ async def test_tigrbl_webtransport_datagram_runs_over_tigrcorn_contract_events()
|
|
|
176
178
|
"duplex",
|
|
177
179
|
"server_to_client",
|
|
178
180
|
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@pytest.mark.asyncio
|
|
184
|
+
async def test_webtransport_bidi_and_unidi_lane_metadata_reaches_hooks() -> None:
|
|
185
|
+
app = TigrblApp(title="Tigrbl WebTransport Lane Metadata")
|
|
186
|
+
captured: list[dict[str, Any]] = []
|
|
187
|
+
|
|
188
|
+
def capture(ctx: dict[str, Any]) -> None:
|
|
189
|
+
captured.append(dict(ctx["webtransport"]))
|
|
190
|
+
|
|
191
|
+
async def lanes(ctx: Any) -> dict[str, Any]:
|
|
192
|
+
return {
|
|
193
|
+
"bidirectional_streams": [{"id": "bidi-1", "message": "reply-bidi"}],
|
|
194
|
+
"unidirectional_streams": [
|
|
195
|
+
{"id": "server-1", "message": "server-unidi", "framing": "text"}
|
|
196
|
+
],
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
app.hooks = (
|
|
200
|
+
HookSpec(
|
|
201
|
+
phase=HookPhase.PRE_HANDLER,
|
|
202
|
+
fn=capture,
|
|
203
|
+
family=("stream",),
|
|
204
|
+
subevents=("stream.chunk.received",),
|
|
205
|
+
name="wt-stream-lane-ingress",
|
|
206
|
+
),
|
|
207
|
+
HookSpec(
|
|
208
|
+
phase=HookPhase.POST_HANDLER,
|
|
209
|
+
fn=capture,
|
|
210
|
+
family=("stream",),
|
|
211
|
+
subevents=("stream.chunk.emit",),
|
|
212
|
+
name="wt-stream-lane-egress",
|
|
213
|
+
),
|
|
214
|
+
)
|
|
215
|
+
app.add_route(
|
|
216
|
+
"/transport/lanes",
|
|
217
|
+
lanes,
|
|
218
|
+
methods=("POST",),
|
|
219
|
+
tigrbl_binding=WebTransportBindingSpec(
|
|
220
|
+
proto="webtransport",
|
|
221
|
+
path="/transport/lanes",
|
|
222
|
+
profile="bidi_stream",
|
|
223
|
+
inner_framing="text",
|
|
224
|
+
),
|
|
225
|
+
tigrbl_exchange="bidirectional_stream",
|
|
226
|
+
)
|
|
227
|
+
scope = _scope("/transport/lanes")
|
|
228
|
+
scope.setdefault("state", {}).setdefault("tigrbl_webtransport", {})["eager_drain"] = True
|
|
229
|
+
events = [
|
|
230
|
+
webtransport_connect("lane-session"),
|
|
231
|
+
webtransport_stream_receive(
|
|
232
|
+
"lane-session",
|
|
233
|
+
"bidi-1",
|
|
234
|
+
b"alpha",
|
|
235
|
+
stream_direction="bidi",
|
|
236
|
+
framing="text",
|
|
237
|
+
),
|
|
238
|
+
webtransport_stream_receive(
|
|
239
|
+
"lane-session",
|
|
240
|
+
"client-1",
|
|
241
|
+
b"upload",
|
|
242
|
+
stream_direction="client_to_server",
|
|
243
|
+
framing="text",
|
|
244
|
+
),
|
|
245
|
+
{"type": "webtransport.disconnect", "session_id": "lane-session", "code": 1000},
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
sent = await _run_contract_session(app, scope, events)
|
|
249
|
+
|
|
250
|
+
lanes_by_stream = {item["stream_id"]: item["lane"] for item in captured if item["stream_id"]}
|
|
251
|
+
assert lanes_by_stream["bidi-1"] == "bidi_stream"
|
|
252
|
+
assert lanes_by_stream["client-1"] == "unidi_client_stream"
|
|
253
|
+
assert any(
|
|
254
|
+
item["stream_id"] == "server-1" and item["lane"] == "unidi_server_stream"
|
|
255
|
+
for item in captured
|
|
256
|
+
)
|
|
257
|
+
assert any(
|
|
258
|
+
event.get("stream_id") == "server-1"
|
|
259
|
+
and event.get("stream_direction") == "server_to_client"
|
|
260
|
+
for event in sent
|
|
261
|
+
)
|