Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions appium/webdriver/appium_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# limitations under the License.

import os
import re
import subprocess as sp
import sys
import time
Expand All @@ -24,7 +25,8 @@
DEFAULT_PORT = 4723
STARTUP_TIMEOUT_MS = 60000
MAIN_SCRIPT_PATH = 'appium/build/lib/main.js'
STATUS_URL = '/wd/hub/status'
STATUS_URL = '/status'
DEFAULT_BASE_PATH = '/'


def find_executable(executable: str) -> Optional[str]:
Expand All @@ -47,9 +49,9 @@ def find_executable(executable: str) -> Optional[str]:

def poll_url(host: str, port: int, path: str, timeout_ms: int) -> bool:
time_started_sec = time.time()
conn = urllib3.PoolManager(timeout=1.0)
while time.time() < time_started_sec + timeout_ms / 1000.0:
try:
conn = urllib3.PoolManager(timeout=1.0)
resp = conn.request('HEAD', f'http://{host}:{port}{path}')
if resp.status < 400:
return True
Expand Down Expand Up @@ -112,6 +114,13 @@ def _parse_port(args: List[str]) -> int:
return int(args[idx + 1])
return DEFAULT_PORT

@staticmethod
def _parse_base_path(args: List[str]) -> str:
for idx, arg in enumerate(args or []):
if arg in ('--base-path', '-pa') and idx < len(args) - 1:
return args[idx + 1]
return DEFAULT_BASE_PATH

@staticmethod
def _parse_host(args: List[str]) -> str:
for idx, arg in enumerate(args or []):
Expand Down Expand Up @@ -166,7 +175,11 @@ def start(self, **kwargs: Any) -> sp.Popen:
host = self._parse_host(args)
port = self._parse_port(args)
error_msg: Optional[str] = None
if not self.is_running or (timeout_ms > 0 and not poll_url(host, port, STATUS_URL, timeout_ms)):
base_path = self._parse_base_path(args)
status_url_path = (
STATUS_URL if base_path == DEFAULT_BASE_PATH else f'{re.sub(r"[/]+$", "", base_path)}{STATUS_URL}'
)
if not self.is_running or (timeout_ms > 0 and not poll_url(host, port, status_url_path, timeout_ms)):
error_msg = f'Appium has failed to start on {host}:{port} within {timeout_ms}ms timeout'
if error_msg is not None:
if stderr == sp.PIPE and self._process.stderr is not None:
Expand Down Expand Up @@ -218,7 +231,11 @@ def is_listening(self) -> bool:
return False
host = self._parse_host(self._cmd)
port = self._parse_port(self._cmd)
return self.is_running and poll_url(host, port, STATUS_URL, 1000)
base_path = self._parse_base_path(self._cmd)
status_url_path = (
STATUS_URL if base_path == DEFAULT_BASE_PATH else f'{re.sub(r"[/]+$", "", base_path)}{STATUS_URL}'
)
return self.is_running and poll_url(host, port, status_url_path, 1000)


if __name__ == '__main__':
Expand Down
25 changes: 20 additions & 5 deletions appium/webdriver/webdriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from typing import Any, Callable, Dict, List, Optional, Tuple, Union

