Skip to content

Commit

Permalink
[DoD] Fix the third (smart) mode
Browse files Browse the repository at this point in the history
…finish and refactor the tests.
  • Loading branch information
Leonid Kozarin committed Sep 15, 2024
1 parent aada05c commit 0b35dc6
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 149 deletions.

This file was deleted.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 16 additions & 7 deletions src/repo/test/dicks.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use num_traits::ToPrimitive;
use sqlx::{Pool, Postgres};
use teloxide::types::{ChatId, UserId};
use testcontainers::clients;
Expand Down Expand Up @@ -150,13 +151,21 @@ pub async fn create_user(db: &Pool<Postgres>) {
}

pub async fn create_user_and_dick_2(db: &Pool<Postgres>, chat_id: &ChatIdPartiality, name: &str) {
let users = repo::Users::new(db.clone());
let dicks = repo::Dicks::new(db.clone(), Default::default());
let uid2 = UserId((UID + 1) as u64);
users.create_or_update(uid2, name)
.await.expect("couldn't create a user #2");
dicks.create_or_grow(uid2, chat_id, 1)
.await.expect("couldn't create a dick #2");
create_another_user_and_dick(db, chat_id, 2, name, 1).await;
}

pub async fn create_another_user_and_dick(db: &Pool<Postgres>, chat_id: &ChatIdPartiality,
n: u8, name: &str, increment: i32) {
assert!(n > 1);
let n = n.to_i64().expect("couldn't convert n to i64");

let users = repo::Users::new(db.clone());
let dicks = repo::Dicks::new(db.clone(), Default::default());
let uid2 = UserId((UID + n - 1) as u64);
users.create_or_update(uid2, name)
.await.unwrap_or_else(|_| panic!("couldn't create a user #{n}"));
dicks.create_or_grow(uid2, chat_id, increment)
.await.unwrap_or_else(|_| panic!("couldn't create a dick #{n}"));
}

