Skip to content

Commit e981b2c

Browse files
authored
Handle missing tests gracefully by adding MissingTestError to avoid backtrace (#640)
1 parent b52cd2b commit e981b2c

File tree

5 files changed

+81
-19
lines changed

5 files changed

+81
-19
lines changed

src/cloudai/_core/exceptions.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,21 @@ class SystemConfigParsingError(Exception):
129129
pass
130130

131131

132+
class MissingTestError(Exception):
133+
"""Exception raised when a test specified in a test scenario is not found in the test directory."""
134+
135+
def __init__(self, test_name: str):
136+
self.test_name = test_name
137+
self.message = (
138+
f"Test '{test_name}' is not defined.\n"
139+
"Please check:\n"
140+
"1. The tests directory argument (--tests-dir) is set correctly\n"
141+
"2. The test name in your test scenario matches the test name defined in the test file\n"
142+
"3. The test file exists in your tests directory"
143+
)
144+
super().__init__(self.message)
145+
146+
132147
def format_validation_error(err: ErrorDetails) -> str:
133148
flattened_field = ".".join(str(v) for v in err["loc"])
134149

src/cloudai/cli/handlers.py

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
BaseInstaller,
3232
CloudAIGymEnv,
3333
Installable,
34+
MissingTestError,
3435
Parser,
3536
Registry,
3637
Runner,
@@ -195,34 +196,47 @@ def register_signal_handlers(signal_handler: Callable) -> None:
195196
signal.signal(sig, signal_handler)
196197

197198

198-
def handle_dry_run_and_run(args: argparse.Namespace) -> int:
199+
def _setup_system_and_scenario(
200+
args: argparse.Namespace,
201+
) -> tuple[Optional[System], Optional[TestScenario], Optional[list[Test]]]:
199202
parser = Parser(args.system_config)
200-
system, tests, test_scenario = parser.parse(args.tests_dir, args.test_scenario)
203+
try:
204+
system, tests, test_scenario = parser.parse(args.tests_dir, args.test_scenario)
205+
except MissingTestError as e:
206+
logging.error(e.message)
207+
return None, None, None
201208

202209
assert test_scenario is not None
203210

204211
if args.output_dir:
205212
system.output_path = args.output_dir.absolute()
206213

207214
if not prepare_output_dir(system.output_path):
208-
return 1
215+
return None, None, None
216+
209217
if args.mode == "dry-run":
210218
system.monitor_interval = 1
211219
system.update()
212220

213-
if args.single_sbatch:
214-
if not isinstance(system, SlurmSystem):
215-
logging.error("Single sbatch is only supported for Slurm systems.")
216-
return 1
221+
return system, test_scenario, tests
217222

218-
Registry().update_runner("slurm", SingleSbatchRunner)
219223

220-
logging.info(f"System Name: {system.name}")
221-
logging.info(f"Scheduler: {system.scheduler}")
222-
logging.info(f"Test Scenario Name: {test_scenario.name}")
224+
def _handle_single_sbatch(args: argparse.Namespace, system: System) -> bool:
225+
if not args.single_sbatch:
226+
return True
223227

224-
logging.info("Checking if test templates are installed.")
228+
if not isinstance(system, SlurmSystem):
229+
logging.error("Single sbatch is only supported for Slurm systems.")
230+
return False
225231

232+
Registry().update_runner("slurm", SingleSbatchRunner)
233+
return True
234+
235+
236+
def _check_installation(
237+
args: argparse.Namespace, system: System, tests: list[Test], test_scenario: TestScenario
238+
) -> bool:
239+
logging.info("Checking if test templates are installed.")
226240
installables, installer = prepare_installation(system, tests, test_scenario)
227241

228242
if args.enable_cache_without_check:
@@ -233,6 +247,29 @@ def handle_dry_run_and_run(args: argparse.Namespace) -> int:
233247
if args.mode == "run" and not result.success:
234248
logging.error("CloudAI has not been installed. Please run install mode first.")
235249
logging.error(result.message)
250+
return False
251+
252+
return True
253+
254+
255+
def handle_dry_run_and_run(args: argparse.Namespace) -> int:
256+
setup_result = _setup_system_and_scenario(args)
257+
if setup_result == (None, None, None):
258+
return 1
259+
260+
system, test_scenario, tests = setup_result
261+
assert system is not None
262+
assert test_scenario is not None
263+
assert tests is not None
264+
265+
if not _handle_single_sbatch(args, system):
266+
return 1
267+
268+
logging.info(f"System Name: {system.name}")
269+
logging.info(f"Scheduler: {system.scheduler}")
270+
logging.info(f"Test Scenario Name: {test_scenario.name}")
271+
272+
if not _check_installation(args, system, tests, test_scenario):
236273
return 1
237274

238275
logging.info(test_scenario.pretty_print())
@@ -247,11 +284,10 @@ def handle_dry_run_and_run(args: argparse.Namespace) -> int:
247284

248285
if all(tr.is_dse_job for tr in test_scenario.test_runs):
249286
handle_dse_job(runner, args)
250-
else:
251-
logging.error("Mixing DSE and non-DSE jobs is not allowed.")
252-
return 1
287+
return 0
253288

254-
return 0
289+
logging.error("Mixing DSE and non-DSE jobs is not allowed.")
290+
return 1
255291

256292

257293
def handle_generate_report(args: argparse.Namespace) -> int:

src/cloudai/core.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from ._core.command_gen_strategy import CommandGenStrategy
2525
from ._core.exceptions import (
2626
JobIdRetrievalError,
27+
MissingTestError,
2728
SystemConfigParsingError,
2829
TestConfigParsingError,
2930
TestScenarioParsingError,
@@ -72,6 +73,7 @@
7273
"JobIdRetrievalError",
7374
"JobStatusResult",
7475
"JsonGenStrategy",
76+
"MissingTestError",
7577
"NsysConfiguration",
7678
"Parser",
7779
"PerTestReporter",

src/cloudai/test_scenario_parser.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from cloudai.util import format_time_limit, parse_time_limit
2727

2828
from .core import (
29+
MissingTestError,
2930
Registry,
3031
ReportGenerationStrategy,
3132
System,
@@ -231,7 +232,7 @@ def _prepare_tdef(self, test_info: TestRunModel) -> Tuple[Test, TestDefinition]:
231232

232233
if test_info.test_name:
233234
if test_info.test_name not in self.test_mapping:
234-
raise ValueError(f"Test '{test_info.test_name}' is not defined. Was tests directory correctly set?")
235+
raise MissingTestError(test_info.test_name)
235236
test = self.test_mapping[test_info.test_name]
236237

237238
test_defined = test.test_definition.model_dump(by_alias=True)

tests/test_test_scenario.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import pytest
2323
import toml
2424

25+
from cloudai._core.exceptions import MissingTestError
2526
from cloudai.core import (
2627
CmdArgs,
2728
GitRepo,
@@ -301,9 +302,16 @@ def test_without_base(self, missing_arg: str):
301302
)
302303

303304
def test_name_is_not_in_mapping(self, test_scenario_parser: TestScenarioParser):
304-
with pytest.raises(ValueError) as exc_info:
305+
with pytest.raises(MissingTestError) as exc_info:
305306
test_scenario_parser._prepare_tdef(TestRunModel(id="1", test_name="nccl"))
306-
assert exc_info.match("Test 'nccl' is not defined. Was tests directory correctly set?")
307+
expected_msg = (
308+
"Test 'nccl' is not defined.\n"
309+
"Please check:\n"
310+
"1. The tests directory argument (--tests-dir) is set correctly\n"
311+
"2. The test name in your test scenario matches the test name defined in the test file\n"
312+
"3. The test file exists in your tests directory"
313+
)
314+
assert str(exc_info.value) == expected_msg
307315

308316
@pytest.mark.parametrize("override_arg", ["name", "description"])
309317
def test_can_override_name_and_description(self, override_arg: str):

0 commit comments

Comments
 (0)