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.
Files changed (175) hide show
  1. orionis/console/base/command.py +57 -50
  2. orionis/console/base/contracts/command.py +68 -0
  3. orionis/console/dynamic/contracts/progress_bar.py +3 -3
  4. orionis/console/dynamic/progress_bar.py +8 -8
  5. orionis/console/output/console.py +8 -2
  6. orionis/console/output/contracts/console.py +1 -1
  7. orionis/container/container.py +2 -2
  8. orionis/container/context/scope.py +4 -1
  9. orionis/container/contracts/service_provider.py +2 -2
  10. orionis/container/entities/binding.py +31 -44
  11. orionis/container/enums/lifetimes.py +22 -1
  12. orionis/container/facades/facade.py +1 -2
  13. orionis/container/providers/service_provider.py +2 -2
  14. orionis/foundation/application.py +542 -248
  15. orionis/foundation/config/app/entities/app.py +107 -90
  16. orionis/foundation/config/auth/entities/auth.py +4 -33
  17. orionis/foundation/config/cache/entities/cache.py +18 -41
  18. orionis/foundation/config/cache/entities/file.py +8 -35
  19. orionis/foundation/config/cache/entities/stores.py +17 -38
  20. orionis/foundation/config/cors/entities/cors.py +41 -54
  21. orionis/foundation/config/database/entities/connections.py +40 -56
  22. orionis/foundation/config/database/entities/database.py +11 -38
  23. orionis/foundation/config/database/entities/mysql.py +48 -76
  24. orionis/foundation/config/database/entities/oracle.py +30 -57
  25. orionis/foundation/config/database/entities/pgsql.py +45 -61
  26. orionis/foundation/config/database/entities/sqlite.py +26 -53
  27. orionis/foundation/config/filesystems/entitites/aws.py +28 -49
  28. orionis/foundation/config/filesystems/entitites/disks.py +27 -47
  29. orionis/foundation/config/filesystems/entitites/filesystems.py +15 -37
  30. orionis/foundation/config/filesystems/entitites/local.py +9 -35
  31. orionis/foundation/config/filesystems/entitites/public.py +14 -41
  32. orionis/foundation/config/logging/entities/channels.py +56 -86
  33. orionis/foundation/config/logging/entities/chunked.py +9 -9
  34. orionis/foundation/config/logging/entities/daily.py +8 -8
  35. orionis/foundation/config/logging/entities/hourly.py +6 -6
  36. orionis/foundation/config/logging/entities/logging.py +12 -18
  37. orionis/foundation/config/logging/entities/monthly.py +7 -7
  38. orionis/foundation/config/logging/entities/stack.py +5 -5
  39. orionis/foundation/config/logging/entities/weekly.py +6 -6
  40. orionis/foundation/config/mail/entities/file.py +9 -36
  41. orionis/foundation/config/mail/entities/mail.py +22 -40
  42. orionis/foundation/config/mail/entities/mailers.py +29 -44
  43. orionis/foundation/config/mail/entities/smtp.py +47 -48
  44. orionis/foundation/config/queue/entities/brokers.py +19 -41
  45. orionis/foundation/config/queue/entities/database.py +24 -46
  46. orionis/foundation/config/queue/entities/queue.py +28 -40
  47. orionis/foundation/config/roots/paths.py +272 -468
  48. orionis/foundation/config/session/entities/session.py +23 -53
  49. orionis/foundation/config/startup.py +165 -135
  50. orionis/foundation/config/testing/entities/testing.py +137 -122
  51. orionis/foundation/config/testing/enums/__init__.py +6 -2
  52. orionis/foundation/config/testing/enums/drivers.py +16 -0
  53. orionis/foundation/config/testing/enums/verbosity.py +18 -0
  54. orionis/foundation/contracts/application.py +152 -362
  55. orionis/foundation/providers/console_provider.py +24 -2
  56. orionis/foundation/providers/dumper_provider.py +24 -2
  57. orionis/foundation/providers/logger_provider.py +24 -2
  58. orionis/foundation/providers/path_resolver_provider.py +25 -2
  59. orionis/foundation/providers/progress_bar_provider.py +24 -2
  60. orionis/foundation/providers/testing_provider.py +39 -0
  61. orionis/foundation/providers/workers_provider.py +24 -2
  62. orionis/metadata/framework.py +1 -1
  63. orionis/services/asynchrony/contracts/coroutines.py +13 -5
  64. orionis/services/asynchrony/coroutines.py +33 -29
  65. orionis/services/asynchrony/exceptions/exception.py +9 -1
  66. orionis/services/environment/core/dot_env.py +46 -34
  67. orionis/services/environment/enums/__init__.py +0 -0
  68. orionis/services/environment/enums/cast_type.py +42 -0
  69. orionis/services/environment/helpers/functions.py +1 -2
  70. orionis/services/environment/key/__init__.py +0 -0
  71. orionis/services/environment/key/key_generator.py +37 -0
  72. orionis/services/environment/serializer/__init__.py +0 -0
  73. orionis/services/environment/serializer/values.py +21 -0
  74. orionis/services/environment/validators/__init__.py +0 -0
  75. orionis/services/environment/validators/key_name.py +46 -0
  76. orionis/services/environment/validators/types.py +45 -0
  77. orionis/services/system/contracts/imports.py +38 -18
  78. orionis/services/system/contracts/workers.py +29 -12
  79. orionis/services/system/imports.py +65 -25
  80. orionis/services/system/runtime/imports.py +18 -9
  81. orionis/services/system/workers.py +49 -16
  82. orionis/support/entities/__init__.py +0 -0
  83. orionis/support/entities/base.py +104 -0
  84. orionis/support/facades/testing.py +15 -0
  85. orionis/support/facades/workers.py +1 -1
  86. orionis/test/cases/asynchronous.py +0 -11
  87. orionis/test/cases/synchronous.py +0 -9
  88. orionis/test/contracts/dumper.py +11 -4
  89. orionis/test/contracts/kernel.py +5 -110
  90. orionis/test/contracts/logs.py +27 -65
  91. orionis/test/contracts/printer.py +16 -128
  92. orionis/test/contracts/test_result.py +100 -0
  93. orionis/test/contracts/unit_test.py +87 -150
  94. orionis/test/core/unit_test.py +608 -554
  95. orionis/test/entities/result.py +22 -2
  96. orionis/test/enums/__init__.py +0 -2
  97. orionis/test/enums/status.py +14 -9
  98. orionis/test/exceptions/config.py +9 -1
  99. orionis/test/exceptions/failure.py +34 -11
  100. orionis/test/exceptions/persistence.py +10 -2
  101. orionis/test/exceptions/runtime.py +9 -1
  102. orionis/test/exceptions/value.py +13 -1
  103. orionis/test/kernel.py +87 -289
  104. orionis/test/output/dumper.py +83 -18
  105. orionis/test/output/printer.py +399 -156
  106. orionis/test/records/logs.py +203 -82
  107. orionis/test/validators/__init__.py +33 -0
  108. orionis/test/validators/base_path.py +45 -0
  109. orionis/test/validators/execution_mode.py +45 -0
  110. orionis/test/validators/fail_fast.py +37 -0
  111. orionis/test/validators/folder_path.py +34 -0
  112. orionis/test/validators/module_name.py +31 -0
  113. orionis/test/validators/name_pattern.py +40 -0
  114. orionis/test/validators/pattern.py +36 -0
  115. orionis/test/validators/persistent.py +42 -0
  116. orionis/test/validators/persistent_driver.py +43 -0
  117. orionis/test/validators/print_result.py +37 -0
  118. orionis/test/validators/tags.py +37 -0
  119. orionis/test/validators/throw_exception.py +39 -0
  120. orionis/test/validators/verbosity.py +37 -0
  121. orionis/test/validators/web_report.py +35 -0
  122. orionis/test/validators/workers.py +31 -0
  123. orionis/test/view/render.py +48 -54
  124. {orionis-0.405.0.dist-info → orionis-0.407.0.dist-info}/METADATA +1 -1
  125. {orionis-0.405.0.dist-info → orionis-0.407.0.dist-info}/RECORD +170 -112
  126. tests/container/__init__.py +0 -0
  127. tests/container/context/__init__.py +0 -0
  128. tests/container/context/test_manager.py +27 -0
  129. tests/container/context/test_scope.py +23 -0
  130. tests/container/entities/__init__.py +0 -0
  131. tests/container/entities/test_binding.py +133 -0
  132. tests/container/enums/__init__.py +0 -0
  133. tests/container/enums/test_lifetimes.py +63 -0
  134. tests/container/facades/__init__.py +0 -0
  135. tests/container/facades/test_facade.py +61 -0
  136. tests/container/mocks/__init__.py +0 -0
  137. tests/container/mocks/mock_complex_classes.py +482 -0
  138. tests/container/mocks/mock_simple_classes.py +32 -0
  139. tests/container/providers/__init__.py +0 -0
  140. tests/container/providers/test_providers.py +48 -0
  141. tests/container/resolver/__init__.py +0 -0
  142. tests/container/resolver/test_resolver.py +55 -0
  143. tests/container/test_container.py +254 -0
  144. tests/container/test_singleton.py +98 -0
  145. tests/container/test_thread_safety.py +217 -0
  146. tests/container/validators/__init__.py +0 -0
  147. tests/container/validators/test_implements.py +140 -0
  148. tests/container/validators/test_is_abstract_class.py +99 -0
  149. tests/container/validators/test_is_callable.py +73 -0
  150. tests/container/validators/test_is_concrete_class.py +97 -0
  151. tests/container/validators/test_is_instance.py +105 -0
  152. tests/container/validators/test_is_not_subclass.py +117 -0
  153. tests/container/validators/test_is_subclass.py +115 -0
  154. tests/container/validators/test_is_valid_alias.py +113 -0
  155. tests/container/validators/test_lifetime.py +75 -0
  156. tests/example/test_example.py +2 -2
  157. tests/foundation/config/testing/test_foundation_config_testing.py +1 -1
  158. tests/metadata/test_metadata_framework.py +89 -24
  159. tests/metadata/test_metadata_package.py +55 -10
  160. tests/services/asynchrony/test_services_asynchrony_coroutine.py +52 -7
  161. tests/services/system/test_services_system_imports.py +119 -16
  162. tests/services/system/test_services_system_workers.py +71 -30
  163. tests/testing/test_testing_result.py +117 -117
  164. tests/testing/test_testing_unit.py +209 -209
  165. orionis/foundation/config/base.py +0 -112
  166. orionis/test/arguments/parser.py +0 -187
  167. orionis/test/contracts/parser.py +0 -43
  168. orionis/test/entities/arguments.py +0 -38
  169. orionis/test/enums/execution_mode.py +0 -16
  170. /orionis/{test/arguments → console/base/contracts}/__init__.py +0 -0
  171. /orionis/foundation/config/testing/enums/{test_mode.py → mode.py} +0 -0
  172. {orionis-0.405.0.dist-info → orionis-0.407.0.dist-info}/WHEEL +0 -0
  173. {orionis-0.405.0.dist-info → orionis-0.407.0.dist-info}/licenses/LICENCE +0 -0
  174. {orionis-0.405.0.dist-info → orionis-0.407.0.dist-info}/top_level.txt +0 -0
  175. {orionis-0.405.0.dist-info → orionis-0.407.0.dist-info}/zip-safe +0 -0
