Skip to content

Commit 97e5b6e

Browse files
authored
[core] add testserver (Azure#19153)
1 parent 0dfbd7a commit 97e5b6e

File tree

17 files changed

+672
-0
lines changed

17 files changed

+672
-0
lines changed

eng/.docsettings.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ known_content_issues:
9494
- ['sdk/containerregistry/azure-containerregistry/swagger/README.md', '#4554']
9595
- ['sdk/appconfiguration/azure-appconfiguration/swagger/README.md', '#4554']
9696
- ['sdk/attestation/azure-security-attestation/swagger/README.md', '#4554']
97+
- ['sdk/core/azure-core/tests/testserver_tests/coretestserver/README.rst', '#4554']
9798

9899
# common.
99100
- ['sdk/appconfiguration/azure-appconfiguration/README.md', 'common']

sdk/core/azure-core/dev_requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ opencensus-ext-threading
77
mock; python_version < '3.3'
88
-e ../../../tools/azure-sdk-tools
99
-e ../../../tools/azure-devtools
10+
-e tests/testserver_tests/coretestserver
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# --------------------------------------------------------------------------
2+
#
3+
# Copyright (c) Microsoft Corporation. All rights reserved.
4+
#
5+
# The MIT License (MIT)
6+
#
7+
# Permission is hereby granted, free of charge, to any person obtaining a copy
8+
# of this software and associated documentation files (the ""Software""), to
9+
# deal in the Software without restriction, including without limitation the
10+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
11+
# sell copies of the Software, and to permit persons to whom the Software is
12+
# furnished to do so, subject to the following conditions:
13+
#
14+
# The above copyright notice and this permission notice shall be included in
15+
# all copies or substantial portions of the Software.
16+
#
17+
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
23+
# IN THE SOFTWARE.
24+
#
25+
# --------------------------------------------------------------------------
26+
import time
27+
import pytest
28+
import signal
29+
import os
30+
import subprocess
31+
import sys
32+
import random
33+
from six.moves import urllib
34+
35+
def is_port_available(port_num):
36+
req = urllib.request.Request("http://localhost:{}/health".format(port_num))
37+
try:
38+
return urllib.request.urlopen(req).code != 200
39+
except Exception as e:
40+
return True
41+
42+
def get_port():
43+
count = 3
44+
for _ in range(count):
45+
port_num = random.randrange(3000, 5000)
46+
if is_port_available(port_num):
47+
return port_num
48+
raise TypeError("Tried {} times, can't find an open port".format(count))
49+
50+
@pytest.fixture
51+
def port():
52+
return os.environ["FLASK_PORT"]
53+
54+
def start_testserver():
55+
port = get_port()
56+
os.environ["FLASK_APP"] = "coretestserver"
57+
os.environ["FLASK_PORT"] = str(port)
58+
cmd = "flask run -p {}".format(port)
59+
if os.name == 'nt': #On windows, subprocess creation works without being in the shell
60+
child_process = subprocess.Popen(cmd, env=dict(os.environ))
61+
else:
62+
#On linux, have to set shell=True
63+
child_process = subprocess.Popen(cmd, shell=True, preexec_fn=os.setsid, env=dict(os.environ))
64+
count = 5
65+
for _ in range(count):
66+
if not is_port_available(port):
67+
return child_process
68+
time.sleep(1)
69+
raise ValueError("Didn't start!")
70+
71+
def terminate_testserver(process):
72+
if os.name == 'nt':
73+
process.kill()
74+
else:
75+
os.killpg(os.getpgid(process.pid), signal.SIGTERM) # Send the signal to all the process groups
76+
77+
@pytest.fixture(autouse=True, scope="package")
78+
def testserver():
79+
"""Start the Autorest testserver."""
80+
server = start_testserver()
81+
yield
82+
terminate_testserver(server)
83+
84+
85+
# Ignore collection of async tests for Python 2
86+
collect_ignore_glob = []
87+
if sys.version_info < (3, 5):
88+
collect_ignore_glob.append("*_async.py")
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Testserver for Python Azure Core
2+
==============================================
3+
4+
This package contains a testserver to aid in testing of Python's Azure Core package
5+
6+
It has the following component:
7+
8+
coretestserver
9+
--------------
10+
11+
A testing server for Azure Core tests to use
12+
13+
Contributing
14+
============
15+
16+
This project has adopted the
17+
`Microsoft Open Source Code of Conduct <https://opensource.microsoft.com/codeofconduct/>`__.
18+
For more information see the
19+
`Code of Conduct FAQ <https://opensource.microsoft.com/codeofconduct/faq/>`__
20+
or contact
21+
`opencode@microsoft.com <mailto:opencode@microsoft.com>`__
22+
with any additional questions or comments.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# coding: utf-8
2+
# -------------------------------------------------------------------------
3+
# Copyright (c) Microsoft Corporation. All rights reserved.
4+
# Licensed under the MIT License. See LICENSE.txt in the project root for
5+
# license information.
6+
# -------------------------------------------------------------------------
7+
8+
from flask import Flask, Response
9+
from .test_routes import (
10+
basic_api,
11+
encoding_api,
12+
errors_api,
13+
streams_api,
14+
urlencoded_api,
15+
multipart_api,
16+
xml_api
17+
)
18+
19+
app = Flask(__name__)
20+
app.register_blueprint(basic_api, url_prefix="/basic")
21+
app.register_blueprint(encoding_api, url_prefix="/encoding")
22+
app.register_blueprint(errors_api, url_prefix="/errors")
23+
app.register_blueprint(streams_api, url_prefix="/streams")
24+
app.register_blueprint(urlencoded_api, url_prefix="/urlencoded")
25+
app.register_blueprint(multipart_api, url_prefix="/multipart")
26+
app.register_blueprint(xml_api, url_prefix="/xml")
27+
28+
@app.route('/health', methods=['GET'])
29+
def latin_1_charset_utf8():
30+
return Response(status=200)
31+
32+
if __name__ == "__main__":
33+
app.run(debug=True)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# coding: utf-8
2+
# -------------------------------------------------------------------------
3+
# Copyright (c) Microsoft Corporation. All rights reserved.
4+
# Licensed under the MIT License. See LICENSE.txt in the project root for
5+
# license information.
6+
# -------------------------------------------------------------------------
7+
8+
from .basic import basic_api
9+
from .encoding import encoding_api
10+
from .errors import errors_api
11+
from .multipart import multipart_api
12+
from .streams import streams_api
13+
from .urlencoded import urlencoded_api
14+
from .xml_route import xml_api
15+
16+
__all__ = [
17+
"basic_api",
18+
"encoding_api",
19+
"errors_api",
20+
"multipart_api",
21+
"streams_api",
22+
"urlencoded_api",
23+
"xml_api",
24+
]
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# coding: utf-8
2+
# -------------------------------------------------------------------------
3+
# Copyright (c) Microsoft Corporation. All rights reserved.
4+
# Licensed under the MIT License. See LICENSE.txt in the project root for
5+
# license information.
6+
# -------------------------------------------------------------------------
7+
8+
from flask import (
9+
Response,
10+
Blueprint,
11+
request
12+
)
13+
14+
basic_api = Blueprint('basic_api', __name__)
15+
16+
@basic_api.route('/string', methods=['GET'])
17+
def string():
18+
return Response(
19+
"Hello, world!", status=200, mimetype="text/plain"
20+
)
21+
22+
@basic_api.route('/lines', methods=['GET'])
23+
def lines():
24+
return Response(
25+
"Hello,\nworld!", status=200, mimetype="text/plain"
26+
)
27+
28+
@basic_api.route("/bytes", methods=['GET'])
29+
def bytes():
30+
return Response(
31+
"Hello, world!".encode(), status=200, mimetype="text/plain"
32+
)
33+
34+
@basic_api.route("/html", methods=['GET'])
35+
def html():
36+
return Response(
37+
"<html><body>Hello, world!</html></body>", status=200, mimetype="text/html"
38+
)
39+
40+
@basic_api.route("/json", methods=['GET'])
41+
def json():
42+
return Response(
43+
'{"greeting": "hello", "recipient": "world"}', status=200, mimetype="application/json"
44+
)
45+
46+
@basic_api.route("/complicated-json", methods=['POST'])
47+
def complicated_json():
48+
# thanks to Sean Kane for this test!
49+
assert request.json['EmptyByte'] == ''
50+
assert request.json['EmptyUnicode'] == ''
51+
assert request.json['SpacesOnlyByte'] == ' '
52+
assert request.json['SpacesOnlyUnicode'] == ' '
53+
assert request.json['SpacesBeforeByte'] == ' Text'
54+
assert request.json['SpacesBeforeUnicode'] == ' Text'
55+
assert request.json['SpacesAfterByte'] == 'Text '
56+
assert request.json['SpacesAfterUnicode'] == 'Text '
57+
assert request.json['SpacesBeforeAndAfterByte'] == ' Text '
58+
assert request.json['SpacesBeforeAndAfterUnicode'] == ' Text '
59+
assert request.json['啊齄丂狛'] == 'ꀕ'
60+
assert request.json['RowKey'] == 'test2'
61+
assert request.json['啊齄丂狛狜'] == 'hello'
62+
assert request.json["singlequote"] == "a''''b"
63+
assert request.json["doublequote"] == 'a""""b'
64+
assert request.json["None"] == None
65+
66+
return Response(status=200)
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# coding: utf-8
2+
# -------------------------------------------------------------------------
3+
# Copyright (c) Microsoft Corporation. All rights reserved.
4+
# Licensed under the MIT License. See LICENSE.txt in the project root for
5+
# license information.
6+
# -------------------------------------------------------------------------
7+
from json import dumps
8+
from flask import (
9+
Response,
10+
Blueprint,
11+
)
12+
13+
encoding_api = Blueprint('encoding_api', __name__)
14+
15+
@encoding_api.route('/latin-1', methods=['GET'])
16+
def latin_1():
17+
r = Response(
18+
"Latin 1: ÿ".encode("latin-1"), status=200
19+
)
20+
r.headers["Content-Type"] = "text/plain; charset=latin-1"
21+
return r
22+
23+
@encoding_api.route('/latin-1-with-utf-8', methods=['GET'])
24+
def latin_1_charset_utf8():
25+
r = Response(
26+
"Latin 1: ÿ".encode("latin-1"), status=200
27+
)
28+
r.headers["Content-Type"] = "text/plain; charset=utf-8"
29+
return r
30+
31+
@encoding_api.route('/no-charset', methods=['GET'])
32+
def latin_1_no_charset():
33+
r = Response(
34+
"Hello, world!", status=200
35+
)
36+
r.headers["Content-Type"] = "text/plain"
37+
return r
38+
39+
@encoding_api.route('/iso-8859-1', methods=['GET'])
40+
def iso_8859_1():
41+
r = Response(
42+
"Accented: Österreich".encode("iso-8859-1"), status=200
43+
)
44+
r.headers["Content-Type"] = "text/plain"
45+
return r
46+
47+
@encoding_api.route('/emoji', methods=['GET'])
48+
def emoji():
49+
r = Response(
50+
"👩", status=200
51+
)
52+
return r
53+
54+
@encoding_api.route('/emoji-family-skin-tone-modifier', methods=['GET'])
55+
def emoji_family_skin_tone_modifier():
56+
r = Response(
57+
"👩🏻‍👩🏽‍👧🏾‍👦🏿 SSN: 859-98-0987", status=200
58+
)
59+
return r
60+
61+
@encoding_api.route('/korean', methods=['GET'])
62+
def korean():
63+
r = Response(
64+
"아가", status=200
65+
)
66+
return r
67+
68+
@encoding_api.route('/json', methods=['GET'])
69+
def json():
70+
data = {"greeting": "hello", "recipient": "world"}
71+
content = dumps(data).encode("utf-16")
72+
r = Response(
73+
content, status=200
74+
)
75+
r.headers["Content-Type"] = "application/json; charset=utf-16"
76+
return r
77+
78+
@encoding_api.route('/invalid-codec-name', methods=['GET'])
79+
def invalid_codec_name():
80+
r = Response(
81+
"おはようございます。".encode("utf-8"), status=200
82+
)
83+
r.headers["Content-Type"] = "text/plain; charset=invalid-codec-name"
84+
return r
85+
86+
@encoding_api.route('/no-charset', methods=['GET'])
87+
def no_charset():
88+
r = Response(
89+
"Hello, world!", status=200
90+
)
91+
r.headers["Content-Type"] = "text/plain"
92+
return r
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# coding: utf-8
2+
# -------------------------------------------------------------------------
3+
# Copyright (c) Microsoft Corporation. All rights reserved.
4+
# Licensed under the MIT License. See LICENSE.txt in the project root for
5+
# license information.
6+
# -------------------------------------------------------------------------
7+
from flask import (
8+
Response,
9+
Blueprint,
10+
)
11+
12+
errors_api = Blueprint('errors_api', __name__)
13+
14+
@errors_api.route('/403', methods=['GET'])
15+
def get_403():
16+
return Response(status=403)
17+
18+
@errors_api.route('/500', methods=['GET'])
19+
def get_500():
20+
return Response(status=500)
21+
22+
@errors_api.route('/stream', methods=['GET'])
23+
def get_stream():
24+
class StreamingBody:
25+
def __iter__(self):
26+
yield b"Hello, "
27+
yield b"world!"
28+
return Response(StreamingBody(), status=500)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# -------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See LICENSE.txt in the project root for
4+
# license information.
5+
# -------------------------------------------------------------------------
6+
7+
def assert_with_message(param_name, expected_value, actual_value):
8+
assert expected_value == actual_value, "Expected '{}' to be '{}', got '{}'".format(
9+
param_name, expected_value, actual_value
10+
)
11+
12+
__all__ = ["assert_with_message"]

0 commit comments

Comments
 (0)