Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions examples/multiline_test_docstrings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""
Examples of multi-line test docstrings.

This file demonstrates the new feature that allows test docstrings to have
multi-line descriptions for arrange/act/assert or given/when/then patterns
using indentation-based continuation.
"""


def test_arrange_act_assert_pattern():
"""
arrange: This is a very important part of a very important test so
the arrange sentence has to be loooong and span multiple lines
to properly describe the complex setup.
act: Do the test.
assert: It better not fail 😠.
"""
# Test implementation here
pass


def test_given_when_then_pattern():
"""
given: A complex setup scenario that requires multiple lines
to properly explain all the preconditions and
initial state of the system under test.
when: The user performs an action.
then: The system should respond appropriately with
the expected behavior and state changes.
"""
# Test implementation here
pass


def test_multiline_continuation():
"""
arrange: First we need to set up the database with
initial data that includes users and
their associated permissions and
various configuration settings that
are necessary for this particular test scenario.
act: Execute the migration script.
assert: All tables should be updated correctly and
all constraints should be in place.
"""
# Test implementation here
pass


def function_with_multiline_args(param1, param2, param3):
"""Function demonstrating multiline Args section.

Args:
param1: This is a very long parameter description that needs to
span multiple lines because it describes something complex
and important about the parameter usage.
param2: Short description.
param3: Another long description that also
needs multiple lines to explain properly.

Returns:
A result value.
"""
return param1 + param2 + param3
31 changes: 23 additions & 8 deletions src/docstring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ impl _Section {
subs: subsections,
}
}

#[getter]
fn name(&self) -> Option<String> {
self.name.clone()
}

#[getter]
fn subs(&self) -> Vec<String> {
self.subs.clone()
}

fn __eq__(&self, other: &Self) -> PyResult<bool> {
let mut self_subs = self.subs.clone();
let mut other_subs = other.subs.clone();
Expand Down Expand Up @@ -312,14 +323,18 @@ pub fn _get_sections(lines: Vec<String>) -> Vec<_Section> {
section_lines.push(lines.next().unwrap());
}

let subs = section_lines
.iter()
.filter_map(|line| {
_SUB_SECTION_PATTERN
.captures(line)
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
})
.collect();
// Extract subsections, handling multi-line descriptions
let mut subs: Vec<String> = Vec::new();
for line in section_lines.iter() {
// Check if this line starts a new subsection
if let Some(caps) = _SUB_SECTION_PATTERN.captures(line) {
if let Some(sub_name) = caps.get(1).map(|m| m.as_str().to_string()) {
subs.push(sub_name);
}
}
// Otherwise, it's a continuation line for the previous subsection - we just skip it
// (the description is not stored, only the subsection names)
}

sections.push(_Section {
name: section_name,
Expand Down
92 changes: 92 additions & 0 deletions src/docstring/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -627,3 +627,95 @@ fn parse_extracts_expected_sections() {
);
}
}

#[test]
fn test_multiline_subsections() {
let lines = vec![
"arrange: This is a very important part so".to_string(),
" the arrange sentence has to be loooong.".to_string(),
"act: Do the test.".to_string(),
"assert: It better not fail.".to_string(),
];

let sections = _get_sections(lines);

// Print for debugging
for section in &sections {
eprintln!("Section: {:?}", section);
}

// The first line "arrange: ..." is treated as a section (because it's first line and matches pattern)
// The continuation line should not create a new subsection
// "act:" and "assert:" should be subsections under "arrange:"
assert_eq!(sections.len(), 1, "Expected 1 section");
assert_eq!(sections[0].name, Some("arrange".to_string()), "Expected section name to be 'arrange'");
assert_eq!(sections[0].subs.len(), 2, "Expected 2 subsections, got: {:?}", sections[0].subs);
assert!(sections[0].subs.contains(&"act".to_string()));
assert!(sections[0].subs.contains(&"assert".to_string()));
}

#[test]
fn test_multiline_subsections_in_named_section() {
// Test when subsections with multi-line descriptions are within a named section
let lines = vec![
"Args:".to_string(),
" arg1: This is a very long description that needs to".to_string(),
" span multiple lines because it's important.".to_string(),
" arg2: Short description.".to_string(),
" arg3: Another long description that also".to_string(),
" needs multiple lines.".to_string(),
];

let sections = _get_sections(lines);

eprintln!("Sections: {:?}", sections);

assert_eq!(sections.len(), 1);
assert_eq!(sections[0].name, Some("Args".to_string()));
assert_eq!(sections[0].subs.len(), 3, "Expected 3 args, got: {:?}", sections[0].subs);
assert!(sections[0].subs.contains(&"arg1".to_string()));
assert!(sections[0].subs.contains(&"arg2".to_string()));
assert!(sections[0].subs.contains(&"arg3".to_string()));
}

#[test]
fn test_multiline_subsections_given_when_then() {
// Test the given/when/then pattern common in tests
let lines = vec![
"given: A test setup with multiple components that require".to_string(),
" detailed explanation across lines.".to_string(),
"when: The action is performed.".to_string(),
"then: The expected outcome should be this specific thing that".to_string(),
" also needs a detailed explanation.".to_string(),
];

let sections = _get_sections(lines);

eprintln!("Sections: {:?}", sections);

assert_eq!(sections.len(), 1);
assert_eq!(sections[0].name, Some("given".to_string()));
assert_eq!(sections[0].subs.len(), 2, "Expected 2 subsections, got: {:?}", sections[0].subs);
assert!(sections[0].subs.contains(&"when".to_string()));
assert!(sections[0].subs.contains(&"then".to_string()));
}

#[test]
fn test_multiline_with_multiple_continuation_lines() {
// Test with multiple continuation lines for a single subsection
let lines = vec![
"Args:".to_string(),
" param1: This is line 1".to_string(),
" This is line 2 of the same parameter".to_string(),
" And this is line 3".to_string(),
" param2: Another parameter.".to_string(),
];

let sections = _get_sections(lines);

assert_eq!(sections.len(), 1);
assert_eq!(sections[0].name, Some("Args".to_string()));
assert_eq!(sections[0].subs.len(), 2);
assert!(sections[0].subs.contains(&"param1".to_string()));
assert!(sections[0].subs.contains(&"param2".to_string()));
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ fn _core(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {

let submodule = PyModule::new_bound(py, "docstring")?;
submodule.add_class::<docstring::_Section>()?;
submodule.add_function(wrap_pyfunction!(docstring::_get_sections, m)?)?;

m.add_submodule(&submodule)?;
let constants = PyModule::new_bound(py, "constants")?;
Expand Down
93 changes: 93 additions & 0 deletions tests/python_tests/test_multiline_docstrings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""Test cases for multiline docstring support."""
from ruff_docstrings_complete._core import docstring


