Skip to content

Commit d3afb07

Browse files
committed
refactor(e2e_appium): config management and device handling
- Refactored TestConfig and session management to use EnvironmentConfig and DeviceConfig. - Updated session management to support multiple providers, including BrowserStack. - Updated error handling for configuration validation and device selection.
1 parent 2a1b063 commit d3afb07

File tree

7 files changed

+562
-625
lines changed

7 files changed

+562
-625
lines changed

test/e2e_appium/config/settings.py

Lines changed: 118 additions & 261 deletions
Original file line numberDiff line numberDiff line change
@@ -1,269 +1,126 @@
11
import os
2-
import logging
32
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
58

69

710
@dataclass
811
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
264120

265121

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

Comments
 (0)