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,322 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test discovery and structure analysis functionality.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import List, Dict, Optional, Tuple, Literal
|
|
10
|
+
from ..models import UnittestDiscovery
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
VerboseLevel = Literal["module", "file", "class", "function"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class UnittestDiscoveryService:
|
|
18
|
+
"""Handles discovery of test structure and target parsing."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, python_path: str, verbose: VerboseLevel = "class"):
|
|
21
|
+
logger.debug("Initializing UnittestDiscoveryService with python_path=%s, verbose=%s", python_path, verbose)
|
|
22
|
+
self._python_path = python_path
|
|
23
|
+
self._verbose = verbose
|
|
24
|
+
|
|
25
|
+
def discover_test_modules(self, tests_dir: str = "tests", target: Optional[str] = None) -> List[str]:
|
|
26
|
+
"""Discover all test modules based on target or default tests directory."""
|
|
27
|
+
logger.info("Discovering test modules with tests_dir=%s, target=%s", tests_dir, target)
|
|
28
|
+
|
|
29
|
+
if target:
|
|
30
|
+
# If specific target provided, look for tests relative to that target
|
|
31
|
+
logger.debug("Target specified, looking for modules matching: %s", target)
|
|
32
|
+
if "." in target:
|
|
33
|
+
# Target is a module path, look for tests in that module's directory
|
|
34
|
+
target_parts = target.split(".")
|
|
35
|
+
if target_parts[0] == "test_runner":
|
|
36
|
+
# Look in test_runner/tests directory
|
|
37
|
+
tests_path = Path("test_runner/tests")
|
|
38
|
+
else:
|
|
39
|
+
# Look in the main tests directory
|
|
40
|
+
tests_path = Path("tests")
|
|
41
|
+
else:
|
|
42
|
+
# Single module name, look in default tests directory
|
|
43
|
+
tests_path = Path("tests")
|
|
44
|
+
|
|
45
|
+
if tests_path.exists():
|
|
46
|
+
found_modules = []
|
|
47
|
+
logger.debug("Searching for test files in: %s", tests_path)
|
|
48
|
+
for py_file in tests_path.rglob("test_*.py"):
|
|
49
|
+
# Convert path to module format
|
|
50
|
+
relative_path = py_file.relative_to(tests_path.parent)
|
|
51
|
+
module_path = str(relative_path).replace(os.sep, ".").replace(".py", "")
|
|
52
|
+
# Check if this module matches our target
|
|
53
|
+
if module_path.startswith(target):
|
|
54
|
+
found_modules.append(module_path)
|
|
55
|
+
logger.debug("Found matching module: %s", module_path)
|
|
56
|
+
|
|
57
|
+
# If we're looking for a specific module.class target, find all test modules in that module
|
|
58
|
+
if "." in target and not found_modules:
|
|
59
|
+
# Split target like "test_runner.tests" into "test_runner" and "tests"
|
|
60
|
+
target_parts = target.split(".")
|
|
61
|
+
if len(target_parts) >= 2:
|
|
62
|
+
module_prefix = ".".join(target_parts[:-1]) # "test_runner" for "test_runner.tests"
|
|
63
|
+
logger.debug("No exact matches, trying module prefix: %s", module_prefix)
|
|
64
|
+
for py_file in tests_path.rglob("test_*.py"):
|
|
65
|
+
relative_path = py_file.relative_to(tests_path.parent)
|
|
66
|
+
module_path = str(relative_path).replace(os.sep, ".").replace(".py", "")
|
|
67
|
+
if module_path.startswith(module_prefix):
|
|
68
|
+
found_modules.append(module_path)
|
|
69
|
+
logger.debug("Found module with prefix match: %s", module_path)
|
|
70
|
+
|
|
71
|
+
if found_modules:
|
|
72
|
+
logger.info("Discovered %d test modules for target '%s'", len(found_modules), target)
|
|
73
|
+
if self._verbose in ["module", "file", "class", "function"]:
|
|
74
|
+
print(f" 📁 Discovered {len(found_modules)} test module(s) for target '{target}':")
|
|
75
|
+
for module in sorted(found_modules):
|
|
76
|
+
print(f" 📄 {module}")
|
|
77
|
+
return found_modules
|
|
78
|
+
|
|
79
|
+
# If no tests found relative to target, return the target itself
|
|
80
|
+
logger.warning("No test modules found for target '%s', using target as module", target)
|
|
81
|
+
if self._verbose in ["module", "file", "class", "function"]:
|
|
82
|
+
print(f" 📁 No test modules found for target '{target}', using target as module")
|
|
83
|
+
return [target]
|
|
84
|
+
|
|
85
|
+
# No target provided, look in the default tests directory
|
|
86
|
+
test_modules = []
|
|
87
|
+
tests_path = Path(tests_dir)
|
|
88
|
+
|
|
89
|
+
if not tests_path.exists():
|
|
90
|
+
logger.error("Tests directory not found: %s", tests_dir)
|
|
91
|
+
print(f"Tests directory not found: {tests_dir}")
|
|
92
|
+
return []
|
|
93
|
+
|
|
94
|
+
logger.debug("Searching for test files in default directory: %s", tests_path)
|
|
95
|
+
for py_file in tests_path.rglob("test_*.py"):
|
|
96
|
+
# Convert path to module format
|
|
97
|
+
relative_path = py_file.relative_to(tests_path.parent)
|
|
98
|
+
module_path = str(relative_path).replace(os.sep, ".").replace(".py", "")
|
|
99
|
+
test_modules.append(module_path)
|
|
100
|
+
logger.debug("Found test module: %s", module_path)
|
|
101
|
+
|
|
102
|
+
logger.info("Discovered %d test modules in '%s'", len(test_modules), tests_dir)
|
|
103
|
+
if self._verbose in ["module", "file", "class", "function"]:
|
|
104
|
+
print(f" 📁 Discovered {len(test_modules)} test module(s) in '{tests_dir}':")
|
|
105
|
+
for module in sorted(test_modules):
|
|
106
|
+
print(f" 📄 {module}")
|
|
107
|
+
|
|
108
|
+
return sorted(test_modules)
|
|
109
|
+
|
|
110
|
+
def discover_test_structure(self, test_modules: List[str]) -> UnittestDiscovery:
|
|
111
|
+
"""Discover the complete test structure for progress tracking."""
|
|
112
|
+
logger.info("Discovering test structure for %d modules", len(test_modules))
|
|
113
|
+
if self._verbose in ["module", "file", "class", "function"]:
|
|
114
|
+
print(f" 🔍 Discovering test structure for {len(test_modules)} module(s)...")
|
|
115
|
+
|
|
116
|
+
modules = []
|
|
117
|
+
classes = {} # module -> list of classes
|
|
118
|
+
functions = {} # class -> list of functions
|
|
119
|
+
total_functions = 0
|
|
120
|
+
total_classes = 0
|
|
121
|
+
|
|
122
|
+
for i, module_path in enumerate(test_modules):
|
|
123
|
+
modules.append(module_path)
|
|
124
|
+
|
|
125
|
+
logger.debug("Analyzing module %d/%d: %s", i+1, len(test_modules), module_path)
|
|
126
|
+
if self._verbose in ["class", "function"]:
|
|
127
|
+
print(f" 📦 Analyzing module {i+1}/{len(test_modules)}: {module_path}")
|
|
128
|
+
|
|
129
|
+
# Discover classes and functions in this module
|
|
130
|
+
try:
|
|
131
|
+
# Run unittest discovery to get test structure
|
|
132
|
+
cmd = [self._python_path, "-m", "unittest", "discover", "-s", "tests", "-p", f"{module_path.split('.')[-1]}.py", "--dry-run"]
|
|
133
|
+
logger.debug("Running discovery command: %s", " ".join(cmd))
|
|
134
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
135
|
+
|
|
136
|
+
if result.returncode == 0:
|
|
137
|
+
logger.debug("Discovery command successful for module: %s", module_path)
|
|
138
|
+
# Parse the discovery output to find classes and functions
|
|
139
|
+
module_classes = []
|
|
140
|
+
for line in result.stdout.split('\n'):
|
|
141
|
+
if 'test_' in line and '(' in line and ')' in line:
|
|
142
|
+
# Extract class name from line like "test_something (class_name)"
|
|
143
|
+
parts = line.split()
|
|
144
|
+
for part in parts:
|
|
145
|
+
if part.startswith('(') and part.endswith(')'):
|
|
146
|
+
class_name = part[1:-1]
|
|
147
|
+
if class_name not in module_classes:
|
|
148
|
+
module_classes.append(class_name)
|
|
149
|
+
|
|
150
|
+
# Count functions for this class
|
|
151
|
+
class_functions = []
|
|
152
|
+
for func_line in result.stdout.split('\n'):
|
|
153
|
+
if f'({class_name})' in func_line and 'test_' in func_line:
|
|
154
|
+
func_parts = func_line.split()
|
|
155
|
+
for func_part in func_parts:
|
|
156
|
+
if func_part.startswith('test_'):
|
|
157
|
+
class_functions.append(func_part)
|
|
158
|
+
break
|
|
159
|
+
|
|
160
|
+
functions[f"{module_path}.{class_name}"] = class_functions
|
|
161
|
+
total_functions += len(class_functions)
|
|
162
|
+
logger.debug("Found class %s with %d functions", class_name, len(class_functions))
|
|
163
|
+
|
|
164
|
+
classes[module_path] = module_classes
|
|
165
|
+
total_classes += len(module_classes)
|
|
166
|
+
|
|
167
|
+
if self._verbose == "function":
|
|
168
|
+
if module_classes:
|
|
169
|
+
print(f" 🏛️ Found {len(module_classes)} test class(es): {', '.join(module_classes)}")
|
|
170
|
+
for class_name in module_classes:
|
|
171
|
+
class_funcs = functions.get(f"{module_path}.{class_name}", [])
|
|
172
|
+
if class_funcs:
|
|
173
|
+
print(f" 🧪 {class_name}: {len(class_funcs)} function(s) - {', '.join(class_funcs[:3])}{'...' if len(class_funcs) > 3 else ''}")
|
|
174
|
+
else:
|
|
175
|
+
print(f" ⚠️ No test classes found")
|
|
176
|
+
else:
|
|
177
|
+
logger.warning("Discovery command failed for module %s (returncode: %d)", module_path, result.returncode)
|
|
178
|
+
# Fallback: assume one class per module
|
|
179
|
+
classes[module_path] = ["UnknownClass"]
|
|
180
|
+
functions[f"{module_path}.UnknownClass"] = []
|
|
181
|
+
|
|
182
|
+
except Exception as e:
|
|
183
|
+
logger.error("Error discovering structure for module %s: %s", module_path, e)
|
|
184
|
+
if self._verbose in ["class", "function"]:
|
|
185
|
+
print(f" ❌ Error discovering structure for {module_path}: {e}")
|
|
186
|
+
# Fallback: assume one class per module
|
|
187
|
+
classes[module_path] = ["UnknownClass"]
|
|
188
|
+
functions[f"{module_path}.UnknownClass"] = []
|
|
189
|
+
|
|
190
|
+
logger.info("Test structure discovery complete: %d modules, %d classes, %d functions",
|
|
191
|
+
len(modules), total_classes, total_functions)
|
|
192
|
+
if self._verbose in ["module", "file", "class", "function"]:
|
|
193
|
+
print(f" ✅ Discovery complete: {len(modules)} modules, {total_classes} classes, {total_functions} functions")
|
|
194
|
+
|
|
195
|
+
discovery = UnittestDiscovery(
|
|
196
|
+
modules=modules,
|
|
197
|
+
classes=classes,
|
|
198
|
+
functions=functions,
|
|
199
|
+
total_functions=total_functions,
|
|
200
|
+
total_classes=total_classes,
|
|
201
|
+
total_modules=len(modules)
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
if self._verbose == "function":
|
|
205
|
+
print(f"DEBUG: Discovered {len(modules)} modules, {total_classes} classes, {total_functions} functions")
|
|
206
|
+
|
|
207
|
+
return discovery
|
|
208
|
+
|
|
209
|
+
def parse_target(self, target: str) -> Tuple[Optional[str], Optional[str], Optional[str]]:
|
|
210
|
+
"""Parse unittest dot-notation target into module, class, function components."""
|
|
211
|
+
logger.debug("Parsing target: %s", target)
|
|
212
|
+
if not target:
|
|
213
|
+
return None, None, None
|
|
214
|
+
|
|
215
|
+
parts = target.split('.')
|
|
216
|
+
|
|
217
|
+
# Handle different cases:
|
|
218
|
+
# 1. "module" -> module=module, class=None, function=None
|
|
219
|
+
# 2. "module.class" -> module=module, class=class, function=None
|
|
220
|
+
# 3. "module.class.function" -> module=module, class=class, function=function
|
|
221
|
+
# 4. "module.class.function.too.much" -> module=module.class.function, class=too, function=much
|
|
222
|
+
|
|
223
|
+
if len(parts) == 1:
|
|
224
|
+
# Just module name
|
|
225
|
+
module = parts[0]
|
|
226
|
+
class_name = None
|
|
227
|
+
function_name = None
|
|
228
|
+
elif len(parts) == 2:
|
|
229
|
+
# Could be module.class or module.submodule
|
|
230
|
+
# Check if second part starts with uppercase (class) or lowercase (submodule)
|
|
231
|
+
if parts[1][0].isupper():
|
|
232
|
+
# Second part starts with uppercase, it's a class
|
|
233
|
+
module = parts[0]
|
|
234
|
+
class_name = parts[1]
|
|
235
|
+
function_name = None
|
|
236
|
+
else:
|
|
237
|
+
# Second part starts with lowercase, it's a submodule
|
|
238
|
+
module = '.'.join(parts)
|
|
239
|
+
class_name = None
|
|
240
|
+
function_name = None
|
|
241
|
+
elif len(parts) == 3:
|
|
242
|
+
# Module.submodule.class - join first two as module, third is class
|
|
243
|
+
module = '.'.join(parts[:2])
|
|
244
|
+
class_name = parts[2]
|
|
245
|
+
function_name = None
|
|
246
|
+
else:
|
|
247
|
+
# More than 3 parts - for invalid formats like "invalid.target.too.many.parts"
|
|
248
|
+
# Join first 2 parts as module, take 3rd as class, last part as function
|
|
249
|
+
module = '.'.join(parts[:2])
|
|
250
|
+
class_name = parts[2]
|
|
251
|
+
function_name = parts[-1]
|
|
252
|
+
|
|
253
|
+
logger.debug("Parsed target components - module: %s, class: %s, function: %s", module, class_name, function_name)
|
|
254
|
+
return module, class_name, function_name
|
|
255
|
+
|
|
256
|
+
def create_focused_discovery(self, target_module: str, target_class: Optional[str],
|
|
257
|
+
target_function: Optional[str], full_discovery: UnittestDiscovery) -> UnittestDiscovery:
|
|
258
|
+
"""Create a focused test discovery based on target specification."""
|
|
259
|
+
logger.debug("Creating focused discovery for target: %s.%s.%s", target_module, target_class or '', target_function or '')
|
|
260
|
+
if self._verbose == "function":
|
|
261
|
+
print(f"DEBUG: Creating focused discovery for target: {target_module}.{target_class or ''}.{target_function or ''}")
|
|
262
|
+
|
|
263
|
+
# Find the target module
|
|
264
|
+
target_modules = [m for m in full_discovery.modules if m == target_module or m.endswith(f".{target_module}")]
|
|
265
|
+
if not target_modules:
|
|
266
|
+
logger.error("Target module '%s' not found in discovered modules", target_module)
|
|
267
|
+
raise ValueError(f"Target module '{target_module}' not found in discovered modules")
|
|
268
|
+
|
|
269
|
+
module = target_modules[0]
|
|
270
|
+
modules = [module]
|
|
271
|
+
classes = {}
|
|
272
|
+
functions = {}
|
|
273
|
+
|
|
274
|
+
# Get classes for this module
|
|
275
|
+
module_classes = full_discovery.classes.get(module, [])
|
|
276
|
+
logger.debug("Found %d classes in target module %s", len(module_classes), module)
|
|
277
|
+
|
|
278
|
+
if target_class:
|
|
279
|
+
# Filter to specific class
|
|
280
|
+
if target_class in module_classes:
|
|
281
|
+
logger.debug("Filtering to specific class: %s", target_class)
|
|
282
|
+
classes[module] = [target_class]
|
|
283
|
+
class_key = f"{module}.{target_class}"
|
|
284
|
+
class_functions = full_discovery.functions.get(class_key, [])
|
|
285
|
+
|
|
286
|
+
if target_function:
|
|
287
|
+
# Filter to specific function
|
|
288
|
+
if target_function in class_functions:
|
|
289
|
+
logger.debug("Filtering to specific function: %s", target_function)
|
|
290
|
+
functions[class_key] = [target_function]
|
|
291
|
+
else:
|
|
292
|
+
logger.warning("Target function '%s' not found in class '%s'", target_function, target_class)
|
|
293
|
+
functions[class_key] = []
|
|
294
|
+
else:
|
|
295
|
+
functions[class_key] = class_functions
|
|
296
|
+
logger.debug("Including all %d functions in class %s", len(class_functions), target_class)
|
|
297
|
+
else:
|
|
298
|
+
logger.warning("Target class '%s' not found in module '%s'", target_class, module)
|
|
299
|
+
classes[module] = []
|
|
300
|
+
functions[f"{module}.{target_class}"] = []
|
|
301
|
+
else:
|
|
302
|
+
# Include all classes in module
|
|
303
|
+
logger.debug("Including all %d classes in module %s", len(module_classes), module)
|
|
304
|
+
classes[module] = module_classes
|
|
305
|
+
for class_name in module_classes:
|
|
306
|
+
class_key = f"{module}.{class_name}"
|
|
307
|
+
functions[class_key] = full_discovery.functions.get(class_key, [])
|
|
308
|
+
|
|
309
|
+
total_functions = sum(len(funcs) for funcs in functions.values())
|
|
310
|
+
total_classes = sum(len(cls) for cls in classes.values())
|
|
311
|
+
|
|
312
|
+
logger.info("Created focused discovery: %d modules, %d classes, %d functions",
|
|
313
|
+
len(modules), total_classes, total_functions)
|
|
314
|
+
|
|
315
|
+
return UnittestDiscovery(
|
|
316
|
+
modules=modules,
|
|
317
|
+
classes=classes,
|
|
318
|
+
functions=functions,
|
|
319
|
+
total_functions=total_functions,
|
|
320
|
+
total_classes=total_classes,
|
|
321
|
+
total_modules=len(modules)
|
|
322
|
+
)
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test execution hierarchy functionality.
|
|
3
|
+
Handles the core execution context with _run_test_function as the base.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
import subprocess
|
|
9
|
+
from typing import List, Optional, Literal
|
|
10
|
+
from ..models import UnittestFunctionState, UnittestResult, ModuleState
|
|
11
|
+
from .parser import UnittestOutputParser
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
VerboseLevel = Literal["module", "file", "class", "function"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class UnittestExecutor:
|
|
19
|
+
"""Handles hierarchical test execution with _run_test_function as the core."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, python_path: str, verbose: VerboseLevel = "class",
|
|
22
|
+
test_discovery: Optional[object] = None):
|
|
23
|
+
logger.debug("Initializing UnittestExecutor with python_path=%s, verbose=%s", python_path, verbose)
|
|
24
|
+
self._python_path = python_path
|
|
25
|
+
self._verbose = verbose
|
|
26
|
+
self._test_discovery = test_discovery
|
|
27
|
+
self._parser = UnittestOutputParser(verbose)
|
|
28
|
+
|
|
29
|
+
def run_test_function(self, module_path: str, test_class: str, test_function: str,
|
|
30
|
+
function_index: int = 0, total_functions: int = 0) -> UnittestFunctionState:
|
|
31
|
+
"""Run a single test function and return its state. This is the core execution context."""
|
|
32
|
+
logger.debug("Running test function: %s.%s.%s", module_path, test_class, test_function)
|
|
33
|
+
indent = " " # 8 spaces for function level
|
|
34
|
+
|
|
35
|
+
if self._verbose == "function":
|
|
36
|
+
progress = f"[{function_index + 1}/{total_functions}]" if total_functions > 0 else ""
|
|
37
|
+
print(f"{indent}{progress} Running test function: {test_class}.{test_function}")
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
# Run specific test function: python -m unittest module.class.function
|
|
41
|
+
cmd = [self._python_path, "-m", "unittest", f"{module_path}.{test_class}.{test_function}", "-v"]
|
|
42
|
+
logger.debug("Executing command: %s", " ".join(cmd))
|
|
43
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
44
|
+
|
|
45
|
+
# Parse the output
|
|
46
|
+
output_lines = result.stdout.split('\n')
|
|
47
|
+
stderr_lines = result.stderr.split('\n')
|
|
48
|
+
|
|
49
|
+
# If stdout is empty but stderr has test output, use stderr
|
|
50
|
+
if not any("test_" in line for line in output_lines) and any("test_" in line for line in stderr_lines):
|
|
51
|
+
logger.debug("Using stderr output for test function: %s", test_function)
|
|
52
|
+
output_lines = stderr_lines
|
|
53
|
+
stderr_lines = []
|
|
54
|
+
|
|
55
|
+
# Parse test function result
|
|
56
|
+
test_functions, _, _ = self._parser.parse_test_output(output_lines, stderr_lines)
|
|
57
|
+
|
|
58
|
+
if test_functions:
|
|
59
|
+
test_state = test_functions[0]
|
|
60
|
+
logger.debug("Test function %s completed with status: %s (runtime: %.3fs)",
|
|
61
|
+
test_function, test_state.status, test_state.runtime)
|
|
62
|
+
if self._verbose == "function":
|
|
63
|
+
status_symbol = "✓" if test_state.status == "passed" else "✗" if test_state.status == "failed" else "⚠" if test_state.status == "error" else "⏭" if test_state.status == "skipped" else "?"
|
|
64
|
+
print(f"{indent} {status_symbol} {test_function} ({test_state.status}) - {test_state.runtime:.3f}s")
|
|
65
|
+
return test_state
|
|
66
|
+
else:
|
|
67
|
+
# Create a failed state if parsing failed
|
|
68
|
+
logger.warning("Failed to parse test output for function: %s", test_function)
|
|
69
|
+
error_state = UnittestFunctionState(
|
|
70
|
+
function_name=test_function,
|
|
71
|
+
status="error",
|
|
72
|
+
runtime=0.0,
|
|
73
|
+
timestamp=time.strftime('%Y-%m-%d %H:%M:%S'),
|
|
74
|
+
error_message="Failed to parse test output",
|
|
75
|
+
warning_count=0,
|
|
76
|
+
should_skip=False,
|
|
77
|
+
skip_reason=""
|
|
78
|
+
)
|
|
79
|
+
if self._verbose == "function":
|
|
80
|
+
print(f"{indent} ⚠ {test_function} (error) - Failed to parse output")
|
|
81
|
+
return error_state
|
|
82
|
+
|
|
83
|
+
except Exception as e:
|
|
84
|
+
logger.error("Error running test function %s: %s", test_function, e)
|
|
85
|
+
error_state = UnittestFunctionState(
|
|
86
|
+
function_name=test_function,
|
|
87
|
+
status="error",
|
|
88
|
+
runtime=0.0,
|
|
89
|
+
timestamp=time.strftime('%Y-%m-%d %H:%M:%S'),
|
|
90
|
+
error_message=f"Error running test: {e}",
|
|
91
|
+
warning_count=0,
|
|
92
|
+
should_skip=False,
|
|
93
|
+
skip_reason=""
|
|
94
|
+
)
|
|
95
|
+
if self._verbose == "function":
|
|
96
|
+
print(f"{indent} ⚠ {test_function} (error) - {e}")
|
|
97
|
+
return error_state
|
|
98
|
+
|
|
99
|
+
def run_test_class(self, module_path: str, test_class: str, class_index: int = 0, total_classes: int = 0) -> List[UnittestFunctionState]:
|
|
100
|
+
"""Run all test functions in a test class and return their states."""
|
|
101
|
+
logger.debug("Running test class: %s.%s", module_path, test_class)
|
|
102
|
+
indent = " " # 6 spaces for class level
|
|
103
|
+
|
|
104
|
+
if self._verbose in ["class", "function"]:
|
|
105
|
+
progress = f"[{class_index + 1}/{total_classes}]" if total_classes > 0 else ""
|
|
106
|
+
print(f"{indent}{progress} Running test class: {test_class}")
|
|
107
|
+
|
|
108
|
+
# Get functions for this class from discovery
|
|
109
|
+
class_key = f"{module_path}.{test_class}"
|
|
110
|
+
class_functions = self._test_discovery.functions.get(class_key, []) if self._test_discovery else []
|
|
111
|
+
logger.debug("Found %d functions in class %s", len(class_functions), test_class)
|
|
112
|
+
|
|
113
|
+
if not class_functions:
|
|
114
|
+
# Fallback: run the class as a whole
|
|
115
|
+
logger.debug("No discovery info for class %s, running as whole", test_class)
|
|
116
|
+
try:
|
|
117
|
+
cmd = [self._python_path, "-m", "unittest", f"{module_path}.{test_class}", "-v"]
|
|
118
|
+
logger.debug("Executing class command: %s", " ".join(cmd))
|
|
119
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
120
|
+
|
|
121
|
+
output_lines = result.stdout.split('\n')
|
|
122
|
+
stderr_lines = result.stderr.split('\n')
|
|
123
|
+
|
|
124
|
+
if not any("test_" in line for line in output_lines) and any("test_" in line for line in stderr_lines):
|
|
125
|
+
logger.debug("Using stderr output for class: %s", test_class)
|
|
126
|
+
output_lines = stderr_lines
|
|
127
|
+
stderr_lines = []
|
|
128
|
+
|
|
129
|
+
test_functions, _, _ = self._parser.parse_test_output(output_lines, stderr_lines)
|
|
130
|
+
logger.debug("Class %s completed with %d test functions", test_class, len(test_functions))
|
|
131
|
+
|
|
132
|
+
if self._verbose == "function" and test_functions:
|
|
133
|
+
for test_func in test_functions:
|
|
134
|
+
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 "?"
|
|
135
|
+
print(f" {status_symbol} {test_func.function_name} ({test_func.status}) - {test_func.runtime:.3f}s")
|
|
136
|
+
|
|
137
|
+
return test_functions
|
|
138
|
+
|
|
139
|
+
except Exception as e:
|
|
140
|
+
logger.error("Failed to run test class %s: %s", test_class, e)
|
|
141
|
+
if self._verbose in ["class", "function"]:
|
|
142
|
+
print(f" ERROR: Failed to run test class {test_class}: {e}")
|
|
143
|
+
return []
|
|
144
|
+
else:
|
|
145
|
+
# Run each function individually using run_test_function
|
|
146
|
+
logger.debug("Running %d individual functions in class %s", len(class_functions), test_class)
|
|
147
|
+
test_functions = []
|
|
148
|
+
for i, function_name in enumerate(class_functions):
|
|
149
|
+
test_state = self.run_test_function(module_path, test_class, function_name, i, len(class_functions))
|
|
150
|
+
test_functions.append(test_state)
|
|
151
|
+
|
|
152
|
+
# Show class summary
|
|
153
|
+
passed = sum(1 for t in test_functions if t.status == "passed")
|
|
154
|
+
failed = sum(1 for t in test_functions if t.status == "failed")
|
|
155
|
+
errors = sum(1 for t in test_functions if t.status == "error")
|
|
156
|
+
skipped = sum(1 for t in test_functions if t.status == "skipped")
|
|
157
|
+
logger.debug("Class %s summary: %d passed, %d failed, %d errors, %d skipped",
|
|
158
|
+
test_class, passed, failed, errors, skipped)
|
|
159
|
+
|
|
160
|
+
if self._verbose in ["class", "function"]:
|
|
161
|
+
print(f"{indent} {test_class}: {passed} passed, {failed} failed, {errors} errors, {skipped} skipped")
|
|
162
|
+
|
|
163
|
+
return test_functions
|
|
164
|
+
|
|
165
|
+
def run_test_file(self, module_path: str, file_index: int = 0, total_files: int = 0) -> List[UnittestFunctionState]:
|
|
166
|
+
"""Run all test classes in a test file and return all test function states."""
|
|
167
|
+
logger.debug("Running test file: %s", module_path)
|
|
168
|
+
indent = " " # 4 spaces for file level
|
|
169
|
+
|
|
170
|
+
if self._verbose in ["file", "class", "function"]:
|
|
171
|
+
progress = f"[{file_index + 1}/{total_files}]" if total_files > 0 else ""
|
|
172
|
+
print(f"{indent}{progress} Running test file: {module_path}")
|
|
173
|
+
|
|
174
|
+
# Get classes for this module from discovery
|
|
175
|
+
module_classes = self._test_discovery.classes.get(module_path, []) if self._test_discovery else []
|
|
176
|
+
logger.debug("Found %d classes in module %s", len(module_classes), module_path)
|
|
177
|
+
|
|
178
|
+
if not module_classes:
|
|
179
|
+
# Fallback: run the module as a whole
|
|
180
|
+
logger.debug("No discovery info for module %s, running as whole", module_path)
|
|
181
|
+
try:
|
|
182
|
+
cmd = [self._python_path, "-m", "unittest", module_path, "-v"]
|
|
183
|
+
logger.debug("Executing module command: %s", " ".join(cmd))
|
|
184
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
185
|
+
|
|
186
|
+
output_lines = result.stdout.split('\n')
|
|
187
|
+
stderr_lines = result.stderr.split('\n')
|
|
188
|
+
|
|
189
|
+
if not any("test_" in line for line in output_lines) and any("test_" in line for line in stderr_lines):
|
|
190
|
+
logger.debug("Using stderr output for module: %s", module_path)
|
|
191
|
+
output_lines = stderr_lines
|
|
192
|
+
stderr_lines = []
|
|
193
|
+
|
|
194
|
+
test_functions, _, _ = self._parser.parse_test_output(output_lines, stderr_lines)
|
|
195
|
+
logger.debug("Module %s completed with %d test functions", module_path, len(test_functions))
|
|
196
|
+
|
|
197
|
+
if self._verbose in ["class", "function"] and test_functions:
|
|
198
|
+
# Group by test class
|
|
199
|
+
classes = {}
|
|
200
|
+
for test_func in test_functions:
|
|
201
|
+
class_name = "UnknownClass" # We'll improve this later
|
|
202
|
+
if class_name not in classes:
|
|
203
|
+
classes[class_name] = []
|
|
204
|
+
classes[class_name].append(test_func)
|
|
205
|
+
|
|
206
|
+
for class_name, class_tests in classes.items():
|
|
207
|
+
if self._verbose == "class":
|
|
208
|
+
passed = sum(1 for t in class_tests if t.status == "passed")
|
|
209
|
+
failed = sum(1 for t in class_tests if t.status == "failed")
|
|
210
|
+
errors = sum(1 for t in class_tests if t.status == "error")
|
|
211
|
+
print(f" {class_name}: {passed} passed, {failed} failed, {errors} errors")
|
|
212
|
+
elif self._verbose == "function":
|
|
213
|
+
print(f" {class_name}:")
|
|
214
|
+
for test_func in class_tests:
|
|
215
|
+
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 "?"
|
|
216
|
+
print(f" {status_symbol} {test_func.function_name} ({test_func.status}) - {test_func.runtime:.3f}s")
|
|
217
|
+
|
|
218
|
+
return test_functions
|
|
219
|
+
|
|
220
|
+
except Exception as e:
|
|
221
|
+
logger.error("Failed to run test file %s: %s", module_path, e)
|
|
222
|
+
if self._verbose in ["file", "class", "function"]:
|
|
223
|
+
print(f" ERROR: Failed to run test file {module_path}: {e}")
|
|
224
|
+
return []
|
|
225
|
+
else:
|
|
226
|
+
# Run each class individually using run_test_class
|
|
227
|
+
logger.debug("Running %d individual classes in module %s", len(module_classes), module_path)
|
|
228
|
+
all_test_functions = []
|
|
229
|
+
for i, class_name in enumerate(module_classes):
|
|
230
|
+
class_functions = self.run_test_class(module_path, class_name, i, len(module_classes))
|
|
231
|
+
all_test_functions.extend(class_functions)
|
|
232
|
+
|
|
233
|
+
# Show file summary
|
|
234
|
+
passed = sum(1 for t in all_test_functions if t.status == "passed")
|
|
235
|
+
failed = sum(1 for t in all_test_functions if t.status == "failed")
|
|
236
|
+
errors = sum(1 for t in all_test_functions if t.status == "error")
|
|
237
|
+
skipped = sum(1 for t in all_test_functions if t.status == "skipped")
|
|
238
|
+
logger.debug("Module %s summary: %d passed, %d failed, %d errors, %d skipped",
|
|
239
|
+
module_path, passed, failed, errors, skipped)
|
|
240
|
+
|
|
241
|
+
if self._verbose in ["file", "class", "function"]:
|
|
242
|
+
print(f"{indent} {module_path}: {passed} passed, {failed} failed, {errors} errors, {skipped} skipped")
|
|
243
|
+
|
|
244
|
+
return all_test_functions
|