Merge master into the macOS front branch

This commit is contained in:
Gökhan
2026-08-25 00:16:20 +03:00
48 changed files with 3523 additions and 258 deletions
+16 -1
View File
@@ -25,6 +25,7 @@ from unittest import mock
from dikte import assistant
from dikte import config as cfg
from dikte import i18n
from dikte import update
# What the application is, rather than what it does: PipeWire, wl-clipboard,
# ydotool, KDE's shortcut file, /dev/input. A port to another desktop replaces
@@ -40,6 +41,18 @@ linux_only = unittest.skipUnless(
"covers the Linux desktop stack (PipeWire, wl-clipboard, ydotool, KDE)",
)
# The launchers a downloaded build writes for itself. There are two downloads,
# an AppImage and a disk image, so `integrate` has a Linux half and a macOS half
# and no third one, and the tests that pin them stand in a home laid out the way
# those two systems lay one out: paths that start at the root, a $HOME the
# library reads, a symlink for the command. None of that is a Windows machine,
# where the same code never runs. A Windows build would add an entry there and
# take the mark off these.
posix_only = unittest.skipIf(
sys.platform == "win32",
"covers what an AppImage and a .app write into the desktop they landed on",
)
def _no_network(*args, **kwargs):
raise AssertionError(
@@ -74,8 +87,10 @@ class DikteTest(unittest.TestCase):
MEETINGS_FILE=data_dir / "meetings.jsonl",
)
# Resolved from cfg.DATA_DIR when assistant was imported, so it needs
# moving on its own.
# moving on its own. The same goes for where the update check writes
# down when it last ran.
self.patch_attr(assistant, "SESSION_FILE", data_dir / "assistant.json")
self.patch_attr(update, "STATE_FILE", data_dir / "update.json")
i18n.set_language("en")
self.addCleanup(i18n.set_language, "en")
+27
View File
@@ -79,6 +79,33 @@ class Explain(DikteTest):
def test_the_status_is_carried_through(self):
self.assertEqual(self.error(429).status, 429)
def test_so_is_whether_it_is_worth_asking_again(self):
self.assertTrue(self.error(502).retryable)
self.assertFalse(self.error(401).retryable)
class Retryable(unittest.TestCase):
"""Which failures a second try can fix, and which will fail the same way."""
def test_a_gateway_that_gave_up_waiting(self):
for status in (408, 429, 500, 502, 503, 504):
with self.subTest(status=status):
self.assertTrue(api.ApiError("x", status).retryable)
def test_a_request_that_was_wrong(self):
for status in (400, 401, 402, 403, 404, 413, 422):
with self.subTest(status=status):
self.assertFalse(api.ApiError("x", status).retryable)
def test_an_error_of_our_own_is_not_the_network(self):
self.assertFalse(api.ApiError("Transcript came back empty.").retryable)
def test_a_connection_that_dropped_is_worth_a_second_try(self):
with fake_urlopen(url_error("connection reset")):
with self.assertRaises(api.ApiError) as caught:
api._request("https://example.test", b"{}", {})
self.assertTrue(caught.exception.retryable)
class ExtractError(unittest.TestCase):
def test_the_usual_shape(self):
+196
View File
@@ -887,5 +887,201 @@ class MacRecordingCommand(OnMacOS, DikteTest):
self.assertFalse(recorder.active)
class NoFarSideToRecord(DikteTest):
"""Two different answers, and the table is what tells them apart.
A sound system that records the far side has a device this machine could
not pick out, and Settings is where to choose one. A sound system that does
not had nothing to offer there in the first place, and "pick one" would
send somebody to an empty box and an installation that cannot help.
"""
def failure(self, meetings):
recorder = audio.MeetingRecorder()
failures = []
recorder.failed.connect(failures.append)
with only_these_tools("ffmpeg"), \
mock.patch.object(audio, "default_monitor", return_value=""), \
mock.patch.object(audio, "sound",
return_value=audio.PULSE._replace(
meetings=meetings)):
recorder.start(str(self.path("meeting.wav")))
self.assertFalse(recorder.active)
return failures[0]
def test_a_system_that_records_the_far_side_sends_you_to_settings(self):
self.assertIn("Settings", self.failure(True))
def test_a_system_that_does_not_says_that_instead(self):
message = self.failure(False)
self.assertIn("nothing that records what the speakers", message)
self.assertNotIn("Settings", message)
def test_the_three_sound_systems_each_answer_the_question(self):
self.assertTrue(audio.PULSE.meetings)
self.assertTrue(audio.COREAUDIO.meetings)
self.assertFalse(audio.DSHOW.meetings)
class OnWindows:
"""A test that runs as if the machine ran Windows."""
def setUp(self):
super().setUp()
self.enterContext(mock.patch.object(sys, "platform", "win32"))
class WindowsDevices(OnWindows, DikteTest):
"""The one ffmpeg listing the device questions are answered from.
dshow names devices rather than numbering them, and the names carry
whatever alphabet the machine speaks, so the listing here does too.
"""
MIC = "@device_cm_{33D9A762}\\wave_{B1C2}"
LISTING = (
'[dshow @ 0000020c] "Integrated Camera" (video)\n'
'[dshow @ 0000020c] Alternative name "@device_pnp_\\...."\n'
'[dshow @ 0000020c] "Mikrofon Dizisi (Intel Smart Sound)" (audio)\n'
f'[dshow @ 0000020c] Alternative name "{MIC}"\n'
'[dshow @ 0000020c] "Kulaklık (Soundcore Life Q30)" (audio)\n'
'[dshow @ 0000020c] Could not find audio only device with name '
'"dummy" among source devices of type audio.\n'
"dummy: Immediate exit requested\n"
).encode("utf-8")
def setUp(self):
super().setUp()
# The listing is remembered between calls, so that a dictation does not
# run ffmpeg of its own. It cannot be remembered between tests.
audio._DSHOW_SEEN.clear()
self.addCleanup(audio._DSHOW_SEEN.clear)
@contextlib.contextmanager
def listing(self, stderr=None, tools=("ffmpeg",)):
completed = FakeCompleted(
returncode=1, stderr=self.LISTING if stderr is None else stderr)
with only_these_tools(*tools), \
mock.patch.object(subprocess, "run",
return_value=completed) as run:
yield run
def test_windows_records_through_dshow(self):
self.assertIs(audio.sound(), audio.DSHOW)
def test_the_audio_devices_are_the_only_ones_read(self):
with self.listing():
self.assertEqual(audio.list_sources(), [
(self.MIC, "Mikrofon Dizisi (Intel Smart Sound)"),
("Kulaklık (Soundcore Life Q30)",
"Kulaklık (Soundcore Life Q30)"),
])
def test_the_device_ffmpeg_could_not_open_is_not_one_of_them(self):
"""The command ends by quoting the name it was sent to look for."""
with self.listing():
self.assertNotIn("dummy", [name for _, name in audio.list_sources()])
def test_a_listing_from_ffmpeg_8_which_renamed_the_prefix(self):
"""ffmpeg 8 writes `[in#0 @ ...]` where older builds wrote `[dshow @ ...]`."""
listing = (
'[in#0 @ 00000238c3300ac0] "Integrated Camera" (video)\n'
'[in#0 @ 00000238c3300ac0] Alternative name "@device_pnp_\\..."\n'
'[in#0 @ 00000238c3300ac0] "OBS Virtual Camera" (none)\n'
'[in#0 @ 00000238c3300ac0] Alternative name "@device_sw_{860B}"\n'
'[in#0 @ 00000238c3300ac0] "Mikrofon Dizisi (Intel® Smart Sound)" (audio)\n'
'[in#0 @ 00000238c3300ac0] Alternative name "@device_cm_{33D9}"\n'
"Error opening input file dummy.\n"
).encode("utf-8")
with self.listing(stderr=listing):
self.assertEqual(audio.list_sources(),
[("@device_cm_{33D9}",
"Mikrofon Dizisi (Intel® Smart Sound)")])
def test_a_listing_from_an_ffmpeg_that_marks_nothing(self):
"""Older builds print a heading instead of an (audio) on every line."""
listing = (
'[dshow @ 0] DirectShow video devices\n'
'[dshow @ 0] "Integrated Camera"\n'
'[dshow @ 0] Alternative name "@device_pnp_\\..."\n'
'[dshow @ 0] DirectShow audio devices\n'
'[dshow @ 0] "Microphone (Realtek Audio)"\n'
'[dshow @ 0] Alternative name "@device_cm_{ABCD}"\n'
).encode("utf-8")
with self.listing(stderr=listing):
self.assertEqual(audio.list_sources(),
[("@device_cm_{ABCD}", "Microphone (Realtek Audio)")])
def test_two_devices_called_the_same_thing_stay_apart(self):
"""The normal state of a laptop with a headset plugged into it."""
listing = (
'[dshow @ 0] "Microphone" (audio)\n'
'[dshow @ 0] Alternative name "@device_cm_{ONE}"\n'
'[dshow @ 0] "Microphone" (audio)\n'
'[dshow @ 0] Alternative name "@device_cm_{TWO}"\n'
).encode("utf-8")
with self.listing(stderr=listing):
sources = audio.list_sources()
self.assertEqual([identifier for identifier, _ in sources],
["@device_cm_{ONE}", "@device_cm_{TWO}"])
self.assertEqual({name for _, name in sources}, {"Microphone"})
def test_no_ffmpeg_installed(self):
with only_these_tools():
self.assertEqual(audio.list_sources(), [])
self.assertEqual(audio.recording_command(), [])
def test_the_identifier_is_what_the_recorder_is_given_back(self):
with self.listing():
cmd = audio.recording_command(self.MIC)
self.assertEqual(cmd[cmd.index("-f") + 1], "dshow")
self.assertIn(f"audio={self.MIC}", cmd)
def test_no_microphone_named_means_the_first_one_listed(self):
"""dshow has no default device for an empty target to mean."""
with self.listing():
self.assertIn(f"audio={self.MIC}", audio.recording_command())
def test_a_dictation_does_not_run_a_listing_of_its_own(self):
"""Two hundred milliseconds of ffmpeg in front of every key press."""
with self.listing() as run:
audio.list_sources()
audio.recording_command()
audio.recording_command()
self.assertEqual(run.call_count, 1)
def test_opening_the_device_list_asks_again(self):
"""Which is what somebody who has just plugged one in does."""
with self.listing() as run:
audio.list_sources()
audio.list_sources()
self.assertEqual(run.call_count, 2)
def test_a_machine_with_no_microphone_at_all(self):
with self.listing(stderr=b'[dshow @ 0] "Integrated Camera" (video)\n'):
self.assertEqual(audio.recording_command(), [])
def test_an_ffmpeg_that_will_not_run(self):
with only_these_tools("ffmpeg"), \
mock.patch.object(subprocess, "run", side_effect=OSError("nope")):
self.assertEqual(audio.list_sources(), [])
def test_nothing_offers_the_far_side_of_a_meeting(self):
"""What the speakers play is not a capture device Windows hands out."""
with self.listing():
self.assertEqual(audio.list_monitors(), [])
self.assertEqual(audio.default_monitor(), "")
self.assertEqual(audio.meeting_commands("mic", "sys"), [])
def test_a_meeting_says_what_is_wrong_rather_than_where_to_look(self):
recorder = audio.MeetingRecorder()
failures = []
recorder.failed.connect(failures.append)
with self.listing():
recorder.start(str(self.path("meeting.wav")))
self.assertIn("nothing that records what the speakers", failures[0])
self.assertFalse(recorder.active)
if __name__ == "__main__":
unittest.main()
+135 -1
View File
@@ -10,13 +10,20 @@ import contextlib
import io
import json
import unittest
import webbrowser
from typing import ClassVar
from unittest import mock
from dikte import audio
from dikte import cli
from dikte import config as cfg
from dikte import ggml
from dikte import hotkey
from dikte import hub
from dikte import ipc
from tests.support import DikteTest, fake_urlopen
from dikte import paste
from dikte import update
from tests.support import DikteTest, fake_urlopen, only_these_tools, url_error
class Options:
@@ -417,6 +424,68 @@ class Providers(DikteTest):
self.assertIn("Groq", out)
class Updates(DikteTest):
"""`dikte update` looks, says what it found, and installs nothing."""
RELEASE: ClassVar[dict] = {
"tag_name": "v9.9.9",
"html_url": "https://github.com/yusufipk/dikte/releases/tag/v9.9.9",
}
def setUp(self):
super().setUp()
self.patch_attr(hub, "CACHE_DIR", self.path("cache"))
# Nothing here may reach a browser, whatever the answer turns out to be.
self.opened = []
self.patch_attr(webbrowser, "open", self.opened.append)
def run_update(self, reply, **values):
with fake_urlopen(reply), captured() as (out, err):
code = cli.cmd_update(Options(open=False, **values))
return code, out.getvalue(), err.getvalue()
def test_a_newer_release_is_named_with_its_page(self):
code, out, _ = self.run_update(self.RELEASE)
self.assertEqual(code, 0)
self.assertIn("9.9.9", out)
self.assertIn(self.RELEASE["html_url"], out)
def test_this_build_being_the_newest_is_not_a_failure(self):
code, out, _ = self.run_update({"tag_name": f"v{cli.__version__}"})
self.assertEqual(code, 0)
self.assertIn("newest", out)
def test_the_json_answer_says_both_numbers(self):
code, out, _ = self.run_update(self.RELEASE, json=True)
answer = json.loads(out)
self.assertTrue(answer["update"])
self.assertEqual(answer["latest"], "9.9.9")
self.assertEqual(answer["current"], cli.__version__)
def test_github_being_unreachable_is_a_failure_with_a_reason(self):
with fake_urlopen(url_error("no route to host")), captured() as (_, err):
code = cli.cmd_update(Options(open=False))
self.assertEqual(code, 1)
self.assertIn("api.github.com", err.getvalue())
def test_the_browser_is_opened_only_when_asked_and_only_when_there_is_one(self):
self.run_update(self.RELEASE)
self.assertEqual(self.opened, [])
with fake_urlopen({"tag_name": f"v{cli.__version__}"}), captured():
cli.cmd_update(Options(open=True))
self.assertEqual(self.opened, [])
with fake_urlopen(self.RELEASE), captured():
cli.cmd_update(Options(open=True))
self.assertEqual(self.opened, [self.RELEASE["html_url"]])
def test_the_answer_is_written_down_for_the_application(self):
"""A check at a terminal is a check; the tray must not go and ask the
same question an hour later."""
self.run_update(self.RELEASE)
self.assertEqual(update.state()["version"], "9.9.9")
self.assertFalse(update.due())
class Doctor(DikteTest):
"""One pass over everything the settings window checks behind its buttons."""
@@ -434,6 +503,30 @@ class Doctor(DikteTest):
self.assertIn("OpenRouter key, cleaning up on some/model",
self.run_doctor(as_json=False, cleanup_model="some/model"))
def test_it_asks_after_the_programs_this_desktop_actually_uses(self):
"""A missing ydotool on a Mac is a red mark with nothing behind it."""
with mock.patch.object(cli.paste, "desktop", return_value=paste.MACOS):
mac = self.run_doctor()["programs"]
with mock.patch.object(cli.paste, "desktop", return_value=paste.WAYLAND):
wayland = self.run_doctor()["programs"]
self.assertIn("pbcopy", mac)
self.assertNotIn("ydotool", mac)
self.assertIn("ydotool", wayland)
self.assertIn("ffmpeg", mac) # the one every system records through
def test_a_system_that_shells_out_for_neither_half_is_asked_for_neither(self):
# shutil.which is faked as well as the platform: the real one reads
# sys.platform too, and reaches for a Windows API this machine has not
# got the moment it is told it is on Windows.
with mock.patch.object(cli.paste, "desktop", return_value=paste.WINDOWS), \
only_these_tools("ffmpeg"), \
mock.patch.object(cli.sys, "platform", "win32"):
programs = self.run_doctor()["programs"]
self.assertNotIn("", programs)
self.assertEqual([name for name in ("wl-copy", "ydotool", "pactl",
"pw-record", "kwriteconfig6")
if name in programs], [])
def test_cleanup_on_a_cli_is_a_question_about_the_program(self):
reply = self.run_doctor(cleanup_provider="codex",
cleanup_codex_model="gpt-5.4")
@@ -445,6 +538,25 @@ class Doctor(DikteTest):
cleanup_codex_model="gpt-5.4"))
class Devices(DikteTest):
def test_a_machine_with_nothing_names_its_own_missing_program(self):
"""The Windows README sends people here, and pactl is not on it."""
for here, expected in ((audio.DSHOW, "ffmpeg"),
(audio.PULSE, "pulseaudio-utils")):
with self.subTest(sound=expected):
with mock.patch.object(cli.audio, "sound", return_value=here), \
mock.patch.object(cli.audio, "list_sources",
return_value=[]), \
mock.patch.object(cli.audio, "list_monitors",
return_value=[]), \
mock.patch.object(cli.audio, "default_monitor",
return_value=""), \
captured() as (out, _err):
code = cli.cmd_devices(Options(json=True))
self.assertEqual(code, 1)
self.assertIn(expected, json.loads(out.getvalue())["error"])
class Finding(DikteTest):
def test_no_history_at_all(self):
self.assertIsNone(cli._find_history("last"))
@@ -623,5 +735,27 @@ class Replies(DikteTest):
self.assertFalse(launched.called)
class TranscribeRunsHere(DikteTest):
"""`dikte transcribe` runs in this process, not in the instance."""
def test_the_local_servers_are_handed_the_settings_first(self):
# The GUI does this at startup; a CLI run has no GUI to have done it,
# and without it the whisper server holds an empty model name.
wav = self.path("clip.wav")
wav.write_bytes(b"RIFF not really audio")
self.write_config({"local_model": "ggml-base.bin"})
self.addCleanup(ggml.whisper.configure,
model="", threads=0, gpu=True, binary="")
opts = cli.build_parser().parse_args(["transcribe", str(wav)])
with mock.patch.object(cli.filetranscribe, "FileTranscriber"), \
mock.patch.object(cli, "_headless",
return_value={"error": "stopped"}), \
captured():
cli.cmd_transcribe(opts)
self.assertEqual(ggml.whisper.settings()["model"], "ggml-base.bin")
if __name__ == "__main__":
unittest.main()
+6
View File
@@ -8,6 +8,7 @@ config and now shadows the default.
import json
import os
import sys
import unittest
from unittest import mock
@@ -89,6 +90,8 @@ class Saving(DikteTest):
cfg.Config().save()
self.assertTrue(cfg.CONFIG_FILE.exists())
@unittest.skipIf(sys.platform == "win32",
"NTFS access is decided by ACLs, not by the mode bits")
def test_the_file_is_readable_by_nobody_else(self):
"""It holds two API keys."""
cfg.Config().save()
@@ -491,6 +494,9 @@ class ReadyToRun(DikteTest):
def setUp(self):
super().setUp()
self.patch_attr(ggml, "MODELS_DIR", self.path("models"))
# A machine Dikte is actually installed on would otherwise answer for
# the "missing program" below through the real install record.
self.patch_attr(ggml, "BIN_DIR", self.path("bin"))
def install(self, name):
path = ggml.whisper_model_path(name)
+68 -4
View File
@@ -214,10 +214,22 @@ class ChunkSeconds(DikteTest):
self.assertEqual(ft.chunk_seconds(self.file(1024), 600), 0.0)
def test_a_file_over_the_limit_is_cut_by_what_it_measured(self):
# Twice the limit over an hour, so a little under half an hour fits.
seconds = ft.chunk_seconds(self.file(ft.UPLOAD_LIMIT * 2), 3600)
self.assertGreater(seconds, 1500)
self.assertLess(seconds, 1800)
# Twice the limit over twenty minutes, so a little under ten fits.
seconds = ft.chunk_seconds(self.file(ft.UPLOAD_LIMIT * 2), 1200)
self.assertGreater(seconds, 500)
self.assertLess(seconds, 600)
def test_a_chunk_is_never_more_audio_than_a_request_can_outlive(self):
"""An hour in one request is a 502 from the gateway, whatever it weighs."""
self.assertEqual(ft.chunk_seconds(self.file(ft.UPLOAD_LIMIT * 2), 3600),
ft.MAX_CHUNK_SECONDS)
def test_a_small_file_that_is_still_hours_long_is_cut_on_the_clock(self):
self.assertEqual(ft.chunk_seconds(self.file(1024), 7200),
ft.MAX_CHUNK_SECONDS)
def test_a_file_short_enough_on_both_counts_is_not_cut(self):
self.assertEqual(ft.chunk_seconds(self.file(1024), ft.MAX_CHUNK_SECONDS), 0.0)
def test_a_file_with_no_length_is_left_whole(self):
self.assertEqual(ft.chunk_seconds(self.file(ft.UPLOAD_LIMIT * 2), 0), 0.0)
@@ -378,6 +390,58 @@ class Transcriber(DikteTest):
worker.stop()
self.assertTrue(worker._abort.aborted)
def test_a_chunk_is_given_longer_to_answer_than_a_dictation(self):
"""A quarter hour of audio is not a sentence: the default would cut it off."""
worker = ft.FileTranscriber(self.conf)
with mock.patch.object(ft, "_to_wav", side_effect=lambda *a: self.source), \
mock.patch.object(ft, "_to_mp3",
side_effect=lambda path, *a, **k: path), \
mock.patch.object(ft.shutil, "which", return_value="/usr/bin/ffmpeg"), \
mock.patch.object(api, "transcribe", return_value="text") as call:
worker._work(self.source, False, False)
self.assertEqual(call.call_args.kwargs["timeout"], ft.HOSTED_TIMEOUT)
def test_a_gateway_having_a_bad_moment_is_asked_again(self):
with mock.patch.object(ft.FileTranscriber, "_wait"):
done, failures, progress, _ = self.run_chain(
fail=[api.ApiError("HTTP 502: timeout", 502), "raw text"])
self.assertEqual(failures, [])
self.assertEqual(done[0][0], "raw text")
self.assertTrue(any("Trying again" in message for message in progress))
def test_a_rejected_key_is_not_asked_again(self):
"""Trying again with the same key is only a slower way to fail."""
call = mock.Mock(side_effect=api.ApiError("rejected the API key", 401))
with mock.patch.object(ft.FileTranscriber, "_wait"):
_, failures, _, _ = self.run_chain(fail=call)
self.assertEqual(call.call_count, 1)
self.assertIn("rejected", failures[0])
def test_a_chunk_is_given_up_on_after_the_last_try(self):
call = mock.Mock(side_effect=api.ApiError("HTTP 502: timeout", 502))
with mock.patch.object(ft.FileTranscriber, "_wait"):
_, failures, _, _ = self.run_chain(fail=call)
self.assertEqual(call.call_count, ft.RETRIES)
self.assertIn("502", failures[0])
def test_what_was_heard_before_the_failure_is_still_handed_over(self):
"""An hour already transcribed is not thrown away over the chunk after it."""
boom = api.ApiError("HTTP 502: timeout", 502)
with mock.patch.object(ft.FileTranscriber, "_wait"), \
mock.patch.object(ft.FileTranscriber, "_chunks",
side_effect=lambda wav, *a: [(wav, 0.0), (wav, 10.0)]):
done, failures, _, _ = self.run_chain(
fail=["first half"] + [boom] * ft.RETRIES)
self.assertEqual(done[0][0], "first half")
self.assertIn("502", failures[0])
def test_nothing_heard_at_all_is_a_plain_failure(self):
call = mock.Mock(side_effect=api.ApiError("rejected the API key", 401))
with mock.patch.object(ft.FileTranscriber, "_wait"):
done, failures, _, _ = self.run_chain(fail=call)
self.assertEqual(done, [])
self.assertEqual(failures[0], "rejected the API key")
def test_a_second_start_while_one_is_running_is_ignored(self):
worker = ft.FileTranscriber(self.conf)
worker._thread = mock.Mock(is_alive=lambda: True)
+94
View File
@@ -15,6 +15,7 @@ import tarfile
import textwrap
import threading
import time
import zipfile
from unittest import mock
from dikte import ggml
@@ -77,6 +78,15 @@ def tarball(entries):
return buf.getvalue()
def zipball(entries):
"""A .zip laid out the way the Windows releases are."""
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as bundle:
for name, content in entries.items():
bundle.writestr(name, content)
return buf.getvalue()
class Local(DikteTest):
"""A test with its own bin, models and cache directories."""
@@ -691,3 +701,87 @@ class Sizes(DikteTest):
self.assertEqual(ggml.human_size(512), "512 B")
self.assertEqual(ggml.human_size(574041195), "547.4 MB")
self.assertEqual(ggml.human_size(3_095_033_483), "2.9 GB")
# --- Windows ----------------------------------------------------------------
class WindowsAssets(Local):
"""Which archive a Windows machine is handed."""
def setUp(self):
super().setUp()
self.patch_attr(sys, "platform", "win32")
self.patch_attr(ggml, "_arch", lambda: "x64")
def test_whisper_prefers_the_blas_build(self):
# On a plain CPU it transcribes about twice as fast as the stock one.
self.assertEqual(ggml._wanted_assets(ggml.WHISPER),
("whisper-blas-bin-x64.zip", "whisper-bin-x64.zip"))
def test_llama_takes_the_vulkan_build_when_there_is_a_loader(self):
self.patch_attr(ggml, "_has_vulkan", lambda: True)
self.assertEqual(ggml._wanted_assets(ggml.LLAMA),
("bin-win-vulkan-x64.zip", "bin-win-cpu-x64.zip"))
def test_llama_falls_back_to_the_cpu_build_without_one(self):
self.patch_attr(ggml, "_has_vulkan", lambda: False)
self.assertEqual(ggml._wanted_assets(ggml.LLAMA),
("bin-win-cpu-x64.zip",))
def test_an_arm_machine_is_not_handed_the_x64_build(self):
self.patch_attr(ggml, "_arch", lambda: "arm64")
self.patch_attr(ggml, "_has_vulkan", lambda: True)
self.assertEqual(ggml._wanted_assets(ggml.LLAMA),
("bin-win-cpu-arm64.zip",))
def test_an_arm_machine_is_handed_the_x64_whisper_anyway(self):
"""whisper.cpp publishes no arm64 build for Windows: the release has
Win32 and x64 and nothing else, so emulated is the only local option
a Snapdragon has. Pinned here so that a release which does start
publishing one is noticed rather than quietly ignored."""
self.patch_attr(ggml, "_arch", lambda: "arm64")
self.assertEqual(ggml._wanted_assets(ggml.WHISPER),
("whisper-blas-bin-x64.zip", "whisper-bin-x64.zip"))
class InstallOnWindows(Local):
"""The Windows releases are zips, and the binary carries .exe."""
def setUp(self):
super().setUp()
self.patch_attr(sys, "platform", "win32")
self.patch_attr(ggml, "_arch", lambda: "x64")
self.archive = zipball({
"Release/whisper-server.exe": b"MZ not really a program",
"Release/whisper.dll": b"not really a library",
})
def release(self, *names):
digest = hashlib.sha256(self.archive)
return {"tag_name": "v1.9.1", "assets": [
{"name": name, "browser_download_url": f"https://example.invalid/{name}",
"size": 10, "digest": "sha256:" + digest.hexdigest()}
for name in names]}
def test_the_zip_lands_and_the_exe_inside_it_is_found(self):
with serving(self.release("whisper-blas-bin-x64.zip"), self.archive):
path = ggml.install_program(ggml.WHISPER)
self.assertTrue(path.endswith("whisper-server.exe"))
self.assertTrue(os.path.isfile(path))
self.assertTrue(os.path.isfile(os.path.join(os.path.dirname(path),
"whisper.dll")))
self.assertEqual(ggml.installed_program(ggml.WHISPER), path)
def test_the_blas_build_is_the_one_fetched_when_both_are_offered(self):
listing = self.release("whisper-bin-x64.zip", "whisper-blas-bin-x64.zip")
with serving(listing, self.archive) as calls:
ggml.install_program(ggml.WHISPER)
urls = [call.args[0].full_url for call in calls.call_args_list]
self.assertTrue(urls[1].endswith("whisper-blas-bin-x64.zip"))
def test_a_release_with_nothing_for_windows_says_so(self):
with fake_urlopen(json_body(self.release("whisper-bin-ubuntu-x64.tar.gz"))):
with self.assertRaises(ggml.LocalError) as caught:
ggml.install_program(ggml.WHISPER)
self.assertIn("this machine", str(caught.exception))
+203
View File
@@ -2,10 +2,14 @@
import contextlib
import os
import queue
import subprocess
import time
import unittest
from unittest import mock
from PyQt6.QtCore import Qt
from dikte import config as cfg
from dikte import hotkey
from tests.support import DikteTest, FakeCompleted, linux_only
@@ -753,5 +757,204 @@ class MacChooser(DikteTest):
self.assertFalse(hotkey.valid_shortcut("Cmd+Space"))
# --- Windows ----------------------------------------------------------------
class ParseWindowsShortcut(unittest.TestCase):
def test_the_default(self):
self.assertEqual(hotkey.parse_windows_shortcut("Ctrl+Space"),
(hotkey.WIN_MODS["ctrl"], 0x20))
def test_case_and_spacing_do_not_matter(self):
self.assertEqual(hotkey.parse_windows_shortcut(" ctrl + SPACE "),
hotkey.parse_windows_shortcut("Ctrl+Space"))
def test_several_modifiers_are_one_number(self):
modifiers, key = hotkey.parse_windows_shortcut("Ctrl+Shift+M")
self.assertEqual(modifiers,
hotkey.WIN_MODS["ctrl"] | hotkey.WIN_MODS["shift"])
self.assertEqual(key, hotkey.WIN_KEYS["m"])
def test_the_synonyms_land_on_one_number(self):
for name in ("meta", "super", "win"):
with self.subTest(name=name):
self.assertEqual(hotkey.parse_windows_shortcut(f"{name}+space"),
(hotkey.WIN_MODS["win"], 0x20))
self.assertEqual(hotkey.parse_windows_shortcut("Control+Space"),
hotkey.parse_windows_shortcut("Ctrl+Space"))
def test_a_key_on_its_own(self):
self.assertEqual(hotkey.parse_windows_shortcut("F9"),
(0, hotkey.WIN_KEYS["f9"]))
def test_modifiers_with_no_key(self):
self.assertEqual(hotkey.parse_windows_shortcut("Ctrl+Alt"), (None, None))
def test_a_key_nobody_mapped(self):
self.assertEqual(hotkey.parse_windows_shortcut("Ctrl+F13"), (None, None))
def test_something_that_is_not_even_a_string(self):
self.assertEqual(hotkey.parse_windows_shortcut(None), (None, None))
class FakeWinHotkeys:
"""user32 and kernel32, as much of both as the listener calls.
The message queue is a real queue: GetMessageW blocks on it the way the
real one blocks on the thread's, so the listener runs its actual loop and
a test presses the key by posting the message a press would.
"""
def __init__(self):
self.registered = {} # identifier -> (modifiers, key)
self.refused = set() # (modifiers, key) another program holds
self.unregistered = []
self.queue = queue.Queue()
# --- user32
def RegisterHotKey(self, hwnd, identifier, modifiers, key):
if (modifiers & ~hotkey.WIN_MOD_NOREPEAT, key) in self.refused:
return 0
self.registered[identifier] = (modifiers, key)
return 1
def UnregisterHotKey(self, hwnd, identifier):
self.unregistered.append(identifier)
self.registered.pop(identifier, None)
return 1
def PeekMessageW(self, reference, hwnd, low, high, remove):
return 0
def GetMessageW(self, reference, hwnd, low, high):
kind, wparam = self.queue.get()
if kind == hotkey.WM_QUIT:
return 0
message = reference._obj
message.message = kind
message.wParam = wparam
return 1
def PostThreadMessageW(self, thread_id, message, wparam, lparam):
self.queue.put((message, wparam))
return 1
# --- kernel32
def GetCurrentThreadId(self):
return 1
# --- the keyboard
def press(self, identifier):
self.queue.put((hotkey.WM_HOTKEY, identifier))
class WinListener(DikteTest):
"""What the listener asks Windows for, without a Windows to ask."""
def setUp(self):
super().setUp()
self.api = FakeWinHotkeys()
self.patch_attr(hotkey, "_win_input", lambda: (self.api, self.api))
self.addCleanup(hotkey._REGISTERED.clear)
self.listener = hotkey.WinHotkey()
self.addCleanup(self.listener.stop)
self.failures = []
# Direct, because the emits come from the listener's own thread and
# there is no event loop here to carry a queued one across.
self.listener.failed.connect(self.failures.append,
Qt.ConnectionType.DirectConnection)
@staticmethod
def settles(seen, count=1):
"""The signals arrive from the listener's own thread, not this one."""
deadline = time.monotonic() + 2
while len(seen) < count and time.monotonic() < deadline:
time.sleep(0.01)
return seen
def test_every_binding_is_registered_with_its_modifiers(self):
self.assertTrue(self.listener.start({"toggle": "Ctrl+Space",
"cancel": "Ctrl+Shift+Space"}))
norepeat = hotkey.WIN_MOD_NOREPEAT
self.assertEqual(self.api.registered, {
1: (hotkey.WIN_MODS["ctrl"] | norepeat, 0x20),
2: (hotkey.WIN_MODS["ctrl"] | hotkey.WIN_MODS["shift"] | norepeat, 0x20),
})
def test_what_landed_is_what_the_status_line_shows(self):
self.listener.start({"toggle": "Ctrl+Space"})
self.assertEqual(hotkey._REGISTERED,
{hotkey.DESKTOP_ID: "Ctrl+Space"})
def test_a_press_arrives_under_the_name_it_was_registered_as(self):
seen = []
self.listener.triggered.connect(seen.append,
Qt.ConnectionType.DirectConnection)
self.listener.start({"toggle": "Ctrl+Space", "cancel": "Ctrl+Shift+Space"})
self.api.press(2)
self.assertEqual(self.settles(seen), ["cancel"])
def test_a_held_combination_is_reported_and_the_rest_still_land(self):
self.api.refused = {(hotkey.WIN_MODS["ctrl"], 0x20)}
started = self.listener.start({"toggle": "Ctrl+Space",
"cancel": "Ctrl+Shift+Space"})
self.assertTrue(started)
self.assertIn("Ctrl+Space", self.settles(self.failures)[0])
self.assertEqual(list(self.api.registered), [2])
def test_an_unparsable_binding_is_reported(self):
self.assertFalse(self.listener.start({"toggle": "Ctrl+F13"}))
self.assertIn("Ctrl+F13", self.failures[0])
def test_nothing_but_empty_bindings_does_not_start(self):
self.assertFalse(self.listener.start({"toggle": "", "cancel": ""}))
self.assertFalse(self.listener.running)
def test_stop_lets_go_of_everything(self):
self.listener.start({"toggle": "Ctrl+Space", "cancel": "Ctrl+Shift+Space"})
self.listener.stop()
self.assertEqual(self.api.registered, {})
self.assertEqual(hotkey._REGISTERED, {})
self.assertFalse(self.listener.running)
def test_a_second_start_is_a_clean_slate(self):
self.listener.start({"toggle": "Ctrl+Space"})
self.assertTrue(self.listener.start({"toggle": "Ctrl+Shift+Space"}))
self.assertEqual(self.api.registered,
{1: (hotkey.WIN_MODS["ctrl"] | hotkey.WIN_MODS["shift"]
| hotkey.WIN_MOD_NOREPEAT, 0x20)})
class WindowsChooser(DikteTest):
def setUp(self):
super().setUp()
self.enterContext(mock.patch.object(hotkey.sys, "platform", "win32"))
self.addCleanup(hotkey._REGISTERED.clear)
def test_the_listener_is_the_windows_hotkey_service(self):
self.assertIsInstance(hotkey.listener(), hotkey.WinHotkey)
def test_a_combination_is_checked_against_the_windows_table(self):
self.assertTrue(hotkey.valid_shortcut("Ctrl+Space"))
self.assertFalse(hotkey.valid_shortcut("Ctrl+F13"))
def test_no_registry_to_write_into_and_no_restart_to_wait_for(self):
self.assertFalse(hotkey.installs_shortcuts())
self.assertFalse(hotkey.shortcut_needs_restart())
self.assertEqual(hotkey.desktop_name(), "Windows")
def test_installing_records_it_rather_than_writing_anything(self):
with mock.patch.object(hotkey.subprocess, "run") as run:
ok, message = hotkey.install_shortcut("Ctrl+Space", "dikte toggle")
run.assert_not_called()
self.assertTrue(ok)
self.assertEqual(hotkey.shortcut_status(), "Ctrl+Space")
hotkey.remove_shortcut()
self.assertIsNone(hotkey.shortcut_status())
def test_no_list_of_conflicts_to_read(self):
"""Not even KDE's file, which a dual-boot home directory could hold."""
self.assertEqual(hotkey.conflicting_shortcuts("Ctrl+Space"), [])
if __name__ == "__main__":
unittest.main()
+130
View File
@@ -9,12 +9,14 @@ has to notice AppImageLauncher's entry, which is not under the name ours is.
import os
import pathlib
import plistlib
import re
import sys
import tempfile
import unittest
from unittest import mock
from dikte import integrate
from tests.support import posix_only
class Frozen:
@@ -75,6 +77,7 @@ class WhatToStart(unittest.TestCase):
def test_a_checkout_names_this_interpreter_and_the_entry_point(self):
self.assertFalse(integrate.packaged())
@posix_only
def test_an_appimage_names_the_file_and_not_the_mount(self):
"""The mount is a fresh /tmp path every run; a shortcut written to it
would work until the next login and never again."""
@@ -83,6 +86,7 @@ class WhatToStart(unittest.TestCase):
self.assertEqual(str(integrate.target()),
"/home/someone/Downloads/Dikte.AppImage")
@posix_only
def test_a_mac_names_the_bundle_and_not_the_executable_inside_it(self):
with Frozen("/Applications/Dikte.app/Contents/MacOS/Dikte",
platform="darwin"):
@@ -95,6 +99,7 @@ class WhatToStart(unittest.TestCase):
class BundledTools(unittest.TestCase):
"""The ffmpeg the disk image carries, and how anything finds it."""
@posix_only
def test_a_mac_looks_beside_the_bundle_not_beside_the_executable(self):
with Frozen("/Applications/Dikte.app/Contents/MacOS/Dikte",
platform="darwin"):
@@ -213,6 +218,7 @@ class Certificates(unittest.TestCase):
self.assertIsNone(integrate.use_system_certificates())
@posix_only
class Linux(Home):
def install(self, appimage, force=False):
with Frozen("/tmp/.mount_x/usr/bin/dikte", appimage=str(appimage),
@@ -348,6 +354,7 @@ class Linux(Home):
self.assertEqual(integrate.ensure(), [])
@posix_only
class MacOS(Home):
def agent(self):
return self.home / "Library/LaunchAgents/io.github.yusufipk.dikte.plist"
@@ -358,6 +365,12 @@ class MacOS(Home):
mock.patch.object(integrate, "_launchctl_reload"):
return integrate.install(force=force)
def remove(self):
with Frozen("/Applications/Dikte.app/Contents/MacOS/Dikte",
home=self.home, platform="darwin"), \
mock.patch("subprocess.run"):
return integrate.remove()
def test_it_writes_a_login_item_and_the_command(self):
app = self.home / "Applications" / "Dikte.app"
(app / "Contents/MacOS").mkdir(parents=True)
@@ -419,6 +432,123 @@ class MacOS(Home):
self.install(app)
self.assertIn("install-mac.sh", command.read_text())
def test_removing_takes_back_the_login_item_and_command_it_wrote(self):
app = self.home / "Applications" / "Dikte.app"
(app / "Contents/MacOS").mkdir(parents=True)
self.install(app)
command = self.home / ".local/bin/dikte"
self.assertEqual(self.remove(), [self.agent(), command])
self.assertFalse(self.agent().exists())
self.assertFalse(command.exists())
def test_removing_leaves_another_installers_command_alone(self):
command = self.home / ".local/bin/dikte"
command.parent.mkdir(parents=True)
command.write_text("#!/bin/sh\n# Written by install-mac.sh.\n"
"exec /usr/local/bin/python3 /src/__main__.py \"$@\"\n")
self.assertEqual(self.remove(), [])
self.assertIn("install-mac.sh", command.read_text())
class Windows(unittest.TestCase):
"""The half of the Windows install the setup program cannot do.
It writes the Start Menu entry, the command and the uninstaller as it runs,
and asks once whether Dikte should start at sign-in. Changing that answer
afterwards is what is left here, and the value is faked rather than the
registry, so that all of it is read on every platform the tests run on.
"""
def setUp(self):
self.value = ""
for name, function in (("_run_entry", lambda: self.value),
("_write_run_entry", self._write),
("_delete_run_entry", self._delete)):
patch = mock.patch.object(integrate, name, function)
patch.start()
self.addCleanup(patch.stop)
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.installed = pathlib.Path(self.tmp.name).resolve()
self.app = self.installed / "Dikte.exe"
self.app.write_text("")
def _write(self, command):
self.value = command
def _delete(self):
there, self.value = bool(self.value), ""
return there
def install(self, force=False):
with Frozen(str(self.app), platform="win32"):
return integrate.install(force=force)
def remove(self):
with Frozen(str(self.app), platform="win32"):
return integrate.remove()
def test_the_windowed_executable_is_what_starts_at_sign_in(self):
"""The console one is what the `dikte` command runs, and a sign-in that
started that would open a console window nobody asked for."""
with Frozen(str(self.installed / "dikte-cli.exe"), platform="win32"):
self.assertEqual(integrate.target(), self.app)
def test_a_start_does_not_turn_it_on_for_somebody_who_said_no(self):
self.assertEqual(self.install(), [])
self.assertEqual(self.value, "")
def test_typing_it_turns_starting_at_sign_in_on(self):
self.assertEqual(len(self.install(force=True)), 1)
self.assertEqual(self.value, f'"{self.app}"')
def test_an_installation_that_moved_is_pointed_at_where_it_is_now(self):
self.value = '"D:\\Dikte\\Dikte.exe"'
self.assertEqual(len(self.install()), 1)
self.assertEqual(self.value, f'"{self.app}"')
def test_running_it_again_changes_nothing(self):
self.install(force=True)
self.assertEqual(self.install(), [])
def test_removing_stops_it_starting_and_says_so_once(self):
self.install(force=True)
self.assertEqual(len(self.remove()), 1)
self.assertEqual(self.value, "")
self.assertEqual(self.remove(), [])
class WindowsExecutableNames(unittest.TestCase):
"""The two Windows executables, read out of the files that name them.
Windows matches a filename without regard to its case, so Dikte.exe and
dikte.exe are one file in one directory and whichever was written second is
the only one installed. Nothing else here would catch that: these tests and
the builds that check the packaging both run on filesystems where the two
names are two files.
"""
root = pathlib.Path(__file__).resolve().parent.parent
def executables(self):
"""What the spec calls each one, in the order it builds them."""
spec = (self.root / "packaging" / "dikte.spec").read_text()
return [re.search(r'name="(.*?)"', block).group(1)
for block in spec.split("EXE(")[1:]]
def test_the_two_are_more_than_a_case_apart(self):
windowed, console = self.executables()
self.assertNotEqual(windowed.lower(), console.lower())
def test_the_command_runs_the_console_one(self):
"""The setup writes the shim, so it is the setup that has to be right."""
console = self.executables()[1]
setup = (self.root / "packaging" / "dikte.iss").read_text()
self.assertIn(f"{{app}}\\{console}.exe", setup)
if __name__ == "__main__":
unittest.main()
+10 -4
View File
@@ -7,6 +7,8 @@ answers by saying nothing at all.
import json
import os
import pathlib
import shlex
import sys
import unittest
from unittest import mock
@@ -57,13 +59,17 @@ class FakeSocket:
class Paths(unittest.TestCase):
def test_script_path_points_at_dikte(self):
self.assertTrue(ipc.script_path().endswith("dikte/__main__.py"))
# By its parts rather than as a string: the separator is a backslash on
# Windows, and the path is what a shortcut there runs too.
path = pathlib.Path(ipc.script_path())
self.assertEqual(path.parts[-2:], ("dikte", "__main__.py"))
self.assertTrue(os.path.exists(ipc.script_path()))
def test_the_shortcut_command_runs_it_with_this_interpreter(self):
command = ipc.command_for("toggle")
self.assertTrue(command.startswith(sys.executable))
self.assertTrue(command.endswith(" toggle"))
# Read back through the same quoting it went out with: a Windows path
# is spelled with backslashes and comes out of the join quoted.
self.assertEqual(shlex.split(ipc.command_for("toggle")),
[sys.executable, ipc.script_path(), "toggle"])
def test_a_packaged_build_names_itself_and_no_interpreter(self):
"""There is no __main__.py on disk in one, and sys.executable is the
+148
View File
@@ -13,6 +13,7 @@ is checked on a Mac and the macOS half on Linux, and a change to the chooser
cannot quietly break the platform nobody is sitting at.
"""
import ctypes
import os
import pathlib
import subprocess
@@ -56,6 +57,9 @@ class Chooser(DikteTest):
def test_a_mac(self):
self.assertIs(self.under("darwin"), paste.MACOS)
def test_windows(self):
self.assertIs(self.under("win32"), paste.WINDOWS)
def test_a_mac_running_an_x_server_is_still_a_mac(self):
"""XQuartz sets DISPLAY, and none of X's programs are what pastes here."""
self.assertIs(self.under("darwin", DISPLAY=":0"), paste.MACOS)
@@ -504,5 +508,149 @@ class MacClipboardSnapshot(DikteTest):
self.assertFalse(os.path.exists(directory))
class FakeWin32:
"""user32 and kernel32, as much of both as paste.py calls.
The clipboard is a string held here. A read materialises it as this
machine's own wide characters, which is what wstring_at reads wherever the
test runs; a write arrives as the UTF-16 the real clipboard is handed, so
what the code sent is exactly what is checked.
"""
def __init__(self):
self.text = None
self.buffers = {}
self.next_handle = 1
self.pressed = [] # (virtual key, flags), in the order sent
self.send_result = None # None: report every event as delivered
self.held = False # another program has the clipboard open
self.out_of_memory = False
def _keep(self, buffer):
handle = self.next_handle
self.next_handle += 1
self.buffers[handle] = buffer
return handle
# --- user32
def OpenClipboard(self, owner):
return 0 if self.held else 1
def CloseClipboard(self):
return 1
def EmptyClipboard(self):
self.text = None
return 1
def GetClipboardData(self, fmt):
if self.text is None:
return 0
return self._keep(ctypes.create_unicode_buffer(self.text))
def SetClipboardData(self, fmt, handle):
raw = self.buffers[handle].raw
self.text = raw.decode("utf-16-le").split("\x00", 1)[0]
return handle
def SendInput(self, count, inputs, size):
self.pressed.extend((entry.union.ki.wVk, entry.union.ki.dwFlags)
for entry in inputs)
return count if self.send_result is None else self.send_result
# --- kernel32
def GlobalAlloc(self, flags, size):
if self.out_of_memory:
return 0
return self._keep(ctypes.create_string_buffer(size))
def GlobalLock(self, handle):
buffer = self.buffers.get(handle)
return ctypes.addressof(buffer) if buffer else 0
def GlobalUnlock(self, handle):
return 1
def GlobalFree(self, handle):
self.buffers.pop(handle, None)
return 1
class Windows(Standing, DikteTest):
"""Windows shells out to nothing: both halves are calls into the system."""
platform = "win32"
here = paste.WINDOWS
def setUp(self):
super().setUp()
self.api = FakeWin32()
self.patch_attr(paste, "_win_api", lambda: (self.api, self.api))
self.patch_attr(paste.time, "sleep", lambda seconds: None)
def test_what_is_copied_is_what_reads_back(self):
paste.copy("ığüşöç İ")
self.assertEqual(paste.read_clipboard(), "ığüşöç İ".encode("utf-8"))
def test_an_empty_clipboard_reads_as_empty_text(self):
self.assertEqual(paste.read_clipboard(), b"")
def test_what_was_saved_goes_back_after_the_paste(self):
paste.copy("mine")
saved = paste.read_clipboard()
paste.copy("the dictation")
paste.copy_bytes(saved)
self.assertEqual(self.api.text, "mine")
def test_a_copy_that_fails_leaves_what_was_there(self):
"""EmptyClipboard is the point of no return, so nothing runs after it."""
paste.copy("mine")
for failure in ("out_of_memory", "held"):
with self.subTest(failure=failure):
setattr(self.api, failure, True)
with self.assertRaises(paste.PasteError):
paste.copy("the dictation")
self.assertEqual(self.api.text, "mine")
setattr(self.api, failure, False)
def test_the_handle_is_not_leaked_when_the_copy_fails(self):
self.api.held = True
with self.assertRaises(paste.PasteError):
paste.copy("the dictation")
self.assertEqual(self.api.buffers, {})
def test_readiness_asks_for_no_program_and_no_permission(self):
with only_these_tools():
self.assertTrue(paste.paste_ready())
def test_the_keys_go_down_in_order_and_up_in_reverse(self):
paste.press("ctrl+v")
keyup = 0x0002
self.assertEqual(self.api.pressed,
[(0x11, 0), (0x56, 0), (0x56, keyup), (0x11, keyup)])
def test_three_keys(self):
paste.press("ctrl+shift+v")
self.assertEqual([code for code, _ in self.api.pressed],
[0x11, 0x10, 0x56, 0x56, 0x10, 0x11])
def test_the_other_spellings_land_on_the_same_keys(self):
paste.press("super+enter")
first, self.api.pressed = self.api.pressed, []
paste.press("meta+return")
self.assertEqual(self.api.pressed, first)
def test_a_key_nobody_mapped_is_refused_before_anything_is_sent(self):
with self.assertRaises(paste.PasteError):
paste.press("ctrl+f13")
self.assertEqual(self.api.pressed, [])
def test_a_press_the_system_did_not_take_says_so(self):
self.api.send_result = 0
with self.assertRaises(paste.PasteError) as caught:
paste.press("ctrl+v")
self.assertIn("SendInput", str(caught.exception))
if __name__ == "__main__":
unittest.main()
+27 -6
View File
@@ -15,29 +15,50 @@ from dikte 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(str(config_dir), "/c/dikte")
self.assertEqual(str(data_dir), "/d/dikte")
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(str(config_dir).endswith("/.config/dikte"))
self.assertTrue(str(data_dir).endswith("/.local/share/dikte"))
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(str(config_dir).endswith("/Library/Application Support/Dikte"))
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", str(config_dir))
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):
+22
View File
@@ -7,6 +7,8 @@ test blends each icon onto a bar of its own and asks whether anything of it
survives, once over black and once over white.
"""
import pathlib
import struct
import sys
import tempfile
import unittest
@@ -150,6 +152,26 @@ class ApplicationIcon(DikteTest):
self.assertFalse(icon.isNull())
self.assertIn(48, [size.width() for size in icon.availableSizes()])
def test_the_windows_icon_is_one_file_holding_every_size(self):
"""Written by hand, so the header is what a test can be wrong about:
Windows reads the sizes out of the directory at the front rather than
out of the images, and a 256 is written there as a zero."""
with tempfile.TemporaryDirectory() as root:
path = trayicon.write_ico(pathlib.Path(root) / "Dikte.ico")
data = path.read_bytes()
reserved, kind, count = struct.unpack_from("<HHH", data)
self.assertEqual((reserved, kind), (0, 1))
self.assertEqual(count, len(trayicon.ICO_SIZES))
for index, size in enumerate(trayicon.ICO_SIZES):
with self.subTest(size=size):
(width, height, _, _, _, _,
length, offset) = struct.unpack_from("<BBBBHHII", data,
6 + 16 * index)
self.assertEqual((width, height), (size % 256, size % 256))
image = QImage.fromData(data[offset:offset + length])
self.assertEqual((image.width(), image.height()),
(size, size))
if __name__ == "__main__":
unittest.main()
+70 -3
View File
@@ -16,12 +16,16 @@ from PyQt6.QtCore import QPoint, QPointF, Qt
from PyQt6.QtGui import QWheelEvent
from PyQt6.QtWidgets import QApplication, QMessageBox
from dikte import audio
from dikte import cleanup
from dikte import config as cfg
from dikte import ggml
from dikte import hotkey
from dikte import ipc
from dikte import overlay as overlay_module
from dikte import paste
from dikte import settings_ui
from dikte import update
from tests.support import DikteTest, only_these_tools
# One application for the whole run; Qt allows no second one.
@@ -100,6 +104,7 @@ CHANGED = {
"pause_shortcut": "Meta+P",
"evdev_hotkey": True,
"history_limit": 50,
"update_check": False,
}
@@ -249,6 +254,31 @@ class Settings(DikteTest):
self.assertEqual(shown, [provider])
self.assertFalse(box.isHidden())
def test_the_update_line_names_the_version_that_is_running(self):
window = self.window(cfg.Config())
self.assertIn(settings_ui.__version__, window.update_status.text())
# Nothing to open until a check has found something to open.
self.assertTrue(window.update_page.isHidden())
def test_a_newer_release_puts_the_page_button_on_screen(self):
window = self.window(cfg.Config())
told = []
window.update_found.connect(told.append)
release = update.Release("9.9.9", "https://example.invalid/9.9.9", "")
window._on_update_checked(release, "")
self.assertIn("9.9.9", window.update_status.text())
self.assertFalse(window.update_page.isHidden())
self.assertEqual(window._release_url, release.url)
# And the tray hears about it from here rather than waiting a day.
self.assertEqual(told, [release])
def test_a_check_that_failed_says_why_and_hands_the_button_back(self):
window = self.window(cfg.Config())
window.update_now.setEnabled(False)
window._on_update_checked(None, "api.github.com answered HTTP 403.")
self.assertIn("403", window.update_status.text())
self.assertTrue(window.update_now.isEnabled())
def test_the_settings_the_window_does_not_show_are_left_alone(self):
"""A tab nobody wrote must not reset what the command line set."""
self.write_config({"silence_db": -42.0, "speech_margin_db": 15.0,
@@ -281,7 +311,7 @@ class Settings(DikteTest):
text = self.shortcut_tab_text(window)
self.assertIn("i3 keeps no shortcut registry", text)
self.assertNotIn("KWin", text)
self.assertIn("__main__.py toggle", text)
self.assertIn(ipc.command_for("toggle"), text)
# Not a choice to offer where it is the only mechanism there is.
self.assertTrue(window.evdev_enabled.isHidden())
self.assertFalse([button for button in
@@ -458,7 +488,7 @@ class MacSettings(Settings):
text = self.shortcut_tab_text(window)
self.assertIn("Dikte asks macOS for these combinations", text)
self.assertNotIn("KWin", text)
self.assertNotIn("__main__.py toggle", text)
self.assertNotIn(ipc.command_for("toggle"), text)
def test_the_paste_keys_on_offer_are_the_ones_a_mac_uses(self):
window = self.window(cfg.Config())
@@ -482,7 +512,7 @@ class KdeSettings(Settings):
self.assertIn("KWin only reads shortcut settings at startup", text)
self.assertIn("Install as a KDE shortcut", text)
self.assertNotIn("keeps no shortcut registry", text)
self.assertNotIn("__main__.py toggle", text)
self.assertNotIn(ipc.command_for("toggle"), text)
# Here it is a choice: the wait for the next login, or the key press
# reaching the focused application as well.
self.assertFalse(window.evdev_enabled.isHidden())
@@ -925,6 +955,36 @@ class Overlay(DikteTest):
self.assertFalse(widget.muted)
class MeetingSources(DikteTest):
"""What the Meeting tab says about the far side, per sound system.
The box that picks it is empty on a system that cannot record it, and an
empty box with nothing next to it reads as a list that has not loaded yet.
"""
def notes(self, meetings):
with mock.patch.object(audio, "sound",
return_value=audio.PULSE._replace(
meetings=meetings)), \
only_these_tools(), \
mock.patch.object(settings_ui.SettingsWindow, "_load_models"), \
mock.patch.object(settings_ui.SettingsWindow,
"_load_transcribe_models"):
window = settings_ui.SettingsWindow(cfg.Config())
self.addCleanup(window.deleteLater)
self.addCleanup(window.close)
return " ".join(label.text()
for label in window.findChildren(settings_ui.QLabel))
def test_a_system_that_cannot_record_the_far_side_says_so(self):
self.assertIn("nothing that records what the speakers",
self.notes(meetings=False))
def test_a_system_that_can_says_nothing_of_the_sort(self):
self.assertNotIn("nothing that records what the speakers",
self.notes(meetings=True))
if __name__ == "__main__":
unittest.main()
@@ -932,6 +992,13 @@ if __name__ == "__main__":
class LocalModels(DikteTest):
"""The download boxes, without a network and without either program."""
def setUp(self):
super().setUp()
# A machine Dikte is actually installed on would otherwise answer the
# "nothing can transcribe" question from its real binary and model.
self.patch_attr(ggml, "BIN_DIR", self.path("bin"))
self.patch_attr(ggml, "MODELS_DIR", self.path("models"))
def window(self, conf):
window = settings_ui.SettingsWindow(conf)
self.addCleanup(window.deleteLater)
+175
View File
@@ -0,0 +1,175 @@
"""Whether a newer release is one worth telling somebody about.
Two things carry the weight here. A version is compared by its numbers alone,
because a build off master carries the released number with its commit after
it and is ahead of that release rather than behind it. And the clock lives in a
file, so a day of asking nobody has to survive a restart.
"""
import json
import time
from dikte import hub
from dikte import update
from tests.support import DikteTest, fake_urlopen, url_error
RELEASE = {
"tag_name": "v1.4.0",
"html_url": "https://github.com/yusufipk/dikte/releases/tag/v1.4.0",
"published_at": "2026-08-01T10:00:00Z",
}
class Numbers(DikteTest):
def test_a_tag_and_a_bare_number_read_the_same(self):
self.assertEqual(update._numbers("v1.4.0"), (1, 4, 0))
self.assertEqual(update._numbers("1.4.0"), (1, 4, 0))
def test_a_short_number_is_filled_out(self):
self.assertEqual(update._numbers("2"), (2, 0, 0))
self.assertEqual(update._numbers("2.1"), (2, 1, 0))
def test_what_follows_the_number_is_dropped(self):
self.assertEqual(update._numbers("1.0.1-dev.abc1234"), (1, 0, 1))
self.assertEqual(update._numbers("1.0.1+build7"), (1, 0, 1))
def test_something_that_is_not_a_version_is_no_version(self):
self.assertEqual(update._numbers("latest"), ())
self.assertEqual(update._numbers(""), ())
self.assertEqual(update._numbers(None), ())
class Newer(DikteTest):
def test_a_higher_number_is_newer(self):
self.assertTrue(update.newer("1.4.0", "1.3.9"))
self.assertTrue(update.newer("v2.0.0", "1.9.9"))
def test_the_same_number_is_not(self):
self.assertFalse(update.newer("1.4.0", "1.4.0"))
self.assertFalse(update.newer("1.3.0", "1.4.0"))
def test_a_build_off_master_is_ahead_of_the_release_it_names(self):
"""1.0.1-dev.abc1234 was built after 1.0.1 went out, not before it.
Read as a version suffix it would be older, and every nightly would be
told to go back to the release it had already passed."""
self.assertFalse(update.newer("1.0.1", "1.0.1-dev.abc1234"))
self.assertTrue(update.newer("1.0.2", "1.0.1-dev.abc1234"))
def test_a_tag_that_is_not_a_version_is_never_newer(self):
self.assertFalse(update.newer("nightly", "1.0.0"))
class Asking(DikteTest):
def setUp(self):
super().setUp()
self.patch_attr(hub, "CACHE_DIR", self.path("cache"))
self.patch_attr(update, "__version__", "1.0.0")
def test_the_newest_release_comes_back_with_its_page(self):
with fake_urlopen(RELEASE) as calls:
release = update.latest()
self.assertEqual(release.version, "1.4.0")
self.assertEqual(release.url, RELEASE["html_url"])
self.assertEqual(
calls[0].full_url,
"https://api.github.com/repos/yusufipk/dikte/releases/latest")
def test_a_release_with_no_page_falls_back_to_the_redirect(self):
with fake_urlopen({"tag_name": "v1.4.0"}):
release = update.latest()
self.assertEqual(release.url, update.RELEASES_PAGE)
def test_a_repository_with_no_release_is_an_error(self):
with fake_urlopen({"message": "Not Found"}):
with self.assertRaises(hub.HubError):
update.latest()
def test_a_check_answers_with_the_newer_release(self):
with fake_urlopen(RELEASE):
release = update.check()
self.assertEqual(release.version, "1.4.0")
def test_a_check_that_finds_nothing_new_answers_with_nothing(self):
self.patch_attr(update, "__version__", "1.4.0")
with fake_urlopen(RELEASE):
self.assertIsNone(update.check())
def test_a_second_check_the_same_day_asks_nobody(self):
with fake_urlopen(RELEASE) as calls:
update.check()
release = update.check()
self.assertEqual(len(calls), 1)
# And still says what the first one found, since it is still true.
self.assertEqual(release.version, "1.4.0")
def test_a_day_later_it_asks_again(self):
with fake_urlopen(RELEASE) as calls:
update.check()
update._store(checked=time.time() - update.INTERVAL - 60)
update.check()
self.assertEqual(len(calls), 2)
def test_the_button_asks_whatever_the_clock_says(self):
with fake_urlopen(RELEASE) as calls:
update.check()
update.check(force=True)
self.assertEqual(len(calls), 2)
def test_a_check_that_cannot_reach_github_says_so(self):
with fake_urlopen(url_error("no route to host")):
with self.assertRaises(hub.HubError):
update.check()
class Remembering(DikteTest):
def setUp(self):
super().setUp()
self.patch_attr(hub, "CACHE_DIR", self.path("cache"))
self.patch_attr(update, "__version__", "1.0.0")
def test_what_the_last_check_found_survives_a_restart(self):
with fake_urlopen(RELEASE):
update.check()
release = update.pending()
self.assertEqual(release.version, "1.4.0")
self.assertEqual(release.url, RELEASE["html_url"])
def test_nothing_was_ever_checked(self):
self.assertIsNone(update.pending())
self.assertEqual(update.state(), {})
self.assertTrue(update.due())
def test_a_release_that_is_no_longer_newer_is_not_pending(self):
"""The state file outlives the build that wrote it: an update that was
found and then installed must not still be waiting afterwards."""
update._store(version="1.4.0")
self.patch_attr(update, "__version__", "1.4.0")
self.assertIsNone(update.pending())
def test_a_version_is_announced_once(self):
self.assertEqual(update.announced(), "")
update.mark_announced("1.4.0")
self.assertEqual(update.announced(), "1.4.0")
def test_a_state_file_that_is_rubbish_is_no_state_at_all(self):
update.STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
update.STATE_FILE.write_text("half a {", encoding="utf-8")
self.assertEqual(update.state(), {})
self.assertIsNone(update.pending())
def test_a_state_file_that_cannot_be_written_is_not_a_failure(self):
self.patch_attr(update, "STATE_FILE",
self.path("nope") / "deeper" / "update.json")
self.path("nope").write_text("a file where a directory would go")
with fake_urlopen(RELEASE):
release = update.check()
self.assertEqual(release.version, "1.4.0")
def test_the_clock_is_kept_out_of_the_settings(self):
"""A background check writes while the settings window may be open, and
a write into config.json there would undo whatever it holds."""
with fake_urlopen(RELEASE):
update.check()
stored = json.loads(update.STATE_FILE.read_text(encoding="utf-8"))
self.assertEqual(stored["version"], "1.4.0")
self.assertGreater(stored["checked"], 0)