use std::io::Cursor;
use std::process::{Command, Output};
use arrow::array::FixedSizeBinaryArray;
use arrow::ipc::reader::StreamReader;
use tempfile::TempDir;
use uuid::Uuid;
fn gf(project: &TempDir, args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_gf"))
.arg("--project")
.arg(project.path())
.args(args)
.output()
.expect("run same-build gf binary")
}
fn filesystem_unsupported(project: &TempDir) -> bool {
let probe = gf(project, &["source-artifact", "project-capabilities"]);
if probe.status.success() {
return false;
}
String::from_utf8_lossy(&probe.stderr).contains("GF_UNSUPPORTED_FILESYSTEM")
}
fn success_ipc(output: &Output) -> (Vec<String>, Vec<arrow::record_batch::RecordBatch>) {
assert!(
output.status.success(),
"gf failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let reader = StreamReader::try_new(Cursor::new(&output.stdout), None)
.expect("stdout is an Arrow IPC stream");
let columns = reader
.schema()
.fields()
.iter()
.map(|f| f.name().clone())
.collect();
let batches = reader
.collect::<Result<Vec<_>, _>>()
.expect("read Arrow IPC batches");
(columns, batches)
}
fn row_count(batches: &[arrow::record_batch::RecordBatch]) -> usize {
batches
.iter()
.map(arrow::record_batch::RecordBatch::num_rows)
.sum()
}
fn uuid_column(batches: &[arrow::record_batch::RecordBatch], name: &str) -> Vec<Uuid> {
batches
.iter()
.flat_map(|batch| {
let col = batch
.column_by_name(name)
.unwrap()
.as_any()
.downcast_ref::<FixedSizeBinaryArray>()
.unwrap();
(0..batch.num_rows())
.map(|i| Uuid::from_slice(col.value(i)).unwrap())
.collect::<Vec<_>>()
})
.collect()
}
#[test]
#[allow(clippy::too_many_lines)]
fn source_artifact_lifecycle_survives_reopen() {
let project = TempDir::new().expect("temporary project");
if filesystem_unsupported(&project) {
return;
}
let source_uuid = "018f0f4e-7b8c-7000-8000-000000001301";
let scan_uuid = "018f0f4e-7b8c-7000-8000-000000001302";
let ocr_uuid = "018f0f4e-7b8c-7000-8000-000000001303";
let preference_uuid = "018f0f4e-7b8c-7000-8000-000000001304";
let en_prov = gf(
&project,
&[
"source-artifact",
"enable-capability",
"--operation-uuid",
"018f0f4e-7b8c-7000-8000-000000001100",
"--capability-id",
"provenance",
],
);
assert!(
en_prov.status.success(),
"enable provenance failed: {}",
String::from_utf8_lossy(&en_prov.stderr)
);
let en_know = gf(
&project,
&[
"source-artifact",
"enable-capability",
"--operation-uuid",
"018f0f4e-7b8c-7000-8000-000000001101",
"--capability-id",
"knowledge",
],
);
assert!(
en_know.status.success(),
"enable knowledge failed: {}",
String::from_utf8_lossy(&en_know.stderr)
);
let reg_source = gf(
&project,
&[
"source-artifact",
"register-source",
"--operation-uuid",
"018f0f4e-7b8c-7000-8000-000000001201",
"--source-uuid",
source_uuid,
"--label",
"Codex A",
"--source-kind",
"manuscript",
],
);
let (src_cols, src_batches) = success_ipc(®_source);
assert!(src_cols.contains(&"source_uuid".to_owned()));
assert_eq!(row_count(&src_batches), 1);
let scan_payload_file = {
let file = tempfile::NamedTempFile::new_in(project.path()).unwrap();
std::fs::write(file.path(), b"scan bytes").unwrap();
file
};
let reg_scan = gf(
&project,
&[
"source-artifact",
"register-artifact",
"--operation-uuid",
"018f0f4e-7b8c-7000-8000-000000001202",
"--artifact-uuid",
scan_uuid,
"--source-uuid",
source_uuid,
"--artifact-kind",
"raw_scan",
"--media-type",
"image/tiff",
"--payload-file",
scan_payload_file.path().to_str().unwrap(),
],
);
let (scan_cols, scan_batches) = success_ipc(®_scan);
assert!(scan_cols.contains(&"artifact_uuid".to_owned()));
assert_eq!(row_count(&scan_batches), 1);
let ocr_payload_file = {
let file = tempfile::NamedTempFile::new_in(project.path()).unwrap();
std::fs::write(file.path(), b"ocr text").unwrap();
file
};
let derivation_input = format!("{scan_uuid}:artifact");
let reg_ocr = gf(
&project,
&[
"source-artifact",
"register-artifact",
"--operation-uuid",
"018f0f4e-7b8c-7000-8000-000000001203",
"--artifact-uuid",
ocr_uuid,
"--source-uuid",
source_uuid,
"--artifact-kind",
"ocr_text",
"--media-type",
"text/plain",
"--payload-file",
ocr_payload_file.path().to_str().unwrap(),
"--derivation-input",
&derivation_input,
],
);
success_ipc(®_ocr);
let set_pref = gf(
&project,
&[
"source-artifact",
"set-preferred-artifact",
"--operation-uuid",
"018f0f4e-7b8c-7000-8000-000000001204",
"--preference-event-uuid",
preference_uuid,
"--source-uuid",
source_uuid,
"--artifact-uuid",
scan_uuid,
"--reason",
"initial preferred scan",
],
);
success_ipc(&set_pref);
let impact = gf(
&project,
&[
"source-artifact",
"replacement-impact",
"--source-uuid",
source_uuid,
"--artifact-uuid",
ocr_uuid,
],
);
let (_, impact_batches) = success_ipc(&impact);
assert_eq!(row_count(&impact_batches), 2, "expected 2 impact rows");
let impacted = uuid_column(&impact_batches, "artifact_uuid");
let scan = Uuid::parse_str(scan_uuid).unwrap();
let ocr = Uuid::parse_str(ocr_uuid).unwrap();
assert!(impacted.contains(&scan));
assert!(impacted.contains(&ocr));
let set_pref2 = gf(
&project,
&[
"source-artifact",
"set-preferred-artifact",
"--operation-uuid",
"018f0f4e-7b8c-7000-8000-000000001205",
"--preference-event-uuid",
"018f0f4e-7b8c-7000-8000-000000001305",
"--source-uuid",
source_uuid,
"--artifact-uuid",
ocr_uuid,
"--reason",
"better OCR available",
],
);
success_ipc(&set_pref2);
let reopened_project_path = project.path().to_path_buf();
let reopened = Command::new(env!("CARGO_BIN_EXE_gf"))
.arg("--project")
.arg(&reopened_project_path)
.args([
"source-artifact",
"research-lineage",
"--subject-uuid",
ocr_uuid,
"--subject-kind",
"artifact",
"--direction",
"backward",
"--max-depth",
"4",
])
.output()
.expect("run gf binary for lineage");
let (_, lineage_batches) = success_ipc(&reopened);
assert_eq!(row_count(&lineage_batches), 1, "expected 1 backward edge");
let input_uuids = uuid_column(&lineage_batches, "input_uuid");
assert_eq!(
input_uuids,
vec![scan],
"backward lineage must point to scan"
);
let closure = Command::new(env!("CARGO_BIN_EXE_gf"))
.arg("--project")
.arg(&reopened_project_path)
.args([
"source-artifact",
"retention-dependency-closure",
"--scope-uuid",
source_uuid,
])
.output()
.expect("run gf binary for closure");
let (_, closure_batches) = success_ipc(&closure);
assert_eq!(
row_count(&closure_batches),
0,
"retention closure must be empty"
);
let post_impact = Command::new(env!("CARGO_BIN_EXE_gf"))
.arg("--project")
.arg(&reopened_project_path)
.args([
"source-artifact",
"replacement-impact",
"--source-uuid",
source_uuid,
"--artifact-uuid",
scan_uuid,
])
.output()
.expect("run gf binary for post-reopen impact");
let (_, post_impact_batches) = success_ipc(&post_impact);
assert_eq!(
row_count(&post_impact_batches),
1,
"post-reopen impact must be exactly ocr"
);
let post_impacted = uuid_column(&post_impact_batches, "artifact_uuid");
assert_eq!(post_impacted, vec![ocr]);
}