Skip to content

Commit 95cc6c8

Browse files
committed
test(error-paths): add comprehensive error path tests
Add test-error-paths.py with 30 tests specifically targeting error-handling code paths that previously had memory leaks (fixed in PR asg017#258). These tests ensure that error paths are exercised by the test suite so that memory leaks would be caught by sanitizers (ASan/LSan) if reintroduced. Tests cover: - vec_each with invalid inputs (NULL, wrong types, malformed JSON, empty arrays) - vec_slice error conditions (NULL, invalid types, bad indices) - vector_from_value error paths (mismatched types/dimensions in distance/add/sub) - vec0 INSERT/KNN errors (NULL, dimension mismatches, type mismatches) - Metadata operations with invalid inputs - Repeated error operations to stress-test cleanup paths Note: malloc failure paths (OOM conditions) are not tested as they require fault injection (SQLITE_TESTCTRL_FAULT_INSTALL) which is not available in the standard build. The fixes ensure proper cleanup via 'goto done' instead of early 'return' statements. Related: Upstream PR asg017#258
1 parent c9be38c commit 95cc6c8

File tree

1 file changed

+265
-0
lines changed

1 file changed

+265
-0
lines changed

tests/test-error-paths.py

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
"""
2+
Tests for error paths and edge cases that previously had memory leaks.
3+
4+
These tests specifically target error-handling code paths that were fixed
5+
in PR #258 and related commits. The goal is to ensure these paths are
6+
exercised by the test suite so that memory leaks would be caught by
7+
sanitizers (ASan/LSan) if reintroduced.
8+
"""
9+
10+
import sqlite3
11+
import pytest
12+
import struct
13+
import re
14+
15+
16+
def _raises(message, error=sqlite3.OperationalError):
17+
"""Context manager for testing expected errors."""
18+
return pytest.raises(error, match=re.escape(message))
19+
20+
21+
# Helper to create malformed vector blobs
22+
def _malformed_blob(data):
23+
"""Create a blob that looks like a vector but is malformed."""
24+
return data
25+
26+
27+
class TestVecEachErrorPaths:
28+
"""Test error paths in vec_each that previously leaked pzErrMsg."""
29+
30+
def test_vec_each_with_null_input(self, db):
31+
"""Test vec_each with NULL input - should error without leaking."""
32+
# The key is that it errors - exact message may vary
33+
with pytest.raises(sqlite3.OperationalError):
34+
db.execute("SELECT * FROM vec_each(NULL)").fetchall()
35+
36+
def test_vec_each_with_integer_input(self, db):
37+
"""Test vec_each with wrong type - should error without leaking."""
38+
with pytest.raises(sqlite3.OperationalError):
39+
db.execute("SELECT * FROM vec_each(42)").fetchall()
40+
41+
def test_vec_each_with_malformed_json(self, db):
42+
"""Test vec_each with malformed JSON - should error without leaking."""
43+
with pytest.raises(sqlite3.OperationalError):
44+
db.execute("SELECT * FROM vec_each('[1, 2, not valid json]')").fetchall()
45+
46+
def test_vec_each_with_empty_json_array(self, db):
47+
"""Test vec_each with empty array - should error without leaking."""
48+
with pytest.raises(sqlite3.OperationalError):
49+
db.execute("SELECT * FROM vec_each('[]')").fetchall()
50+
51+
52+
class TestVecSliceErrorPaths:
53+
"""
54+
Test error paths in vec_slice.
55+
56+
Note: The malloc failure paths (INT8 and BIT cases) are very difficult to test
57+
without fault injection. Those paths are triggered when sqlite3_malloc() fails
58+
due to out-of-memory conditions. Without SQLITE_TESTCTRL_FAULT_INSTALL or
59+
similar fault injection, we cannot reliably trigger malloc failures.
60+
61+
The fixes ensure that if malloc fails, the vector cleanup function is called
62+
via 'goto done' instead of 'return', preventing memory leaks.
63+
64+
These tests cover other error paths to ensure the general error handling works.
65+
"""
66+
67+
def test_vec_slice_with_null_vector(self, db):
68+
"""Test vec_slice with NULL vector - should error without leaking."""
69+
with pytest.raises(sqlite3.OperationalError):
70+
db.execute("SELECT vec_slice(NULL, 0, 1)").fetchone()
71+
72+
def test_vec_slice_with_invalid_type(self, db):
73+
"""Test vec_slice with non-vector type - should error without leaking."""
74+
with pytest.raises(sqlite3.OperationalError):
75+
db.execute("SELECT vec_slice(42, 0, 1)").fetchone()
76+
77+
def test_vec_slice_with_negative_start(self, db):
78+
"""Test vec_slice with negative start index."""
79+
with _raises("slice 'start' index must be a postive number."):
80+
db.execute("SELECT vec_slice(vec_f32('[1,2,3]'), -1, 2)").fetchone()
81+
82+
def test_vec_slice_with_negative_end(self, db):
83+
"""Test vec_slice with negative end index."""
84+
with _raises("slice 'end' index must be a postive number."):
85+
db.execute("SELECT vec_slice(vec_f32('[1,2,3]'), 0, -1)").fetchone()
86+
87+
def test_vec_slice_with_start_greater_than_end(self, db):
88+
"""Test vec_slice with start > end."""
89+
with _raises("slice 'start' index is greater than 'end' index"):
90+
db.execute("SELECT vec_slice(vec_f32('[1,2,3]'), 2, 1)").fetchone()
91+
92+
def test_vec_slice_with_start_equal_to_end(self, db):
93+
"""Test vec_slice with start == end (zero-length result)."""
94+
with _raises("slice 'start' index is equal to the 'end' index, vectors must have non-zero length"):
95+
db.execute("SELECT vec_slice(vec_f32('[1,2,3]'), 1, 1)").fetchone()
96+
97+
def test_vec_slice_int8_with_out_of_bounds(self, db):
98+
"""Test vec_slice on int8 vector with out of bounds indices."""
99+
with _raises("slice 'end' index is greater than the number of dimensions"):
100+
db.execute("SELECT vec_slice(vec_int8('[1,2,3]'), 0, 10)").fetchone()
101+
102+
def test_vec_slice_bit_with_non_aligned_start(self, db):
103+
"""Test vec_slice on bit vector with non-8-aligned start."""
104+
with _raises("start index must be divisible by 8."):
105+
db.execute("SELECT vec_slice(vec_bit(x'AABBCCDD'), 4, 16)").fetchone()
106+
107+
def test_vec_slice_bit_with_non_aligned_end(self, db):
108+
"""Test vec_slice on bit vector with non-8-aligned end."""
109+
with _raises("end index must be divisible by 8."):
110+
db.execute("SELECT vec_slice(vec_bit(x'AABBCCDD'), 0, 12)").fetchone()
111+
112+
113+
class TestVectorFromValueErrorPaths:
114+
"""
115+
Test various error paths in vector_from_value() which is called by many functions.
116+
117+
This exercises the error handling that allocates pzErrMsg and ensures it's freed
118+
properly in all error cases.
119+
"""
120+
121+
def test_vec_length_with_null(self, db):
122+
"""Test vec_length with NULL input."""
123+
with pytest.raises(sqlite3.OperationalError):
124+
db.execute("SELECT vec_length(NULL)").fetchone()
125+
126+
def test_vec_length_with_wrong_type(self, db):
127+
"""Test vec_length with wrong input type."""
128+
with pytest.raises(sqlite3.OperationalError):
129+
db.execute("SELECT vec_length(123)").fetchone()
130+
131+
def test_vec_distance_l2_with_null(self, db):
132+
"""Test vec_distance_l2 with NULL inputs."""
133+
with pytest.raises(sqlite3.OperationalError):
134+
db.execute("SELECT vec_distance_l2(NULL, vec_f32('[1,2,3]'))").fetchone()
135+
136+
def test_vec_distance_l2_with_mismatched_types(self, db):
137+
"""Test vec_distance_l2 with mismatched vector types."""
138+
with pytest.raises(sqlite3.OperationalError):
139+
db.execute("SELECT vec_distance_l2(vec_f32('[1,2,3]'), vec_int8('[1,2,3]'))").fetchone()
140+
141+
def test_vec_distance_l2_with_mismatched_dimensions(self, db):
142+
"""Test vec_distance_l2 with mismatched dimensions."""
143+
with pytest.raises(sqlite3.OperationalError):
144+
db.execute("SELECT vec_distance_l2(vec_f32('[1,2,3]'), vec_f32('[1,2,3,4]'))").fetchone()
145+
146+
def test_vec_add_with_null(self, db):
147+
"""Test vec_add with NULL input."""
148+
with pytest.raises(sqlite3.OperationalError):
149+
db.execute("SELECT vec_add(NULL, vec_f32('[1,2,3]'))").fetchone()
150+
151+
def test_vec_add_with_mismatched_dimensions(self, db):
152+
"""Test vec_add with mismatched dimensions."""
153+
with pytest.raises(sqlite3.OperationalError):
154+
db.execute("SELECT vec_add(vec_f32('[1,2]'), vec_f32('[1,2,3]'))").fetchone()
155+
156+
def test_vec_sub_with_mismatched_types(self, db):
157+
"""Test vec_sub with mismatched types."""
158+
with pytest.raises(sqlite3.OperationalError):
159+
db.execute("SELECT vec_sub(vec_f32('[1,2,3]'), vec_int8('[1,2,3]'))").fetchone()
160+
161+
162+
class TestVec0ErrorPaths:
163+
"""
164+
Test error paths in vec0 virtual table operations.
165+
166+
These test paths that allocate memory (zSql, knn_data, etc.) and ensure
167+
proper cleanup on errors.
168+
"""
169+
170+
def test_vec0_insert_with_null_vector(self, db):
171+
"""Test INSERT with NULL vector - should error without leaking."""
172+
db.execute("CREATE VIRTUAL TABLE test USING vec0(v float[3])")
173+
with pytest.raises(sqlite3.OperationalError):
174+
db.execute("INSERT INTO test(rowid, v) VALUES (1, NULL)")
175+
db.execute("DROP TABLE test")
176+
177+
def test_vec0_insert_with_wrong_dimensions(self, db):
178+
"""Test INSERT with wrong number of dimensions."""
179+
db.execute("CREATE VIRTUAL TABLE test USING vec0(v float[3])")
180+
with pytest.raises(sqlite3.OperationalError):
181+
db.execute("INSERT INTO test(rowid, v) VALUES (1, vec_f32('[1,2,3,4]'))")
182+
db.execute("DROP TABLE test")
183+
184+
def test_vec0_insert_with_wrong_type(self, db):
185+
"""Test INSERT with wrong vector type."""
186+
db.execute("CREATE VIRTUAL TABLE test USING vec0(v float[3])")
187+
with pytest.raises(sqlite3.OperationalError):
188+
db.execute("INSERT INTO test(rowid, v) VALUES (1, vec_int8('[1,2,3]'))")
189+
db.execute("DROP TABLE test")
190+
191+
def test_vec0_knn_with_null_query(self, db):
192+
"""Test KNN query with NULL query vector - should error without leaking knn_data."""
193+
db.execute("CREATE VIRTUAL TABLE test USING vec0(v float[3])")
194+
db.execute("INSERT INTO test(rowid, v) VALUES (1, vec_f32('[1,2,3]'))")
195+
with pytest.raises(sqlite3.OperationalError):
196+
db.execute("SELECT * FROM test WHERE v MATCH NULL AND k = 5").fetchall()
197+
db.execute("DROP TABLE test")
198+
199+
def test_vec0_knn_with_mismatched_dimensions(self, db):
200+
"""Test KNN query with wrong dimensions - should error without leaking."""
201+
db.execute("CREATE VIRTUAL TABLE test USING vec0(v float[3])")
202+
db.execute("INSERT INTO test(rowid, v) VALUES (1, vec_f32('[1,2,3]'))")
203+
with pytest.raises(sqlite3.OperationalError):
204+
db.execute("SELECT * FROM test WHERE v MATCH vec_f32('[1,2,3,4]') AND k = 5").fetchall()
205+
db.execute("DROP TABLE test")
206+
207+
def test_vec0_knn_with_mismatched_type(self, db):
208+
"""Test KNN query with wrong type - should error without leaking."""
209+
db.execute("CREATE VIRTUAL TABLE test USING vec0(v float[3])")
210+
db.execute("INSERT INTO test(rowid, v) VALUES (1, vec_f32('[1,2,3]'))")
211+
with pytest.raises(sqlite3.OperationalError):
212+
db.execute("SELECT * FROM test WHERE v MATCH vec_int8('[1,2,3]') AND k = 5").fetchall()
213+
db.execute("DROP TABLE test")
214+
215+
def test_vec0_metadata_insert_with_null_metadata(self, db):
216+
"""Test INSERT with NULL metadata value - should error."""
217+
db.execute("CREATE VIRTUAL TABLE test USING vec0(v float[3], category text)")
218+
# NULL metadata is not supported - should error
219+
with pytest.raises(sqlite3.OperationalError):
220+
db.execute("INSERT INTO test(rowid, v, category) VALUES (1, vec_f32('[1,2,3]'), NULL)")
221+
db.execute("DROP TABLE test")
222+
223+
def test_vec0_with_invalid_metadata_filter(self, db):
224+
"""Test query with invalid metadata IN clause."""
225+
db.execute("CREATE VIRTUAL TABLE test USING vec0(v float[3], score integer)")
226+
db.execute("INSERT INTO test(rowid, v, score) VALUES (1, vec_f32('[1,2,3]'), 100)")
227+
228+
# This exercises the metadata IN clause path
229+
result = db.execute(
230+
"SELECT * FROM test WHERE v MATCH vec_f32('[1,2,3]') AND k = 5 AND score IN (100, 200)"
231+
).fetchall()
232+
assert len(result) == 1
233+
234+
db.execute("DROP TABLE test")
235+
236+
237+
def test_repeated_error_operations(db):
238+
"""
239+
Test repeated error conditions to stress-test cleanup paths.
240+
241+
If memory leaks exist in error paths, this will accumulate them
242+
and make them more visible to memory leak detectors.
243+
"""
244+
db.execute("CREATE VIRTUAL TABLE test USING vec0(v float[3])")
245+
db.execute("INSERT INTO test(rowid, v) VALUES (1, vec_f32('[1,2,3]'))")
246+
247+
# Repeat error conditions many times
248+
for i in range(50):
249+
# Invalid dimension
250+
with pytest.raises(sqlite3.OperationalError):
251+
db.execute("INSERT INTO test(rowid, v) VALUES (?, vec_f32('[1,2,3,4]'))", [i + 2])
252+
253+
# Invalid type
254+
with pytest.raises(sqlite3.OperationalError):
255+
db.execute("INSERT INTO test(rowid, v) VALUES (?, vec_int8('[1,2,3]'))", [i + 2])
256+
257+
# Invalid KNN query
258+
with pytest.raises(sqlite3.OperationalError):
259+
db.execute("SELECT * FROM test WHERE v MATCH vec_f32('[1,2,3,4]') AND k = 5").fetchall()
260+
261+
db.execute("DROP TABLE test")
262+
263+
264+
if __name__ == "__main__":
265+
pytest.main([__file__, "-v"])

0 commit comments

Comments
 (0)