Skip to main content

mq_dap/
executor.rs

1use crossbeam_channel::Sender;
2use std::{fs, path::PathBuf};
3use tracing::{debug, error, info};
4
5use crate::error::MqAdapterError;
6use crate::protocol::DebuggerMessage;
7
8type DynResult<T> = miette::Result<T, Box<dyn std::error::Error>>;
9
10/// Execute query in a separate thread
11pub fn execute_query(
12    mut engine: mq_lang::DefaultEngine,
13    query: String,
14    input_file: Option<String>,
15    message_tx: Sender<DebuggerMessage>,
16) -> DynResult<()> {
17    debug!(query = %query, input_file = ?input_file, "Executing query in background thread");
18
19    let query = fs::read_to_string(&query).map_err(|e| {
20        let error_msg = format!("Failed to read query file '{}': {}", query, e);
21        error!(error = %error_msg);
22        Box::new(MqAdapterError::FileError(error_msg)) as Box<dyn std::error::Error>
23    })?;
24
25    // Prepare input data
26    let input_data = if let Some(file_path) = input_file {
27        let input = fs::read_to_string(&file_path).map_err(|e| {
28            let error_msg = format!("Failed to read input file '{}': {}", file_path, e);
29            error!(error = %error_msg);
30            Box::new(MqAdapterError::FileError(error_msg)) as Box<dyn std::error::Error>
31        })?;
32
33        parse_input_data(&file_path, &input)?
34    } else {
35        mq_lang::null_input()
36    };
37
38    let result = engine.eval(&query, input_data.into_iter());
39
40    match result {
41        Ok(values) => {
42            let output = values
43                .values()
44                .iter()
45                .map(|v| v.to_string())
46                .collect::<Vec<_>>()
47                .join("\n");
48            info!(output = %output, "Query execution completed successfully");
49        }
50        Err(e) => {
51            let error_msg = format!("Query execution failed: {}", e);
52            error!(error = %error_msg);
53            // Send terminated message even on error
54            let _ = message_tx.send(DebuggerMessage::Terminated);
55            return Err(Box::new(MqAdapterError::QueryError(error_msg)));
56        }
57    }
58
59    if let Err(e) = message_tx.send(DebuggerMessage::Terminated) {
60        error!(error = %e, "Failed to send terminated message");
61    }
62
63    Ok(())
64}
65
66/// Parse input data based on file extension
67fn parse_input_data(file_path: &str, input: &str) -> DynResult<Vec<mq_lang::RuntimeValue>> {
68    match PathBuf::from(file_path)
69        .extension()
70        .unwrap_or_default()
71        .to_string_lossy()
72        .to_lowercase()
73        .as_str()
74    {
75        "json" | "csv" | "tsv" | "xml" | "toml" | "yaml" | "yml" | "txt" => Ok(mq_lang::raw_input(input)),
76        "html" | "htm" => mq_lang::parse_html_input(input).map_err(|e| {
77            let error_msg = format!("Failed to parse input file '{}': {}", file_path, e);
78            error!(error = %error_msg);
79            Box::new(MqAdapterError::FileError(error_msg)) as Box<dyn std::error::Error>
80        }),
81        "mdx" => mq_lang::parse_mdx_input(input).map_err(|e| {
82            let error_msg = format!("Failed to parse input file '{}': {}", file_path, e);
83            error!(error = %error_msg);
84            Box::new(MqAdapterError::FileError(error_msg)) as Box<dyn std::error::Error>
85        }),
86        _ => mq_lang::parse_markdown_input(input).map_err(|e| {
87            let error_msg = format!("Failed to parse input file '{}': {}", file_path, e);
88            error!(error = %error_msg);
89            Box::new(MqAdapterError::FileError(error_msg)) as Box<dyn std::error::Error>
90        }),
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use crossbeam_channel::unbounded;
98    use std::fs;
99    use tempfile::TempDir;
100
101    /// Helper to create a dummy engine that echoes input
102    fn dummy_engine() -> mq_lang::DefaultEngine {
103        mq_lang::DefaultEngine::default()
104    }
105
106    #[test]
107    fn test_parse_input_data_json() {
108        let input = r#"{"key": "value"}"#;
109        let result = parse_input_data("test.json", input).unwrap();
110        assert!(!result.is_empty(), "Should parse JSON input as raw");
111    }
112
113    #[test]
114    fn test_parse_input_data_csv() {
115        let input = "name,age\nJohn,30\nJane,25";
116        let result = parse_input_data("test.csv", input).unwrap();
117        assert!(!result.is_empty());
118    }
119
120    #[test]
121    fn test_parse_input_data_tsv() {
122        let input = "name\tage\nJohn\t30\nJane\t25";
123        let result = parse_input_data("test.tsv", input).unwrap();
124        assert!(!result.is_empty());
125    }
126
127    #[test]
128    fn test_parse_input_data_xml() {
129        let input = "<root><item>test</item></root>";
130        let result = parse_input_data("test.xml", input).unwrap();
131        assert!(!result.is_empty());
132    }
133
134    #[test]
135    fn test_parse_input_data_toml() {
136        let input = r#"
137[package]
138name = "test"
139"#;
140        let result = parse_input_data("test.toml", input).unwrap();
141        assert!(!result.is_empty());
142    }
143
144    #[test]
145    fn test_parse_input_data_yaml() {
146        let input = r#"
147name: test
148version: 1.0.0
149"#;
150        let result = parse_input_data("test.yaml", input).unwrap();
151        assert!(!result.is_empty());
152    }
153
154    #[test]
155    fn test_parse_input_data_txt() {
156        let input = "This is plain text";
157        let result = parse_input_data("test.txt", input).unwrap();
158        assert!(!result.is_empty());
159    }
160
161    #[test]
162    fn test_parse_input_data_html() {
163        let input = r#"<div>Hello</div>"#;
164        let result = parse_input_data("test.html", input);
165        assert!(result.is_ok(), "Should parse HTML input");
166
167        let input = "<html><body><h1>Hello</h1></body></html>";
168        let result = parse_input_data("test.html", input);
169        // HTML parsing might succeed or fail depending on implementation
170        // We just check that it returns a result
171        assert!(result.is_ok() || result.is_err());
172    }
173
174    #[test]
175    fn test_parse_input_data_htm() {
176        let input = "<html><body><h1>Hello</h1></body></html>";
177        let result = parse_input_data("test.htm", input);
178        // HTML parsing might succeed or fail depending on implementation
179        assert!(result.is_ok() || result.is_err());
180    }
181
182    #[test]
183    fn test_parse_input_data_mdx() {
184        let input = r#"# Hello MDX"#;
185        let result = parse_input_data("test.mdx", input);
186        assert!(result.is_ok(), "Should parse MDX input");
187
188        let input = "# Hello\n\n```js\nconsole.log('hello');\n```";
189        let result = parse_input_data("test.mdx", input);
190        // MDX parsing might succeed or fail depending on implementation
191        assert!(result.is_ok() || result.is_err());
192    }
193
194    #[test]
195    fn test_parse_input_data_markdown_default() {
196        let input = r#"# Hello Markdown"#;
197        let result = parse_input_data("test.unknown", input);
198        assert!(result.is_ok(), "Should parse unknown extension as Markdown");
199        let input = "# Hello World\n\nThis is markdown content.";
200        let result = parse_input_data("test.md", input);
201        // Markdown parsing should work
202        assert!(result.is_ok() || result.is_err());
203    }
204
205    #[test]
206    fn test_parse_input_data_unknown_extension() {
207        let input = "# Hello World\n\nThis is treated as markdown.";
208        let result = parse_input_data("test.unknown", input);
209        // Unknown extensions are treated as markdown
210        assert!(result.is_ok() || result.is_err());
211    }
212
213    #[test]
214    fn test_execute_query_success() {
215        let engine = dummy_engine();
216        let query_file = "test_query.txt";
217        let input_file = None;
218        let (tx, rx) = unbounded();
219
220        // Write a dummy query file
221        std::fs::write(query_file, ".h").unwrap();
222
223        let result = execute_query(engine, query_file.to_string(), input_file, tx);
224        assert!(result.is_ok(), "Query execution should succeed");
225        assert!(matches!(rx.recv().unwrap(), DebuggerMessage::Terminated));
226
227        // Clean up
228        let _ = std::fs::remove_file(query_file);
229    }
230
231    #[test]
232    fn test_execute_query_query_file_not_found() {
233        let engine = dummy_engine();
234        let query_file = "non_existent_query.txt";
235        let input_file = None;
236        let (tx, _rx) = unbounded();
237
238        let result = execute_query(engine, query_file.to_string(), input_file, tx);
239        assert!(result.is_err(), "Should error if query file does not exist");
240        let temp_dir = TempDir::new().unwrap();
241        let query_file_path = temp_dir.path().join("test.mq");
242        let input_file_path = temp_dir.path().join("input.md");
243
244        // Create query file
245        fs::write(&query_file_path, "# Simple Query\n.").unwrap();
246
247        // Create input file
248        fs::write(&input_file_path, "# Test Input\n\nThis is a test.").unwrap();
249
250        let engine = mq_lang::DefaultEngine::default();
251        let (tx, _rx) = unbounded::<DebuggerMessage>();
252
253        let result = execute_query(
254            engine,
255            query_file_path.to_string_lossy().to_string(),
256            Some(input_file_path.to_string_lossy().to_string()),
257            tx,
258        );
259
260        // The result might succeed or fail depending on the query execution
261        // We're mainly testing that the function doesn't panic
262        assert!(result.is_ok() || result.is_err());
263    }
264
265    #[test]
266    fn test_execute_query_without_input_file() {
267        let temp_dir = TempDir::new().unwrap();
268        let query_file_path = temp_dir.path().join("test.mq");
269
270        // Create query file
271        fs::write(&query_file_path, ".").unwrap();
272
273        let engine = mq_lang::DefaultEngine::default();
274        let (tx, _rx) = unbounded::<DebuggerMessage>();
275
276        let result = execute_query(engine, query_file_path.to_string_lossy().to_string(), None, tx);
277
278        // The result might succeed or fail depending on the query execution
279        assert!(result.is_ok() || result.is_err());
280    }
281
282    #[test]
283    fn test_execute_query_nonexistent_query_file() {
284        let engine = mq_lang::DefaultEngine::default();
285        let (tx, _rx) = unbounded::<DebuggerMessage>();
286
287        let result = execute_query(engine, "nonexistent.mq".to_string(), None, tx);
288
289        assert!(result.is_err());
290        assert!(result.unwrap_err().to_string().contains("Failed to read query file"));
291    }
292
293    #[test]
294    fn test_execute_query_nonexistent_input_file() {
295        let temp_dir = TempDir::new().unwrap();
296        let query_file_path = temp_dir.path().join("test.mq");
297
298        // Create query file
299        fs::write(&query_file_path, ".").unwrap();
300
301        let engine = mq_lang::DefaultEngine::default();
302        let (tx, _rx) = unbounded::<DebuggerMessage>();
303
304        let result = execute_query(
305            engine,
306            query_file_path.to_string_lossy().to_string(),
307            Some("nonexistent_input.md".to_string()),
308            tx,
309        );
310
311        assert!(result.is_err());
312        assert!(result.unwrap_err().to_string().contains("Failed to read input file"));
313    }
314
315    #[test]
316    fn test_execute_query_sends_terminated_message() {
317        let temp_dir = TempDir::new().unwrap();
318        let query_file_path = temp_dir.path().join("test.mq");
319
320        // Create a simple query file
321        fs::write(&query_file_path, ".").unwrap();
322
323        let engine = mq_lang::DefaultEngine::default();
324        let (tx, rx) = unbounded::<DebuggerMessage>();
325
326        let result = execute_query(engine, query_file_path.to_string_lossy().to_string(), None, tx);
327
328        // Whether the execution succeeds or fails, a Terminated message should be sent
329        if result.is_ok() {
330            // If successful, check for Terminated message
331            if let Ok(message) = rx.try_recv() {
332                assert!(matches!(message, DebuggerMessage::Terminated));
333            }
334        } else {
335            // If failed, Terminated message should still be sent
336            if let Ok(message) = rx.try_recv() {
337                assert!(matches!(message, DebuggerMessage::Terminated));
338            }
339        }
340    }
341
342    #[test]
343    fn test_parse_input_data_edge_cases() {
344        // Test empty file path
345        let result = parse_input_data("", "content");
346        assert!(result.is_ok() || result.is_err());
347
348        // Test file path without extension
349        let result = parse_input_data("filename", "content");
350        assert!(result.is_ok() || result.is_err());
351
352        // Test file path with multiple dots
353        let result = parse_input_data("file.name.with.dots.md", "# Content");
354        assert!(result.is_ok() || result.is_err());
355
356        // Test case sensitivity
357        let result = parse_input_data("test.JSON", r#"{"key": "value"}"#);
358        assert!(!result.unwrap().is_empty());
359    }
360}