diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b20a3e35..fc56c5cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,9 +12,9 @@ env: jobs: tests: - runs-on: ubuntu-latest strategy: matrix: + os: [ubuntu-latest] include: - rust: 1.56.0 # MSRV features: @@ -30,6 +30,13 @@ jobs: features: - rust: nightly bench: test build benchmarks + - rust: nightly + features: debugger_visualizer + - rust: nightly + features: debugger_visualizer + os: windows-latest + + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 @@ -46,6 +53,10 @@ jobs: if: matrix.features == 'serde' run: | cargo test --verbose -p test-serde + - name: Tests (debugger_visualizer) + if: matrix.features == 'debugger_visualizer' + run: | + cargo test --verbose --test debugger_visualizer --features "${{ matrix.features }}" -- --test-threads=1 - name: Test run benchmarks if: matrix.bench != '' run: cargo test -v --benches diff --git a/Cargo.toml b/Cargo.toml index 2037f0b2..323515d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,8 @@ fnv = "1.0" lazy_static = "1.3" fxhash = "0.2.1" serde_derive = "1.0" +debugger_test = "0.1" +debugger_test_parser = "0.1" [features] default = ["std"] @@ -43,6 +45,10 @@ std = [] # for testing only, of course test_debug = [] +# UNSTABLE FEATURES (requires Rust nightly) +# Enable to use the #[debugger_visualizer] attribute. +debugger_visualizer = [] + [profile.bench] debug = true @@ -55,3 +61,9 @@ features = ["serde", "rayon"] [workspace] members = ["test-nostd", "test-serde"] + +[[test]] +path = "tests/debugger_visualizer.rs" +name = "debugger_visualizer" +required-features = ["debugger_visualizer"] +test = false diff --git a/debug_metadata/README.md b/debug_metadata/README.md new file mode 100644 index 00000000..d11caf63 --- /dev/null +++ b/debug_metadata/README.md @@ -0,0 +1,111 @@ +## Debugger Visualizers + +Many languages and debuggers enable developers to control how a type is +displayed in a debugger. These are called "debugger visualizations" or "debugger +views". + +The Windows debuggers (WinDbg\CDB) support defining custom debugger visualizations using +the `Natvis` framework. To use Natvis, developers write XML documents using the natvis +schema that describe how debugger types should be displayed with the `.natvis` extension. +(See: https://docs.microsoft.com/en-us/visualstudio/debugger/create-custom-views-of-native-objects?view=vs-2019) +The Natvis files provide patterns which match type names a description of how to display +those types. + +The Natvis schema can be found either online (See: https://code.visualstudio.com/docs/cpp/natvis#_schema) +or locally at `\Xml\Schemas\1033\natvis.xsd`. + +The GNU debugger (GDB) supports defining custom debugger views using Pretty Printers. +Pretty printers are written as python scripts that describe how a type should be displayed +when loaded up in GDB/LLDB. (See: https://sourceware.org/gdb/onlinedocs/gdb/Pretty-Printing.html#Pretty-Printing) +The pretty printers provide patterns, which match type names, and for matching +types, descibe how to display those types. (For writing a pretty printer, see: https://sourceware.org/gdb/onlinedocs/gdb/Writing-a-Pretty_002dPrinter.html#Writing-a-Pretty_002dPrinter). + +### Embedding Visualizers + +Through the use of the currently unstable `#[debugger_visualizer]` attribute, the `indexmap` +crate can embed debugger visualizers into the crate metadata. + +Currently the two types of visualizers supported are Natvis and Pretty printers. + +For Natvis files, when linking an executable with a crate that includes Natvis files, +the MSVC linker will embed the contents of all Natvis files into the generated `PDB`. + +For pretty printers, the compiler will encode the contents of the pretty printer +in the `.debug_gdb_scripts` section of the `ELF` generated. + +### Testing Visualizers + +The `indexmap` crate supports testing debugger visualizers defined for this crate. The entry point for +these tests are `tests/debugger_visualizer.rs`. These tests are defined using the `debugger_test` and +`debugger_test_parser` crates. The `debugger_test` crate is a proc macro crate which defines a +single proc macro attribute, `#[debugger_test]`. For more detailed information about this crate, +see https://crates.io/crates/debugger_test. The CI pipeline for the `indexmap` crate has been updated +to run the debugger visualizer tests to ensure debugger visualizers do not become broken/stale. + +The `#[debugger_test]` proc macro attribute may only be used on test functions and will run the +function under the debugger specified by the `debugger` meta item. + +This proc macro attribute has 3 required values: + +1. The first required meta item, `debugger`, takes a string value which specifies the debugger to launch. +2. The second required meta item, `commands`, takes a string of new line (`\n`) separated list of debugger +commands to run. +3. The third required meta item, `expected_statements`, takes a string of new line (`\n`) separated list of +statements that must exist in the debugger output. Pattern matching through regular expressions is also +supported by using the `pattern:` prefix for each expected statement. + +#### Example: + +```rust +#[debugger_test( + debugger = "cdb", + commands = "command1\ncommand2\ncommand3", + expected_statements = "statement1\nstatement2\nstatement3")] +fn test() { + +} +``` + +Using a multiline string is also supported, with a single debugger command/expected statement per line: + +```rust +#[debugger_test( + debugger = "cdb", + commands = " +command1 +command2 +command3", + expected_statements = " +statement1 +pattern:statement[0-9]+ +statement3")] +fn test() { + +} +``` + +In the example above, the second expected statement uses pattern matching through a regular expression +by using the `pattern:` prefix. + +#### Testing Locally + +Currently, only Natvis visualizations have been defined for the `indexmap` crate via `debug_metadata/indexmap.natvis`, +which means the `tests/debugger_visualizer.rs` tests need to be run on Windows using the `*-pc-windows-msvc` targets. +To run these tests locally, first ensure the debugging tools for Windows are installed or install them following +the steps listed here, [Debugging Tools for Windows](https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/). +Once the debugging tools have been installed, the tests can be run in the same manner as they are in the CI +pipeline. + +#### Note + +When running the debugger visualizer tests, `tests/debugger_visualizer.rs`, they need to be run consecutively +and not in parallel. This can be achieved by passing the flag `--test-threads=1` to rustc. This is due to +how the debugger tests are run. Each test marked with the `#[debugger_test]` attribute launches a debugger +and attaches it to the current test process. If tests are running in parallel, the test will try to attach +a debugger to the current process which may already have a debugger attached causing the test to fail. + +For example: + +``` +cargo test --test debugger_visualizer --features debugger_visualizer -- --test-threads=1 +``` diff --git a/debug_metadata/indexmap.natvis b/debug_metadata/indexmap.natvis new file mode 100644 index 00000000..463553c3 --- /dev/null +++ b/debug_metadata/indexmap.natvis @@ -0,0 +1,29 @@ + + + {{ len={map.core.entries.len} }} + + map + + + + {{ len={core.entries.len} }} + + core.entries + + + + {key} + + key + hash.__0,d + + + + {{ key={key}, value={value} }} + + key + value + hash.__0,d + + + diff --git a/src/lib.rs b/src/lib.rs index 3d796ea7..e45e44f7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,11 @@ #![warn(rust_2018_idioms)] #![doc(html_root_url = "https://docs.rs/indexmap/1/")] #![no_std] +#![cfg_attr( + feature = "debugger_visualizer", + feature(debugger_visualizer), + debugger_visualizer(natvis_file = "../debug_metadata/indexmap.natvis") +)] //! [`IndexMap`] is a hash table where the iteration order of the key-value //! pairs is independent of the hash values of the keys. diff --git a/tests/debugger_visualizer.rs b/tests/debugger_visualizer.rs new file mode 100644 index 00000000..01b797bc --- /dev/null +++ b/tests/debugger_visualizer.rs @@ -0,0 +1,89 @@ +use debugger_test::debugger_test; +use indexmap::IndexMap; +use indexmap::IndexSet; + +#[inline(never)] +fn __break() {} + +#[debugger_test( + debugger = "cdb", + commands = r#" +.nvlist +dx set +dx map +"#, + expected_statements = r#" +set : { len=0x19 } [Type: indexmap::set::IndexSet] + [len] : 0x19 [Type: unsigned __int64] + [capacity] : 0x1c [Type: unsigned __int64] + [0] : 0x74 't' [Type: indexmap::Bucket >] + [1] : 0x68 'h' [Type: indexmap::Bucket >] + [2] : 0x65 'e' [Type: indexmap::Bucket >] + [3] : 0x20 ' ' [Type: indexmap::Bucket >] + [4] : 0x71 'q' [Type: indexmap::Bucket >] + [5] : 0x75 'u' [Type: indexmap::Bucket >] + [6] : 0x69 'i' [Type: indexmap::Bucket >] + [7] : 0x63 'c' [Type: indexmap::Bucket >] + [8] : 0x6b 'k' [Type: indexmap::Bucket >] + [9] : 0x72 'r' [Type: indexmap::Bucket >] + [10] : 0x6f 'o' [Type: indexmap::Bucket >] + [11] : 0x77 'w' [Type: indexmap::Bucket >] + [12] : 0x6e 'n' [Type: indexmap::Bucket >] + [13] : 0x66 'f' [Type: indexmap::Bucket >] + [14] : 0x78 'x' [Type: indexmap::Bucket >] + [15] : 0x6a 'j' [Type: indexmap::Bucket >] + [16] : 0x6d 'm' [Type: indexmap::Bucket >] + [17] : 0x70 'p' [Type: indexmap::Bucket >] + [18] : 0x64 'd' [Type: indexmap::Bucket >] + [19] : 0x76 'v' [Type: indexmap::Bucket >] + [20] : 0x6c 'l' [Type: indexmap::Bucket >] + [21] : 0x61 'a' [Type: indexmap::Bucket >] + [22] : 0x7a 'z' [Type: indexmap::Bucket >] + [23] : 0x79 'y' [Type: indexmap::Bucket >] + [24] : 0x67 'g' [Type: indexmap::Bucket >] + +map : { len=0x19 } [Type: indexmap::map::IndexMap] + [len] : 0x19 [Type: unsigned __int64] + [capacity] : 0x1c [Type: unsigned __int64] + [0] : { key=0x74 't', value=2 } [Type: indexmap::Bucket] + [1] : { key=0x68 'h', value=2 } [Type: indexmap::Bucket] + [2] : { key=0x65 'e', value=4 } [Type: indexmap::Bucket] + [3] : { key=0x20 ' ', value=8 } [Type: indexmap::Bucket] + [4] : { key=0x71 'q', value=1 } [Type: indexmap::Bucket] + [5] : { key=0x75 'u', value=2 } [Type: indexmap::Bucket] + [6] : { key=0x69 'i', value=1 } [Type: indexmap::Bucket] + [7] : { key=0x63 'c', value=1 } [Type: indexmap::Bucket] + [8] : { key=0x6b 'k', value=1 } [Type: indexmap::Bucket] + [9] : { key=0x67 'g', value=1 } [Type: indexmap::Bucket] + [10] : { key=0x72 'r', value=2 } [Type: indexmap::Bucket] + [11] : { key=0x6f 'o', value=4 } [Type: indexmap::Bucket] + [12] : { key=0x77 'w', value=1 } [Type: indexmap::Bucket] + [13] : { key=0x6e 'n', value=1 } [Type: indexmap::Bucket] + [14] : { key=0x66 'f', value=1 } [Type: indexmap::Bucket] + [15] : { key=0x78 'x', value=1 } [Type: indexmap::Bucket] + [16] : { key=0x6a 'j', value=1 } [Type: indexmap::Bucket] + [17] : { key=0x6d 'm', value=1 } [Type: indexmap::Bucket] + [18] : { key=0x70 'p', value=1 } [Type: indexmap::Bucket] + [19] : { key=0x64 'd', value=2 } [Type: indexmap::Bucket] + [20] : { key=0x76 'v', value=1 } [Type: indexmap::Bucket] + [21] : { key=0x6c 'l', value=1 } [Type: indexmap::Bucket] + [22] : { key=0x61 'a', value=1 } [Type: indexmap::Bucket] + [23] : { key=0x7a 'z', value=1 } [Type: indexmap::Bucket] + [24] : { key=0x79 'y', value=1 } [Type: indexmap::Bucket] +"# +)] +fn test_debugger_visualizer() { + let mut set = IndexSet::new(); + let mut map = IndexMap::new(); + + for ch in "the quick brown fox jumped over the lazy dog".chars() { + set.insert(ch); + *map.entry(ch).or_insert(0) += 1; + } + + let b = 'b'; + assert!(set.shift_remove(&b)); + assert_eq!(Some(1), map.remove(&b)); + + __break(); +}