//! Test run and per-test detail storage, plus regression detection (tests that //! passed in the previous run for a target and fail in the current one). use super::{Result, SqlitePool, TestDetail, TestRun, TestRunId, TestSummary}; use tracing::instrument; #[instrument(skip_all)] pub async fn insert_test_run(pool: &SqlitePool, run: &TestRun) -> Result { let summary_json = serde_json::to_string(&run.summary).unwrap_or_default(); let result = sqlx::query( "INSERT INTO test_runs (target, started_at, finished_at, duration_secs, exit_code, passed, summary_json, raw_output, filter) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", ) .bind(&run.target) .bind(&run.started_at) .bind(&run.finished_at) .bind(run.duration_secs) .bind(run.exit_code) .bind(run.passed) .bind(&summary_json) .bind(&run.raw_output) .bind(&run.filter) .execute(pool) .await?; Ok(TestRunId(result.last_insert_rowid())) } #[instrument(skip_all)] pub async fn get_test_history( pool: &SqlitePool, target: Option<&str>, limit: i64, ) -> Result> { let rows = match target { Some(t) => { sqlx::query_as::<_, TestRunRow>( "SELECT id, target, started_at, finished_at, duration_secs, exit_code, passed, summary_json, raw_output, filter FROM test_runs WHERE target = ? ORDER BY id DESC LIMIT ?", ) .bind(t) .bind(limit) .fetch_all(pool) .await? } None => { sqlx::query_as::<_, TestRunRow>( "SELECT id, target, started_at, finished_at, duration_secs, exit_code, passed, summary_json, raw_output, filter FROM test_runs ORDER BY id DESC LIMIT ?", ) .bind(limit) .fetch_all(pool) .await? } }; Ok(rows.into_iter().map(TestRunRow::into_test_run).collect()) } #[instrument(skip_all)] pub async fn get_latest_test_run(pool: &SqlitePool, target: &str) -> Result> { let row = sqlx::query_as::<_, TestRunRow>( "SELECT id, target, started_at, finished_at, duration_secs, exit_code, passed, summary_json, raw_output, filter FROM test_runs WHERE target = ? ORDER BY id DESC LIMIT 1", ) .bind(target) .fetch_optional(pool) .await?; Ok(row.map(TestRunRow::into_test_run)) } /// Insert per-test results for a given test run. #[instrument(skip_all)] pub async fn insert_test_details( pool: &SqlitePool, run_id: TestRunId, details: &[TestDetail], ) -> Result<()> { // One transaction for the whole detail set: a crash mid-loop otherwise left a // partial set, which skews get_test_regressions (fuzz-2026-07-06 crash // atomicity). All-or-nothing. let mut tx = pool.begin().await?; for detail in details { sqlx::query("INSERT INTO test_details (run_id, test_name, passed) VALUES (?, ?, ?)") .bind(run_id.0) .bind(&detail.test_name) .bind(detail.passed) .execute(&mut *tx) .await?; } tx.commit().await?; Ok(()) } /// Find tests that passed in the previous run but failed in this one (regressions). #[instrument(skip_all)] pub async fn get_test_regressions( pool: &SqlitePool, target: &str, current_run_id: TestRunId, ) -> Result> { // Find the run immediately before this one for the same target let prev_run = sqlx::query_as::<_, (i64,)>( "SELECT id FROM test_runs WHERE target = ? AND id < ? ORDER BY id DESC LIMIT 1", ) .bind(target) .bind(current_run_id.0) .fetch_optional(pool) .await?; let Some((prev_id,)) = prev_run else { return Ok(vec![]); }; // Tests that passed in prev run but failed in current run let rows = sqlx::query_as::<_, (String,)>( "SELECT curr.test_name FROM test_details curr INNER JOIN test_details prev ON prev.test_name = curr.test_name AND prev.run_id = ? WHERE curr.run_id = ? AND curr.passed = 0 AND prev.passed = 1", ) .bind(prev_id) .bind(current_run_id.0) .fetch_all(pool) .await?; Ok(rows.into_iter().map(|r| r.0).collect()) } /// Get test duration history for a target (most recent first). #[instrument(skip_all)] pub async fn get_test_durations( pool: &SqlitePool, target: &str, limit: i64, ) -> Result> { let rows = sqlx::query_as::<_, (String, i64)>( "SELECT started_at, duration_secs FROM test_runs WHERE target = ? AND duration_secs IS NOT NULL ORDER BY id DESC LIMIT ?", ) .bind(target) .bind(limit) .fetch_all(pool) .await?; Ok(rows) } /// Get the version from the health check closest to (but before) a given timestamp. #[instrument(skip_all)] pub async fn get_version_at_time( pool: &SqlitePool, target: &str, before_rfc3339: &str, ) -> Result> { let row = sqlx::query_as::<_, (Option,)>( "SELECT details_json FROM health_checks WHERE target = ? AND checked_at <= ? ORDER BY checked_at DESC LIMIT 1", ) .bind(target) .bind(before_rfc3339) .fetch_optional(pool) .await?; let version = row .and_then(|r| r.0) .and_then(|json_str| serde_json::from_str::(&json_str).ok()) .and_then(|json| { json.get("version") .and_then(|v| v.as_str()) .map(String::from) }); Ok(version) } #[derive(sqlx::FromRow)] struct TestRunRow { id: i64, target: String, started_at: String, finished_at: Option, duration_secs: Option, exit_code: Option, passed: bool, summary_json: String, raw_output: String, filter: Option, } impl TestRunRow { fn into_test_run(self) -> TestRun { let summary = serde_json::from_str::(&self.summary_json).unwrap_or(TestSummary { steps: vec![], total_passed: None, total_failed: None, details: vec![], }); TestRun { id: Some(self.id), target: self.target, started_at: self.started_at, finished_at: self.finished_at, duration_secs: self.duration_secs, exit_code: self.exit_code, passed: self.passed, summary, raw_output: self.raw_output, filter: self.filter, } } }