@@ -11,376 +11,311 @@ from datetime import datetime
11
11
  from pathlib import Path
12
12
  from typing import Any, Dict, List, Optional, Tuple
13
13
  from orionis.container.resolver.resolver import Resolver
14
+ from orionis.foundation.config.testing.enums.drivers import PersistentDrivers
15
+ from orionis.foundation.config.testing.enums.mode import ExecutionMode
16
+ from orionis.foundation.config.testing.enums.verbosity import VerbosityMode
14
17
  from orionis.foundation.contracts.application import IApplication
15
18
  from orionis.services.introspection.instances.reflection import ReflectionInstance
16
- from orionis.services.system.workers import Workers
19
+ from orionis.test.contracts.test_result import IOrionisTestResult
20
+ from orionis.test.contracts.unit_test import IUnitTest
17
21
  from orionis.test.entities.result import TestResult
18
- from orionis.test.enums import (
19
- ExecutionMode,
20
- TestStatus
21
- )
22
+ from orionis.test.enums import TestStatus
22
23
  from orionis.test.exceptions import (
23
24
  OrionisTestFailureException,
24
25
  OrionisTestPersistenceError,
25
- OrionisTestValueError
26
+ OrionisTestValueError,
26
27
  )
27
- from orionis.test.records.logs import TestLogs
28
- from orionis.test.contracts.unit_test import IUnitTest
29
28
  from orionis.test.output.printer import TestPrinter
29
+ from orionis.test.records.logs import TestLogs
30
+ from orionis.test.validators import (
31
+ ValidExecutionMode,
32
+ ValidFailFast,
33
+ ValidPersistent,
34
+ ValidPersistentDriver,
35
+ ValidPrintResult,
36
+ ValidThrowException,
37
+ ValidVerbosity,
38
+ ValidWebReport,
39
+ ValidWorkers,
40
+ ValidBasePath,
41
+ ValidFolderPath,
42
+ ValidNamePattern,
43
+ ValidPattern,
44
+ ValidTags,
45
+ ValidModuleName,
46
+ )
30
47
  from orionis.test.view.render import TestingResultRender
31
48
 
32
49
  class UnitTest(IUnitTest):
33
50
  """
34
51
  Orionis UnitTest
35
52
 
36
- The main class of the Orionis framework for advanced unit test management.
53
+ Advanced unit testing manager for the Orionis framework.
37
54
 
38
- This class provides a comprehensive solution for discovering, executing, and reporting unit tests in a flexible and configurable way, surpassing the usual limitations of traditional frameworks.
39
- It supports sequential or parallel execution, filtering by name or tags, and detailed result capture, including timings, errors, and tracebacks.
55
+ This class offers a robust and extensible solution for discovering, executing, and reporting unit tests with high configurability. It supports both sequential and parallel execution modes, filtering by test name or tags, and provides detailed result tracking including execution times, error messages, and tracebacks.
40
56
 
41
- It includes persistence options in multiple formats (SQLite or JSON) and generates rich reports both in the console and on the web.
42
- Its intuitive interface and high degree of customization make it easy to integrate into CI/CD pipelines and adapt to the specific needs of any project.
57
+ Key features:
58
+ - Flexible test discovery from folders or modules, with pattern and tag filtering.
59
+ - Rich result reporting: console output, persistent storage (SQLite or JSON), and web-based reports.
60
+ - Dependency injection for test methods via the application context.
61
+ - Customizable verbosity, fail-fast, and exception handling options.
62
+ - Designed for easy integration into CI/CD pipelines and adaptable to diverse project requirements.
43
63
 
44
- This is an especially suitable choice for those seeking greater robustness, traceability, and visibility in their automated testing processes, offering advantages often missing from other alternatives.
64
+ Orionis UnitTest is ideal for teams seeking enhanced traceability, reliability, and visibility in automated testing, with capabilities that go beyond standard unittest frameworks.
45
65
  """
46
66
 
47
67
  def __init__(
48
68
  self
49
69
  ) -> None:
50
70
  """
51
- Initializes the test suite configuration and supporting components.
71
+ Initialize a new UnitTest instance with default configuration and internal state.
52
72
 
53
- Parameters
54
- ----------
55
- None
73
+ This constructor sets up all internal attributes required for test discovery, execution,
74
+ result reporting, and configuration management. It prepares the instance for further
75
+ configuration and use, but does not perform any test discovery or execution itself.
56
76
 
57
77
  Attributes
58
78
  ----------
59
- verbosity : int
60
- Level of verbosity for test output.
61
- execution_mode : str
62
- Mode in which tests are executed.
63
- max_workers : int
64
- Maximum number of worker threads/processes.
65
- fail_fast : bool
66
- Whether to stop on the first test failure.
67
- print_result : bool
68
- Whether to print test results to the console.
69
- throw_exception : bool
70
- Whether to raise exceptions on test failures.
71
- persistent : bool
72
- Whether to use persistent storage for test results.
73
- persistent_driver : str
74
- Driver used for persistent storage.
75
- web_report : bool
76
- Whether to generate a web-based report.
77
- full_path : Optional[str]
78
- Full path for test discovery.
79
- folder_path : str
80
- Folder path for test discovery.
81
- base_path : str
82
- Base path for test discovery.
83
- pattern : str
84
- Pattern to match test files.
85
- test_name_pattern : Optional[str]
86
- Pattern to match test names.
87
- tags : Optional[List[str]]
88
- Tags to filter tests.
89
- module_name : str
90
- Name of the module for test discovery.
91
- loader : unittest.TestLoader
79
+ __app : Optional[IApplication]
80
+ The application instance used for dependency injection in test cases.
81
+ __verbosity : Optional[int]
82
+ Verbosity level for test output (None until configured).
83
+ __execution_mode : Optional[str]
84
+ Test execution mode, e.g., 'SEQUENTIAL' or 'PARALLEL' (None until configured).
85
+ __max_workers : Optional[int]
86
+ Maximum number of worker threads/processes for parallel execution (None until configured).
87
+ __fail_fast : Optional[bool]
88
+ If True, stops execution upon the first test failure (None until configured).
89
+ __throw_exception : Optional[bool]
90
+ If True, raises exceptions on test failures (None until configured).
91
+ __persistent : Optional[bool]
92
+ If True, enables persistent storage for test results (None until configured).
93
+ __persistent_driver : Optional[str]
94
+ The driver to use for persistence, e.g., 'sqlite' or 'json' (None until configured).
95
+ __web_report : Optional[bool]
96
+ If True, enables web-based reporting of test results (None until configured).
97
+ __folder_path : Optional[str]
98
+ Relative folder path for test discovery (None until set).
99
+ __base_path : Optional[str]
100
+ Base directory for test discovery (None until set).
101
+ __pattern : Optional[str]
102
+ File name pattern to match test files (None until set).
103
+ __test_name_pattern : Optional[str]
104
+ Pattern to filter test names (None until set).
105
+ __tags : Optional[List[str]]
106
+ List of tags to filter tests (None until set).
107
+ __module_name : Optional[str]
108
+ Name of the module for test discovery (None until set).
109
+ __loader : unittest.TestLoader
92
110
  Loader for discovering tests.
93
- suite : unittest.TestSuite
94
- Test suite to hold discovered tests.
95
- discovered_tests : list
96
- List of discovered tests.
97
- printer : TestPrinter
111
+ __suite : unittest.TestSuite
112
+ Test suite containing discovered tests.
113
+ __discovered_tests : List
114
+ List of discovered test metadata.
115
+ __printer : Optional[TestPrinter]
98
116
  Utility for printing test results to the console.
99
- __output_buffer
117
+ __output_buffer : Optional[str]
100
118
  Buffer for capturing standard output during tests.
101
- __error_buffer
119
+ __error_buffer : Optional[str]
102
120
  Buffer for capturing error output during tests.
103
- __result
104
- Result of the test execution.
121
+ __result : Optional[dict]
122
+ Result summary of the test execution.
123
+
124
+ Returns
125
+ -------
126
+ None
127
+ This constructor does not return a value.
105
128
  """
106
129
 
107
- # Value for application instance
108
- self.app: Optional[IApplication] = None
109
-
110
- # Values for configuration
111
- self.verbosity: Optional[int] = None
112
- self.execution_mode: Optional[str] = None
113
- self.max_workers: Optional[int] = None
114
- self.fail_fast: Optional[bool] = None
115
- self.print_result: Optional[bool] = None
116
- self.throw_exception: Optional[bool] = None
117
- self.persistent: Optional[bool] = None
118
- self.persistent_driver: Optional[str] = None
119
- self.web_report: Optional[bool] = None
120
-
121
- # Values for discovering tests in folders
122
- self.full_path: Optional[str] = None
123
- self.folder_path: Optional[str] = None
124
- self.base_path: Optional[str] = None
125
- self.pattern: Optional[str] = None
126
- self.test_name_pattern: Optional[str] = None
127
- self.tags: Optional[List[str]] = None
128
-
129
- # Values for discovering tests in modules
130
- self.module_name: Optional[str] = None
131
-
132
- # Initialize the test loader and suite
133
- self.loader = unittest.TestLoader()
134
- self.suite = unittest.TestSuite()
135
- self.discovered_tests: List = []
136
-
137
- # Initialize the class for printing in the console
138
- self.printer = TestPrinter()
139
-
140
- # Variables for capturing output and error streams
130
+ # Application instance for dependency injection (set via __setApp)
131
+ self.__app: Optional[IApplication] = None
132
+
133
+ # Storage path for test results (set via __setApp)
134
+ self.__storage: Optional[str] = None
135
+
136
+ # Configuration values (set via configure)
137
+ self.__verbosity: Optional[int] = None
138
+ self.__execution_mode: Optional[str] = None
139
+ self.__max_workers: Optional[int] = None
140
+ self.__fail_fast: Optional[bool] = None
141
+ self.__throw_exception: Optional[bool] = None
142
+ self.__persistent: Optional[bool] = None
143
+ self.__persistent_driver: Optional[str] = None
144
+ self.__web_report: Optional[bool] = None
145
+
146
+ # Test discovery parameters for folders
147
+ self.__folder_path: Optional[str] = None
148
+ self.__base_path: Optional[str] = None
149
+ self.__pattern: Optional[str] = None
150
+ self.__test_name_pattern: Optional[str] = None
151
+ self.__tags: Optional[List[str]] = None
152
+
153
+ # Test discovery parameter for modules
154
+ self.__module_name: Optional[str] = None
155
+
156
+ # Initialize the unittest loader and suite for test discovery and execution
157
+ self.__loader = unittest.TestLoader()
158
+ self.__suite = unittest.TestSuite()
159
+ self.__discovered_tests: List = []
160
+
161
+ # Printer for console output (set during configuration)
162
+ self.__printer: TestPrinter = None
163
+
164
+ # Buffers for capturing standard output and error during test execution
141
165
  self.__output_buffer = None
142
166
  self.__error_buffer = None
143
167
 
144
- # Result of the test execution
168
+ # Stores the result summary after test execution
145
169
  self.__result = None
146
170
 
147
- def setApplication(
171
+ def configure(
148
172
  self,
149
- app: 'IApplication'
173
+ *,
174
+ verbosity: int | VerbosityMode,
175
+ execution_mode: str | ExecutionMode,
176
+ max_workers: int,
177
+ fail_fast: bool,
178
+ print_result: bool,
179
+ throw_exception: bool,
180
+ persistent: bool,
181
+ persistent_driver: str | PersistentDrivers,
182
+ web_report: bool
150
183
  ) -> 'UnitTest':
151
184
  """
152
- Set the application instance for the UnitTest.
153
- This method allows the UnitTest to access the application instance, which is necessary for resolving dependencies and executing tests.
154
-
155
- Parameters
156
- ----------
157
- app : IApplication
158
- The application instance to be set for the UnitTest.
159
-
160
- Returns
161
- -------
162
- UnitTest
163
- """
164
-
165
- # Validate the provided application instance
166
- if not isinstance(app, IApplication):
167
- raise OrionisTestValueError(
168
- f"The provided application is not a valid instance of IApplication: {type(app).__name__}."
169
- )
170
-
171
- # Set the application instance
172
- self.app = app
185
+ Configures the UnitTest instance with the main execution and reporting parameters.
173
186
 
174
- def configure(
175
- self,
176
- *,
177
- verbosity: int = 2,
178
- execution_mode: str | ExecutionMode = ExecutionMode.SEQUENTIAL,
179
- max_workers: int = Workers().calculate(),
180
- fail_fast: bool = False,
181
- print_result: bool = True,
182
- throw_exception: bool = False,
183
- persistent: bool = False,
184
- persistent_driver: str = 'sqlite',
185
- web_report: bool = False
186
- ) -> 'UnitTest':
187
- """
188
- Configure the UnitTest instance with various execution and reporting options.
187
+ This method sets all relevant options for running unit tests in Orionis, including execution mode
188
+ (sequential or parallel), verbosity level, maximum number of workers, result persistence, exception
189
+ handling, and web report generation.
189
190
 
190
191
  Parameters
191
192
  ----------
192
- verbosity : int, optional
193
- Level of output verbosity.
194
- execution_mode : str or ExecutionMode, optional
195
- Test execution mode.
196
- max_workers : int, optional
197
- Maximum number of worker threads/processes for parallel execution. Must be a positive integer.
198
- fail_fast : bool, optional
199
- If True, stop execution on first failure.
200
- print_result : bool, optional
201
- If True, print test results to the console.
202
- throw_exception : bool, default: False
203
- If True, raise exceptions on test failures.
204
- persistent : bool, default: False
205
- If True, enable persistent storage of test results.
206
- persistent_driver : str, default: 'sqlite'
207
- Backend for persistent storage. Must be 'sqlite' or 'json'.
208
- web_report : bool, default: False
209
- If True, enable web-based reporting.
193
+ verbosity : int | VerbosityMode
194
+ Verbosity level for test output. Can be an integer or a VerbosityMode enum member.
195
+ execution_mode : str | ExecutionMode
196
+ Test execution mode ('SEQUENTIAL' or 'PARALLEL'), as a string or ExecutionMode enum.
197
+ max_workers : int
198
+ Maximum number of threads/processes for parallel execution. Must be between 1 and the maximum allowed by Workers.
199
+ fail_fast : bool
200
+ If True, stops execution on the first failure.
201
+ print_result : bool
202
+ If True, prints results to the console.
203
+ throw_exception : bool
204
+ If True, raises exceptions on test failures.
205
+ persistent : bool
206
+ If True, enables result persistence.
207
+ persistent_driver : str or PersistentDrivers
208
+ Persistence driver to use ('sqlite' or 'json').
209
+ web_report : bool
210
+ If True, enables web report generation.
210
211
 
211
212
  Returns
212
213
  -------
213
214
  UnitTest
214
- The configured UnitTest instance.
215
+ The configured UnitTest instance, allowing method chaining.
215
216
 
216
217
  Raises
217
218
  ------
218
219
  OrionisTestValueError
219
- If any parameter value is invalid.
220
+ If any parameter is invalid or does not meet the expected requirements.
220
221
  """
221
222
 
222
- # Validate and set verbosity
223
- if verbosity is not None:
224
- if isinstance(verbosity, int) and verbosity in [0, 1, 2]:
225
- self.verbosity = verbosity
226
- else:
227
- raise OrionisTestValueError("Verbosity must be an integer: 0 (quiet), 1 (default), or 2 (verbose).")
228
-
229
- # Validate and set execution mode
230
- if execution_mode is not None and isinstance(execution_mode, ExecutionMode):
231
- self.execution_mode = execution_mode.value
232
- else:
233
- if isinstance(execution_mode, str) and execution_mode in [ExecutionMode.SEQUENTIAL.value, ExecutionMode.PARALLEL.value]:
234
- self.execution_mode = execution_mode
235
- else:
236
- raise OrionisTestValueError("Execution mode must be 'SEQUENTIAL' or 'PARALLEL'.")
237
-
238
- # Validate and set max_workers
239
- if max_workers is not None:
240
- if isinstance(max_workers, int) and max_workers > 0:
241
- self.max_workers = max_workers
242
- else:
243
- raise OrionisTestValueError("Max workers must be a positive integer.")
244
-
245
- # Validate and set other parameters
246
- if fail_fast is not None:
247
- if isinstance(fail_fast, bool):
248
- self.fail_fast = fail_fast
249
- else:
250
- raise OrionisTestValueError("Fail fast must be a boolean value.")
251
-
252
- # Validate and set print_result
253
- if print_result is not None:
254
- if isinstance(print_result, bool):
255
- self.print_result = print_result
256
- else:
257
- raise OrionisTestValueError("Print result must be a boolean value.")
258
-
259
- # Validate and set throw_exception
260
- if throw_exception is not None:
261
- if isinstance(throw_exception, bool):
262
- self.throw_exception = throw_exception
263
- else:
264
- raise OrionisTestValueError("Throw exception must be a boolean value.")
265
-
266
- # Validate and set persistent and persistent_driver
267
- if persistent is not None:
268
- if isinstance(persistent, bool):
269
- self.persistent = persistent
270
- else:
271
- raise OrionisTestValueError("Persistent must be a boolean value.")
272
-
273
- # Validate and set persistent_driver
274
- if persistent_driver is not None:
275
- if isinstance(persistent_driver, str) and persistent_driver in ['sqlite', 'json']:
276
- self.persistent_driver = persistent_driver
277
- else:
278
- raise OrionisTestValueError("Persistent driver must be 'sqlite' or 'json'.")
279
-
280
- # Validate and set web_report
281
- if web_report is not None:
282
- if isinstance(web_report, bool):
283
- self.web_report = web_report
284
- else:
285
- raise OrionisTestValueError("Web report must be a boolean value.")
223
+ # Validate and assign parameters using specialized validators
224
+ self.__verbosity = ValidVerbosity(verbosity)
225
+ self.__execution_mode = ValidExecutionMode(execution_mode)
226
+ self.__max_workers = ValidWorkers(max_workers)
227
+ self.__fail_fast = ValidFailFast(fail_fast)
228
+ self.__throw_exception = ValidThrowException(throw_exception)
229
+ self.__persistent = ValidPersistent(persistent)
230
+ self.__persistent_driver = ValidPersistentDriver(persistent_driver)
231
+ self.__web_report = ValidWebReport(web_report)
232
+
233
+ # Initialize the result printer with the current configuration
234
+ self.__printer = TestPrinter(
235
+ print_result = ValidPrintResult(print_result)
236
+ )
286
237
 
287
- # Return the configured instance
238
+ # Return the instance to allow method chaining
288
239
  return self
289
240
 
290
241
  def discoverTestsInFolder(
291
242
  self,
292
243
  *,
293
- base_path: str = "tests",
244
+ base_path: str | Path,
294
245
  folder_path: str,
295
- pattern: str = "test_*.py",
246
+ pattern: str,
296
247
  test_name_pattern: Optional[str] = None,
297
248
  tags: Optional[List[str]] = None
298
249
  ) -> 'UnitTest':
299
250
  """
300
251
  Discover and add unit tests from a specified folder to the test suite.
301
252
 
302
- Searches for test files in the given folder path, optionally filtering by file name pattern,
303
- test name pattern, and tags. Discovered tests are added to the suite, and information about
304
- the discovery is recorded.
253
+ This method searches for test files within a given folder, using a file name pattern,
254
+ and optionally filters discovered tests by test name pattern and tags. All matching
255
+ tests are added to the internal test suite. The method also records metadata about
256
+ the discovery process, such as the folder path and the number of tests found.
305
257
 
306
258
  Parameters
307
259
  ----------
308
- base_path : str, optional
309
- The base directory to search for tests. Defaults to "tests".
260
+ base_path : str or Path
261
+ The base directory from which the folder path is resolved.
310
262
  folder_path : str
311
- The relative path to the folder containing test files.
312
- pattern : str, optional
313
- The file name pattern to match test files. Defaults to "test_*.py".
314
- test_name_pattern : Optional[str], optional
315
- A pattern to filter test names. Defaults to None.
316
- tags : Optional[List[str]], optional
317
- A list of tags to filter tests. Defaults to None.
263
+ The relative path to the folder containing test files, relative to `base_path`.
264
+ pattern : str
265
+ The file name pattern to match test files (e.g., 'test_*.py').
266
+ test_name_pattern : str, optional
267
+ A regular expression pattern to filter test names. Only tests whose names match
268
+ this pattern will be included. If None, all test names are included.
269
+ tags : list of str, optional
270
+ A list of tags to filter tests. Only tests decorated or marked with any of these
271
+ tags will be included. If None, no tag filtering is applied.
318
272
 
319
273
  Returns
320
274
  -------
321
275
  UnitTest
322
- The current instance with discovered tests added to the suite.
276
+ The current instance with the discovered tests added to the suite.
323
277
 
324
278
  Raises
325
279
  ------
326
280
  OrionisTestValueError
327
281
  If any argument is invalid, the folder does not exist, no tests are found,
328
282
  or if there are import or discovery errors.
329
- """
330
283
 
331
- # Validate folder_path
332
- if folder_path is None or not isinstance(folder_path, str):
333
- raise OrionisTestValueError(
334
- f"Invalid folder_path: Expected a non-empty string, got '{folder_path}' ({type(folder_path).__name__})."
335
- )
336
- self.folder_path = folder_path
337
-
338
- # Validate base_path and set value
339
- if base_path is None or not isinstance(base_path, str):
340
- raise OrionisTestValueError(
341
- f"Invalid base_path: Expected a non-empty string, got '{base_path}' ({type(base_path).__name__})."
342
- )
343
- self.base_path = base_path
344
-
345
- # Validate pattern
346
- if pattern is None or not isinstance(pattern, str):
347
- raise OrionisTestValueError(
348
- f"Invalid pattern: Expected a non-empty string, got '{pattern}' ({type(pattern).__name__})."
349
- )
350
- self.pattern = pattern
351
-
352
- # Validate test_name_pattern
353
- if test_name_pattern is not None:
354
- if not isinstance(test_name_pattern, str):
355
- raise OrionisTestValueError(
356
- f"Invalid test_name_pattern: Expected a string, got '{test_name_pattern}' ({type(test_name_pattern).__name__})."
357
- )
358
- self.test_name_pattern = test_name_pattern
284
+ Notes
285
+ -----
286
+ - The method validates all input parameters using Orionis validators.
287
+ - The folder path is resolved relative to the provided base path.
288
+ - Test discovery uses Python's unittest loader.
289
+ - If `test_name_pattern` is provided, only tests whose names match the pattern are included.
290
+ - If `tags` are provided, only tests with matching tags are included.
291
+ - If no tests are found after filtering, an exception is raised.
292
+ - Metadata about the discovery (folder and test count) is appended to the internal record.
293
+ """
359
294
 
360
- # Validate tags
361
- if tags is not None:
362
- if not isinstance(tags, list) or not all(isinstance(tag, str) for tag in tags):
363
- raise OrionisTestValueError(
364
- f"Invalid tags: Expected a list of strings, got '{tags}' ({type(tags).__name__})."
365
- )
366
- self.tags = tags
295
+ # Validate Parameters
296
+ self.__base_path = ValidBasePath(base_path)
297
+ self.__folder_path = ValidFolderPath(folder_path)
298
+ self.__pattern = ValidPattern(pattern)
299
+ self.__test_name_pattern = ValidNamePattern(test_name_pattern)
300
+ self.__tags = ValidTags(tags)
367
301
 
368
302
  # Try to discover tests in the specified folder
369
303
  try:
370
304
 
371
305
  # Ensure the folder path is absolute
372
- full_path = Path(self.base_path) / self.folder_path
306
+ full_path = Path(self.__base_path / self.__folder_path).resolve()
307
+
308
+ # Validate the full path
373
309
  if not full_path.exists():
374
310
  raise OrionisTestValueError(
375
- f"Test folder not found at the specified path: '{full_path}'. "
311
+ f"Test folder not found at the specified path: '{str(full_path)}'. "
376
312
  "Please verify that the path is correct and the folder exists."
377
313
  )
378
- self.full_path = str(full_path.resolve())
379
314
 
380
315
  # Discover tests using the unittest TestLoader
381
- tests = self.loader.discover(
316
+ tests = self.__loader.discover(
382
317
  start_dir=str(full_path),
383
- pattern=pattern,
318
+ pattern=self.__pattern,
384
319
  top_level_dir=None
385
320
  )
386
321
 
@@ -388,53 +323,53 @@ class UnitTest(IUnitTest):
388
323
  if test_name_pattern:
389
324
  tests = self.__filterTestsByName(
390
325
  suite=tests,
391
- pattern=test_name_pattern
326
+ pattern=self.__test_name_pattern
392
327
  )
393
328
 
394
329
  # If tags are provided, filter tests by tags
395
330
  if tags:
396
331
  tests = self.__filterTestsByTags(
397
332
  suite=tests,
398
- tags=tags
333
+ tags=self.__tags
399
334
  )
400
335
 
401
336
  # If no tests are found, raise an error
402
337
  if not list(tests):
403
338
  raise OrionisTestValueError(
404
- f"No tests were found in the path '{full_path}' matching the file pattern '{pattern}'"
405
- + (f" and the test name pattern '{test_name_pattern}'" if test_name_pattern else "")
406
- + (f" and the tags {tags}" if tags else "") +
407
- ".\nPlease ensure that test files exist and that the patterns and tags are correct."
339
+ f"No tests found in '{str(full_path)}' matching file pattern '{pattern}'"
340
+ + (f", test name pattern '{test_name_pattern}'" if test_name_pattern else "")
341
+ + (f", and tags {tags}" if tags else "") +
342
+ ". Please check your patterns, tags, and test files."
408
343
  )
409
344
 
410
345
  # Add discovered tests to the suite
411
- self.suite.addTests(tests)
346
+ self.__suite.addTests(tests)
412
347
 
413
348
  # Count the number of tests discovered
414
349
  # Using __flattenTestSuite to ensure we count all individual test cases
415
350
  test_count = len(list(self.__flattenTestSuite(tests)))
416
351
 
417
352
  # Append the discovered tests information
418
- self.discovered_tests.append({
353
+ self.__discovered_tests.append({
419
354
  "folder": str(full_path),
420
355
  "test_count": test_count,
421
356
  })
422
357
 
423
- # Rereturn the current instance
358
+ # Return the current instance
424
359
  return self
425
360
 
426
361
  except ImportError as e:
427
362
 
428
363
  # Raise a specific error if the import fails
429
364
  raise OrionisTestValueError(
430
- f"Error importing tests from path '{full_path}': {str(e)}.\n"
365
+ f"Error importing tests from path '{str(full_path)}': {str(e)}.\n"
431
366
  "Please verify that the directory and test modules are accessible and correct."
432
367
  )
433
368
  except Exception as e:
434
369
 
435
370
  # Raise a general error for unexpected issues
436
371
  raise OrionisTestValueError(
437
- f"Unexpected error while discovering tests in '{full_path}': {str(e)}.\n"
372
+ f"Unexpected error while discovering tests in '{str(full_path)}': {str(e)}.\n"
438
373
  "Ensure that the test files are valid and that there are no syntax errors or missing dependencies."
439
374
  )
440
375
 
@@ -445,137 +380,168 @@ class UnitTest(IUnitTest):
445
380
  test_name_pattern: Optional[str] = None
446
381
  ) -> 'UnitTest':
447
382
  """
448
- Discover and add unit tests from a specified module to the test suite.
383
+ Discover and add unit tests from a specified Python module to the test suite.
384
+
385
+ This method loads all unit tests defined within the given module and adds them to the internal test suite.
386
+ Optionally, it can filter discovered tests by a regular expression pattern applied to test names.
449
387
 
450
388
  Parameters
451
389
  ----------
452
390
  module_name : str
453
- The name of the module from which to discover tests. Must be a non-empty string.
454
- test_name_pattern : Optional[str], optional
455
- A pattern to filter test names. If provided, only tests matching this pattern will be included.
391
+ The fully qualified name of the module from which to discover tests (e.g., 'myproject.tests.test_example').
392
+ Must be a non-empty string and importable from the current environment.
393
+ test_name_pattern : str or None, optional
394
+ A regular expression pattern to filter test names. Only tests whose names match this pattern
395
+ will be included in the suite. If None, all discovered tests are included.
456
396
 
457
397
  Returns
458
398
  -------
459
399
  UnitTest
460
- The current instance with the discovered tests added to the suite.
400
+ The current UnitTest instance with the discovered tests added, allowing method chaining.
461
401
 
462
402
  Raises
463
403
  ------
464
404
  OrionisTestValueError
465
- If the module_name is invalid, the test_name_pattern is invalid, the module cannot be imported,
466
- or any unexpected error occurs during test discovery.
405
+ If `module_name` is invalid, `test_name_pattern` is not a valid regex, the module cannot be imported,
406
+ or if no tests are found after filtering.
407
+ OrionisTestValueError
408
+ For any unexpected error during test discovery, with details about the failure.
467
409
 
468
410
  Notes
469
411
  -----
470
- - The method validates the input parameters before attempting to discover tests.
471
- - If a test_name_pattern is provided, only tests matching the pattern are included.
472
- - Information about the discovered tests is appended to the 'discovered_tests' attribute.
412
+ - Input parameters are validated using Orionis validators before discovery.
413
+ - If `test_name_pattern` is provided, only tests matching the pattern are included.
414
+ - Metadata about the discovery (module name and test count) is appended to the internal `__discovered_tests` list.
415
+ - This method is useful for dynamically loading tests from specific modules, such as in plugin architectures or
416
+ when tests are not organized in standard file patterns.
473
417
  """
474
418
 
475
- # Validate module_name
476
- if not module_name or not isinstance(module_name, str):
477
- raise OrionisTestValueError(
478
- f"Invalid module_name: Expected a non-empty string, got '{module_name}' ({type(module_name).__name__})."
479
- )
480
- self.module_name = module_name
481
-
482
- # Validate test_name_pattern
483
- if test_name_pattern is not None and not isinstance(test_name_pattern, str):
484
- raise OrionisTestValueError(
485
- f"Invalid test_name_pattern: Expected a string, got '{test_name_pattern}' ({type(test_name_pattern).__name__})."
486
- )
487
- self.test_name_pattern = test_name_pattern
419
+ # Validate input parameters
420
+ self.__module_name = ValidModuleName(module_name)
421
+ self.__test_name_pattern = ValidNamePattern(test_name_pattern)
488
422
 
489
- # Try to load tests from the specified module
490
423
  try:
491
-
492
- # Load the tests from the specified module
493
- tests = self.loader.loadTestsFromName(
494
- name=module_name
424
+ # Load all tests from the specified module
425
+ tests = self.__loader.loadTestsFromName(
426
+ name=self.__module_name
495
427
  )
496
428
 
497
- # If test_name_pattern provided
429
+ # If a test name pattern is provided, filter the discovered tests
498
430
  if test_name_pattern:
499
431
  tests = self.__filterTestsByName(
500
432
  suite=tests,
501
- pattern=test_name_pattern
433
+ pattern=self.__test_name_pattern
502
434
  )
503
435
 
504
- # Add the discovered tests to the suite
505
- self.suite.addTests(tests)
436
+ # Add the filtered (or all) tests to the suite
437
+ self.__suite.addTests(tests)
506
438
 
507
- # Count the number of tests discovered
439
+ # Count the number of discovered tests
508
440
  test_count = len(list(self.__flattenTestSuite(tests)))
509
441
 
510
- # Append the discovered tests information
511
- self.discovered_tests.append({
512
- "module": module_name,
513
- "test_count": test_count,
442
+ if test_count == 0:
443
+ raise OrionisTestValueError(
444
+ f"No tests found in module '{self.__module_name}'"
445
+ + (f" matching test name pattern '{test_name_pattern}'." if test_name_pattern else ".")
446
+ + " Please ensure the module contains valid test cases and the pattern is correct."
447
+ )
448
+
449
+ # Record discovery metadata
450
+ self.__discovered_tests.append({
451
+ "module": self.__module_name,
452
+ "test_count": test_count
514
453
  })
515
454
 
516
- # Return the current instance
455
+ # Return the current instance for method chaining
517
456
  return self
518
457
 
519
458
  except ImportError as e:
520
459
 
521
- # Raise a specific error if the import fails
460
+ # Raise an error if the module cannot be imported
461
+ raise OrionisTestValueError(
462
+ f"Failed to import tests from module '{self.__module_name}': {str(e)}. "
463
+ "Ensure the module exists, is importable, and contains valid test cases."
464
+ )
465
+
466
+ except re.error as e:
467
+
468
+ # Raise an error if the test name pattern is not a valid regex
522
469
  raise OrionisTestValueError(
523
- f"Error importing tests from module '{module_name}': {str(e)}.\n"
524
- "Please verify that the module exists, is accessible, and contains valid test cases."
470
+ f"Invalid regular expression for test_name_pattern: '{test_name_pattern}'. "
471
+ f"Regex compilation error: {str(e)}. Please check the pattern syntax."
525
472
  )
473
+
526
474
  except Exception as e:
527
475
 
528
476
  # Raise a general error for unexpected issues
529
477
  raise OrionisTestValueError(
530
- f"Unexpected error while discovering tests in module '{module_name}': {str(e)}.\n"
531
- "Ensure that the module name is correct, the test methods are valid, and there are no syntax errors or missing dependencies."
478
+ f"An unexpected error occurred while discovering tests in module '{self.__module_name}': {str(e)}. "
479
+ "Verify that the module name is correct, test methods are valid, and there are no syntax errors or missing dependencies."
532
480
  )
533
481
 
534
482
  def run(
535
483
  self
536
484
  ) -> Dict[str, Any]:
485
+ """
486
+ Execute the test suite and return a summary of the results.
537
487
 
538
- # Start the timer and print the start message
539
- start_time = time.time()
488
+ This method manages the full test execution lifecycle: it prints start and finish messages,
489
+ executes the test suite, captures output and error buffers, processes the results, and
490
+ optionally raises an exception if failures occur and exception throwing is enabled.
540
491
 
541
- # Print the start message
542
- self.printer.startMessage(
543
- print_result=self.print_result,
544
- length_tests=len(list(self.__flattenTestSuite(self.suite))),
545
- execution_mode=self.execution_mode,
546
- max_workers=self.max_workers
492
+ Returns
493
+ -------
494
+ Dict[str, Any]
495
+ A dictionary summarizing the test results, including statistics and execution time.
496
+
497
+ Raises
498
+ ------
499
+ OrionisTestFailureException
500
+ If the test suite execution fails and `throw_exception` is set to True.
501
+
502
+ Notes
503
+ -----
504
+ - Measures total execution time in milliseconds.
505
+ - Uses the configured printer to display start, result, and finish messages.
506
+ - Captures and stores output and error buffers.
507
+ - Raises an exception if tests fail and exception throwing is enabled.
508
+ """
509
+
510
+ # Record the start time in nanoseconds
511
+ start_time = time.time_ns()
512
+
513
+ # Print the start message with test suite details
514
+ self.__printer.startMessage(
515
+ length_tests=len(list(self.__flattenTestSuite(self.__suite))),
516
+ execution_mode=self.__execution_mode,
517
+ max_workers=self.__max_workers
547
518
  )
548
519
 
549
- # Execute the test suite and capture the results
550
- result, output_buffer, error_buffer = self.printer.executePanel(
551
- print_result=self.print_result,
552
- flatten_test_suite= self.__flattenTestSuite(self.suite),
520
+ # Execute the test suite and capture result, output, and error buffers
521
+ result, output_buffer, error_buffer = self.__printer.executePanel(
522
+ flatten_test_suite=self.__flattenTestSuite(self.__suite),
553
523
  callable=self.__runSuite
554
524
  )
555
525
 
556
- # Save Outputs
526
+ # Store the captured output and error buffers as strings
557
527
  self.__output_buffer = output_buffer.getvalue()
558
528
  self.__error_buffer = error_buffer.getvalue()
559
529
 
560
- # Process results
561
- execution_time = time.time() - start_time
530
+ # Calculate execution time in milliseconds
531
+ execution_time = (time.time_ns() - start_time) / 1_000_000_000
532
+
533
+ # Generate a summary of the test results
562
534
  summary = self.__generateSummary(result, execution_time)
563
535
 
564
- # Print captured output
565
- self.printer.displayResults(
566
- print_result=self.print_result,
567
- summary=summary
568
- )
536
+ # Display the test results using the printer
537
+ self.__printer.displayResults(summary=summary)
569
538
 
570
- # Print Execution Time
571
- if not result.wasSuccessful() and self.throw_exception:
539
+ # Raise an exception if tests failed and exception throwing is enabled
540
+ if not result.wasSuccessful() and self.__throw_exception:
572
541
  raise OrionisTestFailureException(result)
573
542
 
574
543
  # Print the final summary message
575
- self.printer.finishMessage(
576
- print_result=self.print_result,
577
- summary=summary
578
- )
544
+ self.__printer.finishMessage(summary=summary)
579
545
 
580
546
  # Return the summary of the test results
581
547
  return summary
@@ -585,95 +551,125 @@ class UnitTest(IUnitTest):
585
551
  suite: unittest.TestSuite
586
552
  ) -> List[unittest.TestCase]:
587
553
  """
588
- Recursively flattens a nested unittest.TestSuite into a list of unique unittest.TestCase instances.
554
+ Recursively flattens a (potentially nested) unittest.TestSuite into a list of unique unittest.TestCase instances.
555
+
556
+ This method traverses the provided test suite, which may contain nested suites or individual test cases,
557
+ and collects all unique TestCase instances into a flat list. It ensures that each test case appears only once
558
+ in the resulting list, based on a short identifier derived from the test's id. This is particularly useful
559
+ for operations that require direct access to all test cases, such as filtering, counting, or custom execution.
589
560
 
590
561
  Parameters
591
562
  ----------
592
563
  suite : unittest.TestSuite
593
- The test suite to flatten, which may contain nested suites or test cases.
564
+ The test suite to flatten. This can be a single suite, a nested suite, or a suite containing test cases.
594
565
 
595
566
  Returns
596
567
  -------
597
568
  List[unittest.TestCase]
598
- A list containing all unique TestCase instances extracted from the suite.
569
+ A flat list containing all unique unittest.TestCase instances found within the input suite.
599
570
 
600
571
  Notes
601
572
  -----
602
- This method traverses the given TestSuite recursively, collecting all TestCase instances
603
- and ensuring that each test appears only once in the resulting list.
573
+ - The uniqueness of test cases is determined by a "short id", which is composed of the last two segments
574
+ of the test's full id (typically "ClassName.methodName"). This helps avoid duplicate test cases in the result.
575
+ - The method uses recursion to traverse all levels of nested suites.
576
+ - Only objects with an 'id' attribute (i.e., test cases) are included in the result.
604
577
  """
605
578
  tests = []
606
579
  seen_ids = set()
607
580
 
608
581
  def _flatten(item):
582
+ """
583
+ Recursively process a TestSuite or test case, collecting unique test cases.
584
+
585
+ - If the item is a TestSuite, recursively process its children.
586
+ - If the item is a test case (has 'id'), generate a short id and add it if not already seen.
587
+ """
609
588
  if isinstance(item, unittest.TestSuite):
589
+ # Recursively flatten all sub-items in the suite
610
590
  for sub_item in item:
611
591
  _flatten(sub_item)
612
592
  elif hasattr(item, "id"):
593
+ # Generate a short id for uniqueness (e.g., "ClassName.methodName")
613
594
  test_id = item.id()
614
595
  parts = test_id.split('.')
615
596
  if len(parts) >= 2:
616
597
  short_id = '.'.join(parts[-2:])
617
598
  else:
618
599
  short_id = test_id
600
+ # Add the test case only if its short id has not been seen
619
601
  if short_id not in seen_ids:
620
602
  seen_ids.add(short_id)
621
603
  tests.append(item)
622
604
 
605
+ # Start flattening from the root suite
623
606
  _flatten(suite)
607
+
608
+ # Return a flat list of unique unittest.TestCase instances
624
609
  return tests
625
610
 
626
611
  def __runSuite(
627
612
  self
628
- ):
613
+ ) -> Tuple[unittest.TestResult, io.StringIO, io.StringIO]:
629
614
  """
630
- Run the test suite according to the selected execution mode (parallel or sequential),
631
- capturing standard output and error streams during execution.
615
+ Executes the test suite using the configured execution mode (sequential or parallel),
616
+ while capturing both standard output and error streams during the test run.
617
+
618
+ This method determines the execution mode (sequential or parallel) based on the current
619
+ configuration and delegates the actual test execution to the appropriate internal method.
620
+ It ensures that all output and error messages generated during the test run are captured
621
+ in dedicated buffers for later inspection or reporting.
632
622
 
633
623
  Returns
634
624
  -------
635
- tuple
636
- result : unittest.TestResult
637
- The result object from the test execution.
638
- output_buffer : io.StringIO
639
- Captured standard output during test execution.
640
- error_buffer : io.StringIO
641
- Captured standard error during test execution.
625
+ Tuple[unittest.TestResult, io.StringIO, io.StringIO]
626
+ A tuple containing:
627
+ - result: The unittest.TestResult object with detailed information about the test run,
628
+ including passed, failed, errored, and skipped tests.
629
+ - output_buffer: An io.StringIO object containing all captured standard output produced
630
+ during the test execution.
631
+ - error_buffer: An io.StringIO object containing all captured standard error output
632
+ produced during the test execution.
633
+
634
+ Notes
635
+ -----
636
+ - The execution mode is determined by the value of self.__execution_mode.
637
+ - Output and error streams are always captured, regardless of execution mode.
638
+ - The returned buffers can be used for further processing, logging, or displaying test output.
642
639
  """
643
640
 
644
- # Setup output capture
641
+ # Create buffers to capture standard output and error during test execution
645
642
  output_buffer = io.StringIO()
646
643
  error_buffer = io.StringIO()
647
644
 
648
- # Execute tests based on selected mode
649
- if self.execution_mode == ExecutionMode.PARALLEL.value:
650
-
651
- # Run tests in parallel
645
+ # Determine execution mode and run tests accordingly
646
+ if self.__execution_mode == ExecutionMode.PARALLEL.value:
647
+ # Run tests in parallel mode
652
648
  result = self.__runTestsInParallel(
653
649
  output_buffer,
654
650
  error_buffer
655
651
  )
656
-
657
652
  else:
658
-
659
- # Run tests sequentially
653
+ # Run tests sequentially (default)
660
654
  result = self.__runTestsSequentially(
661
655
  output_buffer,
662
656
  error_buffer
663
657
  )
664
658
 
665
- # Return the result along with captured output and error streams
659
+ # Return the test result along with the captured output and error buffers
666
660
  return result, output_buffer, error_buffer
667
661
 
668
662
  def __resolveFlattenedTestSuite(
669
663
  self
670
664
  ) -> unittest.TestSuite:
671
665
  """
672
- Resolves dependencies for all test cases in the suite and creates a flattened test suite with injected dependencies.
666
+ Resolves and injects dependencies for all test cases in the suite, returning a flattened TestSuite.
673
667
 
674
- Processes each test case, identifies dependencies in test method signatures, and resolves
675
- them using the application's dependency resolver. Creates wrapper methods that automatically
676
- inject the resolved dependencies when tests are executed.
668
+ This method processes each test case in the internal suite, inspects the test method signatures,
669
+ and uses the application's dependency resolver to inject any required dependencies. It handles
670
+ decorated methods, methods without dependencies, and raises errors for unresolved dependencies.
671
+ The result is a new, flat unittest.TestSuite containing test cases with all dependencies resolved
672
+ and injected, ready for execution.
677
673
 
678
674
  Parameters
679
675
  ----------
@@ -682,7 +678,8 @@ class UnitTest(IUnitTest):
682
678
  Returns
683
679
  -------
684
680
  unittest.TestSuite
685
- A new test suite with all tests having their dependencies resolved and injected.
681
+ A new TestSuite containing all test cases from the original suite, with dependencies injected
682
+ where required. Test cases with unresolved dependencies will cause an exception to be raised.
686
683
 
687
684
  Raises
688
685
  ------
@@ -691,53 +688,59 @@ class UnitTest(IUnitTest):
691
688
 
692
689
  Notes
693
690
  -----
694
- - Test methods with decorators are left as-is
695
- - Test methods without dependencies are added directly
696
- - Test methods with unresolved dependencies trigger an error
691
+ - Decorated test methods are left unchanged and added as-is.
692
+ - Test methods without dependencies are added directly.
693
+ - Test methods with unresolved dependencies will trigger an error.
694
+ - The returned TestSuite is flat and contains all processed test cases.
697
695
  """
698
696
 
699
- # Create a new test suite with tests that have their dependencies resolved
697
+ # Create a new test suite to hold test cases with dependencies resolved
700
698
  flattened_suite = unittest.TestSuite()
701
699
 
702
- # Iterate through all test cases
703
- for test_case in self.__flattenTestSuite(self.suite):
700
+ # Iterate through all test cases in the original (possibly nested) suite
701
+ for test_case in self.__flattenTestSuite(self.__suite):
704
702
 
705
- # Get the test method name
706
- method_name = ReflectionInstance(test_case).getAttribute("_testMethodName")
703
+ # Get the test method name using reflection
704
+ rf_instance = ReflectionInstance(test_case)
705
+ method_name = rf_instance.getAttribute("_testMethodName")
707
706
 
708
- # Is not method_name, use the original test case
707
+ # If no method name is found, add the test case as-is
709
708
  if not method_name:
710
709
  flattened_suite.addTest(test_case)
711
710
  continue
712
711
 
713
- # Get the actual method object
712
+ # Retrieve the actual test method object from the class
714
713
  test_method = getattr(test_case.__class__, method_name, None)
715
714
 
716
- # Check for all decorators on the test method
715
+ # Check if the test method is decorated by looking for __wrapped__ attributes
717
716
  decorators = []
718
-
719
- # Get decorators from the test method
720
717
  if hasattr(test_method, '__wrapped__'):
721
718
  original = test_method
722
719
  while hasattr(original, '__wrapped__'):
723
- # Try to get decorator name
720
+ # Collect decorator names for informational purposes
724
721
  if hasattr(original, '__qualname__'):
725
722
  decorators.append(original.__qualname__)
726
723
  elif hasattr(original, '__name__'):
727
724
  decorators.append(original.__name__)
728
725
  original = original.__wrapped__
729
726
 
730
- # If use decorators, use original test method
727
+ # If decorators are present, add the test case as-is (do not inject dependencies)
731
728
  if decorators:
732
729
  flattened_suite.addTest(test_case)
733
730
  continue
734
731
 
735
- # Extract dependencies for the test method
736
- signature = ReflectionInstance(test_case).getMethodDependencies(method_name)
732
+ # Attempt to extract dependency information from the test method signature
733
+ try:
734
+ signature = rf_instance.getMethodDependencies(method_name)
735
+ except Exception as e:
736
+ raise OrionisTestValueError(
737
+ f"Failed to resolve test dependencies in '../{method_name}.py. "
738
+ f"This may be caused by incorrect imports, missing modules, or undefined classes. "
739
+ f"Please verify that all test methods and their dependencies are correctly declared and accessible."
740
+ )
737
741
 
738
- # If no dependencies to resolve, just add the original test
739
- if (not signature.resolved and not signature.unresolved) or \
740
- (not signature.resolved and len(signature.unresolved) > 0):
742
+ # If there are no dependencies to resolve, or unresolved dependencies exist, add as-is
743
+ if ((not signature.resolved and not signature.unresolved) or (not signature.resolved and len(signature.unresolved) > 0)):
741
744
  flattened_suite.addTest(test_case)
742
745
  continue
743
746
 
@@ -748,32 +751,32 @@ class UnitTest(IUnitTest):
748
751
  "Please ensure all dependencies are correctly defined and available."
749
752
  )
750
753
 
751
- # Create a specialized test case with resolved dependencies
754
+ # All dependencies are resolved; prepare to inject them into the test method
752
755
  test_class = ReflectionInstance(test_case).getClass()
753
756
  original_method = getattr(test_class, method_name)
754
757
 
755
- # Create a dict of resolved dependencies
756
- params = Resolver(self.app).resolveSignature(signature)
758
+ # Resolve dependencies using the application's resolver
759
+ params = Resolver(self.__app).resolveSignature(signature)
757
760
 
758
- # Create a wrapper method that injects dependencies
759
- def create_test_wrapper(original_test, resolved_args:dict):
761
+ # Create a wrapper function that injects resolved dependencies into the test method
762
+ def create_test_wrapper(original_test, resolved_args: dict):
760
763
  def wrapper(self_instance):
761
764
  return original_test(self_instance, **resolved_args)
762
765
  return wrapper
763
766
 
764
- # Create the wrapped method with injected dependencies
767
+ # Wrap the original test method with the dependency-injecting wrapper
765
768
  wrapped_method = create_test_wrapper(original_method, params)
766
769
 
767
770
  # Bind the wrapped method to the test case instance
768
771
  bound_method = wrapped_method.__get__(test_case, test_case.__class__)
769
772
 
770
- # Replace the original test method with the wrapped method
773
+ # Replace the original test method on the test case with the wrapped version
771
774
  setattr(test_case, method_name, bound_method)
772
775
 
773
- # Add the modified test case to the suite
776
+ # Add the modified test case to the flattened suite
774
777
  flattened_suite.addTest(test_case)
775
778
 
776
- # Return the flattened suite with resolved dependencies
779
+ # Return the new flattened suite with all dependencies resolved and injected
777
780
  return flattened_suite
778
781
 
779
782
  def __runTestsSequentially(
@@ -799,14 +802,31 @@ class UnitTest(IUnitTest):
799
802
  """
800
803
 
801
804
  # Create a custom result class to capture detailed test results
802
- with redirect_stdout(output_buffer), redirect_stderr(error_buffer):
803
- runner = unittest.TextTestRunner(
804
- stream=output_buffer,
805
- verbosity=self.verbosity,
806
- failfast=self.fail_fast,
807
- resultclass=self.__customResultClass()
808
- )
809
- result = runner.run(self.__resolveFlattenedTestSuite())
805
+ result = None
806
+ for case in self.__resolveFlattenedTestSuite():
807
+
808
+ if not isinstance(case, unittest.TestCase):
809
+ raise OrionisTestValueError(
810
+ f"Invalid test case type: Expected unittest.TestCase, got {type(case).__name__}."
811
+ )
812
+
813
+ with redirect_stdout(output_buffer), redirect_stderr(error_buffer):
814
+ runner = unittest.TextTestRunner(
815
+ stream=output_buffer,
816
+ verbosity=self.__verbosity,
817
+ failfast=self.__fail_fast,
818
+ resultclass=self.__customResultClass()
819
+ )
820
+ single_result: IOrionisTestResult = runner.run(unittest.TestSuite([case]))
821
+
822
+ # Print a concise summary for each test.
823
+ self.__printer.unittestResult(single_result.test_results[0])
824
+
825
+ # Merge results
826
+ if result is None:
827
+ result = single_result
828
+ else:
829
+ self.__mergeTestResults(result, single_result)
810
830
 
811
831
  # Return the result object containing test outcomes
812
832
  return result
@@ -817,26 +837,35 @@ class UnitTest(IUnitTest):
817
837
  error_buffer: io.StringIO
818
838
  ) -> unittest.TestResult:
819
839
  """
820
- Runs all test cases in the provided test suite concurrently using a thread pool,
821
- aggregating the results into a single result object. Standard output and error
822
- are redirected to the provided buffers during execution.
840
+ Executes all test cases in the test suite concurrently using a thread pool,
841
+ aggregating their results into a single result object. Standard output and error
842
+ streams are redirected to the provided buffers during execution.
843
+
844
+ This method is designed to speed up test execution by running multiple test cases
845
+ in parallel threads, making use of the configured maximum number of workers. Each
846
+ test case is executed in isolation, and their results are merged into a combined
847
+ result object. If the `fail_fast` option is enabled and a test fails, remaining
848
+ tests are canceled as soon as possible.
823
849
 
824
850
  Parameters
825
851
  ----------
826
852
  output_buffer : io.StringIO
827
- Buffer to capture standard output during test execution.
853
+ Buffer to capture standard output produced during test execution.
828
854
  error_buffer : io.StringIO
829
- Buffer to capture standard error during test execution.
855
+ Buffer to capture standard error produced during test execution.
830
856
 
831
857
  Returns
832
858
  -------
833
859
  unittest.TestResult
834
- Combined result object containing the outcomes of all executed tests.
860
+ A combined result object (instance of the custom result class) containing
861
+ the aggregated outcomes of all executed tests, including detailed information
862
+ about passed, failed, errored, and skipped tests.
835
863
 
836
864
  Notes
837
865
  -----
838
- - Uses a custom result class to aggregate test results.
866
+ - Uses a custom result class to collect detailed test outcomes.
839
867
  - If `fail_fast` is enabled and a test fails, remaining tests are canceled.
868
+ - Output and error streams are captured for the entire parallel execution.
840
869
  """
841
870
 
842
871
  # Flatten the test suite to get individual test cases
@@ -844,24 +873,25 @@ class UnitTest(IUnitTest):
844
873
 
845
874
  # Create a custom result instance to collect all results
846
875
  result_class = self.__customResultClass()
847
- combined_result = result_class(io.StringIO(), descriptions=True, verbosity=self.verbosity)
876
+ combined_result = result_class(io.StringIO(), descriptions=True, verbosity=self.__verbosity)
848
877
 
849
878
  # Helper function to run a single test and return its result.
850
- # Minimal output for parallel runs
879
+ # Each test runs in its own thread with minimal output.
851
880
  def run_single_test(test):
852
881
  runner = unittest.TextTestRunner(
853
- stream=io.StringIO(),
882
+ stream=io.StringIO(), # Use a dummy stream for individual test output
854
883
  verbosity=0,
855
884
  failfast=False,
856
885
  resultclass=result_class
857
886
  )
887
+ # Run the test and return its result object
858
888
  return runner.run(unittest.TestSuite([test]))
859
889
 
860
- # Use ThreadPoolExecutor to run tests concurrently
890
+ # Redirect stdout and stderr to the provided buffers during parallel execution
861
891
  with redirect_stdout(output_buffer), redirect_stderr(error_buffer):
862
892
 
863
893
  # Create a ThreadPoolExecutor to run tests in parallel
864
- with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
894
+ with ThreadPoolExecutor(max_workers=self.__max_workers) as executor:
865
895
 
866
896
  # Submit all test cases to the executor
867
897
  futures = [executor.submit(run_single_test, test) for test in test_cases]
@@ -869,15 +899,20 @@ class UnitTest(IUnitTest):
869
899
  # Process the results as they complete
870
900
  for future in as_completed(futures):
871
901
  test_result = future.result()
902
+ # Merge each individual test result into the combined result
872
903
  self.__mergeTestResults(combined_result, test_result)
873
904
 
874
905
  # If fail_fast is enabled and a test failed, cancel remaining futures
875
- if self.fail_fast and not combined_result.wasSuccessful():
906
+ if self.__fail_fast and not combined_result.wasSuccessful():
876
907
  for f in futures:
877
908
  f.cancel()
878
909
  break
879
910
 
880
- # Return the combined result object
911
+ # Print a concise summary for each test in the combined result
912
+ for test_result in combined_result.test_results:
913
+ self.__printer.unittestResult(test_result)
914
+
915
+ # Return the combined result object containing all test outcomes
881
916
  return combined_result
882
917
 
883
918
  def __mergeTestResults(
@@ -886,34 +921,44 @@ class UnitTest(IUnitTest):
886
921
  individual_result: unittest.TestResult
887
922
  ) -> None:
888
923
  """
889
- Merge the results of two unittest.TestResult objects.
924
+ Merge the results of two unittest.TestResult objects into a single result.
890
925
 
891
- This method updates the `combined_result` object by adding the test run counts,
892
- failures, errors, skipped tests, expected failures, and unexpected successes
893
- from the `individual_result` object. Additionally, it merges any custom test
894
- results stored in the `test_results` attribute, if present.
926
+ This method updates the `combined_result` object by aggregating the test run counts,
927
+ failures, errors, skipped tests, expected failures, and unexpected successes from the
928
+ `individual_result` object. It also merges any custom test results stored in the
929
+ `test_results` attribute, if present, ensuring that all detailed test outcomes are
930
+ included in the combined result.
895
931
 
896
932
  Parameters
897
933
  ----------
898
934
  combined_result : unittest.TestResult
899
- The TestResult object to which the results will be merged.
935
+ The TestResult object that will be updated to include the results from `individual_result`.
900
936
  individual_result : unittest.TestResult
901
- The TestResult object containing the results to be merged into the combined_result.
937
+ The TestResult object whose results will be merged into `combined_result`.
902
938
 
903
939
  Returns
904
940
  -------
905
941
  None
942
+ This method does not return a value. It modifies `combined_result` in place.
943
+
944
+ Notes
945
+ -----
946
+ - The method aggregates all relevant test outcome lists and counters.
947
+ - If the `test_results` attribute exists (for custom result classes), it is also merged.
948
+ - This is useful for combining results from parallel or sequential test executions.
906
949
  """
907
950
 
908
- # Update the combined result with counts and lists from the individual result
951
+ # Aggregate the number of tests run
909
952
  combined_result.testsRun += individual_result.testsRun
953
+
954
+ # Extend the lists of failures, errors, skipped, expected failures, and unexpected successes
910
955
  combined_result.failures.extend(individual_result.failures)
911
956
  combined_result.errors.extend(individual_result.errors)
912
957
  combined_result.skipped.extend(individual_result.skipped)
913
958
  combined_result.expectedFailures.extend(individual_result.expectedFailures)
914
959
  combined_result.unexpectedSuccesses.extend(individual_result.unexpectedSuccesses)
915
960
 
916
- # Merge our custom test results
961
+ # Merge custom test results if present (for enhanced result tracking)
917
962
  if hasattr(individual_result, 'test_results'):
918
963
  if not hasattr(combined_result, 'test_results'):
919
964
  combined_result.test_results = []
@@ -1048,30 +1093,38 @@ class UnitTest(IUnitTest):
1048
1093
  traceback_str: str
1049
1094
  ) -> Tuple[Optional[str], Optional[str]]:
