From 18510f6af9df84dbb23bebeb8e60dff6ffc87c6c Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Mon, 11 Apr 2022 13:44:06 +0300 Subject: [PATCH 01/15] WIP tests for units-for-slot route --- sentry/src/routes/units_for_slot_test.rs | 761 +++++++++++++++++++++++ 1 file changed, 761 insertions(+) create mode 100644 sentry/src/routes/units_for_slot_test.rs diff --git a/sentry/src/routes/units_for_slot_test.rs b/sentry/src/routes/units_for_slot_test.rs new file mode 100644 index 000000000..fdc496169 --- /dev/null +++ b/sentry/src/routes/units_for_slot_test.rs @@ -0,0 +1,761 @@ +use crate::{platform::PlatformApi, test_util::setup_dummy_app}; + +use super::*; +use adapter::Dummy; +use chrono::{DateTime, TimeZone, Utc}; +use hyper::Body; +use hyper::{ + body::Bytes, + http::{header::USER_AGENT, request::Request}, +}; +use primitives::{ + campaign::Pricing, + platform::AdUnitsResponse, + supermarket::units_for_slot::response::{Campaign as ResponseCampaign, UnitsWithPrice}, + targeting::Rules, + targeting::{input, Function, Rule, Value}, + test_util::{DUMMY_AD_UNITS, DUMMY_CAMPAIGN, IDS, LEADER_2, PUBLISHER, PUBLISHER_2}, + AdSlot, BigNum, Channel, ChannelId, +}; +use reqwest::Url; +use std::{collections::HashMap, iter::Iterator, str::FromStr}; +use wiremock::{ + matchers::{method, path}, + Mock, MockServer, ResponseTemplate, +}; + +// User Agent OS: Linux (only in `woothee`) +// User Agent Browser Family: Firefox +const TEST_USER_AGENT: &str = + "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:83.0) Gecko/20100101 Firefox/83.0"; +// uses two-letter country codes: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 +const TEST_CLOUDFLARE_IPCOUNTY: &str = "BG"; + +/// Uses the Channel AdUnits as UnitsWithPrice for the response +fn get_mock_campaign(campaign: Campaign) -> ResponseCampaign { + let units_with_price = get_units_with_price(&campaign); + ResponseCampaign { + campaign: ResponseCampaign::from(campaign), + units_with_price, + } +} + +fn get_units_with_price(campaign: &Campaign) -> Vec { + campaign + .ad_units + .iter() + .map(|u| UnitsWithPrice { + unit: u.into(), + price: campaign + .pricing_bounds + .get(&IMPRESSION) + .expect("Campaign should have Pricing Bounds for impression") + .min, + }) + .collect() +} + +fn get_mock_rules(categories: &[&str]) -> Vec { + let get_rule = Function::new_get("adSlot.categories"); + let categories_array = Value::Array(categories.iter().map(|s| Value::new_string(s)).collect()); + let intersects_rule = Function::new_intersects(get_rule, categories_array); + vec![Function::new_only_show_if(intersects_rule).into()] +} + +fn get_test_ad_slot(rules: &[Rule], categories: &[&str]) -> AdSlot { + AdSlot { + // TODO: Replace with IPFS for testing + ipfs: "QmVwXu9oEgYSsL6G1WZtUQy6dEReqs3Nz9iaW4Cq5QLV8C" + .parse() + .expect("Valid IPFS"), + ad_type: "legacy_250x250".to_string(), + archived: false, + created: Utc.timestamp(1_564_383_600, 0), + description: Some("Test slot for running integration tests".to_string()), + fallback_unit: None, + min_per_impression: Some( + vec![( + "0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359" + .parse() + .expect("Valid Address"), + // 0.0007 + 70000.into(), + )] + .into_iter() + .collect(), + ), + modified: Some(Utc.timestamp(1_564_383_600, 0)), + owner: IDS[PUBLISHER], + title: Some("Test slot 1".to_string()), + website: Some("https://adex.network".to_string()), + rules: rules.to_vec(), + } + + // AdSlotResponse { + // slot: ad_slot, + // accepted_referrers: vec![ + // Url::from_str("https://adex.network").expect("should parse"), + // Url::from_str("https://www.adex.network").expect("should parse"), + // ], + // categories: categories.iter().map(|s| String::from(*s)).collect(), + // alexa_rank: Some(1337.0), + // } +} + +/// `seconds_since_epoch` should be set from the actual response, +/// this ensures that the timestamp will always match in the tests, +/// otherwise random tests will fail with +- 1-2-3 seconds difference +fn get_expected_response( + campaigns: Vec, + seconds_since_epoch: DateTime, +) -> UnitsForSlotResponse { + let targeting_input_base = Input { + ad_view: None, + global: input::Global { + ad_slot_id: "QmVwXu9oEgYSsL6G1WZtUQy6dEReqs3Nz9iaW4Cq5QLV8C" + .parse() + .expect("Valid IPFS"), + ad_slot_type: "legacy_250x250".to_string(), + publisher_id: *PUBLISHER, + country: Some(TEST_CLOUDFLARE_IPCOUNTY.to_string()), + event_type: IMPRESSION, + seconds_since_epoch, + user_agent_os: Some("Linux".to_string()), + user_agent_browser_family: Some("Firefox".to_string()), + }, + ad_unit_id: None, + balances: None, + campaign: None, + ad_slot: Some(input::AdSlot { + categories: vec!["IAB3".into(), "IAB13-7".into(), "IAB5".into()], + hostname: "adex.network".to_string(), + alexa_rank: Some(1337.0), + }), + }; + + UnitsForSlotResponse { + targeting_input_base: targeting_input_base.into(), + accepted_referrers: vec![], + campaigns, + fallback_unit: None, + } +} + +fn mock_campaign(rules: &[Rule]) -> Campaign { + let mut campaign = DUMMY_CAMPAIGN.clone(); + + campaign.ad_units = DUMMY_AD_UNITS.to_vec(); + // NOTE: always set the spec.targeting_rules first + campaign.targeting_rules = Rules(rules.to_vec()); + // override pricing for `IMPRESSION` + campaign.pricing_bounds.insert( + IMPRESSION, + Pricing { + // 0.0001 + min: 10_000.into(), + // 0.001 + max: 100_000.into(), + }, + ); + // Timestamp: 1_606_136_400_000 + campaign.active.from = Some(Utc.ymd(2020, 11, 23).and_hms(15, 0, 0)); + + campaign +} + +// fn mock_cache_campaign(channel: Channel, status: Status) -> HashMap { +// let mut campaigns = HashMap::new(); + +// let mut campaign = Campaign { +// channel, +// status, +// balances: Default::default(), +// }; +// campaign +// .balances +// .insert(*PUBLISHER, 100_000_000_000_000.into()); + +// campaigns.insert(campaign.channel.id, campaign); +// campaigns +// } + +/// Assumes all `Campaign`s are `Active` +/// adds to Balances the `Publisher` address with `1 * 10^14` balance +// fn mock_multiple_cache_campaigns(channels: Vec) -> HashMap { +// let mut campaigns = HashMap::new(); + +// for channel in channels { +// let mut campaign = Campaign { +// channel, +// status: Status::Active, +// balances: Default::default(), +// }; +// campaign +// .balances +// .insert(*PUBLISHER, 100_000_000_000_000.into()); + +// campaigns.insert(campaign.channel.id, campaign); +// } + +// campaigns +// } + +/// Sets platform at the `{server_uri}/platform` path of the [`MockServer`] +pub async fn init_app_with_mocked_platform(server: &MockServer) -> Application { + let platform_url = (server.uri() + "/platform").parse().unwrap(); + let mut dummy_app = setup_dummy_app().await; + + let platform_api = + PlatformApi::new(platform_url, dummy_app.config.platform.keep_alive_interval) + .expect("should build test PlatformApi"); + // override the Dummy app PlatformApi + dummy_app.platform_api = platform_api; + + dummy_app +} + +#[tokio::test] +async fn targeting_input() { + let server = MockServer::start().await; + + let app = init_app_with_mocked_platform(&server).await; + + let categories: [&str; 3] = ["IAB3", "IAB13-7", "IAB5"]; + let rules = get_mock_rules(&categories); + let campaign = mock_campaign(&rules); + + let platform_ad_units = AdUnitsResponse(campaign.ad_units.clone()); + let mock_slot = get_test_ad_slot(&rules, &categories); + + Mock::given(method("GET")) + .and(path("/platform/units")) + .respond_with(ResponseTemplate::new(200).set_body_json(&platform_ad_units)) + .mount(&server) + .await; + + let request = Request::post("/units-for-slot") + .header(USER_AGENT, TEST_USER_AGENT) + .header(CLOUDFLARE_IPCOUNTY_HEADER.clone(), TEST_CLOUDFLARE_IPCOUNTY) + .body( + serde_json::to_vec(&RequestBody { + ad_slot: mock_slot, + deposit_assets: Some(vec![campaign.channel.token].into_iter().collect()), + }) + .expect("Should serialize"), + ) + .unwrap(); + + let actual_response = post_units_for_slot(request, &app) + .await + .expect("call shouldn't fail with provided data"); + + assert_eq!(StatusCode::OK, actual_response.status()); + + let units_for_slot: UnitsForSlotResponse = + serde_json::from_slice(&hyper::body::to_bytes(actual_response).await.unwrap()) + .expect("Should deserialize"); + + // we must use the same timestamp as the response, otherwise our tests will fail randomly + let expected_response = get_expected_response( + vec![campaign], + units_for_slot + .targeting_input_base + .global + .seconds_since_epoch + .clone(), + ); + + pretty_assertions::assert_eq!( + expected_response.targeting_input_base, + units_for_slot.targeting_input_base + ); + + assert_eq!( + expected_response.campaigns.len(), + units_for_slot.campaigns.len() + ); + assert_eq!(expected_response.campaigns, units_for_slot.campaigns); + assert_eq!( + expected_response.fallback_unit, + units_for_slot.fallback_unit + ); +} + +#[tokio::test] +async fn non_active_campaign() { + let server = MockServer::start().await; + + let app = init_app_with_mocked_platform(&server).await; + + let categories: [&str; 3] = ["IAB3", "IAB13-7", "IAB5"]; + let rules = get_mock_rules(&categories); + let campaign = mock_campaign(&rules); + + let platform_ad_units = AdUnitsResponse(campaign.ad_units); + let mock_slot = get_test_ad_slot(&rules, &categories); + + Mock::given(method("GET")) + .and(path("/platform/units")) + .respond_with(ResponseTemplate::new(200).set_body_json(&platform_ad_units)) + .mount(&server) + .await; + + let request = Request::post("/units-for-slot") + .header(USER_AGENT, TEST_USER_AGENT) + .header(CLOUDFLARE_IPCOUNTY_HEADER.clone(), TEST_CLOUDFLARE_IPCOUNTY) + .body( + serde_json::to_vec(&RequestBody { + ad_slot: mock_slot, + deposit_assets: Some(vec![campaign.channel.token].into_iter().collect()), + }) + .expect("Should serialize"), + ) + .unwrap(); + + let actual_response = post_units_for_slot(request, &app) + .await + .expect("call shouldn't fail with provided data"); + + assert_eq!(StatusCode::OK, actual_response.status()); + + let units_for_slot: UnitsForSlotResponse = + serde_json::from_slice(&hyper::body::to_bytes(actual_response).await.unwrap()) + .expect("Should deserialize"); + + // we must use the same timestamp as the response, otherwise our tests will fail randomly + let expected_response = get_expected_response( + vec![], + units_for_slot + .targeting_input_base + .global + .seconds_since_epoch + .clone(), + ); + + pretty_assertions::assert_eq!( + expected_response.targeting_input_base, + units_for_slot.targeting_input_base + ); + + assert_eq!( + expected_response.campaigns.len(), + units_for_slot.campaigns.len() + ); + assert_eq!(expected_response.campaigns, units_for_slot.campaigns); + assert_eq!( + expected_response.fallback_unit, + units_for_slot.fallback_unit + ); +} + +#[tokio::test] +async fn creator_is_publisher() { + let server = MockServer::start().await; + + let app = init_app_with_mocked_platform(&server).await; + + let categories: [&str; 3] = ["IAB3", "IAB13-7", "IAB5"]; + let rules = get_mock_rules(&categories); + let mut campaign = mock_campaign(&rules); + campaign.creator = *PUBLISHER; + + let platform_ad_units = AdUnitsResponse(campaign.ad_units.clone()); + let mock_slot = get_test_ad_slot(&rules, &categories); + + Mock::given(method("GET")) + .and(path("/platform/units")) + .respond_with(ResponseTemplate::new(200).set_body_json(&platform_ad_units)) + .mount(&server) + .await; + + let request = Request::post("/units-for-slot") + .header(USER_AGENT, TEST_USER_AGENT) + .header(CLOUDFLARE_IPCOUNTY_HEADER.clone(), TEST_CLOUDFLARE_IPCOUNTY) + .body( + Bytes::from_static( + &serde_json::to_vec(&RequestBody { + ad_slot: mock_slot, + deposit_assets: Some(vec![campaign.channel.token].into_iter().collect()), + }) + .expect("Should serialize"), + ) + .into(), + ) + .unwrap(); + + let actual_response = post_units_for_slot(request, &app) + .await + .expect("call shouldn't fail with provided data"); + + assert_eq!(StatusCode::OK, actual_response.status()); + + let units_for_slot: UnitsForSlotResponse = + serde_json::from_slice(&hyper::body::to_bytes(actual_response).await.unwrap()) + .expect("Should deserialize"); + + // we must use the same timestamp as the response, otherwise our tests will fail randomly + let expected_response = get_expected_response( + vec![], + units_for_slot + .targeting_input_base + .global + .seconds_since_epoch + .clone(), + ); + + pretty_assertions::assert_eq!( + expected_response.targeting_input_base, + units_for_slot.targeting_input_base + ); + + assert_eq!( + expected_response.campaigns.len(), + units_for_slot.campaigns.len() + ); + assert_eq!(expected_response.campaigns, units_for_slot.campaigns); + assert_eq!( + expected_response.fallback_unit, + units_for_slot.fallback_unit + ); +} + +#[tokio::test] +async fn no_ad_units() { + let server = MockServer::start().await; + + let app = init_app_with_mocked_platform(&server).await; + + let categories: [&str; 3] = ["IAB3", "IAB13-7", "IAB5"]; + let rules = get_mock_rules(&categories); + let mut campaign = mock_campaign(&rules); + campaign.ad_units = vec![]; + + let platform_ad_units = AdUnitsResponse(campaign.ad_units.clone()); + let mock_slot = get_test_ad_slot(&rules, &categories); + + Mock::given(method("GET")) + .and(path("/platform/units")) + .respond_with(ResponseTemplate::new(200).set_body_json(&platform_ad_units)) + .mount(&server) + .await; + + let request = Request::post("/units-for-slot") + .header(USER_AGENT, TEST_USER_AGENT) + .header(CLOUDFLARE_IPCOUNTY_HEADER.clone(), TEST_CLOUDFLARE_IPCOUNTY) + .body( + serde_json::to_vec(&RequestBody { + ad_slot: mock_slot, + deposit_assets: Some(vec![campaign.channel.token].into_iter().collect()), + }) + .expect("Should serialize"), + ) + .unwrap(); + + let actual_response = post_units_for_slot(request, &app) + .await + .expect("call shouldn't fail with provided data"); + + assert_eq!(StatusCode::OK, actual_response.status()); + + let units_for_slot: UnitsForSlotResponse = + serde_json::from_slice(&hyper::body::to_bytes(actual_response).await.unwrap()) + .expect("Should deserialize"); + + // we must use the same timestamp as the response, otherwise our tests will fail randomly + let expected_response = get_expected_response( + vec![], + units_for_slot + .targeting_input_base + .global + .seconds_since_epoch + .clone(), + ); + + pretty_assertions::assert_eq!( + expected_response.targeting_input_base, + units_for_slot.targeting_input_base + ); + + assert_eq!( + expected_response.campaigns.len(), + units_for_slot.campaigns.len() + ); + assert_eq!(expected_response.campaigns, units_for_slot.campaigns); + assert_eq!( + expected_response.fallback_unit, + units_for_slot.fallback_unit + ); +} + +#[tokio::test] +async fn price_less_than_min_per_impression() { + let server = MockServer::start().await; + + let app = init_app_with_mocked_platform(&server).await; + + let categories: [&str; 3] = ["IAB3", "IAB13-7", "IAB5"]; + let rules = get_mock_rules(&categories); + let mut campaign = mock_campaign(&rules); + campaign + .pricing_bounds + .get_mut(&IMPRESSION) + .expect("Campaign should have IMPRESSION pricing bound") + // 0.00001 + // should be less than `config.limits.units_for_slot.global_min_impression_price` + .min = 1_000.into(); + + let platform_ad_units = AdUnitsResponse(campaign.ad_units.clone()); + let mock_slot = get_test_ad_slot(&rules, &categories); + + Mock::given(method("GET")) + .and(path("/platform/units")) + .respond_with(ResponseTemplate::new(200).set_body_json(&platform_ad_units)) + .mount(&server) + .await; + + let request = Request::post("/units-for-slot") + .header(USER_AGENT, TEST_USER_AGENT) + .header(CLOUDFLARE_IPCOUNTY_HEADER.clone(), TEST_CLOUDFLARE_IPCOUNTY) + .body( + serde_json::to_vec(&RequestBody { + ad_slot: mock_slot, + deposit_assets: Some(vec![campaign.channel.token].into_iter().collect()), + }) + .expect("Should serialize"), + ) + .unwrap(); + + let actual_response = post_units_for_slot(request, &app) + .await + .expect("call shouldn't fail with provided data"); + + assert_eq!(StatusCode::OK, actual_response.status()); + + let units_for_slot: UnitsForSlotResponse = + serde_json::from_slice(&hyper::body::to_bytes(actual_response).await.unwrap()) + .expect("Should deserialize"); + + // we must use the same timestamp as the response, otherwise our tests will fail randomly + let expected_response = get_expected_response( + vec![], + units_for_slot + .targeting_input_base + .global + .seconds_since_epoch + .clone(), + ); + + pretty_assertions::assert_eq!( + expected_response.targeting_input_base, + units_for_slot.targeting_input_base + ); + + assert_eq!( + expected_response.campaigns.len(), + units_for_slot.campaigns.len() + ); + assert_eq!(expected_response.campaigns, units_for_slot.campaigns); + assert_eq!( + expected_response.fallback_unit, + units_for_slot.fallback_unit + ); +} + +#[tokio::test] +async fn non_matching_deposit_asset() { + let server = MockServer::start().await; + + let app = init_app_with_mocked_platform(&server).await; + + let categories: [&str; 3] = ["IAB3", "IAB13-7", "IAB5"]; + let rules = get_mock_rules(&categories); + let mut campaign = mock_campaign(&rules); + campaign.channel.token = "0x000000000000000000000000000000000000000".parse().unwrap(); + + let platform_ad_units = AdUnitsResponse(campaign.ad_units.clone()); + let mock_slot = get_test_ad_slot(&rules, &categories); + + Mock::given(method("GET")) + .and(path("/platform/units")) + .respond_with(ResponseTemplate::new(200).set_body_json(&platform_ad_units)) + .mount(&server) + .await; + + let request = Request::post("/units-for-slot/") + .header(USER_AGENT, TEST_USER_AGENT) + .header(CLOUDFLARE_IPCOUNTY_HEADER.clone(), TEST_CLOUDFLARE_IPCOUNTY) + .body( + serde_json::to_vec(&RequestBody { + ad_slot: mock_slot, + deposit_assets: Some(vec![DUMMY_CAMPAIGN.channel.token].into_iter().collect()), + }) + .expect("Should serialize"), + ) + .unwrap(); + + let actual_response = post_units_for_slot(request, &app) + .await + .expect("call shouldn't fail with provided data"); + + assert_eq!(StatusCode::OK, actual_response.status()); + + let units_for_slot: UnitsForSlotResponse = + serde_json::from_slice(&hyper::body::to_bytes(actual_response).await.unwrap()) + .expect("Should deserialize"); + + // we must use the same timestamp as the response, otherwise our tests will fail randomly + let expected_response = get_expected_response( + vec![], + units_for_slot + .targeting_input_base + .global + .seconds_since_epoch + .clone(), + ); + + pretty_assertions::assert_eq!( + expected_response.targeting_input_base, + units_for_slot.targeting_input_base + ); + + assert_eq!( + expected_response.campaigns.len(), + units_for_slot.campaigns.len() + ); + assert_eq!(expected_response.campaigns, units_for_slot.campaigns); + assert_eq!( + expected_response.fallback_unit, + units_for_slot.fallback_unit + ); +} + +#[tokio::test] +async fn multiple_campaigns() { + let server = MockServer::start().await; + + let app = init_app_with_mocked_platform(&server).await; + + let categories: [&str; 3] = ["IAB3", "IAB13-7", "IAB5"]; + let rules = get_mock_rules(&categories); + let campaign = mock_campaign(&rules); + + let non_matching_categories: [&str; 3] = ["IAB2", "IAB9-WS1", "IAB19"]; + let non_matching_rules = get_mock_rules(&non_matching_categories); + let mut non_matching_campaign = mock_campaign(&non_matching_rules); + non_matching_campaign.channel.leader = LEADER_2; + non_matching_campaign.creator = *PUBLISHER; + + let platform_ad_units = AdUnitsResponse(campaign.ad_units.clone()); + let mock_slot = get_test_ad_slot(&rules, &categories); + + Mock::given(method("GET")) + .and(path("/platform/units")) + .respond_with(ResponseTemplate::new(200).set_body_json(&platform_ad_units)) + .mount(&server) + .await; + + let campaign = get_mock_campaign(campaign.clone()); + + let request = Request::post("/units-for-slot") + .header(USER_AGENT, TEST_USER_AGENT) + .header(CLOUDFLARE_IPCOUNTY_HEADER.clone(), TEST_CLOUDFLARE_IPCOUNTY) + .body( + serde_json::to_vec(&RequestBody { + ad_slot: mock_slot, + deposit_assets: Some(vec![DUMMY_CAMPAIGN.channel.token].into_iter().collect()), + }) + .expect("Should serialize"), + ) + .unwrap(); + + let actual_response = post_units_for_slot(request, &app) + .await + .expect("call shouldn't fail with provided data"); + + assert_eq!(StatusCode::OK, actual_response.status()); + + let units_for_slot: UnitsForSlotResponse = + serde_json::from_slice(&hyper::body::to_bytes(actual_response).await.unwrap()) + .expect("Should deserialize"); + + // we must use the same timestamp as the response, otherwise our tests will fail randomly + let expected_response = get_expected_response( + vec![campaign], + units_for_slot + .targeting_input_base + .global + .seconds_since_epoch + .clone(), + ); + + pretty_assertions::assert_eq!( + expected_response.targeting_input_base, + units_for_slot.targeting_input_base + ); + + assert_eq!( + expected_response.campaigns.len(), + units_for_slot.campaigns.len() + ); + assert_eq!(expected_response.campaigns, units_for_slot.campaigns); + assert_eq!( + expected_response.fallback_unit, + units_for_slot.fallback_unit + ); +} + +#[tokio::test] +#[ignore = "exists to print output for comparison"] +async fn get_sample_units_for_slot_output() { + let logger = discard_logger(); + + let server = MockServer::start().await; + + let market = MarketApi::new( + (server.uri() + "/platform/") + .parse() + .expect("Wrong Market url"), + &DEVELOPMENT, + logger.clone(), + ) + .expect("should create market instance"); + + let categories: [&str; 3] = ["IAB3", "IAB13-7", "IAB5"]; + let rules = get_mock_rules(&categories); + let campaign = mock_campaign(&rules); + + let platform_ad_units = AdUnitsResponse(campaign.ad_units.clone()); + let mock_slot = get_test_ad_slot(&rules, &categories); + + Mock::given(method("GET")) + .and(path("/platform/units")) + .respond_with(ResponseTemplate::new(200).set_body_json(&platform_ad_units)) + .mount(&server) + .await; + + let request = Request::post("/units-for-slot") + .header(USER_AGENT, TEST_USER_AGENT) + .header(CLOUDFLARE_IPCOUNTY_HEADER.clone(), TEST_CLOUDFLARE_IPCOUNTY) + .body( + serde_json::to_vec(&RequestBody { + ad_slot: mock_slot, + deposit_assets: Some(vec![campaign.channel.token].into_iter().collect()), + }) + .expect("Should serialize"), + ) + .unwrap(); + + let actual_response = post_units_for_slot(request, &app) + .await + .expect("call shouldn't fail with provided data"); + + assert_eq!(StatusCode::OK, actual_response.status()); + + let units_for_slot: UnitsForSlotResponse = + serde_json::from_slice(&hyper::body::to_bytes(actual_response).await.unwrap()) + .expect("Should deserialize"); + let units_for_slot_pretty = + serde_json::to_string_pretty(&units_for_slot).expect("should turn to string"); + + println!("{}", units_for_slot_pretty); +} From 837b512c9d5e9d21315a69dbe4765c2f5d721661 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Tue, 26 Apr 2022 17:41:33 +0200 Subject: [PATCH 02/15] test_harness - multichain tests for CAMPAIGN_3 --- adapter/src/ethereum/test_util.rs | 6 +- primitives/src/test_util.rs | 2 +- sentry/src/db/campaign.rs | 2 +- test_harness/src/lib.rs | 229 +++++++++++++++++++++++++----- 4 files changed, 197 insertions(+), 42 deletions(-) diff --git a/adapter/src/ethereum/test_util.rs b/adapter/src/ethereum/test_util.rs index 067435e61..889141342 100644 --- a/adapter/src/ethereum/test_util.rs +++ b/adapter/src/ethereum/test_util.rs @@ -11,7 +11,7 @@ use web3::{ use primitives::{ channel::{Channel, Nonce}, config::{ChainInfo, TokenInfo, GANACHE_CONFIG}, - test_util::{ADVERTISER, CREATOR, FOLLOWER, GUARDIAN, GUARDIAN_2, LEADER, PUBLISHER}, + test_util::{ADVERTISER, CREATOR, FOLLOWER, GUARDIAN, GUARDIAN_2, LEADER, PUBLISHER, ADVERTISER_2}, Address, BigNum, Chain, ValidatorId, }; @@ -83,6 +83,10 @@ pub static KEYSTORES: Lazy> = Lazy::new(|| { *GUARDIAN_2, keystore_options(&format!("{}_keystore.json", *GUARDIAN_2), "ganache6"), ), + ( + *ADVERTISER_2, + keystore_options(&format!("{}_keystore.json", *ADVERTISER_2), "ganache8"), + ), ] .into_iter() .collect() diff --git a/primitives/src/test_util.rs b/primitives/src/test_util.rs index 4a39567ec..4c9b864c3 100644 --- a/primitives/src/test_util.rs +++ b/primitives/src/test_util.rs @@ -230,7 +230,7 @@ pub static DUMMY_AD_UNITS: Lazy<[AdUnit; 4]> = Lazy::new(|| { min_targeting_score: None, modified: None, owner: IDS[&PUBLISHER], - title: Some("Dummy AdUnit 3".to_string()), + title: Some("Dummy AdUnit 1".to_string()), }, AdUnit { ipfs: IPFS::try_from("QmVhRDGXoM3Fg3HZD5xwMuxtb9ZErwC8wHt8CjsfxaiUbZ") diff --git a/sentry/src/db/campaign.rs b/sentry/src/db/campaign.rs index 0a9ed4d02..5c2e14380 100644 --- a/sentry/src/db/campaign.rs +++ b/sentry/src/db/campaign.rs @@ -328,7 +328,7 @@ mod campaign_remaining { Ok(campaigns_remaining) } - /// This method will get the remaining of the provided [`Campaign`]s + /// This method will get the remaining of the provided [`Campaign`](primitives::Campaign)s /// and it will also match the returned values to the [`CampaignId`]s /// `MGET` should always return results in the same order! pub async fn get_multiple_with_ids( diff --git a/test_harness/src/lib.rs b/test_harness/src/lib.rs index cc1659b55..df7c6c6e5 100644 --- a/test_harness/src/lib.rs +++ b/test_harness/src/lib.rs @@ -267,7 +267,10 @@ mod tests { IMPRESSION, }, spender::Spender, - test_util::{ADVERTISER, DUMMY_AD_UNITS, DUMMY_IPFS, GUARDIAN, GUARDIAN_2, IDS, PUBLISHER}, + test_util::{ + ADVERTISER, ADVERTISER_2, DUMMY_AD_UNITS, DUMMY_IPFS, GUARDIAN, GUARDIAN_2, IDS, + PUBLISHER, PUBLISHER_2, + }, util::{logging::new_logger, ApiUrl}, validator::{Heartbeat, NewState}, Balances, BigNum, Campaign, CampaignId, Channel, ChannelId, UnifiedNum, @@ -441,7 +444,7 @@ mod tests { // 20.00000000 budget: UnifiedNum::from(2_000_000_000), validators, - title: Some("Dummy Campaign".to_string()), + title: Some("Dummy Campaign 2 in Chain #1337".to_string()), pricing_bounds: vec![ ( IMPRESSION, @@ -497,9 +500,9 @@ mod tests { id: VALIDATORS[&LEADER].address.into(), url: VALIDATORS[&LEADER].sentry_url.to_string(), // min_validator_fee for token: 0.000_010 - // fee per 1000 (pro mille) = 0.00003000 (UnifiedNum) - // fee per 1 payout: payout * fee / 1000 = payout * 0.00000003 - fee: 3_000.into(), + // fee per 1000 (pro mille) = 0.00020000 (UnifiedNum) + // fee per 1 payout: payout * fee / 1000 = payout * 0.00000020 + fee: 20_000.into(), fee_addr: None, }; @@ -507,9 +510,9 @@ mod tests { id: VALIDATORS[&FOLLOWER].address.into(), url: VALIDATORS[&FOLLOWER].sentry_url.to_string(), // min_validator_fee for token: 0.000_010 - // fee per 1000 (pro mille) = 0.00002000 (UnifiedNum) - // fee per 1 payout: payout * fee / 1000 = payout * 0.00000002 - fee: 2_000.into(), + // fee per 1000 (pro mille) = 0.00010000 (UnifiedNum) + // fee per 1 payout: payout * fee / 1000 = payout * 0.0000001 + fee: 10_000.into(), fee_addr: None, }; @@ -520,39 +523,39 @@ mod tests { .parse() .expect("Should parse"), channel, - creator: *ADVERTISER, + creator: *ADVERTISER_2, // 20.00000000 budget: UnifiedNum::from(2_000_000_000), validators, - title: Some("Dummy Campaign in Chain #1".to_string()), + title: Some("Dummy Campaign 3 in Chain #1".to_string()), pricing_bounds: vec![ ( IMPRESSION, Pricing { - // 0.00003000 - // Per 1000 = 0.03000000 - min: 3_000.into(), - // 0.00005000 - // Per 1000 = 0.05000000 - max: 5_000.into(), + // 0.00030000 + // Per 1000 = 0.30000000 + min: 30_000.into(), + // 0.00050000 + // Per 1000 = 0.50000000 + max: 50_000.into(), }, ), ( CLICK, Pricing { - // 0.00006000 - // Per 1000 = 0.06000000 - min: 6_000.into(), - // 0.00010000 - // Per 1000 = 0.10000000 - max: 10_000.into(), + // 0.00060000 + // Per 1000 = 0.60000000 + min: 60_000.into(), + // 0.00090000 + // Per 1000 = 0.90000000 + max: 90_000.into(), }, ), ] .into_iter() .collect(), event_submission: Some(EventSubmission { allow: vec![] }), - ad_units: vec![DUMMY_AD_UNITS[0].clone(), DUMMY_AD_UNITS[1].clone()], + ad_units: vec![DUMMY_AD_UNITS[2].clone(), DUMMY_AD_UNITS[3].clone()], targeting_rules: Rules::new(), created: Utc.ymd(2021, 2, 1).and_hms(7, 0, 0), active: Active { @@ -563,7 +566,6 @@ mod tests { }); #[tokio::test(flavor = "multi_thread", worker_threads = 4)] - // #[ignore = "for now"] async fn run_full_test() { let chain = GANACHE_1337.clone(); assert_eq!(CAMPAIGN_1.channel.token, CAMPAIGN_2.channel.token); @@ -572,7 +574,7 @@ mod tests { .find_chain_of(CAMPAIGN_1.channel.token) .expect("Should find CAMPAIGN_1 channel token address in Config!"); - let second_token_chain = GANACHE_CONFIG + let token_chain_1 = GANACHE_CONFIG .find_chain_of(CAMPAIGN_3.channel.token) .expect("Should find CAMPAIGN_3 channel token address in Config!"); @@ -598,11 +600,19 @@ mod tests { // We use the Advertiser's `EthereumAdapter::get_auth` for authentication! let advertiser_adapter = Adapter::new( Ethereum::init(KEYSTORES[&ADVERTISER].clone(), &GANACHE_CONFIG) - .expect("Should initialize creator adapter"), + .expect("Should initialize ADVERTISER adapter"), ) .unlock() .expect("Should unlock advertiser's Ethereum Adapter"); + // We use the Advertiser's `EthereumAdapter::get_auth` for authentication! + let advertiser2_adapter = Adapter::new( + Ethereum::init(KEYSTORES[&ADVERTISER_2].clone(), &GANACHE_CONFIG) + .expect("Should initialize ADVERTISER_2 adapter"), + ) + .unlock() + .expect("Should unlock Advertiser 2 Ethereum Adapter"); + // setup Sentry & returns Adapter let leader_adapter = setup_sentry(&leader) .await @@ -649,6 +659,7 @@ mod tests { // Advertiser deposits // + // Advertiser // Channel 1 in Chain #1337: // - Outpace: 20 TOKENs // - Counterfactual: 10 TOKENs @@ -657,8 +668,9 @@ mod tests { // - Outpace: 30 TOKENs // - Counterfactual: 20 TOKENs // + // Advertiser 2 // Channel 3 in Chain #1: - // - Outpace: 30 TOKENS + // - Outpace: 100 TOKENS // - Counterfactual: 20 TOKENs { let advertiser_deposits = [ @@ -679,7 +691,7 @@ mod tests { Deposit { channel: CAMPAIGN_3.channel, token: contracts_1.token.info.clone(), - address: advertiser_adapter.whoami().to_address(), + address: advertiser2_adapter.whoami().to_address(), outpace_amount: BigNum::with_precision(100, token_1_precision), counterfactual_amount: BigNum::with_precision(20, token_1_precision), }, @@ -731,8 +743,8 @@ mod tests { // make sure we have the expected deposit returned from EthereumAdapter let eth_deposit = leader_adapter .get_deposit( - &second_token_chain.clone().with_channel(CAMPAIGN_3.channel), - advertiser_adapter.whoami().to_address(), + &token_chain_1.clone().with_channel(CAMPAIGN_3.channel), + advertiser2_adapter.whoami().to_address(), ) .await .expect("Should get deposit for advertiser"); @@ -920,7 +932,7 @@ mod tests { } } - // Create Campaign 3 w/ Channel 3 using Advertiser on a different chain (Chain #1) + // Create Campaign 3 w/ Channel 3 using Advertiser 2 on a different chain (Chain #1) // In Leader & Follower sentries // Response: 200 Ok // POST /v5/campaign @@ -929,12 +941,12 @@ mod tests { let create_campaign_3 = CreateCampaign::from_campaign(CAMPAIGN_3.clone()); assert_eq!( - &second_token_chain.chain, &second_chain, + &token_chain_1.chain, &second_chain, "CAMPAIGN_3 should be using the #1 Chain which is setup in the Ganache Config" ); { - let leader_token = advertiser_adapter + let leader_token = advertiser2_adapter .get_auth(second_chain.chain_id, leader_adapter.whoami()) .expect("Get authentication"); @@ -956,7 +968,7 @@ mod tests { } { - let follower_token = advertiser_adapter + let follower_token = advertiser2_adapter .get_auth(second_chain.chain_id, follower_adapter.whoami()) .expect("Get authentication"); @@ -1058,7 +1070,7 @@ mod tests { }, ]; - let response = post_new_events( + let leader_response = post_new_events( &leader_sentry, token_chain_1337.clone().with(CAMPAIGN_1.id), &events, @@ -1066,15 +1078,97 @@ mod tests { .await .expect("Posted events"); - assert_eq!(SuccessResponse { success: true }, response); + assert_eq!(SuccessResponse { success: true }, leader_response); + + let follower_response = post_new_events( + &follower_sentry, + token_chain_1337.clone().with(CAMPAIGN_1.id), + &events, + ) + .await + .expect("Posted events"); + + assert_eq!(SuccessResponse { success: true }, follower_response); info!( setup.logger, - "Successfully POST events for CAMPAIGN_1 {:?} and Channel {:?} ", + "Successfully POST events for CAMPAIGN_1 {:?} and Channel {:?} to Leader & Follower", CAMPAIGN_1.id, CAMPAIGN_1.channel.id() ); } + // Add new events for `CAMPAIGN_3` to sentry + { + let events = vec![ + Event::Impression { + publisher: *PUBLISHER_2, + ad_unit: CAMPAIGN_3 + .ad_units + .get(1) + .expect("Should exist in Campaign") + .ipfs, + ad_slot: DUMMY_IPFS[3], + referrer: Some("https://adex.network".into()), + }, + Event::Impression { + publisher: *PUBLISHER_2, + ad_unit: CAMPAIGN_3 + .ad_units + .get(1) + .expect("Should exist in Campaign") + .ipfs, + ad_slot: DUMMY_IPFS[3], + referrer: Some("https://adex.network".into()), + }, + Event::Click { + publisher: *PUBLISHER_2, + ad_unit: CAMPAIGN_3 + .ad_units + .get(1) + .expect("Should exist in Campaign") + .ipfs, + ad_slot: DUMMY_IPFS[3], + referrer: Some("https://ambire.com".into()), + }, + Event::Click { + publisher: *PUBLISHER_2, + ad_unit: CAMPAIGN_3 + .ad_units + .get(1) + .expect("Should exist in Campaign") + .ipfs, + ad_slot: DUMMY_IPFS[3], + referrer: Some("https://ambire.com".into()), + }, + ]; + + let response_leader = post_new_events( + &leader_sentry, + token_chain_1.clone().with(CAMPAIGN_3.id), + &events, + ) + .await + .expect("Posted events"); + + assert_eq!(SuccessResponse { success: true }, response_leader); + + let follower_response = post_new_events( + &follower_sentry, + token_chain_1.clone().with(CAMPAIGN_3.id), + &events, + ) + .await + .expect("Posted events"); + + assert_eq!(SuccessResponse { success: true }, follower_response); + info!( + setup.logger, + "Successfully POST events for CAMPAIGN_3 {:?} and Channel {:?} to Leader & Follower", + CAMPAIGN_3.id, + CAMPAIGN_3.channel.id() + ); + } + // Channel 1 expected Accounting // Fees are calculated based on pro mile of the payout // event payout * fee / 1000 @@ -1128,6 +1222,63 @@ mod tests { info!(setup.logger, "Successfully validated Accounting Balances for Channel 1 {:?} after CAMPAIGN_1 events {:?}", CAMPAIGN_1.channel.id(), CAMPAIGN_1.id); } + // Channel 3 expected Accounting + // Fees are calculated based on pro mile of the payout + // event payout * fee / 1000 + // + // + // IMPRESSION: + // - Publisher2 payout: 2 * 30 000 = 60 000 + // - Leader fees: 60 000 * 20 000 / 1 000 = 1 200 000 + // - Follower fees: 60 000 * 10 000 / 1000 = 600 000 + // + // CLICK: + // - Publisher2 payout: 2 * 60 000 = 120 000 + // - Leader fees: 120 000 * 20 000 / 1000 = 2 400 000 + // - Follower fees: 120 000 * 10 000 / 1000 = 1 200 000 + // + // Creator (Advertiser2) pays out: + // events_payout + leader fee + follower fee + // events_payout = 60 000 (impression) + 120 000 (click) = 180 000 + // 180 000 + (1 200 000 + 2 400 000) + (600 000 + 1 200 000) = 5 580 000 + // + // Publisher2 total payout: 180 000 + // leader total fees: 1 200 000 + 2 400 000 = 3 600 000 + // follower total fees: 600 000 + 1 200 000 = 1 800 000 + { + let mut expected_balances = Balances::new(); + + expected_balances + .spend( + CAMPAIGN_3.creator, + CAMPAIGN_3.channel.leader.to_address(), + UnifiedNum::from(3_600_000), + ) + .expect("Should spend for Leader"); + expected_balances + .spend( + CAMPAIGN_3.creator, + CAMPAIGN_3.channel.follower.to_address(), + UnifiedNum::from(1_800_000), + ) + .expect("Should spend for Follower"); + expected_balances + .spend(CAMPAIGN_3.creator, *PUBLISHER_2, UnifiedNum::from(180_000)) + .expect("Should spend for Publisher"); + + let expected_accounting = AccountingResponse { + balances: expected_balances, + }; + + let actual_accounting = leader_sentry + .get_accounting(&token_chain_1.with_channel(CAMPAIGN_3.channel)) + .await + .expect("Should get Channel Accounting"); + + pretty_assertions::assert_eq!(expected_accounting, actual_accounting); + info!(setup.logger, "Successfully validated Accounting Balances for Channel 3 {:?} after CAMPAIGN_3 events {:?}", CAMPAIGN_3.channel.id(), CAMPAIGN_3.id); + } + // leader single worker tick leader_worker.all_channels_tick().await; // follower single worker tick @@ -1159,14 +1310,14 @@ mod tests { .spend( CAMPAIGN_1.creator, CAMPAIGN_1.channel.leader.to_address(), - UnifiedNum::from_u64(27000), + UnifiedNum::from_u64(27_000), ) .expect("Should spend"); expected_balances .spend( CAMPAIGN_1.creator, CAMPAIGN_1.channel.follower.to_address(), - UnifiedNum::from_u64(18000), + UnifiedNum::from_u64(18_000), ) .expect("Should spend"); expected_balances From 9bd511df76c3b1dbc862f09ced6d644e7dfb5a0c Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Thu, 12 May 2022 10:21:54 +0200 Subject: [PATCH 03/15] primitives - UnifiedNum: - Fix bugs with math operations - trait FromWhole for easy creation of UnifiedNum from f64 & u64 --- primitives/src/lib.rs | 2 +- primitives/src/unified_num.rs | 386 ++++++++++++++++++++++++++++++++-- 2 files changed, 366 insertions(+), 22 deletions(-) diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs index e035d9c70..57738ae05 100644 --- a/primitives/src/lib.rs +++ b/primitives/src/lib.rs @@ -47,7 +47,7 @@ pub mod targeting; #[cfg(feature = "test-util")] #[cfg_attr(docsrs, doc(cfg(feature = "test-util")))] pub mod test_util; -mod unified_num; +pub mod unified_num; pub mod validator; /// This module is available with the `postgres` feature. diff --git a/primitives/src/unified_num.rs b/primitives/src/unified_num.rs index 9d40c11dc..69de32e91 100644 --- a/primitives/src/unified_num.rs +++ b/primitives/src/unified_num.rs @@ -1,17 +1,110 @@ use crate::BigNum; use num::{ - pow::Pow, traits::CheckedRem, CheckedAdd, CheckedDiv, CheckedMul, CheckedSub, Integer, One, + pow::Pow, rational::Ratio, traits::CheckedRem, CheckedAdd, CheckedDiv, CheckedMul, CheckedSub, + Integer, One, }; -use num_derive::{FromPrimitive, Num, NumCast, NumOps, ToPrimitive, Zero}; +use num_derive::{FromPrimitive, Num, NumCast, ToPrimitive, Zero}; use parse_display::{Display, FromStr, ParseError}; use serde::{Deserialize, Serialize}; use std::{ cmp::Ordering, fmt, iter::Sum, - ops::{Add, AddAssign, Div, Mul, Sub}, + ops::{Add, AddAssign, Div, Mul, Rem, Sub}, }; +pub use whole_number::FromWhole; + +mod whole_number { + use num::ToPrimitive; + + use crate::UnifiedNum; + + /// Helper trait for handling the creation of special numbers from a whole number + pub trait FromWhole: Sized { + /// # Panics + /// If the number is greater than one can be represented. + fn from_whole(whole_number: T) -> Self; + + /// Same as [`Self::from_whole`] but instead of panicking it returns an Option. + fn from_whole_opt(whole_number: T) -> Option; + } + + impl FromWhole for UnifiedNum { + fn from_whole(number: f64) -> Self { + Self::from_whole_opt(number).expect("The number is too large") + } + + fn from_whole_opt(number: f64) -> Option { + let whole_number = number.trunc().to_u64()?.checked_mul(Self::MULTIPLIER)?; + + // multiply the fractional part by the multiplier + // truncate it to get the fractional part only + // convert it to u64 + let fract_number = (number.fract() * 10_f64.powf(Self::PRECISION.into())) + .round() + .to_u64()?; + + whole_number.checked_add(fract_number).map(Self) + } + } + + impl FromWhole for UnifiedNum { + fn from_whole(whole_number: u64) -> Self { + Self( + whole_number + .checked_mul(UnifiedNum::MULTIPLIER) + .expect("The whole number is too large"), + ) + } + + fn from_whole_opt(whole_number: u64) -> Option { + whole_number.checked_mul(UnifiedNum::MULTIPLIER).map(Self) + } + } + + #[cfg(test)] + mod test { + use crate::UnifiedNum; + + use super::FromWhole; + + #[test] + fn test_whole_number_impl_for_f64() { + assert_eq!( + UnifiedNum::from(800_000_000_u64), + UnifiedNum::from_whole(8.0_f64) + ); + assert_eq!( + UnifiedNum::from(810_000_000_u64), + UnifiedNum::from_whole(8.1_f64) + ); + assert_eq!( + UnifiedNum::from(800_000_009_u64), + UnifiedNum::from_whole(8.00_000_009_f64) + ); + + assert_eq!( + UnifiedNum::from(800_000_001_u64), + UnifiedNum::from_whole(8.000_000_009_f64), + "Should round up the floating number" + ); + + assert_eq!( + UnifiedNum::from(800_000_000_u64), + UnifiedNum::from_whole(8.000_000_004_f64), + "Should round down the floating number" + ); + + assert_eq!( + UnifiedNum::from(123_456_789_000_000_00_u64), + UnifiedNum::from_whole(123_456_789.000_000_004_f64), + "Should round down the floating number" + ); + } + } +} + /// Unified Number with a precision of 8 digits after the decimal point. /// /// The number can be a maximum of `u64::MAX` (the underlying type), @@ -23,7 +116,7 @@ use std::{ /// /// # Examples /// -/// ```rust +/// ``` /// use primitives::UnifiedNum; /// use serde_json::Value; /// @@ -44,7 +137,6 @@ use std::{ Clone, Copy, Num, - NumOps, NumCast, ToPrimitive, FromPrimitive, @@ -78,9 +170,30 @@ impl TryFrom for UnifiedNum { } impl UnifiedNum { + /// The precision of the [`UnifiedNum`] is 8 decimal numbers after the comma. pub const PRECISION: u8 = 8; + /// The whole number multiplier when dealing with a [`UnifiedNum`]. + /// + /// # Examples + /// + /// ``` + /// use primitives::UnifiedNum; + /// + /// let whole_number = 8_u64; // we want to represent 8.00_000_000 + /// + /// assert_eq!(UnifiedNum::from_u64(800_000_000), UnifiedNum::from(whole_number * UnifiedNum::MULTIPLIER)); + /// ``` + pub const MULTIPLIER: u64 = 10_u64.pow(Self::PRECISION as u32); pub const DEBUG_DELIMITER: char = '.'; + pub const ZERO: UnifiedNum = UnifiedNum(0); + /// The whole number `1` as a [`UnifiedNum`]. + /// One (`1`) followed by exactly 8 zeroes (`0`). + /// + /// `1.00_000_000` + /// `100_000_000` + pub const ONE: UnifiedNum = UnifiedNum(100_000_000); + pub fn div_floor(&self, other: &Self) -> Self { Self(self.0.div_floor(&other.0)) } @@ -175,8 +288,9 @@ impl fmt::Debug for UnifiedNum { } impl One for UnifiedNum { + /// 1.00_000_000 fn one() -> Self { - Self(100_000_000) + UnifiedNum::ONE } } @@ -220,6 +334,14 @@ impl Integer for UnifiedNum { } } +impl Add for UnifiedNum { + type Output = UnifiedNum; + + fn add(self, rhs: UnifiedNum) -> Self::Output { + UnifiedNum(self.0 + rhs.0) + } +} + impl Add<&UnifiedNum> for &UnifiedNum { type Output = UnifiedNum; @@ -234,6 +356,14 @@ impl AddAssign<&UnifiedNum> for UnifiedNum { } } +impl Sub for UnifiedNum { + type Output = UnifiedNum; + + fn sub(self, rhs: UnifiedNum) -> Self::Output { + UnifiedNum(self.0 - rhs.0) + } +} + impl Sub<&UnifiedNum> for &UnifiedNum { type Output = UnifiedNum; @@ -262,7 +392,33 @@ impl Div<&UnifiedNum> for &UnifiedNum { type Output = UnifiedNum; fn div(self, rhs: &UnifiedNum) -> Self::Output { - UnifiedNum(self.0 / rhs.0) + if rhs == &UnifiedNum::ONE { + return *self; + } + + // checks for denom = 0 and panics if it is + // if both are less than 1.0 + // or both are > 1.0 + // we must use the multiplier + let ratio = if self < &UnifiedNum::ONE && rhs < &UnifiedNum::ONE + || self > &UnifiedNum::ONE && rhs > &UnifiedNum::ONE + || self < &UnifiedNum::ONE && rhs > &UnifiedNum::ONE + { + Ratio::from_integer(self.0) / Ratio::new(rhs.0, UnifiedNum::MULTIPLIER) + } else { + Ratio::new(self.0, rhs.0) + }; + + UnifiedNum(ratio.round().to_integer()) + } +} + +impl Div for UnifiedNum { + type Output = UnifiedNum; + + fn div(self, rhs: UnifiedNum) -> Self::Output { + // Use &UnifiedNum / &UnifiedNum + &self / &rhs } } @@ -270,7 +426,16 @@ impl Div<&UnifiedNum> for UnifiedNum { type Output = UnifiedNum; fn div(self, rhs: &UnifiedNum) -> Self::Output { - UnifiedNum(self.0 / rhs.0) + // use &UnifiedNum / &UnifiedNum + &self / rhs + } +} + +impl Mul for UnifiedNum { + type Output = UnifiedNum; + + fn mul(self, rhs: UnifiedNum) -> Self::Output { + &self * &rhs } } @@ -278,7 +443,11 @@ impl Mul<&UnifiedNum> for &UnifiedNum { type Output = UnifiedNum; fn mul(self, rhs: &UnifiedNum) -> Self::Output { - UnifiedNum(self.0 * rhs.0) + // checks for denom = 0 and panics if it is + // No need for `checked_div`, because MULTIPLIER is always > 0 + let ratio = Ratio::from_integer(self.0) * Ratio::new(rhs.0, UnifiedNum::MULTIPLIER); + + UnifiedNum(ratio.round().to_integer()) } } @@ -286,7 +455,8 @@ impl Mul<&UnifiedNum> for UnifiedNum { type Output = UnifiedNum; fn mul(self, rhs: &UnifiedNum) -> Self::Output { - UnifiedNum(self.0 * rhs.0) + // Use &UnifiedNum * &UnifiedNum + &self * rhs } } @@ -294,7 +464,8 @@ impl Mul for &UnifiedNum { type Output = UnifiedNum; fn mul(self, rhs: u64) -> Self::Output { - UnifiedNum(self.0 * rhs) + // Use &UnifiedNum * &UnifiedNum + self * &UnifiedNum(rhs) } } @@ -302,7 +473,8 @@ impl Mul for UnifiedNum { type Output = UnifiedNum; fn mul(self, rhs: u64) -> Self::Output { - UnifiedNum(self.0 * rhs) + // Use &UnifiedNum * &UnifiedNum + &self * &UnifiedNum(rhs) } } @@ -310,7 +482,8 @@ impl Mul for u64 { type Output = UnifiedNum; fn mul(self, rhs: UnifiedNum) -> Self::Output { - UnifiedNum(self * rhs.0) + // Use &UnifiedNum * &UnifiedNum + &UnifiedNum(self) * &rhs } } @@ -318,7 +491,30 @@ impl Mul<&UnifiedNum> for u64 { type Output = UnifiedNum; fn mul(self, rhs: &UnifiedNum) -> Self::Output { - UnifiedNum(self * rhs.0) + // Use &UnifiedNum * &UnifiedNum + &UnifiedNum(self) * rhs + } +} + +impl Rem for UnifiedNum { + type Output = UnifiedNum; + + fn rem(self, rhs: UnifiedNum) -> Self::Output { + UnifiedNum(self.0.rem(rhs.0)) + } +} + +impl Rem<&UnifiedNum> for &UnifiedNum { + type Output = UnifiedNum; + + fn rem(self, rhs: &UnifiedNum) -> Self::Output { + UnifiedNum(self.0.rem(rhs.0)) + } +} + +impl CheckedRem for UnifiedNum { + fn checked_rem(&self, v: &Self) -> Option { + self.0.checked_rem(v.0).map(Self) } } @@ -350,19 +546,22 @@ impl CheckedSub for UnifiedNum { impl CheckedMul for UnifiedNum { fn checked_mul(&self, v: &Self) -> Option { - self.0.checked_mul(v.0).map(Self) + let ratio = + Ratio::from_integer(self.0).checked_mul(&Ratio::new(v.0, UnifiedNum::MULTIPLIER)); + + ratio.map(|ratio| Self(ratio.round().to_integer())) } } impl CheckedDiv for UnifiedNum { fn checked_div(&self, v: &Self) -> Option { - self.0.checked_div(v.0).map(Self) - } -} + let ratio = if self.0 > v.0 { + Ratio::new(self.0, v.0).checked_mul(&Ratio::from_integer(UnifiedNum::MULTIPLIER)) + } else { + Ratio::from_integer(self.0).checked_div(&Ratio::new(v.0, UnifiedNum::MULTIPLIER)) + }; -impl CheckedRem for UnifiedNum { - fn checked_rem(&self, v: &Self) -> Option { - self.0.checked_rem(v.0).map(Self) + ratio.map(|ratio| Self(ratio.round().to_integer())) } } @@ -465,6 +664,151 @@ mod test { "Should floor the large BigNum" ); } + + #[test] + fn test_unified_num_mul_and_div() { + // 0.0003 + let three_ten_thousands = UnifiedNum::from(30_000_u64); + // 0.1 + let one_tenth = UnifiedNum::from(10_000_000_u64); + + // 1.0 + let one = UnifiedNum::ONE; + + // 2.0 + let two = UnifiedNum::from(2_00_000_000); + + // 3.0 + let three = UnifiedNum::from(3_00_000_000); + + // division + { + // Case 1: + // 0.0003 / 0.1 = 0.003 + // + // 0.00 030 000 / 0.10 000 000 = 0.00 300 000 + // 30 000 / 10 000 000 = 300 000 + assert_eq!(UnifiedNum::from(300_000), three_ten_thousands / one_tenth); + + // Case 2: + // 3.0 / 0.1 = 30 + // 3 00 000 000 / 10 000 000 = 30 + assert_eq!(UnifiedNum::from(30), three / one_tenth); + + // Case 3: + // 3.0 / 1.0 = 3.0 + // 3 00 000 000 / 1 00 000 000 * MULTIPLIER = 3 00 000 000 + assert_eq!(three, three / one); + + // Case 4: + // 3.0 / 2.0 = 1.5 + // 3 00 000 000 / 2 00 000 000 * MULTIPLIER = 1 50 000 000 + assert_eq!(UnifiedNum::from(1_50_000_000), three / two); + + // Case 4: + // 2.0 / 3.0 = 0.66666667 + // 3 00 000 000 / 2 00 000 000 * MULTIPLIER = 66 666 667 + assert_eq!(UnifiedNum::from(66_666_667), two / three); + + // Case 5: + // 0.1 / 3.0 = 0.03333333 + // 10 000 000 / 3 00 000 000 * MULTIPLIER = 3 333 333 + assert_eq!(UnifiedNum::from(3_333_333), one_tenth / three); + } + + // multiplication + { + // Case 1: + // 0.0003 * 1 = 0.0003 + // 0.00 030 000 * 1 = 0.00 030 000 + assert_eq!(three_ten_thousands * one, three_ten_thousands); + + // Case 2: + // 3 * 1 = 3 + assert_eq!(three * one, three); + + // Case 3: + // 0.0003 * 0.1 = 0.00003 + // 0.00030000 * 0.10000000 = 0.00 003 000 + assert_eq!(three_ten_thousands * one_tenth, UnifiedNum::from(3_000_u64)); + + // Case 4: + // 0.0003 * 2 = 0.0006 + // 0.00 030 000 * 2 = 0.00 060 000 + assert_eq!(three_ten_thousands * two, UnifiedNum::from(60_000_u64)); + + // Case 5: + // 3 * 2 = 6 + assert_eq!(three * two, UnifiedNum::from(600_000_000_u64)); + + // Case 6: + // 3 * 0.1 = 0.30 000 000 + assert_eq!(three * one_tenth, UnifiedNum::from(30_000_000_u64)); + } + } + + #[test] + fn test_unified_num_rem_and_checked_rem_and_with_whole() { + // 10.0 % 3.0 = 1.0 + assert_eq!( + UnifiedNum::ONE, + UnifiedNum::from(10_00_000_000) % UnifiedNum::from(3_00_000_000) + ); + + assert_eq!( + UnifiedNum::ONE, + UnifiedNum::from_whole(10) % UnifiedNum::from_whole(3) + ); + + assert_eq!( + UnifiedNum::ONE, + UnifiedNum::from_whole(10.0) % UnifiedNum::from_whole(3.0) + ); + assert_eq!( + UnifiedNum::from(100_000_000), + UnifiedNum::from_whole(10) % UnifiedNum::from_whole(3) + ); + + // 10.0 % 0.3 = 0.1 + { + assert_eq!( + UnifiedNum::from_whole(10.0), + UnifiedNum::from(10_00_000_000) % UnifiedNum::from_whole(30_000_000) + ); + + assert_eq!( + UnifiedNum::from(10_000_000), + UnifiedNum::from_whole(10.0) % UnifiedNum::from_whole(0.3) + ); + + assert_eq!( + UnifiedNum::from(10_000_000), + UnifiedNum::from_whole(10) % UnifiedNum::from_whole(0.3) + ); + } + + // 0.3 % 10.0 = 0.3 + { + assert_eq!( + UnifiedNum::from(30_000_000), + UnifiedNum::from(30_000_000) % UnifiedNum::from(10_00_000_000) + ); + + assert_eq!( + UnifiedNum::from_whole(0.3), + UnifiedNum::from_whole(0.3) % UnifiedNum::from_whole(10.0) + ); + } + + // CheckedRem by 0 + { + assert_eq!( + None, + UnifiedNum::from_whole(3).checked_rem(&UnifiedNum::ZERO), + "CheckedRem by zero should result in None" + ); + } + } } #[cfg(feature = "postgres")] From ce7d506a9edc1a2dd51f2f5f5804d4ad31a5938f Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Thu, 12 May 2022 11:14:59 +0200 Subject: [PATCH 04/15] primitives - UnifiedNum - try to fix div_floor --- primitives/src/unified_num.rs | 90 +++++++++++++++++++++++++---------- 1 file changed, 65 insertions(+), 25 deletions(-) diff --git a/primitives/src/unified_num.rs b/primitives/src/unified_num.rs index 69de32e91..715b8f68f 100644 --- a/primitives/src/unified_num.rs +++ b/primitives/src/unified_num.rs @@ -195,7 +195,8 @@ impl UnifiedNum { pub const ONE: UnifiedNum = UnifiedNum(100_000_000); pub fn div_floor(&self, other: &Self) -> Self { - Self(self.0.div_floor(&other.0)) + // just division will floor the result! + self / other } pub const fn from_u64(value: u64) -> Self { @@ -392,24 +393,7 @@ impl Div<&UnifiedNum> for &UnifiedNum { type Output = UnifiedNum; fn div(self, rhs: &UnifiedNum) -> Self::Output { - if rhs == &UnifiedNum::ONE { - return *self; - } - - // checks for denom = 0 and panics if it is - // if both are less than 1.0 - // or both are > 1.0 - // we must use the multiplier - let ratio = if self < &UnifiedNum::ONE && rhs < &UnifiedNum::ONE - || self > &UnifiedNum::ONE && rhs > &UnifiedNum::ONE - || self < &UnifiedNum::ONE && rhs > &UnifiedNum::ONE - { - Ratio::from_integer(self.0) / Ratio::new(rhs.0, UnifiedNum::MULTIPLIER) - } else { - Ratio::new(self.0, rhs.0) - }; - - UnifiedNum(ratio.round().to_integer()) + self.checked_div(rhs).expect("Division by 0") } } @@ -554,14 +538,30 @@ impl CheckedMul for UnifiedNum { } impl CheckedDiv for UnifiedNum { - fn checked_div(&self, v: &Self) -> Option { - let ratio = if self.0 > v.0 { - Ratio::new(self.0, v.0).checked_mul(&Ratio::from_integer(UnifiedNum::MULTIPLIER)) + fn checked_div(&self, rhs: &Self) -> Option { + if rhs == &UnifiedNum::ONE { + return Some(*self); + } + + // checks for denom = 0 and panics if it is + // if both are less than 1.0 + // or both are > 1.0 + // or one is left hand side is < 1.0 + // we must use the multiplier + let ratio = if self < &UnifiedNum::ONE && rhs < &UnifiedNum::ONE + || self > &UnifiedNum::ONE && rhs > &UnifiedNum::ONE + || self < &UnifiedNum::ONE && rhs > &UnifiedNum::ONE + { + Ratio::from_integer(self.0).checked_div(&Ratio::new(rhs.0, UnifiedNum::MULTIPLIER)) } else { - Ratio::from_integer(self.0).checked_div(&Ratio::new(v.0, UnifiedNum::MULTIPLIER)) + Some(Ratio::new(self.0, rhs.0)) }; - ratio.map(|ratio| Self(ratio.round().to_integer())) + ratio.map(|ratio| { + let integer = ratio.round().to_integer(); + + UnifiedNum(integer) + }) } } @@ -666,7 +666,7 @@ mod test { } #[test] - fn test_unified_num_mul_and_div() { + fn test_unified_num_mul_and_div_and_div_floor() { // 0.0003 let three_ten_thousands = UnifiedNum::from(30_000_u64); // 0.1 @@ -745,6 +745,46 @@ mod test { // 3 * 0.1 = 0.30 000 000 assert_eq!(three * one_tenth, UnifiedNum::from(30_000_000_u64)); } + + // Mul & then Div with `checked_mul` & `checked_div` + { + // 0.00030 * 0.1 / 1000.0 = 0.00 000 003 + // 30 000 * 10 000 000 / 1 000 00 000 000 = 3 + let result = UnifiedNum::from(30_000) + .checked_mul(&UnifiedNum::from(10_000_000)) + .and_then(|number| number.checked_div(&UnifiedNum::from(1_000_00_000_000))) + .unwrap(); + + assert_eq!(UnifiedNum::from(3), result); + + let result = UnifiedNum::from_whole(0.0003) + .checked_mul(&UnifiedNum::from_whole(0.1)) + .and_then(|number| number.checked_div(&UnifiedNum::from_whole(1000))) + .unwrap(); + + assert_eq!(UnifiedNum::from(3), result); + } + + // div_floor + { + // 0.00006 * 0.1 / 1000.0 = 0.00 000 000 6 + // 6 000 * 10 000 000 / 1 000 00 000 000 = 0.6 = UnifiedNum(0) + let result = UnifiedNum::from(6_000) + .checked_mul(&UnifiedNum::from(10_000_000)) + .map(|number| number.div_floor(&UnifiedNum::from(1_000_00_000_000))) + .unwrap(); + + assert_eq!(UnifiedNum::ZERO, result); + + // 0.00016 * 0.1 / 1000.0 = 0.00 000 001 6 + // 16 000 * 10 000 000 / 1 000 00 000 000 = 1.6 = UnifiedNum(1) + let result = UnifiedNum::from(16_000) + .checked_mul(&UnifiedNum::from(10_000_000)) + .map(|number| number.div_floor(&UnifiedNum::from(1_000_00_000_000))) + .unwrap(); + + assert_eq!(UnifiedNum::from(1), result); + } } #[test] From 12c12f942a5f92b1d4534a779ab247c690e52615 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Mon, 16 May 2022 15:04:19 +0200 Subject: [PATCH 05/15] primitives - UnifiedNum - fix bugs with: - div, mul, div_floor, checked_div and other - primitives - targeting - fix whole number conversion to UnifiedNum --- primitives/src/targeting/eval.rs | 18 +- primitives/src/targeting/eval_test.rs | 220 ++++++++++++++++------ primitives/src/unified_num.rs | 257 +++++++++++++++++++++----- primitives/src/validator.rs | 2 +- 4 files changed, 384 insertions(+), 113 deletions(-) diff --git a/primitives/src/targeting/eval.rs b/primitives/src/targeting/eval.rs index 910e6a9d7..863981cd0 100644 --- a/primitives/src/targeting/eval.rs +++ b/primitives/src/targeting/eval.rs @@ -1,4 +1,4 @@ -use crate::UnifiedNum; +use crate::{unified_num::FromWhole, UnifiedNum}; use serde::{Deserialize, Serialize}; use serde_json::{value::Value as SerdeValue, Number}; use std::{ @@ -453,7 +453,7 @@ impl Value { } } -/// The UnifiedNum can be extracted from the DSL either String or UnifiedNum +/// The UnifiedNum can be expressed in the DSL either with a String or UnifiedNum impl TryFrom for UnifiedNum { type Error = Error; fn try_from(value: Value) -> Result { @@ -461,7 +461,19 @@ impl TryFrom for UnifiedNum { Value::String(string) => UnifiedNum::from_str(&string).map_err(|_| Error::TypeError), Value::UnifiedNum(unified) => Ok(unified), Value::Number(number) => { - UnifiedNum::from_str(&number.to_string()).map_err(|_| Error::TypeError) + if number.is_u64() { + // a whole number + let whole_number = number.as_u64().ok_or(Error::TypeError)?; + + UnifiedNum::from_whole_opt(whole_number).ok_or(Error::TypeError) + } else if number.is_f64() { + // a floating point whole number + let whole_number = number.as_f64().ok_or(Error::TypeError)?; + + UnifiedNum::from_whole_opt(whole_number).ok_or(Error::TypeError) + } else { + Err(Error::TypeError) + } } _ => Err(Error::TypeError), } diff --git a/primitives/src/targeting/eval_test.rs b/primitives/src/targeting/eval_test.rs index b2ec576ea..2c001b9a7 100644 --- a/primitives/src/targeting/eval_test.rs +++ b/primitives/src/targeting/eval_test.rs @@ -230,19 +230,45 @@ mod dsl_test { let cases = vec![ (Value::new_string("1000"), Value::UnifiedNum(1000.into())), - (Value::new_number(2_000), Value::UnifiedNum(2_000.into())), + ( + Value::new_number(2_000), + Value::UnifiedNum(UnifiedNum::from_whole(2_000)), + ), (Value::UnifiedNum(3.into()), Value::UnifiedNum(3.into())), - // rounded floats should work! + // whole number floats should work! ( Value::Number(Number::from_f64(40.0).expect("should create float number")), - Value::UnifiedNum(40.into()), + Value::UnifiedNum(UnifiedNum::from_whole(40)), + ), + // as whole non-rounded floats should too + ( + Value::Number(Number::from_f64(2.5).expect("should create float number")), + Value::UnifiedNum(UnifiedNum::from_whole(2.5)), + ), + // with rounding up + ( + Value::Number( + Number::from_f64(9.99_999_999_9).expect("should create float number"), + ), + Value::UnifiedNum(UnifiedNum::from_whole(10)), + ), + // rounding down + ( + Value::Number( + Number::from_f64(8.99_999_999_4).expect("should create float number"), + ), + Value::UnifiedNum(UnifiedNum::from(8_99_999_999)), ), ]; for (from, expected) in cases.into_iter() { let rule = Rule::Function(Function::new_bn(from)); - assert_eq!(Ok(Some(expected)), rule.eval(&input, &mut output)); + assert_eq!( + Ok(Some(expected)), + rule.clone().eval(&input, &mut output), + "rule: {rule:?}" + ); } } @@ -261,13 +287,18 @@ mod dsl_test { Value::new_number(-100), Value::Bool(true), Value::Array(vec![Value::Bool(false)]), - Value::Number(Number::from_f64(2.5).expect("should create float number")), + // UnifiedNums can only be positive + Value::Number(Number::from_f64(-2.5).expect("should create float number")), ]; for error_case in error_cases.into_iter() { let rule = Rule::Function(Function::new_bn(error_case)); - assert_eq!(Err(Error::TypeError), rule.eval(&input, &mut output)); + assert_eq!( + Err(Error::TypeError), + rule.clone().eval(&input, &mut output), + "rule: {rule:?}" + ); } } @@ -348,20 +379,24 @@ mod math_functions { }; let cases = vec![ + // Divide non whole number to whole number ( Value::UnifiedNum(100.into()), - Value::UnifiedNum(3.into()), + Value::UnifiedNum(UnifiedNum::from_whole(3)), Value::UnifiedNum(33.into()), ), + // Divide whole numbers ( Value::new_number(100), - Value::UnifiedNum(3.into()), - Value::UnifiedNum(33.into()), + Value::UnifiedNum(UnifiedNum::from_whole(3)), + // 33.33 333 333 + Value::UnifiedNum(UnifiedNum::from(33_33_333_333)), ), ( - Value::UnifiedNum(100.into()), + Value::UnifiedNum(UnifiedNum::from_whole(100)), Value::new_number(3), - Value::UnifiedNum(33.into()), + // 33.33 333 333 + Value::UnifiedNum(UnifiedNum::from(33_33_333_333)), ), ( Value::Number(Number::from_f64(100.0).expect("should create float number")), @@ -380,7 +415,11 @@ mod math_functions { for (lhs, rhs, expected) in cases.into_iter() { let rule = Rule::Function(Function::new_div(lhs, rhs)); - assert_eq!(Ok(Some(expected)), rule.eval(&input, &mut output)); + assert_eq!( + Ok(Some(expected)), + rule.clone().eval(&input, &mut output), + "rule: {rule:?}" + ); } } #[test] @@ -394,9 +433,9 @@ mod math_functions { let cases = vec![ ( - Value::UnifiedNum(3.into()), - Value::UnifiedNum(1000.into()), - Value::UnifiedNum(3000.into()), + Value::UnifiedNum(UnifiedNum::from_whole(3)), + Value::UnifiedNum(UnifiedNum::from_whole(1000)), + Value::UnifiedNum(UnifiedNum::from_whole(3000)), ), ( Value::new_number(3), @@ -404,9 +443,9 @@ mod math_functions { Value::UnifiedNum(3000.into()), ), ( - Value::UnifiedNum(3.into()), + Value::UnifiedNum(UnifiedNum::from_whole(3)), Value::new_number(1000), - Value::UnifiedNum(3000.into()), + Value::UnifiedNum(UnifiedNum::from_whole(3000)), ), ( Value::Number(Number::from_f64(0.5).expect("should create float number")), @@ -423,7 +462,11 @@ mod math_functions { for (lhs, rhs, expected) in cases.into_iter() { let rule = Rule::Function(Function::new_mul(lhs, rhs)); - assert_eq!(Ok(Some(expected)), rule.eval(&input, &mut output)); + assert_eq!( + Ok(Some(expected)), + rule.clone().eval(&input, &mut output), + "rule: {rule:?}" + ); } } #[test] @@ -443,13 +486,13 @@ mod math_functions { ), ( Value::new_number(10), - Value::UnifiedNum(3.into()), - Value::UnifiedNum(1.into()), + Value::UnifiedNum(UnifiedNum::from_whole(3)), + Value::UnifiedNum(UnifiedNum::from_whole(1)), ), ( - Value::UnifiedNum(10.into()), + Value::UnifiedNum(UnifiedNum::from_whole(10)), Value::new_number(4), - Value::UnifiedNum(2.into()), + Value::UnifiedNum(UnifiedNum::from_whole(2)), ), ( Value::Number(Number::from_f64(10.0).expect("should create float number")), @@ -466,7 +509,11 @@ mod math_functions { for (lhs, rhs, expected) in cases.into_iter() { let rule = Function::new_mod(lhs, rhs); - assert_eq!(Ok(Some(expected)), rule.eval(&input, &mut output)); + assert_eq!( + Ok(Some(expected)), + rule.clone().eval(&input, &mut output), + "rule: {rule:?}" + ); } } #[test] @@ -486,13 +533,13 @@ mod math_functions { ), ( Value::new_number(2), - Value::UnifiedNum(2.into()), - Value::UnifiedNum(4.into()), + Value::UnifiedNum(UnifiedNum::from_whole(2)), + Value::UnifiedNum(UnifiedNum::from_whole(4)), ), ( - Value::UnifiedNum(2.into()), + Value::UnifiedNum(UnifiedNum::from_whole(2)), Value::new_number(2), - Value::UnifiedNum(4.into()), + Value::UnifiedNum(UnifiedNum::from_whole(4)), ), ( Value::Number(Number::from_f64(2.2).expect("should create float number")), @@ -509,7 +556,11 @@ mod math_functions { for (lhs, rhs, expected) in cases.into_iter() { let rule = Rule::Function(Function::new_add(lhs, rhs)); - assert_eq!(Ok(Some(expected)), rule.eval(&input, &mut output)); + assert_eq!( + Ok(Some(expected)), + rule.eval(&input, &mut output), + "rule: {rule:?}" + ); } } #[test] @@ -522,37 +573,46 @@ mod math_functions { }; let cases = vec![ + // Not whole number operation ( + // 0.00 000 010 Value::UnifiedNum(10.into()), Value::UnifiedNum(2.into()), Value::UnifiedNum(8.into()), ), + // whole number operations ( - Value::new_number(10), - Value::UnifiedNum(10.into()), - Value::UnifiedNum(0.into()), + Value::new_number(10_u64), + Value::UnifiedNum(UnifiedNum::from_whole(10)), + Value::UnifiedNum(UnifiedNum::ZERO), ), ( - Value::UnifiedNum(10.into()), - Value::new_number(5), - Value::UnifiedNum(5.into()), + Value::UnifiedNum(UnifiedNum::from_whole(10)), + Value::new_number(5_u64), + Value::UnifiedNum(UnifiedNum::from_whole(5)), ), + // Float whole number operation ( Value::Number(Number::from_f64(8.4).expect("should create float number")), Value::Number(Number::from_f64(2.7).expect("should create float number")), Value::Number(Number::from_f64(5.7).expect("should create float number")), ), + // Whole number operation ( - Value::new_number(10), - Value::new_number(4), - Value::new_number(6), + Value::new_number(10_u64), + Value::new_number(4_u64), + Value::new_number(6_u64), ), ]; for (lhs, rhs, expected) in cases.into_iter() { let rule = Rule::Function(Function::new_sub(lhs, rhs)); - assert_eq!(Ok(Some(expected)), rule.eval(&input, &mut output)); + assert_eq!( + Ok(Some(expected.clone())), + rule.eval(&input, &mut output), + "rule: {rule:?}" + ); } } #[test] @@ -572,13 +632,13 @@ mod math_functions { ), ( Value::new_number(10), - Value::UnifiedNum(100.into()), - Value::UnifiedNum(10.into()), + Value::UnifiedNum(UnifiedNum::from_whole(100)), + Value::UnifiedNum(UnifiedNum::from_whole(10)), ), ( - Value::UnifiedNum(10.into()), + Value::UnifiedNum(UnifiedNum::from_whole(10)), Value::new_number(10), - Value::UnifiedNum(10.into()), + Value::UnifiedNum(UnifiedNum::from_whole(10)), ), ( Value::Number(Number::from_f64(0.1).expect("should create float number")), @@ -595,7 +655,11 @@ mod math_functions { for (lhs, rhs, expected) in cases.into_iter() { let rule = Rule::Function(Function::new_min(lhs, rhs)); - assert_eq!(Ok(Some(expected)), rule.eval(&input, &mut output)); + assert_eq!( + Ok(Some(expected)), + rule.eval(&input, &mut output), + "rule: {rule:?}" + ); } } #[test] @@ -615,13 +679,13 @@ mod math_functions { ), ( Value::new_number(10), - Value::UnifiedNum(100.into()), - Value::UnifiedNum(100.into()), + Value::UnifiedNum(UnifiedNum::from_whole(100)), + Value::UnifiedNum(UnifiedNum::from_whole(100)), ), ( - Value::UnifiedNum(10.into()), + Value::UnifiedNum(UnifiedNum::from_whole(10)), Value::new_number(10), - Value::UnifiedNum(10.into()), + Value::UnifiedNum(UnifiedNum::from_whole(10)), ), ( Value::Number(Number::from_f64(0.1).expect("should create float number")), @@ -701,11 +765,16 @@ mod math_functions { ), ( Value::new_number(100), - Value::UnifiedNum(100.into()), + Value::UnifiedNum(UnifiedNum::from_whole(100)), Value::Bool(true), ), ( - Value::UnifiedNum(10.into()), + Value::new_number(100), + Value::UnifiedNum(100.into()), + Value::Bool(false), + ), + ( + Value::UnifiedNum(UnifiedNum::from_whole(10)), Value::new_number(100), Value::Bool(true), ), @@ -714,6 +783,11 @@ mod math_functions { Value::Number(Number::from_f64(0.11).expect("should create float number")), Value::Bool(true), ), + ( + Value::new_number(20), + Value::new_number(15), + Value::Bool(false), + ), ( Value::new_number(0), Value::new_number(0), @@ -724,7 +798,11 @@ mod math_functions { for (lhs, rhs, expected) in cases.into_iter() { let rule = Rule::Function(Function::new_lte(lhs, rhs)); - assert_eq!(Ok(Some(expected)), rule.eval(&input, &mut output)); + assert_eq!( + Ok(Some(expected)), + rule.clone().eval(&input, &mut output), + "rule: {rule:?}" + ); } } #[test] @@ -744,14 +822,19 @@ mod math_functions { ), ( Value::new_number(100), - Value::UnifiedNum(100.into()), + Value::UnifiedNum(UnifiedNum::from_whole(100)), Value::Bool(false), ), ( - Value::UnifiedNum(10.into()), + Value::UnifiedNum(UnifiedNum::from_whole(10)), Value::new_number(100), Value::Bool(false), ), + ( + Value::new_number(10), + Value::UnifiedNum(UnifiedNum::from_whole(100)), + Value::Bool(false), + ), ( Value::Number(Number::from_f64(0.1).expect("should create float number")), Value::Number(Number::from_f64(0.11).expect("should create float number")), @@ -767,7 +850,11 @@ mod math_functions { for (lhs, rhs, expected) in cases.into_iter() { let rule = Rule::Function(Function::new_gt(lhs, rhs)); - assert_eq!(Ok(Some(expected)), rule.eval(&input, &mut output)); + assert_eq!( + Ok(Some(expected)), + rule.clone().eval(&input, &mut output), + "rule: {rule:?}" + ); } } #[test] @@ -787,11 +874,16 @@ mod math_functions { ), ( Value::new_number(100), - Value::UnifiedNum(100.into()), + Value::UnifiedNum(UnifiedNum::from_whole(100)), Value::Bool(true), ), ( - Value::UnifiedNum(10.into()), + Value::UnifiedNum(UnifiedNum::from_whole(100)), + Value::new_number(100), + Value::Bool(true), + ), + ( + Value::UnifiedNum(UnifiedNum::from_whole(10)), Value::new_number(100), Value::Bool(false), ), @@ -810,7 +902,11 @@ mod math_functions { for (lhs, rhs, expected) in cases.into_iter() { let rule = Rule::Function(Function::new_gte(lhs, rhs)); - assert_eq!(Ok(Some(expected)), rule.eval(&input, &mut output)); + assert_eq!( + Ok(Some(expected)), + rule.clone().eval(&input, &mut output), + "rule: {rule:?}" + ); } } #[test] @@ -870,14 +966,16 @@ mod math_functions { price: Default::default(), }; + // multiply and divide against whole numbers let rule = Rule::Function(Function::new_muldiv( - Value::UnifiedNum(10.into()), - Value::UnifiedNum(10.into()), - Value::UnifiedNum(2.into()), + Value::UnifiedNum(UnifiedNum::from_whole(10)), + Value::UnifiedNum(UnifiedNum::from_whole(10)), + Value::UnifiedNum(UnifiedNum::from_whole(2)), )); assert_eq!( - Ok(Some(Value::UnifiedNum(50.into()))), - rule.eval(&input, &mut output) + Ok(Some(Value::UnifiedNum(UnifiedNum::from_whole(50)))), + rule.clone().eval(&input, &mut output), + "rule: {rule:?}" ); } } diff --git a/primitives/src/unified_num.rs b/primitives/src/unified_num.rs index 715b8f68f..4abaecbf7 100644 --- a/primitives/src/unified_num.rs +++ b/primitives/src/unified_num.rs @@ -3,7 +3,7 @@ use num::{ pow::Pow, rational::Ratio, traits::CheckedRem, CheckedAdd, CheckedDiv, CheckedMul, CheckedSub, Integer, One, }; -use num_derive::{FromPrimitive, Num, NumCast, ToPrimitive, Zero}; +use num_derive::{FromPrimitive, Num, ToPrimitive, Zero}; use parse_display::{Display, FromStr, ParseError}; use serde::{Deserialize, Serialize}; use std::{ @@ -137,7 +137,6 @@ mod whole_number { Clone, Copy, Num, - NumCast, ToPrimitive, FromPrimitive, Zero, @@ -297,25 +296,30 @@ impl One for UnifiedNum { impl Integer for UnifiedNum { fn div_floor(&self, other: &Self) -> Self { - self.0.div_floor(&other.0).into() + UnifiedNum::div_floor(self, &other) } + // TODO: Check math and write tests fn mod_floor(&self, other: &Self) -> Self { self.0.mod_floor(&other.0).into() } + // TODO: Check math and write tests fn gcd(&self, other: &Self) -> Self { self.0.gcd(&other.0).into() } + // TODO: Check math and write tests fn lcm(&self, other: &Self) -> Self { self.0.lcm(&other.0).into() } + // TODO: Check math and write tests fn divides(&self, other: &Self) -> bool { self.0.divides(&other.0) } + // TODO: Check math and write tests fn is_multiple_of(&self, other: &Self) -> bool { self.0.is_multiple_of(&other.0) } @@ -328,6 +332,7 @@ impl Integer for UnifiedNum { !self.is_even() } + // TODO: Check math and write tests fn div_rem(&self, other: &Self) -> (Self, Self) { let (quotient, remainder) = self.0.div_rem(&other.0); @@ -539,32 +544,46 @@ impl CheckedMul for UnifiedNum { impl CheckedDiv for UnifiedNum { fn checked_div(&self, rhs: &Self) -> Option { - if rhs == &UnifiedNum::ONE { - return Some(*self); - } - - // checks for denom = 0 and panics if it is - // if both are less than 1.0 - // or both are > 1.0 - // or one is left hand side is < 1.0 - // we must use the multiplier - let ratio = if self < &UnifiedNum::ONE && rhs < &UnifiedNum::ONE - || self > &UnifiedNum::ONE && rhs > &UnifiedNum::ONE - || self < &UnifiedNum::ONE && rhs > &UnifiedNum::ONE - { - Ratio::from_integer(self.0).checked_div(&Ratio::new(rhs.0, UnifiedNum::MULTIPLIER)) - } else { - Some(Ratio::new(self.0, rhs.0)) - }; - - ratio.map(|ratio| { - let integer = ratio.round().to_integer(); - - UnifiedNum(integer) - }) + div_unified_num_to_ratio(self, rhs).map(|ratio| UnifiedNum(ratio.floor().to_integer())) } } +/// Flooring, rounding and ceiling of the [`Ratio`] will produce [`u64`] and **not** a [`UnifiedNum`] ready to use value. +/// +/// This means that while `1_u64 / 2_u64 = 0.5` (for [`u64`]) should be rounded to `1_u64`, ceiled to `1_u64` and floored to `0_u64`, +/// the same is not applicable to [`UnifiedNum`]. +/// +/// [`UnifiedNum`] should be rounded based on the [`UnifiedNum::MULTIPLIER`], +/// i.e. `UnifiedNum(1_u64)` (or `0.00 000 001`) should be rounded to `0` and ceiled to `UnifiedNum::ONE` +/// (`1_00_000_000_u64` or `1.00 000 000` in [`UnifiedNum`] precision) +fn div_unified_num_to_ratio(lhs: &UnifiedNum, rhs: &UnifiedNum) -> Option> { + if rhs == &UnifiedNum::ONE { + return Some(Ratio::from_integer(lhs.0)); + } + + if rhs == &UnifiedNum::ZERO { + return None; + } + + // checks for denom = 0 and panics if it is + // if both are < 1.0 + // or both are > 1.0 + // or one of the sides is < 1.0 with the special case of lhs >= UnifiedNum::ONE (1_00_000_000) + // we must use the multiplier + let ratio = if lhs < &UnifiedNum::ONE && rhs < &UnifiedNum::ONE // 0.5 / 0.8 + || lhs > &UnifiedNum::ONE && rhs > &UnifiedNum::ONE // 1 / 2 | 7 / 2 | 15 / 3 + || lhs < &UnifiedNum::ONE && rhs > &UnifiedNum::ONE // 0.1 / 1.2 + || lhs >= &UnifiedNum::ONE && rhs < &UnifiedNum::ONE + // 1 / 0.1 | 1.2 / 0.2 + { + Ratio::new(lhs.0, rhs.0).checked_mul(&Ratio::new(UnifiedNum::MULTIPLIER, 1)) + } else { + Some(Ratio::new(lhs.0, rhs.0)) + }; + + ratio +} + #[cfg(test)] mod test { use super::*; @@ -584,7 +603,129 @@ mod test { } #[test] - fn unified_num_displays_debug_and_de_serializes_correctly() { + fn test_unified_num_div_to_u64_ratio() { + let one = UnifiedNum::one(); + let two = one + one; + let three = one + one + one; + let zero = UnifiedNum::zero(); + let one_tenth = UnifiedNum::from_whole(0.1); + // 0.00 000 001 = UnifiedNum(1) + // the smallest representable value of UnifiedNum + let smallest = UnifiedNum::from(1); + // 0.00 000 015 = UnifiedNum(15) + let fifteen = UnifiedNum::from(15); + + // 2 / 0.1 = 20 + assert_eq!( + UnifiedNum::from_whole(20), + UnifiedNum( + div_unified_num_to_ratio(&two, &one_tenth) + .unwrap() + .to_integer() + ) + ); + assert_eq!(UnifiedNum::from_whole(20), two / one_tenth); + + assert_eq!( + UnifiedNum::from_whole(30), + UnifiedNum( + div_unified_num_to_ratio(&three, &one_tenth) + .unwrap() + .to_integer() + ) + ); + assert_eq!(UnifiedNum::from_whole(30), three / one_tenth); + + // 1 / 0.1 = 10 + assert_eq!( + UnifiedNum::from_whole(10), + UnifiedNum( + div_unified_num_to_ratio(&one, &one_tenth) + .unwrap() + .to_integer() + ) + ); + assert_eq!(UnifiedNum::from_whole(10), one / one_tenth); + + // 0.1 / 1 = 0.1 + assert_eq!( + UnifiedNum::from_whole(0.1), + UnifiedNum( + div_unified_num_to_ratio(&one_tenth, &one) + .unwrap() + .to_integer() + ) + ); + assert_eq!(UnifiedNum::from_whole(0.1), one_tenth / one); + + // 0.1 / 2 = 0.05 + assert_eq!( + UnifiedNum::from_whole(0.05), + UnifiedNum( + div_unified_num_to_ratio(&one_tenth, &two) + .unwrap() + .to_integer() + ) + ); + assert_eq!(UnifiedNum::from_whole(0.05), one_tenth / two); + // 0.00 000 001 + // should round + assert_eq!( + UnifiedNum::from_whole(0.00_000_001), + UnifiedNum( + div_unified_num_to_ratio(&smallest, &two) + .unwrap() + .round() + .to_integer() + ) + ); + + // 0.00 000 000 + // should floor + assert_eq!( + UnifiedNum::from_whole(0), + UnifiedNum( + div_unified_num_to_ratio(&smallest, &two) + .unwrap() + .floor() + .to_integer() + ) + ); + assert_eq!(UnifiedNum::from_whole(0), smallest / two); + assert_eq!(UnifiedNum::from_whole(0), smallest.div_floor(&two)); + + // 0.00 000 015 / 2 = 0.00 000 007 5 + // 0.00 000 008 (when rounding) + // 0.00 000 007 (when flooring) + assert_eq!( + UnifiedNum::from(8), + UnifiedNum( + div_unified_num_to_ratio(&fifteen, &two) + .unwrap() + .round() + .to_integer() + ) + ); + assert_eq!(UnifiedNum::from(7), fifteen.div_floor(&two)); + + // Check Division with zero & Zero division + { + assert_eq!(None, div_unified_num_to_ratio(&one, &zero), "Division by 0"); + assert_eq!( + zero, + UnifiedNum( + div_unified_num_to_ratio(&zero, &one) + .unwrap() + .round() + .to_integer() + ), + "0 divided by any number is 0" + ); + } + } + + #[test] + fn test_unified_num_displays_debug_and_de_serializes_correctly() { let zero = UnifiedNum::zero(); let one = { let manual_one = UnifiedNum::from(100_000_000); @@ -615,7 +756,7 @@ mod test { } #[test] - fn test_convert_unified_num_to_new_precision_and_from_precision() { + fn test_unified_num_convert_to_new_precision_and_from_precision() { let dai_precision: u8 = 18; let usdt_precision: u8 = 6; let same_precision = UnifiedNum::PRECISION; @@ -693,7 +834,7 @@ mod test { // Case 2: // 3.0 / 0.1 = 30 // 3 00 000 000 / 10 000 000 = 30 - assert_eq!(UnifiedNum::from(30), three / one_tenth); + assert_eq!(UnifiedNum::from_whole(30), three / one_tenth); // Case 3: // 3.0 / 1.0 = 3.0 @@ -706,9 +847,9 @@ mod test { assert_eq!(UnifiedNum::from(1_50_000_000), three / two); // Case 4: - // 2.0 / 3.0 = 0.66666667 - // 3 00 000 000 / 2 00 000 000 * MULTIPLIER = 66 666 667 - assert_eq!(UnifiedNum::from(66_666_667), two / three); + // 2.0 / 3.0 = 0.6666666... + // 3 00 000 000 / 2 00 000 000 * MULTIPLIER = 66 666 666 (floored) + assert_eq!(UnifiedNum::from(66_666_666), two / three); // Case 5: // 0.1 / 3.0 = 0.03333333 @@ -790,24 +931,26 @@ mod test { #[test] fn test_unified_num_rem_and_checked_rem_and_with_whole() { // 10.0 % 3.0 = 1.0 - assert_eq!( - UnifiedNum::ONE, - UnifiedNum::from(10_00_000_000) % UnifiedNum::from(3_00_000_000) - ); + { + assert_eq!( + UnifiedNum::ONE, + UnifiedNum::from(10_00_000_000) % UnifiedNum::from(3_00_000_000) + ); - assert_eq!( - UnifiedNum::ONE, - UnifiedNum::from_whole(10) % UnifiedNum::from_whole(3) - ); + assert_eq!( + UnifiedNum::ONE, + UnifiedNum::from_whole(10) % UnifiedNum::from_whole(3) + ); - assert_eq!( - UnifiedNum::ONE, - UnifiedNum::from_whole(10.0) % UnifiedNum::from_whole(3.0) - ); - assert_eq!( - UnifiedNum::from(100_000_000), - UnifiedNum::from_whole(10) % UnifiedNum::from_whole(3) - ); + assert_eq!( + UnifiedNum::ONE, + UnifiedNum::from_whole(10.0) % UnifiedNum::from_whole(3.0) + ); + assert_eq!( + UnifiedNum::from(100_000_000), + UnifiedNum::from_whole(10) % UnifiedNum::from_whole(3) + ); + } // 10.0 % 0.3 = 0.1 { @@ -827,6 +970,24 @@ mod test { ); } + // 10.0 % 0.03 = 0.01 + { + assert_eq!( + UnifiedNum::from_whole(10.0), + UnifiedNum::from(10_00_000_000) % UnifiedNum::from_whole(3_000_000) + ); + + assert_eq!( + UnifiedNum::from(1_000_000), + UnifiedNum::from_whole(10.0) % UnifiedNum::from_whole(0.03) + ); + + assert_eq!( + UnifiedNum::from(1_000_000), + UnifiedNum::from_whole(10) % UnifiedNum::from_whole(0.03) + ); + } + // 0.3 % 10.0 = 0.3 { assert_eq!( diff --git a/primitives/src/validator.rs b/primitives/src/validator.rs index 8b6adc472..1e800bc51 100644 --- a/primitives/src/validator.rs +++ b/primitives/src/validator.rs @@ -115,7 +115,7 @@ pub struct ValidatorDesc { /// /// Each fee is calculated based on the payout for an event. /// - /// payout * fee / 1000 = event fee payoout + /// payout * fee / 1000 = event fee payout pub fee: UnifiedNum, #[serde(default, skip_serializing_if = "Option::is_none")] /// The address which will receive the fees From 2115a0acba11347a830f8053c4f9c2c7af574b54 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Mon, 16 May 2022 15:53:48 +0200 Subject: [PATCH 06/15] UnifiedNum - fix div_floor and add more tests --- primitives/src/unified_num.rs | 195 ++++++++++++++++------------------ 1 file changed, 91 insertions(+), 104 deletions(-) diff --git a/primitives/src/unified_num.rs b/primitives/src/unified_num.rs index 4abaecbf7..19157fdc4 100644 --- a/primitives/src/unified_num.rs +++ b/primitives/src/unified_num.rs @@ -194,8 +194,14 @@ impl UnifiedNum { pub const ONE: UnifiedNum = UnifiedNum(100_000_000); pub fn div_floor(&self, other: &Self) -> Self { - // just division will floor the result! - self / other + let ratio = + div_unified_num_to_ratio(self, other).expect("Failed to create ratio for div_floor"); + + let whole_number = ratio + .checked_div(&Ratio::from_integer(UnifiedNum::MULTIPLIER)) + .expect("Should divide with Multiplier for div_floor"); + + UnifiedNum::from_whole(whole_number.to_integer()) } pub const fn from_u64(value: u64) -> Self { @@ -617,97 +623,93 @@ mod test { // 2 / 0.1 = 20 assert_eq!( - UnifiedNum::from_whole(20), - UnifiedNum( - div_unified_num_to_ratio(&two, &one_tenth) - .unwrap() - .to_integer() - ) + 20 * UnifiedNum::MULTIPLIER, + div_unified_num_to_ratio(&two, &one_tenth) + .unwrap() + .to_integer() ); - assert_eq!(UnifiedNum::from_whole(20), two / one_tenth); + // 3 / 0.1 = 30 assert_eq!( - UnifiedNum::from_whole(30), - UnifiedNum( - div_unified_num_to_ratio(&three, &one_tenth) - .unwrap() - .to_integer() - ) + 30 * UnifiedNum::MULTIPLIER, + div_unified_num_to_ratio(&three, &one_tenth) + .unwrap() + .to_integer() ); - assert_eq!(UnifiedNum::from_whole(30), three / one_tenth); // 1 / 0.1 = 10 assert_eq!( - UnifiedNum::from_whole(10), - UnifiedNum( - div_unified_num_to_ratio(&one, &one_tenth) - .unwrap() - .to_integer() - ) + 10 * UnifiedNum::MULTIPLIER, + div_unified_num_to_ratio(&one, &one_tenth) + .unwrap() + .to_integer() ); - assert_eq!(UnifiedNum::from_whole(10), one / one_tenth); // 0.1 / 1 = 0.1 assert_eq!( - UnifiedNum::from_whole(0.1), - UnifiedNum( - div_unified_num_to_ratio(&one_tenth, &one) - .unwrap() - .to_integer() - ) + 10_000_000_u64, + div_unified_num_to_ratio(&one_tenth, &one) + .unwrap() + .to_integer() ); - assert_eq!(UnifiedNum::from_whole(0.1), one_tenth / one); // 0.1 / 2 = 0.05 assert_eq!( - UnifiedNum::from_whole(0.05), - UnifiedNum( - div_unified_num_to_ratio(&one_tenth, &two) - .unwrap() - .to_integer() - ) + 5_000_000, + div_unified_num_to_ratio(&one_tenth, &two) + .unwrap() + .to_integer() ); - assert_eq!(UnifiedNum::from_whole(0.05), one_tenth / two); + // 0.00 000 001 + // 0.00 000 001 / 2.0 = 0.00 000 001 // should round assert_eq!( - UnifiedNum::from_whole(0.00_000_001), - UnifiedNum( - div_unified_num_to_ratio(&smallest, &two) - .unwrap() - .round() - .to_integer() - ) + 1_u64, + div_unified_num_to_ratio(&smallest, &two) + .unwrap() + .round() + .to_integer() ); // 0.00 000 000 // should floor assert_eq!( - UnifiedNum::from_whole(0), - UnifiedNum( - div_unified_num_to_ratio(&smallest, &two) - .unwrap() - .floor() - .to_integer() - ) + 0, + div_unified_num_to_ratio(&smallest, &two) + .unwrap() + .floor() + .to_integer() ); - assert_eq!(UnifiedNum::from_whole(0), smallest / two); - assert_eq!(UnifiedNum::from_whole(0), smallest.div_floor(&two)); // 0.00 000 015 / 2 = 0.00 000 007 5 // 0.00 000 008 (when rounding) // 0.00 000 007 (when flooring) assert_eq!( - UnifiedNum::from(8), - UnifiedNum( - div_unified_num_to_ratio(&fifteen, &two) - .unwrap() - .round() - .to_integer() - ) + 8, + div_unified_num_to_ratio(&fifteen, &two) + .unwrap() + .round() + .to_integer() ); - assert_eq!(UnifiedNum::from(7), fifteen.div_floor(&two)); + assert_eq!( + 7, + div_unified_num_to_ratio(&fifteen, &two) + .unwrap() + .floor() + .to_integer() + ); + + // should ceil to smallest + // 0.00 000 001 / 3.0 = 0.00 000 000 3333.. + assert_eq!( + smallest.to_u64(), + div_unified_num_to_ratio(&smallest, &three) + .unwrap() + .ceil() + .to_integer() + ); // Check Division with zero & Zero division { assert_eq!(None, div_unified_num_to_ratio(&one, &zero), "Division by 0"); @@ -822,78 +824,70 @@ mod test { // 3.0 let three = UnifiedNum::from(3_00_000_000); + let fifteen = UnifiedNum::from_whole(15); + // division { - // Case 1: // 0.0003 / 0.1 = 0.003 - // - // 0.00 030 000 / 0.10 000 000 = 0.00 300 000 - // 30 000 / 10 000 000 = 300 000 assert_eq!(UnifiedNum::from(300_000), three_ten_thousands / one_tenth); - // Case 2: - // 3.0 / 0.1 = 30 - // 3 00 000 000 / 10 000 000 = 30 + // 3.0 / 0.1 = 30.0 assert_eq!(UnifiedNum::from_whole(30), three / one_tenth); + // 2.0 / 0.1 = 20.0 + assert_eq!(UnifiedNum::from_whole(20), two / one_tenth); + // 1.0 / 0.1 = 10.0 + assert_eq!(UnifiedNum::from_whole(10), one / one_tenth); - // Case 3: // 3.0 / 1.0 = 3.0 - // 3 00 000 000 / 1 00 000 000 * MULTIPLIER = 3 00 000 000 assert_eq!(three, three / one); - // Case 4: // 3.0 / 2.0 = 1.5 - // 3 00 000 000 / 2 00 000 000 * MULTIPLIER = 1 50 000 000 assert_eq!(UnifiedNum::from(1_50_000_000), three / two); - // Case 4: // 2.0 / 3.0 = 0.6666666... - // 3 00 000 000 / 2 00 000 000 * MULTIPLIER = 66 666 666 (floored) assert_eq!(UnifiedNum::from(66_666_666), two / three); - // Case 5: // 0.1 / 3.0 = 0.03333333 - // 10 000 000 / 3 00 000 000 * MULTIPLIER = 3 333 333 assert_eq!(UnifiedNum::from(3_333_333), one_tenth / three); + + // 0.1 / 2.0 = 0.05 + assert_eq!(UnifiedNum::from_whole(0.05), one_tenth / two); + + // 0.1 / 1.0 = 0.1 + assert_eq!(one_tenth, one_tenth / one); + + // 15.0 / 2 = 7.5 + assert_eq!(UnifiedNum::from_whole(7.5), fifteen / &two); } // multiplication { - // Case 1: // 0.0003 * 1 = 0.0003 - // 0.00 030 000 * 1 = 0.00 030 000 assert_eq!(three_ten_thousands * one, three_ten_thousands); - // Case 2: // 3 * 1 = 3 assert_eq!(three * one, three); - // Case 3: // 0.0003 * 0.1 = 0.00003 - // 0.00030000 * 0.10000000 = 0.00 003 000 assert_eq!(three_ten_thousands * one_tenth, UnifiedNum::from(3_000_u64)); - // Case 4: // 0.0003 * 2 = 0.0006 - // 0.00 030 000 * 2 = 0.00 060 000 assert_eq!(three_ten_thousands * two, UnifiedNum::from(60_000_u64)); - // Case 5: // 3 * 2 = 6 assert_eq!(three * two, UnifiedNum::from(600_000_000_u64)); - // Case 6: - // 3 * 0.1 = 0.30 000 000 + // 3 * 0.1 = 0.3 assert_eq!(three * one_tenth, UnifiedNum::from(30_000_000_u64)); } // Mul & then Div with `checked_mul` & `checked_div` { - // 0.00030 * 0.1 / 1000.0 = 0.00 000 003 + // 0.0003 * 0.1 / 1000.0 = 0.00 000 003 // 30 000 * 10 000 000 / 1 000 00 000 000 = 3 - let result = UnifiedNum::from(30_000) - .checked_mul(&UnifiedNum::from(10_000_000)) - .and_then(|number| number.checked_div(&UnifiedNum::from(1_000_00_000_000))) + let result = UnifiedNum::from_whole(0.0003) + .checked_mul(&UnifiedNum::from_whole(0.1)) + .and_then(|number| number.checked_div(&UnifiedNum::from_whole(1_000))) .unwrap(); assert_eq!(UnifiedNum::from(3), result); @@ -908,23 +902,16 @@ mod test { // div_floor { - // 0.00006 * 0.1 / 1000.0 = 0.00 000 000 6 - // 6 000 * 10 000 000 / 1 000 00 000 000 = 0.6 = UnifiedNum(0) - let result = UnifiedNum::from(6_000) - .checked_mul(&UnifiedNum::from(10_000_000)) - .map(|number| number.div_floor(&UnifiedNum::from(1_000_00_000_000))) - .unwrap(); - + // 1.2 / 2 = 0.6 = 0.0 (floored) + let result = UnifiedNum::from_whole(1.2).div_floor(&UnifiedNum::from_whole(2)); assert_eq!(UnifiedNum::ZERO, result); - // 0.00016 * 0.1 / 1000.0 = 0.00 000 001 6 - // 16 000 * 10 000 000 / 1 000 00 000 000 = 1.6 = UnifiedNum(1) - let result = UnifiedNum::from(16_000) - .checked_mul(&UnifiedNum::from(10_000_000)) - .map(|number| number.div_floor(&UnifiedNum::from(1_000_00_000_000))) - .unwrap(); + // 3.8 / 2 = 1.9 = 1.0 (floored) + let result = UnifiedNum::from_whole(3.2).div_floor(&UnifiedNum::from_whole(2)); + assert_eq!(UnifiedNum::ONE, result); - assert_eq!(UnifiedNum::from(1), result); + // 15 / 2 = 7 (floored) + assert_eq!(UnifiedNum::from_whole(7), fifteen.div_floor(&two)); } } From 1fb5c4d4f011ad62fe1ebca9bdc6bcb5c5588032 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Mon, 16 May 2022 16:38:23 +0200 Subject: [PATCH 07/15] primitives - UnifiedNum - use from_whole when mul with u64 --- primitives/src/unified_num.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/primitives/src/unified_num.rs b/primitives/src/unified_num.rs index 19157fdc4..3935c8dad 100644 --- a/primitives/src/unified_num.rs +++ b/primitives/src/unified_num.rs @@ -460,7 +460,7 @@ impl Mul for &UnifiedNum { fn mul(self, rhs: u64) -> Self::Output { // Use &UnifiedNum * &UnifiedNum - self * &UnifiedNum(rhs) + self * &UnifiedNum::from_whole(rhs) } } @@ -469,7 +469,7 @@ impl Mul for UnifiedNum { fn mul(self, rhs: u64) -> Self::Output { // Use &UnifiedNum * &UnifiedNum - &self * &UnifiedNum(rhs) + &self * &UnifiedNum::from_whole(rhs) } } @@ -478,7 +478,7 @@ impl Mul for u64 { fn mul(self, rhs: UnifiedNum) -> Self::Output { // Use &UnifiedNum * &UnifiedNum - &UnifiedNum(self) * &rhs + &UnifiedNum::from_whole(self) * &rhs } } @@ -487,7 +487,7 @@ impl Mul<&UnifiedNum> for u64 { fn mul(self, rhs: &UnifiedNum) -> Self::Output { // Use &UnifiedNum * &UnifiedNum - &UnifiedNum(self) * rhs + &UnifiedNum::from_whole(self) * rhs } } From 9235ec2b6a8f4d0f584e4d7fba5bebc77e183a01 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Mon, 16 May 2022 16:47:03 +0200 Subject: [PATCH 08/15] sentry - fix tests related to UnifiedNum & fees --- primitives/src/test_util.rs | 8 ++--- sentry/src/db/analytics.rs | 5 +-- sentry/src/routes/campaign.rs | 28 ++++++++++------ sentry/src/spender.rs | 62 ++++++++++++++++++++++++----------- 4 files changed, 68 insertions(+), 35 deletions(-) diff --git a/primitives/src/test_util.rs b/primitives/src/test_util.rs index 88ed5dad8..94fe74ee7 100644 --- a/primitives/src/test_util.rs +++ b/primitives/src/test_util.rs @@ -10,7 +10,7 @@ use crate::{ sentry::{CLICK, IMPRESSION}, targeting::Rules, AdUnit, Address, Campaign, Channel, EventSubmission, UnifiedNum, ValidatorDesc, ValidatorId, - IPFS, + IPFS, unified_num::FromWhole, }; pub use logger::discard_logger; @@ -145,14 +145,14 @@ mod logger { pub static DUMMY_VALIDATOR_LEADER: Lazy = Lazy::new(|| ValidatorDesc { id: IDS[&LEADER], url: "http://localhost:8005".to_string(), - fee: 100.into(), + fee: UnifiedNum::from_whole(0.02), fee_addr: None, }); pub static DUMMY_VALIDATOR_FOLLOWER: Lazy = Lazy::new(|| ValidatorDesc { id: IDS[&FOLLOWER], url: "http://localhost:8006".to_string(), - fee: 100.into(), + fee: UnifiedNum::from_whole(0.03), fee_addr: None, }); @@ -179,7 +179,7 @@ pub static DUMMY_CAMPAIGN: Lazy = Lazy::new(|| { }, creator: *CREATOR, // 1000.00000000 - budget: UnifiedNum::from(100_000_000_000), + budget: UnifiedNum::from_whole(1_000), validators: Validators::new(( DUMMY_VALIDATOR_LEADER.clone(), DUMMY_VALIDATOR_FOLLOWER.clone(), diff --git a/sentry/src/db/analytics.rs b/sentry/src/db/analytics.rs index 1c7499b3a..19942a60b 100644 --- a/sentry/src/db/analytics.rs +++ b/sentry/src/db/analytics.rs @@ -159,6 +159,7 @@ mod test { }, sentry::{DateHour, CLICK, IMPRESSION}, test_util::{CREATOR, DUMMY_AD_UNITS, DUMMY_CAMPAIGN, DUMMY_IPFS, PUBLISHER, PUBLISHER_2}, + unified_num::FromWhole, AdUnit, UnifiedNum, ValidatorId, IPFS, }; @@ -314,12 +315,12 @@ mod test { let amount_per_day: UnifiedNum = hours .clone() - .map(|hour| UnifiedNum::from(hour as u64 * 100_000_000)) + .map(|hour| UnifiedNum::from_whole(hour as u64)) .sum::>() .expect("Should not overflow"); let amount_for_month: UnifiedNum = december_days .clone() - .map(|day_n| UnifiedNum::from(amount_per_day * day_n as u64)) + .map(|day_n| amount_per_day * UnifiedNum::from_whole(day_n as u64)) .sum::>() .expect("Should not overflow"); diff --git a/sentry/src/routes/campaign.rs b/sentry/src/routes/campaign.rs index 925a3b2c3..97c6e3d56 100644 --- a/sentry/src/routes/campaign.rs +++ b/sentry/src/routes/campaign.rs @@ -583,7 +583,7 @@ pub mod insert_events { sentry::{Event, SuccessResponse}, Address, Campaign, CampaignId, ChainOf, DomainError, UnifiedNum, ValidatorDesc, }; - use slog::error; + use slog::{debug, error, Logger}; use thiserror::Error; #[derive(Debug, Error)] @@ -808,7 +808,10 @@ pub mod insert_events { #[cfg(test)] mod test { - use primitives::test_util::{DUMMY_CAMPAIGN, PUBLISHER}; + use primitives::{ + test_util::{DUMMY_CAMPAIGN, PUBLISHER}, + unified_num::FromWhole, + }; use redis::aio::MultiplexedConnection; use crate::db::{ @@ -924,7 +927,7 @@ pub mod insert_events { let leader = campaign.leader().unwrap(); let follower = campaign.follower().unwrap(); - let payout = UnifiedNum::from(300); + let payout = UnifiedNum::from_whole(0.03); // No Campaign Remaining set, should error { @@ -952,7 +955,8 @@ pub mod insert_events { // Repeat the same call, but set the Campaign remaining budget in Redis { - set_campaign_remaining(&mut redis, campaign.id, 11_000).await; + // 0.11 budget left + set_campaign_remaining(&mut redis, campaign.id, 11_000_000).await; let spend_event = spend_for_event( &database.pool, @@ -970,13 +974,17 @@ pub mod insert_events { "Campaign budget has no remaining funds to spend or an error occurred" ); - // Payout: 300 - // Leader fee: 100 - // Leader payout: 300 * 100 / 1000 = 30 - // Follower fee: 100 - // Follower payout: 300 * 100 / 1000 = 30 + // Payout: 0.03 + // Leader fee: 0.03 + // Leader payout: 0.03 * 0.03 / 1000.0 = 0.00 000 090 = UnifiedNum(90) + // + // Follower fee: 0.02 + // Follower payout: 0.03 * 0.02 / 1000.0 = 0.00 000 060 = UnifiedNum(60) + + // campaign budget left - payout - leader fee - follower fee + // 0.11 - 0.03 - 0.00 000 090 - 0.00 000 060 = 0.07999850 assert_eq!( - Some(10_640_i64), + Some(7_999_850_i64), campaign_remaining .get_remaining_opt(campaign.id) .await diff --git a/sentry/src/spender.rs b/sentry/src/spender.rs index 43a5ed96d..a54411a20 100644 --- a/sentry/src/spender.rs +++ b/sentry/src/spender.rs @@ -1,8 +1,10 @@ pub mod fee { - pub const PRO_MILLE: UnifiedNum = UnifiedNum::from_u64(1_000); use primitives::{Address, DomainError, UnifiedNum, ValidatorDesc}; + /// Pro mile (`1 000`) in [`UnifiedNum`] precision, i.e. `1_000.00_000_000`. + pub const PRO_MILLE: UnifiedNum = UnifiedNum::from_u64(1_000 * UnifiedNum::MULTIPLIER); + /// Calculates the fee for a given payout of the specified validator /// This function will return None if the provided validator is not part of the Campaign / Channel /// In the case of overflow when calculating the payout, an error will be returned @@ -10,10 +12,11 @@ pub mod fee { (_earner, payout): (Address, UnifiedNum), validator: &ValidatorDesc, ) -> Result { - // should never overflow + // should never overflow, but we guard against overflow + // `UnifiedNum::checked_div` will accurately floor the inner `u64` for the `UnifiedNum::PRECISION` payout .checked_mul(&validator.fee) - .map(|pro_mille_fee| pro_mille_fee.div_floor(&PRO_MILLE)) + .and_then(|pro_mille_fee| pro_mille_fee.checked_div(&PRO_MILLE)) .ok_or_else(|| DomainError::InvalidArgument("payout calculation overflow".to_string())) } @@ -21,47 +24,68 @@ pub mod fee { mod test { use primitives::{ test_util::{DUMMY_VALIDATOR_LEADER, PUBLISHER}, - UnifiedNum, + UnifiedNum, ValidatorDesc, }; use crate::spender::fee::calculate_fee; #[test] - fn test_calcualtion_of_fee() { - let dummy_leader = DUMMY_VALIDATOR_LEADER.clone(); - assert_eq!( - UnifiedNum::from(100), - dummy_leader.fee, - "Dummy validator leader fee has changed, please revisit this test!" - ); + fn test_calculation_of_fee() { + let mut dummy_leader = DUMMY_VALIDATOR_LEADER.clone(); + dummy_leader.fee = UnifiedNum::from(10_000_000); // normal payout - no flooring { - // 300 * 100 / 1000 = 30 - let payout = (*PUBLISHER, UnifiedNum::from(300)); + // 30 000 * 10 000 000 / 1 000 00 000 000 = 3 + + // 0.0003 * 0.1 / 1000.0 = 0.00000003 = UnifiedNum(3) + // 0.00 030 000 * 0.10 000 000 / 1 000 = 0.00 000 003 + let payout = (*PUBLISHER, UnifiedNum::from(30_000)); let validator_fee = calculate_fee(payout, &dummy_leader).expect("Should not overflow"); - assert_eq!(UnifiedNum::from(30), validator_fee); + assert_eq!( + UnifiedNum::from(3), + validator_fee, + "fee should be 0.00000003 in UnifiedNum" + ); } // payout with flooring { - // 66 * 100 / 1000 = 6.6 = 6 - let payout = (*PUBLISHER, UnifiedNum::from(66)); + // 66 000 * 10 000 000 / 100 000 000 000 = 6.6 = 6 + let payout = (*PUBLISHER, UnifiedNum::from(66_000)); let validator_fee = calculate_fee(payout, &dummy_leader).expect("Should not overflow"); assert_eq!(UnifiedNum::from(6), validator_fee); } - // Overflow + // Overflow - even using `Ratio` for `UnifiedNum`, this should overflow { - // u64::MAX * 100 (overflow) / 1000 + let very_high_fee = ValidatorDesc { + fee: UnifiedNum::from(u64::MAX), + ..dummy_leader.clone() + }; + // u64::MAX * u64::MAX / 100 000 000 000 let payout = (*PUBLISHER, UnifiedNum::from(u64::MAX)); - calculate_fee(payout, &dummy_leader).expect_err("Should overflow"); + calculate_fee(payout, &very_high_fee).expect_err("Should overflow"); + } + + // whole number payout + { + // e.g. 3 TOKENs + let payout = (*PUBLISHER, UnifiedNum::from(300_000_000_u64)); + + // 300 000 000 × 10 000 000 / 100 000 000 000 + let validator_fee = + calculate_fee(payout, &dummy_leader).expect("Should not overflow"); + + // : 3 000 000 000 000 + // 0.000003 + assert_eq!(UnifiedNum::from(30_000), validator_fee); } } } From f425a3e73c4fc079b7c3fedf92dce6b53e51bb68 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Mon, 16 May 2022 16:57:02 +0200 Subject: [PATCH 09/15] worker - fix get_health calculation --- primitives/src/unified_num.rs | 6 + validator_worker/src/core/follower_rules.rs | 339 +++++++++++++------- 2 files changed, 229 insertions(+), 116 deletions(-) diff --git a/primitives/src/unified_num.rs b/primitives/src/unified_num.rs index 3935c8dad..c037cc86f 100644 --- a/primitives/src/unified_num.rs +++ b/primitives/src/unified_num.rs @@ -204,10 +204,16 @@ impl UnifiedNum { UnifiedNum::from_whole(whole_number.to_integer()) } + /// This method returns the inner [`u64`] representation of the [`UnifiedNum`]. + /// + /// This method does **not** take into account precision of [`UnifiedNum`]! pub const fn from_u64(value: u64) -> Self { Self(value) } + /// This method returns the inner [`u64`] representation of the [`UnifiedNum`]. + /// + /// This method does **not** take into account precision of [`UnifiedNum`]! pub const fn to_u64(self) -> u64 { self.0 } diff --git a/validator_worker/src/core/follower_rules.rs b/validator_worker/src/core/follower_rules.rs index e5577f0ef..0251e6d69 100644 --- a/validator_worker/src/core/follower_rules.rs +++ b/validator_worker/src/core/follower_rules.rs @@ -1,5 +1,7 @@ use primitives::{UnifiedMap, UnifiedNum}; +static MAX_HEALTH: u64 = 1_000; + pub fn is_valid_transition( all_spenders_sum: UnifiedNum, prev: &UnifiedMap, @@ -24,25 +26,31 @@ pub fn get_health( ) -> Option { let sum_our: UnifiedNum = our.values().sum::>()?; - let zero = UnifiedNum::from(0); let sum_approved_mins = our .iter() - .map(|(acc, val)| val.min(approved.get(acc).unwrap_or(&zero))) + .map(|(acc, val)| val.min(approved.get(acc).unwrap_or(&UnifiedNum::ZERO))) .sum::>()?; if sum_approved_mins >= sum_our { - return Some(1_000); + return Some(MAX_HEALTH); } - let diff = sum_our - sum_approved_mins; - let health_penalty = diff * UnifiedNum::from(1_000) / all_spenders_sum; - Some(1_000 - health_penalty.to_u64()) + // it's easier to work with `u64` instead of later dividing the `UnifiedNum`'s inner `u64` with `10.pow(UnifiedNum::PRECISION)` + let health_penalty = diff + .to_u64() + .checked_mul(MAX_HEALTH)? + .checked_div(all_spenders_sum.to_u64())?; + + Some(MAX_HEALTH - health_penalty) } #[cfg(test)] mod test { - use primitives::test_util::{PUBLISHER, PUBLISHER_2}; + use primitives::{ + test_util::{PUBLISHER, PUBLISHER_2}, + unified_num::FromWhole, + }; use super::*; @@ -52,222 +60,321 @@ mod test { fn is_valid_transition_empty_to_empty() { assert!( is_valid_transition( - UnifiedNum::from_u64(100), + UnifiedNum::from_whole(100_u64), &UnifiedMap::default(), &UnifiedMap::default() ) - .expect("No overflow"), + .expect("Should return health and not overflow"), "is valid transition" ) } #[test] fn is_valid_transition_a_valid_transition() { - let next = vec![(*PUBLISHER, 100.into())].into_iter().collect(); + let next = vec![(*PUBLISHER, UnifiedNum::from_whole(100_u64))] + .into_iter() + .collect(); assert!( - is_valid_transition(UnifiedNum::from_u64(100), &UnifiedMap::default(), &next) - .expect("No overflow"), + is_valid_transition( + UnifiedNum::from_whole(100_u64), + &UnifiedMap::default(), + &next + ) + .expect("Should return health and not overflow"), "is valid transition" ) } #[test] fn is_valid_transition_more_funds_than_all_spenders_sum() { - let next = vec![(*PUBLISHER, 51.into()), (*PUBLISHER_2, 50.into())] - .into_iter() - .collect(); + let next = vec![ + (*PUBLISHER, UnifiedNum::from_whole(51_u64)), + (*PUBLISHER_2, UnifiedNum::from_whole(50_u64)), + ] + .into_iter() + .collect(); assert!( - !is_valid_transition(UnifiedNum::from_u64(100), &UnifiedMap::default(), &next) - .expect("No overflow"), + !is_valid_transition( + UnifiedNum::from_whole(100_u64), + &UnifiedMap::default(), + &next + ) + .expect("Should return health and not overflow"), "not a valid transition" ); } #[test] fn is_valid_transition_single_value_is_lower() { - let prev = vec![(*PUBLISHER, 55.into())].into_iter().collect(); + let prev = vec![(*PUBLISHER, UnifiedNum::from_whole(55_u64))] + .into_iter() + .collect(); - let next = vec![(*PUBLISHER, 54.into())].into_iter().collect(); + let next = vec![(*PUBLISHER, UnifiedNum::from_whole(54_u64))] + .into_iter() + .collect(); assert!( - !is_valid_transition(UnifiedNum::from_u64(100), &prev, &next).expect("No overflow"), + !is_valid_transition(UnifiedNum::from_whole(100_u64), &prev, &next) + .expect("Should return health and not overflow"), "not a valid transition" ); } #[test] fn is_valid_transition_a_value_is_lower_but_overall_sum_is_higher() { - let prev = vec![(*PUBLISHER, 55.into())].into_iter().collect(); - - let next = vec![(*PUBLISHER, 54.into()), (*PUBLISHER_2, 3.into())] + let prev = vec![(*PUBLISHER, UnifiedNum::from_whole(55_u64))] .into_iter() .collect(); + let next = vec![ + (*PUBLISHER, UnifiedNum::from_whole(54_u64)), + (*PUBLISHER_2, UnifiedNum::from_whole(3_u64)), + ] + .into_iter() + .collect(); + assert!( - !is_valid_transition(UnifiedNum::from_u64(100), &prev, &next).expect("No overflow"), + !is_valid_transition(UnifiedNum::from_whole(100_u64), &prev, &next) + .expect("Should return health and not overflow"), "not a valid transition" ); } #[test] fn is_valid_transition_overall_sum_is_lower() { - let prev = vec![(*PUBLISHER, 54.into()), (*PUBLISHER_2, 3.into())] + let prev = vec![ + (*PUBLISHER, UnifiedNum::from_whole(54_u64)), + (*PUBLISHER_2, UnifiedNum::from_whole(3_u64)), + ] + .into_iter() + .collect(); + + let next = vec![(*PUBLISHER, UnifiedNum::from_whole(54_u64))] .into_iter() .collect(); - let next = vec![(*PUBLISHER, 54.into())].into_iter().collect(); - assert!( - !is_valid_transition(UnifiedNum::from_u64(100), &prev, &next).expect("No overflow"), + !is_valid_transition(UnifiedNum::from_whole(100_u64), &prev, &next) + .expect("Should return health and not overflow"), "not a valid transition" ); } #[test] fn is_valid_transition_overall_sum_is_the_same_but_we_remove_an_entry() { - let prev = vec![(*PUBLISHER, 54.into()), (*PUBLISHER_2, 3.into())] + let prev = vec![ + (*PUBLISHER, UnifiedNum::from_whole(54_u64)), + (*PUBLISHER_2, UnifiedNum::from_whole(3_u64)), + ] + .into_iter() + .collect(); + + let next = vec![(*PUBLISHER, UnifiedNum::from_whole(57_u64))] .into_iter() .collect(); - let next = vec![(*PUBLISHER, 57.into())].into_iter().collect(); - assert!( - !is_valid_transition(UnifiedNum::from_u64(100), &prev, &next).expect("No overflow"), + !is_valid_transition(UnifiedNum::from_whole(100_u64), &prev, &next) + .expect("Should return health and not overflow"), "not a valid transition" ); } #[test] fn get_health_the_approved_balance_tree_gte_our_accounting_is_healthy() { - let all_spenders_sum = UnifiedNum::from(50); - let our = vec![(*PUBLISHER, 50.into())].into_iter().collect(); - assert!( - get_health(all_spenders_sum, &our, &our).expect("Should not overflow") - >= HEALTH_THRESHOLD - ); + let all_spenders_sum = UnifiedNum::from_whole(50_u64); + let our = vec![(*PUBLISHER, UnifiedNum::from_whole(50_u64))] + .into_iter() + .collect(); - assert!( - get_health( + { + let health = get_health(all_spenders_sum, &our, &our) + .expect("Should return health and not overflow"); + assert!(health >= HEALTH_THRESHOLD); + } + { + let health = get_health( all_spenders_sum, &our, - &vec![(*PUBLISHER, 60.into())].into_iter().collect() + &vec![(*PUBLISHER, UnifiedNum::from_whole(60_u64))] + .into_iter() + .collect(), ) - .expect("Should not overflow") - >= HEALTH_THRESHOLD - ); + .expect("Should return health and not overflow"); + assert!(health >= HEALTH_THRESHOLD); + } } #[test] fn get_health_the_approved_balance_tree_is_positive_our_accounting_is_0_and_it_is_healthy() { - let approved = vec![(*PUBLISHER, 50.into())].into_iter().collect(); + let approved = vec![(*PUBLISHER, UnifiedNum::from_whole(50_u64))] + .into_iter() + .collect(); - assert!( - get_health(UnifiedNum::from(50), &UnifiedMap::default(), &approved) - .expect("Should not overflow") - >= HEALTH_THRESHOLD - ); + let health = get_health( + UnifiedNum::from_whole(50_u64), + &UnifiedMap::default(), + &approved, + ) + .expect("Should return health and not overflow"); + + assert_eq!(1000, health); + + assert!(health >= HEALTH_THRESHOLD, "healthy"); } #[test] fn get_health_the_approved_balance_tree_has_less_but_within_margin_it_is_healthy() { - let all_spenders_sum = UnifiedNum::from(80); + let all_spenders_sum = UnifiedNum::from_whole(80_u64); - assert!( - get_health( + { + let health = get_health( all_spenders_sum, - &vec![(*PUBLISHER, 80.into())].into_iter().collect(), - &vec![(*PUBLISHER, 79.into())].into_iter().collect() + &vec![(*PUBLISHER, UnifiedNum::from_whole(80_u64))] + .into_iter() + .collect(), + &vec![(*PUBLISHER, UnifiedNum::from_whole(79_u64))] + .into_iter() + .collect(), ) - .expect("Should not overflow") - >= HEALTH_THRESHOLD - ); + .expect("Should return health and not overflow"); - assert!( - get_health( + assert_eq!(health, 988, "Very small difference from all spender sum"); + assert!(health >= HEALTH_THRESHOLD, "healthy"); + } + + { + let health = get_health( all_spenders_sum, - &vec![(*PUBLISHER, 2.into())].into_iter().collect(), - &vec![(*PUBLISHER, 1.into())].into_iter().collect() + &vec![(*PUBLISHER, UnifiedNum::from_whole(2_u64))] + .into_iter() + .collect(), + &vec![(*PUBLISHER, UnifiedNum::from_whole(1_u64))] + .into_iter() + .collect(), ) - .expect("Should not overflow") - >= HEALTH_THRESHOLD - ); + .expect("Should return health and not overflow"); + assert_eq!(health, 988, "Major difference from all spenders sum"); + + assert!(health >= HEALTH_THRESHOLD, "healthy"); + } } #[test] fn get_health_the_approved_balance_tree_has_less_it_is_unhealthy() { - assert!( - get_health( - UnifiedNum::from(80), - &vec![(*PUBLISHER, 80.into())].into_iter().collect(), - &vec![(*PUBLISHER, 70.into())].into_iter().collect() - ) - .expect("Should not overflow") - < HEALTH_THRESHOLD - ); + let health = get_health( + UnifiedNum::from_whole(80_u64), + &vec![(*PUBLISHER, UnifiedNum::from_whole(80_u64))] + .into_iter() + .collect(), + &vec![(*PUBLISHER, UnifiedNum::from_whole(70_u64))] + .into_iter() + .collect(), + ) + .expect("Should return health and not overflow"); + + assert_eq!(875, health); + assert!(health < HEALTH_THRESHOLD, "unhealthy"); } #[test] fn get_health_they_have_the_same_sum_but_different_entities_are_earning() { - let all_spenders_sum = UnifiedNum::from(80); + let all_spenders_sum = UnifiedNum::from_whole(80_u64); - assert!( - get_health( + // Unhealthy + { + let health = get_health( all_spenders_sum, - &vec![(*PUBLISHER, 80.into())].into_iter().collect(), - &vec![(*PUBLISHER_2, 80.into())].into_iter().collect() + &vec![(*PUBLISHER, UnifiedNum::from_whole(80_u64))] + .into_iter() + .collect(), + &vec![(*PUBLISHER_2, UnifiedNum::from_whole(80_u64))] + .into_iter() + .collect(), ) - .expect("Should not overflow") - < HEALTH_THRESHOLD - ); - - assert!( - get_health( + .expect("Should return health and not overflow"); + assert_eq!(health, 0, "None of the spenders match in ours/approved"); + assert!(health < HEALTH_THRESHOLD, "unhealthy"); + } + + // Unhealthy + { + let health = get_health( all_spenders_sum, - &vec![(*PUBLISHER, 80.into())].into_iter().collect(), - &vec![(*PUBLISHER_2, 40.into()), (*PUBLISHER, 40.into())] + &vec![(*PUBLISHER, UnifiedNum::from_whole(80_u64))] .into_iter() - .collect() + .collect(), + &vec![ + (*PUBLISHER_2, UnifiedNum::from_whole(40_u64)), + (*PUBLISHER, UnifiedNum::from_whole(40_u64)), + ] + .into_iter() + .collect(), ) - .expect("Should not overflow") - < HEALTH_THRESHOLD - ); - - assert!( - get_health( + .expect("Should return health and not overflow"); + assert_eq!(health, 500, "Exactly half of the health"); + assert!(health < HEALTH_THRESHOLD, "unhealthy"); + } + + // Unhealthy + { + let health = get_health( all_spenders_sum, - &vec![(*PUBLISHER, 80.into())].into_iter().collect(), - &vec![(*PUBLISHER_2, 20.into()), (*PUBLISHER, 60.into())] + &vec![(*PUBLISHER, UnifiedNum::from_whole(80_u64))] .into_iter() - .collect() + .collect(), + &vec![ + (*PUBLISHER_2, UnifiedNum::from_whole(20_u64)), + (*PUBLISHER, UnifiedNum::from_whole(60_u64)), + ] + .into_iter() + .collect(), ) - .expect("Should not overflow") - < HEALTH_THRESHOLD - ); - - assert!( - get_health( + .expect("Should return health and not overflow"); + assert_eq!(health, 750, "One fourth expected"); + assert!(health < HEALTH_THRESHOLD, "unhealthy"); + } + + // Healthy + { + let health = get_health( all_spenders_sum, - &vec![(*PUBLISHER, 80.into())].into_iter().collect(), - &vec![(*PUBLISHER_2, 2.into()), (*PUBLISHER, 78.into())] + &vec![(*PUBLISHER, UnifiedNum::from_whole(80_u64))] .into_iter() - .collect() + .collect(), + &vec![ + (*PUBLISHER_2, UnifiedNum::from_whole(2_u64)), + (*PUBLISHER, UnifiedNum::from_whole(78_u64)), + ] + .into_iter() + .collect(), ) - .expect("Should not overflow") - >= HEALTH_THRESHOLD - ); + .expect("Should return health and not overflow"); - assert!( - get_health( + assert_eq!(health, 975,); + assert!(health >= HEALTH_THRESHOLD, "healthy"); + } + + // Healthy + { + let health = get_health( all_spenders_sum, - &vec![(*PUBLISHER, 100.into()), (*PUBLISHER_2, 1.into())] + &vec![ + (*PUBLISHER, UnifiedNum::from_whole(100_u64)), + (*PUBLISHER_2, UnifiedNum::from_whole(1_u64)), + ] + .into_iter() + .collect(), + &vec![(*PUBLISHER, UnifiedNum::from_whole(100_u64))] .into_iter() .collect(), - &vec![(*PUBLISHER, 100.into())].into_iter().collect() ) - .expect("Should not overflow") - >= HEALTH_THRESHOLD - ); + .expect("Should return health and not overflow"); + assert_eq!(health, 988); + assert!(health >= HEALTH_THRESHOLD, "healthy"); + } } } From f5c2ddc31546b26ee8e8b4959a3c2f67460f13b6 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Tue, 17 May 2022 14:45:30 +0200 Subject: [PATCH 10/15] primitives - clean up UnifiedNum & fix warnings --- primitives/src/balances.rs | 3 +- primitives/src/test_util.rs | 3 +- primitives/src/unified_num.rs | 70 ++++++++--------------------------- sentry/src/db/analytics.rs | 16 ++++---- sentry/src/routes/campaign.rs | 2 +- 5 files changed, 28 insertions(+), 66 deletions(-) diff --git a/primitives/src/balances.rs b/primitives/src/balances.rs index 47358bb94..19af7ff70 100644 --- a/primitives/src/balances.rs +++ b/primitives/src/balances.rs @@ -73,12 +73,11 @@ impl Balances { self.spenders .values() .sum::>() - .map(|spenders| { + .and_then(|spenders| { let earners = self.earners.values().sum::>()?; Some((earners, spenders)) }) - .flatten() } } diff --git a/primitives/src/test_util.rs b/primitives/src/test_util.rs index 94fe74ee7..1de7f0662 100644 --- a/primitives/src/test_util.rs +++ b/primitives/src/test_util.rs @@ -9,8 +9,9 @@ use crate::{ config::GANACHE_CONFIG, sentry::{CLICK, IMPRESSION}, targeting::Rules, + unified_num::FromWhole, AdUnit, Address, Campaign, Channel, EventSubmission, UnifiedNum, ValidatorDesc, ValidatorId, - IPFS, unified_num::FromWhole, + IPFS, }; pub use logger::discard_logger; diff --git a/primitives/src/unified_num.rs b/primitives/src/unified_num.rs index c037cc86f..6fb7f574d 100644 --- a/primitives/src/unified_num.rs +++ b/primitives/src/unified_num.rs @@ -308,7 +308,7 @@ impl One for UnifiedNum { impl Integer for UnifiedNum { fn div_floor(&self, other: &Self) -> Self { - UnifiedNum::div_floor(self, &other) + UnifiedNum::div_floor(self, other) } // TODO: Check math and write tests @@ -426,6 +426,7 @@ impl Div for UnifiedNum { impl Div<&UnifiedNum> for UnifiedNum { type Output = UnifiedNum; + #[allow(clippy::op_ref)] fn div(self, rhs: &UnifiedNum) -> Self::Output { // use &UnifiedNum / &UnifiedNum &self / rhs @@ -455,48 +456,13 @@ impl Mul<&UnifiedNum> for &UnifiedNum { impl Mul<&UnifiedNum> for UnifiedNum { type Output = UnifiedNum; + #[allow(clippy::op_ref)] fn mul(self, rhs: &UnifiedNum) -> Self::Output { // Use &UnifiedNum * &UnifiedNum &self * rhs } } -impl Mul for &UnifiedNum { - type Output = UnifiedNum; - - fn mul(self, rhs: u64) -> Self::Output { - // Use &UnifiedNum * &UnifiedNum - self * &UnifiedNum::from_whole(rhs) - } -} - -impl Mul for UnifiedNum { - type Output = UnifiedNum; - - fn mul(self, rhs: u64) -> Self::Output { - // Use &UnifiedNum * &UnifiedNum - &self * &UnifiedNum::from_whole(rhs) - } -} - -impl Mul for u64 { - type Output = UnifiedNum; - - fn mul(self, rhs: UnifiedNum) -> Self::Output { - // Use &UnifiedNum * &UnifiedNum - &UnifiedNum::from_whole(self) * &rhs - } -} - -impl Mul<&UnifiedNum> for u64 { - type Output = UnifiedNum; - - fn mul(self, rhs: &UnifiedNum) -> Self::Output { - // Use &UnifiedNum * &UnifiedNum - &UnifiedNum::from_whole(self) * rhs - } -} - impl Rem for UnifiedNum { type Output = UnifiedNum; @@ -573,27 +539,12 @@ fn div_unified_num_to_ratio(lhs: &UnifiedNum, rhs: &UnifiedNum) -> Option 1.0 - // or one of the sides is < 1.0 with the special case of lhs >= UnifiedNum::ONE (1_00_000_000) - // we must use the multiplier - let ratio = if lhs < &UnifiedNum::ONE && rhs < &UnifiedNum::ONE // 0.5 / 0.8 - || lhs > &UnifiedNum::ONE && rhs > &UnifiedNum::ONE // 1 / 2 | 7 / 2 | 15 / 3 - || lhs < &UnifiedNum::ONE && rhs > &UnifiedNum::ONE // 0.1 / 1.2 - || lhs >= &UnifiedNum::ONE && rhs < &UnifiedNum::ONE - // 1 / 0.1 | 1.2 / 0.2 - { - Ratio::new(lhs.0, rhs.0).checked_mul(&Ratio::new(UnifiedNum::MULTIPLIER, 1)) - } else { - Some(Ratio::new(lhs.0, rhs.0)) - }; - - ratio + Ratio::new(lhs.0, rhs.0).checked_mul(&Ratio::new(UnifiedNum::MULTIPLIER, 1)) } #[cfg(test)] @@ -618,6 +569,7 @@ mod test { fn test_unified_num_div_to_u64_ratio() { let one = UnifiedNum::one(); let two = one + one; + let twenty = UnifiedNum::from_whole(20); let three = one + one + one; let zero = UnifiedNum::zero(); let one_tenth = UnifiedNum::from_whole(0.1); @@ -627,6 +579,14 @@ mod test { // 0.00 000 015 = UnifiedNum(15) let fifteen = UnifiedNum::from(15); + // 20 / 2 = 10 + assert_eq!( + 10 * UnifiedNum::MULTIPLIER, + div_unified_num_to_ratio(&twenty, &two) + .unwrap() + .to_integer(), + ); + // 2 / 0.1 = 20 assert_eq!( 20 * UnifiedNum::MULTIPLIER, @@ -855,7 +815,7 @@ mod test { // 0.1 / 3.0 = 0.03333333 assert_eq!(UnifiedNum::from(3_333_333), one_tenth / three); - + // 0.1 / 2.0 = 0.05 assert_eq!(UnifiedNum::from_whole(0.05), one_tenth / two); diff --git a/sentry/src/db/analytics.rs b/sentry/src/db/analytics.rs index 19942a60b..f1d76ebcb 100644 --- a/sentry/src/db/analytics.rs +++ b/sentry/src/db/analytics.rs @@ -432,7 +432,9 @@ mod test { .expect("Should not overflow"); assert_eq!( - 14 * amount_per_day + 15 * amount_per_day + 16 * amount_per_day, + UnifiedNum::from_whole(14) * amount_per_day + + UnifiedNum::from_whole(15) * amount_per_day + + UnifiedNum::from_whole(16) * amount_per_day, three_days_fetched_paid ); } @@ -534,12 +536,12 @@ mod test { .expect("Should not overflow"); assert_eq!( - 10 * amount_per_day - + 11 * amount_per_day - + 12 * amount_per_day - + 13 * amount_per_day - + 14 * amount_per_day - + 15 * amount_per_day, + UnifiedNum::from_whole(10) * amount_per_day + + UnifiedNum::from_whole(11) * amount_per_day + + UnifiedNum::from_whole(12) * amount_per_day + + UnifiedNum::from_whole(13) * amount_per_day + + UnifiedNum::from_whole(14) * amount_per_day + + UnifiedNum::from_whole(15) * amount_per_day, six_days_fetched_paid ); } diff --git a/sentry/src/routes/campaign.rs b/sentry/src/routes/campaign.rs index 97c6e3d56..2ddf0a99b 100644 --- a/sentry/src/routes/campaign.rs +++ b/sentry/src/routes/campaign.rs @@ -583,7 +583,7 @@ pub mod insert_events { sentry::{Event, SuccessResponse}, Address, Campaign, CampaignId, ChainOf, DomainError, UnifiedNum, ValidatorDesc, }; - use slog::{debug, error, Logger}; + use slog::error; use thiserror::Error; #[derive(Debug, Error)] From 4011455c0f33dd6019ff0bbb18b2744d85ca1679 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Tue, 17 May 2022 16:25:45 +0200 Subject: [PATCH 11/15] test_harness - update tests and calculations --- adapter/src/ethereum/test_util.rs | 13 +- test_harness/src/lib.rs | 218 +++++++++++++++++------------- 2 files changed, 138 insertions(+), 93 deletions(-) diff --git a/adapter/src/ethereum/test_util.rs b/adapter/src/ethereum/test_util.rs index 067435e61..e1c2d9151 100644 --- a/adapter/src/ethereum/test_util.rs +++ b/adapter/src/ethereum/test_util.rs @@ -11,7 +11,9 @@ use web3::{ use primitives::{ channel::{Channel, Nonce}, config::{ChainInfo, TokenInfo, GANACHE_CONFIG}, - test_util::{ADVERTISER, CREATOR, FOLLOWER, GUARDIAN, GUARDIAN_2, LEADER, PUBLISHER}, + test_util::{ + ADVERTISER, ADVERTISER_2, CREATOR, FOLLOWER, GUARDIAN, GUARDIAN_2, LEADER, PUBLISHER, + }, Address, BigNum, Chain, ValidatorId, }; @@ -83,6 +85,15 @@ pub static KEYSTORES: Lazy> = Lazy::new(|| { *GUARDIAN_2, keystore_options(&format!("{}_keystore.json", *GUARDIAN_2), "ganache6"), ), + // Address 7 + // ( + // *PUBLISHER_2, + // keystore_options(&format!("{}_keystore.json", *PUBLISHER_2), "ganache7"), + // ), + ( + *ADVERTISER_2, + keystore_options(&format!("{}_keystore.json", *ADVERTISER_2), "ganache8"), + ), ] .into_iter() .collect() diff --git a/test_harness/src/lib.rs b/test_harness/src/lib.rs index cc1659b55..e9155648e 100644 --- a/test_harness/src/lib.rs +++ b/test_harness/src/lib.rs @@ -267,7 +267,10 @@ mod tests { IMPRESSION, }, spender::Spender, - test_util::{ADVERTISER, DUMMY_AD_UNITS, DUMMY_IPFS, GUARDIAN, GUARDIAN_2, IDS, PUBLISHER}, + test_util::{ + ADVERTISER, ADVERTISER_2, DUMMY_AD_UNITS, DUMMY_IPFS, GUARDIAN, GUARDIAN_2, IDS, + PUBLISHER, + }, util::{logging::new_logger, ApiUrl}, validator::{Heartbeat, NewState}, Balances, BigNum, Campaign, CampaignId, Channel, ChannelId, UnifiedNum, @@ -323,9 +326,9 @@ mod tests { id: VALIDATORS[&LEADER].address.into(), url: VALIDATORS[&LEADER].sentry_url.to_string(), // min_validator_fee for token: 0.000_010 - // fee per 1000 (pro mille) = 0.00003000 (UnifiedNum) - // fee per 1 payout: payout * fee / 1000 = payout * 0.00000003 - fee: 3_000.into(), + // fee per 1000 (pro mille) = 5.00000000 (UnifiedNum) + // fee per 1 payout: payout * fee / 1000 = payout * 0.00500000 + fee: 500_000_000.into(), fee_addr: None, }; @@ -333,9 +336,9 @@ mod tests { id: VALIDATORS[&FOLLOWER].address.into(), url: VALIDATORS[&FOLLOWER].sentry_url.to_string(), // min_validator_fee for token: 0.000_010 - // fee per 1000 (pro mille) = 0.00002000 (UnifiedNum) - // fee per 1 payout: payout * fee / 1000 = payout * 0.00000002 - fee: 2_000.into(), + // fee per 1000 (pro mille) = 4.00000000 (UnifiedNum) + // fee per 1 payout: payout * fee / 1000 = payout * 0.00400000 + fee: 400_000_000.into(), fee_addr: None, }; @@ -347,31 +350,31 @@ mod tests { .expect("Should parse"), channel, creator: *ADVERTISER, - // 2.00000000 - budget: UnifiedNum::from(200_000_000), + // 150.00000000 + budget: UnifiedNum::from(15_000_000_000), validators, title: Some("Dummy Campaign".to_string()), pricing_bounds: vec![ ( IMPRESSION, Pricing { - // 0.00003000 - // Per 1000 = 0.03000000 - min: 3_000.into(), - // 0.00005000 - // Per 1000 = 0.05000000 - max: 5_000.into(), + // 0.04000000 + // Per 1000 = 40.00000000 + min: 4_000_000.into(), + // 0.05000000 + // Per 1000 = 50.00000000 + max: 5_000_000.into(), }, ), ( CLICK, Pricing { - // 0.00006000 - // Per 1000 = 0.06000000 - min: 6_000.into(), - // 0.00010000 - // Per 1000 = 0.10000000 - max: 10_000.into(), + // 0.06000000 + // Per 1000 = 60.00000000 + min: 6_000_000.into(), + // 0.10000000 + // Per 1000 = 100.00000000 + max: 10_000_000.into(), }, ), ] @@ -441,31 +444,31 @@ mod tests { // 20.00000000 budget: UnifiedNum::from(2_000_000_000), validators, - title: Some("Dummy Campaign".to_string()), + title: Some("Dummy Campaign 2 in Chain #1337".to_string()), pricing_bounds: vec![ ( IMPRESSION, Pricing { - // 0.00001000 - min: 1_000.into(), - // 0.00002000 - max: 2_000.into(), + // 1 TOKEN + min: 100_000_000.into(), + // 2 TOKENs + max: 200_000_000.into(), }, ), ( CLICK, Pricing { - // 0.00003000 - min: 3_000.into(), - // 0.00005000 - max: 5_000.into(), + // 3 TOKENs + min: 300_000_000.into(), + // 5 TOKENs + max: 500_000_000.into(), }, ), ] .into_iter() .collect(), event_submission: Some(EventSubmission { allow: vec![] }), - ad_units: vec![], + ad_units: vec![DUMMY_AD_UNITS[0].clone(), DUMMY_AD_UNITS[1].clone()], targeting_rules: Rules::new(), created: Utc.ymd(2021, 2, 1).and_hms(7, 0, 0), active: Active { @@ -497,9 +500,9 @@ mod tests { id: VALIDATORS[&LEADER].address.into(), url: VALIDATORS[&LEADER].sentry_url.to_string(), // min_validator_fee for token: 0.000_010 - // fee per 1000 (pro mille) = 0.00003000 (UnifiedNum) - // fee per 1 payout: payout * fee / 1000 = payout * 0.00000003 - fee: 3_000.into(), + // fee per 1000 (pro mille) = 2.00000000 + // fee per 1 payout: payout * fee / 1000 = payout * 0.00200000 + fee: 200_000_000.into(), fee_addr: None, }; @@ -507,9 +510,9 @@ mod tests { id: VALIDATORS[&FOLLOWER].address.into(), url: VALIDATORS[&FOLLOWER].sentry_url.to_string(), // min_validator_fee for token: 0.000_010 - // fee per 1000 (pro mille) = 0.00002000 (UnifiedNum) - // fee per 1 payout: payout * fee / 1000 = payout * 0.00000002 - fee: 2_000.into(), + // fee per 1000 (pro mille) = 1.75000000 + // fee per 1 payout: payout * fee / 1000 = payout * 0.00175000 + fee: 175_000_000.into(), fee_addr: None, }; @@ -520,39 +523,39 @@ mod tests { .parse() .expect("Should parse"), channel, - creator: *ADVERTISER, + creator: *ADVERTISER_2, // 20.00000000 budget: UnifiedNum::from(2_000_000_000), validators, - title: Some("Dummy Campaign in Chain #1".to_string()), + title: Some("Dummy Campaign 3 in Chain #1".to_string()), pricing_bounds: vec![ ( IMPRESSION, Pricing { - // 0.00003000 - // Per 1000 = 0.03000000 - min: 3_000.into(), - // 0.00005000 - // Per 1000 = 0.05000000 - max: 5_000.into(), + // 0.01500000 + // Per 1000 = 15.00000000 + min: 1_500_000.into(), + // 0.0250000 + // Per 1000 = 25.00000000 + max: 2_500_000.into(), }, ), ( CLICK, Pricing { - // 0.00006000 - // Per 1000 = 0.06000000 - min: 6_000.into(), - // 0.00010000 - // Per 1000 = 0.10000000 - max: 10_000.into(), + // 0.03500000 + // Per 1000 = 35.00000000 + min: 3_500_000.into(), + // 0.06500000 + // Per 1000 = 65.00000000 + max: 6_500_000.into(), }, ), ] .into_iter() .collect(), event_submission: Some(EventSubmission { allow: vec![] }), - ad_units: vec![DUMMY_AD_UNITS[0].clone(), DUMMY_AD_UNITS[1].clone()], + ad_units: vec![DUMMY_AD_UNITS[2].clone(), DUMMY_AD_UNITS[3].clone()], targeting_rules: Rules::new(), created: Utc.ymd(2021, 2, 1).and_hms(7, 0, 0), active: Active { @@ -563,7 +566,6 @@ mod tests { }); #[tokio::test(flavor = "multi_thread", worker_threads = 4)] - // #[ignore = "for now"] async fn run_full_test() { let chain = GANACHE_1337.clone(); assert_eq!(CAMPAIGN_1.channel.token, CAMPAIGN_2.channel.token); @@ -572,7 +574,7 @@ mod tests { .find_chain_of(CAMPAIGN_1.channel.token) .expect("Should find CAMPAIGN_1 channel token address in Config!"); - let second_token_chain = GANACHE_CONFIG + let token_chain_1 = GANACHE_CONFIG .find_chain_of(CAMPAIGN_3.channel.token) .expect("Should find CAMPAIGN_3 channel token address in Config!"); @@ -598,11 +600,19 @@ mod tests { // We use the Advertiser's `EthereumAdapter::get_auth` for authentication! let advertiser_adapter = Adapter::new( Ethereum::init(KEYSTORES[&ADVERTISER].clone(), &GANACHE_CONFIG) - .expect("Should initialize creator adapter"), + .expect("Should initialize ADVERTISER adapter"), ) .unlock() .expect("Should unlock advertiser's Ethereum Adapter"); + // We use the Advertiser's `EthereumAdapter::get_auth` for authentication! + let advertiser2_adapter = Adapter::new( + Ethereum::init(KEYSTORES[&ADVERTISER_2].clone(), &GANACHE_CONFIG) + .expect("Should initialize ADVERTISER_2 adapter"), + ) + .unlock() + .expect("Should unlock Advertiser 2 Ethereum Adapter"); + // setup Sentry & returns Adapter let leader_adapter = setup_sentry(&leader) .await @@ -649,16 +659,18 @@ mod tests { // Advertiser deposits // + // Advertiser // Channel 1 in Chain #1337: - // - Outpace: 20 TOKENs + // - Outpace: 150 TOKENs // - Counterfactual: 10 TOKENs // // Channel 2 in Chain #1337: // - Outpace: 30 TOKENs // - Counterfactual: 20 TOKENs // + // Advertiser 2 // Channel 3 in Chain #1: - // - Outpace: 30 TOKENS + // - Outpace: 100 TOKENS // - Counterfactual: 20 TOKENs { let advertiser_deposits = [ @@ -666,7 +678,7 @@ mod tests { channel: CAMPAIGN_1.channel, token: contracts_1337.token.info.clone(), address: advertiser_adapter.whoami().to_address(), - outpace_amount: BigNum::with_precision(20, token_1337_precision), + outpace_amount: BigNum::with_precision(150, token_1337_precision), counterfactual_amount: BigNum::with_precision(10, token_1337_precision), }, Deposit { @@ -679,7 +691,7 @@ mod tests { Deposit { channel: CAMPAIGN_3.channel, token: contracts_1.token.info.clone(), - address: advertiser_adapter.whoami().to_address(), + address: advertiser2_adapter.whoami().to_address(), outpace_amount: BigNum::with_precision(100, token_1_precision), counterfactual_amount: BigNum::with_precision(20, token_1_precision), }, @@ -731,8 +743,8 @@ mod tests { // make sure we have the expected deposit returned from EthereumAdapter let eth_deposit = leader_adapter .get_deposit( - &second_token_chain.clone().with_channel(CAMPAIGN_3.channel), - advertiser_adapter.whoami().to_address(), + &token_chain_1.clone().with_channel(CAMPAIGN_3.channel), + advertiser2_adapter.whoami().to_address(), ) .await .expect("Should get deposit for advertiser"); @@ -772,9 +784,9 @@ mod tests { .expect("Get authentication"); let mut no_budget_campaign = CreateCampaign::from_campaign(CAMPAIGN_1.clone()); - // Deposit of Advertiser for Channel 2: 20 (outpace) + 10 (create2) - // Campaign Budget: 40 TOKENs - no_budget_campaign.budget = UnifiedNum::from(4_000_000_000); + // Deposit of Advertiser for Channel 1: 150 (outpace) + 10 (create2) + // Campaign Budget: 400 TOKENs + no_budget_campaign.budget = UnifiedNum::from(40_000_000_000); let no_budget_response = create_campaign( &api_client, @@ -810,8 +822,8 @@ mod tests { let expected = vec![( advertiser_adapter.whoami().to_address(), Spender { - // Expected: 30 TOKENs - total_deposited: UnifiedNum::from(3_000_000_000), + // Expected: 160 TOKENs + total_deposited: UnifiedNum::from(16_000_000_000), total_spent: None, }, )] @@ -920,7 +932,7 @@ mod tests { } } - // Create Campaign 3 w/ Channel 3 using Advertiser on a different chain (Chain #1) + // Create Campaign 3 w/ Channel 3 using Advertiser 2 on a different chain (Chain #1) // In Leader & Follower sentries // Response: 200 Ok // POST /v5/campaign @@ -929,12 +941,12 @@ mod tests { let create_campaign_3 = CreateCampaign::from_campaign(CAMPAIGN_3.clone()); assert_eq!( - &second_token_chain.chain, &second_chain, + &token_chain_1.chain, &second_chain, "CAMPAIGN_3 should be using the #1 Chain which is setup in the Ganache Config" ); { - let leader_token = advertiser_adapter + let leader_token = advertiser2_adapter .get_auth(second_chain.chain_id, leader_adapter.whoami()) .expect("Get authentication"); @@ -956,7 +968,7 @@ mod tests { } { - let follower_token = advertiser_adapter + let follower_token = advertiser2_adapter .get_auth(second_chain.chain_id, follower_adapter.whoami()) .expect("Get authentication"); @@ -1058,7 +1070,7 @@ mod tests { }, ]; - let response = post_new_events( + let leader_response = post_new_events( &leader_sentry, token_chain_1337.clone().with(CAMPAIGN_1.id), &events, @@ -1066,34 +1078,52 @@ mod tests { .await .expect("Posted events"); - assert_eq!(SuccessResponse { success: true }, response); + assert_eq!(SuccessResponse { success: true }, leader_response); + + let follower_response = post_new_events( + &follower_sentry, + token_chain_1337.clone().with(CAMPAIGN_1.id), + &events, + ) + .await + .expect("Posted events"); + + assert_eq!(SuccessResponse { success: true }, follower_response); info!( setup.logger, - "Successfully POST events for CAMPAIGN_1 {:?} and Channel {:?} ", + "Successful POST of events for CAMPAIGN_1 {:?} and Channel {:?} to Leader & Follower", CAMPAIGN_1.id, CAMPAIGN_1.channel.id() ); } - // Channel 1 expected Accounting + // CAMPAIGN_1 & Channel 1 expected Accounting // Fees are calculated based on pro mile of the payout // event payout * fee / 1000 // + // leader fee (per 1000): 5 + // follower fee (per 1000): 4 + // IMPRESSION price (min): 0.04 + // CLICK price (min): 0.06 // - // IMPRESSION: - // - Publisher payout: 3000 - // - Leader fees: 3000 * 3000 / 1 000 = 9 000 - // - Follower fees: 3000 * 2000 / 1000 = 6 000 + // 1 x IMPRESSION: + // - Publisher payout: 0.04 = UnifiedNum(4 000 000) + // - Leader fees: 0.04 * 5 / 1 000 = 0.0002 = UnifiedNum(20 000) + // - Follower fees: 0.04 * 4 / 1 000 = 0.00016 = UnifiedNum(16 000) // - // CLICK: - // - Publisher payout: 6000 - // - Leader fees: 6000 * 3000 / 1000 = 18 000 - // - Follower fees: 6000 * 2000 / 1000 = 12 000 + // 1 x CLICK: + // - Publisher payout: 0.06 = UnifiedNum(6 000 000) + // - Leader fees: 0.06 * 5 / 1 000 = 0.0003 = UnifiedNum(30 000) + // - Follower fees: 0.06 * 4 / 1 000 = 0.00024 = UnifiedNum(24 000) // // Creator (Advertiser) pays out: + // + // Publisher total payout: 0.04 (impression) + 0.06 (click) = 0.1 = UnifiedNum(10 000 000) + // Leader total fees: 0.0002 + 0.0003 = 0.0005 = UnifiedNum (50 000) + // Follower total fees: 0.00016 + 0.00024 = 0.0004 = UnifiedNum(40 000) + // // events_payout + leader fee + follower fee - // events_payout = 3000 (impression) + 6000 (click) = 9 000 - // 9000 + (9000 + 18000) + (6000 + 12000) = 54 000 + // 0.1 + (0.0002 + 0.0003) + (0.00016 + 0.00024) = 0.1009 = UnifiedNum(10 090 000) { let mut expected_balances = Balances::new(); @@ -1101,18 +1131,18 @@ mod tests { .spend( CAMPAIGN_1.creator, CAMPAIGN_1.channel.leader.to_address(), - UnifiedNum::from(27_000), + UnifiedNum::from(50_000), ) .expect("Should spend for Leader"); expected_balances .spend( CAMPAIGN_1.creator, CAMPAIGN_1.channel.follower.to_address(), - UnifiedNum::from(18_000), + UnifiedNum::from(40_000), ) .expect("Should spend for Follower"); expected_balances - .spend(CAMPAIGN_1.creator, *PUBLISHER, UnifiedNum::from(9_000)) + .spend(CAMPAIGN_1.creator, *PUBLISHER, UnifiedNum::from(10_000_000)) .expect("Should spend for Publisher"); let expected_accounting = AccountingResponse { @@ -1120,7 +1150,7 @@ mod tests { }; let actual_accounting = leader_sentry - .get_accounting(&token_chain_1337.with_channel(CAMPAIGN_1.channel)) + .get_accounting(&token_chain_1337.clone().with_channel(CAMPAIGN_1.channel)) .await .expect("Should get Channel Accounting"); @@ -1159,18 +1189,22 @@ mod tests { .spend( CAMPAIGN_1.creator, CAMPAIGN_1.channel.leader.to_address(), - UnifiedNum::from_u64(27000), + UnifiedNum::from_u64(50_000), ) .expect("Should spend"); expected_balances .spend( CAMPAIGN_1.creator, CAMPAIGN_1.channel.follower.to_address(), - UnifiedNum::from_u64(18000), + UnifiedNum::from_u64(40_000), ) .expect("Should spend"); expected_balances - .spend(CAMPAIGN_1.creator, *PUBLISHER, UnifiedNum::from_u64(9000)) + .spend( + CAMPAIGN_1.creator, + *PUBLISHER, + UnifiedNum::from_u64(10_000_000), + ) .expect("Should spend"); pretty_assertions::assert_eq!( @@ -1183,7 +1217,7 @@ mod tests { let last_approved_response_follower = follower_sentry .get_last_approved(CAMPAIGN_1.channel.id()) .await - .expect("Should fetch APprove state from Follower"); + .expect("Should fetch Approve state from Follower"); let last_approved_response_leader = leader_sentry .get_last_approved(CAMPAIGN_1.channel.id()) From 2feeb3a9ad37f61e082f0fe7cd8b6e1506c206e3 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Fri, 20 May 2022 18:34:22 +0200 Subject: [PATCH 12/15] test_harness - test more states on CAMPAIGN_2: - RejectState - Unhealthy ApproveState - transitioning to Healthy ApproveState --- docs/config/ganache.toml | 4 +- test_harness/src/lib.rs | 836 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 799 insertions(+), 41 deletions(-) diff --git a/docs/config/ganache.toml b/docs/config/ganache.toml index fa3fafeac..45ce25b6d 100644 --- a/docs/config/ganache.toml +++ b/docs/config/ganache.toml @@ -20,7 +20,9 @@ propagation_timeout = 2000 fetch_timeout = 5000 all_campaigns_timeout = 5000 -channel_tick_timeout = 5000 +# for test_harness make it larger +# Default: 5000 +channel_tick_timeout = 8000 ip_rate_limit = { type = 'ip', timeframe = 1200000 } sid_rate_limit = { type = 'sid', timeframe = 0 } diff --git a/test_harness/src/lib.rs b/test_harness/src/lib.rs index 58d07c2ab..c91d3b28f 100644 --- a/test_harness/src/lib.rs +++ b/test_harness/src/lib.rs @@ -271,8 +271,9 @@ mod tests { ADVERTISER, ADVERTISER_2, DUMMY_AD_UNITS, DUMMY_IPFS, GUARDIAN, GUARDIAN_2, IDS, PUBLISHER, PUBLISHER_2, }, + unified_num::FromWhole, util::{logging::new_logger, ApiUrl}, - validator::{Heartbeat, NewState}, + validator::{ApproveState, Heartbeat, NewState, RejectState}, Balances, BigNum, Campaign, CampaignId, Channel, ChannelId, UnifiedNum, }; use reqwest::{Client, StatusCode}; @@ -419,7 +420,8 @@ mod tests { id: VALIDATORS[&FOLLOWER].address.into(), url: VALIDATORS[&FOLLOWER].sentry_url.to_string(), // fee per 1000 (pro mille) = 0.10000000 (UnifiedNum) - fee: 10_000_000.into(), + // fee per 1 = 0.00010000 + fee: UnifiedNum::from_whole(0.1), fee_addr: None, }; @@ -429,12 +431,19 @@ mod tests { id: VALIDATORS[&LEADER].address.into(), url: VALIDATORS[&LEADER].sentry_url.to_string(), // fee per 1000 (pro mille) = 0.05000000 (UnifiedNum) - fee: 5_000_000.into(), + // fee per 1 = 0.00005000 + fee: UnifiedNum::from_whole(0.05), fee_addr: None, }; let validators = Validators::new((leader_desc, follower_desc)); + // CAMPAIGN_2 budget 20 TOKENs (2_000_000_000) + // leader fee (pro mile) 10_000_000 = 0.10000000 TOKENs + // follower fee (pro mile) 5_000_000 = 0.05000000 TOKENs + // IMPRESSION pricing (min) - 1 TOKEN + // CLICK pricing (min) - 3 TOKENs + // Campaign { id: "0x127b98248f4e4b73af409d10f62daeaa" .parse() @@ -442,7 +451,7 @@ mod tests { channel, creator: *ADVERTISER, // 20.00000000 - budget: UnifiedNum::from(2_000_000_000), + budget: UnifiedNum::from_whole(20), validators, title: Some("Dummy Campaign 2 in Chain #1337".to_string()), pricing_bounds: vec![ @@ -450,18 +459,18 @@ mod tests { IMPRESSION, Pricing { // 1 TOKEN - min: 100_000_000.into(), + min: UnifiedNum::from_whole(1), // 2 TOKENs - max: 200_000_000.into(), + max: UnifiedNum::from_whole(2), }, ), ( CLICK, Pricing { // 3 TOKENs - min: 300_000_000.into(), + min: UnifiedNum::from_whole(3), // 5 TOKENs - max: 500_000_000.into(), + max: UnifiedNum::from_whole(5), }, ), ] @@ -502,7 +511,7 @@ mod tests { // min_validator_fee for token: 0.000_010 // fee per 1000 (pro mille) = 2.00000000 // fee per 1 payout: payout * fee / 1000 = payout * 0.00200000 - fee: 200_000_000.into(), + fee: UnifiedNum::from_whole(2), fee_addr: None, }; @@ -512,7 +521,7 @@ mod tests { // min_validator_fee for token: 0.000_010 // fee per 1000 (pro mille) = 1.75000000 // fee per 1 payout: payout * fee / 1000 = payout * 0.00175000 - fee: 175_000_000.into(), + fee: UnifiedNum::from_whole(1.75), fee_addr: None, }; @@ -525,7 +534,7 @@ mod tests { channel, creator: *ADVERTISER_2, // 20.00000000 - budget: UnifiedNum::from(2_000_000_000), + budget: UnifiedNum::from_whole(20), validators, title: Some("Dummy Campaign 3 in Chain #1".to_string()), pricing_bounds: vec![ @@ -534,10 +543,10 @@ mod tests { Pricing { // 0.01500000 // Per 1000 = 15.00000000 - min: 1_500_000.into(), + min: UnifiedNum::from_whole(0.015), // 0.0250000 // Per 1000 = 25.00000000 - max: 2_500_000.into(), + max: UnifiedNum::from_whole(0.025), }, ), ( @@ -545,10 +554,10 @@ mod tests { Pricing { // 0.03500000 // Per 1000 = 35.00000000 - min: 3_500_000.into(), + min: UnifiedNum::from_whole(0.035), // 0.06500000 // Per 1000 = 65.00000000 - max: 6_500_000.into(), + max: UnifiedNum::from_whole(0.065), }, ), ] @@ -565,6 +574,95 @@ mod tests { } }); + /// These `CAMPAIGN_2` events are used to test the `ApproveState` with `is_healthy: false` + /// and `RejectState` + /// 5 x IMPRESSIONs + /// 4 x CLICKs + static CAMPAIGN_2_EVENTS: Lazy<[Event; 8]> = Lazy::new(|| { + [ + Event::Impression { + publisher: *PUBLISHER_2, + ad_unit: CAMPAIGN_2 + .ad_units + .get(0) + .expect("Should exist in Campaign") + .ipfs, + ad_slot: DUMMY_IPFS[3], + referrer: Some("https://adex.network".into()), + }, + Event::Impression { + publisher: *PUBLISHER_2, + ad_unit: CAMPAIGN_2 + .ad_units + .get(0) + .expect("Should exist in Campaign") + .ipfs, + ad_slot: DUMMY_IPFS[3], + referrer: Some("https://adex.network".into()), + }, + Event::Impression { + publisher: *PUBLISHER_2, + ad_unit: CAMPAIGN_2 + .ad_units + .get(1) + .expect("Should exist in Campaign") + .ipfs, + ad_slot: DUMMY_IPFS[3], + referrer: Some("https://adex.network".into()), + }, + Event::Impression { + publisher: *PUBLISHER_2, + ad_unit: CAMPAIGN_2 + .ad_units + .get(1) + .expect("Should exist in Campaign") + .ipfs, + ad_slot: DUMMY_IPFS[3], + referrer: Some("https://adex.network".into()), + }, + Event::Impression { + publisher: *PUBLISHER_2, + ad_unit: CAMPAIGN_2 + .ad_units + .get(1) + .expect("Should exist in Campaign") + .ipfs, + ad_slot: DUMMY_IPFS[3], + referrer: Some("https://adex.network".into()), + }, + Event::Click { + publisher: *PUBLISHER_2, + ad_unit: CAMPAIGN_2 + .ad_units + .get(0) + .expect("Should exist in Campaign") + .ipfs, + ad_slot: DUMMY_IPFS[3], + referrer: Some("https://ambire.com".into()), + }, + Event::Click { + publisher: *PUBLISHER_2, + ad_unit: CAMPAIGN_2 + .ad_units + .get(1) + .expect("Should exist in Campaign") + .ipfs, + ad_slot: DUMMY_IPFS[3], + referrer: Some("https://ambire.com".into()), + }, + Event::Click { + publisher: *PUBLISHER_2, + ad_unit: CAMPAIGN_2 + .ad_units + .get(1) + .expect("Should exist in Campaign") + .ipfs, + ad_slot: DUMMY_IPFS[3], + referrer: Some("https://ambire.com".into()), + }, + ] + }); + #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn run_full_test() { let chain = GANACHE_1337.clone(); @@ -666,7 +764,7 @@ mod tests { // // Channel 2 in Chain #1337: // - Outpace: 30 TOKENs - // - Counterfactual: 20 TOKENs + // - Counterfactual: 0 TOKENs // // Advertiser 2 // Channel 3 in Chain #1: @@ -686,7 +784,7 @@ mod tests { token: contracts_1337.token.info.clone(), address: advertiser_adapter.whoami().to_address(), outpace_amount: BigNum::with_precision(30, token_1337_precision), - counterfactual_amount: BigNum::with_precision(20, token_1337_precision), + counterfactual_amount: BigNum::from(0), }, Deposit { channel: CAMPAIGN_3.channel, @@ -996,6 +1094,130 @@ mod tests { let leader_worker = Worker::from_sentry(leader_sentry.clone()); let follower_worker = Worker::from_sentry(follower_sentry.clone()); + // Add new events for `CAMPAIGN_2` to sentry + // Should trigger RejectedState on LEADER (the Channel's follower) + // + // Note: Follower and Leader for this channel are reversed! + // + // Channel 2 has only 1 spender with 50 deposit! + // All spenders sum: 50.00000000 + // + // + // Channel Leader (FOLLOWER) has: + // 5 IMPRESSIONS + // + // Channel Follower (LEADER) has: + // 5 IMPRESSIONS + // 3 CLICKS + // + // RejectState should be triggered by the Channel follower (LEADER) because: + // + // 5 x IMPRESSION = 5 TOKENs + // 3 x CLICK = 9 TOKENs + // ---------- + // 14 TOKENs + // + // IMPRESSIONs: + // + // 5 x leader fee = 5 * (1 TOKENs * 0.1 (fee) / 1000 (pro mile) ) = 5 * 0.0001 TOKENs = 0.0005 TOKENs + // 5 x follower fee = 5 * ( 1 TOKENs * 0.05 (fee) / 1000 (pro_mile) ) = 5 * 0.00005 TOKENs = 0.00025 TOKENs + // + // CLICKs (for 3): + // + // 3 x leader fee = 3 * ( 3 TOKENs * 0.1 (fee) / 1000 (pro mile) ) = 3 x 0.0003 TOKENs = 0.0009 TOKENs + // 3 x follower fee = 3 x ( 3 TOKENs * 0.05 (fee) / 1000 (pro_mile) ) = 3 x 0.00015 TOKENs = 0.00045 TOKENs + // + // All payouts (for 5 IMPRESSIONs + 3 CLICKs): + // Advertiser (spender) = 14.0021 TOKENs + // Publisher (earner) = 14 TOKENs + // Leader (earner) = 0.0005 (IMPRESSIONs) + 0.0009 (CLICKs) = 0.0014 TOKENs + // Follower (earner) = 0.00025 (IMPRESSIONs) + 0.00045 (CLICKs) = 0.00070 TOKENs + // + // Total: 14.0021 TOKENs + // + // For Channel Follower (LEADER) which has all 5 IMPRESSIONs and 3/4 CLICKs: + // + // sum_our = 14.0021 + // sum_approved_mins (5 x IMPRESSION) = 5 + 0.0005 + 0.00025 = 5.00075 + // sum_approved_mins (3 x CLICKS) = 9 + 0.0009 + 0.00045 = 9.00135 + // + // For Channel Leader (FOLLOWER) which has only 5 x IMPRESSION events + // + // 5 x IMPRESSIONs (only) + // diff = 14.0021 - 5.00075 = 9.00135 + // health_penalty = 9.00135 * 1 000 / 30.0 = 300.045 + // + // health = 1 000 - health_penalty = 699.955 (Unsignable) + // + // Ganache health_threshold_promilles = 950 + // Ganache health_unsignable_promilles = 750 + { + // Take all 5 IMPRESSIONs and None of the CLICKs to trigger `RejectState` on the Follower of the Channel (LEADER) + let channel_leader_events = &CAMPAIGN_2_EVENTS[..=4]; + // follower should receive all IMPRESSIONs & 3/3 CLICKs so it will trigger `RejectState` + let channel_follower_events = CAMPAIGN_2_EVENTS.as_ref(); + + let channel_leader_response = post_new_events( + &follower_sentry, + token_chain_1337.clone().with(CAMPAIGN_2.id), + // the Leader of this channel is FOLLOWER! + &channel_leader_events, + ) + .await + .expect("Posted events"); + + assert_eq!(SuccessResponse { success: true }, channel_leader_response); + + let channel_follower_response = post_new_events( + &leader_sentry, + token_chain_1337.clone().with(CAMPAIGN_2.id), + // the Follower of this channel is LEADER! + channel_follower_events, + ) + .await + .expect("Posted events"); + + assert_eq!(SuccessResponse { success: true }, channel_follower_response); + + info!( + setup.logger, + "Successful POST of events for CAMPAIGN_2 {:?} and Channel {:?} to Leader & Follower to trigger RejectState", + CAMPAIGN_2.id, + CAMPAIGN_2.channel.id() + ); + + // Channel 2 expected Accounting on Leader (FOLLOWER) + { + let expected_accounting = AccountingResponse { + balances: { + let mut balances = Balances::::new(); + // publisher (PUBLISHER_2) payout = 5.0 + balances + .spend(*ADVERTISER, *PUBLISHER_2, UnifiedNum::from_whole(5)) + .expect("Should not overflow"); + // leader (FOLLOWER) payout = 0.0005 + balances + .spend(*ADVERTISER, *FOLLOWER, UnifiedNum::from_whole(0.0005)) + .expect("Should not overflow"); + // follower (LEADER) payout = 0.00025 + balances + .spend(*ADVERTISER, *LEADER, UnifiedNum::from_whole(0.00025)) + .expect("Should not overflow"); + + balances + }, + }; + // Channel Leader (FOLLOWER) + let actual_accounting = follower_sentry + .get_accounting(&token_chain_1337.clone().with_channel(CAMPAIGN_2.channel)) + .await + .expect("Should get Channel Accounting"); + + assert_eq!(expected_accounting, actual_accounting); + info!(setup.logger, "Channel 1 {:?} has empty Accounting because no events have been submitted to any Campaign", CAMPAIGN_1.channel.id()); + } + } + // leader single worker tick leader_worker.all_channels_tick().await; // follower single worker tick @@ -1012,7 +1234,77 @@ mod tests { .expect("Should get Channel Accounting"); assert_eq!(expected_accounting, actual_accounting); - info!(setup.logger, "Channel 1 {:?} has empty Accounting because of no events have been submitted to any Campaign", CAMPAIGN_1.channel.id()); + info!(setup.logger, "Channel 1 {:?} has empty Accounting because no events have been submitted to any Campaign", CAMPAIGN_1.channel.id()); + } + + // For CAMPAIGN_2 & Channel 2 + // + // Check NewState on Leader (FOLLOWER) + // RejectState should not be generated by Follower (LEADER) because tick has ran before the Leader's (FOLLOWER) + // + // Leader (FOLLOWER) events: + // - 5 IMPRESSIONs + // + // event payout * fee / 1000 (pro mile) + // + // 5 x leader fee = 5 * ( 1 * 0.1 / 1000 ) = 5 * 0.0001 = 0.0005 + // 5 x follower fee = 5 * ( 1 * 0.05 / 1000 ) = 5 * 0.00005 = 0.00025 + // + // Payouts for 5 IMPRESSIONs: + // + // Advertiser (spender) = 5.00075 + // Publisher (earner) = 5 + // Leader (earner) = 0.0005 + // Follower (earner) = 0.00025 + // + // Total: 5.00075 TOKENs + { + let latest_new_state_leader = follower_sentry + .get_our_latest_msg(CAMPAIGN_2.channel.id(), &["NewState"]) + .await + .expect("Should fetch NewState from Channel's Leader (Who am I) in FOLLOWER sentry") + .map(|message| { + NewState::::try_from(message) + .expect("Should be NewState with Checked Balances") + }) + .expect("Should have a NewState in Channel's Leader for the Campaign 2 channel"); + + let latest_reject_state_follower = leader_sentry + .get_our_latest_msg(CAMPAIGN_2.channel.id(), &["RejectState"]) + .await + .expect( + "Should successfully try to fetch a RejectState from Channel's Follower (Who am I) in LEADER sentry", + ); + + let expected_new_state_balances = { + let mut balances = Balances::::new(); + let multiplier = 10_u64.pow(UnifiedNum::PRECISION.into()); + // Channel's Leader (FOLLOWER) & Follower (LEADER) are reversed! + balances + .spend(*ADVERTISER, *PUBLISHER_2, UnifiedNum::from(5 * multiplier)) + .expect("Should not overflow"); + // total Leader fee: 0.0005 TOKENs + balances + .spend(*ADVERTISER, *FOLLOWER, 50_000_u64.into()) + .expect("Should not overflow"); + // total Follower fee: 0.00025 TOKENs + balances + .spend(*ADVERTISER, *LEADER, 25_000_u64.into()) + .expect("Should not overflow"); + + balances + }; + + pretty_assertions::assert_eq!( + expected_new_state_balances, + latest_new_state_leader.balances, + "Expected Channel's Leader (FOLLOWER) balances should match" + ); + + assert_eq!( + None, latest_reject_state_follower, + "Channel's follower should not have RejectState yet" + ); } // All Channels should have a heartbeat message now @@ -1098,6 +1390,8 @@ mod tests { } // Add new events for `CAMPAIGN_3` to sentry + // 2 IMPRESSIONS + // 2 CLICKS { let events = vec![ Event::Impression { @@ -1163,7 +1457,7 @@ mod tests { assert_eq!(SuccessResponse { success: true }, follower_response); info!( setup.logger, - "Successfully POST events for CAMPAIGN_3 {:?} and Channel {:?} to Leader & Follower", + "Successful POST of events for CAMPAIGN_3 {:?} and Channel {:?} to Leader & Follower", CAMPAIGN_3.id, CAMPAIGN_3.channel.id() ); @@ -1204,18 +1498,18 @@ mod tests { .spend( CAMPAIGN_1.creator, CAMPAIGN_1.channel.leader.to_address(), - UnifiedNum::from(50_000), + UnifiedNum::from_whole(0.0005), ) .expect("Should spend for Leader"); expected_balances .spend( CAMPAIGN_1.creator, CAMPAIGN_1.channel.follower.to_address(), - UnifiedNum::from(40_000), + UnifiedNum::from_whole(0.0004), ) .expect("Should spend for Follower"); expected_balances - .spend(CAMPAIGN_1.creator, *PUBLISHER, UnifiedNum::from(10_000_000)) + .spend(CAMPAIGN_1.creator, *PUBLISHER, UnifiedNum::from_whole(0.1)) .expect("Should spend for Publisher"); let expected_accounting = AccountingResponse { @@ -1231,29 +1525,33 @@ mod tests { info!(setup.logger, "Successfully validated Accounting Balances for Channel 1 {:?} after CAMPAIGN_1 events {:?}", CAMPAIGN_1.channel.id(), CAMPAIGN_1.id); } - // Channel 3 expected Accounting + // CAMPAIGN_3 & Channel 3 expected Accounting // Fees are calculated based on pro mile of the payout // event payout * fee / 1000 // + // leader fee (per 1000): 2 + // follower fee (per 1000): 1.75 + // IMPRESSION price (min): 0.015 + // CLICK price (min): 0.035 // - // IMPRESSION: - // - Publisher2 payout: 2 * 30 000 = 60 000 - // - Leader fees: 60 000 * 20 000 / 1 000 = 1 200 000 - // - Follower fees: 60 000 * 10 000 / 1000 = 600 000 + // 2 x IMPRESSION: + // - Publisher2 payout: 2 * 0.015 = = 0.030 UnifiedNum(3 000 000) + // - Leader fees: 2 * (0.015 * 2 / 1000) = 0.00006 = UnifiedNum(6 000) + // - Follower fees: 2 * (0.015 * 1.75 / 1000) = 0.0000525 = UnifiedNum(5 250) // - // CLICK: - // - Publisher2 payout: 2 * 60 000 = 120 000 - // - Leader fees: 120 000 * 20 000 / 1000 = 2 400 000 - // - Follower fees: 120 000 * 10 000 / 1000 = 1 200 000 + // 2 x CLICK: + // - Publisher2 payout: 2 * 0.035 = UnifiedNum(7 000 000) + // - Leader fees: 2 * (0.035 * 2 / 1000) = 0.00014 = UnifiedNum(14 000) + // - Follower fees: 2 * (0.035 * 1.75 / 1000) = 0.0001225 = UnifiedNum(12 250) // // Creator (Advertiser2) pays out: - // events_payout + leader fee + follower fee - // events_payout = 60 000 (impression) + 120 000 (click) = 180 000 - // 180 000 + (1 200 000 + 2 400 000) + (600 000 + 1 200 000) = 5 580 000 // - // Publisher2 total payout: 180 000 - // leader total fees: 1 200 000 + 2 400 000 = 3 600 000 - // follower total fees: 600 000 + 1 200 000 = 1 800 000 + // Publisher2 total payout: 2 * 0.015 (impression) + 2 * 0.035 (click) = 0.1 = UnifiedNum(10 000 000) + // Leader total fees: 0.00006 + 0.00014 = 0.00020 = UnifiedNum(20 000) + // Follower total fees: 0.0000525 + 0.0001225 = 0.000175 = UnifiedNum(17 500) + // + // events_payout + leader fee + follower fee + // 0.1 + 0.00020 + 0.000175 = 0.100375 = UnifiedNum(10 037 500) { let mut expected_balances = Balances::new(); @@ -1261,18 +1559,22 @@ mod tests { .spend( CAMPAIGN_3.creator, CAMPAIGN_3.channel.leader.to_address(), - UnifiedNum::from(3_600_000), + UnifiedNum::from_whole(0.00020), ) .expect("Should spend for Leader"); expected_balances .spend( CAMPAIGN_3.creator, CAMPAIGN_3.channel.follower.to_address(), - UnifiedNum::from(1_800_000), + UnifiedNum::from_whole(0.000175), ) .expect("Should spend for Follower"); expected_balances - .spend(CAMPAIGN_3.creator, *PUBLISHER_2, UnifiedNum::from(180_000)) + .spend( + CAMPAIGN_3.creator, + *PUBLISHER_2, + UnifiedNum::from_whole(0.1), + ) .expect("Should spend for Publisher"); let expected_accounting = AccountingResponse { @@ -1447,6 +1749,460 @@ mod tests { ); } } + + // For CAMPAIGN_2 & Channel 2 + // + // Check RejectState on Follower (LEADER) in Channel 2 after validator tick + // + // For Channel Follower (LEADER) which has all events: + // + // All payouts (for 5 IMPRESSIONs + 3/4 CLICKs): + // Advertiser (spender) = 14.0021 TOKENs + // Publisher (earner) = 14 TOKENs + // Leader (earner) = 0.0005 (IMPRESSIONs) + 0.0009 (CLICKs) = 0.0014 TOKENs + // Follower (earner) = 0.00025 (IMPRESSIONs) + 0.00045 (CLICKs) = 0.00070 TOKENs + // + // Total: 14.0021 TOKENs + // + // sum_our = 14.0021 + // sum_approved_mins (5 x IMPRESSION) = 5 + 0.0005 + 0.00025 = 5.00075 + // sum_approved_mins (3 x CLICKS) = 9 + 0.0009 + 0.00045 = 9.00135 + // + // For Channel Leader (FOLLOWER) which has only the 5 IMPRESSION events + // + // 5 x IMPRESSIONs (only) + // diff = 14.0021 - 5.00075 = 9.00135 + // health_penalty = 9.00135 * 1 000 / 30.0 = 300.045 + // health = 1 000 - health_penalty = 699.955 (Unsignable) + // + // Ganache health_threshold_promilles = 950 + // Ganache health_unsignable_promilles = 750 + { + let latest_reject_state_follower = leader_sentry + .get_our_latest_msg(CAMPAIGN_2.channel.id(), &["RejectState"]) + .await + .expect( + "Should fetch RejectState from Channel's Follower (Who am I) in Leader sentry", + ) + .map(|message| { + RejectState::::try_from(message) + .expect("Should be RejectState with valid Checked Balances") + }) + .expect( + "Should have a RejectState in Channel's Follower for the Campaign 2 channel", + ); + + let rejected_balances = latest_reject_state_follower + .balances + .expect("Channel Follower (LEADER) should have RejectState with balances"); + // Expected Leader (FOLLOWER) total rejected in Follower: 5.00075 + assert_eq!( + UnifiedNum::from_whole(5.00075), + rejected_balances + .sum() + .expect("Should not overflow summing balances") + // does not really matter if we're checking earners or spenders for CheckedState + .0 + ) + } + + // For CAMPAIGN_3 + // + // Check NewState existence of Channel 3 after the validator ticks + // For both Leader & Follower + // Assert that both states are the same! + // + // Check ApproveState of the Follower + // Assert that it exists in both validators + { + let latest_new_state_leader = leader_sentry + .get_our_latest_msg(CAMPAIGN_3.channel.id(), &["NewState"]) + .await + .expect("Should fetch NewState from Leader (Who am I) in Leader sentry") + .map(|message| { + NewState::::try_from(message) + .expect("Should be NewState with Checked Balances") + }) + .expect("Should have a NewState in Leader for the Campaign 3 channel"); + + // Check balances in Leader's NewState + { + let mut expected_balances = Balances::new(); + expected_balances + .spend( + CAMPAIGN_3.creator, + CAMPAIGN_3.channel.leader.to_address(), + UnifiedNum::from_whole(0.0002), + ) + .expect("Should spend"); + expected_balances + .spend( + CAMPAIGN_3.creator, + CAMPAIGN_3.channel.follower.to_address(), + UnifiedNum::from_whole(0.000175), + ) + .expect("Should spend"); + expected_balances + .spend( + CAMPAIGN_3.creator, + *PUBLISHER_2, + UnifiedNum::from_whole(0.1), + ) + .expect("Should spend"); + + pretty_assertions::assert_eq!( + latest_new_state_leader.balances, + expected_balances, + "Balances are as expected" + ); + } + + let last_approved_response_follower = follower_sentry + .get_last_approved(CAMPAIGN_3.channel.id()) + .await + .expect("Should fetch Approve state from Follower"); + + let last_approved_response_leader = leader_sentry + .get_last_approved(CAMPAIGN_3.channel.id()) + .await + .expect("Should fetch Approve state from Leader"); + + // Due to timestamp differences in the `received` field + // we can only `assert_eq!` the messages themselves + pretty_assertions::assert_eq!( + last_approved_response_leader + .heartbeats + .expect("Leader response should have heartbeats") + .clone() + .into_iter() + .map(|message| message.msg) + .collect::>(), + last_approved_response_follower + .heartbeats + .expect("Follower response should have heartbeats") + .clone() + .into_iter() + .map(|message| message.msg) + .collect::>(), + "Leader and Follower should both have the same last Approved response" + ); + + let last_approved_follower = last_approved_response_follower + .last_approved + .expect("Should have last approved messages for the events we've submitted"); + + let last_approved_leader = last_approved_response_leader + .last_approved + .expect("Should have last approved messages for the events we've submitted"); + + // Due to the received time that can be different in messages + // we must check the actual ValidatorMessage without the timestamps + { + let msg_new_state_leader = last_approved_leader + .new_state + .expect("Leader should have last approved NewState"); + + assert_eq!( + msg_new_state_leader.from, IDS[&LEADER], + "NewState should be received from Leader" + ); + + let msg_approve_state_leader = last_approved_leader + .approve_state + .expect("Leader should have last approved ApproveState"); + + assert_eq!( + msg_approve_state_leader.from, IDS[&FOLLOWER], + "ApproveState should be received from Follower" + ); + + let msg_new_state_follower = last_approved_follower + .new_state + .expect("Follower should have last approved NewState"); + + assert_eq!( + msg_new_state_follower.from, IDS[&LEADER], + "NewState should be received from Leader" + ); + + let msg_approve_state_follower = last_approved_follower + .approve_state + .expect("Follower should have last approved ApproveState"); + + assert_eq!( + msg_approve_state_follower.from, IDS[&FOLLOWER], + "ApproveState should be received from Follower" + ); + + let new_state_leader = msg_new_state_leader + .msg + .clone() + .into_inner() + .try_checked() + .expect("NewState should have valid CheckedState Balances"); + + let new_state_follower = msg_new_state_follower + .msg + .clone() + .into_inner() + .try_checked() + .expect("NewState should have valid CheckedState Balances"); + + assert_eq!( + new_state_leader, new_state_follower, + "Last approved NewState in Leader & Follower should be the same" + ); + + pretty_assertions::assert_eq!( + latest_new_state_leader, + new_state_leader, + "Latest NewState from Leader should be the same as last approved NewState from Leader & Follower" + ); + } + } + + // For CAMPAIGN_2 & Channel 2 + // Trigger Unhealthy but signable NewState with 5 IMPRESSIONs & 2 CLICKs in Leader + // As opposed to 5 IMPRESSIONs & 3 CLICKs in Follower + // + // 5 x IMPRESSION = 5 TOKENs + // 3 x CLICK = 9 TOKENs + // + // 14 TOKENs + // + // IMPRESSIONs: + // + // 5 x leader fee = 5 * (1 TOKENs * 0.1 (fee) / 1000 (pro mile) ) = 5 x 0.0001 TOKENs = 0.0005 TOKENs + // 5 x follower fee = 5 x ( 1 TOKENs * 0.05 (fee) / 1000 (pro_mile) ) = 5 x 0.00005 TOKENs = 0.00025 TOKENs + // + // CLICKs (for 3): + // + // 3 x leader fee = 3 * (3 TOKENs * 0.1 (fee) / 1000 (pro mile) ) = 3 x 0.0003 TOKENs = 0.0009 TOKENs + // 3 x follower fee = 3 x ( 3 TOKENs * 0.05 (fee) / 1000 (pro_mile) ) = 3 x 0.00015 TOKENs = 0.00045 TOKENs + // + // CLICKS (for 2): + // 2 x leader fee = 2 * (3 TOKENs * 0.1 (fee) / 1000 (pro mile) ) = 2 x 0.0003 TOKENs = 0.0006 TOKENs + // 2 x follower fee = 2 x ( 3 TOKENs * 0.05 (fee) / 1000 (pro_mile) ) = 2 x 0.00015 TOKENs = 0.00030 TOKENs + // + // Payouts (all current Follower events: 5 IMPRESSIONs + 3 CLICKs): + // Advertiser (spender) = 14.0021 + // Publisher (earner) = 14 TOKENs + // Leader (earner) = 0.0005 (IMPRESSIONs) + 0.0009 (CLICKs) = 0.0014 TOKENs + // Follower (earner) = 0.00025 (IMPRESSIONs) + 0.00045 (CLICKs) = 0.00070 TOKENs + // + // Total: 14.0021 + // + // sum_our = 14.0021 + // sum_approved_mins (5 x IMPRESSION) = 5 + 0.0005 + 0.00025 = 5.00075 + // sum_approved_mins (2 x CLICKS) = 6 + 0.0006 + 0.00030 = 6.0009 + // ------------ + // 11.00165 + // + // 5 IMPRESSIONs + 2 CLICKs + // + // diff = 14.0021 - (5.00075 + 6.0009) = 3.00045 + // health_penalty = 3.00045 * 1 000 / 30.0 = 100.015 + // + // health = 1 000 - health_penalty = 899.985 (Unhealthy but Signable) + // + // Ganache health_threshold_promilles = 950 + // Ganache health_unsignable_promilles = 750 + // + // Add new events for `CAMPAIGN_2` to sentry Follower + // Should trigger RejectedState + // + // Note: Follower and Leader for this channel are reversed! + // + // Channel's Leader (FOLLOWER) events opposed to Channel's Follower (LEADER) events: + // + // 5 IMPRESSIONs + // 2 (out of 3) CLICKs + { + // Prepare 2 out of 4 CLICK events to trigger unhealthy `ApproveState` on the Follower of the Channel (LEADER) + let channel_leader_events = &CAMPAIGN_2_EVENTS[5..=6]; + + let channel_leader_response = post_new_events( + // the Leader of this channel is FOLLOWER! + &follower_sentry, + token_chain_1337.clone().with(CAMPAIGN_2.id), + &channel_leader_events, + ) + .await + .expect("Posted events"); + + assert_eq!(SuccessResponse { success: true }, channel_leader_response); + + info!( + setup.logger, + "Successful POST of 2/3 CLICK events found in the Channel's Follower for CAMPAIGN_2 {:?} and Channel {:?} to Channel Leader to trigger unhealthy ApproveState", + CAMPAIGN_2.id, + CAMPAIGN_2.channel.id() + ); + } + + // IMPORTANT! Call the FOLLOWER tick first as it's the Channel 2 Leader! + // This will trigger a the new state and the Channel 2 follower (LEADER) will process that. + + // follower single worker tick + follower_worker.all_channels_tick().await; + // leader single worker tick + leader_worker.all_channels_tick().await; + + { + let latest_approve_state_follower = leader_sentry + .get_our_latest_msg(CAMPAIGN_2.channel.id(), &["ApproveState"]) + .await + .expect( + "Should fetch ApproveState from Channel's Follower (Who am I) from LEADER sentry", + ) + .map(|message| { + ApproveState::try_from(message) + .expect("Should be ApproveState with valid Checked Balances") + }) + .expect("Should have a ApproveState in Channel's Follower for the Campaign 2 channel"); + + let latest_new_state_leader = follower_sentry + .get_our_latest_msg(CAMPAIGN_2.channel.id(), &["NewState"]) + .await + .expect( + "Should fetch NewState from Channel's Leader (Who am I) from FOLLOWER sentry", + ) + .map(|message| { + NewState::::try_from(message) + .expect("Should be NewState with valid Checked Balances") + }) + .expect("Should have a NewState in Channel's Follower for the Campaign 2 channel"); + + assert_eq!( + latest_approve_state_follower.state_root, latest_new_state_leader.state_root, + "Latest ApproveState in Follower should correspond to the latest NewState Leader" + ); + assert!(!latest_approve_state_follower.is_healthy); + assert_eq!( + UnifiedNum::from_whole(11.00165), + latest_new_state_leader + .balances + .sum() + .expect("Should not overflow summing balances") + // does not really matter if we're checking earners or spenders for CheckedState + .0 + ) + } + + // For CAMPAIGN_2 & Channel 2 + // + // Post new events to Channel Leader (FOLLOWER) + // + // Trigger a healthy ApproveState by posting the last CLICK event in Leader (FOLLOWER) + // this will create a healthy NewState and have the exact same number of events as the Follower (LEADER) + // + // 5 x IMPRESSION = 5 TOKENs + // 3 x CLICK = 9 TOKENs + // + // 14 TOKENs + // + // Channel's Leader (FOLLOWER) events opposed to Channel's Follower (LEADER) events: + // + // 5 IMPRESSIONs + // 3 (out of 3) CLICKs + { + // Take the last CLICK event + let channel_leader_events = [CAMPAIGN_2_EVENTS[7].clone()]; + + let channel_leader_response = post_new_events( + &follower_sentry, + token_chain_1337.clone().with(CAMPAIGN_2.id), + // the Leader of this channel is FOLLOWER! + &channel_leader_events, + ) + .await + .expect("Posted events"); + + assert_eq!(SuccessResponse { success: true }, channel_leader_response); + + info!( + setup.logger, + "Successful POST of the last CLICK event for CAMPAIGN_2 {:?} and Channel {:?} to Leader to trigger Healthy NewState", + CAMPAIGN_2.id, + CAMPAIGN_2.channel.id() + ); + } + + // IMPORTANT! Call the FOLLOWER tick first as it's the Channel 2 Leader! + // This will trigger a the new state and the Channel 2 follower (LEADER) will process that. + + // follower single worker tick + follower_worker.all_channels_tick().await; + // leader single worker tick + leader_worker.all_channels_tick().await; + + // For CAMPAIGN_2 Channel 2 + // + // Healthy ApproveState + // + { + let latest_approve_state_follower = leader_sentry + .get_our_latest_msg(CAMPAIGN_2.channel.id(), &["ApproveState"]) + .await + .expect( + "Should fetch ApproveState from Channel's Follower (Who am I) from LEADER sentry", + ) + .map(|message| { + ApproveState::try_from(message) + .expect("Should be ApproveState with valid Checked Balances") + }) + .expect("Should have a ApproveState in Channel's Follower for the Campaign 2 channel"); + + assert!( + latest_approve_state_follower.is_healthy, + "ApproveState in Channel's Follower (LEADER) should be healthy" + ); + + let latest_new_state_leader = follower_sentry + .get_our_latest_msg(CAMPAIGN_2.channel.id(), &["NewState"]) + .await + .expect( + "Should fetch NewState from Channel's Leader (Who am I) from FOLLOWER sentry", + ) + .map(|message| { + NewState::::try_from(message) + .expect("Should be NewState with valid Checked Balances") + }) + .expect("Should have a NewState in Channel's Follower for the Campaign 2 channel"); + + assert_eq!( + latest_new_state_leader.state_root, + latest_approve_state_follower.state_root + ); + + // double check that the ApproveStateResponse in both validators is present + // and that they are the same + let last_approve_state_leader = follower_sentry + .get_last_approved(CAMPAIGN_2.channel.id()) + .await + .expect( + "Should fetch Last Approved Response from Channel's Leader (Who am I) from FOLLOWER sentry", + ).last_approved.expect("Should have an ApproveState & NewState"); + + let last_approve_state_follower = leader_sentry + .get_last_approved(CAMPAIGN_2.channel.id()) + .await + .expect( + "Should fetch Last Approved Response from Channel's FOllower (Who am I) from LEADER sentry", + ).last_approved.expect("Should have an ApproveState & NewState"); + + assert_eq!( + last_approve_state_leader.new_state.unwrap(), + last_approve_state_follower.new_state.unwrap(), + "The NewState Messages in Channel's Leader & Follower should be the same" + ); + + assert_eq!( + last_approve_state_leader.approve_state.unwrap(), + last_approve_state_follower.approve_state.unwrap(), + "The ApproveState Messages in Channel's Leader & Follower should be the same" + ); + } } async fn setup_sentry(validator: &TestValidator) -> adapter::ethereum::LockedAdapter { From b3ad0afda707ae53c15f4fc8056beb810e824943 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Wed, 25 May 2022 11:54:44 +0200 Subject: [PATCH 13/15] test_harness - fix assertions with timestamp for Validator Messages --- test_harness/src/lib.rs | 46 +++++++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/test_harness/src/lib.rs b/test_harness/src/lib.rs index c91d3b28f..5ed90b765 100644 --- a/test_harness/src/lib.rs +++ b/test_harness/src/lib.rs @@ -2188,20 +2188,44 @@ mod tests { .get_last_approved(CAMPAIGN_2.channel.id()) .await .expect( - "Should fetch Last Approved Response from Channel's FOllower (Who am I) from LEADER sentry", + "Should fetch Last Approved Response from Channel's Follower (Who am I) from LEADER sentry", ).last_approved.expect("Should have an ApproveState & NewState"); - assert_eq!( - last_approve_state_leader.new_state.unwrap(), - last_approve_state_follower.new_state.unwrap(), - "The NewState Messages in Channel's Leader & Follower should be the same" - ); + // compare NewState from Leader & Follower + // NOTE: The `received` timestamp can differ, so compare everything except `received`! + { + let leader = last_approve_state_leader.new_state.unwrap(); + let follower = last_approve_state_follower.new_state.unwrap(); - assert_eq!( - last_approve_state_leader.approve_state.unwrap(), - last_approve_state_follower.approve_state.unwrap(), - "The ApproveState Messages in Channel's Leader & Follower should be the same" - ); + assert_eq!( + leader.msg, + follower.msg, + "The NewState Messages in Channel's Leader & Follower should be the same" + ); + assert_eq!( + leader.from, + follower.from, + "The NewState Messages in Channel's Leader & Follower should have the same `from` address" + ); + } + + // compare ApproveState from Leader & Follower + // NOTE: The `received` timestamp can differ, so compare everything except `received`! + { + let leader = last_approve_state_leader.approve_state.unwrap(); + let follower = last_approve_state_follower.approve_state.unwrap(); + + assert_eq!( + leader.msg, + follower.msg, + "The ApproveState Messages in Channel's Leader & Follower should be the same" + ); + assert_eq!( + leader.from, + follower.from, + "The ApproveState Messages in Channel's Leader & Follower should have the same `from` address" + ); + } } } From e2040506d94f11b03afa0b7f8dd5b1e0f62c9b9c Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Wed, 25 May 2022 13:56:54 +0200 Subject: [PATCH 14/15] sentry - fix drop of Object holding the Pools --- sentry/src/lib.rs | 47 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/sentry/src/lib.rs b/sentry/src/lib.rs index cc3abb2f6..7ce642b31 100644 --- a/sentry/src/lib.rs +++ b/sentry/src/lib.rs @@ -491,13 +491,15 @@ pub struct Auth { #[cfg(test)] pub mod test_util { + use std::ops; + use adapter::{ dummy::{Dummy, Options}, Adapter, }; use primitives::{ config::GANACHE_CONFIG, - test_util::{discard_logger, CREATOR, FOLLOWER, IDS, LEADER}, + test_util::{discard_logger, DUMMY_AUTH, IDS, LEADER}, }; use crate::{ @@ -510,19 +512,38 @@ pub mod test_util { Application, }; + /// This guard holds the Redis and Postgres pools taken from their respective Pool of pools. + /// + /// This ensures that they will not be dropped which will cause tests to fail randomly. + pub struct ApplicationGuard { + pub app: Application, + #[allow(dead_code)] + redis_pool: deadpool::managed::Object, + #[allow(dead_code)] + db_pool: deadpool::managed::Object, + } + + impl ops::Deref for ApplicationGuard { + type Target = Application; + + fn deref(&self) -> &Self::Target { + &self.app + } + } + impl ops::DerefMut for ApplicationGuard { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.app + } + } + /// Uses development and therefore the goerli testnet addresses of the tokens - /// It still uses DummyAdapter. - pub async fn setup_dummy_app() -> Application { + /// but still uses the `DummyAdapter`. + pub async fn setup_dummy_app() -> ApplicationGuard { let config = GANACHE_CONFIG.clone(); + let adapter = Adapter::new(Dummy::init(Options { dummy_identity: IDS[&LEADER], - dummy_auth_tokens: vec![ - (*CREATOR, "AUTH_Creator".into()), - (*LEADER, "AUTH_Leader".into()), - (*FOLLOWER, "AUTH_Follower".into()), - ] - .into_iter() - .collect(), + dummy_auth_tokens: DUMMY_AUTH.clone(), })); let redis = TESTS_POOL.get().await.expect("Should return Object"); @@ -550,6 +571,10 @@ pub mod test_util { platform_api, ); - app + ApplicationGuard { + app, + redis_pool: redis, + db_pool: database, + } } } From 30fc79ce686922ee6fa424e207b4fdacd6786486 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Wed, 25 May 2022 16:20:58 +0200 Subject: [PATCH 15/15] remove WIP units-for-slot tests --- sentry/src/routes/units_for_slot_test.rs | 761 ----------------------- 1 file changed, 761 deletions(-) delete mode 100644 sentry/src/routes/units_for_slot_test.rs diff --git a/sentry/src/routes/units_for_slot_test.rs b/sentry/src/routes/units_for_slot_test.rs deleted file mode 100644 index fdc496169..000000000 --- a/sentry/src/routes/units_for_slot_test.rs +++ /dev/null @@ -1,761 +0,0 @@ -use crate::{platform::PlatformApi, test_util::setup_dummy_app}; - -use super::*; -use adapter::Dummy; -use chrono::{DateTime, TimeZone, Utc}; -use hyper::Body; -use hyper::{ - body::Bytes, - http::{header::USER_AGENT, request::Request}, -}; -use primitives::{ - campaign::Pricing, - platform::AdUnitsResponse, - supermarket::units_for_slot::response::{Campaign as ResponseCampaign, UnitsWithPrice}, - targeting::Rules, - targeting::{input, Function, Rule, Value}, - test_util::{DUMMY_AD_UNITS, DUMMY_CAMPAIGN, IDS, LEADER_2, PUBLISHER, PUBLISHER_2}, - AdSlot, BigNum, Channel, ChannelId, -}; -use reqwest::Url; -use std::{collections::HashMap, iter::Iterator, str::FromStr}; -use wiremock::{ - matchers::{method, path}, - Mock, MockServer, ResponseTemplate, -}; - -// User Agent OS: Linux (only in `woothee`) -// User Agent Browser Family: Firefox -const TEST_USER_AGENT: &str = - "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:83.0) Gecko/20100101 Firefox/83.0"; -// uses two-letter country codes: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 -const TEST_CLOUDFLARE_IPCOUNTY: &str = "BG"; - -/// Uses the Channel AdUnits as UnitsWithPrice for the response -fn get_mock_campaign(campaign: Campaign) -> ResponseCampaign { - let units_with_price = get_units_with_price(&campaign); - ResponseCampaign { - campaign: ResponseCampaign::from(campaign), - units_with_price, - } -} - -fn get_units_with_price(campaign: &Campaign) -> Vec { - campaign - .ad_units - .iter() - .map(|u| UnitsWithPrice { - unit: u.into(), - price: campaign - .pricing_bounds - .get(&IMPRESSION) - .expect("Campaign should have Pricing Bounds for impression") - .min, - }) - .collect() -} - -fn get_mock_rules(categories: &[&str]) -> Vec { - let get_rule = Function::new_get("adSlot.categories"); - let categories_array = Value::Array(categories.iter().map(|s| Value::new_string(s)).collect()); - let intersects_rule = Function::new_intersects(get_rule, categories_array); - vec![Function::new_only_show_if(intersects_rule).into()] -} - -fn get_test_ad_slot(rules: &[Rule], categories: &[&str]) -> AdSlot { - AdSlot { - // TODO: Replace with IPFS for testing - ipfs: "QmVwXu9oEgYSsL6G1WZtUQy6dEReqs3Nz9iaW4Cq5QLV8C" - .parse() - .expect("Valid IPFS"), - ad_type: "legacy_250x250".to_string(), - archived: false, - created: Utc.timestamp(1_564_383_600, 0), - description: Some("Test slot for running integration tests".to_string()), - fallback_unit: None, - min_per_impression: Some( - vec![( - "0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359" - .parse() - .expect("Valid Address"), - // 0.0007 - 70000.into(), - )] - .into_iter() - .collect(), - ), - modified: Some(Utc.timestamp(1_564_383_600, 0)), - owner: IDS[PUBLISHER], - title: Some("Test slot 1".to_string()), - website: Some("https://adex.network".to_string()), - rules: rules.to_vec(), - } - - // AdSlotResponse { - // slot: ad_slot, - // accepted_referrers: vec![ - // Url::from_str("https://adex.network").expect("should parse"), - // Url::from_str("https://www.adex.network").expect("should parse"), - // ], - // categories: categories.iter().map(|s| String::from(*s)).collect(), - // alexa_rank: Some(1337.0), - // } -} - -/// `seconds_since_epoch` should be set from the actual response, -/// this ensures that the timestamp will always match in the tests, -/// otherwise random tests will fail with +- 1-2-3 seconds difference -fn get_expected_response( - campaigns: Vec, - seconds_since_epoch: DateTime, -) -> UnitsForSlotResponse { - let targeting_input_base = Input { - ad_view: None, - global: input::Global { - ad_slot_id: "QmVwXu9oEgYSsL6G1WZtUQy6dEReqs3Nz9iaW4Cq5QLV8C" - .parse() - .expect("Valid IPFS"), - ad_slot_type: "legacy_250x250".to_string(), - publisher_id: *PUBLISHER, - country: Some(TEST_CLOUDFLARE_IPCOUNTY.to_string()), - event_type: IMPRESSION, - seconds_since_epoch, - user_agent_os: Some("Linux".to_string()), - user_agent_browser_family: Some("Firefox".to_string()), - }, - ad_unit_id: None, - balances: None, - campaign: None, - ad_slot: Some(input::AdSlot { - categories: vec!["IAB3".into(), "IAB13-7".into(), "IAB5".into()], - hostname: "adex.network".to_string(), - alexa_rank: Some(1337.0), - }), - }; - - UnitsForSlotResponse { - targeting_input_base: targeting_input_base.into(), - accepted_referrers: vec![], - campaigns, - fallback_unit: None, - } -} - -fn mock_campaign(rules: &[Rule]) -> Campaign { - let mut campaign = DUMMY_CAMPAIGN.clone(); - - campaign.ad_units = DUMMY_AD_UNITS.to_vec(); - // NOTE: always set the spec.targeting_rules first - campaign.targeting_rules = Rules(rules.to_vec()); - // override pricing for `IMPRESSION` - campaign.pricing_bounds.insert( - IMPRESSION, - Pricing { - // 0.0001 - min: 10_000.into(), - // 0.001 - max: 100_000.into(), - }, - ); - // Timestamp: 1_606_136_400_000 - campaign.active.from = Some(Utc.ymd(2020, 11, 23).and_hms(15, 0, 0)); - - campaign -} - -// fn mock_cache_campaign(channel: Channel, status: Status) -> HashMap { -// let mut campaigns = HashMap::new(); - -// let mut campaign = Campaign { -// channel, -// status, -// balances: Default::default(), -// }; -// campaign -// .balances -// .insert(*PUBLISHER, 100_000_000_000_000.into()); - -// campaigns.insert(campaign.channel.id, campaign); -// campaigns -// } - -/// Assumes all `Campaign`s are `Active` -/// adds to Balances the `Publisher` address with `1 * 10^14` balance -// fn mock_multiple_cache_campaigns(channels: Vec) -> HashMap { -// let mut campaigns = HashMap::new(); - -// for channel in channels { -// let mut campaign = Campaign { -// channel, -// status: Status::Active, -// balances: Default::default(), -// }; -// campaign -// .balances -// .insert(*PUBLISHER, 100_000_000_000_000.into()); - -// campaigns.insert(campaign.channel.id, campaign); -// } - -// campaigns -// } - -/// Sets platform at the `{server_uri}/platform` path of the [`MockServer`] -pub async fn init_app_with_mocked_platform(server: &MockServer) -> Application { - let platform_url = (server.uri() + "/platform").parse().unwrap(); - let mut dummy_app = setup_dummy_app().await; - - let platform_api = - PlatformApi::new(platform_url, dummy_app.config.platform.keep_alive_interval) - .expect("should build test PlatformApi"); - // override the Dummy app PlatformApi - dummy_app.platform_api = platform_api; - - dummy_app -} - -#[tokio::test] -async fn targeting_input() { - let server = MockServer::start().await; - - let app = init_app_with_mocked_platform(&server).await; - - let categories: [&str; 3] = ["IAB3", "IAB13-7", "IAB5"]; - let rules = get_mock_rules(&categories); - let campaign = mock_campaign(&rules); - - let platform_ad_units = AdUnitsResponse(campaign.ad_units.clone()); - let mock_slot = get_test_ad_slot(&rules, &categories); - - Mock::given(method("GET")) - .and(path("/platform/units")) - .respond_with(ResponseTemplate::new(200).set_body_json(&platform_ad_units)) - .mount(&server) - .await; - - let request = Request::post("/units-for-slot") - .header(USER_AGENT, TEST_USER_AGENT) - .header(CLOUDFLARE_IPCOUNTY_HEADER.clone(), TEST_CLOUDFLARE_IPCOUNTY) - .body( - serde_json::to_vec(&RequestBody { - ad_slot: mock_slot, - deposit_assets: Some(vec![campaign.channel.token].into_iter().collect()), - }) - .expect("Should serialize"), - ) - .unwrap(); - - let actual_response = post_units_for_slot(request, &app) - .await - .expect("call shouldn't fail with provided data"); - - assert_eq!(StatusCode::OK, actual_response.status()); - - let units_for_slot: UnitsForSlotResponse = - serde_json::from_slice(&hyper::body::to_bytes(actual_response).await.unwrap()) - .expect("Should deserialize"); - - // we must use the same timestamp as the response, otherwise our tests will fail randomly - let expected_response = get_expected_response( - vec![campaign], - units_for_slot - .targeting_input_base - .global - .seconds_since_epoch - .clone(), - ); - - pretty_assertions::assert_eq!( - expected_response.targeting_input_base, - units_for_slot.targeting_input_base - ); - - assert_eq!( - expected_response.campaigns.len(), - units_for_slot.campaigns.len() - ); - assert_eq!(expected_response.campaigns, units_for_slot.campaigns); - assert_eq!( - expected_response.fallback_unit, - units_for_slot.fallback_unit - ); -} - -#[tokio::test] -async fn non_active_campaign() { - let server = MockServer::start().await; - - let app = init_app_with_mocked_platform(&server).await; - - let categories: [&str; 3] = ["IAB3", "IAB13-7", "IAB5"]; - let rules = get_mock_rules(&categories); - let campaign = mock_campaign(&rules); - - let platform_ad_units = AdUnitsResponse(campaign.ad_units); - let mock_slot = get_test_ad_slot(&rules, &categories); - - Mock::given(method("GET")) - .and(path("/platform/units")) - .respond_with(ResponseTemplate::new(200).set_body_json(&platform_ad_units)) - .mount(&server) - .await; - - let request = Request::post("/units-for-slot") - .header(USER_AGENT, TEST_USER_AGENT) - .header(CLOUDFLARE_IPCOUNTY_HEADER.clone(), TEST_CLOUDFLARE_IPCOUNTY) - .body( - serde_json::to_vec(&RequestBody { - ad_slot: mock_slot, - deposit_assets: Some(vec![campaign.channel.token].into_iter().collect()), - }) - .expect("Should serialize"), - ) - .unwrap(); - - let actual_response = post_units_for_slot(request, &app) - .await - .expect("call shouldn't fail with provided data"); - - assert_eq!(StatusCode::OK, actual_response.status()); - - let units_for_slot: UnitsForSlotResponse = - serde_json::from_slice(&hyper::body::to_bytes(actual_response).await.unwrap()) - .expect("Should deserialize"); - - // we must use the same timestamp as the response, otherwise our tests will fail randomly - let expected_response = get_expected_response( - vec![], - units_for_slot - .targeting_input_base - .global - .seconds_since_epoch - .clone(), - ); - - pretty_assertions::assert_eq!( - expected_response.targeting_input_base, - units_for_slot.targeting_input_base - ); - - assert_eq!( - expected_response.campaigns.len(), - units_for_slot.campaigns.len() - ); - assert_eq!(expected_response.campaigns, units_for_slot.campaigns); - assert_eq!( - expected_response.fallback_unit, - units_for_slot.fallback_unit - ); -} - -#[tokio::test] -async fn creator_is_publisher() { - let server = MockServer::start().await; - - let app = init_app_with_mocked_platform(&server).await; - - let categories: [&str; 3] = ["IAB3", "IAB13-7", "IAB5"]; - let rules = get_mock_rules(&categories); - let mut campaign = mock_campaign(&rules); - campaign.creator = *PUBLISHER; - - let platform_ad_units = AdUnitsResponse(campaign.ad_units.clone()); - let mock_slot = get_test_ad_slot(&rules, &categories); - - Mock::given(method("GET")) - .and(path("/platform/units")) - .respond_with(ResponseTemplate::new(200).set_body_json(&platform_ad_units)) - .mount(&server) - .await; - - let request = Request::post("/units-for-slot") - .header(USER_AGENT, TEST_USER_AGENT) - .header(CLOUDFLARE_IPCOUNTY_HEADER.clone(), TEST_CLOUDFLARE_IPCOUNTY) - .body( - Bytes::from_static( - &serde_json::to_vec(&RequestBody { - ad_slot: mock_slot, - deposit_assets: Some(vec![campaign.channel.token].into_iter().collect()), - }) - .expect("Should serialize"), - ) - .into(), - ) - .unwrap(); - - let actual_response = post_units_for_slot(request, &app) - .await - .expect("call shouldn't fail with provided data"); - - assert_eq!(StatusCode::OK, actual_response.status()); - - let units_for_slot: UnitsForSlotResponse = - serde_json::from_slice(&hyper::body::to_bytes(actual_response).await.unwrap()) - .expect("Should deserialize"); - - // we must use the same timestamp as the response, otherwise our tests will fail randomly - let expected_response = get_expected_response( - vec![], - units_for_slot - .targeting_input_base - .global - .seconds_since_epoch - .clone(), - ); - - pretty_assertions::assert_eq!( - expected_response.targeting_input_base, - units_for_slot.targeting_input_base - ); - - assert_eq!( - expected_response.campaigns.len(), - units_for_slot.campaigns.len() - ); - assert_eq!(expected_response.campaigns, units_for_slot.campaigns); - assert_eq!( - expected_response.fallback_unit, - units_for_slot.fallback_unit - ); -} - -#[tokio::test] -async fn no_ad_units() { - let server = MockServer::start().await; - - let app = init_app_with_mocked_platform(&server).await; - - let categories: [&str; 3] = ["IAB3", "IAB13-7", "IAB5"]; - let rules = get_mock_rules(&categories); - let mut campaign = mock_campaign(&rules); - campaign.ad_units = vec![]; - - let platform_ad_units = AdUnitsResponse(campaign.ad_units.clone()); - let mock_slot = get_test_ad_slot(&rules, &categories); - - Mock::given(method("GET")) - .and(path("/platform/units")) - .respond_with(ResponseTemplate::new(200).set_body_json(&platform_ad_units)) - .mount(&server) - .await; - - let request = Request::post("/units-for-slot") - .header(USER_AGENT, TEST_USER_AGENT) - .header(CLOUDFLARE_IPCOUNTY_HEADER.clone(), TEST_CLOUDFLARE_IPCOUNTY) - .body( - serde_json::to_vec(&RequestBody { - ad_slot: mock_slot, - deposit_assets: Some(vec![campaign.channel.token].into_iter().collect()), - }) - .expect("Should serialize"), - ) - .unwrap(); - - let actual_response = post_units_for_slot(request, &app) - .await - .expect("call shouldn't fail with provided data"); - - assert_eq!(StatusCode::OK, actual_response.status()); - - let units_for_slot: UnitsForSlotResponse = - serde_json::from_slice(&hyper::body::to_bytes(actual_response).await.unwrap()) - .expect("Should deserialize"); - - // we must use the same timestamp as the response, otherwise our tests will fail randomly - let expected_response = get_expected_response( - vec![], - units_for_slot - .targeting_input_base - .global - .seconds_since_epoch - .clone(), - ); - - pretty_assertions::assert_eq!( - expected_response.targeting_input_base, - units_for_slot.targeting_input_base - ); - - assert_eq!( - expected_response.campaigns.len(), - units_for_slot.campaigns.len() - ); - assert_eq!(expected_response.campaigns, units_for_slot.campaigns); - assert_eq!( - expected_response.fallback_unit, - units_for_slot.fallback_unit - ); -} - -#[tokio::test] -async fn price_less_than_min_per_impression() { - let server = MockServer::start().await; - - let app = init_app_with_mocked_platform(&server).await; - - let categories: [&str; 3] = ["IAB3", "IAB13-7", "IAB5"]; - let rules = get_mock_rules(&categories); - let mut campaign = mock_campaign(&rules); - campaign - .pricing_bounds - .get_mut(&IMPRESSION) - .expect("Campaign should have IMPRESSION pricing bound") - // 0.00001 - // should be less than `config.limits.units_for_slot.global_min_impression_price` - .min = 1_000.into(); - - let platform_ad_units = AdUnitsResponse(campaign.ad_units.clone()); - let mock_slot = get_test_ad_slot(&rules, &categories); - - Mock::given(method("GET")) - .and(path("/platform/units")) - .respond_with(ResponseTemplate::new(200).set_body_json(&platform_ad_units)) - .mount(&server) - .await; - - let request = Request::post("/units-for-slot") - .header(USER_AGENT, TEST_USER_AGENT) - .header(CLOUDFLARE_IPCOUNTY_HEADER.clone(), TEST_CLOUDFLARE_IPCOUNTY) - .body( - serde_json::to_vec(&RequestBody { - ad_slot: mock_slot, - deposit_assets: Some(vec![campaign.channel.token].into_iter().collect()), - }) - .expect("Should serialize"), - ) - .unwrap(); - - let actual_response = post_units_for_slot(request, &app) - .await - .expect("call shouldn't fail with provided data"); - - assert_eq!(StatusCode::OK, actual_response.status()); - - let units_for_slot: UnitsForSlotResponse = - serde_json::from_slice(&hyper::body::to_bytes(actual_response).await.unwrap()) - .expect("Should deserialize"); - - // we must use the same timestamp as the response, otherwise our tests will fail randomly - let expected_response = get_expected_response( - vec![], - units_for_slot - .targeting_input_base - .global - .seconds_since_epoch - .clone(), - ); - - pretty_assertions::assert_eq!( - expected_response.targeting_input_base, - units_for_slot.targeting_input_base - ); - - assert_eq!( - expected_response.campaigns.len(), - units_for_slot.campaigns.len() - ); - assert_eq!(expected_response.campaigns, units_for_slot.campaigns); - assert_eq!( - expected_response.fallback_unit, - units_for_slot.fallback_unit - ); -} - -#[tokio::test] -async fn non_matching_deposit_asset() { - let server = MockServer::start().await; - - let app = init_app_with_mocked_platform(&server).await; - - let categories: [&str; 3] = ["IAB3", "IAB13-7", "IAB5"]; - let rules = get_mock_rules(&categories); - let mut campaign = mock_campaign(&rules); - campaign.channel.token = "0x000000000000000000000000000000000000000".parse().unwrap(); - - let platform_ad_units = AdUnitsResponse(campaign.ad_units.clone()); - let mock_slot = get_test_ad_slot(&rules, &categories); - - Mock::given(method("GET")) - .and(path("/platform/units")) - .respond_with(ResponseTemplate::new(200).set_body_json(&platform_ad_units)) - .mount(&server) - .await; - - let request = Request::post("/units-for-slot/") - .header(USER_AGENT, TEST_USER_AGENT) - .header(CLOUDFLARE_IPCOUNTY_HEADER.clone(), TEST_CLOUDFLARE_IPCOUNTY) - .body( - serde_json::to_vec(&RequestBody { - ad_slot: mock_slot, - deposit_assets: Some(vec![DUMMY_CAMPAIGN.channel.token].into_iter().collect()), - }) - .expect("Should serialize"), - ) - .unwrap(); - - let actual_response = post_units_for_slot(request, &app) - .await - .expect("call shouldn't fail with provided data"); - - assert_eq!(StatusCode::OK, actual_response.status()); - - let units_for_slot: UnitsForSlotResponse = - serde_json::from_slice(&hyper::body::to_bytes(actual_response).await.unwrap()) - .expect("Should deserialize"); - - // we must use the same timestamp as the response, otherwise our tests will fail randomly - let expected_response = get_expected_response( - vec![], - units_for_slot - .targeting_input_base - .global - .seconds_since_epoch - .clone(), - ); - - pretty_assertions::assert_eq!( - expected_response.targeting_input_base, - units_for_slot.targeting_input_base - ); - - assert_eq!( - expected_response.campaigns.len(), - units_for_slot.campaigns.len() - ); - assert_eq!(expected_response.campaigns, units_for_slot.campaigns); - assert_eq!( - expected_response.fallback_unit, - units_for_slot.fallback_unit - ); -} - -#[tokio::test] -async fn multiple_campaigns() { - let server = MockServer::start().await; - - let app = init_app_with_mocked_platform(&server).await; - - let categories: [&str; 3] = ["IAB3", "IAB13-7", "IAB5"]; - let rules = get_mock_rules(&categories); - let campaign = mock_campaign(&rules); - - let non_matching_categories: [&str; 3] = ["IAB2", "IAB9-WS1", "IAB19"]; - let non_matching_rules = get_mock_rules(&non_matching_categories); - let mut non_matching_campaign = mock_campaign(&non_matching_rules); - non_matching_campaign.channel.leader = LEADER_2; - non_matching_campaign.creator = *PUBLISHER; - - let platform_ad_units = AdUnitsResponse(campaign.ad_units.clone()); - let mock_slot = get_test_ad_slot(&rules, &categories); - - Mock::given(method("GET")) - .and(path("/platform/units")) - .respond_with(ResponseTemplate::new(200).set_body_json(&platform_ad_units)) - .mount(&server) - .await; - - let campaign = get_mock_campaign(campaign.clone()); - - let request = Request::post("/units-for-slot") - .header(USER_AGENT, TEST_USER_AGENT) - .header(CLOUDFLARE_IPCOUNTY_HEADER.clone(), TEST_CLOUDFLARE_IPCOUNTY) - .body( - serde_json::to_vec(&RequestBody { - ad_slot: mock_slot, - deposit_assets: Some(vec![DUMMY_CAMPAIGN.channel.token].into_iter().collect()), - }) - .expect("Should serialize"), - ) - .unwrap(); - - let actual_response = post_units_for_slot(request, &app) - .await - .expect("call shouldn't fail with provided data"); - - assert_eq!(StatusCode::OK, actual_response.status()); - - let units_for_slot: UnitsForSlotResponse = - serde_json::from_slice(&hyper::body::to_bytes(actual_response).await.unwrap()) - .expect("Should deserialize"); - - // we must use the same timestamp as the response, otherwise our tests will fail randomly - let expected_response = get_expected_response( - vec![campaign], - units_for_slot - .targeting_input_base - .global - .seconds_since_epoch - .clone(), - ); - - pretty_assertions::assert_eq!( - expected_response.targeting_input_base, - units_for_slot.targeting_input_base - ); - - assert_eq!( - expected_response.campaigns.len(), - units_for_slot.campaigns.len() - ); - assert_eq!(expected_response.campaigns, units_for_slot.campaigns); - assert_eq!( - expected_response.fallback_unit, - units_for_slot.fallback_unit - ); -} - -#[tokio::test] -#[ignore = "exists to print output for comparison"] -async fn get_sample_units_for_slot_output() { - let logger = discard_logger(); - - let server = MockServer::start().await; - - let market = MarketApi::new( - (server.uri() + "/platform/") - .parse() - .expect("Wrong Market url"), - &DEVELOPMENT, - logger.clone(), - ) - .expect("should create market instance"); - - let categories: [&str; 3] = ["IAB3", "IAB13-7", "IAB5"]; - let rules = get_mock_rules(&categories); - let campaign = mock_campaign(&rules); - - let platform_ad_units = AdUnitsResponse(campaign.ad_units.clone()); - let mock_slot = get_test_ad_slot(&rules, &categories); - - Mock::given(method("GET")) - .and(path("/platform/units")) - .respond_with(ResponseTemplate::new(200).set_body_json(&platform_ad_units)) - .mount(&server) - .await; - - let request = Request::post("/units-for-slot") - .header(USER_AGENT, TEST_USER_AGENT) - .header(CLOUDFLARE_IPCOUNTY_HEADER.clone(), TEST_CLOUDFLARE_IPCOUNTY) - .body( - serde_json::to_vec(&RequestBody { - ad_slot: mock_slot, - deposit_assets: Some(vec![campaign.channel.token].into_iter().collect()), - }) - .expect("Should serialize"), - ) - .unwrap(); - - let actual_response = post_units_for_slot(request, &app) - .await - .expect("call shouldn't fail with provided data"); - - assert_eq!(StatusCode::OK, actual_response.status()); - - let units_for_slot: UnitsForSlotResponse = - serde_json::from_slice(&hyper::body::to_bytes(actual_response).await.unwrap()) - .expect("Should deserialize"); - let units_for_slot_pretty = - serde_json::to_string_pretty(&units_for_slot).expect("should turn to string"); - - println!("{}", units_for_slot_pretty); -}