i5-add-tests (part 4) #10
11 changed files with 254 additions and 43 deletions
23
Cargo.lock
generated
23
Cargo.lock
generated
|
@ -325,6 +325,12 @@ dependencies = [
|
||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "diff"
|
||||||
|
version = "0.1.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "diligent-date-parser"
|
name = "diligent-date-parser"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
|
@ -1016,6 +1022,7 @@ dependencies = [
|
||||||
"atom_syndication",
|
"atom_syndication",
|
||||||
"bytes",
|
"bytes",
|
||||||
"clap",
|
"clap",
|
||||||
|
"pretty_assertions",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"scraper",
|
"scraper",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
@ -1033,6 +1040,16 @@ version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
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]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.66"
|
version = "1.0.66"
|
||||||
|
@ -1726,3 +1743,9 @@ checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yansi"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
|
||||||
|
|
|
@ -14,3 +14,4 @@ bytes = "1.4.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "*"
|
tempfile = "*"
|
||||||
|
pretty_assertions = "*"
|
||||||
|
|
|
@ -37,6 +37,13 @@ impl From<std::io::Error> for Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl From<std::sync::mpsc::SendError<String>> for Error {
|
||||||
|
fn from(value: std::sync::mpsc::SendError<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
details: value.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
impl From<atom_syndication::Error> for Error {
|
impl From<atom_syndication::Error> for Error {
|
||||||
fn from(value: atom_syndication::Error) -> Self {
|
fn from(value: atom_syndication::Error) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
|
@ -23,16 +23,23 @@ pub fn find(site: &str, channel_name: &str, e: &NetworkEnv) -> Result<String> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
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::*;
|
use super::*;
|
||||||
#[test]
|
#[test]
|
||||||
fn finds_rss_url() -> Result<()> {
|
fn finds_rss_url() -> Result<()> {
|
||||||
//given
|
//given
|
||||||
let network_env = NetworkEnv {
|
let network_env = NetworkEnv {
|
||||||
fetch_as_text: dummy_fetch_as_text,
|
fetch_as_text: mock_fetch_as_text_with_rss_url(HashMap::from([(
|
||||||
fetch_as_bytes: dummy_fetch_as_bytes,
|
"site@channel",
|
||||||
download_as_mp3: dummy_download_as_mp3,
|
"the-rss-url",
|
||||||
|
)])),
|
||||||
|
fetch_as_bytes: stub_network_fetch_as_bytes(),
|
||||||
|
download_as_mp3: stub_network_download_as_mp3(),
|
||||||
};
|
};
|
||||||
//when
|
//when
|
||||||
let result = find("site", "@channel", &network_env)?;
|
let result = find("site", "@channel", &network_env)?;
|
||||||
|
@ -45,9 +52,9 @@ mod tests {
|
||||||
fn error_if_channel_name_is_invalid() -> Result<()> {
|
fn error_if_channel_name_is_invalid() -> Result<()> {
|
||||||
//given
|
//given
|
||||||
let network_env = NetworkEnv {
|
let network_env = NetworkEnv {
|
||||||
fetch_as_text: dummy_fetch_as_text,
|
fetch_as_text: mock_fetch_as_text_with_rss_url(HashMap::from([])),
|
||||||
fetch_as_bytes: dummy_fetch_as_bytes,
|
fetch_as_bytes: stub_network_fetch_as_bytes(),
|
||||||
download_as_mp3: dummy_download_as_mp3,
|
download_as_mp3: stub_network_download_as_mp3(),
|
||||||
};
|
};
|
||||||
//when
|
//when
|
||||||
let result = find("site", "invalid-channel-name", &network_env);
|
let result = find("site", "invalid-channel-name", &network_env);
|
||||||
|
@ -55,19 +62,4 @@ mod tests {
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dummy_fetch_as_text(_url: &str) -> Result<String> {
|
|
||||||
Ok(r#"
|
|
||||||
<html>
|
|
||||||
<link title="RSS" href="the-rss-url">
|
|
||||||
</html>
|
|
||||||
"#
|
|
||||||
.to_string())
|
|
||||||
}
|
|
||||||
fn dummy_fetch_as_bytes(_url: &str) -> Result<bytes::Bytes> {
|
|
||||||
Err(Error::message("Not implemented"))
|
|
||||||
}
|
|
||||||
fn dummy_download_as_mp3(_url: &str) -> Result<()> {
|
|
||||||
Err(Error::message("Not implemented"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,21 @@
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
use std::fs::{File, OpenOptions};
|
use std::fs::{File, OpenOptions};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
|
pub type FileOpenFn = Box<dyn Fn(&str) -> Result<File>>;
|
||||||
|
|
||||||
|
pub type FileAppendLineFn = Box<dyn Fn(&str, &str) -> Result<()>>;
|
||||||
|
|
||||||
pub struct FileEnv {
|
pub struct FileEnv {
|
||||||
pub open: FileOpen,
|
pub open: FileOpenFn,
|
||||||
pub append_line: FileAppendLine,
|
pub append_line: FileAppendLineFn,
|
||||||
}
|
}
|
||||||
impl Default for FileEnv {
|
impl Default for FileEnv {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
open: |path| File::open(path),
|
open: Box::new(|path| Ok(File::open(path)?)),
|
||||||
append_line: |file_name, line| {
|
append_line: Box::new(|file_name, line| {
|
||||||
let mut file = OpenOptions::new()
|
let mut file = OpenOptions::new()
|
||||||
.write(true)
|
.write(true)
|
||||||
.append(true)
|
.append(true)
|
||||||
|
@ -18,9 +24,7 @@ impl Default for FileEnv {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
writeln!(file, "{}", line)?;
|
writeln!(file, "{}", line)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub type FileOpen = fn(path: &str) -> std::io::Result<File>;
|
|
||||||
pub type FileAppendLine = fn(paht: &str, line: &str) -> std::io::Result<()>;
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
mod env;
|
mod env;
|
||||||
pub mod read;
|
pub mod read;
|
||||||
|
|
||||||
|
pub use env::FileAppendLineFn;
|
||||||
pub use env::FileEnv;
|
pub use env::FileEnv;
|
||||||
|
pub use env::FileOpenFn;
|
||||||
|
|
|
@ -5,12 +5,15 @@ use std::io::{BufRead, BufReader};
|
||||||
use super::Link;
|
use super::Link;
|
||||||
|
|
||||||
pub fn find(link: &Link, file_name: &str, e: &FileEnv) -> Result<bool> {
|
pub fn find(link: &Link, file_name: &str, e: &FileEnv) -> Result<bool> {
|
||||||
if let Ok(file) = (e.open)(file_name) {
|
println!("Opening file: {file_name}");
|
||||||
let reader = BufReader::new(file);
|
let file = (e.open)(file_name)?;
|
||||||
for line in reader.lines() {
|
println!("Opened file: {file_name}");
|
||||||
if line? == link.href {
|
let reader = BufReader::new(file);
|
||||||
return Ok(true); // is already downloaded
|
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
|
Ok(false) // is not already downloaded
|
||||||
|
|
84
src/lib.rs
84
src/lib.rs
|
@ -34,3 +34,87 @@ pub fn run(subscriptions: &str, history: &str, site: &str, e: Env) -> Result<()>
|
||||||
}
|
}
|
||||||
Ok(())
|
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::<String>(); // 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<String> = 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,17 +2,23 @@ use std::process::Command;
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
pub type NetworkFetchAsTextFn = Box<dyn Fn(&str) -> Result<String>>;
|
||||||
|
|
||||||
|
pub type NetworkFetchAsBytesFn = Box<dyn Fn(&str) -> Result<bytes::Bytes>>;
|
||||||
|
|
||||||
|
pub type NetworkDownloadAsMp3Fn = Box<dyn Fn(&str) -> Result<()>>;
|
||||||
|
|
||||||
pub struct NetworkEnv {
|
pub struct NetworkEnv {
|
||||||
pub fetch_as_text: fn(url: &str) -> Result<String>,
|
pub fetch_as_text: NetworkFetchAsTextFn,
|
||||||
pub fetch_as_bytes: fn(url: &str) -> Result<bytes::Bytes>,
|
pub fetch_as_bytes: NetworkFetchAsBytesFn,
|
||||||
pub download_as_mp3: fn(url: &str) -> Result<()>,
|
pub download_as_mp3: NetworkDownloadAsMp3Fn,
|
||||||
}
|
}
|
||||||
impl Default for NetworkEnv {
|
impl Default for NetworkEnv {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
fetch_as_text: |url| Ok(reqwest::blocking::get(url)?.text()?),
|
fetch_as_text: Box::new(|url| Ok(reqwest::blocking::get(url)?.text()?)),
|
||||||
fetch_as_bytes: |url| Ok(reqwest::blocking::get(url)?.bytes()?),
|
fetch_as_bytes: Box::new(|url| Ok(reqwest::blocking::get(url)?.bytes()?)),
|
||||||
download_as_mp3: |url| {
|
download_as_mp3: Box::new(|url| {
|
||||||
let cmd = "yt-dlp";
|
let cmd = "yt-dlp";
|
||||||
// println!("{} --extract-audio --audio-format mp3 {}", cmd, &link.href);
|
// println!("{} --extract-audio --audio-format mp3 {}", cmd, &link.href);
|
||||||
let output = Command::new(cmd)
|
let output = Command::new(cmd)
|
||||||
|
@ -26,7 +32,7 @@ impl Default for NetworkEnv {
|
||||||
println!("{}", String::from_utf8(output.stdout)?);
|
println!("{}", String::from_utf8(output.stdout)?);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
mod env;
|
mod env;
|
||||||
|
|
||||||
|
pub use env::NetworkDownloadAsMp3Fn;
|
||||||
pub use env::NetworkEnv;
|
pub use env::NetworkEnv;
|
||||||
|
pub use env::NetworkFetchAsBytesFn;
|
||||||
|
pub use env::NetworkFetchAsTextFn;
|
||||||
|
|
|
@ -1,12 +1,19 @@
|
||||||
use std::{
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
fs::{read_to_string, File},
|
fs::{read_to_string, File},
|
||||||
io::Write,
|
io::Write,
|
||||||
str::from_utf8,
|
str::from_utf8,
|
||||||
|
sync::mpsc::Sender,
|
||||||
};
|
};
|
||||||
|
|
||||||
use tempfile::{tempdir, TempDir};
|
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)> {
|
pub fn create_text_file(name: &str, data: &[u8]) -> Result<(TempDir, String)> {
|
||||||
let data = from_utf8(data)?;
|
let data = from_utf8(data)?;
|
||||||
|
@ -23,3 +30,82 @@ pub fn read_text_file(file_name: &str) -> Result<Vec<String>> {
|
||||||
.map(String::from)
|
.map(String::from)
|
||||||
.collect())
|
.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#"
|
||||||
|
<html>
|
||||||
|
<link title="RSS" href="{}">
|
||||||
|
</html>
|
||||||
|
"#,
|
||||||
|
url
|
||||||
|
)),
|
||||||
|
None => Err(Error::message(
|
||||||
|
format!("Unexpected request for {}", url).as_str(),
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn mock_network_fetch_as_bytes_with_rss_entries(
|
||||||
|
feeds: HashMap<String, String>,
|
||||||
|
) -> 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<String>) -> 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<String>) -> 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(),
|
||||||
|
// ))
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
Loading…
Add table
Reference in a new issue