Skip to content

Commit edb6c72

Browse files
Merge pull request #41 from pact-foundation/fix-running-on-windows
Move to using the `service` command with pact-mock-service
2 parents 244fff1 + 4661406 commit edb6c72

File tree

3 files changed

+123
-25
lines changed

3 files changed

+123
-25
lines changed

pact/pact.py

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22
from __future__ import unicode_literals
33

44
import os
5+
import platform
56
from subprocess import Popen
67

8+
import psutil
79
import requests
10+
from requests.adapters import HTTPAdapter
11+
from requests.packages.urllib3 import Retry
812

913
from .constants import MOCK_SERVICE_PATH
1014
from .matchers import from_term
@@ -87,6 +91,7 @@ def __init__(self, consumer, provider, host_name='localhost', port=1234,
8791
self.sslkey = sslkey
8892
self.version = version
8993
self._interactions = []
94+
self._process = None
9095

9196
def given(self, provider_state):
9297
"""
@@ -120,10 +125,14 @@ def setup(self):
120125
raise
121126

122127
def start_service(self):
123-
"""Start the external Mock Service."""
128+
"""
129+
Start the external Mock Service.
130+
131+
:raises RuntimeError: if there is a problem starting the mock service.
132+
"""
124133
command = [
125134
MOCK_SERVICE_PATH,
126-
'start',
135+
'service',
127136
'--host={}'.format(self.host_name),
128137
'--port={}'.format(self.port),
129138
'--log', '{}/pact-mock-service.log'.format(self.log_dir),
@@ -139,17 +148,22 @@ def start_service(self):
139148
if self.sslkey:
140149
command.extend(['--sslkey', self.sslkey])
141150

142-
process = Popen(command)
143-
process.communicate()
144-
if process.returncode != 0:
145-
raise RuntimeError('The Pact mock service failed to start.')
151+
self._process = Popen(command)
152+
self._wait_for_server_start()
146153

147154
def stop_service(self):
148155
"""Stop the external Mock Service."""
149-
command = [MOCK_SERVICE_PATH, 'stop', '--port={}'.format(self.port)]
150-
popen = Popen(command)
151-
popen.communicate()
152-
if popen.returncode != 0:
156+
is_windows = 'windows' in platform.platform().lower()
157+
if is_windows:
158+
# Send the signal to ruby.exe, not the *.bat process
159+
p = psutil.Process(self._process.pid)
160+
for child in p.children(recursive=True):
161+
child.terminate()
162+
else:
163+
self._process.terminate()
164+
165+
self._process.communicate()
166+
if self._process.returncode != 0:
153167
raise RuntimeError(
154168
'There was an error when stopping the Pact mock service.')
155169

@@ -229,6 +243,23 @@ def will_respond_with(self, status, headers=None, body=None):
229243
body=body).json()
230244
return self
231245

246+
def _wait_for_server_start(self):
247+
"""
248+
Wait for the mock service to be ready for requests.
249+
250+
:rtype: None
251+
:raises RuntimeError: If there is a problem starting the mock service.
252+
"""
253+
s = requests.Session()
254+
retries = Retry(total=15, backoff_factor=0.1)
255+
s.mount('http://', HTTPAdapter(max_retries=retries))
256+
resp = s.get(self.uri, headers=self.HEADERS)
257+
if resp.status_code != 200:
258+
self._process.terminate()
259+
self._process.communicate()
260+
raise RuntimeError(
261+
'There was a problem starting the mock service: %s', resp.text)
262+
232263
def __enter__(self):
233264
"""
234265
Handler for entering a Python context.

pact/test/test_pact.py

Lines changed: 81 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import os
2+
from subprocess import Popen
23
from unittest import TestCase
34

45
from mock import patch, call, Mock
6+
from psutil import Process
57

68
from .. import Consumer, Provider, pact
79
from ..constants import MOCK_SERVICE_PATH
@@ -208,17 +210,24 @@ def setUp(self):
208210
self.addCleanup(patch.stopall)
209211
self.mock_Popen = patch.object(pact, 'Popen', autospec=True).start()
210212
self.mock_Popen.return_value.returncode = 0
213+
self.mock_Process = patch.object(
214+
pact.psutil, 'Process', autospec=True).start()
215+
self.mock_platform = patch.object(
216+
pact.platform, 'platform', autospec=True).start()
217+
self.mock_wait_for_server_start = patch.object(
218+
pact.Pact, '_wait_for_server_start', autospec=True).start()
211219

212220
def test_start_fails(self):
213221
self.mock_Popen.return_value.returncode = 1
222+
self.mock_wait_for_server_start.side_effect = RuntimeError
214223
pact = Pact(Consumer('consumer'), Provider('provider'),
215224
log_dir='/logs', pact_dir='/pacts')
216225

217226
with self.assertRaises(RuntimeError):
218227
pact.start_service()
219228

220229
self.mock_Popen.assert_called_once_with([
221-
MOCK_SERVICE_PATH, 'start',
230+
MOCK_SERVICE_PATH, 'service',
222231
'--host=localhost',
223232
'--port=1234',
224233
'--log', '/logs/pact-mock-service.log',
@@ -227,15 +236,13 @@ def test_start_fails(self):
227236
'--consumer', 'consumer',
228237
'--provider', 'provider'])
229238

230-
self.mock_Popen.return_value.communicate.assert_called_once_with()
231-
232239
def test_start_no_ssl(self):
233240
pact = Pact(Consumer('consumer'), Provider('provider'),
234241
log_dir='/logs', pact_dir='/pacts')
235242
pact.start_service()
236243

237244
self.mock_Popen.assert_called_once_with([
238-
MOCK_SERVICE_PATH, 'start',
245+
MOCK_SERVICE_PATH, 'service',
239246
'--host=localhost',
240247
'--port=1234',
241248
'--log', '/logs/pact-mock-service.log',
@@ -244,16 +251,14 @@ def test_start_no_ssl(self):
244251
'--consumer', 'consumer',
245252
'--provider', 'provider'])
246253

247-
self.mock_Popen.return_value.communicate.assert_called_once_with()
248-
249254
def test_start_with_ssl(self):
250255
pact = Pact(Consumer('consumer'), Provider('provider'),
251256
log_dir='/logs', pact_dir='/pacts',
252257
ssl=True, sslcert='/ssl.cert', sslkey='/ssl.key')
253258
pact.start_service()
254259

255260
self.mock_Popen.assert_called_once_with([
256-
MOCK_SERVICE_PATH, 'start',
261+
MOCK_SERVICE_PATH, 'service',
257262
'--host=localhost',
258263
'--port=1234',
259264
'--log', '/logs/pact-mock-service.log',
@@ -265,25 +270,86 @@ def test_start_with_ssl(self):
265270
'--sslcert', '/ssl.cert',
266271
'--sslkey', '/ssl.key'])
267272

268-
self.mock_Popen.return_value.communicate.assert_called_once_with()
273+
def test_stop_posix(self):
274+
self.mock_platform.return_value = 'Linux'
275+
pact = Pact(Consumer('consumer'), Provider('provider'))
276+
pact._process = Mock(spec=Popen, pid=999, returncode=0)
277+
pact.stop_service()
278+
279+
pact._process.terminate.assert_called_once_with()
280+
pact._process.communicate.assert_called_once_with()
281+
self.assertFalse(self.mock_Process.called)
269282

270-
def test_stop(self):
283+
def test_stop_windows(self):
284+
self.mock_platform.return_value = 'Windows'
285+
ruby_exe = Mock(spec=Process)
286+
self.mock_Process.return_value.children.return_value = [ruby_exe]
271287
pact = Pact(Consumer('consumer'), Provider('provider'))
288+
pact._process = Mock(spec=Popen, pid=999, returncode=0)
272289
pact.stop_service()
273290

274-
self.mock_Popen.assert_called_once_with(
275-
[MOCK_SERVICE_PATH, 'stop', '--port=1234'])
276-
self.mock_Popen.return_value.communicate.assert_called_once_with()
291+
self.assertFalse(pact._process.terminate.called)
292+
pact._process.communicate.assert_called_once_with()
293+
self.mock_Process.assert_called_once_with(999)
294+
self.mock_Process.return_value.children.assert_called_once_with(
295+
recursive=True)
296+
ruby_exe.terminate.assert_called_once_with()
277297

278298
def test_stop_fails(self):
279299
self.mock_Popen.return_value.returncode = 1
280300
pact = Pact(Consumer('consumer'), Provider('provider'))
301+
pact._process = Mock(spec=Popen, pid=999, returncode=1)
281302
with self.assertRaises(RuntimeError):
282303
pact.stop_service()
283304

284-
self.mock_Popen.assert_called_once_with(
285-
[MOCK_SERVICE_PATH, 'stop', '--port=1234'])
286-
self.mock_Popen.return_value.communicate.assert_called_once_with()
305+
pact._process.terminate.assert_called_once_with()
306+
pact._process.communicate.assert_called_once_with()
307+
308+
309+
class PactWaitForServerStartTestCase(TestCase):
310+
def setUp(self):
311+
super(PactWaitForServerStartTestCase, self).setUp()
312+
self.addCleanup(patch.stopall)
313+
self.mock_HTTPAdapter = patch.object(
314+
pact, 'HTTPAdapter', autospec=True).start()
315+
self.mock_Retry = patch.object(pact, 'Retry', autospec=True).start()
316+
self.mock_Session = patch.object(
317+
pact.requests, 'Session', autospec=True).start()
318+
319+
def test_wait_for_server_start_success(self):
320+
self.mock_Session.return_value.get.return_value.status_code = 200
321+
pact = Pact(Consumer('consumer'), Provider('provider'))
322+
pact._process = Mock(spec=Popen)
323+
pact._wait_for_server_start()
324+
325+
session = self.mock_Session.return_value
326+
session.mount.assert_called_once_with(
327+
'http://', self.mock_HTTPAdapter.return_value)
328+
session.get.assert_called_once_with(
329+
'http://localhost:1234', headers={'X-Pact-Mock-Service': 'true'})
330+
self.mock_HTTPAdapter.assert_called_once_with(
331+
max_retries=self.mock_Retry.return_value)
332+
self.mock_Retry.assert_called_once_with(total=15, backoff_factor=0.1)
333+
self.assertFalse(pact._process.communicate.called)
334+
self.assertFalse(pact._process.terminate.called)
335+
336+
def test_wait_for_server_start_failure(self):
337+
self.mock_Session.return_value.get.return_value.status_code = 500
338+
pact = Pact(Consumer('consumer'), Provider('provider'))
339+
pact._process = Mock(spec=Popen)
340+
with self.assertRaises(RuntimeError):
341+
pact._wait_for_server_start()
342+
343+
session = self.mock_Session.return_value
344+
session.mount.assert_called_once_with(
345+
'http://', self.mock_HTTPAdapter.return_value)
346+
session.get.assert_called_once_with(
347+
'http://localhost:1234', headers={'X-Pact-Mock-Service': 'true'})
348+
self.mock_HTTPAdapter.assert_called_once_with(
349+
max_retries=self.mock_Retry.return_value)
350+
self.mock_Retry.assert_called_once_with(total=15, backoff_factor=0.1)
351+
pact._process.communicate.assert_called_once_with()
352+
pact._process.terminate.assert_called_once_with()
287353

288354

289355
class PactVerifyTestCase(PactTestCase):

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
click==6.7
2+
psutil==5.2.2
23
requests==2.12.3
34
six==1.10.0

0 commit comments

Comments
 (0)