1use std::path::{Component, Path};
7
8use nova_diagnostics::{FileDb, FileId, Spanned};
9use nova_lexer::Token;
10
11use crate::markdown::{self, Block, CodeBlock, DocText, Docs};
12use crate::model::{self, Failure, Impl, Package};
13
14use nova_driver::DOC_TEST_FUNCTION as FUNCTION;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Origin {
21 Added,
24 Code {
26 at: u32,
28 cut: Option<(u32, u32)>,
31 },
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct DocTest {
38 pub name: String,
40 pub place: String,
43 pub text: String,
45 pub file: String,
49 pub start: u32,
52 pub lines: Vec<Origin>,
54 pub unknown: Vec<String>,
56}
57
58impl DocTest {
59 pub fn map(&self, offset: u32) -> u32 {
63 let bytes = self.text.as_bytes();
64 let offset = (offset as usize).min(bytes.len());
65 let line = bytes[..offset].iter().filter(|&&b| b == b'\n').count();
66 let line_start = bytes[..offset]
67 .iter()
68 .rposition(|&b| b == b'\n')
69 .map_or(0, |i| i + 1);
70 let col = (offset - line_start) as u32;
71 match self.lines.get(line) {
72 Some(Origin::Code {
73 at,
74 cut: Some((c, len)),
75 }) if col >= *c => at + col + len,
76 Some(Origin::Code { at, .. }) => at + col,
77 _ => self.start,
78 }
79 }
80}
81
82pub fn package_doc_tests(root: &Path) -> Result<Option<Vec<DocTest>>, Failure> {
85 let Some((package, a)) = model::library_package(root)? else {
86 return Ok(None);
87 };
88 let Some(graph) = a.graph.as_ref() else {
89 return Ok(Some(Vec::new()));
90 };
91 let (dir, import) = (&graph.root().dir, graph.root().import_name.as_str());
92 let shown = |file: FileId| match a.modules.iter().find(|(f, _)| *f == file) {
93 Some((_, path)) => slashed(path.strip_prefix(dir).unwrap_or(path)),
94 None => a.db.get_name(file).unwrap_or("?").to_string(),
95 };
96 Ok(Some(collect(&package, &a.db, Some(import), &shown)))
97}
98
99pub fn std_doc_tests() -> Result<Vec<DocTest>, Failure> {
101 let (package, a) = model::std_package()?;
102 let shown = |file: FileId| std_place(a.db.get_name(file).unwrap_or("?"));
103 Ok(collect(&package, &a.db, None, &shown))
104}
105
106fn std_place(name: &str) -> String {
109 match name.strip_prefix("<std/").and_then(|n| n.strip_suffix('>')) {
110 Some(m) => format!("std/{m}/lib.nova"),
111 None => name.to_string(),
112 }
113}
114
115fn slashed(path: &Path) -> String {
117 path.components()
118 .filter_map(|c| match c {
119 Component::Normal(s) => s.to_str(),
120 _ => None,
121 })
122 .collect::<Vec<_>>()
123 .join("/")
124}
125
126fn collect(
130 package: &Package,
131 db: &FileDb,
132 import: Option<&str>,
133 shown: &dyn Fn(FileId) -> String,
134) -> Vec<DocTest> {
135 let mut tests = Vec::new();
136 for (path, docs) in documented(package) {
137 tests.extend(doc_tests_of(&path, &docs.source, db, import, shown));
138 }
139 tests
140}
141
142fn documented(package: &Package) -> Vec<(String, &Docs)> {
146 fn impl_docs<'p>(out: &mut Vec<(String, &'p Docs)>, path: &str, i: &'p Impl) {
147 out.push((path.to_string(), &i.docs));
148 for m in &i.methods {
149 out.push((format!("{path}::{}", m.name), &m.docs));
150 }
151 }
152 let mut out = Vec::new();
153 for m in &package.modules {
154 out.push((m.qual.clone(), &m.docs));
155 for it in &m.items {
156 let path = format!("{}::{}", m.qual, it.name);
157 out.push((path.clone(), &it.docs));
158 for member in &it.members {
159 out.push((format!("{path}::{}", member.name), &member.docs));
160 }
161 for i in &it.impls {
162 impl_docs(&mut out, &path, i);
163 }
164 }
165 for i in &m.other_impls {
166 impl_docs(&mut out, &format!("{}::{}", m.qual, i.on), i);
167 }
168 }
169 for pr in &package.primitives {
170 for i in &pr.impls {
171 impl_docs(&mut out, &format!("std::{}", pr.name), i);
172 }
173 }
174 out
175}
176
177fn doc_tests_of(
180 path: &str,
181 doc: &DocText,
182 db: &FileDb,
183 import: Option<&str>,
184 shown: &dyn Fn(FileId) -> String,
185) -> Vec<DocTest> {
186 let Some(file) = doc.file else {
187 return Vec::new();
188 };
189 let Some(file_name) = db.get_name(file) else {
190 return Vec::new();
191 };
192 let mut out = Vec::new();
193 for b in markdown::code_blocks(&doc.text) {
194 let Block::Test { unknown } = &b.block else {
195 continue;
196 };
197 let Some(start) = doc.source(b.start) else {
198 continue;
199 };
200 let line = db.location(file, start).map_or(1, |(l, _)| l);
201 let place = format!("{}:{line}", shown(file));
202 let (text, lines) = module(import, run_lines(&b, doc));
203 out.push(DocTest {
204 name: format!("{path} ({place})"),
205 place,
206 text,
207 file: file_name.to_string(),
208 start,
209 lines,
210 unknown: unknown.clone(),
211 });
212 }
213 out
214}
215
216struct RunLine {
218 text: String,
219 origin: Origin,
220}
221
222fn run_lines(b: &CodeBlock, doc: &DocText) -> Vec<RunLine> {
225 b.lines
226 .iter()
227 .map(|l| {
228 let at = doc.source(l.at).unwrap_or(0).saturating_sub(l.pad as u32);
231 match markdown::hidden(&l.text) {
232 Some((cut, len)) => RunLine {
233 text: format!("{}{}", &l.text[..cut], &l.text[cut + len..]),
234 origin: Origin::Code {
235 at,
236 cut: Some((cut as u32, len as u32)),
237 },
238 },
239 None => RunLine {
240 text: l.text.clone(),
241 origin: Origin::Code { at, cut: None },
242 },
243 }
244 })
245 .collect()
246}
247
248fn module(import: Option<&str>, lines: Vec<RunLine>) -> (String, Vec<Origin>) {
252 let code = lines
253 .iter()
254 .map(|l| l.text.as_str())
255 .collect::<Vec<_>>()
256 .join("\n");
257 let (tokens, _) = nova_lexer::lex(&code, FileId::DUMMY);
260 let hoisted = leading_imports(&code, &tokens);
261 let main = declares_main(&tokens);
262 let mut out: Vec<(String, Origin)> = Vec::new();
263 if let Some(p) = import {
264 if !imports(&tokens, p) {
265 out.push((format!("import {p}"), Origin::Added));
266 }
267 }
268 let mut rest = lines.into_iter();
269 out.extend(rest.by_ref().take(hoisted).map(|l| (l.text, l.origin)));
270 match main {
271 Some(is_async) => {
272 out.extend(rest.map(|l| (l.text, l.origin)));
273 let call = if is_async {
275 "let _ = block_on(main())"
276 } else {
277 "let _ = main()"
278 };
279 out.push((format!("@test fn {FUNCTION}() {{ {call} }}"), Origin::Added));
280 }
281 None => {
282 out.push((format!("@test fn {FUNCTION}() {{"), Origin::Added));
283 out.extend(rest.map(|l| (l.text, l.origin)));
284 out.push(("}".to_string(), Origin::Added));
285 }
286 }
287 let mut text = out
288 .iter()
289 .map(|(t, _)| t.as_str())
290 .collect::<Vec<_>>()
291 .join("\n");
292 text.push('\n');
293 (text, out.into_iter().map(|(_, o)| o).collect())
294}
295
296fn import_end(tokens: &[Spanned<Token>], i: usize) -> usize {
299 let at = |j: usize| tokens.get(j).map(|t| &t.value);
300 let mut j = i + 1;
301 if matches!(at(j), Some(Token::Ident(_))) {
302 j += 1;
303 }
304 while at(j) == Some(&Token::ColonColon) && matches!(at(j + 1), Some(Token::Ident(_))) {
305 j += 2;
306 }
307 if at(j) == Some(&Token::ColonColon) {
308 j += 1;
309 }
310 match at(j) {
311 Some(Token::LBrace) => {
312 while !matches!(at(j), None | Some(Token::RBrace) | Some(Token::Eof)) {
313 j += 1;
314 }
315 if at(j) == Some(&Token::RBrace) {
316 j += 1;
317 }
318 }
319 Some(Token::As) => {
320 j += 1;
321 if matches!(at(j), Some(Token::Ident(_))) {
322 j += 1;
323 }
324 }
325 _ => {}
326 }
327 if at(j) == Some(&Token::Semicolon) {
328 j += 1;
329 }
330 j
331}
332
333fn leading_imports(code: &str, tokens: &[Spanned<Token>]) -> usize {
337 let line = |offset: u32| {
338 code.as_bytes()[..(offset as usize).min(code.len())]
339 .iter()
340 .filter(|&&b| b == b'\n')
341 .count()
342 };
343 let mut lines = 0;
344 let mut i = 0;
345 while tokens.get(i).is_some_and(|t| t.value == Token::Import) {
346 let end = import_end(tokens, i);
347 let last = line(tokens[end - 1].span.end.saturating_sub(1));
348 let next = &tokens[end.min(tokens.len() - 1)];
349 if next.value != Token::Eof && line(next.span.start) <= last {
350 break;
351 }
352 lines = last + 1;
353 i = end;
354 }
355 lines
356}
357
358fn declares_main(tokens: &[Spanned<Token>]) -> Option<bool> {
361 let mut depth = 0usize;
362 for (i, t) in tokens.iter().enumerate() {
363 match &t.value {
364 Token::LBrace => depth += 1,
365 Token::RBrace => depth = depth.saturating_sub(1),
366 Token::Fn if depth == 0 => {
367 let named_main = matches!(
368 tokens.get(i + 1).map(|t| &t.value),
369 Some(Token::Ident(n)) if n == "main"
370 );
371 if named_main {
372 return Some(i > 0 && tokens[i - 1].value == Token::Async);
373 }
374 }
375 _ => {}
376 }
377 }
378 None
379}
380
381fn imports(tokens: &[Spanned<Token>], name: &str) -> bool {
384 tokens
385 .windows(2)
386 .any(|w| w[0].value == Token::Import && matches!(&w[1].value, Token::Ident(n) if n == name))
387}
388
389#[cfg(test)]
390mod tests {
391 use super::*;
392 use crate::signature::FileText;
393 use std::path::PathBuf;
394
395 fn fresh(name: &str) -> PathBuf {
398 let dir = std::env::temp_dir().join(format!("nova-doc-doctest-{name}"));
399 let _ = std::fs::remove_dir_all(&dir);
400 std::fs::create_dir_all(&dir).unwrap();
401 dir
402 }
403
404 fn write(dir: &Path, files: &[(&str, &str)]) {
405 for (path, text) in files {
406 let path = dir.join(path);
407 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
408 std::fs::write(path, text).unwrap();
409 }
410 }
411
412 fn manifest(name: &str) -> String {
413 format!("[package]\nname = \"{name}\"\nversion = \"0.1.0\"\nedition = \"2026\"\n")
414 }
415
416 fn doc_tests(root: &Path) -> Option<Vec<DocTest>> {
418 match package_doc_tests(root) {
419 Ok(tests) => tests,
420 Err(Failure::Diagnostics { db, diagnostics }) => panic!(
421 "{}",
422 nova_diagnostics::render::render_to_string(&db, &diagnostics)
423 ),
424 Err(Failure::Other(why)) => panic!("{why}"),
425 }
426 }
427
428 fn lines(texts: &[&str]) -> Vec<RunLine> {
430 texts
431 .iter()
432 .enumerate()
433 .map(|(i, t)| RunLine {
434 text: t.to_string(),
435 origin: Origin::Code {
436 at: 100 * i as u32,
437 cut: None,
438 },
439 })
440 .collect()
441 }
442
443 fn doc_lines(src: &str, file: FileId) -> Vec<Spanned<String>> {
445 nova_lexer::lex(src, file)
446 .0
447 .into_iter()
448 .filter_map(|t| match t.value {
449 Token::DocComment(s) => Some(Spanned::new(s, t.span)),
450 _ => None,
451 })
452 .collect()
453 }
454
455 #[test]
456 fn a_block_becomes_a_module() {
457 let code = |at: u32| Origin::Code { at, cut: None };
459 let (text, origins) = module(
460 Some("shapes"),
461 lines(&[
462 "// Measure.",
463 "import shapes::{circle}",
464 "",
465 "let c = circle(1.0)",
466 ]),
467 );
468 assert_eq!(
469 text,
470 "// Measure.\nimport shapes::{circle}\n@test fn nova_doctest() {\n\n\
471 let c = circle(1.0)\n}\n"
472 );
473 assert_eq!(
474 origins,
475 [
476 code(0),
477 code(100),
478 Origin::Added,
479 code(200),
480 code(300),
481 Origin::Added
482 ]
483 );
484 for (import, block, expected) in [
485 (
486 Some("shapes"),
487 &["let c = circle(1.0)"][..],
488 "import shapes\n@test fn nova_doctest() {\nlet c = circle(1.0)\n}\n",
489 ),
490 (
491 Some("shapes"),
492 &["import shapes", "let c = circle(1.0)"][..],
493 "import shapes\n@test fn nova_doctest() {\nlet c = circle(1.0)\n}\n",
494 ),
495 (
496 Some("shapes"),
497 &["let s = \"import shapes\" // import shapes"][..],
498 "import shapes\n@test fn nova_doctest() {\n\
499 let s = \"import shapes\" // import shapes\n}\n",
500 ),
501 (
502 Some("shapes"),
503 &["fn main() {", " let a = 1", "}"][..],
504 "import shapes\nfn main() {\n let a = 1\n}\n\
505 @test fn nova_doctest() { let _ = main() }\n",
506 ),
507 (
508 Some("shapes"),
509 &["async fn main() {", "}"][..],
510 "import shapes\nasync fn main() {\n}\n\
511 @test fn nova_doctest() { let _ = block_on(main()) }\n",
512 ),
513 (
514 Some("shapes"),
515 &["fn helper() { fn main() {} }", "helper()"][..],
516 "import shapes\n@test fn nova_doctest() {\nfn helper() { fn main() {} }\n\
517 helper()\n}\n",
518 ),
519 (
520 None,
521 &["import a::{", " b,", "}", "f()"][..],
522 "import a::{\n b,\n}\n@test fn nova_doctest() {\nf()\n}\n",
523 ),
524 (
525 None,
526 &["import x; let a = 1", "let b = 2"][..],
527 "@test fn nova_doctest() {\nimport x; let a = 1\nlet b = 2\n}\n",
528 ),
529 ] {
530 assert_eq!(module(import, lines(block)).0, expected, "{block:?}");
531 }
532 let (_, origins) = module(Some("shapes"), lines(&["fn main() {", "}"]));
533 assert_eq!(origins, [Origin::Added, code(0), code(100), Origin::Added]);
534 }
535
536 struct Memory(String);
538
539 impl nova_driver::Sources for Memory {
540 fn read(&self, _: &Path) -> std::io::Result<String> {
541 Ok(self.0.clone())
542 }
543 }
544
545 #[test]
546 fn a_main_that_returns_a_value_runs_as_a_program_would() {
547 for (block, call) in [
550 ("fn main() -> Int { 1 }", "let _ = main()"),
551 ("async fn main() -> Int { 1 }", "let _ = block_on(main())"),
552 ("fn main() {}", "let _ = main()"),
553 ] {
554 let (text, _) = module(None, lines(&[block]));
555 let program = nova_driver::Program::doc_tests(
556 None,
557 vec![PathBuf::from("<doc tests>").join("doc0.nova")],
558 );
559 let options = nova_driver::Options {
560 tests: true,
561 ..nova_driver::Options::default()
562 };
563 let a = nova_driver::analyze_program(program, &Memory(text.clone()), &options)
564 .expect("the module reads");
565 let errors: Vec<&str> = a
566 .diagnostics
567 .iter()
568 .filter(|d| d.severity == nova_diagnostics::Severity::Error)
569 .map(|d| d.message.as_str())
570 .collect();
571 assert_eq!(errors, Vec::<&str>::new(), "{block}");
572 assert_eq!(
573 text,
574 format!("{block}\n@test fn nova_doctest() {{ {call} }}\n")
575 );
576 }
577 }
578
579 #[test]
580 fn the_line_map_points_into_the_doc_comment() {
581 for (src, line) in [
585 (
586 "/// A circle.\n///\n/// ```\n/// # let r = 1.0\n/// let c = circle(r)\n/// ```\n\
587 pub fn circle(r: Float) -> Float { r }\n",
588 3,
589 ),
590 (
591 "/// A circle.\r\n///\r\n/// ```\r\n/// # let r = 1.0\r\n/// let c = circle(r)\r\n\
592 /// ```\r\npub fn circle(r: Float) -> Float { r }\r\n",
593 3,
594 ),
595 (
596 " /// - In a list:\n ///\n /// ```\n /// # let r = 1.0\n \
597 /// let c = circle(r)\n /// ```\n pub fn circle(r: Float) -> Float { r }\n",
598 3,
599 ),
600 (
601 "///> ```\n///> # let r = 1.0\n///> let c = circle(r)\n///> ```\nfn f() {}\n",
602 1,
603 ),
604 ("///\tlet r = 1.0\n///\tlet c = circle(r)\nfn f() {}\n", 1),
605 ] {
606 let mut db = FileDb::new();
607 let file = db.add("src/lib.nova", src);
608 let doc = markdown::join(&doc_lines(src, file));
609 let tests = doc_tests_of("shapes::circle", &doc, &db, Some("shapes"), &|_| {
610 "src/lib.nova".to_string()
611 });
612 assert_eq!(tests.len(), 1, "{src:?}");
613 let t = &tests[0];
614 assert_eq!(t.name, format!("shapes::circle (src/lib.nova:{line})"));
615 assert_eq!(t.place, format!("src/lib.nova:{line}"));
616 assert_eq!(t.file, "src/lib.nova");
617 assert_eq!(
619 t.text,
620 "import shapes\n@test fn nova_doctest() {\nlet r = 1.0\nlet c = circle(r)\n}\n",
621 "{src:?}"
622 );
623 let at = |s: &str| src.find(s).unwrap() as u32;
624 let made = |s: &str| t.text.find(s).unwrap() as u32;
625 assert_eq!(t.map(made("c = circle")), at("c = circle"), "{src:?}");
626 assert_eq!(t.map(made("let r")), at("let r"), "{src:?}");
627 assert_eq!(t.map(made("1.0")), at("1.0"), "{src:?}");
628 let first = src.find("```").or_else(|| src.find("let r")).unwrap() as u32;
629 assert_eq!(t.start, first, "{src:?}");
630 assert_eq!(t.map(0), first);
632 assert_eq!(t.map(t.text.len() as u32 - 2), first);
633 }
634 }
635
636 #[test]
637 fn a_std_doc_test_imports_nothing_and_names_its_modules_file() {
638 let src = "//! Time.\n//!\n//! ```\n//! let d = 1\n//! ```\npub fn now() -> Int { 1 }\n";
639 let mut db = FileDb::new();
640 let file = db.add("<std/time>", src);
641 let doc = FileText::new(src, file).module_docs();
642 let tests = doc_tests_of("std::time", &doc, &db, None, &|f| {
643 std_place(db.get_name(f).unwrap_or("?"))
644 });
645 assert_eq!(tests.len(), 1);
646 let t = &tests[0];
647 assert_eq!(t.name, "std::time (std/time/lib.nova:3)");
648 assert_eq!(t.file, "<std/time>");
649 assert_eq!(t.text, "@test fn nova_doctest() {\nlet d = 1\n}\n");
650 assert_eq!(
651 t.map(t.text.find("d = 1").unwrap() as u32),
652 src.find("d = 1").unwrap() as u32
653 );
654 let Ok(tests) = std_doc_tests() else {
655 panic!("std's doc tests are not read")
656 };
657 for t in &tests {
658 assert!(t.name.starts_with("std::"), "{}", t.name);
659 assert!(t.place.starts_with("std/"), "{}", t.place);
660 assert!(t.file.starts_with("<std/"), "{}", t.file);
661 }
662 }
663
664 const SHAPES_LIB: &str = "//! Shapes.\n//!\n//! ```\n//! let a = circle(1.0)\n//! ```\n\n\
665 import units\n\n\
666 /// A circle's area.\n///\n/// ```nova,should_panic\n/// let a = circle(2.0)\n/// ```\n\
667 ///\n/// ```text\n/// not a test\n/// ```\n\
668 pub fn circle(r: Float) -> Float { 3.0 * r * r }\n\n\
669 /// A rectangle.\npub record Rect {\n /// Its width.\n ///\n /// ```\n \
670 /// let r = Rect { width: 1.0 }\n /// ```\n pub width: Float,\n}\n\n\
671 /// Rect's own methods.\n///\n/// let r = Rect { width: 2.0 }\n\
672 impl Rect {\n /// Its width, twice.\n ///\n /// ```ignore\n \
673 /// let w = Rect { width: 1.0 }.twice()\n /// ```\n \
674 pub fn twice(self) -> Float { self.width * 2.0 }\n}\n\n\
675 /// Not exported.\n///\n/// ```\n/// let s = secret()\n/// ```\n\
676 fn secret() -> Int { 1 }\n";
677
678 const SHAPES_UNITS: &str = "//! Lengths.\n//!\n//! ```\n//! let l = Length { metres: 1.0 }\n\
679 //! ```\n\n/// A length.\npub record Length {\n pub metres: Float,\n}\n\n\
680 impl Length {\n /// The sum of two lengths.\n ///\n /// ```\n \
681 /// let l = Length { metres: 1.0 }.plus(Length { metres: 2.0 })\n /// ```\n \
682 pub fn plus(self, other: Length) -> Length {\n \
683 Length { metres: self.metres + other.metres }\n }\n}\n";
684
685 #[test]
686 fn doc_tests_are_named_by_the_sites_paths() {
687 let dir = fresh("names");
690 write(
691 &dir,
692 &[
693 ("nova.toml", &manifest("shapes")),
694 ("src/lib.nova", SHAPES_LIB),
695 ("src/units.nova", SHAPES_UNITS),
696 ],
697 );
698 let tests = doc_tests(&dir).expect("a library");
699 let names: Vec<&str> = tests.iter().map(|t| t.name.as_str()).collect();
700 assert_eq!(
701 names,
702 [
703 "shapes (src/lib.nova:3)",
704 "shapes::circle (src/lib.nova:11)",
705 "shapes::Rect::width (src/lib.nova:24)",
706 "shapes::Rect (src/lib.nova:32)",
707 "shapes::units (src/units.nova:3)",
708 "shapes::units::Length::plus (src/units.nova:15)",
709 ]
710 );
711 assert_eq!(tests[1].unknown, ["should_panic"]);
712 assert!(tests[0].unknown.is_empty());
713 assert_eq!(
714 tests[1].text,
715 "import shapes\n@test fn nova_doctest() {\nlet a = circle(2.0)\n}\n"
716 );
717 let lib = dir.join("src").join("lib.nova").display().to_string();
720 let units = dir.join("src").join("units.nova").display().to_string();
721 assert_eq!(tests[0].file, lib);
722 assert_eq!(tests[5].file, units);
723 assert_eq!(tests[0].start, SHAPES_LIB.find("```").unwrap() as u32);
724 }
725
726 #[test]
727 fn only_a_librarys_documented_items_give_doc_tests() {
728 let dir = fresh("program");
730 write(
731 &dir,
732 &[
733 ("nova.toml", &manifest("app")),
734 (
735 "src/main.nova",
736 "/// Runs.\n///\n/// ```\n/// main()\n/// ```\nfn main() {}\n",
737 ),
738 ],
739 );
740 assert_eq!(doc_tests(&dir), None);
741 let dir = fresh("quiet");
743 write(
744 &dir,
745 &[
746 ("nova.toml", &manifest("quiet")),
747 (
748 "src/lib.nova",
749 "/// ```\n/// let a = secret()\n/// ```\nfn secret() -> Int { 1 }\n\n\
750 /// ```text\n/// shown()\n/// ```\npub fn shown() -> Int { secret() }\n\n\
751 /// ```ignore\n/// skipped()\n/// ```\npub fn skipped() -> Int { 2 }\n",
752 ),
753 ],
754 );
755 assert_eq!(doc_tests(&dir), Some(Vec::new()));
756 }
757}