Skip to content

Commit 1830af4

Browse files
authored
Add direct connect flag to be able to handle directConnectXxxxc (appium#338)
* add direct connect feature * rmeove todo * update readme, extract _update_command_executor * add logger * make log level info * show warning if no directConnectXxxxx in dict * tweak error message * tweak message format
1 parent 1c179d2 commit 1830af4

File tree

4 files changed

+166
-3
lines changed

4 files changed

+166
-3
lines changed

README.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,11 +154,35 @@ desired_caps['app'] = PATH('../../apps/UICatalog.app.zip')
154154
self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
155155
```
156156

157-
158157
## Changed or added functionality
159158

160159
The methods that do change are...
161160

161+
### Direct Connect URLs
162+
163+
If your Selenium/Appium server decorates the new session capabilities response with the following keys:
164+
165+
- `directConnectProtocol`
166+
- `directConnectHost`
167+
- `directConnectPort`
168+
- `directConnectPath`
169+
170+
Then python client will switch its endpoint to the one specified by the values of those keys.
171+
172+
```python
173+
import unittest
174+
from appium import webdriver
175+
176+
desired_caps = {}
177+
desired_caps['platformName'] = 'iOS'
178+
desired_caps['platformVersion'] = '11.4'
179+
desired_caps['automationName'] = 'xcuitest'
180+
desired_caps['deviceName'] = 'iPhone Simulator'
181+
desired_caps['app'] = PATH('../../apps/UICatalog.app.zip')
182+
183+
self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps, direct_connection=True)
184+
```
185+
162186

163187
### Switching between 'Native' and 'Webview'
164188

appium/common/logger.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/usr/bin/env python
2+
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import logging
16+
import sys
17+
18+
19+
def setup_logger(level=logging.NOTSET):
20+
logger.propagate = False
21+
logger.setLevel(level)
22+
handler = logging.StreamHandler(stream=sys.stderr)
23+
logger.addHandler(handler)
24+
25+
26+
# global logger
27+
logger = logging.getLogger(__name__)
28+
setup_logger()

appium/webdriver/webdriver.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
from selenium.common.exceptions import InvalidArgumentException
2121
from selenium.webdriver.common.by import By
2222
from selenium.webdriver.remote.command import Command as RemoteCommand
23+
from selenium.webdriver.remote.remote_connection import RemoteConnection
24+
2325

2426
from appium.webdriver.common.mobileby import MobileBy
2527
from .appium_connection import AppiumConnection
@@ -43,6 +45,7 @@
4345
from .switch_to import MobileSwitchTo
4446
from .webelement import WebElement as MobileWebElement
4547

48+
from appium.common.logger import logger
4649

4750
# From remote/webdriver.py
4851
_W3C_CAPABILITY_NAMES = frozenset([
@@ -117,7 +120,7 @@ class WebDriver(
117120
):
118121

119122
def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub',
120-
desired_capabilities=None, browser_profile=None, proxy=None, keep_alive=False):
123+
desired_capabilities=None, browser_profile=None, proxy=None, keep_alive=False, direct_connection=False):
121124

122125
super(WebDriver, self).__init__(
123126
AppiumConnection(command_executor, keep_alive=keep_alive),
@@ -126,12 +129,15 @@ def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub',
126129
proxy
127130
)
128131

129-
if self.command_executor is not None:
132+
if hasattr(self, 'command_executor'):
130133
self._addCommands()
131134

132135
self.error_handler = MobileErrorHandler()
133136
self._switch_to = MobileSwitchTo(self)
134137

138+
if direct_connection:
139+
self._update_command_executor(keep_alive=keep_alive)
140+
135141
# add new method to the `find_by_*` pantheon
136142
By.IOS_UIAUTOMATION = MobileBy.IOS_UIAUTOMATION
137143
By.IOS_PREDICATE = MobileBy.IOS_PREDICATE
@@ -142,6 +148,36 @@ def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub',
142148
By.IMAGE = MobileBy.IMAGE
143149
By.CUSTOM = MobileBy.CUSTOM
144150

151+
def _update_command_executor(self, keep_alive):
152+
"""Update command executor following directConnect feature"""
153+
direct_protocol = 'directConnectProtocol'
154+
direct_host = 'directConnectHost'
155+
direct_port = 'directConnectPort'
156+
direct_path = 'directConnectPath'
157+
158+
if (not {direct_protocol, direct_host, direct_port, direct_path}.issubset(set(self.capabilities))):
159+
message = 'Direct connect capabilities from server were:\n'
160+
for key in [direct_protocol, direct_host, direct_port, direct_path]:
161+
message += '{}: \'{}\'\n'.format(key, self.capabilities.get(key, ''))
162+
logger.warning(message)
163+
return
164+
165+
protocol = self.capabilities[direct_protocol]
166+
hostname = self.capabilities[direct_host]
167+
port = self.capabilities[direct_port]
168+
path = self.capabilities[direct_path]
169+
executor = '{scheme}://{hostname}:{port}{path}'.format(
170+
scheme=protocol,
171+
hostname=hostname,
172+
port=port,
173+
path=path
174+
)
175+
176+
logger.info('Updated request endpoint to %s', executor)
177+
# Override command executor
178+
self.command_executor = RemoteConnection(executor, keep_alive=keep_alive)
179+
self._addCommands()
180+
145181
def start_session(self, capabilities, browser_profile=None):
146182
"""
147183
Override for Appium

test/unit/webdriver/webdriver_test.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,78 @@ def test_find_elements_by_android_data_matcher_no_value(self):
176176
assert d['using'] == '-android datamatcher'
177177
assert d['value'] == '{}'
178178
assert len(els) == 0
179+
180+
@httpretty.activate
181+
def test_create_session_register_uridirect(self):
182+
httpretty.register_uri(
183+
httpretty.POST,
184+
'http://localhost:4723/wd/hub/session',
185+
body=json.dumps({'value': {
186+
'sessionId': 'session-id',
187+
'capabilities': {
188+
'deviceName': 'Android Emulator',
189+
'directConnectProtocol': 'http',
190+
'directConnectHost': 'localhost2',
191+
'directConnectPort': 4800,
192+
'directConnectPath': '/special/path/wd/hub',
193+
}
194+
}})
195+
)
196+
197+
httpretty.register_uri(
198+
httpretty.GET,
199+
'http://localhost2:4800/special/path/wd/hub/session/session-id/contexts',
200+
body=json.dumps({'value': ['NATIVE_APP', 'CHROMIUM']})
201+
)
202+
203+
desired_caps = {
204+
'platformName': 'Android',
205+
'deviceName': 'Android Emulator',
206+
'app': 'path/to/app',
207+
'automationName': 'UIAutomator2'
208+
}
209+
driver = webdriver.Remote(
210+
'http://localhost:4723/wd/hub',
211+
desired_caps,
212+
direct_connection=True
213+
)
214+
215+
assert 'http://localhost2:4800/special/path/wd/hub' == driver.command_executor._url
216+
assert ['NATIVE_APP', 'CHROMIUM'] == driver.contexts
217+
218+
@httpretty.activate
219+
def test_create_session_register_uridirect_no_direct_connect_path(self):
220+
httpretty.register_uri(
221+
httpretty.POST,
222+
'http://localhost:4723/wd/hub/session',
223+
body=json.dumps({'value': {
224+
'sessionId': 'session-id',
225+
'capabilities': {
226+
'deviceName': 'Android Emulator',
227+
'directConnectProtocol': 'http',
228+
'directConnectHost': 'localhost2',
229+
'directConnectPort': 4800
230+
}
231+
}})
232+
)
233+
234+
httpretty.register_uri(
235+
httpretty.GET,
236+
'http://localhost:4723/wd/hub/session/session-id/contexts',
237+
body=json.dumps({'value': ['NATIVE_APP', 'CHROMIUM']})
238+
)
239+
240+
desired_caps = {
241+
'platformName': 'Android',
242+
'deviceName': 'Android Emulator',
243+
'app': 'path/to/app',
244+
'automationName': 'UIAutomator2'
245+
}
246+
driver = webdriver.Remote(
247+
'http://localhost:4723/wd/hub',
248+
desired_caps,
249+
direct_connection=True
250+
)
251+
252+
assert 'http://localhost:4723/wd/hub' == driver.command_executor._url
253+
assert ['NATIVE_APP', 'CHROMIUM'] == driver.contexts

0 commit comments

Comments
 (0)