From 6c5fa9881281b5f049f4c3ef61b7072a75fb9529 Mon Sep 17 00:00:00 2001 From: Roy7Kim Date: Thu, 4 May 2023 14:46:13 +0900 Subject: [PATCH] Import debugger_test 0.1.5 --- .cargo_vcs_info.json | 6 + .gitignore | 23 ++++ CODE_OF_CONDUCT.md | 9 ++ Cargo.toml | 52 +++++++++ Cargo.toml.orig | 27 +++++ LICENSE | 21 ++++ README.md | 139 +++++++++++++++++++++++ SECURITY.md | 41 +++++++ SUPPORT.md | 25 ++++ src/debugger.rs | 152 +++++++++++++++++++++++++ src/debugger_script.rs | 69 +++++++++++ src/lib.rs | 251 +++++++++++++++++++++++++++++++++++++++++ tests/test.rs | 49 ++++++++ 13 files changed, 864 insertions(+) create mode 100644 .cargo_vcs_info.json create mode 100644 .gitignore create mode 100644 CODE_OF_CONDUCT.md create mode 100644 Cargo.toml create mode 100644 Cargo.toml.orig create mode 100644 LICENSE create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 SUPPORT.md create mode 100644 src/debugger.rs create mode 100644 src/debugger_script.rs create mode 100644 src/lib.rs create mode 100644 tests/test.rs diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json new file mode 100644 index 0000000..0ba13a7 --- /dev/null +++ b/.cargo_vcs_info.json @@ -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 index 0000000..30ecd0d --- /dev/null +++ b/.gitignore @@ -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 index 0000000..f9ba8cf --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -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 index 0000000..50f433e --- /dev/null +++ b/Cargo.toml @@ -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 index 0000000..1715fc7 --- /dev/null +++ b/Cargo.toml.orig @@ -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 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 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 index 0000000..869fdfe --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,41 @@ + + +## 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). + + diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 0000000..291d4d4 --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,25 @@ +# TODO: The maintainer of this repo has not yet edited this file + +**REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project? + +- **No CSS support:** Fill out this template with information about how to file issues and get help. +- **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. +- **Not sure?** Fill out an intake as though the answer were "Yes". CSS will help you decide. + +*Then remove this first heading from this SUPPORT.MD file before publishing your repo.* + +# Support + +## How to file issues and get help + +This project uses GitHub Issues to track bugs and feature requests. Please search the existing +issues before filing new issues to avoid duplicates. For new issues, file your bug or +feature request as a new Issue. + +For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE +FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER +CHANNEL. WHERE WILL YOU HELP PEOPLE?**. + +## Microsoft Support Policy + +Support for this **PROJECT or PRODUCT** is limited to the resources listed above. diff --git a/src/debugger.rs b/src/debugger.rs new file mode 100644 index 0000000..8ad683f --- /dev/null +++ b/src/debugger.rs @@ -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 { + 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 { + // 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 index 0000000..49ab9b5 --- /dev/null +++ b/src/debugger_script.rs @@ -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 index 0000000..037f341 --- /dev/null +++ b/src/lib.rs @@ -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 { + let debugger_meta = input.parse::()?; + 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::()?; + + let commands_meta = input.parse::()?; + 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::()?; + + let expected_statements_meta = input.parse::()?; + 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::(attr) { + Ok(s) => s, + Err(e) => return e.to_compile_error().into(), + }; + + let item = match syn::parse::(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::>(); + + 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::>(); + + // 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> { + 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 index 0000000..38caca2 --- /dev/null +++ b/tests/test.rs @@ -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(); +} -- 2.34.1