--- /dev/null
+{
+ "git": {
+ "sha1": "5a484e7d207de70b4730bc6043661787d4471a99"
+ },
+ "path_in_vcs": ""
+}
\ No newline at end of file
--- /dev/null
+# Generated by VS Code
+# will have custom VS Code specific settings
+/.vscode/
+
+# Generated by Cargo
+# will have compiled files and executables
+/target/
+
+# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
+# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
+Cargo.lock
+
+# These are backup files generated by rustfmt
+**/*.rs.bk
+
+# Code coverage file used by VS Code extensions
+lcov.info
+
+# Generated by Cargo for sub-crates
+debugger_test_parser/target
+
+# Ignore all *.profraw files
+**/*.profraw
--- /dev/null
+# Microsoft Open Source Code of Conduct
+
+This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
+
+Resources:
+
+- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)
+- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
+- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns
--- /dev/null
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies.
+#
+# If you are reading this file be aware that the original Cargo.toml
+# will likely look very different (and much more reasonable).
+# See Cargo.toml.orig for the original contents.
+
+[package]
+edition = "2018"
+name = "debugger_test"
+version = "0.1.5"
+exclude = ["/.github/*"]
+description = """
+Provides a proc macro for writing tests that launch a debugger and run commands while verifying the output.
+"""
+homepage = "https://github.com/microsoft/rust_debugger_test"
+documentation = "https://docs.rs/debugger_test"
+readme = "README.md"
+keywords = [
+ "debugger",
+ "cdb",
+ "natvis",
+ "debugger_visualizer",
+]
+license = "MIT OR Apache-2.0"
+repository = "https://github.com/microsoft/rust_debugger_test"
+
+[lib]
+proc-macro = true
+
+[dependencies.anyhow]
+version = "1.0.40"
+
+[dependencies.log]
+version = "0.4.17"
+
+[dependencies.quote]
+version = "1.0.20"
+
+[dependencies.syn]
+version = "1.0"
+features = ["full"]
+
+[dev-dependencies.debugger_test_parser]
+version = "0.1.0"
+
+[dev-dependencies.regex]
+version = "1.6.0"
--- /dev/null
+[package]
+name = "debugger_test"
+version = "0.1.5"
+edition = "2018"
+description = """
+Provides a proc macro for writing tests that launch a debugger and run commands while verifying the output.
+"""
+documentation = "https://docs.rs/debugger_test"
+readme = "README.md"
+homepage = "https://github.com/microsoft/rust_debugger_test"
+repository = "https://github.com/microsoft/rust_debugger_test"
+license = "MIT OR Apache-2.0"
+keywords = ["debugger", "cdb", "natvis", "debugger_visualizer"]
+exclude = ["/.github/*"]
+
+[lib]
+proc-macro = true
+
+[dependencies]
+anyhow = "1.0.40"
+log = "0.4.17"
+quote = "1.0.20"
+syn = { version = "1.0", features = ["full"] }
+
+[dev-dependencies]
+debugger_test_parser = "0.1.0"
+regex = "1.6.0"
--- /dev/null
+ MIT License
+
+ Copyright (c) Microsoft Corporation.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE
--- /dev/null
+# debugger_test
+
+Provides an easy way of integrating debugger specific tests into a crate.
+
+This crate is responsible for generating the `#[debugger_test]` proc macro attribute.
+
+## Usage
+
+To use, add this crate and the `debugger_test_parser` as a dependency in your `Cargo.toml`.
+
+This crate uses the `debugger_test_parser` to parse the output of the specified debugger
+and verify all expected statements were found.
+
+In order to set breakpoints, an `__break()` function will need to be defined and called
+at each place the debugger should stop.
+
+For example:
+
+```rust
+#[inline(never)]
+fn __break() { }
+
+#[debugger_test(
+ debugger = "cdb",
+ commands = r#"
+.nvlist
+dv
+g"#,
+ expected_statements = r#"
+pattern:test\.exe .*\.natvis
+a = 0n10
+ "#)]
+fn test() {
+ let a = 10;
+ __break();
+}
+```
+
+The `#[debugger_test]` proc macro attribute has 3 required meta items which all take a string value:
+
+1. debugger
+2. commands
+3. expected_statements
+
+The `debugger` meta item expects the name of a supported debugger. Currently the only supported debugger is `cdb`.
+This crate will try to find the specified debugger, first by testing if it is on the `PATH`. If the debugger is
+not found, this crate will search the default installation directory for the debugger. Specifying an exact path
+for which debugger to use is not currently supported.
+
+The `commands` meta item expects a string of a debugger command to run. To run multiple commands, separate each
+command by the new line character (`\n`).
+
+The `expected_statements` meta item expects a string of output to verify in the debugger output.
+Each statement should be separated by a new line character (`\n`).
+
+For example:
+
+```rust
+#[debugger_test(
+ debugger = "cdb",
+ commands = "command1\ncommand2\ncommand3",
+ expected_statements = "statement1\nstatement2\nstatement3")]
+```
+
+Using a multiline string is also supported:
+
+```rust
+#[debugger_test(
+ debugger = "cdb",
+ commands = r#"
+command1
+command2
+command3"#,
+ expected_statements = r#"
+statement1
+statement2
+statement3"#)]
+```
+
+Pattern matching is also supported for a given `expected_statement`. Use the prefix, `pattern:` for the
+expected statement. This is useful for ignoring debugger output that contain memory address and/or paths:
+
+```rust
+#[debugger_test(
+ debugger = "cdb",
+ commands = "command3",
+ expected_statements = "pattern:abc.*")]
+```
+
+The `#[debugger_test]` proc macro attribute will generate a new test function that will be marked
+with the `#[test]` attribute. This generated test function will add a suffix to the test name to ensure
+the test is unique. In the example above, the proc macro attribute will generate the following function:
+
+```rust
+#[test]
+fn test__cdb() {
+ .....
+ test();
+ .....
+}
+```
+
+The proc macro attribute will generate a test function that will do the following:
+
+1. Launch the specified debugger
+2. Attach the debugger to the current test executable process
+3. Set breakpoints at all call sites of the `__break()` function
+4. Run the debugger to the first breakpoint specified by the debugger
+5. Run all of the user specified commands and exit the debugger
+6. Parse the debugger output using the `debugger_test_parser` crate and verify all the `expected_statements` were found
+
+Based on the debugger specified via the `#[debugger_test]` attribute, the path used to launch the debugger will
+be one of the following:
+
+1. If the environment variable, _debugger_type_ _DEBUGGER_DIR is set, i.e. `CDB_DEBUGGER_DIR`, the proc macro attribute will try to launch the debugger from this directory
+2. The default installation directory for the given debugger if it exists at that path
+3. Invoking the executable directly, i.e. `cdb` or `cdb.exe` depending on the OS
+
+## Contributing
+
+This project welcomes contributions and suggestions. Most contributions require you to agree to a
+Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
+the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
+
+When you submit a pull request, a CLA bot will automatically determine whether you need to provide
+a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
+provided by the bot. You will only need to do this once across all repos using our CLA.
+
+This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
+For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
+contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
+
+## Trademarks
+
+This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft
+trademarks or logos is subject to and must follow
+[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general).
+Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.
+Any use of third-party trademarks or logos are subject to those third-party's policies.
--- /dev/null
+<!-- BEGIN MICROSOFT SECURITY.MD V0.0.7 BLOCK -->
+
+## Security
+
+Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
+
+If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below.
+
+## Reporting Security Issues
+
+**Please do not report security vulnerabilities through public GitHub issues.**
+
+Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report).
+
+If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey).
+
+You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc).
+
+Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
+
+ * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
+ * Full paths of source file(s) related to the manifestation of the issue
+ * The location of the affected source code (tag/branch/commit or direct URL)
+ * Any special configuration required to reproduce the issue
+ * Step-by-step instructions to reproduce the issue
+ * Proof-of-concept or exploit code (if possible)
+ * Impact of the issue, including how an attacker might exploit the issue
+
+This information will help us triage your report more quickly.
+
+If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs.
+
+## Preferred Languages
+
+We prefer all communications to be in English.
+
+## Policy
+
+Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd).
+
+<!-- END MICROSOFT SECURITY.MD BLOCK -->
--- /dev/null
+# TODO: The maintainer of this repo has not yet edited this file\r
+\r
+**REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project?\r
+\r
+- **No CSS support:** Fill out this template with information about how to file issues and get help.\r
+- **Yes CSS support:** Fill out an intake form at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). CSS will work with/help you to determine next steps.\r
+- **Not sure?** Fill out an intake as though the answer were "Yes". CSS will help you decide.\r
+\r
+*Then remove this first heading from this SUPPORT.MD file before publishing your repo.*\r
+\r
+# Support\r
+\r
+## How to file issues and get help \r
+\r
+This project uses GitHub Issues to track bugs and feature requests. Please search the existing \r
+issues before filing new issues to avoid duplicates. For new issues, file your bug or \r
+feature request as a new Issue.\r
+\r
+For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE \r
+FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER\r
+CHANNEL. WHERE WILL YOU HELP PEOPLE?**.\r
+\r
+## Microsoft Support Policy \r
+\r
+Support for this **PROJECT or PRODUCT** is limited to the resources listed above.\r
--- /dev/null
+use std::env;
+use std::ffi::OsString;
+use std::fmt::Display;
+use std::path::PathBuf;
+use std::str::FromStr;
+
+#[cfg(windows)]
+pub static EXECUTABLE_EXTENSION: &str = ".exe";
+#[cfg(not(windows))]
+pub static EXECUTABLE_EXTENSION: &str = "";
+
+#[derive(Debug)]
+pub enum DebuggerType {
+ Cdb,
+}
+
+impl Display for DebuggerType {
+ fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
+ let debugger_type = match self {
+ DebuggerType::Cdb => "cdb",
+ };
+ write!(fmt, "{}", debugger_type)
+ }
+}
+
+impl FromStr for DebuggerType {
+ type Err = anyhow::Error;
+
+ /// Attempts to parse a string into a DebuggerType
+ fn from_str(s: &str) -> Result<DebuggerType, Self::Err> {
+ let debugger = s.to_lowercase();
+ match debugger.as_str() {
+ "cdb" => Ok(DebuggerType::Cdb),
+ _ => anyhow::bail!("Invalid debugger type option: `{}`.", s),
+ }
+ }
+}
+
+/// Find the CDB debugger by searching its default installation directory.
+fn find_cdb() -> Option<OsString> {
+ // Inspired by https://github.com/rust-lang/rust/blob/1.62.0/src/tools/compiletest/src/main.rs#L821
+ let pf86 = env::var_os("ProgramFiles(x86)").or_else(|| env::var_os("ProgramFiles"))?;
+
+ let cdb_arch = if cfg!(target_arch = "x86") {
+ "x86"
+ } else if cfg!(target_arch = "x86_64") {
+ "x64"
+ } else if cfg!(target_arch = "aarch64") {
+ "arm64"
+ } else if cfg!(target_arch = "arm") {
+ "arm"
+ } else {
+ return None; // No compatible cdb.exe in the Windows 10 SDK
+ };
+
+ let mut path = PathBuf::new();
+ path.push(pf86);
+ path.push(r"Windows Kits\10\Debuggers");
+ path.push(cdb_arch);
+ path.push("cdb.exe");
+
+ if !path.exists() {
+ return None;
+ }
+
+ Some(path.into_os_string())
+}
+
+/// Get the debugger specified by the debugger_type parameter.
+pub fn get_debugger(debugger_type: &DebuggerType) -> PathBuf {
+ let debugger_executable = OsString::from(format!("{}{}", debugger_type, EXECUTABLE_EXTENSION));
+
+ let debugger_env_dir = match debugger_type {
+ DebuggerType::Cdb => env::var_os("CDB_DEBUGGER_DIR"),
+ };
+
+ // First check to see if the %debugger_type%_DEBUGGER_DIR environment variable is set.
+ // If set, use this directory for all debugger invocations.
+ // If not set, fallback to the default installation directory.
+ // If the debugger is not found there, fallback to the current path.
+ let debugger_executable_path = if let Some(debugger_env_path) = debugger_env_dir {
+ PathBuf::from(debugger_env_path).join(debugger_executable)
+ } else {
+ match debugger_type {
+ DebuggerType::Cdb => PathBuf::from(find_cdb().unwrap_or(debugger_executable)),
+ }
+ };
+
+ debugger_executable_path
+}
+
+#[test]
+#[cfg_attr(
+ not(target_os = "windows"),
+ ignore = "test only runs on windows platforms."
+)]
+fn test_find_cdb() {
+ let result = find_cdb();
+ assert!(result.is_some());
+
+ let cdb = result.unwrap();
+ let cdb_path = std::path::PathBuf::from(cdb.to_string_lossy().to_string());
+ assert!(cdb_path.file_name().is_some());
+
+ let cdb_exe = cdb_path.file_name().unwrap();
+ assert_eq!("cdb.exe", cdb_exe);
+}
+
+#[test]
+fn test_get_debugger() {
+ let debugger_type = DebuggerType::Cdb;
+ let cdb_executable = format!("cdb{}", EXECUTABLE_EXTENSION);
+
+ // Test setting the environment variable to find the debugger
+ let cdb_debugger_dir = "debugger_path/debugger";
+ env::set_var("CDB_DEBUGGER_DIR", cdb_debugger_dir);
+ assert!(env::var_os("CDB_DEBUGGER_DIR").unwrap() == OsString::from("debugger_path/debugger"));
+
+ let mut debugger_path = get_debugger(&debugger_type);
+ let expected_path = PathBuf::from(cdb_debugger_dir).join(&cdb_executable);
+ assert_eq!(expected_path, debugger_path);
+ env::remove_var("CDB_DEBUGGER_DIR");
+
+ debugger_path = get_debugger(&debugger_type);
+ assert_eq!(
+ cdb_executable,
+ debugger_path
+ .file_name()
+ .unwrap()
+ .to_string_lossy()
+ .to_string()
+ );
+}
+
+#[test]
+fn test_debugger_type_from_str() {
+ assert!(DebuggerType::from_str("cdb").is_ok());
+
+ let gdb_debugger_type = DebuggerType::from_str("gdb");
+ assert!(gdb_debugger_type.is_err());
+ assert_eq!(
+ "Invalid debugger type option: `gdb`.",
+ format!("{}", gdb_debugger_type.unwrap_err())
+ );
+
+ let mock_debugger_debugger_type = DebuggerType::from_str("mock debugger");
+ assert!(mock_debugger_debugger_type.is_err());
+ assert_eq!(
+ "Invalid debugger type option: `mock debugger`.",
+ format!("{}", mock_debugger_debugger_type.unwrap_err())
+ );
+}
--- /dev/null
+pub fn create_debugger_script(fn_name: &String, debugger_commands: &Vec<&str>) -> String {
+ let mut debugger_script = String::new();
+
+ // Add an inital breakpoint for the test function.
+ // Also add a breakpoint at the end of the test function which quits the debugger.
+ debugger_script.push_str(format!("bm *!*::{} \"bp /1 @$ra \\\"qd\\\" \"\n", fn_name).as_str());
+
+ // Add the user specified breakpoints.
+ debugger_script.push_str("bm *!*::__break \"gu\"\n");
+
+ // Run the debugger to the start of the test.
+ debugger_script.push_str("g\n");
+ debugger_script.push_str("bl\n");
+
+ // Run the debugger to the first user set breakpoint.
+ debugger_script.push_str("g\n");
+
+ for (i, debugger_comamand) in debugger_commands.iter().enumerate() {
+ debugger_script.push_str(format!(".echo start_debugger_command_{}\n", i).as_str());
+ debugger_script.push_str(format!("{}\n", debugger_comamand).as_str());
+ debugger_script.push_str(format!(".echo end_debugger_command_{}\n", i).as_str());
+ }
+
+ // Quit and detach the debugger
+ debugger_script.push_str("qd\n");
+
+ debugger_script
+}
+
+#[test]
+fn test_debugger_script_empty() {
+ let test_name = String::from("test1");
+ let debugger_commands = vec![];
+ let debugger_script = create_debugger_script(&test_name, &debugger_commands);
+ let expected = r#"bm *!*::test1 "bp /1 @$ra \"qd\" "
+bm *!*::__break "gu"
+g
+bl
+g
+qd
+"#;
+
+ assert_eq!(expected.to_string(), debugger_script);
+}
+
+#[test]
+fn test_debugger_script() {
+ let test_name = String::from("test1");
+ let debugger_commands = vec!["dv", "g", ".nvlist"];
+ let debugger_script = create_debugger_script(&test_name, &debugger_commands);
+ let expected = r#"bm *!*::test1 "bp /1 @$ra \"qd\" "
+bm *!*::__break "gu"
+g
+bl
+g
+.echo start_debugger_command_0
+dv
+.echo end_debugger_command_0
+.echo start_debugger_command_1
+g
+.echo end_debugger_command_1
+.echo start_debugger_command_2
+.nvlist
+.echo end_debugger_command_2
+qd
+"#;
+
+ assert_eq!(expected.to_string(), debugger_script);
+}
--- /dev/null
+mod debugger;
+mod debugger_script;
+
+use std::str::FromStr;
+
+use debugger::DebuggerType;
+use proc_macro::TokenStream;
+use quote::{format_ident, quote, ToTokens};
+use syn::{parse::Parse, Token};
+
+use crate::debugger_script::create_debugger_script;
+
+struct DebuggerTest {
+ debugger: String,
+ commands: String,
+ expected_statements: String,
+}
+
+impl Parse for DebuggerTest {
+ fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+ let debugger_meta = input.parse::<syn::MetaNameValue>()?;
+ let debugger = if debugger_meta.path.is_ident("debugger") {
+ match debugger_meta.lit {
+ syn::Lit::Str(lit_str) => lit_str.value(),
+ _ => {
+ return Err(input.error("Expected a literal string for the value of `debugger`"))
+ }
+ }
+ } else {
+ return Err(input.error("Expected value `debugger`"));
+ };
+
+ input.parse::<Token![,]>()?;
+
+ let commands_meta = input.parse::<syn::MetaNameValue>()?;
+ let commands = if commands_meta.path.is_ident("commands") {
+ match commands_meta.lit {
+ syn::Lit::Str(lit_str) => lit_str.value(),
+ _ => {
+ return Err(input.error("Expected a literal string for the value of `commands`"))
+ }
+ }
+ } else {
+ return Err(input.error("Expected value `commands`"));
+ };
+
+ input.parse::<Token![,]>()?;
+
+ let expected_statements_meta = input.parse::<syn::MetaNameValue>()?;
+ let expected_statements = if expected_statements_meta
+ .path
+ .is_ident("expected_statements")
+ {
+ match expected_statements_meta.lit {
+ syn::Lit::Str(lit_str) => lit_str.value(),
+ _ => {
+ return Err(input
+ .error("Expected a literal string for the value of `expected_statements`"))
+ }
+ }
+ } else {
+ return Err(input.error("Expected value `expected_statements`"));
+ };
+
+ Ok(DebuggerTest {
+ debugger,
+ commands,
+ expected_statements,
+ })
+ }
+}
+
+#[proc_macro_attribute]
+pub fn debugger_test(attr: TokenStream, item: TokenStream) -> TokenStream {
+ let invoc = match syn::parse::<DebuggerTest>(attr) {
+ Ok(s) => s,
+ Err(e) => return e.to_compile_error().into(),
+ };
+
+ let item = match syn::parse::<syn::Item>(item) {
+ Ok(s) => s,
+ Err(e) => return e.to_compile_error().into(),
+ };
+
+ let func = match item {
+ syn::Item::Fn(ref f) => f,
+ _ => panic!("must be attached to a function"),
+ };
+
+ let debugger_commands = &invoc
+ .commands
+ .trim()
+ .lines()
+ .into_iter()
+ .map(|line| line.trim())
+ .collect::<Vec<&str>>();
+
+ let debugger_type = DebuggerType::from_str(invoc.debugger.as_str()).expect(
+ format!(
+ "debugger `{}` must be a valid debugger option.",
+ invoc.debugger.as_str()
+ )
+ .as_str(),
+ );
+ let debugger_executable_path = debugger::get_debugger(&debugger_type);
+
+ let fn_name = func.sig.ident.to_string();
+ let fn_ident = format_ident!("{}", fn_name);
+ let test_fn_name = format!("{}__{}", fn_name, debugger_type.to_string());
+ let test_fn_ident = format_ident!("{}", test_fn_name);
+
+ let debugger_script_contents = create_debugger_script(&fn_name, debugger_commands);
+
+ // Trim all whitespace and remove any empty lines.
+ let expected_statements = &invoc
+ .expected_statements
+ .trim()
+ .lines()
+ .collect::<Vec<&str>>();
+
+ // Create the cli for the given debugger.
+ let (debugger_command_line, cfg_attr) = match debugger_type {
+ DebuggerType::Cdb => {
+ let debugger_path = debugger_executable_path.to_string_lossy().to_string();
+ let command_line = quote!(
+ match std::process::Command::new(#debugger_path)
+ .stdout(std::process::Stdio::from(debugger_stdout_file))
+ .stderr(std::process::Stdio::from(debugger_stderr_file))
+ .arg("-pd")
+ .arg("-p")
+ .arg(pid.to_string())
+ .arg("-cf")
+ .arg(&debugger_script_path)
+ .spawn() {
+ Ok(child) => child,
+ Err(error) => {
+ return Err(std::boxed::Box::from(format!("Failed to launch CDB: {}\n", error.to_string())));
+ }
+ }
+ );
+
+ // cdb is only supported on Windows.
+ let cfg_attr = quote!(
+ #[cfg_attr(not(target_os = "windows"), ignore = "test only runs on windows platforms.")]
+ );
+
+ (command_line, cfg_attr)
+ }
+ };
+
+ // Create the test function that will launch the debugger and run debugger commands.
+ let mut debugger_test_fn = proc_macro::TokenStream::from(quote!(
+ #[test]
+ #cfg_attr
+ fn #test_fn_ident() -> std::result::Result<(), Box<dyn std::error::Error>> {
+ use std::io::Read;
+ use std::io::Write;
+
+ let pid = std::process::id();
+ let current_exe_filename = std::env::current_exe()?.file_stem().expect("must have a valid file name").to_string_lossy().to_string();
+
+ // Create a temporary file to store the debugger script to run.
+ let debugger_script_filename = format!("{}_{}.debugger_script", current_exe_filename, #test_fn_name);
+ let debugger_script_path = std::env::temp_dir().join(debugger_script_filename);
+
+ // Write the contents of the debugger script to a new file.
+ let mut debugger_script = std::fs::File::create(&debugger_script_path)?;
+ writeln!(debugger_script, #debugger_script_contents)?;
+
+ // Create a temporary file to store the stdout and stderr from the debugger output.
+ let debugger_stdout_path = debugger_script_path.with_extension("debugger_out");
+ let debugger_stderr_path = debugger_script_path.with_extension("debugger_err");
+
+ let debugger_stdout_file = std::fs::File::create(&debugger_stdout_path)?;
+ let debugger_stderr_file = std::fs::File::create(&debugger_stderr_path)?;
+
+ // Start the debugger and run the debugger commands.
+ let mut child = #debugger_command_line;
+
+ // Wait for the debugger to launch
+ // On Windows, use the IsDebuggerPresent API to check if a debugger is present
+ // for the current process. https://docs.microsoft.com/en-us/windows/win32/api/debugapi/nf-debugapi-isdebuggerpresent
+ #[cfg(windows)]
+ extern "stdcall" {
+ fn IsDebuggerPresent() -> i32;
+ };
+ #[cfg(windows)]
+ unsafe {
+ while IsDebuggerPresent() == 0 {
+ std::thread::sleep(std::time::Duration::from_secs(1));
+ }
+ }
+
+ // Wait 3 seconds to ensure the debugger is in control of the process.
+ std::thread::sleep(std::time::Duration::from_secs(3));
+
+ // Call the test function.
+ #fn_ident();
+
+ // Wait for the debugger to exit.
+ std::thread::sleep(std::time::Duration::from_secs(3));
+
+ // If debugger has not already quit, force quit the debugger.
+ let mut debugger_stdout = String::new();
+ match child.try_wait()? {
+ Some(status) => {
+ // Bail early if the debugger process didn't execute successfully.
+ let mut debugger_stdout_file = std::fs::File::open(&debugger_stdout_path)?;
+ debugger_stdout_file.read_to_string(&mut debugger_stdout)?;
+
+ if !status.success() {
+ let mut debugger_stderr = String::new();
+ let mut debugger_stderr_file = std::fs::File::open(&debugger_stderr_path)?;
+ debugger_stderr_file.read_to_string(&mut debugger_stderr)?;
+ return Err(std::boxed::Box::from(format!("Debugger failed with {}.\n{}\n{}\n", status, debugger_stderr, debugger_stdout)));
+ }
+
+ println!("Debugger stdout:\n{}\n", &debugger_stdout);
+ },
+ None => {
+ // Force kill the debugger process if it has not exited yet.
+ println!("killing debugger process.");
+ child.kill().expect("debugger has been running for too long");
+
+ let mut debugger_stdout_file = std::fs::File::open(&debugger_stdout_path)?;
+ debugger_stdout_file.read_to_string(&mut debugger_stdout)?;
+ println!("Debugger stdout:\n{}\n", &debugger_stdout);
+ }
+ }
+
+ // Verify the expected contents of the debugger output.
+ let expected_statements = vec![#(#expected_statements),*];
+ debugger_test_parser::parse(debugger_stdout, expected_statements)?;
+
+ #[cfg(windows)]
+ unsafe {
+ while IsDebuggerPresent() == 1 {
+ std::thread::sleep(std::time::Duration::from_secs(1));
+ }
+ }
+
+ #[cfg(not(windows))]
+ std::thread::sleep(std::time::Duration::from_secs(3));
+
+ Ok(())
+ }
+ ));
+
+ debugger_test_fn.extend(proc_macro::TokenStream::from(item.to_token_stream()).into_iter());
+ debugger_test_fn
+}
--- /dev/null
+use debugger_test::debugger_test;
+
+#[inline(never)]
+fn __break() {}
+
+#[debugger_test(debugger = "cdb", commands = "", expected_statements = "")]
+fn test_empty_commands() {
+ __break();
+}
+
+#[debugger_test(debugger = "cdb", commands = ".nvlist", expected_statements = "")]
+fn test_no_expectations() {}
+
+#[debugger_test(
+ debugger = "cdb",
+ commands = r#"
+dv
+dx a
+g
+dv
+dx a
+g
+dv
+dx b
+g
+dv
+dx b"#,
+ expected_statements = r#"
+a = 0n0
+a = 0n5
+b = 0n25
+a = 0n5
+b = 0n10"#
+)]
+fn test_commands_with_expectations() {
+ let mut a = 0;
+ __break();
+
+ a += 5;
+ assert_eq!(a, 5);
+ __break();
+
+ let mut b = 25;
+ __break();
+
+ b -= 15;
+ assert_eq!(b, 10);
+ __break();
+}