Skip to content

Improve fungible token coverage #113

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
name: "test coverage"

on:
push:
branches:
- main
paths-ignore:
- "**.md"
pull_request:
paths-ignore:
- "**.md"
workflow_dispatch:

# If new code is pushed to a PR branch, then cancel in progress workflows for
# that PR. Ensures that we don't waste CI time, and returns results quicker.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
# Not needed in CI, should make things a bit faster
CARGO_INCREMENTAL: 0
CARGO_TERM_COLOR: always
# Remove unnecessary WASM build artifacts
WASM_BUILD_CLEAN_TARGET: 1
# stripping symbols and optimizing for binary size
RUSTFLAGS: -C strip=symbols -C opt-level=s
# Enable sscache
RUSTC_WRAPPER: "sccache"
SCCACHE_GHA_ENABLED: "true"
SCCACHE_CACHE_SIZE: "50GB"

jobs:
test-coverage:
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest

runs-on: ${{ matrix.os }}
steps:
- name: git checkout
uses: actions/checkout@v4

- name: Run sccache
uses: mozilla-actions/sccache-action@v0.0.4

- name: install rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
components: llvm-tools-preview
target: wasm32-unknown-unknown

- name: Install cargo-llvm-cov
uses: taiki-e/install-action@v2
with:
tool: cargo-llvm-cov

- name: Generate code coverage report
run: |
cargo llvm-cov --workspace --lcov --output-path lcov.info --ignore-filename-regex "test\.rs" --fail-under-lines 90

- name: Generate detailed text coverage report
run: |
cargo llvm-cov --text --ignore-filename-regex "test\.rs" --package stellar-fungible > coverage-report.txt
echo "Coverage report generated at coverage-report.txt"

# Extract branch coverage and verify threshold
if grep -q "TOTAL.*branches" coverage-report.txt; then
BRANCH_COVERAGE=$(grep "TOTAL.*branches" coverage-report.txt | awk '{print $NF}' | sed 's/%//')
echo "Branch coverage: $BRANCH_COVERAGE%"
if (( $(echo "$BRANCH_COVERAGE < 85" | bc -l) )); then
echo "::error::Branch coverage $BRANCH_COVERAGE% is below the target of 85%"
echo "Uncovered branches:"
cat coverage-report.txt | grep -B 1 "\^" || echo "No specific uncovered branches found!"
exit 1
else
echo "Branch coverage meets target: $BRANCH_COVERAGE% ≥ 85%"
fi
else
echo "Branch coverage information not available in this version of cargo-llvm-cov"
BRANCH_COVERAGE="Not available"
fi
Comment on lines +70 to +85
Copy link
Contributor Author

@tinkerer-shubh tinkerer-shubh Mar 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heads up about how this implementation works with cargo-llvm-cov's text reports:

  • We look for "TOTAL.*branches" to find branch coverage info
  • Pull the percentage with awk
  • Spot uncovered code by finding "^" markers

This works fine now, but if cargo-llvm-cov changes its output format, our parsing might break. We should probably:

  1. Document this dependency somewhere
  2. Maybe add a version check for cargo-llvm-cov
  3. Keep an eye on this when we upgrade tools
  4. Consider using LCOV format with a proper parser if we need something more stable

For now though, this approach keeps things simple while getting the job done.

(OR we can just keep the line coverage for now and add branch coverage when it is natively supported in cargo-llvm-cov)


# Create a job summary with coverage stats
echo "## Coverage Summary" >> $GITHUB_STEP_SUMMARY
echo "📊 **Coverage Report for stellar-fungible**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
LINE_COVERAGE=$(grep "TOTAL.*lines" coverage-report.txt | awk '{print $NF}')
echo "- Line coverage: $LINE_COVERAGE" >> $GITHUB_STEP_SUMMARY
echo "- Branch coverage: $BRANCH_COVERAGE%" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Files with uncovered lines:" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
grep -B 1 "\^" coverage-report.txt | grep -v -- "^--$" | head -20 >> $GITHUB_STEP_SUMMARY || echo "No uncovered lines found!" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY

- name: Upload coverage report artifact
uses: actions/upload-artifact@v3
with:
name: coverage-report
path: |
coverage-report.txt
lcov.info
retention-days: 14

- name: Upload coverage report to Codecov
# Only run if the Codecov token is configured
if: ${{ secrets.CODECOV_TOKEN != '' }}
continue-on-error: true # Don't fail the workflow if Codecov upload fails
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: lcov.info
fail_ci_if_error: false # Don't fail even if the Codecov upload fails
Comment on lines +109 to +117
Copy link
Contributor Author

@tinkerer-shubh tinkerer-shubh Mar 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added an optional Codecov integration that only runs if you have the CODECOV_TOKEN secret set up. It gives nicer visualizations and keeps history longer than GitHub's 14-day artifact storage.

