diff --git a/.github/workflows/build-snowflake-connector-python.yml b/.github/workflows/build-snowflake-connector-python.yml index ae217844ab..39bb197e22 100644 --- a/.github/workflows/build-snowflake-connector-python.yml +++ b/.github/workflows/build-snowflake-connector-python.yml @@ -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" @@ -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" @@ -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 diff --git a/docs/packages/snowflake-connector-python.yaml b/docs/packages/snowflake-connector-python.yaml index 9777767cda..51924d11e3 100644 --- a/docs/packages/snowflake-connector-python.yaml +++ b/docs/packages/snowflake-connector-python.yaml @@ -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 diff --git a/patches/snowflake-connector-python/4.7.3/0001-test-wiremock-raise-the-server-start-timeout-to-120s.patch b/patches/snowflake-connector-python/4.7.3/0001-test-wiremock-raise-the-server-start-timeout-to-120s.patch new file mode 100644 index 0000000000..55276dea10 --- /dev/null +++ b/patches/snowflake-connector-python/4.7.3/0001-test-wiremock-raise-the-server-start-timeout-to-120s.patch @@ -0,0 +1,33 @@ +From 9a08b490a7efc884676b5ad0ad8f582b1f596f4f Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +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 diff --git a/patches/snowflake-connector-python/4.7.3/0002-test-detect_platforms-raise-the-generous-timeout-fro.patch b/patches/snowflake-connector-python/4.7.3/0002-test-detect_platforms-raise-the-generous-timeout-fro.patch new file mode 100644 index 0000000000..9fa2353cc0 --- /dev/null +++ b/patches/snowflake-connector-python/4.7.3/0002-test-detect_platforms-raise-the-generous-timeout-fro.patch @@ -0,0 +1,167 @@ +From 450b09950bd41f428e1594560a7c70988d94b138 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +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 "_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 diff --git a/patches/snowflake-connector-python/4.7.3/0003-test-auth-fix-the-mock_cnt-race-that-test_auth_mfa-s.patch b/patches/snowflake-connector-python/4.7.3/0003-test-auth-fix-the-mock_cnt-race-that-test_auth_mfa-s.patch new file mode 100644 index 0000000000..aaa2f56930 --- /dev/null +++ b/patches/snowflake-connector-python/4.7.3/0003-test-auth-fix-the-mock_cnt-race-that-test_auth_mfa-s.patch @@ -0,0 +1,76 @@ +From 4cad97079ae16e3d32525884fa741aeb413d5808 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +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 + + diff --git a/patches/snowflake-connector-python/4.7.3/0004-test-util-raise-the-get_application_path-timing-regr.patch b/patches/snowflake-connector-python/4.7.3/0004-test-util-raise-the-get_application_path-timing-regr.patch new file mode 100644 index 0000000000..e26d2a96b6 --- /dev/null +++ b/patches/snowflake-connector-python/4.7.3/0004-test-util-raise-the-get_application_path-timing-regr.patch @@ -0,0 +1,41 @@ +From 8bcd734d265ee8fee0813631bc36509c8fbfc80c Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +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 + diff --git a/patches/snowflake-connector-python/4.7.3/0005-test-cache-sleep-before-the-final-mtime-advancing-sa.patch b/patches/snowflake-connector-python/4.7.3/0005-test-cache-sleep-before-the-final-mtime-advancing-sa.patch new file mode 100644 index 0000000000..5ff7d14f31 --- /dev/null +++ b/patches/snowflake-connector-python/4.7.3/0005-test-cache-sleep-before-the-final-mtime-advancing-sa.patch @@ -0,0 +1,43 @@ +From 27d6cc73a823c9df9e6bee17a4440aaae660313e Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Sat, 19 Sep 2026 16:52:55 +0000 +Subject: [PATCH] test: cache: sleep before the final mtime-advancing save in + test_file_is_not_updated + +test_file_is_not_updated writes the cache file three times and checks +os.path.getmtime() strictly advances each time a write happens. Before the +second write-then-assert-greater pair (line 557/561 upstream) the test +already sleeps 0.1s to guarantee the filesystem's mtime clock ticks forward +between writes. The third write-then-assert-greater pair (triggered by +advancing the mocked snowflake.connector.cache.now() past entry_lifetime) +has no such sleep, even though the write it precedes still hits the real +filesystem clock, not the mocked one. + +On this repo's riscv64 runners the two real-wall-clock writes can land +within the same mtime tick, making the final +`os.path.getmtime(tmp_cache_file) > second_updated_time` assertion fail +with equal values (e.g. run 35440238662, job +Build snowflake-connector-python 4.7.4 cp313-manylinux_riscv64). Add the +same 0.1s sleep used earlier in this test before the last write, matching +the established pattern. + +Upstream-Status: Inappropriate [riscv64 runner filesystem mtime resolution/timing is coarser than upstream's CI hosts; the test already sleeps before an analogous write two lines earlier but is missing the same sleep before this one] +--- + test/unit/test_cache.py | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/test/unit/test_cache.py b/test/unit/test_cache.py +index 78ab517..095caf6 100644 +--- a/test/unit/test_cache.py ++++ b/test/unit/test_cache.py +@@ -566,6 +566,7 @@ def test_file_is_not_updated(tmpdir): + assert os.path.getmtime(tmp_cache_file) == second_updated_time + # Advance mocked time past entry_lifetime to trigger expiry + mock_now.return_value = start_time + datetime.timedelta(seconds=2) ++ time.sleep(0.1) # sleep 0.1 to ensure mtime advances on the filesystem + sfcache.save() # this save call will dump cache because cache item is expired + assert not sfcache._cache_modified + assert os.path.getmtime(tmp_cache_file) > second_updated_time +-- +2.43.0 + diff --git a/patches/snowflake-connector-python/4.7.4/0001-test-wiremock-raise-the-server-start-timeout-to-120s.patch b/patches/snowflake-connector-python/4.7.4/0001-test-wiremock-raise-the-server-start-timeout-to-120s.patch new file mode 100644 index 0000000000..55276dea10 --- /dev/null +++ b/patches/snowflake-connector-python/4.7.4/0001-test-wiremock-raise-the-server-start-timeout-to-120s.patch @@ -0,0 +1,33 @@ +From 9a08b490a7efc884676b5ad0ad8f582b1f596f4f Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +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 diff --git a/patches/snowflake-connector-python/4.7.4/0002-test-detect_platforms-raise-the-generous-timeout-fro.patch b/patches/snowflake-connector-python/4.7.4/0002-test-detect_platforms-raise-the-generous-timeout-fro.patch new file mode 100644 index 0000000000..9fa2353cc0 --- /dev/null +++ b/patches/snowflake-connector-python/4.7.4/0002-test-detect_platforms-raise-the-generous-timeout-fro.patch @@ -0,0 +1,167 @@ +From 450b09950bd41f428e1594560a7c70988d94b138 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +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 "_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 diff --git a/patches/snowflake-connector-python/4.7.4/0003-test-auth-fix-the-mock_cnt-race-that-test_auth_mfa-s.patch b/patches/snowflake-connector-python/4.7.4/0003-test-auth-fix-the-mock_cnt-race-that-test_auth_mfa-s.patch new file mode 100644 index 0000000000..aaa2f56930 --- /dev/null +++ b/patches/snowflake-connector-python/4.7.4/0003-test-auth-fix-the-mock_cnt-race-that-test_auth_mfa-s.patch @@ -0,0 +1,76 @@ +From 4cad97079ae16e3d32525884fa741aeb413d5808 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +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 + + diff --git a/patches/snowflake-connector-python/4.7.4/0004-test-util-raise-the-get_application_path-timing-regr.patch b/patches/snowflake-connector-python/4.7.4/0004-test-util-raise-the-get_application_path-timing-regr.patch new file mode 100644 index 0000000000..85bcf405b6 --- /dev/null +++ b/patches/snowflake-connector-python/4.7.4/0004-test-util-raise-the-get_application_path-timing-regr.patch @@ -0,0 +1,41 @@ +From e861774346e0b2cd63cf6a48fc204fcfa829ad87 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Sat, 19 Sep 2026 11:28:05 +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 + diff --git a/patches/snowflake-connector-python/4.7.4/0005-test-cache-sleep-before-the-final-mtime-advancing-sa.patch b/patches/snowflake-connector-python/4.7.4/0005-test-cache-sleep-before-the-final-mtime-advancing-sa.patch new file mode 100644 index 0000000000..5ff7d14f31 --- /dev/null +++ b/patches/snowflake-connector-python/4.7.4/0005-test-cache-sleep-before-the-final-mtime-advancing-sa.patch @@ -0,0 +1,43 @@ +From 27d6cc73a823c9df9e6bee17a4440aaae660313e Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Sat, 19 Sep 2026 16:52:55 +0000 +Subject: [PATCH] test: cache: sleep before the final mtime-advancing save in + test_file_is_not_updated + +test_file_is_not_updated writes the cache file three times and checks +os.path.getmtime() strictly advances each time a write happens. Before the +second write-then-assert-greater pair (line 557/561 upstream) the test +already sleeps 0.1s to guarantee the filesystem's mtime clock ticks forward +between writes. The third write-then-assert-greater pair (triggered by +advancing the mocked snowflake.connector.cache.now() past entry_lifetime) +has no such sleep, even though the write it precedes still hits the real +filesystem clock, not the mocked one. + +On this repo's riscv64 runners the two real-wall-clock writes can land +within the same mtime tick, making the final +`os.path.getmtime(tmp_cache_file) > second_updated_time` assertion fail +with equal values (e.g. run 35440238662, job +Build snowflake-connector-python 4.7.4 cp313-manylinux_riscv64). Add the +same 0.1s sleep used earlier in this test before the last write, matching +the established pattern. + +Upstream-Status: Inappropriate [riscv64 runner filesystem mtime resolution/timing is coarser than upstream's CI hosts; the test already sleeps before an analogous write two lines earlier but is missing the same sleep before this one] +--- + test/unit/test_cache.py | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/test/unit/test_cache.py b/test/unit/test_cache.py +index 78ab517..095caf6 100644 +--- a/test/unit/test_cache.py ++++ b/test/unit/test_cache.py +@@ -566,6 +566,7 @@ def test_file_is_not_updated(tmpdir): + assert os.path.getmtime(tmp_cache_file) == second_updated_time + # Advance mocked time past entry_lifetime to trigger expiry + mock_now.return_value = start_time + datetime.timedelta(seconds=2) ++ time.sleep(0.1) # sleep 0.1 to ensure mtime advances on the filesystem + sfcache.save() # this save call will dump cache because cache item is expired + assert not sfcache._cache_modified + assert os.path.getmtime(tmp_cache_file) > second_updated_time +-- +2.43.0 +