test(storage): Crash-Simulation und Stresstest gegen unvollständige Schreibvorgänge

This commit is contained in:
2026-10-06 19:21:59 +02:00
parent 510035e807
commit 12755d8eea
2 changed files with 303 additions and 0 deletions
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env bash
# ==============================================================================
# scripts/crash-test.sh
# Stresstest für ink-notes Speicher- und Autosave-Robustheit.
# Simuliert harte Programmabstürze (SIGKILL / kill -9) während Schreibvorgängen
# und verifiziert die Integrität der Notizbücher und Seiten.
# ==============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
ITERATIONS="${1:-200}"
TEST_DIR="${REPO_ROOT}/target/crash_test_workspace"
CRASH_SIM_BIN="${REPO_ROOT}/target/release/crash_sim"
echo "=================================================================="
echo " ink-notes Crash- & Datenverlust-Stresstest"
echo " Iterationen: ${ITERATIONS}"
echo " Testverzeichnis: ${TEST_DIR}"
echo "=================================================================="
# 1. Sicherstellen, dass das Test-Binary gebaut ist
if [ ! -f "${CRASH_SIM_BIN}" ]; then
echo "Baue crash_sim Release-Binary..."
cargo build --release --bin crash_sim --manifest-path "${REPO_ROOT}/Cargo.toml"
fi
# 2. Sauberes Testverzeichnis anlegen und Notizbuch initialisieren
rm -rf "${TEST_DIR}"
mkdir -p "${TEST_DIR}"
"${CRASH_SIM_BIN}" init "${TEST_DIR}"
SUCCESS_COUNT=0
INTERRUPTED_WRITES=0
trap 'rm -rf "${TEST_DIR}"' EXIT
echo "Starte Testlauf mit ${ITERATIONS} SIGKILL-Zyklen..."
for i in $(seq 1 "${ITERATIONS}"); do
# Worker im Hintergrund starten
"${CRASH_SIM_BIN}" worker "${TEST_DIR}" > /dev/null 2>&1 &
WORKER_PID=$!
# Zufällige Schlafdauer zwischen 2ms und 35ms, um Schreiboperationen mitten im Flug zu treffen
# RANDOM % 33 + 2 ergibt 2..34 Millisekunden
MS_DELAY=$(( (RANDOM % 33) + 2 ))
DELAY="0.$(printf "%03d" "$MS_DELAY")"
sleep "${DELAY}"
# Harter Prozessabbruch ohne Clean-up (SIGKILL)
if kill -9 "${WORKER_PID}" 2>/dev/null; then
INTERRUPTED_WRITES=$((INTERRUPTED_WRITES + 1))
fi
wait "${WORKER_PID}" 2>/dev/null || true
# Integritäts- und Konsistenzprüfung
if ! "${CRASH_SIM_BIN}" verify "${TEST_DIR}" > /dev/null 2>&1; then
echo ""
echo "❌ FEHLER in Iteration ${i}!"
echo "Notizbuch im Zustand nach Absturz nicht konsistent ladbar!"
"${CRASH_SIM_BIN}" verify "${TEST_DIR}" || true
trap - EXIT
exit 1
fi
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
if [ $((i % 25)) -eq 0 ] || [ "${i}" -eq "${ITERATIONS}" ]; then
echo " [$(date +%T)] Iteration ${i}/${ITERATIONS} bestanden (erfolgreich unterbrochene Schreibvorgänge: ${INTERRUPTED_WRITES})."
fi
done
echo ""
echo "=================================================================="
echo "✅ ALLE ${ITERATIONS} CRASH-TESTS ERFOLGREICH BESTANDEN!"
echo " - Überstandene SIGKILLs: ${INTERRUPTED_WRITES}"
echo " - Konsistente Ladevorgänge: ${SUCCESS_COUNT}/${ITERATIONS}"
echo " - Keine korrupten JSON-Dateien ohne Quarantäne"
echo " - Keine unvollständigen Atom-Schreibungen"
echo "=================================================================="
+221
View File
@@ -0,0 +1,221 @@
//! Crash-Simulationstesthelfer für ink-notes.
//! Wird von scripts/crash-test.sh verwendet, um unvollständige Schreibvorgänge
//! und SIGKILL (kill -9) Robustheit zu verifizieren.
use std::env;
use std::path::Path;
use std::process;
use std::time::Duration;
use glam::Vec2;
use ink_notes::document::{Notebook, PageBackground, StylusSample};
use ink_notes::spline::{finalize_stroke_with_config, SplineConfig};
use ink_notes::storage::{
cleanup_orphaned_temp_files, load_notebook, load_page, save_manifest, save_notebook, save_page,
NotebookManifest, PageData,
};
fn run_init(dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
let nb = Notebook::new();
let id = dir
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("crash_test");
let mut m = NotebookManifest::new(id, "Crash Test Notebook");
m.sync_from_notebook(&nb);
save_notebook(dir, &nb, &mut m)?;
println!("[init] Initialized clean notebook in {:?}", dir);
Ok(())
}
fn run_worker(dir: &Path) {
let (mut notebook, mut manifest) = match load_notebook(dir) {
Ok((nb, m)) => (nb, m),
Err(_) => {
let nb = Notebook::new();
let id = dir
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("crash_test");
let mut m = NotebookManifest::new(id, "Crash Test Notebook");
m.sync_from_notebook(&nb);
let _ = save_notebook(dir, &nb, &mut m);
(nb, m)
}
};
let spline_cfg = SplineConfig::default();
let mut stroke_counter = manifest.next_stroke_id;
println!("[worker] Started writing loop in {:?}", dir);
loop {
// Erzeuge neuen Strich mit einigen Samples
let base_x = (stroke_counter % 500) as f32;
let samples = vec![
StylusSample {
position: Vec2::new(base_x, 100.0),
pressure: 0.5,
timestamp: 0.0,
},
StylusSample {
position: Vec2::new(base_x + 10.0, 150.0),
pressure: 0.7,
timestamp: 0.01,
},
StylusSample {
position: Vec2::new(base_x + 20.0, 200.0),
pressure: 0.6,
timestamp: 0.02,
},
];
let stroke = finalize_stroke_with_config(
stroke_counter,
&samples,
[0.0, 0.0, 0.0, 1.0],
&spline_cfg,
);
stroke_counter += 1;
manifest.next_stroke_id = stroke_counter;
let page = notebook.current_page_mut();
page.add_stroke(stroke);
let page_data = PageData::from(&*page);
// 1. Schreibe Seite atomar
if let Err(e) = save_page(dir, &page_data) {
eprintln!("[worker] Error saving page: {:?}", e);
}
// 2. Aktualisiere Manifest atomar
manifest.sync_from_notebook(&notebook);
if let Err(e) = save_manifest(dir, &manifest) {
eprintln!("[worker] Error saving manifest: {:?}", e);
}
// 3. Gelegentlich Gesamtsave (wie Autosave-Flush)
if stroke_counter % 5 == 0 {
if let Err(e) = save_notebook(dir, &notebook, &mut manifest) {
eprintln!("[worker] Error saving notebook: {:?}", e);
}
}
// 4. Evtl. neue Seite anlegen und sofort sichern, bevor Manifest aktualisiert wird
if stroke_counter % 25 == 0 {
let page_idx = notebook.add_page(PageBackground::DotGrid);
notebook.current_page_index = page_idx;
let new_page_data = PageData::from(notebook.current_page());
if let Err(e) = save_page(dir, &new_page_data) {
eprintln!("[worker] Error saving new page: {:?}", e);
}
manifest.sync_from_notebook(&notebook);
if let Err(e) = save_manifest(dir, &manifest) {
eprintln!("[worker] Error saving manifest: {:?}", e);
}
}
}
}
fn run_verify(dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
println!("[verify] Checking notebook in {:?}", dir);
// 1. Bereinige eventuelle verwaiste .tmp-Dateien
let _ = cleanup_orphaned_temp_files(dir, Duration::from_secs(0));
// Prüfe, ob das Verzeichnis noch gar kein Notizbuch enthält (Prozess vor erstem Flush getötet)
let manifest_path = dir.join("manifest.json");
let manifest_bak = dir.join("manifest.json.bak");
if !manifest_path.exists() && !manifest_bak.exists() {
println!("[verify] Notebook not yet created on disk (killed prior to first save). Valid empty state.");
return Ok(());
}
// 2. Notizbuch laden
let (_notebook, manifest) = match load_notebook(dir) {
Ok(res) => res,
Err(e) => {
eprintln!("[verify] FAILED to load notebook: {:?}", e);
return Err(Box::new(e));
}
};
println!(
"[verify] Manifest loaded successfully: title='{}', pages={}, next_stroke_id={}",
manifest.title,
manifest.pages.len(),
manifest.next_stroke_id
);
// 3. Alle im Manifest registrierten Seiten laden und prüfen
for page_meta in &manifest.pages {
match load_page(dir, page_meta.id) {
Ok(page_data) => {
let page = page_data.to_page();
println!(
"[verify] Page {} verified: {} strokes, background={:?}",
page_meta.id,
page.strokes.len(),
page.background
);
}
Err(e) => {
eprintln!(
"[verify] FAILED to load page {} referenced by manifest: {:?}",
page_meta.id, e
);
return Err(Box::new(e));
}
}
}
// 4. Prüfen ob Quarantäne-Dateien existieren
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let file_name = entry.file_name();
let name = file_name.to_string_lossy();
if name.contains(".corrupt-") {
println!("[verify] Notice: Found quarantined file: {}", name);
}
}
}
println!("[verify] SUCCESS: All pages and manifest are fully consistent!");
Ok(())
}
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 3 {
eprintln!("Usage: crash_sim <init|worker|verify> <notebook_dir>");
process::exit(2);
}
let command = &args[1];
let notebook_dir = Path::new(&args[2]);
match command.as_str() {
"init" => match run_init(notebook_dir) {
Ok(_) => process::exit(0),
Err(e) => {
eprintln!("[init] Error: {}", e);
process::exit(1);
}
},
"worker" => {
run_worker(notebook_dir);
}
"verify" => match run_verify(notebook_dir) {
Ok(_) => process::exit(0),
Err(e) => {
eprintln!("[verify] Error: {}", e);
process::exit(1);
}
},
other => {
eprintln!("Unknown command: {}", other);
process::exit(2);
}
}
}