use crate::{CoreError as Error, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum StepStatus {
Pending,
Running,
Passed,
Failed,
Skipped,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TestResult {
Passed,
Failed,
Skipped,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestStep {
pub name: String,
pub description: String,
pub status: StepStatus,
pub duration_ms: Option<u64>,
pub error: Option<String>,
}
impl TestStep {
pub fn new(name: &str, description: &str) -> Self {
Self {
name: name.to_string(),
description: description.to_string(),
status: StepStatus::Pending,
duration_ms: None,
error: None,
}
}
pub fn pass(&mut self, duration: Duration) {
self.status = StepStatus::Passed;
self.duration_ms = Some(duration.as_millis() as u64);
}
pub fn fail(&mut self, error: &str, duration: Duration) {
self.status = StepStatus::Failed;
self.duration_ms = Some(duration.as_millis() as u64);
self.error = Some(error.to_string());
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestScenario {
pub name: String,
pub steps: Vec<TestStep>,
pub result: Option<TestResult>,
pub total_duration_ms: Option<u64>,
}
impl TestScenario {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
steps: Vec::new(),
result: None,
total_duration_ms: None,
}
}
pub fn with_step(mut self, name: &str) -> Self {
self.steps.push(TestStep::new(name, ""));
self
}
pub fn with_step_desc(mut self, name: &str, description: &str) -> Self {
self.steps.push(TestStep::new(name, description));
self
}
pub fn execute(&mut self, env: &TestEnvironment) -> Result<TestResult> {
let start = Instant::now();
let mut all_passed = true;
for step in &mut self.steps {
if !env.is_service_running(&step.name) && env.services.contains_key(&step.name) {
step.status = StepStatus::Skipped;
continue;
}
step.status = StepStatus::Running;
let step_start = Instant::now();
if env.should_fail(&step.name) {
step.fail("Step failed", step_start.elapsed());
all_passed = false;
} else {
step.pass(step_start.elapsed());
}
}
self.total_duration_ms = Some(start.elapsed().as_millis() as u64);
self.result = Some(if all_passed {
TestResult::Passed
} else {
TestResult::Failed
});
Ok(self.result.unwrap())
}
pub fn pass_rate(&self) -> f64 {
if self.steps.is_empty() {
return 0.0;
}
let passed = self
.steps
.iter()
.filter(|s| s.status == StepStatus::Passed)
.count();
(passed as f64 / self.steps.len() as f64) * 100.0
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestService {
pub name: String,
pub is_running: bool,
pub port: Option<u16>,
pub health_endpoint: Option<String>,
}
impl TestService {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
is_running: false,
port: None,
health_endpoint: None,
}
}
pub fn start(&mut self) {
self.is_running = true;
}
pub fn stop(&mut self) {
self.is_running = false;
}
}
pub struct TestEnvironment {
pub services: HashMap<String, TestService>,
pub env_vars: HashMap<String, String>,
failure_injections: HashMap<String, bool>,
}
impl TestEnvironment {
pub fn new() -> Self {
Self {
services: HashMap::new(),
env_vars: HashMap::new(),
failure_injections: HashMap::new(),
}
}
pub fn register_service(&mut self, name: &str) {
self.services
.insert(name.to_string(), TestService::new(name));
}
pub fn start_service(&mut self, name: &str) -> Result<()> {
let service = self
.services
.get_mut(name)
.ok_or_else(|| Error::Validation(format!("Service {} not found", name)))?;
service.start();
Ok(())
}
pub fn stop_service(&mut self, name: &str) -> Result<()> {
let service = self
.services
.get_mut(name)
.ok_or_else(|| Error::Validation(format!("Service {} not found", name)))?;
service.stop();
Ok(())
}
pub fn is_service_running(&self, name: &str) -> bool {
self.services
.get(name)
.map(|s| s.is_running)
.unwrap_or(false)
}
pub fn set_env(&mut self, key: &str, value: &str) {
self.env_vars.insert(key.to_string(), value.to_string());
}
pub fn get_env(&self, key: &str) -> Option<&str> {
self.env_vars.get(key).map(|s| s.as_str())
}
pub fn inject_failure(&mut self, name: &str) {
self.failure_injections.insert(name.to_string(), true);
}
pub fn clear_failure(&mut self, name: &str) {
self.failure_injections.remove(name);
}
fn should_fail(&self, name: &str) -> bool {
self.failure_injections.get(name).copied().unwrap_or(false)
}
pub fn cleanup(&mut self) {
for service in self.services.values_mut() {
service.stop();
}
}
}
impl Default for TestEnvironment {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServiceContract {
pub service_name: String,
pub expected_request: String,
pub expected_response: String,
pub version: String,
}
impl ServiceContract {
pub fn new(service_name: &str, version: &str) -> Self {
Self {
service_name: service_name.to_string(),
expected_request: "{}".to_string(),
expected_response: "{}".to_string(),
version: version.to_string(),
}
}
pub fn validate(&self) -> Result<()> {
Ok(())
}
}
pub struct IntegrationTestSuite {
pub name: String,
pub scenarios: Vec<TestScenario>,
pub contracts: Vec<ServiceContract>,
}
impl IntegrationTestSuite {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
scenarios: Vec::new(),
contracts: Vec::new(),
}
}
pub fn add_scenario(&mut self, scenario: TestScenario) {
self.scenarios.push(scenario);
}
pub fn add_contract(&mut self, contract: ServiceContract) {
self.contracts.push(contract);
}
pub fn execute_all(&mut self, env: &TestEnvironment) -> SuiteResult {
let start = Instant::now();
let mut passed = 0;
let mut failed = 0;
for scenario in &mut self.scenarios {
match scenario.execute(env) {
Ok(TestResult::Passed) => passed += 1,
_ => failed += 1,
}
}
let mut contract_failures = 0;
for contract in &self.contracts {
if contract.validate().is_err() {
contract_failures += 1;
}
}
SuiteResult {
total_scenarios: self.scenarios.len(),
passed_scenarios: passed,
failed_scenarios: failed,
contract_failures,
duration_ms: start.elapsed().as_millis() as u64,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SuiteResult {
pub total_scenarios: usize,
pub passed_scenarios: usize,
pub failed_scenarios: usize,
pub contract_failures: usize,
pub duration_ms: u64,
}
impl SuiteResult {
pub fn success_rate(&self) -> f64 {
if self.total_scenarios == 0 {
return 0.0;
}
(self.passed_scenarios as f64 / self.total_scenarios as f64) * 100.0
}
pub fn all_passed(&self) -> bool {
self.failed_scenarios == 0 && self.contract_failures == 0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_test_environment_creation() {
let env = TestEnvironment::new();
assert_eq!(env.services.len(), 0);
}
#[test]
fn test_register_and_start_service() {
let mut env = TestEnvironment::new();
env.register_service("database");
assert!(env.start_service("database").is_ok());
assert!(env.is_service_running("database"));
}
#[test]
fn test_stop_service() {
let mut env = TestEnvironment::new();
env.register_service("api");
env.start_service("api").unwrap();
assert!(env.is_service_running("api"));
env.stop_service("api").unwrap();
assert!(!env.is_service_running("api"));
}
#[test]
fn test_environment_variables() {
let mut env = TestEnvironment::new();
env.set_env("DATABASE_URL", "postgresql://localhost/test");
assert_eq!(
env.get_env("DATABASE_URL"),
Some("postgresql://localhost/test")
);
}
#[test]
fn test_test_scenario_creation() {
let scenario = TestScenario::new("user_flow")
.with_step("register")
.with_step("login")
.with_step("logout");
assert_eq!(scenario.steps.len(), 3);
}
#[test]
fn test_scenario_execution() {
let mut env = TestEnvironment::new();
env.register_service("api");
env.start_service("api").unwrap();
let mut scenario = TestScenario::new("simple_test").with_step("test_step");
let result = scenario.execute(&env).unwrap();
assert_eq!(result, TestResult::Passed);
}
#[test]
fn test_failure_injection() {
let mut env = TestEnvironment::new();
env.inject_failure("test_step");
let mut scenario = TestScenario::new("failing_test").with_step("test_step");
let result = scenario.execute(&env).unwrap();
assert_eq!(result, TestResult::Failed);
}
#[test]
fn test_integration_suite() {
let mut env = TestEnvironment::new();
env.register_service("api");
env.start_service("api").unwrap();
let mut suite = IntegrationTestSuite::new("Main Suite");
suite.add_scenario(TestScenario::new("test1").with_step("step1"));
suite.add_scenario(TestScenario::new("test2").with_step("step2"));
let result = suite.execute_all(&env);
assert_eq!(result.total_scenarios, 2);
assert!(result.all_passed());
}
#[test]
fn test_contract_validation() {
let contract = ServiceContract::new("user_service", "v1");
assert!(contract.validate().is_ok());
}
#[test]
fn test_suite_success_rate() {
let result = SuiteResult {
total_scenarios: 10,
passed_scenarios: 8,
failed_scenarios: 2,
contract_failures: 0,
duration_ms: 1000,
};
assert_eq!(result.success_rate(), 80.0);
}
}