248 lines
8.8 KiB
Python
248 lines
8.8 KiB
Python
"""``database.synchronous`` is configurable, validated, and floored on macOS.
|
|
|
|
Before this, `apply_database_pragmas()` accepted five sizing pragmas and no
|
|
durability one, and `_enforce_macos_synchronous_full()` returned early off
|
|
Darwin. So on Linux and Windows nothing in the process ever executed
|
|
`PRAGMA synchronous` against state.db, and the effective level was whichever
|
|
`SQLITE_DEFAULT_WAL_SYNCHRONOUS` the interpreter's SQLite happened to be
|
|
compiled with -- invisible from config, unpinnable, and different between
|
|
builds. See #90837, where three weeks of corruption forensics were carried out
|
|
under the stated assumption of `synchronous=FULL` on Ubuntu.
|
|
"""
|
|
|
|
import sqlite3
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
import hermes_state
|
|
from hermes_state import (
|
|
apply_database_pragmas,
|
|
resolve_synchronous_level,
|
|
)
|
|
|
|
|
|
def _wal_conn(tmp_path):
|
|
conn = sqlite3.connect(str(tmp_path / "state.db"), isolation_level=None)
|
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
return conn
|
|
|
|
|
|
def _level(conn):
|
|
return conn.execute("PRAGMA synchronous").fetchone()[0]
|
|
|
|
|
|
def _config(monkeypatch, database_section):
|
|
"""Point apply_database_pragmas at an in-memory config.
|
|
|
|
It imports `hermes_cli.config` lazily inside the function body, so the
|
|
patch has to land on that module rather than on a name in this one.
|
|
"""
|
|
import hermes_cli.config as config_mod
|
|
|
|
cfg = {"database": database_section}
|
|
monkeypatch.setattr(config_mod, "load_config_readonly", lambda *a, **k: cfg)
|
|
return cfg
|
|
|
|
|
|
class TestResolveSynchronousLevel:
|
|
"""The parser, in isolation -- a wrong answer here is a silent downgrade."""
|
|
|
|
@pytest.mark.parametrize(
|
|
"raw,expected",
|
|
[
|
|
("OFF", 0),
|
|
("NORMAL", 1),
|
|
("FULL", 2),
|
|
("EXTRA", 3),
|
|
("full", 2),
|
|
("Full", 2),
|
|
(" FULL ", 2),
|
|
(0, 0),
|
|
(1, 1),
|
|
(2, 2),
|
|
(3, 3),
|
|
("2", 2),
|
|
(" 2 ", 2),
|
|
],
|
|
)
|
|
def test_accepts_documented_spellings(self, raw, expected):
|
|
assert resolve_synchronous_level(raw) == expected
|
|
|
|
@pytest.mark.parametrize(
|
|
"raw",
|
|
[
|
|
"PARANOID",
|
|
"",
|
|
" ",
|
|
4,
|
|
-1,
|
|
"4",
|
|
None,
|
|
[],
|
|
{},
|
|
2.5,
|
|
"2.0",
|
|
],
|
|
)
|
|
def test_rejects_everything_else(self, raw):
|
|
assert resolve_synchronous_level(raw) is None
|
|
|
|
def test_yaml_off_is_a_level_but_yaml_on_is_not(self):
|
|
"""`synchronous: off` is bool False in YAML and means OFF.
|
|
|
|
`synchronous: on` is bool True and means nothing -- there is no
|
|
durability level it could map to, so it must be rejected rather than
|
|
coerced to 1 by int(True).
|
|
"""
|
|
assert resolve_synchronous_level(False) == 0
|
|
assert resolve_synchronous_level(True) is None
|
|
|
|
|
|
class TestAppliedFromConfig:
|
|
def test_configured_level_reaches_the_connection(self, tmp_path, monkeypatch):
|
|
_config(monkeypatch, {"synchronous": "NORMAL"})
|
|
conn = _wal_conn(tmp_path)
|
|
try:
|
|
monkeypatch.setattr(sys, "platform", "linux")
|
|
apply_database_pragmas(conn, db_label="state.db")
|
|
assert _level(conn) == 1
|
|
finally:
|
|
conn.close()
|
|
|
|
def test_full_reaches_the_connection(self, tmp_path, monkeypatch):
|
|
"""The #90837 case: an operator asking for FULL on Linux gets FULL.
|
|
|
|
Fails without the patch -- the pragma was never executed at all, so the
|
|
connection kept whatever the build's default was.
|
|
"""
|
|
_config(monkeypatch, {"synchronous": "FULL"})
|
|
conn = _wal_conn(tmp_path)
|
|
try:
|
|
monkeypatch.setattr(sys, "platform", "linux")
|
|
conn.execute("PRAGMA synchronous=0")
|
|
assert _level(conn) == 0
|
|
apply_database_pragmas(conn, db_label="state.db")
|
|
assert _level(conn) == 2
|
|
finally:
|
|
conn.close()
|
|
|
|
def test_unset_leaves_the_level_alone(self, tmp_path, monkeypatch):
|
|
"""No config key must mean no write -- this is the default path."""
|
|
_config(monkeypatch, {"wal_autocheckpoint": 1000})
|
|
conn = _wal_conn(tmp_path)
|
|
try:
|
|
monkeypatch.setattr(sys, "platform", "linux")
|
|
conn.execute("PRAGMA synchronous=0")
|
|
apply_database_pragmas(conn, db_label="state.db")
|
|
assert _level(conn) == 0
|
|
assert conn.execute("PRAGMA wal_autocheckpoint").fetchone()[0] == 1000
|
|
finally:
|
|
conn.close()
|
|
|
|
def test_garbage_warns_and_changes_nothing(self, tmp_path, monkeypatch, caplog):
|
|
"""A typo must not fall through to a different durability level."""
|
|
_config(monkeypatch, {"synchronous": "PARANOID"})
|
|
conn = _wal_conn(tmp_path)
|
|
try:
|
|
monkeypatch.setattr(sys, "platform", "linux")
|
|
conn.execute("PRAGMA synchronous=2")
|
|
with caplog.at_level("WARNING"):
|
|
apply_database_pragmas(conn, db_label="state.db")
|
|
assert _level(conn) == 2
|
|
assert "PARANOID" in caplog.text
|
|
finally:
|
|
conn.close()
|
|
|
|
def test_the_other_pragmas_still_apply(self, tmp_path, monkeypatch):
|
|
"""Guardrail for #77630's five keys -- they share the same function."""
|
|
_config(
|
|
monkeypatch,
|
|
{"synchronous": "FULL", "wal_autocheckpoint": 250, "mmap_size": 0},
|
|
)
|
|
conn = _wal_conn(tmp_path)
|
|
try:
|
|
monkeypatch.setattr(sys, "platform", "linux")
|
|
apply_database_pragmas(conn, db_label="state.db")
|
|
assert _level(conn) == 2
|
|
assert conn.execute("PRAGMA wal_autocheckpoint").fetchone()[0] == 250
|
|
assert conn.execute("PRAGMA mmap_size").fetchone()[0] == 0
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
class TestMacOSFloor:
|
|
"""#64355 enforced FULL on Darwin. Config must not be able to undo it.
|
|
|
|
`_enforce_macos_synchronous_full()` runs inside `apply_wal_with_fallback()`,
|
|
which is earlier than `apply_database_pragmas()`, so without an explicit
|
|
floor the config value would simply win by running last.
|
|
"""
|
|
|
|
def test_lowering_below_full_is_refused_on_darwin(
|
|
self, tmp_path, monkeypatch, caplog
|
|
):
|
|
_config(monkeypatch, {"synchronous": "NORMAL"})
|
|
conn = _wal_conn(tmp_path)
|
|
try:
|
|
monkeypatch.setattr(sys, "platform", "darwin")
|
|
hermes_state._enforce_macos_synchronous_full(conn)
|
|
assert _level(conn) == 2
|
|
with caplog.at_level("WARNING"):
|
|
apply_database_pragmas(conn, db_label="state.db")
|
|
assert _level(conn) == 2, "config lowered the macOS btree protection"
|
|
assert "macOS" in caplog.text
|
|
finally:
|
|
conn.close()
|
|
|
|
def test_raising_above_full_is_allowed_on_darwin(self, tmp_path, monkeypatch):
|
|
"""The floor is a floor, not a pin -- EXTRA is strictly safer."""
|
|
_config(monkeypatch, {"synchronous": "EXTRA"})
|
|
conn = _wal_conn(tmp_path)
|
|
try:
|
|
monkeypatch.setattr(sys, "platform", "darwin")
|
|
apply_database_pragmas(conn, db_label="state.db")
|
|
assert _level(conn) == 3
|
|
finally:
|
|
conn.close()
|
|
|
|
def test_the_floor_does_not_apply_off_darwin(self, tmp_path, monkeypatch):
|
|
"""Linux operators may deliberately choose NORMAL for write volume."""
|
|
_config(monkeypatch, {"synchronous": "NORMAL"})
|
|
conn = _wal_conn(tmp_path)
|
|
try:
|
|
monkeypatch.setattr(sys, "platform", "linux")
|
|
conn.execute("PRAGMA synchronous=2")
|
|
apply_database_pragmas(conn, db_label="state.db")
|
|
assert _level(conn) == 1
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def test_example_config_documents_the_key():
|
|
"""An unpinnable durability level is the bug; docs are half the fix."""
|
|
from pathlib import Path
|
|
|
|
text = Path(__file__).resolve().parents[1] / "cli-config.yaml.example"
|
|
body = text.read_text(encoding="utf-8")
|
|
assert "synchronous: FULL" in body
|
|
|
|
|
|
def test_example_config_calls_the_darwin_rule_a_floor_not_a_pin():
|
|
"""The macOS wording has to match _apply_synchronous_pragma's actual rule.
|
|
|
|
Saying macOS is "always held at FULL" reads as "your setting is ignored
|
|
here", which would talk an operator out of choosing EXTRA -- the one level
|
|
that IS honored on Darwin. Only sub-FULL values are refused. The wording
|
|
is the whole interface for a key nobody can otherwise observe, so pin the
|
|
distinction rather than just the key's presence.
|
|
"""
|
|
from pathlib import Path
|
|
|
|
body = (
|
|
Path(__file__).resolve().parents[1] / "cli-config.yaml.example"
|
|
).read_text(encoding="utf-8")
|
|
assert "floor, not a pin" in body
|
|
assert "EXTRA is honored" in body
|
|
assert "always held" not in body
|