use std::collections::HashMap;
use std::time::Duration;
use super::super::effects::*;
use super::super::tempo::TempoMap;
fn build_fixture_states(
fixture_registry: &HashMap<String, FixtureInfo>,
effect: &EffectInstance,
mut apply_fn: impl FnMut(&FixtureProfile) -> HashMap<String, ChannelState>,
) -> HashMap<String, FixtureState> {
let mut fixture_states = HashMap::new();
for fixture_name in &effect.target_fixtures {
if let Some(fixture) = fixture_registry.get(fixture_name) {
let profile = FixtureProfile::for_fixture(fixture);
let channel_commands = apply_fn(profile);
fixture_states.insert(
fixture_name.clone(),
FixtureState::from_channels(channel_commands),
);
}
}
fixture_states
}
fn build_fixture_states_with_info(
fixture_registry: &HashMap<String, FixtureInfo>,
effect: &EffectInstance,
mut apply_fn: impl FnMut(&FixtureInfo, &FixtureProfile) -> FixtureState,
) -> HashMap<String, FixtureState> {
let mut fixture_states = HashMap::new();
for fixture_name in &effect.target_fixtures {
if let Some(fixture) = fixture_registry.get(fixture_name) {
let profile = FixtureProfile::for_fixture(fixture);
let fixture_state = apply_fn(fixture, profile);
fixture_states.insert(fixture_name.clone(), fixture_state);
}
}
fixture_states
}
#[inline]
fn cycle_progress(elapsed: Duration, cycle_period: f64) -> f64 {
(elapsed.as_secs_f64() % cycle_period) / cycle_period
}
#[inline]
fn phase(elapsed: Duration, frequency: f64) -> f64 {
elapsed.as_secs_f64() * frequency * 2.0 * std::f64::consts::PI
}
fn calculate_color_indices(
cycle_progress: f64,
color_count: usize,
direction: &CycleDirection,
) -> (usize, usize, f64) {
match direction {
CycleDirection::Forward => {
let color_index_f = cycle_progress * color_count as f64;
let color_index = color_index_f.floor() as usize;
let next_index = (color_index + 1) % color_count;
let segment_progress = color_index_f - color_index as f64;
(color_index, next_index, segment_progress)
}
CycleDirection::Backward => {
let reversed_progress = 1.0 - cycle_progress;
let color_index_f = reversed_progress * color_count as f64;
let color_index = color_index_f.floor() as usize;
if color_index >= color_count {
(color_count - 1, color_count - 1, 0.0)
} else {
let next_index = if color_index == 0 {
color_count - 1
} else {
color_index - 1
};
let segment_progress = color_index_f - color_index as f64;
(color_index, next_index, segment_progress)
}
}
CycleDirection::PingPong => {
let ping_pong_progress = if cycle_progress < 0.5 {
cycle_progress * 2.0
} else {
2.0 - cycle_progress * 2.0
};
let max_index = (color_count - 1) as f64;
let color_progress = ping_pong_progress * max_index;
let color_index = color_progress.floor() as usize;
let seg_progress = color_progress - color_index as f64;
if color_index >= color_count - 1 {
(color_count - 1, color_count - 1, 0.0)
} else {
(color_index, color_index + 1, seg_progress)
}
}
}
}
pub(crate) fn process_effect(
fixture_registry: &HashMap<String, FixtureInfo>,
effect: &EffectInstance,
elapsed: Duration,
engine_elapsed: Duration,
tempo_map: Option<&TempoMap>,
) -> Result<Option<HashMap<String, FixtureState>>, EffectError> {
if !effect.enabled {
return Ok(None);
}
let absolute_time = engine_elapsed;
match &effect.effect_type {
EffectType::Static { parameters, .. } => {
apply_static_effect(fixture_registry, effect, parameters, elapsed)
}
EffectType::ColorCycle {
colors,
speed,
direction,
transition,
..
} => {
let current_speed = speed.to_cycles_per_second(tempo_map, absolute_time);
apply_color_cycle(
fixture_registry,
effect,
colors,
current_speed,
direction,
*transition,
elapsed,
)
}
EffectType::Strobe { frequency, .. } => {
let current_frequency = frequency.to_hz(tempo_map, absolute_time);
apply_strobe(fixture_registry, effect, current_frequency, elapsed)
}
EffectType::Dimmer {
start_level,
end_level,
duration,
curve,
} => apply_dimmer(
fixture_registry,
effect,
*start_level,
*end_level,
curve,
elapsed,
*duration,
),
EffectType::Chase {
pattern,
speed,
direction,
transition,
..
} => {
let current_speed = speed.to_cycles_per_second(tempo_map, absolute_time);
apply_chase(
fixture_registry,
effect,
pattern,
current_speed,
direction,
*transition,
elapsed,
)
}
EffectType::Rainbow {
speed,
saturation,
brightness,
..
} => {
let current_speed = speed.to_cycles_per_second(tempo_map, absolute_time);
apply_rainbow(
fixture_registry,
effect,
current_speed,
*saturation,
*brightness,
elapsed,
)
}
EffectType::Pulse {
base_level,
pulse_amplitude,
frequency,
..
} => {
let current_frequency = frequency.to_hz(tempo_map, absolute_time);
apply_pulse(
fixture_registry,
effect,
*base_level,
*pulse_amplitude,
current_frequency,
elapsed,
)
}
}
}
fn apply_static_effect(
fixture_registry: &HashMap<String, FixtureInfo>,
effect: &EffectInstance,
parameters: &HashMap<String, f64>,
elapsed: Duration,
) -> Result<Option<HashMap<String, FixtureState>>, EffectError> {
let crossfade_multiplier = effect.calculate_crossfade_multiplier(elapsed);
let fixture_states =
build_fixture_states_with_info(fixture_registry, effect, |fixture, _profile| {
let channels = parameters
.iter()
.filter(|(param_name, _)| fixture.channels.contains_key(*param_name))
.map(|(param_name, value)| {
let faded_value = *value * crossfade_multiplier;
(
param_name.clone(),
ChannelState::new(faded_value, effect.layer, effect.blend_mode),
)
});
FixtureState::from_channels(channels)
});
Ok(Some(fixture_states))
}
fn apply_color_cycle(
fixture_registry: &HashMap<String, FixtureInfo>,
effect: &EffectInstance,
colors: &[Color],
speed: f64,
direction: &CycleDirection,
transition: CycleTransition,
elapsed: Duration,
) -> Result<Option<HashMap<String, FixtureState>>, EffectError> {
if colors.is_empty() {
return Ok(None);
}
let crossfade_multiplier = effect.calculate_crossfade_multiplier(elapsed);
if speed <= 0.0 {
let color = colors[0];
let fixture_states = build_fixture_states(fixture_registry, effect, |profile| {
let mut commands = profile.apply_color(color, effect.layer, effect.blend_mode);
for state in commands.values_mut() {
state.value *= crossfade_multiplier;
}
commands
});
return Ok(Some(fixture_states));
}
let cycle_time = 1.0 / speed;
let cycle_progress_val = cycle_progress(elapsed, cycle_time);
let (color_index, next_index, segment_progress) =
calculate_color_indices(cycle_progress_val, colors.len(), direction);
let color = match transition {
CycleTransition::Fade => {
let current_color = colors[color_index % colors.len()];
let next_color = colors[next_index % colors.len()];
current_color.lerp(&next_color, segment_progress)
}
CycleTransition::Snap => {
colors[color_index % colors.len()]
}
};
let fixture_states = build_fixture_states(fixture_registry, effect, |profile| {
let mut commands = profile.apply_color(color, effect.layer, effect.blend_mode);
for state in commands.values_mut() {
state.value *= crossfade_multiplier;
}
commands
});
Ok(Some(fixture_states))
}
fn apply_strobe(
fixture_registry: &HashMap<String, FixtureInfo>,
effect: &EffectInstance,
frequency: f64,
elapsed: Duration,
) -> Result<Option<HashMap<String, FixtureState>>, EffectError> {
let crossfade_multiplier = effect.calculate_crossfade_multiplier(elapsed);
let fixture_states =
build_fixture_states_with_info(fixture_registry, effect, |fixture, profile| {
if frequency == 0.0 {
let mut fixture_state = FixtureState::new();
if fixture.has_capability(FixtureCapabilities::STROBING) {
fixture_state.set_channel(
"strobe".to_string(),
ChannelState::new(0.0, effect.layer, effect.blend_mode),
);
}
fixture_state
} else {
let (normalized_frequency, strobe_value) = if profile.strobe_strategy
== StrobeStrategy::DedicatedChannel
{
let max_freq = fixture.max_strobe_frequency.unwrap_or(20.0);
let min_freq = fixture.min_strobe_frequency.unwrap_or(0.0);
let dmx_offset = fixture.strobe_dmx_offset.unwrap_or(0);
let min_normalized = dmx_offset as f64 / 255.0;
let normalized = if max_freq > 0.0 && min_freq > 0.0 && dmx_offset > 0 {
let max_period = 1.0 / min_freq;
let min_period = 1.0 / max_freq;
let desired_period = 1.0 / frequency.clamp(min_freq, max_freq);
let period_range = max_period - min_period;
let period_fraction = if period_range.abs() < f64::EPSILON {
1.0
} else {
(max_period - desired_period) / period_range
};
(min_normalized + period_fraction * (1.0 - min_normalized)).clamp(0.0, 1.0)
} else {
(frequency / max_freq).min(1.0)
};
(normalized, None)
} else {
let strobe_period = 1.0 / frequency;
let strobe_phase = cycle_progress(elapsed, strobe_period);
let is_strobe_on = strobe_phase < 0.5; (frequency, Some(if is_strobe_on { 1.0 } else { 0.0 }))
};
let channel_commands = profile.apply_strobe(
normalized_frequency,
effect.layer,
effect.blend_mode,
crossfade_multiplier,
strobe_value,
);
FixtureState::from_channels(channel_commands)
}
});
Ok(Some(fixture_states))
}
fn apply_dimmer(
fixture_registry: &HashMap<String, FixtureInfo>,
effect: &EffectInstance,
start_level: f64,
end_level: f64,
curve: &DimmerCurve,
elapsed: Duration,
duration: Duration,
) -> Result<Option<HashMap<String, FixtureState>>, EffectError> {
let dimmer_value = if duration.is_zero() {
end_level } else {
let linear_progress = (elapsed.as_secs_f64() / duration.as_secs_f64()).clamp(0.0, 1.0);
let curved_progress = match curve {
DimmerCurve::Linear => linear_progress,
DimmerCurve::Exponential => linear_progress * linear_progress,
DimmerCurve::Logarithmic => {
if linear_progress <= 0.0 {
0.0
} else {
(1.0 + 9.0 * linear_progress).log10()
}
}
DimmerCurve::Sine => {
(1.0 - ((linear_progress * std::f64::consts::PI).cos())) / 2.0
}
DimmerCurve::Cosine => {
1.0 - (linear_progress * std::f64::consts::FRAC_PI_2).cos()
}
};
start_level + (end_level - start_level) * curved_progress
};
let fixture_states = build_fixture_states(fixture_registry, effect, |profile| {
profile.apply_brightness(dimmer_value, effect.layer, effect.blend_mode)
});
Ok(Some(fixture_states))
}
fn apply_chase(
fixture_registry: &HashMap<String, FixtureInfo>,
effect: &EffectInstance,
pattern: &ChasePattern,
speed: f64,
direction: &ChaseDirection,
transition: CycleTransition,
elapsed: Duration,
) -> Result<Option<HashMap<String, FixtureState>>, EffectError> {
let crossfade_multiplier = effect.calculate_crossfade_multiplier(elapsed);
if speed <= 0.0 {
let mut fixture_states = HashMap::new();
for (i, fixture_name) in effect.target_fixtures.iter().enumerate() {
if let Some(fixture) = fixture_registry.get(fixture_name) {
let chase_value = if i == 0 { crossfade_multiplier } else { 0.0 };
let profile = FixtureProfile::for_fixture(fixture);
let channel_commands =
profile.apply_chase(chase_value, effect.layer, effect.blend_mode);
fixture_states.insert(
fixture_name.clone(),
FixtureState::from_channels(channel_commands),
);
}
}
return Ok(Some(fixture_states));
}
let chase_period = if speed > 0.0 {
1.0 / speed
} else {
f64::INFINITY
};
let mut fixture_states = HashMap::new();
let fixture_count = effect.target_fixtures.len();
if fixture_count == 0 {
return Ok(Some(fixture_states));
}
let fixture_order = calculate_fixture_order(fixture_count, pattern, direction, effect.cue_time);
let pattern_length = fixture_order.len();
let position_duration = chase_period / fixture_count as f64;
let pattern_cycle_period = position_duration * pattern_length as f64;
let pattern_progress = cycle_progress(elapsed, pattern_cycle_period);
let current_pattern_index_f = pattern_progress * pattern_length as f64;
let current_pattern_index = current_pattern_index_f.floor() as usize;
let position_progress = current_pattern_index_f - current_pattern_index as f64;
for (i, fixture_name) in effect.target_fixtures.iter().enumerate() {
if let Some(fixture) = fixture_registry.get(fixture_name) {
let chase_value = match transition {
CycleTransition::Snap => {
let is_fixture_active = if current_pattern_index < pattern_length {
fixture_order[current_pattern_index] == i
} else {
false
};
if is_fixture_active {
1.0
} else {
0.0
}
}
CycleTransition::Fade => {
let fade_ratio = 0.5;
let is_current = if current_pattern_index < pattern_length {
fixture_order[current_pattern_index] == i
} else {
false
};
let prev_index = if current_pattern_index > 0 {
current_pattern_index - 1
} else {
pattern_length - 1
};
let is_previous = if prev_index < pattern_length {
fixture_order[prev_index] == i
} else {
false
};
if is_current {
if position_progress < fade_ratio {
position_progress / fade_ratio
} else {
1.0
}
} else if is_previous {
if position_progress < fade_ratio {
1.0 - (position_progress / fade_ratio)
} else {
0.0
}
} else {
0.0
}
}
} * crossfade_multiplier;
let profile = FixtureProfile::for_fixture(fixture);
let channel_commands =
profile.apply_chase(chase_value, effect.layer, effect.blend_mode);
fixture_states.insert(
fixture_name.clone(),
FixtureState::from_channels(channel_commands),
);
}
}
Ok(Some(fixture_states))
}
fn calculate_fixture_order(
fixture_count: usize,
pattern: &ChasePattern,
direction: &ChaseDirection,
cue_time: Option<Duration>,
) -> Vec<usize> {
match pattern {
ChasePattern::Random => {
use rand::rngs::StdRng;
use rand::{RngExt, SeedableRng};
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut order: Vec<usize> = (0..fixture_count).collect();
let seed = if let Some(cue) = cue_time {
let mut hasher = DefaultHasher::new();
cue.as_nanos().hash(&mut hasher);
fixture_count.hash(&mut hasher); hasher.finish()
} else {
fixture_count as u64 * 7 + 13
};
let mut rng = StdRng::seed_from_u64(seed);
for i in (1..fixture_count).rev() {
let j = rng.random_range(0..=i);
order.swap(i, j);
}
order
}
ChasePattern::Linear => {
let mut order: Vec<usize> = (0..fixture_count).collect();
match direction {
ChaseDirection::LeftToRight
| ChaseDirection::TopToBottom
| ChaseDirection::Clockwise => {
order
}
ChaseDirection::RightToLeft
| ChaseDirection::BottomToTop
| ChaseDirection::CounterClockwise => {
order.reverse();
order
}
}
}
ChasePattern::Snake => {
let mut snake_order = Vec::new();
for i in 0..fixture_count {
snake_order.push(i);
}
for i in (1..fixture_count - 1).rev() {
snake_order.push(i);
}
match direction {
ChaseDirection::LeftToRight
| ChaseDirection::TopToBottom
| ChaseDirection::Clockwise => {
snake_order
}
ChaseDirection::RightToLeft
| ChaseDirection::BottomToTop
| ChaseDirection::CounterClockwise => {
snake_order.reverse();
snake_order
}
}
}
}
}
fn apply_rainbow(
fixture_registry: &HashMap<String, FixtureInfo>,
effect: &EffectInstance,
speed: f64,
saturation: f64,
brightness: f64,
elapsed: Duration,
) -> Result<Option<HashMap<String, FixtureState>>, EffectError> {
let crossfade_multiplier = effect.calculate_crossfade_multiplier(elapsed);
let hue = (elapsed.as_secs_f64() * speed * 360.0) % 360.0;
let color = Color::from_hsv(hue, saturation, brightness);
let fixture_states = build_fixture_states(fixture_registry, effect, |profile| {
let mut commands = profile.apply_color(color, effect.layer, effect.blend_mode);
for state in commands.values_mut() {
state.value *= crossfade_multiplier;
}
commands
});
Ok(Some(fixture_states))
}
fn apply_pulse(
fixture_registry: &HashMap<String, FixtureInfo>,
effect: &EffectInstance,
base_level: f64,
pulse_amplitude: f64,
frequency: f64,
elapsed: Duration,
) -> Result<Option<HashMap<String, FixtureState>>, EffectError> {
let crossfade_multiplier = effect.calculate_crossfade_multiplier(elapsed);
let pulse_phase = phase(elapsed, frequency);
let pulse_value =
(base_level + pulse_amplitude * (pulse_phase.sin() * 0.5 + 0.5)) * crossfade_multiplier;
let fixture_states = build_fixture_states(fixture_registry, effect, |profile| {
profile.apply_pulse(pulse_value, effect.layer, effect.blend_mode)
});
Ok(Some(fixture_states))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cycle_progress_at_zero() {
assert!((cycle_progress(Duration::ZERO, 1.0) - 0.0).abs() < 1e-10);
}
#[test]
fn cycle_progress_halfway() {
let p = cycle_progress(Duration::from_secs_f64(0.5), 1.0);
assert!((p - 0.5).abs() < 1e-10);
}
#[test]
fn cycle_progress_wraps() {
let p = cycle_progress(Duration::from_secs_f64(1.5), 1.0);
assert!((p - 0.5).abs() < 1e-10);
}
#[test]
fn cycle_progress_quarter() {
let p = cycle_progress(Duration::from_secs_f64(0.25), 1.0);
assert!((p - 0.25).abs() < 1e-10);
}
#[test]
fn cycle_progress_fast_period() {
let p = cycle_progress(Duration::from_secs_f64(0.1), 0.2);
assert!((p - 0.5).abs() < 1e-10);
}
#[test]
fn phase_at_zero() {
assert!((phase(Duration::ZERO, 1.0) - 0.0).abs() < 1e-10);
}
#[test]
fn phase_one_cycle() {
let p = phase(Duration::from_secs(1), 1.0);
assert!((p - 2.0 * std::f64::consts::PI).abs() < 1e-10);
}
#[test]
fn phase_half_cycle() {
let p = phase(Duration::from_secs_f64(0.5), 1.0);
assert!((p - std::f64::consts::PI).abs() < 1e-10);
}
#[test]
fn phase_2hz() {
let p = phase(Duration::from_secs(1), 2.0);
assert!((p - 4.0 * std::f64::consts::PI).abs() < 1e-10);
}
#[test]
fn color_indices_forward_start() {
let (idx, next, progress) = calculate_color_indices(0.0, 3, &CycleDirection::Forward);
assert_eq!(idx, 0);
assert_eq!(next, 1);
assert!(progress < 0.01);
}
#[test]
fn color_indices_forward_mid() {
let (idx, next, progress) = calculate_color_indices(0.5, 3, &CycleDirection::Forward);
assert_eq!(idx, 1);
assert_eq!(next, 2);
assert!((progress - 0.5).abs() < 0.01);
}
#[test]
fn color_indices_forward_wraps() {
let (idx, next, _) = calculate_color_indices(0.9, 3, &CycleDirection::Forward);
assert_eq!(idx, 2);
assert_eq!(next, 0);
}
#[test]
fn color_indices_backward_start() {
let (idx, next, progress) = calculate_color_indices(0.0, 3, &CycleDirection::Backward);
assert_eq!(idx, 2);
assert_eq!(next, 2);
assert!(progress.abs() < 0.01);
}
#[test]
fn color_indices_backward_mid() {
let (idx, _, _) = calculate_color_indices(0.5, 3, &CycleDirection::Backward);
assert_eq!(idx, 1);
}
#[test]
fn color_indices_pingpong_start() {
let (idx, next, _) = calculate_color_indices(0.0, 4, &CycleDirection::PingPong);
assert_eq!(idx, 0);
assert_eq!(next, 1);
}
#[test]
fn color_indices_pingpong_midpoint() {
let (idx, next, _) = calculate_color_indices(0.5, 4, &CycleDirection::PingPong);
assert_eq!(idx, 3);
assert_eq!(next, 3); }
#[test]
fn color_indices_pingpong_quarter() {
let (idx, next, progress) = calculate_color_indices(0.25, 4, &CycleDirection::PingPong);
assert_eq!(idx, 1);
assert_eq!(next, 2);
assert!((progress - 0.5).abs() < 0.01);
}
#[test]
fn fixture_order_linear_forward() {
let order =
calculate_fixture_order(4, &ChasePattern::Linear, &ChaseDirection::LeftToRight, None);
assert_eq!(order, vec![0, 1, 2, 3]);
}
#[test]
fn fixture_order_linear_reverse() {
let order =
calculate_fixture_order(4, &ChasePattern::Linear, &ChaseDirection::RightToLeft, None);
assert_eq!(order, vec![3, 2, 1, 0]);
}
#[test]
fn fixture_order_snake_forward() {
let order =
calculate_fixture_order(4, &ChasePattern::Snake, &ChaseDirection::LeftToRight, None);
assert_eq!(order, vec![0, 1, 2, 3, 2, 1]);
}
#[test]
fn fixture_order_snake_reverse() {
let order =
calculate_fixture_order(4, &ChasePattern::Snake, &ChaseDirection::RightToLeft, None);
assert_eq!(order, vec![1, 2, 3, 2, 1, 0]);
}
#[test]
fn fixture_order_random_deterministic() {
let cue_time = Some(Duration::from_secs(10));
let order1 = calculate_fixture_order(
5,
&ChasePattern::Random,
&ChaseDirection::LeftToRight,
cue_time,
);
let order2 = calculate_fixture_order(
5,
&ChasePattern::Random,
&ChaseDirection::LeftToRight,
cue_time,
);
assert_eq!(order1, order2);
let mut sorted = order1.clone();
sorted.sort();
assert_eq!(sorted, vec![0, 1, 2, 3, 4]);
}
#[test]
fn fixture_order_random_different_seeds() {
let order1 = calculate_fixture_order(
8,
&ChasePattern::Random,
&ChaseDirection::LeftToRight,
Some(Duration::from_secs(1)),
);
let order2 = calculate_fixture_order(
8,
&ChasePattern::Random,
&ChaseDirection::LeftToRight,
Some(Duration::from_secs(999)),
);
assert_ne!(order1, order2);
}
#[test]
fn fixture_order_linear_single() {
let order =
calculate_fixture_order(1, &ChasePattern::Linear, &ChaseDirection::LeftToRight, None);
assert_eq!(order, vec![0]);
}
#[test]
fn process_effect_disabled() {
let registry = HashMap::new();
let mut effect = EffectInstance::new(
"test".to_string(),
EffectType::Static {
parameters: HashMap::new(),
duration: Duration::ZERO,
},
vec![],
None,
None,
None,
);
effect.enabled = false;
let result =
process_effect(®istry, &effect, Duration::ZERO, Duration::ZERO, None).unwrap();
assert!(result.is_none());
}
#[test]
fn build_fixture_states_skips_unknown() {
let registry = HashMap::new(); let effect = EffectInstance::new(
"test".to_string(),
EffectType::Static {
parameters: HashMap::new(),
duration: Duration::ZERO,
},
vec!["unknown_fixture".to_string()],
None,
None,
None,
);
let states = build_fixture_states(®istry, &effect, |_profile| HashMap::new());
assert!(states.is_empty());
}
#[test]
fn build_fixture_states_with_fixture() {
let mut channels = HashMap::new();
channels.insert("dimmer".to_string(), 1u16);
let fixture = FixtureInfo::new(
"test_fix".to_string(),
1,
1,
"generic".to_string(),
channels,
None,
);
let mut registry = HashMap::new();
registry.insert("test_fix".to_string(), fixture);
let effect = EffectInstance::new(
"test".to_string(),
EffectType::Static {
parameters: HashMap::new(),
duration: Duration::ZERO,
},
vec!["test_fix".to_string()],
None,
None,
None,
);
let states = build_fixture_states(®istry, &effect, |_profile| {
let mut cmds = HashMap::new();
cmds.insert(
"dimmer".to_string(),
ChannelState::new(1.0, EffectLayer::Foreground, BlendMode::Replace),
);
cmds
});
assert_eq!(states.len(), 1);
assert!(states.contains_key("test_fix"));
}
}