Add pretty serializer for betamax fixture

Saving json responses all on one line escaped inside of json
cassettes is great for computers, but is impossible for humans to
read. Add a serializer that is nicely flowed yaml that emits
multi-line values as yaml blocks. Additionally, re-flow and indent
the nested json, which will stay as json.

An example of the output produced can be seen at:

  https://review.openstack.org/#/c/328338/2/shade/tests/unit/fixtures/test_create_flavor.yaml

Hook it in to the keystoneauth1 betamax fixture by default, because
why in the world would you want ugly when you can have pretty.

Change-Id: I457408fcbbdca240090228d18f0482f958a7d6e4
This commit is contained in:
Monty Taylor 2016-07-17 13:52:56 -05:00
parent bc90281f27
commit c21ce26ff3
No known key found for this signature in database
GPG Key ID: 7BAE94BC7141A594
5 changed files with 190 additions and 5 deletions

View File

@ -20,6 +20,7 @@ import mock
import requests
from keystoneauth1.fixture import hooks
from keystoneauth1.fixture import serializer as yaml_serializer
from keystoneauth1 import session
@ -29,11 +30,12 @@ class BetamaxFixture(fixtures.Fixture):
serializer=None, record=False,
pre_record_hook=hooks.pre_record_hook):
self.cassette_library_dir = cassette_library_dir
self.serializer = serializer
self.record = record
self.cassette_name = cassette_name
if serializer:
betamax.Betamax.register_serializer(serializer)
if not serializer:
serializer = yaml_serializer.YamlJsonSerializer
self.serializer = serializer
betamax.Betamax.register_serializer(serializer)
self.pre_record_hook = pre_record_hook
def setUp(self):

View File

