Test & infrastructure cleanup, stage 1 (#850)

* Add new JSON IPC implementation to be shared between the product and the tests.

Add pytest-based test support, and wire it up to setup.py and Travis.

Dial pylint down to complain about important things only.

Various minor fixes exposed by pylint.

Add basic .vscode/settings.json for linter settings (and anything else that's workspace-specific).

Fixes #831.
This commit is contained in:
Pavel Minaev 2018-09-27 00:36:48 -07:00 committed by GitHub
parent 04cdf337ff
commit b47df51478
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 902 additions and 8 deletions

View file

@ -1,5 +1,8 @@
[flake8]
ignore = E24,E121,E123,E125,E126,E221,E226,E266,E704,E265
exclude =
ignore = W,
E24,E121,E123,E125,E126,E221,E226,E266,E704,
E265,E722,E501,E731,E306,E401,E302,E222
exclude =
ptvsd/_vendored/pydevd,
./.eggs,
./versioneer.py

3
.gitignore vendored
View file

@ -45,6 +45,7 @@ nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
@ -101,8 +102,8 @@ ENV/
.mypy_cache/
# vs code and vs
.vscode/
.vs/
.vscode/
# PyDev
.project

6
.pylintrc Normal file
View file

@ -0,0 +1,6 @@
[MESSAGES CONTROL]
# Disable
disable=C,R,
attribute-defined-outside-init,
redefined-builtin,
unused-argument

View file

@ -22,8 +22,11 @@ matrix:
#- python: 3.6
#- env: TARGET=ci-check-schemafile
before_install: |
export TRAVIS_PYTHON_PATH=`which python`
before_install:
- export TRAVIS_PYTHON_PATH=`which python`
# Travis comes with an ancient version of pytest preinstalled globally,
# which breaks automatic dependency checks for setup.py. Force upgrade.
- $TRAVIS_PYTHON_PATH -m pip install --upgrade pytest
install:
- make depends PYTHON=$TRAVIS_PYTHON_PATH
@ -31,3 +34,4 @@ install:
script:
#- make $TARGET PYTHON=python$TRAVIS_PYTHON_VERSION
- make $TARGET PYTHON=$TRAVIS_PYTHON_PATH

View file

@ -15,6 +15,7 @@ depends:
$(PYTHON) -m pip install requests
$(PYTHON) -m pip install flask
$(PYTHON) -m pip install django
$(PYTHON) -m pip install pytest
.PHONY: lint
lint: ## Lint the Python source code.
@ -49,6 +50,7 @@ ci-lint: depends lint
ci-test: depends
# For now we use --quickpy2.
$(PYTHON) -m tests -v --full --no-network --quick-py2
$(PYTHON) setup.py test
.PHONY: ci-coverage
ci-coverage: depends

13
ptvsd.code-workspace Normal file
View file

@ -0,0 +1,13 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
"python.linting.enabled": true,
"python.linting.pylintEnabled": false,
// "python.unitTest.pyTestEnabled": true,
// "python.unitTest.pyTestArgs": ["--no-cov"],
}
}

23
ptvsd/compat.py Normal file
View file

@ -0,0 +1,23 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE in the project root
# for license information.
# Python 2.x/3.x compatibility helpers
try:
import builtins
except:
import __builtin__ as builtins
try:
unicode = builtins.unicode
bytes = builtins.str
except:
unicode = builtins.str
bytes = builtins.bytes
try:
xrange = builtins.xrange
except:
xrange = builtins.range

293
ptvsd/messaging.py Normal file
View file

