Skip to content

Commit 2695900

Browse files
jahnvi480Copilot
andcommitted
CHORE: Keep fetch tests focused on driver coverage
Consolidate essential fetch regressions into existing suites and remove PR-added benchmark-only and unified-coverage-skipped tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 4dbfb55 commit 2695900

5 files changed

Lines changed: 846 additions & 3044 deletions

‎tests/test_017_fetch_bounded_text.py‎

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@
77
belongs to the separate LOB decoder and is not covered by this regression.
88
"""
99

10+
import os
11+
import subprocess
1012
import sys
13+
import textwrap
1114

1215
import pytest
1316

14-
from mssql_python import SQL_CHAR, SQL_WCHAR
17+
from mssql_python import SQL_CHAR, SQL_WCHAR, ddbc_bindings as ddbc
1518

1619

1720
@pytest.fixture(scope="module")
@@ -294,3 +297,122 @@ def getdata(handle, column, ctype, buffer, capacity, indicator):
294297
check=False,
295298
)
296299
assert result.returncode == 0, result.stdout + result.stderr
300+
301+
302+
def _run_fetch_script(conn_str, script, *arguments, timeout=45):
303+
result = subprocess.run(
304+
[sys.executable, "-c", script, *arguments],
305+
env={**os.environ, "DB_CONNECTION_STRING": conn_str},
306+
capture_output=True,
307+
text=True,
308+
timeout=timeout,
309+
check=False,
310+
)
311+
assert result.returncode == 0, result.stdout + result.stderr
312+
313+
314+
@pytest.mark.parametrize("size", (1, 2))
315+
def test_getdata_decode_error_keeps_preexisting_and_completed_cells(cursor, size):
316+
cursor.execute("SELECT 7, CAST(0x00D8 AS NVARCHAR(1))")
317+
assert ddbc.DDBCSQLFetch(cursor.hstmt) == 0
318+
marker = object()
319+
row = [marker]
320+
if size == 1:
321+
assert ddbc.DDBCSQLGetData(cursor.hstmt, size, row, "utf-16le", "utf-16le", -8) == 0
322+
else:
323+
with pytest.raises(UnicodeDecodeError):
324+
ddbc.DDBCSQLGetData(cursor.hstmt, size, row, "utf-16le", "utf-16le", -8)
325+
assert row == [marker, 7]
326+
cursor.execute("SELECT 42")
327+
assert cursor.fetchval() == 42
328+
329+
330+
@pytest.mark.skipif(
331+
sys.platform == "win32",
332+
reason="Windows does not export the native driver function-pointer globals",
333+
)
334+
@pytest.mark.parametrize("payload", ("utf8", "empty", "null", "invalid", "warning"))
335+
def test_narrow_getdata_decoding_in_subprocess(conn_str, payload):
336+
script = textwrap.dedent("""
337+
import ctypes as c
338+
import os
339+
import sys
340+
import mssql_python
341+
from mssql_python import ddbc_bindings as ddbc
342+
343+
mode = sys.argv[1]
344+
data = b"\\xff" if mode == "invalid" else (
345+
b"" if mode == "empty" else "\\ufeffA\\x00\\U0001f600".encode("utf-8")
346+
)
347+
pointer, short, length = c.c_void_p, c.c_short, c.c_ssize_t
348+
get_type = c.CFUNCTYPE(short, pointer, c.c_ushort, short, pointer, length, pointer)
349+
diag_type = c.CFUNCTYPE(
350+
short, short, pointer, short, pointer, pointer, pointer, short, pointer
351+
)
352+
library = c.CDLL(ddbc.module.__file__)
353+
slot = pointer.in_dll(library, "SQLGetData_ptr")
354+
diag_slot = pointer.in_dll(library, "SQLGetDiagRec_ptr")
355+
calls, errors = [], []
356+
357+
@diag_type
358+
def diagnostic(handle_type, handle, record, state, native, message, capacity, size):
359+
try:
360+
if record > 1:
361+
return 100
362+
text = "getdata warning".encode("utf-16le")
363+
assert capacity > len(text) // 2
364+
c.memmove(state, "01000\\0".encode("utf-16le"), 12)
365+
c.memmove(message, text + b"\\0\\0", len(text) + 2)
366+
c.cast(native, c.POINTER(c.c_int))[0] = 0
367+
c.cast(size, c.POINTER(short))[0] = len(text) // 2
368+
return 0
369+
except BaseException as error:
370+
errors.append(type(error).__name__)
371+
return -1
372+
373+
@get_type
374+
def getdata(handle, column, ctype, buffer, capacity, indicator):
375+
try:
376+
assert ctype == 1 # SQL_C_CHAR
377+
calls.append(column)
378+
result = original(handle, column, ctype, buffer, capacity, indicator)
379+
assert result == 0 and capacity > len(data)
380+
c.memmove(buffer, data + b"\\0", len(data) + 1)
381+
c.cast(indicator, c.POINTER(length))[0] = -1 if mode == "null" else len(data)
382+
return 1 if mode == "warning" else result
383+
except BaseException as error:
384+
errors.append(type(error).__name__)
385+
return -1
386+
387+
with mssql_python.connect(os.environ["DB_CONNECTION_STRING"]) as connection:
388+
with connection.cursor() as cursor:
389+
cursor.execute("SELECT CAST('text' AS VARCHAR(30))")
390+
saved, saved_diag = slot.value, diag_slot.value
391+
assert saved and saved_diag
392+
original = get_type(saved)
393+
assert ddbc.DDBCSQLFetch(cursor.hstmt) == 0
394+
marker = object()
395+
row = [marker]
396+
slot.value = c.cast(getdata, pointer).value
397+
if mode == "warning":
398+
diag_slot.value = c.cast(diagnostic, pointer).value
399+
try:
400+
ret = ddbc.DDBCSQLGetData(
401+
cursor.hstmt, 1, row, "utf-8", "utf-16le", 1, cursor.messages
402+
)
403+
assert ret == (1 if mode == "warning" else 0)
404+
finally:
405+
slot.value, diag_slot.value = saved, saved_diag
406+
expected = None if mode == "null" else (
407+
data if mode == "invalid" else data.decode("utf-8")
408+
)
409+
assert row == [marker, expected]
410+
assert type(row[1]) is type(expected)
411+
assert calls == [1] and not errors, (calls, errors)
412+
assert cursor.messages == (
413+
[("[01000] (0)", "getdata warning")] if mode == "warning" else []
414+
)
415+
cursor.execute("SELECT 42")
416+
assert cursor.fetchval() == 42
417+
""")
418+
_run_fetch_script(conn_str, script, payload)

0 commit comments

Comments
 (0)