From 50e2ab379c6e3b1f31e8d4d26437993e7d36dff6 Mon Sep 17 00:00:00 2001 From: Sudip Date: Sun, 15 Feb 2026 09:13:28 +0000 Subject: [PATCH 1/5] feat: add basic unit tests for some utility functions --- src/engine.rs | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/src/engine.rs b/src/engine.rs index d7738df..fc06803 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -377,3 +377,66 @@ impl Downloader { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use reqwest::header::HeaderMap; + use tokio::runtime::Runtime; + + #[test] + fn test_extract_filename_from_url() { + let url = "https://example.com/path/to/file.txt"; + assert_eq!(extract_filename_from_url(url), Some("file.txt".to_string())); + let url2 = "https://example.com/path/to/"; + assert_eq!(extract_filename_from_url(url2), None); + } + + #[test] + fn test_header_extract_filename() { + let mut headers = HeaderMap::new(); + headers.insert( + reqwest::header::CONTENT_DISPOSITION, + "attachment; filename=\"myfile.bin\"".parse().unwrap(), + ); + let name = headers.extract_filename().unwrap(); + assert_eq!(name, "myfile.bin"); + } + + #[test] + fn test_header_extract_file_size() { + let mut headers = HeaderMap::new(); + headers.insert( + reqwest::header::CONTENT_RANGE, + "bytes 0-0/12345".parse().unwrap(), + ); + let size = headers.extract_file_size().unwrap(); + assert_eq!(size, 12345u64); + } + + #[test] + fn test_downloader_new_and_defaults() { + let d = Downloader::new("https://example.com/file"); + assert_eq!(d.url, "https://example.com/file"); + assert!(d.filename.is_none()); + assert!(d.file_size.is_none()); + } + + // Placeholder async test for download-related behavior; does not perform network IO. + #[test] + fn test_download_placeholder() { + // Create a runtime to run async parts if needed. + let rt = Runtime::new().unwrap(); + rt.block_on(async { + let mut downloader = Downloader::new("https://example.com/file"); + // Set internal fields to avoid real network operations in this placeholder. + downloader.headers = HeaderMap::new(); + downloader.filename = Some("tmp_download.bin".to_string()); + downloader.file_size = Some(0); + + // Ensure setters/readers behave as expected in a minimal scenario. + assert_eq!(downloader.filename.as_deref(), Some("tmp_download.bin")); + assert_eq!(downloader.file_size, Some(0)); + }); + } +} From 9dc236f25ba2eb530f3520f6cbc3f52741d37f10 Mon Sep 17 00:00:00 2001 From: Sudip Date: Sun, 15 Feb 2026 09:18:44 +0000 Subject: [PATCH 2/5] refactor: update example documentation --- src/lib.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7ad298d..70a715f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,14 +13,16 @@ //! //! or check [tokio](https://crates.io/crates/tokio) package for more detailed implementation. //! -//! ``` +//! ```rust //! use furl_core::Downloader; //! -//! async fn my_function() { -//! let mut downloader = Downloader::new("https://example.com/files/file_1.txt".to_owned()); -//! downloader.download(&args.dest).await?; +//! #[tokio::main] +//! async fn main() -> Result<(), Box> { +//! let mut downloader = Downloader::new("https://example.com/files/file_1.txt"); +//! // download into the tmp directory using default thread count +//! downloader.download(".", None).await?; +//! Ok(()) //! } -//! //! ``` pub mod engine; From 113442d63e2a507a9055a5d97c9952969b7d4506 Mon Sep 17 00:00:00 2001 From: Sudip Date: Sun, 15 Feb 2026 09:40:35 +0000 Subject: [PATCH 3/5] feat: add test workflow on PR creation --- .github/workflows/ci.yml | 51 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8113026 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,51 @@ +name: CI + +on: + pull_request: + branches: [main, master] +env: + CARGO_TERM_COLOR: always +jobs: + test: + name: Run cargo checks and tests + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Cache cargo registry, git index and target + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Install Rust (stable) + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + override: true + + - name: Show rustc version + run: rustc --version + + - name: Install rustfmt and clippy + run: rustup component add rustfmt clippy + + - name: Check formatting + run: cargo fmt --all -- --check + + - name: Run clippy (deny warnings) + run: cargo clippy --workspace --all-targets --all-features -- -D warnings + + - name: Run cargo test + env: + RUST_BACKTRACE: 1 + run: | + cargo test --workspace --verbose From 81962b3f06dfc28a0ca690bd831ef67947f8cdcb Mon Sep 17 00:00:00 2001 From: Sudip Date: Sun, 15 Feb 2026 17:13:51 +0000 Subject: [PATCH 4/5] fix: remove clippy error --- src/engine.rs | 20 ++++++++++---------- src/main.rs | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index fc06803..0bcaece 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -81,7 +81,7 @@ impl HeaderUtils for HeaderMap { return Ok(filename.trim_matches('"').to_string()); } } - return Err(Box::from("Unable to extract filename".to_owned())); + Err(Box::from("Unable to extract filename".to_owned())) // TODO: guess filename from content type } @@ -94,7 +94,7 @@ impl HeaderUtils for HeaderMap { .get(CONTENT_RANGE) .ok_or_else(|| Box::::from("Content_range not found"))?; let content_range = - cr.to_str()?.split("/").into_iter().last().ok_or_else(|| { + cr.to_str()?.split("/").last().ok_or_else(|| { Box::::from("Invalid Content_range_format") })?; Ok(content_range.parse()?) @@ -106,14 +106,14 @@ impl HeaderUtils for HeaderMap { /// For example: if content disposition is not provided, but there is a valid /// filename in the request url pub fn extract_filename_from_url(url: &str) -> Option { - if let Ok(parsed_url) = Url::parse(&url) { - if let Some(segment) = parsed_url.path_segments().and_then(|s| s.last()) { - if !segment.is_empty() { - return Some(segment.to_string()); - } - } + if let Ok(parsed_url) = Url::parse(url) + && let Some(segment) = parsed_url.path_segments().and_then(|mut s| s.next_back()) + && !segment.is_empty() + { + return Some(segment.to_string()); } - return None; + + None } impl Downloader { @@ -360,7 +360,7 @@ impl Downloader { let file_clone = Arc::clone(&file); let bar = ProgressBar::new_spinner(); bar.enable_steady_tick(Duration::from_millis(100)); - println!(""); + println!("\n"); bar.set_style( ProgressStyle::with_template(&format!( "{{spinner:.cyan}} {:?} ({{binary_bytes}} downloaded)", diff --git a/src/main.rs b/src/main.rs index d431efa..7c71478 100644 --- a/src/main.rs +++ b/src/main.rs @@ -45,9 +45,9 @@ async fn main() { // TODO: add extensive url pattern matcher let re = Regex::new(r"https?://[^\s/$.?#].[^\s]*").unwrap(); - if let Some(_) = re.captures(&args.url) { + if re.captures(&args.url).is_some() { let mut downloader = Downloader::new(&args.url); - if let Ok(_) = downloader.download(&args.out, Some(threads)).await { + if downloader.download(&args.out, Some(threads)).await.is_ok() { println!("Download Complete!") } return; From 4f66823f16e9ead7e3135f6d94674ca4d75f2446 Mon Sep 17 00:00:00 2001 From: Sudip Date: Sun, 15 Feb 2026 17:17:31 +0000 Subject: [PATCH 5/5] refactor: update CI check conditions --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8113026..cda5f90 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,10 @@ name: CI on: + push: + branches: [main, dev] pull_request: - branches: [main, master] + branches: [main, dev] env: CARGO_TERM_COLOR: always jobs: