Add basic CDB support to debuginfo compiletest s, to help catch *.natvis regressions, like those fixed in #60687.

Several Microsoft debuggers (VS, VS Code, WinDbg, CDB, ...) consume the `*.natvis` files we embed into rust `*.pdb` files.
While this only tests CDB, that test coverage should help for all of them.

CHANGES

src\bootstrap
  - test.rs:  Run CDB debuginfo tests on MSVC targets

src\test\debuginfo
  - issue-13213.rs:  CDB has trouble with this, skip for now (newly discovered regression?)
  - pretty-std.rs:  Was ignored, re-enable for CDB only to start with, add CDB tests.
  - should-fail.rs:  Add CDB tests.

src\tools\compiletest:
  - Added "-cdb" option
  - Added Mode::DebugInfoCdb ("debuginfo-cdb")
  - Added run_debuginfo_cdb_test[_no_opt]
  - Renamed Mode::DebugInfoBoth -> DebugInfoGdbLldb ("debuginfo-gdb+lldb") since it's no longer clear what "Both" means.
  - Find CDB at the default Win10 SDK install path "C:\Program Files (x86)\Windows Kits\10\Debugger\*\cdb.exe"
  - Ignore CDB tests if CDB not found.

ISSUES

  - `compute_stamp_hash`: not sure if there's any point in hashing `%ProgramFiles(x86)%`
  - `OsString` lacks any `*.natvis` entries (would be nice to add in a followup changelist)
  - DSTs (array/string slices) which work in VS & VS Code fail in CDB.
  - I've avoided `Mode::DebugInfoAll` as 3 debuggers leads to pow(2,3)=8 possible combinations.

REFERENCE

CDB is not part of the base Visual Studio install, but can be added via the Windows 10 SDK:
  https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk
Installing just "Debugging Tools for Windows" is sufficient.

CDB appears to already be installed on appveyor CI, where this changelist can find it, based on it's use here:
  0ffc573110/appveyor.yml (L227)

CDB commands and command line reference:
  https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/debugger-reference
This commit is contained in:
MaulingMonkey 2019-05-19 17:10:48 -07:00
parent 26ab32499c
commit e33c4415b3
8 changed files with 251 additions and 31 deletions

View file

