1#![warn(missing_docs)]
28#![warn(clippy::all)]
29#![warn(clippy::pedantic)]
30#![warn(clippy::nursery)]
31#![allow(clippy::module_name_repetitions)]
32
33mod error;
34mod format;
35mod metadata;
36mod model;
37
38pub use error::AprError;
39pub use format::{AprFile, APR_MAGIC, APR_VERSION};
40pub use metadata::AprMetadata;
41pub use model::{AprModel, ModelArchitecture, ModelData};
42
43pub const MAX_MODEL_SIZE: usize = 1024 * 1024;
45
46pub const MIN_SUPPORTED_VERSION: u16 = 1;
48
49pub const CURRENT_VERSION: u16 = 1;
51
52#[cfg(test)]
53#[allow(clippy::unwrap_used, clippy::expect_used)]
54mod tests {
55 use super::*;
56
57 mod magic_number_tests {
62 use super::*;
63
64 #[test]
65 fn test_apr_magic_is_apnr() {
66 assert_eq!(APR_MAGIC, b"APNR");
68 assert_eq!(APR_MAGIC.len(), 4);
69 }
70
71 #[test]
72 fn test_apr_magic_detection() {
73 let valid = b"APNRxxxxxx";
74 let invalid = b"WRONGxxxxx";
75
76 assert!(AprFile::has_magic(valid));
77 assert!(!AprFile::has_magic(invalid));
78 }
79
80 #[test]
81 fn test_rejects_wrong_magic() {
82 let bad_magic = b"BAAD\x00\x01\x00\x00\x00\x00";
83 let result = AprFile::from_bytes(bad_magic);
84 assert!(matches!(result, Err(AprError::InvalidMagic { .. })));
85 }
86 }
87
88 mod version_tests {
89 use super::*;
90
91 #[test]
92 fn test_apr_version_is_u16() {
93 assert_eq!(APR_VERSION, 1_u16);
95 }
96
97 #[test]
98 fn test_current_version_supported() {
99 let current = CURRENT_VERSION;
101 let min = MIN_SUPPORTED_VERSION;
102 assert!(current >= min);
103 }
104
105 #[test]
106 fn test_rejects_unsupported_version() {
107 let mut bytes = Vec::new();
109 bytes.extend_from_slice(APR_MAGIC);
110 bytes.extend_from_slice(&0_u16.to_le_bytes()); bytes.extend_from_slice(&0_u32.to_le_bytes()); let result = AprFile::from_bytes(&bytes);
114 assert!(matches!(result, Err(AprError::UnsupportedVersion { .. })));
115 }
116 }
117
118 mod checksum_tests {
119 use super::*;
120
121 #[test]
122 fn test_checksum_is_crc32() {
123 let data = b"test data";
125 let checksum = crc32fast::hash(data);
126 assert_eq!(core::mem::size_of_val(&checksum), 4);
127 }
128
129 #[test]
130 fn test_checksum_verification() {
131 let model = AprModel::new_test_model();
133 let bytes = model.to_bytes().expect("Should serialize");
134
135 let loaded = AprFile::from_bytes(&bytes).expect("Should load");
137 assert_eq!(loaded.model.metadata.name, model.metadata.name);
138 }
139
140 #[test]
141 fn test_rejects_corrupted_checksum() {
142 let model = AprModel::new_test_model();
143 let mut bytes = model.to_bytes().expect("Should serialize");
144
145 if bytes.len() > 9 {
147 bytes[6] ^= 0xFF;
148 }
149
150 let result = AprFile::from_bytes(&bytes);
151 assert!(matches!(result, Err(AprError::ChecksumMismatch { .. })));
152 }
153 }
154
155 mod metadata_tests {
156 use super::*;
157
158 #[test]
159 fn test_metadata_has_required_fields() {
160 let metadata = AprMetadata::builder()
161 .name("test-model")
162 .version("1.0.0")
163 .author("Test Author")
164 .license("MIT")
165 .build()
166 .expect("Should build metadata");
167
168 assert_eq!(metadata.name, "test-model");
169 assert_eq!(metadata.version.to_string(), "1.0.0");
170 assert_eq!(metadata.author, "Test Author");
171 assert_eq!(metadata.license, "MIT");
172 }
173
174 #[test]
175 fn test_metadata_optional_difficulty_levels() {
176 let metadata = AprMetadata::builder()
177 .name("pong-ai")
178 .version("1.0.0")
179 .author("PAIML")
180 .license("MIT")
181 .difficulty_levels(10)
182 .build()
183 .expect("Should build");
184
185 assert_eq!(metadata.difficulty_levels, Some(10));
186 }
187
188 #[test]
189 fn test_metadata_validates_name_length() {
190 let result = AprMetadata::builder()
192 .name("ab") .version("1.0.0")
194 .author("Test")
195 .license("MIT")
196 .build();
197
198 assert!(result.is_err());
199 }
200
201 #[test]
202 fn test_metadata_cbor_roundtrip() {
203 let original = AprMetadata::builder()
204 .name("test-model")
205 .version("1.0.0")
206 .author("Test")
207 .license("MIT")
208 .description("A test model")
209 .build()
210 .expect("Should build");
211
212 let encoded = original.to_cbor().expect("Should encode");
213 let decoded = AprMetadata::from_cbor(&encoded).expect("Should decode");
214
215 assert_eq!(original.name, decoded.name);
216 assert_eq!(original.description, decoded.description);
217 }
218 }
219
220 mod model_data_tests {
221 use super::*;
222
223 #[test]
224 fn test_model_data_weights_and_biases() {
225 let data = ModelData {
226 weights: vec![0.5, -0.3, 0.8, 0.1],
227 biases: vec![0.0, 0.1],
228 architecture: ModelArchitecture::Mlp {
229 layers: vec![2, 4, 1],
230 },
231 };
232
233 assert_eq!(data.weights.len(), 4);
234 assert_eq!(data.biases.len(), 2);
235 }
236
237 #[test]
238 fn test_model_architecture_mlp() {
239 let arch = ModelArchitecture::Mlp {
241 layers: vec![2, 16, 1],
242 };
243
244 assert_eq!(arch.to_string(), "mlp-2-16-1");
245 }
246
247 #[test]
248 fn test_model_data_compression() {
249 let data = ModelData {
250 weights: vec![0.5; 1000], biases: vec![0.0; 100],
252 architecture: ModelArchitecture::Mlp {
253 layers: vec![10, 100, 10],
254 },
255 };
256
257 let compressed = data.compress().expect("Should compress");
258 let decompressed = ModelData::decompress(&compressed).expect("Should decompress");
259
260 assert_eq!(data.weights, decompressed.weights);
261 assert_eq!(data.biases, decompressed.biases);
262 }
263 }
264
265 mod size_limit_tests {
266 use super::*;
267
268 #[test]
269 fn test_max_model_size_is_1mb() {
270 assert_eq!(MAX_MODEL_SIZE, 1024 * 1024);
272 }
273
274 #[test]
275 #[allow(clippy::cast_precision_loss)]
276 fn test_rejects_oversized_model() {
277 let huge_data = ModelData {
280 weights: (0..MAX_MODEL_SIZE)
281 .map(|i| (i as f32) * 0.000_001)
282 .collect(),
283 biases: vec![0.0],
284 architecture: ModelArchitecture::Mlp { layers: vec![1] },
285 };
286
287 let model = AprModel {
288 metadata: AprMetadata::builder()
289 .name("too-big")
290 .version("1.0.0")
291 .author("Test")
292 .license("MIT")
293 .build()
294 .expect("metadata"),
295 data: huge_data,
296 };
297
298 let result = model.to_bytes();
299 assert!(matches!(result, Err(AprError::ModelTooLarge { .. })));
300 }
301 }
302
303 mod roundtrip_tests {
304 use super::*;
305
306 #[test]
307 fn test_full_roundtrip() {
308 let original = AprModel {
309 metadata: AprMetadata::builder()
310 .name("pong-champion")
311 .version("2.0.0")
312 .author("PAIML")
313 .license("MIT")
314 .description("A really good pong player!")
315 .difficulty_levels(10)
316 .build()
317 .expect("metadata"),
318 data: ModelData {
319 weights: vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8],
320 biases: vec![0.01, 0.02],
321 architecture: ModelArchitecture::Mlp {
322 layers: vec![2, 4, 2],
323 },
324 },
325 };
326
327 let bytes = original.to_bytes().expect("Should serialize");
329
330 assert_eq!(&bytes[0..4], APR_MAGIC);
332
333 let file = AprFile::from_bytes(&bytes).expect("Should deserialize");
335
336 assert_eq!(file.model.metadata.name, "pong-champion");
338 assert_eq!(file.model.metadata.version.to_string(), "2.0.0");
339 assert_eq!(file.model.metadata.author, "PAIML");
340 assert_eq!(file.model.metadata.difficulty_levels, Some(10));
341 assert_eq!(file.model.data.weights.len(), 8);
342 assert_eq!(file.model.data.biases.len(), 2);
343 }
344
345 #[test]
346 fn test_deterministic_serialization() {
347 let model = AprModel::new_test_model();
348
349 let bytes1 = model.to_bytes().expect("Should serialize");
350 let bytes2 = model.to_bytes().expect("Should serialize again");
351
352 assert_eq!(bytes1, bytes2, "Serialization should be deterministic");
353 }
354 }
355
356 mod builtin_model_tests {
357 use super::*;
358
359 #[test]
360 fn test_builtin_chase() {
361 let model = AprModel::builtin("chase").expect("Should have builtin chase");
363 assert_eq!(model.metadata.name, "builtin-chase");
364 }
365
366 #[test]
367 fn test_builtin_patrol() {
368 let model = AprModel::builtin("patrol").expect("Should have builtin patrol");
370 assert_eq!(model.metadata.name, "builtin-patrol");
371 }
372
373 #[test]
374 fn test_builtin_wander() {
375 let model = AprModel::builtin("wander").expect("Should have builtin wander");
377 assert_eq!(model.metadata.name, "builtin-wander");
378 }
379
380 #[test]
381 fn test_unknown_builtin_fails() {
382 let result = AprModel::builtin("nonexistent");
383 assert!(matches!(result, Err(AprError::UnknownBuiltin { .. })));
384 }
385 }
386
387 mod cosmin_quality_tests {
388 use super::*;
389
390 #[test]
392 fn test_model_reliability_score() {
393 let model = AprModel::new_test_model();
394 let quality = model.assess_quality();
395
396 assert!(
398 quality.test_retest_reliability >= 0.70,
399 "Reliability should be >= 0.70"
400 );
401 }
402
403 #[test]
404 fn test_model_meets_minimum_standards() {
405 let model = AprModel::new_test_model();
406 let quality = model.assess_quality();
407
408 assert!(
409 quality.meets_minimum_standards(),
410 "Test model should meet minimum standards"
411 );
412 }
413 }
414}