From 11d3b4f26a2010886d9ff5d93992de0e8a632ffd Mon Sep 17 00:00:00 2001 From: Henning Ottesen Date: Sun, 11 Jun 2017 02:08:40 +0200 Subject: [PATCH] Custom test harness - Run tests on a secondary console on windows. - Workaround for GenerateConsoleCtrlEvent() signaling the whole process group. - Enforces serialization and deterministic order for tests. - Uses SetStdHandle(). This breaks Rust's stdout for Rust pre 1.18.0. --- Cargo.toml | 5 + src/lib.rs | 30 ------ src/tests.rs | 265 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 270 insertions(+), 30 deletions(-) create mode 100644 src/tests.rs diff --git a/Cargo.toml b/Cargo.toml index e16f468..93838b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,8 @@ kernel32-sys = "0.2" [features] termination = [] + +[[test]] +name = "tests" +path = "src/tests.rs" +harness = false diff --git a/src/lib.rs b/src/lib.rs index 9d1ed16..db6b72e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -178,11 +178,6 @@ mod platform { Ok(()) } - - #[cfg(test)] - pub fn raise_ctrl_c() { - signal::raise(signal::Signal::SIGINT).unwrap(); - } } #[cfg(windows)] @@ -249,14 +244,6 @@ mod platform { ))), } } - - #[cfg(test)] - pub fn raise_ctrl_c() { - unsafe { - // This will signal the whole process group. - assert!(kernel32::GenerateConsoleCtrlEvent(winapi::CTRL_C_EVENT, 0) != 0); - } - } } /// Register signal handler for Ctrl-C. @@ -313,20 +300,3 @@ pub fn set_handler(user_handler: F) -> Result<(), Error> Ok(()) } - -#[cfg(test)] -mod tests { - #[test] - fn test_set_handler() { - let (tx, rx) = ::std::sync::mpsc::channel(); - super::set_handler(move || { - tx.send(true).unwrap(); - }).unwrap(); - - super::platform::raise_ctrl_c(); - - rx.recv_timeout(::std::time::Duration::from_secs(1)).unwrap(); - - assert!(super::set_handler(|| {}).is_err()); - } -} diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..b5a2f0d --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,265 @@ +// Copyright (c) 2015 CtrlC developers +// Licensed under the Apache License, Version 2.0 +// or the MIT +// license , +// at your option. All files in the project carrying such +// notice may not be copied, modified, or distributed except +// according to those terms. + +extern crate ctrlc; + +#[cfg(unix)] +mod platform { + extern crate nix; + + use ::std::io; + + pub unsafe fn setup() -> io::Result<()> { + Ok(()) + } + + pub unsafe fn cleanup() -> io::Result<()> { + Ok(()) + } + + pub unsafe fn raise_ctrl_c() { + self::nix::sys::signal::raise(self::nix::sys::signal::SIGINT).unwrap(); + } + + pub unsafe fn print(fmt: ::std::fmt::Arguments) { + use self::io::Write; + let stdout = ::std::io::stdout(); + stdout.lock().write_fmt(fmt).unwrap(); + } +} + +#[cfg(windows)] +mod platform { + extern crate winapi; + extern crate kernel32; + + use std::io; + use std::ptr; + use self::winapi::winnt::{CHAR, HANDLE}; + use self::winapi::minwindef::DWORD; + + /// Stores a piped stdout handle or a cache that gets flushed when we reattached to the old console. + enum Output { + Pipe(HANDLE), + Cached(Vec), + } + + static mut OLD_OUT: *mut Output = 0 as *mut Output; + + impl io::Write for Output { + fn write(&mut self, buf: &[u8]) -> io::Result { + match *self { + Output::Pipe(handle) => unsafe { + use self::winapi::winnt::VOID; + + let mut n = 0u32; + if self::kernel32::WriteFile( + handle, + buf.as_ptr() as *const VOID, + buf.len() as DWORD, + &mut n as *mut DWORD, + ptr::null_mut() + ) == 0 { + Err(io::Error::last_os_error()) + } else { + Ok(n as usize) + } + }, + Output::Cached(ref mut s) => s.write(buf), + } + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + } + + impl Output { + /// Stores current piped stdout or creates a new output cache that will + /// be written to stdout at a later time. + fn new() -> io::Result { + use self::winapi::shlobj::INVALID_HANDLE_VALUE; + + unsafe { + let stdout = self::kernel32::GetStdHandle(winapi::STD_OUTPUT_HANDLE); + if stdout.is_null() || stdout == INVALID_HANDLE_VALUE { + return Err(io::Error::last_os_error()); + } + + let mut out = 0u32; + match self::kernel32::GetConsoleMode(stdout, &mut out as *mut DWORD) { + 0 => Ok(Output::Pipe(stdout)), + _ => Ok(Output::Cached(Vec::new())), + } + } + } + + /// Set stdout/stderr and flush cache. + unsafe fn set_as_std(self) -> io::Result<()> { + let stdout = match self { + Output::Pipe(h) => h, + Output::Cached(_) => get_stdout()?, + }; + + if self::kernel32::SetStdHandle(winapi::STD_OUTPUT_HANDLE, stdout) == 0 { + return Err(io::Error::last_os_error()); + } + + if self::kernel32::SetStdHandle(winapi::STD_ERROR_HANDLE, stdout) == 0 { + return Err(io::Error::last_os_error()); + } + + match self { + Output::Pipe(_) => Ok(()), + Output::Cached(ref s) => { + // Write cached output + use self::io::Write; + let out = io::stdout(); + out.lock().write_all(&s[..])?; + Ok(()) + } + } + } + } + + unsafe fn get_stdout() -> io::Result { + use self::winapi::winnt::{GENERIC_READ, GENERIC_WRITE, FILE_SHARE_WRITE}; + use self::winapi::shlobj::INVALID_HANDLE_VALUE; + use self::winapi::fileapi::OPEN_EXISTING; + + let stdout = self::kernel32::CreateFileA( + "CONOUT$\0".as_ptr() as *const CHAR, + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_WRITE, + ptr::null_mut(), + OPEN_EXISTING, + 0, + ptr::null_mut() + ); + + if stdout.is_null() || stdout == INVALID_HANDLE_VALUE { + Err(io::Error::last_os_error()) + } else { + Ok(stdout) + } + } + + /// Detach from the current console and create a new one, + /// We do this because GenerateConsoleCtrlEvent() sends ctrl-c events + /// to all processes on the same console. We want events to be received + /// only by our process. + /// + /// This breaks rust's stdout pre 1.18.0. Rust used to + /// [cache the std handles](https://github.com/rust-lang/rust/pull/40516) + /// + pub unsafe fn setup() -> io::Result<()> { + let old_out = Output::new()?; + + if self::kernel32::FreeConsole() == 0 { + return Err(io::Error::last_os_error()); + } + + if self::kernel32::AllocConsole() == 0 { + return Err(io::Error::last_os_error()); + } + + // AllocConsole will not always set stdout/stderr to the to the console buffer + // of the new terminal. + + let stdout = get_stdout()?; + if self::kernel32::SetStdHandle(winapi::STD_OUTPUT_HANDLE, stdout) == 0 { + return Err(io::Error::last_os_error()); + } + + if self::kernel32::SetStdHandle(winapi::STD_ERROR_HANDLE, stdout) == 0 { + return Err(io::Error::last_os_error()); + } + + OLD_OUT = Box::into_raw(Box::new(old_out)); + + Ok(()) + } + + /// Reattach to the old console. + pub unsafe fn cleanup() -> io::Result<()> { + if self::kernel32::FreeConsole() == 0 { + return Err(io::Error::last_os_error()); + } + + if self::kernel32::AttachConsole(winapi::wincon::ATTACH_PARENT_PROCESS) == 0 { + return Err(io::Error::last_os_error()); + } + + Box::from_raw(OLD_OUT).set_as_std()?; + + Ok(()) + } + + /// This will signal the whole process group. + pub unsafe fn raise_ctrl_c() { + assert!(self::kernel32::GenerateConsoleCtrlEvent(self::winapi::CTRL_C_EVENT, 0) != 0); + } + + /// Print to both consoles, this is not thread safe. + pub unsafe fn print(fmt: ::std::fmt::Arguments) { + use self::io::Write; + { + let stdout = io::stdout(); + stdout.lock().write_fmt(fmt).unwrap(); + } + { + assert!(!OLD_OUT.is_null()); + (*OLD_OUT).write_fmt(fmt).unwrap(); + } + } +} + +fn test_set_handler() { + let (tx, rx) = ::std::sync::mpsc::channel(); + ctrlc::set_handler(move || { + tx.send(true).unwrap(); + }).unwrap(); + + unsafe { platform::raise_ctrl_c(); } + + rx.recv_timeout(::std::time::Duration::from_secs(10)).unwrap(); + + match ctrlc::set_handler(|| {}) { + Err(ctrlc::Error::MultipleHandlers) => {}, + ret => panic!("{:?}", ret), + } +} + +macro_rules! run_tests { + ( $($test_fn:ident),* ) => { + unsafe { + platform::print(format_args!("\n")); + $( + platform::print(format_args!("test tests::{} ... ", stringify!($test_fn))); + $test_fn(); + platform::print(format_args!("ok\n")); + )* + platform::print(format_args!("\n")); + } + } +} + +fn main() { + unsafe { platform::setup().unwrap(); } + + let default = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + unsafe { platform::cleanup().unwrap(); } + (default)(info); + })); + + run_tests!(test_set_handler); + + unsafe { platform::cleanup().unwrap(); } +} \ No newline at end of file