Skip to content

Commit 40c209e

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 489d4d2 commit 40c209e

File tree

6 files changed

+142
-17
lines changed

6 files changed

+142
-17
lines changed

mig/shared/compat.py

Lines changed: 10 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
@@ -93,6 +94,15 @@ def ensure_native_string(string_or_bytes):
9394
return textual_output
9495

9596

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

mig/shared/configuration.py

Lines changed: 28 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, \
@@ -73,6 +74,17 @@
7374
print("could not import migrid modules")
7475

7576

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+
7688
def expand_external_sources(logger, val):
7789
"""Expand a string containing ENV::NAME, FILE::PATH or FILE::PATH$$CACHE
7890
references to fill in the content of the corresponding environment, file or
@@ -392,6 +404,10 @@ def fix_missing(config_file, verbose=True):
392404
fd.close()
393405

394406

407+
def _without_noforward_keys(d):
408+
return { k: v for k, v in d.items() if k not in _CONFIGURATION_NOFORWARD_KEYS }
409+
410+
395411
class NativeConfigParser(ConfigParser):
396412
"""Wraps configparser.ConfigParser to force get method to return native
397413
string instead of always returning unicode.
@@ -426,6 +442,7 @@ def get(self, *args, **kwargs):
426442
'ca_smtp': '',
427443
'ca_user': 'mig-ca',
428444
'resource_home': '',
445+
'short_title': 'MiG',
429446
'vgrid_home': '',
430447
'vgrid_public_base': '',
431448
'vgrid_private_base': '',
@@ -470,6 +487,7 @@ def get(self, *args, **kwargs):
470487
'workflows_vgrid_patterns_home': '',
471488
'workflows_vgrid_recipes_home': '',
472489
'workflows_vgrid_history_home': '',
490+
'site_user_id_format': DEFAULT_USER_ID_FORMAT,
473491
'site_prefer_python3': False,
474492
'site_autolaunch_page': '',
475493
'site_landing_page': '',
@@ -681,6 +699,7 @@ def get(self, *args, **kwargs):
681699
'expire_peer': 600,
682700
'language': ['English'],
683701
'user_interface': ['V2', 'V3'],
702+
'new_user_default_ui': 'V2',
684703
'submitui': ['fields', 'textarea', 'files'],
685704
# Init user default page with no selection to use site landing page
686705
'default_page': [''],
@@ -703,6 +722,8 @@ def get(self, *args, **kwargs):
703722
# fyrgrid, benedict. Otherwise, ldap://bla.bla:2135/...
704723

705724
'arc_clusters': [],
725+
726+
'cloud_services': [],
706727
}
707728

708729

@@ -893,8 +914,6 @@ def reload_config(self, verbose, skip_log=False, disable_auth_log=False,
893914
self.site_title = "Minimum intrusion Grid"
894915
if config.has_option('SITE', 'short_title'):
895916
self.short_title = config.get('SITE', 'short_title')
896-
else:
897-
self.short_title = "MiG"
898917
if config.has_option('SITE', 'user_interface'):
899918
self.user_interface = config.get(
900919
'SITE', 'user_interface').split()
@@ -904,8 +923,6 @@ def reload_config(self, verbose, skip_log=False, disable_auth_log=False,
904923
if config.has_option('SITE', 'new_user_default_ui'):
905924
self.new_user_default_ui = config.get(
906925
'SITE', 'new_user_default_ui').strip()
907-
else:
908-
self.new_user_default_ui = self.user_interface[0]
909926

910927
if config.has_option('GLOBAL', 'state_path'):
911928
self.state_path = config.get('GLOBAL', 'state_path')
@@ -1682,7 +1699,6 @@ def reload_config(self, verbose, skip_log=False, disable_auth_log=False,
16821699
for option in
16831700
config.options(section)})
16841701

1685-
self.cloud_services = []
16861702
# List of service options with default and override map
16871703
override_map_keys = ['service_user', 'service_max_user_instances',
16881704
'service_image_alias', 'service_allowed_images',
@@ -1693,6 +1709,7 @@ def reload_config(self, verbose, skip_log=False, disable_auth_log=False,
16931709
'service_jumphost_address',
16941710
'service_jumphost_user',
16951711
'service_jumphost_key']
1712+
16961713
# Load generated cloud sections
16971714
for section in config.sections():
16981715
if 'CLOUD_' in section:
@@ -1913,8 +1930,6 @@ def reload_config(self, verbose, skip_log=False, disable_auth_log=False,
19131930
logger.warning("invalid user_id_format %r - using default" %
19141931
self.site_user_id_format)
19151932
self.site_user_id_format = DEFAULT_USER_ID_FORMAT
1916-
else:
1917-
self.site_user_id_format = DEFAULT_USER_ID_FORMAT
19181933
if config.has_option('SITE', 'autolaunch_page'):
19191934
self.site_autolaunch_page = config.get('SITE', 'autolaunch_page')
19201935
else:
@@ -2742,6 +2757,12 @@ def parse_peers(self, peerfile):
27422757
peerfile)
27432758
return peers_dict
27442759

2760+
def to_dict(self):
2761+
return _without_noforward_keys(self.__dict__)
2762+
2763+
2764+
_CONFIGURATION_ARGUMENTS = set(_CONFIGURATION_DEFAULTS.keys()) - _CONFIGURATION_NOFORWARD_KEYS
2765+
27452766

27462767
if '__main__' == __name__:
27472768
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: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,46 @@
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+
self.__dict__.update(kwargs)
69+
70+
if 'logger' not in kwargs:
71+
self.logger = FakeLogger()
72+
73+
def to_dict(self):
74+
return _without_noforward_keys(self.__dict__)

tests/test_mig_shared_configuration.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
import unittest
3333

3434
from tests.support import MigTestCase, TEST_DATA_DIR, PY2, testmain, fixturefile
35-
from mig.shared.configuration import Configuration
35+
from mig.shared.configuration import Configuration, \
36+
_CONFIGURATION_ARGUMENTS, _CONFIGURATION_DEFAULTS
3637

3738

3839
def _is_method(value):
@@ -47,6 +48,13 @@ def _to_dict(obj):
4748
class MigSharedConfiguration(MigTestCase):
4849
"""Wrap unit tests for the corresponding module"""
4950

51+
def test_consistent_parameters(self):
52+
configuration_defaults_keys = set(_CONFIGURATION_DEFAULTS.keys())
53+
mismatched = _CONFIGURATION_ARGUMENTS - configuration_defaults_keys
54+
55+
self.assertEqual(len(mismatched), 0,
56+
"configuration defaults do not match arguments")
57+
5058
def test_argument_storage_protocols(self):
5159
test_conf_file = os.path.join(
5260
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+
default_configuration.to_dict(),
46+
fake_configuration.to_dict()
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)