Merge branch 'master' into rm-allows
diff --git a/core/platform/src/async_traits.rs b/core/platform/src/async_traits.rs
new file mode 100644
index 0000000..2853026
--- /dev/null
+++ b/core/platform/src/async_traits.rs
@@ -0,0 +1,93 @@
+//! Traits for building lightweight asynchronous APIs. These traits lack the
+//! dynamic capabilities of `core::future::Future`, but have much smaller code
+//! size and RAM usage costs.
+//!
+//! Tock kernel developers will be familiar with the 2-phase pattern of
+//! operation these traits support. Client code (code using an asynchronous
+//! component) calls a function or method provided by the asynchronous component
+//! to start an asynchronous operation. When the operation is complete, the
+//! asynchronous component calls a callback defined using the `FreeCallback`
+//! and/or `MethodCallback` traits.
+//!
+//! Note that asynchronous callbacks must only be called from within "callback
+//! context" -- that is, within a kernel callback (registered using the
+//! `subscribe` system call). To enforce that, the callback traits require the
+//! `CallbackContext` type, which is only instantiated by an instance of the
+//! `Syscalls` trait.
+//!
+//! There is no prohibition on a callback calling back into the asynchronous
+//! component that called it. In other words, if component B calls
+//! `<A as FreeCallback<Done>>::call(...)`, then `call()` can call back into B
+//! to start a new operation. Asynchronous components should clean up their
+//! state internally before calling callbacks so as to support reentrant calls
+//! into themselves!
+//!
+//! In client code, prefer to implement `FreeCallback` instead of
+//! `MethodCallback` when possible, as it is easier to pass a `FreeCallback` to
+//! an asynchronous component.
+//!
+//! To bridge the gap between `FreeCallback` and `MethodCallback`, we also have
+//! the `Locator` trait. `Locator` allows `FreeCallback` implementations to find
+//! their global state, and also provides `FreeCallback` versions of callbacks
+//! for types that implement `MethodCallback`. In general, `Locator` should be
+//! implemented by `libtock_runtime::static_component!` (in real Tock apps) and
+//! `libtock_unittest::test_component!` (in unit tests), rather than directly by
+//! user code.
+// TODO: At the time of this writing, neither `libtock_runtime` nor
+// `libtock_unittest` are implemented. Remove this TODO when they are
+// implemented.
+
+/// `FreeCallback` is the callback equivalent of a free function: it does not
+/// have access to the client component's data. `FreeCallback` is used by
+/// asynchronous components -- such as `Syscalls` -- which cannot efficiently
+/// store a client reference.
+pub trait FreeCallback<AsyncResponse> {
+ fn call(context: CallbackContext, response: AsyncResponse);
+}
+
+/// `MethodCallback` is a callback method; it can access the client component's
+/// data. Note that asynchronous components generally need to use interior
+/// mutability to mutate data, as `MethodCallback` is designed under the
+/// assumption that there are multiple references to most asynchronous
+/// components at any given time.
+pub trait MethodCallback<AsyncResponse> {
+ fn call(&self, context: CallbackContext, response: AsyncResponse);
+}
+
+/// `Syscalls` instantiates a `CallbackContext` when a kernel callback is
+/// called. The lifetime prevents the `CallbackContext` from being copied into
+/// storage that outlives the callback. Code that is only safe to call from
+/// callback context can request a `CallbackContext` argument. `CallbackContext`
+/// is a zero-sized type so passing it around has no runtime cost.
+#[derive(Clone, Copy)]
+pub struct CallbackContext<'c> {
+ // `_phantom` serves three purposes. It uses the `c lifetime to avoid an
+ // "unused lifetime" error, it provides the proper variance over 'c, and it
+ // prevents code outside this crate from directly constructing a
+ // CallbackContext (because of its visibility control). Code outside this
+ // crate can copy a CallbackContext, but that is fine as copying the
+ // CallbackContext preserves its associated lifetime.
+ pub(crate) _phantom: core::marker::PhantomData<&'c ()>,
+}
+
+/// Provides access to a global instance of type `Target`. Every call to
+/// `locate()` on a given Locator type should return a reference to the same
+/// instance of `Target`. An instance of `Locator` generally isn't instantiated
+/// directly; instead, its type is passed to where it is needed via generic
+/// arguments.
+///
+/// For convenience, Locator provides a `FreeCallback` implementation for every
+/// `MethodCallback` implementation that `Target` has.
+pub trait Locator: 'static {
+ type Target;
+ fn locate() -> &'static Self::Target;
+}
+
+impl<L: Locator, AsyncResponse> FreeCallback<AsyncResponse> for L
+where
+ L::Target: MethodCallback<AsyncResponse>,
+{
+ fn call(context: CallbackContext, response: AsyncResponse) {
+ L::locate().call(context, response);
+ }
+}
diff --git a/core/platform/src/lib.rs b/core/platform/src/lib.rs
index 6a23dc5..0a9f936 100644
--- a/core/platform/src/lib.rs
+++ b/core/platform/src/lib.rs
@@ -1,7 +1,12 @@
#![no_std]
+mod async_traits;
mod error_code;
+mod raw_syscalls;
mod syscalls;
+mod syscalls_impl;
+pub use async_traits::{CallbackContext, FreeCallback, Locator, MethodCallback};
pub use error_code::ErrorCode;
-pub use syscalls::{MemopNoArg, MemopWithArg, Syscalls};
+pub use raw_syscalls::{OneArgMemop, RawSyscalls, YieldType, ZeroArgMemop};
+pub use syscalls::Syscalls;
diff --git a/core/platform/src/raw_syscalls.rs b/core/platform/src/raw_syscalls.rs
new file mode 100644
index 0000000..90fc602
--- /dev/null
+++ b/core/platform/src/raw_syscalls.rs
@@ -0,0 +1,239 @@
+// TODO: Implement `libtock_runtime` and `libtock_unittest`, which are
+// referenced in the comment on `RawSyscalls`.
+
+/// `RawSyscalls` allows a fake Tock kernel to be injected into components for
+/// unit testing. It is implemented by `libtock_runtime::TockSyscalls` and
+/// `libtock_unittest::FakeSyscalls`. Components should not use `RawSyscalls`
+/// directly; instead, use the `Syscalls` trait, which provides higher-level
+/// interfaces to the system calls.
+
+// RawSyscalls is designed to minimize the amount of handwritten assembly code
+// needed without generating unnecessary instructions. This comment describes
+// the thought process that led to the choice of methods for RawSyscalls. There
+// are a few major considerations affecting its design:
+//
+// 1. Most system calls only clobber r0-r4 (*), while yield has a far longer
+// clobber list. As such, yield must have its own assembly
+// implementation.
+// 2. The compiler is unable to optimize away unused arguments. For example,
+// memop's "get process RAM start address" operation only needs r0 set,
+// while memop's "break" operation needs both r0 and r1 set. If our
+// inline assembly calls "get process RAM start address" but sets both r0
+// and r1, the compiler doesn't know that r1 will be
+// ignored so setting that register will not be optimized away. Therefore
+// we want to set the minimum number of argument registers possible.
+// 3. The cost of specifying unused return registers is only that of
+// unnecessarily marking a register as clobbered. Explanation: After
+// inlining, an unused register is marked as "changed by the assembly"
+// but can immediately be re-used by the compiler, which is the same as a
+// clobbered register. System calls should generally be
+// inlined -- and even if they aren't, the unused return values will
+// probably be passed in caller-saved registers (this is true for the C
+// ABI, so probably true for the Rust ABI), which are treated as
+// clobbered regardless.
+//
+// (*) When this file refers to registers, it uses the same naming convention as
+// the Tock 2.0 syscalls TRD. Registers r0-r4 correspond to ARM registers r0-r4
+// and RISC-V registers a0-a4.
+//
+// Currently, yield takes exactly one argument (to specify what yield type to
+// do). Therefore we only need one raw yield call.
+//
+// Based on these considerations, it would make sense to have the following
+// methods:
+// yield
+// zero_arg_syscall
+// one_arg_syscall
+// two_arg_syscall
+// three_arg_syscall
+// four_arg_syscall
+//
+// However, there are no system calls that take 0 or 3 arguments, so we do not
+// need the corresponding methods. This leaves yield, 1-arg, 2-arg, and 4-arg
+// system calls.
+//
+// The 1-arg and 2-arg system calls are only used for memop. Memop currently has
+// the property that none of its operations can lead to undefined behavior.
+// Therefore, we can rename the 1-arg and 2-arg system calls to zero_arg_memop
+// and one_arg_memop and make them safe methods (the argument counts change
+// because the number of system call arguments is one greater than the number of
+// arguments passed to the specific op).
+//
+// This only leaves four_arg_syscall, which is used to implement subscribe,
+// command, read-write allow, and read-only allow.
+//
+// Therefore the final design has 4 methods:
+// yield
+// zero_arg_memop
+// one_arg_memop
+// four_arg_syscall
+//
+// If a new system call class that uses fewer than four arguments is added, then
+// the above list will need to be revised.
+//
+// Note that `command` always needs to use four_arg_syscall, even when calling a
+// command with fewer arguments, because we don't want to leak the
+// (possibly secret) values in the r2 and r3 registers to untrusted capsules.
+// Yield and memop do not have this concern and can leave arbitrary data in r2
+// and r3, because they are implemented by the core kernel, which is trusted.
+//
+// The success type for Memop calls depends on the operation performed. However,
+// all *currently defined* memop operations return either Success or Success
+// with u32. Therefore, the memop implementations only need to mark r0 and r1 as
+// clobbered, not r2 and r3. This choice of clobbers will need to be revisited
+// if and when a memop operation that returns more data is added.
+//
+// The decision of where to use u32 and usize can be a bit tricky. The Tock
+// syscall ABI is currently only specified for 32-bit systems, so on real Tock
+// systems both types match the size of a register, but the unit test
+// environment can be either 32 bit or 64 bit. This interface uses usize for
+// values that can contain pointers, so that pointers are not truncated in the
+// unit test environment. To keep types as consistent as possible, it uses u32
+// for all values that cannot be pointers.
+pub trait RawSyscalls {
+ // raw_yield should:
+ // 1. Call syscall class 0
+ // 2. Use register r0 for input and output as an inlateout register,
+ // passing in r0_in and returning its value.
+ // 3. Mark all caller-saved registers as lateout clobbers.
+ // 4. NOT provide any of the following options:
+ // pure (yield has side effects)
+ // nomem (a callback can read + write globals)
+ // readonly (a callback can write globals)
+ // preserves_flags (a callback can change flags)
+ // noreturn (yield is expected to return)
+ // nostack (a callback needs the stack)
+ //
+ // Design note: This is safe because the yield types that currently exist
+ // are safe. If an unsafe yield type is added, we will need to make
+ // raw_yield unsafe. Although raw_yield shouldn't be called by code outside
+ // this crate, it can be, so that is a backwards-incompatible change. We
+ // pass YieldType rather than a usize because if we used usize directly then
+ // this API becomes unsound if the kernel adds support for an unsafe yield
+ // type (or even one that takes one more argument).
+ fn raw_yield(r0_in: YieldType) -> u32;
+
+ // four_arg_syscall is used to invoke the subscribe, command, read-write
+ // allow, and read-only allow system calls.
+ //
+ // four_arg_syscall's inline assembly should have the following properties:
+ // 1. Calls the syscall class specified by class
+ // 2. Passes r0-r3 in the corresponding registers as inlateout
+ // registers. Returns r0-r3 in order.
+ // 3. Does not mark any registers as clobbered.
+ // 4. Has all of the following options:
+ // preserves_flags (these system calls do not touch flags)
+ // nostack (these system calls do not touch the stack)
+ // 5. Does NOT have any of the following options:
+ // pure (these system calls have side effects)
+ // nomem (the compiler needs to write to globals before allow)
+ // readonly (rw allow can modify memory)
+ // noreturn (all these system calls are expected to return)
+ //
+ // Note that subscribe's application data argument can potentially contain a
+ // pointer, so r3 can contain a pointer (in addition to r1 and r2, which
+ // more obviously contain pointers for subscribe and memop).
+ //
+ // For subscribe(), the callback pointer should be either 0 (for the null
+ // callback) or an `unsafe extern fn(u32, u32, u32, usize)`.
+ /// # Safety
+ /// `four_arg_syscall` must NOT be used to invoke yield. Otherwise, it has
+ /// the same safety invariants as the underlying system call, which varies
+ /// depending on the system call class.
+ unsafe fn four_arg_syscall(
+ r0: u32,
+ r1: u32,
+ r2: usize,
+ r3: usize,
+ class: u8,
+ ) -> (u32, usize, usize, usize);
+
+ // zero_arg_memop is used to invoke memop operations that do not accept an
+ // argument register. Because there are no memop commands that set r2 or r3,
+ // this only needs to return r0 and r1.
+ //
+ // Memop commands may panic in the unit test environment, as not all memop
+ // calls can be sensibly implemented in that environment.
+ //
+ // zero_arg_memop's inline assembly should have the following properties:
+ // 1. Calls syscall class 5
+ // 2. Specifies r0 as an inlateout register, and r1 as a lateout
+ // register.
+ // 3. Does not mark any registers as clobbered.
+ // 4. Has all of the following options:
+ // preserves_flags
+ // nostack
+ // nomem (it is okay for the compiler to cache globals
+ // across memop calls)
+ // 5. Does NOT have any of the following options:
+ // pure (two invocations of the same memop can return
+ // different values)
+ // readonly (incompatible with nomem)
+ // noreturn
+ //
+ // Design note: like raw_yield, this is safe because memops that currently
+ // exist are safe. zero_arg_memop takes a ZeroArgMemop rather than a u32 so
+ // that if the kernel adds an unsafe memop -- or one that can clobber r2/r3
+ // -- this API doesn't become unsound.
+ fn zero_arg_memop(r0_in: ZeroArgMemop) -> (u32, usize);
+
+ // one_arg_memop is used to invoke memop operations that take an argument.
+ // Because there are no memop operations that set r2 or r3, this only needs
+ // to return r0 and r1.
+ //
+ // one_arg_memop's inline assembly should:
+ // 1. Call syscall class 5
+ // 2. Specify r0 and r1 as inlateout registers, and return (r0, r1)
+ // 3. Not mark any registers as clobbered.
+ // 4. Have all of the following options:
+ // preserves_flags
+ // nostack
+ // nomem (the compiler can cache globals across memop
+ // calls)
+ // 5. Does NOT have any of the following options:
+ // pure Two invocations of sbrk can return different values
+ // readonly Incompatible with nomem
+ // noreturn
+ //
+ // Design note: like raw_yield, this is safe because memops that currently
+ // exist are safe. zero_arg_memop takes a ZeroArgMemop rather than a u32 so
+ // that if the kernel adds an unsafe memop -- or one that can clobber r2/r3
+ // -- this API doesn't become unsound.
+ fn one_arg_memop(r0_in: OneArgMemop, r1: usize) -> (u32, usize);
+}
+
+#[non_exhaustive]
+#[repr(u32)]
+pub enum OneArgMemop {
+ Brk = 0,
+ Sbrk = 1,
+ FlashRegionStart = 8,
+ FlashRegionEnd = 9,
+ SpecifyStackTop = 10,
+ SpecifyHeapStart = 11,
+ // Note: before adding new memop operations, make sure the assumptions in
+ // the design notes on `one_arg_memop` are valid for the new operation type.
+}
+
+// TODO: When the numeric values (0 and 1) are assigned to the yield types,
+// specify those values here.
+#[non_exhaustive]
+#[repr(u32)]
+pub enum YieldType {
+ Wait,
+ NoWait,
+}
+
+#[non_exhaustive]
+#[repr(u32)]
+pub enum ZeroArgMemop {
+ MemoryStart = 2,
+ MemoryEnd = 3,
+ FlashStart = 4,
+ FlashEnd = 5,
+ GrantStart = 6,
+ FlashRegions = 7,
+ // Note: before adding new memop operations, make sure the assumptions in
+ // the design notes on `zero_arg_memop` are valid for the new operation
+ // type.
+}
diff --git a/core/platform/src/syscalls.rs b/core/platform/src/syscalls.rs
index 9a78169..8d85380 100644
--- a/core/platform/src/syscalls.rs
+++ b/core/platform/src/syscalls.rs
@@ -1,99 +1,26 @@
-//! Provides the Syscalls trait which directly represents Tock's system call
-//! APIs. Syscalls is implemented by both `libtock_runtime` which makes system
-//! calls into a real Tock kernel, and `libtock_fake` which is a fake Tock
-//! kernel.
+// TODO: Implement `libtock_runtime` and `libtock_unittest`, which are
+// referenced in the comment on `Syscalls`.
-// TODO: Implement `libtock_runtime` and `libtock_fake`.
+/// `Syscalls` provides safe abstractions over Tock's system calls. It is
+/// implemented for `libtock_runtime::TockSyscalls` and
+/// `libtock_unittest::FakeSyscalls` (by way of `RawSyscalls`).
+pub trait Syscalls {
+ /// Puts the process to sleep until a callback becomes pending, invokes the
+ /// callback, then returns.
+ fn yield_wait();
-/// Syscalls represents Tock's system call APIs. It is designed to be
-/// implemented as easily as possible -- its arguments and return values
-/// correspond directly to registers in the ABI. For a higher-level abstraction,
-/// see Platform.
-///
-/// By design, syscalls is designed to be zero-cost in a TBF binary and
-/// functional (but not zero-cost) in unit tests. In a TBF binary, Syscalls is
-/// implemented with the `'static` lifetime, and is a zero-sized type. Syscalls
-/// requires `Copy` in order to support defining it usefully on zero-sized
-/// types. When used in unit tests, the Syscalls implementation carries a
-/// lifetime local to that unit test.
-///
-/// With the exception of `memop`, this trait aligns closely to Tock's
-/// kernel::Driver trait.
-pub trait Syscalls<'k>: Copy {
- /// Calls the `allow` system call.
- ///
- /// # Safety
- /// `allow` is unsafe because callers must guarantee that `pointer` and
- /// `length` refer to memory that the kernel can mutate safely. The buffer
- /// must last for the lifetime 'k.
- // `driver` and `minor` are `usize` because the kernel internally treats
- // them as `usize`s. `allow`'s return value is a kernel `ReturnCode`;
- // Platform translates the `isize` into a `ReturnCode`.
- unsafe fn allow(self, driver: usize, minor: usize, pointer: *mut u8, length: usize) -> isize;
+ /// Runs the next pending callback, if a callback is pending. Unlike
+ /// `yield_wait`, `yield_no_wait` returns immediately if no callback is
+ /// pending. Returns true if a callback was executed, false otherwise.
+ fn yield_no_wait() -> bool;
- /// Calls the `command` system call.
- // `driver`, `minor`, `arg1`, and `arg2` are all `usize` (rather than `u32`)
- // because the kernel refers to them internally as `usize`s. command returns
- // a kernel ReturnCode; Platform is responsible for translating an isize
- // into the local ReturnCode.
- fn command(self, driver: usize, minor: usize, arg1: usize, arg2: usize) -> isize;
+ // TODO: Add a subscribe interface.
- /// Calls the `memop` system call with an argument. Note that memop() cannot
- /// cause memory unsafety, although it can cause the app to fault (e.g. Brk
- /// can move the app break below the stack, causing a fault). The isize
- /// returned is a kernel ReturnCode.
- // Platform performs the translation from isize into ReturnCode to keep
- // Syscalls implementations simple.
- fn memop_arg(self, op: MemopWithArg, arg: usize) -> isize;
+ // TODO: Add a command interface.
- /// Calls the `memop` system call with no arguments. This version is
- /// slightly cheaper because it does not need to set the argument register.
- // We're okay with leaking the value in the argument register because
- // memop() is always handled by the core kernel, never by an untrusted
- // capsule.
- fn memop_noarg(self, op: MemopNoArg) -> isize;
+ // TODO: Add a read-write allow interface.
- /// Calls the `subscribe` system call.
- ///
- /// # Safety
- /// `subscribe` is unsafe because the callback can potentially be unsafe,
- /// and callers of `subscribe` must assert that calling the callback with
- /// the provided `data` value is safe. The callback must last for the 'k
- /// lifetime.
- // Driver, minor, the callback args, and data are all represented as `usize`
- // because that is the type the kernel uses internally to store them (e.g.
- // as opposed to u32).
- unsafe fn subscribe(
- self,
- driver: usize,
- minor: usize,
- callback: Option<unsafe extern "C" fn(usize, usize, usize, usize)>,
- data: usize,
- );
+ // TODO: Add a read-only allow interface.
- /// Puts the process to sleep until a callback becomes pending, then invokes
- /// the callback.
- fn yieldk(self);
-}
-
-#[non_exhaustive]
-#[repr(usize)]
-pub enum MemopWithArg {
- Brk = 0,
- Sbrk = 1,
- FlashRegionStart = 8,
- FlashRegionEnd = 9,
- SpecifyStackTop = 10,
- SpecifyHeapStart = 11,
-}
-
-#[non_exhaustive]
-#[repr(usize)]
-pub enum MemopNoArg {
- MemoryStart = 2,
- MemoryEnd = 3,
- FlashStart = 4,
- FlashEnd = 5,
- GrantStart = 6,
- FlashRegions = 7,
+ // TODO: Add memop() methods.
}
diff --git a/core/platform/src/syscalls_impl.rs b/core/platform/src/syscalls_impl.rs
new file mode 100644
index 0000000..0af15c5
--- /dev/null
+++ b/core/platform/src/syscalls_impl.rs
@@ -0,0 +1,18 @@
+//! Implements `Syscalls` for all types that implement `RawSyscalls`.
+
+use crate::{RawSyscalls, Syscalls, YieldType};
+
+impl<S: RawSyscalls> Syscalls for S {
+ // -------------------------------------------------------------------------
+ // Yield
+ // -------------------------------------------------------------------------
+
+ fn yield_wait() {
+ Self::raw_yield(YieldType::Wait);
+ }
+
+ fn yield_no_wait() -> bool {
+ // TODO: Introduce a return type abstraction so this 0 isn't hardcoded.
+ Self::raw_yield(YieldType::NoWait) != 0
+ }
+}
diff --git a/test_runner/Cargo.toml b/test_runner/Cargo.toml
index 66a407a..9471138 100644
--- a/test_runner/Cargo.toml
+++ b/test_runner/Cargo.toml
@@ -3,20 +3,3 @@
version = "0.1.0"
authors = ["torfmaster <briefe@kebes.de>"]
edition = "2018"
-
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
-
-[dependencies]
-structopt = { version = "0.3", default-features = false }
-futures = "0.3.4"
-
-[dependencies.async-std]
-# async-std 1.7 pulls in crossbeam_utils 1.8, which does not work with the
-# nightly Rust toolchain we use. Temporarily block async-std 1.7 until we can
-# update our Rust toolchain.
-version = "1.5.0, <1.7"
-features = ["attributes"]
-
-[dependencies.tokio]
-version = "0.2.12"
-features = ["process", "rt-threaded", "macros", "io-util", "time"]
diff --git a/test_runner/src/main.rs b/test_runner/src/main.rs
index 0bea1cf..b21b945 100644
--- a/test_runner/src/main.rs
+++ b/test_runner/src/main.rs
@@ -1,20 +1,10 @@
use std::fmt;
-use std::process::Stdio;
+use std::io::{BufRead, BufReader};
+use std::process::{ChildStdout, Command, Stdio};
use std::time::Duration;
-use tokio::io::AsyncBufReadExt;
-use tokio::io::BufReader;
-use tokio::process::Command;
-use tokio::time::timeout;
-#[tokio::main]
-async fn main() -> Result<(), Box<dyn std::error::Error>> {
- timeout(Duration::from_secs(10), perform_tests()).await?
-}
-
-async fn perform_tests() -> Result<(), Box<dyn std::error::Error>> {
- let mut failed_tests = Vec::new();
-
- let tests = Command::new("tock/tools/qemu-build/riscv32-softmmu/qemu-system-riscv32")
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let mut tests = Command::new("tock/tools/qemu-build/riscv32-softmmu/qemu-system-riscv32")
.arg("-M")
.arg("sifive_e,revb=true")
.arg("-kernel")
@@ -24,15 +14,32 @@
.arg("-nographic")
.stdin(Stdio::null())
.stdout(Stdio::piped())
- .kill_on_drop(true)
.spawn()?;
+ let stdout = tests.stdout.take().unwrap();
+ let child_handle = std::sync::Arc::new(std::sync::Mutex::new(tests));
+ let timeout_handle = child_handle.clone();
+ std::thread::spawn(move || {
+ std::thread::sleep(Duration::from_secs(10));
+ let _ = timeout_handle
+ .lock()
+ .unwrap_or_else(std::sync::PoisonError::into_inner)
+ .kill();
+ });
+ let result = process_output(stdout);
+ let _ = child_handle
+ .lock()
+ .unwrap_or_else(std::sync::PoisonError::into_inner)
+ .kill();
+ result
+}
- let stdout = tests.stdout.unwrap();
-
+fn process_output(stdout: ChildStdout) -> Result<(), Box<dyn std::error::Error>> {
+ let mut failed_tests = Vec::new();
let stdout_reader = BufReader::new(stdout);
- let mut stdout_lines = stdout_reader.lines();
+ let stdout_lines = stdout_reader.lines();
- while let Some(line) = stdout_lines.next_line().await? {
+ for line in stdout_lines {
+ let line = line?;
println!("UART: {}", line);
let test_result = test_succeeded(line, &mut failed_tests);
if let Some(true) = test_result {