Skip to content

Commit 92800e3

Browse files
author
Theofilos Manitaras
authored
Merge branch 'develop' into feat/custom-config-imports
2 parents d2008d2 + 8000b58 commit 92800e3

File tree

8 files changed

+102
-38
lines changed

8 files changed

+102
-38
lines changed

docs/config_reference.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2118,6 +2118,14 @@ General Configuration
21182118
- ``pretty``: (default) Generate a pretty table
21192119

21202120

2121+
.. py:attribute:: general.table_format_delim
2122+
2123+
:required: No
2124+
:default: ``","``
2125+
2126+
Delimiter to use when emitting tables in CSV format.
2127+
2128+
21212129
.. py:attribute:: general.timestamp_dirs
21222130
21232131
:required: No

docs/manpage.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1204,6 +1204,14 @@ Miscellaneous options
12041204

12051205
.. versionadded:: 4.7
12061206

1207+
.. option:: --table-format-delim[=DELIM]
1208+
1209+
Delimiter to use when emitting tables in CSV format using the :option:`--table-format=csv` option.
1210+
1211+
The default delimiter is ``,``.
1212+
1213+
.. versionadded:: 4.9
1214+
12071215
.. option:: --upgrade-config-file=OLD[:NEW]
12081216

12091217
Convert the old-style configuration file ``OLD``, place it into the new file ``NEW`` and exit.
@@ -2316,6 +2324,21 @@ Whenever an environment variable is associated with a configuration option, its
23162324

23172325
.. versionadded:: 4.7
23182326

2327+
.. envvar:: RFM_TABLE_FORMAT_DELIM
2328+
2329+
Delimiter for CSV tables.
2330+
2331+
2332+
.. table::
2333+
:align: left
2334+
2335+
================================== ==================
2336+
Associated command line option :option:`--table-format-delim`
2337+
Associated configuration parameter :attr:`~config.general.table_format_delim`
2338+
================================== ==================
2339+
2340+
.. versionadded:: 4.9
2341+
23192342

23202343
.. envvar:: RFM_TIMESTAMP_DIRS
23212344

reframe/frontend/cli.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,11 @@ def main():
697697
help='Table formatting',
698698
envvar='RFM_TABLE_FORMAT', configvar='general/table_format'
699699
)
700+
misc_options.add_argument(
701+
'--table-format-delim', action='store',
702+
help='The delimiter to use when using `--table-format=csv`',
703+
envvar='RFM_TABLE_FORMAT_DELIM', configvar='general/table_format_delim'
704+
)
700705
misc_options.add_argument(
701706
'-v', '--verbose', action='count',
702707
help='Increase verbosity level of output',
@@ -1083,9 +1088,7 @@ def restrict_logging():
10831088
lambda htype: htype != 'stream')
10841089
with exit_gracefully_on_error('failed to retrieve session data',
10851090
printer):
1086-
printer.info(jsonext.dumps(reporting.session_info(
1087-
options.describe_stored_sessions
1088-
), indent=2))
1091+
printer.info(reporting.session_info(options.describe_stored_sessions))
10891092
sys.exit(0)
10901093

10911094
if options.describe_stored_testcases:

reframe/frontend/printer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,8 +277,9 @@ def performance_report(self, data, **kwargs):
277277
self.info('')
278278

279279
def _table_as_csv(self, data):
280+
delim = rt.runtime().get_option('general/0/table_format_delim')
280281
for line in data:
281-
self.info(','.join(str(x) for x in line))
282+
self.info(delim.join(str(x) for x in line))
282283

283284
def table(self, data, **kwargs):
284285
'''Print tabular data'''

reframe/frontend/reporting/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -860,8 +860,10 @@ def testcase_data(spec, namepatt=None, test_filter=None):
860860
@time_function
861861
def session_info(query):
862862
'''Retrieve session details as JSON'''
863-
864-
return StorageBackend.default().fetch_sessions(parse_query_spec(query))
863+
sessions = StorageBackend.default().fetch_sessions(
864+
parse_query_spec(query), False
865+
)
866+
return rf'[{",".join(sessions)}]'
865867

866868

867869
@time_function

reframe/frontend/reporting/storage.py

