solverforge_core/solver/
config.rs1use 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}