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