Skip to content

Commit 1342d84

Browse files
committed
Expand hints with better pickle bits and use it to cover create_user.
Provide a means to load pickled data from disk to which the hints mechanism as introduced for fixtures can be applied. Extensively document the rationale for hinting in light of expansion of this facility and its re-use. Use all the facilities for a test covering useradm create_user function. Note, create_user is mostly about writing rather than the population of the user dictionary with a majority of the important prep work with regard to what will be written being done by the calling script. This means that the we are only comparing written data as of now, meaning it is something of a loose test, but this towards additional rigour. Note that we used the opportunity to make sure that calling create_user writes exactly what was declared within our on-disk user fixture. This begins to verify the fixture itself and strenghtens the assurances of any test consuming that fixture (to the extent allowed given the responsibilities of create_user noted above - a start).
1 parent fa2a3ba commit 1342d84

File tree

6 files changed

+274
-40
lines changed

6 files changed

+274
-40
lines changed

envhelp/makeconfig.py

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,10 @@
4040

4141
from mig.shared.conf import get_configuration_object
4242
from mig.shared.install import MIG_BASE, generate_confs
43+
from mig.shared.useradm import _ensure_dirs_needed_for_userdb
4344

4445
_LOCAL_ENVHELP_OUTPUT_DIR = os.path.join(_LOCAL_MIG_BASE, "envhelp/output")
4546
_MAKECONFIG_ALLOWED = ["local", "test"]
46-
_USERADM_PATH_KEYS = ('user_cache', 'user_db_home', 'user_home',
47-
'user_settings', 'mrsl_files_dir', 'resource_pending')
4847

4948

5049
def _at(sequence, index=-1, default=None):
@@ -55,18 +54,6 @@ def _at(sequence, index=-1, default=None):
5554
return default
5655

5756

58-
def _ensure_dirs_needed_for_userdb(configuration):
59-
"""Provision the basic directories needed for the operation of the
60-
userdb deriving paths from the supplied configuration object."""
61-
62-
for config_key in _USERADM_PATH_KEYS:
63-
dir_path = getattr(configuration, config_key).rstrip(os.path.sep)
64-
try:
65-
os.makedirs(dir_path, exist_ok=True)
66-
except OSError as exc:
67-
pass
68-
69-
7057
def write_testconfig(env_name, is_docker=False):
7158
is_predefined = env_name == 'test'
7259
confs_name = '%sconfs' % (env_name,)

mig/shared/useradm.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,22 @@
102102
https_authdigests = user_db_filename
103103

104104

