use axum::{
extract::Path,
http::StatusCode,
response::{IntoResponse, Json},
};
use chrono::{DateTime, Duration, Utc};
use mockforge_core::{
time_travel::cron::{CronJob, CronJobAction},
RepeatConfig, ScheduledResponse, TimeScenario, TimeTravelManager, VirtualClock,
};
use mockforge_vbr::{MutationOperation, MutationRule, MutationRuleManager, MutationTrigger};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use tracing::info;
static TIME_TRAVEL_MANAGER: once_cell::sync::OnceCell<Arc<RwLock<Option<Arc<TimeTravelManager>>>>> =
once_cell::sync::OnceCell::new();
static SCENARIO_STORE: once_cell::sync::Lazy<Arc<RwLock<HashMap<String, TimeScenario>>>> =
once_cell::sync::Lazy::new(|| Arc::new(RwLock::new(HashMap::new())));
pub fn init_time_travel_manager(manager: Arc<TimeTravelManager>) {
let cell = TIME_TRAVEL_MANAGER.get_or_init(|| Arc::new(RwLock::new(None)));
let mut guard = cell.write().unwrap();
*guard = Some(manager);
}
fn get_time_travel_manager() -> Option<Arc<TimeTravelManager>> {
TIME_TRAVEL_MANAGER.get().and_then(|cell| cell.read().unwrap().clone())
}
fn save_scenario_to_store(scenario: TimeScenario) {
let mut store = SCENARIO_STORE.write().unwrap();
store.insert(scenario.name.clone(), scenario);
}
fn load_scenario_from_store(name: &str) -> Option<TimeScenario> {
let store = SCENARIO_STORE.read().unwrap();
store.get(name).cloned()
}
static MUTATION_RULE_MANAGER: once_cell::sync::OnceCell<
Arc<RwLock<Option<Arc<MutationRuleManager>>>>,
> = once_cell::sync::OnceCell::new();
pub fn init_mutation_rule_manager(manager: Arc<MutationRuleManager>) {
let cell = MUTATION_RULE_MANAGER.get_or_init(|| Arc::new(RwLock::new(None)));
let mut guard = cell.write().unwrap();
*guard = Some(manager);
}
fn get_mutation_rule_manager() -> Option<Arc<MutationRuleManager>> {
MUTATION_RULE_MANAGER.get().and_then(|cell| cell.read().unwrap().clone())
}
#[derive(Debug, Serialize, Deserialize)]
pub struct EnableTimeTravelRequest {
pub time: Option<DateTime<Utc>>,
pub scale: Option<f64>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AdvanceTimeRequest {
pub duration: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SetScaleRequest {
pub scale: f64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ScheduleResponseRequest {
pub trigger_time: String,
pub body: serde_json::Value,
#[serde(default = "default_status")]
pub status: u16,
#[serde(default)]
pub headers: HashMap<String, String>,
pub name: Option<String>,
pub repeat: Option<RepeatConfig>,
}
fn default_status() -> u16 {
200
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ScheduleResponseResponse {
pub id: String,
pub trigger_time: DateTime<Utc>,
}
pub async fn get_time_travel_status() -> impl IntoResponse {
match get_time_travel_manager() {
Some(manager) => {
let status = manager.clock().status();
Json(status).into_response()
}
None => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": "Time travel not initialized"
})),
)
.into_response(),
}
}
pub async fn enable_time_travel(Json(req): Json<EnableTimeTravelRequest>) -> impl IntoResponse {
match get_time_travel_manager() {
Some(manager) => {
let time = req.time.unwrap_or_else(Utc::now);
manager.enable_and_set(time);
if let Some(scale) = req.scale {
manager.set_scale(scale);
}
info!("Time travel enabled at {}", time);
Json(serde_json::json!({
"success": true,
"status": manager.clock().status()
}))
.into_response()
}
None => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": "Time travel not initialized"
})),
)
.into_response(),
}
}
pub async fn disable_time_travel() -> impl IntoResponse {
match get_time_travel_manager() {
Some(manager) => {
manager.disable();
info!("Time travel disabled");
Json(serde_json::json!({
"success": true,
"status": manager.clock().status()
}))
.into_response()
}
None => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": "Time travel not initialized"
})),
)
.into_response(),
}
}
pub async fn advance_time(Json(req): Json<AdvanceTimeRequest>) -> impl IntoResponse {
match get_time_travel_manager() {
Some(manager) => {
let duration = parse_duration(&req.duration);
match duration {
Ok(dur) => {
manager.advance(dur);
info!("Time advanced by {}", req.duration);
Json(serde_json::json!({
"success": true,
"status": manager.clock().status()
}))
.into_response()
}
Err(e) => (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": format!("Invalid duration format: {}", e)
})),
)
.into_response(),
}
}
None => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": "Time travel not initialized"
})),
)
.into_response(),
}
}
pub async fn set_time_scale(Json(req): Json<SetScaleRequest>) -> impl IntoResponse {
match get_time_travel_manager() {
Some(manager) => {
manager.set_scale(req.scale);
info!("Time scale set to {}x", req.scale);
Json(serde_json::json!({
"success": true,
"status": manager.clock().status()
}))
.into_response()
}
None => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": "Time travel not initialized"
})),
)
.into_response(),
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SetTimeRequest {
pub time: DateTime<Utc>,
}
pub async fn set_time(Json(req): Json<SetTimeRequest>) -> impl IntoResponse {
match get_time_travel_manager() {
Some(manager) => {
manager.clock().set_time(req.time);
info!("Virtual time set to {}", req.time);
Json(serde_json::json!({
"success": true,
"status": manager.clock().status()
}))
.into_response()
}
None => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": "Time travel not initialized"
})),
)
.into_response(),
}
}
pub async fn reset_time_travel() -> impl IntoResponse {
match get_time_travel_manager() {
Some(manager) => {
manager.clock().reset();
info!("Time travel reset");
Json(serde_json::json!({
"success": true,
"status": manager.clock().status()
}))
.into_response()
}
None => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": "Time travel not initialized"
})),
)
.into_response(),
}
}
pub async fn schedule_response(Json(req): Json<ScheduleResponseRequest>) -> impl IntoResponse {
match get_time_travel_manager() {
Some(manager) => {
let trigger_time = parse_trigger_time(&req.trigger_time, manager.clock());
match trigger_time {
Ok(time) => {
let scheduled_response = ScheduledResponse {
id: uuid::Uuid::new_v4().to_string(),
trigger_time: time,
body: req.body,
status: req.status,
headers: req.headers,
name: req.name,
repeat: req.repeat,
};
match manager.scheduler().schedule(scheduled_response.clone()) {
Ok(id) => {
info!("Scheduled response {} for {}", id, time);
Json(ScheduleResponseResponse {
id,
trigger_time: time,
})
.into_response()
}
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": format!("Failed to schedule response: {}", e)
})),
)
.into_response(),
}
}
Err(e) => (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": format!("Invalid trigger time: {}", e)
})),
)
.into_response(),
}
}
None => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": "Time travel not initialized"
})),
)
.into_response(),
}
}
pub async fn list_scheduled_responses() -> impl IntoResponse {
match get_time_travel_manager() {
Some(manager) => {
let scheduled = manager.scheduler().list_scheduled();
Json(scheduled).into_response()
}
None => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": "Time travel not initialized"
})),
)
.into_response(),
}
}
pub async fn cancel_scheduled_response(Path(id): Path<String>) -> impl IntoResponse {
match get_time_travel_manager() {
Some(manager) => {
let cancelled = manager.scheduler().cancel(&id);
if cancelled {
info!("Cancelled scheduled response {}", id);
Json(serde_json::json!({
"success": true
}))
.into_response()
} else {
(
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": "Scheduled response not found"
})),
)
.into_response()
}
}
None => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": "Time travel not initialized"
})),
)
.into_response(),
}
}
pub async fn clear_scheduled_responses() -> impl IntoResponse {
match get_time_travel_manager() {
Some(manager) => {
manager.scheduler().clear_all();
info!("Cleared all scheduled responses");
Json(serde_json::json!({
"success": true
}))
.into_response()
}
None => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": "Time travel not initialized"
})),
)
.into_response(),
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SaveScenarioRequest {
pub name: String,
pub description: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LoadScenarioRequest {
pub name: String,
}
pub async fn save_scenario(Json(req): Json<SaveScenarioRequest>) -> impl IntoResponse {
match get_time_travel_manager() {
Some(manager) => {
let mut scenario = manager.save_scenario(req.name.clone());
scenario.description = req.description;
save_scenario_to_store(scenario.clone());
Json(scenario).into_response()
}
None => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": "Time travel not initialized"
})),
)
.into_response(),
}
}
pub async fn load_scenario(Json(req): Json<LoadScenarioRequest>) -> impl IntoResponse {
match get_time_travel_manager() {
Some(manager) => {
if let Some(scenario) = load_scenario_from_store(&req.name) {
manager.load_scenario(&scenario);
Json(serde_json::json!({
"success": true,
"scenario": scenario,
}))
.into_response()
} else {
(
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": format!("Scenario '{}' not found", req.name)
})),
)
.into_response()
}
}
None => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": "Time travel not initialized"
})),
)
.into_response(),
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateCronJobRequest {
pub id: String,
pub name: String,
pub schedule: String,
#[serde(default)]
pub description: Option<String>,
pub action_type: String,
#[serde(default)]
pub action_metadata: serde_json::Value,
}
pub async fn list_cron_jobs() -> impl IntoResponse {
match get_time_travel_manager() {
Some(manager) => {
let jobs = manager.cron_scheduler().list_jobs().await;
Json(serde_json::json!({
"success": true,
"jobs": jobs
}))
.into_response()
}
None => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": "Time travel not initialized"
})),
)
.into_response(),
}
}
pub async fn get_cron_job(Path(id): Path<String>) -> impl IntoResponse {
match get_time_travel_manager() {
Some(manager) => match manager.cron_scheduler().get_job(&id).await {
Some(job) => Json(serde_json::json!({
"success": true,
"job": job
}))
.into_response(),
None => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": format!("Cron job '{}' not found", id)
})),
)
.into_response(),
},
None => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": "Time travel not initialized"
})),
)
.into_response(),
}
}
pub async fn create_cron_job(Json(req): Json<CreateCronJobRequest>) -> impl IntoResponse {
match get_time_travel_manager() {
Some(manager) => {
let mut job = CronJob::new(req.id.clone(), req.name.clone(), req.schedule.clone());
if let Some(desc) = req.description {
job.description = Some(desc);
}
let action = match req.action_type.as_str() {
"callback" => {
let job_id = req.id.clone();
CronJobAction::Callback(Box::new(move |_| {
info!("Cron job '{}' executed (callback)", job_id);
Ok(())
}))
}
"response" => {
let body =
req.action_metadata.get("body").cloned().unwrap_or(serde_json::json!({}));
let status = req
.action_metadata
.get("status")
.and_then(|v| v.as_u64())
.map(|v| v as u16)
.unwrap_or(200);
let headers = req
.action_metadata
.get("headers")
.and_then(|v| v.as_object())
.map(|obj| {
obj.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
.collect()
})
.unwrap_or_default();
CronJobAction::ScheduledResponse {
body,
status,
headers,
}
}
"mutation" => {
let entity = req
.action_metadata
.get("entity")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.unwrap_or_default();
let operation = req
.action_metadata
.get("operation")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.unwrap_or_default();
CronJobAction::DataMutation { entity, operation }
}
_ => {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": format!("Invalid action type: {}", req.action_type)
})),
)
.into_response();
}
};
match manager.cron_scheduler().add_job(job, action).await {
Ok(_) => {
info!("Created cron job '{}'", req.id);
Json(serde_json::json!({
"success": true,
"message": format!("Cron job '{}' created", req.id)
}))
.into_response()
}
Err(e) => (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": e.to_string()
})),
)
.into_response(),
}
}
None => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": "Time travel not initialized"
})),
)
.into_response(),
}
}
pub async fn delete_cron_job(Path(id): Path<String>) -> impl IntoResponse {
match get_time_travel_manager() {
Some(manager) => {
let removed = manager.cron_scheduler().remove_job(&id).await;
if removed {
info!("Deleted cron job '{}'", id);
Json(serde_json::json!({
"success": true,
"message": format!("Cron job '{}' deleted", id)
}))
.into_response()
} else {
(
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": format!("Cron job '{}' not found", id)
})),
)
.into_response()
}
}
None => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": "Time travel not initialized"
})),
)
.into_response(),
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SetCronJobEnabledRequest {
pub enabled: bool,
}
pub async fn set_cron_job_enabled(
Path(id): Path<String>,
Json(req): Json<SetCronJobEnabledRequest>,
) -> impl IntoResponse {
match get_time_travel_manager() {
Some(manager) => match manager.cron_scheduler().set_job_enabled(&id, req.enabled).await {
Ok(_) => {
info!("Cron job '{}' {}", id, if req.enabled { "enabled" } else { "disabled" });
Json(serde_json::json!({
"success": true,
"message": format!("Cron job '{}' {}", id, if req.enabled { "enabled" } else { "disabled" })
}))
.into_response()
}
Err(e) => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": e.to_string()
})),
)
.into_response(),
},
None => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": "Time travel not initialized"
})),
)
.into_response(),
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateMutationRuleRequest {
pub id: String,
pub entity_name: String,
pub trigger: MutationTrigger,
pub operation: MutationOperation,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub condition: Option<String>,
}
pub async fn list_mutation_rules() -> impl IntoResponse {
match get_mutation_rule_manager() {
Some(manager) => {
let rules = manager.list_rules().await;
Json(serde_json::json!({
"success": true,
"rules": rules
}))
.into_response()
}
None => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": "Mutation rule manager not initialized"
})),
)
.into_response(),
}
}
pub async fn get_mutation_rule(Path(id): Path<String>) -> impl IntoResponse {
match get_mutation_rule_manager() {
Some(manager) => match manager.get_rule(&id).await {
Some(rule) => Json(serde_json::json!({
"success": true,
"rule": rule
}))
.into_response(),
None => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": format!("Mutation rule '{}' not found", id)
})),
)
.into_response(),
},
None => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": "Mutation rule manager not initialized"
})),
)
.into_response(),
}
}
pub async fn create_mutation_rule(Json(req): Json<CreateMutationRuleRequest>) -> impl IntoResponse {
match get_mutation_rule_manager() {
Some(manager) => {
let mut rule = MutationRule::new(
req.id.clone(),
req.entity_name.clone(),
req.trigger.clone(),
req.operation.clone(),
);
if let Some(desc) = req.description {
rule.description = Some(desc);
}
if let Some(cond) = req.condition {
rule.condition = Some(cond);
}
match manager.add_rule(rule).await {
Ok(_) => {
info!("Created mutation rule '{}'", req.id);
Json(serde_json::json!({
"success": true,
"message": format!("Mutation rule '{}' created", req.id)
}))
.into_response()
}
Err(e) => (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": e.to_string().to_string()
})),
)
.into_response(),
}
}
None => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": "Mutation rule manager not initialized"
})),
)
.into_response(),
}
}
pub async fn delete_mutation_rule(Path(id): Path<String>) -> impl IntoResponse {
match get_mutation_rule_manager() {
Some(manager) => {
let removed = manager.remove_rule(&id).await;
if removed {
info!("Deleted mutation rule '{}'", id);
Json(serde_json::json!({
"success": true,
"message": format!("Mutation rule '{}' deleted", id)
}))
.into_response()
} else {
(
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": format!("Mutation rule '{}' not found", id)
})),
)
.into_response()
}
}
None => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": "Mutation rule manager not initialized"
})),
)
.into_response(),
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SetMutationRuleEnabledRequest {
pub enabled: bool,
}
pub async fn set_mutation_rule_enabled(
Path(id): Path<String>,
Json(req): Json<SetMutationRuleEnabledRequest>,
) -> impl IntoResponse {
match get_mutation_rule_manager() {
Some(manager) => match manager.set_rule_enabled(&id, req.enabled).await {
Ok(_) => {
info!(
"Mutation rule '{}' {}",
id,
if req.enabled { "enabled" } else { "disabled" }
);
Json(serde_json::json!({
"success": true,
"message": format!("Mutation rule '{}' {}", id, if req.enabled { "enabled" } else { "disabled" })
}))
.into_response()
}
Err(e) => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": e.to_string()
})),
)
.into_response(),
},
None => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": "Mutation rule manager not initialized"
})),
)
.into_response(),
}
}
fn parse_duration(s: &str) -> Result<Duration, String> {
let s = s.trim();
if s.is_empty() {
return Err("Empty duration string".to_string());
}
let s = s.strip_prefix('+').unwrap_or(s);
let s = s.strip_prefix('-').unwrap_or(s);
if s.ends_with("week") || s.ends_with("weeks") || s.ends_with(" week") || s.ends_with(" weeks")
{
let num_str = s
.trim_end_matches("week")
.trim_end_matches("weeks")
.trim_end_matches(" week")
.trim_end_matches(" weeks")
.trim();
let amount: i64 =
num_str.parse().map_err(|e| format!("Invalid number for weeks: {}", e))?;
return Ok(Duration::days(amount * 7));
}
if s.ends_with("month")
|| s.ends_with("months")
|| s.ends_with(" month")
|| s.ends_with(" months")
{
let num_str = s
.trim_end_matches("month")
.trim_end_matches("months")
.trim_end_matches(" month")
.trim_end_matches(" months")
.trim();
let amount: i64 =
num_str.parse().map_err(|e| format!("Invalid number for months: {}", e))?;
return Ok(Duration::days(amount * 30));
}
if s.ends_with('y')
|| s.ends_with("year")
|| s.ends_with("years")
|| s.ends_with(" year")
|| s.ends_with(" years")
{
let num_str = s
.trim_end_matches('y')
.trim_end_matches("year")
.trim_end_matches("years")
.trim_end_matches(" year")
.trim_end_matches(" years")
.trim();
let amount: i64 =
num_str.parse().map_err(|e| format!("Invalid number for years: {}", e))?;
return Ok(Duration::days(amount * 365));
}
let (num_str, unit) = if let Some(pos) = s.chars().position(|c| !c.is_numeric() && c != '-') {
(&s[..pos], &s[pos..].trim())
} else {
return Err("No unit specified (use s, m, h, d, week, month, or year)".to_string());
};
let amount: i64 = num_str.parse().map_err(|e| format!("Invalid number: {}", e))?;
match *unit {
"s" | "sec" | "secs" | "second" | "seconds" => Ok(Duration::seconds(amount)),
"m" | "min" | "mins" | "minute" | "minutes" => Ok(Duration::minutes(amount)),
"h" | "hr" | "hrs" | "hour" | "hours" => Ok(Duration::hours(amount)),
"d" | "day" | "days" => Ok(Duration::days(amount)),
"w" | "week" | "weeks" => Ok(Duration::days(amount * 7)),
_ => Err(format!("Unknown unit: {}. Use s, m, h, d, week, month, or year", unit)),
}
}
fn parse_trigger_time(s: &str, clock: Arc<VirtualClock>) -> Result<DateTime<Utc>, String> {
let s = s.trim();
if s.starts_with('+') || s.starts_with('-') {
let duration = parse_duration(&s[1..])?;
let current = clock.now();
if s.starts_with('+') {
Ok(current + duration)
} else {
Ok(current - duration)
}
} else {
DateTime::parse_from_rfc3339(s)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| format!("Invalid ISO 8601 date: {}", e))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_duration() {
assert_eq!(parse_duration("10s").unwrap(), Duration::seconds(10));
assert_eq!(parse_duration("30m").unwrap(), Duration::minutes(30));
assert_eq!(parse_duration("2h").unwrap(), Duration::hours(2));
assert_eq!(parse_duration("1d").unwrap(), Duration::days(1));
assert!(parse_duration("").is_err());
assert!(parse_duration("10").is_err());
assert!(parse_duration("10x").is_err());
}
#[test]
fn test_parse_trigger_time_relative() {
let clock = Arc::new(VirtualClock::new());
let now = Utc::now();
clock.enable_and_set(now);
let future = parse_trigger_time("+1h", clock.clone()).unwrap();
assert!((future - now - Duration::hours(1)).num_seconds().abs() < 1);
let past = parse_trigger_time("-30m", clock.clone()).unwrap();
assert!((past - now + Duration::minutes(30)).num_seconds().abs() < 1);
}
#[test]
fn test_scenario_store_roundtrip() {
let scenario = TimeScenario {
name: "roundtrip".to_string(),
enabled: true,
current_time: Some(Utc::now()),
scale_factor: 1.5,
scheduled_responses: Vec::new(),
created_at: Utc::now(),
description: Some("test".to_string()),
};
save_scenario_to_store(scenario.clone());
let loaded = load_scenario_from_store("roundtrip").expect("scenario");
assert_eq!(loaded.name, scenario.name);
assert_eq!(loaded.scale_factor, scenario.scale_factor);
assert_eq!(loaded.description, scenario.description);
}
}