1050
1095
  """
1051
- Extract error information from a traceback string.
1052
- This method processes a traceback string to extract the file path of the Python file where the error occurred and
1053
- cleans up the traceback by removing framework internals and irrelevant noise.
1096
+ Extracts the file path and a cleaned traceback from a given traceback string.
1097
+
1098
+ This method analyzes a Python traceback string to determine the file path of the Python file
1099
+ where the error occurred (typically the last file in the traceback). It also removes lines
1100
+ related to framework internals and irrelevant noise, such as those containing 'unittest/',
1101
+ 'lib/python', or 'site-packages', to produce a more concise and relevant traceback for reporting.
1054
1102
 
1055
1103
  Parameters
1056
1104
  ----------
1057
1105
  traceback_str : str
1058
- The traceback string to process.
1106
+ The full traceback string to process.
1059
1107
 
1060
1108
  Returns
1061
1109
  -------
1062
1110
  Tuple[Optional[str], Optional[str]]
1063
1111
  A tuple containing:
1112
+ - file_path (Optional[str]): The path to the Python file where the error occurred, or None if not found.
1113
+ - clean_tb (Optional[str]): The cleaned traceback string, with framework internals and unrelated lines removed.
1064
1114
 
1065
1115
  Notes
1066
1116
  -----
1067
- Framework internals and lines containing 'unittest/', 'lib/python', or 'site-packages' are removed from the traceback.
1068
- The cleaned traceback starts from the first occurrence of the test file path.
1117
+ The cleaned traceback starts from the first occurrence of the test file path and omits lines
1118
+ that are part of the Python standard library or third-party packages, focusing on user code.
1069
1119
  """