@ -0,0 +1,92 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""A serializer to emit YAML but with request body in nicely formatted JSON."""
import json
import os
import betamax.serializers.base
import yaml
def _should_use_block(value):
for c in u"\u000a\u000d\u001c\u001d\u001e\u0085\u2028\u2029":
if c in value:
return True
return False
def _represent_scalar(self, tag, value, style=None):
if style is None:
if _should_use_block(value):
style = '|'
else:
style = self.default_style
node = yaml.representer.ScalarNode(tag, value, style=style)
if self.alias_key is not None:
self.represented_objects[self.alias_key] = node
return node
def _unicode_representer(dumper, uni):
node = yaml.ScalarNode(tag=u'tag:yaml.org,2002:str', value=uni)
return node
def _indent_json(val):
if not val:
return ''
return json.dumps(
json.loads(val), indent=2,
separators=(',', ': '), sort_keys=False,
default=unicode)
def _is_json_body(interaction):
content_type = interaction['headers'].get('Content-Type', [])
return 'application/json' in content_type
class YamlJsonSerializer(betamax.serializers.base.BaseSerializer):
name = "yamljson"
@staticmethod
def generate_cassette_name(cassette_library_dir, cassette_name):
return os.path.join(
cassette_library_dir, "{name}.yaml".format(name=cassette_name))
def serialize(self, cassette_data):
# Reserialize internal json with indentation
for interaction in cassette_data['http_interactions']:
for key in ('request', 'response'):
if _is_json_body(interaction[key]):
interaction[key]['body']['string'] = _indent_json(
interaction[key]['body']['string'])
class MyDumper(yaml.Dumper):
"""Specialized Dumper which does nice blocks and unicode."""
yaml.representer.BaseRepresenter.represent_scalar = _represent_scalar
MyDumper.add_representer(unicode, _unicode_representer)
return yaml.dump(
cassette_data, Dumper=MyDumper, default_flow_style=False)
def deserialize(self, cassette_data):
try:
# There should be only one document
return list(yaml.load_all(cassette_data))[0]
except yaml.error.YAMLError:
return {}

View File

@ -1,2 +0,0 @@
{"http_interactions": [{"request": {"body": {"string": "{\"auth\": {\"tenantName\": \"test_tenant_name\", \"passwordCredentials\": {\"username\": \"test_user_name\", \"password\": \"test_password\"}}}", "encoding": "utf-8"}, "headers": {"Content-Length": ["128"], "Accept-Encoding": ["gzip, deflate"], "Accept": ["application/json"], "User-Agent": ["keystoneauth1"], "Connection": ["keep-alive"], "Content-Type": ["application/json"]}, "method": "POST", "uri": "http://keystonauth.betamax_test/v2.0/tokens"}, "response": {"body": {"string": "{\"access\": {\"token\": {\"issued_at\": \"2015-11-27T15:17:19.755470\", \"expires\": \"2015-11-27T16:17:19Z\", \"id\": \"c000c5ee4ba04594a00886028584b50d\", \"tenant\": {\"description\": null, \"enabled\": true, \"id\": \"6932cad596634a61ac9c759fb91beef1\", \"name\": \"test_tenant_name\"}, \"audit_ids\": [\"jY3gYg_YTbmzY2a4ioGuCw\"]}, \"user\": {\"username\": \"test_user_name\", \"roles_links\": [], \"id\": \"96995e6cc15b40fa8e7cd762f6a5d4c0\", \"roles\": [{\"name\": \"_member_\"}], \"name\": \"67eff5f6-9477-4961-88b4-437e6596a795\"}, \"metadata\": {\"is_admin\": 0, \"roles\": [\"9fe2ff9ee4384b1894a90878d3e92bab\"]}}}", "encoding": null}, "headers": {"X-Openstack-Request-Id": ["req-f9e188b4-06fd-4a4c-a952-2315b368218c"], "Content-Length": ["2684"], "Connection": ["keep-alive"], "Date": ["Fri, 27 Nov 2015 15:17:19 GMT"], "Content-Type": ["application/json"], "Vary": ["X-Auth-Token"], "X-Distribution": ["Ubuntu"], "Server": ["Fake"]}, "status": {"message": "OK", "code": 200}, "url": "http://keystonauth.betamax_test/v2.0/tokens"}, "recorded_at": "2015-11-27T15:17:19"}], "recorded_with": "betamax/0.5.1"}

View File

@ -0,0 +1,92 @@
http_interactions:
- request:
body:
string: |-
{
"auth": {
"tenantName": "test_tenant_name",
"passwordCredentials": {
"username": "test_user_name",
"password": "test_password"
}
}
}
encoding: utf-8
headers:
Content-Length:
- '128'
Accept-Encoding:
- gzip, deflate
Accept:
- application/json
User-Agent:
- keystoneauth1
Connection:
- keep-alive
Content-Type:
- application/json
method: POST
uri: http://keystonauth.betamax_test/v2.0/tokens
response:
body:
string: |-
{
"access": {
"token": {
"issued_at": "2015-11-27T15:17:19.755470",
"expires": "2015-11-27T16:17:19Z",
"id": "c000c5ee4ba04594a00886028584b50d",
"tenant": {
"enabled": true,
"description": null,
"name": "test_tenant_name",
"id": "6932cad596634a61ac9c759fb91beef1"
},
"audit_ids": [
"jY3gYg_YTbmzY2a4ioGuCw"
]
},
"user": {
"username": "test_user_name",
"roles_links": [],
"id": "96995e6cc15b40fa8e7cd762f6a5d4c0",
"roles": [
{
"name": "_member_"
}
],
"name": "67eff5f6-9477-4961-88b4-437e6596a795"
},
"metadata": {
"is_admin": 0,
"roles": [
"9fe2ff9ee4384b1894a90878d3e92bab"
]
}
}
}
encoding: null
headers:
X-Openstack-Request-Id:
- req-f9e188b4-06fd-4a4c-a952-2315b368218c
Content-Length:
- '2684'
Connection:
- keep-alive
Date:
- Fri, 27 Nov 2015 15:17:19 GMT
Content-Type:
- application/json
Vary:
- X-Auth-Token
X-Distribution:
- Ubuntu
Server:
- Fake
status:
message: OK
code: 200
url: http://keystonauth.betamax_test/v2.0/tokens
recorded_at: '2015-11-27T15:17:19'
recorded_with: betamax/0.5.1

View File

@ -23,3 +23,4 @@ sphinx!=1.3b1,<1.3,>=1.2.1 # BSD
testrepository>=0.0.18 # Apache-2.0/BSD
testresources>=0.2.4 # Apache-2.0/BSD
testtools>=1.4.0 # MIT
PyYAML>=3.1.0 # MIT