def test_arrange_act_assert_multiline():
"""Test arrange/act/assert pattern with multiline descriptions."""
lines = [
"arrange: This is a very important part of a very important test so",
" the arrange sentence has to be loooong.",
"act: Do the test.",
"assert: It better not fail.",
]

sections = docstring._get_sections(lines)

# Should have one section named 'arrange' with 'act' and 'assert' as subsections
assert len(sections) == 1
assert sections[0].name == "arrange"
assert len(sections[0].subs) == 2
assert "act" in sections[0].subs
assert "assert" in sections[0].subs
print("✓ arrange/act/assert multiline test passed")


def test_given_when_then_multiline():
"""Test given/when/then pattern with multiline descriptions."""
lines = [
"given: A complex setup scenario that requires multiple lines",
" to properly explain all the preconditions.",
"when: The user performs an action.",
"then: The system should respond appropriately with",
" the expected behavior.",
]

sections = docstring._get_sections(lines)

assert len(sections) == 1
assert sections[0].name == "given"
assert len(sections[0].subs) == 2
assert "when" in sections[0].subs
assert "then" in sections[0].subs
print("✓ given/when/then multiline test passed")


def test_args_section_multiline():
"""Test Args section with multiline parameter descriptions."""
lines = [
"Args:",
" param1: This is a very long description that needs to",
" span multiple lines because it's important.",
" param2: Short description.",
" param3: Another long description that also",
" needs multiple lines.",
]

sections = docstring._get_sections(lines)

assert len(sections) == 1
assert sections[0].name == "Args"
assert len(sections[0].subs) == 3
assert "param1" in sections[0].subs
assert "param2" in sections[0].subs
assert "param3" in sections[0].subs
print("✓ Args multiline test passed")


def test_multiple_continuation_lines():
"""Test subsections with multiple continuation lines."""
lines = [
"Args:",
" param1: This is line 1",
" This is line 2 of the same parameter",
" And this is line 3",
" And line 4",
" param2: Another parameter.",
]

sections = docstring._get_sections(lines)

assert len(sections) == 1
assert sections[0].name == "Args"
assert len(sections[0].subs) == 2
assert "param1" in sections[0].subs
assert "param2" in sections[0].subs
print("✓ Multiple continuation lines test passed")


if __name__ == "__main__":
test_arrange_act_assert_multiline()
test_given_when_then_multiline()
test_args_section_multiline()
test_multiple_continuation_lines()
print("\n✅ All Python integration tests passed!")