Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

src/warehouse/coverage.rs

//! Iceberg writer + reader for **source-based coverage** (feature N — maven
//! style): the `coverage` (overall + per-crate + per-file rollup) and
//! `coverage_fn` (per-function, boundary-tagged) tables.
//!
//! Mirrors [`super::surface_coverage`] / [`super::test_results`]: one Iceberg
//! snapshot per `nornir coverage` run, all rows sharing one `(run_id,
//! ts_micros)`, historized so coverage is time-travellable and feeds the docs
//! `nornir:gen:coverage` table + the viz 🧪 Test-pane coverage panel.
//!
//! The PURE model — [`crate::coverage::CoverageReport`] + the percentage / rollup
//! / boundary logic — lives in [`crate::coverage`]; this module only adds the
//! `CoverageReport → rows → warehouse → rows → panel data` write/read seam.

use std::sync::Arc;

use anyhow::{Result, anyhow};
use arrow::array::{Array, Int32Array, Int64Array, RecordBatch, StringArray, TimestampMicrosecondArray};
use iceberg::Catalog;
use iceberg::arrow::schema_to_arrow_schema;

use super::iceberg::{
    IcebergWarehouse, TABLE_COVERAGE, TABLE_COVERAGE_FN, append_batch, ensure_table_schema,
};
use crate::coverage::{Boundary, CoverageReport, CrateCoverage, FileCoverage, FnCoverage};

// ─── persisted row shapes ───────────────────────────────────────────────────

/// One `coverage` row: an overall / per-crate / per-file rollup.
///
/// The struct itself now lives in the `nornir-coverage` leaf (a plain serde POD
/// whose only logic is `pct_i64`, which already lived there) so the
/// warehouse-free doc assembly (`nornir-docgen`'s coverage section) can accept
/// pre-fetched rows without linking this warehouse. Re-exported here unchanged —
/// every `crate::warehouse::coverage::CoverageRow` call site resolves as before.
///
/// `serde` derives let the server ship the exact rows to a thin viz client over
/// `Viz.Coverage` and the client decode them into the identical struct (the same
/// fat/thin parity pattern as [`super::release_events::ReleaseEventRow`]).
pub use nornir_coverage::CoverageRow;

/// One `coverage_fn` row: a boundary-tagged function's coverage.
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct CoverageFnRow {
    pub run_id: String,
    pub ts_micros: i64,
    pub repo: String,
    pub name: String,
    pub file: String,
    /// `ui` | `grpc` | `emitter` | `core`.
    pub boundary: String,
    pub lines: i64,
    pub lines_covered: i64,
    pub regions: i64,
    pub regions_covered: i64,
    /// The function's first source line (`0` = the export named none, or a row
    /// written before the column) — with `file`, the key the Mímir code room's
    /// functions join on.
    #[serde(default)]
    pub line: u32,
    /// The function's last source line.
    #[serde(default)]
    pub end_line: u32,
    /// The checkout's HEAD the run measured (`""` = not known).
    #[serde(default)]
    pub commit: String,
    /// An `only: Affected` run, merged ([`merge_affected`]): the commit whose
    /// run measured these counts — the function did not change since, so the
    /// selected tests did not run it again (`""` = measured by THIS run).
    #[serde(default)]
    pub carried_from: String,
    /// An `only: Affected` run: did the function change since the baseline?
    /// (`None` = a run of every test, which does not say.)
    #[serde(default)]
    pub changed: Option<bool>,
}

impl CoverageFnRow {
    pub fn exercised(&self) -> bool {
        self.regions_covered > 0 || self.lines_covered > 0
    }
    pub fn is_boundary(&self) -> bool {
        self.boundary != Boundary::Core.as_str()
    }

    /// A function that changed since the baseline and that no selected test
    /// executed — what an `only: Affected` coverage run flags.
    pub fn changed_uncovered(&self) -> bool {
        self.changed == Some(true) && !self.exercised()
    }
}