@ -0,0 +1,293 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE in the project root
# for license information.
from __future__ import print_function, with_statement, absolute_import
import collections
import itertools
import json
import sys
import threading
class JsonIOStream(object):
"""Implements a JSON value stream over two byte streams (input and output).
Each value is encoded as a packet consisting of a header and a body, as defined by the
Debug Adapter Protocol (https://microsoft.github.io/debug-adapter-protocol/overview).
"""
MAX_BODY_SIZE = 0xFFFFFF
@classmethod
def from_stdio(cls):
if sys.version_info >= (3,):
stdin = sys.stdin.buffer
stdout = sys.stdout.buffer
else:
stdin = sys.stdin
stdout = sys.stdout
if sys.platform == 'win32':
import os, msvcrt
msvcrt.setmode(stdin.fileno(), os.O_BINARY)
msvcrt.setmode(stdout.fileno(), os.O_BINARY)
return cls(stdin, stdout)
def __init__(self, reader, writer):
"""Creates a new JsonIOStream.
reader is a BytesIO-like object from which incoming messages are read;
reader.readline() must treat '\n' as the line terminator, and must leave
'\r' as is (i.e. it must not translate '\r\n' to just plain '\n'!).
writer is a BytesIO-like object to which outgoing messages are written.
"""
self._reader = reader
self._writer = writer
def _read_line(self):
line = b''
while True:
line += self._reader.readline()
if not line:
raise EOFError
if line.endswith(b'\r\n'):
line = line[0:-2]
return line
def read_json(self):
"""Read a single JSON value from reader.
Returns JSON value as parsed by json.loads(), or raises EOFError
if there are no more objects to be read.
"""
headers = {}
while True:
line = self._read_line()
if line == b'':
break
key, _, value = line.partition(b':')
headers[key] = value
try:
length = int(headers[b'Content-Length'])
if not (0 <= length <= self.MAX_BODY_SIZE):
raise ValueError
except (KeyError, ValueError):
raise IOError('Content-Length is missing or invalid')
body = self._reader.read(length)
if isinstance(body, bytes):
body = body.decode('utf-8')
return json.loads(body)
def write_json(self, value):
"""Write a single JSON object to writer.
object must be in the format suitable for json.dump().
"""
body = json.dumps(value, sort_keys=True)
if not isinstance(body, bytes):
body = body.encode('utf-8')
header = 'Content-Length: %d\r\n\r\n' % len(body)
if not isinstance(header, bytes):
header = header.encode('ascii')
self._writer.write(header)
self._writer.write(body)
class JsonMemoryStream(object):
"""Like JsonIOStream, but without serialization, working directly
with values stored as-is in memory.
For input, values are read from the supplied sequence or iterator.
For output, values are appended to the supplied collection.
"""
def __init__(self, input, output):
self._input = iter(input)
self._output = output
def read_json(self):
try:
return next(self._input)
except StopIteration:
raise EOFError
def write_json(self, value):
self._output.append(value)
Response = collections.namedtuple('Response', ('success', 'command', 'error_message', 'body'))
Response.__new__.__defaults__ = (None, None)
class Response(Response):
"""Represents a received response to a Request."""
class Request(object):
"""Represents a request that was sent to the other party, and is awaiting or has
already received a response.
"""
def __init__(self, channel, seq):
self.channel = channel
self.seq = seq
self.response = None
self._lock = threading.Lock()
self._got_response = threading.Event()
self._handler = lambda _: None
def _handle_response(self, success, command, error_message=None, body=None):
assert self.response is None
with self._lock:
response = Response(success, command, error_message, body)
self.response = response
handler = self._handler
handler(response)
self._got_response.set()
def wait_for_response(self):
self._got_response.wait()
return self.response
def on_response(self, handler):
with self._lock:
response = self.response
if response is None:
self._handler = handler
return
handler(response)
class JsonMessageChannel(object):
"""Implements a JSON message channel on top of a JSON stream, with
support for generic Request, Response and Event messages as defined by the
Debug Adapter Protocol (https://microsoft.github.io/debug-adapter-protocol/overview).
"""
def __init__(self, stream, handlers=None):
self.send_callback = lambda channel, message: None
self.receive_callback = lambda channel, message: None
self._lock = threading.Lock()
self._stop = threading.Event()
self._stream = stream
self._seq_iter = itertools.count(1)
self._requests = {}
self._handlers = handlers
self._worker = threading.Thread(target=self._process_incoming_messages)
self._worker.daemon = True
def start(self):
self._worker.start()
def wait(self):
self._worker.join()
def _send_message(self, type, rest={}):
with self._lock:
seq = next(self._seq_iter)
message = {
'seq': seq,
'type': type,
}
message.update(rest)
with self._lock:
self._stream.write_json(message)
self.send_callback(self, message)
return seq
def send_request(self, command, arguments=None):
d = {'command': command}
if arguments is not None:
d['arguments'] = arguments
seq = self._send_message('request', d)
request = Request(self, seq)
with self._lock:
self._requests[seq] = request
return request
def send_event(self, event, body=None):
d = {'event': event}
if body is not None:
d['body'] = body
self._send_message('event', d)
def send_response(self, request_seq, success, command, error_message=None, body=None):
d = {
'request_seq': request_seq,
'success': success,
'command': command,
}
if success:
if body is not None:
d['body'] = body
else:
if error_message is not None:
d['message'] = error_message
self._send_message('response', d)
def on_message(self, message):
self.receive_callback(self, message)
seq = message['seq']
typ = message['type']
if typ == 'request':
command = message['command']
arguments = message.get('arguments', None)
self.on_request(seq, command, arguments)
elif typ == 'event':
event = message['event']
body = message.get('body', None)
self.on_event(seq, event, body)
elif typ == 'response':
request_seq = message['request_seq']
success = message['success']
command = message['command']
error_message = message.get('message', None)
body = message.get('body', None)
self.on_response(seq, request_seq, success, command, error_message, body)
else:
raise IOError('Incoming message has invalid "type":\n%r' % message)
def on_request(self, seq, command, arguments):
handler_name = '%s_request' % command
specific_handler = getattr(self._handlers, handler_name, None)
if specific_handler is not None:
handler = lambda: specific_handler(self, arguments)
else:
generic_handler = getattr(self._handlers, 'request')
handler = lambda: generic_handler(self, command, arguments)
try:
response_body = handler()
except Exception as ex:
self.send_response(seq, False, command, str(ex))
else:
self.send_response(seq, True, command, None, response_body)
def on_event(self, seq, event, body):
handler_name = '%s_event' % event
specific_handler = getattr(self._handlers, handler_name, None)
if specific_handler is not None:
handler = lambda: specific_handler(self, body)
else:
generic_handler = getattr(self._handlers, 'event')
handler = lambda: generic_handler(self, event, body)
handler()
def on_response(self, seq, request_seq, success, command, error_message, body):
try:
with self._lock:
request = self._requests.pop(request_seq)
except KeyError:
raise KeyError('Received response to unknown request %d', request_seq)
return request._handle_response(success, command, error_message, body)
def _process_incoming_messages(self):
while True:
try:
message = self._stream.read_json()
except EOFError:
break
try:
self.on_message(message)
except Exception:
print('Error while processing message:\n%r\n\n' % message, file=sys.__stderr__)
raise