pub async fn create_dick(db: &Pool<Postgres>) {
Expand Down
179 changes: 92 additions & 87 deletions src/repo/test/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ use teloxide::types::{ChatId, UserId};
use testcontainers::clients;
use crate::domain::Ratio;
use crate::repo;
use crate::repo::ChatIdKind;
use crate::repo::{ChatIdKind, ChatIdPartiality};
use crate::repo::test::{CHAT_ID, NAME, start_postgres, UID};
use crate::repo::test::dicks::{create_another_user_and_dick, create_user_and_dick_2};

#[tokio::test]
async fn create_or_update() {
Expand Down Expand Up @@ -53,113 +54,117 @@ async fn get_chat_members() {
check_member_with_name(&members, NAME);
}

macro_rules! base_checks {
($db:ident, $method:ident) => {
base_checks!($db, $method,)
};
($db:ident, $method:ident, $($args:tt),*) => {
let users = repo::Users::new($db.clone());

let chat_id = ChatIdKind::ID(ChatId(CHAT_ID));
let user = users.$method(&chat_id$(,$args)*)
.await.expect("couldn't fetch None");
assert!(user.is_none());

create_member(&$db).await;

let user = users.$method(&chat_id$(,$args)*)
.await
.expect("couldn't fetch Some(User)")
.expect("no active member");
assert_eq!(user.uid, UID);
assert_eq!(user.name.value_ref(), NAME);

// check inactive member is not found
sqlx::query!("DROP TRIGGER IF EXISTS trg_check_and_update_dicks_timestamp ON Dicks")
.execute(&$db)
.await.expect("couldn't drop the trigger");
sqlx::query!("UPDATE Dicks SET updated_at = '1997-01-01' WHERE chat_id = (SELECT id FROM Chats WHERE chat_id = $1) AND uid = $2", CHAT_ID, UID)
.execute(&$db)
.await.expect("couldn't reset the updated_at column");

let user = users.$method(&chat_id$(,$args)*)
.await
.expect("couldn't fetch Some(User)");
assert!(user.is_none());

sqlx::query!("UPDATE Dicks SET updated_at = now() WHERE chat_id = (SELECT id FROM Chats WHERE chat_id = $1) AND uid = $2", CHAT_ID, UID)
.execute(&$db)
.await.expect("couldn't rollback the updated_at column");
};
}

#[tokio::test]
async fn get_random_active_member() {
let docker = clients::Cli::default();
let (_container, db) = start_postgres(&docker).await;
let users = repo::Users::new(db.clone());

let chat_id = ChatIdKind::ID(ChatId(CHAT_ID));
let user = users.get_random_active_member(&chat_id)
.await.expect("couldn't fetch None");
assert!(user.is_none());

create_member(&db).await;

let user = users.get_random_active_member(&chat_id)
.await
.expect("couldn't fetch Some(User)")
.expect("no active member");
assert_eq!(user.uid, UID);
assert_eq!(user.name.value_ref(), NAME);

// check inactive member is not found
sqlx::query!("DROP TRIGGER IF EXISTS trg_check_and_update_dicks_timestamp ON Dicks")
.execute(&db)
.await.expect("couldn't drop the trigger");
sqlx::query!("UPDATE Dicks SET updated_at = '1997-01-01' WHERE chat_id = (SELECT id FROM Chats WHERE chat_id = $1) AND uid = $2", CHAT_ID, UID)
.execute(&db)
.await.expect("couldn't reset the updated_at column");

let user = users.get_random_active_member(&chat_id)
.await
.expect("couldn't fetch Some(User)");
assert!(user.is_none());
base_checks!(db, get_random_active_member);
}

#[tokio::test]
async fn get_random_active_poor_member() {
let docker = clients::Cli::default();
let (_container, db) = start_postgres(&docker).await;
let users = repo::Users::new(db.clone());
let ratio = Ratio::new(0.9).unwrap();
base_checks!(db, get_random_active_poor_member, ratio);

let chat_id = ChatIdKind::ID(ChatId(CHAT_ID));
let user = users.get_random_active_poor_member(&chat_id, ratio)
.await.expect("couldn't fetch None");
assert!(user.is_none());

create_member(&db).await;

let user = users.get_random_active_poor_member(&chat_id, ratio)
.await
.expect("couldn't fetch Some(User)")
.expect("no active member");
assert_eq!(user.uid, UID);
assert_eq!(user.name.value_ref(), NAME);

// check inactive member is not found
sqlx::query!("DROP TRIGGER IF EXISTS trg_check_and_update_dicks_timestamp ON Dicks")
.execute(&db)
.await.expect("couldn't drop the trigger");
sqlx::query!("UPDATE Dicks SET updated_at = '1997-01-01' WHERE chat_id = (SELECT id FROM Chats WHERE chat_id = $1) AND uid = $2", CHAT_ID, UID)
.execute(&db)
.await.expect("couldn't reset the updated_at column");

let user = users.get_random_active_poor_member(&chat_id, ratio)
.await
.expect("couldn't fetch Some(User)");
assert!(user.is_none());

// TODO: create multiple users and check the top one is not chosen
// TODO: rewrite these tests to comply with the DRY principle
// create middle-class and rich users and ensure they will never be selected as a winner
let (users, chat_id) = prepare_for_additional_tests(&db).await;

for attempt in 1..=10 {
let user = users.get_random_active_poor_member(&chat_id.kind(), ratio)
.await
.unwrap_or_else(|_| panic!("couldn't fetch poor active user on attempt {attempt}"))
.unwrap_or_else(| | panic!("nobody has been found on attempt {attempt}"));
assert_ne!(user.uid, UID+2);
}
}

#[tokio::test]
async fn get_random_active_member_with_poor_in_priority() {
let docker = clients::Cli::default();
let (_container, db) = start_postgres(&docker).await;
let users = repo::Users::new(db.clone());
base_checks!(db, get_random_active_member_with_poor_in_priority);

let chat_id = ChatIdKind::ID(ChatId(CHAT_ID));
let user = users.get_random_active_member_with_poor_in_priority(&chat_id)
.await.expect("couldn't fetch None");
assert!(user.is_none());
// create middle-class and rich users and ensure the chance they win is the lower, the longer their dicks are
let (users, chat_id) = prepare_for_additional_tests(&db).await;

let mut results = Vec::with_capacity(20);
for attempt in 1..=100 {
let user = users.get_random_active_member_with_poor_in_priority(&chat_id.kind())
.await
.unwrap_or_else(|_| panic!("couldn't fetch poor active user on attempt {attempt}"))
.unwrap_or_else(| | {
panic!("nobody has been found on attempt {attempt}")
});
results.push(user.uid);
}
let user_1_wins = count(&results, UID);
let user_2_wins = count(&results, UID+1);
let user_3_wins = count(&results, UID+2);

println!("=== DoD wins using smart mode ===");
println!("User #1: {user_1_wins}");
println!("User #2: {user_2_wins}");
println!("User #3: {user_3_wins}");

assert!(user_1_wins > user_2_wins);
assert!(user_2_wins > user_3_wins);
assert!(user_3_wins > 0);
}

create_member(&db).await;
async fn prepare_for_additional_tests(db: &Pool<Postgres>) -> (repo::Users, ChatIdPartiality) {
let users = repo::Users::new(db.clone());
let chat_id = ChatId(CHAT_ID).into();
create_user_and_dick_2(db, &chat_id, "User-2").await;
create_another_user_and_dick(db, &chat_id, 3, "User-3", 10).await;
(users, chat_id)
}

let user = users.get_random_active_member_with_poor_in_priority(&chat_id)
.await
.expect("couldn't fetch Some(User)")
.expect("no active member");
assert_eq!(user.uid, UID);
assert_eq!(user.name.value_ref(), NAME);

// check inactive member is not found
sqlx::query!("DROP TRIGGER IF EXISTS trg_check_and_update_dicks_timestamp ON Dicks")
.execute(&db)
.await.expect("couldn't drop the trigger");
sqlx::query!("UPDATE Dicks SET updated_at = '1997-01-01' WHERE chat_id = (SELECT id FROM Chats WHERE chat_id = $1) AND uid = $2", CHAT_ID, UID)
.execute(&db)
.await.expect("couldn't reset the updated_at column");

let user = users.get_random_active_member_with_poor_in_priority(&chat_id)
.await
.expect("couldn't fetch Some(User)");
assert!(user.is_none());

// TODO: think how to check the probability in the test
// TODO: rewrite these tests to comply with the DRY principle
fn count(v: &[i64], uid: i64) -> usize {
v.iter()
.filter(|u| **u == uid)
.count()
}

fn check_user_with_name(user: &repo::User, name: &str) {
Expand Down
38 changes: 17 additions & 21 deletions src/repo/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,31 +75,27 @@ repository!(Users,
sqlx::query_as!(User,
"WITH user_weights AS (
SELECT u.uid, u.name, u.created_at, d.length,
CASE
WHEN d.length = 0 THEN 1.0
ELSE 1.0 / d.length
END AS weight,
SUM(CASE
WHEN d.length = 0 THEN 1.0
ELSE 1.0 / d.length
END) OVER () AS total_weight
1.0 / LOG(d.length + 2) AS weight -- Logarithmic transformation to smooth the weights
FROM Users u
JOIN Dicks d USING (uid)
JOIN Chats c ON d.chat_id = c.id
JOIN Dicks d USING (uid)
JOIN Chats c ON d.chat_id = c.id
WHERE (c.chat_id = $1::bigint OR c.chat_instance = $1::text)
AND d.updated_at > current_timestamp - interval '1 week'
AND d.updated_at > current_timestamp - interval '1 week'
),
weighted_users AS (
SELECT uid, name, created_at, weight,
total_weight,
(RANDOM() * total_weight) AS rand_weight
FROM user_weights
)
cumulative_weights AS (
SELECT uid, name, created_at, weight,
SUM(weight) OVER (ORDER BY uid) AS cumulative_weight, -- Cumulative weight
SUM(weight) OVER () AS total_weight
FROM user_weights
),
random_value AS (
SELECT RANDOM() * (SELECT total_weight FROM cumulative_weights LIMIT 1) AS rand_value -- Generate one random value
)
SELECT uid, name, created_at
FROM weighted_users
WHERE rand_weight < weight
ORDER BY random()
LIMIT 1",
FROM cumulative_weights, random_value
WHERE cumulative_weight >= random_value.rand_value
ORDER BY cumulative_weight
LIMIT 1; -- Select the first user whose cumulative weight exceeds the random value",
chat_id.value() as String)
.fetch_optional(&self.pool)
.await
Expand Down

0 comments on commit 0b35dc6

Please sign in to comment.