From e79ed86bdc1779441d76b6e46826144a1107c220 Mon Sep 17 00:00:00 2001 From: 0xripleys <105607696+0xripleys@users.noreply.github.com> Date: Fri, 22 Jul 2022 13:37:59 -0400 Subject: [PATCH] test coverage github action (#94) * test coverage github action * move actions to another job * moving stuff around * grr * artifact error * try cobertura format with codecov * add decimal test * formatting * tests for decimal * rate tests * format * tests for calculate liquidation --- .github/workflows/pull-request.yml | 40 ++++++ ci/install-program-deps.sh | 1 + ci/rust-version.sh | 6 +- coverage.sh | 18 ++- token-lending/program/src/math/decimal.rs | 66 +++++++++ token-lending/program/src/math/rate.rs | 46 +++++++ token-lending/program/src/state/reserve.rs | 151 ++++++++++++++++++++- 7 files changed, 314 insertions(+), 14 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index f9ef786e5d6..ce8e5385519 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -124,3 +124,43 @@ jobs: - name: Build and test run: ./ci/cargo-build-test.sh + + cargo-coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set env vars + run: | + source ci/rust-version.sh nightly + echo "RUST_NIGHTLY=$rust_nightly" >> $GITHUB_ENV + source ci/solana-version.sh + echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV + + - uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_NIGHTLY }} + override: true + profile: minimal + + - uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + # target # Removed due to build dependency caching conflicts + key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_NIGHTLY }} + + - name: Install dependencies + run: | + ./ci/install-build-deps.sh + ./ci/install-program-deps.sh + echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH + + - name: run test coverage + run: ./coverage.sh token-lending + + - name: Codecov + uses: codecov/codecov-action@v3.1.0 + with: + file: target/cov/cobertura.xml diff --git a/ci/install-program-deps.sh b/ci/install-program-deps.sh index 8c076bf8b8e..8607fd82ce8 100755 --- a/ci/install-program-deps.sh +++ b/ci/install-program-deps.sh @@ -10,5 +10,6 @@ set -x cargo --version cargo install rustfilt || true cargo install honggfuzz --version=0.5.52 --force || true +cargo install grcov --force cargo +"$rust_stable" build-bpf --version diff --git a/ci/rust-version.sh b/ci/rust-version.sh index 150ef983fd5..7f25c5243f4 100644 --- a/ci/rust-version.sh +++ b/ci/rust-version.sh @@ -51,9 +51,9 @@ export rust_nightly_docker_image=solanalabs/rust-nightly:"$nightly_version" stable) rustup_install "$rust_stable" ;; - # nightly) - # rustup_install "$rust_nightly" - # ;; + nightly) + rustup_install "$rust_nightly" + ;; all) rustup_install "$rust_stable" rustup_install "$rust_nightly" diff --git a/coverage.sh b/coverage.sh index e21e0fc92fb..25a8b723b44 100755 --- a/coverage.sh +++ b/coverage.sh @@ -3,6 +3,8 @@ # Runs all program tests and builds a code coverage report # +source ci/rust-version.sh nightly # get $rust_nightly env variable + set -e cd "$(dirname "$0")" @@ -11,11 +13,6 @@ if ! which grcov; then exit 1 fi -if [[ ! "$(grcov --version)" =~ "0.6.1" ]]; then - echo Error: Required grcov version not installed - exit 1 -fi - : "${CI_COMMIT:=local}" reportName="lcov-${CI_COMMIT:0:9}" @@ -57,7 +54,7 @@ for program in ${programs[@]}; do ( set -ex cd $program - cargo +nightly test --target-dir $here/target/cov + cargo +"$rust_nightly" test --features test-bpf --target-dir $here/target/cov ) done @@ -81,14 +78,15 @@ find target/cov -type f -name '*.gcda' -newer target/cov/before-test ! -newer ta ( set -x - grcov target/cov/tmp --llvm -t html -o target/cov/$reportName - grcov target/cov/tmp --llvm -t lcov -o target/cov/lcov.info + grcov target/cov/tmp -t html -o target/cov/$reportName + grcov target/cov/tmp -t lcov -o target/cov/lcov.info + grcov target/cov/tmp -t cobertura -o target/cov/cobertura.xml cd target/cov tar zcf report.tar.gz $reportName ) -ls -l target/cov/$reportName/index.html -ln -sfT $reportName target/cov/LATEST +ls -l target/cov/ +ln -sf $reportName target/cov/LATEST exit $test_status diff --git a/token-lending/program/src/math/decimal.rs b/token-lending/program/src/math/decimal.rs index 8cc3fc8baa9..861c64322fd 100644 --- a/token-lending/program/src/math/decimal.rs +++ b/token-lending/program/src/math/decimal.rs @@ -213,4 +213,70 @@ mod test { fn test_scaler() { assert_eq!(U192::exp10(SCALE), Decimal::wad()); } + + #[test] + fn test_u192() { + let one = U192::from(1); + assert_eq!(one.0, [1u64, 0, 0]); + + let wad = Decimal::wad(); + assert_eq!(wad.0, [WAD, 0, 0]); + + let hundred = Decimal::from(100u64); + // 2^64 * 5 + 7766279631452241920 = 1e20 + assert_eq!(hundred.0 .0, [7766279631452241920, 5, 0]); + } + + #[test] + fn test_from_percent() { + let left = Decimal::from_percent(20); + let right = Decimal::from(20u64).try_div(Decimal::from(100u64)).unwrap(); + + assert_eq!(left, right); + } + + #[test] + fn test_to_scaled_val() { + assert_eq!( + Decimal(U192::from(u128::MAX)).to_scaled_val().unwrap(), + u128::MAX + ); + + assert_eq!( + Decimal(U192::from(u128::MAX)) + .try_add(Decimal(U192::from(1))) + .unwrap() + .to_scaled_val(), + Err(ProgramError::from(LendingError::MathOverflow)) + ); + } + + #[test] + fn test_round_floor_ceil_u64() { + let mut val = Decimal::one(); + assert_eq!(val.try_round_u64().unwrap(), 1); + assert_eq!(val.try_floor_u64().unwrap(), 1); + assert_eq!(val.try_ceil_u64().unwrap(), 1); + + val = val + .try_add(Decimal::from_scaled_val(HALF_WAD as u128 - 1)) + .unwrap(); + assert_eq!(val.try_round_u64().unwrap(), 1); + assert_eq!(val.try_floor_u64().unwrap(), 1); + assert_eq!(val.try_ceil_u64().unwrap(), 2); + + val = val.try_add(Decimal::from_scaled_val(1)).unwrap(); + assert_eq!(val.try_round_u64().unwrap(), 2); + assert_eq!(val.try_floor_u64().unwrap(), 1); + assert_eq!(val.try_ceil_u64().unwrap(), 2); + } + + #[test] + fn test_display() { + assert_eq!(Decimal::from(1u64).to_string(), "1.000000000000000000"); + assert_eq!( + Decimal::from_scaled_val(1u128).to_string(), + "0.000000000000000001" + ); + } } diff --git a/token-lending/program/src/math/rate.rs b/token-lending/program/src/math/rate.rs index c404c8ff2b8..0190b5edad2 100644 --- a/token-lending/program/src/math/rate.rs +++ b/token-lending/program/src/math/rate.rs @@ -176,9 +176,55 @@ impl TryMul for Rate { #[cfg(test)] mod test { use super::*; + use std::convert::TryInto; + + #[test] + fn test_scaled_val() { + assert_eq!(Rate::from_percent(50).to_scaled_val(), HALF_WAD as u128); + } #[test] fn checked_pow() { assert_eq!(Rate::one(), Rate::one().try_pow(u64::MAX).unwrap()); + assert_eq!( + Rate::from_percent(200).try_pow(7).unwrap(), + Decimal::from(128u64).try_into().unwrap() + ); + } + + #[test] + fn test_display() { + assert_eq!( + Rate::one().try_div(3u64).unwrap().to_string(), + "0.333333333333333333" + ); + } + + #[test] + fn test_basic_arithmetic() { + assert_eq!( + Rate::one().try_add(Rate::one()).unwrap(), + Rate::from_scaled_val(2 * WAD) + ); + + assert_eq!(Rate::one().try_sub(Rate::one()).unwrap(), Rate::zero()); + + assert_eq!( + Rate::from_percent(240) + .try_mul(Rate::from_percent(50)) + .unwrap(), + Rate::from_percent(120) + ); + assert_eq!( + Rate::from_percent(240).try_mul(10).unwrap(), + Decimal::from(24u64).try_into().unwrap() + ); + + assert_eq!( + Rate::from_percent(240) + .try_div(Rate::from_percent(60)) + .unwrap(), + Rate::from_scaled_val(4 * WAD) + ); } } diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index 3f9dd97bdda..322e2593b61 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -380,7 +380,7 @@ pub struct CalculateRepayResult { } /// Calculate liquidation result -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct CalculateLiquidationResult { /// Amount of liquidity that is settled from the obligation. It includes /// the amount of loan that was defaulted if collateral is depleted. @@ -1069,6 +1069,7 @@ mod test { use crate::math::{PERCENT_SCALER, WAD}; use proptest::prelude::*; use std::cmp::Ordering; + use std::default::Default; const MAX_LIQUIDITY: u64 = u64::MAX / 5; @@ -1443,4 +1444,152 @@ mod test { assert_eq!(total_fee, 10); // 1% of 1000 assert_eq!(host_fee, 0); // 0 host fee } + + #[derive(Debug, Clone)] + struct LiquidationTestCase { + deposit_amount: u64, + deposit_market_value: u64, + borrow_amount: u64, + borrow_market_value: u64, + liquidation_result: CalculateLiquidationResult, + } + + fn calculate_liquidation_test_cases() -> impl Strategy { + let close_factor: Decimal = Rate::from_percent(LIQUIDATION_CLOSE_FACTOR) + .try_into() + .unwrap(); + let liquidation_bonus: Decimal = Rate::from_percent(5) + .try_add(Rate::one()) + .unwrap() + .try_into() + .unwrap(); + + prop_oneof![ + // collateral market value > liquidation value + Just(LiquidationTestCase { + deposit_amount: 1000, + deposit_market_value: 100, + borrow_amount: 800, + borrow_market_value: 80, + liquidation_result: CalculateLiquidationResult { + settle_amount: close_factor.try_mul(Decimal::from(800u64)).unwrap(), + repay_amount: close_factor + .try_mul(Decimal::from(800u64)) + .unwrap() + .try_ceil_u64() + .unwrap(), + withdraw_amount: close_factor + .try_mul(liquidation_bonus) + .unwrap() + .try_mul(Decimal::from(800u64)) + .unwrap() + .try_floor_u64() + .unwrap(), + }, + }), + // collateral market value == liquidation_value + Just(LiquidationTestCase { + borrow_amount: 8000, + borrow_market_value: 8000, + deposit_amount: (8000 * LIQUIDATION_CLOSE_FACTOR as u64) * 105 / 10000, + deposit_market_value: (8000 * LIQUIDATION_CLOSE_FACTOR as u64) * 105 / 10000, + + liquidation_result: CalculateLiquidationResult { + settle_amount: Decimal::from((8000 * LIQUIDATION_CLOSE_FACTOR as u64) / 100), + repay_amount: (8000 * LIQUIDATION_CLOSE_FACTOR as u64) / 100, + withdraw_amount: (8000 * LIQUIDATION_CLOSE_FACTOR as u64) * 105 / 10000, + }, + }), + // collateral market value < liquidation_value + Just(LiquidationTestCase { + borrow_amount: 8000, + borrow_market_value: 8000, + + // half of liquidation value + deposit_amount: (8000 * LIQUIDATION_CLOSE_FACTOR as u64) * 105 / 10000 / 2, + deposit_market_value: (8000 * LIQUIDATION_CLOSE_FACTOR as u64) * 105 / 10000 / 2, + + liquidation_result: CalculateLiquidationResult { + settle_amount: Decimal::from( + (8000 * LIQUIDATION_CLOSE_FACTOR as u64) / 100 / 2 + ), + repay_amount: (8000 * LIQUIDATION_CLOSE_FACTOR as u64) / 100 / 2, + withdraw_amount: (8000 * LIQUIDATION_CLOSE_FACTOR as u64) * 105 / 10000 / 2, + }, + }), + // dust ObligationLiquidity where collateral market value > liquidation value + Just(LiquidationTestCase { + borrow_amount: 1, + borrow_market_value: 1000, + deposit_amount: 1000, + deposit_market_value: 2100, + + liquidation_result: CalculateLiquidationResult { + settle_amount: Decimal::from(1u64), + repay_amount: 1, + withdraw_amount: 500, + }, + }), + // dust ObligationLiquidity where collateral market value == liquidation value + Just(LiquidationTestCase { + borrow_amount: 1, + borrow_market_value: 1000, + deposit_amount: 1000, + deposit_market_value: 1050, + + liquidation_result: CalculateLiquidationResult { + settle_amount: Decimal::from(1u64), + repay_amount: 1, + withdraw_amount: 1000, + }, + }), + // dust ObligationLiquidity where collateral market value < liquidation value + Just(LiquidationTestCase { + borrow_amount: 1, + borrow_market_value: 1000, + deposit_amount: 1000, + deposit_market_value: 1000, + + liquidation_result: CalculateLiquidationResult { + settle_amount: Decimal::from(1u64), + repay_amount: 1, + withdraw_amount: 1000, + }, + }), + ] + } + + proptest! { + #[test] + fn calculate_liquidation(test_case in calculate_liquidation_test_cases()) { + let reserve = Reserve { + config: ReserveConfig { + liquidation_bonus: 5, + ..ReserveConfig::default() + }, + ..Reserve::default() + }; + + let obligation = Obligation { + deposits: vec![ObligationCollateral { + deposit_reserve: Pubkey::new_unique(), + deposited_amount: test_case.deposit_amount, + market_value: Decimal::from(test_case.deposit_market_value), + }], + borrows: vec![ObligationLiquidity { + borrow_reserve: Pubkey::new_unique(), + cumulative_borrow_rate_wads: Decimal::one(), + borrowed_amount_wads: Decimal::from(test_case.borrow_amount), + market_value: Decimal::from(test_case.borrow_market_value), + }], + borrowed_value: Decimal::from(test_case.borrow_market_value), + ..Obligation::default() + }; + + assert_eq!( + reserve.calculate_liquidation( + u64::MAX, &obligation, &obligation.borrows[0], &obligation.deposits[0]).unwrap(), + test_case.liquidation_result); + } + } }