|
1 | 1 | import os |
2 | | -import logging |
3 | 2 | from dataclasses import dataclass, field |
4 | | -from typing import Dict, Any |
| 3 | +from pathlib import Path |
| 4 | +from typing import Any, Dict, List, Optional |
| 5 | + |
| 6 | +from core.config_manager import ConfigurationManager, EnvironmentSwitcher |
| 7 | +from core.environment import DeviceConfig, EnvironmentConfig |
5 | 8 |
|
6 | 9 |
|
7 | 10 | @dataclass |
8 | 11 | class TestConfig: |
9 | | - lt_username: str = field(default_factory=lambda: os.getenv("LT_USERNAME", "")) |
10 | | - lt_access_key: str = field(default_factory=lambda: os.getenv("LT_ACCESS_KEY", "")) |
11 | | - |
12 | | - device_name: str = field( |
13 | | - default_factory=lambda: os.getenv("DEVICE_NAME", "Galaxy Tab S8") |
14 | | - ) |
15 | | - platform_name: str = field( |
16 | | - default_factory=lambda: os.getenv("PLATFORM_NAME", "android") |
17 | | - ) |
18 | | - platform_version: str = field( |
19 | | - default_factory=lambda: os.getenv("PLATFORM_VERSION", "14") |
20 | | - ) |
21 | | - device_orientation: str = field( |
22 | | - default_factory=lambda: os.getenv("DEVICE_ORIENTATION", "landscape") |
23 | | - ) |
24 | | - |
25 | | - default_timeout: int = field( |
26 | | - default_factory=lambda: int(os.getenv("DEFAULT_TIMEOUT", "30")) |
27 | | - ) |
28 | | - element_wait_timeout: int = field( |
29 | | - default_factory=lambda: int(os.getenv("ELEMENT_WAIT_TIMEOUT", "30")) |
30 | | - ) |
31 | | - element_click_timeout: int = field( |
32 | | - default_factory=lambda: int(os.getenv("ELEMENT_CLICK_TIMEOUT", "10")) |
33 | | - ) |
34 | | - element_find_timeout: int = field( |
35 | | - default_factory=lambda: int(os.getenv("ELEMENT_FIND_TIMEOUT", "15")) |
36 | | - ) |
37 | | - |
38 | | - status_app_url: str = field( |
39 | | - default_factory=lambda: os.getenv("STATUS_APP_URL", "lt://") |
40 | | - ) |
41 | | - |
42 | | - lt_hub_url: str = "https://mobile-hub.lambdatest.com/wd/hub" |
43 | | - build_name: str = field( |
44 | | - default_factory=lambda: os.getenv("BUILD_NAME", "E2E_Appium Tests") |
45 | | - ) |
46 | | - test_name: str = field( |
47 | | - default_factory=lambda: os.getenv("TEST_NAME", "Automated Test Run") |
48 | | - ) |
49 | | - idle_timeout: int = 600 |
50 | | - |
51 | | - log_level: str = field(default_factory=lambda: os.getenv("LOG_LEVEL", "INFO")) |
52 | | - enable_screenshots: bool = field( |
53 | | - default_factory=lambda: os.getenv("ENABLE_SCREENSHOTS", "true").lower() |
54 | | - == "true" |
55 | | - ) |
56 | | - enable_video_recording: bool = field( |
57 | | - default_factory=lambda: os.getenv("ENABLE_VIDEO_RECORDING", "true").lower() |
58 | | - == "true" |
59 | | - ) |
60 | | - enable_network_logs: bool = True |
61 | | - enable_device_logs: bool = True |
62 | | - |
63 | | - screenshots_dir: str = field( |
64 | | - default_factory=lambda: os.getenv("SCREENSHOTS_DIR", "screenshots") |
65 | | - ) |
66 | | - logs_dir: str = field(default_factory=lambda: os.getenv("LOGS_DIR", "logs")) |
67 | | - reports_dir: str = field( |
68 | | - default_factory=lambda: os.getenv("REPORTS_DIR", "reports") |
69 | | - ) |
70 | | - |
71 | | - enable_xml_report: bool = field( |
72 | | - default_factory=lambda: os.getenv("ENABLE_XML_REPORT", "true").lower() == "true" |
73 | | - ) |
74 | | - enable_html_report: bool = field( |
75 | | - default_factory=lambda: os.getenv("ENABLE_HTML_REPORT", "true").lower() |
76 | | - == "true" |
77 | | - ) |
78 | | - enable_junit_report: bool = field( |
79 | | - default_factory=lambda: os.getenv("ENABLE_JUNIT_REPORT", "true").lower() |
80 | | - == "true" |
81 | | - ) |
82 | | - |
83 | | - enable_performance_analytics: bool = field( |
84 | | - default_factory=lambda: os.getenv( |
85 | | - "E2E_ENABLE_PERFORMANCE_ANALYTICS", "false" |
86 | | - ).lower() |
87 | | - in ("true", "1", "yes", "on") |
88 | | - ) |
89 | | - performance_report_days: int = field( |
90 | | - default_factory=lambda: int(os.getenv("E2E_PERFORMANCE_REPORT_DAYS", "7")) |
91 | | - ) |
92 | | - |
93 | | - build_number: str = field(default_factory=lambda: os.getenv("BUILD_NUMBER", "")) |
94 | | - build_url: str = field(default_factory=lambda: os.getenv("BUILD_URL", "")) |
95 | | - git_commit: str = field(default_factory=lambda: os.getenv("GIT_COMMIT", "")) |
96 | | - git_branch: str = field(default_factory=lambda: os.getenv("GIT_BRANCH", "")) |
97 | | - |
98 | | - local_appium_server: str = field( |
99 | | - default_factory=lambda: os.getenv( |
100 | | - "LOCAL_APPIUM_SERVER", "http://localhost:4723" |
101 | | - ) |
102 | | - ) |
103 | | - local_app_path: str = field(default_factory=lambda: os.getenv("LOCAL_APP_PATH", "")) |
104 | | - |
105 | | - def __post_init__(self): |
106 | | - self._validate_required_fields() |
107 | | - self._validate_timeouts() |
108 | | - self._validate_urls() |
109 | | - self._create_directories() |
110 | | - |
111 | | - def _validate_required_fields(self): |
112 | | - errors = [] |
113 | | - warnings = [] |
114 | | - |
115 | | - test_environment = os.getenv("TEST_ENVIRONMENT", "local") |
116 | | - |
117 | | - if test_environment in ["lambdatest", "lt"]: |
118 | | - if not self.lt_username: |
119 | | - errors.append( |
120 | | - "LT_USERNAME environment variable is required for LambdaTest execution" |
121 | | - ) |
122 | | - |
123 | | - if not self.lt_access_key: |
124 | | - errors.append( |
125 | | - "LT_ACCESS_KEY environment variable is required for LambdaTest execution" |
126 | | - ) |
127 | | - |
128 | | - if not self.status_app_url or self.status_app_url == "lt://": |
129 | | - errors.append("STATUS_APP_URL must be provided (LambdaTest app ID)") |
130 | | - else: |
131 | | - if not self.lt_username or not self.lt_access_key: |
132 | | - warnings.append( |
133 | | - "LambdaTest credentials not set (OK for local development)" |
134 | | - ) |
135 | | - |
136 | | - if warnings: |
137 | | - logger = logging.getLogger(__name__) |
138 | | - for warning in warnings: |
139 | | - logger.warning(f"⚠️ {warning}") |
140 | | - |
141 | | - if errors: |
142 | | - error_msg = "Configuration validation failed:\n" + "\n".join( |
143 | | - f" • {error}" for error in errors |
144 | | - ) |
145 | | - error_msg += "\n\nPlease set the required environment variables. See env_variables.example for guidance." |
146 | | - raise ValueError(error_msg) |
147 | | - |
148 | | - def _validate_timeouts(self): |
149 | | - timeouts = { |
150 | | - "default_timeout": self.default_timeout, |
151 | | - "element_wait_timeout": self.element_wait_timeout, |
152 | | - "element_click_timeout": self.element_click_timeout, |
153 | | - "element_find_timeout": self.element_find_timeout, |
154 | | - } |
155 | | - |
156 | | - for name, value in timeouts.items(): |
157 | | - if value < 5: |
158 | | - raise ValueError(f"{name} must be at least 5 seconds, got {value}") |
159 | | - if value > 300: |
160 | | - raise ValueError(f"{name} should not exceed 300 seconds, got {value}") |
161 | | - |
162 | | - def _validate_urls(self): |
163 | | - if self.status_app_url and not ( |
164 | | - self.status_app_url.startswith("lt://") |
165 | | - or self.status_app_url.startswith("http") |
166 | | - ): |
167 | | - raise ValueError( |
168 | | - "STATUS_APP_URL must be a LambdaTest app ID (lt://) or valid URL" |
169 | | - ) |
170 | | - |
171 | | - def _create_directories(self): |
172 | | - for directory in [self.screenshots_dir, self.logs_dir, self.reports_dir]: |
173 | | - if directory: |
174 | | - os.makedirs(directory, exist_ok=True) |
175 | | - |
176 | | - def get_lambdatest_capabilities(self) -> Dict[str, Any]: |
177 | | - build_name = self.build_name |
178 | | - if self.build_number: |
179 | | - build_name += f" - Build {self.build_number}" |
180 | | - |
181 | | - test_name = self.test_name |
182 | | - if self.git_branch: |
183 | | - test_name += f" ({self.git_branch})" |
184 | | - |
185 | | - return { |
186 | | - "lt:options": { |
187 | | - "w3c": True, |
188 | | - "platformName": self.platform_name, |
189 | | - "deviceName": self.device_name, |
190 | | - "appiumVersion": "2.1.3", |
191 | | - "platformVersion": self.platform_version, |
192 | | - "app": self.status_app_url, |
193 | | - "devicelog": self.enable_device_logs, |
194 | | - "visual": self.enable_screenshots, |
195 | | - "video": self.enable_video_recording, |
196 | | - "build": build_name, |
197 | | - "name": test_name, |
198 | | - "project": "Status E2E_Appium", |
199 | | - "deviceOrientation": self.device_orientation, |
200 | | - "idleTimeout": self.idle_timeout, |
201 | | - "isRealMobile": False, |
202 | | - }, |
203 | | - "appium:options": {"automationName": "UiAutomator2"}, |
204 | | - } |
205 | | - |
206 | | - def get_local_capabilities(self) -> Dict[str, Any]: |
207 | | - if not self.local_app_path: |
208 | | - raise ValueError("local_app_path is required for local testing") |
209 | | - |
210 | | - return { |
211 | | - "platformName": self.platform_name, |
212 | | - "deviceName": self.device_name, |
213 | | - "platformVersion": self.platform_version, |
214 | | - "app": self.local_app_path, |
215 | | - "automationName": "UiAutomator2", |
216 | | - } |
217 | | - |
218 | | - def get_build_info(self) -> Dict[str, str]: |
219 | | - return { |
220 | | - "build_name": self.build_name, |
221 | | - "test_name": self.test_name, |
222 | | - "build_number": self.build_number, |
223 | | - "build_url": self.build_url, |
224 | | - "git_commit": self.git_commit, |
225 | | - "git_branch": self.git_branch, |
226 | | - } |
227 | | - |
228 | | - def summary(self) -> Dict[str, Any]: |
229 | | - return { |
230 | | - "device": f"{self.device_name} ({self.platform_name} {self.platform_version})", |
231 | | - "app_url": self.status_app_url, |
232 | | - "lt_username": self.lt_username, |
233 | | - "lt_access_key": "***" + self.lt_access_key[-4:] |
234 | | - if self.lt_access_key |
235 | | - else "NOT SET", |
236 | | - "hub_url": self.lt_hub_url + " (using secure ClientConfig auth)", |
237 | | - "timeouts": { |
238 | | - "default": self.default_timeout, |
239 | | - "element_wait": self.element_wait_timeout, |
240 | | - "click": self.element_click_timeout, |
241 | | - "find": self.element_find_timeout, |
242 | | - }, |
243 | | - "build_info": self.get_build_info(), |
244 | | - "logging": { |
245 | | - "level": self.log_level, |
246 | | - "screenshots": self.enable_screenshots, |
247 | | - "video": self.enable_video_recording, |
248 | | - }, |
249 | | - "performance_analytics": { |
250 | | - "enabled": self.enable_performance_analytics, |
251 | | - "report_days": self.performance_report_days, |
252 | | - }, |
253 | | - } |
254 | | - |
255 | | - |
256 | | -_config_instance = None |
257 | | - |
258 | | - |
259 | | -def get_config() -> TestConfig: |
260 | | - global _config_instance |
261 | | - if _config_instance is None: |
262 | | - _config_instance = TestConfig() |
263 | | - return _config_instance |
| 12 | + environment: EnvironmentConfig |
| 13 | + device: DeviceConfig |
| 14 | + capabilities: Dict[str, Any] |
| 15 | + app_reference: str |
| 16 | + provider_name: str |
| 17 | + reports_dir: str |
| 18 | + logs_dir: str |
| 19 | + screenshots_dir: str |
| 20 | + enable_xml_report: bool |
| 21 | + enable_html_report: bool |
| 22 | + enable_junit_report: bool |
| 23 | + logging_level: str |
| 24 | + concurrency: Dict[str, int] |
| 25 | + pytest_addopts: List[str] |
| 26 | + provider_options: Dict[str, Any] = field(default_factory=dict) |
| 27 | + |
| 28 | + @property |
| 29 | + def environment_name(self) -> str: |
| 30 | + return self.environment.name |
| 31 | + |
| 32 | + @property |
| 33 | + def device_name(self) -> str: |
| 34 | + return self.capabilities.get("deviceName", self.device.display_name or "") |
| 35 | + |
| 36 | + @property |
| 37 | + def platform_name(self) -> str: |
| 38 | + return self.capabilities.get("platformName", "") |
| 39 | + |
| 40 | + @property |
| 41 | + def platform_version(self) -> str: |
| 42 | + return self.capabilities.get("platformVersion", "") |
| 43 | + |
| 44 | + |
| 45 | +_CONFIG_CACHE: Optional[TestConfig] = None |
| 46 | + |
| 47 | + |
| 48 | +def _select_device(env_config: EnvironmentConfig) -> DeviceConfig: |
| 49 | + device_id = os.getenv("TEST_DEVICE_ID") |
| 50 | + tag_env = os.getenv("TEST_DEVICE_TAGS", "") |
| 51 | + tags = [tag.strip() for tag in tag_env.split(",") if tag.strip()] |
| 52 | + |
| 53 | + if device_id: |
| 54 | + return env_config.get_device(device_id) |
| 55 | + |
| 56 | + if tags: |
| 57 | + matches = env_config.find_devices_by_tags(tags) |
| 58 | + if matches: |
| 59 | + return matches[0] |
| 60 | + |
| 61 | + return env_config.get_device() |
| 62 | + |
| 63 | + |
| 64 | +def _resolve_app_reference(env_config: EnvironmentConfig) -> str: |
| 65 | + app_cfg = env_config.get_provider_option("app", {}) |
| 66 | + if app_cfg.get("path_template"): |
| 67 | + return env_config.resolve_template(app_cfg.get("path_template")) |
| 68 | + if app_cfg.get("app_id_template"): |
| 69 | + return env_config.resolve_template(app_cfg.get("app_id_template")) |
| 70 | + return "" |
| 71 | + |
| 72 | + |
| 73 | +def _ensure_directories(*paths: str) -> None: |
| 74 | + for path in paths: |
| 75 | + if path: |
| 76 | + Path(path).mkdir(parents=True, exist_ok=True) |
| 77 | + |
| 78 | + |
| 79 | +def load_config() -> TestConfig: |
| 80 | + env_name = os.getenv("TEST_ENVIRONMENT") |
| 81 | + manager = ConfigurationManager() |
| 82 | + if not env_name: |
| 83 | + auto = EnvironmentSwitcher().auto_detect_environment() |
| 84 | + env_name = os.getenv("E2E_PROVIDER", auto) |
| 85 | + env_config = manager.load_environment(env_name) |
| 86 | + |
| 87 | + device = _select_device(env_config) |
| 88 | + capabilities = device.merged_capabilities( |
| 89 | + env_config.device_defaults.get("capabilities", {}) |
| 90 | + ) |
| 91 | + |
| 92 | + reports_dir = env_config.directories.get("reports", "reports") |
| 93 | + logs_dir = env_config.directories.get("logs", "logs") |
| 94 | + screenshots_dir = env_config.directories.get("screenshots", "screenshots") |
| 95 | + _ensure_directories(reports_dir, logs_dir, screenshots_dir) |
| 96 | + |
| 97 | + execution = env_config.execution or {} |
| 98 | + pytest_addopts = execution.get("pytest", {}).get("addopts", []) |
| 99 | + |
| 100 | + logging_config = env_config.logging or {} |
| 101 | + |
| 102 | + config = TestConfig( |
| 103 | + environment=env_config, |
| 104 | + device=device, |
| 105 | + capabilities=capabilities, |
| 106 | + app_reference=_resolve_app_reference(env_config), |
| 107 | + provider_name=env_config.provider.name, |
| 108 | + reports_dir=reports_dir, |
| 109 | + logs_dir=logs_dir, |
| 110 | + screenshots_dir=screenshots_dir, |
| 111 | + enable_xml_report=logging_config.get("enable_xml_report", True), |
| 112 | + enable_html_report=logging_config.get("enable_html_report", True), |
| 113 | + enable_junit_report=logging_config.get("enable_junit_report", True), |
| 114 | + logging_level=logging_config.get("level", "INFO"), |
| 115 | + concurrency=env_config.concurrency_limits(), |
| 116 | + pytest_addopts=pytest_addopts, |
| 117 | + provider_options=env_config.provider.options, |
| 118 | + ) |
| 119 | + return config |
264 | 120 |
|
265 | 121 |
|
266 | | -def reload_config() -> TestConfig: |
267 | | - global _config_instance |
268 | | - _config_instance = None |
269 | | - return get_config() |
| 122 | +def get_config(refresh: bool = False) -> TestConfig: |
| 123 | + global _CONFIG_CACHE |
| 124 | + if _CONFIG_CACHE is None or refresh: |
| 125 | + _CONFIG_CACHE = load_config() |
| 126 | + return _CONFIG_CACHE |
0 commit comments