use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use std::time::Duration;
use anyhow::{Context, Result};
use rusqlite::Connection;
use crate::coverage::{HitRange, CRATE_ROOT_SENTINEL_END};
use crate::fingerprint::FingerprintComponent;
use crate::project::LineRange;
const BUSY_TIMEOUT: Duration = Duration::from_secs(30);
fn translate_busy(err: rusqlite::Error, ctx: &'static str) -> anyhow::Error {
if matches!(
&err,
rusqlite::Error::SqliteFailure(e, _) if e.code == rusqlite::ErrorCode::DatabaseBusy
) {
anyhow::anyhow!(
"another cargo-affected process appears to be holding the \
database lock — try again in a moment"
)
} else {
anyhow::Error::from(err).context(ctx)
}
}
pub fn affected_dir(project_root: &Path) -> PathBuf {
project_root.join("target").join("affected")
}
pub fn db_path(project_root: &Path) -> PathBuf {
affected_dir(project_root).join("coverage.db")
}
const SCHEMA: &str = "\
CREATE TABLE IF NOT EXISTS meta (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS test_regions (
binary_id TEXT NOT NULL,
test_name TEXT NOT NULL,
source_file TEXT NOT NULL,
line_start INTEGER NOT NULL,
line_end INTEGER NOT NULL,
env_fingerprint TEXT NOT NULL,
collect_sha TEXT NOT NULL,
PRIMARY KEY (binary_id, test_name, source_file, line_start, line_end, env_fingerprint, collect_sha)
);
-- Range-overlap query: equality on (source_file, env_fingerprint, collect_sha)
-- plus a bound on line_start. The fifth column (line_end) lets the planner
-- skip rows once it has line_start beyond the hunk end. SQLite's composite
-- index can use equality + one range bound efficiently; the second range
-- bound is best-effort. Benchmark on real workspaces if status/run latency
-- matters.
CREATE INDEX IF NOT EXISTS idx_test_regions_lookup
ON test_regions(source_file, env_fingerprint, collect_sha, line_start, line_end);
CREATE TABLE IF NOT EXISTS fingerprints (
fingerprint TEXT PRIMARY KEY,
last_seen TEXT NOT NULL
);
-- Per-fingerprint per-input component hashes. Lets a 'fingerprint
-- mismatch' miss be diagnosed at the component level: 'this PR's
-- manifest:tests/helpers/wt-perf/Cargo.toml differs from every cached
-- fingerprint, but Cargo.lock and rustc match'. Composite hash in
-- env_fingerprint already encodes the same information; this is the
-- diagnostic decomposition.
CREATE TABLE IF NOT EXISTS fingerprint_components (
env_fingerprint TEXT NOT NULL,
label TEXT NOT NULL,
content_hash TEXT NOT NULL,
PRIMARY KEY (env_fingerprint, label)
);
";
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct TestId {
pub binary_id: String,
pub test_name: String,
}
impl TestId {
pub fn new(binary_id: impl Into<String>, test_name: impl Into<String>) -> Self {
Self {
binary_id: binary_id.into(),
test_name: test_name.into(),
}
}
}
#[derive(Debug, Clone)]
pub struct StoredFingerprintRow {
pub fingerprint: String,
pub last_seen: String,
pub components: Vec<FingerprintComponent>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TestHit {
pub test_id: TestId,
pub reason: HitReason,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HitReason {
pub collect_sha: String,
pub file: String,
pub kind: HitKind,
pub matched_hunk: (i64, i64),
pub stored_range: Option<(i64, i64)>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HitKind {
LineOverlap,
StructuralBackstop,
CrateRootSentinel,
ConfigRule,
}
pub const FINGERPRINT_KEEP: usize = 10;
fn touch_fingerprint(conn: &Connection, fingerprint: &str) -> Result<()> {
conn.execute(
"INSERT INTO fingerprints (fingerprint, last_seen) VALUES (?1, ?2) \
ON CONFLICT(fingerprint) DO UPDATE SET last_seen = excluded.last_seen",
rusqlite::params![fingerprint, chrono_free_timestamp()],
)?;
Ok(())
}
fn in_placeholders(start_idx: usize, count: usize) -> String {
(0..count)
.map(|i| format!("?{}", i + start_idx))
.collect::<Vec<_>>()
.join(", ")
}
fn batch_delete_tests<'a>(
tx: &rusqlite::Transaction<'_>,
fingerprint: &str,
tests: impl IntoIterator<Item = &'a TestId>,
) -> Result<()> {
let tests: Vec<&TestId> = tests.into_iter().collect();
if tests.is_empty() {
return Ok(());
}
const CHUNK: usize = 400;
for chunk in tests.chunks(CHUNK) {
let predicate = (0..chunk.len())
.map(|i| format!("(binary_id = ?{} AND test_name = ?{})", 2 + i * 2, 3 + i * 2))
.collect::<Vec<_>>()
.join(" OR ");
let sql = format!(
"DELETE FROM test_regions \
WHERE env_fingerprint = ?1 AND ({predicate})"
);
let params = std::iter::once(fingerprint).chain(
chunk
.iter()
.flat_map(|t| [t.binary_id.as_str(), t.test_name.as_str()]),
);
tx.execute(&sql, rusqlite::params_from_iter(params))?;
}
Ok(())
}
fn upsert_components(
tx: &rusqlite::Transaction<'_>,
fingerprint: &str,
components: &[FingerprintComponent],
) -> Result<()> {
tx.execute(
"DELETE FROM fingerprint_components WHERE env_fingerprint = ?1",
[fingerprint],
)?;
let mut stmt = tx.prepare(
"INSERT INTO fingerprint_components (env_fingerprint, label, content_hash) \
VALUES (?1, ?2, ?3)",
)?;
for component in components {
stmt.execute(rusqlite::params![
fingerprint,
component.label.as_str(),
component.hash.as_str(),
])?;
}
Ok(())
}
fn insert_mappings(
tx: &rusqlite::Transaction<'_>,
fingerprint: &str,
collect_sha: &str,
mappings: &[(TestId, BTreeSet<HitRange>)],
) -> Result<()> {
let mut stmt = tx.prepare(
"INSERT OR IGNORE INTO test_regions \
(binary_id, test_name, source_file, line_start, line_end, env_fingerprint, collect_sha) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
)?;
for (test_id, ranges) in mappings {
for range in ranges {
stmt.execute(rusqlite::params![
test_id.binary_id,
test_id.test_name,
range.file.as_str(),
range.line_start,
range.line_end,
fingerprint,
collect_sha,
])?;
}
}
Ok(())
}
pub struct Db {
conn: Connection,
}
impl Db {
pub fn open(project_root: &Path) -> Result<Self> {
let path = db_path(project_root);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("failed to create {}", parent.display()))?;
}
let conn = Connection::open(&path)
.with_context(|| format!("failed to open database at {}", path.display()))?;
conn.busy_timeout(BUSY_TIMEOUT)
.context("failed to configure SQLite busy_timeout")?;
conn.pragma_update(None, "journal_mode", "WAL")
.context("failed to set journal_mode=WAL")?;
conn.pragma_update(None, "synchronous", "NORMAL")
.context("failed to set synchronous=NORMAL")?;
migrate_legacy_tables(&conn)?;
conn.execute_batch(SCHEMA)
.map_err(|e| translate_busy(e, "failed to initialize database schema"))?;
Ok(Self { conn })
}
pub fn store_coverage(
&mut self,
fingerprint: &str,
components: &[FingerprintComponent],
collect_sha: &str,
mappings: &[(TestId, BTreeSet<HitRange>)],
) -> Result<()> {
let tx = self.conn.transaction()?;
tx.execute(
"DELETE FROM test_regions WHERE env_fingerprint = ?1",
[fingerprint],
)?;
insert_mappings(&tx, fingerprint, collect_sha, mappings)?;
upsert_components(&tx, fingerprint, components)?;
touch_fingerprint(&tx, fingerprint)?;
write_last_collected(&tx)?;
tx.commit()
.map_err(|e| translate_busy(e, "failed to commit coverage data"))?;
Ok(())
}
pub fn update_coverage_for_tests(
&mut self,
fingerprint: &str,
components: &[FingerprintComponent],
new_collect_sha: &str,
mappings: &[(TestId, BTreeSet<HitRange>)],
) -> Result<()> {
let tx = self.conn.transaction()?;
batch_delete_tests(&tx, fingerprint, mappings.iter().map(|(t, _)| t))?;
insert_mappings(&tx, fingerprint, new_collect_sha, mappings)?;
upsert_components(&tx, fingerprint, components)?;
touch_fingerprint(&tx, fingerprint)?;
write_last_collected(&tx)?;
tx.commit()
.map_err(|e| translate_busy(e, "failed to commit coverage update"))?;
Ok(())
}
pub fn prune_missing_tests(
&mut self,
fingerprint: &str,
present: &BTreeSet<TestId>,
) -> Result<usize> {
let stored: BTreeSet<TestId> = {
let mut stmt = self.conn.prepare(
"SELECT DISTINCT binary_id, test_name FROM test_regions \
WHERE env_fingerprint = ?1",
)?;
let rows: BTreeSet<TestId> = stmt
.query_map([fingerprint], |row| {
Ok(TestId::new(row.get::<_, String>(0)?, row.get::<_, String>(1)?))
})?
.collect::<rusqlite::Result<_>>()?;
rows
};
let to_drop: Vec<TestId> = stored.difference(present).cloned().collect();
if to_drop.is_empty() {
return Ok(0);
}
let tx = self.conn.transaction()?;
batch_delete_tests(&tx, fingerprint, to_drop.iter())?;
tx.commit()
.map_err(|e| translate_busy(e, "failed to commit prune"))?;
Ok(to_drop.len())
}
pub fn tests_covering_ranges(
&self,
fingerprint: &str,
collect_sha: &str,
file: &str,
hunks: &[LineRange],
) -> Result<Vec<TestHit>> {
let mut hits = Vec::new();
if hunks.is_empty() {
return Ok(hits);
}
let mut stmt = self.conn.prepare(
"SELECT binary_id, test_name, line_start, line_end FROM test_regions \
WHERE source_file = ?1 AND env_fingerprint = ?2 AND collect_sha = ?3",
)?;
let rows: Vec<(TestId, i64, i64)> = stmt
.query_map(rusqlite::params![file, fingerprint, collect_sha], |row| {
Ok((
TestId::new(row.get::<_, String>(0)?, row.get::<_, String>(1)?),
row.get::<_, i64>(2)?,
row.get::<_, i64>(3)?,
))
})?
.collect::<rusqlite::Result<_>>()?;
if rows.is_empty() {
return Ok(hits);
}
for hunk in hunks {
let mut overlapped = false;
for (test, ls, le) in &rows {
if *ls <= hunk.end && *le >= hunk.start {
let kind = if *le == CRATE_ROOT_SENTINEL_END && *ls == 1 {
HitKind::CrateRootSentinel
} else {
HitKind::LineOverlap
};
hits.push(TestHit {
test_id: test.clone(),
reason: HitReason {
collect_sha: collect_sha.to_string(),
file: file.to_string(),
kind,
matched_hunk: (hunk.start, hunk.end),
stored_range: Some((*ls, *le)),
},
});
overlapped = true;
}
}
if !overlapped {
let mut seen: BTreeSet<&TestId> = BTreeSet::new();
for (test, _, _) in &rows {
if !seen.insert(test) {
continue;
}
hits.push(TestHit {
test_id: test.clone(),
reason: HitReason {
collect_sha: collect_sha.to_string(),
file: file.to_string(),
kind: HitKind::StructuralBackstop,
matched_hunk: (hunk.start, hunk.end),
stored_range: None,
},
});
}
}
}
Ok(hits)
}
pub fn touch(&self, fingerprint: &str) -> Result<()> {
touch_fingerprint(&self.conn, fingerprint)
}
pub fn collect_shas(&self, fingerprint: &str) -> Result<BTreeSet<String>> {
let mut stmt = self.conn.prepare(
"SELECT DISTINCT collect_sha FROM test_regions WHERE env_fingerprint = ?1",
)?;
let shas = stmt
.query_map([fingerprint], |row| row.get::<_, String>(0))?
.collect::<rusqlite::Result<_>>()?;
Ok(shas)
}
pub fn test_count(&self, fingerprint: &str) -> Result<usize> {
let count: i64 = self.conn.query_row(
"SELECT COUNT(*) FROM \
(SELECT DISTINCT binary_id, test_name FROM test_regions WHERE env_fingerprint = ?1)",
[fingerprint],
|r| r.get(0),
)?;
Ok(count as usize)
}
pub fn region_count(&self, fingerprint: &str) -> Result<usize> {
let count: i64 = self.conn.query_row(
"SELECT COUNT(*) FROM test_regions WHERE env_fingerprint = ?1",
[fingerprint],
|r| r.get(0),
)?;
Ok(count as usize)
}
pub fn tracked_files_at_shas(
&self,
fingerprint: &str,
shas: &BTreeSet<String>,
) -> Result<BTreeSet<String>> {
if shas.is_empty() {
return Ok(BTreeSet::new());
}
let sql = format!(
"SELECT DISTINCT source_file FROM test_regions \
WHERE env_fingerprint = ?1 AND collect_sha IN ({})",
in_placeholders(2, shas.len()),
);
let mut stmt = self.conn.prepare(&sql)?;
let params = std::iter::once(fingerprint).chain(shas.iter().map(String::as_str));
let rows = stmt.query_map(rusqlite::params_from_iter(params), |row| {
row.get::<_, String>(0)
})?;
rows.collect::<rusqlite::Result<BTreeSet<String>>>()
.map_err(Into::into)
}
pub fn file_tracked(&self, fingerprint: &str, file: &str) -> Result<bool> {
let count: i64 = self.conn.query_row(
"SELECT COUNT(*) FROM test_regions \
WHERE env_fingerprint = ?1 AND source_file = ?2",
[fingerprint, file],
|r| r.get(0),
)?;
Ok(count > 0)
}
pub fn stored_fingerprint_snapshots(&self) -> Result<Vec<StoredFingerprintRow>> {
let mut stmt = self.conn.prepare(
"SELECT fingerprint, last_seen FROM fingerprints \
ORDER BY last_seen DESC, fingerprint ASC",
)?;
let mut fps: Vec<(String, String)> = stmt
.query_map([], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
})?
.collect::<rusqlite::Result<Vec<_>>>()?;
drop(stmt);
let mut out = Vec::with_capacity(fps.len());
let mut comp_stmt = self.conn.prepare(
"SELECT label, content_hash FROM fingerprint_components \
WHERE env_fingerprint = ?1 ORDER BY label",
)?;
for (fp, last_seen) in fps.drain(..) {
let components: Vec<FingerprintComponent> = comp_stmt
.query_map([&fp], |row| {
Ok(FingerprintComponent {
label: row.get::<_, String>(0)?,
hash: row.get::<_, String>(1)?,
})
})?
.collect::<rusqlite::Result<Vec<_>>>()?;
out.push(StoredFingerprintRow {
fingerprint: fp,
last_seen,
components,
});
}
Ok(out)
}
pub fn row_counts_by_sha(
&self,
fingerprint: &str,
) -> Result<BTreeMap<String, usize>> {
let mut stmt = self.conn.prepare(
"SELECT collect_sha, COUNT(*) FROM test_regions \
WHERE env_fingerprint = ?1 \
GROUP BY collect_sha",
)?;
let rows = stmt.query_map([fingerprint], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
})?;
let mut out = BTreeMap::new();
for r in rows {
let (sha, count) = r?;
out.insert(sha, count as usize);
}
Ok(out)
}
pub fn all_tests_for_fingerprint(
&self,
fingerprint: &str,
) -> Result<BTreeSet<TestId>> {
let mut stmt = self.conn.prepare(
"SELECT DISTINCT binary_id, test_name FROM test_regions \
WHERE env_fingerprint = ?1",
)?;
let rows = stmt.query_map([fingerprint], |row| {
Ok(TestId::new(row.get::<_, String>(0)?, row.get::<_, String>(1)?))
})?;
rows.collect::<rusqlite::Result<BTreeSet<TestId>>>()
.map_err(Into::into)
}
pub fn all_tests_at_shas(
&self,
fingerprint: &str,
shas: &BTreeSet<String>,
) -> Result<BTreeSet<TestId>> {
if shas.is_empty() {
return Ok(BTreeSet::new());
}
let sql = format!(
"SELECT DISTINCT binary_id, test_name FROM test_regions \
WHERE env_fingerprint = ?1 AND collect_sha IN ({})",
in_placeholders(2, shas.len()),
);
let mut stmt = self.conn.prepare(&sql)?;
let params = std::iter::once(fingerprint).chain(shas.iter().map(String::as_str));
let rows = stmt.query_map(rusqlite::params_from_iter(params), |row| {
Ok(TestId::new(row.get::<_, String>(0)?, row.get::<_, String>(1)?))
})?;
rows.collect::<rusqlite::Result<BTreeSet<TestId>>>()
.map_err(Into::into)
}
pub fn region_count_at_shas(
&self,
fingerprint: &str,
shas: &BTreeSet<String>,
) -> Result<usize> {
if shas.is_empty() {
return Ok(0);
}
let sql = format!(
"SELECT COUNT(*) FROM test_regions \
WHERE env_fingerprint = ?1 AND collect_sha IN ({})",
in_placeholders(2, shas.len()),
);
let params = std::iter::once(fingerprint).chain(shas.iter().map(String::as_str));
let count: i64 = self
.conn
.query_row(&sql, rusqlite::params_from_iter(params), |r| r.get(0))?;
Ok(count as usize)
}
#[cfg(test)]
pub fn has_any_coverage(&self) -> Result<bool> {
let count: i64 = self
.conn
.query_row("SELECT COUNT(*) FROM test_regions", [], |r| r.get(0))?;
Ok(count > 0)
}
pub fn clear(&mut self) -> Result<()> {
let tx = self
.conn
.transaction()
.map_err(|e| translate_busy(e, "failed to start clear transaction"))?;
tx.execute("DELETE FROM test_regions", [])?;
tx.execute("DELETE FROM fingerprints", [])?;
tx.execute("DELETE FROM fingerprint_components", [])?;
tx.execute("DELETE FROM meta", [])?;
tx.commit()
.map_err(|e| translate_busy(e, "failed to commit clear"))?;
Ok(())
}
pub fn gc(&mut self, current: &str, keep: usize) -> Result<usize> {
let tx = self
.conn
.transaction()
.map_err(|e| translate_busy(e, "failed to start gc transaction"))?;
let to_evict: Vec<String> = {
let mut stmt = tx.prepare(
"SELECT fingerprint FROM fingerprints \
WHERE fingerprint != ?1 \
ORDER BY last_seen DESC, fingerprint ASC \
LIMIT -1 OFFSET ?2",
)?;
let rows = stmt.query_map(
rusqlite::params![current, keep.saturating_sub(1) as i64],
|r| r.get::<_, String>(0),
)?;
rows.collect::<rusqlite::Result<Vec<String>>>()?
};
for fp in &to_evict {
tx.execute("DELETE FROM test_regions WHERE env_fingerprint = ?1", [fp])?;
tx.execute("DELETE FROM fingerprints WHERE fingerprint = ?1", [fp])?;
tx.execute(
"DELETE FROM fingerprint_components WHERE env_fingerprint = ?1",
[fp],
)?;
}
tx.commit()
.map_err(|e| translate_busy(e, "failed to commit gc"))?;
Ok(to_evict.len())
}
pub fn fingerprint_count(&self) -> Result<usize> {
let count: i64 = self
.conn
.query_row("SELECT COUNT(*) FROM fingerprints", [], |r| r.get(0))?;
Ok(count as usize)
}
pub fn last_collected(&self) -> Result<Option<String>> {
let result = self.conn.query_row(
"SELECT value FROM meta WHERE key = 'last_collected'",
[],
|r| r.get::<_, String>(0),
);
match result {
Ok(v) => Ok(Some(v)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e.into()),
}
}
}
pub fn warn_untracked_rs_files(
db: &Db,
fingerprint: &str,
changed_files: &[String],
) -> Result<()> {
for file in changed_files {
if file.ends_with(".rs") && !db.file_tracked(fingerprint, file)? {
eprintln!(
"warning: {file} has no coverage data \
— run `cargo affected collect` to include it"
);
}
}
Ok(())
}
fn migrate_legacy_tables(conn: &Connection) -> Result<()> {
conn.execute("DROP TABLE IF EXISTS test_files", [])?;
conn.execute("DROP INDEX IF EXISTS idx_source_file_fp", [])?;
if table_exists(conn, "test_regions")? {
let columns = table_columns(conn, "test_regions")?;
let needed = [
"binary_id",
"test_name",
"source_file",
"line_start",
"line_end",
"env_fingerprint",
"collect_sha",
];
if !needed.iter().all(|c| columns.iter().any(|col| col == c)) {
conn.execute("DROP TABLE test_regions", [])?;
conn.execute("DROP INDEX IF EXISTS idx_test_regions_lookup", [])?;
}
}
if table_exists(conn, "fingerprints")? {
let columns = table_columns(conn, "fingerprints")?;
if columns.iter().any(|c| c == "collect_sha") {
conn.execute("DROP TABLE fingerprints", [])?;
}
}
let coverage_needs_reset = if table_exists(conn, "fingerprint_components")? {
let columns = table_columns(conn, "fingerprint_components")?;
let needed = ["env_fingerprint", "label", "content_hash"];
!needed.iter().all(|c| columns.iter().any(|col| col == c))
} else {
table_exists(conn, "test_regions")?
};
if coverage_needs_reset {
drop_coverage_tables(conn)?;
}
if table_exists(conn, "test_regions")? && table_exists(conn, "fingerprint_components")? {
let orphan: i64 = conn.query_row(
"SELECT COUNT(*) FROM ( \
SELECT 1 FROM test_regions tr \
WHERE NOT EXISTS ( \
SELECT 1 FROM fingerprint_components fc \
WHERE fc.env_fingerprint = tr.env_fingerprint \
) LIMIT 1 \
)",
[],
|r| r.get(0),
)?;
if orphan > 0 {
drop_coverage_tables(conn)?;
}
}
Ok(())
}
fn drop_coverage_tables(conn: &Connection) -> Result<()> {
conn.execute("DROP TABLE IF EXISTS test_regions", [])?;
conn.execute("DROP INDEX IF EXISTS idx_test_regions_lookup", [])?;
conn.execute("DROP TABLE IF EXISTS fingerprints", [])?;
conn.execute("DROP TABLE IF EXISTS fingerprint_components", [])?;
Ok(())
}
fn table_exists(conn: &Connection, table: &str) -> Result<bool> {
let count: i64 = conn.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?1",
[table],
|r| r.get(0),
)?;
Ok(count > 0)
}
fn table_columns(conn: &Connection, table: &str) -> Result<Vec<String>> {
let sql = format!("PRAGMA table_info({table})");
let mut stmt = conn.prepare(&sql)?;
let cols: Vec<String> = stmt
.query_map([], |row| row.get::<_, String>(1))?
.collect::<std::result::Result<_, _>>()?;
Ok(cols)
}
fn write_last_collected(tx: &rusqlite::Transaction<'_>) -> Result<()> {
let timestamp = chrono_free_timestamp();
tx.execute(
"INSERT OR REPLACE INTO meta (key, value) VALUES ('last_collected', ?1)",
[×tamp],
)?;
Ok(())
}
fn chrono_free_timestamp() -> String {
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let time_of_day = secs % 86400;
let hours = time_of_day / 3600;
let minutes = (time_of_day % 3600) / 60;
let seconds = time_of_day % 60;
let (year, month, day) = days_to_civil(secs / 86400);
format!("{year:04}-{month:02}-{day:02}T{hours:02}:{minutes:02}:{seconds:02}Z")
}
fn days_to_civil(days: u64) -> (u64, u64, u64) {
let z = days + 719468;
let era = z / 146097;
let doe = z - era * 146097;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
(y, m, d)
}
#[cfg(test)]
mod tests {
use super::*;
use camino::Utf8PathBuf;
const FP_A: &str = "aaaaaaaa";
const FP_B: &str = "bbbbbbbb";
const SHA_A: &str = "0000000000000000000000000000000000000000";
const SHA_B: &str = "1111111111111111111111111111111111111111";
const BIN_A: &str = "crate_a";
const BIN_B: &str = "crate_b";
fn tid(binary_id: &str, test_name: &str) -> TestId {
TestId::new(binary_id, test_name)
}
fn rng(file: &str, line_start: i64, line_end: i64) -> HitRange {
HitRange {
file: Utf8PathBuf::from(file),
line_start,
line_end,
}
}
fn hunk(start: i64, end: i64) -> LineRange {
LineRange { start, end }
}
fn comps_for(fp: &str) -> Vec<FingerprintComponent> {
vec![FingerprintComponent {
label: "cargo_lock".to_string(),
hash: format!("hash-of-{fp}-cargo_lock"),
}]
}
fn ids(hits: Vec<TestHit>) -> BTreeSet<TestId> {
hits.into_iter().map(|h| h.test_id).collect()
}
#[test]
fn roundtrip_with_range_overlap() -> Result<()> {
let dir = tempfile::tempdir()?;
let mut db = Db::open(dir.path())?;
let a_ranges = BTreeSet::from([
rng("src/lib.rs", 10, 20),
rng("src/utils.rs", 5, 15),
]);
let b_ranges = BTreeSet::from([rng("src/lib.rs", 50, 60)]);
db.store_coverage(
FP_A,
&comps_for(FP_A),
SHA_A,
&[(tid(BIN_A, "test_a"), a_ranges), (tid(BIN_A, "test_b"), b_ranges)],
)?;
assert_eq!(db.test_count(FP_A)?, 2);
assert_eq!(db.region_count(FP_A)?, 3);
assert_eq!(db.collect_shas(FP_A)?, BTreeSet::from([SHA_A.to_string()]));
let hits = ids(db.tests_covering_ranges(FP_A, SHA_A, "src/lib.rs", &[hunk(15, 18)])?);
assert_eq!(hits, BTreeSet::from([tid(BIN_A, "test_a")]));
let hits = ids(db.tests_covering_ranges(FP_A, SHA_A, "src/lib.rs", &[hunk(55, 55)])?);
assert_eq!(hits, BTreeSet::from([tid(BIN_A, "test_b")]));
let hits = ids(
db.tests_covering_ranges(FP_A, SHA_A, "src/lib.rs", &[hunk(15, 18), hunk(55, 55)])?,
);
assert_eq!(
hits,
BTreeSet::from([tid(BIN_A, "test_a"), tid(BIN_A, "test_b")])
);
Ok(())
}
#[test]
fn structural_edit_backstop() -> Result<()> {
let dir = tempfile::tempdir()?;
let mut db = Db::open(dir.path())?;
db.store_coverage(
FP_A,
&comps_for(FP_A),
SHA_A,
&[
(tid(BIN_A, "test_a"), BTreeSet::from([rng("src/lib.rs", 10, 20)])),
(tid(BIN_A, "test_b"), BTreeSet::from([rng("src/lib.rs", 50, 60)])),
],
)?;
let hits = ids(db.tests_covering_ranges(FP_A, SHA_A, "src/lib.rs", &[hunk(25, 25)])?);
assert_eq!(
hits,
BTreeSet::from([tid(BIN_A, "test_a"), tid(BIN_A, "test_b")])
);
let hits = ids(db.tests_covering_ranges(
FP_A,
SHA_A,
"src/lib.rs",
&[hunk(15, 15), hunk(25, 25)],
)?);
assert_eq!(
hits,
BTreeSet::from([tid(BIN_A, "test_a"), tid(BIN_A, "test_b")])
);
Ok(())
}
#[test]
fn crate_root_sentinel_overlaps_any_hunk() -> Result<()> {
let dir = tempfile::tempdir()?;
let mut db = Db::open(dir.path())?;
let mappings = vec![
(
tid(BIN_A, "test_a"),
BTreeSet::from([HitRange::sentinel(Utf8PathBuf::from("src/lib.rs"))]),
),
(
tid(BIN_A, "test_b"),
BTreeSet::from([HitRange::sentinel(Utf8PathBuf::from("src/lib.rs"))]),
),
];
db.store_coverage(FP_A, &comps_for(FP_A), SHA_A, &mappings)?;
let hits = ids(db.tests_covering_ranges(FP_A, SHA_A, "src/lib.rs", &[hunk(7, 7)])?);
assert_eq!(
hits,
BTreeSet::from([tid(BIN_A, "test_a"), tid(BIN_A, "test_b")])
);
Ok(())
}
#[test]
fn different_fingerprint_reads_empty() -> Result<()> {
let dir = tempfile::tempdir()?;
let mut db = Db::open(dir.path())?;
db.store_coverage(
FP_A,
&comps_for(FP_A),
SHA_A,
&[(tid(BIN_A, "test_a"), BTreeSet::from([rng("src/lib.rs", 1, 10)]))],
)?;
assert_eq!(db.test_count(FP_B)?, 0);
assert_eq!(db.region_count(FP_B)?, 0);
assert!(db
.tests_covering_ranges(FP_B, SHA_A, "src/lib.rs", &[hunk(1, 5)])?
.is_empty()); assert!(!db.file_tracked(FP_B, "src/lib.rs")?);
assert!(db
.all_tests_at_shas(FP_B, &BTreeSet::from([SHA_A.to_string()]))?
.is_empty());
assert!(db.collect_shas(FP_B)?.is_empty());
assert!(db.has_any_coverage()?);
Ok(())
}
#[test]
fn full_collect_preserves_other_fingerprints() -> Result<()> {
let dir = tempfile::tempdir()?;
let mut db = Db::open(dir.path())?;
db.store_coverage(
FP_A,
&comps_for(FP_A),
SHA_A,
&[(tid(BIN_A, "test_a"), BTreeSet::from([rng("src/lib.rs", 1, 5)]))],
)?;
db.store_coverage(
FP_B,
&comps_for(FP_B),
SHA_B,
&[(tid(BIN_A, "test_b"), BTreeSet::from([rng("src/other.rs", 10, 15)]))],
)?;
db.store_coverage(
FP_A,
&comps_for(FP_A),
SHA_A,
&[(tid(BIN_A, "test_a"), BTreeSet::from([rng("src/new.rs", 1, 1)]))],
)?;
assert_eq!(db.test_count(FP_A)?, 1);
assert_eq!(db.test_count(FP_B)?, 1);
assert_eq!(
ids(db.tests_covering_ranges(FP_B, SHA_B, "src/other.rs", &[hunk(10, 12)])?),
BTreeSet::from([tid(BIN_A, "test_b")])
);
assert_eq!(
ids(db.tests_covering_ranges(FP_A, SHA_A, "src/new.rs", &[hunk(1, 1)])?),
BTreeSet::from([tid(BIN_A, "test_a")])
);
assert!(db
.tests_covering_ranges(FP_A, SHA_A, "src/lib.rs", &[hunk(1, 5)])?
.is_empty());
Ok(())
}
#[test]
fn same_test_name_in_different_binaries() -> Result<()> {
let dir = tempfile::tempdir()?;
let mut db = Db::open(dir.path())?;
db.store_coverage(
FP_A,
&comps_for(FP_A),
SHA_A,
&[
(tid(BIN_A, "builds"), BTreeSet::from([rng("crate_a/tests/builds.rs", 1, 5)])),
(tid(BIN_B, "builds"), BTreeSet::from([rng("crate_b/tests/builds.rs", 1, 5)])),
],
)?;
assert_eq!(db.test_count(FP_A)?, 2);
let a = ids(db.tests_covering_ranges(FP_A, SHA_A, "crate_a/tests/builds.rs", &[hunk(2, 3)])?);
assert_eq!(a, BTreeSet::from([tid(BIN_A, "builds")]));
let b = ids(db.tests_covering_ranges(FP_A, SHA_A, "crate_b/tests/builds.rs", &[hunk(2, 3)])?);
assert_eq!(b, BTreeSet::from([tid(BIN_B, "builds")]));
Ok(())
}
#[test]
fn clear_wipes_all_fingerprints() -> Result<()> {
let dir = tempfile::tempdir()?;
let mut db = Db::open(dir.path())?;
let mappings =
vec![(tid(BIN_A, "test_a"), BTreeSet::from([rng("src/lib.rs", 1, 5)]))];
db.store_coverage(FP_A, &comps_for(FP_A), SHA_A, &mappings)?;
db.store_coverage(FP_B, &comps_for(FP_B), SHA_B, &mappings)?;
assert!(db.has_any_coverage()?);
db.clear()?;
assert!(!db.has_any_coverage()?);
assert_eq!(db.test_count(FP_A)?, 0);
assert_eq!(db.test_count(FP_B)?, 0);
assert!(db.last_collected()?.is_none());
db.store_coverage(FP_A, &comps_for(FP_A), SHA_A, &mappings)?;
assert_eq!(db.test_count(FP_A)?, 1);
Ok(())
}
#[test]
fn gc_keeps_current_and_most_recent_others() -> Result<()> {
let dir = tempfile::tempdir()?;
let mut db = Db::open(dir.path())?;
let mappings =
vec![(tid(BIN_A, "t"), BTreeSet::from([rng("src/lib.rs", 1, 5)]))];
for fp in ["fp1", "fp2", "fp3", "fp4"] {
db.store_coverage(fp, &comps_for(fp), SHA_A, &mappings)?;
db.conn.execute(
"UPDATE fingerprints SET last_seen = ?2 WHERE fingerprint = ?1",
rusqlite::params![fp, format!("2020-01-01T00:00:{:02}Z", fp.as_bytes()[2] - b'0')],
)?;
}
assert_eq!(db.fingerprint_count()?, 4);
let evicted = db.gc("fp4", 2)?;
assert_eq!(evicted, 2);
assert_eq!(db.fingerprint_count()?, 2);
assert_eq!(db.test_count("fp1")?, 0);
assert_eq!(db.test_count("fp2")?, 0);
assert_eq!(db.test_count("fp3")?, 1);
assert_eq!(db.test_count("fp4")?, 1);
Ok(())
}
#[test]
fn pre_range_schema_is_dropped() -> Result<()> {
let dir = tempfile::tempdir()?;
let path = db_path(dir.path());
std::fs::create_dir_all(path.parent().unwrap())?;
{
let conn = Connection::open(&path)?;
conn.execute_batch(
"\
CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
CREATE TABLE test_files (
binary_id TEXT NOT NULL,
test_name TEXT NOT NULL,
source_file TEXT NOT NULL,
env_fingerprint TEXT NOT NULL,
PRIMARY KEY (binary_id, test_name, source_file, env_fingerprint)
);
INSERT INTO test_files VALUES ('bin1', 't1', 'src/lib.rs', 'fp_x');
CREATE TABLE fingerprints (
fingerprint TEXT PRIMARY KEY,
last_seen TEXT NOT NULL
);
INSERT INTO fingerprints VALUES ('fp_x', '2020-01-01T00:00:00Z');
",
)?;
}
let db = Db::open(dir.path())?;
assert!(!db.has_any_coverage()?);
Ok(())
}
#[test]
fn pre_diff_schema_is_dropped() -> Result<()> {
let dir = tempfile::tempdir()?;
let path = db_path(dir.path());
std::fs::create_dir_all(path.parent().unwrap())?;
{
let conn = Connection::open(&path)?;
conn.execute_batch(
"\
CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
CREATE TABLE test_regions (
binary_id TEXT NOT NULL,
test_name TEXT NOT NULL,
source_file TEXT NOT NULL,
line_start INTEGER NOT NULL,
line_end INTEGER NOT NULL,
env_fingerprint TEXT NOT NULL,
PRIMARY KEY (binary_id, test_name, source_file, line_start, line_end, env_fingerprint)
);
INSERT INTO test_regions VALUES ('bin1', 't1', 'src/lib.rs', 1, 5, 'fp_x');
CREATE TABLE fingerprints (
fingerprint TEXT PRIMARY KEY,
last_seen TEXT NOT NULL,
collect_sha TEXT
);
INSERT INTO fingerprints VALUES ('fp_x', '2020-01-01T00:00:00Z', 'deadbeef');
",
)?;
}
let db = Db::open(dir.path())?;
assert!(!db.has_any_coverage()?);
assert_eq!(db.fingerprint_count()?, 0);
Ok(())
}
#[test]
fn diff_update_replaces_only_rerun_tests() -> Result<()> {
let dir = tempfile::tempdir()?;
let mut db = Db::open(dir.path())?;
db.store_coverage(
FP_A,
&comps_for(FP_A),
SHA_A,
&[
(tid(BIN_A, "test_a"), BTreeSet::from([rng("src/a.rs", 1, 5)])),
(tid(BIN_A, "test_b"), BTreeSet::from([rng("src/b.rs", 1, 5)])),
(tid(BIN_A, "test_c"), BTreeSet::from([rng("src/c.rs", 1, 5)])),
],
)?;
assert_eq!(db.collect_shas(FP_A)?, BTreeSet::from([SHA_A.to_string()]));
db.update_coverage_for_tests(
FP_A,
&comps_for(FP_A),
SHA_B,
&[(tid(BIN_A, "test_a"), BTreeSet::from([rng("src/a.rs", 10, 20)]))],
)?;
assert_eq!(
db.collect_shas(FP_A)?,
BTreeSet::from([SHA_A.to_string(), SHA_B.to_string()]),
);
assert_eq!(
ids(db.tests_covering_ranges(FP_A, SHA_B, "src/a.rs", &[hunk(15, 15)])?),
BTreeSet::from([tid(BIN_A, "test_a")]),
);
assert!(db
.tests_covering_ranges(FP_A, SHA_A, "src/a.rs", &[hunk(1, 5)])?
.is_empty());
assert_eq!(
ids(db.tests_covering_ranges(FP_A, SHA_A, "src/b.rs", &[hunk(1, 5)])?),
BTreeSet::from([tid(BIN_A, "test_b")]),
);
assert_eq!(
ids(db.tests_covering_ranges(FP_A, SHA_A, "src/c.rs", &[hunk(1, 5)])?),
BTreeSet::from([tid(BIN_A, "test_c")]),
);
Ok(())
}
#[test]
fn diff_prune_drops_absent_tests() -> Result<()> {
let dir = tempfile::tempdir()?;
let mut db = Db::open(dir.path())?;
db.store_coverage(
FP_A,
&comps_for(FP_A),
SHA_A,
&[
(tid(BIN_A, "test_a"), BTreeSet::from([rng("src/a.rs", 1, 5)])),
(tid(BIN_A, "test_b"), BTreeSet::from([rng("src/b.rs", 1, 5)])),
(tid(BIN_A, "test_c"), BTreeSet::from([rng("src/c.rs", 1, 5)])),
],
)?;
let present = BTreeSet::from([tid(BIN_A, "test_a"), tid(BIN_A, "test_c")]);
let evicted = db.prune_missing_tests(FP_A, &present)?;
assert_eq!(evicted, 1);
assert_eq!(db.test_count(FP_A)?, 2);
assert!(db
.tests_covering_ranges(FP_A, SHA_A, "src/b.rs", &[hunk(1, 5)])?
.is_empty());
assert_eq!(
ids(db.tests_covering_ranges(FP_A, SHA_A, "src/a.rs", &[hunk(1, 5)])?),
BTreeSet::from([tid(BIN_A, "test_a")]),
);
Ok(())
}
#[test]
fn region_count_scoped_by_shas() -> Result<()> {
let dir = tempfile::tempdir()?;
let mut db = Db::open(dir.path())?;
db.store_coverage(
FP_A,
&comps_for(FP_A),
SHA_A,
&[
(tid(BIN_A, "ta"), BTreeSet::from([rng("src/a.rs", 1, 5)])),
(tid(BIN_A, "tb"), BTreeSet::from([rng("src/b.rs", 1, 5)])),
],
)?;
db.update_coverage_for_tests(
FP_A,
&comps_for(FP_A),
SHA_B,
&[(tid(BIN_A, "ta"), BTreeSet::from([rng("src/a.rs", 10, 20)]))],
)?;
assert_eq!(
db.region_count_at_shas(FP_A, &BTreeSet::from([SHA_A.to_string()]))?,
1,
);
assert_eq!(
db.region_count_at_shas(FP_A, &BTreeSet::from([SHA_B.to_string()]))?,
1,
);
assert_eq!(
db.region_count_at_shas(
FP_A,
&BTreeSet::from([SHA_A.to_string(), SHA_B.to_string()]),
)?,
2,
);
assert_eq!(db.region_count_at_shas(FP_A, &BTreeSet::new())?, 0);
assert_eq!(
db.region_count_at_shas(FP_B, &BTreeSet::from([SHA_A.to_string()]))?,
0,
);
Ok(())
}
fn read_components(db: &Db, fingerprint: &str) -> Vec<(String, String)> {
let mut stmt = db
.conn
.prepare(
"SELECT label, content_hash FROM fingerprint_components \
WHERE env_fingerprint = ?1 ORDER BY label",
)
.unwrap();
stmt.query_map([fingerprint], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
})
.unwrap()
.collect::<rusqlite::Result<Vec<_>>>()
.unwrap()
}
#[test]
fn store_coverage_writes_components() -> Result<()> {
let dir = tempfile::tempdir()?;
let mut db = Db::open(dir.path())?;
let comps = vec![
FingerprintComponent {
label: "cargo_lock".to_string(),
hash: "h-lock".to_string(),
},
FingerprintComponent {
label: "rustc".to_string(),
hash: "h-rustc".to_string(),
},
];
db.store_coverage(
FP_A,
&comps,
SHA_A,
&[(tid(BIN_A, "test_a"), BTreeSet::from([rng("src/lib.rs", 1, 5)]))],
)?;
assert_eq!(
read_components(&db, FP_A),
vec![
("cargo_lock".to_string(), "h-lock".to_string()),
("rustc".to_string(), "h-rustc".to_string()),
],
);
Ok(())
}
#[test]
fn store_coverage_replaces_components_idempotently() -> Result<()> {
let dir = tempfile::tempdir()?;
let mut db = Db::open(dir.path())?;
let mappings =
vec![(tid(BIN_A, "t"), BTreeSet::from([rng("src/lib.rs", 1, 5)]))];
db.store_coverage(
FP_A,
&[FingerprintComponent {
label: "cargo_lock".to_string(),
hash: "first".to_string(),
}],
SHA_A,
&mappings,
)?;
db.store_coverage(
FP_A,
&[FingerprintComponent {
label: "cargo_lock".to_string(),
hash: "second".to_string(),
}],
SHA_A,
&mappings,
)?;
assert_eq!(
read_components(&db, FP_A),
vec![("cargo_lock".to_string(), "second".to_string())],
);
Ok(())
}
#[test]
fn gc_evicts_components() -> Result<()> {
let dir = tempfile::tempdir()?;
let mut db = Db::open(dir.path())?;
let mappings =
vec![(tid(BIN_A, "t"), BTreeSet::from([rng("src/lib.rs", 1, 5)]))];
for fp in ["fp1", "fp2", "fp3", "fp4"] {
db.store_coverage(fp, &comps_for(fp), SHA_A, &mappings)?;
db.conn.execute(
"UPDATE fingerprints SET last_seen = ?2 WHERE fingerprint = ?1",
rusqlite::params![fp, format!("2020-01-01T00:00:{:02}Z", fp.as_bytes()[2] - b'0')],
)?;
}
for fp in ["fp1", "fp2", "fp3", "fp4"] {
assert_eq!(read_components(&db, fp).len(), 1);
}
db.gc("fp4", 2)?;
assert!(read_components(&db, "fp1").is_empty());
assert!(read_components(&db, "fp2").is_empty());
assert_eq!(read_components(&db, "fp3").len(), 1);
assert_eq!(read_components(&db, "fp4").len(), 1);
Ok(())
}
#[test]
fn clear_wipes_components() -> Result<()> {
let dir = tempfile::tempdir()?;
let mut db = Db::open(dir.path())?;
db.store_coverage(
FP_A,
&comps_for(FP_A),
SHA_A,
&[(tid(BIN_A, "t"), BTreeSet::from([rng("src/lib.rs", 1, 5)]))],
)?;
assert!(!read_components(&db, FP_A).is_empty());
db.clear()?;
assert!(read_components(&db, FP_A).is_empty());
Ok(())
}
#[test]
fn migration_resets_pre_components_db() -> Result<()> {
let dir = tempfile::tempdir()?;
let path = db_path(dir.path());
std::fs::create_dir_all(path.parent().unwrap())?;
{
let conn = Connection::open(&path)?;
conn.execute_batch(
"\
CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
CREATE TABLE test_regions (
binary_id TEXT NOT NULL,
test_name TEXT NOT NULL,
source_file TEXT NOT NULL,
line_start INTEGER NOT NULL,
line_end INTEGER NOT NULL,
env_fingerprint TEXT NOT NULL,
collect_sha TEXT NOT NULL,
PRIMARY KEY (binary_id, test_name, source_file, line_start, line_end, env_fingerprint, collect_sha)
);
INSERT INTO test_regions VALUES ('bin1', 't1', 'src/lib.rs', 1, 5, 'old_fp', 'sha');
CREATE TABLE fingerprints (
fingerprint TEXT PRIMARY KEY,
last_seen TEXT NOT NULL
);
INSERT INTO fingerprints VALUES ('old_fp', '2020-01-01T00:00:00Z');
",
)?;
}
let db = Db::open(dir.path())?;
assert!(!db.has_any_coverage()?);
assert_eq!(db.fingerprint_count()?, 0);
Ok(())
}
#[test]
fn migration_resets_on_components_column_mismatch() -> Result<()> {
let dir = tempfile::tempdir()?;
let path = db_path(dir.path());
std::fs::create_dir_all(path.parent().unwrap())?;
{
let conn = Connection::open(&path)?;
conn.execute_batch(
"\
CREATE TABLE test_regions (
binary_id TEXT NOT NULL,
test_name TEXT NOT NULL,
source_file TEXT NOT NULL,
line_start INTEGER NOT NULL,
line_end INTEGER NOT NULL,
env_fingerprint TEXT NOT NULL,
collect_sha TEXT NOT NULL,
PRIMARY KEY (binary_id, test_name, source_file, line_start, line_end, env_fingerprint, collect_sha)
);
INSERT INTO test_regions VALUES ('bin1', 't1', 'src/lib.rs', 1, 5, 'fp', 'sha');
CREATE TABLE fingerprint_components (
env_fingerprint TEXT NOT NULL,
label_OLD TEXT NOT NULL, -- wrong column name
content_hash TEXT NOT NULL,
PRIMARY KEY (env_fingerprint, label_OLD)
);
",
)?;
}
let db = Db::open(dir.path())?;
assert!(!db.has_any_coverage()?);
Ok(())
}
#[test]
fn migration_resets_on_orphan_test_regions_rows() -> Result<()> {
let dir = tempfile::tempdir()?;
let path = db_path(dir.path());
std::fs::create_dir_all(path.parent().unwrap())?;
{
let conn = Connection::open(&path)?;
conn.execute_batch(
"\
CREATE TABLE test_regions (
binary_id TEXT NOT NULL,
test_name TEXT NOT NULL,
source_file TEXT NOT NULL,
line_start INTEGER NOT NULL,
line_end INTEGER NOT NULL,
env_fingerprint TEXT NOT NULL,
collect_sha TEXT NOT NULL,
PRIMARY KEY (binary_id, test_name, source_file, line_start, line_end, env_fingerprint, collect_sha)
);
CREATE TABLE fingerprint_components (
env_fingerprint TEXT NOT NULL,
label TEXT NOT NULL,
content_hash TEXT NOT NULL,
PRIMARY KEY (env_fingerprint, label)
);
INSERT INTO test_regions VALUES ('bin1', 't1', 'src/lib.rs', 1, 5, 'orphan_fp', 'sha');
-- No matching fingerprint_components row for orphan_fp.
INSERT INTO fingerprint_components VALUES ('different_fp', 'cargo_lock', 'h');
",
)?;
}
let db = Db::open(dir.path())?;
assert!(!db.has_any_coverage()?);
Ok(())
}
#[test]
fn hits_classify_line_overlap() -> Result<()> {
let dir = tempfile::tempdir()?;
let mut db = Db::open(dir.path())?;
db.store_coverage(
FP_A,
&comps_for(FP_A),
SHA_A,
&[(tid(BIN_A, "test_a"), BTreeSet::from([rng("src/lib.rs", 10, 20)]))],
)?;
let hits = db.tests_covering_ranges(FP_A, SHA_A, "src/lib.rs", &[hunk(15, 18)])?;
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].test_id, tid(BIN_A, "test_a"));
assert_eq!(hits[0].reason.kind, HitKind::LineOverlap);
assert_eq!(hits[0].reason.file, "src/lib.rs");
assert_eq!(hits[0].reason.collect_sha, SHA_A);
assert_eq!(hits[0].reason.matched_hunk, (15, 18));
assert_eq!(hits[0].reason.stored_range, Some((10, 20)));
Ok(())
}
#[test]
fn hits_classify_crate_root_sentinel() -> Result<()> {
let dir = tempfile::tempdir()?;
let mut db = Db::open(dir.path())?;
db.store_coverage(
FP_A,
&comps_for(FP_A),
SHA_A,
&[(
tid(BIN_A, "test_a"),
BTreeSet::from([HitRange::sentinel(Utf8PathBuf::from("src/lib.rs"))]),
)],
)?;
let hits = db.tests_covering_ranges(FP_A, SHA_A, "src/lib.rs", &[hunk(7, 7)])?;
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].reason.kind, HitKind::CrateRootSentinel);
assert_eq!(hits[0].reason.stored_range, Some((1, CRATE_ROOT_SENTINEL_END)));
Ok(())
}
#[test]
fn hits_classify_structural_backstop_dedupes_per_test() -> Result<()> {
let dir = tempfile::tempdir()?;
let mut db = Db::open(dir.path())?;
db.store_coverage(
FP_A,
&comps_for(FP_A),
SHA_A,
&[
(
tid(BIN_A, "test_a"),
BTreeSet::from([
rng("src/lib.rs", 10, 15),
rng("src/lib.rs", 30, 35),
]),
),
(tid(BIN_A, "test_b"), BTreeSet::from([rng("src/lib.rs", 50, 55)])),
],
)?;
let hits = db.tests_covering_ranges(FP_A, SHA_A, "src/lib.rs", &[hunk(25, 25)])?;
assert_eq!(hits.len(), 2, "one hit per unique test, not per row");
for hit in &hits {
assert_eq!(hit.reason.kind, HitKind::StructuralBackstop);
assert_eq!(hit.reason.stored_range, None);
assert_eq!(hit.reason.matched_hunk, (25, 25));
}
let unique: BTreeSet<TestId> = hits.iter().map(|h| h.test_id.clone()).collect();
assert_eq!(
unique,
BTreeSet::from([tid(BIN_A, "test_a"), tid(BIN_A, "test_b")])
);
Ok(())
}
}