Skip to content

Commit dc297fa

Browse files
committed
feat(cli): add 'gym env validate' pre-flight config check
Ports ng_validate into the unified gym CLI (#1434): a validate() command in cli/env.py registered as 'gym env validate' (+ ng_validate/nemo_gym_validate deprecated shims). Runs the full parse with no Ray, exits 0/1 with a clean, rich-escaped message. Targets martas/1434. Epic #1205 friction #12 (no config validation tooling). Signed-off-by: Wojciech Prazuch <wprazuch@nvidia.com>
1 parent bed6576 commit dc297fa

5 files changed

Lines changed: 63 additions & 0 deletions

File tree

nemo_gym/cli/env.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import json
1818
import os
1919
import shlex
20+
import sys
2021
from glob import glob
2122
from os import makedirs
2223
from os.path import exists
@@ -33,6 +34,7 @@
3334
from devtools import pprint
3435
from omegaconf import DictConfig, OmegaConf
3536
from pydantic import Field
37+
from rich.markup import escape
3638
from rich.table import Table
3739
from tqdm.auto import tqdm
3840

@@ -799,6 +801,34 @@ def dump_config(): # pragma: no cover
799801
print(OmegaConf.to_yaml(global_config_dict, resolve=True))
800802

801803

804+
def validate():
805+
"""
806+
Validate a NeMo Gym configuration without starting any servers.
807+
808+
Runs the full config parse and validation pipeline — config_paths loading, key resolution,
809+
cross-reference checks, and per-server validation — with no Ray initialization and no server
810+
subprocesses. Exits 0 if the configuration is valid, 1 otherwise. Intended as a fast pre-flight
811+
check before a run or a cluster submission.
812+
813+
Examples:
814+
815+
```bash
816+
gym env validate --config <config1> --config <config2>
817+
```
818+
"""
819+
try:
820+
global_config_dict = get_global_config_dict()
821+
# Mirrors `run`/`dump_config`: handles the +h=true help path and final config validation.
822+
BaseNeMoGymCLIConfig.model_validate(global_config_dict)
823+
except Exception as e:
824+
# escape() so '[...]' in the message (e.g. config_paths examples) isn't eaten as rich markup.
825+
rich.print(f"[red]✗ Config validation failed:[/red] {escape(f'{type(e).__name__}: {e}')}")
826+
sys.exit(1)
827+
828+
server_instances = [k for k in global_config_dict.keys() if k not in NEMO_GYM_RESERVED_TOP_LEVEL_KEYS]
829+
rich.print(f"[green]✓ Config is valid[/green] — {len(server_instances)} server instance(s) configured.")
830+
831+
802832
def status(): # pragma: no cover
803833
global_config_dict = get_global_config_dict()
804834
BaseNeMoGymCLIConfig.model_validate(global_config_dict)

nemo_gym/cli/main.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,11 @@ def _dataset_download(args: argparse.Namespace, overrides: list[str]) -> None:
229229
summary="Resolve the final config from configs, flags, and overrides.",
230230
flags=(CONFIG,),
231231
),
232+
"env validate": Command(
233+
target="nemo_gym.cli.env:validate",
234+
summary="Validate a config without starting servers (no Ray); exits 0 if valid, 1 if not.",
235+
flags=(CONFIG,),
236+
),
232237
"env packages": Command(
233238
target="nemo_gym.cli.env:pip_list",
234239
summary="Print pip packages for the selected resource server.",

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,8 @@ ng_delete_dataset_from_gitlab = "nemo_gym.cli.dataset:delete_jsonl_dataset_from_
378378
# Configuration utils
379379
nemo_gym_dump_config = "nemo_gym.cli.env:dump_config"
380380
ng_dump_config = "nemo_gym.cli.env:dump_config"
381+
nemo_gym_validate = "nemo_gym.cli.env:validate"
382+
ng_validate = "nemo_gym.cli.env:validate"
381383

382384
# Display help
383385
nemo_gym_help = "nemo_gym.cli.general:display_help_legacy"

tests/unit_tests/test_cli.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from omegaconf import OmegaConf
2525
from pytest import MonkeyPatch, raises
2626

27+
import nemo_gym.cli.env
2728
import nemo_gym.global_config
2829
from nemo_gym import PARENT_DIR
2930
from nemo_gym.cli.env import (
@@ -32,6 +33,7 @@
3233
RunConfig,
3334
RunHelper,
3435
init_resources_server,
36+
validate,
3537
)
3638
from nemo_gym.cli.general import display_help_legacy
3739
from nemo_gym.config_types import ResourcesServerInstanceConfig
@@ -227,3 +229,26 @@ def test_graceful_termination_does_not_kill(self) -> None:
227229
assert a.wait.call_count == 1
228230
assert b.wait.call_count == 1
229231
assert runner._processes == {}
232+
233+
234+
class TestValidate:
235+
"""ng_validate / `gym env validate` parses + validates the config and exits 0/1 without Ray."""
236+
237+
def test_validate_passes_on_valid_config(self, monkeypatch: MonkeyPatch) -> None:
238+
cfg = OmegaConf.create(
239+
{"my_server": {"resources_servers": {"x": {"entrypoint": "app.py", "domain": "other"}}}}
240+
)
241+
monkeypatch.setattr(nemo_gym.cli.env, "get_global_config_dict", lambda *a, **k: cfg)
242+
243+
# Should return normally (no SystemExit) on a valid config.
244+
validate()
245+
246+
def test_validate_exits_nonzero_on_invalid_config(self, monkeypatch: MonkeyPatch) -> None:
247+
def boom(*a, **k):
248+
raise ValueError("references resources_servers/'typo' which is not defined")
249+
250+
monkeypatch.setattr(nemo_gym.cli.env, "get_global_config_dict", boom)
251+
252+
with raises(SystemExit) as exc_info:
253+
validate()
254+
assert exc_info.value.code == 1

tests/unit_tests/test_cli_main.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def fake_dispatch(target: str, overrides: list[str]) -> None:
3939
CONFIG_COMMANDS = [
4040
(["env", "run"], "nemo_gym.cli.env:run"),
4141
(["env", "resolve"], "nemo_gym.cli.env:dump_config"),
42+
(["env", "validate"], "nemo_gym.cli.env:validate"),
4243
(["eval", "prepare"], "nemo_gym.cli.eval:prepare_benchmark"),
4344
(["eval", "aggregate"], "nemo_gym.cli.eval:aggregate_rollouts"),
4445
(["eval", "run"], "nemo_gym.cli.eval:e2e_rollout_collection"),

0 commit comments

Comments
 (0)