1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct BatutaConfig {
7 pub version: String,
9
10 pub project: ProjectConfig,
12
13 pub source: SourceConfig,
15
16 pub transpilation: TranspilationConfig,
18
19 pub optimization: OptimizationConfig,
21
22 pub validation: ValidationConfig,
24
25 pub build: BuildConfig,
27}
28
29impl Default for BatutaConfig {
30 fn default() -> Self {
31 Self {
32 version: "1.0".to_string(),
33 project: ProjectConfig::default(),
34 source: SourceConfig::default(),
35 transpilation: TranspilationConfig::default(),
36 optimization: OptimizationConfig::default(),
37 validation: ValidationConfig::default(),
38 build: BuildConfig::default(),
39 }
40 }
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct ProjectConfig {
45 pub name: String,
47
48 pub description: Option<String>,
50
51 pub primary_language: Option<String>,
53
54 pub authors: Vec<String>,
56
57 pub license: Option<String>,
59}
60
61impl Default for ProjectConfig {
62 fn default() -> Self {
63 Self {
64 name: "untitled".to_string(),
65 description: None,
66 primary_language: None,
67 authors: vec![],
68 license: Some("MIT".to_string()),
69 }
70 }
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct SourceConfig {
75 pub path: PathBuf,
77
78 pub exclude: Vec<String>,
80
81 pub include: Vec<String>,
83}
84
85impl Default for SourceConfig {
86 fn default() -> Self {
87 Self {
88 path: PathBuf::from("."),
89 exclude: vec![
90 ".git".to_string(),
91 "target".to_string(),
92 "build".to_string(),
93 "dist".to_string(),
94 "node_modules".to_string(),
95 "__pycache__".to_string(),
96 "*.pyc".to_string(),
97 ".venv".to_string(),
98 "venv".to_string(),
99 ],
100 include: vec![],
101 }
102 }
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct TranspilationConfig {
107 pub output_dir: PathBuf,
109
110 pub incremental: bool,
112
113 pub cache: bool,
115
116 pub use_ruchy: bool,
118
119 pub ruchy_strictness: Option<String>,
121
122 pub modules: Vec<String>,
124
125 pub decy: DecyConfig,
127 pub depyler: DepylerConfig,
128 pub bashrs: BashrsConfig,
129}
130
131impl Default for TranspilationConfig {
132 fn default() -> Self {
133 Self {
134 output_dir: PathBuf::from("./rust-output"),
135 incremental: true,
136 cache: true,
137 use_ruchy: false,
138 ruchy_strictness: Some("gradual".to_string()),
139 modules: vec![],
140 decy: DecyConfig::default(),
141 depyler: DepylerConfig::default(),
142 bashrs: BashrsConfig::default(),
143 }
144 }
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct DecyConfig {
149 pub ownership_inference: bool,
151
152 pub actionable_diagnostics: bool,
154
155 pub use_static_fixer: bool,
157}
158
159impl Default for DecyConfig {
160 fn default() -> Self {
161 Self { ownership_inference: true, actionable_diagnostics: true, use_static_fixer: true }
162 }
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct DepylerConfig {
167 pub type_inference: bool,
169
170 pub numpy_to_trueno: bool,
172
173 pub sklearn_to_aprender: bool,
175
176 pub pytorch_to_realizar: bool,
178}
179
180impl Default for DepylerConfig {
181 fn default() -> Self {
182 Self {
183 type_inference: true,
184 numpy_to_trueno: true,
185 sklearn_to_aprender: true,
186 pytorch_to_realizar: true,
187 }
188 }
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct BashrsConfig {
193 pub target_shell: String,
195
196 pub use_clap: bool,
198}
199
200impl Default for BashrsConfig {
201 fn default() -> Self {
202 Self { target_shell: "bash".to_string(), use_clap: true }
203 }
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct OptimizationConfig {
208 pub profile: String,
210
211 pub enable_simd: bool,
213
214 pub enable_gpu: bool,
216
217 pub gpu_threshold: usize,
219
220 pub use_moe_routing: bool,
222
223 pub trueno: TruenoConfig,
225}
226
227impl Default for OptimizationConfig {
228 fn default() -> Self {
229 Self {
230 profile: "balanced".to_string(),
231 enable_simd: true,
232 enable_gpu: false,
233 gpu_threshold: 500,
234 use_moe_routing: false,
235 trueno: TruenoConfig::default(),
236 }
237 }
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct TruenoConfig {
242 pub backends: Vec<String>,
244
245 pub adaptive_thresholds: bool,
247
248 pub cpu_threshold: usize,
250}
251
252impl Default for TruenoConfig {
253 fn default() -> Self {
254 Self {
255 backends: vec!["simd".to_string(), "cpu".to_string()],
256 adaptive_thresholds: false,
257 cpu_threshold: 500,
258 }
259 }
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct ValidationConfig {
264 pub trace_syscalls: bool,
266
267 pub run_original_tests: bool,
269
270 pub diff_output: bool,
272
273 pub benchmark: bool,
275
276 pub renacer: RenacerConfig,
278}
279
280impl Default for ValidationConfig {
281 fn default() -> Self {
282 Self {
283 trace_syscalls: true,
284 run_original_tests: true,
285 diff_output: true,
286 benchmark: false,
287 renacer: RenacerConfig::default(),
288 }
289 }
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct RenacerConfig {
294 pub trace_syscalls: Vec<String>,
296
297 pub output_format: String,
299}
300
301impl Default for RenacerConfig {
302 fn default() -> Self {
303 Self { trace_syscalls: vec![], output_format: "json".to_string() }
304 }
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct BuildConfig {
309 pub release: bool,
311
312 pub target: Option<String>,
314
315 pub wasm: bool,
317
318 pub cargo_flags: Vec<String>,
320}
321
322impl Default for BuildConfig {
323 fn default() -> Self {
324 Self { release: true, target: None, wasm: false, cargo_flags: vec![] }
325 }
326}
327
328pub const PRIVATE_CONFIG_FILENAME: &str = ".batuta-private.toml";
334
335#[derive(Debug, Clone, Default, Serialize, Deserialize)]
337pub struct PrivateConfig {
338 #[serde(default)]
340 pub private: PrivateExtensions,
341}
342
343#[derive(Debug, Clone, Default, Serialize, Deserialize)]
345pub struct PrivateExtensions {
346 #[serde(default)]
348 pub rust_stack_dirs: Vec<String>,
349
350 #[serde(default)]
352 pub rust_corpus_dirs: Vec<String>,
353
354 #[serde(default)]
356 pub python_corpus_dirs: Vec<String>,
357
358 #[serde(default)]
360 pub endpoints: Vec<PrivateEndpoint>,
361}
362
363#[derive(Debug, Clone, Serialize, Deserialize)]
365pub struct PrivateEndpoint {
366 pub name: String,
367 #[serde(rename = "type")]
368 pub endpoint_type: String,
369 pub host: String,
370 pub index_path: String,
371}
372
373impl PrivateConfig {
374 #[cfg(feature = "native")]
382 pub fn load_optional() -> anyhow::Result<Option<Self>> {
383 if let Some(path) = Self::find_config_path() {
384 let content = std::fs::read_to_string(&path)?;
385 let config: Self = toml::from_str(&content)?;
386 return Ok(Some(config));
387 }
388 Ok(None)
389 }
390
391 #[cfg(not(feature = "native"))]
393 pub fn load_optional() -> anyhow::Result<Option<Self>> {
394 Ok(None)
395 }
396
397 #[cfg(feature = "native")]
399 fn find_config_path() -> Option<std::path::PathBuf> {
400 if let Some(home) = dirs::home_dir() {
402 let home_path = home.join(PRIVATE_CONFIG_FILENAME);
403 if home_path.exists() {
404 return Some(home_path);
405 }
406 }
407 let cwd_path = std::path::PathBuf::from(PRIVATE_CONFIG_FILENAME);
409 if cwd_path.exists() {
410 return Some(cwd_path);
411 }
412 None
413 }
414
415 pub fn has_dirs(&self) -> bool {
417 !self.private.rust_stack_dirs.is_empty()
418 || !self.private.rust_corpus_dirs.is_empty()
419 || !self.private.python_corpus_dirs.is_empty()
420 }
421
422 pub fn dir_count(&self) -> usize {
424 self.private.rust_stack_dirs.len()
425 + self.private.rust_corpus_dirs.len()
426 + self.private.python_corpus_dirs.len()
427 }
428}
429
430impl BatutaConfig {
431 #[cfg(feature = "native")]
433 pub fn load(path: &std::path::Path) -> anyhow::Result<Self> {
434 let content = std::fs::read_to_string(path)?;
435 let config = toml::from_str(&content)?;
436 Ok(config)
437 }
438
439 #[cfg(feature = "native")]
441 pub fn save(&self, path: &std::path::Path) -> anyhow::Result<()> {
442 let content = toml::to_string_pretty(self)?;
443 std::fs::write(path, content)?;
444 Ok(())
445 }
446
447 pub fn from_analysis(analysis: &crate::types::ProjectAnalysis) -> Self {
449 let mut config = Self::default();
450
451 if let Some(name) = analysis.root_path.file_name() {
453 config.project.name = name.to_string_lossy().to_string();
454 }
455
456 if let Some(lang) = &analysis.primary_language {
458 config.project.primary_language = Some(format!("{}", lang));
459 }
460
461 if analysis.has_ml_dependencies() {
463 config.transpilation.depyler.numpy_to_trueno = true;
464 config.transpilation.depyler.sklearn_to_aprender = true;
465 config.transpilation.depyler.pytorch_to_realizar = true;
466 }
467
468 config
469 }
470}
471
472#[cfg(test)]
473mod tests {
474 use super::*;
475 use std::path::PathBuf;
476 use tempfile::TempDir;
477
478 #[test]
483 fn test_batuta_config_default() {
484 let config = BatutaConfig::default();
485
486 assert_eq!(config.version, "1.0");
487 assert_eq!(config.project.name, "untitled");
488 assert_eq!(config.source.path, PathBuf::from("."));
489 assert_eq!(config.transpilation.output_dir, PathBuf::from("./rust-output"));
490 assert_eq!(config.optimization.profile, "balanced");
491 assert!(config.validation.trace_syscalls);
492 assert!(config.build.release);
493 }
494
495 #[test]
496 fn test_project_config_default() {
497 let config = ProjectConfig::default();
498
499 assert_eq!(config.name, "untitled");
500 assert!(config.description.is_none());
501 assert!(config.primary_language.is_none());
502 assert!(config.authors.is_empty());
503 assert_eq!(config.license, Some("MIT".to_string()));
504 }
505
506 #[test]
507 fn test_source_config_default() {
508 let config = SourceConfig::default();
509
510 assert_eq!(config.path, PathBuf::from("."));
511 assert!(config.exclude.contains(&".git".to_string()));
512 assert!(config.exclude.contains(&"target".to_string()));
513 assert!(config.exclude.contains(&"node_modules".to_string()));
514 assert!(config.exclude.contains(&"__pycache__".to_string()));
515 assert!(config.include.is_empty());
516 }
517
518 #[test]
519 fn test_transpilation_config_default() {
520 let config = TranspilationConfig::default();
521
522 assert_eq!(config.output_dir, PathBuf::from("./rust-output"));
523 assert!(config.incremental);
524 assert!(config.cache);
525 assert!(!config.use_ruchy);
526 assert_eq!(config.ruchy_strictness, Some("gradual".to_string()));
527 assert!(config.modules.is_empty());
528 }
529
530 #[test]
531 fn test_decy_config_default() {
532 let config = DecyConfig::default();
533
534 assert!(config.ownership_inference);
535 assert!(config.actionable_diagnostics);
536 assert!(config.use_static_fixer);
537 }
538
539 #[test]
540 fn test_depyler_config_default() {
541 let config = DepylerConfig::default();
542
543 assert!(config.type_inference);
544 assert!(config.numpy_to_trueno);
545 assert!(config.sklearn_to_aprender);
546 assert!(config.pytorch_to_realizar);
547 }
548
549 #[test]
550 fn test_bashrs_config_default() {
551 let config = BashrsConfig::default();
552
553 assert_eq!(config.target_shell, "bash");
554 assert!(config.use_clap);
555 }
556
557 #[test]
558 fn test_optimization_config_default() {
559 let config = OptimizationConfig::default();
560
561 assert_eq!(config.profile, "balanced");
562 assert!(config.enable_simd);
563 assert!(!config.enable_gpu);
564 assert_eq!(config.gpu_threshold, 500);
565 assert!(!config.use_moe_routing);
566 }
567
568 #[test]
569 fn test_trueno_config_default() {
570 let config = TruenoConfig::default();
571
572 assert_eq!(config.backends, vec!["simd".to_string(), "cpu".to_string()]);
573 assert!(!config.adaptive_thresholds);
574 assert_eq!(config.cpu_threshold, 500);
575 }
576
577 #[test]
578 fn test_validation_config_default() {
579 let config = ValidationConfig::default();
580
581 assert!(config.trace_syscalls);
582 assert!(config.run_original_tests);
583 assert!(config.diff_output);
584 assert!(!config.benchmark);
585 }
586
587 #[test]
588 fn test_renacer_config_default() {
589 let config = RenacerConfig::default();
590
591 assert!(config.trace_syscalls.is_empty());
592 assert_eq!(config.output_format, "json");
593 }
594
595 #[test]
596 fn test_build_config_default() {
597 let config = BuildConfig::default();
598
599 assert!(config.release);
600 assert!(config.target.is_none());
601 assert!(!config.wasm);
602 assert!(config.cargo_flags.is_empty());
603 }
604
605 #[test]
610 fn test_save_and_load_config() {
611 let temp_dir = TempDir::new().expect("tempdir creation failed");
612 let config_path = temp_dir.path().join("batuta.toml");
613
614 let mut config = BatutaConfig::default();
616 config.project.name = "test-project".to_string();
617 config.project.description = Some("A test project".to_string());
618 config.optimization.enable_gpu = true;
619 config.optimization.gpu_threshold = 1000;
620
621 config.save(&config_path).expect("save failed");
623
624 assert!(config_path.exists());
626
627 let loaded_config = BatutaConfig::load(&config_path).expect("unexpected failure");
629
630 assert_eq!(loaded_config.project.name, "test-project");
632 assert_eq!(loaded_config.project.description, Some("A test project".to_string()));
633 assert!(loaded_config.optimization.enable_gpu);
634 assert_eq!(loaded_config.optimization.gpu_threshold, 1000);
635 }
636
637 #[test]
638 fn test_load_nonexistent_file() {
639 let result = BatutaConfig::load(std::path::Path::new("/nonexistent/file.toml"));
640 assert!(result.is_err());
641 }
642
643 #[test]
644 fn test_load_invalid_toml() {
645 let temp_dir = TempDir::new().expect("tempdir creation failed");
646 let config_path = temp_dir.path().join("invalid.toml");
647
648 std::fs::write(&config_path, "invalid toml content [[[").expect("fs write failed");
650
651 let result = BatutaConfig::load(&config_path);
652 assert!(result.is_err());
653 }
654
655 #[test]
656 fn test_save_config_creates_parent_dirs() {
657 let temp_dir = TempDir::new().expect("tempdir creation failed");
658 let nested_path = temp_dir.path().join("nested").join("dir").join("batuta.toml");
659
660 if let Some(parent) = nested_path.parent() {
662 std::fs::create_dir_all(parent).expect("mkdir failed");
663 }
664
665 let config = BatutaConfig::default();
666 let result = config.save(&nested_path);
667
668 assert!(result.is_ok());
669 assert!(nested_path.exists());
670 }
671
672 #[test]
673 fn test_save_config_toml_format() {
674 let temp_dir = TempDir::new().expect("tempdir creation failed");
675 let config_path = temp_dir.path().join("batuta.toml");
676
677 let config = BatutaConfig::default();
678 config.save(&config_path).expect("save failed");
679
680 let content = std::fs::read_to_string(&config_path).expect("fs read failed");
682
683 assert!(content.contains("[project]"));
685 assert!(content.contains("[source]"));
686 assert!(content.contains("[transpilation]"));
687 assert!(content.contains("[optimization]"));
688 assert!(content.contains("[validation]"));
689 assert!(content.contains("[build]"));
690 }
691
692 #[test]
697 fn test_from_analysis_basic() {
698 let analysis = crate::types::ProjectAnalysis {
699 root_path: PathBuf::from("/home/user/my-project"),
700 total_files: 10,
701 total_lines: 1000,
702 languages: vec![],
703 primary_language: Some(crate::types::Language::Python),
704 dependencies: vec![],
705 tdg_score: Some(85.0),
706 };
707
708 let config = BatutaConfig::from_analysis(&analysis);
709
710 assert_eq!(config.project.name, "my-project");
711 assert_eq!(config.project.primary_language, Some("Python".to_string()));
712 }
713
714 #[test]
715 fn test_from_analysis_with_ml_dependencies() {
716 let analysis = crate::types::ProjectAnalysis {
717 root_path: PathBuf::from("/test/project"),
718 total_files: 5,
719 total_lines: 500,
720 languages: vec![],
721 primary_language: Some(crate::types::Language::Python),
722 dependencies: vec![crate::types::DependencyInfo {
723 manager: crate::types::DependencyManager::Pip,
724 file_path: PathBuf::from("requirements.txt"),
725 count: Some(3),
726 }],
727 tdg_score: None,
728 };
729
730 let config = BatutaConfig::from_analysis(&analysis);
731
732 assert!(config.transpilation.depyler.numpy_to_trueno);
734 assert!(config.transpilation.depyler.sklearn_to_aprender);
735 assert!(config.transpilation.depyler.pytorch_to_realizar);
736 }
737
738 #[test]
739 fn test_from_analysis_without_ml_dependencies() {
740 let analysis = crate::types::ProjectAnalysis {
741 root_path: PathBuf::from("/test/project"),
742 total_files: 5,
743 total_lines: 500,
744 languages: vec![],
745 primary_language: Some(crate::types::Language::Python),
746 dependencies: vec![crate::types::DependencyInfo {
747 manager: crate::types::DependencyManager::Pip,
748 file_path: PathBuf::from("requirements.txt"),
749 count: Some(1),
750 }],
751 tdg_score: None,
752 };
753
754 let config = BatutaConfig::from_analysis(&analysis);
755
756 assert!(config.transpilation.depyler.numpy_to_trueno);
758 }
759
760 #[test]
761 fn test_from_analysis_rust_project() {
762 let analysis = crate::types::ProjectAnalysis {
763 root_path: PathBuf::from("/rust/project"),
764 total_files: 20,
765 total_lines: 2000,
766 languages: vec![],
767 primary_language: Some(crate::types::Language::Rust),
768 dependencies: vec![],
769 tdg_score: Some(95.0),
770 };
771
772 let config = BatutaConfig::from_analysis(&analysis);
773
774 assert_eq!(config.project.name, "project");
775 assert_eq!(config.project.primary_language, Some("Rust".to_string()));
776 }
777
778 #[test]
779 fn test_from_analysis_no_primary_language() {
780 let analysis = crate::types::ProjectAnalysis {
781 root_path: PathBuf::from("/unknown/project"),
782 total_files: 1,
783 total_lines: 10,
784 languages: vec![],
785 primary_language: None,
786 dependencies: vec![],
787 tdg_score: None,
788 };
789
790 let config = BatutaConfig::from_analysis(&analysis);
791
792 assert_eq!(config.project.name, "project");
793 assert!(config.project.primary_language.is_none());
794 }
795
796 #[test]
801 fn test_serialize_deserialize_batuta_config() {
802 let config = BatutaConfig::default();
803
804 let serialized = toml::to_string(&config).expect("toml serialize failed");
805 let deserialized: BatutaConfig = toml::from_str(&serialized).expect("toml parse failed");
806
807 assert_eq!(config.version, deserialized.version);
808 assert_eq!(config.project.name, deserialized.project.name);
809 assert_eq!(config.optimization.profile, deserialized.optimization.profile);
810 }
811
812 #[test]
813 fn test_serialize_deserialize_with_optional_fields() {
814 let mut config = BatutaConfig::default();
815 config.project.description = Some("Test description".to_string());
816 config.project.primary_language = Some("Python".to_string());
817 config.build.target = Some("x86_64-unknown-linux-gnu".to_string());
818
819 let serialized = toml::to_string(&config).expect("toml serialize failed");
820 let deserialized: BatutaConfig = toml::from_str(&serialized).expect("toml parse failed");
821
822 assert_eq!(config.project.description, deserialized.project.description);
823 assert_eq!(config.project.primary_language, deserialized.project.primary_language);
824 assert_eq!(config.build.target, deserialized.build.target);
825 }
826
827 #[test]
828 fn test_serialize_deserialize_with_vectors() {
829 let mut config = BatutaConfig::default();
830 config.project.authors = vec!["Alice".to_string(), "Bob".to_string()];
831 config.source.exclude = vec!["test".to_string(), "docs".to_string()];
832 config.transpilation.modules = vec!["mod1".to_string(), "mod2".to_string()];
833
834 let serialized = toml::to_string(&config).expect("toml serialize failed");
835 let deserialized: BatutaConfig = toml::from_str(&serialized).expect("toml parse failed");
836
837 assert_eq!(config.project.authors, deserialized.project.authors);
838 assert_eq!(config.source.exclude, deserialized.source.exclude);
839 assert_eq!(config.transpilation.modules, deserialized.transpilation.modules);
840 }
841
842 #[test]
843 fn test_full_toml_deserialization() {
844 let config = BatutaConfig::default();
846 let serialized = toml::to_string(&config).expect("toml serialize failed");
847 let deserialized: BatutaConfig = toml::from_str(&serialized).expect("toml parse failed");
848
849 assert_eq!(config.version, deserialized.version);
850 assert_eq!(config.project.name, deserialized.project.name);
851 assert_eq!(config.optimization.profile, deserialized.optimization.profile);
852 }
853
854 #[test]
855 fn test_modified_toml_deserialization() {
856 let mut config = BatutaConfig::default();
858 config.project.name = "custom-name".to_string();
859 config.optimization.profile = "aggressive".to_string();
860 config.build.release = false;
861
862 let serialized = toml::to_string(&config).expect("toml serialize failed");
863 let deserialized: BatutaConfig = toml::from_str(&serialized).expect("toml parse failed");
864
865 assert_eq!(deserialized.project.name, "custom-name");
866 assert_eq!(deserialized.optimization.profile, "aggressive");
867 assert!(!deserialized.build.release);
868 }
869
870 #[test]
875 fn test_decy_config_in_transpilation() {
876 let config = BatutaConfig::default();
877
878 assert!(config.transpilation.decy.ownership_inference);
879 assert!(config.transpilation.decy.actionable_diagnostics);
880 assert!(config.transpilation.decy.use_static_fixer);
881 }
882
883 #[test]
884 fn test_depyler_config_in_transpilation() {
885 let config = BatutaConfig::default();
886
887 assert!(config.transpilation.depyler.type_inference);
888 assert!(config.transpilation.depyler.numpy_to_trueno);
889 assert!(config.transpilation.depyler.sklearn_to_aprender);
890 assert!(config.transpilation.depyler.pytorch_to_realizar);
891 }
892
893 #[test]
894 fn test_bashrs_config_in_transpilation() {
895 let config = BatutaConfig::default();
896
897 assert_eq!(config.transpilation.bashrs.target_shell, "bash");
898 assert!(config.transpilation.bashrs.use_clap);
899 }
900
901 #[test]
902 fn test_trueno_config_in_optimization() {
903 let config = BatutaConfig::default();
904
905 assert_eq!(
906 config.optimization.trueno.backends,
907 vec!["simd".to_string(), "cpu".to_string()]
908 );
909 assert!(!config.optimization.trueno.adaptive_thresholds);
910 assert_eq!(config.optimization.trueno.cpu_threshold, 500);
911 }
912
913 #[test]
914 fn test_renacer_config_in_validation() {
915 let config = BatutaConfig::default();
916
917 assert!(config.validation.renacer.trace_syscalls.is_empty());
918 assert_eq!(config.validation.renacer.output_format, "json");
919 }
920
921 #[test]
926 fn test_config_modification() {
927 let mut config = BatutaConfig::default();
928
929 config.project.name = "new-name".to_string();
931 config.optimization.enable_gpu = true;
932 config.optimization.gpu_threshold = 2000;
933 config.transpilation.incremental = false;
934
935 assert_eq!(config.project.name, "new-name");
936 assert!(config.optimization.enable_gpu);
937 assert_eq!(config.optimization.gpu_threshold, 2000);
938 assert!(!config.transpilation.incremental);
939 }
940
941 #[test]
942 fn test_config_clone() {
943 let config = BatutaConfig::default();
944 let cloned = config.clone();
945
946 assert_eq!(config.version, cloned.version);
947 assert_eq!(config.project.name, cloned.project.name);
948 assert_eq!(config.optimization.profile, cloned.optimization.profile);
949 }
950
951 #[test]
952 fn test_save_modified_config() {
953 let temp_dir = TempDir::new().expect("tempdir creation failed");
954 let config_path = temp_dir.path().join("config.toml");
955
956 let mut config = BatutaConfig::default();
957 config.project.name = "modified-project".to_string();
958 config.project.authors = vec!["Author1".to_string(), "Author2".to_string()];
959 config.optimization.enable_gpu = true;
960
961 config.save(&config_path).expect("save failed");
962
963 let loaded = BatutaConfig::load(&config_path).expect("unexpected failure");
964
965 assert_eq!(loaded.project.name, "modified-project");
966 assert_eq!(loaded.project.authors.len(), 2);
967 assert!(loaded.optimization.enable_gpu);
968 }
969
970 #[test]
975 fn test_private_config_default() {
976 let config = PrivateConfig::default();
977 assert!(config.private.rust_stack_dirs.is_empty());
978 assert!(config.private.rust_corpus_dirs.is_empty());
979 assert!(config.private.python_corpus_dirs.is_empty());
980 assert!(config.private.endpoints.is_empty());
981 assert!(!config.has_dirs());
982 assert_eq!(config.dir_count(), 0);
983 }
984
985 #[test]
986 fn test_private_config_deserialize_full() {
987 let toml_str = r#"
988[private]
989rust_stack_dirs = ["../rmedia", "../infra"]
990rust_corpus_dirs = ["../internal-cookbook"]
991python_corpus_dirs = ["../private-notebooks"]
992"#;
993 let config: PrivateConfig = toml::from_str(toml_str).expect("toml parse failed");
994 assert_eq!(config.private.rust_stack_dirs.len(), 2);
995 assert_eq!(config.private.rust_corpus_dirs.len(), 1);
996 assert_eq!(config.private.python_corpus_dirs.len(), 1);
997 assert!(config.has_dirs());
998 assert_eq!(config.dir_count(), 4);
999 }
1000
1001 #[test]
1002 fn test_private_config_deserialize_partial() {
1003 let toml_str = r#"
1004[private]
1005rust_stack_dirs = ["../rmedia"]
1006"#;
1007 let config: PrivateConfig = toml::from_str(toml_str).expect("toml parse failed");
1008 assert_eq!(config.private.rust_stack_dirs, vec!["../rmedia"]);
1009 assert!(config.private.rust_corpus_dirs.is_empty());
1010 assert!(config.private.python_corpus_dirs.is_empty());
1011 assert!(config.has_dirs());
1012 assert_eq!(config.dir_count(), 1);
1013 }
1014
1015 #[test]
1016 fn test_private_config_deserialize_empty_private() {
1017 let toml_str = r#"
1018[private]
1019"#;
1020 let config: PrivateConfig = toml::from_str(toml_str).expect("toml parse failed");
1021 assert!(!config.has_dirs());
1022 assert_eq!(config.dir_count(), 0);
1023 }
1024
1025 #[test]
1026 fn test_private_config_with_endpoints() {
1027 let toml_str = r#"
1028[private]
1029rust_stack_dirs = ["../rmedia"]
1030
1031[[private.endpoints]]
1032name = "intel"
1033type = "ssh"
1034host = "intel.local"
1035index_path = "/tmp/batuta/rag/index.sqlite"
1036"#;
1037 let config: PrivateConfig = toml::from_str(toml_str).expect("toml parse failed");
1038 assert_eq!(config.private.endpoints.len(), 1);
1039 assert_eq!(config.private.endpoints[0].name, "intel");
1040 assert_eq!(config.private.endpoints[0].endpoint_type, "ssh");
1041 assert_eq!(config.private.endpoints[0].host, "intel.local");
1042 }
1043
1044 #[test]
1045 fn test_private_config_serialize_roundtrip() {
1046 let toml_str = r#"
1047[private]
1048rust_stack_dirs = ["../rmedia", "../infra"]
1049rust_corpus_dirs = ["../internal-cookbook"]
1050python_corpus_dirs = []
1051"#;
1052 let config: PrivateConfig = toml::from_str(toml_str).expect("toml parse failed");
1053 let serialized = toml::to_string(&config).expect("toml serialize failed");
1054 let roundtripped: PrivateConfig = toml::from_str(&serialized).expect("toml parse failed");
1055 assert_eq!(config.private.rust_stack_dirs, roundtripped.private.rust_stack_dirs);
1056 assert_eq!(config.private.rust_corpus_dirs, roundtripped.private.rust_corpus_dirs);
1057 }
1058
1059 #[test]
1060 fn test_private_config_find_config_path_checks_home() {
1061 let result = PrivateConfig::find_config_path();
1066 let _ = result;
1068 }
1069
1070 #[test]
1071 fn test_private_config_has_dirs() {
1072 let mut config = PrivateConfig::default();
1073 assert!(!config.has_dirs());
1074
1075 config.private.rust_stack_dirs.push("../foo".to_string());
1076 assert!(config.has_dirs());
1077 }
1078
1079 #[test]
1080 fn test_private_config_dir_count() {
1081 let mut config = PrivateConfig::default();
1082 assert_eq!(config.dir_count(), 0);
1083
1084 config.private.rust_stack_dirs.push("../a".to_string());
1085 config.private.rust_corpus_dirs.push("../b".to_string());
1086 config.private.python_corpus_dirs.push("../c".to_string());
1087 assert_eq!(config.dir_count(), 3);
1088 }
1089}