orionis 0.405.0__py3-none-any.whl → 0.407.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.
- orionis/console/base/command.py +57 -50
- orionis/console/base/contracts/command.py +68 -0
- orionis/console/dynamic/contracts/progress_bar.py +3 -3
- orionis/console/dynamic/progress_bar.py +8 -8
- orionis/console/output/console.py +8 -2
- orionis/console/output/contracts/console.py +1 -1
- orionis/container/container.py +2 -2
- orionis/container/context/scope.py +4 -1
- orionis/container/contracts/service_provider.py +2 -2
- orionis/container/entities/binding.py +31 -44
- orionis/container/enums/lifetimes.py +22 -1
- orionis/container/facades/facade.py +1 -2
- orionis/container/providers/service_provider.py +2 -2
- orionis/foundation/application.py +542 -248
- orionis/foundation/config/app/entities/app.py +107 -90
- orionis/foundation/config/auth/entities/auth.py +4 -33
- orionis/foundation/config/cache/entities/cache.py +18 -41
- orionis/foundation/config/cache/entities/file.py +8 -35
- orionis/foundation/config/cache/entities/stores.py +17 -38
- orionis/foundation/config/cors/entities/cors.py +41 -54
- orionis/foundation/config/database/entities/connections.py +40 -56
- orionis/foundation/config/database/entities/database.py +11 -38
- orionis/foundation/config/database/entities/mysql.py +48 -76
- orionis/foundation/config/database/entities/oracle.py +30 -57
- orionis/foundation/config/database/entities/pgsql.py +45 -61
- orionis/foundation/config/database/entities/sqlite.py +26 -53
- orionis/foundation/config/filesystems/entitites/aws.py +28 -49
- orionis/foundation/config/filesystems/entitites/disks.py +27 -47
- orionis/foundation/config/filesystems/entitites/filesystems.py +15 -37
- orionis/foundation/config/filesystems/entitites/local.py +9 -35
- orionis/foundation/config/filesystems/entitites/public.py +14 -41
- orionis/foundation/config/logging/entities/channels.py +56 -86
- orionis/foundation/config/logging/entities/chunked.py +9 -9
- orionis/foundation/config/logging/entities/daily.py +8 -8
- orionis/foundation/config/logging/entities/hourly.py +6 -6
- orionis/foundation/config/logging/entities/logging.py +12 -18
- orionis/foundation/config/logging/entities/monthly.py +7 -7
- orionis/foundation/config/logging/entities/stack.py +5 -5
- orionis/foundation/config/logging/entities/weekly.py +6 -6
- orionis/foundation/config/mail/entities/file.py +9 -36
- orionis/foundation/config/mail/entities/mail.py +22 -40
- orionis/foundation/config/mail/entities/mailers.py +29 -44
- orionis/foundation/config/mail/entities/smtp.py +47 -48
- orionis/foundation/config/queue/entities/brokers.py +19 -41
- orionis/foundation/config/queue/entities/database.py +24 -46
- orionis/foundation/config/queue/entities/queue.py +28 -40
- orionis/foundation/config/roots/paths.py +272 -468
- orionis/foundation/config/session/entities/session.py +23 -53
- orionis/foundation/config/startup.py +165 -135
- orionis/foundation/config/testing/entities/testing.py +137 -122
- orionis/foundation/config/testing/enums/__init__.py +6 -2
- orionis/foundation/config/testing/enums/drivers.py +16 -0
- orionis/foundation/config/testing/enums/verbosity.py +18 -0
- orionis/foundation/contracts/application.py +152 -362
- orionis/foundation/providers/console_provider.py +24 -2
- orionis/foundation/providers/dumper_provider.py +24 -2
- orionis/foundation/providers/logger_provider.py +24 -2
- orionis/foundation/providers/path_resolver_provider.py +25 -2
- orionis/foundation/providers/progress_bar_provider.py +24 -2
- orionis/foundation/providers/testing_provider.py +39 -0
- orionis/foundation/providers/workers_provider.py +24 -2
- orionis/metadata/framework.py +1 -1
- orionis/services/asynchrony/contracts/coroutines.py +13 -5
- orionis/services/asynchrony/coroutines.py +33 -29
- orionis/services/asynchrony/exceptions/exception.py +9 -1
- orionis/services/environment/core/dot_env.py +46 -34
- orionis/services/environment/enums/__init__.py +0 -0
- orionis/services/environment/enums/cast_type.py +42 -0
- orionis/services/environment/helpers/functions.py +1 -2
- orionis/services/environment/key/__init__.py +0 -0
- orionis/services/environment/key/key_generator.py +37 -0
- orionis/services/environment/serializer/__init__.py +0 -0
- orionis/services/environment/serializer/values.py +21 -0
- orionis/services/environment/validators/__init__.py +0 -0
- orionis/services/environment/validators/key_name.py +46 -0
- orionis/services/environment/validators/types.py +45 -0
- orionis/services/system/contracts/imports.py +38 -18
- orionis/services/system/contracts/workers.py +29 -12
- orionis/services/system/imports.py +65 -25
- orionis/services/system/runtime/imports.py +18 -9
- orionis/services/system/workers.py +49 -16
- orionis/support/entities/__init__.py +0 -0
- orionis/support/entities/base.py +104 -0
- orionis/support/facades/testing.py +15 -0
- orionis/support/facades/workers.py +1 -1
- orionis/test/cases/asynchronous.py +0 -11
- orionis/test/cases/synchronous.py +0 -9
- orionis/test/contracts/dumper.py +11 -4
- orionis/test/contracts/kernel.py +5 -110
- orionis/test/contracts/logs.py +27 -65
- orionis/test/contracts/printer.py +16 -128
- orionis/test/contracts/test_result.py +100 -0
- orionis/test/contracts/unit_test.py +87 -150
- orionis/test/core/unit_test.py +608 -554
- orionis/test/entities/result.py +22 -2
- orionis/test/enums/__init__.py +0 -2
- orionis/test/enums/status.py +14 -9
- orionis/test/exceptions/config.py +9 -1
- orionis/test/exceptions/failure.py +34 -11
- orionis/test/exceptions/persistence.py +10 -2
- orionis/test/exceptions/runtime.py +9 -1
- orionis/test/exceptions/value.py +13 -1
- orionis/test/kernel.py +87 -289
- orionis/test/output/dumper.py +83 -18
- orionis/test/output/printer.py +399 -156
- orionis/test/records/logs.py +203 -82
- orionis/test/validators/__init__.py +33 -0
- orionis/test/validators/base_path.py +45 -0
- orionis/test/validators/execution_mode.py +45 -0
- orionis/test/validators/fail_fast.py +37 -0
- orionis/test/validators/folder_path.py +34 -0
- orionis/test/validators/module_name.py +31 -0
- orionis/test/validators/name_pattern.py +40 -0
- orionis/test/validators/pattern.py +36 -0
- orionis/test/validators/persistent.py +42 -0
- orionis/test/validators/persistent_driver.py +43 -0
- orionis/test/validators/print_result.py +37 -0
- orionis/test/validators/tags.py +37 -0
- orionis/test/validators/throw_exception.py +39 -0
- orionis/test/validators/verbosity.py +37 -0
- orionis/test/validators/web_report.py +35 -0
- orionis/test/validators/workers.py +31 -0
- orionis/test/view/render.py +48 -54
- {orionis-0.405.0.dist-info → orionis-0.407.0.dist-info}/METADATA +1 -1
- {orionis-0.405.0.dist-info → orionis-0.407.0.dist-info}/RECORD +170 -112
- tests/container/__init__.py +0 -0
- tests/container/context/__init__.py +0 -0
- tests/container/context/test_manager.py +27 -0
- tests/container/context/test_scope.py +23 -0
- tests/container/entities/__init__.py +0 -0
- tests/container/entities/test_binding.py +133 -0
- tests/container/enums/__init__.py +0 -0
- tests/container/enums/test_lifetimes.py +63 -0
- tests/container/facades/__init__.py +0 -0
- tests/container/facades/test_facade.py +61 -0
- tests/container/mocks/__init__.py +0 -0
- tests/container/mocks/mock_complex_classes.py +482 -0
- tests/container/mocks/mock_simple_classes.py +32 -0
- tests/container/providers/__init__.py +0 -0
- tests/container/providers/test_providers.py +48 -0
- tests/container/resolver/__init__.py +0 -0
- tests/container/resolver/test_resolver.py +55 -0
- tests/container/test_container.py +254 -0
- tests/container/test_singleton.py +98 -0
- tests/container/test_thread_safety.py +217 -0
- tests/container/validators/__init__.py +0 -0
- tests/container/validators/test_implements.py +140 -0
- tests/container/validators/test_is_abstract_class.py +99 -0
- tests/container/validators/test_is_callable.py +73 -0
- tests/container/validators/test_is_concrete_class.py +97 -0
- tests/container/validators/test_is_instance.py +105 -0
- tests/container/validators/test_is_not_subclass.py +117 -0
- tests/container/validators/test_is_subclass.py +115 -0
- tests/container/validators/test_is_valid_alias.py +113 -0
- tests/container/validators/test_lifetime.py +75 -0
- tests/example/test_example.py +2 -2
- tests/foundation/config/testing/test_foundation_config_testing.py +1 -1
- tests/metadata/test_metadata_framework.py +89 -24
- tests/metadata/test_metadata_package.py +55 -10
- tests/services/asynchrony/test_services_asynchrony_coroutine.py +52 -7
- tests/services/system/test_services_system_imports.py +119 -16
- tests/services/system/test_services_system_workers.py +71 -30
- tests/testing/test_testing_result.py +117 -117
- tests/testing/test_testing_unit.py +209 -209
- orionis/foundation/config/base.py +0 -112
- orionis/test/arguments/parser.py +0 -187
- orionis/test/contracts/parser.py +0 -43
- orionis/test/entities/arguments.py +0 -38
- orionis/test/enums/execution_mode.py +0 -16
- /orionis/{test/arguments → console/base/contracts}/__init__.py +0 -0
- /orionis/foundation/config/testing/enums/{test_mode.py → mode.py} +0 -0
- {orionis-0.405.0.dist-info → orionis-0.407.0.dist-info}/WHEEL +0 -0
- {orionis-0.405.0.dist-info → orionis-0.407.0.dist-info}/licenses/LICENCE +0 -0
- {orionis-0.405.0.dist-info → orionis-0.407.0.dist-info}/top_level.txt +0 -0
- {orionis-0.405.0.dist-info → orionis-0.407.0.dist-info}/zip-safe +0 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
from orionis.container.container import Container
|
|
2
|
+
from orionis.container.facades.facade import Facade
|
|
3
|
+
from orionis.foundation.application import Application
|
|
4
|
+
from orionis.test.cases.asynchronous import AsyncTestCase
|
|
5
|
+
from tests.container.mocks.mock_simple_classes import Car, ICar
|
|
6
|
+
|
|
7
|
+
class TestContainer(AsyncTestCase):
|
|
8
|
+
"""Test suite for the Container class functionality."""
|
|
9
|
+
|
|
10
|
+
async def testTransientRegistration(self) -> None:
|
|
11
|
+
"""
|
|
12
|
+
Tests the transient registration of a service in the container.
|
|
13
|
+
It verifies that:
|
|
14
|
+
1. The container.transient() method correctly registers a type (ICar) to be resolved as another type (Car)
|
|
15
|
+
2. The resolved instances are of the correct type (Car)
|
|
16
|
+
3. Each resolution returns a new instance (instance1 is not the same object as instance2)
|
|
17
|
+
"""
|
|
18
|
+
container = Container()
|
|
19
|
+
container.transient(ICar, Car)
|
|
20
|
+
instance1 = container.make(ICar)
|
|
21
|
+
instance2 = container.make(ICar)
|
|
22
|
+
|
|
23
|
+
self.assertIsInstance(instance1, Car)
|
|
24
|
+
self.assertIsInstance(instance2, Car)
|
|
25
|
+
self.assertIsNot(instance1, instance2)
|
|
26
|
+
|
|
27
|
+
container.drop(abstract=ICar)
|
|
28
|
+
|
|
29
|
+
async def testSingletonRegistration(self) -> None:
|
|
30
|
+
"""
|
|
31
|
+
Test that verifies singleton registration and resolution from the container.
|
|
32
|
+
This test ensures that:
|
|
33
|
+
1. A class can be registered as a singleton implementation of an interface
|
|
34
|
+
2. The container correctly returns an instance of the registered implementation
|
|
35
|
+
3. Multiple requests for the same interface return the same instance
|
|
36
|
+
The test binds the ICar interface to the Car implementation as a singleton,
|
|
37
|
+
then verifies that two requests for ICar:
|
|
38
|
+
- Both return instances of Car
|
|
39
|
+
- Return the exact same instance (object identity)
|
|
40
|
+
"""
|
|
41
|
+
container = Container()
|
|
42
|
+
container.singleton(ICar, Car)
|
|
43
|
+
instance1 = container.make(ICar)
|
|
44
|
+
instance2 = container.make(ICar)
|
|
45
|
+
|
|
46
|
+
self.assertIsInstance(instance1, Car)
|
|
47
|
+
self.assertIsInstance(instance2, Car)
|
|
48
|
+
self.assertIs(instance1, instance2)
|
|
49
|
+
|
|
50
|
+
container.drop(abstract=ICar)
|
|
51
|
+
|
|
52
|
+
async def testScopedRegistration(self) -> None:
|
|
53
|
+
"""
|
|
54
|
+
Tests the scoped registration functionality of the container.
|
|
55
|
+
This test verifies that:
|
|
56
|
+
1. Within a single context, scoped registrations return the same instance
|
|
57
|
+
when the same interface is requested multiple times
|
|
58
|
+
2. Different contexts produce different instances of the same registration
|
|
59
|
+
The test creates two separate contexts and confirms that instances obtained
|
|
60
|
+
within a single context are identical, but instances from different contexts
|
|
61
|
+
are distinct.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
container = Container()
|
|
65
|
+
with container.createContext():
|
|
66
|
+
container.scoped(ICar, Car)
|
|
67
|
+
instance1 = container.make(ICar)
|
|
68
|
+
instance2 = container.make(ICar)
|
|
69
|
+
|
|
70
|
+
self.assertIsInstance(instance1, Car)
|
|
71
|
+
self.assertIsInstance(instance2, Car)
|
|
72
|
+
self.assertIs(instance1, instance2)
|
|
73
|
+
|
|
74
|
+
with container.createContext():
|
|
75
|
+
container.scoped(ICar, Car)
|
|
76
|
+
instance3 = container.make(ICar)
|
|
77
|
+
self.assertIsNot(instance1, instance3)
|
|
78
|
+
|
|
79
|
+
container.drop(abstract=ICar)
|
|
80
|
+
|
|
81
|
+
async def testInstanceRegistration(self) -> None:
|
|
82
|
+
"""
|
|
83
|
+
Test case for instance registration in the container.
|
|
84
|
+
This test ensures that when an instance is registered to a service in the container,
|
|
85
|
+
the container returns exactly the same instance when resolving that service.
|
|
86
|
+
The test:
|
|
87
|
+
1. Creates an instance of Car
|
|
88
|
+
2. Registers this instance to the ICar interface in the container
|
|
89
|
+
3. Resolves the ICar interface from the container
|
|
90
|
+
4. Asserts that the resolved instance is exactly the same object as the one registered
|
|
91
|
+
"""
|
|
92
|
+
car_instance = Car()
|
|
93
|
+
container = Container()
|
|
94
|
+
container.instance(ICar, car_instance)
|
|
95
|
+
resolved = container.make(ICar)
|
|
96
|
+
|
|
97
|
+
self.assertIs(resolved, car_instance)
|
|
98
|
+
|
|
99
|
+
container.drop(abstract=ICar)
|
|
100
|
+
|
|
101
|
+
async def testCallableRegistration(self) -> None:
|
|
102
|
+
"""
|
|
103
|
+
Test that callables can be registered and resolved from the container.
|
|
104
|
+
This test verifies that:
|
|
105
|
+
1. Functions can be registered in the container using the callable() method
|
|
106
|
+
2. Registered functions can be resolved and executed using the make() method
|
|
107
|
+
3. Arguments can be passed to the resolved functions as positional arguments
|
|
108
|
+
4. Arguments can be passed to the resolved functions as keyword arguments
|
|
109
|
+
The test registers two simple functions (add and multiply) and verifies
|
|
110
|
+
they can be correctly resolved and executed with the expected results.
|
|
111
|
+
"""
|
|
112
|
+
def add(a: int, b: int) -> int:
|
|
113
|
+
return a + b
|
|
114
|
+
|
|
115
|
+
def multiply(a: int, b: int) -> int:
|
|
116
|
+
return a * b
|
|
117
|
+
|
|
118
|
+
container = Container()
|
|
119
|
+
container.callable('add', add)
|
|
120
|
+
container.callable('multiply', multiply)
|
|
121
|
+
|
|
122
|
+
self.assertEqual(container.make('add', 1, 2), 3)
|
|
123
|
+
self.assertEqual(container.make('multiply', 3, 4), 12)
|
|
124
|
+
self.assertEqual(container.make('add', a=5, b=7), 12)
|
|
125
|
+
|
|
126
|
+
container.drop(alias='add')
|
|
127
|
+
container.drop(alias='multiply')
|
|
128
|
+
|
|
129
|
+
async def testTransientFacade(self) -> None:
|
|
130
|
+
"""
|
|
131
|
+
Test case for transient instance resolution using a Facade pattern.
|
|
132
|
+
This test validates that:
|
|
133
|
+
1. The container can register a transient binding between an interface and a class
|
|
134
|
+
2. The Facade pattern correctly resolves instances of the registered interface
|
|
135
|
+
3. Multiple calls to the Facade's resolve() method return different instances
|
|
136
|
+
when the binding is transient
|
|
137
|
+
The test creates a CarFacade that accesses ICar interface which is bound to the Car
|
|
138
|
+
implementation in a transient manner, ensuring each resolution yields a new instance.
|
|
139
|
+
"""
|
|
140
|
+
container = Application()
|
|
141
|
+
container.transient(ICar, Car)
|
|
142
|
+
|
|
143
|
+
class CarFacade(Facade):
|
|
144
|
+
@classmethod
|
|
145
|
+
def getFacadeAccessor(cls):
|
|
146
|
+
return ICar
|
|
147
|
+
|
|
148
|
+
instance1 = CarFacade.resolve()
|
|
149
|
+
instance2 = CarFacade.resolve()
|
|
150
|
+
|
|
151
|
+
self.assertIsInstance(instance1, Car)
|
|
152
|
+
self.assertIsInstance(instance2, Car)
|
|
153
|
+
self.assertIsNot(instance1, instance2)
|
|
154
|
+
|
|
155
|
+
container.drop(abstract=ICar)
|
|
156
|
+
|
|
157
|
+
async def testSingletonFacade(self) -> None:
|
|
158
|
+
"""
|
|
159
|
+
Tests if the Facade pattern correctly resolves singleton instances.
|
|
160
|
+
This test verifies that:
|
|
161
|
+
1. A singleton binding can be registered in the container
|
|
162
|
+
2. A Facade class can be created to access this binding
|
|
163
|
+
3. Multiple resolutions through the Facade return the same instance
|
|
164
|
+
4. The resolved instances are of the correct type (Car)
|
|
165
|
+
The test demonstrates how Facades act as static proxies to container bindings,
|
|
166
|
+
maintaining the singleton behavior defined in the container.
|
|
167
|
+
"""
|
|
168
|
+
container = Application()
|
|
169
|
+
container.singleton(ICar, Car)
|
|
170
|
+
|
|
171
|
+
class CarFacade(Facade):
|
|
172
|
+
@classmethod
|
|
173
|
+
def getFacadeAccessor(cls):
|
|
174
|
+
return ICar
|
|
175
|
+
|
|
176
|
+
instance1 = CarFacade.resolve()
|
|
177
|
+
instance2 = CarFacade.resolve()
|
|
178
|
+
|
|
179
|
+
self.assertIsInstance(instance1, Car)
|
|
180
|
+
self.assertIsInstance(instance2, Car)
|
|
181
|
+
self.assertIs(instance1, instance2)
|
|
182
|
+
|
|
183
|
+
container.drop(abstract=ICar)
|
|
184
|
+
|
|
185
|
+
async def testScopedFacade(self) -> None:
|
|
186
|
+
"""
|
|
187
|
+
Tests the functionality of a Facade accessing a scoped service within a container context.
|
|
188
|
+
This test verifies that:
|
|
189
|
+
1. The Facade can properly resolve a scoped service
|
|
190
|
+
2. Multiple resolves within the same scope return the same instance
|
|
191
|
+
3. The resolved instance is of the correct type
|
|
192
|
+
The test creates a scoped registration for ICar interface to Car implementation,
|
|
193
|
+
defines a CarFacade class that accesses ICar, and then confirms that:
|
|
194
|
+
- Resolved instances are of Car type
|
|
195
|
+
- Multiple resolutions return the same instance (scoped behavior)
|
|
196
|
+
"""
|
|
197
|
+
container = Application()
|
|
198
|
+
with container.createContext():
|
|
199
|
+
container.scoped(ICar, Car)
|
|
200
|
+
|
|
201
|
+
class CarFacade(Facade):
|
|
202
|
+
@classmethod
|
|
203
|
+
def getFacadeAccessor(cls):
|
|
204
|
+
return ICar
|
|
205
|
+
|
|
206
|
+
instance1 = CarFacade.resolve()
|
|
207
|
+
instance2 = CarFacade.resolve()
|
|
208
|
+
|
|
209
|
+
self.assertIsInstance(instance1, Car)
|
|
210
|
+
self.assertIsInstance(instance2, Car)
|
|
211
|
+
self.assertIs(instance1, instance2)
|
|
212
|
+
|
|
213
|
+
container.drop(abstract=ICar)
|
|
214
|
+
|
|
215
|
+
async def testResolvingUnregisteredType(self) -> None:
|
|
216
|
+
"""
|
|
217
|
+
Tests that attempting to resolve an unregistered type from the container raises an exception.
|
|
218
|
+
This test ensures that the container correctly validates that a type is registered before attempting to resolve it.
|
|
219
|
+
|
|
220
|
+
Raises:
|
|
221
|
+
Exception: Expected to be raised when attempting to resolve an unregistered type.
|
|
222
|
+
"""
|
|
223
|
+
container = Container()
|
|
224
|
+
with self.assertRaises(Exception):
|
|
225
|
+
container.make('ICar')
|
|
226
|
+
|
|
227
|
+
async def testOverridingRegistration(self) -> None:
|
|
228
|
+
"""
|
|
229
|
+
Tests the ability of the container to override existing registrations.
|
|
230
|
+
This test verifies that:
|
|
231
|
+
1. When a class is registered as a singleton for an interface
|
|
232
|
+
2. And later a different class is registered for the same interface
|
|
233
|
+
3. The container returns the new class when resolving the interface
|
|
234
|
+
4. The new instance is different from the previous instance
|
|
235
|
+
This demonstrates the container's support for overriding service implementations.
|
|
236
|
+
"""
|
|
237
|
+
class SportsCar(Car):
|
|
238
|
+
def start(self):
|
|
239
|
+
return f"{self.brand} {self.model} is starting."
|
|
240
|
+
def stop(self):
|
|
241
|
+
return f"{self.brand} {self.model} is stopping."
|
|
242
|
+
|
|
243
|
+
container = Container()
|
|
244
|
+
container.singleton(ICar, Car)
|
|
245
|
+
first = container.make(ICar)
|
|
246
|
+
self.assertIsInstance(first, Car)
|
|
247
|
+
self.assertNotIsInstance(first, SportsCar)
|
|
248
|
+
|
|
249
|
+
container.singleton(ICar, SportsCar)
|
|
250
|
+
second = container.make(ICar)
|
|
251
|
+
self.assertIsInstance(second, SportsCar)
|
|
252
|
+
self.assertIsNot(first, second)
|
|
253
|
+
|
|
254
|
+
container.drop(abstract=ICar)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import time
|
|
3
|
+
from orionis.foundation.application import Application as Orionis
|
|
4
|
+
from orionis.container.container import Container
|
|
5
|
+
from orionis.test.cases.asynchronous import AsyncTestCase
|
|
6
|
+
|
|
7
|
+
class TestSingleton(AsyncTestCase):
|
|
8
|
+
"""Test suite for singleton pattern implementation."""
|
|
9
|
+
|
|
10
|
+
async def testSingletonBasicFunctionality(self) -> None:
|
|
11
|
+
"""
|
|
12
|
+
Test basic singleton functionality.
|
|
13
|
+
|
|
14
|
+
This test verifies that:
|
|
15
|
+
1. Multiple Container instances are the same object
|
|
16
|
+
2. Multiple Orionis instances are the same object
|
|
17
|
+
3. Container and Orionis are different singletons
|
|
18
|
+
"""
|
|
19
|
+
# Create multiple instances
|
|
20
|
+
container1 = Container()
|
|
21
|
+
container2 = Container()
|
|
22
|
+
orionis1 = Orionis()
|
|
23
|
+
orionis2 = Orionis()
|
|
24
|
+
|
|
25
|
+
# Test that Container instances are the same
|
|
26
|
+
self.assertIs(container1, container2)
|
|
27
|
+
self.assertEqual(id(container1), id(container2))
|
|
28
|
+
|
|
29
|
+
# Test that Orionis instances are the same
|
|
30
|
+
self.assertIs(orionis1, orionis2)
|
|
31
|
+
self.assertEqual(id(orionis1), id(orionis2))
|
|
32
|
+
|
|
33
|
+
# Test that Container and Orionis are different singletons
|
|
34
|
+
self.assertIsNot(container1, orionis1)
|
|
35
|
+
|
|
36
|
+
async def testSingletonThreadingSafety(self) -> None:
|
|
37
|
+
"""
|
|
38
|
+
Test singleton in multi-threaded environment.
|
|
39
|
+
|
|
40
|
+
This test verifies that singleton pattern works correctly
|
|
41
|
+
when instances are created from multiple threads simultaneously.
|
|
42
|
+
"""
|
|
43
|
+
container_instances = []
|
|
44
|
+
orionis_instances = []
|
|
45
|
+
|
|
46
|
+
def create_container():
|
|
47
|
+
"""Create container instance in thread."""
|
|
48
|
+
time.sleep(0.01) # Small delay to increase chance of race condition
|
|
49
|
+
container_instances.append(Container())
|
|
50
|
+
|
|
51
|
+
def create_orionis():
|
|
52
|
+
"""Create orionis instance in thread."""
|
|
53
|
+
time.sleep(0.01) # Small delay to increase chance of race condition
|
|
54
|
+
orionis_instances.append(Orionis())
|
|
55
|
+
|
|
56
|
+
# Create multiple threads
|
|
57
|
+
threads = []
|
|
58
|
+
for i in range(10):
|
|
59
|
+
t1 = threading.Thread(target=create_container)
|
|
60
|
+
t2 = threading.Thread(target=create_orionis)
|
|
61
|
+
threads.extend([t1, t2])
|
|
62
|
+
|
|
63
|
+
# Start all threads
|
|
64
|
+
for t in threads:
|
|
65
|
+
t.start()
|
|
66
|
+
|
|
67
|
+
# Wait for all threads to complete
|
|
68
|
+
for t in threads:
|
|
69
|
+
t.join()
|
|
70
|
+
|
|
71
|
+
# Check that all instances are the same
|
|
72
|
+
container_ids = [id(c) for c in container_instances]
|
|
73
|
+
orionis_ids = [id(o) for o in orionis_instances]
|
|
74
|
+
|
|
75
|
+
self.assertEqual(len(set(container_ids)), 1)
|
|
76
|
+
self.assertEqual(len(set(orionis_ids)), 1)
|
|
77
|
+
self.assertEqual(len(container_instances), 10)
|
|
78
|
+
self.assertEqual(len(orionis_instances), 10)
|
|
79
|
+
|
|
80
|
+
async def testInheritanceSeparation(self) -> None:
|
|
81
|
+
"""
|
|
82
|
+
Test that Container and Orionis maintain separate singleton instances.
|
|
83
|
+
|
|
84
|
+
This test verifies that different singleton classes maintain
|
|
85
|
+
their own separate instances while both implementing singleton pattern.
|
|
86
|
+
"""
|
|
87
|
+
container = Container()
|
|
88
|
+
orionis = Orionis()
|
|
89
|
+
|
|
90
|
+
# Add some data to each to verify they're separate
|
|
91
|
+
container.callable("test_container", lambda: "container_value")
|
|
92
|
+
|
|
93
|
+
# Check that they're different instances but both are singletons
|
|
94
|
+
self.assertEqual(type(container).__name__, "Container")
|
|
95
|
+
self.assertEqual(type(orionis).__name__, "Application")
|
|
96
|
+
self.assertIsNot(container, orionis)
|
|
97
|
+
self.assertTrue(container.bound('test_container'))
|
|
98
|
+
self.assertFalse(orionis.bound('test_container'))
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import time
|
|
3
|
+
import random
|
|
4
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
5
|
+
from orionis.foundation.application import Application as Orionis
|
|
6
|
+
from orionis.container.container import Container
|
|
7
|
+
from orionis.test.cases.asynchronous import AsyncTestCase
|
|
8
|
+
|
|
9
|
+
class TestThreadSafety(AsyncTestCase):
|
|
10
|
+
|
|
11
|
+
async def testStressSingleton(self) -> None:
|
|
12
|
+
"""
|
|
13
|
+
Test singleton under extreme concurrent conditions.
|
|
14
|
+
|
|
15
|
+
This test creates a large number of threads that simultaneously
|
|
16
|
+
attempt to create singleton instances to verify thread-safety.
|
|
17
|
+
"""
|
|
18
|
+
container_instances = []
|
|
19
|
+
orionis_instances = []
|
|
20
|
+
|
|
21
|
+
def create_container_with_delay():
|
|
22
|
+
"""Create container with random delay to simulate real conditions."""
|
|
23
|
+
# Random delay to increase chance of race conditions
|
|
24
|
+
time.sleep(random.uniform(0.001, 0.01))
|
|
25
|
+
container = Container()
|
|
26
|
+
container_instances.append(container)
|
|
27
|
+
return id(container)
|
|
28
|
+
|
|
29
|
+
def create_orionis_with_delay():
|
|
30
|
+
"""Create orionis with random delay to simulate real conditions."""
|
|
31
|
+
# Random delay to increase chance of race conditions
|
|
32
|
+
time.sleep(random.uniform(0.001, 0.01))
|
|
33
|
+
orionis = Orionis()
|
|
34
|
+
orionis_instances.append(orionis)
|
|
35
|
+
return id(orionis)
|
|
36
|
+
|
|
37
|
+
# Create a large number of threads
|
|
38
|
+
num_threads = 100
|
|
39
|
+
|
|
40
|
+
with ThreadPoolExecutor(max_workers=50) as executor:
|
|
41
|
+
# Submit container creation tasks
|
|
42
|
+
container_futures = [
|
|
43
|
+
executor.submit(create_container_with_delay)
|
|
44
|
+
for _ in range(num_threads)
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
# Submit orionis creation tasks
|
|
48
|
+
orionis_futures = [
|
|
49
|
+
executor.submit(create_orionis_with_delay)
|
|
50
|
+
for _ in range(num_threads)
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
# Wait for all tasks to complete
|
|
54
|
+
container_ids = [future.result() for future in as_completed(container_futures)]
|
|
55
|
+
orionis_ids = [future.result() for future in as_completed(orionis_futures)]
|
|
56
|
+
|
|
57
|
+
# Verify all instances are the same
|
|
58
|
+
unique_container_ids = set(container_ids)
|
|
59
|
+
unique_orionis_ids = set(orionis_ids)
|
|
60
|
+
|
|
61
|
+
self.assertEqual(len(container_instances), num_threads)
|
|
62
|
+
self.assertEqual(len(unique_container_ids), 1)
|
|
63
|
+
self.assertEqual(len(orionis_instances), num_threads)
|
|
64
|
+
self.assertEqual(len(unique_orionis_ids), 1)
|
|
65
|
+
|
|
66
|
+
# Verify that Container and Orionis are different singletons
|
|
67
|
+
container_id = list(unique_container_ids)[0] if unique_container_ids else None
|
|
68
|
+
orionis_id = list(unique_orionis_ids)[0] if unique_orionis_ids else None
|
|
69
|
+
|
|
70
|
+
self.assertNotEqual(container_id, orionis_id)
|
|
71
|
+
|
|
72
|
+
async def testRapidAccess(self) -> None:
|
|
73
|
+
"""
|
|
74
|
+
Test rapid concurrent access to existing singleton instances.
|
|
75
|
+
|
|
76
|
+
This test verifies that rapid concurrent access to already
|
|
77
|
+
created singleton instances maintains consistency.
|
|
78
|
+
"""
|
|
79
|
+
# Create initial instances
|
|
80
|
+
initial_container = Container()
|
|
81
|
+
initial_orionis = Orionis()
|
|
82
|
+
|
|
83
|
+
container_ids = []
|
|
84
|
+
orionis_ids = []
|
|
85
|
+
|
|
86
|
+
def rapid_container_access():
|
|
87
|
+
"""Rapidly access container singleton."""
|
|
88
|
+
for _ in range(100):
|
|
89
|
+
container = Container()
|
|
90
|
+
container_ids.append(id(container))
|
|
91
|
+
|
|
92
|
+
def rapid_orionis_access():
|
|
93
|
+
"""Rapidly access orionis singleton."""
|
|
94
|
+
for _ in range(100):
|
|
95
|
+
orionis = Orionis()
|
|
96
|
+
orionis_ids.append(id(orionis))
|
|
97
|
+
|
|
98
|
+
# Create threads for rapid access
|
|
99
|
+
threads = []
|
|
100
|
+
for _ in range(20):
|
|
101
|
+
t1 = threading.Thread(target=rapid_container_access)
|
|
102
|
+
t2 = threading.Thread(target=rapid_orionis_access)
|
|
103
|
+
threads.extend([t1, t2])
|
|
104
|
+
|
|
105
|
+
# Start all threads simultaneously
|
|
106
|
+
start_time = time.time()
|
|
107
|
+
for t in threads:
|
|
108
|
+
t.start()
|
|
109
|
+
|
|
110
|
+
# Wait for all threads to complete
|
|
111
|
+
for t in threads:
|
|
112
|
+
t.join()
|
|
113
|
+
|
|
114
|
+
end_time = time.time()
|
|
115
|
+
|
|
116
|
+
# Verify consistency
|
|
117
|
+
unique_container_ids = set(container_ids)
|
|
118
|
+
unique_orionis_ids = set(orionis_ids)
|
|
119
|
+
|
|
120
|
+
self.assertEqual(len(container_ids), 20 * 100) # 20 threads * 100 accesses each
|
|
121
|
+
self.assertEqual(len(unique_container_ids), 1)
|
|
122
|
+
self.assertEqual(len(orionis_ids), 20 * 100)
|
|
123
|
+
self.assertEqual(len(unique_orionis_ids), 1)
|
|
124
|
+
|
|
125
|
+
# Verify performance is reasonable (should complete quickly)
|
|
126
|
+
self.assertLess(end_time - start_time, 10.0) # Should complete in less than 10 seconds
|
|
127
|
+
|
|
128
|
+
async def testMixedOperations(self) -> None:
|
|
129
|
+
"""
|
|
130
|
+
Test mixed read/write operations on singletons.
|
|
131
|
+
|
|
132
|
+
This test verifies that concurrent read/write operations
|
|
133
|
+
maintain singleton consistency and data integrity.
|
|
134
|
+
"""
|
|
135
|
+
errors = []
|
|
136
|
+
|
|
137
|
+
def mixed_operations():
|
|
138
|
+
"""Perform mixed operations on containers."""
|
|
139
|
+
try:
|
|
140
|
+
# Get instances
|
|
141
|
+
container = Container()
|
|
142
|
+
orionis = Orionis()
|
|
143
|
+
|
|
144
|
+
# Perform some operations
|
|
145
|
+
test_key = f"test_func_{threading.current_thread().ident}"
|
|
146
|
+
container.callable(test_key, lambda: "test_value")
|
|
147
|
+
|
|
148
|
+
# Verify the same instance
|
|
149
|
+
container2 = Container()
|
|
150
|
+
orionis2 = Orionis()
|
|
151
|
+
|
|
152
|
+
if container is not container2:
|
|
153
|
+
errors.append("Container singleton violated")
|
|
154
|
+
|
|
155
|
+
if orionis is not orionis2:
|
|
156
|
+
errors.append("Orionis singleton violated")
|
|
157
|
+
|
|
158
|
+
# Check bindings consistency
|
|
159
|
+
if not container2.bound(test_key):
|
|
160
|
+
errors.append("Binding not consistent across instances")
|
|
161
|
+
|
|
162
|
+
except Exception as e:
|
|
163
|
+
errors.append(f"Exception in mixed operations: {e}")
|
|
164
|
+
|
|
165
|
+
# Run mixed operations in multiple threads
|
|
166
|
+
threads = []
|
|
167
|
+
for _ in range(50):
|
|
168
|
+
t = threading.Thread(target=mixed_operations)
|
|
169
|
+
threads.append(t)
|
|
170
|
+
|
|
171
|
+
for t in threads:
|
|
172
|
+
t.start()
|
|
173
|
+
|
|
174
|
+
for t in threads:
|
|
175
|
+
t.join()
|
|
176
|
+
|
|
177
|
+
# Assert no errors occurred
|
|
178
|
+
self.assertEqual(len(errors), 0, f"Errors found: {errors[:5]}")
|
|
179
|
+
|
|
180
|
+
async def testMemoryConsistency(self) -> None:
|
|
181
|
+
"""
|
|
182
|
+
Test memory consistency across threads.
|
|
183
|
+
|
|
184
|
+
This test verifies that changes made in one thread
|
|
185
|
+
are visible in other threads due to singleton behavior.
|
|
186
|
+
"""
|
|
187
|
+
results = []
|
|
188
|
+
|
|
189
|
+
def thread_a():
|
|
190
|
+
"""Thread A - modifies the container."""
|
|
191
|
+
container = Container()
|
|
192
|
+
container.callable("thread_a_marker", lambda: "from_thread_a")
|
|
193
|
+
results.append("A_completed")
|
|
194
|
+
|
|
195
|
+
def thread_b():
|
|
196
|
+
"""Thread B - reads from the container."""
|
|
197
|
+
# Small delay to ensure thread A runs first
|
|
198
|
+
time.sleep(0.01)
|
|
199
|
+
container = Container()
|
|
200
|
+
has_marker = container.bound("thread_a_marker")
|
|
201
|
+
results.append(f"B_sees_marker: {has_marker}")
|
|
202
|
+
|
|
203
|
+
t1 = threading.Thread(target=thread_a)
|
|
204
|
+
t2 = threading.Thread(target=thread_b)
|
|
205
|
+
|
|
206
|
+
t1.start()
|
|
207
|
+
t2.start()
|
|
208
|
+
|
|
209
|
+
t1.join()
|
|
210
|
+
t2.join()
|
|
211
|
+
|
|
212
|
+
# Verify that thread B saw the changes made by thread A
|
|
213
|
+
a_completed = "A_completed" in results
|
|
214
|
+
b_saw_marker = any("B_sees_marker: True" in r for r in results)
|
|
215
|
+
|
|
216
|
+
self.assertTrue(a_completed, "Thread A should have completed")
|
|
217
|
+
self.assertTrue(b_saw_marker, "Thread B should see Thread A's changes")
|
|
File without changes
|