Skip to content

Commit

Permalink
feat: #[test(should_revert)] attribute to indicate a unit test shou…
Browse files Browse the repository at this point in the history
…ld revert (FuelLabs#3490)

## Summary

This PR adds `#[test(should_revert)]` tests to forc-test, enabling users
to write reverting cases in their unit tests.
Example:

```rust
script;    

fn main() {}  
fn my_func() {}  
  
#[test(should_revert)]  
fn my_test_func() {  
  my_func();  
  revert(0)  
}
```

## Implementation Details

Since FuelLabs#3509 is merged, this PR only adds the required section to
`forc-test` so that it can detect if the given entry point's
corresponding function declarations has `#[test(should_revert)]`
attribute. If that is the case the passing condition is selected as
`TestPassCondition::ShouldRevert` otherwise it defaults to
`TestPassCondition::ShouldNotRevert`.

I also added a safety check for erroneous declarations such as
`#[test(foo)]`. Since we just have `should revert` tests right now, I am
checking if there is an argument provided with the test attirbute. If
there is none, test is considered to be
`TestPassCondition::ShouldNotRevert`, tl-dr; default is non reverting
tests. If there is an argument and it is `should_revert`, test is
considered to be `TestPassCondition::ShouldRevert` and finally if there
is an argument but it is not `should_revert` an error is produced.

A simple should revert test with `revert(0)` added to unit tests as
well.
  • Loading branch information
kayagokalp authored Dec 8, 2022
1 parent 80aa366 commit 716b34f
Show file tree
Hide file tree
Showing 8 changed files with 95 additions and 8 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

14 changes: 10 additions & 4 deletions docs/src/testing/unit-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ fn test_meaning_of_life() {

Each test function is ran as if it were the entry point for a
[script](../sway-program-types/scripts.md). Tests "pass" if they return
successfully, and "fail" if they revert.
successfully, and "fail" if they revert or vice versa while [testing failure](#testing-failure).

## Building and Running Tests

Expand All @@ -40,10 +40,16 @@ the options available for `forc test`.

## Testing Failure

***Coming Soon***
Forc supports testing failing cases for test functions declared with `#[test(should_revert)]`. For example:

```sway
#[test(should_revert)]
fn test_meaning_of_life() {
assert(6 * 6 == 42);
}
```

*Track progress on `#[test(should_revert)]`
[here](https://github.com/FuelLabs/sway/issues/3260).*
Tests with `#[test(should_revert)]` considered to be passing if they are reverting.

## Calling Contracts

Expand Down
1 change: 1 addition & 0 deletions forc-test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ fuel-tx = { version = "0.23", features = ["builder"] }
fuel-vm = { version = "0.22", features = ["random"] }
rand = "0.8"
sway-core = { version = "0.31.3", path = "../sway-core" }
sway-types = { version = "0.31.3", path = "../sway-types/" }
58 changes: 54 additions & 4 deletions forc-test/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
use std::collections::HashSet;

use forc_pkg as pkg;
use fuel_tx as tx;
use fuel_vm::{self as vm, prelude::Opcode};
use rand::{Rng, SeedableRng};
use sway_core::{language::ty::TyFunctionDeclaration, transform::AttributeKind};
use sway_types::Spanned;

/// The result of a `forc test` invocation.
#[derive(Debug)]
Expand All @@ -28,6 +32,15 @@ pub struct TestResult {
pub state: vm::state::ProgramState,
/// The time taken for the test to execute.
pub duration: std::time::Duration,
/// The required state of the VM for this test to pass.
pub condition: TestPassCondition,
}

/// The possible conditions for a test result to be considered "passing".
#[derive(Debug)]
pub enum TestPassCondition {
ShouldRevert,
ShouldNotRevert,
}

/// A package that has been built, ready for test execution.
Expand Down Expand Up @@ -76,7 +89,14 @@ impl Opts {
impl TestResult {
/// Whether or not the test passed.
pub fn passed(&self) -> bool {
!matches!(self.state, vm::state::ProgramState::Revert(_))
match &self.condition {
TestPassCondition::ShouldRevert => {
matches!(self.state, vm::state::ProgramState::Revert(_))
}
TestPassCondition::ShouldNotRevert => {
!matches!(self.state, vm::state::ProgramState::Revert(_))
}
}
}
}

Expand Down Expand Up @@ -106,6 +126,26 @@ pub fn build(opts: Opts) -> anyhow::Result<BuiltTests> {
Ok(BuiltTests { built_pkg })
}

fn test_pass_condition(
test_function_decl: &TyFunctionDeclaration,
) -> anyhow::Result<TestPassCondition> {
let test_args: HashSet<String> = test_function_decl
.attributes
.get(&AttributeKind::Test)
.expect("test declaration is missing test attribute")
.iter()
.flat_map(|attr| attr.args.iter().map(|arg| arg.to_string()))
.collect();
let test_name = &test_function_decl.name;
if test_args.is_empty() {
Ok(TestPassCondition::ShouldNotRevert)
} else if test_args.get("should_revert").is_some() {
Ok(TestPassCondition::ShouldRevert)
} else {
anyhow::bail!("Invalid test argument(s) for test: {test_name}.")
}
}

/// Build the the given package and run its tests, returning the results.
fn run_tests(built: BuiltTests) -> anyhow::Result<Tested> {
let BuiltTests { built_pkg } = built;
Expand All @@ -120,13 +160,23 @@ fn run_tests(built: BuiltTests) -> anyhow::Result<Tested> {
let offset = u32::try_from(entry.imm).expect("test instruction offset out of range");
let name = entry.fn_name.clone();
let (state, duration) = exec_test(&built_pkg.bytecode, offset);
TestResult {
let test_decl_id = entry
.test_decl_id
.clone()
.expect("test entry point is missing declaration id");
let test_decl_span = test_decl_id.span();
let test_function_decl =
sway_core::declaration_engine::de_get_function(test_decl_id, &test_decl_span)
.expect("declaration engine is missing function declaration for test");
let condition = test_pass_condition(&test_function_decl)?;
Ok(TestResult {
name,
state,
duration,
}
condition,
})
})
.collect();
.collect::<anyhow::Result<_>>()?;

let built = built_pkg;
let tested_pkg = TestedPackage { built, tests };
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
out
target
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[[package]]
name = 'core'
source = 'path+from-root-98FF531C5C738A40'

[[package]]
name = 'should_revert'
source = 'member'
dependencies = ['std']

[[package]]
name = 'std'
source = 'path+from-root-98FF531C5C738A40'
dependencies = ['core']
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[project]
authors = ["Fuel Labs <[email protected]>"]
entry = "lib.sw"
license = "Apache-2.0"
name = "should_revert"

[dependencies]
std = { path = "../../../../../../../sway-lib-std" }
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
library should_revert;

#[test(should_revert)]
fn should_revert_test() {
assert(0 == 1)
}

0 comments on commit 716b34f

Please sign in to comment.