Skip to content

Commit 552e35a

Browse files
Merge pull request #400 from NVIDIA/am/scenario-report
Generate scenario-level report
2 parents c91ef1e + 18939fc commit 552e35a

File tree

6 files changed

+65
-8
lines changed

6 files changed

+65
-8
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ dependencies = [
2424
"toml==0.10.2",
2525
"kubernetes==30.1.0",
2626
"pydantic==2.8.2",
27+
"jinja2==3.1.5",
2728
]
2829
[project.scripts]
2930
cloudai = "cloudai.__main__:main"

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ tbparse==0.0.8
44
toml==0.10.2
55
kubernetes==30.1.0
66
pydantic==2.8.2
7+
jinja2==3.1.5

src/cloudai/_core/base_runner.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
import asyncio
1818
import datetime
1919
import logging
20-
import sys
2120
from abc import ABC, abstractmethod
2221
from asyncio import Task
2322
from pathlib import Path
@@ -94,14 +93,11 @@ async def shutdown(self):
9493
self.system.kill(job)
9594
logging.info("All jobs have been killed.")
9695

97-
sys.exit(0)
98-
9996
async def run(self):
10097
"""Asynchronously run the test scenario."""
10198
if self.shutting_down:
10299
return
103100

104-
logging.info("Starting test scenario execution.")
105101
total_tests = len(self.test_scenario.test_runs)
106102
completed_jobs_count = 0
107103

src/cloudai/_core/reporter.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
import logging
1818
from pathlib import Path
1919

20+
import jinja2
21+
2022
from .system import System
2123
from .test_scenario import TestRun, TestScenario
2224

@@ -44,15 +46,41 @@ def generate(self) -> None:
4446
Args:
4547
test_scenario (TestScenario): The scenario containing tests.
4648
"""
49+
self.generate_scenario_report()
50+
4751
for tr in self.test_scenario.test_runs:
4852
test_output_dir = self.results_root / tr.name
4953
if not test_output_dir.exists() or not test_output_dir.is_dir():
5054
logging.warning(f"Directory '{test_output_dir}' not found.")
5155
continue
5256

53-
self._generate_test_report(test_output_dir, tr)
57+
self.generate_per_case_reports(test_output_dir, tr)
5458

55-
def _generate_test_report(self, directory_path: Path, tr: TestRun) -> None:
59+
def generate_scenario_report(self) -> None:
60+
template = jinja2.Environment(loader=jinja2.FileSystemLoader("src/cloudai/util")).get_template(
61+
"general-report.jinja2"
62+
)
63+
64+
results = {}
65+
for tr in self.test_scenario.test_runs:
66+
for iter in range(tr.iterations):
67+
run_dir = self.results_root / tr.name / f"{iter}"
68+
if run_dir.exists():
69+
results.setdefault(
70+
tr.name + f"{iter}", {"logs_path": f"./{run_dir.relative_to(self.results_root)}"}
71+
)
72+
73+
report = template.render(
74+
test_scenario=self.test_scenario,
75+
tr_results=results,
76+
)
77+
report_path = self.results_root / f"{self.test_scenario.name}.html"
78+
with report_path.open("w") as f:
79+
f.write(report)
80+
81+
logging.info(f"Generated scenario report at {report_path}")
82+
83+
def generate_per_case_reports(self, directory_path: Path, tr: TestRun) -> None:
5684
"""
5785
Generate reports for a test by iterating through subdirectories within the directory path.
5886
@@ -71,7 +99,7 @@ def _generate_test_report(self, directory_path: Path, tr: TestRun) -> None:
7199
tr.output_path = subdir
72100

73101
if not rgs.can_handle_directory():
74-
logging.warning(f"Skipping '{tr.output_path}', can't handle with " f"strategy={reporter}.")
102+
logging.warning(f"Skipping '{tr.output_path}', can't handle with " f"strategy={reporter.__name__}.")
75103
continue
76104

77105
rgs.generate_report()

src/cloudai/_core/runner.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from typing import Optional
2121

2222
from .base_runner import BaseRunner
23+
from .exceptions import JobFailureError
2324
from .registry import Registry
2425
from .system import System
2526
from .test_scenario import TestScenario
@@ -74,7 +75,11 @@ def create_runner(self, mode: str, system: System, test_scenario: TestScenario)
7475

7576
async def run(self):
7677
"""Run the test scenario using the instantiated runner."""
77-
await self.runner.run()
78+
try:
79+
await self.runner.run()
80+
logging.debug("All jobs finished successfully.")
81+
except JobFailureError as exc:
82+
logging.debug(f"Runner failed JobFailure exception: {exc}", exc_info=True)
7883

7984
def _cancel_all(self):
8085
# the below code might look excessive, this is to address https://docs.astral.sh/ruff/rules/asyncio-dangling-task/
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<html>
2+
<head>
3+
<title>{{ test_scenario.name }}</title>
4+
</head>
5+
<body>
6+
<h1>{{ test_scenario.name }}</h1>
7+
</body>
8+
<table>
9+
<tr>
10+
<th>Test</th>
11+
<th>Status</th>
12+
</tr>
13+
{% for tr in test_scenario.test_runs %}
14+
{% for iter in range(tr.iterations) %}
15+
<tr>
16+
<td>{{ tr.name }}.{{ iter }}</td>
17+
{% if tr.name + iter|string in tr_results %}
18+
<td><a href="{{ tr_results[tr.name + iter|string]["logs_path"] }}">logs</a></td>
19+
{% else %}
20+
<td>no logs</td>
21+
{% endif %}
22+
</tr>
23+
{% endfor %}
24+
{% endfor %}
25+
</table>
26+
</html>

0 commit comments

Comments
 (0)