Skip to content

Commit 8be7576

Browse files
author
subrata-ms
committed
improving test coverage
1 parent b247063 commit 8be7576

File tree

1 file changed

+190
-0
lines changed

1 file changed

+190
-0
lines changed

tests/test_013_SqlHandle_free_shutdown.py

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1177,3 +1177,193 @@ def register_connections(thread_id, count):
11771177
assert result.returncode == 0, f"Test failed. stderr: {result.stderr}"
11781178
assert "Thread safety test: PASSED" in result.stdout
11791179
print(f"PASS: Active connections thread safety")
1180+
1181+
def test_cleanup_connections_list_copy_isolation(self, conn_str):
1182+
"""
1183+
Test that connections_to_close = list(_active_connections) creates a proper copy.
1184+
1185+
This test validates the critical line: connections_to_close = list(_active_connections)
1186+
1187+
Validates that:
1188+
1. The list() call creates a snapshot copy of _active_connections
1189+
2. Modifications to _active_connections during iteration don't affect the iteration
1190+
3. WeakSet can be modified (e.g., connections removed by GC) without breaking iteration
1191+
4. The copy prevents "Set changed size during iteration" RuntimeError
1192+
"""
1193+
script = textwrap.dedent(
1194+
f"""
1195+
import mssql_python
1196+
import weakref
1197+
import gc
1198+
1199+
class TestConnection:
1200+
def __init__(self, conn_id):
1201+
self.conn_id = conn_id
1202+
self._closed = False
1203+
self.close_call_count = 0
1204+
1205+
def close(self):
1206+
self.close_call_count += 1
1207+
self._closed = True
1208+
1209+
# Register multiple connections
1210+
connections = []
1211+
for i in range(5):
1212+
conn = TestConnection(i)
1213+
mssql_python._register_connection(conn)
1214+
connections.append(conn)
1215+
1216+
print(f"Registered {{len(connections)}} connections")
1217+
1218+
# Verify connections_to_close creates a proper list copy
1219+
# by checking that the original WeakSet can be modified without affecting cleanup
1220+
1221+
# Create a connection that will be garbage collected during cleanup simulation
1222+
temp_conn = TestConnection(999)
1223+
mssql_python._register_connection(temp_conn)
1224+
temp_ref = weakref.ref(temp_conn)
1225+
1226+
print(f"WeakSet size before: {{len(mssql_python._active_connections)}}")
1227+
1228+
# Now simulate what _cleanup_connections does:
1229+
# 1. Create list copy (this is the line we're testing)
1230+
with mssql_python._connections_lock:
1231+
connections_to_close = list(mssql_python._active_connections)
1232+
1233+
print(f"List copy created with {{len(connections_to_close)}} items")
1234+
1235+
# 2. Delete temp_conn and force GC - this modifies WeakSet
1236+
del temp_conn
1237+
gc.collect()
1238+
1239+
print(f"WeakSet size after GC: {{len(mssql_python._active_connections)}}")
1240+
1241+
# 3. Iterate over the COPY (not the original WeakSet)
1242+
# This should work even though WeakSet was modified
1243+
closed_count = 0
1244+
for conn in connections_to_close:
1245+
try:
1246+
if hasattr(conn, "_closed") and not conn._closed:
1247+
conn.close()
1248+
closed_count += 1
1249+
except Exception:
1250+
pass # Ignore errors from GC'd connection
1251+
1252+
print(f"Closed {{closed_count}} connections from list copy")
1253+
1254+
# Verify that the list copy isolated us from WeakSet modifications
1255+
assert closed_count >= len(connections), "Should have processed snapshot connections"
1256+
1257+
# Verify all live connections were closed
1258+
for conn in connections:
1259+
assert conn._closed, f"Connection {{conn.conn_id}} should be closed"
1260+
assert conn.close_call_count == 1, f"Connection {{conn.conn_id}} close called {{conn.close_call_count}} times"
1261+
1262+
# Key validation: The list copy preserved the snapshot even if GC happened
1263+
# The temp_conn is in the list copy (being iterated), keeping it alive
1264+
# This proves the list() call created a proper snapshot at that moment
1265+
print(f"List copy had {{len(connections_to_close)}} items at snapshot time")
1266+
1267+
print("List copy isolation: PASSED")
1268+
print("✓ connections_to_close = list(_active_connections) properly tested")
1269+
"""
1270+
)
1271+
1272+
result = subprocess.run(
1273+
[sys.executable, "-c", script], capture_output=True, text=True, timeout=10
1274+
)
1275+
1276+
assert result.returncode == 0, f"Test failed. stderr: {result.stderr}"
1277+
assert "List copy isolation: PASSED" in result.stdout
1278+
assert "✓ connections_to_close = list(_active_connections) properly tested" in result.stdout
1279+
print(f"PASS: Cleanup connections list copy isolation")
1280+
1281+
def test_cleanup_connections_weakset_modification_during_iteration(self, conn_str):
1282+
"""
1283+
Test that list copy prevents RuntimeError when WeakSet is modified during iteration.
1284+
1285+
This is a more aggressive test of the connections_to_close = list(_active_connections) line.
1286+
1287+
Validates that:
1288+
1. Without the list copy, iterating WeakSet directly would fail if modified
1289+
2. With the list copy, iteration is safe even if WeakSet shrinks due to GC
1290+
3. The pattern prevents "dictionary changed size during iteration" type errors
1291+
"""
1292+
script = textwrap.dedent(
1293+
f"""
1294+
import mssql_python
1295+
import weakref
1296+
import gc
1297+
1298+
class TestConnection:
1299+
def __init__(self, conn_id):
1300+
self.conn_id = conn_id
1301+
self._closed = False
1302+
1303+
def close(self):
1304+
self._closed = True
1305+
1306+
# Create connections with only weak references so they can be GC'd easily
1307+
weak_refs = []
1308+
for i in range(10):
1309+
conn = TestConnection(i)
1310+
mssql_python._register_connection(conn)
1311+
weak_refs.append(weakref.ref(conn))
1312+
# Don't keep strong reference - only weak_refs list has refs
1313+
1314+
initial_size = len(mssql_python._active_connections)
1315+
print(f"Initial WeakSet size: {{initial_size}}")
1316+
1317+
# TEST 1: Demonstrate that direct iteration would be unsafe
1318+
# (We can't actually do this in the real code, but we can show the principle)
1319+
print("TEST 1: Verifying list copy is necessary...")
1320+
1321+
# Force some garbage collection
1322+
gc.collect()
1323+
after_gc_size = len(mssql_python._active_connections)
1324+
print(f"WeakSet size after GC: {{after_gc_size}}")
1325+
1326+
# TEST 2: Verify list copy allows safe iteration
1327+
print("TEST 2: Testing list copy creates stable snapshot...")
1328+
1329+
# This is what _cleanup_connections does - creates a list copy
1330+
with mssql_python._connections_lock:
1331+
connections_to_close = list(mssql_python._active_connections)
1332+
1333+
snapshot_size = len(connections_to_close)
1334+
print(f"Snapshot list size: {{snapshot_size}}")
1335+
1336+
# Now cause more GC while we iterate the snapshot
1337+
gc.collect()
1338+
1339+
# Iterate the snapshot - this should work even though WeakSet may have changed
1340+
processed = 0
1341+
for conn in connections_to_close:
1342+
try:
1343+
if hasattr(conn, "_closed") and not conn._closed:
1344+
conn.close()
1345+
processed += 1
1346+
except Exception:
1347+
# Connection may have been GC'd, that's OK
1348+
pass
1349+
1350+
final_size = len(mssql_python._active_connections)
1351+
print(f"Final WeakSet size: {{final_size}}")
1352+
print(f"Processed {{processed}} connections from snapshot")
1353+
1354+
# Key assertion: We could iterate the full snapshot even if WeakSet changed
1355+
assert processed == snapshot_size, f"Should process all snapshot items: {{processed}} == {{snapshot_size}}"
1356+
1357+
print("WeakSet modification during iteration: PASSED")
1358+
print("✓ list() copy prevents 'set changed size during iteration' errors")
1359+
"""
1360+
)
1361+
1362+
result = subprocess.run(
1363+
[sys.executable, "-c", script], capture_output=True, text=True, timeout=10
1364+
)
1365+
1366+
assert result.returncode == 0, f"Test failed. stderr: {result.stderr}"
1367+
assert "WeakSet modification during iteration: PASSED" in result.stdout
1368+
assert "✓ list() copy prevents 'set changed size during iteration' errors" in result.stdout
1369+
print(f"PASS: Cleanup connections WeakSet modification during iteration")

0 commit comments

Comments
 (0)