symtest-cli 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- symtest/__init__.py +45 -0
- symtest/cli.py +549 -0
- symtest/commands/__init__.py +9 -0
- symtest/commands/compare.py +221 -0
- symtest/config/__init__.py +7 -0
- symtest/config/config_io.py +346 -0
- symtest/config/config_schema.py +330 -0
- symtest/config/import_expander.py +149 -0
- symtest/config/inheritance_expander.py +197 -0
- symtest/core/__init__.py +15 -0
- symtest/core/assertions.py +253 -0
- symtest/core/base_runner.py +299 -0
- symtest/core/config_loader.py +536 -0
- symtest/core/execution.py +498 -0
- symtest/core/history_store.py +96 -0
- symtest/core/last_run_store.py +109 -0
- symtest/core/parallel_runner.py +251 -0
- symtest/core/process_worker.py +93 -0
- symtest/core/sequence_state.py +143 -0
- symtest/core/setup.py +137 -0
- symtest/core/test_case.py +76 -0
- symtest/core/types.py +92 -0
- symtest/file_comparator/__init__.py +10 -0
- symtest/file_comparator/base_comparator.py +109 -0
- symtest/file_comparator/binary_comparator.py +399 -0
- symtest/file_comparator/csv_comparator.py +241 -0
- symtest/file_comparator/factory.py +191 -0
- symtest/file_comparator/h5_comparator.py +777 -0
- symtest/file_comparator/json_comparator.py +323 -0
- symtest/file_comparator/result.py +213 -0
- symtest/file_comparator/script_comparator.py +182 -0
- symtest/file_comparator/text_comparator.py +182 -0
- symtest/file_comparator/xml_comparator.py +150 -0
- symtest/logging_config.py +66 -0
- symtest/runners/__init__.py +15 -0
- symtest/runners/config_runner.py +96 -0
- symtest/runners/json_runner.py +21 -0
- symtest/runners/parallel_config_runner.py +278 -0
- symtest/runners/parallel_json_runner.py +26 -0
- symtest/runners/parallel_yaml_runner.py +31 -0
- symtest/runners/yaml_runner.py +26 -0
- symtest/tui/__init__.py +11 -0
- symtest/tui/app.py +90 -0
- symtest/tui/controllers/__init__.py +0 -0
- symtest/tui/controllers/case_controller.py +322 -0
- symtest/tui/screens/__init__.py +0 -0
- symtest/tui/screens/case_editor.py +244 -0
- symtest/tui/screens/case_list.py +255 -0
- symtest/tui/widgets/__init__.py +0 -0
- symtest/tui/widgets/case_table.py +113 -0
- symtest/tui/widgets/expected_editor.py +159 -0
- symtest/tui/widgets/search_bar.py +160 -0
- symtest/tui/widgets/steps_editor.py +243 -0
- symtest/utils/__init__.py +21 -0
- symtest/utils/junit_xml_writer.py +137 -0
- symtest/utils/path_resolver.py +124 -0
- symtest/utils/report_generator.py +208 -0
- symtest_cli-1.3.0.dist-info/METADATA +316 -0
- symtest_cli-1.3.0.dist-info/RECORD +63 -0
- symtest_cli-1.3.0.dist-info/WHEEL +5 -0
- symtest_cli-1.3.0.dist-info/entry_points.txt +4 -0
- symtest_cli-1.3.0.dist-info/licenses/LICENSE +21 -0
- symtest_cli-1.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
@file factory.py
|
|
6
|
+
@brief Factory class for creating file comparators based on file type
|
|
7
|
+
@author Xiaotong Wang
|
|
8
|
+
@date 2025
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import importlib
|
|
12
|
+
import importlib.util
|
|
13
|
+
import os
|
|
14
|
+
import pkgutil
|
|
15
|
+
import logging
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("symtest.file_comparator.factory")
|
|
19
|
+
|
|
20
|
+
_ENV_VAR = "CLITEST_PLUGIN_DIRS"
|
|
21
|
+
|
|
22
|
+
class ComparatorFactory:
|
|
23
|
+
"""
|
|
24
|
+
@brief Factory class for creating file comparators
|
|
25
|
+
@details This class manages the creation and registration of different types of file comparators.
|
|
26
|
+
It provides a centralized way to create appropriate comparators based on file type
|
|
27
|
+
and automatically discovers and registers comparator classes via plugin scanning.
|
|
28
|
+
"""
|
|
29
|
+
_comparators = {}
|
|
30
|
+
_initialized = False
|
|
31
|
+
_plugin_dirs = []
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def register_comparator(file_type, comparator_class):
|
|
35
|
+
"""
|
|
36
|
+
@brief Register a new comparator class for a specific file type
|
|
37
|
+
@param file_type str: Type of file the comparator handles
|
|
38
|
+
@param comparator_class class: Comparator class to register
|
|
39
|
+
"""
|
|
40
|
+
ComparatorFactory._comparators[file_type.lower()] = comparator_class
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def create_comparator(file_type, **kwargs):
|
|
44
|
+
"""
|
|
45
|
+
@brief Create a comparator instance for the specified file type
|
|
46
|
+
@param file_type str: Type of file to compare
|
|
47
|
+
@param **kwargs: Additional arguments to pass to the comparator
|
|
48
|
+
@return BaseComparator: An instance of the appropriate comparator class
|
|
49
|
+
@details Creates and returns a comparator instance based on the file type.
|
|
50
|
+
If no specific comparator is found, falls back to TextComparator
|
|
51
|
+
for text files or BinaryComparator for other types.
|
|
52
|
+
"""
|
|
53
|
+
if not ComparatorFactory._initialized:
|
|
54
|
+
ComparatorFactory._load_comparators()
|
|
55
|
+
|
|
56
|
+
comparator_class = ComparatorFactory._comparators.get(file_type.lower())
|
|
57
|
+
if not comparator_class:
|
|
58
|
+
if file_type.lower() in ['auto', 'text']:
|
|
59
|
+
from .text_comparator import TextComparator
|
|
60
|
+
return TextComparator(**kwargs)
|
|
61
|
+
else:
|
|
62
|
+
from .binary_comparator import BinaryComparator
|
|
63
|
+
return BinaryComparator(**kwargs)
|
|
64
|
+
|
|
65
|
+
return comparator_class(**kwargs)
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def set_plugin_dirs(dirs):
|
|
69
|
+
"""Persist workspace-level plugin directories and expose them via env var.
|
|
70
|
+
|
|
71
|
+
Thread-pool runners share ``_plugin_dirs`` in-process. Process-pool
|
|
72
|
+
runners (``spawn``) pick up the plugin paths from ``CLITEST_PLUGIN_DIRS``
|
|
73
|
+
so that the lazy ``_load_comparators()`` in each worker discovers the
|
|
74
|
+
same workspace plugins.
|
|
75
|
+
|
|
76
|
+
:param dirs: Iterable of absolute or relative directory paths.
|
|
77
|
+
"""
|
|
78
|
+
dirs = list(dirs) if dirs else []
|
|
79
|
+
deduped = []
|
|
80
|
+
seen = set()
|
|
81
|
+
for d in dirs:
|
|
82
|
+
resolved = str(Path(d).resolve())
|
|
83
|
+
if resolved not in seen:
|
|
84
|
+
deduped.append(resolved)
|
|
85
|
+
seen.add(resolved)
|
|
86
|
+
ComparatorFactory._plugin_dirs = deduped
|
|
87
|
+
os.environ[_ENV_VAR] = os.pathsep.join(deduped)
|
|
88
|
+
if ComparatorFactory._initialized:
|
|
89
|
+
ComparatorFactory._load_from_dirs(deduped)
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def _load_comparators():
|
|
93
|
+
"""
|
|
94
|
+
@brief Load and register all available comparators
|
|
95
|
+
@details Automatically discovers and registers comparator classes from the package.
|
|
96
|
+
This includes both built-in comparators and any additional comparators
|
|
97
|
+
that follow the naming convention '*_comparator.py'.
|
|
98
|
+
"""
|
|
99
|
+
package_dir = Path(__file__).parent
|
|
100
|
+
for module_info in pkgutil.iter_modules([str(package_dir)]):
|
|
101
|
+
if module_info.name.endswith('_comparator') and module_info.name != 'base_comparator':
|
|
102
|
+
try:
|
|
103
|
+
module = importlib.import_module(f".{module_info.name}", package=__package__)
|
|
104
|
+
|
|
105
|
+
for attr_name in dir(module):
|
|
106
|
+
attr = getattr(module, attr_name)
|
|
107
|
+
if (isinstance(attr, type) and
|
|
108
|
+
attr.__module__ == module.__name__ and
|
|
109
|
+
attr_name.endswith('Comparator')):
|
|
110
|
+
type_name = attr_name.lower().replace('comparator', '')
|
|
111
|
+
ComparatorFactory.register_comparator(type_name, attr)
|
|
112
|
+
except ImportError as e:
|
|
113
|
+
logger.warning("Failed to import comparator module %s: %s", module_info.name, e)
|
|
114
|
+
|
|
115
|
+
# --- workspace & env-var plugin dirs ---
|
|
116
|
+
extra_dirs = list(ComparatorFactory._plugin_dirs)
|
|
117
|
+
env_val = os.environ.get(_ENV_VAR, "")
|
|
118
|
+
if env_val:
|
|
119
|
+
for p in env_val.split(os.pathsep):
|
|
120
|
+
p = p.strip()
|
|
121
|
+
if p and p not in extra_dirs:
|
|
122
|
+
extra_dirs.append(p)
|
|
123
|
+
if extra_dirs:
|
|
124
|
+
ComparatorFactory._load_from_dirs(extra_dirs)
|
|
125
|
+
|
|
126
|
+
ComparatorFactory._initialized = True
|
|
127
|
+
|
|
128
|
+
@staticmethod
|
|
129
|
+
def _load_from_dirs(dirs):
|
|
130
|
+
"""Scan *directory* paths for ``*_comparator.py`` modules and auto-register them.
|
|
131
|
+
|
|
132
|
+
Uses ``importlib.util.spec_from_file_location`` so that files outside the
|
|
133
|
+
framework package tree can be loaded.
|
|
134
|
+
"""
|
|
135
|
+
for dir_path in dirs:
|
|
136
|
+
d = Path(dir_path)
|
|
137
|
+
if not d.is_dir():
|
|
138
|
+
if os.path.isabs(dir_path):
|
|
139
|
+
logger.debug("Plugin dir not found, skipped: %s", dir_path)
|
|
140
|
+
continue
|
|
141
|
+
for py_file in sorted(d.glob("*_comparator.py")):
|
|
142
|
+
mod_name = py_file.stem
|
|
143
|
+
# Skip base_comparator
|
|
144
|
+
if mod_name == "base_comparator":
|
|
145
|
+
continue
|
|
146
|
+
try:
|
|
147
|
+
spec = importlib.util.spec_from_file_location(
|
|
148
|
+
f"symtest.plugins.{mod_name}",
|
|
149
|
+
str(py_file),
|
|
150
|
+
)
|
|
151
|
+
if spec is None or spec.loader is None:
|
|
152
|
+
logger.warning("Cannot load plugin spec for %s", py_file)
|
|
153
|
+
continue
|
|
154
|
+
module = importlib.util.module_from_spec(spec)
|
|
155
|
+
spec.loader.exec_module(module)
|
|
156
|
+
|
|
157
|
+
for attr_name in dir(module):
|
|
158
|
+
attr = getattr(module, attr_name)
|
|
159
|
+
if (isinstance(attr, type)
|
|
160
|
+
and attr.__module__ == module.__name__
|
|
161
|
+
and attr_name.endswith("Comparator")):
|
|
162
|
+
type_name = attr_name.lower().replace("comparator", "")
|
|
163
|
+
ComparatorFactory.register_comparator(type_name, attr)
|
|
164
|
+
logger.info(
|
|
165
|
+
"Registered workspace plugin '%s' -> %s from %s",
|
|
166
|
+
type_name, attr_name, py_file,
|
|
167
|
+
)
|
|
168
|
+
except Exception as e:
|
|
169
|
+
logger.warning(
|
|
170
|
+
"Failed to load workspace plugin %s: %s", py_file, e,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def get_available_comparators():
|
|
175
|
+
"""
|
|
176
|
+
@brief Get a list of all registered comparator types
|
|
177
|
+
@return list: List of available comparator type names
|
|
178
|
+
"""
|
|
179
|
+
if not ComparatorFactory._initialized:
|
|
180
|
+
ComparatorFactory._load_comparators()
|
|
181
|
+
return sorted(ComparatorFactory._comparators.keys())
|
|
182
|
+
|
|
183
|
+
@staticmethod
|
|
184
|
+
def reset():
|
|
185
|
+
"""Reset all internal state (for testing)."""
|
|
186
|
+
ComparatorFactory._comparators = {}
|
|
187
|
+
ComparatorFactory._initialized = False
|
|
188
|
+
ComparatorFactory._plugin_dirs = []
|
|
189
|
+
if _ENV_VAR in os.environ:
|
|
190
|
+
del os.environ[_ENV_VAR]
|
|
191
|
+
|