• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kaidokert / xacro / 21202256677

21 Jan 2026 08:20AM UTC coverage: 90.102%. First build
21202256677

Pull #86

github

web-flow
Merge a18009358 into ef96bb522
Pull Request #86: Refactor expander into focused submodules

516 of 576 new or added lines in 7 files covered. (89.58%)

5471 of 6072 relevant lines covered (90.1%)

286.51 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.1
/src/expander/include.rs
1
//! Include directive handling and file resolution
2
//!
3
//! This module provides functionality for processing `xacro:include` directives,
4
//! including glob pattern resolution, optional includes, and circular include detection.
5

6
use crate::{error::XacroError, parse::xml::extract_xacro_namespace};
7
use std::path::PathBuf;
8
use xmltree::{Element, XMLNode};
9

10
use super::*;
11

12
/// Check if a filename contains glob pattern characters
13
///
14
/// Python xacro regex: `re.search('[*[?]+', filename_spec)`
15
/// Detects wildcard patterns: *, [, or ?
16
pub(super) fn is_glob_pattern(filename: &str) -> bool {
41✔
17
    filename.contains('*') || filename.contains('[') || filename.contains('?')
41✔
18
}
41✔
19

20
/// Process a single include file
21
///
22
/// Handles:
23
/// - Circular include detection
24
/// - File reading and XML parsing
25
/// - Namespace extraction
26
/// - Include stack management with RAII guard
27
///
28
/// # Arguments
29
/// * `file_path` - Absolute path to the file to include
30
/// * `ctx` - XacroContext with include stack and namespace stack
31
///
32
/// # Returns
33
/// Expanded nodes from the included file
34
fn process_single_include(
35✔
35
    file_path: PathBuf,
35✔
36
    ctx: &XacroContext,
35✔
37
) -> Result<Vec<XMLNode>, XacroError> {
35✔
38
    // Check for circular includes
39
    if ctx.include_stack.borrow().contains(&file_path) {
35✔
NEW
40
        return Err(XacroError::Include(format!(
×
NEW
41
            "Circular include detected: {}",
×
NEW
42
            file_path.display()
×
NEW
43
        )));
×
44
    }
35✔
45

46
    // Read and parse included file
47
    let content = std::fs::read_to_string(&file_path).map_err(|e| {
35✔
48
        XacroError::Include(format!(
2✔
49
            "Failed to read file '{}': {}",
2✔
50
            file_path.display(),
2✔
51
            e
2✔
52
        ))
2✔
53
    })?;
2✔
54

55
    let included_root = Element::parse(content.as_bytes()).map_err(|e| {
33✔
NEW
56
        XacroError::Include(format!(
×
NEW
57
            "Failed to parse XML in file '{}': {}",
×
NEW
58
            file_path.display(),
×
NEW
59
            e
×
NEW
60
        ))
×
NEW
61
    })?;
×
62

63
    // Extract xacro namespace from included file
64
    // Use the same namespace validation mode as the parent document
65
    let included_ns = extract_xacro_namespace(&included_root, ctx.compat_mode.namespace)?;
33✔
66

67
    // Push to include stack with RAII guard for automatic cleanup
68
    let old_base_path = ctx.base_path.borrow().clone();
33✔
69
    let mut new_base_path = file_path.clone();
33✔
70
    new_base_path.pop();
33✔
71

72
    // Create RAII guard BEFORE state mutations to ensure cleanup on panic
73
    // Capture current stack lengths to detect partial pushes
74
    let _include_guard = IncludeGuard {
33✔
75
        base_path: &ctx.base_path,
33✔
76
        include_stack: &ctx.include_stack,
33✔
77
        namespace_stack: &ctx.namespace_stack,
33✔
78
        old_base_path,
33✔
79
        include_stack_len: ctx.include_stack.borrow().len(),
33✔
80
        namespace_stack_len: ctx.namespace_stack.borrow().len(),
33✔
81
    };
33✔
82

83
    // Now perform state updates (guard will restore state on panic)
84
    *ctx.base_path.borrow_mut() = new_base_path;
33✔
85
    ctx.include_stack.borrow_mut().push(file_path.clone());
33✔
86

87
    // Track included file. Deduplication is handled by get_all_includes().
88
    ctx.all_includes.borrow_mut().push(file_path.clone());
33✔
89

90
    ctx.namespace_stack
33✔
91
        .borrow_mut()
33✔
92
        .push((file_path.clone(), included_ns));
33✔
93

94
    // Expand children and return
95
    expand_children_list(included_root.children, ctx)
33✔
96
}
35✔
97

