Make LocalSettingsContext more robust to priority

The relation data for for the LocalSettings context could cause the
priority sorting to break if the priority key wasn't cmpable (e.g. using
<, > or ==).  This patch fixes the associated bug, by making the sorting
extra robust and ensuring that un-cmp-able values are 'greater' (e.g.
further down the list) that cmp-able values, and equal to each other.
E.g. a partially ordered set.

Change-Id: I6bbf7e5f81a772ffc6ea859c9ab7c05f2eb9fdc5
Closes-bug: #2023404
This commit is contained in:
Alex Kavanagh 2023-07-04 12:03:08 +01:00
parent cbcb084af8
commit e8d0ca39a1
2 changed files with 180 additions and 2 deletions

View File

@ -14,6 +14,7 @@
# vim: set ts=4:et
import functools
import json
from charmhelpers.core.hookenv import (
@ -382,11 +383,79 @@ class LocalSettingsContext(OSContextGenerator):
ctxt = {
'settings': [
'# {0}\n{1}'.format(u, rd['local-settings'])
for u, rd in sorted(relations,
key=lambda r: r[1]['priority'])]
for u, rd in sorted(
relations,
key=functools.cmp_to_key(self._priority_sort_cmp))]
}
return ctxt
@staticmethod
def _priority_sort_cmp(unit_rdata_l, unit_rdata_r):
"""Sort two replates for __call__ .
This is used from functools.cmp_to_key() function.
It compares items in the (unit, rdata) as rdata['priority'] where
* No 'priority' value > one that is present; technically this isn't
possible, but is defensive here.
* None > that something that is present.
* types are int-ed() and attempted to be compared
* Two identical types are cmp-ed
* If types won't compare or can't be converted to an int, then they
are assumed to be non comparable and return 'equal' (0)
:param unit_rdata_l: the (unit, relation_data) on the left.
:type unit_data_l: Tuple[str, Any]
:param unit_rdata_r: the (unit, relation_data) on the right.
:type unit_data_r: Tuple[str, Any]
:returns: a cmp return value (-1 is l<r, +1 is l>r, 0 is l==r)
:rtype: int
"""
try:
priority_l = unit_rdata_l[1].get('priority')
if priority_l is None:
# cmp(None, Anything) is 1
return 1
except IndexError:
# cmp( <missing>, Anything) is 1: i.e. missing to end of list
return 1
try:
priority_r = unit_rdata_r[1]['priority']
if priority_r is None:
# cmp(Anything, None) is -1
return -1
except IndexError:
# cmp( Anything, <missing>) is -1: i.e. missing already at end
return -1
# define a _cmp function.
def _cmp(a, b):
if a < b:
return -1
elif a > b:
return 1
else:
return 0
# try converting to ints before doing strings, as the strings may be
# numbers.
try:
return _cmp(int(priority_l), int(priority_r))
except ValueError:
pass
# if they are the same type, try comparing them.
if type(priority_l) == type(priority_r):
try:
return _cmp(priority_l, priority_r)
except TypeError:
# types don't support comparisions
return 0
# different types, that won't become ints, and also don't support
# so just say they are the same.
return 0
class WebSSOFIDServiceProviderContext(OSContextGenerator):
interfaces = ['websso-fid-service-provider']

View File

