use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use crate::{Backend, BackendKind, Capabilities, ClipboardError, MimeType, Selection};
type GetEntry = ((Selection, MimeType), Result<Vec<u8>, ClipboardError>);
type AvailableEntry = (Selection, Result<Vec<MimeType>, ClipboardError>);
#[derive(Debug, Clone, PartialEq)]
pub struct SetCall {
pub sel: Selection,
pub mime: MimeType,
pub bytes: Vec<u8>,
}
#[derive(Default)]
struct State {
sets: Vec<SetCall>,
clears: Vec<Selection>,
gets: Vec<GetEntry>,
availables: Vec<AvailableEntry>,
}
fn upsert_get(
slots: &mut Vec<GetEntry>,
key: (Selection, MimeType),
v: Result<Vec<u8>, ClipboardError>,
) {
if let Some(slot) = slots.iter_mut().find(|(k, _)| *k == key) {
slot.1 = v;
} else {
slots.push((key, v));
}
}
fn upsert_available(
slots: &mut Vec<AvailableEntry>,
key: Selection,
v: Result<Vec<MimeType>, ClipboardError>,
) {
if let Some(slot) = slots.iter_mut().find(|(k, _)| *k == key) {
slot.1 = v;
} else {
slots.push((key, v));
}
}
pub struct MockBackend {
kind: BackendKind,
caps: Capabilities,
state: Arc<Mutex<State>>,
}
impl MockBackend {
pub fn new(kind: BackendKind, caps: Capabilities) -> Self {
Self {
kind,
caps,
state: Arc::new(Mutex::new(State::default())),
}
}
pub fn handle(&self) -> MockHandle {
MockHandle {
state: Arc::clone(&self.state),
}
}
pub fn preset_get(
&self,
sel: Selection,
mime: MimeType,
response: Result<Vec<u8>, ClipboardError>,
) {
upsert_get(&mut self.state.lock().unwrap().gets, (sel, mime), response);
}
pub fn preset_available(
&self,
sel: Selection,
response: Result<Vec<MimeType>, ClipboardError>,
) {
upsert_available(&mut self.state.lock().unwrap().availables, sel, response);
}
}
#[derive(Clone)]
pub struct MockHandle {
state: Arc<Mutex<State>>,
}
impl MockHandle {
pub fn set_calls(&self) -> Vec<SetCall> {
self.state.lock().unwrap().sets.clone()
}
pub fn clear_calls(&self) -> Vec<Selection> {
self.state.lock().unwrap().clears.clone()
}
}
#[async_trait]
impl Backend for MockBackend {
fn kind(&self) -> BackendKind {
self.kind
}
fn capabilities(&self) -> Capabilities {
self.caps
}
fn set(&self, sel: Selection, mime: MimeType, bytes: &[u8]) -> Result<(), ClipboardError> {
self.state.lock().unwrap().sets.push(SetCall {
sel,
mime,
bytes: bytes.to_vec(),
});
Ok(())
}
fn get(&self, sel: Selection, mime: MimeType) -> Result<Vec<u8>, ClipboardError> {
let key = (sel, mime);
self.state
.lock()
.unwrap()
.gets
.iter()
.find(|(k, _)| *k == key)
.map_or(Err(ClipboardError::UnsupportedMime), |(_, v)| v.clone())
}
fn clear(&self, sel: Selection) -> Result<(), ClipboardError> {
self.state.lock().unwrap().clears.push(sel);
Ok(())
}
fn available(&self, sel: Selection) -> Result<Vec<MimeType>, ClipboardError> {
self.state
.lock()
.unwrap()
.availables
.iter()
.find(|(k, _)| *k == sel)
.map_or(Ok(Vec::new()), |(_, v)| v.clone())
}
async fn set_async(
&self,
sel: Selection,
mime: MimeType,
bytes: Vec<u8>,
) -> Result<(), ClipboardError> {
self.set(sel, mime, &bytes)
}
async fn get_async(&self, sel: Selection, mime: MimeType) -> Result<Vec<u8>, ClipboardError> {
self.get(sel, mime)
}
async fn clear_async(&self, sel: Selection) -> Result<(), ClipboardError> {
self.clear(sel)
}
async fn available_async(&self, sel: Selection) -> Result<Vec<MimeType>, ClipboardError> {
self.available(sel)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Clipboard;
#[test]
fn records_sets() {
let mock = MockBackend::new(BackendKind::Mock, Capabilities::all());
let handle = mock.handle();
let cb = Clipboard::with_backend(Box::new(mock));
cb.set(Selection::Clipboard, MimeType::Text, b"hi").unwrap();
cb.set(Selection::Primary, MimeType::Html, b"<p>x</p>")
.unwrap();
let calls = handle.set_calls();
assert_eq!(calls.len(), 2);
assert_eq!(calls[0].bytes, b"hi");
assert_eq!(calls[1].mime, MimeType::Html);
}
#[test]
fn preset_get_returns_canned_response() {
let mock = MockBackend::new(BackendKind::Mock, Capabilities::READ);
mock.preset_get(Selection::Clipboard, MimeType::Text, Ok(b"canned".to_vec()));
let cb = Clipboard::with_backend(Box::new(mock));
let got = cb.get(Selection::Clipboard, MimeType::Text).unwrap();
assert_eq!(got, b"canned");
}
#[test]
fn unprogrammed_get_returns_unsupported() {
let mock = MockBackend::new(BackendKind::Mock, Capabilities::READ);
let cb = Clipboard::with_backend(Box::new(mock));
let err = cb.get(Selection::Clipboard, MimeType::Text).unwrap_err();
assert!(matches!(err, ClipboardError::UnsupportedMime));
}
#[test]
fn kind_and_capabilities_round_trip() {
let mock = MockBackend::new(
BackendKind::Osc52,
Capabilities::WRITE | Capabilities::CLEAR,
);
let cb = Clipboard::with_backend(Box::new(mock));
assert_eq!(cb.kind(), BackendKind::Osc52);
assert_eq!(cb.capabilities(), Capabilities::WRITE | Capabilities::CLEAR);
}
}