#[cfg(test)]
mod tests {
use crate::store::Repository;
use crate::writer::fault_injection::lucene_fixture::{
LUCENE_REINDEX_SCENARIO, write_lucene_reindex_fixture,
};
use crate::writer::fault_injection::test_support::{
TestDirectory, reindex_work_directory, run_crash_child, run_error_child,
};
use std::collections::BTreeMap;
use std::ffi::OsString;
use std::path::Path;
const AFTER_LAST_DOCUMENT: &str = "lucene-reindex.after-last-document";
const AFTER_SEGMENT_FINISHED: &str = "lucene-reindex.after-segment-finished";
const MID_FILE_COPY: &str = "lucene-reindex.mid-file-copy";
fn store_files(directory: &Path) -> BTreeMap<OsString, Vec<u8>> {
std::fs::read_dir(directory)
.expect("read the store directory")
.map(|entry| {
let entry = entry.expect("directory entry");
(
entry.file_name(),
std::fs::read(entry.path()).expect("read the file"),
)
})
.collect()
}
fn digest_under(store: &Path, path: &str) -> Vec<String> {
let repository = Repository::open(store).expect("open the repository");
let mut rendered = Vec::new();
crate::tooling::digest::digest_repository_excluding(&repository, &[], &[], &mut rendered)
.expect("digest");
String::from_utf8(rendered)
.expect("UTF-8")
.lines()
.filter(|line| line.starts_with(path))
.map(str::to_owned)
.collect()
}
fn assert_the_original_store_survived(
store: &Path,
before: &BTreeMap<OsString, Vec<u8>>,
definition_before: &[String],
) {
let after = store_files(store);
for (name, bytes) in before {
if name == "repo.lock" {
continue;
}
assert_eq!(
after.get(name),
Some(bytes),
"{} must survive a pre-publication failure byte-identical",
Path::new(name).display()
);
}
assert_eq!(
digest_under(store, "/oak:index/lucene"),
definition_before,
"the definition must be exactly as the run found it"
);
}
fn work_leftovers(store: &Path) -> Vec<OsString> {
let work = reindex_work_directory(store);
let Ok(entries) = std::fs::read_dir(&work) else {
return Vec::new();
};
entries
.map(|entry| entry.expect("entry").file_name())
.collect()
}
fn work_files(store: &Path) -> Vec<String> {
fn walk(directory: &Path, found: &mut Vec<String>) {
let Ok(entries) = std::fs::read_dir(directory) else {
return;
};
for entry in entries {
let entry = entry.expect("entry");
if entry.file_type().expect("file type").is_dir() {
walk(&entry.path(), found);
} else {
found.push(entry.file_name().to_string_lossy().into_owned());
}
}
}
let mut found = Vec::new();
walk(&reindex_work_directory(store), &mut found);
found.sort();
found
}
fn assert_the_retry_refuses_the_residue_then_rebuilds(store: &Path) {
use crate::index::lucene::documents::binaries::{BinaryTextFallback, BinaryTextPolicy};
use crate::writer::index::{ReindexOptions, WorkDirectory, plan_reindex, reindex};
let work = reindex_work_directory(store);
let residue: Vec<OsString> = std::fs::read_dir(&work)
.expect("read the work directory")
.map(|entry| entry.expect("entry").file_name())
.collect();
assert!(
!residue.is_empty(),
"the death left no run subdirectory to retry against"
);
let options = ReindexOptions::new()
.with_work_directory(WorkDirectory::OperatorNamed(work.clone()))
.with_binary_text_policy(BinaryTextPolicy::new(BinaryTextFallback::Marker))
.with_sort_budget_bytes(1024);
let error = plan_reindex(store, &options)
.expect_err("an operator-named directory holding residue must be refused");
assert!(
error
.to_string()
.contains("left over from an earlier froe reindex"),
"the refusal names what it found: {error}"
);
for name in residue {
std::fs::remove_dir_all(work.join(name)).expect("clear the residue");
}
let outcome = reindex(store, options).expect("the retry runs");
assert!(outcome.moved_the_head(), "the retry published nothing");
assert!(
!digest_under(store, "/oak:index/lucene/:data").is_empty(),
"the retry did not rebuild the index"
);
}
#[test]
fn an_error_after_the_last_document_leaves_the_store_unchanged() {
let directory = TestDirectory::new("lucene-reindex-documents-error");
let store = write_lucene_reindex_fixture(&directory.path);
let before = store_files(&store);
let definition_before = digest_under(&store, "/oak:index/lucene");
run_error_child(&store, LUCENE_REINDEX_SCENARIO, AFTER_LAST_DOCUMENT);
assert_the_original_store_survived(&store, &before, &definition_before);
assert!(
work_leftovers(&store).is_empty(),
"a returned error removes the run's subdirectory: {:?}",
work_leftovers(&store)
);
}
#[test]
fn a_death_after_the_last_document_leaves_files_outside_the_store() {
let directory = TestDirectory::new("lucene-reindex-documents-crash");
let store = write_lucene_reindex_fixture(&directory.path);
let before = store_files(&store);
let definition_before = digest_under(&store, "/oak:index/lucene");
run_crash_child(&store, LUCENE_REINDEX_SCENARIO, AFTER_LAST_DOCUMENT);
assert_the_original_store_survived(&store, &before, &definition_before);
assert!(
!work_files(&store).is_empty(),
"the dead run left nothing behind, so the claim about where it left it is untested"
);
assert_the_retry_refuses_the_residue_then_rebuilds(&store);
}
#[test]
fn an_error_after_the_segment_is_finished_leaves_the_store_unchanged() {
let directory = TestDirectory::new("lucene-reindex-segment-error");
let store = write_lucene_reindex_fixture(&directory.path);
let before = store_files(&store);
let definition_before = digest_under(&store, "/oak:index/lucene");
run_error_child(&store, LUCENE_REINDEX_SCENARIO, AFTER_SEGMENT_FINISHED);
assert_the_original_store_survived(&store, &before, &definition_before);
assert!(work_leftovers(&store).is_empty());
}
#[test]
fn a_death_after_the_segment_is_finished_leaves_a_whole_segment_outside_the_store() {
let directory = TestDirectory::new("lucene-reindex-segment-crash");
let store = write_lucene_reindex_fixture(&directory.path);
let before = store_files(&store);
let definition_before = digest_under(&store, "/oak:index/lucene");
run_crash_child(&store, LUCENE_REINDEX_SCENARIO, AFTER_SEGMENT_FINISHED);
assert_the_original_store_survived(&store, &before, &definition_before);
let left = work_files(&store);
assert!(
left.iter().any(|name| name == "segments_1"),
"a complete segment must be what the death left: {left:?}"
);
assert_the_retry_refuses_the_residue_then_rebuilds(&store);
}
#[test]
fn an_error_in_the_middle_of_the_copy_changes_no_byte_of_the_store() {
let directory = TestDirectory::new("lucene-reindex-copy-error");
let store = write_lucene_reindex_fixture(&directory.path);
let before = store_files(&store);
let definition_before = digest_under(&store, "/oak:index/lucene");
run_error_child(&store, LUCENE_REINDEX_SCENARIO, MID_FILE_COPY);
assert_the_original_store_survived(&store, &before, &definition_before);
}
#[test]
fn a_death_in_the_middle_of_the_copy_leaves_the_head_where_it_was() {
let directory = TestDirectory::new("lucene-reindex-copy-crash");
let store = write_lucene_reindex_fixture(&directory.path);
let before = store_files(&store);
let definition_before = digest_under(&store, "/oak:index/lucene");
run_crash_child(&store, LUCENE_REINDEX_SCENARIO, MID_FILE_COPY);
assert_the_original_store_survived(&store, &before, &definition_before);
assert_the_retry_refuses_the_residue_then_rebuilds(&store);
}
}