mirror of
https://github.com/yusufipk/dikte.git
synced 2026-09-11 10:56:10 +00:00
Recording goes through ffmpeg's avfoundation input, and opening a capture
session there brings the process that did it to the front. ffmpeg is a
child of Dikte with no bundle of its own, so macOS credits the move to
Dikte: the window the user was dictating into goes inactive, its caret
stops, its title bar greys out, and the Cmd+V at the end of the run lands
somewhere other than the document it was meant for. Measured with a
TextEdit document in front:
press the shortcut front = TextEdit
recorder.start returns front = TextEdit
89 ms later front = Dikte
Nothing about the capture session can be asked not to do this. Starting
ffmpeg in its own session, and clearing __CFBundleIdentifier from its
environment, were both tried and both measured to make no difference, so
it is undone instead: the application in front is noted before the
indicator goes up, and a short watch puts it back the moment Dikte takes
the front. Measured at 99 ms from the moment it is taken, against the
title bar staying grey for the whole dictation before. All three ways in
do it, dictation, agent and meeting, since all three open the same
capture.
The indicator had a share of the same problem and needed AppKit for it
too, so mac_window.py carries both. An NSPanel is hidden by the system
the moment its application stops being the active one, which for a
dictation indicator is immediately, and ordering one to the front brings
its application with it unless it carries the nonactivating style bit.
Neither is reachable through Qt.
The runtime is loaded in _appkit() rather than at import, the way paste.py
loads its frameworks in _macos_api(), so the module imports on a machine
with no AppKit and the tests below run there as well: 52 new tests, none
of them skipped anywhere.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
509 lines
21 KiB
Python
509 lines
21 KiB
Python
"""The clipboard and the key press, which is where a dictation actually lands.
|
||
|
||
Everything here is faked: the programs the two Linux desktops shell out to, and
|
||
the frameworks macOS goes through. What the tests hold onto is what was asked
|
||
for. A paste that presses the wrong keys, or in the wrong order, types nothing
|
||
and looks like a hang.
|
||
|
||
Every system owes the same promises about the clipboard, so those are written
|
||
once and run against each of them. A fourth one added to paste.py inherits the
|
||
same list rather than needing its own copy of it. Each class says which system
|
||
it is standing on, which is why none of this is skipped anywhere: the Linux half
|
||
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 os
|
||
import pathlib
|
||
import subprocess
|
||
import sys
|
||
import tempfile
|
||
import unittest
|
||
from typing import ClassVar
|
||
from unittest import mock
|
||
|
||
from dikte import paste
|
||
from tests.support import DikteTest, FakeCompleted, only_these_tools
|
||
|
||
|
||
class Chooser(DikteTest):
|
||
"""Which pair of programs this session's clipboard goes through."""
|
||
|
||
def under(self, platform="linux", **env):
|
||
with mock.patch.object(sys, "platform", platform), \
|
||
mock.patch.dict(os.environ, env, clear=True):
|
||
return paste.desktop()
|
||
|
||
def test_a_wayland_session(self):
|
||
self.assertIs(self.under(XDG_SESSION_TYPE="wayland",
|
||
WAYLAND_DISPLAY="wayland-0"), paste.WAYLAND)
|
||
|
||
def test_an_x11_session(self):
|
||
self.assertIs(self.under(XDG_SESSION_TYPE="x11", DISPLAY=":0"), paste.X11)
|
||
|
||
def test_a_display_with_no_wayland_beside_it(self):
|
||
self.assertIs(self.under(DISPLAY=":0"), paste.X11)
|
||
|
||
def test_an_x11_display_under_wayland_is_still_wayland(self):
|
||
"""XWayland sets DISPLAY too; the session type is the one to believe."""
|
||
self.assertIs(self.under(XDG_SESSION_TYPE="wayland",
|
||
DISPLAY=":0", WAYLAND_DISPLAY="wayland-0"),
|
||
paste.WAYLAND)
|
||
|
||
def test_nothing_set_at_all(self):
|
||
self.assertIs(self.under(), paste.WAYLAND)
|
||
|
||
def test_a_mac(self):
|
||
self.assertIs(self.under("darwin"), paste.MACOS)
|
||
|
||
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)
|
||
|
||
|
||
class Standing:
|
||
"""A test that runs as if it were sitting at one particular system."""
|
||
|
||
env: ClassVar[dict] = {}
|
||
platform = "linux"
|
||
here = None
|
||
|
||
def setUp(self):
|
||
super().setUp()
|
||
self.enterContext(mock.patch.object(sys, "platform", self.platform))
|
||
self.enterContext(mock.patch.dict(os.environ, self.env, clear=True))
|
||
|
||
|
||
class ClipboardContract(Standing):
|
||
"""What every system owes the text on its way to the clipboard."""
|
||
|
||
def test_no_reader_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(self.here.read_command[0]), \
|
||
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], self.here.read_command)
|
||
|
||
def test_an_empty_clipboard_is_not_an_error(self):
|
||
with only_these_tools(self.here.read_command[0]), \
|
||
mock.patch.object(subprocess, "run",
|
||
return_value=FakeCompleted(returncode=1)):
|
||
self.assertIsNone(paste.read_clipboard())
|
||
|
||
def test_a_reader_that_will_not_run(self):
|
||
with only_these_tools(self.here.read_command[0]), \
|
||
mock.patch.object(subprocess, "run", side_effect=OSError("nope")):
|
||
self.assertIsNone(paste.read_clipboard())
|
||
|
||
def test_no_clipboard_tool_installed_names_it(self):
|
||
with only_these_tools(), self.assertRaises(paste.PasteError) as caught:
|
||
paste.copy("hello")
|
||
self.assertIn(self.here.clipboard, str(caught.exception))
|
||
|
||
def test_the_text_goes_in_as_utf8(self):
|
||
with only_these_tools(self.here.clipboard), \
|
||
mock.patch.object(subprocess, "run",
|
||
return_value=FakeCompleted()) as run:
|
||
paste.copy("günaydın")
|
||
self.assertEqual(run.call_args.args[0], self.here.copy_command)
|
||
self.assertEqual(run.call_args.kwargs["input"], "günaydın".encode())
|
||
|
||
def test_the_pipes_are_closed_so_the_call_can_return(self):
|
||
"""The clipboard owner forks; a pipe nobody drains hangs the caller."""
|
||
with only_these_tools(self.here.clipboard), \
|
||
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(self.here.clipboard), \
|
||
mock.patch.object(subprocess, "run",
|
||
return_value=FakeCompleted(returncode=1)), \
|
||
self.assertRaises(paste.PasteError):
|
||
paste.copy("hello")
|
||
|
||
def test_a_clipboard_tool_that_will_not_run(self):
|
||
with only_these_tools(self.here.clipboard), \
|
||
mock.patch.object(subprocess, "run", side_effect=OSError("nope")), \
|
||
self.assertRaises(paste.PasteError):
|
||
paste.copy("hello")
|
||
|
||
def test_there_is_nothing_to_restore(self):
|
||
with only_these_tools(self.here.clipboard), \
|
||
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(self.here.clipboard), \
|
||
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(self.here.clipboard), \
|
||
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")
|
||
|
||
def test_the_paste_key_it_offers_is_one_it_can_press(self):
|
||
"""Whatever Settings lists, pressing it must not come back unknown."""
|
||
for shortcut in self.here.shortcuts:
|
||
with self.subTest(shortcut=shortcut):
|
||
self.assertTrue(self.pressing(shortcut))
|
||
|
||
|
||
class KeyProgramContract(Standing):
|
||
"""The half of it that is another program: ydotool, xdotool."""
|
||
|
||
def press(self, shortcut, result=None):
|
||
with only_these_tools(self.here.keyboard), \
|
||
mock.patch.object(paste.time, "sleep", lambda seconds: None), \
|
||
mock.patch.object(subprocess, "run",
|
||
return_value=result or FakeCompleted()) as run:
|
||
paste.press(shortcut)
|
||
return run.call_args.args[0]
|
||
|
||
def pressing(self, shortcut):
|
||
"""The command a shortcut would run, for the contract above."""
|
||
return self.press(shortcut)
|
||
|
||
def test_no_keyboard_tool_installed(self):
|
||
with only_these_tools():
|
||
self.assertFalse(paste.paste_ready())
|
||
with self.assertRaises(paste.PasteError) as caught:
|
||
paste.press()
|
||
self.assertIn(self.here.keyboard, str(caught.exception))
|
||
|
||
def test_case_and_spacing_do_not_matter(self):
|
||
self.assertEqual(self.press(" Ctrl + V "), self.press("ctrl+v"))
|
||
|
||
def test_a_key_nobody_mapped_is_refused_before_the_tool_runs(self):
|
||
"""Whichever desktop it is, the shortcut is held to one table."""
|
||
with only_these_tools(self.here.keyboard), \
|
||
mock.patch.object(subprocess, "run") as run, \
|
||
self.assertRaises(paste.PasteError) as caught:
|
||
paste.press("ctrl+f13")
|
||
self.assertIn("f13", str(caught.exception))
|
||
run.assert_not_called()
|
||
|
||
def test_a_tool_that_will_not_run(self):
|
||
with only_these_tools(self.here.keyboard), \
|
||
mock.patch.object(paste.time, "sleep", lambda seconds: None), \
|
||
mock.patch.object(subprocess, "run", side_effect=OSError("nope")), \
|
||
self.assertRaises(paste.PasteError):
|
||
paste.press("ctrl+v")
|
||
|
||
def test_a_failed_key_press_names_the_tool_and_what_it_said(self):
|
||
with self.assertRaises(paste.PasteError) as caught:
|
||
self.press("ctrl+v", FakeCompleted(returncode=1, stderr="no socket"))
|
||
self.assertIn(self.here.keyboard, str(caught.exception))
|
||
self.assertIn("no socket", str(caught.exception))
|
||
|
||
def test_nothing_named_presses_the_one_this_desktop_pastes_with(self):
|
||
self.assertEqual(self.press(""), self.press(self.here.shortcuts[0]))
|
||
|
||
|
||
class Wayland(ClipboardContract, KeyProgramContract, DikteTest):
|
||
env: ClassVar[dict] = {"XDG_SESSION_TYPE": "wayland",
|
||
"WAYLAND_DISPLAY": "wayland-0"}
|
||
here = paste.WAYLAND
|
||
|
||
def test_ydotool_presses_down_then_lets_go_in_reverse(self):
|
||
self.assertEqual(self.press("ctrl+v"),
|
||
["ydotool", "key", "29:1", "47:1", "47:0", "29:0"])
|
||
|
||
def test_three_keys(self):
|
||
self.assertEqual(self.press("ctrl+shift+v"),
|
||
["ydotool", "key", "29:1", "42:1", "47:1",
|
||
"47:0", "42:0", "29:0"])
|
||
|
||
def test_the_synonyms_land_on_the_same_codes(self):
|
||
self.assertEqual(self.press("control+insert"),
|
||
["ydotool", "key", "29:1", "110:1", "110:0", "29:0"])
|
||
self.assertEqual(self.press("super+enter"), self.press("meta+return"))
|
||
|
||
def test_a_failure_asks_after_the_daemon(self):
|
||
"""ydotool needs ydotoold, and says nothing useful when it is not up."""
|
||
with self.assertRaises(paste.PasteError) as caught:
|
||
self.press("ctrl+v", FakeCompleted(returncode=1, stderr="no socket"))
|
||
self.assertIn("ydotoold", str(caught.exception))
|
||
|
||
|
||
class X11(ClipboardContract, KeyProgramContract, DikteTest):
|
||
env: ClassVar[dict] = {"XDG_SESSION_TYPE": "x11", "DISPLAY": ":0"}
|
||
here = paste.X11
|
||
|
||
def test_xdotool_takes_the_combination_as_one_argument(self):
|
||
self.assertEqual(self.press("ctrl+v"),
|
||
["xdotool", "key", "--clearmodifiers", "ctrl+v"])
|
||
|
||
def test_three_keys(self):
|
||
self.assertEqual(self.press("ctrl+shift+v"),
|
||
["xdotool", "key", "--clearmodifiers", "ctrl+shift+v"])
|
||
|
||
def test_the_keys_x_spells_differently(self):
|
||
"""xdotool wants keysyms, not the names the code table is keyed by."""
|
||
self.assertEqual(self.press("control+insert")[-1], "ctrl+Insert")
|
||
self.assertEqual(self.press("meta+enter")[-1], "super+Return")
|
||
|
||
def test_no_daemon_to_ask_after(self):
|
||
with self.assertRaises(paste.PasteError) as caught:
|
||
self.press("ctrl+v", FakeCompleted(returncode=1, stderr="bad keysym"))
|
||
self.assertNotIn("ydotoold", str(caught.exception))
|
||
|
||
|
||
class FakeCoreGraphics:
|
||
"""Enough of the two frameworks to watch what a paste does to them.
|
||
|
||
The events are numbers standing in for pointers, which is all the code
|
||
treats them as: it makes them, sets flags on them, posts them, and hands
|
||
them back.
|
||
"""
|
||
|
||
def __init__(self, trusted=True, makes=None):
|
||
self.trusted = trusted
|
||
self.makes = makes # how many it will hand out; None for as many as asked
|
||
self.made = [] # (keycode, is_down)
|
||
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:
|
||
return None
|
||
return 1000 + len(self.made)
|
||
|
||
def CGEventSetFlags(self, event, flags):
|
||
self.flags.append((event, flags))
|
||
|
||
def CGEventPost(self, tap, event):
|
||
self.posted.append((tap, event))
|
||
|
||
# --- CoreFoundation
|
||
def CFRelease(self, event):
|
||
self.released.append(event)
|
||
|
||
|
||
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)
|
||
self.opened = self.patch_attr(paste.subprocess, "Popen", mock.Mock())
|
||
|
||
def pressing(self, shortcut):
|
||
paste.press(shortcut)
|
||
return self.api.posted
|
||
|
||
def test_the_key_goes_in_by_position_with_the_modifiers_on_it(self):
|
||
paste.press("cmd+v")
|
||
self.assertEqual(self.api.made, [(9, True), (9, False)])
|
||
self.assertEqual([flags for _, flags in self.api.flags],
|
||
[paste.MAC_FLAGS["command"]] * 2)
|
||
self.assertEqual([tap for tap, _ in self.api.posted],
|
||
[paste.HID_EVENT_TAP] * 2)
|
||
|
||
def test_the_down_is_posted_before_the_up(self):
|
||
paste.press("cmd+v")
|
||
self.assertEqual([event for _, event in self.api.posted], [1001, 1002])
|
||
|
||
def test_both_events_are_handed_back(self):
|
||
"""CoreGraphics gives out memory that nothing else will free."""
|
||
paste.press("cmd+v")
|
||
self.assertEqual(sorted(self.api.released), [1001, 1002])
|
||
|
||
def test_several_modifiers_are_one_number(self):
|
||
paste.press("cmd+shift+v")
|
||
self.assertEqual(self.api.flags[0][1],
|
||
paste.MAC_FLAGS["command"] | paste.MAC_FLAGS["shift"])
|
||
|
||
def test_the_names_a_mac_keyboard_uses(self):
|
||
for name in ("cmd", "command", "meta", "super"):
|
||
with self.subTest(name=name):
|
||
self.assertEqual(paste._macos_keys(f"{name}+v"),
|
||
(9, paste.MAC_FLAGS["command"]))
|
||
self.assertEqual(paste._macos_keys("option+v"), paste._macos_keys("alt+v"))
|
||
self.assertEqual(paste._macos_keys("control+v"), paste._macos_keys("ctrl+v"))
|
||
|
||
def test_case_and_spacing_do_not_matter(self):
|
||
self.assertEqual(paste._macos_keys(" Cmd + V "), paste._macos_keys("cmd+v"))
|
||
|
||
def test_a_key_nobody_mapped_is_refused_before_anything_is_posted(self):
|
||
with self.assertRaises(paste.PasteError) as caught:
|
||
paste.press("cmd+f13")
|
||
self.assertIn("f13", str(caught.exception))
|
||
self.assertEqual(self.api.posted, [])
|
||
|
||
def test_a_modifier_nobody_mapped(self):
|
||
with self.assertRaises(paste.PasteError) as caught:
|
||
paste.press("hyper+v")
|
||
self.assertIn("hyper", str(caught.exception))
|
||
|
||
def test_nothing_at_all(self):
|
||
with self.assertRaises(paste.PasteError):
|
||
paste.press("+")
|
||
|
||
def test_nothing_is_typed_until_macos_says_so(self):
|
||
self.api.trusted = False
|
||
with self.assertRaises(paste.PasteError) as caught:
|
||
paste.press("cmd+v")
|
||
self.assertIn("Accessibility", str(caught.exception))
|
||
self.assertEqual(self.api.posted, [])
|
||
|
||
def test_the_permission_pane_is_opened_once_and_not_again(self):
|
||
"""It is a window in the user's face, and one paste is every dictation."""
|
||
self.api.trusted = False
|
||
for _ in range(3):
|
||
with self.assertRaises(paste.PasteError):
|
||
paste.press("cmd+v")
|
||
self.opened.assert_called_once()
|
||
self.assertIn("Privacy_Accessibility", self.opened.call_args.args[0][1])
|
||
|
||
def test_a_system_that_will_not_open_the_pane_still_says_what_is_wrong(self):
|
||
self.api.trusted = False
|
||
self.opened.side_effect = OSError("no open(1) here")
|
||
with self.assertRaises(paste.PasteError) as caught:
|
||
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
|
||
self.assertFalse(paste.paste_ready())
|
||
|
||
def test_an_event_that_could_not_be_made_takes_its_pair_with_it(self):
|
||
self.api.makes = 1 # the second one comes back null
|
||
with self.assertRaises(paste.PasteError):
|
||
paste.press("cmd+v")
|
||
self.assertEqual(self.api.released, [1001])
|
||
self.assertEqual(self.api.posted, [])
|
||
|
||
def test_the_frameworks_not_being_there_is_not_a_crash(self):
|
||
"""Every other system imports this module too, and must survive it."""
|
||
self.patch_attr(paste, "_macos_api",
|
||
mock.Mock(side_effect=paste.PasteError("no such library")))
|
||
self.assertFalse(paste.paste_ready())
|
||
|
||
|
||
class MacPasteGoesWhereTheDictationStarted(MacOS):
|
||
"""The keys land in the frontmost window, so the front is what decides
|
||
where a transcript ends up."""
|
||
|
||
def setUp(self):
|
||
super().setUp()
|
||
from dikte import mac_window
|
||
self.mac_window = mac_window
|
||
self.activated = []
|
||
self.patch_attr(mac_window, "activate", self.activated.append)
|
||
|
||
def frontmost(self, dikte_is):
|
||
self.patch_attr(self.mac_window, "is_frontmost", lambda: dikte_is)
|
||
|
||
def test_a_dikte_that_took_the_front_hands_it_back_before_pressing(self):
|
||
self.frontmost(True)
|
||
paste.press("cmd+v", focus=4242)
|
||
self.assertEqual(self.activated, [4242])
|
||
self.assertEqual([event for _, event in self.api.posted], [1001, 1002])
|
||
|
||
def test_another_application_in_front_is_where_the_user_went_and_is_left(self):
|
||
self.frontmost(False)
|
||
paste.press("cmd+v", focus=4242)
|
||
self.assertEqual(self.activated, [])
|
||
|
||
def test_a_run_that_remembered_nobody_asks_nothing(self):
|
||
self.frontmost(True)
|
||
paste.press("cmd+v")
|
||
self.assertEqual(self.activated, [])
|
||
|
||
def test_the_front_is_handed_back_only_once_macos_trusts_dikte(self):
|
||
"""Pulling the user out of their window and then failing to type would
|
||
be the worst of both."""
|
||
self.frontmost(True)
|
||
self.api.trusted = False
|
||
with self.assertRaises(paste.PasteError):
|
||
paste.press("cmd+v", focus=4242)
|
||
self.assertEqual(self.activated, [])
|
||
|
||
class MacClipboardSnapshot(DikteTest):
|
||
def test_every_native_type_is_restored_and_the_files_are_removed(self):
|
||
directory = tempfile.mkdtemp(prefix="dikte-test-clipboard-")
|
||
manifest = '[[{"type":"public.tiff","file":"0-0.bin"}]]'
|
||
pathlib.Path(directory, "0-0.bin").write_bytes(b"a TIFF")
|
||
snapshot = paste._MAC_SNAPSHOT(directory, manifest)
|
||
|
||
with mock.patch.object(subprocess, "run",
|
||
return_value=FakeCompleted()) as run:
|
||
paste.copy_bytes(snapshot)
|
||
|
||
self.assertEqual(run.call_args.kwargs["input"], manifest)
|
||
self.assertEqual(run.call_args.kwargs["env"]["DIKTE_PASTEBOARD_DIR"],
|
||
directory)
|
||
self.assertFalse(os.path.exists(directory))
|
||
|
||
def test_a_failed_snapshot_leaves_no_temporary_directory(self):
|
||
directory = tempfile.mkdtemp(prefix="dikte-test-clipboard-")
|
||
with mock.patch.object(paste.tempfile, "mkdtemp", return_value=directory), \
|
||
mock.patch.object(subprocess, "run",
|
||
return_value=FakeCompleted(stdout=b"not json")):
|
||
self.assertIsNone(paste._macos_snapshot())
|
||
self.assertFalse(os.path.exists(directory))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|