Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions .github/workflows/build-snowflake-connector-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ jobs:
matrix:
version: ${{ fromJSON(needs.setup.outputs.versions) }}
# Per-interpreter (not abi3): the Cython extension links the version-specific ABI.
python: ["cp312", "cp313", "cp314", "cp314t"]
# `include` entries that share no key with another axis merge into every
# combination in order, so without the `python` axis above the last entry
# here (cp314t) silently overwrote matrix.python for every version and the
# cp312/cp313/cp314 legs never ran.
include:
- python: "cp312"
pytest_dist: "-n auto --dist loadfile"
Expand Down Expand Up @@ -133,7 +138,10 @@ jobs:
# The first three -k names need a native minicore blob, which upstream
# ships for eight platforms but not riscv64; the last five chmod a path
# and expect the EACCES that root - which cibuildwheel runs the tests
# as - never gets.
# as - never gets. test_auth_keypair_ecdsa_unsupported_curve generates a
# SECP192R1 key as test setup; our riscv64 cryptography wheel's OpenSSL
# build has no legacy provider, so cryptography itself raises
# UnsupportedAlgorithm before the connector's own curve check runs.
CIBW_TEST_COMMAND: >-
python -c "from snowflake.connector.nanoarrow_arrow_iterator import PyArrowRowIterator" &&
pytest -m "unit and not sso and not pandas and not lambda and not aio"
Expand All @@ -144,7 +152,8 @@ jobs:
and not test_log_debug_config_file_parent_dir_permissions
and not test_read_only
and not test_config_file_inaccessible_path
and not test_put_error"
and not test_put_error
and not test_auth_keypair_ecdsa_unsupported_curve"
${{ matrix.pytest_dist }}
--ignore {package}/test/unit/aio
--ignore {package}/test/unit/test_ocsp.py
Expand Down
2 changes: 2 additions & 0 deletions docs/packages/snowflake-connector-python.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ versions:
- filename: snowflake_connector_python-4.7.2-cp314-cp314t-manylinux_2_39_riscv64.whl
sha256: 7528a56efc7172b8c4d4d92e727f9619f5a012707af67a7d622acd270e591e63
requires-python: '>=3.10'
- version: 4.7.3
- version: 4.7.4
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
From 9a08b490a7efc884676b5ad0ad8f582b1f596f4f Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
Date: Fri, 18 Sep 2026 06:44:38 +0000
Subject: [PATCH] test: wiremock: raise the server start timeout to 120s

WiremockClient waits WIREMOCK_START_MAX_RETRY_COUNT seconds (12) for the
standalone server to answer /__admin/health, then fails the test. On the
riscv64 runners a JVM start plus WireMock/Jetty init routinely exceeds that
when several pytest-xdist workers each spin up their own server, so ~10 of
the auth/oauth/redirect tests error out nondeterministically while the rest
of the same suite passes.

The loop returns as soon as the health check succeeds, so a larger ceiling
costs nothing on faster hardware.

Upstream-Status: Inappropriate [native runner specific]
---
test/test_utils/wiremock/wiremock_utils.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/test_utils/wiremock/wiremock_utils.py b/test/test_utils/wiremock/wiremock_utils.py
index 0e108b3d..1c487ce4 100644
--- a/test/test_utils/wiremock/wiremock_utils.py
+++ b/test/test_utils/wiremock/wiremock_utils.py
@@ -16,7 +16,7 @@ except ImportError:

# Total budget for a Wiremock instance to become usable: the JVM has to boot,
# report the ports it bound and answer the health endpoint within this time.
-WIREMOCK_START_TIMEOUT_SECONDS = 12
+WIREMOCK_START_TIMEOUT_SECONDS = 120
WIREMOCK_STOP_TIMEOUT_SECONDS = 10
# How long to wait between polls while waiting for the startup banner / health.
_WIREMOCK_START_POLL_INTERVAL_SECONDS = 0.1
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
From 450b09950bd41f428e1594560a7c70988d94b138 Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
Date: Fri, 18 Sep 2026 06:44:38 +0000
Subject: [PATCH] test: detect_platforms: raise the generous timeout from 1s to
30s

detect_platforms() runs its six probes in a ThreadPoolExecutor and labels
any that miss platform_detection_timeout_seconds with a "<name>_timeout"
suffix. The tests that want no timeout label at all pass 1 second, with an
inline comment saying the value is only there "to make sure no Thread-based
timeout messes the results".