@ -1363,6 +1363,115 @@ class TestHorizonContexts(CharmTestCase):
'# horizon-plugin/0\n'
'FOO = True']})
def test_LocalSettingsContext_unusual_priority(self):
# First, left priority missing.
self.related_units.side_effect = [['horizon-plugin/0'],
['horizon-plugin-too/0']]
self.relation_get.side_effect = [{'local-settings': 'FOO = True'},
{'priority': 60,
'local-settings': 'BAR = False'}]
self.assertEqual(horizon_contexts.LocalSettingsContext()(),
{'settings': []})
# First, right priority missing.
self.related_units.side_effect = [['horizon-plugin/0'],
['horizon-plugin-too/0']]
self.relation_get.side_effect = [{'priority': 99,
'local-settings': 'FOO = True'},
{'local-settings': 'BAR = False'}]
self.assertEqual(horizon_contexts.LocalSettingsContext()(),
{'settings': []})
# Left priority is None.
self.relation_ids.return_value = ['plugin:0', 'plugin-too:0']
self.related_units.side_effect = [['horizon-plugin/0'],
['horizon-plugin-too/0']]
self.relation_get.side_effect = [{'priority': None,
'local-settings': 'FOO = True'},
{'priority': 60,
'local-settings': 'BAR = False'}]
self.assertEqual(horizon_contexts.LocalSettingsContext()(),
{'settings': ['# horizon-plugin-too/0\n'
'BAR = False',
'# horizon-plugin/0\n'
'FOO = True']})
# Right priority is None.
self.relation_ids.return_value = ['plugin:0', 'plugin-too:0']
self.related_units.side_effect = [['horizon-plugin/0'],
['horizon-plugin-too/0']]
self.relation_get.side_effect = [{'priority': 99,
'local-settings': 'FOO = True'},
{'priority': None,
'local-settings': 'BAR = False'}]
self.assertEqual(horizon_contexts.LocalSettingsContext()(),
{'settings': ['# horizon-plugin/0\n'
'FOO = True',
'# horizon-plugin-too/0\n'
'BAR = False']})
# Left priority is stringy number.
self.relation_ids.return_value = ['plugin:0', 'plugin-too:0']
self.related_units.side_effect = [['horizon-plugin/0'],
['horizon-plugin-too/0']]
self.relation_get.side_effect = [{'priority': "99",
'local-settings': 'FOO = True'},
{'priority': 60,
'local-settings': 'BAR = False'}]
self.assertEqual(horizon_contexts.LocalSettingsContext()(),
{'settings': ['# horizon-plugin-too/0\n'
'BAR = False',
'# horizon-plugin/0\n'
'FOO = True']})
# Right priority is stringy number.
self.relation_ids.return_value = ['plugin:0', 'plugin-too:0']
self.related_units.side_effect = [['horizon-plugin/0'],
['horizon-plugin-too/0']]
self.relation_get.side_effect = [{'priority': 99,
'local-settings': 'FOO = True'},
{'priority': "60",
'local-settings': 'BAR = False'}]
self.assertEqual(horizon_contexts.LocalSettingsContext()(),
{'settings': ['# horizon-plugin-too/0\n'
'BAR = False',
'# horizon-plugin/0\n'
'FOO = True']})
# Both priorities are strings
self.relation_ids.return_value = ['plugin:0', 'plugin-too:0']
self.related_units.side_effect = [['horizon-plugin/0'],
['horizon-plugin-too/0']]
self.relation_get.side_effect = [{'priority': "99",
'local-settings': 'FOO = True'},
{'priority': "60",
'local-settings': 'BAR = False'}]
self.assertEqual(horizon_contexts.LocalSettingsContext()(),
{'settings': ['# horizon-plugin-too/0\n'
'BAR = False',
'# horizon-plugin/0\n'
'FOO = True']})
# Left priority is weired json object
self.relation_ids.return_value = ['plugin:0', 'plugin-too:0']
self.related_units.side_effect = [['horizon-plugin/0'],
['horizon-plugin-too/0']]
self.relation_get.side_effect = [{'priority': "{'a': 1}",
'local-settings': 'FOO = True'},
{'priority': "60",
'local-settings': 'BAR = False'}]
self.assertEqual(horizon_contexts.LocalSettingsContext()(),
{'settings': ['# horizon-plugin-too/0\n'
'BAR = False',
'# horizon-plugin/0\n'
'FOO = True']})
# right priority is weired json object
self.relation_ids.return_value = ['plugin:0', 'plugin-too:0']
self.related_units.side_effect = [['horizon-plugin/0'],
['horizon-plugin-too/0']]
self.relation_get.side_effect = [{'priority': "99",
'local-settings': 'FOO = True'},
{'priority': "[1,2,3]",
'local-settings': 'BAR = False'}]
self.assertEqual(horizon_contexts.LocalSettingsContext()(),
{'settings': ['# horizon-plugin/0\n'
'FOO = True',
'# horizon-plugin-too/0\n'
'BAR = False']})
def test_WebSSOFIDServiceProviderContext(self):
def relation_ids_side_effect(rname):
return {