#![cfg(feature = "recalc")]
use spreadsheet_mcp::diff::{Change, calculate_changeset, names::NameDiff, tables::TableDiff};
use std::fs::File;
use std::io::{Read, Write};
use std::path::PathBuf;
use umya_spreadsheet::{Spreadsheet, structs::Table};
use zip::write::FileOptions;
use zip::{ZipArchive, ZipWriter};
#[path = "./support/mod.rs"]
mod support;
struct DiffScenario {
_temp_dir: tempfile::TempDir,
base_path: PathBuf,
fork_path: PathBuf,
}
impl DiffScenario {
fn new() -> Self {
let temp_dir = tempfile::tempdir().expect("failed to create temp dir");
let base_path = temp_dir.path().join("base.xlsx");
let fork_path = temp_dir.path().join("fork.xlsx");
Self {
_temp_dir: temp_dir,
base_path,
fork_path,
}
}
fn setup<F1, F2>(&self, setup_base: F1, setup_fork: F2)
where
F1: FnOnce(&mut Spreadsheet),
F2: FnOnce(&mut Spreadsheet),
{
let mut base_book = umya_spreadsheet::new_file();
setup_base(&mut base_book);
umya_spreadsheet::writer::xlsx::write(&base_book, &self.base_path)
.expect("failed to write base");
let mut fork_book = umya_spreadsheet::new_file();
setup_fork(&mut fork_book);
umya_spreadsheet::writer::xlsx::write(&fork_book, &self.fork_path)
.expect("failed to write fork");
}
fn run_diff(&self, sheet_filter: Option<&str>) -> Vec<Change> {
calculate_changeset(&self.base_path, &self.fork_path, sheet_filter).expect("diff failed")
}
}
fn inject_defined_names(path: &PathBuf, names: &[(&str, &str)]) {
let file = File::open(path).unwrap();
let mut archive = ZipArchive::new(file).unwrap();
let mut workbook_xml = String::new();
let mut files = std::collections::HashMap::new();
for i in 0..archive.len() {
let mut file = archive.by_index(i).unwrap();
let name = file.name().to_string();
let mut buffer = Vec::new();
file.read_to_end(&mut buffer).unwrap();
if name == "xl/workbook.xml" {
workbook_xml = String::from_utf8(buffer).unwrap();
} else {
files.insert(name, buffer);
}
}
let names_xml: String = names
.iter()
.map(|(n, f)| format!("<definedName name=\"{}\">{}</definedName>", n, f))
.collect();
let replacement = format!("<definedNames>{}</definedNames></workbook>", names_xml);
workbook_xml = workbook_xml.replace("</workbook>", &replacement);
files.insert("xl/workbook.xml".to_string(), workbook_xml.into_bytes());
let file = File::create(path).unwrap();
let mut zip = ZipWriter::new(file);
for (name, content) in files {
zip.start_file(name, FileOptions::default()).unwrap();
zip.write_all(&content).unwrap();
}
zip.finish().unwrap();
}
#[test]
fn test_defined_name_changes() {
let scenario = DiffScenario::new();
scenario.setup(|_| {}, |_| {});
inject_defined_names(&scenario.base_path, &[("Input", "Sheet1!$A$1")]);
inject_defined_names(
&scenario.fork_path,
&[("Input", "Sheet1!$B$1"), ("Output", "Sheet1!$C$1")],
);
let diffs = scenario.run_diff(None);
let input_mod = diffs.iter().find(
|d| matches!(d, Change::Name(NameDiff::NameModified { name, .. }) if name == "Input"),
);
assert!(input_mod.is_some(), "Expected Input modification");
if let Some(Change::Name(NameDiff::NameModified {
old_formula,
new_formula,
..
})) = input_mod
{
assert_eq!(old_formula, "Sheet1!$A$1");
assert_eq!(new_formula, "Sheet1!$B$1");
}
let output_add = diffs
.iter()
.find(|d| matches!(d, Change::Name(NameDiff::NameAdded { name, .. }) if name == "Output"));
assert!(output_add.is_some(), "Expected Output addition");
}
#[test]
fn test_table_changes() {
let scenario = DiffScenario::new();
scenario.setup(
|book| {
let sheet = book.get_sheet_mut(&0).unwrap();
sheet.set_name("Sheet1");
let mut table = Table::default();
table.set_name("Table1");
table.set_display_name("Sales");
table.set_area(((1, 1), (3, 5))); sheet.add_table(table);
},
|book| {
let sheet = book.get_sheet_mut(&0).unwrap();
sheet.set_name("Sheet1");
let mut table = Table::default();
table.set_name("Table1");
table.set_display_name("Sales");
table.set_area(((1, 1), (3, 10))); sheet.add_table(table);
},
);
let diffs = scenario.run_diff(None);
let sales_mod = diffs.iter().find(|d| matches!(d, Change::Table(TableDiff::TableModified { display_name, .. }) if display_name == "Sales"));
assert!(sales_mod.is_some(), "Expected Sales table modification");
if let Some(Change::Table(TableDiff::TableModified {
sheet,
old_range,
new_range,
..
})) = sales_mod
{
assert_eq!(sheet, "Sheet1");
assert_eq!(old_range, "A1:C5");
assert_eq!(new_range, "A1:C10");
}
}
#[test]
fn test_table_addition() {
let scenario = DiffScenario::new();
scenario.setup(
|book| {
let sheet = book.get_sheet_mut(&0).unwrap();
sheet.set_name("Sheet1");
},
|book| {
let sheet = book.get_sheet_mut(&0).unwrap();
sheet.set_name("Sheet1");
let mut table = Table::default();
table.set_name("NewTable");
table.set_display_name("NewTable");
table.set_area(((1, 1), (2, 2)));
sheet.add_table(table);
},
);
let diffs = scenario.run_diff(None);
let added = diffs.iter().find(|d| matches!(d, Change::Table(TableDiff::TableAdded { display_name, .. }) if display_name == "NewTable"));
assert!(added.is_some());
}