use std::path::{Path, PathBuf};
use defmt_json_schema::v1::JsonFrame;
use regex::Regex;
use time::OffsetDateTime;
use crate::db::{DbError, MantraDb};
#[derive(Debug, Clone, clap::Args)]
pub struct CliConfig {
pub data_file: PathBuf,
#[command(flatten)]
pub cfg: Config,
}
#[derive(Debug, Clone, clap::Args)]
pub struct Config {
#[arg(long)]
pub test_run: String,
#[arg(value_enum)]
pub fmt: CoverageFormat,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct TestRunConfig {
pub name: String,
#[serde(serialize_with = "time::serde::iso8601::serialize")]
pub date: time::OffsetDateTime,
}
pub fn iso8601_str_to_offsetdatetime(time_str: &str) -> OffsetDateTime {
OffsetDateTime::parse(
time_str,
&time::format_description::well_known::Iso8601::DEFAULT,
)
.expect("Test run date was added to db in ISO8601 format.")
}
#[derive(Debug, Clone, clap::ValueEnum)]
pub enum CoverageFormat {
DefmtJson,
}
#[derive(Debug, thiserror::Error)]
pub enum CoverageError {
#[error("{}", .0)]
ReadingData(String),
#[error("{}", .0)]
DefmtJson(String),
#[error("{}", .0)]
Db(DbError),
}
pub async fn coverage_from_path(
data: &Path,
db: &MantraDb,
cfg: &Config,
) -> Result<(), CoverageError> {
let data_str = std::fs::read_to_string(data).map_err(|_| {
CoverageError::ReadingData(format!(
"Could not read coverage data from '{}'.",
data.display()
))
})?;
coverage_from_str(&data_str, db, cfg).await
}
pub async fn coverage_from_str(
data: &str,
db: &MantraDb,
cfg: &Config,
) -> Result<(), CoverageError> {
match cfg.fmt {
CoverageFormat::DefmtJson => {
let mut frames = Vec::new();
for line in data.lines() {
frames.push(serde_json::from_str::<JsonFrame>(line).map_err(|err| {
CoverageError::DefmtJson(format!(
"Could not extract defmt log frame from line '{}'. Cause: {}",
line, err
))
})?);
}
coverage_from_defmt_frames(&frames, db, &cfg.test_run).await
}
}
}
pub async fn coverage_from_defmt_frames(
frames: &[JsonFrame],
db: &MantraDb,
test_run_name: &str,
) -> Result<(), CoverageError> {
let mut current_test_fn: Option<String> = None;
let test_run = TestRunConfig {
name: test_run_name.to_string(),
date: OffsetDateTime::now_utc(),
};
db.add_test_run(
&test_run,
&serde_json::to_string(frames)
.expect("Serializing log frames must work, because frames were deserialized."),
)
.await
.map_err(CoverageError::Db)?;
for frame in frames {
let new_test_fn = add_frame_to_db(frame, db, &test_run, current_test_fn.as_deref()).await?;
if current_test_fn != new_test_fn || frame.data.to_lowercase() == "all tests passed!" {
if let Some(passed_test_fn) = ¤t_test_fn {
db.test_passed(&test_run, passed_test_fn)
.await
.map_err(CoverageError::Db)?;
}
}
current_test_fn = new_test_fn;
}
Ok(())
}
async fn add_frame_to_db(
frame: &JsonFrame,
db: &MantraDb,
test_run: &TestRunConfig,
current_test_fn: Option<&str>,
) -> Result<Option<String>, CoverageError> {
let test_fn_matcher = TEST_FN_MATCHER.get_or_init(|| {
Regex::new(
r"^\(\d+/(?<nr_tests>\d+)\)\s(?<state>(?:running)|(?:ignoring))\s`(?<fn_name>.+)`...",
)
.expect("Could not create regex matcher for defmt test-fn entries.")
});
let mut new_test_fn: Option<String> = current_test_fn.map(|t| t.to_string());
if let Some(captured_test_fn) = test_fn_matcher.captures(&frame.data) {
let nr_tests: u32 = captured_test_fn
.name("nr_tests")
.expect("Number of tests from the test-fn was not captured.")
.as_str()
.parse()
.expect("Number of tests must be convertible to u32.");
let fn_state = captured_test_fn
.name("state")
.expect("State of the test-fn was not captured.");
let fn_name = captured_test_fn
.name("fn_name")
.expect("Name of the test-fn was not captured.");
let Some(file) = &frame.location.file else {
return Err(CoverageError::DefmtJson(format!(
"Missing file location information for log entry '{}'.",
frame.data
)));
};
let Some(line_nr) = frame.location.line else {
return Err(CoverageError::DefmtJson(format!(
"Missing line location information for log entry '{}'.",
frame.data
)));
};
let Some(mod_path) = &frame.location.module_path else {
return Err(CoverageError::DefmtJson(format!(
"Missing line location information for log entry '{}'.",
frame.data
)));
};
let mod_path_str = format!(
"{}{}",
mod_path.crate_name,
if mod_path.modules.is_empty() {
String::new()
} else {
format!("::{}", mod_path.modules.join("::"))
}
);
match fn_state.as_str() {
"running" => {
let test_fn_name = format!("{}::{}", mod_path_str, fn_name.as_str());
new_test_fn = Some(test_fn_name.clone());
db.update_nr_of_tests(test_run, nr_tests)
.await.map_err(CoverageError::Db)?;
db.add_test(
test_run,
&test_fn_name,
&PathBuf::from(file),
line_nr,
None, )
.await.map_err(CoverageError::Db)?;
}
"ignoring" => {
let test_fn_name = format!("{}::{}", mod_path_str, fn_name.as_str());
db.update_nr_of_tests(test_run, nr_tests)
.await.map_err(CoverageError::Db)?;
db.add_skipped_test(
test_run,
&test_fn_name,
&PathBuf::from(file),
line_nr,
None, )
.await.map_err(CoverageError::Db)?;
new_test_fn = None;
}
_ => unreachable!("Invalid state '{}' for test function '{}' in log entry '{}'. Only 'running' and 'ignoring' are allowed.", fn_state.as_str(), fn_name.as_str(), frame.data),
}
} else if let Some(covered_req) =
mantra_rust_macros::extract::extract_first_coverage(&frame.data)
{
if let Some(current_test) = current_test_fn {
let db_result = db
.add_coverage(
test_run,
current_test,
&covered_req.file,
covered_req.line,
&covered_req.id,
)
.await;
if let Err(DbError::ForeignKeyViolation(_)) = &db_result {
log::debug!(
"Skipping unrelated coverage for reg-id=`{}`, file='{}', line='{}'.",
covered_req.id,
covered_req.file.display(),
covered_req.line
);
} else {
db_result.map_err(CoverageError::Db)?;
}
};
}
Ok(new_test_fn)
}
static TEST_FN_MATCHER: std::sync::OnceLock<Regex> = std::sync::OnceLock::new();