From 8b0faf609312a0eaedfaf516e4acffe93f502322 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Tue, 28 Jul 2026 22:31:19 +0200 Subject: [PATCH] extract source code of doctests --- src/lib.rs | 58 ++++++++++++++-- tests/example-doctests/expected_results.json | 72 ++++++++++---------- 2 files changed, 89 insertions(+), 41 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 53f72e3..2138108 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,7 @@ where message: Some("no tests detected; probable build failure".into()), tests: Vec::new(), }; + let mut doctest_cache = HashMap::new(); for (idx, event) in events.enumerate() { let event = match event { Err(e) => { @@ -39,11 +40,58 @@ where break; } }; - let test_code = name_to_code - .get(&name) - .map(String::as_str) - .unwrap_or(TEST_CODE_NOT_FOUND_MSG) - .to_string(); + let (name, test_code) = if name.contains("src/") { + // We're dealing with a doctest, those contain "src/" in their name. + // + // example name: + // "macros/src/compile_fail_tests.rs - compile_fail_tests::_ONLY_ARROW (line 48)" + // + // The "macros/" prefix is optional, in case the user manually + // declared a workspace. That's why we ignore that prefix. + // + // We parse the files lazily here, because only few exercises + // contain doctests. Closure for error boundary. + (|| { + let mut words = name.split_ascii_whitespace(); + let file_name = words.next()?.split('/').next_back()?; + let item_name = words.nth(1)?.split("::").last()?.trim_start_matches("_"); + let line = words.nth(1)?.trim_end_matches(")").parse::().ok()? - 1; + if !doctest_cache.contains_key(file_name) { + let mut line_to_code = HashMap::new(); + let content = std::fs::read_to_string(format!("src/{file_name}")).ok()?; + let mut lines = content.lines().enumerate(); + 'find_doctest: while let Some((i, line)) = lines.next() { + if !line.starts_with("/// ```") { + continue; + } + // doctest block start, gather code lines + let mut code = String::new(); + for (_, line) in lines.by_ref() { + if line.starts_with("/// ```") { + // doctest block end + code.pop(); // trim trailing newline + line_to_code.insert(i, code); + continue 'find_doctest; + } + code.push_str(line.trim_start_matches("/// ")); + code.push('\n'); + } + // no end of code block found. very strange. ignore. + } + doctest_cache.insert(file_name.to_owned(), line_to_code); + } + let test_code = doctest_cache.get(file_name)?.get(&line)?; + Some((item_name.into(), test_code.into())) + })() + .unwrap_or_else(|| (name.clone(), TEST_CODE_NOT_FOUND_MSG.into())) + } else { + let test_code = name_to_code + .get(&name) + .map(String::as_str) + .unwrap_or(TEST_CODE_NOT_FOUND_MSG) + .to_string(); + (name, test_code) + }; match event.event { ct::EventKind::Started => continue, ct::EventKind::Ok => { diff --git a/tests/example-doctests/expected_results.json b/tests/example-doctests/expected_results.json index f16a155..267c22c 100644 --- a/tests/example-doctests/expected_results.json +++ b/tests/example-doctests/expected_results.json @@ -4,116 +4,116 @@ "message": null, "tests": [ { - "name": "Empty", - "test_code": "let expected: HashMap = HashMap::new();\nlet computed: HashMap = hashmap!();\nassert_eq!(computed, expected);", + "name": "Comma separator", + "test_code": "use macros::hashmap;\nuse std::collections::HashMap;\n///\n// using only commas is invalid\nlet _hm: HashMap<_, _> = hashmap!('a', 1);", "status": "pass", "message": null }, { - "name": "Nested", - "test_code": "let mut expected = HashMap::new();\nexpected.insert(\"non-empty\", {\n let mut subhashmap = HashMap::new();\n subhashmap.insert(23, 623);\n subhashmap.insert(34, 21);\n subhashmap\n});\nexpected.insert(\"empty\", HashMap::new());\nassert_eq!(\n hashmap!(\n \"non-empty\" => hashmap!(\n 23 => 623,\n 34 => 21\n ),\n \"empty\" => hashmap!()\n ),\n expected\n);", + "name": "Double trailing commas", + "test_code": "use macros::hashmap;\nuse std::collections::HashMap;\n///\n// a single trailing comma is okay, but two is not\nlet _hm: HashMap<_, _> = hashmap!('a' => 2, ,);", "status": "pass", "message": null }, { - "name": "No trailing comma", - "test_code": "let mut expected = HashMap::new();\nexpected.insert(1, \"one\");\nexpected.insert(2, \"two\");\nassert_eq!(hashmap!(1 => \"one\", 2 => \"two\"), expected);", + "name": "Empty", + "test_code": "let expected: HashMap = HashMap::new();\nlet computed: HashMap = hashmap!();\nassert_eq!(computed, expected);", "status": "pass", "message": null }, { - "name": "Single", - "test_code": "let mut expected = HashMap::new();\nexpected.insert(1, \"one\");\nassert_eq!(hashmap!(1 => \"one\"), expected);", + "name": "Leading comma", + "test_code": "use macros::hashmap;\nuse std::collections::HashMap;\n///\n// leading commas are not valid\nlet _hm: HashMap<_, _> = hashmap!(, 'a' => 2);", "status": "pass", "message": null }, { - "name": "Src/compile fail tests.rs - compile fail tests:: comma separator (line 3)", - "test_code": "It looks like the test runner failed to retrieve the code for this test. Please consider reporting this on the forum so we can try to fix it. Thanks!\n\nhttps://forum.exercism.org/c/exercism/bugs-and-features/126", + "name": "Missing argument", + "test_code": "use macros::hashmap;\nuse std::collections::HashMap;\n///\n// an argument should come between each pair of commas\nlet _hm: HashMap<_, _> = hashmap!('a' => 1, , 'b' => 2);", "status": "pass", "message": null }, { - "name": "Src/compile fail tests.rs - compile fail tests:: double trailing commas (line 12)", - "test_code": "It looks like the test runner failed to retrieve the code for this test. Please consider reporting this on the forum so we can try to fix it. Thanks!\n\nhttps://forum.exercism.org/c/exercism/bugs-and-features/126", + "name": "Missing comma", + "test_code": "use macros::hashmap;\nuse std::collections::HashMap;\n///\n// Key value pairs must be separated by commas\nlet _hm: HashMap<_, _> = hashmap!('a' => 1 'b' => 2);", "status": "pass", "message": null }, { - "name": "Src/compile fail tests.rs - compile fail tests:: leading comma (line 66)", - "test_code": "It looks like the test runner failed to retrieve the code for this test. Please consider reporting this on the forum so we can try to fix it. Thanks!\n\nhttps://forum.exercism.org/c/exercism/bugs-and-features/126", + "name": "Nested", + "test_code": "let mut expected = HashMap::new();\nexpected.insert(\"non-empty\", {\n let mut subhashmap = HashMap::new();\n subhashmap.insert(23, 623);\n subhashmap.insert(34, 21);\n subhashmap\n});\nexpected.insert(\"empty\", HashMap::new());\nassert_eq!(\n hashmap!(\n \"non-empty\" => hashmap!(\n 23 => 623,\n 34 => 21\n ),\n \"empty\" => hashmap!()\n ),\n expected\n);", "status": "pass", "message": null }, { - "name": "Src/compile fail tests.rs - compile fail tests:: missing argument (line 84)", - "test_code": "It looks like the test runner failed to retrieve the code for this test. Please consider reporting this on the forum so we can try to fix it. Thanks!\n\nhttps://forum.exercism.org/c/exercism/bugs-and-features/126", + "name": "No trailing comma", + "test_code": "let mut expected = HashMap::new();\nexpected.insert(1, \"one\");\nexpected.insert(2, \"two\");\nassert_eq!(hashmap!(1 => \"one\", 2 => \"two\"), expected);", "status": "pass", "message": null }, { - "name": "Src/compile fail tests.rs - compile fail tests:: missing comma (line 75)", - "test_code": "It looks like the test runner failed to retrieve the code for this test. Please consider reporting this on the forum so we can try to fix it. Thanks!\n\nhttps://forum.exercism.org/c/exercism/bugs-and-features/126", + "name": "Only arrow", + "test_code": "use macros::hashmap;\nuse std::collections::HashMap;\n///\n// a single random arrow is not valid\nlet _hm: HashMap<(), ()> = hashmap!(=>);", "status": "pass", "message": null }, { - "name": "Src/compile fail tests.rs - compile fail tests:: only arrow (line 48)", - "test_code": "It looks like the test runner failed to retrieve the code for this test. Please consider reporting this on the forum so we can try to fix it. Thanks!\n\nhttps://forum.exercism.org/c/exercism/bugs-and-features/126", + "name": "Only comma", + "test_code": "use macros::hashmap;\nuse std::collections::HashMap;\n///\n// a single random comma is not valid\nlet _hm: HashMap<(), ()> = hashmap!(,);", "status": "pass", "message": null }, { - "name": "Src/compile fail tests.rs - compile fail tests:: only comma (line 21)", - "test_code": "It looks like the test runner failed to retrieve the code for this test. Please consider reporting this on the forum so we can try to fix it. Thanks!\n\nhttps://forum.exercism.org/c/exercism/bugs-and-features/126", + "name": "Single", + "test_code": "let mut expected = HashMap::new();\nexpected.insert(1, \"one\");\nassert_eq!(hashmap!(1 => \"one\"), expected);", "status": "pass", "message": null }, { - "name": "Src/compile fail tests.rs - compile fail tests:: single argument (line 30)", - "test_code": "It looks like the test runner failed to retrieve the code for this test. Please consider reporting this on the forum so we can try to fix it. Thanks!\n\nhttps://forum.exercism.org/c/exercism/bugs-and-features/126", + "name": "Single argument", + "test_code": "use macros::hashmap;\nuse std::collections::HashMap;\n///\n// a single argument is invalid\nlet _hm: HashMap<_, _> = hashmap!('a');", "status": "pass", "message": null }, { - "name": "Src/compile fail tests.rs - compile fail tests:: trailing arrow (line 57)", + "name": "Test::macro out of scope", "test_code": "It looks like the test runner failed to retrieve the code for this test. Please consider reporting this on the forum so we can try to fix it. Thanks!\n\nhttps://forum.exercism.org/c/exercism/bugs-and-features/126", "status": "pass", "message": null }, { - "name": "Src/compile fail tests.rs - compile fail tests:: triple arguments (line 39)", + "name": "Test::type not in scope", "test_code": "It looks like the test runner failed to retrieve the code for this test. Please consider reporting this on the forum so we can try to fix it. Thanks!\n\nhttps://forum.exercism.org/c/exercism/bugs-and-features/126", "status": "pass", "message": null }, { - "name": "Src/lib.rs - whatever (line 24)", - "test_code": "It looks like the test runner failed to retrieve the code for this test. Please consider reporting this on the forum so we can try to fix it. Thanks!\n\nhttps://forum.exercism.org/c/exercism/bugs-and-features/126", + "name": "Trailing arrow", + "test_code": "use macros::hashmap;\nuse std::collections::HashMap;\n///\n// a trailing => isn't valid either\nhashmap!('a' => 2, =>);", "status": "pass", "message": null }, { - "name": "Test::macro out of scope", - "test_code": "It looks like the test runner failed to retrieve the code for this test. Please consider reporting this on the forum so we can try to fix it. Thanks!\n\nhttps://forum.exercism.org/c/exercism/bugs-and-features/126", + "name": "Trailing comma", + "test_code": "let mut expected = HashMap::new();\nexpected.insert('h', 89);\nexpected.insert('a', 1);\nexpected.insert('s', 19);\nexpected.insert('h', 8);\nassert_eq!(\n hashmap!(\n 'h' => 89,\n 'a' => 1,\n 's' => 19,\n 'h' => 8,\n ),\n expected\n);", "status": "pass", "message": null }, { - "name": "Test::type not in scope", - "test_code": "It looks like the test runner failed to retrieve the code for this test. Please consider reporting this on the forum so we can try to fix it. Thanks!\n\nhttps://forum.exercism.org/c/exercism/bugs-and-features/126", + "name": "Triple arguments", + "test_code": "use macros::hashmap;\nuse std::collections::HashMap;\n///\n// three arguments are invalid\nhashmap!('a' => 1, 'b');", "status": "pass", "message": null }, { - "name": "Trailing comma", - "test_code": "let mut expected = HashMap::new();\nexpected.insert('h', 89);\nexpected.insert('a', 1);\nexpected.insert('s', 19);\nexpected.insert('h', 8);\nassert_eq!(\n hashmap!(\n 'h' => 89,\n 'a' => 1,\n 's' => 19,\n 'h' => 8,\n ),\n expected\n);", + "name": "Type override", + "test_code": "// The macro should always use std::collections::HashMap and ignore crate::std::collections::HashMap\nmod std {\n pub mod collections {\n pub struct HashMap;\n\n impl HashMap {\n #[allow(dead_code)]\n pub fn new() -> Self {\n panic!(\"Do not allow users to override which HashMap is used\");\n }\n\n #[allow(dead_code)]\n pub fn insert(&mut self, _key: K, _val: V) {\n panic!(\"Do not allow users to override which HashMap is used\");\n }\n }\n }\n}\n\nlet _empty: ::std::collections::HashMap<(), ()> = hashmap!();\nlet _without_comma = hashmap!(1 => 2, 3 => 4);\nlet _with_trailing = hashmap!(1 => 2, 3 => 4,);", "status": "pass", "message": null }, { - "name": "Type override", - "test_code": "// The macro should always use std::collections::HashMap and ignore crate::std::collections::HashMap\nmod std {\n pub mod collections {\n pub struct HashMap;\n\n impl HashMap {\n #[allow(dead_code)]\n pub fn new() -> Self {\n panic!(\"Do not allow users to override which HashMap is used\");\n }\n\n #[allow(dead_code)]\n pub fn insert(&mut self, _key: K, _val: V) {\n panic!(\"Do not allow users to override which HashMap is used\");\n }\n }\n }\n}\n\nlet _empty: ::std::collections::HashMap<(), ()> = hashmap!();\nlet _without_comma = hashmap!(1 => 2, 3 => 4);\nlet _with_trailing = hashmap!(1 => 2, 3 => 4,);", + "name": "Whatever", + "test_code": "invalid rust syntax", "status": "pass", "message": null }