mirror of
https://github.com/readur/readur.git
synced 2026-02-22 06:49:38 -06:00
feat(tests): implement dav-server instead of mock for stress testing
This commit is contained in:
143
Cargo.lock
generated
143
Cargo.lock
generated
@@ -400,7 +400,7 @@ dependencies = [
|
||||
"http 0.2.12",
|
||||
"http 1.3.1",
|
||||
"http-body 0.4.6",
|
||||
"lru",
|
||||
"lru 0.12.5",
|
||||
"percent-encoding",
|
||||
"regex-lite",
|
||||
"sha2",
|
||||
@@ -1382,6 +1382,41 @@ version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
|
||||
|
||||
[[package]]
|
||||
name = "dav-server"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1325ec68fa2627b069d06d5096d6109da5ebf46d45099e01e2dc59a4bcc4641e"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"derivative",
|
||||
"dyn-clone",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"headers",
|
||||
"htmlescape",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
"lru 0.14.0",
|
||||
"mime_guess",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"pin-project",
|
||||
"pin-utils",
|
||||
"reflink-copy",
|
||||
"regex",
|
||||
"time",
|
||||
"tokio",
|
||||
"url",
|
||||
"uuid",
|
||||
"xml-rs",
|
||||
"xmltree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deadpool"
|
||||
version = "0.12.2"
|
||||
@@ -1430,6 +1465,17 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derivative"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.1"
|
||||
@@ -1993,6 +2039,30 @@ dependencies = [
|
||||
"hashbrown 0.15.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "headers"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"headers-core",
|
||||
"http 1.3.1",
|
||||
"httpdate",
|
||||
"mime",
|
||||
"sha1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "headers-core"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4"
|
||||
dependencies = [
|
||||
"http 1.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
@@ -2049,6 +2119,12 @@ dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "htmlescape"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.12"
|
||||
@@ -2799,6 +2875,15 @@ dependencies = [
|
||||
"hashbrown 0.15.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f8cc7106155f10bdf99a6f379688f543ad6596a415375b36a59a054ceda1198"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.2.0"
|
||||
@@ -3376,6 +3461,26 @@ version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
|
||||
dependencies = [
|
||||
"pin-project-internal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-internal"
|
||||
version = "1.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.103",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.16"
|
||||
@@ -3716,12 +3821,17 @@ dependencies = [
|
||||
"axum",
|
||||
"base64ct",
|
||||
"bcrypt",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"clap",
|
||||
"dav-server",
|
||||
"dotenvy",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"hostname",
|
||||
"http-body-util",
|
||||
"hyper 1.7.0",
|
||||
"hyper-util",
|
||||
"image",
|
||||
"imageproc",
|
||||
"infer",
|
||||
@@ -3802,6 +3912,18 @@ dependencies = [
|
||||
"syn 2.0.103",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reflink-copy"
|
||||
version = "0.1.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23bbed272e39c47a095a5242218a67412a220006842558b03fe2935e8f3d7b92"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"rustix 1.0.7",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.2"
|
||||
@@ -4907,7 +5029,7 @@ dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
"once_cell",
|
||||
"rustix 1.0.7",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5746,7 +5868,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6171,12 +6293,27 @@ dependencies = [
|
||||
"rustix 1.0.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xml-rs"
|
||||
version = "0.8.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
|
||||
|
||||
[[package]]
|
||||
name = "xmlparser"
|
||||
version = "0.13.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4"
|
||||
|
||||
[[package]]
|
||||
name = "xmltree"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b619f8c85654798007fb10afa5125590b43b088c225a25fc2fec100a9fad0fc6"
|
||||
dependencies = [
|
||||
"xml-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "y4m"
|
||||
version = "0.8.0"
|
||||
|
||||
@@ -80,7 +80,7 @@ stress-testing = ["test-utils"]
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
wiremock = "0.6"
|
||||
wiremock = "0.6"
|
||||
tokio-test = "0.4"
|
||||
futures = "0.3"
|
||||
rand = "0.8"
|
||||
@@ -89,6 +89,12 @@ testcontainers = "0.24"
|
||||
testcontainers-modules = { version = "0.12", features = ["postgres"] }
|
||||
# Dependencies for creating proper test Office documents
|
||||
rust_xlsxwriter = "0.92" # For creating proper XLSX test files
|
||||
# WebDAV server for realistic stress testing
|
||||
dav-server = { version = "0.8", features = ["memfs"] }
|
||||
hyper = { version = "1", features = ["server", "http1"] }
|
||||
hyper-util = { version = "0.1", features = ["tokio"] }
|
||||
http-body-util = "0.1"
|
||||
bytes = "1"
|
||||
|
||||
# Enable test-utils feature for all tests
|
||||
readur = { path = ".", features = ["test-utils"] }
|
||||
|
||||
16
stress-test-metrics.json
Normal file
16
stress-test-metrics.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"test_suite_version": "2.5.3",
|
||||
"test_timestamp": "2025-12-15T20:10:14.695379292Z",
|
||||
"overall_result": "PASSED",
|
||||
"test_summary": {
|
||||
"total_tests": 4,
|
||||
"passed_tests": 4,
|
||||
"failed_tests": 0,
|
||||
"skipped_tests": 0
|
||||
},
|
||||
"recommendations": [
|
||||
"WebDAV sync appears to be functioning correctly under stress conditions",
|
||||
"No infinite loop patterns detected in current test scenarios",
|
||||
"Consider running more intensive stress tests in staging environment"
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* WebDAV Stress Testing Suite
|
||||
*
|
||||
*
|
||||
* Comprehensive stress tests for WebDAV sync functionality with infinite loop detection.
|
||||
* These tests create complex directory structures and monitor for problematic behavior
|
||||
* patterns that could indicate infinite loops or performance issues.
|
||||
@@ -15,9 +15,15 @@ use std::time::{Duration, Instant};
|
||||
use tokio::sync::{Mutex, RwLock, Semaphore};
|
||||
use tokio::time::{sleep, timeout, interval};
|
||||
use tokio::net::TcpListener;
|
||||
use axum::{Router, routing::any, http::StatusCode, response::IntoResponse};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
// dav-server imports for realistic WebDAV testing
|
||||
use dav_server::{fakels::FakeLs, memfs::MemFs, DavHandler};
|
||||
use http_body_util::{BodyExt, Full};
|
||||
use hyper::server::conn::http1;
|
||||
use hyper::service::service_fn;
|
||||
use hyper_util::rt::TokioIo;
|
||||
|
||||
#[cfg(feature = "stress-testing")]
|
||||
use readur::services::webdav::{WebDAVService, WebDAVConfig};
|
||||
|
||||
@@ -31,17 +37,26 @@ fn init_tracing() {
|
||||
});
|
||||
}
|
||||
|
||||
/// Mock WebDAV server for testing
|
||||
/// Realistic WebDAV server for testing using dav-server with in-memory filesystem
|
||||
struct MockWebDAVServer {
|
||||
port: u16,
|
||||
server_handle: Option<tokio::task::JoinHandle<()>>,
|
||||
shutdown_signal: Option<tokio::sync::oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
impl MockWebDAVServer {
|
||||
async fn start() -> Result<Self> {
|
||||
let app = Router::new()
|
||||
.route("/", any(mock_webdav_handler))
|
||||
.fallback(mock_webdav_handler);
|
||||
// Create in-memory filesystem with realistic test structure
|
||||
let memfs = MemFs::new();
|
||||
|
||||
// Create the test directory structure that matches the test paths
|
||||
Self::create_test_structure(&memfs).await?;
|
||||
|
||||
// Build the WebDAV handler
|
||||
let dav_handler = DavHandler::builder()
|
||||
.filesystem(memfs)
|
||||
.locksystem(FakeLs::new())
|
||||
.build_handler();
|
||||
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await
|
||||
.map_err(|e| anyhow!("Failed to bind to port: {}", e))?;
|
||||
@@ -49,97 +64,197 @@ impl MockWebDAVServer {
|
||||
.map_err(|e| anyhow!("Failed to get local address: {}", e))?
|
||||
.port();
|
||||
|
||||
let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel::<()>();
|
||||
|
||||
let server_handle = tokio::spawn(async move {
|
||||
if let Err(e) = axum::serve(listener, app).await {
|
||||
error!("Mock WebDAV server error: {}", e);
|
||||
loop {
|
||||
tokio::select! {
|
||||
result = listener.accept() => {
|
||||
match result {
|
||||
Ok((stream, _addr)) => {
|
||||
let io = TokioIo::new(stream);
|
||||
let handler = dav_handler.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let service = service_fn(move |req| {
|
||||
let handler = handler.clone();
|
||||
async move {
|
||||
let response = handler.handle(req).await;
|
||||
// Convert DavResponse to hyper Response
|
||||
let (parts, body) = response.into_parts();
|
||||
let body_bytes = body.collect().await
|
||||
.map(|c| c.to_bytes())
|
||||
.unwrap_or_default();
|
||||
Ok::<_, std::convert::Infallible>(
|
||||
hyper::Response::from_parts(parts, Full::new(body_bytes))
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
if let Err(e) = http1::Builder::new()
|
||||
.serve_connection(io, service)
|
||||
.await
|
||||
{
|
||||
debug!("WebDAV connection error: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to accept connection: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = &mut shutdown_rx => {
|
||||
info!("WebDAV server shutting down");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Give the server a moment to start
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
|
||||
info!("Mock WebDAV server started on port {}", port);
|
||||
info!("Realistic WebDAV server (dav-server) started on port {}", port);
|
||||
|
||||
Ok(Self {
|
||||
port,
|
||||
server_handle: Some(server_handle),
|
||||
shutdown_signal: Some(shutdown_tx),
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a realistic test directory structure in the in-memory filesystem
|
||||
async fn create_test_structure(memfs: &MemFs) -> Result<()> {
|
||||
use dav_server::davpath::DavPath;
|
||||
use dav_server::fs::{DavFile, DavFileSystem, OpenOptions};
|
||||
use bytes::Bytes;
|
||||
|
||||
// Define the directory structure matching our test paths
|
||||
let directories = vec![
|
||||
"/main-structure",
|
||||
"/main-structure/documents",
|
||||
"/main-structure/images",
|
||||
"/main-structure/archives",
|
||||
"/loop-traps",
|
||||
"/loop-traps/deep-nesting",
|
||||
"/loop-traps/deep-nesting/level1",
|
||||
"/loop-traps/deep-nesting/level1/level2",
|
||||
"/loop-traps/deep-nesting/level1/level2/level3",
|
||||
"/symlink-test",
|
||||
"/symlink-test/folder1",
|
||||
"/symlink-test/folder2",
|
||||
"/test-repo-1",
|
||||
"/test-repo-1/src",
|
||||
"/test-repo-1/docs",
|
||||
"/large-directory",
|
||||
"/unicode-test",
|
||||
"/unicode-test/subfolder1",
|
||||
"/unicode-test/subfolder2",
|
||||
];
|
||||
|
||||
// Create directories
|
||||
for dir_path in &directories {
|
||||
let path = DavPath::new(dir_path)
|
||||
.map_err(|e| anyhow!("Invalid path {}: {:?}", dir_path, e))?;
|
||||
if let Err(e) = memfs.create_dir(&path).await {
|
||||
debug!("Directory {} may already exist: {:?}", dir_path, e);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to create a file with content
|
||||
async fn create_file(memfs: &MemFs, file_path: &str, content: &str) -> Result<()> {
|
||||
let path = DavPath::new(file_path)
|
||||
.map_err(|e| anyhow!("Invalid path {}: {:?}", file_path, e))?;
|
||||
let options = OpenOptions {
|
||||
read: false,
|
||||
write: true,
|
||||
append: false,
|
||||
truncate: true,
|
||||
create: true,
|
||||
create_new: false,
|
||||
size: Some(content.len() as u64),
|
||||
checksum: None,
|
||||
};
|
||||
let mut file = memfs.open(&path, options).await
|
||||
.map_err(|e| anyhow!("Failed to create file {}: {:?}", file_path, e))?;
|
||||
file.write_bytes(Bytes::from(content.to_string())).await
|
||||
.map_err(|e| anyhow!("Failed to write to file {}: {:?}", file_path, e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Create some test files in various directories
|
||||
let files = vec![
|
||||
("/main-structure/readme.txt", "Main structure readme content"),
|
||||
("/main-structure/documents/report.pdf", "Fake PDF content for testing"),
|
||||
("/main-structure/documents/notes.txt", "Some notes here"),
|
||||
("/main-structure/images/photo.jpg", "Fake JPEG data"),
|
||||
("/loop-traps/trap-file.txt", "Loop trap test file"),
|
||||
("/loop-traps/deep-nesting/nested-file.txt", "Deeply nested file"),
|
||||
("/loop-traps/deep-nesting/level1/level2/level3/bottom.txt", "Bottom of nesting"),
|
||||
("/symlink-test/test.txt", "Symlink test file"),
|
||||
("/symlink-test/folder1/file1.txt", "File in folder1"),
|
||||
("/symlink-test/folder2/file2.txt", "File in folder2"),
|
||||
("/test-repo-1/README.md", "# Test Repository\n\nThis is a test."),
|
||||
("/test-repo-1/src/main.rs", "fn main() { println!(\"Hello\"); }"),
|
||||
("/test-repo-1/docs/guide.md", "# User Guide"),
|
||||
("/unicode-test/subfolder1/test1.txt", "Unicode test content 1"),
|
||||
("/unicode-test/subfolder2/test2.txt", "Unicode test content 2"),
|
||||
];
|
||||
|
||||
let num_files = files.len();
|
||||
|
||||
// Add many files to large-directory for stress testing
|
||||
for i in 0..50 {
|
||||
let path_str = format!("/large-directory/file_{:04}.txt", i);
|
||||
let content = format!("Content of file {}", i);
|
||||
if let Err(e) = create_file(memfs, &path_str, &content).await {
|
||||
debug!("Failed to create file {}: {:?}", path_str, e);
|
||||
}
|
||||
}
|
||||
|
||||
// Create the regular test files
|
||||
for (file_path, content) in files {
|
||||
if let Err(e) = create_file(memfs, file_path, content).await {
|
||||
debug!("Failed to create file {}: {:?}", file_path, e);
|
||||
}
|
||||
}
|
||||
|
||||
info!("Created test directory structure with {} directories and {} files",
|
||||
directories.len(), num_files + 50);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn url(&self) -> String {
|
||||
format!("http://127.0.0.1:{}", self.port)
|
||||
}
|
||||
|
||||
async fn stop(&mut self) {
|
||||
// Send shutdown signal
|
||||
if let Some(tx) = self.shutdown_signal.take() {
|
||||
let _ = tx.send(());
|
||||
}
|
||||
|
||||
// Wait for server to stop
|
||||
if let Some(handle) = self.server_handle.take() {
|
||||
handle.abort();
|
||||
let _ = handle.await; // Ignore the abort error
|
||||
let _ = handle.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MockWebDAVServer {
|
||||
fn drop(&mut self) {
|
||||
if let Some(tx) = self.shutdown_signal.take() {
|
||||
let _ = tx.send(());
|
||||
}
|
||||
if let Some(handle) = self.server_handle.take() {
|
||||
handle.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mock WebDAV request handler that responds with valid WebDAV XML
|
||||
async fn mock_webdav_handler(req: axum::extract::Request) -> impl IntoResponse {
|
||||
let path = req.uri().path();
|
||||
let method = req.method();
|
||||
|
||||
debug!("Mock WebDAV request: {} {}", method, path);
|
||||
|
||||
match method.as_str() {
|
||||
"PROPFIND" => {
|
||||
// Return a valid WebDAV PROPFIND response
|
||||
let response_body = format!(r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<d:multistatus xmlns:d="DAV:">
|
||||
<d:response>
|
||||
<d:href>{}</d:href>
|
||||
<d:propstat>
|
||||
<d:prop>
|
||||
<d:resourcetype><d:collection/></d:resourcetype>
|
||||
<d:displayname>{}</d:displayname>
|
||||
<d:getlastmodified>Wed, 01 Jan 2025 12:00:00 GMT</d:getlastmodified>
|
||||
<d:creationdate>2025-01-01T12:00:00Z</d:creationdate>
|
||||
</d:prop>
|
||||
<d:status>HTTP/1.1 200 OK</d:status>
|
||||
</d:propstat>
|
||||
</d:response>
|
||||
<d:response>
|
||||
<d:href>{}/test-file.txt</d:href>
|
||||
<d:propstat>
|
||||
<d:prop>
|
||||
<d:resourcetype/>
|
||||
<d:displayname>test-file.txt</d:displayname>
|
||||
<d:getcontentlength>42</d:getcontentlength>
|
||||
<d:getlastmodified>Wed, 01 Jan 2025 12:00:00 GMT</d:getlastmodified>
|
||||
<d:creationdate>2025-01-01T12:00:00Z</d:creationdate>
|
||||
</d:prop>
|
||||
<d:status>HTTP/1.1 200 OK</d:status>
|
||||
</d:propstat>
|
||||
</d:response>
|
||||
</d:multistatus>"#, path, path.trim_start_matches('/'), path);
|
||||
|
||||
(StatusCode::MULTI_STATUS, [("Content-Type", "application/xml; charset=utf-8")], response_body)
|
||||
},
|
||||
"GET" => {
|
||||
if path.ends_with(".txt") {
|
||||
(StatusCode::OK, [("Content-Type", "text/plain")], "Mock file content".to_string())
|
||||
} else {
|
||||
(StatusCode::NOT_FOUND, [("Content-Type", "text/plain")], "Not found".to_string())
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
(StatusCode::METHOD_NOT_ALLOWED, [("Content-Type", "text/plain")], "Method not allowed".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Circuit breaker for protecting against infinite loops and cascading failures
|
||||
#[derive(Debug)]
|
||||
pub struct CircuitBreaker {
|
||||
@@ -755,6 +870,14 @@ fn get_stress_test_config(mock_server_url: Option<String>) -> Result<StressTestC
|
||||
.unwrap_or_else(|_| "120".to_string()) // Shorter timeout for tests
|
||||
.parse::<u64>()?;
|
||||
|
||||
// Calculate loop detection threshold based on stress level
|
||||
// The test cycles through 8 paths, so each path is accessed (operation_count / 8) times
|
||||
// We set the threshold to 3x the expected accesses to allow for legitimate concurrent access
|
||||
// while still detecting actual infinite loops (which would have much higher access counts)
|
||||
let num_test_paths = 8;
|
||||
let expected_accesses_per_path = stress_level.operation_count() / num_test_paths;
|
||||
let loop_detection_threshold = std::cmp::max(expected_accesses_per_path * 3, 20);
|
||||
|
||||
Ok(StressTestConfig {
|
||||
webdav_server_url,
|
||||
username,
|
||||
@@ -762,7 +885,7 @@ fn get_stress_test_config(mock_server_url: Option<String>) -> Result<StressTestC
|
||||
stress_level,
|
||||
test_timeout_seconds,
|
||||
max_concurrent_operations: 4, // Reduced for tests
|
||||
loop_detection_threshold: 20, // Lower threshold for tests
|
||||
loop_detection_threshold,
|
||||
scan_timeout_seconds: 15, // Shorter scan timeout
|
||||
})
|
||||
}
|
||||
@@ -774,11 +897,22 @@ async fn test_infinite_loop_detection() -> Result<()> {
|
||||
|
||||
info!("Starting infinite loop detection stress test");
|
||||
|
||||
// Start mock WebDAV server
|
||||
let mut mock_server = MockWebDAVServer::start().await
|
||||
.map_err(|e| anyhow!("Failed to start mock server: {}", e))?;
|
||||
// Use real WebDAV server if WEBDAV_SERVER_URL is set (e.g., in CI with Dufs),
|
||||
// otherwise fall back to mock server for local testing
|
||||
let mut mock_server: Option<MockWebDAVServer> = None;
|
||||
let config = if std::env::var("WEBDAV_SERVER_URL").is_ok() {
|
||||
info!("Using real WebDAV server from WEBDAV_SERVER_URL environment variable");
|
||||
get_stress_test_config(None)?
|
||||
} else {
|
||||
info!("No WEBDAV_SERVER_URL set, starting mock WebDAV server for local testing");
|
||||
let server = MockWebDAVServer::start().await
|
||||
.map_err(|e| anyhow!("Failed to start mock server: {}", e))?;
|
||||
let url = server.url();
|
||||
mock_server = Some(server);
|
||||
get_stress_test_config(Some(url))?
|
||||
};
|
||||
|
||||
let config = get_stress_test_config(Some(mock_server.url()))?;
|
||||
info!("WebDAV server URL: {}", config.webdav_server_url);
|
||||
let webdav_service = create_stress_test_webdav_service(&config)?;
|
||||
let loop_monitor = Arc::new(LoopDetectionMonitor::new(config.loop_detection_threshold));
|
||||
|
||||
@@ -823,7 +957,9 @@ async fn test_infinite_loop_detection() -> Result<()> {
|
||||
|
||||
// Clean up monitoring resources
|
||||
loop_monitor.stop_monitoring().await;
|
||||
mock_server.stop().await;
|
||||
if let Some(mut server) = mock_server {
|
||||
server.stop().await;
|
||||
}
|
||||
|
||||
cleanup_result
|
||||
}
|
||||
@@ -888,9 +1024,13 @@ async fn perform_loop_detection_test(
|
||||
result.files.len(), result.directories.len(), path);
|
||||
|
||||
// If we find subdirectories, recursively scan some of them
|
||||
for subdir in result.directories.iter().take(3) {
|
||||
// Skip directories that match the parent path (mock server returns parent as a directory)
|
||||
for subdir in result.directories.iter()
|
||||
.filter(|d| d.relative_path != path && d.relative_path.trim_end_matches('/') != path)
|
||||
.take(3)
|
||||
{
|
||||
monitor.record_directory_access(&subdir.relative_path).await;
|
||||
|
||||
|
||||
match service.discover_files(&subdir.relative_path, false).await {
|
||||
Ok(files) => {
|
||||
debug!("Found {} files in subdirectory {}", files.len(), subdir.relative_path);
|
||||
@@ -960,11 +1100,22 @@ async fn test_directory_scanning_stress() -> Result<()> {
|
||||
|
||||
info!("Starting directory scanning stress test");
|
||||
|
||||
// Start mock WebDAV server
|
||||
let mut mock_server = MockWebDAVServer::start().await
|
||||
.map_err(|e| anyhow!("Failed to start mock server: {}", e))?;
|
||||
// Use real WebDAV server if WEBDAV_SERVER_URL is set (e.g., in CI with Dufs),
|
||||
// otherwise fall back to mock server for local testing
|
||||
let mut mock_server: Option<MockWebDAVServer> = None;
|
||||
let config = if std::env::var("WEBDAV_SERVER_URL").is_ok() {
|
||||
info!("Using real WebDAV server from WEBDAV_SERVER_URL environment variable");
|
||||
get_stress_test_config(None)?
|
||||
} else {
|
||||
info!("No WEBDAV_SERVER_URL set, starting mock WebDAV server for local testing");
|
||||
let server = MockWebDAVServer::start().await
|
||||
.map_err(|e| anyhow!("Failed to start mock server: {}", e))?;
|
||||
let url = server.url();
|
||||
mock_server = Some(server);
|
||||
get_stress_test_config(Some(url))?
|
||||
};
|
||||
|
||||
let config = get_stress_test_config(Some(mock_server.url()))?;
|
||||
info!("WebDAV server URL: {}", config.webdav_server_url);
|
||||
let webdav_service = create_stress_test_webdav_service(&config)?;
|
||||
|
||||
// Test deep recursive scanning
|
||||
@@ -1017,8 +1168,10 @@ async fn test_directory_scanning_stress() -> Result<()> {
|
||||
}
|
||||
};
|
||||
|
||||
// Clean up mock server
|
||||
mock_server.stop().await;
|
||||
// Clean up mock server if we started one
|
||||
if let Some(mut server) = mock_server {
|
||||
server.stop().await;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
@@ -1233,11 +1386,22 @@ async fn test_concurrent_webdav_access() -> Result<()> {
|
||||
|
||||
info!("Starting concurrent WebDAV access stress test");
|
||||
|
||||
// Start mock WebDAV server
|
||||
let mut mock_server = MockWebDAVServer::start().await
|
||||
.map_err(|e| anyhow!("Failed to start mock server: {}", e))?;
|
||||
// Use real WebDAV server if WEBDAV_SERVER_URL is set (e.g., in CI with Dufs),
|
||||
// otherwise fall back to mock server for local testing
|
||||
let mut mock_server: Option<MockWebDAVServer> = None;
|
||||
let config = if std::env::var("WEBDAV_SERVER_URL").is_ok() {
|
||||
info!("Using real WebDAV server from WEBDAV_SERVER_URL environment variable");
|
||||
get_stress_test_config(None)?
|
||||
} else {
|
||||
info!("No WEBDAV_SERVER_URL set, starting mock WebDAV server for local testing");
|
||||
let server = MockWebDAVServer::start().await
|
||||
.map_err(|e| anyhow!("Failed to start mock server: {}", e))?;
|
||||
let url = server.url();
|
||||
mock_server = Some(server);
|
||||
get_stress_test_config(Some(url))?
|
||||
};
|
||||
|
||||
let config = get_stress_test_config(Some(mock_server.url()))?;
|
||||
info!("WebDAV server URL: {}", config.webdav_server_url);
|
||||
let webdav_service = create_stress_test_webdav_service(&config)?;
|
||||
|
||||
let concurrent_operations = config.stress_level.concurrent_operations();
|
||||
@@ -1266,8 +1430,8 @@ async fn test_concurrent_webdav_access() -> Result<()> {
|
||||
let test_paths = vec![
|
||||
"/",
|
||||
"/main-structure",
|
||||
"/docs-structure",
|
||||
"/images-structure",
|
||||
"/loop-traps",
|
||||
"/test-repo-1",
|
||||
"/large-directory",
|
||||
"/unicode-test",
|
||||
];
|
||||
@@ -1354,8 +1518,10 @@ async fn test_concurrent_webdav_access() -> Result<()> {
|
||||
info!("Total operations: {} ({}% success rate)", total_operations, success_rate);
|
||||
info!("Operations per second: {:.2}", total_operations as f64 / total_time.as_secs_f64());
|
||||
|
||||
// Clean up mock server
|
||||
mock_server.stop().await;
|
||||
// Clean up mock server if we started one
|
||||
if let Some(mut server) = mock_server {
|
||||
server.stop().await;
|
||||
}
|
||||
|
||||
// Test passes if success rate is reasonable (>= 80%)
|
||||
if success_rate >= 80.0 {
|
||||
@@ -1374,11 +1540,22 @@ async fn test_edge_case_handling() -> Result<()> {
|
||||
|
||||
info!("Starting edge case handling stress test");
|
||||
|
||||
// Start mock WebDAV server
|
||||
let mut mock_server = MockWebDAVServer::start().await
|
||||
.map_err(|e| anyhow!("Failed to start mock server: {}", e))?;
|
||||
// Use real WebDAV server if WEBDAV_SERVER_URL is set (e.g., in CI with Dufs),
|
||||
// otherwise fall back to mock server for local testing
|
||||
let mut mock_server: Option<MockWebDAVServer> = None;
|
||||
let config = if std::env::var("WEBDAV_SERVER_URL").is_ok() {
|
||||
info!("Using real WebDAV server from WEBDAV_SERVER_URL environment variable");
|
||||
get_stress_test_config(None)?
|
||||
} else {
|
||||
info!("No WEBDAV_SERVER_URL set, starting mock WebDAV server for local testing");
|
||||
let server = MockWebDAVServer::start().await
|
||||
.map_err(|e| anyhow!("Failed to start mock server: {}", e))?;
|
||||
let url = server.url();
|
||||
mock_server = Some(server);
|
||||
get_stress_test_config(Some(url))?
|
||||
};
|
||||
|
||||
let config = get_stress_test_config(Some(mock_server.url()))?;
|
||||
info!("WebDAV server URL: {}", config.webdav_server_url);
|
||||
let webdav_service = create_stress_test_webdav_service(&config)?;
|
||||
|
||||
// Test various edge cases that might cause infinite loops or crashes
|
||||
@@ -1452,8 +1629,10 @@ async fn test_edge_case_handling() -> Result<()> {
|
||||
info!(" - Expected failures: {}", expected_failures);
|
||||
info!(" - Timeouts: {}", timeouts);
|
||||
|
||||
// Clean up mock server
|
||||
mock_server.stop().await;
|
||||
// Clean up mock server if we started one
|
||||
if let Some(mut server) = mock_server {
|
||||
server.stop().await;
|
||||
}
|
||||
|
||||
// Test passes if no timeouts occurred (timeouts suggest infinite loops)
|
||||
if timeouts == 0 {
|
||||
|
||||
Reference in New Issue
Block a user