Compare commits
No commits in common. "5cb1bfe9f3ea004dbf5aa9cdda1aa9db8fef6dd1" and "8701f5df51b34ccb82ca10af2d522ccbe482a745" have entirely different histories.
5cb1bfe9f3
...
8701f5df51
6 changed files with 28 additions and 563 deletions
|
|
@ -2384,7 +2384,6 @@ script_mod! {
|
||||||
// `demo` builds unhide it on init.
|
// `demo` builds unhide it on init.
|
||||||
pin_input := mod.widgets.RobrixTextInput {
|
pin_input := mod.widgets.RobrixTextInput {
|
||||||
width: Fill
|
width: Fill
|
||||||
visible: false
|
|
||||||
empty_text: "M-Pesa PIN"
|
empty_text: "M-Pesa PIN"
|
||||||
is_password: true
|
is_password: true
|
||||||
draw_text +: {
|
draw_text +: {
|
||||||
|
|
|
||||||
|
|
@ -784,177 +784,6 @@ mod tests {
|
||||||
assert_eq!(wb.get_raw(0, 2), "");
|
assert_eq!(wb.get_raw(0, 2), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read a cell's style through the public data map. `SpreadsheetData`
|
|
||||||
/// exposes `cells` directly rather than a getter, and a missing cell is
|
|
||||||
/// indistinguishable from a default-styled one for these assertions.
|
|
||||||
fn style_of(wb: &Workbook, row: u32, col: u32) -> crate::style::CellStyle {
|
|
||||||
wb.active_sheet_data()
|
|
||||||
.cells
|
|
||||||
.get(&CellId::new(row, col))
|
|
||||||
.map(|cell| cell.style.clone())
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Coverage-directed tests ───────────────────────────────────────────
|
|
||||||
//
|
|
||||||
// Added from an uncovered-line report, not by guesswork: workbook_api.rs
|
|
||||||
// sat at 77.80% with 13 public methods never entered by any test. These
|
|
||||||
// are the styling and cell-mutation entry points the toolbar calls, so
|
|
||||||
// "never executed in a test" meant a typo in one would ship.
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn with_sheets_builds_a_workbook_and_defaults_to_the_first() {
|
|
||||||
let mut alpha_data = SpreadsheetData::default();
|
|
||||||
alpha_data.set_cell(0, 0, "from-alpha");
|
|
||||||
let a = Sheet::new("Alpha", alpha_data);
|
|
||||||
let b = Sheet::new("Beta", SpreadsheetData::default());
|
|
||||||
|
|
||||||
let wb = Workbook::with_sheets(vec![a, b]);
|
|
||||||
assert_eq!(wb.sheet_count(), 2);
|
|
||||||
assert_eq!(wb.active_sheet(), 0);
|
|
||||||
assert_eq!(wb.get_raw(0, 0), "from-alpha");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn sheet_data_mut_edits_the_addressed_sheet_only() {
|
|
||||||
let mut wb = Workbook::new();
|
|
||||||
wb.add_sheet("Second");
|
|
||||||
|
|
||||||
wb.sheet_data_mut(1)
|
|
||||||
.expect("sheet 1 exists")
|
|
||||||
.set_cell(0, 0, "only-here");
|
|
||||||
|
|
||||||
assert_eq!(wb.sheet_data(0).expect("sheet 0").get_raw(0, 0), "");
|
|
||||||
assert_eq!(
|
|
||||||
wb.sheet_data(1).expect("sheet 1").get_raw(0, 0),
|
|
||||||
"only-here"
|
|
||||||
);
|
|
||||||
// An out-of-range index must not panic or silently target sheet 0.
|
|
||||||
assert!(wb.sheet_data_mut(99).is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn get_edit_value_returns_the_formula_not_the_result() {
|
|
||||||
let mut wb = Workbook::new();
|
|
||||||
wb.set_cell(0, 0, "2");
|
|
||||||
wb.set_cell(0, 1, "=A1+3");
|
|
||||||
wb.evaluate();
|
|
||||||
|
|
||||||
// The formula bar must show what the user typed.
|
|
||||||
assert_eq!(wb.get_edit_value(0, 1), "=A1+3");
|
|
||||||
// While the grid shows the computed value.
|
|
||||||
assert_eq!(wb.get_display_value(0, 1), "5");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn remove_cell_clears_the_value() {
|
|
||||||
let mut wb = Workbook::new();
|
|
||||||
wb.set_cell(0, 0, "doomed");
|
|
||||||
assert_eq!(wb.get_raw(0, 0), "doomed");
|
|
||||||
|
|
||||||
wb.remove_cell(0, 0);
|
|
||||||
assert_eq!(wb.get_raw(0, 0), "");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Pins a sharp edge in this API, found while writing coverage tests.
|
|
||||||
///
|
|
||||||
/// `set_cell` calls `begin_recording()` before applying. The other eight
|
|
||||||
/// public mutators — `remove_cell`, `put_cell`, `set_col_width`,
|
|
||||||
/// `set_row_height`, `toggle_bold`, `set_number_format`, `set_alignment`,
|
|
||||||
/// `set_bg_color` — do not. Calling one of those directly and then
|
|
||||||
/// `undo()` reverts *the previous recorded edit*, not the mutation just
|
|
||||||
/// made.
|
|
||||||
///
|
|
||||||
/// This is **not** a shipped defect: every `spreadsheet-ui` call site
|
|
||||||
/// takes its own `data.snapshot()` first, which is why the behaviour has
|
|
||||||
/// never been visible. It is a trap for the next caller, so it is
|
|
||||||
/// asserted here rather than left to be rediscovered — and if the
|
|
||||||
/// mutators are ever made self-recording, this test fails and says so.
|
|
||||||
#[test]
|
|
||||||
fn only_set_cell_records_its_own_undo_step() {
|
|
||||||
let mut wb = Workbook::new();
|
|
||||||
wb.set_cell(0, 0, "first");
|
|
||||||
wb.set_cell(0, 0, "second");
|
|
||||||
|
|
||||||
// A direct mutator does not open a new undo step...
|
|
||||||
wb.remove_cell(0, 0);
|
|
||||||
assert_eq!(wb.get_raw(0, 0), "");
|
|
||||||
|
|
||||||
// ...so undo rewinds the last *recorded* edit, restoring "first"
|
|
||||||
// rather than the removed "second".
|
|
||||||
assert!(wb.undo());
|
|
||||||
assert_eq!(
|
|
||||||
wb.get_raw(0, 0),
|
|
||||||
"first",
|
|
||||||
"if this now reads \"second\", the mutators became self-recording \
|
|
||||||
and the doc comment above is stale"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn column_width_and_row_height_overrides_are_stored() {
|
|
||||||
let mut wb = Workbook::new();
|
|
||||||
wb.set_col_width(2, 180.0);
|
|
||||||
wb.set_row_height(5, 42.0);
|
|
||||||
|
|
||||||
let data = wb.active_sheet_data();
|
|
||||||
assert_eq!(data.col_widths.get(&2).copied(), Some(180.0));
|
|
||||||
assert_eq!(data.row_heights.get(&5).copied(), Some(42.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn toggle_bold_flips_and_restores() {
|
|
||||||
let mut wb = Workbook::new();
|
|
||||||
wb.set_cell(0, 0, "x");
|
|
||||||
|
|
||||||
wb.toggle_bold(0, 0);
|
|
||||||
assert!(style_of(&wb, 0, 0).bold);
|
|
||||||
|
|
||||||
wb.toggle_bold(0, 0);
|
|
||||||
assert!(!style_of(&wb, 0, 0).bold, "toggle must be symmetric");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Styling an empty cell must create it rather than being dropped: the
|
|
||||||
/// toolbar lets a user format a blank cell before typing into it.
|
|
||||||
#[test]
|
|
||||||
fn styling_an_empty_cell_creates_it() {
|
|
||||||
let mut wb = Workbook::new();
|
|
||||||
wb.toggle_bold(3, 3);
|
|
||||||
assert!(style_of(&wb, 3, 3).bold);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn number_format_alignment_and_background_are_applied() {
|
|
||||||
let mut wb = Workbook::new();
|
|
||||||
wb.set_cell(0, 0, "1234.5");
|
|
||||||
|
|
||||||
wb.set_number_format(0, 0, NumberFormat::Currency);
|
|
||||||
wb.set_alignment(0, 0, CellAlign::Right);
|
|
||||||
let teal = Color::new(0.0, 0.5, 0.5, 1.0);
|
|
||||||
wb.set_bg_color(0, 0, Some(teal));
|
|
||||||
|
|
||||||
let style = style_of(&wb, 0, 0);
|
|
||||||
assert_eq!(style.number_format, NumberFormat::Currency);
|
|
||||||
assert_eq!(style.align, CellAlign::Right);
|
|
||||||
assert_eq!(style.bg_color, Some(teal));
|
|
||||||
|
|
||||||
// Clearing the background must be expressible.
|
|
||||||
wb.set_bg_color(0, 0, None);
|
|
||||||
assert_eq!(style_of(&wb, 0, 0).bg_color, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn evaluate_recalculates_only_the_active_sheet() {
|
|
||||||
let mut wb = Workbook::new();
|
|
||||||
wb.add_sheet("Second");
|
|
||||||
|
|
||||||
wb.set_active_sheet(1);
|
|
||||||
wb.set_cell(0, 0, "7");
|
|
||||||
wb.set_cell(0, 1, "=A1*2");
|
|
||||||
wb.evaluate();
|
|
||||||
assert_eq!(wb.get_display_value(0, 1), "14");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn workbook_evaluate_all() {
|
fn workbook_evaluate_all() {
|
||||||
let mut wb = Workbook::new();
|
let mut wb = Workbook::new();
|
||||||
|
|
|
||||||
|
|
@ -89,90 +89,4 @@ mod tests {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Coverage-directed: only `Up` was exercised, so a transposed or
|
|
||||||
/// sign-flipped delta in any other arrow would have shipped silently.
|
|
||||||
/// Each direction is asserted against its own axis and sign.
|
|
||||||
#[test]
|
|
||||||
fn every_arrow_maps_to_its_own_axis_and_sign() {
|
|
||||||
let cases = [
|
|
||||||
(NavigationKey::Up, -1i32, 0i32),
|
|
||||||
(NavigationKey::Down, 1, 0),
|
|
||||||
(NavigationKey::Left, 0, -1),
|
|
||||||
(NavigationKey::Right, 0, 1),
|
|
||||||
];
|
|
||||||
for (key, want_row, want_col) in cases {
|
|
||||||
match route_navigation(key, false) {
|
|
||||||
InputIntent::MoveSelection {
|
|
||||||
row_delta,
|
|
||||||
col_delta,
|
|
||||||
extend,
|
|
||||||
} => {
|
|
||||||
assert_eq!(row_delta, want_row, "{key:?} row delta");
|
|
||||||
assert_eq!(col_delta, want_col, "{key:?} col delta");
|
|
||||||
assert!(!extend, "{key:?} must not extend when not asked");
|
|
||||||
}
|
|
||||||
other => panic!("{key:?} routed to {other:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `extend` is what turns an arrow into a shift-arrow range grab. It must
|
|
||||||
/// pass through on every direction, not just the one that was tested.
|
|
||||||
#[test]
|
|
||||||
fn extend_passes_through_on_every_direction() {
|
|
||||||
for key in [
|
|
||||||
NavigationKey::Up,
|
|
||||||
NavigationKey::Down,
|
|
||||||
NavigationKey::Left,
|
|
||||||
NavigationKey::Right,
|
|
||||||
] {
|
|
||||||
match route_navigation(key, true) {
|
|
||||||
InputIntent::MoveSelection { extend, .. } => {
|
|
||||||
assert!(extend, "{key:?} dropped the extend flag")
|
|
||||||
}
|
|
||||||
other => panic!("{key:?} routed to {other:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Undo and Redo are not movements and must not be routed as one — a
|
|
||||||
/// `MoveSelection { 0, 0 }` would look harmless and do nothing.
|
|
||||||
#[test]
|
|
||||||
fn undo_and_redo_are_not_routed_as_movement() {
|
|
||||||
assert_eq!(
|
|
||||||
route_navigation(NavigationKey::Undo, false),
|
|
||||||
InputIntent::Undo
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
route_navigation(NavigationKey::Redo, false),
|
|
||||||
InputIntent::Redo
|
|
||||||
);
|
|
||||||
// The extend flag is meaningless for these and must not change them.
|
|
||||||
assert_eq!(
|
|
||||||
route_navigation(NavigationKey::Undo, true),
|
|
||||||
InputIntent::Undo
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
route_navigation(NavigationKey::Redo, true),
|
|
||||||
InputIntent::Redo
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn edit_and_range_routes_carry_their_arguments() {
|
|
||||||
assert_eq!(
|
|
||||||
route_select_range((1, 2), (3, 4)),
|
|
||||||
InputIntent::SelectRange {
|
|
||||||
anchor: (1, 2),
|
|
||||||
head: (3, 4)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
route_edit_begin(7, 9),
|
|
||||||
InputIntent::BeginEdit { row: 7, col: 9 }
|
|
||||||
);
|
|
||||||
assert_eq!(route_edit_commit(), InputIntent::CommitEdit);
|
|
||||||
assert_eq!(route_edit_cancel(), InputIntent::CancelEdit);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,64 +48,4 @@ mod tests {
|
||||||
selection.clear();
|
selection.clear();
|
||||||
assert_eq!(selection.bounds(), None);
|
assert_eq!(selection.bounds(), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `anchor()` and `head()` were never called by a test, so the two could
|
|
||||||
/// have been swapped without anything noticing. `bounds()` normalises
|
|
||||||
/// them, which hides the difference — these assert the raw values.
|
|
||||||
#[test]
|
|
||||||
fn anchor_stays_put_while_head_follows_an_extend() {
|
|
||||||
let mut selection = SelectionController::new();
|
|
||||||
assert_eq!(selection.anchor(), None);
|
|
||||||
assert_eq!(selection.head(), None);
|
|
||||||
|
|
||||||
selection.set(2, 3, false);
|
|
||||||
assert_eq!(selection.anchor(), Some((2, 3)));
|
|
||||||
assert_eq!(selection.head(), Some((2, 3)));
|
|
||||||
|
|
||||||
// Extending moves only the head.
|
|
||||||
selection.set(5, 1, true);
|
|
||||||
assert_eq!(selection.anchor(), Some((2, 3)), "anchor moved on extend");
|
|
||||||
assert_eq!(selection.head(), Some((5, 1)));
|
|
||||||
|
|
||||||
// A non-extending click re-anchors both.
|
|
||||||
selection.set(9, 9, false);
|
|
||||||
assert_eq!(selection.anchor(), Some((9, 9)));
|
|
||||||
assert_eq!(selection.head(), Some((9, 9)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shift-clicking with nothing selected must anchor rather than leave a
|
|
||||||
/// head with no anchor, which `bounds()` would report as no selection.
|
|
||||||
#[test]
|
|
||||||
fn extending_from_an_empty_selection_anchors_instead() {
|
|
||||||
let mut selection = SelectionController::new();
|
|
||||||
selection.set(4, 4, true);
|
|
||||||
assert_eq!(selection.anchor(), Some((4, 4)));
|
|
||||||
assert_eq!(selection.bounds(), Some((4, 4, 4, 4)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn clear_resets_both_ends() {
|
|
||||||
let mut selection = SelectionController::new();
|
|
||||||
selection.set(1, 1, false);
|
|
||||||
selection.set(3, 3, true);
|
|
||||||
selection.clear();
|
|
||||||
assert_eq!(selection.anchor(), None);
|
|
||||||
assert_eq!(selection.head(), None);
|
|
||||||
assert_eq!(selection.bounds(), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Bounds must normalise whichever way the drag went.
|
|
||||||
#[test]
|
|
||||||
fn bounds_are_normalised_in_both_drag_directions() {
|
|
||||||
let mut forward = SelectionController::new();
|
|
||||||
forward.set(1, 1, false);
|
|
||||||
forward.set(4, 6, true);
|
|
||||||
|
|
||||||
let mut backward = SelectionController::new();
|
|
||||||
backward.set(4, 6, false);
|
|
||||||
backward.set(1, 1, true);
|
|
||||||
|
|
||||||
assert_eq!(forward.bounds(), backward.bounds());
|
|
||||||
assert_eq!(forward.bounds(), Some((1, 1, 4, 6)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,31 +30,6 @@ if [[ ! -f "$SHEET" ]]; then
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Part 1: the PIN controls must be hidden in the UI definition itself ──────
|
|
||||||
#
|
|
||||||
# The compile probe below proves the *field* is absent from a packaging build.
|
|
||||||
# It says nothing about the default build, where the field legitimately exists
|
|
||||||
# and the DSL is what keeps the control off screen until a demo build unhides
|
|
||||||
# it on init.
|
|
||||||
#
|
|
||||||
# That distinction matters: an unrelated commit removed `visible: false` from
|
|
||||||
# `pin_input` while leaving it on `pin_eye_btn`, and every existing gate still
|
|
||||||
# passed. A hidden-by-default control that becomes visible-by-default is
|
|
||||||
# exactly the DSL-reload hole review item 0.3 asks to close (defect B11).
|
|
||||||
for control in pin_input pin_eye_btn; do
|
|
||||||
if ! awk -v ctrl="$control" '
|
|
||||||
index($0, ctrl " :=") { found = 1; next }
|
|
||||||
found && /visible: false/ { ok = 1; exit }
|
|
||||||
found && /^[[:space:]]*}/ { exit }
|
|
||||||
END { exit(ok ? 0 : 1) }
|
|
||||||
' "$SHEET"; then
|
|
||||||
echo "FAIL: $control is not hidden by default in the DSL" >&2
|
|
||||||
echo " (review item 0.3: hiding must survive a DSL reload)" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
echo "PIN controls are hidden by default in the UI definition"
|
|
||||||
|
|
||||||
BACKUP="$(mktemp)"
|
BACKUP="$(mktemp)"
|
||||||
cp "$SHEET" "$BACKUP"
|
cp "$SHEET" "$BACKUP"
|
||||||
restore() {
|
restore() {
|
||||||
|
|
|
||||||
|
|
@ -1,232 +1,40 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# LLVM source-coverage for the Spreadsheet engine and UI controller logic.
|
# Temporary LLVM source-coverage run for Spreadsheet's pure engine.
|
||||||
#
|
|
||||||
# Everything lives in one temporary directory — toolchain, cargo caches, crate
|
|
||||||
# copies, build artefacts, profiles and the report — and it is removed on any
|
|
||||||
# exit path. Nothing is installed into the host, and the repository's own
|
|
||||||
# `target/` is untouched.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# ./tools/test-spreadsheet-coverage.sh # engine + ui
|
|
||||||
# COVERAGE_TARGET=engine ./tools/test-spreadsheet-coverage.sh
|
|
||||||
# COVERAGE_TARGET=ui ./tools/test-spreadsheet-coverage.sh
|
|
||||||
# KEEP_COVERAGE=1 ./tools/test-spreadsheet-coverage.sh # keeps the report
|
|
||||||
#
|
|
||||||
# `KEEP_COVERAGE=1` additionally writes an uncovered-line listing, which is
|
|
||||||
# what makes "add tests for uncovered branches" a directed activity rather
|
|
||||||
# than guesswork.
|
|
||||||
|
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
IFS=$'\n\t'
|
IFS=$'\n\t'
|
||||||
|
|
||||||
ROOT="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
COVERAGE_TARGET="${COVERAGE_TARGET:-all}"
|
WORK="$(mktemp -d "${TMPDIR:-/tmp}/spreadsheet-coverage.XXXXXXXX")"
|
||||||
KEEP_COVERAGE="${KEEP_COVERAGE:-0}"
|
KEEP_COVERAGE="${KEEP_COVERAGE:-0}"
|
||||||
ENGINE_FLOOR="${ENGINE_FLOOR:-90}"
|
|
||||||
UI_FLOOR="${UI_FLOOR:-94}"
|
|
||||||
|
|
||||||
case "$COVERAGE_TARGET" in
|
|
||||||
all | engine | ui) ;;
|
|
||||||
*)
|
|
||||||
echo "error: COVERAGE_TARGET must be all, engine or ui" >&2
|
|
||||||
exit 2
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Two constraints pull against each other here.
|
|
||||||
#
|
|
||||||
# `/tmp` is a small tmpfs on some hosts and an instrumented build plus a
|
|
||||||
# toolchain will not fit, so the default must be somewhere roomier. But the
|
|
||||||
# work directory must *not* sit inside the repository: Cargo would then treat
|
|
||||||
# the copied crates as workspace members and refuse to build them.
|
|
||||||
#
|
|
||||||
# `$HOME/.cache` satisfies both — same roomy filesystem, outside the
|
|
||||||
# workspace. A caller-supplied TMPDIR wins, and is assumed to be sane.
|
|
||||||
DEFAULT_TMP="${HOME:-/var/tmp}/.cache/nigig-coverage"
|
|
||||||
mkdir -p "${TMPDIR:-$DEFAULT_TMP}"
|
|
||||||
WORK="$(mktemp -d "${TMPDIR:-$DEFAULT_TMP}/spreadsheet-coverage.XXXXXXXX")"
|
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
local status=$?
|
local s=$?
|
||||||
if [[ "$KEEP_COVERAGE" == "1" ]]; then
|
if [[ "$KEEP_COVERAGE" == "1" ]]; then
|
||||||
echo "coverage environment retained: $WORK" >&2
|
echo "coverage environment retained: $WORK" >&2
|
||||||
else
|
else
|
||||||
rm -rf -- "$WORK"
|
rm -rf "$WORK"
|
||||||
# Only remove the default parent, and only when we created it and it
|
|
||||||
# is empty: a caller-supplied TMPDIR is not ours to delete.
|
|
||||||
rmdir "$DEFAULT_TMP" 2>/dev/null || true
|
|
||||||
rmdir "${HOME:-/var/tmp}/.cache" 2>/dev/null || true
|
|
||||||
echo "cleaned isolated coverage environment" >&2
|
echo "cleaned isolated coverage environment" >&2
|
||||||
fi
|
fi
|
||||||
exit "$status"
|
exit "$s"
|
||||||
}
|
}
|
||||||
trap cleanup EXIT HUP INT TERM
|
trap cleanup EXIT HUP INT TERM
|
||||||
|
export RUSTUP_HOME="$WORK/rustup" CARGO_HOME="$WORK/cargo" CARGO_TARGET_DIR="$WORK/target"
|
||||||
TOOLCHAIN="$(sed -n 's/^[[:space:]]*channel[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' \
|
export PATH="$CARGO_HOME/bin:$PATH" LLVM_PROFILE_FILE="$WORK/profiles/%p-%m.profraw"
|
||||||
"$ROOT/rust-toolchain.toml" | head -n 1)"
|
|
||||||
if [[ -z "$TOOLCHAIN" ]]; then
|
|
||||||
echo "error: could not read channel from rust-toolchain.toml" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
export RUSTUP_HOME="$WORK/rustup"
|
|
||||||
export CARGO_HOME="$WORK/cargo"
|
|
||||||
export CARGO_TARGET_DIR="$WORK/target"
|
|
||||||
export PATH="$CARGO_HOME/bin:$PATH"
|
|
||||||
export CARGO_NET_GIT_FETCH_WITH_CLI=true
|
|
||||||
export CARGO_INCREMENTAL=0
|
|
||||||
export LLVM_PROFILE_FILE="$WORK/profiles/%p-%m.profraw"
|
|
||||||
# `-C opt-level=0` keeps line mapping honest: optimisation merges and drops
|
|
||||||
# lines, which makes a coverage report describe a program nobody wrote.
|
|
||||||
export RUSTFLAGS="-C instrument-coverage -C codegen-units=1 -C opt-level=0"
|
export RUSTFLAGS="-C instrument-coverage -C codegen-units=1 -C opt-level=0"
|
||||||
mkdir -p "$WORK/profiles"
|
mkdir -p "$WORK/profiles"
|
||||||
|
curl --fail --location https://sh.rustup.rs -o "$WORK/rustup-init"
|
||||||
echo "creating isolated coverage toolchain ($TOOLCHAIN) in $WORK" >&2
|
|
||||||
curl --fail --location --proto '=https' --tlsv1.2 https://sh.rustup.rs \
|
|
||||||
-o "$WORK/rustup-init"
|
|
||||||
chmod 700 "$WORK/rustup-init"
|
chmod 700 "$WORK/rustup-init"
|
||||||
"$WORK/rustup-init" -y --profile minimal --default-toolchain "$TOOLCHAIN" \
|
"$WORK/rustup-init" -y --profile minimal --default-toolchain 1.97.1 --component llvm-tools-preview --no-modify-path
|
||||||
--component llvm-tools-preview --no-modify-path
|
CRATE="$WORK/spreadsheet-engine"
|
||||||
|
cp -a "$ROOT/crates/apps/spreadsheet/spreadsheet-engine" "$CRATE"
|
||||||
LLVM_BIN="$RUSTUP_HOME/toolchains/$TOOLCHAIN-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/bin"
|
cargo test --manifest-path "$CRATE/Cargo.toml" --all-targets
|
||||||
|
LLVM_BIN="$RUSTUP_HOME/toolchains/1.97.1-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/bin"
|
||||||
# ── Exclusions (step 4) ──────────────────────────────────────────────────────
|
"$LLVM_BIN/llvm-profdata" merge -sparse "$WORK/profiles"/*.profraw -o "$WORK/coverage.profdata"
|
||||||
#
|
TEST_BIN=$(find "$CARGO_TARGET_DIR/debug/deps" -maxdepth 1 -type f -executable -name 'spreadsheet_engine-*' | head -1)
|
||||||
# Two categories are excluded, for different reasons.
|
"$LLVM_BIN/llvm-cov" report "$TEST_BIN" -instr-profile="$WORK/coverage.profdata" \
|
||||||
#
|
-ignore-filename-regex='(/cargo/registry|/cargo/git|/rustc/|/tmp/spreadsheet-coverage)' \
|
||||||
# 1. **Not our code.** Registry and git dependencies, the Rust standard
|
"$CRATE/src/lib.rs" "$CRATE/src/data.rs" "$CRATE/src/formula2.rs" "$CRATE/src/workbook.rs"
|
||||||
# library, and the copy path itself. Including them measures other
|
if [[ "$KEEP_COVERAGE" == "1" ]]; then
|
||||||
# people's tests.
|
"$LLVM_BIN/llvm-cov" show "$TEST_BIN" -instr-profile="$WORK/coverage.profdata" \
|
||||||
#
|
-ignore-filename-regex='(/cargo/registry|/cargo/git|/rustc/)' \
|
||||||
# 2. **Generated Makepad DSL and platform startup.** `script_mod! { ... }`
|
| grep -E '^ *[0-9]+\| *0\|' > "$WORK/uncovered-lines.txt" || true
|
||||||
# expands to widget-registration code that no unit test can reach without
|
echo "uncovered line report: $WORK/uncovered-lines.txt" >&2
|
||||||
# a live `Cx` and a window, and `src/bin/` is the desktop entry point.
|
|
||||||
# Counting them does not report "untested logic", it reports "logic that
|
|
||||||
# cannot be reached from a test", which drags the number down while
|
|
||||||
# telling you nothing actionable. They are excluded by *file*, and the
|
|
||||||
# files that hold them are named here rather than pattern-matched, so
|
|
||||||
# adding a new one is a deliberate act.
|
|
||||||
#
|
|
||||||
# Everything else in the UI crate — the controller modules — is measured.
|
|
||||||
# Only foreign code is filtered. The copied crates live *under* the work
|
|
||||||
# directory, so matching the work path here would exclude the sources being
|
|
||||||
# measured — which is exactly what an earlier version of this script did,
|
|
||||||
# reporting a confident 0%.
|
|
||||||
IGNORE_COMMON='(/cargo/registry|/cargo/git|/rustc/)'
|
|
||||||
# grid.rs, ui.rs and workspace.rs are the three files carrying `script_mod!`.
|
|
||||||
IGNORE_UI="$IGNORE_COMMON"'|spreadsheet-ui/src/(grid|ui|workspace)\.rs|spreadsheet-ui/src/bin/'
|
|
||||||
|
|
||||||
report_for() {
|
|
||||||
local label="$1" test_bin="$2" profdata="$3" ignore="$4" floor="$5"
|
|
||||||
shift 5
|
|
||||||
local sources=("$@")
|
|
||||||
|
|
||||||
echo >&2
|
|
||||||
echo "── $label ─────────────────────────────────────────────" >&2
|
|
||||||
"$LLVM_BIN/llvm-cov" report "$test_bin" \
|
|
||||||
-instr-profile="$profdata" \
|
|
||||||
-ignore-filename-regex="$ignore" \
|
|
||||||
"${sources[@]}"
|
|
||||||
|
|
||||||
local covered
|
|
||||||
covered="$("$LLVM_BIN/llvm-cov" export "$test_bin" \
|
|
||||||
-instr-profile="$profdata" \
|
|
||||||
-ignore-filename-regex="$ignore" \
|
|
||||||
-summary-only "${sources[@]}" \
|
|
||||||
| grep -o '"lines":{[^}]*}' | tail -1 \
|
|
||||||
| grep -o '"percent":[0-9.]*' | cut -d: -f2)"
|
|
||||||
covered="${covered%%.*}"
|
|
||||||
if [[ -z "$covered" ]]; then
|
|
||||||
echo "error: could not read $label line coverage" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if (( covered < floor )); then
|
|
||||||
echo "FAIL: $label line coverage ${covered}% is below the ${floor}% floor" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "$label line coverage ${covered}% meets the ${floor}% floor" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
uncovered_listing() {
|
|
||||||
local label="$1" test_bin="$2" profdata="$3" ignore="$4"
|
|
||||||
shift 4
|
|
||||||
"$LLVM_BIN/llvm-cov" show "$test_bin" \
|
|
||||||
-instr-profile="$profdata" \
|
|
||||||
-ignore-filename-regex="$ignore" \
|
|
||||||
"$@" \
|
|
||||||
| awk '
|
|
||||||
# `llvm-cov show` prints a bare path line before each file. Match
|
|
||||||
# any line that is not a numbered source line and ends in `.rs`
|
|
||||||
# or `.rs:` — an earlier version required a trailing colon and
|
|
||||||
# silently produced an empty listing.
|
|
||||||
/^[^ 0-9]/ && /\.rs:?$/ { file = $0; sub(/:$/, "", file); next }
|
|
||||||
/^ *[0-9]+\| *0\|/ { print file ": " $0 }
|
|
||||||
' > "$WORK/uncovered-$label.txt" || true
|
|
||||||
echo "uncovered lines: $WORK/uncovered-$label.txt" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Engine ───────────────────────────────────────────────────────────────────
|
|
||||||
if [[ "$COVERAGE_TARGET" == "all" || "$COVERAGE_TARGET" == "engine" ]]; then
|
|
||||||
ENGINE="$WORK/spreadsheet-engine"
|
|
||||||
cp -a "$ROOT/crates/apps/spreadsheet/spreadsheet-engine" "$ENGINE"
|
|
||||||
|
|
||||||
# `--all-targets` picks up tests/sync_flow.rs as well as the unit tests.
|
|
||||||
cargo test --manifest-path "$ENGINE/Cargo.toml" --all-targets
|
|
||||||
|
|
||||||
"$LLVM_BIN/llvm-profdata" merge -sparse "$WORK/profiles"/*.profraw \
|
|
||||||
-o "$WORK/engine.profdata"
|
|
||||||
ENGINE_BIN="$(find "$CARGO_TARGET_DIR/debug/deps" -maxdepth 1 -type f \
|
|
||||||
-executable -name 'spreadsheet_engine-*' | head -1)"
|
|
||||||
if [[ -z "$ENGINE_BIN" ]]; then
|
|
||||||
echo "error: could not find the engine test binary" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
report_for "engine" "$ENGINE_BIN" "$WORK/engine.profdata" \
|
|
||||||
"$IGNORE_COMMON" "$ENGINE_FLOOR" "$ENGINE/src"
|
|
||||||
if [[ "$KEEP_COVERAGE" == "1" ]]; then
|
|
||||||
uncovered_listing "engine" "$ENGINE_BIN" "$WORK/engine.profdata" \
|
|
||||||
"$IGNORE_COMMON" "$ENGINE/src"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── UI controller logic ──────────────────────────────────────────────────────
|
|
||||||
if [[ "$COVERAGE_TARGET" == "all" || "$COVERAGE_TARGET" == "ui" ]]; then
|
|
||||||
# Makepad's Linux backend needs native libraries to link a test binary.
|
|
||||||
"$ROOT/tools/makepad-native-libs.sh" --check
|
|
||||||
|
|
||||||
rm -f "$WORK/profiles"/*.profraw
|
|
||||||
mkdir -p "$WORK/spreadsheet"
|
|
||||||
cp -a "$ROOT/crates/apps/spreadsheet/spreadsheet-engine" \
|
|
||||||
"$WORK/spreadsheet/spreadsheet-engine"
|
|
||||||
cp -a "$ROOT/crates/apps/spreadsheet/spreadsheet-ui" \
|
|
||||||
"$WORK/spreadsheet/spreadsheet-ui"
|
|
||||||
UI="$WORK/spreadsheet/spreadsheet-ui"
|
|
||||||
|
|
||||||
cargo test --manifest-path "$UI/Cargo.toml" --lib
|
|
||||||
|
|
||||||
"$LLVM_BIN/llvm-profdata" merge -sparse "$WORK/profiles"/*.profraw \
|
|
||||||
-o "$WORK/ui.profdata"
|
|
||||||
UI_BIN="$(find "$CARGO_TARGET_DIR/debug/deps" -maxdepth 1 -type f \
|
|
||||||
-executable -name 'spreadsheet_ui-*' | head -1)"
|
|
||||||
if [[ -z "$UI_BIN" ]]; then
|
|
||||||
echo "error: could not find the UI test binary" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Only the controller modules are measured; see the exclusion note above.
|
|
||||||
report_for "ui-controllers" "$UI_BIN" "$WORK/ui.profdata" \
|
|
||||||
"$IGNORE_UI" "$UI_FLOOR" \
|
|
||||||
"$UI/src/clipboard.rs" "$UI/src/edit.rs" "$UI/src/event_router.rs" \
|
|
||||||
"$UI/src/input.rs" "$UI/src/model.rs" "$UI/src/render_cache.rs" \
|
|
||||||
"$UI/src/selection.rs" "$UI/src/text_measure.rs"
|
|
||||||
if [[ "$KEEP_COVERAGE" == "1" ]]; then
|
|
||||||
uncovered_listing "ui" "$UI_BIN" "$WORK/ui.profdata" "$IGNORE_UI" \
|
|
||||||
"$UI/src/clipboard.rs" "$UI/src/edit.rs" "$UI/src/event_router.rs" \
|
|
||||||
"$UI/src/input.rs" "$UI/src/model.rs" "$UI/src/render_cache.rs" \
|
|
||||||
"$UI/src/selection.rs" "$UI/src/text_measure.rs"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo >&2
|
|
||||||
echo "spreadsheet coverage run complete" >&2
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue