Skip to content

Commit 219c4b4

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 laythe 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 7ec746c commit 219c4b4

6 files changed

Lines changed: 151 additions & 22 deletions

File tree

mig/shared/compat.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from past.builtins import basestring
3535

3636
import codecs
37+
import inspect
3738
import io
3839
import sys
3940
# NOTE: StringIO is only available in python2
@@ -55,6 +56,9 @@ def __getattribute__(self, name):
5556
return dict(**self)
5657

5758
return self[name]
59+
60+
def __setattr__(self, name, value):
61+
self[name] = value
5862
else:
5963
from types import SimpleNamespace
6064

@@ -93,6 +97,15 @@ def ensure_native_string(string_or_bytes):
9397
return textual_output
9498

9599

100+
def inspect_args(func):
101+
"""Wrapper to return the arguments of a function."""
102+
103+
if PY2:
104+
return inspect.getargspec(func).args
105+
else:
106+
return inspect.getfullargspec(func).args
107+
108+
96109
def NativeStringIO(initial_value=''):
97110
"""Mock StringIO pseudo-class to create a StringIO matching the native
98111
string coding form. That is a BytesIO with utf8 on python 2 and unicode

mig/shared/configuration.py

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
# NOTE: protect migrid import from autopep8 reordering
6060
try:
6161
from mig.shared.base import force_native_str
62+
from mig.shared.compat import inspect_args
6263
from mig.shared.defaults import CSRF_MINIMAL, CSRF_WARN, CSRF_MEDIUM, \
6364
CSRF_FULL, POLICY_NONE, POLICY_WEAK, POLICY_MEDIUM, POLICY_HIGH, \
6465
POLICY_MODERN, POLICY_CUSTOM, freeze_flavors, cert_field_order, \
@@ -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,10 @@ def fix_missing(config_file, verbose=True):
435447
fd.close()
436448

437449

450+
def _without_noforward_keys(d):
451+
return { k: v for k, v in d.items() if k not in _CONFIGURATION_NOFORWARD_KEYS }
452+
453+
438454
class NativeConfigParser(ConfigParser):
439455
"""Wraps configparser.ConfigParser to force get method to return native
440456
string instead of always returning unicode.
@@ -469,6 +485,7 @@ def get(self, *args, **kwargs):
469485
'ca_smtp': '',
470486
'ca_user': 'mig-ca',
471487
'resource_home': '',
488+
'short_title': 'MiG',
472489
'vgrid_home': '',
473490
'vgrid_public_base': '',
474491
'vgrid_private_base': '',
@@ -513,6 +530,7 @@ def get(self, *args, **kwargs):
513530
'workflows_vgrid_patterns_home': '',
514531
'workflows_vgrid_recipes_home': '',
515532
'workflows_vgrid_history_home': '',
533+
'site_user_id_format': DEFAULT_USER_ID_FORMAT,
516534
'site_prefer_python3': False,
517535
'site_autolaunch_page': '',
518536
'site_landing_page': '',
@@ -724,6 +742,7 @@ def get(self, *args, **kwargs):
724742
'expire_peer': 600,
725743
'language': ['English'],
726744
'user_interface': ['V2', 'V3'],
745+
'new_user_default_ui': 'V2',
727746
'submitui': ['fields', 'textarea', 'files'],
728747
# Init user default page with no selection to use site landing page
729748
'default_page': [''],
@@ -746,6 +765,8 @@ def get(self, *args, **kwargs):
746765
# fyrgrid, benedict. Otherwise, ldap://bla.bla:2135/...
747766

748767
'arc_clusters': [],
768+
769+
'cloud_services': [],
749770
}
750771

751772

