Skip to content

Commit 84d8031

Browse files
committed
Make FakeConfiguration a representative configuration object.
As of this commit fake configuration instances are populated with the same properties, including the same default values, as a genuine Configuration object instance. This ensures that logic under test is going to behave far more as it would with a real configuration object which means a great deal more assurance in the tests. Achieve this by using the dictionary of defaults that were split out and made statically defined some time ago. FakeConfiguration becomes a SimpleNamespace thereby ensuring both that normal attribute lookup works correctly but also disallows unknown properties being attached to configuration arbitrarily. This keeps us honest in the properties we expose and prevents accidental additions. Since FakeConfiguration needs to track the real Configuration the only permissible keys are those of a genuine object. Unfortunately it seems that beyond the specified defaults many properties are set dynamically when loading a configuration. Lay the first steps for these objects becoming regular by making a couple of properties used by existing tests static; either existing defaults are re-used to avoid functional change or properties are explicitly defined with auto load-time behaviour. Of particular note is the use of keyword_auto for new_user_default_ui. Finally, use the opportunity to improve the integration of the various fake objects into test cases. The provided fake configuration will now be passed the fake logger attached provided by the MiG testcase, which means that it will be included in the logger message detection logic.
1 parent d5b2230 commit 84d8031

8 files changed

Lines changed: 242 additions & 43 deletions

mig/shared/configuration.py

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import copy
3838
import datetime
3939
import functools
40+
import inspect
4041
import os
4142
import pwd
4243
import re
@@ -74,6 +75,17 @@
7475
print("could not import migrid modules")
7576

7677

78+
_CONFIGURATION_NOFORWARD_KEYS = set([
79+
'self',
80+
'config_file',
81+
'mig_server_id',
82+
'disable_auth_log',
83+
'skip_log',
84+
'verbose',
85+
'logger',
86+
])
87+
88+
7789
def include_section_contents(logger, config, section, load_path, verbose=False,
7890
reject_overrides=[]):
7991
"""Include additional section contents from load_path in config."""
@@ -435,6 +447,11 @@ def fix_missing(config_file, verbose=True):
435447
fd.close()
436448

437449

