Skip to content

Commit

Permalink
Merge pull request #57 from str4d/53-test-harness
Browse files Browse the repository at this point in the history
Test harness
  • Loading branch information
dcoles authored Apr 25, 2023
2 parents d5a3d14 + 33865cc commit bd2139e
Show file tree
Hide file tree
Showing 19 changed files with 924 additions and 2 deletions.
7 changes: 6 additions & 1 deletion .github/workflows/_build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,16 @@ jobs:
working-directory: ${{ inputs.target }}
steps:
- uses: actions/checkout@v3
- run: sudo apt install libudev-dev
- name: Rustup
run: rustup +nightly target add thumbv7em-none-eabihf
- name: Build
run: cargo +nightly build --release --verbose
- name: Build examples
run: cargo +nightly build --examples --release --verbose
- name: Run tests
run: cargo +nightly test --release --verbose
run: |
cargo +nightly test --release --verbose 2>&1 | tee stderr.txt
- name: Check that tests failed for the expected reason
run: |
cat stderr.txt | grep -q "Error: unable to find Flipper Zero"
1 change: 1 addition & 0 deletions crates/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[target.thumbv7em-none-eabihf]
runner = "python3 ../cargo-runner.py"
rustflags = [
# CPU is Cortex-M4 (STM32WB55)
"-C", "target-cpu=cortex-m4",
Expand Down
2 changes: 2 additions & 0 deletions crates/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ members = [
"flipperzero",
"sys",
"rt",
"test",
]
resolver = "2"

Expand All @@ -20,6 +21,7 @@ license = "MIT"
flipperzero-sys = { path = "sys", version = "0.8.0" }
flipperzero-rt = { path = "rt", version = "0.8.0" }
flipperzero-alloc = { path = "alloc", version = "0.8.0" }
flipperzero-test = { path = "test", version = "0.1.0" }
ufmt = "0.2.0"

[profile.dev]
Expand Down
42 changes: 42 additions & 0 deletions crates/cargo-runner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/usr/bin/env python3
# Helper script for running binaries on a connected Flipper Zero.

import argparse
import os
import sys
from pathlib import Path
from subprocess import run

TOOLS_PATH = '../tools'


def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument('binary', type=Path)
parser.add_argument('arguments', nargs=argparse.REMAINDER)
return parser.parse_args()


def main():
args = parse_args()

# Run the given FAP binary on a connected Flipper Zero.
result = run(
[
'cargo',
'run',
'--quiet',
'--release',
'--bin',
'run-fap',
'--',
os.fspath(args.binary),
] + args.arguments,
cwd=os.path.join(os.path.dirname(__file__), TOOLS_PATH),
)
if result.returncode:
sys.exit(result.returncode)


if __name__ == '__main__':
main()
7 changes: 6 additions & 1 deletion crates/flipperzero/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ all-features = true

[lib]
bench = false
test = false
harness = false

[dependencies]
flipperzero-sys.workspace = true
flipperzero-test.workspace = true
ufmt.workspace = true

[dev-dependencies]
Expand All @@ -32,6 +33,10 @@ flipperzero-rt.workspace = true
# enables features requiring an allocator
alloc = []

[[test]]
name = "dolphin"
harness = false

[[example]]
name = "dialog"
required-features = ["alloc"]
42 changes: 42 additions & 0 deletions crates/flipperzero/src/furi/message_queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,45 @@ impl<M: Sized> Drop for MessageQueue<M> {
unsafe { sys::furi_message_queue_free(self.hnd) }
}
}

#[flipperzero_test::tests]
mod tests {
use core::time::Duration;

use flipperzero_sys::furi::Status;

use super::MessageQueue;

#[test]
fn capacity() {
let queue = MessageQueue::new(3);
assert_eq!(queue.len(), 0);
assert_eq!(queue.space(), 3);
assert_eq!(queue.capacity(), 3);

// Adding a message to the queue should consume capacity.
queue.put(2, Duration::from_millis(1)).unwrap();
assert_eq!(queue.len(), 1);
assert_eq!(queue.space(), 2);
assert_eq!(queue.capacity(), 3);

// We should be able to fill the queue to capacity.
queue.put(4, Duration::from_millis(1)).unwrap();
queue.put(6, Duration::from_millis(1)).unwrap();
assert_eq!(queue.len(), 3);
assert_eq!(queue.space(), 0);
assert_eq!(queue.capacity(), 3);

// Attempting to add another message should time out.
assert_eq!(
queue.put(7, Duration::from_millis(1)),
Err(Status::ERR_TIMEOUT),
);

// Removing a message from the queue frees up capacity.
assert_eq!(queue.get(Duration::from_millis(1)), Ok(2));
assert_eq!(queue.len(), 2);
assert_eq!(queue.space(), 1);
assert_eq!(queue.capacity(), 3);
}
}
21 changes: 21 additions & 0 deletions crates/flipperzero/src/furi/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,24 @@ impl<T: ?Sized> Drop for MutexGuard<'_, T> {
// `UnsendUnsync` is actually a bit too strong.
// As long as `T` implements `Sync`, it's fine to access it from another thread.
unsafe impl<T: ?Sized + Sync> Sync for MutexGuard<'_, T> {}

#[flipperzero_test::tests]
mod tests {
use super::Mutex;

#[test]
fn unshared_mutex_does_not_block() {
let mutex = Mutex::new(7u64);

{
let mut value = mutex.lock().expect("should not fail");
assert_eq!(*value, 7);
*value = 42;
}

{
let value = mutex.lock().expect("should not fail");
assert_eq!(*value, 42);
}
}
}
6 changes: 6 additions & 0 deletions crates/flipperzero/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! High-level bindings for the Flipper Zero.