On the riscv64 runners 1 second is not generous enough: has_aws_identity
builds a boto3 STS client, and botocore's first service-model load, running
alongside three other pytest-xdist workers, overshoots the budget. The
future is then cancelled and test_no_platforms_detected sees
['has_aws_identity_timeout'] instead of []. The four "not true" cases of
test_platform_detection_disable_env_var_values assert the same empty list
and are exposed the same way.

csp_helpers patches urllib3's HTTPConnection.request to raise ConnectTimeout
outright, so no probe ever waits on the network and a larger ceiling adds no
wall-clock time anywhere. Sites that deliberately exercise the timeout path
(None, 0, EXPECTED_MAX_TIMEOUT_FOR_PLATFORM_DETECTION) are left alone.

Upstream-Status: Inappropriate [native runner specific]
---
test/unit/test_detect_platforms.py | 36 +++++++++++++++---------------
1 file changed, 18 insertions(+), 18 deletions(-)

diff --git a/test/unit/test_detect_platforms.py b/test/unit/test_detect_platforms.py
index b9b2d4f4..66a35420 100644
--- a/test/unit/test_detect_platforms.py
+++ b/test/unit/test_detect_platforms.py
@@ -69,14 +69,14 @@ class TestDetectPlatforms:
self, unavailable_metadata_service_with_request_exception
):
result = detect_platforms(
- platform_detection_timeout_seconds=1
+ platform_detection_timeout_seconds=30
) # increase timeout to make sure no Thread-based timeout messes the results
assert result == []

def test_ec2_instance_detection(
self, unavailable_metadata_service_with_request_exception, fake_aws_environment
):
- result = detect_platforms(platform_detection_timeout_seconds=1)
+ result = detect_platforms(platform_detection_timeout_seconds=30)
assert "is_ec2_instance" in result

def test_aws_lambda_detection(
@@ -84,7 +84,7 @@ class TestDetectPlatforms:
unavailable_metadata_service_with_request_exception,
fake_aws_lambda_environment,
):
- result = detect_platforms(platform_detection_timeout_seconds=1)
+ result = detect_platforms(platform_detection_timeout_seconds=30)
assert "is_aws_lambda" in result

@pytest.mark.parametrize(
@@ -104,44 +104,44 @@ class TestDetectPlatforms:
fake_aws_environment,
arn,
):
- result = detect_platforms(platform_detection_timeout_seconds=1)
+ result = detect_platforms(platform_detection_timeout_seconds=30)
assert "has_aws_identity" in result

def test_azure_vm_detection(self, fake_azure_vm_metadata_service):
- result = detect_platforms(platform_detection_timeout_seconds=1)
+ result = detect_platforms(platform_detection_timeout_seconds=30)
assert "is_azure_vm" in result

def test_azure_function_detection(self, fake_azure_function_metadata_service):
- result = detect_platforms(platform_detection_timeout_seconds=1)
+ result = detect_platforms(platform_detection_timeout_seconds=30)
assert "is_azure_function" in result

def test_azure_function_with_managed_identity(
self, fake_azure_function_metadata_service
):
- result = detect_platforms(platform_detection_timeout_seconds=1)
+ result = detect_platforms(platform_detection_timeout_seconds=30)
assert "is_azure_function" in result
assert "has_azure_managed_identity" in result

def test_gce_vm_detection(self, fake_gce_metadata_service):
- result = detect_platforms(platform_detection_timeout_seconds=1)
+ result = detect_platforms(platform_detection_timeout_seconds=30)
assert "is_gce_vm" in result

def test_gce_cloud_run_service_detection(
self, fake_gce_cloud_run_service_metadata_service
):
- result = detect_platforms(platform_detection_timeout_seconds=1)
+ result = detect_platforms(platform_detection_timeout_seconds=30)
assert "is_gce_cloud_run_service" in result

def test_gce_cloud_run_job_detection(self, fake_gce_cloud_run_job_metadata_service):
- result = detect_platforms(platform_detection_timeout_seconds=1)
+ result = detect_platforms(platform_detection_timeout_seconds=30)
assert "is_gce_cloud_run_job" in result

def test_gcp_identity_detection(self, fake_gce_metadata_service):
- result = detect_platforms(platform_detection_timeout_seconds=1)
+ result = detect_platforms(platform_detection_timeout_seconds=30)
assert "has_gcp_identity" in result

def test_github_actions_detection(self, fake_github_actions_metadata_service):
- result = detect_platforms(platform_detection_timeout_seconds=1)
+ result = detect_platforms(platform_detection_timeout_seconds=30)
assert "is_github_action" in result

