Cleanup, add command tests

This commit is contained in:
Malte Tammena 2021-10-22 14:10:17 +02:00
parent 15e5133f8c
commit d1b0dc36fe
13 changed files with 231 additions and 67 deletions

80
Cargo.lock generated
View file

@ -48,6 +48,20 @@ dependencies = [
"serde_json",
]
[[package]]
name = "assert_cmd"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e996dc7940838b7ef1096b882e29ec30a3149a3a443cdc8dba19ed382eca1fe2"
dependencies = [
"bstr",
"doc-comment",
"predicates",
"predicates-core",
"predicates-tree",
"wait-timeout",
]
[[package]]
name = "async-channel"
version = "1.6.1"
@ -311,6 +325,17 @@ dependencies = [
"once_cell",
]
[[package]]
name = "bstr"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
dependencies = [
"lazy_static",
"memchr",
"regex-automata",
]
[[package]]
name = "bumpalo"
version = "3.7.1"
@ -535,6 +560,12 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
[[package]]
name = "difflib"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]]
name = "digest"
version = "0.8.1"
@ -584,6 +615,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "doc-comment"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]]
name = "either"
version = "1.6.1"
@ -1200,6 +1237,7 @@ dependencies = [
name = "mensa"
version = "0.4.0"
dependencies = [
"assert_cmd",
"cacache",
"chrono",
"date_time_parser",
@ -1534,6 +1572,33 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "predicates"
version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c6ce811d0b2e103743eec01db1c50612221f173084ce2f7941053e94b6bb474"
dependencies = [
"difflib",
"itertools",
"predicates-core",
]
[[package]]
name = "predicates-core"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57e35a3326b75e49aa85f5dc6ec15b41108cf5aee58eabb1f274dd18b73c2451"
[[package]]
name = "predicates-tree"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "338c7be2905b732ae3984a2f40032b5e94fd8f52505b186c7d4d68d193445df7"
dependencies = [
"predicates-core",
"termtree",
]
[[package]]
name = "pretty_assertions"
version = "1.0.0"
@ -2155,6 +2220,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "termtree"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78fbf2dd23e79c28ccfa2472d3e6b3b189866ffef1aeb91f17c2d968b6586378"
[[package]]
name = "textwrap"
version = "0.11.0"
@ -2488,6 +2559,15 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
name = "wait-timeout"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
dependencies = [
"libc",
]
[[package]]
name = "waker-fn"
version = "1.1.0"

View file

@ -40,3 +40,4 @@ itertools = "0.10"
temp-dir = "0.1"
httpmock = "0.6"
pretty_assertions = "1.0"
assert_cmd = "2.0"

View file

@ -32,10 +32,7 @@ use cacache::Metadata;
use chrono::{Duration, TimeZone};
use lazy_static::lazy_static;
use regex::Regex;
use reqwest::{
blocking::{Client, Response},
IntoUrl, StatusCode,
};
use reqwest::{blocking::Response, IntoUrl, StatusCode};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use tracing::{info, warn};
@ -89,14 +86,14 @@ where
U: IntoUrl,
T: DeserializeOwned,
{
fetch(&CONF.client, url, local_ttl, |text, _| {
fetch(url, local_ttl, |text, _| {
// TODO: Check content header?
serde_json::from_str(&text).map_err(|why| Error::Deserializing(why, "fetching json"))
})
}
/// Generic method for fetching remote url-based resources that may be cached.
pub fn fetch<Map, U, T>(client: &Client, url: U, local_ttl: Duration, map: Map) -> Result<T>
pub fn fetch<Map, U, T>(url: U, local_ttl: Duration, map: Map) -> Result<T>
where
U: IntoUrl,
Map: FnOnce(String, Headers) -> Result<T>,
@ -114,20 +111,20 @@ where
}
Ok(CacheResult::Miss) => {
info!("Missed cache on {:?}", url);
get_and_update_cache(client, url, None, None)?
get_and_update_cache(url, None, None)?
}
Ok(CacheResult::Stale(old_headers, meta)) => {
info!("Stale cache on {:?}", url);
// The cache is stale but may still be valid
// Request the resource with set IF_NONE_MATCH tag and update
// the caches metadata or value
match get_and_update_cache(client, url, old_headers.etag, Some(meta)) {
match get_and_update_cache(url, old_headers.etag, Some(meta)) {
Ok(tah) => tah,
Err(why) => {
warn!("{}", why);
// Fetching and updating failed for some reason, retry
// without the IF_NONE_MATCH tag and fail if unsuccessful
get_and_update_cache(client, url, None, None)?
get_and_update_cache(url, None, None)?
}
}
}
@ -135,7 +132,7 @@ where
// Fetching from the cache failed for some reason, just
// request the resource and update the cache
warn!("{}", why);
get_and_update_cache(client, url, None, None)?
get_and_update_cache(url, None, None)?
}
};
// Apply the map and return the result
@ -174,13 +171,12 @@ fn try_load_cache(url: &str, local_ttl: Duration) -> Result<CacheResult<TextAndH
/// If an optional `etag` is provided, add the If-None-Match header, and thus
/// only get an update if the new ETAG differs from the given `etag`.
fn get_and_update_cache(
client: &Client,
url: &str,
etag: Option<String>,
meta: Option<Metadata>,
) -> Result<TextAndHeaders> {
// Construct the request
let mut builder = client.get(url);
let mut builder = CONF.client.get(url);
// Add If-None-Match header, if etag is present
if let Some(etag) = etag {
let etag_key = reqwest::header::IF_NONE_MATCH;

View file

@ -12,10 +12,6 @@ pub enum Fetchable<T> {
}
impl<T> Fetchable<T> {
pub fn is_fetched(&self) -> bool {
matches!(self, Self::Fetched(_))
}
pub fn fetch<F>(&mut self, f: F) -> Result<&T>
where
F: FnOnce() -> Result<T>,

3
src/cache/tests.rs vendored
View file

@ -6,7 +6,6 @@ use pretty_assertions::assert_eq;
use super::*;
lazy_static! {
static ref CLIENT: Client = Client::new();
static ref TTL: Duration = Duration::minutes(1);
}
@ -48,7 +47,7 @@ fn basic_caching() {
print_cache_list("After first read");
assert_eq!(val, CacheResult::Miss);
// Populate the cache with the first request
let val = fetch(&*CLIENT, server.url("/test"), *TTL, |txt, _| Ok(txt)).unwrap();
let val = fetch(server.url("/test"), *TTL, |txt, _| Ok(txt)).unwrap();
assert_eq!(val, "This page works!",);
// The cache should now be hit
let val = try_load_cache(&server.url("/test"), Duration::max_value()).unwrap();

View file

@ -56,11 +56,12 @@ pub struct Meta {
#[serde(try_from = "de::DayDeserialized")]
pub struct Day {
date: NaiveDate,
closed: bool,
#[serde(rename = "closed")]
_closed: bool,
}
impl Meta {
pub fn fetch(id: CanteenId) -> Result<Self> {
pub fn fetch(_id: CanteenId) -> Result<Self> {
todo!()
}
}
@ -108,10 +109,6 @@ impl Canteen {
self.id
}
pub fn name(&mut self) -> Result<&String> {
Ok(&self.meta()?.address)
}
pub fn address(&mut self) -> Result<&String> {
Ok(&self.meta()?.address)
}
@ -174,7 +171,7 @@ impl Canteen {
ENDPOINT, lat, long, geo.radius,
)
};
PaginatedList::from(&CONF.client, url, *TTL_CANTEENS)?.try_flatten_and_collect()
PaginatedList::new(url, *TTL_CANTEENS)?.consume()
}
}

View file

@ -42,7 +42,7 @@ impl TryFrom<DayDeserialized> for super::Day {
fn try_from(raw: DayDeserialized) -> Result<Self, Self::Error> {
Ok(Self {
date: NaiveDate::parse_from_str(&raw.date, "%Y-%m-%d").map_err(Error::InvalidDate)?,
closed: raw.closed,
_closed: raw.closed,
})
}
}

View file

@ -1,7 +1,5 @@
use core::fmt;
use thiserror::Error;
use tracing::{error, info, warn};
use tracing::{error, warn};
pub type Result<T> = std::result::Result<T, Error>;
@ -80,9 +78,3 @@ where
}
}
}
/// Debug print the given value using [`info`].
pub fn pass_info<T: fmt::Debug>(t: T) -> T {
info!("{:#?}", &t);
t
}

View file

@ -40,15 +40,15 @@ pub fn infer() -> Result<(f32, f32)> {
},
Command::Tags => (None, None),
};
let (lat, long) = if lat.is_none() || long.is_none() {
let guessed = fetch_geoip()?;
(
lat.unwrap_or(guessed.latitude),
long.unwrap_or(guessed.longitude),
)
} else {
// Cannot panic, due to above if
(lat.unwrap(), long.unwrap())
let (lat, long) = match (lat, long) {
(Some(lat), Some(long)) => (lat, long),
(lat, long) => {
let guessed = fetch_geoip()?;
(
lat.unwrap_or(guessed.latitude),
long.unwrap_or(guessed.longitude),
)
}
};
Ok((lat, long))
}

View file

@ -109,6 +109,8 @@ mod geoip;
mod meal;
mod pagination;
mod tag;
#[cfg(test)]
mod tests;
use crate::{
canteen::Canteen,

View file

@ -53,18 +53,12 @@ enum Note {
}
impl Meta {
fn fetch(id: MealId) -> Result<Meta> {
fn fetch(_id: MealId) -> Result<Meta> {
todo!()
}
}
impl Meal {
/// Print this [`Meal`] to the terminal.
pub fn print(&mut self, highlight: bool) -> Result<()> {
self.complete()?.print(highlight);
Ok(())
}
pub fn meta(&mut self) -> Result<&Meta> {
self.meta.fetch(|| Meta::fetch(self.id))
}

View file

@ -1,5 +1,9 @@
//! Wrapper around [`reqwest`] for multi-page requests.
//!
//! Only relevant/tested on the OpenMensa API.
use chrono::Duration;
use reqwest::blocking::Client;
use itertools::Itertools;
use serde::de::DeserializeOwned;
use std::marker::PhantomData;
@ -9,23 +13,42 @@ use crate::{
error::{Error, Result},
};
pub struct PaginatedList<'client, T>
/// An iterator over json pages containing lists.
///
/// # Example
///
/// ## Page 1
///
/// ```json
/// [ { "id": 1 },
/// { "id": 2 } ]
/// ```
///
/// ## Page 2
///
/// ```json
/// [ { "id": 3 },
/// { "id": 4 } ]
/// ```
pub struct PaginatedList<T>
where
T: DeserializeOwned,
{
client: &'client Client,
next_page: Option<String>,
ttl: Duration,
__item: PhantomData<T>,
}
impl<'client, T> PaginatedList<'client, T>
impl<T> PaginatedList<T>
where
T: DeserializeOwned,
{
pub fn from<S: AsRef<str>>(client: &'client Client, url: S, ttl: Duration) -> Result<Self> {
/// Create a new page iterator
///
/// Takes the `url` for the first page and a
/// `local_ttl` for the cached values.
pub fn new<S: AsRef<str>>(url: S, ttl: Duration) -> Result<Self> {
Ok(PaginatedList {
client,
ttl,
next_page: Some(url.as_ref().into()),
__item: PhantomData,
@ -33,21 +56,17 @@ where
}
}
impl<'client, T> PaginatedList<'client, T>
impl<T> PaginatedList<T>
where
T: DeserializeOwned,
{
pub fn try_flatten_and_collect(self) -> Result<Vec<T>> {
let mut ret = vec![];
for value in self {
let value = value?;
ret.extend(value);
}
Ok(ret)
/// Consumes this iterator, flattening the collected pages.
pub fn consume(self) -> Result<Vec<T>> {
self.flatten_ok().try_collect()
}
}
impl<'client, T> Iterator for PaginatedList<'client, T>
impl<T> Iterator for PaginatedList<T>
where
T: DeserializeOwned,
{
@ -56,7 +75,7 @@ where
fn next(&mut self) -> Option<Self::Item> {
// This will yield until no next_page is available
let curr_page = self.next_page.take()?;
let res = cache::fetch(self.client, &curr_page, self.ttl, |text, headers| {
let res = cache::fetch(curr_page, self.ttl, |text, headers| {
let val = serde_json::from_str::<Vec<_>>(&text)
.map_err(|why| Error::Deserializing(why, "fetching json in pagination iterator"))?;
Ok((val, headers.this_page, headers.next_page, headers.last_page))

88
src/tests.rs Normal file
View file

@ -0,0 +1,88 @@
use std::time::Duration;
use assert_cmd::Command;
#[test]
pub fn cmd_mensa_meals() {
Command::cargo_bin("mensa")
.unwrap()
// Prevent loading the config
.args(&["--config", "/does/not/exist"])
// Show meals
.arg("meals")
// Use canteen id 1
.args(&["--id", "1"])
.timeout(Duration::from_secs(10))
.assert()
.success();
}
#[test]
pub fn cmd_mensa_meals_json() {
Command::cargo_bin("mensa")
.unwrap()
// Prevent loading the config
.args(&["--config", "/does/not/exist"])
// Show meals
.arg("meals")
// Use canteen id 1
.args(&["--id", "1"])
.arg("--json")
.timeout(Duration::from_secs(10))
.assert()
.success();
}
#[test]
pub fn cmd_mensa_canteens() {
Command::cargo_bin("mensa")
.unwrap()
// Prevent loading the config
.args(&["--config", "/does/not/exist"])
// Show meals
.arg("canteens")
.timeout(Duration::from_secs(10))
.assert()
.success();
}
#[test]
pub fn cmd_mensa_canteens_json() {
Command::cargo_bin("mensa")
.unwrap()
// Prevent loading the config
.args(&["--config", "/does/not/exist"])
// Show meals
.arg("canteens")
.arg("--json")
.timeout(Duration::from_secs(10))
.assert()
.success();
}
#[test]
pub fn cmd_mensa_tags() {
Command::cargo_bin("mensa")
.unwrap()
// Prevent loading the config
.args(&["--config", "/does/not/exist"])
// Show tags
.arg("tags")
.timeout(Duration::from_secs(10))
.assert()
.success();
}
#[test]
pub fn cmd_mensa_tags_json() {
Command::cargo_bin("mensa")
.unwrap()
// Prevent loading the config
.args(&["--config", "/does/not/exist"])
// Show tags
.arg("tags")
.arg("--json")
.timeout(Duration::from_secs(10))
.assert()
.success();
}