105+
_USERADM_PATH_KEYS = ('user_db_home', 'user_home', 'user_settings',
106+
'user_cache', 'mrsl_files_dir', 'resource_pending')
107+
108+
109+
def _ensure_dirs_needed_for_userdb(configuration):
110+
"""Provision the basic directories needed for the operation of the
111+
userdb deriving paths from the supplied configuration object."""
112+
113+
for config_key in _USERADM_PATH_KEYS:
114+
dir_path = getattr(configuration, config_key).rstrip(os.path.sep)
115+
try:
116+
os.makedirs(dir_path, exist_ok=True)
117+
except OSError as exc:
118+
pass
119+
120+
105121
def init_user_adm(dynamic_db_path=True):
106122
"""Shared init function for all user administration scripts.
107123
The optional dynamic_db_path argument toggles dynamic user db path lookup
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
[ONWRITE]
2-
convert_dict_bytes_to_strings_kv = True
2+
convert_dict_strings_to_bytes_kv = True

tests/support/fixturesupp.py

Lines changed: 86 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ def _fixturefile_loadrelative(relative_path, fixture_format=None):
6767
elif fixture_format == 'json':
6868
with open(tmp_path) as jsonfile:
6969
data = json.load(jsonfile, object_hook=_FixtureHint.object_hook)
70-
_hints_apply_if_present(tmp_path, data)
70+
_hints_apply_from_instances_if_present(data)
71+
_hints_apply_from_fixture_ini_if_present(tmp_path, data)
7172
else:
7273
raise AssertionError(
7374
"unsupported fixture format: %s" % (fixture_format,))
@@ -83,14 +84,34 @@ def _fixturefile_normname(relative_path, prefix=''):
8384
return normname
8485

8586

87+
# The following chunk of code is all related to "hints": small transformations
88+
# that can be requested to data as it read (and in some cases written) in the
89+
# course of a test run.
90+
#
91+
# The observation here is that the on-disk format of various structures may not
92+
# always be suitable for either as an actual or expected value in a comparison
93+
# or as a human-centric fixture format. But, we explicitly wish to consume the
94+
# value as written by the production code.
95+
#
96+
# Thus, we provide a series of small named transformations which can be
97+
# explicitly requested at a few strategic points (e.g. loading an on-disk file)
98+
# that allows assertions in tests to succinctly make assertions as opposed to
99+
# the intent of the check becoming drowned in the details of conversions etc.
100+
#
101+
# <hints>
102+
86103
def _hints_apply_array_of_tuples(value, modifier):
87-
"""Generate values for array_of_tuples hint."""
104+
"""
105+
Convert list of lists such that its values are instead tuples.
106+
"""
88107
assert modifier is None
89108
return [tuple(x) for x in value]
90109

91110

92111
def _hints_apply_today_relative(value, modifier):
93-
"""Generate values for today_relative hint."""
112+
"""
113+
Geneate a time value by applying a declared delta to today's date.
114+
"""
94115

95116
kind, delta = modifier.split('|')
96117
if kind == "days":
@@ -101,7 +122,42 @@ def _hints_apply_today_relative(value, modifier):
101122
raise NotImplementedError("unspported today_relative modifier")
102123

103124

104-
def _hints_apply_dict_bytes_to_strings_kv(input_dict):
125+
def _hints_apply_dict_bytes_to_strings_kv(input_dict, modifier):
126+
"""
127+
Convert a dictionary whose keys/values are bytes to one whose
128+
keys/values are strings.
129+
"""
130+
131+
assert modifier is None
132+
133+
output_dict = {}
134+
135+
for k, v in input_dict.items():
136+
key_to_use = k
137+
if isinstance(k, bytes):
138+
key_to_use = str(k, 'utf8')
139+
140+
if isinstance(v, dict):
141+
output_dict[key_to_use] = _hints_apply_dict_bytes_to_strings_kv(v, modifier)
142+
continue
143+
144+
val_to_use = v
145+
if isinstance(v, bytes):
146+
val_to_use = str(v, 'utf8')
147+
148+
output_dict[key_to_use] = val_to_use
149+
150+
return output_dict
151+
152+
153+
def _hints_apply_dict_strings_to_bytes_kv(input_dict, modifier):
154+
"""
155+
Convert a dictionary whose keys/values are strings to one whose
156+
keys/values are bytes.
157+
"""
158+
159+
assert modifier is None
160+
105161
output_dict = {}
106162

107163
for k, v in input_dict.items():
@@ -110,7 +166,7 @@ def _hints_apply_dict_bytes_to_strings_kv(input_dict):
110166
key_to_use = bytes(k, 'utf8')
111167

112168
if isinstance(v, dict):
113-
output_dict[key_to_use] = _hints_apply_dict_bytes_to_strings_kv(v)
169+
output_dict[key_to_use] = _hints_apply_dict_strings_to_bytes_kv(v, modifier)
114170
continue
115171

116172
val_to_use = v
@@ -122,26 +178,28 @@ def _hints_apply_dict_bytes_to_strings_kv(input_dict):
122178
return output_dict
123179

124180

125-
_FIXTUREFILE_APPLIERS_ATTRIBUTES = {
181+
# hints that can be aplied without an additional modifier argument
182+
_HINTS_APPLIERS_ARGLESS = {
126183
'array_of_tuples': _hints_apply_array_of_tuples,
127184
'today_relative': _hints_apply_today_relative,
185+
'convert_dict_bytes_to_strings_kv': _hints_apply_dict_bytes_to_strings_kv,
186+
'convert_dict_strings_to_bytes_kv': _hints_apply_dict_strings_to_bytes_kv,
128187
}
129188

189+
# hints applicable to the conversion of attributes during fixture loading
190+
_FIXTUREFILE_APPLIERS_ATTRIBUTES = {
191+
'array_of_tuples': _hints_apply_array_of_tuples,
192+
'today_relative': _hints_apply_today_relative,
193+
}
130194

195+
# hints applied when writing the contents of a fixture as a temporary file
131196
_FIXTUREFILE_APPLIERS_ONWRITE = {
132-
'convert_dict_bytes_to_strings_kv': _hints_apply_dict_bytes_to_strings_kv,
197+
'convert_dict_strings_to_bytes_kv': _hints_apply_dict_strings_to_bytes_kv,
133198
}
134199

135200

136-
def _hints_apply_if_present(fixture_path, json_object):
137-
"""Apply hints to the supplied data in-place if relevant."""
138-
139-
_hints_apply_from_instances_if_present(json_object)
140-
_hints_apply_from_ini_if_present(fixture_path, json_object)
141-
142-
143201
def _hints_apply_from_instances_if_present(json_object):
144-
"""Recursively aply hints to any hint instances in the supplied data."""
202+
"""Recursively apply hints to any hint instances in the supplied data."""
145203

146204
for k, v in json_object.items():
147205
if isinstance(v, dict):
@@ -153,7 +211,7 @@ def _hints_apply_from_instances_if_present(json_object):
153211
pass
154212

155213

156-
def _hints_for_fixture(fixture_path):
214+
def _load_hints_ini_for_fixture_if_present(fixture_path):
157215
"""Load any hints that may be specified for a given fixture."""
158216

159217
hints = ConfigParser()
@@ -174,10 +232,13 @@ def _hints_for_fixture(fixture_path):
174232
return hints
175233

176234

177-
def _hints_apply_from_ini_if_present(fixture_path, json_object):
178-
"""Amend the supplied object in place with any applicable hints."""
235+
def _hints_apply_from_fixture_ini_if_present(fixture_path, json_object):
236+
"""
237+
Amend the supplied object loaded from a fixture in place as specified
238+
by an optional ini file corresponding to the fixture itself.
239+
"""
179240

180-
hints = _hints_for_fixture(fixture_path)
241+
hints = _load_hints_ini_for_fixture_if_present(fixture_path)
181242

182243
# apply any attriutes hints ahead of specified conversions such that any
183244
# key can be specified matching what is visible within the loaded fixture
@@ -198,7 +259,7 @@ def _hints_apply_from_ini_if_present(fixture_path, json_object):
198259

199260

200261
class _FixtureHint:
201-
"""Named type allowing idenfication of fixture hints."""
262+
"""Named type allowing identification of fixture hints."""
202263

203264
def __init__(self, hint=None, modifier=None, value=None):
204265
self.hint = hint
@@ -225,6 +286,8 @@ def object_hook(decoded_object):
225286

226287
return decoded_object
227288

289+
# </hints>
290+
228291

229292
def fixturepath(relative_path):
230293
"""Get absolute fixture path for relative_path"""
@@ -290,7 +353,7 @@ def write_to_dir(self, target_dir, output_format=None):
290353
output_data = self.fixture_data
291354

292355
# now apply any onwrite conversions
293-
hints = _hints_for_fixture(self.fixture_path)
356+
hints = _load_hints_ini_for_fixture_if_present(self.fixture_path)
294357
for item_name in hints['ONWRITE']:
295358
if item_name not in _FIXTUREFILE_APPLIERS_ONWRITE:
296359
raise AssertionError(
@@ -300,8 +363,8 @@ def write_to_dir(self, target_dir, output_format=None):
300363
if not enabled:
301364
continue
302365

303-
apply_conversion = _FIXTUREFILE_APPLIERS_ONWRITE[item_name]
304-
output_data = apply_conversion(output_data)
366+
hint_fn = _FIXTUREFILE_APPLIERS_ONWRITE[item_name]
367+
output_data = hint_fn(output_data, None)
305368

306369
if output_format == 'binary':
307370
with open(fixture_file_target, 'wb') as fixture_outputfile:

tests/support/picklesupp.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/usr/bin/python
2+
# -*- coding: utf-8 -*-
3+
#
4+
# --- BEGIN_HEADER ---
5+
#
6+
# picklesupp - pickled file helpers for unit tests
7+
# Copyright (C) 2003-2025 The MiG Project by the Science HPC Center at UCPH
8+
#
9+
# This file is part of MiG.
10+
#
11+
# MiG is free software: you can redistribute it and/or modify
12+
# it under the terms of the GNU General Public License as published by
13+
# the Free Software Foundation; either version 2 of the License, or
14+
# (at your option) any later version.
15+
#
16+
# MiG is distributed in the hope that it will be useful,
17+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
18+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19+
# GNU General Public License for more details.
20+
#
21+
# You should have received a copy of the GNU General Public License
22+
# along with this program; if not, write to the Free Software
23+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
24+
#
25+
# -- END_HEADER ---
26+
#
27+
28+
"""Pickle related details within the test support library."""
29+
30+
import pickle
31+
32+
from tests.support.suppconst import TEST_OUTPUT_DIR
33+
from tests.support.fixturesupp import _HINTS_APPLIERS_ARGLESS
34+
35+
36+
class PickleAssertMixin:
37+
"""Assertions for working with pickled files to be used as a mixin."""
38+
39+
def assertPickledFile(self, pickle_file_path, apply_hints=None):
40+
"""
41+
Check a particular pickled file exists and is loadable.
42+
43+
Any data contained within it is returned for further assertions
44+
having been optionally transformed as requested by hints.
45+
"""
46+
47+
with open(pickle_file_path, 'rb') as picklefile:
48+
pickled = pickle.load(picklefile)
49+
50+
if not apply_hints:
51+
return pickled
52+
53+
result = pickled
54+
for hint_name in apply_hints:
55+
if not hint_name in _HINTS_APPLIERS_ARGLESS:
56+
raise NotImplementedError("unknown hint %s" % (hint_name,))
57+
hint_fn = _HINTS_APPLIERS_ARGLESS[hint_name]
58+
result = hint_fn(pickled, modifier=None)
59+
return result

0 commit comments

Comments
 (0)