use std::collections::HashMap;
use std::error::Error;
use std::path::Path;
use tracing::info;
use super::parser::{parse_fixture_types, parse_venues};
use super::types::{Fixture, FixtureType, Group, Venue};
use crate::config::lighting::{GroupConstraint, LogicalGroup};
use crate::config::Lighting;
pub struct LightingSystem {
fixture_types: HashMap<String, FixtureType>,
venues: HashMap<String, Venue>,
current_venue: Option<String>,
inline_fixtures: HashMap<String, String>,
logical_groups: HashMap<String, LogicalGroup>,
group_cache: HashMap<String, HashMap<String, Vec<String>>>,
}
impl Default for LightingSystem {
fn default() -> Self {
Self::new()
}
}
impl LightingSystem {
pub fn new() -> LightingSystem {
LightingSystem {
fixture_types: HashMap::new(),
venues: HashMap::new(),
current_venue: None,
inline_fixtures: HashMap::new(),
logical_groups: HashMap::new(),
group_cache: HashMap::new(),
}
}
pub fn load(&mut self, config: &Lighting, base_path: &Path) -> Result<(), Box<dyn Error>> {
info!(
"Loading lighting system from base path: {}",
base_path.display()
);
if let Some(venue) = config.current_venue() {
self.current_venue = Some(venue.to_string());
}
self.inline_fixtures = config.fixtures().clone();
self.logical_groups = config.groups().clone();
if let Some(dirs) = config.directories() {
if let Some(fixture_types_dir) = dirs.fixture_types() {
let path = base_path.join(fixture_types_dir);
self.load_fixture_types_directory(&path)?;
}
if let Some(venues_dir) = dirs.venues() {
let path = base_path.join(venues_dir);
self.load_venues_directory(&path)?;
}
}
Ok(())
}
fn load_fixture_types_directory(&mut self, dir: &Path) -> Result<(), Box<dyn Error>> {
if !dir.exists() {
return Ok(()); }
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
self.load_fixture_types_directory(&path)?;
} else if path.extension().is_some_and(|ext| ext == "light") {
self.load_fixture_types_file(&path)?;
}
}
Ok(())
}
fn load_venues_directory(&mut self, dir: &Path) -> Result<(), Box<dyn Error>> {
if !dir.exists() {
return Ok(()); }
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
self.load_venues_directory(&path)?;
} else if path.extension().is_some_and(|ext| ext == "light") {
self.load_venue_file(&path)?;
}
}
Ok(())
}
fn load_fixture_types_file(&mut self, path: &Path) -> Result<(), Box<dyn Error>> {
let content = std::fs::read_to_string(path)?;
match parse_fixture_types(&content) {
Ok(types) => {
for (name, fixture_type) in types {
info!(fixture_type = name, "Loading fixture type");
self.fixture_types.insert(name, fixture_type);
}
}
Err(_e) => {
}
}
Ok(())
}
fn load_venue_file(&mut self, path: &Path) -> Result<(), Box<dyn Error>> {
let content = std::fs::read_to_string(path)?;
match parse_venues(&content) {
Ok(venues) => {
for (name, venue) in venues {
info!(fixture_type = name, "Loading venue");
self.venues.insert(name, venue);
}
}
Err(_e) => {
}
}
Ok(())
}
pub fn current_venue(&self) -> Option<&str> {
self.current_venue.as_deref()
}
pub fn get_current_venue(&self) -> Option<&crate::lighting::types::Venue> {
let venue_name = self.current_venue.as_deref()?;
self.venues.get(venue_name)
}
pub fn get_group(&self, group_name: &str) -> Result<&Group, Box<dyn Error>> {
if let Some(venue_name) = self.current_venue() {
if let Some(venue) = self.venues.get(venue_name) {
if let Some(group) = venue.groups().get(group_name) {
return Ok(group);
}
}
}
Err(format!("Group '{}' not found in current venue", group_name).into())
}
pub fn resolve_logical_group(
&mut self,
group_name: &str,
) -> Result<Vec<String>, Box<dyn Error>> {
let venue_name = self.current_venue().ok_or("No current venue selected")?;
if let Some(cached) = self
.group_cache
.get(venue_name)
.and_then(|venue_cache| venue_cache.get(group_name))
{
return Ok(cached.clone());
}
let logical_group = self
.logical_groups
.get(group_name)
.ok_or_else(|| format!("Logical group '{}' not found", group_name))?;
let venue = self
.venues
.get(venue_name)
.ok_or_else(|| format!("Venue '{}' not found", venue_name))?;
let resolved_fixtures = self.resolve_group_constraints(logical_group, venue)?;
self.group_cache
.entry(venue_name.to_string())
.or_default()
.insert(group_name.to_string(), resolved_fixtures.clone());
Ok(resolved_fixtures)
}
pub fn resolve_logical_group_graceful(&mut self, group_name: &str) -> Vec<String> {
match self.resolve_logical_group(group_name) {
Ok(fixtures) => fixtures,
Err(_) => {
let fallback_group =
if let Some(logical_group) = self.logical_groups.get(group_name) {
logical_group.constraints().iter().find_map(|constraint| {
if let GroupConstraint::FallbackTo(fallback_group) = constraint {
Some(fallback_group.clone())
} else {
None
}
})
} else {
None
};
if let Some(fallback_group) = fallback_group {
return self.resolve_logical_group_graceful(&fallback_group);
}
if let Ok(venue_group) = self.get_group(group_name) {
return venue_group.fixtures().to_vec();
}
Vec::new()
}
}
}
pub fn get_current_venue_fixtures(
&self,
) -> Result<Vec<crate::lighting::effects::FixtureInfo>, Box<dyn Error>> {
let venue_name = self.current_venue().ok_or("No current venue selected")?;
let venue = self
.venues
.get(venue_name)
.ok_or_else(|| format!("Venue '{}' not found", venue_name))?;
let mut fixture_infos = Vec::new();
for (name, fixture) in venue.fixtures() {
let fixture_type = self
.fixture_types
.get(fixture.fixture_type())
.ok_or_else(|| format!("Fixture type '{}' not found", fixture.fixture_type()))?;
let mut fixture_info = crate::lighting::effects::FixtureInfo::new(
name.clone(),
fixture.universe(),
fixture.start_channel(),
fixture.fixture_type().to_string(),
fixture_type.channels().clone(),
fixture_type.max_strobe_frequency(),
);
fixture_info.min_strobe_frequency = fixture_type.min_strobe_frequency();
fixture_info.strobe_dmx_offset = fixture_type.strobe_dmx_offset();
fixture_infos.push(fixture_info);
}
Ok(fixture_infos)
}
fn resolve_group_constraints(
&self,
logical_group: &LogicalGroup,
venue: &Venue,
) -> Result<Vec<String>, Box<dyn Error>> {
let mut candidates: Vec<&Fixture> = venue.fixtures().values().collect();
let mut min_count = 1;
let mut max_count = candidates.len();
let mut allow_empty = false;
let mut preferred_tags: Vec<String> = Vec::new();
for constraint in logical_group.constraints() {
match constraint {
GroupConstraint::AllOf(required_tags) => {
candidates.retain(|fixture| {
required_tags.iter().all(|tag| fixture.tags().contains(tag))
});
}
GroupConstraint::AnyOf(any_tags) => {
candidates
.retain(|fixture| any_tags.iter().any(|tag| fixture.tags().contains(tag)));
}
GroupConstraint::Prefer(tags) => {
preferred_tags = tags.clone();
}
GroupConstraint::MinCount(count) => {
min_count = *count;
}
GroupConstraint::MaxCount(count) => {
max_count = *count;
}
GroupConstraint::AllowEmpty(allow) => {
allow_empty = *allow;
}
GroupConstraint::FallbackTo(_) => {
}
}
}
if candidates.len() < min_count {
if allow_empty {
return Ok(Vec::new());
} else {
return Err(format!(
"Not enough fixtures found for group '{}': found {}, required {}",
logical_group.name(),
candidates.len(),
min_count
)
.into());
}
}
if !preferred_tags.is_empty() {
candidates.sort_by(|a, b| {
let a_score = preferred_tags
.iter()
.filter(|tag| a.tags().contains(tag))
.count();
let b_score = preferred_tags
.iter()
.filter(|tag| b.tags().contains(tag))
.count();
match b_score.cmp(&a_score) {
std::cmp::Ordering::Equal => {
a.name().cmp(b.name())
}
other => other,
}
});
} else {
candidates.sort_by(|a, b| a.name().cmp(b.name()));
}
let selected: Vec<String> = candidates
.iter()
.take(max_count)
.map(|fixture| fixture.name().to_string())
.collect();
Ok(selected)
}
pub fn resolve_effect_groups(
&mut self,
mut effect: super::EffectInstance,
) -> super::EffectInstance {
let mut resolved_fixtures = Vec::new();
for group_name in &effect.target_fixtures {
let fixtures = self.resolve_logical_group_graceful(group_name);
resolved_fixtures.extend(fixtures);
}
effect.target_fixtures = resolved_fixtures;
effect
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn test_tag_based_group_resolution() {
let mut system = LightingSystem::new();
let mut fixtures = HashMap::new();
fixtures.insert(
"Wash1".to_string(),
Fixture::new(
"Wash1".to_string(),
"RGBW_Par".to_string(),
1,
1,
vec!["wash".to_string(), "front".to_string(), "rgb".to_string()],
),
);
fixtures.insert(
"Wash2".to_string(),
Fixture::new(
"Wash2".to_string(),
"RGBW_Par".to_string(),
1,
7,
vec!["wash".to_string(), "front".to_string(), "rgb".to_string()],
),
);
fixtures.insert(
"Mover1".to_string(),
Fixture::new(
"Mover1".to_string(),
"MovingHead".to_string(),
1,
101,
vec!["moving_head".to_string(), "spot".to_string()],
),
);
let venue = Venue::new("Test Venue".to_string(), fixtures, HashMap::new());
system.venues.insert("Test Venue".to_string(), venue);
system.current_venue = Some("Test Venue".to_string());
let front_wash_group = LogicalGroup::new(
"front_wash".to_string(),
vec![
GroupConstraint::AllOf(vec!["wash".to_string(), "front".to_string()]),
GroupConstraint::MinCount(2),
],
);
let movers_group = LogicalGroup::new(
"movers".to_string(),
vec![
GroupConstraint::AnyOf(vec!["moving_head".to_string()]),
GroupConstraint::MinCount(1),
],
);
system
.logical_groups
.insert("front_wash".to_string(), front_wash_group);
system
.logical_groups
.insert("movers".to_string(), movers_group);
let front_wash_fixtures = system.resolve_logical_group("front_wash").unwrap();
assert_eq!(front_wash_fixtures.len(), 2);
assert!(front_wash_fixtures.contains(&"Wash1".to_string()));
assert!(front_wash_fixtures.contains(&"Wash2".to_string()));
let movers_fixtures = system.resolve_logical_group("movers").unwrap();
assert_eq!(movers_fixtures.len(), 1);
assert!(movers_fixtures.contains(&"Mover1".to_string()));
}
#[test]
fn test_group_resolution_insufficient_fixtures() {
let mut system = LightingSystem::new();
let mut fixtures = HashMap::new();
fixtures.insert(
"Wash1".to_string(),
Fixture::new(
"Wash1".to_string(),
"RGBW_Par".to_string(),
1,
1,
vec!["wash".to_string(), "front".to_string()],
),
);
let venue = Venue::new("Test Venue".to_string(), fixtures, HashMap::new());
system.venues.insert("Test Venue".to_string(), venue);
system.current_venue = Some("Test Venue".to_string());
let group = LogicalGroup::new(
"front_wash".to_string(),
vec![
GroupConstraint::AllOf(vec!["wash".to_string(), "front".to_string()]),
GroupConstraint::MinCount(3),
],
);
system
.logical_groups
.insert("front_wash".to_string(), group);
let result = system.resolve_logical_group("front_wash");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Not enough fixtures found"));
}
#[test]
fn test_prefer_constraint() {
let mut system = LightingSystem::new();
let mut fixtures = HashMap::new();
fixtures.insert(
"Wash1".to_string(),
Fixture::new(
"Wash1".to_string(),
"RGBW_Par".to_string(),
1,
1,
vec!["wash".to_string(), "front".to_string()],
),
);
fixtures.insert(
"Wash2".to_string(),
Fixture::new(
"Wash2".to_string(),
"RGBW_Par".to_string(),
1,
7,
vec![
"wash".to_string(),
"front".to_string(),
"premium".to_string(),
],
),
);
fixtures.insert(
"Wash3".to_string(),
Fixture::new(
"Wash3".to_string(),
"RGBW_Par".to_string(),
1,
13,
vec![
"wash".to_string(),
"front".to_string(),
"premium".to_string(),
"rgb".to_string(),
],
),
);
let venue = Venue::new("Test Venue".to_string(), fixtures, HashMap::new());
system.venues.insert("Test Venue".to_string(), venue);
system.current_venue = Some("Test Venue".to_string());
let group = LogicalGroup::new(
"premium_wash".to_string(),
vec![
GroupConstraint::AllOf(vec!["wash".to_string(), "front".to_string()]),
GroupConstraint::Prefer(vec!["premium".to_string()]),
GroupConstraint::MinCount(2),
GroupConstraint::MaxCount(2),
],
);
system
.logical_groups
.insert("premium_wash".to_string(), group);
let fixtures = system.resolve_logical_group("premium_wash").unwrap();
assert_eq!(fixtures.len(), 2);
assert!(fixtures.contains(&"Wash2".to_string()));
assert!(fixtures.contains(&"Wash3".to_string()));
assert!(!fixtures.contains(&"Wash1".to_string()));
}
#[test]
fn test_max_count_constraint() {
let mut system = LightingSystem::new();
let mut fixtures = HashMap::new();
for i in 1..=5 {
fixtures.insert(
format!("Wash{}", i),
Fixture::new(
format!("Wash{}", i),
"RGBW_Par".to_string(),
1,
(i * 6) as u16,
vec!["wash".to_string(), "front".to_string()],
),
);
}
let venue = Venue::new("Test Venue".to_string(), fixtures, HashMap::new());
system.venues.insert("Test Venue".to_string(), venue);
system.current_venue = Some("Test Venue".to_string());
let group = LogicalGroup::new(
"limited_wash".to_string(),
vec![
GroupConstraint::AllOf(vec!["wash".to_string(), "front".to_string()]),
GroupConstraint::MaxCount(3),
],
);
system
.logical_groups
.insert("limited_wash".to_string(), group);
let fixtures = system.resolve_logical_group("limited_wash").unwrap();
assert_eq!(fixtures.len(), 3);
}
#[test]
fn test_any_of_constraint() {
let mut system = LightingSystem::new();
let mut fixtures = HashMap::new();
fixtures.insert(
"Wash1".to_string(),
Fixture::new(
"Wash1".to_string(),
"RGBW_Par".to_string(),
1,
1,
vec!["wash".to_string()],
),
);
fixtures.insert(
"Spot1".to_string(),
Fixture::new(
"Spot1".to_string(),
"MovingHead".to_string(),
1,
7,
vec!["spot".to_string()],
),
);
fixtures.insert(
"Beam1".to_string(),
Fixture::new(
"Beam1".to_string(),
"Beam".to_string(),
1,
13,
vec!["beam".to_string()],
),
);
let venue = Venue::new("Test Venue".to_string(), fixtures, HashMap::new());
system.venues.insert("Test Venue".to_string(), venue);
system.current_venue = Some("Test Venue".to_string());
let group = LogicalGroup::new(
"any_light".to_string(),
vec![
GroupConstraint::AnyOf(vec![
"wash".to_string(),
"spot".to_string(),
"beam".to_string(),
]),
GroupConstraint::MinCount(2),
],
);
system.logical_groups.insert("any_light".to_string(), group);
let fixtures = system.resolve_logical_group("any_light").unwrap();
assert_eq!(fixtures.len(), 3);
assert!(fixtures.contains(&"Wash1".to_string()));
assert!(fixtures.contains(&"Spot1".to_string()));
assert!(fixtures.contains(&"Beam1".to_string()));
}
#[test]
fn test_complex_constraint_combination() {
let mut system = LightingSystem::new();
let mut fixtures = HashMap::new();
fixtures.insert(
"Wash1".to_string(),
Fixture::new(
"Wash1".to_string(),
"RGBW_Par".to_string(),
1,
1,
vec!["wash".to_string(), "front".to_string(), "rgb".to_string()],
),
);
fixtures.insert(
"Wash2".to_string(),
Fixture::new(
"Wash2".to_string(),
"RGBW_Par".to_string(),
1,
7,
vec![
"wash".to_string(),
"front".to_string(),
"rgb".to_string(),
"premium".to_string(),
],
),
);
fixtures.insert(
"Wash3".to_string(),
Fixture::new(
"Wash3".to_string(),
"RGBW_Par".to_string(),
1,
13,
vec![
"wash".to_string(),
"front".to_string(),
"rgb".to_string(),
"premium".to_string(),
],
),
);
fixtures.insert(
"Wash4".to_string(),
Fixture::new(
"Wash4".to_string(),
"RGBW_Par".to_string(),
1,
19,
vec!["wash".to_string(), "front".to_string()],
),
);
let venue = Venue::new("Test Venue".to_string(), fixtures, HashMap::new());
system.venues.insert("Test Venue".to_string(), venue);
system.current_venue = Some("Test Venue".to_string());
let group = LogicalGroup::new(
"complex_group".to_string(),
vec![
GroupConstraint::AllOf(vec!["wash".to_string(), "front".to_string()]),
GroupConstraint::Prefer(vec!["premium".to_string()]),
GroupConstraint::MinCount(2),
GroupConstraint::MaxCount(3),
],
);
system
.logical_groups
.insert("complex_group".to_string(), group);
let fixtures = system.resolve_logical_group("complex_group").unwrap();
assert_eq!(fixtures.len(), 3);
assert!(fixtures.contains(&"Wash2".to_string()));
assert!(fixtures.contains(&"Wash3".to_string()));
assert!(fixtures.contains(&"Wash1".to_string()) || fixtures.contains(&"Wash4".to_string()));
}
#[test]
fn test_group_resolution_no_current_venue() {
let mut system = LightingSystem::new();
system.current_venue = None;
let group = LogicalGroup::new("test_group".to_string(), vec![GroupConstraint::MinCount(1)]);
system
.logical_groups
.insert("test_group".to_string(), group);
let result = system.resolve_logical_group("test_group");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("No current venue selected"));
}
#[test]
fn test_group_resolution_nonexistent_group() {
let mut system = LightingSystem::new();
let mut fixtures = HashMap::new();
fixtures.insert(
"Wash1".to_string(),
Fixture::new(
"Wash1".to_string(),
"RGBW_Par".to_string(),
1,
1,
vec!["wash".to_string()],
),
);
let venue = Venue::new("Test Venue".to_string(), fixtures, HashMap::new());
system.venues.insert("Test Venue".to_string(), venue);
system.current_venue = Some("Test Venue".to_string());
let result = system.resolve_logical_group("nonexistent_group");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Logical group 'nonexistent_group' not found"));
}
#[test]
fn test_group_resolution_nonexistent_venue() {
let mut system = LightingSystem::new();
system.current_venue = Some("Nonexistent Venue".to_string());
let group = LogicalGroup::new("test_group".to_string(), vec![GroupConstraint::MinCount(1)]);
system
.logical_groups
.insert("test_group".to_string(), group);
let result = system.resolve_logical_group("test_group");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Venue 'Nonexistent Venue' not found"));
}
#[test]
fn test_group_caching() {
let mut system = LightingSystem::new();
let mut fixtures = HashMap::new();
fixtures.insert(
"Wash1".to_string(),
Fixture::new(
"Wash1".to_string(),
"RGBW_Par".to_string(),
1,
1,
vec!["wash".to_string(), "front".to_string()],
),
);
let venue = Venue::new("Test Venue".to_string(), fixtures, HashMap::new());
system.venues.insert("Test Venue".to_string(), venue);
system.current_venue = Some("Test Venue".to_string());
let group = LogicalGroup::new(
"cached_group".to_string(),
vec![GroupConstraint::MinCount(1)],
);
system
.logical_groups
.insert("cached_group".to_string(), group);
let fixtures1 = system.resolve_logical_group("cached_group").unwrap();
assert_eq!(fixtures1.len(), 1);
assert!(fixtures1.contains(&"Wash1".to_string()));
let fixtures2 = system.resolve_logical_group("cached_group").unwrap();
assert_eq!(fixtures2.len(), 1);
assert!(fixtures2.contains(&"Wash1".to_string()));
assert!(system.group_cache.contains_key("Test Venue"));
assert!(system
.group_cache
.get("Test Venue")
.unwrap()
.contains_key("cached_group"));
}
#[test]
fn test_graceful_fallback_missing_group() {
let mut system = LightingSystem::new();
let mut fixtures = HashMap::new();
fixtures.insert(
"Wash1".to_string(),
Fixture::new(
"Wash1".to_string(),
"RGBW_Par".to_string(),
1,
1,
vec!["wash".to_string(), "front".to_string()],
),
);
let venue = Venue::new("Test Venue".to_string(), fixtures, HashMap::new());
system.venues.insert("Test Venue".to_string(), venue);
system.current_venue = Some("Test Venue".to_string());
let fixtures = system.resolve_logical_group_graceful("nonexistent_group");
assert_eq!(fixtures.len(), 0);
}
#[test]
fn test_graceful_fallback_insufficient_fixtures() {
let mut system = LightingSystem::new();
let mut fixtures = HashMap::new();
fixtures.insert(
"Wash1".to_string(),
Fixture::new(
"Wash1".to_string(),
"RGBW_Par".to_string(),
1,
1,
vec!["wash".to_string(), "front".to_string()],
),
);
let venue = Venue::new("Test Venue".to_string(), fixtures, HashMap::new());
system.venues.insert("Test Venue".to_string(), venue);
system.current_venue = Some("Test Venue".to_string());
let group = LogicalGroup::new(
"front_wash".to_string(),
vec![
GroupConstraint::AllOf(vec!["wash".to_string(), "front".to_string()]),
GroupConstraint::MinCount(3),
],
);
system
.logical_groups
.insert("front_wash".to_string(), group);
let fixtures = system.resolve_logical_group_graceful("front_wash");
assert_eq!(fixtures.len(), 0);
}
#[test]
fn test_allow_empty_constraint() {
let mut system = LightingSystem::new();
let mut fixtures = HashMap::new();
fixtures.insert(
"Mover1".to_string(),
Fixture::new(
"Mover1".to_string(),
"MovingHead".to_string(),
1,
1,
vec!["moving_head".to_string()],
),
);
let venue = Venue::new("Test Venue".to_string(), fixtures, HashMap::new());
system.venues.insert("Test Venue".to_string(), venue);
system.current_venue = Some("Test Venue".to_string());
let group = LogicalGroup::new(
"wash_lights".to_string(),
vec![
GroupConstraint::AllOf(vec!["wash".to_string()]),
GroupConstraint::AllowEmpty(true),
],
);
system
.logical_groups
.insert("wash_lights".to_string(), group);
let fixtures = system.resolve_logical_group("wash_lights").unwrap();
assert_eq!(fixtures.len(), 0);
}
#[test]
fn test_multiple_groups_graceful() {
let mut system = LightingSystem::new();
let mut fixtures = HashMap::new();
fixtures.insert(
"Wash1".to_string(),
Fixture::new(
"Wash1".to_string(),
"RGBW_Par".to_string(),
1,
1,
vec!["wash".to_string(), "front".to_string()],
),
);
let venue = Venue::new("Test Venue".to_string(), fixtures, HashMap::new());
system.venues.insert("Test Venue".to_string(), venue);
system.current_venue = Some("Test Venue".to_string());
let front_wash_group = LogicalGroup::new(
"front_wash".to_string(),
vec![
GroupConstraint::AllOf(vec!["wash".to_string(), "front".to_string()]),
GroupConstraint::MinCount(1),
],
);
let movers_group = LogicalGroup::new(
"movers".to_string(),
vec![
GroupConstraint::AllOf(vec!["moving_head".to_string()]),
GroupConstraint::MinCount(1),
],
);
system
.logical_groups
.insert("front_wash".to_string(), front_wash_group);
system
.logical_groups
.insert("movers".to_string(), movers_group);
let _group_names = ["front_wash".to_string(), "movers".to_string()];
let results = system.resolve_logical_group_graceful("front_wash");
assert_eq!(results.len(), 1);
assert!(results.contains(&"Wash1".to_string()));
}
#[test]
fn test_fallback_to_constraint() {
let mut system = LightingSystem::new();
let mut fixtures = HashMap::new();
fixtures.insert(
"Wash1".to_string(),
Fixture::new(
"Wash1".to_string(),
"RGBW_Par".to_string(),
1,
1,
vec!["wash".to_string(), "front".to_string()],
),
);
let venue = Venue::new("Test Venue".to_string(), fixtures, HashMap::new());
system.venues.insert("Test Venue".to_string(), venue);
system.current_venue = Some("Test Venue".to_string());
let primary_group = LogicalGroup::new(
"movers".to_string(),
vec![
GroupConstraint::AllOf(vec!["moving_head".to_string()]),
GroupConstraint::MinCount(1),
GroupConstraint::FallbackTo("front_wash".to_string()),
],
);
let fallback_group = LogicalGroup::new(
"front_wash".to_string(),
vec![GroupConstraint::AllOf(vec![
"wash".to_string(),
"front".to_string(),
])],
);
system
.logical_groups
.insert("movers".to_string(), primary_group);
system
.logical_groups
.insert("front_wash".to_string(), fallback_group);
let results = system.resolve_logical_group_graceful("movers");
assert_eq!(
results.len(),
1,
"FallbackTo should resolve to fallback group"
);
assert!(
results.contains(&"Wash1".to_string()),
"FallbackTo should contain Wash1"
);
}
#[test]
fn test_get_group_not_found() {
let mut system = LightingSystem::new();
let fixtures = HashMap::new();
let venue = Venue::new("Test Venue".to_string(), fixtures, HashMap::new());
system.venues.insert("Test Venue".to_string(), venue);
system.current_venue = Some("Test Venue".to_string());
let result = system.get_group("nonexistent");
assert!(result.is_err(), "Should error for missing group");
match result {
Err(error) => {
assert!(
error.to_string().contains("not found"),
"Error should mention 'not found': {}",
error
);
}
Ok(_) => panic!("Expected error for missing group"),
}
}
#[test]
fn test_get_group_no_venue() {
let system = LightingSystem::new();
let result = system.get_group("some_group");
assert!(result.is_err(), "Should error when no venue selected");
}
#[test]
fn test_get_current_venue_fixtures_no_venue() {
let system = LightingSystem::new();
let result = system.get_current_venue_fixtures();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("No current venue"));
}
#[test]
fn test_get_current_venue_fixtures_with_fixtures() {
let mut system = LightingSystem::new();
let mut channels = HashMap::new();
channels.insert("dimmer".to_string(), 1);
channels.insert("red".to_string(), 2);
let ft = super::super::types::FixtureType::new("Par".to_string(), channels);
system.fixture_types.insert("Par".to_string(), ft);
let mut fixtures = HashMap::new();
fixtures.insert(
"front1".to_string(),
super::super::types::Fixture::new(
"front1".to_string(),
"Par".to_string(),
1,
10,
vec!["front".to_string()],
),
);
let venue =
super::super::types::Venue::new("TestVenue".to_string(), fixtures, HashMap::new());
system.venues.insert("TestVenue".to_string(), venue);
system.current_venue = Some("TestVenue".to_string());
let infos = system.get_current_venue_fixtures().unwrap();
assert_eq!(infos.len(), 1);
assert_eq!(infos[0].name, "front1");
assert_eq!(infos[0].universe, 1);
assert_eq!(infos[0].address, 10);
}
#[test]
fn test_get_current_venue_fixtures_unknown_type() {
let mut system = LightingSystem::new();
let mut fixtures = HashMap::new();
fixtures.insert(
"broken".to_string(),
super::super::types::Fixture::new(
"broken".to_string(),
"UnknownType".to_string(),
1,
1,
vec![],
),
);
let venue =
super::super::types::Venue::new("TestVenue".to_string(), fixtures, HashMap::new());
system.venues.insert("TestVenue".to_string(), venue);
system.current_venue = Some("TestVenue".to_string());
let result = system.get_current_venue_fixtures();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Fixture type 'UnknownType' not found"));
}
#[test]
fn test_graceful_with_venue_group_fallback() {
let mut system = LightingSystem::new();
let mut fixtures = HashMap::new();
fixtures.insert(
"Wash1".to_string(),
Fixture::new(
"Wash1".to_string(),
"RGBW_Par".to_string(),
1,
1,
vec!["wash".to_string()],
),
);
let mut groups = HashMap::new();
groups.insert(
"venue_wash".to_string(),
Group::new("venue_wash".to_string(), vec!["Wash1".to_string()]),
);
let venue = Venue::new("Test Venue".to_string(), fixtures, groups);
system.venues.insert("Test Venue".to_string(), venue);
system.current_venue = Some("Test Venue".to_string());
let results = system.resolve_logical_group_graceful("venue_wash");
assert_eq!(
results.len(),
1,
"Venue group fallback should return 1 fixture"
);
assert!(
results.contains(&"Wash1".to_string()),
"Venue group fallback should contain Wash1"
);
}
}