Skip to content
16 changes: 8 additions & 8 deletions docs/rust-testing-with-rstest-fixtures.md
Original file line number Diff line number Diff line change
Expand Up @@ -1345,19 +1345,19 @@ provided by `rstest`:
| ---------------------------- | -------------------------------------------------------------------------------------------- |
| #[rstest] | Marks a function as an rstest test; enables fixture injection and parameterization. |
| #[fixture] | Defines a function that provides a test fixture (setup data or services). |
| #[case(...)] | Defines a single parameterized test case with specific input values. |
| #[values(...)] | Defines a list of values for an argument, generating tests for each value or combination. |
| #[case(…)] | Defines a single parameterized test case with specific input values. |
| #[values(…)] | Defines a list of values for an argument, generating tests for each value or combination. |
| #[once] | Marks a fixture to be initialized only once and shared (as a static reference) across tests. |
| #[future] | Simplifies async argument types by removing impl Future boilerplate. |
| #[awt] | (Function or argument level) Automatically .awaits future arguments in async tests. |
| #[from(original_name)] | Allows renaming an injected fixture argument in the test function. |
| #[with(...)] | Overrides default arguments of a fixture for a specific test. |
| #[default(...)] | Provides default values for arguments within a fixture function. |
| #[timeout(...)] | Sets a timeout for an asynchronous test. |
| #[files("glob_pattern",...)] | Injects file paths (or contents, with mode=) matching a glob pattern as test arguments. |
| #[with(…)] | Overrides default arguments of a fixture for a specific test. |
| #[default(…)] | Provides default values for arguments within a fixture function. |
| #[timeout(…)] | Sets a timeout for an asynchronous test. |
| #[files("glob_pattern",…)] | Injects file paths (or contents, with mode=) matching a glob pattern as test arguments. |

By mastering `rstest`, Rust developers can significantly elevate the quality
and efficiency of their testing practices, leading to more reliable and
Mastering `rstest` can significantly elevate the quality and efficiency of
testing practices for Rust developers, leading to more reliable and
maintainable software.

#### Works cited
Expand Down
5 changes: 4 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
76 changes: 76 additions & 0 deletions tests/breaks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//! Integration tests for formatting thematic breaks.
//!
//! Verifies `format_breaks` function and `--breaks` CLI option.

use std::borrow::Cow;

use mdtablefix::{THEMATIC_BREAK_LEN, format_breaks};

#[macro_use]
mod prelude;
use prelude::*;

#[test]
fn test_format_breaks_basic() {
let input = lines_vec!["foo", "***", "bar"];
let expected: Vec<Cow<str>> = 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<Cow<str>> = 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<Cow<str>> = 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<Cow<str>> = vec![Cow::Owned("_".repeat(THEMATIC_BREAK_LEN))];
assert_eq!(format_breaks(&input), expected);
}
Comment thread
leynos marked this conversation as resolved.

#[test]
fn test_format_breaks_with_tabs_and_underscores() {
let input = lines_vec!["\t_\t_\t_\t"];
let expected: Vec<Cow<str>> = vec![Cow::Owned("_".repeat(THEMATIC_BREAK_LEN))];
assert_eq!(format_breaks(&input), expected);
}

#[test]
fn test_format_breaks_mixed_chars_excessive_length() {
let input = lines_vec!["***---___"];
let expected: Vec<Cow<str>> = input.iter().map(|s| s.as_str().into()).collect();
assert_eq!(format_breaks(&input), expected);
}

/// Tests the CLI `--breaks` option to ensure thematic breaks are normalised.
///
/// Provides a single line of hyphens and asserts the output is the standard
/// underscore-based thematic break.
#[test]
fn test_cli_breaks_option() {
let output = Command::cargo_bin("mdtablefix")
.expect("Failed to create cargo command for mdtablefix")
.arg("--breaks")
.write_stdin("---\n")
.output()
.expect("Failed to execute mdtablefix command");
assert!(output.status.success());
assert_eq!(
String::from_utf8_lossy(&output.stdout),
format!("{}\n", "_".repeat(THEMATIC_BREAK_LEN))
);
}
135 changes: 135 additions & 0 deletions tests/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
//! Integration tests for CLI interface behaviour of the `mdtablefix` tool.
//!
//! This module validates the command-line interface functionality, including:
//! - File handling with the `--in-place` flag
//! - Ellipsis replacement with the `--ellipsis` option
//! - Error handling for invalid argument combinations
//! - Processing of Markdown files through the CLI interface

use std::{fs::File, io::Write};

use tempfile::tempdir;

#[macro_use]
mod prelude;
use prelude::*;

/// Verifies that the CLI fails when the `--in-place` flag is used without specifying a file.
///
/// This test ensures that running `mdtablefix --in-place` without a file argument results in a
/// command failure.
#[test]
fn test_cli_in_place_requires_file() {
Command::cargo_bin("mdtablefix")
.expect("Failed to create cargo command for mdtablefix")
.arg("--in-place")
.assert()
.failure();
}

/// Tests that the CLI processes a file containing a broken Markdown table and outputs the corrected
/// table to stdout.
///
/// This test creates a temporary file with a malformed table, runs the `mdtablefix` binary on it,
/// and asserts that the output is the expected fixed table.
#[rstest]
fn test_cli_process_file(broken_table: Vec<String>) {
let dir = tempdir().expect("failed to create temporary directory");
let file_path = dir.path().join("sample.md");
let mut f = File::create(&file_path).expect("failed to create temporary file");
for line in &broken_table {
writeln!(f, "{line}").expect("failed to write line");
}
f.flush().expect("failed to flush file");
drop(f);
Command::cargo_bin("mdtablefix")
.expect("Failed to create cargo command for mdtablefix")
.arg(&file_path)
.assert()
.success()
.stdout("| A | B |\n| 1 | 2 |\n| 3 | 4 |\n");
}

