From 95f5ccb31283fc4d3583036adad6deb81996d10b Mon Sep 17 00:00:00 2001 From: nilsk Date: Fri, 13 Jun 2025 15:25:11 +0200 Subject: [PATCH 1/4] covered all functions from slides with unit tests --- src/slide.rs | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 1 deletion(-) diff --git a/src/slide.rs b/src/slide.rs index 3923446..a309e33 100644 --- a/src/slide.rs +++ b/src/slide.rs @@ -164,6 +164,16 @@ impl Slide { /// Ensures that each image referenced by its ID is correctly /// linked to the actual internal resource paths stored in the slide. /// This method is typically used internally after parsing a slide + /// + /// # Notes + /// + /// Internally those are the values image references are holding + /// + /// | Parameter | Example value | + /// |---------- |---------------------- | + /// | `id` | *rId2* | + /// | `target` | *../media/image2.png* | + /// pub fn link_images(&mut self) { let id_to_target: HashMap = self.images .iter() @@ -188,7 +198,7 @@ impl Slide { .to_string() } - /// Compresses the image data and returning it as a jpg byte slice + /// Compresses the image data and returning it as a `jpg` byte slice /// /// # Parameter /// @@ -197,6 +207,10 @@ impl Slide { /// # Returns /// /// - `Vec`: Returns the compressed and converted jpg byte array + /// + /// # Notes + /// + /// All images will be converted to `jpg` pub fn compress_image(&self, image_data: &[u8]) -> Option> { let img = match image::load_from_memory(image_data) { Ok(image) => image, @@ -212,6 +226,93 @@ impl Slide { None } } +} + +#[cfg(test)] +mod tests { + use std::fs; + use std::path::PathBuf; + // Note this useful idiom: importing names from outer (for mod tests) scope. + use super::*; + + fn mock_slide() -> Slide { + Slide { + rel_path: "ppt/slides/slide1.xml".to_string(), + slide_number: 1, + elements: vec![], + images: vec![], + image_data: HashMap::new(), + config: ParserConfig::default(), + } + } + + fn load_image_data(filename: &str) -> Vec { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("tests"); + path.push("test_data"); + path.push(filename); + fs::read(path).expect("Unable to read test data file") + } + + #[test] + fn test_extract_slide_number() { + let input = "ppt/slides/slide5.xml"; + + let actual = Slide::extract_slide_number(input).unwrap(); + let expected: u32 = 5; + + assert_eq!(actual, expected); + } + + #[test] + fn test_get_image_extension() { + let slide = mock_slide(); + let input = "../media/image1.png"; + + let actual = slide.get_image_extension(input); + let expected = "png"; + + assert_eq!(actual, expected); + } + + #[test] + fn test_link_images() { + let mut slide = mock_slide(); + slide.images.push(ImageReference { id: "rId2".to_string(), target: "../media/image1.png".to_string() }); + slide.elements.push(SlideElement::Image(ImageReference { id: "rId2".to_string(), target: "".to_string() })); + slide.link_images(); + + if let SlideElement::Image(img_ref) = &slide.elements[0] { + assert_eq!(img_ref.target, "../media/image1.png"); + } + } + + #[test] + fn test_image_compression_reduces_size() { + let mut slide = mock_slide(); + slide.config.quality = 50; + + let raw_image = load_image_data("example-image.jpg"); + + if let Some(compression_result) = slide.compress_image(&*raw_image) { + assert!(compression_result.len() < raw_image.len()); + } else { + panic!("Compression failed"); + } + } + + #[test] + fn test_compressed_image_is_valid_jpg() { + let slide = mock_slide(); + let raw_image = load_image_data("example-image.jpg"); + + if let Some(compression_result) = slide.compress_image(&*raw_image) { + let result = image::load_from_memory(&compression_result); + assert!(result.is_ok()); + } else { + panic!("Compression failed"); + } + } } \ No newline at end of file From edba9ecdd1df5e96fb654a907a99325e2f2b1ae3 Mon Sep 17 00:00:00 2001 From: nilsk Date: Fri, 13 Jun 2025 15:31:21 +0200 Subject: [PATCH 2/4] included an image for testing purposes --- tests/test_data/example-image.jpg | Bin 0 -> 27854 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/test_data/example-image.jpg diff --git a/tests/test_data/example-image.jpg b/tests/test_data/example-image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bf5d7de0651682bc4d26a58a31440a3efbec7f2f GIT binary patch literal 27854 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 Jijc>X{|EZRs(Ana literal 0 HcmV?d00001 From d0386b5e87ca416c4c0bf36e76c2e8146779942e Mon Sep 17 00:00:00 2001 From: nilsk Date: Fri, 13 Jun 2025 17:08:44 +0200 Subject: [PATCH 3/4] tested all xml parsing functions with real well-formated xml files --- src/parse_xml.rs | 573 +++++++++++++++++++++ src/slide.rs | 2 +- tests/test_data/xml/complex_table.xml | 86 ++++ tests/test_data/xml/empty_table.xml | 35 ++ tests/test_data/xml/multilevel_list.xml | 50 ++ tests/test_data/xml/non_table_graphic.xml | 7 + tests/test_data/xml/paragraph_empty.xml | 2 + tests/test_data/xml/paragraph_multiple.xml | 14 + tests/test_data/xml/paragraph_single.xml | 6 + tests/test_data/xml/pic_with_image.xml | 32 ++ tests/test_data/xml/pic_without_blip.xml | 17 + tests/test_data/xml/pic_without_embed.xml | 24 + tests/test_data/xml/run_empty.xml | 4 + tests/test_data/xml/run_no_format.xml | 4 + tests/test_data/xml/run_styles.xml | 4 + tests/test_data/xml/simple_list.xml | 32 ++ tests/test_data/xml/simple_table.xml | 50 ++ tests/test_data/xml/tx_body.xml | 21 + 18 files changed, 962 insertions(+), 1 deletion(-) create mode 100644 tests/test_data/xml/complex_table.xml create mode 100644 tests/test_data/xml/empty_table.xml create mode 100644 tests/test_data/xml/multilevel_list.xml create mode 100644 tests/test_data/xml/non_table_graphic.xml create mode 100644 tests/test_data/xml/paragraph_empty.xml create mode 100644 tests/test_data/xml/paragraph_multiple.xml create mode 100644 tests/test_data/xml/paragraph_single.xml create mode 100644 tests/test_data/xml/pic_with_image.xml create mode 100644 tests/test_data/xml/pic_without_blip.xml create mode 100644 tests/test_data/xml/pic_without_embed.xml create mode 100644 tests/test_data/xml/run_empty.xml create mode 100644 tests/test_data/xml/run_no_format.xml create mode 100644 tests/test_data/xml/run_styles.xml create mode 100644 tests/test_data/xml/simple_list.xml create mode 100644 tests/test_data/xml/simple_table.xml create mode 100644 tests/test_data/xml/tx_body.xml diff --git a/src/parse_xml.rs b/src/parse_xml.rs index 44d2ca7..6388fcb 100644 --- a/src/parse_xml.rs +++ b/src/parse_xml.rs @@ -358,4 +358,577 @@ fn parse_run(r_node: &Node) -> Result { } } Ok(Run { text, formatting }) +} + +#[cfg(test)] +mod tests { + use std::fs; + use std::path::PathBuf; + use super::*; + + fn load_xml(filename: &str) -> String { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("tests"); + path.push("test_data"); + path.push("xml"); + path.push(filename); + fs::read_to_string(path).expect("Unable to read test data file") + } + + fn normalize_test_string(input: &str) -> String { + input + .trim_start_matches('\u{feff}') // remove BOM + .replace("\r\n", "\n") // normalize line breaks + .replace(" ", "\t") // replace 4 whitespaces with a tab + .trim() // trim leading and trailing whitespace + .to_string() + } + + #[test] + fn test_parse_text() { + let xml_data = load_xml("tx_body.xml"); + let doc = Document::parse(&*xml_data).expect("Parsing XML failes"); + let tx_body_node = doc.root_element(); + + match parse_text(&tx_body_node) { + Ok(SlideElement::Text(text_element)) => { + assert_eq!(text_element.runs.len(), 3); + assert_eq!(normalize_test_string(&text_element.runs[0].text), normalize_test_string("Hello")); + assert_eq!(normalize_test_string(&text_element.runs[1].text), normalize_test_string("World")); + assert_eq!(normalize_test_string(&text_element.runs[2].text), normalize_test_string("!")); + }, + Err(_) => panic!("Fehler beim Parsen der XML-Datei"), + _ => {} + } + } + + #[test] + fn test_parse_run_with_format() { + let xml_data = load_xml("run_styles.xml"); + let doc = Document::parse(&*xml_data).expect("Parsing XML failed"); + let r_node = doc.root_element(); + + match parse_run(&r_node) { + Ok(run) => { + assert_eq!(normalize_test_string(&run.text), normalize_test_string("Formatted text")); + assert!(run.formatting.bold); + assert!(run.formatting.italic); + assert!(run.formatting.underlined); + assert_eq!(run.formatting.lang, "de-DE"); + }, + Err(_) => panic!("Fehler beim Parsen des Runs mit Formatierung") + } + } + + #[test] + fn test_parse_run_no_format() { + let xml_data = load_xml("run_no_format.xml"); + let doc = Document::parse(&*xml_data).expect("Parsing XML failed"); + let r_node = doc.root_element(); + + match parse_run(&r_node) { + Ok(run) => { + assert_eq!(normalize_test_string(&run.text), normalize_test_string("Unformatted text")); + assert!(!run.formatting.bold); + assert!(!run.formatting.italic); + assert!(!run.formatting.underlined); + }, + Err(_) => panic!("Fehler beim Parsen des Runs ohne Formatierung") + } + } + + + #[test] + fn test_parse_run_empty_text() { + let xml_data = load_xml("run_empty.xml"); + let doc = Document::parse(&*xml_data).expect("Parsing XML failed"); + let r_node = doc.root_element(); + + match parse_run(&r_node) { + Ok(run) => { + assert_eq!(run.text, ""); + }, + Err(_) => panic!("Failed to parse an empty Run") + } + } + + #[test] + fn test_parse_paragraph_single() { + let xml_data = load_xml("paragraph_single.xml"); + let doc = Document::parse(&*xml_data).expect("Parsing XML failed"); + let p_node = doc.root_element(); + + match parse_paragraph(&p_node, true) { + Ok(runs) => { + assert_eq!(runs.len(), 1); + assert_eq!(normalize_test_string(&runs[0].text), normalize_test_string("Single run\n")); + }, + Err(_) => panic!("Failed to parse paragraph with a single run") + } + } + + #[test] + fn test_parse_paragraph_multiple() { + let xml_data = load_xml("paragraph_multiple.xml"); + let doc = Document::parse(&*xml_data).expect("Parsing XML failed"); + let p_node = doc.root_element(); + + match parse_paragraph(&p_node, true) { + Ok(runs) => { + assert_eq!(runs.len(), 3); + assert_eq!(normalize_test_string(&runs[0].text), normalize_test_string("First run")); + assert_eq!(normalize_test_string(&runs[1].text), normalize_test_string("Second run")); + assert_eq!(normalize_test_string(&runs[2].text), normalize_test_string("Third run\n")); + assert!(runs[1].formatting.bold); + assert!(runs[2].formatting.italic); + }, + Err(_) => panic!("Failed to parse paragraph with multiple runs (`add_new_line: true)") + } + + match parse_paragraph(&p_node, false) { + Ok(runs) => { + assert_eq!(runs.len(), 3); + assert!(!runs[2].text.ends_with('\n')); + }, + Err(_) => panic!("Failed to parse paragraph with multiple runs (`add_new_line: false)`") + } + } + + #[test] + fn test_parse_paragraph_empty() { + let xml_data = load_xml("paragraph_empty.xml"); + let doc = Document::parse(&*xml_data).expect("Parsing XML failed"); + let p_node = doc.root_element(); + + match parse_paragraph(&p_node, true) { + Ok(runs) => { + assert_eq!(runs.len(), 0); + }, + Err(_) => panic!("Failed to parse paragraph with empty runs") + } + } + + #[test] + fn test_parse_list_properties_unordered() { + // Test for unordered list properties + let xml_data = load_xml("simple_list.xml"); + let doc = Document::parse(&*xml_data).expect("Failed to parse XML"); + + let p_node = doc.root_element() + .children() + .find(|n| n.is_element() && n.tag_name().name() == "p") + .expect("No paragraph element found"); + + match parse_list_properties(&p_node) { + Ok((level, is_ordered)) => { + assert_eq!(level, 0, "List level should be 0"); + assert!(is_ordered, "List should be identified as ordered due to buChar element"); + }, + Err(_) => panic!("Failed to parse list properties") + } + } + + #[test] + fn test_parse_list_properties_ordered() { + // Test for ordered list properties + let xml_data = load_xml("multilevel_list.xml"); + let doc = Document::parse(&*xml_data).expect("Failed to parse XML"); + + // Get the first paragraph (level 0 with buAutoNum) + let p_node = doc.root_element() + .children() + .find(|n| n.is_element() && n.tag_name().name() == "p") + .expect("No paragraph element found"); + + match parse_list_properties(&p_node) { + Ok((level, is_ordered)) => { + assert_eq!(level, 0, "List level should be 0"); + assert!(is_ordered, "List should be identified as ordered due to buAutoNum element"); + }, + Err(_) => panic!("Failed to parse ordered list properties") + } + + // Get the second paragraph (level 1 with buChar) + let p_node = doc.root_element() + .children() + .filter(|n| n.is_element() && n.tag_name().name() == "p") + .nth(1) + .expect("Second paragraph element not found"); + + match parse_list_properties(&p_node) { + Ok((level, is_ordered)) => { + assert_eq!(level, 1, "List level should be 1"); + assert!(is_ordered, "List should be identified as ordered due to buChar element"); + }, + Err(_) => panic!("Failed to parse level 1 list properties") + } + + // Get the fourth paragraph (level 2 with buAutoNum) + let p_node = doc.root_element() + .children() + .filter(|n| n.is_element() && n.tag_name().name() == "p") + .nth(3) + .expect("Fourth paragraph element not found"); + + match parse_list_properties(&p_node) { + Ok((level, is_ordered)) => { + assert_eq!(level, 2, "List level should be 2"); + assert!(is_ordered, "Level 2 list should be identified as ordered"); + }, + Err(_) => panic!("Failed to parse level 2 list properties") + } + } + + #[test] + fn test_parse_simple_list() { + // Test for parsing a complete simple list + let xml_data = load_xml("simple_list.xml"); + let doc = Document::parse(&*xml_data).expect("Failed to parse XML"); + let tx_body_node = doc.root_element(); + + match parse_list(&tx_body_node) { + Ok(SlideElement::List(list)) => { + assert_eq!(list.items.len(), 3, "List should have 3 items"); + + // Check the first item + assert_eq!(list.items[0].level, 0, "First item should be level 0"); + assert!(list.items[0].is_ordered, "First item should be ordered (has buChar)"); + assert_eq!(normalize_test_string(&list.items[0].runs[0].text), normalize_test_string("First item\n"), "First item text mismatch"); + + // Check the second item + assert_eq!(list.items[1].level, 0, "Second item should be level 0"); + assert!(list.items[1].is_ordered, "Second item should be ordered (has buChar)"); + assert_eq!(normalize_test_string(&list.items[1].runs[0].text), normalize_test_string("Second item\n"), "Second item text mismatch"); + + // Check the third item + assert_eq!(list.items[2].level, 0, "Third item should be level 0"); + assert!(list.items[2].is_ordered, "Third item should be ordered (has buChar)"); + assert_eq!(normalize_test_string(&list.items[2].runs[0].text), normalize_test_string("Third item\n"), "Third item text mismatch"); + }, + Ok(_) => panic!("Expected a List element but got something else"), + Err(_) => panic!("Failed to parse simple list") + } + } + + #[test] + fn test_parse_multilevel_list() { + // Test for parsing a multilevel list + let xml_data = load_xml("multilevel_list.xml"); + let doc = Document::parse(&*xml_data).expect("Failed to parse XML"); + let tx_body_node = doc.root_element(); + + match parse_list(&tx_body_node) { + Ok(SlideElement::List(list)) => { + assert_eq!(list.items.len(), 5, "List should have 5 items"); + + // Check first item (level 0, ordered) + assert_eq!(list.items[0].level, 0, "First item should be level 0"); + assert!(list.items[0].is_ordered, "First item should be ordered"); + assert_eq!(normalize_test_string(&list.items[0].runs[0].text), normalize_test_string("Main topic\n"), "First item text mismatch"); + + // Check second item (level 1, unordered but detected as ordered due to buChar) + assert_eq!(list.items[1].level, 1, "Second item should be level 1"); + assert!(list.items[1].is_ordered, "Second item should be detected as ordered due to buChar"); + assert_eq!(normalize_test_string(&list.items[1].runs[0].text), normalize_test_string("Subtopic bullet\n"), "Second item text mismatch"); + + // Check fourth item (level 2, ordered) + assert_eq!(list.items[3].level, 2, "Fourth item should be level 2"); + assert!(list.items[3].is_ordered, "Fourth item should be ordered"); + assert_eq!(normalize_test_string(&list.items[3].runs[0].text), normalize_test_string("Numbered sub-subtopic\n"), "Fourth item text mismatch"); + + // Check fifth item (back to level 0) + assert_eq!(list.items[4].level, 0, "Fifth item should be level 0"); + assert!(list.items[4].is_ordered, "Fifth item should be ordered"); + assert_eq!(normalize_test_string(&list.items[4].runs[0].text), normalize_test_string("Second main topic\n"), "Fifth item text mismatch"); + }, + Ok(_) => panic!("Expected a List element but got something else"), + Err(_) => panic!("Failed to parse multilevel list") + } + } + + /// Test for a simple table for a cell with a single paragraph + #[test] + fn test_parse_table_cell_simple() { + let xml_data = load_xml("simple_table.xml"); + let doc = Document::parse(&*xml_data).expect("Parsing XML failed"); + + let tc_node = doc.root_element() + .descendants() + .find(|n| n.is_element() && n.tag_name().name() == "tc") + .expect("Couldn't find tc node"); + + match parse_table_cell(&tc_node) { + Ok(cell) => { + assert_eq!(cell.runs.len(), 1); + assert_eq!(normalize_test_string(&cell.runs[0].text), normalize_test_string("Cell 1,1")); + }, + Err(_) => panic!("Failed to parse the table cell") + } + } + + /// Test for a complex table with multiple paragraphs in a table cell + #[test] + fn test_parse_table_cell_complex() { + let xml_data = load_xml("complex_table.xml"); + let doc = Document::parse(&*xml_data).expect("Parsing XML failed"); + + // second row, first cell + let tc_node = doc.root_element() + .descendants() + .filter(|n| n.is_element() && n.tag_name().name() == "tc") + .nth(3) + .expect("Failed to find table cell with multiple paragraphs"); + + match parse_table_cell(&tc_node) { + Ok(cell) => { + assert_eq!(cell.runs.len(), 3); + assert_eq!(normalize_test_string(&cell.runs[0].text), normalize_test_string("Multiple")); + assert_eq!(normalize_test_string(&cell.runs[1].text), normalize_test_string("paragraphs")); + assert_eq!(normalize_test_string(&cell.runs[2].text), normalize_test_string("in one cell")); + }, + Err(_) => panic!("Failed to parse table cell with multiple paragraphs") + } + } + #[test] + fn test_parse_table_cell_empty() { + let xml_data = load_xml("empty_table.xml"); + let doc = Document::parse(&*xml_data).expect("Parsing XML failed"); + + let tc_node = doc.root_element() + .descendants() + .find(|n| n.is_element() && n.tag_name().name() == "tc") + .expect("Failed to find empty table cell"); + + match parse_table_cell(&tc_node) { + Ok(cell) => { + assert_eq!(cell.runs.len(), 0); + }, + Err(_) => panic!("Failed to parse empty table cell") + } + } + + #[test] + fn test_parse_table_row_simple() { + let xml_data = load_xml("simple_table.xml"); + let doc = Document::parse(&*xml_data).expect("Parsing XML failed"); + + let tr_node = doc.root_element() + .descendants() + .find(|n| n.is_element() && n.tag_name().name() == "tr") + .expect("Couldn't find tc node"); + + match parse_table_row(&tr_node) { + Ok(row) => { + assert_eq!(row.cells.len(), 2); + assert_eq!(normalize_test_string(&row.cells[0].runs[0].text), normalize_test_string("Cell 1,1")); + assert_eq!(normalize_test_string(&row.cells[1].runs[0].text), normalize_test_string("Cell 1,2")); + }, + Err(_) => panic!("Failed to parse the table row") + } + } + + #[test] + fn test_parse_table_row_complex() { + let xml_data = load_xml("complex_table.xml"); + let doc = Document::parse(&*xml_data).expect("Parsing XML failed"); + + let tr_node = doc.root_element() + .descendants() + .filter(|n| n.is_element() && n.tag_name().name() == "tr") + .nth(0) // Erste Zeile mit fetten Überschriften + .expect("Couldn't find a table row with formatting"); + + match parse_table_row(&tr_node) { + Ok(row) => { + assert_eq!(row.cells.len(), 3); + for i in 0..3 { + assert!(row.cells[i].runs[0].formatting.bold); + assert!(normalize_test_string(&row.cells[i].runs[0].text).starts_with("Heading")); + } + }, + Err(_) => panic!("Failed to parse a table row with formatting") + } + } + + #[test] + fn test_parse_simple_table() { + // Test for a simple table with 2x2 structure + let xml_data = load_xml("simple_table.xml"); + let doc = Document::parse(&*xml_data).expect("Failed to parse XML"); + + let tbl_node = doc.root_element() + .descendants() + .find(|n| n.is_element() && n.tag_name().name() == "tbl") + .expect("No table element found"); + + match parse_table(&tbl_node) { + Ok(table) => { + assert_eq!(table.rows.len(), 2, "Table should have 2 rows"); + assert_eq!(table.rows[0].cells.len(), 2, "First row should have 2 cells"); + assert_eq!(table.rows[1].cells.len(), 2, "Second row should have 2 cells"); + + // Check contents of the first row + assert_eq!(normalize_test_string(&table.rows[0].cells[0].runs[0].text), normalize_test_string("Cell 1,1"), "Cell content mismatch"); + assert_eq!(normalize_test_string(&table.rows[0].cells[1].runs[0].text), normalize_test_string("Cell 1,2"), "Cell content mismatch"); + + // Check contents of the second row + assert_eq!(normalize_test_string(&table.rows[1].cells[0].runs[0].text), normalize_test_string("Cell 2,1"), "Cell content mismatch"); + assert_eq!(normalize_test_string(&table.rows[1].cells[1].runs[0].text), normalize_test_string("Cell 2,2"), "Cell content mismatch"); + }, + Err(_) => panic!("Failed to parse table structure") + } + } + + #[test] + fn test_parse_complex_table() { + // Test for a complex table with different formatting and multiple paragraphs + let xml_data = load_xml("complex_table.xml"); + let doc = Document::parse(&*xml_data).expect("Failed to parse XML"); + + let tbl_node = doc.root_element() + .descendants() + .find(|n| n.is_element() && n.tag_name().name() == "tbl") + .expect("No table element found"); + + match parse_table(&tbl_node) { + Ok(table) => { + assert_eq!(table.rows.len(), 2, "Table should have 2 rows"); + assert_eq!(table.rows[0].cells.len(), 3, "First row should have 3 cells"); + assert_eq!(table.rows[1].cells.len(), 3, "Second row should have 3 cells"); + + // Check bold formatting in headers + for i in 0..3 { + assert!(table.rows[0].cells[i].runs[0].formatting.bold, "Header cell should have bold formatting"); + assert!(normalize_test_string(&table.rows[0].cells[i].runs[0].text).starts_with("Heading"), "Header should start with 'Heading'"); + } + + // Check the cell with multiple paragraphs + assert_eq!(table.rows[1].cells[0].runs.len(), 3); + assert_eq!(normalize_test_string(&table.rows[1].cells[0].runs[0].text), normalize_test_string("Multiple"), "First paragraph content mismatch"); + assert_eq!(normalize_test_string(&table.rows[1].cells[0].runs[1].text), normalize_test_string("paragraphs"), "Second paragraph content mismatch"); + assert_eq!(normalize_test_string(&table.rows[1].cells[0].runs[2].text), normalize_test_string("in one cell"), "Third paragraph content mismatch"); + + // Check the cell with italic text + assert!(table.rows[1].cells[1].runs[0].formatting.italic, "Text should have italic formatting"); + assert_eq!(normalize_test_string(&table.rows[1].cells[1].runs[0].text), normalize_test_string("Cursive"), "Italic text content mismatch"); + }, + Err(_) => panic!("Failed to parse complex table structure") + } + } + + #[test] + fn test_parse_empty_table() { + // Test for a table with empty cells + let xml_data = load_xml("empty_table.xml"); + let doc = Document::parse(&*xml_data).expect("Failed to parse XML"); + + let tbl_node = doc.root_element() + .descendants() + .find(|n| n.is_element() && n.tag_name().name() == "tbl") + .expect("No table element found"); + + match parse_table(&tbl_node) { + Ok(table) => { + assert_eq!(table.rows.len(), 2, "Table should have 2 rows"); + assert_eq!(table.rows[0].cells.len(), 2, "First row should have 2 cells"); + assert_eq!(table.rows[1].cells.len(), 2, "Second row should have 2 cells"); + + // Check that empty cells have no runs + assert_eq!(table.rows[0].cells[0].runs.len(), 0, "Empty cell should have no runs"); + assert_eq!(table.rows[0].cells[1].runs.len(), 0, "Empty cell should have no runs"); + assert_eq!(table.rows[1].cells[0].runs.len(), 0, "Empty cell should have no runs"); + + // Check the one cell with content + assert_eq!(table.rows[1].cells[1].runs.len(), 1, "Cell should have one run"); + assert_eq!(normalize_test_string(&table.rows[1].cells[1].runs[0].text), normalize_test_string("Only content"), "Cell content mismatch"); + }, + Err(_) => panic!("Failed to parse table with empty cells") + } + } + + #[test] + fn test_parse_graphic_frame_with_table() { + // Test for a graphic frame containing a table + let xml_data = load_xml("simple_table.xml"); + let doc = Document::parse(&*xml_data).expect("Failed to parse XML"); + let node = doc.root_element(); + + match parse_graphic_frame(&node) { + Ok(Some(SlideElement::Table(table))) => { + assert_eq!(table.rows.len(), 2, "Table should have 2 rows"); + assert_eq!(table.rows[0].cells.len(), 2, "First row should have 2 cells"); + + // Basic content check to confirm we got the right table + assert_eq!(normalize_test_string(&table.rows[0].cells[0].runs[0].text), normalize_test_string("Cell 1,1"), "Cell content mismatch"); + }, + Ok(None) => panic!("Should have found a table, but got None"), + Ok(_) => panic!("Found a different slide element, expected a table"), + Err(_) => panic!("Failed to parse graphic frame with table") + } + } + + #[test] + fn test_parse_graphic_frame_without_table() { + // Test for a graphic frame that doesn't contain a table + let xml_data = load_xml("non_table_graphic.xml"); + let doc = Document::parse(&*xml_data).expect("Failed to parse XML"); + let node = doc.root_element(); + + match parse_graphic_frame(&node) { + Ok(None) => { + // This is the expected result - no table found + }, + Ok(Some(_)) => panic!("Found a table where none should exist"), + Err(_) => panic!("Failed to parse non-table graphic frame") + } + } + + #[test] + fn test_parse_pic_with_image() { + // Test for parsing a picture with a valid image reference + let xml_data = load_xml("pic_with_image.xml"); + let doc = Document::parse(&*xml_data).expect("Failed to parse XML"); + let pic_node = doc.root_element(); + + match parse_pic(&pic_node) { + Ok(SlideElement::Image(image_ref)) => { + assert_eq!(image_ref.id, "rId2", "Image reference ID should be 'rId2'"); + assert_eq!(image_ref.target, "", "Image target should be empty initially"); + }, + Ok(_) => panic!("Expected an Image element but got something else"), + Err(e) => panic!("Failed to parse picture: {:?}", e) + } + } + + #[test] + fn test_parse_pic_without_embed() { + // Test for parsing a picture without an embed attribute + let xml_data = load_xml("pic_without_embed.xml"); + let doc = Document::parse(&*xml_data).expect("Failed to parse XML"); + let pic_node = doc.root_element(); + + match parse_pic(&pic_node) { + Ok(_) => panic!("Should have failed due to missing embed attribute"), + Err(Error::ImageNotFound) => { + // This is the expected behavior - should fail with ImageNotFound + }, + Err(e) => panic!("Expected ImageNotFound error but got: {:?}", e) + } + } + + #[test] + fn test_parse_pic_without_blip() { + // Test for parsing a picture without a blip node + let xml_data = load_xml("pic_without_blip.xml"); + let doc = Document::parse(&*xml_data).expect("Failed to parse XML"); + let pic_node = doc.root_element(); + + match parse_pic(&pic_node) { + Ok(_) => panic!("Should have failed due to missing blip node"), + Err(Error::ImageNotFound) => { + // This is the expected behavior - should fail with ImageNotFound + }, + Err(e) => panic!("Expected ImageNotFound error but got: {:?}", e) + } + } } \ No newline at end of file diff --git a/src/slide.rs b/src/slide.rs index a309e33..81bbff2 100644 --- a/src/slide.rs +++ b/src/slide.rs @@ -232,7 +232,7 @@ impl Slide { mod tests { use std::fs; use std::path::PathBuf; - // Note this useful idiom: importing names from outer (for mod tests) scope. + use super::*; fn mock_slide() -> Slide { diff --git a/tests/test_data/xml/complex_table.xml b/tests/test_data/xml/complex_table.xml new file mode 100644 index 0000000..494aa90 --- /dev/null +++ b/tests/test_data/xml/complex_table.xml @@ -0,0 +1,86 @@ + + + + + + + + + + Heading 1 + + + + + + + + + + Heading 2 + + + + + + + + + + Heading 3 + + + + + + + + + + + + Multiple + + + + + + paragraphs + + + + + + in one cell + + + + + + + + + + Cursive + + + + text + + + + + + + + + + Normal cell + + + + + + + + \ No newline at end of file diff --git a/tests/test_data/xml/empty_table.xml b/tests/test_data/xml/empty_table.xml new file mode 100644 index 0000000..cd2f1af --- /dev/null +++ b/tests/test_data/xml/empty_table.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Only content + + + + + + + + \ No newline at end of file diff --git a/tests/test_data/xml/multilevel_list.xml b/tests/test_data/xml/multilevel_list.xml new file mode 100644 index 0000000..612b839 --- /dev/null +++ b/tests/test_data/xml/multilevel_list.xml @@ -0,0 +1,50 @@ + + + + + + + + + + Main topic + + + + + + + + + Subtopic bullet + + + + + + + + + Another subtopic + + + + + + + + + Numbered sub-subtopic + + + + + + + + + Second main topic + + + \ No newline at end of file diff --git a/tests/test_data/xml/non_table_graphic.xml b/tests/test_data/xml/non_table_graphic.xml new file mode 100644 index 0000000..ebd1f0a --- /dev/null +++ b/tests/test_data/xml/non_table_graphic.xml @@ -0,0 +1,7 @@ + + + + a chart, not a table + + + \ No newline at end of file diff --git a/tests/test_data/xml/paragraph_empty.xml b/tests/test_data/xml/paragraph_empty.xml new file mode 100644 index 0000000..bc80904 --- /dev/null +++ b/tests/test_data/xml/paragraph_empty.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/tests/test_data/xml/paragraph_multiple.xml b/tests/test_data/xml/paragraph_multiple.xml new file mode 100644 index 0000000..a07fdb6 --- /dev/null +++ b/tests/test_data/xml/paragraph_multiple.xml @@ -0,0 +1,14 @@ + + + + First run + + + + Second run + + + + Third run + + \ No newline at end of file diff --git a/tests/test_data/xml/paragraph_single.xml b/tests/test_data/xml/paragraph_single.xml new file mode 100644 index 0000000..fbb9951 --- /dev/null +++ b/tests/test_data/xml/paragraph_single.xml @@ -0,0 +1,6 @@ + + + + Single run + + \ No newline at end of file diff --git a/tests/test_data/xml/pic_with_image.xml b/tests/test_data/xml/pic_with_image.xml new file mode 100644 index 0000000..2c95a11 --- /dev/null +++ b/tests/test_data/xml/pic_with_image.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/test_data/xml/pic_without_blip.xml b/tests/test_data/xml/pic_without_blip.xml new file mode 100644 index 0000000..859bdc4 --- /dev/null +++ b/tests/test_data/xml/pic_without_blip.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/test_data/xml/pic_without_embed.xml b/tests/test_data/xml/pic_without_embed.xml new file mode 100644 index 0000000..4cd5858 --- /dev/null +++ b/tests/test_data/xml/pic_without_embed.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/test_data/xml/run_empty.xml b/tests/test_data/xml/run_empty.xml new file mode 100644 index 0000000..e157c61 --- /dev/null +++ b/tests/test_data/xml/run_empty.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/tests/test_data/xml/run_no_format.xml b/tests/test_data/xml/run_no_format.xml new file mode 100644 index 0000000..ffcb4c2 --- /dev/null +++ b/tests/test_data/xml/run_no_format.xml @@ -0,0 +1,4 @@ + + + Unformatted text + \ No newline at end of file diff --git a/tests/test_data/xml/run_styles.xml b/tests/test_data/xml/run_styles.xml new file mode 100644 index 0000000..ef2943e --- /dev/null +++ b/tests/test_data/xml/run_styles.xml @@ -0,0 +1,4 @@ + + + Formatted text + \ No newline at end of file diff --git a/tests/test_data/xml/simple_list.xml b/tests/test_data/xml/simple_list.xml new file mode 100644 index 0000000..3deffb3 --- /dev/null +++ b/tests/test_data/xml/simple_list.xml @@ -0,0 +1,32 @@ + + + + + + + + + + First item + + + + + + + + + Second item + + + + + + + + + Third item + + + \ No newline at end of file diff --git a/tests/test_data/xml/simple_table.xml b/tests/test_data/xml/simple_table.xml new file mode 100644 index 0000000..ab58eec --- /dev/null +++ b/tests/test_data/xml/simple_table.xml @@ -0,0 +1,50 @@ + + + + + + + + + + Cell 1,1 + + + + + + + + + + Cell 1,2 + + + + + + + + + + + + Cell 2,1 + + + + + + + + + + Cell 2,2 + + + + + + + + \ No newline at end of file diff --git a/tests/test_data/xml/tx_body.xml b/tests/test_data/xml/tx_body.xml new file mode 100644 index 0000000..a29ded9 --- /dev/null +++ b/tests/test_data/xml/tx_body.xml @@ -0,0 +1,21 @@ + + + + + + + Hello + + + + World + + + + ! + + + \ No newline at end of file From f2d49137bd61d195f6d94f04e597c343603e6bfa Mon Sep 17 00:00:00 2001 From: krut_ni Date: Sat, 14 Jun 2025 02:59:59 +0200 Subject: [PATCH 4/4] added tests to relationship parsing logic --- src/parse_rels.rs | 53 ++++++++++++++++++++- src/parse_xml.rs | 5 +- tests/test_data/xml/rels_with_images.xml | 5 ++ tests/test_data/xml/rels_without_images.xml | 4 ++ 4 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 tests/test_data/xml/rels_with_images.xml create mode 100644 tests/test_data/xml/rels_without_images.xml diff --git a/src/parse_rels.rs b/src/parse_rels.rs index d4453ae..4c4a5c7 100644 --- a/src/parse_rels.rs +++ b/src/parse_rels.rs @@ -46,4 +46,55 @@ pub fn parse_slide_rels(xml_data: &[u8]) -> Result> { } Ok(images) -} \ No newline at end of file +} + +#[cfg(test)] +mod tests { + use std::fs; + use std::path::PathBuf; + use super::*; + + fn load_xml(filename: &str) -> Vec { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("tests"); + path.push("test_data"); + path.push("xml"); + path.push(filename); + fs::read(path).expect("Unable to read test data file") + } + + fn normalize_test_string(input: &str) -> String { + input + .trim_start_matches('\u{feff}') // remove BOM + .replace("\r\n", "\n") // normalize line breaks + .replace(" ", "\t") // replace 4 whitespaces with a tab + .trim() // trim leading and trailing whitespace + .to_string() + } + + #[test] + fn test_parse_slide_rels_with_images() { + let xml_data = load_xml("rels_with_images.xml"); + match parse_slide_rels(&xml_data) { + Ok(images) => { + assert_eq!(images.len(), 2); + assert_eq!(images[0].id, "rId1"); + assert_eq!(normalize_test_string(&images[0].target), normalize_test_string("../media/image1.png")); + assert_eq!(images[1].id, "rId2"); + assert_eq!(normalize_test_string(&images[1].target), normalize_test_string("../media/image2.jpg")); + }, + Err(_) => panic!("Fehler beim Parsen der Slide-Relationships mit Bildern") + } + } + + #[test] + fn test_parse_slide_rels_empty() { + let xml_data = load_xml("rels_without_images.xml"); + match parse_slide_rels(&xml_data) { + Ok(images) => { + assert_eq!(images.len(), 0); + }, + Err(_) => panic!("Fehler beim Parsen der leeren Slide-Relationships") + } + } +} diff --git a/src/parse_xml.rs b/src/parse_xml.rs index 6388fcb..52440ba 100644 --- a/src/parse_xml.rs +++ b/src/parse_xml.rs @@ -77,7 +77,7 @@ pub fn parse_slide_xml(xml_data: &[u8]) -> Result> { } /// Parses the text body node (``) ito search for shape nodes (``) and -/// evaluates if a shape is formatted list or a common text +/// evaluates if a shape is a formatted list or a common text fn parse_sp(sp_node: &Node) -> Result { let tx_body_node = sp_node.children().find(|n| { n.is_element() @@ -436,8 +436,7 @@ mod tests { Err(_) => panic!("Fehler beim Parsen des Runs ohne Formatierung") } } - - + #[test] fn test_parse_run_empty_text() { let xml_data = load_xml("run_empty.xml"); diff --git a/tests/test_data/xml/rels_with_images.xml b/tests/test_data/xml/rels_with_images.xml new file mode 100644 index 0000000..5c9388b --- /dev/null +++ b/tests/test_data/xml/rels_with_images.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/tests/test_data/xml/rels_without_images.xml b/tests/test_data/xml/rels_without_images.xml new file mode 100644 index 0000000..5cb30cc --- /dev/null +++ b/tests/test_data/xml/rels_without_images.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file