Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ ureq = { version = "3.0.12", features = ["rustls"] }
tokio = { version = "1.52.3", features = ["rt-multi-thread", "macros", "sync", "process", "time"] }
async-trait = "0.1"
nix = { version = "0.31", features = ["signal"] }
base64 = "0.22"

[dev-dependencies]
ctor = "0.2"
Expand Down
101 changes: 84 additions & 17 deletions src/app/dependencies.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
use tracing::{event, Level};

use crate::{
app::errors::AppError, config::ConfigSnapshot, infrastructure::env::EnvTrait,
app::errors::AppError,
config::ConfigSnapshot,
infrastructure::{env::EnvTrait, shell::ShellTrait},
kw::readiness::{probe_kw_binary, KwVersionCheck},
render_prefs::PatchRenderer,
};

/// Verifies required and optional external binaries before the terminal starts.
///
/// A missing `b4` is a hard failure; all other missing binaries only emit
/// warnings. This keeps fatal startup failures out of terminal raw mode.
/// warnings — including `kw`, and including an unverifiable kw version,
/// since kw's own VERSION file is stale upstream (it reports `beta-0.9`
/// even at the 0.10 tag). This keeps fatal startup failures out of
/// terminal raw mode.
pub(crate) fn check_external_deps(
env: &dyn EnvTrait,
shell: &dyn ShellTrait,
config: &ConfigSnapshot,
) -> Result<(), AppError> {
if !env.which("b4") {
Expand Down Expand Up @@ -55,42 +62,76 @@ pub(crate) fn check_external_deps(
_ => {}
}

let kw = probe_kw_binary(env, shell);
if !kw.available {
event!(
Level::WARN,
"kw is not installed, kernel build/deploy won't work"
);
} else if !matches!(kw.check, KwVersionCheck::Meets) {
event!(
Level::WARN,
version = kw.version_line.as_deref().unwrap_or("unknown"),
"could not confirm kw >= 0.10; the build/deploy integration is \
verified against kw 0.10 (kw's own VERSION file may be stale)"
);
}

Ok(())
}

#[cfg(test)]
mod tests {
use crate::{
config::{ConfigState, ValidatedConfigUpdate},
infrastructure::env::MockEnvTrait,
infrastructure::{
env::MockEnvTrait,
shell::{MockShellTrait, ShellOutput},
},
render_prefs::PatchRenderer,
};

use super::*;

/// An env where every binary is present and kw reports a current
/// version, so individual tests only need to override their own case.
fn happy_env() -> (MockEnvTrait, MockShellTrait) {
let mut env = MockEnvTrait::new();
env.expect_which().returning(|_| true);
let mut shell = MockShellTrait::new();
shell.expect_execute().returning(|_| {
Ok(ShellOutput {
stdout: b"0.10.0\n".to_vec(),
stderr: Vec::new(),
success: true,
})
});
(env, shell)
}

#[test]
fn missing_b4_returns_dependencies_error() {
let mut env = MockEnvTrait::new();
env.expect_which()
.withf(|name| name == "b4")
.returning(|_| false);
let mut shell = MockShellTrait::new();
shell.expect_execute().times(0);

let err = check_external_deps(&env, &ConfigState::default().to_snapshot()).unwrap_err();
let err =
check_external_deps(&env, &shell, &ConfigState::default().to_snapshot()).unwrap_err();

assert!(matches!(err, AppError::Dependencies(_)));
}

#[test]
fn missing_git_is_not_fatal() {
let mut env = MockEnvTrait::new();
env.expect_which()
.withf(|name| name == "b4")
.returning(|_| true);
let (mut env, shell) = happy_env();
env.expect_which()
.withf(|name| name == "git")
.returning(|_| false);

let result = check_external_deps(&env, &ConfigState::default().to_snapshot());
let result = check_external_deps(&env, &shell, &ConfigState::default().to_snapshot());

assert!(result.is_ok());
}
Expand All @@ -103,18 +144,44 @@ mod tests {
..Default::default()
});

let mut env = MockEnvTrait::new();
env.expect_which()
.withf(|name| name == "b4")
.returning(|_| true);
env.expect_which()
.withf(|name| name == "git")
.returning(|_| true);
let (mut env, shell) = happy_env();
env.expect_which()
.withf(|name| name == "bat")
.returning(|_| false);

let result = check_external_deps(&env, &state.to_snapshot());
let result = check_external_deps(&env, &shell, &state.to_snapshot());

assert!(result.is_ok());
}

