diff --git a/src/api/v1/stats.rs b/src/api/v1/stats.rs index 80fe069f..83130bfe 100644 --- a/src/api/v1/stats.rs +++ b/src/api/v1/stats.rs @@ -32,82 +32,89 @@ pub mod routes { } } -/// Get difficulty factor with max time limit for percentile of stats -#[my_codegen::post(path = "crate::V1_API_ROUTES.stats.percentile_benches")] -async fn percentile_benches( - data: AppData, - payload: web::Json, -) -> ServiceResult { - let count = data.db.stats_get_num_logs_under_time(payload.time).await?; +pub async fn percentile_bench_runner( + data: &AppData, + req: &PercentileReq, +) -> ServiceResult { + let count = data.db.stats_get_num_logs_under_time(req.time).await?; if count == 0 { - return Ok(HttpResponse::Ok().json(PercentileResp { + return Ok(PercentileResp { difficulty_factor: None, - })); + }); } if count < 2 { - return Ok(HttpResponse::Ok().json(PercentileResp { + return Ok(PercentileResp { difficulty_factor: None, - })); + }); } - let location = ((count - 1) as f64 * (payload.percentile / 100.00)) + 1.00; + let location = ((count - 1) as f64 * (req.percentile / 100.00)) + 1.00; let fraction = location - location.floor(); if fraction > 0.00 { if let (Some(base), Some(ceiling)) = ( data.db .stats_get_entry_at_location_for_time_limit_asc( - payload.time, + req.time, location.floor() as u32, ) .await?, data.db .stats_get_entry_at_location_for_time_limit_asc( - payload.time, + req.time, location.floor() as u32 + 1, ) .await?, ) { let res = base as u32 + ((ceiling - base) as f64 * fraction).floor() as u32; - return Ok(HttpResponse::Ok().json(PercentileResp { + return Ok(PercentileResp { difficulty_factor: Some(res), - })); + }); } } else { if let Some(base) = data .db .stats_get_entry_at_location_for_time_limit_asc( - payload.time, + req.time, location.floor() as u32, ) .await? { let res = base as u32; - return Ok(HttpResponse::Ok().json(PercentileResp { + return Ok(PercentileResp { difficulty_factor: Some(res), - })); + }); } }; - Ok(HttpResponse::Ok().json(PercentileResp { + Ok(PercentileResp { difficulty_factor: None, - })) + }) +} + +/// Get difficulty factor with max time limit for percentile of stats +#[my_codegen::post(path = "crate::V1_API_ROUTES.stats.percentile_benches")] +async fn percentile_benches( + data: AppData, + payload: web::Json, +) -> ServiceResult { + Ok(HttpResponse::Ok().json(percentile_bench_runner(&data, &payload).await?)) } #[derive(Clone, Debug, Deserialize, Builder, Serialize)] /// Health check return datatype pub struct PercentileReq { - time: u32, - percentile: f64, + pub time: u32, + pub percentile: f64, } #[derive(Clone, Debug, Deserialize, Builder, Serialize)] /// Health check return datatype pub struct PercentileResp { - difficulty_factor: Option, + pub difficulty_factor: Option, } pub fn services(cfg: &mut web::ServiceConfig) { diff --git a/src/pages/panel/mod.rs b/src/pages/panel/mod.rs index 3b2f285f..b67dbe33 100644 --- a/src/pages/panel/mod.rs +++ b/src/pages/panel/mod.rs @@ -10,6 +10,7 @@ use sailfish::TemplateOnce; mod notifications; mod settings; pub mod sitekey; +mod utils; use db_core::Captcha; @@ -47,18 +48,21 @@ pub fn services(cfg: &mut actix_web::web::ServiceConfig) { cfg.service(panel); settings::services(cfg); sitekey::services(cfg); + utils::services(cfg); cfg.service(notifications::notifications); } pub mod routes { use super::settings::routes::Settings; use super::sitekey::routes::Sitekey; + use super::utils::routes::Utils; pub struct Panel { pub home: &'static str, pub sitekey: Sitekey, pub notifications: &'static str, pub settings: Settings, + pub utils: Utils, } impl Panel { @@ -68,10 +72,11 @@ pub mod routes { sitekey: Sitekey::new(), notifications: "/notifications", settings: Settings::new(), + utils: Utils::new(), } } - pub const fn get_sitemap() -> [&'static str; 5] { + pub const fn get_sitemap() -> [&'static str; 6] { const PANEL: Panel = Panel::new(); const S: [&str; 2] = Sitekey::get_sitemap(); @@ -81,6 +86,7 @@ pub mod routes { S[0], S[1], Settings::get_sitemap()[0], + Utils::get_sitemap()[0], ] } } diff --git a/src/pages/panel/utils.rs b/src/pages/panel/utils.rs new file mode 100644 index 00000000..c7e4912e --- /dev/null +++ b/src/pages/panel/utils.rs @@ -0,0 +1,280 @@ +// Copyright (C) 2024 Aravinth Manivannan +// SPDX-FileCopyrightText: 2023 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use actix_identity::Identity; +use actix_web::{web, HttpResponse, Responder}; +use sailfish::TemplateOnce; + +use crate::api::v1::stats::{percentile_bench_runner, PercentileReq, PercentileResp}; +use crate::errors::PageResult; +use crate::pages::auth::sudo::SudoPage; +use crate::AppData; + +pub mod routes { + pub struct Utils { + pub percentile: &'static str, + } + + impl Utils { + pub const fn new() -> Self { + Utils { + percentile: "/utils/percentile", + } + } + + pub const fn get_sitemap() -> [&'static str; 1] { + const S: Utils = Utils::new(); + [S.percentile] + } + } +} + +pub fn services(cfg: &mut actix_web::web::ServiceConfig) { + cfg.service(get_percentile); + cfg.service(post_percentile); +} + +const PAGE: &str = "Difficulty factor statistics"; + +#[derive(TemplateOnce, Clone)] +#[template(path = "panel/utils/percentile/index.html")] +pub struct PercentilePage { + time: Option, + percentile: Option, + difficulty_factor: Option, +} + +#[my_codegen::get( + path = "crate::PAGES.panel.utils.percentile", + wrap = "crate::pages::get_middleware()" +)] +async fn get_percentile(id: Identity) -> PageResult { + let data = PercentilePage { + time: None, + percentile: None, + difficulty_factor: None, + }; + + let body = data.render_once().unwrap(); + Ok(HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body(body)) +} + +#[my_codegen::post( + path = "crate::PAGES.panel.utils.percentile", + wrap = "crate::pages::get_middleware()" +)] +async fn post_percentile( + data: AppData, + id: Identity, + payload: web::Form, +) -> PageResult { + let resp = percentile_bench_runner(&data, &payload).await?; + let page = PercentilePage { + time: Some(payload.time), + percentile: Some(payload.percentile), + difficulty_factor: resp.difficulty_factor, + }; + + let body = page.render_once().unwrap(); + Ok(HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body(body)) +} + +#[cfg(test)] +mod tests { + use actix_web::{http::StatusCode, test, web::Bytes, App}; + + use super::*; + use crate::api::v1::services; + use crate::*; + + #[actix_rt::test] + async fn page_stats_bench_work_pg() { + let data = crate::tests::pg::get_data().await; + page_stats_bench_work(data).await; + } + + #[actix_rt::test] + async fn page_stats_bench_work_maria() { + let data = crate::tests::maria::get_data().await; + page_stats_bench_work(data).await; + } + + async fn page_stats_bench_work(data: ArcData) { + use crate::tests::*; + + const NAME: &str = "pagebenchstatsuesr"; + const EMAIL: &str = "pagebenchstatsuesr@testadminuser.com"; + const PASSWORD: &str = "longpassword2"; + + const DEVICE_USER_PROVIDED: &str = "foo"; + const DEVICE_SOFTWARE_RECOGNISED: &str = "Foobar.v2"; + const THREADS: i32 = 4; + + let data = &data; + { + delete_user(&data, NAME).await; + } + + register_and_signin(data, NAME, EMAIL, PASSWORD).await; + // create captcha + let (_, signin_resp, key) = add_levels_util(data, NAME, PASSWORD).await; + let app = get_app!(data).await; + let cookies = get_cookie!(signin_resp); + + let page = 1; + let tmp_id = uuid::Uuid::new_v4(); + let download_rotue = V1_API_ROUTES + .survey + .get_download_route(&tmp_id.to_string(), page); + + let download_req = test::call_service( + &app, + test::TestRequest::get().uri(&download_rotue).to_request(), + ) + .await; + assert_eq!(download_req.status(), StatusCode::NOT_FOUND); + + data.db + .analytics_create_psuedo_id_if_not_exists(&key.key) + .await + .unwrap(); + + let psuedo_id = data + .db + .analytics_get_psuedo_id_from_capmaign_id(&key.key) + .await + .unwrap(); + + for i in 1..6 { + println!("[{i}] Saving analytics"); + let analytics = db_core::CreatePerformanceAnalytics { + time: i, + difficulty_factor: i, + worker_type: "wasm".into(), + }; + data.db.analysis_save(&key.key, &analytics).await.unwrap(); + } + + let msg = PercentileReq { + time: 1, + percentile: 99.00, + }; + let resp = test::call_service( + &app, + post_request!(&msg, V1_API_ROUTES.stats.percentile_benches).to_request(), + ) + .await; + assert_eq!(resp.status(), StatusCode::OK); + let resp: PercentileResp = test::read_body_json(resp).await; + + assert!(resp.difficulty_factor.is_none()); + + let msg = PercentileReq { + time: 1, + percentile: 100.00, + }; + + let resp = test::call_service( + &app, + post_request!(&msg, V1_API_ROUTES.stats.percentile_benches).to_request(), + ) + .await; + assert_eq!(resp.status(), StatusCode::OK); + let resp: PercentileResp = test::read_body_json(resp).await; + + // start + let percentile_resp = test::call_service( + &app, + test::TestRequest::get() + .uri(&crate::PAGES.panel.utils.percentile) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + + assert_eq!(percentile_resp.status(), StatusCode::OK); + + let body: Bytes = test::read_body(percentile_resp).await; + let body = String::from_utf8(body.to_vec()).unwrap(); + + assert!(body.contains("Maximum time taken")); + + let percentile_resp = test::call_service( + &app, + test::TestRequest::get() + .uri(&crate::PAGES.panel.utils.percentile) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + + assert_eq!(percentile_resp.status(), StatusCode::OK); + + let body: Bytes = test::read_body(percentile_resp).await; + let body = String::from_utf8(body.to_vec()).unwrap(); + + assert!(body.contains("Maximum time taken")); + + // end + // start post + + let msg = PercentileReq { + time: 1, + percentile: 99.00, + }; + + let percentile_resp = test::call_service( + &app, + test::TestRequest::post() + .uri(&crate::PAGES.panel.utils.percentile) + .set_form(&msg) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + + assert_eq!(percentile_resp.status(), StatusCode::OK); + + let body: Bytes = test::read_body(percentile_resp).await; + let body = String::from_utf8(body.to_vec()).unwrap(); + + assert!(body.contains( + "Not enough inputs to compute statistics. Please try again later" + )); + assert!(body.contains(&1.to_string())); + assert!(body.contains(&99.00.to_string())); + // end post + + // start post + + let msg = PercentileReq { + time: 2, + percentile: 100.00, + }; + + let percentile_resp = test::call_service( + &app, + test::TestRequest::post() + .uri(&crate::PAGES.panel.utils.percentile) + .set_form(&msg) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + + assert_eq!(percentile_resp.status(), StatusCode::OK); + + let body: Bytes = test::read_body(percentile_resp).await; + let body = String::from_utf8(body.to_vec()).unwrap(); + + assert!(body.contains("Difficulty factor: 2")); + assert!(body.contains(&2.to_string())); + assert!(body.contains(&100.00.to_string())); + } +} diff --git a/templates/panel/navbar/index.html b/templates/panel/navbar/index.html index bc1d3b55..f748815a 100644 --- a/templates/panel/navbar/index.html +++ b/templates/panel/navbar/index.html @@ -5,15 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-or-later -->