Skip to content

Commit b0962f5

Browse files
committed
Write out useful data from suite for Omega PRs
1 parent 962f202 commit b0962f5

File tree

1 file changed

+188
-2
lines changed

1 file changed

+188
-2
lines changed

polaris/run/serial.py

Lines changed: 188 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import sys
66
import time
77
from datetime import timedelta
8+
from typing import Dict, List, Optional
89

910
import mpas_tools.io
1011
from mpas_tools.logging import LoggingContext, check_call
@@ -89,6 +90,9 @@ def run_tasks(
8990
suite_start = time.time()
9091
task_times = dict()
9192
result_strs = dict()
93+
total_tasks = len(suite['tasks'])
94+
exec_fail_tasks: List[str] = []
95+
diff_fail_tasks: List[str] = []
9296
for task_name in suite['tasks']:
9397
stdout_logger.info(f'{task_name}')
9498

@@ -102,7 +106,13 @@ def run_tasks(
102106
log_filename = f'{cwd}/case_outputs/{task_prefix}.log'
103107
task_logger = None
104108

105-
result_str, success, task_time = _log_and_run_task(
109+
(
110+
result_str,
111+
success,
112+
task_time,
113+
exec_failed,
114+
diff_failed,
115+
) = _log_and_run_task(
106116
task,
107117
stdout_logger,
108118
task_logger,
@@ -116,11 +126,27 @@ def run_tasks(
116126
result_strs[task_name] = result_str
117127
if not success:
118128
failures += 1
129+
if exec_failed:
130+
exec_fail_tasks.append(task_name)
131+
if diff_failed:
132+
diff_fail_tasks.append(task_name)
119133
task_times[task_name] = task_time
120134

121135
suite_time = time.time() - suite_start
122136

123137
os.chdir(cwd)
138+
139+
# Write a concise, copy/paste-friendly summary for Omega PRs
140+
_write_output_for_pull_request(
141+
suite_name,
142+
suite,
143+
results={
144+
'total': total_tasks,
145+
'failures': exec_fail_tasks,
146+
'diffs': diff_fail_tasks,
147+
},
148+
)
149+
124150
_log_task_runtimes(
125151
stdout_logger, task_times, result_strs, suite_time, failures
126152
)
@@ -366,6 +392,8 @@ def _log_and_run_task(
366392
task_logger.info('')
367393
task_list = ', '.join(task.steps_to_run)
368394
task_logger.info(f'Running steps: {task_list}')
395+
# Default in case execution fails before setting this
396+
baselines_passed = None
369397
try:
370398
baselines_passed = _run_task(task, available_resources)
371399
run_status = success_str
@@ -409,7 +437,9 @@ def _log_and_run_task(
409437
f' task runtime: {start_time_color}{task_time_str}{end_color}'
410438
)
411439

412-
return result_str, success, task_time
440+
exec_failed = not task_pass
441+
diff_failed = baselines_passed is False
442+
return result_str, success, task_time, exec_failed, diff_failed
413443

414444

415445
def _run_task(task, available_resources):
@@ -604,3 +634,159 @@ def _run_step_as_subprocess(logger, step, new_log_file):
604634
os.chdir(step.work_dir)
605635
step_args = ['polaris', 'serial', '--step_is_subprocess']
606636
check_call(step_args, step_logger)
637+
638+
639+
def _write_output_for_pull_request(
640+
suite_name, suite, results: Optional[dict] = None
641+
):
642+
"""
643+
Parse metadata from the provenance file and write a concise summary that
644+
can be copy/pasted into an Omega pull request.
645+
646+
Parameters
647+
----------
648+
suite_name : str
649+
The name of the suite (or 'task') being run.
650+
651+
suite : dict
652+
The unpickled suite dictionary containing tasks and base work dir.
653+
"""
654+
work_dir = suite.get('work_dir', os.getcwd())
655+
provenance_path = os.path.join(work_dir, 'provenance')
656+
if not os.path.exists(provenance_path):
657+
return
658+
659+
# keys in provenance are written exactly like these labels
660+
labels = {
661+
'baseline work directory': 'baseline',
662+
'build directory': 'build',
663+
'work directory': 'work',
664+
'machine': 'machine',
665+
'partition': 'partition',
666+
'compiler': 'compiler',
667+
}
668+
669+
values: Dict[str, Optional[str]] = {v: None for v in labels.values()}
670+
_parse_provenance_into(provenance_path, labels, values)
671+
672+
# If a baseline workdir exists, parse its provenance to get baseline build
673+
baseline_build: Optional[str] = None
674+
baseline_build = _parse_baseline_build(values.get('baseline'))
675+
676+
# Build the output content. Only include optional lines if present.
677+
lines = [f'Polaris {suite_name} suite']
678+
679+
if values['baseline']:
680+
lines.append(f'- Baseline workdir: {values["baseline"]}')
681+
if baseline_build:
682+
lines.append(f'- Baseline build: {baseline_build}')
683+
if values['build']:
684+
lines.append(f'- PR build: {values["build"]}')
685+
if values['work']:
686+
lines.append(f'- PR workdir: {values["work"]}')
687+
if values['machine']:
688+
lines.append(f'- Machine: {values["machine"]}')
689+
if values['partition']:
690+
lines.append(f'- Partition: {values["partition"]}')
691+
if values['compiler']:
692+
lines.append(f'- Compiler: {values["compiler"]}')
693+
694+
# Placeholder for developer to fill in
695+
lines.append('- Build type: <Debug|Release>')
696+
697+
# Try to include job scheduler log path for Slurm
698+
job_log = _derive_job_log_path(suite_name, suite)
699+
if job_log:
700+
lines.append(f'- Log: {job_log}')
701+
702+
# If we have results, summarize them
703+
if results is not None and isinstance(results, dict):
704+
total = int(results.get('total', 0) or 0)
705+
failures: List[str] = list(results.get('failures', []) or [])
706+
diffs: List[str] = list(results.get('diffs', []) or [])
707+
708+
if total > 0 and not failures and not diffs:
709+
lines.append('- Result: All tests passed')
710+
else:
711+
lines.append('- Result:')
712+
if failures:
713+
lines.append(f' - Failures ({len(failures)} of {total}):')
714+
for name in failures:
715+
lines.append(f' - {name}')
716+
if diffs:
717+
lines.append(f' - Diffs ({len(diffs)} of {total}):')
718+
for name in diffs:
719+
lines.append(f' - {name}')
720+
721+
out_path = os.path.join(work_dir, f'{suite_name}_output_for_pr.log')
722+
print(f'Writing output useful for copy/paste into PRs to:\n {out_path}')
723+
with open(out_path, 'w') as out:
724+
out.write('\n'.join(lines) + '\n')
725+
print('Done.')
726+
727+
728+
def _parse_provenance_into(path, labels, target_values):
729+
if not os.path.exists(path):
730+
return
731+
try:
732+
with open(path, 'r') as f:
733+
for line in f:
734+
if ':' not in line:
735+
continue
736+
parts = line.strip().split(':', 1)
737+
if len(parts) != 2:
738+
continue
739+
key = parts[0].strip().lower()
740+
val = parts[1].strip()
741+
if key in labels:
742+
target_values[labels[key]] = val
743+
except (OSError, UnicodeDecodeError):
744+
# silent failure; helper is best-effort
745+
pass
746+
747+
748+
def _parse_baseline_build(baseline_workdir: Optional[str]) -> Optional[str]:
749+
if not baseline_workdir:
750+
return None
751+
path = os.path.join(baseline_workdir, 'provenance')
752+
if not os.path.exists(path):
753+
return None
754+
try:
755+
with open(path, 'r') as f:
756+
for line in f:
757+
if ':' not in line:
758+
continue
759+
parts = line.strip().split(':', 1)
760+
if len(parts) != 2:
761+
continue
762+
key = parts[0].strip().lower()
763+
val = parts[1].strip()
764+
if key == 'build directory':
765+
return val
766+
except (OSError, UnicodeDecodeError):
767+
return None
768+
return None
769+
770+
771+
def _derive_job_log_path(suite_name: str, suite: dict) -> Optional[str]:
772+
"""Best-effort reconstruction of the Slurm job log path."""
773+
job_id = os.environ.get('SLURM_JOB_ID')
774+
if not job_id:
775+
return None
776+
777+
# Reconstruct the job_name the same way the job script did
778+
# using the common component config
779+
try:
780+
task = next(iter(suite['tasks'].values()))
781+
component = task.component
782+
common_config = setup_config(
783+
task.base_work_dir, f'{component.name}.cfg'
784+
)
785+
job_name = common_config.get('job', 'job_name')
786+
if job_name == '<<<default>>>':
787+
suite_suffix = f'_{suite_name}' if suite_name else ''
788+
job_name = f'polaris{suite_suffix}'
789+
work_dir = suite.get('work_dir', os.getcwd())
790+
return os.path.join(work_dir, f'{job_name}.o{job_id}')
791+
except (StopIteration, KeyError, OSError, AttributeError):
792+
return None

0 commit comments

Comments
 (0)