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.
@@ -1,6 +1,6 @@
1
1
  import io
2
2
  import json
3
- from os import walk
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. It supports sequential and parallel execution, test filtering by name or tags, and detailed result tracking including execution times, error messages, and tracebacks.
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 a UnitTest instance with default configuration and internal state.
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
- Sets up all internal attributes required for test discovery, execution, result reporting, and configuration management. Does not perform test discovery or execution.
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
- # Application instance for dependency injection
98
+ # Store the application instance for dependency injection and configuration access
68
99
  self.__app: IApplication = app
69
100
 
70
- # Storage path for test results
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
- # Printer for console output (set during configuration)
99
- self.__printer: TestPrinter = None
113
+ # Variable to store the result summary after test execution
114
+ self.__result: Optional[Dict[str, Any]] = None
100
115
 
101
- # Buffers for capturing standard output and error during test execution
102
- self.__output_buffer = None
103
- self.__error_buffer = None
116
+ # Load the output and error buffers for capturing test execution output
117
+ self.__loadOutputBuffer()
104
118
 
105
- # Stores the result summary after test execution
106
- self.__result = None
119
+ # Load and set internal paths for test discovery and result storage
120
+ self.__loadPaths()
107
121
 
108
- def configure(
109
- self,
110
- *,
111
- verbosity: int | VerbosityMode,
112
- execution_mode: str | ExecutionMode,
113
- max_workers: int,
114
- fail_fast: bool,
115
- print_result: bool,
116
- throw_exception: bool,
117
- persistent: bool,
118
- persistent_driver: str | PersistentDrivers,
119
- web_report: bool
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
- Configure the UnitTest instance with execution and reporting parameters.
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
- verbosity : int or VerbosityMode
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
- UnitTest
148
- The configured UnitTest instance.
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
- # Validate and assign parameters using specialized validators
157
- self.__verbosity = ValidVerbosity(verbosity)
158
- self.__execution_mode = ValidExecutionMode(execution_mode)
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
- Discover test cases from specified folders using flexible path discovery.
156
+ Load and set internal paths required for test discovery and result storage.
184
157
 
185
- This method provides a convenient way to discover and load test cases from multiple folders
186
- based on various path specifications. It supports wildcard discovery, single folder loading,
187
- and multiple folder loading. The method automatically resolves paths relative to the base
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
- base_path : str or Path
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
- UnitTest
213
- The current UnitTest instance with discovered tests added to the suite,
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
- - All paths are resolved as absolute paths relative to the base_path
219
- - When folder_path is '*', the method searches recursively through all subdirectories
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
- # Use a set to store discovered folders and automatically eliminate duplicates
229
- discovered_folders = set()
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
- # Handle wildcard discovery: search all folders containing matching files
232
- if folder_path == '*':
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
- # Search recursively through the entire base path for folders with matching files
235
- discovered_folders.update(self.__listMatchingFolders(base_path, base_path, pattern))
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
- # Handle multiple folder paths: process each folder in the provided list
238
- elif isinstance(folder_path, list):
239
- for custom in folder_path:
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
- Discover and add unit tests from a specified folder to the test suite.
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
- base_path : str or Path
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
- UnitTest
296
- The current instance with discovered tests added.
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 arguments are invalid, folder does not exist, no tests are found, or import/discovery errors occur.
212
+ If the testing configuration is invalid or missing required fields.
302
213
  """
303
- # Validate Parameters
304
- self.__base_path = ValidBasePath(base_path)
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
- # Ensure the folder path is absolute
314
- full_path = Path(self.__base_path / self.__folder_path).resolve()
224
+ # Set verbosity level for test output
225
+ self.__verbosity: Optional[int] = ValidVerbosity(config.verbosity)
315
226
 
316
- # Validate the full path
317
- if not full_path.exists():
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
- # Discover tests using the unittest TestLoader
324
- tests = self.__loader.discover(
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
- # Check for failed test imports (unittest.loader._FailedTest)
331
- for test in self.__flattenTestSuite(tests):
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
- # Extract the error message from the test's traceback
335
- error_message = ""
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
- raise OrionisTestValueError(
345
- f"Failed to import test module: {test.id()}.\n"
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
- # If name pattern is provided, filter tests by name
351
- if test_name_pattern:
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
- # If tags are provided, filter tests by tags
358
- if tags:
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
- # If no tests are found, raise an error
365
- if not list(tests):
366
- raise OrionisTestValueError(
367
- f"No tests found in '{str(full_path)}' matching file pattern '{pattern}'"
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
- # Add discovered tests to the suite
374
- self.__suite.addTests(tests)
253
+ # Set the file name pattern for test discovery
254
+ self.__pattern: Optional[str] = ValidPattern(config.pattern)
375
255
 
376
- # Count the number of tests discovered
377
- # Using __flattenTestSuite to ensure we count all individual test cases
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
- # Append the discovered tests information
381
- self.__discovered_tests.append({
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
- # Return the current instance
387
- return self
262
+ # If folder_path is a list, validate each entry
263
+ if isinstance(folder_path, list):
388
264
 
389
- except ImportError as e:
265
+ # Clean and validate each folder path in the list
266
+ cleaned_folders = []
390
267
 
391
- # Raise a specific error if the import fails
392
- raise OrionisTestValueError(
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
- except Exception as e:
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
- # Raise a general error for unexpected issues
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"Unexpected error while discovering tests in '{str(full_path)}': {str(e)}.\n"
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 discoverTestsInModule(
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
- Discover and add unit tests from a specified Python module to the test suite.
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
- module_name : str
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
- UnitTest
424
- The current UnitTest instance with discovered tests added.
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 module_name is invalid, test_name_pattern is not a valid regex, the module cannot be imported, or no tests are found.
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
- # Validate input parameters
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
- try:
437
- # Load all tests from the specified module
438
- tests = self.__loader.loadTestsFromName(
439
- name=self.__module_name
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
- # If a test name pattern is provided, filter the discovered tests
443
- if test_name_pattern:
444
- tests = self.__filterTestsByName(
445
- suite=tests,
446
- pattern=self.__test_name_pattern
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
- # Add the filtered (or all) tests to the suite
450
- self.__suite.addTests(tests)
356
+ # Extend the internal module list with the sorted discovered modules
357
+ self.__modules.extend(modules)
451
358
 
452
- # Count the number of discovered tests
453
- test_count = len(list(self.__flattenTestSuite(tests)))
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
- if test_count == 0:
456
- raise OrionisTestValueError(
457
- f"No tests found in module '{self.__module_name}'"
458
- + (f" matching test name pattern '{test_name_pattern}'." if test_name_pattern else ".")
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
- # Record discovery metadata
463
- self.__discovered_tests.append({
464
- "module": self.__module_name,
465
- "test_count": test_count
466
- })
370
+ Returns
371
+ -------
372
+ None
467
373
 
468
- # Return the current instance for method chaining
469
- return self
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
- except ImportError as e:
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
- # Raise an error if the module cannot be imported
474
- raise OrionisTestValueError(
475
- f"Failed to import tests from module '{self.__module_name}': {str(e)}. "
476
- "Ensure the module exists, is importable, and contains valid test cases."
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
- except re.error as e:
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 an error if the test name pattern is not a valid regex
442
+ # Raise a specific error if the import fails
482
443
  raise OrionisTestValueError(
483
- f"Invalid regular expression for test_name_pattern: '{test_name_pattern}'. "
484
- f"Regex compilation error: {str(e)}. Please check the pattern syntax."
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"An unexpected error occurred while discovering tests in module '{self.__module_name}': {str(e)}. "
492
- "Verify that the module name is correct, test methods are valid, and there are no syntax errors or missing dependencies."
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(), # Use a separate buffer for each test
954
- verbosity=0,
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 __filterTestsByTags(
1421
+ def __listMatchingModules(
1461
1422
  self,
1462
- suite: unittest.TestSuite,
1463
- tags: List[str]
1464
- ) -> unittest.TestSuite:
1423
+ root_path: Path,
1424
+ test_path: Path,
1425
+ custom_path: Path,
1426
+ pattern_file: str
1427
+ ) -> List[str]:
1465
1428
  """
1466
- Filters tests in a unittest TestSuite by matching specified tags.
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
- suite : unittest.TestSuite
1471
- The original TestSuite containing all test cases to be filtered.
1472
- tags : list of str
1473
- List of tags to filter the tests by.
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
- unittest.TestSuite
1478
- A new TestSuite containing only the tests that have at least one matching tag.
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
- This method inspects each test case in the provided suite and checks for the presence of tags
1483
- either on the test method (via a `__tags__` attribute) or on the test class instance itself.
1484
- If any of the specified tags are found in the test's tags, the test is included in the returned suite.
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
- # Create a new TestSuite to hold the filtered tests
1488
- filtered_suite = unittest.TestSuite()
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
- # 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)
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
- # 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)
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
- # If the method does not have tags, check if the test case itself has a __tags__ attribute
1508
- elif hasattr(test, '__tags__'):
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
- # If there is any intersection between the class's tags and the filter tags, add the test
1512
- if tag_set.intersection(class_tags):
1513
- filtered_suite.addTest(test)
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
- # Return the suite containing only the filtered tests
1516
- return filtered_suite
1477
+ # Build the full module name.
1478
+ full_module = f"{ralative_path}.{module_name}" if ralative_path != '.' else module_name
1517
1479
 
1518
- def __listMatchingFolders(
1519
- self,
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
- Parameters
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(