From 4e2ce4b89e5e8cb68d49b251600b9da22d7e02dd Mon Sep 17 00:00:00 2001 From: Paul Campbell Date: Sat, 5 Aug 2023 06:50:00 +0100 Subject: [PATCH] i5-add-tests (part 4) (#10) Closes kemitix/podal#5 Reviewed-on: https://git.kemitix.net/kemitix/podal/pulls/10 Co-authored-by: Paul Campbell Co-committed-by: Paul Campbell --- Cargo.lock | 23 ++++++++++++ Cargo.toml | 1 + src/errors.rs | 7 ++++ src/feed/find.rs | 36 ++++++++----------- src/file/env.rs | 18 ++++++---- src/file/mod.rs | 2 ++ src/history/find.rs | 15 ++++---- src/lib.rs | 84 +++++++++++++++++++++++++++++++++++++++++++ src/network/env.rs | 20 +++++++---- src/network/mod.rs | 3 ++ src/test_utils.rs | 88 ++++++++++++++++++++++++++++++++++++++++++++- 11 files changed, 254 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aba38b1..04cc534 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -325,6 +325,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "diligent-date-parser" version = "0.1.4" @@ -1016,6 +1022,7 @@ dependencies = [ "atom_syndication", "bytes", "clap", + "pretty_assertions", "reqwest", "scraper", "tempfile", @@ -1033,6 +1040,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "proc-macro2" version = "1.0.66" @@ -1726,3 +1743,9 @@ checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" dependencies = [ "winapi", ] + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" diff --git a/Cargo.toml b/Cargo.toml index a01c713..3c17ea9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,3 +14,4 @@ bytes = "1.4.0" [dev-dependencies] tempfile = "*" +pretty_assertions = "*" diff --git a/src/errors.rs b/src/errors.rs index 70fed6e..d8f4236 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -37,6 +37,13 @@ impl From for Error { } } } +impl From> for Error { + fn from(value: std::sync::mpsc::SendError) -> Self { + Self { + details: value.to_string(), + } + } +} impl From for Error { fn from(value: atom_syndication::Error) -> Self { Self { diff --git a/src/feed/find.rs b/src/feed/find.rs index 27cef15..154c596 100644 --- a/src/feed/find.rs +++ b/src/feed/find.rs @@ -23,16 +23,23 @@ pub fn find(site: &str, channel_name: &str, e: &NetworkEnv) -> Result { #[cfg(test)] mod tests { - use crate::errors::Error; + use std::collections::HashMap; + + use crate::test_utils::{ + mock_fetch_as_text_with_rss_url, stub_network_download_as_mp3, stub_network_fetch_as_bytes, + }; use super::*; #[test] fn finds_rss_url() -> Result<()> { //given let network_env = NetworkEnv { - fetch_as_text: dummy_fetch_as_text, - fetch_as_bytes: dummy_fetch_as_bytes, - download_as_mp3: dummy_download_as_mp3, + fetch_as_text: mock_fetch_as_text_with_rss_url(HashMap::from([( + "site@channel", + "the-rss-url", + )])), + fetch_as_bytes: stub_network_fetch_as_bytes(), + download_as_mp3: stub_network_download_as_mp3(), }; //when let result = find("site", "@channel", &network_env)?; @@ -45,9 +52,9 @@ mod tests { fn error_if_channel_name_is_invalid() -> Result<()> { //given let network_env = NetworkEnv { - fetch_as_text: dummy_fetch_as_text, - fetch_as_bytes: dummy_fetch_as_bytes, - download_as_mp3: dummy_download_as_mp3, + fetch_as_text: mock_fetch_as_text_with_rss_url(HashMap::from([])), + fetch_as_bytes: stub_network_fetch_as_bytes(), + download_as_mp3: stub_network_download_as_mp3(), }; //when let result = find("site", "invalid-channel-name", &network_env); @@ -55,19 +62,4 @@ mod tests { assert!(result.is_err()); Ok(()) } - - fn dummy_fetch_as_text(_url: &str) -> Result { - Ok(r#" - - - - "# - .to_string()) - } - fn dummy_fetch_as_bytes(_url: &str) -> Result { - Err(Error::message("Not implemented")) - } - fn dummy_download_as_mp3(_url: &str) -> Result<()> { - Err(Error::message("Not implemented")) - } } diff --git a/src/file/env.rs b/src/file/env.rs index 005ced2..2cdc30d 100644 --- a/src/file/env.rs +++ b/src/file/env.rs @@ -1,15 +1,21 @@ +use crate::prelude::*; + use std::fs::{File, OpenOptions}; use std::io::Write; +pub type FileOpenFn = Box Result>; + +pub type FileAppendLineFn = Box Result<()>>; + pub struct FileEnv { - pub open: FileOpen, - pub append_line: FileAppendLine, + pub open: FileOpenFn, + pub append_line: FileAppendLineFn, } impl Default for FileEnv { fn default() -> Self { Self { - open: |path| File::open(path), - append_line: |file_name, line| { + open: Box::new(|path| Ok(File::open(path)?)), + append_line: Box::new(|file_name, line| { let mut file = OpenOptions::new() .write(true) .append(true) @@ -18,9 +24,7 @@ impl Default for FileEnv { .unwrap(); writeln!(file, "{}", line)?; Ok(()) - }, + }), } } } -pub type FileOpen = fn(path: &str) -> std::io::Result; -pub type FileAppendLine = fn(paht: &str, line: &str) -> std::io::Result<()>; diff --git a/src/file/mod.rs b/src/file/mod.rs index 3a55a73..6b6c968 100644 --- a/src/file/mod.rs +++ b/src/file/mod.rs @@ -1,4 +1,6 @@ mod env; pub mod read; +pub use env::FileAppendLineFn; pub use env::FileEnv; +pub use env::FileOpenFn; diff --git a/src/history/find.rs b/src/history/find.rs index bb6cdc2..b413925 100644 --- a/src/history/find.rs +++ b/src/history/find.rs @@ -5,12 +5,15 @@ use std::io::{BufRead, BufReader}; use super::Link; pub fn find(link: &Link, file_name: &str, e: &FileEnv) -> Result { - if let Ok(file) = (e.open)(file_name) { - let reader = BufReader::new(file); - for line in reader.lines() { - if line? == link.href { - return Ok(true); // is already downloaded - } + println!("Opening file: {file_name}"); + let file = (e.open)(file_name)?; + println!("Opened file: {file_name}"); + let reader = BufReader::new(file); + for line in reader.lines() { + let line = line?; + if line == link.href { + println!("history: {}", line); + return Ok(true); // is already downloaded } } Ok(false) // is not already downloaded diff --git a/src/lib.rs b/src/lib.rs index b7f1a9d..d59f443 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,3 +34,87 @@ pub fn run(subscriptions: &str, history: &str, site: &str, e: Env) -> Result<()> } Ok(()) } + +#[cfg(test)] +mod tests { + use std::{collections::HashMap, sync::mpsc}; + + use atom_syndication::{Entry, EntryBuilder, Feed, FeedBuilder, LinkBuilder, Text}; + + use crate::test_utils::{ + create_text_file, mock_fetch_as_text_with_rss_url, mock_file_append_line, mock_file_open, + mock_network_download_as_mp3, mock_network_fetch_as_bytes_with_rss_entries, + }; + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn downloads_two_items_from_two_feeds_ignoring_existing_items_in_history() -> Result<()> { + //given + + let site = "http://example.com/"; + let (tx, rx) = mpsc::channel::(); // channel to recieve notice of downloaded urls + + // two channels in subscriptions.txt + let (subs_dir, subs_file_name) = + create_text_file("subs", "@channel1\nignore me\n@channel2".as_bytes())?; + // one item from each channel is already listed in the downloads.txt file + let (history_dir, history_file_name) = + create_text_file("history", "c1-f2\nc2-f3".as_bytes())?; + + let env = Env { + network: NetworkEnv { + fetch_as_text: mock_fetch_as_text_with_rss_url(HashMap::from([ + ("http://example.com/@channel1", "rss-feed-1"), + ("http://example.com/@channel2", "rss-feed-2"), + ])), + fetch_as_bytes: mock_network_fetch_as_bytes_with_rss_entries(HashMap::from([ + ( + "rss-feed-1".into(), + feed_with_three_links("c1-f1", "c1-f2", "c1-f3").to_string(), + ), + ( + "rss-feed-2".into(), + feed_with_three_links("c2-f1", "c2-f2", "c2-f3").to_string(), + ), + ])), + download_as_mp3: mock_network_download_as_mp3(tx), + }, + file: FileEnv { + open: mock_file_open(vec![subs_file_name.clone(), history_file_name.clone()]), + append_line: mock_file_append_line(), + }, + }; + + //when + run(&subs_file_name, &history_file_name, site, env)?; + //then + drop(subs_dir); + drop(history_dir); + + let mut downloads: Vec = vec![]; + for m in rx { + downloads.push(m); + } + + assert_eq!(downloads, vec!["c1-f1", "c1-f3", "c2-f1", "c2-f2"]); + Ok(()) + } + + fn entry_with_link(link: &str, title: &str) -> Entry { + EntryBuilder::default() + .links(vec![LinkBuilder::default().href(link.to_string()).build()]) + .title(Text::from(title)) + .build() + } + fn feed_with_three_links(l1: &str, l2: &str, l3: &str) -> Feed { + FeedBuilder::default() + .entries(vec![ + entry_with_link(l1, "l1"), + entry_with_link(l2, "l2"), + entry_with_link(l3, "l3"), + ]) + .build() + } +} diff --git a/src/network/env.rs b/src/network/env.rs index 7cbd626..24ec8cd 100644 --- a/src/network/env.rs +++ b/src/network/env.rs @@ -2,17 +2,23 @@ use std::process::Command; use crate::prelude::*; +pub type NetworkFetchAsTextFn = Box Result>; + +pub type NetworkFetchAsBytesFn = Box Result>; + +pub type NetworkDownloadAsMp3Fn = Box Result<()>>; + pub struct NetworkEnv { - pub fetch_as_text: fn(url: &str) -> Result, - pub fetch_as_bytes: fn(url: &str) -> Result, - pub download_as_mp3: fn(url: &str) -> Result<()>, + pub fetch_as_text: NetworkFetchAsTextFn, + pub fetch_as_bytes: NetworkFetchAsBytesFn, + pub download_as_mp3: NetworkDownloadAsMp3Fn, } impl Default for NetworkEnv { fn default() -> Self { Self { - fetch_as_text: |url| Ok(reqwest::blocking::get(url)?.text()?), - fetch_as_bytes: |url| Ok(reqwest::blocking::get(url)?.bytes()?), - download_as_mp3: |url| { + fetch_as_text: Box::new(|url| Ok(reqwest::blocking::get(url)?.text()?)), + fetch_as_bytes: Box::new(|url| Ok(reqwest::blocking::get(url)?.bytes()?)), + download_as_mp3: Box::new(|url| { let cmd = "yt-dlp"; // println!("{} --extract-audio --audio-format mp3 {}", cmd, &link.href); let output = Command::new(cmd) @@ -26,7 +32,7 @@ impl Default for NetworkEnv { println!("{}", String::from_utf8(output.stdout)?); } Ok(()) - }, + }), } } } diff --git a/src/network/mod.rs b/src/network/mod.rs index 3af1de6..2d626a3 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -1,3 +1,6 @@ mod env; +pub use env::NetworkDownloadAsMp3Fn; pub use env::NetworkEnv; +pub use env::NetworkFetchAsBytesFn; +pub use env::NetworkFetchAsTextFn; diff --git a/src/test_utils.rs b/src/test_utils.rs index 305ae44..5afc9d5 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -1,12 +1,19 @@ use std::{ + collections::HashMap, fs::{read_to_string, File}, io::Write, str::from_utf8, + sync::mpsc::Sender, }; use tempfile::{tempdir, TempDir}; -use crate::prelude::*; +use crate::{ + errors::Error, + file::{FileAppendLineFn, FileOpenFn}, + network::{NetworkDownloadAsMp3Fn, NetworkFetchAsBytesFn, NetworkFetchAsTextFn}, + prelude::*, +}; pub fn create_text_file(name: &str, data: &[u8]) -> Result<(TempDir, String)> { let data = from_utf8(data)?; @@ -23,3 +30,82 @@ pub fn read_text_file(file_name: &str) -> Result> { .map(String::from) .collect()) } +pub fn mock_fetch_as_text_with_rss_url( + map: HashMap<&'static str, &'static str>, +) -> NetworkFetchAsTextFn { + Box::new(move |url: &str| match map.get(url) { + Some(url) => Ok(format!( + r#" + + + + "#, + url + )), + None => Err(Error::message( + format!("Unexpected request for {}", url).as_str(), + )), + }) +} +pub fn mock_network_fetch_as_bytes_with_rss_entries( + feeds: HashMap, +) -> NetworkFetchAsBytesFn { + Box::new(move |url| { + if let Some(feed) = feeds.get(url).cloned() { + Ok(bytes::Bytes::from(feed)) + } else { + Err(Error::message(format!("No mock feed: {}", url).as_str())) + } + }) +} +pub fn mock_file_open(real_paths: Vec) -> FileOpenFn { + Box::new(move |path: &str| { + if real_paths.contains(&path.to_string()) { + Ok(File::open(path)?) + } else { + Err(Error::message( + format!("Not implemented: file_open: {}", path).as_str(), + )) + } + }) +} + +pub fn mock_network_download_as_mp3(tx: Sender) -> NetworkDownloadAsMp3Fn { + Box::new(move |url: &str| { + tx.send(url.into())?; + Ok(()) + }) +} + +pub fn mock_file_append_line() -> FileAppendLineFn { + Box::new(|_, _| Ok(())) +} + +pub fn stub_network_fetch_as_bytes() -> NetworkFetchAsBytesFn { + Box::new(|url: &str| { + Err(Error::message( + format!("Not implemented: network_fetch_as_bytes: {}", url).as_str(), + )) + }) +} +pub fn stub_network_download_as_mp3() -> NetworkDownloadAsMp3Fn { + Box::new(|url: &str| { + Err(Error::message( + format!("Not implemented: network_download_as_mp3: {}", url).as_str(), + )) + }) +} +// pub fn stub_file_open() -> FileOpenFn { +// Box::new(|path: &str| { +// Err(Error::message( +// format!("Not implemented: file_open: {}", path).as_str(), +// )) +// }) +// } +// pub fn stub_file_append_line() -> FileAppendLineFn { +// Box::new(|path: &str, line: &str| { +// Err(Error::message( +// format!("Not implemented: file_append_line: {} to {}", line, path).as_str(), +// )) +// }) +// }