Skip to content

Commit b95de49

Browse files
authored
SNOW-2878951 reduce rt tests duration (#650)
1 parent 646efbf commit b95de49

File tree

8 files changed

+101
-51
lines changed

8 files changed

+101
-51
lines changed

ci/test_linux.sh

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
# - This is the script that test_docker.sh runs inside of the docker container
88

99
PYTHON_VERSIONS="${1:-3.8 3.9 3.10 3.11 3.12 3.13 3.14}"
10+
# Python versions where pyarrow (required by pandas extra) is not available
11+
PANDAS_SKIP_VERSIONS="3.14"
1012
THIS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
1113
SQLALCHEMY_DIR="$(dirname "${THIS_DIR}")"
1214

@@ -19,7 +21,12 @@ for PYTHON_VERSION in ${PYTHON_VERSIONS}; do
1921
echo "[Info] Testing with ${PYTHON_VERSION}"
2022
SHORT_VERSION=$(python3 -c "print('${PYTHON_VERSION}'.replace('.', ''))")
2123
SQLALCHEMY_WHL=$(ls $SQLALCHEMY_DIR/dist/snowflake_sqlalchemy-*-py3-none-any.whl | sort -r | head -n 1)
22-
TEST_ENVLIST=fix_lint,py${SHORT_VERSION}-ci,py${SHORT_VERSION}-coverage,py${SHORT_VERSION}-pandas-ci,py${SHORT_VERSION}-pandas-coverage
24+
TEST_ENVLIST=fix_lint,py${SHORT_VERSION}-ci
25+
if [[ ! " ${PANDAS_SKIP_VERSIONS} " =~ " ${PYTHON_VERSION} " ]]; then
26+
TEST_ENVLIST="${TEST_ENVLIST},py${SHORT_VERSION}-pandas-ci"
27+
else
28+
echo "[Info] Skipping pandas tests for Python ${PYTHON_VERSION} (pyarrow not available)"
29+
fi
2330
echo "[Info] Running tox for ${TEST_ENVLIST}"
24-
python3 -m tox -e ${TEST_ENVLIST} --installpkg ${SQLALCHEMY_WHL}
31+
python3 -m tox -p auto -e ${TEST_ENVLIST} --installpkg ${SQLALCHEMY_WHL}
2532
done

pyproject.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ development = [
5555
"pytest-xdist",
5656
"pytz",
5757
"numpy",
58-
"mock",
5958
"syrupy",
6059
]
6160
pandas = ["snowflake-connector-python[pandas]"]
@@ -85,7 +84,11 @@ installer = "uv"
8584
[tool.hatch.envs.sa14]
8685
installer = "uv"
8786
builder = true
88-
extra-dependencies = ["SQLAlchemy>=1.4.19,<2.0.0", "pandas>=2.1.1,<2.2", "numpy<2"]
87+
extra-dependencies = [
88+
"SQLAlchemy>=1.4.19,<2.0.0",
89+
"pandas>=2.1.1,<2.2",
90+
"numpy<2",
91+
]
8992
features = ["development", "pandas"]
9093
python = "3.12"
9194

@@ -142,4 +145,5 @@ markers = [
142145
"external: tests that could but should only run on our external CI",
143146
"feature_max_lob_size: tests that could but should only run on our external CI",
144147
"feature_v20: tests that could but should only run on SqlAlchemy v20",
148+
"mypy: typing tests",
145149
]

tests/conftest.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,17 @@ def on_public_ci():
112112
return running_on_public_ci()
113113

114114

115+
@pytest.fixture()
116+
def default_warehouse(db_parameters, engine_testaccount):
117+
wh = db_parameters.get("warehouse")
118+
if wh is None:
119+
with engine_testaccount.connect() as conn:
120+
wh = conn.exec_driver_sql("SELECT CURRENT_WAREHOUSE()").scalar()
121+
if wh is None:
122+
pytest.fail("No warehouse configured for the current user/session")
123+
return wh
124+
125+
115126
@pytest.fixture(scope="function")
116127
def base_location(external_stage, engine_testaccount):
117128
unique_id = str(uuid.uuid4())

tests/custom_tables/test_create_dynamic_table.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@
1717
)
1818

1919