1070
- # Extract file path
1120
+
1121
+ # Extract all Python file paths from the traceback string
1071
1122
  file_matches = re.findall(r'File ["\'](.*?.py)["\']', traceback_str)
1123
+
1124
+ # Use the last file in the traceback as the most relevant (where the error occurred)
1072
1125
  file_path = file_matches[-1] if file_matches else None
1073
1126
 
1074
- # Clean up traceback by removing framework internals and noise
1127
+ # Split the traceback into individual lines for processing
1075
1128
  tb_lines = traceback_str.split('\n')
1076
1129
  clean_lines = []
1077
1130
  relevant_lines_started = False
@@ -1079,19 +1132,22 @@ class UnitTest(IUnitTest):
1079
1132
  # Iterate through each line in the traceback
1080
1133
  for line in tb_lines:
1081
1134
 
1082
- # Skip framework internal lines
1135
+ # Skip lines that are part of framework internals or third-party libraries
1083
1136
  if any(s in line for s in ['unittest/', 'lib/python', 'site-packages']):
1084
1137
  continue
1085
1138
 
1086
- # Start capturing when we hit the test file
1139
+ # Start including lines once the relevant file path is found
1087
1140
  if file_path and file_path in line and not relevant_lines_started:
1088
1141
  relevant_lines_started = True
