diff --git a/samcli/cli/lazy_group.py b/samcli/cli/lazy_group.py new file mode 100644 index 00000000000..4c37beb2a63 --- /dev/null +++ b/samcli/cli/lazy_group.py @@ -0,0 +1,45 @@ +""" +LazyGroup implementation for Click CLI performance optimization +""" + +import importlib + +import click +from click import ClickException + + +class LazyGroup(click.Group): + def __init__(self, *args, lazy_subcommands=None, **kwargs): + super().__init__(*args, **kwargs) + # lazy_subcommands is a map of the form: + # {command-name} -> {module-name}.{command-object-name} + self.lazy_subcommands = lazy_subcommands or {} + + def list_commands(self, ctx): + base = super().list_commands(ctx) + lazy = sorted(self.lazy_subcommands.keys()) + return base + lazy + + def get_command(self, ctx, cmd_name): + if cmd_name in self.lazy_subcommands: + return self._lazy_load(cmd_name) + return super().get_command(ctx, cmd_name) + + def _lazy_load(self, cmd_name): + # lazily loading a command, first get the module name and attribute name + import_path = self.lazy_subcommands[cmd_name] + modname, cmd_object_name = import_path.rsplit(".", 1) + # do the import + try: + mod = importlib.import_module(modname) + except ImportError as e: + raise ClickException(f"Failed to load command '{cmd_name}': {str(e)}") + # get the Command object from that module + try: + cmd_object = getattr(mod, cmd_object_name) + except AttributeError: + raise ClickException(f"Command '{cmd_name}' not found in module '{modname}'") + # check the result to make debugging easier + if not isinstance(cmd_object, click.Command): + raise ClickException(f"Lazy loading of {import_path} failed by returning a non-command object") + return cmd_object diff --git a/samcli/commands/list/list.py b/samcli/commands/list/list.py index 2e235bab652..ef02db371b0 100644 --- a/samcli/commands/list/list.py +++ b/samcli/commands/list/list.py @@ -4,19 +4,18 @@ import click -from samcli.commands.list.endpoints.command import cli as testable_resources_cli -from samcli.commands.list.resources.command import cli as resources_cli -from samcli.commands.list.stack_outputs.command import cli as stack_outputs_cli +from samcli.cli.lazy_group import LazyGroup -@click.group() +@click.group( + cls=LazyGroup, + lazy_subcommands={ + "endpoints": "samcli.commands.list.endpoints.command.cli", + "resources": "samcli.commands.list.resources.command.cli", + "stack-outputs": "samcli.commands.list.stack_outputs.command.cli", + }, +) def cli(): """ Get local and deployed state of serverless application. """ - - -# Add individual commands under this group -cli.add_command(resources_cli) -cli.add_command(stack_outputs_cli) -cli.add_command(testable_resources_cli) diff --git a/samcli/commands/local/local.py b/samcli/commands/local/local.py index 70f243a21c0..047393217c8 100644 --- a/samcli/commands/local/local.py +++ b/samcli/commands/local/local.py @@ -5,21 +5,19 @@ import click -from .generate_event.cli import cli as generate_event_cli -from .invoke.cli import cli as invoke_cli -from .start_api.cli import cli as start_api_cli -from .start_lambda.cli import cli as start_lambda_cli +from samcli.cli.lazy_group import LazyGroup -@click.group() +@click.group( + cls=LazyGroup, + lazy_subcommands={ + "invoke": "samcli.commands.local.invoke.cli.cli", + "start-api": "samcli.commands.local.start_api.cli.cli", + "start-lambda": "samcli.commands.local.start_lambda.cli.cli", + "generate-event": "samcli.commands.local.generate_event.cli.cli", + }, +) def cli(): """ Run your Serverless application locally for quick development & testing """ - - -# Add individual commands under this group -cli.add_command(invoke_cli) -cli.add_command(start_api_cli) -cli.add_command(generate_event_cli) -cli.add_command(start_lambda_cli) diff --git a/samcli/commands/pipeline/pipeline.py b/samcli/commands/pipeline/pipeline.py index 2d8df4463e7..68a23584f54 100644 --- a/samcli/commands/pipeline/pipeline.py +++ b/samcli/commands/pipeline/pipeline.py @@ -5,17 +5,17 @@ import click -from .bootstrap.cli import cli as bootstrap_cli -from .init.cli import cli as init_cli +from samcli.cli.lazy_group import LazyGroup -@click.group() +@click.group( + cls=LazyGroup, + lazy_subcommands={ + "bootstrap": "samcli.commands.pipeline.bootstrap.cli.cli", + "init": "samcli.commands.pipeline.init.cli.cli", + }, +) def cli() -> None: """ Manage the continuous delivery of the application """ - - -# Add individual commands under this group -cli.add_command(bootstrap_cli) -cli.add_command(init_cli) diff --git a/samcli/commands/remote/remote.py b/samcli/commands/remote/remote.py index 76da99e3a19..f164f3eb763 100644 --- a/samcli/commands/remote/remote.py +++ b/samcli/commands/remote/remote.py @@ -5,17 +5,17 @@ import click -from samcli.commands.remote.invoke.cli import cli as invoke_cli -from samcli.commands.remote.test_event.test_event import cli as event_cli +from samcli.cli.lazy_group import LazyGroup -@click.group() +@click.group( + cls=LazyGroup, + lazy_subcommands={ + "invoke": "samcli.commands.remote.invoke.cli.cli", + "test-event": "samcli.commands.remote.test_event.test_event.cli", + }, +) def cli(): """ Interact with your Serverless application in the cloud for quick development & testing """ - - -# Add individual commands under this group -cli.add_command(invoke_cli) -cli.add_command(event_cli) diff --git a/samcli/commands/remote/test_event/test_event.py b/samcli/commands/remote/test_event/test_event.py index f725fad6025..80a40cb04ca 100644 --- a/samcli/commands/remote/test_event/test_event.py +++ b/samcli/commands/remote/test_event/test_event.py @@ -2,20 +2,20 @@ import click -from samcli.commands.remote.test_event.delete.cli import cli as delete_cli -from samcli.commands.remote.test_event.get.cli import cli as get_cli -from samcli.commands.remote.test_event.list.cli import cli as list_cli -from samcli.commands.remote.test_event.put.cli import cli as put_cli +from samcli.cli.lazy_group import LazyGroup -@click.group("test-event") +@click.group( + "test-event", + cls=LazyGroup, + lazy_subcommands={ + "delete": "samcli.commands.remote.test_event.delete.cli.cli", + "get": "samcli.commands.remote.test_event.get.cli.cli", + "put": "samcli.commands.remote.test_event.put.cli.cli", + "list": "samcli.commands.remote.test_event.list.cli.cli", + }, +) def cli(): """ Manage remote test events """ - - -cli.add_command(delete_cli) -cli.add_command(get_cli) -cli.add_command(put_cli) -cli.add_command(list_cli) diff --git a/schema/make_schema.py b/schema/make_schema.py index 8e1a8e15687..5749465e191 100644 --- a/schema/make_schema.py +++ b/schema/make_schema.py @@ -186,18 +186,23 @@ def retrieve_command_structure(package_name: str) -> List[SamCliCommandSchema]: subcommands within the package. """ module = importlib.import_module(package_name) + + def create_command_schema(module, subcommand): + cmd_name = SamConfig.to_key([module.__name__.split(".")[-1], str(subcommand.name)]) + return SamCliCommandSchema( + cmd_name, + clean_text(subcommand.help or subcommand.short_help or ""), + get_params_from_command(subcommand), + ) + command = [] if isinstance(module.cli, click.core.Group): # command has subcommands (e.g. local invoke) - for subcommand in module.cli.commands.values(): - cmd_name = SamConfig.to_key([module.__name__.split(".")[-1], str(subcommand.name)]) - command.append( - SamCliCommandSchema( - cmd_name, - clean_text(subcommand.help or subcommand.short_help or ""), - get_params_from_command(subcommand), - ) - ) + ctx = click.Context(module.cli) + for subcommand_name in module.cli.list_commands(ctx): + subcommand = module.cli.get_command(ctx, subcommand_name) + if subcommand: + command.append(create_command_schema(module, subcommand)) else: cmd_name = SamConfig.to_key([module.__name__.split(".")[-1]]) command.append( diff --git a/schema/samcli.json b/schema/samcli.json index 7097be9d143..fcffe9c001a 100644 --- a/schema/samcli.json +++ b/schema/samcli.json @@ -408,6 +408,21 @@ "parameters" ] }, + "local_generate_event": { + "title": "Local Generate Event command", + "description": "Generate events for Lambda functions.", + "properties": { + "parameters": { + "title": "Parameters for the local generate event command", + "description": "Available parameters for the local generate event command:\n* ", + "type": "object", + "properties": {} + } + }, + "required": [ + "parameters" + ] + }, "local_invoke": { "title": "Local Invoke command", "description": "Invoke AWS serverless functions locally.", @@ -835,21 +850,6 @@ "parameters" ] }, - "local_generate_event": { - "title": "Local Generate Event command", - "description": "Generate events for Lambda functions.", - "properties": { - "parameters": { - "title": "Parameters for the local generate event command", - "description": "Available parameters for the local generate event command:\n* ", - "type": "object", - "properties": {} - } - }, - "required": [ - "parameters" - ] - }, "local_start_lambda": { "title": "Local Start Lambda command", "description": "Emulate AWS serverless functions locally.", @@ -2040,13 +2040,13 @@ "parameters" ] }, - "list_resources": { - "title": "List Resources command", - "description": "Get a list of resources that will be deployed to CloudFormation.\n\nIf a stack name is provided, the corresponding physical IDs of each\nresource will be mapped to the logical ID of each resource.", + "list_endpoints": { + "title": "List Endpoints command", + "description": "Get a summary of the cloud endpoints in the stack.\n\nThis command will show both the cloud and local endpoints that can\nbe used with sam local and sam sync. Currently the endpoint resources\nare Lambda functions and API Gateway API resources.", "properties": { "parameters": { - "title": "Parameters for the list resources command", - "description": "Available parameters for the list resources command:\n* parameter_overrides:\nString that contains AWS CloudFormation parameter overrides encoded as key=value pairs.\n* stack_name:\nName of corresponding deployed stack.(Not including a stack name will only show local resources defined in the template.)\n* output:\nOutput the results from the command in a given output format (json or table).\n* template_file:\nAWS SAM template file.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* save_params:\nSave the parameters provided via the command line to the configuration file.", + "title": "Parameters for the list endpoints command", + "description": "Available parameters for the list endpoints command:\n* parameter_overrides:\nString that contains AWS CloudFormation parameter overrides encoded as key=value pairs.\n* stack_name:\nName of corresponding deployed stack.(Not including a stack name will only show local resources defined in the template.)\n* output:\nOutput the results from the command in a given output format (json or table).\n* template_file:\nAWS SAM template file.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* save_params:\nSave the parameters provided via the command line to the configuration file.", "type": "object", "properties": { "parameter_overrides": { @@ -2113,19 +2113,30 @@ "parameters" ] }, - "list_stack_outputs": { - "title": "List Stack Outputs command", - "description": "Get the stack outputs as defined in the SAM/CloudFormation template.", + "list_resources": { + "title": "List Resources command", + "description": "Get a list of resources that will be deployed to CloudFormation.\n\nIf a stack name is provided, the corresponding physical IDs of each\nresource will be mapped to the logical ID of each resource.", "properties": { "parameters": { - "title": "Parameters for the list stack outputs command", - "description": "Available parameters for the list stack outputs command:\n* stack_name:\nName of corresponding deployed stack.\n* output:\nOutput the results from the command in a given output format (json or table).\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* save_params:\nSave the parameters provided via the command line to the configuration file.", + "title": "Parameters for the list resources command", + "description": "Available parameters for the list resources command:\n* parameter_overrides:\nString that contains AWS CloudFormation parameter overrides encoded as key=value pairs.\n* stack_name:\nName of corresponding deployed stack.(Not including a stack name will only show local resources defined in the template.)\n* output:\nOutput the results from the command in a given output format (json or table).\n* template_file:\nAWS SAM template file.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* save_params:\nSave the parameters provided via the command line to the configuration file.", "type": "object", "properties": { + "parameter_overrides": { + "title": "parameter_overrides", + "type": [ + "array", + "string" + ], + "description": "String that contains AWS CloudFormation parameter overrides encoded as key=value pairs.", + "items": { + "type": "string" + } + }, "stack_name": { "title": "stack_name", "type": "string", - "description": "Name of corresponding deployed stack." + "description": "Name of corresponding deployed stack.(Not including a stack name will only show local resources defined in the template.)" }, "output": { "title": "output", @@ -2137,6 +2148,12 @@ "table" ] }, + "template_file": { + "title": "template_file", + "type": "string", + "description": "AWS SAM template file.", + "default": "template.[yaml|yml|json]" + }, "profile": { "title": "profile", "type": "string", @@ -2169,30 +2186,19 @@ "parameters" ] }, - "list_endpoints": { - "title": "List Endpoints command", - "description": "Get a summary of the cloud endpoints in the stack.\n\nThis command will show both the cloud and local endpoints that can\nbe used with sam local and sam sync. Currently the endpoint resources\nare Lambda functions and API Gateway API resources.", + "list_stack_outputs": { + "title": "List Stack Outputs command", + "description": "Get the stack outputs as defined in the SAM/CloudFormation template.", "properties": { "parameters": { - "title": "Parameters for the list endpoints command", - "description": "Available parameters for the list endpoints command:\n* parameter_overrides:\nString that contains AWS CloudFormation parameter overrides encoded as key=value pairs.\n* stack_name:\nName of corresponding deployed stack.(Not including a stack name will only show local resources defined in the template.)\n* output:\nOutput the results from the command in a given output format (json or table).\n* template_file:\nAWS SAM template file.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* save_params:\nSave the parameters provided via the command line to the configuration file.", + "title": "Parameters for the list stack outputs command", + "description": "Available parameters for the list stack outputs command:\n* stack_name:\nName of corresponding deployed stack.\n* output:\nOutput the results from the command in a given output format (json or table).\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* save_params:\nSave the parameters provided via the command line to the configuration file.", "type": "object", "properties": { - "parameter_overrides": { - "title": "parameter_overrides", - "type": [ - "array", - "string" - ], - "description": "String that contains AWS CloudFormation parameter overrides encoded as key=value pairs.", - "items": { - "type": "string" - } - }, "stack_name": { "title": "stack_name", "type": "string", - "description": "Name of corresponding deployed stack.(Not including a stack name will only show local resources defined in the template.)" + "description": "Name of corresponding deployed stack." }, "output": { "title": "output", @@ -2204,12 +2210,6 @@ "table" ] }, - "template_file": { - "title": "template_file", - "type": "string", - "description": "AWS SAM template file.", - "default": "template.[yaml|yml|json]" - }, "profile": { "title": "profile", "type": "string", diff --git a/tests/unit/cli/test_lazy_group.py b/tests/unit/cli/test_lazy_group.py new file mode 100644 index 00000000000..f424a8cfaed --- /dev/null +++ b/tests/unit/cli/test_lazy_group.py @@ -0,0 +1,125 @@ +""" +Unit tests for LazyGroup functionality +""" + +import unittest +from unittest.mock import Mock, patch + +import click +from click.testing import CliRunner + +from samcli.cli.lazy_group import LazyGroup + + +class TestLazyGroup(unittest.TestCase): + def setUp(self): + self.runner = CliRunner() + + def test_lazy_group_lists_commands(self): + """Test that LazyGroup correctly lists available commands""" + + @click.group( + cls=LazyGroup, + lazy_subcommands={ + "cmd1": "fake.module.cmd1", + "cmd2": "fake.module.cmd2", + }, + ) + def test_group(): + pass + + # Should list lazy commands + commands = test_group.list_commands(None) + self.assertIn("cmd1", commands) + self.assertIn("cmd2", commands) + + @patch("samcli.cli.lazy_group.importlib.import_module") + def test_lazy_group_loads_command_on_demand(self, mock_import): + """Test that LazyGroup only imports modules when commands are accessed""" + # Mock the imported module and command + mock_module = Mock() + mock_command = Mock(spec=click.Command) + mock_module.cli = mock_command + mock_import.return_value = mock_module + + @click.group( + cls=LazyGroup, + lazy_subcommands={ + "test-cmd": "fake.module.cli", + }, + ) + def test_group(): + pass + + # Import should not happen until command is accessed + mock_import.assert_not_called() + + # Access the command + result = test_group.get_command(None, "test-cmd") + + # Now import should have happened + mock_import.assert_called_once_with("fake.module") + self.assertEqual(result, mock_command) + + @patch("samcli.cli.lazy_group.importlib.import_module") + def test_lazy_group_handles_missing_command(self, mock_import): + """Test that LazyGroup handles requests for non-existent commands""" + + @click.group( + cls=LazyGroup, + lazy_subcommands={ + "existing-cmd": "fake.module.cli", + }, + ) + def test_group(): + pass + + # Request non-existent command + result = test_group.get_command(None, "non-existent") + + # Should return None and not attempt import + self.assertIsNone(result) + mock_import.assert_not_called() + + @patch("samcli.cli.lazy_group.importlib.import_module") + def test_lazy_group_handles_import_error(self, mock_import): + """Test that LazyGroup handles import errors gracefully""" + mock_import.side_effect = ImportError("Module not found") + + @click.group( + cls=LazyGroup, + lazy_subcommands={ + "broken-cmd": "nonexistent.module.cli", + }, + ) + def test_group(): + pass + + # Should raise ClickException when import fails + with self.assertRaises(click.ClickException) as cm: + test_group.get_command(None, "broken-cmd") + + self.assertIn("Failed to load command 'broken-cmd'", str(cm.exception)) + + @patch("samcli.cli.lazy_group.importlib.import_module") + def test_lazy_group_validates_command_object(self, mock_import): + """Test that LazyGroup validates imported objects are Click commands""" + # Mock module with non-command object + mock_module = Mock() + mock_module.cli = "not a command" + mock_import.return_value = mock_module + + @click.group( + cls=LazyGroup, + lazy_subcommands={ + "invalid-cmd": "fake.module.cli", + }, + ) + def test_group(): + pass + + # Should raise ClickException for non-command objects + with self.assertRaises(click.ClickException) as cm: + test_group.get_command(None, "invalid-cmd") + + self.assertIn("non-command object", str(cm.exception)) diff --git a/tests/unit/commands/list/test_list.py b/tests/unit/commands/list/test_list.py new file mode 100644 index 00000000000..8f1db28c373 --- /dev/null +++ b/tests/unit/commands/list/test_list.py @@ -0,0 +1,31 @@ +""" +Unit tests for sam list CLI group +""" + +import unittest +from click.testing import CliRunner +from parameterized import parameterized + +from samcli.commands.list.list import cli + + +class TestListCliGroup(unittest.TestCase): + """Test cases for list CLI group functionality""" + + def setUp(self): + self.runner = CliRunner() + + def test_list_group_help(self): + """Test that list group shows help and lists subcommands""" + result = self.runner.invoke(cli, ["--help"]) + self.assertEqual(result.exit_code, 0) + self.assertIn("Get local and deployed state of serverless application", result.output) + self.assertIn("endpoints", result.output) + self.assertIn("resources", result.output) + self.assertIn("stack-outputs", result.output) + + @parameterized.expand([("endpoints",), ("resources",), ("stack-outputs",)]) + def test_subcommand_help(self, command): + """Test that subcommands can be loaded and show help""" + result = self.runner.invoke(cli, [command, "--help"]) + self.assertEqual(result.exit_code, 0) diff --git a/tests/unit/commands/local/test_local.py b/tests/unit/commands/local/test_local.py new file mode 100644 index 00000000000..111e881a594 --- /dev/null +++ b/tests/unit/commands/local/test_local.py @@ -0,0 +1,32 @@ +""" +Unit tests for sam local CLI group +""" + +import unittest +from click.testing import CliRunner +from parameterized import parameterized + +from samcli.commands.local.local import cli + + +class TestLocalCliGroup(unittest.TestCase): + """Test cases for local CLI group functionality""" + + def setUp(self): + self.runner = CliRunner() + + def test_local_group_help(self): + """Test that local group shows help and lists subcommands""" + result = self.runner.invoke(cli, ["--help"]) + self.assertEqual(result.exit_code, 0) + self.assertIn("Run your Serverless application locally", result.output) + self.assertIn("invoke", result.output) + self.assertIn("start-api", result.output) + self.assertIn("start-lambda", result.output) + self.assertIn("generate-event", result.output) + + @parameterized.expand([("invoke",), ("start-api",), ("start-lambda",), ("generate-event",)]) + def test_subcommand_help(self, command): + """Test that subcommands can be loaded and show help""" + result = self.runner.invoke(cli, [command, "--help"]) + self.assertEqual(result.exit_code, 0) diff --git a/tests/unit/commands/pipeline/test_pipeline.py b/tests/unit/commands/pipeline/test_pipeline.py new file mode 100644 index 00000000000..3ba7ee9e642 --- /dev/null +++ b/tests/unit/commands/pipeline/test_pipeline.py @@ -0,0 +1,30 @@ +""" +Unit tests for sam pipeline CLI group +""" + +import unittest +from click.testing import CliRunner +from parameterized import parameterized + +from samcli.commands.pipeline.pipeline import cli + + +class TestPipelineCliGroup(unittest.TestCase): + """Test cases for pipeline CLI group functionality""" + + def setUp(self): + self.runner = CliRunner() + + def test_pipeline_group_help(self): + """Test that pipeline group shows help and lists subcommands""" + result = self.runner.invoke(cli, ["--help"]) + self.assertEqual(result.exit_code, 0) + self.assertIn("Manage the continuous delivery of the application", result.output) + self.assertIn("bootstrap", result.output) + self.assertIn("init", result.output) + + @parameterized.expand([("bootstrap",), ("init",)]) + def test_subcommand_help(self, command): + """Test that subcommands can be loaded and show help""" + result = self.runner.invoke(cli, [command, "--help"]) + self.assertEqual(result.exit_code, 0) diff --git a/tests/unit/commands/remote/test_event/test_test_event.py b/tests/unit/commands/remote/test_event/test_test_event.py new file mode 100644 index 00000000000..463555e6633 --- /dev/null +++ b/tests/unit/commands/remote/test_event/test_test_event.py @@ -0,0 +1,32 @@ +""" +Unit tests for sam remote test-event CLI group +""" + +import unittest +from click.testing import CliRunner +from parameterized import parameterized + +from samcli.commands.remote.test_event.test_event import cli + + +class TestTestEventCliGroup(unittest.TestCase): + """Test cases for test-event CLI group functionality""" + + def setUp(self): + self.runner = CliRunner() + + def test_test_event_group_help(self): + """Test that test-event group shows help and lists subcommands""" + result = self.runner.invoke(cli, ["--help"]) + self.assertEqual(result.exit_code, 0) + self.assertIn("Manage remote test events", result.output) + self.assertIn("delete", result.output) + self.assertIn("get", result.output) + self.assertIn("put", result.output) + self.assertIn("list", result.output) + + @parameterized.expand([("delete",), ("get",), ("put",), ("list",)]) + def test_subcommand_help(self, command): + """Test that subcommands can be loaded and show help""" + result = self.runner.invoke(cli, [command, "--help"]) + self.assertEqual(result.exit_code, 0) diff --git a/tests/unit/commands/remote/test_remote.py b/tests/unit/commands/remote/test_remote.py new file mode 100644 index 00000000000..8bf5535cfa2 --- /dev/null +++ b/tests/unit/commands/remote/test_remote.py @@ -0,0 +1,30 @@ +""" +Unit tests for sam remote CLI group +""" + +import unittest +from click.testing import CliRunner +from parameterized import parameterized + +from samcli.commands.remote.remote import cli + + +class TestRemoteCliGroup(unittest.TestCase): + """Test cases for remote CLI group functionality""" + + def setUp(self): + self.runner = CliRunner() + + def test_remote_group_help(self): + """Test that remote group shows help and lists subcommands""" + result = self.runner.invoke(cli, ["--help"]) + self.assertEqual(result.exit_code, 0) + self.assertIn("Interact with your Serverless application in the cloud", result.output) + self.assertIn("invoke", result.output) + self.assertIn("test-event", result.output) + + @parameterized.expand([("invoke",), ("test-event",)]) + def test_subcommand_help(self, command): + """Test that subcommands can be loaded and show help""" + result = self.runner.invoke(cli, [command, "--help"]) + self.assertEqual(result.exit_code, 0)