use super::verification::{SubtreePerturbation, perturb_subtree};
use crate::content::property::PropertyType;
use crate::writer::index::{ReindexOptions, WorkDirectory, reindex};
use crate::writer::record_writer::{ChildNodesToWrite, PropertyToWrite, PropertyValuesToWrite};
use crate::writer::store_writer::WritableRepository;
struct TestDirectory {
path: std::path::PathBuf,
}
impl TestDirectory {
fn new(name: &str) -> Self {
let path = std::env::temp_dir().join(format!(
"froe-reindex-tail-{name}-{}-{:?}",
std::process::id(),
std::thread::current().id()
));
let _ = std::fs::remove_dir_all(&path);
std::fs::create_dir_all(&path).expect("create the test directory");
Self { path }
}
fn store(&self) -> std::path::PathBuf {
let store = self.path.join("store");
std::fs::create_dir_all(&store).expect("create the store directory");
store
}
fn options(&self) -> ReindexOptions {
ReindexOptions::new().with_work_directory(WorkDirectory::OperatorNamed(self.path.clone()))
}
}
impl Drop for TestDirectory {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.path);
}
}
fn text<Sink: crate::writer::SegmentSink>(
writer: &mut crate::writer::record_writer::RecordWriter<Sink>,
name: &str,
value: &str,
) -> PropertyToWrite {
let value = writer.write_string(value).expect("write a string");
PropertyToWrite {
name: name.to_owned(),
property_type: PropertyType::String,
values: PropertyValuesToWrite::Single(value),
}
}
fn name_of<Sink: crate::writer::SegmentSink>(
writer: &mut crate::writer::record_writer::RecordWriter<Sink>,
name: &str,
value: &str,
) -> PropertyToWrite {
let value = writer.write_string(value).expect("write a string");
PropertyToWrite {
name: name.to_owned(),
property_type: PropertyType::Name,
values: PropertyValuesToWrite::Single(value),
}
}
fn boolean<Sink: crate::writer::SegmentSink>(
writer: &mut crate::writer::record_writer::RecordWriter<Sink>,
name: &str,
value: bool,
) -> PropertyToWrite {
let value = writer
.write_string(if value { "true" } else { "false" })
.expect("write a string");
PropertyToWrite {
name: name.to_owned(),
property_type: PropertyType::Boolean,
values: PropertyValuesToWrite::Single(value),
}
}
fn definition_properties<Sink: crate::writer::SegmentSink>(
writer: &mut crate::writer::record_writer::RecordWriter<Sink>,
index_type: &str,
) -> Vec<PropertyToWrite> {
let mut properties = vec![
name_of(writer, "jcr:primaryType", "oak:QueryIndexDefinition"),
text(writer, "type", index_type),
boolean(writer, "reindex", true),
];
if index_type == "property" {
properties.push(names(writer, "propertyNames", &["jcr:title"]));
} else {
properties.push(long(writer, "seed", "-7610761686379641542"));
properties.push(long(writer, "resolution", "1"));
}
properties
}
fn names<Sink: crate::writer::SegmentSink>(
writer: &mut crate::writer::record_writer::RecordWriter<Sink>,
name: &str,
values: &[&str],
) -> PropertyToWrite {
let values = values
.iter()
.map(|value| writer.write_string(value).expect("write a string"))
.collect();
PropertyToWrite {
name: name.to_owned(),
property_type: PropertyType::Name,
values: PropertyValuesToWrite::Multiple(values),
}
}
fn long<Sink: crate::writer::SegmentSink>(
writer: &mut crate::writer::record_writer::RecordWriter<Sink>,
name: &str,
value: &str,
) -> PropertyToWrite {
let value = writer.write_string(value).expect("write a string");
PropertyToWrite {
name: name.to_owned(),
property_type: PropertyType::Long,
values: PropertyValuesToWrite::Single(value),
}
}
fn store_with_a_flagged_definition(
directory: &TestDirectory,
index_type: &str,
) -> std::path::PathBuf {
let path = directory.store();
let store = WritableRepository::open(&path).expect("open");
let generation = store.writing_generation().expect("generation");
let mut writer = store.record_writer(generation);
let page_properties = vec![
name_of(&mut writer, "jcr:primaryType", "nt:unstructured"),
text(&mut writer, "jcr:title", "Alpha"),
];
let page = writer
.write_node(None, &[], &ChildNodesToWrite::Zero, &page_properties)
.expect("write the page");
let content_properties = vec![name_of(&mut writer, "jcr:primaryType", "nt:unstructured")];
let content = writer
.write_node(
None,
&[],
&ChildNodesToWrite::One {
name: "page".to_owned(),
node: page,
},
&content_properties,
)
.expect("write the content");
let subject = definition_properties(&mut writer, index_type);
let subject = writer
.write_node(None, &[], &ChildNodesToWrite::Zero, &subject)
.expect("write the definition");
let nodetype_properties = vec![
name_of(&mut writer, "jcr:primaryType", "oak:QueryIndexDefinition"),
text(&mut writer, "type", "property"),
names(&mut writer, "propertyNames", &["jcr:primaryType"]),
];
let nodetype = writer
.write_node(None, &[], &ChildNodesToWrite::Zero, &nodetype_properties)
.expect("write the nodetype definition");
let oak_index = writer
.write_node(
None,
&[],
&ChildNodesToWrite::Many(vec![
("nodetype".to_owned(), nodetype),
("subject".to_owned(), subject),
]),
&[],
)
.expect("write oak:index");
let root = writer
.write_node(
None,
&[],
&ChildNodesToWrite::Many(vec![
("content".to_owned(), content),
(crate::index::INDEX_DEFINITIONS_NAME.to_owned(), oak_index),
]),
&[],
)
.expect("write the root");
let head = writer
.write_node(
None,
&[],
&ChildNodesToWrite::One {
name: "root".to_owned(),
node: root,
},
&[],
)
.expect("write the super root");
writer.finish().expect("finish");
let previous = store.head();
assert!(store.compare_and_set_head(previous, head));
store.close().expect("close");
path
}
fn head_of(store: &std::path::Path) -> crate::segment::record::RecordIdentifier {
crate::store::Repository::open(store)
.expect("open")
.head_record_identifier()
}
struct Perturbed;
impl Perturbed {
fn to(perturbation: SubtreePerturbation) -> Self {
perturb_subtree(Some(perturbation));
Self
}
}
impl Drop for Perturbed {
fn drop(&mut self) {
perturb_subtree(None);
}
}
#[test]
fn a_forged_entry_the_content_does_not_carry_is_refused() {
let directory = TestDirectory::new("forged-entry");
let store = store_with_a_flagged_definition(&directory, "property");
let before = head_of(&store);
let _perturbed = Perturbed::to(SubtreePerturbation::SpliceForgedEntries(1));
let error = reindex(&store, directory.options()).expect_err("the tail must refuse");
assert!(
error
.to_string()
.contains("does not agree with the content it indexes"),
"the refusal says what it found: {error}"
);
assert_eq!(
head_of(&store),
before,
"a refused reindex moved the head anyway"
);
}
#[test]
fn a_subtree_holding_more_entries_than_were_collected_exhausts_the_budget() {
let directory = TestDirectory::new("budget");
let store = store_with_a_flagged_definition(&directory, "property");
let before = head_of(&store);
let _perturbed = Perturbed::to(SubtreePerturbation::SpliceForgedEntries(2));
let error = reindex(&store, directory.options()).expect_err("the tail must refuse");
let message = error.to_string();
assert!(
message.contains("would examine more than") && message.contains("entries"),
"the refusal names the limit it reached: {message}"
);
assert_eq!(head_of(&store), before);
}
#[test]
fn a_counter_count_the_build_did_not_credit_is_refused() {
let directory = TestDirectory::new("counter-count");
let store = store_with_a_flagged_definition(&directory, "counter");
let before = head_of(&store);
let _perturbed = Perturbed::to(SubtreePerturbation::AlterOneCount);
let error = reindex(&store, directory.options()).expect_err("the tail must refuse");
assert!(
error.to_string().contains("where the build credited it"),
"the refusal says what it found: {error}"
);
assert_eq!(head_of(&store), before);
}
#[test]
fn the_same_fixtures_pass_unperturbed() {
for index_type in ["property", "counter"] {
let directory = TestDirectory::new(&format!("unperturbed-{index_type}"));
let store = store_with_a_flagged_definition(&directory, index_type);
let outcome = reindex(&store, directory.options()).expect("reindex");
assert!(outcome.moved_the_head(), "{index_type} rebuilt nothing");
}
}
#[cfg(unix)]
#[test]
fn a_lucene_selection_runs_through_the_prepared_wrapper() {
use crate::index::lucene::documents::binaries::{BinaryTextFallback, BinaryTextPolicy};
let directory = TestDirectory::new("lucene-wiring");
let store = crate::writer::fault_injection::lucene_fixture::write_lucene_reindex_fixture(
&directory.path,
);
let options = ReindexOptions::new()
.with_work_directory(crate::writer::index::WorkDirectory::OperatorNamed(
crate::writer::fault_injection::test_support::reindex_work_directory(&store),
))
.with_binary_text_policy(BinaryTextPolicy::new(BinaryTextFallback::Marker));
let outcome = crate::writer::index::reindex(&store, options).expect("reindex");
assert!(outcome.moved_the_head(), "the wrapper published nothing");
let (path, report) = outcome
.definitions
.first()
.expect("the Lucene definition reached the shared tail");
assert_eq!(path, "/oak:index/lucene");
assert!(
matches!(
report,
crate::writer::index::DefinitionReport::RebuiltIndex { .. }
),
"{report:?}"
);
}