Skip to content

Commit f7e4c8d

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 f7e4c8d

8 files changed

Lines changed: 235 additions & 40 deletions

mig/shared/configuration.py

Lines changed: 51 additions & 5 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.
@@ -469,6 +486,7 @@ def get(self, *args, **kwargs):
469486
'ca_smtp': '',
470487
'ca_user': 'mig-ca',
471488
'resource_home': '',
489+
'short_title': 'MiG',
472490
'vgrid_home': '',
473491
'vgrid_public_base': '',
474492
'vgrid_private_base': '',
@@ -511,6 +529,7 @@ def get(self, *args, **kwargs):
511529
'workflows_vgrid_patterns_home': '',
512530
'workflows_vgrid_recipes_home': '',
513531
'workflows_vgrid_history_home': '',
532+
'site_user_id_format': DEFAULT_USER_ID_FORMAT,
514533
'site_prefer_python3': False,
515534
'site_autolaunch_page': '',
516535
'site_landing_page': '',
@@ -538,6 +557,12 @@ def get(self, *args, **kwargs):
538557
'site_password_policy': POLICY_MEDIUM,
539558
'site_password_legacy_policy': False,
540559
'site_password_cracklib': False,
560+
561+
# Salt values which are undonditionally populatde on configurtion load
562+
'site_crypto_salt': '',
563+
'site_password_salt': '',
564+
'site_digest_salt': '',
565+
541566
'site_extra_userpage_scripts': "",
542567
'site_extra_userpage_styles': "",
543568
'hg_path': '',
@@ -725,6 +750,7 @@ def get(self, *args, **kwargs):
725750
'expire_peer': 600,
726751
'language': ['English'],
727752
'user_interface': ['V2', 'V3'],
753+
'new_user_default_ui': keyword_auto,
728754
'submitui': ['fields', 'textarea', 'files'],
729755
# Init user default page with no selection to use site landing page
730756
'default_page': [''],
@@ -747,6 +773,8 @@ def get(self, *args, **kwargs):
747773
# fyrgrid, benedict. Otherwise, ldap://bla.bla:2135/...
748774

749775
'arc_clusters': [],
776+
777+
'cloud_services': [],
750778
}
751779

752780

@@ -1018,19 +1046,24 @@ def reload_config(self, verbose, skip_log=False, disable_auth_log=False,
10181046
self.site_title = "Minimum intrusion Grid"
10191047
if config.has_option('SITE', 'short_title'):
10201048
self.short_title = config.get('SITE', 'short_title')
1021-
else:
1022-
self.short_title = "MiG"
1049+
10231050
if config.has_option('SITE', 'user_interface'):
10241051
self.user_interface = config.get(
10251052
'SITE', 'user_interface').split()
10261053
else:
10271054
self.user_interface = ['V2']
1055+
10281056
# Allow gradual transition to new user interface - only new sign ups
10291057
if config.has_option('SITE', 'new_user_default_ui'):
10301058
self.new_user_default_ui = config.get(
10311059
'SITE', 'new_user_default_ui').strip()
1032-
else:
1060+
elif self.new_user_default_ui == keyword_auto and self.user_interface:
1061+
# an explicit default ui value for new users was not specified so
1062+
# use the first entry in supported user interfaces as a fallback
10331063
self.new_user_default_ui = self.user_interface[0]
1064+
else:
1065+
print("No usable value supplied for default new user ui version.")
1066+
raise IOError
10341067

10351068
if config.has_option('GLOBAL', 'state_path'):
10361069
self.state_path = config.get('GLOBAL', 'state_path')
@@ -2034,8 +2067,6 @@ def reload_config(self, verbose, skip_log=False, disable_auth_log=False,
20342067
logger.warning("invalid user_id_format %r - using default" %
20352068
self.site_user_id_format)
20362069
self.site_user_id_format = DEFAULT_USER_ID_FORMAT
2037-
else:
2038-
self.site_user_id_format = DEFAULT_USER_ID_FORMAT
20392070
if config.has_option('SITE', 'autolaunch_page'):
20402071
self.site_autolaunch_page = config.get('SITE', 'autolaunch_page')
20412072
else:
@@ -2863,6 +2894,21 @@ def parse_peers(self, peerfile):
28632894
peerfile)
28642895
return peers_dict
28652896

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

28672913
if '__main__' == __name__:
28682914
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_DEFAULTS
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_DEFAULTS)
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 fresh
61+
Configuration instance such that code under test can be handed something
62+
representative. The defaults are overlaid by any explciit 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_DEFAULTS
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_DEFAULTS.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)