Skip to content

Commit c7c5a26

Browse files
committed
Merge branch 'test_windows' into test_macos
2 parents 977f5c1 + f2ce139 commit c7c5a26

File tree

10 files changed

+177
-65
lines changed

10 files changed

+177
-65
lines changed

.github/workflows/ci.yml

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ jobs:
2020
name: ${{ matrix.task.name }} - ${{ matrix.os.name }} ${{ matrix.python.name }} ${{ matrix.arch.name }}
2121
runs-on: ${{ matrix.os.runs-on }}
2222
container: ${{ matrix.os.container[matrix.python.docker] }}
23+
# present runtime seems to be about 1 minute 30 seconds
24+
timeout-minutes: 10
2325
strategy:
2426
fail-fast: false
2527
matrix:
@@ -38,10 +40,15 @@ jobs:
3840
3.7: docker://python:3.7-buster
3941
3.8: docker://python:3.8-buster
4042
3.9: docker://python:3.9-buster
41-
# - name: Windows
42-
# runs-on: windows-latest
43-
# python_platform: win32
44-
# matrix: windows
43+
pypy2: docker://pypy:2-jessie
44+
pypy3: docker://pypy:3-stretch
45+
- name: Windows
46+
runs-on: windows-latest
47+
python_platform: win32
48+
matrix: windows
49+
openssl:
50+
x86: win32
51+
x64: win64
4552
- name: macOS
4653
runs-on: macos-latest
4754
python_platform: darwin
@@ -51,27 +58,53 @@ jobs:
5158
tox: py27
5259
action: 2.7
5360
docker: 2.7
61+
matrix: 2.7
5462
implementation: cpython
63+
- name: PyPy 2.7
64+
tox: pypy27
65+
action: pypy-2.7
66+
docker: pypy2.7
67+
matrix: 2.7
68+
implementation: pypy
69+
openssl_msvc_version: 2019
5570
- name: CPython 3.6
5671
tox: py36
5772
action: 3.6
5873
docker: 3.6
74+
matrix: 3.6
5975
implementation: cpython
6076
- name: CPython 3.7
6177
tox: py37
6278
action: 3.7
6379
docker: 3.7
80+
matrix: 3.7
6481
implementation: cpython
6582
- name: CPython 3.8
6683
tox: py38
6784
action: 3.8
6885
docker: 3.8
86+
matrix: 3.8
6987
implementation: cpython
7088
- name: CPython 3.9
7189
tox: py39
7290
action: 3.9
7391
docker: 3.9
92+
matrix: 3.9
7493
implementation: cpython
94+
- name: PyPy 3.6
95+
tox: pypy36
96+
action: pypy-3.6
97+
docker: pypy3.6
98+
matrix: 3.6
99+
implementation: pypy
100+
openssl_msvc_version: 2019
101+
- name: PyPy 3.7
102+
tox: pypy37
103+
action: pypy-3.7
104+
docker: pypy3.7
105+
matrix: 3.7
106+
implementation: pypy
107+
openssl_msvc_version: 2019
75108
arch:
76109
- name: x86
77110
action: x86
@@ -88,6 +121,12 @@ jobs:
88121
matrix: macos
89122
arch:
90123
matrix: x86
124+
- os:
125+
matrix: windows
126+
python:
127+
implementation: pypy
128+
arch:
129+
matrix: x64
91130
env:
92131
# Should match name above
93132
JOB_NAME: ${{ matrix.task.name }} - ${{ matrix.os.name }} ${{ matrix.python.name }} ${{ matrix.arch.name }}
@@ -106,7 +145,30 @@ jobs:
106145
pip install --upgrade pip setuptools wheel
107146
pip install --upgrade tox
108147
- uses: twisted/[email protected]
148+
- name: Add PyPy Externals
149+
if: ${{ matrix.os.matrix == 'windows' && matrix.python.implementation == 'pypy'}}
150+
env:
151+
PYPY_EXTERNALS_PATH: ${{ github.workspace }}/pypy_externals
152+
shell: bash
153+
run: |
154+
echo $PYPY_EXTERNALS_PATH
155+
mkdir --parents $(dirname $PYPY_EXTERNALS_PATH)
156+
hg clone https://foss.heptapod.net/pypy/externals/ $PYPY_EXTERNALS_PATH
157+
dir $PYPY_EXTERNALS_PATH
158+
cd $PYPY_EXTERNALS_PATH && hg update win32_14x
159+
echo "INCLUDE=$PYPY_EXTERNALS_PATH/include;$INCLUDE" >> $GITHUB_ENV
160+
echo "LIB=$PYPY_EXTERNALS_PATH/lib;$LIB" >> $GITHUB_ENV
161+
# echo "CL=${{ matrix.PYTHON.CL_FLAGS }}" >> $GITHUB_ENV
162+
- name: rustup
163+
if: ${{ matrix.os.matrix == 'windows' && matrix.python.implementation == 'pypy'}}
164+
shell: bash
165+
run: |
166+
rustup target add i686-pc-windows-msvc
109167
- name: Test
168+
env:
169+
# When compiling Cryptography for PyPy on Windows there is a cleanup
170+
# failure. This is CI, it doesn't matter.
171+
PIP_NO_CLEAN: 1
110172
run: |
111173
tox -vv -e ${{ matrix.python.tox }}
112174
- name: Coverage Processing