#[test]
fn missing_kw_is_not_fatal() {
let (mut env, mut shell) = happy_env();
env.expect_which()
.withf(|name| name == "kw")
.returning(|_| false);
// No kw on PATH: the version probe must not spawn anything.
shell.expect_execute().times(0);

let result = check_external_deps(&env, &shell, &ConfigState::default().to_snapshot());

assert!(result.is_ok());
}

#[test]
fn unverifiable_kw_version_is_not_fatal() {
let (env, mut shell) = happy_env();
// Real 0.10 installs can still report the stale beta-0.9: the floor
// check stays a warning regardless of what kw answers.
shell.expect_execute().returning(|_| {
Ok(ShellOutput {
stdout: b"beta-0.9\nBranch: master\nCommit: 3575d38\n".to_vec(),
stderr: Vec::new(),
success: true,
})
});

let result = check_external_deps(&env, &shell, &ConfigState::default().to_snapshot());

assert!(result.is_ok());
}
Expand Down
10 changes: 9 additions & 1 deletion src/infrastructure/file_system/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pub use r#trait::MockFileSystemTrait;
use std::{
fs::{self, File},
io::{self, BufReader},
path::Path,
path::{Path, PathBuf},
};

#[cfg(test)]
Expand Down Expand Up @@ -43,6 +43,14 @@ impl FileSystemTrait for OsFileSystem {
path.is_dir()
}

fn read_dir(&self, path: &Path) -> Result<Vec<PathBuf>, FileSystemError> {
let mut entries = fs::read_dir(path)?
.map(|entry| entry.map(|e| e.path()))
.collect::<Result<Vec<_>, io::Error>>()?;
entries.sort();
Ok(entries)
}

fn rename(&self, from: &Path, to: &Path) -> Result<(), FileSystemError> {
Ok(fs::rename(from, to)?)
}
Expand Down
28 changes: 28 additions & 0 deletions src/infrastructure/file_system/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,34 @@ fn is_dir_distinguishes_dirs_from_files() {
assert!(!fs.is_dir(&file_path));
}

#[test]
fn read_dir_lists_immediate_children_sorted() {
let dir = TempDir::new("read_dir");
std::fs::write(dir.path().join("b.txt"), "").unwrap();
std::fs::create_dir(dir.path().join("a_sub")).unwrap();
std::fs::create_dir(dir.path().join("a_sub/nested")).unwrap();
std::fs::write(dir.path().join("c.txt"), "").unwrap();

let fs = OsFileSystem;
let entries = fs.read_dir(dir.path()).unwrap();

assert_eq!(
entries,
vec![
dir.path().join("a_sub"),
dir.path().join("b.txt"),
dir.path().join("c.txt"),
]
);
}

#[test]
fn read_dir_returns_error_for_missing_dir() {
let fs = OsFileSystem;
let result = fs.read_dir(Path::new("/nonexistent/path"));
assert!(result.is_err());
}

#[test]
fn rename_moves_file() {
let dir = TempDir::new("rename");
Expand Down
10 changes: 9 additions & 1 deletion src/infrastructure/file_system/trait.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
use mockall::automock;
use thiserror::Error;

use std::{fs::Metadata, io, path::Path};
use std::{
fs::Metadata,
io,
path::{Path, PathBuf},
};

#[derive(Debug, Error)]
pub enum FileSystemError {
Expand All @@ -17,6 +21,10 @@ pub trait FileSystemTrait: Send + Sync {
fn exists(&self, path: &Path) -> bool;
fn is_file(&self, path: &Path) -> bool;
fn is_dir(&self, path: &Path) -> bool;
/// Returns the immediate children of directory `path` as full paths,
/// sorted for determinism. Entry kind and metadata are queried
/// separately via `is_dir`/`is_file`/`metadata`.
fn read_dir(&self, path: &Path) -> Result<Vec<PathBuf>, FileSystemError>;
fn rename(&self, from: &Path, to: &Path) -> Result<(), FileSystemError>;
fn create_writer(&self, path: &Path) -> Result<Box<dyn io::Write + Send>, FileSystemError>;
fn open_bufreader(&self, path: &Path) -> Result<Box<dyn io::BufRead + Send>, FileSystemError>;
Expand Down
Loading
Loading