#![cfg(windows)]
mod common;
use futures::FutureExt;
use mtp_rs::{Backend, DeviceEvent, MtpDevice, ObjectHandle, Storage};
use std::panic::AssertUnwindSafe;
use std::time::{Duration, Instant};
use tokio::time::timeout;
async fn open_scoped(folder: &str) -> (MtpDevice, Storage, ObjectHandle) {
let device = MtpDevice::builder()
.backend(Backend::Wpd)
.open_first()
.await
.expect("open a WPD device (phone connected, unlocked, MTP mode)");
let storage = device
.storages()
.await
.expect("list storages")
.into_iter()
.next()
.expect("device has at least one storage");
let root = storage.list_objects(None).await.expect("list storage root");
let download = root
.iter()
.find(|o| o.is_folder() && o.filename.eq_ignore_ascii_case("Download"))
.expect("a Download folder in the storage root")
.handle;
if let Ok(existing) = storage.list_objects(Some(download)).await {
for stale in existing
.iter()
.filter(|o| o.is_folder() && o.filename == folder)
{
let _ = storage.delete(stale.handle).await;
}
}
let test_dir = storage
.create_folder(Some(download), folder)
.await
.expect("create the scoped test folder under Download");
(device, storage, test_dir)
}
macro_rules! wpd_conformance {
($name:ident, $folder:expr, $body:expr) => {
#[tokio::test]
#[ignore = "requires a real WPD device connected in MTP mode"]
async fn $name() {
let (_device, storage, dir) = open_scoped($folder).await;
let result = AssertUnwindSafe(($body)(&storage, dir))
.catch_unwind()
.await;
if let Err(e) = storage.delete(dir).await {
eprintln!(
"cleanup: failed to delete scoped folder {dir:?}: {e} — remove it manually"
);
}
if let Err(panic) = result {
std::panic::resume_unwind(panic);
}
}
};
}
wpd_conformance!(wpd_round_trip, "mtp-rs-conformance-roundtrip", |s, dir| {
common::run_round_trip(s, Some(dir))
});
wpd_conformance!(
wpd_ranged_and_resumable,
"mtp-rs-conformance-ranged",
|s, dir| { common::run_ranged_and_resumable(s, Some(dir)) }
);
wpd_conformance!(
wpd_windowed_download,
"mtp-rs-conformance-windowed",
|s, dir| { common::run_windowed_download(s, Some(dir)) }
);
wpd_conformance!(
wpd_create_folder_rename_move_copy,
"mtp-rs-conformance-fsops",
|s, dir| common::run_create_folder_rename_move_copy(s, Some(dir))
);
wpd_conformance!(
wpd_recursive_listing,
"mtp-rs-conformance-recursive",
|s, dir| { common::run_recursive_listing(s, Some(dir)) }
);
wpd_conformance!(
wpd_upload_with_progress,
"mtp-rs-conformance-progress",
|s, dir| { common::run_upload_with_progress(s, Some(dir)) }
);
wpd_conformance!(
wpd_upload_cancel,
"mtp-rs-conformance-upload-cancel",
|s, dir| { common::run_upload_cancel(s, Some(dir), false) }
);
wpd_conformance!(
wpd_download_cancel,
"mtp-rs-conformance-dl-cancel",
|s, dir| { common::run_download_cancel(s, Some(dir)) }
);
wpd_conformance!(wpd_download_drop, "mtp-rs-conformance-dl-drop", |s, dir| {
common::run_download_drop(s, Some(dir))
});
wpd_conformance!(
wpd_list_cancel_token,
"mtp-rs-conformance-list-cancel",
|s, dir| { common::run_list_cancel_token(s, Some(dir)) }
);
wpd_conformance!(
wpd_thumbnail_unsupported,
"mtp-rs-conformance-thumb",
|s, dir| { common::run_thumbnail_unsupported(s, Some(dir)) }
);
#[tokio::test]
#[ignore = "requires a real WPD device connected in MTP mode"]
async fn wpd_object_added_event() {
let (device, storage, dir) = open_scoped("mtp-rs-conformance-events").await;
let result = AssertUnwindSafe(async {
while timeout(Duration::from_millis(500), device.next_event())
.await
.is_ok()
{}
let child = storage
.create_folder(Some(dir), "evt-probe")
.await
.expect("create child folder to trigger an event");
let mut observed: Option<DeviceEvent> = None;
let watch_until = Instant::now() + Duration::from_secs(5);
while Instant::now() < watch_until && observed.is_none() {
match timeout(Duration::from_secs(1), device.next_event()).await {
Ok(Ok(ev)) => match &ev {
DeviceEvent::ObjectAdded { handle }
| DeviceEvent::ObjectInfoChanged { handle }
if *handle == child =>
{
observed = Some(ev);
}
other => eprintln!(" (other event while waiting: {other:?})"),
},
Ok(Err(e)) => panic!("next_event errored unexpectedly: {e}"),
Err(_) => { }
}
}
match observed {
Some(ev) => {
println!("FINDING: the device DID emit an event for a host create_folder: {ev:?}")
}
None => println!(
"FINDING: the device did NOT emit an event for a host create_folder within 5s \
(tolerated — event delivery for host-initiated changes is device-dependent)"
),
}
})
.catch_unwind()
.await;
if let Err(e) = storage.delete(dir).await {
eprintln!("cleanup: failed to delete scoped folder {dir:?}: {e} — remove it manually");
}
if let Err(panic) = result {
std::panic::resume_unwind(panic);
}
}
#[tokio::test]
#[ignore = "requires a real WPD device connected in MTP mode"]
async fn wpd_open_by_location() {
let target = MtpDevice::list_devices()
.expect("list devices")
.into_iter()
.find(|d| d.serial_number.is_some())
.expect("an enumerable USB device (the phone) to take a location_id from");
let device = MtpDevice::open_by_location(target.location_id)
.await
.expect(
"open_by_location should route to WPD on Windows, not fail on the WPD-bound driver",
);
let storages = device.storages().await.expect("storages over WPD");
assert!(
!storages.is_empty(),
"device should report at least one storage"
);
}