framework: reorganize core module structure

This commit is contained in:
Andrus Salumets 2026-03-25 14:12:01 +07:00 committed by GitHub
commit fadc8632e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 532 additions and 468 deletions

8
Cargo.lock generated
View File

@ -3348,7 +3348,7 @@ dependencies = [
"rcgen",
"ring",
"rustls 0.23.36",
"rustls-webpki 0.103.9",
"rustls-webpki 0.103.10",
"thiserror 2.0.18",
"x509-parser",
"yasna",
@ -5953,7 +5953,7 @@ dependencies = [
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki 0.103.9",
"rustls-webpki 0.103.10",
"subtle",
"zeroize",
]
@ -6001,9 +6001,9 @@ dependencies = [
[[package]]
name = "rustls-webpki"
version = "0.103.9"
version = "0.103.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
dependencies = [
"ring",
"rustls-pki-types",

View File

@ -9,7 +9,7 @@ use lb_framework::{
};
pub use testing_framework_core::scenario::ObservabilityBuilderExt;
use testing_framework_core::{
scenario::{NodeControlScenarioBuilder, ObservabilityScenarioBuilder},
scenario::internal::{NodeControlScenarioBuilder, ObservabilityScenarioBuilder},
topology::{DeploymentProvider, DeploymentSeed, DynTopologyError},
};
use tracing::warn;
@ -18,7 +18,7 @@ use crate::LbcExtEnv;
pub type ScenarioBuilder = testing_framework_core::scenario::ScenarioBuilder<LbcExtEnv>;
pub type ScenarioBuilderWith<Caps = ()> =
testing_framework_core::scenario::CoreBuilder<LbcExtEnv, Caps>;
testing_framework_core::scenario::internal::CoreBuilder<LbcExtEnv, Caps>;
pub trait CoreBuilderExt: Sized {
fn deployment_with(f: impl FnOnce(DeploymentBuilder) -> DeploymentBuilder) -> Self;

View File

@ -1,6 +1,8 @@
use reqwest::Url;
use super::{Application, ObservabilityCapability, ObservabilityScenarioBuilder, ScenarioBuilder};
use super::{
Application, ObservabilityCapability, ScenarioBuilder, internal::ObservabilityScenarioBuilder,
};
const METRICS_QUERY_URL_FIELD: &str = "metrics_query_url";
const METRICS_OTLP_INGEST_URL_FIELD: &str = "metrics_otlp_ingest_url";

View File

@ -1,8 +1,8 @@
use std::time::Duration;
use super::{
Application, CleanupPolicy, CoreBuilderAccess, DeploymentPolicy, Expectation,
HttpReadinessRequirement, RetryPolicy, Workload,
Application, CleanupPolicy, DeploymentPolicy, Expectation, HttpReadinessRequirement,
RetryPolicy, Workload, internal::CoreBuilderAccess,
};
use crate::topology::{DeploymentProvider, DeploymentSeed};

View File

@ -1,184 +1,48 @@
use std::{sync::Arc, time::Duration};
use thiserror::Error;
use tracing::{debug, info};
use tracing::info;
use super::{
Application, ClusterControlProfile, ClusterMode, DeploymentPolicy, DynError, ExistingCluster,
ExternalNodeSource, HttpReadinessRequirement, IntoExistingCluster, NodeControlCapability,
ObservabilityCapability, RequiresNodeControl,
builder_ops::CoreBuilderAccess,
expectation::Expectation,
runtime::{
context::RunMetrics,
orchestration::{SourceOrchestrationPlan, SourceOrchestrationPlanError},
model::{Scenario, ScenarioBuildError},
validation::{
build_source_orchestration_plan, enforce_min_duration, expectation_cooldown_for,
initialize_components, validate_source_contract,
},
sources::ScenarioSources,
workload::Workload,
};
use crate::topology::{DeploymentDescriptor, DeploymentProvider, DeploymentSeed, DynTopologyError};
const MIN_EXPECTATION_FALLBACK_SECS: u64 = 10;
const MIN_RUN_DURATION_SECS: u64 = 10;
#[derive(Debug, Error)]
pub enum ScenarioBuildError {
#[error("topology build failed: {0}")]
Topology(#[source] DynTopologyError),
#[error("workload '{name}' failed to initialize")]
WorkloadInit { name: String, source: DynError },
#[error("expectation '{name}' failed to initialize")]
ExpectationInit { name: String, source: DynError },
#[error("invalid scenario source configuration: {message}")]
SourceConfiguration { message: String },
#[error("scenario source mode '{mode}' is not wired into deployers yet")]
SourceModeNotWiredYet { mode: &'static str },
}
/// Immutable scenario definition used by the runner, workloads, and
/// expectations.
pub struct Scenario<E: Application, Caps = ()> {
deployment: E::Deployment,
workloads: Vec<Arc<dyn Workload<E>>>,
expectations: Vec<Box<dyn Expectation<E>>>,
duration: Duration,
expectation_cooldown: Duration,
deployment_policy: DeploymentPolicy,
sources: ScenarioSources,
source_orchestration_plan: SourceOrchestrationPlan,
capabilities: Caps,
}
impl<E: Application, Caps> Scenario<E, Caps> {
fn new(
deployment: E::Deployment,
workloads: Vec<Arc<dyn Workload<E>>>,
expectations: Vec<Box<dyn Expectation<E>>>,
duration: Duration,
expectation_cooldown: Duration,
deployment_policy: DeploymentPolicy,
sources: ScenarioSources,
source_orchestration_plan: SourceOrchestrationPlan,
capabilities: Caps,
) -> Self {
Self {
deployment,
workloads,
expectations,
duration,
expectation_cooldown,
deployment_policy,
sources,
source_orchestration_plan,
capabilities,
}
}
#[must_use]
pub fn deployment(&self) -> &E::Deployment {
&self.deployment
}
#[must_use]
pub fn workloads(&self) -> &[Arc<dyn Workload<E>>] {
&self.workloads
}
#[must_use]
pub fn expectations(&self) -> &[Box<dyn Expectation<E>>] {
&self.expectations
}
#[must_use]
pub fn expectations_mut(&mut self) -> &mut [Box<dyn Expectation<E>>] {
&mut self.expectations
}
#[must_use]
pub const fn duration(&self) -> Duration {
self.duration
}
#[must_use]
pub const fn expectation_cooldown(&self) -> Duration {
self.expectation_cooldown
}
#[must_use]
pub const fn http_readiness_requirement(&self) -> HttpReadinessRequirement {
self.deployment_policy.readiness_requirement
}
#[must_use]
pub const fn deployment_policy(&self) -> DeploymentPolicy {
self.deployment_policy
}
#[must_use]
pub fn existing_cluster(&self) -> Option<&ExistingCluster> {
self.sources.existing_cluster()
}
#[must_use]
pub const fn cluster_mode(&self) -> ClusterMode {
self.sources.cluster_mode()
}
#[must_use]
pub const fn cluster_control_profile(&self) -> ClusterControlProfile {
self.sources.control_profile()
}
#[must_use]
#[doc(hidden)]
pub fn attached_source(&self) -> Option<&ExistingCluster> {
self.existing_cluster()
}
#[must_use]
pub fn external_nodes(&self) -> &[ExternalNodeSource] {
self.sources.external_nodes()
}
#[must_use]
pub fn has_external_nodes(&self) -> bool {
!self.sources.external_nodes().is_empty()
}
#[must_use]
pub const fn source_orchestration_plan(&self) -> &SourceOrchestrationPlan {
&self.source_orchestration_plan
}
#[must_use]
pub const fn capabilities(&self) -> &Caps {
&self.capabilities
}
}
use crate::{
scenario::{
Application, DeploymentPolicy, DynError, ExistingCluster, ExternalNodeSource,
HttpReadinessRequirement, IntoExistingCluster, NodeControlCapability,
ObservabilityCapability, RequiresNodeControl, builder_ops::CoreBuilderAccess,
expectation::Expectation, runtime::context::RunMetrics, sources::ScenarioSources,
workload::Workload,
},
topology::{DeploymentDescriptor, DeploymentProvider, DeploymentSeed},
};
/// Scenario builder entry point.
pub struct Builder<E: Application, Caps = ()> {
deployment_provider: Box<dyn DeploymentProvider<E::Deployment>>,
topology_seed: Option<DeploymentSeed>,
workloads: Vec<Box<dyn Workload<E>>>,
expectations: Vec<Box<dyn Expectation<E>>>,
duration: Duration,
expectation_cooldown: Option<Duration>,
deployment_policy: DeploymentPolicy,
sources: ScenarioSources,
capabilities: Caps,
pub(super) deployment_provider: Box<dyn DeploymentProvider<E::Deployment>>,
pub(super) topology_seed: Option<DeploymentSeed>,
pub(super) workloads: Vec<Box<dyn Workload<E>>>,
pub(super) expectations: Vec<Box<dyn Expectation<E>>>,
pub(super) duration: Duration,
pub(super) expectation_cooldown: Option<Duration>,
pub(super) deployment_policy: DeploymentPolicy,
pub(super) sources: ScenarioSources,
pub(super) capabilities: Caps,
}
pub struct ScenarioBuilder<E: Application> {
inner: Builder<E, ()>,
pub(super) inner: Builder<E, ()>,
}
pub struct NodeControlScenarioBuilder<E: Application> {
inner: Builder<E, NodeControlCapability>,
pub(super) inner: Builder<E, NodeControlCapability>,
}
pub struct ObservabilityScenarioBuilder<E: Application> {
inner: Builder<E, ObservabilityCapability>,
pub(super) inner: Builder<E, ObservabilityCapability>,
}
macro_rules! impl_common_builder_methods {
@ -432,10 +296,6 @@ impl<E: Application> ScenarioBuilder<E> {
self.with_observability()
}
pub fn build(self) -> Result<Scenario<E>, ScenarioBuildError> {
self.inner.build()
}
pub(crate) fn with_observability_capability(
self,
observability: ObservabilityCapability,
@ -447,27 +307,15 @@ impl<E: Application> ScenarioBuilder<E> {
}
impl_common_builder_methods!(ScenarioBuilder);
impl<E: Application> NodeControlScenarioBuilder<E> {
pub fn build(self) -> Result<Scenario<E, NodeControlCapability>, ScenarioBuildError> {
self.inner.build()
}
}
impl_common_builder_methods!(NodeControlScenarioBuilder);
impl_common_builder_methods!(ObservabilityScenarioBuilder);
impl<E: Application> ObservabilityScenarioBuilder<E> {
pub fn build(self) -> Result<Scenario<E, ObservabilityCapability>, ScenarioBuildError> {
self.inner.build()
}
pub(crate) fn capabilities_mut(&mut self) -> &mut ObservabilityCapability {
self.inner.capabilities_mut()
}
}
impl_common_builder_methods!(ObservabilityScenarioBuilder);
impl<E: Application, Caps> Builder<E, Caps> {
#[must_use]
/// Transform the existing deployment provider while preserving all
@ -521,36 +369,6 @@ impl<E: Application, Caps> Builder<E, Caps> {
}
}
#[must_use]
pub const fn capabilities(&self) -> &Caps {
&self.capabilities
}
#[must_use]
pub const fn capabilities_mut(&mut self) -> &mut Caps {
&mut self.capabilities
}
#[must_use]
pub const fn run_duration(&self) -> Duration {
self.duration
}
#[must_use]
pub const fn expectation_cooldown_override(&self) -> Option<Duration> {
self.expectation_cooldown
}
#[must_use]
pub const fn http_readiness_requirement(&self) -> HttpReadinessRequirement {
self.deployment_policy.readiness_requirement
}
#[must_use]
pub const fn deployment_policy(&self) -> DeploymentPolicy {
self.deployment_policy
}
#[must_use]
pub fn with_deployment_seed(mut self, seed: DeploymentSeed) -> Self {
self.topology_seed = Some(seed);
@ -796,62 +614,6 @@ impl<E: Application, Caps> BuilderParts<E, Caps> {
}
}
fn build_source_orchestration_plan(
sources: &ScenarioSources,
) -> Result<SourceOrchestrationPlan, ScenarioBuildError> {
SourceOrchestrationPlan::try_from_sources(sources).map_err(source_plan_error_to_build_error)
}
fn validate_source_contract<Caps>(sources: &ScenarioSources) -> Result<(), ScenarioBuildError>
where
Caps: RequiresNodeControl,
{
validate_external_only_sources(sources)?;
validate_node_control_profile::<Caps>(sources)?;
Ok(())
}
fn source_plan_error_to_build_error(error: SourceOrchestrationPlanError) -> ScenarioBuildError {
match error {
SourceOrchestrationPlanError::SourceModeNotWiredYet { mode } => {
ScenarioBuildError::SourceModeNotWiredYet { mode }
}
}
}
fn validate_external_only_sources(sources: &ScenarioSources) -> Result<(), ScenarioBuildError> {
if matches!(sources.cluster_mode(), ClusterMode::ExternalOnly)
&& sources.external_nodes().is_empty()
{
return Err(ScenarioBuildError::SourceConfiguration {
message: "external-only scenarios require at least one external node".to_owned(),
});
}
Ok(())
}
fn validate_node_control_profile<Caps>(sources: &ScenarioSources) -> Result<(), ScenarioBuildError>
where
Caps: RequiresNodeControl,
{
let profile = sources.control_profile();
if Caps::REQUIRED && matches!(profile, ClusterControlProfile::ExternalUncontrolled) {
return Err(ScenarioBuildError::SourceConfiguration {
message: format!(
"node control is not available for cluster mode '{}' with control profile '{}'",
sources.cluster_mode().as_str(),
profile.as_str(),
),
});
}
Ok(())
}
impl<E: Application> Builder<E, ()> {
#[must_use]
pub fn with_node_control(self) -> Builder<E, NodeControlCapability> {
@ -875,132 +637,3 @@ impl<E: Application> Builder<E, ()> {
self.with_observability()
}
}
fn initialize_components<E: Application>(
descriptors: &E::Deployment,
run_metrics: &RunMetrics,
workloads: &mut [Box<dyn Workload<E>>],
expectations: &mut [Box<dyn Expectation<E>>],
) -> Result<(), ScenarioBuildError> {
initialize_workloads(descriptors, run_metrics, workloads)?;
initialize_expectations(descriptors, run_metrics, expectations)?;
Ok(())
}
fn initialize_workloads<E: Application>(
descriptors: &E::Deployment,
run_metrics: &RunMetrics,
workloads: &mut [Box<dyn Workload<E>>],
) -> Result<(), ScenarioBuildError> {
for workload in workloads {
debug!(workload = workload.name(), "initializing workload");
let name = workload.name().to_owned();
workload
.init(descriptors, run_metrics)
.map_err(|source| workload_init_error(name, source))?;
}
Ok(())
}
fn initialize_expectations<E: Application>(
descriptors: &E::Deployment,
run_metrics: &RunMetrics,
expectations: &mut [Box<dyn Expectation<E>>],
) -> Result<(), ScenarioBuildError> {
for expectation in expectations {
debug!(expectation = expectation.name(), "initializing expectation");
let name = expectation.name().to_owned();
expectation
.init(descriptors, run_metrics)
.map_err(|source| expectation_init_error(name, source))?;
}
Ok(())
}
fn workload_init_error(name: String, source: DynError) -> ScenarioBuildError {
ScenarioBuildError::WorkloadInit { name, source }
}
fn expectation_init_error(name: String, source: DynError) -> ScenarioBuildError {
ScenarioBuildError::ExpectationInit { name, source }
}
fn enforce_min_duration(requested: Duration) -> Duration {
let min_duration = min_run_duration();
requested.max(min_duration)
}
fn default_expectation_cooldown() -> Duration {
Duration::from_secs(MIN_EXPECTATION_FALLBACK_SECS)
}
fn expectation_cooldown_for(override_value: Option<Duration>) -> Duration {
override_value.unwrap_or_else(default_expectation_cooldown)
}
fn min_run_duration() -> Duration {
Duration::from_secs(MIN_RUN_DURATION_SECS)
}
#[cfg(test)]
mod tests {
use super::{
ScenarioBuildError, validate_external_only_sources, validate_node_control_profile,
};
use crate::scenario::{
ExistingCluster, ExternalNodeSource, NodeControlCapability, sources::ScenarioSources,
};
#[test]
fn external_only_requires_external_nodes() {
let error =
validate_external_only_sources(&ScenarioSources::default().into_external_only())
.expect_err("external-only without nodes should fail");
assert!(matches!(
error,
ScenarioBuildError::SourceConfiguration { .. }
));
assert_eq!(
error.to_string(),
"invalid scenario source configuration: external-only scenarios require at least one external node"
);
}
#[test]
fn external_only_rejects_node_control_requirement() {
let sources = ScenarioSources::default()
.with_external_node(ExternalNodeSource::new(
"node-0".to_owned(),
"http://127.0.0.1:1".to_owned(),
))
.into_external_only();
let error = validate_node_control_profile::<NodeControlCapability>(&sources)
.expect_err("external-only should reject node control");
assert!(matches!(
error,
ScenarioBuildError::SourceConfiguration { .. }
));
assert_eq!(
error.to_string(),
"invalid scenario source configuration: node control is not available for cluster mode 'external-only' with control profile 'external-uncontrolled'"
);
}
#[test]
fn existing_cluster_accepts_node_control_requirement() {
let sources = ScenarioSources::default()
.with_attach(ExistingCluster::for_compose_project("project".to_owned()));
validate_node_control_profile::<NodeControlCapability>(&sources)
.expect("existing cluster should be considered controllable");
}
}

View File

@ -0,0 +1,8 @@
mod builder;
mod model;
mod validation;
pub use builder::{
Builder, NodeControlScenarioBuilder, ObservabilityScenarioBuilder, ScenarioBuilder,
};
pub use model::{Scenario, ScenarioBuildError};

View File

@ -0,0 +1,202 @@
use std::{sync::Arc, time::Duration};
use thiserror::Error;
use super::builder::Builder;
use crate::{
scenario::{
Application, ClusterControlProfile, ClusterMode, DeploymentPolicy, DynError,
ExistingCluster, ExternalNodeSource, HttpReadinessRequirement, expectation::Expectation,
runtime::SourceOrchestrationPlan, sources::ScenarioSources, workload::Workload,
},
topology::DynTopologyError,
};
#[derive(Debug, Error)]
pub enum ScenarioBuildError {
#[error("topology build failed: {0}")]
Topology(#[source] DynTopologyError),
#[error("workload '{name}' failed to initialize")]
WorkloadInit { name: String, source: DynError },
#[error("expectation '{name}' failed to initialize")]
ExpectationInit { name: String, source: DynError },
#[error("invalid scenario source configuration: {message}")]
SourceConfiguration { message: String },
#[error("scenario source mode '{mode}' is not wired into deployers yet")]
SourceModeNotWiredYet { mode: &'static str },
}
/// Immutable scenario definition used by the runner, workloads, and
/// expectations.
pub struct Scenario<E: Application, Caps = ()> {
deployment: E::Deployment,
workloads: Vec<Arc<dyn Workload<E>>>,
expectations: Vec<Box<dyn Expectation<E>>>,
duration: Duration,
expectation_cooldown: Duration,
deployment_policy: DeploymentPolicy,
sources: ScenarioSources,
source_orchestration_plan: SourceOrchestrationPlan,
capabilities: Caps,
}
impl<E: Application, Caps> Scenario<E, Caps> {
pub(super) fn new(
deployment: E::Deployment,
workloads: Vec<Arc<dyn Workload<E>>>,
expectations: Vec<Box<dyn Expectation<E>>>,
duration: Duration,
expectation_cooldown: Duration,
deployment_policy: DeploymentPolicy,
sources: ScenarioSources,
source_orchestration_plan: SourceOrchestrationPlan,
capabilities: Caps,
) -> Self {
Self {
deployment,
workloads,
expectations,
duration,
expectation_cooldown,
deployment_policy,
sources,
source_orchestration_plan,
capabilities,
}
}
#[must_use]
pub fn deployment(&self) -> &E::Deployment {
&self.deployment
}
#[must_use]
pub fn workloads(&self) -> &[Arc<dyn Workload<E>>] {
&self.workloads
}
#[must_use]
pub fn expectations(&self) -> &[Box<dyn Expectation<E>>] {
&self.expectations
}
#[must_use]
pub fn expectations_mut(&mut self) -> &mut [Box<dyn Expectation<E>>] {
&mut self.expectations
}
#[must_use]
pub const fn duration(&self) -> Duration {
self.duration
}
#[must_use]
pub const fn expectation_cooldown(&self) -> Duration {
self.expectation_cooldown
}
#[must_use]
pub const fn http_readiness_requirement(&self) -> HttpReadinessRequirement {
self.deployment_policy.readiness_requirement
}
#[must_use]
pub const fn deployment_policy(&self) -> DeploymentPolicy {
self.deployment_policy
}
#[must_use]
pub fn existing_cluster(&self) -> Option<&ExistingCluster> {
self.sources.existing_cluster()
}
#[must_use]
pub const fn cluster_mode(&self) -> ClusterMode {
self.sources.cluster_mode()
}
#[must_use]
pub const fn cluster_control_profile(&self) -> ClusterControlProfile {
self.sources.control_profile()
}
#[must_use]
#[doc(hidden)]
pub fn attached_source(&self) -> Option<&ExistingCluster> {
self.existing_cluster()
}
#[must_use]
pub fn external_nodes(&self) -> &[ExternalNodeSource] {
self.sources.external_nodes()
}
#[must_use]
pub fn has_external_nodes(&self) -> bool {
!self.sources.external_nodes().is_empty()
}
#[must_use]
pub const fn source_orchestration_plan(&self) -> &SourceOrchestrationPlan {
&self.source_orchestration_plan
}
#[must_use]
pub const fn capabilities(&self) -> &Caps {
&self.capabilities
}
}
impl<E: Application> super::builder::ScenarioBuilder<E> {
pub fn build(self) -> Result<Scenario<E>, ScenarioBuildError> {
self.inner.build()
}
}
impl<E: Application> super::builder::NodeControlScenarioBuilder<E> {
pub fn build(
self,
) -> Result<Scenario<E, crate::scenario::NodeControlCapability>, ScenarioBuildError> {
self.inner.build()
}
}
impl<E: Application> super::builder::ObservabilityScenarioBuilder<E> {
pub fn build(
self,
) -> Result<Scenario<E, crate::scenario::ObservabilityCapability>, ScenarioBuildError> {
self.inner.build()
}
}
impl<E: Application, Caps> Builder<E, Caps> {
#[must_use]
pub const fn capabilities(&self) -> &Caps {
&self.capabilities
}
#[must_use]
pub const fn capabilities_mut(&mut self) -> &mut Caps {
&mut self.capabilities
}
#[must_use]
pub const fn run_duration(&self) -> Duration {
self.duration
}
#[must_use]
pub const fn expectation_cooldown_override(&self) -> Option<Duration> {
self.expectation_cooldown
}
#[must_use]
pub const fn http_readiness_requirement(&self) -> HttpReadinessRequirement {
self.deployment_policy.readiness_requirement
}
#[must_use]
pub const fn deployment_policy(&self) -> DeploymentPolicy {
self.deployment_policy
}
}

View File

@ -0,0 +1,196 @@
use std::time::Duration;
use tracing::debug;
use super::model::ScenarioBuildError;
use crate::scenario::{
Application, ClusterControlProfile, ClusterMode, DynError, RequiresNodeControl,
expectation::Expectation,
runtime::{SourceOrchestrationPlan, SourceOrchestrationPlanError, context::RunMetrics},
sources::ScenarioSources,
workload::Workload,
};
const MIN_EXPECTATION_FALLBACK_SECS: u64 = 10;
const MIN_RUN_DURATION_SECS: u64 = 10;
pub(super) fn build_source_orchestration_plan(
sources: &ScenarioSources,
) -> Result<SourceOrchestrationPlan, ScenarioBuildError> {
SourceOrchestrationPlan::try_from_sources(sources).map_err(source_plan_error_to_build_error)
}
pub(super) fn validate_source_contract<Caps>(
sources: &ScenarioSources,
) -> Result<(), ScenarioBuildError>
where
Caps: RequiresNodeControl,
{
validate_external_only_sources(sources)?;
validate_node_control_profile::<Caps>(sources)?;
Ok(())
}
fn source_plan_error_to_build_error(error: SourceOrchestrationPlanError) -> ScenarioBuildError {
match error {
SourceOrchestrationPlanError::SourceModeNotWiredYet { mode } => {
ScenarioBuildError::SourceModeNotWiredYet { mode }
}
}
}
pub(super) fn initialize_components<E: Application>(
descriptors: &E::Deployment,
run_metrics: &RunMetrics,
workloads: &mut [Box<dyn Workload<E>>],
expectations: &mut [Box<dyn Expectation<E>>],
) -> Result<(), ScenarioBuildError> {
initialize_workloads(descriptors, run_metrics, workloads)?;
initialize_expectations(descriptors, run_metrics, expectations)?;
Ok(())
}
fn initialize_workloads<E: Application>(
descriptors: &E::Deployment,
run_metrics: &RunMetrics,
workloads: &mut [Box<dyn Workload<E>>],
) -> Result<(), ScenarioBuildError> {
for workload in workloads {
debug!(workload = workload.name(), "initializing workload");
let name = workload.name().to_owned();
workload
.init(descriptors, run_metrics)
.map_err(|source| workload_init_error(name, source))?;
}
Ok(())
}
fn initialize_expectations<E: Application>(
descriptors: &E::Deployment,
run_metrics: &RunMetrics,
expectations: &mut [Box<dyn Expectation<E>>],
) -> Result<(), ScenarioBuildError> {
for expectation in expectations {
debug!(expectation = expectation.name(), "initializing expectation");
let name = expectation.name().to_owned();
expectation
.init(descriptors, run_metrics)
.map_err(|source| expectation_init_error(name, source))?;
}
Ok(())
}
fn workload_init_error(name: String, source: DynError) -> ScenarioBuildError {
ScenarioBuildError::WorkloadInit { name, source }
}
fn expectation_init_error(name: String, source: DynError) -> ScenarioBuildError {
ScenarioBuildError::ExpectationInit { name, source }
}
pub(super) fn enforce_min_duration(requested: Duration) -> Duration {
requested.max(min_run_duration())
}
fn default_expectation_cooldown() -> Duration {
Duration::from_secs(MIN_EXPECTATION_FALLBACK_SECS)
}
pub(super) fn expectation_cooldown_for(override_value: Option<Duration>) -> Duration {
override_value.unwrap_or_else(default_expectation_cooldown)
}
fn min_run_duration() -> Duration {
Duration::from_secs(MIN_RUN_DURATION_SECS)
}
fn validate_external_only_sources(sources: &ScenarioSources) -> Result<(), ScenarioBuildError> {
if matches!(sources.cluster_mode(), ClusterMode::ExternalOnly)
&& sources.external_nodes().is_empty()
{
return Err(ScenarioBuildError::SourceConfiguration {
message: "external-only scenarios require at least one external node".to_owned(),
});
}
Ok(())
}
fn validate_node_control_profile<Caps>(sources: &ScenarioSources) -> Result<(), ScenarioBuildError>
where
Caps: RequiresNodeControl,
{
let profile = sources.control_profile();
if Caps::REQUIRED && matches!(profile, ClusterControlProfile::ExternalUncontrolled) {
return Err(ScenarioBuildError::SourceConfiguration {
message: format!(
"node control is not available for cluster mode '{}' with control profile '{}'",
sources.cluster_mode().as_str(),
profile.as_str(),
),
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{
ScenarioBuildError, validate_external_only_sources, validate_node_control_profile,
};
use crate::scenario::{
ExistingCluster, ExternalNodeSource, NodeControlCapability, sources::ScenarioSources,
};
#[test]
fn external_only_requires_external_nodes() {
let error =
validate_external_only_sources(&ScenarioSources::default().into_external_only())
.expect_err("external-only without nodes should fail");
assert!(matches!(
error,
ScenarioBuildError::SourceConfiguration { .. }
));
assert_eq!(
error.to_string(),
"invalid scenario source configuration: external-only scenarios require at least one external node"
);
}
#[test]
fn external_only_rejects_node_control_requirement() {
let sources = ScenarioSources::default()
.with_external_node(ExternalNodeSource::new(
"node-0".to_owned(),
"http://127.0.0.1:1".to_owned(),
))
.into_external_only();
let error = validate_node_control_profile::<NodeControlCapability>(&sources)
.expect_err("external-only should reject node control");
assert!(matches!(
error,
ScenarioBuildError::SourceConfiguration { .. }
));
assert_eq!(
error.to_string(),
"invalid scenario source configuration: node control is not available for cluster mode 'external-only' with control profile 'external-uncontrolled'"
);
}
#[test]
fn existing_cluster_accepts_node_control_requirement() {
let sources = ScenarioSources::default()
.with_attach(ExistingCluster::for_compose_project("project".to_owned()));
validate_node_control_profile::<NodeControlCapability>(&sources)
.expect("existing cluster should be considered controllable");
}
}

View File

@ -0,0 +1,13 @@
#[doc(hidden)]
pub use super::builder_ops::CoreBuilderAccess;
#[doc(hidden)]
pub use super::definition::{
Builder as CoreBuilder, NodeControlScenarioBuilder, ObservabilityScenarioBuilder,
};
#[doc(hidden)]
pub use super::runtime::{
ApplicationExternalProvider, AttachProvider, AttachProviderError, AttachedNode, CleanupGuard,
FeedHandle, ManagedSource, RuntimeAssembly, SourceOrchestrationPlan, SourceProviders,
StaticManagedProvider, build_source_orchestration_plan, orchestrate_sources,
orchestrate_sources_with_providers, resolve_sources,
};

View File

@ -10,6 +10,7 @@ mod control;
mod definition;
mod deployment_policy;
mod expectation;
pub mod internal;
mod observability;
mod runtime;
mod sources;
@ -18,31 +19,16 @@ mod workload;
pub type DynError = Box<dyn Error + Send + Sync + 'static>;
pub use builder_ext::{BuilderInputError, ObservabilityBuilderExt};
#[doc(hidden)]
pub use builder_ops::CoreBuilderAccess;
pub use capabilities::{
NodeControlCapability, ObservabilityCapability, PeerSelection, RequiresNodeControl,
StartNodeOptions, StartedNode,
};
pub use common_builder_ext::CoreBuilderExt;
pub use control::{ClusterWaitHandle, NodeControlHandle};
#[doc(hidden)]
pub use definition::{
Builder as CoreBuilder, // internal adapter-facing core builder
NodeControlScenarioBuilder,
ObservabilityScenarioBuilder,
};
pub use definition::{Scenario, ScenarioBuildError, ScenarioBuilder};
pub use deployment_policy::{CleanupPolicy, DeploymentPolicy, RetryPolicy};
pub use expectation::Expectation;
pub use observability::{ObservabilityCapabilityProvider, ObservabilityInputs};
#[doc(hidden)]
pub use runtime::{
ApplicationExternalProvider, AttachProvider, AttachProviderError, AttachedNode, CleanupGuard,
FeedHandle, ManagedSource, RuntimeAssembly, SourceOrchestrationPlan, SourceProviders,
StaticManagedProvider, build_source_orchestration_plan, orchestrate_sources,
orchestrate_sources_with_providers, resolve_sources,
};
pub use runtime::{
Deployer, Feed, FeedRuntime, HttpReadinessRequirement, NodeClients, ReadinessError, RunContext,
RunHandle, RunMetrics, Runner, ScenarioError, StabilizationConfig,

View File

@ -0,0 +1,13 @@
mod orchestration;
mod providers;
pub use orchestration::{
ManagedSource, SourceOrchestrationPlan, SourceOrchestrationPlanError,
build_source_orchestration_plan, orchestrate_sources, orchestrate_sources_with_providers,
resolve_sources,
};
pub use providers::{
ApplicationExternalProvider, AttachProvider, AttachProviderError, AttachedNode, ExternalNode,
ExternalProviderError, ManagedProviderError, ManagedProvisionedNode, SourceProviders,
StaticManagedProvider,
};

View File

@ -1,15 +1,13 @@
use std::sync::Arc;
use super::{SourceOrchestrationMode, SourceOrchestrationPlan, SourceOrchestrationPlanError};
use crate::scenario::{
Application, DynError, NodeClients, Scenario,
runtime::{
orchestration::{
SourceOrchestrationMode, SourceOrchestrationPlan, SourceOrchestrationPlanError,
},
providers::{
ApplicationExternalProvider, AttachProviderError, AttachedNode, ExternalNode,
ExternalProviderError, ManagedProviderError, ManagedProvisionedNode, SourceProviders,
StaticManagedProvider,
ApplicationExternalProvider, AttachProviderError, AttachedNode, SourceProviders,
StaticManagedProvider,
internal::{
ExternalNode, ExternalProviderError, ManagedProviderError, ManagedProvisionedNode,
},
},
};

View File

@ -1,6 +1,6 @@
use async_trait::async_trait;
use crate::scenario::{Application, DynError, runtime::orchestration::ManagedSource};
use crate::scenario::{Application, DynError, runtime::ManagedSource};
/// Managed node produced by the managed provider path.
#[derive(Clone, Debug)]

View File

@ -1,27 +1,23 @@
pub mod context;
mod deployer;
mod internal;
mod inventory;
pub mod metrics;
mod node_clients;
pub mod orchestration;
pub mod providers;
pub mod readiness;
mod runner;
use async_trait::async_trait;
pub use context::{CleanupGuard, RunContext, RunHandle, RunMetrics, RuntimeAssembly};
pub use deployer::{Deployer, ScenarioError};
#[doc(hidden)]
pub use internal::{
ApplicationExternalProvider, AttachProvider, AttachProviderError, AttachedNode, ManagedSource,
SourceOrchestrationPlan, SourceOrchestrationPlanError, SourceProviders, StaticManagedProvider,
build_source_orchestration_plan, orchestrate_sources, orchestrate_sources_with_providers,
resolve_sources,
};
pub use node_clients::NodeClients;
#[doc(hidden)]
pub use orchestration::{
ManagedSource, SourceOrchestrationPlan, build_source_orchestration_plan, orchestrate_sources,
orchestrate_sources_with_providers, resolve_sources,
};
#[doc(hidden)]
pub use providers::{
ApplicationExternalProvider, AttachProvider, AttachProviderError, AttachedNode,
SourceProviders, StaticManagedProvider,
};
pub use readiness::{
HttpReadinessRequirement, ReadinessError, StabilizationConfig, wait_for_http_ports,
wait_for_http_ports_with_host, wait_for_http_ports_with_host_and_requirement,

View File

@ -5,7 +5,9 @@ use rand::{Rng as _, seq::SliceRandom as _, thread_rng};
use tokio::time::{Instant, sleep};
use crate::{
scenario::{Application, CoreBuilder, DynError, NodeControlCapability, RunContext, Workload},
scenario::{
Application, DynError, NodeControlCapability, RunContext, Workload, internal::CoreBuilder,
},
topology::DeploymentDescriptor,
};

View File

@ -2,8 +2,9 @@ use std::marker::PhantomData;
use async_trait::async_trait;
use testing_framework_core::scenario::{
AttachProvider, AttachProviderError, AttachedNode, ClusterWaitHandle, DynError,
ExistingCluster, ExternalNodeSource, HttpReadinessRequirement, wait_http_readiness,
ClusterWaitHandle, DynError, ExistingCluster, ExternalNodeSource, HttpReadinessRequirement,
internal::{AttachProvider, AttachProviderError, AttachedNode},
wait_http_readiness,
};
use url::Url;

View File

@ -1,6 +1,8 @@
use std::{fmt::Debug, marker::PhantomData};
use testing_framework_core::scenario::{Application, FeedHandle, FeedRuntime, NodeClients};
use testing_framework_core::scenario::{
Application, FeedRuntime, NodeClients, internal::FeedHandle,
};
use tracing::{info, warn};
use crate::{

View File

@ -9,8 +9,9 @@ use std::marker::PhantomData;
use async_trait::async_trait;
use testing_framework_core::scenario::{
CleanupGuard, Deployer, DynError, ExistingCluster, FeedHandle, IntoExistingCluster,
ObservabilityCapabilityProvider, RequiresNodeControl, Runner, Scenario,
Deployer, DynError, ExistingCluster, IntoExistingCluster, ObservabilityCapabilityProvider,
RequiresNodeControl, Runner, Scenario,
internal::{CleanupGuard, FeedHandle},
};
use crate::{env::ComposeDeployEnv, errors::ComposeRunnerError, lifecycle::cleanup::RunnerCleanup};

View File

@ -3,12 +3,15 @@ use std::{env, sync::Arc, time::Duration};
use reqwest::Url;
use testing_framework_core::{
scenario::{
Application, ApplicationExternalProvider, CleanupGuard, ClusterControlProfile, ClusterMode,
ClusterWaitHandle, DeploymentPolicy, DynError, ExistingCluster, FeedHandle, FeedRuntime,
HttpReadinessRequirement, Metrics, NodeClients, NodeControlHandle,
ObservabilityCapabilityProvider, ObservabilityInputs, RequiresNodeControl, Runner,
RuntimeAssembly, Scenario, SourceOrchestrationPlan, SourceProviders, StaticManagedProvider,
build_source_orchestration_plan, orchestrate_sources_with_providers,
Application, ClusterControlProfile, ClusterMode, ClusterWaitHandle, DeploymentPolicy,
DynError, ExistingCluster, FeedRuntime, HttpReadinessRequirement, Metrics, NodeClients,
NodeControlHandle, ObservabilityCapabilityProvider, ObservabilityInputs,
RequiresNodeControl, Runner, Scenario,
internal::{
ApplicationExternalProvider, CleanupGuard, FeedHandle, RuntimeAssembly,
SourceOrchestrationPlan, SourceProviders, StaticManagedProvider,
build_source_orchestration_plan, orchestrate_sources_with_providers,
},
},
topology::DeploymentDescriptor,
};

View File

@ -6,7 +6,7 @@ use std::{
use anyhow::anyhow;
use reqwest::Url;
use testing_framework_core::{scenario::CleanupGuard, topology::DeploymentDescriptor};
use testing_framework_core::{scenario::internal::CleanupGuard, topology::DeploymentDescriptor};
use tokio::{net::TcpStream, process::Command};
use tokio_retry::{Retry, strategy::FixedInterval};
use tracing::{debug, error, info, warn};

View File

@ -1,7 +1,7 @@
use std::time::Duration;
use testing_framework_core::scenario::{
Application, FeedHandle, FeedRuntime, NodeClients, spawn_feed,
Application, FeedRuntime, NodeClients, internal::FeedHandle, spawn_feed,
};
use tokio::time::sleep;
use tracing::{debug, info, warn};

View File

@ -4,7 +4,7 @@ use std::{
thread,
};
use testing_framework_core::scenario::CleanupGuard;
use testing_framework_core::scenario::internal::CleanupGuard;
use tracing::{debug, info, warn};
use crate::{

View File

@ -7,8 +7,9 @@ use kube::{
api::{ListParams, ObjectList},
};
use testing_framework_core::scenario::{
AttachProvider, AttachProviderError, AttachedNode, ClusterWaitHandle, DynError,
ExistingCluster, ExternalNodeSource, HttpReadinessRequirement, wait_http_readiness,
ClusterWaitHandle, DynError, ExistingCluster, ExternalNodeSource, HttpReadinessRequirement,
internal::{AttachProvider, AttachProviderError, AttachedNode},
wait_http_readiness,
};
use url::Url;

View File

@ -5,12 +5,15 @@ use kube::Client;
use reqwest::Url;
use testing_framework_core::{
scenario::{
Application, ApplicationExternalProvider, CleanupGuard, ClusterControlProfile, ClusterMode,
ClusterWaitHandle, Deployer, DynError, ExistingCluster, FeedHandle, FeedRuntime,
HttpReadinessRequirement, Metrics, MetricsError, NodeClients,
Application, ClusterControlProfile, ClusterMode, ClusterWaitHandle, Deployer, DynError,
ExistingCluster, FeedRuntime, HttpReadinessRequirement, Metrics, MetricsError, NodeClients,
ObservabilityCapabilityProvider, ObservabilityInputs, RequiresNodeControl, Runner,
RuntimeAssembly, Scenario, SourceOrchestrationPlan, SourceProviders, StaticManagedProvider,
build_source_orchestration_plan, orchestrate_sources_with_providers,
Scenario,
internal::{
ApplicationExternalProvider, CleanupGuard, FeedHandle, RuntimeAssembly,
SourceOrchestrationPlan, SourceProviders, StaticManagedProvider,
build_source_orchestration_plan, orchestrate_sources_with_providers,
},
},
topology::DeploymentDescriptor,
};

View File

@ -1,7 +1,7 @@
use kube::Client;
use reqwest::Url;
use testing_framework_core::scenario::{
CleanupGuard, DynError, HttpReadinessRequirement, NodeClients,
DynError, HttpReadinessRequirement, NodeClients, internal::CleanupGuard,
};
use tracing::{debug, info};
use url::ParseError;

View File

@ -1,5 +1,5 @@
use testing_framework_core::scenario::{
Application, FeedHandle, FeedRuntime, NodeClients, spawn_feed,
Application, FeedRuntime, NodeClients, internal::FeedHandle, spawn_feed,
};
use tracing::{debug, info};

View File

@ -2,7 +2,7 @@ use std::{io, process::Output, thread};
use k8s_openapi::api::core::v1::Namespace;
use kube::{Api, Client, api::DeleteParams};
use testing_framework_core::scenario::CleanupGuard;
use testing_framework_core::scenario::internal::CleanupGuard;
use tokio::{
process::Command,
runtime::{Handle, Runtime},

View File

@ -10,10 +10,14 @@ use std::{
use async_trait::async_trait;
use testing_framework_core::{
scenario::{
Application, CleanupGuard, ClusterControlProfile, ClusterMode, Deployer, DeploymentPolicy,
DynError, FeedHandle, FeedRuntime, HttpReadinessRequirement, Metrics, NodeClients,
NodeControlCapability, NodeControlHandle, RetryPolicy, Runner, RuntimeAssembly, Scenario,
ScenarioError, SourceOrchestrationPlan, build_source_orchestration_plan, spawn_feed,
Application, ClusterControlProfile, ClusterMode, Deployer, DeploymentPolicy, DynError,
FeedRuntime, HttpReadinessRequirement, Metrics, NodeClients, NodeControlCapability,
NodeControlHandle, RetryPolicy, Runner, Scenario, ScenarioError,
internal::{
CleanupGuard, FeedHandle, RuntimeAssembly, SourceOrchestrationPlan,
build_source_orchestration_plan,
},
spawn_feed,
},
topology::DeploymentDescriptor,
};