Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Request Serialization Protocol Tests #3378

Draft
wants to merge 5 commits into
base: update-protocol-tests
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 152 additions & 54 deletions botocore/serialize.py

Large diffs are not rendered by default.

1,689 changes: 1,188 additions & 501 deletions tests/unit/protocols/input/ec2.json

Large diffs are not rendered by default.

3,190 changes: 2,325 additions & 865 deletions tests/unit/protocols/input/json.json

Large diffs are not rendered by default.

878 changes: 878 additions & 0 deletions tests/unit/protocols/input/json_1_0.json

Large diffs are not rendered by default.

2,535 changes: 1,572 additions & 963 deletions tests/unit/protocols/input/query.json

Large diffs are not rendered by default.

8,203 changes: 6,063 additions & 2,140 deletions tests/unit/protocols/input/rest-json.json

Large diffs are not rendered by default.

7,639 changes: 5,717 additions & 1,922 deletions tests/unit/protocols/input/rest-xml.json

Large diffs are not rendered by default.

39 changes: 38 additions & 1 deletion tests/unit/protocols/protocol-tests-ignore-list.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
{
"general": {},
"general": {
"input": {
"suites": [
"Test cases for PutWithContentEncoding operation",
"Test cases for QueryIdempotencyTokenAutoFill operation",
"Test cases for HostWithPathOperation operation"
]
}
},
"protocols": {
"query" : {
"input": {
"cases": [
"QueryFlattenedListArgWithXmlName"
]
},
"output": {
"cases": [
"QueryXmlLists",
Expand All @@ -10,6 +23,13 @@
}
},
"ec2" : {
"input" : {
"cases": [
"Ec2QueryEndpointTraitWithHostLabel",
"Ec2Lists",
"Ec2TimestampsInput"
]
},
"output": {
"cases": [
"Ec2XmlLists",
Expand Down Expand Up @@ -42,6 +62,12 @@
}
},
"rest-json" : {
"input" : {
"cases": [
"MediaTypeHeaderInputBase64",
"RestJsonHttpChecksumRequired"
]
},
"output" : {
"cases": [
"RestJsonFooErrorUsingXAmznErrorType",
Expand All @@ -57,6 +83,17 @@
}
},
"rest-xml": {
"input": {
"cases": [
"BodyWithXmlName",
"RestXmlHttpPayloadWithUnion",
"HttpPayloadWithXmlName",
"HttpPayloadWithXmlNamespace",
"HttpPayloadWithXmlNamespaceAndPrefix",
"XmlAttributesOnPayload",
"NestedXmlMapWithXmlNameSerializes"
]
},
"output": {
"cases": [
"InputAndOutputWithTimestampHeaders",
Expand Down
84 changes: 65 additions & 19 deletions tests/unit/test_protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@

import copy
import os
import xml.etree.ElementTree as ET
from base64 import b64decode
from enum import Enum

Expand Down Expand Up @@ -97,7 +98,6 @@
'rest-json': RestJSONParser,
'rest-xml': RestXMLParser,
}
PROTOCOL_TEST_BLACKLIST = ['Idempotency token auto fill']
IGNORE_LIST_FILENAME = "protocol-tests-ignore-list.json"


Expand All @@ -123,18 +123,16 @@ def _compliance_tests(test_type=None):
if full_path.endswith('.json'):
for model, case, basename in _load_cases(full_path):
protocol = basename.replace('.json', '')
if _should_ignore_test(
protocol,
"input" if inp else "output",
model['description'],
case['id'],
):
continue
if 'params' in case and inp:
if model.get('description') in PROTOCOL_TEST_BLACKLIST:
continue
yield model, case, basename
elif 'response' in case and out:
if _should_ignore_test(
protocol,
"output",
model['description'],
case['id'],
):
continue
yield model, case, basename


Expand All @@ -144,7 +142,7 @@ def _compliance_tests(test_type=None):
def test_input_compliance(json_description, case, basename):
service_description = copy.deepcopy(json_description)
service_description['operations'] = {
case.get('name', 'OperationName'): case,
case.get('given', {}).get('name', 'OperationName'): case,
}
model = ServiceModel(service_description)
protocol_type = model.metadata['protocol']
Expand All @@ -160,7 +158,7 @@ def test_input_compliance(json_description, case, basename):
client_endpoint = service_description.get('clientEndpoint')
try:
_assert_request_body_is_bytes(request['body'])
_assert_requests_equal(request, case['serialized'])
_assert_requests_equal(request, case['serialized'], protocol_type)
_assert_endpoints_equal(request, case['serialized'], client_endpoint)
except AssertionError as e:
_input_failure_message(protocol_type, case, request, e)
Expand Down Expand Up @@ -438,22 +436,68 @@ def _serialize_request_description(request_dict):
request_dict['url_path'] += f'&{encoded}'


def _assert_requests_equal(actual, expected):
assert_equal(
actual['body'], expected.get('body', '').encode('utf-8'), 'Body value'
)
def _assert_requests_equal(actual, expected, protocol_type):
if 'body' in expected:
expected_body = expected['body'].encode('utf-8')
actual_body = actual['body']
_assert_request_body(actual_body, expected_body, protocol_type)

actual_headers = HeadersDict(actual['headers'])
expected_headers = HeadersDict(expected.get('headers', {}))
excluded_headers = expected.get('forbidHeaders', [])
_assert_expected_headers_in_request(
actual_headers, expected_headers, excluded_headers
actual_headers, expected_headers, excluded_headers, protocol_type
)
assert_equal(actual['url_path'], expected.get('uri', ''), "URI")
if 'method' in expected:
assert_equal(actual['method'], expected['method'], "Method")


def _assert_expected_headers_in_request(actual, expected, excluded_headers):
def _assert_request_body(actual, expected, protocol_type):
"""
Asserts the equivalence of actual and expected request bodies based
on protocol type.

The expected bodies in our consumed protocol tests have extra
whitespace and newlines that need to be handled. We need to normalize
the expected and actual response bodies before evaluating equivalence.
"""
if protocol_type in ['json', 'rest-json']:
_assert_json_bodies(actual, expected, protocol_type)
elif protocol_type in ['rest-xml']:
_assert_xml_bodies(actual, expected)
else:
assert_equal(actual, expected, 'Body value')


def _assert_json_bodies(actual, expected, protocol_type):
try:
assert_equal(json.loads(actual), json.loads(expected), 'Body value')
except json.JSONDecodeError as e:
if protocol_type == 'json':
raise e
assert_equal(actual, expected, 'Body value')


def _assert_xml_bodies(actual, expected):
try:
tree1 = ET.canonicalize(actual, strip_text=True)
tree2 = ET.canonicalize(expected, strip_text=True)
assert_equal(tree1, tree2, 'Body value')
except ET.ParseError:
assert_equal(actual, expected, 'Body value')


def _assert_expected_headers_in_request(
actual, expected, excluded_headers, protocol_type
):
if protocol_type in ['query', 'ec2']:
# Botocore sets the Content-Type header to the following for query and ec2:
# Content-Type: application/x-www-form-urlencoded; charset=utf-8
# The protocol test files do not include "; charset=utf-8".
# We'll add this to the expected header value before asserting equivalence.
if expected.get('Content-Type'):
expected['Content-Type'] += '; charset=utf-8'
for header, value in expected.items():
assert header in actual
assert actual[header] == value
Expand Down Expand Up @@ -482,7 +526,9 @@ def _load_cases(full_path):
# The format is BOTOCORE_TEST_ID=suite_id:test_id or
# BOTOCORE_TEST_ID=suite_id
suite_id, test_id = _get_suite_test_id()
all_test_data = json.load(open(full_path), object_pairs_hook=OrderedDict)
all_test_data = json.load(
open(full_path, encoding='utf-8'), object_pairs_hook=OrderedDict
)
basename = os.path.basename(full_path)
for i, test_data in enumerate(all_test_data):
if suite_id is not None and i != suite_id:
Expand Down
Loading