Skip to content

Commit

Permalink
feat: behavior tests (#7)
Browse files Browse the repository at this point in the history
* feat: init behavior tests

* feat: async filter behavior test

* fix: format and lint

* ci: setup test behavior

* fix: ci

* fix: ci
  • Loading branch information
ho-229 authored Aug 3, 2024
1 parent 13286c2 commit 99cbd90
Show file tree
Hide file tree
Showing 10 changed files with 384 additions and 13 deletions.
1 change: 1 addition & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
name: check

on:
workflow_dispatch:
push:
Expand Down
24 changes: 24 additions & 0 deletions .github/workflows/test_behavior.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: test behavior

on:
workflow_dispatch:
push:
paths:
- "**.rs"
- "**/Cargo.toml"
- ".github/workflows/test_behavior.yml"
pull_request:
paths:
- "**.rs"
- "**/Cargo.toml"
- ".github/workflows/test_behavior.yml"

jobs:
test:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable

- name: cargo test
run: cargo test behavior --all-features
13 changes: 12 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ description = "A safe and idiomatic wrapper around the Windows Cloud Filter API"
license = "MIT"
repository = "https://github.com/ho-229/cloud-filter-rs"
documentation = "https://docs.rs/cloud-filter"
exclude = ["examples/", ".github/"]
exclude = ["examples/", ".github/", "test/"]

[package.metadata.docs.rs]
default-target = "x86_64-pc-windows-msvc"
Expand Down Expand Up @@ -43,10 +43,21 @@ windows = { version = "0.58.0", features = [
] }
globset = { version = "0.4.9", optional = true }

[dev-dependencies]
libtest-mimic = "0.7.3"
futures = "0.3.30"
anyhow = "1.0.86"
powershell_script = "1.1.0"

[features]
# Enable globs in the `info::FetchPlaceholders` struct.
globs = ["globset"]

# TODO: temporarily ignored
[workspace]
members = ["examples/sftp"]

[[test]]
harness = false
name = "behavior"
path = "tests/behavior/main.rs"
16 changes: 14 additions & 2 deletions src/filter/async_filter.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{future::Future, mem::MaybeUninit, path::PathBuf};
use std::{future::Future, mem::MaybeUninit, ops::Deref, path::PathBuf};

use crate::{
error::{CResult, CloudErrorKind},
Expand Down Expand Up @@ -162,7 +162,7 @@ where
F: Filter,
B: Fn(LocalBoxFuture<'_, ()>) + Send + Sync,
{
pub fn new(filter: F, block_on: B) -> Self {
pub(crate) fn new(filter: F, block_on: B) -> Self {
Self { filter, block_on }
}
}
Expand Down Expand Up @@ -280,3 +280,15 @@ where
(self.block_on)(Box::pin(self.filter.state_changed(changes)))
}
}

impl<F, B> Deref for AsyncBridge<F, B>
where
F: Filter,
B: Fn(LocalBoxFuture<'_, ()>) + Send + Sync,
{
type Target = F;

fn deref(&self) -> &Self::Target {
&self.filter
}
}
3 changes: 1 addition & 2 deletions src/filter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ pub mod info;
/// Tickets for callbacks in the [SyncFilter][crate::SyncFilter] trait.
pub mod ticket;

pub(crate) use async_filter::AsyncBridge;
pub use async_filter::Filter;
pub use async_filter::{AsyncBridge, Filter};
pub(crate) use proxy::{callbacks, Callbacks};
pub use sync_filter::SyncFilter;

Expand Down
8 changes: 4 additions & 4 deletions src/root/connect.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::{
sync::mpsc::Sender,
sync::{mpsc::Sender, Arc},
thread::{self, JoinHandle},
time::Duration,
};
Expand All @@ -14,14 +14,14 @@ use crate::{filter::Callbacks, request::RawConnectionKey};
/// does **NOT** mean the sync root will be unregistered. To do so, call
/// [SyncRootId::unregister][crate::root::SyncRootId::unregister].
#[derive(Debug)]
pub struct Connection<T> {
pub struct Connection<F> {
connection_key: RawConnectionKey,

cancel_token: Sender<()>,
join_handle: JoinHandle<()>,

_callbacks: Callbacks,
filter: T,
filter: Arc<F>,
}

// this struct could house many more windows api functions, although they all seem to do nothing
Expand All @@ -32,7 +32,7 @@ impl<T> Connection<T> {
cancel_token: Sender<()>,
join_handle: JoinHandle<()>,
callbacks: Callbacks,
filter: T,
filter: Arc<T>,
) -> Self {
Self {
connection_key,
Expand Down
8 changes: 4 additions & 4 deletions src/root/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,16 +60,16 @@ impl Session {
}

/// Initiates a connection to the sync root with the given [SyncFilter].
pub fn connect<P, T>(self, path: P, filter: T) -> core::Result<Connection<Arc<T>>>
pub fn connect<P, F>(self, path: P, filter: F) -> core::Result<Connection<F>>
where
P: AsRef<Path>,
T: SyncFilter + 'static,
F: SyncFilter + 'static,
{
// https://github.com/microsoft/Windows-classic-samples/blob/27ffb0811ca761741502feaefdb591aebf592193/Samples/CloudMirror/CloudMirror/Utilities.cpp#L19
index_path(path.as_ref())?;

let filter = Arc::new(filter);
let callbacks = filter::callbacks::<T>();
let callbacks = filter::callbacks::<F>();
let key = unsafe {
CfConnectSyncRoot(
PCWSTR(
Expand Down Expand Up @@ -107,7 +107,7 @@ impl Session {
path: P,
filter: F,
block_on: B,
) -> core::Result<Connection<Arc<AsyncBridge<F, B>>>>
) -> core::Result<Connection<AsyncBridge<F, B>>>
where
P: AsRef<Path>,
F: Filter + 'static,
Expand Down
144 changes: 144 additions & 0 deletions tests/behavior/async_filter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
use core::str;
use std::{fs, future::Future, path::Path, pin::Pin};

use anyhow::Context;
use cloud_filter::{
error::{CResult, CloudErrorKind},
filter::{info, ticket, AsyncBridge, Filter},
metadata::Metadata,
placeholder_file::PlaceholderFile,
request::Request,
root::{
Connection, HydrationType, PopulationType, SecurityId, Session, SyncRootId,
SyncRootIdBuilder, SyncRootInfo,
},
utility::WriteAt,
};
use libtest_mimic::Failed;
use nt_time::FileTime;

const ROOT_PATH: &str = "C:\\async_filter_test";

struct MemFilter;

impl Filter for MemFilter {
async fn fetch_data(
&self,
request: Request,
ticket: ticket::FetchData,
info: info::FetchData,
) -> CResult<()> {
let path = unsafe { str::from_utf8_unchecked(request.file_blob()) };
println!("fetch_data: path: {path:?}");

let content = match path.as_ref() {
"test1.txt" | "dir1\\test2.txt" => path,
_ => Err(CloudErrorKind::InvalidRequest)?,
};

if info.required_file_range() != (0..content.len() as u64) {
Err(CloudErrorKind::InvalidRequest)?;
}

ticket.write_at(content.as_bytes(), 0).unwrap();

Ok(())
}

async fn fetch_placeholders(
&self,
request: Request,
ticket: ticket::FetchPlaceholders,
_info: info::FetchPlaceholders,
) -> CResult<()> {
let path = request.path();
let relative_path = path.strip_prefix(ROOT_PATH).unwrap();
println!("fetch_placeholders: path: {path:?}, relative path: {relative_path:?}");

let now = FileTime::now();
let mut placeholders = match relative_path.to_string_lossy().as_ref() {
"" => vec![
PlaceholderFile::new("dir1")
.mark_in_sync()
.metadata(Metadata::directory().created(now).written(now).size(0))
.blob("dir1".into()),
PlaceholderFile::new("test1.txt")
.has_no_children()
.mark_in_sync()
.metadata(
Metadata::file()
.created(now)
.written(now)
.size("test1.txt".len() as _),
)
.blob("test1.txt".into()),
],
"dir1" => vec![PlaceholderFile::new("test2.txt")
.has_no_children()
.mark_in_sync()
.metadata(
Metadata::file()
.created(now)
.written(now)
.size("dir1\\test2.txt".len() as _),
)
.blob("dir1\\test2.txt".into())],
_ => Err(CloudErrorKind::InvalidRequest)?,
};

ticket.pass_with_placeholder(&mut placeholders).unwrap();
Ok(())
}
}

fn init() -> anyhow::Result<(
SyncRootId,
Connection<AsyncBridge<MemFilter, impl Fn(Pin<Box<dyn Future<Output = ()>>>)>>,
)> {
let sync_root_id = SyncRootIdBuilder::new("sync_filter_test_provider")
.user_security_id(SecurityId::current_user().context("current_user")?)
.build();

if !sync_root_id.is_registered().context("is_registered")? {
sync_root_id
.register(
SyncRootInfo::default()
.with_display_name("Sync Filter Test")
.with_hydration_type(HydrationType::Full)
.with_population_type(PopulationType::Full)
.with_icon("%SystemRoot%\\system32\\charmap.exe,0")
.with_version("1.0.0")
.with_recycle_bin_uri("http://cloudmirror.example.com/recyclebin")
.context("recycle_bin_uri")?
.with_path(ROOT_PATH)
.context("path")?,
)
.context("register")?
}

let connection = Session::new()
.connect_async(ROOT_PATH, MemFilter, move |f| {
futures::executor::block_on(f)
})
.context("connect")?;

Ok((sync_root_id, connection))
}

pub fn test() -> Result<(), Failed> {
if !Path::new(ROOT_PATH).try_exists().context("exists")? {
fs::create_dir(ROOT_PATH).context("create root dir")?;
}

let (sync_root_id, connection) = init().context("init")?;

crate::test_list_folders(ROOT_PATH);
crate::test_read_file(ROOT_PATH);

drop(connection);
sync_root_id.unregister().context("unregister")?;

fs::remove_dir_all(ROOT_PATH).context("remove root dir")?;

Ok(())
}
41 changes: 41 additions & 0 deletions tests/behavior/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use std::process::ExitCode;

use libtest_mimic::{run, Arguments, Trial};

mod async_filter;
mod sync_filter;

fn main() -> ExitCode {
let args = Arguments::from_args();
let tests = vec![Trial::test("sync_filter", sync_filter::test)];

let conclusion = run(&args, tests);
if conclusion.has_failed() {
return conclusion.exit_code();
}

let tests = vec![Trial::test("async_filter", async_filter::test)];
let conclusion = run(&args, tests);

conclusion.exit_code()
}

fn test_list_folders(root: &str) {
let output = powershell_script::run(&format!("Get-ChildItem {root} -Recurse -Name"))
.expect("run script");
assert_eq!(
"dir1\r\n\
test1.txt\r\n\
dir1\\test2.txt\r\n",
output.stdout().expect("stdout"),
);
}

fn test_read_file(root: &str) {
for relative in ["test1.txt", "dir1\\test2.txt"] {
let path = format!("{root}\\{relative}");
let output =
powershell_script::run(&format!("Get-Content {path} -Raw")).expect("run script");
assert_eq!(output.stdout().expect("stdout"), format!("{relative}\r\n"));
}
}
Loading

0 comments on commit 99cbd90

Please sign in to comment.