From 68f35d818852d8b955aed532b886adabb3f76e5b Mon Sep 17 00:00:00 2001 From: Leynos Date: Fri, 18 Jul 2025 23:39:43 +0100 Subject: [PATCH 01/11] Split integration tests by feature --- docs/rust-testing-with-rstest-fixtures.md | 12 +- tests/breaks.rs | 65 ++ tests/cli.rs | 110 +++ tests/common/mod.rs | 4 + tests/integration.rs | 1055 --------------------- tests/lists.rs | 102 +- tests/table.rs | 397 ++++++++ tests/wrap.rs | 323 +++++++ 8 files changed, 1002 insertions(+), 1066 deletions(-) create mode 100644 tests/breaks.rs create mode 100644 tests/cli.rs delete mode 100644 tests/integration.rs create mode 100644 tests/table.rs create mode 100644 tests/wrap.rs diff --git a/docs/rust-testing-with-rstest-fixtures.md b/docs/rust-testing-with-rstest-fixtures.md index 31251f2a..bff4240f 100644 --- a/docs/rust-testing-with-rstest-fixtures.md +++ b/docs/rust-testing-with-rstest-fixtures.md @@ -1345,16 +1345,16 @@ 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 diff --git a/tests/breaks.rs b/tests/breaks.rs new file mode 100644 index 00000000..ff66cea9 --- /dev/null +++ b/tests/breaks.rs @@ -0,0 +1,65 @@ +//! Integration tests for formatting thematic breaks. +//! +//! Verifies `format_breaks` function and `--breaks` CLI option. + +use std::borrow::Cow; + +use assert_cmd::Command; +use mdtablefix::{THEMATIC_BREAK_LEN, format_breaks}; + +#[macro_use] +mod common; + +#[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)) + ); +} diff --git a/tests/cli.rs b/tests/cli.rs new file mode 100644 index 00000000..e1bf8375 --- /dev/null +++ b/tests/cli.rs @@ -0,0 +1,110 @@ +//! Integration tests for remaining CLI behaviour. +//! +//! Covers file handling with `--in-place` and ellipsis replacement. + +use std::{fs::File, io::Write}; + +use assert_cmd::Command; +use rstest::{fixture, rstest}; +use tempfile::tempdir; + +#[macro_use] +mod common; + +#[fixture] +fn broken_table() -> Vec { + vec![ + "| A | B | |".to_string(), + "| 1 | 2 | | 3 | 4 |".to_string(), + ] +} + +#[test] +/// 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_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/common/mod.rs b/tests/common/mod.rs index f893484b..42da8c37 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -4,6 +4,7 @@ /// /// This macro is primarily used in tests to reduce boilerplate when /// constructing example tables or other collections of lines. +#[allow(unused_macros)] macro_rules! lines_vec { ($($line:expr),* $(,)?) => { vec![$($line.to_string()),*] @@ -16,6 +17,7 @@ macro_rules! lines_vec { /// ``` /// let input: Vec = include_lines!("data/bold_header_input.txt"); /// ``` +#[allow(unused_macros)] macro_rules! include_lines { ($path:literal $(,)?) => {{ const _TXT: &str = include_str!($path); @@ -27,6 +29,7 @@ macro_rules! include_lines { /// /// Verifies the number of lines, prefix on the first line, length of all lines, /// and indentation of continuation lines. +#[allow(dead_code)] 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,6 +70,7 @@ 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. +#[allow(dead_code)] pub fn assert_wrapped_blockquote(output: &[String], prefix: &str, expected: usize) { assert!(!output.is_empty(), "output slice is empty"); assert_eq!(output.len(), expected); 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..179f2ab3 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 common; +use assert_cmd::Command; #[test] fn pop_counters_removes_deeper_levels() { @@ -61,3 +61,95 @@ fn restart_after_formatting_paragraph() { let expected = lines_vec!("1. Start", "", "**Bold intro**", "", "1. Next"); assert_eq!(renumber_lists(&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); +} diff --git a/tests/table.rs b/tests/table.rs new file mode 100644 index 00000000..33ceb305 --- /dev/null +++ b/tests/table.rs @@ -0,0 +1,397 @@ +//! 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}; +use rstest::{fixture, rstest}; + +#[macro_use] +mod common; + +#[fixture] +fn broken_table() -> Vec { return lines_vec!["| A | B | |", "| 1 | 2 | | 3 | 4 |"]; } + +#[fixture] +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] +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", + 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); +} + +#[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..1f186e99 --- /dev/null +++ b/tests/wrap.rs @@ -0,0 +1,323 @@ +//! Integration tests for wrapping behaviour. +//! +//! Covers paragraphs, list items, blockquotes and footnotes, +//! including the `--wrap` CLI option. + +use assert_cmd::Command; +use mdtablefix::process_stream; +use rstest::rstest; + +#[macro_use] +mod common; +#[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)); +} + +#[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] +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)); +} From 26ed71f2b923f9ba4a845313b324664a744b8812 Mon Sep 17 00:00:00 2001 From: Leynos Date: Fri, 18 Jul 2025 23:51:37 +0100 Subject: [PATCH 02/11] Replace String::new with empty string in table test --- tests/table.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/table.rs b/tests/table.rs index 33ceb305..a0c46684 100644 --- a/tests/table.rs +++ b/tests/table.rs @@ -263,26 +263,26 @@ fn test_uniform_example_two() { 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); } From a05a25139dcbb67b9dd18086824bb05f8996b4c5 Mon Sep 17 00:00:00 2001 From: Leynos Date: Sat, 19 Jul 2025 01:26:44 +0100 Subject: [PATCH 03/11] Add whitespace tests and test prelude --- docs/rust-testing-with-rstest-fixtures.md | 4 +- tests/breaks.rs | 12 ++- tests/cli.rs | 20 +++- tests/common/mod.rs | 11 ++ tests/lists.rs | 120 ++++++++-------------- tests/prelude.rs | 7 ++ tests/table.rs | 10 +- tests/wrap.rs | 32 +++++- 8 files changed, 123 insertions(+), 93 deletions(-) create mode 100644 tests/prelude.rs diff --git a/docs/rust-testing-with-rstest-fixtures.md b/docs/rust-testing-with-rstest-fixtures.md index bff4240f..8c517b15 100644 --- a/docs/rust-testing-with-rstest-fixtures.md +++ b/docs/rust-testing-with-rstest-fixtures.md @@ -1356,8 +1356,8 @@ provided by `rstest`: | #[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/tests/breaks.rs b/tests/breaks.rs index ff66cea9..92e13a84 100644 --- a/tests/breaks.rs +++ b/tests/breaks.rs @@ -4,11 +4,10 @@ use std::borrow::Cow; -use assert_cmd::Command; use mdtablefix::{THEMATIC_BREAK_LEN, format_breaks}; -#[macro_use] -mod common; +mod prelude; +use prelude::*; #[test] fn test_format_breaks_basic() { @@ -49,6 +48,13 @@ fn test_format_breaks_with_tabs_and_underscores() { 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); +} + #[test] fn test_cli_breaks_option() { let output = Command::cargo_bin("mdtablefix") diff --git a/tests/cli.rs b/tests/cli.rs index e1bf8375..67f948b8 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -4,12 +4,11 @@ use std::{fs::File, io::Write}; -use assert_cmd::Command; use rstest::{fixture, rstest}; use tempfile::tempdir; -#[macro_use] -mod common; +mod prelude; +use prelude::*; #[fixture] fn broken_table() -> Vec { @@ -108,3 +107,18 @@ fn test_cli_ellipsis_long_sequence() { assert!(output.status.success()); assert_eq!(String::from_utf8_lossy(&output.stdout), "wait….\n"); } + +#[test] +fn test_cli_ellipsis_multiple_sequences() { + let output = Command::cargo_bin("mdtablefix") + .unwrap() + .arg("--ellipsis") + .write_stdin("First... then second... done.\n") + .output() + .unwrap(); + 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 42da8c37..d59d4bc5 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -5,6 +5,7 @@ /// This macro is primarily used in tests to reduce boilerplate when /// constructing example tables or other collections of lines. #[allow(unused_macros)] +#[macro_export] macro_rules! lines_vec { ($($line:expr),* $(,)?) => { vec![$($line.to_string()),*] @@ -18,6 +19,7 @@ macro_rules! lines_vec { /// let input: Vec = include_lines!("data/bold_header_input.txt"); /// ``` #[allow(unused_macros)] +#[macro_export] macro_rules! include_lines { ($path:literal $(,)?) => {{ const _TXT: &str = include_str!($path); @@ -29,6 +31,11 @@ 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. #[allow(dead_code)] pub fn assert_wrapped_list_item(output: &[String], prefix: &str, expected: usize) { assert!(expected > 0, "expected line count must be positive"); @@ -70,6 +77,10 @@ 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. #[allow(dead_code)] pub fn assert_wrapped_blockquote(output: &[String], prefix: &str, expected: usize) { assert!(!output.is_empty(), "output slice is empty"); diff --git a/tests/lists.rs b/tests/lists.rs index 179f2ab3..238dc724 100644 --- a/tests/lists.rs +++ b/tests/lists.rs @@ -2,9 +2,9 @@ use mdtablefix::{lists::pop_counters_upto, renumber_lists}; -#[macro_use] -mod common; -use assert_cmd::Command; +mod prelude; +use prelude::*; +use rstest::rstest; #[test] fn pop_counters_removes_deeper_levels() { @@ -61,20 +61,6 @@ fn restart_after_formatting_paragraph() { let expected = lines_vec!("1. Start", "", "**Bold intro**", "", "1. Next"); assert_eq!(renumber_lists(&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") @@ -88,68 +74,42 @@ fn test_cli_renumber_option() { 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(); +#[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.rs b/tests/prelude.rs new file mode 100644 index 00000000..c149ac83 --- /dev/null +++ b/tests/prelude.rs @@ -0,0 +1,7 @@ +//! Common imports for integration tests. + +#[allow(unused_imports)] +pub use assert_cmd::Command; + +#[path = "common/mod.rs"] +pub mod common; diff --git a/tests/table.rs b/tests/table.rs index a0c46684..66619c35 100644 --- a/tests/table.rs +++ b/tests/table.rs @@ -6,8 +6,7 @@ use mdtablefix::{convert_html_tables, process_stream, reflow_table}; use rstest::{fixture, rstest}; -#[macro_use] -mod common; +mod prelude; #[fixture] fn broken_table() -> Vec { return lines_vec!["| A | B | |", "| 1 | 2 | | 3 | 4 |"]; } @@ -287,6 +286,13 @@ fn test_non_table_lines_unchanged() { 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, diff --git a/tests/wrap.rs b/tests/wrap.rs index 1f186e99..fb74c387 100644 --- a/tests/wrap.rs +++ b/tests/wrap.rs @@ -3,12 +3,11 @@ //! Covers paragraphs, list items, blockquotes and footnotes, //! including the `--wrap` CLI option. -use assert_cmd::Command; use mdtablefix::process_stream; use rstest::rstest; -#[macro_use] -mod common; +mod prelude; +use prelude::*; #[test] fn test_wrap_paragraph() { @@ -21,6 +20,17 @@ fn test_wrap_paragraph() { 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); +} + #[test] fn test_wrap_list_item() { let input = lines_vec![ @@ -216,6 +226,22 @@ fn test_wrap_blockquote_nested() { assert_eq!(joined, input[0].trim_start_matches("> > ")); } +#[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); + common::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")); +} + #[test] fn test_wrap_blockquote_with_blank_lines() { let input = lines_vec![ From f8abfd1bcb86b61e116629f0cb35571a7aafe53e Mon Sep 17 00:00:00 2001 From: Leynos Date: Sat, 19 Jul 2025 02:01:08 +0100 Subject: [PATCH 04/11] Refine test modules per review --- tests/breaks.rs | 8 ++++-- tests/cli.rs | 59 ++++++++++++++++++++++++++++++--------------- tests/common/mod.rs | 8 +++--- tests/lists.rs | 4 +-- tests/prelude.rs | 2 +- tests/table.rs | 18 ++++++++++---- tests/wrap.rs | 8 ++++-- 7 files changed, 71 insertions(+), 36 deletions(-) diff --git a/tests/breaks.rs b/tests/breaks.rs index 92e13a84..52707046 100644 --- a/tests/breaks.rs +++ b/tests/breaks.rs @@ -55,14 +55,18 @@ fn test_format_breaks_mixed_chars_excessive_length() { 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") - .unwrap() + .expect("Failed to create cargo command for mdtablefix") .arg("--breaks") .write_stdin("---\n") .output() - .unwrap(); + .expect("Failed to execute mdtablefix command"); assert!(output.status.success()); assert_eq!( String::from_utf8_lossy(&output.stdout), diff --git a/tests/cli.rs b/tests/cli.rs index 67f948b8..882a7251 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -1,6 +1,10 @@ -//! Integration tests for remaining CLI behaviour. +//! Integration tests for CLI interface behaviour of the `mdtablefix` tool. //! -//! Covers file handling with `--in-place` and ellipsis replacement. +//! 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}; @@ -18,62 +22,68 @@ fn broken_table() -> Vec { ] } -#[test] /// 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") - .unwrap() + .expect("Failed to create cargo command for mdtablefix") .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. +#[rstest] fn test_cli_process_file(broken_table: Vec) { - let dir = tempdir().unwrap(); + let dir = tempdir().expect("failed to create temporary directory"); let file_path = dir.path().join("sample.md"); - let mut f = File::create(&file_path).unwrap(); + let mut f = File::create(&file_path).expect("failed to create temporary file"); for line in &broken_table { - writeln!(f, "{line}").unwrap(); + writeln!(f, "{line}").expect("failed to write line"); } - f.flush().unwrap(); + f.flush().expect("failed to flush file"); drop(f); Command::cargo_bin("mdtablefix") - .unwrap() + .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") - .unwrap() + .expect("Failed to create cargo command for mdtablefix") .arg("--ellipsis") .write_stdin("foo...\n") .output() - .unwrap(); + .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") - .unwrap() + .expect("Failed to create cargo command for mdtablefix") .arg("--ellipsis") .write_stdin("before `dots...` after\n") .output() - .unwrap(); + .expect("Failed to execute mdtablefix command"); assert!(output.status.success()); assert_eq!( String::from_utf8_lossy(&output.stdout), @@ -81,14 +91,17 @@ fn test_cli_ellipsis_code_span() { ); } +/// 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") - .unwrap() + .expect("Failed to create cargo command for mdtablefix") .arg("--ellipsis") .write_stdin("```\nlet x = ...;\n```\n") .output() - .unwrap(); + .expect("Failed to execute mdtablefix command"); assert!(output.status.success()); assert_eq!( String::from_utf8_lossy(&output.stdout), @@ -96,26 +109,32 @@ fn test_cli_ellipsis_fenced_block() { ); } +/// 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") - .unwrap() + .expect("Failed to create cargo command for mdtablefix") .arg("--ellipsis") .write_stdin("wait....\n") .output() - .unwrap(); + .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") - .unwrap() + .expect("Failed to create cargo command for mdtablefix") .arg("--ellipsis") .write_stdin("First... then second... done.\n") .output() - .unwrap(); + .expect("Failed to execute mdtablefix command"); assert!(output.status.success()); assert_eq!( String::from_utf8_lossy(&output.stdout), diff --git a/tests/common/mod.rs b/tests/common/mod.rs index d59d4bc5..a9b55d4c 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -4,7 +4,7 @@ /// /// This macro is primarily used in tests to reduce boilerplate when /// constructing example tables or other collections of lines. -#[allow(unused_macros)] +#[allow(unused_macros)] // macros are optional helpers across modules #[macro_export] macro_rules! lines_vec { ($($line:expr),* $(,)?) => { @@ -18,7 +18,7 @@ macro_rules! lines_vec { /// ``` /// let input: Vec = include_lines!("data/bold_header_input.txt"); /// ``` -#[allow(unused_macros)] +#[allow(unused_macros)] // macros are optional helpers across modules #[macro_export] macro_rules! include_lines { ($path:literal $(,)?) => {{ @@ -36,7 +36,7 @@ macro_rules! include_lines { /// /// Panics if the output slice is empty, expected count is zero, or if the lines /// do not meet the asserted conditions. -#[allow(dead_code)] +#[allow(dead_code)] // 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"); @@ -81,7 +81,7 @@ pub fn assert_wrapped_list_item(output: &[String], prefix: &str, expected: usize /// # Panics /// /// Panics if the output slice is empty or the prefix is missing from any line. -#[allow(dead_code)] +#[allow(dead_code)] // 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); diff --git a/tests/lists.rs b/tests/lists.rs index 238dc724..81a34be6 100644 --- a/tests/lists.rs +++ b/tests/lists.rs @@ -64,11 +64,11 @@ fn restart_after_formatting_paragraph() { #[test] fn test_cli_renumber_option() { let output = Command::cargo_bin("mdtablefix") - .unwrap() + .expect("Failed to create cargo command for mdtablefix") .arg("--renumber") .write_stdin("1. a\n4. b\n") .output() - .unwrap(); + .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"); diff --git a/tests/prelude.rs b/tests/prelude.rs index c149ac83..37fbfab7 100644 --- a/tests/prelude.rs +++ b/tests/prelude.rs @@ -1,6 +1,6 @@ //! Common imports for integration tests. -#[allow(unused_imports)] +#[allow(unused_imports)] // re-exporting for test modules pub use assert_cmd::Command; #[path = "common/mod.rs"] diff --git a/tests/table.rs b/tests/table.rs index 66619c35..21f448ab 100644 --- a/tests/table.rs +++ b/tests/table.rs @@ -9,10 +9,14 @@ use rstest::{fixture, rstest}; mod prelude; #[fixture] -fn broken_table() -> Vec { return lines_vec!["| A | B | |", "| 1 | 2 | | 3 | 4 |"]; } +fn broken_table() -> Vec { + lines_vec!["| A | B | |", "| 1 | 2 | | 3 | 4 |"] +} #[fixture] -fn malformed_table() -> Vec { return lines_vec!["| A | |", "| 1 | 2 | 3 |"]; } +fn malformed_table() -> Vec { + lines_vec!["| A | |", "| 1 | 2 | 3 |"] +} #[fixture] fn header_table() -> Vec { @@ -26,7 +30,7 @@ fn escaped_pipe_table() -> Vec { #[fixture] fn indented_table() -> Vec { - return lines_vec![" | I | J | |", " | 1 | 2 | | 3 | 4 |"]; + lines_vec![" | I | J | |", " | 1 | 2 | | 3 | 4 |"] } #[fixture] @@ -100,10 +104,14 @@ fn html_table_inconsistent_first_row() -> Vec { } #[fixture] -fn html_table_empty() -> Vec { return lines_vec!["
"]; } +fn html_table_empty() -> Vec { + lines_vec!["
"] +} #[fixture] -fn html_table_unclosed() -> Vec { return lines_vec!["", ""]; } +fn html_table_unclosed() -> Vec { + lines_vec!["
1
", ""] +} #[fixture] fn html_table_uppercase() -> Vec { diff --git a/tests/wrap.rs b/tests/wrap.rs index fb74c387..6549b5f8 100644 --- a/tests/wrap.rs +++ b/tests/wrap.rs @@ -328,17 +328,21 @@ fn test_wrap_hard_linebreak_backslash_edge_cases() { 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") - .unwrap() + .expect("Failed to create cargo command for mdtablefix") .arg("--wrap") .write_stdin(format!("{input}\n")) .output() - .unwrap(); + .expect("Failed to execute mdtablefix command"); assert!(output.status.success()); let text = String::from_utf8_lossy(&output.stdout); assert!( From 7913237689beccceae8b65ab2d07b38a94c1b35a Mon Sep 17 00:00:00 2001 From: Leynos Date: Sat, 19 Jul 2025 02:19:04 +0100 Subject: [PATCH 05/11] Refine table fixtures --- tests/table.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/table.rs b/tests/table.rs index 21f448ab..8697196b 100644 --- a/tests/table.rs +++ b/tests/table.rs @@ -10,12 +10,14 @@ mod prelude; #[fixture] fn broken_table() -> Vec { - lines_vec!["| A | B | |", "| 1 | 2 | | 3 | 4 |"] + let lines = lines_vec!["| A | B | |", "| 1 | 2 | | 3 | 4 |"]; + lines } #[fixture] fn malformed_table() -> Vec { - lines_vec!["| A | |", "| 1 | 2 | 3 |"] + let lines = lines_vec!["| A | |", "| 1 | 2 | 3 |"]; + lines } #[fixture] @@ -30,7 +32,8 @@ fn escaped_pipe_table() -> Vec { #[fixture] fn indented_table() -> Vec { - lines_vec![" | I | J | |", " | 1 | 2 | | 3 | 4 |"] + let lines = lines_vec![" | I | J | |", " | 1 | 2 | | 3 | 4 |"]; + lines } #[fixture] @@ -105,12 +108,14 @@ fn html_table_inconsistent_first_row() -> Vec { #[fixture] fn html_table_empty() -> Vec { - lines_vec!["
1
"] + let lines = lines_vec!["
"]; + lines } #[fixture] fn html_table_unclosed() -> Vec { - lines_vec!["", ""] + let lines = lines_vec!["
1
", ""]; + lines } #[fixture] From ae2f17b85819abda5c22c37dbb6a392548c9c749 Mon Sep 17 00:00:00 2001 From: Leynos Date: Sat, 19 Jul 2025 03:03:50 +0100 Subject: [PATCH 06/11] Refine prelude module and wrap docs --- src/main.rs | 5 ++++- tests/{prelude.rs => prelude/mod.rs} | 2 +- tests/wrap.rs | 23 +++++++++++++++++++---- 3 files changed, 24 insertions(+), 6 deletions(-) rename tests/{prelude.rs => prelude/mod.rs} (83%) 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/prelude.rs b/tests/prelude/mod.rs similarity index 83% rename from tests/prelude.rs rename to tests/prelude/mod.rs index 37fbfab7..2fc96751 100644 --- a/tests/prelude.rs +++ b/tests/prelude/mod.rs @@ -3,5 +3,5 @@ #[allow(unused_imports)] // re-exporting for test modules pub use assert_cmd::Command; -#[path = "common/mod.rs"] +#[path = "../common/mod.rs"] pub mod common; diff --git a/tests/wrap.rs b/tests/wrap.rs index 6549b5f8..4f90aa1a 100644 --- a/tests/wrap.rs +++ b/tests/wrap.rs @@ -1,7 +1,14 @@ -//! Integration tests for wrapping behaviour. +//! Integration tests for text wrapping behaviour in Markdown content. //! -//! Covers paragraphs, list items, blockquotes and footnotes, -//! including the `--wrap` CLI option. +//! 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; use rstest::rstest; @@ -66,6 +73,10 @@ fn test_wrap_preserves_inline_code_spans() { common::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![ @@ -98,7 +109,7 @@ fn test_wrap_long_inline_code_item() { assert!( output .first() - .expect("output should not be empty") + .expect("wrapped output should contain at least one line") .ends_with("`:") ); } @@ -210,6 +221,10 @@ fn test_wrap_blockquote() { ); } +/// 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!( From daca755201d3f394f76291d8ed9a16c93378ed0e Mon Sep 17 00:00:00 2001 From: Leynos Date: Sat, 19 Jul 2025 09:44:06 +0100 Subject: [PATCH 07/11] Centralise fixtures and re-export macros --- tests/breaks.rs | 1 + tests/cli.rs | 10 +--------- tests/common/mod.rs | 14 ++++++++++++-- tests/lists.rs | 2 +- tests/prelude/mod.rs | 7 ++++++- tests/table.rs | 9 ++------- tests/wrap.rs | 28 ++++++++++++++-------------- 7 files changed, 37 insertions(+), 34 deletions(-) diff --git a/tests/breaks.rs b/tests/breaks.rs index 52707046..bcfe9742 100644 --- a/tests/breaks.rs +++ b/tests/breaks.rs @@ -6,6 +6,7 @@ use std::borrow::Cow; use mdtablefix::{THEMATIC_BREAK_LEN, format_breaks}; +#[macro_use] mod prelude; use prelude::*; diff --git a/tests/cli.rs b/tests/cli.rs index 882a7251..1aafda4f 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -8,20 +8,12 @@ use std::{fs::File, io::Write}; -use rstest::{fixture, rstest}; use tempfile::tempdir; +#[macro_use] mod prelude; use prelude::*; -#[fixture] -fn broken_table() -> Vec { - vec![ - "| A | B | |".to_string(), - "| 1 | 2 | | 3 | 4 |".to_string(), - ] -} - /// 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 diff --git a/tests/common/mod.rs b/tests/common/mod.rs index a9b55d4c..873d522c 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1,11 +1,12 @@ //! Utility helpers shared across integration tests. +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. #[allow(unused_macros)] // macros are optional helpers across modules -#[macro_export] macro_rules! lines_vec { ($($line:expr),* $(,)?) => { vec![$($line.to_string()),*] @@ -19,7 +20,6 @@ macro_rules! lines_vec { /// let input: Vec = include_lines!("data/bold_header_input.txt"); /// ``` #[allow(unused_macros)] // macros are optional helpers across modules -#[macro_export] macro_rules! include_lines { ($path:literal $(,)?) => {{ const _TXT: &str = include_str!($path); @@ -88,3 +88,13 @@ pub fn assert_wrapped_blockquote(output: &[String], prefix: &str, expected: usiz assert!(output.iter().all(|l| l.starts_with(prefix))); assert!(output.iter().all(|l| l.len() <= 80)); } + +/// Fixture representing a simple broken table. +#[allow(dead_code)] +#[fixture] +pub fn broken_table() -> Vec { + vec![ + "| A | B | |".to_string(), + "| 1 | 2 | | 3 | 4 |".to_string(), + ] +} diff --git a/tests/lists.rs b/tests/lists.rs index 81a34be6..d2a3dd01 100644 --- a/tests/lists.rs +++ b/tests/lists.rs @@ -2,9 +2,9 @@ use mdtablefix::{lists::pop_counters_upto, renumber_lists}; +#[macro_use] mod prelude; use prelude::*; -use rstest::rstest; #[test] fn pop_counters_removes_deeper_levels() { diff --git a/tests/prelude/mod.rs b/tests/prelude/mod.rs index 2fc96751..e60a83f2 100644 --- a/tests/prelude/mod.rs +++ b/tests/prelude/mod.rs @@ -2,6 +2,11 @@ #[allow(unused_imports)] // re-exporting for test modules pub use assert_cmd::Command; +#[allow(unused_imports)] +pub use rstest::{fixture, rstest}; +#[macro_use] #[path = "../common/mod.rs"] -pub mod common; +mod common; +#[allow(unused_imports)] +pub use common::*; diff --git a/tests/table.rs b/tests/table.rs index 8697196b..6660d059 100644 --- a/tests/table.rs +++ b/tests/table.rs @@ -4,15 +4,10 @@ //! `process_stream` behaviour. use mdtablefix::{convert_html_tables, process_stream, reflow_table}; -use rstest::{fixture, rstest}; +#[macro_use] mod prelude; - -#[fixture] -fn broken_table() -> Vec { - let lines = lines_vec!["| A | B | |", "| 1 | 2 | | 3 | 4 |"]; - lines -} +use prelude::*; #[fixture] fn malformed_table() -> Vec { diff --git a/tests/wrap.rs b/tests/wrap.rs index 4f90aa1a..68fa8a1f 100644 --- a/tests/wrap.rs +++ b/tests/wrap.rs @@ -11,8 +11,8 @@ //! - CLI `--wrap` option functionality use mdtablefix::process_stream; -use rstest::rstest; +#[macro_use] mod prelude; use prelude::*; #[test] @@ -44,7 +44,7 @@ fn test_wrap_list_item() { 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); + assert_wrapped_list_item(&output, "- ", 2); } #[rstest] @@ -59,7 +59,7 @@ fn test_wrap_list_items_with_inline_code(#[case] prefix: &str, #[case] expected: the script verbatim, respecting the shebang." )]; let output = process_stream(&input); - common::assert_wrapped_list_item(&output, prefix, expected); + assert_wrapped_list_item(&output, prefix, expected); } #[test] @@ -70,7 +70,7 @@ fn test_wrap_preserves_inline_code_spans() { script verbatim, respecting the shebang.", ]; let output = process_stream(&input); - common::assert_wrapped_list_item(&output, "- ", 3); + assert_wrapped_list_item(&output, "- ", 3); } /// Tests that multi-backtick code spans are preserved during wrapping. @@ -84,7 +84,7 @@ fn test_wrap_multi_backtick_code() { console", ]; let output = process_stream(&input); - common::assert_wrapped_list_item(&output, "- ", 2); + assert_wrapped_list_item(&output, "- ", 2); } #[test] @@ -94,7 +94,7 @@ fn test_wrap_multiple_inline_code_spans() { wrapping width", ]; let output = process_stream(&input); - common::assert_wrapped_list_item(&output, "- ", 2); + assert_wrapped_list_item(&output, "- ", 2); } #[test] fn test_wrap_long_inline_code_item() { @@ -105,7 +105,7 @@ fn test_wrap_long_inline_code_item() { " don't conform to the expected structured format." )]; let output = process_stream(&input); - common::assert_wrapped_list_item(&output, "- ", 4); + assert_wrapped_list_item(&output, "- ", 4); assert!( output .first() @@ -143,7 +143,7 @@ fn test_wrap_footnote_multiline() { "across multiple lines so we can verify indentation." )]; let output = process_stream(&input); - common::assert_wrapped_list_item(&output, "[^note]: ", 2); + assert_wrapped_list_item(&output, "[^note]: ", 2); } #[test] @@ -153,7 +153,7 @@ fn test_wrap_footnote_with_inline_code() { "across multiple lines without breaking the span." )]; let output = process_stream(&input); - common::assert_wrapped_list_item(&output, " [^code_note]: ", 2); + assert_wrapped_list_item(&output, " [^code_note]: ", 2); } /// Tests that footnotes with angle-bracketed URLs are wrapped correctly. @@ -232,7 +232,7 @@ fn test_wrap_blockquote_nested() { "can verify multi-level handling." )]; let output = process_stream(&input); - common::assert_wrapped_blockquote(&output, "> > ", 2); + assert_wrapped_blockquote(&output, "> > ", 2); let joined = output .iter() .map(|l| l.trim_start_matches("> > ")) @@ -248,7 +248,7 @@ fn test_wrap_blockquote_mixed_indentation() { indentation handling." ]; let output = process_stream(&input); - common::assert_wrapped_blockquote(&output, "> \t> \t", 2); + assert_wrapped_blockquote(&output, "> \t> \t", 2); let joined = output .iter() .map(|l| l.trim_start_matches("> \t> \t")) @@ -274,8 +274,8 @@ fn test_wrap_blockquote_with_blank_lines() { ]; let output = process_stream(&input); assert_eq!(output[3], ">"); - common::assert_wrapped_blockquote(&output[..3], "> ", 3); - common::assert_wrapped_blockquote(&output[4..], "> ", 3); + assert_wrapped_blockquote(&output[..3], "> ", 3); + assert_wrapped_blockquote(&output[4..], "> ", 3); } #[test] @@ -285,7 +285,7 @@ fn test_wrap_blockquote_extra_whitespace() { the line width.", ]; let output = process_stream(&input); - common::assert_wrapped_blockquote(&output, "> ", 2); + assert_wrapped_blockquote(&output, "> ", 2); let joined = output .iter() .map(|l| l.trim_start_matches("> ")) From 2269ad512d4a5a1c180292cc010011753fa77f7c Mon Sep 17 00:00:00 2001 From: Leynos Date: Sat, 19 Jul 2025 09:59:18 +0100 Subject: [PATCH 08/11] Document wrap tests and adjust lint expects --- tests/common/mod.rs | 11 ++++++----- tests/lists.rs | 3 +++ tests/wrap.rs | 9 ++++++++- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 873d522c..f86905be 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1,4 +1,5 @@ //! Utility helpers shared across integration tests. +#![allow(unfulfilled_lint_expectations)] use rstest::fixture; @@ -6,7 +7,7 @@ use rstest::fixture; /// /// This macro is primarily used in tests to reduce boilerplate when /// constructing example tables or other collections of lines. -#[allow(unused_macros)] // macros are optional helpers across modules +#[expect(unused_macros, reason = "macros are optional helpers across modules")] macro_rules! lines_vec { ($($line:expr),* $(,)?) => { vec![$($line.to_string()),*] @@ -19,7 +20,7 @@ macro_rules! lines_vec { /// ``` /// let input: Vec = include_lines!("data/bold_header_input.txt"); /// ``` -#[allow(unused_macros)] // macros are optional helpers across modules +#[expect(unused_macros, reason = "macros are optional helpers across modules")] macro_rules! include_lines { ($path:literal $(,)?) => {{ const _TXT: &str = include_str!($path); @@ -36,7 +37,7 @@ macro_rules! include_lines { /// /// Panics if the output slice is empty, expected count is zero, or if the lines /// do not meet the asserted conditions. -#[allow(dead_code)] // helper used selectively across modules +#[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"); @@ -81,7 +82,7 @@ pub fn assert_wrapped_list_item(output: &[String], prefix: &str, expected: usize /// # Panics /// /// Panics if the output slice is empty or the prefix is missing from any line. -#[allow(dead_code)] // helper used selectively across modules +#[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); @@ -90,7 +91,7 @@ pub fn assert_wrapped_blockquote(output: &[String], prefix: &str, expected: usiz } /// Fixture representing a simple broken table. -#[allow(dead_code)] +#[expect(dead_code, reason = "helper used selectively across modules")] #[fixture] pub fn broken_table() -> Vec { vec![ diff --git a/tests/lists.rs b/tests/lists.rs index d2a3dd01..13d8e037 100644 --- a/tests/lists.rs +++ b/tests/lists.rs @@ -61,6 +61,9 @@ 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") diff --git a/tests/wrap.rs b/tests/wrap.rs index 68fa8a1f..eaa972d9 100644 --- a/tests/wrap.rs +++ b/tests/wrap.rs @@ -15,8 +15,11 @@ 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 \ @@ -38,6 +41,10 @@ fn test_wrap_paragraph_with_long_word() { 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![ From 4bb5642ade9b454e63d076a37dece28bc5e1b892 Mon Sep 17 00:00:00 2001 From: Leynos Date: Sat, 19 Jul 2025 10:18:18 +0100 Subject: [PATCH 09/11] Address review feedback --- tests/prelude/mod.rs | 7 ++++--- tests/wrap.rs | 12 ++++++++++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/tests/prelude/mod.rs b/tests/prelude/mod.rs index e60a83f2..32f7534b 100644 --- a/tests/prelude/mod.rs +++ b/tests/prelude/mod.rs @@ -1,12 +1,13 @@ //! Common imports for integration tests. +#![allow(unfulfilled_lint_expectations)] -#[allow(unused_imports)] // re-exporting for test modules +#[expect(unused_imports, reason = "re-exporting common test utilities")] pub use assert_cmd::Command; -#[allow(unused_imports)] +#[expect(unused_imports, reason = "re-exporting common test utilities")] pub use rstest::{fixture, rstest}; #[macro_use] #[path = "../common/mod.rs"] mod common; -#[allow(unused_imports)] +#[expect(unused_imports, reason = "re-exporting common test utilities")] pub use common::*; diff --git a/tests/wrap.rs b/tests/wrap.rs index eaa972d9..3467c2b5 100644 --- a/tests/wrap.rs +++ b/tests/wrap.rs @@ -202,10 +202,10 @@ fn test_wrap_footnote_collection() { 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. +#[test] fn test_wrap_short_list_item() { let input = lines_vec!["- short item"]; let output = process_stream(&input); @@ -264,6 +264,10 @@ fn test_wrap_blockquote_mixed_indentation() { 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![ @@ -285,6 +289,10 @@ fn test_wrap_blockquote_with_blank_lines() { 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![ @@ -308,11 +316,11 @@ fn test_wrap_blockquote_short() { 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. +#[test] fn test_preserve_hard_line_breaks() { let input = lines_vec!["Line one with break. ", "Line two follows."]; let output = process_stream(&input); From 8a52a67a6b5b11e70f94b29169f177aa9df7c42c Mon Sep 17 00:00:00 2001 From: Leynos Date: Sat, 19 Jul 2025 10:58:50 +0100 Subject: [PATCH 10/11] Add missing doc comments to wrap tests --- tests/wrap.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/wrap.rs b/tests/wrap.rs index 3467c2b5..560ac27e 100644 --- a/tests/wrap.rs +++ b/tests/wrap.rs @@ -121,6 +121,10 @@ fn test_wrap_long_inline_code_item() { ); } +/// 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 = vec![ @@ -143,6 +147,10 @@ fn test_wrap_future_attribute_punctuation() { ); } +/// 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!( @@ -212,6 +220,10 @@ fn test_wrap_short_list_item() { 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![ From 7fb10727304b5cfc6995a6e425fa1e3de4eb8737 Mon Sep 17 00:00:00 2001 From: Leynos Date: Sat, 19 Jul 2025 11:42:49 +0100 Subject: [PATCH 11/11] Document wrap tests and unify inputs --- tests/wrap.rs | 47 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/tests/wrap.rs b/tests/wrap.rs index 560ac27e..594b9ade 100644 --- a/tests/wrap.rs +++ b/tests/wrap.rs @@ -54,6 +54,10 @@ fn test_wrap_list_item() { 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)] @@ -69,6 +73,10 @@ fn test_wrap_list_items_with_inline_code(#[case] prefix: &str, #[case] expected: 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![ @@ -94,6 +102,10 @@ fn test_wrap_multi_backtick_code() { 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![ @@ -103,6 +115,10 @@ fn test_wrap_multiple_inline_code_spans() { 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!( @@ -127,14 +143,11 @@ fn test_wrap_long_inline_code_item() { /// `#[future]` are wrapped correctly without splitting the punctuation. #[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 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, @@ -161,6 +174,9 @@ fn test_wrap_footnote_multiline() { 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!( @@ -260,6 +276,10 @@ fn test_wrap_blockquote_nested() { 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![ @@ -321,6 +341,9 @@ fn test_wrap_blockquote_extra_whitespace() { 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"]; @@ -341,6 +364,10 @@ fn test_preserve_hard_line_breaks() { 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"); @@ -348,6 +375,10 @@ fn test_wrap_hard_linebreak_backslash() { 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!(
1