From 40c99e1c521d35c254fdbe8b19473b7bfb32f435 Mon Sep 17 00:00:00 2001 From: Doondi-Ashlesh Date: Wed, 15 Apr 2026 16:15:13 +0000 Subject: [PATCH] test(cli): add CLI structure tests for nano3 and super3 Add typer.testing.CliRunner-based tests that verify all nano3 and super3 subcommands are importable and correctly registered. These structural tests catch missing subpackages (e.g. the super3/data gap fixed in #100) before they reach users, mirroring the existing tests/recipes/embed/test_cli.py pattern. Coverage: - nano3: top-level commands, data prep (pretrain/sft/rl), data import (pretrain/sft/rl) - super3: top-level commands, rl subcommands (rlvr/swe1/swe2/rlhf), data prep (pretrain/sft/rl), data import (pretrain/sft/rl) Signed-off-by: Doondi-Ashlesh --- tests/recipes/nano3/test_cli.py | 110 ++++++++++++++++++++++++++ tests/recipes/super3/test_cli.py | 130 +++++++++++++++++++++++++++++++ 2 files changed, 240 insertions(+) create mode 100644 tests/recipes/nano3/test_cli.py create mode 100644 tests/recipes/super3/test_cli.py diff --git a/tests/recipes/nano3/test_cli.py b/tests/recipes/nano3/test_cli.py new file mode 100644 index 000000000..68400123e --- /dev/null +++ b/tests/recipes/nano3/test_cli.py @@ -0,0 +1,110 @@ +# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""CLI structure tests for nano3 commands. + +Uses ``typer.testing.CliRunner`` for in-process, fast CLI testing. +These tests verify that all nano3 subcommands are importable and registered +correctly — catching import errors (e.g. missing subpackages) before they +reach users. +""" + +from __future__ import annotations + +import pytest +from typer.testing import CliRunner + +from nemotron.cli.bin.nemotron import app + +runner = CliRunner() + +# Top-level nano3 commands +NANO3_TOP_COMMANDS = ["pretrain", "sft", "rl", "eval", "pipe", "data", "model"] + +# nano3 data prep subcommands +NANO3_DATA_PREP_COMMANDS = ["pretrain", "sft", "rl"] + +# nano3 data import subcommands +NANO3_DATA_IMPORT_COMMANDS = ["pretrain", "sft", "rl"] + + +class TestNano3AppStructure: + def test_help_succeeds(self): + result = runner.invoke(app, ["nano3", "--help"]) + assert result.exit_code == 0, f"nano3 --help failed: {result.output}" + + @pytest.mark.parametrize("command", NANO3_TOP_COMMANDS) + def test_top_command_listed(self, command): + result = runner.invoke(app, ["nano3", "--help"]) + assert result.exit_code == 0 + assert command in result.output, ( + f"'{command}' not found in nano3 --help output" + ) + + @pytest.mark.parametrize("command", NANO3_TOP_COMMANDS) + def test_top_command_help_succeeds(self, command): + result = runner.invoke(app, ["nano3", command, "--help"]) + assert result.exit_code == 0, ( + f"nano3 {command} --help failed: {result.output}\n{result.exception}" + ) + + +class TestNano3DataStructure: + def test_data_subcommands_listed(self): + result = runner.invoke(app, ["nano3", "data", "--help"]) + assert result.exit_code == 0 + assert "prep" in result.output + assert "import" in result.output + + def test_data_prep_help_succeeds(self): + result = runner.invoke(app, ["nano3", "data", "prep", "--help"]) + assert result.exit_code == 0, ( + f"nano3 data prep --help failed: {result.output}" + ) + + @pytest.mark.parametrize("command", NANO3_DATA_PREP_COMMANDS) + def test_data_prep_subcommand_listed(self, command): + result = runner.invoke(app, ["nano3", "data", "prep", "--help"]) + assert result.exit_code == 0 + assert command in result.output, ( + f"'{command}' not found in nano3 data prep --help output" + ) + + @pytest.mark.parametrize("command", NANO3_DATA_PREP_COMMANDS) + def test_data_prep_subcommand_help_succeeds(self, command): + result = runner.invoke(app, ["nano3", "data", "prep", command, "--help"]) + assert result.exit_code == 0, ( + f"nano3 data prep {command} --help failed: {result.output}\n{result.exception}" + ) + + def test_data_import_help_succeeds(self): + result = runner.invoke(app, ["nano3", "data", "import", "--help"]) + assert result.exit_code == 0, ( + f"nano3 data import --help failed: {result.output}" + ) + + @pytest.mark.parametrize("command", NANO3_DATA_IMPORT_COMMANDS) + def test_data_import_subcommand_listed(self, command): + result = runner.invoke(app, ["nano3", "data", "import", "--help"]) + assert result.exit_code == 0 + assert command in result.output, ( + f"'{command}' not found in nano3 data import --help output" + ) + + @pytest.mark.parametrize("command", NANO3_DATA_IMPORT_COMMANDS) + def test_data_import_subcommand_help_succeeds(self, command): + result = runner.invoke(app, ["nano3", "data", "import", command, "--help"]) + assert result.exit_code == 0, ( + f"nano3 data import {command} --help failed: {result.output}\n{result.exception}" + ) diff --git a/tests/recipes/super3/test_cli.py b/tests/recipes/super3/test_cli.py new file mode 100644 index 000000000..1c51d9a47 --- /dev/null +++ b/tests/recipes/super3/test_cli.py @@ -0,0 +1,130 @@ +# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""CLI structure tests for super3 commands. + +Uses ``typer.testing.CliRunner`` for in-process, fast CLI testing. +These tests verify that all super3 subcommands are importable and registered +correctly — catching import errors (e.g. missing subpackages) before they +reach users. +""" + +from __future__ import annotations + +import pytest +from typer.testing import CliRunner + +from nemotron.cli.bin.nemotron import app + +runner = CliRunner() + +# Top-level super3 commands +SUPER3_TOP_COMMANDS = ["pretrain", "sft", "rl", "eval", "pipe", "data", "model"] + +# super3 rl subcommands +SUPER3_RL_COMMANDS = ["rlvr", "swe1", "swe2", "rlhf"] + +# super3 data prep subcommands +SUPER3_DATA_PREP_COMMANDS = ["pretrain", "sft", "rl"] + +# super3 data import subcommands +SUPER3_DATA_IMPORT_COMMANDS = ["pretrain", "sft", "rl"] + + +class TestSuper3AppStructure: + def test_help_succeeds(self): + result = runner.invoke(app, ["super3", "--help"]) + assert result.exit_code == 0, f"super3 --help failed: {result.output}" + + @pytest.mark.parametrize("command", SUPER3_TOP_COMMANDS) + def test_top_command_listed(self, command): + result = runner.invoke(app, ["super3", "--help"]) + assert result.exit_code == 0 + assert command in result.output, ( + f"'{command}' not found in super3 --help output" + ) + + @pytest.mark.parametrize("command", SUPER3_TOP_COMMANDS) + def test_top_command_help_succeeds(self, command): + result = runner.invoke(app, ["super3", command, "--help"]) + assert result.exit_code == 0, ( + f"super3 {command} --help failed: {result.output}\n{result.exception}" + ) + + +class TestSuper3RlStructure: + @pytest.mark.parametrize("command", SUPER3_RL_COMMANDS) + def test_rl_subcommand_listed(self, command): + result = runner.invoke(app, ["super3", "rl", "--help"]) + assert result.exit_code == 0 + assert command in result.output, ( + f"'{command}' not found in super3 rl --help output" + ) + + @pytest.mark.parametrize("command", SUPER3_RL_COMMANDS) + def test_rl_subcommand_help_succeeds(self, command): + result = runner.invoke(app, ["super3", "rl", command, "--help"]) + assert result.exit_code == 0, ( + f"super3 rl {command} --help failed: {result.output}\n{result.exception}" + ) + + +class TestSuper3DataStructure: + def test_data_subcommands_listed(self): + result = runner.invoke(app, ["super3", "data", "--help"]) + assert result.exit_code == 0 + assert "prep" in result.output + assert "import" in result.output + + def test_data_prep_help_succeeds(self): + result = runner.invoke(app, ["super3", "data", "prep", "--help"]) + assert result.exit_code == 0, ( + f"super3 data prep --help failed: {result.output}" + ) + + @pytest.mark.parametrize("command", SUPER3_DATA_PREP_COMMANDS) + def test_data_prep_subcommand_listed(self, command): + result = runner.invoke(app, ["super3", "data", "prep", "--help"]) + assert result.exit_code == 0 + assert command in result.output, ( + f"'{command}' not found in super3 data prep --help output" + ) + + @pytest.mark.parametrize("command", SUPER3_DATA_PREP_COMMANDS) + def test_data_prep_subcommand_help_succeeds(self, command): + result = runner.invoke(app, ["super3", "data", "prep", command, "--help"]) + assert result.exit_code == 0, ( + f"super3 data prep {command} --help failed: {result.output}\n{result.exception}" + ) + + def test_data_import_help_succeeds(self): + result = runner.invoke(app, ["super3", "data", "import", "--help"]) + assert result.exit_code == 0, ( + f"super3 data import --help failed: {result.output}" + ) + + @pytest.mark.parametrize("command", SUPER3_DATA_IMPORT_COMMANDS) + def test_data_import_subcommand_listed(self, command): + result = runner.invoke(app, ["super3", "data", "import", "--help"]) + assert result.exit_code == 0 + assert command in result.output, ( + f"'{command}' not found in super3 data import --help output" + ) + + @pytest.mark.parametrize("command", SUPER3_DATA_IMPORT_COMMANDS) + def test_data_import_subcommand_help_succeeds(self, command): + result = runner.invoke(app, ["super3", "data", "import", command, "--help"]) + assert result.exit_code == 0, ( + f"super3 data import {command} --help failed: {result.output}\n{result.exception}" + )