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.
@@ -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.__modules: List = []
113
+ self.__imported_modules: List = []
110
114
 
111
- # List to track discovered tests and their metadata
112
- self.__discovered_tests: List = []
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
- # Load the output and error buffers for capturing test execution output
118
- self.__loadOutputBuffer()
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
- print_result = ValidPrintResult(config.print_result)
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.__modules` list with the discovered and imported module objects.
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
- modules = set() # Use a set to avoid duplicate module imports
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.__modules.extend(modules)
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
- # Iterate through all imported test modules
390
- for test_module in self.__modules:
391
-
392
- # Load all tests from the current module
393
- module_suite = self.__loader.loadTestsFromModule(test_module)
394
-
395
- # Flatten the suite to get individual test cases
396
- flat_tests = self.__flattenTestSuite(module_suite)
397
-
398
- # Check for failed imports and raise a detailed error if found
399
- for test in flat_tests:
400
- if test.__class__.__name__ == "_FailedTest":
401
- error_message = ""
402
- if hasattr(test, "_exception"):
403
- error_message = str(test._exception)
404
- elif hasattr(test, "_outcome") and hasattr(test._outcome, "errors"):
405
- error_message = str(test._outcome.errors)
406
- else:
407
- error_message = str(test)
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"Failed to import test module: {test.id()}.\n"
410
- f"Error details: {error_message}\n"
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
- # Rebuild the suite with only valid tests
415
- valid_suite = unittest.TestSuite(flat_tests)
416
-
417
- # If a test name pattern is provided, filter tests by name
418
- if self.__test_name_pattern:
419
- valid_suite = self.__filterTestsByName(
420
- suite=valid_suite,
421
- pattern=self.__test_name_pattern
422
- )
682
+ # Update the progress bar after processing each module
683
+ progress.advance(task, advance=1)
423
684
 
424
- # If no tests are found, raise an error
425
- if not list(valid_suite):
426
- raise OrionisTestValueError(
427
- f"No tests found in module '{test_module.__name__}' matching file pattern '{self.__pattern}'"
428
- + (f", test name pattern '{self.__test_name_pattern}'" if self.__test_name_pattern else "")
429
- + ". Please check your patterns and test files."
430
- )
685
+ # Add debug tests first
686
+ self.__suite.addTests(debug_tests)
431
687
 
432
- # Add discovered tests to the main suite
433
- self.__suite.addTests(valid_suite)
688
+ # Then add normal tests
689
+ self.__suite.addTests(normal_tests)
434
690
 
435
- # Count the number of tests discovered
436
- test_count = len(list(self.__flattenTestSuite(valid_suite)))
691
+ # Flatten the entire suite for easier access later
692
+ self.__flatten_test_suite = self.__flattenTestSuite(self.__suite)
437
693
 
438
- # Append discovered tests information for reporting
439
- self.__discovered_tests.append({
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 = len(list(self.__flattenTestSuite(self.__suite)))
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, output_buffer, error_buffer = self.__printer.executePanel(
497
- flatten_test_suite=self.__flattenTestSuite(self.__suite),
498
- callable=self.__runSuite
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, execution_time)
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
- Check if the test case does not have a valid test method.
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
- test_case : unittest.TestCase
642
- The test case to check.
850
+ suite : unittest.TestSuite
851
+ The test suite to flatten.
643
852
 
644
853
  Returns
645
854
  -------
646
- bool
647
- True if the test case does not have a valid test method, False otherwise.
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 reflection to get the test method name
651
- rf_instance = ReflectionInstance(test_case)
652
- method_name = rf_instance.getAttribute("_testMethodName")
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
- # If no method name is found, return True indicating no valid test method
655
- return not method_name or not hasattr(test_case.__class__, method_name)
897
+ _flatten(suite)
898
+ return list(tests.values())
656
899
 
657
- def __isDecoratedMethod(
658
- self,
659
- test_case: unittest.TestCase
660
- ) -> bool:
900
+ def __runSuite(
901
+ self
902
+ ) -> unittest.TestResult:
661
903
  """
662
- Determine if the test case's test method is decorated (wrapped by decorators).
904
+ Executes the test suite according to the configured execution mode, capturing both standard output and error streams.
663
905
 
664
- This method examines the test method of a given test case to determine if it has been
665
- decorated with one or more Python decorators. It traverses the decorator chain by
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
- test_case : unittest.TestCase
673
- The test case instance whose test method will be examined for decorators.
911
+ None
674
912
 
675
913
  Returns
676
914
  -------
677
- bool
678
- True if the test method has one or more decorators applied to it, False if
679
- the test method is not decorated or if no test method is found.
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
- This method checks for decorators by examining the `__wrapped__` attribute chain.
684
- The method collects decorator names from `__qualname__` or `__name__` attributes
685
- as it traverses the wrapper chain. If any decorators are found in the chain,
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
- # Retrieve the test method from the test case's class using the test method name
690
- test_method = getattr(test_case.__class__, getattr(test_case, "_testMethodName"), None)
691
-
692
- # Initialize a list to store decorator information found during traversal
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
- # Return True if any decorators were found during the traversal
714
- if decorators:
715
- return True
931
+ # Otherwise, run tests sequentially
932
+ else:
933
+ # Execute tests one by one and aggregate results
934
+ result = self.__runTestsSequentially()
716
935
 
717
- # Return False if no decorators are found or if the method is not decorated
718
- return False
936
+ # Return the aggregated test result object
937
+ return result
719
938
 
720
- def __resolveFlattenedTestSuite(
721
- self
939
+ def __resolveTestDependencies(
940
+ self,
941
+ test_case: unittest.TestCase
722
942
  ) -> unittest.TestSuite:
723
943
  """
724
- Resolves and injects dependencies for all test cases in the current suite, returning a flattened TestSuite.
944
+ Inject dependencies into a single test case if required, returning a TestSuite containing the resolved test case.
725
945
 
726
- This method iterates through all test cases in the suite, checks for failed imports, decorated methods, and unresolved dependencies.
727
- For each test case, it uses reflection to determine the test method and its dependencies. If dependencies are required and can be resolved,
728
- it injects them using the application's resolver. If a test method has unresolved dependencies, an exception is raised.
729
- Decorated methods and failed imports are added as-is. The resulting TestSuite contains all test cases with dependencies injected where needed.
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 new TestSuite containing all test cases with dependencies injected as required.
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 any test method has unresolved dependencies that cannot be resolved by the resolver.
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
- # Iterate through all test cases in the flattened suite
746
- for test_case in self.__flattenTestSuite(self.__suite):
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
- # If the test case is a failed import, add it directly
749
- if self.__isFailedImport(test_case):
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
- # If no method name is found, add the test case as-is
754
- if self.__notFoundTestMethod(test_case):
755
- flattened_suite.addTest(test_case)
756
- continue
978
+ try:
757
979
 
758
- # If decorators are present, add the test case as-is
759
- if self.__isDecoratedMethod(test_case):
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
- try:
983
+ # Get the test method name
984
+ method_name = getattr(test_case, "_testMethodName", None)
764
985
 
765
- # Get the method's dependency signature
766
- rf_instance = ReflectionInstance(test_case)
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
- # If no dependencies are required or unresolved, add the test case as-is
772
- if ((not dependencies.resolved and not dependencies.unresolved) or (not dependencies.resolved and len(dependencies.unresolved) > 0)):
773
- flattened_suite.addTest(test_case)
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
- # If there are unresolved dependencies, raise an error
777
- if (len(dependencies.unresolved) > 0):
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 original test class and method
996
+ # Get the test class and original method
784
997
  test_class = rf_instance.getClass()
785
- original_method = getattr(test_class, getattr(test_case, "_testMethodName"))
998
+ original_method = getattr(test_class, method_name)
786
999
 
787
- # Resolve the dependencies using the application's resolver
788
- params = self.__app.resolveDependencyArguments(
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
- # Create a wrapper to inject resolved dependencies into the test method
794
- def create_test_wrapper(original_test, resolved_args: dict):
795
- def wrapper(self_instance):
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
- wrapped_method = create_test_wrapper(original_method, params)
801
- bound_method = wrapped_method.__get__(test_case, test_case.__class__)
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
- except Exception:
1011
+ # Add the test case to the suite (with injected dependencies if applicable)
1012
+ suite.addTest(test_case)
806
1013
 
807
- # If dependency resolution fails, add the original test case
808
- flattened_suite.addTest(test_case)
1014
+ # Return the TestSuite containing the resolved test case
1015
+ return suite
809
1016
 
810
- return flattened_suite
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.__resolveFlattenedTestSuite():
1056
+ for case in self.__flatten_test_suite:
849
1057
 
850
- # Ensure the test case is a valid unittest.TestCase instance
851
- if not isinstance(case, unittest.TestCase):
852
- raise OrionisTestValueError(
853
- f"Invalid test case type: Expected unittest.TestCase, got {type(case).__name__}."
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
- # Redirect output and error streams for the current test case
857
- with redirect_stdout(output_buffer), redirect_stderr(error_buffer):
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
- output_buffer : io.StringIO
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
- Combined result object containing the outcomes of all executed test cases.
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 a ThreadPoolExecutor.
902
- Results from all threads are merged into a single result object.
903
- Output and error streams are redirected for the entire parallel execution.
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
- # Resolve and flatten all test cases in the suite, injecting dependencies if needed
908
- test_cases = list(self.__resolveFlattenedTestSuite())
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=result_class
1114
+ resultclass=self.__customResultClass()
923
1115
  )
924
1116
  return runner.run(unittest.TestSuite([test]))
925
1117
 
926
- # Redirect output and error streams for the entire parallel execution
927
- with redirect_stdout(output_buffer), redirect_stderr(error_buffer):
1118
+ # Create a thread pool with the configured number of workers
1119
+ with ThreadPoolExecutor(max_workers=self.__max_workers) as executor:
928
1120
 
929
- # Create a thread pool with the configured number of workers
930
- with ThreadPoolExecutor(max_workers=self.__max_workers) as executor:
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
- # Submit all test cases to the thread pool for execution
933
- futures = [executor.submit(run_single_test, test) for test in test_cases]
1124
+ # As each test completes, merge its result into the combined result
1125
+ for future in as_completed(futures):
934
1126
 
935
- # As each test completes, merge its result into the combined result
936
- for future in as_completed(futures):
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
- # If fail-fast is enabled and a failure occurs, cancel remaining tests
941
- if self.__fail_fast and not combined_result.wasSuccessful():
942
- for f in futures:
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
- # Print the result of each individual test using the printer
947
- for test_result in combined_result.test_results:
948
- self.__printer.unittestResult(test_result)
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 combined_result
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 TestResult object that will be updated with the merged results.
1164
+ The result object to be updated with merged statistics and details.
965
1165
  individual_result : unittest.TestResult
966
- The TestResult object whose results will be merged into the combined_result.
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 a value. It updates combined_result in place.
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
- This method aggregates the test statistics and detailed results from individual_result into combined_result.
976
- It updates the total number of tests run, and extends the lists of failures, errors, skipped tests,
977
- expected failures, and unexpected successes. If the result objects contain a 'test_results' attribute,
978
- this method also merges the detailed test result entries.
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
- # Extend the list of failures with those from the individual result
1184
+ # Merge failures from the individual result
985
1185
  combined_result.failures.extend(individual_result.failures)
986
1186
 
987
- # Extend the list of errors with those from the individual result
1187
+ # Merge errors from the individual result
988
1188
  combined_result.errors.extend(individual_result.errors)
989
1189
 
990
- # Extend the list of skipped tests with those from the individual result
1190
+ # Merge skipped tests from the individual result
991
1191
  combined_result.skipped.extend(individual_result.skipped)
992
1192
 
993
- # Extend the list of expected failures with those from the individual result
1193
+ # Merge expected failures from the individual result
994
1194
  combined_result.expectedFailures.extend(individual_result.expectedFailures)
995
1195
 
996
- # Extend the list of unexpected successes with those from the individual result
1196
+ # Merge unexpected successes from the individual result
997
1197
  combined_result.unexpectedSuccesses.extend(individual_result.unexpectedSuccesses)
998
1198
 
999
- # If the individual result contains detailed test results, merge them as well
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
- Generates a summary dictionary of the test suite execution, including statistics,
1195
- timing, and detailed results for each test. Optionally persists the summary and/or
1196
- generates a web report if configured.
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
- dict
1208
- A dictionary containing test statistics, details, and metadata.
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
- # Extraer información solo del último frame del traceback si existe
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
- Summary of test results for web report generation.
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 uses the TestingResultRender class to generate the report, passing the storage path,
1297
- the summary result, and a flag indicating whether to persist the report based on the
1298
- persistence configuration and driver. After rendering, it prints a link to the generated
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 with the storage path, result summary,
1303
- # and persistence flag (True if persistent and using sqlite driver)
1304
- render = TestingResultRender(
1305
- storage_path=self.__storage,
1306
- result=summary,
1307
- persist=self.__persistent and self.__persistent_driver == 'sqlite'
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(render.render())
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
- The summary dictionary containing test results and metadata to be persisted.
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 persists the test results summary according to the configured persistence driver.
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
- with a filename based on the current timestamp. The method ensures that the target directory exists,
1338
- and handles any errors that may occur during file or database operations.
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
- # If the persistence driver is SQLite, store the summary in the database
1569
+ # Persist results using SQLite database if configured
1343
1570
  if self.__persistent_driver == PersistentDrivers.SQLITE.value:
1344
- history = TestLogs(self.__storage)
1345
- history.create(summary)
1571
+ TestLogs(self.__storage).create(summary)
1346
1572
 
1347
- # If the persistence driver is JSON, write the summary to a JSON file
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
- # Ensure the parent directory exists
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(f"Error creating directories or writing files: {str(e)}")
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(f"Error persisting test results: {str(e)}")
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 __filterTestsByName(
1368
- self,
1369
- suite: unittest.TestSuite,
1370
- pattern: str
1371
- ) -> unittest.TestSuite:
1603
+ def getDiscoveredTestCases(
1604
+ self
1605
+ ) -> List[unittest.TestCase]:
1372
1606
  """
1373
- Filter tests in a test suite by a regular expression pattern applied to test names.
1607
+ Return a list of all discovered test case classes in the test suite.
1374
1608
 
1375
- Parameters
1376
- ----------
1377
- suite : unittest.TestSuite
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.TestSuite
1385
- A new TestSuite containing only the tests whose names match the given pattern.
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
- This method compiles the provided regular expression and applies it to the IDs of all test cases
1395
- in the flattened suite. Only tests whose IDs match the pattern are included in the returned suite.
1396
- If the pattern is invalid, an OrionisTestValueError is raised with details about the regex error.
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
- # Create a new TestSuite to hold the filtered tests
1400
- filtered_suite = unittest.TestSuite()
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
- # Return the suite containing only the filtered tests
1424
- return filtered_suite
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
- Discover and import Python modules containing test files that match a given filename pattern within a specified directory.
1632
+ Return a list of all discovered test module names in the test suite.
1435
1633
 
1436
- This method recursively searches for Python files in the directory specified by `test_path / custom_path` that match the provided
1437
- filename pattern. For each matching file, it constructs the module's fully qualified name relative to the project root, imports
1438
- the module using `importlib.import_module`, and adds it to a set to avoid duplicates. The method returns a list of imported module objects.
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
- root_path : Path
1443
- The root directory of the project, used to calculate the relative module path.
1444
- test_path : Path
1445
- The base directory where tests are located.
1446
- custom_path : Path
1447
- The subdirectory within `test_path` to search for matching test files.
1448
- pattern_file : str
1449
- The filename pattern to match (supports '*' and '?' wildcards).
1640
+ None
1450
1641
 
1451
1642
  Returns
1452
1643
  -------
1453
- List[module]
1454
- A list of imported Python module objects corresponding to test files that match the pattern.
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
- - Only files ending with `.py` are considered as Python modules.
1459
- - Duplicate modules are avoided by using a set.
1460
- - The module name is constructed by converting the relative path to dot notation.
1461
- - If the relative path is '.', only the module name is used.
1462
- - The method imports modules dynamically and returns them as objects.
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
- # Compile the filename pattern into a regular expression for matching.
1466
- regex = re.compile('^' + pattern_file.replace('*', '.*').replace('?', '.') + '$')
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 getTestNames(
1657
+ def getTestIds(
1492
1658
  self
1493
1659
  ) -> List[str]:
1494
1660
  """
1495
- Get a list of test names (unique identifiers) from the test suite.
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
- list of str
1500
- List of test names from the test suite.
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
- return [test.id() for test in self.__flattenTestSuite(self.__suite)]
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
- Get the total number of test cases in the test suite.
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
- Total number of individual test cases in the suite.
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
- def clearTests(
1518
- self
1519
- ) -> None:
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
- Returns
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