/// One changed item an `only: Affected` run was told about (`Test.Affected`'s
/// `changed`): where it is at the measured commit.
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ChangedSpan {
    pub file: String,
    pub line: u32,
    pub end_line: u32,
    #[serde(default)]
    pub fqn: String,
}

/// What [`merge_affected`] did.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Merged {
    /// Rows whose counts came from the previous run (unchanged functions).
    pub carried: usize,
    /// Rows measured by this run (changed functions, and ones the previous
    /// run did not have).
    pub measured: usize,
    /// Changed functions no selected test executed.
    pub changed_uncovered: Vec<String>,
}

/// **Merge an `only: Affected` coverage run with the previous one.** `new` is
/// this run's per-function rows: every instrumented function, at this
/// commit's lines, but counted over the SELECTED tests only. A function that
/// did not change since the baseline (its span meets no `changed` span of its
/// file) keeps the previous run's counts — the tests that would execute it
/// were not run because nothing they reach changed; `carried_from` names the
/// commit that measured them. A changed function (or one the previous run did
/// not have) keeps this run's counts; a changed one no selected test executed
/// is flagged. `previous` is the repo's newest earlier run.
pub fn merge_affected(new: &mut [CoverageFnRow], previous: &[CoverageFnRow], changed: &[ChangedSpan]) -> Merged {
    use std::collections::HashMap;
    let mut prev: HashMap<(&str, &str), &CoverageFnRow> = HashMap::new();
    for p in previous {
        prev.insert((p.file.as_str(), p.name.as_str()), p);
    }
    let touches = |r: &CoverageFnRow| {
        r.line == 0
            || changed.iter().any(|c| c.file == r.file && c.line <= r.end_line.max(r.line) && r.line <= c.end_line.max(c.line))
    };
    let mut m = Merged::default();
    for r in new.iter_mut() {
        let changed_here = touches(r);
        r.changed = Some(changed_here);
        match prev.get(&(r.file.as_str(), r.name.as_str())) {
            // unchanged: what the tests NOT run executed is what they executed
            // before; what the selected tests executed now adds to it
            Some(p) if !changed_here && (p.lines_covered > r.lines_covered || p.regions_covered > r.regions_covered) => {
                r.lines_covered = r.lines_covered.max(p.lines_covered).min(r.lines.max(p.lines));
                r.regions_covered = r.regions_covered.max(p.regions_covered).min(r.regions.max(p.regions));
                r.carried_from = if p.carried_from.is_empty() { p.commit.clone() } else { p.carried_from.clone() };
                m.carried += 1;
            }
            _ => {
                m.measured += 1;
                if r.changed_uncovered() {
                    m.changed_uncovered.push(r.name.clone());
                }
            }
        }
    }
    m
}

