use crate::{clipboard::Board, Connection, Noise, Secret};
use arboard::ImageData;
use std::{
sync::{
atomic::{AtomicUsize, Ordering},
Arc, LazyLock, RwLock,
},
time::Duration,
};
use tokio::{io::DuplexStream, runtime::Runtime, sync::Notify};
use tracing::*;
static RUNTIME: LazyLock<Runtime> = LazyLock::new(|| Runtime::new().unwrap());
static NOTIFY_A: LazyLock<Arc<Notify>> = LazyLock::new(|| Arc::new(Notify::const_new()));
static NOTIFY_B: LazyLock<Arc<Notify>> = LazyLock::new(|| Arc::new(Notify::const_new()));
static CLIPBOARD_A: RwLock<Option<String>> = RwLock::new(None);
static CLIPBOARD_B: RwLock<Option<String>> = RwLock::new(None);
struct ClipboardA;
struct ClipboardB;
impl Board for ClipboardA {
fn new() -> anyhow::Result<Self> {
Ok(Self)
}
fn set_text(&mut self, text: &str) {
let new = (!text.is_empty()).then(|| text.to_owned());
*CLIPBOARD_A.write().unwrap() = new;
}
fn get_text(&mut self) -> anyhow::Result<Option<String>> {
Ok(CLIPBOARD_A.read().unwrap().clone())
}
fn set_image(&mut self, _image: ImageData<'_>) {
unimplemented!()
}
fn get_image(&mut self) -> anyhow::Result<Option<ImageData<'static>>> {
Ok(None)
}
}
impl Board for ClipboardB {
fn new() -> anyhow::Result<Self> {
Ok(Self)
}
fn set_text(&mut self, text: &str) {
let new = (!text.is_empty()).then(|| text.to_owned());
*CLIPBOARD_B.write().unwrap() = new;
}
fn get_text(&mut self) -> anyhow::Result<Option<String>> {
let res = Ok(CLIPBOARD_B.read().unwrap().clone());
res
}
fn set_image(&mut self, _image: ImageData<'_>) {
unimplemented!()
}
fn get_image(&mut self) -> anyhow::Result<Option<ImageData<'static>>> {
Ok(None)
}
}
static COUNT: AtomicUsize = AtomicUsize::new(0);
pub fn test_setup() {
let notify_a = Arc::clone(&*NOTIFY_A);
RUNTIME.spawn(async {
crate::watch_local::<ClipboardA>(notify_a, Duration::from_millis(2))
.await
.unwrap();
});
}
pub fn test(input: &[String], password: &str) {
RUNTIME.block_on(async {
let (conn_a, conn_b) = dummy_connections(password).await;
let handle_a = tokio::task::spawn(async {
crate::watch_remote(conn_a, ClipboardA::new().unwrap(), Arc::clone(&*NOTIFY_A))
.await
.unwrap();
});
let handle_b = tokio::task::spawn(async {
crate::watch_remote(conn_b, ClipboardB::new().unwrap(), Arc::clone(&*NOTIFY_B))
.await
.unwrap();
});
tokio::time::sleep(Duration::from_millis(1)).await;
for x in input {
let prev = ClipboardB.get_text().unwrap();
let notified = NOTIFY_B.notified();
ClipboardA.set_text(x.as_str());
let change_expected = prev.as_deref() != Some(x.as_str()) && !x.is_empty();
let expected = if !change_expected {
prev.as_deref()
} else {
tokio::time::timeout(Duration::from_secs(5), notified)
.await
.expect("Waited 5 seconds but didn't see the clipboard come through?");
Some(x.as_str())
};
let b_contents = ClipboardB.get_text().unwrap();
if std::cmp::min(b_contents.unwrap_or_default().len(), expected.unwrap_or_default().len()) <= 1000 {
assert_eq!(
ClipboardB.get_text().unwrap().as_deref(),
expected,
"input: {}, password: {password}, change expected: {change_expected}. {} successful tests in total",
if x.len() < 100 { x } else { "<long, omitting>" },
COUNT.load(Ordering::Relaxed)
);
} else if ClipboardB.get_text().unwrap().as_deref() != expected {
panic!("ClipboardB's contents didn't match expected. Omitting contents due to length");
}
}
handle_a.abort();
handle_b.abort();
});
let done = COUNT.fetch_add(1, Ordering::Relaxed) + 1;
if done % 10 == 0 {
info!("Passed {done} tests (latest in: {input:?} pw: {password})");
}
}
async fn dummy_connections(password: &str) -> (Connection<DuplexStream>, Connection<DuplexStream>) {
let (mut stream_a, mut stream_b) = tokio::io::duplex(65535);
let secret = Secret::new(password, None);
let (host, client) = tokio::join! {
Noise::host(&mut stream_a, "b", &secret),
Noise::satellite(&mut stream_b, password),
};
let host = host.unwrap();
let client = client.unwrap();
let connection_a = Connection::new(stream_a, "b", host);
let connection_b = Connection::new(stream_b, "a", client);
(connection_a, connection_b)
}
#[test]
fn test_chunking() {
use rand::distr::{SampleString, Uniform};
crate::init_logging(Level::TRACE).unwrap();
let mut rng = rand::rng();
let uniform = Uniform::<char>::new('\u{0020}', '\u{10FFFF}').unwrap();
let mut clipboard = String::new();
uniform.append_string(&mut rng, &mut clipboard, 5_000_000);
test_setup();
test(&[clipboard], "foobar");
let mut clipboard = String::new();
let mut inputs = Vec::new();
for _ in 0..50 {
uniform.append_string(&mut rng, &mut clipboard, 500_000);
inputs.push(clipboard.clone());
clipboard.clear();
}
test(inputs.as_slice(), "foobar");
}