98
/// Handle `xacro:include` directive
99
///
100
/// Supports:
101
/// - Glob patterns (*, [, ?)
102
/// - Optional includes (optional="true")
103
/// - Circular include detection
104
///
105
/// Python xacro has 3 different behaviors:
106
/// 1. Glob patterns with no matches → warn, continue
107
/// 2. optional="true" attribute → silent skip if not found
108
/// 3. Regular missing file → error
109
///
110
/// # Arguments
111
/// * `elem` - The `<xacro:include>` element
112
/// * `ctx` - XacroContext with properties and include stack
113
///
114
/// # Returns
115
/// Expanded nodes from included file(s), or empty vec if no match/optional
116
pub(crate) fn handle_include_directive(
41✔
117
    elem: Element,
41✔
118
    ctx: &XacroContext,
41✔
119
) -> Result<Vec<XMLNode>, XacroError> {
41✔
120
    // Extract filename and substitute expressions
121
    let filename = ctx
41✔
122
        .properties
41✔
123
        .substitute_text(elem.get_attribute("filename").ok_or_else(|| {
41✔
NEW
124
            XacroError::MissingAttribute {
×
NEW
125
                element: "xacro:include".to_string(),
×
NEW
126
                attribute: "filename".to_string(),
×
NEW
127
            }
×
NEW
128
        })?)?;
×
129

130
    // Check for optional attribute (default: false)
131
    let optional = elem
41✔
132
        .get_attribute("optional")
41✔
133
        .map(|v| v == "true" || v == "1")
41✔
134
        .unwrap_or(false);
41✔
135

136
    // Case 1: Glob pattern (detected by *, [, or ? characters)
137
    if is_glob_pattern(&filename) {
41✔
138
        // Resolve glob pattern relative to base_path
139
        let glob_pattern = {
6✔
140
            let base = ctx.base_path.borrow();
6✔
141
            if std::path::Path::new(&filename).is_absolute() {
6✔
142
                filename.clone()
4✔
143
            } else {
144
                base.join(&filename)
2✔
145
                    .to_str()
2✔
146
                    .ok_or_else(|| {
2✔
NEW
147
                        XacroError::Include(format!("Invalid UTF-8 in glob pattern: {}", filename))
×
NEW
148
                    })?
×
149
                    .to_string()
2✔
150
            }
151
        };
152

153
        // Find matches
154
        let mut matches: Vec<PathBuf> = glob::glob(&glob_pattern)
6✔
155
            .map_err(|e| {
6✔
NEW
156
                XacroError::Include(format!("Invalid glob pattern '{}': {}", filename, e))
×
NEW
157
            })?
×
158
            .filter_map(|result| match result {
6✔
159
                Ok(path) => Some(path),
1✔
NEW
160
                Err(e) => {
×
NEW
161
                    log::warn!("Error reading glob match: {}", e);
×
NEW
162
                    None
×
163
                }
164
            })
1✔
165
            .collect();
6✔
166

167
        // No matches - warn (unless optional) and continue (Python behavior)
168
        if matches.is_empty() {
6✔
169
            if !optional {
5✔
170
                log::warn!(
4✔
NEW
171
                    "Include tag's filename spec \"{}\" matched no files.",
×
172
                    filename
173
                );
174
            }
1✔
175
            return Ok(vec![]);
5✔
176
        }
1✔
177

178
        // Process all matches in sorted order (Python sorts matches)
179
        matches.sort();
1✔
180
        let mut result = Vec::new();
1✔
181
        for file_path in matches {
2✔
182
            let expanded = process_single_include(file_path, ctx)?;
1✔
183
            result.extend(expanded);
1✔
184
        }
185

186
        return Ok(result);
1✔
187
    }
35✔
188

189
    // Case 2 & 3: Regular file (not a glob pattern)
190
    // Resolve path relative to base_path
191
    let file_path = ctx.base_path.borrow().join(&filename);
35✔
192

193
    // Case 2: optional="true" - check if file exists before processing
194
    if optional && !file_path.exists() {
35✔
195
        return Ok(vec![]);
1✔
196
    }
34✔
197

198
    // Case 3: Process the include (will error if file not found)
199
    process_single_include(file_path, ctx)
34✔
200
}
41✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc