Files
wehub-resource-sync 7a0da7932b
Backwards Compatibility / Verify Encryption Constants (push) Waiting to run
Backwards Compatibility / PyPI Version Compatibility (push) Waiting to run
Backwards Compatibility / Database Migration Tests (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Blocked by required conditions
Docker Tests (Consolidated) / detect-changes (push) Waiting to run
Docker Tests (Consolidated) / Build Test Image (push) Waiting to run
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Blocked by required conditions
Docker Tests (Consolidated) / Accessibility Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Unit Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Example Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / Production Image Smoke Test (push) Blocked by required conditions
Docker Tests (Consolidated) / Infrastructure Tests (push) Blocked by required conditions
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Waiting to run
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

388 lines
12 KiB
Python

"""
Tests for SQLCipher thread safety.
These tests verify that SQLCipher connections work correctly when
accessed from multiple threads.
"""
import shutil
import tempfile
import threading
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
import pytest
from src.local_deep_research.database.sqlcipher_utils import (
apply_cipher_defaults_before_key,
apply_performance_pragmas,
apply_sqlcipher_pragmas,
set_sqlcipher_key,
)
from src.local_deep_research.database.sqlcipher_compat import (
get_sqlcipher_module,
)
@pytest.fixture
def temp_db_path():
"""Create a temporary database path."""
temp_dir = tempfile.mkdtemp()
db_path = Path(temp_dir) / "test_threads.db"
yield db_path
shutil.rmtree(temp_dir)
@pytest.fixture
def sqlcipher_module():
"""Get the SQLCipher module."""
return get_sqlcipher_module()
def create_configured_connection(sqlcipher_module, db_path, password):
"""Helper to create a properly configured connection."""
# Detect if we're creating a new database or opening an existing one
creation_mode = not db_path.exists()
conn = sqlcipher_module.connect(str(db_path), check_same_thread=False)
cursor = conn.cursor()
if creation_mode:
apply_cipher_defaults_before_key(cursor)
set_sqlcipher_key(cursor, password)
apply_sqlcipher_pragmas(cursor, creation_mode=creation_mode)
apply_performance_pragmas(cursor)
cursor.close()
return conn
class TestThreadSafety:
"""Tests for basic thread safety."""
def test_connection_per_thread(self, sqlcipher_module, temp_db_path):
"""Verify each thread can create its own connection."""
password = "thread_test"
# Create initial database
conn = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
conn.execute("CREATE TABLE thread_data (thread_id INTEGER, value TEXT)")
conn.commit()
conn.close()
results = []
errors = []
def thread_work(thread_id):
try:
# Each thread creates its own connection
thread_conn = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
thread_conn.execute(
"INSERT INTO thread_data VALUES (?, ?)",
(thread_id, f"data_from_{thread_id}"),
)
thread_conn.commit()
thread_conn.close()
results.append(thread_id)
except Exception as e:
errors.append((thread_id, str(e)))
# Run 5 threads concurrently
threads = []
for i in range(5):
t = threading.Thread(target=thread_work, args=(i,))
threads.append(t)
t.start()
for t in threads:
t.join()
# Verify no errors
assert not errors, f"Thread errors: {errors}"
# Verify all data was written
conn = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
count = conn.execute("SELECT COUNT(*) FROM thread_data").fetchone()[0]
assert count == 5, f"Expected 5 rows, got {count}"
conn.close()
def test_concurrent_reads_different_connections(
self, sqlcipher_module, temp_db_path
):
"""Verify concurrent reads work from different connections."""
password = "concurrent_read_test"
# Create and populate database
conn = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
conn.execute("CREATE TABLE test (id INTEGER PRIMARY KEY, data TEXT)")
for i in range(100):
conn.execute("INSERT INTO test VALUES (?, ?)", (i, f"data_{i}"))
conn.commit()
conn.close()
results = []
errors = []
def read_work(reader_id):
try:
reader_conn = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
# Read random rows
data = reader_conn.execute(
"SELECT COUNT(*) FROM test"
).fetchone()[0]
results.append((reader_id, data))
reader_conn.close()
except Exception as e:
errors.append((reader_id, str(e)))
# Run 10 concurrent readers
with ThreadPoolExecutor(max_workers=10) as executor:
futures = [executor.submit(read_work, i) for i in range(10)]
for future in as_completed(futures):
pass # Just wait for completion
assert not errors, f"Read errors: {errors}"
assert len(results) == 10
# All readers should see 100 rows
assert all(count == 100 for _, count in results)
def test_write_serialization(self, sqlcipher_module, temp_db_path):
"""Verify concurrent writes are properly serialized."""
# audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: keep but increase timeout or mark @pytest.mark.slow).
password = "write_serial_test"
# Create database
conn = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
conn.execute(
"CREATE TABLE counter (id INTEGER PRIMARY KEY, count INTEGER)"
)
conn.execute("INSERT INTO counter VALUES (1, 0)")
conn.commit()
conn.close()
errors = []
def increment_work(worker_id, increments):
try:
worker_conn = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
for _ in range(increments):
# SQLite handles serialization with busy_timeout
worker_conn.execute(
"UPDATE counter SET count = count + 1 WHERE id = 1"
)
worker_conn.commit()
worker_conn.close()
except Exception as e:
errors.append((worker_id, str(e)))
# 5 workers each doing 10 increments = 50 total
threads = []
for i in range(5):
t = threading.Thread(target=increment_work, args=(i, 10))
threads.append(t)
t.start()
for t in threads:
t.join()
assert not errors, f"Write errors: {errors}"
# Verify final count
conn = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
count = conn.execute(
"SELECT count FROM counter WHERE id = 1"
).fetchone()[0]
assert count == 50, f"Expected 50, got {count}"
conn.close()
class TestConnectionPooling:
"""Tests for connection pooling behavior."""
def test_multiple_connections_same_database(
self, sqlcipher_module, temp_db_path
):
"""Verify multiple simultaneous connections to same database."""
password = "pool_test"
# Create database
conn0 = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
conn0.execute(
"CREATE TABLE shared (id INTEGER PRIMARY KEY, owner TEXT)"
)
conn0.commit()
# Create additional connections
conns = [conn0]
for i in range(4):
c = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
conns.append(c)
# Each connection writes to shared table
for i, conn in enumerate(conns):
conn.execute("INSERT INTO shared VALUES (?, ?)", (i, f"conn_{i}"))
conn.commit()
# All connections should see all data
for conn in conns:
count = conn.execute("SELECT COUNT(*) FROM shared").fetchone()[0]
assert count == 5, f"Connection should see 5 rows, got {count}"
# Close all connections
for conn in conns:
conn.close()
def test_connection_reuse_safe(self, sqlcipher_module, temp_db_path):
"""Verify connections can be safely reused across operations."""
password = "reuse_test"
conn = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
conn.execute(
"CREATE TABLE reuse_test (id INTEGER PRIMARY KEY, data TEXT)"
)
conn.commit()
# Perform many operations on same connection
for i in range(100):
conn.execute(
"INSERT INTO reuse_test VALUES (?, ?)", (i, f"data_{i}")
)
if i % 10 == 0:
conn.commit()
conn.commit()
# Verify all data
count = conn.execute("SELECT COUNT(*) FROM reuse_test").fetchone()[0]
assert count == 100
conn.close()
class TestBackgroundThreads:
"""Tests for background thread database access."""
def test_background_thread_writes(self, sqlcipher_module, temp_db_path):
"""Verify background thread can write to database."""
password = "bg_write_test"
# Create database in main thread
main_conn = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
main_conn.execute(
"CREATE TABLE bg_data (id INTEGER PRIMARY KEY, source TEXT)"
)
main_conn.commit()
main_conn.close()
background_done = threading.Event()
background_error = []
def background_work():
try:
bg_conn = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
bg_conn.execute(
"INSERT INTO bg_data VALUES (1, 'background_thread')"
)
bg_conn.commit()
bg_conn.close()
except Exception as e:
background_error.append(str(e))
finally:
background_done.set()
# Start background thread
bg_thread = threading.Thread(target=background_work, daemon=True)
bg_thread.start()
# Wait for completion
background_done.wait(timeout=10)
assert not background_error, (
f"Background thread error: {background_error}"
)
# Verify data in main thread
main_conn = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
result = main_conn.execute(
"SELECT source FROM bg_data WHERE id = 1"
).fetchone()
assert result[0] == "background_thread"
main_conn.close()
def test_main_thread_reads_background_writes(
self, sqlcipher_module, temp_db_path
):
"""Verify main thread can read data written by background thread."""
# audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: keep but consider removing sleep (not load-bearing for read-after-write semantics)).
password = "main_bg_test"
# Create database
conn = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
conn.execute(
"CREATE TABLE bg_writes (id INTEGER PRIMARY KEY, timestamp REAL)"
)
conn.commit()
conn.close()
write_count = 20
writes_done = threading.Event()
def writer_thread():
writer_conn = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
for i in range(write_count):
writer_conn.execute(
"INSERT INTO bg_writes VALUES (?, ?)",
(i, time.time()),
)
writer_conn.commit()
time.sleep(0.01) # Small delay between writes
writer_conn.close()
writes_done.set()
# Start writer thread
writer = threading.Thread(target=writer_thread, daemon=True)
writer.start()
# Wait for writes to complete
writes_done.wait(timeout=30)
# Read from main thread
reader_conn = create_configured_connection(
sqlcipher_module, temp_db_path, password
)
count = reader_conn.execute(
"SELECT COUNT(*) FROM bg_writes"
).fetchone()[0]
assert count == write_count, f"Expected {write_count} rows, got {count}"
reader_conn.close()