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

kaidokert / xacro / 21200689519

21 Jan 2026 07:16AM UTC coverage: 90.104%. First build
21200689519

Pull #86

github

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

517 of 577 new or added lines in 7 files covered. (89.6%)

5472 of 6073 relevant lines covered (90.1%)

281.37 hits per line

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

78.7
/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
    // Update state in separate scope to release borrows
73
    {
33✔
74
        *ctx.base_path.borrow_mut() = new_base_path;
33✔
75
        ctx.include_stack.borrow_mut().push(file_path.clone());
33✔
76

33✔
77
        // Track included file. Deduplication is handled by get_all_includes().
33✔
78
        ctx.all_includes.borrow_mut().push(file_path.clone());
33✔
79

33✔
80
        ctx.namespace_stack
33✔
81
            .borrow_mut()
33✔
82
            .push((file_path.clone(), included_ns));
33✔
83
    }
33✔
84

85
    let _include_guard = IncludeGuard {
33✔
86
        base_path: &ctx.base_path,
33✔
87
        include_stack: &ctx.include_stack,
33✔
88
        namespace_stack: &ctx.namespace_stack,
33✔
89
        old_base_path,
33✔
90
    };
33✔
91

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

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

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

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

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

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

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

184
        return Ok(result);
1✔
185
    }
35✔
186

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

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

196
    // Case 3: Process the include (will error if file not found)
197
    process_single_include(file_path, ctx)
34✔
198
}
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