1089
1142
 
1143
+ # If we've started collecting relevant lines, add them to the cleaned traceback
1090
1144
  if relevant_lines_started:
1091
1145
  clean_lines.append(line)
1092
1146
 
1147
+ # Join the cleaned lines into a single string; if none, return the original traceback
1093
1148
  clean_tb = str('\n').join(clean_lines) if clean_lines else traceback_str
1094
1149
 
1150
+ # Return the file path and cleaned traceback
1095
1151
  return file_path, clean_tb
1096
1152
 
1097
1153
  def __generateSummary(
@@ -1100,56 +1156,51 @@ class UnitTest(IUnitTest):
1100
1156
  execution_time: float
1101
1157
  ) -> Dict[str, Any]:
1102
1158
  """
1103
- Generate a summary of the test results, including statistics and details for each test.
1159
+ Generates a comprehensive summary of the test suite execution.
1160
+
1161
+ This method processes the provided unittest.TestResult object and aggregates
1162
+ statistics such as the total number of tests, counts of passed, failed, errored,
1163
+ and skipped tests, as well as the overall execution time and success rate.
1164
+ It also collects detailed information for each individual test, including
1165
+ identifiers, class and method names, status, execution time, error messages,
1166
+ tracebacks, file paths, and docstrings.
1167
+
1168
+ If result persistence is enabled, the summary is saved using the configured
1169
+ persistence driver (e.g., SQLite or JSON). If web reporting is enabled, a
1170
+ web report is generated and linked.
1104
1171
 
1105
1172
  Parameters
1106
1173
  ----------
1107
1174
  result : unittest.TestResult
1108
1175
  The result object containing details of the test execution.
1109
1176
  execution_time : float
1110
- The total execution time of the test suite in seconds.
1177
+ The total execution time of the test suite in milliseconds.
1111
1178
 
1112
1179
  Returns
1113
1180
  -------
1114
1181
  Dict[str, Any]
1115
- A dictionary containing the following keys:
1116
- total_tests : int
1117
- The total number of tests executed.
1118
- passed : int
1119
- The number of tests that passed.
1120
- failed : int
1121
- The number of tests that failed.
1122
- errors : int
1123
- The number of tests that encountered errors.
1124
- skipped : int
1125
- The number of tests that were skipped.
1126
- total_time : float
1127
- The total execution time of the test suite.
1128
- success_rate : float
1129
- The percentage of tests that passed.
1130
- test_details : List[Dict[str, Any]]
1131
- A list of dictionaries with details for each test, including:
1132
- id : str
1133
- The unique identifier of the test.
1134
- class : str
1135
- The class name of the test.
1136
- method : str
1137
- The method name of the test.
1138
- status : str
1139
- The status of the test (e.g., "PASSED", "FAILED").
1140
- execution_time : float
1141
- The execution time of the test in seconds.
1142
- error_message : str
1143
- The error message if the test failed or errored.
1144
- traceback : str
1145
- The traceback information if the test failed or errored.
1146
- file_path : str
1147
- The file path of the test.
1148
- doc_string : str
1149
- The docstring of the test method, if available.
1182
+ A dictionary containing:
1183
+ - total_tests (int): Total number of tests executed.
1184
+ - passed (int): Number of tests that passed.
1185
+ - failed (int): Number of tests that failed.
1186
+ - errors (int): Number of tests that encountered errors.
1187
+ - skipped (int): Number of tests that were skipped.
1188
+ - total_time (float): Total execution time in milliseconds.
1189
+ - success_rate (float): Percentage of tests that passed.
1190
+ - test_details (List[Dict[str, Any]]): List of dictionaries with details for each test,
1191
+ including id, class, method, status, execution_time, error_message, traceback,
1192
+ file_path, and doc_string.
1193
+ - timestamp (str): ISO-formatted timestamp of when the summary was generated.
1194
+
1195
+ Side Effects
1196
+ ------------
1197
+ - If persistence is enabled, the summary is persisted to storage.
1198
+ - If web reporting is enabled, a web report is generated.
1150
1199
  """
