use std::fs;
use std::path::{Path, PathBuf};
fn collect_rust_files(dir: &Path, out: &mut Vec<PathBuf>) {
let entries = fs::read_dir(dir).expect("failed to read directory");
for entry in entries {
let entry = entry.expect("failed to read directory entry");
let path = entry.path();
if path.is_dir() {
collect_rust_files(&path, out);
} else if path.extension().is_some_and(|ext| ext == "rs") {
out.push(path);
}
}
}
fn assert_no_forbidden_imports(layer_dir: &str, forbidden: &[&str]) {
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("src")
.join(layer_dir);
let mut files = Vec::new();
collect_rust_files(&root, &mut files);
for file in files {
let content = fs::read_to_string(&file).expect("failed to read source file");
for pattern in forbidden {
assert!(
!content.contains(pattern),
"hexagonal boundary violation in {}: found forbidden import pattern `{}`",
file.display(),
pattern
);
}
}
}
fn assert_file_contains(path: &Path, required: &[&str]) {
let content = fs::read_to_string(path).expect("failed to read source file");
for pattern in required {
assert!(
content.contains(pattern),
"expected pattern `{}` in {}",
pattern,
path.display()
);
}
}
fn assert_file_not_contains(path: &Path, forbidden: &[&str]) {
let content = fs::read_to_string(path).expect("failed to read source file");
for pattern in forbidden {
assert!(
!content.contains(pattern),
"found forbidden pattern `{}` in {}",
pattern,
path.display()
);
}
}
fn assert_file_prefix_not_contains(path: &Path, split_at: &str, forbidden: &[&str]) {
let content = fs::read_to_string(path).expect("failed to read source file");
let prefix = match content.split_once(split_at) {
Some((before, _)) => before,
None => content.as_str(),
};
for pattern in forbidden {
assert!(
!prefix.contains(pattern),
"found forbidden pattern `{}` in {} before `{}`",
pattern,
path.display(),
split_at
);
}
}
fn assert_file_section_contains(path: &Path, section_start: &str, required: &[&str]) {
let content = fs::read_to_string(path).expect("failed to read source file");
let section = content
.split(section_start)
.nth(1)
.unwrap_or_else(|| panic!("missing section `{section_start}` in {}", path.display()));
let section = section.split("#[cfg(test)]").next().unwrap_or(section);
for pattern in required {
assert!(
section.contains(pattern),
"expected pattern `{}` in section `{}` of {}",
pattern,
section_start,
path.display()
);
}
}
fn assert_file_section_not_contains(path: &Path, section_start: &str, forbidden: &[&str]) {
let content = fs::read_to_string(path).expect("failed to read source file");
let section = content
.split(section_start)
.nth(1)
.unwrap_or_else(|| panic!("missing section `{section_start}` in {}", path.display()));
let section = section.split("#[cfg(test)]").next().unwrap_or(section);
for pattern in forbidden {
assert!(
!section.contains(pattern),
"found forbidden pattern `{}` in section `{}` of {}",
pattern,
section_start,
path.display()
);
}
}
#[test]
fn domain_layer_has_no_infrastructure_dependencies() {
assert_no_forbidden_imports(
"lifecycle/domain",
&[
"crate::lifecycle::adapters",
"crate::lifecycle::application",
"crate::context",
"crate::reload",
"crate::route_controller",
"crate::supervising_route_controller",
],
);
}
#[test]
fn application_layer_depends_on_ports_and_domain_only() {
assert_no_forbidden_imports(
"lifecycle/application",
&[
"crate::context",
"crate::reload",
"crate::route_controller",
"crate::supervising_route_controller",
],
);
}
#[test]
fn ports_layer_only_uses_approved_application_imports() {
assert_no_forbidden_imports(
"lifecycle/ports",
&[
"crate::lifecycle::adapters",
"crate::context",
"crate::reload",
"crate::route_controller",
"crate::supervising_route_controller",
"crate::lifecycle::application::commands",
"crate::lifecycle::application::runtime_bus",
"crate::lifecycle::application::queries",
"crate::lifecycle::application::supervision_service",
],
);
}
#[test]
fn domain_and_ports_do_not_import_runtime_contract_types_from_camel_api_directly() {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let route_runtime = root.join("lifecycle/domain/route_runtime.rs");
let runtime_ports = root.join("lifecycle/ports/runtime_ports.rs");
assert_file_not_contains(
&route_runtime,
&["use camel_api::{CamelError, RuntimeEvent};"],
);
assert_file_not_contains(
&runtime_ports,
&[
"use camel_api::{CamelError, RuntimeEvent};",
"use crate::route::RouteDefinition;",
],
);
assert_file_contains(
&runtime_ports,
&[
"use crate::lifecycle::application::route_definition::RouteDefinition",
"use crate::lifecycle::domain::{RouteRuntimeAggregate, RuntimeEvent}",
],
);
assert_file_not_contains(
&runtime_ports,
&["use crate::lifecycle::domain::{RouteDefinition"],
);
}
#[test]
fn runtime_bus_and_command_handlers_are_controller_agnostic() {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let runtime_bus = root.join("lifecycle/application/runtime_bus.rs");
let commands = root.join("lifecycle/application/commands.rs");
assert_file_not_contains(
&runtime_bus,
&[
"RouteControllerInternal",
"with_controller(",
"route_status(",
],
);
assert_file_not_contains(
&commands,
&[
"RouteControllerInternal",
"with_controller(",
"route_status(",
],
);
}
#[test]
fn runtime_side_effects_flow_through_execution_port() {
let commands = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("src")
.join("lifecycle/application/commands.rs");
assert_file_contains(
&commands,
&[
"RuntimeExecutionPort",
"pub execution: Option<Arc<dyn RuntimeExecutionPort>>",
"if let Some(execution) = &deps.execution",
],
);
}
#[test]
fn reload_runtime_path_does_not_use_controller_local_status_heuristics() {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let reload_watcher = root.join("hot_reload/adapters/reload_watcher.rs");
let reload = root.join("hot_reload/application/reload.rs");
assert_file_contains(&reload_watcher, &["runtime_route_ids()"]);
assert_file_contains(&reload, &["runtime_route_status("]);
assert_file_contains(
&reload,
&["pub(crate) fn compute_reload_actions_from_runtime_snapshot"],
);
assert_file_contains(&reload, &["pub async fn execute_reload_actions"]);
assert_file_prefix_not_contains(&reload, "#[cfg(test)]", &[".route_status("]);
}
#[test]
fn supervision_loop_decisions_are_runtime_query_driven() {
let supervising = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("src")
.join("lifecycle")
.join("adapters")
.join("controller_actor.rs");
assert_file_section_contains(
&supervising,
"pub fn spawn_supervision_task(",
&[
"controller: RouteControllerHandle",
"controller.restart_route(route_id.clone()).await",
],
);
assert_file_section_not_contains(
&supervising,
"pub fn spawn_supervision_task(",
&[
"RuntimeQuery::GetRouteStatus",
"RuntimeCommand::FailRoute",
"RuntimeCommand::ReloadRoute",
".route_status(",
],
);
}
#[test]
fn runtime_execution_handle_has_no_raw_controller_escape_hatch() {
let context = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("src")
.join("context.rs");
assert_file_not_contains(&context, &["fn raw(&self)"]);
assert_file_contains(
&context,
&["ProducerContext::new().with_runtime(self.runtime())"],
);
assert_file_not_contains(&context, &["ProducerContext::new(route_controller)"]);
}
#[test]
fn public_route_controller_trait_exposes_no_lifecycle_read_model() {
let route_controller_api = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("camel-api")
.join("src")
.join("route_controller.rs");
assert_file_not_contains(&route_controller_api, &["fn route_status("]);
let core_lib = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("src")
.join("lib.rs");
assert_file_not_contains(
&core_lib,
&["pub use route_controller::{DefaultRouteController, RouteControllerInternal};"],
);
}
#[test]
fn runtime_execution_adapter_uses_semantic_executor_naming() {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let adapters_mod = root.join("lifecycle/adapters/mod.rs");
let context = root.join("context.rs");
assert_file_contains(&adapters_mod, &["RuntimeExecutionAdapter"]);
assert_file_contains(&context, &["RuntimeExecutionAdapter::new("]);
assert_file_not_contains(&context, &["RouteControllerExecutionAdapter::new("]);
}
#[test]
fn domain_has_no_tower_or_framework_types() {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/lifecycle/domain");
let mut files = Vec::new();
collect_rust_files(&root, &mut files);
let forbidden = &[
"tower::",
"BoxProcessor",
"compose_pipeline",
"TracingProcessor",
"use camel_component_api::",
];
for file in &files {
let content = std::fs::read_to_string(file).expect("failed to read source file");
for pattern in forbidden {
assert!(
!content.contains(pattern),
"domain boundary violation in {}: found forbidden pattern `{}`",
file.display(),
pattern
);
}
}
}
#[test]
fn ports_imports_route_definition_from_application_not_adapters() {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let ports_file = root.join("lifecycle/ports/runtime_ports.rs");
assert_file_contains(
&ports_file,
&["use crate::lifecycle::application::route_definition::RouteDefinition"],
);
assert_file_not_contains(&ports_file, &["crate::lifecycle::adapters::"]);
}
#[test]
fn domain_does_not_import_application_or_adapters() {
assert_no_forbidden_imports(
"lifecycle/domain",
&[
"crate::lifecycle::application::",
"crate::lifecycle::adapters::",
],
);
}
#[test]
fn lifecycle_domain_does_not_import_hot_reload() {
assert_no_forbidden_imports("lifecycle/domain", &["crate::hot_reload", "hot_reload::"]);
}
#[test]
fn lifecycle_domain_does_not_import_lifecycle_application_or_adapters() {
assert_no_forbidden_imports(
"lifecycle/domain",
&[
"crate::lifecycle::application",
"crate::lifecycle::adapters",
],
);
}
#[test]
fn lifecycle_application_does_not_import_hot_reload() {
assert_no_forbidden_imports(
"lifecycle/application",
&["crate::hot_reload", "hot_reload::"],
);
}
#[test]
fn hot_reload_does_not_import_lifecycle_domain_directly() {
assert_no_forbidden_imports("hot_reload", &["crate::lifecycle::domain"]);
}
#[test]
fn lifecycle_ports_registration_port_is_pub_crate_only() {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("src/lifecycle/ports/registration_port.rs");
let content = std::fs::read_to_string(&path).expect("failed to read registration_port.rs");
assert!(
content.contains("pub(crate) trait RouteRegistrationPort"),
"RouteRegistrationPort must be pub(crate), not pub"
);
assert!(
!content.contains("pub trait RouteRegistrationPort"),
"RouteRegistrationPort must NOT be pub (only pub(crate))"
);
}
#[test]
fn lifecycle_does_not_use_old_flat_config_path() {
assert_no_forbidden_imports(
"lifecycle",
&["crate::config::", "crate::tracer::", "crate::registry::"],
);
}
#[test]
fn hot_reload_does_not_use_old_flat_config_path() {
assert_no_forbidden_imports(
"hot_reload",
&["crate::config::", "crate::tracer::", "crate::registry::"],
);
}