View file

@ -2144,7 +2144,7 @@ class VSCodeMessageProcessor(VSCLifecycleMsgProcessor):
else:
is_logpoint = True
condition = None
expressions = re.findall('\{.*?\}', logMessage)
expressions = re.findall(r'\{.*?\}', logMessage)
if len(expressions) == 0:
expression = '{}'.format(repr(logMessage)) # noqa
else:

5
pytests/__init__.py Normal file
View file

@ -0,0 +1,5 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE in the project root
# for license information.
__doc__ = """pytest-based ptvsd tests."""

5
pytests/func/__init__.py Normal file
View file

@ -0,0 +1,5 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE in the project root
# for license information.
__doc__ = """ptvsd functional tests."""

View file

@ -0,0 +1,5 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE in the project root
# for license information.
__doc__ = """ptvsd functional test helpers."""

81
pytests/helpers/jsonre.py Normal file
View file

@ -0,0 +1,81 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE in the project root
# for license information.
from collections import defaultdict
class Any(object):
"""Represents a wildcard in a pattern as used by json_matches(),
and matches any single object in the same place in input data.
"""
def __repr__(self):
return '<*>'
@staticmethod
def dict_with(items):
return Any.DictWith(items)
class DictWith(defaultdict):
"""A dict subclass that returns ANY for any non-existent key.
This can be used in conjunction with json_matches to match some keys
in a dict while ignoring others. For example:
d1 = {'a': 1, 'b': 2, 'c': 3}
d2 = {'a': 1, 'b': 2}
json_matches(d1, d2) # False
json_matches(d1, ANY.dict_with(d2)) # True
"""
def __init__(self, other=None):
super(Any.DictWith, self).__init__(lambda: ANY, other or {})
def __repr__(self):
return dict.__repr__(self)[:-2] + ', ...}'
ANY = Any()
def json_matches(data, pattern):
"""Match data against pattern, returning True if it matches, and False otherwise.
The data argument must be an object obtained via json.load, or equivalent.
In other words, it must be a recursive data structure consisting only of
dicts, lists, strings, numbers, Booleans, and None.
The pattern argument is like data, but can also use the special value ANY.
For strings, numbers, Booleans and None, the data matches the pattern if they're
equal as defined by ==, or if the pattern is ANY.
For lists, the data matches the pattern if they're both of the same length,
and if every element json_matches() the element in the other list with the same
index.
For dicts, the data matches the pattern if, for every K in data.keys() + pattern.keys(),
data.has_key(K) and json_matches(data[K], pattern[K]). ANY.dict_with() can be used to
perform partial matches.
See test_jsonre.py for examples.
"""
if isinstance(pattern, Any):
return True
elif isinstance(data, list):
return len(data) == len(pattern) and all((json_matches(x, y) for x, y in zip(data, pattern)))
elif isinstance(data, dict):
keys = set(tuple(data.keys()) + tuple(pattern.keys()))
def pairs_match(key):
try:
dval = data[key]
pval = pattern[key]
except KeyError:
return False
return json_matches(dval, pval)
return all((pairs_match(key) for key in keys))
else:
return data == pattern