1200
+
1151
1201
  test_details = []
1152
1202
 
1203
+ # Collect detailed information for each test result
1153
1204
  for test_result in result.test_results:
1154
1205
  rst: TestResult = test_result
1155
1206
  test_details.append({
@@ -1164,10 +1215,12 @@ class UnitTest(IUnitTest):
1164
1215
  'doc_string': rst.doc_string
1165
1216
  })
1166
1217
 
1218
+ # Calculate the number of passed tests
1167
1219
  passed = result.testsRun - len(result.failures) - len(result.errors) - len(result.skipped)
1220
+ # Calculate the success rate as a percentage
1168
1221
  success_rate = (passed / result.testsRun * 100) if result.testsRun > 0 else 100.0
1169
1222
 
1170
- # Create a summary report
1223
+ # Build the summary dictionary
1171
1224
  self.__result = {
1172
1225
  "total_tests": result.testsRun,
1173
1226
  "passed": passed,
@@ -1180,15 +1233,15 @@ class UnitTest(IUnitTest):
1180
1233
  "timestamp": datetime.now().isoformat()
1181
1234
  }
1182
1235
 
1183
- # Handle persistence of the report
1184
- if self.persistent:
1236
+ # Persist the summary if persistence is enabled
1237
+ if self.__persistent:
1185
1238
  self.__handlePersistResults(self.__result)