/// Flatten a parsed [`CoverageReport`] into the `(coverage, coverage_fn)` row
/// sets for one run. PURE — fully unit-testable. The `coverage` set is: one
/// `overall` row + one `crate:*` row per crate + one `file:*` row per file. The
/// `coverage_fn` set is one row per discovered function (all boundaries, so the
/// boundary view can also show which non-boundary fns exist if wanted; the
/// reader filters to ui/grpc/emitter).
pub fn rows_for(
    report: &CoverageReport,
    run_id: &str,
    repo: &str,
    ts_micros: i64,
) -> (Vec<CoverageRow>, Vec<CoverageFnRow>) {
    let mut rows = Vec::new();
    let mk = |scope: String, krate: String, file: String, l: u64, lc: u64, r: u64, rc: u64| {
        CoverageRow {
            run_id: run_id.to_string(),
            ts_micros,
            repo: repo.to_string(),
            scope,
            krate,
            file,
            lines: l as i64,
            lines_covered: lc as i64,
            regions: r as i64,
            regions_covered: rc as i64,
        }
    };
    rows.push(mk(
        "overall".into(),
        String::new(),
        String::new(),
        report.lines,
        report.lines_covered,
        report.regions,
        report.regions_covered,
    ));
    for c in &report.crates {
        let CrateCoverage {
            krate,
            lines,
            lines_covered,
            regions,
            regions_covered,
            ..
        } = c;
        rows.push(mk(
            format!("crate:{krate}"),
            krate.clone(),
            String::new(),
            *lines,
            *lines_covered,
            *regions,
            *regions_covered,
        ));
    }
    for f in &report.files {
        let FileCoverage {
            file,
            krate,
            lines,
            lines_covered,
            regions,
            regions_covered,
            ..
        } = f;
        rows.push(mk(
            format!("file:{file}"),
            krate.clone(),
            file.clone(),
            *lines,
            *lines_covered,
            *regions,
            *regions_covered,
        ));
    }

    let mut fn_rows = Vec::new();
    for f in &report.files {
        for fnc in &f.functions {
            let FnCoverage {
                name,
                file,
                lines,
                lines_covered,
                regions,
                regions_covered,
                boundary,
                line,
                end_line,
            } = fnc;
            fn_rows.push(CoverageFnRow {
                run_id: run_id.to_string(),
                ts_micros,
                repo: repo.to_string(),
                name: name.clone(),
                file: file.clone(),
                boundary: boundary.as_str().to_string(),
                lines: *lines as i64,
                lines_covered: *lines_covered as i64,
                regions: *regions as i64,
                regions_covered: *regions_covered as i64,
                line: *line,
                end_line: *end_line,
                commit: String::new(),
                carried_from: String::new(),
                changed: None,
            });
        }
    }
    (rows, fn_rows)
}

// ─── write ───────────────────────────────────────────────────────────────────

/// Append a run's `coverage` rows as ONE Iceberg snapshot.
pub async fn append_coverage(wh: &IcebergWarehouse, rows: &[CoverageRow]) -> Result<()> {
    if rows.is_empty() {
        return Ok(());
    }
    let ident = wh.table_ident(TABLE_COVERAGE);
    let table = wh.catalog().load_table(&ident).await?;
    let table = ensure_table_schema(
        wh.catalog(),
        &ident,
        table,
        &super::iceberg_schema::coverage()?,
    )
    .await?;
    let arrow_schema = Arc::new(schema_to_arrow_schema(table.metadata().current_schema())?);
    let cols: Vec<Arc<dyn Array>> = vec![
        Arc::new(StringArray::from(
            rows.iter().map(|r| r.run_id.clone()).collect::<Vec<_>>(),
        )),
        Arc::new(
            TimestampMicrosecondArray::from(rows.iter().map(|r| r.ts_micros).collect::<Vec<_>>())
                .with_timezone("+00:00"),
        ),
        Arc::new(StringArray::from(
            rows.iter().map(|r| r.repo.clone()).collect::<Vec<_>>(),
        )),
        Arc::new(StringArray::from(
            rows.iter().map(|r| r.scope.clone()).collect::<Vec<_>>(),
        )),
        Arc::new(StringArray::from(
            rows.iter().map(|r| r.krate.clone()).collect::<Vec<_>>(),
        )),
        Arc::new(StringArray::from(
            rows.iter().map(|r| r.file.clone()).collect::<Vec<_>>(),
        )),
        Arc::new(Int64Array::from(
            rows.iter().map(|r| r.lines).collect::<Vec<_>>(),
        )),
        Arc::new(Int64Array::from(
            rows.iter().map(|r| r.lines_covered).collect::<Vec<_>>(),
        )),
        Arc::new(Int64Array::from(
            rows.iter().map(|r| r.regions).collect::<Vec<_>>(),
        )),
        Arc::new(Int64Array::from(
            rows.iter().map(|r| r.regions_covered).collect::<Vec<_>>(),
        )),
    ];
    let batch = RecordBatch::try_new(arrow_schema, cols)?;
    append_batch(wh.catalog(), table, batch).await?;
    Ok(())
}

