use super::{
super::confirm::count_pending_collisions, run_import_catching_panics,
verify_every_imported_id_reads_back,
};
use crate::gui::library::local::helpers::test_support::{
VALID_ASC, open_temp_db, temp_db_path_for_test, temp_dir_for_test,
};
const OLD_LAYOUT_SCHEMA_SQL: &str = "
CREATE TABLE diagram_entries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
url TEXT NOT NULL UNIQUE,
design_id TEXT
);
CREATE TABLE diagram_details (
id INTEGER PRIMARY KEY AUTOINCREMENT,
entry_id INTEGER NOT NULL UNIQUE,
page_url TEXT NOT NULL,
diagram_image_name TEXT,
diagram_image_data BLOB,
competition_diagram TEXT,
lw_ratio TEXT,
refractive_index TEXT,
index_gear TEXT,
volume TEXT,
facets_count TEXT,
shape TEXT,
designer_info TEXT,
FOREIGN KEY (entry_id) REFERENCES diagram_entries (id) ON DELETE CASCADE
);
CREATE TABLE angle_settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_idx INTEGER NOT NULL,
detail_id INTEGER NOT NULL,
facet TEXT NOT NULL,
angle TEXT NOT NULL,
index_val TEXT NOT NULL,
notes TEXT NOT NULL,
FOREIGN KEY (detail_id) REFERENCES diagram_details (id) ON DELETE CASCADE
);
CREATE TABLE attached_files (
id INTEGER PRIMARY KEY AUTOINCREMENT,
detail_id INTEGER NOT NULL,
name TEXT NOT NULL,
url TEXT NOT NULL,
content BLOB NOT NULL,
FOREIGN KEY (detail_id) REFERENCES diagram_details (id) ON DELETE CASCADE
);
CREATE TABLE custom_gem_materials (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
refractive_index REAL NOT NULL,
dispersion REAL NOT NULL,
birefringence REAL NOT NULL,
absorption_r REAL NOT NULL,
absorption_g REAL NOT NULL,
absorption_b REAL NOT NULL
);
CREATE TABLE diagram_previews (
entry_id INTEGER PRIMARY KEY,
preview_front BLOB,
preview_top BLOB,
preview_material TEXT,
preview_generated_at INTEGER,
FOREIGN KEY (entry_id) REFERENCES diagram_entries (id) ON DELETE CASCADE
);
CREATE TABLE diagram_tilt_curves (
entry_id INTEGER PRIMARY KEY,
curves BLOB,
curve_image BLOB,
generated_at INTEGER,
perf_brilliance_global_min REAL,
perf_brilliance_global_max REAL,
perf_extinction_global_min REAL,
perf_extinction_global_max REAL,
perf_windowing_global_min REAL,
perf_windowing_global_max REAL,
FOREIGN KEY (entry_id) REFERENCES diagram_entries (id) ON DELETE CASCADE
);
";
fn seed_old_layout_catalogue(path: &std::path::Path) -> [i64; 3] {
let conn = indicatrix_vault::db::sqlite::Connection::open(path)
.unwrap_or_else(|e| panic!("open raw pre-migration fixture connection: {e}"));
conn.execute_batch(OLD_LAYOUT_SCHEMA_SQL)
.unwrap_or_else(|e| panic!("create pre-migration schema: {e}"));
conn.execute_batch(
"INSERT INTO diagram_entries (title, url, design_id)
VALUES ('Legacy Dash Index', 'legacy://entry-1', NULL);",
)
.unwrap_or_else(|e| panic!("insert entry 1: {e}"));
let entry_1 = conn.last_insert_rowid();
conn.execute_batch(&format!(
"INSERT INTO diagram_details (
entry_id, page_url, diagram_image_name, competition_diagram, lw_ratio,
refractive_index, index_gear, volume, facets_count, shape, designer_info
) VALUES ({entry_1}, '', NULL, NULL, '1.0', '1.62', '96', '1.1', '55+6', 'Round', 'J. Doe');"
))
.unwrap_or_else(|e| panic!("insert detail 1: {e}"));
let detail_1 = conn.last_insert_rowid();
conn.execute_batch(&format!(
"INSERT INTO angle_settings (order_idx, detail_id, facet, angle, index_val, notes)
VALUES (0, {detail_1}, 'P1', '41.000000', '96-08-16', '');"
))
.unwrap_or_else(|e| panic!("insert angle_settings for entry 1: {e}"));
conn.execute_batch(
"INSERT INTO diagram_entries (title, url, design_id)
VALUES ('Legacy Null Fields', 'legacy://entry-2', NULL);",
)
.unwrap_or_else(|e| panic!("insert entry 2: {e}"));
let entry_2 = conn.last_insert_rowid();
conn.execute_batch(&format!(
"INSERT INTO diagram_details (
entry_id, page_url, diagram_image_name, competition_diagram, lw_ratio,
refractive_index, index_gear, volume, facets_count, shape, designer_info
) VALUES ({entry_2}, '', NULL, NULL, '1.0', NULL, '96', '1.1', '55+6', NULL, NULL);"
))
.unwrap_or_else(|e| panic!("insert detail 2: {e}"));
let detail_2 = conn.last_insert_rowid();
conn.execute_batch(&format!(
"INSERT INTO angle_settings (order_idx, detail_id, facet, angle, index_val, notes)
VALUES (0, {detail_2}, 'P1', '41.000000', '0', '');"
))
.unwrap_or_else(|e| panic!("insert angle_settings for entry 2: {e}"));
conn.execute_batch(
"INSERT INTO diagram_entries (title, url, design_id)
VALUES ('Legacy With Attachment', 'legacy://entry-3', NULL);",
)
.unwrap_or_else(|e| panic!("insert entry 3: {e}"));
let entry_3 = conn.last_insert_rowid();
conn.execute_batch(&format!(
"INSERT INTO diagram_details (
entry_id, page_url, diagram_image_name, competition_diagram, lw_ratio,
refractive_index, index_gear, volume, facets_count, shape, designer_info
) VALUES ({entry_3}, '', NULL, NULL, '1.0', '1.72', '96', '1.1', '55+6', 'Round', 'A. Cutter');"
))
.unwrap_or_else(|e| panic!("insert detail 3: {e}"));
let detail_3 = conn.last_insert_rowid();
conn.execute_batch(&format!(
"INSERT INTO angle_settings (order_idx, detail_id, facet, angle, index_val, notes)
VALUES (0, {detail_3}, 'P1', '41.000000', '0', '');"
))
.unwrap_or_else(|e| panic!("insert angle_settings for entry 3: {e}"));
conn.execute_batch(&format!(
"INSERT INTO attached_files (detail_id, name, url, content)
VALUES ({detail_3}, 'legacy.asc', '', X'01020304');"
))
.unwrap_or_else(|e| panic!("insert attached_files for entry 3: {e}"));
conn.execute_batch(&format!(
"INSERT INTO diagram_previews (
entry_id, preview_front, preview_top, preview_material, preview_generated_at
) VALUES ({entry_3}, X'0102', X'0304', 'Quartz', 1700000000);"
))
.unwrap_or_else(|e| panic!("insert diagram_previews for entry 3: {e}"));
drop(conn);
[entry_1, entry_2, entry_3]
}
fn write_single_sidecar_pair(dir: &std::path::Path) {
let spec = indicatrix_cut_core::templates::TEMPLATES
.first()
.expect("TEMPLATES is non-empty");
let design = indicatrix_cut_core::design::Design::new(
indicatrix_cut_core::preform::PreformSpec::cylinder(
spec.gear_teeth.unsigned_abs() as usize,
1.5,
1.0,
1.5,
),
spec.schedule_meta(),
spec.tiers(),
);
design
.solve()
.unwrap_or_else(|e| panic!("template {:?} failed to solve: {e:?}", spec.name));
let asc_filename = "old_layout_sidecar.asc";
let paired = indicatrix_cut_core::native::save_paired(
&design,
asc_filename.to_string(),
None,
None,
None,
)
.unwrap_or_else(|e| panic!("save_paired for template {:?}: {e}", spec.name));
std::fs::write(dir.join(asc_filename), &paired.asc_text)
.unwrap_or_else(|e| panic!("write {asc_filename}: {e}"));
std::fs::write(
dir.join("old_layout_sidecar.indicatrix.toml"),
&paired.native_toml,
)
.unwrap_or_else(|e| panic!("write old_layout_sidecar.indicatrix.toml: {e}"));
}
#[test]
fn import_into_a_catalogue_created_in_the_old_column_layout_survives_the_migration_and_reads_back()
{
let db_path = temp_db_path_for_test("old_layout_migrated");
let [entry_1, entry_2, entry_3] = seed_old_layout_catalogue(&db_path);
let db = open_temp_db(&db_path);
let corpus_dir = temp_dir_for_test("old_layout_migrated_corpus");
std::fs::write(corpus_dir.join("valid.asc"), VALID_ASC).expect("write valid.asc");
write_single_sidecar_pair(&corpus_dir);
std::fs::write(corpus_dir.join("bad.asc"), []).expect("write bad.asc (empty -> unparsable)");
let outcome = run_import_catching_panics(&db, &corpus_dir).unwrap_or_else(|panic_msg| {
panic!(
"import_path PANICKED against a catalogue migrated from the old column \
layout: {panic_msg}"
)
});
assert_eq!(
outcome.imported_ids.len(),
2,
"valid.asc + the sidecar pair's .asc must both import: {}",
outcome.summary
);
assert!(
outcome.had_failures,
"the empty bad.asc must be reported as a failure/skip: {}",
outcome.summary
);
assert!(
outcome.summary.contains("Imported 2 file(s)"),
"expected exactly 2 imported: {}",
outcome.summary
);
assert!(
outcome.summary.contains("1 skipped"),
"expected exactly 1 skipped: {}",
outcome.summary
);
assert!(
!outcome.had_collision,
"the first import must not collide with anything: {}",
outcome.summary
);
verify_every_imported_id_reads_back(&db, &[entry_1, entry_2, entry_3]);
verify_every_imported_id_reads_back(&db, &outcome.imported_ids);
let preview_material = db
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get_preview_material(entry_3)
.unwrap_or_else(|e| panic!("get_preview_material({entry_3}) failed: {e}"));
assert_eq!(
preview_material.as_deref(),
Some("Quartz"),
"entry 3's pre-existing preview row must survive the migration"
);
let (collisions, total) = count_pending_collisions(&db, &corpus_dir, true);
assert_eq!(
(collisions, total),
(2, 3),
"the 2 previously-imported files must collide on re-import; the empty file is \
still a candidate but was never actually saved"
);
let second = run_import_catching_panics(&db, &corpus_dir).unwrap_or_else(|panic_msg| {
panic!("import_path PANICKED on the second (collision) pass: {panic_msg}")
});
assert_eq!(
second.imported_ids.len(),
2,
"the second pass replaces both real files: {}",
second.summary
);
assert!(
second.had_collision,
"the second pass must report a collision: {}",
second.summary
);
verify_every_imported_id_reads_back(&db, &second.imported_ids);
let _ = std::fs::remove_dir_all(&corpus_dir);
let _ = std::fs::remove_file(&db_path);
}