solverforge_core/solver/
config.rs

1use crate::solver::{EnvironmentMode, MoveThreadCount, TerminationConfig};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
5#[serde(rename_all = "camelCase")]
6pub struct SolverConfig {
7    #[serde(skip_serializing_if = "Option::is_none")]
8    pub solution_class: Option<String>,
9    #[serde(default, skip_serializing_if = "Vec::is_empty")]
10    pub entity_class_list: Vec<String>,
11    #[serde(skip_serializing_if = "Option::is_none")]
12    pub environment_mode: Option<EnvironmentMode>,
13    #[serde(skip_serializing_if = "Option::is_none")]
14    pub random_seed: Option<u64>,
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub move_thread_count: Option<MoveThreadCount>,
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub termination: Option<TerminationConfig>,
19}
20
21impl SolverConfig {
22    pub fn new() -> Self {
23        Self::default()
24    }
25
26    pub fn with_solution_class(mut self, class_name: impl Into<String>) -> Self {
27        self.solution_class = Some(class_name.into());
28        self
29    }
30
31    pub fn with_entity_class(mut self, class_name: impl Into<String>) -> Self {
32        self.entity_class_list.push(class_name.into());
33        self
34    }
35
36    pub fn with_entity_classes(mut self, classes: Vec<String>) -> Self {
37        self.entity_class_list = classes;
38        self
39    }
40
41    pub fn with_environment_mode(mut self, mode: EnvironmentMode) -> Self {
42        self.environment_mode = Some(mode);
43        self
44    }
45
46    pub fn with_random_seed(mut self, seed: u64) -> Self {
47        self.random_seed = Some(seed);
48        self
49    }
50
51    pub fn with_move_thread_count(mut self, count: MoveThreadCount) -> Self {
52        self.move_thread_count = Some(count);
53        self
54    }
55
56    pub fn with_termination(mut self, termination: TerminationConfig) -> Self {
57        self.termination = Some(termination);
58        self
59    }
60
61    pub fn environment_mode_or_default(&self) -> EnvironmentMode {
62        self.environment_mode.unwrap_or_default()
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn test_solver_config_new() {
72        let config = SolverConfig::new();
73        assert!(config.solution_class.is_none());
74        assert!(config.entity_class_list.is_empty());
75        assert!(config.environment_mode.is_none());
76    }
77
78    #[test]
79    fn test_solver_config_with_solution_class() {
80        let config = SolverConfig::new().with_solution_class("Timetable");
81        assert_eq!(config.solution_class, Some("Timetable".to_string()));
82    }
83
84    #[test]
85    fn test_solver_config_with_entity_class() {
86        let config = SolverConfig::new()
87            .with_entity_class("Lesson")
88            .with_entity_class("Room");
89        assert_eq!(config.entity_class_list.len(), 2);
90        assert!(config.entity_class_list.contains(&"Lesson".to_string()));
91        assert!(config.entity_class_list.contains(&"Room".to_string()));
92    }
93
94    #[test]
95    fn test_solver_config_with_entity_classes() {
96        let config =
97            SolverConfig::new().with_entity_classes(vec!["Lesson".to_string(), "Room".to_string()]);
98        assert_eq!(config.entity_class_list.len(), 2);
99    }
100
101    #[test]
102    fn test_solver_config_with_environment_mode() {
103        let config = SolverConfig::new().with_environment_mode(EnvironmentMode::FullAssert);
104        assert_eq!(config.environment_mode, Some(EnvironmentMode::FullAssert));
105    }
106
107    #[test]
108    fn test_solver_config_with_random_seed() {
109        let config = SolverConfig::new().with_random_seed(42);
110        assert_eq!(config.random_seed, Some(42));
111    }
112
113    #[test]
114    fn test_solver_config_with_move_thread_count() {
115        let config = SolverConfig::new().with_move_thread_count(MoveThreadCount::Auto);
116        assert_eq!(config.move_thread_count, Some(MoveThreadCount::Auto));
117    }
118
119    #[test]
120    fn test_solver_config_with_termination() {
121        let termination = TerminationConfig::new().with_spent_limit("PT5M");
122        let config = SolverConfig::new().with_termination(termination.clone());
123        assert_eq!(config.termination, Some(termination));
124    }
125
126    #[test]
127    fn test_solver_config_environment_mode_or_default() {
128        let config = SolverConfig::new();
129        assert_eq!(
130            config.environment_mode_or_default(),
131            EnvironmentMode::Reproducible
132        );
133
134        let config = SolverConfig::new().with_environment_mode(EnvironmentMode::FullAssert);
135        assert_eq!(
136            config.environment_mode_or_default(),
137            EnvironmentMode::FullAssert
138        );
139    }
140
141    #[test]
142    fn test_solver_config_chained() {
143        let config = SolverConfig::new()
144            .with_solution_class("Timetable")
145            .with_entity_class("Lesson")
146            .with_environment_mode(EnvironmentMode::NoAssert)
147            .with_random_seed(12345)
148            .with_termination(TerminationConfig::new().with_spent_limit("PT10M"));
149
150        assert_eq!(config.solution_class, Some("Timetable".to_string()));
151        assert_eq!(config.entity_class_list, vec!["Lesson".to_string()]);
152        assert_eq!(config.environment_mode, Some(EnvironmentMode::NoAssert));
153        assert_eq!(config.random_seed, Some(12345));
154        assert!(config.termination.is_some());
155    }
156
157    #[test]
158    fn test_solver_config_json_serialization() {
159        let config = SolverConfig::new()
160            .with_solution_class("Timetable")
161            .with_entity_class("Lesson")
162            .with_environment_mode(EnvironmentMode::PhaseAssert);
163
164        let json = serde_json::to_string(&config).unwrap();
165        assert!(json.contains("\"solutionClass\":\"Timetable\""));
166        assert!(json.contains("\"entityClassList\":[\"Lesson\"]"));
167        assert!(json.contains("\"environmentMode\":\"PHASE_ASSERT\""));
168
169        let parsed: SolverConfig = serde_json::from_str(&json).unwrap();
170        assert_eq!(parsed, config);
171    }
172
173    #[test]
174    fn test_solver_config_json_omits_none() {
175        let config = SolverConfig::new().with_solution_class("Timetable");
176        let json = serde_json::to_string(&config).unwrap();
177        assert!(!json.contains("randomSeed"));
178        assert!(!json.contains("termination"));
179    }
180
181    #[test]
182    fn test_solver_config_full_json() {
183        let config = SolverConfig::new()
184            .with_solution_class("Timetable")
185            .with_entity_class("Lesson")
186            .with_environment_mode(EnvironmentMode::FullAssert)
187            .with_random_seed(42)
188            .with_move_thread_count(MoveThreadCount::Count(4))
189            .with_termination(
190                TerminationConfig::new()
191                    .with_spent_limit("PT5M")
192                    .with_best_score_feasible(true),
193            );
194
195        let json = serde_json::to_string_pretty(&config).unwrap();
196        let parsed: SolverConfig = serde_json::from_str(&json).unwrap();
197        assert_eq!(parsed, config);
198    }
199
200    #[test]
201    fn test_solver_config_clone() {
202        let config = SolverConfig::new()
203            .with_solution_class("Timetable")
204            .with_entity_class("Lesson");
205        let cloned = config.clone();
206        assert_eq!(config, cloned);
207    }
208
209    #[test]
210    fn test_solver_config_debug() {
211        let config = SolverConfig::new().with_solution_class("Timetable");
212        let debug = format!("{:?}", config);
213        assert!(debug.contains("SolverConfig"));
214        assert!(debug.contains("Timetable"));
215    }
216}