Allow parentSelector to use multiple labels to select parent document
This PS updates the layering module so that a child document with a compound parentSelector like: [{'a': 'b'}, {'c': 'd'}] can select a parent with the exact same labels. Further, this works if child's parentSelector is a sub-subset of parent's labels. Adds unit tests for positive and negative test cases. Change-Id: I7c4aac583365d90803eda77b82763decd78cfdcf
This commit is contained in:
parent
af6c2ea8ee
commit
4cc801bcc4
|
@ -55,7 +55,9 @@ class DocumentLayering(object):
|
||||||
if is_potential_child:
|
if is_potential_child:
|
||||||
parent_selector = potential_child.parent_selector
|
parent_selector = potential_child.parent_selector
|
||||||
labels = document.labels
|
labels = document.labels
|
||||||
return parent_selector == labels
|
# Labels are key-value pairs which are unhashable, so use ``all``
|
||||||
|
# instead.
|
||||||
|
return all(labels.get(x) == y for x, y in parent_selector.items())
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _calc_document_children(self, document):
|
def _calc_document_children(self, document):
|
||||||
|
@ -66,8 +68,12 @@ class DocumentLayering(object):
|
||||||
# The lowest layer has been reached, so no children.
|
# The lowest layer has been reached, so no children.
|
||||||
return
|
return
|
||||||
|
|
||||||
potential_children = self._documents_by_labels.get(
|
potential_children = set()
|
||||||
str(document.labels), [])
|
for label_key, label_val in document.labels.items():
|
||||||
|
_potential_children = self._documents_by_labels.get(
|
||||||
|
(label_key, label_val), [])
|
||||||
|
potential_children |= set(_potential_children)
|
||||||
|
|
||||||
for potential_child in potential_children:
|
for potential_child in potential_children:
|
||||||
if self._is_actual_child_document(document, potential_child,
|
if self._is_actual_child_document(document, potential_child,
|
||||||
child_layer):
|
child_layer):
|
||||||
|
@ -189,10 +195,11 @@ class DocumentLayering(object):
|
||||||
self._documents_by_layer.setdefault(document.layer, [])
|
self._documents_by_layer.setdefault(document.layer, [])
|
||||||
self._documents_by_layer[document.layer].append(document)
|
self._documents_by_layer[document.layer].append(document)
|
||||||
if document.parent_selector:
|
if document.parent_selector:
|
||||||
self._documents_by_labels.setdefault(
|
for label_key, label_val in document.parent_selector.items():
|
||||||
str(document.parent_selector), [])
|
self._documents_by_labels.setdefault(
|
||||||
self._documents_by_labels[
|
(label_key, label_val), [])
|
||||||
str(document.parent_selector)].append(document)
|
self._documents_by_labels[
|
||||||
|
(label_key, label_val)].append(document)
|
||||||
|
|
||||||
if self._layering_policy is None:
|
if self._layering_policy is None:
|
||||||
error_msg = (
|
error_msg = (
|
||||||
|
|
|
@ -14,6 +14,7 @@ metadata:
|
||||||
name: global-1234
|
name: global-1234
|
||||||
labels:
|
labels:
|
||||||
key1: value1
|
key1: value1
|
||||||
|
key2: value2
|
||||||
layeringDefinition:
|
layeringDefinition:
|
||||||
abstract: true
|
abstract: true
|
||||||
layer: global
|
layer: global
|
||||||
|
@ -30,6 +31,7 @@ metadata:
|
||||||
layer: site
|
layer: site
|
||||||
parentSelector:
|
parentSelector:
|
||||||
key1: value1
|
key1: value1
|
||||||
|
key2: value2
|
||||||
actions:
|
actions:
|
||||||
- method: merge
|
- method: merge
|
||||||
path: .
|
path: .
|
||||||
|
|
|
@ -96,6 +96,74 @@ class TestDocumentLayering2Layers(TestDocumentLayering):
|
||||||
site_expected = {'a': {'x': 1, 'y': 2}, 'b': 4}
|
site_expected = {'a': {'x': 1, 'y': 2}, 'b': 4}
|
||||||
self._test_layering(documents, site_expected)
|
self._test_layering(documents, site_expected)
|
||||||
|
|
||||||
|
def test_layering_default_scenario_multi_parentselector(self):
|
||||||
|
mapping = {
|
||||||
|
"_GLOBAL_DATA_1_": {"data": {"a": {"x": 1, "y": 2}}},
|
||||||
|
"_SITE_DATA_1_": {"data": {"b": 4}},
|
||||||
|
"_SITE_ACTIONS_1_": {
|
||||||
|
"actions": [{"method": "merge", "path": "."}]}
|
||||||
|
}
|
||||||
|
doc_factory = factories.DocumentFactory(2, [1, 1])
|
||||||
|
documents = doc_factory.gen_test(mapping, site_abstract=False)
|
||||||
|
|
||||||
|
# Test case where the same number of labels are found in parent
|
||||||
|
# labels and child's parentSelector.
|
||||||
|
labels = {'foo': 'bar', 'baz': 'qux'}
|
||||||
|
documents[1]['metadata']['labels'] = labels
|
||||||
|
documents[-1]['metadata']['layeringDefinition']['parentSelector'] = (
|
||||||
|
labels)
|
||||||
|
site_expected = {'a': {'x': 1, 'y': 2}, 'b': 4}
|
||||||
|
self._test_layering(documents, site_expected)
|
||||||
|
|
||||||
|
# Test case where child's parentSelector is a subset of parent's
|
||||||
|
# labels.
|
||||||
|
documents[-1]['metadata']['layeringDefinition']['parentSelector'] = {
|
||||||
|
'foo': 'bar'}
|
||||||
|
site_expected = {'a': {'x': 1, 'y': 2}, 'b': 4}
|
||||||
|
self._test_layering(documents, site_expected)
|
||||||
|
|
||||||
|
documents[-1]['metadata']['layeringDefinition']['parentSelector'] = {
|
||||||
|
'baz': 'qux'}
|
||||||
|
site_expected = {'a': {'x': 1, 'y': 2}, 'b': 4}
|
||||||
|
self._test_layering(documents, site_expected)
|
||||||
|
|
||||||
|
def test_layering_default_scenario_multi_parentselector_no_match(self):
|
||||||
|
mapping = {
|
||||||
|
"_GLOBAL_DATA_1_": {"data": {"a": {"x": 1, "y": 2}}},
|
||||||
|
"_SITE_DATA_1_": {"data": {"b": 4}},
|
||||||
|
"_SITE_ACTIONS_1_": {
|
||||||
|
"actions": [{"method": "merge", "path": "."}]}
|
||||||
|
}
|
||||||
|
doc_factory = factories.DocumentFactory(2, [1, 1])
|
||||||
|
documents = doc_factory.gen_test(mapping, site_abstract=False)
|
||||||
|
|
||||||
|
labels = {'a': 'b', 'c': 'd'}
|
||||||
|
documents[1]['metadata']['labels'] = labels
|
||||||
|
|
||||||
|
# Test case where none of the labels in parentSelector match.
|
||||||
|
documents[-1]['metadata']['layeringDefinition']['parentSelector'] = {
|
||||||
|
'w': 'x', 'y': 'z'
|
||||||
|
}
|
||||||
|
self._test_layering(documents, site_expected={})
|
||||||
|
|
||||||
|
# Test case where parentSelector has one too many labels to be a match.
|
||||||
|
documents[-1]['metadata']['layeringDefinition']['parentSelector'] = {
|
||||||
|
'a': 'b', 'c': 'd', 'e': 'f'
|
||||||
|
}
|
||||||
|
self._test_layering(documents, site_expected={})
|
||||||
|
|
||||||
|
# Test case where parentSelector keys match (but not values).
|
||||||
|
documents[-1]['metadata']['layeringDefinition']['parentSelector'] = {
|
||||||
|
'a': 'x', 'c': 'y'
|
||||||
|
}
|
||||||
|
self._test_layering(documents, site_expected={})
|
||||||
|
|
||||||
|
# Test case where parentSelector values match (but not keys).
|
||||||
|
documents[-1]['metadata']['layeringDefinition']['parentSelector'] = {
|
||||||
|
'x': 'b', 'y': 'd'
|
||||||
|
}
|
||||||
|
self._test_layering(documents, site_expected={})
|
||||||
|
|
||||||
def test_layering_method_delete(self):
|
def test_layering_method_delete(self):
|
||||||
site_expected = [{}, {'c': 9}, {"a": {"x": 1, "y": 2}}]
|
site_expected = [{}, {'c': 9}, {"a": {"x": 1, "y": 2}}]
|
||||||
doc_factory = factories.DocumentFactory(2, [1, 1])
|
doc_factory = factories.DocumentFactory(2, [1, 1])
|
||||||
|
|
|
@ -84,6 +84,23 @@ class TestDocumentLayeringNegative(
|
||||||
' during the layering process.')
|
' during the layering process.')
|
||||||
mock_log.info.reset_mock()
|
mock_log.info.reset_mock()
|
||||||
|
|
||||||
|
@mock.patch.object(layering, 'LOG', autospec=True)
|
||||||
|
def test_layering_with_invalid_layer(self, mock_log):
|
||||||
|
doc_factory = factories.DocumentFactory(1, [1])
|
||||||
|
documents = doc_factory.gen_test({}, site_abstract=False)
|
||||||
|
documents[-1]['metadata']['layeringDefinition']['layer'] = 'invalid'
|
||||||
|
|
||||||
|
self._test_layering(documents, global_expected={})
|
||||||
|
mock_log.info.assert_has_calls([
|
||||||
|
mock.call(
|
||||||
|
'%s is an empty layer with no documents. It will be discarded '
|
||||||
|
'from the layerOrder during the layering process.', 'global'),
|
||||||
|
mock.call('Either the layerOrder in the LayeringPolicy was empty '
|
||||||
|
'to begin with or no document layers were found in the '
|
||||||
|
'layerOrder, causing it to become empty. No layering '
|
||||||
|
'will be performed.')
|
||||||
|
])
|
||||||
|
|
||||||
@mock.patch.object(layering, 'LOG', autospec=True)
|
@mock.patch.object(layering, 'LOG', autospec=True)
|
||||||
def test_layering_child_with_invalid_parent_selector(self, mock_log):
|
def test_layering_child_with_invalid_parent_selector(self, mock_log):
|
||||||
doc_factory = factories.DocumentFactory(2, [1, 1])
|
doc_factory = factories.DocumentFactory(2, [1, 1])
|
||||||
|
@ -174,3 +191,16 @@ class TestDocumentLayeringNegative(
|
||||||
documents = doc_factory.gen_test({}, site_abstract=False)[1:]
|
documents = doc_factory.gen_test({}, site_abstract=False)[1:]
|
||||||
self.assertRaises(errors.LayeringPolicyNotFound,
|
self.assertRaises(errors.LayeringPolicyNotFound,
|
||||||
layering.DocumentLayering, documents)
|
layering.DocumentLayering, documents)
|
||||||
|
|
||||||
|
@mock.patch.object(layering, 'LOG', autospec=True)
|
||||||
|
def test_multiple_layering_policy_logs_warning(self, mock_log):
|
||||||
|
doc_factory = factories.DocumentFactory(1, [1])
|
||||||
|
documents = doc_factory.gen_test({}, site_abstract=False)
|
||||||
|
# Copy the same layering policy so that 2 are passed in, causing a
|
||||||
|
# warning to be raised.
|
||||||
|
documents.append(documents[0])
|
||||||
|
self._test_layering(documents, site_expected={})
|
||||||
|
mock_log.warning.assert_called_once_with(
|
||||||
|
'More than one layering policy document was passed in. Using the '
|
||||||
|
'first one found: [%s] %s.', documents[0]['schema'],
|
||||||
|
documents[0]['metadata']['name'])
|
||||||
|
|
Loading…
Reference in New Issue