pymodbus/server/sync.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -327,16 +327,17 @@ def __init__(self, context, framer=None, identity=None,
327327
self.control = ModbusControlBlock()
328328
self.address = address or ("", Defaults.Port)
329329
self.handler = handler or ModbusConnectedRequestHandler
330-
self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves',
330+
self.ignore_missing_slaves = kwargs.pop('ignore_missing_slaves',
331331
Defaults.IgnoreMissingSlaves)
332-
self.broadcast_enable = kwargs.get('broadcast_enable',
332+
self.broadcast_enable = kwargs.pop('broadcast_enable',
333333
Defaults.broadcast_enable)
334334

335335
if isinstance(identity, ModbusDeviceIdentification):
336336
self.control.Identity.update(identity)
337337

338338
socketserver.ThreadingTCPServer.__init__(self, self.address,
339-
self.handler)
339+
self.handler,
340+
**kwargs)
340341

341342
def process_request(self, request, client):
342343
""" Callback for connecting a new client thread
@@ -456,16 +457,16 @@ def __init__(self, context, framer=None, identity=None, address=None,
456457
self.control = ModbusControlBlock()
457458
self.address = address or ("", Defaults.Port)
458459
self.handler = handler or ModbusDisconnectedRequestHandler
459-
self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves',
460+
self.ignore_missing_slaves = kwargs.pop('ignore_missing_slaves',
460461
Defaults.IgnoreMissingSlaves)
461-
self.broadcast_enable = kwargs.get('broadcast_enable',
462+
self.broadcast_enable = kwargs.pop('broadcast_enable',
462463
Defaults.broadcast_enable)
463464

464465
if isinstance(identity, ModbusDeviceIdentification):
465466
self.control.Identity.update(identity)
466467

467468
socketserver.ThreadingUDPServer.__init__(self,
468-
self.address, self.handler)
469+
self.address, self.handler, **kwargs)
469470
# self._BaseServer__shutdown_request = True
470471

471472
def process_request(self, request, client):

requirements-tests.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ sqlalchemy>=1.1.15
1414
#wsgiref>=0.1.2
1515
verboselogs >= 1.5
1616
tornado==4.5.3
17-
Twisted[serial]>=20.3.0
17+
# using platform_python_implementation rather than
18+
# implementation_name for Python 2 support
19+
Twisted[conch,serial]>=20.3.0; platform_python_implementation != "PyPy" or sys_platform != "win32"
20+
# pywin32 isn't supported on pypy
21+
# https://github.com/mhammond/pywin32/issues/1289
22+
Twisted[conch]>=20.3.0; platform_python_implementation == "PyPy" and sys_platform == "win32"
1823
zope.interface>=4.4.0
1924
asynctest>=0.10.0

setup.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,12 @@
9797
'sphinx_rtd_theme',
9898
'humanfriendly'],
9999
'twisted': [
100-
'twisted[serial] >= 20.3.0',
101-
'pyasn1 >= 0.1.4',
100+
# using platform_python_implementation rather than
101+
# implementation_name for Python 2 support
102+
'Twisted[conch,serial]>=20.3.0; platform_python_implementation != "PyPy" or sys_platform != "win32"',
103+
# pywin32 isn't supported on pypy
104+
# https://github.com/mhammond/pywin32/issues/1289
105+
'Twisted[conch]>=20.3.0; platform_python_implementation == "PyPy" and sys_platform == "win32"',
102106
],
103107
'tornado': [
104108
'tornado == 4.5.3'

test/test_client_async.py

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,18 @@
3232
import ssl
3333

3434
IS_DARWIN = platform.system().lower() == "darwin"
35+
IS_WINDOWS = platform.system().lower() == "windows"
3536
OSX_SIERRA = LooseVersion("10.12")
3637
if IS_DARWIN:
3738
IS_HIGH_SIERRA_OR_ABOVE = LooseVersion(platform.mac_ver()[0])
3839
SERIAL_PORT = '/dev/ttyp0' if not IS_HIGH_SIERRA_OR_ABOVE else '/dev/ptyp0'
3940
else:
4041
IS_HIGH_SIERRA_OR_ABOVE = False
41-
SERIAL_PORT = "/dev/ptmx"
42+
if IS_WINDOWS:
43+
# the use is mocked out
44+
SERIAL_PORT = ""
45+
else:
46+
SERIAL_PORT = "/dev/ptmx"
4247

4348
# ---------------------------------------------------------------------------#
4449
# Fixture
@@ -186,6 +191,10 @@ def testUdpAsycioClient(self, mock_gather, mock_event_loop):
186191
# Test Serial client
187192
# -----------------------------------------------------------------------#
188193

194+
@pytest.mark.skipif(
195+
sys.platform == 'win32' and platform.python_implementation() == 'PyPy',
196+
reason='Twisted serial requires pywin32 which is not compatible with PyPy',
197+
)
189198
@pytest.mark.parametrize("method, framer", [("rtu", ModbusRtuFramer),
190199
("socket", ModbusSocketFramer),
191200
("binary", ModbusBinaryFramer),
@@ -196,30 +205,30 @@ def testSerialTwistedClient(self, method, framer):
196205
with patch("serial.Serial") as mock_sp:
197206
from twisted.internet import reactor
198207
from twisted.internet.serialport import SerialPort
208+
with maybe_manage(sys.platform == 'win32', patch.object(SerialPort, "_finishPortSetup")):
209+
with patch('twisted.internet.reactor') as mock_reactor:
199210

200-
with patch('twisted.internet.reactor') as mock_reactor:
201-
202-
protocol, client = AsyncModbusSerialClient(schedulers.REACTOR,
203-
method=method,
204-
port=SERIAL_PORT,
205-
proto_cls=ModbusSerClientProtocol)
211+
protocol, client = AsyncModbusSerialClient(schedulers.REACTOR,
212+
method=method,
213+
port=SERIAL_PORT,
214+
proto_cls=ModbusSerClientProtocol)
206215

207-
assert (isinstance(client, SerialPort))
208-
assert (isinstance(client.protocol, ModbusSerClientProtocol))
209-
assert (0 == len(list(client.protocol.transaction)))
210-
assert (isinstance(client.protocol.framer, framer))
211-
assert (client.protocol._connected)
216+
assert (isinstance(client, SerialPort))
217+
assert (isinstance(client.protocol, ModbusSerClientProtocol))
218+
assert (0 == len(list(client.protocol.transaction)))
219+
assert (isinstance(client.protocol.framer, framer))
220+
assert (client.protocol._connected)
212221

213-
def handle_failure(failure):
214-
assert (isinstance(failure.exception(), ConnectionException))
222+
def handle_failure(failure):
223+
assert (isinstance(failure.exception(), ConnectionException))
215224

216-
d = client.protocol._buildResponse(0x00)
217-
d.addCallback(handle_failure)
225+
d = client.protocol._buildResponse(0x00)
226+
d.addCallback(handle_failure)
218227

219-
assert (client.protocol._connected)
220-
client.protocol.close()
221-
protocol.stop()
222-
assert (not client.protocol._connected)
228+
assert (client.protocol._connected)
229+
client.protocol.close()
230+
protocol.stop()
231+
assert (not client.protocol._connected)
223232

224233
@pytest.mark.parametrize("method, framer", [("rtu", ModbusRtuFramer),
225234
("socket", ModbusSocketFramer),
@@ -228,7 +237,7 @@ def handle_failure(failure):
228237
def testSerialTornadoClient(self, method, framer):
229238
""" Test the serial tornado client client initialize """
230239
from serial import Serial
231-
with maybe_manage(sys.platform == 'darwin', patch.object(Serial, "open")):
240+
with maybe_manage(sys.platform in ('darwin', 'win32'), patch.object(Serial, "open")):
232241
protocol, future = AsyncModbusSerialClient(schedulers.IO_LOOP, method=method, port=SERIAL_PORT)
233242
client = future.result()
234243
assert(isinstance(client, AsyncTornadoModbusSerialClient))

test/test_client_sync.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
import socket
1212
import serial
1313
import ssl
14+
import sys
15+
16+
import pytest
1417

1518
from pymodbus.client.sync import ModbusTcpClient, ModbusUdpClient
1619
from pymodbus.client.sync import ModbusSerialClient, BaseModbusClient
@@ -47,6 +50,15 @@ def setblocking(self, flag): return None
4750
def in_waiting(self): return None
4851

4952

53+
inet_pton_skipif = pytest.mark.skipif(
54+
sys.platform == "win32" and sys.version_info < (3, 4),
55+
reason=(
56+
"Uses socket.inet_pton() which wasn't available on Windows until"
57+
" 3.4.",
58+
)
59+
)
60+
61+
5062

5163
# ---------------------------------------------------------------------------#
5264
# Fixture
@@ -128,13 +140,15 @@ def testBasicSyncUdpClient(self):
128140

129141
self.assertEqual("ModbusUdpClient(127.0.0.1:502)", str(client))
130142

143+
@inet_pton_skipif
131144
def testUdpClientAddressFamily(self):
132145
''' Test the Udp client get address family method'''
133146
client = ModbusUdpClient()
134147
self.assertEqual(socket.AF_INET,
135148
client._get_address_family('127.0.0.1'))
136149
self.assertEqual(socket.AF_INET6, client._get_address_family('::1'))
137150

151+
@inet_pton_skipif
138152
def testUdpClientConnect(self):
139153
''' Test the Udp client connection method'''
140154
with patch.object(socket, 'socket') as mock_method:
@@ -151,6 +165,7 @@ def settimeout(self, *a, **kwa):
151165
client = ModbusUdpClient()
152166
self.assertFalse(client.connect())
153167

168+
@inet_pton_skipif
154169
def testUdpClientIsSocketOpen(self):
155170
''' Test the udp client is_socket_open method'''
156171
client = ModbusUdpClient()

test/test_server_async.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env python
22
from pymodbus.compat import IS_PYTHON3
33
import unittest
4+
import pytest
45
if IS_PYTHON3: # Python 3
56
from unittest.mock import patch, Mock, MagicMock
67
else: # Python 2
@@ -32,6 +33,11 @@
3233
IS_HIGH_SIERRA_OR_ABOVE = False
3334
SERIAL_PORT = "/dev/ptmx"
3435

36+
no_twisted_serial_on_windows_with_pypy = pytest.mark.skipif(
37+
sys.platform == 'win32' and platform.python_implementation() == 'PyPy',
38+
reason='Twisted serial requires pywin32 which is not compatible with PyPy',
39+
)
40+
3541

3642
class AsynchronousServerTest(unittest.TestCase):
3743
'''
@@ -188,13 +194,15 @@ def testUdpServerStartup(self):
188194
self.assertEqual(mock_reactor.listenUDP.call_count, 1)
189195
self.assertEqual(mock_reactor.run.call_count, 1)
190196

197+
@no_twisted_serial_on_windows_with_pypy
191198
@patch("twisted.internet.serialport.SerialPort")
192199
def testSerialServerStartup(self, mock_sp):
193200
''' Test that the modbus serial asynchronous server starts correctly '''
194201
with patch('twisted.internet.reactor') as mock_reactor:
195202
StartSerialServer(context=None, port=SERIAL_PORT)
196203
self.assertEqual(mock_reactor.run.call_count, 1)
197204

205+
@no_twisted_serial_on_windows_with_pypy
198206
@patch("twisted.internet.serialport.SerialPort")
199207
def testStopServerFromMainThread(self, mock_sp):
200208
"""
@@ -207,6 +215,7 @@ def testStopServerFromMainThread(self, mock_sp):
207215
StopServer()
208216
self.assertEqual(mock_reactor.stop.call_count, 1)
209217

218+
@no_twisted_serial_on_windows_with_pypy
210219
@patch("twisted.internet.serialport.SerialPort")
211220
def testStopServerFromThread(self, mock_sp):
212221
"""

test/test_server_asyncio.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,9 @@ def connection_made(self, transport):
216216

217217
transport, protocol = yield from self.loop.create_connection(BasicClient, host='127.0.0.1', port=random_port)
218218
yield from step1
219-
# await asyncio.sleep(1)
219+
# On Windows we seem to need to give this an extra chance to finish,
220+
# otherwise there ends up being an active connection at the assert.
221+
yield from asyncio.sleep(0.0)
220222
self.assertTrue(len(server.active_connections) == 1)
221223

222224
protocol.transport.close() # close isn't synchronous and there's no notification that it's done

0 commit comments

Comments
 (0)