synapse-hub 0.0.0-stage → 2.0.0
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.
- package/.agents/agents/failure-analyst.md +47 -0
- package/.agents/agents/live-explorer.md +41 -0
- package/.agents/agents/orchestrator.md +43 -0
- package/.agents/agents/pom-crafter.md +81 -0
- package/.agents/agents/quality-gatekeeper.md +47 -0
- package/.agents/agents/test-runner.md +43 -0
- package/.agents/rules/context-engineering.md +88 -0
- package/.agents/rules/git-push-protocol.md +101 -0
- package/.agents/rules/graphify.md +14 -0
- package/.agents/rules/lessons-learned.md +23 -0
- package/.agents/rules/mandatory-playwright-live.md +29 -0
- package/.agents/rules/skill-precedence.md +42 -0
- package/.agents/rules/systematic-debugging.md +99 -0
- package/.agents/skills/agent-orchestration-multi-agent-optimize/SKILL.md +239 -0
- package/.agents/skills/antigravity-test-sentinel/SKILL.md +64 -0
- package/.agents/skills/api-network-mocking-interception/SKILL.md +142 -0
- package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +70 -0
- package/.agents/skills/architecture-decision-records/SKILL.md +441 -0
- package/.agents/skills/auth-session-and-cookie-manager/SKILL.md +72 -0
- package/.agents/skills/backend-load-and-stress-patterns/SKILL.md +214 -0
- package/.agents/skills/bats-testing-patterns/SKILL.md +100 -0
- package/.agents/skills/bats-testing-patterns/resources/implementation-playbook.md +614 -0
- package/.agents/skills/clean-pom-crafter/SKILL.md +75 -0
- package/.agents/skills/clean-pom-crafter/examples/standard_test_case.groovy +28 -0
- package/.agents/skills/clean-pom-crafter/references/pom_patterns.groovy +138 -0
- package/.agents/skills/code-refactoring-refactor-clean/SKILL.md +64 -0
- package/.agents/skills/code-refactoring-refactor-clean/resources/implementation-playbook.md +879 -0
- package/.agents/skills/db-state-verifier-and-seeder/SKILL.md +123 -0
- package/.agents/skills/e2e-testing-patterns/SKILL.md +103 -0
- package/.agents/skills/e2e-testing-patterns/resources/implementation-playbook.md +522 -0
- package/.agents/skills/failure-autopsy-and-healing/SKILL.md +54 -0
- package/.agents/skills/failure-autopsy-and-healing/references/healing_matrix.md +64 -0
- package/.agents/skills/failure-autopsy-and-healing/references/qa_investigation_protocol.md +56 -0
- package/.agents/skills/failure-autopsy-and-healing/scripts/triage_failures.sh +98 -0
- package/.agents/skills/file-and-report-validator/SKILL.md +90 -0
- package/.agents/skills/flaky-test-sentinel-and-quarantine/SKILL.md +114 -0
- package/.agents/skills/github-actions-templates/SKILL.md +334 -0
- package/.agents/skills/graphify-impact-analyzer/SKILL.md +78 -0
- package/.agents/skills/katalon-cli-runner/SKILL.md +76 -0
- package/.agents/skills/katalon-cli-runner/references/groovy-patterns.md +209 -0
- package/.agents/skills/katalon-cli-runner/references/katalon-api.md +155 -0
- package/.agents/skills/katalon-cli-runner/references/runner_commands.md +67 -0
- package/.agents/skills/lam-script-generator/SKILL.md +83 -0
- package/.agents/skills/playwright-live-interaction/SKILL.md +86 -0
- package/.agents/skills/playwright-live-interaction/references/advanced_superpowers.md +160 -0
- package/.agents/skills/playwright-live-interaction/references/compact_accessibility_snapshots.md +75 -0
- package/.agents/skills/playwright-live-interaction/references/recipes.md +388 -0
- package/.agents/skills/playwright-live-interaction/references/swarm_orchestration.md +134 -0
- package/.agents/skills/security-dast-sentinel/SKILL.md +176 -0
- package/.agents/skills/self-healing-locator/SKILL.md +73 -0
- package/.agents/skills/synthetic-test-data-generator/SKILL.md +135 -0
- package/.agents/skills/token-and-context-optimizer/SKILL.md +70 -0
- package/.agents/skills/visual-regression-sentinel/SKILL.md +62 -0
- package/.agents/skills/wcag-audit-patterns/SKILL.md +77 -0
- package/.agents/skills/wcag-audit-patterns/resources/implementation-playbook.md +541 -0
- package/.agents/skills/web-vitals-and-performance-profiler/SKILL.md +65 -0
- package/.agents/templates/completion_report.md +57 -0
- package/.agents/workflows/graphify.md +10 -0
- package/.agents/workflows/test-impact.md +15 -0
- package/.agents/workflows/verify.md +34 -0
- package/AGENTS.md +115 -0
- package/README.md +394 -2
- package/bin/agent-qa.js +90 -0
- package/package.json +59 -4
- package/skills_manifest.json +262 -0
- package/templates/PROJECT_AI_GUIDE_TEMPLATE.md +97 -0
- package/templates/PROJECT_STATE_TEMPLATE.md +20 -0
- package/tools/TOOLS_RUNBOOK.md +298 -0
- package/tools/agent_canary.py +501 -0
- package/tools/analyze_project.py +242 -0
- package/tools/api_drift_sentinel.py +208 -0
- package/tools/auto_heal.py +189 -0
- package/tools/browser_helpers.js +593 -0
- package/tools/bug_reporter.py +224 -0
- package/tools/cache_resolver.py +846 -0
- package/tools/check_env.py +288 -0
- package/tools/ci_healing_hook.py +128 -0
- package/tools/cli.py +402 -0
- package/tools/config_decoupler.py +253 -0
- package/tools/db_sandbox.py +341 -0
- package/tools/distill_log.py +152 -0
- package/tools/e2e_flow_prober.ps1 +65 -0
- package/tools/flaky_detector.py +222 -0
- package/tools/fuzz_payloads.py +197 -0
- package/tools/generate_allure_report.ps1 +30 -0
- package/tools/generate_api_matrix.py +193 -0
- package/tools/generate_tc_from_story.py +182 -0
- package/tools/generate_test_reports.ps1 +135 -0
- package/tools/git_pre_commit_guard.py +203 -0
- package/tools/heal_pr_automator.py +195 -0
- package/tools/heuristic_locator_matcher.py +202 -0
- package/tools/infer_linter.py +238 -0
- package/tools/lint_ai_docs.py +254 -0
- package/tools/lookup_tc.py +165 -0
- package/tools/lookup_xpath.py +167 -0
- package/tools/matrix_sharder.py +146 -0
- package/tools/mcp_excel_server.ps1 +251 -0
- package/tools/mcp_server.py +490 -0
- package/tools/micro_mutator.py +309 -0
- package/tools/network_mock_shield.py +170 -0
- package/tools/omni_emitter.py +500 -0
- package/tools/omni_parser.py +435 -0
- package/tools/omni_transpiler.py +168 -0
- package/tools/pinpoint_failure.py +88 -0
- package/tools/project_state.py +169 -0
- package/tools/repo_map.json +12 -0
- package/tools/repo_map.py +277 -0
- package/tools/repo_map.txt +7 -0
- package/tools/safety_firewall.py +245 -0
- package/tools/setup_project.py +265 -0
- package/tools/smart_visual_masker.py +134 -0
- package/tools/test_impact.py +321 -0
- package/tools/tms_sync_bridge.py +342 -0
- package/tools/trace_data_flow.py +240 -0
- package/tools/traffic_session_converter.py +256 -0
- package/tools/universal_ir.py +480 -0
- package/tools/validate_groovy_scripts.py +173 -0
- package/tools/verify_all.py +496 -0
- package/tools/verify_all.sh +4 -0
- package/tools/visual_diff_canvas.py +142 -0
- package/tools/visual_som_helper.py +418 -0
- package/tools/wcag_audit.py +214 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Enterprise Flaky Test Detector & Quarantine Sentinel
|
|
4
|
+
---------------------------------------------------
|
|
5
|
+
Executes a target test scenario N times (default: 5) or parses historical JUnit XML
|
|
6
|
+
reports to calculate the Flakiness Index, detect state flips, and recommend quarantine isolation.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python tools/flaky_detector.py --repeat 5 --cmd "python -m unittest tests/test_tools.py"
|
|
10
|
+
python tools/flaky_detector.py --reports "reports/**/junit*.xml"
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
import glob
|
|
16
|
+
import json
|
|
17
|
+
import time
|
|
18
|
+
import argparse
|
|
19
|
+
import subprocess
|
|
20
|
+
import xml.etree.ElementTree as ET
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
|
|
23
|
+
# Enforce UTF-8 stdout encoding for Windows console compatibility
|
|
24
|
+
if hasattr(sys.stdout, 'reconfigure'):
|
|
25
|
+
try:
|
|
26
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
27
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
|
28
|
+
except Exception:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
32
|
+
REPORTS_DIR = os.path.join(REPO_ROOT, "reports")
|
|
33
|
+
|
|
34
|
+
GREEN = "\033[92m"
|
|
35
|
+
RED = "\033[91m"
|
|
36
|
+
YELLOW = "\033[93m"
|
|
37
|
+
CYAN = "\033[96m"
|
|
38
|
+
BOLD = "\033[1m"
|
|
39
|
+
RESET = "\033[0m"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def run_command(cmd, timeout=300):
|
|
43
|
+
"""Executes a shell command and returns (returncode, stdout, stderr, elapsed_ms)."""
|
|
44
|
+
start = time.time()
|
|
45
|
+
try:
|
|
46
|
+
res = subprocess.run(
|
|
47
|
+
cmd,
|
|
48
|
+
shell=True,
|
|
49
|
+
stdout=subprocess.PIPE,
|
|
50
|
+
stderr=subprocess.PIPE,
|
|
51
|
+
text=True,
|
|
52
|
+
timeout=timeout
|
|
53
|
+
)
|
|
54
|
+
elapsed_ms = round((time.time() - start) * 1000, 2)
|
|
55
|
+
return res.returncode, res.stdout, res.stderr, elapsed_ms
|
|
56
|
+
except subprocess.TimeoutExpired:
|
|
57
|
+
elapsed_ms = round((time.time() - start) * 1000, 2)
|
|
58
|
+
return -1, "", "Execution timed out", elapsed_ms
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def calculate_flakiness(runs):
|
|
62
|
+
"""
|
|
63
|
+
Given a list of boolean statuses [True, False, True, ...],
|
|
64
|
+
calculates pass rate, flip count, and flakiness tier.
|
|
65
|
+
"""
|
|
66
|
+
total = len(runs)
|
|
67
|
+
if total == 0:
|
|
68
|
+
return {
|
|
69
|
+
"total": 0,
|
|
70
|
+
"passes": 0,
|
|
71
|
+
"failures": 0,
|
|
72
|
+
"pass_rate": 0.0,
|
|
73
|
+
"flips": 0,
|
|
74
|
+
"status": "NO_DATA",
|
|
75
|
+
"quarantine": False
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
passes = sum(1 for r in runs if r)
|
|
79
|
+
failures = total - passes
|
|
80
|
+
pass_rate = round((passes / total) * 100, 1)
|
|
81
|
+
|
|
82
|
+
# Count transitions (Pass -> Fail or Fail -> Pass)
|
|
83
|
+
flips = 0
|
|
84
|
+
for i in range(len(runs) - 1):
|
|
85
|
+
if runs[i] != runs[i + 1]:
|
|
86
|
+
flips += 1
|
|
87
|
+
|
|
88
|
+
if pass_rate == 100.0:
|
|
89
|
+
status = "STABLE"
|
|
90
|
+
quarantine = False
|
|
91
|
+
elif pass_rate == 0.0:
|
|
92
|
+
status = "DETERMINISTIC_FAIL"
|
|
93
|
+
quarantine = False
|
|
94
|
+
elif pass_rate >= 80.0:
|
|
95
|
+
status = "MILD_FLAKE"
|
|
96
|
+
quarantine = (flips >= 2)
|
|
97
|
+
else:
|
|
98
|
+
status = "SEVERE_FLAKE"
|
|
99
|
+
quarantine = True
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
"total": total,
|
|
103
|
+
"passes": passes,
|
|
104
|
+
"failures": failures,
|
|
105
|
+
"pass_rate": pass_rate,
|
|
106
|
+
"flips": flips,
|
|
107
|
+
"status": status,
|
|
108
|
+
"quarantine": quarantine
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def parse_junit_reports(glob_pattern):
|
|
113
|
+
"""Parses JUnit XML report files and aggregates pass/fail counts by test case."""
|
|
114
|
+
test_results = {}
|
|
115
|
+
files = glob.glob(glob_pattern, recursive=True)
|
|
116
|
+
if not files:
|
|
117
|
+
print(f"⚠️ No JUnit XML reports found matching: {glob_pattern}")
|
|
118
|
+
return test_results
|
|
119
|
+
|
|
120
|
+
for f in files:
|
|
121
|
+
try:
|
|
122
|
+
tree = ET.parse(f)
|
|
123
|
+
root = tree.getroot()
|
|
124
|
+
for tc in root.iter("testcase"):
|
|
125
|
+
name = tc.attrib.get("name", "unknown")
|
|
126
|
+
classname = tc.attrib.get("classname", "")
|
|
127
|
+
full_id = f"{classname}#{name}" if classname else name
|
|
128
|
+
has_failure = tc.find("failure") is not None or tc.find("error") is not None
|
|
129
|
+
is_passed = not has_failure
|
|
130
|
+
test_results.setdefault(full_id, []).append(is_passed)
|
|
131
|
+
except Exception as e:
|
|
132
|
+
print(f"⚠️ Could not parse {f}: {e}")
|
|
133
|
+
|
|
134
|
+
return test_results
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def benchmark_live_command(cmd, iterations=5):
|
|
138
|
+
"""Executes a live test command N times to observe flakiness in real time."""
|
|
139
|
+
print(f"\n{BOLD}{CYAN}======================================================================{RESET}")
|
|
140
|
+
print(f"{BOLD}{CYAN} ENTERPRISE FLAKY TEST SENTINEL (Multi-Run Verification) {RESET}")
|
|
141
|
+
print(f"{BOLD}{CYAN}======================================================================{RESET}")
|
|
142
|
+
print(f" Target Command : {cmd}")
|
|
143
|
+
print(f" Repetition Count : {iterations} consecutive iterations")
|
|
144
|
+
print(f" Timestamp : {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
|
145
|
+
|
|
146
|
+
runs = []
|
|
147
|
+
latencies = []
|
|
148
|
+
for i in range(1, iterations + 1):
|
|
149
|
+
sys.stdout.write(f" [Run {i}/{iterations}] Executing... ")
|
|
150
|
+
sys.stdout.flush()
|
|
151
|
+
code, stdout, stderr, ms = run_command(cmd)
|
|
152
|
+
is_pass = (code == 0)
|
|
153
|
+
runs.append(is_pass)
|
|
154
|
+
latencies.append(ms)
|
|
155
|
+
status_str = f"{GREEN}PASS{RESET}" if is_pass else f"{RED}FAIL (Exit {code}){RESET}"
|
|
156
|
+
print(f"{status_str} ({ms} ms)")
|
|
157
|
+
|
|
158
|
+
stats = calculate_flakiness(runs)
|
|
159
|
+
stats["latencies_ms"] = latencies
|
|
160
|
+
stats["avg_latency_ms"] = round(sum(latencies) / max(1, len(latencies)), 2)
|
|
161
|
+
stats["command"] = cmd
|
|
162
|
+
|
|
163
|
+
# Print Scorecard
|
|
164
|
+
print(f"\n{BOLD}======================================================================{RESET}")
|
|
165
|
+
print(f"{BOLD} FLAKINESS DIAGNOSTIC SCORECARD {RESET}")
|
|
166
|
+
print(f"{BOLD}======================================================================{RESET}")
|
|
167
|
+
print(f" Pass Rate : {stats['pass_rate']}% ({stats['passes']}/{stats['total']} passes)")
|
|
168
|
+
print(f" Status Flips : {stats['flips']} transitions")
|
|
169
|
+
print(f" Average Runtime : {stats['avg_latency_ms']} ms")
|
|
170
|
+
print(f" Stability Status : {stats['status']}")
|
|
171
|
+
|
|
172
|
+
if stats["quarantine"]:
|
|
173
|
+
print(f" Recommendation : {RED}{BOLD}[QUARANTINE REQUIRED]{RESET} Tag with @quarantine immediately!")
|
|
174
|
+
else:
|
|
175
|
+
print(f" Recommendation : {GREEN}[SUITE APPROVED]{RESET} Test is deterministic.")
|
|
176
|
+
print(f"======================================================================\n")
|
|
177
|
+
|
|
178
|
+
# Persist Report
|
|
179
|
+
os.makedirs(REPORTS_DIR, exist_ok=True)
|
|
180
|
+
report_file = os.path.join(REPORTS_DIR, "quarantine_board.json")
|
|
181
|
+
with open(report_file, "w", encoding="utf-8") as fp:
|
|
182
|
+
json.dump(stats, fp, indent=2)
|
|
183
|
+
print(f"Detailed telemetry saved to: {report_file}")
|
|
184
|
+
|
|
185
|
+
return stats
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def main():
|
|
189
|
+
parser = argparse.ArgumentParser(description="Enterprise Flaky Test Detector & Quarantine Sentinel")
|
|
190
|
+
parser.add_argument("--cmd", "-c", default=None, help="Test command to execute repeatedly")
|
|
191
|
+
parser.add_argument("--repeat", "-r", type=int, default=5, help="Number of repetitions (default: 5)")
|
|
192
|
+
parser.add_argument("--reports", default=None, help="Glob pattern for JUnit XML reports to analyze")
|
|
193
|
+
args = parser.parse_args()
|
|
194
|
+
|
|
195
|
+
if args.cmd:
|
|
196
|
+
stats = benchmark_live_command(args.cmd, iterations=args.repeat)
|
|
197
|
+
sys.exit(1 if stats["quarantine"] else 0)
|
|
198
|
+
elif args.reports:
|
|
199
|
+
results = parse_junit_reports(args.reports)
|
|
200
|
+
quarantine_candidates = []
|
|
201
|
+
for test_id, runs in results.items():
|
|
202
|
+
st = calculate_flakiness(runs)
|
|
203
|
+
st["test_id"] = test_id
|
|
204
|
+
if st["quarantine"]:
|
|
205
|
+
quarantine_candidates.append(st)
|
|
206
|
+
|
|
207
|
+
print(f"\nAnalyzed {len(results)} distinct test scenarios from reports.")
|
|
208
|
+
print(f"Quarantine Candidates Detected: {len(quarantine_candidates)}")
|
|
209
|
+
for qc in quarantine_candidates:
|
|
210
|
+
print(f" - {RED}{qc['test_id']}{RESET}: Pass rate {qc['pass_rate']}%, Flips: {qc['flips']}")
|
|
211
|
+
|
|
212
|
+
report_file = os.path.join(REPORTS_DIR, "quarantine_board.json")
|
|
213
|
+
with open(report_file, "w", encoding="utf-8") as fp:
|
|
214
|
+
json.dump(quarantine_candidates, fp, indent=2)
|
|
215
|
+
sys.exit(1 if quarantine_candidates else 0)
|
|
216
|
+
else:
|
|
217
|
+
parser.print_help()
|
|
218
|
+
sys.exit(0)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
if __name__ == "__main__":
|
|
222
|
+
main()
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Enterprise Edge-Case Fuzzing & Input Sanitization Payload Catalog
|
|
4
|
+
------------------------------------------------------------------
|
|
5
|
+
Supplies standardized boundary, resilience, and sanitization payloads
|
|
6
|
+
for transforming functional automated test suites into robust input
|
|
7
|
+
sanitization and vulnerability boundary validators.
|
|
8
|
+
|
|
9
|
+
Categories Covered:
|
|
10
|
+
1. SQLi Harmless Polyglots (no data destruction, tests parser escaping)
|
|
11
|
+
2. XSS & Script Injection Polyglots
|
|
12
|
+
3. Unicode BiDi / RTL Trojan Source & Zero-Width characters
|
|
13
|
+
4. Multi-byte Emoji Surrogate Pairs & ZWJ Sequences
|
|
14
|
+
5. Extreme Length Strings (10^4 buffer stress testing)
|
|
15
|
+
6. Null Byte & Control Character Injections
|
|
16
|
+
7. Numeric Precision & IEEE-754 Boundary Limits
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import sys
|
|
20
|
+
import json
|
|
21
|
+
import argparse
|
|
22
|
+
from typing import Dict, Any, List, Optional
|
|
23
|
+
|
|
24
|
+
# Enforce UTF-8 stdout encoding for Windows console compatibility
|
|
25
|
+
if hasattr(sys.stdout, 'reconfigure'):
|
|
26
|
+
try:
|
|
27
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
28
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
|
29
|
+
except Exception:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
FUZZ_PAYLOAD_CATALOG: Dict[str, List[Dict[str, Any]]] = {
|
|
34
|
+
"sqli_polyglots": [
|
|
35
|
+
{"name": "Standard Tautology", "payload": "' OR '1'='1' -- ", "expected_status": 400},
|
|
36
|
+
{"name": "Inline Polyglot Comment", "payload": "SLEEP(0)/*'/*\"/*--", "expected_status": 400},
|
|
37
|
+
{"name": "Union Select Null Probe", "payload": "1' UNION SELECT NULL, NULL, NULL -- ", "expected_status": 400},
|
|
38
|
+
{"name": "Batched Query Delay Probe", "payload": "'; WAITFOR DELAY '0:0:0'--", "expected_status": 400},
|
|
39
|
+
{"name": "Admin Comment Bypass", "payload": "admin' -- ", "expected_status": 400}
|
|
40
|
+
],
|
|
41
|
+
"xss_polyglots": [
|
|
42
|
+
{"name": "Standard Script Tag", "payload": "<script>alert(1)</script>", "expected_status": 400},
|
|
43
|
+
{"name": "Image Tag OnError", "payload": "<img src=x onerror=console.warn('qa_xss')>", "expected_status": 400},
|
|
44
|
+
{"name": "Universal Polyglot Vector", "payload": "javascript:/*--></title></style></textarea></script></xmp><svg/onload='+/\"/+/onmouseover=1+(alert)(1)//'>", "expected_status": 400},
|
|
45
|
+
{"name": "SVG FromCharCode Injection", "payload": "\"><svg/onload=alert(String.fromCharCode(88,83,83))>", "expected_status": 400}
|
|
46
|
+
],
|
|
47
|
+
"unicode_bidi_rtl": [
|
|
48
|
+
{"name": "Right-to-Left Override (Trojan Source)", "payload": "\u202E\u0041\u0042\u0043_AUTO_TX", "expected_status": 400},
|
|
49
|
+
{"name": "Zero-Width Space & Joiner Injection", "payload": "AUTO\u200B\u200C\u200D_DATA", "expected_status": 400},
|
|
50
|
+
{"name": "Byte Order Mark (BOM) In-String", "payload": "\uFEFFAUTO_USER", "expected_status": 400},
|
|
51
|
+
{"name": "Mixed BiDi Arabic/Latin", "payload": "USER_مرحبا_بالعالم_2026", "expected_status": 200}
|
|
52
|
+
],
|
|
53
|
+
"emoji_surrogates": [
|
|
54
|
+
{"name": "Complex ZWJ Family Sequence", "payload": "👨👩👧👦_QA_RECORD", "expected_status": 200},
|
|
55
|
+
{"name": "Rainbow Flag Multi-Code", "payload": "🏳️🌈_FLAG_TAG", "expected_status": 200},
|
|
56
|
+
{"name": "Unpaired Surrogate Pair Boundary", "payload": "\uD83D\uDE00\uD83D", "expected_status": 400},
|
|
57
|
+
{"name": "4-Byte High Plane UTF-8", "payload": "🔥🚀⚡🧪✨", "expected_status": 200}
|
|
58
|
+
],
|
|
59
|
+
"extreme_length": [
|
|
60
|
+
{"name": "10^4 (10,000 Chars) Buffer Stress", "payload": "A" * 10000, "expected_status": 400},
|
|
61
|
+
{"name": "1,024 Chars Text Block", "payload": "X" * 1024, "expected_status": 400},
|
|
62
|
+
{"name": "Repetitive Delimiter Stress", "payload": "{\"test\": [" + ",".join(["1"] * 500) + "]}", "expected_status": 400}
|
|
63
|
+
],
|
|
64
|
+
"null_byte_control": [
|
|
65
|
+
{"name": "Null Byte Extension Truncation", "payload": "document\x00.pdf", "expected_status": 400},
|
|
66
|
+
{"name": "URL Encoded Null Byte", "payload": "test%00.png", "expected_status": 400},
|
|
67
|
+
{"name": "Carriage Return Linefeed Header Split", "payload": "test\r\nSet-Cookie: QA_LEAK=1", "expected_status": 400}
|
|
68
|
+
],
|
|
69
|
+
"numeric_boundaries": [
|
|
70
|
+
{"name": "32-bit Integer Max Overflow", "payload": 2147483648, "expected_status": 400},
|
|
71
|
+
{"name": "32-bit Integer Min Underflow", "payload": -2147483649, "expected_status": 400},
|
|
72
|
+
{"name": "64-bit BigInt Max Boundary", "payload": 9223372036854775807, "expected_status": 200},
|
|
73
|
+
{"name": "JavaScript MAX_SAFE_INTEGER + 1", "payload": 9007199254740992, "expected_status": 400},
|
|
74
|
+
{"name": "IEEE-754 Extreme Float Overflow", "payload": 1e308, "expected_status": 400},
|
|
75
|
+
{"name": "Negative Zero Float", "payload": -0.0, "expected_status": 200}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_fuzz_payloads(category: Optional[str] = None) -> Dict[str, List[Dict[str, Any]]]:
|
|
81
|
+
"""Returns payloads filtered by category or the entire catalog."""
|
|
82
|
+
if category and category.lower() != "all":
|
|
83
|
+
cat_key = category.lower()
|
|
84
|
+
if cat_key in FUZZ_PAYLOAD_CATALOG:
|
|
85
|
+
return {cat_key: FUZZ_PAYLOAD_CATALOG[cat_key]}
|
|
86
|
+
# Fuzzy match
|
|
87
|
+
for k in FUZZ_PAYLOAD_CATALOG:
|
|
88
|
+
if cat_key in k:
|
|
89
|
+
return {k: FUZZ_PAYLOAD_CATALOG[k]}
|
|
90
|
+
return FUZZ_PAYLOAD_CATALOG
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def get_field_fuzz_scenarios(field_name: str, field_type: str = "string") -> List[Dict[str, Any]]:
|
|
94
|
+
"""
|
|
95
|
+
Generates actionable test scenarios for a given field name and data type.
|
|
96
|
+
"""
|
|
97
|
+
scenarios = []
|
|
98
|
+
if field_type == "string":
|
|
99
|
+
# 1. SQLi Polyglot
|
|
100
|
+
scenarios.append({
|
|
101
|
+
"scenario": f"{field_name} SQLi Polyglot Sanitization",
|
|
102
|
+
"category": "SECURITY_SANITY",
|
|
103
|
+
"value": FUZZ_PAYLOAD_CATALOG["sqli_polyglots"][0]["payload"],
|
|
104
|
+
"expected": 400
|
|
105
|
+
})
|
|
106
|
+
# 2. XSS Probe
|
|
107
|
+
scenarios.append({
|
|
108
|
+
"scenario": f"{field_name} XSS Universal Polyglot",
|
|
109
|
+
"category": "SECURITY_SANITY",
|
|
110
|
+
"value": FUZZ_PAYLOAD_CATALOG["xss_polyglots"][1]["payload"],
|
|
111
|
+
"expected": 400
|
|
112
|
+
})
|
|
113
|
+
# 3. BiDi RTL Trojan Source
|
|
114
|
+
scenarios.append({
|
|
115
|
+
"scenario": f"{field_name} Unicode BiDi RTL Override",
|
|
116
|
+
"category": "RESILIENCE_UNICODE",
|
|
117
|
+
"value": FUZZ_PAYLOAD_CATALOG["unicode_bidi_rtl"][0]["payload"],
|
|
118
|
+
"expected": 400
|
|
119
|
+
})
|
|
120
|
+
# 4. Multi-byte Emoji Surrogate
|
|
121
|
+
scenarios.append({
|
|
122
|
+
"scenario": f"{field_name} Emoji Surrogate & ZWJ Sequence",
|
|
123
|
+
"category": "RESILIENCE_UNICODE",
|
|
124
|
+
"value": FUZZ_PAYLOAD_CATALOG["emoji_surrogates"][0]["payload"],
|
|
125
|
+
"expected": 200
|
|
126
|
+
})
|
|
127
|
+
# 5. Extreme Length 10^4
|
|
128
|
+
scenarios.append({
|
|
129
|
+
"scenario": f"{field_name} 10^4 Chars Buffer Stress",
|
|
130
|
+
"category": "RESILIENCE_OVERFLOW",
|
|
131
|
+
"value": "A" * 10000,
|
|
132
|
+
"expected": 400
|
|
133
|
+
})
|
|
134
|
+
# 6. Null Byte Injection
|
|
135
|
+
scenarios.append({
|
|
136
|
+
"scenario": f"{field_name} Null Byte Injection",
|
|
137
|
+
"category": "SECURITY_SANITY",
|
|
138
|
+
"value": "item\x00_test",
|
|
139
|
+
"expected": 400
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
elif field_type in ("integer", "number"):
|
|
143
|
+
for item in FUZZ_PAYLOAD_CATALOG["numeric_boundaries"]:
|
|
144
|
+
scenarios.append({
|
|
145
|
+
"scenario": f"{field_name} {item['name']}",
|
|
146
|
+
"category": "RESILIENCE_NUMERIC",
|
|
147
|
+
"value": item["payload"],
|
|
148
|
+
"expected": item["expected_status"]
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
return scenarios
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def main():
|
|
155
|
+
parser = argparse.ArgumentParser(description="Edge-Case Fuzzing & Input Sanitization Payload Generator")
|
|
156
|
+
parser.add_argument("--category", default="all", help="Payload category (sqli, xss, bidi, emoji, length, null, numeric, or all)")
|
|
157
|
+
parser.add_argument("--field", default="", help="Optional field name to generate targeted boundary suite")
|
|
158
|
+
parser.add_argument("--type", default="string", choices=["string", "integer", "number"], help="Field data type")
|
|
159
|
+
parser.add_argument("--json", action="store_true", help="Output results in JSON format")
|
|
160
|
+
args = parser.parse_args()
|
|
161
|
+
|
|
162
|
+
if args.field:
|
|
163
|
+
scenarios = get_field_fuzz_scenarios(args.field, field_type=args.type)
|
|
164
|
+
if args.json:
|
|
165
|
+
print(json.dumps(scenarios, indent=2, ensure_ascii=False))
|
|
166
|
+
else:
|
|
167
|
+
print(f"\n======================================================================")
|
|
168
|
+
print(f" EDGE-CASE FUZZING SCENARIOS FOR FIELD: '{args.field}' ({args.type.upper()})")
|
|
169
|
+
print(f"======================================================================")
|
|
170
|
+
for idx, sc in enumerate(scenarios, start=1):
|
|
171
|
+
val_disp = repr(sc['value']) if len(str(sc['value'])) < 60 else repr(str(sc['value'])[:50] + "...")
|
|
172
|
+
print(f" [{idx:02d}] {sc['scenario']:<45} | Expected: HTTP {sc['expected']} | Value: {val_disp}")
|
|
173
|
+
print()
|
|
174
|
+
return 0
|
|
175
|
+
|
|
176
|
+
catalog = get_fuzz_payloads(args.category)
|
|
177
|
+
if args.json:
|
|
178
|
+
print(json.dumps(catalog, indent=2, ensure_ascii=False))
|
|
179
|
+
else:
|
|
180
|
+
print(f"\n======================================================================")
|
|
181
|
+
print(f" ENTERPRISE EDGE-CASE FUZZING & SANITIZATION CATALOG ")
|
|
182
|
+
print(f"======================================================================")
|
|
183
|
+
total_payloads = sum(len(items) for items in catalog.values())
|
|
184
|
+
print(f"Categories: {len(catalog)} | Total Payloads: {total_payloads}\n")
|
|
185
|
+
|
|
186
|
+
for cat, items in catalog.items():
|
|
187
|
+
print(f"📁 [{cat.upper()}] ({len(items)} vectors):")
|
|
188
|
+
for it in items:
|
|
189
|
+
p_disp = repr(it['payload']) if len(str(it['payload'])) < 60 else repr(str(it['payload'])[:50] + "...")
|
|
190
|
+
print(f" • {it['name']:<35} -> HTTP {it['expected_status']} | {p_disp}")
|
|
191
|
+
print()
|
|
192
|
+
|
|
193
|
+
return 0
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
if __name__ == "__main__":
|
|
197
|
+
sys.exit(main())
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Write-Host "==========================================================" -ForegroundColor Cyan
|
|
2
|
+
Write-Host " ENTERPRISE AUTOMATION - ALLURE REPORT GENERATOR" -ForegroundColor Cyan
|
|
3
|
+
Write-Host "==========================================================" -ForegroundColor Cyan
|
|
4
|
+
|
|
5
|
+
$projectDir = Split-Path -Parent $PSScriptRoot
|
|
6
|
+
$resultsDir = Join-Path $projectDir "allure-results"
|
|
7
|
+
$reportDir = Join-Path $projectDir "allure-report"
|
|
8
|
+
|
|
9
|
+
if (-not (Test-Path $resultsDir)) {
|
|
10
|
+
Write-Host "[!] Không tìm thấy thư mục: $resultsDir" -ForegroundColor Yellow
|
|
11
|
+
Write-Host "[!] Hãy thực thi Test Suite trước để sinh dữ liệu kết quả." -ForegroundColor Yellow
|
|
12
|
+
exit 0
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
# Kiểm tra Allure CLI
|
|
16
|
+
$allureCmd = Get-Command allure -ErrorAction SilentlyContinue
|
|
17
|
+
|
|
18
|
+
if ($allureCmd) {
|
|
19
|
+
Write-Host "[+] Đang xuất báo cáo Allure HTML..." -ForegroundColor Green
|
|
20
|
+
allure generate $resultsDir -o $reportDir --clean
|
|
21
|
+
Write-Host "[+] Xuất báo cáo hoàn tất tại: $reportDir" -ForegroundColor Green
|
|
22
|
+
Write-Host "[+] Đang mở báo cáo trên trình duyệt..." -ForegroundColor Cyan
|
|
23
|
+
allure open $reportDir
|
|
24
|
+
} else {
|
|
25
|
+
Write-Host "[!] Không tìm thấy công cụ Allure CLI trong hệ thống." -ForegroundColor Yellow
|
|
26
|
+
Write-Host "[*] Hướng dẫn cài đặt nhanh:" -ForegroundColor White
|
|
27
|
+
Write-Host " Cách 1: npm install -g allure-commandline" -ForegroundColor Gray
|
|
28
|
+
Write-Host " Cách 2: scoop install allure" -ForegroundColor Gray
|
|
29
|
+
Write-Host " Cách 3: choco install allure-commandline" -ForegroundColor Gray
|
|
30
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
tools/generate_api_matrix.py — OpenAPI / Swagger to E2E Testing Matrix Generator
|
|
5
|
+
Generates Positive, Boundary Value, Negative, and Edge-Case Fuzzing scenarios from API schemas.
|
|
6
|
+
Compatible with Windows, macOS, and Linux.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
import os
|
|
11
|
+
import json
|
|
12
|
+
import argparse
|
|
13
|
+
|
|
14
|
+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
15
|
+
if SCRIPT_DIR not in sys.path:
|
|
16
|
+
sys.path.insert(0, SCRIPT_DIR)
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
from fuzz_payloads import get_field_fuzz_scenarios
|
|
20
|
+
except ImportError:
|
|
21
|
+
get_field_fuzz_scenarios = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def generate_field_boundaries(field_name, field_schema):
|
|
25
|
+
f_type = field_schema.get("type", "string")
|
|
26
|
+
boundaries = []
|
|
27
|
+
|
|
28
|
+
if f_type == "string":
|
|
29
|
+
min_l = field_schema.get("minLength", 1)
|
|
30
|
+
max_l = field_schema.get("maxLength", 50)
|
|
31
|
+
boundaries.append({"scenario": f"{field_name} Empty String", "value": "", "expected": 400})
|
|
32
|
+
boundaries.append({"scenario": f"{field_name} Below MinLength ({min_l - 1} chars)", "value": "A" * max(0, min_l - 1), "expected": 400 if min_l > 0 else 200})
|
|
33
|
+
boundaries.append({"scenario": f"{field_name} Exact MaxLength ({max_l} chars)", "value": "A" * max_l, "expected": 200})
|
|
34
|
+
boundaries.append({"scenario": f"{field_name} Exceed MaxLength ({max_l + 1} chars)", "value": "A" * (max_l + 1), "expected": 400})
|
|
35
|
+
boundaries.append({"scenario": f"{field_name} Special & XSS Payload", "value": "<script>alert(1)</script>", "expected": 400})
|
|
36
|
+
|
|
37
|
+
# --- Enterprise Edge-Case Fuzzing & Input Sanitization Scenarios ---
|
|
38
|
+
if get_field_fuzz_scenarios:
|
|
39
|
+
fuzz_items = get_field_fuzz_scenarios(field_name, "string")
|
|
40
|
+
for item in fuzz_items:
|
|
41
|
+
boundaries.append({
|
|
42
|
+
"scenario": item["scenario"],
|
|
43
|
+
"value": item["value"],
|
|
44
|
+
"expected": item["expected"]
|
|
45
|
+
})
|
|
46
|
+
else:
|
|
47
|
+
boundaries.append({"scenario": f"{field_name} SQLi Polyglot Probe", "value": "' OR '1'='1' -- ", "expected": 400})
|
|
48
|
+
boundaries.append({"scenario": f"{field_name} Unicode BiDi RTL Override", "value": "\u202E\u0041\u0042\u0043_AUTO_TX", "expected": 400})
|
|
49
|
+
boundaries.append({"scenario": f"{field_name} Emoji Surrogate & ZWJ Sequence", "value": "👨👩👧👦_QA_TAG", "expected": 200})
|
|
50
|
+
boundaries.append({"scenario": f"{field_name} Extreme Length 10^4 Buffer Stress", "value": "A" * 10000, "expected": 400})
|
|
51
|
+
boundaries.append({"scenario": f"{field_name} Null Byte Truncation Injection", "value": "data\x00.json", "expected": 400})
|
|
52
|
+
|
|
53
|
+
elif f_type in ("integer", "number"):
|
|
54
|
+
minimum = field_schema.get("minimum", 0)
|
|
55
|
+
maximum = field_schema.get("maximum", 1000)
|
|
56
|
+
boundaries.append({"scenario": f"{field_name} Negative Value ({minimum - 1})", "value": minimum - 1, "expected": 400})
|
|
57
|
+
boundaries.append({"scenario": f"{field_name} Boundary Minimum ({minimum})", "value": minimum, "expected": 200})
|
|
58
|
+
boundaries.append({"scenario": f"{field_name} Boundary Maximum ({maximum})", "value": maximum, "expected": 200})
|
|
59
|
+
boundaries.append({"scenario": f"{field_name} Exceed Maximum ({maximum + 1})", "value": maximum + 1, "expected": 400})
|
|
60
|
+
boundaries.append({"scenario": f"{field_name} 32-bit Integer Overflow (2^31)", "value": 2147483648, "expected": 400})
|
|
61
|
+
|
|
62
|
+
elif f_type == "boolean":
|
|
63
|
+
boundaries.append({"scenario": f"{field_name} Invalid String Instead of Boolean", "value": "not_a_bool", "expected": 400})
|
|
64
|
+
|
|
65
|
+
return boundaries
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def parse_spec(spec_data):
|
|
69
|
+
matrix = []
|
|
70
|
+
paths = spec_data.get("paths", {})
|
|
71
|
+
|
|
72
|
+
for endpoint, methods in paths.items():
|
|
73
|
+
for method, details in methods.items():
|
|
74
|
+
if method.lower() not in ("get", "post", "put", "delete", "patch"):
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
summary = details.get("summary", details.get("operationId", f"{method.upper()} {endpoint}"))
|
|
78
|
+
req_body = details.get("requestBody", {}).get("content", {}).get("application/json", {}).get("schema", {})
|
|
79
|
+
properties = req_body.get("properties", {})
|
|
80
|
+
required_fields = req_body.get("required", [])
|
|
81
|
+
|
|
82
|
+
# 1. Happy Path
|
|
83
|
+
matrix.append({
|
|
84
|
+
"endpoint": endpoint,
|
|
85
|
+
"method": method.upper(),
|
|
86
|
+
"type": "POSITIVE",
|
|
87
|
+
"name": f"TC_API_{method.upper()}_{endpoint.replace('/', '_')}_HappyPath",
|
|
88
|
+
"description": f"Valid payload execution for {summary}",
|
|
89
|
+
"expected_status": 200 if method.lower() == "get" else 201
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
# 2. Missing Required Fields (Negative)
|
|
93
|
+
for req in required_fields:
|
|
94
|
+
matrix.append({
|
|
95
|
+
"endpoint": endpoint,
|
|
96
|
+
"method": method.upper(),
|
|
97
|
+
"type": "NEGATIVE",
|
|
98
|
+
"name": f"TC_API_{method.upper()}_{endpoint.replace('/', '_')}_Missing_{req}",
|
|
99
|
+
"description": f"Submit request omitting required field '{req}'",
|
|
100
|
+
"expected_status": 400
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
# 3. Boundary Values & Edge-Case Fuzzing
|
|
104
|
+
for fname, fschema in properties.items():
|
|
105
|
+
b_list = generate_field_boundaries(fname, fschema)
|
|
106
|
+
for b in b_list:
|
|
107
|
+
clean_scenario = b['scenario'].replace(' ', '_').replace('(', '').replace(')', '').replace('&', 'and')
|
|
108
|
+
val_str = str(b['value'])
|
|
109
|
+
if len(val_str) > 30:
|
|
110
|
+
val_str = val_str[:27] + "..."
|
|
111
|
+
matrix.append({
|
|
112
|
+
"endpoint": endpoint,
|
|
113
|
+
"method": method.upper(),
|
|
114
|
+
"type": "BOUNDARY",
|
|
115
|
+
"name": f"TC_API_{method.upper()}_{endpoint.replace('/', '_')}_{clean_scenario}",
|
|
116
|
+
"description": f"Boundary/Fuzz testing: {b['scenario']} with value '{val_str}'",
|
|
117
|
+
"expected_status": b["expected"]
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
return matrix
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def to_markdown(matrix):
|
|
124
|
+
lines = []
|
|
125
|
+
lines.append("# Automated E2E & API Contract Testing Matrix")
|
|
126
|
+
lines.append("")
|
|
127
|
+
lines.append("| ID | Method | Endpoint | Test Type | Scenario Description | Expected Status |")
|
|
128
|
+
lines.append("| :--- | :---: | :--- | :---: | :--- | :---: |")
|
|
129
|
+
|
|
130
|
+
for idx, tc in enumerate(matrix, 1):
|
|
131
|
+
lines.append(f"| TC-API-{idx:03d} | `{tc['method']}` | `{tc['endpoint']}` | **{tc['type']}** | {tc['description']} | `{tc['expected_status']}` |")
|
|
132
|
+
|
|
133
|
+
return "\n".join(lines)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def main():
|
|
137
|
+
parser = argparse.ArgumentParser(description="Generate E2E Positive, Boundary, and Negative test matrix from OpenAPI/Swagger schema.")
|
|
138
|
+
parser.add_argument("--spec", "-s", help="Path to OpenAPI / Swagger JSON schema file.")
|
|
139
|
+
parser.add_argument("--format", "-f", choices=["md", "json"], default="md", help="Output format (markdown or json).")
|
|
140
|
+
parser.add_argument("--output", "-o", help="Output file path.")
|
|
141
|
+
parser.add_argument("--sample", action="store_true", help="Generate a sample matrix using built-in demonstration schema.")
|
|
142
|
+
args = parser.parse_args()
|
|
143
|
+
|
|
144
|
+
if args.sample or not args.spec:
|
|
145
|
+
sample_spec = {
|
|
146
|
+
"paths": {
|
|
147
|
+
"/api/v1/categories": {
|
|
148
|
+
"post": {
|
|
149
|
+
"summary": "Create new category",
|
|
150
|
+
"requestBody": {
|
|
151
|
+
"content": {
|
|
152
|
+
"application/json": {
|
|
153
|
+
"schema": {
|
|
154
|
+
"type": "object",
|
|
155
|
+
"required": ["code", "name"],
|
|
156
|
+
"properties": {
|
|
157
|
+
"code": {"type": "string", "minLength": 3, "maxLength": 20},
|
|
158
|
+
"name": {"type": "string", "minLength": 1, "maxLength": 100},
|
|
159
|
+
"priority": {"type": "integer", "minimum": 1, "maximum": 100}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
spec_data = sample_spec
|
|
170
|
+
else:
|
|
171
|
+
if not os.path.exists(args.spec):
|
|
172
|
+
print(f"Error: Specification file not found: {args.spec}", file=sys.stderr)
|
|
173
|
+
sys.exit(1)
|
|
174
|
+
with open(args.spec, "r", encoding="utf-8") as fp:
|
|
175
|
+
spec_data = json.load(fp)
|
|
176
|
+
|
|
177
|
+
matrix = parse_spec(spec_data)
|
|
178
|
+
|
|
179
|
+
if args.format == "json":
|
|
180
|
+
result = json.dumps(matrix, indent=2, ensure_ascii=False)
|
|
181
|
+
else:
|
|
182
|
+
result = to_markdown(matrix)
|
|
183
|
+
|
|
184
|
+
if args.output:
|
|
185
|
+
with open(args.output, "w", encoding="utf-8") as fp:
|
|
186
|
+
fp.write(result)
|
|
187
|
+
print(f"✅ Matrix successfully generated to: {args.output} ({len(matrix)} test cases)")
|
|
188
|
+
else:
|
|
189
|
+
print(result)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
if __name__ == "__main__":
|
|
193
|
+
main()
|