1use crate::error::Error;
2use crate::state::{GroupName, Model};
3use rand::{rng, Rng};
4use serde::de::{value, IntoDeserializer};
5use serde::{Deserialize, Serialize};
6use std::collections::BTreeMap;
7use std::path::Path;
8use std::time::Duration;
9
10const ITEM_UPPER_BOUND: u64 = 1 << 40;
12
13const DEFAULT_GROUP_COUNT: u64 = 10;
14const DEFAULT_PERSON_COUNT: u64 = 10;
15
16const DEFAULT_WARMUP_TIME: u64 = 10;
17const DEFAULT_TEST_TIME: Option<u64> = Some(180);
18
19#[derive(Debug, Serialize, Deserialize)]
20pub struct GroupProperties {
21 pub member_count: Option<u64>,
22}
23
24#[derive(Debug, Serialize, Deserialize)]
25pub struct Profile {
26 control_uri: String,
27 admin_password: String,
28 idm_admin_password: String,
29 seed: i64,
30 extra_uris: Vec<String>,
31 warmup_time: u64,
33 test_time: Option<u64>,
34 group_count: u64,
35 person_count: u64,
36 thread_count: Option<usize>,
37 model: Model,
38 group: BTreeMap<String, GroupProperties>,
39 #[serde(default)]
40 dump_raw_data: bool,
41}
42
43impl Profile {
44 pub fn control_uri(&self) -> &str {
45 self.control_uri.as_str()
46 }
47
48 pub fn extra_uris(&self) -> &[String] {
49 self.extra_uris.as_slice()
50 }
51
52 pub fn admin_password(&self) -> &str {
53 self.admin_password.as_str()
54 }
55
56 pub fn idm_admin_password(&self) -> &str {
57 self.idm_admin_password.as_str()
58 }
59
60 #[allow(dead_code)]
61 pub fn group_count(&self) -> u64 {
62 self.group_count
63 }
64
65 pub fn person_count(&self) -> u64 {
66 self.person_count
67 }
68
69 pub fn thread_count(&self) -> Option<usize> {
70 self.thread_count
71 }
72
73 pub fn get_properties_by_group(&self) -> &BTreeMap<String, GroupProperties> {
74 &self.group
75 }
76
77 pub fn seed(&self) -> u64 {
78 if self.seed < 0 {
79 self.seed.wrapping_mul(-1) as u64
80 } else {
81 self.seed as u64
82 }
83 }
84
85 pub fn model(&self) -> &Model {
86 &self.model
87 }
88
89 pub fn warmup_time(&self) -> Duration {
90 Duration::from_secs(self.warmup_time)
91 }
92
93 pub fn test_time(&self) -> Option<Duration> {
94 self.test_time.map(Duration::from_secs)
95 }
96
97 pub fn dump_raw_data(&self) -> bool {
98 self.dump_raw_data
99 }
100}
101
102pub struct ProfileBuilder {
103 pub control_uri: String,
104 pub admin_password: String,
105 pub idm_admin_password: String,
106 pub seed: Option<u64>,
107 pub extra_uris: Vec<String>,
108 pub warmup_time: Option<u64>,
110 pub test_time: Option<Option<u64>>,
111 pub group_count: Option<u64>,
112 pub person_count: Option<u64>,
113 pub thread_count: Option<usize>,
114 pub model: Model,
115 pub dump_raw_data: bool,
116}
117
118fn validate_u64_bound(value: Option<u64>, default: u64) -> Result<u64, Error> {
119 if let Some(v) = value {
120 if v > ITEM_UPPER_BOUND {
121 error!("group count exceeds upper bound ({})", ITEM_UPPER_BOUND);
122 Err(Error::ProfileBuilder)
123 } else {
124 Ok(v)
125 }
126 } else {
127 Ok(default)
128 }
129}
130
131impl ProfileBuilder {
132 pub fn new(
133 control_uri: String,
134 extra_uris: Vec<String>,
135 admin_password: String,
136 idm_admin_password: String,
137 model: Model,
138 thread_count: Option<usize>,
139 dump_raw_data: bool,
140 ) -> Self {
141 ProfileBuilder {
142 control_uri,
143 extra_uris,
144 admin_password,
145 idm_admin_password,
146 seed: None,
147 warmup_time: None,
148 test_time: None,
149 group_count: None,
150 person_count: None,
151 thread_count,
152 model,
153 dump_raw_data,
154 }
155 }
156
157 pub fn seed(mut self, seed: Option<u64>) -> Self {
158 self.seed = seed;
159 self
160 }
161
162 #[allow(dead_code)]
163 pub fn warmup_time(mut self, time: Option<u64>) -> Self {
164 self.warmup_time = time;
165 self
166 }
167
168 #[allow(dead_code)]
169 pub fn test_time(mut self, time: Option<Option<u64>>) -> Self {
170 self.test_time = time;
171 self
172 }
173
174 #[allow(dead_code)]
175 pub fn group_count(mut self, group_count: Option<u64>) -> Self {
176 self.group_count = group_count;
177 self
178 }
179
180 #[allow(dead_code)]
181 pub fn person_count(mut self, person_count: Option<u64>) -> Self {
182 self.person_count = person_count;
183 self
184 }
185
186 pub fn build(self) -> Result<Profile, Error> {
187 let ProfileBuilder {
188 control_uri,
189 admin_password,
190 idm_admin_password,
191 seed,
192 extra_uris,
193 warmup_time,
194 test_time,
195 group_count,
196 person_count,
197 thread_count,
198 model,
199 dump_raw_data,
200 } = self;
201
202 let seed: u64 = seed.unwrap_or_else(|| {
203 let mut rng = rng();
204 rng.random()
205 });
206
207 let group = BTreeMap::new();
209
210 let group_count = validate_u64_bound(group_count, DEFAULT_GROUP_COUNT)?;
211 let person_count = validate_u64_bound(person_count, DEFAULT_PERSON_COUNT)?;
212
213 let warmup_time = warmup_time.unwrap_or(DEFAULT_WARMUP_TIME);
214 let test_time = test_time.unwrap_or(DEFAULT_TEST_TIME);
215
216 let seed: i64 = if seed > i64::MAX as u64 {
217 let seed = seed - i64::MAX as u64;
219 -(seed as i64)
220 } else {
221 seed as i64
222 };
223
224 Ok(Profile {
225 control_uri,
226 admin_password,
227 idm_admin_password,
228 seed,
229 extra_uris,
230 warmup_time,
231 test_time,
232 group_count,
233 person_count,
234 thread_count,
235 group,
236 model,
237 dump_raw_data,
238 })
239 }
240}
241
242impl Profile {
243 pub fn write_to_path(&self, path: &Path) -> Result<(), Error> {
244 let file_contents = toml::to_string(self).map_err(|toml_err| {
245 error!(?toml_err);
246 Error::SerdeToml
247 })?;
248
249 std::fs::write(path, file_contents).map_err(|io_err| {
250 error!(?io_err);
251 Error::Io
252 })
253 }
254
255 fn validate_group_names_and_member_count(&self) -> Result<(), Error> {
256 for (group_name, group_properties) in self.group.iter() {
257 let _ = GroupName::deserialize(group_name.as_str().into_deserializer()).map_err(
258 |_: value::Error| {
259 error!("Invalid group name provided: {group_name}");
260 Error::InvalidState
261 },
262 )?;
263 let provided_member_count = group_properties.member_count.unwrap_or_default();
264 let max_member_count = self.person_count();
265 if provided_member_count > max_member_count {
266 error!("Member count of {group_name} is out of bound: max value is {max_member_count}, but {provided_member_count} was provided");
267 return Err(Error::InvalidState);
268 }
269 }
270 Ok(())
271 }
272}
273
274impl TryFrom<&Path> for Profile {
275 type Error = Error;
276
277 fn try_from(path: &Path) -> Result<Self, Self::Error> {
278 let file_contents = std::fs::read_to_string(path).map_err(|io_err| {
279 error!(?io_err);
280 Error::Io
281 })?;
282
283 let profile: Profile = toml::from_str(&file_contents).map_err(|toml_err| {
284 error!(?toml_err);
285 Error::SerdeToml
286 })?;
287 profile.validate_group_names_and_member_count()?;
288
289 Ok(profile)
290 }
291}