@@ -1017,8 +1038,6 @@ def reload_config(self, verbose, skip_log=False, disable_auth_log=False,
10171038
self.site_title = "Minimum intrusion Grid"
10181039
if config.has_option('SITE', 'short_title'):
10191040
self.short_title = config.get('SITE', 'short_title')
1020-
else:
1021-
self.short_title = "MiG"
10221041
if config.has_option('SITE', 'user_interface'):
10231042
self.user_interface = config.get(
10241043
'SITE', 'user_interface').split()
@@ -1028,8 +1047,6 @@ def reload_config(self, verbose, skip_log=False, disable_auth_log=False,
10281047
if config.has_option('SITE', 'new_user_default_ui'):
10291048
self.new_user_default_ui = config.get(
10301049
'SITE', 'new_user_default_ui').strip()
1031-
else:
1032-
self.new_user_default_ui = self.user_interface[0]
10331050

10341051
if config.has_option('GLOBAL', 'state_path'):
10351052
self.state_path = config.get('GLOBAL', 'state_path')
@@ -1803,7 +1820,6 @@ def reload_config(self, verbose, skip_log=False, disable_auth_log=False,
18031820
for option in
18041821
config.options(section)})
18051822

1806-
self.cloud_services = []
18071823
# List of service options with default and override map
18081824
override_map_keys = ['service_user', 'service_max_user_instances',
18091825
'service_image_alias', 'service_allowed_images',
@@ -1814,6 +1830,7 @@ def reload_config(self, verbose, skip_log=False, disable_auth_log=False,
18141830
'service_jumphost_address',
18151831
'service_jumphost_user',
18161832
'service_jumphost_key']
1833+
18171834
# Load generated cloud sections
18181835
for section in config.sections():
18191836
if section.startswith('CLOUD_'):
@@ -2031,8 +2048,6 @@ def reload_config(self, verbose, skip_log=False, disable_auth_log=False,
20312048
logger.warning("invalid user_id_format %r - using default" %
20322049
self.site_user_id_format)
20332050
self.site_user_id_format = DEFAULT_USER_ID_FORMAT
2034-
else:
2035-
self.site_user_id_format = DEFAULT_USER_ID_FORMAT
20362051
if config.has_option('SITE', 'autolaunch_page'):
20372052
self.site_autolaunch_page = config.get('SITE', 'autolaunch_page')
20382053
else:
@@ -2860,6 +2875,14 @@ def parse_peers(self, peerfile):
28602875
peerfile)
28612876
return peers_dict
28622877

2878+
@staticmethod
2879+
def as_dict(thing):
2880+
assert isinstance(thing, Configuration)
2881+
return _without_noforward_keys(thing.__dict__)
2882+
2883+
2884+
_CONFIGURATION_ARGUMENTS = set(_CONFIGURATION_DEFAULTS.keys()) - _CONFIGURATION_NOFORWARD_KEYS
2885+
28632886

28642887
if '__main__' == __name__:
28652888
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
"paraview_home": "",
@@ -154,6 +156,7 @@
154156
"sessid_to_jupyter_mount_link_home": "",
155157
"sessid_to_mrsl_link_home": "",
156158
"sharelink_home": "",
159+
"short_title": "MiG",
157160
"site_advanced_vgrid_links": [],
158161
"site_autolaunch_page": "",
159162
"site_cloud_access": [
@@ -187,6 +190,7 @@
187190
"extcert"
188191
],
189192
"site_skin": "",
193+
"site_user_id_format": "X509",
190194
"site_vgrid_creators": [
191195
[
192196
"distinguished_name",

tests/support/configsupp.py

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,49 @@
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 the dictionary arguments contains only premitted keys."""
45+
46+
unknown_keys = set(d.keys()) - set(_CONFIGURATION_ARGUMENTS)
47+
assert len(unknown_keys) == 0, \
48+
"non-Configuration keys: %s" % (', '.join(unknown_keys),)
49+
50+
51+
class FakeConfiguration(SimpleNamespace):
52+
"""A simple helper to pretend we have a Configuration object populated
53+
with defaults overlaid with any explicitly supplied attributes.
3254
33-
class FakeConfiguration:
34-
"""A simple helper to pretend we have a real Configuration object with any
35-
required attributes explicitly passed.
3655
Automatically attaches a FakeLogger instance if no logger is provided in
3756
kwargs.
3857
"""
3958

4059
def __init__(self, **kwargs):
41-
"""Initialise instance attributes to be any named args provided and a
42-
FakeLogger instance attached if not provided.
60+
"""Initialise instance attributes based on the defaults plus any
61+
supplied additional options.
4362
"""
44-
self.__dict__.update(kwargs)
45-
if not 'logger' in self.__dict__:
46-
dummy_logger = FakeLogger()
47-
self.__dict__.update({'logger': dummy_logger})
63+
64+
SimpleNamespace.__init__(self, **_generate_namespace_kwargs())
65+
66+
if kwargs:
67+
_ensure_only_configuration_keys(kwargs)
68+
for k, v in kwargs.items():
69+
setattr(self, k, v)
70+
71+
if 'logger' not in kwargs:
72+
self.logger = FakeLogger()
73+
74+
@staticmethod
75+
def as_dict(thing):
76+
assert isinstance(thing, FakeConfiguration)
77+
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)