Skip to content

Commit 1df94f6

Browse files
authored
handle allocation wait timeout properly and run tests in py3.11 (#31)
1 parent 43de787 commit 1df94f6

File tree

7 files changed

+89
-50
lines changed

7 files changed

+89
-50
lines changed

.github/workflows/test.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,22 @@ jobs:
1515
run-unittests:
1616
runs-on: ${{ matrix.os }}
1717
strategy:
18+
fail-fast: false
1819
matrix:
1920
os: [ ubuntu-latest, macos-latest, windows-latest ]
20-
python-version: [ '3.7', '3.8', '3.9', '3.10' ]
21+
python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11' ]
2122
name: ${{ matrix.os }}-Python-${{ matrix.python-version }}
2223
steps:
23-
- uses: actions/checkout@v3
24+
- uses: actions/checkout@v4
2425

2526
- name: Set up Python
26-
uses: actions/setup-python@v4
27+
uses: actions/setup-python@v5
2728
with:
2829
python-version: ${{ matrix.python-version }}
2930

3031
- name: Setup Node.js
3132
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.10'
32-
uses: actions/setup-node@v3
33+
uses: actions/setup-node@v4
3334
with:
3435
node-version: '16'
3536

README.md

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,30 @@
55
[![PyPI version](https://badge.fury.io/py/stf-appium-client.svg)](https://badge.fury.io/py/stf-appium-client)
66

77
Library provides basic functionality for test automation which allows allocating
8-
phone from [OpenSTF](https://github.com/DeviceFarmer/stf) server using [python stf-client](https://pypi.org/project/stf-client/), initialise adb connection to it and
8+
phone from [STF](https://github.com/DeviceFarmer/stf) server using [python stf-client](https://pypi.org/project/stf-client/), initialise adb connection to it and
99
start [appium][https://github.com/appium/python-client] server for it.
1010

1111
Basic idea is to run tests against remote openstf device farm with minimum
1212
requirements.
1313

1414

1515
### Flow
16+
```mermaid
17+
sequenceDiagram
18+
participant C as User
19+
participant A as stf-appium-client
20+
participant B as STF(device)
21+
C->>A: allocation_context(requirements, wait_timeout, timeout, shuffle)
22+
A->>B: Find suitable device
23+
A->>B: allocate device
24+
A->>B: remoteConnect
25+
A->>B: ADB Connection
26+
A->>A: Start AppiumServer(ADB)
27+
A->>A: Start AppiumClient(AppiumServer)
28+
A->>C: AppiumClient(AppiumServer(ADB))
29+
C->>A: Run Appium Tests
1630
```
17-
stf-appium-client --find/allocate--> OpenSTF(device)
18-
stf-appium-client --remoteConnect--> OpenSTF(device)
19-
stf-appium-client(ADB) <----------------> OpenSTF(ADB)
20-
stf-appium-client(AppiumServer(ADB))
21-
stf-appium-client(AppiumClient(AppiumServer))
22-
..appium tests..
23-
```
31+
2432

2533
### Getting Started
2634

@@ -36,6 +44,8 @@ These instructions will get you a copy of the project up and running on your loc
3644
* remember to install appium drivers, e.g. `appium driver install uiautomator2`
3745
* appium 1
3846
* note that appium server and client need to be compatible with each other!
47+
* see compatibility matrix from [python-client readme](https://github.com/appium/python-client?tab=readme-ov-file#compatibility-matrix)
48+
3949
### Installing
4050

4151
* `pip install stf-appium-client`
@@ -56,6 +66,7 @@ CI runs tests against following environments:
5666
| 3.8 ||||
5767
| 3.9 ||||
5868
| 3.10 ||||
69+
| 3.11 ||||
5970

6071
### Deployment
6172

@@ -140,6 +151,6 @@ optional arguments:
140151

141152
```
142153
143-
License
154+
## License
144155
145156
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details

setup.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env python
22
# -*- coding: utf-8 -*-
33
"""
4-
:copyright: (c) 2021 by Jussi Vatjus-Anttila
4+
:copyright: (c) 2024 by Jussi Vatjus-Anttila
55
:license: MIT, see LICENSE for more details.
66
"""
77
from setuptools import setup, find_packages
@@ -21,6 +21,7 @@
2121
Programming Language :: Python :: 3.8
2222
Programming Language :: Python :: 3.9
2323
Programming Language :: Python :: 3.10
24+
Programming Language :: Python :: 3.11
2425
Topic :: Software Development :: Testing
2526
""".strip().splitlines()
2627

@@ -53,9 +54,10 @@
5354
extras_require={ # Optional
5455
'dev': ['wheel', 'mock', 'pylint', 'pytest', 'pytest-cov', 'pytest-mock', 'pyinstaller', 'coveralls']
5556
},
56-
keywords="OpenSTF appium robot-framework lockable resource android",
57+
keywords="DeviceFarmer STF appium pytest robot-framework lockable resource android",
5758
python_requires=">=3.7",
58-
project_urls={ # Optionaly
59+
project_urls={
60+
'Homepage': 'https://github.com/OpenTMI/stf-appium-python-client',
5961
'Bug Reports': 'https://github.com/OpenTMI/stf-appium-python-client/issues',
6062
'Source': 'https://github.com/OpenTMI/stf-appium-python-client',
6163
}

stf_appium_client/AppiumServer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def get_api_path(self) -> str:
2929
if version.startswith("1."):
3030
return f'http://127.0.0.1:{self.port}/wd/hub'
3131
else:
32-
return f'http://127.0.0.1:{self.port}'
32+
return f'http://127.0.0.1:{self.port}' # Appium >= 2.0
3333

3434
def start(self):
3535
assert not self.service.is_running, 'Appium already running'

stf_appium_client/StfClient.py

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@
1414
from stf_client.api.user_api import UserApi
1515
from stf_client.api.devices_api import DevicesApi
1616

17+
1718
class StfClient(Logger):
1819
DEFAULT_ALLOCATION_TIMEOUT_SECONDS = 900
1920

2021
def __init__(self, host: str):
2122
"""
22-
OpenSTF Client consructor
23+
STF Client constructor
2324
:param host: Server address of OpenSTF
2425
"""
2526
super().__init__()
@@ -185,11 +186,15 @@ def find_and_allocate(self, requirements: dict,
185186
timeout_seconds: int = DEFAULT_ALLOCATION_TIMEOUT_SECONDS,
186187
shuffle: bool = True) -> dict:
187188
"""
188-
Find device based on requirements and allocate first
189+
Find device based on requirements and allocate first.
190+
Note that this method doesn't wait for device to be free.
191+
189192
:param requirements: dictionary about requirements, e.g. `dict(platform='android')`
190193
:param timeout_seconds: allocation timeout when idle, see more from allocation api.
191194
:param shuffle: randomize allocation
192195
:return: device dictionary
196+
197+
:raises DeviceNotFound: suitable device not found or all devices are allocated already
193198
"""
194199
NotConnectedError.invariant(self._client, 'Not connected')
195200
suitable_devices = self.list_devices(requirements=requirements)
@@ -199,22 +204,20 @@ def find_and_allocate(self, requirements: dict,
199204

200205
self.logger.debug(f'Found {len(suitable_devices)} suitable devices, try to allocate one')
201206

202-
def allocate_first():
203-
def try_allocate(device):
204-
try:
205-
return self.allocate(device, timeout_seconds=timeout_seconds)
206-
except (AssertionError, ForbiddenException) as error:
207-
self.logger.warning(f"{device.get('serial')}Allocation fails: {error}")
208-
return None
209-
210-
tasks = map_(suitable_devices, lambda item: wrap(item, try_allocate))
211-
return find(tasks, lambda allocFunc: allocFunc())
212-
213-
result = allocate_first()
214-
if not result:
215-
raise DeviceNotFound()
216-
device = result.args[0]
217-
return device
207+
def try_allocate(device_candidate):
208+
try:
209+
return self.allocate(device_candidate, timeout_seconds=timeout_seconds)
210+
except (AssertionError, ForbiddenException) as error:
211+
self.logger.warning(f"{device_candidate.get('serial')} allocation fails: {error}")
212+
return None
213+
214+
# generate try_allocate tasks for suitable devices
215+
tasks = map_(suitable_devices, lambda item: wrap(item, try_allocate))
216+
# find first successful allocation
217+
result = find(tasks, lambda allocFunc: allocFunc())
218+
219+
DeviceNotFound.invariant(result, 'no suitable devices found')
220+
return result.args[0]
218221

219222
def find_wait_and_allocate(self,
220223
requirements: dict,
@@ -229,19 +232,24 @@ def find_wait_and_allocate(self,
229232
:param shuffle: allocate suitable device randomly.
230233
:return: device dictionary
231234
"""
232-
device = None
233-
for i in range(wait_timeout): # try to allocate for 1 minute..
235+
wait_until = time.time() + wait_timeout
236+
print(f'wait_until: {wait_until}')
237+
while True:
238+
remaining_time = int(wait_until - time.time())
239+
print(f'remaining_time: {remaining_time}')
234240
try:
235-
device = self.find_and_allocate(requirements=requirements,
236-
timeout_seconds=timeout_seconds,
237-
shuffle=shuffle)
238-
break
241+
return self.find_and_allocate(requirements=requirements,
242+
timeout_seconds=timeout_seconds,
243+
shuffle=shuffle)
239244
except DeviceNotFound:
240245
# Wait a while
241-
time.sleep(1)
242-
pass
243-
DeviceNotFound.invariant(device, 'Suitable device not found')
244-
return device
246+
self.logger.debug(f'Suitable device not available, '
247+
f'wait a while and try again. Timeout in {remaining_time} seconds')
248+
if (wait_until - time.time()) <= 0:
249+
break
250+
# Wait a while to avoid too frequent polling
251+
time.sleep(1)
252+
raise DeviceNotFound(f'Suitable device not found within {wait_timeout}s timeout ({json.dumps(requirements)})')
245253

246254
@contextmanager
247255
def allocation_context(self, requirements: dict,
@@ -267,4 +275,3 @@ def allocation_context(self, requirements: dict,
267275
self.remote_connect(device)
268276
yield device
269277
self.release(device)
270-

stf_appium_client/cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ def main():
2828
'DEV1_MODEL\n'
2929
'DEV1_MARKET_NAME\n'
3030
'DEV1_REQUIREMENTS user given requirements\n'
31-
'DEV1_INFO phone details\n'
32-
'\nExample: stf --token 123 -- echo \$DEV1_SERIAL',
31+
'DEV1_INFO phone details\n\n'
32+
'Example: stf --token 123 -- echo $DEV1_SERIAL',
3333
formatter_class=argparse.RawTextHelpFormatter)
3434
parser.add_argument('--token',
3535
required=True,

test/test_StfClient.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99
from stf_appium_client.exceptions import *
1010

1111

12-
13-
1412
class TestStfClientBasics(unittest.TestCase):
1513

1614
@classmethod
@@ -214,4 +212,24 @@ def test_allocation_context_wait_success(self, mock_sleep):
214212
self.assertEqual(device['serial'], '123')
215213
self.assertEqual(device['remote_adb_url'], url)
216214

215+
@patch('time.sleep', side_effect=MagicMock())
216+
def test_allocation_context_timeout(self, mock_sleep):
217+
dev1 = {'serial': '123', 'present': True, 'ready': True, 'using': True, 'owner': "asd", 'status': 3}
218+
self.client.get_devices = MagicMock(return_value=[dev1])
219+
220+
with self.assertRaises(DeviceNotFound) as error:
221+
with self.client.allocation_context({"serial": '123'}, wait_timeout=0) as device:
222+
pass
223+
self.assertEqual(str(error.exception), 'Suitable device not found within 0s timeout ({"serial": "123"})')
217224

225+
@patch('time.sleep', return_value=MagicMock())
226+
@patch('time.time')
227+
def test_allocation_context_timeout_long(self, mock_time, mock_sleep):
228+
dev1 = {'serial': '123', 'present': True, 'ready': True, 'using': True, 'owner': "asd", 'status': 3}
229+
self.client.get_devices = MagicMock(return_value=[dev1])
230+
self.client.stf_find_and_allocate = MagicMock(side_effect=DeviceNotFound)
231+
mock_time.side_effect = [0, 0, 0, 10, 10, 10]
232+
with self.assertRaises(DeviceNotFound) as error:
233+
with self.client.allocation_context({"serial": '123'}, wait_timeout=10):
234+
pass
235+
self.assertEqual(str(error.exception), 'Suitable device not found within 10s timeout ({"serial": "123"})')

0 commit comments

Comments
 (0)