Skip to content

Commit 403e09e

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 was split out previously to initialize the FakeConfiguration which itself is now a SimpleNamespace. Making it a namespace ensures that attribute lookup, something that normal Configuration objects support, work correctly but in addition forces the attributes to be set "up front". This keeps us honest in the properties we expose. Since a FakeConfiguration needs to track the real Configuration we also prevent the addition of attributes that not keys of a real configuration. Unfortunarely it seems that a lot of properties are set dynamically as part of loading a configuration, but lay the first steps to a canonical configuration object by making a couple of properties used by existing tests static; existing defaults are re-used to avoid functional change.
1 parent ba904c2 commit 403e09e

5 files changed

Lines changed: 141 additions & 21 deletions

File tree

mig/shared/configuration.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,17 @@
7474
print("could not import migrid modules")
7575

7676

77+
_CONFIGURATION_NOFORWARD_KEYS = set([
78+
'self',
79+
'config_file',
80+
'mig_server_id',
81+
'disable_auth_log',
82+
'skip_log',
83+
'verbose',
84+
'logger',
85+
])
86+
87+
7788
def include_section_contents(logger, config, section, load_path, verbose=False,
7889
reject_overrides=[]):
7990
"""Include additional section contents from load_path in config."""
@@ -434,6 +445,10 @@ def fix_missing(config_file, verbose=True):
434445
fd.close()
435446

436447

448+
def _without_noforward_keys(d):
449+
return { k: v for k, v in d.items() if k not in _CONFIGURATION_NOFORWARD_KEYS }
450+
451+
437452
class NativeConfigParser(ConfigParser):
438453
"""Wraps configparser.ConfigParser to force get method to return native
439454
string instead of always returning unicode.
@@ -468,6 +483,7 @@ def get(self, *args, **kwargs):
468483
'ca_smtp': '',
469484
'ca_user': 'mig-ca',
470485
'resource_home': '',
486+
'short_title': 'MiG',
471487
'vgrid_home': '',
472488
'vgrid_public_base': '',
473489
'vgrid_private_base': '',
@@ -510,6 +526,7 @@ def get(self, *args, **kwargs):
510526
'workflows_vgrid_patterns_home': '',
511527
'workflows_vgrid_recipes_home': '',
512528
'workflows_vgrid_history_home': '',
529+
'site_user_id_format': DEFAULT_USER_ID_FORMAT,
513530
'site_prefer_python3': False,
514531
'site_autolaunch_page': '',
515532
'site_landing_page': '',
@@ -722,6 +739,7 @@ def get(self, *args, **kwargs):
722739
'expire_peer': 600,
723740
'language': ['English'],
724741
'user_interface': ['V2', 'V3'],
742+
'new_user_default_ui': 'V2',
725743
'submitui': ['fields', 'textarea', 'files'],
726744
# Init user default page with no selection to use site landing page
727745
'default_page': [''],
@@ -744,6 +762,8 @@ def get(self, *args, **kwargs):
744762
# fyrgrid, benedict. Otherwise, ldap://bla.bla:2135/...
745763

746764
'arc_clusters': [],
765+
766+
'cloud_services': [],
747767
}
748768

749769

@@ -1015,8 +1035,6 @@ def reload_config(self, verbose, skip_log=False, disable_auth_log=False,
10151035
self.site_title = "Minimum intrusion Grid"
10161036
if config.has_option('SITE', 'short_title'):
10171037
self.short_title = config.get('SITE', 'short_title')
1018-
else:
1019-
self.short_title = "MiG"
10201038
if config.has_option('SITE', 'user_interface'):
10211039
self.user_interface = config.get(
10221040
'SITE', 'user_interface').split()
@@ -1026,8 +1044,6 @@ def reload_config(self, verbose, skip_log=False, disable_auth_log=False,
10261044
if config.has_option('SITE', 'new_user_default_ui'):
10271045
self.new_user_default_ui = config.get(
10281046
'SITE', 'new_user_default_ui').strip()
1029-
else:
1030-
self.new_user_default_ui = self.user_interface[0]
10311047

10321048
if config.has_option('GLOBAL', 'state_path'):
10331049
self.state_path = config.get('GLOBAL', 'state_path')
@@ -2024,8 +2040,6 @@ def reload_config(self, verbose, skip_log=False, disable_auth_log=False,
20242040
logger.warning("invalid user_id_format %r - using default" %
20252041
self.site_user_id_format)
20262042
self.site_user_id_format = DEFAULT_USER_ID_FORMAT
2027-
else:
2028-
self.site_user_id_format = DEFAULT_USER_ID_FORMAT
20292043
if config.has_option('SITE', 'autolaunch_page'):
20302044
self.site_autolaunch_page = config.get('SITE', 'autolaunch_page')
20312045
else:
@@ -2850,6 +2864,14 @@ def parse_peers(self, peerfile):
28502864
peerfile)
28512865
return peers_dict
28522866

2867+
@staticmethod
2868+
def as_dict(thing):
2869+
assert isinstance(thing, Configuration)
2870+
return _without_noforward_keys(thing.__dict__)
2871+
2872+
2873+
_CONFIGURATION_ARGUMENTS = set(_CONFIGURATION_DEFAULTS.keys()) - _CONFIGURATION_NOFORWARD_KEYS
2874+
28532875

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

tests/fixture/mig_shared_configuration--new.json

Lines changed: 4 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": "V2",
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": [
@@ -192,6 +195,7 @@
192195
"extcert"
193196
],
194197
"site_skin": "",
198+
"site_user_id_format": "X509",
195199
"site_vgrid_creators": [
196200
[
197201
"distinguished_name",

tests/support/configsupp.py

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,54 @@
2929

3030
from tests.support.loggersupp import FakeLogger
3131

32+
from mig.shared.compat import SimpleNamespace
33+
from mig.shared.configuration import _without_noforward_keys, \
34+
_CONFIGURATION_ARGUMENTS, _CONFIGURATION_DEFAULTS
35+
36+
37+
def _generate_namespace_kwargs():
38+
d = dict(_CONFIGURATION_DEFAULTS)
39+
d['logger'] = None
40+
return d
41+
42+
43+
def _ensure_only_configuration_keys(d):
44+
"""Check a dictionary contains only keys valid as Configuration properties.
45+
"""
46+
47+
unknown_keys = set(d.keys()) - set(_CONFIGURATION_ARGUMENTS)
48+
assert len(unknown_keys) == 0, \
49+
"non-Configuration keys: %s" % (', '.join(unknown_keys),)
50+
51+
52+
class FakeConfiguration(SimpleNamespace):
53+
"""An object that can act as a representative Configuration which can be
54+
programmed with particular values required to exercise code under test.
55+
56+
This object will track standard values as would be present on a fresh
57+
Configuration instance such that code under test can be handed something
58+
representative. The defaults are overlaid by any explciit keyword args.
3259
33-
class FakeConfiguration:
34-
"""A simple helper to pretend we have a real Configuration object with any
35-
required attributes explicitly passed.
3660
Automatically attaches a FakeLogger instance if no logger is provided in
3761
kwargs.
3862
"""
3963