View file

@ -0,0 +1,99 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE in the project root
# for license information.
from .jsonre import ANY, json_matches
def test_scalars():
values = [None, True, False, 0, -1, -1.0, 1.23, 'abc', 'abcd']
for x in values:
assert json_matches(x, ANY)
for y in values:
assert json_matches(x, y) == (x == y)
def test_lists():
assert json_matches([], [])
assert json_matches(
[1, 2, 3],
ANY)
assert json_matches(
[1, 2, 3],
[1, 2, 3])
assert not json_matches(
[1, 2, 3],
[1, 2, 3, 4])
assert not json_matches(
[1, 2, 3],
[1, 2, 3, 4])
assert not json_matches(
[1, 2, 3],
[2, 3, 1])
assert json_matches(
[1, 2, 3],
[1, ANY, 3])
assert not json_matches(
[1, 2, 3, 4],
[1, ANY, 4])
def test_dicts():
assert json_matches({}, {})
assert json_matches(
{'a': 1, 'b': 2},
ANY)
assert json_matches(
{'a': 1, 'b': 2},
{'b': 2, 'a': 1})
assert not json_matches(
{'a': 1, 'b': 2},
{'a': 1, 'b': 2, 'c': 3})
assert not json_matches(
{'a': 1, 'b': 2, 'c': 3},
{'a': 1, 'b': 2})
assert json_matches(
{'a': 1, 'b': 2},
{'a': ANY, 'b': 2})
assert json_matches(
{'a': 1, 'b': 2},
ANY.dict_with({'a': 1}))
def test_recursive():
assert json_matches(
[
False,
True,
[1, 2, 3, {'aa': 4}],
{
'ba': [5, 6],
'bb': [None],
'bc': {},
'bd': True,
'be': [],
}
],
[
ANY,
True,
[1, ANY, 3, {'aa': 4}],
ANY.dict_with({
'ba': ANY,
'bb': [None],
'bc': {},
}),
])

