Skip to main content

dynamic_cli/interface/
cli.rs

1//! CLI (Command-Line Interface) implementation
2//!
3//! This module provides a simple CLI interface that parses command-line
4//! arguments, executes the corresponding command, and exits.
5//!
6//! # Example
7//!
8//! ```no_run
9//! use dynamic_cli::interface::CliInterface;
10//! use dynamic_cli::prelude::*;
11//!
12//! # #[derive(Default)]
13//! # struct MyContext;
14//! # impl ExecutionContext for MyContext {
15//! #     fn as_any(&self) -> &dyn std::any::Any { self }
16//! #     fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
17//! # }
18//! # fn main() -> dynamic_cli::Result<()> {
19//! let registry = CommandRegistry::new();
20//! let context = Box::new(MyContext::default());
21//!
22//! let cli = CliInterface::new(registry, context);
23//! cli.run(std::env::args().skip(1).collect())?;
24//! # Ok(())
25//! # }
26//! ```
27
28use crate::context::ExecutionContext;
29use crate::error::{display_error, DynamicCliError, ExecutionError, Result};
30use crate::parser::{CliParser, ParsedArgs, ReplParser};
31use crate::registry::CommandRegistry;
32use std::path::Path;
33use std::process;
34
35/// CLI (Command-Line Interface) handler
36///
37/// Provides a simple interface for executing commands from command-line arguments.
38/// The CLI parses arguments, executes the command, and exits.
39///
40/// # Architecture
41///
42/// ```text
43/// Command-line args → CliParser → CommandExecutor → Handler
44///                                       ↓
45///                                  ExecutionContext
46/// ```
47///
48/// # Error Handling
49///
50/// Errors are displayed to stderr with colored formatting (if enabled)
51/// and the process exits with appropriate exit codes:
52/// - `0`: Success
53/// - `1`: Execution error
54/// - `2`: Argument parsing error
55/// - `3`: Other errors
56pub struct CliInterface {
57    /// Command registry containing all available commands
58    registry: CommandRegistry,
59
60    /// Execution context (owned by the interface)
61    context: Box<dyn ExecutionContext>,
62}
63
64impl CliInterface {
65    /// Create a new CLI interface
66    ///
67    /// # Arguments
68    ///
69    /// * `registry` - Command registry with all registered commands
70    /// * `context` - Execution context (will be consumed by the interface)
71    ///
72    /// # Example
73    ///
74    /// ```no_run
75    /// use dynamic_cli::interface::CliInterface;
76    /// use dynamic_cli::prelude::*;
77    ///
78    /// # #[derive(Default)]
79    /// # struct MyContext;
80    /// # impl ExecutionContext for MyContext {
81    /// #     fn as_any(&self) -> &dyn std::any::Any { self }
82    /// #     fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
83    /// # }
84    /// let registry = CommandRegistry::new();
85    /// let context = Box::new(MyContext::default());
86    ///
87    /// let cli = CliInterface::new(registry, context);
88    /// ```
89    pub fn new(registry: CommandRegistry, context: Box<dyn ExecutionContext>) -> Self {
90        Self { registry, context }
91    }
92
93    /// Run the CLI with provided arguments
94    ///
95    /// Parses the arguments, executes the corresponding command, and handles errors.
96    /// This method consumes `self` as the CLI typically runs once and exits.
97    ///
98    /// # Arguments
99    ///
100    /// * `args` - Command-line arguments (typically from `env::args().skip(1)`)
101    ///
102    /// # Returns
103    ///
104    /// - `Ok(())` on success
105    /// - `Err(DynamicCliError)` on any error (parsing, validation, execution)
106    ///
107    /// # Exit Codes
108    ///
109    /// The caller should handle errors and exit with appropriate codes:
110    /// - Parse errors → exit code 2
111    /// - Execution errors → exit code 1
112    /// - Other errors → exit code 3
113    ///
114    /// # Example
115    ///
116    /// ```no_run
117    /// use dynamic_cli::interface::CliInterface;
118    /// use dynamic_cli::prelude::*;
119    /// use std::process;
120    ///
121    /// # #[derive(Default)]
122    /// # struct MyContext;
123    /// # impl ExecutionContext for MyContext {
124    /// #     fn as_any(&self) -> &dyn std::any::Any { self }
125    /// #     fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
126    /// # }
127    /// # fn main() {
128    /// let registry = CommandRegistry::new();
129    /// let context = Box::new(MyContext::default());
130    /// let cli = CliInterface::new(registry, context);
131    ///
132    /// if let Err(e) = cli.run(std::env::args().skip(1).collect()) {
133    ///     eprintln!("Error: {}", e);
134    ///     process::exit(1);
135    /// }
136    /// # }
137    /// ```
138    pub fn run(mut self, args: Vec<String>) -> Result<()> {
139        // Handle empty arguments (show help or error)
140        if args.is_empty() {
141            return Err(DynamicCliError::Parse(
142                crate::error::ParseError::InvalidSyntax {
143                    details: "No command specified".to_string(),
144                    hint: Some("Try 'help' to see available commands".to_string()),
145                },
146            ));
147        }
148
149        self.dispatch(&args)
150    }
151
152    /// Resolve, parse, and execute a single already-tokenized command line.
153    ///
154    /// Shared by [`run`][Self::run] (one dispatch from CLI args) and
155    /// [`run_script`][Self::run_script] (one dispatch per script line) —
156    /// the actual resolution/parsing/execution logic lives here exactly
157    /// once, per DD-024's "reuse the existing `ParsedArgs` path, no
158    /// duplicate parsing logic" requirement (see #41).
159    fn dispatch(&mut self, args: &[String]) -> Result<()> {
160        // First argument is the command name
161        let command_name = &args[0];
162
163        // Resolve command name (handles aliases)
164        let resolved_name = self.registry.resolve_name(command_name).ok_or_else(|| {
165            crate::error::ParseError::unknown_command_with_suggestions(
166                command_name,
167                &self
168                    .registry
169                    .list_commands()
170                    .iter()
171                    .map(|cmd| cmd.name.clone())
172                    .collect::<Vec<_>>(),
173            )
174        })?;
175
176        // Get command definition
177        let definition = self.registry.get_definition(resolved_name).ok_or_else(|| {
178            DynamicCliError::Registry(crate::error::RegistryError::missing_handler(resolved_name))
179        })?;
180
181        // Parse arguments using CLI parser (DD-024/#39: typed to preserve
182        // repeatable-option occurrences; ParsedArgs is the shape every
183        // handler now receives).
184        let parser = CliParser::new(definition);
185        let parsed_args = ParsedArgs::new(parser.parse_typed(&args[1..])?);
186
187        // Get handler and execute command. Sync is tried first (unchanged
188        // behaviour); if no sync handler matches, fall through to the async
189        // path (DD-022) and drive it via `block_on`. Safe here because
190        // `run()`/`run_script()` are strictly sequential, one-shot dispatch —
191        // there is no other async task waiting behind it that `block_on`
192        // could starve.
193        if let Some(handler) = self.registry.get_handler_sync(resolved_name) {
194            handler.execute(&mut *self.context, &parsed_args)?;
195        } else if let Some(handler) = self.registry.get_handler_async(resolved_name) {
196            futures::executor::block_on(handler.execute(&mut *self.context, &parsed_args))?;
197        } else {
198            return Err(DynamicCliError::Execution(
199                crate::error::ExecutionError::handler_not_found(
200                    resolved_name,
201                    &definition.implementation,
202                ),
203            ));
204        }
205
206        Ok(())
207    }
208
209    /// Run the CLI with automatic error handling and exit
210    ///
211    /// This is a convenience method that:
212    /// 1. Runs the CLI with provided arguments
213    /// 2. Handles errors by displaying them to stderr
214    /// 3. Exits the process with appropriate exit code
215    ///
216    /// This method never returns.
217    ///
218    /// # Arguments
219    ///
220    /// * `args` - Command-line arguments
221    ///
222    /// # Example
223    ///
224    /// ```no_run
225    /// use dynamic_cli::interface::CliInterface;
226    /// use dynamic_cli::prelude::*;
227    ///
228    /// # #[derive(Default)]
229    /// # struct MyContext;
230    /// # impl ExecutionContext for MyContext {
231    /// #     fn as_any(&self) -> &dyn std::any::Any { self }
232    /// #     fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
233    /// # }
234    /// # fn main() {
235    /// let registry = CommandRegistry::new();
236    /// let context = Box::new(MyContext::default());
237    /// let cli = CliInterface::new(registry, context);
238    ///
239    /// // This will handle errors and exit automatically
240    /// cli.run_and_exit(std::env::args().skip(1).collect());
241    /// # }
242    /// ```
243    pub fn run_and_exit(self, args: Vec<String>) -> ! {
244        match self.run(args) {
245            Ok(()) => process::exit(0),
246            Err(e) => {
247                display_error(&e);
248
249                // Exit with appropriate code based on error type
250                let exit_code = match e {
251                    DynamicCliError::Parse(_) => 2,
252                    DynamicCliError::Validation(_) => 2,
253                    DynamicCliError::Execution(_) => 1,
254                    _ => 3,
255                };
256
257                process::exit(exit_code);
258            }
259        }
260    }
261
262    /// Run a batch of command lines read from a file (#41).
263    ///
264    /// Each non-blank, non-comment (`#`-prefixed) line is tokenized the
265    /// same quote-aware way as a typed REPL line (via
266    /// [`ReplParser::tokenize`]), then dispatched through the exact same
267    /// resolve → parse → execute path as [`run`][Self::run] — no
268    /// duplicate parsing logic, and repeatable options (DD-024) are fully
269    /// preserved since dispatch goes through `parse_typed()` either way.
270    ///
271    /// # Error policy
272    ///
273    /// `policy` decides what happens when a line fails:
274    /// - [`ScriptErrorPolicy::Abort`]: stop immediately, returning `Err`
275    ///   for the failing line. Lines before it have already run.
276    /// - [`ScriptErrorPolicy::Continue`]: record the failure and proceed
277    ///   to the next line. The method still returns `Ok`, with every
278    ///   failure listed in the returned [`ScriptOutcome`].
279    ///
280    /// Every failure — whether it aborts the run or not — is reported
281    /// with its 1-based line number, wrapped in
282    /// [`ExecutionError::CommandFailed`][crate::error::ExecutionError::CommandFailed]
283    /// (reusing the existing error hierarchy; no new enum variant, so no
284    /// breaking change to `ExecutionError`'s non-`#[non_exhaustive]`
285    /// shape).
286    ///
287    /// # Example
288    ///
289    /// ```no_run
290    /// use dynamic_cli::interface::{CliInterface, ScriptErrorPolicy};
291    /// use dynamic_cli::prelude::*;
292    ///
293    /// # #[derive(Default)]
294    /// # struct MyContext;
295    /// # impl ExecutionContext for MyContext {
296    /// #     fn as_any(&self) -> &dyn std::any::Any { self }
297    /// #     fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
298    /// # }
299    /// # fn main() -> dynamic_cli::Result<()> {
300    /// let registry = CommandRegistry::new();
301    /// let context = Box::new(MyContext::default());
302    /// let cli = CliInterface::new(registry, context);
303    ///
304    /// let outcome = cli.run_script("commands.txt", ScriptErrorPolicy::Continue)?;
305    /// println!("{}/{} lines succeeded", outcome.lines_succeeded, outcome.lines_executed);
306    /// # Ok(())
307    /// # }
308    /// ```
309    pub fn run_script(
310        mut self,
311        path: impl AsRef<Path>,
312        policy: ScriptErrorPolicy,
313    ) -> Result<ScriptOutcome> {
314        let path = path.as_ref();
315        let content = std::fs::read_to_string(path).map_err(|e| {
316            DynamicCliError::Execution(ExecutionError::CommandFailed(anyhow::anyhow!(
317                "failed to read script file {}: {}",
318                path.display(),
319                e
320            )))
321        })?;
322
323        let mut outcome = ScriptOutcome {
324            lines_executed: 0,
325            lines_succeeded: 0,
326            failures: Vec::new(),
327        };
328
329        for (idx, raw_line) in content.lines().enumerate() {
330            let line_number = idx + 1;
331            let line = raw_line.trim();
332
333            if line.is_empty() || line.starts_with('#') {
334                continue;
335            }
336
337            outcome.lines_executed += 1;
338
339            // Scoped so the borrow of `self.registry` ends before
340            // `self.dispatch(&mut self, ...)` needs exclusive access below.
341            // `tokenize` is a pure function of the line text — it doesn't
342            // read `self.registry` — but it lives on `ReplParser`, so a
343            // throwaway instance is the reuse path rather than duplicating
344            // the quote-handling logic here.
345            let tokens_result = {
346                let tokenizer = ReplParser::new(&self.registry);
347                tokenizer.tokenize(line)
348            };
349
350            let tokens = match tokens_result {
351                Ok(t) => t,
352                Err(e) => {
353                    let wrapped = wrap_line_error(line_number, e);
354                    if policy == ScriptErrorPolicy::Abort {
355                        return Err(wrapped);
356                    }
357                    outcome.failures.push((line_number, wrapped));
358                    continue;
359                }
360            };
361
362            if tokens.is_empty() {
363                continue;
364            }
365
366            match self.dispatch(&tokens) {
367                Ok(()) => outcome.lines_succeeded += 1,
368                Err(e) => {
369                    let wrapped = wrap_line_error(line_number, e);
370                    if policy == ScriptErrorPolicy::Abort {
371                        return Err(wrapped);
372                    }
373                    outcome.failures.push((line_number, wrapped));
374                }
375            }
376        }
377
378        Ok(outcome)
379    }
380}
381
382/// Wrap an error with its 1-based script line number, reusing the
383/// existing [`ExecutionError::CommandFailed`] variant so adding
384/// line-number context never requires a breaking change to the error
385/// hierarchy.
386fn wrap_line_error(line_number: usize, source: DynamicCliError) -> DynamicCliError {
387    DynamicCliError::Execution(ExecutionError::CommandFailed(anyhow::anyhow!(
388        "line {}: {}",
389        line_number,
390        source
391    )))
392}
393
394/// What [`CliInterface::run_script`] does when a line fails.
395#[derive(Debug, Clone, Copy, PartialEq, Eq)]
396pub enum ScriptErrorPolicy {
397    /// Stop at the first failing line — [`run_script`][CliInterface::run_script]
398    /// returns `Err` immediately, with the lines before it already run.
399    Abort,
400    /// Record the failure and keep going —
401    /// [`run_script`][CliInterface::run_script] returns `Ok` with every
402    /// failure listed in [`ScriptOutcome::failures`].
403    Continue,
404}
405
406/// Result of a full [`CliInterface::run_script`] run.
407#[derive(Debug)]
408pub struct ScriptOutcome {
409    /// Number of non-blank, non-comment lines dispatched (attempted).
410    pub lines_executed: usize,
411    /// Number of those lines that succeeded.
412    pub lines_succeeded: usize,
413    /// `(1-based line number, wrapped error)` for every line that failed.
414    /// Always empty when `policy` was
415    /// [`ScriptErrorPolicy::Abort`][ScriptErrorPolicy::Abort] and the run
416    /// completed (an abort returns `Err` instead of populating this).
417    pub failures: Vec<(usize, DynamicCliError)>,
418}
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423    use crate::config::schema::{ArgumentDefinition, ArgumentType, CommandDefinition};
424
425    // Test context
426    #[derive(Default)]
427    struct TestContext {
428        executed_command: Option<String>,
429    }
430
431    impl ExecutionContext for TestContext {
432        fn as_any(&self) -> &dyn std::any::Any {
433            self
434        }
435
436        fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
437            self
438        }
439    }
440
441    // Test handler
442    struct TestHandler {
443        name: String,
444    }
445
446    impl crate::executor::CommandHandler for TestHandler {
447        fn execute(&self, context: &mut dyn ExecutionContext, _args: &ParsedArgs) -> Result<()> {
448            let ctx = crate::context::downcast_mut::<TestContext>(context)
449                .expect("Failed to downcast context");
450            ctx.executed_command = Some(self.name.clone());
451            Ok(())
452        }
453    }
454
455    fn create_test_registry() -> CommandRegistry {
456        let mut registry = CommandRegistry::new();
457
458        // Create a simple command definition
459        let cmd_def = CommandDefinition {
460            name: "test".to_string(),
461            aliases: vec!["t".to_string()],
462            description: "Test command".to_string(),
463            required: false,
464            arguments: vec![],
465            options: vec![],
466            implementation: "test_handler".to_string(),
467        };
468
469        let handler = Box::new(TestHandler {
470            name: "test".to_string(),
471        });
472
473        registry
474            .register_sync(cmd_def, handler)
475            .expect("Failed to register command");
476
477        registry
478    }
479
480    #[test]
481    fn test_cli_interface_creation() {
482        let registry = create_test_registry();
483        let context = Box::new(TestContext::default());
484
485        let _cli = CliInterface::new(registry, context);
486        // If this compiles and runs, creation works
487    }
488
489    #[test]
490    fn test_cli_run_simple_command() {
491        let registry = create_test_registry();
492        let context = Box::new(TestContext::default());
493        let cli = CliInterface::new(registry, context);
494
495        let result = cli.run(vec!["test".to_string()]);
496        assert!(result.is_ok());
497    }
498
499    #[test]
500    fn test_cli_run_with_alias() {
501        let registry = create_test_registry();
502        let context = Box::new(TestContext::default());
503        let cli = CliInterface::new(registry, context);
504
505        let result = cli.run(vec!["t".to_string()]);
506        assert!(result.is_ok());
507    }
508
509    #[test]
510    fn test_cli_empty_args() {
511        let registry = create_test_registry();
512        let context = Box::new(TestContext::default());
513        let cli = CliInterface::new(registry, context);
514
515        let result = cli.run(vec![]);
516        assert!(result.is_err());
517
518        match result.unwrap_err() {
519            DynamicCliError::Parse(crate::error::ParseError::InvalidSyntax { .. }) => {}
520            other => panic!("Expected InvalidSyntax error, got: {:?}", other),
521        }
522    }
523
524    #[test]
525    fn test_cli_unknown_command() {
526        let registry = create_test_registry();
527        let context = Box::new(TestContext::default());
528        let cli = CliInterface::new(registry, context);
529
530        let result = cli.run(vec!["unknown".to_string()]);
531        assert!(result.is_err());
532
533        match result.unwrap_err() {
534            DynamicCliError::Parse(crate::error::ParseError::UnknownCommand { .. }) => {}
535            other => panic!("Expected UnknownCommand error, got: {:?}", other),
536        }
537    }
538
539    #[test]
540    fn test_cli_command_with_args() {
541        let mut registry = CommandRegistry::new();
542
543        // Command with argument
544        let cmd_def = CommandDefinition {
545            name: "greet".to_string(),
546            aliases: vec![],
547            description: "Greet someone".to_string(),
548            required: false,
549            arguments: vec![ArgumentDefinition {
550                name: "name".to_string(),
551                arg_type: ArgumentType::String,
552                required: true,
553                description: "Name to greet".to_string(),
554                validation: vec![],
555                secure: false,
556            }],
557            options: vec![],
558            implementation: "greet_handler".to_string(),
559        };
560
561        struct GreetHandler;
562        impl crate::executor::CommandHandler for GreetHandler {
563            fn execute(
564                &self,
565                _context: &mut dyn ExecutionContext,
566                args: &ParsedArgs,
567            ) -> Result<()> {
568                assert_eq!(args.get_scalar("name"), Some("Alice"));
569                Ok(())
570            }
571        }
572
573        registry
574            .register_sync(cmd_def, Box::new(GreetHandler))
575            .unwrap();
576
577        let context = Box::new(TestContext::default());
578        let cli = CliInterface::new(registry, context);
579
580        let result = cli.run(vec!["greet".to_string(), "Alice".to_string()]);
581        assert!(result.is_ok());
582    }
583
584    // ========================================================================
585    // run_script tests (#41)
586    // ========================================================================
587
588    fn write_script(content: &str) -> tempfile::NamedTempFile {
589        use std::io::Write;
590        let mut file = tempfile::NamedTempFile::new().expect("failed to create temp script file");
591        file.write_all(content.as_bytes())
592            .expect("failed to write temp script file");
593        file
594    }
595
596    #[test]
597    fn test_run_script_all_lines_succeed() {
598        let registry = create_test_registry();
599        let context = Box::new(TestContext::default());
600        let cli = CliInterface::new(registry, context);
601
602        let script = write_script("test\nt\ntest\n");
603        let outcome = cli
604            .run_script(script.path(), ScriptErrorPolicy::Abort)
605            .expect("run_script should succeed when every line succeeds");
606
607        assert_eq!(outcome.lines_executed, 3);
608        assert_eq!(outcome.lines_succeeded, 3);
609        assert!(outcome.failures.is_empty());
610    }
611
612    #[test]
613    fn test_run_script_skips_blank_lines_and_comments() {
614        let registry = create_test_registry();
615        let context = Box::new(TestContext::default());
616        let cli = CliInterface::new(registry, context);
617
618        let script = write_script("# a comment\n\ntest\n   \n# another\nt\n");
619        let outcome = cli
620            .run_script(script.path(), ScriptErrorPolicy::Abort)
621            .expect("run_script should succeed");
622
623        // Only the two real command lines count.
624        assert_eq!(outcome.lines_executed, 2);
625        assert_eq!(outcome.lines_succeeded, 2);
626    }
627
628    #[test]
629    fn test_run_script_continue_policy_records_failures_and_keeps_going() {
630        let registry = create_test_registry();
631        let context = Box::new(TestContext::default());
632        let cli = CliInterface::new(registry, context);
633
634        let script = write_script("test\nunknown_command\ntest\n");
635        let outcome = cli
636            .run_script(script.path(), ScriptErrorPolicy::Continue)
637            .expect("Continue policy should return Ok even with a failing line");
638
639        assert_eq!(outcome.lines_executed, 3);
640        assert_eq!(outcome.lines_succeeded, 2);
641        assert_eq!(outcome.failures.len(), 1);
642        assert_eq!(outcome.failures[0].0, 2); // 1-based line number
643    }
644
645    #[test]
646    fn test_run_script_abort_policy_stops_at_first_failure() {
647        let registry = create_test_registry();
648        let context = Box::new(TestContext::default());
649        let cli = CliInterface::new(registry, context);
650
651        // A third "test" line would succeed if reached — it must not be.
652        let script = write_script("test\nunknown_command\ntest\n");
653        let result = cli.run_script(script.path(), ScriptErrorPolicy::Abort);
654
655        assert!(result.is_err());
656        match result.unwrap_err() {
657            DynamicCliError::Execution(ExecutionError::CommandFailed(e)) => {
658                assert!(e.to_string().contains("line 2"));
659            }
660            other => panic!("Expected wrapped CommandFailed error, got: {:?}", other),
661        }
662    }
663
664    #[test]
665    fn test_run_script_respects_quoted_tokens() {
666        let mut registry = CommandRegistry::new();
667        let cmd_def = CommandDefinition {
668            name: "greet".to_string(),
669            aliases: vec![],
670            description: "Greet someone".to_string(),
671            required: false,
672            arguments: vec![ArgumentDefinition {
673                name: "name".to_string(),
674                arg_type: ArgumentType::String,
675                required: true,
676                description: "Name to greet".to_string(),
677                validation: vec![],
678                secure: false,
679            }],
680            options: vec![],
681            implementation: "greet_handler".to_string(),
682        };
683
684        struct GreetHandler;
685        impl crate::executor::CommandHandler for GreetHandler {
686            fn execute(
687                &self,
688                _context: &mut dyn ExecutionContext,
689                args: &ParsedArgs,
690            ) -> Result<()> {
691                assert_eq!(args.get_scalar("name"), Some("Alice Wonderland"));
692                Ok(())
693            }
694        }
695
696        registry
697            .register_sync(cmd_def, Box::new(GreetHandler))
698            .unwrap();
699
700        let context = Box::new(TestContext::default());
701        let cli = CliInterface::new(registry, context);
702
703        let script = write_script(r#"greet "Alice Wonderland""#);
704        let outcome = cli
705            .run_script(script.path(), ScriptErrorPolicy::Abort)
706            .expect("quoted argument should tokenize as a single value");
707
708        assert_eq!(outcome.lines_succeeded, 1);
709    }
710
711    #[test]
712    fn test_run_script_missing_file() {
713        let registry = create_test_registry();
714        let context = Box::new(TestContext::default());
715        let cli = CliInterface::new(registry, context);
716
717        let result = cli.run_script("/nonexistent/path/to/script.txt", ScriptErrorPolicy::Abort);
718        assert!(result.is_err());
719    }
720}