mirror of
https://github.com/logos-blockchain/logos-blockchain-testing.git
synced 2026-05-31 05:59:38 +00:00
test(tf): add configurable binary providers
This commit is contained in:
parent
4854942dce
commit
14fd7130be
93
Cargo.lock
generated
93
Cargo.lock
generated
@ -1073,8 +1073,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1084,9 +1086,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1283,6 +1287,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"webpki-roots 1.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1800,6 +1805,12 @@ version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "maplit"
|
||||
version = "1.0.2"
|
||||
@ -2683,6 +2694,61 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cfg_aliases",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2 0.6.2",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.3.4",
|
||||
"lru-slab",
|
||||
"rand 0.9.3",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.18",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
|
||||
dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2 0.6.2",
|
||||
"tracing",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.44"
|
||||
@ -2898,11 +2964,14 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-tls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
@ -2910,6 +2979,8 @@ dependencies = [
|
||||
"native-tls",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -2917,6 +2988,7 @@ dependencies = [
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
@ -2924,6 +2996,7 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"webpki-roots 1.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2986,6 +3059,12 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
@ -3054,6 +3133,7 @@ version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
|
||||
dependencies = [
|
||||
"web-time",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@ -3593,15 +3673,16 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"fs_extra",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_yaml",
|
||||
"sha2",
|
||||
"tempfile",
|
||||
"testing-framework-core",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-retry",
|
||||
"tracing",
|
||||
"which",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4223,6 +4304,16 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.11"
|
||||
|
||||
@ -28,7 +28,7 @@ impl LocalBinaryApp for KvEnv {
|
||||
}
|
||||
|
||||
fn local_process_spec() -> LocalProcessSpec {
|
||||
LocalProcessSpec::new("KVSTORE_NODE_BIN", "kvstore-node").with_rust_log("kvstore_node=info")
|
||||
LocalProcessSpec::new("KVSTORE_NODE_BIN").with_rust_log("kvstore_node=info")
|
||||
}
|
||||
|
||||
fn render_local_config(config: &KvNodeConfig) -> Result<Vec<u8>, DynError> {
|
||||
|
||||
@ -30,8 +30,7 @@ impl LocalBinaryApp for MetricsCounterEnv {
|
||||
}
|
||||
|
||||
fn local_process_spec() -> LocalProcessSpec {
|
||||
LocalProcessSpec::new("METRICS_COUNTER_NODE_BIN", "metrics-counter-node")
|
||||
.with_rust_log("metrics_counter_node=info")
|
||||
LocalProcessSpec::new("METRICS_COUNTER_NODE_BIN").with_rust_log("metrics_counter_node=info")
|
||||
}
|
||||
|
||||
fn render_local_config(config: &MetricsCounterNodeConfig) -> Result<Vec<u8>, DynError> {
|
||||
|
||||
@ -35,10 +35,7 @@ impl LocalDeployerEnv for NatsEnv {
|
||||
}
|
||||
|
||||
fn local_process_spec() -> Option<LocalProcessSpec> {
|
||||
Some(
|
||||
LocalProcessSpec::new("NATS_SERVER_BIN", "nats-server")
|
||||
.with_config_file("nats.conf", "-c"),
|
||||
)
|
||||
Some(LocalProcessSpec::new("NATS_SERVER_BIN").with_config_file("nats.conf", "-c"))
|
||||
}
|
||||
|
||||
fn render_local_config(config: &NatsNodeConfig) -> Result<Vec<u8>, DynError> {
|
||||
|
||||
@ -80,9 +80,7 @@ impl LocalDeployerEnv for OpenRaftKvEnv {
|
||||
}
|
||||
|
||||
fn local_process_spec() -> Option<LocalProcessSpec> {
|
||||
Some(
|
||||
LocalProcessSpec::new("OPENRAFT_KV_NODE_BIN", "openraft-kv-node").with_rust_log("info"),
|
||||
)
|
||||
Some(LocalProcessSpec::new("OPENRAFT_KV_NODE_BIN").with_rust_log("info"))
|
||||
}
|
||||
|
||||
fn render_local_config(config: &OpenRaftKvNodeConfig) -> Result<Vec<u8>, DynError> {
|
||||
|
||||
@ -28,7 +28,7 @@ impl LocalBinaryApp for PubSubEnv {
|
||||
}
|
||||
|
||||
fn local_process_spec() -> LocalProcessSpec {
|
||||
LocalProcessSpec::new("PUBSUB_NODE_BIN", "pubsub-node").with_rust_log("pubsub_node=info")
|
||||
LocalProcessSpec::new("PUBSUB_NODE_BIN").with_rust_log("pubsub_node=info")
|
||||
}
|
||||
|
||||
fn render_local_config(config: &PubSubNodeConfig) -> Result<Vec<u8>, DynError> {
|
||||
|
||||
@ -28,7 +28,7 @@ impl LocalBinaryApp for QueueEnv {
|
||||
}
|
||||
|
||||
fn local_process_spec() -> LocalProcessSpec {
|
||||
LocalProcessSpec::new("QUEUE_NODE_BIN", "queue-node").with_rust_log("queue_node=info")
|
||||
LocalProcessSpec::new("QUEUE_NODE_BIN").with_rust_log("queue_node=info")
|
||||
}
|
||||
|
||||
fn render_local_config(config: &QueueNodeConfig) -> Result<Vec<u8>, DynError> {
|
||||
|
||||
@ -15,12 +15,13 @@ workspace = true
|
||||
[dependencies]
|
||||
async-trait = "0.1"
|
||||
fs_extra = "1.3"
|
||||
reqwest = { features = ["blocking", "rustls-tls"], workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_yaml = { workspace = true }
|
||||
sha2 = "0.10"
|
||||
tempfile = { workspace = true }
|
||||
testing-framework-core = { path = "../../core" }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-retry = "0.3"
|
||||
tracing = { workspace = true }
|
||||
which = "6.0"
|
||||
|
||||
@ -1,73 +0,0 @@
|
||||
use std::{env, path::PathBuf};
|
||||
|
||||
use tracing::{debug, info};
|
||||
|
||||
pub struct BinaryConfig {
|
||||
/// Env var that overrides binary path.
|
||||
pub env_var: &'static str,
|
||||
/// Binary name expected on PATH.
|
||||
pub binary_name: &'static str,
|
||||
/// Repository-local fallback path when PATH lookup fails.
|
||||
pub fallback_path: &'static str,
|
||||
}
|
||||
|
||||
pub struct BinaryResolver;
|
||||
|
||||
impl BinaryResolver {
|
||||
#[must_use]
|
||||
pub fn resolve_path(config: &BinaryConfig) -> PathBuf {
|
||||
if let Some(path) = Self::resolve_from_env(config) {
|
||||
return path;
|
||||
}
|
||||
|
||||
if let Some(path) = Self::resolve_from_path(config.binary_name) {
|
||||
return path;
|
||||
}
|
||||
|
||||
Self::fallback_path(config.binary_name, config.fallback_path)
|
||||
}
|
||||
|
||||
fn which_on_path(bin: &str) -> Option<PathBuf> {
|
||||
let path_env = env::var_os("PATH")?;
|
||||
env::split_paths(&path_env)
|
||||
.map(|p| p.join(bin))
|
||||
.find(|candidate| candidate.is_file())
|
||||
}
|
||||
|
||||
fn resolve_from_env(config: &BinaryConfig) -> Option<PathBuf> {
|
||||
let path = env::var_os(config.env_var).map(PathBuf::from)?;
|
||||
|
||||
info!(
|
||||
env = config.env_var,
|
||||
binary = config.binary_name,
|
||||
path = %path.display(),
|
||||
"resolved binary from env override"
|
||||
);
|
||||
|
||||
Some(path)
|
||||
}
|
||||
|
||||
fn resolve_from_path(binary_name: &str) -> Option<PathBuf> {
|
||||
let path = Self::which_on_path(binary_name)?;
|
||||
|
||||
info!(
|
||||
binary = binary_name,
|
||||
path = %path.display(),
|
||||
"resolved binary from PATH"
|
||||
);
|
||||
|
||||
Some(path)
|
||||
}
|
||||
|
||||
fn fallback_path(binary_name: &str, fallback_path: &str) -> PathBuf {
|
||||
let fallback = PathBuf::from(fallback_path);
|
||||
|
||||
debug!(
|
||||
binary = binary_name,
|
||||
path = %fallback.display(),
|
||||
"falling back to binary path"
|
||||
);
|
||||
|
||||
fallback
|
||||
}
|
||||
}
|
||||
32
testing-framework/deployers/local/src/binary/cache.rs
Normal file
32
testing-framework/deployers/local/src/binary/cache.rs
Normal file
@ -0,0 +1,32 @@
|
||||
//! Per-process cache for resolved binary paths.
|
||||
//!
|
||||
//! Provider resolution can involve filesystem scans, downloads, or Cargo
|
||||
//! builds. The local runner may render launch specs repeatedly during one test
|
||||
//! process, so successful resolutions are cached by the expanded request key.
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::PathBuf,
|
||||
sync::{Mutex, OnceLock},
|
||||
};
|
||||
|
||||
static RESOLVED_BINARIES: OnceLock<Mutex<HashMap<String, PathBuf>>> = OnceLock::new();
|
||||
|
||||
/// Shared in-process cache for provider results.
|
||||
pub(super) struct BinaryCache;
|
||||
|
||||
impl BinaryCache {
|
||||
pub(super) fn get(key: &str) -> Option<PathBuf> {
|
||||
let cache = RESOLVED_BINARIES.get_or_init(|| Mutex::new(HashMap::new()));
|
||||
let guard = cache.lock().expect("binary cache mutex poisoned");
|
||||
|
||||
guard.get(key).cloned()
|
||||
}
|
||||
|
||||
pub(super) fn insert(key: String, path: PathBuf) {
|
||||
let cache = RESOLVED_BINARIES.get_or_init(|| Mutex::new(HashMap::new()));
|
||||
let mut guard = cache.lock().expect("binary cache mutex poisoned");
|
||||
|
||||
guard.insert(key, path);
|
||||
}
|
||||
}
|
||||
70
testing-framework/deployers/local/src/binary/lock.rs
Normal file
70
testing-framework/deployers/local/src/binary/lock.rs
Normal file
@ -0,0 +1,70 @@
|
||||
//! Cross-process lock used by providers that materialize binaries.
|
||||
//!
|
||||
//! Cargo builds and downloads can be requested by multiple local test
|
||||
//! processes at the same time. The lock keeps those processes from writing the
|
||||
//! same target/cache path concurrently.
|
||||
|
||||
use std::{
|
||||
fs, io,
|
||||
path::{Path, PathBuf},
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use super::types::BinaryProviderError;
|
||||
|
||||
const LOCK_RETRY_DELAY: Duration = Duration::from_millis(200);
|
||||
const LOCK_TIMEOUT: Duration = Duration::from_secs(10 * 60);
|
||||
|
||||
/// File-backed lock removed automatically when dropped.
|
||||
pub(super) struct BinaryProviderLock {
|
||||
/// Path of the lock file currently owned by this process.
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl BinaryProviderLock {
|
||||
pub(super) fn acquire(path: &Path) -> Result<Self, BinaryProviderError> {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent).map_err(|source| BinaryProviderError::Io {
|
||||
path: parent.to_owned(),
|
||||
source,
|
||||
})?;
|
||||
}
|
||||
|
||||
let started = Instant::now();
|
||||
loop {
|
||||
match fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(path)
|
||||
{
|
||||
Ok(_) => {
|
||||
return Ok(Self {
|
||||
path: path.to_owned(),
|
||||
});
|
||||
}
|
||||
Err(source) if source.kind() == io::ErrorKind::AlreadyExists => {
|
||||
if started.elapsed() >= LOCK_TIMEOUT {
|
||||
return Err(BinaryProviderError::LockTimeout {
|
||||
path: path.to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
thread::sleep(LOCK_RETRY_DELAY);
|
||||
}
|
||||
Err(source) => {
|
||||
return Err(BinaryProviderError::Io {
|
||||
path: path.to_owned(),
|
||||
source,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for BinaryProviderLock {
|
||||
fn drop(&mut self) {
|
||||
drop(fs::remove_file(&self.path));
|
||||
}
|
||||
}
|
||||
64
testing-framework/deployers/local/src/binary/mod.rs
Normal file
64
testing-framework/deployers/local/src/binary/mod.rs
Normal file
@ -0,0 +1,64 @@
|
||||
//! Local binary resolution for process-based test deployments.
|
||||
//!
|
||||
//! A local node process has exactly one binary provider selected at launch
|
||||
//! time. That provider owns its configuration and returns the executable path
|
||||
//! used by [`LocalProcessSpec`](crate::LocalProcessSpec).
|
||||
|
||||
mod cache;
|
||||
mod lock;
|
||||
mod providers;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod types;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use cache::BinaryCache;
|
||||
pub(super) use types::optional_path_display;
|
||||
pub use types::{
|
||||
BinaryProviderError, BinaryProviderRef, BuildBinaryProvider, BuildCommand,
|
||||
DownloadBinaryProvider, DownloadChecksum, DownloadUrl, EnvBinaryProvider,
|
||||
FallbackBinaryProvider, PathBinaryProvider,
|
||||
};
|
||||
|
||||
/// Resolves an executable path for a local process.
|
||||
///
|
||||
/// Implementations return `Ok(None)` when they are valid but cannot resolve a
|
||||
/// binary in the current environment. The default [`resolve`](Self::resolve)
|
||||
/// method turns that into a launch error, while [`FallbackBinaryProvider`] uses
|
||||
/// it to try several providers in order.
|
||||
pub trait BinaryProvider: Send + Sync {
|
||||
fn try_resolve(&self) -> Result<Option<PathBuf>, BinaryProviderError>;
|
||||
|
||||
fn display(&self) -> String;
|
||||
|
||||
fn cache_key(&self) -> String;
|
||||
|
||||
/// Resolves this provider into an executable path.
|
||||
///
|
||||
/// Resolution is cached per process so repeated node starts using the same
|
||||
/// provider config do not rebuild, redownload, or rediscover the same
|
||||
/// binary.
|
||||
fn resolve(&self) -> Result<PathBuf, BinaryProviderError> {
|
||||
let cache_key = self.cache_key();
|
||||
|
||||
if let Some(path) = BinaryCache::get(&cache_key) {
|
||||
return Ok(path);
|
||||
}
|
||||
|
||||
let path = self.resolve_uncached()?;
|
||||
BinaryCache::insert(cache_key, path.clone());
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn resolve_uncached(&self) -> Result<PathBuf, BinaryProviderError> {
|
||||
if let Some(path) = self.try_resolve()? {
|
||||
return Ok(path);
|
||||
}
|
||||
|
||||
Err(BinaryProviderError::NotFound {
|
||||
provider: self.display(),
|
||||
})
|
||||
}
|
||||
}
|
||||
115
testing-framework/deployers/local/src/binary/providers/build.rs
Normal file
115
testing-framework/deployers/local/src/binary/providers/build.rs
Normal file
@ -0,0 +1,115 @@
|
||||
//! Command build provider.
|
||||
//!
|
||||
//! This provider delegates binary preparation to a command supplied by the test
|
||||
//! or app integration. The command may run Cargo, fetch from an internal cache,
|
||||
//! invoke a project-specific build script, or do anything else needed to
|
||||
//! produce the expected executable path.
|
||||
|
||||
use std::{
|
||||
env,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use tracing::info;
|
||||
|
||||
use crate::binary::{
|
||||
BinaryProvider, BinaryProviderError, BuildBinaryProvider, lock::BinaryProviderLock,
|
||||
optional_path_display,
|
||||
};
|
||||
|
||||
impl BinaryProvider for BuildBinaryProvider {
|
||||
fn try_resolve(&self) -> Result<Option<PathBuf>, BinaryProviderError> {
|
||||
let output_path = self.output_path();
|
||||
let _lock = BinaryProviderLock::acquire(&self.lock_path())?;
|
||||
|
||||
self.run_build()?;
|
||||
self.ensure_output_exists(&output_path)?;
|
||||
|
||||
Ok(Some(output_path))
|
||||
}
|
||||
|
||||
fn display(&self) -> String {
|
||||
"build".to_owned()
|
||||
}
|
||||
|
||||
fn cache_key(&self) -> String {
|
||||
format!(
|
||||
"build:{}:{}:{}",
|
||||
self.command.display(),
|
||||
self.output_path.display(),
|
||||
optional_path_display(&self.working_dir)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl BuildBinaryProvider {
|
||||
fn run_build(&self) -> Result<(), BinaryProviderError> {
|
||||
info!(
|
||||
command = self.command.display(),
|
||||
workspace = %self.workspace_dir().display(),
|
||||
"building binary"
|
||||
);
|
||||
|
||||
let status = self
|
||||
.command()
|
||||
.status()
|
||||
.map_err(|source| BinaryProviderError::Io {
|
||||
path: self.workspace_dir(),
|
||||
source,
|
||||
})?;
|
||||
|
||||
if !status.success() {
|
||||
return Err(BinaryProviderError::BuildFailed {
|
||||
status: status.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_output_exists(&self, output_path: &Path) -> Result<(), BinaryProviderError> {
|
||||
if output_path.is_file() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(BinaryProviderError::MissingBuildOutput {
|
||||
path: output_path.to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
fn command(&self) -> Command {
|
||||
let mut command = Command::new(&self.command.program);
|
||||
command
|
||||
.current_dir(self.workspace_dir())
|
||||
.args(&self.command.args);
|
||||
command
|
||||
}
|
||||
|
||||
fn output_path(&self) -> PathBuf {
|
||||
if self.output_path.is_absolute() {
|
||||
return self.output_path.clone();
|
||||
}
|
||||
|
||||
self.workspace_dir().join(&self.output_path)
|
||||
}
|
||||
|
||||
fn lock_path(&self) -> PathBuf {
|
||||
let lock_file_name = self
|
||||
.output_path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or("binary");
|
||||
|
||||
self.lock_dir
|
||||
.clone()
|
||||
.unwrap_or_else(|| self.workspace_dir().join(".tf-binaries"))
|
||||
.join(format!("{lock_file_name}.lock"))
|
||||
}
|
||||
|
||||
fn workspace_dir(&self) -> PathBuf {
|
||||
self.working_dir
|
||||
.clone()
|
||||
.unwrap_or_else(|| env::current_dir().unwrap_or_else(|_| PathBuf::from(".")))
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,169 @@
|
||||
//! Download provider.
|
||||
//!
|
||||
//! This provider fetches an executable into a local cache, optionally validates
|
||||
//! a SHA-256 checksum, and marks the downloaded file executable on Unix.
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt as _;
|
||||
use std::{
|
||||
collections::hash_map::DefaultHasher,
|
||||
env, fs,
|
||||
hash::{Hash as _, Hasher as _},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use reqwest::blocking;
|
||||
use sha2::{Digest as _, Sha256};
|
||||
use tracing::info;
|
||||
|
||||
use crate::binary::{
|
||||
BinaryProvider, BinaryProviderError, DownloadBinaryProvider, DownloadUrl,
|
||||
lock::BinaryProviderLock, optional_path_display,
|
||||
};
|
||||
|
||||
impl BinaryProvider for DownloadBinaryProvider {
|
||||
fn try_resolve(&self) -> Result<Option<PathBuf>, BinaryProviderError> {
|
||||
let url = self.url.resolve()?;
|
||||
let path = self.cached_binary_path(&url)?;
|
||||
let _lock = BinaryProviderLock::acquire(&self.lock_path(&url))?;
|
||||
|
||||
if path.is_file() {
|
||||
return Ok(Some(path));
|
||||
}
|
||||
|
||||
let bytes = self.download_bytes(&url)?;
|
||||
self.verify_checksum(&path, &bytes)?;
|
||||
self.write_binary(&path, &bytes)?;
|
||||
|
||||
Ok(Some(path))
|
||||
}
|
||||
|
||||
fn display(&self) -> String {
|
||||
"download".to_owned()
|
||||
}
|
||||
|
||||
fn cache_key(&self) -> String {
|
||||
format!(
|
||||
"download:{}:{}",
|
||||
self.url.cache_key(),
|
||||
optional_path_display(&self.cache_dir)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl DownloadUrl {
|
||||
fn cache_key(&self) -> String {
|
||||
match self {
|
||||
Self::Fixed(url) => url.clone(),
|
||||
Self::Env(env_var) => format!("env:{env_var}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DownloadBinaryProvider {
|
||||
fn cached_binary_path(&self, url: &str) -> Result<PathBuf, BinaryProviderError> {
|
||||
let cache_dir = self.cache_dir();
|
||||
fs::create_dir_all(&cache_dir).map_err(|source| BinaryProviderError::Io {
|
||||
path: cache_dir.clone(),
|
||||
source,
|
||||
})?;
|
||||
|
||||
Ok(cache_dir.join(self.download_file_name(url)))
|
||||
}
|
||||
|
||||
fn download_bytes(&self, url: &str) -> Result<Vec<u8>, BinaryProviderError> {
|
||||
info!(url, "downloading binary");
|
||||
|
||||
blocking::get(url)
|
||||
.map_err(|source| BinaryProviderError::Download {
|
||||
url: url.to_owned(),
|
||||
source,
|
||||
})?
|
||||
.error_for_status()
|
||||
.map_err(|source| BinaryProviderError::Download {
|
||||
url: url.to_owned(),
|
||||
source,
|
||||
})?
|
||||
.bytes()
|
||||
.map(|bytes| bytes.to_vec())
|
||||
.map_err(|source| BinaryProviderError::Download {
|
||||
url: url.to_owned(),
|
||||
source,
|
||||
})
|
||||
}
|
||||
|
||||
fn write_binary(&self, path: &Path, bytes: &[u8]) -> Result<(), BinaryProviderError> {
|
||||
fs::write(path, bytes).map_err(|source| BinaryProviderError::Io {
|
||||
path: path.to_owned(),
|
||||
source,
|
||||
})?;
|
||||
|
||||
self.make_executable(path)
|
||||
}
|
||||
|
||||
fn verify_checksum(&self, path: &Path, bytes: &[u8]) -> Result<(), BinaryProviderError> {
|
||||
let Some(expected) = self.sha256.as_ref().and_then(|checksum| checksum.resolve()) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let actual = self.encode_sha256(bytes);
|
||||
if expected == actual {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(BinaryProviderError::ChecksumMismatch {
|
||||
path: path.to_owned(),
|
||||
expected,
|
||||
actual,
|
||||
})
|
||||
}
|
||||
|
||||
fn cache_dir(&self) -> PathBuf {
|
||||
self.cache_dir.clone().unwrap_or_else(|| {
|
||||
env::current_dir()
|
||||
.unwrap_or_else(|_| PathBuf::from("."))
|
||||
.join("target")
|
||||
.join(".tf-binaries")
|
||||
})
|
||||
}
|
||||
|
||||
fn lock_path(&self, url: &str) -> PathBuf {
|
||||
self.cache_dir()
|
||||
.join(format!("{}.lock", self.download_file_name(url)))
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn make_executable(&self, path: &Path) -> Result<(), BinaryProviderError> {
|
||||
let mut permissions = fs::metadata(path)
|
||||
.map_err(|source| BinaryProviderError::Io {
|
||||
path: path.to_owned(),
|
||||
source,
|
||||
})?
|
||||
.permissions();
|
||||
|
||||
permissions.set_mode(0o755);
|
||||
fs::set_permissions(path, permissions).map_err(|source| BinaryProviderError::Io {
|
||||
path: path.to_owned(),
|
||||
source,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn make_executable(&self, _path: &Path) -> Result<(), BinaryProviderError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn encode_sha256(&self, bytes: &[u8]) -> String {
|
||||
Sha256::digest(bytes)
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn download_file_name(&self, url: &str) -> String {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
url.hash(&mut hasher);
|
||||
|
||||
format!("binary-{:x}", hasher.finish())
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
//! Env-var provider.
|
||||
//!
|
||||
//! This provider is the explicit override path: the configured env var must
|
||||
//! contain a concrete executable path. Missing or non-file values are treated
|
||||
//! as unresolved so a fallback provider can continue.
|
||||
|
||||
use std::{env, path::PathBuf};
|
||||
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::binary::{BinaryProvider, BinaryProviderError, EnvBinaryProvider};
|
||||
|
||||
impl BinaryProvider for EnvBinaryProvider {
|
||||
fn try_resolve(&self) -> Result<Option<PathBuf>, BinaryProviderError> {
|
||||
let Some(path) = env::var_os(&self.env_var).map(PathBuf::from) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if !path.is_file() {
|
||||
debug!(
|
||||
env = self.env_var,
|
||||
path = %path.display(),
|
||||
"binary env override does not point to a file"
|
||||
);
|
||||
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
info!(
|
||||
env = self.env_var,
|
||||
path = %path.display(),
|
||||
"resolved binary from env override"
|
||||
);
|
||||
|
||||
Ok(Some(path))
|
||||
}
|
||||
|
||||
fn display(&self) -> String {
|
||||
"env".to_owned()
|
||||
}
|
||||
|
||||
fn cache_key(&self) -> String {
|
||||
format!("env:{}", self.env_var)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
//! Fallback provider.
|
||||
//!
|
||||
//! A fallback is still a single configured provider from the process launch
|
||||
//! perspective. It simply composes several concrete providers and returns the
|
||||
//! first executable path they can produce.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::binary::{BinaryProvider, BinaryProviderError, FallbackBinaryProvider};
|
||||
|
||||
impl BinaryProvider for FallbackBinaryProvider {
|
||||
fn try_resolve(&self) -> Result<Option<PathBuf>, BinaryProviderError> {
|
||||
for provider in &self.providers {
|
||||
if let Some(path) = provider.try_resolve()? {
|
||||
return Ok(Some(path));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn display(&self) -> String {
|
||||
self.providers
|
||||
.iter()
|
||||
.map(|provider| provider.display())
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
}
|
||||
|
||||
fn cache_key(&self) -> String {
|
||||
self.providers
|
||||
.iter()
|
||||
.map(|provider| provider.cache_key())
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
//! Provider execution implementations.
|
||||
|
||||
mod build;
|
||||
mod download;
|
||||
mod env;
|
||||
mod fallback;
|
||||
mod path;
|
||||
@ -0,0 +1,40 @@
|
||||
//! Explicit path provider.
|
||||
//!
|
||||
//! This provider uses an absolute executable path supplied by the test or app
|
||||
//! integration. It does not search the filesystem or inspect the process
|
||||
//! `PATH`, which keeps CI and mixed-version cluster setups deterministic.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use tracing::info;
|
||||
|
||||
use crate::binary::{BinaryProvider, BinaryProviderError, PathBinaryProvider};
|
||||
|
||||
impl BinaryProvider for PathBinaryProvider {
|
||||
fn try_resolve(&self) -> Result<Option<PathBuf>, BinaryProviderError> {
|
||||
if !self.path.is_absolute() {
|
||||
return Err(BinaryProviderError::RelativePath {
|
||||
path: self.path.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
if !self.path.is_file() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
info!(
|
||||
path = %self.path.display(),
|
||||
"resolved binary from configured path"
|
||||
);
|
||||
|
||||
Ok(Some(self.path.clone()))
|
||||
}
|
||||
|
||||
fn display(&self) -> String {
|
||||
"path".to_owned()
|
||||
}
|
||||
|
||||
fn cache_key(&self) -> String {
|
||||
format!("path:{}", self.path.display())
|
||||
}
|
||||
}
|
||||
200
testing-framework/deployers/local/src/binary/tests.rs
Normal file
200
testing-framework/deployers/local/src/binary/tests.rs
Normal file
@ -0,0 +1,200 @@
|
||||
use std::{
|
||||
fs,
|
||||
io::{Read as _, Write as _},
|
||||
net::TcpListener,
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
thread,
|
||||
};
|
||||
|
||||
use sha2::{Digest as _, Sha256};
|
||||
use tempfile::TempDir;
|
||||
|
||||
use super::{
|
||||
BinaryProvider, BinaryProviderError, BinaryProviderRef, BuildBinaryProvider, BuildCommand,
|
||||
DownloadBinaryProvider, DownloadChecksum, DownloadUrl, FallbackBinaryProvider,
|
||||
PathBinaryProvider,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn resolves_configured_absolute_path() {
|
||||
let temp = TempDir::new().expect("temp dir");
|
||||
let binary = temp.path().join("node");
|
||||
write_file(&binary, b"binary");
|
||||
|
||||
let path = PathBinaryProvider::new(&binary)
|
||||
.resolve()
|
||||
.expect("path provider resolves");
|
||||
|
||||
assert_eq!(path, binary);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_relative_configured_path() {
|
||||
let error = PathBinaryProvider::new("relative-node")
|
||||
.resolve()
|
||||
.expect_err("relative path is rejected");
|
||||
|
||||
assert!(matches!(error, BinaryProviderError::RelativePath { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_first_available_fallback_provider() {
|
||||
let temp = TempDir::new().expect("temp dir");
|
||||
let binary = temp.path().join("node");
|
||||
write_file(&binary, b"binary");
|
||||
|
||||
let providers: Vec<BinaryProviderRef> = vec![
|
||||
Arc::new(PathBinaryProvider::new(temp.path().join("missing-node"))),
|
||||
Arc::new(PathBinaryProvider::new(&binary)),
|
||||
];
|
||||
let provider = FallbackBinaryProvider::new(providers);
|
||||
let path = provider.resolve().expect("fallback provider resolves");
|
||||
|
||||
assert_eq!(path, binary);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runs_build_command_and_returns_output_path() {
|
||||
let temp = TempDir::new().expect("temp dir");
|
||||
let output = temp.path().join("built-node");
|
||||
let script = temp.path().join("build.sh");
|
||||
write_file(
|
||||
&script,
|
||||
format!("#!/bin/sh\nprintf built > '{}'\n", output.display()).as_bytes(),
|
||||
);
|
||||
|
||||
let provider = BuildBinaryProvider {
|
||||
command: BuildCommand::new("sh").with_args([script.to_string_lossy().to_string()]),
|
||||
output_path: output.clone(),
|
||||
working_dir: Some(temp.path().to_owned()),
|
||||
lock_dir: Some(temp.path().join("locks")),
|
||||
};
|
||||
let path = provider.resolve().expect("build provider resolves");
|
||||
|
||||
assert_eq!(path, output);
|
||||
assert_eq!(fs::read(path).expect("built file"), b"built");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_provider_runs_even_when_output_exists() {
|
||||
let temp = TempDir::new().expect("temp dir");
|
||||
let output = temp.path().join("built-node");
|
||||
let script = temp.path().join("build.sh");
|
||||
write_file(&output, b"old");
|
||||
write_file(
|
||||
&script,
|
||||
format!("#!/bin/sh\nprintf new > '{}'\n", output.display()).as_bytes(),
|
||||
);
|
||||
|
||||
let provider = BuildBinaryProvider {
|
||||
command: BuildCommand::new("sh").with_args([script.to_string_lossy().to_string()]),
|
||||
output_path: output.clone(),
|
||||
working_dir: Some(temp.path().to_owned()),
|
||||
lock_dir: Some(temp.path().join("locks")),
|
||||
};
|
||||
let path = provider.resolve().expect("build provider resolves");
|
||||
|
||||
assert_eq!(path, output);
|
||||
assert_eq!(fs::read(path).expect("built file"), b"new");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_when_build_command_does_not_create_output() {
|
||||
let temp = TempDir::new().expect("temp dir");
|
||||
let output = temp.path().join("missing-node");
|
||||
let provider = BuildBinaryProvider {
|
||||
command: BuildCommand::new("sh").with_args(["-c", "true"]),
|
||||
output_path: output,
|
||||
working_dir: Some(temp.path().to_owned()),
|
||||
lock_dir: Some(temp.path().join("locks")),
|
||||
};
|
||||
|
||||
let error = provider
|
||||
.resolve()
|
||||
.expect_err("missing build output is rejected");
|
||||
|
||||
assert!(matches!(
|
||||
error,
|
||||
BinaryProviderError::MissingBuildOutput { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn downloads_binary_from_minimal_http_server() {
|
||||
let temp = TempDir::new().expect("temp dir");
|
||||
let body = b"downloaded-node";
|
||||
let server = SingleResponseServer::start(body);
|
||||
|
||||
let provider = DownloadBinaryProvider {
|
||||
url: DownloadUrl::Fixed(server.url()),
|
||||
sha256: Some(DownloadChecksum::Fixed(sha256_hex(body))),
|
||||
cache_dir: Some(temp.path().join("cache")),
|
||||
};
|
||||
let path = provider.resolve().expect("download provider resolves");
|
||||
|
||||
assert_eq!(fs::read(path).expect("downloaded file"), body);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_download_checksum_mismatch() {
|
||||
let temp = TempDir::new().expect("temp dir");
|
||||
let server = SingleResponseServer::start(b"downloaded-node");
|
||||
let provider = DownloadBinaryProvider {
|
||||
url: DownloadUrl::Fixed(server.url()),
|
||||
sha256: Some(DownloadChecksum::Fixed("00".repeat(32))),
|
||||
cache_dir: Some(temp.path().join("cache")),
|
||||
};
|
||||
|
||||
let error = provider
|
||||
.resolve()
|
||||
.expect_err("checksum mismatch is rejected");
|
||||
|
||||
assert!(matches!(
|
||||
error,
|
||||
BinaryProviderError::ChecksumMismatch { .. }
|
||||
));
|
||||
}
|
||||
|
||||
fn write_file(path: &Path, contents: &[u8]) {
|
||||
fs::write(path, contents).expect("write file");
|
||||
}
|
||||
|
||||
fn sha256_hex(bytes: &[u8]) -> String {
|
||||
Sha256::digest(bytes)
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
struct SingleResponseServer {
|
||||
addr: String,
|
||||
}
|
||||
|
||||
impl SingleResponseServer {
|
||||
fn start(body: &'static [u8]) -> Self {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").expect("bind test http server");
|
||||
let addr = listener.local_addr().expect("server addr").to_string();
|
||||
|
||||
thread::spawn(move || {
|
||||
let (mut stream, _) = listener.accept().expect("accept one request");
|
||||
let mut buffer = [0; 1024];
|
||||
let _ = stream.read(&mut buffer);
|
||||
let response = format!(
|
||||
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
||||
body.len()
|
||||
);
|
||||
|
||||
stream
|
||||
.write_all(response.as_bytes())
|
||||
.expect("write headers");
|
||||
stream.write_all(body).expect("write body");
|
||||
});
|
||||
|
||||
Self { addr }
|
||||
}
|
||||
|
||||
fn url(&self) -> String {
|
||||
format!("http://{}/binary", self.addr)
|
||||
}
|
||||
}
|
||||
271
testing-framework/deployers/local/src/binary/types.rs
Normal file
271
testing-framework/deployers/local/src/binary/types.rs
Normal file
@ -0,0 +1,271 @@
|
||||
use std::{env, fmt, iter, path::PathBuf, sync::Arc};
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// Shared provider handle used by local process specs.
|
||||
pub type BinaryProviderRef = Arc<dyn crate::binary::BinaryProvider>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FallbackBinaryProvider {
|
||||
/// Providers tried from first to last.
|
||||
///
|
||||
/// This is useful when a test wants "prefer explicit override, otherwise
|
||||
/// build/download" behavior while still configuring one provider on the
|
||||
/// process spec.
|
||||
pub providers: Vec<BinaryProviderRef>,
|
||||
}
|
||||
|
||||
impl FallbackBinaryProvider {
|
||||
#[must_use]
|
||||
pub fn new(providers: impl IntoIterator<Item = BinaryProviderRef>) -> Self {
|
||||
Self {
|
||||
providers: providers.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for FallbackBinaryProvider {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter
|
||||
.debug_struct("FallbackBinaryProvider")
|
||||
.field(
|
||||
"providers",
|
||||
&self
|
||||
.providers
|
||||
.iter()
|
||||
.map(|provider| provider.display())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct EnvBinaryProvider {
|
||||
/// Env var expected to contain a full executable path.
|
||||
pub env_var: String,
|
||||
}
|
||||
|
||||
impl EnvBinaryProvider {
|
||||
#[must_use]
|
||||
pub fn new(env_var: impl Into<String>) -> Self {
|
||||
Self {
|
||||
env_var: env_var.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PathBinaryProvider {
|
||||
/// Absolute executable path selected by the test/app config.
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
impl PathBinaryProvider {
|
||||
#[must_use]
|
||||
pub fn new(path: impl Into<PathBuf>) -> Self {
|
||||
Self { path: path.into() }
|
||||
}
|
||||
}
|
||||
|
||||
/// User-supplied command used by [`BuildBinaryProvider`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BuildCommand {
|
||||
/// Program executed by the build provider.
|
||||
pub program: String,
|
||||
/// Arguments passed to `program`.
|
||||
pub args: Vec<String>,
|
||||
}
|
||||
|
||||
impl BuildCommand {
|
||||
#[must_use]
|
||||
pub fn new(program: impl Into<String>) -> Self {
|
||||
Self {
|
||||
program: program.into(),
|
||||
args: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_args(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
|
||||
self.args = args.into_iter().map(Into::into).collect();
|
||||
self
|
||||
}
|
||||
|
||||
pub(super) fn display(&self) -> String {
|
||||
iter::once(self.program.as_str())
|
||||
.chain(self.args.iter().map(String::as_str))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BuildBinaryProvider {
|
||||
/// Command that prepares the executable.
|
||||
///
|
||||
/// The command is intentionally not Cargo-specific. Tests can point this at
|
||||
/// a shell script, Make target, Cargo invocation, remote cache fetch, or
|
||||
/// any other project-specific build step.
|
||||
pub command: BuildCommand,
|
||||
/// Executable path expected to exist after `command` completes.
|
||||
pub output_path: PathBuf,
|
||||
/// Working directory for the build command.
|
||||
///
|
||||
/// If unset, the current process directory is used.
|
||||
pub working_dir: Option<PathBuf>,
|
||||
/// Directory used for the build lock file.
|
||||
///
|
||||
/// If unset, the lock is placed under `.tf-binaries` in `working_dir`.
|
||||
pub lock_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl BuildBinaryProvider {
|
||||
#[must_use]
|
||||
pub fn new(command: BuildCommand, output_path: impl Into<PathBuf>) -> Self {
|
||||
Self {
|
||||
command,
|
||||
output_path: output_path.into(),
|
||||
working_dir: None,
|
||||
lock_dir: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DownloadBinaryProvider {
|
||||
/// Download source used to fetch the executable.
|
||||
pub url: DownloadUrl,
|
||||
/// Optional SHA-256 checksum validated before the binary is accepted.
|
||||
pub sha256: Option<DownloadChecksum>,
|
||||
/// Directory used to store downloaded binaries and provider lock files.
|
||||
///
|
||||
/// If unset, downloads are cached under `target/.tf-binaries` in the
|
||||
/// current process directory.
|
||||
pub cache_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl DownloadBinaryProvider {
|
||||
#[must_use]
|
||||
pub fn from_url(url: impl Into<String>) -> Self {
|
||||
Self {
|
||||
url: DownloadUrl::Fixed(url.into()),
|
||||
sha256: None,
|
||||
cache_dir: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum DownloadUrl {
|
||||
/// Fixed URL embedded in the provider config.
|
||||
Fixed(String),
|
||||
/// Env var containing the URL.
|
||||
Env(String),
|
||||
}
|
||||
|
||||
impl DownloadUrl {
|
||||
pub(super) fn resolve(&self) -> Result<String, BinaryProviderError> {
|
||||
match self {
|
||||
Self::Fixed(url) => Ok(url.clone()),
|
||||
Self::Env(env_var) => {
|
||||
env::var(env_var).map_err(|_| BinaryProviderError::MissingDownloadUrl {
|
||||
env_var: env_var.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum DownloadChecksum {
|
||||
/// Fixed expected SHA-256 checksum.
|
||||
Fixed(String),
|
||||
/// Env var containing the expected SHA-256 checksum.
|
||||
Env(String),
|
||||
}
|
||||
|
||||
impl DownloadChecksum {
|
||||
pub(super) fn resolve(&self) -> Option<String> {
|
||||
match self {
|
||||
Self::Fixed(checksum) => Some(checksum.to_ascii_lowercase()),
|
||||
Self::Env(env_var) => env::var(env_var)
|
||||
.ok()
|
||||
.map(|checksum| checksum.to_ascii_lowercase()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn optional_path_display(path: &Option<PathBuf>) -> String {
|
||||
path.as_ref()
|
||||
.map(|path| path.display().to_string())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum BinaryProviderError {
|
||||
/// The selected provider completed without producing an executable path.
|
||||
#[error("binary could not be resolved by provider {provider}")]
|
||||
NotFound {
|
||||
/// Human-readable provider name or fallback provider chain.
|
||||
provider: String,
|
||||
},
|
||||
/// Build command returned a non-zero status.
|
||||
#[error("build command failed with status {status}")]
|
||||
BuildFailed {
|
||||
/// Build command exit status.
|
||||
status: String,
|
||||
},
|
||||
/// Build command succeeded but did not create the configured output file.
|
||||
#[error("build command did not produce configured binary output {path:?}")]
|
||||
MissingBuildOutput {
|
||||
/// Expected output path configured on the build provider.
|
||||
path: PathBuf,
|
||||
},
|
||||
/// Download provider was selected but no URL was configured.
|
||||
#[error("download provider requires env var {env_var} to contain a binary URL")]
|
||||
MissingDownloadUrl {
|
||||
/// Env var expected to contain the download URL.
|
||||
env_var: String,
|
||||
},
|
||||
/// Configured path provider received a relative path.
|
||||
#[error("binary path must be absolute: {path:?}")]
|
||||
RelativePath {
|
||||
/// Relative path rejected by the path provider.
|
||||
path: PathBuf,
|
||||
},
|
||||
/// HTTP download failed or returned an error status.
|
||||
#[error("failed to download binary from {url}: {source}")]
|
||||
Download {
|
||||
/// URL that failed.
|
||||
url: String,
|
||||
#[source]
|
||||
/// HTTP client error.
|
||||
source: reqwest::Error,
|
||||
},
|
||||
/// Downloaded bytes did not match the configured SHA-256 checksum.
|
||||
#[error("downloaded binary sha256 mismatch for {path:?}: expected {expected}, got {actual}")]
|
||||
ChecksumMismatch {
|
||||
/// Cache path the downloaded binary would have been written to.
|
||||
path: PathBuf,
|
||||
/// Configured lowercase SHA-256 checksum.
|
||||
expected: String,
|
||||
/// Actual lowercase SHA-256 checksum of the downloaded bytes.
|
||||
actual: String,
|
||||
},
|
||||
/// Filesystem operation failed while preparing or resolving a binary.
|
||||
#[error("failed to prepare binary path {path:?}: {source}")]
|
||||
Io {
|
||||
/// Path involved in the failing filesystem operation.
|
||||
path: PathBuf,
|
||||
#[source]
|
||||
/// Underlying filesystem error.
|
||||
source: std::io::Error,
|
||||
},
|
||||
/// Another test process held the provider lock for too long.
|
||||
#[error("timed out waiting for binary provider lock {path:?}")]
|
||||
LockTimeout {
|
||||
/// Lock file path that could not be acquired.
|
||||
path: PathBuf,
|
||||
},
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use serde::Serialize;
|
||||
use testing_framework_core::{
|
||||
@ -10,6 +10,7 @@ use testing_framework_core::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
binary::{BinaryProvider, BinaryProviderRef, EnvBinaryProvider},
|
||||
env::LocalBuildContext,
|
||||
process::{LaunchSpec, NodeEndpointPort, NodeEndpoints, ProcessSpawnError},
|
||||
};
|
||||
@ -95,13 +96,11 @@ impl LocalPeerNode {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
/// Standard local process description for one node binary plus one config file.
|
||||
#[derive(Clone)]
|
||||
pub struct LocalProcessSpec {
|
||||
/// Environment variable that points to the node binary.
|
||||
pub binary_env_var: String,
|
||||
/// Fallback binary name resolved from `PATH` or `target/`.
|
||||
pub binary_name: String,
|
||||
/// Binary preparation and resolution policy.
|
||||
pub binary: BinaryProviderRef,
|
||||
/// Config file name written into the temp launch directory.
|
||||
pub config_file_name: String,
|
||||
/// CLI flag used to point the process at `config_file_name`.
|
||||
@ -115,10 +114,9 @@ pub struct LocalProcessSpec {
|
||||
impl LocalProcessSpec {
|
||||
/// Creates a standard binary+config local process spec.
|
||||
#[must_use]
|
||||
pub fn new(binary_env_var: &str, binary_name: &str) -> Self {
|
||||
pub fn new(binary_env_var: &str) -> Self {
|
||||
Self {
|
||||
binary_env_var: binary_env_var.to_owned(),
|
||||
binary_name: binary_name.to_owned(),
|
||||
binary: Arc::new(EnvBinaryProvider::new(binary_env_var)),
|
||||
config_file_name: "config.yaml".to_owned(),
|
||||
config_arg: "--config".to_owned(),
|
||||
extra_args: Vec::new(),
|
||||
@ -153,6 +151,20 @@ impl LocalProcessSpec {
|
||||
self.extra_args.extend(args);
|
||||
self
|
||||
}
|
||||
|
||||
/// Overrides the binary provider used by this process.
|
||||
#[must_use]
|
||||
pub fn with_binary_provider(mut self, binary: impl BinaryProvider + 'static) -> Self {
|
||||
self.binary = Arc::new(binary);
|
||||
self
|
||||
}
|
||||
|
||||
/// Overrides the binary provider with an already shared provider handle.
|
||||
#[must_use]
|
||||
pub fn with_binary_provider_ref(mut self, binary: BinaryProviderRef) -> Self {
|
||||
self.binary = binary;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Preallocates `count` local TCP ports for later use.
|
||||
@ -369,12 +381,11 @@ pub fn text_config_launch_spec(
|
||||
pub fn default_yaml_launch_spec<T: Serialize>(
|
||||
config: &T,
|
||||
binary_env_var: &str,
|
||||
binary_name: &str,
|
||||
rust_log: &str,
|
||||
) -> Result<LaunchSpec, DynError> {
|
||||
yaml_config_launch_spec(
|
||||
config,
|
||||
&LocalProcessSpec::new(binary_env_var, binary_name).with_rust_log(rust_log),
|
||||
&LocalProcessSpec::new(binary_env_var).with_rust_log(rust_log),
|
||||
)
|
||||
}
|
||||
|
||||
@ -392,7 +403,7 @@ pub(crate) fn rendered_config_launch_spec(
|
||||
rendered_config: Vec<u8>,
|
||||
spec: &LocalProcessSpec,
|
||||
) -> Result<LaunchSpec, DynError> {
|
||||
let binary = resolve_binary(spec);
|
||||
let binary = spec.binary.resolve()?;
|
||||
let mut args = vec![spec.config_arg.clone(), spec.config_file_name.clone()];
|
||||
args.extend(spec.extra_args.iter().cloned());
|
||||
|
||||
@ -406,20 +417,3 @@ pub(crate) fn rendered_config_launch_spec(
|
||||
env: spec.env.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_binary(spec: &LocalProcessSpec) -> PathBuf {
|
||||
std::env::var(&spec.binary_env_var)
|
||||
.map(PathBuf::from)
|
||||
.or_else(|_| which::which(&spec.binary_name))
|
||||
.unwrap_or_else(|_| {
|
||||
let mut path = std::env::current_dir().unwrap_or_default();
|
||||
let mut debug = path.clone();
|
||||
debug.push(format!("target/debug/{}", spec.binary_name));
|
||||
if debug.exists() {
|
||||
return debug;
|
||||
}
|
||||
|
||||
path.push(format!("target/release/{}", spec.binary_name));
|
||||
path
|
||||
})
|
||||
}
|
||||
|
||||
16
testing-framework/deployers/local/src/env/mod.rs
vendored
16
testing-framework/deployers/local/src/env/mod.rs
vendored
@ -239,6 +239,18 @@ where
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the local process description for this concrete node config.
|
||||
///
|
||||
/// Apps that need mixed-version or mixed-binary clusters can override this
|
||||
/// hook and choose a different binary provider per node while keeping the
|
||||
/// standard launch-spec rendering path.
|
||||
fn local_process_spec_for_node(
|
||||
_config: &<Self as Application>::NodeConfig,
|
||||
_label: &str,
|
||||
) -> Option<LocalProcessSpec> {
|
||||
Self::local_process_spec()
|
||||
}
|
||||
|
||||
/// Serializes a local node config into the file bytes written next to the
|
||||
/// spawned process.
|
||||
fn render_local_config(
|
||||
@ -251,9 +263,9 @@ where
|
||||
fn build_launch_spec(
|
||||
config: &<Self as Application>::NodeConfig,
|
||||
_dir: &Path,
|
||||
_label: &str,
|
||||
label: &str,
|
||||
) -> Result<LaunchSpec, DynError> {
|
||||
let spec = Self::local_process_spec().ok_or_else(|| {
|
||||
let spec = Self::local_process_spec_for_node(config, label).ok_or_else(|| {
|
||||
std::io::Error::other("build_launch_spec is not implemented for this app")
|
||||
})?;
|
||||
let rendered = Self::render_local_config(config)?;
|
||||
|
||||
@ -6,7 +6,11 @@ mod manual;
|
||||
mod node_control;
|
||||
pub mod process;
|
||||
|
||||
pub use binary::{BinaryConfig, BinaryResolver};
|
||||
pub use binary::{
|
||||
BinaryProvider, BinaryProviderError, BinaryProviderRef, BuildBinaryProvider, BuildCommand,
|
||||
DownloadBinaryProvider, DownloadChecksum, DownloadUrl, EnvBinaryProvider,
|
||||
FallbackBinaryProvider, PathBinaryProvider,
|
||||
};
|
||||
pub use deployer::{ProcessDeployer, ProcessDeployerError};
|
||||
pub use env::{
|
||||
BuiltNodeConfig, LocalBinaryApp, LocalBuildContext, LocalDeployerEnv, LocalNodePorts,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user