Skip to content

Commit

Permalink
Append SNOWFLAKE_CLI_TEST_RESOURCE_SUFFIX value to app and package na…
Browse files Browse the repository at this point in the history
…me (#1443)

Prerequisite for #1437. After discussing in-person, we decided to not overwrite the `USER` environment variable in tests and instead add a new `SNOWFLAKE_CLI_TEST_RESOURCE_SUFFIX` env var that will be appended to app package and app identifiers. In #1437, we'll use this variable to generate unique names for these resources so they don't clash during concurrent tests.
  • Loading branch information
sfc-gh-fcampbell authored Aug 14, 2024
1 parent 7ed0a38 commit 760d5f8
Show file tree
Hide file tree
Showing 4 changed files with 288 additions and 10 deletions.
32 changes: 25 additions & 7 deletions src/snowflake/cli/_plugins/nativeapp/project_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from __future__ import annotations

import os
from functools import cached_property
from pathlib import Path
from typing import List, Optional
Expand All @@ -31,9 +32,15 @@
)
from snowflake.cli.api.project.schemas.native_app.native_app import NativeApp
from snowflake.cli.api.project.schemas.native_app.path_mapping import PathMapping
from snowflake.cli.api.project.util import extract_schema, to_identifier
from snowflake.cli.api.project.util import (
concat_identifiers,
extract_schema,
to_identifier,
)
from snowflake.connector import DictCursor

RESOURCE_SUFFIX_VAR = "SNOWFLAKE_CLI_TEST_RESOURCE_SUFFIX"


def current_role() -> str:
conn = get_cli_context().connection
Expand Down Expand Up @@ -129,12 +136,13 @@ def project_identifier(self) -> str:
# sometimes strip out double quotes, so we try to get them back here.
return to_identifier(self.definition.name)

@cached_property
@property
def package_name(self) -> str:
if self.definition.package and self.definition.package.name:
return to_identifier(self.definition.package.name)
name = self.definition.package.name
else:
return to_identifier(default_app_package(self.project_identifier))
name = default_app_package(self.project_identifier)
return concat_identifiers([name, resource_suffix()])

@cached_property
def package_role(self) -> str:
Expand All @@ -150,12 +158,13 @@ def package_distribution(self) -> str:
else:
return "internal"

@cached_property
@property
def app_name(self) -> str:
if self.definition.application and self.definition.application.name:
return to_identifier(self.definition.application.name)
name = to_identifier(self.definition.application.name)
else:
return to_identifier(default_application(self.project_identifier))
name = to_identifier(default_application(self.project_identifier))
return concat_identifiers([name, resource_suffix()])

@cached_property
def app_role(self) -> str:
Expand Down Expand Up @@ -206,3 +215,12 @@ def get_bundle_context(self) -> BundleContext:
deploy_root=self.deploy_root,
generated_root=self.generated_root,
)