4064
def __init__(self, **kwargs):
41-
"""Initialise instance attributes to be any named args provided and a
42-
FakeLogger instance attached if not provided.
65+
"""Initialise instance attributes based on the defaults plus any
66+
supplied additional options.
4367
"""
44-
self.__dict__.update(kwargs)
45-
if not 'logger' in self.__dict__:
46-
dummy_logger = FakeLogger()
47-
self.__dict__.update({'logger': dummy_logger})
68+
69+
SimpleNamespace.__init__(self, **_generate_namespace_kwargs())
70+
71+
if kwargs:
72+
_ensure_only_configuration_keys(kwargs)
73+
for k, v in kwargs.items():
74+
setattr(self, k, v)
75+
76+
if 'logger' not in kwargs:
77+
self.logger = FakeLogger()
78+
79+
@staticmethod
80+
def as_dict(thing):
81+
assert isinstance(thing, FakeConfiguration)
82+
return _without_noforward_keys(thing.__dict__)

tests/test_mig_shared_configuration.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,21 +33,25 @@
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))}
4643

4744

4845
class MigSharedConfiguration(MigTestCase):
4946
"""Wrap unit tests for the corresponding module"""
5047

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+
5155
def test_argument_storage_protocols(self):
5256
test_conf_file = os.path.join(
5357
TEST_DATA_DIR, 'MiGserver--customised.conf')
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# --- BEGIN_HEADER ---
4+
#
5+
# test_tests_support_configsupp - unit test of the corresponding tests module
6+
# Copyright (C) 2003-2024 The MiG Project by the Science HPC Center at UCPH
7+
#
8+
# This file is part of MiG.
9+
#
10+
# MiG is free software: you can redistribute it and/or modify
11+
# it under the terms of the GNU General Public License as published by
12+
# the Free Software Foundation; either version 2 of the License, or
13+
# (at your option) any later version.
14+
#
15+
# MiG is distributed in the hope that it will be useful,
16+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
# GNU General Public License for more details.
19+
#
20+
# You should have received a copy of the GNU General Public License
21+
# along with this program; if not, write to the Free Software
22+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
23+
# USA.
24+
#
25+
# --- END_HEADER ---
26+
#
27+
28+
"""Unit tests for the tests module pointed to in the filename"""
29+
30+
from tests.support import MigTestCase, testmain
31+
from tests.support.configsupp import FakeConfiguration
32+
33+
from mig.shared.configuration import Configuration, \
34+
_CONFIGURATION_ARGUMENTS, _CONFIGURATION_DEFAULTS, \
35+
_CONFIGURATION_NOFORWARD_KEYS, _without_noforward_keys
36+
37+
38+
class MigSharedInstall_FakeConfiguration(MigTestCase):
39+
def test_consistent_parameters(self):
40+
default_configuration = Configuration(None)
41+
fake_configuration = FakeConfiguration()
42+
43+
self.maxDiff = None
44+
self.assertEqual(
45+
Configuration.as_dict(default_configuration),
46+
FakeConfiguration.as_dict(fake_configuration)
47+
)
48+
49+
def test_only_configuration_keys(self):
50+
with self.assertRaises(AssertionError):
51+
FakeConfiguration(bar='1')
52+
53+
54+
if __name__ == '__main__':
55+
testmain()

0 commit comments

Comments
 (0)