blob: 43a908e679ea76f5a29c955a50d43ce0c5646fb2 [file]
use std::{
io::Write,
path::{Path, PathBuf},
process::ExitCode,
time::{Duration, Instant},
};
use crate::dut_definition::{DefinitionSource, DutDefinition};
use clap::{Parser, Subcommand};
use colored::Colorize;
use linkme::distributed_slice;
use miette::{Context, IntoDiagnostic, Result};
use probe_rs::Permissions;
mod dut_definition;
mod macros;
mod tests;
#[derive(Debug, miette::Diagnostic, thiserror::Error)]
pub enum TestFailure {
#[error("Test returned an error")]
Error(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
#[error("The test was skipped: {0}")]
Skipped(String),
#[error("Test is not implemented for target {0:?}: {1}")]
UnimplementedForTarget(Box<probe_rs::Target>, String),
#[error("A resource necessary to execute the test is not available: {0}")]
MissingResource(String),
/// A fatal error means that all future tests will be cancelled as well.
#[error("A fatal error occured")]
Fatal(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
}
impl From<miette::ErrReport> for TestFailure {
fn from(report: miette::ErrReport) -> Self {
TestFailure::Error(report.into())
}
}
impl From<probe_rs::Error> for TestFailure {
fn from(error: probe_rs::Error) -> Self {
TestFailure::Error(error.into())
}
}
pub type TestResult = Result<(), TestFailure>;
#[derive(Debug)]
struct SingleTestReport {
result: TestResult,
_duration: Duration,
}
impl SingleTestReport {
fn has_fatal_error(&self) -> bool {
matches!(self.result, Err(TestFailure::Fatal(_)))
}
fn failed(&self) -> bool {
matches!(
self.result,
Err(TestFailure::Error(_) | TestFailure::Fatal(_))
)
}
}
#[derive(Debug, Subcommand)]
enum Command {
Test {
#[arg(long, value_name = "FILE")]
markdown_summary: Option<PathBuf>,
},
}
#[derive(Debug, Parser)]
struct Opt {
#[command(subcommand)]
command: Command,
#[arg(long, global = true, value_name = "DIRECTORY", conflicts_with_all = ["chip", "probe", "single_dut"])]
dut_definitions: Option<PathBuf>,
#[arg(long, global = true, value_name = "CHIP", conflicts_with_all = ["dut_definitions", "single_dut"])]
chip: Option<String>,
#[arg(long, global = true, value_name = "PROBE")]
probe: Option<String>,
#[arg(long, global = true, value_name = "PROBE_SPEED")]
probe_speed: Option<u32>,
#[arg(long, global = true, value_name = "FILE", conflicts_with_all = ["chip", "dut_definitions"])]
single_dut: Option<PathBuf>,
}
fn main() -> Result<ExitCode> {
pretty_env_logger::init();
let opt = Opt::parse();
let mut definitions = if let Some(dut_definitions) = opt.dut_definitions.as_deref() {
let definitions = DutDefinition::collect(dut_definitions)?;
println!("Found {} target definitions.", definitions.len());
definitions
} else if let Some(single_dut) = opt.single_dut.as_deref() {
vec![DutDefinition::from_file(Path::new(single_dut))?]
} else {
// Chip needs to be specified
let chip = opt.chip.as_deref().unwrap(); // If dut-definitions is not present, chip must be present
if let Some(probe) = &opt.probe {
vec![DutDefinition::new(chip, probe)?]
} else {
vec![DutDefinition::autodetect_probe(chip)?]
}
};
for definition in &mut definitions {
if let Some(probe_speed) = opt.probe_speed {
definition.probe_speed = Some(probe_speed);
}
}
match opt.command {
Command::Test { markdown_summary } => run_test(&definitions, markdown_summary),
}
}
fn run_test(definitions: &[DutDefinition], markdown_summary: Option<PathBuf>) -> Result<ExitCode> {
let mut test_tracker = TestTracker::new(definitions);
let result = test_tracker.run(|tracker, definition| {
let probe = definition.open_probe()?;
println_dut_status!(tracker, blue, "Probe: {:?}", probe.get_name());
println_dut_status!(tracker, blue, "Chip: {:?}", &definition.chip.name);
// We don't care about existing flash contents
let permissions = Permissions::default().allow_erase_all();
let mut fail_counter = 0;
let mut session = probe
.attach(definition.chip.clone(), permissions)
.into_diagnostic()
.wrap_err("Failed to attach to chip")?;
let cores = session.list_cores();
// TODO: Handle different cores. Handling multiple cores is not supported properly yet,
// some cores need additional setup so that they can be used, and this is not handled yet.
for (core_index, core_type) in cores.into_iter().take(1) {
println_dut_status!(tracker, blue, "Core {}: {:?}", core_index, core_type);
let mut core = session.core(core_index).into_diagnostic()?;
println_dut_status!(tracker, blue, "Halting core..");
core.reset_and_halt(Duration::from_millis(500))
.into_diagnostic()?;
for test_fn in CORE_TESTS {
let result = tracker.run_test(|tracker| test_fn(tracker, &mut core));
if result.has_fatal_error() {
return Err(miette::miette!("Test failed with fatal error"));
}
if result.failed() {
fail_counter += 1;
}
}
// Ensure core is not running anymore.
core.reset_and_halt(Duration::from_millis(200))
.into_diagnostic()
.wrap_err_with(|| {
format!("Failed to reset core with index {core_index} after test")
})?;
}
for test in SESSION_TESTS {
let result = tracker.run_test(|tracker| test(tracker, &mut session));
if result.has_fatal_error() {
return Err(miette::miette!("Test failed with fatal error"));
}
if result.failed() {
fail_counter += 1;
}
}
drop(session);
// Try attaching with hard reset
if definition.reset_connected {
let probe = definition.open_probe()?;
let _session = probe
.attach_under_reset(definition.chip.clone(), Permissions::default())
.into_diagnostic()?;
}
match fail_counter {
0 => Ok(()),
1 => Err(miette::miette!("1 test failed")),
count => Err(miette::miette!("{count} tests failed")),
}
});
println!();
let printer = ConsoleReportPrinter;
printer
.print(&result, std::io::stdout())
.into_diagnostic()?;
if let Some(summary_file) = &markdown_summary {
let mut file = std::fs::File::create(summary_file)
.into_diagnostic()
.wrap_err_with(|| {
format!(
"Failed to create markdown summary file at location {}",
summary_file.display()
)
})?;
writeln!(file, "## smoke-tester").into_diagnostic()?;
for dut in &result.dut_tests {
let test_state = if dut.succesful { "Passed" } else { "Failed" };
writeln!(file, " - {}: {}", dut.name, test_state).into_diagnostic()?;
}
}
Ok(result.exit_code())
}
#[derive(Debug)]
struct DutReport {
name: String,
succesful: bool,
}
#[derive(Debug)]
struct TestReport {
dut_tests: Vec<DutReport>,
}
impl TestReport {
fn new() -> Self {
TestReport { dut_tests: vec![] }
}
fn add_report(&mut self, report: DutReport) {
self.dut_tests.push(report)
}
fn any_failed(&self) -> bool {
self.dut_tests.iter().any(|d| !d.succesful)
}
/// Return the appropriate exit code for the test result.
///
/// This is current 0, or success, if all tests have passed,
/// and the default failure exit code for the platform otherwise.
fn exit_code(&self) -> ExitCode {
if self.any_failed() {
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
}
}
fn num_failed_tests(&self) -> usize {
self.dut_tests.iter().filter(|d| !d.succesful).count()
}
fn num_tests(&self) -> usize {
self.dut_tests.len()
}
}
#[derive(Debug)]
struct ConsoleReportPrinter;
impl ConsoleReportPrinter {
fn print(
&self,
report: &TestReport,
mut writer: impl std::io::Write,
) -> Result<(), std::io::Error> {
writeln!(writer, "Test summary:")?;
for dut in &report.dut_tests {
if dut.succesful {
writeln!(writer, " [{}] passed", &dut.name)?;
} else {
writeln!(writer, " [{}] failed", &dut.name)?;
}
}
// Write summary
if report.any_failed() {
writeln!(
writer,
"{} out of {} tests failed.",
report.num_failed_tests(),
report.num_tests()
)?;
} else {
writeln!(writer, "All tests passed.")?;
}
Ok(())
}
}
#[derive(Debug)]
pub struct TestTracker<'dut> {
dut_definitions: &'dut [DutDefinition],
current_dut: usize,
current_test: usize,
}
impl<'dut> TestTracker<'dut> {
fn new(dut_definitions: &'dut [DutDefinition]) -> Self {
Self {
dut_definitions,
current_dut: 0,
current_test: 0,
}
}
fn advance_dut(&mut self) {
self.current_dut += 1;
self.current_test = 0;
}
fn current_dut(&self) -> usize {
self.current_dut + 1
}
fn current_dut_name(&self) -> &str {
&self.dut_definitions[self.current_dut].chip.name
}
fn num_duts(&self) -> usize {
self.dut_definitions.len()
}
fn current_test(&self) -> usize {
self.current_test + 1
}
fn advance_test(&mut self) {
self.current_test += 1;
}
pub fn current_target(&self) -> &probe_rs::Target {
&self.dut_definitions[self.current_dut].chip
}
pub fn current_dut_definition(&self) -> &DutDefinition {
&self.dut_definitions[self.current_dut]
}
#[must_use]
fn run(
&mut self,
handle_dut: impl Fn(&mut TestTracker, &DutDefinition) -> miette::Result<()> + Sync + Send,
) -> TestReport {
let mut report = TestReport::new();
let mut tests_ok = true;
for definition in self.dut_definitions {
print_dut_status!(self, blue, "Starting Test");
if let DefinitionSource::File(path) = &definition.source {
print!(" - {}", path.display());
}
println!();
let join_result =
std::thread::scope(|s| s.spawn(|| handle_dut(self, definition)).join());
match join_result {
Ok(Ok(())) => {
report.add_report(DutReport {
name: definition.chip.name.clone(),
succesful: true,
});
println_dut_status!(self, green, "Tests Passed");
}
Ok(Err(e)) => {
tests_ok = false;
report.add_report(DutReport {
name: definition.chip.name.clone(),
succesful: false,
});
println_dut_status!(self, red, "Error message: {:#}", e);
if let Some(source) = e.source() {
println_dut_status!(self, red, " caused by: {}", source);
}
println_dut_status!(self, red, "Tests Failed");
}
Err(_join_err) => {
tests_ok = false;
report.add_report(DutReport {
name: definition.chip.name.clone(),
succesful: false,
});
println_dut_status!(self, red, "Panic while running tests.");
}
}
self.advance_dut();
}
if tests_ok {
println_status!(self, green, "All DUTs passed.");
} else {
println_status!(self, red, "Some DUTs failed some tests.");
}
report
}
fn run_test(
&mut self,
test: impl FnOnce(&TestTracker) -> Result<(), TestFailure>,
) -> SingleTestReport {
let start_time = Instant::now();
let test_result = test(self);
let duration = start_time.elapsed();
let formatted_duration = if duration < Duration::from_secs(1) {
format!("{} ms", duration.as_millis())
} else {
format!("{:.2} s", duration.as_secs_f32())
};
match &test_result {
Ok(()) => {
println_test_status!(self, green, "Test passed in {formatted_duration}.");
}
Err(TestFailure::UnimplementedForTarget(target, message)) => {
println_test_status!(
self,
yellow,
"Test not implemented for {}: {}",
target.name,
message
);
}
Err(TestFailure::MissingResource(message)) => {
println_test_status!(self, yellow, "Missing resource for test: {}", message);
}
Err(_e) => {
println_test_status!(self, red, "Test failed in {formatted_duration}.");
}
};
self.advance_test();
SingleTestReport {
result: test_result,
_duration: duration,
}
}
}
/// A list of all tests which run on cores.
#[distributed_slice]
pub static CORE_TESTS: [fn(&TestTracker, &mut probe_rs::Core) -> TestResult];
/// A list of all tests which run on `Session`.
#[distributed_slice]
pub static SESSION_TESTS: [fn(&TestTracker, &mut probe_rs::Session) -> TestResult];