From 8c3c85d5509096539f5f4f377a4e810da98e898b Mon Sep 17 00:00:00 2001 From: Michel El Nacouzi Date: Wed, 11 Dec 2024 14:32:28 -0500 Subject: [PATCH] Add support for release channels in snow app version create (#1946) --- RELEASE-NOTES.md | 3 + .../nativeapp/entities/application_package.py | 31 +-- .../cli/_plugins/nativeapp/sf_sql_facade.py | 71 ++++- .../_plugins/nativeapp/version/commands.py | 26 +- tests/nativeapp/test_sf_sql_facade.py | 251 +++++++++++++++++- tests/nativeapp/test_version_create.py | 110 ++++---- tests/nativeapp/test_version_drop.py | 57 +--- tests/nativeapp/utils.py | 2 + .../nativeapp/__snapshots__/test_version.ambr | 4 +- tests_integration/nativeapp/test_version.py | 70 +++++ 10 files changed, 484 insertions(+), 141 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 739758cab1..d2b0da52b7 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -23,10 +23,13 @@ * `snow app release-directive list` * `snow app release-directive set` * `snow app release-directive unset` +* Add support for release channels feature in native app version creation/drop. +* `snow app version create` now returns version, patch, and label in JSON format. ## Fixes and improvements * Fixed crashes with older x86_64 Intel CPUs. * Fixed inability to add patches to lowercase quoted versions +* Fixes label being set to blank instead of None when not provided. # v3.2.0 diff --git a/src/snowflake/cli/_plugins/nativeapp/entities/application_package.py b/src/snowflake/cli/_plugins/nativeapp/entities/application_package.py index 35d5963f73..54c643a628 100644 --- a/src/snowflake/cli/_plugins/nativeapp/entities/application_package.py +++ b/src/snowflake/cli/_plugins/nativeapp/entities/application_package.py @@ -370,7 +370,7 @@ def action_version_create( force: bool, *args, **kwargs, - ): + ) -> VersionInfo: """ Create a version and/or patch for a new or existing application package. Always performs a deploy action before creating version or patch. @@ -453,12 +453,14 @@ def action_version_create( # Define a new version in the application package if not self.get_existing_version_info(resolved_version): self.add_new_version(version=resolved_version, label=resolved_label) - return # A new version created automatically has patch 0, we do not need to further increment the patch. + # A new version created automatically has patch 0, we do not need to further increment the patch. + return VersionInfo(resolved_version, 0, resolved_label) # Add a new patch to an existing (old) version - self.add_new_patch_to_version( + patch = self.add_new_patch_to_version( version=resolved_version, patch=resolved_patch, label=resolved_label ) + return VersionInfo(resolved_version, patch, resolved_label) def action_version_drop( self, @@ -537,14 +539,9 @@ def action_version_drop( raise typer.Exit(1) # Drop the version - sql_executor = get_sql_executor() - with sql_executor.use_role(self.role): - try: - sql_executor.execute_query( - f"alter application package {self.name} drop version {version}" - ) - except ProgrammingError as err: - raise err # e.g. version is referenced in a release directive(s) + get_snowflake_facade().drop_version_from_package( + package_name=self.name, version=version, role=self.role + ) console.message( f"Version {version} in application package {self.name} dropped successfully." @@ -846,9 +843,10 @@ def add_new_version(self, version: str, label: str | None = None) -> None: def add_new_patch_to_version( self, version: str, patch: int | None = None, label: str | None = None - ): + ) -> int: """ Add a new patch, optionally a custom one, to an existing version in an application package. + Returns the patch number of the newly created patch. """ console = self._workspace_ctx.console @@ -868,6 +866,7 @@ def add_new_patch_to_version( console.message( f"Patch {new_patch}{with_label_prompt} created for version {version} defined in application package {self.name}." ) + return new_patch def check_index_changes_in_git_repo( self, policy: PolicyBase, interactive: bool @@ -1134,7 +1133,7 @@ def resolve_version_info( bundle_map: BundleMap | None, policy: PolicyBase, interactive: bool, - ): + ) -> VersionInfo: """Determine version name, patch number, and label from CLI provided values and manifest.yml version entry. @param [Optional] version: version name as specified in the command @param [Optional] patch: patch number as specified in the command @@ -1142,12 +1141,14 @@ def resolve_version_info( @param [Optional] bundle_map: bundle_map if a deploy_root is prepared. _bundle() is performed otherwise. @param policy: CLI policy @param interactive: True if command is run in interactive mode, otherwise False + + @return VersionInfo: version_name, patch_number, label resolved from CLI and manifest.yml """ console = self._workspace_ctx.console resolved_version = None resolved_patch = None - resolved_label = "" + resolved_label = None # If version is specified in CLI, no version information from manifest.yml is used (except for comment, we can't control comment as of now). if version is not None: @@ -1155,7 +1156,7 @@ def resolve_version_info( "Ignoring version information from the application manifest since a version was explicitly specified with the command." ) resolved_patch = patch - resolved_label = label if label is not None else "" + resolved_label = label resolved_version = version # When version is not set by CLI, version name is read from manifest.yml. patch and label from CLI will be used, if provided. diff --git a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py index 9bbed2b4cc..f3c164f4a8 100644 --- a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +++ b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py @@ -53,6 +53,7 @@ NO_WAREHOUSE_SELECTED_IN_SESSION, RELEASE_DIRECTIVE_DOES_NOT_EXIST, RELEASE_DIRECTIVES_VERSION_PATCH_NOT_FOUND, + SQL_COMPILATION_ERROR, VERSION_DOES_NOT_EXIST, VERSION_NOT_ADDED_TO_RELEASE_CHANNEL, ) @@ -264,27 +265,62 @@ def create_version_in_package( @param [Optional] label: Label for this version, visible to consumers. """ - # Make the version a valid identifier, adding quotes if necessary version = to_identifier(version) + package_name = to_identifier(package_name) + + available_release_channels = self.show_release_channels(package_name, role) # Label must be a string literal - with_label_cause = ( - f"\nlabel={to_string_literal(label)}" if label is not None else "" + with_label_clause = ( + f"label={to_string_literal(label)}" if label is not None else "" ) - add_version_query = dedent( - f"""\ - alter application package {package_name} - add version {version} - using @{stage_fqn}{with_label_cause} - """ + + action = "register" if available_release_channels else "add" + + query = dedent( + _strip_empty_lines( + f"""\ + alter application package {package_name} + {action} version {version} + using @{stage_fqn} + {with_label_clause} + """ + ) ) + + with self._use_role_optional(role): + try: + self._sql_executor.execute_query(query) + except Exception as err: + handle_unclassified_error( + err, + f"Failed to {action} version {version} to application package {package_name}.", + ) + + def drop_version_from_package( + self, package_name: str, version: str, role: str | None = None + ): + """ + Drops a version from an existing application package. + @param package_name: Name of the application package to alter. + @param version: Version name to drop. + @param [Optional] role: Switch to this role while executing drop version. + """ + + version = to_identifier(version) + package_name = to_identifier(package_name) + + release_channels = self.show_release_channels(package_name, role) + action = "deregister" if release_channels else "drop" + + query = f"alter application package {package_name} {action} version {version}" with self._use_role_optional(role): try: - self._sql_executor.execute_query(add_version_query) + self._sql_executor.execute_query(query) except Exception as err: handle_unclassified_error( err, - f"Failed to add version {version} to application package {package_name}.", + f"Failed to {action} version {version} from application package {package_name}.", ) def add_patch_to_package_version( @@ -1085,6 +1121,10 @@ def show_release_channels( cursor_class=DictCursor, ) except ProgrammingError as err: + # TODO: Temporary check for syntax until UI Parameter is available in production + if err.errno == SQL_COMPILATION_ERROR: + # Release not out yet and param not out yet + return [] handle_unclassified_error( err, f"Failed to show release channels for application package {package_name}.", @@ -1095,8 +1135,15 @@ def show_release_channels( def _strip_empty_lines(text: str) -> str: """ Strips empty lines from the input string. + Preserves the new line at the end of the string if it exists. """ - return "\n".join(line for line in text.splitlines() if line.strip()) + all_lines = text.splitlines() + + # join all non-empty lines, but preserve the new line at the end if it exists + last_line = all_lines[-1] + other_lines = [line for line in all_lines[:-1] if line.strip()] + + return "\n".join(other_lines) + "\n" + last_line def _handle_release_directive_version_error( diff --git a/src/snowflake/cli/_plugins/nativeapp/version/commands.py b/src/snowflake/cli/_plugins/nativeapp/version/commands.py index ad1cd5f1c0..b7ad13c0c0 100644 --- a/src/snowflake/cli/_plugins/nativeapp/version/commands.py +++ b/src/snowflake/cli/_plugins/nativeapp/version/commands.py @@ -18,6 +18,7 @@ from typing import Optional import typer +from snowflake.cli._plugins.nativeapp.artifacts import VersionInfo from snowflake.cli._plugins.nativeapp.common_flags import ForceOption, InteractiveOption from snowflake.cli._plugins.nativeapp.v2_conversions.compat import ( force_project_definition_v2, @@ -29,7 +30,14 @@ ) from snowflake.cli.api.commands.snow_typer import SnowTyperFactory from snowflake.cli.api.entities.common import EntityActions -from snowflake.cli.api.output.types import CommandResult, MessageResult, QueryResult +from snowflake.cli.api.output.formats import OutputFormat +from snowflake.cli.api.output.types import ( + CommandResult, + MessageResult, + ObjectResult, + QueryResult, +) +from snowflake.cli.api.project.util import to_identifier app = SnowTyperFactory( name="version", @@ -78,7 +86,7 @@ def create( project_root=cli_context.project_root, ) package_id = options["package_entity_id"] - ws.perform_action( + result: VersionInfo = ws.perform_action( package_id, EntityActions.VERSION_CREATE, version=version, @@ -88,7 +96,19 @@ def create( interactive=interactive, skip_git_check=skip_git_check, ) - return MessageResult(f"Version create is now complete.") + + message = "Version create is now complete." + if cli_context.output_format == OutputFormat.JSON: + return ObjectResult( + { + "message": message, + "version": to_identifier(result.version_name), + "patch": result.patch_number, + "label": result.label, + } + ) + else: + return MessageResult(message) @app.command("list", requires_connection=True) diff --git a/tests/nativeapp/test_sf_sql_facade.py b/tests/nativeapp/test_sf_sql_facade.py index c0cb93c33f..8d8107249c 100644 --- a/tests/nativeapp/test_sf_sql_facade.py +++ b/tests/nativeapp/test_sf_sql_facade.py @@ -2355,7 +2355,7 @@ def test_given_basic_pkg_when_create_application_package_then_success( comment = {SPECIAL_COMMENT} distribution = {distribution} """ - ).strip() + ) ), ) ] @@ -2384,7 +2384,7 @@ def test_given_release_channels_when_create_application_package_then_success( distribution = {distribution} enable_release_channels = {str(enable_release_channels).lower()} """ - ).strip() + ) ), ) ] @@ -2416,7 +2416,7 @@ def test_given_programming_error_when_create_application_package_then_error( comment = {SPECIAL_COMMENT} distribution = {distribution} """ - ).strip() + ) ), ) ] @@ -2448,7 +2448,7 @@ def test_given_privilege_error_when_create_application_package_then_raise_priv_e comment = {SPECIAL_COMMENT} distribution = {distribution} """ - ).strip() + ) ), ) ] @@ -2902,7 +2902,7 @@ def test_set_release_directive_with_non_default_directive( accounts = ({",".join(target_accounts)}) version = "{version}" patch = {patch} """ - ).strip() + ) sql_facade.set_release_directive( package_name, @@ -2932,7 +2932,7 @@ def test_set_default_release_directive( set default release directive version = "{version}" patch = {patch} """ - ).strip() + ) sql_facade.set_release_directive( package_name, @@ -2963,7 +2963,7 @@ def test_set_release_directive_with_special_chars_in_names( accounts = ({",".join(target_accounts)}) version = "{version}" patch = {patch} """ - ).strip() + ) sql_facade.set_release_directive( package_name, @@ -2992,7 +2992,7 @@ def test_set_release_directive_no_release_channel( accounts = ({",".join(target_accounts)}) version = "{version}" patch = {patch} """ - ).strip() + ) sql_facade.set_release_directive( package_name, @@ -3020,7 +3020,7 @@ def test_set_default_release_directive_no_release_channel( set default release directive version = "{version}" patch = {patch} """ - ).strip() + ) sql_facade.set_release_directive( package_name, @@ -3104,7 +3104,7 @@ def test_modify_release_directive_with_non_default_directive( modify release directive {release_directive} version = "{version}" patch = {patch} """ - ).strip() + ) sql_facade.modify_release_directive( package_name, @@ -3132,7 +3132,7 @@ def test_modify_release_directive_with_default_directive( modify default release directive version = "{version}" patch = {patch} """ - ).strip() + ) sql_facade.modify_release_directive( package_name, @@ -3160,7 +3160,7 @@ def test_modify_release_directive_with_special_chars_in_names( modify release directive "{release_directive}" version = "{version}" patch = {patch} """ - ).strip() + ) sql_facade.modify_release_directive( package_name, @@ -3186,7 +3186,7 @@ def test_modify_release_directive_no_release_channel( modify release directive {release_directive} version = "{version}" patch = {patch} """ - ).strip() + ) sql_facade.modify_release_directive( package_name, @@ -3212,7 +3212,7 @@ def test_modify_default_release_directive_no_release_channel( modify default release directive version = "{version}" patch = {patch} """ - ).strip() + ) sql_facade.modify_release_directive( package_name, @@ -3271,3 +3271,226 @@ def test_modify_release_directive_errors( ) assert error_message in str(err) + + +@contextmanager +def mock_release_channels(facade, enabled): + with mock.patch.object( + facade, "show_release_channels" + ) as mock_show_release_channels: + mock_show_release_channels.return_value = ( + [{"name": "test_channel"}] if enabled else [] + ) + yield + + +@pytest.mark.parametrize("release_channels_enabled", [True, False]) +def test_create_version_in_package( + release_channels_enabled, mock_use_role, mock_execute_query +): + action = "register" if release_channels_enabled else "add" + package_name = "test_package" + version = "v1" + role = "test_role" + stage_fqn = f"{package_name}.app_src.stage" + + expected_use_objects = [ + (mock_use_role, mock.call(role)), + ] + expected_execute_query = [ + ( + mock_execute_query, + mock.call( + dedent( + f"""\ + alter application package {package_name} + {action} version {version} + using @{stage_fqn} + """ + ) + ), + ), + ] + + with mock_release_channels(sql_facade, release_channels_enabled): + with assert_in_context(expected_use_objects, expected_execute_query): + sql_facade.create_version_in_package( + package_name=package_name, + version=version, + role=role, + stage_fqn=stage_fqn, + ) + + +@pytest.mark.parametrize("release_channels_enabled", [True, False]) +@pytest.mark.parametrize("label", ["test_label", ""]) +def test_create_version_in_package_with_label( + label, release_channels_enabled, mock_use_role, mock_execute_query +): + action = "register" if release_channels_enabled else "add" + package_name = "test_package" + version = "v1" + role = "test_role" + stage_fqn = f"{package_name}.app_src.stage" + + expected_use_objects = [ + (mock_use_role, mock.call(role)), + ] + expected_execute_query = [ + ( + mock_execute_query, + mock.call( + dedent( + f"""\ + alter application package {package_name} + {action} version {version} + using @{stage_fqn} + label='{label}' + """ + ) + ), + ), + ] + + with mock_release_channels(sql_facade, release_channels_enabled): + with assert_in_context(expected_use_objects, expected_execute_query): + sql_facade.create_version_in_package( + package_name=package_name, + version=version, + role=role, + stage_fqn=stage_fqn, + label=label, + ) + + +@pytest.mark.parametrize("release_channels_enabled", [True, False]) +def test_create_version_with_special_characters( + release_channels_enabled, mock_use_role, mock_execute_query +): + action = "register" if release_channels_enabled else "add" + package_name = "test.package" + version = "v1.0" + role = "test_role" + stage_fqn = f"{package_name}.app_src.stage" + + expected_use_objects = [ + (mock_use_role, mock.call(role)), + ] + expected_execute_query = [ + ( + mock_execute_query, + mock.call( + dedent( + f"""\ + alter application package "{package_name}" + {action} version "{version}" + using @{stage_fqn} + """ + ) + ), + ), + ] + + with mock_release_channels(sql_facade, release_channels_enabled): + with assert_in_context(expected_use_objects, expected_execute_query): + sql_facade.create_version_in_package( + package_name=package_name, + version=version, + role=role, + stage_fqn=stage_fqn, + ) + + +@pytest.mark.parametrize("release_channels_enabled", [True, False]) +def test_create_version_in_package_with_error( + release_channels_enabled, mock_use_role, mock_execute_query +): + package_name = "test_package" + version = "v1" + role = "test_role" + stage_fqn = f"{package_name}.app_src.stage" + + mock_execute_query.side_effect = ProgrammingError() + + with mock_release_channels(sql_facade, release_channels_enabled): + with pytest.raises(InvalidSQLError): + sql_facade.create_version_in_package( + package_name=package_name, + version=version, + role=role, + stage_fqn=stage_fqn, + ) + + +@pytest.mark.parametrize("release_channels_enabled", [True, False]) +def test_drop_version_from_package( + release_channels_enabled, mock_use_role, mock_execute_query +): + action = "deregister" if release_channels_enabled else "drop" + package_name = "test_package" + version = "v1" + role = "test_role" + + expected_use_objects = [ + (mock_use_role, mock.call(role)), + ] + expected_execute_query = [ + ( + mock_execute_query, + mock.call( + f"alter application package {package_name} {action} version {version}" + ), + ), + ] + + with mock_release_channels(sql_facade, release_channels_enabled): + with assert_in_context(expected_use_objects, expected_execute_query): + sql_facade.drop_version_from_package( + package_name=package_name, version=version, role=role + ) + + +@pytest.mark.parametrize("release_channels_enabled", [True, False]) +def test_drop_version_from_package_with_special_characters( + release_channels_enabled, mock_use_role, mock_execute_query +): + action = "deregister" if release_channels_enabled else "drop" + package_name = "test.package" + version = "v1.0" + role = "test_role" + + expected_use_objects = [ + (mock_use_role, mock.call(role)), + ] + expected_execute_query = [ + ( + mock_execute_query, + mock.call( + f'alter application package "{package_name}" {action} version "{version}"' + ), + ), + ] + + with mock_release_channels(sql_facade, release_channels_enabled): + with assert_in_context(expected_use_objects, expected_execute_query): + sql_facade.drop_version_from_package( + package_name=package_name, version=version, role=role + ) + + +@pytest.mark.parametrize("available_release_channels", [[], [{"name": "test_channel"}]]) +def test_drop_version_from_package_with_error( + available_release_channels, mock_use_role, mock_execute_query +): + package_name = "test_package" + version = "v1" + role = "test_role" + + mock_execute_query.side_effect = ProgrammingError() + + with mock_release_channels(sql_facade, bool(available_release_channels)): + + with pytest.raises(InvalidSQLError): + sql_facade.drop_version_from_package( + package_name=package_name, version=version, role=role + ) diff --git a/tests/nativeapp/test_version_create.py b/tests/nativeapp/test_version_create.py index e035d50b65..fe70c0d6d6 100644 --- a/tests/nativeapp/test_version_create.py +++ b/tests/nativeapp/test_version_create.py @@ -45,6 +45,7 @@ APPLICATION_PACKAGE_ENTITY_MODULE, SQL_EXECUTOR_EXECUTE, SQL_FACADE, + SQL_FACADE_CREATE_VERSION, mock_execute_helper, mock_snowflake_yml_file_v2, ) @@ -134,38 +135,18 @@ def test_get_existing_release_direction_info( # Test add_new_version adds a new version to an app pkg correctly -@mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch(SQL_FACADE_CREATE_VERSION) @pytest.mark.parametrize( - ["version", "version_identifier"], - [("V1", "V1"), ("1.0.0", '"1.0.0"'), ('"1.0.0"', '"1.0.0"')], + "version", + ["V1", "1.0.0", '"1.0.0"'], ) def test_add_version( - mock_execute, temp_dir, mock_cursor, version, version_identifier, workspace_context + mock_create_version, + temp_dir, + mock_cursor, + version, + workspace_context, ): - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role package_role")), - ( - None, - mock.call( - dedent( - f"""\ - alter application package app_pkg - add version {version_identifier} - using @app_pkg.app_src.stage - """ - ), - ), - ), - (None, mock.call("use role old_role")), - ] - ) - mock_execute.side_effect = side_effects - current_working_directory = os.getcwd() create_named_file( file_name="snowflake.yml", @@ -178,7 +159,14 @@ def test_add_version( pkg_model: ApplicationPackageEntityModel = pd.entities["app_pkg"] pkg = ApplicationPackageEntity(pkg_model, workspace_context) pkg.add_new_version(version=version) - assert mock_execute.mock_calls == expected + + mock_create_version.assert_called_once_with( + package_name="app_pkg", + version=version, + stage_fqn=f"app_pkg.{pkg_model.stage}", + role="package_role", + label=None, + ) # Test add_new_patch_to_version adds an "auto-increment" patch to an existing version @@ -226,7 +214,9 @@ def test_add_new_patch_auto( pd = dm.project_definition pkg_model: ApplicationPackageEntityModel = pd.entities["app_pkg"] pkg = ApplicationPackageEntity(pkg_model, workspace_context) - pkg.add_new_patch_to_version(version=version) + result_patch = pkg.add_new_patch_to_version(version=version) + assert result_patch == 12 + assert mock_execute.mock_calls == expected @@ -275,7 +265,8 @@ def test_add_new_patch_custom( pd = dm.project_definition pkg_model: ApplicationPackageEntityModel = pd.entities["app_pkg"] pkg = ApplicationPackageEntity(pkg_model, workspace_context) - pkg.add_new_patch_to_version(version=version, patch=12) + result_patch = pkg.add_new_patch_to_version(version=version, patch=12) + assert result_patch == 12 assert mock_execute.mock_calls == expected @@ -429,13 +420,16 @@ def test_process_no_existing_release_directives_or_versions( contents=[mock_snowflake_yml_file_v2], ) - _version_create( + result = _version_create( version=version, patch=None, force=force, interactive=interactive, skip_git_check=False, ) # last three parameters do not matter here + + assert result == VersionInfo(version, 0, None) + mock_find_version.assert_not_called() mock_check_git.assert_called_once() mock_rd.assert_called_once() @@ -460,9 +454,7 @@ def test_process_no_existing_release_directives_or_versions( ) @mock.patch.object(ApplicationPackageEntity, "get_existing_version_info") @mock.patch.object(ApplicationPackageEntity, "add_new_version") -@mock.patch.object( - ApplicationPackageEntity, "add_new_patch_to_version", return_value=None -) +@mock.patch.object(ApplicationPackageEntity, "add_new_patch_to_version") @pytest.mark.parametrize("force", [True, False]) @pytest.mark.parametrize("interactive", [True, False]) def test_process_no_existing_release_directives_w_existing_version( @@ -493,14 +485,18 @@ def test_process_no_existing_release_directives_w_existing_version( dir_name=current_working_directory, contents=[mock_snowflake_yml_file_v2], ) + mock_add_patch.return_value = 12 - _version_create( + result = _version_create( version=version, patch=12, force=force, interactive=interactive, skip_git_check=False, ) # last three parameters do not matter here + + assert result == VersionInfo(version, 12, None) + mock_find_version.assert_not_called() mock_check_git.assert_called_once() mock_rd.assert_called_once() @@ -587,9 +583,7 @@ def test_process_existing_release_directives_user_does_not_proceed( @mock.patch.object( ApplicationPackageEntity, "get_existing_version_info", return_value=None ) -@mock.patch.object( - ApplicationPackageEntity, "add_new_patch_to_version", return_value=None -) +@mock.patch.object(ApplicationPackageEntity, "add_new_patch_to_version") @mock.patch.object(typer, "confirm", return_value=True) @pytest.mark.parametrize( "force, interactive", @@ -629,14 +623,18 @@ def test_process_existing_release_directives_w_existing_version_two( dir_name=current_working_directory, contents=[mock_snowflake_yml_file_v2], ) + mock_add_patch.return_value = 12 - _version_create( + result = _version_create( version=version, patch=12, force=force, interactive=interactive, skip_git_check=False, ) + + assert result == VersionInfo(version, 12, None) + mock_check_git.assert_called_once() mock_rd.assert_called_once() mock_deploy.assert_called_once() @@ -687,7 +685,7 @@ def test_manifest_version_info_not_used( ) ) - _version_create( + result = _version_create( version=version_cli, patch=None, label=None, @@ -696,12 +694,14 @@ def test_manifest_version_info_not_used( force=False, ) + assert result == VersionInfo(version_cli, 0, None) + mock_create_version.assert_called_with( role=role, package_name="app_pkg", stage_fqn=f"app_pkg.{stage}", version=version_cli, - label="", + label=None, ) mock_find_info_manifest.assert_not_called() @@ -724,7 +724,6 @@ def test_manifest_version_info_not_used( ) @mock.patch( f"{SQL_FACADE}.add_patch_to_package_version", - return_value=None, ) @pytest.mark.parametrize("label", [None, "some label"]) @pytest.mark.parametrize("patch", [None, 2, 7]) @@ -752,8 +751,9 @@ def test_manifest_patch_is_not_used( ), ) ) + mock_create_patch.return_value = patch or 0 - _version_create( + result = _version_create( version=version_cli, patch=patch, label=label, @@ -762,6 +762,8 @@ def test_manifest_patch_is_not_used( force=False, ) + assert result == VersionInfo(version_cli, patch or 0, label) + mock_create_patch.assert_called_with( role=role, package_name="app_pkg", @@ -769,7 +771,7 @@ def test_manifest_patch_is_not_used( version=version_cli, patch=patch, # ensure empty label is used to replace label from manifest.yml - label=label or "", + label=label, ) mock_find_info_manifest.assert_not_called() @@ -791,7 +793,6 @@ def test_manifest_patch_is_not_used( ) @mock.patch( f"{SQL_FACADE}.add_patch_to_package_version", - return_value=None, ) @pytest.mark.parametrize("manifest_label", [None, "some label", ""]) @pytest.mark.parametrize("manifest_patch", [None, 4]) @@ -826,9 +827,10 @@ def test_version_from_manifest( ), ) ) + mock_create_patch.return_value = manifest_patch # no version or patch through cli - _version_create( + result = _version_create( version=None, patch=None, label=cli_label, @@ -836,6 +838,9 @@ def test_version_from_manifest( interactive=True, force=False, ) + expected_label = cli_label if cli_label is not None else manifest_label + + assert result == VersionInfo("manifest_version", manifest_patch, expected_label) mock_create_patch.assert_called_with( role=role, @@ -843,7 +848,7 @@ def test_version_from_manifest( stage_fqn=f"app_pkg.{stage}", version="manifest_version", patch=manifest_patch, - label=cli_label if cli_label is not None else manifest_label, + label=expected_label, ) @@ -864,7 +869,6 @@ def test_version_from_manifest( ) @mock.patch( f"{SQL_FACADE}.add_patch_to_package_version", - return_value=None, ) @pytest.mark.parametrize("manifest_label", [None, "some label", ""]) @pytest.mark.parametrize("cli_label", [None, "", "cli label"]) @@ -898,9 +902,10 @@ def test_patch_from_manifest( ), ) ) + mock_create_patch.return_value = cli_patch # patch through cli, but no version - _version_create( + result = _version_create( version=None, patch=cli_patch, label=cli_label, @@ -911,6 +916,9 @@ def test_patch_from_manifest( console=mock_console, ) + expected_label = cli_label if cli_label is not None else manifest_label + assert result == VersionInfo("manifest_version", cli_patch, expected_label) + mock_create_patch.assert_called_with( role=role, package_name="app_pkg", @@ -918,7 +926,7 @@ def test_patch_from_manifest( version="manifest_version", # cli patch overrides the manifest patch=cli_patch, - label=cli_label if cli_label is not None else manifest_label, + label=expected_label, ) mock_console.warning.assert_called_with( f"Cannot resolve version. Found patch: {manifest_patch} in manifest.yml which is different from provided patch {cli_patch}." diff --git a/tests/nativeapp/test_version_drop.py b/tests/nativeapp/test_version_drop.py index 0877f4c66a..ba94fbdda2 100644 --- a/tests/nativeapp/test_version_drop.py +++ b/tests/nativeapp/test_version_drop.py @@ -39,9 +39,8 @@ from tests.nativeapp.utils import ( APP_PACKAGE_ENTITY_GET_EXISTING_APP_PKG_INFO, APPLICATION_PACKAGE_ENTITY_MODULE, - SQL_EXECUTOR_EXECUTE, + SQL_FACADE_DROP_VERSION, TYPER_CONFIRM, - mock_execute_helper, mock_snowflake_yml_file_v2, ) from tests.testing_utils.files_and_dirs import create_named_file @@ -199,41 +198,23 @@ def test_process_drop_cannot_complete( f"{APPLICATION_PACKAGE_ENTITY_MODULE}.find_version_info_in_manifest_file", return_value=VersionInfo("manifest_version", None, None), ) -@mock.patch(SQL_EXECUTOR_EXECUTE) @mock.patch( f"snowflake.cli._plugins.nativeapp.policy.{TYPER_CONFIRM}", return_value=True ) +@mock.patch(SQL_FACADE_DROP_VERSION) @pytest.mark.parametrize("force", [True, False]) def test_process_drop_from_manifest( + mock_drop_version, mock_typer_confirm, - mock_execute, mock_version_info_in_manifest, mock_build_bundle, mock_distribution, mock_get_existing, force, temp_dir, - mock_cursor, ): mock_distribution.return_value = "internal" - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role package_role")), - ( - None, - mock.call( - "alter application package app_pkg drop version manifest_version" - ), - ), - (None, mock.call("use role old_role")), - ] - ) - mock_execute.side_effect = side_effects current_working_directory = os.getcwd() create_named_file( @@ -243,7 +224,10 @@ def test_process_drop_from_manifest( ) _drop_version(version=None, force=force, interactive=True) - assert mock_execute.mock_calls == expected + + mock_drop_version.assert_called_once_with( + package_name="app_pkg", version="manifest_version", role="package_role" + ) @mock.patch( @@ -255,46 +239,28 @@ def test_process_drop_from_manifest( f"{APPLICATION_PACKAGE_ENTITY_MODULE}.ApplicationPackageEntity._bundle", return_value=None, ) -@mock.patch(SQL_EXECUTOR_EXECUTE) @mock.patch( f"snowflake.cli._plugins.nativeapp.policy.{TYPER_CONFIRM}", return_value=True ) +@mock.patch(SQL_FACADE_DROP_VERSION) @pytest.mark.parametrize("force", [True, False]) @pytest.mark.parametrize( ["version", "version_identifier"], [("V1", "V1"), ("1.0.0", '"1.0.0"'), ('"1.0.0"', '"1.0.0"')], ) def test_process_drop_specific_version( + mock_drop_version, mock_typer_confirm, - mock_execute, mock_build_bundle, mock_distribution, mock_get_existing, force, temp_dir, - mock_cursor, version, version_identifier, ): mock_distribution.return_value = "internal" - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role package_role")), - ( - None, - mock.call( - f"alter application package app_pkg drop version {version_identifier}" - ), - ), - (None, mock.call("use role old_role")), - ] - ) - mock_execute.side_effect = side_effects current_working_directory = os.getcwd() create_named_file( @@ -304,4 +270,7 @@ def test_process_drop_specific_version( ) _drop_version(version=version, force=force, interactive=True) - assert mock_execute.mock_calls == expected + + mock_drop_version.assert_called_once_with( + package_name="app_pkg", version=version_identifier, role="package_role" + ) diff --git a/tests/nativeapp/utils.py b/tests/nativeapp/utils.py index 7b9db67291..dcaae91080 100644 --- a/tests/nativeapp/utils.py +++ b/tests/nativeapp/utils.py @@ -94,6 +94,8 @@ SQL_FACADE_MODIFY_RELEASE_DIRECTIVE = f"{SQL_FACADE}.modify_release_directive" SQL_FACADE_UNSET_RELEASE_DIRECTIVE = f"{SQL_FACADE}.unset_release_directive" SQL_FACADE_SHOW_RELEASE_CHANNELS = f"{SQL_FACADE}.show_release_channels" +SQL_FACADE_DROP_VERSION = f"{SQL_FACADE}.drop_version_from_package" +SQL_FACADE_CREATE_VERSION = f"{SQL_FACADE}.create_version_in_package" mock_snowflake_yml_file = dedent( """\ diff --git a/tests_integration/nativeapp/__snapshots__/test_version.ambr b/tests_integration/nativeapp/__snapshots__/test_version.ambr index fcc3c0c6bc..4a7e2bde70 100644 --- a/tests_integration/nativeapp/__snapshots__/test_version.ambr +++ b/tests_integration/nativeapp/__snapshots__/test_version.ambr @@ -3,7 +3,7 @@ list([ dict({ 'comment': None, - 'label': '', + 'label': None, 'patch': 0, 'review_status': 'NOT_REVIEWED', 'state': 'READY', @@ -11,7 +11,7 @@ }), dict({ 'comment': None, - 'label': '', + 'label': None, 'patch': 1, 'review_status': 'NOT_REVIEWED', 'state': 'READY', diff --git a/tests_integration/nativeapp/test_version.py b/tests_integration/nativeapp/test_version.py index 61d8fb5fe9..3214457102 100644 --- a/tests_integration/nativeapp/test_version.py +++ b/tests_integration/nativeapp/test_version.py @@ -567,3 +567,73 @@ def test_nativeapp_version_create_quoted_identifiers( actual = runner.invoke_with_connection_json(["app", "version", "list"]) assert len(actual.json) == 0 + + +@pytest.mark.integration +def test_version_create_with_json_result(runner, nativeapp_project_directory): + with nativeapp_project_directory("napp_init_v2"): + result = runner.invoke_with_connection_json( + ["app", "version", "create", "v1", "--force", "--skip-git-check"] + ) + assert result.exit_code == 0 + assert result.json == { + "version": "v1", + "patch": 0, + "label": None, + "message": "Version create is now complete.", + } + + result = runner.invoke_with_connection_json( + [ + "app", + "version", + "create", + "v1", + "--force", + "--skip-git-check", + "--label", + "test", + ] + ) + assert result.exit_code == 0 + assert result.json == { + "version": "v1", + "patch": 1, + "label": "test", + "message": "Version create is now complete.", + } + + # try with custom patch: + result = runner.invoke_with_connection_json( + [ + "app", + "version", + "create", + "v1", + "--force", + "--skip-git-check", + "--patch", + 3, + "--label", + "", + ] + ) + assert result.exit_code == 0 + assert result.json == { + "version": "v1", + "patch": 3, + "label": "", + "message": "Version create is now complete.", + } + + # create version with special characters: + result = runner.invoke_with_connection_json( + ["app", "version", "create", "v1.1", "--force", "--skip-git-check"] + ) + assert result.exit_code == 0 + assert result.json == { + "version": '"v1.1"', + "patch": 0, + "label": None, + "message": "Version create is now complete.", + }