#![no_std]
#![cfg_attr(test, no_main)]

#[cfg(feature = "alloc")]
extern crate alloc;
Expand All @@ -18,3 +19,8 @@ pub mod __internal {
// Re-export for use in macros
pub use ufmt;
}

flipperzero_test::tests_runner!(
name = "flipperzero-rs Unit Tests",
[crate::furi::message_queue::tests, crate::furi::sync::tests]
);
16 changes: 16 additions & 0 deletions crates/flipperzero/tests/dolphin.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#![no_std]
#![no_main]

#[flipperzero_test::tests]
mod tests {
use flipperzero::dolphin::Dolphin;

#[test]
fn stats() {
let mut dolphin = Dolphin::open();
let stats = dolphin.stats();
assert!(stats.level >= 1);
}
}

flipperzero_test::tests_runner!(name = "Dolphin Integration Test", [crate::tests]);
12 changes: 12 additions & 0 deletions crates/sys/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

#![no_std]

// Features that identify thumbv7em-none-eabihf.
// Until target_abi is stable, this also permits thumbv7em-none-eabi.
#[cfg(not(all(
target_arch = "arm",
target_feature = "thumb2",
target_feature = "v7",
target_feature = "dsp",
target_os = "none",
//target_abi = "eabihf",
)))]
core::compile_error!("This crate requires `--target thumbv7em-none-eabihf`");

pub mod furi;
mod inlines;

Expand Down
20 changes: 20 additions & 0 deletions crates/test/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "flipperzero-test"
version = "0.1.0"
repository.workspace = true
readme.workspace = true
license.workspace = true
edition.workspace = true
rust-version.workspace = true
autobins = false
autotests = false
autobenches = false

[dependencies]
flipperzero-sys.workspace = true
flipperzero-test-macros = { version = "=0.1.0", path = "macros" }
ufmt.workspace = true

[lib]
bench = false
test = false
12 changes: 12 additions & 0 deletions crates/test/macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "flipperzero-test-macros"
version = "0.1.0"
edition = "2021"

[lib]
proc-macro = true

[dependencies]
proc-macro2 = "1"
quote = "1"
syn = { version = "1", features = ["full"] }
101 changes: 101 additions & 0 deletions crates/test/macros/src/deassert.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
use quote::quote;
use syn::{parse, Block, Expr, ExprMacro, ExprTuple, Stmt};

/// Find and replace macro assertions inside the given block with `return Err(..)`.
///
/// The following assertion macros are replaced:
/// - [`assert`]
/// - [`assert_eq`]
/// - [`assert_ne`]
pub(crate) fn box_block(mut block: Box<Block>) -> parse::Result<Box<Block>> {
block.stmts = block_stmts(block.stmts)?;
Ok(block)
}

/// Searches recursively through block statements to find and replace macro assertions
/// with `return Err(..)`.
///
/// The following assertion macros are replaced:
/// - [`assert`]
/// - [`assert_eq`]
/// - [`assert_ne`]
fn block_stmts(stmts: Vec<Stmt>) -> parse::Result<Vec<Stmt>> {
stmts
.into_iter()
.map(|stmt| match stmt {
Stmt::Expr(Expr::Block(mut e)) => {
e.block.stmts = block_stmts(e.block.stmts)?;
Ok(Stmt::Expr(Expr::Block(e)))
}
Stmt::Expr(Expr::Macro(m)) => expr_macro(m).map(Stmt::Expr),
Stmt::Semi(Expr::Macro(m), trailing) => expr_macro(m).map(|m| Stmt::Semi(m, trailing)),
_ => Ok(stmt),
})
.collect::<Result<_, _>>()
}

/// Replaces macro assertions with `return Err(..)`.
///
/// The following assertion macros are replaced:
/// - [`assert`]
/// - [`assert_eq`]
/// - [`assert_ne`]
fn expr_macro(m: ExprMacro) -> parse::Result<Expr> {
if m.mac.path.is_ident("assert") {
let tokens = m.mac.tokens;
let tokens_str = tokens.to_string();
syn::parse(
quote!(
if !(#tokens) {
return ::core::result::Result::Err(
::core::concat!("assertion failed: ", #tokens_str).into(),
);
}
)
.into(),
)
} else if m.mac.path.is_ident("assert_eq") {
let (left, right) = binary_macro(m.mac.tokens)?;
let left_str = quote!(#left).to_string();
let right_str = quote!(#right).to_string();
syn::parse(
quote!(
if #left != #right {
return ::core::result::Result::Err(
::flipperzero_test::TestFailure::AssertEq {
left: #left_str,
right: #right_str,
}
);
}
)
.into(),
)
} else if m.mac.path.is_ident("assert_ne") {
let (left, right) = binary_macro(m.mac.tokens)?;
let left_str = quote!(#left).to_string();
let right_str = quote!(#right).to_string();
syn::parse(
quote!(
if #left == #right {
return ::core::result::Result::Err(
::flipperzero_test::TestFailure::AssertNe {
left: #left_str,
right: #right_str,
}
);
}
)
.into(),
)
} else {
Ok(Expr::Macro(m))
}
}

fn binary_macro(tokens: proc_macro2::TokenStream) -> parse::Result<(Expr, Expr)> {
let parts: ExprTuple = syn::parse(quote!((#tokens)).into())?;
assert_eq!(parts.elems.len(), 2);
let mut elems = parts.elems.into_iter();
Ok((elems.next().unwrap(), elems.next().unwrap()))
}
Loading

0 comments on commit bd2139e

Please sign in to comment.