diff --git a/Cargo.lock b/Cargo.lock index 452673a..b7aaa3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2119,6 +2119,7 @@ dependencies = [ "serial_test", "tempfile", "tokio", + "tokio-stream", "tracing", "tracing-subscriber", ] diff --git a/Cargo.toml b/Cargo.toml index 7cbf520..24eb5a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,10 @@ features = [ "time", ] +[dependencies.tokio-stream] +version = "0.1" +features = ["fs"] + [dev-dependencies] rstest = "~0.26" serial_test = "3.3" diff --git a/src/command.rs b/src/command.rs index 0abb65e..7e93a86 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,4 +1,4 @@ -//! Commands that are associated with external processes (commands). +//! Commands that are associated with external processes. //! //! Process based plugins are just an assortment of executable files in //! a provided directory. They are given arguments, and the response from @@ -11,10 +11,16 @@ use std::{ use bytes::Bytes; use color_eyre::{Result, eyre::eyre}; -use tokio::{fs::try_exists, process::Command, time::timeout}; +use futures::TryStreamExt; +use tokio::{ + fs::{read_dir, try_exists}, + process::Command, + time::timeout, +}; +use tokio_stream::wrappers::ReadDirStream; use tracing::{Level, event}; -/// Handle containing information about the directory containing commands. +/// Directory containing commands. #[derive(Debug)] pub struct CommandDir { command_path: PathBuf, @@ -50,6 +56,23 @@ impl CommandDir { } } + /// List available bot commands. + pub async fn list_commands(&self) -> Result> { + let contents = read_dir(&self.command_path).await?; + + Ok(ReadDirStream::new(contents) + .try_filter_map(|entry| async move { + let file_type = entry.file_type().await?; + if file_type.is_file() { + Ok(Some(entry.path())) + } else { + Ok(None) + } + }) + .try_collect::>() + .await?) + } + /// Run the given [`command_name`]. It should exist in the directory provided as /// the command_path. pub async fn run_command( @@ -101,6 +124,42 @@ mod tests { path } + #[tokio::test] + async fn test_list_commands_returns_files() { + let temp = TempDir::new().unwrap(); + create_test_script(temp.path(), "cmd_a", "#!/bin/bash\necho a"); + create_test_script(temp.path(), "cmd_b", "#!/bin/bash\necho b"); + + let cmd_dir = CommandDir::new(temp.path()); + let mut result = cmd_dir.list_commands().await.unwrap(); + result.sort(); + + assert_eq!(result.len(), 2); + assert!(result.iter().any(|p| p.ends_with("cmd_a"))); + assert!(result.iter().any(|p| p.ends_with("cmd_b"))); + } + + #[tokio::test] + async fn test_list_commands_excludes_directories() { + let temp = TempDir::new().unwrap(); + create_test_script(temp.path(), "cmd_a", "#!/bin/bash\necho a"); + fs::create_dir(temp.path().join("subdir")).unwrap(); + + let cmd_dir = CommandDir::new(temp.path()); + let result = cmd_dir.list_commands().await.unwrap(); + + assert_eq!(result.len(), 1); + assert!(result[0].ends_with("cmd_a")); + } + + #[tokio::test] + async fn test_list_commands_empty_dir() { + let temp = TempDir::new().unwrap(); + let cmd_dir = CommandDir::new(temp.path()); + let result = cmd_dir.list_commands().await.unwrap(); + assert!(result.is_empty()); + } + #[test] fn test_command_dir_new() { let dir = CommandDir::new("/some/path"); diff --git a/tests/event_test.rs b/tests/event_test.rs index e30a84b..4dc4bfe 100644 --- a/tests/event_test.rs +++ b/tests/event_test.rs @@ -16,6 +16,7 @@ fn test_socket_path(name: &str) -> String { } /// Helper to read one JSON event from a stream +#[allow(unused)] async fn read_event( reader: &mut BufReader, ) -> Result> {