The fork takes its own name everywhere a user can see it: binaries (voculinux, voculinux-gui), desktop entry, autostart, tray, D-Bus namespace (com.voculinux.Voculinux, no alias for the old name), config/data/cache dirs, single-instance lock, IBus engine identity, tray icons, resource prefixes, VOCULINUX_* environment names, and the update-checker user agent. The internal Python module stays vocalinux and the distribution metadata keeps the upstream URLs as provenance. On first run a previous Vocalinux install's config.json, custom dictionary, and portal restore token are copied once into the new locations (new config always wins, no merge, no write-back); models re-resolve through the pinned-checksum download path. An offline naming-sweep test proves no old-name strings remain on user-visible surfaces outside the allowlist. Refs #18
137 lines
5.7 KiB
Python
137 lines
5.7 KiB
Python
"""Behaviour tests for the ticket-19 performance-baseline protocol script.
|
|
|
|
The script (scripts/measure_perf_baseline.py) is the locked measurement
|
|
protocol for the installed-build baseline: every number it reports comes
|
|
from an observed run on the target machine class, parsed by the pure
|
|
helpers tested here. Tests run offline with fixture text only and never
|
|
touch audio hardware, the GPU, or the network.
|
|
"""
|
|
|
|
import importlib.util
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "measure_perf_baseline.py"
|
|
SPEC = importlib.util.spec_from_file_location("measure_perf_baseline", SCRIPT)
|
|
assert SPEC is not None and SPEC.loader is not None
|
|
PROTOCOL = importlib.util.module_from_spec(SPEC)
|
|
SPEC.loader.exec_module(PROTOCOL)
|
|
|
|
|
|
def test_parse_vmrss_kb_reads_kilobytes() -> None:
|
|
"""VmRSS lines parse to kilobytes; anything else yields None."""
|
|
status = "Name:\tvoculinux\nVmRSS:\t 412336 kB\nVmSwap:\t0 kB\n"
|
|
assert PROTOCOL.parse_vmrss_kb(status) == 412336
|
|
assert PROTOCOL.parse_vmrss_kb("Name:\tvoculinux\n") is None
|
|
|
|
|
|
def test_describe_reports_count_mean_min_max() -> None:
|
|
"""Spread statistics come from the samples, never fabricated."""
|
|
stats = PROTOCOL.describe([1.0, 2.0, 3.0])
|
|
assert stats["n"] == 3
|
|
assert stats["mean"] == pytest.approx(2.0)
|
|
assert stats["min"] == pytest.approx(1.0)
|
|
assert stats["max"] == pytest.approx(3.0)
|
|
|
|
|
|
def test_describe_empty_is_explicit() -> None:
|
|
"""No samples means n == 0, never a zero mean pretending to be data."""
|
|
stats = PROTOCOL.describe([])
|
|
assert stats["n"] == 0
|
|
assert stats["mean"] is None
|
|
|
|
|
|
WAKE_LOG = """\
|
|
2026-11-05 22:44:01,100 - vocalinux.ui.action_handler - DEBUG - Handling action: toggle
|
|
2026-11-05 22:44:01,350 - vocalinux.audio.capture - DEBUG - Opened capture stream: 1 channel(s) at 16000Hz
|
|
2026-11-05 22:44:05,100 - vocalinux.ui.action_handler - DEBUG - Handling action: toggle
|
|
2026-11-05 22:44:05,500 - vocalinux.audio.capture - DEBUG - Opened capture stream: 1 channel(s) at 16000Hz
|
|
"""
|
|
|
|
|
|
def test_parse_wake_latencies_pairs_toggle_to_capture() -> None:
|
|
"""Each toggle pairs with the next capture-stream line, in seconds."""
|
|
assert PROTOCOL.parse_wake_latencies(WAKE_LOG) == pytest.approx([0.25, 0.4])
|
|
|
|
|
|
def test_parse_wake_latencies_ignores_unpaired_toggle() -> None:
|
|
"""A toggle with no later capture start is dropped, not zero-filled."""
|
|
log = (
|
|
WAKE_LOG
|
|
+ "2026-11-05 22:44:09,100 - vocalinux.ui.action_handler - DEBUG - Handling action: start\n"
|
|
)
|
|
assert PROTOCOL.parse_wake_latencies(log) == pytest.approx([0.25, 0.4])
|
|
|
|
|
|
def test_realtime_factor_is_audio_over_wall() -> None:
|
|
"""RTF below one means faster than real time; zero audio is refused."""
|
|
assert PROTOCOL.realtime_factor(30.0, 15.0) == pytest.approx(0.5)
|
|
with pytest.raises(ValueError):
|
|
PROTOCOL.realtime_factor(0.0, 1.0)
|
|
|
|
|
|
def test_parse_nvidia_smi_csv_reads_first_gpu() -> None:
|
|
"""The locked nvidia-smi query shape parses to total/free MiB."""
|
|
out = (
|
|
"name, driver_version, memory.total [MiB], memory.free [MiB]\n"
|
|
"NVIDIA GeForce RTX 2050, 595.112.07, 4096 MiB, 3711 MiB\n"
|
|
)
|
|
assert PROTOCOL.parse_nvidia_smi_memory(out) == (4096, 3711)
|
|
assert PROTOCOL.parse_nvidia_smi_memory("no gpu here\n") is None
|
|
|
|
|
|
def test_recommend_thresholds_come_from_footprints() -> None:
|
|
"""Ladder thresholds derive from measured footprints plus headroom."""
|
|
footprints = {"q8_0": 1000.0, "q5_0": 700.0, "small": 500.0}
|
|
advice = PROTOCOL.recommend_thresholds(footprints, headroom_mib=512.0)
|
|
assert advice["prefer_q8_0_above_mib"] == pytest.approx(1512.0)
|
|
assert advice["prefer_q5_0_above_mib"] == pytest.approx(1212.0)
|
|
assert advice["floor_small_below_mib"] == pytest.approx(1212.0)
|
|
|
|
|
|
def test_render_report_names_protocol_and_host() -> None:
|
|
"""The evidence report carries protocol version and host facts."""
|
|
report = PROTOCOL.render_markdown_report(
|
|
{"os": "Void", "gpu": "NVIDIA GeForce RTX 2050"}, {"idle": {}}
|
|
)
|
|
assert PROTOCOL.PROTOCOL_VERSION in report
|
|
assert "Void" in report
|
|
assert "NVIDIA GeForce RTX 2050" in report
|
|
|
|
|
|
def test_sample_generator_is_deterministic(tmp_path) -> None:
|
|
"""The fixed sample regenerates byte-identical from its seed."""
|
|
first = tmp_path / "a.wav"
|
|
second = tmp_path / "b.wav"
|
|
PROTOCOL.make_sample_wav(first, seed=19)
|
|
PROTOCOL.make_sample_wav(second, seed=19)
|
|
assert first.read_bytes() == second.read_bytes()
|
|
|
|
|
|
def test_sample_shape_is_thirty_seconds_mono_16k(tmp_path) -> None:
|
|
"""The sample is 30 s, mono, 16 kHz, 16-bit for whisper.cpp."""
|
|
import wave
|
|
|
|
target = tmp_path / "sample.wav"
|
|
PROTOCOL.make_sample_wav(target, seed=19)
|
|
with wave.open(str(target), "rb") as handle:
|
|
assert handle.getnchannels() == 1
|
|
assert handle.getsampwidth() == 2
|
|
assert handle.getframerate() == 16000
|
|
assert handle.getnframes() / 16000 == pytest.approx(30.0)
|
|
|
|
|
|
DBUS_TOGGLE_LOG = """\
|
|
2026-11-05 23:01:11,400 - vocalinux.ui.tray_indicator - DEBUG - Toggle requested in state IDLE
|
|
2026-11-05 23:01:11,546 - vocalinux.audio.capture - DEBUG - Opened capture stream: 2 channel(s) at 44100Hz
|
|
2026-11-05 23:01:16,400 - vocalinux.ui.tray_indicator - DEBUG - Toggle requested in state LISTENING
|
|
2026-11-05 23:01:22,100 - vocalinux.ui.tray_indicator - DEBUG - Toggle requested in state IDLE
|
|
2026-11-05 23:01:22,749 - vocalinux.audio.capture - DEBUG - Opened capture stream: 2 channel(s) at 44100Hz
|
|
"""
|
|
|
|
|
|
def test_parse_wake_latencies_pairs_dbus_toggle_starts_only() -> None:
|
|
"""Only IDLE toggles open capture; a stop toggle never borrows the next run."""
|
|
assert PROTOCOL.parse_wake_latencies(DBUS_TOGGLE_LOG) == pytest.approx([0.146, 0.649])
|