use crate::dependency_binaries::find_dependency_binary;
#[cfg(any(test, feature = "test-support"))]
use crate::dependency_binaries::{
DependencyBinary, DependencyBinaryInstallError, DependencyBinaryInstaller,
};
#[cfg(any(test, feature = "test-support"))]
use crate::dirs::BaseDirs;
use crate::error::Result;
#[cfg(any(test, feature = "test-support"))]
use crate::installer_packaging::TargetTriple;
#[cfg(any(test, feature = "test-support"))]
use crate::test_support::env_test_guard;
use crate::test_utils::{ExpectedCall, failure_output, stdout_output, success_output};
#[cfg(any(test, feature = "test-support"))]
use std::fs;
#[cfg(all(any(test, feature = "test-support"), unix))]
use std::os::unix::fs::PermissionsExt;
#[cfg(any(test, feature = "test-support"))]
use std::path::{Path, PathBuf};
use std::process::Output;
#[cfg(any(test, feature = "test-support"))]
pub struct AlwaysNotFoundRepositoryInstaller;
#[cfg(any(test, feature = "test-support"))]
impl DependencyBinaryInstaller for AlwaysNotFoundRepositoryInstaller {
fn install(
&self,
dependency: &DependencyBinary,
target: &TargetTriple,
_dirs: &dyn BaseDirs,
) -> std::result::Result<PathBuf, DependencyBinaryInstallError> {
Err(DependencyBinaryInstallError::NotFound {
url: format!(
"https://example.test/{}-{}-v{}.tgz",
dependency.package(),
target,
dependency.version()
),
})
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn write_fake_binary(path: &Path, is_executable: bool) {
write_fake_binary_with_status(path, is_executable, 0);
}
#[cfg(any(test, feature = "test-support"))]
pub fn write_fake_binary_with_status(path: &Path, is_executable: bool, exit_code: i32) {
fs::write(path, fake_binary_contents(exit_code)).expect("write fake binary");
#[cfg(unix)]
{
let mode = if is_executable { 0o755 } else { 0o644 };
let mut permissions = fs::metadata(path)
.expect("read fake binary metadata")
.permissions();
permissions.set_mode(mode);
fs::set_permissions(path, permissions).expect("set fake binary permissions");
}
#[cfg(not(unix))]
let _ = is_executable;
}
#[cfg(any(test, feature = "test-support"))]
fn fake_binary_contents(exit_code: i32) -> Vec<u8> {
#[cfg(windows)]
{
format!("@echo off\r\nexit /b {exit_code}\r\n").into_bytes()
}
#[cfg(not(windows))]
{
format!("#!/bin/sh\nexit {exit_code}\n").into_bytes()
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn with_fake_path<T>(setup: impl FnOnce(&[PathBuf]), run: impl FnOnce() -> T) -> T {
let _guard = env_test_guard();
let temp_dirs = [
tempfile::tempdir().expect("create temp dir"),
tempfile::tempdir().expect("create temp dir"),
];
let path_dirs = temp_dirs
.iter()
.map(|dir| dir.path().to_path_buf())
.collect::<Vec<_>>();
setup(&path_dirs);
let path = std::env::join_paths(path_dirs.iter().map(PathBuf::as_path))
.expect("join fake PATH directories");
temp_env::with_var("PATH", Some(path), run)
}
#[cfg(any(test, feature = "test-support"))]
pub fn with_fake_binary_on_path<T>(binary_name: &str, run: impl FnOnce() -> T) -> T {
with_fake_path(
|directories| write_fake_binary(&path_binary_location(&directories[0], binary_name), true),
run,
)
}
#[cfg(any(test, feature = "test-support"))]
pub fn path_binary_location(directory: &Path, binary_name: &str) -> PathBuf {
#[cfg(windows)]
{
directory.join(format!("{binary_name}.cmd"))
}
#[cfg(not(windows))]
{
directory.join(binary_name)
}
}
pub struct ExpectedCallConfig<'a> {
pub is_binstall_available: bool,
pub has_repository_context: bool,
pub is_repository_asset_missing: bool,
pub should_verify_repository_install: bool,
pub is_repository_verification_failing: bool,
pub cargo_binstall_failure: Option<&'a str>,
pub cargo_install_failure: Option<&'a str>,
}
pub fn binstall_version_check(is_binstall_available: bool) -> ExpectedCall {
ExpectedCall {
cmd: "cargo",
args: vec!["binstall", "--version"],
result: if is_binstall_available {
Ok(success_output())
} else {
Ok(failure_output("missing binstall"))
},
}
}
pub fn binstall_version_check_with_result(result: Result<Output>) -> ExpectedCall {
ExpectedCall {
cmd: "cargo",
args: vec!["binstall", "--version"],
result,
}
}
pub fn binstall_install(tool: &'static str, result: Result<Output>) -> ExpectedCall {
let version = dependency_version(tool);
ExpectedCall {
cmd: "cargo",
args: vec!["binstall", "-y", "--version", version, tool],
result,
}
}
pub fn cargo_install(tool: &'static str, result: Result<Output>) -> ExpectedCall {
ExpectedCall {
cmd: "cargo",
args: vec!["install", tool],
result,
}
}
fn cargo_source_install(
tool: &'static str,
version: &'static str,
result: Result<Output>,
) -> ExpectedCall {
ExpectedCall {
cmd: "cargo",
args: vec!["install", "--locked", "--version", version, tool],
result,
}
}
pub fn dependency_version(tool: &str) -> &'static str {
find_dependency_binary(tool)
.expect("dependency manifest should parse")
.map(|dependency| dependency.version())
.unwrap_or_else(|| panic!("unexpected tool: {tool}"))
}
pub fn cargo_dylint_version_output() -> Output {
stdout_output(format!(
"cargo-dylint {}\n",
dependency_version("cargo-dylint")
))
}
pub fn dylint_link_install_list_check() -> ExpectedCall {
dylint_link_install_list_check_with_version(dependency_version("dylint-link"))
}
pub fn dylint_link_install_list_check_with_version(version: &str) -> ExpectedCall {
ExpectedCall {
cmd: "cargo",
args: vec!["install", "--list"],
result: Ok(stdout_output(format!(
"dylint-link v{version}:\n dylint-link\n"
))),
}
}
pub fn repository_verification_call(tool: &str, verification_fails: bool) -> Option<ExpectedCall> {
match tool {
"cargo-dylint" => Some(ExpectedCall {
cmd: "cargo",
args: vec!["dylint", "--version"],
result: if verification_fails {
Ok(failure_output("still missing"))
} else {
Ok(cargo_dylint_version_output())
},
}),
"dylint-link" => {
let _ = verification_fails;
None
}
other => panic!("unexpected tool: {other}"),
}
}
fn tool_verification_check(tool: &str) -> Option<ExpectedCall> {
match tool {
"cargo-dylint" => Some(cargo_dylint_check()),
"dylint-link" => None,
other => panic!("unexpected tool: {other}"),
}
}
struct PostPrimaryConfig {
tool: String,
tool_static: &'static str,
has_repository_context: bool,
primary_succeeded: bool,
use_binstall: bool,
cargo_install_failure: Option<String>,
}
fn repo_aware_cargo_install(
tool: &'static str,
has_repository_context: bool,
result: Result<Output>,
) -> ExpectedCall {
if has_repository_context {
cargo_source_install(tool, dependency_version(tool), result)
} else {
cargo_install(tool, result)
}
}
fn post_primary_calls(cfg: &PostPrimaryConfig) -> Vec<ExpectedCall> {
if cfg.primary_succeeded {
return tool_verification_check(&cfg.tool).into_iter().collect();
}
if !cfg.use_binstall {
return vec![];
}
if cfg.cargo_install_failure.is_none() {
let cargo_call = repo_aware_cargo_install(
cfg.tool_static,
cfg.has_repository_context,
Ok(success_output()),
);
let mut calls = vec![cargo_call];
calls.extend(tool_verification_check(&cfg.tool));
return calls;
}
if let Some(message) = cfg.cargo_install_failure.as_deref() {
let cargo_call = repo_aware_cargo_install(
cfg.tool_static,
cfg.has_repository_context,
Ok(failure_output(message)),
);
vec![cargo_call]
} else {
vec![]
}
}
fn source_install_fallback_calls(
tool: &str,
tool_static: &'static str,
config: &ExpectedCallConfig<'_>,
) -> Vec<ExpectedCall> {
let version = dependency_version(tool);
let result = config.cargo_install_failure.map_or_else(
|| Ok(success_output()),
|message| Ok(failure_output(message)),
);
let install_call = cargo_source_install(tool_static, version, result);
if config.cargo_install_failure.is_none() {
let mut calls = vec![install_call];
calls.extend(tool_verification_check(tool));
calls
} else {
vec![install_call]
}
}
fn binstall_args_for_tool(
tool: &str,
tool_static: &'static str,
config: &ExpectedCallConfig<'_>,
) -> Vec<&'static str> {
if config.has_repository_context {
let version = dependency_version(tool);
vec!["binstall", "-y", "--version", version, tool_static]
} else {
vec!["binstall", "-y", tool_static]
}
}
pub fn cargo_fallback_calls(tool: &str, config: &ExpectedCallConfig<'_>) -> Vec<ExpectedCall> {
let tool_static: &'static str = Box::leak(tool.to_owned().into_boxed_str());
if config.is_repository_asset_missing {
return source_install_fallback_calls(tool, tool_static, config);
}
let (use_binstall, failure_message) = if config.is_binstall_available {
(true, config.cargo_binstall_failure)
} else {
(false, config.cargo_install_failure)
};
let args = if use_binstall {
binstall_args_for_tool(tool, tool_static, config)
} else {
vec!["install", tool_static]
};
let install_call = ExpectedCall {
cmd: "cargo",
args,
result: Ok(match failure_message {
Some(message) => failure_output(message),
None => success_output(),
}),
};
let mut calls = vec![install_call];
let post_config = PostPrimaryConfig {
tool: tool.to_owned(),
tool_static,
has_repository_context: config.has_repository_context,
primary_succeeded: failure_message.is_none(),
use_binstall,
cargo_install_failure: config.cargo_install_failure.map(String::from),
};
calls.extend(post_primary_calls(&post_config));
calls
}
pub fn expected_calls(tool: &str, config: ExpectedCallConfig<'_>) -> Vec<ExpectedCall> {
let mut calls = vec![binstall_version_check(config.is_binstall_available)];
if config.should_verify_repository_install {
calls.extend(repository_verification_call(
tool,
config.is_repository_verification_failing,
));
if !config.is_repository_verification_failing {
return calls;
}
}
calls.extend(cargo_fallback_calls(tool, &config));
calls
}
pub fn cargo_dylint_check() -> ExpectedCall {
ExpectedCall {
cmd: "cargo",
args: vec!["dylint", "--version"],
result: Ok(cargo_dylint_version_output()),
}
}
pub fn cargo_dylint_check_with_result(result: Result<Output>) -> ExpectedCall {
ExpectedCall {
cmd: "cargo",
args: vec!["dylint", "--version"],
result,
}
}