Skip to content

Commit 9f91508

Browse files
committed
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.
1 parent 5f1a755 commit 9f91508

File tree

3 files changed

+270
-30
lines changed

3 files changed

+270
-30
lines changed

Cargo.toml

+5
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,8 @@ kernel32-sys = "0.2"
1717

1818
[features]
1919
termination = []
20+
21+
[[test]]
22+
name = "tests"
23+
path = "src/tests.rs"
24+
harness = false

src/lib.rs

-30
Original file line numberDiff line numberDiff line change
@@ -178,11 +178,6 @@ mod platform {
178178

179179
Ok(())
180180
}
181-
182-
#[cfg(test)]
183-
pub fn raise_ctrl_c() {
184-
signal::raise(signal::Signal::SIGINT).unwrap();
185-
}
186181
}
187182

188183
#[cfg(windows)]
@@ -249,14 +244,6 @@ mod platform {
249244
))),
250245
}
251246
}
252-
253-
#[cfg(test)]
254-
pub fn raise_ctrl_c() {
255-
unsafe {
256-
// This will signal the whole process group.
257-
assert!(kernel32::GenerateConsoleCtrlEvent(winapi::CTRL_C_EVENT, 0) != 0);
258-
}
259-
}
260247
}
261248

262249
/// Register signal handler for Ctrl-C.
@@ -313,20 +300,3 @@ pub fn set_handler<F>(user_handler: F) -> Result<(), Error>
313300

314301
Ok(())
315302
}
316-
317-
#[cfg(test)]
318-
mod tests {
319-
#[test]
320-
fn test_set_handler() {
321-
let (tx, rx) = ::std::sync::mpsc::channel();
322-
super::set_handler(move || {
323-
tx.send(true).unwrap();
324-
}).unwrap();
325-
326-
super::platform::raise_ctrl_c();
327-
328-
rx.recv_timeout(::std::time::Duration::from_secs(1)).unwrap();
329-
330-
assert!(super::set_handler(|| {}).is_err());
331-
}
332-
}

src/tests.rs