1186
1239
 
1187
- # Handle Web Report Rendering
1188
- if self.web_report:
1240
+ # Generate a web report if web reporting is enabled
1241
+ if self.__web_report:
1189
1242
  self.__handleWebReport(self.__result)
1190
1243
 
1191
- # Return the summary
1244
+ # Return the summary dictionary
1192
1245
  return self.__result
1193
1246
 
1194
1247
  def __handleWebReport(
@@ -1196,102 +1249,103 @@ class UnitTest(IUnitTest):
1196
1249
  summary: Dict[str, Any]
1197
1250
  ) -> None:
1198
1251
  """
1199
- Generates a web report for the test results summary.
1252
+ Generate a web-based report for the provided test results summary.
1253
+
1254
+ This method creates a web report for the test execution summary using the `TestingResultRender` class.
1255
+ It determines the appropriate storage path for the report, configures persistence options based on the
1256
+ current settings, and invokes the rendering process. After generating the report, it prints a link to
1257
+ the web report using the configured printer.
1200
1258
 
1201
1259
  Parameters
1202
1260
  ----------
1203
1261
  summary : dict
1204
- The summary of test results to generate a web report for.
1262
+ The summary of test results for which the web report will be generated.
1205
1263
 
1206
1264
  Returns
1207
1265
  -------
1208
- str
1209
- The path to the generated web report.
1266
+ None
1267
+ This method does not return any value. The generated web report is rendered and a link to it is printed
1268
+ to the console via the printer.
1210
1269
 
1211
1270
  Notes
1212
1271
  -----
1213
- - Determines the storage path based on the current working directory and base_path.
1214
- - Uses TestingResultRender to generate the report.
1215
- - If persistence is enabled and the driver is 'sqlite', the report is marked as persistent.
1216
- - Returns the path to the generated report for further use.
1272
+ - The storage path for the report is determined by `self.__base_path`.
1273
+ - If result persistence is enabled and the driver is set to 'sqlite', the report is marked as persistent.
1274
+ - The web report is generated using the `TestingResultRender` class.
1275
+ - The method prints the link to the generated web report using the printer.
1217
1276
  """
1218
1277
 
1219
- # Determine the absolute path for storing results
1220
- project = os.path.basename(os.getcwd())
1221
- storage_path = os.path.abspath(os.path.join(os.getcwd(), self.base_path))
1222
-
1223
- # Only use storage_path if project is recognized
1224
- if project not in ['framework', 'orionis']:
1225
- storage_path = None
1226
-
1227
- # Create the TestingResultRender instance with the storage path and summary
1278
+ # Create the TestingResultRender instance with the storage path and summary.
1228
1279
  render = TestingResultRender(
1229
- storage_path=storage_path,
1280
+ storage_path=self.__storage,
1230
1281
  result=summary,
1231
- persist=self.persistent and self.persistent_driver == 'sqlite'
1282
+ persist=self.__persistent and self.__persistent_driver == 'sqlite'
1232
1283
  )
1233
1284
 
1234
- # Render the report and print the web report link
1235
- self.printer.linkWebReport(render.render())
1285
+ # Render the web report and print the link using the printer.
1286
+ self.__printer.linkWebReport(render.render())
1236
1287
 
1237
1288
  def __handlePersistResults(
1238
1289
  self,
1239
1290
  summary: Dict[str, Any]
1240
1291
  ) -> None:
1241
1292
  """
1242
- Persist the test results summary using the configured persistent driver.
1293
+ Persist the test results summary using the configured persistence driver.
1294
+
1295
+ This method saves the provided test results summary to persistent storage, based on the
1296
+ current configuration. Supported drivers include SQLite (using the TestLogs class) and
1297
+ JSON file output. The storage location is determined by the configured base path.
1243
1298
 
1244
1299
  Parameters
1245
1300
  ----------
1246
1301
  summary : dict
1247
- The summary of test results to persist.
1302
+ The summary of test results to persist. This should include all relevant test execution
1303
+ details, such as test counts, statuses, execution times, and individual test results.
1248
1304
 
1249
- Notes
1250
- -----
1251
- Depending on the value of `self.persistent_driver`, the summary is either:
1252
- - Stored in an SQLite database (using the TestLogs class), or
1253
- - Written to a timestamped JSON file in the specified base path.
1305
+ Returns
1306
+ -------
1307
+ None
1308
+ This method does not return any value. It performs persistence as a side effect.
1254
1309
 
1255
1310
  Raises
1256
1311
  ------
1257
1312
  OSError
1258
- If there is an error creating directories or writing files.
1259
- Exception
1260
- If database operations fail.
1313
+ If there is an error creating directories or writing files to disk.
1314
+ OrionisTestPersistenceError
1315
+ If database operations fail or any other error occurs during persistence.
1316
+
1317
+ Notes
1318
+ -----
1319
+ - If `self.__persistent_driver` is set to 'sqlite', the summary is stored in an SQLite database
1320
+ using the TestLogs class.
1321
+ - If `self.__persistent_driver` is set to 'json', the summary is written to a timestamped JSON
1322
+ file in the specified base path.
1323
+ - The method ensures that the target directory exists before writing files.
1324
+ - Any errors encountered during persistence are raised as exceptions for the caller to handle.
1261
1325
  """
1262
1326
 
1263
1327
  try:
1264
1328
 
1265
- # Determine the absolute path for storing results
1266
- project = os.getcwd().split(os.sep)[-1]
1267
- storage_path = None
1268
- if project in ['framework', 'orionis']:
1269
- storage_path = os.path.abspath(os.path.join(os.getcwd(), self.base_path))
1270
-
1271
- if self.persistent_driver == 'sqlite':
1329
+ if self.__persistent_driver == PersistentDrivers.SQLITE.value:
1272
1330
 
1273
- # Initialize the TestLogs class for database operations
1274
- history = TestLogs(
1275
- storage_path=storage_path,
1276
- db_name='tests.sqlite',
1277
- table_name='reports'
1278
- )
1331
+ # Persist results to SQLite database using TestLogs
1332
+ history = TestLogs(self.__storage)
1279
1333
 
1280
1334
  # Insert the summary into the database
1281
1335
  history.create(summary)
1282
1336
 
1283
- elif self.persistent_driver == 'json':
1337
+ elif self.__persistent_driver == PersistentDrivers.JSON.value:
1284
1338
 
1285
- # Ensure the base path exists and write the summary to a JSON file
1286
- os.makedirs(storage_path, exist_ok=True)
1287
-
1288
- # Get the current timestamp for the log file name
1339
+ # Generate a timestamp for the log file name
1289
1340
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
1290
1341
 
1291
- # Create the log file path with the timestamp
1292
- log_path = os.path.abspath(os.path.join(storage_path, f'test_{timestamp}.json'))
1342
+ # Construct the log file path with the timestamp
1343
+ log_path = Path(self.__storage) / f"{timestamp}_test_results.json"
1344
+
1345
+ # Ensure the directory exists
1346
+ log_path.parent.mkdir(parents=True, exist_ok=True)
1293
1347
 
1294
- # Write the summary to the JSON file
1348
+ # Write the summary dictionary to the JSON file
1295
1349
  with open(log_path, 'w', encoding='utf-8') as log:
1296
1350
  json.dump(summary, log, indent=4)
1297
1351
 
@@ -1302,7 +1356,7 @@ class UnitTest(IUnitTest):
1302
1356
 
1303
1357
  except Exception as e:
1304
1358
 
1305
- # Raise a general exception for any other issues during persistence
1359
+ # Raise a custom exception for any other issues during persistence
1306
1360
  raise OrionisTestPersistenceError(f"Error persisting test results: {str(e)}")
1307
1361
 
1308
1362
  def __filterTestsByName(
@@ -1411,7 +1465,7 @@ class UnitTest(IUnitTest):
1411
1465
  List[str]
1412
1466
  List of test names (unique identifiers) from the test suite.
1413
1467
  """
1414
- return [test.id() for test in self.__flattenTestSuite(self.suite)]
1468
+ return [test.id() for test in self.__flattenTestSuite(self.__suite)]
1415
1469
 
1416
1470
  def getTestCount(
1417
1471
  self
@@ -1424,7 +1478,7 @@ class UnitTest(IUnitTest):
1424
1478
  int
1425
1479
  The total number of individual test cases in the suite.
1426
1480
  """
1427
- return len(list(self.__flattenTestSuite(self.suite)))
1481
+ return len(list(self.__flattenTestSuite(self.__suite)))
1428
1482
 
1429
1483
  def clearTests(
1430
1484
  self
@@ -1434,7 +1488,7 @@ class UnitTest(IUnitTest):
1434
1488
 
1435
1489
  Resets the internal test suite to an empty `unittest.TestSuite`, removing any previously added tests.
1436
1490
  """
1437
- self.suite = unittest.TestSuite()
1491
+ self.__suite = unittest.TestSuite()
1438
1492
 
1439
1493
  def getResult(
1440
1494
  self
@@ -1469,7 +1523,7 @@ class UnitTest(IUnitTest):
1469
1523
  Prints the contents of the output buffer to the console.
1470
1524
  This method retrieves the output buffer and prints its contents using the rich console.
1471
1525
  """
1472
- self.printer.print(self.__output_buffer)
1526
+ self.__printer.print(self.__output_buffer)
1473
1527
 
1474
1528
  def getErrorBuffer(
1475
1529
  self
@@ -1491,4 +1545,4 @@ class UnitTest(IUnitTest):
1491
1545
  Prints the contents of the error buffer to the console.
1492
1546
  This method retrieves the error buffer and prints its contents using the rich console.
1493
1547
  """
1494
- self.printer.print(self.__error_buffer)
1548
+ self.__printer.print(self.__error_buffer)