450+
def _configuraton_dict_without_noforward_keys(configuration_like):
451+
return { k: v for k, v in configuration_like.__dict__.items()
452+
if k not in _CONFIGURATION_NOFORWARD_KEYS }
453+
454+
438455
class NativeConfigParser(ConfigParser):
439456
"""Wraps configparser.ConfigParser to force get method to return native
440457
string instead of always returning unicode.
@@ -445,7 +462,11 @@ def get(self, *args, **kwargs):
445462
return force_native_str(ConfigParser.get(self, *args, **kwargs))
446463

447464

448-
_CONFIGURATION_DEFAULTS = {
465+
# TODO: this static definition is incomplete with many properties being
466+
# dynamically assigned to Configuration objects at the point they are
467+
# loaded - expand these as code that makes use of those properties comes
468+
# under test with the ultimate goal that this becomes exhaustive
469+
_CONFIGURATION_PROPERTIES = {
449470
# Optional conf options with default values
450471
'state_path': os.path.expanduser('~/state'),
451472
'mig_path': os.path.expanduser('~/mig'),
@@ -469,6 +490,7 @@ def get(self, *args, **kwargs):
469490
'ca_smtp': '',
470491
'ca_user': 'mig-ca',
471492
'resource_home': '',
493+
'short_title': 'MiG',
472494
'vgrid_home': '',
473495
'vgrid_public_base': '',
474496
'vgrid_private_base': '',
@@ -511,6 +533,7 @@ def get(self, *args, **kwargs):
511533
'workflows_vgrid_patterns_home': '',
512534
'workflows_vgrid_recipes_home': '',
513535
'workflows_vgrid_history_home': '',
536+
'site_user_id_format': DEFAULT_USER_ID_FORMAT,
514537
'site_prefer_python3': False,
515538
'site_autolaunch_page': '',
516539
'site_landing_page': '',
@@ -538,6 +561,12 @@ def get(self, *args, **kwargs):
538561
'site_password_policy': POLICY_MEDIUM,
539562
'site_password_legacy_policy': False,
540563
'site_password_cracklib': False,
564+
565+
# Salt values which are undonditionally populated on configuration load
566+
'site_crypto_salt': '',
567+
'site_password_salt': '',
568+
'site_digest_salt': '',
569+
541570
'site_extra_userpage_scripts': "",
542571
'site_extra_userpage_styles': "",
543572
'hg_path': '',
@@ -725,6 +754,7 @@ def get(self, *args, **kwargs):
725754
'expire_peer': 600,
726755
'language': ['English'],
727756
'user_interface': ['V2', 'V3'],
757+
'new_user_default_ui': keyword_auto,
728758
'submitui': ['fields', 'textarea', 'files'],
729759
# Init user default page with no selection to use site landing page
730760
'default_page': [''],
@@ -747,6 +777,8 @@ def get(self, *args, **kwargs):
747777
# fyrgrid, benedict. Otherwise, ldap://bla.bla:2135/...
748778

749779
'arc_clusters': [],
780+
781+
'cloud_services': [],
750782
}
751783

752784

@@ -763,9 +795,9 @@ def __init__(self, config_file, verbose=False, skip_log=False,
763795
self.auth_logger_obj = None
764796
self.gdp_logger_obj = None
765797

766-
configuration_options = copy.deepcopy(_CONFIGURATION_DEFAULTS)
798+
configuration_properties = copy.deepcopy(_CONFIGURATION_PROPERTIES)
767799

768-
for k, v in configuration_options.items():
800+
for k, v in configuration_properties.items():
769801
setattr(self, k, v)
770802

771803
if config_file is not None:
@@ -1018,19 +1050,24 @@ def reload_config(self, verbose, skip_log=False, disable_auth_log=False,
10181050
self.site_title = "Minimum intrusion Grid"
10191051
if config.has_option('SITE', 'short_title'):
10201052
self.short_title = config.get('SITE', 'short_title')
1021-
else:
1022-
self.short_title = "MiG"
1053+
10231054
if config.has_option('SITE', 'user_interface'):
10241055
self.user_interface = config.get(
10251056
'SITE', 'user_interface').split()
10261057
else:
10271058
self.user_interface = ['V2']
1059+
10281060
# Allow gradual transition to new user interface - only new sign ups
10291061
if config.has_option('SITE', 'new_user_default_ui'):
10301062
self.new_user_default_ui = config.get(
10311063
'SITE', 'new_user_default_ui').strip()
1032-
else:
1064+
elif self.new_user_default_ui == keyword_auto and self.user_interface:
1065+
# an explicit default ui value for new users was not specified so
1066+
# use the first entry in supported user interfaces as a fallback
10331067
self.new_user_default_ui = self.user_interface[0]
1068+
else:
1069+
print("No usable value supplied for default new user ui version.")
1070+
raise IOError
10341071

10351072
if config.has_option('GLOBAL', 'state_path'):
10361073
self.state_path = config.get('GLOBAL', 'state_path')
@@ -2034,8 +2071,6 @@ def reload_config(self, verbose, skip_log=False, disable_auth_log=False,
20342071
logger.warning("invalid user_id_format %r - using default" %
20352072
self.site_user_id_format)
20362073
self.site_user_id_format = DEFAULT_USER_ID_FORMAT
2037-
else:
2038-
self.site_user_id_format = DEFAULT_USER_ID_FORMAT
20392074
if config.has_option('SITE', 'autolaunch_page'):
20402075
self.site_autolaunch_page = config.get('SITE', 'autolaunch_page')
20412076
else:
@@ -2863,6 +2898,21 @@ def parse_peers(self, peerfile):
28632898
peerfile)
28642899
return peers_dict
28652900

2901+
@staticmethod
2902+
def is_configuration_like(obj):
2903+
"""Does the given object quack like a MiG Configuration."""
2904+
return inspect.ismethod(getattr(obj, 'reload_config', None))
2905+
2906+
@staticmethod
2907+
def to_dict(obj):
2908+
"""Return a plain dictionary of the loaded configuration values only
2909+
given a Configuration-like object as input."""
2910+
assert Configuration.is_configuration_like(obj)
2911+
return _configuraton_dict_without_noforward_keys(obj)
2912+
2913+
2914+
_CONFIGURATION_ARGUMENTS = set(_CONFIGURATION_PROPERTIES.keys()) - _CONFIGURATION_NOFORWARD_KEYS
2915+
28662916

28672917
if '__main__' == __name__:
28682918
conf = Configuration(os.path.expanduser('~/mig/server/MiGserver.conf'),

tests/fixture/mig_shared_configuration--new.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"ca_smtp": "",
3030
"ca_user": "mig-ca",
3131
"certs_path": "/some/place/certs",
32+
"cloud_services": [],
3233
"config_file": null,
3334
"cputime_for_empty_jobs": 0,
3435
"default_page": [
@@ -130,6 +131,7 @@
130131
"min_seconds_between_live_update_requests": 0,
131132
"mrsl_files_dir": "",
132133
"myfiles_py_location": "",
134+
"new_user_default_ui": "AUTO",
133135
"notify_home": "",
134136
"openid_store": "",
135137
"passphrase_file": "",
@@ -153,6 +155,7 @@
153155
"sessid_to_jupyter_mount_link_home": "",
154156
"sessid_to_mrsl_link_home": "",
155157
"sharelink_home": "",
158+
"short_title": "MiG",
156159
"site_advanced_vgrid_links": [],
157160
"site_autolaunch_page": "",
158161
"site_cloud_access": [
@@ -173,6 +176,9 @@
173176
"site_password_cracklib": false,
174177
"site_password_legacy_policy": false,
175178
"site_password_policy": "MEDIUM",
179+
"site_crypto_salt": "",
180+
"site_password_salt": "",
181+
"site_digest_salt": "",
176182
"site_peers_notice": "",
177183
"site_peers_permit": [
178184
[
@@ -198,6 +204,7 @@
198204
]
199205
],
200206
"site_skin": "",
207+
"site_user_id_format": "X509",
201208
"site_vgrid_creators": [
202209
[
203210
"distinguished_name",

tests/support/__init__.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -218,9 +218,9 @@ def _reset_logging(self, stream):
218218
# testcase defaults
219219

220220
@staticmethod
221-
def _make_configuration_instance(configuration_to_make):
221+
def _make_configuration_instance(testcase, configuration_to_make):
222222
if configuration_to_make == 'fakeconfig':
223-
return FakeConfiguration()
223+
return FakeConfiguration(logger=testcase.logger)
224224
elif configuration_to_make == 'testconfig':
225225
from mig.shared.conf import get_configuration_object
226226
return get_configuration_object(skip_log=True, disable_auth_log=True)
@@ -229,7 +229,7 @@ def _make_configuration_instance(configuration_to_make):
229229
"MigTestCase: unknown configuration %r" % (configuration_to_make,))
230230

231231
def _provide_configuration(self):
232-
return 'fakeconfig'
232+
return 'unspecified'
233233

234234
@property
235235
def configuration(self):
@@ -239,8 +239,13 @@ def configuration(self):
239239
return self._configuration
240240

241241
configuration_to_make = self._provide_configuration()
242+
243+
if configuration_to_make == 'unspecified':
244+
raise AssertionError(
245+
"configuration access but testcase did not request it")
246+
242247
configuration_instance = self._make_configuration_instance(
243-
configuration_to_make)
248+
self, configuration_to_make)
244249

245250
if configuration_to_make == 'testconfig':
246251
# use the paths defined by the loaded configuration to create

tests/support/configsupp.py

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,61 @@
2929

3030
from tests.support.loggersupp import FakeLogger
3131

32+
from mig.shared.compat import SimpleNamespace
33+
from mig.shared.configuration import \
34+
_CONFIGURATION_ARGUMENTS, _CONFIGURATION_PROPERTIES
35+
36+
37+
def _ensure_only_configuration_keys(thedict):
38+
"""Check a dictionary contains only keys valid as Configuration properties.
39+
"""
40+
41+
unknown_keys = set(thedict.keys()) - set(_CONFIGURATION_ARGUMENTS)
42+
assert len(unknown_keys) == 0, \
43+
"non-Configuration keys: %s" % (', '.join(unknown_keys),)
44+
45+
46+
def _generate_namespace_kwargs():
47+
"""Create plain dictionary with supported properties and keys that map to
48+
their default values suitable for use in fabricating a namespace.
49+
"""
50+
51+
properties_and_defaults = dict(_CONFIGURATION_PROPERTIES)
52+
properties_and_defaults['logger'] = None
53+
return properties_and_defaults
54+
55+
56+
class FakeConfiguration(SimpleNamespace):
57+
"""An object that can act as a representative Configuration which can be
58+
programmed with particular values required to exercise code under test.
59+
60+
This object will track standard values as would be present on a genuine
61+
Configuration instance such that code under test expecting such can be
62+
handed something. The defaults are overlaid by any explicit keyword args.
3263
33-
class FakeConfiguration:
34-
"""A simple helper to pretend we have a real Configuration object with any
35-
required attributes explicitly passed.
3664
Automatically attaches a FakeLogger instance if no logger is provided in
3765
kwargs.
3866
"""
3967