/// Append a run's `coverage_fn` rows as ONE Iceberg snapshot.
pub async fn append_coverage_fn(wh: &IcebergWarehouse, rows: &[CoverageFnRow]) -> Result<()> {
    if rows.is_empty() {
        return Ok(());
    }
    let ident = wh.table_ident(TABLE_COVERAGE_FN);
    let table = wh.catalog().load_table(&ident).await?;
    let table = ensure_table_schema(
        wh.catalog(),
        &ident,
        table,
        &super::iceberg_schema::coverage_fn()?,
    )
    .await?;
    let arrow_schema = Arc::new(schema_to_arrow_schema(table.metadata().current_schema())?);
    let cols: Vec<Arc<dyn Array>> = vec![
        Arc::new(StringArray::from(
            rows.iter().map(|r| r.run_id.clone()).collect::<Vec<_>>(),
        )),
        Arc::new(
            TimestampMicrosecondArray::from(rows.iter().map(|r| r.ts_micros).collect::<Vec<_>>())
                .with_timezone("+00:00"),
        ),
        Arc::new(StringArray::from(
            rows.iter().map(|r| r.repo.clone()).collect::<Vec<_>>(),
        )),
        Arc::new(StringArray::from(
            rows.iter().map(|r| r.name.clone()).collect::<Vec<_>>(),
        )),
        Arc::new(StringArray::from(
            rows.iter().map(|r| r.file.clone()).collect::<Vec<_>>(),
        )),
        Arc::new(StringArray::from(
            rows.iter().map(|r| r.boundary.clone()).collect::<Vec<_>>(),
        )),
        Arc::new(Int64Array::from(
            rows.iter().map(|r| r.lines).collect::<Vec<_>>(),
        )),
        Arc::new(Int64Array::from(
            rows.iter().map(|r| r.lines_covered).collect::<Vec<_>>(),
        )),
        Arc::new(Int64Array::from(
            rows.iter().map(|r| r.regions).collect::<Vec<_>>(),
        )),
        Arc::new(Int64Array::from(
            rows.iter().map(|r| r.regions_covered).collect::<Vec<_>>(),
        )),
        // 11-13: where the function is, and the commit (NULL = not known)
        Arc::new(Int32Array::from(
            rows.iter().map(|r| (r.line > 0).then_some(r.line as i32)).collect::<Vec<_>>(),
        )),
        Arc::new(Int32Array::from(
            rows.iter().map(|r| (r.end_line > 0).then_some(r.end_line as i32)).collect::<Vec<_>>(),
        )),
        Arc::new(StringArray::from(
            rows.iter().map(|r| Some(r.commit.clone()).filter(|c| !c.is_empty())).collect::<Vec<_>>(),
        )),
        // 14-15: an `only: Affected` run, merged (NULL = a run of every test)
        Arc::new(StringArray::from(
            rows.iter().map(|r| Some(r.carried_from.clone()).filter(|c| !c.is_empty())).collect::<Vec<_>>(),
        )),
        Arc::new(arrow::array::BooleanArray::from(rows.iter().map(|r| r.changed).collect::<Vec<_>>())),
    ];
    let batch = RecordBatch::try_new(arrow_schema, cols)?;
    append_batch(wh.catalog(), table, batch).await?;
    Ok(())
}

/// Append both row sets for a run (the usual write — `coverage` then `coverage_fn`).
pub async fn append_report(
    wh: &IcebergWarehouse,
    rows: &[CoverageRow],
    fn_rows: &[CoverageFnRow],
) -> Result<()> {
    append_coverage(wh, rows).await?;
    append_coverage_fn(wh, fn_rows).await
}

// ─── read ──────────────────────────────────────────────────────────────────

/// An optional int column read by name: absent (a table older than it) or NULL → 0.
fn opt_i32(b: &RecordBatch, name: &str, i: usize) -> u32 {
    b.column_by_name(name)
        .and_then(|c| c.as_any().downcast_ref::<Int32Array>())
        .filter(|c| !c.is_null(i))
        .map_or(0, |c| c.value(i).max(0) as u32)
}

