From 438789f71395e9fff1c1d8c467b694c28446a04e Mon Sep 17 00:00:00 2001 From: Sidney Richards Date: Fri, 13 Dec 2024 17:22:51 +0100 Subject: [PATCH] Add a validate-only flag to support web-init/CI usage --- README.rst | 12 ++++ .../commands/setup_configuration.py | 16 +++++ tests/test_command.py | 69 +++++++++++++++++++ 3 files changed, 97 insertions(+) diff --git a/README.rst b/README.rst index 8475eb3..a60054d 100644 --- a/README.rst +++ b/README.rst @@ -225,6 +225,18 @@ Command Line python manage.py setup_configuration --yaml-file /path/to/config.yaml +You can also validate that the configuration source can be successfully loaded, +without actually running the steps, by adding the ``validate-only`` flag: + +.. code-block:: bash + + python manage.py setup_configuration --yaml-file /path/to/config.yaml --validate-only + +The command will either return 0 and a success message if the configuration file can +be loaded without issues, otherwise it will return a non-zero exit code and print any +validation errors. This can be useful e.g. in CI to confirm that your sources are +valid without actually running any steps. + Programmatically ^^^^^^^^^^^^^^^^ diff --git a/django_setup_configuration/management/commands/setup_configuration.py b/django_setup_configuration/management/commands/setup_configuration.py index 118c23b..39abd8d 100644 --- a/django_setup_configuration/management/commands/setup_configuration.py +++ b/django_setup_configuration/management/commands/setup_configuration.py @@ -32,9 +32,17 @@ def add_arguments(self, parser): required=True, help="Path to YAML file containing the configurations", ) + parser.add_argument( + "--validate-only", + action="store_true", + default=False, + help="Validate that all the step configurations can be successfully loaded " + "from source, without actually executing the steps.", + ) @transaction.atomic def handle(self, **options): + validate_only = options["validate_only"] yaml_file = Path(options["yaml_file"]).resolve() if not yaml_file.exists(): raise CommandError(f"Yaml file `{yaml_file}` does not exist.") @@ -74,6 +82,14 @@ def handle(self, **options): f"Prerequisites for configuration are not fulfilled: {errors.as_text()}" ) + if validate_only: + self.stdout.write( + self.style.SUCCESS( + "All configuration values could be successfully read from source." + ) + ) + return + self.stdout.write("Executing steps...") # 2. Configure steps diff --git a/tests/test_command.py b/tests/test_command.py index b67cfd3..07f9b65 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -147,6 +147,40 @@ def test_command_success( step_execute_mock.assert_called_once_with(expected_step_config) +def test_command_success_with_validate_only_flag_does_not_run( + settings, + yaml_file_with_valid_configuration, + expected_step_config, + step_execute_mock, +): + """ + test happy flow + """ + assert User.objects.count() == 0 + stdout = StringIO() + + call_command( + "setup_configuration", + yaml_file=yaml_file_with_valid_configuration, + stdout=stdout, + validate_only=True, + ) + + output = stdout.getvalue().splitlines() + expected_output = [ + f"Loading config settings from {yaml_file_with_valid_configuration}", + "The following steps are configured:", + "User Configuration", + "TestStep", + "All configuration values could be successfully read from source.", + ] + + assert output == expected_output + + assert User.objects.count() == 0 + step_execute_mock.assert_not_called() + + def test_command_with_failing_requirements_reports_errors( step_execute_mock, yaml_file_factory ): @@ -181,6 +215,41 @@ def test_command_with_failing_requirements_reports_errors( step_execute_mock.assert_not_called() +def test_command_with_failing_requirements_and_validate_reports_errors( + step_execute_mock, yaml_file_factory +): + yaml_path = yaml_file_factory( + { + "user_configuration_enabled": True, + "user_configuration": { + "username": 1874, + }, + "some_extra_attrs": "should be allowed", + "test_step_is_enabled": True, + "test_step": { + "a_string": 42, + "username": None, + }, + } + ) + + with pytest.raises(CommandError) as exc: + call_command( + "setup_configuration", + yaml_file=yaml_path, + validate_only=False, + ) + + assert ( + "User Configuration: Failed to load config model for User Configuration" + in str(exc.value) + ) + assert "Failed to load config model for TestStep" in str(exc.value) + + assert User.objects.count() == 0 + step_execute_mock.assert_not_called() + + def test_command_with_failing_execute_reports_errors( expected_step_config, step_execute_mock, yaml_file_with_valid_configuration ):