3
pytests/pytest.ini Normal file
View file

@ -0,0 +1,3 @@
[pytest]
timeout=3
#addopts=--verbose

343
pytests/test_messaging.py Normal file
View file

@ -0,0 +1,343 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE in the project root
# for license information.
from __future__ import print_function, with_statement, absolute_import
import json
import io
import pytest
import random
import socket
import threading
import time
from ptvsd.messaging import JsonIOStream, JsonMemoryStream, JsonMessageChannel, Response
class TestJsonIOStream(object):
MESSAGE_BODY_TEMPLATE = u'{"arguments": {"threadId": 3}, "command": "next", "seq": %d, "type": "request"}'
MESSAGES = []
SERIALIZED_MESSAGES = b''
@classmethod
def setup_class(cls):
for seq in range(0, 3):
message_body = cls.MESSAGE_BODY_TEMPLATE % seq
message = json.loads(message_body)
message_body = message_body.encode('utf-8')
cls.MESSAGES.append(message)
message_header = u'Content-Length: %d\r\n\r\n' % len(message_body)
cls.SERIALIZED_MESSAGES += message_header.encode('ascii') + message_body
def test_read(self):
data = io.BytesIO(self.SERIALIZED_MESSAGES)
stream = JsonIOStream(data, data)
for expected_message in self.MESSAGES:
message = stream.read_json()
assert message == expected_message
with pytest.raises(EOFError):
stream.read_json()
def test_write(self):
data = io.BytesIO()
stream = JsonIOStream(data, data)
for message in self.MESSAGES:
stream.write_json(message)
data = data.getvalue()
assert data == self.SERIALIZED_MESSAGES
class TestJsonMemoryStream(object):
MESSAGES = [
{'seq': 1, 'type': 'request', 'command': 'next', 'arguments': {'threadId': 3}},
{'seq': 2, 'type': 'request', 'command': 'next', 'arguments': {'threadId': 5}},
]
def test_read(self):
stream = JsonMemoryStream(self.MESSAGES, [])
for expected_message in self.MESSAGES:
message = stream.read_json()
assert message == expected_message
with pytest.raises(EOFError):
stream.read_json()
def test_write(self):
messages = []
stream = JsonMemoryStream([], messages)
for message in self.MESSAGES:
stream.write_json(message)
assert messages == self.MESSAGES
class TestJsonMessageChannel(object):
logging_lock = threading.Lock()
@staticmethod
def make_channel_with_logging(stream, handlers):
def send_callback(channel, message):
with TestJsonMessageChannel.logging_lock:
print(id(channel), '->', message)
def receive_callback(channel, message):
with TestJsonMessageChannel.logging_lock:
print(id(channel), '<-', message)
channel = JsonMessageChannel(stream, handlers)
channel.send_callback = send_callback
channel.receive_callback = receive_callback
return channel
@staticmethod
def iter_with_event(collection):
"""Like iter(), but also exposes a threading.Event that is set
when the returned iterator is exhausted.
"""
exhausted = threading.Event()
def iterate():
for x in collection:
yield x
exhausted.set()
return iterate(), exhausted
def test_events(self):
EVENTS = [
{'seq': 1, 'type': 'event', 'event': 'stopped', 'body': {'reason': 'pause'}},
{'seq': 2, 'type': 'event', 'event': 'unknown', 'body': {'something': 'else'}},
]
events_received = []
class Handlers(object):
def stopped_event(self, channel, body):
events_received.append((channel, body))
def event(self, channel, event, body):
events_received.append((channel, event, body))
input, input_exhausted = self.iter_with_event(EVENTS)
stream = JsonMemoryStream(input, [])
handlers = Handlers()
channel = self.make_channel_with_logging(stream, handlers)
channel.start()
input_exhausted.wait()
assert events_received == [
(channel, EVENTS[0]['body']),
(channel, 'unknown', EVENTS[1]['body']),
]
def test_requests(self):
REQUESTS = [
{'seq': 1, 'type': 'request', 'command': 'next', 'arguments': {'threadId': 3}},
{'seq': 2, 'type': 'request', 'command': 'unknown', 'arguments': {'answer': 42}},
{'seq': 3, 'type': 'request', 'command': 'pause', 'arguments': {'threadId': 5}},
]
requests_received = []
class Handlers(object):
def next_request(self, channel, arguments):
requests_received.append((channel, arguments))
return {'threadId': 7}
def request(self, channel, command, arguments):
requests_received.append((channel, command, arguments))
def pause_request(self, channel, arguments):
requests_received.append((channel, arguments))
raise RuntimeError('pause error')
input, input_exhausted = self.iter_with_event(REQUESTS)
output = []
stream = JsonMemoryStream(input, output)
channel = self.make_channel_with_logging(stream, Handlers())
channel.start()
input_exhausted.wait()
assert requests_received == [
(channel, REQUESTS[0]['arguments']),
(channel, 'unknown', REQUESTS[1]['arguments']),
(channel, REQUESTS[2]['arguments']),
]
assert output == [
{'seq': 1, 'type': 'response', 'request_seq': 1, 'command': 'next', 'success': True, 'body': {'threadId': 7}},
{'seq': 2, 'type': 'response', 'request_seq': 2, 'command': 'unknown', 'success': True},
{'seq': 3, 'type': 'response', 'request_seq': 3, 'command': 'pause', 'success': False, 'message': 'pause error'},
]
def test_responses(self):
request1_sent = threading.Event()
request2_sent = threading.Event()
request3_sent = threading.Event()
def iter_responses():
request1_sent.wait()
yield {'seq': 1, 'type': 'response', 'request_seq': 1, 'command': 'next', 'success': True, 'body': {'threadId': 3}}
request2_sent.wait()
yield {'seq': 2, 'type': 'response', 'request_seq': 2, 'command': 'pause', 'success': False, 'message': 'pause error'}
request3_sent.wait()
yield {'seq': 3, 'type': 'response', 'request_seq': 3, 'command': 'next', 'success': True, 'body': {'threadId': 5}}
stream = JsonMemoryStream(iter_responses(), [])
channel = self.make_channel_with_logging(stream, None)
channel.start()
# Blocking wait.
request1 = channel.send_request('next')
request1_sent.set()
response1 = request1.wait_for_response()
assert response1 == Response(True, 'next', body={'threadId': 3})
# Async callback, registered before response is received.
request2 = channel.send_request('pause')
response2 = [None]
response2_received = threading.Event()
def response2_handler(response):
response2[0] = response
response2_received.set()
request2.on_response(response2_handler)
request2_sent.set()
response2_received.wait()
assert response2[0] == Response(False, 'pause', error_message='pause error')
# Async callback, registered after response is received.
request3 = channel.send_request('next')
request3_sent.set()
request3.wait_for_response()
response3 = [None]
response3_received = threading.Event()
def response3_handler(response):
response3[0] = response
response3_received.set()
request3.on_response(response3_handler)
response3_received.wait()
assert response3[0] == Response(True, 'next', body={'threadId': 5})
def test_fuzz(self):
# Set up two channels over the same stream that send messages to each other
# asynchronously, and record everything that they send and receive.
# All records should match at the end.
class Fuzzer(object):
def __init__(self, name):
self.name = name
self.lock = threading.Lock()
self.sent = []
self.received = []
self.responses_sent = []
self.responses_received = []
def start(self, channel):
self._worker = threading.Thread(name=self.name, target=lambda: self._send_requests_and_events(channel))
self._worker.daemon = True
self._worker.start()
def wait(self):
self._worker.join()
def fizz_event(self, channel, body):
with self.lock:
self.received.append(('event', 'fizz', body))
def buzz_event(self, channel, body):
with self.lock:
self.received.append(('event', 'buzz', body))
def event(self, channel, event, body):
with self.lock:
self.received.append(('event', event, body))
def make_and_log_response(self, command):
x = random.randint(-100, 100)
if x >= 0:
response = Response(True, command, body=x)
else:
response = Response(False, command, error_message=str(x))
with self.lock:
self.responses_sent.append(response)
if response.success:
return x
else:
raise RuntimeError(response.error_message)
def fizz_request(self, channel, arguments):
with self.lock:
self.received.append(('request', 'fizz', arguments))
return self.make_and_log_response('fizz')
def buzz_request(self, channel, arguments):
with self.lock:
self.received.append(('request', 'buzz', arguments))
return self.make_and_log_response('buzz')
def request(self, channel, command, arguments):
with self.lock:
self.received.append(('request', command, arguments))
return self.make_and_log_response(command)
def _send_requests_and_events(self, channel):
pending_requests = [0]
for _ in range(0, 100):
typ = random.choice(('event', 'request'))
name = random.choice(('fizz', 'buzz', 'fizzbuzz'))
body = random.randint(0, 100)
with self.lock:
self.sent.append((typ, name, body))
if typ == 'event':
channel.send_event(name, body)
elif typ == 'request':
with self.lock:
pending_requests[0] += 1
req = channel.send_request(name, body)
def response_handler(response):
with self.lock:
self.responses_received.append(response)
pending_requests[0] -= 1
req.on_response(response_handler)
# Spin until we get responses to all requests.
while True:
with self.lock:
if pending_requests[0] == 0:
break
time.sleep(0.1)
fuzzer1 = Fuzzer('fuzzer1')
fuzzer2 = Fuzzer('fuzzer2')
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('localhost', 0))
_, port = server_socket.getsockname()
server_socket.listen(0)
socket1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
socket1_thread = threading.Thread(target=lambda: socket1.connect(('localhost', port)))
socket1_thread.start()
socket2, _ = server_socket.accept()
socket1_thread.join()
try:
io1 = socket1.makefile('rwb', 0)
io2 = socket2.makefile('rwb', 0)
stream1 = JsonIOStream(io1, io1)
channel1 = self.make_channel_with_logging(stream1, fuzzer1)
channel1.start()
fuzzer1.start(channel1)
stream2 = JsonIOStream(io2, io2)
channel2 = self.make_channel_with_logging(stream2, fuzzer2)
channel2.start()
fuzzer2.start(channel2)
fuzzer1.wait()
fuzzer2.wait()
finally:
socket1.close()
socket2.close()
assert fuzzer1.sent == fuzzer2.received
assert fuzzer2.sent == fuzzer1.received
assert fuzzer1.responses_sent == fuzzer2.responses_received
assert fuzzer2.responses_sent == fuzzer1.responses_received

View file

@ -10,4 +10,10 @@ style = pep440
versionfile_source = ptvsd/_version.py
versionfile_build = ptvsd/_version.py
tag_prefix = v
parentdir_prefix = ptvsd-
parentdir_prefix = ptvsd-
[aliases]
test=pytest
[tool:pytest]
testpaths=pytests

View file

@ -57,7 +57,9 @@ if __name__ == '__main__':
author='Microsoft Corporation',
author_email='ptvshelp@microsoft.com',
url='https://aka.ms/ptvs',
python_requires=">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*",
python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*',
setup_requires=['pytest_runner>=4.2'],
tests_require=['pytest>=3.8', 'pytest-timeout>=1.3'],
classifiers=[
'Development Status :: 5 - Production/Stable',
'Programming Language :: Python :: 2.7',