Skip to content
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

Test harness #57

Merged
merged 6 commits into from
Apr 25, 2023
Merged
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
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,
Comment on lines +33 to +34
Copy link
Collaborator

Choose a reason for hiding this comment

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

For something as simple as this script, we could probably even get away with just + sys.argv.

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)]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we just apply #[no_main] unconditionally? I'm not sure if there's much of an impact on lib crates, but FAP applications are unlikely to support a standard main function.


#[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`]
dcoles marked this conversation as resolved.
Show resolved Hide resolved
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)> {
dcoles marked this conversation as resolved.
Show resolved Hide resolved
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