| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690 |
- //! 应用全局日志模块
- //! 日志文件放在 exe 同目录下,每天一个日志文件
- //! 格式:[时间戳] [LEVEL] 消息内容
- use chrono::Local;
- use std::fs::{self, OpenOptions};
- use std::io::Write;
- use std::path::PathBuf;
- use std::sync::Mutex;
- #[cfg(target_os = "windows")]
- use std::sync::atomic::{AtomicBool, Ordering};
- /// 全局日志写入锁
- static LOG_FILE: Mutex<Option<PathBuf>> = Mutex::new(None);
- /// VEH 是否已注册(防止重复注册)
- #[cfg(target_os = "windows")]
- static VEH_REGISTERED: AtomicBool = AtomicBool::new(false);
- /// 进程启动时间(用于计算运行时间)
- static PROCESS_START_TIME: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
- /// 获取 exe 所在目录
- fn get_exe_dir() -> PathBuf {
- if let Ok(exe_path) = std::env::current_exe() {
- if let Some(parent) = exe_path.parent() {
- return parent.to_path_buf();
- }
- }
- std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
- }
- /// 获取日志目录
- fn get_log_dir() -> PathBuf {
- get_exe_dir().join("logs")
- }
- /// 获取今天的日志文件路径
- fn get_today_log_path() -> Result<PathBuf, String> {
- let log_dir = get_log_dir();
- fs::create_dir_all(&log_dir)
- .map_err(|e| format!("创建日志目录失败: {}", e))?;
- let today = Local::now().format("%Y-%m-%d").to_string();
- Ok(log_dir.join(format!("app_{}.log", today)))
- }
- /// 初始化日志系统
- pub fn init_app_logger() -> Result<(), String> {
- let log_path = get_today_log_path()?;
- if !log_path.exists() {
- fs::write(&log_path, "").map_err(|e| format!("创建日志文件失败: {}", e))?;
- }
- let mut guard = LOG_FILE.lock().map_err(|e| format!("获取日志锁失败: {}", e))?;
- *guard = Some(log_path.clone());
- let start_msg = format!(
- "[{}] [INFO] ==================== 应用启动 ====================\n",
- Local::now().format("%Y-%m-%d %H:%M:%S")
- );
- write_to_log_file(&log_path, &start_msg);
- Ok(())
- }
- /// 获取当前日志文件路径
- pub fn get_current_log_path() -> Option<String> {
- LOG_FILE
- .lock()
- .ok()
- .and_then(|guard| guard.clone())
- .map(|p| p.to_string_lossy().to_string())
- }
- /// 写入日志到文件(不带时间戳前缀,供内部/panic 使用)
- fn write_to_log_file(path: &PathBuf, content: &str) {
- if let Ok(mut file) = OpenOptions::new()
- .create(true)
- .append(true)
- .open(path)
- {
- let _ = file.write_all(content.as_bytes());
- let _ = file.flush();
- }
- }
- /// 写入带时间戳的日志行
- fn write_log_line(level: &str, message: &str) {
- let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S%.3f").to_string();
- let log_line = format!("[{}] [{}] {}\n", timestamp, level, message);
- if let Ok(guard) = LOG_FILE.lock() {
- if let Some(path) = guard.as_ref() {
- write_to_log_file(path, &log_line);
- }
- }
- eprintln!("{}", log_line.trim());
- }
- /// Info 级别日志
- pub fn info(msg: &str) {
- write_log_line("INFO", msg);
- }
- /// Warning 级别日志
- pub fn warn(msg: &str) {
- write_log_line("WARN", msg);
- }
- /// Error 级别日志
- pub fn error(msg: &str) {
- write_log_line("ERROR", msg);
- }
- /// Debug 级别日志
- pub fn debug(msg: &str) {
- write_log_line("DEBUG", msg);
- }
- /// 记录 panic 信息
- pub fn log_panic(panic_info: &std::panic::PanicHookInfo) {
- let location = panic_info
- .location()
- .map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column()))
- .unwrap_or_else(|| "unknown location".to_string());
- let message = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
- s.to_string()
- } else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
- s.clone()
- } else {
- "Unknown panic payload".to_string()
- };
- let full_msg = format!(
- "PANIC at {}: {}\nBacktrace:\n{:?}",
- location,
- message,
- std::backtrace::Backtrace::capture()
- );
- write_log_line("PANIC", &full_msg);
- }
- /// 设置 panic hook
- pub fn setup_panic_hook() {
- std::panic::set_hook(Box::new(|panic_info| {
- log_panic(&panic_info);
- }));
- }
- // ============================================================
- // Windows 顶层异常处理(Vectored Exception Handler)
- // 能捕获 Rust panic 无法覆盖的崩溃,如访问违规、栈溢出等
- // ============================================================
- #[cfg(target_os = "windows")]
- const EXCEPTION_ACCESS_VIOLATION: u32 = 0xC0000005;
- #[cfg(target_os = "windows")]
- const EXCEPTION_STACK_OVERFLOW: u32 = 0xC00000FD;
- #[cfg(target_os = "windows")]
- const EXCEPTION_INT_DIVIDE_BY_ZERO: u32 = 0xC0000094;
- #[cfg(target_os = "windows")]
- const EXCEPTION_ILLEGAL_INSTRUCTION: u32 = 0xC000001D;
- #[cfg(target_os = "windows")]
- const EXCEPTION_BREAKPOINT: u32 = 0x80000003;
- #[cfg(target_os = "windows")]
- fn exception_code_to_name(code: u32) -> &'static str {
- match code {
- 0xC0000005 => "ACCESS_VIOLATION",
- 0xC00000FD => "STACK_OVERFLOW",
- 0xC0000094 => "INT_DIVIDE_BY_ZERO",
- 0xC000001D => "ILLEGAL_INSTRUCTION",
- 0x80000003 => "BREAKPOINT",
- 0xC0000008 => "INVALID_HANDLE",
- 0xC000001E => "LOCK_VIOLATION",
- 0xC0000025 => "NOT_MAPPED_VIEW",
- 0xC000000D => "DATA_TYPE_MISALIGNMENT",
- 0xC0000409 => "STACK_BUFFER_OVERRUN",
- 0xE0000001 => "GUARD_PAGE_VIOLATION",
- 0xC0000374 => "HEAP_CORRUPTION",
- 0xC0000142 => "DLL_NOT_FOUND",
- 0xC0000135 => "ENTRYPOINT_NOT_FOUND",
- _ => "UNKNOWN",
- }
- }
- #[cfg(target_os = "windows")]
- fn format_windows_exception_record(exception_pointers: *mut std::ffi::c_void) -> String {
- use std::ffi::c_void;
- // LPEXCEPTION_POINTERS 结构:
- // typedef struct _EXCEPTION_POINTERS {
- // PEXCEPTION_RECORD ExceptionRecord; // 偏移 0
- // PCONTEXT ContextRecord; // 偏移 8 (64位)
- // } EXCEPTION_POINTERS;
- let exc_record_ptr = unsafe { *(exception_pointers as *const *const c_void) };
- if exc_record_ptr.is_null() {
- return "ExceptionRecord is null".to_string();
- }
- // EXCEPTION_RECORD 结构 (前几个字段):
- // DWORD ExceptionCode; // 偏移 0, 4字节
- // DWORD ExceptionFlags; // 偏移 4, 4字节
- // struct _EXCEPTION_RECORD *ExceptionRecord; // 偏移 8, 8字节 (64位)
- // PVOID ExceptionAddress; // 偏移 16, 8字节 (64位)
- // DWORD NumberParameters; // 偏移 24, 4字节
- // ULONG_PTR ExceptionInformation[15]; // 偏移 32
- let exc_code = unsafe { *(exc_record_ptr as *const u32) };
- let exc_flags = unsafe { *((exc_record_ptr as *const u32).offset(1)) };
- let exc_addr = unsafe { *((exc_record_ptr as *const *const c_void).offset(2)) };
- let mut info = format!(
- "ExceptionCode=0x{:08X} ({})\n ExceptionFlags=0x{:08X}\n ExceptionAddress={:?}",
- exc_code,
- exception_code_to_name(exc_code),
- exc_flags,
- exc_addr
- );
- if exc_code == EXCEPTION_ACCESS_VIOLATION {
- // ExceptionInformation[0]: 0 = read, 1 = write
- // ExceptionInformation[1]: 访问的虚拟地址
- let read_write = unsafe { *((exc_record_ptr as *const u64).offset(4)) };
- let virt_addr = unsafe { *((exc_record_ptr as *const *const c_void).offset(5)) };
- info.push_str(&format!(
- "\n Attempted {} at address {:?}",
- if read_write == 0 { "read" } else { "write" },
- virt_addr
- ));
- }
- info
- }
- #[cfg(target_os = "windows")]
- extern "system" fn veh_exception_handler(
- exception_pointers: *mut std::ffi::c_void,
- ) -> i32 {
- use std::ffi::c_void;
- // LPEXCEPTION_POINTERS 结构
- let exc_record_ptr = unsafe { *(exception_pointers as *const *const c_void) };
- if exc_record_ptr.is_null() {
- // 记录基本信息
- let msg = format!(
- "==================== WINDOWS CRASH ====================\n\
- ExceptionRecord is null\n\
- ProcessInfo:\n\
- PID={}\n\
- ThreadId={:?}\n\
- ========================================================\n",
- std::process::id(),
- std::thread::current().id()
- );
- eprintln!("{}", msg);
- return 0; // EXCEPTION_CONTINUE_SEARCH
- }
- let exc_code = unsafe { *(exc_record_ptr as *const u32) };
- let exc_addr = unsafe { *((exc_record_ptr as *const *const c_void).offset(2)) };
- let record_str = format_windows_exception_record(exception_pointers);
- let backtrace = format!("{:?}", std::backtrace::Backtrace::capture());
- let msg = format!(
- "==================== WINDOWS CRASH ====================\n\
- ExceptionCode=0x{:08X} ({})\n\
- ExceptionAddress={:?}\n\
- RecordInfo:\n{}\n\
- Backtrace:\n{}\n\
- ProcessInfo:\n\
- PID={}\n\
- ThreadId={:?}\n\
- ========================================================\n",
- exc_code,
- exception_code_to_name(exc_code),
- exc_addr,
- record_str,
- backtrace,
- std::process::id(),
- std::thread::current().id()
- );
- if let Ok(guard) = LOG_FILE.lock() {
- if let Some(path) = guard.as_ref() {
- let _ = write_to_log_file(path, &msg);
- }
- }
- eprintln!("{}", msg);
- // 继续执行,让 Rust 的 panic hook 也有机会处理
- // EXCEPTION_CONTINUE_SEARCH = 0(交给其他处理器)
- // EXCEPTION_EXECUTE_HANDLER = 1(执行我们的处理然后终止)
- // 这里返回 CONTINUE_SEARCH,让其他处理器(包括 Rust)处理
- // 但如果是非常严重的错误,可以返回 EXECUTE_HANDLER
- if exc_code == EXCEPTION_STACK_OVERFLOW || exc_code == EXCEPTION_ACCESS_VIOLATION {
- // 栈溢出和访问违规非常危险,返回 EXECUTE_HANDLER
- // 让程序优雅退出,而不是继续执行导致不可预测行为
- 1 // EXCEPTION_EXECUTE_HANDLER
- } else {
- 0 // EXCEPTION_CONTINUE_SEARCH
- }
- }
- /// 注册 Windows 顶层异常处理器(Vectored Exception Handler)
- /// 这能捕获一些 Rust panic 无法覆盖的崩溃,如:
- /// - Access Violation(访问违规)
- /// - Stack Overflow(栈溢出)
- /// - 非法指令
- /// - 除零错误
- #[cfg(target_os = "windows")]
- pub fn setup_windows_exception_handler() {
- if VEH_REGISTERED.swap(true, Ordering::SeqCst) {
- return; // 已经注册过了
- }
- use std::ffi::c_void;
- // AddVectoredExceptionHandler 来自 winapi
- // RUSTSEC-2023-0071: 使用 winapi 而非 libc
- // 函数签名:unsafe extern "system" fn(LPEXCEPTION_POINTERS) -> LONG
- // 返回值:EXCEPTION_CONTINUE_SEARCH(0) 或 EXCEPTION_EXECUTE_HANDLER(1)
- #[link(name = "kernel32")]
- extern "system" {
- fn AddVectoredExceptionHandler(
- first_handler: u32, // 1 = CALL_FIRST, 0 = CALL_LAST
- handler: *mut std::ffi::c_void,
- ) -> *mut std::ffi::c_void;
- }
- unsafe {
- let result = AddVectoredExceptionHandler(1, veh_exception_handler as *mut c_void);
- if result.is_null() {
- if let Ok(guard) = LOG_FILE.lock() {
- if let Some(path) = guard.as_ref() {
- let msg = "[VEH] 注册 Windows 顶层异常处理器失败\n";
- let _ = write_to_log_file(path, msg);
- }
- }
- } else {
- if let Ok(guard) = LOG_FILE.lock() {
- if let Some(path) = guard.as_ref() {
- let msg = "[VEH] Windows 顶层异常处理器注册成功\n";
- let _ = write_to_log_file(path, msg);
- }
- }
- }
- }
- }
- /// 非 Windows 平台的空实现
- #[cfg(not(target_os = "windows"))]
- pub fn setup_windows_exception_handler() {
- // no-op on non-Windows
- }
- /// 设置所有崩溃处理机制
- pub fn setup_all_crash_handlers() {
- setup_panic_hook();
- setup_windows_exception_handler();
- }
- // ============================================================
- // 扫货相关日志
- // ============================================================
- /// 扫货任务开始日志
- pub fn log_sweep_start(sweep_request_id: i64, selection_request_id: i64, worker_count: u32) {
- info(&format!(
- "[SWEEP-START] sweep_request_id={} selection_request_id={} worker_count={}",
- sweep_request_id, selection_request_id, worker_count
- ));
- }
- /// 扫货任务结束日志
- pub fn log_sweep_end(sweep_request_id: i64, status: &str) {
- info(&format!(
- "[SWEEP-END] sweep_request_id={} status={}",
- sweep_request_id, status
- ));
- }
- /// 扫货轮次日志
- pub fn log_sweep_round(sweep_request_id: i64, round: u64, items_count: usize) {
- info(&format!(
- "[SWEEP-ROUND] sweep_request_id={} round={} items_count={}",
- sweep_request_id, round, items_count
- ));
- }
- /// API 调用开始日志
- pub fn log_api_call_start(api_name: &str, url: &str) {
- debug(&format!("[API-START] {} {}", api_name, url));
- }
- /// API 调用结束日志
- pub fn log_api_call_end(api_name: &str, status: u16, elapsed_ms: u64) {
- debug(&format!(
- "[API-END] {} status={} elapsed={}ms",
- api_name, status, elapsed_ms
- ));
- }
- /// API 调用错误日志
- pub fn log_api_error(api_name: &str, err_msg: &str) {
- write_log_line("ERROR", &format!("[API-ERROR] {}: {}", api_name, err_msg));
- }
- /// Steam 账号操作日志
- pub fn log_steam_account_op(account: &str, op: &str, result: &str) {
- info(&format!(
- "[STEAM-ACCOUNT] account={} op={} result={}",
- account, op, result
- ));
- }
- /// 购买成功日志
- pub fn log_purchase_success(
- goods_id: &str,
- goods_name: &str,
- listing_id: &str,
- price_usd: f64,
- account: &str,
- ) {
- info(&format!(
- "[PURCHASE-SUCCESS] goods_id={} goods_name={} listing_id={} price=${} account={}",
- goods_id, goods_name, listing_id, price_usd, account
- ));
- }
- /// 购买失败日志
- pub fn log_purchase_failure(
- goods_id: &str,
- goods_name: &str,
- listing_id: &str,
- reason: &str,
- ) {
- warn(&format!(
- "[PURCHASE-FAIL] goods_id={} goods_name={} listing_id={} reason={}",
- goods_id, goods_name, listing_id, reason
- ));
- }
- /// 代理切换日志
- pub fn log_proxy_change(proxy_endpoint: &str, reason: &str) {
- debug(&format!(
- "[PROXY-CHANGE] endpoint={} reason={}",
- proxy_endpoint, reason
- ));
- }
- /// 心跳日志
- pub fn log_heartbeat(status: &str) {
- debug(&format!("[HEARTBEAT] status={}", status));
- }
- /// 线程/任务开始
- pub fn log_task_start(task_name: &str) {
- debug(&format!("[TASK-START] {}", task_name));
- }
- /// 线程/任务结束
- pub fn log_task_end(task_name: &str) {
- debug(&format!("[TASK-END] {}", task_name));
- }
- // ============================================================
- // 内存和资源监控诊断日志
- // ============================================================
- /// 获取当前进程的内存使用情况(Windows)
- #[cfg(target_os = "windows")]
- pub fn get_memory_usage_mb() -> f64 {
- use std::mem::MaybeUninit;
-
- #[repr(C)]
- struct MEMORYSTATUSEX {
- dw_length: u32,
- dw_memory_load: u32,
- ull_total_phys: u64,
- ull_avail_phys: u64,
- ull_total_page_file: u64,
- ull_avail_page_file: u64,
- ull_total_virtual: u64,
- ull_avail_virtual: u64,
- ull_avail_extended_virtual: u64,
- }
- #[link(name = "kernel32")]
- extern "system" {
- fn GlobalMemoryStatusEx(lpBuffer: *mut MEMORYSTATUSEX) -> i32;
- }
- let mut mem_status = MaybeUninit::<MEMORYSTATUSEX>::uninit();
- let mem_status_ptr = mem_status.as_mut_ptr();
-
- // 初始化 dw_length 字段
- unsafe {
- (*mem_status_ptr).dw_length = std::mem::size_of::<MEMORYSTATUSEX>() as u32;
- }
- unsafe {
- if GlobalMemoryStatusEx(mem_status_ptr) != 0 {
- let status = mem_status.assume_init();
- // 返回已使用的物理内存(MB)
- ((status.ull_total_phys - status.ull_avail_phys) as f64) / (1024.0 * 1024.0)
- } else {
- 0.0
- }
- }
- }
- /// 获取当前进程的内存使用情况(非 Windows 平台)
- #[cfg(not(target_os = "windows"))]
- pub fn get_memory_usage_mb() -> f64 {
- 0.0
- }
- /// 获取当前进程的总内存(MB)
- #[cfg(target_os = "windows")]
- pub fn get_total_memory_mb() -> f64 {
- use std::mem::MaybeUninit;
-
- #[repr(C)]
- struct MEMORYSTATUSEX {
- dw_length: u32,
- dw_memory_load: u32,
- ull_total_phys: u64,
- ull_avail_phys: u64,
- ull_total_page_file: u64,
- ull_avail_page_file: u64,
- ull_total_virtual: u64,
- ull_avail_virtual: u64,
- ull_avail_extended_virtual: u64,
- }
- #[link(name = "kernel32")]
- extern "system" {
- fn GlobalMemoryStatusEx(lpBuffer: *mut MEMORYSTATUSEX) -> i32;
- }
- let mut mem_status = MaybeUninit::<MEMORYSTATUSEX>::uninit();
- let mem_status_ptr = mem_status.as_mut_ptr();
-
- unsafe {
- (*mem_status_ptr).dw_length = std::mem::size_of::<MEMORYSTATUSEX>() as u32;
- }
- unsafe {
- if GlobalMemoryStatusEx(mem_status_ptr) != 0 {
- let status = mem_status.assume_init();
- (status.ull_total_phys as f64) / (1024.0 * 1024.0)
- } else {
- 0.0
- }
- }
- }
- #[cfg(not(target_os = "windows"))]
- pub fn get_total_memory_mb() -> f64 {
- 0.0
- }
- /// 获取当前活跃的线程数量
- pub fn get_thread_count() -> usize {
- std::thread::available_parallelism()
- .map(|p| p.get())
- .unwrap_or(0)
- }
- /// 获取当前进程运行时间(秒)
- pub fn get_process_uptime_secs() -> u64 {
- let start = PROCESS_START_TIME.get_or_init(|| std::time::Instant::now());
- start.elapsed().as_secs()
- }
- /// 记录完整的系统资源诊断日志
- pub fn log_resource_diagnostics(tag: &str) {
- let mem_used = get_memory_usage_mb();
- let mem_total = get_total_memory_mb();
- let mem_percent = if mem_total > 0.0 {
- (mem_used / mem_total * 100.0).min(100.0)
- } else {
- 0.0
- };
- let thread_count = get_thread_count();
- let uptime = get_process_uptime_secs();
- let uptime_formatted = format_uptime(uptime);
-
- info(&format!(
- "[DIAGNOSTIC][{}] 内存: {:.1}/{:.0}MB ({:.1}%), 线程: {}, 运行时间: {}",
- tag, mem_used, mem_total, mem_percent, thread_count, uptime_formatted
- ));
- }
- /// 格式化运行时间
- fn format_uptime(secs: u64) -> String {
- let hours = secs / 3600;
- let minutes = (secs % 3600) / 60;
- let seconds = secs % 60;
- if hours > 0 {
- format!("{}h{}m{}s", hours, minutes, seconds)
- } else if minutes > 0 {
- format!("{}m{}s", minutes, seconds)
- } else {
- format!("{}s", seconds)
- }
- }
- /// 记录代理池状态诊断日志
- pub fn log_proxy_pool_status(tag: &str, pool_size: usize, in_use: usize) {
- info(&format!(
- "[DIAGNOSTIC][{}] 代理池: 可用={}, 使用中={}, 空闲={}",
- tag, pool_size, in_use, pool_size.saturating_sub(in_use)
- ));
- }
- /// 记录 Worker 状态诊断日志
- pub fn log_worker_status(tag: &str, worker_index: u32, active_tasks: usize) {
- info(&format!(
- "[DIAGNOSTIC][{}] Worker[{}]: 活跃任务={}",
- tag, worker_index, active_tasks
- ));
- }
- /// 记录扫货进度诊断日志(包含详细统计)
- pub fn log_sweep_progress_diagnostic(
- sweep_request_id: i64,
- round: u64,
- items_processed: u64,
- purchases_success: u64,
- purchases_failed: u64,
- proxy_pool_available: usize,
- proxy_pool_in_use: usize,
- ) {
- let mem_used = get_memory_usage_mb();
- let thread_count = get_thread_count();
-
- info(&format!(
- "[DIAGNOSTIC][PROGRESS] sweep={} round={} processed={} success={} failed={} mem={:.0}MB threads={} proxies=可用{}/使用中{}",
- sweep_request_id,
- round,
- items_processed,
- purchases_success,
- purchases_failed,
- mem_used,
- thread_count,
- proxy_pool_available,
- proxy_pool_in_use
- ));
- }
- #[cfg(test)]
- mod tests {
- use super::*;
- #[test]
- fn test_log_dir_creation() {
- let log_dir = get_log_dir();
- assert!(log_dir.ends_with("logs"));
- }
-
- #[test]
- fn test_memory_functions() {
- let mem_used = get_memory_usage_mb();
- let mem_total = get_total_memory_mb();
- assert!(mem_used >= 0.0);
- assert!(mem_total >= 0.0);
- assert!(mem_used <= mem_total);
- }
-
- #[test]
- fn test_uptime_format() {
- assert_eq!(format_uptime(30), "30s");
- assert_eq!(format_uptime(90), "1m30s");
- assert_eq!(format_uptime(3660), "1h1m0s");
- }
- }
|