/// Read every `coverage` row for `repo`, sorted by `(ts_micros, scope)`.
pub async fn query_coverage(wh: &IcebergWarehouse, repo: &str) -> Result<Vec<CoverageRow>> {
    // Blank/torn Iceberg metadata JSON → 0 rows, never a JSON-EOF crash (load+read).
    let batches: Vec<RecordBatch> = super::iceberg::load_and_read_all(wh, TABLE_COVERAGE).await?;
    let mut out = Vec::new();
    for b in &batches {
        let run_id = col_str(b, 0, TABLE_COVERAGE)?;
        let ts = col_ts(b, 1, TABLE_COVERAGE)?;
        let r = col_str(b, 2, TABLE_COVERAGE)?;
        let scope = col_str(b, 3, TABLE_COVERAGE)?;
        let krate = col_str(b, 4, TABLE_COVERAGE)?;
        let file = col_str(b, 5, TABLE_COVERAGE)?;
        let lines = col_i64(b, 6, TABLE_COVERAGE)?;
        let lc = col_i64(b, 7, TABLE_COVERAGE)?;
        let regions = col_i64(b, 8, TABLE_COVERAGE)?;
        let rc = col_i64(b, 9, TABLE_COVERAGE)?;
        for i in 0..b.num_rows() {
            if r.value(i) != repo {
                continue;
            }
            out.push(CoverageRow {
                run_id: run_id.value(i).to_string(),
                ts_micros: ts.value(i),
                repo: r.value(i).to_string(),
                scope: scope.value(i).to_string(),
                krate: krate.value(i).to_string(),
                file: file.value(i).to_string(),
                lines: lines.value(i),
                lines_covered: lc.value(i),
                regions: regions.value(i),
                regions_covered: rc.value(i),
            });
        }
    }
    out.sort_by(|a, b| (a.ts_micros, &a.scope).cmp(&(b.ts_micros, &b.scope)));
    Ok(out)
}

/// Read every `coverage_fn` row for `repo`, sorted by `(ts_micros, boundary, name)`.
pub async fn query_coverage_fn(wh: &IcebergWarehouse, repo: &str) -> Result<Vec<CoverageFnRow>> {
    // Blank/torn Iceberg metadata JSON → 0 rows, never a JSON-EOF crash (load+read).
    let batches: Vec<RecordBatch> =
        super::iceberg::load_and_read_all(wh, TABLE_COVERAGE_FN).await?;
    let mut out = Vec::new();
    for b in &batches {
        let run_id = col_str(b, 0, TABLE_COVERAGE_FN)?;
        let ts = col_ts(b, 1, TABLE_COVERAGE_FN)?;
        let r = col_str(b, 2, TABLE_COVERAGE_FN)?;
        let name = col_str(b, 3, TABLE_COVERAGE_FN)?;
        let file = col_str(b, 4, TABLE_COVERAGE_FN)?;
        let boundary = col_str(b, 5, TABLE_COVERAGE_FN)?;
        let lines = col_i64(b, 6, TABLE_COVERAGE_FN)?;
        let lc = col_i64(b, 7, TABLE_COVERAGE_FN)?;
        let regions = col_i64(b, 8, TABLE_COVERAGE_FN)?;
        let rc = col_i64(b, 9, TABLE_COVERAGE_FN)?;
        for i in 0..b.num_rows() {
            if r.value(i) != repo {
                continue;
            }
            out.push(CoverageFnRow {
                run_id: run_id.value(i).to_string(),
                ts_micros: ts.value(i),
                repo: r.value(i).to_string(),
                name: name.value(i).to_string(),
                file: file.value(i).to_string(),
                boundary: boundary.value(i).to_string(),
                lines: lines.value(i),
                lines_covered: lc.value(i),
                regions: regions.value(i),
                regions_covered: rc.value(i),
                line: opt_i32(b, "line", i),
                end_line: opt_i32(b, "end_line", i),
                commit: b
                    .column_by_name("commit")
                    .and_then(|c| c.as_any().downcast_ref::<StringArray>())
                    .filter(|c| !c.is_null(i))
                    .map(|c| c.value(i).to_string())
                    .unwrap_or_default(),
                carried_from: b
                    .column_by_name("carried_from")
                    .and_then(|c| c.as_any().downcast_ref::<StringArray>())
                    .filter(|c| !c.is_null(i))
                    .map(|c| c.value(i).to_string())
                    .unwrap_or_default(),
                changed: b
                    .column_by_name("changed")
                    .and_then(|c| c.as_any().downcast_ref::<arrow::array::BooleanArray>())
                    .filter(|c| !c.is_null(i))
                    .map(|c| c.value(i)),
            });
        }
    }
    out.sort_by(|a, b| {
        (a.ts_micros, &a.boundary, &a.name).cmp(&(b.ts_micros, &b.boundary, &b.name))
    });
    Ok(out)
}

/// The newest run's `(coverage, coverage_fn)` rows for `repo` — what the viz
/// Test pane + docs read. Empty when no run is persisted.
pub async fn latest(
    wh: &IcebergWarehouse,
    repo: &str,
) -> Result<(Vec<CoverageRow>, Vec<CoverageFnRow>)> {
    let all = query_coverage(wh, repo).await?;
    let Some(latest_ts) = all.iter().map(|r| r.ts_micros).max() else {
        return Ok((Vec::new(), Vec::new()));
    };
    let run_id = all
        .iter()
        .filter(|r| r.ts_micros == latest_ts)
        .map(|r| r.run_id.clone())
        .next()
        .unwrap_or_default();
    let rows: Vec<CoverageRow> = all.into_iter().filter(|r| r.run_id == run_id).collect();
    let fns: Vec<CoverageFnRow> = query_coverage_fn(wh, repo)
        .await?
        .into_iter()
        .filter(|r| r.run_id == run_id)
        .collect();
    Ok((rows, fns))
}

// ─── column helpers ──────────────────────────────────────────────────────────

fn col_str<'a>(b: &'a RecordBatch, idx: usize, t: &str) -> Result<&'a StringArray> {
    b.column(idx)
        .as_any()
        .downcast_ref::<StringArray>()
        .ok_or_else(|| anyhow!("{t} col {idx} is not StringArray"))
}

fn col_i64<'a>(b: &'a RecordBatch, idx: usize, t: &str) -> Result<&'a Int64Array> {
    b.column(idx)
        .as_any()
        .downcast_ref::<Int64Array>()
        .ok_or_else(|| anyhow!("{t} col {idx} is not Int64Array"))
}

