Factor out duplicated notification sample data

Versioned notifications are functionally tested against stored sample
data. Also most of the instance notitication shares payload structure.
Today we store a separate sample for every instance notification.
These samples store similar, mostly redundant data. When a new field
is added to the InstancePayload then every instance notification
related sample file needs to be modified. This leads to big and
redundant changes like I18af99479562e2fe5e74e6e1252b804b074fee58.

To remove the redundancy this patch proposes to use json references
in the sample files instead of copy pasting the same notification
sample fragment to every sample.

As a first step this patch introduces a small json ref resolver.
Then subsequent patches will replace the duplicated sample data
with references to common sample fragments.

This proposed resolver supports resolving the refs recursively so
a referenced json fragment can reference further fragments. However
the current implementation does not handle reference loops.
The resolver also supports overriding parts of the referenced
json fragment to support content customization needed in the next
patch.

Change-Id: Ic3ab7d60e4ac12b767fe70bef97b327545a86e74
This commit is contained in:
Balazs Gibizer 2017-04-03 18:05:07 +02:00
parent 47bdd10137
commit 5a5155ea4e
4 changed files with 216 additions and 4 deletions

View File

@ -19,15 +19,18 @@ It is used via a single directive in the .rst file
.. versioned_notifications::
"""
import os
from docutils import nodes
from docutils.parsers import rst
import importlib
from oslo_serialization import jsonutils
import pkgutil
import nova.notifications.objects
from nova.notifications.objects import base as notification
from nova.objects import base
from nova.tests import json_ref
import nova.utils
class VersionedNotificationDirective(rst.Directive):
@ -125,9 +128,17 @@ jQuery(document).ready(function(){
col = nodes.entry()
row.append(col)
with open(self.SAMPLE_ROOT + sample_file, 'r') as f:
with open(os.path.join(self.SAMPLE_ROOT, sample_file), 'r') as f:
sample_content = f.read()
sample_obj = jsonutils.loads(sample_content)
sample_obj = json_ref.resolve_refs(
sample_obj,
base_path=os.path.abspath(self.SAMPLE_ROOT))
sample_content = jsonutils.dumps(sample_obj,
sort_keys=True, indent=4,
separators=(',', ': '))
event_type = sample_file[0: -5]
html_str = self.TOGGLE_SCRIPT % ((event_type, ) * 3)
html_str += ("<input type='button' id='%s-hideshow' "

View File

@ -21,6 +21,7 @@ from oslo_utils import fixture as utils_fixture
from nova import test
from nova.tests import fixtures as nova_fixtures
from nova.tests.functional import integrated_helpers
from nova.tests import json_ref
from nova.tests.unit.api.openstack.compute import test_services
from nova.tests.unit import fake_crypto
from nova.tests.unit import fake_notifier
@ -133,11 +134,14 @@ class NotificationSampleTestBase(test.TestCase,
notification = fake_notifier.VERSIONED_NOTIFICATIONS[0]
else:
notification = actual
with open(self._get_notification_sample(sample_file_name)) as sample:
sample_file = self._get_notification_sample(sample_file_name)
with open(sample_file) as sample:
sample_data = sample.read()
sample_obj = jsonutils.loads(sample_data)
sample_base_dir = os.path.dirname(sample_file)
sample_obj = json_ref.resolve_refs(
sample_obj, base_path=sample_base_dir)
self._apply_replacements(replacements, sample_obj, notification)
self.assertJsonEqual(sample_obj, notification)

63
nova/tests/json_ref.py Normal file
View File

@ -0,0 +1,63 @@
# All Rights Reserved.
#
# 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.
import os
from oslo_serialization import jsonutils
def _resolve_ref(ref, base_path):
file_path, _, json_path = ref.partition('#')
if json_path:
raise NotImplementedError('JSON refs with JSON path after the "#" is'
'not yet supported')
path = os.path.join(base_path, file_path)
# binary mode is needed due to bug/1515231
with open(path, 'r+b') as f:
ref_value = jsonutils.load(f)
base_path = os.path.dirname(path)
res = resolve_refs(ref_value, base_path)
return res
def resolve_refs(obj_with_refs, base_path):
if isinstance(obj_with_refs, list):
for i, item in enumerate(obj_with_refs):
obj_with_refs[i] = resolve_refs(item, base_path)
elif isinstance(obj_with_refs, dict):
if '$ref' in obj_with_refs.keys():
ref = obj_with_refs.pop('$ref')
resolved_ref = _resolve_ref(ref, base_path)
# the rest of the ref dict contains overrides for the ref. Apply
# those overrides recursively here.
_update_dict_recursively(resolved_ref, obj_with_refs)
return resolved_ref
else:
for key, value in obj_with_refs.items():
obj_with_refs[key] = resolve_refs(value, base_path)
else:
# scalar, nothing to do
pass
return obj_with_refs
def _update_dict_recursively(d, update):
"""Update dict d recursively with data from dict update"""
for k, v in update.items():
if k in d and isinstance(d[k], dict) and isinstance(v, dict):
_update_dict_recursively(d[k], v)
else:
d[k] = v

View File

@ -0,0 +1,134 @@
# All Rights Reserved.
#
# 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.
import copy
import mock
from nova import test
from nova.tests import json_ref
class TestJsonRef(test.NoDBTestCase):
def test_update_dict_recursively(self):
input = {'foo': 1,
'bar': 13,
'a list': [],
'nesting': {
'baz': 42,
'foobar': 121
}}
d = copy.deepcopy(input)
json_ref._update_dict_recursively(d, {})
self.assertDictEqual(input, d)
d = copy.deepcopy(input)
json_ref._update_dict_recursively(d, {'foo': 111,
'new_key': 1,
'nesting': {
'baz': 142,
'new_nested': 1
}})
expected = copy.deepcopy(input)
expected['foo'] = 111
expected['new_key'] = 1
expected['nesting']['baz'] = 142
expected['nesting']['new_nested'] = 1
self.assertDictEqual(expected, d)
d = copy.deepcopy(input)
json_ref._update_dict_recursively(d, {'nesting': 1})
expected = copy.deepcopy(input)
expected['nesting'] = 1
self.assertDictEqual(expected, d)
d = copy.deepcopy(input)
@mock.patch('oslo_serialization.jsonutils.load')
@mock.patch('nova.tests.json_ref.open')
def test_resolve_ref(self, mock_open, mock_json_load):
mock_json_load.return_value = {'baz': 13}
actual = json_ref.resolve_refs(
{'foo': 1,
'bar': {'$ref': 'another.json#'}},
'some/base/path/')
self.assertDictEqual({'foo': 1,
'bar': {'baz': 13}},
actual)
mock_open.assert_called_once_with('some/base/path/another.json', 'r+b')
@mock.patch('oslo_serialization.jsonutils.load')
@mock.patch('nova.tests.json_ref.open')
def test_resolve_ref_recursively(self, mock_open, mock_json_load):
mock_json_load.side_effect = [
# this is the content of direct_ref.json
{'baz': 13,
'nesting': {'$ref': 'subdir/nested_ref.json#'}},
# this is the content of subdir/nested_ref.json
{'a deep key': 'happiness'}]
actual = json_ref.resolve_refs(
{'foo': 1,
'bar': {'$ref': 'direct_ref.json#'}},
'some/base/path/')
self.assertDictEqual({'foo': 1,
'bar': {'baz': 13,
'nesting':
{'a deep key': 'happiness'}}},
actual)
mock_open.assert_any_call('some/base/path/direct_ref.json', 'r+b')
mock_open.assert_any_call('some/base/path/subdir/nested_ref.json',
'r+b')
@mock.patch('oslo_serialization.jsonutils.load')
@mock.patch('nova.tests.json_ref.open')
def test_resolve_ref_with_override(self, mock_open, mock_json_load):
mock_json_load.return_value = {'baz': 13,
'boo': 42}
actual = json_ref.resolve_refs(
{'foo': 1,
'bar': {'$ref': 'another.json#',
'boo': 0}},
'some/base/path/')
self.assertDictEqual({'foo': 1,
'bar': {'baz': 13,
'boo': 0}},
actual)
mock_open.assert_called_once_with('some/base/path/another.json', 'r+b')
@mock.patch('oslo_serialization.jsonutils.load')
@mock.patch('nova.tests.json_ref.open')
def test_resolve_ref_with_nested_override(self, mock_open, mock_json_load):
mock_json_load.return_value = {'baz': 13,
'boo': {'a': 1,
'b': 2}}
actual = json_ref.resolve_refs(
{'foo': 1,
'bar': {'$ref': 'another.json#',
'boo': {'b': 3,
'c': 13}}},
'some/base/path/')
self.assertDictEqual({'foo': 1,
'bar': {'baz': 13,
'boo': {'a': 1,
'b': 3,
'c': 13}}},
actual)
mock_open.assert_called_once_with('some/base/path/another.json', 'r+b')