+265
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
// Copyright (c) 2015 CtrlC developers
2+
// Licensed under the Apache License, Version 2.0
3+
// <LICENSE-APACHE or
4+
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT
5+
// license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
6+
// at your option. All files in the project carrying such
7+
// notice may not be copied, modified, or distributed except
8+
// according to those terms.
9+
10+
extern crate ctrlc;
11+
12+
#[cfg(unix)]
13+
mod platform {
14+
extern crate nix;
15+
16+
use ::std::io;
17+
18+
pub unsafe fn setup() -> io::Result<()> {
19+
Ok(())
20+
}
21+
22+
pub unsafe fn cleanup() -> io::Result<()> {
23+
Ok(())
24+
}
25+
26+
pub unsafe fn raise_ctrl_c() {
27+
self::nix::sys::signal::raise(self::nix::sys::signal::SIGINT).unwrap();
28+
}
29+
30+
pub unsafe fn print(fmt: ::std::fmt::Arguments) {
31+
use self::io::Write;
32+
let stdout = ::std::io::stdout();
33+
stdout.lock().write_fmt(fmt).unwrap();
34+
}
35+
}
36+
37+
#[cfg(windows)]
38+
mod platform {
39+
extern crate winapi;
40+
extern crate kernel32;
41+
42+
use std::io;
43+
use std::ptr;
44+
use self::winapi::winnt::{CHAR, HANDLE};
45+
use self::winapi::minwindef::DWORD;
46+
47+
/// Stores a piped stdout handle or a cache that gets flushed when we reattached to the old console.
48+
enum Output {
49+
Pipe(HANDLE),
50+
Cached(Vec<u8>),
51+
}
52+
53+
static mut OLD_OUT: *mut Output = 0 as *mut Output;
54+
55+
impl io::Write for Output {
56+
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
57+
match *self {
58+
Output::Pipe(handle) => unsafe {
59+
use self::winapi::winnt::VOID;
60+
61+
let mut n = 0u32;
62+
if self::kernel32::WriteFile(
63+
handle,
64+
buf.as_ptr() as *const VOID,
65+
buf.len() as DWORD,
66+
&mut n as *mut DWORD,
67+
ptr::null_mut()
68+
) == 0 {
69+
Err(io::Error::last_os_error())
70+
} else {
71+
Ok(n as usize)
72+
}
73+
},
74+
Output::Cached(ref mut s) => s.write(buf),
75+
}
76+
}
77+
78+
fn flush(&mut self) -> io::Result<()> {
79+
Ok(())
80+
}
81+
}
82+
83+
impl Output {
84+
/// Stores current piped stdout or creates a new output cache that will
85+
/// be written to stdout at a later time.
86+
fn new() -> io::Result<Output> {
87+
use self::winapi::shlobj::INVALID_HANDLE_VALUE;
88+
89+
unsafe {
90+
let stdout = self::kernel32::GetStdHandle(winapi::STD_OUTPUT_HANDLE);
91+
if stdout.is_null() || stdout == INVALID_HANDLE_VALUE {
92+
return Err(io::Error::last_os_error());
93+
}
94+
95+
let mut out = 0u32;
96+
match self::kernel32::GetConsoleMode(stdout, &mut out as *mut DWORD) {
97+
0 => Ok(Output::Pipe(stdout)),
98+
_ => Ok(Output::Cached(Vec::new())),
99+
}
100+
}
101+
}
102+
103+
/// Set stdout/stderr and flush cache.
104+
unsafe fn set_as_std(self) -> io::Result<()> {
105+
let stdout = match self {
106+
Output::Pipe(h) => h,
107+
Output::Cached(_) => get_stdout()?,
108+
};
109+
110+
if self::kernel32::SetStdHandle(winapi::STD_OUTPUT_HANDLE, stdout) == 0 {
111+
return Err(io::Error::last_os_error());
112+
}
113+
114+
if self::kernel32::SetStdHandle(winapi::STD_ERROR_HANDLE, stdout) == 0 {
115+
return Err(io::Error::last_os_error());
116+
}
117+
118+
match self {
119+
Output::Pipe(_) => Ok(()),
120+
Output::Cached(ref s) => {
121+
// Write cached output
122+
use self::io::Write;
123+
let out = io::stdout();
124+
out.lock().write_all(&s[..])?;
125+
Ok(())
126+
}
127+
}
128+
}
129+
}
130+
131+
unsafe fn get_stdout() -> io::Result<HANDLE> {
132+
use self::winapi::winnt::{GENERIC_READ, GENERIC_WRITE, FILE_SHARE_WRITE};
133+
use self::winapi::shlobj::INVALID_HANDLE_VALUE;
134+
use self::winapi::fileapi::OPEN_EXISTING;
135+
136+
let stdout = self::kernel32::CreateFileA(
137+
"CONOUT$\0".as_ptr() as *const CHAR,
138+
GENERIC_READ | GENERIC_WRITE,
139+
FILE_SHARE_WRITE,
140+
ptr::null_mut(),
141+
OPEN_EXISTING,
142+
0,
143+
ptr::null_mut()
144+
);
145+
146+
if stdout.is_null() || stdout == INVALID_HANDLE_VALUE {
147+
Err(io::Error::last_os_error())
148+
} else {
149+
Ok(stdout)
150+
}
151+
}
152+
153+
/// Detach from the current console and create a new one,
154+
/// We do this because GenerateConsoleCtrlEvent() sends ctrl-c events
155+
/// to all processes on the same console. We want events to be received
156+
/// only by our process.
157+
///
158+
/// This breaks rust's stdout pre 1.18.0. Rust used to
159+
/// [cache the std handles](https://github.com/rust-lang/rust/pull/40516)
160+
///
161+
pub unsafe fn setup() -> io::Result<()> {
162+
let old_out = Output::new()?;
163+
164+
if self::kernel32::FreeConsole() == 0 {
165+
return Err(io::Error::last_os_error());
166+
}
167+
168+
if self::kernel32::AllocConsole() == 0 {
169+
return Err(io::Error::last_os_error());
170+
}
171+
172+
// AllocConsole will not always set stdout/stderr to the to the console buffer
173+
// of the new terminal.
174+
175+
let stdout = get_stdout()?;
176+
if self::kernel32::SetStdHandle(winapi::STD_OUTPUT_HANDLE, stdout) == 0 {
177+
return Err(io::Error::last_os_error());
178+
}
179+
180+
if self::kernel32::SetStdHandle(winapi::STD_ERROR_HANDLE, stdout) == 0 {
181+
return Err(io::Error::last_os_error());
182+
}
183+
184+
OLD_OUT = Box::into_raw(Box::new(old_out));
185+
186+
Ok(())
187+
}
188+
189+
/// Reattach to the old console.
190+
pub unsafe fn cleanup() -> io::Result<()> {
191+
if self::kernel32::FreeConsole() == 0 {
192+
return Err(io::Error::last_os_error());
193+
}
194+
195+
if self::kernel32::AttachConsole(winapi::wincon::ATTACH_PARENT_PROCESS) == 0 {
196+
return Err(io::Error::last_os_error());
197+
}
198+
199+
Box::from_raw(OLD_OUT).set_as_std()?;
200+
201+
Ok(())
202+
}
203+
204+
/// This will signal the whole process group.
205+
pub unsafe fn raise_ctrl_c() {
206+
assert!(self::kernel32::GenerateConsoleCtrlEvent(self::winapi::CTRL_C_EVENT, 0) != 0);
207+
}
208+
209+
/// Print to both consoles, this is not thread safe.
210+
pub unsafe fn print(fmt: ::std::fmt::Arguments) {
211+
use self::io::Write;
212+
{
213+
let stdout = io::stdout();
214+
stdout.lock().write_fmt(fmt).unwrap();
215+
}
216+
{
217+
assert!(!OLD_OUT.is_null());
218+
(*OLD_OUT).write_fmt(fmt).unwrap();
219+
}
220+
}
221+
}
222+
223+
fn test_set_handler() {
224+
let (tx, rx) = ::std::sync::mpsc::channel();
225+
ctrlc::set_handler(move || {
226+
tx.send(true).unwrap();
227+
}).unwrap();
228+
229+
unsafe { platform::raise_ctrl_c(); }
230+
231+
rx.recv_timeout(::std::time::Duration::from_secs(10)).unwrap();
232+
233+
match ctrlc::set_handler(|| {}) {
234+
Err(ctrlc::Error::MultipleHandlers) => {},
235+
ret => panic!("{:?}", ret),
236+
}
237+
}
238+
239+
macro_rules! run_tests {
240+
( $($test_fn:ident),* ) => {
241+
unsafe {
242+
platform::print(format_args!("\n"));
243+
$(
244+
platform::print(format_args!("test tests::{} ... ", stringify!($test_fn)));
245+
$test_fn();
246+
platform::print(format_args!("ok\n"));
247+
)*
248+
platform::print(format_args!("\n"));
249+
}
250+
}
251+
}
252+
253+
fn main() {
254+
unsafe { platform::setup().unwrap(); }
255+
256+
let default = std::panic::take_hook();
257+
std::panic::set_hook(Box::new(move |info| {
258+
unsafe { platform::cleanup().unwrap(); }
259+
(default)(info);
260+
}));
261+
262+
run_tests!(test_set_handler);
263+
264+
unsafe { platform::cleanup().unwrap(); }
265+
}

0 commit comments

Comments
 (0)