Support mocking DB to increase testability.

This commit is contained in:
Condorra 2023-02-19 14:03:15 +11:00
parent 8d12c88904
commit 4652fa52cf
12 changed files with 240 additions and 48 deletions

110
Cargo.lock generated
View File

@ -151,6 +151,8 @@ dependencies = [
"humantime",
"itertools",
"log",
"mockall",
"mockall_double",
"nix",
"nom",
"once_cell",
@ -479,6 +481,12 @@ dependencies = [
"tokio",
]
[[package]]
name = "difflib"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]]
name = "digest"
version = "0.10.6"
@ -490,6 +498,12 @@ dependencies = [
"subtle",
]
[[package]]
name = "downcast"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1"
[[package]]
name = "educe"
version = "0.4.20"
@ -537,6 +551,15 @@ dependencies = [
"instant",
]
[[package]]
name = "float-cmp"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
dependencies = [
"num-traits",
]
[[package]]
name = "fnv"
version = "1.0.7"
@ -552,6 +575,12 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "fragile"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa"
[[package]]
name = "futures"
version = "0.3.25"
@ -1019,6 +1048,45 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "mockall"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50e4a1c770583dac7ab5e2f6c139153b783a53a1bbee9729613f193e59828326"
dependencies = [
"cfg-if",
"downcast",
"fragile",
"lazy_static",
"mockall_derive",
"predicates",
"predicates-tree",
]
[[package]]
name = "mockall_derive"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "832663583d5fa284ca8810bf7015e46c9fff9622d3cf34bd1eea5003fec06dd0"
dependencies = [
"cfg-if",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "mockall_double"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae71c7bb287375187c775cf82e2dcf1bef3388aaf58f0789a77f9c7ab28466f6"
dependencies = [
"cfg-if",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "multipart"
version = "0.18.0"
@ -1061,6 +1129,12 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "normalize-line-endings"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
[[package]]
name = "num-bigint"
version = "0.4.3"
@ -1281,6 +1355,36 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "predicates"
version = "2.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd"
dependencies = [
"difflib",
"float-cmp",
"itertools",
"normalize-line-endings",
"predicates-core",
"regex",
]
[[package]]
name = "predicates-core"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f883590242d3c6fc5bf50299011695fa6590c2c70eac95ee1bdb9a733ad1a2"
[[package]]
name = "predicates-tree"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54ff541861505aabf6ea722d2131ee980b8276e10a1297b94e896dd8b621850d"
dependencies = [
"predicates-core",
"termtree",
]
[[package]]
name = "proc-macro-crate"
version = "0.1.5"
@ -1780,6 +1884,12 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "termtree"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95059e91184749cb66be6dc994f67f182b6d897cb3df74a5bf66b5e709295fd8"
[[package]]
name = "thiserror"
version = "1.0.38"

View File

@ -39,3 +39,5 @@ async-recursion = "1.0.0"
rand_distr = "0.4.3"
humantime = "2.1.0"
rust_decimal = "1.28.0"
mockall = "0.11.3"
mockall_double = "0.3.0"

View File

@ -25,6 +25,8 @@ use serde::{Serialize, Deserialize};
use serde_json::{self, Value};
use futures::FutureExt;
use chrono::{DateTime, Utc};
#[cfg(test)]
use mockall::automock;
#[derive(Clone, Debug)]
pub struct DBPool {
@ -70,6 +72,7 @@ pub struct LocationStats {
pub total_weight: u64,
}
#[cfg_attr(test, allow(dead_code))]
impl DBPool {
pub async fn record_listener_ping(self: &DBPool, listener: Uuid) -> DResult<()> {
self.get_conn().await?.execute(
@ -246,18 +249,19 @@ impl ItemSearchParams<'_> {
}
}
#[cfg_attr(test, automock)]
#[cfg_attr(test, allow(dead_code))]
impl DBTrans {
pub async fn queue_for_session(self: &Self,
session: &ListenerSession,
message: Option<&str>) -> DResult<()> {
pub async fn queue_for_session<'a>(self: &'a Self,
session: &'a ListenerSession,
message: Option<&'a str>) -> DResult<()> {
self.pg_trans()?
.execute("INSERT INTO sendqueue (session, listener, message) VALUES ($1, $2, $3)",
&[&session.session, &session.listener, &message]).await?;
Ok(())
}
pub async fn get_session_user_model(self: &Self, session: &ListenerSession) -> DResult<Option<(Session, Option<User>)>> {
pub async fn get_session_user_model<'a>(self: &'a Self, session: &'a ListenerSession) -> DResult<Option<(Session, Option<User>)>> {
match self.pg_trans()?
.query_opt("SELECT s.details AS sess_details, \
u.details AS user_details FROM sessions s \
@ -277,7 +281,7 @@ impl DBTrans {
}
}
pub async fn save_session_model(self: &Self, session: &ListenerSession, details: &Session)
pub async fn save_session_model<'a>(self: &'a Self, session: &'a ListenerSession, details: &Session)
-> DResult<()> {
self.pg_trans()?
.execute("UPDATE sessions SET details = $1 WHERE session = $2",
@ -285,7 +289,7 @@ impl DBTrans {
Ok(())
}
pub async fn find_by_username(self: &Self, username: &str) -> DResult<Option<User>> {
pub async fn find_by_username<'a>(self: &'a Self, username: &'a str) -> DResult<Option<User>> {
if let Some(details_json) = self.pg_trans()?
.query_opt("SELECT details FROM users WHERE username=$1",
&[&username.to_lowercase()]).await? {
@ -294,13 +298,13 @@ impl DBTrans {
Ok(None)
}
pub async fn create_item(self: &Self, item: &Item) -> DResult<i64> {
pub async fn create_item<'a>(self: &'a Self, item: &'a Item) -> DResult<i64> {
Ok(self.pg_trans()?.query_one("INSERT INTO items (details) VALUES ($1) RETURNING item_id",
&[&serde_json::to_value(item)?]).await?
.get("item_id"))
}
pub async fn limited_update_static_item(self: &Self, item: &Item) -> DResult<()> {
pub async fn limited_update_static_item<'a>(self: &'a Self, item: &'a Item) -> DResult<()> {
let value = serde_json::to_value(item)?;
let obj_map = value.as_object()
.expect("Static item to be object in JSON");
@ -323,7 +327,7 @@ impl DBTrans {
Ok(())
}
pub async fn limited_update_static_task(self: &Self, task: &Task) -> DResult<()> {
pub async fn limited_update_static_task<'a>(self: &'a Self, task: &'a Task) -> DResult<()> {
let value = serde_json::to_value(task)?;
let obj_map = value.as_object()
.expect("Static task to be object in JSON");
@ -346,7 +350,7 @@ impl DBTrans {
}
pub async fn create_user(self: &Self, session: &ListenerSession, user_dat: &User) -> DResult<()> {
pub async fn create_user<'a>(self: &'a Self, session: &'a ListenerSession, user_dat: &'a User) -> DResult<()> {
self.pg_trans()?.execute("INSERT INTO users (\
username, current_session, current_listener, details\
) VALUES ($1, $2, $3, $4)", &[&user_dat.username.to_lowercase(),
@ -356,7 +360,7 @@ impl DBTrans {
Ok(())
}
pub async fn save_user_model(self: &Self, details: &User)
pub async fn save_user_model<'a>(self: &'a Self, details: &'a User)
-> DResult<()> {
self.pg_trans()?
.execute("UPDATE users SET details = $1 WHERE username = $2",
@ -365,8 +369,8 @@ impl DBTrans {
Ok(())
}
pub async fn attach_user_to_session(self: &Self, username: &str,
session: &ListenerSession) -> DResult<()> {
pub async fn attach_user_to_session<'a>(self: &'a Self, username: &'a str,
session: &'a ListenerSession) -> DResult<()> {
let username_l = username.to_lowercase();
self.pg_trans()?
.execute("INSERT INTO sendqueue (session, listener, message) \
@ -386,7 +390,7 @@ impl DBTrans {
Ok(())
}
pub async fn find_static_items_by_type(self: &Self, item_type: &str) ->
pub async fn find_static_items_by_type<'a>(self: &'a Self, item_type: &'a str) ->
DResult<Box<BTreeSet<String>>> {
Ok(Box::new(
self.pg_trans()?
@ -399,7 +403,7 @@ impl DBTrans {
.collect()))
}
pub async fn find_static_tasks_by_type(self: &Self, task_type: &str) ->
pub async fn find_static_tasks_by_type<'a>(self: &'a Self, task_type: &'a str) ->
DResult<Box<BTreeSet<String>>> {
Ok(Box::new(
self.pg_trans()?
@ -412,7 +416,7 @@ impl DBTrans {
.collect()))
}
pub async fn delete_static_items_by_code(self: &Self, item_type: &str,
pub async fn delete_static_items_by_code<'a>(self: &'a Self, item_type: &'a str,
item_code: &str) -> DResult<()> {
self.pg_trans()?.query(
"DELETE FROM items WHERE details->>'is_static' = 'true' AND \
@ -422,8 +426,8 @@ impl DBTrans {
Ok(())
}
pub async fn delete_static_tasks_by_code(self: &Self, task_type: &str,
task_code: &str) -> DResult<()> {
pub async fn delete_static_tasks_by_code<'a>(self: &'a Self, task_type: &'a str,
task_code: &'a str) -> DResult<()> {
self.pg_trans()?.query(
"DELETE FROM task WHERE details->>'is_static' = 'true' AND \
details->>'task_type' = $1 AND \
@ -432,7 +436,7 @@ impl DBTrans {
Ok(())
}
pub async fn find_item_by_type_code(self: &Self, item_type: &str, item_code: &str) ->
pub async fn find_item_by_type_code<'a>(self: &'a Self, item_type: &'a str, item_code: &'a str) ->
DResult<Option<Arc<Item>>> {
if let Some(item) = self.pg_trans()?.query_opt(
"SELECT details FROM items WHERE \
@ -443,7 +447,7 @@ impl DBTrans {
Ok(None)
}
pub async fn transfer_all_possessions_code(self: &Self, src_loc: &str, dst_loc: &str) -> DResult<()> {
pub async fn transfer_all_possessions_code<'a>(self: &'a Self, src_loc: &'a str, dst_loc: &'a str) -> DResult<()> {
self.pg_trans()?.execute(
"UPDATE items SET details=JSONB_SET(details, '{location}', $1) \
WHERE details->>'location' = $2",
@ -451,14 +455,14 @@ impl DBTrans {
Ok(())
}
pub async fn transfer_all_possessions(self: &Self, source: &Item, dest: &Item) -> DResult<()> {
pub async fn transfer_all_possessions<'a>(self: &'a Self, source: &'a Item, dest: &'a Item) -> DResult<()> {
let src_loc = format!("{}/{}", &source.item_type, &source.item_code);
let dst_loc = format!("{}/{}", &dest.item_type, &dest.item_code);
self.transfer_all_possessions_code(&src_loc, &dst_loc).await?;
Ok(())
}
pub async fn find_items_by_location(self: &Self, location: &str) -> DResult<Vec<Arc<Item>>> {
pub async fn find_items_by_location<'a>(self: &'a Self, location: &'a str) -> DResult<Vec<Arc<Item>>> {
Ok(self.pg_trans()?.query(
"SELECT details FROM items WHERE details->>'location' = $1 \
ORDER BY details->>'display'
@ -480,7 +484,7 @@ impl DBTrans {
Ok(())
}
pub async fn delete_item(self: &Self, item_type: &str, item_code: &str) -> DResult<()> {
pub async fn delete_item<'a>(self: &'a Self, item_type: &'a str, item_code: &'a str) -> DResult<()> {
self.pg_trans()?
.execute("DELETE FROM items WHERE \
details->>'item_type' = $1 AND \
@ -489,7 +493,7 @@ impl DBTrans {
Ok(())
}
pub async fn find_session_for_player(self: &Self, item_code: &str) -> DResult<Option<(ListenerSession, Session)>> {
pub async fn find_session_for_player<'a>(self: &'a Self, item_code: &'a str) -> DResult<Option<(ListenerSession, Session)>> {
Ok(self.pg_trans()?
.query_opt("SELECT u.current_listener, u.current_session, s.details \
FROM users u JOIN sessions s ON s.session = u.current_session \
@ -505,7 +509,7 @@ impl DBTrans {
}
pub async fn resolve_items_by_display_name_for_player<'l>(
self: &Self,
self: &'l Self,
search: &'l ItemSearchParams<'l>
) -> DResult<Arc<Vec<Arc<Item>>>> {
let mut ctes: Vec<String> = Vec::new();
@ -587,7 +591,7 @@ impl DBTrans {
}
}
pub async fn delete_task(&self, task_type: &str, task_code: &str) -> DResult<()> {
pub async fn delete_task<'a>(&'a self, task_type: &'a str, task_code: &'a str) -> DResult<()> {
self.pg_trans()?.execute(
"DELETE FROM tasks WHERE details->>'task_type' = $1 AND \
details->>'task_code' = $2", &[&task_type, &task_code]
@ -595,7 +599,7 @@ impl DBTrans {
Ok(())
}
pub async fn upsert_task(&self, task: &Task) -> DResult<()> {
pub async fn upsert_task<'a>(&'a self, task: &'a Task) -> DResult<()> {
self.pg_trans()?.execute(
"INSERT INTO tasks (details) \
VALUES ($1) \
@ -604,7 +608,7 @@ impl DBTrans {
Ok(())
}
pub async fn update_task(&self, task_type: &str, task_code: &str, task: &TaskParse) -> DResult<()> {
pub async fn update_task<'a>(&'a self, task_type: &'a str, task_code: &'a str, task: &'a TaskParse) -> DResult<()> {
self.pg_trans()?.execute(
"UPDATE tasks SET details = $3 WHERE details->>'task_type' = $1 AND \
details->>'task_code' = $2",
@ -681,7 +685,7 @@ impl DBTrans {
Ok(())
}
pub fn pg_trans(self: &Self) -> DResult<&Transaction> {
pub fn pg_trans<'a>(self: &'a Self) -> DResult<&'a Transaction<'a>> {
self.borrow_trans().as_ref().ok_or("Transaction already closed".into())
}
}

View File

@ -13,6 +13,17 @@ pub struct ListenerSession {
pub session: Uuid
}
#[cfg(test)]
impl Default for ListenerSession {
fn default() -> ListenerSession {
use uuid::uuid;
ListenerSession {
listener: uuid!("6f9c9b61-9228-4427-abd7-c4aef127a862"),
session: uuid!("668efb68-79d3-4004-9d6a-1e5757792e1a")
}
}
}
pub async fn handle(listener: Uuid, msg: MessageFromListener, pool: db::DBPool)
-> DResult<()> {
match msg {

View File

@ -1,11 +1,13 @@
use super::ListenerSession;
use crate::DResult;
use crate::db::{DBTrans, DBPool, ItemSearchParams};
use ansi::ansi;
use crate::db::{DBPool, ItemSearchParams};
use mockall_double::double;
#[double] use crate::db::DBTrans;
#[cfg(not(test))] use ansi::ansi;
use phf::phf_map;
use async_trait::async_trait;
use crate::models::{session::Session, user::User, item::Item};
use log::warn;
#[cfg(not(test))] use log::warn;
use once_cell::sync::OnceCell;
use std::sync::Arc;
@ -149,6 +151,7 @@ fn resolve_handler(ctx: &VerbContext, cmd: &str) -> Option<&'static UserVerbRef>
result
}
#[cfg(not(test))]
pub async fn handle(session: &ListenerSession, msg: &str, pool: &DBPool) -> DResult<()> {
let (cmd, params) = parsing::parse_command_name(msg);
let trans = pool.start_transaction().await?;
@ -161,11 +164,11 @@ pub async fn handle(session: &ListenerSession, msg: &str, pool: &DBPool) -> DRes
}
Some(v) => v
};
let mut ctx = VerbContext { session, trans: &trans, session_dat: &mut session_dat,
user_dat: &mut user_dat };
let handler_opt = resolve_handler(&ctx, cmd);
match handler_opt {
None => {
trans.queue_for_session(session,
@ -190,6 +193,11 @@ pub async fn handle(session: &ListenerSession, msg: &str, pool: &DBPool) -> DRes
pool.bump_session_time(&session).await?;
Ok(())
}
#[cfg(test)]
pub async fn handle(_session: &ListenerSession, _msg: &str, _pool: &DBPool) -> DResult<()> {
unimplemented!();
}
pub fn is_likely_explicit(msg: &str) -> bool {
static EXPLICIT_MARKER_WORDS: OnceCell<Vec<&'static str>> =
@ -230,3 +238,22 @@ pub async fn search_item_for_user<'l>(ctx: &'l VerbContext<'l>, search: &'l Item
item1.clone(),
})
}
#[cfg(test)] mod test {
use crate::db::MockDBTrans;
#[test]
fn resolve_handler_finds_unregistered() {
use super::*;
let trans = MockDBTrans::new();
let sess: ListenerSession = Default::default();
let mut user_dat: Option<User> = None;
let mut session_dat: Session = Default::default();
let ctx = VerbContext { session: &sess,
trans: &trans,
session_dat: &mut session_dat,
user_dat: &mut user_dat };
resolve_handler(&ctx, "less_explicit_mode");
}
}

View File

@ -13,7 +13,6 @@ use crate::{
queue_command
},
static_content::room::{self, Direction, ExitType},
db::DBTrans,
models::item::{
Item,
SkillType,
@ -26,6 +25,8 @@ use crate::{
combat::handle_resurrect,
}
};
use mockall_double::double;
#[double] use crate::db::DBTrans;
use std::time;
pub async fn announce_move(trans: &DBTrans, character: &Item, leaving: &Item, arriving: &Item) -> DResult<()> {

View File

@ -4,8 +4,9 @@ use super::{VerbContext, UserVerb, UserVerbRef, UResult, UserError,
use crate::{
models::item::{Item, ItemFlag},
services::broadcast_to_room,
db::DBTrans
};
use mockall_double::double;
#[double] use crate::db::DBTrans;
use async_trait::async_trait;
use ansi::{ignore_special_characters, ansi};

View File

@ -3,22 +3,25 @@ use async_trait::async_trait;
use crate::{
DResult,
db,
models::task::{Task, TaskParse, TaskRecurrence},
models::task::Task,
listener::{ListenerMap, ListenerSend},
static_content::npc,
services::combat,
};
#[cfg(not(test))] use crate::models::task::{TaskParse, TaskRecurrence};
use mockall_double::double;
#[double] use crate::db::DBTrans;
use blastmud_interfaces::MessageToListener;
use log::warn;
use once_cell::sync::OnceCell;
use std::ops::AddAssign;
#[cfg(not(test))] use std::ops::AddAssign;
use std::collections::BTreeMap;
use chrono::Utc;
#[cfg(not(test))] use chrono::Utc;
pub mod queued_command;
pub struct TaskRunContext<'l> {
pub trans: &'l db::DBTrans,
pub trans: &'l DBTrans,
pub task: &'l mut Task
}
@ -112,6 +115,7 @@ fn start_send_queue_task(pool: db::DBPool, listener_map: ListenerMap) {
});
}
#[cfg(not(test))]
async fn process_tasks_once(pool: db::DBPool) -> DResult<()> {
loop {
let tx = pool.start_transaction().await?;
@ -198,6 +202,12 @@ async fn process_tasks_once(pool: db::DBPool) -> DResult<()> {
Ok(())
}
#[cfg(test)]
async fn process_tasks_once(_pool: db::DBPool) -> DResult<()> {
task_handler_registry();
unimplemented!();
}
fn start_task_runner(pool: db::DBPool) {
task::spawn(async move {
loop {

View File

@ -1,8 +1,9 @@
use crate::{
db::DBTrans,
DResult,
models::item::Item,
};
use mockall_double::double;
#[double] use crate::db::DBTrans;
pub mod skills;
pub mod combat;

View File

@ -1,8 +1,9 @@
use crate::{
db::DBTrans,
DResult,
};
use crate::DResult;
use mockall_double::double;
#[double] use crate::db::DBTrans;
#[derive(Debug, PartialEq)]
pub enum CapacityLevel {
Unburdened,
SlightlyBurdened,
@ -34,3 +35,25 @@ pub async fn check_item_capacity(trans: &DBTrans,
}
Ok(CapacityLevel::Unburdened)
}
#[cfg(test)]
mod test {
use crate::db::{
MockDBTrans,
LocationStats
};
use super::*;
#[tokio::test]
async fn check_item_capacity_should_say_above_item_limit_if_over() {
let mut mock_db = MockDBTrans::new();
mock_db.expect_get_location_stats()
.withf(|s| s == "player/foo")
.returning(|_| Ok(LocationStats {
total_count: 49,
total_weight: 100,
}));
assert_eq!(check_item_capacity(&mock_db, "player/foo", 10).await.unwrap(),
CapacityLevel::AboveItemLimit);
}
}

View File

@ -15,8 +15,9 @@ use crate::{
message_handler::user_commands::{user_error, UResult},
regular_tasks::{TaskRunContext, TaskHandler},
DResult,
db::DBTrans,
};
use mockall_double::double;
#[double] use crate::db::DBTrans;
use async_trait::async_trait;
use chrono::Utc;
use async_recursion::async_recursion;

View File

@ -3,12 +3,13 @@ use crate::{
item::{Item, SkillType, StatType, BuffImpact},
user::User
},
db::DBTrans,
DResult,
};
use rand::{self, Rng};
use chrono::Utc;
use std::collections::BTreeMap;
use mockall_double::double;
#[double] use crate::db::DBTrans;
pub fn calculate_total_stats_skills_for_user(target_item: &mut Item, user: &User) {
target_item.total_stats = BTreeMap::new();