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,435 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Omni-Parser: Universal Polyglot AST & Structure Parser
|
|
4
|
+
------------------------------------------------------
|
|
5
|
+
Zero-Token deterministic parser supporting Java, TypeScript/JavaScript,
|
|
6
|
+
Python, Groovy (Katalon), and C#.
|
|
7
|
+
Extracts Page Classes, Locators, Action Methods, and Test Cases into
|
|
8
|
+
the Universal Intermediate Representation (universal_ir.ProjectIR).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
from typing import Dict, List, Optional, Tuple
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from tools.universal_ir import (
|
|
18
|
+
ProjectIR, PageClassIR, TestCaseIR, MethodIR, StepIR, LocatorIR,
|
|
19
|
+
ActionType, LocatorStrategy, AssertionType, WaitStrategy
|
|
20
|
+
)
|
|
21
|
+
except ImportError:
|
|
22
|
+
from universal_ir import (
|
|
23
|
+
ProjectIR, PageClassIR, TestCaseIR, MethodIR, StepIR, LocatorIR,
|
|
24
|
+
ActionType, LocatorStrategy, AssertionType, WaitStrategy
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def extract_quoted_string(match_groups: Tuple[Optional[str], Optional[str]]) -> str:
|
|
29
|
+
"""Helper to return the unescaped string from either double-quote or single-quote match."""
|
|
30
|
+
g1, g2 = match_groups
|
|
31
|
+
val = g1 if g1 else (g2 if g2 else "")
|
|
32
|
+
return val.replace(r"\'", "'").replace(r'\"', '"')
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class OmniParser:
|
|
36
|
+
"""Universal Zero-Token Polyglot Parser for Automation Codebases."""
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def detect_language_and_framework(file_path: str, content: str) -> Tuple[str, str]:
|
|
40
|
+
ext = os.path.splitext(file_path)[1].lower()
|
|
41
|
+
|
|
42
|
+
# Groovy / Katalon
|
|
43
|
+
if ext == ".groovy":
|
|
44
|
+
return "groovy", "katalon"
|
|
45
|
+
|
|
46
|
+
# Java
|
|
47
|
+
if ext == ".java":
|
|
48
|
+
if "playwright" in content.lower():
|
|
49
|
+
return "java", "playwright"
|
|
50
|
+
if "restassured" in content.lower():
|
|
51
|
+
return "java", "restassured"
|
|
52
|
+
return "java", "selenium"
|
|
53
|
+
|
|
54
|
+
# TypeScript / JavaScript
|
|
55
|
+
if ext in [".ts", ".tsx", ".js", ".jsx"]:
|
|
56
|
+
lang = "typescript" if "ts" in ext else "javascript"
|
|
57
|
+
if "@playwright/test" in content or "page.locator" in content or "getByRole" in content:
|
|
58
|
+
return lang, "playwright"
|
|
59
|
+
if "cy." in content or "cypress" in content.lower():
|
|
60
|
+
return lang, "cypress"
|
|
61
|
+
if "webdriverio" in content.lower() or "browser.$" in content:
|
|
62
|
+
return lang, "webdriverio"
|
|
63
|
+
return lang, "playwright"
|
|
64
|
+
|
|
65
|
+
# Python
|
|
66
|
+
if ext == ".py":
|
|
67
|
+
if "playwright" in content:
|
|
68
|
+
return "python", "playwright"
|
|
69
|
+
if "selenium" in content:
|
|
70
|
+
return "python", "selenium"
|
|
71
|
+
if "robot" in content.lower():
|
|
72
|
+
return "python", "robot"
|
|
73
|
+
return "python", "pytest"
|
|
74
|
+
|
|
75
|
+
# C#
|
|
76
|
+
if ext == ".cs":
|
|
77
|
+
if "playwright" in content.lower():
|
|
78
|
+
return "csharp", "playwright"
|
|
79
|
+
return "csharp", "selenium"
|
|
80
|
+
|
|
81
|
+
return "unknown", "generic"
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def parse_file(cls, file_path: str) -> ProjectIR:
|
|
85
|
+
if not os.path.exists(file_path):
|
|
86
|
+
raise FileNotFoundError(f"Source file not found: {file_path}")
|
|
87
|
+
|
|
88
|
+
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
|
89
|
+
content = f.read()
|
|
90
|
+
|
|
91
|
+
lang, framework = cls.detect_language_and_framework(file_path, content)
|
|
92
|
+
basename = os.path.basename(file_path)
|
|
93
|
+
project = ProjectIR(
|
|
94
|
+
name=os.path.splitext(basename)[0],
|
|
95
|
+
source_language=lang,
|
|
96
|
+
source_framework=framework
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if lang in ["typescript", "javascript"]:
|
|
100
|
+
cls._parse_ts_js(content, project, framework)
|
|
101
|
+
elif lang == "java":
|
|
102
|
+
cls._parse_java(content, project, framework)
|
|
103
|
+
elif lang == "groovy":
|
|
104
|
+
cls._parse_groovy(content, project, framework)
|
|
105
|
+
elif lang == "python":
|
|
106
|
+
cls._parse_python(content, project, framework)
|
|
107
|
+
elif lang == "csharp":
|
|
108
|
+
cls._parse_csharp(content, project, framework)
|
|
109
|
+
else:
|
|
110
|
+
cls._parse_generic(content, project)
|
|
111
|
+
|
|
112
|
+
return project
|
|
113
|
+
|
|
114
|
+
# -------------------------------------------------------------------------
|
|
115
|
+
# TypeScript / JavaScript Parser
|
|
116
|
+
# -------------------------------------------------------------------------
|
|
117
|
+
@classmethod
|
|
118
|
+
def _parse_ts_js(cls, content: str, project: ProjectIR, framework: str):
|
|
119
|
+
class_match = re.search(r"class\s+([A-Za-z0-9_]+)(?:\s+extends\s+([A-Za-z0-9_]+))?", content)
|
|
120
|
+
page_name = class_match.group(1) if class_match else f"{project.name}Page"
|
|
121
|
+
base_class = class_match.group(2) if (class_match and class_match.group(2)) else "BasePage"
|
|
122
|
+
|
|
123
|
+
page = PageClassIR(name=page_name, base_class=base_class)
|
|
124
|
+
locators: Dict[str, LocatorIR] = {}
|
|
125
|
+
|
|
126
|
+
# 1. Playwright getByRole
|
|
127
|
+
role_matches = re.finditer(
|
|
128
|
+
r"(?:readonly\s+)?(?:this\.)?([A-Za-z0-9_$]+)\s*=\s*(?:this\.)?page\.getByRole\(\s*(?:\"([^\"]*)\"|'([^']*)')\s*,\s*\{\s*name:\s*(?:\"([^\"]*)\"|'([^']*)')\s*\}\s*\)",
|
|
129
|
+
content
|
|
130
|
+
)
|
|
131
|
+
for m in role_matches:
|
|
132
|
+
loc_name = m.group(1)
|
|
133
|
+
role_type = extract_quoted_string((m.group(2), m.group(3)))
|
|
134
|
+
acc_name = extract_quoted_string((m.group(4), m.group(5)))
|
|
135
|
+
locators[loc_name] = LocatorIR(name=loc_name, strategy=LocatorStrategy.ROLE, value=f"{role_type}[name='{acc_name}']")
|
|
136
|
+
|
|
137
|
+
# 2. Playwright getByTestId
|
|
138
|
+
tid_matches = re.finditer(
|
|
139
|
+
r"(?:readonly\s+)?(?:this\.)?([A-Za-z0-9_$]+)\s*=\s*(?:this\.)?page\.getByTestId\(\s*(?:\"([^\"]*)\"|'([^']*)')\s*\)",
|
|
140
|
+
content
|
|
141
|
+
)
|
|
142
|
+
for m in tid_matches:
|
|
143
|
+
loc_name = m.group(1)
|
|
144
|
+
val = extract_quoted_string((m.group(2), m.group(3)))
|
|
145
|
+
locators[loc_name] = LocatorIR(name=loc_name, strategy=LocatorStrategy.TEST_ID, value=val)
|
|
146
|
+
|
|
147
|
+
# 3. Playwright getByLabel
|
|
148
|
+
label_matches = re.finditer(
|
|
149
|
+
r"(?:readonly\s+)?(?:this\.)?([A-Za-z0-9_$]+)\s*=\s*(?:this\.)?page\.getByLabel\(\s*(?:\"([^\"]*)\"|'([^']*)')\s*\)",
|
|
150
|
+
content
|
|
151
|
+
)
|
|
152
|
+
for m in label_matches:
|
|
153
|
+
loc_name = m.group(1)
|
|
154
|
+
val = extract_quoted_string((m.group(2), m.group(3)))
|
|
155
|
+
locators[loc_name] = LocatorIR(name=loc_name, strategy=LocatorStrategy.LABEL, value=val)
|
|
156
|
+
|
|
157
|
+
# 4. Playwright page.locator('...')
|
|
158
|
+
loc_matches = re.finditer(
|
|
159
|
+
r"""(?:readonly\s+)?(?:this\.)?([A-Za-z0-9_$]+)\s*=\s*(?:this\.)?page\.locator\(\s*(?:\"((?:[^"\\]|\\.)*)\"|'((?:[^'\\]|\\.)*)')\s*\)""",
|
|
160
|
+
content
|
|
161
|
+
)
|
|
162
|
+
for m in loc_matches:
|
|
163
|
+
loc_name = m.group(1)
|
|
164
|
+
selector = extract_quoted_string((m.group(2), m.group(3)))
|
|
165
|
+
strat = LocatorStrategy.XPATH if selector.startswith(("//", "(//")) else LocatorStrategy.CSS
|
|
166
|
+
locators[loc_name] = LocatorIR(name=loc_name, strategy=strat, value=selector)
|
|
167
|
+
|
|
168
|
+
page.locators = list(locators.values())
|
|
169
|
+
|
|
170
|
+
# Methods
|
|
171
|
+
method_blocks = re.findall(r"(async\s+)?([A-Za-z0-9_$]+)\s*\(([^)]*)\)\s*(?::\s*[^\{]+)?\s*\{([^}]+)\}", content, re.MULTILINE)
|
|
172
|
+
for _, m_name, params_str, body in method_blocks:
|
|
173
|
+
if m_name in ["constructor"]:
|
|
174
|
+
continue
|
|
175
|
+
params = [p.strip().split(":")[0].strip() for p in params_str.split(",") if p.strip()]
|
|
176
|
+
steps = cls._extract_steps_from_body(body)
|
|
177
|
+
page.methods.append(MethodIR(name=m_name, parameters=params, steps=steps))
|
|
178
|
+
|
|
179
|
+
project.pages.append(page)
|
|
180
|
+
|
|
181
|
+
# Test cases (Playwright `test(...)` or Cypress / Mocha `it(...)`)
|
|
182
|
+
test_blocks = re.findall(
|
|
183
|
+
r"(?:test|it)(?:\.only|\.skip)?\s*\(\s*(?:\"([^\"]*)\"|'([^']*)')\s*,\s*(?:async\s*)?(?:\([^)]*\)|function\s*\([^)]*\))\s*=>?\s*\{([^}]+)\}",
|
|
184
|
+
content,
|
|
185
|
+
re.MULTILINE
|
|
186
|
+
)
|
|
187
|
+
for idx, (t_m1, t_m2, t_body) in enumerate(test_blocks, 1):
|
|
188
|
+
t_title = t_m1 if t_m1 else t_m2
|
|
189
|
+
steps = cls._extract_steps_from_body(t_body)
|
|
190
|
+
project.test_cases.append(TestCaseIR(id=f"TC_{idx:03d}", title=t_title, steps=steps))
|
|
191
|
+
|
|
192
|
+
# -------------------------------------------------------------------------
|
|
193
|
+
# Java Parser (Selenium & Playwright)
|
|
194
|
+
# -------------------------------------------------------------------------
|
|
195
|
+
@classmethod
|
|
196
|
+
def _parse_java(cls, content: str, project: ProjectIR, framework: str):
|
|
197
|
+
class_match = re.search(r"public\s+class\s+([A-Za-z0-9_]+)(?:\s+extends\s+([A-Za-z0-9_]+))?", content)
|
|
198
|
+
page_name = class_match.group(1) if class_match else f"{project.name}Page"
|
|
199
|
+
base_class = class_match.group(2) if (class_match and class_match.group(2)) else "BasePage"
|
|
200
|
+
|
|
201
|
+
page = PageClassIR(name=page_name, base_class=base_class)
|
|
202
|
+
locators: Dict[str, LocatorIR] = {}
|
|
203
|
+
|
|
204
|
+
# 1. Selenium @FindBy annotations
|
|
205
|
+
findby_matches = re.finditer(
|
|
206
|
+
r"@FindBy\s*\(\s*([a-zA-Z0-9_]+)\s*=\s*(?:\"([^\"]*)\"|'([^']*)')\s*\)\s*(?:(?:private|public|protected|static|final)\s+)*(?:WebElement|List<WebElement>)\s+([A-Za-z0-9_]+);",
|
|
207
|
+
content
|
|
208
|
+
)
|
|
209
|
+
for m in findby_matches:
|
|
210
|
+
by_type = m.group(1).lower()
|
|
211
|
+
selector = extract_quoted_string((m.group(2), m.group(3)))
|
|
212
|
+
var_name = m.group(4)
|
|
213
|
+
strat = LocatorStrategy.XPATH if by_type == "xpath" else (LocatorStrategy.ID if by_type == "id" else LocatorStrategy.CSS)
|
|
214
|
+
locators[var_name] = LocatorIR(name=var_name, strategy=strat, value=selector)
|
|
215
|
+
|
|
216
|
+
# 2. By locators
|
|
217
|
+
by_matches = re.finditer(
|
|
218
|
+
r"(?:(?:private|public|protected|static|final)\s+)*By\s+([A-Za-z0-9_]+)\s*=\s*By\.([a-zA-Z0-9_]+)\s*\(\s*(?:\"([^\"]*)\"|'([^']*)')\s*\);",
|
|
219
|
+
content
|
|
220
|
+
)
|
|
221
|
+
for m in by_matches:
|
|
222
|
+
var_name = m.group(1)
|
|
223
|
+
by_type = m.group(2).lower()
|
|
224
|
+
selector = extract_quoted_string((m.group(3), m.group(4)))
|
|
225
|
+
strat = LocatorStrategy.XPATH if "xpath" in by_type else (LocatorStrategy.ID if "id" in by_type else LocatorStrategy.CSS)
|
|
226
|
+
locators[var_name] = LocatorIR(name=var_name, strategy=strat, value=selector)
|
|
227
|
+
|
|
228
|
+
page.locators = list(locators.values())
|
|
229
|
+
|
|
230
|
+
# 3. Methods
|
|
231
|
+
method_blocks = re.findall(r"(?:public|protected|private)\s+([A-Za-z0-9_<>\s]+)\s+([A-Za-z0-9_]+)\s*\(([^)]*)\)\s*(?:throws\s+[A-Za-z0-9_,\s]+)?\s*\{([^}]+)\}", content, re.MULTILINE)
|
|
232
|
+
for ret_type, m_name, params_str, body in method_blocks:
|
|
233
|
+
if m_name == page_name:
|
|
234
|
+
continue
|
|
235
|
+
params = [p.strip().split()[-1] for p in params_str.split(",") if p.strip()]
|
|
236
|
+
steps = cls._extract_steps_from_body(body)
|
|
237
|
+
page.methods.append(MethodIR(name=m_name, parameters=params, return_type=ret_type.strip(), steps=steps))
|
|
238
|
+
|
|
239
|
+
project.pages.append(page)
|
|
240
|
+
|
|
241
|
+
# 4. TestNG / JUnit
|
|
242
|
+
test_matches = re.findall(r"@Test[^\n]*\s*(?:public|void|\s)+([A-Za-z0-9_]+)\s*\([^)]*\)[^{]*\{([^}]+)\}", content, re.MULTILINE)
|
|
243
|
+
for idx, (t_name, t_body) in enumerate(test_matches, 1):
|
|
244
|
+
steps = cls._extract_steps_from_body(t_body)
|
|
245
|
+
project.test_cases.append(TestCaseIR(id=f"TC_{idx:03d}", title=t_name, steps=steps))
|
|
246
|
+
|
|
247
|
+
# -------------------------------------------------------------------------
|
|
248
|
+
# Groovy / Katalon Parser (Clean POM Flat Architecture)
|
|
249
|
+
# -------------------------------------------------------------------------
|
|
250
|
+
@classmethod
|
|
251
|
+
def _parse_groovy(cls, content: str, project: ProjectIR, framework: str):
|
|
252
|
+
class_match = re.search(r"class\s+([A-Za-z0-9_]+)(?:\s+extends\s+([A-Za-z0-9_]+))?", content)
|
|
253
|
+
page_name = class_match.group(1) if class_match else f"{project.name}Page"
|
|
254
|
+
base_class = class_match.group(2) if (class_match and class_match.group(2)) else "BasePage"
|
|
255
|
+
|
|
256
|
+
page = PageClassIR(name=page_name, base_class=base_class)
|
|
257
|
+
locators: Dict[str, LocatorIR] = {}
|
|
258
|
+
|
|
259
|
+
# Extract static final String XPaths
|
|
260
|
+
xpath_matches = re.finditer(
|
|
261
|
+
r"(?:(?:public|private|protected|static|final)\s+)*(?:String|def)\s+([A-Za-z0-9_]+)\s*=\s*(?:\"([^\"]*)\"|'([^']*)')",
|
|
262
|
+
content
|
|
263
|
+
)
|
|
264
|
+
for m in xpath_matches:
|
|
265
|
+
var_name = m.group(1)
|
|
266
|
+
selector = extract_quoted_string((m.group(2), m.group(3)))
|
|
267
|
+
strat = LocatorStrategy.XPATH if selector.startswith(("//", "(//")) else LocatorStrategy.CSS
|
|
268
|
+
locators[var_name] = LocatorIR(name=var_name, strategy=strat, value=selector)
|
|
269
|
+
|
|
270
|
+
page.locators = list(locators.values())
|
|
271
|
+
|
|
272
|
+
# Methods
|
|
273
|
+
method_blocks = re.findall(r"(?:(?:public|private|protected|def|static|void|[A-Za-z0-9_<>]+)\s+)+([A-Za-z0-9_]+)\s*\(([^)]*)\)\s*\{([^}]+)\}", content, re.MULTILINE)
|
|
274
|
+
for m_name, params_str, body in method_blocks:
|
|
275
|
+
if m_name in [page_name, "main"]:
|
|
276
|
+
continue
|
|
277
|
+
params = [p.strip().split()[-1] for p in params_str.split(",") if p.strip()]
|
|
278
|
+
steps = cls._extract_steps_from_body(body)
|
|
279
|
+
page.methods.append(MethodIR(name=m_name, parameters=params, steps=steps))
|
|
280
|
+
|
|
281
|
+
project.pages.append(page)
|
|
282
|
+
|
|
283
|
+
if not class_match:
|
|
284
|
+
steps = cls._extract_steps_from_body(content)
|
|
285
|
+
project.test_cases.append(TestCaseIR(id="TC_001", title=project.name, steps=steps))
|
|
286
|
+
|
|
287
|
+
# -------------------------------------------------------------------------
|
|
288
|
+
# Python Parser (Pytest / Playwright / Selenium)
|
|
289
|
+
# -------------------------------------------------------------------------
|
|
290
|
+
@classmethod
|
|
291
|
+
def _parse_python(cls, content: str, project: ProjectIR, framework: str):
|
|
292
|
+
class_match = re.search(r"class\s+([A-Za-z0-9_]+)(?:\(([^)]+)\))?:", content)
|
|
293
|
+
page_name = class_match.group(1) if class_match else f"{project.name}Page"
|
|
294
|
+
base_class = class_match.group(2) if (class_match and class_match.group(2)) else "BasePage"
|
|
295
|
+
|
|
296
|
+
page = PageClassIR(name=page_name, base_class=base_class)
|
|
297
|
+
locators: Dict[str, LocatorIR] = {}
|
|
298
|
+
|
|
299
|
+
loc_matches = re.finditer(
|
|
300
|
+
r"self\.([a-zA-Z0-9_]+)\s*=\s*(?:self\.)?page\.locator\(\s*(?:\"([^\"]*)\"|'([^']*)')\s*\)",
|
|
301
|
+
content
|
|
302
|
+
)
|
|
303
|
+
for m in loc_matches:
|
|
304
|
+
name = m.group(1)
|
|
305
|
+
sel = extract_quoted_string((m.group(2), m.group(3)))
|
|
306
|
+
strat = LocatorStrategy.XPATH if sel.startswith(("//", "(//")) else LocatorStrategy.CSS
|
|
307
|
+
locators[name] = LocatorIR(name=name, strategy=strat, value=sel)
|
|
308
|
+
|
|
309
|
+
role_matches = re.finditer(
|
|
310
|
+
r"self\.([a-zA-Z0-9_]+)\s*=\s*(?:self\.)?page\.get_by_role\(\s*(?:\"([^\"]*)\"|'([^']*)')\s*,\s*name=(?:\"([^\"]*)\"|'([^']*)')\s*\)",
|
|
311
|
+
content
|
|
312
|
+
)
|
|
313
|
+
for m in role_matches:
|
|
314
|
+
name = m.group(1)
|
|
315
|
+
r_type = extract_quoted_string((m.group(2), m.group(3)))
|
|
316
|
+
r_name = extract_quoted_string((m.group(4), m.group(5)))
|
|
317
|
+
locators[name] = LocatorIR(name=name, strategy=LocatorStrategy.ROLE, value=f"{r_type}[name='{r_name}']")
|
|
318
|
+
|
|
319
|
+
page.locators = list(locators.values())
|
|
320
|
+
|
|
321
|
+
func_blocks = re.findall(r"def\s+([A-Za-z0-9_]+)\s*\(([^)]*)\)\s*(?:->\s*[^:]+)?:([^d]+(?=def\s+|$))", content, re.MULTILINE)
|
|
322
|
+
for f_name, f_params, f_body in func_blocks:
|
|
323
|
+
if f_name.startswith("test_"):
|
|
324
|
+
steps = cls._extract_steps_from_body(f_body)
|
|
325
|
+
project.test_cases.append(TestCaseIR(id=f"TC_{len(project.test_cases)+1:03d}", title=f_name, steps=steps))
|
|
326
|
+
elif f_name != "__init__":
|
|
327
|
+
params = [p.strip().split(":")[0].strip() for p in f_params.split(",") if p.strip() and p.strip() != "self"]
|
|
328
|
+
steps = cls._extract_steps_from_body(f_body)
|
|
329
|
+
page.methods.append(MethodIR(name=f_name, parameters=params, steps=steps))
|
|
330
|
+
|
|
331
|
+
project.pages.append(page)
|
|
332
|
+
|
|
333
|
+
# -------------------------------------------------------------------------
|
|
334
|
+
# C# Parser (.NET Selenium / Playwright)
|
|
335
|
+
# -------------------------------------------------------------------------
|
|
336
|
+
@classmethod
|
|
337
|
+
def _parse_csharp(cls, content: str, project: ProjectIR, framework: str):
|
|
338
|
+
class_match = re.search(r"class\s+([A-Za-z0-9_]+)(?:\s*:\s*([A-Za-z0-9_]+))?", content)
|
|
339
|
+
page_name = class_match.group(1) if class_match else f"{project.name}Page"
|
|
340
|
+
base_class = class_match.group(2) if (class_match and class_match.group(2)) else "BasePage"
|
|
341
|
+
|
|
342
|
+
page = PageClassIR(name=page_name, base_class=base_class)
|
|
343
|
+
project.pages.append(page)
|
|
344
|
+
|
|
345
|
+
# -------------------------------------------------------------------------
|
|
346
|
+
# Generic Fallback Parser
|
|
347
|
+
# -------------------------------------------------------------------------
|
|
348
|
+
@classmethod
|
|
349
|
+
def _parse_generic(cls, content: str, project: ProjectIR):
|
|
350
|
+
page = PageClassIR(name=f"{project.name}Page")
|
|
351
|
+
steps = cls._extract_steps_from_body(content)
|
|
352
|
+
project.test_cases.append(TestCaseIR(id="TC_001", title=project.name, steps=steps))
|
|
353
|
+
project.pages.append(page)
|
|
354
|
+
|
|
355
|
+
# -------------------------------------------------------------------------
|
|
356
|
+
# Universal Step Extractor (With Async/Sync Bridge & Antipattern Sanitizer)
|
|
357
|
+
# -------------------------------------------------------------------------
|
|
358
|
+
@classmethod
|
|
359
|
+
def _extract_steps_from_body(cls, body: str) -> List[StepIR]:
|
|
360
|
+
steps: List[StepIR] = []
|
|
361
|
+
# Unroll Cypress .then() closures into sequential lines
|
|
362
|
+
normalized_body = re.sub(r"\.then\s*\(\s*\([^)]*\)\s*=>\s*\{?", "\n", body)
|
|
363
|
+
|
|
364
|
+
for line in normalized_body.splitlines():
|
|
365
|
+
line_str = line.strip()
|
|
366
|
+
if not line_str or line_str.startswith(("//", "#", "/*", "*")) or line_str in ["}", "});"]:
|
|
367
|
+
continue
|
|
368
|
+
|
|
369
|
+
# 0. Detect Hardcoded Sleep Antipatterns (Thread.sleep, WebUI.delay, cy.wait, time.sleep)
|
|
370
|
+
sleep_m = re.search(r"(?:Thread\.sleep|WebUI\.delay|time\.sleep|cy\.wait)\s*\(\s*(\d+)\s*\)", line_str, re.IGNORECASE)
|
|
371
|
+
if sleep_m:
|
|
372
|
+
val = int(sleep_m.group(1))
|
|
373
|
+
ms = val * 1000 if val <= 60 else val # WebUI.delay(3) vs Thread.sleep(3000)
|
|
374
|
+
steps.append(StepIR(
|
|
375
|
+
action=ActionType.WAIT,
|
|
376
|
+
wait_strategy=WaitStrategy.STATIC_SLEEP,
|
|
377
|
+
is_sanitized_wait=True,
|
|
378
|
+
timeout_ms=ms,
|
|
379
|
+
description=f"Auto-healed from static sleep {line_str}"
|
|
380
|
+
))
|
|
381
|
+
continue
|
|
382
|
+
|
|
383
|
+
# 1. Cypress Command Chaining: cy.get('selector').action(...)
|
|
384
|
+
cy_chain = re.search(r"cy\.get\(\s*(?:\"([^\"]*)\"|'([^']*)')\s*\)\s*\.\s*(click|type|clear|should)\s*\(([^)]*)\)", line_str, re.IGNORECASE)
|
|
385
|
+
if cy_chain:
|
|
386
|
+
sel = extract_quoted_string((cy_chain.group(1), cy_chain.group(2)))
|
|
387
|
+
cy_act = cy_chain.group(3).lower()
|
|
388
|
+
cy_args = cy_chain.group(4)
|
|
389
|
+
if cy_act == "click":
|
|
390
|
+
steps.append(StepIR(action=ActionType.CLICK, target=sel, description=f"Cypress click on {sel}"))
|
|
391
|
+
elif cy_act in ["type", "clear"]:
|
|
392
|
+
type_val = extract_quoted_string(re.search(r"(?:\"([^\"]*)\"|'([^']*)')", cy_args).groups()) if re.search(r"(?:\"([^\"]*)\"|'([^']*)')", cy_args) else ""
|
|
393
|
+
steps.append(StepIR(action=ActionType.FILL, target=sel, value=type_val, description=f"Cypress type on {sel}"))
|
|
394
|
+
elif cy_act == "should":
|
|
395
|
+
steps.append(StepIR(action=ActionType.ASSERT, target=sel, assertion_type=AssertionType.VISIBLE, description=line_str))
|
|
396
|
+
continue
|
|
397
|
+
|
|
398
|
+
# 2. Navigation
|
|
399
|
+
nav_m = re.search(r"(?:goto|navigate|openBrowser|navigateToUrl|visit|get)\s*\(\s*(?:\"([^\"]*)\"|'([^']*)')\s*\)", line_str, re.IGNORECASE)
|
|
400
|
+
if nav_m:
|
|
401
|
+
val = extract_quoted_string((nav_m.group(1), nav_m.group(2)))
|
|
402
|
+
steps.append(StepIR(action=ActionType.GOTO, value=val, description=f"Navigate to {val}"))
|
|
403
|
+
continue
|
|
404
|
+
|
|
405
|
+
# 3. Click
|
|
406
|
+
click_m = re.search(r"(?:click|clickJS)\s*\(\s*(?:(?:\"([^\"]*)\"|'([^']*)')|([A-Za-z0-9_$.]+))?\s*\)", line_str, re.IGNORECASE)
|
|
407
|
+
if click_m:
|
|
408
|
+
target = extract_quoted_string((click_m.group(1), click_m.group(2))) or click_m.group(3) or "element"
|
|
409
|
+
target_clean = target.replace("this.", "").replace("self.", "")
|
|
410
|
+
steps.append(StepIR(action=ActionType.CLICK, target=target_clean, description=f"Click on {target_clean}"))
|
|
411
|
+
continue
|
|
412
|
+
|
|
413
|
+
# 4. Fill
|
|
414
|
+
fill_m = re.search(r"(?:fill|sendKeys|setText|type)\s*\(\s*(?:([A-Za-z0-9_$.]+)\s*,\s*)?(?:\"([^\"]*)\"|'([^']*)')\s*\)", line_str, re.IGNORECASE)
|
|
415
|
+
if fill_m:
|
|
416
|
+
target = fill_m.group(1) or "input"
|
|
417
|
+
val = extract_quoted_string((fill_m.group(2), fill_m.group(3)))
|
|
418
|
+
target_clean = target.replace("this.", "").replace("self.", "")
|
|
419
|
+
steps.append(StepIR(action=ActionType.FILL, target=target_clean, value=val, description=f"Fill {val} into {target_clean}"))
|
|
420
|
+
continue
|
|
421
|
+
|
|
422
|
+
# 5. Assertions
|
|
423
|
+
if any(k in line_str for k in ["expect", "Assert.", "verify", "should("]):
|
|
424
|
+
if "visible" in line_str.lower() or "present" in line_str.lower():
|
|
425
|
+
steps.append(StepIR(action=ActionType.ASSERT, assertion_type=AssertionType.VISIBLE, description=line_str))
|
|
426
|
+
elif "equal" in line_str.lower() or "match" in line_str.lower():
|
|
427
|
+
steps.append(StepIR(action=ActionType.ASSERT, assertion_type=AssertionType.EQUALS, description=line_str))
|
|
428
|
+
else:
|
|
429
|
+
steps.append(StepIR(action=ActionType.ASSERT, description=line_str))
|
|
430
|
+
continue
|
|
431
|
+
|
|
432
|
+
if "(" in line_str and ")" in line_str:
|
|
433
|
+
steps.append(StepIR(action=ActionType.CUSTOM_CALL, description=line_str))
|
|
434
|
+
|
|
435
|
+
return steps
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Omni-Transpiler & Repository Scanner
|
|
4
|
+
-----------------------------------
|
|
5
|
+
Connects OmniParser and OmniEmitter to provide:
|
|
6
|
+
1. Instant 0-token transpilation between test automation frameworks.
|
|
7
|
+
2. Recursive repo scanner that inventories Page Classes, Locators, and Test Cases
|
|
8
|
+
across polyglot codebases (Java, Groovy, TS, Python, C#).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import json
|
|
14
|
+
import argparse
|
|
15
|
+
from typing import Dict, List, Any, Optional
|
|
16
|
+
|
|
17
|
+
# Dynamic imports
|
|
18
|
+
try:
|
|
19
|
+
from tools.universal_ir import ProjectIR, resolve_hermetic_fixtures, analyze_data_dependencies
|
|
20
|
+
from tools.omni_parser import OmniParser
|
|
21
|
+
from tools.omni_emitter import OmniEmitter
|
|
22
|
+
except ImportError:
|
|
23
|
+
from universal_ir import ProjectIR, resolve_hermetic_fixtures, analyze_data_dependencies
|
|
24
|
+
from omni_parser import OmniParser
|
|
25
|
+
from omni_emitter import OmniEmitter
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def transpile_file(
|
|
29
|
+
source_path: str,
|
|
30
|
+
target_framework: str,
|
|
31
|
+
output_dir: Optional[str] = None,
|
|
32
|
+
hermetic: bool = True
|
|
33
|
+
) -> Dict[str, str]:
|
|
34
|
+
"""Transpiles a single automation source file to the target framework."""
|
|
35
|
+
if not os.path.exists(source_path):
|
|
36
|
+
raise FileNotFoundError(f"Source file not found: {source_path}")
|
|
37
|
+
|
|
38
|
+
# 1. Parse source with 0-token deterministic parser
|
|
39
|
+
project_ir = OmniParser.parse_file(source_path)
|
|
40
|
+
|
|
41
|
+
# 2. Resolve data dependencies & inject hermetic pre-flight fixtures
|
|
42
|
+
if hermetic:
|
|
43
|
+
project_ir = resolve_hermetic_fixtures(project_ir)
|
|
44
|
+
|
|
45
|
+
# 3. Emit target code
|
|
46
|
+
emitted_files = OmniEmitter.emit(project_ir, target_framework)
|
|
47
|
+
|
|
48
|
+
# 4. Write to disk if output_dir specified
|
|
49
|
+
if output_dir:
|
|
50
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
51
|
+
for rel_path, content in emitted_files.items():
|
|
52
|
+
full_out_path = os.path.join(output_dir, rel_path)
|
|
53
|
+
os.makedirs(os.path.dirname(full_out_path), exist_ok=True)
|
|
54
|
+
with open(full_out_path, "w", encoding="utf-8") as f:
|
|
55
|
+
f.write(content)
|
|
56
|
+
|
|
57
|
+
return emitted_files
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def scan_repository(repo_path: str) -> Dict[str, Any]:
|
|
61
|
+
"""
|
|
62
|
+
Recursively scans a repository and builds a unified polyglot inventory
|
|
63
|
+
of all Page Objects, Locators, and Test Cases with 0 tokens.
|
|
64
|
+
"""
|
|
65
|
+
if not os.path.exists(repo_path):
|
|
66
|
+
raise FileNotFoundError(f"Path does not exist: {repo_path}")
|
|
67
|
+
|
|
68
|
+
inventory = {
|
|
69
|
+
"repository": os.path.abspath(repo_path),
|
|
70
|
+
"total_files_scanned": 0,
|
|
71
|
+
"languages": {},
|
|
72
|
+
"frameworks": {},
|
|
73
|
+
"pages": [],
|
|
74
|
+
"test_cases": [],
|
|
75
|
+
"total_locators": 0,
|
|
76
|
+
"total_methods": 0
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
test_extensions = {".java", ".ts", ".js", ".groovy", ".py", ".cs"}
|
|
80
|
+
exclude_dirs = {"node_modules", ".git", ".gradle", "build", "target", "dist", ".gemini", "__pycache__"}
|
|
81
|
+
|
|
82
|
+
for root, dirs, files in os.walk(repo_path):
|
|
83
|
+
dirs[:] = [d for d in dirs if d not in exclude_dirs]
|
|
84
|
+
for f in files:
|
|
85
|
+
ext = os.path.splitext(f)[1].lower()
|
|
86
|
+
if ext in test_extensions:
|
|
87
|
+
file_full = os.path.join(root, f)
|
|
88
|
+
try:
|
|
89
|
+
p_ir = OmniParser.parse_file(file_full)
|
|
90
|
+
inventory["total_files_scanned"] += 1
|
|
91
|
+
|
|
92
|
+
# Language counts
|
|
93
|
+
inventory["languages"][p_ir.source_language] = inventory["languages"].get(p_ir.source_language, 0) + 1
|
|
94
|
+
inventory["frameworks"][p_ir.source_framework] = inventory["frameworks"].get(p_ir.source_framework, 0) + 1
|
|
95
|
+
|
|
96
|
+
for p in p_ir.pages:
|
|
97
|
+
if p.locators or p.methods:
|
|
98
|
+
inventory["pages"].append({
|
|
99
|
+
"name": p.name,
|
|
100
|
+
"file": os.path.relpath(file_full, repo_path).replace("\\", "/"),
|
|
101
|
+
"locators_count": len(p.locators),
|
|
102
|
+
"methods_count": len(p.methods)
|
|
103
|
+
})
|
|
104
|
+
inventory["total_locators"] += len(p.locators)
|
|
105
|
+
inventory["total_methods"] += len(p.methods)
|
|
106
|
+
|
|
107
|
+
for tc in p_ir.test_cases:
|
|
108
|
+
inventory["test_cases"].append({
|
|
109
|
+
"id": tc.id,
|
|
110
|
+
"title": tc.title,
|
|
111
|
+
"file": os.path.relpath(file_full, repo_path).replace("\\", "/"),
|
|
112
|
+
"steps_count": len(tc.steps)
|
|
113
|
+
})
|
|
114
|
+
except Exception:
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
return inventory
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def main():
|
|
121
|
+
parser = argparse.ArgumentParser(description="Omni-Transpiler & Polyglot Test Inventory Scanner")
|
|
122
|
+
sub = parser.add_subparsers(dest="cmd")
|
|
123
|
+
|
|
124
|
+
# transpile command
|
|
125
|
+
p_trans = sub.add_parser("transpile", help="Transpile test files across languages and frameworks")
|
|
126
|
+
p_trans.add_argument("source", help="Source file path")
|
|
127
|
+
p_trans.add_argument("--to", required=True, choices=OmniEmitter.SUPPORTED_TARGETS, help="Target framework")
|
|
128
|
+
p_trans.add_argument("--out", default=None, help="Output directory")
|
|
129
|
+
p_trans.add_argument("--no-hermetic", action="store_true", help="Disable automatic hermetic data dependency resolution")
|
|
130
|
+
|
|
131
|
+
# scan command
|
|
132
|
+
p_scan = sub.add_parser("scan", help="Scan repository and build polyglot test inventory")
|
|
133
|
+
p_scan.add_argument("path", default=".", nargs="?", help="Directory to scan")
|
|
134
|
+
p_scan.add_argument("--json", action="store_true", help="Output raw JSON")
|
|
135
|
+
|
|
136
|
+
args = parser.parse_args()
|
|
137
|
+
|
|
138
|
+
if args.cmd == "transpile":
|
|
139
|
+
res = transpile_file(args.source, args.to, args.out, hermetic=not args.no_hermetic)
|
|
140
|
+
print(f"✅ Successfully transpiled '{args.source}' -> {args.to} ({len(res)} files generated):")
|
|
141
|
+
for p, code in res.items():
|
|
142
|
+
print(f" • {p} ({len(code.splitlines())} lines)")
|
|
143
|
+
if not args.out:
|
|
144
|
+
print("---")
|
|
145
|
+
print(code)
|
|
146
|
+
print("---")
|
|
147
|
+
|
|
148
|
+
elif args.cmd == "scan":
|
|
149
|
+
inv = scan_repository(args.path)
|
|
150
|
+
if args.json:
|
|
151
|
+
print(json.dumps(inv, indent=2))
|
|
152
|
+
else:
|
|
153
|
+
print("======================================================================")
|
|
154
|
+
print(" OMNI-AUTOMATION POLYGLOT REPOSITORY INVENTORY (0-TOKEN AST SCAN)")
|
|
155
|
+
print("======================================================================")
|
|
156
|
+
print(f"📁 Target Directory : {inv['repository']}")
|
|
157
|
+
print(f"📄 Scanned Files : {inv['total_files_scanned']}")
|
|
158
|
+
print(f"🌐 Languages Detected : {inv['languages']}")
|
|
159
|
+
print(f"⚡ Frameworks : {inv['frameworks']}")
|
|
160
|
+
print(f"🏛️ Page Classes Found : {len(inv['pages'])}")
|
|
161
|
+
print(f"🎯 Total Locators : {inv['total_locators']}")
|
|
162
|
+
print(f"⚙️ Action Methods : {inv['total_methods']}")
|
|
163
|
+
print(f"🧪 Test Cases Found : {len(inv['test_cases'])}")
|
|
164
|
+
print("======================================================================")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
if __name__ == "__main__":
|
|
168
|
+
main()
|