def test_multiple_platforms_detection(
@@ -150,7 +150,7 @@ class TestDetectPlatforms:
fake_github_actions_metadata_service,
fake_gce_cloud_run_service_metadata_service,
):
- result = detect_platforms(platform_detection_timeout_seconds=1)
+ result = detect_platforms(platform_detection_timeout_seconds=30)
assert "is_aws_lambda" in result
assert "has_aws_identity" in result
assert "is_github_action" in result
@@ -267,14 +267,14 @@ class TestDetectPlatforms:
arn,
):
fake_aws_environment.caller_identity = {"Arn": arn}
- result = detect_platforms(platform_detection_timeout_seconds=1)
+ result = detect_platforms(platform_detection_timeout_seconds=30)
assert "has_aws_identity" not in result

def test_missing_arn_handling(
self, unavailable_metadata_service_with_request_exception, fake_aws_environment
):
fake_aws_environment.caller_identity = {"UserId": "test-user"}
- result = detect_platforms(platform_detection_timeout_seconds=1)
+ result = detect_platforms(platform_detection_timeout_seconds=30)
assert "has_aws_identity" not in result

def test_azure_managed_identity_no_token_endpoint(
@@ -294,7 +294,7 @@ class TestDetectPlatforms:
self, unavailable_metadata_service_with_request_exception, fake_aws_environment
):
fake_aws_environment.instance_document = b""
- result = detect_platforms(platform_detection_timeout_seconds=1)
+ result = detect_platforms(platform_detection_timeout_seconds=30)
assert "is_ec2_instance" not in result

def test_aws_lambda_empty_task_root(
@@ -373,7 +373,7 @@ class TestDetectPlatforms:
):
"""Test that ENV_VAR_DISABLE_PLATFORM_DETECTION only disables when set to 'true' (case-insensitive)"""
with patch.dict(os.environ, {ENV_VAR_DISABLE_PLATFORM_DETECTION: env_value}):
- result = detect_platforms(platform_detection_timeout_seconds=1)
+ result = detect_platforms(platform_detection_timeout_seconds=30)
assert result == expected_result

def test_platform_detection_disabled_overrides_all_other_detection(
@@ -384,7 +384,7 @@ class TestDetectPlatforms:
):
"""Test that ENV_VAR_DISABLE_PLATFORM_DETECTION takes precedence over all detections"""
with patch.dict(os.environ, {ENV_VAR_DISABLE_PLATFORM_DETECTION: "true"}):
- result = detect_platforms(platform_detection_timeout_seconds=1)
+ result = detect_platforms(platform_detection_timeout_seconds=30)
assert result == _PLATFORM_DETECTION_DISABLED_RESULT
assert "is_aws_lambda" not in result
assert "is_github_action" not in result
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
From 4cad97079ae16e3d32525884fa741aeb413d5808 Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
Date: Fri, 18 Sep 2026 06:44:38 +0000
Subject: [PATCH] test: auth: fix the mock_cnt race that test_auth_mfa's
timeout case leaves behind

Auth.authenticate() runs the MFA wait in a daemon Thread and gives up on it
with t.join(timeout=timeout), so the request mock keeps running after the
call returns. test_auth_mfa's third case relies on exactly that: the mock's
mock_cnt == 1 branch sleeps 10 seconds, the test passes timeout=1, and the
join returns while that thread is still sleeping.

The abandoned thread then falls through to `mock_cnt += 1` at the bottom of
the mock, roughly nine seconds later. By that time the fourth case has
already set `mock_cnt = 2` to select the "data is None" response, so the
stray increment turns it into 3, the mock returns the empty else-branch
dict, and Auth.authenticate() raises KeyError: 'data' instead of the
snowflake.connector.errors.Error the test expects.

Whether the increment lands inside the fourth case is pure timing. It never
does on a fast machine, which is why this only shows up where the fourth
case's setup takes longer than the rest of the sleep. It is the same race
the test is already skipped for on Windows.

Read and advance mock_cnt in one step at the top of the mock instead, before
the sleep, so a call's counter effect is complete before the caller can walk
away from it. Branch selection is unchanged: calls still see 0, 1, 2, ... in
order.

Reproducible anywhere by inserting `time.sleep(11)` after the fourth case's
`mock_cnt = 2`: KeyError: 'data' before this change, passing after.