def resource_suffix() -> str:
"""
A suffix that should be added to account-level Native App resources.
This is an internal concern that is currently only used in tests.
"""
return os.environ.get(RESOURCE_SUFFIX_VAR, "")
89 changes: 86 additions & 3 deletions tests/nativeapp/test_project_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@
import pytest
import yaml
from snowflake.cli._plugins.nativeapp.bundle_context import BundleContext
from snowflake.cli._plugins.nativeapp.project_model import NativeAppProjectModel
from snowflake.cli.api.project.definition import default_app_package, load_project
from snowflake.cli._plugins.nativeapp.project_model import (
RESOURCE_SUFFIX_VAR,
NativeAppProjectModel,
)
from snowflake.cli.api.project.definition import load_project
from snowflake.cli.api.project.schemas.native_app.application import SqlScriptHookType
from snowflake.cli.api.project.schemas.native_app.path_mapping import PathMapping
from snowflake.cli.api.project.schemas.project_definition import (
Expand Down Expand Up @@ -79,6 +82,31 @@ def test_project_model_all_defaults(
assert project.debug_mode is None


@pytest.mark.parametrize("project_definition_files", ["minimal"], indirect=True)
@mock.patch("snowflake.cli._app.snow_connector.connect_to_snowflake")
@mock.patch.dict(
os.environ,
{"USER": "test_user", RESOURCE_SUFFIX_VAR: "_suffix!"},
clear=True,
)
def test_project_model_default_package_app_name_with_suffix(
mock_connect, project_definition_files: List[Path], mock_ctx
):
ctx = mock_ctx()
mock_connect.return_value = ctx

project_defn = load_project(project_definition_files).project_definition

project_dir = Path().resolve()
project = NativeAppProjectModel(
project_definition=project_defn.native_app,
project_root=project_dir,
)

assert project.package_name == '"minimal_pkg_test_user_suffix!"'
assert project.app_name == '"minimal_test_user_suffix!"'


@mock.patch("snowflake.cli._app.snow_connector.connect_to_snowflake")
@mock.patch.dict(os.environ, {"USER": "test_user"}, clear=True)
def test_project_model_all_explicit(mock_connect, mock_ctx):
Expand Down Expand Up @@ -153,6 +181,61 @@ def test_project_model_all_explicit(mock_connect, mock_ctx):
assert project.debug_mode is False


@pytest.mark.parametrize("project_definition_files", ["minimal"], indirect=True)
@mock.patch("snowflake.cli._app.snow_connector.connect_to_snowflake")
@mock.patch.dict(
os.environ,
{"USER": "test_user", RESOURCE_SUFFIX_VAR: "_suffix!"},
clear=True,
)
def test_project_model_explicit_package_app_name_with_suffix(
mock_connect, project_definition_files: List[Path], mock_ctx
):
ctx = mock_ctx()
mock_connect.return_value = ctx

project_defition_file_yml = dedent(
f"""
definition_version: 1.1
native_app:
name: minimal
artifacts:
- setup.sql
- README.md
package:
name: minimal_test_pkg
role: PkgRole
distribution: external
warehouse: PkgWarehouse
scripts:
- scripts/package_setup.sql
application:
name: minimal_test_app
warehouse: AppWarehouse
role: AppRole
debug: false
post_deploy:
- sql_script: scripts/app_setup.sql
"""
)

project_defn = build_project_definition(
**yaml.load(project_defition_file_yml, Loader=yaml.BaseLoader)
)
project_dir = Path().resolve()
project = NativeAppProjectModel(
project_definition=project_defn.native_app,
project_root=project_dir,
)

assert project.package_name == '"minimal_test_pkg_suffix!"'
assert project.app_name == '"minimal_test_app_suffix!"'


@pytest.mark.parametrize("project_definition_files", ["minimal"], indirect=True)
@mock.patch("snowflake.cli._app.snow_connector.connect_to_snowflake")
@mock.patch.dict(os.environ, {"USER": "test_user"}, clear=True)
Expand Down Expand Up @@ -188,7 +271,7 @@ def test_bundle_context_from_project_model(project_definition_files: List[Path])
actual_bundle_ctx = project.get_bundle_context()

expected_bundle_ctx = BundleContext(
package_name=default_app_package("minimal"),
package_name=project.package_name,
artifacts=[
PathMapping(src="setup.sql", dest=None),
PathMapping(src="README.md", dest=None),
Expand Down
82 changes: 82 additions & 0 deletions tests_integration/nativeapp/test_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import os
import uuid

from snowflake.cli._plugins.nativeapp.project_model import RESOURCE_SUFFIX_VAR
from snowflake.cli.api.project.util import generate_user_env


Expand Down Expand Up @@ -112,6 +113,87 @@ def test_nativeapp_deploy(
assert result.exit_code == 0


@pytest.mark.integration
@enable_definition_v2_feature_flag
@pytest.mark.parametrize("test_project", ["napp_init_v1", "napp_init_v2"])
def test_nativeapp_deploy_with_resource_suffix(
test_project,
project_directory,
runner,
snowflake_session,
):
suffix = f"_some_suffix_{uuid.uuid4().hex}"
test_env_with_suffix = TEST_ENV | {RESOURCE_SUFFIX_VAR: suffix}
with project_directory(test_project):
result = runner.invoke_with_connection(
["app", "deploy"],
env=test_env_with_suffix,
)
assert result.exit_code == 0

try:
# package exist
assert row_from_snowflake_session(
snowflake_session.execute_string(
f"show application packages like '%{suffix}'",
)
)

# make sure we always delete the app
result = runner.invoke_with_connection_json(
["app", "teardown"],
env=test_env_with_suffix,
)
assert result.exit_code == 0
finally:
# teardown is idempotent, so we can execute it again with no ill effects
result = runner.invoke_with_connection_json(
["app", "teardown", "--force"],
env=test_env_with_suffix,
)
assert result.exit_code == 0


@pytest.mark.integration
@enable_definition_v2_feature_flag
@pytest.mark.parametrize("test_project", ["napp_init_v1", "napp_init_v2"])
def test_nativeapp_deploy_with_resource_suffix_quoted(
test_project,
project_directory,
runner,
snowflake_session,
):
suffix = f"_must.be.quoted!!!_{uuid.uuid4().hex}"
test_env_with_quoted_suffix = TEST_ENV | {RESOURCE_SUFFIX_VAR: suffix}
with project_directory(test_project):
result = runner.invoke_with_connection(
["app", "deploy"],
env=test_env_with_quoted_suffix,
)
assert result.exit_code == 0

try:
# package exist
assert row_from_snowflake_session(
snowflake_session.execute_string(
f"show application packages like '%{suffix}'",
)
)
# make sure we always delete the app
result = runner.invoke_with_connection_json(
["app", "teardown"],
env=test_env_with_quoted_suffix,
)
assert result.exit_code == 0
finally:
# teardown is idempotent, so we can execute it again with no ill effects
result = runner.invoke_with_connection_json(
["app", "teardown", "--force"],
env=test_env_with_quoted_suffix,
)
assert result.exit_code == 0


@pytest.mark.integration
@enable_definition_v2_feature_flag
@pytest.mark.parametrize(
Expand Down
95 changes: 95 additions & 0 deletions tests_integration/nativeapp/test_init_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import os
import uuid

from snowflake.cli._plugins.nativeapp.project_model import RESOURCE_SUFFIX_VAR
from snowflake.cli.api.project.util import generate_user_env
from snowflake.cli.api.secure_path import SecurePath
from snowflake.cli._plugins.nativeapp.init import OFFICIAL_TEMPLATES_GITHUB_URL
Expand Down Expand Up @@ -90,6 +91,100 @@ def test_nativeapp_init_run_without_modifications(
assert result.exit_code == 0


@pytest.mark.integration
@enable_definition_v2_feature_flag
@pytest.mark.parametrize("test_project", ["napp_init_v1", "napp_init_v2"])
def test_nativeapp_init_run_with_resource_suffix(
test_project,
project_directory,
runner,
snowflake_session,
):
suffix = f"_some_suffix_{uuid.uuid4().hex}"
test_env_with_suffix = TEST_ENV | {RESOURCE_SUFFIX_VAR: suffix}
with project_directory(test_project):
result = runner.invoke_with_connection_json(
["app", "run"],
env=test_env_with_suffix,
)
assert result.exit_code == 0

try:
# app + package exist
assert row_from_snowflake_session(
snowflake_session.execute_string(
f"show application packages like '%{suffix}'",
)
)
assert row_from_snowflake_session(
snowflake_session.execute_string(
f"show applications like '%{suffix}'",
)
)

# make sure we always delete the app
result = runner.invoke_with_connection_json(
["app", "teardown"],
env=test_env_with_suffix,
)
assert result.exit_code == 0

finally:
# teardown is idempotent, so we can execute it again with no ill effects
result = runner.invoke_with_connection_json(
["app", "teardown", "--force"],
env=test_env_with_suffix,
)
assert result.exit_code == 0


@pytest.mark.integration
@enable_definition_v2_feature_flag
@pytest.mark.parametrize("test_project", ["napp_init_v1", "napp_init_v2"])
def test_nativeapp_init_run_with_resource_suffix_quoted(
test_project,
project_directory,
runner,
snowflake_session,
):
suffix = f"_must.be.quoted!!!_{uuid.uuid4().hex}"
test_env_with_quoted_suffix = TEST_ENV | {RESOURCE_SUFFIX_VAR: suffix}
with project_directory(test_project):
result = runner.invoke_with_connection_json(
["app", "run"],
env=test_env_with_quoted_suffix,
)
assert result.exit_code == 0

try:
# app + package exist
assert row_from_snowflake_session(
snowflake_session.execute_string(
f"show application packages like '%{suffix}'",
)
)
assert row_from_snowflake_session(
snowflake_session.execute_string(
f"show applications like '%{suffix}'",
)
)

# make sure we always delete the app
result = runner.invoke_with_connection_json(
["app", "teardown"],
env=test_env_with_quoted_suffix,
)
assert result.exit_code == 0

finally:
# teardown is idempotent, so we can execute it again with no ill effects
result = runner.invoke_with_connection_json(
["app", "teardown", "--force"],
env=test_env_with_quoted_suffix,
)
assert result.exit_code == 0


# Tests a simple flow of an existing project, but executing snow app run and teardown, all with distribution=internal
@pytest.mark.integration
@enable_definition_v2_feature_flag
Expand Down

0 comments on commit 760d5f8

Please sign in to comment.