orionis 0.591.0__py3-none-any.whl → 0.593.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/config/testing/entities/testing.py +0 -16
- orionis/metadata/framework.py +1 -1
- orionis/test/contracts/unit_test.py +0 -81
- orionis/test/core/unit_test.py +737 -604
- orionis/test/output/printer.py +258 -174
- orionis/test/records/logs.py +223 -83
- orionis/test/view/render.py +45 -17
- {orionis-0.591.0.dist-info → orionis-0.593.0.dist-info}/METADATA +1 -1
- {orionis-0.591.0.dist-info → orionis-0.593.0.dist-info}/RECORD +12 -12
- {orionis-0.591.0.dist-info → orionis-0.593.0.dist-info}/WHEEL +0 -0
- {orionis-0.591.0.dist-info → orionis-0.593.0.dist-info}/licenses/LICENCE +0 -0
- {orionis-0.591.0.dist-info → orionis-0.593.0.dist-info}/top_level.txt +0 -0
orionis/test/core/unit_test.py
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
import io
|
2
2
|
import json
|
3
|
+
import logging
|
3
4
|
import os
|
4
5
|
import re
|
5
6
|
import time
|
6
7
|
import traceback
|
7
8
|
import unittest
|
8
9
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
9
|
-
from contextlib import redirect_stdout, redirect_stderr
|
10
10
|
from datetime import datetime
|
11
11
|
from importlib import import_module
|
12
12
|
from os import walk
|
@@ -35,13 +35,13 @@ from orionis.test.validators import (
|
|
35
35
|
ValidPattern,
|
36
36
|
ValidPersistentDriver,
|
37
37
|
ValidPersistent,
|
38
|
-
ValidPrintResult,
|
39
38
|
ValidThrowException,
|
40
39
|
ValidVerbosity,
|
41
40
|
ValidWebReport,
|
42
41
|
ValidWorkers,
|
43
42
|
)
|
44
43
|
from orionis.test.view.render import TestingResultRender
|
44
|
+
import inspect
|
45
45
|
|
46
46
|
class UnitTest(IUnitTest):
|
47
47
|
"""
|
@@ -96,6 +96,9 @@ class UnitTest(IUnitTest):
|
|
96
96
|
- Output buffers, paths, configuration, modules, and tests are loaded in sequence to prepare the test manager.
|
97
97
|
"""
|
98
98
|
|
99
|
+
# Suppress overly verbose asyncio logging during test execution
|
100
|
+
logging.getLogger("asyncio").setLevel(logging.ERROR)
|
101
|
+
|
99
102
|
# Store the application instance for dependency injection and configuration access
|
100
103
|
self.__app: IApplication = app
|
101
104
|
|
@@ -104,18 +107,24 @@ class UnitTest(IUnitTest):
|
|
104
107
|
|
105
108
|
# Initialize the test suite to hold discovered tests
|
106
109
|
self.__suite = unittest.TestSuite()
|
110
|
+
self.__flatten_test_suite: Optional[List[unittest.TestCase]] = None
|
107
111
|
|
108
112
|
# List to store imported test modules
|
109
|
-
self.
|
113
|
+
self.__imported_modules: List = []
|
110
114
|
|
111
|
-
#
|
112
|
-
self.
|
115
|
+
# Sets to track discovered test cases, modules, and IDs
|
116
|
+
self.__discovered_test_cases: set = set()
|
117
|
+
self.__discovered_test_modules: set = set()
|
118
|
+
self.__discovered_test_ids: set = set()
|
113
119
|
|
114
120
|
# Variable to store the result summary after test execution
|
115
121
|
self.__result: Optional[Dict[str, Any]] = None
|
116
122
|
|
117
|
-
#
|
118
|
-
self.
|
123
|
+
# Define keywords to detect debugging or dump calls in test code
|
124
|
+
self.__debbug_keywords: list = ['self.dd', 'self.dump']
|
125
|
+
|
126
|
+
# Use live console output during test execution
|
127
|
+
self.__live_console: bool = True
|
119
128
|
|
120
129
|
# Load and set internal paths for test discovery and result storage
|
121
130
|
self.__loadPaths()
|
@@ -129,27 +138,6 @@ class UnitTest(IUnitTest):
|
|
129
138
|
# Discover and load all test cases from the imported modules into the suite
|
130
139
|
self.__loadTests()
|
131
140
|
|
132
|
-
def __loadOutputBuffer(
|
133
|
-
self
|
134
|
-
) -> None:
|
135
|
-
"""
|
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.
|
140
|
-
|
141
|
-
Parameters
|
142
|
-
----------
|
143
|
-
None
|
144
|
-
|
145
|
-
Returns
|
146
|
-
-------
|
147
|
-
None
|
148
|
-
This method does not return a value. It sets the internal output buffer attribute.
|
149
|
-
"""
|
150
|
-
self.__output_buffer = None
|
151
|
-
self.__error_buffer = None
|
152
|
-
|
153
141
|
def __loadPaths(
|
154
142
|
self
|
155
143
|
) -> None:
|
@@ -176,8 +164,8 @@ class UnitTest(IUnitTest):
|
|
176
164
|
"""
|
177
165
|
|
178
166
|
# 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'))
|
167
|
+
self.__test_path: Path = ValidBasePath(self.__app.path('tests'))
|
168
|
+
self.__root_path: Path = ValidBasePath(self.__app.path('root'))
|
181
169
|
|
182
170
|
# Compute the base path for test discovery, relative to the project root
|
183
171
|
# Remove the root path prefix and leading slash
|
@@ -248,7 +236,7 @@ class UnitTest(IUnitTest):
|
|
248
236
|
|
249
237
|
# Initialize the printer for console output
|
250
238
|
self.__printer = TestPrinter(
|
251
|
-
|
239
|
+
verbosity=self.__verbosity
|
252
240
|
)
|
253
241
|
|
254
242
|
# Set the file name pattern for test discovery
|
@@ -323,7 +311,7 @@ class UnitTest(IUnitTest):
|
|
323
311
|
-------
|
324
312
|
None
|
325
313
|
This method does not return any value. It updates the internal state of the UnitTest instance by extending
|
326
|
-
the `self.
|
314
|
+
the `self.__imported_modules` list with the discovered and imported module objects.
|
327
315
|
|
328
316
|
Raises
|
329
317
|
------
|
@@ -337,7 +325,8 @@ class UnitTest(IUnitTest):
|
|
337
325
|
- Updates the internal module list for subsequent test discovery.
|
338
326
|
"""
|
339
327
|
|
340
|
-
|
328
|
+
# Use a set to avoid duplicate module imports
|
329
|
+
modules = set()
|
341
330
|
|
342
331
|
# If folder_path is '*', discover all modules matching the pattern in the test directory
|
343
332
|
if self.__folder_path == '*':
|
@@ -355,7 +344,248 @@ class UnitTest(IUnitTest):
|
|
355
344
|
modules.update(list_modules)
|
356
345
|
|
357
346
|
# Extend the internal module list with the sorted discovered modules
|
358
|
-
self.
|
347
|
+
self.__imported_modules.extend(modules)
|
348
|
+
|
349
|
+
def __listMatchingModules(
|
350
|
+
self,
|
351
|
+
root_path: Path,
|
352
|
+
test_path: Path,
|
353
|
+
custom_path: Path,
|
354
|
+
pattern_file: str
|
355
|
+
) -> List[str]:
|
356
|
+
"""
|
357
|
+
Discover and import Python modules containing test files that match a given filename pattern within a specified directory.
|
358
|
+
|
359
|
+
This method recursively searches for Python files in the directory specified by `test_path / custom_path` that match the provided
|
360
|
+
filename pattern. For each matching file, it constructs the module's fully qualified name relative to the project root, imports
|
361
|
+
the module using `importlib.import_module`, and adds it to a set to avoid duplicates. The method returns a list of imported module objects.
|
362
|
+
|
363
|
+
Parameters
|
364
|
+
----------
|
365
|
+
root_path : Path
|
366
|
+
The root directory of the project, used to calculate the relative module path.
|
367
|
+
test_path : Path
|
368
|
+
The base directory where tests are located.
|
369
|
+
custom_path : Path
|
370
|
+
The subdirectory within `test_path` to search for matching test files.
|
371
|
+
pattern_file : str
|
372
|
+
The filename pattern to match (supports '*' and '?' wildcards).
|
373
|
+
|
374
|
+
Returns
|
375
|
+
-------
|
376
|
+
List[module]
|
377
|
+
A list of imported Python module objects corresponding to test files that match the pattern.
|
378
|
+
|
379
|
+
Notes
|
380
|
+
-----
|
381
|
+
- Only files ending with `.py` are considered as Python modules.
|
382
|
+
- Duplicate modules are avoided by using a set.
|
383
|
+
- The module name is constructed by converting the relative path to dot notation.
|
384
|
+
- If the relative path is '.', only the module name is used.
|
385
|
+
- The method imports modules dynamically and returns them as objects.
|
386
|
+
"""
|
387
|
+
|
388
|
+
# Compile the filename pattern into a regular expression for matching.
|
389
|
+
regex = re.compile('^' + pattern_file.replace('*', '.*').replace('?', '.') + '$')
|
390
|
+
|
391
|
+
# Use a set to avoid duplicate module imports.
|
392
|
+
matched_folders = set()
|
393
|
+
|
394
|
+
# Walk through all files in the target directory.
|
395
|
+
for root, _, files in walk(str(test_path / custom_path) if custom_path else str(test_path)):
|
396
|
+
|
397
|
+
# Iterate through each file in the current directory
|
398
|
+
for file in files:
|
399
|
+
|
400
|
+
# Check if the file matches the pattern and is a Python file.
|
401
|
+
if regex.fullmatch(file) and file.endswith('.py'):
|
402
|
+
|
403
|
+
# Calculate the relative path from the root, convert to module notation.
|
404
|
+
ralative_path = str(Path(root).relative_to(root_path)).replace(os.sep, '.')
|
405
|
+
|
406
|
+
# Remove '.py' extension.
|
407
|
+
module_name = file[:-3]
|
408
|
+
|
409
|
+
# Build the full module name.
|
410
|
+
full_module = f"{ralative_path}.{module_name}" if ralative_path != '.' else module_name
|
411
|
+
|
412
|
+
# Import the module and add to the set.
|
413
|
+
matched_folders.add(import_module(ValidModuleName(full_module)))
|
414
|
+
|
415
|
+
# Return the list of imported module objects.
|
416
|
+
return list(matched_folders)
|
417
|
+
|
418
|
+
def __raiseIsFailedTest(
|
419
|
+
self,
|
420
|
+
test_case: unittest.TestCase
|
421
|
+
) -> None:
|
422
|
+
"""
|
423
|
+
Raises an error if the provided test case represents a failed import.
|
424
|
+
|
425
|
+
This method checks whether the given test case is an instance of a failed import
|
426
|
+
(typically indicated by the class name '_FailedTest'). If so, it extracts the error
|
427
|
+
details from the test case and raises an `OrionisTestValueError` with a descriptive
|
428
|
+
message, including the test case ID and error information. This helps to surface
|
429
|
+
import errors or missing dependencies during test discovery.
|
430
|
+
|
431
|
+
Parameters
|
432
|
+
----------
|
433
|
+
test_case : unittest.TestCase
|
434
|
+
The test case to check for failed import status.
|
435
|
+
|
436
|
+
Returns
|
437
|
+
-------
|
438
|
+
None
|
439
|
+
This method does not return a value. If the test case is a failed import,
|
440
|
+
an exception is raised.
|
441
|
+
|
442
|
+
Raises
|
443
|
+
------
|
444
|
+
OrionisTestValueError
|
445
|
+
If the test case is a failed import, with details about the failure.
|
446
|
+
|
447
|
+
Notes
|
448
|
+
-----
|
449
|
+
- The error message is extracted from the `_exception` attribute if present,
|
450
|
+
otherwise from the `_outcome.errors` or the string representation of the test case.
|
451
|
+
- This method is typically used during test discovery to halt execution and
|
452
|
+
provide immediate feedback about import failures.
|
453
|
+
"""
|
454
|
+
|
455
|
+
# Check if the test case is a failed import by its class name
|
456
|
+
if test_case.__class__.__name__ == "_FailedTest":
|
457
|
+
error_message = ""
|
458
|
+
|
459
|
+
# Try to extract the error message from known attributes
|
460
|
+
if hasattr(test_case, "_exception"):
|
461
|
+
error_message = str(test_case._exception)
|
462
|
+
elif hasattr(test_case, "_outcome") and hasattr(test_case._outcome, "errors"):
|
463
|
+
error_message = str(test_case._outcome.errors)
|
464
|
+
else:
|
465
|
+
error_message = str(test_case)
|
466
|
+
|
467
|
+
# Raise a value error with detailed information about the failure
|
468
|
+
raise OrionisTestValueError(
|
469
|
+
f"Failed to import test module: {test_case.id()}.\n"
|
470
|
+
f"Error details: {error_message}\n"
|
471
|
+
"Please check for import errors or missing dependencies."
|
472
|
+
)
|
473
|
+
|
474
|
+
def __raiseIfNotFoundTestMethod(
|
475
|
+
self,
|
476
|
+
test_case: unittest.TestCase
|
477
|
+
) -> None:
|
478
|
+
"""
|
479
|
+
Raises an error if the provided test case does not have a valid test method.
|
480
|
+
|
481
|
+
This method uses reflection to check whether the given `unittest.TestCase` instance
|
482
|
+
contains a valid test method. It retrieves the method name from the test case and
|
483
|
+
verifies that the method exists in the test case's class. If the method is missing
|
484
|
+
or invalid, an `OrionisTestValueError` is raised with a descriptive message.
|
485
|
+
|
486
|
+
Parameters
|
487
|
+
----------
|
488
|
+
test_case : unittest.TestCase
|
489
|
+
The test case instance to validate.
|
490
|
+
|
491
|
+
Returns
|
492
|
+
-------
|
493
|
+
None
|
494
|
+
This method does not return any value. If the test case is invalid, an exception is raised.
|
495
|
+
|
496
|
+
Raises
|
497
|
+
------
|
498
|
+
OrionisTestValueError
|
499
|
+
If the test case does not have a valid test method.
|
500
|
+
|
501
|
+
Notes
|
502
|
+
-----
|
503
|
+
- Uses `ReflectionInstance` to retrieve the test method name.
|
504
|
+
- Checks for both missing method names and missing attributes in the test case class.
|
505
|
+
- Provides detailed error information including test case ID, class name, and module name.
|
506
|
+
"""
|
507
|
+
|
508
|
+
# Use reflection to get the test method name
|
509
|
+
rf_instance = ReflectionInstance(test_case)
|
510
|
+
method_name = rf_instance.getAttribute("_testMethodName")
|
511
|
+
|
512
|
+
# Check for missing or invalid test method
|
513
|
+
if not method_name or not hasattr(test_case.__class__, method_name):
|
514
|
+
class_name = test_case.__class__.__name__
|
515
|
+
module_name = getattr(test_case, "__module__", "unknown")
|
516
|
+
|
517
|
+
# Raise an error with detailed information
|
518
|
+
raise OrionisTestValueError(
|
519
|
+
f"Test case '{test_case.id()}' in class '{class_name}' (module '{module_name}') "
|
520
|
+
f"does not have a valid test method '{method_name}'. "
|
521
|
+
"Please ensure the test case is correctly defined and contains valid test methods."
|
522
|
+
)
|
523
|
+
|
524
|
+
def __isDecoratedMethod(
|
525
|
+
self,
|
526
|
+
test_case: unittest.TestCase
|
527
|
+
) -> bool:
|
528
|
+
"""
|
529
|
+
Determines whether the test method of a given test case is decorated (i.e., wrapped by one or more Python decorators).
|
530
|
+
|
531
|
+
This method inspects the test method associated with the provided `unittest.TestCase` instance to detect the presence of decorators.
|
532
|
+
It traverses the decorator chain by following the `__wrapped__` attribute, which is set by Python's `functools.wraps` or similar mechanisms.
|
533
|
+
Decorators are identified by the existence of the `__wrapped__` attribute, and their names are collected from the `__qualname__` or `__name__` attributes.
|
534
|
+
|
535
|
+
Parameters
|
536
|
+
----------
|
537
|
+
test_case : unittest.TestCase
|
538
|
+
The test case instance whose test method will be checked for decorators.
|
539
|
+
|
540
|
+
Returns
|
541
|
+
-------
|
542
|
+
bool
|
543
|
+
True if the test method has one or more decorators applied (i.e., if any decorators are found in the chain).
|
544
|
+
False if the test method is not decorated or if no test method is found.
|
545
|
+
|
546
|
+
Notes
|
547
|
+
-----
|
548
|
+
- The method checks for decorators by traversing the `__wrapped__` attribute chain.
|
549
|
+
- Decorator names are collected for informational purposes but are not returned.
|
550
|
+
- If the test method is not decorated, or if no test method is found, the method returns False.
|
551
|
+
- This method does not modify the test case or its method; it only inspects for decoration.
|
552
|
+
"""
|
553
|
+
|
554
|
+
# Retrieve the test method from the test case's class using the test method name
|
555
|
+
test_method = getattr(test_case.__class__, getattr(test_case, "_testMethodName"), None)
|
556
|
+
|
557
|
+
# List to store decorator names found during traversal
|
558
|
+
decorators = []
|
559
|
+
|
560
|
+
# Check if the method has the __wrapped__ attribute, indicating it is decorated
|
561
|
+
if hasattr(test_method, '__wrapped__'):
|
562
|
+
|
563
|
+
# Start with the outermost decorated method
|
564
|
+
original = test_method
|
565
|
+
|
566
|
+
# Traverse the decorator chain by following __wrapped__ attributes
|
567
|
+
while hasattr(original, '__wrapped__'):
|
568
|
+
|
569
|
+
# Collect decorator name information for tracking purposes
|
570
|
+
if hasattr(original, '__qualname__'):
|
571
|
+
|
572
|
+
# Prefer __qualname__ for detailed naming information
|
573
|
+
decorators.append(original.__qualname__)
|
574
|
+
|
575
|
+
elif hasattr(original, '__name__'):
|
576
|
+
|
577
|
+
# Fall back to __name__ if __qualname__ is not available
|
578
|
+
decorators.append(original.__name__)
|
579
|
+
|
580
|
+
# Move to the next level in the decorator chain
|
581
|
+
original = original.__wrapped__
|
582
|
+
|
583
|
+
# Return True if any decorators were found during the traversal
|
584
|
+
if decorators:
|
585
|
+
return True
|
586
|
+
|
587
|
+
# Return False if no decorators are found or if the method is not decorated
|
588
|
+
return False
|
359
589
|
|
360
590
|
def __loadTests(
|
361
591
|
self
|
@@ -368,9 +598,15 @@ class UnitTest(IUnitTest):
|
|
368
598
|
and adds the discovered tests to the main test suite. It also tracks the number of discovered
|
369
599
|
tests per module and raises detailed errors for import failures or missing tests.
|
370
600
|
|
601
|
+
Parameters
|
602
|
+
----------
|
603
|
+
None
|
604
|
+
|
371
605
|
Returns
|
372
606
|
-------
|
373
607
|
None
|
608
|
+
This method does not return any value. It updates the internal test suite and
|
609
|
+
discovered tests metadata.
|
374
610
|
|
375
611
|
Raises
|
376
612
|
------
|
@@ -386,60 +622,77 @@ class UnitTest(IUnitTest):
|
|
386
622
|
"""
|
387
623
|
try:
|
388
624
|
|
389
|
-
#
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
for
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
625
|
+
# Lists to categorize tests with and without debugger calls
|
626
|
+
normal_tests = []
|
627
|
+
debug_tests = []
|
628
|
+
|
629
|
+
# Use a progress bar to indicate module loading status
|
630
|
+
with self.__printer.progressBar() as progress:
|
631
|
+
|
632
|
+
# Set total steps for the progress bar
|
633
|
+
steps = len(self.__imported_modules) + 1
|
634
|
+
|
635
|
+
# Add a task to the progress bar for loading modules
|
636
|
+
task = progress.add_task("Loading test modules...", total=steps)
|
637
|
+
|
638
|
+
# Print a newline for better console formatting
|
639
|
+
self.__printer.line(1)
|
640
|
+
|
641
|
+
# Iterate through all imported test modules
|
642
|
+
for test_module in self.__imported_modules:
|
643
|
+
|
644
|
+
# Load all tests from the current module using the unittest loader
|
645
|
+
module_suite = self.__loader.loadTestsFromModule(test_module)
|
646
|
+
|
647
|
+
# Flatten the suite to get individual test cases
|
648
|
+
flat_tests = self.__flattenTestSuite(module_suite)
|
649
|
+
|
650
|
+
# Iterate through each test case
|
651
|
+
for test in flat_tests:
|
652
|
+
|
653
|
+
# Raise an error if the test case is a failed import
|
654
|
+
self.__raiseIsFailedTest(test)
|
655
|
+
|
656
|
+
# Raise an error if the test case does not have a valid test method
|
657
|
+
self.__raiseIfNotFoundTestMethod(test)
|
658
|
+
|
659
|
+
# Add the test case to the discovered tests list
|
660
|
+
self.__discovered_test_cases.add(test.__class__)
|
661
|
+
|
662
|
+
# Track the module name of the discovered test case
|
663
|
+
self.__discovered_test_modules.add(test.__module__)
|
664
|
+
|
665
|
+
# Track the test ID of the discovered test case
|
666
|
+
self.__discovered_test_ids.add(test.id())
|
667
|
+
|
668
|
+
# Categorize and resolve test dependencies efficiently
|
669
|
+
target_list = debug_tests if self.__withDebugger(test) else normal_tests
|
670
|
+
resolved_test = test
|
671
|
+
if not self.__isDecoratedMethod(test):
|
672
|
+
resolved_test = self.__resolveTestDependencies(test)
|
673
|
+
target_list.append(resolved_test)
|
674
|
+
|
675
|
+
# If no tests are found, raise an error
|
676
|
+
if not flat_tests:
|
408
677
|
raise OrionisTestValueError(
|
409
|
-
f"
|
410
|
-
|
411
|
-
"Please check for import errors or missing dependencies."
|
678
|
+
f"No tests found in module '{test_module.__name__}'. "
|
679
|
+
"Please ensure that the module contains valid unittest.TestCase classes with test methods."
|
412
680
|
)
|
413
681
|
|
414
|
-
|
415
|
-
|
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
|
-
)
|
682
|
+
# Update the progress bar after processing each module
|
683
|
+
progress.advance(task, advance=1)
|
423
684
|
|
424
|
-
#
|
425
|
-
|
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
|
-
)
|
685
|
+
# Add debug tests first
|
686
|
+
self.__suite.addTests(debug_tests)
|
431
687
|
|
432
|
-
#
|
433
|
-
self.__suite.addTests(
|
688
|
+
# Then add normal tests
|
689
|
+
self.__suite.addTests(normal_tests)
|
434
690
|
|
435
|
-
#
|
436
|
-
|
691
|
+
# Flatten the entire suite for easier access later
|
692
|
+
self.__flatten_test_suite = self.__flattenTestSuite(self.__suite)
|
437
693
|
|
438
|
-
#
|
439
|
-
|
440
|
-
"module": test_module.__name__,
|
441
|
-
"test_count": test_count,
|
442
|
-
})
|
694
|
+
# Finalize the progress bar
|
695
|
+
progress.update(task, completed=steps)
|
443
696
|
|
444
697
|
except ImportError as e:
|
445
698
|
|
@@ -457,6 +710,70 @@ class UnitTest(IUnitTest):
|
|
457
710
|
"Ensure that the test files are valid and that there are no syntax errors or missing dependencies."
|
458
711
|
)
|
459
712
|
|
713
|
+
def __withDebugger(
|
714
|
+
self,
|
715
|
+
test_case: unittest.TestCase
|
716
|
+
) -> bool:
|
717
|
+
"""
|
718
|
+
Check if the given test case contains any debugging or dump calls.
|
719
|
+
|
720
|
+
This method inspects the source code of the provided test case to determine
|
721
|
+
whether it contains any lines that invoke debugging or dump functions, as
|
722
|
+
specified by the internal `__debbug_keywords` list (e.g., 'self.dd', 'self.dump').
|
723
|
+
It ignores commented lines and only considers actual code statements.
|
724
|
+
|
725
|
+
Parameters
|
726
|
+
----------
|
727
|
+
test_case : unittest.TestCase
|
728
|
+
The test case instance whose source code will be inspected.
|
729
|
+
|
730
|
+
Returns
|
731
|
+
-------
|
732
|
+
bool
|
733
|
+
True if any debug or dump keyword is found in the test case source code,
|
734
|
+
or if the internal debug flag (`__debbug`) is set. False otherwise.
|
735
|
+
|
736
|
+
Notes
|
737
|
+
-----
|
738
|
+
- The method uses reflection to retrieve the source code of the test case.
|
739
|
+
- Lines that are commented out are skipped during inspection.
|
740
|
+
- If an error occurs during source code retrieval or inspection, the method returns False.
|
741
|
+
"""
|
742
|
+
|
743
|
+
try:
|
744
|
+
|
745
|
+
# Retrieve the source code of the test case using reflection
|
746
|
+
method_name = getattr(test_case, "_testMethodName", None)
|
747
|
+
|
748
|
+
# If a method name is found, proceed to inspect its source code
|
749
|
+
if method_name:
|
750
|
+
|
751
|
+
# Get the source code of the specific test method
|
752
|
+
source = inspect.getsource(getattr(test_case, method_name))
|
753
|
+
|
754
|
+
# Check each line of the source code
|
755
|
+
for line in source.splitlines():
|
756
|
+
|
757
|
+
# Strip leading and trailing whitespace from the line
|
758
|
+
stripped = line.strip()
|
759
|
+
|
760
|
+
# Skip lines that are commented out
|
761
|
+
if stripped.startswith('#') or re.match(r'^\s*#', line):
|
762
|
+
continue
|
763
|
+
|
764
|
+
# If any debug keyword is present in the line, return True
|
765
|
+
if any(keyword in line for keyword in self.__debbug_keywords):
|
766
|
+
self.__live_console = False if self.__live_console is True else self.__live_console
|
767
|
+
return True
|
768
|
+
|
769
|
+
except Exception:
|
770
|
+
|
771
|
+
# If any error occurs during inspection, return False
|
772
|
+
return False
|
773
|
+
|
774
|
+
# No debug keywords found; return False
|
775
|
+
return False
|
776
|
+
|
460
777
|
def run(
|
461
778
|
self,
|
462
779
|
performance_counter: IPerformanceCounter
|
@@ -479,7 +796,7 @@ class UnitTest(IUnitTest):
|
|
479
796
|
performance_counter.start()
|
480
797
|
|
481
798
|
# Length of all tests in the suite
|
482
|
-
total_tests =
|
799
|
+
total_tests = self.getTestCount()
|
483
800
|
|
484
801
|
# If no tests are found, print a message and return early
|
485
802
|
if total_tests == 0:
|
@@ -493,21 +810,16 @@ class UnitTest(IUnitTest):
|
|
493
810
|
)
|
494
811
|
|
495
812
|
# Execute the test suite and capture result, output, and error buffers
|
496
|
-
result
|
497
|
-
|
498
|
-
|
813
|
+
result = self.__printer.executePanel(
|
814
|
+
func=self.__runSuite,
|
815
|
+
live_console=self.__live_console
|
499
816
|
)
|
500
817
|
|
501
|
-
# Store the captured output and error buffers as strings
|
502
|
-
self.__output_buffer = output_buffer.getvalue()
|
503
|
-
self.__error_buffer = error_buffer.getvalue()
|
504
|
-
|
505
818
|
# Calculate execution time in milliseconds
|
506
819
|
performance_counter.stop()
|
507
|
-
execution_time = performance_counter.getSeconds()
|
508
820
|
|
509
821
|
# Generate a summary of the test results
|
510
|
-
summary = self.__generateSummary(result,
|
822
|
+
summary = self.__generateSummary(result, performance_counter.getSeconds())
|
511
823
|
|
512
824
|
# Display the test results using the printer
|
513
825
|
self.__printer.displayResults(summary=summary)
|
@@ -523,296 +835,192 @@ class UnitTest(IUnitTest):
|
|
523
835
|
return summary
|
524
836
|
|
525
837
|
def __flattenTestSuite(
|
526
|
-
self,
|
527
|
-
suite: unittest.TestSuite
|
528
|
-
) -> List[unittest.TestCase]:
|
529
|
-
"""
|
530
|
-
Recursively flattens a unittest.TestSuite into a list of unique unittest.TestCase instances.
|
531
|
-
|
532
|
-
Parameters
|
533
|
-
----------
|
534
|
-
suite : unittest.TestSuite
|
535
|
-
The test suite to be flattened.
|
536
|
-
|
537
|
-
Returns
|
538
|
-
-------
|
539
|
-
List[unittest.TestCase]
|
540
|
-
A flat list containing unique unittest.TestCase instances extracted from the suite.
|
541
|
-
|
542
|
-
Notes
|
543
|
-
-----
|
544
|
-
Test uniqueness is determined by a shortened test identifier (the last two components of the test id).
|
545
|
-
This helps avoid duplicate test cases in the returned list.
|
546
|
-
"""
|
547
|
-
|
548
|
-
# Initialize an empty list to hold unique test cases and a set to track seen test IDs
|
549
|
-
tests = []
|
550
|
-
seen_ids = set()
|
551
|
-
|
552
|
-
# Recursive function to flatten the test suite
|
553
|
-
def _flatten(item):
|
554
|
-
if isinstance(item, unittest.TestSuite):
|
555
|
-
for sub_item in item:
|
556
|
-
_flatten(sub_item)
|
557
|
-
elif hasattr(item, "id"):
|
558
|
-
test_id = item.id()
|
559
|
-
|
560
|
-
# Use the last two components of the test id for uniqueness
|
561
|
-
parts = test_id.split('.')
|
562
|
-
if len(parts) >= 2:
|
563
|
-
short_id = '.'.join(parts[-2:])
|
564
|
-
else:
|
565
|
-
short_id = test_id
|
566
|
-
if short_id not in seen_ids:
|
567
|
-
seen_ids.add(short_id)
|
568
|
-
tests.append(item)
|
569
|
-
|
570
|
-
# Start the flattening process
|
571
|
-
_flatten(suite)
|
572
|
-
return tests
|
573
|
-
|
574
|
-
def __runSuite(
|
575
|
-
self
|
576
|
-
) -> Tuple[unittest.TestResult, io.StringIO, io.StringIO]:
|
577
|
-
"""
|
578
|
-
Executes the test suite according to the configured execution mode, capturing both standard output and error streams.
|
579
|
-
|
580
|
-
Returns
|
581
|
-
-------
|
582
|
-
tuple
|
583
|
-
result : unittest.TestResult
|
584
|
-
The result object containing the outcomes of the executed tests.
|
585
|
-
output_buffer : io.StringIO
|
586
|
-
Buffer capturing the standard output generated during test execution.
|
587
|
-
error_buffer : io.StringIO
|
588
|
-
Buffer capturing the standard error generated during test execution.
|
589
|
-
"""
|
590
|
-
|
591
|
-
# Initialize output and error buffers to capture test execution output
|
592
|
-
output_buffer = io.StringIO()
|
593
|
-
error_buffer = io.StringIO()
|
594
|
-
|
595
|
-
# Run tests in parallel mode using multiple workers
|
596
|
-
if self.__execution_mode == ExecutionMode.PARALLEL.value:
|
597
|
-
result = self.__runTestsInParallel(
|
598
|
-
output_buffer,
|
599
|
-
error_buffer
|
600
|
-
)
|
601
|
-
|
602
|
-
# Run tests sequentially
|
603
|
-
else:
|
604
|
-
result = self.__runTestsSequentially(
|
605
|
-
output_buffer,
|
606
|
-
error_buffer
|
607
|
-
)
|
608
|
-
|
609
|
-
# Return the result, output, and error buffers
|
610
|
-
return result, output_buffer, error_buffer
|
611
|
-
|
612
|
-
def __isFailedImport(
|
613
|
-
self,
|
614
|
-
test_case: unittest.TestCase
|
615
|
-
) -> bool:
|
616
|
-
"""
|
617
|
-
Check if the given test case is a failed import.
|
618
|
-
|
619
|
-
Parameters
|
620
|
-
----------
|
621
|
-
test_case : unittest.TestCase
|
622
|
-
The test case to check.
|
623
|
-
|
624
|
-
Returns
|
625
|
-
-------
|
626
|
-
bool
|
627
|
-
True if the test case is a failed import, False otherwise.
|
628
|
-
"""
|
629
|
-
|
630
|
-
return test_case.__class__.__name__ == "_FailedTest"
|
631
|
-
|
632
|
-
def __notFoundTestMethod(
|
633
|
-
self,
|
634
|
-
test_case: unittest.TestCase
|
635
|
-
) -> bool:
|
838
|
+
self,
|
839
|
+
suite: unittest.TestSuite
|
840
|
+
) -> List[unittest.TestCase]:
|
636
841
|
"""
|
637
|
-
|
842
|
+
Recursively flatten a unittest.TestSuite into a list of unique unittest.TestCase instances.
|
843
|
+
|
844
|
+
This method traverses the given test suite, recursively extracting all individual test cases,
|
845
|
+
while preserving their order and ensuring uniqueness by test ID. If a test name pattern is configured,
|
846
|
+
only test cases whose IDs match the regular expression are included.
|
638
847
|
|
639
848
|
Parameters
|
640
849
|
----------
|
641
|
-
|
642
|
-
The test
|
850
|
+
suite : unittest.TestSuite
|
851
|
+
The test suite to flatten.
|
643
852
|
|
644
853
|
Returns
|
645
854
|
-------
|
646
|
-
|
647
|
-
|
855
|
+
List[unittest.TestCase]
|
856
|
+
List of unique test case instances contained in the suite, optionally filtered by name pattern.
|
857
|
+
|
858
|
+
Raises
|
859
|
+
------
|
860
|
+
OrionisTestValueError
|
861
|
+
If the configured test name pattern is not a valid regular expression.
|
862
|
+
|
863
|
+
Notes
|
864
|
+
-----
|
865
|
+
- The returned list preserves the order in which test cases appear in the suite.
|
866
|
+
- If a test name pattern is set, only test cases matching the pattern are included.
|
867
|
+
- Uniqueness is enforced by test ID.
|
648
868
|
"""
|
869
|
+
# Determine if test name pattern filtering is enabled
|
870
|
+
regex = None
|
871
|
+
if self.__test_name_pattern:
|
872
|
+
try:
|
873
|
+
regex = re.compile(self.__test_name_pattern)
|
874
|
+
except re.error as e:
|
875
|
+
raise OrionisTestValueError(
|
876
|
+
f"The provided test name pattern is invalid: '{self.__test_name_pattern}'. "
|
877
|
+
f"Regular expression compilation error: {str(e)}. "
|
878
|
+
"Please check the pattern syntax and try again."
|
879
|
+
)
|
649
880
|
|
650
|
-
# Use
|
651
|
-
|
652
|
-
|
881
|
+
# Use an ordered dict to preserve order and uniqueness by test id
|
882
|
+
tests = {}
|
883
|
+
|
884
|
+
def _flatten(item):
|
885
|
+
if isinstance(item, unittest.TestSuite):
|
886
|
+
for sub_item in item:
|
887
|
+
_flatten(sub_item)
|
888
|
+
elif isinstance(item, unittest.TestCase):
|
889
|
+
test_id = item.id() if hasattr(item, "id") else None
|
890
|
+
if test_id and test_id not in tests:
|
891
|
+
if regex:
|
892
|
+
if regex.search(test_id):
|
893
|
+
tests[test_id] = item
|
894
|
+
else:
|
895
|
+
tests[test_id] = item
|
653
896
|
|
654
|
-
|
655
|
-
return
|
897
|
+
_flatten(suite)
|
898
|
+
return list(tests.values())
|
656
899
|
|
657
|
-
def
|
658
|
-
self
|
659
|
-
|
660
|
-
) -> bool:
|
900
|
+
def __runSuite(
|
901
|
+
self
|
902
|
+
) -> unittest.TestResult:
|
661
903
|
"""
|
662
|
-
|
904
|
+
Executes the test suite according to the configured execution mode, capturing both standard output and error streams.
|
663
905
|
|
664
|
-
This method
|
665
|
-
|
666
|
-
following the `__wrapped__` attribute to identify the presence of any decorators.
|
667
|
-
Decorated methods typically have a `__wrapped__` attribute that points to the
|
668
|
-
original unwrapped function.
|
906
|
+
This method determines whether to run the test suite sequentially or in parallel based on the configured execution mode.
|
907
|
+
It delegates execution to either `__runTestsSequentially` or `__runTestsInParallel`, and returns the aggregated test result.
|
669
908
|
|
670
909
|
Parameters
|
671
910
|
----------
|
672
|
-
|
673
|
-
The test case instance whose test method will be examined for decorators.
|
911
|
+
None
|
674
912
|
|
675
913
|
Returns
|
676
914
|
-------
|
677
|
-
|
678
|
-
|
679
|
-
|
915
|
+
unittest.TestResult
|
916
|
+
The aggregated result object containing the outcomes of all executed test cases, including
|
917
|
+
detailed per-test results, aggregated statistics, and error information.
|
680
918
|
|
681
919
|
Notes
|
682
920
|
-----
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
the method returns True.
|
921
|
+
- If the execution mode is set to parallel, tests are run concurrently using multiple workers.
|
922
|
+
- If the execution mode is sequential, tests are run one after another.
|
923
|
+
- The returned result object contains all test outcomes, including successes, failures, errors, skips, and custom metadata.
|
687
924
|
"""
|
688
925
|
|
689
|
-
#
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
decorators = []
|
694
|
-
|
695
|
-
# Check if the method has the __wrapped__ attribute, indicating it's decorated
|
696
|
-
if hasattr(test_method, '__wrapped__'):
|
697
|
-
# Start with the outermost decorated method
|
698
|
-
original = test_method
|
699
|
-
|
700
|
-
# Traverse the decorator chain by following __wrapped__ attributes
|
701
|
-
while hasattr(original, '__wrapped__'):
|
702
|
-
# Collect decorator name information for tracking purposes
|
703
|
-
if hasattr(original, '__qualname__'):
|
704
|
-
# Prefer __qualname__ as it provides more detailed naming information
|
705
|
-
decorators.append(original.__qualname__)
|
706
|
-
elif hasattr(original, '__name__'):
|
707
|
-
# Fall back to __name__ if __qualname__ is not available
|
708
|
-
decorators.append(original.__name__)
|
709
|
-
|
710
|
-
# Move to the next level in the decorator chain
|
711
|
-
original = original.__wrapped__
|
926
|
+
# Run tests in parallel mode using multiple workers if configured
|
927
|
+
if self.__execution_mode == ExecutionMode.PARALLEL.value:
|
928
|
+
# Execute tests concurrently and aggregate results
|
929
|
+
result = self.__runTestsInParallel()
|
712
930
|
|
713
|
-
#
|
714
|
-
|
715
|
-
|
931
|
+
# Otherwise, run tests sequentially
|
932
|
+
else:
|
933
|
+
# Execute tests one by one and aggregate results
|
934
|
+
result = self.__runTestsSequentially()
|
716
935
|
|
717
|
-
# Return
|
718
|
-
return
|
936
|
+
# Return the aggregated test result object
|
937
|
+
return result
|
719
938
|
|
720
|
-
def
|
721
|
-
self
|
939
|
+
def __resolveTestDependencies(
|
940
|
+
self,
|
941
|
+
test_case: unittest.TestCase
|
722
942
|
) -> unittest.TestSuite:
|
723
943
|
"""
|
724
|
-
|
944
|
+
Inject dependencies into a single test case if required, returning a TestSuite containing the resolved test case.
|
725
945
|
|
726
|
-
This method
|
727
|
-
|
728
|
-
|
729
|
-
|
946
|
+
This method uses reflection to inspect the test method's dependencies. If all dependencies are resolved,
|
947
|
+
it injects them using the application's resolver. If there are unresolved dependencies, the original test case
|
948
|
+
is returned as-is. Decorated methods and failed imports are also returned without modification. The returned
|
949
|
+
TestSuite contains the test case with dependencies injected if applicable.
|
950
|
+
|
951
|
+
Parameters
|
952
|
+
----------
|
953
|
+
test_case : unittest.TestCase
|
954
|
+
The test case instance to resolve dependencies for.
|
730
955
|
|
731
956
|
Returns
|
732
957
|
-------
|
733
958
|
unittest.TestSuite
|
734
|
-
A
|
959
|
+
A TestSuite containing the test case with dependencies injected if required.
|
960
|
+
If dependency injection is not possible or fails, the original test case is returned as-is within the suite.
|
735
961
|
|
736
962
|
Raises
|
737
963
|
------
|
738
964
|
OrionisTestValueError
|
739
|
-
If
|
740
|
-
"""
|
741
|
-
|
742
|
-
# Create a new TestSuite to hold the resolved test cases
|
743
|
-
flattened_suite = unittest.TestSuite()
|
965
|
+
If the test method has unresolved dependencies.
|
744
966
|
|
745
|
-
|
746
|
-
|
967
|
+
Notes
|
968
|
+
-----
|
969
|
+
- Uses reflection to determine method dependencies.
|
970
|
+
- If dependencies are resolved, injects them into the test method.
|
971
|
+
- If dependencies are unresolved or an error occurs, the original test case is returned.
|
972
|
+
- The returned value is always a unittest.TestSuite containing the test case (with or without injected dependencies).
|
973
|
+
"""
|
747
974
|
|
748
|
-
|
749
|
-
|
750
|
-
flattened_suite.addTest(test_case)
|
751
|
-
continue
|
975
|
+
# Create a new TestSuite to hold the resolved test case
|
976
|
+
suite = unittest.TestSuite()
|
752
977
|
|
753
|
-
|
754
|
-
if self.__notFoundTestMethod(test_case):
|
755
|
-
flattened_suite.addTest(test_case)
|
756
|
-
continue
|
978
|
+
try:
|
757
979
|
|
758
|
-
#
|
759
|
-
|
760
|
-
flattened_suite.addTest(test_case)
|
761
|
-
continue
|
980
|
+
# Get the reflection instance for the test case
|
981
|
+
rf_instance = ReflectionInstance(test_case)
|
762
982
|
|
763
|
-
|
983
|
+
# Get the test method name
|
984
|
+
method_name = getattr(test_case, "_testMethodName", None)
|
764
985
|
|
765
|
-
|
766
|
-
|
767
|
-
dependencies = rf_instance.getMethodDependencies(
|
768
|
-
method_name=getattr(test_case, "_testMethodName")
|
769
|
-
)
|
986
|
+
# Get method dependencies (resolved and unresolved)
|
987
|
+
dependencies = rf_instance.getMethodDependencies(method_name)
|
770
988
|
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
continue
|
989
|
+
# If there are unresolved dependencies, return the original test case as-is
|
990
|
+
if dependencies.unresolved:
|
991
|
+
return test_case
|
775
992
|
|
776
|
-
|
777
|
-
|
778
|
-
raise OrionisTestValueError(
|
779
|
-
f"Test method '{getattr(test_case, "_testMethodName")}' in class '{test_case.__class__.__name__}' has unresolved dependencies: {dependencies.unresolved}. "
|
780
|
-
"Please ensure all dependencies are correctly defined and available."
|
781
|
-
)
|
993
|
+
# If there are resolved dependencies, inject them into the test method
|
994
|
+
if dependencies.resolved:
|
782
995
|
|
783
|
-
# Get the
|
996
|
+
# Get the test class and original method
|
784
997
|
test_class = rf_instance.getClass()
|
785
|
-
original_method = getattr(test_class,
|
998
|
+
original_method = getattr(test_class, method_name)
|
786
999
|
|
787
|
-
# Resolve
|
788
|
-
|
789
|
-
rf_instance.getClassName(),
|
790
|
-
dependencies
|
791
|
-
)
|
1000
|
+
# Resolve dependencies using the application container
|
1001
|
+
resolved_args = self.__app.resolveDependencyArguments(rf_instance.getClassName(), dependencies)
|
792
1002
|
|
793
|
-
#
|
794
|
-
def
|
795
|
-
|
796
|
-
return original_test(self_instance, **resolved_args)
|
797
|
-
return wrapper
|
1003
|
+
# Define a wrapper function to inject dependencies
|
1004
|
+
def wrapper(self_instance):
|
1005
|
+
return original_method(self_instance, **resolved_args)
|
798
1006
|
|
799
1007
|
# Bind the wrapped method to the test case instance
|
800
|
-
|
801
|
-
|
802
|
-
setattr(test_case, getattr(test_case, "_testMethodName"), bound_method)
|
803
|
-
flattened_suite.addTest(test_case)
|
1008
|
+
bound_method = wrapper.__get__(test_case, test_case.__class__)
|
1009
|
+
setattr(test_case, method_name, bound_method)
|
804
1010
|
|
805
|
-
|
1011
|
+
# Add the test case to the suite (with injected dependencies if applicable)
|
1012
|
+
suite.addTest(test_case)
|
806
1013
|
|
807
|
-
|
808
|
-
|
1014
|
+
# Return the TestSuite containing the resolved test case
|
1015
|
+
return suite
|
809
1016
|
|
810
|
-
|
1017
|
+
except Exception as e:
|
1018
|
+
|
1019
|
+
# On any error, return the original test case without injection
|
1020
|
+
return test_case
|
811
1021
|
|
812
1022
|
def __runTestsSequentially(
|
813
|
-
self
|
814
|
-
output_buffer: io.StringIO,
|
815
|
-
error_buffer: io.StringIO
|
1023
|
+
self
|
816
1024
|
) -> unittest.TestResult:
|
817
1025
|
"""
|
818
1026
|
Executes all test cases in the test suite sequentially, capturing standard output and error streams.
|
@@ -842,27 +1050,20 @@ class UnitTest(IUnitTest):
|
|
842
1050
|
"""
|
843
1051
|
|
844
1052
|
# Initialize output and error buffers to capture test execution output
|
845
|
-
result = None
|
1053
|
+
result: unittest.TestResult = None
|
846
1054
|
|
847
1055
|
# Iterate through all resolved test cases in the suite
|
848
|
-
for case in self.
|
1056
|
+
for case in self.__flatten_test_suite:
|
849
1057
|
|
850
|
-
|
851
|
-
|
852
|
-
|
853
|
-
|
854
|
-
)
|
1058
|
+
runner = unittest.TextTestRunner(
|
1059
|
+
stream=io.StringIO(),
|
1060
|
+
verbosity=self.__verbosity,
|
1061
|
+
failfast=self.__fail_fast,
|
1062
|
+
resultclass=self.__customResultClass()
|
1063
|
+
)
|
855
1064
|
|
856
|
-
#
|
857
|
-
|
858
|
-
runner = unittest.TextTestRunner(
|
859
|
-
stream=output_buffer,
|
860
|
-
verbosity=self.__verbosity,
|
861
|
-
failfast=self.__fail_fast,
|
862
|
-
resultclass=self.__customResultClass()
|
863
|
-
)
|
864
|
-
# Run the current test case and obtain the result
|
865
|
-
single_result: IOrionisTestResult = runner.run(unittest.TestSuite([case]))
|
1065
|
+
# Run the current test case and obtain the result
|
1066
|
+
single_result: IOrionisTestResult = runner.run(unittest.TestSuite([case]))
|
866
1067
|
|
867
1068
|
# Print the result of the current test case using the printer
|
868
1069
|
self.__printer.unittestResult(single_result.test_results[0])
|
@@ -877,41 +1078,32 @@ class UnitTest(IUnitTest):
|
|
877
1078
|
return result
|
878
1079
|
|
879
1080
|
def __runTestsInParallel(
|
880
|
-
self
|
881
|
-
output_buffer: io.StringIO,
|
882
|
-
error_buffer: io.StringIO
|
1081
|
+
self
|
883
1082
|
) -> unittest.TestResult:
|
884
1083
|
"""
|
885
1084
|
Executes all test cases in the test suite concurrently using a thread pool and aggregates their results.
|
886
1085
|
|
887
1086
|
Parameters
|
888
1087
|
----------
|
889
|
-
|
890
|
-
Buffer to capture the standard output generated during test execution.
|
891
|
-
error_buffer : io.StringIO
|
892
|
-
Buffer to capture the standard error generated during test execution.
|
1088
|
+
None
|
893
1089
|
|
894
1090
|
Returns
|
895
1091
|
-------
|
896
1092
|
unittest.TestResult
|
897
|
-
|
1093
|
+
A combined `unittest.TestResult` object containing the outcomes of all executed test cases.
|
1094
|
+
This includes detailed per-test results, aggregated statistics, error information, and custom metadata.
|
898
1095
|
|
899
1096
|
Notes
|
900
1097
|
-----
|
901
|
-
Each test case is executed in a separate thread using
|
902
|
-
Results from all threads are merged into a single result object.
|
903
|
-
Output and error streams are redirected for
|
904
|
-
If fail-fast is enabled, execution stops as soon as a failure is detected.
|
1098
|
+
- Each test case is executed in a separate thread using `ThreadPoolExecutor`.
|
1099
|
+
- Results from all threads are merged into a single aggregated result object.
|
1100
|
+
- Output and error streams are redirected for each test case.
|
1101
|
+
- If fail-fast is enabled, execution stops as soon as a failure is detected and remaining tests are cancelled.
|
1102
|
+
- The returned result object contains all test outcomes, including successes, failures, errors, skips, and custom metadata.
|
905
1103
|
"""
|
906
1104
|
|
907
|
-
#
|
908
|
-
|
909
|
-
|
910
|
-
# Get the custom result class for enhanced test tracking
|
911
|
-
result_class = self.__customResultClass()
|
912
|
-
|
913
|
-
# Create a combined result object to aggregate all individual test results
|
914
|
-
combined_result = result_class(io.StringIO(), descriptions=True, verbosity=self.__verbosity)
|
1105
|
+
# Initialize the aggregated result object
|
1106
|
+
result: unittest.TestResult = None
|
915
1107
|
|
916
1108
|
# Define a function to run a single test case and return its result
|
917
1109
|
def run_single_test(test):
|
@@ -919,36 +1111,40 @@ class UnitTest(IUnitTest):
|
|
919
1111
|
stream=io.StringIO(),
|
920
1112
|
verbosity=self.__verbosity,
|
921
1113
|
failfast=False,
|
922
|
-
resultclass=
|
1114
|
+
resultclass=self.__customResultClass()
|
923
1115
|
)
|
924
1116
|
return runner.run(unittest.TestSuite([test]))
|
925
1117
|
|
926
|
-
#
|
927
|
-
with
|
1118
|
+
# Create a thread pool with the configured number of workers
|
1119
|
+
with ThreadPoolExecutor(max_workers=self.__max_workers) as executor:
|
928
1120
|
|
929
|
-
#
|
930
|
-
|
1121
|
+
# Submit all test cases to the thread pool for execution
|
1122
|
+
futures = [executor.submit(run_single_test, test) for test in self.__flatten_test_suite]
|
931
1123
|
|
932
|
-
|
933
|
-
|
1124
|
+
# As each test completes, merge its result into the combined result
|
1125
|
+
for future in as_completed(futures):
|
934
1126
|
|
935
|
-
#
|
936
|
-
|
937
|
-
test_result = future.result()
|
938
|
-
self.__mergeTestResults(combined_result, test_result)
|
1127
|
+
# Get the result of the completed test case
|
1128
|
+
single_result: IOrionisTestResult = future.result()
|
939
1129
|
|
940
|
-
|
941
|
-
|
942
|
-
|
943
|
-
f.cancel()
|
944
|
-
break
|
1130
|
+
# Print the result of the current test case using the printer
|
1131
|
+
# Ensure print goes to the real stdout even inside redirected context
|
1132
|
+
self.__printer.unittestResult(single_result.test_results[0])
|
945
1133
|
|
946
|
-
|
947
|
-
|
948
|
-
|
1134
|
+
# Merge the result of the current test case into the aggregated result
|
1135
|
+
if result is None:
|
1136
|
+
result = single_result
|
1137
|
+
else:
|
1138
|
+
self.__mergeTestResults(result, single_result)
|
1139
|
+
|
1140
|
+
# If fail-fast is enabled and a failure occurs, cancel remaining tests
|
1141
|
+
if self.__fail_fast and not result.wasSuccessful():
|
1142
|
+
for f in futures:
|
1143
|
+
f.cancel()
|
1144
|
+
break
|
949
1145
|
|
950
1146
|
# Return the aggregated result containing all test outcomes
|
951
|
-
return
|
1147
|
+
return result
|
952
1148
|
|
953
1149
|
def __mergeTestResults(
|
954
1150
|
self,
|
@@ -956,47 +1152,51 @@ class UnitTest(IUnitTest):
|
|
956
1152
|
individual_result: unittest.TestResult
|
957
1153
|
) -> None:
|
958
1154
|
"""
|
959
|
-
Merge the results of two unittest.TestResult objects into a single result.
|
1155
|
+
Merge the results of two unittest.TestResult objects into a single aggregated result.
|
1156
|
+
|
1157
|
+
This method updates the `combined_result` in place by aggregating test statistics and detailed results
|
1158
|
+
from `individual_result`. It ensures that all test outcomes, including failures, errors, skipped tests,
|
1159
|
+
expected failures, unexpected successes, and custom test result entries, are merged for comprehensive reporting.
|
960
1160
|
|
961
1161
|
Parameters
|
962
1162
|
----------
|
963
1163
|
combined_result : unittest.TestResult
|
964
|
-
The
|
1164
|
+
The result object to be updated with merged statistics and details.
|
965
1165
|
individual_result : unittest.TestResult
|
966
|
-
The
|
1166
|
+
The result object whose statistics and details will be merged into `combined_result`.
|
967
1167
|
|
968
1168
|
Returns
|
969
1169
|
-------
|
970
1170
|
None
|
971
|
-
This method does not return
|
1171
|
+
This method does not return any value. The `combined_result` is updated in place with merged data.
|
972
1172
|
|
973
1173
|
Notes
|
974
1174
|
-----
|
975
|
-
|
976
|
-
|
977
|
-
|
978
|
-
|
1175
|
+
- Increments the total number of tests run.
|
1176
|
+
- Extends lists of failures, errors, skipped tests, expected failures, and unexpected successes.
|
1177
|
+
- If present, merges custom `test_results` entries for detailed per-test reporting.
|
1178
|
+
- This method is used to aggregate results from parallel or sequential test execution.
|
979
1179
|
"""
|
980
1180
|
|
981
1181
|
# Increment the total number of tests run
|
982
1182
|
combined_result.testsRun += individual_result.testsRun
|
983
1183
|
|
984
|
-
#
|
1184
|
+
# Merge failures from the individual result
|
985
1185
|
combined_result.failures.extend(individual_result.failures)
|
986
1186
|
|
987
|
-
#
|
1187
|
+
# Merge errors from the individual result
|
988
1188
|
combined_result.errors.extend(individual_result.errors)
|
989
1189
|
|
990
|
-
#
|
1190
|
+
# Merge skipped tests from the individual result
|
991
1191
|
combined_result.skipped.extend(individual_result.skipped)
|
992
1192
|
|
993
|
-
#
|
1193
|
+
# Merge expected failures from the individual result
|
994
1194
|
combined_result.expectedFailures.extend(individual_result.expectedFailures)
|
995
1195
|
|
996
|
-
#
|
1196
|
+
# Merge unexpected successes from the individual result
|
997
1197
|
combined_result.unexpectedSuccesses.extend(individual_result.unexpectedSuccesses)
|
998
1198
|
|
999
|
-
#
|
1199
|
+
# Merge custom detailed test results if available
|
1000
1200
|
if hasattr(individual_result, 'test_results'):
|
1001
1201
|
if not hasattr(combined_result, 'test_results'):
|
1002
1202
|
combined_result.test_results = []
|
@@ -1022,7 +1222,7 @@ class UnitTest(IUnitTest):
|
|
1022
1222
|
includes execution time, error details, and test metadata, which are stored
|
1023
1223
|
in a list of TestResult objects for later reporting and analysis.
|
1024
1224
|
"""
|
1025
|
-
this = self
|
1225
|
+
this: "UnitTest" = self
|
1026
1226
|
|
1027
1227
|
class OrionisTestResult(unittest.TextTestResult):
|
1028
1228
|
|
@@ -1191,26 +1391,45 @@ class UnitTest(IUnitTest):
|
|
1191
1391
|
execution_time: float
|
1192
1392
|
) -> Dict[str, Any]:
|
1193
1393
|
"""
|
1194
|
-
|
1195
|
-
|
1196
|
-
|
1394
|
+
Generate a summary dictionary of the test suite execution.
|
1395
|
+
|
1396
|
+
This method aggregates statistics, timing, and detailed results for each test case in the suite.
|
1397
|
+
It optionally persists the summary and/or generates a web report if configured in the test manager.
|
1197
1398
|
|
1198
1399
|
Parameters
|
1199
1400
|
----------
|
1200
1401
|
result : unittest.TestResult
|
1201
|
-
The result object containing details of the test execution.
|
1402
|
+
The result object containing details of the test execution, including per-test outcomes.
|
1202
1403
|
execution_time : float
|
1203
1404
|
The total execution time of the test suite in seconds.
|
1204
1405
|
|
1205
1406
|
Returns
|
1206
1407
|
-------
|
1207
|
-
|
1208
|
-
|
1408
|
+
Dict[str, Any]
|
1409
|
+
Dictionary containing:
|
1410
|
+
- total_tests: int
|
1411
|
+
Total number of tests executed.
|
1412
|
+
- passed: int
|
1413
|
+
Number of tests that passed.
|
1414
|
+
- failed: int
|
1415
|
+
Number of tests that failed.
|
1416
|
+
- errors: int
|
1417
|
+
Number of tests that raised errors.
|
1418
|
+
- skipped: int
|
1419
|
+
Number of tests that were skipped.
|
1420
|
+
- total_time: float
|
1421
|
+
Total execution time in seconds.
|
1422
|
+
- success_rate: float
|
1423
|
+
Percentage of tests that passed.
|
1424
|
+
- test_details: List[dict]
|
1425
|
+
List of dictionaries with per-test details (ID, class, method, status, timing, error info, traceback, etc.).
|
1426
|
+
- timestamp: str
|
1427
|
+
ISO-formatted timestamp of when the summary was generated.
|
1209
1428
|
|
1210
1429
|
Notes
|
1211
1430
|
-----
|
1212
|
-
- If persistence is enabled, the summary is saved to storage.
|
1213
|
-
- If web reporting is enabled, a web report is generated.
|
1431
|
+
- If persistence is enabled, the summary is saved to storage using the configured driver.
|
1432
|
+
- If web reporting is enabled, a web report is generated and a link is printed.
|
1214
1433
|
- The summary includes per-test details, overall statistics, and a timestamp.
|
1215
1434
|
"""
|
1216
1435
|
|
@@ -1219,7 +1438,7 @@ class UnitTest(IUnitTest):
|
|
1219
1438
|
for test_result in result.test_results:
|
1220
1439
|
rst: TestResult = test_result
|
1221
1440
|
|
1222
|
-
#
|
1441
|
+
# Extract traceback frames from the exception, if available
|
1223
1442
|
traceback_frames = []
|
1224
1443
|
if rst.exception and rst.exception.__traceback__:
|
1225
1444
|
tb = traceback.extract_tb(rst.exception.__traceback__)
|
@@ -1231,6 +1450,7 @@ class UnitTest(IUnitTest):
|
|
1231
1450
|
'code': frame.line
|
1232
1451
|
})
|
1233
1452
|
|
1453
|
+
# Build the per-test detail dictionary
|
1234
1454
|
test_details.append({
|
1235
1455
|
'id': rst.id,
|
1236
1456
|
'class': rst.class_name,
|
@@ -1271,7 +1491,7 @@ class UnitTest(IUnitTest):
|
|
1271
1491
|
if self.__web_report:
|
1272
1492
|
self.__handleWebReport(self.__result)
|
1273
1493
|
|
1274
|
-
# Return the summary dictionary
|
1494
|
+
# Return the summary dictionary containing all test statistics and details
|
1275
1495
|
return self.__result
|
1276
1496
|
|
1277
1497
|
def __handleWebReport(
|
@@ -1284,31 +1504,31 @@ class UnitTest(IUnitTest):
|
|
1284
1504
|
Parameters
|
1285
1505
|
----------
|
1286
1506
|
summary : dict
|
1287
|
-
|
1507
|
+
Dictionary containing the summary of test results to be used for web report generation.
|
1288
1508
|
|
1289
1509
|
Returns
|
1290
1510
|
-------
|
1291
1511
|
None
|
1512
|
+
This method does not return any value. It generates a web report and prints a link to it.
|
1292
1513
|
|
1293
1514
|
Notes
|
1294
1515
|
-----
|
1295
|
-
This method creates a web-based report for the given test results summary.
|
1296
|
-
It
|
1297
|
-
the
|
1298
|
-
|
1299
|
-
web report using the printer.
|
1516
|
+
This method creates a web-based report for the given test results summary using the `TestingResultRender` class.
|
1517
|
+
It passes the storage path, the summary result, and a persistence flag (True if persistence is enabled and the driver is set to 'sqlite').
|
1518
|
+
After rendering the report, it prints a link to the generated web report using the internal printer.
|
1519
|
+
The report is persisted only if configured to do so.
|
1300
1520
|
"""
|
1301
1521
|
|
1302
|
-
# Create a TestingResultRender instance
|
1303
|
-
#
|
1304
|
-
|
1305
|
-
|
1306
|
-
|
1307
|
-
persist=self.__persistent and self.__persistent_driver ==
|
1522
|
+
# Create a TestingResultRender instance to generate the web report.
|
1523
|
+
# The 'persist' flag is True only if persistence is enabled and the driver is 'sqlite'.
|
1524
|
+
html_report = TestingResultRender(
|
1525
|
+
result = summary,
|
1526
|
+
storage_path = self.__storage,
|
1527
|
+
persist = self.__persistent and self.__persistent_driver == PersistentDrivers.SQLITE.value
|
1308
1528
|
)
|
1309
1529
|
|
1310
|
-
# Print the link to the generated web report
|
1311
|
-
self.__printer.linkWebReport(
|
1530
|
+
# Print the link to the generated web report using the printer.
|
1531
|
+
self.__printer.linkWebReport(html_report.render())
|
1312
1532
|
|
1313
1533
|
def __handlePersistResults(
|
1314
1534
|
self,
|
@@ -1320,7 +1540,12 @@ class UnitTest(IUnitTest):
|
|
1320
1540
|
Parameters
|
1321
1541
|
----------
|
1322
1542
|
summary : dict
|
1323
|
-
|
1543
|
+
Dictionary containing the test results and metadata to be persisted.
|
1544
|
+
|
1545
|
+
Returns
|
1546
|
+
-------
|
1547
|
+
None
|
1548
|
+
This method does not return any value. It performs persistence operations as a side effect.
|
1324
1549
|
|
1325
1550
|
Raises
|
1326
1551
|
------
|
@@ -1331,200 +1556,158 @@ class UnitTest(IUnitTest):
|
|
1331
1556
|
|
1332
1557
|
Notes
|
1333
1558
|
-----
|
1334
|
-
This method
|
1335
|
-
If the driver is set to 'sqlite', the summary is stored in a SQLite database using the TestLogs class.
|
1336
|
-
If the driver is set to 'json', the summary is saved as a JSON file in the specified storage directory,
|
1337
|
-
|
1338
|
-
and handles any errors that may
|
1559
|
+
This method saves the test results summary according to the configured persistence driver.
|
1560
|
+
- If the driver is set to 'sqlite', the summary is stored in a SQLite database using the TestLogs class.
|
1561
|
+
- If the driver is set to 'json', the summary is saved as a JSON file in the specified storage directory,
|
1562
|
+
with a filename based on the current timestamp.
|
1563
|
+
The method ensures that the target directory exists before writing files, and handles any errors that may
|
1564
|
+
occur during file or database operations.
|
1339
1565
|
"""
|
1566
|
+
|
1340
1567
|
try:
|
1341
1568
|
|
1342
|
-
#
|
1569
|
+
# Persist results using SQLite database if configured
|
1343
1570
|
if self.__persistent_driver == PersistentDrivers.SQLITE.value:
|
1344
|
-
|
1345
|
-
history.create(summary)
|
1571
|
+
TestLogs(self.__storage).create(summary)
|
1346
1572
|
|
1347
|
-
#
|
1573
|
+
# Persist results as a JSON file if configured
|
1348
1574
|
elif self.__persistent_driver == PersistentDrivers.JSON.value:
|
1349
|
-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
1350
|
-
log_path = Path(self.__storage) / f"{timestamp}_test_results.json"
|
1351
1575
|
|
1352
|
-
#
|
1576
|
+
# Generate a unique filename based on the current timestamp
|
1577
|
+
timestamp = str(int(datetime.now().timestamp()))
|
1578
|
+
log_path = Path(self.__storage) / f"{timestamp}.json"
|
1579
|
+
|
1580
|
+
# Ensure the parent directory exists before writing the file
|
1353
1581
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
1354
1582
|
|
1355
|
-
# Write the summary to the JSON file
|
1583
|
+
# Write the summary dictionary to the JSON file
|
1356
1584
|
with open(log_path, 'w', encoding='utf-8') as log:
|
1357
1585
|
json.dump(summary, log, indent=4)
|
1586
|
+
|
1358
1587
|
except OSError as e:
|
1359
1588
|
|
1360
1589
|
# Raise an error if directory creation or file writing fails
|
1361
|
-
raise OSError(
|
1590
|
+
raise OSError(
|
1591
|
+
f"Failed to create directories or write the test results file: {str(e)}. "
|
1592
|
+
"Please check the storage path permissions and ensure there is enough disk space."
|
1593
|
+
)
|
1594
|
+
|
1362
1595
|
except Exception as e:
|
1363
1596
|
|
1364
1597
|
# Raise a persistence error for any other exceptions
|
1365
|
-
raise OrionisTestPersistenceError(
|
1598
|
+
raise OrionisTestPersistenceError(
|
1599
|
+
f"An unexpected error occurred while persisting test results: {str(e)}. "
|
1600
|
+
"Please verify the persistence configuration and check for possible issues with the storage backend."
|
1601
|
+
)
|
1366
1602
|
|
1367
|
-
def
|
1368
|
-
self
|
1369
|
-
|
1370
|
-
pattern: str
|
1371
|
-
) -> unittest.TestSuite:
|
1603
|
+
def getDiscoveredTestCases(
|
1604
|
+
self
|
1605
|
+
) -> List[unittest.TestCase]:
|
1372
1606
|
"""
|
1373
|
-
|
1607
|
+
Return a list of all discovered test case classes in the test suite.
|
1374
1608
|
|
1375
|
-
|
1376
|
-
|
1377
|
-
|
1378
|
-
The test suite containing the tests to be filtered.
|
1379
|
-
pattern : str
|
1380
|
-
Regular expression pattern to match against test names (test IDs).
|
1609
|
+
This method provides access to all unique test case classes that have been discovered
|
1610
|
+
during test suite initialization and loading. It does not execute any tests, but simply
|
1611
|
+
reports the discovered test case classes.
|
1381
1612
|
|
1382
1613
|
Returns
|
1383
1614
|
-------
|
1384
|
-
unittest.
|
1385
|
-
A
|
1386
|
-
|
1387
|
-
Raises
|
1388
|
-
------
|
1389
|
-
OrionisTestValueError
|
1390
|
-
If the provided pattern is not a valid regular expression.
|
1615
|
+
List[unittest.TestCase]
|
1616
|
+
A list of unique `unittest.TestCase` classes that have been discovered in the suite.
|
1391
1617
|
|
1392
1618
|
Notes
|
1393
1619
|
-----
|
1394
|
-
|
1395
|
-
|
1396
|
-
|
1620
|
+
- The returned list contains the test case classes, not instances or names.
|
1621
|
+
- The classes are derived from the `__class__` attribute of each discovered test case.
|
1622
|
+
- This method is useful for introspection or reporting purposes.
|
1397
1623
|
"""
|
1398
1624
|
|
1399
|
-
#
|
1400
|
-
|
1401
|
-
|
1402
|
-
try:
|
1403
|
-
|
1404
|
-
# Compile the provided regular expression pattern
|
1405
|
-
regex = re.compile(pattern)
|
1406
|
-
|
1407
|
-
except re.error as e:
|
1408
|
-
|
1409
|
-
# Raise a value error if the regex is invalid
|
1410
|
-
raise OrionisTestValueError(
|
1411
|
-
f"The provided test name pattern is invalid: '{pattern}'. "
|
1412
|
-
f"Regular expression compilation error: {str(e)}. "
|
1413
|
-
"Please check the pattern syntax and try again."
|
1414
|
-
)
|
1415
|
-
|
1416
|
-
# Iterate through all test cases in the flattened suite
|
1417
|
-
for test in self.__flattenTestSuite(suite):
|
1418
|
-
|
1419
|
-
# Add the test to the filtered suite if its ID matches the regex
|
1420
|
-
if regex.search(test.id()):
|
1421
|
-
filtered_suite.addTest(test)
|
1625
|
+
# Return all unique discovered test case classes as a list
|
1626
|
+
return list(self.__discovered_test_cases)
|
1422
1627
|
|
1423
|
-
|
1424
|
-
|
1425
|
-
|
1426
|
-
def __listMatchingModules(
|
1427
|
-
self,
|
1428
|
-
root_path: Path,
|
1429
|
-
test_path: Path,
|
1430
|
-
custom_path: Path,
|
1431
|
-
pattern_file: str
|
1432
|
-
) -> List[str]:
|
1628
|
+
def getDiscoveredModules(
|
1629
|
+
self
|
1630
|
+
) -> List:
|
1433
1631
|
"""
|
1434
|
-
|
1632
|
+
Return a list of all discovered test module names in the test suite.
|
1435
1633
|
|
1436
|
-
This method
|
1437
|
-
|
1438
|
-
the
|
1634
|
+
This method provides access to all unique test modules that have been discovered
|
1635
|
+
during test suite initialization and loading. It does not execute any tests, but simply
|
1636
|
+
reports the discovered module names.
|
1439
1637
|
|
1440
1638
|
Parameters
|
1441
1639
|
----------
|
1442
|
-
|
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).
|
1640
|
+
None
|
1450
1641
|
|
1451
1642
|
Returns
|
1452
1643
|
-------
|
1453
|
-
List[
|
1454
|
-
A list of
|
1644
|
+
List[str]
|
1645
|
+
A list of unique module names (as strings) that have been discovered in the suite.
|
1455
1646
|
|
1456
1647
|
Notes
|
1457
1648
|
-----
|
1458
|
-
-
|
1459
|
-
-
|
1460
|
-
-
|
1461
|
-
- If the relative path is '.', only the module name is used.
|
1462
|
-
- The method imports modules dynamically and returns them as objects.
|
1649
|
+
- The returned list contains the module names, not module objects.
|
1650
|
+
- The module names are derived from the `__module__` attribute of each discovered test case.
|
1651
|
+
- This method is useful for introspection or reporting purposes.
|
1463
1652
|
"""
|
1464
1653
|
|
1465
|
-
#
|
1466
|
-
|
1467
|
-
|
1468
|
-
# Use a set to avoid duplicate module imports.
|
1469
|
-
matched_folders = set()
|
1470
|
-
|
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:
|
1474
|
-
|
1475
|
-
# Check if the file matches the pattern and is a Python file.
|
1476
|
-
if regex.fullmatch(file) and file.endswith('.py'):
|
1477
|
-
|
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.
|
1481
|
-
|
1482
|
-
# Build the full module name.
|
1483
|
-
full_module = f"{ralative_path}.{module_name}" if ralative_path != '.' else module_name
|
1484
|
-
|
1485
|
-
# Import the module and add to the set.
|
1486
|
-
matched_folders.add(import_module(ValidModuleName(full_module)))
|
1487
|
-
|
1488
|
-
# Return the list of imported module objects.
|
1489
|
-
return list(matched_folders)
|
1654
|
+
# Return all unique discovered test module names as a list
|
1655
|
+
return list(self.__discovered_test_modules)
|
1490
1656
|
|
1491
|
-
def
|
1657
|
+
def getTestIds(
|
1492
1658
|
self
|
1493
1659
|
) -> List[str]:
|
1494
1660
|
"""
|
1495
|
-
|
1661
|
+
Return a list of all unique test IDs discovered in the test suite.
|
1662
|
+
|
1663
|
+
This method provides access to the unique identifiers (IDs) of all test cases
|
1664
|
+
that have been discovered and loaded into the suite. The IDs are collected from
|
1665
|
+
each `unittest.TestCase` instance during test discovery and are returned as a list
|
1666
|
+
of strings. This is useful for introspection, reporting, or filtering purposes.
|
1667
|
+
|
1668
|
+
Parameters
|
1669
|
+
----------
|
1670
|
+
None
|
1496
1671
|
|
1497
1672
|
Returns
|
1498
1673
|
-------
|
1499
|
-
|
1500
|
-
|
1674
|
+
List[str]
|
1675
|
+
A list of strings, where each string is the unique ID of a discovered test case.
|
1676
|
+
The IDs are generated by the `id()` method of each `unittest.TestCase` instance.
|
1677
|
+
|
1678
|
+
Notes
|
1679
|
+
-----
|
1680
|
+
- The returned list contains only unique test IDs.
|
1681
|
+
- This method does not execute any tests; it only reports the discovered IDs.
|
1682
|
+
- The IDs typically include the module, class, and method name for each test case.
|
1501
1683
|
"""
|
1502
|
-
|
1684
|
+
|
1685
|
+
# Return all unique discovered test IDs as a list
|
1686
|
+
return list(self.__discovered_test_ids)
|
1503
1687
|
|
1504
1688
|
def getTestCount(
|
1505
1689
|
self
|
1506
1690
|
) -> int:
|
1507
1691
|
"""
|
1508
|
-
|
1692
|
+
Return the total number of individual test cases discovered in the test suite.
|
1693
|
+
|
1694
|
+
This method calculates and returns the total number of test cases that have been
|
1695
|
+
discovered and loaded into the suite, including all modules and filtered tests.
|
1696
|
+
It uses the internal metadata collected during test discovery to provide an accurate count.
|
1509
1697
|
|
1510
1698
|
Returns
|
1511
1699
|
-------
|
1512
1700
|
int
|
1513
|
-
|
1514
|
-
"""
|
1515
|
-
return len(list(self.__flattenTestSuite(self.__suite)))
|
1701
|
+
The total number of individual test cases discovered and loaded in the suite.
|
1516
1702
|
|
1517
|
-
|
1518
|
-
|
1519
|
-
|
1703
|
+
Notes
|
1704
|
+
-----
|
1705
|
+
- The count reflects all tests after applying any name pattern or folder filtering.
|
1706
|
+
- This method does not execute any tests; it only reports the discovered count.
|
1520
1707
|
"""
|
1521
|
-
Clear all tests from the current test suite.
|
1522
1708
|
|
1523
|
-
|
1524
|
-
|
1525
|
-
None
|
1526
|
-
"""
|
1527
|
-
self.__suite = unittest.TestSuite()
|
1709
|
+
# Return the sum of all discovered test cases across modules
|
1710
|
+
return len(self.__discovered_test_ids)
|
1528
1711
|
|
1529
1712
|
def getResult(
|
1530
1713
|
self
|
@@ -1537,54 +1720,4 @@ class UnitTest(IUnitTest):
|
|
1537
1720
|
dict
|
1538
1721
|
Result of the executed test suite.
|
1539
1722
|
"""
|
1540
|
-
return self.__result
|
1541
|
-
|
1542
|
-
def getOutputBuffer(
|
1543
|
-
self
|
1544
|
-
) -> int:
|
1545
|
-
"""
|
1546
|
-
Get the output buffer used for capturing test results.
|
1547
|
-
|
1548
|
-
Returns
|
1549
|
-
-------
|
1550
|
-
int
|
1551
|
-
Output buffer containing the results of the test execution.
|
1552
|
-
"""
|
1553
|
-
return self.__output_buffer
|
1554
|
-
|
1555
|
-
def printOutputBuffer(
|
1556
|
-
self
|
1557
|
-
) -> None:
|
1558
|
-
"""
|
1559
|
-
Print the contents of the output buffer to the console.
|
1560
|
-
|
1561
|
-
Returns
|
1562
|
-
-------
|
1563
|
-
None
|
1564
|
-
"""
|
1565
|
-
self.__printer.print(self.__output_buffer)
|
1566
|
-
|
1567
|
-
def getErrorBuffer(
|
1568
|
-
self
|
1569
|
-
) -> int:
|
1570
|
-
"""
|
1571
|
-
Get the error buffer used for capturing test errors.
|
1572
|
-
|
1573
|
-
Returns
|
1574
|
-
-------
|
1575
|
-
int
|
1576
|
-
Error buffer containing errors encountered during test execution.
|
1577
|
-
"""
|
1578
|
-
return self.__error_buffer
|
1579
|
-
|
1580
|
-
def printErrorBuffer(
|
1581
|
-
self
|
1582
|
-
) -> None:
|
1583
|
-
"""
|
1584
|
-
Print the contents of the error buffer to the console.
|
1585
|
-
|
1586
|
-
Returns
|
1587
|
-
-------
|
1588
|
-
None
|
1589
|
-
"""
|
1590
|
-
self.__printer.print(self.__error_buffer)
|
1723
|
+
return self.__result
|