Upstream-Status: To upstream [not submitted yet; the race is upstream's own, not riscv64-specific -- the existing IS_WINDOWS skip documents it]
---
test/unit/test_auth.py | 12 ++++++++----
1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/test/unit/test_auth.py b/test/unit/test_auth.py
index fdb2a998..7b3fbe63 100644
--- a/test/unit/test_auth.py
+++ b/test/unit/test_auth.py
@@ -132,7 +132,12 @@ def _mock_auth_mfa_rest_response_timeout(url, headers, body, **kwargs):
_ = headers
_ = body
_ = kwargs.get("dummy")
- if mock_cnt == 0:
+ # Advance the shared counter up front: the sleeping branch below is reached
+ # from a daemon thread that Auth.authenticate() abandons on timeout, and a
+ # post-sleep increment lands after the caller has reset mock_cnt for the
+ # next case.
+ call_cnt, mock_cnt = mock_cnt, mock_cnt + 1
+ if call_cnt == 0:
ret = {
"success": True,
"message": None,
@@ -141,10 +146,10 @@ def _mock_auth_mfa_rest_response_timeout(url, headers, body, **kwargs):
"inFlightCtx": "inFlightCtx",
},
}
- elif mock_cnt == 1:
+ elif call_cnt == 1:
time.sleep(10) # should timeout while here
ret = {}
- elif mock_cnt == 2:
+ elif call_cnt == 2:
ret = {
"success": True,
"message": None,
@@ -153,7 +158,6 @@ def _mock_auth_mfa_rest_response_timeout(url, headers, body, **kwargs):
else:
ret = {}

- mock_cnt += 1
return ret


Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
From 8bcd734d265ee8fee0813631bc36509c8fbfc80c Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
Date: Sat, 19 Sep 2026 11:27:54 +0000
Subject: [PATCH] test: util: raise the get_application_path timing-regression
threshold from 1ms to 10ms

test_get_application_path_is_fast_on_deep_call_stacks asserts the average
wall-clock time of get_application_path() at a 150-frame-deep call stack
stays under a hardcoded 1.0ms, guarding against a regression to an
inspect.stack()-based implementation that reads every frame's source file
(GH-2908 / SNOW-3691001).

On this repo's shared riscv64 runners the same frame-walking implementation
averages 2.286ms (observed on Build 4.7.4 cp312), 2.3x the threshold, with
no regression to the slow inspect.stack() path: it is plain per-instruction
cost on this architecture plus load from other concurrent matrix jobs on
the same shared runner pool. Raise the ceiling to 10ms, well above the one
observed sample, while still catching the ~100-500ms regression the test
was written to guard against.

Upstream-Status: Inappropriate [riscv64 runner is slower per-instruction than upstream's CI hosts, and shared with concurrent matrix jobs; the 1ms threshold assumes faster/dedicated hardware]
---
test/unit/test_util.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/unit/test_util.py b/test/unit/test_util.py
index eb99987..bab07c5 100644
--- a/test/unit/test_util.py
+++ b/test/unit/test_util.py
@@ -34,7 +34,7 @@ def test_get_application_path_is_fast_on_deep_call_stacks():
"""
depth = 150
n_runs = 5
- threshold_ms = 1.0
+ threshold_ms = 10.0

def recurse(n):
if n == 0:
--
2.43.0

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
From 9a08b490a7efc884676b5ad0ad8f582b1f596f4f Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
Date: Fri, 18 Sep 2026 06:44:38 +0000
Subject: [PATCH] test: wiremock: raise the server start timeout to 120s

WiremockClient waits WIREMOCK_START_MAX_RETRY_COUNT seconds (12) for the
standalone server to answer /__admin/health, then fails the test. On the
riscv64 runners a JVM start plus WireMock/Jetty init routinely exceeds that
when several pytest-xdist workers each spin up their own server, so ~10 of
the auth/oauth/redirect tests error out nondeterministically while the rest
of the same suite passes.

The loop returns as soon as the health check succeeds, so a larger ceiling
costs nothing on faster hardware.

Upstream-Status: Inappropriate [native runner specific]
---
test/test_utils/wiremock/wiremock_utils.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/test_utils/wiremock/wiremock_utils.py b/test/test_utils/wiremock/wiremock_utils.py
index 0e108b3d..1c487ce4 100644
--- a/test/test_utils/wiremock/wiremock_utils.py
+++ b/test/test_utils/wiremock/wiremock_utils.py
@@ -16,7 +16,7 @@ except ImportError:

# Total budget for a Wiremock instance to become usable: the JVM has to boot,
# report the ports it bound and answer the health endpoint within this time.
-WIREMOCK_START_TIMEOUT_SECONDS = 12
+WIREMOCK_START_TIMEOUT_SECONDS = 120
WIREMOCK_STOP_TIMEOUT_SECONDS = 10
# How long to wait between polls while waiting for the startup banner / health.
_WIREMOCK_START_POLL_INTERVAL_SECONDS = 0.1
Loading
Loading