fn col_ts<'a>(b: &'a RecordBatch, idx: usize, t: &str) -> Result<&'a TimestampMicrosecondArray> {
    b.column(idx)
        .as_any()
        .downcast_ref::<TimestampMicrosecondArray>()
        .ok_or_else(|| anyhow!("{t} col {idx} is not TimestampMicrosecondArray"))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::coverage::{Boundary, FileCoverage, FnCoverage, summarise};

    fn sample_report() -> CoverageReport {
        // One viz file (a `ui` fn + a `state_json` emitter) + one core file.
        let viz = FileCoverage {
            file: "src/viz/test_tab.rs".into(),
            krate: "nornir".into(),
            lines: 10,
            lines_covered: 6,
            regions: 8,
            regions_covered: 5,
            functions: vec![
                FnCoverage {
                    name: "nornir::viz::test_tab::TestTabState::ui".into(),
                    file: "src/viz/test_tab.rs".into(),
                    lines: 4,
                    lines_covered: 4,
                    regions: 4,
                    regions_covered: 4,
                    boundary: Boundary::Ui,
                    line: 0,
                    end_line: 0,
                },
                FnCoverage {
                    name: "nornir::viz::test_tab::TestTabState::state_json".into(),
                    file: "src/viz/test_tab.rs".into(),
                    lines: 3,
                    lines_covered: 2,
                    regions: 3,
                    regions_covered: 1,
                    boundary: Boundary::Emitter,
                    line: 0,
                    end_line: 0,
                },
            ],
        };
        let core = FileCoverage {
            file: "src/deps.rs".into(),
            krate: "nornir".into(),
            lines: 5,
            lines_covered: 5,
            regions: 5,
            regions_covered: 5,
            functions: vec![FnCoverage {
                name: "nornir::deps::topo_sort".into(),
                file: "src/deps.rs".into(),
                lines: 5,
                lines_covered: 5,
                regions: 5,
                regions_covered: 5,
                boundary: Boundary::Core,
                line: 0,
                end_line: 0,
            }],
        };
        summarise("nornir", vec![viz, core]).unwrap()
    }

    /// LAW 1 round-trip: build a known report → rows → warehouse → rows back,
    /// asserting the overall/crate/file rollup AND the boundary fn rows.
    #[test]
    fn append_query_round_trip_rollup_and_boundary() {
        let dir = tempfile::tempdir().unwrap();
        let wh = IcebergWarehouse::open(dir.path()).unwrap();
        let report = sample_report();

        let (rows, fn_rows) = rows_for(&report, "run-1", "nornir", 1000);
        // overall + 1 crate + 2 files = 4 coverage rows; 3 fn rows.
        assert_eq!(rows.len(), 4);
        assert_eq!(fn_rows.len(), 3);

        wh.block_on(append_report(&wh, &rows, &fn_rows)).unwrap();

        // Read coverage rows back; the overall row carries the right totals.
        let back = wh.block_on(query_coverage(&wh, "nornir")).unwrap();
        assert_eq!(back.len(), 4);
        let overall = back.iter().find(|r| r.scope == "overall").unwrap();
        assert_eq!(overall.lines, 15, "10 + 5 lines");
        assert_eq!(overall.lines_covered, 11, "6 + 5 covered");
        assert!((overall.line_pct() - 11.0 / 15.0 * 100.0).abs() < 1e-9);

        // The per-file row for the viz pane reads back at 60% lines.
        let vizf = back
            .iter()
            .find(|r| r.scope == "file:src/viz/test_tab.rs")
            .unwrap();
        assert_eq!(vizf.krate, "nornir");
        assert!((vizf.line_pct() - 60.0).abs() < 1e-9);

        // Read fn rows; the ui + emitter boundary fns round-trip with their tags.
        let fns = wh.block_on(query_coverage_fn(&wh, "nornir")).unwrap();
        assert_eq!(fns.len(), 3);
        let ui = fns.iter().find(|r| r.name.ends_with("::ui")).unwrap();
        assert_eq!(ui.boundary, "ui");
        assert!(ui.is_boundary());
        assert!(ui.exercised(), "ui fn covered → exercised");
        let emit = fns
            .iter()
            .find(|r| r.name.ends_with("::state_json"))
            .unwrap();
        assert_eq!(emit.boundary, "emitter");
        // The core fn round-trips as core (boundary view will filter it out).
        let core = fns
            .iter()
            .find(|r| r.name.ends_with("::topo_sort"))
            .unwrap();
        assert_eq!(core.boundary, "core");
        assert!(!core.is_boundary());

        // latest() returns the same run's rows.
        let (lrows, lfns) = wh.block_on(latest(&wh, "nornir")).unwrap();
        assert_eq!(lrows.len(), 4);
        assert_eq!(lfns.len(), 3);
        // A repo with no rows yields empty.
        let (er, ef) = wh.block_on(latest(&wh, "ghost")).unwrap();
        assert!(er.is_empty() && ef.is_empty());
    }

    #[test]
    fn latest_picks_newest_run() {
        let dir = tempfile::tempdir().unwrap();
        let wh = IcebergWarehouse::open(dir.path()).unwrap();
        let report = sample_report();

        let (a, af) = rows_for(&report, "old", "nornir", 1000);
        wh.block_on(append_report(&wh, &a, &af)).unwrap();
        let (b, bf) = rows_for(&report, "new", "nornir", 2000);
        wh.block_on(append_report(&wh, &b, &bf)).unwrap();

        let (lrows, _) = wh.block_on(latest(&wh, "nornir")).unwrap();
        assert!(lrows.iter().all(|r| r.run_id == "new"), "newest run only");
    }

    fn fnrow(run: &str, commit: &str, name: &str, line: u32, end_line: u32, covered: i64) -> CoverageFnRow {
        CoverageFnRow {
            run_id: run.into(),
            ts_micros: 0,
            repo: "shop".into(),
            name: name.into(),
            file: "src/lib.rs".into(),
            boundary: "core".into(),
            lines: 4,
            lines_covered: covered,
            regions: 2,
            regions_covered: covered.min(2),
            line,
            end_line,
            commit: commit.into(),
            carried_from: String::new(),
            changed: None,
        }
    }

    /// The owner's case: an `only: Affected` coverage run counted the SELECTED
    /// tests only. Merged with the previous run, an unchanged function keeps its
    /// row's counts (the tests that executed it were not run, nothing they reach
    /// changed) and says where they came from; a changed function's row is this
    /// run's — and a changed one no selected test executed is flagged.
    #[test]
    fn an_affected_run_keeps_unchanged_rows_and_replaces_changed_ones() {
        let previous = vec![
            fnrow("r1", "c1", "shop::total", 10, 14, 4),
            fnrow("r1", "c1", "shop::checkout", 20, 24, 3),
            fnrow("r1", "c1", "shop::refund", 30, 34, 2),
        ];
        // c2 moved `checkout` (20-24 changed) and `refund` (30-34); only the test
        // of checkout ran: total and refund read 0 in the new run
        let mut new = vec![
            fnrow("r2", "c2", "shop::total", 10, 14, 0),
            fnrow("r2", "c2", "shop::checkout", 20, 24, 1),
            fnrow("r2", "c2", "shop::refund", 30, 34, 0),
            fnrow("r2", "c2", "shop::born", 40, 44, 0),
        ];
        let changed = [
            ChangedSpan { file: "src/lib.rs".into(), line: 21, end_line: 21, fqn: "shop::checkout".into() },
            ChangedSpan { file: "src/lib.rs".into(), line: 30, end_line: 34, fqn: "shop::refund".into() },
            ChangedSpan { file: "src/other.rs".into(), line: 10, end_line: 14, fqn: "other::x".into() },
        ];
        let m = merge_affected(&mut new, &previous, &changed);
        let by = |n: &str| new.iter().find(|r| r.name == n).unwrap().clone();
        let total = by("shop::total");
        assert_eq!((total.lines_covered, total.carried_from.as_str(), total.changed), (4, "c1", Some(false)), "unchanged: the previous row's counts");
        let checkout = by("shop::checkout");
        assert_eq!((checkout.lines_covered, checkout.carried_from.as_str(), checkout.changed), (1, "", Some(true)), "changed: this run's counts, not the old 3");
        let refund = by("shop::refund");
        assert_eq!((refund.lines_covered, refund.changed), (0, Some(true)), "changed and not executed: 0, never the old 2");
        assert!(refund.changed_uncovered() && !checkout.changed_uncovered());
        assert_eq!((m.carried, m.measured), (1, 3), "{m:?}");
        assert_eq!(m.changed_uncovered, vec!["shop::refund".to_string()]);
        // the merged rows round-trip through the warehouse with their marks
        let dir = tempfile::tempdir().unwrap();
        let wh = IcebergWarehouse::open(dir.path()).unwrap();
        wh.block_on(append_coverage_fn(&wh, &new)).unwrap();
        let back = wh.block_on(query_coverage_fn(&wh, "shop")).unwrap();
        let t = back.iter().find(|r| r.name == "shop::total").unwrap();
        assert_eq!((t.carried_from.as_str(), t.changed), ("c1", Some(false)));
        let r = back.iter().find(|r| r.name == "shop::refund").unwrap();
        assert!(r.changed_uncovered(), "{r:?}");
    }
}