Skip to content

Commit 9705d69

Browse files
author
Vasileios Karakasis
committed
Fix and expand JSON report
1 parent 3f5bb99 commit 9705d69

File tree

8 files changed

+260
-189
lines changed

8 files changed

+260
-189
lines changed

reframe/core/pipeline.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -925,6 +925,10 @@ def stderr(self):
925925
'''
926926
return self._job.stderr
927927

928+
@property
929+
def build_job(self):
930+
return self._build_job
931+
928932
@property
929933
@sn.sanity_function
930934
def build_stdout(self):

reframe/frontend/cli.py

Lines changed: 75 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@
33
#
44
# SPDX-License-Identifier: BSD-3-Clause
55

6-
import datetime
76
import inspect
87
import json
98
import os
109
import re
1110
import socket
1211
import sys
12+
import time
1313
import traceback
14-
from pathlib import Path
1514

1615
import reframe
1716
import reframe.core.config as config
@@ -32,7 +31,6 @@
3231
AsynchronousExecutionPolicy)
3332
from reframe.frontend.loader import RegressionCheckLoader
3433
from reframe.frontend.printer import PrettyPrinter
35-
from reframe.utility import get_next_runreport_index
3634

3735

3836
def format_check(check, detailed):
@@ -72,6 +70,22 @@ def list_checks(checks, printer, detailed=False):
7270
printer.info('\nFound %d check(s).' % len(checks))
7371

7472

73+
def generate_report_filename(filepatt):
74+
if not '{sessionid}' in filepatt:
75+
return filepatt
76+
77+
search_patt = os.path.basename(filepatt).replace('{sessionid}', r'(\d+)')
78+
new_id = -1
79+
for filename in os.listdir(os.path.dirname(filepatt)):
80+
match = re.match(search_patt, filename)
81+
if match:
82+
found_id = int(match.group(1))
83+
new_id = max(found_id, new_id)
84+
85+
new_id += 1
86+
return filepatt.format(sessionid=new_id)
87+
88+
7589
def main():
7690
# Setup command line options
7791
argparser = argparse.ArgumentParser()
@@ -140,8 +154,10 @@ def main():
140154
envvar='RFM_SAVE_LOG_FILES', configvar='general/save_log_files'
141155
)
142156
output_options.add_argument(
143-
'--runreport-name', action='store', metavar='NAME',
144-
help="Do not overwrite runreport.json",
157+
'--report-file', action='store', metavar='FILE',
158+
help="Store JSON run report in FILE",
159+
envvar='RFM_REPORT_FILE',
160+
configvar='general/report_file'
145161
)
146162

147163
# Check discovery options
@@ -516,32 +532,33 @@ def print_infoline(param, value):
516532
param = param + ':'
517533
printer.info(f" {param.ljust(18)} {value}")
518534

519-
reframe_info = {
535+
session_info = {
536+
'cmdline': ' '.join(sys.argv),
537+
'config_file': rt.site_config.filename,
538+
'data_version': '1.0',
539+
'hostname': socket.gethostname(),
540+
'prefix_output': rt.output_prefix,
541+
'prefix_stage': rt.stage_prefix,
542+
'user': os_ext.osuser(),
520543
'version': os_ext.reframe_version(),
521-
'command': repr(' '.join(sys.argv)),
522-
'user': f"{os_ext.osuser() or '<unknown>'}",
523-
'host': socket.gethostname(),
524-
'working_directory': repr(os.getcwd()),
525-
'check_search_path': f"{':'.join(loader.load_path)!r}",
526-
'recursive_search_path': loader.recurse,
527-
'settings_file': site_config.filename,
528-
'stage_prefix': repr(rt.stage_prefix),
529-
'output_prefix': repr(rt.output_prefix),
544+
'workdir': os.getcwd(),
530545
}
531546

532547
# Print command line
533548
printer.info(f"[ReFrame Setup]")
534-
print_infoline('version', reframe_info['version'])
535-
print_infoline('command', reframe_info['command'])
536-
print_infoline('launched by',
537-
f"{reframe_info['user']}@{reframe_info['host']}")
538-
print_infoline('working directory', reframe_info['working_directory'])
539-
print_infoline('settings file', f"{reframe_info['settings_file']!r}")
549+
print_infoline('version', session_info['version'])
550+
print_infoline('command', session_info['cmdline'])
551+
print_infoline(
552+
f"launched by",
553+
f"{session_info['user'] or '<unknown>'}@{session_info['hostname']}"
554+
)
555+
print_infoline('working directory', repr(session_info['workdir']))
556+
print_infoline('settings file', f"{session_info['config_file']!r}")
540557
print_infoline('check search path',
541-
f"{'(R) ' if reframe_info['recursive_search_path'] else ''}"
542-
f"{reframe_info['check_search_path']!r}")
543-
print_infoline('stage directory', reframe_info['stage_prefix'])
544-
print_infoline('output directory', reframe_info['output_prefix'])
558+
f"{'(R) ' if loader.recurse else ''}"
559+
f"{':'.join(loader.load_path)!r}")
560+
print_infoline('stage directory', repr(session_info['prefix_stage']))
561+
print_infoline('output directory', repr(session_info['prefix_output']))
545562
printer.info('')
546563
try:
547564
# Locate and load checks
@@ -716,13 +733,19 @@ def print_infoline(param, value):
716733
max_retries) from None
717734
runner = Runner(exec_policy, printer, max_retries)
718735
try:
719-
reframe_info['start_time'] = (
720-
datetime.datetime.today().strftime('%c %Z'))
736+
time_start = time.time()
737+
session_info['time_start'] = time.strftime(
738+
'%FT%T%z', time.localtime(time_start),
739+
)
721740
runner.runall(testcases)
722741
finally:
742+
time_end = time.time()
743+
session_info['time_end'] = time.strftime(
744+
'%FT%T%z', time.localtime(time_end)
745+
)
746+
session_info['time_elapsed'] = time_end - time_start
747+
723748
# Print a retry report if we did any retries
724-
reframe_info['end_time'] = (
725-
datetime.datetime.today().strftime('%c %Z'))
726749
if runner.stats.failures(run=0):
727750
printer.info(runner.stats.retry_report())
728751

@@ -736,23 +759,30 @@ def print_infoline(param, value):
736759
if options.performance_report:
737760
printer.info(runner.stats.performance_report())
738761

739-
if options.runreport_name:
740-
runreport_file = options.runreport_name
741-
else:
742-
runreport_dir = os.path.join(Path.home(), '.local/reframe')
743-
runreport_id = get_next_runreport_index(runreport_dir)
744-
runreport_name = f'runreport-{runreport_id}.json'
745-
Path(runreport_dir).mkdir(parents=True, exist_ok=True)
746-
runreport_file = os.path.join(runreport_dir,
747-
runreport_name)
748-
762+
# Generate the report for this session
763+
report_file = os.path.normpath(
764+
os_ext.expandvars(rt.get_option('general/0/report_file'))
765+
)
766+
os.makedirs(os.path.dirname(report_file), exist_ok=True)
767+
768+
# Build final report JSON
769+
run_stats = runner.stats.json()
770+
session_info.update({
771+
'num_cases': run_stats[0]['num_cases'],
772+
'num_failures': run_stats[-1]['num_failures']
773+
})
774+
json_report = {
775+
'session_info': session_info,
776+
'runs': run_stats
777+
}
778+
report_file = generate_report_filename(report_file)
749779
try:
750-
with open(runreport_file, 'w') as fp:
751-
json.dump(runner.stats.json(reframe_info, force=True),
752-
fp, indent=4)
753-
except OSError:
754-
printer.error(f'invalid path: {runreport_file}')
755-
sys.exit(1)
780+
with open(report_file, 'w') as fp:
781+
json.dump(json_report, fp, indent=2)
782+
except OSError as e:
783+
printer.warning(
784+
f'failed to generate report in {report_file!r}: {e}'
785+
)
756786

757787
else:
758788
printer.error("No action specified. Please specify `-l'/`-L' for "

reframe/frontend/statistics.py

Lines changed: 73 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ class TestStats:
1313
def __init__(self):
1414
# Tasks per run stored as follows: [[run0_tasks], [run1_tasks], ...]
1515
self._alltasks = [[]]
16-
self._records = []
16+
17+
# Data collected for all the runs of this session in JSON format
18+
self._run_data = []
1719

1820
def add_task(self, task):
1921
current_run = rt.runtime().current_run
@@ -44,7 +46,6 @@ def retry_report(self):
4446
report.append('SUMMARY OF RETRIES')
4547
report.append(line_width * '-')
4648
messages = {}
47-
4849
for run in range(1, len(self._alltasks)):
4950
for t in self.tasks(run):
5051
partition_name = ''
@@ -67,42 +68,41 @@ def retry_report(self):
6768

6869
return '\n'.join(report)
6970

70-
def json(self, reframe_info=None, force=False):
71-
if not force and self._records:
72-
return self._records
71+
def json(self, force=False):
72+
if not force and self._run_data:
73+
return self._run_data
7374

74-
self._records = {'run_info': []}
75-
current_run = rt.runtime().current_run
76-
for run_no, run in enumerate(self._alltasks):
77-
tests = []
75+
for runid, run in enumerate(self._alltasks):
76+
testcases = []
77+
num_failures = 0
7878
for t in run:
7979
check = t.check
8080
partition = check.current_partition
8181
entry = {
82-
'testname': check.name,
82+
'build_stderr': None,
83+
'build_stdout': None,
8384
'description': check.descr,
84-
'system': check.current_system.name,
8585
'environment': None,
86-
'tags': list(check.tags),
87-
'maintainers': check.maintainers,
88-
'scheduler': None,
86+
'fail_reason': None,
87+
'fail_phase': None,
8988
'jobid': None,
90-
'nodelist': [],
91-
'job_stdout': None,
9289
'job_stderr': None,
93-
'build_stdout': None,
94-
'build_stderr': None,
95-
'failing_reason': None,
96-
'failing_phase': None,
90+
'job_stdout': None,
91+
'name': check.name,
92+
'maintainers': check.maintainers,
93+
'nodelist': [],
9794
'outputdir': None,
95+
'perfvars': None,
96+
'result': None,
9897
'stagedir': None,
99-
'job_stdout': None,
100-
'job_stderr': None,
101-
'time_setup': t.duration('setup'),
102-
'time_compile': t.duration('complete_complete'),
98+
'scheduler': None,
99+
'system': check.current_system.name,
100+
'tags': list(check.tags),
101+
'time_compile': t.duration('compile_complete'),
102+
'time_performance': t.duration('performance'),
103103
'time_run': t.duration('run_complete'),
104104
'time_sanity': t.duration('sanity'),
105-
'time_performance': t.duration('performance'),
105+
'time_setup': t.duration('setup'),
106106
'time_total': t.duration('total')
107107
}
108108
partition = check.current_partition
@@ -116,47 +116,66 @@ def json(self, reframe_info=None, force=False):
116116

117117
if check.job:
118118
entry['jobid'] = check.job.jobid
119-
entry['nodelist'] = check.job.nodelist or []
120-
entry['job_stdout'] = check.stdout.evaluate()
121119
entry['job_stderr'] = check.stderr.evaluate()
120+
entry['job_stdout'] = check.stdout.evaluate()
121+
entry['nodelist'] = check.job.nodelist or []
122122

123-
if check._build_job:
124-
entry['build_stdout'] = check.build_stdout.evaluate()
123+
if check.build_job:
125124
entry['build_stderr'] = check.build_stderr.evaluate()
125+
entry['build_stdout'] = check.build_stdout.evaluate()
126126

127127
if t.failed:
128-
entry['result'] = 'fail'
128+
num_failures += 1
129+
entry['result'] = 'failure'
130+
entry['stagedir'] = check.stagedir
131+
entry['fail_phase'] = t.failed_stage
129132
if t.exc_info is not None:
130-
entry['failing_reason'] = format_exception(
131-
*t.exc_info)
132-
entry['failing_phase'] = t.failed_stage
133-
entry['stagedir'] = check.stagedir
133+
entry['fail_reason'] = format_exception(*t.exc_info)
134134
else:
135135
entry['result'] = 'success'
136136
entry['outputdir'] = check.outputdir
137137

138-
tests.append(entry)
139-
140-
self._records['run_info'].append({'runid': run_no, 'tests': tests})
141-
142-
if reframe_info:
143-
self._records['reframe_info'] = reframe_info
144-
145-
return self._records
138+
if check.perf_patterns:
139+
# Record performance variables
140+
entry['perfvars'] = []
141+
for key, ref in check.perfvalues.items():
142+
var = key.split(':')[-1]
143+
val, ref, lower, upper, unit = ref
144+
entry['perfvars'].append({
145+
'name': var,
146+
'reference': ref,
147+
'thres_lower': lower,
148+
'thres_upper': upper,
149+
'unit': unit,
150+
'value': val
151+
})
152+
153+
testcases.append(entry)
154+
155+
self._run_data.append({
156+
'num_cases': len(run),
157+
'num_failures': num_failures,
158+
'runid': runid,
159+
'testcases': testcases
160+
})
161+
162+
return self._run_data
146163

147164
def failure_report(self):
148165
line_width = 78
149166
report = [line_width * '=']
150167
report.append('SUMMARY OF FAILURES')
151-
last_run = rt.runtime().current_run
152-
for r in self.json()['run_info'][last_run]['tests']:
168+
run_report = self.json()[-1]
169+
last_run = run_report['runid']
170+
for r in run_report['testcases']:
153171
if r['result'] == 'success':
154172
continue
155173

156-
retry_info = (f'(for the last of {last_run} retries)'
157-
if last_run > 0 else '')
174+
retry_info = (
175+
f'(for the last of {last_run} retries)' if last_run > 0 else ''
176+
)
158177
report.append(line_width * '-')
159-
report.append(f"FAILURE INFO for {r['testname']} {retry_info}")
178+
report.append(f"FAILURE INFO for {r['name']} {retry_info}")
160179
report.append(f" * Test Description: {r['description']}")
161180
report.append(f" * System partition: {r['system']}")
162181
report.append(f" * Environment: {r['environment']}")
@@ -167,13 +186,13 @@ def failure_report(self):
167186
jobid = r['jobid']
168187
report.append(f" * Job type: {job_type} (id={r['jobid']})")
169188
report.append(f" * Maintainers: {r['maintainers']}")
170-
report.append(f" * Failing phase: {r['failing_phase']}")
171-
report.append(f" * Rerun with '-n {r['testname']}"
189+
report.append(f" * Failing phase: {r['fail_phase']}")
190+
report.append(f" * Rerun with '-n {r['name']}"
172191
f" -p {r['environment']} --system {r['system']}'")
173-
report.append(f" * Reason: {r['failing_reason']}")
174-
if r['failing_phase'] == 'sanity':
192+
report.append(f" * Reason: {r['fail_reason']}")
193+
if r['fail_phase'] == 'sanity':
175194
report.append('Sanity check failure')
176-
elif r['failing_phase'] == 'performance':
195+
elif r['fail_phase'] == 'performance':
177196
report.append('Performance check failure')
178197
else:
179198
# This shouldn't happen...
@@ -227,6 +246,8 @@ def failure_stats(self):
227246
return ''
228247

229248
def performance_report(self):
249+
# FIXME: Adapt this function to use the JSON report
250+
230251
line_width = 78
231252
report_start = line_width * '='
232253
report_title = 'PERFORMANCE REPORT'

0 commit comments

Comments
 (0)