eventsourcing 9.2.22__py3-none-any.whl → 9.3.0__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.
Potentially problematic release.
This version of eventsourcing might be problematic. Click here for more details.
- eventsourcing/__init__.py +1 -1
- eventsourcing/application.py +116 -135
- eventsourcing/cipher.py +15 -12
- eventsourcing/dispatch.py +31 -91
- eventsourcing/domain.py +220 -226
- eventsourcing/examples/__init__.py +0 -0
- eventsourcing/examples/aggregate1/__init__.py +0 -0
- eventsourcing/examples/aggregate1/application.py +27 -0
- eventsourcing/examples/aggregate1/domainmodel.py +16 -0
- eventsourcing/examples/aggregate1/test_application.py +37 -0
- eventsourcing/examples/aggregate2/__init__.py +0 -0
- eventsourcing/examples/aggregate2/application.py +27 -0
- eventsourcing/examples/aggregate2/domainmodel.py +22 -0
- eventsourcing/examples/aggregate2/test_application.py +37 -0
- eventsourcing/examples/aggregate3/__init__.py +0 -0
- eventsourcing/examples/aggregate3/application.py +27 -0
- eventsourcing/examples/aggregate3/domainmodel.py +38 -0
- eventsourcing/examples/aggregate3/test_application.py +37 -0
- eventsourcing/examples/aggregate4/__init__.py +0 -0
- eventsourcing/examples/aggregate4/application.py +27 -0
- eventsourcing/examples/aggregate4/domainmodel.py +114 -0
- eventsourcing/examples/aggregate4/test_application.py +38 -0
- eventsourcing/examples/aggregate5/__init__.py +0 -0
- eventsourcing/examples/aggregate5/application.py +27 -0
- eventsourcing/examples/aggregate5/domainmodel.py +131 -0
- eventsourcing/examples/aggregate5/test_application.py +38 -0
- eventsourcing/examples/aggregate6/__init__.py +0 -0
- eventsourcing/examples/aggregate6/application.py +30 -0
- eventsourcing/examples/aggregate6/domainmodel.py +123 -0
- eventsourcing/examples/aggregate6/test_application.py +38 -0
- eventsourcing/examples/aggregate6a/__init__.py +0 -0
- eventsourcing/examples/aggregate6a/application.py +40 -0
- eventsourcing/examples/aggregate6a/domainmodel.py +149 -0
- eventsourcing/examples/aggregate6a/test_application.py +45 -0
- eventsourcing/examples/aggregate7/__init__.py +0 -0
- eventsourcing/examples/aggregate7/application.py +48 -0
- eventsourcing/examples/aggregate7/domainmodel.py +144 -0
- eventsourcing/examples/aggregate7/persistence.py +57 -0
- eventsourcing/examples/aggregate7/test_application.py +38 -0
- eventsourcing/examples/aggregate7/test_compression_and_encryption.py +45 -0
- eventsourcing/examples/aggregate7/test_snapshotting_intervals.py +67 -0
- eventsourcing/examples/aggregate7a/__init__.py +0 -0
- eventsourcing/examples/aggregate7a/application.py +56 -0
- eventsourcing/examples/aggregate7a/domainmodel.py +170 -0
- eventsourcing/examples/aggregate7a/test_application.py +46 -0
- eventsourcing/examples/aggregate7a/test_compression_and_encryption.py +45 -0
- eventsourcing/examples/aggregate8/__init__.py +0 -0
- eventsourcing/examples/aggregate8/application.py +47 -0
- eventsourcing/examples/aggregate8/domainmodel.py +65 -0
- eventsourcing/examples/aggregate8/persistence.py +57 -0
- eventsourcing/examples/aggregate8/test_application.py +37 -0
- eventsourcing/examples/aggregate8/test_compression_and_encryption.py +44 -0
- eventsourcing/examples/aggregate8/test_snapshotting_intervals.py +38 -0
- eventsourcing/examples/bankaccounts/__init__.py +0 -0
- eventsourcing/examples/bankaccounts/application.py +70 -0
- eventsourcing/examples/bankaccounts/domainmodel.py +56 -0
- eventsourcing/examples/bankaccounts/test.py +173 -0
- eventsourcing/examples/cargoshipping/__init__.py +0 -0
- eventsourcing/examples/cargoshipping/application.py +126 -0
- eventsourcing/examples/cargoshipping/domainmodel.py +330 -0
- eventsourcing/examples/cargoshipping/interface.py +143 -0
- eventsourcing/examples/cargoshipping/test.py +231 -0
- eventsourcing/examples/contentmanagement/__init__.py +0 -0
- eventsourcing/examples/contentmanagement/application.py +118 -0
- eventsourcing/examples/contentmanagement/domainmodel.py +69 -0
- eventsourcing/examples/contentmanagement/test.py +180 -0
- eventsourcing/examples/contentmanagement/utils.py +26 -0
- eventsourcing/examples/contentmanagementsystem/__init__.py +0 -0
- eventsourcing/examples/contentmanagementsystem/application.py +54 -0
- eventsourcing/examples/contentmanagementsystem/postgres.py +17 -0
- eventsourcing/examples/contentmanagementsystem/sqlite.py +17 -0
- eventsourcing/examples/contentmanagementsystem/system.py +14 -0
- eventsourcing/examples/contentmanagementsystem/test_system.py +180 -0
- eventsourcing/examples/searchablecontent/__init__.py +0 -0
- eventsourcing/examples/searchablecontent/application.py +45 -0
- eventsourcing/examples/searchablecontent/persistence.py +23 -0
- eventsourcing/examples/searchablecontent/postgres.py +118 -0
- eventsourcing/examples/searchablecontent/sqlite.py +136 -0
- eventsourcing/examples/searchablecontent/test_application.py +110 -0
- eventsourcing/examples/searchablecontent/test_recorder.py +68 -0
- eventsourcing/examples/searchabletimestamps/__init__.py +0 -0
- eventsourcing/examples/searchabletimestamps/application.py +32 -0
- eventsourcing/examples/searchabletimestamps/persistence.py +20 -0
- eventsourcing/examples/searchabletimestamps/postgres.py +110 -0
- eventsourcing/examples/searchabletimestamps/sqlite.py +99 -0
- eventsourcing/examples/searchabletimestamps/test_searchabletimestamps.py +94 -0
- eventsourcing/examples/test_invoice.py +176 -0
- eventsourcing/examples/test_parking_lot.py +206 -0
- eventsourcing/interface.py +2 -2
- eventsourcing/persistence.py +85 -81
- eventsourcing/popo.py +30 -31
- eventsourcing/postgres.py +379 -590
- eventsourcing/sqlite.py +91 -99
- eventsourcing/system.py +52 -57
- eventsourcing/tests/application.py +20 -32
- eventsourcing/tests/application_tests/__init__.py +0 -0
- eventsourcing/tests/application_tests/test_application_with_automatic_snapshotting.py +55 -0
- eventsourcing/tests/application_tests/test_application_with_popo.py +22 -0
- eventsourcing/tests/application_tests/test_application_with_postgres.py +75 -0
- eventsourcing/tests/application_tests/test_application_with_sqlite.py +72 -0
- eventsourcing/tests/application_tests/test_cache.py +134 -0
- eventsourcing/tests/application_tests/test_event_sourced_log.py +162 -0
- eventsourcing/tests/application_tests/test_notificationlog.py +232 -0
- eventsourcing/tests/application_tests/test_notificationlogreader.py +126 -0
- eventsourcing/tests/application_tests/test_processapplication.py +110 -0
- eventsourcing/tests/application_tests/test_processingpolicy.py +109 -0
- eventsourcing/tests/application_tests/test_repository.py +504 -0
- eventsourcing/tests/application_tests/test_snapshotting.py +68 -0
- eventsourcing/tests/application_tests/test_upcasting.py +459 -0
- eventsourcing/tests/docs_tests/__init__.py +0 -0
- eventsourcing/tests/docs_tests/test_docs.py +293 -0
- eventsourcing/tests/domain.py +1 -1
- eventsourcing/tests/domain_tests/__init__.py +0 -0
- eventsourcing/tests/domain_tests/test_aggregate.py +1180 -0
- eventsourcing/tests/domain_tests/test_aggregate_decorators.py +1604 -0
- eventsourcing/tests/domain_tests/test_domainevent.py +80 -0
- eventsourcing/tests/interface_tests/__init__.py +0 -0
- eventsourcing/tests/interface_tests/test_remotenotificationlog.py +258 -0
- eventsourcing/tests/persistence.py +52 -50
- eventsourcing/tests/persistence_tests/__init__.py +0 -0
- eventsourcing/tests/persistence_tests/test_aes.py +93 -0
- eventsourcing/tests/persistence_tests/test_connection_pool.py +722 -0
- eventsourcing/tests/persistence_tests/test_eventstore.py +72 -0
- eventsourcing/tests/persistence_tests/test_infrastructure_factory.py +21 -0
- eventsourcing/tests/persistence_tests/test_mapper.py +113 -0
- eventsourcing/tests/persistence_tests/test_noninterleaving_notification_ids.py +69 -0
- eventsourcing/tests/persistence_tests/test_popo.py +124 -0
- eventsourcing/tests/persistence_tests/test_postgres.py +1119 -0
- eventsourcing/tests/persistence_tests/test_sqlite.py +348 -0
- eventsourcing/tests/persistence_tests/test_transcoder.py +44 -0
- eventsourcing/tests/postgres_utils.py +7 -7
- eventsourcing/tests/system_tests/__init__.py +0 -0
- eventsourcing/tests/system_tests/test_runner.py +935 -0
- eventsourcing/tests/system_tests/test_system.py +284 -0
- eventsourcing/tests/utils_tests/__init__.py +0 -0
- eventsourcing/tests/utils_tests/test_utils.py +226 -0
- eventsourcing/utils.py +47 -50
- {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0.dist-info}/METADATA +29 -79
- eventsourcing-9.3.0.dist-info/RECORD +145 -0
- {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0.dist-info}/WHEEL +1 -2
- eventsourcing-9.2.22.dist-info/RECORD +0 -25
- eventsourcing-9.2.22.dist-info/top_level.txt +0 -1
- {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0.dist-info}/AUTHORS +0 -0
- {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0.dist-info}/LICENSE +0 -0
|
@@ -0,0 +1,935 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shlex
|
|
5
|
+
import subprocess
|
|
6
|
+
from queue import Queue
|
|
7
|
+
from threading import Event
|
|
8
|
+
from time import sleep
|
|
9
|
+
from typing import (
|
|
10
|
+
TYPE_CHECKING,
|
|
11
|
+
ClassVar,
|
|
12
|
+
Generic,
|
|
13
|
+
Iterable,
|
|
14
|
+
Iterator,
|
|
15
|
+
List,
|
|
16
|
+
Sequence,
|
|
17
|
+
Tuple,
|
|
18
|
+
Type,
|
|
19
|
+
TypeVar,
|
|
20
|
+
)
|
|
21
|
+
from unittest.case import TestCase
|
|
22
|
+
from unittest.mock import MagicMock
|
|
23
|
+
|
|
24
|
+
from eventsourcing.application import ProcessingEvent, RecordingEvent
|
|
25
|
+
from eventsourcing.domain import Aggregate, AggregateEvent, event
|
|
26
|
+
from eventsourcing.persistence import Notification, ProgrammingError
|
|
27
|
+
from eventsourcing.postgres import PostgresDatastore
|
|
28
|
+
from eventsourcing.system import (
|
|
29
|
+
ConvertingThread,
|
|
30
|
+
EventProcessingError,
|
|
31
|
+
MultiThreadedRunner,
|
|
32
|
+
NewMultiThreadedRunner,
|
|
33
|
+
NewSingleThreadedRunner,
|
|
34
|
+
NotificationConvertingError,
|
|
35
|
+
NotificationPullingError,
|
|
36
|
+
ProcessApplication,
|
|
37
|
+
ProcessingJob,
|
|
38
|
+
PullingThread,
|
|
39
|
+
Runner,
|
|
40
|
+
RunnerAlreadyStartedError,
|
|
41
|
+
SingleThreadedRunner,
|
|
42
|
+
System,
|
|
43
|
+
)
|
|
44
|
+
from eventsourcing.tests.application import BankAccounts
|
|
45
|
+
from eventsourcing.tests.application_tests.test_processapplication import EmailProcess
|
|
46
|
+
from eventsourcing.tests.persistence import tmpfile_uris
|
|
47
|
+
from eventsourcing.tests.postgres_utils import drop_postgres_table
|
|
48
|
+
from eventsourcing.utils import clear_topic_cache, get_topic
|
|
49
|
+
|
|
50
|
+
if TYPE_CHECKING: # pragma: nocover
|
|
51
|
+
from uuid import UUID
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class EmailProcess2(EmailProcess):
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
TRunner = TypeVar("TRunner", bound=Runner)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class RunnerTestCase(TestCase, Generic[TRunner]):
|
|
62
|
+
runner_class: Type[TRunner]
|
|
63
|
+
runner: TRunner | None
|
|
64
|
+
|
|
65
|
+
def setUp(self) -> None:
|
|
66
|
+
self.runner: TRunner | None = None
|
|
67
|
+
|
|
68
|
+
def tearDown(self) -> None:
|
|
69
|
+
if self.runner:
|
|
70
|
+
try:
|
|
71
|
+
self.runner.stop()
|
|
72
|
+
except Exception as e:
|
|
73
|
+
raise Exception("Runner errored: " + str(e)) from e
|
|
74
|
+
|
|
75
|
+
def start_runner(self, system):
|
|
76
|
+
self.runner = self.runner_class(system)
|
|
77
|
+
self.runner.start()
|
|
78
|
+
|
|
79
|
+
def test_runner_constructed_with_env_has_apps_with_env(self):
|
|
80
|
+
system = System(pipes=[[BankAccounts, EmailProcess]])
|
|
81
|
+
env = {"MY_ENV_VAR": "my_env_val"}
|
|
82
|
+
self.runner = self.runner_class(system, env)
|
|
83
|
+
self.runner.start()
|
|
84
|
+
|
|
85
|
+
# Check leaders get the environment.
|
|
86
|
+
app = self.runner.get(BankAccounts)
|
|
87
|
+
self.assertEqual(app.env.get("MY_ENV_VAR"), "my_env_val")
|
|
88
|
+
|
|
89
|
+
# Check followers get the environment.
|
|
90
|
+
app = self.runner.get(EmailProcess)
|
|
91
|
+
self.assertEqual(app.env.get("MY_ENV_VAR"), "my_env_val")
|
|
92
|
+
|
|
93
|
+
# Stop the runner before we start another.
|
|
94
|
+
self.runner.stop()
|
|
95
|
+
|
|
96
|
+
# Check singles get the environment.
|
|
97
|
+
system = System(pipes=[[BankAccounts]])
|
|
98
|
+
env = {"MY_ENV_VAR": "my_env_val"}
|
|
99
|
+
self.runner = self.runner_class(system, env)
|
|
100
|
+
self.runner.start()
|
|
101
|
+
app = self.runner.get(BankAccounts)
|
|
102
|
+
self.assertEqual(app.env.get("MY_ENV_VAR"), "my_env_val")
|
|
103
|
+
|
|
104
|
+
def test_starts_with_single_app(self):
|
|
105
|
+
self.start_runner(System(pipes=[[BankAccounts]]))
|
|
106
|
+
app = self.runner.get(BankAccounts)
|
|
107
|
+
self.assertIsInstance(app, BankAccounts)
|
|
108
|
+
|
|
109
|
+
def test_calling_start_twice_raises_error(self):
|
|
110
|
+
self.start_runner(System(pipes=[[BankAccounts]]))
|
|
111
|
+
with self.assertRaises(RunnerAlreadyStartedError):
|
|
112
|
+
self.runner.start()
|
|
113
|
+
|
|
114
|
+
def test_system_with_one_edge(self):
|
|
115
|
+
self.start_runner(System(pipes=[[BankAccounts, EmailProcess]]))
|
|
116
|
+
accounts = self.runner.get(BankAccounts)
|
|
117
|
+
email_process = self.runner.get(EmailProcess)
|
|
118
|
+
|
|
119
|
+
section = email_process.notification_log["1,5"]
|
|
120
|
+
self.assertEqual(len(section.items), 0, section.items)
|
|
121
|
+
|
|
122
|
+
for _ in range(10):
|
|
123
|
+
accounts.open_account(
|
|
124
|
+
full_name="Alice",
|
|
125
|
+
email_address="alice@example.com",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
self.wait_for_runner()
|
|
129
|
+
|
|
130
|
+
section = email_process.notification_log["1,10"]
|
|
131
|
+
self.assertEqual(len(section.items), 10)
|
|
132
|
+
|
|
133
|
+
def test_system_with_two_edges(self):
|
|
134
|
+
clear_topic_cache()
|
|
135
|
+
|
|
136
|
+
# Construct system and runner.
|
|
137
|
+
system = System(
|
|
138
|
+
pipes=[
|
|
139
|
+
[
|
|
140
|
+
BankAccounts,
|
|
141
|
+
EmailProcess,
|
|
142
|
+
],
|
|
143
|
+
[
|
|
144
|
+
BankAccounts,
|
|
145
|
+
EmailProcess2,
|
|
146
|
+
],
|
|
147
|
+
]
|
|
148
|
+
)
|
|
149
|
+
self.start_runner(system)
|
|
150
|
+
|
|
151
|
+
# Get apps.
|
|
152
|
+
accounts = self.runner.get(BankAccounts)
|
|
153
|
+
email_process1 = self.runner.get(EmailProcess)
|
|
154
|
+
email_process2 = self.runner.get(EmailProcess2)
|
|
155
|
+
|
|
156
|
+
# Check we processed nothing.
|
|
157
|
+
section = email_process1.notification_log["1,5"]
|
|
158
|
+
self.assertEqual(len(section.items), 0, section.items)
|
|
159
|
+
section = email_process2.notification_log["1,5"]
|
|
160
|
+
self.assertEqual(len(section.items), 0, section.items)
|
|
161
|
+
|
|
162
|
+
# Create ten events.
|
|
163
|
+
for _ in range(10):
|
|
164
|
+
accounts.open_account(
|
|
165
|
+
full_name="Alice",
|
|
166
|
+
email_address="alice@example.com",
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Check we processed ten events.
|
|
170
|
+
self.wait_for_runner()
|
|
171
|
+
section = email_process1.notification_log["1,10"]
|
|
172
|
+
self.assertEqual(len(section.items), 10)
|
|
173
|
+
section = email_process2.notification_log["1,10"]
|
|
174
|
+
self.assertEqual(len(section.items), 10)
|
|
175
|
+
|
|
176
|
+
def test_system_with_processing_loop(self):
|
|
177
|
+
class Command(Aggregate):
|
|
178
|
+
def __init__(self, text: str):
|
|
179
|
+
self.text = text
|
|
180
|
+
self.output: str | None = None
|
|
181
|
+
self.error: str | None = None
|
|
182
|
+
|
|
183
|
+
@event
|
|
184
|
+
def done(self, output: str, error: str):
|
|
185
|
+
self.output = output
|
|
186
|
+
self.error = error
|
|
187
|
+
|
|
188
|
+
class Result(Aggregate):
|
|
189
|
+
def __init__(self, command_id: UUID, output: str, error: str):
|
|
190
|
+
self.command_id = command_id
|
|
191
|
+
self.output = output
|
|
192
|
+
self.error = error
|
|
193
|
+
|
|
194
|
+
class Commands(ProcessApplication):
|
|
195
|
+
def create_command(self, text: str) -> UUID:
|
|
196
|
+
command = Command(text=text)
|
|
197
|
+
self.save(command)
|
|
198
|
+
return command.id
|
|
199
|
+
|
|
200
|
+
def policy(
|
|
201
|
+
self,
|
|
202
|
+
domain_event: AggregateEvent,
|
|
203
|
+
processing_event: ProcessingEvent,
|
|
204
|
+
) -> None:
|
|
205
|
+
if isinstance(domain_event, Result.Created):
|
|
206
|
+
command = self.repository.get(domain_event.command_id)
|
|
207
|
+
command.done(
|
|
208
|
+
output=domain_event.output,
|
|
209
|
+
error=domain_event.error,
|
|
210
|
+
)
|
|
211
|
+
processing_event.collect_events(command)
|
|
212
|
+
|
|
213
|
+
def get_result(self, command_id: UUID) -> Tuple[str, str]:
|
|
214
|
+
command = self.repository.get(command_id)
|
|
215
|
+
return command.output, command.error
|
|
216
|
+
|
|
217
|
+
class Results(ProcessApplication):
|
|
218
|
+
def policy(
|
|
219
|
+
self,
|
|
220
|
+
domain_event: AggregateEvent,
|
|
221
|
+
processing_event: ProcessingEvent,
|
|
222
|
+
) -> None:
|
|
223
|
+
if isinstance(domain_event, Command.Created):
|
|
224
|
+
try:
|
|
225
|
+
openargs = shlex.split(domain_event.text)
|
|
226
|
+
output = subprocess.check_output(openargs) # noqa: S603
|
|
227
|
+
error = ""
|
|
228
|
+
except Exception as e:
|
|
229
|
+
error = str(e)
|
|
230
|
+
output = b""
|
|
231
|
+
result = Result(
|
|
232
|
+
command_id=domain_event.originator_id,
|
|
233
|
+
output=output.decode("utf8"),
|
|
234
|
+
error=error,
|
|
235
|
+
)
|
|
236
|
+
processing_event.collect_events(result)
|
|
237
|
+
|
|
238
|
+
self.start_runner(System([[Commands, Results, Commands]]))
|
|
239
|
+
|
|
240
|
+
commands = self.runner.get(Commands)
|
|
241
|
+
command_id1 = commands.create_command("echo 'Hello World'")
|
|
242
|
+
command_id2 = commands.create_command("notacommand")
|
|
243
|
+
|
|
244
|
+
self.wait_for_runner()
|
|
245
|
+
self.wait_for_runner()
|
|
246
|
+
|
|
247
|
+
for _ in range(10):
|
|
248
|
+
output, error = commands.get_result(command_id1)
|
|
249
|
+
if output is None:
|
|
250
|
+
sleep(0.1)
|
|
251
|
+
else:
|
|
252
|
+
break
|
|
253
|
+
else:
|
|
254
|
+
self.fail("No results from command")
|
|
255
|
+
|
|
256
|
+
self.assertEqual(output, "Hello World\n")
|
|
257
|
+
self.assertEqual(error, "")
|
|
258
|
+
|
|
259
|
+
for _ in range(10):
|
|
260
|
+
output, error = commands.get_result(command_id2)
|
|
261
|
+
if output is None:
|
|
262
|
+
sleep(0.1)
|
|
263
|
+
else:
|
|
264
|
+
break
|
|
265
|
+
else:
|
|
266
|
+
self.fail("No results from command")
|
|
267
|
+
|
|
268
|
+
self.assertEqual(output, "")
|
|
269
|
+
self.assertIn("No such file or directory: 'notacommand'", error)
|
|
270
|
+
|
|
271
|
+
def test_catches_up_with_outstanding_notifications(self):
|
|
272
|
+
# Construct system and runner.
|
|
273
|
+
system = System(pipes=[[BankAccounts, EmailProcess]])
|
|
274
|
+
self.runner = self.runner_class(system)
|
|
275
|
+
|
|
276
|
+
# Get apps.
|
|
277
|
+
accounts = self.runner.get(BankAccounts)
|
|
278
|
+
email_process1 = self.runner.get(EmailProcess)
|
|
279
|
+
|
|
280
|
+
# Create an event.
|
|
281
|
+
accounts.open_account(
|
|
282
|
+
full_name="Alice",
|
|
283
|
+
email_address="alice@example.com",
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Check we processed nothing.
|
|
287
|
+
self.assertEqual(email_process1.recorder.max_tracking_id("BankAccounts"), 0)
|
|
288
|
+
|
|
289
|
+
# Start the runner.
|
|
290
|
+
self.runner.start()
|
|
291
|
+
|
|
292
|
+
# Create another event.
|
|
293
|
+
accounts.open_account(
|
|
294
|
+
full_name="Alice",
|
|
295
|
+
email_address="alice@example.com",
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# Check we processed two events.
|
|
299
|
+
self.wait_for_runner()
|
|
300
|
+
self.assertEqual(email_process1.recorder.max_tracking_id("BankAccounts"), 2)
|
|
301
|
+
|
|
302
|
+
def test_filters_notifications_by_follow_topics(self):
|
|
303
|
+
class MyEmailProcess(EmailProcess):
|
|
304
|
+
follow_topics: ClassVar[Sequence[str]] = [
|
|
305
|
+
get_topic(AggregateEvent)
|
|
306
|
+
] # follow nothing
|
|
307
|
+
|
|
308
|
+
system = System(pipes=[[BankAccounts, MyEmailProcess]])
|
|
309
|
+
self.runner = self.runner_class(system)
|
|
310
|
+
|
|
311
|
+
accounts = self.runner.get(BankAccounts)
|
|
312
|
+
email_process = self.runner.get(MyEmailProcess)
|
|
313
|
+
|
|
314
|
+
accounts.open_account(
|
|
315
|
+
full_name="Alice",
|
|
316
|
+
email_address="alice@example.com",
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
self.runner.start()
|
|
320
|
+
|
|
321
|
+
accounts.open_account(
|
|
322
|
+
full_name="Alice",
|
|
323
|
+
email_address="alice@example.com",
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
self.wait_for_runner()
|
|
327
|
+
|
|
328
|
+
self.assertEqual(len(email_process.notification_log["1,10"].items), 0)
|
|
329
|
+
|
|
330
|
+
def wait_for_runner(self):
|
|
331
|
+
pass
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
class SingleThreadedRunnerFollowersOrderingMixin:
|
|
335
|
+
"""Followers ordering tests for single-threaded runners."""
|
|
336
|
+
|
|
337
|
+
def test_followers_are_prompted_in_declaration_order(self):
|
|
338
|
+
"""Validate the order in which followers are prompted by the runner.
|
|
339
|
+
|
|
340
|
+
This test can, by nature, show some flakiness. That is, we can
|
|
341
|
+
see false negatives at times when a random ordering would match
|
|
342
|
+
the expected ordering. We mitigate this problem by increasing
|
|
343
|
+
the number of followers to be ordered.
|
|
344
|
+
"""
|
|
345
|
+
clear_topic_cache()
|
|
346
|
+
app_calls = []
|
|
347
|
+
|
|
348
|
+
class NameLogger(EmailProcess):
|
|
349
|
+
def policy(self, _, __):
|
|
350
|
+
app_calls.append(self.__class__.__name__)
|
|
351
|
+
|
|
352
|
+
def make_name_logger(n: int) -> type:
|
|
353
|
+
return type(f"NameLogger{n}", (NameLogger,), {})
|
|
354
|
+
|
|
355
|
+
# Construct system and runner.
|
|
356
|
+
system = System(
|
|
357
|
+
pipes=[
|
|
358
|
+
[BankAccounts, make_name_logger(3)],
|
|
359
|
+
[BankAccounts, make_name_logger(4)],
|
|
360
|
+
[BankAccounts, make_name_logger(1)],
|
|
361
|
+
[BankAccounts, make_name_logger(5)],
|
|
362
|
+
[BankAccounts, make_name_logger(2)],
|
|
363
|
+
]
|
|
364
|
+
)
|
|
365
|
+
self.start_runner(system)
|
|
366
|
+
|
|
367
|
+
# Create an event.
|
|
368
|
+
self.runner.get(BankAccounts).open_account(
|
|
369
|
+
full_name="Alice",
|
|
370
|
+
email_address="alice@example.com",
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
self.wait_for_runner()
|
|
374
|
+
|
|
375
|
+
# Check the applications' policy were called in the right order.
|
|
376
|
+
self.assertEqual(
|
|
377
|
+
app_calls,
|
|
378
|
+
["NameLogger3", "NameLogger4", "NameLogger1", "NameLogger5", "NameLogger2"],
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class TestSingleThreadedRunner(
|
|
383
|
+
RunnerTestCase[SingleThreadedRunner], SingleThreadedRunnerFollowersOrderingMixin
|
|
384
|
+
):
|
|
385
|
+
runner_class = SingleThreadedRunner
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
class TestNewSingleThreadedRunner(
|
|
389
|
+
RunnerTestCase[NewSingleThreadedRunner], SingleThreadedRunnerFollowersOrderingMixin
|
|
390
|
+
):
|
|
391
|
+
runner_class = NewSingleThreadedRunner
|
|
392
|
+
|
|
393
|
+
def test_ignores_recording_event_if_seen_subsequent(self):
|
|
394
|
+
system = System(pipes=[[BankAccounts, EmailProcess]])
|
|
395
|
+
self.start_runner(system)
|
|
396
|
+
|
|
397
|
+
accounts = self.runner.get(BankAccounts)
|
|
398
|
+
email_process = self.runner.get(EmailProcess)
|
|
399
|
+
|
|
400
|
+
accounts.open_account(
|
|
401
|
+
full_name="Alice",
|
|
402
|
+
email_address="alice@example.com",
|
|
403
|
+
)
|
|
404
|
+
self.wait_for_runner()
|
|
405
|
+
|
|
406
|
+
self.assertEqual(len(email_process.notification_log["1,10"].items), 1)
|
|
407
|
+
|
|
408
|
+
# Reset this to break sequence.
|
|
409
|
+
accounts.previous_max_notification_id -= 1
|
|
410
|
+
|
|
411
|
+
accounts.open_account(
|
|
412
|
+
full_name="Alice",
|
|
413
|
+
email_address="alice@example.com",
|
|
414
|
+
)
|
|
415
|
+
self.wait_for_runner()
|
|
416
|
+
|
|
417
|
+
self.assertEqual(len(email_process.notification_log["1,10"].items), 1)
|
|
418
|
+
|
|
419
|
+
def test_received_notifications_accumulate(self):
|
|
420
|
+
self.start_runner(
|
|
421
|
+
System(
|
|
422
|
+
[
|
|
423
|
+
[
|
|
424
|
+
BankAccounts,
|
|
425
|
+
EmailProcess,
|
|
426
|
+
]
|
|
427
|
+
]
|
|
428
|
+
)
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
accounts = self.runner.get(BankAccounts)
|
|
432
|
+
# Need to get the lock, so that they aren't cleared.
|
|
433
|
+
with self.runner._processing_lock:
|
|
434
|
+
accounts.open_account("Alice", "alice@example.com")
|
|
435
|
+
self.assertEqual(len(self.runner._recording_events_received), 1)
|
|
436
|
+
accounts.open_account("Bob", "bob@example.com")
|
|
437
|
+
self.assertEqual(len(self.runner._recording_events_received), 2)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
class TestPullingThread(TestCase):
|
|
441
|
+
def test_receive_recording_event_does_not_block(self):
|
|
442
|
+
thread = PullingThread(
|
|
443
|
+
converting_queue=Queue(),
|
|
444
|
+
follower=MagicMock(),
|
|
445
|
+
leader_name="BankAccounts",
|
|
446
|
+
has_errored=Event(),
|
|
447
|
+
)
|
|
448
|
+
thread.recording_event_queue.maxsize = 1
|
|
449
|
+
self.assertEqual(thread.recording_event_queue.qsize(), 0)
|
|
450
|
+
thread.receive_recording_event(
|
|
451
|
+
RecordingEvent(
|
|
452
|
+
application_name="BankAccounts",
|
|
453
|
+
recordings=[],
|
|
454
|
+
previous_max_notification_id=None,
|
|
455
|
+
)
|
|
456
|
+
)
|
|
457
|
+
self.assertEqual(thread.recording_event_queue.qsize(), 1)
|
|
458
|
+
self.assertFalse(thread.overflow_event.is_set())
|
|
459
|
+
thread.receive_recording_event(
|
|
460
|
+
RecordingEvent(
|
|
461
|
+
application_name="BankAccounts",
|
|
462
|
+
recordings=[],
|
|
463
|
+
previous_max_notification_id=1,
|
|
464
|
+
)
|
|
465
|
+
)
|
|
466
|
+
self.assertEqual(thread.recording_event_queue.qsize(), 1)
|
|
467
|
+
self.assertTrue(thread.overflow_event.is_set())
|
|
468
|
+
|
|
469
|
+
def test_stops_because_stopping_event_is_set(self):
|
|
470
|
+
thread = PullingThread(
|
|
471
|
+
converting_queue=Queue(),
|
|
472
|
+
follower=MagicMock(),
|
|
473
|
+
leader_name="BankAccounts",
|
|
474
|
+
has_errored=Event(),
|
|
475
|
+
)
|
|
476
|
+
self.assertEqual(thread.recording_event_queue.qsize(), 0)
|
|
477
|
+
thread.receive_recording_event(
|
|
478
|
+
RecordingEvent(
|
|
479
|
+
application_name="BankAccounts",
|
|
480
|
+
recordings=[],
|
|
481
|
+
previous_max_notification_id=None,
|
|
482
|
+
)
|
|
483
|
+
)
|
|
484
|
+
self.assertEqual(thread.recording_event_queue.qsize(), 1)
|
|
485
|
+
thread.stop() # Set 'is_stopping' event.
|
|
486
|
+
self.assertEqual(thread.recording_event_queue.qsize(), 2)
|
|
487
|
+
thread.start()
|
|
488
|
+
thread.join(timeout=1)
|
|
489
|
+
self.assertFalse(thread.is_alive())
|
|
490
|
+
self.assertEqual(thread.recording_event_queue.qsize(), 2)
|
|
491
|
+
|
|
492
|
+
def test_stops_because_recording_event_queue_was_poisoned(self):
|
|
493
|
+
thread = PullingThread(
|
|
494
|
+
converting_queue=Queue(),
|
|
495
|
+
follower=MagicMock(),
|
|
496
|
+
leader_name="BankAccounts",
|
|
497
|
+
has_errored=Event(),
|
|
498
|
+
)
|
|
499
|
+
self.assertEqual(thread.recording_event_queue.qsize(), 0)
|
|
500
|
+
thread.start()
|
|
501
|
+
thread.stop() # Poison queue.
|
|
502
|
+
thread.join(timeout=1)
|
|
503
|
+
self.assertFalse(thread.is_alive())
|
|
504
|
+
self.assertEqual(thread.recording_event_queue.qsize(), 0)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
class TestMultiThreadedRunner(RunnerTestCase[MultiThreadedRunner]):
|
|
508
|
+
runner_class = MultiThreadedRunner
|
|
509
|
+
|
|
510
|
+
def test_ignores_recording_event_if_seen_subsequent(self):
|
|
511
|
+
# Skipping this because this runner doesn't take
|
|
512
|
+
# notice of attribute previous_max_notification_id.
|
|
513
|
+
pass
|
|
514
|
+
|
|
515
|
+
def wait_for_runner(self):
|
|
516
|
+
sleep(0.3)
|
|
517
|
+
try:
|
|
518
|
+
self.runner.reraise_thread_errors()
|
|
519
|
+
except Exception as e:
|
|
520
|
+
self.runner = None
|
|
521
|
+
raise Exception("Runner errored: " + str(e)) from e
|
|
522
|
+
|
|
523
|
+
class BrokenInitialisation(EmailProcess):
|
|
524
|
+
def __init__(self, *_, **__):
|
|
525
|
+
msg = "Just testing error handling when initialisation is broken"
|
|
526
|
+
raise ProgrammingError(msg)
|
|
527
|
+
|
|
528
|
+
class BrokenProcessing(EmailProcess):
|
|
529
|
+
def process_event(self, _, __):
|
|
530
|
+
msg = "Just testing error handling when processing is broken"
|
|
531
|
+
raise ProgrammingError(msg)
|
|
532
|
+
|
|
533
|
+
def test_stops_if_app_initialisation_is_broken(self):
|
|
534
|
+
system = System(
|
|
535
|
+
pipes=[
|
|
536
|
+
[
|
|
537
|
+
BankAccounts,
|
|
538
|
+
TestMultiThreadedRunner.BrokenInitialisation,
|
|
539
|
+
],
|
|
540
|
+
]
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
with self.assertRaises(Exception) as cm:
|
|
544
|
+
self.runner_class(system)
|
|
545
|
+
|
|
546
|
+
self.assertEqual(
|
|
547
|
+
cm.exception.args[0],
|
|
548
|
+
"Just testing error handling when initialisation is broken",
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
def test_stop_raises_if_event_processing_is_broken(self):
|
|
552
|
+
system = System(
|
|
553
|
+
pipes=[
|
|
554
|
+
[
|
|
555
|
+
BankAccounts,
|
|
556
|
+
TestMultiThreadedRunner.BrokenProcessing,
|
|
557
|
+
],
|
|
558
|
+
]
|
|
559
|
+
)
|
|
560
|
+
self.start_runner(system)
|
|
561
|
+
|
|
562
|
+
accounts = self.runner.get(BankAccounts)
|
|
563
|
+
accounts.open_account(
|
|
564
|
+
full_name="Alice",
|
|
565
|
+
email_address="alice@example.com",
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
# Wait for runner to stop.
|
|
569
|
+
self.assertTrue(self.runner.has_errored.wait(timeout=1))
|
|
570
|
+
|
|
571
|
+
# Check stop() raises exception.
|
|
572
|
+
with self.assertRaises(EventProcessingError) as cm:
|
|
573
|
+
self.runner.stop()
|
|
574
|
+
self.assertIn(
|
|
575
|
+
"Just testing error handling when processing is broken",
|
|
576
|
+
cm.exception.args[0],
|
|
577
|
+
)
|
|
578
|
+
self.runner = None
|
|
579
|
+
|
|
580
|
+
def test_watch_for_errors_raises_if_runner_errors(self):
|
|
581
|
+
system = System(
|
|
582
|
+
pipes=[
|
|
583
|
+
[
|
|
584
|
+
BankAccounts,
|
|
585
|
+
TestMultiThreadedRunner.BrokenProcessing,
|
|
586
|
+
],
|
|
587
|
+
]
|
|
588
|
+
)
|
|
589
|
+
# Create runner.
|
|
590
|
+
self.runner = self.runner_class(system)
|
|
591
|
+
|
|
592
|
+
# Create some notifications.
|
|
593
|
+
accounts = self.runner.get(BankAccounts)
|
|
594
|
+
accounts.open_account(
|
|
595
|
+
full_name="Alice",
|
|
596
|
+
email_address="alice@example.com",
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
# Start runner.
|
|
600
|
+
self.runner.start()
|
|
601
|
+
|
|
602
|
+
# Trigger pulling of notifications.
|
|
603
|
+
accounts = self.runner.get(BankAccounts)
|
|
604
|
+
accounts.open_account(
|
|
605
|
+
full_name="Alice",
|
|
606
|
+
email_address="alice@example.com",
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
# Check watch_for_errors() raises exception.
|
|
610
|
+
with self.assertRaises(EventProcessingError) as cm:
|
|
611
|
+
self.runner.watch_for_errors(timeout=1)
|
|
612
|
+
self.assertEqual(
|
|
613
|
+
cm.exception.args[0],
|
|
614
|
+
"Just testing error handling when processing is broken",
|
|
615
|
+
)
|
|
616
|
+
self.runner = None
|
|
617
|
+
|
|
618
|
+
def test_watch_for_errors_exits_without_raising_after_timeout(self):
|
|
619
|
+
# Construct system and start runner
|
|
620
|
+
system = System(
|
|
621
|
+
pipes=[
|
|
622
|
+
[
|
|
623
|
+
BankAccounts,
|
|
624
|
+
EmailProcess,
|
|
625
|
+
],
|
|
626
|
+
]
|
|
627
|
+
)
|
|
628
|
+
self.start_runner(system)
|
|
629
|
+
|
|
630
|
+
# Watch for error with a timeout. Check returns False.
|
|
631
|
+
self.assertFalse(self.runner.watch_for_errors(timeout=0.0))
|
|
632
|
+
|
|
633
|
+
def test_stops_if_app_processing_is_broken(self):
|
|
634
|
+
system = System(
|
|
635
|
+
pipes=[
|
|
636
|
+
[
|
|
637
|
+
BankAccounts,
|
|
638
|
+
TestMultiThreadedRunner.BrokenProcessing,
|
|
639
|
+
],
|
|
640
|
+
]
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
self.start_runner(system)
|
|
644
|
+
|
|
645
|
+
accounts = self.runner.get(BankAccounts)
|
|
646
|
+
accounts.open_account(
|
|
647
|
+
full_name="Alice",
|
|
648
|
+
email_address="alice@example.com",
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
# Check watch_for_errors() raises exception.
|
|
652
|
+
with self.assertRaises(EventProcessingError) as cm:
|
|
653
|
+
self.runner.watch_for_errors(timeout=1)
|
|
654
|
+
self.assertIn(
|
|
655
|
+
"Just testing error handling when processing is broken",
|
|
656
|
+
cm.exception.args[0],
|
|
657
|
+
)
|
|
658
|
+
self.runner = None
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
class TestMultiThreadedRunnerWithSQLiteFileBased(TestMultiThreadedRunner):
|
|
662
|
+
def setUp(self):
|
|
663
|
+
super().setUp()
|
|
664
|
+
os.environ["PERSISTENCE_MODULE"] = "eventsourcing.sqlite"
|
|
665
|
+
uris = tmpfile_uris()
|
|
666
|
+
os.environ[f"{BankAccounts.name.upper()}_SQLITE_DBNAME"] = next(uris)
|
|
667
|
+
os.environ[f"{EmailProcess.name.upper()}_SQLITE_DBNAME"] = next(uris)
|
|
668
|
+
os.environ[f"{EmailProcess.name.upper()}2_SQLITE_DBNAME"] = next(uris)
|
|
669
|
+
os.environ[f"MY{EmailProcess.name.upper()}_SQLITE_DBNAME"] = next(uris)
|
|
670
|
+
os.environ["BROKENPROCESSING_SQLITE_DBNAME"] = next(uris)
|
|
671
|
+
os.environ["BROKENCONVERTING_SQLITE_DBNAME"] = next(uris)
|
|
672
|
+
os.environ["BROKENPULLING_SQLITE_DBNAME"] = next(uris)
|
|
673
|
+
os.environ["COMMANDS_SQLITE_DBNAME"] = next(uris)
|
|
674
|
+
os.environ["RESULTS_SQLITE_DBNAME"] = next(uris)
|
|
675
|
+
|
|
676
|
+
def tearDown(self):
|
|
677
|
+
del os.environ["PERSISTENCE_MODULE"]
|
|
678
|
+
del os.environ[f"{BankAccounts.name.upper()}_SQLITE_DBNAME"]
|
|
679
|
+
del os.environ[f"{EmailProcess.name.upper()}_SQLITE_DBNAME"]
|
|
680
|
+
del os.environ[f"MY{EmailProcess.name.upper()}_SQLITE_DBNAME"]
|
|
681
|
+
del os.environ[f"{EmailProcess.name.upper()}2_SQLITE_DBNAME"]
|
|
682
|
+
del os.environ["BROKENPROCESSING_SQLITE_DBNAME"]
|
|
683
|
+
del os.environ["BROKENCONVERTING_SQLITE_DBNAME"]
|
|
684
|
+
del os.environ["BROKENPULLING_SQLITE_DBNAME"]
|
|
685
|
+
del os.environ["COMMANDS_SQLITE_DBNAME"]
|
|
686
|
+
del os.environ["RESULTS_SQLITE_DBNAME"]
|
|
687
|
+
super().tearDown()
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
class TestMultiThreadedRunnerWithSQLiteInMemory(TestMultiThreadedRunner):
|
|
691
|
+
def setUp(self):
|
|
692
|
+
super().setUp()
|
|
693
|
+
os.environ["PERSISTENCE_MODULE"] = "eventsourcing.sqlite"
|
|
694
|
+
os.environ[f"{BankAccounts.name.upper()}_SQLITE_DBNAME"] = (
|
|
695
|
+
f"file:{BankAccounts.name.lower()}?mode=memory&cache=shared"
|
|
696
|
+
)
|
|
697
|
+
os.environ[f"{EmailProcess.name.upper()}_SQLITE_DBNAME"] = (
|
|
698
|
+
f"file:{EmailProcess.name.lower()}?mode=memory&cache=shared"
|
|
699
|
+
)
|
|
700
|
+
os.environ[f"MY{EmailProcess.name.upper()}_SQLITE_DBNAME"] = (
|
|
701
|
+
f"file:{EmailProcess.name.lower()}?mode=memory&cache=shared"
|
|
702
|
+
)
|
|
703
|
+
os.environ[f"{EmailProcess.name.upper()}2_SQLITE_DBNAME"] = (
|
|
704
|
+
f"file:{EmailProcess.name.lower()}2?mode=memory&cache=shared"
|
|
705
|
+
)
|
|
706
|
+
os.environ["BROKENPROCESSING_SQLITE_DBNAME"] = (
|
|
707
|
+
"file:brokenprocessing?mode=memory&cache=shared"
|
|
708
|
+
)
|
|
709
|
+
os.environ["BROKENCONVERTING_SQLITE_DBNAME"] = (
|
|
710
|
+
"file:brokenconverting?mode=memory&cache=shared"
|
|
711
|
+
)
|
|
712
|
+
os.environ["BROKENPULLING_SQLITE_DBNAME"] = (
|
|
713
|
+
"file:brokenprocessing?mode=memory&cache=shared"
|
|
714
|
+
)
|
|
715
|
+
os.environ["COMMANDS_SQLITE_DBNAME"] = "file:commands?mode=memory&cache=shared"
|
|
716
|
+
os.environ["RESULTS_SQLITE_DBNAME"] = "file:results?mode=memory&cache=shared"
|
|
717
|
+
|
|
718
|
+
def tearDown(self):
|
|
719
|
+
del os.environ["PERSISTENCE_MODULE"]
|
|
720
|
+
del os.environ[f"{BankAccounts.name.upper()}_SQLITE_DBNAME"]
|
|
721
|
+
del os.environ[f"MY{EmailProcess.name.upper()}_SQLITE_DBNAME"]
|
|
722
|
+
del os.environ[f"{EmailProcess.name.upper()}_SQLITE_DBNAME"]
|
|
723
|
+
del os.environ[f"{EmailProcess.name.upper()}2_SQLITE_DBNAME"]
|
|
724
|
+
del os.environ["BROKENPROCESSING_SQLITE_DBNAME"]
|
|
725
|
+
del os.environ["BROKENCONVERTING_SQLITE_DBNAME"]
|
|
726
|
+
del os.environ["BROKENPULLING_SQLITE_DBNAME"]
|
|
727
|
+
del os.environ["COMMANDS_SQLITE_DBNAME"]
|
|
728
|
+
del os.environ["RESULTS_SQLITE_DBNAME"]
|
|
729
|
+
super().tearDown()
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
class TestMultiThreadedRunnerWithPostgres(TestMultiThreadedRunner):
|
|
733
|
+
def setUp(self):
|
|
734
|
+
super().setUp()
|
|
735
|
+
os.environ["POSTGRES_DBNAME"] = "eventsourcing"
|
|
736
|
+
os.environ["POSTGRES_HOST"] = "127.0.0.1"
|
|
737
|
+
os.environ["POSTGRES_PORT"] = "5432"
|
|
738
|
+
os.environ["POSTGRES_USER"] = "eventsourcing"
|
|
739
|
+
os.environ["POSTGRES_PASSWORD"] = "eventsourcing" # noqa: S105
|
|
740
|
+
|
|
741
|
+
with PostgresDatastore(
|
|
742
|
+
os.getenv("POSTGRES_DBNAME"),
|
|
743
|
+
os.getenv("POSTGRES_HOST"),
|
|
744
|
+
os.getenv("POSTGRES_PORT"),
|
|
745
|
+
os.getenv("POSTGRES_USER"),
|
|
746
|
+
os.getenv("POSTGRES_PASSWORD"),
|
|
747
|
+
) as datastore:
|
|
748
|
+
drop_postgres_table(datastore, f"{BankAccounts.name.lower()}_events")
|
|
749
|
+
drop_postgres_table(datastore, f"{EmailProcess.name.lower()}_events")
|
|
750
|
+
drop_postgres_table(datastore, f"{EmailProcess.name.lower()}_tracking")
|
|
751
|
+
drop_postgres_table(datastore, f"{EmailProcess.name.lower()}2_events")
|
|
752
|
+
drop_postgres_table(datastore, f"{EmailProcess.name.lower()}2_tracking")
|
|
753
|
+
drop_postgres_table(datastore, "brokenprocessing_events")
|
|
754
|
+
drop_postgres_table(datastore, "brokenprocessing_tracking")
|
|
755
|
+
drop_postgres_table(datastore, "brokenconverting_events")
|
|
756
|
+
drop_postgres_table(datastore, "brokenconverting_tracking")
|
|
757
|
+
drop_postgres_table(datastore, "brokenpulling_events")
|
|
758
|
+
drop_postgres_table(datastore, "brokenpulling_tracking")
|
|
759
|
+
drop_postgres_table(datastore, "commands_events")
|
|
760
|
+
drop_postgres_table(datastore, "commands_tracking")
|
|
761
|
+
drop_postgres_table(datastore, "results_events")
|
|
762
|
+
drop_postgres_table(datastore, "results_tracking")
|
|
763
|
+
|
|
764
|
+
os.environ["PERSISTENCE_MODULE"] = "eventsourcing.postgres"
|
|
765
|
+
|
|
766
|
+
def tearDown(self):
|
|
767
|
+
del os.environ["PERSISTENCE_MODULE"]
|
|
768
|
+
del os.environ["POSTGRES_DBNAME"]
|
|
769
|
+
del os.environ["POSTGRES_HOST"]
|
|
770
|
+
del os.environ["POSTGRES_PORT"]
|
|
771
|
+
del os.environ["POSTGRES_USER"]
|
|
772
|
+
del os.environ["POSTGRES_PASSWORD"]
|
|
773
|
+
super().tearDown()
|
|
774
|
+
|
|
775
|
+
def wait_for_runner(self):
|
|
776
|
+
sleep(0.6)
|
|
777
|
+
super().wait_for_runner()
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
class TestNewMultiThreadedRunner(TestMultiThreadedRunner):
|
|
781
|
+
runner_class = NewMultiThreadedRunner
|
|
782
|
+
|
|
783
|
+
class BrokenPulling(EmailProcess):
|
|
784
|
+
def pull_notifications(
|
|
785
|
+
self, leader_name: str, start: int, stop: int | None = None
|
|
786
|
+
) -> Iterator[List[Notification]]:
|
|
787
|
+
msg = "Just testing error handling when pulling is broken"
|
|
788
|
+
raise ProgrammingError(msg)
|
|
789
|
+
|
|
790
|
+
class BrokenConverting(EmailProcess):
|
|
791
|
+
def convert_notifications(
|
|
792
|
+
self, leader_name: str, notifications: Iterable[Notification]
|
|
793
|
+
) -> List[ProcessingJob]:
|
|
794
|
+
msg = "Just testing error handling when converting is broken"
|
|
795
|
+
raise ProgrammingError(msg)
|
|
796
|
+
|
|
797
|
+
# This duplicates test method above.
|
|
798
|
+
def test_ignores_recording_event_if_seen_subsequent(self):
|
|
799
|
+
system = System(pipes=[[BankAccounts, EmailProcess]])
|
|
800
|
+
self.start_runner(system)
|
|
801
|
+
|
|
802
|
+
accounts = self.runner.get(BankAccounts)
|
|
803
|
+
email_process = self.runner.get(EmailProcess)
|
|
804
|
+
|
|
805
|
+
accounts.open_account(
|
|
806
|
+
full_name="Alice",
|
|
807
|
+
email_address="alice@example.com",
|
|
808
|
+
)
|
|
809
|
+
self.wait_for_runner()
|
|
810
|
+
|
|
811
|
+
self.assertEqual(len(email_process.notification_log["1,10"].items), 1)
|
|
812
|
+
|
|
813
|
+
# Reset this to break sequence.
|
|
814
|
+
accounts.previous_max_notification_id -= 1
|
|
815
|
+
|
|
816
|
+
accounts.open_account(
|
|
817
|
+
full_name="Alice",
|
|
818
|
+
email_address="alice@example.com",
|
|
819
|
+
)
|
|
820
|
+
self.wait_for_runner()
|
|
821
|
+
|
|
822
|
+
self.assertEqual(len(email_process.notification_log["1,10"].items), 1)
|
|
823
|
+
|
|
824
|
+
def test_queue_task_done_is_called(self):
|
|
825
|
+
system = System(pipes=[[BankAccounts, EmailProcess]])
|
|
826
|
+
self.start_runner(system)
|
|
827
|
+
|
|
828
|
+
accounts = self.runner.get(BankAccounts)
|
|
829
|
+
email_process1 = self.runner.get(EmailProcess)
|
|
830
|
+
|
|
831
|
+
accounts.open_account(
|
|
832
|
+
full_name="Alice",
|
|
833
|
+
email_address="alice@example.com",
|
|
834
|
+
)
|
|
835
|
+
sleep(0.1)
|
|
836
|
+
self.assertEqual(len(email_process1.notification_log["1,10"].items), 1)
|
|
837
|
+
|
|
838
|
+
for thread in self.runner.all_threads:
|
|
839
|
+
if isinstance(thread, ConvertingThread):
|
|
840
|
+
self.assertEqual(thread.converting_queue.unfinished_tasks, 0)
|
|
841
|
+
self.assertEqual(thread.processing_queue.unfinished_tasks, 0)
|
|
842
|
+
|
|
843
|
+
def test_stop_raises_if_notification_converting_is_broken(self):
|
|
844
|
+
system = System(
|
|
845
|
+
pipes=[
|
|
846
|
+
[
|
|
847
|
+
BankAccounts,
|
|
848
|
+
TestNewMultiThreadedRunner.BrokenConverting,
|
|
849
|
+
],
|
|
850
|
+
]
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
# Create runner.
|
|
854
|
+
self.runner = self.runner_class(system)
|
|
855
|
+
|
|
856
|
+
# Create some notifications.
|
|
857
|
+
accounts = self.runner.get(BankAccounts)
|
|
858
|
+
accounts.open_account(
|
|
859
|
+
full_name="Alice",
|
|
860
|
+
email_address="alice@example.com",
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
# Start runner.
|
|
864
|
+
self.runner.start()
|
|
865
|
+
|
|
866
|
+
# Trigger pulling of notifications.
|
|
867
|
+
accounts = self.runner.get(BankAccounts)
|
|
868
|
+
accounts.open_account(
|
|
869
|
+
full_name="Alice",
|
|
870
|
+
email_address="alice@example.com",
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
# Wait for runner to error.
|
|
874
|
+
self.assertTrue(self.runner.has_errored.wait(timeout=1000))
|
|
875
|
+
|
|
876
|
+
# Check stop() raises exception.
|
|
877
|
+
with self.assertRaises(NotificationConvertingError) as cm:
|
|
878
|
+
self.runner.stop()
|
|
879
|
+
self.assertIn(
|
|
880
|
+
"Just testing error handling when converting is broken",
|
|
881
|
+
cm.exception.args[0],
|
|
882
|
+
)
|
|
883
|
+
self.runner = None
|
|
884
|
+
|
|
885
|
+
def test_stop_raises_if_notification_pulling_is_broken(self):
|
|
886
|
+
system = System(
|
|
887
|
+
pipes=[
|
|
888
|
+
[
|
|
889
|
+
BankAccounts,
|
|
890
|
+
TestNewMultiThreadedRunner.BrokenPulling,
|
|
891
|
+
],
|
|
892
|
+
]
|
|
893
|
+
)
|
|
894
|
+
# Create runner.
|
|
895
|
+
self.runner = self.runner_class(system)
|
|
896
|
+
|
|
897
|
+
# Create some notifications.
|
|
898
|
+
accounts = self.runner.get(BankAccounts)
|
|
899
|
+
accounts.open_account(
|
|
900
|
+
full_name="Alice",
|
|
901
|
+
email_address="alice@example.com",
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
# Start runner.
|
|
905
|
+
self.runner.start()
|
|
906
|
+
|
|
907
|
+
# Trigger pulling of notifications.
|
|
908
|
+
accounts = self.runner.get(BankAccounts)
|
|
909
|
+
accounts.open_account(
|
|
910
|
+
full_name="Alice",
|
|
911
|
+
email_address="alice@example.com",
|
|
912
|
+
)
|
|
913
|
+
|
|
914
|
+
# Wait for runner to error.
|
|
915
|
+
self.assertTrue(self.runner.has_errored.wait(timeout=1))
|
|
916
|
+
|
|
917
|
+
# Check stop() raises exception.
|
|
918
|
+
with self.assertRaises(NotificationPullingError) as cm:
|
|
919
|
+
self.runner.stop()
|
|
920
|
+
self.assertIn(
|
|
921
|
+
"Just testing error handling when pulling is broken",
|
|
922
|
+
cm.exception.args[0],
|
|
923
|
+
)
|
|
924
|
+
self.runner = None
|
|
925
|
+
|
|
926
|
+
def wait_for_runner(self):
|
|
927
|
+
sleep(0.1)
|
|
928
|
+
try:
|
|
929
|
+
self.runner.reraise_thread_errors()
|
|
930
|
+
except Exception as e:
|
|
931
|
+
self.runner = None
|
|
932
|
+
raise Exception("Runner errored: " + str(e)) from e
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
del RunnerTestCase
|