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.
Files changed (63) hide show
  1. symtest/__init__.py +45 -0
  2. symtest/cli.py +549 -0
  3. symtest/commands/__init__.py +9 -0
  4. symtest/commands/compare.py +221 -0
  5. symtest/config/__init__.py +7 -0
  6. symtest/config/config_io.py +346 -0
  7. symtest/config/config_schema.py +330 -0
  8. symtest/config/import_expander.py +149 -0
  9. symtest/config/inheritance_expander.py +197 -0
  10. symtest/core/__init__.py +15 -0
  11. symtest/core/assertions.py +253 -0
  12. symtest/core/base_runner.py +299 -0
  13. symtest/core/config_loader.py +536 -0
  14. symtest/core/execution.py +498 -0
  15. symtest/core/history_store.py +96 -0
  16. symtest/core/last_run_store.py +109 -0
  17. symtest/core/parallel_runner.py +251 -0
  18. symtest/core/process_worker.py +93 -0
  19. symtest/core/sequence_state.py +143 -0
  20. symtest/core/setup.py +137 -0
  21. symtest/core/test_case.py +76 -0
  22. symtest/core/types.py +92 -0
  23. symtest/file_comparator/__init__.py +10 -0
  24. symtest/file_comparator/base_comparator.py +109 -0
  25. symtest/file_comparator/binary_comparator.py +399 -0
  26. symtest/file_comparator/csv_comparator.py +241 -0
  27. symtest/file_comparator/factory.py +191 -0
  28. symtest/file_comparator/h5_comparator.py +777 -0
  29. symtest/file_comparator/json_comparator.py +323 -0
  30. symtest/file_comparator/result.py +213 -0
  31. symtest/file_comparator/script_comparator.py +182 -0
  32. symtest/file_comparator/text_comparator.py +182 -0
  33. symtest/file_comparator/xml_comparator.py +150 -0
  34. symtest/logging_config.py +66 -0
  35. symtest/runners/__init__.py +15 -0
  36. symtest/runners/config_runner.py +96 -0
  37. symtest/runners/json_runner.py +21 -0
  38. symtest/runners/parallel_config_runner.py +278 -0
  39. symtest/runners/parallel_json_runner.py +26 -0
  40. symtest/runners/parallel_yaml_runner.py +31 -0
  41. symtest/runners/yaml_runner.py +26 -0
  42. symtest/tui/__init__.py +11 -0
  43. symtest/tui/app.py +90 -0
  44. symtest/tui/controllers/__init__.py +0 -0
  45. symtest/tui/controllers/case_controller.py +322 -0
  46. symtest/tui/screens/__init__.py +0 -0
  47. symtest/tui/screens/case_editor.py +244 -0
  48. symtest/tui/screens/case_list.py +255 -0
  49. symtest/tui/widgets/__init__.py +0 -0
  50. symtest/tui/widgets/case_table.py +113 -0
  51. symtest/tui/widgets/expected_editor.py +159 -0
  52. symtest/tui/widgets/search_bar.py +160 -0
  53. symtest/tui/widgets/steps_editor.py +243 -0
  54. symtest/utils/__init__.py +21 -0
  55. symtest/utils/junit_xml_writer.py +137 -0
  56. symtest/utils/path_resolver.py +124 -0
  57. symtest/utils/report_generator.py +208 -0
  58. symtest_cli-1.3.0.dist-info/METADATA +316 -0
  59. symtest_cli-1.3.0.dist-info/RECORD +63 -0
  60. symtest_cli-1.3.0.dist-info/WHEEL +5 -0
  61. symtest_cli-1.3.0.dist-info/entry_points.txt +4 -0
  62. symtest_cli-1.3.0.dist-info/licenses/LICENSE +21 -0
  63. 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
+