feat: FTPS, AppError, Site Manager, tests, multi-window, auto-clear, CI/CD, virtual scroll
- FTPS: реализован FtpsClient через suppaftp + async-native-tls - AppError: кастомный enum с Serialize вместо Result<T, String> - Site Manager UI: создание/редактирование/удаление сайтов - Тесты: 53 тестов (domain, storage, transfer, fs, remote) - Multi-window: новое окно + сохранение геометрии - Auto-clear: воркер чистит завершённые задачи по таймеру - CI/CD: 3-ступенчатый пайплайн (lint→build→test) с кэшированием - Virtual scroll: fileTable с рендером только видимых строк - UI: исправлены close-кнопки, color-scheme для select, скриншоты в README
This commit is contained in:
parent
545dd4ed0b
commit
e188fbef36
52 changed files with 5188 additions and 523 deletions
|
|
@ -51,3 +51,115 @@ impl TransferQueue {
|
|||
q.retain(|t| t.id != id);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::domain::transfer::{TaskState, TransferKind, TransferTask};
|
||||
|
||||
fn make_task(id: &str, total: u64) -> TransferTask {
|
||||
TransferTask {
|
||||
id: id.into(),
|
||||
kind: TransferKind::Download,
|
||||
connection_id: "conn-1".into(),
|
||||
local_path: "/local".into(),
|
||||
remote_path: "/remote".into(),
|
||||
file_name: "file.txt".into(),
|
||||
total_bytes: total,
|
||||
transferred_bytes: 0,
|
||||
state: TaskState::Queued,
|
||||
speed: None,
|
||||
eta_secs: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push_pop_all() {
|
||||
let q = TransferQueue::default();
|
||||
assert!(q.all().is_empty());
|
||||
|
||||
q.push(make_task("t1", 100));
|
||||
q.push(make_task("t2", 200));
|
||||
assert_eq!(q.all().len(), 2);
|
||||
|
||||
let t1 = q.pop().unwrap();
|
||||
assert_eq!(t1.id, "t1");
|
||||
assert_eq!(q.all().len(), 1);
|
||||
|
||||
let t2 = q.pop().unwrap();
|
||||
assert_eq!(t2.id, "t2");
|
||||
assert!(q.pop().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get() {
|
||||
let q = TransferQueue::default();
|
||||
q.push(make_task("t1", 100));
|
||||
q.push(make_task("t2", 200));
|
||||
|
||||
assert_eq!(q.get("t2").unwrap().total_bytes, 200);
|
||||
assert!(q.get("nonexistent").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_state() {
|
||||
let q = TransferQueue::default();
|
||||
q.push(make_task("t1", 100));
|
||||
|
||||
for state in [
|
||||
TaskState::Running,
|
||||
TaskState::Paused,
|
||||
TaskState::Completed,
|
||||
TaskState::Failed("err".into()),
|
||||
TaskState::Cancelled,
|
||||
TaskState::Retrying(5),
|
||||
] {
|
||||
q.update_state("t1", state.clone());
|
||||
assert_eq!(q.get("t1").unwrap().state, state);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_progress() {
|
||||
let q = TransferQueue::default();
|
||||
q.push(make_task("t1", 1000));
|
||||
|
||||
q.update_progress("t1", 250, 50);
|
||||
let t = q.get("t1").unwrap();
|
||||
assert_eq!(t.transferred_bytes, 250);
|
||||
assert_eq!(t.speed, Some(50));
|
||||
assert_eq!(t.eta_secs, Some(15));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_progress_zero_speed() {
|
||||
let q = TransferQueue::default();
|
||||
q.push(make_task("t1", 1000));
|
||||
|
||||
q.update_progress("t1", 100, 0);
|
||||
let t = q.get("t1").unwrap();
|
||||
assert_eq!(t.transferred_bytes, 100);
|
||||
assert_eq!(t.speed, Some(0));
|
||||
assert_eq!(t.eta_secs, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove() {
|
||||
let q = TransferQueue::default();
|
||||
q.push(make_task("t1", 100));
|
||||
q.push(make_task("t2", 200));
|
||||
q.push(make_task("t3", 300));
|
||||
|
||||
q.remove("t2");
|
||||
let ids: Vec<String> = q.all().into_iter().map(|t| t.id).collect();
|
||||
assert_eq!(ids, vec!["t1", "t3"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_nonexistent() {
|
||||
let q = TransferQueue::default();
|
||||
q.push(make_task("t1", 100));
|
||||
q.remove("does-not-exist");
|
||||
assert_eq!(q.all().len(), 1);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue