Skip to content

Commit dac41a4

Browse files
committed
test: introduce the new runner based on pytest (./test.py)
1 parent 4588240 commit dac41a4

12 files changed

Lines changed: 864 additions & 0 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ test/**/*.pyc
4141
test/**/*.dat
4242
test/**/cmake_packages*
4343
test/**/
44+
idefix-tests.junit.xml
4445

4546
# machine specific cache and hidden files
4647
.*

pytest.ini

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[pytest]
2+
markers=
3+
default: Test to run by default.
4+
python_files=./test.py
5+
junit_logging="system-out"
6+
junit_log_passing_tests=false

pytools/idfx_test_gen.py

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
#####################################################################################
2+
# Idefix MHD astrophysical code
3+
# Copyright(C) Sébastien Valat <sebastien.valat@univ-grenoble-alpes.fr>
4+
# and other code contributors
5+
# Licensed under CeCILL 2.1 License, see COPYING for more information
6+
#####################################################################################
7+
8+
import copy
9+
import pytest
10+
11+
DO_NOT_LOOP_ON = ['restart_no_overwrite', "dec", "multirun"]
12+
13+
class IdefixDirTestGenerator:
14+
'''
15+
Class used to generate the various configuration to run by parsing the
16+
files `testme.py` found in the hierarchy of the /test directory of Idefix.
17+
'''
18+
19+
def __init__(self, currentTestFile: str, name: str = ""):
20+
'''
21+
Constructor or the class.
22+
23+
Args:
24+
currentTestFile (str):
25+
Path the the current python file. Normally you
26+
simply pass __file__ to this parameter.
27+
name (str):
28+
Define a name for the test, can be empty.
29+
'''
30+
31+
self.currentTestFile = currentTestFile
32+
self.currentTestName = name
33+
34+
# generate the list of configs to run
35+
def genTestConfigs(self, names:str, params, whenClauses = {}) -> list:
36+
'''
37+
Generate the the list of configurations as pytest parameters.
38+
It will unpack the configuration set by looping on all combinations defined
39+
by the given sets.
40+
41+
Args:
42+
names (str):
43+
Comma separated list of variables do consider to build the
44+
name of the file.
45+
params (dict|list):
46+
A configuration set as a dictionnary or a list of
47+
configuration set.
48+
whenCaluses (dict|list):
49+
Provide a set of clauses to apply after unpacking
50+
the configuration so we can patch some values depending on some others.
51+
52+
Returns:
53+
A list of pytest.param() ready to be fiven to parametrized pytest functions.
54+
'''
55+
# get name ordering list
56+
nameList = names.split(',')
57+
58+
# gen list of complete configs
59+
all_configs = []
60+
if isinstance(params, dict):
61+
all_configs += self._genOneConfigSeries(names, params)
62+
elif isinstance(params, list):
63+
for p in params:
64+
all_configs += self._genOneConfigSeries(names, p)
65+
else:
66+
raise Exception("Should never be called !")
67+
68+
# convert as parametrize with nice name
69+
result = []
70+
for config in all_configs:
71+
# append the file
72+
config['testfile'] = self.currentTestFile
73+
config['testname'] = self.currentTestName
74+
# gen name
75+
nameParts = [self.currentTestName]
76+
for name in nameList:
77+
if isinstance(config[name], bool):
78+
if config[name]:
79+
nameParts.append(name)
80+
elif isinstance(config[name], str):
81+
nameParts.append(str(config[name]))
82+
else:
83+
nameParts.append(f"{name}-{config[name]}")
84+
confName = "-".join(nameParts)
85+
86+
# apply when clause
87+
config = self._applyWhen(config, whenClauses)
88+
89+
result.append(pytest.param(config, id=confName))
90+
91+
# ok
92+
return result
93+
94+
def extractNamingParameters(self, params) -> list:
95+
'''
96+
Loop on the parameters and check automatically what are the list of variable parameters.
97+
98+
Args:
99+
params (list|dict):
100+
The list of configuration sets to scan or a single set.
101+
102+
Returns:
103+
The list of names of the variable parameters.
104+
'''
105+
106+
# if not a list make a list
107+
if not isinstance(params, list):
108+
params = [params]
109+
110+
# see params
111+
seen = {}
112+
variables = []
113+
114+
# loop
115+
for param_set in params:
116+
for key, value in param_set.items():
117+
if key in variables:
118+
pass
119+
elif key in DO_NOT_LOOP_ON:
120+
pass
121+
elif isinstance(value, list):
122+
variables.append(key)
123+
elif key in seen and seen[key] != value:
124+
variables.append(key)
125+
elif key not in seen:
126+
seen[key] = value
127+
128+
# by default sort by alphabetic order about var names
129+
# TODO make something better by assigning a priority to vars
130+
variables.sort()
131+
132+
# ok
133+
return ','.join(variables)
134+
135+
def _genNextLevelCombinations(self, input: list, paramName: str, paramValues: list) -> list:
136+
'''
137+
Take an input case list and unpack the given parameter to build the new combinations.
138+
139+
Args:
140+
input (list):
141+
The incoming list of combinations already unpacked before this call.
142+
paramName (str):
143+
Name of the parameter to unpack.
144+
paramValues (list):
145+
The list of values to unpack and to build combinations for.
146+
147+
Returns:
148+
The updated list of run sets.
149+
'''
150+
result = []
151+
for entry in input:
152+
for value in paramValues:
153+
v = copy.deepcopy(entry)
154+
v[paramName] = value
155+
result.append(v)
156+
return result
157+
158+
def _genOneConfigSeries(self, names: str, config: dict) -> list:
159+
'''
160+
Generate the the list of configurations as pytest parameters.
161+
It will unpack the configuration set by looping on all combinations defined
162+
by the given sets.
163+
164+
Args:
165+
names (str):
166+
Comma separated list of variables do consider to build the
167+
name of the file.
168+
config (dict):
169+
A configuration set as a dictionnary.
170+
171+
Returns:
172+
A list of pytest.param() ready to be fiven to parametrized pytest functions.
173+
'''
174+
# get name ordering list
175+
nameList = names.split(',')
176+
177+
# if there is ini in the list we put it at the end
178+
loopOrder = nameList.copy()
179+
if 'ini' in loopOrder:
180+
loopOrder.remove('ini')
181+
loopOrder.append('ini')
182+
183+
# init core with everything not a list
184+
core = {}
185+
for key, value in config.items():
186+
if isinstance(value, list) and key not in DO_NOT_LOOP_ON:
187+
assert key in nameList, f"All variable parameteres should be ordered in the names list, '{key}' is not."
188+
else:
189+
core[key] = copy.deepcopy(value)
190+
191+
# at start we have only default core
192+
result = [core]
193+
194+
# loop
195+
for key in loopOrder:
196+
value = config[key]
197+
assert isinstance(value, list), f"This parameter is marked as a list but is not a list : {key}={value} !"
198+
result = self._genNextLevelCombinations(result, key, value)
199+
200+
# ok
201+
return result
202+
203+
def _matchWhenClause(self, config: dict, when_clause: dict) -> bool:
204+
'''
205+
Check the matching of a given when clause on the given configuration.
206+
207+
Args:
208+
config (dict): The configuration.
209+
when_clause (dict): The when clause to check.
210+
211+
Returns:
212+
True if the clause, match, False otherwise.
213+
'''
214+
for key, value in when_clause.items():
215+
if config[key] != value:
216+
return False
217+
return True
218+
219+
def _applyWhen(self, config: dict, when: dict) -> dict:
220+
'''
221+
Check if the when clause applies and apply if if any.
222+
223+
Args:
224+
config (dict): The configuration.
225+
when_clause (dict): The when clause to check and apply.
226+
227+
Returns:
228+
The fixed config.
229+
'''
230+
# nothing to do
231+
if when == {}:
232+
return config
233+
234+
# clone
235+
result = copy.deepcopy(config)
236+
237+
# loop on when
238+
if isinstance(when, list):
239+
for clause in when:
240+
if self._matchWhenClause(config, clause['conditions']):
241+
result.update(clause['apply'])
242+
elif isinstance(when, dict):
243+
if self._matchWhenClause(config, when['conditions']):
244+
result.update(when['apply'])
245+
else:
246+
raise Exception(f"Invalid type for 'when' : {when}")
247+
248+
# ok
249+
return result

0 commit comments

Comments
 (0)