Merge master into the Windows port

Three of the four collisions were the same one: master moved the directory
rule into paths.py while this branch was adding a Windows case to the copy in
config.py and the second copy in ggml.py. The case moves to paths.py with the
rest of it, and the directories test moves to tests/test_paths.py where master
put its neighbours.

The fourth is MeetingRecorder, which now starts a process per capture device.
Windows keeps its two lines there: no console window for either process, and
a stop that terminates rather than sending a signal the platform does not have.
This commit is contained in:
2026-08-16 10:14:37 +03:00
26 changed files with 1691 additions and 220 deletions
+244 -28
View File
@@ -16,6 +16,7 @@ import json
import os
import subprocess
import sys
import threading
import unittest
import wave
from unittest import mock
@@ -100,6 +101,22 @@ class StereoLevels(unittest.TestCase):
self.assertAlmostEqual(left, 0.25, places=3)
self.assertAlmostEqual(right, 0.5, places=3)
def test_two_mono_streams_are_interleaved_left_then_right(self):
self.assertEqual(
list(array.array("h", audio.interleave_mono(
pcm([100, 200, 300]), pcm([-100, -200, -300])
))),
[100, -100, 200, -200, 300, -300],
)
def test_interleaving_stops_at_the_shorter_stream(self):
self.assertEqual(
list(array.array("h", audio.interleave_mono(
pcm([100, 200]), pcm([-100])
))),
[100, -100],
)
class WriteWav(DikteTest):
def test_the_header_says_what_the_recorder_captured(self):
@@ -235,6 +252,34 @@ class FakeProcess:
self._alive = False
class StalledProcess(FakeProcess):
"""A capture that hands over a buffer and then stops answering at all.
Not the same thing as one that ends: the device is still there and the pipe
is still open, and a read of it never comes back.
"""
def __init__(self, data):
super().__init__(data)
self.stdout = _StalledStream(data)
class _StalledStream:
def __init__(self, data):
self._data = io.BytesIO(data)
self._released = threading.Event()
def read(self, size):
chunk = self._data.read(size)
if chunk:
return chunk
self._released.wait()
return b""
def release(self):
self._released.set()
class RecordingCommand(OnLinux, DikteTest):
"""Which program captures the microphone, and how it is asked to."""
@@ -465,42 +510,178 @@ class RecorderChain(OnLinux, DikteTest):
self.assertFalse(recorder.active)
class MeetingCommand(unittest.TestCase):
"""One process reading both devices, because two would drift apart."""
class MeetingCommands(unittest.TestCase):
"""Pulse can share a process; AVFoundation sessions cannot."""
def command(self, platform, mic="", system="them"):
with mock.patch.object(sys, "platform", platform):
return audio.meeting_command(mic, system)
def commands(self, platform, mic="", system="them"):
with mock.patch.object(sys, "platform", platform), \
mock.patch.object(audio, "_avfoundation_inputs", return_value=[]), \
mock.patch.object(
audio, "_resolve_avfoundation_target",
side_effect=lambda target, inputs=None: target or "default"):
return audio.meeting_commands(mic, system)
def test_linux_reads_both_through_pulse(self):
cmd = self.command("linux", mic="mine")
commands = self.commands("linux", mic="mine")
self.assertEqual(len(commands), 1)
cmd = commands[0]
self.assertEqual(cmd.count("pulse"), 2)
self.assertEqual(cmd[cmd.index("mine") - 1], "-i")
self.assertEqual(cmd[cmd.index("them") - 1], "-i")
def test_a_mac_reads_both_through_avfoundation(self):
cmd = self.command("darwin", mic="1")
self.assertEqual(cmd.count("avfoundation"), 2)
self.assertIn(":1", cmd)
self.assertIn(":them", cmd)
def test_a_mac_gives_each_avfoundation_device_its_own_process(self):
commands = self.commands("darwin", mic="mine")
self.assertEqual(len(commands), 2)
self.assertTrue(all(command.count("avfoundation") == 1
for command in commands))
self.assertIn(":mine", commands[0])
self.assertIn(":them", commands[1])
def test_no_microphone_named_means_the_default_one(self):
self.assertIn("default", self.command("linux"))
self.assertIn(":default", self.command("darwin"))
self.assertIn("default", self.commands("linux")[0])
self.assertIn(":default", self.commands("darwin")[0])
def test_both_merge_the_two_into_one_stereo_stream(self):
for platform in ("linux", "darwin"):
with self.subTest(platform=platform):
cmd = self.command(platform)
self.assertIn(audio.MERGE_FILTER, cmd)
self.assertEqual(cmd[cmd.index("-map") + 1], "[out]")
self.assertEqual(cmd[cmd.index("-f", cmd.index("-map")) + 1], "s16le")
def test_pulse_merges_the_two_into_one_stereo_stream(self):
cmd = self.commands("linux")[0]
self.assertIn(audio.MERGE_FILTER, cmd)
self.assertEqual(cmd[cmd.index("-map") + 1], "[out]")
self.assertEqual(cmd[cmd.index("-f", cmd.index("-map")) + 1], "s16le")
def test_each_mac_process_produces_clock_corrected_mono_pcm(self):
for cmd in self.commands("darwin"):
self.assertIn("first_pts=0", cmd[cmd.index("-af") + 1])
self.assertEqual(cmd[cmd.index("-ac") + 1], "1")
self.assertEqual(cmd[-2:], ["1", "-"])
def test_neither_lets_ffmpeg_read_the_terminal(self):
"""It shares stdin with Dikte, and would eat a keypress meant for it."""
for platform in ("linux", "darwin"):
with self.subTest(platform=platform):
self.assertIn("-nostdin", self.command(platform))
for command in self.commands(platform):
self.assertIn("-nostdin", command)
def test_both_mac_devices_are_read_off_one_listing(self):
"""Asking twice costs an ffmpeg run, and the second answer could have
renumbered between the two."""
with mock.patch.object(sys, "platform", "darwin"), \
mock.patch.object(audio, "_avfoundation_inputs",
return_value=[("0", "mine"),
("1", "them")]) as inputs:
audio.meeting_commands("mine", "them")
inputs.assert_called_once_with()
class MacMeetingRecorder(OnMacOS, DikteTest):
# Whichever way the machine has them ordered, a name is what is saved and
# the index it happens to hold now is what ffmpeg is given.
DEVICES = [("0", "External Headset"), ("1", "BlackHole 2ch"),
("2", "MacBook Pro Microphone")]
def devices(self):
return mock.patch.object(audio, "_avfoundation_inputs",
return_value=self.DEVICES)
def record(self, mine, theirs):
path = str(self.path("meeting.wav"))
recorder = audio.MeetingRecorder()
stopped, failed, warnings = [], [], []
recorder.stopped.connect(lambda *args: stopped.append(args))
recorder.failed.connect(failed.append)
recorder.warned.connect(warnings.append)
processes = [FakeProcess(mine), FakeProcess(theirs)]
with only_these_tools("ffmpeg"), self.devices(), \
mock.patch.object(subprocess, "Popen", side_effect=processes) as popen:
recorder.start(path, "MacBook Pro Microphone", "BlackHole 2ch")
recorder._thread.join(timeout=5)
recorder.stop()
return path, warnings, stopped, failed, processes, popen
def test_the_two_capture_processes_become_one_stereo_file(self):
path, _, stopped, failed, _, _ = self.record(
tone(1.0, freq=440), tone(1.0, freq=880)
)
self.assertEqual(failed, [])
self.assertEqual(len(stopped), 1)
with contextlib.closing(wave.open(path, "rb")) as wav:
self.assertEqual(wav.getnchannels(), 2)
self.assertEqual(wav.getframerate(), audio.RATE)
self.assertEqual(wav.getnframes(), audio.RATE)
def test_each_avfoundation_device_is_opened_by_a_different_process(self):
_, _, _, _, _, popen = self.record(tone(0.5), tone(0.5))
commands = [call.args[0] for call in popen.call_args_list]
self.assertEqual(len(commands), 2)
self.assertTrue(all(command.count("avfoundation") == 1
for command in commands))
self.assertIn(":2", commands[0])
self.assertIn(":1", commands[1])
def test_a_mostly_empty_microphone_is_said_out_loud_and_still_kept(self):
"""Half the file is everyone else, and an hour of them is worth more
than the empty channel costs."""
path, warnings, stopped, failed, _, _ = self.record(
silence(11.0), tone(11.0)
)
self.assertEqual(failed, [])
self.assertEqual(len(stopped), 1)
self.assertTrue(os.path.exists(path))
self.assertIn("empty", warnings[0])
def test_a_microphone_that_was_merely_quiet_is_not_complained_about(self):
_, warnings, stopped, _, _, _ = self.record(tone(11.0), tone(11.0))
self.assertEqual(warnings, [])
self.assertEqual(len(stopped), 1)
def test_a_capture_that_falls_silent_ends_the_meeting_rather_than_hanging(self):
"""One thread taking turns on both pipes would sit on the dead read
until somebody noticed, an hour later."""
path = str(self.path("meeting.wav"))
recorder = audio.MeetingRecorder()
stopped = []
recorder.stopped.connect(lambda *args: stopped.append(args))
mine, theirs = StalledProcess(tone(0.512)), FakeProcess(tone(30.0))
with only_these_tools("ffmpeg"), self.devices(), \
mock.patch.object(audio, "STALL_SECONDS", 0.2), \
mock.patch.object(subprocess, "Popen", side_effect=(mine, theirs)):
try:
recorder.start(path, "MacBook Pro Microphone", "BlackHole 2ch")
recorder._thread.join(timeout=2)
self.assertFalse(recorder.active)
recorder.stop()
finally:
mine.stdout.release()
self.assertAlmostEqual(stopped[0][1], 0.512, places=3)
self.assertTrue(os.path.exists(path))
def test_stopping_ends_both_capture_processes(self):
_, _, _, _, processes, _ = self.record(tone(0.5), tone(0.5))
self.assertTrue(all(process.signals for process in processes))
def test_a_legacy_numeric_target_fails_before_recording(self):
recorder = audio.MeetingRecorder()
failed = []
recorder.failed.connect(failed.append)
with only_these_tools("ffmpeg"), self.devices(), \
mock.patch.object(subprocess, "Popen") as popen:
recorder.start(str(self.path("meeting.wav")), "2", "1")
popen.assert_not_called()
self.assertIn("old numeric index", failed[0])
def test_a_second_capture_process_that_cannot_start_cleans_up_the_first(self):
"""A Mac left holding an open AVFoundation session records nothing
else until it is let go."""
path = str(self.path("meeting.wav"))
recorder = audio.MeetingRecorder()
failed = []
recorder.failed.connect(failed.append)
first = FakeProcess(tone(1.0))
with only_these_tools("ffmpeg"), self.devices(), \
mock.patch.object(subprocess, "Popen",
side_effect=(first, OSError("refused"))):
recorder.start(path, "MacBook Pro Microphone", "BlackHole 2ch")
self.assertTrue(first.signals)
self.assertIn("refused", failed[0])
self.assertFalse(os.path.exists(path))
class MacDevices(OnMacOS, DikteTest):
@@ -527,12 +708,13 @@ class MacDevices(OnMacOS, DikteTest):
def test_the_audio_half_of_the_listing_is_the_only_half_read(self):
with self.listing():
self.assertEqual(audio.list_sources(),
[("0", "MacBook Pro Microphone"), ("1", "BlackHole 2ch")])
[("MacBook Pro Microphone", "MacBook Pro Microphone"),
("BlackHole 2ch", "BlackHole 2ch")])
def test_the_index_is_what_ffmpeg_is_given_and_the_name_what_is_shown(self):
def test_the_name_is_both_saved_and_shown(self):
with self.listing():
name, description = audio.list_sources()[1]
self.assertEqual(name, "1")
self.assertEqual(name, "BlackHole 2ch")
self.assertIn("BlackHole", description)
def test_no_ffmpeg_installed(self):
@@ -557,7 +739,7 @@ class MacDevices(OnMacOS, DikteTest):
def test_the_loopback_driver_is_picked_out_by_name(self):
with self.listing():
self.assertEqual(audio.default_monitor(), "1")
self.assertEqual(audio.default_monitor(), "BlackHole 2ch")
def test_the_other_two_drivers_people_install(self):
for name in ("Loopback Audio", "Soundflower (2ch)"):
@@ -565,13 +747,43 @@ class MacDevices(OnMacOS, DikteTest):
listing = ("AVFoundation audio devices:\n"
f"[0] Built-in Microphone\n[1] {name}\n")
with self.listing(stderr=listing):
self.assertEqual(audio.default_monitor(), "1")
self.assertEqual(audio.default_monitor(), name)
def test_a_mac_with_nothing_to_record_the_far_side_from(self):
listing = "AVFoundation audio devices:\n[0] MacBook Pro Microphone\n"
with self.listing(stderr=listing):
self.assertEqual(audio.default_monitor(), "")
def test_a_saved_name_is_resolved_against_the_current_index(self):
with self.listing():
self.assertEqual(audio._resolve_avfoundation_target("BlackHole 2ch"), "1")
def test_a_saved_name_follows_the_device_when_an_earlier_one_disappears(self):
listing = ("AVFoundation audio devices:\n"
"[0] BlackHole 2ch\n[1] MacBook Pro Microphone\n")
with self.listing(stderr=listing):
self.assertEqual(
audio._resolve_avfoundation_target("MacBook Pro Microphone"), "1"
)
def test_an_old_numeric_setting_is_not_silently_reused(self):
with self.assertRaises(audio.AudioDeviceError) as caught:
audio._resolve_avfoundation_target("1")
self.assertIn("old numeric index", str(caught.exception))
def test_a_device_that_went_away_is_said_out_loud(self):
with self.listing(), self.assertRaises(audio.AudioDeviceError) as caught:
audio._resolve_avfoundation_target("USB Microphone")
self.assertIn("no longer connected", str(caught.exception))
def test_duplicate_names_are_not_guessed_between(self):
listing = ("AVFoundation audio devices:\n"
"[0] USB Microphone\n[1] USB Microphone\n")
with self.listing(stderr=listing), \
self.assertRaises(audio.AudioDeviceError) as caught:
audio._resolve_avfoundation_target("USB Microphone")
self.assertIn("More than one", str(caught.exception))
class MacRecordingCommand(OnMacOS, DikteTest):
def test_the_microphone_is_read_through_avfoundation(self):
@@ -583,7 +795,11 @@ class MacRecordingCommand(OnMacOS, DikteTest):
def test_the_empty_half_in_front_of_the_colon_is_the_missing_picture(self):
with only_these_tools("ffmpeg"):
self.assertIn(":default", audio.recording_command())
self.assertIn(":2", audio.recording_command("2"))
listing = "AVFoundation audio devices:\n[2] USB Microphone\n"
completed = FakeCompleted(returncode=1, stderr=listing)
with only_these_tools("ffmpeg"), \
mock.patch.object(subprocess, "run", return_value=completed):
self.assertIn(":2", audio.recording_command("USB Microphone"))
def test_it_captures_the_format_the_rest_of_the_code_expects(self):
with only_these_tools("ffmpeg"):
@@ -681,7 +897,7 @@ class WindowsDevices(OnWindows, DikteTest):
with self.listing():
self.assertEqual(audio.list_monitors(), [])
self.assertEqual(audio.default_monitor(), "")
self.assertEqual(audio.meeting_command("mic", "sys"), [])
self.assertEqual(audio.meeting_commands("mic", "sys"), [])
if __name__ == "__main__":
+51
View File
@@ -334,6 +334,57 @@ class ConfigCommands(DikteTest):
{"cleanup", "subtitles", "meeting", "agent"})
class ShortcutStatus(DikteTest):
"""Where the answer comes from, which is not one place on every system.
macOS keeps no shortcut registry: a combination is held by the running
process and by nothing else, so a command line that reads its own idea of
"installed" reports every shortcut as missing while all of them work.
"""
def run_cmd(self, func, **values):
with captured() as (out, err):
code = func(Options(**values))
return code, out.getvalue(), err.getvalue()
def status(self, reply):
with mock.patch.object(ipc, "send", return_value=reply) as send:
code, out, _ = self.run_cmd(cli.cmd_shortcut, shortcut="status",
json=True)
return code, json.loads(out), send
def test_what_the_running_instance_holds_is_what_is_reported(self):
code, answer, _ = self.status({
"shortcuts": {"toggle": "Ctrl+Option+Space", "cancel": None,
"ask": None, "meeting": None},
"listener": True,
})
self.assertEqual(code, 0)
self.assertEqual(answer["shortcuts"]["toggle"]["registered"],
"Ctrl+Option+Space")
self.assertIsNone(answer["shortcuts"]["cancel"]["registered"])
self.assertIs(answer["listener"], True)
def test_the_instance_is_the_one_asked(self):
_, _, send = self.status({"shortcuts": {}, "listener": False})
send.assert_called_once_with("status")
def test_nothing_running_falls_back_to_what_this_process_can_read(self):
"""Which on Linux is the registry, and on macOS is nothing, correctly
so, because there the keys really are gone with the process."""
code, answer, _ = self.status(None)
self.assertEqual(code, 0)
for name, spec in hotkey.SHORTCUTS.items():
with self.subTest(name=name):
self.assertEqual(answer["shortcuts"][name]["registered"],
hotkey.shortcut_status(spec.desktop_id))
def test_the_configured_combination_is_reported_either_way(self):
code, answer, _ = self.status(None)
self.assertEqual(answer["shortcuts"]["toggle"]["configured"],
cfg.Config()["shortcut"])
class Providers(DikteTest):
"""The terminal reaches every provider the settings window does."""
-38
View File
@@ -463,44 +463,6 @@ class Defaults(unittest.TestCase):
paste.desktop().shortcuts[0])
class Directories(unittest.TestCase):
"""Where the settings and the recordings are kept, per system."""
def test_linux_keeps_them_apart_and_follows_xdg(self):
with mock.patch.dict(os.environ, {"XDG_CONFIG_HOME": "/c",
"XDG_DATA_HOME": "/d"}):
config_dir, data_dir = cfg._directories("linux")
self.assertEqual(config_dir.as_posix(), "/c/dikte")
self.assertEqual(data_dir.as_posix(), "/d/dikte")
def test_linux_without_the_variables_set(self):
with mock.patch.dict(os.environ, {}, clear=True):
config_dir, data_dir = cfg._directories("linux")
self.assertTrue(config_dir.as_posix().endswith("/.config/dikte"))
self.assertTrue(data_dir.as_posix().endswith("/.local/share/dikte"))
def test_a_mac_keeps_both_in_application_support(self):
config_dir, data_dir = cfg._directories("darwin")
self.assertEqual(config_dir, data_dir)
self.assertTrue(config_dir.as_posix()
.endswith("/Library/Application Support/Dikte"))
def test_a_mac_does_not_read_the_xdg_variables(self):
"""A Mac with them set from some other tool still stores in one place."""
with mock.patch.dict(os.environ, {"XDG_CONFIG_HOME": "/c"}):
config_dir, _ = cfg._directories("darwin")
self.assertNotIn("/c", config_dir.as_posix())
def test_windows_keeps_settings_and_data_apart(self):
# Forward slashes, because a backslash only separates on Windows and
# this test also runs on the Linux that checks the Windows half.
with mock.patch.dict(os.environ, {"APPDATA": "C:/roam",
"LOCALAPPDATA": "C:/local"}):
config_dir, data_dir = cfg._directories("win32")
self.assertEqual(config_dir.as_posix(), "C:/roam/Dikte")
self.assertEqual(data_dir.as_posix(), "C:/local/Dikte")
if __name__ == "__main__":
unittest.main()
+1 -1
View File
@@ -236,7 +236,7 @@ class InstallProgram(Local):
with fake_urlopen(listing):
with self.assertRaises(ggml.LocalError) as caught:
ggml.install_program(ggml.WHISPER)
self.assertIn("brew install whisper-cpp", str(caught.exception))
self.assertIn("Build whisper-server yourself", str(caught.exception))
def test_a_mac_uses_the_native_llama_archive_instead_of_ubuntu(self):
self.patch_attr(sys, "platform", "darwin")
+10
View File
@@ -82,6 +82,16 @@ class Table(unittest.TestCase):
self.assertEqual([name for name, spec in hotkey.SHORTCUTS.items()
if spec.fallback], ["toggle"])
def test_the_fallback_a_mac_gets_is_not_one_macos_already_holds(self):
"""Ctrl+Space switches the input source there and Cmd+Space is
Spotlight, so the table's own fallback is Linux's and only Linux's."""
with mock.patch.object(hotkey.sys, "platform", "darwin"):
self.assertEqual(hotkey.default_combo("toggle"), "Ctrl+Option+Space")
self.assertEqual(hotkey.default_combo("cancel"), "")
with mock.patch.object(hotkey.sys, "platform", "linux"):
self.assertEqual(hotkey.default_combo("toggle"), "Ctrl+Space")
self.assertEqual(hotkey.default_combo("cancel"), "")
class ModsMatch(unittest.TestCase):
"""The combination has to be exact, or Ctrl+Space fires on Ctrl+Shift+Space."""
+36
View File
@@ -279,11 +279,16 @@ class FakeCoreGraphics:
self.flags = [] # (event, flags)
self.posted = [] # (tap, event)
self.released = []
self.prompted = [] # the options dictionaries asked with
# --- ApplicationServices
def AXIsProcessTrusted(self):
return self.trusted
def AXIsProcessTrustedWithOptions(self, options):
self.prompted.append(options)
return self.trusted
def CGEventCreateKeyboardEvent(self, source, keycode, down):
self.made.append((keycode, down))
if self.makes is not None and len(self.made) > self.makes:
@@ -305,10 +310,16 @@ class MacOS(ClipboardContract, DikteTest):
platform = "darwin"
here = paste.MACOS
# A stand-in for the CFDictionary: the real one is built out of constants
# read from the frameworks, which a fake has none of.
OPTIONS = 4242
def setUp(self):
super().setUp()
self.api = FakeCoreGraphics()
self.patch_attr(paste, "_macos_api", lambda: (self.api, self.api))
self.patch_attr(paste, "_macos_prompt_options",
lambda services, core: self.OPTIONS)
self.patch_attr(paste.time, "sleep", lambda seconds: None)
# It opens the settings pane once per run; each test gets its own run.
self.patch_attr(paste, "_asked_for_permission", False)
@@ -389,6 +400,31 @@ class MacOS(ClipboardContract, DikteTest):
paste.press("cmd+v")
self.assertIn("Accessibility", str(caught.exception))
def test_asking_is_what_puts_dikte_in_the_accessibility_list(self):
"""Opening the pane is not enough on its own.
AXIsProcessTrusted only answers the question; an application that has
never asked with the prompt is not in the list, so the pane opens on a
list Dikte is not in and the only way through is the + button.
"""
self.api.trusted = False
with self.assertRaises(paste.PasteError):
paste.press("cmd+v")
self.assertEqual(self.api.prompted, [self.OPTIONS])
# The dictionary is ours to release, and nothing else made an event.
self.assertIn(self.OPTIONS, self.api.released)
def test_it_asks_once_however_many_dictations_fail(self):
self.api.trusted = False
for _ in range(3):
with self.assertRaises(paste.PasteError):
paste.press("cmd+v")
self.assertEqual(len(self.api.prompted), 1)
def test_a_trusted_process_is_never_prompted(self):
paste.press("cmd+v")
self.assertEqual(self.api.prompted, [])
def test_readiness_is_the_permission_rather_than_a_program(self):
self.assertTrue(paste.paste_ready())
self.api.trusted = False
+83
View File
@@ -0,0 +1,83 @@
"""Where the settings and the data are kept, per system.
Its own file because the answer has to be one answer: config.py and ggml.py
both need it, and when each worked it out for itself only one of them knew
about macOS.
"""
import os
import unittest
from unittest import mock
import config as cfg
import ggml
import paths
class Directories(unittest.TestCase):
"""Spelled with forward slashes throughout.
A backslash separates on Windows only, and every one of these runs on all
three systems: `as_posix()` is the one spelling they can all be read in.
"""
def test_linux_keeps_them_apart_and_follows_xdg(self):
with mock.patch.dict(os.environ, {"XDG_CONFIG_HOME": "/c",
"XDG_DATA_HOME": "/d"}):
config_dir, data_dir = paths.directories("linux")
self.assertEqual(config_dir.as_posix(), "/c/dikte")
self.assertEqual(data_dir.as_posix(), "/d/dikte")
def test_linux_without_the_variables_set(self):
with mock.patch.dict(os.environ, {}, clear=True):
config_dir, data_dir = paths.directories("linux")
self.assertTrue(config_dir.as_posix().endswith("/.config/dikte"))
self.assertTrue(data_dir.as_posix().endswith("/.local/share/dikte"))
def test_a_mac_keeps_both_in_application_support(self):
config_dir, data_dir = paths.directories("darwin")
self.assertEqual(config_dir, data_dir)
self.assertTrue(config_dir.as_posix()
.endswith("/Library/Application Support/Dikte"))
def test_a_mac_does_not_read_the_xdg_variables(self):
"""A Mac with them set from some other tool still stores in one place."""
with mock.patch.dict(os.environ, {"XDG_CONFIG_HOME": "/c"}):
config_dir, _ = paths.directories("darwin")
self.assertNotIn("/c", config_dir.as_posix())
def test_windows_keeps_the_models_out_of_the_roaming_profile(self):
"""Settings roam with the account; several gigabytes must not."""
with mock.patch.dict(os.environ, {"APPDATA": "C:/roam",
"LOCALAPPDATA": "C:/local"}):
config_dir, data_dir = paths.directories("win32")
self.assertEqual(config_dir.as_posix(), "C:/roam/Dikte")
self.assertEqual(data_dir.as_posix(), "C:/local/Dikte")
def test_windows_without_the_variables_set(self):
with mock.patch.dict(os.environ, {}, clear=True):
config_dir, data_dir = paths.directories("win32")
self.assertTrue(config_dir.as_posix().endswith("/AppData/Roaming/Dikte"))
self.assertTrue(data_dir.as_posix().endswith("/AppData/Local/Dikte"))
class OnePlace(unittest.TestCase):
"""The programs and the models go where everything else goes.
ggml.py used to read XDG_DATA_HOME itself, which is right on Linux and
wrong on a Mac: the settings and the dictations went to ~/Library while
several gigabytes of models went to ~/.local/share, where no Mac user looks
and where `uninstall.sh --purge` would never have found them.
"""
def test_the_models_live_under_the_data_directory(self):
self.assertEqual(ggml.DATA_DIR, paths.DATA_DIR)
self.assertEqual(ggml.MODELS_DIR.parent, paths.DATA_DIR)
self.assertEqual(ggml.BIN_DIR.parent, paths.DATA_DIR)
def test_config_and_ggml_cannot_disagree(self):
self.assertEqual(cfg.DATA_DIR, ggml.DATA_DIR)
if __name__ == "__main__":
unittest.main()
+8 -2
View File
@@ -181,13 +181,19 @@ class Settings(DikteTest):
def test_emptying_a_shortcut_turns_it_off_but_not_the_toggle(self):
"""The application is unusable without the toggle, so that one box
falls back. The rest stay empty, which is how they are switched off."""
falls back. The rest stay empty, which is how they are switched off.
Which combination it falls back to is the platform's and is pinned in
test_hotkey; MacSettings runs this too, and there the answer is not
Ctrl+Space.
"""
conf = cfg.Config()
window = self.window(conf)
for box, _status, _missing in window._shortcut_rows.values():
box.setCurrentText("")
window._save()
self.assertEqual(conf["shortcut"], "Ctrl+Space")
self.assertTrue(conf["shortcut"])
self.assertEqual(conf["shortcut"], hotkey.default_combo("toggle"))
self.assertEqual(conf["cancel_shortcut"], "")
self.assertEqual(conf["assistant_shortcut"], "")
self.assertEqual(conf["meeting_shortcut"], "")