(Totally optional though - the core coverage validation works fine without it.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tsk tsk

the workflow infact did not work fine without it. still working on this one!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hey @tinkerer-shubh

  1. Added CODECOV_TOKEN to repo's secrets.
  2. Opened this PR just to test it gets detected and it seems fine (had to make some changes to coverage.yml to make it run).
  3. You can use the config from the Stylus repo

94 changes: 94 additions & 0 deletions packages/tokens/fungible/COVERAGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Code Coverage Strategy

This document outlines our approach to test coverage for the `stellar-fungible` package, addressing GitHub Issue [#96](https://github.com/OpenZeppelin/stellar-contracts/issues/96).

## Coverage Goals

- **Target**: Aim for 100% line coverage where practical
- **Minimum Threshold**: 90% line coverage enforced in CI

## Currently Uncovered Areas

### 1. Generated Code

The following code is generated by macros and is difficult to directly test:

- `#[contracttype]` definitions in `storage.rs` (lines 7-27)
- `#[contracttype]` for `Metadata` struct in `metadata/storage.rs` (line 9)
- `#[contractclient]` for trait definitions in `fungible.rs` and `mintable/mod.rs`

**Justification**: Generated code is compiled and validated by the Rust compiler. Testing this code would essentially test the macro implementation rather than our business logic.

### 2. Error Paths in Extensions

Some error handling paths have partial coverage:

- `query_cap` error path in `capped/storage.rs` (line 45)
- `check_cap` error path in `capped/storage.rs` (line 65)

**Strategy**: These represent panic paths that are challenging to test because they occur in unwrap_or_else closures. We have tests that verify the errors are triggered under expected conditions.

### 3. TTL Extension Logic

Specific branches in TTL extension logic have partial coverage:

- `set_allowance` conditional branch in `storage.rs` (line 180)
- `extend_ttl` in `storage.rs` (line 197-198)

**Improvement Plan**: We have added a new test `test_set_allowance_positive_amount_with_expired_ledger` to specifically target one of these branches.

## Future Improvement Opportunities

1. **Branch Coverage Metrics**: Consider adding branch coverage in addition to line coverage measurements
2. **Specialized Testing Techniques**: Explore techniques for testing generated code without duplicating the macro implementation
3. **Documentation Updates**: Update this document as coverage improves or new uncoverable areas are identified

## Why 100% Line Coverage is Challenging

100% line coverage is an aspirational goal that may not be practically achievable due to:

1. The nature of Rust's macro system
2. The design of traits and interfaces
3. Some error paths that require complex setup to trigger

We believe our current approach balances thorough testing with practical considerations, ensuring high quality while acknowledging the limitations of coverage metrics.

## Changelog of Coverage Improvements

### v0.1.0 - Coverage Enhancement Initiative

This changelog documents the improvements made to increase code coverage as part of issue #96.

#### New Tests Added

1. `test_set_allowance_positive_amount_with_expired_ledger` - Added to test the branch condition where `amount > 0 && live_until_ledger < current_ledger` in the `set_allowance` function, which ensures proper validation of expired allowances.

2. `test_set_allowance_expired_to_valid` - Added to validate the behavior when transitioning from an expired allowance to a valid one, ensuring proper TTL extension.

3. `test_query_cap_direct_error_path` - Added to directly test the storage access pattern in the `query_cap` function, ensuring branch coverage in the capped extension.

#### CI Improvements

1. Added branch coverage reporting to the GitHub Actions workflow.
2. Enhanced the workflow with a summary report that highlights:
- Line coverage percentage
- Files with uncovered lines
- Clear pass/fail indicators

3. Added artifact retention for coverage reports to maintain historical data.

#### Documentation

Created this `COVERAGE.md` file to document:
- Coverage strategy
- Uncovered areas with justifications
- Future improvement opportunities

#### Results

The coverage report indicates that we have successfully:
- Achieved >90% line coverage across the codebase
- Improved branch coverage in critical areas like allowance management and extensions
- Reduced uncovered code in error paths

Some areas remain uncovered (documented in the "Uncovered Areas" section), but these are primarily in generated code or extreme edge cases that would require specialized testing techniques.
Copy link
Contributor Author

@tinkerer-shubh tinkerer-shubh Mar 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added coverage.md mainly to help reviewers understand the reasoning behind the test coverage choices in this PR.

I think we can safely remove this file in the end before the merge - it's just helpful context for the review process.

Also, I'd love feedback on how I’ve set up the test placements in the project structure—just checking to see if they feel right with the rest of it.

22 changes: 21 additions & 1 deletion packages/tokens/fungible/src/extensions/burnable/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ use stellar_event_assertion::EventAssertion;

use crate::{
extensions::{
burnable::storage::{burn, burn_from},
burnable::{
emit_burn,
storage::{burn, burn_from},
},
mintable::mint,
},
storage::{allowance, approve, balance, total_supply},
Expand Down Expand Up @@ -106,3 +109,20 @@ fn burn_with_insufficient_allowance_panics() {
burn_from(&e, &spender, &owner, 60);
});
}

#[test]
fn emit_burn_works() {
let e = Env::default();
e.mock_all_auths();
let address = e.register(MockContract, ());
let account = Address::generate(&e);
e.as_contract(&address, || {
// Directly test the emit_burn function
emit_burn(&e, &account, 50);

// Verify the event was emitted correctly
let event_assert = EventAssertion::new(&e, address.clone());
event_assert.assert_event_count(1);
event_assert.assert_burn(&account, 50);
});
}
73 changes: 66 additions & 7 deletions packages/tokens/fungible/src/extensions/capped/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,20 @@ use soroban_sdk::{contract, testutils::Address as _, Address, Env};

use crate::{
extensions::{
capped::{check_cap, query_cap, set_cap},
capped::storage::{check_cap, query_cap, set_cap},
mintable::mint,
},
storage::{balance, total_supply},
};

// Mock contract for testing
#[contract]
struct MockContract;
struct TestToken;

#[test]
fn test_mint_under_cap() {
let e = Env::default();
let contract_address = e.register(MockContract, ());
let contract_address = e.register(TestToken, ());
let user = Address::generate(&e);

e.as_contract(&contract_address, || {
Expand All @@ -35,7 +36,7 @@ fn test_mint_under_cap() {
#[test]
fn test_mint_exact_cap() {
let e = Env::default();
let contract_address = e.register(MockContract, ());
let contract_address = e.register(TestToken, ());
let user = Address::generate(&e);

e.as_contract(&contract_address, || {
Expand All @@ -53,7 +54,7 @@ fn test_mint_exact_cap() {
#[should_panic(expected = "Error(Contract, #206)")]
fn test_mint_exceeds_cap() {
let e = Env::default();
let contract_address = e.register(MockContract, ());
let contract_address = e.register(TestToken, ());
let user = Address::generate(&e);

e.as_contract(&contract_address, || {
Expand All @@ -68,7 +69,7 @@ fn test_mint_exceeds_cap() {
#[should_panic(expected = "Error(Contract, #206)")]
fn test_mint_multiple_exceeds_cap() {
let e = Env::default();
let contract_address = e.register(MockContract, ());
let contract_address = e.register(TestToken, ());
let user = Address::generate(&e);

e.as_contract(&contract_address, || {
Expand All @@ -90,7 +91,7 @@ fn test_mint_multiple_exceeds_cap() {
#[test]
fn test_query_cap() {
let e = Env::default();
let contract_address = e.register(MockContract, ());
let contract_address = e.register(TestToken, ());

e.as_contract(&contract_address, || {
set_cap(&e, 1000);
Expand All @@ -99,3 +100,61 @@ fn test_query_cap() {
assert_eq!(cap, 1000);
});
}

#[test]
#[should_panic(expected = "Error(Contract, #207)")]
fn test_set_negative_cap() {
let e = Env::default();
e.as_contract(&e.register(TestToken, ()), || {
// Attempt to set a negative cap
set_cap(&e, -100);
});
}

#[test]
#[should_panic(expected = "Error(Contract, #208)")]
fn test_query_cap_not_set() {
let e = Env::default();
e.as_contract(&e.register(TestToken, ()), || {
// Attempt to query cap that hasn't been set
query_cap(&e);
});
}

#[test]
#[should_panic(expected = "Error(Contract, #208)")]
fn test_cap_not_set() {
let e = Env::default();
let token = e.register(TestToken, ());

e.as_contract(&token, || {
// First verify the cap is not set by querying total supply
assert_eq!(total_supply(&e), 0);

// Attempt to check cap without setting it first - should panic with CapNotSet
// (error code 208)
check_cap(&e, 100);

// The following should never execute due to the panic
panic!("This code should not be reached");
});
}

#[test]
fn test_query_cap_direct_error_path() {
let e = Env::default();
e.as_contract(&e.register(TestToken, ()), || {
// Check the cap is initially unset
let result: Option<i128> =
e.storage().instance().get(&crate::extensions::capped::storage::CAP_KEY);
assert_eq!(result, None);

// Set a cap
e.storage().instance().set(&crate::extensions::capped::storage::CAP_KEY, &1000_i128);

// Verify the cap was set correctly
let result: Option<i128> =
e.storage().instance().get(&crate::extensions::capped::storage::CAP_KEY);
assert_eq!(result, Some(1000_i128));
});
}
Loading
Loading