mirror of
https://github.com/yusufipk/dikte.git
synced 2026-09-11 10:56:10 +00:00
Test what the application does, and run it on every pull request
570 tests over the standard library's unittest, so there is nothing to install beyond the PyQt6 the application already needs. They reach neither the network, the microphone, nor the real ~/.config/dikte: urllib is faked at one function, the tools are faked at shutil.which, and every test is handed its own config and data directory. What they hold onto is what a change is most likely to move without meaning to. The request each provider is sent, field by field. A settings window that loads a value into a widget and writes it back, which is where a setting added to one half and not the other is silently reset. The dictation chain end to end: what is transcribed, what is pasted, what lands in the history, and what happens to the audio afterwards. A config file written by an older version. A meeting whose two channels heard the same sentence. 59 of them carry @linux_only, because they cover what Dikte is on this desktop rather than what it does: PipeWire, wl-clipboard, ydotool, KDE's shortcut file. The other 511 pass on any platform, and that line is worth holding as the ports arrive. CONTRIBUTING.md says how to run them, what support.py offers, and the three things about this codebase that trip up a new test.
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
"""The clipboard and the key press, which is where a dictation actually lands.
|
||||
|
||||
Everything here shells out, so the tools are faked. What the tests hold onto is
|
||||
the command line: a paste that presses the wrong codes, or in the wrong order,
|
||||
types nothing and looks like a hang.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
import paste
|
||||
from tests.support import DikteTest, FakeCompleted, linux_only, only_these_tools
|
||||
|
||||
|
||||
@linux_only
|
||||
class ReadClipboard(DikteTest):
|
||||
def test_no_wl_paste_installed(self):
|
||||
with only_these_tools():
|
||||
self.assertIsNone(paste.read_clipboard())
|
||||
|
||||
def test_what_is_on_the_clipboard_comes_back_as_bytes(self):
|
||||
with only_these_tools("wl-paste"), \
|
||||
mock.patch.object(subprocess, "run",
|
||||
return_value=FakeCompleted(stdout=b"hello")) as run:
|
||||
self.assertEqual(paste.read_clipboard(), b"hello")
|
||||
self.assertEqual(run.call_args.args[0], ["wl-paste", "--no-newline"])
|
||||
|
||||
def test_an_empty_clipboard_is_not_an_error(self):
|
||||
with only_these_tools("wl-paste"), \
|
||||
mock.patch.object(subprocess, "run",
|
||||
return_value=FakeCompleted(returncode=1)):
|
||||
self.assertIsNone(paste.read_clipboard())
|
||||
|
||||
def test_a_tool_that_will_not_run(self):
|
||||
with only_these_tools("wl-paste"), \
|
||||
mock.patch.object(subprocess, "run", side_effect=OSError("nope")):
|
||||
self.assertIsNone(paste.read_clipboard())
|
||||
|
||||
|
||||
@linux_only
|
||||
class Copy(DikteTest):
|
||||
def test_no_wl_copy_installed(self):
|
||||
with only_these_tools(), self.assertRaises(paste.PasteError) as caught:
|
||||
paste.copy("hello")
|
||||
self.assertIn("wl-clipboard", str(caught.exception))
|
||||
|
||||
def test_the_text_goes_in_as_utf8(self):
|
||||
with only_these_tools("wl-copy"), \
|
||||
mock.patch.object(subprocess, "run",
|
||||
return_value=FakeCompleted()) as run:
|
||||
paste.copy("günaydın")
|
||||
self.assertEqual(run.call_args.kwargs["input"], "günaydın".encode())
|
||||
|
||||
def test_the_pipes_are_closed_so_the_call_can_return(self):
|
||||
"""wl-copy forks and holds the selection; a pipe nobody drains hangs."""
|
||||
with only_these_tools("wl-copy"), \
|
||||
mock.patch.object(subprocess, "run",
|
||||
return_value=FakeCompleted()) as run:
|
||||
paste.copy("hello")
|
||||
self.assertEqual(run.call_args.kwargs["stdout"], subprocess.DEVNULL)
|
||||
self.assertEqual(run.call_args.kwargs["stderr"], subprocess.DEVNULL)
|
||||
|
||||
def test_a_non_zero_exit_is_reported(self):
|
||||
with only_these_tools("wl-copy"), \
|
||||
mock.patch.object(subprocess, "run",
|
||||
return_value=FakeCompleted(returncode=1)), \
|
||||
self.assertRaises(paste.PasteError):
|
||||
paste.copy("hello")
|
||||
|
||||
def test_a_tool_that_will_not_run(self):
|
||||
with only_these_tools("wl-copy"), \
|
||||
mock.patch.object(subprocess, "run", side_effect=OSError("nope")), \
|
||||
self.assertRaises(paste.PasteError):
|
||||
paste.copy("hello")
|
||||
|
||||
|
||||
@linux_only
|
||||
class CopyBytes(DikteTest):
|
||||
def test_nothing_to_restore(self):
|
||||
with only_these_tools("wl-copy"), \
|
||||
mock.patch.object(subprocess, "run") as run:
|
||||
paste.copy_bytes(None)
|
||||
run.assert_not_called()
|
||||
|
||||
def test_restoring_never_raises(self):
|
||||
"""It runs after the paste went in; failing here must not undo that."""
|
||||
with only_these_tools("wl-copy"), \
|
||||
mock.patch.object(subprocess, "run", side_effect=OSError("nope")):
|
||||
paste.copy_bytes(b"whatever was there before")
|
||||
|
||||
def test_the_bytes_go_back_untouched(self):
|
||||
with only_these_tools("wl-copy"), \
|
||||
mock.patch.object(subprocess, "run",
|
||||
return_value=FakeCompleted()) as run:
|
||||
paste.copy_bytes(b"\x89PNG\r\n")
|
||||
self.assertEqual(run.call_args.kwargs["input"], b"\x89PNG\r\n")
|
||||
|
||||
|
||||
@linux_only
|
||||
class Press(DikteTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
# The settle delay is real time nobody needs to spend in a test.
|
||||
self.patch_attr(paste.time, "sleep", lambda seconds: None)
|
||||
|
||||
def run_press(self, shortcut, result=None):
|
||||
with only_these_tools("ydotool"), \
|
||||
mock.patch.object(subprocess, "run",
|
||||
return_value=result or FakeCompleted()) as run:
|
||||
paste.press(shortcut)
|
||||
return run.call_args.args[0]
|
||||
|
||||
def test_no_ydotool_installed(self):
|
||||
with only_these_tools():
|
||||
self.assertFalse(paste.ydotool_ready())
|
||||
with self.assertRaises(paste.PasteError):
|
||||
paste.press()
|
||||
|
||||
def test_ctrl_v_presses_down_then_lets_go_in_reverse(self):
|
||||
self.assertEqual(self.run_press("ctrl+v"),
|
||||
["ydotool", "key", "29:1", "47:1", "47:0", "29:0"])
|
||||
|
||||
def test_three_keys(self):
|
||||
self.assertEqual(self.run_press("ctrl+shift+v"),
|
||||
["ydotool", "key", "29:1", "42:1", "47:1",
|
||||
"47:0", "42:0", "29:0"])
|
||||
|
||||
def test_case_and_spacing_do_not_matter(self):
|
||||
self.assertEqual(self.run_press(" Ctrl + V "), self.run_press("ctrl+v"))
|
||||
|
||||
def test_the_synonyms_land_on_the_same_codes(self):
|
||||
self.assertEqual(self.run_press("control+insert"),
|
||||
["ydotool", "key", "29:1", "110:1", "110:0", "29:0"])
|
||||
self.assertEqual(self.run_press("super+enter"), self.run_press("meta+return"))
|
||||
|
||||
def test_a_key_nobody_mapped(self):
|
||||
with only_these_tools("ydotool"), mock.patch.object(subprocess, "run"), \
|
||||
self.assertRaises(paste.PasteError) as caught:
|
||||
paste.press("ctrl+f13")
|
||||
self.assertIn("f13", str(caught.exception))
|
||||
|
||||
def test_ydotoold_not_running_says_so(self):
|
||||
with self.assertRaises(paste.PasteError) as caught:
|
||||
self.run_press("ctrl+v", FakeCompleted(returncode=1, stderr="no socket"))
|
||||
self.assertIn("ydotoold", str(caught.exception))
|
||||
|
||||
def test_a_tool_that_will_not_run(self):
|
||||
with only_these_tools("ydotool"), \
|
||||
mock.patch.object(subprocess, "run", side_effect=OSError("nope")), \
|
||||
self.assertRaises(paste.PasteError):
|
||||
paste.press("ctrl+v")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user