from selenium import webdriver
from selenium.common.exceptions import InvalidArgumentException, WebDriverException
from selenium.common.exceptions import InvalidArgumentException, SessionNotCreatedException, WebDriverException
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.command import Command as RemoteCommand
from selenium.webdriver.remote.remote_connection import RemoteConnection
Expand Down Expand Up @@ -317,10 +317,25 @@ def start_session(self, capabilities: Union[Dict, AppiumOptions], browser_profil

w3c_caps = AppiumOptions.as_w3c(capabilities) if isinstance(capabilities, dict) else capabilities.to_w3c()
response = self.execute(RemoteCommand.NEW_SESSION, w3c_caps)
if 'sessionId' not in response:
response = response['value']
self.session_id = response['sessionId']
self.caps = response.get('value') or response.get('capabilities')
# https://w3c.github.io/webdriver/#new-session
if not isinstance(response, dict):
raise SessionNotCreatedException(
f'A valid W3C session creation response must be a dictionary. Got "{response}" instead'
)
# Due to a W3C spec parsing misconception some servers
# pack the createSession response stuff into 'value' dictionary and
# some other put it to the top level of the response JSON nesting hierarchy
get_response_value: Callable[[str], Optional[Any]] = lambda key: response.get(key) or (
response['value'].get(key) if isinstance(response.get('value'), dict) else None
)
session_id = get_response_value('sessionId')
if not session_id:
raise SessionNotCreatedException(
f'A valid W3C session creation response must contain a non-empty "sessionId" entry. '
f'Got "{response}" instead'
)
self.session_id = session_id
self.caps = get_response_value('capabilities') or {}

def find_element(self, by: str = AppiumBy.ID, value: Union[str, Dict] = None) -> MobileWebElement:
"""
Expand Down
7 changes: 3 additions & 4 deletions ci-jobs/functional/setup_appium.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,14 @@ steps:
versionSpec: '3.x'
- script: brew install ffmpeg
displayName: Resolve dependencies (Appium server)
# - script: pip install trio==0.17.0
# displayName: Install trio
- script: python setup.py install
displayName: Install python language bindings for Appium
- script: |
pip install --upgrade pip
pip install pipenv
pipenv lock --clear
pipenv install --system
displayName: Resolve dependencies (Python)
- script: python setup.py install
displayName: Install python language bindings for Appium
- script: |
git --no-pager log -n1
python --version
Expand Down
48 changes: 26 additions & 22 deletions test/functional/android/appium_service_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,33 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from appium.webdriver.appium_service import AppiumService
from appium.webdriver.common.appiumby import AppiumBy
from test.functional.android.helper.test_helper import BaseTestCase
from test.functional.test_helper import wait_for_element

DEFAULT_PORT = 4723

from typing import Generator

class TestAppiumService(BaseTestCase):
import pytest

service: AppiumService

@classmethod
def setup_class(cls) -> None:
cls.service = AppiumService()
cls.service.start(args=['--address', '127.0.0.1', '-p', str(DEFAULT_PORT)])
from appium.webdriver.appium_service import AppiumService

def test_appium_service(self) -> None:
assert self.service.is_running
assert self.service.is_listening
el = wait_for_element(self.driver, AppiumBy.ACCESSIBILITY_ID, 'Accessibility')
assert el is not None

@classmethod
def teardown_class(cls) -> None:
cls.service.stop()
@pytest.fixture
def appium_service() -> Generator[AppiumService, None, None]:
service = AppiumService()
service.start(
args=[
'--address',
'127.0.0.1',
'-p',
'4773',
'--base-path',
'/wd/hub',
]
)
try:
yield service
finally:
service.stop()


@pytest.skip('Unstable in CI env')
def test_appium_service(appium_service: AppiumService) -> None:
assert appium_service.is_running
assert appium_service.is_listening
67 changes: 30 additions & 37 deletions test/unit/helper/test_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,29 +50,27 @@ def android_w3c_driver() -> 'WebDriver':

response_body_json = json.dumps(
{
'value': {
'sessionId': '1234567890',
'capabilities': {
'platform': 'LINUX',
'desired': {
'platformName': 'Android',
'automationName': 'uiautomator2',
'platformVersion': '7.1.1',
'deviceName': 'Android Emulator',
'app': '/test/apps/ApiDemos-debug.apk',
},
'sessionId': '1234567890',
'capabilities': {
'platform': 'LINUX',
'desired': {
'platformName': 'Android',
'automationName': 'uiautomator2',
'platformVersion': '7.1.1',
'deviceName': 'emulator-5554',
'deviceName': 'Android Emulator',
'app': '/test/apps/ApiDemos-debug.apk',
'deviceUDID': 'emulator-5554',
'appPackage': 'io.appium.android.apis',
'appWaitPackage': 'io.appium.android.apis',
'appActivity': 'io.appium.android.apis.ApiDemos',
'appWaitActivity': 'io.appium.android.apis.ApiDemos',
},
}
'platformName': 'Android',
'automationName': 'uiautomator2',
'platformVersion': '7.1.1',
'deviceName': 'emulator-5554',
'app': '/test/apps/ApiDemos-debug.apk',
'deviceUDID': 'emulator-5554',
'appPackage': 'io.appium.android.apis',
'appWaitPackage': 'io.appium.android.apis',
'appActivity': 'io.appium.android.apis.ApiDemos',
'appWaitActivity': 'io.appium.android.apis.ApiDemos',
},
}
)

Expand All @@ -95,18 +93,15 @@ def ios_w3c_driver() -> 'WebDriver':
Returns:
`webdriver.webdriver.WebDriver`: An instance of WebDriver
"""

response_body_json = json.dumps(
{
'value': {
'sessionId': '1234567890',
'capabilities': {
'device': 'iphone',
'browserName': 'UICatalog',
'sdkVersion': '11.4',
'CFBundleIdentifier': 'com.example.apple-samplecode.UICatalog',
},
}
'sessionId': '1234567890',
'capabilities': {
'device': 'iphone',
'browserName': 'UICatalog',
'sdkVersion': '11.4',
'CFBundleIdentifier': 'com.example.apple-samplecode.UICatalog',
},
}
)

Expand All @@ -132,15 +127,13 @@ def ios_w3c_driver_with_extensions(extensions) -> 'WebDriver':

response_body_json = json.dumps(
{
'value': {
'sessionId': '1234567890',
'capabilities': {
'device': 'iphone',
'browserName': 'UICatalog',
'sdkVersion': '11.4',
'CFBundleIdentifier': 'com.example.apple-samplecode.UICatalog',
},
}
'sessionId': '1234567890',
'capabilities': {
'device': 'iphone',
'browserName': 'UICatalog',
'sdkVersion': '11.4',
'CFBundleIdentifier': 'com.example.apple-samplecode.UICatalog',
},
}
)

Expand Down
72 changes: 33 additions & 39 deletions test/unit/webdriver/webdriver_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def test_create_session(self):
httpretty.register_uri(
httpretty.POST,
f'{SERVER_URL_BASE}/session',
body='{ "value": { "sessionId": "session-id", "capabilities": {"deviceName": "Android Emulator"}}}',
body='{ "value": {"sessionId": "session-id", "capabilities": {"deviceName": "Android Emulator"}} }',
)

desired_caps = {
Expand Down Expand Up @@ -71,7 +71,7 @@ def test_create_session_change_session_id(self):
httpretty.register_uri(
httpretty.POST,
f'{SERVER_URL_BASE}/session',
body='{ "value": { "sessionId": "session-id", "capabilities": {"deviceName": "Android Emulator"}}}',
body='{ "sessionId": "session-id", "capabilities": {"deviceName": "Android Emulator"} }',
)

httpretty.register_uri(
Expand Down Expand Up @@ -100,16 +100,14 @@ def test_create_session_register_uridirect(self):
f'{SERVER_URL_BASE}/session',
body=json.dumps(
{
'value': {
'sessionId': 'session-id',
'capabilities': {
'deviceName': 'Android Emulator',
'directConnectProtocol': 'http',
'directConnectHost': 'localhost2',
'directConnectPort': 4800,
'directConnectPath': '/special/path/wd/hub',
},
}
'sessionId': 'session-id',
'capabilities': {
'deviceName': 'Android Emulator',
'directConnectProtocol': 'http',
'directConnectHost': 'localhost2',
'directConnectPort': 4800,
'directConnectPath': '/special/path/wd/hub',
},
}
),
)
Expand Down Expand Up @@ -142,15 +140,13 @@ def test_create_session_register_uridirect_no_direct_connect_path(self):
f'{SERVER_URL_BASE}/session',
body=json.dumps(
{
'value': {
'sessionId': 'session-id',
'capabilities': {
'deviceName': 'Android Emulator',
'directConnectProtocol': 'http',
'directConnectHost': 'localhost2',
'directConnectPort': 4800,
},
}
'sessionId': 'session-id',
'capabilities': {
'deviceName': 'Android Emulator',
'directConnectProtocol': 'http',
'directConnectHost': 'localhost2',
'directConnectPort': 4800,
},
}
),
)
Expand Down Expand Up @@ -327,29 +323,27 @@ class TestSubModuleWebDriver(object):
def android_w3c_driver(self, driver_class):
response_body_json = json.dumps(
{
'value': {
'sessionId': '1234567890',
'capabilities': {
'platform': 'LINUX',
'desired': {
'platformName': 'Android',
'automationName': 'uiautomator2',
'platformVersion': '7.1.1',
'deviceName': 'Android Emulator',
'app': '/test/apps/ApiDemos-debug.apk',
},
'sessionId': '1234567890',
'capabilities': {
'platform': 'LINUX',
'desired': {
'platformName': 'Android',
'automationName': 'uiautomator2',
'platformVersion': '7.1.1',
'deviceName': 'emulator-5554',
'deviceName': 'Android Emulator',
'app': '/test/apps/ApiDemos-debug.apk',
'deviceUDID': 'emulator-5554',
'appPackage': 'io.appium.android.apis',
'appWaitPackage': 'io.appium.android.apis',
'appActivity': 'io.appium.android.apis.ApiDemos',
'appWaitActivity': 'io.appium.android.apis.ApiDemos',
},
}
'platformName': 'Android',
'automationName': 'uiautomator2',
'platformVersion': '7.1.1',
'deviceName': 'emulator-5554',
'app': '/test/apps/ApiDemos-debug.apk',
'deviceUDID': 'emulator-5554',
'appPackage': 'io.appium.android.apis',
'appWaitPackage': 'io.appium.android.apis',
'appActivity': 'io.appium.android.apis.ApiDemos',
'appWaitActivity': 'io.appium.android.apis.ApiDemos',
},
}
)

Expand Down