test(storage): Crash-Simulation und Stresstest gegen unvollständige Schreibvorgänge
This commit is contained in:
Executable
+82
@@ -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 "=================================================================="
|
||||||
@@ -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(¬ebook);
|
||||||
|
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, ¬ebook, &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(¬ebook);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user