Import debugger_test 0.1.5 upstream upstream/0.1.5
authorRoy7Kim <myoungwoon.kim@samsung.com>
Thu, 4 May 2023 05:46:13 +0000 (14:46 +0900)
committerRoy7Kim <myoungwoon.kim@samsung.com>
Thu, 4 May 2023 05:46:13 +0000 (14:46 +0900)
13 files changed:
.cargo_vcs_info.json [new file with mode: 0644]
.gitignore [new file with mode: 0644]
CODE_OF_CONDUCT.md [new file with mode: 0644]
Cargo.toml [new file with mode: 0644]
Cargo.toml.orig [new file with mode: 0644]
LICENSE [new file with mode: 0644]
README.md [new file with mode: 0644]
SECURITY.md [new file with mode: 0644]
SUPPORT.md [new file with mode: 0644]
src/debugger.rs [new file with mode: 0644]
src/debugger_script.rs [new file with mode: 0644]
src/lib.rs [new file with mode: 0644]
tests/test.rs [new file with mode: 0644]

diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json
new file mode 100644 (file)
index 0000000..0ba13a7
--- /dev/null
@@ -0,0 +1,6 @@
+{
+  "git": {
+    "sha1": "5a484e7d207de70b4730bc6043661787d4471a99"
+  },
+  "path_in_vcs": ""
+}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..30ecd0d
--- /dev/null
@@ -0,0 +1,23 @@
+# 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
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644 (file)
index 0000000..f9ba8cf
--- /dev/null
@@ -0,0 +1,9 @@
+# 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
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644 (file)
index 0000000..50f433e
--- /dev/null
@@ -0,0 +1,52 @@
+# 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"
diff --git a/Cargo.toml.orig b/Cargo.toml.orig
new file mode 100644 (file)
index 0000000..1715fc7
--- /dev/null
@@ -0,0 +1,27 @@
+[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"
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..9e841e7
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+    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
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..40290e9
--- /dev/null
+++ b/README.md
@@ -0,0 +1,139 @@
+# 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.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644 (file)
index 0000000..869fdfe
--- /dev/null
@@ -0,0 +1,41 @@
+<!-- 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 -->
diff --git a/SUPPORT.md b/SUPPORT.md
new file mode 100644 (file)
index 0000000..291d4d4
--- /dev/null
@@ -0,0 +1,25 @@
+# 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
diff --git a/src/debugger.rs b/src/debugger.rs
new file mode 100644 (file)
index 0000000..8ad683f
--- /dev/null
@@ -0,0 +1,152 @@
+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())
+    );
+}
diff --git a/src/debugger_script.rs b/src/debugger_script.rs
new file mode 100644 (file)
index 0000000..49ab9b5
--- /dev/null
@@ -0,0 +1,69 @@
+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);
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644 (file)
index 0000000..037f341
--- /dev/null
@@ -0,0 +1,251 @@
+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
+}
diff --git a/tests/test.rs b/tests/test.rs
new file mode 100644 (file)
index 0000000..38caca2
--- /dev/null
@@ -0,0 +1,49 @@
+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();
+}