orionis 0.588.0__py3-none-any.whl → 0.590.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/core/reactor.py +4 -2
- orionis/foundation/application.py +52 -13
- orionis/foundation/config/testing/entities/testing.py +0 -23
- orionis/foundation/contracts/application.py +27 -0
- orionis/metadata/framework.py +1 -1
- orionis/support/performance/contracts/counter.py +102 -5
- orionis/support/performance/counter.py +118 -8
- orionis/test/contracts/unit_test.py +0 -156
- orionis/test/core/unit_test.py +351 -409
- orionis/test/kernel.py +45 -60
- orionis/test/validators/__init__.py +0 -2
- {orionis-0.588.0.dist-info → orionis-0.590.0.dist-info}/METADATA +1 -1
- {orionis-0.588.0.dist-info → orionis-0.590.0.dist-info}/RECORD +16 -17
- orionis/test/validators/tags.py +0 -36
- {orionis-0.588.0.dist-info → orionis-0.590.0.dist-info}/WHEEL +0 -0
- {orionis-0.588.0.dist-info → orionis-0.590.0.dist-info}/licenses/LICENCE +0 -0
- {orionis-0.588.0.dist-info → orionis-0.590.0.dist-info}/top_level.txt +0 -0
orionis/test/core/unit_test.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
import io
|
2
2
|
import json
|
3
|
-
|
3
|
+
import os
|
4
4
|
import re
|
5
5
|
import time
|
6
6
|
import traceback
|
@@ -8,13 +8,16 @@ import unittest
|
|
8
8
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
9
9
|
from contextlib import redirect_stdout, redirect_stderr
|
10
10
|
from datetime import datetime
|
11
|
+
from importlib import import_module
|
12
|
+
from os import walk
|
11
13
|
from pathlib import Path
|
12
14
|
from typing import Any, Dict, List, Optional, Tuple
|
15
|
+
from orionis.foundation.config.testing.entities.testing import Testing
|
13
16
|
from orionis.foundation.config.testing.enums.drivers import PersistentDrivers
|
14
17
|
from orionis.foundation.config.testing.enums.mode import ExecutionMode
|
15
|
-
from orionis.foundation.config.testing.enums.verbosity import VerbosityMode
|
16
18
|
from orionis.foundation.contracts.application import IApplication
|
17
19
|
from orionis.services.introspection.instances.reflection import ReflectionInstance
|
20
|
+
from orionis.support.performance.contracts.counter import IPerformanceCounter
|
18
21
|
from orionis.test.contracts.test_result import IOrionisTestResult
|
19
22
|
from orionis.test.contracts.unit_test import IUnitTest
|
20
23
|
from orionis.test.entities.result import TestResult
|
@@ -33,11 +36,10 @@ from orionis.test.validators import (
|
|
33
36
|
ValidPersistentDriver,
|
34
37
|
ValidPersistent,
|
35
38
|
ValidPrintResult,
|
36
|
-
ValidTags,
|
37
39
|
ValidThrowException,
|
38
40
|
ValidVerbosity,
|
39
41
|
ValidWebReport,
|
40
|
-
ValidWorkers
|
42
|
+
ValidWorkers,
|
41
43
|
)
|
42
44
|
from orionis.test.view.render import TestingResultRender
|
43
45
|
|
@@ -47,7 +49,22 @@ class UnitTest(IUnitTest):
|
|
47
49
|
|
48
50
|
Advanced unit testing manager for the Orionis framework.
|
49
51
|
|
50
|
-
This class provides mechanisms for discovering, executing, and reporting unit tests with extensive configurability.
|
52
|
+
This class provides mechanisms for discovering, executing, and reporting unit tests with extensive configurability.
|
53
|
+
It supports sequential and parallel execution, test filtering by name or tags, and detailed result tracking including
|
54
|
+
execution times, error messages, and tracebacks. The UnitTest manager integrates with the Orionis application for
|
55
|
+
dependency injection, configuration loading, and result persistence.
|
56
|
+
|
57
|
+
Parameters
|
58
|
+
----------
|
59
|
+
app : IApplication
|
60
|
+
The application instance used for dependency injection, configuration access, and path resolution.
|
61
|
+
|
62
|
+
Notes
|
63
|
+
-----
|
64
|
+
- The application instance is stored for later use in dependency resolution and configuration access.
|
65
|
+
- The test loader and suite are initialized for test discovery and execution.
|
66
|
+
- Output buffers, paths, configuration, modules, and tests are loaded in sequence to prepare the test manager.
|
67
|
+
- Provides methods for running tests, retrieving results, and printing output/error buffers.
|
51
68
|
"""
|
52
69
|
|
53
70
|
def __init__(
|
@@ -55,445 +72,394 @@ class UnitTest(IUnitTest):
|
|
55
72
|
app: IApplication
|
56
73
|
) -> None:
|
57
74
|
"""
|
58
|
-
Initialize
|
75
|
+
Initialize the UnitTest manager for the Orionis framework.
|
76
|
+
|
77
|
+
This constructor sets up the internal state required for advanced unit testing,
|
78
|
+
including dependency injection, configuration loading, test discovery, and result tracking.
|
79
|
+
It initializes the application instance, test loader, test suite, module list, and result storage.
|
80
|
+
The constructor also loads output buffers, paths, configuration, test modules, and discovered tests.
|
59
81
|
|
60
|
-
|
82
|
+
Parameters
|
83
|
+
----------
|
84
|
+
app : IApplication
|
85
|
+
The application instance used for dependency injection, configuration access, and path resolution.
|
61
86
|
|
62
87
|
Returns
|
63
88
|
-------
|
64
89
|
None
|
90
|
+
This method does not return a value. It initializes the internal state of the UnitTest instance.
|
91
|
+
|
92
|
+
Notes
|
93
|
+
-----
|
94
|
+
- The application instance is stored for later use in dependency resolution and configuration access.
|
95
|
+
- The test loader and suite are initialized for test discovery and execution.
|
96
|
+
- Output buffers, paths, configuration, modules, and tests are loaded in sequence to prepare the test manager.
|
65
97
|
"""
|
66
98
|
|
67
|
-
#
|
99
|
+
# Store the application instance for dependency injection and configuration access
|
68
100
|
self.__app: IApplication = app
|
69
101
|
|
70
|
-
#
|
71
|
-
self.__storage: Optional[str] = self.__app.path('testing')
|
72
|
-
|
73
|
-
# Configuration values (set via configure)
|
74
|
-
self.__verbosity: Optional[int] = None
|
75
|
-
self.__execution_mode: Optional[str] = None
|
76
|
-
self.__max_workers: Optional[int] = None
|
77
|
-
self.__fail_fast: Optional[bool] = None
|
78
|
-
self.__throw_exception: Optional[bool] = None
|
79
|
-
self.__persistent: Optional[bool] = None
|
80
|
-
self.__persistent_driver: Optional[str] = None
|
81
|
-
self.__web_report: Optional[bool] = None
|
82
|
-
|
83
|
-
# Test discovery parameters for folders
|
84
|
-
self.__folder_path: Optional[str] = None
|
85
|
-
self.__base_path: Optional[str] = None
|
86
|
-
self.__pattern: Optional[str] = None
|
87
|
-
self.__test_name_pattern: Optional[str] = None
|
88
|
-
self.__tags: Optional[List[str]] = None
|
89
|
-
|
90
|
-
# Test discovery parameter for modules
|
91
|
-
self.__module_name: Optional[str] = None
|
92
|
-
|
93
|
-
# Initialize the unittest loader and suite for test discovery and execution
|
102
|
+
# Initialize the unittest loader for discovering test cases
|
94
103
|
self.__loader = unittest.TestLoader()
|
104
|
+
|
105
|
+
# Initialize the test suite to hold discovered tests
|
95
106
|
self.__suite = unittest.TestSuite()
|
107
|
+
|
108
|
+
# List to store imported test modules
|
109
|
+
self.__modules: List = []
|
110
|
+
|
111
|
+
# List to track discovered tests and their metadata
|
96
112
|
self.__discovered_tests: List = []
|
97
113
|
|
98
|
-
#
|
99
|
-
self.
|
114
|
+
# Variable to store the result summary after test execution
|
115
|
+
self.__result: Optional[Dict[str, Any]] = None
|
100
116
|
|
101
|
-
#
|
102
|
-
self.
|
103
|
-
self.__error_buffer = None
|
117
|
+
# Load the output and error buffers for capturing test execution output
|
118
|
+
self.__loadOutputBuffer()
|
104
119
|
|
105
|
-
#
|
106
|
-
self.
|
120
|
+
# Load and set internal paths for test discovery and result storage
|
121
|
+
self.__loadPaths()
|
107
122
|
|
108
|
-
|
109
|
-
self
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
) -> 'UnitTest':
|
123
|
+
# Load and validate the testing configuration from the application
|
124
|
+
self.__loadConfig()
|
125
|
+
|
126
|
+
# Discover and import test modules based on the configuration
|
127
|
+
self.__loadModules()
|
128
|
+
|
129
|
+
# Discover and load all test cases from the imported modules into the suite
|
130
|
+
self.__loadTests()
|
131
|
+
|
132
|
+
def __loadOutputBuffer(
|
133
|
+
self
|
134
|
+
) -> None:
|
121
135
|
"""
|
122
|
-
|
136
|
+
Load the output buffer from the last test execution.
|
137
|
+
|
138
|
+
This method retrieves the output buffer containing standard output generated during
|
139
|
+
the last test run. It stores the output as a string in an internal attribute for later access.
|
123
140
|
|
124
141
|
Parameters
|
125
142
|
----------
|
126
|
-
|
127
|
-
Verbosity level for test output.
|
128
|
-
execution_mode : str or ExecutionMode
|
129
|
-
Execution mode ('SEQUENTIAL' or 'PARALLEL').
|
130
|
-
max_workers : int
|
131
|
-
Maximum number of workers for parallel execution.
|
132
|
-
fail_fast : bool
|
133
|
-
Whether to stop on the first failure.
|
134
|
-
print_result : bool
|
135
|
-
Whether to print results to the console.
|
136
|
-
throw_exception : bool
|
137
|
-
Whether to raise exceptions on test failures.
|
138
|
-
persistent : bool
|
139
|
-
Whether to enable result persistence.
|
140
|
-
persistent_driver : str or PersistentDrivers
|
141
|
-
Persistence driver ('sqlite' or 'json').
|
142
|
-
web_report : bool
|
143
|
-
Whether to enable web report generation.
|
143
|
+
None
|
144
144
|
|
145
145
|
Returns
|
146
146
|
-------
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
Raises
|
151
|
-
------
|
152
|
-
OrionisTestValueError
|
153
|
-
If any parameter is invalid.
|
147
|
+
None
|
148
|
+
This method does not return a value. It sets the internal output buffer attribute.
|
154
149
|
"""
|
150
|
+
self.__output_buffer = None
|
151
|
+
self.__error_buffer = None
|
155
152
|
|
156
|
-
|
157
|
-
self
|
158
|
-
|
159
|
-
self.__max_workers = ValidWorkers(max_workers)
|
160
|
-
self.__fail_fast = ValidFailFast(fail_fast)
|
161
|
-
self.__throw_exception = ValidThrowException(throw_exception)
|
162
|
-
self.__persistent = ValidPersistent(persistent)
|
163
|
-
self.__persistent_driver = ValidPersistentDriver(persistent_driver)
|
164
|
-
self.__web_report = ValidWebReport(web_report)
|
165
|
-
|
166
|
-
# Initialize the result printer with the current configuration
|
167
|
-
self.__printer = TestPrinter(
|
168
|
-
print_result = ValidPrintResult(print_result)
|
169
|
-
)
|
170
|
-
|
171
|
-
# Return the instance to allow method chaining
|
172
|
-
return self
|
173
|
-
|
174
|
-
def discoverTests(
|
175
|
-
self,
|
176
|
-
base_path: str | Path,
|
177
|
-
folder_path: str | List[str],
|
178
|
-
pattern: str,
|
179
|
-
test_name_pattern: Optional[str] = None,
|
180
|
-
tags: Optional[List[str]] = None
|
181
|
-
) -> 'UnitTest':
|
153
|
+
def __loadPaths(
|
154
|
+
self
|
155
|
+
) -> None:
|
182
156
|
"""
|
183
|
-
|
157
|
+
Load and set internal paths required for test discovery and result storage.
|
184
158
|
|
185
|
-
This method
|
186
|
-
|
187
|
-
and
|
188
|
-
directory and discovers all folders containing files matching the specified pattern.
|
159
|
+
This method retrieves the base test path, project root path, and storage path from the application instance.
|
160
|
+
It then sets the internal attributes for the test path, root path, base path (relative to the project root),
|
161
|
+
and the absolute storage path for test results.
|
189
162
|
|
190
163
|
Parameters
|
191
164
|
----------
|
192
|
-
|
193
|
-
Base directory path for resolving relative folder paths. This serves as the root
|
194
|
-
directory from which all folder searches are conducted.
|
195
|
-
folder_path : str or list of str
|
196
|
-
Specification of folders to search for test cases. Can be:
|
197
|
-
- '*' : Discover all folders containing matching files within base_path
|
198
|
-
- str : Single folder path relative to base_path
|
199
|
-
- list of str : Multiple folder paths relative to base_path
|
200
|
-
pattern : str
|
201
|
-
File name pattern to match test files, supporting wildcards (* and ?).
|
202
|
-
Examples: 'test_*.py', '*_test.py', 'test*.py'
|
203
|
-
test_name_pattern : str, optional
|
204
|
-
Regular expression pattern to filter test method names. Only tests whose
|
205
|
-
names match this pattern will be included. Default is None (no filtering).
|
206
|
-
tags : list of str, optional
|
207
|
-
List of tags to filter tests. Only tests decorated with matching tags
|
208
|
-
will be included. Default is None (no tag filtering).
|
165
|
+
None
|
209
166
|
|
210
167
|
Returns
|
211
168
|
-------
|
212
|
-
|
213
|
-
|
214
|
-
enabling method chaining.
|
169
|
+
None
|
170
|
+
This method does not return any value. It sets internal attributes for test and storage paths.
|
215
171
|
|
216
172
|
Notes
|
217
173
|
-----
|
218
|
-
-
|
219
|
-
-
|
220
|
-
- The method uses the existing discoverTestsInFolder method for actual test discovery
|
221
|
-
- Duplicate folders are automatically eliminated using a set data structure
|
222
|
-
- The method does not validate the existence of specified folders; validation
|
223
|
-
occurs during the actual test discovery process
|
174
|
+
- The base path is computed as the relative path from the test directory to the project root.
|
175
|
+
- The storage path is set to an absolute path for storing test results under 'testing/results'.
|
224
176
|
"""
|
225
|
-
# Resolve the base path as an absolute path from the current working directory
|
226
|
-
base_path = (Path.cwd() / base_path).resolve()
|
227
|
-
|
228
|
-
# Use a set to store discovered folders and automatically eliminate duplicates
|
229
|
-
discovered_folders = set()
|
230
177
|
|
231
|
-
#
|
232
|
-
|
178
|
+
# Get the base test path and project root path from the application
|
179
|
+
self.__test_path = ValidBasePath(self.__app.path('tests'))
|
180
|
+
self.__root_path = ValidBasePath(self.__app.path('root'))
|
233
181
|
|
234
|
-
|
235
|
-
|
182
|
+
# Compute the base path for test discovery, relative to the project root
|
183
|
+
# Remove the root path prefix and leading slash
|
184
|
+
self.__base_path: Optional[str] = self.__test_path.as_posix().replace(self.__root_path.as_posix(), '')[1:]
|
236
185
|
|
237
|
-
#
|
238
|
-
|
239
|
-
|
240
|
-
# Resolve each custom folder path relative to the base path
|
241
|
-
custom_path = (base_path / custom).resolve()
|
242
|
-
# Add all matching folders found within this custom path
|
243
|
-
discovered_folders.update(self.__listMatchingFolders(base_path, custom_path, pattern))
|
244
|
-
|
245
|
-
# Handle single folder path: process the single specified folder
|
246
|
-
else:
|
186
|
+
# Get the storage path from the application and set the absolute path for test results
|
187
|
+
storage_path = self.__app.path('storage')
|
188
|
+
self.__storage: Path = (storage_path / 'testing' / 'results').resolve()
|
247
189
|
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
discovered_folders.update(self.__listMatchingFolders(base_path, custom_path, pattern))
|
252
|
-
|
253
|
-
# Iterate through all discovered folders and perform test discovery
|
254
|
-
for folder in discovered_folders:
|
255
|
-
|
256
|
-
# Use the existing discoverTestsInFolder method to actually discover and load tests
|
257
|
-
self.discoverTestsInFolder(
|
258
|
-
base_path=base_path,
|
259
|
-
folder_path=folder,
|
260
|
-
pattern=pattern,
|
261
|
-
test_name_pattern=test_name_pattern or None,
|
262
|
-
tags=tags or None
|
263
|
-
)
|
264
|
-
|
265
|
-
# Return the current instance to enable method chaining
|
266
|
-
return self
|
267
|
-
|
268
|
-
def discoverTestsInFolder(
|
269
|
-
self,
|
270
|
-
*,
|
271
|
-
base_path: str | Path,
|
272
|
-
folder_path: str,
|
273
|
-
pattern: str,
|
274
|
-
test_name_pattern: Optional[str] = None,
|
275
|
-
tags: Optional[List[str]] = None
|
276
|
-
) -> 'UnitTest':
|
190
|
+
def __loadConfig(
|
191
|
+
self
|
192
|
+
) -> None:
|
277
193
|
"""
|
278
|
-
|
194
|
+
Load and validate the testing configuration from the application.
|
195
|
+
|
196
|
+
This method retrieves the testing configuration from the application instance,
|
197
|
+
validates each configuration parameter, and updates the internal state of the
|
198
|
+
UnitTest instance accordingly. It ensures that all required fields are present
|
199
|
+
and correctly formatted.
|
279
200
|
|
280
201
|
Parameters
|
281
202
|
----------
|
282
|
-
|
283
|
-
Base directory for resolving the folder path.
|
284
|
-
folder_path : str
|
285
|
-
Relative path to the folder containing test files.
|
286
|
-
pattern : str
|
287
|
-
File name pattern to match test files.
|
288
|
-
test_name_pattern : str, optional
|
289
|
-
Regular expression pattern to filter test names.
|
290
|
-
tags : list of str, optional
|
291
|
-
Tags to filter tests.
|
203
|
+
None
|
292
204
|
|
293
205
|
Returns
|
294
206
|
-------
|
295
|
-
|
296
|
-
|
207
|
+
None
|
208
|
+
This method does not return a value. It updates the internal state of the UnitTest instance.
|
297
209
|
|
298
210
|
Raises
|
299
211
|
------
|
300
212
|
OrionisTestValueError
|
301
|
-
If
|
213
|
+
If the testing configuration is invalid or missing required fields.
|
302
214
|
"""
|
303
|
-
|
304
|
-
|
305
|
-
self.__folder_path = ValidFolderPath(folder_path)
|
306
|
-
self.__pattern = ValidPattern(pattern)
|
307
|
-
self.__test_name_pattern = ValidNamePattern(test_name_pattern)
|
308
|
-
self.__tags = ValidTags(tags)
|
309
|
-
|
310
|
-
# Try to discover tests in the specified folder
|
215
|
+
|
216
|
+
# Load the testing configuration from the application
|
311
217
|
try:
|
218
|
+
config = Testing(**self.__app.config('testing'))
|
219
|
+
except Exception as e:
|
220
|
+
raise OrionisTestValueError(
|
221
|
+
f"Failed to load testing configuration: {str(e)}. "
|
222
|
+
"Please ensure the testing configuration is correctly defined in the application settings."
|
223
|
+
)
|
312
224
|
|
313
|
-
|
314
|
-
|
225
|
+
# Set verbosity level for test output
|
226
|
+
self.__verbosity: Optional[int] = ValidVerbosity(config.verbosity)
|
315
227
|
|
316
|
-
|
317
|
-
|
318
|
-
raise OrionisTestValueError(
|
319
|
-
f"Test folder not found at the specified path: '{str(full_path)}'. "
|
320
|
-
"Please verify that the path is correct and the folder exists."
|
321
|
-
)
|
228
|
+
# Set execution mode (sequential or parallel)
|
229
|
+
self.__execution_mode: Optional[str] = ValidExecutionMode(config.execution_mode)
|
322
230
|
|
323
|
-
|
324
|
-
|
325
|
-
start_dir=str(full_path),
|
326
|
-
pattern=self.__pattern,
|
327
|
-
top_level_dir="."
|
328
|
-
)
|
231
|
+
# Set maximum number of workers for parallel execution
|
232
|
+
self.__max_workers: Optional[int] = ValidWorkers(config.max_workers)
|
329
233
|
|
330
|
-
|
331
|
-
|
332
|
-
if test.__class__.__name__ == "_FailedTest":
|
234
|
+
# Set fail-fast behavior (stop on first failure)
|
235
|
+
self.__fail_fast: Optional[bool] = ValidFailFast(config.fail_fast)
|
333
236
|
|
334
|
-
|
335
|
-
|
336
|
-
if hasattr(test, "_exception"):
|
337
|
-
error_message = str(test._exception)
|
338
|
-
elif hasattr(test, "_outcome") and hasattr(test._outcome, "errors"):
|
339
|
-
error_message = str(test._outcome.errors)
|
340
|
-
# Try to get error from test id or str(test)
|
341
|
-
else:
|
342
|
-
error_message = str(test)
|
237
|
+
# Set whether to throw an exception if tests fail
|
238
|
+
self.__throw_exception: Optional[bool] = ValidThrowException(config.throw_exception)
|
343
239
|
|
344
|
-
|
345
|
-
|
346
|
-
f"Error details: {error_message}\n"
|
347
|
-
"Please check for import errors or missing dependencies."
|
348
|
-
)
|
240
|
+
# Set persistence flag for saving test results
|
241
|
+
self.__persistent: Optional[bool] = ValidPersistent(config.persistent)
|
349
242
|
|
350
|
-
|
351
|
-
|
352
|
-
tests = self.__filterTestsByName(
|
353
|
-
suite=tests,
|
354
|
-
pattern=self.__test_name_pattern
|
355
|
-
)
|
243
|
+
# Set the persistence driver (e.g., 'sqlite', 'json')
|
244
|
+
self.__persistent_driver: Optional[str] = ValidPersistentDriver(config.persistent_driver)
|
356
245
|
|
357
|
-
|
358
|
-
|
359
|
-
tests = self.__filterTestsByTags(
|
360
|
-
suite=tests,
|
361
|
-
tags=self.__tags
|
362
|
-
)
|
246
|
+
# Set web report flag for generating web-based test reports
|
247
|
+
self.__web_report: Optional[bool] = ValidWebReport(config.web_report)
|
363
248
|
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
+ (f", test name pattern '{test_name_pattern}'" if test_name_pattern else "")
|
369
|
-
+ (f", and tags {tags}" if tags else "") +
|
370
|
-
". Please check your patterns, tags, and test files."
|
371
|
-
)
|
249
|
+
# Initialize the printer for console output
|
250
|
+
self.__printer = TestPrinter(
|
251
|
+
print_result = ValidPrintResult(config.print_result)
|
252
|
+
)
|
372
253
|
|
373
|
-
|
374
|
-
|
254
|
+
# Set the file name pattern for test discovery
|
255
|
+
self.__pattern: Optional[str] = ValidPattern(config.pattern)
|
375
256
|
|
376
|
-
|
377
|
-
|
378
|
-
test_count = len(list(self.__flattenTestSuite(tests)))
|
257
|
+
# Set the test method name pattern for filtering
|
258
|
+
self.__test_name_pattern: Optional[str] = ValidNamePattern(config.test_name_pattern)
|
379
259
|
|
380
|
-
|
381
|
-
|
382
|
-
"folder": str(full_path),
|
383
|
-
"test_count": test_count,
|
384
|
-
})
|
260
|
+
# Set the folder(s) where test files are located
|
261
|
+
folder_path = config.folder_path
|
385
262
|
|
386
|
-
|
387
|
-
|
263
|
+
# If folder_path is a list, validate each entry
|
264
|
+
if isinstance(folder_path, list):
|
388
265
|
|
389
|
-
|
266
|
+
# Clean and validate each folder path in the list
|
267
|
+
cleaned_folders = []
|
390
268
|
|
391
|
-
#
|
392
|
-
|
393
|
-
f"Error importing tests from path '{str(full_path)}': {str(e)}.\n"
|
394
|
-
"Please verify that the directory and test modules are accessible and correct."
|
395
|
-
)
|
269
|
+
# Validate each folder path in the list
|
270
|
+
for folder in folder_path:
|
396
271
|
|
397
|
-
|
272
|
+
# If any folder is invalid, raise an error
|
273
|
+
if not isinstance(folder, str) or not folder.strip():
|
274
|
+
raise OrionisTestValueError(
|
275
|
+
f"Invalid 'folder_path' configuration: expected '*' or a list of relative folder paths, got {repr(folder_path)}."
|
276
|
+
)
|
398
277
|
|
399
|
-
|
278
|
+
# Remove leading/trailing slashes and base path
|
279
|
+
scope_folder = folder.strip().lstrip("/\\").rstrip("/\\")
|
280
|
+
|
281
|
+
# Make folder path relative to base path if it starts with it
|
282
|
+
if scope_folder.startswith(self.__base_path):
|
283
|
+
scope_folder = scope_folder[len(self.__base_path):].lstrip("/\\")
|
284
|
+
if not scope_folder:
|
285
|
+
raise OrionisTestValueError(
|
286
|
+
f"Invalid 'folder_path' configuration: expected '*' or a list of relative folder paths, got {repr(folder_path)}."
|
287
|
+
)
|
288
|
+
|
289
|
+
# Add the cleaned folder path to the list
|
290
|
+
cleaned_folders.append(ValidFolderPath(scope_folder))
|
291
|
+
|
292
|
+
# Store the cleaned list of folder paths
|
293
|
+
self.__folder_path: Optional[List[str]] = cleaned_folders
|
294
|
+
|
295
|
+
elif isinstance(folder_path, str) and folder_path == '*':
|
296
|
+
|
297
|
+
# Use wildcard to search all folders
|
298
|
+
self.__folder_path: Optional[str] = '*'
|
299
|
+
|
300
|
+
else:
|
301
|
+
|
302
|
+
# Invalid folder_path configuration
|
400
303
|
raise OrionisTestValueError(
|
401
|
-
f"
|
402
|
-
"Ensure that the test files are valid and that there are no syntax errors or missing dependencies."
|
304
|
+
f"Invalid 'folder_path' configuration: expected '*' or a list of relative folder paths, got {repr(folder_path)}."
|
403
305
|
)
|
404
306
|
|
405
|
-
def
|
406
|
-
self
|
407
|
-
|
408
|
-
module_name: str,
|
409
|
-
test_name_pattern: Optional[str] = None
|
410
|
-
) -> 'UnitTest':
|
307
|
+
def __loadModules(
|
308
|
+
self
|
309
|
+
) -> None:
|
411
310
|
"""
|
412
|
-
|
311
|
+
Loads and validates Python modules for test discovery based on the configured folder paths and file patterns.
|
312
|
+
|
313
|
+
This method determines which test modules to load by inspecting the `folder_path` configuration.
|
314
|
+
If the folder path is set to '*', it discovers all modules matching the configured file pattern in the test directory.
|
315
|
+
If the folder path is a list, it discovers modules in each specified subdirectory.
|
316
|
+
The discovered modules are imported and stored in the internal state for later test discovery and execution.
|
413
317
|
|
414
318
|
Parameters
|
415
319
|
----------
|
416
|
-
|
417
|
-
Fully qualified name of the module to discover tests from.
|
418
|
-
test_name_pattern : str, optional
|
419
|
-
Regular expression pattern to filter test names.
|
320
|
+
None
|
420
321
|
|
421
322
|
Returns
|
422
323
|
-------
|
423
|
-
|
424
|
-
|
324
|
+
None
|
325
|
+
This method does not return any value. It updates the internal state of the UnitTest instance by extending
|
326
|
+
the `self.__modules` list with the discovered and imported module objects.
|
425
327
|
|
426
328
|
Raises
|
427
329
|
------
|
428
330
|
OrionisTestValueError
|
429
|
-
If
|
331
|
+
If any module name or folder path is invalid, or if module discovery fails.
|
332
|
+
|
333
|
+
Notes
|
334
|
+
-----
|
335
|
+
- Uses `__listMatchingModules` to find and import modules matching the file pattern.
|
336
|
+
- Avoids duplicate modules by using a set.
|
337
|
+
- Updates the internal module list for subsequent test discovery.
|
430
338
|
"""
|
431
339
|
|
432
|
-
#
|
433
|
-
self.__module_name = ValidModuleName(module_name)
|
434
|
-
self.__test_name_pattern = ValidNamePattern(test_name_pattern)
|
340
|
+
modules = set() # Use a set to avoid duplicate module imports
|
435
341
|
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
342
|
+
# If folder_path is '*', discover all modules matching the pattern in the test directory
|
343
|
+
if self.__folder_path == '*':
|
344
|
+
list_modules = self.__listMatchingModules(
|
345
|
+
self.__root_path, self.__test_path, None, self.__pattern
|
440
346
|
)
|
347
|
+
modules.update(list_modules)
|
441
348
|
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
349
|
+
# If folder_path is a list, discover modules in each specified subdirectory
|
350
|
+
elif isinstance(self.__folder_path, list):
|
351
|
+
for custom_path in self.__folder_path:
|
352
|
+
list_modules = self.__listMatchingModules(
|
353
|
+
self.__root_path, self.__test_path, custom_path, self.__pattern
|
447
354
|
)
|
355
|
+
modules.update(list_modules)
|
448
356
|
|
449
|
-
|
450
|
-
|
357
|
+
# Extend the internal module list with the sorted discovered modules
|
358
|
+
self.__modules.extend(modules)
|
451
359
|
|
452
|
-
|
453
|
-
|
360
|
+
def __loadTests(
|
361
|
+
self
|
362
|
+
) -> None:
|
363
|
+
"""
|
364
|
+
Discover and load all test cases from the imported test modules into the test suite.
|
454
365
|
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
+ " Please ensure the module contains valid test cases and the pattern is correct."
|
460
|
-
)
|
366
|
+
This method iterates through all imported test modules, loads their test cases,
|
367
|
+
flattens nested suites, checks for failed imports, applies optional test name filtering,
|
368
|
+
and adds the discovered tests to the main test suite. It also tracks the number of discovered
|
369
|
+
tests per module and raises detailed errors for import failures or missing tests.
|
461
370
|
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
"test_count": test_count
|
466
|
-
})
|
371
|
+
Returns
|
372
|
+
-------
|
373
|
+
None
|
467
374
|
|
468
|
-
|
469
|
-
|
375
|
+
Raises
|
376
|
+
------
|
377
|
+
OrionisTestValueError
|
378
|
+
If a test module fails to import, or if no tests are found matching the provided patterns.
|
470
379
|
|
471
|
-
|
380
|
+
Notes
|
381
|
+
-----
|
382
|
+
- Uses `__flattenTestSuite` to extract individual test cases from each module.
|
383
|
+
- Applies test name filtering if `self.__test_name_pattern` is set.
|
384
|
+
- Updates `self.__suite` and `self.__discovered_tests` with discovered tests and metadata.
|
385
|
+
- Provides detailed error messages for failed imports and missing tests.
|
386
|
+
"""
|
387
|
+
try:
|
472
388
|
|
473
|
-
#
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
389
|
+
# Iterate through all imported test modules
|
390
|
+
for test_module in self.__modules:
|
391
|
+
|
392
|
+
# Load all tests from the current module
|
393
|
+
module_suite = self.__loader.loadTestsFromModule(test_module)
|
394
|
+
|
395
|
+
# Flatten the suite to get individual test cases
|
396
|
+
flat_tests = self.__flattenTestSuite(module_suite)
|
397
|
+
|
398
|
+
# Check for failed imports and raise a detailed error if found
|
399
|
+
for test in flat_tests:
|
400
|
+
if test.__class__.__name__ == "_FailedTest":
|
401
|
+
error_message = ""
|
402
|
+
if hasattr(test, "_exception"):
|
403
|
+
error_message = str(test._exception)
|
404
|
+
elif hasattr(test, "_outcome") and hasattr(test._outcome, "errors"):
|
405
|
+
error_message = str(test._outcome.errors)
|
406
|
+
else:
|
407
|
+
error_message = str(test)
|
408
|
+
raise OrionisTestValueError(
|
409
|
+
f"Failed to import test module: {test.id()}.\n"
|
410
|
+
f"Error details: {error_message}\n"
|
411
|
+
"Please check for import errors or missing dependencies."
|
412
|
+
)
|
413
|
+
|
414
|
+
# Rebuild the suite with only valid tests
|
415
|
+
valid_suite = unittest.TestSuite(flat_tests)
|
416
|
+
|
417
|
+
# If a test name pattern is provided, filter tests by name
|
418
|
+
if self.__test_name_pattern:
|
419
|
+
valid_suite = self.__filterTestsByName(
|
420
|
+
suite=valid_suite,
|
421
|
+
pattern=self.__test_name_pattern
|
422
|
+
)
|
478
423
|
|
479
|
-
|
424
|
+
# If no tests are found, raise an error
|
425
|
+
if not list(valid_suite):
|
426
|
+
raise OrionisTestValueError(
|
427
|
+
f"No tests found in module '{test_module.__name__}' matching file pattern '{self.__pattern}'"
|
428
|
+
+ (f", test name pattern '{self.__test_name_pattern}'" if self.__test_name_pattern else "")
|
429
|
+
+ ". Please check your patterns and test files."
|
430
|
+
)
|
431
|
+
|
432
|
+
# Add discovered tests to the main suite
|
433
|
+
self.__suite.addTests(valid_suite)
|
434
|
+
|
435
|
+
# Count the number of tests discovered
|
436
|
+
test_count = len(list(self.__flattenTestSuite(valid_suite)))
|
437
|
+
|
438
|
+
# Append discovered tests information for reporting
|
439
|
+
self.__discovered_tests.append({
|
440
|
+
"module": test_module.__name__,
|
441
|
+
"test_count": test_count,
|
442
|
+
})
|
480
443
|
|
481
|
-
|
444
|
+
except ImportError as e:
|
445
|
+
|
446
|
+
# Raise a specific error if the import fails
|
482
447
|
raise OrionisTestValueError(
|
483
|
-
f"
|
484
|
-
|
448
|
+
f"Error importing tests from module '{getattr(test_module, '__name__', str(test_module))}': {str(e)}.\n"
|
449
|
+
"Please verify that the module and test files are accessible and correct."
|
485
450
|
)
|
486
451
|
|
487
452
|
except Exception as e:
|
488
453
|
|
489
454
|
# Raise a general error for unexpected issues
|
490
455
|
raise OrionisTestValueError(
|
491
|
-
f"
|
492
|
-
"
|
456
|
+
f"Unexpected error while discovering tests in module '{getattr(test_module, '__name__', str(test_module))}': {str(e)}.\n"
|
457
|
+
"Ensure that the test files are valid and that there are no syntax errors or missing dependencies."
|
493
458
|
)
|
494
459
|
|
495
460
|
def run(
|
496
|
-
self
|
461
|
+
self,
|
462
|
+
performance_counter: IPerformanceCounter
|
497
463
|
) -> Dict[str, Any]:
|
498
464
|
"""
|
499
465
|
Execute the test suite and return a summary of the results.
|
@@ -509,16 +475,15 @@ class UnitTest(IUnitTest):
|
|
509
475
|
If the test suite execution fails and throw_exception is True.
|
510
476
|
"""
|
511
477
|
|
478
|
+
# Record the start time in seconds
|
479
|
+
performance_counter.start()
|
480
|
+
|
512
481
|
# Length of all tests in the suite
|
513
482
|
total_tests = len(list(self.__flattenTestSuite(self.__suite)))
|
514
483
|
|
515
484
|
# If no tests are found, print a message and return early
|
516
485
|
if total_tests == 0:
|
517
|
-
self.__printer.zeroTestsMessage()
|
518
|
-
return
|
519
|
-
|
520
|
-
# Record the start time in nanoseconds
|
521
|
-
start_time = time.time_ns()
|
486
|
+
return self.__printer.zeroTestsMessage()
|
522
487
|
|
523
488
|
# Print the start message with test suite details
|
524
489
|
self.__printer.startMessage(
|
@@ -538,7 +503,8 @@ class UnitTest(IUnitTest):
|
|
538
503
|
self.__error_buffer = error_buffer.getvalue()
|
539
504
|
|
540
505
|
# Calculate execution time in milliseconds
|
541
|
-
|
506
|
+
performance_counter.stop()
|
507
|
+
execution_time = performance_counter.getSeconds()
|
542
508
|
|
543
509
|
# Generate a summary of the test results
|
544
510
|
summary = self.__generateSummary(result, execution_time)
|
@@ -950,8 +916,8 @@ class UnitTest(IUnitTest):
|
|
950
916
|
# Define a function to run a single test case and return its result
|
951
917
|
def run_single_test(test):
|
952
918
|
runner = unittest.TextTestRunner(
|
953
|
-
stream=io.StringIO(),
|
954
|
-
verbosity=
|
919
|
+
stream=io.StringIO(),
|
920
|
+
verbosity=self.__verbosity,
|
955
921
|
failfast=False,
|
956
922
|
resultclass=result_class
|
957
923
|
)
|
@@ -1457,93 +1423,69 @@ class UnitTest(IUnitTest):
|
|
1457
1423
|
# Return the suite containing only the filtered tests
|
1458
1424
|
return filtered_suite
|
1459
1425
|
|
1460
|
-
def
|
1426
|
+
def __listMatchingModules(
|
1461
1427
|
self,
|
1462
|
-
|
1463
|
-
|
1464
|
-
|
1428
|
+
root_path: Path,
|
1429
|
+
test_path: Path,
|
1430
|
+
custom_path: Path,
|
1431
|
+
pattern_file: str
|
1432
|
+
) -> List[str]:
|
1465
1433
|
"""
|
1466
|
-
|
1434
|
+
Discover and import Python modules containing test files that match a given filename pattern within a specified directory.
|
1435
|
+
|
1436
|
+
This method recursively searches for Python files in the directory specified by `test_path / custom_path` that match the provided
|
1437
|
+
filename pattern. For each matching file, it constructs the module's fully qualified name relative to the project root, imports
|
1438
|
+
the module using `importlib.import_module`, and adds it to a set to avoid duplicates. The method returns a list of imported module objects.
|
1467
1439
|
|
1468
1440
|
Parameters
|
1469
1441
|
----------
|
1470
|
-
|
1471
|
-
The
|
1472
|
-
|
1473
|
-
|
1442
|
+
root_path : Path
|
1443
|
+
The root directory of the project, used to calculate the relative module path.
|
1444
|
+
test_path : Path
|
1445
|
+
The base directory where tests are located.
|
1446
|
+
custom_path : Path
|
1447
|
+
The subdirectory within `test_path` to search for matching test files.
|
1448
|
+
pattern_file : str
|
1449
|
+
The filename pattern to match (supports '*' and '?' wildcards).
|
1474
1450
|
|
1475
1451
|
Returns
|
1476
1452
|
-------
|
1477
|
-
|
1478
|
-
A
|
1453
|
+
List[module]
|
1454
|
+
A list of imported Python module objects corresponding to test files that match the pattern.
|
1479
1455
|
|
1480
1456
|
Notes
|
1481
1457
|
-----
|
1482
|
-
|
1483
|
-
|
1484
|
-
|
1458
|
+
- Only files ending with `.py` are considered as Python modules.
|
1459
|
+
- Duplicate modules are avoided by using a set.
|
1460
|
+
- The module name is constructed by converting the relative path to dot notation.
|
1461
|
+
- If the relative path is '.', only the module name is used.
|
1462
|
+
- The method imports modules dynamically and returns them as objects.
|
1485
1463
|
"""
|
1486
1464
|
|
1487
|
-
#
|
1488
|
-
|
1489
|
-
|
1490
|
-
# Convert the list of tags to a set for efficient intersection checks
|
1491
|
-
tag_set = set(tags)
|
1492
|
-
|
1493
|
-
# Iterate through all test cases in the flattened suite
|
1494
|
-
for test in self.__flattenTestSuite(suite):
|
1495
|
-
|
1496
|
-
# Attempt to retrieve the test method from the test case
|
1497
|
-
test_method = getattr(test, test._testMethodName, None)
|
1465
|
+
# Compile the filename pattern into a regular expression for matching.
|
1466
|
+
regex = re.compile('^' + pattern_file.replace('*', '.*').replace('?', '.') + '$')
|
1498
1467
|
|
1499
|
-
|
1500
|
-
|
1501
|
-
method_tags = set(getattr(test_method, '__tags__'))
|
1502
|
-
|
1503
|
-
# If there is any intersection between the method's tags and the filter tags, add the test
|
1504
|
-
if tag_set.intersection(method_tags):
|
1505
|
-
filtered_suite.addTest(test)
|
1468
|
+
# Use a set to avoid duplicate module imports.
|
1469
|
+
matched_folders = set()
|
1506
1470
|
|
1507
|
-
|
1508
|
-
|
1509
|
-
|
1471
|
+
# Walk through all files in the target directory.
|
1472
|
+
for root, _, files in walk(str(test_path / custom_path) if custom_path else str(test_path)):
|
1473
|
+
for file in files:
|
1510
1474
|
|
1511
|
-
#
|
1512
|
-
if
|
1513
|
-
filtered_suite.addTest(test)
|
1475
|
+
# Check if the file matches the pattern and is a Python file.
|
1476
|
+
if regex.fullmatch(file) and file.endswith('.py'):
|
1514
1477
|
|
1515
|
-
|
1516
|
-
|
1478
|
+
# Calculate the relative path from the root, convert to module notation.
|
1479
|
+
ralative_path = str(Path(root).relative_to(root_path)).replace(os.sep, '.')
|
1480
|
+
module_name = file[:-3] # Remove '.py' extension.
|
1517
1481
|
|
1518
|
-
|
1519
|
-
|
1520
|
-
base_path: Path,
|
1521
|
-
custom_path: Path,
|
1522
|
-
pattern: str
|
1523
|
-
) -> List[str]:
|
1524
|
-
"""
|
1525
|
-
List folders within a given path containing files matching a pattern.
|
1482
|
+
# Build the full module name.
|
1483
|
+
full_module = f"{ralative_path}.{module_name}" if ralative_path != '.' else module_name
|
1526
1484
|
|
1527
|
-
|
1528
|
-
|
1529
|
-
base_path : Path
|
1530
|
-
The base directory path for calculating relative paths.
|
1531
|
-
custom_path : Path
|
1532
|
-
The directory path to search for matching files.
|
1533
|
-
pattern : str
|
1534
|
-
The filename pattern to match, supporting '*' and '?' wildcards.
|
1485
|
+
# Import the module and add to the set.
|
1486
|
+
matched_folders.add(import_module(ValidModuleName(full_module)))
|
1535
1487
|
|
1536
|
-
|
1537
|
-
-------
|
1538
|
-
List[str]
|
1539
|
-
List of relative folder paths containing files matching the pattern.
|
1540
|
-
"""
|
1541
|
-
regex = re.compile('^' + pattern.replace('*', '.*').replace('?', '.') + '$')
|
1542
|
-
matched_folders = set()
|
1543
|
-
for root, _, files in walk(str(custom_path)):
|
1544
|
-
if any(regex.fullmatch(file) for file in files):
|
1545
|
-
rel_path = Path(root).relative_to(base_path).as_posix()
|
1546
|
-
matched_folders.add(rel_path)
|
1488
|
+
# Return the list of imported module objects.
|
1547
1489
|
return list(matched_folders)
|
1548
1490
|
|
1549
1491
|
def getTestNames(
|