Fix race between tests, add DummyCache

Reworked DummyCache indexing, removed broken top-level tests.
Added a simple test deserializing a canteen.
Added DummyCache for tests.
See #11.
This commit is contained in:
Malte Tammena 2021-10-27 08:25:27 +02:00
parent a2eaa80d08
commit 6960ae6ba9
12 changed files with 150 additions and 225 deletions

80
Cargo.lock generated
View file

@ -29,20 +29,6 @@ dependencies = [
"winapi",
]
[[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"
@ -260,17 +246,6 @@ 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.8.0"
@ -446,12 +421,6 @@ version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499"
[[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"
@ -491,12 +460,6 @@ 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"
@ -974,7 +937,6 @@ dependencies = [
name = "mensa"
version = "0.4.2"
dependencies = [
"assert_cmd",
"cacache",
"chrono",
"date_time_parser",
@ -1226,33 +1188,6 @@ version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3ca011bd0129ff4ae15cd04c4eef202cadf6c51c21e47aba319b4e0501db741"
[[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"
@ -1796,12 +1731,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "termtree"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76565a2f8df1d2170b5c365aa39d0623fd93fec20545edde299233cea82d0f16"
[[package]]
name = "textwrap"
version = "0.11.0"
@ -2101,15 +2030,6 @@ 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

@ -39,4 +39,3 @@ itertools = "0.10"
[dev-dependencies]
pretty_assertions = "1.0"
ssri = "7.0"
assert_cmd = "2.0"

49
src/cache/dummy.rs vendored
View file

@ -1,7 +1,7 @@
use std::{collections::BTreeMap, sync::RwLock};
use cacache::Metadata;
use ssri::{Hash, Integrity};
use ssri::Integrity;
use super::Cache;
@ -10,6 +10,7 @@ use crate::{
request::Headers,
};
#[derive(Debug)]
struct Entry {
meta: Metadata,
text: String,
@ -19,7 +20,9 @@ pub struct DummyCache {
/// The real, cacache-based implementation takes only immutable references
/// and the API is adopted to handle that. Thus we'll have to do our
/// own interior mutability here.
content: RwLock<BTreeMap<Hash, Entry>>,
///
/// This maps paths to entries.
content: RwLock<BTreeMap<String, Entry>>,
}
impl Cache for DummyCache {
@ -30,18 +33,21 @@ impl Cache for DummyCache {
}
fn read(&self, meta: &Metadata) -> Result<String> {
let (algorithm, digest) = meta.integrity.to_hex();
let hash = Hash { algorithm, digest };
let path = path_from_integrity(&meta.integrity);
let read = self.content.read().expect("Reading cache failed");
let entry = read
.get(&hash)
.get(&path)
.expect("BUG: Metadata exists, but entry does not!");
eprintln!(
"Cache read for {:?}\n-> Returning: {:#?}",
meta.integrity, entry.text
);
Ok(entry.text.clone())
}
fn write(&self, headers: &Headers, url: &str, text: &str) -> Result<()> {
let mut write = self.content.write().expect("Writing cache failed");
let hash = hash_from_key(url);
let hash = path_from_key(url);
let meta = assemble_meta(headers, url, text)?;
write.insert(
hash,
@ -50,17 +56,19 @@ impl Cache for DummyCache {
text: text.to_owned(),
},
);
eprintln!(
"Cache write to {:?}\n-> Headers: {:#?}\n-> Content: {:#?}",
url, headers, text
);
Ok(())
}
fn meta(&self, url: &str) -> Result<Option<Metadata>> {
let hash = hash_from_key(url);
match self
.content
.read()
.expect("Reading cache failed")
.get(&hash)
{
let hash = path_from_key(url);
let read = self.content.read().expect("Reading cache failed");
let entry = read.get(&hash);
eprintln!("Cache Metadata for {:?}: {:?}", url, entry);
match entry {
Some(entry) => Ok(Some(clone_metadata(&entry.meta))),
None => Ok(None),
}
@ -68,6 +76,7 @@ impl Cache for DummyCache {
fn clear(&self) -> Result<()> {
self.content.write().expect("Writing cache failed").clear();
eprintln!("Cache cleared");
Ok(())
}
@ -81,14 +90,20 @@ impl Cache for DummyCache {
}
}
fn hash_from_key(key: &str) -> Hash {
fn path_from_key(key: &str) -> String {
let integrity = Integrity::from(key);
hash_from_integrity(&integrity)
let path = path_from_integrity(&integrity);
eprintln!("Hashing key {:?} -> {:#?}", key, path);
path
}
fn hash_from_integrity(integrity: &Integrity) -> Hash {
fn path_from_integrity(integrity: &Integrity) -> String {
let mut path = String::new();
let (algorithm, digest) = integrity.to_hex();
Hash { algorithm, digest }
path += &algorithm.to_string();
path += &digest;
eprintln!("Hashing integrity {:?} -> {:#?}", integrity, path);
path
}
fn clone_metadata(meta: &Metadata) -> Metadata {

View file

@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
use crate::error::Result;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(from = "T", untagged)]
pub enum Fetchable<T> {
/// The value does not exist, but can be fetched.

4
src/cache/mod.rs vendored
View file

@ -61,6 +61,7 @@ type TextAndHeaders = (String, Headers);
lazy_static! {
pub static ref CACHE: DefaultCache = DefaultCache::init().expect("Initialized cache");
pub static ref API: DefaultApi = DefaultApi::create().expect("Failed to create API");
}
/// Possible results from a cache load.
@ -206,9 +207,6 @@ fn get_and_update_cache<C: Cache>(
etag: Option<String>,
meta: Option<Metadata>,
) -> Result<TextAndHeaders> {
lazy_static! {
static ref API: DefaultApi = DefaultApi::create().expect("Failed to create API");
}
// Send request with optional ETag header
let resp = API.get(url, etag)?;
info!("Request to {:?} returned {}", url, resp.status);

3
src/cache/tests.rs vendored
View file

@ -33,6 +33,7 @@ fn test_cache_is_empty() {
#[test]
fn basic_caching() {
let url = "http://invalid.local/test";
API.register(url, "It works", Some("static"), Some(1), None, Some(1));
// Cache is empty
let val = try_load_cache(&*CACHE, url, Duration::max_value()).unwrap();
print_cache_list("After first read").unwrap();
@ -41,7 +42,7 @@ fn basic_caching() {
let val = CACHE.fetch(url, *TTL, |txt, _| Ok(txt)).unwrap();
assert_eq!(val, "It works",);
// The cache should now be hit
let val = try_load_cache(&*CACHE, url, Duration::max_value()).unwrap();
let val = dbg!(try_load_cache(&*CACHE, url, Duration::max_value()).unwrap());
print_cache_list("After second read").unwrap();
assert_eq!(
val,

View file

@ -8,6 +8,8 @@ use tracing::info;
mod de;
mod ser;
#[cfg(test)]
mod tests;
use crate::{
cache::{Cache, Fetchable, CACHE},
@ -32,7 +34,7 @@ lazy_static! {
static ref EMPTY: Vec<Meal> = Vec::new();
}
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(from = "de::CanteenDeserialized")]
pub struct Canteen {
id: CanteenId,
@ -44,7 +46,7 @@ pub struct Canteen {
meals: Fetchable<HashMap<NaiveDate, Fetchable<Vec<Meal>>>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Meta {
name: String,
city: String,

38
src/canteen/tests.rs Normal file
View file

@ -0,0 +1,38 @@
use chrono::Duration;
use pretty_assertions::assert_eq;
use crate::cache::{Fetchable, API};
use super::*;
#[test]
fn it_parses_a_canteen() {
let url = "http://invalid.local/canteen/1";
let value = r#"
{
"id": 1,
"name": "Awesome Canteen",
"city": "Lummerland",
"address": "Some place!",
"coordinates": [
52.13,
11.64
]
}
"#;
API.register(url, value, None, Some(1), None, Some(1));
let canteen: Canteen = CACHE.fetch_json(url, Duration::zero()).unwrap();
assert_eq!(
canteen,
Canteen {
id: 1,
meta: Fetchable::Fetched(Meta {
name: String::from("Awesome Canteen"),
city: String::from("Lummerland"),
address: String::from("Some place!"),
coordinates: Some([52.13, 11.64]),
}),
meals: Fetchable::None,
}
);
}

View file

@ -136,8 +136,6 @@ mod meal;
mod pagination;
mod request;
mod tag;
// #[cfg(test)]
// mod tests;
use crate::{
cache::CACHE,

View file

@ -27,14 +27,14 @@ lazy_static! {
static ref PRE: String = color!(if_plain!("", " |"); bright_black);
}
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(from = "de::Meal")]
pub struct Meal {
pub id: MealId,
pub meta: Fetchable<Meta>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Meta {
pub name: String,
pub tags: HashSet<Tag>,
@ -43,7 +43,7 @@ pub struct Meta {
pub category: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(debug, serde(deny_unknown_fields))]
pub struct Prices {
students: Option<f32>,

View file

@ -1,47 +1,92 @@
//! This contains the [`DummyApi`] used for testing purposes.
use std::{collections::HashMap, sync::RwLock};
use reqwest::StatusCode;
use crate::error::Result;
use super::{Api, Headers, Response};
#[derive(Debug, Clone)]
struct KnownResp {
etag: Option<String>,
value: String,
this_page: Option<usize>,
next_page: Option<String>,
last_page: Option<usize>,
}
/// A dummy API, serving local, deterministic Responses
#[derive(Debug)]
pub struct DummyApi;
pub struct DummyApi {
known: RwLock<HashMap<String, KnownResp>>,
}
impl Api for DummyApi {
fn create() -> Result<Self> {
Ok(DummyApi)
Ok(DummyApi {
known: RwLock::new(HashMap::new()),
})
}
fn get<'url, S>(&self, url: &'url str, etag: Option<S>) -> Result<Response<'url>>
where
S: AsRef<str>,
{
if url == "http://invalid.local/test" {
get_test_page(etag)
} else {
panic!("BUG: Invalid url in dummy api: {:?}", url)
let read = self.known.read().expect("Reading known urls failed");
let etag = etag.map(|etag| etag.as_ref().to_owned());
match read.get(url) {
Some(resp) => {
let resp = resp.clone();
Ok(Response {
url,
status: status_from_etags(&resp.etag, &etag),
headers: Headers {
etag: resp.etag,
this_page: resp.this_page,
next_page: resp.next_page,
last_page: resp.last_page,
},
body: resp.value,
})
}
None => panic!("BUG: Invalid url in dummy api: {:?}", url),
}
}
}
/// GET http://invalid.local/test
fn get_test_page<S: AsRef<str>>(etag: Option<S>) -> Result<Response<'static>> {
let etag = etag.map(|etag| etag.as_ref().to_owned());
Ok(Response {
url: "http://invalid.local/test",
status: if etag == Some("static".into()) {
StatusCode::NOT_MODIFIED
} else {
StatusCode::OK
},
headers: Headers {
etag: Some("static".into()),
this_page: Some(1),
next_page: None,
last_page: Some(1),
},
body: "It works".to_owned(),
})
impl DummyApi {
pub fn register(
&self,
url: &str,
value: &str,
etag: Option<&str>,
this_page: Option<usize>,
next_page: Option<&str>,
last_page: Option<usize>,
) {
let mut write = self.known.write().expect("Writing known urls failed");
let etag = etag.map(str::to_owned);
let next_page = next_page.map(str::to_owned);
let old = write.insert(
url.to_owned(),
KnownResp {
etag,
value: value.to_owned(),
this_page,
next_page,
last_page,
},
);
if old.is_some() {
panic!("Adress {:?} already registered!", url);
}
}
}
fn status_from_etags(old: &Option<String>, new: &Option<String>) -> StatusCode {
match (old, new) {
(Some(old), Some(new)) if old == new => StatusCode::NOT_MODIFIED,
_ => StatusCode::OK,
}
}

View file

@ -1,91 +0,0 @@
//! TODO: These tests fail when run via `nix flake check`
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]
#[cfg(not(windows))]
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();
}