/// Tests that the `--ellipsis` option replaces triple dots with a Unicode ellipsis character.
///
/// Verifies that the CLI correctly processes input containing "..." and outputs "…".
#[test]
fn test_cli_ellipsis_option() {
let output = Command::cargo_bin("mdtablefix")
.expect("Failed to create cargo command for mdtablefix")
.arg("--ellipsis")
.write_stdin("foo...\n")
.output()
.expect("Failed to execute mdtablefix command");
assert!(output.status.success());
assert_eq!(String::from_utf8_lossy(&output.stdout), "foo…\n");
}

/// Tests that the `--ellipsis` option preserves dots within inline code spans.
///
/// Verifies that triple dots inside backtick-delimited code spans are not converted to ellipsis.
#[test]
fn test_cli_ellipsis_code_span() {
let output = Command::cargo_bin("mdtablefix")
.expect("Failed to create cargo command for mdtablefix")
.arg("--ellipsis")
.write_stdin("before `dots...` after\n")
.output()
.expect("Failed to execute mdtablefix command");
assert!(output.status.success());
assert_eq!(
String::from_utf8_lossy(&output.stdout),
"before `dots...` after\n"
);
}

/// Tests that the `--ellipsis` option does not alter fenced code blocks.
///
/// Ensures that sequences like "..." inside a fenced code block remain unchanged.
#[test]
fn test_cli_ellipsis_fenced_block() {
let output = Command::cargo_bin("mdtablefix")
.expect("Failed to create cargo command for mdtablefix")
.arg("--ellipsis")
.write_stdin("```\nlet x = ...;\n```\n")
.output()
.expect("Failed to execute mdtablefix command");
assert!(output.status.success());
assert_eq!(
String::from_utf8_lossy(&output.stdout),
"```\nlet x = ...;\n```\n"
);
}

/// Tests ellipsis replacement for sequences longer than three characters.
///
/// Confirms that only the first three dots are replaced with an ellipsis.
#[test]
fn test_cli_ellipsis_long_sequence() {
let output = Command::cargo_bin("mdtablefix")
.expect("Failed to create cargo command for mdtablefix")
.arg("--ellipsis")
.write_stdin("wait....\n")
.output()
.expect("Failed to execute mdtablefix command");
assert!(output.status.success());
assert_eq!(String::from_utf8_lossy(&output.stdout), "wait….\n");
}

/// Tests that the `--ellipsis` option handles multiple ellipsis sequences in one line.
///
/// Verifies that all occurrences of "..." are replaced with "…".
#[test]
fn test_cli_ellipsis_multiple_sequences() {
let output = Command::cargo_bin("mdtablefix")
.expect("Failed to create cargo command for mdtablefix")
.arg("--ellipsis")
.write_stdin("First... then second... done.\n")
.output()
.expect("Failed to execute mdtablefix command");
assert!(output.status.success());
assert_eq!(
String::from_utf8_lossy(&output.stdout),
"First… then second… done.\n"
);
}
26 changes: 26 additions & 0 deletions tests/common/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
//! Utility helpers shared across integration tests.
#![allow(unfulfilled_lint_expectations)]

use rstest::fixture;

/// Build a `Vec<String>` from a list of string slices.
///
/// This macro is primarily used in tests to reduce boilerplate when
/// constructing example tables or other collections of lines.
#[expect(unused_macros, reason = "macros are optional helpers across modules")]
macro_rules! lines_vec {
($($line:expr),* $(,)?) => {
vec![$($line.to_string()),*]
Expand All @@ -16,6 +20,7 @@ macro_rules! lines_vec {
/// ```
/// let input: Vec<String> = include_lines!("data/bold_header_input.txt");
/// ```
#[expect(unused_macros, reason = "macros are optional helpers across modules")]
macro_rules! include_lines {
($path:literal $(,)?) => {{
const _TXT: &str = include_str!($path);
Expand All @@ -27,6 +32,12 @@ macro_rules! include_lines {
///
/// Verifies the number of lines, prefix on the first line, length of all lines,
/// and indentation of continuation lines.
///
/// # Panics
///
/// Panics if the output slice is empty, expected count is zero, or if the lines
/// do not meet the asserted conditions.
#[expect(dead_code, reason = "helper used selectively across modules")]
pub fn assert_wrapped_list_item(output: &[String], prefix: &str, expected: usize) {
assert!(expected > 0, "expected line count must be positive");
assert!(!output.is_empty(), "output slice is empty");
Expand Down Expand Up @@ -67,9 +78,24 @@ pub fn assert_wrapped_list_item(output: &[String], prefix: &str, expected: usize

/// Assert that every line in a blockquote starts with the given prefix and is at most 80
/// characters.
///
/// # Panics
///
/// Panics if the output slice is empty or the prefix is missing from any line.
#[expect(dead_code, reason = "helper used selectively across modules")]
pub fn assert_wrapped_blockquote(output: &[String], prefix: &str, expected: usize) {
assert!(!output.is_empty(), "output slice is empty");
assert_eq!(output.len(), expected);
assert!(output.iter().all(|l| l.starts_with(prefix)));
assert!(output.iter().all(|l| l.len() <= 80));
}

/// Fixture representing a simple broken table.
#[expect(dead_code, reason = "helper used selectively across modules")]
#[fixture]
pub fn broken_table() -> Vec<String> {
vec![
"| A | B | |".to_string(),
"| 1 | 2 | | 3 | 4 |".to_string(),
]
}
Loading