i5-add-tests (part 2) #7
16 changed files with 300 additions and 48 deletions
|
@ -1,14 +1,16 @@
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
pub fn find(site: &str, channel_name: &str) -> Result<String> {
|
use crate::fetch::FetchGet;
|
||||||
|
|
||||||
|
pub fn find(site: &str, channel_name: &str, e: &FetchGet) -> Result<String> {
|
||||||
if let Some(channel_prefix) = channel_name.chars().next() {
|
if let Some(channel_prefix) = channel_name.chars().next() {
|
||||||
if channel_prefix != '@' {
|
if channel_prefix != '@' {
|
||||||
return Err(format!("Channel Name must begin with an '@': {}", channel_name).into());
|
return Err(format!("Channel Name must begin with an '@': {}", channel_name).into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let channel_url = format!("{}{}", site, channel_name);
|
let channel_url = format!("{}{}", site, channel_name);
|
||||||
let response = reqwest::blocking::get(channel_url)?;
|
let response = (e)(&channel_url)?;
|
||||||
let rss_url = scraper::Html::parse_document(&response.text()?)
|
let rss_url = scraper::Html::parse_document(&response)
|
||||||
.select(&scraper::Selector::parse("link[title='RSS']").unwrap())
|
.select(&scraper::Selector::parse("link[title='RSS']").unwrap())
|
||||||
.next()
|
.next()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -18,3 +20,40 @@ pub fn find(site: &str, channel_name: &str) -> Result<String> {
|
||||||
.to_string();
|
.to_string();
|
||||||
Ok(rss_url)
|
Ok(rss_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::fetch::Response;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn finds_rss_url() -> Result<()> {
|
||||||
|
//given
|
||||||
|
let fetch_get = &(get as FetchGet);
|
||||||
|
//when
|
||||||
|
let result = find("site", "@channel", fetch_get)?;
|
||||||
|
//then
|
||||||
|
assert_eq!(result, "the-rss-url");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn error_if_channel_name_is_invalid() -> Result<()> {
|
||||||
|
//given
|
||||||
|
let fetch_get = &(get as FetchGet);
|
||||||
|
//when
|
||||||
|
let result = find("site", "invalid-channel-name", fetch_get);
|
||||||
|
//then
|
||||||
|
assert!(result.is_err());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get(_url: &str) -> Result<Response> {
|
||||||
|
Ok(r#"
|
||||||
|
<html>
|
||||||
|
<link title="RSS" href="the-rss-url">
|
||||||
|
</html>
|
||||||
|
"#
|
||||||
|
.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use atom_syndication::Feed;
|
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
pub fn get(url: &str) -> Result<Feed> {
|
use atom_syndication::Feed;
|
||||||
|
|
||||||
|
pub fn reqwest_blocking_get(url: &str) -> Result<Feed> {
|
||||||
let content = reqwest::blocking::get(url)?.bytes()?;
|
let content = reqwest::blocking::get(url)?.bytes()?;
|
||||||
let channel = Feed::read_from(&content[..])?;
|
let channel = Feed::read_from(&content[..])?;
|
||||||
Ok(channel)
|
Ok(channel)
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
use crate::fetch::FetchGet;
|
||||||
|
|
||||||
mod find;
|
mod find;
|
||||||
mod get;
|
mod get;
|
||||||
|
|
||||||
|
use atom_syndication::Feed;
|
||||||
pub use find::find;
|
pub use find::find;
|
||||||
pub use get::get;
|
pub use get::reqwest_blocking_get;
|
||||||
|
|
||||||
type Feed = atom_syndication::Feed;
|
pub struct FeedEnv {
|
||||||
|
pub find: FeedFind,
|
||||||
|
pub get: FeedGet,
|
||||||
|
}
|
||||||
|
|
||||||
pub type FeedFind = fn(&str, &str) -> Result<String>;
|
pub type FeedFind = fn(&str, &str, &FetchGet) -> Result<String>;
|
||||||
pub type FeedGet = fn(&str) -> Result<Feed>;
|
pub type FeedGet = fn(&str) -> Result<Feed>;
|
||||||
|
|
12
src/fetch.rs
12
src/fetch.rs
|
@ -3,7 +3,13 @@ use crate::prelude::*;
|
||||||
use atom_syndication::Link;
|
use atom_syndication::Link;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
pub struct FetchEnv {
|
||||||
|
pub download: FetchDownload,
|
||||||
|
pub get: FetchGet,
|
||||||
|
}
|
||||||
|
|
||||||
pub type FetchDownload = fn(&Link) -> Result<()>;
|
pub type FetchDownload = fn(&Link) -> Result<()>;
|
||||||
|
pub type FetchGet = fn(&str) -> Result<Response>;
|
||||||
|
|
||||||
pub fn download(link: &Link) -> Result<()> {
|
pub fn download(link: &Link) -> Result<()> {
|
||||||
let cmd = "yt-dlp";
|
let cmd = "yt-dlp";
|
||||||
|
@ -20,3 +26,9 @@ pub fn download(link: &Link) -> Result<()> {
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type Response = String;
|
||||||
|
|
||||||
|
pub fn get(url: &str) -> Result<Response> {
|
||||||
|
Ok(reqwest::blocking::get(url)?.text()?)
|
||||||
|
}
|
||||||
|
|
|
@ -15,3 +15,75 @@ pub fn add(link: &Link, file_name: &str) -> Result<()> {
|
||||||
writeln!(file, "{}", link.href)?;
|
writeln!(file, "{}", link.href)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
history::Link,
|
||||||
|
test_utils::{create_text_file, read_text_file},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn creates_file_if_missing() -> Result<()> {
|
||||||
|
//given
|
||||||
|
let (dir, file_name) =
|
||||||
|
create_text_file("download.txt", include_bytes!("../../test/data/empty.txt"))?;
|
||||||
|
std::fs::remove_file(&file_name)?;
|
||||||
|
|
||||||
|
let link = Link {
|
||||||
|
href: "foo".to_string(),
|
||||||
|
rel: "bar".to_string(),
|
||||||
|
hreflang: None,
|
||||||
|
mime_type: None,
|
||||||
|
title: None,
|
||||||
|
length: None,
|
||||||
|
};
|
||||||
|
//when
|
||||||
|
add(&link, &file_name)?;
|
||||||
|
|
||||||
|
//then
|
||||||
|
let content: Vec<String> = read_text_file(&file_name)?;
|
||||||
|
drop(dir);
|
||||||
|
|
||||||
|
let expected = vec!["foo".to_string()];
|
||||||
|
assert_eq!(content, expected);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn appends_to_exising_file() -> Result<()> {
|
||||||
|
// given
|
||||||
|
let (dir, file_name) = create_text_file(
|
||||||
|
"download.txt",
|
||||||
|
include_bytes!("../../test/data/downloads.txt"),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let link = Link {
|
||||||
|
href: "foo".to_string(),
|
||||||
|
rel: "bar".to_string(),
|
||||||
|
hreflang: None,
|
||||||
|
mime_type: None,
|
||||||
|
title: None,
|
||||||
|
length: None,
|
||||||
|
};
|
||||||
|
//when
|
||||||
|
add(&link, &file_name)?;
|
||||||
|
|
||||||
|
//then
|
||||||
|
let content: Vec<String> = read_text_file(&file_name)?;
|
||||||
|
drop(dir);
|
||||||
|
|
||||||
|
let expected = vec![
|
||||||
|
"line-1".to_string(),
|
||||||
|
"line-2".to_string(),
|
||||||
|
"foo".to_string(),
|
||||||
|
];
|
||||||
|
assert_eq!(content, expected);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -15,3 +15,86 @@ pub fn find(link: &Link, file_name: &str) -> Result<bool> {
|
||||||
}
|
}
|
||||||
Ok(false) // is not already downloaded
|
Ok(false) // is not already downloaded
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::{history::Link, test_utils::create_text_file};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn true_if_line_exists() -> Result<()> {
|
||||||
|
//given
|
||||||
|
let (dir, file_name) =
|
||||||
|
create_text_file("file", include_bytes!("../../test/data/with-llamma.txt"))?;
|
||||||
|
let link = Link {
|
||||||
|
href: "llamma".to_string(),
|
||||||
|
rel: "".to_string(),
|
||||||
|
hreflang: None,
|
||||||
|
mime_type: None,
|
||||||
|
title: None,
|
||||||
|
length: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
//when
|
||||||
|
let result = find(&link, &file_name)?;
|
||||||
|
|
||||||
|
//then
|
||||||
|
drop(dir);
|
||||||
|
|
||||||
|
assert_eq!(result, true);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn false_if_line_absent() -> Result<()> {
|
||||||
|
//given
|
||||||
|
let (dir, file_name) =
|
||||||
|
create_text_file("file", include_bytes!("../../test/data/without-llamma.txt"))?;
|
||||||
|
let link = Link {
|
||||||
|
href: "llamma".to_string(),
|
||||||
|
rel: "".to_string(),
|
||||||
|
hreflang: None,
|
||||||
|
mime_type: None,
|
||||||
|
title: None,
|
||||||
|
length: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
//when
|
||||||
|
let result = find(&link, &file_name)?;
|
||||||
|
|
||||||
|
//then
|
||||||
|
drop(dir);
|
||||||
|
|
||||||
|
assert_eq!(result, false);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn false_if_embedded_within_line() -> Result<()> {
|
||||||
|
//given
|
||||||
|
let (dir, file_name) = create_text_file(
|
||||||
|
"file",
|
||||||
|
include_bytes!("../../test/data/with-embedded-llamma.txt"),
|
||||||
|
)?;
|
||||||
|
let link = Link {
|
||||||
|
href: "llamma".to_string(),
|
||||||
|
rel: "".to_string(),
|
||||||
|
hreflang: None,
|
||||||
|
mime_type: None,
|
||||||
|
title: None,
|
||||||
|
length: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
//when
|
||||||
|
let result = find(&link, &file_name)?;
|
||||||
|
|
||||||
|
//then
|
||||||
|
drop(dir);
|
||||||
|
|
||||||
|
assert_eq!(result, false);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,5 +8,10 @@ pub use find::find;
|
||||||
|
|
||||||
type Link = atom_syndication::Link;
|
type Link = atom_syndication::Link;
|
||||||
|
|
||||||
|
pub struct HistoryEnv {
|
||||||
|
pub find: HistoryFind,
|
||||||
|
pub add: HistoryAdd,
|
||||||
|
}
|
||||||
|
|
||||||
pub type HistoryFind = fn(&Link, &str) -> Result<bool>;
|
pub type HistoryFind = fn(&Link, &str) -> Result<bool>;
|
||||||
pub type HistoryAdd = fn(&Link, &str) -> Result<()>;
|
pub type HistoryAdd = fn(&Link, &str) -> Result<()>;
|
||||||
|
|
36
src/lib.rs
36
src/lib.rs
|
@ -5,30 +5,30 @@ pub mod history;
|
||||||
pub mod prelude;
|
pub mod prelude;
|
||||||
mod subscriptions;
|
mod subscriptions;
|
||||||
|
|
||||||
use feed::{FeedFind, FeedGet};
|
#[cfg(test)]
|
||||||
use fetch::FetchDownload;
|
mod test_utils;
|
||||||
use history::{HistoryAdd, HistoryFind};
|
|
||||||
|
use feed::FeedEnv;
|
||||||
|
use fetch::FetchEnv;
|
||||||
|
use history::HistoryEnv;
|
||||||
use prelude::*;
|
use prelude::*;
|
||||||
|
|
||||||
pub fn run(
|
pub struct Env {
|
||||||
subscriptions: &str,
|
pub feed: FeedEnv,
|
||||||
history: &str,
|
pub history: HistoryEnv,
|
||||||
site: &str,
|
pub fetch: FetchEnv,
|
||||||
feed_find: FeedFind,
|
}
|
||||||
feed_get: FeedGet,
|
|
||||||
history_find: HistoryFind,
|
pub fn run(subscriptions: &str, history: &str, site: &str, e: Env) -> Result<()> {
|
||||||
history_add: HistoryAdd,
|
|
||||||
fetch_download: FetchDownload,
|
|
||||||
) -> Result<()> {
|
|
||||||
for channel_name in subscriptions::lines_from(subscriptions)? {
|
for channel_name in subscriptions::lines_from(subscriptions)? {
|
||||||
println!("Channel: {}", channel_name);
|
println!("Channel: {}", channel_name);
|
||||||
let feed_url = feed_find(site, &channel_name)?;
|
let feed_url = (e.feed.find)(site, &channel_name, &e.fetch.get)?;
|
||||||
for entry in feed_get(&feed_url)?.entries() {
|
for entry in (e.feed.get)(&feed_url)?.entries() {
|
||||||
if let Some(link) = entry.links().get(0).cloned() {
|
if let Some(link) = entry.links().get(0).cloned() {
|
||||||
if !history_find(&link, history)? {
|
if !(e.history.find)(&link, history)? {
|
||||||
println!("Downloading {}: {}", &channel_name, entry.title().as_str());
|
println!("Downloading {}: {}", &channel_name, entry.title().as_str());
|
||||||
fetch_download(&link)?;
|
(e.fetch.download)(&link)?;
|
||||||
history_add(&link, history)?;
|
(e.history.add)(&link, history)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
22
src/main.rs
22
src/main.rs
|
@ -1,5 +1,7 @@
|
||||||
use podal::prelude::*;
|
use podal::prelude::*;
|
||||||
|
|
||||||
|
use podal::{feed::FeedEnv, fetch::FetchEnv, history::HistoryEnv};
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
println!("Podal");
|
println!("Podal");
|
||||||
let subscriptions = "subscriptions.txt";
|
let subscriptions = "subscriptions.txt";
|
||||||
|
@ -10,11 +12,21 @@ fn main() -> Result<()> {
|
||||||
subscriptions,
|
subscriptions,
|
||||||
history,
|
history,
|
||||||
site,
|
site,
|
||||||
podal::feed::find,
|
podal::Env {
|
||||||
podal::feed::get,
|
feed: FeedEnv {
|
||||||
podal::history::find,
|
find: podal::feed::find,
|
||||||
podal::history::add,
|
get: podal::feed::reqwest_blocking_get,
|
||||||
podal::fetch::download,
|
},
|
||||||
|
|
||||||
|
history: HistoryEnv {
|
||||||
|
find: podal::history::find,
|
||||||
|
add: podal::history::add,
|
||||||
|
},
|
||||||
|
fetch: FetchEnv {
|
||||||
|
download: podal::fetch::download,
|
||||||
|
get: podal::fetch::get,
|
||||||
|
},
|
||||||
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
println!("Done");
|
println!("Done");
|
||||||
|
|
|
@ -7,11 +7,9 @@ pub fn lines_from(file_name: &str) -> Result<Vec<String>> {
|
||||||
let file = File::open(file_name)?;
|
let file = File::open(file_name)?;
|
||||||
let reader = BufReader::new(file);
|
let reader = BufReader::new(file);
|
||||||
let mut lines = vec![];
|
let mut lines = vec![];
|
||||||
for line in reader.lines() {
|
for line in reader.lines().flatten() {
|
||||||
if let Ok(line) = line {
|
if line.starts_with('@') {
|
||||||
if line.starts_with('@') {
|
lines.push(line);
|
||||||
lines.push(line);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(lines)
|
Ok(lines)
|
||||||
|
@ -19,9 +17,8 @@ pub fn lines_from(file_name: &str) -> Result<Vec<String>> {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::{fs::File, io::Write, str::from_utf8};
|
|
||||||
|
|
||||||
use tempfile::{tempdir, TempDir};
|
use crate::test_utils::create_text_file;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
@ -75,13 +72,4 @@ mod tests {
|
||||||
assert_eq!(result, ["@sub1", "@sub3"]);
|
assert_eq!(result, ["@sub1", "@sub3"]);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_text_file(name: &str, data: &[u8]) -> Result<(TempDir, String)> {
|
|
||||||
let data = from_utf8(data)?;
|
|
||||||
let dir = tempdir()?;
|
|
||||||
let filename = format!("{}", &dir.path().join(name).display());
|
|
||||||
let file = File::create(&filename)?;
|
|
||||||
write!(&file, "{data}")?;
|
|
||||||
Ok((dir, filename))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
25
src/test_utils.rs
Normal file
25
src/test_utils.rs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
use std::{
|
||||||
|
fs::{read_to_string, File},
|
||||||
|
io::Write,
|
||||||
|
str::from_utf8,
|
||||||
|
};
|
||||||
|
|
||||||
|
use tempfile::{tempdir, TempDir};
|
||||||
|
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
pub fn create_text_file(name: &str, data: &[u8]) -> Result<(TempDir, String)> {
|
||||||
|
let data = from_utf8(data)?;
|
||||||
|
let dir = tempdir()?;
|
||||||
|
let filename = format!("{}", &dir.path().join(name).display());
|
||||||
|
let file = File::create(&filename)?;
|
||||||
|
write!(&file, "{data}")?;
|
||||||
|
Ok((dir, filename))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_text_file(file_name: &str) -> Result<Vec<String>> {
|
||||||
|
Ok(read_to_string(file_name)?
|
||||||
|
.lines()
|
||||||
|
.map(String::from)
|
||||||
|
.collect())
|
||||||
|
}
|
2
test/data/downloads.txt
Normal file
2
test/data/downloads.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
line-1
|
||||||
|
line-2
|
0
test/data/empty.txt
Normal file
0
test/data/empty.txt
Normal file
3
test/data/with-embedded-llamma.txt
Normal file
3
test/data/with-embedded-llamma.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
this is a text file
|
||||||
|
embedded llamma
|
||||||
|
it has three lines
|
3
test/data/with-llamma.txt
Normal file
3
test/data/with-llamma.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
this is a text file
|
||||||
|
llamma
|
||||||
|
it has three lines
|
2
test/data/without-llamma.txt
Normal file
2
test/data/without-llamma.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
this is a text file
|
||||||
|
it has three lines
|
Loading…
Add table
Reference in a new issue