@ -976,14 +976,10 @@ fn run(self, builder: &Builder<'_>) {
}
if suite == "debuginfo" {
// Skip debuginfo tests on MSVC
if builder.config.build.contains("msvc") {
return;
}
let msvc = builder.config.build.contains("msvc");
if mode == "debuginfo" {
return builder.ensure(Compiletest {
mode: "debuginfo-both",
mode: if msvc { "debuginfo-cdb" } else { "debuginfo-gdb+lldb" },
..self
});
}

View file

@ -1,4 +1,5 @@
// min-lldb-version: 310
// ignore-cdb: Fails with exit code 0xc0000135 ("the application failed to initialize properly")
// aux-build:issue-13213-aux.rs

View file

@ -1,6 +1,5 @@
// ignore-windows failing on win32 bot
// ignore-freebsd: gdb package too new
// ignore-test // Test temporarily ignored due to debuginfo tests being disabled, see PR 47155
// only-cdb // Test temporarily ignored on GDB/LLDB due to debuginfo tests being disabled, see PR 47155
// ignore-android: FIXME(#10381)
// compile-flags:-g
// min-gdb-version 7.7
@ -63,6 +62,56 @@
// lldb-check:[...]$5 = None
// === CDB TESTS ==================================================================================
// cdb-command: g
// cdb-command: dx slice,d
// cdb-check:slice,d [...]
// NOTE: While slices have a .natvis entry that works in VS & VS Code, it fails in CDB 10.0.18362.1
// cdb-command: dx vec,d
// cdb-check:vec,d [...] : { size=4 } [Type: [...]::Vec<u64>]
// cdb-check: [size] : 4 [Type: [...]]
// cdb-check: [capacity] : [...] [Type: [...]]
// cdb-check: [0] : 4 [Type: unsigned __int64]
// cdb-check: [1] : 5 [Type: unsigned __int64]
// cdb-check: [2] : 6 [Type: unsigned __int64]
// cdb-check: [3] : 7 [Type: unsigned __int64]
// cdb-command: dx str_slice
// cdb-check:str_slice [...]
// NOTE: While string slices have a .natvis entry that works in VS & VS Code, it fails in CDB 10.0.18362.1
// cdb-command: dx string
// cdb-check:string : "IAMA string!" [Type: [...]::String]
// cdb-check: [<Raw View>] [Type: [...]::String]
// cdb-check: [size] : 0xc [Type: [...]]
// cdb-check: [capacity] : 0xc [Type: [...]]
// cdb-check: [0] : 73 'I' [Type: char]
// cdb-check: [1] : 65 'A' [Type: char]
// cdb-check: [2] : 77 'M' [Type: char]
// cdb-check: [3] : 65 'A' [Type: char]
// cdb-check: [4] : 32 ' ' [Type: char]
// cdb-check: [5] : 115 's' [Type: char]
// cdb-check: [6] : 116 't' [Type: char]
// cdb-check: [7] : 114 'r' [Type: char]
// cdb-check: [8] : 105 'i' [Type: char]
// cdb-check: [9] : 110 'n' [Type: char]
// cdb-check: [10] : 103 'g' [Type: char]
// cdb-check: [11] : 33 '!' [Type: char]
// cdb-command: dx os_string
// cdb-check:os_string [Type: [...]::OsString]
// NOTE: OsString doesn't have a .natvis entry yet.
// cdb-command: dx some
// cdb-check:some : { Some 8 } [Type: [...]::Option<i16>]
// cdb-command: dx none
// cdb-check:none : { None } [Type: [...]::Option<i64>]
// cdb-command: dx some_string
// cdb-check:some_string : { Some "IAMA optional string!" } [Type: [...]::Option<[...]::String>]
#![allow(unused_variables)]
use std::ffi::OsString;

View file

@ -18,6 +18,13 @@
// lldb-command:print x
// lldb-check:[...]$0 = 5
// === CDB TESTS ==================================================================================
// cdb-command:g
// cdb-command:dx x
// cdb-check:string [...] : 5 [Type: [...]]
fn main() {
let x = 1;

View file

@ -1,5 +1,6 @@
pub use self::Mode::*;
use std::ffi::OsString;
use std::fmt;
use std::path::{Path, PathBuf};
use std::str::FromStr;
@ -15,7 +16,8 @@ pub enum Mode {
RunPass,
RunPassValgrind,
Pretty,
DebugInfoBoth,
DebugInfoCdb,
DebugInfoGdbLldb,
DebugInfoGdb,
DebugInfoLldb,
Codegen,
@ -33,9 +35,10 @@ impl Mode {
pub fn disambiguator(self) -> &'static str {
// Run-pass and pretty run-pass tests could run concurrently, and if they do,
// they need to keep their output segregated. Same is true for debuginfo tests that
// can be run both on gdb and lldb.
// can be run on cdb, gdb, and lldb.
match self {
Pretty => ".pretty",
DebugInfoCdb => ".cdb",
DebugInfoGdb => ".gdb",
DebugInfoLldb => ".lldb",
_ => "",
@ -52,7 +55,8 @@ fn from_str(s: &str) -> Result<Mode, ()> {
"run-pass" => Ok(RunPass),
"run-pass-valgrind" => Ok(RunPassValgrind),
"pretty" => Ok(Pretty),
"debuginfo-both" => Ok(DebugInfoBoth),
"debuginfo-cdb" => Ok(DebugInfoCdb),
"debuginfo-gdb+lldb" => Ok(DebugInfoGdbLldb),
"debuginfo-lldb" => Ok(DebugInfoLldb),
"debuginfo-gdb" => Ok(DebugInfoGdb),
"codegen" => Ok(Codegen),
@ -77,7 +81,8 @@ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
RunPass => "run-pass",
RunPassValgrind => "run-pass-valgrind",
Pretty => "pretty",
DebugInfoBoth => "debuginfo-both",
DebugInfoCdb => "debuginfo-cdb",
DebugInfoGdbLldb => "debuginfo-gdb+lldb",
DebugInfoGdb => "debuginfo-gdb",
DebugInfoLldb => "debuginfo-lldb",
Codegen => "codegen",
@ -198,6 +203,9 @@ pub struct Config {
/// Host triple for the compiler being invoked
pub host: String,
/// Path to / name of the Microsoft Console Debugger (CDB) executable
pub cdb: Option<OsString>,
/// Path to / name of the GDB executable
pub gdb: Option<String>,

View file

@ -57,9 +57,9 @@ enum ParsedNameDirective {
NoMatch,
/// Match.
Match,
/// Mode was DebugInfoBoth and this matched gdb.
/// Mode was DebugInfoGdbLldb and this matched gdb.
MatchGdb,
/// Mode was DebugInfoBoth and this matched lldb.
/// Mode was DebugInfoGdbLldb and this matched lldb.
MatchLldb,
}
@ -81,13 +81,17 @@ pub fn from_file(config: &Config, testfile: &Path) -> Self {
revisions: vec![],
};
if config.mode == common::DebugInfoBoth {
if config.mode == common::DebugInfoGdbLldb {
if config.lldb_python_dir.is_none() {
props.ignore = props.ignore.no_lldb();
}
if config.gdb_version.is_none() {
props.ignore = props.ignore.no_gdb();
}
} else if config.mode == common::DebugInfoCdb {
if config.cdb.is_none() {
props.ignore = Ignore::Ignore;
}
}
let rustc_has_profiler_support = env::var_os("RUSTC_PROFILER_SUPPORT").is_some();
@ -133,12 +137,12 @@ pub fn from_file(config: &Config, testfile: &Path) -> Self {
}
}
if (config.mode == common::DebugInfoGdb || config.mode == common::DebugInfoBoth) &&
if (config.mode == common::DebugInfoGdb || config.mode == common::DebugInfoGdbLldb) &&
props.ignore.can_run_gdb() && ignore_gdb(config, ln) {
props.ignore = props.ignore.no_gdb();
}
if (config.mode == common::DebugInfoLldb || config.mode == common::DebugInfoBoth) &&
if (config.mode == common::DebugInfoLldb || config.mode == common::DebugInfoGdbLldb) &&
props.ignore.can_run_lldb() && ignore_lldb(config, ln) {
props.ignore = props.ignore.no_lldb();
}
@ -804,7 +808,7 @@ fn parse_cfg_name_directive(&self, line: &str, prefix: &str) -> ParsedNameDirect
ParsedNameDirective::Match
} else {
match self.mode {
common::DebugInfoBoth => {
common::DebugInfoGdbLldb => {
if name == "gdb" {
ParsedNameDirective::MatchGdb
} else if name == "lldb" {
@ -813,6 +817,11 @@ fn parse_cfg_name_directive(&self, line: &str, prefix: &str) -> ParsedNameDirect
ParsedNameDirective::NoMatch
}
},
common::DebugInfoCdb => if name == "cdb" {
ParsedNameDirective::Match
} else {
ParsedNameDirective::NoMatch
},
common::DebugInfoGdb => if name == "gdb" {
ParsedNameDirective::Match
} else {

View file

@ -1,5 +1,6 @@
#![crate_name = "compiletest"]
#![feature(test)]
#![feature(path_buf_capacity)]
#![feature(vec_remove_item)]
#![deny(warnings, rust_2018_idioms)]
@ -8,7 +9,7 @@
use crate::common::CompareMode;
use crate::common::{expected_output_path, output_base_dir, output_relative_path, UI_EXTENSIONS};
use crate::common::{Config, TestPaths};
use crate::common::{DebugInfoBoth, DebugInfoGdb, DebugInfoLldb, Mode, Pretty};
use crate::common::{DebugInfoCdb, DebugInfoGdbLldb, DebugInfoGdb, DebugInfoLldb, Mode, Pretty};
use getopts::Options;
use std::env;
use std::ffi::OsString;
@ -164,6 +165,12 @@ pub fn parse_config(args: Vec<String>) -> Config {
.optopt("", "logfile", "file to log test execution to", "FILE")
.optopt("", "target", "the target to build for", "TARGET")
.optopt("", "host", "the host to build for", "HOST")
.optopt(
"",
"cdb",
"path to CDB to use for CDB debuginfo tests",
"PATH",
)
.optopt(
"",
"gdb",
@ -273,6 +280,7 @@ fn make_absolute(path: PathBuf) -> PathBuf {
let target = opt_str2(matches.opt_str("target"));
let android_cross_path = opt_path(matches, "android-cross-path");
let cdb = analyze_cdb(matches.opt_str("cdb"), &target);
let (gdb, gdb_version, gdb_native_rust) = analyze_gdb(matches.opt_str("gdb"), &target,
&android_cross_path);
let (lldb_version, lldb_native_rust) = extract_lldb_version(matches.opt_str("lldb-version"));
@ -319,6 +327,7 @@ fn make_absolute(path: PathBuf) -> PathBuf {
target_rustcflags: matches.opt_str("target-rustcflags"),
target: target,
host: opt_str2(matches.opt_str("host")),
cdb,
gdb,
gdb_version,
gdb_native_rust,
@ -421,7 +430,7 @@ pub fn opt_str2(maybestr: Option<String>) -> String {
pub fn run_tests(config: &Config) {
if config.target.contains("android") {
if config.mode == DebugInfoGdb || config.mode == DebugInfoBoth {
if config.mode == DebugInfoGdb || config.mode == DebugInfoGdbLldb {
println!(
"{} debug-info test uses tcp 5039 port.\
please reserve it",
@ -440,8 +449,8 @@ pub fn run_tests(config: &Config) {
match config.mode {
// Note that we don't need to emit the gdb warning when
// DebugInfoBoth, so it is ok to list that here.
DebugInfoBoth | DebugInfoLldb => {
// DebugInfoGdbLldb, so it is ok to list that here.
DebugInfoGdbLldb | DebugInfoLldb => {
if let Some(lldb_version) = config.lldb_version.as_ref() {
if is_blacklisted_lldb_version(&lldb_version[..]) {
println!(
@ -470,7 +479,8 @@ pub fn run_tests(config: &Config) {
return;
}
}
_ => { /* proceed */ }
DebugInfoCdb | _ => { /* proceed */ }
}
// FIXME(#33435) Avoid spurious failures in codegen-units/partitioning tests.
@ -667,7 +677,7 @@ pub fn make_test(config: &Config, testpaths: &TestPaths) -> Vec<test::TestDescAn
&early_props,
revision.map(|s| s.as_str()),
)
|| ((config.mode == DebugInfoBoth ||
|| ((config.mode == DebugInfoGdbLldb || config.mode == DebugInfoCdb ||
config.mode == DebugInfoGdb || config.mode == DebugInfoLldb)
&& config.target.contains("emscripten"))
|| (config.mode == DebugInfoGdb && !early_props.ignore.can_run_gdb())
@ -815,7 +825,7 @@ fn make_test_closure(
revision: Option<&String>,
) -> test::TestFn {
let mut config = config.clone();
if config.mode == DebugInfoBoth {
if config.mode == DebugInfoGdbLldb {
// If both gdb and lldb were ignored, then the test as a whole
// would be ignored.
if !ignore.can_run_gdb() {
@ -841,6 +851,48 @@ fn is_android_gdb_target(target: &String) -> bool {
}
}
/// Returns `true` if the given target is a MSVC target for the purpouses of CDB testing.
fn is_pc_windows_msvc_target(target: &String) -> bool {
target.ends_with("-pc-windows-msvc")
}
fn find_cdb(target: &String) -> Option<OsString> {
if cfg!(windows) && is_pc_windows_msvc_target(target) {
let pf86 = env::var_os("ProgramFiles(x86)").or(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::with_capacity(64);
path.push(pf86);
path.push(r"Windows Kits\10\Debuggers"); // We could check more known install locations (8.1?)
path.push(cdb_arch);
path.push(r"cdb.exe");
if path.exists() {
Some(path.into_os_string())
} else {
None
}
}
else {
None
}
}
/// Returns Path to CDB
fn analyze_cdb(cdb: Option<String>, target: &String) -> Option<OsString> {
cdb.map(|s| OsString::from(s)).or(find_cdb(target))
}
/// Returns (Path to GDB, GDB Version, GDB has Rust Support)
fn analyze_gdb(gdb: Option<String>, target: &String, android_cross_path: &PathBuf)
-> (Option<String>, Option<u32>, bool) {

View file

@ -3,7 +3,7 @@
use crate::common::CompareMode;
use crate::common::{expected_output_path, UI_EXTENSIONS, UI_FIXED, UI_STDERR, UI_STDOUT};
use crate::common::{output_base_dir, output_base_name, output_testname_unique};
use crate::common::{Codegen, CodegenUnits, DebugInfoBoth, DebugInfoGdb, DebugInfoLldb, Rustdoc};
use crate::common::{Codegen, CodegenUnits, DebugInfoCdb, DebugInfoGdbLldb, DebugInfoGdb, DebugInfoLldb, Rustdoc};
use crate::common::{CompileFail, Pretty, RunFail, RunPass, RunPassValgrind};
use crate::common::{Config, TestPaths};
use crate::common::{Incremental, MirOpt, RunMake, Ui, JsDocTest, Assembly};
@ -242,7 +242,15 @@ pub fn compute_stamp_hash(config: &Config) -> String {
let mut hash = DefaultHasher::new();
config.stage_id.hash(&mut hash);
if config.mode == DebugInfoGdb || config.mode == DebugInfoBoth {
if config.mode == DebugInfoCdb {
match config.cdb {
None => env::var_os("ProgramFiles(x86)").hash(&mut hash),
Some(ref s) if s.is_empty() => env::var_os("ProgramFiles(x86)").hash(&mut hash),
Some(ref s) => s.hash(&mut hash),
}
}
if config.mode == DebugInfoGdb || config.mode == DebugInfoGdbLldb {
match config.gdb {
None => env::var_os("PATH").hash(&mut hash),
Some(ref s) if s.is_empty() => env::var_os("PATH").hash(&mut hash),
@ -250,7 +258,7 @@ pub fn compute_stamp_hash(config: &Config) -> String {
};
}
if config.mode == DebugInfoLldb || config.mode == DebugInfoBoth {
if config.mode == DebugInfoLldb || config.mode == DebugInfoGdbLldb {
env::var_os("PATH").hash(&mut hash);
env::var_os("PYTHONPATH").hash(&mut hash);
}
@ -285,10 +293,11 @@ fn run_revision(&self) {
RunFail => self.run_rfail_test(),
RunPassValgrind => self.run_valgrind_test(),
Pretty => self.run_pretty_test(),
DebugInfoBoth => {
DebugInfoGdbLldb => {
self.run_debuginfo_gdb_test();
self.run_debuginfo_lldb_test();
},
DebugInfoCdb => self.run_debuginfo_cdb_test(),
DebugInfoGdb => self.run_debuginfo_gdb_test(),
DebugInfoLldb => self.run_debuginfo_lldb_test(),
Codegen => self.run_codegen_test(),
@ -656,6 +665,95 @@ fn typecheck_source(&self, src: String) -> ProcRes {
self.compose_and_run_compiler(rustc, Some(src))
}
fn run_debuginfo_cdb_test(&self) {
assert!(self.revision.is_none(), "revisions not relevant here");
let config = Config {
target_rustcflags: self.cleanup_debug_info_options(&self.config.target_rustcflags),
host_rustcflags: self.cleanup_debug_info_options(&self.config.host_rustcflags),
mode: DebugInfoCdb,
..self.config.clone()
};
let test_cx = TestCx {
config: &config,
..*self
};
test_cx.run_debuginfo_cdb_test_no_opt();
}
fn run_debuginfo_cdb_test_no_opt(&self) {
// compile test file (it should have 'compile-flags:-g' in the header)
let compile_result = self.compile_test();
if !compile_result.status.success() {
self.fatal_proc_rec("compilation failed!", &compile_result);
}
let exe_file = self.make_exe_name();
let prefixes = {
static PREFIXES: &'static [&'static str] = &["cdb", "cdbg"];
// No "native rust support" variation for CDB yet.
PREFIXES
};
// Parse debugger commands etc from test files
let DebuggerCommands {
commands,
check_lines,
breakpoint_lines,
..
} = self.parse_debugger_commands(prefixes);
// https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/debugger-commands
let mut script_str = String::with_capacity(2048);
script_str.push_str("version\n"); // List CDB (and more) version info in test output
script_str.push_str(".nvlist\n"); // List loaded `*.natvis` files (bulk of MSVC debugger customizations)
// Set breakpoints on every line that contains the string "#break"
let source_file_name = self.testpaths.file.file_name().unwrap().to_string_lossy();
for line in &breakpoint_lines {
script_str.push_str(&format!(
"bp `{}:{}`\n",
source_file_name, line
));
}
// Append the other `cdb-command:`s
for line in &commands {
script_str.push_str(line);
script_str.push_str("\n");
}
script_str.push_str("\nqq\n"); // Quit the debugger (including remote debugger, if any)
// Write the script into a file
debug!("script_str = {}", script_str);
self.dump_output_file(&script_str, "debugger.script");
let debugger_script = self.make_out_name("debugger.script");
let cdb_path = &self.config.cdb.as_ref().unwrap();
let mut cdb = Command::new(cdb_path);
cdb
.arg("-lines") // Enable source line debugging.
.arg("-cf").arg(&debugger_script)
.arg(&exe_file);
let debugger_run_result = self.compose_and_run(
cdb,
self.config.run_lib_path.to_str().unwrap(),
None, // aux_path
None // input
);
if !debugger_run_result.status.success() {
self.fatal_proc_rec("Error while running CDB", &debugger_run_result);
}
self.check_debugger_output(&debugger_run_result, &check_lines);
}
fn run_debuginfo_gdb_test(&self) {
assert!(self.revision.is_none(), "revisions not relevant here");
@ -1429,7 +1527,7 @@ fn compile_test(&self) -> ProcRes {
RunPass | Ui => self.should_run_successfully(),
Incremental => self.revision.unwrap().starts_with("r"),
RunFail | RunPassValgrind | MirOpt |
DebugInfoBoth | DebugInfoGdb | DebugInfoLldb => true,
DebugInfoCdb | DebugInfoGdbLldb | DebugInfoGdb | DebugInfoLldb => true,
_ => false,
};
let output_file = if will_execute {
@ -1870,7 +1968,7 @@ fn make_compile_args(&self, input_file: &Path, output_file: TargetLocation) -> C
rustc.arg(dir_opt);
}
RunFail | RunPassValgrind | Pretty | DebugInfoBoth | DebugInfoGdb | DebugInfoLldb
RunFail | RunPassValgrind | Pretty | DebugInfoCdb | DebugInfoGdbLldb | DebugInfoGdb | DebugInfoLldb
| Codegen | Rustdoc | RunMake | CodegenUnits | JsDocTest | Assembly => {
// do not use JSON output
}