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.
- exonware/__init__.py +1 -1
- exonware/conf.py +1 -1
- exonware/xwsystem/__init__.py +2 -2
- exonware/xwsystem/caching/__init__.py +1 -1
- exonware/xwsystem/caching/base.py +2 -2
- exonware/xwsystem/caching/bloom_cache.py +2 -2
- exonware/xwsystem/caching/cache_manager.py +1 -1
- exonware/xwsystem/caching/conditional.py +2 -2
- exonware/xwsystem/caching/contracts.py +1 -1
- exonware/xwsystem/caching/decorators.py +2 -2
- exonware/xwsystem/caching/defs.py +1 -1
- exonware/xwsystem/caching/disk_cache.py +1 -1
- exonware/xwsystem/caching/distributed.py +1 -1
- exonware/xwsystem/caching/errors.py +1 -1
- exonware/xwsystem/caching/events.py +2 -2
- exonware/xwsystem/caching/eviction_strategies.py +1 -1
- exonware/xwsystem/caching/fluent.py +1 -1
- exonware/xwsystem/caching/integrity.py +1 -1
- exonware/xwsystem/caching/lfu_cache.py +2 -2
- exonware/xwsystem/caching/lfu_optimized.py +3 -3
- exonware/xwsystem/caching/lru_cache.py +2 -2
- exonware/xwsystem/caching/memory_bounded.py +2 -2
- exonware/xwsystem/caching/metrics_exporter.py +2 -2
- exonware/xwsystem/caching/observable_cache.py +1 -1
- exonware/xwsystem/caching/pluggable_cache.py +2 -2
- exonware/xwsystem/caching/rate_limiter.py +1 -1
- exonware/xwsystem/caching/read_through.py +2 -2
- exonware/xwsystem/caching/secure_cache.py +1 -1
- exonware/xwsystem/caching/serializable.py +2 -2
- exonware/xwsystem/caching/stats.py +1 -1
- exonware/xwsystem/caching/tagging.py +2 -2
- exonware/xwsystem/caching/ttl_cache.py +1 -1
- exonware/xwsystem/caching/two_tier_cache.py +1 -1
- exonware/xwsystem/caching/utils.py +1 -1
- exonware/xwsystem/caching/validation.py +1 -1
- exonware/xwsystem/caching/warming.py +2 -2
- exonware/xwsystem/caching/write_behind.py +2 -2
- exonware/xwsystem/cli/__init__.py +1 -1
- exonware/xwsystem/cli/args.py +1 -1
- exonware/xwsystem/cli/base.py +1 -1
- exonware/xwsystem/cli/colors.py +1 -1
- exonware/xwsystem/cli/console.py +1 -1
- exonware/xwsystem/cli/contracts.py +1 -1
- exonware/xwsystem/cli/defs.py +1 -1
- exonware/xwsystem/cli/errors.py +1 -1
- exonware/xwsystem/cli/progress.py +1 -1
- exonware/xwsystem/cli/prompts.py +1 -1
- exonware/xwsystem/cli/tables.py +1 -1
- exonware/xwsystem/config/__init__.py +1 -1
- exonware/xwsystem/config/base.py +2 -2
- exonware/xwsystem/config/contracts.py +1 -1
- exonware/xwsystem/config/defaults.py +1 -1
- exonware/xwsystem/config/defs.py +1 -1
- exonware/xwsystem/config/errors.py +2 -2
- exonware/xwsystem/config/logging.py +1 -1
- exonware/xwsystem/config/logging_setup.py +2 -2
- exonware/xwsystem/config/performance.py +115 -388
- exonware/xwsystem/http_client/__init__.py +1 -1
- exonware/xwsystem/http_client/advanced_client.py +2 -2
- exonware/xwsystem/http_client/base.py +2 -2
- exonware/xwsystem/http_client/client.py +2 -2
- exonware/xwsystem/http_client/contracts.py +1 -1
- exonware/xwsystem/http_client/defs.py +1 -1
- exonware/xwsystem/http_client/errors.py +2 -2
- exonware/xwsystem/io/__init__.py +1 -1
- exonware/xwsystem/io/archive/__init__.py +1 -1
- exonware/xwsystem/io/archive/archive.py +1 -1
- exonware/xwsystem/io/archive/archive_files.py +1 -1
- exonware/xwsystem/io/archive/archivers.py +2 -2
- exonware/xwsystem/io/archive/base.py +6 -6
- exonware/xwsystem/io/archive/codec_integration.py +1 -1
- exonware/xwsystem/io/archive/compression.py +1 -1
- exonware/xwsystem/io/archive/formats/__init__.py +1 -1
- exonware/xwsystem/io/archive/formats/brotli_format.py +6 -3
- exonware/xwsystem/io/archive/formats/lz4_format.py +6 -3
- exonware/xwsystem/io/archive/formats/rar.py +6 -3
- exonware/xwsystem/io/archive/formats/sevenzip.py +6 -3
- exonware/xwsystem/io/archive/formats/squashfs_format.py +1 -1
- exonware/xwsystem/io/archive/formats/tar.py +1 -1
- exonware/xwsystem/io/archive/formats/wim_format.py +6 -3
- exonware/xwsystem/io/archive/formats/zip.py +1 -1
- exonware/xwsystem/io/archive/formats/zpaq_format.py +1 -1
- exonware/xwsystem/io/archive/formats/zstandard.py +6 -3
- exonware/xwsystem/io/base.py +1 -1
- exonware/xwsystem/io/codec/__init__.py +1 -1
- exonware/xwsystem/io/codec/base.py +6 -6
- exonware/xwsystem/io/codec/contracts.py +1 -1
- exonware/xwsystem/io/codec/registry.py +5 -5
- exonware/xwsystem/io/common/__init__.py +1 -1
- exonware/xwsystem/io/common/base.py +1 -1
- exonware/xwsystem/io/common/lock.py +1 -1
- exonware/xwsystem/io/common/watcher.py +1 -1
- exonware/xwsystem/io/contracts.py +1 -1
- exonware/xwsystem/io/data_operations.py +746 -0
- exonware/xwsystem/io/defs.py +1 -1
- exonware/xwsystem/io/errors.py +1 -1
- exonware/xwsystem/io/facade.py +2 -2
- exonware/xwsystem/io/file/__init__.py +1 -1
- exonware/xwsystem/io/file/base.py +1 -1
- exonware/xwsystem/io/file/conversion.py +1 -1
- exonware/xwsystem/io/file/file.py +8 -6
- exonware/xwsystem/io/file/paged_source.py +8 -1
- exonware/xwsystem/io/file/paging/__init__.py +1 -1
- exonware/xwsystem/io/file/paging/byte_paging.py +1 -1
- exonware/xwsystem/io/file/paging/line_paging.py +1 -1
- exonware/xwsystem/io/file/paging/record_paging.py +1 -1
- exonware/xwsystem/io/file/paging/registry.py +4 -4
- exonware/xwsystem/io/file/source.py +20 -9
- exonware/xwsystem/io/filesystem/__init__.py +1 -1
- exonware/xwsystem/io/filesystem/base.py +1 -1
- exonware/xwsystem/io/filesystem/local.py +9 -1
- exonware/xwsystem/io/folder/__init__.py +1 -1
- exonware/xwsystem/io/folder/base.py +1 -1
- exonware/xwsystem/io/folder/folder.py +2 -2
- exonware/xwsystem/io/serialization/__init__.py +1 -1
- exonware/xwsystem/io/serialization/auto_serializer.py +52 -39
- exonware/xwsystem/io/serialization/base.py +165 -1
- exonware/xwsystem/io/serialization/contracts.py +88 -1
- exonware/xwsystem/io/serialization/defs.py +1 -1
- exonware/xwsystem/io/serialization/errors.py +1 -1
- exonware/xwsystem/io/serialization/flyweight.py +10 -10
- exonware/xwsystem/io/serialization/format_detector.py +8 -5
- exonware/xwsystem/io/serialization/formats/__init__.py +1 -1
- exonware/xwsystem/io/serialization/formats/binary/bson.py +1 -1
- exonware/xwsystem/io/serialization/formats/binary/cbor.py +1 -1
- exonware/xwsystem/io/serialization/formats/binary/marshal.py +1 -1
- exonware/xwsystem/io/serialization/formats/binary/msgpack.py +1 -1
- exonware/xwsystem/io/serialization/formats/binary/pickle.py +1 -1
- exonware/xwsystem/io/serialization/formats/binary/plistlib.py +1 -1
- exonware/xwsystem/io/serialization/formats/database/dbm.py +53 -1
- exonware/xwsystem/io/serialization/formats/database/shelve.py +48 -1
- exonware/xwsystem/io/serialization/formats/database/sqlite3.py +85 -1
- exonware/xwsystem/io/serialization/formats/text/append_only_log.py +201 -0
- exonware/xwsystem/io/serialization/formats/text/configparser.py +1 -1
- exonware/xwsystem/io/serialization/formats/text/csv.py +1 -1
- exonware/xwsystem/io/serialization/formats/text/formdata.py +1 -1
- exonware/xwsystem/io/serialization/formats/text/json.py +43 -20
- exonware/xwsystem/io/serialization/formats/text/json5.py +7 -5
- exonware/xwsystem/io/serialization/formats/text/jsonlines.py +316 -22
- exonware/xwsystem/io/serialization/formats/text/multipart.py +1 -1
- exonware/xwsystem/io/serialization/formats/text/toml.py +19 -3
- exonware/xwsystem/io/serialization/formats/text/xml.py +8 -1
- exonware/xwsystem/io/serialization/formats/text/yaml.py +52 -2
- exonware/xwsystem/io/serialization/parsers/__init__.py +15 -0
- exonware/xwsystem/io/serialization/parsers/base.py +59 -0
- exonware/xwsystem/io/serialization/parsers/hybrid_parser.py +61 -0
- exonware/xwsystem/io/serialization/parsers/msgspec_parser.py +45 -0
- exonware/xwsystem/io/serialization/parsers/orjson_direct_parser.py +53 -0
- exonware/xwsystem/io/serialization/parsers/orjson_parser.py +59 -0
- exonware/xwsystem/io/serialization/parsers/pysimdjson_parser.py +51 -0
- exonware/xwsystem/io/serialization/parsers/rapidjson_parser.py +50 -0
- exonware/xwsystem/io/serialization/parsers/registry.py +90 -0
- exonware/xwsystem/io/serialization/parsers/standard.py +43 -0
- exonware/xwsystem/io/serialization/parsers/ujson_parser.py +50 -0
- exonware/xwsystem/io/serialization/registry.py +1 -1
- exonware/xwsystem/io/serialization/serializer.py +175 -3
- exonware/xwsystem/io/serialization/utils/__init__.py +1 -1
- exonware/xwsystem/io/serialization/utils/path_ops.py +1 -1
- exonware/xwsystem/io/stream/__init__.py +1 -1
- exonware/xwsystem/io/stream/async_operations.py +1 -1
- exonware/xwsystem/io/stream/base.py +1 -1
- exonware/xwsystem/io/stream/codec_io.py +1 -1
- exonware/xwsystem/ipc/async_fabric.py +1 -2
- exonware/xwsystem/ipc/base.py +2 -2
- exonware/xwsystem/ipc/contracts.py +2 -2
- exonware/xwsystem/ipc/defs.py +1 -1
- exonware/xwsystem/ipc/errors.py +2 -2
- exonware/xwsystem/ipc/pipes.py +2 -2
- exonware/xwsystem/ipc/shared_memory.py +2 -2
- exonware/xwsystem/monitoring/base.py +2 -2
- exonware/xwsystem/monitoring/contracts.py +1 -1
- exonware/xwsystem/monitoring/defs.py +1 -1
- exonware/xwsystem/monitoring/error_recovery.py +2 -2
- exonware/xwsystem/monitoring/errors.py +2 -2
- exonware/xwsystem/monitoring/memory_monitor.py +1 -1
- exonware/xwsystem/monitoring/performance_manager_generic.py +2 -2
- exonware/xwsystem/monitoring/performance_validator.py +1 -1
- exonware/xwsystem/monitoring/system_monitor.py +2 -2
- exonware/xwsystem/monitoring/tracing.py +2 -2
- exonware/xwsystem/monitoring/tracker.py +1 -1
- exonware/xwsystem/operations/__init__.py +1 -1
- exonware/xwsystem/operations/base.py +1 -1
- exonware/xwsystem/operations/defs.py +1 -1
- exonware/xwsystem/operations/diff.py +1 -1
- exonware/xwsystem/operations/merge.py +1 -1
- exonware/xwsystem/operations/patch.py +1 -1
- exonware/xwsystem/patterns/base.py +2 -2
- exonware/xwsystem/patterns/context_manager.py +2 -2
- exonware/xwsystem/patterns/contracts.py +9 -9
- exonware/xwsystem/patterns/defs.py +1 -1
- exonware/xwsystem/patterns/dynamic_facade.py +8 -8
- exonware/xwsystem/patterns/errors.py +5 -5
- exonware/xwsystem/patterns/handler_factory.py +6 -6
- exonware/xwsystem/patterns/object_pool.py +7 -7
- exonware/xwsystem/patterns/registry.py +3 -3
- exonware/xwsystem/plugins/__init__.py +1 -1
- exonware/xwsystem/plugins/base.py +5 -5
- exonware/xwsystem/plugins/contracts.py +5 -5
- exonware/xwsystem/plugins/defs.py +1 -1
- exonware/xwsystem/plugins/errors.py +4 -4
- exonware/xwsystem/runtime/__init__.py +1 -1
- exonware/xwsystem/runtime/base.py +6 -6
- exonware/xwsystem/runtime/contracts.py +6 -6
- exonware/xwsystem/runtime/defs.py +1 -1
- exonware/xwsystem/runtime/env.py +2 -2
- exonware/xwsystem/runtime/errors.py +1 -1
- exonware/xwsystem/runtime/reflection.py +8 -8
- exonware/xwsystem/security/auth.py +1 -1
- exonware/xwsystem/security/base.py +2 -2
- exonware/xwsystem/security/contracts.py +1 -1
- exonware/xwsystem/security/crypto.py +2 -2
- exonware/xwsystem/security/defs.py +1 -1
- exonware/xwsystem/security/errors.py +2 -2
- exonware/xwsystem/security/hazmat.py +2 -2
- exonware/xwsystem/shared/__init__.py +1 -1
- exonware/xwsystem/shared/base.py +1 -1
- exonware/xwsystem/shared/contracts.py +1 -1
- exonware/xwsystem/shared/defs.py +1 -1
- exonware/xwsystem/shared/errors.py +1 -1
- exonware/xwsystem/structures/__init__.py +1 -1
- exonware/xwsystem/structures/base.py +2 -2
- exonware/xwsystem/structures/contracts.py +1 -1
- exonware/xwsystem/structures/defs.py +1 -1
- exonware/xwsystem/structures/errors.py +2 -2
- exonware/xwsystem/threading/async_primitives.py +2 -2
- exonware/xwsystem/threading/base.py +2 -2
- exonware/xwsystem/threading/contracts.py +1 -1
- exonware/xwsystem/threading/defs.py +1 -1
- exonware/xwsystem/threading/errors.py +2 -2
- exonware/xwsystem/threading/safe_factory.py +6 -6
- exonware/xwsystem/utils/base.py +2 -2
- exonware/xwsystem/utils/contracts.py +1 -1
- exonware/xwsystem/utils/dt/__init__.py +1 -1
- exonware/xwsystem/utils/dt/base.py +2 -2
- exonware/xwsystem/utils/dt/contracts.py +1 -1
- exonware/xwsystem/utils/dt/defs.py +1 -1
- exonware/xwsystem/utils/dt/errors.py +2 -2
- exonware/xwsystem/utils/dt/formatting.py +1 -1
- exonware/xwsystem/utils/dt/humanize.py +2 -2
- exonware/xwsystem/utils/dt/parsing.py +1 -1
- exonware/xwsystem/utils/dt/timezone_utils.py +1 -1
- exonware/xwsystem/utils/errors.py +2 -2
- exonware/xwsystem/utils/utils_contracts.py +1 -1
- exonware/xwsystem/validation/__init__.py +1 -1
- exonware/xwsystem/validation/base.py +15 -15
- exonware/xwsystem/validation/contracts.py +1 -1
- exonware/xwsystem/validation/data_validator.py +10 -0
- exonware/xwsystem/validation/declarative.py +9 -9
- exonware/xwsystem/validation/defs.py +1 -1
- exonware/xwsystem/validation/errors.py +2 -2
- exonware/xwsystem/validation/fluent_validator.py +4 -4
- exonware/xwsystem/version.py +4 -4
- {exonware_xwsystem-0.0.1.410.dist-info → exonware_xwsystem-0.1.0.1.dist-info}/METADATA +3 -3
- exonware_xwsystem-0.1.0.1.dist-info/RECORD +284 -0
- exonware/xwsystem/caching/USAGE_GUIDE.md +0 -779
- exonware/xwsystem/utils/test_runner.py +0 -526
- exonware_xwsystem-0.0.1.410.dist-info/RECORD +0 -273
- {exonware_xwsystem-0.0.1.410.dist-info → exonware_xwsystem-0.1.0.1.dist-info}/WHEEL +0 -0
- {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
|
-
|