From 61333c53abd4def6537ce8d4826186da6d72b2e0 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Tue, 5 Feb 2019 17:51:41 +0900 Subject: [PATCH 1/8] add direct connect feature --- appium/webdriver/webdriver.py | 22 ++++++++++++++-- test/unit/webdriver/webdriver_test.py | 38 +++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index a2d085c6..47ae9c9f 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -20,6 +20,8 @@ from selenium.common.exceptions import InvalidArgumentException from selenium.webdriver.common.by import By from selenium.webdriver.remote.command import Command as RemoteCommand +from selenium.webdriver.remote.remote_connection import RemoteConnection + from appium.webdriver.common.mobileby import MobileBy from .appium_connection import AppiumConnection @@ -117,7 +119,7 @@ class WebDriver( ): def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub', - desired_capabilities=None, browser_profile=None, proxy=None, keep_alive=False): + desired_capabilities=None, browser_profile=None, proxy=None, keep_alive=False, direct_connection=False): super(WebDriver, self).__init__( AppiumConnection(command_executor, keep_alive=keep_alive), @@ -126,12 +128,28 @@ def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub', proxy ) - if self.command_executor is not None: + if self.command_executor is not None: # pylint: disable=access-member-before-definition self._addCommands() self.error_handler = MobileErrorHandler() self._switch_to = MobileSwitchTo(self) + if direct_connection: + protocol = self.capabilities['directConnectProtocol'] + hostname = self.capabilities['directConnectHost'] + port = self.capabilities['directConnectPort'] + path = self.capabilities['directConnectPath'] + # TODO: create a new client to the above params + executor = '{scheme}://{hostname}:{port}{path}'.format( + scheme=protocol, + hostname=hostname, + port=port, + path=path + ) + # Override command executor + self.command_executor = RemoteConnection(executor, keep_alive=keep_alive) + self._addCommands() + # add new method to the `find_by_*` pantheon By.IOS_UIAUTOMATION = MobileBy.IOS_UIAUTOMATION By.IOS_PREDICATE = MobileBy.IOS_PREDICATE diff --git a/test/unit/webdriver/webdriver_test.py b/test/unit/webdriver/webdriver_test.py index 52cb0fe9..def9254b 100644 --- a/test/unit/webdriver/webdriver_test.py +++ b/test/unit/webdriver/webdriver_test.py @@ -176,3 +176,41 @@ def test_find_elements_by_android_data_matcher_no_value(self): assert d['using'] == '-android datamatcher' assert d['value'] == '{}' assert len(els) == 0 + + @httpretty.activate + def test_create_session_register_uridirect(self): + httpretty.register_uri( + httpretty.POST, + 'http://localhost:4723/wd/hub/session', + body=json.dumps({'value': { + 'sessionId': 'session-id', + 'capabilities': { + 'deviceName': 'Android Emulator', + 'directConnectProtocol': 'http', + 'directConnectHost': 'localhost2', + 'directConnectPort': 4800, + 'directConnectPath': '/special/path/wd/hub', + } + }}) + ) + + httpretty.register_uri( + httpretty.GET, + 'http://localhost2:4800/special/path/wd/hub/session/session-id/contexts', + body=json.dumps({'value': ['NATIVE_APP', 'CHROMIUM']}) + ) + + desired_caps = { + 'platformName': 'Android', + 'deviceName': 'Android Emulator', + 'app': 'path/to/app', + 'automationName': 'UIAutomator2' + } + driver = webdriver.Remote( + 'http://localhost:4723/wd/hub', + desired_caps, + direct_connection=True + ) + + assert 'http://localhost2:4800/special/path/wd/hub' == driver.command_executor._url + assert ['NATIVE_APP', 'CHROMIUM'] == driver.contexts From 6b0d4234d9b34a637478e99ad23d02a4f732a009 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Wed, 6 Feb 2019 12:05:57 +0900 Subject: [PATCH 2/8] rmeove todo --- appium/webdriver/webdriver.py | 1 - 1 file changed, 1 deletion(-) diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index 47ae9c9f..70962281 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -139,7 +139,6 @@ def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub', hostname = self.capabilities['directConnectHost'] port = self.capabilities['directConnectPort'] path = self.capabilities['directConnectPath'] - # TODO: create a new client to the above params executor = '{scheme}://{hostname}:{port}{path}'.format( scheme=protocol, hostname=hostname, From cbf1b09a528bea0f4b8bc163b51c382cd52f64cb Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Mon, 25 Feb 2019 14:00:47 +0900 Subject: [PATCH 3/8] update readme, extract _update_command_executor --- README.md | 26 +++++++++++++++++++++++++- appium/webdriver/webdriver.py | 30 +++++++++++++++++------------- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 543da044..1f49579c 100644 --- a/README.md +++ b/README.md @@ -154,11 +154,35 @@ desired_caps['app'] = PATH('../../apps/UICatalog.app.zip') self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) ``` - ## Changed or added functionality The methods that do change are... +### Direct Connect URLs + +If your Selenium/Appium server decorates the new session capabilities response with the following keys: + +- `directConnectProtocol` +- `directConnectHost` +- `directConnectPort` +- `directConnectPath` + +Then python client will switch its endpoint to the one specified by the values of those keys. + +```python +import unittest +from appium import webdriver + +desired_caps = {} +desired_caps['platformName'] = 'iOS' +desired_caps['platformVersion'] = '11.4' +desired_caps['automationName'] = 'xcuitest' +desired_caps['deviceName'] = 'iPhone Simulator' +desired_caps['app'] = PATH('../../apps/UICatalog.app.zip') + +self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps, direct_connection=True) +``` + ### Switching between 'Native' and 'Webview' diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index 70962281..1bb7286d 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -135,19 +135,7 @@ def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub', self._switch_to = MobileSwitchTo(self) if direct_connection: - protocol = self.capabilities['directConnectProtocol'] - hostname = self.capabilities['directConnectHost'] - port = self.capabilities['directConnectPort'] - path = self.capabilities['directConnectPath'] - executor = '{scheme}://{hostname}:{port}{path}'.format( - scheme=protocol, - hostname=hostname, - port=port, - path=path - ) - # Override command executor - self.command_executor = RemoteConnection(executor, keep_alive=keep_alive) - self._addCommands() + self._update_command_executor(keep_alive=keep_alive) # add new method to the `find_by_*` pantheon By.IOS_UIAUTOMATION = MobileBy.IOS_UIAUTOMATION @@ -159,6 +147,22 @@ def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub', By.IMAGE = MobileBy.IMAGE By.CUSTOM = MobileBy.CUSTOM + def _update_command_executor(self, keep_alive): + """Update command executor following directConnect feature""" + protocol = self.capabilities['directConnectProtocol'] + hostname = self.capabilities['directConnectHost'] + port = self.capabilities['directConnectPort'] + path = self.capabilities['directConnectPath'] + executor = '{scheme}://{hostname}:{port}{path}'.format( + scheme=protocol, + hostname=hostname, + port=port, + path=path + ) + # Override command executor + self.command_executor = RemoteConnection(executor, keep_alive=keep_alive) + self._addCommands() + def start_session(self, capabilities, browser_profile=None): """ Override for Appium From 24996f631fde78be15f6d9b51815b9680fd053da Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Mon, 25 Feb 2019 20:47:32 +0900 Subject: [PATCH 4/8] add logger --- appium/common/logger.py | 28 ++++++++++++++++++++++++++++ appium/webdriver/webdriver.py | 3 +++ 2 files changed, 31 insertions(+) create mode 100644 appium/common/logger.py diff --git a/appium/common/logger.py b/appium/common/logger.py new file mode 100644 index 00000000..372d7fa8 --- /dev/null +++ b/appium/common/logger.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import sys + + +def setup_logger(level=logging.NOTSET): + logger.propagate = False + logger.setLevel(level) + handler = logging.StreamHandler(stream=sys.stderr) + logger.addHandler(handler) + + +# global logger +logger = logging.getLogger(__name__) +setup_logger() diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index 1bb7286d..f5d87e32 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -45,6 +45,7 @@ from .switch_to import MobileSwitchTo from .webelement import WebElement as MobileWebElement +from appium.common.logger import logger # From remote/webdriver.py _W3C_CAPABILITY_NAMES = frozenset([ @@ -159,6 +160,8 @@ def _update_command_executor(self, keep_alive): port=port, path=path ) + + logger.error('Update request endpoint to %s', executor) # Override command executor self.command_executor = RemoteConnection(executor, keep_alive=keep_alive) self._addCommands() From 87ec908ed6332ac5ac7715227e3b572c6b8ed098 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Mon, 25 Feb 2019 23:00:13 +0900 Subject: [PATCH 5/8] make log level info --- appium/webdriver/webdriver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index f5d87e32..46dacb6a 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -161,7 +161,7 @@ def _update_command_executor(self, keep_alive): path=path ) - logger.error('Update request endpoint to %s', executor) + logger.info('Update request endpoint to %s', executor) # Override command executor self.command_executor = RemoteConnection(executor, keep_alive=keep_alive) self._addCommands() From caa1ce30fa3aa1ee0683fe368fb7743cc411cadf Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Tue, 26 Feb 2019 09:59:00 +0900 Subject: [PATCH 6/8] show warning if no directConnectXxxxx in dict --- appium/webdriver/webdriver.py | 26 +++++++++++++++---- test/unit/webdriver/webdriver_test.py | 37 +++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index 46dacb6a..3f44968b 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -150,10 +150,26 @@ def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub', def _update_command_executor(self, keep_alive): """Update command executor following directConnect feature""" - protocol = self.capabilities['directConnectProtocol'] - hostname = self.capabilities['directConnectHost'] - port = self.capabilities['directConnectPort'] - path = self.capabilities['directConnectPath'] + direct_protocol = 'directConnectProtocol' + direct_host = 'directConnectHost' + direct_port = 'directConnectPort' + direct_path = 'directConnectPath' + + if (not {direct_protocol, direct_host, direct_port, direct_path}.issubset(set(self.capabilities))): + message = 'Could get direct capabilities:\n'\ + '{protocol}: {protocol_get}, {host}: {host_get}, {port}: {port_get}, {path}: {path_get}'.format( + protocol=direct_protocol, protocol_get=self.capabilities.get(direct_protocol, ''), + host=direct_host, host_get=self.capabilities.get(direct_host, ''), + port=direct_port, port_get=self.capabilities.get(direct_port, ''), + path=direct_path, path_get=self.capabilities.get(direct_path, ''), + ) + logger.warning(message) + return + + protocol = self.capabilities[direct_protocol] + hostname = self.capabilities[direct_host] + port = self.capabilities[direct_port] + path = self.capabilities[direct_path] executor = '{scheme}://{hostname}:{port}{path}'.format( scheme=protocol, hostname=hostname, @@ -161,7 +177,7 @@ def _update_command_executor(self, keep_alive): path=path ) - logger.info('Update request endpoint to %s', executor) + logger.info('Updated request endpoint to %s', executor) # Override command executor self.command_executor = RemoteConnection(executor, keep_alive=keep_alive) self._addCommands() diff --git a/test/unit/webdriver/webdriver_test.py b/test/unit/webdriver/webdriver_test.py index def9254b..ccf55349 100644 --- a/test/unit/webdriver/webdriver_test.py +++ b/test/unit/webdriver/webdriver_test.py @@ -214,3 +214,40 @@ def test_create_session_register_uridirect(self): assert 'http://localhost2:4800/special/path/wd/hub' == driver.command_executor._url assert ['NATIVE_APP', 'CHROMIUM'] == driver.contexts + + @httpretty.activate + def test_create_session_register_uridirect_no_direct_connect_path(self): + httpretty.register_uri( + httpretty.POST, + 'http://localhost:4723/wd/hub/session', + body=json.dumps({'value': { + 'sessionId': 'session-id', + 'capabilities': { + 'deviceName': 'Android Emulator', + 'directConnectProtocol': 'http', + 'directConnectHost': 'localhost2', + 'directConnectPort': 4800 + } + }}) + ) + + httpretty.register_uri( + httpretty.GET, + 'http://localhost:4723/wd/hub/session/session-id/contexts', + body=json.dumps({'value': ['NATIVE_APP', 'CHROMIUM']}) + ) + + desired_caps = { + 'platformName': 'Android', + 'deviceName': 'Android Emulator', + 'app': 'path/to/app', + 'automationName': 'UIAutomator2' + } + driver = webdriver.Remote( + 'http://localhost:4723/wd/hub', + desired_caps, + direct_connection=True + ) + + assert 'http://localhost:4723/wd/hub' == driver.command_executor._url + assert ['NATIVE_APP', 'CHROMIUM'] == driver.contexts From dcc76e931807face43fbfe171420d320dd31fe7e Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Tue, 26 Feb 2019 11:08:58 +0900 Subject: [PATCH 7/8] tweak error message --- appium/webdriver/webdriver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index 3f44968b..4340a754 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -156,7 +156,7 @@ def _update_command_executor(self, keep_alive): direct_path = 'directConnectPath' if (not {direct_protocol, direct_host, direct_port, direct_path}.issubset(set(self.capabilities))): - message = 'Could get direct capabilities:\n'\ + message = 'Direct connect capabilities from server were:\n'\ '{protocol}: {protocol_get}, {host}: {host_get}, {port}: {port_get}, {path}: {path_get}'.format( protocol=direct_protocol, protocol_get=self.capabilities.get(direct_protocol, ''), host=direct_host, host_get=self.capabilities.get(direct_host, ''), From 694a368eefe287815ad27ac2566c8b2164df5a25 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Tue, 26 Feb 2019 16:14:00 +0900 Subject: [PATCH 8/8] tweak message format --- appium/webdriver/webdriver.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index 4340a754..87f439e2 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -129,7 +129,7 @@ def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub', proxy ) - if self.command_executor is not None: # pylint: disable=access-member-before-definition + if hasattr(self, 'command_executor'): self._addCommands() self.error_handler = MobileErrorHandler() @@ -156,13 +156,9 @@ def _update_command_executor(self, keep_alive): direct_path = 'directConnectPath' if (not {direct_protocol, direct_host, direct_port, direct_path}.issubset(set(self.capabilities))): - message = 'Direct connect capabilities from server were:\n'\ - '{protocol}: {protocol_get}, {host}: {host_get}, {port}: {port_get}, {path}: {path_get}'.format( - protocol=direct_protocol, protocol_get=self.capabilities.get(direct_protocol, ''), - host=direct_host, host_get=self.capabilities.get(direct_host, ''), - port=direct_port, port_get=self.capabilities.get(direct_port, ''), - path=direct_path, path_get=self.capabilities.get(direct_path, ''), - ) + message = 'Direct connect capabilities from server were:\n' + for key in [direct_protocol, direct_host, direct_port, direct_path]: + message += '{}: \'{}\'\n'.format(key, self.capabilities.get(key, '')) logger.warning(message) return