diff --git a/crates/synd_term/src/application/cache/mod.rs b/crates/synd_term/src/application/cache/mod.rs index 62d81527..c455bdff 100644 --- a/crates/synd_term/src/application/cache/mod.rs +++ b/crates/synd_term/src/application/cache/mod.rs @@ -3,16 +3,30 @@ use std::{borrow::Borrow, io, path::PathBuf}; use crate::{ auth::{Credential, CredentialError, Unverified}, config, + filesystem::{fsimpl, FileSystem}, ui::components::gh_notifications::GhNotificationFilterOptions, }; -pub struct Cache { +pub struct Cache { dir: PathBuf, + fs: FS, } -impl Cache { +impl Cache { pub fn new(dir: impl Into) -> Self { - Self { dir: dir.into() } + Self::with(dir, fsimpl::FileSystem::new()) + } +} + +impl Cache +where + FS: FileSystem, +{ + pub fn with(dir: impl Into, fs: FS) -> Self { + Self { + dir: dir.into(), + fs, + } } /// Persist credential in filesystem. @@ -85,7 +99,7 @@ impl Cache { pub(crate) fn clean(&self) -> io::Result<()> { // User can specify any directory as the cache // so instead of deleting the entire directory with `remove_dir_all`, delete files individually. - match std::fs::remove_file(self.credential_file()) { + match self.fs.remove_file(self.credential_file()) { Ok(()) => Ok(()), Err(err) => match err.kind() { io::ErrorKind::NotFound => Ok(()), diff --git a/crates/synd_term/src/cli/clean.rs b/crates/synd_term/src/cli/clean.rs deleted file mode 100644 index 327fa880..00000000 --- a/crates/synd_term/src/cli/clean.rs +++ /dev/null @@ -1,56 +0,0 @@ -use std::{io::ErrorKind, path::PathBuf}; - -use anyhow::Context; -use clap::Args; - -use crate::{application::Cache, config}; - -/// Clean cache and logs -#[derive(Args, Debug)] -pub struct CleanCommand { - /// Cache directory - #[arg( - long, - default_value = config::cache::dir().to_path_buf().into_os_string(), - )] - cache_dir: PathBuf, -} - -impl CleanCommand { - #[allow(clippy::unused_self)] - pub fn run(self) -> i32 { - if let Err(err) = self.clean() { - tracing::error!("{err}"); - 1 - } else { - 0 - } - } - - fn clean(self) -> anyhow::Result<()> { - let CleanCommand { cache_dir } = self; - - let cache = Cache::new(&cache_dir); - cache - .clean() - .map_err(anyhow::Error::from) - .with_context(|| format!("path: {}", cache_dir.display()))?; - - // remove log - let log_file = config::log_path(); - match std::fs::remove_file(&log_file) { - Ok(()) => { - tracing::info!("Remove {}", log_file.display()); - } - Err(err) => match err.kind() { - ErrorKind::NotFound => {} - _ => { - return Err(anyhow::Error::from(err)) - .with_context(|| format!("path: {}", log_file.display())) - } - }, - } - - Ok(()) - } -} diff --git a/crates/synd_term/src/cli/check.rs b/crates/synd_term/src/cli/command/check.rs similarity index 93% rename from crates/synd_term/src/cli/check.rs rename to crates/synd_term/src/cli/command/check.rs index 2b7bbf61..9c3f0e3e 100644 --- a/crates/synd_term/src/cli/check.rs +++ b/crates/synd_term/src/cli/command/check.rs @@ -1,4 +1,4 @@ -use std::{io, path::Path, time::Duration}; +use std::{io, path::Path, process::ExitCode, time::Duration}; use anyhow::Context; use clap::Args; @@ -22,12 +22,12 @@ pub struct CheckCommand { impl CheckCommand { #[allow(clippy::unused_self)] - pub async fn run(self, endpoint: Url) -> i32 { + pub async fn run(self, endpoint: Url) -> ExitCode { if let Err(err) = self.check(endpoint).await { tracing::error!("{err:?}"); - 1 + ExitCode::from(1) } else { - 0 + ExitCode::SUCCESS } } diff --git a/crates/synd_term/src/cli/command/clean.rs b/crates/synd_term/src/cli/command/clean.rs new file mode 100644 index 00000000..e4942019 --- /dev/null +++ b/crates/synd_term/src/cli/command/clean.rs @@ -0,0 +1,116 @@ +use std::{ + io::ErrorKind, + path::{Path, PathBuf}, + process::ExitCode, +}; + +use anyhow::Context; +use clap::Args; + +use crate::{application::Cache, config, filesystem::FileSystem}; + +/// Clean cache and logs +#[derive(Args, Debug)] +pub struct CleanCommand { + /// Cache directory + #[arg( + long, + default_value = config::cache::dir().to_path_buf().into_os_string(), + )] + cache_dir: PathBuf, +} + +impl CleanCommand { + #[allow(clippy::unused_self)] + pub fn run(self, fs: &FS) -> ExitCode + where + FS: FileSystem + Clone, + { + ExitCode::from(self.clean(fs, config::log_path().as_path())) + } + + fn clean(self, fs: &FS, log: &Path) -> u8 + where + FS: FileSystem + Clone, + { + if let Err(err) = self.try_clean(fs, log) { + tracing::error!("{err}"); + 1 + } else { + 0 + } + } + fn try_clean(self, fs: &FS, log: &Path) -> anyhow::Result<()> + where + FS: FileSystem + Clone, + { + let CleanCommand { cache_dir } = self; + + let cache = Cache::with(&cache_dir, fs.clone()); + cache + .clean() + .map_err(anyhow::Error::from) + .with_context(|| format!("path: {}", cache_dir.display()))?; + + // remove log + match fs.remove_file(log) { + Ok(()) => { + tracing::info!("Remove {}", log.display()); + } + Err(err) => match err.kind() { + ErrorKind::NotFound => {} + _ => { + return Err(anyhow::Error::from(err)) + .with_context(|| format!("path: {}", log.display())) + } + }, + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::io; + + use tempfile::{NamedTempFile, TempDir}; + + use crate::filesystem::{fsimpl, mock::MockFileSystem}; + + use super::*; + + #[test] + fn remove_log_file() { + let clean = CleanCommand { + cache_dir: TempDir::new().unwrap().into_path(), + }; + let log_file = NamedTempFile::new().unwrap(); + let exit_code = clean.clean(&fsimpl::FileSystem::new(), log_file.path()); + assert_eq!(exit_code, 0); + assert!(!log_file.path().exists()); + } + + #[test] + fn ignore_log_file_not_found() { + let clean = CleanCommand { + cache_dir: TempDir::new().unwrap().into_path(), + }; + let log_file = Path::new("./not_exists"); + let fs = MockFileSystem::default().with_remove_errors(log_file, io::ErrorKind::NotFound); + let exit_code = clean.clean(&fs, log_file); + assert_eq!(exit_code, 0); + } + + #[test] + fn exit_code_on_permission_error() { + let clean = CleanCommand { + cache_dir: TempDir::new().unwrap().into_path(), + }; + let log_file = Path::new("./not_allowed"); + let fs = + MockFileSystem::default().with_remove_errors(log_file, io::ErrorKind::PermissionDenied); + let exit_code = clean.clean(&fs, log_file); + assert_eq!(exit_code, 1); + } +} diff --git a/crates/synd_term/src/cli/export.rs b/crates/synd_term/src/cli/command/export.rs similarity index 93% rename from crates/synd_term/src/cli/export.rs rename to crates/synd_term/src/cli/command/export.rs index 03429a12..c3bdc627 100644 --- a/crates/synd_term/src/cli/export.rs +++ b/crates/synd_term/src/cli/command/export.rs @@ -1,4 +1,4 @@ -use std::{path::PathBuf, time::Duration}; +use std::{path::PathBuf, process::ExitCode, time::Duration}; use anyhow::anyhow; use clap::Args; @@ -39,7 +39,7 @@ pub struct ExportCommand { } impl ExportCommand { - pub async fn run(self, endpoint: Url) -> i32 { + pub async fn run(self, endpoint: Url) -> ExitCode { let err = if self.print_schema { Self::print_json_schema() } else { @@ -47,9 +47,9 @@ impl ExportCommand { }; if let Err(err) = err { tracing::error!("{err:?}"); - 1 + ExitCode::from(1) } else { - 0 + ExitCode::SUCCESS } } diff --git a/crates/synd_term/src/cli/command/mod.rs b/crates/synd_term/src/cli/command/mod.rs new file mode 100644 index 00000000..7a61971e --- /dev/null +++ b/crates/synd_term/src/cli/command/mod.rs @@ -0,0 +1,3 @@ +pub mod check; +pub mod clean; +pub mod export; diff --git a/crates/synd_term/src/cli/mod.rs b/crates/synd_term/src/cli/mod.rs index 38141a29..97982cf6 100644 --- a/crates/synd_term/src/cli/mod.rs +++ b/crates/synd_term/src/cli/mod.rs @@ -5,9 +5,7 @@ use url::Url; use crate::{config, ui::theme}; -mod check; -mod clean; -mod export; +mod command; #[derive(Copy, Clone, PartialEq, Eq, Debug, clap::ValueEnum)] pub enum Palette { @@ -102,9 +100,9 @@ pub struct GithubOptions { #[derive(Subcommand, Debug)] pub enum Command { #[command(alias = "clear")] - Clean(clean::CleanCommand), - Check(check::CheckCommand), - Export(export::ExportCommand), + Clean(command::clean::CleanCommand), + Check(command::check::CheckCommand), + Export(command::export::ExportCommand), } pub fn parse() -> Args { diff --git a/crates/synd_term/src/filesystem.rs b/crates/synd_term/src/filesystem.rs new file mode 100644 index 00000000..425f94b5 --- /dev/null +++ b/crates/synd_term/src/filesystem.rs @@ -0,0 +1,53 @@ +use std::{io, path::Path}; + +pub trait FileSystem { + fn remove_file>(&self, path: P) -> io::Result<()>; +} + +pub mod fsimpl { + #[derive(Debug, Clone)] + pub struct FileSystem {} + + impl FileSystem { + pub fn new() -> Self { + Self {} + } + } + + impl super::FileSystem for FileSystem { + fn remove_file>(&self, path: P) -> std::io::Result<()> { + std::fs::remove_file(path) + } + } +} + +#[cfg(test)] +pub(crate) mod mock { + use std::{collections::HashMap, io, path::PathBuf}; + + #[derive(Default, Clone)] + pub(crate) struct MockFileSystem { + remove_errors: HashMap, + } + + impl MockFileSystem { + pub(crate) fn with_remove_errors( + mut self, + path: impl Into, + err: io::ErrorKind, + ) -> Self { + self.remove_errors.insert(path.into(), err); + self + } + } + + impl super::FileSystem for MockFileSystem { + fn remove_file>(&self, path: P) -> io::Result<()> { + let path = path.as_ref(); + match self.remove_errors.get(path) { + Some(err) => Err(io::Error::from(*err)), + None => Ok(()), + } + } + } +} diff --git a/crates/synd_term/src/lib.rs b/crates/synd_term/src/lib.rs index a172e719..d4d07ca5 100644 --- a/crates/synd_term/src/lib.rs +++ b/crates/synd_term/src/lib.rs @@ -7,6 +7,7 @@ pub mod cli; pub mod client; pub(crate) mod command; pub mod config; +pub mod filesystem; pub mod interact; pub mod job; pub mod keymap; diff --git a/crates/synd_term/src/main.rs b/crates/synd_term/src/main.rs index ca9bf236..2d4692cb 100644 --- a/crates/synd_term/src/main.rs +++ b/crates/synd_term/src/main.rs @@ -1,4 +1,4 @@ -use std::{future, path::PathBuf, time::Duration}; +use std::{future, path::PathBuf, process::ExitCode, time::Duration}; use anyhow::Context as _; use futures_util::TryFutureExt; @@ -7,6 +7,7 @@ use synd_term::{ cli::{self, ApiOptions, Args, FeedOptions, GithubOptions, Palette}, client::{github::GithubClient, Client}, config::{self, Categories}, + filesystem::fsimpl::FileSystem, terminal::{self, Terminal}, ui::theme::Theme, }; @@ -101,7 +102,7 @@ fn build_app( } #[tokio::main] -async fn main() { +async fn main() -> ExitCode { let Args { api: ApiOptions { endpoint, @@ -121,13 +122,11 @@ async fn main() { let _guard = init_tracing(log).unwrap(); if let Some(command) = command { - let exit_code = match command { - cli::Command::Clean(clean) => clean.run(), + return match command { + cli::Command::Clean(clean) => clean.run(&FileSystem::new()), cli::Command::Check(check) => check.run(endpoint).await, cli::Command::Export(export) => export.run(endpoint).await, }; - - std::process::exit(exit_code); }; let mut event_stream = terminal::event_stream(); @@ -148,6 +147,8 @@ async fn main() { .await { error!("{err:?}"); - std::process::exit(1); + ExitCode::FAILURE + } else { + ExitCode::SUCCESS } } diff --git a/justfile b/justfile index 4b9be559..1e9b2956 100644 --- a/justfile +++ b/justfile @@ -83,7 +83,7 @@ review: coverage *flags: nix run nixpkgs#cargo-llvm-cov -- llvm-cov nextest \ --all-features --open \ - --ignore-filename-regex '(integration_backend.rs|client/generated/.*.rs)' \ + --ignore-filename-regex '(integration_backend.rs|client/generated/.*.rs|synd_ebpf_task/src/.*.rs)' \ {{ flags }} [macos]