exonware-xwsystem 0.0.1.410__py3-none-any.whl → 0.1.0.1__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 (259) hide show
  1. exonware/__init__.py +1 -1
  2. exonware/conf.py +1 -1
  3. exonware/xwsystem/__init__.py +2 -2
  4. exonware/xwsystem/caching/__init__.py +1 -1
  5. exonware/xwsystem/caching/base.py +2 -2
  6. exonware/xwsystem/caching/bloom_cache.py +2 -2
  7. exonware/xwsystem/caching/cache_manager.py +1 -1
  8. exonware/xwsystem/caching/conditional.py +2 -2
  9. exonware/xwsystem/caching/contracts.py +1 -1
  10. exonware/xwsystem/caching/decorators.py +2 -2
  11. exonware/xwsystem/caching/defs.py +1 -1
  12. exonware/xwsystem/caching/disk_cache.py +1 -1
  13. exonware/xwsystem/caching/distributed.py +1 -1
  14. exonware/xwsystem/caching/errors.py +1 -1
  15. exonware/xwsystem/caching/events.py +2 -2
  16. exonware/xwsystem/caching/eviction_strategies.py +1 -1
  17. exonware/xwsystem/caching/fluent.py +1 -1
  18. exonware/xwsystem/caching/integrity.py +1 -1
  19. exonware/xwsystem/caching/lfu_cache.py +2 -2
  20. exonware/xwsystem/caching/lfu_optimized.py +3 -3
  21. exonware/xwsystem/caching/lru_cache.py +2 -2
  22. exonware/xwsystem/caching/memory_bounded.py +2 -2
  23. exonware/xwsystem/caching/metrics_exporter.py +2 -2
  24. exonware/xwsystem/caching/observable_cache.py +1 -1
  25. exonware/xwsystem/caching/pluggable_cache.py +2 -2
  26. exonware/xwsystem/caching/rate_limiter.py +1 -1
  27. exonware/xwsystem/caching/read_through.py +2 -2
  28. exonware/xwsystem/caching/secure_cache.py +1 -1
  29. exonware/xwsystem/caching/serializable.py +2 -2
  30. exonware/xwsystem/caching/stats.py +1 -1
  31. exonware/xwsystem/caching/tagging.py +2 -2
  32. exonware/xwsystem/caching/ttl_cache.py +1 -1
  33. exonware/xwsystem/caching/two_tier_cache.py +1 -1
  34. exonware/xwsystem/caching/utils.py +1 -1
  35. exonware/xwsystem/caching/validation.py +1 -1
  36. exonware/xwsystem/caching/warming.py +2 -2
  37. exonware/xwsystem/caching/write_behind.py +2 -2
  38. exonware/xwsystem/cli/__init__.py +1 -1
  39. exonware/xwsystem/cli/args.py +1 -1
  40. exonware/xwsystem/cli/base.py +1 -1
  41. exonware/xwsystem/cli/colors.py +1 -1
  42. exonware/xwsystem/cli/console.py +1 -1
  43. exonware/xwsystem/cli/contracts.py +1 -1
  44. exonware/xwsystem/cli/defs.py +1 -1
  45. exonware/xwsystem/cli/errors.py +1 -1
  46. exonware/xwsystem/cli/progress.py +1 -1
  47. exonware/xwsystem/cli/prompts.py +1 -1
  48. exonware/xwsystem/cli/tables.py +1 -1
  49. exonware/xwsystem/config/__init__.py +1 -1
  50. exonware/xwsystem/config/base.py +2 -2
  51. exonware/xwsystem/config/contracts.py +1 -1
  52. exonware/xwsystem/config/defaults.py +1 -1
  53. exonware/xwsystem/config/defs.py +1 -1
  54. exonware/xwsystem/config/errors.py +2 -2
  55. exonware/xwsystem/config/logging.py +1 -1
  56. exonware/xwsystem/config/logging_setup.py +2 -2
  57. exonware/xwsystem/config/performance.py +115 -388
  58. exonware/xwsystem/http_client/__init__.py +1 -1
  59. exonware/xwsystem/http_client/advanced_client.py +2 -2
  60. exonware/xwsystem/http_client/base.py +2 -2
  61. exonware/xwsystem/http_client/client.py +2 -2
  62. exonware/xwsystem/http_client/contracts.py +1 -1
  63. exonware/xwsystem/http_client/defs.py +1 -1
  64. exonware/xwsystem/http_client/errors.py +2 -2
  65. exonware/xwsystem/io/__init__.py +1 -1
  66. exonware/xwsystem/io/archive/__init__.py +1 -1
  67. exonware/xwsystem/io/archive/archive.py +1 -1
  68. exonware/xwsystem/io/archive/archive_files.py +1 -1
  69. exonware/xwsystem/io/archive/archivers.py +2 -2
  70. exonware/xwsystem/io/archive/base.py +6 -6
  71. exonware/xwsystem/io/archive/codec_integration.py +1 -1
  72. exonware/xwsystem/io/archive/compression.py +1 -1
  73. exonware/xwsystem/io/archive/formats/__init__.py +1 -1
  74. exonware/xwsystem/io/archive/formats/brotli_format.py +6 -3
  75. exonware/xwsystem/io/archive/formats/lz4_format.py +6 -3
  76. exonware/xwsystem/io/archive/formats/rar.py +6 -3
  77. exonware/xwsystem/io/archive/formats/sevenzip.py +6 -3
  78. exonware/xwsystem/io/archive/formats/squashfs_format.py +1 -1
  79. exonware/xwsystem/io/archive/formats/tar.py +1 -1
  80. exonware/xwsystem/io/archive/formats/wim_format.py +6 -3
  81. exonware/xwsystem/io/archive/formats/zip.py +1 -1
  82. exonware/xwsystem/io/archive/formats/zpaq_format.py +1 -1
  83. exonware/xwsystem/io/archive/formats/zstandard.py +6 -3
  84. exonware/xwsystem/io/base.py +1 -1
  85. exonware/xwsystem/io/codec/__init__.py +1 -1
  86. exonware/xwsystem/io/codec/base.py +6 -6
  87. exonware/xwsystem/io/codec/contracts.py +1 -1
  88. exonware/xwsystem/io/codec/registry.py +5 -5
  89. exonware/xwsystem/io/common/__init__.py +1 -1
  90. exonware/xwsystem/io/common/base.py +1 -1
  91. exonware/xwsystem/io/common/lock.py +1 -1
  92. exonware/xwsystem/io/common/watcher.py +1 -1
  93. exonware/xwsystem/io/contracts.py +1 -1
  94. exonware/xwsystem/io/data_operations.py +746 -0
  95. exonware/xwsystem/io/defs.py +1 -1
  96. exonware/xwsystem/io/errors.py +1 -1
  97. exonware/xwsystem/io/facade.py +2 -2
  98. exonware/xwsystem/io/file/__init__.py +1 -1
  99. exonware/xwsystem/io/file/base.py +1 -1
  100. exonware/xwsystem/io/file/conversion.py +1 -1
  101. exonware/xwsystem/io/file/file.py +8 -6
  102. exonware/xwsystem/io/file/paged_source.py +8 -1
  103. exonware/xwsystem/io/file/paging/__init__.py +1 -1
  104. exonware/xwsystem/io/file/paging/byte_paging.py +1 -1
  105. exonware/xwsystem/io/file/paging/line_paging.py +1 -1
  106. exonware/xwsystem/io/file/paging/record_paging.py +1 -1
  107. exonware/xwsystem/io/file/paging/registry.py +4 -4
  108. exonware/xwsystem/io/file/source.py +20 -9
  109. exonware/xwsystem/io/filesystem/__init__.py +1 -1
  110. exonware/xwsystem/io/filesystem/base.py +1 -1
  111. exonware/xwsystem/io/filesystem/local.py +9 -1
  112. exonware/xwsystem/io/folder/__init__.py +1 -1
  113. exonware/xwsystem/io/folder/base.py +1 -1
  114. exonware/xwsystem/io/folder/folder.py +2 -2
  115. exonware/xwsystem/io/serialization/__init__.py +1 -1
  116. exonware/xwsystem/io/serialization/auto_serializer.py +52 -39
  117. exonware/xwsystem/io/serialization/base.py +165 -1
  118. exonware/xwsystem/io/serialization/contracts.py +88 -1
  119. exonware/xwsystem/io/serialization/defs.py +1 -1
  120. exonware/xwsystem/io/serialization/errors.py +1 -1
  121. exonware/xwsystem/io/serialization/flyweight.py +10 -10
  122. exonware/xwsystem/io/serialization/format_detector.py +8 -5
  123. exonware/xwsystem/io/serialization/formats/__init__.py +1 -1
  124. exonware/xwsystem/io/serialization/formats/binary/bson.py +1 -1
  125. exonware/xwsystem/io/serialization/formats/binary/cbor.py +1 -1
  126. exonware/xwsystem/io/serialization/formats/binary/marshal.py +1 -1
  127. exonware/xwsystem/io/serialization/formats/binary/msgpack.py +1 -1
  128. exonware/xwsystem/io/serialization/formats/binary/pickle.py +1 -1
  129. exonware/xwsystem/io/serialization/formats/binary/plistlib.py +1 -1
  130. exonware/xwsystem/io/serialization/formats/database/dbm.py +53 -1
  131. exonware/xwsystem/io/serialization/formats/database/shelve.py +48 -1
  132. exonware/xwsystem/io/serialization/formats/database/sqlite3.py +85 -1
  133. exonware/xwsystem/io/serialization/formats/text/append_only_log.py +201 -0
  134. exonware/xwsystem/io/serialization/formats/text/configparser.py +1 -1
  135. exonware/xwsystem/io/serialization/formats/text/csv.py +1 -1
  136. exonware/xwsystem/io/serialization/formats/text/formdata.py +1 -1
  137. exonware/xwsystem/io/serialization/formats/text/json.py +43 -20
  138. exonware/xwsystem/io/serialization/formats/text/json5.py +7 -5
  139. exonware/xwsystem/io/serialization/formats/text/jsonlines.py +316 -22
  140. exonware/xwsystem/io/serialization/formats/text/multipart.py +1 -1
  141. exonware/xwsystem/io/serialization/formats/text/toml.py +19 -3
  142. exonware/xwsystem/io/serialization/formats/text/xml.py +8 -1
  143. exonware/xwsystem/io/serialization/formats/text/yaml.py +52 -2
  144. exonware/xwsystem/io/serialization/parsers/__init__.py +15 -0
  145. exonware/xwsystem/io/serialization/parsers/base.py +59 -0
  146. exonware/xwsystem/io/serialization/parsers/hybrid_parser.py +61 -0
  147. exonware/xwsystem/io/serialization/parsers/msgspec_parser.py +45 -0
  148. exonware/xwsystem/io/serialization/parsers/orjson_direct_parser.py +53 -0
  149. exonware/xwsystem/io/serialization/parsers/orjson_parser.py +59 -0
  150. exonware/xwsystem/io/serialization/parsers/pysimdjson_parser.py +51 -0
  151. exonware/xwsystem/io/serialization/parsers/rapidjson_parser.py +50 -0
  152. exonware/xwsystem/io/serialization/parsers/registry.py +90 -0
  153. exonware/xwsystem/io/serialization/parsers/standard.py +43 -0
  154. exonware/xwsystem/io/serialization/parsers/ujson_parser.py +50 -0
  155. exonware/xwsystem/io/serialization/registry.py +1 -1
  156. exonware/xwsystem/io/serialization/serializer.py +175 -3
  157. exonware/xwsystem/io/serialization/utils/__init__.py +1 -1
  158. exonware/xwsystem/io/serialization/utils/path_ops.py +1 -1
  159. exonware/xwsystem/io/stream/__init__.py +1 -1
  160. exonware/xwsystem/io/stream/async_operations.py +1 -1
  161. exonware/xwsystem/io/stream/base.py +1 -1
  162. exonware/xwsystem/io/stream/codec_io.py +1 -1
  163. exonware/xwsystem/ipc/async_fabric.py +1 -2
  164. exonware/xwsystem/ipc/base.py +2 -2
  165. exonware/xwsystem/ipc/contracts.py +2 -2
  166. exonware/xwsystem/ipc/defs.py +1 -1
  167. exonware/xwsystem/ipc/errors.py +2 -2
  168. exonware/xwsystem/ipc/pipes.py +2 -2
  169. exonware/xwsystem/ipc/shared_memory.py +2 -2
  170. exonware/xwsystem/monitoring/base.py +2 -2
  171. exonware/xwsystem/monitoring/contracts.py +1 -1
  172. exonware/xwsystem/monitoring/defs.py +1 -1
  173. exonware/xwsystem/monitoring/error_recovery.py +2 -2
  174. exonware/xwsystem/monitoring/errors.py +2 -2
  175. exonware/xwsystem/monitoring/memory_monitor.py +1 -1
  176. exonware/xwsystem/monitoring/performance_manager_generic.py +2 -2
  177. exonware/xwsystem/monitoring/performance_validator.py +1 -1
  178. exonware/xwsystem/monitoring/system_monitor.py +2 -2
  179. exonware/xwsystem/monitoring/tracing.py +2 -2
  180. exonware/xwsystem/monitoring/tracker.py +1 -1
  181. exonware/xwsystem/operations/__init__.py +1 -1
  182. exonware/xwsystem/operations/base.py +1 -1
  183. exonware/xwsystem/operations/defs.py +1 -1
  184. exonware/xwsystem/operations/diff.py +1 -1
  185. exonware/xwsystem/operations/merge.py +1 -1
  186. exonware/xwsystem/operations/patch.py +1 -1
  187. exonware/xwsystem/patterns/base.py +2 -2
  188. exonware/xwsystem/patterns/context_manager.py +2 -2
  189. exonware/xwsystem/patterns/contracts.py +9 -9
  190. exonware/xwsystem/patterns/defs.py +1 -1
  191. exonware/xwsystem/patterns/dynamic_facade.py +8 -8
  192. exonware/xwsystem/patterns/errors.py +5 -5
  193. exonware/xwsystem/patterns/handler_factory.py +6 -6
  194. exonware/xwsystem/patterns/object_pool.py +7 -7
  195. exonware/xwsystem/patterns/registry.py +3 -3
  196. exonware/xwsystem/plugins/__init__.py +1 -1
  197. exonware/xwsystem/plugins/base.py +5 -5
  198. exonware/xwsystem/plugins/contracts.py +5 -5
  199. exonware/xwsystem/plugins/defs.py +1 -1
  200. exonware/xwsystem/plugins/errors.py +4 -4
  201. exonware/xwsystem/runtime/__init__.py +1 -1
  202. exonware/xwsystem/runtime/base.py +6 -6
  203. exonware/xwsystem/runtime/contracts.py +6 -6
  204. exonware/xwsystem/runtime/defs.py +1 -1
  205. exonware/xwsystem/runtime/env.py +2 -2
  206. exonware/xwsystem/runtime/errors.py +1 -1
  207. exonware/xwsystem/runtime/reflection.py +8 -8
  208. exonware/xwsystem/security/auth.py +1 -1
  209. exonware/xwsystem/security/base.py +2 -2
  210. exonware/xwsystem/security/contracts.py +1 -1
  211. exonware/xwsystem/security/crypto.py +2 -2
  212. exonware/xwsystem/security/defs.py +1 -1
  213. exonware/xwsystem/security/errors.py +2 -2
  214. exonware/xwsystem/security/hazmat.py +2 -2
  215. exonware/xwsystem/shared/__init__.py +1 -1
  216. exonware/xwsystem/shared/base.py +1 -1
  217. exonware/xwsystem/shared/contracts.py +1 -1
  218. exonware/xwsystem/shared/defs.py +1 -1
  219. exonware/xwsystem/shared/errors.py +1 -1
  220. exonware/xwsystem/structures/__init__.py +1 -1
  221. exonware/xwsystem/structures/base.py +2 -2
  222. exonware/xwsystem/structures/contracts.py +1 -1
  223. exonware/xwsystem/structures/defs.py +1 -1
  224. exonware/xwsystem/structures/errors.py +2 -2
  225. exonware/xwsystem/threading/async_primitives.py +2 -2
  226. exonware/xwsystem/threading/base.py +2 -2
  227. exonware/xwsystem/threading/contracts.py +1 -1
  228. exonware/xwsystem/threading/defs.py +1 -1
  229. exonware/xwsystem/threading/errors.py +2 -2
  230. exonware/xwsystem/threading/safe_factory.py +6 -6
  231. exonware/xwsystem/utils/base.py +2 -2
  232. exonware/xwsystem/utils/contracts.py +1 -1
  233. exonware/xwsystem/utils/dt/__init__.py +1 -1
  234. exonware/xwsystem/utils/dt/base.py +2 -2
  235. exonware/xwsystem/utils/dt/contracts.py +1 -1
  236. exonware/xwsystem/utils/dt/defs.py +1 -1
  237. exonware/xwsystem/utils/dt/errors.py +2 -2
  238. exonware/xwsystem/utils/dt/formatting.py +1 -1
  239. exonware/xwsystem/utils/dt/humanize.py +2 -2
  240. exonware/xwsystem/utils/dt/parsing.py +1 -1
  241. exonware/xwsystem/utils/dt/timezone_utils.py +1 -1
  242. exonware/xwsystem/utils/errors.py +2 -2
  243. exonware/xwsystem/utils/utils_contracts.py +1 -1
  244. exonware/xwsystem/validation/__init__.py +1 -1
  245. exonware/xwsystem/validation/base.py +15 -15
  246. exonware/xwsystem/validation/contracts.py +1 -1
  247. exonware/xwsystem/validation/data_validator.py +10 -0
  248. exonware/xwsystem/validation/declarative.py +9 -9
  249. exonware/xwsystem/validation/defs.py +1 -1
  250. exonware/xwsystem/validation/errors.py +2 -2
  251. exonware/xwsystem/validation/fluent_validator.py +4 -4
  252. exonware/xwsystem/version.py +4 -4
  253. {exonware_xwsystem-0.0.1.410.dist-info → exonware_xwsystem-0.1.0.1.dist-info}/METADATA +3 -3
  254. exonware_xwsystem-0.1.0.1.dist-info/RECORD +284 -0
  255. exonware/xwsystem/caching/USAGE_GUIDE.md +0 -779
  256. exonware/xwsystem/utils/test_runner.py +0 -526
  257. exonware_xwsystem-0.0.1.410.dist-info/RECORD +0 -273
  258. {exonware_xwsystem-0.0.1.410.dist-info → exonware_xwsystem-0.1.0.1.dist-info}/WHEEL +0 -0
  259. {exonware_xwsystem-0.0.1.410.dist-info → exonware_xwsystem-0.1.0.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,526 +0,0 @@
1
- """
2
- Company: eXonware.com
3
- Author: Eng. Muhammad AlShehri
4
- Email: connect@exonware.com
5
- Version: 0.0.1.410
6
- Generation Date: 11-Oct-2025
7
-
8
- Reusable test runner utilities with colored output and Markdown generation.
9
- Designed to minimize code duplication across all eXonware test runners.
10
- """
11
-
12
- import sys
13
- import subprocess
14
- from pathlib import Path
15
- from datetime import datetime
16
- from typing import Optional
17
-
18
- # Set UTF-8 encoding for Windows console to support emojis
19
- if sys.platform == 'win32':
20
- try:
21
- sys.stdout.reconfigure(encoding='utf-8')
22
- sys.stderr.reconfigure(encoding='utf-8')
23
- except (AttributeError, OSError):
24
- # If reconfigure not available or fails, continue without it
25
- pass
26
-
27
- # Import colors from xwsystem CLI module
28
- try:
29
- from exonware.xwsystem.cli.colors import ColoredOutput, Colors, Style
30
- except ImportError:
31
- # Fallback if running standalone
32
- class ColoredOutput:
33
- def colorize(self, text, color, style=None):
34
- return text
35
- def success(self, text, **kwargs):
36
- print(f"✅ {text}", **kwargs)
37
- def error(self, text, **kwargs):
38
- print(f"❌ {text}", **kwargs)
39
- def warning(self, text, **kwargs):
40
- print(f"⚠️ {text}", **kwargs)
41
- def header(self, text, **kwargs):
42
- print(text, **kwargs)
43
- def subheader(self, text, **kwargs):
44
- print(text, **kwargs)
45
- def info(self, text, **kwargs):
46
- print(f"ℹ️ {text}", **kwargs)
47
-
48
- Colors = None
49
- Style = None
50
-
51
-
52
- class DualOutput:
53
- """
54
- Capture output for both terminal (with colors/emojis) and Markdown file.
55
-
56
- Features:
57
- - Colored terminal output with emojis
58
- - Clean Markdown output without ANSI codes
59
- - Automatic file path formatting
60
- - Timestamped output
61
- """
62
-
63
- def __init__(self, output_file: Path):
64
- """
65
- Initialize dual output handler.
66
-
67
- Args:
68
- output_file: Path to Markdown output file
69
- """
70
- self.output_file = output_file
71
- self.colored = ColoredOutput()
72
- self.markdown_lines = []
73
-
74
- def print(self, text: str = "", markdown_format: Optional[str] = None,
75
- color: Optional[str] = None, emoji: Optional[str] = None):
76
- """
77
- Print to terminal and capture for Markdown.
78
-
79
- Args:
80
- text: Text to print
81
- markdown_format: Optional Markdown-specific format
82
- color: Optional color name (success, error, info, header, subheader)
83
- emoji: Optional emoji to prepend
84
- """
85
- # Terminal output with color and emoji
86
- display_text = text
87
- if emoji:
88
- display_text = f"{emoji} {text}"
89
-
90
- # Handle Unicode encoding errors gracefully
91
- try:
92
- if color:
93
- if color == 'success':
94
- self.colored.success(display_text)
95
- elif color == 'error':
96
- self.colored.error(display_text)
97
- elif color == 'info':
98
- self.colored.info(display_text)
99
- elif color == 'header':
100
- self.colored.header(display_text)
101
- elif color == 'subheader':
102
- self.colored.subheader(display_text)
103
- else:
104
- print(display_text)
105
- else:
106
- print(display_text)
107
- except UnicodeEncodeError:
108
- # Fallback without emoji if encoding fails
109
- if color:
110
- if color == 'success':
111
- self.colored.success(text)
112
- elif color == 'error':
113
- self.colored.error(text)
114
- elif color == 'info':
115
- self.colored.info(text)
116
- elif color == 'header':
117
- self.colored.header(text)
118
- elif color == 'subheader':
119
- self.colored.subheader(text)
120
- else:
121
- print(text)
122
- else:
123
- print(text)
124
-
125
- # Markdown output (clean, no colors)
126
- if markdown_format:
127
- self.markdown_lines.append(markdown_format)
128
- else:
129
- # Clean emoji and special chars for Markdown
130
- cleaned = text.replace("="*80, "---")
131
- if emoji:
132
- cleaned = f"{emoji} {cleaned}"
133
- self.markdown_lines.append(cleaned)
134
-
135
- def save(self, header_info: dict):
136
- """
137
- Save Markdown output to file.
138
-
139
- Args:
140
- header_info: Dictionary with header information
141
- - library: Library name
142
- - layer: Test layer
143
- - description: Description
144
- """
145
- header = f"""# Test Runner Output
146
-
147
- **Library:** {header_info.get('library', 'unknown')}
148
- **Layer:** {header_info.get('layer', 'unknown')}
149
- **Generated:** {datetime.now().strftime("%d-%b-%Y %H:%M:%S")}
150
- **Description:** {header_info.get('description', 'Test execution')}
151
-
152
- ---
153
-
154
- """
155
- content = header + "\n".join(self.markdown_lines) + "\n"
156
- self.output_file.write_text(content, encoding='utf-8')
157
-
158
-
159
- def format_path(path: Path, relative_to: Optional[Path] = None) -> str:
160
- """
161
- Format path for display with full absolute path.
162
-
163
- Args:
164
- path: Path to format
165
- relative_to: Optional base path to show relative path alongside absolute
166
-
167
- Returns:
168
- Formatted path string
169
- """
170
- abs_path = path.resolve()
171
-
172
- if relative_to:
173
- try:
174
- rel_path = path.relative_to(relative_to)
175
- return f"{abs_path} (relative: {rel_path})"
176
- except ValueError:
177
- # Not relative to the base
178
- pass
179
-
180
- return str(abs_path)
181
-
182
-
183
- def print_header(title: str, output: Optional[DualOutput] = None):
184
- """
185
- Print a formatted header with separator.
186
-
187
- Args:
188
- title: Header title
189
- output: Optional DualOutput instance for dual output
190
- """
191
- separator = "=" * 80
192
-
193
- if output:
194
- output.print(separator, "---")
195
- output.print(title, f"# {title}", color='header') # No extra emoji - colored.header adds ℹ
196
- output.print(separator, "---")
197
- else:
198
- colored = ColoredOutput()
199
- print(separator)
200
- colored.header(f"🎯 {title}")
201
- print(separator)
202
-
203
-
204
- def print_section(title: str, output: Optional[DualOutput] = None):
205
- """
206
- Print a formatted section header.
207
-
208
- Args:
209
- title: Section title
210
- output: Optional DualOutput instance for dual output
211
- """
212
- if output:
213
- output.print(f"\n{title}", f"\n## {title}", color='subheader') # No extra emoji
214
- else:
215
- colored = ColoredOutput()
216
- colored.subheader(f"\n📋 {title}")
217
-
218
-
219
- def print_status(success: bool, message: str, output: Optional[DualOutput] = None):
220
- """
221
- Print a status message with appropriate color and emoji.
222
-
223
- Args:
224
- success: True for success, False for failure
225
- message: Status message
226
- output: Optional DualOutput instance for dual output
227
- """
228
- if success:
229
- emoji = '✅'
230
- color = 'success'
231
- else:
232
- emoji = '❌'
233
- color = 'error'
234
-
235
- if output:
236
- output.print(message, f"{emoji} {message}", color=color) # No extra emoji - colored method adds ✓/✗
237
- else:
238
- colored = ColoredOutput()
239
- if success:
240
- colored.success(message)
241
- else:
242
- colored.error(message)
243
-
244
-
245
- def run_pytest(
246
- test_dir: Path,
247
- markers: list[str],
248
- options: Optional[list[str]] = None,
249
- output: Optional[DualOutput] = None
250
- ) -> tuple[int, str, str]:
251
- """
252
- Run pytest with specified options and capture output.
253
-
254
- Args:
255
- test_dir: Directory containing tests
256
- markers: List of pytest markers to run
257
- options: Additional pytest options
258
- output: Optional DualOutput instance for logging
259
-
260
- Returns:
261
- Tuple of (exit_code, stdout, stderr)
262
- """
263
- if options is None:
264
- options = ["-v", "--tb=short"]
265
-
266
- cmd = [sys.executable, "-m", "pytest"] + options + [str(test_dir)]
267
-
268
- # Add markers
269
- if markers:
270
- cmd.extend(["-m", " or ".join(markers)])
271
-
272
- # Print command
273
- cmd_str = " ".join(cmd)
274
- if output:
275
- output.print(f"Command: {cmd_str}", f"```bash\n{cmd_str}\n```", color='info') # No extra emoji
276
- output.print(f"Working directory: {format_path(test_dir.parent)}",
277
- f"**Working directory:** `{format_path(test_dir.parent)}`",
278
- color='info')
279
-
280
- # Run pytest
281
- result = subprocess.run(
282
- cmd,
283
- capture_output=True,
284
- text=True,
285
- cwd=test_dir.parent
286
- )
287
-
288
- return result.returncode, result.stdout, result.stderr
289
-
290
-
291
- class TestRunner:
292
- """
293
- Reusable test runner with colored output and Markdown generation.
294
-
295
- Features:
296
- - Automatic path formatting (full absolute paths)
297
- - Colored terminal output with emojis
298
- - Markdown output generation
299
- - Exit code handling
300
- - Summary statistics
301
-
302
- Usage:
303
- runner = TestRunner(
304
- library_name="xwnode",
305
- layer_name="0.core",
306
- description="Core Tests - Fast, High-Value Checks"
307
- )
308
- runner.run()
309
- """
310
-
311
- def __init__(
312
- self,
313
- library_name: str,
314
- layer_name: str,
315
- description: str,
316
- test_dir: Optional[Path] = None,
317
- markers: Optional[list[str]] = None,
318
- output_file: Optional[Path] = None
319
- ):
320
- """
321
- Initialize test runner.
322
-
323
- Args:
324
- library_name: Name of library (e.g., 'xwnode', 'xwsystem')
325
- layer_name: Test layer (e.g., '0.core', '1.unit')
326
- description: Description of test layer
327
- test_dir: Directory containing tests (auto-detected if None)
328
- markers: Pytest markers to run (auto-detected if None)
329
- output_file: Output file path (auto-detected if None)
330
- """
331
- self.library_name = library_name
332
- self.layer_name = layer_name
333
- self.description = description
334
-
335
- # Auto-detect paths
336
- if test_dir is None:
337
- # Assume runner is in the test directory
338
- self.test_dir = Path.cwd()
339
- else:
340
- self.test_dir = test_dir
341
-
342
- # Default markers based on layer
343
- if markers is None:
344
- marker_name = library_name.lower().replace('-', '_').replace('exonware-', '')
345
- if 'core' in layer_name:
346
- markers = [f"{marker_name}_core"]
347
- elif 'unit' in layer_name:
348
- markers = [f"{marker_name}_unit"]
349
- elif 'integration' in layer_name:
350
- markers = [f"{marker_name}_integration"]
351
- elif 'advance' in layer_name:
352
- markers = [f"{marker_name}_advance"]
353
- else:
354
- markers = []
355
- self.markers = markers
356
-
357
- # Output file
358
- if output_file is None:
359
- self.output_file = self.test_dir / "runner_out.md"
360
- else:
361
- self.output_file = output_file
362
-
363
- # Create output handler
364
- self.output = DualOutput(self.output_file)
365
-
366
- def run(self) -> int:
367
- """
368
- Run tests and generate output.
369
- Auto-discovers sub-runners and test files.
370
-
371
- Returns:
372
- Exit code (0 for success, non-zero for failure)
373
- """
374
- # Print header
375
- print_header(f"{self.library_name} - {self.description}", self.output)
376
-
377
- # Print paths
378
- self.output.print(f"Test Directory: {format_path(self.test_dir)}",
379
- f"**Test Directory:** `{format_path(self.test_dir)}`",
380
- color='info') # No extra emoji - colored.info adds ℹ
381
- self.output.print(f"Output File: {format_path(self.output_file)}",
382
- f"**Output File:** `{format_path(self.output_file)}`",
383
- color='info')
384
-
385
- # Add src to Python path
386
- src_path = self.test_dir.parent.parent / "src"
387
- if src_path.exists():
388
- sys.path.insert(0, str(src_path))
389
- self.output.print(f"Added to path: {format_path(src_path)}",
390
- f"**Added to path:** `{format_path(src_path)}`",
391
- color='info')
392
-
393
- # Auto-discovery: Check for sub-runners first
394
- sub_runners = []
395
- for subdir in sorted(self.test_dir.iterdir()):
396
- if subdir.is_dir() and not subdir.name.startswith('__'):
397
- runner_path = subdir / "runner.py"
398
- if runner_path.exists():
399
- sub_runners.append((subdir.name, runner_path))
400
-
401
- # If sub-runners found, execute them
402
- if sub_runners:
403
- self.output.print(f"\nDiscovered {len(sub_runners)} sub-module(s) with runners",
404
- f"\n**Discovered:** {len(sub_runners)} sub-module(s) with runners",
405
- color='info')
406
-
407
- exit_codes = []
408
- for subdir_name, runner_path in sub_runners:
409
- self.output.print(f" Sub-module: {subdir_name}",
410
- f"- Sub-module: `{subdir_name}`",
411
- color='subheader')
412
- result = subprocess.run([sys.executable, str(runner_path)])
413
- exit_codes.append(result.returncode)
414
-
415
- # Aggregate results
416
- success = all(code == 0 for code in exit_codes)
417
- exit_code = 0 if success else 1
418
-
419
- # Print final status
420
- status_emoji = "✅" if success else "❌"
421
- status_text = "ALL SUB-MODULES PASSED" if success else "SOME SUB-MODULES FAILED"
422
-
423
- if self.output.colored:
424
- if success:
425
- self.output.colored.success(f"\n{status_emoji} {status_text}")
426
- else:
427
- self.output.colored.error(f"\n{status_emoji} {status_text}")
428
- else:
429
- print(f"\n{status_emoji} {status_text}")
430
-
431
- # Add status to markdown
432
- self.output.markdown_lines.append(f"\n**Status:** {status_emoji} {status_text}\n")
433
- self.output.markdown_lines.append(f"\n**Sub-module Results:** {sum(1 for c in exit_codes if c == 0)}/{len(exit_codes)} passed\n")
434
-
435
- else:
436
- # No sub-runners: scan for test files and run pytest directly
437
- test_files = list(self.test_dir.glob("test_*.py"))
438
- self.output.print(f"\nDiscovered {len(test_files)} test file(s)",
439
- f"\n**Discovered:** {len(test_files)} test file(s)",
440
- color='info')
441
-
442
- print_section("Running Tests", self.output)
443
- exit_code, stdout, stderr = run_pytest(
444
- self.test_dir,
445
- self.markers,
446
- output=self.output
447
- )
448
-
449
- # Show pytest output to terminal
450
- if stdout:
451
- print(stdout)
452
-
453
- if stderr:
454
- print(stderr, file=sys.stderr)
455
-
456
- # Add full output to markdown
457
- if stdout:
458
- self.output.markdown_lines.append("\n### Test Output\n```")
459
- self.output.markdown_lines.append(stdout)
460
- self.output.markdown_lines.append("```")
461
-
462
- if stderr:
463
- self.output.markdown_lines.append("\n### Errors\n```")
464
- self.output.markdown_lines.append(stderr)
465
- self.output.markdown_lines.append("```")
466
-
467
- # Extract summary line (the line with === and test counts)
468
- summary_line = None
469
- for line in stdout.split('\n'):
470
- if '===' in line and ('passed' in line.lower() or 'failed' in line.lower()):
471
- summary_line = line.strip()
472
- break
473
-
474
- # Print colorful summary as separator
475
- print() # Blank line before summary
476
- if summary_line:
477
- # Color the summary line
478
- if self.output.colored:
479
- # Parse the summary to colorize different parts
480
- if 'passed' in summary_line.lower() and 'failed' not in summary_line.lower():
481
- # All passed - show in green
482
- self.output.colored.success(f"\n{summary_line}\n")
483
- elif 'failed' in summary_line.lower():
484
- # Some failed - show in red
485
- self.output.colored.error(f"\n{summary_line}\n")
486
- else:
487
- # Other status - show normally
488
- print(f"\n{summary_line}\n")
489
- else:
490
- print(f"\n{summary_line}\n")
491
-
492
- # Add summary to markdown
493
- self.output.markdown_lines.append(f"\n### Summary\n\n```\n{summary_line}\n```")
494
-
495
- # Print final status (concise - no duplication)
496
- success = exit_code == 0
497
- status_emoji = "✅" if success else "❌"
498
- status_text = "PASSED" if success else "FAILED"
499
-
500
- if self.output.colored:
501
- if success:
502
- self.output.colored.success(f"{status_emoji} {status_text}")
503
- else:
504
- self.output.colored.error(f"{status_emoji} {status_text}")
505
- else:
506
- print(f"{status_emoji} {status_text}")
507
-
508
- # Add status to markdown only
509
- self.output.markdown_lines.append(f"\n**Status:** {status_emoji} {status_text}\n")
510
-
511
- # Save output
512
- self.output.save({
513
- 'library': self.library_name,
514
- 'layer': self.layer_name,
515
- 'description': self.description
516
- })
517
-
518
- # Print save location
519
- save_msg = f"Results saved to: {format_path(self.output_file)}"
520
- if self.output.colored:
521
- self.output.colored.info(save_msg)
522
- else:
523
- print(f"💾 {save_msg}")
524
-
525
- return exit_code
526
-