Lines changed: 54 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,14 @@ def fetch_testcases(self, selector: QuerySelector, name_patt=None,
5454
'''
5555

5656
@abc.abstractmethod
57-
def fetch_sessions(self, selector: QuerySelector):
57+
def fetch_sessions(self, selector: QuerySelector, decode=True):
5858
'''Fetch sessions based on the specified query selector.
5959
6060
:arg selector: an instance of :class:`QuerySelector` that will specify
6161
the actual type of query requested.
62-
:returns: A list of matching sessions.
62+
:arg decode: If set to :obj:`False`, do not decode the returned
63+
sessions and leave them JSON-encoded.
64+
:returns: A list of matching sessions, either decoded or not.
6365
'''
6466

6567
@abc.abstractmethod
@@ -101,6 +103,7 @@ def _db_file(self):
101103

102104
self._db_create()
103105

106+
self._db_create_indexes()
104107
self._db_schema_check()
105108
return self.__db_file
106109

@@ -161,12 +164,20 @@ def _db_create(self):
161164
'uuid TEXT, '
162165
'FOREIGN KEY(session_uuid) '
163166
'REFERENCES sessions(uuid) ON DELETE CASCADE)')
167+
168+
# Update DB file mode
169+
os.chmod(self.__db_file, self.__db_file_mode)
170+
171+
def _db_create_indexes(self):
172+
clsname = type(self).__name__
173+
getlogger().debug(f'{clsname}: creating database indexes if needed')
174+
with self._db_connect(self.__db_file) as conn:
164175
conn.execute('CREATE INDEX IF NOT EXISTS index_testcases_time '
165176
'on testcases(job_completion_time_unix)')
166177
conn.execute('CREATE TABLE IF NOT EXISTS metadata('
167178
'schema_version TEXT)')
168-
# Update DB file mode
169-
os.chmod(self.__db_file, self.__db_file_mode)
179+
conn.execute('CREATE INDEX IF NOT EXISTS index_sessions_time '
180+
'on sessions(session_start_unix)')
170181

171182
def _db_schema_check(self):
172183
with self._db_read(self.__db_file) as conn:
@@ -232,10 +243,16 @@ def store(self, report, report_file=None):
232243
return self._db_store_report(conn, report, report_file)
233244

234245
@time_function
235-
def _decode_sessions(self, results, sess_filter):
236-
'''Decode sessions from the raw DB results.
246+
def _mass_json_decode(self, *json_objs):
247+
data = rf'[{",".join(json_objs)}]'
248+
getlogger().debug(f'decoding JSON raw data of length {len(data)}')
249+
return json.loads(data)
237250

238-
Return a map of session uuids to decoded session data
251+
@time_function
252+
def _fetch_sessions(self, results, sess_filter):
253+
'''Fetch JSON-encoded sessions from the DB by applying a filter.
254+
255+
:returns: A list of the JSON-encoded valid sessions.
239256
'''
240257
sess_info_patt = re.compile(
241258
r'\"session_info\":\s+(?P<sess_info>\{.*?\})'
@@ -245,34 +262,31 @@ def _decode_sessions(self, results, sess_filter):
245262
def _extract_sess_info(s):
246263
return sess_info_patt.search(s).group('sess_info')
247264

248-
@time_function
249-
def _mass_json_decode(json_objs):
250-
data = '[' + ','.join(json_objs) + ']'
251-
getlogger().debug(
252-
f'decoding JSON raw data of length {len(data)}'
253-
)
254-
return json.loads(data)
255-
256265
session_infos = {}
257266
sessions = {}
258267
for uuid, json_blob in results:
259268
sessions.setdefault(uuid, json_blob)
260269
session_infos.setdefault(uuid, _extract_sess_info(json_blob))
261270

262-
# Find the UUIDs to decode fully by inspecting only the session info
271+
# Find the relevant sessions by inspecting only the session info
263272
uuids = []
264-
for sess_info in _mass_json_decode(session_infos.values()):
273+
infos = self._mass_json_decode(*session_infos.values())
274+
for sess_info in infos:
265275
try:
266276
if self._db_filter_json(sess_filter, sess_info):
267277
uuids.append(sess_info['uuid'])
268278
except Exception:
269279
continue
270280

271-
# Decode selected sessions
272-
reports = _mass_json_decode(sessions[uuid] for uuid in uuids)
281+
return [sessions[uuid] for uuid in uuids]
273282

274-
# Return only the selected sessions
275-
return {rpt['session_info']['uuid']: rpt for rpt in reports}
283+
def _decode_and_index_sessions(self, json_blobs):
284+
'''Decode the sessions and index them by their uuid.
285+
286+
:returns: A dictionary with uuids as keys and the sessions as values.
287+
'''
288+
return {sess['session_info']['uuid']: sess
289+
for sess in self._mass_json_decode(*json_blobs)}
276290

277291
@time_function
278292
def _fetch_testcases_raw(self, condition):
@@ -289,7 +303,11 @@ def _fetch_testcases_raw(self, condition):
289303
results = conn.execute(query).fetchall()
290304

291305
getprofiler().exit_region()
292-
sessions = self._decode_sessions(results, None)
306+
307+
# Fetch, decode and index the sessions by their uuid
308+
sessions = self._decode_and_index_sessions(
309+
self._fetch_sessions(results, None)
310+
)
293311

294312
# Extract the test case data by extracting their UUIDs
295313
getprofiler().enter_region('sqlite testcase query')
@@ -319,8 +337,8 @@ def _fetch_testcases_raw(self, condition):
319337
return testcases
320338

321339
@time_function
322-
def _fetch_testcases_from_session(self, selector,
323-
name_patt=None, test_filter=None):
340+
def _fetch_testcases_from_session(self, selector, name_patt=None,
341+
test_filter=None):
324342
query = 'SELECT uuid, json_blob from sessions'
325343
if selector.by_session_uuid():
326344
query += f' WHERE uuid == "{selector.uuid}"'
@@ -338,9 +356,11 @@ def _fetch_testcases_from_session(self, selector,
338356
if not results:
339357
return []
340358

341-
sessions = self._decode_sessions(
342-
results,
343-
selector.sess_filter if selector.by_session_filter() else None
359+
sessions = self._decode_and_index_sessions(
360+
self._fetch_sessions(
361+
results,
362+
selector.sess_filter if selector.by_session_filter() else None
363+
)
344364
)
345365
return [tc for sess in sessions.values()
346366
for run in sess['runs'] for tc in run['testcases']
@@ -366,15 +386,15 @@ def fetch_testcases(self, selector: QuerySelector,
366386
name_patt=None, test_filter=None):
367387
if selector.by_session():
368388
return self._fetch_testcases_from_session(
369-
selector, name_patt, test_filter
389+
selector, name_patt, test_filter,
370390
)
371391
else:
372392
return self._fetch_testcases_time_period(
373393
*selector.time_period, name_patt, test_filter
374394
)
375395

376396
@time_function
377-
def fetch_sessions(self, selector: QuerySelector):
397+
def fetch_sessions(self, selector: QuerySelector, decode=True):
378398
query = 'SELECT uuid, json_blob FROM sessions'
379399
if selector.by_time_period():
380400
ts_start, ts_end = selector.time_period
@@ -389,11 +409,14 @@ def fetch_sessions(self, selector: QuerySelector):
389409
results = conn.execute(query).fetchall()
390410

391411
getprofiler().exit_region()
392-
session = self._decode_sessions(
412+
raw_sessions = self._fetch_sessions(
393413
results,
394414
selector.sess_filter if selector.by_session_filter() else None
395415
)
396-
return [*session.values()]
416+
if decode:
417+
return [*self._decode_and_index_sessions(raw_sessions).values()]
418+
else:
419+
return raw_sessions
397420

398421
def _do_remove(self, conn, uuids):
399422
'''Remove sessions'''

reframe/frontend/reporting/utility.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,9 @@ def by_session_filter(self):
200200

201201
def __repr__(self):
202202
clsname = type(self).__name__
203-
return f'{clsname}(value={self.__value}, kind={self.__kind})'
203+
return (f'{clsname}(uuid={self.__uuid!r}, '
204+
f'time_period={self.__time_period!r}, '
205+
f'sess_filter={self.__sess_filter!r})')
204206

205207

206208
def parse_time_period(s):

reframe/schemas/config.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,7 @@
538538
"save_log_files": {"type": "boolean"},
539539
"target_systems": {"$ref": "#/defs/system_ref"},
540540
"table_format": {"enum": ["csv", "plain", "pretty"]},
541+
"table_format_delim": {"type": "string"},
541542
"timestamp_dirs": {"type": "string"},
542543
"topology_prefix": {"type": "string"},
543544
"trap_job_errors": {"type": "boolean"},
@@ -609,6 +610,7 @@
609610
"general/resolve_module_conflicts": true,
610611
"general/save_log_files": false,
611612
"general/table_format": "pretty",
613+
"general/table_format_delim": ",",
612614
"general/target_systems": ["*"],
613615
"general/topology_prefix": "${HOME}/.reframe/topology",
614616
"general/timestamp_dirs": "%Y%m%dT%H%M%S%z",

0 commit comments

Comments
 (0)