40-
def __init__(self, **kwargs):
41-
"""Initialise instance attributes to be any named args provided and a
42-
FakeLogger instance attached if not provided.
68+
def __init__(self, logger=None, **kwargs):
69+
"""Initialise instance attributes based on the defaults plus any
70+
supplied additional options.
4371
"""
44-
self.__dict__.update(kwargs)
45-
if not 'logger' in self.__dict__:
46-
dummy_logger = FakeLogger()
47-
self.__dict__.update({'logger': dummy_logger})
72+
73+
SimpleNamespace.__init__(self, **_generate_namespace_kwargs())
74+
75+
if logger is None:
76+
# TODO: remove this conditional once all callers that require a
77+
# FakeConfiguration request it via _provide_configuration()
78+
logger = FakeLogger()
79+
self.logger = logger
80+
81+
if kwargs:
82+
_ensure_only_configuration_keys(kwargs)
83+
for k, v in kwargs.items():
84+
setattr(self, k, v)
85+
86+
def reload_config(self, *args, **kwargs):
87+
"""Stub defined to quack like Configuration."""
88+
89+
pass

tests/test_mig_lib_janitor.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737
class MigLibJanitor(MigTestCase):
3838
"""Unit tests for janitor related helper functions"""
3939

40+
def _provide_configuration(self):
41+
return 'fakeconfig'
42+
4043
def test_last_run_bookkeeping(self):
4144
"""Register a last run timestamp and check it"""
4245
expect = -1

tests/test_mig_shared_configuration.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,38 @@
3333

3434
from tests.support import MigTestCase, TEST_DATA_DIR, PY2, testmain, \
3535
fixturefile
36-
from mig.shared.configuration import Configuration
37-
38-
39-
def _is_method(value):
40-
return type(value).__name__ == 'method'
36+
from mig.shared.configuration import Configuration, \
37+
_CONFIGURATION_ARGUMENTS, _CONFIGURATION_PROPERTIES
4138

4239

4340
def _to_dict(obj):
4441
return {k: v for k, v in inspect.getmembers(obj)
45-
if not (k.startswith('__') or _is_method(v))}
42+
if not (k.startswith('__') or inspect.ismethod(v) or inspect.isfunction(v))}
43+
44+
45+
class MigSharedConfiguration__static_definitions(MigTestCase):
46+
"""Coverage of the static definitions underlying Configuration objects."""
47+
48+
def test_consistent_parameters(self):
49+
configuration_defaults_keys = set(_CONFIGURATION_PROPERTIES.keys())
50+
mismatched = _CONFIGURATION_ARGUMENTS - configuration_defaults_keys
51+
52+
self.assertEqual(len(mismatched), 0,
53+
"configuration defaults do not match arguments")
54+
55+
56+
class MigSharedConfiguration__loaded_configurations(MigTestCase):
57+
"""Coverage of loaded Configuration instances."""
58+
4659

60+
def test_argument_new_user_default_ui_is_replaced(self):
61+
test_conf_file = os.path.join(
62+
TEST_DATA_DIR, 'MiGserver--customised.conf')
4763

48-
class MigSharedConfiguration(MigTestCase):
49-
"""Wrap unit tests for the corresponding module"""
64+
configuration = Configuration(
65+
test_conf_file, skip_log=True, disable_auth_log=True)
66+
67+
self.assertEqual(configuration.new_user_default_ui, 'V3')
5068

5169
def test_argument_storage_protocols(self):
5270
test_conf_file = os.path.join(
@@ -61,6 +79,7 @@ def test_argument_storage_protocols(self):
6179
# self.assertEqual(configuration.storage_protocols, ['sftp'])
6280
self.assertEqual(configuration.storage_protocols, [])
6381

82+
6483
def test_argument_wwwserve_max_bytes(self):
6584
test_conf_file = os.path.join(
6685
TEST_DATA_DIR, 'MiGserver--customised.conf')
@@ -314,6 +333,10 @@ def test_argument_include_sections_multi_ignores_other_sections(self):
314333
# TODO: rename file to valid section name we can check and enable next?
315334
# self.assertEqual(configuration.multi, 'blabla')
316335

336+
337+
class MigSharedConfiguration__new_instance(MigTestCase):
338+
"""Coverage of programatically created Configuration instances."""
339+
317340
@unittest.skipIf(PY2, "Python 3 only")
318341
def test_default_object(self):
319342
prepared_fixture = self.prepareFixtureAssert(

0 commit comments

Comments
 (0)