orionis 0.434.0__py3-none-any.whl → 0.436.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/metadata/framework.py +1 -1
- orionis/services/asynchrony/contracts/coroutines.py +5 -10
- orionis/services/asynchrony/coroutines.py +8 -23
- orionis/services/asynchrony/exceptions/exception.py +3 -5
- orionis/services/environment/contracts/caster.py +8 -9
- orionis/services/environment/contracts/env.py +9 -9
- orionis/services/environment/core/dot_env.py +47 -71
- orionis/services/environment/dynamic/caster.py +191 -220
- orionis/services/environment/enums/__init__.py +5 -0
- orionis/services/environment/enums/value_type.py +22 -25
- orionis/services/environment/env.py +28 -12
- orionis/services/environment/exceptions/exception.py +6 -2
- orionis/services/environment/exceptions/value.py +6 -2
- orionis/services/environment/helpers/functions.py +5 -3
- orionis/services/environment/key/key_generator.py +8 -12
- orionis/services/environment/validators/__init__.py +7 -0
- orionis/services/environment/validators/key_name.py +5 -6
- orionis/services/environment/validators/types.py +29 -13
- orionis/test/core/unit_test.py +292 -55
- {orionis-0.434.0.dist-info → orionis-0.436.0.dist-info}/METADATA +1 -1
- {orionis-0.434.0.dist-info → orionis-0.436.0.dist-info}/RECORD +25 -25
- {orionis-0.434.0.dist-info → orionis-0.436.0.dist-info}/WHEEL +0 -0
- {orionis-0.434.0.dist-info → orionis-0.436.0.dist-info}/licenses/LICENCE +0 -0
- {orionis-0.434.0.dist-info → orionis-0.436.0.dist-info}/top_level.txt +0 -0
- {orionis-0.434.0.dist-info → orionis-0.436.0.dist-info}/zip-safe +0 -0
orionis/test/core/unit_test.py
CHANGED
|
@@ -201,6 +201,7 @@ class UnitTest(IUnitTest):
|
|
|
201
201
|
OrionisTestValueError
|
|
202
202
|
If any parameter is invalid.
|
|
203
203
|
"""
|
|
204
|
+
|
|
204
205
|
# Validate and assign parameters using specialized validators
|
|
205
206
|
self.__verbosity = ValidVerbosity(verbosity)
|
|
206
207
|
self.__execution_mode = ValidExecutionMode(execution_mode)
|
|
@@ -284,15 +285,17 @@ class UnitTest(IUnitTest):
|
|
|
284
285
|
# Check for failed test imports (unittest.loader._FailedTest)
|
|
285
286
|
for test in self.__flattenTestSuite(tests):
|
|
286
287
|
if test.__class__.__name__ == "_FailedTest":
|
|
288
|
+
|
|
287
289
|
# Extract the error message from the test's traceback
|
|
288
290
|
error_message = ""
|
|
289
291
|
if hasattr(test, "_exception"):
|
|
290
292
|
error_message = str(test._exception)
|
|
291
293
|
elif hasattr(test, "_outcome") and hasattr(test._outcome, "errors"):
|
|
292
294
|
error_message = str(test._outcome.errors)
|
|
295
|
+
# Try to get error from test id or str(test)
|
|
293
296
|
else:
|
|
294
|
-
# Try to get error from test id or str(test)
|
|
295
297
|
error_message = str(test)
|
|
298
|
+
|
|
296
299
|
raise OrionisTestValueError(
|
|
297
300
|
f"Failed to import test module: {test.id()}.\n"
|
|
298
301
|
f"Error details: {error_message}\n"
|
|
@@ -380,6 +383,7 @@ class UnitTest(IUnitTest):
|
|
|
380
383
|
OrionisTestValueError
|
|
381
384
|
If module_name is invalid, test_name_pattern is not a valid regex, the module cannot be imported, or no tests are found.
|
|
382
385
|
"""
|
|
386
|
+
|
|
383
387
|
# Validate input parameters
|
|
384
388
|
self.__module_name = ValidModuleName(module_name)
|
|
385
389
|
self.__test_name_pattern = ValidNamePattern(test_name_pattern)
|
|
@@ -503,27 +507,37 @@ class UnitTest(IUnitTest):
|
|
|
503
507
|
suite: unittest.TestSuite
|
|
504
508
|
) -> List[unittest.TestCase]:
|
|
505
509
|
"""
|
|
506
|
-
Recursively
|
|
510
|
+
Recursively flattens a unittest.TestSuite into a list of unique unittest.TestCase instances.
|
|
507
511
|
|
|
508
512
|
Parameters
|
|
509
513
|
----------
|
|
510
514
|
suite : unittest.TestSuite
|
|
511
|
-
The test suite to
|
|
515
|
+
The test suite to be flattened.
|
|
512
516
|
|
|
513
517
|
Returns
|
|
514
518
|
-------
|
|
515
|
-
|
|
516
|
-
|
|
519
|
+
List[unittest.TestCase]
|
|
520
|
+
A flat list containing unique unittest.TestCase instances extracted from the suite.
|
|
521
|
+
|
|
522
|
+
Notes
|
|
523
|
+
-----
|
|
524
|
+
Test uniqueness is determined by a shortened test identifier (the last two components of the test id).
|
|
525
|
+
This helps avoid duplicate test cases in the returned list.
|
|
517
526
|
"""
|
|
527
|
+
|
|
528
|
+
# Initialize an empty list to hold unique test cases and a set to track seen test IDs
|
|
518
529
|
tests = []
|
|
519
530
|
seen_ids = set()
|
|
520
531
|
|
|
532
|
+
# Recursive function to flatten the test suite
|
|
521
533
|
def _flatten(item):
|
|
522
534
|
if isinstance(item, unittest.TestSuite):
|
|
523
535
|
for sub_item in item:
|
|
524
536
|
_flatten(sub_item)
|
|
525
537
|
elif hasattr(item, "id"):
|
|
526
538
|
test_id = item.id()
|
|
539
|
+
|
|
540
|
+
# Use the last two components of the test id for uniqueness
|
|
527
541
|
parts = test_id.split('.')
|
|
528
542
|
if len(parts) >= 2:
|
|
529
543
|
short_id = '.'.join(parts[-2:])
|
|
@@ -533,6 +547,7 @@ class UnitTest(IUnitTest):
|
|
|
533
547
|
seen_ids.add(short_id)
|
|
534
548
|
tests.append(item)
|
|
535
549
|
|
|
550
|
+
# Start the flattening process
|
|
536
551
|
_flatten(suite)
|
|
537
552
|
return tests
|
|
538
553
|
|
|
@@ -540,68 +555,86 @@ class UnitTest(IUnitTest):
|
|
|
540
555
|
self
|
|
541
556
|
) -> Tuple[unittest.TestResult, io.StringIO, io.StringIO]:
|
|
542
557
|
"""
|
|
543
|
-
|
|
558
|
+
Executes the test suite according to the configured execution mode, capturing both standard output and error streams.
|
|
544
559
|
|
|
545
560
|
Returns
|
|
546
561
|
-------
|
|
547
562
|
tuple
|
|
548
|
-
(result, output_buffer, error_buffer)
|
|
549
563
|
result : unittest.TestResult
|
|
550
|
-
|
|
564
|
+
The result object containing the outcomes of the executed tests.
|
|
551
565
|
output_buffer : io.StringIO
|
|
552
|
-
|
|
566
|
+
Buffer capturing the standard output generated during test execution.
|
|
553
567
|
error_buffer : io.StringIO
|
|
554
|
-
|
|
568
|
+
Buffer capturing the standard error generated during test execution.
|
|
555
569
|
"""
|
|
570
|
+
|
|
571
|
+
# Initialize output and error buffers to capture test execution output
|
|
556
572
|
output_buffer = io.StringIO()
|
|
557
573
|
error_buffer = io.StringIO()
|
|
558
574
|
|
|
575
|
+
# Run tests in parallel mode using multiple workers
|
|
559
576
|
if self.__execution_mode == ExecutionMode.PARALLEL.value:
|
|
560
577
|
result = self.__runTestsInParallel(
|
|
561
578
|
output_buffer,
|
|
562
579
|
error_buffer
|
|
563
580
|
)
|
|
581
|
+
|
|
582
|
+
# Run tests sequentially
|
|
564
583
|
else:
|
|
565
584
|
result = self.__runTestsSequentially(
|
|
566
585
|
output_buffer,
|
|
567
586
|
error_buffer
|
|
568
587
|
)
|
|
569
588
|
|
|
589
|
+
# Return the result, output, and error buffers
|
|
570
590
|
return result, output_buffer, error_buffer
|
|
571
591
|
|
|
572
592
|
def __resolveFlattenedTestSuite(
|
|
573
593
|
self
|
|
574
594
|
) -> unittest.TestSuite:
|
|
575
595
|
"""
|
|
576
|
-
|
|
596
|
+
Resolves and injects dependencies for all test cases in the current suite, returning a flattened TestSuite.
|
|
597
|
+
|
|
598
|
+
This method iterates through all test cases in the suite, checks for failed imports, decorated methods, and unresolved dependencies.
|
|
599
|
+
For each test case, it uses reflection to determine the test method and its dependencies. If dependencies are required and can be resolved,
|
|
600
|
+
it injects them using the application's resolver. If a test method has unresolved dependencies, an exception is raised.
|
|
601
|
+
Decorated methods and failed imports are added as-is. The resulting TestSuite contains all test cases with dependencies injected where needed.
|
|
577
602
|
|
|
578
603
|
Returns
|
|
579
604
|
-------
|
|
580
605
|
unittest.TestSuite
|
|
581
|
-
|
|
606
|
+
A new TestSuite containing all test cases with dependencies injected as required.
|
|
582
607
|
|
|
583
608
|
Raises
|
|
584
609
|
------
|
|
585
610
|
OrionisTestValueError
|
|
586
|
-
If any test method has unresolved dependencies.
|
|
611
|
+
If any test method has unresolved dependencies that cannot be resolved by the resolver.
|
|
587
612
|
"""
|
|
613
|
+
|
|
614
|
+
# Create a new TestSuite to hold the resolved test cases
|
|
588
615
|
flattened_suite = unittest.TestSuite()
|
|
589
616
|
|
|
617
|
+
# Iterate through all test cases in the flattened suite
|
|
590
618
|
for test_case in self.__flattenTestSuite(self.__suite):
|
|
591
619
|
|
|
620
|
+
# If the test case is a failed import, add it directly
|
|
592
621
|
if test_case.__class__.__name__ == "_FailedTest":
|
|
593
622
|
flattened_suite.addTest(test_case)
|
|
594
623
|
continue
|
|
595
624
|
|
|
625
|
+
# Use reflection to get the test method name
|
|
596
626
|
rf_instance = ReflectionInstance(test_case)
|
|
597
627
|
method_name = rf_instance.getAttribute("_testMethodName")
|
|
598
628
|
|
|
629
|
+
# If no method name is found, add the test case as-is
|
|
599
630
|
if not method_name:
|
|
600
631
|
flattened_suite.addTest(test_case)
|
|
601
632
|
continue
|
|
602
633
|
|
|
634
|
+
# Retrieve the test method from the class
|
|
603
635
|
test_method = getattr(test_case.__class__, method_name, None)
|
|
604
636
|
|
|
637
|
+
# Check if the method is decorated (wrapped)
|
|
605
638
|
decorators = []
|
|
606
639
|
if hasattr(test_method, '__wrapped__'):
|
|
607
640
|
original = test_method
|
|
@@ -612,32 +645,40 @@ class UnitTest(IUnitTest):
|
|
|
612
645
|
decorators.append(original.__name__)
|
|
613
646
|
original = original.__wrapped__
|
|
614
647
|
|
|
648
|
+
# If decorators are present, add the test case as-is
|
|
615
649
|
if decorators:
|
|
616
650
|
flattened_suite.addTest(test_case)
|
|
617
651
|
continue
|
|
618
652
|
|
|
653
|
+
# Get the method's dependency signature
|
|
619
654
|
signature = rf_instance.getMethodDependencies(method_name)
|
|
620
655
|
|
|
656
|
+
# If no dependencies are required or unresolved, add the test case as-is
|
|
621
657
|
if ((not signature.resolved and not signature.unresolved) or (not signature.resolved and len(signature.unresolved) > 0)):
|
|
622
658
|
flattened_suite.addTest(test_case)
|
|
623
659
|
continue
|
|
624
660
|
|
|
661
|
+
# If there are unresolved dependencies, raise an error
|
|
625
662
|
if (len(signature.unresolved) > 0):
|
|
626
663
|
raise OrionisTestValueError(
|
|
627
664
|
f"Test method '{method_name}' in class '{test_case.__class__.__name__}' has unresolved dependencies: {signature.unresolved}. "
|
|
628
665
|
"Please ensure all dependencies are correctly defined and available."
|
|
629
666
|
)
|
|
630
667
|
|
|
668
|
+
# Get the original test class and method
|
|
631
669
|
test_class = ReflectionInstance(test_case).getClass()
|
|
632
670
|
original_method = getattr(test_class, method_name)
|
|
633
671
|
|
|
672
|
+
# Resolve dependencies using the application's resolver
|
|
634
673
|
params = Resolver(self.__app).resolveSignature(signature)
|
|
635
674
|
|
|
675
|
+
# Create a wrapper to inject resolved dependencies into the test method
|
|
636
676
|
def create_test_wrapper(original_test, resolved_args: dict):
|
|
637
677
|
def wrapper(self_instance):
|
|
638
678
|
return original_test(self_instance, **resolved_args)
|
|
639
679
|
return wrapper
|
|
640
680
|
|
|
681
|
+
# Bind the wrapped method to the test case instance
|
|
641
682
|
wrapped_method = create_test_wrapper(original_method, params)
|
|
642
683
|
bound_method = wrapped_method.__get__(test_case, test_case.__class__)
|
|
643
684
|
setattr(test_case, method_name, bound_method)
|
|
@@ -651,28 +692,45 @@ class UnitTest(IUnitTest):
|
|
|
651
692
|
error_buffer: io.StringIO
|
|
652
693
|
) -> unittest.TestResult:
|
|
653
694
|
"""
|
|
654
|
-
|
|
695
|
+
Executes all test cases in the test suite sequentially, capturing standard output and error streams.
|
|
655
696
|
|
|
656
697
|
Parameters
|
|
657
698
|
----------
|
|
658
699
|
output_buffer : io.StringIO
|
|
659
|
-
Buffer to capture standard output.
|
|
700
|
+
Buffer to capture the standard output generated during test execution.
|
|
660
701
|
error_buffer : io.StringIO
|
|
661
|
-
Buffer to capture standard error.
|
|
702
|
+
Buffer to capture the standard error generated during test execution.
|
|
662
703
|
|
|
663
704
|
Returns
|
|
664
705
|
-------
|
|
665
706
|
unittest.TestResult
|
|
666
|
-
|
|
707
|
+
The aggregated result object containing the outcomes of all executed test cases.
|
|
708
|
+
|
|
709
|
+
Raises
|
|
710
|
+
------
|
|
711
|
+
OrionisTestValueError
|
|
712
|
+
If an item in the suite is not a valid unittest.TestCase instance.
|
|
713
|
+
|
|
714
|
+
Notes
|
|
715
|
+
-----
|
|
716
|
+
Each test case is executed individually, and results are merged into a single result object.
|
|
717
|
+
Output and error streams are redirected for each test case to ensure complete capture.
|
|
718
|
+
The printer is used to display the result of each test immediately after execution.
|
|
667
719
|
"""
|
|
720
|
+
|
|
721
|
+
# Initialize output and error buffers to capture test execution output
|
|
668
722
|
result = None
|
|
723
|
+
|
|
724
|
+
# Iterate through all resolved test cases in the suite
|
|
669
725
|
for case in self.__resolveFlattenedTestSuite():
|
|
670
726
|
|
|
727
|
+
# Ensure the test case is a valid unittest.TestCase instance
|
|
671
728
|
if not isinstance(case, unittest.TestCase):
|
|
672
729
|
raise OrionisTestValueError(
|
|
673
730
|
f"Invalid test case type: Expected unittest.TestCase, got {type(case).__name__}."
|
|
674
731
|
)
|
|
675
732
|
|
|
733
|
+
# Redirect output and error streams for the current test case
|
|
676
734
|
with redirect_stdout(output_buffer), redirect_stderr(error_buffer):
|
|
677
735
|
runner = unittest.TextTestRunner(
|
|
678
736
|
stream=output_buffer,
|
|
@@ -680,15 +738,19 @@ class UnitTest(IUnitTest):
|
|
|
680
738
|
failfast=self.__fail_fast,
|
|
681
739
|
resultclass=self.__customResultClass()
|
|
682
740
|
)
|
|
741
|
+
# Run the current test case and obtain the result
|
|
683
742
|
single_result: IOrionisTestResult = runner.run(unittest.TestSuite([case]))
|
|
684
743
|
|
|
744
|
+
# Print the result of the current test case using the printer
|
|
685
745
|
self.__printer.unittestResult(single_result.test_results[0])
|
|
686
746
|
|
|
747
|
+
# Merge the result of the current test case into the aggregated result
|
|
687
748
|
if result is None:
|
|
688
749
|
result = single_result
|
|
689
750
|
else:
|
|
690
751
|
self.__mergeTestResults(result, single_result)
|
|
691
752
|
|
|
753
|
+
# Return the aggregated result containing all test outcomes
|
|
692
754
|
return result
|
|
693
755
|
|
|
694
756
|
def __runTestsInParallel(
|
|
@@ -697,47 +759,72 @@ class UnitTest(IUnitTest):
|
|
|
697
759
|
error_buffer: io.StringIO
|
|
698
760
|
) -> unittest.TestResult:
|
|
699
761
|
"""
|
|
700
|
-
|
|
762
|
+
Executes all test cases in the test suite concurrently using a thread pool and aggregates their results.
|
|
701
763
|
|
|
702
764
|
Parameters
|
|
703
765
|
----------
|
|
704
766
|
output_buffer : io.StringIO
|
|
705
|
-
Buffer to capture standard output.
|
|
767
|
+
Buffer to capture the standard output generated during test execution.
|
|
706
768
|
error_buffer : io.StringIO
|
|
707
|
-
Buffer to capture standard error.
|
|
769
|
+
Buffer to capture the standard error generated during test execution.
|
|
708
770
|
|
|
709
771
|
Returns
|
|
710
772
|
-------
|
|
711
773
|
unittest.TestResult
|
|
712
|
-
Combined result object containing all test
|
|
774
|
+
Combined result object containing the outcomes of all executed test cases.
|
|
775
|
+
|
|
776
|
+
Notes
|
|
777
|
+
-----
|
|
778
|
+
Each test case is executed in a separate thread using a ThreadPoolExecutor.
|
|
779
|
+
Results from all threads are merged into a single result object.
|
|
780
|
+
Output and error streams are redirected for the entire parallel execution.
|
|
781
|
+
If fail-fast is enabled, execution stops as soon as a failure is detected.
|
|
713
782
|
"""
|
|
783
|
+
|
|
784
|
+
# Resolve and flatten all test cases in the suite, injecting dependencies if needed
|
|
714
785
|
test_cases = list(self.__resolveFlattenedTestSuite())
|
|
786
|
+
|
|
787
|
+
# Get the custom result class for enhanced test tracking
|
|
715
788
|
result_class = self.__customResultClass()
|
|
789
|
+
|
|
790
|
+
# Create a combined result object to aggregate all individual test results
|
|
716
791
|
combined_result = result_class(io.StringIO(), descriptions=True, verbosity=self.__verbosity)
|
|
717
792
|
|
|
793
|
+
# Define a function to run a single test case and return its result
|
|
718
794
|
def run_single_test(test):
|
|
719
795
|
runner = unittest.TextTestRunner(
|
|
720
|
-
stream=io.StringIO(),
|
|
796
|
+
stream=io.StringIO(), # Use a separate buffer for each test
|
|
721
797
|
verbosity=0,
|
|
722
798
|
failfast=False,
|
|
723
799
|
resultclass=result_class
|
|
724
800
|
)
|
|
725
801
|
return runner.run(unittest.TestSuite([test]))
|
|
726
802
|
|
|
803
|
+
# Redirect output and error streams for the entire parallel execution
|
|
727
804
|
with redirect_stdout(output_buffer), redirect_stderr(error_buffer):
|
|
805
|
+
|
|
806
|
+
# Create a thread pool with the configured number of workers
|
|
728
807
|
with ThreadPoolExecutor(max_workers=self.__max_workers) as executor:
|
|
808
|
+
|
|
809
|
+
# Submit all test cases to the thread pool for execution
|
|
729
810
|
futures = [executor.submit(run_single_test, test) for test in test_cases]
|
|
811
|
+
|
|
812
|
+
# As each test completes, merge its result into the combined result
|
|
730
813
|
for future in as_completed(futures):
|
|
731
814
|
test_result = future.result()
|
|
732
815
|
self.__mergeTestResults(combined_result, test_result)
|
|
816
|
+
|
|
817
|
+
# If fail-fast is enabled and a failure occurs, cancel remaining tests
|
|
733
818
|
if self.__fail_fast and not combined_result.wasSuccessful():
|
|
734
819
|
for f in futures:
|
|
735
820
|
f.cancel()
|
|
736
821
|
break
|
|
737
822
|
|
|
823
|
+
# Print the result of each individual test using the printer
|
|
738
824
|
for test_result in combined_result.test_results:
|
|
739
825
|
self.__printer.unittestResult(test_result)
|
|
740
826
|
|
|
827
|
+
# Return the aggregated result containing all test outcomes
|
|
741
828
|
return combined_result
|
|
742
829
|
|
|
743
830
|
def __mergeTestResults(
|
|
@@ -751,20 +838,42 @@ class UnitTest(IUnitTest):
|
|
|
751
838
|
Parameters
|
|
752
839
|
----------
|
|
753
840
|
combined_result : unittest.TestResult
|
|
754
|
-
The TestResult object
|
|
841
|
+
The TestResult object that will be updated with the merged results.
|
|
755
842
|
individual_result : unittest.TestResult
|
|
756
|
-
The TestResult object
|
|
843
|
+
The TestResult object whose results will be merged into the combined_result.
|
|
757
844
|
|
|
758
845
|
Returns
|
|
759
846
|
-------
|
|
760
847
|
None
|
|
848
|
+
This method does not return a value. It updates combined_result in place.
|
|
849
|
+
|
|
850
|
+
Notes
|
|
851
|
+
-----
|
|
852
|
+
This method aggregates the test statistics and detailed results from individual_result into combined_result.
|
|
853
|
+
It updates the total number of tests run, and extends the lists of failures, errors, skipped tests,
|
|
854
|
+
expected failures, and unexpected successes. If the result objects contain a 'test_results' attribute,
|
|
855
|
+
this method also merges the detailed test result entries.
|
|
761
856
|
"""
|
|
857
|
+
|
|
858
|
+
# Increment the total number of tests run
|
|
762
859
|
combined_result.testsRun += individual_result.testsRun
|
|
860
|
+
|
|
861
|
+
# Extend the list of failures with those from the individual result
|
|
763
862
|
combined_result.failures.extend(individual_result.failures)
|
|
863
|
+
|
|
864
|
+
# Extend the list of errors with those from the individual result
|
|
764
865
|
combined_result.errors.extend(individual_result.errors)
|
|
866
|
+
|
|
867
|
+
# Extend the list of skipped tests with those from the individual result
|
|
765
868
|
combined_result.skipped.extend(individual_result.skipped)
|
|
869
|
+
|
|
870
|
+
# Extend the list of expected failures with those from the individual result
|
|
766
871
|
combined_result.expectedFailures.extend(individual_result.expectedFailures)
|
|
872
|
+
|
|
873
|
+
# Extend the list of unexpected successes with those from the individual result
|
|
767
874
|
combined_result.unexpectedSuccesses.extend(individual_result.unexpectedSuccesses)
|
|
875
|
+
|
|
876
|
+
# If the individual result contains detailed test results, merge them as well
|
|
768
877
|
if hasattr(individual_result, 'test_results'):
|
|
769
878
|
if not hasattr(combined_result, 'test_results'):
|
|
770
879
|
combined_result.test_results = []
|
|
@@ -774,31 +883,45 @@ class UnitTest(IUnitTest):
|
|
|
774
883
|
self
|
|
775
884
|
) -> type:
|
|
776
885
|
"""
|
|
777
|
-
Create a custom test result class for enhanced test tracking.
|
|
886
|
+
Create and return a custom test result class for enhanced test tracking.
|
|
778
887
|
|
|
779
888
|
Returns
|
|
780
889
|
-------
|
|
781
890
|
type
|
|
782
|
-
|
|
891
|
+
A dynamically created subclass of unittest.TextTestResult that collects
|
|
892
|
+
detailed information about each test execution, including timing, status,
|
|
893
|
+
error messages, tracebacks, and metadata.
|
|
894
|
+
|
|
895
|
+
Notes
|
|
896
|
+
-----
|
|
897
|
+
The returned class, OrionisTestResult, extends unittest.TextTestResult and
|
|
898
|
+
overrides key methods to capture additional data for each test case. This
|
|
899
|
+
includes execution time, error details, and test metadata, which are stored
|
|
900
|
+
in a list of TestResult objects for later reporting and analysis.
|
|
783
901
|
"""
|
|
784
902
|
this = self
|
|
785
903
|
|
|
786
904
|
class OrionisTestResult(unittest.TextTestResult):
|
|
905
|
+
|
|
906
|
+
# Initialize the parent class and custom attributes for tracking results and timings
|
|
787
907
|
def __init__(self, *args, **kwargs):
|
|
788
908
|
super().__init__(*args, **kwargs)
|
|
789
|
-
self.test_results = []
|
|
790
|
-
self._test_timings = {}
|
|
791
|
-
self._current_test_start = None
|
|
909
|
+
self.test_results = [] # Stores detailed results for each test
|
|
910
|
+
self._test_timings = {} # Maps test instances to their execution time
|
|
911
|
+
self._current_test_start = None # Tracks the start time of the current test
|
|
792
912
|
|
|
913
|
+
# Record the start time of the test
|
|
793
914
|
def startTest(self, test):
|
|
794
915
|
self._current_test_start = time.time()
|
|
795
916
|
super().startTest(test)
|
|
796
917
|
|
|
918
|
+
# Calculate and store the elapsed time for the test
|
|
797
919
|
def stopTest(self, test):
|
|
798
920
|
elapsed = time.time() - self._current_test_start
|
|
799
921
|
self._test_timings[test] = elapsed
|
|
800
922
|
super().stopTest(test)
|
|
801
923
|
|
|
924
|
+
# Handle a successful test case and record its result
|
|
802
925
|
def addSuccess(self, test):
|
|
803
926
|
super().addSuccess(test)
|
|
804
927
|
elapsed = self._test_timings.get(test, 0.0)
|
|
@@ -816,6 +939,7 @@ class UnitTest(IUnitTest):
|
|
|
816
939
|
)
|
|
817
940
|
)
|
|
818
941
|
|
|
942
|
+
# Handle a failed test case, extract error info, and record its result
|
|
819
943
|
def addFailure(self, test, err):
|
|
820
944
|
super().addFailure(test, err)
|
|
821
945
|
elapsed = self._test_timings.get(test, 0.0)
|
|
@@ -837,6 +961,7 @@ class UnitTest(IUnitTest):
|
|
|
837
961
|
)
|
|
838
962
|
)
|
|
839
963
|
|
|
964
|
+
# Handle a test case that raised an error, extract error info, and record its result
|
|
840
965
|
def addError(self, test, err):
|
|
841
966
|
super().addError(test, err)
|
|
842
967
|
elapsed = self._test_timings.get(test, 0.0)
|
|
@@ -858,6 +983,7 @@ class UnitTest(IUnitTest):
|
|
|
858
983
|
)
|
|
859
984
|
)
|
|
860
985
|
|
|
986
|
+
# Handle a skipped test case and record its result
|
|
861
987
|
def addSkip(self, test, reason):
|
|
862
988
|
super().addSkip(test, reason)
|
|
863
989
|
elapsed = self._test_timings.get(test, 0.0)
|
|
@@ -876,6 +1002,7 @@ class UnitTest(IUnitTest):
|
|
|
876
1002
|
)
|
|
877
1003
|
)
|
|
878
1004
|
|
|
1005
|
+
# Return the dynamically created OrionisTestResult class
|
|
879
1006
|
return OrionisTestResult
|
|
880
1007
|
|
|
881
1008
|
def _extractErrorInfo(
|
|
@@ -883,33 +1010,53 @@ class UnitTest(IUnitTest):
|
|
|
883
1010
|
traceback_str: str
|
|
884
1011
|
) -> Tuple[Optional[str], Optional[str]]:
|
|
885
1012
|
"""
|
|
886
|
-
|
|
1013
|
+
Extracts the file path and a cleaned traceback from a given traceback string.
|
|
887
1014
|
|
|
888
1015
|
Parameters
|
|
889
1016
|
----------
|
|
890
1017
|
traceback_str : str
|
|
891
|
-
|
|
1018
|
+
The full traceback string to process.
|
|
892
1019
|
|
|
893
1020
|
Returns
|
|
894
1021
|
-------
|
|
895
1022
|
tuple
|
|
896
1023
|
file_path : str or None
|
|
897
|
-
|
|
1024
|
+
The path to the Python file where the error occurred, or None if not found.
|
|
898
1025
|
clean_tb : str or None
|
|
899
|
-
|
|
1026
|
+
The cleaned traceback string with framework internals removed, or the original traceback if no cleaning was possible.
|
|
1027
|
+
|
|
1028
|
+
Notes
|
|
1029
|
+
-----
|
|
1030
|
+
This method parses the traceback string to identify the most relevant file path (typically the last Python file in the traceback).
|
|
1031
|
+
It then filters out lines related to framework internals (such as 'unittest/', 'lib/python', or 'site-packages') to produce a more concise and relevant traceback.
|
|
1032
|
+
The cleaned traceback starts from the first occurrence of the relevant file path.
|
|
900
1033
|
"""
|
|
1034
|
+
|
|
1035
|
+
# Find all Python file paths in the traceback
|
|
901
1036
|
file_matches = re.findall(r'File ["\'](.*?.py)["\']', traceback_str)
|
|
1037
|
+
|
|
1038
|
+
# Select the last file path as the most relevant one
|
|
902
1039
|
file_path = file_matches[-1] if file_matches else None
|
|
1040
|
+
|
|
1041
|
+
# Split the traceback into individual lines for processing
|
|
903
1042
|
tb_lines = traceback_str.split('\n')
|
|
904
1043
|
clean_lines = []
|
|
905
1044
|
relevant_lines_started = False
|
|
1045
|
+
|
|
1046
|
+
# Iterate through each line to filter out framework internals
|
|
906
1047
|
for line in tb_lines:
|
|
1048
|
+
|
|
1049
|
+
# Skip lines that are part of unittest, Python standard library, or site-packages
|
|
907
1050
|
if any(s in line for s in ['unittest/', 'lib/python', 'site-packages']):
|
|
908
1051
|
continue
|
|
1052
|
+
|
|
1053
|
+
# Start collecting lines from the first occurrence of the relevant file path
|
|
909
1054
|
if file_path and file_path in line and not relevant_lines_started:
|
|
910
1055
|
relevant_lines_started = True
|
|
911
1056
|
if relevant_lines_started:
|
|
912
1057
|
clean_lines.append(line)
|
|
1058
|
+
|
|
1059
|
+
# Join the filtered lines to form the cleaned traceback
|
|
913
1060
|
clean_tb = str('\n').join(clean_lines) if clean_lines else traceback_str
|
|
914
1061
|
return file_path, clean_tb
|
|
915
1062
|
|
|
@@ -919,25 +1066,30 @@ class UnitTest(IUnitTest):
|
|
|
919
1066
|
execution_time: float
|
|
920
1067
|
) -> Dict[str, Any]:
|
|
921
1068
|
"""
|
|
922
|
-
|
|
1069
|
+
Generates a summary dictionary of the test suite execution, including statistics,
|
|
1070
|
+
timing, and detailed results for each test. Optionally persists the summary and/or
|
|
1071
|
+
generates a web report if configured.
|
|
923
1072
|
|
|
924
1073
|
Parameters
|
|
925
1074
|
----------
|
|
926
1075
|
result : unittest.TestResult
|
|
927
|
-
|
|
1076
|
+
The result object containing details of the test execution.
|
|
928
1077
|
execution_time : float
|
|
929
|
-
|
|
1078
|
+
The total execution time of the test suite in seconds.
|
|
930
1079
|
|
|
931
1080
|
Returns
|
|
932
1081
|
-------
|
|
933
1082
|
dict
|
|
934
|
-
|
|
1083
|
+
A dictionary containing test statistics, details, and metadata.
|
|
935
1084
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
If persistence is enabled, the summary is
|
|
939
|
-
If web reporting is enabled, a web report is generated.
|
|
1085
|
+
Notes
|
|
1086
|
+
-----
|
|
1087
|
+
- If persistence is enabled, the summary is saved to storage.
|
|
1088
|
+
- If web reporting is enabled, a web report is generated.
|
|
1089
|
+
- The summary includes per-test details, overall statistics, and a timestamp.
|
|
940
1090
|
"""
|
|
1091
|
+
|
|
1092
|
+
# Collect detailed information for each test result
|
|
941
1093
|
test_details = []
|
|
942
1094
|
for test_result in result.test_results:
|
|
943
1095
|
rst: TestResult = test_result
|
|
@@ -952,8 +1104,14 @@ class UnitTest(IUnitTest):
|
|
|
952
1104
|
'file_path': rst.file_path,
|
|
953
1105
|
'doc_string': rst.doc_string
|
|
954
1106
|
})
|
|
1107
|
+
|
|
1108
|
+
# Calculate the number of passed tests
|
|
955
1109
|
passed = result.testsRun - len(result.failures) - len(result.errors) - len(result.skipped)
|
|
1110
|
+
|
|
1111
|
+
# Calculate the success rate as a percentage
|
|
956
1112
|
success_rate = (passed / result.testsRun * 100) if result.testsRun > 0 else 100.0
|
|
1113
|
+
|
|
1114
|
+
# Build the summary dictionary with all relevant statistics and details
|
|
957
1115
|
self.__result = {
|
|
958
1116
|
"total_tests": result.testsRun,
|
|
959
1117
|
"passed": passed,
|
|
@@ -965,10 +1123,16 @@ class UnitTest(IUnitTest):
|
|
|
965
1123
|
"test_details": test_details,
|
|
966
1124
|
"timestamp": datetime.now().isoformat()
|
|
967
1125
|
}
|
|
1126
|
+
|
|
1127
|
+
# Persist the summary if persistence is enabled
|
|
968
1128
|
if self.__persistent:
|
|
969
1129
|
self.__handlePersistResults(self.__result)
|
|
1130
|
+
|
|
1131
|
+
# Generate a web report if web reporting is enabled
|
|
970
1132
|
if self.__web_report:
|
|
971
1133
|
self.__handleWebReport(self.__result)
|
|
1134
|
+
|
|
1135
|
+
# Return the summary dictionary
|
|
972
1136
|
return self.__result
|
|
973
1137
|
|
|
974
1138
|
def __handleWebReport(
|
|
@@ -986,12 +1150,25 @@ class UnitTest(IUnitTest):
|
|
|
986
1150
|
Returns
|
|
987
1151
|
-------
|
|
988
1152
|
None
|
|
1153
|
+
|
|
1154
|
+
Notes
|
|
1155
|
+
-----
|
|
1156
|
+
This method creates a web-based report for the given test results summary.
|
|
1157
|
+
It uses the TestingResultRender class to generate the report, passing the storage path,
|
|
1158
|
+
the summary result, and a flag indicating whether to persist the report based on the
|
|
1159
|
+
persistence configuration and driver. After rendering, it prints a link to the generated
|
|
1160
|
+
web report using the printer.
|
|
989
1161
|
"""
|
|
1162
|
+
|
|
1163
|
+
# Create a TestingResultRender instance with the storage path, result summary,
|
|
1164
|
+
# and persistence flag (True if persistent and using sqlite driver)
|
|
990
1165
|
render = TestingResultRender(
|
|
991
1166
|
storage_path=self.__storage,
|
|
992
1167
|
result=summary,
|
|
993
1168
|
persist=self.__persistent and self.__persistent_driver == 'sqlite'
|
|
994
1169
|
)
|
|
1170
|
+
|
|
1171
|
+
# Print the link to the generated web report
|
|
995
1172
|
self.__printer.linkWebReport(render.render())
|
|
996
1173
|
|
|
997
1174
|
def __handlePersistResults(
|
|
@@ -1004,11 +1181,7 @@ class UnitTest(IUnitTest):
|
|
|
1004
1181
|
Parameters
|
|
1005
1182
|
----------
|
|
1006
1183
|
summary : dict
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
Returns
|
|
1010
|
-
-------
|
|
1011
|
-
None
|
|
1184
|
+
The summary dictionary containing test results and metadata to be persisted.
|
|
1012
1185
|
|
|
1013
1186
|
Raises
|
|
1014
1187
|
------
|
|
@@ -1016,20 +1189,40 @@ class UnitTest(IUnitTest):
|
|
|
1016
1189
|
If there is an error creating directories or writing files.
|
|
1017
1190
|
OrionisTestPersistenceError
|
|
1018
1191
|
If database operations fail or any other error occurs during persistence.
|
|
1192
|
+
|
|
1193
|
+
Notes
|
|
1194
|
+
-----
|
|
1195
|
+
This method persists the test results summary according to the configured persistence driver.
|
|
1196
|
+
If the driver is set to 'sqlite', the summary is stored in a SQLite database using the TestLogs class.
|
|
1197
|
+
If the driver is set to 'json', the summary is saved as a JSON file in the specified storage directory,
|
|
1198
|
+
with a filename based on the current timestamp. The method ensures that the target directory exists,
|
|
1199
|
+
and handles any errors that may occur during file or database operations.
|
|
1019
1200
|
"""
|
|
1020
1201
|
try:
|
|
1202
|
+
|
|
1203
|
+
# If the persistence driver is SQLite, store the summary in the database
|
|
1021
1204
|
if self.__persistent_driver == PersistentDrivers.SQLITE.value:
|
|
1022
1205
|
history = TestLogs(self.__storage)
|
|
1023
1206
|
history.create(summary)
|
|
1207
|
+
|
|
1208
|
+
# If the persistence driver is JSON, write the summary to a JSON file
|
|
1024
1209
|
elif self.__persistent_driver == PersistentDrivers.JSON.value:
|
|
1025
1210
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
1026
1211
|
log_path = Path(self.__storage) / f"{timestamp}_test_results.json"
|
|
1212
|
+
|
|
1213
|
+
# Ensure the parent directory exists
|
|
1027
1214
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1215
|
+
|
|
1216
|
+
# Write the summary to the JSON file
|
|
1028
1217
|
with open(log_path, 'w', encoding='utf-8') as log:
|
|
1029
1218
|
json.dump(summary, log, indent=4)
|
|
1030
1219
|
except OSError as e:
|
|
1220
|
+
|
|
1221
|
+
# Raise an error if directory creation or file writing fails
|
|
1031
1222
|
raise OSError(f"Error creating directories or writing files: {str(e)}")
|
|
1032
1223
|
except Exception as e:
|
|
1224
|
+
|
|
1225
|
+
# Raise a persistence error for any other exceptions
|
|
1033
1226
|
raise OrionisTestPersistenceError(f"Error persisting test results: {str(e)}")
|
|
1034
1227
|
|
|
1035
1228
|
def __filterTestsByName(
|
|
@@ -1038,37 +1231,57 @@ class UnitTest(IUnitTest):
|
|
|
1038
1231
|
pattern: str
|
|
1039
1232
|
) -> unittest.TestSuite:
|
|
1040
1233
|
"""
|
|
1041
|
-
Filter tests in a test suite
|
|
1234
|
+
Filter tests in a test suite by a regular expression pattern applied to test names.
|
|
1042
1235
|
|
|
1043
1236
|
Parameters
|
|
1044
1237
|
----------
|
|
1045
1238
|
suite : unittest.TestSuite
|
|
1046
|
-
|
|
1239
|
+
The test suite containing the tests to be filtered.
|
|
1047
1240
|
pattern : str
|
|
1048
|
-
Regular expression pattern to match test names.
|
|
1241
|
+
Regular expression pattern to match against test names (test IDs).
|
|
1049
1242
|
|
|
1050
1243
|
Returns
|
|
1051
1244
|
-------
|
|
1052
1245
|
unittest.TestSuite
|
|
1053
|
-
|
|
1246
|
+
A new TestSuite containing only the tests whose names match the given pattern.
|
|
1054
1247
|
|
|
1055
1248
|
Raises
|
|
1056
1249
|
------
|
|
1057
1250
|
OrionisTestValueError
|
|
1058
1251
|
If the provided pattern is not a valid regular expression.
|
|
1252
|
+
|
|
1253
|
+
Notes
|
|
1254
|
+
-----
|
|
1255
|
+
This method compiles the provided regular expression and applies it to the IDs of all test cases
|
|
1256
|
+
in the flattened suite. Only tests whose IDs match the pattern are included in the returned suite.
|
|
1257
|
+
If the pattern is invalid, an OrionisTestValueError is raised with details about the regex error.
|
|
1059
1258
|
"""
|
|
1259
|
+
|
|
1260
|
+
# Create a new TestSuite to hold the filtered tests
|
|
1060
1261
|
filtered_suite = unittest.TestSuite()
|
|
1262
|
+
|
|
1061
1263
|
try:
|
|
1264
|
+
|
|
1265
|
+
# Compile the provided regular expression pattern
|
|
1062
1266
|
regex = re.compile(pattern)
|
|
1267
|
+
|
|
1063
1268
|
except re.error as e:
|
|
1269
|
+
|
|
1270
|
+
# Raise a value error if the regex is invalid
|
|
1064
1271
|
raise OrionisTestValueError(
|
|
1065
1272
|
f"The provided test name pattern is invalid: '{pattern}'. "
|
|
1066
1273
|
f"Regular expression compilation error: {str(e)}. "
|
|
1067
1274
|
"Please check the pattern syntax and try again."
|
|
1068
1275
|
)
|
|
1276
|
+
|
|
1277
|
+
# Iterate through all test cases in the flattened suite
|
|
1069
1278
|
for test in self.__flattenTestSuite(suite):
|
|
1279
|
+
|
|
1280
|
+
# Add the test to the filtered suite if its ID matches the regex
|
|
1070
1281
|
if regex.search(test.id()):
|
|
1071
1282
|
filtered_suite.addTest(test)
|
|
1283
|
+
|
|
1284
|
+
# Return the suite containing only the filtered tests
|
|
1072
1285
|
return filtered_suite
|
|
1073
1286
|
|
|
1074
1287
|
def __filterTestsByTags(
|
|
@@ -1077,32 +1290,56 @@ class UnitTest(IUnitTest):
|
|
|
1077
1290
|
tags: List[str]
|
|
1078
1291
|
) -> unittest.TestSuite:
|
|
1079
1292
|
"""
|
|
1080
|
-
|
|
1293
|
+
Filters tests in a unittest TestSuite by matching specified tags.
|
|
1081
1294
|
|
|
1082
1295
|
Parameters
|
|
1083
1296
|
----------
|
|
1084
1297
|
suite : unittest.TestSuite
|
|
1085
|
-
|
|
1298
|
+
The original TestSuite containing all test cases to be filtered.
|
|
1086
1299
|
tags : list of str
|
|
1087
|
-
|
|
1300
|
+
List of tags to filter the tests by.
|
|
1088
1301
|
|
|
1089
1302
|
Returns
|
|
1090
1303
|
-------
|
|
1091
1304
|
unittest.TestSuite
|
|
1092
|
-
|
|
1305
|
+
A new TestSuite containing only the tests that have at least one matching tag.
|
|
1306
|
+
|
|
1307
|
+
Notes
|
|
1308
|
+
-----
|
|
1309
|
+
This method inspects each test case in the provided suite and checks for the presence of tags
|
|
1310
|
+
either on the test method (via a `__tags__` attribute) or on the test class instance itself.
|
|
1311
|
+
If any of the specified tags are found in the test's tags, the test is included in the returned suite.
|
|
1093
1312
|
"""
|
|
1313
|
+
|
|
1314
|
+
# Create a new TestSuite to hold the filtered tests
|
|
1094
1315
|
filtered_suite = unittest.TestSuite()
|
|
1316
|
+
|
|
1317
|
+
# Convert the list of tags to a set for efficient intersection checks
|
|
1095
1318
|
tag_set = set(tags)
|
|
1319
|
+
|
|
1320
|
+
# Iterate through all test cases in the flattened suite
|
|
1096
1321
|
for test in self.__flattenTestSuite(suite):
|
|
1322
|
+
|
|
1323
|
+
# Attempt to retrieve the test method from the test case
|
|
1097
1324
|
test_method = getattr(test, test._testMethodName, None)
|
|
1325
|
+
|
|
1326
|
+
# Check if the test method has a __tags__ attribute
|
|
1098
1327
|
if hasattr(test_method, '__tags__'):
|
|
1099
1328
|
method_tags = set(getattr(test_method, '__tags__'))
|
|
1329
|
+
|
|
1330
|
+
# If there is any intersection between the method's tags and the filter tags, add the test
|
|
1100
1331
|
if tag_set.intersection(method_tags):
|
|
1101
1332
|
filtered_suite.addTest(test)
|
|
1333
|
+
|
|
1334
|
+
# If the method does not have tags, check if the test case itself has a __tags__ attribute
|
|
1102
1335
|
elif hasattr(test, '__tags__'):
|
|
1103
1336
|
class_tags = set(getattr(test, '__tags__'))
|
|
1337
|
+
|
|
1338
|
+
# If there is any intersection between the class's tags and the filter tags, add the test
|
|
1104
1339
|
if tag_set.intersection(class_tags):
|
|
1105
1340
|
filtered_suite.addTest(test)
|
|
1341
|
+
|
|
1342
|
+
# Return the suite containing only the filtered tests
|
|
1106
1343
|
return filtered_suite
|
|
1107
1344
|
|
|
1108
1345
|
def getTestNames(
|
|
@@ -1204,4 +1441,4 @@ class UnitTest(IUnitTest):
|
|
|
1204
1441
|
-------
|
|
1205
1442
|
None
|
|
1206
1443
|
"""
|
|
1207
|
-
self.__printer.print(self.__error_buffer)
|
|
1444
|
+
self.__printer.print(self.__error_buffer)
|