danielutils 1.1.28__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.
- danielutils/__init__.py +50 -0
- danielutils/abstractions/__init__.py +3 -0
- danielutils/abstractions/db/__init__.py +10 -0
- danielutils/abstractions/db/database.py +206 -0
- danielutils/abstractions/db/database_definitions.py +219 -0
- danielutils/abstractions/db/database_exceptions.py +34 -0
- danielutils/abstractions/db/database_factory.py +70 -0
- danielutils/abstractions/db/database_initializer.py +261 -0
- danielutils/abstractions/db/database_models.py +213 -0
- danielutils/abstractions/db/dependencies.py +6 -0
- danielutils/abstractions/db/implementations/__init__.py +4 -0
- danielutils/abstractions/db/implementations/in_memory_database.py +365 -0
- danielutils/abstractions/db/implementations/persistent_in_memory_database.py +174 -0
- danielutils/abstractions/db/implementations/redis_database.py +403 -0
- danielutils/abstractions/db/implementations/sqlite_database.py +491 -0
- danielutils/abstractions/multiprogramming/__init__.py +3 -0
- danielutils/abstractions/multiprogramming/multi_id.py +26 -0
- danielutils/abstractions/multiprogramming/worker.py +104 -0
- danielutils/abstractions/multiprogramming/worker_pool.py +98 -0
- danielutils/abstractions/repl.py +125 -0
- danielutils/aliases.py +174 -0
- danielutils/async_/__init__.py +7 -0
- danielutils/async_/async_cmd.py +28 -0
- danielutils/async_/async_command.py +879 -0
- danielutils/async_/async_layered_command.py +125 -0
- danielutils/async_/async_retry_executor.py +86 -0
- danielutils/async_/async_worker_pool.py +110 -0
- danielutils/async_/time_strategy.py +73 -0
- danielutils/async_/utils.py +57 -0
- danielutils/better_builtins/__init__.py +3 -0
- danielutils/better_builtins/counter.py +48 -0
- danielutils/better_builtins/frange.py +266 -0
- danielutils/better_builtins/typed_builtins/__init__.py +4 -0
- danielutils/better_builtins/typed_builtins/factory.py +126 -0
- danielutils/better_builtins/typed_builtins/tdict.py +36 -0
- danielutils/better_builtins/typed_builtins/tlist.py +223 -0
- danielutils/better_builtins/typed_builtins/tset.py +67 -0
- danielutils/better_builtins/typed_builtins/ttuple.py +15 -0
- danielutils/colors.py +203 -0
- danielutils/context_managers/__init__.py +5 -0
- danielutils/context_managers/attr_context.py +40 -0
- danielutils/context_managers/multi_context.py +34 -0
- danielutils/context_managers/optional_context.py +33 -0
- danielutils/context_managers/state_context.py +25 -0
- danielutils/context_managers/temporary_file.py +108 -0
- danielutils/convenience.py +12 -0
- danielutils/conversions/__init__.py +2 -0
- danielutils/conversions/main_conversions.py +131 -0
- danielutils/conversions/specialized_conversions/__init__.py +2 -0
- danielutils/conversions/specialized_conversions/to_hex.py +25 -0
- danielutils/conversions/specialized_conversions/to_int.py +19 -0
- danielutils/custom_types.py +18 -0
- danielutils/data_structures/__init__.py +8 -0
- danielutils/data_structures/algorithms.py +52 -0
- danielutils/data_structures/comparer.py +47 -0
- danielutils/data_structures/default_dict.py +23 -0
- danielutils/data_structures/functions.py +35 -0
- danielutils/data_structures/graph/__init__.py +4 -0
- danielutils/data_structures/graph/binary_node.py +71 -0
- danielutils/data_structures/graph/graph.py +208 -0
- danielutils/data_structures/graph/multinode.py +101 -0
- danielutils/data_structures/graph/node.py +60 -0
- danielutils/data_structures/heap/__init__.py +3 -0
- danielutils/data_structures/heap/heap.py +119 -0
- danielutils/data_structures/heap/max_heap.py +18 -0
- danielutils/data_structures/heap/min_heap.py +18 -0
- danielutils/data_structures/queue/__init__.py +3 -0
- danielutils/data_structures/queue/atomic_queue.py +15 -0
- danielutils/data_structures/queue/priority_queue.py +110 -0
- danielutils/data_structures/queue/queue.py +92 -0
- danielutils/data_structures/stack.py +102 -0
- danielutils/data_structures/trees/__init__.py +2 -0
- danielutils/data_structures/trees/binary_syntax_tree.py +84 -0
- danielutils/data_structures/trees/binary_tree.py +63 -0
- danielutils/date.py +43 -0
- danielutils/date_time.py +23 -0
- danielutils/decorators/__init__.py +18 -0
- danielutils/decorators/atomic.py +45 -0
- danielutils/decorators/attach.py +61 -0
- danielutils/decorators/chain_decorators.py +39 -0
- danielutils/decorators/decorate_conditionally.py +32 -0
- danielutils/decorators/delay_call.py +47 -0
- danielutils/decorators/deprecate.py +46 -0
- danielutils/decorators/final.py +39 -0
- danielutils/decorators/limit_recursion.py +63 -0
- danielutils/decorators/memo.py +68 -0
- danielutils/decorators/normalize_decorator.py +55 -0
- danielutils/decorators/overload.py +204 -0
- danielutils/decorators/partially_implemented.py +32 -0
- danielutils/decorators/processify.py +74 -0
- danielutils/decorators/property.py +42 -0
- danielutils/decorators/singleton.py +59 -0
- danielutils/decorators/threadify.py +40 -0
- danielutils/decorators/timeout.py +82 -0
- danielutils/decorators/total_ordering.py +82 -0
- danielutils/decorators/validate.py +227 -0
- danielutils/exceptions.py +90 -0
- danielutils/functions/__init__.py +13 -0
- danielutils/functions/areoneof.py +47 -0
- danielutils/functions/check_foreach.py +39 -0
- danielutils/functions/factorial.py +24 -0
- danielutils/functions/flatten.py +33 -0
- danielutils/functions/foreach.py +20 -0
- danielutils/functions/isoftype.py +475 -0
- danielutils/functions/isoneof.py +71 -0
- danielutils/functions/multiloop.py +51 -0
- danielutils/functions/parallel_for.py +47 -0
- danielutils/functions/partition.py +45 -0
- danielutils/functions/powerset.py +31 -0
- danielutils/functions/subseteq.py +25 -0
- danielutils/functions/types_subseteq.py +51 -0
- danielutils/generators/__init__.py +3 -0
- danielutils/generators/conditional_generator.py +59 -0
- danielutils/generators/generator_from_stream.py +31 -0
- danielutils/generators/join_generators.py +109 -0
- danielutils/internet.py +92 -0
- danielutils/io_.py +451 -0
- danielutils/java/__init__.py +18 -0
- danielutils/java/interfaces/__init__.py +1 -0
- danielutils/java/interfaces/comparable.py +31 -0
- danielutils/java/java_interface.py +59 -0
- danielutils/logging_/__init__.py +2 -0
- danielutils/logging_/_impl/__init__.py +4 -0
- danielutils/logging_/_impl/builtin_impls/__init__.py +2 -0
- danielutils/logging_/_impl/builtin_impls/file_logger.py +35 -0
- danielutils/logging_/_impl/builtin_impls/print_logger.py +21 -0
- danielutils/logging_/_impl/log_level.py +13 -0
- danielutils/logging_/_impl/logger.py +109 -0
- danielutils/logging_/_impl/logger_strategy_impl_base.py +37 -0
- danielutils/logging_/utils.py +199 -0
- danielutils/lombok/__init__.py +1 -0
- danielutils/lombok/builder.py +68 -0
- danielutils/math_/__init__.py +2 -0
- danielutils/math_/constants.py +103 -0
- danielutils/math_/functions.py +34 -0
- danielutils/math_/math_print.py +52 -0
- danielutils/math_/math_symbols.py +125 -0
- danielutils/metaclasses/__init__.py +5 -0
- danielutils/metaclasses/atomic_class_meta.py +36 -0
- danielutils/metaclasses/implicit_data_deleter_meta.py +87 -0
- danielutils/metaclasses/instance_cache_meta.py +66 -0
- danielutils/metaclasses/interface.py +328 -0
- danielutils/metaclasses/overload_meta.py +48 -0
- danielutils/mock_/__init__.py +1 -0
- danielutils/mock_/mock_module.py +27 -0
- danielutils/path.py +55 -0
- danielutils/print_.py +123 -0
- danielutils/progress_bar/__init__.py +3 -0
- danielutils/progress_bar/ascii_progress_bar.py +161 -0
- danielutils/progress_bar/progress_bar.py +75 -0
- danielutils/progress_bar/progress_bar_pool.py +72 -0
- danielutils/protocols/__init__.py +3 -0
- danielutils/protocols/dictable.py +38 -0
- danielutils/protocols/evaluable.py +26 -0
- danielutils/protocols/serializable.py +51 -0
- danielutils/py.typed +0 -0
- danielutils/random_.py +47 -0
- danielutils/reflection/__init__.py +6 -0
- danielutils/reflection/declarations.py +288 -0
- danielutils/reflection/file/__init__.py +1 -0
- danielutils/reflection/file/file_reflection.py +85 -0
- danielutils/reflection/function/__init__.py +1 -0
- danielutils/reflection/function/function_reflections.py +152 -0
- danielutils/reflection/info_classes/__init__.py +6 -0
- danielutils/reflection/info_classes/argument_info.py +278 -0
- danielutils/reflection/info_classes/class_info.py +204 -0
- danielutils/reflection/info_classes/decorator_info.py +54 -0
- danielutils/reflection/info_classes/file_info.py +1180 -0
- danielutils/reflection/info_classes/folder_info.py +592 -0
- danielutils/reflection/info_classes/function_info.py +551 -0
- danielutils/reflection/info_classes/import_info.py +207 -0
- danielutils/reflection/interpreter/__init__.py +9 -0
- danielutils/reflection/interpreter/callstack.py +57 -0
- danielutils/reflection/interpreter/get_traceback.py +27 -0
- danielutils/reflection/interpreter/interpreter.py +41 -0
- danielutils/reflection/interpreter/is_debugging.py +13 -0
- danielutils/reflection/interpreter/os_.py +33 -0
- danielutils/reflection/interpreter/packages.py +33 -0
- danielutils/reflection/interpreter/python_version.py +33 -0
- danielutils/reflection/interpreter/signals.py +18 -0
- danielutils/reflection/interpreter/tracer.py +175 -0
- danielutils/reflection/module/__init__.py +3 -0
- danielutils/reflection/module/lazy_module.py +104 -0
- danielutils/reflection/module/module_reflections.py +34 -0
- danielutils/reflection/module/package_reflection.py +161 -0
- danielutils/retry_executor/__init__.py +3 -0
- danielutils/retry_executor/backoff_strategies/__init__.py +6 -0
- danielutils/retry_executor/backoff_strategies/constant_backoff.py +25 -0
- danielutils/retry_executor/backoff_strategies/exponential_backoff.py +26 -0
- danielutils/retry_executor/backoff_strategies/functional_backoff.py +29 -0
- danielutils/retry_executor/backoff_strategies/linear_backoff.py +23 -0
- danielutils/retry_executor/backoff_strategies/multiplicative_backoff.py +23 -0
- danielutils/retry_executor/backoff_strategies/no_backoff.py +17 -0
- danielutils/retry_executor/backoff_strategy.py +28 -0
- danielutils/retry_executor/retry_executor.py +55 -0
- danielutils/snippets/__init__.py +1 -0
- danielutils/snippets/try_get.py +28 -0
- danielutils/system/__init__.py +3 -0
- danielutils/system/independent.py +197 -0
- danielutils/system/layered_command.py +130 -0
- danielutils/system/windows/__init__.py +2 -0
- danielutils/system/windows/utils/__init__.py +1 -0
- danielutils/system/windows/utils/filetime.py +19 -0
- danielutils/system/windows/win32_ctime.py +183 -0
- danielutils/system/windows/windows.py +78 -0
- danielutils/testing/__init__.py +1 -0
- danielutils/testing/unittest_/__init__.py +2 -0
- danielutils/testing/unittest_/always_teardown_testcase.py +75 -0
- danielutils/testing/unittest_/auto_cwd_testcase.py +114 -0
- danielutils/text.py +169 -0
- danielutils/time.py +64 -0
- danielutils/tools/__init__.py +1 -0
- danielutils/tools/unittest_test_runner/__init__.py +9 -0
- danielutils/tools/unittest_test_runner/cli.py +70 -0
- danielutils/tools/unittest_test_runner/core/__init__.py +17 -0
- danielutils/tools/unittest_test_runner/core/discovery.py +322 -0
- danielutils/tools/unittest_test_runner/core/execution.py +244 -0
- danielutils/tools/unittest_test_runner/core/parser.py +163 -0
- danielutils/tools/unittest_test_runner/core/runner.py +561 -0
- danielutils/tools/unittest_test_runner/models.py +82 -0
- danielutils/university/__init__.py +7 -0
- danielutils/university/computability_and_complexity/__init__.py +2 -0
- danielutils/university/computability_and_complexity/discreate_finite_automaton.py +89 -0
- danielutils/university/computability_and_complexity/languages/__init__.py +0 -0
- danielutils/university/computability_and_complexity/languages/language.py +17 -0
- danielutils/university/computability_and_complexity/languages/sat.py +71 -0
- danielutils/university/computability_and_complexity/turing_machine.py +47 -0
- danielutils/university/databases/__init__.py +1 -0
- danielutils/university/databases/all.py +756 -0
- danielutils/university/image_proccesing/__init__.py +1 -0
- danielutils/university/image_proccesing/encoding/__init__.py +3 -0
- danielutils/university/image_proccesing/encoding/encoding.py +35 -0
- danielutils/university/image_proccesing/encoding/lossless/__init__.py +4 -0
- danielutils/university/image_proccesing/encoding/lossless/huffman.py +0 -0
- danielutils/university/image_proccesing/encoding/lossless/lossless_encoding.py +10 -0
- danielutils/university/image_proccesing/encoding/lossless/lzw.py +0 -0
- danielutils/university/image_proccesing/encoding/lossless/run_length.py +22 -0
- danielutils/university/image_proccesing/encoding/lossy/__init__.py +1 -0
- danielutils/university/image_proccesing/encoding/lossy/lossy_encoding.py +10 -0
- danielutils/university/image_proccesing/tansformations/__init__.py +0 -0
- danielutils/university/image_proccesing/tansformations/gaussian.py +0 -0
- danielutils/university/image_proccesing/tansformations/gradient.py +0 -0
- danielutils/university/image_proccesing/tansformations/hough.py +0 -0
- danielutils/university/image_proccesing/tansformations/laplacian.py +0 -0
- danielutils/university/image_proccesing/tansformations/transformation.py +11 -0
- danielutils/university/linear_algebra/__init__.py +1 -0
- danielutils/university/linear_algebra/matrix.py +404 -0
- danielutils/university/machine_learning/__init__.py +0 -0
- danielutils/university/machine_learning/activation_functions/__init__.py +2 -0
- danielutils/university/machine_learning/activation_functions/activation_function.py +11 -0
- danielutils/university/machine_learning/activation_functions/relu.py +11 -0
- danielutils/university/machine_learning/neuron.py +15 -0
- danielutils/university/oop/__init__.py +1 -0
- danielutils/university/oop/observer.py +31 -0
- danielutils/university/oop/strategy.py +9 -0
- danielutils/university/probability/__init__.py +7 -0
- danielutils/university/probability/conditional_variable/__init__.py +3 -0
- danielutils/university/probability/conditional_variable/conditional_variable.py +138 -0
- danielutils/university/probability/conditional_variable/continuous/__init__.py +0 -0
- danielutils/university/probability/conditional_variable/discrete/__init__.py +8 -0
- danielutils/university/probability/conditional_variable/discrete/bernoulli.py +38 -0
- danielutils/university/probability/conditional_variable/discrete/bernoulli_sum.py +31 -0
- danielutils/university/probability/conditional_variable/discrete/binomial.py +40 -0
- danielutils/university/probability/conditional_variable/discrete/conditional_from_discrete_probability_func.py +36 -0
- danielutils/university/probability/conditional_variable/discrete/discrete.py +42 -0
- danielutils/university/probability/conditional_variable/discrete/geometric.py +43 -0
- danielutils/university/probability/conditional_variable/discrete/poisson.py +32 -0
- danielutils/university/probability/conditional_variable/discrete/uniform.py +33 -0
- danielutils/university/probability/distributions.py +35 -0
- danielutils/university/probability/expressions/__init__.py +2 -0
- danielutils/university/probability/expressions/accumulation_expression.py +291 -0
- danielutils/university/probability/expressions/probability_expression.py +110 -0
- danielutils/university/probability/funcs/__init__.py +4 -0
- danielutils/university/probability/funcs/covariance.py +12 -0
- danielutils/university/probability/funcs/expected_value.py +41 -0
- danielutils/university/probability/funcs/probability_function.py +14 -0
- danielutils/university/probability/funcs/variance.py +17 -0
- danielutils/university/probability/operator.py +79 -0
- danielutils/university/probability/protocols.py +32 -0
- danielutils/university/probability/supp.py +101 -0
- danielutils/university/probability/transformation.py +44 -0
- danielutils/versioned_imports.py +31 -0
- danielutils-1.1.28.dist-info/METADATA +390 -0
- danielutils-1.1.28.dist-info/RECORD +287 -0
- danielutils-1.1.28.dist-info/WHEEL +5 -0
- danielutils-1.1.28.dist-info/licenses/LICENSE +21 -0
- danielutils-1.1.28.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main UnittestRunner orchestration functionality.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
import json
|
|
10
|
+
import subprocess
|
|
11
|
+
from typing import List, Optional, Literal, Tuple
|
|
12
|
+
from ..models import UnittestFunctionState, ModuleState, UnittestResult, UnittestRunSummary, UnittestDiscovery
|
|
13
|
+
from .discovery import UnittestDiscoveryService
|
|
14
|
+
from .execution import UnittestExecutor
|
|
15
|
+
from .parser import UnittestOutputParser
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
VerboseLevel = Literal["module", "file", "class", "function"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UnittestRunner:
|
|
23
|
+
"""Smart test runner that executes tests module by module and collects detailed statistics."""
|
|
24
|
+
|
|
25
|
+
def __init__(self,
|
|
26
|
+
python_path: Optional[str] = None,
|
|
27
|
+
results_file: str = "test_results.json",
|
|
28
|
+
skip_threshold_hours: int = 24,
|
|
29
|
+
force_run: bool = False,
|
|
30
|
+
test_name: Optional[str] = None,
|
|
31
|
+
target: Optional[str] = None,
|
|
32
|
+
verbose: VerboseLevel = "class",
|
|
33
|
+
show_function_results: bool = False):
|
|
34
|
+
logger.info("Initializing UnittestRunner with python_path=%s, results_file=%s, skip_threshold_hours=%d, force_run=%s, target=%s, verbose=%s",
|
|
35
|
+
python_path, results_file, skip_threshold_hours, force_run, target, verbose)
|
|
36
|
+
|
|
37
|
+
self.python_path = python_path or sys.executable
|
|
38
|
+
self.results_file = results_file
|
|
39
|
+
self.skip_threshold_hours = skip_threshold_hours
|
|
40
|
+
self.force_run = force_run
|
|
41
|
+
self.test_name = test_name
|
|
42
|
+
self.target = target
|
|
43
|
+
self.verbose = verbose
|
|
44
|
+
self.show_function_results = show_function_results
|
|
45
|
+
|
|
46
|
+
# Ensure we have a valid Python executable
|
|
47
|
+
if not self.python_path or not os.path.exists(self.python_path):
|
|
48
|
+
logger.warning("Invalid Python path provided, using sys.executable: %s", sys.executable)
|
|
49
|
+
self.python_path = sys.executable
|
|
50
|
+
|
|
51
|
+
# Validate verbose level (this should be caught by type checker, but good to have runtime check)
|
|
52
|
+
valid_levels: List[VerboseLevel] = ["module", "file", "class", "function"]
|
|
53
|
+
if self.verbose not in valid_levels:
|
|
54
|
+
logger.error("Invalid verbose level '%s'. Must be one of: %s", self.verbose, valid_levels)
|
|
55
|
+
raise ValueError(f"Invalid verbose level '{self.verbose}'. Must be one of: {valid_levels}")
|
|
56
|
+
|
|
57
|
+
# Debug output (only in function level)
|
|
58
|
+
if self.verbose == "function":
|
|
59
|
+
logger.debug("Using Python executable: %s", self.python_path)
|
|
60
|
+
print(f"DEBUG: Using Python executable: {self.python_path}")
|
|
61
|
+
|
|
62
|
+
# Initialize services
|
|
63
|
+
logger.debug("Initializing discovery service and parser")
|
|
64
|
+
self.discovery_service = UnittestDiscoveryService(self.python_path, self.verbose)
|
|
65
|
+
self.parser = UnittestOutputParser(self.verbose)
|
|
66
|
+
|
|
67
|
+
# Initialize state
|
|
68
|
+
self.results: List[UnittestResult] = []
|
|
69
|
+
self.previous_results: Optional[UnittestRunSummary] = None
|
|
70
|
+
logger.debug("Loading previous results from: %s", self.results_file)
|
|
71
|
+
self._load_previous_results()
|
|
72
|
+
|
|
73
|
+
# Discover test modules based on target
|
|
74
|
+
logger.info("Starting test discovery phase")
|
|
75
|
+
if self.verbose in ["module", "file", "class", "function"]:
|
|
76
|
+
print("🔍 Test Discovery Phase")
|
|
77
|
+
print("=" * 50)
|
|
78
|
+
|
|
79
|
+
if self.target:
|
|
80
|
+
# If target is specified, discover modules relative to target
|
|
81
|
+
logger.info("Target specified, discovering modules for target: %s", self.target)
|
|
82
|
+
if self.verbose in ["module", "file", "class", "function"]:
|
|
83
|
+
print(f"🎯 Target specified: {self.target}")
|
|
84
|
+
self.test_modules = self.discovery_service.discover_test_modules(target=self.target)
|
|
85
|
+
else:
|
|
86
|
+
# No target, discover all modules in default tests directory
|
|
87
|
+
logger.info("No target specified, discovering all test modules")
|
|
88
|
+
if self.verbose in ["module", "file", "class", "function"]:
|
|
89
|
+
print("📁 No target specified, discovering all test modules")
|
|
90
|
+
self.test_modules = self.discovery_service.discover_test_modules()
|
|
91
|
+
|
|
92
|
+
logger.info("Discovered %d test modules", len(self.test_modules))
|
|
93
|
+
self.test_discovery: Optional[UnittestDiscovery] = None
|
|
94
|
+
self._discover_test_structure()
|
|
95
|
+
|
|
96
|
+
# Apply target filtering if specified
|
|
97
|
+
if self.target and self.test_discovery:
|
|
98
|
+
logger.debug("Applying target filtering for: %s", self.target)
|
|
99
|
+
target_module, target_class, target_function = self.discovery_service.parse_target(self.target)
|
|
100
|
+
if target_module:
|
|
101
|
+
self.test_discovery = self.discovery_service.create_focused_discovery(
|
|
102
|
+
target_module, target_class, target_function, self.test_discovery
|
|
103
|
+
)
|
|
104
|
+
logger.debug("Focused discovery created for target: %s (module: %s, class: %s, function: %s)",
|
|
105
|
+
self.target, target_module, target_class, target_function)
|
|
106
|
+
if self.verbose == "function":
|
|
107
|
+
print(f"DEBUG: Focused discovery created for target: {self.target}")
|
|
108
|
+
print(f"DEBUG: Target components - module: {target_module}, class: {target_class}, function: {target_function}")
|
|
109
|
+
|
|
110
|
+
# Initialize executor
|
|
111
|
+
logger.debug("Initializing test executor")
|
|
112
|
+
self.executor = UnittestExecutor(self.python_path, self.verbose, self.test_discovery)
|
|
113
|
+
|
|
114
|
+
def _discover_test_structure(self):
|
|
115
|
+
"""Discover the complete test structure for progress tracking."""
|
|
116
|
+
logger.debug("Discovering test structure for %d modules", len(self.test_modules))
|
|
117
|
+
self.test_discovery = self.discovery_service.discover_test_structure(self.test_modules)
|
|
118
|
+
if self.test_discovery:
|
|
119
|
+
logger.info("Test structure discovery complete: %d modules, %d classes, %d functions",
|
|
120
|
+
self.test_discovery.total_modules, self.test_discovery.total_classes, self.test_discovery.total_functions)
|
|
121
|
+
|
|
122
|
+
def _load_previous_results(self):
|
|
123
|
+
"""Load previous test results from JSON file."""
|
|
124
|
+
if not os.path.exists(self.results_file):
|
|
125
|
+
logger.info("No previous results file found: %s", self.results_file)
|
|
126
|
+
print(f"No previous results file found: {self.results_file}")
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
logger.debug("Loading previous results from: %s", self.results_file)
|
|
131
|
+
with open(self.results_file, 'r') as f:
|
|
132
|
+
data = json.load(f)
|
|
133
|
+
|
|
134
|
+
# Convert results list from dicts to UnittestResult objects
|
|
135
|
+
if 'results' in data and isinstance(data['results'], list):
|
|
136
|
+
results = []
|
|
137
|
+
logger.debug("Converting %d previous results from dicts to objects", len(data['results']))
|
|
138
|
+
for result_dict in data['results']:
|
|
139
|
+
# Convert nested dicts to proper objects
|
|
140
|
+
if 'module_state' in result_dict:
|
|
141
|
+
module_state_dict = result_dict['module_state']
|
|
142
|
+
# Convert test_functions dict to UnittestFunctionState objects
|
|
143
|
+
test_functions = {}
|
|
144
|
+
if 'test_functions' in module_state_dict:
|
|
145
|
+
for func_name, func_dict in module_state_dict['test_functions'].items():
|
|
146
|
+
test_functions[func_name] = UnittestFunctionState(**func_dict)
|
|
147
|
+
|
|
148
|
+
# Create ModuleState objects
|
|
149
|
+
initial_state = UnittestFunctionState(**module_state_dict['initial_state']) if module_state_dict.get('initial_state') else None
|
|
150
|
+
current_state = UnittestFunctionState(**module_state_dict['current_state']) if module_state_dict.get('current_state') else None
|
|
151
|
+
|
|
152
|
+
module_state = ModuleState(
|
|
153
|
+
initial_state=initial_state,
|
|
154
|
+
current_state=current_state,
|
|
155
|
+
test_functions=test_functions
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
result = UnittestResult(
|
|
159
|
+
module_path=result_dict['module_path'],
|
|
160
|
+
module_state=module_state,
|
|
161
|
+
errors=result_dict.get('errors', []),
|
|
162
|
+
warnings=result_dict.get('warnings', [])
|
|
163
|
+
)
|
|
164
|
+
results.append(result)
|
|
165
|
+
|
|
166
|
+
data['results'] = results
|
|
167
|
+
|
|
168
|
+
self.previous_results = UnittestRunSummary(**data)
|
|
169
|
+
logger.info("Successfully loaded previous results: %d modules, %d tests",
|
|
170
|
+
self.previous_results.total_modules, self.previous_results.total_tests)
|
|
171
|
+
except Exception as e:
|
|
172
|
+
logger.error("Error loading previous results: %s", e)
|
|
173
|
+
print(f"Error loading previous results: {e}")
|
|
174
|
+
self.previous_results = None
|
|
175
|
+
|
|
176
|
+
def _should_skip_module(self, module_path: str) -> Tuple[bool, str]:
|
|
177
|
+
"""Determine if a module should be skipped based on previous results."""
|
|
178
|
+
logger.debug("Checking if module should be skipped: %s", module_path)
|
|
179
|
+
|
|
180
|
+
if self.force_run:
|
|
181
|
+
logger.debug("Force run enabled, not skipping module: %s", module_path)
|
|
182
|
+
return False, "Force run enabled"
|
|
183
|
+
|
|
184
|
+
if not self.previous_results:
|
|
185
|
+
logger.debug("No previous results available, not skipping module: %s", module_path)
|
|
186
|
+
return False, "No previous results"
|
|
187
|
+
|
|
188
|
+
# Find previous result for this module
|
|
189
|
+
for result in self.previous_results.results:
|
|
190
|
+
if result.module_path == module_path:
|
|
191
|
+
current_state = result.current_state
|
|
192
|
+
|
|
193
|
+
# Check if all tests passed
|
|
194
|
+
if current_state.failed == 0 and current_state.errors == 0:
|
|
195
|
+
# Check if recent enough
|
|
196
|
+
try:
|
|
197
|
+
from datetime import datetime
|
|
198
|
+
result_time = datetime.strptime(current_state.timestamp, '%Y-%m-%d %H:%M:%S')
|
|
199
|
+
current_time = datetime.now()
|
|
200
|
+
hours_ago = (current_time - result_time).total_seconds() / 3600
|
|
201
|
+
|
|
202
|
+
if hours_ago < self.skip_threshold_hours:
|
|
203
|
+
logger.debug("Skipping module %s: all tests passed %.1f hours ago", module_path, hours_ago)
|
|
204
|
+
return True, f"All tests passed {hours_ago:.1f}h ago"
|
|
205
|
+
else:
|
|
206
|
+
logger.debug("Not skipping module %s: tests passed %.1f hours ago (threshold: %d)",
|
|
207
|
+
module_path, hours_ago, self.skip_threshold_hours)
|
|
208
|
+
except Exception as e:
|
|
209
|
+
logger.debug("Error parsing timestamp for module %s: %s", module_path, e)
|
|
210
|
+
pass
|
|
211
|
+
else:
|
|
212
|
+
logger.debug("Not skipping module %s: has failures or errors", module_path)
|
|
213
|
+
|
|
214
|
+
break
|
|
215
|
+
|
|
216
|
+
logger.debug("Module %s needs attention", module_path)
|
|
217
|
+
return False, "Module needs attention"
|
|
218
|
+
|
|
219
|
+
def _create_module_state(self, module_path: str, test_functions: List[UnittestFunctionState],
|
|
220
|
+
total_runtime: float) -> ModuleState:
|
|
221
|
+
"""Create a ModuleState from test function results."""
|
|
222
|
+
if not test_functions:
|
|
223
|
+
return ModuleState(
|
|
224
|
+
module_path=module_path,
|
|
225
|
+
test_functions=[],
|
|
226
|
+
total_tests=0,
|
|
227
|
+
passed=0,
|
|
228
|
+
failed=0,
|
|
229
|
+
skipped=0,
|
|
230
|
+
errors=0,
|
|
231
|
+
total_runtime=total_runtime,
|
|
232
|
+
average_runtime=0.0,
|
|
233
|
+
slowest_test="",
|
|
234
|
+
fastest_test="",
|
|
235
|
+
success_rate=0.0,
|
|
236
|
+
timestamp=time.strftime('%Y-%m-%d %H:%M:%S'),
|
|
237
|
+
should_skip_module=False,
|
|
238
|
+
skip_reason="",
|
|
239
|
+
improvement_from_initial=False
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Calculate statistics
|
|
243
|
+
total_tests = len(test_functions)
|
|
244
|
+
passed = sum(1 for t in test_functions if t.status == "passed")
|
|
245
|
+
failed = sum(1 for t in test_functions if t.status == "failed")
|
|
246
|
+
skipped = sum(1 for t in test_functions if t.status == "skipped")
|
|
247
|
+
errors = sum(1 for t in test_functions if t.status == "error")
|
|
248
|
+
|
|
249
|
+
# Calculate runtimes
|
|
250
|
+
runtimes = [t.runtime for t in test_functions if t.runtime > 0]
|
|
251
|
+
average_runtime = sum(runtimes) / len(runtimes) if runtimes else 0.0
|
|
252
|
+
|
|
253
|
+
# Find slowest and fastest tests
|
|
254
|
+
if runtimes:
|
|
255
|
+
slowest_idx = max(range(len(runtimes)), key=lambda i: runtimes[i])
|
|
256
|
+
fastest_idx = min(range(len(runtimes)), key=lambda i: runtimes[i])
|
|
257
|
+
slowest_test = test_functions[slowest_idx].function_name
|
|
258
|
+
fastest_test = test_functions[fastest_idx].function_name
|
|
259
|
+
else:
|
|
260
|
+
slowest_test = ""
|
|
261
|
+
fastest_test = ""
|
|
262
|
+
|
|
263
|
+
success_rate = (passed / total_tests * 100) if total_tests > 0 else 0.0
|
|
264
|
+
|
|
265
|
+
return ModuleState(
|
|
266
|
+
module_path=module_path,
|
|
267
|
+
test_functions=test_functions,
|
|
268
|
+
total_tests=total_tests,
|
|
269
|
+
passed=passed,
|
|
270
|
+
failed=failed,
|
|
271
|
+
skipped=skipped,
|
|
272
|
+
errors=errors,
|
|
273
|
+
total_runtime=total_runtime,
|
|
274
|
+
average_runtime=average_runtime,
|
|
275
|
+
slowest_test=slowest_test,
|
|
276
|
+
fastest_test=fastest_test,
|
|
277
|
+
success_rate=success_rate,
|
|
278
|
+
timestamp=time.strftime('%Y-%m-%d %H:%M:%S'),
|
|
279
|
+
should_skip_module=False,
|
|
280
|
+
skip_reason="",
|
|
281
|
+
improvement_from_initial=False
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
def run_all_tests(self):
|
|
285
|
+
"""Run all tests with smart skipping and detailed reporting."""
|
|
286
|
+
logger.info("Starting test execution for %d modules", len(self.test_modules))
|
|
287
|
+
print("Loaded previous results from test_results.json")
|
|
288
|
+
print(f"Found {len(self.test_modules)} test modules to evaluate")
|
|
289
|
+
print("=" * 80)
|
|
290
|
+
print()
|
|
291
|
+
|
|
292
|
+
# Determine which modules to run
|
|
293
|
+
modules_to_run = []
|
|
294
|
+
modules_to_skip = []
|
|
295
|
+
|
|
296
|
+
logger.debug("Evaluating %d modules for skipping", len(self.test_modules))
|
|
297
|
+
for module_path in self.test_modules:
|
|
298
|
+
should_skip, reason = self._should_skip_module(module_path)
|
|
299
|
+
if should_skip:
|
|
300
|
+
modules_to_skip.append((module_path, reason))
|
|
301
|
+
else:
|
|
302
|
+
modules_to_run.append(module_path)
|
|
303
|
+
|
|
304
|
+
logger.info("Module evaluation complete: %d to run, %d to skip", len(modules_to_run), len(modules_to_skip))
|
|
305
|
+
|
|
306
|
+
# Print skipping summary
|
|
307
|
+
if modules_to_skip:
|
|
308
|
+
print(f"SKIPPING {len(modules_to_skip)} modules that are in good shape:")
|
|
309
|
+
for module_path, reason in modules_to_skip:
|
|
310
|
+
print(f" → {module_path}: {reason}")
|
|
311
|
+
print()
|
|
312
|
+
|
|
313
|
+
# Print running summary
|
|
314
|
+
if modules_to_run:
|
|
315
|
+
print(f"RUNNING {len(modules_to_run)} modules that need attention:")
|
|
316
|
+
for module_path in modules_to_run:
|
|
317
|
+
should_skip, reason = self._should_skip_module(module_path)
|
|
318
|
+
print(f" → {module_path}: {reason}")
|
|
319
|
+
print()
|
|
320
|
+
print("=" * 80)
|
|
321
|
+
print()
|
|
322
|
+
|
|
323
|
+
# Run the modules
|
|
324
|
+
logger.info("Starting execution of %d modules", len(modules_to_run))
|
|
325
|
+
for i, module_path in enumerate(modules_to_run, 1):
|
|
326
|
+
logger.info("Running module %d/%d: %s", i, len(modules_to_run), module_path)
|
|
327
|
+
if self.verbose in ["module", "file", "class", "function"]:
|
|
328
|
+
print(f"[{i}/{len(modules_to_run)}] Testing module: {module_path}")
|
|
329
|
+
print("-" * 60)
|
|
330
|
+
|
|
331
|
+
result = self._run_test_module(module_path)
|
|
332
|
+
self.results.append(result)
|
|
333
|
+
|
|
334
|
+
# Print module summary based on verbose level
|
|
335
|
+
state = result.current_state
|
|
336
|
+
logger.info("Module %s completed: %d passed, %d failed, %d errors, %d skipped",
|
|
337
|
+
module_path, state.passed, state.failed, state.errors, state.skipped)
|
|
338
|
+
|
|
339
|
+
if self.verbose == "module":
|
|
340
|
+
# Minimal output for module level
|
|
341
|
+
status = "✓" if state.failed == 0 and state.errors == 0 else "✗"
|
|
342
|
+
print(f"{status} {module_path}: {state.passed} passed, {state.failed} failed, {state.errors} errors")
|
|
343
|
+
else:
|
|
344
|
+
# Detailed output for file/class/function levels
|
|
345
|
+
print(f"Results: {state.passed} passed, {state.failed} failed, {state.skipped} skipped, {state.errors} errors")
|
|
346
|
+
print(f"Runtime: {state.total_runtime:.3f}s total, {state.average_runtime:.3f}s average")
|
|
347
|
+
|
|
348
|
+
if result.errors:
|
|
349
|
+
print(f"Errors: {len(result.errors)} issues found")
|
|
350
|
+
|
|
351
|
+
if result.overall_improvement:
|
|
352
|
+
print("✓ Module improved from previous run!")
|
|
353
|
+
|
|
354
|
+
print()
|
|
355
|
+
|
|
356
|
+
# Print final summary
|
|
357
|
+
logger.info("All test execution completed")
|
|
358
|
+
self._print_final_summary()
|
|
359
|
+
|
|
360
|
+
def _run_test_module(self, module_path: str) -> UnittestResult:
|
|
361
|
+
"""Run a single test module and collect detailed statistics."""
|
|
362
|
+
logger.debug("Running test module: %s", module_path)
|
|
363
|
+
if self.verbose in ["module", "file", "class", "function"]:
|
|
364
|
+
print(f"Running tests for module: {module_path}")
|
|
365
|
+
|
|
366
|
+
# Get initial state from previous results if available
|
|
367
|
+
initial_state: Optional[ModuleState] = None
|
|
368
|
+
if self.previous_results:
|
|
369
|
+
for result in self.previous_results.results:
|
|
370
|
+
if result.module_path == module_path:
|
|
371
|
+
initial_state = result.current_state
|
|
372
|
+
logger.debug("Found previous state for module %s: %d passed, %d failed",
|
|
373
|
+
module_path, initial_state.passed, initial_state.failed)
|
|
374
|
+
break
|
|
375
|
+
|
|
376
|
+
# Run the test and collect results
|
|
377
|
+
start_time = time.time()
|
|
378
|
+
errors: List[str] = []
|
|
379
|
+
warnings: List[str] = []
|
|
380
|
+
|
|
381
|
+
try:
|
|
382
|
+
logger.debug("Executing test module %s with verbose level: %s", module_path, self.verbose)
|
|
383
|
+
# Use hierarchical approach based on verbose level
|
|
384
|
+
if self.verbose == "function":
|
|
385
|
+
# Run individual test functions (most verbose)
|
|
386
|
+
test_functions = self.executor.run_test_file(module_path)
|
|
387
|
+
elif self.verbose == "class":
|
|
388
|
+
# Run test classes (default)
|
|
389
|
+
test_functions = self.executor.run_test_file(module_path)
|
|
390
|
+
elif self.verbose == "file":
|
|
391
|
+
# Run test file
|
|
392
|
+
test_functions = self.executor.run_test_file(module_path)
|
|
393
|
+
else: # module level
|
|
394
|
+
# Run entire module at once (least verbose)
|
|
395
|
+
cmd = [self.python_path, "-m", "unittest", module_path, "-v"]
|
|
396
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
|
397
|
+
|
|
398
|
+
# Parse the output to extract test statistics
|
|
399
|
+
output_lines = result.stdout.split('\n')
|
|
400
|
+
stderr_lines = result.stderr.split('\n')
|
|
401
|
+
|
|
402
|
+
# If stdout is empty but stderr has test output, use stderr
|
|
403
|
+
if not any("test_" in line for line in output_lines) and any("test_" in line for line in stderr_lines):
|
|
404
|
+
output_lines = stderr_lines
|
|
405
|
+
stderr_lines = []
|
|
406
|
+
|
|
407
|
+
# Parse individual test functions
|
|
408
|
+
test_functions, parse_errors, parse_warnings = self.parser.parse_test_output(output_lines, stderr_lines)
|
|
409
|
+
errors.extend(parse_errors)
|
|
410
|
+
warnings.extend(parse_warnings)
|
|
411
|
+
|
|
412
|
+
total_runtime = time.time() - start_time
|
|
413
|
+
|
|
414
|
+
# Show individual function results if requested
|
|
415
|
+
if self.show_function_results and test_functions:
|
|
416
|
+
print(f" Individual test results for {module_path}:")
|
|
417
|
+
for test_func in test_functions:
|
|
418
|
+
status_symbol = "✓" if test_func.status == "passed" else "✗" if test_func.status == "failed" else "⚠" if test_func.status == "error" else "⏭" if test_func.status == "skipped" else "?"
|
|
419
|
+
print(f" {status_symbol} {test_func.function_name} ({test_func.status}) - {test_func.runtime:.3f}s")
|
|
420
|
+
|
|
421
|
+
# Create current state
|
|
422
|
+
current_state = self._create_module_state(module_path, test_functions, total_runtime)
|
|
423
|
+
|
|
424
|
+
# Determine if module improved
|
|
425
|
+
improvement_from_initial = False
|
|
426
|
+
if initial_state:
|
|
427
|
+
improvement_from_initial = (
|
|
428
|
+
current_state.passed > initial_state.passed or
|
|
429
|
+
current_state.failed < initial_state.failed or
|
|
430
|
+
current_state.errors < initial_state.errors
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
return UnittestResult(
|
|
434
|
+
module_path=module_path,
|
|
435
|
+
initial_state=initial_state,
|
|
436
|
+
current_state=current_state,
|
|
437
|
+
errors=errors,
|
|
438
|
+
warnings=warnings,
|
|
439
|
+
overall_improvement=improvement_from_initial
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
except subprocess.TimeoutExpired:
|
|
443
|
+
error_msg = f"Test module {module_path} timed out after 5 minutes"
|
|
444
|
+
logger.error("Test module %s timed out after 5 minutes", module_path)
|
|
445
|
+
errors.append(error_msg)
|
|
446
|
+
print(f"ERROR: {error_msg}")
|
|
447
|
+
|
|
448
|
+
# Create error state
|
|
449
|
+
current_state = self._create_module_state(module_path, [], time.time() - start_time)
|
|
450
|
+
return UnittestResult(
|
|
451
|
+
module_path=module_path,
|
|
452
|
+
initial_state=initial_state,
|
|
453
|
+
current_state=current_state,
|
|
454
|
+
errors=errors,
|
|
455
|
+
warnings=warnings,
|
|
456
|
+
overall_improvement=False
|
|
457
|
+
)
|
|
458
|
+
except Exception as e:
|
|
459
|
+
error_msg = f"Error running test module {module_path}: {e}"
|
|
460
|
+
logger.error("Error running test module %s: %s", module_path, e)
|
|
461
|
+
errors.append(error_msg)
|
|
462
|
+
print(f"ERROR: {error_msg}")
|
|
463
|
+
|
|
464
|
+
# Create error state
|
|
465
|
+
current_state = self._create_module_state(module_path, [], time.time() - start_time)
|
|
466
|
+
return UnittestResult(
|
|
467
|
+
module_path=module_path,
|
|
468
|
+
initial_state=initial_state,
|
|
469
|
+
current_state=current_state,
|
|
470
|
+
errors=errors,
|
|
471
|
+
warnings=warnings,
|
|
472
|
+
overall_improvement=False
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
def _print_final_summary(self):
|
|
476
|
+
"""Print final summary of test run."""
|
|
477
|
+
if not self.results:
|
|
478
|
+
logger.warning("No test results to summarize")
|
|
479
|
+
print("No test results to summarize.")
|
|
480
|
+
return
|
|
481
|
+
|
|
482
|
+
total_modules = len(self.results)
|
|
483
|
+
modules_run = len([r for r in self.results if r.current_state.total_tests > 0])
|
|
484
|
+
modules_skipped = total_modules - modules_run
|
|
485
|
+
|
|
486
|
+
total_tests = sum(r.current_state.total_tests for r in self.results)
|
|
487
|
+
total_passed = sum(r.current_state.passed for r in self.results)
|
|
488
|
+
total_failed = sum(r.current_state.failed for r in self.results)
|
|
489
|
+
total_skipped = sum(r.current_state.skipped for r in self.results)
|
|
490
|
+
total_errors = sum(r.current_state.errors for r in self.results)
|
|
491
|
+
|
|
492
|
+
success_rate = (total_passed / total_tests * 100) if total_tests > 0 else 0.0
|
|
493
|
+
total_execution_time = sum(r.current_state.total_runtime for r in self.results)
|
|
494
|
+
|
|
495
|
+
logger.info("Final test summary: %d modules, %d tests, %d passed, %d failed, %d errors, %.1f%% success rate",
|
|
496
|
+
total_modules, total_tests, total_passed, total_failed, total_errors, success_rate)
|
|
497
|
+
|
|
498
|
+
print("=" * 80)
|
|
499
|
+
print("All tests completed!")
|
|
500
|
+
print(f"Results saved to JSON: {self.results_file}")
|
|
501
|
+
print()
|
|
502
|
+
print("FINAL SUMMARY:")
|
|
503
|
+
print(f"Total modules evaluated: {total_modules}")
|
|
504
|
+
print(f"Modules run: {modules_run}")
|
|
505
|
+
print(f"Modules skipped: {modules_skipped}")
|
|
506
|
+
print(f"Total tests: {total_tests}")
|
|
507
|
+
print(f"Passed: {total_passed}")
|
|
508
|
+
print(f"Failed: {total_failed}")
|
|
509
|
+
print(f"Errors: {total_errors}")
|
|
510
|
+
print(f"Success rate: {success_rate:.1f}%")
|
|
511
|
+
print(f"Results saved to {self.results_file}")
|
|
512
|
+
|
|
513
|
+
def save_results_json(self):
|
|
514
|
+
"""Save test results to JSON file."""
|
|
515
|
+
if not self.results:
|
|
516
|
+
logger.warning("No results to save")
|
|
517
|
+
print("No results to save.")
|
|
518
|
+
return
|
|
519
|
+
|
|
520
|
+
logger.info("Saving %d test results to JSON file: %s", len(self.results), self.results_file)
|
|
521
|
+
|
|
522
|
+
# Calculate summary statistics
|
|
523
|
+
total_modules = len(self.results)
|
|
524
|
+
modules_run = len([r for r in self.results if r.current_state.total_tests > 0])
|
|
525
|
+
modules_skipped = total_modules - modules_run
|
|
526
|
+
|
|
527
|
+
total_tests = sum(r.current_state.total_tests for r in self.results)
|
|
528
|
+
total_passed = sum(r.current_state.passed for r in self.results)
|
|
529
|
+
total_failed = sum(r.current_state.failed for r in self.results)
|
|
530
|
+
total_skipped = sum(r.current_state.skipped for r in self.results)
|
|
531
|
+
total_errors = sum(r.current_state.errors for r in self.results)
|
|
532
|
+
|
|
533
|
+
success_rate = (total_passed / total_tests * 100) if total_tests > 0 else 0.0
|
|
534
|
+
total_execution_time = sum(r.current_state.total_runtime for r in self.results)
|
|
535
|
+
|
|
536
|
+
# Create summary
|
|
537
|
+
summary = UnittestRunSummary(
|
|
538
|
+
timestamp=time.strftime('%Y-%m-%d %H:%M:%S'),
|
|
539
|
+
python_executable=self.python_path,
|
|
540
|
+
total_modules=total_modules,
|
|
541
|
+
modules_run=modules_run,
|
|
542
|
+
modules_skipped=modules_skipped,
|
|
543
|
+
total_tests=total_tests,
|
|
544
|
+
total_passed=total_passed,
|
|
545
|
+
total_failed=total_failed,
|
|
546
|
+
total_skipped=total_skipped,
|
|
547
|
+
total_errors=total_errors,
|
|
548
|
+
success_rate=success_rate,
|
|
549
|
+
total_execution_time=total_execution_time,
|
|
550
|
+
results=self.results
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
# Save to JSON
|
|
554
|
+
try:
|
|
555
|
+
with open(self.results_file, 'w') as f:
|
|
556
|
+
json.dump(summary.__dict__, f, indent=2, default=str)
|
|
557
|
+
logger.info("Successfully saved results to: %s", self.results_file)
|
|
558
|
+
print(f"Results saved to {self.results_file}")
|
|
559
|
+
except Exception as e:
|
|
560
|
+
logger.error("Error saving results to %s: %s", self.results_file, e)
|
|
561
|
+
print(f"Error saving results: {e}")
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data models for the test runner.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import List, Optional, Dict
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class UnittestFunctionState:
|
|
12
|
+
"""State of an individual test function."""
|
|
13
|
+
function_name: str
|
|
14
|
+
status: str # "passed", "failed", "skipped", "error", "unknown"
|
|
15
|
+
runtime: float
|
|
16
|
+
timestamp: str
|
|
17
|
+
error_message: str = ""
|
|
18
|
+
warning_count: int = 0
|
|
19
|
+
should_skip: bool = False
|
|
20
|
+
skip_reason: str = ""
|
|
21
|
+
improvement_from_initial: bool = False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ModuleState:
|
|
26
|
+
"""State of a test module containing multiple test functions."""
|
|
27
|
+
module_path: str
|
|
28
|
+
test_functions: List[UnittestFunctionState]
|
|
29
|
+
total_tests: int
|
|
30
|
+
passed: int
|
|
31
|
+
failed: int
|
|
32
|
+
skipped: int
|
|
33
|
+
errors: int
|
|
34
|
+
total_runtime: float
|
|
35
|
+
average_runtime: float
|
|
36
|
+
slowest_test: str
|
|
37
|
+
fastest_test: str
|
|
38
|
+
success_rate: float
|
|
39
|
+
timestamp: str
|
|
40
|
+
should_skip_module: bool = False
|
|
41
|
+
skip_reason: str = ""
|
|
42
|
+
improvement_from_initial: bool = False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class UnittestResult:
|
|
47
|
+
"""Result of running a test module with initial and current states."""
|
|
48
|
+
module_path: str
|
|
49
|
+
initial_state: Optional[ModuleState]
|
|
50
|
+
current_state: ModuleState
|
|
51
|
+
errors: List[str]
|
|
52
|
+
warnings: List[str]
|
|
53
|
+
overall_improvement: bool = False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class UnittestRunSummary:
|
|
58
|
+
"""Summary of a complete test run across all modules."""
|
|
59
|
+
timestamp: str
|
|
60
|
+
python_executable: str
|
|
61
|
+
total_modules: int
|
|
62
|
+
modules_run: int
|
|
63
|
+
modules_skipped: int
|
|
64
|
+
total_tests: int
|
|
65
|
+
total_passed: int
|
|
66
|
+
total_failed: int
|
|
67
|
+
total_skipped: int
|
|
68
|
+
total_errors: int
|
|
69
|
+
success_rate: float
|
|
70
|
+
total_execution_time: float
|
|
71
|
+
results: List[UnittestResult]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class UnittestDiscovery:
|
|
76
|
+
"""Represents discovered test structure for progress tracking."""
|
|
77
|
+
modules: List[str]
|
|
78
|
+
classes: Dict[str, List[str]] # module -> list of classes
|
|
79
|
+
functions: Dict[str, List[str]] # class -> list of functions
|
|
80
|
+
total_functions: int
|
|
81
|
+
total_classes: int
|
|
82
|
+
total_modules: int
|