arena-pytest 5.2.0__py3-none-win_amd64.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.
@@ -0,0 +1,178 @@
1
+ from arena_pytest.ffi._ffi import (
2
+ ArenaBindingError,
3
+ ArenaLogLevel,
4
+ ArenaNativeLib,
5
+ register_dispatcher_logging_target_for_logger,
6
+ unregister_dispatcher_logging_target,
7
+ )
8
+ from arena_pytest.arena import (
9
+ OpenArena,
10
+ arena,
11
+ arena_ffi,
12
+ closed_arena,
13
+ )
14
+ from arena_pytest.closed_arena import ClosedArena
15
+ from arena_pytest.match import Match, MatchBuilder
16
+ from arena_pytest.exec.containerized_component import (
17
+ ContainerizedComponent,
18
+ ContainerizedComponentBuilder,
19
+ )
20
+ from arena_pytest.exec.executable_component import (
21
+ BuildTool,
22
+ ExecutableComponent,
23
+ ExecutableComponentBuilder,
24
+ )
25
+ from arena_pytest.playbook import (
26
+ ActiveHttpPlaybook,
27
+ ActiveLocalstackPlaybook,
28
+ ActiveMssqlPlaybook,
29
+ ActivePlaybook,
30
+ ManagedPlaybook,
31
+ Playbook,
32
+ UnmanagedPlaybook,
33
+ playbook,
34
+ )
35
+ from arena_pytest.dep.http import (
36
+ ActiveHttpPlaybookBuilder,
37
+ HttpDependency,
38
+ HttpDependencyBuilder,
39
+ HttpHeaderPattern,
40
+ HttpMappingBuilder,
41
+ HttpMappingExpect,
42
+ HttpPlaybookBuilder,
43
+ HttpResponse,
44
+ HttpSequenceBuilder,
45
+ ManagedHttpPlaybook,
46
+ created,
47
+ no_content,
48
+ ok,
49
+ ok_json,
50
+ server_error,
51
+ status,
52
+ )
53
+ from arena_pytest.dep.kafka import (
54
+ KAFKA_INTERNAL_DOCKER_PORT,
55
+ KafkaDependency,
56
+ KafkaDependencyBuilder,
57
+ KafkaFlavor,
58
+ )
59
+ from arena_pytest.dep.localstack import (
60
+ LOCALSTACK_DEFAULT_ACCOUNT_ID,
61
+ LOCALSTACK_DEFAULT_REGION,
62
+ LOCALSTACK_INTERNAL_DOCKER_PORT,
63
+ EventBusSpec,
64
+ EventRuleSpec,
65
+ EventRuleTarget,
66
+ EventTargetKind,
67
+ LambdaSpec,
68
+ LambdaTarget,
69
+ LocalstackDependency,
70
+ LocalstackDependencyBuilder,
71
+ ManagedLocalstackPlaybook,
72
+ QueueSpec,
73
+ SqsQueueTarget,
74
+ )
75
+ from arena_pytest.dep.mssql import (
76
+ ManagedMssqlPlaybook,
77
+ MssqlDependency,
78
+ MssqlDependencyBuilder,
79
+ MssqlEncryption,
80
+ )
81
+ from arena_pytest.oauth import (
82
+ DEFAULT_OAUTH_PORT,
83
+ OAUTH_ISSUER,
84
+ OauthDependency,
85
+ OauthDependencyBuilder,
86
+ oauth_issuer_host_is_non_loopback,
87
+ oauth_loopback_tls_pem_pair,
88
+ )
89
+ from arena_pytest.dep.postgres import (
90
+ ManagedPostgresPlaybook,
91
+ PostgresDependency,
92
+ PostgresDependencyBuilder,
93
+ )
94
+ from arena_pytest.dep.smtp import SmtpDependency, SmtpDependencyBuilder
95
+ from arena_pytest.dep.temporal import TemporalDependency, TemporalDependencyBuilder
96
+ from arena_pytest.readiness import HttpReadinessCheck, ReadinessCheck, TcpReadinessCheck
97
+
98
+ __all__ = [
99
+ "ArenaBindingError",
100
+ "ArenaLogLevel",
101
+ "ArenaNativeLib",
102
+ "ClosedArena",
103
+ "register_dispatcher_logging_target_for_logger",
104
+ "unregister_dispatcher_logging_target",
105
+ "ContainerizedComponent",
106
+ "ContainerizedComponentBuilder",
107
+ "Match",
108
+ "MatchBuilder",
109
+ "BuildTool",
110
+ "ExecutableComponent",
111
+ "ExecutableComponentBuilder",
112
+ "HttpDependency",
113
+ "HttpDependencyBuilder",
114
+ "HttpMappingExpect",
115
+ "HttpHeaderPattern",
116
+ "HttpMappingBuilder",
117
+ "HttpResponse",
118
+ "HttpSequenceBuilder",
119
+ "ActiveHttpPlaybookBuilder",
120
+ "HttpPlaybookBuilder",
121
+ "ok",
122
+ "ok_json",
123
+ "status",
124
+ "server_error",
125
+ "created",
126
+ "no_content",
127
+ "ActiveHttpPlaybook",
128
+ "ActiveLocalstackPlaybook",
129
+ "ActiveMssqlPlaybook",
130
+ "ActivePlaybook",
131
+ "ManagedHttpPlaybook",
132
+ "ManagedMssqlPlaybook",
133
+ "ManagedPlaybook",
134
+ "Playbook",
135
+ "UnmanagedPlaybook",
136
+ "KAFKA_INTERNAL_DOCKER_PORT",
137
+ "KafkaDependency",
138
+ "KafkaDependencyBuilder",
139
+ "KafkaFlavor",
140
+ "LOCALSTACK_DEFAULT_ACCOUNT_ID",
141
+ "LOCALSTACK_DEFAULT_REGION",
142
+ "LOCALSTACK_INTERNAL_DOCKER_PORT",
143
+ "EventBusSpec",
144
+ "EventRuleSpec",
145
+ "EventRuleTarget",
146
+ "EventTargetKind",
147
+ "LambdaSpec",
148
+ "LambdaTarget",
149
+ "LocalstackDependency",
150
+ "LocalstackDependencyBuilder",
151
+ "ManagedLocalstackPlaybook",
152
+ "QueueSpec",
153
+ "SqsQueueTarget",
154
+ "MssqlDependency",
155
+ "MssqlDependencyBuilder",
156
+ "MssqlEncryption",
157
+ "DEFAULT_OAUTH_PORT",
158
+ "OAUTH_ISSUER",
159
+ "OauthDependency",
160
+ "OauthDependencyBuilder",
161
+ "oauth_issuer_host_is_non_loopback",
162
+ "oauth_loopback_tls_pem_pair",
163
+ "ManagedPostgresPlaybook",
164
+ "PostgresDependency",
165
+ "PostgresDependencyBuilder",
166
+ "TemporalDependency",
167
+ "TemporalDependencyBuilder",
168
+ "SmtpDependency",
169
+ "SmtpDependencyBuilder",
170
+ "HttpReadinessCheck",
171
+ "TcpReadinessCheck",
172
+ "ReadinessCheck",
173
+ "arena",
174
+ "arena_ffi",
175
+ "playbook",
176
+ "closed_arena",
177
+ "OpenArena",
178
+ ]
arena_pytest/arena.py ADDED
@@ -0,0 +1,195 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import Any, List, Optional, Type
5
+
6
+ import pytest
7
+ import pytest_asyncio
8
+
9
+ from arena_pytest.ffi._ffi import (
10
+ ArenaBindingError,
11
+ ArenaNativeLib,
12
+ close_arena,
13
+ hard_reset as ffi_hard_reset,
14
+ load_ffi,
15
+ soft_reset as ffi_soft_reset,
16
+ )
17
+ from arena_pytest.playbook import (
18
+ PLAYBOOK_MARKER,
19
+ ActivePlaybook,
20
+ Playbook,
21
+ _activate_classes,
22
+ _class_marker_classes,
23
+ _drop_actives,
24
+ _finish_playbook_scope,
25
+ _matches_from_closed_arena,
26
+ _module_marker_classes,
27
+ _own_marker_classes,
28
+ _partition_classes,
29
+ _run_managed_classes,
30
+ )
31
+
32
+
33
+ class OpenArena:
34
+ def __init__(
35
+ self,
36
+ ffi: ArenaNativeLib,
37
+ handle: int,
38
+ dispatcher_logging_target_token: int = 0,
39
+ ):
40
+ self._ffi = ffi
41
+ self._handle = handle
42
+ self._dispatcher_logging_target_token = dispatcher_logging_target_token
43
+
44
+ def handle(self) -> int:
45
+ return self._handle
46
+
47
+ async def close(self) -> None:
48
+ if self._handle:
49
+ await asyncio.to_thread(
50
+ close_arena,
51
+ self._ffi,
52
+ self._handle,
53
+ dispatcher_logging_target_token=self._dispatcher_logging_target_token,
54
+ )
55
+ self._handle = 0
56
+ self._dispatcher_logging_target_token = 0
57
+
58
+ async def soft_reset(self, dependency_identifier: str) -> None:
59
+ await asyncio.to_thread(
60
+ ffi_soft_reset, self._ffi, self._handle, dependency_identifier
61
+ )
62
+
63
+ async def hard_reset(self, dependency_identifier: str) -> None:
64
+ await asyncio.to_thread(
65
+ ffi_hard_reset, self._ffi, self._handle, dependency_identifier
66
+ )
67
+
68
+
69
+ @pytest.fixture(scope="session")
70
+ def arena_ffi() -> ArenaNativeLib:
71
+ ffi = load_ffi()
72
+ if ffi is None:
73
+ pytest.skip(
74
+ "arena shared library not found (set ARENA_FFI_LIB or use Bazel runfiles)"
75
+ )
76
+ return ffi
77
+
78
+
79
+ @pytest.fixture(scope="session")
80
+ def closed_arena() -> Optional[Any]:
81
+ return None
82
+
83
+
84
+ @pytest_asyncio.fixture(scope="session")
85
+ async def arena(closed_arena) -> OpenArena:
86
+ if closed_arena is None:
87
+ pytest.skip("closed_arena fixture not overridden (no arena to open)")
88
+ try:
89
+ open_arena_obj = await closed_arena.open()
90
+ except ArenaBindingError as e:
91
+ pytest.fail(f"arena_open failed: {e}", pytrace=False)
92
+ yield open_arena_obj
93
+ await open_arena_obj.close()
94
+
95
+
96
+ def pytest_configure(config: pytest.Config) -> None:
97
+ config.addinivalue_line(
98
+ "markers",
99
+ f"{PLAYBOOK_MARKER}(klass): open one playbook identified by its Playbook "
100
+ "subclass for the test, class, or module scope; stack multiple "
101
+ "@playbook(...) decorators for more than one.",
102
+ )
103
+ if config.getini("asyncio_mode") in (None, "strict", "STRICT"):
104
+ config._inicache["asyncio_mode"] = "auto"
105
+ if not config.getini("asyncio_default_fixture_loop_scope"):
106
+ config._inicache["asyncio_default_fixture_loop_scope"] = "session"
107
+
108
+
109
+ _FUNCTION_ACTIVES_ATTR = "_arena_pytest_function_actives"
110
+ _FUNCTION_MANAGED_ATTR = "_arena_pytest_function_managed_classes"
111
+
112
+
113
+ def active_playbooks_for_item(item: pytest.Item) -> List[ActivePlaybook]:
114
+ actives: Optional[List[ActivePlaybook]] = getattr(item, _FUNCTION_ACTIVES_ATTR, None)
115
+ return list(actives) if actives else []
116
+
117
+
118
+ def _item_request(item: pytest.Item) -> pytest.FixtureRequest:
119
+ request = getattr(item, "_request", None)
120
+ if request is None:
121
+ raise pytest.UsageError(
122
+ "@pytest.mark.playbook requires a pytest test function with the "
123
+ "'arena' fixture available"
124
+ )
125
+ return request
126
+
127
+
128
+ @pytest.hookimpl(trylast=True)
129
+ def pytest_runtest_setup(item: pytest.Item) -> None:
130
+ classes = _own_marker_classes(item)
131
+ if not classes:
132
+ return
133
+ unmanaged_classes, managed_classes = _partition_classes(classes)
134
+ setattr(item, _FUNCTION_MANAGED_ATTR, managed_classes)
135
+ if not unmanaged_classes:
136
+ return
137
+ request = _item_request(item)
138
+ arena_obj = request.getfixturevalue("arena")
139
+ matches = _matches_from_closed_arena(request.getfixturevalue("closed_arena"))
140
+ actives = _activate_classes(arena_obj, matches, unmanaged_classes)
141
+ setattr(item, _FUNCTION_ACTIVES_ATTR, actives)
142
+
143
+
144
+ @pytest.hookimpl(tryfirst=True)
145
+ def pytest_runtest_teardown(item: pytest.Item, nextitem: Optional[pytest.Item]) -> None:
146
+ actives: Optional[List[ActivePlaybook]] = getattr(item, _FUNCTION_ACTIVES_ATTR, None)
147
+ if actives:
148
+ setattr(item, _FUNCTION_ACTIVES_ATTR, None)
149
+ _drop_actives(actives)
150
+
151
+ managed_classes: Optional[List[Type[Playbook]]] = getattr(
152
+ item, _FUNCTION_MANAGED_ATTR, None
153
+ )
154
+ if managed_classes:
155
+ setattr(item, _FUNCTION_MANAGED_ATTR, None)
156
+ request = _item_request(item)
157
+ arena_obj = request.getfixturevalue("arena")
158
+ matches = _matches_from_closed_arena(request.getfixturevalue("closed_arena"))
159
+ _run_managed_classes(arena_obj, matches, managed_classes)
160
+
161
+
162
+ @pytest.fixture(scope="class", autouse=True)
163
+ def _playbook_class_scope(request: pytest.FixtureRequest):
164
+ cls = getattr(request, "cls", None)
165
+ classes = _class_marker_classes(cls)
166
+ if not classes:
167
+ yield
168
+ return
169
+ unmanaged_classes, managed_classes = _partition_classes(classes)
170
+ arena_obj = request.getfixturevalue("arena")
171
+ closed = request.getfixturevalue("closed_arena")
172
+ matches = _matches_from_closed_arena(closed)
173
+ actives = _activate_classes(arena_obj, matches, unmanaged_classes)
174
+ try:
175
+ yield
176
+ finally:
177
+ _finish_playbook_scope(arena_obj, matches, actives, managed_classes)
178
+
179
+
180
+ @pytest.fixture(scope="module", autouse=True)
181
+ def _playbook_module_scope(request: pytest.FixtureRequest):
182
+ module = getattr(request, "module", None)
183
+ classes = _module_marker_classes(module)
184
+ if not classes:
185
+ yield
186
+ return
187
+ unmanaged_classes, managed_classes = _partition_classes(classes)
188
+ arena_obj = request.getfixturevalue("arena")
189
+ closed = request.getfixturevalue("closed_arena")
190
+ matches = _matches_from_closed_arena(closed)
191
+ actives = _activate_classes(arena_obj, matches, unmanaged_classes)
192
+ try:
193
+ yield
194
+ finally:
195
+ _finish_playbook_scope(arena_obj, matches, actives, managed_classes)
Binary file
@@ -0,0 +1,101 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import logging
6
+ from typing import List, Optional, Sequence
7
+
8
+ from arena_pytest.arena import OpenArena
9
+ from arena_pytest.ffi._ffi import (
10
+ ArenaBindingError,
11
+ ArenaLogLevel,
12
+ close_arena,
13
+ load_ffi,
14
+ open_arena as ffi_open_arena,
15
+ register_default_dispatcher_logging_target,
16
+ register_dispatcher_logging_target_for_logger,
17
+ set_dispatcher_component_allow_json,
18
+ set_dispatcher_dependency_allow_json,
19
+ )
20
+
21
+
22
+ def _normalize_optional_id_seq(
23
+ seq: Optional[Sequence[str]],
24
+ ) -> tuple[str, ...]:
25
+ if not seq:
26
+ return ()
27
+ return tuple(str(x).strip() for x in seq if str(x).strip())
28
+
29
+
30
+ class ClosedArena:
31
+ def __init__(
32
+ self,
33
+ name: str,
34
+ matches: List,
35
+ *,
36
+ log_level: ArenaLogLevel = ArenaLogLevel.INFO,
37
+ logger: Optional[logging.Logger] = None,
38
+ log_component_ids: Optional[Sequence[str]] = None,
39
+ log_dependency_ids: Optional[Sequence[str]] = None,
40
+ ):
41
+ self.name = name
42
+ self._matches = matches
43
+ self._log_level = log_level
44
+ self._logger = logger
45
+ self._log_dependency_ids = _normalize_optional_id_seq(log_dependency_ids)
46
+ self._log_component_ids = _normalize_optional_id_seq(log_component_ids)
47
+
48
+ def _config(self) -> str:
49
+ if not self._matches:
50
+ return "{}"
51
+ a_match = self._matches[0]
52
+ config = a_match._for_ffi() if hasattr(a_match, "_for_ffi") else a_match
53
+ return json.dumps(config)
54
+
55
+ async def open(self) -> OpenArena:
56
+ ffi = load_ffi()
57
+ if ffi is None:
58
+ raise ArenaBindingError(
59
+ "arena shared library not found (set ARENA_FFI_LIB or use Bazel runfiles)"
60
+ )
61
+ config = self._config()
62
+ dep_json = (
63
+ json.dumps(list(self._log_dependency_ids))
64
+ if self._log_dependency_ids
65
+ else None
66
+ )
67
+ comp_json = (
68
+ json.dumps(list(self._log_component_ids))
69
+ if self._log_component_ids
70
+ else None
71
+ )
72
+ await asyncio.to_thread(set_dispatcher_dependency_allow_json, ffi, dep_json)
73
+ await asyncio.to_thread(set_dispatcher_component_allow_json, ffi, comp_json)
74
+ if self._logger is not None:
75
+ log_tok = await asyncio.to_thread(
76
+ register_dispatcher_logging_target_for_logger,
77
+ ffi,
78
+ self._logger,
79
+ arena_log_level=self._log_level,
80
+ )
81
+ else:
82
+ log_tok = await asyncio.to_thread(
83
+ register_default_dispatcher_logging_target,
84
+ ffi,
85
+ arena_log_level=self._log_level,
86
+ )
87
+ try:
88
+ handle = await asyncio.to_thread(
89
+ ffi_open_arena,
90
+ ffi,
91
+ self.name.encode("utf-8"),
92
+ config,
93
+ log_level=self._log_level,
94
+ )
95
+ except ArenaBindingError as e:
96
+ await asyncio.to_thread(
97
+ close_arena, ffi, 0, dispatcher_logging_target_token=log_tok
98
+ )
99
+ raise ArenaBindingError(f"arena_open failed: {e}") from e
100
+
101
+ return OpenArena(ffi, handle, log_tok)
File without changes