20-
def test_create_dynamic_table(engine_testaccount, db_parameters):
21-
warehouse = db_parameters.get("warehouse", "default")
20+
def test_create_dynamic_table(engine_testaccount, default_warehouse):
2221
metadata = MetaData()
2322
test_table_1 = Table(
2423
"test_table_1", metadata, Column("id", Integer), Column("name", String)
@@ -38,7 +37,7 @@ def test_create_dynamic_table(engine_testaccount, db_parameters):
3837
Column("id", Integer),
3938
Column("name", String),
4039
target_lag=(1, TimeUnit.HOURS),
41-
warehouse=warehouse,
40+
warehouse=default_warehouse,
4241
as_query="SELECT id, name from test_table_1;",
4342
refresh_mode=SnowflakeKeyword.FULL,
4443
)
@@ -58,9 +57,8 @@ def test_create_dynamic_table(engine_testaccount, db_parameters):
5857

5958

6059
def test_create_dynamic_table_without_dynamictable_class(
61-
engine_testaccount, db_parameters, snapshot
60+
engine_testaccount, default_warehouse, snapshot
6261
):
63-
warehouse = db_parameters.get("warehouse", "default")
6462
metadata = MetaData()
6563
test_table_1 = Table(
6664
"test_table_1", metadata, Column("id", Integer), Column("name", String)
@@ -79,7 +77,7 @@ def test_create_dynamic_table_without_dynamictable_class(
7977
metadata,
8078
Column("id", Integer),
8179
Column("name", String),
82-
snowflake_warehouse=warehouse,
80+
snowflake_warehouse=default_warehouse,
8381
snowflake_as_query="SELECT id, name from test_table_1;",
8482
prefixes=["DYNAMIC"],
8583
)
@@ -90,9 +88,8 @@ def test_create_dynamic_table_without_dynamictable_class(
9088

9189

9290
def test_create_dynamic_table_without_dynamictable_and_defined_options(
93-
engine_testaccount, db_parameters, snapshot
91+
engine_testaccount, default_warehouse, snapshot
9492
):
95-
warehouse = db_parameters.get("warehouse", "default")
9693
metadata = MetaData()
9794
test_table_1 = Table(
9895
"test_table_1", metadata, Column("id", Integer), Column("name", String)
@@ -113,7 +110,7 @@ def test_create_dynamic_table_without_dynamictable_and_defined_options(
113110
Column("name", String),
114111
snowflake_target_lag=TargetLagOption.create((1, TimeUnit.HOURS)),
115112
snowflake_warehouse=IdentifierOption.create(
116-
TableOptionKey.WAREHOUSE, warehouse
113+
TableOptionKey.WAREHOUSE, default_warehouse
117114
),
118115
snowflake_as_query=AsQueryOption.create("SELECT id, name from test_table_1;"),
119116
prefixes=["DYNAMIC"],

tests/custom_tables/test_reflect_dynamic_table.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
from snowflake.sqlalchemy.custom_commands import NoneType
88

99

10-
def test_simple_reflection_dynamic_table_as_table(engine_testaccount, db_parameters):
11-
warehouse = db_parameters.get("warehouse", "default")
10+
def test_simple_reflection_dynamic_table_as_table(
11+
engine_testaccount, default_warehouse
12+
):
1213
metadata = MetaData()
1314
test_table_1 = Table(
1415
"test_table_1", metadata, Column("id", Integer), Column("name", String)
@@ -24,7 +25,7 @@ def test_simple_reflection_dynamic_table_as_table(engine_testaccount, db_paramet
2425
create_table_sql = f"""
2526
CREATE DYNAMIC TABLE dynamic_test_table (id INT, name VARCHAR)
2627
TARGET_LAG = '20 minutes'
27-
WAREHOUSE = {warehouse}
28+
WAREHOUSE = {default_warehouse}
2829
AS SELECT id, name from test_table_1;
2930
"""
3031
with engine_testaccount.connect() as connection:
@@ -46,8 +47,9 @@ def test_simple_reflection_dynamic_table_as_table(engine_testaccount, db_paramet
4647
metadata.drop_all(engine_testaccount)
4748

4849

49-
def test_simple_reflection_without_options_loading(engine_testaccount, db_parameters):
50-
warehouse = db_parameters.get("warehouse", "default")
50+
def test_simple_reflection_without_options_loading(
51+
engine_testaccount, default_warehouse
52+
):
5153
metadata = MetaData()
5254
test_table_1 = Table(
5355
"test_table_1", metadata, Column("id", Integer), Column("name", String)
@@ -63,7 +65,7 @@ def test_simple_reflection_without_options_loading(engine_testaccount, db_parame
6365
create_table_sql = f"""
6466
CREATE DYNAMIC TABLE dynamic_test_table (id INT, name VARCHAR)
6567
TARGET_LAG = '20 minutes'
66-
WAREHOUSE = {warehouse}
68+
WAREHOUSE = {default_warehouse}
6769
AS SELECT id, name from test_table_1;
6870
"""
6971
with engine_testaccount.connect() as connection:

tests/test_core.py

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1898,31 +1898,39 @@ def test_normalize_name_empty_string_does_not_crash(engine_testaccount):
18981898
Note: Empty string column names are not tested because SQLAlchemy core
18991899
explicitly disallows empty column names (raises ArgumentError).
19001900
"""
1901-
schema = "test_normalize_empty"
1901+
schema = f"TEST_NORMALIZE_EMPTY_{random_string(5, choices=string.ascii_uppercase)}"
1902+
normalized_schema = schema.lower()
19021903
with engine_testaccount.connect() as conn:
19031904
conn.execute(text(f"CREATE OR REPLACE SCHEMA {schema}"))
1904-
conn.execute(text("CREATE OR REPLACE TABLE NORMAL_TABLE (ID INTEGER)"))
1905-
conn.execute(text('CREATE OR REPLACE TABLE "" (ID INTEGER, NAME STRING)'))
1905+
conn.execute(
1906+
text(f"CREATE OR REPLACE TABLE {schema}.NORMAL_TABLE (ID INTEGER)")
1907+
)
1908+
conn.execute(
1909+
text(f'CREATE OR REPLACE TABLE {schema}."" (ID INTEGER, NAME STRING)')
1910+
)
19061911

1907-
md = MetaData(schema=schema)
1908-
md.reflect(bind=engine_testaccount)
1912+
try:
1913+
md = MetaData(schema=normalized_schema)
1914+
md.reflect(bind=engine_testaccount)
19091915

1910-
table_keys = list(md.tables.keys())
1916+
table_keys = list(md.tables.keys())
19111917

1912-
assert any(
1913-
"normal_table" in key for key in table_keys
1914-
), f"Expected normal_table in {table_keys}"
1918+
assert any(
1919+
"normal_table" in key for key in table_keys
1920+
), f"Expected normal_table in {table_keys}"
19151921

1916-
empty_string_as_table_identifier = f"{schema}."
1917-
assert (
1918-
empty_string_as_table_identifier in table_keys
1919-
), f"Expected empty string table '{empty_string_as_table_identifier}' in {table_keys}"
1920-
1921-
empty_table = md.tables[empty_string_as_table_identifier]
1922-
assert empty_table.name == ""
1923-
col_names = [c.name.lower() for c in empty_table.columns]
1924-
assert "id" in col_names
1925-
assert "name" in col_names
1922+
empty_string_as_table_identifier = f"{normalized_schema}."
1923+
assert (
1924+
empty_string_as_table_identifier in table_keys
1925+
), f"Expected empty string table '{empty_string_as_table_identifier}' in {table_keys}"
1926+
1927+
empty_table = md.tables[empty_string_as_table_identifier]
1928+
assert empty_table.name == ""
1929+
col_names = [c.name.lower() for c in empty_table.columns]
1930+
assert "id" in col_names
1931+
assert "name" in col_names
1932+
finally:
1933+
conn.execute(text(f"DROP SCHEMA IF EXISTS {schema}"))
19261934

19271935

19281936
def test_empty_column_names_not_supported_by_sqlalchemy():

tests/test_decfloat.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import decimal
66
import re
7+
import sys
78
import warnings
89
from decimal import Decimal
910

@@ -161,6 +162,10 @@ def test_decfloat_str_conversion(self):
161162
class TestDECFLOATIntegration:
162163
"""Integration tests for DECFLOAT type against real Snowflake database."""
163164

165+
@pytest.mark.skipif(
166+
sys.version_info < (3, 9),
167+
reason="DECFLOAT requires snowflake-connector-python >= 3.14.1",
168+
)
164169
def test_decfloat_precision_with_enable_decfloat_parameter(self, request):
165170
"""Test that enable_decfloat dialect parameter sets decimal context.
166171
@@ -204,6 +209,10 @@ def test_decfloat_precision_with_enable_decfloat_parameter(self, request):
204209
), "enable_decfloat=True should preserve full precision"
205210
assert result == value_38_digits
206211

212+
@pytest.mark.skipif(
213+
sys.version_info < (3, 9),
214+
reason="DECFLOAT requires snowflake-connector-python >= 3.14.1",
215+
)
207216
def test_decfloat_precision_depends_on_decimal_context(self, engine_testaccount):
208217
"""Test that Python decimal context affects DECFLOAT precision from connector.
209218
@@ -275,6 +284,10 @@ def test_create_table_with_decfloat(self, engine_testaccount):
275284
finally:
276285
test_table.drop(engine_testaccount)
277286

287+
@pytest.mark.skipif(
288+
sys.version_info < (3, 9),
289+
reason="DECFLOAT requires snowflake-connector-python >= 3.14.1",
290+
)
278291
def test_insert_and_select_decfloat_values(self, engine_testaccount):
279292
"""Test inserting and selecting DECFLOAT values.
280293

tox.ini

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
[tox]
22
min_version = 4.0.0
33
envlist = fix_lint,
4-
py{38,39,310,311,312,313,314}{,-pandas},
4+
py{38,39,310,311,312,313,314},
5+
py{38,39,310,311,312,313}-pandas,
56
coverage,
67
skip_missing_interpreters = true
78

@@ -39,18 +40,25 @@ setenv =
3940
COVERAGE_FILE = {env:COVERAGE_FILE:{toxworkdir}/.coverage.{envname}}
4041
SQLALCHEMY_WARN_20 = 1
4142
ci: SNOWFLAKE_PYTEST_OPTS = -vvv --tb=long
42-
commands = pytest \
43-
{env:SNOWFLAKE_PYTEST_OPTS:} \
44-
--cov "snowflake.sqlalchemy" \
45-
--junitxml {toxworkdir}/junit_{envname}.xml \
46-
--ignore=tests/sqlalchemy_test_suite \
47-
-n8 \
48-
{posargs:tests}
49-
pytest {env:SNOWFLAKE_PYTEST_OPTS:} \
50-
--cov "snowflake.sqlalchemy" --cov-append \
51-
--junitxml {toxworkdir}/junit_{envname}.xml \
52-
-n8 \
53-
{posargs:tests/sqlalchemy_test_suite}
43+
commands =
44+
!pandas: pytest \
45+
!pandas: {env:SNOWFLAKE_PYTEST_OPTS:} \
46+
!pandas: --cov "snowflake.sqlalchemy" \
47+
!pandas: --junitxml {toxworkdir}/junit_{envname}.xml \
48+
!pandas: --ignore=tests/sqlalchemy_test_suite \
49+
!pandas: -n8 \
50+
!pandas: {posargs:tests}
51+
!pandas: pytest {env:SNOWFLAKE_PYTEST_OPTS:} \
52+
!pandas: --cov "snowflake.sqlalchemy" --cov-append \
53+
!pandas: --junitxml {toxworkdir}/junit_{envname}.xml \
54+
!pandas: {posargs:tests/sqlalchemy_test_suite}
55+
pandas: pytest \
56+
pandas: {env:SNOWFLAKE_PYTEST_OPTS:} \
57+
pandas: --cov "snowflake.sqlalchemy" --cov-append \
58+
pandas: --junitxml {toxworkdir}/junit_{envname}.xml \
59+
pandas: --ignore=tests/sqlalchemy_test_suite \
60+
pandas: -n8 \
61+
pandas: {posargs:tests/test_pandas.py tests/test_qmark.py tests/test_core.py::test_snowflake_sqlalchemy_as_valid_client_type}
5462

5563
[testenv:.pkg_external]
5664
deps = build

0 commit comments

Comments
 (0)