diff --git a/docs/rust-testing-with-rstest-fixtures.md b/docs/rust-testing-with-rstest-fixtures.md index 31251f2a..8c517b15 100644 --- a/docs/rust-testing-with-rstest-fixtures.md +++ b/docs/rust-testing-with-rstest-fixtures.md @@ -1345,19 +1345,19 @@ provided by `rstest`: | ---------------------------- | -------------------------------------------------------------------------------------------- | | #[rstest] | Marks a function as an rstest test; enables fixture injection and parameterization. | | #[fixture] | Defines a function that provides a test fixture (setup data or services). | -| #[case(...)] | Defines a single parameterized test case with specific input values. | -| #[values(...)] | Defines a list of values for an argument, generating tests for each value or combination. | +| #[case(…)] | Defines a single parameterized test case with specific input values. | +| #[values(…)] | Defines a list of values for an argument, generating tests for each value or combination. | | #[once] | Marks a fixture to be initialized only once and shared (as a static reference) across tests. | | #[future] | Simplifies async argument types by removing impl Future boilerplate. | | #[awt] | (Function or argument level) Automatically .awaits future arguments in async tests. | | #[from(original_name)] | Allows renaming an injected fixture argument in the test function. | -| #[with(...)] | Overrides default arguments of a fixture for a specific test. | -| #[default(...)] | Provides default values for arguments within a fixture function. | -| #[timeout(...)] | Sets a timeout for an asynchronous test. | -| #[files("glob_pattern",...)] | Injects file paths (or contents, with mode=) matching a glob pattern as test arguments. | +| #[with(…)] | Overrides default arguments of a fixture for a specific test. | +| #[default(…)] | Provides default values for arguments within a fixture function. | +| #[timeout(…)] | Sets a timeout for an asynchronous test. | +| #[files("glob_pattern",…)] | Injects file paths (or contents, with mode=) matching a glob pattern as test arguments. | -By mastering `rstest`, Rust developers can significantly elevate the quality -and efficiency of their testing practices, leading to more reliable and +Mastering `rstest` can significantly elevate the quality and efficiency of +testing practices for Rust developers, leading to more reliable and maintainable software. #### Works cited diff --git a/src/main.rs b/src/main.rs index bd752366..b7179f27 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,7 +21,10 @@ struct Cli { } #[derive(clap::Args, Clone, Copy)] -#[allow(clippy::struct_excessive_bools)] // CLI exposes four independent flags +#[expect( + clippy::struct_excessive_bools, + reason = "CLI exposes four independent flags" +)] struct FormatOpts { /// Wrap paragraphs and list items to 80 columns #[arg(long = "wrap")] diff --git a/tests/breaks.rs b/tests/breaks.rs new file mode 100644 index 00000000..bcfe9742 --- /dev/null +++ b/tests/breaks.rs @@ -0,0 +1,76 @@ +//! Integration tests for formatting thematic breaks. +//! +//! Verifies `format_breaks` function and `--breaks` CLI option. + +use std::borrow::Cow; + +use mdtablefix::{THEMATIC_BREAK_LEN, format_breaks}; + +#[macro_use] +mod prelude; +use prelude::*; + +#[test] +fn test_format_breaks_basic() { + let input = lines_vec!["foo", "***", "bar"]; + let expected: Vec> = vec![ + input[0].as_str().into(), + Cow::Owned("_".repeat(THEMATIC_BREAK_LEN)), + input[2].as_str().into(), + ]; + assert_eq!(format_breaks(&input), expected); +} + +#[test] +fn test_format_breaks_ignores_code() { + let input = lines_vec!["```", "---", "```"]; + let expected: Vec> = input.iter().map(|s| s.as_str().into()).collect(); + assert_eq!(format_breaks(&input), expected); +} + +#[test] +fn test_format_breaks_mixed_chars() { + let input = lines_vec!["-*-*-"]; + let expected: Vec> = input.iter().map(|s| s.as_str().into()).collect(); + assert_eq!(format_breaks(&input), expected); +} + +#[test] +fn test_format_breaks_with_spaces_and_indent() { + let input = lines_vec![" - - - "]; + let expected: Vec> = vec![Cow::Owned("_".repeat(THEMATIC_BREAK_LEN))]; + assert_eq!(format_breaks(&input), expected); +} + +#[test] +fn test_format_breaks_with_tabs_and_underscores() { + let input = lines_vec!["\t_\t_\t_\t"]; + let expected: Vec> = vec![Cow::Owned("_".repeat(THEMATIC_BREAK_LEN))]; + assert_eq!(format_breaks(&input), expected); +} + +#[test] +fn test_format_breaks_mixed_chars_excessive_length() { + let input = lines_vec!["***---___"]; + let expected: Vec> = input.iter().map(|s| s.as_str().into()).collect(); + assert_eq!(format_breaks(&input), expected); +} + +/// Tests the CLI `--breaks` option to ensure thematic breaks are normalised. +/// +/// Provides a single line of hyphens and asserts the output is the standard +/// underscore-based thematic break. +#[test] +fn test_cli_breaks_option() { + let output = Command::cargo_bin("mdtablefix") + .expect("Failed to create cargo command for mdtablefix") + .arg("--breaks") + .write_stdin("---\n") + .output() + .expect("Failed to execute mdtablefix command"); + assert!(output.status.success()); + assert_eq!( + String::from_utf8_lossy(&output.stdout), + format!("{}\n", "_".repeat(THEMATIC_BREAK_LEN)) + ); +} diff --git a/tests/cli.rs b/tests/cli.rs new file mode 100644 index 00000000..1aafda4f --- /dev/null +++ b/tests/cli.rs @@ -0,0 +1,135 @@ +//! Integration tests for CLI interface behaviour of the `mdtablefix` tool. +//! +//! This module validates the command-line interface functionality, including: +//! - File handling with the `--in-place` flag +//! - Ellipsis replacement with the `--ellipsis` option +//! - Error handling for invalid argument combinations +//! - Processing of Markdown files through the CLI interface + +use std::{fs::File, io::Write}; + +use tempfile::tempdir; + +#[macro_use] +mod prelude; +use prelude::*; + +/// Verifies that the CLI fails when the `--in-place` flag is used without specifying a file. +/// +/// This test ensures that running `mdtablefix --in-place` without a file argument results in a +/// command failure. +#[test] +fn test_cli_in_place_requires_file() { + Command::cargo_bin("mdtablefix") + .expect("Failed to create cargo command for mdtablefix") + .arg("--in-place") + .assert() + .failure(); +} + +/// Tests that the CLI processes a file containing a broken Markdown table and outputs the corrected +/// table to stdout. +/// +/// This test creates a temporary file with a malformed table, runs the `mdtablefix` binary on it, +/// and asserts that the output is the expected fixed table. +#[rstest] +fn test_cli_process_file(broken_table: Vec) { + let dir = tempdir().expect("failed to create temporary directory"); + let file_path = dir.path().join("sample.md"); + let mut f = File::create(&file_path).expect("failed to create temporary file"); + for line in &broken_table { + writeln!(f, "{line}").expect("failed to write line"); + } + f.flush().expect("failed to flush file"); + drop(f); + Command::cargo_bin("mdtablefix") + .expect("Failed to create cargo command for mdtablefix") + .arg(&file_path) + .assert() + .success() + .stdout("| A | B |\n| 1 | 2 |\n| 3 | 4 |\n"); +} + +/// Tests that the `--ellipsis` option replaces triple dots with a Unicode ellipsis character. +/// +/// Verifies that the CLI correctly processes input containing "..." and outputs "…". +#[test] +fn test_cli_ellipsis_option() { + let output = Command::cargo_bin("mdtablefix") + .expect("Failed to create cargo command for mdtablefix") + .arg("--ellipsis") + .write_stdin("foo...\n") + .output() + .expect("Failed to execute mdtablefix command"); + assert!(output.status.success()); + assert_eq!(String::from_utf8_lossy(&output.stdout), "foo…\n"); +} + +/// Tests that the `--ellipsis` option preserves dots within inline code spans. +/// +/// Verifies that triple dots inside backtick-delimited code spans are not converted to ellipsis. +#[test] +fn test_cli_ellipsis_code_span() { + let output = Command::cargo_bin("mdtablefix") + .expect("Failed to create cargo command for mdtablefix") + .arg("--ellipsis") + .write_stdin("before `dots...` after\n") + .output() + .expect("Failed to execute mdtablefix command"); + assert!(output.status.success()); + assert_eq!( + String::from_utf8_lossy(&output.stdout), + "before `dots...` after\n" + ); +} + +/// Tests that the `--ellipsis` option does not alter fenced code blocks. +/// +/// Ensures that sequences like "..." inside a fenced code block remain unchanged. +#[test] +fn test_cli_ellipsis_fenced_block() { + let output = Command::cargo_bin("mdtablefix") + .expect("Failed to create cargo command for mdtablefix") + .arg("--ellipsis") + .write_stdin("```\nlet x = ...;\n```\n") + .output() + .expect("Failed to execute mdtablefix command"); + assert!(output.status.success()); + assert_eq!( + String::from_utf8_lossy(&output.stdout), + "```\nlet x = ...;\n```\n" + ); +} + +/// Tests ellipsis replacement for sequences longer than three characters. +/// +/// Confirms that only the first three dots are replaced with an ellipsis. +#[test] +fn test_cli_ellipsis_long_sequence() { + let output = Command::cargo_bin("mdtablefix") + .expect("Failed to create cargo command for mdtablefix") + .arg("--ellipsis") + .write_stdin("wait....\n") + .output() + .expect("Failed to execute mdtablefix command"); + assert!(output.status.success()); + assert_eq!(String::from_utf8_lossy(&output.stdout), "wait….\n"); +} + +/// Tests that the `--ellipsis` option handles multiple ellipsis sequences in one line. +/// +/// Verifies that all occurrences of "..." are replaced with "…". +#[test] +fn test_cli_ellipsis_multiple_sequences() { + let output = Command::cargo_bin("mdtablefix") + .expect("Failed to create cargo command for mdtablefix") + .arg("--ellipsis") + .write_stdin("First... then second... done.\n") + .output() + .expect("Failed to execute mdtablefix command"); + assert!(output.status.success()); + assert_eq!( + String::from_utf8_lossy(&output.stdout), + "First… then second… done.\n" + ); +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs index f893484b..f86905be 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1,9 +1,13 @@ //! Utility helpers shared across integration tests. +#![allow(unfulfilled_lint_expectations)] + +use rstest::fixture; /// Build a `Vec` from a list of string slices. /// /// This macro is primarily used in tests to reduce boilerplate when /// constructing example tables or other collections of lines. +#[expect(unused_macros, reason = "macros are optional helpers across modules")] macro_rules! lines_vec { ($($line:expr),* $(,)?) => { vec![$($line.to_string()),*] @@ -16,6 +20,7 @@ macro_rules! lines_vec { /// ``` /// let input: Vec = include_lines!("data/bold_header_input.txt"); /// ``` +#[expect(unused_macros, reason = "macros are optional helpers across modules")] macro_rules! include_lines { ($path:literal $(,)?) => {{ const _TXT: &str = include_str!($path); @@ -27,6 +32,12 @@ macro_rules! include_lines { /// /// Verifies the number of lines, prefix on the first line, length of all lines, /// and indentation of continuation lines. +/// +/// # Panics +/// +/// Panics if the output slice is empty, expected count is zero, or if the lines +/// do not meet the asserted conditions. +#[expect(dead_code, reason = "helper used selectively across modules")] pub fn assert_wrapped_list_item(output: &[String], prefix: &str, expected: usize) { assert!(expected > 0, "expected line count must be positive"); assert!(!output.is_empty(), "output slice is empty"); @@ -67,9 +78,24 @@ pub fn assert_wrapped_list_item(output: &[String], prefix: &str, expected: usize /// Assert that every line in a blockquote starts with the given prefix and is at most 80 /// characters. +/// +/// # Panics +/// +/// Panics if the output slice is empty or the prefix is missing from any line. +#[expect(dead_code, reason = "helper used selectively across modules")] pub fn assert_wrapped_blockquote(output: &[String], prefix: &str, expected: usize) { assert!(!output.is_empty(), "output slice is empty"); assert_eq!(output.len(), expected); assert!(output.iter().all(|l| l.starts_with(prefix))); assert!(output.iter().all(|l| l.len() <= 80)); } + +/// Fixture representing a simple broken table. +#[expect(dead_code, reason = "helper used selectively across modules")] +#[fixture] +pub fn broken_table() -> Vec { + vec![ + "| A | B | |".to_string(), + "| 1 | 2 | | 3 | 4 |".to_string(), + ] +} diff --git a/tests/integration.rs b/tests/integration.rs deleted file mode 100644 index be6f40b5..00000000 --- a/tests/integration.rs +++ /dev/null @@ -1,1055 +0,0 @@ -use std::{borrow::Cow, fs::File, io::Write}; - -use assert_cmd::Command; -use mdtablefix::{ - THEMATIC_BREAK_LEN, - convert_html_tables, - format_breaks, - process_stream, - reflow_table, - renumber_lists, -}; -use rstest::{fixture, rstest}; -use tempfile::tempdir; - -#[macro_use] -mod common; - -#[fixture] -/// Provides a sample Markdown table with broken rows for testing purposes. -/// -/// The returned vector contains lines representing a table with inconsistent columns, useful for -/// validating table reflow logic. -fn broken_table() -> Vec { return lines_vec!["| A | B | |", "| 1 | 2 | | 3 | 4 |"]; } - -#[fixture] -/// Returns a vector of strings representing a malformed Markdown table with inconsistent columns. -/// -/// The returned table has rows with differing numbers of columns, making it invalid for standard -/// Markdown table parsing. -fn malformed_table() -> Vec { return lines_vec!["| A | |", "| 1 | 2 | 3 |"]; } - -#[fixture] -fn header_table() -> Vec { - lines_vec!["| A | B | |", "| --- | --- |", "| 1 | 2 | | 3 | 4 |"] -} - -#[fixture] -fn escaped_pipe_table() -> Vec { - lines_vec!["| X | Y | |", "| a \\| b | 1 | | 2 | 3 |"] -} - -#[fixture] -fn indented_table() -> Vec { - return lines_vec![" | I | J | |", " | 1 | 2 | | 3 | 4 |"]; -} - -#[fixture] -fn html_table() -> Vec { - lines_vec![ - "", - "", - "", - "
AB
12
", - ] -} - -#[fixture] -fn html_table_with_attrs() -> Vec { - lines_vec![ - "", - "", - "", - "
AB
12
", - ] -} - -#[fixture] -fn html_table_with_colspan() -> Vec { - lines_vec![ - "", - "", - "", - "
A
12
", - ] -} - -#[fixture] -fn html_table_no_header() -> Vec { - lines_vec![ - "", - "", - "", - "
AB
12
", - ] -} - -#[fixture] -fn html_table_empty_row() -> Vec { - lines_vec![ - "", - "", - "", - "
12
", - ] -} - -#[fixture] -fn html_table_whitespace_header() -> Vec { - lines_vec![ - "", - "", - "", - "
12
", - ] -} - -#[fixture] -fn html_table_inconsistent_first_row() -> Vec { - lines_vec![ - "", - "", - "", - "
A
12
", - ] -} - -#[fixture] -fn html_table_empty() -> Vec { return lines_vec!["
"]; } - -#[fixture] -fn html_table_unclosed() -> Vec { return lines_vec!["", ""]; } - -#[fixture] -fn html_table_uppercase() -> Vec { - lines_vec![ - "
1
", - "", - "", - "
AB
12
", - ] -} - -#[fixture] -fn html_table_mixed_case() -> Vec { - lines_vec![ - "", - "", - "", - "
AB
12
", - ] -} - -#[fixture] -fn multiple_tables() -> Vec { - lines_vec!["| A | B |", "| 1 | 22 |", "", "| X | Y |", "| 3 | 4 |"] -} - -#[rstest] -/// Tests that `reflow_table` correctly restructures a broken Markdown table into a well-formed -/// table. -fn test_reflow_basic(broken_table: Vec) { - let expected = lines_vec!["| A | B |", "| 1 | 2 |", "| 3 | 4 |"]; - assert_eq!(reflow_table(&broken_table), expected); -} - -#[rstest] -/// Tests that `reflow_table` returns the original input unchanged when given a malformed Markdown -/// table. -/// -/// This ensures that the function does not attempt to modify tables with inconsistent columns or -/// structure. -fn test_reflow_malformed_returns_original(malformed_table: Vec) { - assert_eq!(reflow_table(&malformed_table), malformed_table); -} - -#[rstest] -fn test_reflow_preserves_header(header_table: Vec) { - let expected = lines_vec!["| A | B |", "| --- | --- |", "| 1 | 2 |", "| 3 | 4 |"]; - assert_eq!(reflow_table(&header_table), expected); -} - -#[rstest] -fn test_reflow_handles_escaped_pipes(escaped_pipe_table: Vec) { - // The fixture contains a header row followed by a row with an escaped - // pipe sequence (`a \| b`). After reflow the escaped pipe becomes a literal - // `|` inside the first data cell, so the table has three columns and the - // header row is padded to match. - let expected = lines_vec!["| X | Y |", "| a | b | 1 |", "| 2 | 3 |"]; - assert_eq!(reflow_table(&escaped_pipe_table), expected); -} - -#[rstest] -fn test_reflow_preserves_indentation(indented_table: Vec) { - let expected = lines_vec![" | I | J |", " | 1 | 2 |", " | 3 | 4 |"]; - assert_eq!(reflow_table(&indented_table), expected); -} - -#[rstest] -fn test_process_stream_html_table(html_table: Vec) { - let expected = lines_vec!["| A | B |", "| --- | --- |", "| 1 | 2 |"]; - assert_eq!(process_stream(&html_table), expected); -} - -#[rstest] -fn test_process_stream_html_table_with_attrs(html_table_with_attrs: Vec) { - let expected = lines_vec!["| A | B |", "| --- | --- |", "| 1 | 2 |"]; - assert_eq!(process_stream(&html_table_with_attrs), expected); -} - -#[rstest] -fn test_process_stream_html_table_uppercase(html_table_uppercase: Vec) { - let expected = lines_vec!["| A | B |", "| --- | --- |", "| 1 | 2 |"]; - assert_eq!(process_stream(&html_table_uppercase), expected); -} - -#[rstest] -fn test_process_stream_html_table_mixed_case(html_table_mixed_case: Vec) { - let expected = lines_vec!["| A | B |", "| --- | --- |", "| 1 | 2 |"]; - assert_eq!(process_stream(&html_table_mixed_case), expected); -} - -#[rstest] -fn test_process_stream_multiple_tables(multiple_tables: Vec) { - let expected = lines_vec!["| A | B |", "| 1 | 22 |", "", "| X | Y |", "| 3 | 4 |",]; - assert_eq!(process_stream(&multiple_tables), expected); -} - -/// Tests that `process_stream` leaves lines inside code fences unchanged. -/// -/// Verifies that both backtick (```) and tilde (~~~) fenced code blocks are ignored by the table -/// processing logic, ensuring their contents are not altered. -#[rstest] -fn test_process_stream_ignores_code_fences() { - let lines = lines_vec!["```rust", "| not | a | table |", "```"]; - assert_eq!(process_stream(&lines), lines); - - // Test with tilde-based code fences - let tilde_lines = lines_vec!["~~~", "| not | a | table |", "~~~"]; - assert_eq!(process_stream(&tilde_lines), tilde_lines); -} - -#[rstest] -fn test_process_stream_ignores_indented_fences() { - let lines = lines_vec!( - " ```javascript", - " socket.onmessage = function(event) {", - " const message = JSON.parse(event.data);", - " switch(message.type) {", - " case \"serverNewMessage\":", - " // Display message.payload.user and message.payload.text", - " break;", - " case \"serverUserJoined\":", - " // Update user list with message.payload.user", - " break;", - " // Handle other message types...", - " }", - " };", - "", - " ```", - ); - assert_eq!(process_stream(&lines), lines); -} - -#[rstest] -/// Verifies that the CLI fails when the `--in-place` flag is used without specifying a file. -/// -/// This test ensures that running `mdtablefix --in-place` without a file argument results in a -/// command failure. -fn test_cli_in_place_requires_file() { - Command::cargo_bin("mdtablefix") - .unwrap() - .arg("--in-place") - .assert() - .failure(); -} - -#[rstest] -/// Tests that the CLI processes a file containing a broken Markdown table and outputs the corrected -/// table to stdout. -/// -/// This test creates a temporary file with a malformed table, runs the `mdtablefix` binary on it, -/// and asserts that the output is the expected fixed table. -fn test_cli_process_file(broken_table: Vec) { - let dir = tempdir().unwrap(); - let file_path = dir.path().join("sample.md"); - let mut f = File::create(&file_path).unwrap(); - for line in &broken_table { - writeln!(f, "{line}").unwrap(); - } - f.flush().unwrap(); - drop(f); - Command::cargo_bin("mdtablefix") - .unwrap() - .arg(&file_path) - .assert() - .success() - .stdout("| A | B |\n| 1 | 2 |\n| 3 | 4 |\n"); -} - -#[test] -fn test_cli_wrap_option() { - let input = "This line is deliberately made much longer than eighty columns so that the \ - wrapping algorithm is forced to insert a soft line-break somewhere in the middle \ - of the paragraph when the --wrap flag is supplied."; - let output = Command::cargo_bin("mdtablefix") - .unwrap() - .arg("--wrap") - .write_stdin(format!("{input}\n")) - .output() - .unwrap(); - assert!(output.status.success()); - let text = String::from_utf8_lossy(&output.stdout); - assert!( - text.lines().count() > 1, - "expected wrapped output on multiple lines" - ); - assert!(text.lines().all(|l| l.len() <= 80)); -} - -#[test] -fn test_uniform_example_one() { - let input = lines_vec![ - "| Logical type | PostgreSQL | SQLite notes |", - "|--------------|-------------------------|---------------------------------------------------------------------------------|", - "| strings | `TEXT` (or `VARCHAR`) | `TEXT` - SQLite ignores the length specifier anyway |", - "| booleans | `BOOLEAN DEFAULT FALSE` | declare as `BOOLEAN`; Diesel serialises to 0 / 1 so this is fine |", - "| integers | `INTEGER` / `BIGINT` | ditto |", - "| decimals | `NUMERIC` | stored as FLOAT in SQLite; Diesel `Numeric` round-trips, but beware precision |", - "| blobs / raw | `BYTEA` | `BLOB` |", - ]; - let output = reflow_table(&input); - assert!(!output.is_empty()); - let widths: Vec = output[0] - .trim_matches('|') - .split('|') - .map(str::len) - .collect(); - for row in output { - let cols: Vec<&str> = row.trim_matches('|').split('|').collect(); - for (i, col) in cols.iter().enumerate() { - assert_eq!(col.len(), widths[i]); - } - } -} - -#[test] -fn test_uniform_example_two() { - let input = lines_vec![ - "| Option | How it works | When to choose it |", - "|--------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------|", - "| **B. Pure-Rust migrations** | Implement `diesel::migration::Migration` in a Rust file (`up.rs` / `down.rs`) and compile with both `features = [\"postgres\", \"sqlite\"]`. The query builder emits backend-specific SQL at runtime. | You prefer the type-checked DSL and can live with slightly slower compile times. |", - "| **C. Lowest-common-denominator SQL** | Write one `up.sql`/`down.sql` that *already* works on both engines. This demands avoiding SERIAL/IDENTITY, JSONB, `TIMESTAMPTZ`, etc. | Simple schemas, embedded use-case only, you are happy to supply integer primary keys manually. |", - "| **D. Two separate migration trees** | Maintain `migrations/sqlite` and `migrations/postgres` directories with identical version numbers. Use `embed_migrations!(\"migrations/\")` to compile the right set. | You ship a single binary with migrations baked in. |", - ]; - let output = reflow_table(&input); - assert!(!output.is_empty()); - let widths: Vec = output[0] - .trim_matches('|') - .split('|') - .map(str::len) - .collect(); - for row in output { - let cols: Vec<&str> = row.trim_matches('|').split('|').collect(); - for (i, col) in cols.iter().enumerate() { - assert_eq!(col.len(), widths[i]); - } - } -} - -#[test] -fn test_non_table_lines_unchanged() { - let input = lines_vec![ - "# Title", - String::new(), - "Para text.", - String::new(), - "| a | b |", - "| 1 | 22 |", - String::new(), - "* bullet", - String::new(), - ]; - let output = process_stream(&input); - let expected = lines_vec![ - "# Title", - String::new(), - "Para text.", - String::new(), - "| a | b |", - "| 1 | 22 |", - String::new(), - "* bullet", - String::new(), - ]; - assert_eq!(output, expected); -} - -#[test] -fn test_convert_html_table_basic() { - let html_table = lines_vec![ - "", - "", - "", - "
AB
12
", - ]; - let expected = lines_vec!["| A | B |", "| --- | --- |", "| 1 | 2 |"]; - assert_eq!(convert_html_tables(&html_table), expected); -} - -#[rstest] -#[case("```")] -#[case("~~~")] -#[case("```rust")] -fn test_convert_html_table_in_text_and_code(#[case] fence: &str) { - let lines = lines_vec![ - "Intro", - "", - "", - "", - "
AB
12
", - fence, - "
x
", - fence, - "Outro", - ]; - let expected = lines_vec![ - "Intro", - "| A | B |", - "| --- | --- |", - "| 1 | 2 |", - fence, - "
x
", - fence, - "Outro", - ]; - assert_eq!(convert_html_tables(&lines), expected); -} - -#[test] -fn test_convert_html_table_with_attrs_basic() { - let expected = lines_vec!["| A | B |", "| --- | --- |", "| 1 | 2 |"]; - assert_eq!(convert_html_tables(&html_table_with_attrs()), expected); -} - -#[test] -fn test_convert_html_table_uppercase() { - let expected = lines_vec!["| A | B |", "| --- | --- |", "| 1 | 2 |"]; - assert_eq!(convert_html_tables(&html_table_uppercase()), expected); -} - -#[test] -fn test_convert_html_table_with_colspan() { - let expected = lines_vec!["| A |", "| --- |", "| 1 | 2 |"]; - assert_eq!(convert_html_tables(&html_table_with_colspan()), expected); -} - -#[test] -fn test_convert_html_table_no_header() { - let expected = lines_vec!["| A | B |", "| --- | --- |", "| 1 | 2 |"]; - assert_eq!(convert_html_tables(&html_table_no_header()), expected); -} - -#[test] -fn test_convert_html_table_empty_row() { - let expected = lines_vec!["| 1 | 2 |", "| --- | --- |"]; - assert_eq!(convert_html_tables(&html_table_empty_row()), expected); -} - -#[test] -fn test_convert_html_table_whitespace_header() { - let expected = lines_vec!["| --- | --- |", "| --- | --- |", "| 1 | 2 |"]; - assert_eq!( - convert_html_tables(&html_table_whitespace_header()), - expected - ); -} - -#[test] -fn test_convert_html_table_inconsistent_first_row() { - let expected = lines_vec!["| A |", "| --- |", "| 1 | 2 |"]; - assert_eq!( - convert_html_tables(&html_table_inconsistent_first_row()), - expected - ); -} - -#[test] -fn test_convert_html_table_empty() { - assert!(convert_html_tables(&html_table_empty()).is_empty()); -} - -#[test] -fn test_convert_html_table_unclosed_returns_original() { - let html = html_table_unclosed(); - assert_eq!(convert_html_tables(&html), html); -} - -#[test] -fn test_convert_html_table_bold_header() { - let input: Vec = include_lines!("data/bold_header_input.txt"); - let expected: Vec = include_lines!("data/bold_header_expected.txt"); - assert_eq!(convert_html_tables(&input), expected); -} - -#[test] -fn test_logical_type_table_output_matches() { - let input: Vec = include_lines!("data/logical_type_input.txt"); - let expected: Vec = include_lines!("data/logical_type_expected.txt"); - assert_eq!(reflow_table(&input), expected); -} - -#[test] -/// Verifies that reflowing the option table input produces the expected output. -/// -/// Loads the input and expected output from external files and asserts that the -/// `reflow_table` function transforms the input table to match the expected result. -fn test_option_table_output_matches() { - let input: Vec = include_lines!("data/option_table_input.txt"); - let expected: Vec = include_lines!("data/option_table_expected.txt"); - assert_eq!(reflow_table(&input), expected); -} - -#[test] -fn test_month_seconds_table_output_matches() { - let input: Vec = include_lines!("data/month_seconds_input.txt"); - let expected: Vec = include_lines!("data/month_seconds_expected.txt"); - assert_eq!(reflow_table(&input), expected); -} - -#[test] -fn test_offset_table_output_matches() { - let input: Vec = include_lines!("data/offset_table_input.txt"); - let expected: Vec = include_lines!("data/offset_table_expected.txt"); - assert_eq!(reflow_table(&input), expected); -} - -#[test] -/// Tests that `process_stream` correctly processes a complex Markdown table representing logical -/// types by comparing its output to expected results loaded from a file. -fn test_process_stream_logical_type_table() { - let input: Vec = include_lines!("data/logical_type_input.txt"); - let expected: Vec = include_lines!("data/logical_type_expected.txt"); - assert_eq!(process_stream(&input), expected); -} - -#[test] -/// Tests that `process_stream` correctly processes a Markdown table with options, producing the -/// expected output. -/// -/// Loads input and expected output from test data files, runs `process_stream` on the input, and -/// asserts equality. -fn test_process_stream_option_table() { - let input: Vec = include_lines!("data/option_table_input.txt"); - let expected: Vec = include_lines!("data/option_table_expected.txt"); - assert_eq!(process_stream(&input), expected); -} - -#[test] -/// Tests that long paragraphs are wrapped at 80 columns by `process_stream`. -/// -/// Ensures that a single long paragraph is split into multiple lines, each not exceeding 80 -/// characters. -fn test_wrap_paragraph() { - let input = lines_vec![ - "This is a very long paragraph that should be wrapped at eighty columns so it needs to \ - contain enough words to exceed that limit.", - ]; - let output = process_stream(&input); - assert!(output.len() > 1); - assert!(output.iter().all(|l| l.len() <= 80)); -} - -#[test] -fn test_wrap_list_item() { - let input = lines_vec![ - r"- This bullet item is exceptionally long and must be wrapped to keep prefix formatting intact.", - ]; - let output = process_stream(&input); - common::assert_wrapped_list_item(&output, "- ", 2); -} - -#[rstest] -#[case("- ", 3)] -#[case("1. ", 3)] -#[case("10. ", 3)] -#[case("100. ", 3)] -fn test_wrap_list_items_with_inline_code(#[case] prefix: &str, #[case] expected: usize) { - let input = lines_vec![format!( - "{prefix}`script`: A multi-line script declared with the YAML `|` block style. The entire \ - block is passed to an interpreter. If the first line begins with `#!`, Netsuke executes \ - the script verbatim, respecting the shebang." - )]; - let output = process_stream(&input); - common::assert_wrapped_list_item(&output, prefix, expected); -} - -#[test] -fn test_wrap_preserves_inline_code_spans() { - let input = lines_vec![ - "- `script`: A multi-line script declared with the YAML `|` block style. The entire block \ - is passed to an interpreter. If the first line begins with `#!`, Netsuke executes the \ - script verbatim, respecting the shebang.", - ]; - let output = process_stream(&input); - common::assert_wrapped_list_item(&output, "- ", 3); -} - -#[test] -fn test_wrap_multi_backtick_code() { - let input = lines_vec![ - "- ``cmd`` executes ```echo``` output with ``json`` format and prints results to the \ - console", - ]; - let output = process_stream(&input); - common::assert_wrapped_list_item(&output, "- ", 2); -} - -#[test] -fn test_wrap_multiple_inline_code_spans() { - let input = lines_vec![ - "- Use `foo` and `bar` inside ``baz`` for testing with additional commentary to exceed \ - wrapping width", - ]; - let output = process_stream(&input); - common::assert_wrapped_list_item(&output, "- ", 2); -} -#[test] -fn test_wrap_long_inline_code_item() { - let input = lines_vec![concat!( - "- `async def on_unhandled(self, ws: WebSocketLike, message: Union[str, bytes])`:", - " A fallback handler for messages that are not dispatched by the more specific", - " message handlers. This can be used for raw text/binary data or messages that", - " don't conform to the expected structured format." - )]; - let output = process_stream(&input); - common::assert_wrapped_list_item(&output, "- ", 4); - assert!( - output - .first() - .expect("output should not be empty") - .ends_with("`:") - ); -} - -#[test] -fn test_wrap_future_attribute_punctuation() { - let input = vec![ - concat!( - "- Test function (`#[awt]`) or a specific `#[future]` argument ", - "(`#[future(awt)]`), tells `rstest` to automatically insert `.await` ", - "calls for those futures." - ) - .to_string(), - ]; - let output = process_stream(&input); - assert_eq!( - output, - vec![ - "- Test function (`#[awt]`) or a specific `#[future]` argument".to_string(), - " (`#[future(awt)]`), tells `rstest` to automatically insert `.await` calls for" - .to_string(), - " those futures.".to_string(), - ] - ); -} - -#[test] -fn test_wrap_footnote_multiline() { - let input = lines_vec![concat!( - "[^note]: This footnote is sufficiently long to require wrapping ", - "across multiple lines so we can verify indentation." - )]; - let output = process_stream(&input); - common::assert_wrapped_list_item(&output, "[^note]: ", 2); -} - -#[test] -fn test_wrap_footnote_with_inline_code() { - let input = lines_vec![concat!( - " [^code_note]: A footnote containing inline `code` that should wrap ", - "across multiple lines without breaking the span." - )]; - let output = process_stream(&input); - common::assert_wrapped_list_item(&output, " [^code_note]: ", 2); -} - -/// Tests that footnotes with angle-bracketed URLs are wrapped correctly. -/// -/// Verifies that when a footnote line contains a URL enclosed in angle brackets, -/// the URL is moved to a new indented line beneath the footnote text. -#[test] -fn test_wrap_angle_bracket_url() { - let input = lines_vec![concat!( - "[^5]: Given When Then - Martin Fowler, accessed on 14 July 2025, ", - "" - )]; - let expected = lines_vec![ - "[^5]: Given When Then - Martin Fowler, accessed on 14 July 2025,", - " ", - ]; - let output = process_stream(&input); - assert_eq!(output, expected); -} - -/// Checks that a sequence of footnotes is not altered by wrapping. -/// -/// This regression test ensures that the footnote collection remains -/// unchanged when passed to `process_stream`. -#[test] -fn test_wrap_footnote_collection() { - let input = lines_vec![ - "[^1]: ", - "[^2]: ", - "[^3]: ", - "[^4]: ", - "[^5]: ", - "[^6]: ", - "[^7]: ", - "[^8]: ", - ]; - - let output = process_stream(&input); - assert_eq!(output, input); -} - -#[test] -/// Verifies that short list items are not wrapped or altered by the stream processing logic. -/// -/// Ensures that a single-line bullet list item remains unchanged after processing. -fn test_wrap_short_list_item() { - let input = lines_vec!["- short item"]; - let output = process_stream(&input); - assert_eq!(output, input); -} - -#[test] -fn test_wrap_blockquote() { - let input = lines_vec![ - "> **Deprecated**: A :class:`WebSocketRouter` and its `add_route` API should be used to \ - instantiate resources.", - ]; - let output = process_stream(&input); - assert_eq!( - output, - lines_vec![ - "> **Deprecated**: A :class:`WebSocketRouter` and its `add_route` API should be", - "> used to instantiate resources.", - ] - ); -} - -#[test] -fn test_wrap_blockquote_nested() { - let input = lines_vec![concat!( - "> > This nested quote contains enough text to require wrapping so that we ", - "can verify multi-level handling." - )]; - let output = process_stream(&input); - common::assert_wrapped_blockquote(&output, "> > ", 2); - let joined = output - .iter() - .map(|l| l.trim_start_matches("> > ")) - .collect::>() - .join(" "); - assert_eq!(joined, input[0].trim_start_matches("> > ")); -} - -#[test] -fn test_wrap_blockquote_with_blank_lines() { - let input = lines_vec![ - concat!( - "> The first paragraph in this quote is deliberately long enough to wrap ", - "across multiple lines so" - ), - "> demonstrate the behaviour.", - ">", - concat!( - "> The second paragraph is also extended to trigger wrapping in order to ", - "ensure blank lines" - ), - "> are preserved correctly.", - ]; - let output = process_stream(&input); - assert_eq!(output[3], ">"); - common::assert_wrapped_blockquote(&output[..3], "> ", 3); - common::assert_wrapped_blockquote(&output[4..], "> ", 3); -} - -#[test] -fn test_wrap_blockquote_extra_whitespace() { - let input = lines_vec![ - "> Extra spacing should not prevent correct wrapping of this quoted text that exceeds \ - the line width.", - ]; - let output = process_stream(&input); - common::assert_wrapped_blockquote(&output, "> ", 2); - let joined = output - .iter() - .map(|l| l.trim_start_matches("> ")) - .collect::>() - .join(" "); - assert_eq!(joined, input[0].trim_start_matches("> ")); -} - -#[test] -fn test_wrap_blockquote_short() { - let input = lines_vec!["> short"]; - let output = process_stream(&input); - assert_eq!(output, input); -} - -#[test] -/// Tests that lines with hard line breaks (trailing spaces) are preserved after processing. -/// -/// Ensures that the `process_stream` function does not remove or alter lines ending with Markdown -/// hard line breaks. -fn test_preserve_hard_line_breaks() { - let input = lines_vec!["Line one with break. ", "Line two follows."]; - let output = process_stream(&input); - assert_eq!(output.len(), 2); - assert_eq!(output[0], "Line one with break."); - assert_eq!(output[1], "Line two follows."); -} - -#[test] -fn test_wrap_hard_linebreak_backslash() { - let input: Vec = include_lines!("data/hard_linebreak_input.txt"); - let expected: Vec = include_lines!("data/hard_linebreak_expected.txt"); - assert_eq!(process_stream(&input), expected); -} - -#[test] -fn test_wrap_hard_linebreak_backslash_edge_cases() { - let input = lines_vec!( - "This line ends with two backslashes: \\\\", - "This line ends with a single backslash: \\", - " \\ ", - "\\", - "Text before \\ and after", - " \\", - "", - ); - let expected = lines_vec!( - "This line ends with two backslashes: \\\\ This line ends with a single backslash:", - "\\", - "\\", - "\\", - "Text before \\ and after \\", - "", - ); - assert_eq!(process_stream(&input), expected); -} - -#[test] -/// Tests that `process_stream` preserves complex table formatting without modification. -/// -/// This regression test ensures that properly formatted complex tables with multiple -/// columns and detailed content pass through the processing pipeline unchanged, -/// preventing regressions that might inadvertently alter correct formatting. -fn test_regression_complex_table() { - let input: Vec = include_lines!("data/regression_table_input.txt"); - let expected: Vec = include_lines!("data/regression_table_expected.txt"); - assert_eq!(process_stream(&input), expected); -} - -#[test] -fn test_renumber_basic() { - let input = lines_vec!["1. first", "2. second", "7. third"]; - let expected = lines_vec!["1. first", "2. second", "3. third"]; - assert_eq!(renumber_lists(&input), expected); -} - -#[test] -fn test_renumber_with_fence() { - let input = lines_vec!["1. item", "```", "code", "```", "9. next"]; - let expected = lines_vec!["1. item", "```", "code", "```", "2. next"]; - assert_eq!(renumber_lists(&input), expected); -} - -#[test] -fn test_cli_renumber_option() { - let output = Command::cargo_bin("mdtablefix") - .unwrap() - .arg("--renumber") - .write_stdin("1. a\n4. b\n") - .output() - .unwrap(); - assert!(output.status.success()); - let text = String::from_utf8_lossy(&output.stdout); - assert_eq!(text, "1. a\n2. b\n"); -} - -#[test] -fn test_renumber_nested_lists() { - let input = lines_vec![ - "1. first", - " 1. sub first", - " 3. sub second", - "2. second", - ]; - - let expected = lines_vec![ - "1. first", - " 1. sub first", - " 2. sub second", - "2. second", - ]; - - assert_eq!(renumber_lists(&input), expected); -} - -#[test] -fn test_renumber_tabs_in_indent() { - let input = lines_vec!["1. first", "\t1. sub first", "\t5. sub second", "2. second"]; - - let expected = lines_vec!["1. first", "\t1. sub first", "\t2. sub second", "2. second"]; - - assert_eq!(renumber_lists(&input), expected); -} - -#[test] -fn test_renumber_mult_paragraph_items() { - let input = lines_vec!["1. first", "", " still first paragraph", "", "2. second"]; - - let expected = lines_vec!["1. first", "", " still first paragraph", "", "2. second"]; - - assert_eq!(renumber_lists(&input), expected); -} - -#[test] -fn test_renumber_table_in_list() { - let input = lines_vec!["1. first", " | A | B |", " | 1 | 2 |", "5. second"]; - - let expected = lines_vec!["1. first", " | A | B |", " | 1 | 2 |", "2. second"]; - - assert_eq!(renumber_lists(&input), expected); -} - -#[test] -fn test_renumber_restart_after_paragraph() { - let input: Vec = include_lines!("data/renumber_paragraph_restart_input.txt"); - let expected: Vec = include_lines!("data/renumber_paragraph_restart_expected.txt"); - assert_eq!(renumber_lists(&input), expected); -} - -#[test] -fn test_renumber_restart_after_formatting_paragraph() { - let input: Vec = include_str!("data/renumber_formatting_paragraph_input.txt") - .lines() - .map(str::to_string) - .collect(); - let expected: Vec = include_str!("data/renumber_formatting_paragraph_expected.txt") - .lines() - .map(str::to_string) - .collect(); - assert_eq!(renumber_lists(&input), expected); -} - -#[test] -fn test_format_breaks_basic() { - let input = lines_vec!["foo", "***", "bar"]; - let expected: Vec> = vec![ - input[0].as_str().into(), - Cow::Owned("_".repeat(THEMATIC_BREAK_LEN)), - input[2].as_str().into(), - ]; - assert_eq!(format_breaks(&input), expected); -} - -#[test] -fn test_format_breaks_ignores_code() { - let input = lines_vec!["```", "---", "```"]; - let expected: Vec> = input.iter().map(|s| s.as_str().into()).collect(); - assert_eq!(format_breaks(&input), expected); -} - -#[test] -fn test_format_breaks_mixed_chars() { - let input = lines_vec!["-*-*-"]; - let expected: Vec> = input.iter().map(|s| s.as_str().into()).collect(); - assert_eq!(format_breaks(&input), expected); -} - -#[test] -fn test_format_breaks_with_spaces_and_indent() { - let input = lines_vec![" - - - "]; - let expected: Vec> = vec![Cow::Owned("_".repeat(THEMATIC_BREAK_LEN))]; - assert_eq!(format_breaks(&input), expected); -} - -#[test] -fn test_format_breaks_with_tabs_and_underscores() { - let input = lines_vec!["\t_\t_\t_\t"]; - let expected: Vec> = vec![Cow::Owned("_".repeat(THEMATIC_BREAK_LEN))]; - assert_eq!(format_breaks(&input), expected); -} - -#[test] -fn test_cli_breaks_option() { - let output = Command::cargo_bin("mdtablefix") - .unwrap() - .arg("--breaks") - .write_stdin("---\n") - .output() - .unwrap(); - assert!(output.status.success()); - assert_eq!( - String::from_utf8_lossy(&output.stdout), - format!("{}\n", "_".repeat(THEMATIC_BREAK_LEN)) - ); -} - -#[test] -fn test_cli_ellipsis_option() { - let output = Command::cargo_bin("mdtablefix") - .unwrap() - .arg("--ellipsis") - .write_stdin("foo...\n") - .output() - .unwrap(); - assert!(output.status.success()); - assert_eq!(String::from_utf8_lossy(&output.stdout), "foo…\n"); -} - -#[test] -fn test_cli_ellipsis_code_span() { - let output = Command::cargo_bin("mdtablefix") - .unwrap() - .arg("--ellipsis") - .write_stdin("before `dots...` after\n") - .output() - .unwrap(); - assert!(output.status.success()); - assert_eq!( - String::from_utf8_lossy(&output.stdout), - "before `dots...` after\n" - ); -} - -#[test] -fn test_cli_ellipsis_fenced_block() { - let output = Command::cargo_bin("mdtablefix") - .unwrap() - .arg("--ellipsis") - .write_stdin("```\nlet x = ...;\n```\n") - .output() - .unwrap(); - assert!(output.status.success()); - assert_eq!( - String::from_utf8_lossy(&output.stdout), - "```\nlet x = ...;\n```\n" - ); -} - -#[test] -fn test_cli_ellipsis_long_sequence() { - let output = Command::cargo_bin("mdtablefix") - .unwrap() - .arg("--ellipsis") - .write_stdin("wait....\n") - .output() - .unwrap(); - assert!(output.status.success()); - assert_eq!(String::from_utf8_lossy(&output.stdout), "wait….\n"); -} diff --git a/tests/lists.rs b/tests/lists.rs index ea3e785e..13d8e037 100644 --- a/tests/lists.rs +++ b/tests/lists.rs @@ -1,10 +1,10 @@ +//! Integration tests for list renumbering and counters. + use mdtablefix::{lists::pop_counters_upto, renumber_lists}; -macro_rules! lines_vec { - ($($line:expr),* $(,)?) => { - vec![$($line.to_string()),*] - }; -} +#[macro_use] +mod prelude; +use prelude::*; #[test] fn pop_counters_removes_deeper_levels() { @@ -61,3 +61,58 @@ fn restart_after_formatting_paragraph() { let expected = lines_vec!("1. Start", "", "**Bold intro**", "", "1. Next"); assert_eq!(renumber_lists(&input), expected); } +/// Tests the CLI `--renumber` option. +/// +/// Ensures that list numbering is corrected when the flag is supplied. +#[test] +fn test_cli_renumber_option() { + let output = Command::cargo_bin("mdtablefix") + .expect("Failed to create cargo command for mdtablefix") + .arg("--renumber") + .write_stdin("1. a\n4. b\n") + .output() + .expect("Failed to execute mdtablefix command"); + assert!(output.status.success()); + let text = String::from_utf8_lossy(&output.stdout); + assert_eq!(text, "1. a\n2. b\n"); +} + +#[rstest( + input, + expected, + case::basic( + lines_vec!["1. first", "2. second", "7. third"], + lines_vec!["1. first", "2. second", "3. third"] + ), + case::with_fence( + lines_vec!["1. item", "```", "code", "```", "9. next"], + lines_vec!["1. item", "```", "code", "```", "2. next"] + ), + case::nested_lists( + lines_vec!["1. first", " 1. sub first", " 3. sub second", "2. second"], + lines_vec!["1. first", " 1. sub first", " 2. sub second", "2. second"] + ), + case::tabs_in_indent( + lines_vec!["1. first", "\t1. sub first", "\t5. sub second", "2. second"], + lines_vec!["1. first", "\t1. sub first", "\t2. sub second", "2. second"] + ), + case::mult_paragraph_items( + lines_vec!["1. first", "", " still first paragraph", "", "2. second"], + lines_vec!["1. first", "", " still first paragraph", "", "2. second"] + ), + case::table_in_list( + lines_vec!["1. first", " | A | B |", " | 1 | 2 |", "5. second"], + lines_vec!["1. first", " | A | B |", " | 1 | 2 |", "2. second"] + ), + case::restart_after_paragraph( + include_lines!("data/renumber_paragraph_restart_input.txt"), + include_lines!("data/renumber_paragraph_restart_expected.txt") + ), + case::restart_after_formatting( + include_lines!("data/renumber_formatting_paragraph_input.txt"), + include_lines!("data/renumber_formatting_paragraph_expected.txt") + ) +)] +fn test_renumber_cases(input: Vec, expected: Vec) { + assert_eq!(renumber_lists(&input), expected); +} diff --git a/tests/prelude/mod.rs b/tests/prelude/mod.rs new file mode 100644 index 00000000..32f7534b --- /dev/null +++ b/tests/prelude/mod.rs @@ -0,0 +1,13 @@ +//! Common imports for integration tests. +#![allow(unfulfilled_lint_expectations)] + +#[expect(unused_imports, reason = "re-exporting common test utilities")] +pub use assert_cmd::Command; +#[expect(unused_imports, reason = "re-exporting common test utilities")] +pub use rstest::{fixture, rstest}; + +#[macro_use] +#[path = "../common/mod.rs"] +mod common; +#[expect(unused_imports, reason = "re-exporting common test utilities")] +pub use common::*; diff --git a/tests/table.rs b/tests/table.rs new file mode 100644 index 00000000..6660d059 --- /dev/null +++ b/tests/table.rs @@ -0,0 +1,411 @@ +//! Integration tests for table reflow and HTML table conversion. +//! +//! Covers `reflow_table`, `convert_html_tables` and related +//! `process_stream` behaviour. + +use mdtablefix::{convert_html_tables, process_stream, reflow_table}; + +#[macro_use] +mod prelude; +use prelude::*; + +#[fixture] +fn malformed_table() -> Vec { + let lines = lines_vec!["| A | |", "| 1 | 2 | 3 |"]; + lines +} + +#[fixture] +fn header_table() -> Vec { + lines_vec!["| A | B | |", "| --- | --- |", "| 1 | 2 | | 3 | 4 |"] +} + +#[fixture] +fn escaped_pipe_table() -> Vec { + lines_vec!["| X | Y | |", "| a \\| b | 1 | | 2 | 3 |"] +} + +#[fixture] +fn indented_table() -> Vec { + let lines = lines_vec![" | I | J | |", " | 1 | 2 | | 3 | 4 |"]; + lines +} + +#[fixture] +fn html_table() -> Vec { + lines_vec![ + "", + "", + "", + "
AB
12
", + ] +} + +#[fixture] +fn html_table_with_attrs() -> Vec { + lines_vec![ + "", + "", + "", + "
AB
12
", + ] +} + +#[fixture] +fn html_table_with_colspan() -> Vec { + lines_vec![ + "", + "", + "", + "
A
12
", + ] +} + +#[fixture] +fn html_table_no_header() -> Vec { + lines_vec![ + "", + "", + "", + "
AB
12
", + ] +} + +#[fixture] +fn html_table_empty_row() -> Vec { + lines_vec![ + "", + "", + "", + "
12
", + ] +} + +#[fixture] +fn html_table_whitespace_header() -> Vec { + lines_vec![ + "", + "", + "", + "
12
", + ] +} + +#[fixture] +fn html_table_inconsistent_first_row() -> Vec { + lines_vec![ + "", + "", + "", + "
A
12
", + ] +} + +#[fixture] +fn html_table_empty() -> Vec { + let lines = lines_vec!["
"]; + lines +} + +#[fixture] +fn html_table_unclosed() -> Vec { + let lines = lines_vec!["", ""]; + lines +} + +#[fixture] +fn html_table_uppercase() -> Vec { + lines_vec![ + "
1
", + "", + "", + "
AB
12
", + ] +} + +#[fixture] +fn html_table_mixed_case() -> Vec { + lines_vec![ + "", + "", + "", + "
AB
12
", + ] +} + +#[fixture] +fn multiple_tables() -> Vec { + lines_vec!["| A | B |", "| 1 | 22 |", "", "| X | Y |", "| 3 | 4 |"] +} + +#[rstest] +fn test_reflow_basic(broken_table: Vec) { + let expected = lines_vec!["| A | B |", "| 1 | 2 |", "| 3 | 4 |"]; + assert_eq!(reflow_table(&broken_table), expected); +} + +#[rstest] +fn test_reflow_malformed_returns_original(malformed_table: Vec) { + assert_eq!(reflow_table(&malformed_table), malformed_table); +} + +#[rstest] +fn test_reflow_preserves_header(header_table: Vec) { + let expected = lines_vec!["| A | B |", "| --- | --- |", "| 1 | 2 |", "| 3 | 4 |"]; + assert_eq!(reflow_table(&header_table), expected); +} + +#[rstest] +fn test_reflow_handles_escaped_pipes(escaped_pipe_table: Vec) { + let expected = lines_vec!["| X | Y |", "| a | b | 1 |", "| 2 | 3 |"]; + assert_eq!(reflow_table(&escaped_pipe_table), expected); +} + +#[rstest] +fn test_reflow_preserves_indentation(indented_table: Vec) { + let expected = lines_vec![" | I | J |", " | 1 | 2 |", " | 3 | 4 |"]; + assert_eq!(reflow_table(&indented_table), expected); +} + +#[rstest( + table, + case::basic(html_table()), + case::attrs(html_table_with_attrs()), + case::uppercase(html_table_uppercase()), + case::mixed_case(html_table_mixed_case()) +)] +fn test_process_stream_html_variants(table: Vec) { + let expected = lines_vec!["| A | B |", "| --- | --- |", "| 1 | 2 |"]; + assert_eq!(process_stream(&table), expected); +} + +#[rstest] +fn test_process_stream_multiple_tables(multiple_tables: Vec) { + let expected = lines_vec!["| A | B |", "| 1 | 22 |", "", "| X | Y |", "| 3 | 4 |"]; + assert_eq!(process_stream(&multiple_tables), expected); +} + +#[rstest] +fn test_process_stream_ignores_code_fences() { + let lines = lines_vec!["```rust", "| not | a | table |", "```"]; + assert_eq!(process_stream(&lines), lines); + + let tilde_lines = lines_vec!["~~~", "| not | a | table |", "~~~"]; + assert_eq!(process_stream(&tilde_lines), tilde_lines); +} + +#[rstest] +fn test_process_stream_ignores_indented_fences() { + let lines = lines_vec!( + " ```javascript", + " socket.onmessage = function(event) {", + " const message = JSON.parse(event.data);", + " switch(message.type) {", + " case \"serverNewMessage\":", + " // Display message.payload.user and message.payload.text", + " break;", + " case \"serverUserJoined\":", + " // Update user list with message.payload.user", + " break;", + " // Handle other message types...", + " }", + " };", + "", + " ```", + ); + assert_eq!(process_stream(&lines), lines); +} + +#[test] +fn test_uniform_example_one() { + let input = lines_vec![ + "| Logical type | PostgreSQL | SQLite notes |", + "|--------------|-------------------------|----------------------------------------------------|", + "| strings | `TEXT` (or `VARCHAR`) | `TEXT` - SQLite ignores the length specifier anyway |", + "| booleans | `BOOLEAN DEFAULT FALSE` | declare as `BOOLEAN`; Diesel serialises to 0 / 1 so this is fine |", + "| integers | `INTEGER` / `BIGINT` | ditto |", + "| decimals | `NUMERIC` | stored as FLOAT in SQLite; Diesel `Numeric` round-trips, but beware precision |", + "| blobs / raw | `BYTEA` | `BLOB` |", + ]; + let output = reflow_table(&input); + assert!(!output.is_empty()); + let widths: Vec = output[0] + .trim_matches('|') + .split('|') + .map(str::len) + .collect(); + for row in output { + let cols: Vec<&str> = row.trim_matches('|').split('|').collect(); + for (i, col) in cols.iter().enumerate() { + assert_eq!(col.len(), widths[i]); + } + } +} + +#[test] +fn test_uniform_example_two() { + let input = lines_vec![ + "| Option | How it works | When to choose it |", + "|--------------------------------------|---------------------------------------------------------------------------------------|------------------------------------------------------------------------------|", + "| **B. Pure-Rust migrations** | Implement `diesel::migration::Migration` in a Rust file (`up.rs` / `down.rs`) and compile with both `features = [\"postgres\", \"sqlite\"]`. The query builder emits backend-specific SQL at runtime. | You prefer the type-checked DSL and can live with slightly slower compile times. |", + "| **C. Lowest-common-denominator SQL** | Write one `up.sql`/`down.sql` that *already* works on both engines. This demands avoiding SERIAL/IDENTITY, JSONB, `TIMESTAMPTZ`, etc. | Simple schemas, embedded use-case only, you are happy to supply integer primary keys manually. |", + "| **D. Two separate migration trees** | Maintain `migrations/sqlite` and `migrations/postgres` directories with identical version numbers. Use `embed_migrations!(\"migrations/\")` to compile the right set. | You ship a single binary with migrations baked in. |", + ]; + let output = reflow_table(&input); + assert!(!output.is_empty()); + let widths: Vec = output[0] + .trim_matches('|') + .split('|') + .map(str::len) + .collect(); + for row in output { + let cols: Vec<&str> = row.trim_matches('|').split('|').collect(); + for (i, col) in cols.iter().enumerate() { + assert_eq!(col.len(), widths[i]); + } + } +} + +#[test] +fn test_non_table_lines_unchanged() { + let input = lines_vec![ + "# Title", + "", + "Para text.", + "", + "| a | b |", + "| 1 | 22 |", + "", + "* bullet", + "", + ]; + let output = process_stream(&input); + let expected = lines_vec![ + "# Title", + "", + "Para text.", + "", + "| a | b |", + "| 1 | 22 |", + "", + "* bullet", + "", + ]; + assert_eq!(output, expected); +} + +#[test] +fn test_process_stream_only_whitespace() { + let input = lines_vec!["", " ", "\t\t"]; + let expected = lines_vec!["", "", ""]; + assert_eq!(process_stream(&input), expected); +} + +#[rstest( + input, + expected, + case::basic(html_table(), lines_vec!["| A | B |", "| --- | --- |", "| 1 | 2 |"]), + case::with_attrs(html_table_with_attrs(), lines_vec!["| A | B |", "| --- | --- |", "| 1 | 2 |"]), + case::uppercase(html_table_uppercase(), lines_vec!["| A | B |", "| --- | --- |", "| 1 | 2 |"]), +)] +fn test_convert_html_table_standard(input: Vec, expected: Vec) { + assert_eq!(convert_html_tables(&input), expected); +} + +#[rstest( + input, + expected, + case::colspan(html_table_with_colspan(), lines_vec!["| A |", "| --- |", "| 1 | 2 |"]), + case::inconsistent(html_table_inconsistent_first_row(), lines_vec!["| A |", "| --- |", "| 1 | 2 |"]), +)] +fn test_convert_html_table_reduced(input: Vec, expected: Vec) { + assert_eq!(convert_html_tables(&input), expected); +} + +#[test] +fn test_convert_html_table_no_header() { + let expected = lines_vec!["| A | B |", "| --- | --- |", "| 1 | 2 |"]; + assert_eq!(convert_html_tables(&html_table_no_header()), expected); +} + +#[test] +fn test_convert_html_table_empty_row() { + let expected = lines_vec!["| 1 | 2 |", "| --- | --- |"]; + assert_eq!(convert_html_tables(&html_table_empty_row()), expected); +} + +#[test] +fn test_convert_html_table_whitespace_header() { + let expected = lines_vec!["| --- | --- |", "| --- | --- |", "| 1 | 2 |"]; + assert_eq!( + convert_html_tables(&html_table_whitespace_header()), + expected + ); +} + +#[test] +fn test_convert_html_table_empty() { + assert!(convert_html_tables(&html_table_empty()).is_empty()); +} + +#[test] +fn test_convert_html_table_unclosed_returns_original() { + let html = html_table_unclosed(); + assert_eq!(convert_html_tables(&html), html); +} + +#[test] +fn test_convert_html_table_bold_header() { + let input: Vec = include_lines!("data/bold_header_input.txt"); + let expected: Vec = include_lines!("data/bold_header_expected.txt"); + assert_eq!(convert_html_tables(&input), expected); +} + +#[test] +fn test_logical_type_table_output_matches() { + let input: Vec = include_lines!("data/logical_type_input.txt"); + let expected: Vec = include_lines!("data/logical_type_expected.txt"); + assert_eq!(reflow_table(&input), expected); +} + +#[test] +fn test_option_table_output_matches() { + let input: Vec = include_lines!("data/option_table_input.txt"); + let expected: Vec = include_lines!("data/option_table_expected.txt"); + assert_eq!(reflow_table(&input), expected); +} + +#[test] +fn test_month_seconds_table_output_matches() { + let input: Vec = include_lines!("data/month_seconds_input.txt"); + let expected: Vec = include_lines!("data/month_seconds_expected.txt"); + assert_eq!(reflow_table(&input), expected); +} + +#[test] +fn test_offset_table_output_matches() { + let input: Vec = include_lines!("data/offset_table_input.txt"); + let expected: Vec = include_lines!("data/offset_table_expected.txt"); + assert_eq!(reflow_table(&input), expected); +} + +#[test] +fn test_process_stream_logical_type_table() { + let input: Vec = include_lines!("data/logical_type_input.txt"); + let expected: Vec = include_lines!("data/logical_type_expected.txt"); + assert_eq!(process_stream(&input), expected); +} + +#[test] +fn test_process_stream_option_table() { + let input: Vec = include_lines!("data/option_table_input.txt"); + let expected: Vec = include_lines!("data/option_table_expected.txt"); + assert_eq!(process_stream(&input), expected); +} + +#[test] +fn test_regression_complex_table() { + let input: Vec = include_lines!("data/regression_table_input.txt"); + let expected: Vec = include_lines!("data/regression_table_expected.txt"); + assert_eq!(process_stream(&input), expected); +} diff --git a/tests/wrap.rs b/tests/wrap.rs new file mode 100644 index 00000000..594b9ade --- /dev/null +++ b/tests/wrap.rs @@ -0,0 +1,426 @@ +//! Integration tests for text wrapping behaviour in Markdown content. +//! +//! This module validates the wrapping functionality of the `mdtablefix` tool, +//! including: +//! - Paragraph wrapping at 80-character boundaries +//! - List item wrapping with proper indentation preservation +//! - Blockquote wrapping with prefix maintenance +//! - Footnote wrapping with correct formatting +//! - Preservation of inline code spans during wrapping +//! - Hard line break handling +//! - CLI `--wrap` option functionality + +use mdtablefix::process_stream; + +#[macro_use] +mod prelude; +use prelude::*; +/// Tests that long paragraphs are wrapped at 80-character boundaries. +/// +/// Verifies that a paragraph exceeding 80 characters is split into multiple +/// lines, each not exceeding the limit. +#[test] +fn test_wrap_paragraph() { + let input = lines_vec![ + "This is a very long paragraph that should be wrapped at eighty columns so it needs to \ + contain enough words to exceed that limit.", + ]; + let output = process_stream(&input); + assert!(output.len() > 1); + assert!(output.iter().all(|l| l.len() <= 80)); +} + +/// Ensures that a paragraph with a single word longer than 80 characters is +/// handled correctly. +#[test] +fn test_wrap_paragraph_with_long_word() { + let long_word = "a".repeat(100); + let input = lines_vec![&long_word]; + let output = process_stream(&input); + assert_eq!(output.len(), 1); + assert_eq!(output[0], long_word); +} + +/// Tests that list items are wrapped whilst preserving prefix formatting. +/// +/// Verifies that long bullet point items are correctly wrapped across multiple +/// lines with proper indentation maintained. +#[test] +fn test_wrap_list_item() { + let input = lines_vec![ + r"- This bullet item is exceptionally long and must be wrapped to keep prefix formatting intact.", + ]; + let output = process_stream(&input); + assert_wrapped_list_item(&output, "- ", 2); +} + +/// Parameterised test verifying inline code wrapping for various list prefixes. +/// +/// Ensures that list items with inline code spans retain prefix formatting +/// across different bullet and numbered list styles. +#[rstest] +#[case("- ", 3)] +#[case("1. ", 3)] +#[case("10. ", 3)] +#[case("100. ", 3)] +fn test_wrap_list_items_with_inline_code(#[case] prefix: &str, #[case] expected: usize) { + let input = lines_vec![format!( + "{prefix}`script`: A multi-line script declared with the YAML `|` block style. The entire \ + block is passed to an interpreter. If the first line begins with `#!`, Netsuke executes \ + the script verbatim, respecting the shebang." + )]; + let output = process_stream(&input); + assert_wrapped_list_item(&output, prefix, expected); +} + +/// Tests that inline code spans are preserved during list item wrapping. +/// +/// Verifies that backtick-delimited code spans remain intact when wrapping +/// long list items across multiple lines. +#[test] +fn test_wrap_preserves_inline_code_spans() { + let input = lines_vec![ + "- `script`: A multi-line script declared with the YAML `|` block style. The entire block \ + is passed to an interpreter. If the first line begins with `#!`, Netsuke executes the \ + script verbatim, respecting the shebang.", + ]; + let output = process_stream(&input); + assert_wrapped_list_item(&output, "- ", 3); +} + +/// Tests that multi-backtick code spans are preserved during wrapping. +/// +/// Verifies that code spans using multiple backticks (``cmd``, ```echo```) are +/// not broken when wrapping list items. +#[test] +fn test_wrap_multi_backtick_code() { + let input = lines_vec![ + "- ``cmd`` executes ```echo``` output with ``json`` format and prints results to the \ + console", + ]; + let output = process_stream(&input); + assert_wrapped_list_item(&output, "- ", 2); +} + +/// Tests that multiple inline code spans are preserved during wrapping. +/// +/// Verifies that list items containing multiple code spans are wrapped correctly +/// without breaking the span boundaries. +#[test] +fn test_wrap_multiple_inline_code_spans() { + let input = lines_vec![ + "- Use `foo` and `bar` inside ``baz`` for testing with additional commentary to exceed \ + wrapping width", + ]; + let output = process_stream(&input); + assert_wrapped_list_item(&output, "- ", 2); +} +/// Tests wrapping of list items with long inline code spans. +/// +/// Verifies that list items containing lengthy code spans are wrapped +/// appropriately whilst preserving the code span integrity. +#[test] +fn test_wrap_long_inline_code_item() { + let input = lines_vec![concat!( + "- `async def on_unhandled(self, ws: WebSocketLike, message: Union[str, bytes])`:", + " A fallback handler for messages that are not dispatched by the more specific", + " message handlers. This can be used for raw text/binary data or messages that", + " don't conform to the expected structured format." + )]; + let output = process_stream(&input); + assert_wrapped_list_item(&output, "- ", 4); + assert!( + output + .first() + .expect("wrapped output should contain at least one line") + .ends_with("`:") + ); +} + +/// Tests wrapping for punctuation around future attribute references. +/// +/// Ensures that long bullet items containing attribute syntax such as +/// `#[future]` are wrapped correctly without splitting the punctuation. +#[test] +fn test_wrap_future_attribute_punctuation() { + let input = lines_vec![concat!( + "- Test function (`#[awt]`) or a specific `#[future]` argument ", + "(`#[future(awt)]`), tells `rstest` to automatically insert `.await` ", + "calls for those futures." + )]; + let output = process_stream(&input); + assert_eq!( + output, + vec![ + "- Test function (`#[awt]`) or a specific `#[future]` argument".to_string(), + " (`#[future(awt)]`), tells `rstest` to automatically insert `.await` calls for" + .to_string(), + " those futures.".to_string(), + ] + ); +} + +/// Tests wrapping for multi-line footnotes with correct indentation. +/// +/// Verifies that long footnotes are split across lines with the footnote +/// prefix preserved. +#[test] +fn test_wrap_footnote_multiline() { + let input = lines_vec![concat!( + "[^note]: This footnote is sufficiently long to require wrapping ", + "across multiple lines so we can verify indentation." + )]; + let output = process_stream(&input); + assert_wrapped_list_item(&output, "[^note]: ", 2); +} + +/// Tests that footnotes containing inline code are wrapped correctly. +/// +/// Verifies that code spans within footnotes are preserved during wrapping. +#[test] +fn test_wrap_footnote_with_inline_code() { + let input = lines_vec![concat!( + " [^code_note]: A footnote containing inline `code` that should wrap ", + "across multiple lines without breaking the span." + )]; + let output = process_stream(&input); + assert_wrapped_list_item(&output, " [^code_note]: ", 2); +} + +/// Tests that footnotes with angle-bracketed URLs are wrapped correctly. +/// +/// Verifies that when a footnote line contains a URL enclosed in angle brackets, +/// the URL is moved to a new indented line beneath the footnote text. +#[test] +fn test_wrap_angle_bracket_url() { + let input = lines_vec![concat!( + "[^5]: Given When Then - Martin Fowler, accessed on 14 July 2025, ", + "" + )]; + let expected = lines_vec![ + "[^5]: Given When Then - Martin Fowler, accessed on 14 July 2025,", + " ", + ]; + let output = process_stream(&input); + assert_eq!(output, expected); +} + +/// Checks that a sequence of footnotes is not altered by wrapping. +/// +/// This regression test ensures that the footnote collection remains +/// unchanged when passed to `process_stream`. +#[test] +fn test_wrap_footnote_collection() { + let input = lines_vec![ + "[^1]: ", + "[^2]: ", + "[^3]: ", + "[^4]: ", + "[^5]: ", + "[^6]: ", + "[^7]: ", + "[^8]: ", + ]; + + let output = process_stream(&input); + assert_eq!(output, input); +} + +/// Verifies that short list items are not wrapped or altered by the stream processing logic. +/// +/// Ensures that a single-line bullet list item remains unchanged after processing. +#[test] +fn test_wrap_short_list_item() { + let input = lines_vec!["- short item"]; + let output = process_stream(&input); + assert_eq!(output, input); +} + +/// Tests wrapping behaviour for single-level blockquotes. +/// +/// Verifies that long quoted text is wrapped onto multiple lines while +/// preserving the ">" prefix on each line. +#[test] +fn test_wrap_blockquote() { + let input = lines_vec![ + "> **Deprecated**: A :class:`WebSocketRouter` and its `add_route` API should be used to \ + instantiate resources.", + ]; + let output = process_stream(&input); + assert_eq!( + output, + lines_vec![ + "> **Deprecated**: A :class:`WebSocketRouter` and its `add_route` API should be", + "> used to instantiate resources.", + ] + ); +} + +/// Tests that nested blockquotes are wrapped correctly. +/// +/// Verifies that multi-level blockquotes ("> > ") maintain their nesting +/// structure when wrapped across multiple lines. +#[test] +fn test_wrap_blockquote_nested() { + let input = lines_vec![concat!( + "> > This nested quote contains enough text to require wrapping so that we ", + "can verify multi-level handling." + )]; + let output = process_stream(&input); + assert_wrapped_blockquote(&output, "> > ", 2); + let joined = output + .iter() + .map(|l| l.trim_start_matches("> > ")) + .collect::>() + .join(" "); + assert_eq!(joined, input[0].trim_start_matches("> > ")); +} + +/// Tests blockquote wrapping with mixed spaces and tabs in prefix. +/// +/// Verifies that blockquotes using both spaces and tabs maintain correct +/// prefix formatting when wrapped. +#[test] +fn test_wrap_blockquote_mixed_indentation() { + let input = lines_vec![ + "> \t> \tThis blockquote uses both spaces and tabs in the prefix to test mixed \ + indentation handling." + ]; + let output = process_stream(&input); + assert_wrapped_blockquote(&output, "> \t> \t", 2); + let joined = output + .iter() + .map(|l| l.trim_start_matches("> \t> \t")) + .collect::>() + .join(" "); + assert_eq!(joined, input[0].trim_start_matches("> \t> \t")); +} + +/// Tests blockquote wrapping with blank lines preserved. +/// +/// Verifies that blank lines within blockquotes are maintained correctly when wrapping long quoted +/// paragraphs. +#[test] +fn test_wrap_blockquote_with_blank_lines() { + let input = lines_vec![ + concat!( + "> The first paragraph in this quote is deliberately long enough to wrap ", + "across multiple lines so" + ), + "> demonstrate the behaviour.", + ">", + concat!( + "> The second paragraph is also extended to trigger wrapping in order to ", + "ensure blank lines" + ), + "> are preserved correctly.", + ]; + let output = process_stream(&input); + assert_eq!(output[3], ">"); + assert_wrapped_blockquote(&output[..3], "> ", 3); + assert_wrapped_blockquote(&output[4..], "> ", 3); +} + +/// Tests blockquote wrapping with extra spacing in prefix. +/// +/// Verifies that blockquotes with additional spaces after ">" are wrapped correctly whilst +/// preserving the spacing. +#[test] +fn test_wrap_blockquote_extra_whitespace() { + let input = lines_vec![ + "> Extra spacing should not prevent correct wrapping of this quoted text that exceeds \ + the line width.", + ]; + let output = process_stream(&input); + assert_wrapped_blockquote(&output, "> ", 2); + let joined = output + .iter() + .map(|l| l.trim_start_matches("> ")) + .collect::>() + .join(" "); + assert_eq!(joined, input[0].trim_start_matches("> ")); +} + +/// Tests that short blockquotes remain unchanged after processing. +/// +/// Verifies that brief quoted text is not altered by the wrapping logic. +#[test] +fn test_wrap_blockquote_short() { + let input = lines_vec!["> short"]; + let output = process_stream(&input); + assert_eq!(output, input); +} + +/// Tests that lines with hard line breaks (trailing spaces) are preserved after processing. +/// +/// Ensures that the `process_stream` function does not remove or alter lines ending with Markdown +/// hard line breaks. +#[test] +fn test_preserve_hard_line_breaks() { + let input = lines_vec!["Line one with break. ", "Line two follows."]; + let output = process_stream(&input); + assert_eq!(output.len(), 2); + assert_eq!(output[0], "Line one with break."); + assert_eq!(output[1], "Line two follows."); +} + +/// Tests wrapping behaviour with backslash hard line breaks. +/// +/// Verifies that lines ending with backslashes are handled correctly +/// according to Markdown hard line break rules. +#[test] +fn test_wrap_hard_linebreak_backslash() { + let input: Vec = include_lines!("data/hard_linebreak_input.txt"); + let expected: Vec = include_lines!("data/hard_linebreak_expected.txt"); + assert_eq!(process_stream(&input), expected); +} + +/// Tests edge cases for backslash hard line break handling. +/// +/// Verifies correct processing of various backslash scenarios including +/// multiple backslashes, isolated backslashes, and trailing spaces. +#[test] +fn test_wrap_hard_linebreak_backslash_edge_cases() { + let input = lines_vec!( + "This line ends with two backslashes: \\\\", + "This line ends with a single backslash: \\", + " \\ ", + "\\", + "Text before \\ and after", + " \\", + "", + ); + let expected = lines_vec!( + "This line ends with two backslashes: \\\\ This line ends with a single backslash:", + "\\", + "\\", + "\\", + "Text before \\ and after \\", + "", + ); + assert_eq!(process_stream(&input), expected); +} + +/// Tests that the CLI `--wrap` option enables wrapping functionality. +/// +/// Verifies that when the `--wrap` flag is provided, the CLI tool wraps +/// long lines at 80 characters and produces multi-line output. +#[test] +fn test_cli_wrap_option() { + let input = "This line is deliberately made much longer than eighty columns so that the \ + wrapping algorithm is forced to insert a soft line-break somewhere in the middle \ + of the paragraph when the --wrap flag is supplied."; + let output = Command::cargo_bin("mdtablefix") + .expect("Failed to create cargo command for mdtablefix") + .arg("--wrap") + .write_stdin(format!("{input}\n")) + .output() + .expect("Failed to execute mdtablefix command"); + assert!(output.status.success()); + let text = String::from_utf8_lossy(&output.stdout); + assert!( + text.lines().count() > 1, + "expected wrapped output on multiple lines" + ); + assert!(text.lines().all(|l| l.len() <= 80)); +}