1#![allow(clippy::needless_doctest_main)]
2use std::collections::BTreeMap;
79use std::fmt;
80use std::fmt::Write as _;
81use std::fs;
82use std::path::{Path, PathBuf};
83
84#[derive(Debug)]
86pub enum Error {
87 ExtConflict(String),
89 StripPrefix(std::path::StripPrefixError),
91 Io(std::io::Error),
94}
95
96impl fmt::Display for Error {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 match self {
99 Error::ExtConflict(ext) => write!(
100 f,
101 "'{}' cannot be registered as both string and binary",
102 ext
103 ),
104 Error::StripPrefix(e) => write!(f, "failed to compute relative path: {}", e),
105 Error::Io(e) => write!(f, "I/O error: {}", e),
106 }
107 }
108}
109
110impl std::error::Error for Error {
111 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
112 match self {
113 Error::ExtConflict(_) => None,
114 Error::StripPrefix(e) => Some(e),
115 Error::Io(e) => Some(e),
116 }
117 }
118}
119
120impl From<std::path::StripPrefixError> for Error {
121 fn from(e: std::path::StripPrefixError) -> Self {
122 Error::StripPrefix(e)
123 }
124}
125
126impl From<std::io::Error> for Error {
127 fn from(e: std::io::Error) -> Self {
128 Error::Io(e)
129 }
130}
131
132pub type Result<T> = std::result::Result<T, Error>;
134
135#[derive(Debug)]
140pub struct Fixture {
141 fn_name: String,
142 path: PathBuf,
143 ext: String,
144}
145
146#[derive(Debug)]
152struct SourceDirectory {
153 p: PathBuf,
154 rel_to_manifest: bool,
155}
156
157impl SourceDirectory {
158 fn new(p: impl Into<PathBuf>) -> Self {
159 let p = p.into();
160 let mut p = p
161 .canonicalize()
162 .expect("could not make source path absolute");
163 let rel_to_manifest = match std::env::var("CARGO_MANIFEST_DIR") {
164 Ok(c) => match p.strip_prefix(&c) {
165 Ok(stripped) => {
166 p = stripped.to_path_buf();
167 true
168 }
169 _ => false,
170 },
171 _ => false,
172 };
173 Self { p, rel_to_manifest }
174 }
175
176 fn is_rel(&self) -> bool {
177 self.rel_to_manifest
178 }
179
180 fn path(&self) -> &Path {
181 &self.p
182 }
183}
184
185impl Default for SourceDirectory {
186 fn default() -> Self {
187 let cur = std::env::current_dir().expect("could not find the current directory");
188 Self::new(cur)
189 }
190}
191
192#[derive(Debug)]
210pub struct Config {
211 #[cfg(feature = "regex")]
212 pub(crate) allow_regexs: Vec<regex::Regex>,
213 pub(crate) allow_exts: Vec<String>,
214 pub(crate) ignore_paths: Vec<PathBuf>,
215 pub(crate) source: SourceDirectory,
216 pub(crate) include_ext_as_str: Vec<String>,
217 pub(crate) include_ext_as_bin: Vec<String>,
218 pub(crate) out_path: PathBuf,
219}
220
221impl Default for Config {
222 fn default() -> Self {
223 Self::new()
224 }
225}
226
227impl Config {
228 pub fn new() -> Self {
236 let out_path = std::env::var("OUT_DIR")
237 .map(PathBuf::from)
238 .unwrap_or(std::env::current_dir().unwrap());
239 let out_path = out_path.join("fixture_tree_autogen.rs");
240
241 Self {
242 #[cfg(feature = "regex")]
243 allow_regexs: vec![],
244 allow_exts: vec![],
245 ignore_paths: vec![],
246 include_ext_as_bin: vec![],
247 include_ext_as_str: vec![],
248 source: Default::default(),
249 out_path,
250 }
251 }
252
253 pub fn with_ext(mut self, ext: impl Into<String>) -> Self {
258 self.allow_exts.push(ext.into());
259 self
260 }
261
262 pub fn with_exts(mut self, exts: impl IntoIterator<Item = impl Into<String>>) -> Self {
269 self.allow_exts.extend(exts.into_iter().map(|x| x.into()));
270 self
271 }
272
273 pub fn without_path(mut self, p: impl Into<PathBuf>) -> Self {
281 self.ignore_paths.push(p.into());
282 self
283 }
284
285 pub fn without_paths(mut self, paths: impl IntoIterator<Item = impl Into<PathBuf>>) -> Self {
287 self.ignore_paths
288 .extend(paths.into_iter().map(|x| x.into()));
289 self
290 }
291
292 #[cfg(feature = "regex")]
304 pub fn with_allow_pattern(mut self, pat: impl Into<String>) -> Self {
305 self.allow_regexs
306 .push(regex::Regex::new(&pat.into()).expect("could not create regex"));
307 self
308 }
309
310 #[cfg(feature = "regex")]
318 pub fn with_allow_patterns(
319 mut self,
320 pats: impl IntoIterator<Item = impl Into<String>>,
321 ) -> Self {
322 let pats = pats
323 .into_iter()
324 .map(|x| regex::Regex::new(&x.into()).expect("could not create regex"));
325 self.allow_regexs.extend(pats);
326 self
327 }
328
329 pub fn from_path(mut self, p: impl Into<PathBuf>) -> Self {
339 let source = SourceDirectory::new(p);
340 self.source = source;
341 self
342 }
343
344 pub fn with_ext_as_string(mut self, ext: impl Into<String>) -> Self {
350 self.include_ext_as_str.push(ext.into());
351 self
352 }
353
354 pub fn with_exts_as_string(
356 mut self,
357 exts: impl IntoIterator<Item = impl Into<String>>,
358 ) -> Self {
359 self.include_ext_as_str
360 .extend(exts.into_iter().map(|x| x.into()));
361 self
362 }
363
364 pub fn with_ext_as_bin(mut self, ext: impl Into<String>) -> Self {
370 self.include_ext_as_bin.push(ext.into());
371 self
372 }
373
374 pub fn with_exts_as_bin(mut self, exts: impl IntoIterator<Item = impl Into<String>>) -> Self {
376 self.include_ext_as_bin
377 .extend(exts.into_iter().map(|x| x.into()));
378 self
379 }
380
381 pub fn with_output_path(mut self, p: impl Into<PathBuf>) -> Self {
385 self.out_path = p.into();
386 self
387 }
388
389 pub fn build(self) -> Result<FixtureTree> {
397 for ext in &self.include_ext_as_str {
398 if self.include_ext_as_bin.contains(ext) {
399 return Err(Error::ExtConflict(ext.clone()));
400 }
401 }
402 FixtureTree::new(self)
403 }
404}
405
406#[derive(Debug)]
412pub struct Directory {
413 directories: BTreeMap<String, Directory>,
414 fixtures: Vec<Fixture>,
415 path: PathBuf,
416}
417
418impl Directory {
419 pub fn from_path(p: &Path, config: &Config) -> Result<Self> {
421 let mut root = Self {
422 directories: BTreeMap::new(),
423 fixtures: Vec::new(),
424 path: p.to_path_buf(),
425 };
426 root.from_path_inner(p, config)?;
427 Ok(root)
428 }
429
430 #[allow(clippy::wrong_self_convention)]
431 fn from_path_inner(&mut self, p: &Path, config: &Config) -> Result<()> {
432 if let Ok(entries) = fs::read_dir(p) {
433 for entry in entries.flatten() {
434 let path = entry.path();
435 let relpath = path.strip_prefix(config.source.path())?.to_path_buf();
436 let ext = path
437 .extension()
438 .map(|s| s.to_string_lossy().to_string().to_lowercase());
439
440 if path.is_dir() && !config.ignore_paths.contains(&relpath) {
441 let dirname = path.file_name().unwrap().to_str().unwrap().to_string();
443 let subtree = self.directories.entry(dirname.clone()).or_insert(Self {
445 directories: BTreeMap::new(),
446 fixtures: Vec::new(),
447 path: p.join(&dirname),
448 });
449 subtree.from_path_inner(&path, config)?;
450 } else if ext.as_ref().is_some_and(|e| {
451 if config.allow_exts.is_empty() {
452 true
453 } else {
454 config.allow_exts.contains(e)
455 }
456 }) {
457 #[cfg(feature = "regex")]
460 if !config.allow_regexs.is_empty() {
461 let relpath_str = relpath.to_string_lossy();
462 if !config.allow_regexs.iter().any(|r| r.is_match(&relpath_str)) {
463 continue;
464 }
465 }
466
467 let fn_name = path
469 .file_stem()
470 .unwrap()
471 .to_str()
472 .unwrap()
473 .replace('-', "_")
474 .to_lowercase();
475
476 self.fixtures.push(Fixture {
477 fn_name,
478 path,
479 ext: ext.unwrap(),
480 });
481 }
482 }
483 }
484
485 Ok(())
486 }
487
488 pub fn generate_code(&self, config: &Config) -> String {
490 let mut buffer = String::from("// fixture-tree auto-generated fixture accessors\n\n");
491 self.generate_code_inner(config, &mut buffer, 0);
492 buffer
493 }
494
495 fn generate_code_inner(&self, config: &Config, buffer: &mut String, indent_level: usize) {
496 let indent = " ".repeat(indent_level);
497
498 let path = self.path.to_string_lossy().replace('\\', "/");
499
500 if config.source.is_rel() {
501 rel_mod_path(buffer, &path, &indent);
502 } else {
503 non_rel_mod_path(buffer, &path, &indent);
504 }
505
506 for f in &self.fixtures {
508 let path = f.path.to_string_lossy().replace('\\', "/");
509
510 if config.include_ext_as_str.contains(&f.ext) {
511 if config.source.is_rel() {
512 rel_as_string_file(buffer, &f.fn_name, &path, &indent);
513 } else {
514 non_rel_as_string_file(buffer, &f.fn_name, &path, &indent);
515 }
516 } else if config.include_ext_as_bin.contains(&f.ext) {
517 if config.source.is_rel() {
518 rel_as_bin_file(buffer, &f.fn_name, &path, &indent);
519 } else {
520 non_rel_as_bin_file(buffer, &f.fn_name, &path, &indent);
521 }
522 } else if config.source.is_rel() {
523 rel_file_path(buffer, &f.fn_name, &path, &indent);
524 } else {
525 non_rel_file_path(buffer, &f.fn_name, &path, &indent);
526 }
527 }
528
529 for (module_name, subtree) in &self.directories {
531 if subtree.is_empty() {
532 continue;
533 }
534 buffer.push_str(&format!("{}pub mod {} {{\n\n", indent, module_name));
535 subtree.generate_code_inner(config, buffer, indent_level + 1);
536 buffer.push_str(&format!("{}}}\n\n", indent));
537 }
538 }
539
540 pub fn is_empty(&self) -> bool {
543 self.fixtures.is_empty() && self.directories.values().all(|d| d.is_empty())
544 }
545}
546
547#[derive(Debug)]
552pub struct FixtureTree {
553 root: Directory,
554 config: Config,
555}
556
557impl FixtureTree {
558 pub fn new(config: Config) -> Result<Self> {
560 let root = Directory::from_path(config.source.path(), &config)?;
561 Ok(Self { root, config })
562 }
563
564 pub fn generate_fixtures(&self) -> Result<()> {
571 let fixtures = self.root.generate_code(&self.config);
572 fs::write(&self.config.out_path, fixtures)?;
573 Ok(())
574 }
575}
576
577fn non_rel_mod_path(buffer: &mut String, path: &str, indent: &str) {
578 write!(
579 buffer,
580 r#"{indent}pub fn path() -> &'static std::path::Path {{
581{indent} std::path::Path::new("{path}")
582{indent}}}
583
584"#
585 )
586 .unwrap();
587}
588
589fn rel_mod_path(buffer: &mut String, path: &str, indent: &str) {
590 write!(
591 buffer,
592 "{indent}pub fn path() -> &'static std::path::Path {{\n\
593{indent} std::path::Path::new(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/{path}\"))\n\
594{indent}}}\n\n"
595 )
596 .unwrap();
597}
598
599fn non_rel_file_path(buffer: &mut String, fn_name: &str, path: &str, indent: &str) {
600 write!(
601 buffer,
602 r#"{indent}pub fn {fn_name}() -> &'static std::path::Path {{
603{indent} std::path::Path::new("{path}")
604{indent}}}
605
606"#
607 )
608 .unwrap();
609}
610
611fn rel_file_path(buffer: &mut String, fn_name: &str, path: &str, indent: &str) {
612 write!(
613 buffer,
614 r#"{indent}pub fn {fn_name}() -> &'static std::path::Path {{
615{indent} std::path::Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/{path}"))
616{indent}}}
617
618"#
619 )
620 .unwrap();
621}
622
623fn rel_as_string_file(buffer: &mut String, fn_name: &str, path: &str, indent: &str) {
624 write!(buffer,
625r#"{indent}pub fn {fn_name}() -> (&'static std::path::Path, &'static str) {{
626{indent} (std::path::Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/{path}")), include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/{path}")))
627{indent}}}
628
629"#
630 ).unwrap();
631}
632
633fn non_rel_as_string_file(buffer: &mut String, fn_name: &str, path: &str, indent: &str) {
634 write!(
635 buffer,
636 r#"{indent}pub fn {fn_name}() -> (&'static std::path::Path, &'static str) {{
637{indent} (std::path::Path::new("{path}"), include_str!("{path}"))
638{indent}}}
639
640"#
641 )
642 .unwrap();
643}
644
645fn rel_as_bin_file(buffer: &mut String, fn_name: &str, path: &str, indent: &str) {
646 write!(buffer,
647r#"{indent}pub fn {fn_name}() -> (&'static std::path::Path, &'static [u8]) {{
648{indent} (std::path::Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/{path}")), include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/{path}")))
649{indent}}}
650
651"#
652 ).unwrap();
653}
654
655fn non_rel_as_bin_file(buffer: &mut String, fn_name: &str, path: &str, indent: &str) {
656 write!(
657 buffer,
658 r#"{indent}pub fn {fn_name}() -> (&'static std::path::Path, &'static [u8]) {{
659{indent} (std::path::Path::new("{path}"), include_bytes!("{path}"))
660{indent}}}
661
662"#
663 )
664 .unwrap();
665}
666
667#[cfg(test)]
668mod tests {
669 use super::*;
670 use std::path::PathBuf;
671 use tempfile::TempDir;
672
673 fn create_fixture_tree(root: &Path) {
675 let dirs = [
676 "",
677 "models",
678 "configs",
679 "configs/pass",
680 "configs/fail",
681 "ignored",
682 ];
683 for d in &dirs {
684 fs::create_dir_all(root.join(d)).unwrap();
685 }
686
687 let text_files = [
688 ("alpha.json", r#"{"a": 1}"#),
689 ("beta.json", r#"{"b": 2}"#),
690 ("delta.txt", "plain text"),
691 ("models/linear.json", r#"{"type": "linear"}"#),
692 ("models/conv.json", r#"{"type": "conv"}"#),
693 ("configs/pass/basic.json", r#"{"ok": true}"#),
694 (
695 "configs/pass/advanced.json",
696 r#"{"ok": true, "level": "advanced"}"#,
697 ),
698 ("configs/fail/bad_config.json", r#"{"ok": false}"#),
699 ("ignored/should_skip.json", r#"{"skip": true}"#),
700 ];
701 for (name, content) in &text_files {
702 fs::write(root.join(name), content).unwrap();
703 }
704
705 fs::write(root.join("gamma.bin"), &[0xDE, 0xAD, 0xBE, 0xEF]).unwrap();
707 fs::write(root.join("models/weights.bin"), &[0x01, 0x02, 0x03]).unwrap();
708 }
709
710 fn generate_to_string(config: Config) -> String {
712 let out = config.out_path.clone();
713 let ft = config.build().unwrap();
714 ft.generate_fixtures().unwrap();
715 fs::read_to_string(&out).unwrap()
716 }
717
718 use std::sync::atomic::{AtomicUsize, Ordering};
719
720 static TEST_COUNTER: AtomicUsize = AtomicUsize::new(0);
721
722 fn setup_tempdir() -> TempDir {
724 let tmp = TempDir::new().unwrap();
725 create_fixture_tree(tmp.path());
726 tmp
727 }
728
729 struct InManifestDir(PathBuf);
733
734 impl InManifestDir {
735 fn new() -> Self {
736 let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
737 let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
738 let test_root = manifest.join("test_fixtures");
739 let dir = test_root.join(format!("auto_{id}"));
740 if dir.exists() {
741 fs::remove_dir_all(&dir).unwrap();
742 }
743 create_fixture_tree(&dir);
744 Self(dir)
745 }
746
747 fn path(&self) -> &Path {
748 &self.0
749 }
750 }
751
752 impl Drop for InManifestDir {
753 fn drop(&mut self) {
754 let _ = fs::remove_dir_all(&self.0);
755 }
756 }
757
758 #[test]
761 fn manual_review_all() {
762 let dir = InManifestDir::new();
763 let out = dir.path().parent().unwrap().join("manual_review_all.rs");
764
765 Config::new()
766 .from_path(dir.path())
767 .with_output_path(out.clone())
768 .with_exts_as_string(["json", "txt"])
769 .with_exts_as_bin(["bin"])
770 .build()
771 .unwrap()
772 .generate_fixtures()
773 .unwrap();
774 }
775
776 #[test]
777 fn manual_review_only_stringy() {
778 let dir = InManifestDir::new();
779 let out = dir
780 .path()
781 .parent()
782 .unwrap()
783 .join("manual_review_stringy.rs");
784
785 Config::new()
786 .from_path(dir.path())
787 .with_output_path(out.clone())
788 .with_exts_as_string(["json", "txt"])
789 .build()
790 .unwrap()
791 .generate_fixtures()
792 .unwrap();
793 }
794
795 #[test]
796 fn in_manifest_generates_all_json_files() {
797 let dir = InManifestDir::new();
798 let out = dir.path().join("_test_out.rs");
799 let code = generate_to_string(
800 Config::new()
801 .from_path(dir.path())
802 .with_output_path(&out)
803 .with_ext("json")
804 .with_ext_as_string("json"),
805 );
806
807 assert!(code.contains("pub fn alpha()"), "alpha.json missing");
809 assert!(code.contains("pub fn beta()"), "beta.json missing");
810
811 assert!(
813 code.contains("pub fn linear()"),
814 "models/linear.json missing"
815 );
816 assert!(code.contains("pub fn conv()"), "models/conv.json missing");
817 assert!(
818 code.contains("pub fn basic()"),
819 "configs/pass/basic.json missing"
820 );
821 assert!(
822 code.contains("pub fn advanced()"),
823 "configs/pass/advanced.json missing"
824 );
825 assert!(
826 code.contains("pub fn bad_config()"),
827 "configs/fail/bad_config.json missing"
828 );
829
830 assert!(
832 !code.contains("pub fn gamma()"),
833 "gamma.bin should be excluded by ext filter"
834 );
835 assert!(
836 !code.contains("pub fn delta()"),
837 "delta.txt should be excluded by ext filter"
838 );
839 assert!(
840 !code.contains("pub fn weights()"),
841 "weights.bin should be excluded by ext filter"
842 );
843
844 assert!(
846 code.contains("env!(\"CARGO_MANIFEST_DIR\")"),
847 "should use CARGO_MANIFEST_DIR"
848 );
849 assert!(
850 !code.contains("\"/home"),
851 "should not contain absolute paths"
852 );
853 }
854
855 #[test]
856 fn in_manifest_nested_modules_structure() {
857 let dir = InManifestDir::new();
858 let out = dir.path().join("_test_structure.rs");
859 let code = generate_to_string(
860 Config::new()
861 .from_path(dir.path())
862 .with_output_path(&out)
863 .with_ext("json")
864 .with_ext_as_string("json"),
865 );
866
867 assert!(code.contains("pub mod models {"), "models module missing");
868 assert!(code.contains("pub mod configs {"), "configs module missing");
869 assert!(code.contains("pub mod pass {"), "pass module missing");
870 assert!(code.contains("pub mod fail {"), "fail module missing");
871 assert!(code.contains("pub mod ignored {"), "ignored module missing");
872 }
873
874 #[test]
875 fn in_manifest_path_fn_per_module() {
876 let dir = InManifestDir::new();
877 let out = dir.path().join("_test_paths.rs");
878 let code = generate_to_string(
879 Config::new()
880 .from_path(dir.path())
881 .with_output_path(&out)
882 .with_ext("json")
883 .with_ext_as_string("json"),
884 );
885
886 let path_count = code.matches("pub fn path()").count();
888 assert!(
890 path_count >= 6,
891 "expected at least 6 path() fns, got {path_count}"
892 );
893 }
894
895 #[test]
896 fn in_manifest_ext_as_string_returns_tuple() {
897 let dir = InManifestDir::new();
898 let out = dir.path().join("_test_str.rs");
899 let code = generate_to_string(
900 Config::new()
901 .from_path(dir.path())
902 .with_output_path(&out)
903 .with_ext("json")
904 .with_ext_as_string("json"),
905 );
906
907 assert!(
908 code.contains("(&'static std::path::Path, &'static str)"),
909 "string fixtures should return (Path, str) tuple"
910 );
911 assert!(
912 code.contains("include_str!"),
913 "should use include_str! for string fixtures"
914 );
915 }
916
917 #[test]
918 fn in_manifest_ext_as_bin_returns_bytes() {
919 let dir = InManifestDir::new();
920 let out = dir.path().join("_test_bin.rs");
921 let code = generate_to_string(
922 Config::new()
923 .from_path(dir.path())
924 .with_output_path(&out)
925 .with_ext("bin")
926 .with_ext_as_bin("bin"),
927 );
928
929 assert!(code.contains("pub fn gamma()"), "gamma.bin missing");
930 assert!(
931 code.contains("pub fn weights()"),
932 "models/weights.bin missing"
933 );
934 assert!(
935 code.contains("(&'static std::path::Path, &'static [u8])"),
936 "binary fixtures should return (Path, [u8]) tuple"
937 );
938 assert!(
939 code.contains("include_bytes!"),
940 "should use include_bytes! for binary fixtures"
941 );
942 }
943
944 #[test]
945 fn in_manifest_multiple_exts() {
946 let dir = InManifestDir::new();
947 let out = dir.path().join("_test_multi_ext.rs");
948 let code = generate_to_string(
949 Config::new()
950 .from_path(dir.path())
951 .with_output_path(&out)
952 .with_ext("json")
953 .with_ext("bin")
954 .with_ext_as_string("json")
955 .with_ext_as_bin("bin"),
956 );
957
958 assert!(code.contains("pub fn alpha()"), "json fixture missing");
960 assert!(code.contains("pub fn gamma()"), "bin fixture missing");
961 assert!(code.contains("include_str!"), "missing include_str");
962 assert!(code.contains("include_bytes!"), "missing include_bytes");
963 }
964
965 #[test]
966 fn in_manifest_no_ext_filter_includes_everything() {
967 let dir = InManifestDir::new();
968 let out = dir.path().join("_test_no_ext.rs");
969 let code = generate_to_string(
970 Config::new()
971 .from_path(dir.path())
972 .with_output_path(&out)
973 .with_ext_as_string("json")
974 .with_ext_as_string("txt")
975 .with_ext_as_bin("bin"),
976 );
977
978 assert!(code.contains("pub fn alpha()"), "json missing");
980 assert!(code.contains("pub fn gamma()"), "bin missing");
981 assert!(code.contains("pub fn delta()"), "txt missing");
982 }
983
984 #[test]
985 fn in_manifest_ignore_paths() {
986 let dir = InManifestDir::new();
987 let out = dir.path().join("_test_ignore.rs");
988 let code = generate_to_string(
989 Config::new()
990 .from_path(dir.path())
991 .with_output_path(&out)
992 .with_ext("json")
993 .with_ext_as_string("json")
994 .without_path(PathBuf::from("ignored")),
995 );
996
997 assert!(
998 !code.contains("pub fn should_skip()"),
999 "ignored dir should be excluded"
1000 );
1001 assert!(
1002 code.contains("pub fn alpha()"),
1003 "non-ignored files should remain"
1004 );
1005 }
1006
1007 #[test]
1008 fn in_manifest_ignore_multiple_paths() {
1009 let dir = InManifestDir::new();
1010 let out = dir.path().join("_test_ignore_multi.rs");
1011 let code = generate_to_string(
1012 Config::new()
1013 .from_path(dir.path())
1014 .with_output_path(&out)
1015 .with_ext("json")
1016 .with_ext_as_string("json")
1017 .without_paths(vec![PathBuf::from("ignored"), PathBuf::from("configs")]),
1018 );
1019
1020 assert!(!code.contains("pub fn should_skip()"), "ignored/ excluded");
1021 assert!(!code.contains("pub fn basic()"), "configs/ excluded");
1022 assert!(!code.contains("pub fn bad_config()"), "configs/ excluded");
1023 assert!(code.contains("pub fn alpha()"), "root files remain");
1024 assert!(code.contains("pub fn linear()"), "models/ remain");
1025 }
1026
1027 #[test]
1028 #[cfg(feature = "regex")]
1029 fn in_manifest_regex_filter_filename() {
1030 let dir = InManifestDir::new();
1031 let out = dir.path().join("_test_regex_name.rs");
1032 let code = generate_to_string(
1033 Config::new()
1034 .from_path(dir.path())
1035 .with_output_path(&out)
1036 .with_ext("json")
1037 .with_ext_as_string("json")
1038 .with_allow_pattern(r"alpha|beta"),
1039 );
1040
1041 assert!(code.contains("pub fn alpha()"), "alpha should match");
1042 assert!(code.contains("pub fn beta()"), "beta should match");
1043 assert!(!code.contains("pub fn linear()"), "linear should not match");
1044 assert!(!code.contains("pub fn basic()"), "basic should not match");
1045 }
1046
1047 #[test]
1048 #[cfg(feature = "regex")]
1049 fn in_manifest_regex_filter_path_component() {
1050 let dir = InManifestDir::new();
1051 let out = dir.path().join("_test_regex_path.rs");
1052 let code = generate_to_string(
1053 Config::new()
1054 .from_path(dir.path())
1055 .with_output_path(&out)
1056 .with_ext("json")
1057 .with_ext_as_string("json")
1058 .with_allow_pattern(r"^configs/pass/"),
1059 );
1060
1061 assert!(
1062 code.contains("pub fn basic()"),
1063 "configs/pass/basic.json should match"
1064 );
1065 assert!(
1066 code.contains("pub fn advanced()"),
1067 "configs/pass/advanced.json should match"
1068 );
1069 assert!(
1070 !code.contains("pub fn bad_config()"),
1071 "configs/fail/ should not match"
1072 );
1073 assert!(
1074 !code.contains("pub fn alpha()"),
1075 "root files should not match"
1076 );
1077 }
1078
1079 #[test]
1080 #[cfg(feature = "regex")]
1081 fn in_manifest_multiple_regex_patterns_or() {
1082 let dir = InManifestDir::new();
1083 let out = dir.path().join("_test_regex_multi.rs");
1084 let code = generate_to_string(
1085 Config::new()
1086 .from_path(dir.path())
1087 .with_output_path(&out)
1088 .with_ext("json")
1089 .with_ext_as_string("json")
1090 .with_allow_patterns(vec![r"^alpha", r"^models/"]),
1091 );
1092
1093 assert!(
1094 code.contains("pub fn alpha()"),
1095 "alpha should match first pattern"
1096 );
1097 assert!(
1098 code.contains("pub fn linear()"),
1099 "models/ should match second pattern"
1100 );
1101 assert!(
1102 code.contains("pub fn conv()"),
1103 "models/ should match second pattern"
1104 );
1105 assert!(
1106 !code.contains("pub fn beta()"),
1107 "beta should not match either pattern"
1108 );
1109 }
1110
1111 #[test]
1112 fn in_manifest_empty_dirs_omitted() {
1113 let dir = InManifestDir::new();
1114 let out = dir.path().join("_test_empty.rs");
1115 let code = generate_to_string(
1118 Config::new()
1119 .from_path(dir.path())
1120 .with_output_path(&out)
1121 .with_ext("txt")
1122 .with_ext_as_string("txt"),
1123 );
1124
1125 assert!(
1126 code.contains("pub fn delta()"),
1127 "delta.txt should be present"
1128 );
1129 assert!(
1130 !code.contains("pub mod models {"),
1131 "empty models module should be omitted"
1132 );
1133 assert!(
1134 !code.contains("pub mod configs {"),
1135 "empty configs module should be omitted"
1136 );
1137 }
1138
1139 #[test]
1140 fn in_manifest_fn_name_sanitisation() {
1141 let dir = InManifestDir::new();
1142 fs::write(dir.path().join("my-dashed-name.json"), "{}").unwrap();
1144 let out = dir.path().join("_test_sanitise.rs");
1145 let code = generate_to_string(
1146 Config::new()
1147 .from_path(dir.path())
1148 .with_output_path(&out)
1149 .with_ext("json")
1150 .with_ext_as_string("json"),
1151 );
1152
1153 assert!(
1154 code.contains("pub fn my_dashed_name()"),
1155 "dashes should be replaced with underscores"
1156 );
1157 assert!(
1158 !code.contains("pub fn my-dashed-name()"),
1159 "raw dashes should not appear in fn names"
1160 );
1161 }
1162
1163 #[test]
1164 fn in_manifest_ext_overlap_rejected() {
1165 let dir = InManifestDir::new();
1166 let out = dir.path().join("_test_overlap.rs");
1167 let result = Config::new()
1168 .from_path(dir.path())
1169 .with_output_path(&out)
1170 .with_ext("json")
1171 .with_ext_as_string("json")
1172 .with_ext_as_bin("json")
1173 .build();
1174
1175 assert!(result.is_err(), "overlapping string/bin ext should fail");
1176 let msg = result.unwrap_err().to_string();
1177 assert!(
1178 msg.contains("json"),
1179 "error should mention the offending ext"
1180 );
1181 }
1182
1183 #[test]
1186 fn tempdir_generates_absolute_paths() {
1187 let tmp = setup_tempdir();
1188 let out = tmp.path().join("_test_out.rs");
1189 let code = generate_to_string(
1190 Config::new()
1191 .from_path(tmp.path())
1192 .with_output_path(&out)
1193 .with_ext("json")
1194 .with_ext_as_string("json"),
1195 );
1196
1197 assert!(
1199 !code.contains("env!(\"CARGO_MANIFEST_DIR\")"),
1200 "temp-dir output should not use CARGO_MANIFEST_DIR"
1201 );
1202
1203 let tmp_str = tmp.path().to_string_lossy();
1205 assert!(
1206 code.contains(tmp_str.as_ref()),
1207 "output should contain the temp dir absolute path"
1208 );
1209 }
1210
1211 #[test]
1212 fn tempdir_generates_all_json_files() {
1213 let tmp = setup_tempdir();
1214 let out = tmp.path().join("_test_all.rs");
1215 let code = generate_to_string(
1216 Config::new()
1217 .from_path(tmp.path())
1218 .with_output_path(&out)
1219 .with_ext("json")
1220 .with_ext_as_string("json"),
1221 );
1222
1223 assert!(code.contains("pub fn alpha()"), "alpha missing");
1224 assert!(code.contains("pub fn beta()"), "beta missing");
1225 assert!(code.contains("pub fn linear()"), "linear missing");
1226 assert!(code.contains("pub fn basic()"), "basic missing");
1227 assert!(code.contains("pub fn bad_config()"), "bad_config missing");
1228 assert!(
1229 !code.contains("pub fn gamma()"),
1230 "gamma.bin should be excluded"
1231 );
1232 }
1233
1234 #[test]
1235 fn tempdir_ext_as_bin() {
1236 let tmp = setup_tempdir();
1237 let out = tmp.path().join("_test_bin.rs");
1238 let code = generate_to_string(
1239 Config::new()
1240 .from_path(tmp.path())
1241 .with_output_path(&out)
1242 .with_ext("bin")
1243 .with_ext_as_bin("bin"),
1244 );
1245
1246 assert!(code.contains("pub fn gamma()"), "gamma.bin missing");
1247 assert!(code.contains("pub fn weights()"), "weights.bin missing");
1248 assert!(code.contains("include_bytes!"), "should use include_bytes!");
1249 assert!(
1250 !code.contains("include_str!"),
1251 "should not use include_str!"
1252 );
1253 }
1254
1255 #[test]
1256 fn tempdir_ignore_paths() {
1257 let tmp = setup_tempdir();
1258 let out = tmp.path().join("_test_ign.rs");
1259 let code = generate_to_string(
1260 Config::new()
1261 .from_path(tmp.path())
1262 .with_output_path(&out)
1263 .with_ext("json")
1264 .with_ext_as_string("json")
1265 .without_path(PathBuf::from("ignored"))
1266 .without_path(PathBuf::from("models")),
1267 );
1268
1269 assert!(!code.contains("pub fn should_skip()"), "ignored/ excluded");
1270 assert!(!code.contains("pub fn linear()"), "models/ excluded");
1271 assert!(code.contains("pub fn alpha()"), "root files remain");
1272 }
1273
1274 #[test]
1275 #[cfg(feature = "regex")]
1276 fn tempdir_regex_filter() {
1277 let tmp = setup_tempdir();
1278 let out = tmp.path().join("_test_regex.rs");
1279 let code = generate_to_string(
1280 Config::new()
1281 .from_path(tmp.path())
1282 .with_output_path(&out)
1283 .with_ext("json")
1284 .with_ext_as_string("json")
1285 .with_allow_pattern(r"configs/"),
1286 );
1287
1288 assert!(
1289 code.contains("pub fn basic()"),
1290 "configs/ files should match"
1291 );
1292 assert!(
1293 code.contains("pub fn bad_config()"),
1294 "configs/ files should match"
1295 );
1296 assert!(
1297 !code.contains("pub fn alpha()"),
1298 "root files should not match"
1299 );
1300 assert!(
1301 !code.contains("pub fn linear()"),
1302 "models/ should not match"
1303 );
1304 }
1305
1306 #[test]
1307 fn tempdir_nested_modules() {
1308 let tmp = setup_tempdir();
1309 let out = tmp.path().join("_test_mods.rs");
1310 let code = generate_to_string(
1311 Config::new()
1312 .from_path(tmp.path())
1313 .with_output_path(&out)
1314 .with_ext("json")
1315 .with_ext_as_string("json"),
1316 );
1317
1318 assert!(code.contains("pub mod models {"), "models module");
1319 assert!(code.contains("pub mod configs {"), "configs module");
1320 assert!(code.contains("pub mod pass {"), "pass submodule");
1321 assert!(code.contains("pub mod fail {"), "fail submodule");
1322 }
1323
1324 #[test]
1325 fn tempdir_empty_modules_pruned() {
1326 let tmp = setup_tempdir();
1327 let out = tmp.path().join("_test_prune.rs");
1328 let code = generate_to_string(
1329 Config::new()
1330 .from_path(tmp.path())
1331 .with_output_path(&out)
1332 .with_ext("txt")
1333 .with_ext_as_string("txt"),
1334 );
1335
1336 assert!(code.contains("pub fn delta()"), "delta.txt present");
1337 assert!(!code.contains("pub mod models {"), "empty dir pruned");
1338 }
1339
1340 #[test]
1341 fn tempdir_mixed_string_and_bin() {
1342 let tmp = setup_tempdir();
1343 let out = tmp.path().join("_test_mixed.rs");
1344 let code = generate_to_string(
1345 Config::new()
1346 .from_path(tmp.path())
1347 .with_output_path(&out)
1348 .with_ext("json")
1349 .with_ext("bin")
1350 .with_ext_as_string("json")
1351 .with_ext_as_bin("bin"),
1352 );
1353
1354 assert!(code.contains("include_str!"), "json uses include_str!");
1356 assert!(code.contains("include_bytes!"), "bin uses include_bytes!");
1358 assert!(code.contains("pub fn alpha()"), "json fixture");
1360 assert!(code.contains("pub fn gamma()"), "bin fixture");
1361 }
1362
1363 #[test]
1364 fn tempdir_ext_overlap_rejected() {
1365 let tmp = setup_tempdir();
1366 let out = tmp.path().join("_test_overlap.rs");
1367 let result = Config::new()
1368 .from_path(tmp.path())
1369 .with_output_path(&out)
1370 .with_ext("json")
1371 .with_ext_as_string("json")
1372 .with_ext_as_bin("json")
1373 .build();
1374
1375 assert!(result.is_err(), "overlapping ext should be rejected");
1376 }
1377}