diff --git a/Cargo.lock b/Cargo.lock index 3cc0026bf45..0256132be7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1319,15 +1319,6 @@ dependencies = [ "vcpkg 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "libtest" -version = "0.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "getopts 0.2.17 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc_term 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "libz-sys" version = "1.0.25" @@ -2952,11 +2943,6 @@ dependencies = [ "serialize 0.0.0", ] -[[package]] -name = "rustc_term" -version = "0.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "rustc_tools_util" version = "0.1.1" @@ -3439,6 +3425,10 @@ dependencies = [ "utf-8 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "term" +version = "0.0.0" + [[package]] name = "term" version = "0.4.6" @@ -3479,8 +3469,9 @@ dependencies = [ name = "test" version = "0.0.0" dependencies = [ - "libtest 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "getopts 0.2.17 (registry+https://github.com/rust-lang/crates.io-index)", "proc_macro 0.0.0", + "term 0.0.0", ] [[package]] @@ -4113,7 +4104,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum libgit2-sys 0.7.11 (registry+https://github.com/rust-lang/crates.io-index)" = "48441cb35dc255da8ae72825689a95368bf510659ae1ad55dc4aa88cb1789bf1" "checksum libnghttp2-sys 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d75d7966bda4730b722d1eab8e668df445368a24394bae9fc1e8dc0ab3dbe4f4" "checksum libssh2-sys 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "126a1f4078368b163bfdee65fbab072af08a1b374a5551b21e87ade27b1fbf9d" -"checksum libtest 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1a51ac59582b915cdfc426dada72c6d9eba95818a6b481ca340f5c7152166837" "checksum libz-sys 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)" = "2eb5e43362e38e2bca2fd5f5134c4d4564a23a5c28e9b95411652021a8675ebe" "checksum lock_api 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "949826a5ccf18c1b3a7c3d57692778d21768b79e46eb9dd07bfc4c2160036c54" "checksum log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6" @@ -4222,7 +4212,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum rustc-rayon 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8d98c51d9cbbe810c8b6693236d3412d8cd60513ff27a3e1b6af483dca0af544" "checksum rustc-rayon-core 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "526e7b6d2707a5b9bec3927d424ad70fa3cfc68e0ac1b75e46cdbbc95adc5108" "checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" -"checksum rustc_term 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9c69abe7f181d2ea8d2f7b44a4aa86f4b4a567444bcfcf51ed45ede957fbf064" "checksum rustc_tools_util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b3c5a95edfa0c893236ae4778bb7c4752760e4c0d245e19b5eff33c5aa5eb9dc" "checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" "checksum rustfix 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "af7c21531a91512a4a51b490be6ba1c8eff34fdda0dc5bf87dc28d86748aac56" diff --git a/src/bootstrap/dist.rs b/src/bootstrap/dist.rs index a923f2d2ca3..61a7705bd6c 100644 --- a/src/bootstrap/dist.rs +++ b/src/bootstrap/dist.rs @@ -901,6 +901,7 @@ impl Step for Src { "src/libstd", "src/libunwind", "src/libtest", + "src/libterm", "src/libprofiler_builtins", "src/stdsimd", "src/libproc_macro", diff --git a/src/libterm/Cargo.toml b/src/libterm/Cargo.toml new file mode 100644 index 00000000000..4eba9a9d79c --- /dev/null +++ b/src/libterm/Cargo.toml @@ -0,0 +1,10 @@ +[package] +authors = ["The Rust Project Developers"] +name = "term" +version = "0.0.0" +edition = "2018" + +[lib] +name = "term" +path = "lib.rs" +crate-type = ["dylib", "rlib"] diff --git a/src/libterm/lib.rs b/src/libterm/lib.rs new file mode 100644 index 00000000000..711716d9b92 --- /dev/null +++ b/src/libterm/lib.rs @@ -0,0 +1,201 @@ +//! Terminal formatting library. +//! +//! This crate provides the `Terminal` trait, which abstracts over an [ANSI +//! Terminal][ansi] to provide color printing, among other things. There are two +//! implementations, the `TerminfoTerminal`, which uses control characters from +//! a [terminfo][ti] database, and `WinConsole`, which uses the [Win32 Console +//! API][win]. +//! +//! # Examples +//! +//! ```no_run +//! # #![feature(rustc_private)] +//! extern crate term; +//! use std::io::prelude::*; +//! +//! fn main() { +//! let mut t = term::stdout().unwrap(); +//! +//! t.fg(term::color::GREEN).unwrap(); +//! write!(t, "hello, ").unwrap(); +//! +//! t.fg(term::color::RED).unwrap(); +//! writeln!(t, "world!").unwrap(); +//! +//! assert!(t.reset().unwrap()); +//! } +//! ``` +//! +//! [ansi]: https://en.wikipedia.org/wiki/ANSI_escape_code +//! [win]: http://msdn.microsoft.com/en-us/library/windows/desktop/ms682010%28v=vs.85%29.aspx +//! [ti]: https://en.wikipedia.org/wiki/Terminfo + +#![doc(html_root_url = "https://doc.rust-lang.org/nightly/", + html_playground_url = "https://play.rust-lang.org/", + test(attr(deny(warnings))))] +#![deny(missing_docs)] + +#![deny(rust_2018_idioms)] + +#![cfg_attr(windows, feature(libc))] +// Handle rustfmt skips +#![feature(custom_attribute)] +#![allow(unused_attributes)] + +use std::io::prelude::*; +use std::io::{self, Stdout, Stderr}; + +pub use terminfo::TerminfoTerminal; +#[cfg(windows)] +pub use win::WinConsole; + +pub mod terminfo; + +#[cfg(windows)] +mod win; + +/// Alias for stdout terminals. +pub type StdoutTerminal = dyn Terminal + Send; +/// Alias for stderr terminals. +pub type StderrTerminal = dyn Terminal + Send; + +#[cfg(not(windows))] +/// Returns a Terminal wrapping stdout, or None if a terminal couldn't be +/// opened. +pub fn stdout() -> Option> { + TerminfoTerminal::new(io::stdout()).map(|t| Box::new(t) as Box) +} + +#[cfg(windows)] +/// Returns a Terminal wrapping stdout, or None if a terminal couldn't be +/// opened. +pub fn stdout() -> Option> { + TerminfoTerminal::new(io::stdout()) + .map(|t| Box::new(t) as Box) + .or_else(|| WinConsole::new(io::stdout()).ok().map(|t| Box::new(t) as Box)) +} + +#[cfg(not(windows))] +/// Returns a Terminal wrapping stderr, or None if a terminal couldn't be +/// opened. +pub fn stderr() -> Option> { + TerminfoTerminal::new(io::stderr()).map(|t| Box::new(t) as Box) +} + +#[cfg(windows)] +/// Returns a Terminal wrapping stderr, or None if a terminal couldn't be +/// opened. +pub fn stderr() -> Option> { + TerminfoTerminal::new(io::stderr()) + .map(|t| Box::new(t) as Box) + .or_else(|| WinConsole::new(io::stderr()).ok().map(|t| Box::new(t) as Box)) +} + + +/// Terminal color definitions +#[allow(missing_docs)] +pub mod color { + /// Number for a terminal color + pub type Color = u16; + + pub const BLACK: Color = 0; + pub const RED: Color = 1; + pub const GREEN: Color = 2; + pub const YELLOW: Color = 3; + pub const BLUE: Color = 4; + pub const MAGENTA: Color = 5; + pub const CYAN: Color = 6; + pub const WHITE: Color = 7; + + pub const BRIGHT_BLACK: Color = 8; + pub const BRIGHT_RED: Color = 9; + pub const BRIGHT_GREEN: Color = 10; + pub const BRIGHT_YELLOW: Color = 11; + pub const BRIGHT_BLUE: Color = 12; + pub const BRIGHT_MAGENTA: Color = 13; + pub const BRIGHT_CYAN: Color = 14; + pub const BRIGHT_WHITE: Color = 15; +} + +/// Terminal attributes for use with term.attr(). +/// +/// Most attributes can only be turned on and must be turned off with term.reset(). +/// The ones that can be turned off explicitly take a boolean value. +/// Color is also represented as an attribute for convenience. +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub enum Attr { + /// Bold (or possibly bright) mode + Bold, + /// Dim mode, also called faint or half-bright. Often not supported + Dim, + /// Italics mode. Often not supported + Italic(bool), + /// Underline mode + Underline(bool), + /// Blink mode + Blink, + /// Standout mode. Often implemented as Reverse, sometimes coupled with Bold + Standout(bool), + /// Reverse mode, inverts the foreground and background colors + Reverse, + /// Secure mode, also called invis mode. Hides the printed text + Secure, + /// Convenience attribute to set the foreground color + ForegroundColor(color::Color), + /// Convenience attribute to set the background color + BackgroundColor(color::Color), +} + +/// A terminal with similar capabilities to an ANSI Terminal +/// (foreground/background colors etc). +pub trait Terminal: Write { + /// The terminal's output writer type. + type Output: Write; + + /// Sets the foreground color to the given color. + /// + /// If the color is a bright color, but the terminal only supports 8 colors, + /// the corresponding normal color will be used instead. + /// + /// Returns `Ok(true)` if the color was set, `Ok(false)` otherwise, and `Err(e)` + /// if there was an I/O error. + fn fg(&mut self, color: color::Color) -> io::Result; + + /// Sets the background color to the given color. + /// + /// If the color is a bright color, but the terminal only supports 8 colors, + /// the corresponding normal color will be used instead. + /// + /// Returns `Ok(true)` if the color was set, `Ok(false)` otherwise, and `Err(e)` + /// if there was an I/O error. + fn bg(&mut self, color: color::Color) -> io::Result; + + /// Sets the given terminal attribute, if supported. Returns `Ok(true)` + /// if the attribute was supported, `Ok(false)` otherwise, and `Err(e)` if + /// there was an I/O error. + fn attr(&mut self, attr: Attr) -> io::Result; + + /// Returns `true` if the given terminal attribute is supported. + fn supports_attr(&self, attr: Attr) -> bool; + + /// Resets all terminal attributes and colors to their defaults. + /// + /// Returns `Ok(true)` if the terminal was reset, `Ok(false)` otherwise, and `Err(e)` if there + /// was an I/O error. + /// + /// *Note: This does not flush.* + /// + /// That means the reset command may get buffered so, if you aren't planning on doing anything + /// else that might flush stdout's buffer (e.g., writing a line of text), you should flush after + /// calling reset. + fn reset(&mut self) -> io::Result; + + /// Gets an immutable reference to the stream inside + fn get_ref(&self) -> &Self::Output; + + /// Gets a mutable reference to the stream inside + fn get_mut(&mut self) -> &mut Self::Output; + + /// Returns the contained stream, destroying the `Terminal` + fn into_inner(self) -> Self::Output where Self: Sized; +} diff --git a/src/libterm/terminfo/mod.rs b/src/libterm/terminfo/mod.rs new file mode 100644 index 00000000000..be90195065e --- /dev/null +++ b/src/libterm/terminfo/mod.rs @@ -0,0 +1,265 @@ +//! Terminfo database interface. + +use std::collections::HashMap; +use std::env; +use std::error; +use std::fmt; +use std::fs::File; +use std::io::{self, prelude::*, BufReader}; +use std::path::Path; + +use crate::Attr; +use crate::color; +use crate::Terminal; + +use searcher::get_dbpath_for_term; +use parser::compiled::{parse, msys_terminfo}; +use parm::{expand, Variables, Param}; + +/// A parsed terminfo database entry. +#[derive(Debug)] +pub struct TermInfo { + /// Names for the terminal + pub names: Vec, + /// Map of capability name to boolean value + pub bools: HashMap, + /// Map of capability name to numeric value + pub numbers: HashMap, + /// Map of capability name to raw (unexpanded) string + pub strings: HashMap>, +} + +/// A terminfo creation error. +#[derive(Debug)] +pub enum Error { + /// TermUnset Indicates that the environment doesn't include enough information to find + /// the terminfo entry. + TermUnset, + /// MalformedTerminfo indicates that parsing the terminfo entry failed. + MalformedTerminfo(String), + /// io::Error forwards any io::Errors encountered when finding or reading the terminfo entry. + IoError(io::Error), +} + +impl error::Error for Error { + fn description(&self) -> &str { + "failed to create TermInfo" + } + + fn cause(&self) -> Option<&dyn error::Error> { + use Error::*; + match *self { + IoError(ref e) => Some(e), + _ => None, + } + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use Error::*; + match *self { + TermUnset => Ok(()), + MalformedTerminfo(ref e) => e.fmt(f), + IoError(ref e) => e.fmt(f), + } + } +} + +impl TermInfo { + /// Creates a TermInfo based on current environment. + pub fn from_env() -> Result { + let term = match env::var("TERM") { + Ok(name) => TermInfo::from_name(&name), + Err(..) => return Err(Error::TermUnset), + }; + + if term.is_err() && env::var("MSYSCON").ok().map_or(false, |s| "mintty.exe" == s) { + // msys terminal + Ok(msys_terminfo()) + } else { + term + } + } + + /// Creates a TermInfo for the named terminal. + pub fn from_name(name: &str) -> Result { + get_dbpath_for_term(name) + .ok_or_else(|| { + Error::IoError(io::Error::new(io::ErrorKind::NotFound, "terminfo file not found")) + }) + .and_then(|p| TermInfo::from_path(&(*p))) + } + + /// Parse the given TermInfo. + pub fn from_path>(path: P) -> Result { + Self::_from_path(path.as_ref()) + } + // Keep the metadata small + fn _from_path(path: &Path) -> Result { + let file = File::open(path).map_err(Error::IoError)?; + let mut reader = BufReader::new(file); + parse(&mut reader, false).map_err(Error::MalformedTerminfo) + } +} + +pub mod searcher; + +/// TermInfo format parsing. +pub mod parser { + //! ncurses-compatible compiled terminfo format parsing (term(5)) + pub mod compiled; +} +pub mod parm; + + +fn cap_for_attr(attr: Attr) -> &'static str { + match attr { + Attr::Bold => "bold", + Attr::Dim => "dim", + Attr::Italic(true) => "sitm", + Attr::Italic(false) => "ritm", + Attr::Underline(true) => "smul", + Attr::Underline(false) => "rmul", + Attr::Blink => "blink", + Attr::Standout(true) => "smso", + Attr::Standout(false) => "rmso", + Attr::Reverse => "rev", + Attr::Secure => "invis", + Attr::ForegroundColor(_) => "setaf", + Attr::BackgroundColor(_) => "setab", + } +} + +/// A Terminal that knows how many colors it supports, with a reference to its +/// parsed Terminfo database record. +pub struct TerminfoTerminal { + num_colors: u16, + out: T, + ti: TermInfo, +} + +impl Terminal for TerminfoTerminal { + type Output = T; + fn fg(&mut self, color: color::Color) -> io::Result { + let color = self.dim_if_necessary(color); + if self.num_colors > color { + return self.apply_cap("setaf", &[Param::Number(color as i32)]); + } + Ok(false) + } + + fn bg(&mut self, color: color::Color) -> io::Result { + let color = self.dim_if_necessary(color); + if self.num_colors > color { + return self.apply_cap("setab", &[Param::Number(color as i32)]); + } + Ok(false) + } + + fn attr(&mut self, attr: Attr) -> io::Result { + match attr { + Attr::ForegroundColor(c) => self.fg(c), + Attr::BackgroundColor(c) => self.bg(c), + _ => self.apply_cap(cap_for_attr(attr), &[]), + } + } + + fn supports_attr(&self, attr: Attr) -> bool { + match attr { + Attr::ForegroundColor(_) | Attr::BackgroundColor(_) => self.num_colors > 0, + _ => { + let cap = cap_for_attr(attr); + self.ti.strings.get(cap).is_some() + } + } + } + + fn reset(&mut self) -> io::Result { + // are there any terminals that have color/attrs and not sgr0? + // Try falling back to sgr, then op + let cmd = match ["sgr0", "sgr", "op"] + .iter() + .filter_map(|cap| self.ti.strings.get(*cap)) + .next() { + Some(op) => { + match expand(&op, &[], &mut Variables::new()) { + Ok(cmd) => cmd, + Err(e) => return Err(io::Error::new(io::ErrorKind::InvalidData, e)), + } + } + None => return Ok(false), + }; + self.out.write_all(&cmd).and(Ok(true)) + } + + fn get_ref(&self) -> &T { + &self.out + } + + fn get_mut(&mut self) -> &mut T { + &mut self.out + } + + fn into_inner(self) -> T + where Self: Sized + { + self.out + } +} + +impl TerminfoTerminal { + /// Creates a new TerminfoTerminal with the given TermInfo and Write. + pub fn new_with_terminfo(out: T, terminfo: TermInfo) -> TerminfoTerminal { + let nc = if terminfo.strings.contains_key("setaf") && + terminfo.strings.contains_key("setab") { + terminfo.numbers.get("colors").map_or(0, |&n| n) + } else { + 0 + }; + + TerminfoTerminal { + out, + ti: terminfo, + num_colors: nc, + } + } + + /// Creates a new TerminfoTerminal for the current environment with the given Write. + /// + /// Returns `None` when the terminfo cannot be found or parsed. + pub fn new(out: T) -> Option> { + TermInfo::from_env().map(move |ti| TerminfoTerminal::new_with_terminfo(out, ti)).ok() + } + + fn dim_if_necessary(&self, color: color::Color) -> color::Color { + if color >= self.num_colors && color >= 8 && color < 16 { + color - 8 + } else { + color + } + } + + fn apply_cap(&mut self, cmd: &str, params: &[Param]) -> io::Result { + match self.ti.strings.get(cmd) { + Some(cmd) => { + match expand(&cmd, params, &mut Variables::new()) { + Ok(s) => self.out.write_all(&s).and(Ok(true)), + Err(e) => Err(io::Error::new(io::ErrorKind::InvalidData, e)), + } + } + None => Ok(false), + } + } +} + + +impl Write for TerminfoTerminal { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.out.write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + self.out.flush() + } +} diff --git a/src/libterm/terminfo/parm.rs b/src/libterm/terminfo/parm.rs new file mode 100644 index 00000000000..28229bd87b0 --- /dev/null +++ b/src/libterm/terminfo/parm.rs @@ -0,0 +1,669 @@ +//! Parameterized string expansion + +use self::Param::*; +use self::States::*; + +use std::iter::repeat; + +#[derive(Clone, Copy, PartialEq)] +enum States { + Nothing, + Percent, + SetVar, + GetVar, + PushParam, + CharConstant, + CharClose, + IntConstant(i32), + FormatPattern(Flags, FormatState), + SeekIfElse(usize), + SeekIfElsePercent(usize), + SeekIfEnd(usize), + SeekIfEndPercent(usize), +} + +#[derive(Copy, PartialEq, Clone)] +enum FormatState { + Flags, + Width, + Precision, +} + +/// Types of parameters a capability can use +#[allow(missing_docs)] +#[derive(Clone)] +pub enum Param { + Words(String), + Number(i32), +} + +/// Container for static and dynamic variable arrays +pub struct Variables { + /// Static variables A-Z + sta_va: [Param; 26], + /// Dynamic variables a-z + dyn_va: [Param; 26], +} + +impl Variables { + /// Returns a new zero-initialized Variables + pub fn new() -> Variables { + Variables { + sta_va: [ + Number(0), Number(0), Number(0), Number(0), Number(0), Number(0), Number(0), + Number(0), Number(0), Number(0), Number(0), Number(0), Number(0), Number(0), + Number(0), Number(0), Number(0), Number(0), Number(0), Number(0), Number(0), + Number(0), Number(0), Number(0), Number(0), Number(0) + ], + dyn_va: [ + Number(0), Number(0), Number(0), Number(0), Number(0), Number(0), Number(0), + Number(0), Number(0), Number(0), Number(0), Number(0), Number(0), Number(0), + Number(0), Number(0), Number(0), Number(0), Number(0), Number(0), Number(0), + Number(0), Number(0), Number(0), Number(0), Number(0) + ], + } + } +} + +/// Expand a parameterized capability +/// +/// # Arguments +/// * `cap` - string to expand +/// * `params` - vector of params for %p1 etc +/// * `vars` - Variables struct for %Pa etc +/// +/// To be compatible with ncurses, `vars` should be the same between calls to `expand` for +/// multiple capabilities for the same terminal. +pub fn expand(cap: &[u8], params: &[Param], vars: &mut Variables) -> Result, String> { + let mut state = Nothing; + + // expanded cap will only rarely be larger than the cap itself + let mut output = Vec::with_capacity(cap.len()); + + let mut stack: Vec = Vec::new(); + + // Copy parameters into a local vector for mutability + let mut mparams = [Number(0), Number(0), Number(0), Number(0), Number(0), Number(0), + Number(0), Number(0), Number(0)]; + for (dst, src) in mparams.iter_mut().zip(params.iter()) { + *dst = (*src).clone(); + } + + for &c in cap.iter() { + let cur = c as char; + let mut old_state = state; + match state { + Nothing => { + if cur == '%' { + state = Percent; + } else { + output.push(c); + } + } + Percent => { + match cur { + '%' => { + output.push(c); + state = Nothing + } + 'c' => { + match stack.pop() { + // if c is 0, use 0200 (128) for ncurses compatibility + Some(Number(0)) => output.push(128u8), + // Don't check bounds. ncurses just casts and truncates. + Some(Number(c)) => output.push(c as u8), + Some(_) => return Err("a non-char was used with %c".to_string()), + None => return Err("stack is empty".to_string()), + } + } + 'p' => state = PushParam, + 'P' => state = SetVar, + 'g' => state = GetVar, + '\'' => state = CharConstant, + '{' => state = IntConstant(0), + 'l' => { + match stack.pop() { + Some(Words(s)) => stack.push(Number(s.len() as i32)), + Some(_) => return Err("a non-str was used with %l".to_string()), + None => return Err("stack is empty".to_string()), + } + } + '+' | '-' | '/' | '*' | '^' | '&' | '|' | 'm' => { + match (stack.pop(), stack.pop()) { + (Some(Number(y)), Some(Number(x))) => { + stack.push(Number(match cur { + '+' => x + y, + '-' => x - y, + '*' => x * y, + '/' => x / y, + '|' => x | y, + '&' => x & y, + '^' => x ^ y, + 'm' => x % y, + _ => unreachable!("All cases handled"), + })) + } + (Some(_), Some(_)) => { + return Err(format!("non-numbers on stack with {}", cur)) + } + _ => return Err("stack is empty".to_string()), + } + } + '=' | '>' | '<' | 'A' | 'O' => { + match (stack.pop(), stack.pop()) { + (Some(Number(y)), Some(Number(x))) => { + stack.push(Number(if match cur { + '=' => x == y, + '<' => x < y, + '>' => x > y, + 'A' => x > 0 && y > 0, + 'O' => x > 0 || y > 0, + _ => unreachable!(), + } { + 1 + } else { + 0 + })) + } + (Some(_), Some(_)) => { + return Err(format!("non-numbers on stack with {}", cur)) + } + _ => return Err("stack is empty".to_string()), + } + } + '!' | '~' => { + match stack.pop() { + Some(Number(x)) => { + stack.push(Number(match cur { + '!' if x > 0 => 0, + '!' => 1, + '~' => !x, + _ => unreachable!(), + })) + } + Some(_) => return Err(format!("non-numbers on stack with {}", cur)), + None => return Err("stack is empty".to_string()), + } + } + 'i' => { + match (&mparams[0], &mparams[1]) { + (&Number(x), &Number(y)) => { + mparams[0] = Number(x + 1); + mparams[1] = Number(y + 1); + } + _ => { + return Err("first two params not numbers with %i".to_string()) + } + } + } + + // printf-style support for %doxXs + 'd' | 'o' | 'x' | 'X' | 's' => { + if let Some(arg) = stack.pop() { + let flags = Flags::new(); + let res = format(arg, FormatOp::from_char(cur), flags)?; + output.extend(res.iter().cloned()); + } else { + return Err("stack is empty".to_string()); + } + } + ':' | '#' | ' ' | '.' | '0'..='9' => { + let mut flags = Flags::new(); + let mut fstate = FormatState::Flags; + match cur { + ':' => (), + '#' => flags.alternate = true, + ' ' => flags.space = true, + '.' => fstate = FormatState::Precision, + '0'..='9' => { + flags.width = cur as usize - '0' as usize; + fstate = FormatState::Width; + } + _ => unreachable!(), + } + state = FormatPattern(flags, fstate); + } + + // conditionals + '?' => (), + 't' => { + match stack.pop() { + Some(Number(0)) => state = SeekIfElse(0), + Some(Number(_)) => (), + Some(_) => { + return Err("non-number on stack with conditional".to_string()) + } + None => return Err("stack is empty".to_string()), + } + } + 'e' => state = SeekIfEnd(0), + ';' => (), + _ => return Err(format!("unrecognized format option {}", cur)), + } + } + PushParam => { + // params are 1-indexed + stack.push(mparams[match cur.to_digit(10) { + Some(d) => d as usize - 1, + None => return Err("bad param number".to_string()), + }] + .clone()); + } + SetVar => { + if cur >= 'A' && cur <= 'Z' { + if let Some(arg) = stack.pop() { + let idx = (cur as u8) - b'A'; + vars.sta_va[idx as usize] = arg; + } else { + return Err("stack is empty".to_string()); + } + } else if cur >= 'a' && cur <= 'z' { + if let Some(arg) = stack.pop() { + let idx = (cur as u8) - b'a'; + vars.dyn_va[idx as usize] = arg; + } else { + return Err("stack is empty".to_string()); + } + } else { + return Err("bad variable name in %P".to_string()); + } + } + GetVar => { + if cur >= 'A' && cur <= 'Z' { + let idx = (cur as u8) - b'A'; + stack.push(vars.sta_va[idx as usize].clone()); + } else if cur >= 'a' && cur <= 'z' { + let idx = (cur as u8) - b'a'; + stack.push(vars.dyn_va[idx as usize].clone()); + } else { + return Err("bad variable name in %g".to_string()); + } + } + CharConstant => { + stack.push(Number(c as i32)); + state = CharClose; + } + CharClose => { + if cur != '\'' { + return Err("malformed character constant".to_string()); + } + } + IntConstant(i) => { + if cur == '}' { + stack.push(Number(i)); + state = Nothing; + } else if let Some(digit) = cur.to_digit(10) { + match i.checked_mul(10).and_then(|i_ten| i_ten.checked_add(digit as i32)) { + Some(i) => { + state = IntConstant(i); + old_state = Nothing; + } + None => return Err("int constant too large".to_string()), + } + } else { + return Err("bad int constant".to_string()); + } + } + FormatPattern(ref mut flags, ref mut fstate) => { + old_state = Nothing; + match (*fstate, cur) { + (_, 'd') | (_, 'o') | (_, 'x') | (_, 'X') | (_, 's') => { + if let Some(arg) = stack.pop() { + let res = format(arg, FormatOp::from_char(cur), *flags)?; + output.extend(res.iter().cloned()); + // will cause state to go to Nothing + old_state = FormatPattern(*flags, *fstate); + } else { + return Err("stack is empty".to_string()); + } + } + (FormatState::Flags, '#') => { + flags.alternate = true; + } + (FormatState::Flags, '-') => { + flags.left = true; + } + (FormatState::Flags, '+') => { + flags.sign = true; + } + (FormatState::Flags, ' ') => { + flags.space = true; + } + (FormatState::Flags, '0'..='9') => { + flags.width = cur as usize - '0' as usize; + *fstate = FormatState::Width; + } + (FormatState::Flags, '.') => { + *fstate = FormatState::Precision; + } + (FormatState::Width, '0'..='9') => { + let old = flags.width; + flags.width = flags.width * 10 + (cur as usize - '0' as usize); + if flags.width < old { + return Err("format width overflow".to_string()); + } + } + (FormatState::Width, '.') => { + *fstate = FormatState::Precision; + } + (FormatState::Precision, '0'..='9') => { + let old = flags.precision; + flags.precision = flags.precision * 10 + (cur as usize - '0' as usize); + if flags.precision < old { + return Err("format precision overflow".to_string()); + } + } + _ => return Err("invalid format specifier".to_string()), + } + } + SeekIfElse(level) => { + if cur == '%' { + state = SeekIfElsePercent(level); + } + old_state = Nothing; + } + SeekIfElsePercent(level) => { + if cur == ';' { + if level == 0 { + state = Nothing; + } else { + state = SeekIfElse(level - 1); + } + } else if cur == 'e' && level == 0 { + state = Nothing; + } else if cur == '?' { + state = SeekIfElse(level + 1); + } else { + state = SeekIfElse(level); + } + } + SeekIfEnd(level) => { + if cur == '%' { + state = SeekIfEndPercent(level); + } + old_state = Nothing; + } + SeekIfEndPercent(level) => { + if cur == ';' { + if level == 0 { + state = Nothing; + } else { + state = SeekIfEnd(level - 1); + } + } else if cur == '?' { + state = SeekIfEnd(level + 1); + } else { + state = SeekIfEnd(level); + } + } + } + if state == old_state { + state = Nothing; + } + } + Ok(output) +} + +#[derive(Copy, PartialEq, Clone)] +struct Flags { + width: usize, + precision: usize, + alternate: bool, + left: bool, + sign: bool, + space: bool, +} + +impl Flags { + fn new() -> Flags { + Flags { + width: 0, + precision: 0, + alternate: false, + left: false, + sign: false, + space: false, + } + } +} + +#[derive(Copy, Clone)] +enum FormatOp { + Digit, + Octal, + LowerHex, + UpperHex, + String, +} + +impl FormatOp { + fn from_char(c: char) -> FormatOp { + match c { + 'd' => FormatOp::Digit, + 'o' => FormatOp::Octal, + 'x' => FormatOp::LowerHex, + 'X' => FormatOp::UpperHex, + 's' => FormatOp::String, + _ => panic!("bad FormatOp char"), + } + } + fn to_char(self) -> char { + match self { + FormatOp::Digit => 'd', + FormatOp::Octal => 'o', + FormatOp::LowerHex => 'x', + FormatOp::UpperHex => 'X', + FormatOp::String => 's', + } + } +} + +fn format(val: Param, op: FormatOp, flags: Flags) -> Result, String> { + let mut s = match val { + Number(d) => { + match op { + FormatOp::Digit => { + if flags.sign { + format!("{:+01$}", d, flags.precision) + } else if d < 0 { + // C doesn't take sign into account in precision calculation. + format!("{:01$}", d, flags.precision + 1) + } else if flags.space { + format!(" {:01$}", d, flags.precision) + } else { + format!("{:01$}", d, flags.precision) + } + } + FormatOp::Octal => { + if flags.alternate { + // Leading octal zero counts against precision. + format!("0{:01$o}", d, flags.precision.saturating_sub(1)) + } else { + format!("{:01$o}", d, flags.precision) + } + } + FormatOp::LowerHex => { + if flags.alternate && d != 0 { + format!("0x{:01$x}", d, flags.precision) + } else { + format!("{:01$x}", d, flags.precision) + } + } + FormatOp::UpperHex => { + if flags.alternate && d != 0 { + format!("0X{:01$X}", d, flags.precision) + } else { + format!("{:01$X}", d, flags.precision) + } + } + FormatOp::String => return Err("non-number on stack with %s".to_string()), + } + .into_bytes() + } + Words(s) => { + match op { + FormatOp::String => { + let mut s = s.into_bytes(); + if flags.precision > 0 && flags.precision < s.len() { + s.truncate(flags.precision); + } + s + } + _ => return Err(format!("non-string on stack with %{}", op.to_char())), + } + } + }; + if flags.width > s.len() { + let n = flags.width - s.len(); + if flags.left { + s.extend(repeat(b' ').take(n)); + } else { + let mut s_ = Vec::with_capacity(flags.width); + s_.extend(repeat(b' ').take(n)); + s_.extend(s.into_iter()); + s = s_; + } + } + Ok(s) +} + +#[cfg(test)] +mod test { + use super::{expand, Variables}; + use super::Param::{self, Words, Number}; + use std::result::Result::Ok; + + #[test] + fn test_basic_setabf() { + let s = b"\\E[48;5;%p1%dm"; + assert_eq!(expand(s, &[Number(1)], &mut Variables::new()).unwrap(), + "\\E[48;5;1m".bytes().collect::>()); + } + + #[test] + fn test_multiple_int_constants() { + assert_eq!(expand(b"%{1}%{2}%d%d", &[], &mut Variables::new()).unwrap(), + "21".bytes().collect::>()); + } + + #[test] + fn test_op_i() { + let mut vars = Variables::new(); + assert_eq!(expand(b"%p1%d%p2%d%p3%d%i%p1%d%p2%d%p3%d", + &[Number(1), Number(2), Number(3)], + &mut vars), + Ok("123233".bytes().collect::>())); + assert_eq!(expand(b"%p1%d%p2%d%i%p1%d%p2%d", &[], &mut vars), + Ok("0011".bytes().collect::>())); + } + + #[test] + fn test_param_stack_failure_conditions() { + let mut varstruct = Variables::new(); + let vars = &mut varstruct; + fn get_res(fmt: &str, + cap: &str, + params: &[Param], + vars: &mut Variables) + -> Result, String> { + let mut u8v: Vec<_> = fmt.bytes().collect(); + u8v.extend(cap.as_bytes().iter().map(|&b| b)); + expand(&u8v, params, vars) + } + + let caps = ["%d", "%c", "%s", "%Pa", "%l", "%!", "%~"]; + for &cap in caps.iter() { + let res = get_res("", cap, &[], vars); + assert!(res.is_err(), + "Op {} succeeded incorrectly with 0 stack entries", + cap); + let p = if cap == "%s" || cap == "%l" { + Words("foo".to_string()) + } else { + Number(97) + }; + let res = get_res("%p1", cap, &[p], vars); + assert!(res.is_ok(), + "Op {} failed with 1 stack entry: {}", + cap, + res.unwrap_err()); + } + let caps = ["%+", "%-", "%*", "%/", "%m", "%&", "%|", "%A", "%O"]; + for &cap in caps.iter() { + let res = expand(cap.as_bytes(), &[], vars); + assert!(res.is_err(), + "Binop {} succeeded incorrectly with 0 stack entries", + cap); + let res = get_res("%{1}", cap, &[], vars); + assert!(res.is_err(), + "Binop {} succeeded incorrectly with 1 stack entry", + cap); + let res = get_res("%{1}%{2}", cap, &[], vars); + assert!(res.is_ok(), + "Binop {} failed with 2 stack entries: {}", + cap, + res.unwrap_err()); + } + } + + #[test] + fn test_push_bad_param() { + assert!(expand(b"%pa", &[], &mut Variables::new()).is_err()); + } + + #[test] + fn test_comparison_ops() { + let v = [('<', [1u8, 0u8, 0u8]), ('=', [0u8, 1u8, 0u8]), ('>', [0u8, 0u8, 1u8])]; + for &(op, bs) in v.iter() { + let s = format!("%{{1}}%{{2}}%{}%d", op); + let res = expand(s.as_bytes(), &[], &mut Variables::new()); + assert!(res.is_ok(), res.unwrap_err()); + assert_eq!(res.unwrap(), vec![b'0' + bs[0]]); + let s = format!("%{{1}}%{{1}}%{}%d", op); + let res = expand(s.as_bytes(), &[], &mut Variables::new()); + assert!(res.is_ok(), res.unwrap_err()); + assert_eq!(res.unwrap(), vec![b'0' + bs[1]]); + let s = format!("%{{2}}%{{1}}%{}%d", op); + let res = expand(s.as_bytes(), &[], &mut Variables::new()); + assert!(res.is_ok(), res.unwrap_err()); + assert_eq!(res.unwrap(), vec![b'0' + bs[2]]); + } + } + + #[test] + fn test_conditionals() { + let mut vars = Variables::new(); + let s = b"\\E[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m"; + let res = expand(s, &[Number(1)], &mut vars); + assert!(res.is_ok(), res.unwrap_err()); + assert_eq!(res.unwrap(), "\\E[31m".bytes().collect::>()); + let res = expand(s, &[Number(8)], &mut vars); + assert!(res.is_ok(), res.unwrap_err()); + assert_eq!(res.unwrap(), "\\E[90m".bytes().collect::>()); + let res = expand(s, &[Number(42)], &mut vars); + assert!(res.is_ok(), res.unwrap_err()); + assert_eq!(res.unwrap(), "\\E[38;5;42m".bytes().collect::>()); + } + + #[test] + fn test_format() { + let mut varstruct = Variables::new(); + let vars = &mut varstruct; + assert_eq!(expand(b"%p1%s%p2%2s%p3%2s%p4%.2s", + &[Words("foo".to_string()), + Words("foo".to_string()), + Words("f".to_string()), + Words("foo".to_string())], + vars), + Ok("foofoo ffo".bytes().collect::>())); + assert_eq!(expand(b"%p1%:-4.2s", &[Words("foo".to_string())], vars), + Ok("fo ".bytes().collect::>())); + + assert_eq!(expand(b"%p1%d%p1%.3d%p1%5d%p1%:+d", &[Number(1)], vars), + Ok("1001 1+1".bytes().collect::>())); + assert_eq!(expand(b"%p1%o%p1%#o%p2%6.4x%p2%#6.4X", + &[Number(15), Number(27)], + vars), + Ok("17017 001b0X001B".bytes().collect::>())); + } +} diff --git a/src/libterm/terminfo/parser/compiled.rs b/src/libterm/terminfo/parser/compiled.rs new file mode 100644 index 00000000000..05a8c9420ed --- /dev/null +++ b/src/libterm/terminfo/parser/compiled.rs @@ -0,0 +1,346 @@ +#![allow(non_upper_case_globals, missing_docs)] + +//! ncurses-compatible compiled terminfo format parsing (term(5)) + +use std::collections::HashMap; +use std::io; +use std::io::prelude::*; +use super::super::TermInfo; + +// These are the orders ncurses uses in its compiled format (as of 5.9). Not sure if portable. + +#[rustfmt::skip] +pub static boolfnames: &[&str] = &["auto_left_margin", "auto_right_margin", + "no_esc_ctlc", "ceol_standout_glitch", "eat_newline_glitch", "erase_overstrike", "generic_type", + "hard_copy", "has_meta_key", "has_status_line", "insert_null_glitch", "memory_above", + "memory_below", "move_insert_mode", "move_standout_mode", "over_strike", "status_line_esc_ok", + "dest_tabs_magic_smso", "tilde_glitch", "transparent_underline", "xon_xoff", "needs_xon_xoff", + "prtr_silent", "hard_cursor", "non_rev_rmcup", "no_pad_char", "non_dest_scroll_region", + "can_change", "back_color_erase", "hue_lightness_saturation", "col_addr_glitch", + "cr_cancels_micro_mode", "has_print_wheel", "row_addr_glitch", "semi_auto_right_margin", + "cpi_changes_res", "lpi_changes_res", "backspaces_with_bs", "crt_no_scrolling", + "no_correctly_working_cr", "gnu_has_meta_key", "linefeed_is_newline", "has_hardware_tabs", + "return_does_clr_eol"]; + +#[rustfmt::skip] +pub static boolnames: &[&str] = &["bw", "am", "xsb", "xhp", "xenl", "eo", + "gn", "hc", "km", "hs", "in", "db", "da", "mir", "msgr", "os", "eslok", "xt", "hz", "ul", "xon", + "nxon", "mc5i", "chts", "nrrmc", "npc", "ndscr", "ccc", "bce", "hls", "xhpa", "crxm", "daisy", + "xvpa", "sam", "cpix", "lpix", "OTbs", "OTns", "OTnc", "OTMT", "OTNL", "OTpt", "OTxr"]; + +#[rustfmt::skip] +pub static numfnames: &[&str] = &[ "columns", "init_tabs", "lines", + "lines_of_memory", "magic_cookie_glitch", "padding_baud_rate", "virtual_terminal", + "width_status_line", "num_labels", "label_height", "label_width", "max_attributes", + "maximum_windows", "max_colors", "max_pairs", "no_color_video", "buffer_capacity", + "dot_vert_spacing", "dot_horz_spacing", "max_micro_address", "max_micro_jump", "micro_col_size", + "micro_line_size", "number_of_pins", "output_res_char", "output_res_line", + "output_res_horz_inch", "output_res_vert_inch", "print_rate", "wide_char_size", "buttons", + "bit_image_entwining", "bit_image_type", "magic_cookie_glitch_ul", "carriage_return_delay", + "new_line_delay", "backspace_delay", "horizontal_tab_delay", "number_of_function_keys"]; + +#[rustfmt::skip] +pub static numnames: &[&str] = &[ "cols", "it", "lines", "lm", "xmc", "pb", + "vt", "wsl", "nlab", "lh", "lw", "ma", "wnum", "colors", "pairs", "ncv", "bufsz", "spinv", + "spinh", "maddr", "mjump", "mcs", "mls", "npins", "orc", "orl", "orhi", "orvi", "cps", "widcs", + "btns", "bitwin", "bitype", "UTug", "OTdC", "OTdN", "OTdB", "OTdT", "OTkn"]; + +#[rustfmt::skip] +pub static stringfnames: &[&str] = &[ "back_tab", "bell", "carriage_return", + "change_scroll_region", "clear_all_tabs", "clear_screen", "clr_eol", "clr_eos", + "column_address", "command_character", "cursor_address", "cursor_down", "cursor_home", + "cursor_invisible", "cursor_left", "cursor_mem_address", "cursor_normal", "cursor_right", + "cursor_to_ll", "cursor_up", "cursor_visible", "delete_character", "delete_line", + "dis_status_line", "down_half_line", "enter_alt_charset_mode", "enter_blink_mode", + "enter_bold_mode", "enter_ca_mode", "enter_delete_mode", "enter_dim_mode", "enter_insert_mode", + "enter_secure_mode", "enter_protected_mode", "enter_reverse_mode", "enter_standout_mode", + "enter_underline_mode", "erase_chars", "exit_alt_charset_mode", "exit_attribute_mode", + "exit_ca_mode", "exit_delete_mode", "exit_insert_mode", "exit_standout_mode", + "exit_underline_mode", "flash_screen", "form_feed", "from_status_line", "init_1string", + "init_2string", "init_3string", "init_file", "insert_character", "insert_line", + "insert_padding", "key_backspace", "key_catab", "key_clear", "key_ctab", "key_dc", "key_dl", + "key_down", "key_eic", "key_eol", "key_eos", "key_f0", "key_f1", "key_f10", "key_f2", "key_f3", + "key_f4", "key_f5", "key_f6", "key_f7", "key_f8", "key_f9", "key_home", "key_ic", "key_il", + "key_left", "key_ll", "key_npage", "key_ppage", "key_right", "key_sf", "key_sr", "key_stab", + "key_up", "keypad_local", "keypad_xmit", "lab_f0", "lab_f1", "lab_f10", "lab_f2", "lab_f3", + "lab_f4", "lab_f5", "lab_f6", "lab_f7", "lab_f8", "lab_f9", "meta_off", "meta_on", "newline", + "pad_char", "parm_dch", "parm_delete_line", "parm_down_cursor", "parm_ich", "parm_index", + "parm_insert_line", "parm_left_cursor", "parm_right_cursor", "parm_rindex", "parm_up_cursor", + "pkey_key", "pkey_local", "pkey_xmit", "print_screen", "prtr_off", "prtr_on", "repeat_char", + "reset_1string", "reset_2string", "reset_3string", "reset_file", "restore_cursor", + "row_address", "save_cursor", "scroll_forward", "scroll_reverse", "set_attributes", "set_tab", + "set_window", "tab", "to_status_line", "underline_char", "up_half_line", "init_prog", "key_a1", + "key_a3", "key_b2", "key_c1", "key_c3", "prtr_non", "char_padding", "acs_chars", "plab_norm", + "key_btab", "enter_xon_mode", "exit_xon_mode", "enter_am_mode", "exit_am_mode", "xon_character", + "xoff_character", "ena_acs", "label_on", "label_off", "key_beg", "key_cancel", "key_close", + "key_command", "key_copy", "key_create", "key_end", "key_enter", "key_exit", "key_find", + "key_help", "key_mark", "key_message", "key_move", "key_next", "key_open", "key_options", + "key_previous", "key_print", "key_redo", "key_reference", "key_refresh", "key_replace", + "key_restart", "key_resume", "key_save", "key_suspend", "key_undo", "key_sbeg", "key_scancel", + "key_scommand", "key_scopy", "key_screate", "key_sdc", "key_sdl", "key_select", "key_send", + "key_seol", "key_sexit", "key_sfind", "key_shelp", "key_shome", "key_sic", "key_sleft", + "key_smessage", "key_smove", "key_snext", "key_soptions", "key_sprevious", "key_sprint", + "key_sredo", "key_sreplace", "key_sright", "key_srsume", "key_ssave", "key_ssuspend", + "key_sundo", "req_for_input", "key_f11", "key_f12", "key_f13", "key_f14", "key_f15", "key_f16", + "key_f17", "key_f18", "key_f19", "key_f20", "key_f21", "key_f22", "key_f23", "key_f24", + "key_f25", "key_f26", "key_f27", "key_f28", "key_f29", "key_f30", "key_f31", "key_f32", + "key_f33", "key_f34", "key_f35", "key_f36", "key_f37", "key_f38", "key_f39", "key_f40", + "key_f41", "key_f42", "key_f43", "key_f44", "key_f45", "key_f46", "key_f47", "key_f48", + "key_f49", "key_f50", "key_f51", "key_f52", "key_f53", "key_f54", "key_f55", "key_f56", + "key_f57", "key_f58", "key_f59", "key_f60", "key_f61", "key_f62", "key_f63", "clr_bol", + "clear_margins", "set_left_margin", "set_right_margin", "label_format", "set_clock", + "display_clock", "remove_clock", "create_window", "goto_window", "hangup", "dial_phone", + "quick_dial", "tone", "pulse", "flash_hook", "fixed_pause", "wait_tone", "user0", "user1", + "user2", "user3", "user4", "user5", "user6", "user7", "user8", "user9", "orig_pair", + "orig_colors", "initialize_color", "initialize_pair", "set_color_pair", "set_foreground", + "set_background", "change_char_pitch", "change_line_pitch", "change_res_horz", + "change_res_vert", "define_char", "enter_doublewide_mode", "enter_draft_quality", + "enter_italics_mode", "enter_leftward_mode", "enter_micro_mode", "enter_near_letter_quality", + "enter_normal_quality", "enter_shadow_mode", "enter_subscript_mode", "enter_superscript_mode", + "enter_upward_mode", "exit_doublewide_mode", "exit_italics_mode", "exit_leftward_mode", + "exit_micro_mode", "exit_shadow_mode", "exit_subscript_mode", "exit_superscript_mode", + "exit_upward_mode", "micro_column_address", "micro_down", "micro_left", "micro_right", + "micro_row_address", "micro_up", "order_of_pins", "parm_down_micro", "parm_left_micro", + "parm_right_micro", "parm_up_micro", "select_char_set", "set_bottom_margin", + "set_bottom_margin_parm", "set_left_margin_parm", "set_right_margin_parm", "set_top_margin", + "set_top_margin_parm", "start_bit_image", "start_char_set_def", "stop_bit_image", + "stop_char_set_def", "subscript_characters", "superscript_characters", "these_cause_cr", + "zero_motion", "char_set_names", "key_mouse", "mouse_info", "req_mouse_pos", "get_mouse", + "set_a_foreground", "set_a_background", "pkey_plab", "device_type", "code_set_init", + "set0_des_seq", "set1_des_seq", "set2_des_seq", "set3_des_seq", "set_lr_margin", + "set_tb_margin", "bit_image_repeat", "bit_image_newline", "bit_image_carriage_return", + "color_names", "define_bit_image_region", "end_bit_image_region", "set_color_band", + "set_page_length", "display_pc_char", "enter_pc_charset_mode", "exit_pc_charset_mode", + "enter_scancode_mode", "exit_scancode_mode", "pc_term_options", "scancode_escape", + "alt_scancode_esc", "enter_horizontal_hl_mode", "enter_left_hl_mode", "enter_low_hl_mode", + "enter_right_hl_mode", "enter_top_hl_mode", "enter_vertical_hl_mode", "set_a_attributes", + "set_pglen_inch", "termcap_init2", "termcap_reset", "linefeed_if_not_lf", "backspace_if_not_bs", + "other_non_function_keys", "arrow_key_map", "acs_ulcorner", "acs_llcorner", "acs_urcorner", + "acs_lrcorner", "acs_ltee", "acs_rtee", "acs_btee", "acs_ttee", "acs_hline", "acs_vline", + "acs_plus", "memory_lock", "memory_unlock", "box_chars_1"]; + +#[rustfmt::skip] +pub static stringnames: &[&str] = &[ "cbt", "_", "cr", "csr", "tbc", "clear", + "_", "_", "hpa", "cmdch", "cup", "cud1", "home", "civis", "cub1", "mrcup", "cnorm", "cuf1", + "ll", "cuu1", "cvvis", "dch1", "dl1", "dsl", "hd", "smacs", "blink", "bold", "smcup", "smdc", + "dim", "smir", "invis", "prot", "rev", "smso", "smul", "ech", "rmacs", "sgr0", "rmcup", "rmdc", + "rmir", "rmso", "rmul", "flash", "ff", "fsl", "is1", "is2", "is3", "if", "ich1", "il1", "ip", + "kbs", "ktbc", "kclr", "kctab", "_", "_", "kcud1", "_", "_", "_", "_", "_", "_", "_", "_", "_", + "_", "_", "_", "_", "_", "khome", "_", "_", "kcub1", "_", "knp", "kpp", "kcuf1", "_", "_", + "khts", "_", "rmkx", "smkx", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "rmm", "_", + "_", "pad", "dch", "dl", "cud", "ich", "indn", "il", "cub", "cuf", "rin", "cuu", "pfkey", + "pfloc", "pfx", "mc0", "mc4", "_", "rep", "rs1", "rs2", "rs3", "rf", "rc", "vpa", "sc", "ind", + "ri", "sgr", "_", "wind", "_", "tsl", "uc", "hu", "iprog", "_", "_", "_", "_", "_", "mc5p", + "rmp", "acsc", "pln", "kcbt", "smxon", "rmxon", "smam", "rmam", "xonc", "xoffc", "_", "smln", + "rmln", "_", "kcan", "kclo", "kcmd", "kcpy", "kcrt", "_", "kent", "kext", "kfnd", "khlp", + "kmrk", "kmsg", "kmov", "knxt", "kopn", "kopt", "kprv", "kprt", "krdo", "kref", "krfr", "krpl", + "krst", "kres", "ksav", "kspd", "kund", "kBEG", "kCAN", "kCMD", "kCPY", "kCRT", "_", "_", + "kslt", "kEND", "kEOL", "kEXT", "kFND", "kHLP", "kHOM", "_", "kLFT", "kMSG", "kMOV", "kNXT", + "kOPT", "kPRV", "kPRT", "kRDO", "kRPL", "kRIT", "kRES", "kSAV", "kSPD", "kUND", "rfi", "_", "_", + "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", + "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", + "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", + "dclk", "rmclk", "cwin", "wingo", "_", "dial", "qdial", "_", "_", "hook", "pause", "wait", "_", + "_", "_", "_", "_", "_", "_", "_", "_", "_", "op", "oc", "initc", "initp", "scp", "setf", + "setb", "cpi", "lpi", "chr", "cvr", "defc", "swidm", "sdrfq", "sitm", "slm", "smicm", "snlq", + "snrmq", "sshm", "ssubm", "ssupm", "sum", "rwidm", "ritm", "rlm", "rmicm", "rshm", "rsubm", + "rsupm", "rum", "mhpa", "mcud1", "mcub1", "mcuf1", "mvpa", "mcuu1", "porder", "mcud", "mcub", + "mcuf", "mcuu", "scs", "smgb", "smgbp", "smglp", "smgrp", "smgt", "smgtp", "sbim", "scsd", + "rbim", "rcsd", "subcs", "supcs", "docr", "zerom", "csnm", "kmous", "minfo", "reqmp", "getm", + "setaf", "setab", "pfxl", "devt", "csin", "s0ds", "s1ds", "s2ds", "s3ds", "smglr", "smgtb", + "birep", "binel", "bicr", "colornm", "defbi", "endbi", "setcolor", "slines", "dispc", "smpch", + "rmpch", "smsc", "rmsc", "pctrm", "scesc", "scesa", "ehhlm", "elhlm", "elohlm", "erhlm", + "ethlm", "evhlm", "sgr1", "slength", "OTi2", "OTrs", "OTnl", "OTbs", "OTko", "OTma", "OTG2", + "OTG3", "OTG1", "OTG4", "OTGR", "OTGL", "OTGU", "OTGD", "OTGH", "OTGV", "OTGC", "meml", "memu", + "box1"]; + +fn read_le_u16(r: &mut dyn io::Read) -> io::Result { + let mut b = [0; 2]; + let mut amt = 0; + while amt < b.len() { + match r.read(&mut b[amt..])? { + 0 => return Err(io::Error::new(io::ErrorKind::Other, "end of file")), + n => amt += n, + } + } + Ok((b[0] as u16) | ((b[1] as u16) << 8)) +} + +fn read_byte(r: &mut dyn io::Read) -> io::Result { + match r.bytes().next() { + Some(s) => s, + None => Err(io::Error::new(io::ErrorKind::Other, "end of file")), + } +} + +/// Parse a compiled terminfo entry, using long capability names if `longnames` +/// is true +pub fn parse(file: &mut dyn io::Read, longnames: bool) -> Result { + macro_rules! t( ($e:expr) => ( + match $e { + Ok(e) => e, + Err(e) => return Err(e.to_string()) + } + ) ); + + let (bnames, snames, nnames) = if longnames { + (boolfnames, stringfnames, numfnames) + } else { + (boolnames, stringnames, numnames) + }; + + // Check magic number + let magic = t!(read_le_u16(file)); + if magic != 0x011A { + return Err(format!("invalid magic number: expected {:x}, found {:x}", + 0x011A, + magic)); + } + + // According to the spec, these fields must be >= -1 where -1 means that the feature is not + // supported. Using 0 instead of -1 works because we skip sections with length 0. + macro_rules! read_nonneg { + () => {{ + match t!(read_le_u16(file)) as i16 { + n if n >= 0 => n as usize, + -1 => 0, + _ => return Err("incompatible file: length fields must be >= -1".to_string()), + } + }} + } + + let names_bytes = read_nonneg!(); + let bools_bytes = read_nonneg!(); + let numbers_count = read_nonneg!(); + let string_offsets_count = read_nonneg!(); + let string_table_bytes = read_nonneg!(); + + if names_bytes == 0 { + return Err("incompatible file: names field must be at least 1 byte wide".to_string()); + } + + if bools_bytes > boolnames.len() { + return Err("incompatible file: more booleans than expected".to_string()); + } + + if numbers_count > numnames.len() { + return Err("incompatible file: more numbers than expected".to_string()); + } + + if string_offsets_count > stringnames.len() { + return Err("incompatible file: more string offsets than expected".to_string()); + } + + // don't read NUL + let mut bytes = Vec::new(); + t!(file.take((names_bytes - 1) as u64).read_to_end(&mut bytes)); + let names_str = match String::from_utf8(bytes) { + Ok(s) => s, + Err(_) => return Err("input not utf-8".to_string()), + }; + + let term_names: Vec = names_str.split('|') + .map(|s| s.to_string()) + .collect(); + // consume NUL + if t!(read_byte(file)) != b'\0' { + return Err("incompatible file: missing null terminator for names section".to_string()); + } + + let bools_map: HashMap = t! { + (0..bools_bytes).filter_map(|i| match read_byte(file) { + Err(e) => Some(Err(e)), + Ok(1) => Some(Ok((bnames[i].to_string(), true))), + Ok(_) => None + }).collect() + }; + + if (bools_bytes + names_bytes) % 2 == 1 { + t!(read_byte(file)); // compensate for padding + } + + let numbers_map: HashMap = t! { + (0..numbers_count).filter_map(|i| match read_le_u16(file) { + Ok(0xFFFF) => None, + Ok(n) => Some(Ok((nnames[i].to_string(), n))), + Err(e) => Some(Err(e)) + }).collect() + }; + + let string_map: HashMap> = if string_offsets_count > 0 { + let string_offsets: Vec = t!((0..string_offsets_count) + .map(|_| read_le_u16(file)) + .collect()); + + let mut string_table = Vec::new(); + t!(file.take(string_table_bytes as u64).read_to_end(&mut string_table)); + + t!(string_offsets.into_iter().enumerate().filter(|&(_, offset)| { + // non-entry + offset != 0xFFFF + }).map(|(i, offset)| { + let offset = offset as usize; + + let name = if snames[i] == "_" { + stringfnames[i] + } else { + snames[i] + }; + + if offset == 0xFFFE { + // undocumented: FFFE indicates cap@, which means the capability is not present + // unsure if the handling for this is correct + return Ok((name.to_string(), Vec::new())); + } + + // Find the offset of the NUL we want to go to + let nulpos = string_table[offset..string_table_bytes].iter().position(|&b| b == 0); + match nulpos { + Some(len) => Ok((name.to_string(), string_table[offset..offset + len].to_vec())), + None => Err("invalid file: missing NUL in string_table".to_string()), + } + }).collect()) + } else { + HashMap::new() + }; + + // And that's all there is to it + Ok(TermInfo { + names: term_names, + bools: bools_map, + numbers: numbers_map, + strings: string_map, + }) +} + +/// Creates a dummy TermInfo struct for msys terminals +pub fn msys_terminfo() -> TermInfo { + let mut strings = HashMap::new(); + strings.insert("sgr0".to_string(), b"\x1B[0m".to_vec()); + strings.insert("bold".to_string(), b"\x1B[1m".to_vec()); + strings.insert("setaf".to_string(), b"\x1B[3%p1%dm".to_vec()); + strings.insert("setab".to_string(), b"\x1B[4%p1%dm".to_vec()); + + let mut numbers = HashMap::new(); + numbers.insert("colors".to_string(), 8u16); + + TermInfo { + names: vec!["cygwin".to_string()], // msys is a fork of an older cygwin version + bools: HashMap::new(), + numbers, + strings, + } +} + +#[cfg(test)] +mod test { + + use super::{boolnames, boolfnames, numnames, numfnames, stringnames, stringfnames}; + + #[test] + fn test_veclens() { + assert_eq!(boolfnames.len(), boolnames.len()); + assert_eq!(numfnames.len(), numnames.len()); + assert_eq!(stringfnames.len(), stringnames.len()); + } +} diff --git a/src/libterm/terminfo/searcher.rs b/src/libterm/terminfo/searcher.rs new file mode 100644 index 00000000000..0b17ed36fc8 --- /dev/null +++ b/src/libterm/terminfo/searcher.rs @@ -0,0 +1,84 @@ +//! ncurses-compatible database discovery. +//! +//! Does not support hashed database, only filesystem! + +use std::env; +use std::fs; +use std::path::PathBuf; + +/// Return path to database entry for `term` +#[allow(deprecated)] +pub fn get_dbpath_for_term(term: &str) -> Option { + let mut dirs_to_search = Vec::new(); + let first_char = term.chars().next()?; + + // Find search directory + if let Some(dir) = env::var_os("TERMINFO") { + dirs_to_search.push(PathBuf::from(dir)); + } + + if let Ok(dirs) = env::var("TERMINFO_DIRS") { + for i in dirs.split(':') { + if i == "" { + dirs_to_search.push(PathBuf::from("/usr/share/terminfo")); + } else { + dirs_to_search.push(PathBuf::from(i)); + } + } + } else { + // Found nothing in TERMINFO_DIRS, use the default paths: + // According to /etc/terminfo/README, after looking at + // ~/.terminfo, ncurses will search /etc/terminfo, then + // /lib/terminfo, and eventually /usr/share/terminfo. + // On Haiku the database can be found at /boot/system/data/terminfo + if let Some(mut homedir) = env::home_dir() { + homedir.push(".terminfo"); + dirs_to_search.push(homedir) + } + + dirs_to_search.push(PathBuf::from("/etc/terminfo")); + dirs_to_search.push(PathBuf::from("/lib/terminfo")); + dirs_to_search.push(PathBuf::from("/usr/share/terminfo")); + dirs_to_search.push(PathBuf::from("/boot/system/data/terminfo")); + } + + // Look for the terminal in all of the search directories + for mut p in dirs_to_search { + if fs::metadata(&p).is_ok() { + p.push(&first_char.to_string()); + p.push(&term); + if fs::metadata(&p).is_ok() { + return Some(p); + } + p.pop(); + p.pop(); + + // on some installations the dir is named after the hex of the char + // (e.g., macOS) + p.push(&format!("{:x}", first_char as usize)); + p.push(term); + if fs::metadata(&p).is_ok() { + return Some(p); + } + } + } + None +} + +#[test] +#[ignore = "buildbots don't have ncurses installed and I can't mock everything I need"] +fn test_get_dbpath_for_term() { + // woefully inadequate test coverage + // note: current tests won't work with non-standard terminfo hierarchies (e.g., macOS's) + use std::env; + // FIXME (#9639): This needs to handle non-utf8 paths + fn x(t: &str) -> String { + let p = get_dbpath_for_term(t).expect("no terminfo entry found"); + p.to_str().unwrap().to_string() + } + assert!(x("screen") == "/usr/share/terminfo/s/screen"); + assert!(get_dbpath_for_term("") == None); + env::set_var("TERMINFO_DIRS", ":"); + assert!(x("screen") == "/usr/share/terminfo/s/screen"); + env::remove_var("TERMINFO_DIRS"); +} diff --git a/src/libterm/win.rs b/src/libterm/win.rs new file mode 100644 index 00000000000..6d42b01337e --- /dev/null +++ b/src/libterm/win.rs @@ -0,0 +1,203 @@ +//! Windows console handling + +// FIXME (#13400): this is only a tiny fraction of the Windows console api + +extern crate libc; + +use std::io; +use std::io::prelude::*; + +use crate::Attr; +use crate::color; +use crate::Terminal; + +/// A Terminal implementation that uses the Win32 Console API. +pub struct WinConsole { + buf: T, + def_foreground: color::Color, + def_background: color::Color, + foreground: color::Color, + background: color::Color, +} + +type WORD = u16; +type DWORD = u32; +type BOOL = i32; +type HANDLE = *mut u8; + +#[allow(non_snake_case)] +#[repr(C)] +struct CONSOLE_SCREEN_BUFFER_INFO { + dwSize: [libc::c_short; 2], + dwCursorPosition: [libc::c_short; 2], + wAttributes: WORD, + srWindow: [libc::c_short; 4], + dwMaximumWindowSize: [libc::c_short; 2], +} + +#[allow(non_snake_case)] +#[link(name = "kernel32")] +extern "system" { + fn SetConsoleTextAttribute(handle: HANDLE, attr: WORD) -> BOOL; + fn GetStdHandle(which: DWORD) -> HANDLE; + fn GetConsoleScreenBufferInfo(handle: HANDLE, info: *mut CONSOLE_SCREEN_BUFFER_INFO) -> BOOL; +} + +fn color_to_bits(color: color::Color) -> u16 { + // magic numbers from mingw-w64's wincon.h + + let bits = match color % 8 { + color::BLACK => 0, + color::BLUE => 0x1, + color::GREEN => 0x2, + color::RED => 0x4, + color::YELLOW => 0x2 | 0x4, + color::MAGENTA => 0x1 | 0x4, + color::CYAN => 0x1 | 0x2, + color::WHITE => 0x1 | 0x2 | 0x4, + _ => unreachable!(), + }; + + if color >= 8 { + bits | 0x8 + } else { + bits + } +} + +fn bits_to_color(bits: u16) -> color::Color { + let color = match bits & 0x7 { + 0 => color::BLACK, + 0x1 => color::BLUE, + 0x2 => color::GREEN, + 0x4 => color::RED, + 0x6 => color::YELLOW, + 0x5 => color::MAGENTA, + 0x3 => color::CYAN, + 0x7 => color::WHITE, + _ => unreachable!(), + }; + + color | (bits & 0x8) // copy the hi-intensity bit +} + +impl WinConsole { + fn apply(&mut self) { + let _unused = self.buf.flush(); + let mut accum: WORD = 0; + accum |= color_to_bits(self.foreground); + accum |= color_to_bits(self.background) << 4; + + unsafe { + // Magic -11 means stdout, from + // http://msdn.microsoft.com/en-us/library/windows/desktop/ms683231%28v=vs.85%29.aspx + // + // You may be wondering, "but what about stderr?", and the answer + // to that is that setting terminal attributes on the stdout + // handle also sets them for stderr, since they go to the same + // terminal! Admittedly, this is fragile, since stderr could be + // redirected to a different console. This is good enough for + // rustc though. See #13400. + let out = GetStdHandle(-11i32 as DWORD); + SetConsoleTextAttribute(out, accum); + } + } + + /// Returns `None` whenever the terminal cannot be created for some reason. + pub fn new(out: T) -> io::Result> { + let fg; + let bg; + unsafe { + let mut buffer_info = ::std::mem::uninitialized(); + if GetConsoleScreenBufferInfo(GetStdHandle(-11i32 as DWORD), &mut buffer_info) != 0 { + fg = bits_to_color(buffer_info.wAttributes); + bg = bits_to_color(buffer_info.wAttributes >> 4); + } else { + fg = color::WHITE; + bg = color::BLACK; + } + } + Ok(WinConsole { + buf: out, + def_foreground: fg, + def_background: bg, + foreground: fg, + background: bg, + }) + } +} + +impl Write for WinConsole { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.buf.write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + self.buf.flush() + } +} + +impl Terminal for WinConsole { + type Output = T; + + fn fg(&mut self, color: color::Color) -> io::Result { + self.foreground = color; + self.apply(); + + Ok(true) + } + + fn bg(&mut self, color: color::Color) -> io::Result { + self.background = color; + self.apply(); + + Ok(true) + } + + fn attr(&mut self, attr: Attr) -> io::Result { + match attr { + Attr::ForegroundColor(f) => { + self.foreground = f; + self.apply(); + Ok(true) + } + Attr::BackgroundColor(b) => { + self.background = b; + self.apply(); + Ok(true) + } + _ => Ok(false), + } + } + + fn supports_attr(&self, attr: Attr) -> bool { + // it claims support for underscore and reverse video, but I can't get + // it to do anything -cmr + match attr { + Attr::ForegroundColor(_) | Attr::BackgroundColor(_) => true, + _ => false, + } + } + + fn reset(&mut self) -> io::Result { + self.foreground = self.def_foreground; + self.background = self.def_background; + self.apply(); + + Ok(true) + } + + fn get_ref(&self) -> &T { + &self.buf + } + + fn get_mut(&mut self) -> &mut T { + &mut self.buf + } + + fn into_inner(self) -> T + where Self: Sized + { + self.buf + } +} diff --git a/src/libtest/Cargo.toml b/src/libtest/Cargo.toml index 26ac7888184..10bdd6e877c 100644 --- a/src/libtest/Cargo.toml +++ b/src/libtest/Cargo.toml @@ -10,7 +10,8 @@ path = "lib.rs" crate-type = ["dylib", "rlib"] [dependencies] -libtest = { version = "0.0.1" } +getopts = "0.2" +term = { path = "../libterm" } # not actually used but needed to always have proc_macro in the sysroot proc_macro = { path = "../libproc_macro" } diff --git a/src/libtest/README.md b/src/libtest/README.md deleted file mode 100644 index 6d9fe30dcad..00000000000 --- a/src/libtest/README.md +++ /dev/null @@ -1,13 +0,0 @@ -WIP - stable libtest -=== - -The migration of libtest to stable Rust is currently in progress. - -You can find libtest at: https://github.com/rust-lang/libtest . If you need to -make a change: - -* perform the change there, -* do a new crates.io release, and -* send a PR to rust-lang/rust bumping the libtest version. - -The roadmap of the migration is being tracked here: https://github.com/rust-lang/libtest/issues/2 diff --git a/src/libtest/formatters/json.rs b/src/libtest/formatters/json.rs new file mode 100644 index 00000000000..a06497f9862 --- /dev/null +++ b/src/libtest/formatters/json.rs @@ -0,0 +1,208 @@ +use super::*; + +pub(crate) struct JsonFormatter { + out: OutputLocation, +} + +impl JsonFormatter { + pub fn new(out: OutputLocation) -> Self { + Self { out } + } + + fn write_message(&mut self, s: &str) -> io::Result<()> { + assert!(!s.contains('\n')); + + self.out.write_all(s.as_ref())?; + self.out.write_all(b"\n") + } + + fn write_event( + &mut self, + ty: &str, + name: &str, + evt: &str, + extra: Option, + ) -> io::Result<()> { + if let Some(extras) = extra { + self.write_message(&*format!( + r#"{{ "type": "{}", "name": "{}", "event": "{}", {} }}"#, + ty, name, evt, extras + )) + } else { + self.write_message(&*format!( + r#"{{ "type": "{}", "name": "{}", "event": "{}" }}"#, + ty, name, evt + )) + } + } +} + +impl OutputFormatter for JsonFormatter { + fn write_run_start(&mut self, test_count: usize) -> io::Result<()> { + self.write_message(&*format!( + r#"{{ "type": "suite", "event": "started", "test_count": {} }}"#, + test_count + )) + } + + fn write_test_start(&mut self, desc: &TestDesc) -> io::Result<()> { + self.write_message(&*format!( + r#"{{ "type": "test", "event": "started", "name": "{}" }}"#, + desc.name + )) + } + + fn write_result( + &mut self, + desc: &TestDesc, + result: &TestResult, + stdout: &[u8], + ) -> io::Result<()> { + match *result { + TrOk => self.write_event("test", desc.name.as_slice(), "ok", None), + + TrFailed => { + let extra_data = if stdout.len() > 0 { + Some(format!( + r#""stdout": "{}""#, + EscapedString(String::from_utf8_lossy(stdout)) + )) + } else { + None + }; + + self.write_event("test", desc.name.as_slice(), "failed", extra_data) + } + + TrFailedMsg(ref m) => self.write_event( + "test", + desc.name.as_slice(), + "failed", + Some(format!(r#""message": "{}""#, EscapedString(m))), + ), + + TrIgnored => self.write_event("test", desc.name.as_slice(), "ignored", None), + + TrAllowedFail => { + self.write_event("test", desc.name.as_slice(), "allowed_failure", None) + } + + TrBench(ref bs) => { + let median = bs.ns_iter_summ.median as usize; + let deviation = (bs.ns_iter_summ.max - bs.ns_iter_summ.min) as usize; + + let mbps = if bs.mb_s == 0 { + String::new() + } else { + format!(r#", "mib_per_second": {}"#, bs.mb_s) + }; + + let line = format!( + "{{ \"type\": \"bench\", \ + \"name\": \"{}\", \ + \"median\": {}, \ + \"deviation\": {}{} }}", + desc.name, median, deviation, mbps + ); + + self.write_message(&*line) + } + } + } + + fn write_timeout(&mut self, desc: &TestDesc) -> io::Result<()> { + self.write_message(&*format!( + r#"{{ "type": "test", "event": "timeout", "name": "{}" }}"#, + desc.name + )) + } + + fn write_run_finish(&mut self, state: &ConsoleTestState) -> io::Result { + self.write_message(&*format!( + "{{ \"type\": \"suite\", \ + \"event\": \"{}\", \ + \"passed\": {}, \ + \"failed\": {}, \ + \"allowed_fail\": {}, \ + \"ignored\": {}, \ + \"measured\": {}, \ + \"filtered_out\": {} }}", + if state.failed == 0 { "ok" } else { "failed" }, + state.passed, + state.failed + state.allowed_fail, + state.allowed_fail, + state.ignored, + state.measured, + state.filtered_out + ))?; + + Ok(state.failed == 0) + } +} + +/// A formatting utility used to print strings with characters in need of escaping. +/// Base code taken form `libserialize::json::escape_str` +struct EscapedString>(S); + +impl> ::std::fmt::Display for EscapedString { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + let mut start = 0; + + for (i, byte) in self.0.as_ref().bytes().enumerate() { + let escaped = match byte { + b'"' => "\\\"", + b'\\' => "\\\\", + b'\x00' => "\\u0000", + b'\x01' => "\\u0001", + b'\x02' => "\\u0002", + b'\x03' => "\\u0003", + b'\x04' => "\\u0004", + b'\x05' => "\\u0005", + b'\x06' => "\\u0006", + b'\x07' => "\\u0007", + b'\x08' => "\\b", + b'\t' => "\\t", + b'\n' => "\\n", + b'\x0b' => "\\u000b", + b'\x0c' => "\\f", + b'\r' => "\\r", + b'\x0e' => "\\u000e", + b'\x0f' => "\\u000f", + b'\x10' => "\\u0010", + b'\x11' => "\\u0011", + b'\x12' => "\\u0012", + b'\x13' => "\\u0013", + b'\x14' => "\\u0014", + b'\x15' => "\\u0015", + b'\x16' => "\\u0016", + b'\x17' => "\\u0017", + b'\x18' => "\\u0018", + b'\x19' => "\\u0019", + b'\x1a' => "\\u001a", + b'\x1b' => "\\u001b", + b'\x1c' => "\\u001c", + b'\x1d' => "\\u001d", + b'\x1e' => "\\u001e", + b'\x1f' => "\\u001f", + b'\x7f' => "\\u007f", + _ => { + continue; + } + }; + + if start < i { + f.write_str(&self.0.as_ref()[start..i])?; + } + + f.write_str(escaped)?; + + start = i + 1; + } + + if start != self.0.as_ref().len() { + f.write_str(&self.0.as_ref()[start..])?; + } + + Ok(()) + } +} diff --git a/src/libtest/formatters/mod.rs b/src/libtest/formatters/mod.rs new file mode 100644 index 00000000000..be5f6a65039 --- /dev/null +++ b/src/libtest/formatters/mod.rs @@ -0,0 +1,22 @@ +use super::*; + +mod pretty; +mod json; +mod terse; + +pub(crate) use self::pretty::PrettyFormatter; +pub(crate) use self::json::JsonFormatter; +pub(crate) use self::terse::TerseFormatter; + +pub(crate) trait OutputFormatter { + fn write_run_start(&mut self, test_count: usize) -> io::Result<()>; + fn write_test_start(&mut self, desc: &TestDesc) -> io::Result<()>; + fn write_timeout(&mut self, desc: &TestDesc) -> io::Result<()>; + fn write_result( + &mut self, + desc: &TestDesc, + result: &TestResult, + stdout: &[u8], + ) -> io::Result<()>; + fn write_run_finish(&mut self, state: &ConsoleTestState) -> io::Result; +} diff --git a/src/libtest/formatters/pretty.rs b/src/libtest/formatters/pretty.rs new file mode 100644 index 00000000000..4af00428ca8 --- /dev/null +++ b/src/libtest/formatters/pretty.rs @@ -0,0 +1,232 @@ +use super::*; + +pub(crate) struct PrettyFormatter { + out: OutputLocation, + use_color: bool, + + /// Number of columns to fill when aligning names + max_name_len: usize, + + is_multithreaded: bool, +} + +impl PrettyFormatter { + pub fn new( + out: OutputLocation, + use_color: bool, + max_name_len: usize, + is_multithreaded: bool, + ) -> Self { + PrettyFormatter { + out, + use_color, + max_name_len, + is_multithreaded, + } + } + + #[cfg(test)] + pub fn output_location(&self) -> &OutputLocation { + &self.out + } + + pub fn write_ok(&mut self) -> io::Result<()> { + self.write_short_result("ok", term::color::GREEN) + } + + pub fn write_failed(&mut self) -> io::Result<()> { + self.write_short_result("FAILED", term::color::RED) + } + + pub fn write_ignored(&mut self) -> io::Result<()> { + self.write_short_result("ignored", term::color::YELLOW) + } + + pub fn write_allowed_fail(&mut self) -> io::Result<()> { + self.write_short_result("FAILED (allowed)", term::color::YELLOW) + } + + pub fn write_bench(&mut self) -> io::Result<()> { + self.write_pretty("bench", term::color::CYAN) + } + + pub fn write_short_result( + &mut self, + result: &str, + color: term::color::Color, + ) -> io::Result<()> { + self.write_pretty(result, color)?; + self.write_plain("\n") + } + + pub fn write_pretty(&mut self, word: &str, color: term::color::Color) -> io::Result<()> { + match self.out { + Pretty(ref mut term) => { + if self.use_color { + term.fg(color)?; + } + term.write_all(word.as_bytes())?; + if self.use_color { + term.reset()?; + } + term.flush() + } + Raw(ref mut stdout) => { + stdout.write_all(word.as_bytes())?; + stdout.flush() + } + } + } + + pub fn write_plain>(&mut self, s: S) -> io::Result<()> { + let s = s.as_ref(); + self.out.write_all(s.as_bytes())?; + self.out.flush() + } + + pub fn write_successes(&mut self, state: &ConsoleTestState) -> io::Result<()> { + self.write_plain("\nsuccesses:\n")?; + let mut successes = Vec::new(); + let mut stdouts = String::new(); + for &(ref f, ref stdout) in &state.not_failures { + successes.push(f.name.to_string()); + if !stdout.is_empty() { + stdouts.push_str(&format!("---- {} stdout ----\n", f.name)); + let output = String::from_utf8_lossy(stdout); + stdouts.push_str(&output); + stdouts.push_str("\n"); + } + } + if !stdouts.is_empty() { + self.write_plain("\n")?; + self.write_plain(&stdouts)?; + } + + self.write_plain("\nsuccesses:\n")?; + successes.sort(); + for name in &successes { + self.write_plain(&format!(" {}\n", name))?; + } + Ok(()) + } + + pub fn write_failures(&mut self, state: &ConsoleTestState) -> io::Result<()> { + self.write_plain("\nfailures:\n")?; + let mut failures = Vec::new(); + let mut fail_out = String::new(); + for &(ref f, ref stdout) in &state.failures { + failures.push(f.name.to_string()); + if !stdout.is_empty() { + fail_out.push_str(&format!("---- {} stdout ----\n", f.name)); + let output = String::from_utf8_lossy(stdout); + fail_out.push_str(&output); + fail_out.push_str("\n"); + } + } + if !fail_out.is_empty() { + self.write_plain("\n")?; + self.write_plain(&fail_out)?; + } + + self.write_plain("\nfailures:\n")?; + failures.sort(); + for name in &failures { + self.write_plain(&format!(" {}\n", name))?; + } + Ok(()) + } + + fn write_test_name(&mut self, desc: &TestDesc) -> io::Result<()> { + let name = desc.padded_name(self.max_name_len, desc.name.padding()); + self.write_plain(&format!("test {} ... ", name))?; + + Ok(()) + } +} + +impl OutputFormatter for PrettyFormatter { + fn write_run_start(&mut self, test_count: usize) -> io::Result<()> { + let noun = if test_count != 1 { "tests" } else { "test" }; + self.write_plain(&format!("\nrunning {} {}\n", test_count, noun)) + } + + fn write_test_start(&mut self, desc: &TestDesc) -> io::Result<()> { + // When running tests concurrently, we should not print + // the test's name as the result will be mis-aligned. + // When running the tests serially, we print the name here so + // that the user can see which test hangs. + if !self.is_multithreaded { + self.write_test_name(desc)?; + } + + Ok(()) + } + + fn write_result(&mut self, desc: &TestDesc, result: &TestResult, _: &[u8]) -> io::Result<()> { + if self.is_multithreaded { + self.write_test_name(desc)?; + } + + match *result { + TrOk => self.write_ok(), + TrFailed | TrFailedMsg(_) => self.write_failed(), + TrIgnored => self.write_ignored(), + TrAllowedFail => self.write_allowed_fail(), + TrBench(ref bs) => { + self.write_bench()?; + self.write_plain(&format!(": {}\n", fmt_bench_samples(bs))) + } + } + } + + fn write_timeout(&mut self, desc: &TestDesc) -> io::Result<()> { + if self.is_multithreaded { + self.write_test_name(desc)?; + } + + self.write_plain(&format!( + "test {} has been running for over {} seconds\n", + desc.name, TEST_WARN_TIMEOUT_S + )) + } + + fn write_run_finish(&mut self, state: &ConsoleTestState) -> io::Result { + if state.options.display_output { + self.write_successes(state)?; + } + let success = state.failed == 0; + if !success { + self.write_failures(state)?; + } + + self.write_plain("\ntest result: ")?; + + if success { + // There's no parallelism at this point so it's safe to use color + self.write_pretty("ok", term::color::GREEN)?; + } else { + self.write_pretty("FAILED", term::color::RED)?; + } + + let s = if state.allowed_fail > 0 { + format!( + ". {} passed; {} failed ({} allowed); {} ignored; {} measured; {} filtered out\n\n", + state.passed, + state.failed + state.allowed_fail, + state.allowed_fail, + state.ignored, + state.measured, + state.filtered_out + ) + } else { + format!( + ". {} passed; {} failed; {} ignored; {} measured; {} filtered out\n\n", + state.passed, state.failed, state.ignored, state.measured, state.filtered_out + ) + }; + + self.write_plain(&s)?; + + Ok(success) + } +} diff --git a/src/libtest/formatters/terse.rs b/src/libtest/formatters/terse.rs new file mode 100644 index 00000000000..1400fba5d60 --- /dev/null +++ b/src/libtest/formatters/terse.rs @@ -0,0 +1,235 @@ +use super::*; + +pub(crate) struct TerseFormatter { + out: OutputLocation, + use_color: bool, + is_multithreaded: bool, + /// Number of columns to fill when aligning names + max_name_len: usize, + + test_count: usize, + total_test_count: usize, +} + +impl TerseFormatter { + pub fn new( + out: OutputLocation, + use_color: bool, + max_name_len: usize, + is_multithreaded: bool, + ) -> Self { + TerseFormatter { + out, + use_color, + max_name_len, + is_multithreaded, + test_count: 0, + total_test_count: 0, // initialized later, when write_run_start is called + } + } + + pub fn write_ok(&mut self) -> io::Result<()> { + self.write_short_result(".", term::color::GREEN) + } + + pub fn write_failed(&mut self) -> io::Result<()> { + self.write_short_result("F", term::color::RED) + } + + pub fn write_ignored(&mut self) -> io::Result<()> { + self.write_short_result("i", term::color::YELLOW) + } + + pub fn write_allowed_fail(&mut self) -> io::Result<()> { + self.write_short_result("a", term::color::YELLOW) + } + + pub fn write_bench(&mut self) -> io::Result<()> { + self.write_pretty("bench", term::color::CYAN) + } + + pub fn write_short_result( + &mut self, + result: &str, + color: term::color::Color, + ) -> io::Result<()> { + self.write_pretty(result, color)?; + if self.test_count % QUIET_MODE_MAX_COLUMN == QUIET_MODE_MAX_COLUMN - 1 { + // we insert a new line every 100 dots in order to flush the + // screen when dealing with line-buffered output (e.g., piping to + // `stamp` in the rust CI). + let out = format!(" {}/{}\n", self.test_count+1, self.total_test_count); + self.write_plain(&out)?; + } + + self.test_count += 1; + Ok(()) + } + + pub fn write_pretty(&mut self, word: &str, color: term::color::Color) -> io::Result<()> { + match self.out { + Pretty(ref mut term) => { + if self.use_color { + term.fg(color)?; + } + term.write_all(word.as_bytes())?; + if self.use_color { + term.reset()?; + } + term.flush() + } + Raw(ref mut stdout) => { + stdout.write_all(word.as_bytes())?; + stdout.flush() + } + } + } + + pub fn write_plain>(&mut self, s: S) -> io::Result<()> { + let s = s.as_ref(); + self.out.write_all(s.as_bytes())?; + self.out.flush() + } + + pub fn write_outputs(&mut self, state: &ConsoleTestState) -> io::Result<()> { + self.write_plain("\nsuccesses:\n")?; + let mut successes = Vec::new(); + let mut stdouts = String::new(); + for &(ref f, ref stdout) in &state.not_failures { + successes.push(f.name.to_string()); + if !stdout.is_empty() { + stdouts.push_str(&format!("---- {} stdout ----\n", f.name)); + let output = String::from_utf8_lossy(stdout); + stdouts.push_str(&output); + stdouts.push_str("\n"); + } + } + if !stdouts.is_empty() { + self.write_plain("\n")?; + self.write_plain(&stdouts)?; + } + + self.write_plain("\nsuccesses:\n")?; + successes.sort(); + for name in &successes { + self.write_plain(&format!(" {}\n", name))?; + } + Ok(()) + } + + pub fn write_failures(&mut self, state: &ConsoleTestState) -> io::Result<()> { + self.write_plain("\nfailures:\n")?; + let mut failures = Vec::new(); + let mut fail_out = String::new(); + for &(ref f, ref stdout) in &state.failures { + failures.push(f.name.to_string()); + if !stdout.is_empty() { + fail_out.push_str(&format!("---- {} stdout ----\n", f.name)); + let output = String::from_utf8_lossy(stdout); + fail_out.push_str(&output); + fail_out.push_str("\n"); + } + } + if !fail_out.is_empty() { + self.write_plain("\n")?; + self.write_plain(&fail_out)?; + } + + self.write_plain("\nfailures:\n")?; + failures.sort(); + for name in &failures { + self.write_plain(&format!(" {}\n", name))?; + } + Ok(()) + } + + fn write_test_name(&mut self, desc: &TestDesc) -> io::Result<()> { + let name = desc.padded_name(self.max_name_len, desc.name.padding()); + self.write_plain(&format!("test {} ... ", name))?; + + Ok(()) + } +} + +impl OutputFormatter for TerseFormatter { + fn write_run_start(&mut self, test_count: usize) -> io::Result<()> { + self.total_test_count = test_count; + let noun = if test_count != 1 { "tests" } else { "test" }; + self.write_plain(&format!("\nrunning {} {}\n", test_count, noun)) + } + + fn write_test_start(&mut self, desc: &TestDesc) -> io::Result<()> { + // Remnants from old libtest code that used the padding value + // in order to indicate benchmarks. + // When running benchmarks, terse-mode should still print their name as if + // it is the Pretty formatter. + if !self.is_multithreaded && desc.name.padding() == PadOnRight { + self.write_test_name(desc)?; + } + + Ok(()) + } + + fn write_result(&mut self, desc: &TestDesc, result: &TestResult, _: &[u8]) -> io::Result<()> { + match *result { + TrOk => self.write_ok(), + TrFailed | TrFailedMsg(_) => self.write_failed(), + TrIgnored => self.write_ignored(), + TrAllowedFail => self.write_allowed_fail(), + TrBench(ref bs) => { + if self.is_multithreaded { + self.write_test_name(desc)?; + } + self.write_bench()?; + self.write_plain(&format!(": {}\n", fmt_bench_samples(bs))) + } + } + } + + fn write_timeout(&mut self, desc: &TestDesc) -> io::Result<()> { + self.write_plain(&format!( + "test {} has been running for over {} seconds\n", + desc.name, TEST_WARN_TIMEOUT_S + )) + } + + fn write_run_finish(&mut self, state: &ConsoleTestState) -> io::Result { + if state.options.display_output { + self.write_outputs(state)?; + } + let success = state.failed == 0; + if !success { + self.write_failures(state)?; + } + + self.write_plain("\ntest result: ")?; + + if success { + // There's no parallelism at this point so it's safe to use color + self.write_pretty("ok", term::color::GREEN)?; + } else { + self.write_pretty("FAILED", term::color::RED)?; + } + + let s = if state.allowed_fail > 0 { + format!( + ". {} passed; {} failed ({} allowed); {} ignored; {} measured; {} filtered out\n\n", + state.passed, + state.failed + state.allowed_fail, + state.allowed_fail, + state.ignored, + state.measured, + state.filtered_out + ) + } else { + format!( + ". {} passed; {} failed; {} ignored; {} measured; {} filtered out\n\n", + state.passed, state.failed, state.ignored, state.measured, state.filtered_out + ) + }; + + self.write_plain(&s)?; + + Ok(success) + } +} diff --git a/src/libtest/lib.rs b/src/libtest/lib.rs index 5c91c0ec43b..26612964c30 100644 --- a/src/libtest/lib.rs +++ b/src/libtest/lib.rs @@ -8,48 +8,2224 @@ //! //! See the [Testing Chapter](../book/ch11-00-testing.html) of the book for more details. +// Currently, not much of this is meant for users. It is intended to +// support the simplest interface possible for representing and +// running tests while providing a base that other test frameworks may +// build off of. + +// N.B., this is also specified in this crate's Cargo.toml, but libsyntax contains logic specific to +// this crate, which relies on this attribute (rather than the value of `--crate-name` passed by +// cargo) to detect this crate. + +#![deny(rust_2018_idioms)] #![crate_name = "test"] #![unstable(feature = "test", issue = "27812")] -#![doc(html_root_url = "https://doc.rust-lang.org/nightly/", - test(attr(deny(warnings))))] +#![doc(html_root_url = "https://doc.rust-lang.org/nightly/", test(attr(deny(warnings))))] #![feature(asm)] +#![feature(fnbox)] +#![cfg_attr(any(unix, target_os = "cloudabi"), feature(libc, rustc_private))] +#![feature(nll)] +#![feature(set_stdio)] +#![feature(panic_unwind)] #![feature(staged_api)] +#![feature(termination_trait_lib)] #![feature(test)] -extern crate libtest; +use getopts; +#[cfg(any(unix, target_os = "cloudabi"))] +extern crate libc; +use term; -// FIXME: we should be more explicit about the exact APIs that we -// export to users. -pub use libtest::{ - assert_test_result, filter_tests, parse_opts, run_test, test_main, test_main_static, - Bencher, DynTestFn, DynTestName, Metric, MetricMap, Options, RunIgnored, ShouldPanic, - StaticBenchFn, StaticTestFn, StaticTestName, TestDesc, TestDescAndFn, TestName, TestOpts, - TestResult, TrFailed, TrFailedMsg, TrIgnored, TrOk, stats::Summary -}; +// FIXME(#54291): rustc and/or LLVM don't yet support building with panic-unwind +// on aarch64-pc-windows-msvc, so we don't link libtest against +// libunwind (for the time being), even though it means that +// libtest won't be fully functional on this platform. +// +// See also: https://github.com/rust-lang/rust/issues/54190#issuecomment-422904437 +#[cfg(not(all(windows, target_arch = "aarch64")))] +extern crate panic_unwind; + +pub use self::ColorConfig::*; +use self::NamePadding::*; +use self::OutputLocation::*; +use self::TestEvent::*; +pub use self::TestFn::*; +pub use self::TestName::*; +pub use self::TestResult::*; + +use std::any::Any; +use std::borrow::Cow; +use std::boxed::FnBox; +use std::cmp; +use std::collections::BTreeMap; +use std::env; +use std::fmt; +use std::fs::File; +use std::io; +use std::io::prelude::*; +use std::panic::{catch_unwind, AssertUnwindSafe}; +use std::path::PathBuf; +use std::process; +use std::process::Termination; +use std::sync::mpsc::{channel, Sender}; +use std::sync::{Arc, Mutex}; +use std::thread; +use std::time::{Duration, Instant}; + +const TEST_WARN_TIMEOUT_S: u64 = 60; +const QUIET_MODE_MAX_COLUMN: usize = 100; // insert a '\n' after 100 tests in quiet mode + +// to be used by rustc to compile tests in libtest +pub mod test { + pub use crate::{ + assert_test_result, filter_tests, parse_opts, run_test, test_main, test_main_static, + Bencher, DynTestFn, DynTestName, Metric, MetricMap, Options, RunIgnored, ShouldPanic, + StaticBenchFn, StaticTestFn, StaticTestName, TestDesc, TestDescAndFn, TestName, TestOpts, + TestResult, TrFailed, TrFailedMsg, TrIgnored, TrOk, + }; +} + +mod formatters; +pub mod stats; + +use crate::formatters::{JsonFormatter, OutputFormatter, PrettyFormatter, TerseFormatter}; + +/// Whether to execute tests concurrently or not +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Concurrent { + Yes, + No, +} + +// The name of a test. By convention this follows the rules for rust +// paths; i.e., it should be a series of identifiers separated by double +// colons. This way if some test runner wants to arrange the tests +// hierarchically it may. + +#[derive(Clone, PartialEq, Eq, Hash, Debug)] +pub enum TestName { + StaticTestName(&'static str), + DynTestName(String), + AlignedTestName(Cow<'static, str>, NamePadding), +} +impl TestName { + fn as_slice(&self) -> &str { + match *self { + StaticTestName(s) => s, + DynTestName(ref s) => s, + AlignedTestName(ref s, _) => &*s, + } + } + + fn padding(&self) -> NamePadding { + match self { + &AlignedTestName(_, p) => p, + _ => PadNone, + } + } + + fn with_padding(&self, padding: NamePadding) -> TestName { + let name = match self { + &TestName::StaticTestName(name) => Cow::Borrowed(name), + &TestName::DynTestName(ref name) => Cow::Owned(name.clone()), + &TestName::AlignedTestName(ref name, _) => name.clone(), + }; + + TestName::AlignedTestName(name, padding) + } +} +impl fmt::Display for TestName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self.as_slice(), f) + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub enum NamePadding { + PadNone, + PadOnRight, +} + +impl TestDesc { + fn padded_name(&self, column_count: usize, align: NamePadding) -> String { + let mut name = String::from(self.name.as_slice()); + let fill = column_count.saturating_sub(name.len()); + let pad = " ".repeat(fill); + match align { + PadNone => name, + PadOnRight => { + name.push_str(&pad); + name + } + } + } +} + +/// Represents a benchmark function. +pub trait TDynBenchFn: Send { + fn run(&self, harness: &mut Bencher); +} + +// A function that runs a test. If the function returns successfully, +// the test succeeds; if the function panics then the test fails. We +// may need to come up with a more clever definition of test in order +// to support isolation of tests into threads. +pub enum TestFn { + StaticTestFn(fn()), + StaticBenchFn(fn(&mut Bencher)), + DynTestFn(Box), + DynBenchFn(Box), +} + +impl TestFn { + fn padding(&self) -> NamePadding { + match *self { + StaticTestFn(..) => PadNone, + StaticBenchFn(..) => PadOnRight, + DynTestFn(..) => PadNone, + DynBenchFn(..) => PadOnRight, + } + } +} + +impl fmt::Debug for TestFn { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match *self { + StaticTestFn(..) => "StaticTestFn(..)", + StaticBenchFn(..) => "StaticBenchFn(..)", + DynTestFn(..) => "DynTestFn(..)", + DynBenchFn(..) => "DynBenchFn(..)", + }) + } +} + +/// Manager of the benchmarking runs. +/// +/// This is fed into functions marked with `#[bench]` to allow for +/// set-up & tear-down before running a piece of code repeatedly via a +/// call to `iter`. +#[derive(Clone)] +pub struct Bencher { + mode: BenchMode, + summary: Option, + pub bytes: u64, +} + +#[derive(Clone, PartialEq, Eq)] +pub enum BenchMode { + Auto, + Single, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub enum ShouldPanic { + No, + Yes, + YesWithMessage(&'static str), +} + +// The definition of a single test. A test runner will run a list of +// these. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct TestDesc { + pub name: TestName, + pub ignore: bool, + pub should_panic: ShouldPanic, + pub allow_fail: bool, +} + +#[derive(Debug)] +pub struct TestDescAndFn { + pub desc: TestDesc, + pub testfn: TestFn, +} + +#[derive(Clone, PartialEq, Debug, Copy)] +pub struct Metric { + value: f64, + noise: f64, +} + +impl Metric { + pub fn new(value: f64, noise: f64) -> Metric { + Metric { value, noise } + } +} + +/// In case we want to add other options as well, just add them in this struct. +#[derive(Copy, Clone, Debug)] +pub struct Options { + display_output: bool, +} + +impl Options { + pub fn new() -> Options { + Options { + display_output: false, + } + } + + pub fn display_output(mut self, display_output: bool) -> Options { + self.display_output = display_output; + self + } +} + +// The default console test runner. It accepts the command line +// arguments and a vector of test_descs. +pub fn test_main(args: &[String], tests: Vec, options: Options) { + let mut opts = match parse_opts(args) { + Some(Ok(o)) => o, + Some(Err(msg)) => { + eprintln!("error: {}", msg); + process::exit(101); + } + None => return, + }; + + opts.options = options; + if opts.list { + if let Err(e) = list_tests_console(&opts, tests) { + eprintln!("error: io error when listing tests: {:?}", e); + process::exit(101); + } + } else { + match run_tests_console(&opts, tests) { + Ok(true) => {} + Ok(false) => process::exit(101), + Err(e) => { + eprintln!("error: io error when listing tests: {:?}", e); + process::exit(101); + } + } + } +} + +// A variant optimized for invocation with a static test vector. +// This will panic (intentionally) when fed any dynamic tests, because +// it is copying the static values out into a dynamic vector and cannot +// copy dynamic values. It is doing this because from this point on +// a Vec is used in order to effect ownership-transfer +// semantics into parallel test runners, which in turn requires a Vec<> +// rather than a &[]. +pub fn test_main_static(tests: &[&TestDescAndFn]) { + let args = env::args().collect::>(); + let owned_tests = tests + .iter() + .map(|t| match t.testfn { + StaticTestFn(f) => TestDescAndFn { + testfn: StaticTestFn(f), + desc: t.desc.clone(), + }, + StaticBenchFn(f) => TestDescAndFn { + testfn: StaticBenchFn(f), + desc: t.desc.clone(), + }, + _ => panic!("non-static tests passed to test::test_main_static"), + }) + .collect(); + test_main(&args, owned_tests, Options::new()) +} + +/// Invoked when unit tests terminate. Should panic if the unit +/// Tests is considered a failure. By default, invokes `report()` +/// and checks for a `0` result. +pub fn assert_test_result(result: T) { + let code = result.report(); + assert_eq!( + code, 0, + "the test returned a termination value with a non-zero status code ({}) \ + which indicates a failure", + code + ); +} + +#[derive(Copy, Clone, Debug)] +pub enum ColorConfig { + AutoColor, + AlwaysColor, + NeverColor, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum OutputFormat { + Pretty, + Terse, + Json, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum RunIgnored { + Yes, + No, + Only, +} + +#[derive(Debug)] +pub struct TestOpts { + pub list: bool, + pub filter: Option, + pub filter_exact: bool, + pub exclude_should_panic: bool, + pub run_ignored: RunIgnored, + pub run_tests: bool, + pub bench_benchmarks: bool, + pub logfile: Option, + pub nocapture: bool, + pub color: ColorConfig, + pub format: OutputFormat, + pub test_threads: Option, + pub skip: Vec, + pub options: Options, +} + +impl TestOpts { + #[cfg(test)] + fn new() -> TestOpts { + TestOpts { + list: false, + filter: None, + filter_exact: false, + exclude_should_panic: false, + run_ignored: RunIgnored::No, + run_tests: false, + bench_benchmarks: false, + logfile: None, + nocapture: false, + color: AutoColor, + format: OutputFormat::Pretty, + test_threads: None, + skip: vec![], + options: Options::new(), + } + } +} + +/// Result of parsing the options. +pub type OptRes = Result; + +fn optgroups() -> getopts::Options { + let mut opts = getopts::Options::new(); + opts.optflag("", "include-ignored", "Run ignored and not ignored tests") + .optflag("", "ignored", "Run only ignored tests") + .optflag("", "exclude-should-panic", "Excludes tests marked as should_panic") + .optflag("", "test", "Run tests and not benchmarks") + .optflag("", "bench", "Run benchmarks instead of tests") + .optflag("", "list", "List all tests and benchmarks") + .optflag("h", "help", "Display this message (longer with --help)") + .optopt( + "", + "logfile", + "Write logs to the specified file instead \ + of stdout", + "PATH", + ) + .optflag( + "", + "nocapture", + "don't capture stdout/stderr of each \ + task, allow printing directly", + ) + .optopt( + "", + "test-threads", + "Number of threads used for running tests \ + in parallel", + "n_threads", + ) + .optmulti( + "", + "skip", + "Skip tests whose names contain FILTER (this flag can \ + be used multiple times)", + "FILTER", + ) + .optflag( + "q", + "quiet", + "Display one character per test instead of one line. \ + Alias to --format=terse", + ) + .optflag( + "", + "exact", + "Exactly match filters rather than by substring", + ) + .optopt( + "", + "color", + "Configure coloring of output: + auto = colorize if stdout is a tty and tests are run on serially (default); + always = always colorize output; + never = never colorize output;", + "auto|always|never", + ) + .optopt( + "", + "format", + "Configure formatting of output: + pretty = Print verbose output; + terse = Display one character per test; + json = Output a json document", + "pretty|terse|json", + ) + .optopt( + "Z", + "", + "Enable nightly-only flags: + unstable-options = Allow use of experimental features", + "unstable-options", + ); + return opts; +} + +fn usage(binary: &str, options: &getopts::Options) { + let message = format!("Usage: {} [OPTIONS] [FILTER]", binary); + println!( + r#"{usage} + +The FILTER string is tested against the name of all tests, and only those +tests whose names contain the filter are run. + +By default, all tests are run in parallel. This can be altered with the +--test-threads flag or the RUST_TEST_THREADS environment variable when running +tests (set it to 1). + +All tests have their standard output and standard error captured by default. +This can be overridden with the --nocapture flag or setting RUST_TEST_NOCAPTURE +environment variable to a value other than "0". Logging is not captured by default. + +Test Attributes: + + #[test] - Indicates a function is a test to be run. This function + takes no arguments. + #[bench] - Indicates a function is a benchmark to be run. This + function takes one argument (test::Bencher). + #[should_panic] - This function (also labeled with #[test]) will only pass if + the code causes a panic (an assertion failure or panic!) + A message may be provided, which the failure string must + contain: #[should_panic(expected = "foo")]. + #[ignore] - When applied to a function which is already attributed as a + test, then the test runner will ignore these tests during + normal test runs. Running with --ignored or --include-ignored will run + these tests."#, + usage = options.usage(&message) + ); +} + +// FIXME: Copied from libsyntax until linkage errors are resolved. Issue #47566 +fn is_nightly() -> bool { + // Whether this is a feature-staged build, i.e., on the beta or stable channel + let disable_unstable_features = option_env!("CFG_DISABLE_UNSTABLE_FEATURES").is_some(); + // Whether we should enable unstable features for bootstrapping + let bootstrap = env::var("RUSTC_BOOTSTRAP").is_ok(); + + bootstrap || !disable_unstable_features +} + +// Parses command line arguments into test options +pub fn parse_opts(args: &[String]) -> Option { + let mut allow_unstable = false; + let opts = optgroups(); + let args = args.get(1..).unwrap_or(args); + let matches = match opts.parse(args) { + Ok(m) => m, + Err(f) => return Some(Err(f.to_string())), + }; + + if let Some(opt) = matches.opt_str("Z") { + if !is_nightly() { + return Some(Err( + "the option `Z` is only accepted on the nightly compiler".into(), + )); + } + + match &*opt { + "unstable-options" => { + allow_unstable = true; + } + _ => { + return Some(Err("Unrecognized option to `Z`".into())); + } + } + }; + + if matches.opt_present("h") { + usage(&args[0], &opts); + return None; + } + + let filter = if !matches.free.is_empty() { + Some(matches.free[0].clone()) + } else { + None + }; + + let exclude_should_panic = matches.opt_present("exclude-should-panic"); + if !allow_unstable && exclude_should_panic { + return Some(Err( + "The \"exclude-should-panic\" flag is only accepted on the nightly compiler".into(), + )); + } + + let include_ignored = matches.opt_present("include-ignored"); + if !allow_unstable && include_ignored { + return Some(Err( + "The \"include-ignored\" flag is only accepted on the nightly compiler".into(), + )); + } + + let run_ignored = match (include_ignored, matches.opt_present("ignored")) { + (true, true) => { + return Some(Err( + "the options --include-ignored and --ignored are mutually exclusive".into(), + )); + } + (true, false) => RunIgnored::Yes, + (false, true) => RunIgnored::Only, + (false, false) => RunIgnored::No, + }; + let quiet = matches.opt_present("quiet"); + let exact = matches.opt_present("exact"); + let list = matches.opt_present("list"); + + let logfile = matches.opt_str("logfile"); + let logfile = logfile.map(|s| PathBuf::from(&s)); + + let bench_benchmarks = matches.opt_present("bench"); + let run_tests = !bench_benchmarks || matches.opt_present("test"); + + let mut nocapture = matches.opt_present("nocapture"); + if !nocapture { + nocapture = match env::var("RUST_TEST_NOCAPTURE") { + Ok(val) => &val != "0", + Err(_) => false, + }; + } + + let test_threads = match matches.opt_str("test-threads") { + Some(n_str) => match n_str.parse::() { + Ok(0) => return Some(Err("argument for --test-threads must not be 0".to_string())), + Ok(n) => Some(n), + Err(e) => { + return Some(Err(format!( + "argument for --test-threads must be a number > 0 \ + (error: {})", + e + ))); + } + }, + None => None, + }; + + let color = match matches.opt_str("color").as_ref().map(|s| &**s) { + Some("auto") | None => AutoColor, + Some("always") => AlwaysColor, + Some("never") => NeverColor, + + Some(v) => { + return Some(Err(format!( + "argument for --color must be auto, always, or never (was \ + {})", + v + ))); + } + }; + + let format = match matches.opt_str("format").as_ref().map(|s| &**s) { + None if quiet => OutputFormat::Terse, + Some("pretty") | None => OutputFormat::Pretty, + Some("terse") => OutputFormat::Terse, + Some("json") => { + if !allow_unstable { + return Some(Err( + "The \"json\" format is only accepted on the nightly compiler".into(), + )); + } + OutputFormat::Json + } + + Some(v) => { + return Some(Err(format!( + "argument for --format must be pretty, terse, or json (was \ + {})", + v + ))); + } + }; + + let test_opts = TestOpts { + list, + filter, + filter_exact: exact, + exclude_should_panic, + run_ignored, + run_tests, + bench_benchmarks, + logfile, + nocapture, + color, + format, + test_threads, + skip: matches.opt_strs("skip"), + options: Options::new(), + }; + + Some(Ok(test_opts)) +} + +#[derive(Clone, PartialEq)] +pub struct BenchSamples { + ns_iter_summ: stats::Summary, + mb_s: usize, +} + +#[derive(Clone, PartialEq)] +pub enum TestResult { + TrOk, + TrFailed, + TrFailedMsg(String), + TrIgnored, + TrAllowedFail, + TrBench(BenchSamples), +} + +unsafe impl Send for TestResult {} + +enum OutputLocation { + Pretty(Box), + Raw(T), +} + +impl Write for OutputLocation { + fn write(&mut self, buf: &[u8]) -> io::Result { + match *self { + Pretty(ref mut term) => term.write(buf), + Raw(ref mut stdout) => stdout.write(buf), + } + } + + fn flush(&mut self) -> io::Result<()> { + match *self { + Pretty(ref mut term) => term.flush(), + Raw(ref mut stdout) => stdout.flush(), + } + } +} + +struct ConsoleTestState { + log_out: Option, + total: usize, + passed: usize, + failed: usize, + ignored: usize, + allowed_fail: usize, + filtered_out: usize, + measured: usize, + metrics: MetricMap, + failures: Vec<(TestDesc, Vec)>, + not_failures: Vec<(TestDesc, Vec)>, + options: Options, +} + +impl ConsoleTestState { + pub fn new(opts: &TestOpts) -> io::Result { + let log_out = match opts.logfile { + Some(ref path) => Some(File::create(path)?), + None => None, + }; + + Ok(ConsoleTestState { + log_out, + total: 0, + passed: 0, + failed: 0, + ignored: 0, + allowed_fail: 0, + filtered_out: 0, + measured: 0, + metrics: MetricMap::new(), + failures: Vec::new(), + not_failures: Vec::new(), + options: opts.options, + }) + } + + pub fn write_log>(&mut self, msg: S) -> io::Result<()> { + let msg = msg.as_ref(); + match self.log_out { + None => Ok(()), + Some(ref mut o) => o.write_all(msg.as_bytes()), + } + } + + pub fn write_log_result(&mut self, test: &TestDesc, result: &TestResult) -> io::Result<()> { + self.write_log(format!( + "{} {}\n", + match *result { + TrOk => "ok".to_owned(), + TrFailed => "failed".to_owned(), + TrFailedMsg(ref msg) => format!("failed: {}", msg), + TrIgnored => "ignored".to_owned(), + TrAllowedFail => "failed (allowed)".to_owned(), + TrBench(ref bs) => fmt_bench_samples(bs), + }, + test.name + )) + } + + fn current_test_count(&self) -> usize { + self.passed + self.failed + self.ignored + self.measured + self.allowed_fail + } +} + +// Format a number with thousands separators +fn fmt_thousands_sep(mut n: usize, sep: char) -> String { + use std::fmt::Write; + let mut output = String::new(); + let mut trailing = false; + for &pow in &[9, 6, 3, 0] { + let base = 10_usize.pow(pow); + if pow == 0 || trailing || n / base != 0 { + if !trailing { + output.write_fmt(format_args!("{}", n / base)).unwrap(); + } else { + output.write_fmt(format_args!("{:03}", n / base)).unwrap(); + } + if pow != 0 { + output.push(sep); + } + trailing = true; + } + n %= base; + } + + output +} + +pub fn fmt_bench_samples(bs: &BenchSamples) -> String { + use std::fmt::Write; + let mut output = String::new(); + + let median = bs.ns_iter_summ.median as usize; + let deviation = (bs.ns_iter_summ.max - bs.ns_iter_summ.min) as usize; + + output + .write_fmt(format_args!( + "{:>11} ns/iter (+/- {})", + fmt_thousands_sep(median, ','), + fmt_thousands_sep(deviation, ',') + )) + .unwrap(); + if bs.mb_s != 0 { + output + .write_fmt(format_args!(" = {} MB/s", bs.mb_s)) + .unwrap(); + } + output +} + +// List the tests to console, and optionally to logfile. Filters are honored. +pub fn list_tests_console(opts: &TestOpts, tests: Vec) -> io::Result<()> { + let mut output = match term::stdout() { + None => Raw(io::stdout()), + Some(t) => Pretty(t), + }; + + let quiet = opts.format == OutputFormat::Terse; + let mut st = ConsoleTestState::new(opts)?; + + let mut ntest = 0; + let mut nbench = 0; + + for test in filter_tests(&opts, tests) { + use crate::TestFn::*; + + let TestDescAndFn { + desc: TestDesc { name, .. }, + testfn, + } = test; + + let fntype = match testfn { + StaticTestFn(..) | DynTestFn(..) => { + ntest += 1; + "test" + } + StaticBenchFn(..) | DynBenchFn(..) => { + nbench += 1; + "benchmark" + } + }; + + writeln!(output, "{}: {}", name, fntype)?; + st.write_log(format!("{} {}\n", fntype, name))?; + } + + fn plural(count: u32, s: &str) -> String { + match count { + 1 => format!("{} {}", 1, s), + n => format!("{} {}s", n, s), + } + } + + if !quiet { + if ntest != 0 || nbench != 0 { + writeln!(output, "")?; + } + + writeln!( + output, + "{}, {}", + plural(ntest, "test"), + plural(nbench, "benchmark") + )?; + } + + Ok(()) +} + +// A simple console test runner +pub fn run_tests_console(opts: &TestOpts, tests: Vec) -> io::Result { + fn callback( + event: &TestEvent, + st: &mut ConsoleTestState, + out: &mut dyn OutputFormatter, + ) -> io::Result<()> { + match (*event).clone() { + TeFiltered(ref filtered_tests) => { + st.total = filtered_tests.len(); + out.write_run_start(filtered_tests.len()) + } + TeFilteredOut(filtered_out) => Ok(st.filtered_out = filtered_out), + TeWait(ref test) => out.write_test_start(test), + TeTimeout(ref test) => out.write_timeout(test), + TeResult(test, result, stdout) => { + st.write_log_result(&test, &result)?; + out.write_result(&test, &result, &*stdout)?; + match result { + TrOk => { + st.passed += 1; + st.not_failures.push((test, stdout)); + } + TrIgnored => st.ignored += 1, + TrAllowedFail => st.allowed_fail += 1, + TrBench(bs) => { + st.metrics.insert_metric( + test.name.as_slice(), + bs.ns_iter_summ.median, + bs.ns_iter_summ.max - bs.ns_iter_summ.min, + ); + st.measured += 1 + } + TrFailed => { + st.failed += 1; + st.failures.push((test, stdout)); + } + TrFailedMsg(msg) => { + st.failed += 1; + let mut stdout = stdout; + stdout.extend_from_slice(format!("note: {}", msg).as_bytes()); + st.failures.push((test, stdout)); + } + } + Ok(()) + } + } + } + + let output = match term::stdout() { + None => Raw(io::stdout()), + Some(t) => Pretty(t), + }; + + let max_name_len = tests + .iter() + .max_by_key(|t| len_if_padded(*t)) + .map(|t| t.desc.name.as_slice().len()) + .unwrap_or(0); + + let is_multithreaded = opts.test_threads.unwrap_or_else(get_concurrency) > 1; + + let mut out: Box = match opts.format { + OutputFormat::Pretty => Box::new(PrettyFormatter::new( + output, + use_color(opts), + max_name_len, + is_multithreaded, + )), + OutputFormat::Terse => Box::new(TerseFormatter::new( + output, + use_color(opts), + max_name_len, + is_multithreaded, + )), + OutputFormat::Json => Box::new(JsonFormatter::new(output)), + }; + let mut st = ConsoleTestState::new(opts)?; + fn len_if_padded(t: &TestDescAndFn) -> usize { + match t.testfn.padding() { + PadNone => 0, + PadOnRight => t.desc.name.as_slice().len(), + } + } + + run_tests(opts, tests, |x| callback(&x, &mut st, &mut *out))?; + + assert!(st.current_test_count() == st.total); + + return out.write_run_finish(&st); +} + +#[test] +fn should_sort_failures_before_printing_them() { + let test_a = TestDesc { + name: StaticTestName("a"), + ignore: false, + should_panic: ShouldPanic::No, + allow_fail: false, + }; + + let test_b = TestDesc { + name: StaticTestName("b"), + ignore: false, + should_panic: ShouldPanic::No, + allow_fail: false, + }; + + let mut out = PrettyFormatter::new(Raw(Vec::new()), false, 10, false); + + let st = ConsoleTestState { + log_out: None, + total: 0, + passed: 0, + failed: 0, + ignored: 0, + allowed_fail: 0, + filtered_out: 0, + measured: 0, + metrics: MetricMap::new(), + failures: vec![(test_b, Vec::new()), (test_a, Vec::new())], + options: Options::new(), + not_failures: Vec::new(), + }; + + out.write_failures(&st).unwrap(); + let s = match out.output_location() { + &Raw(ref m) => String::from_utf8_lossy(&m[..]), + &Pretty(_) => unreachable!(), + }; + + let apos = s.find("a").unwrap(); + let bpos = s.find("b").unwrap(); + assert!(apos < bpos); +} + +fn use_color(opts: &TestOpts) -> bool { + match opts.color { + AutoColor => !opts.nocapture && stdout_isatty(), + AlwaysColor => true, + NeverColor => false, + } +} + +#[cfg(any( + target_os = "cloudabi", + target_os = "redox", + all(target_arch = "wasm32", not(target_os = "emscripten")), + all(target_vendor = "fortanix", target_env = "sgx") +))] +fn stdout_isatty() -> bool { + // FIXME: Implement isatty on Redox and SGX + false +} +#[cfg(unix)] +fn stdout_isatty() -> bool { + unsafe { libc::isatty(libc::STDOUT_FILENO) != 0 } +} +#[cfg(windows)] +fn stdout_isatty() -> bool { + type DWORD = u32; + type BOOL = i32; + type HANDLE = *mut u8; + type LPDWORD = *mut u32; + const STD_OUTPUT_HANDLE: DWORD = -11i32 as DWORD; + extern "system" { + fn GetStdHandle(which: DWORD) -> HANDLE; + fn GetConsoleMode(hConsoleHandle: HANDLE, lpMode: LPDWORD) -> BOOL; + } + unsafe { + let handle = GetStdHandle(STD_OUTPUT_HANDLE); + let mut out = 0; + GetConsoleMode(handle, &mut out) != 0 + } +} + +#[derive(Clone)] +pub enum TestEvent { + TeFiltered(Vec), + TeWait(TestDesc), + TeResult(TestDesc, TestResult, Vec), + TeTimeout(TestDesc), + TeFilteredOut(usize), +} + +pub type MonitorMsg = (TestDesc, TestResult, Vec); + +struct Sink(Arc>>); +impl Write for Sink { + fn write(&mut self, data: &[u8]) -> io::Result { + Write::write(&mut *self.0.lock().unwrap(), data) + } + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +pub fn run_tests(opts: &TestOpts, tests: Vec, mut callback: F) -> io::Result<()> +where + F: FnMut(TestEvent) -> io::Result<()>, +{ + use std::collections::{self, HashMap}; + use std::hash::BuildHasherDefault; + use std::sync::mpsc::RecvTimeoutError; + // Use a deterministic hasher + type TestMap = + HashMap>; + + let tests_len = tests.len(); + + let mut filtered_tests = filter_tests(opts, tests); + if !opts.bench_benchmarks { + filtered_tests = convert_benchmarks_to_tests(filtered_tests); + } + + let filtered_tests = { + let mut filtered_tests = filtered_tests; + for test in filtered_tests.iter_mut() { + test.desc.name = test.desc.name.with_padding(test.testfn.padding()); + } + + filtered_tests + }; + + let filtered_out = tests_len - filtered_tests.len(); + callback(TeFilteredOut(filtered_out))?; + + let filtered_descs = filtered_tests.iter().map(|t| t.desc.clone()).collect(); + + callback(TeFiltered(filtered_descs))?; + + let (filtered_tests, filtered_benchs): (Vec<_>, _) = + filtered_tests.into_iter().partition(|e| match e.testfn { + StaticTestFn(_) | DynTestFn(_) => true, + _ => false, + }); + + let concurrency = opts.test_threads.unwrap_or_else(get_concurrency); + + let mut remaining = filtered_tests; + remaining.reverse(); + let mut pending = 0; + + let (tx, rx) = channel::(); + + let mut running_tests: TestMap = HashMap::default(); + + fn get_timed_out_tests(running_tests: &mut TestMap) -> Vec { + let now = Instant::now(); + let timed_out = running_tests + .iter() + .filter_map(|(desc, timeout)| { + if &now >= timeout { + Some(desc.clone()) + } else { + None + } + }) + .collect(); + for test in &timed_out { + running_tests.remove(test); + } + timed_out + }; + + fn calc_timeout(running_tests: &TestMap) -> Option { + running_tests.values().min().map(|next_timeout| { + let now = Instant::now(); + if *next_timeout >= now { + *next_timeout - now + } else { + Duration::new(0, 0) + } + }) + }; + + if concurrency == 1 { + while !remaining.is_empty() { + let test = remaining.pop().unwrap(); + callback(TeWait(test.desc.clone()))?; + run_test(opts, !opts.run_tests, test, tx.clone(), Concurrent::No); + let (test, result, stdout) = rx.recv().unwrap(); + callback(TeResult(test, result, stdout))?; + } + } else { + while pending > 0 || !remaining.is_empty() { + while pending < concurrency && !remaining.is_empty() { + let test = remaining.pop().unwrap(); + let timeout = Instant::now() + Duration::from_secs(TEST_WARN_TIMEOUT_S); + running_tests.insert(test.desc.clone(), timeout); + callback(TeWait(test.desc.clone()))?; //here no pad + run_test(opts, !opts.run_tests, test, tx.clone(), Concurrent::Yes); + pending += 1; + } + + let mut res; + loop { + if let Some(timeout) = calc_timeout(&running_tests) { + res = rx.recv_timeout(timeout); + for test in get_timed_out_tests(&mut running_tests) { + callback(TeTimeout(test))?; + } + if res != Err(RecvTimeoutError::Timeout) { + break; + } + } else { + res = rx.recv().map_err(|_| RecvTimeoutError::Disconnected); + break; + } + } + + let (desc, result, stdout) = res.unwrap(); + running_tests.remove(&desc); + + callback(TeResult(desc, result, stdout))?; + pending -= 1; + } + } + + if opts.bench_benchmarks { + // All benchmarks run at the end, in serial. + for b in filtered_benchs { + callback(TeWait(b.desc.clone()))?; + run_test(opts, false, b, tx.clone(), Concurrent::No); + let (test, result, stdout) = rx.recv().unwrap(); + callback(TeResult(test, result, stdout))?; + } + } + Ok(()) +} + +#[allow(deprecated)] +fn get_concurrency() -> usize { + return match env::var("RUST_TEST_THREADS") { + Ok(s) => { + let opt_n: Option = s.parse().ok(); + match opt_n { + Some(n) if n > 0 => n, + _ => panic!( + "RUST_TEST_THREADS is `{}`, should be a positive integer.", + s + ), + } + } + Err(..) => num_cpus(), + }; + + #[cfg(windows)] + #[allow(nonstandard_style)] + fn num_cpus() -> usize { + #[repr(C)] + struct SYSTEM_INFO { + wProcessorArchitecture: u16, + wReserved: u16, + dwPageSize: u32, + lpMinimumApplicationAddress: *mut u8, + lpMaximumApplicationAddress: *mut u8, + dwActiveProcessorMask: *mut u8, + dwNumberOfProcessors: u32, + dwProcessorType: u32, + dwAllocationGranularity: u32, + wProcessorLevel: u16, + wProcessorRevision: u16, + } + extern "system" { + fn GetSystemInfo(info: *mut SYSTEM_INFO) -> i32; + } + unsafe { + let mut sysinfo = std::mem::zeroed(); + GetSystemInfo(&mut sysinfo); + sysinfo.dwNumberOfProcessors as usize + } + } + + #[cfg(target_os = "redox")] + fn num_cpus() -> usize { + // FIXME: Implement num_cpus on Redox + 1 + } + + #[cfg(any( + all(target_arch = "wasm32", not(target_os = "emscripten")), + all(target_vendor = "fortanix", target_env = "sgx") + ))] + fn num_cpus() -> usize { + 1 + } + + #[cfg(any( + target_os = "android", + target_os = "cloudabi", + target_os = "emscripten", + target_os = "fuchsia", + target_os = "ios", + target_os = "linux", + target_os = "macos", + target_os = "solaris" + ))] + fn num_cpus() -> usize { + unsafe { libc::sysconf(libc::_SC_NPROCESSORS_ONLN) as usize } + } + + #[cfg(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "bitrig", + target_os = "netbsd" + ))] + fn num_cpus() -> usize { + use std::ptr; + + let mut cpus: libc::c_uint = 0; + let mut cpus_size = std::mem::size_of_val(&cpus); + + unsafe { + cpus = libc::sysconf(libc::_SC_NPROCESSORS_ONLN) as libc::c_uint; + } + if cpus < 1 { + let mut mib = [libc::CTL_HW, libc::HW_NCPU, 0, 0]; + unsafe { + libc::sysctl( + mib.as_mut_ptr(), + 2, + &mut cpus as *mut _ as *mut _, + &mut cpus_size as *mut _ as *mut _, + ptr::null_mut(), + 0, + ); + } + if cpus < 1 { + cpus = 1; + } + } + cpus as usize + } + + #[cfg(target_os = "openbsd")] + fn num_cpus() -> usize { + use std::ptr; + + let mut cpus: libc::c_uint = 0; + let mut cpus_size = std::mem::size_of_val(&cpus); + let mut mib = [libc::CTL_HW, libc::HW_NCPU, 0, 0]; + + unsafe { + libc::sysctl( + mib.as_mut_ptr(), + 2, + &mut cpus as *mut _ as *mut _, + &mut cpus_size as *mut _ as *mut _, + ptr::null_mut(), + 0, + ); + } + if cpus < 1 { + cpus = 1; + } + cpus as usize + } + + #[cfg(target_os = "haiku")] + fn num_cpus() -> usize { + // FIXME: implement + 1 + } + + #[cfg(target_os = "l4re")] + fn num_cpus() -> usize { + // FIXME: implement + 1 + } +} + +pub fn filter_tests(opts: &TestOpts, tests: Vec) -> Vec { + let mut filtered = tests; + let matches_filter = |test: &TestDescAndFn, filter: &str| { + let test_name = test.desc.name.as_slice(); + + match opts.filter_exact { + true => test_name == filter, + false => test_name.contains(filter), + } + }; + + // Remove tests that don't match the test filter + if let Some(ref filter) = opts.filter { + filtered.retain(|test| matches_filter(test, filter)); + } + + // Skip tests that match any of the skip filters + filtered.retain(|test| !opts.skip.iter().any(|sf| matches_filter(test, sf))); + + // Excludes #[should_panic] tests + if opts.exclude_should_panic { + filtered.retain(|test| test.desc.should_panic == ShouldPanic::No); + } + + // maybe unignore tests + match opts.run_ignored { + RunIgnored::Yes => { + filtered + .iter_mut() + .for_each(|test| test.desc.ignore = false); + } + RunIgnored::Only => { + filtered.retain(|test| test.desc.ignore); + filtered + .iter_mut() + .for_each(|test| test.desc.ignore = false); + } + RunIgnored::No => {} + } + + // Sort the tests alphabetically + filtered.sort_by(|t1, t2| t1.desc.name.as_slice().cmp(t2.desc.name.as_slice())); + + filtered +} + +pub fn convert_benchmarks_to_tests(tests: Vec) -> Vec { + // convert benchmarks to tests, if we're not benchmarking them + tests + .into_iter() + .map(|x| { + let testfn = match x.testfn { + DynBenchFn(bench) => DynTestFn(Box::new(move || { + bench::run_once(|b| __rust_begin_short_backtrace(|| bench.run(b))) + })), + StaticBenchFn(benchfn) => DynTestFn(Box::new(move || { + bench::run_once(|b| __rust_begin_short_backtrace(|| benchfn(b))) + })), + f => f, + }; + TestDescAndFn { + desc: x.desc, + testfn, + } + }) + .collect() +} + +pub fn run_test( + opts: &TestOpts, + force_ignore: bool, + test: TestDescAndFn, + monitor_ch: Sender, + concurrency: Concurrent, +) { + let TestDescAndFn { desc, testfn } = test; + + let ignore_because_panic_abort = cfg!(target_arch = "wasm32") + && !cfg!(target_os = "emscripten") + && desc.should_panic != ShouldPanic::No; + + if force_ignore || desc.ignore || ignore_because_panic_abort { + monitor_ch.send((desc, TrIgnored, Vec::new())).unwrap(); + return; + } + + fn run_test_inner( + desc: TestDesc, + monitor_ch: Sender, + nocapture: bool, + testfn: Box, + concurrency: Concurrent, + ) { + // Buffer for capturing standard I/O + let data = Arc::new(Mutex::new(Vec::new())); + let data2 = data.clone(); + + let name = desc.name.clone(); + let runtest = move || { + let oldio = if !nocapture { + Some(( + io::set_print(Some(Box::new(Sink(data2.clone())))), + io::set_panic(Some(Box::new(Sink(data2)))), + )) + } else { + None + }; + + let result = catch_unwind(AssertUnwindSafe(testfn)); + + if let Some((printio, panicio)) = oldio { + io::set_print(printio); + io::set_panic(panicio); + }; + + let test_result = calc_result(&desc, result); + let stdout = data.lock().unwrap().to_vec(); + monitor_ch + .send((desc.clone(), test_result, stdout)) + .unwrap(); + }; + + // If the platform is single-threaded we're just going to run + // the test synchronously, regardless of the concurrency + // level. + let supports_threads = !cfg!(target_os = "emscripten") && !cfg!(target_arch = "wasm32"); + if concurrency == Concurrent::Yes && supports_threads { + let cfg = thread::Builder::new().name(name.as_slice().to_owned()); + cfg.spawn(runtest).unwrap(); + } else { + runtest(); + } + } + + match testfn { + DynBenchFn(bencher) => { + crate::bench::benchmark(desc, monitor_ch, opts.nocapture, |harness| { + bencher.run(harness) + }); + } + StaticBenchFn(benchfn) => { + crate::bench::benchmark(desc, monitor_ch, opts.nocapture, |harness| { + (benchfn.clone())(harness) + }); + } + DynTestFn(f) => { + let cb = move || __rust_begin_short_backtrace(f); + run_test_inner(desc, monitor_ch, opts.nocapture, Box::new(cb), concurrency) + } + StaticTestFn(f) => run_test_inner( + desc, + monitor_ch, + opts.nocapture, + Box::new(move || __rust_begin_short_backtrace(f)), + concurrency, + ), + } +} + +/// Fixed frame used to clean the backtrace with `RUST_BACKTRACE=1`. +#[inline(never)] +fn __rust_begin_short_backtrace(f: F) { + f() +} + +fn calc_result(desc: &TestDesc, task_result: Result<(), Box>) -> TestResult { + match (&desc.should_panic, task_result) { + (&ShouldPanic::No, Ok(())) | (&ShouldPanic::Yes, Err(_)) => TrOk, + (&ShouldPanic::YesWithMessage(msg), Err(ref err)) => { + if err + .downcast_ref::() + .map(|e| &**e) + .or_else(|| err.downcast_ref::<&'static str>().map(|e| *e)) + .map(|e| e.contains(msg)) + .unwrap_or(false) + { + TrOk + } else { + if desc.allow_fail { + TrAllowedFail + } else { + TrFailedMsg(format!("Panic did not include expected string '{}'", msg)) + } + } + } + _ if desc.allow_fail => TrAllowedFail, + _ => TrFailed, + } +} + +#[derive(Clone, PartialEq)] +pub struct MetricMap(BTreeMap); + +impl MetricMap { + pub fn new() -> MetricMap { + MetricMap(BTreeMap::new()) + } + + /// Insert a named `value` (+/- `noise`) metric into the map. The value + /// must be non-negative. The `noise` indicates the uncertainty of the + /// metric, which doubles as the "noise range" of acceptable + /// pairwise-regressions on this named value, when comparing from one + /// metric to the next using `compare_to_old`. + /// + /// If `noise` is positive, then it means this metric is of a value + /// you want to see grow smaller, so a change larger than `noise` in the + /// positive direction represents a regression. + /// + /// If `noise` is negative, then it means this metric is of a value + /// you want to see grow larger, so a change larger than `noise` in the + /// negative direction represents a regression. + pub fn insert_metric(&mut self, name: &str, value: f64, noise: f64) { + let m = Metric { value, noise }; + self.0.insert(name.to_owned(), m); + } + + pub fn fmt_metrics(&self) -> String { + let v = self + .0 + .iter() + .map(|(k, v)| format!("{}: {} (+/- {})", *k, v.value, v.noise)) + .collect::>(); + v.join(", ") + } +} + +// Benchmarking pub use std::hint::black_box; +impl Bencher { + /// Callback for benchmark functions to run in their body. + pub fn iter(&mut self, mut inner: F) + where + F: FnMut() -> T, + { + if self.mode == BenchMode::Single { + ns_iter_inner(&mut inner, 1); + return; + } + + self.summary = Some(iter(&mut inner)); + } + + pub fn bench(&mut self, mut f: F) -> Option + where + F: FnMut(&mut Bencher), + { + f(self); + return self.summary; + } +} + +fn ns_from_dur(dur: Duration) -> u64 { + dur.as_secs() * 1_000_000_000 + (dur.subsec_nanos() as u64) +} + +fn ns_iter_inner(inner: &mut F, k: u64) -> u64 +where + F: FnMut() -> T, +{ + let start = Instant::now(); + for _ in 0..k { + black_box(inner()); + } + return ns_from_dur(start.elapsed()); +} + +pub fn iter(inner: &mut F) -> stats::Summary +where + F: FnMut() -> T, +{ + // Initial bench run to get ballpark figure. + let ns_single = ns_iter_inner(inner, 1); + + // Try to estimate iter count for 1ms falling back to 1m + // iterations if first run took < 1ns. + let ns_target_total = 1_000_000; // 1ms + let mut n = ns_target_total / cmp::max(1, ns_single); + + // if the first run took more than 1ms we don't want to just + // be left doing 0 iterations on every loop. The unfortunate + // side effect of not being able to do as many runs is + // automatically handled by the statistical analysis below + // (i.e., larger error bars). + n = cmp::max(1, n); + + let mut total_run = Duration::new(0, 0); + let samples: &mut [f64] = &mut [0.0_f64; 50]; + loop { + let loop_start = Instant::now(); + + for p in &mut *samples { + *p = ns_iter_inner(inner, n) as f64 / n as f64; + } + + stats::winsorize(samples, 5.0); + let summ = stats::Summary::new(samples); + + for p in &mut *samples { + let ns = ns_iter_inner(inner, 5 * n); + *p = ns as f64 / (5 * n) as f64; + } + + stats::winsorize(samples, 5.0); + let summ5 = stats::Summary::new(samples); + + let loop_run = loop_start.elapsed(); + + // If we've run for 100ms and seem to have converged to a + // stable median. + if loop_run > Duration::from_millis(100) + && summ.median_abs_dev_pct < 1.0 + && summ.median - summ5.median < summ5.median_abs_dev + { + return summ5; + } + + total_run = total_run + loop_run; + // Longest we ever run for is 3s. + if total_run > Duration::from_secs(3) { + return summ5; + } + + // If we overflow here just return the results so far. We check a + // multiplier of 10 because we're about to multiply by 2 and the + // next iteration of the loop will also multiply by 5 (to calculate + // the summ5 result) + n = match n.checked_mul(10) { + Some(_) => n * 2, + None => { + return summ5; + } + }; + } +} + +pub mod bench { + use super::{BenchMode, BenchSamples, Bencher, MonitorMsg, Sender, Sink, TestDesc, TestResult}; + use crate::stats; + use std::cmp; + use std::io; + use std::panic::{catch_unwind, AssertUnwindSafe}; + use std::sync::{Arc, Mutex}; + + pub fn benchmark(desc: TestDesc, monitor_ch: Sender, nocapture: bool, f: F) + where + F: FnMut(&mut Bencher), + { + let mut bs = Bencher { + mode: BenchMode::Auto, + summary: None, + bytes: 0, + }; + + let data = Arc::new(Mutex::new(Vec::new())); + let data2 = data.clone(); + + let oldio = if !nocapture { + Some(( + io::set_print(Some(Box::new(Sink(data2.clone())))), + io::set_panic(Some(Box::new(Sink(data2)))), + )) + } else { + None + }; + + let result = catch_unwind(AssertUnwindSafe(|| bs.bench(f))); + + if let Some((printio, panicio)) = oldio { + io::set_print(printio); + io::set_panic(panicio); + }; + + let test_result = match result { + //bs.bench(f) { + Ok(Some(ns_iter_summ)) => { + let ns_iter = cmp::max(ns_iter_summ.median as u64, 1); + let mb_s = bs.bytes * 1000 / ns_iter; + + let bs = BenchSamples { + ns_iter_summ, + mb_s: mb_s as usize, + }; + TestResult::TrBench(bs) + } + Ok(None) => { + // iter not called, so no data. + // FIXME: error in this case? + let samples: &mut [f64] = &mut [0.0_f64; 1]; + let bs = BenchSamples { + ns_iter_summ: stats::Summary::new(samples), + mb_s: 0, + }; + TestResult::TrBench(bs) + } + Err(_) => TestResult::TrFailed, + }; + + let stdout = data.lock().unwrap().to_vec(); + monitor_ch.send((desc, test_result, stdout)).unwrap(); + } + + pub fn run_once(f: F) + where + F: FnMut(&mut Bencher), + { + let mut bs = Bencher { + mode: BenchMode::Single, + summary: None, + bytes: 0, + }; + bs.bench(f); + } +} + #[cfg(test)] mod tests { + use crate::bench; + use crate::test::{ + filter_tests, parse_opts, run_test, DynTestFn, DynTestName, MetricMap, RunIgnored, + ShouldPanic, StaticTestName, TestDesc, TestDescAndFn, TestOpts, TrFailed, TrFailedMsg, + TrIgnored, TrOk, + }; use crate::Bencher; - use libtest::stats::Stats; + use crate::Concurrent; + use std::sync::mpsc::channel; - #[bench] - pub fn sum_three_items(b: &mut Bencher) { - b.iter(|| { - [1e20f64, 1.5f64, -1e20f64].sum(); - }) + fn one_ignored_one_unignored_test() -> Vec { + vec![ + TestDescAndFn { + desc: TestDesc { + name: StaticTestName("1"), + ignore: true, + should_panic: ShouldPanic::No, + allow_fail: false, + }, + testfn: DynTestFn(Box::new(move || {})), + }, + TestDescAndFn { + desc: TestDesc { + name: StaticTestName("2"), + ignore: false, + should_panic: ShouldPanic::No, + allow_fail: false, + }, + testfn: DynTestFn(Box::new(move || {})), + }, + ] } - #[bench] - pub fn sum_many_f64(b: &mut Bencher) { - let nums = [-1e30f64, 1e60, 1e30, 1.0, -1e60]; - let v = (0..500).map(|i| nums[i % 5]).collect::>(); - b.iter(|| { - v.sum(); - }) + #[test] + pub fn do_not_run_ignored_tests() { + fn f() { + panic!(); + } + let desc = TestDescAndFn { + desc: TestDesc { + name: StaticTestName("whatever"), + ignore: true, + should_panic: ShouldPanic::No, + allow_fail: false, + }, + testfn: DynTestFn(Box::new(f)), + }; + let (tx, rx) = channel(); + run_test(&TestOpts::new(), false, desc, tx, Concurrent::No); + let (_, res, _) = rx.recv().unwrap(); + assert!(res != TrOk); } - #[bench] - pub fn no_iter(_: &mut Bencher) {} + #[test] + pub fn ignored_tests_result_in_ignored() { + fn f() {} + let desc = TestDescAndFn { + desc: TestDesc { + name: StaticTestName("whatever"), + ignore: true, + should_panic: ShouldPanic::No, + allow_fail: false, + }, + testfn: DynTestFn(Box::new(f)), + }; + let (tx, rx) = channel(); + run_test(&TestOpts::new(), false, desc, tx, Concurrent::No); + let (_, res, _) = rx.recv().unwrap(); + assert!(res == TrIgnored); + } + + #[test] + fn test_should_panic() { + fn f() { + panic!(); + } + let desc = TestDescAndFn { + desc: TestDesc { + name: StaticTestName("whatever"), + ignore: false, + should_panic: ShouldPanic::Yes, + allow_fail: false, + }, + testfn: DynTestFn(Box::new(f)), + }; + let (tx, rx) = channel(); + run_test(&TestOpts::new(), false, desc, tx, Concurrent::No); + let (_, res, _) = rx.recv().unwrap(); + assert!(res == TrOk); + } + + #[test] + fn test_should_panic_good_message() { + fn f() { + panic!("an error message"); + } + let desc = TestDescAndFn { + desc: TestDesc { + name: StaticTestName("whatever"), + ignore: false, + should_panic: ShouldPanic::YesWithMessage("error message"), + allow_fail: false, + }, + testfn: DynTestFn(Box::new(f)), + }; + let (tx, rx) = channel(); + run_test(&TestOpts::new(), false, desc, tx, Concurrent::No); + let (_, res, _) = rx.recv().unwrap(); + assert!(res == TrOk); + } + + #[test] + fn test_should_panic_bad_message() { + fn f() { + panic!("an error message"); + } + let expected = "foobar"; + let failed_msg = "Panic did not include expected string"; + let desc = TestDescAndFn { + desc: TestDesc { + name: StaticTestName("whatever"), + ignore: false, + should_panic: ShouldPanic::YesWithMessage(expected), + allow_fail: false, + }, + testfn: DynTestFn(Box::new(f)), + }; + let (tx, rx) = channel(); + run_test(&TestOpts::new(), false, desc, tx, Concurrent::No); + let (_, res, _) = rx.recv().unwrap(); + assert!(res == TrFailedMsg(format!("{} '{}'", failed_msg, expected))); + } + + #[test] + fn test_should_panic_but_succeeds() { + fn f() {} + let desc = TestDescAndFn { + desc: TestDesc { + name: StaticTestName("whatever"), + ignore: false, + should_panic: ShouldPanic::Yes, + allow_fail: false, + }, + testfn: DynTestFn(Box::new(f)), + }; + let (tx, rx) = channel(); + run_test(&TestOpts::new(), false, desc, tx, Concurrent::No); + let (_, res, _) = rx.recv().unwrap(); + assert!(res == TrFailed); + } + + #[test] + fn parse_ignored_flag() { + let args = vec![ + "progname".to_string(), + "filter".to_string(), + "--ignored".to_string(), + ]; + let opts = parse_opts(&args).unwrap().unwrap(); + assert_eq!(opts.run_ignored, RunIgnored::Only); + } + + #[test] + fn parse_include_ignored_flag() { + let args = vec![ + "progname".to_string(), + "filter".to_string(), + "-Zunstable-options".to_string(), + "--include-ignored".to_string(), + ]; + let opts = parse_opts(&args).unwrap().unwrap(); + assert_eq!(opts.run_ignored, RunIgnored::Yes); + } + + #[test] + pub fn filter_for_ignored_option() { + // When we run ignored tests the test filter should filter out all the + // unignored tests and flip the ignore flag on the rest to false + + let mut opts = TestOpts::new(); + opts.run_tests = true; + opts.run_ignored = RunIgnored::Only; + + let tests = one_ignored_one_unignored_test(); + let filtered = filter_tests(&opts, tests); + + assert_eq!(filtered.len(), 1); + assert_eq!(filtered[0].desc.name.to_string(), "1"); + assert!(!filtered[0].desc.ignore); + } + + #[test] + pub fn run_include_ignored_option() { + // When we "--include-ignored" tests, the ignore flag should be set to false on + // all tests and no test filtered out + + let mut opts = TestOpts::new(); + opts.run_tests = true; + opts.run_ignored = RunIgnored::Yes; + + let tests = one_ignored_one_unignored_test(); + let filtered = filter_tests(&opts, tests); + + assert_eq!(filtered.len(), 2); + assert!(!filtered[0].desc.ignore); + assert!(!filtered[1].desc.ignore); + } + + #[test] + pub fn exclude_should_panic_option() { + let mut opts = TestOpts::new(); + opts.run_tests = true; + opts.exclude_should_panic = true; + + let mut tests = one_ignored_one_unignored_test(); + tests.push(TestDescAndFn { + desc: TestDesc { + name: StaticTestName("3"), + ignore: false, + should_panic: ShouldPanic::Yes, + allow_fail: false, + }, + testfn: DynTestFn(Box::new(move || {})), + }); + + let filtered = filter_tests(&opts, tests); + + assert_eq!(filtered.len(), 2); + assert!(filtered.iter().all(|test| test.desc.should_panic == ShouldPanic::No)); + } + + #[test] + pub fn exact_filter_match() { + fn tests() -> Vec { + vec!["base", "base::test", "base::test1", "base::test2"] + .into_iter() + .map(|name| TestDescAndFn { + desc: TestDesc { + name: StaticTestName(name), + ignore: false, + should_panic: ShouldPanic::No, + allow_fail: false, + }, + testfn: DynTestFn(Box::new(move || {})), + }) + .collect() + } + + let substr = filter_tests( + &TestOpts { + filter: Some("base".into()), + ..TestOpts::new() + }, + tests(), + ); + assert_eq!(substr.len(), 4); + + let substr = filter_tests( + &TestOpts { + filter: Some("bas".into()), + ..TestOpts::new() + }, + tests(), + ); + assert_eq!(substr.len(), 4); + + let substr = filter_tests( + &TestOpts { + filter: Some("::test".into()), + ..TestOpts::new() + }, + tests(), + ); + assert_eq!(substr.len(), 3); + + let substr = filter_tests( + &TestOpts { + filter: Some("base::test".into()), + ..TestOpts::new() + }, + tests(), + ); + assert_eq!(substr.len(), 3); + + let exact = filter_tests( + &TestOpts { + filter: Some("base".into()), + filter_exact: true, + ..TestOpts::new() + }, + tests(), + ); + assert_eq!(exact.len(), 1); + + let exact = filter_tests( + &TestOpts { + filter: Some("bas".into()), + filter_exact: true, + ..TestOpts::new() + }, + tests(), + ); + assert_eq!(exact.len(), 0); + + let exact = filter_tests( + &TestOpts { + filter: Some("::test".into()), + filter_exact: true, + ..TestOpts::new() + }, + tests(), + ); + assert_eq!(exact.len(), 0); + + let exact = filter_tests( + &TestOpts { + filter: Some("base::test".into()), + filter_exact: true, + ..TestOpts::new() + }, + tests(), + ); + assert_eq!(exact.len(), 1); + } + + #[test] + pub fn sort_tests() { + let mut opts = TestOpts::new(); + opts.run_tests = true; + + let names = vec![ + "sha1::test".to_string(), + "isize::test_to_str".to_string(), + "isize::test_pow".to_string(), + "test::do_not_run_ignored_tests".to_string(), + "test::ignored_tests_result_in_ignored".to_string(), + "test::first_free_arg_should_be_a_filter".to_string(), + "test::parse_ignored_flag".to_string(), + "test::parse_include_ignored_flag".to_string(), + "test::filter_for_ignored_option".to_string(), + "test::run_include_ignored_option".to_string(), + "test::sort_tests".to_string(), + ]; + let tests = { + fn testfn() {} + let mut tests = Vec::new(); + for name in &names { + let test = TestDescAndFn { + desc: TestDesc { + name: DynTestName((*name).clone()), + ignore: false, + should_panic: ShouldPanic::No, + allow_fail: false, + }, + testfn: DynTestFn(Box::new(testfn)), + }; + tests.push(test); + } + tests + }; + let filtered = filter_tests(&opts, tests); + + let expected = vec![ + "isize::test_pow".to_string(), + "isize::test_to_str".to_string(), + "sha1::test".to_string(), + "test::do_not_run_ignored_tests".to_string(), + "test::filter_for_ignored_option".to_string(), + "test::first_free_arg_should_be_a_filter".to_string(), + "test::ignored_tests_result_in_ignored".to_string(), + "test::parse_ignored_flag".to_string(), + "test::parse_include_ignored_flag".to_string(), + "test::run_include_ignored_option".to_string(), + "test::sort_tests".to_string(), + ]; + + for (a, b) in expected.iter().zip(filtered) { + assert!(*a == b.desc.name.to_string()); + } + } + + #[test] + pub fn test_metricmap_compare() { + let mut m1 = MetricMap::new(); + let mut m2 = MetricMap::new(); + m1.insert_metric("in-both-noise", 1000.0, 200.0); + m2.insert_metric("in-both-noise", 1100.0, 200.0); + + m1.insert_metric("in-first-noise", 1000.0, 2.0); + m2.insert_metric("in-second-noise", 1000.0, 2.0); + + m1.insert_metric("in-both-want-downwards-but-regressed", 1000.0, 10.0); + m2.insert_metric("in-both-want-downwards-but-regressed", 2000.0, 10.0); + + m1.insert_metric("in-both-want-downwards-and-improved", 2000.0, 10.0); + m2.insert_metric("in-both-want-downwards-and-improved", 1000.0, 10.0); + + m1.insert_metric("in-both-want-upwards-but-regressed", 2000.0, -10.0); + m2.insert_metric("in-both-want-upwards-but-regressed", 1000.0, -10.0); + + m1.insert_metric("in-both-want-upwards-and-improved", 1000.0, -10.0); + m2.insert_metric("in-both-want-upwards-and-improved", 2000.0, -10.0); + } + + #[test] + pub fn test_bench_once_no_iter() { + fn f(_: &mut Bencher) {} + bench::run_once(f); + } + + #[test] + pub fn test_bench_once_iter() { + fn f(b: &mut Bencher) { + b.iter(|| {}) + } + bench::run_once(f); + } + + #[test] + pub fn test_bench_no_iter() { + fn f(_: &mut Bencher) {} + + let (tx, rx) = channel(); + + let desc = TestDesc { + name: StaticTestName("f"), + ignore: false, + should_panic: ShouldPanic::No, + allow_fail: false, + }; + + crate::bench::benchmark(desc, tx, true, f); + rx.recv().unwrap(); + } + + #[test] + pub fn test_bench_iter() { + fn f(b: &mut Bencher) { + b.iter(|| {}) + } + + let (tx, rx) = channel(); + + let desc = TestDesc { + name: StaticTestName("f"), + ignore: false, + should_panic: ShouldPanic::No, + allow_fail: false, + }; + + crate::bench::benchmark(desc, tx, true, f); + rx.recv().unwrap(); + } } diff --git a/src/libtest/stats.rs b/src/libtest/stats.rs new file mode 100644 index 00000000000..5c9421d5ea4 --- /dev/null +++ b/src/libtest/stats.rs @@ -0,0 +1,922 @@ +#![allow(missing_docs)] +#![allow(deprecated)] // Float + +use std::cmp::Ordering::{self, Equal, Greater, Less}; +use std::mem; + +fn local_cmp(x: f64, y: f64) -> Ordering { + // arbitrarily decide that NaNs are larger than everything. + if y.is_nan() { + Less + } else if x.is_nan() { + Greater + } else if x < y { + Less + } else if x == y { + Equal + } else { + Greater + } +} + +fn local_sort(v: &mut [f64]) { + v.sort_by(|x: &f64, y: &f64| local_cmp(*x, *y)); +} + +/// Trait that provides simple descriptive statistics on a univariate set of numeric samples. +pub trait Stats { + /// Sum of the samples. + /// + /// Note: this method sacrifices performance at the altar of accuracy + /// Depends on IEEE-754 arithmetic guarantees. See proof of correctness at: + /// ["Adaptive Precision Floating-Point Arithmetic and Fast Robust Geometric + /// Predicates"][paper] + /// + /// [paper]: http://www.cs.cmu.edu/~quake-papers/robust-arithmetic.ps + fn sum(&self) -> f64; + + /// Minimum value of the samples. + fn min(&self) -> f64; + + /// Maximum value of the samples. + fn max(&self) -> f64; + + /// Arithmetic mean (average) of the samples: sum divided by sample-count. + /// + /// See: + fn mean(&self) -> f64; + + /// Median of the samples: value separating the lower half of the samples from the higher half. + /// Equal to `self.percentile(50.0)`. + /// + /// See: + fn median(&self) -> f64; + + /// Variance of the samples: bias-corrected mean of the squares of the differences of each + /// sample from the sample mean. Note that this calculates the _sample variance_ rather than the + /// population variance, which is assumed to be unknown. It therefore corrects the `(n-1)/n` + /// bias that would appear if we calculated a population variance, by dividing by `(n-1)` rather + /// than `n`. + /// + /// See: + fn var(&self) -> f64; + + /// Standard deviation: the square root of the sample variance. + /// + /// Note: this is not a robust statistic for non-normal distributions. Prefer the + /// `median_abs_dev` for unknown distributions. + /// + /// See: + fn std_dev(&self) -> f64; + + /// Standard deviation as a percent of the mean value. See `std_dev` and `mean`. + /// + /// Note: this is not a robust statistic for non-normal distributions. Prefer the + /// `median_abs_dev_pct` for unknown distributions. + fn std_dev_pct(&self) -> f64; + + /// Scaled median of the absolute deviations of each sample from the sample median. This is a + /// robust (distribution-agnostic) estimator of sample variability. Use this in preference to + /// `std_dev` if you cannot assume your sample is normally distributed. Note that this is scaled + /// by the constant `1.4826` to allow its use as a consistent estimator for the standard + /// deviation. + /// + /// See: + fn median_abs_dev(&self) -> f64; + + /// Median absolute deviation as a percent of the median. See `median_abs_dev` and `median`. + fn median_abs_dev_pct(&self) -> f64; + + /// Percentile: the value below which `pct` percent of the values in `self` fall. For example, + /// percentile(95.0) will return the value `v` such that 95% of the samples `s` in `self` + /// satisfy `s <= v`. + /// + /// Calculated by linear interpolation between closest ranks. + /// + /// See: + fn percentile(&self, pct: f64) -> f64; + + /// Quartiles of the sample: three values that divide the sample into four equal groups, each + /// with 1/4 of the data. The middle value is the median. See `median` and `percentile`. This + /// function may calculate the 3 quartiles more efficiently than 3 calls to `percentile`, but + /// is otherwise equivalent. + /// + /// See also: + fn quartiles(&self) -> (f64, f64, f64); + + /// Inter-quartile range: the difference between the 25th percentile (1st quartile) and the 75th + /// percentile (3rd quartile). See `quartiles`. + /// + /// See also: + fn iqr(&self) -> f64; +} + +/// Extracted collection of all the summary statistics of a sample set. +#[derive(Clone, PartialEq, Copy)] +#[allow(missing_docs)] +pub struct Summary { + pub sum: f64, + pub min: f64, + pub max: f64, + pub mean: f64, + pub median: f64, + pub var: f64, + pub std_dev: f64, + pub std_dev_pct: f64, + pub median_abs_dev: f64, + pub median_abs_dev_pct: f64, + pub quartiles: (f64, f64, f64), + pub iqr: f64, +} + +impl Summary { + /// Construct a new summary of a sample set. + pub fn new(samples: &[f64]) -> Summary { + Summary { + sum: samples.sum(), + min: samples.min(), + max: samples.max(), + mean: samples.mean(), + median: samples.median(), + var: samples.var(), + std_dev: samples.std_dev(), + std_dev_pct: samples.std_dev_pct(), + median_abs_dev: samples.median_abs_dev(), + median_abs_dev_pct: samples.median_abs_dev_pct(), + quartiles: samples.quartiles(), + iqr: samples.iqr(), + } + } +} + +impl Stats for [f64] { + // FIXME #11059 handle NaN, inf and overflow + fn sum(&self) -> f64 { + let mut partials = vec![]; + + for &x in self { + let mut x = x; + let mut j = 0; + // This inner loop applies `hi`/`lo` summation to each + // partial so that the list of partial sums remains exact. + for i in 0..partials.len() { + let mut y: f64 = partials[i]; + if x.abs() < y.abs() { + mem::swap(&mut x, &mut y); + } + // Rounded `x+y` is stored in `hi` with round-off stored in + // `lo`. Together `hi+lo` are exactly equal to `x+y`. + let hi = x + y; + let lo = y - (hi - x); + if lo != 0.0 { + partials[j] = lo; + j += 1; + } + x = hi; + } + if j >= partials.len() { + partials.push(x); + } else { + partials[j] = x; + partials.truncate(j + 1); + } + } + let zero: f64 = 0.0; + partials.iter().fold(zero, |p, q| p + *q) + } + + fn min(&self) -> f64 { + assert!(!self.is_empty()); + self.iter().fold(self[0], |p, q| p.min(*q)) + } + + fn max(&self) -> f64 { + assert!(!self.is_empty()); + self.iter().fold(self[0], |p, q| p.max(*q)) + } + + fn mean(&self) -> f64 { + assert!(!self.is_empty()); + self.sum() / (self.len() as f64) + } + + fn median(&self) -> f64 { + self.percentile(50 as f64) + } + + fn var(&self) -> f64 { + if self.len() < 2 { + 0.0 + } else { + let mean = self.mean(); + let mut v: f64 = 0.0; + for s in self { + let x = *s - mean; + v = v + x * x; + } + // N.B., this is _supposed to be_ len-1, not len. If you + // change it back to len, you will be calculating a + // population variance, not a sample variance. + let denom = (self.len() - 1) as f64; + v / denom + } + } + + fn std_dev(&self) -> f64 { + self.var().sqrt() + } + + fn std_dev_pct(&self) -> f64 { + let hundred = 100 as f64; + (self.std_dev() / self.mean()) * hundred + } + + fn median_abs_dev(&self) -> f64 { + let med = self.median(); + let abs_devs: Vec = self.iter().map(|&v| (med - v).abs()).collect(); + // This constant is derived by smarter statistics brains than me, but it is + // consistent with how R and other packages treat the MAD. + let number = 1.4826; + abs_devs.median() * number + } + + fn median_abs_dev_pct(&self) -> f64 { + let hundred = 100 as f64; + (self.median_abs_dev() / self.median()) * hundred + } + + fn percentile(&self, pct: f64) -> f64 { + let mut tmp = self.to_vec(); + local_sort(&mut tmp); + percentile_of_sorted(&tmp, pct) + } + + fn quartiles(&self) -> (f64, f64, f64) { + let mut tmp = self.to_vec(); + local_sort(&mut tmp); + let first = 25f64; + let a = percentile_of_sorted(&tmp, first); + let second = 50f64; + let b = percentile_of_sorted(&tmp, second); + let third = 75f64; + let c = percentile_of_sorted(&tmp, third); + (a, b, c) + } + + fn iqr(&self) -> f64 { + let (a, _, c) = self.quartiles(); + c - a + } +} + +// Helper function: extract a value representing the `pct` percentile of a sorted sample-set, using +// linear interpolation. If samples are not sorted, return nonsensical value. +fn percentile_of_sorted(sorted_samples: &[f64], pct: f64) -> f64 { + assert!(!sorted_samples.is_empty()); + if sorted_samples.len() == 1 { + return sorted_samples[0]; + } + let zero: f64 = 0.0; + assert!(zero <= pct); + let hundred = 100f64; + assert!(pct <= hundred); + if pct == hundred { + return sorted_samples[sorted_samples.len() - 1]; + } + let length = (sorted_samples.len() - 1) as f64; + let rank = (pct / hundred) * length; + let lrank = rank.floor(); + let d = rank - lrank; + let n = lrank as usize; + let lo = sorted_samples[n]; + let hi = sorted_samples[n + 1]; + lo + (hi - lo) * d +} + +/// Winsorize a set of samples, replacing values above the `100-pct` percentile +/// and below the `pct` percentile with those percentiles themselves. This is a +/// way of minimizing the effect of outliers, at the cost of biasing the sample. +/// It differs from trimming in that it does not change the number of samples, +/// just changes the values of those that are outliers. +/// +/// See: +pub fn winsorize(samples: &mut [f64], pct: f64) { + let mut tmp = samples.to_vec(); + local_sort(&mut tmp); + let lo = percentile_of_sorted(&tmp, pct); + let hundred = 100 as f64; + let hi = percentile_of_sorted(&tmp, hundred - pct); + for samp in samples { + if *samp > hi { + *samp = hi + } else if *samp < lo { + *samp = lo + } + } +} + +// Test vectors generated from R, using the script src/etc/stat-test-vectors.r. + +#[cfg(test)] +mod tests { + use crate::stats::Stats; + use crate::stats::Summary; + use std::f64; + use std::io::prelude::*; + use std::io; + + macro_rules! assert_approx_eq { + ($a: expr, $b: expr) => {{ + let (a, b) = (&$a, &$b); + assert!( + (*a - *b).abs() < 1.0e-6, + "{} is not approximately equal to {}", + *a, + *b + ); + }}; + } + + fn check(samples: &[f64], summ: &Summary) { + let summ2 = Summary::new(samples); + + let mut w = io::sink(); + let w = &mut w; + (write!(w, "\n")).unwrap(); + + assert_eq!(summ.sum, summ2.sum); + assert_eq!(summ.min, summ2.min); + assert_eq!(summ.max, summ2.max); + assert_eq!(summ.mean, summ2.mean); + assert_eq!(summ.median, summ2.median); + + // We needed a few more digits to get exact equality on these + // but they're within float epsilon, which is 1.0e-6. + assert_approx_eq!(summ.var, summ2.var); + assert_approx_eq!(summ.std_dev, summ2.std_dev); + assert_approx_eq!(summ.std_dev_pct, summ2.std_dev_pct); + assert_approx_eq!(summ.median_abs_dev, summ2.median_abs_dev); + assert_approx_eq!(summ.median_abs_dev_pct, summ2.median_abs_dev_pct); + + assert_eq!(summ.quartiles, summ2.quartiles); + assert_eq!(summ.iqr, summ2.iqr); + } + + #[test] + fn test_min_max_nan() { + let xs = &[1.0, 2.0, f64::NAN, 3.0, 4.0]; + let summary = Summary::new(xs); + assert_eq!(summary.min, 1.0); + assert_eq!(summary.max, 4.0); + } + + #[test] + fn test_norm2() { + let val = &[958.0000000000, 924.0000000000]; + let summ = &Summary { + sum: 1882.0000000000, + min: 924.0000000000, + max: 958.0000000000, + mean: 941.0000000000, + median: 941.0000000000, + var: 578.0000000000, + std_dev: 24.0416305603, + std_dev_pct: 2.5549022912, + median_abs_dev: 25.2042000000, + median_abs_dev_pct: 2.6784484591, + quartiles: (932.5000000000, 941.0000000000, 949.5000000000), + iqr: 17.0000000000, + }; + check(val, summ); + } + #[test] + fn test_norm10narrow() { + let val = &[ + 966.0000000000, + 985.0000000000, + 1110.0000000000, + 848.0000000000, + 821.0000000000, + 975.0000000000, + 962.0000000000, + 1157.0000000000, + 1217.0000000000, + 955.0000000000, + ]; + let summ = &Summary { + sum: 9996.0000000000, + min: 821.0000000000, + max: 1217.0000000000, + mean: 999.6000000000, + median: 970.5000000000, + var: 16050.7111111111, + std_dev: 126.6914010938, + std_dev_pct: 12.6742097933, + median_abs_dev: 102.2994000000, + median_abs_dev_pct: 10.5408964451, + quartiles: (956.7500000000, 970.5000000000, 1078.7500000000), + iqr: 122.0000000000, + }; + check(val, summ); + } + #[test] + fn test_norm10medium() { + let val = &[ + 954.0000000000, + 1064.0000000000, + 855.0000000000, + 1000.0000000000, + 743.0000000000, + 1084.0000000000, + 704.0000000000, + 1023.0000000000, + 357.0000000000, + 869.0000000000, + ]; + let summ = &Summary { + sum: 8653.0000000000, + min: 357.0000000000, + max: 1084.0000000000, + mean: 865.3000000000, + median: 911.5000000000, + var: 48628.4555555556, + std_dev: 220.5186059170, + std_dev_pct: 25.4846418487, + median_abs_dev: 195.7032000000, + median_abs_dev_pct: 21.4704552935, + quartiles: (771.0000000000, 911.5000000000, 1017.2500000000), + iqr: 246.2500000000, + }; + check(val, summ); + } + #[test] + fn test_norm10wide() { + let val = &[ + 505.0000000000, + 497.0000000000, + 1591.0000000000, + 887.0000000000, + 1026.0000000000, + 136.0000000000, + 1580.0000000000, + 940.0000000000, + 754.0000000000, + 1433.0000000000, + ]; + let summ = &Summary { + sum: 9349.0000000000, + min: 136.0000000000, + max: 1591.0000000000, + mean: 934.9000000000, + median: 913.5000000000, + var: 239208.9888888889, + std_dev: 489.0899599142, + std_dev_pct: 52.3146817750, + median_abs_dev: 611.5725000000, + median_abs_dev_pct: 66.9482758621, + quartiles: (567.2500000000, 913.5000000000, 1331.2500000000), + iqr: 764.0000000000, + }; + check(val, summ); + } + #[test] + fn test_norm25verynarrow() { + let val = &[ + 991.0000000000, + 1018.0000000000, + 998.0000000000, + 1013.0000000000, + 974.0000000000, + 1007.0000000000, + 1014.0000000000, + 999.0000000000, + 1011.0000000000, + 978.0000000000, + 985.0000000000, + 999.0000000000, + 983.0000000000, + 982.0000000000, + 1015.0000000000, + 1002.0000000000, + 977.0000000000, + 948.0000000000, + 1040.0000000000, + 974.0000000000, + 996.0000000000, + 989.0000000000, + 1015.0000000000, + 994.0000000000, + 1024.0000000000, + ]; + let summ = &Summary { + sum: 24926.0000000000, + min: 948.0000000000, + max: 1040.0000000000, + mean: 997.0400000000, + median: 998.0000000000, + var: 393.2066666667, + std_dev: 19.8294393937, + std_dev_pct: 1.9888308788, + median_abs_dev: 22.2390000000, + median_abs_dev_pct: 2.2283567134, + quartiles: (983.0000000000, 998.0000000000, 1013.0000000000), + iqr: 30.0000000000, + }; + check(val, summ); + } + #[test] + fn test_exp10a() { + let val = &[ + 23.0000000000, + 11.0000000000, + 2.0000000000, + 57.0000000000, + 4.0000000000, + 12.0000000000, + 5.0000000000, + 29.0000000000, + 3.0000000000, + 21.0000000000, + ]; + let summ = &Summary { + sum: 167.0000000000, + min: 2.0000000000, + max: 57.0000000000, + mean: 16.7000000000, + median: 11.5000000000, + var: 287.7888888889, + std_dev: 16.9643416875, + std_dev_pct: 101.5828843560, + median_abs_dev: 13.3434000000, + median_abs_dev_pct: 116.0295652174, + quartiles: (4.2500000000, 11.5000000000, 22.5000000000), + iqr: 18.2500000000, + }; + check(val, summ); + } + #[test] + fn test_exp10b() { + let val = &[ + 24.0000000000, + 17.0000000000, + 6.0000000000, + 38.0000000000, + 25.0000000000, + 7.0000000000, + 51.0000000000, + 2.0000000000, + 61.0000000000, + 32.0000000000, + ]; + let summ = &Summary { + sum: 263.0000000000, + min: 2.0000000000, + max: 61.0000000000, + mean: 26.3000000000, + median: 24.5000000000, + var: 383.5666666667, + std_dev: 19.5848580967, + std_dev_pct: 74.4671410520, + median_abs_dev: 22.9803000000, + median_abs_dev_pct: 93.7971428571, + quartiles: (9.5000000000, 24.5000000000, 36.5000000000), + iqr: 27.0000000000, + }; + check(val, summ); + } + #[test] + fn test_exp10c() { + let val = &[ + 71.0000000000, + 2.0000000000, + 32.0000000000, + 1.0000000000, + 6.0000000000, + 28.0000000000, + 13.0000000000, + 37.0000000000, + 16.0000000000, + 36.0000000000, + ]; + let summ = &Summary { + sum: 242.0000000000, + min: 1.0000000000, + max: 71.0000000000, + mean: 24.2000000000, + median: 22.0000000000, + var: 458.1777777778, + std_dev: 21.4050876611, + std_dev_pct: 88.4507754589, + median_abs_dev: 21.4977000000, + median_abs_dev_pct: 97.7168181818, + quartiles: (7.7500000000, 22.0000000000, 35.0000000000), + iqr: 27.2500000000, + }; + check(val, summ); + } + #[test] + fn test_exp25() { + let val = &[ + 3.0000000000, + 24.0000000000, + 1.0000000000, + 19.0000000000, + 7.0000000000, + 5.0000000000, + 30.0000000000, + 39.0000000000, + 31.0000000000, + 13.0000000000, + 25.0000000000, + 48.0000000000, + 1.0000000000, + 6.0000000000, + 42.0000000000, + 63.0000000000, + 2.0000000000, + 12.0000000000, + 108.0000000000, + 26.0000000000, + 1.0000000000, + 7.0000000000, + 44.0000000000, + 25.0000000000, + 11.0000000000, + ]; + let summ = &Summary { + sum: 593.0000000000, + min: 1.0000000000, + max: 108.0000000000, + mean: 23.7200000000, + median: 19.0000000000, + var: 601.0433333333, + std_dev: 24.5161851301, + std_dev_pct: 103.3565983562, + median_abs_dev: 19.2738000000, + median_abs_dev_pct: 101.4410526316, + quartiles: (6.0000000000, 19.0000000000, 31.0000000000), + iqr: 25.0000000000, + }; + check(val, summ); + } + #[test] + fn test_binom25() { + let val = &[ + 18.0000000000, + 17.0000000000, + 27.0000000000, + 15.0000000000, + 21.0000000000, + 25.0000000000, + 17.0000000000, + 24.0000000000, + 25.0000000000, + 24.0000000000, + 26.0000000000, + 26.0000000000, + 23.0000000000, + 15.0000000000, + 23.0000000000, + 17.0000000000, + 18.0000000000, + 18.0000000000, + 21.0000000000, + 16.0000000000, + 15.0000000000, + 31.0000000000, + 20.0000000000, + 17.0000000000, + 15.0000000000, + ]; + let summ = &Summary { + sum: 514.0000000000, + min: 15.0000000000, + max: 31.0000000000, + mean: 20.5600000000, + median: 20.0000000000, + var: 20.8400000000, + std_dev: 4.5650848842, + std_dev_pct: 22.2037202539, + median_abs_dev: 5.9304000000, + median_abs_dev_pct: 29.6520000000, + quartiles: (17.0000000000, 20.0000000000, 24.0000000000), + iqr: 7.0000000000, + }; + check(val, summ); + } + #[test] + fn test_pois25lambda30() { + let val = &[ + 27.0000000000, + 33.0000000000, + 34.0000000000, + 34.0000000000, + 24.0000000000, + 39.0000000000, + 28.0000000000, + 27.0000000000, + 31.0000000000, + 28.0000000000, + 38.0000000000, + 21.0000000000, + 33.0000000000, + 36.0000000000, + 29.0000000000, + 37.0000000000, + 32.0000000000, + 34.0000000000, + 31.0000000000, + 39.0000000000, + 25.0000000000, + 31.0000000000, + 32.0000000000, + 40.0000000000, + 24.0000000000, + ]; + let summ = &Summary { + sum: 787.0000000000, + min: 21.0000000000, + max: 40.0000000000, + mean: 31.4800000000, + median: 32.0000000000, + var: 26.5933333333, + std_dev: 5.1568724372, + std_dev_pct: 16.3814245145, + median_abs_dev: 5.9304000000, + median_abs_dev_pct: 18.5325000000, + quartiles: (28.0000000000, 32.0000000000, 34.0000000000), + iqr: 6.0000000000, + }; + check(val, summ); + } + #[test] + fn test_pois25lambda40() { + let val = &[ + 42.0000000000, + 50.0000000000, + 42.0000000000, + 46.0000000000, + 34.0000000000, + 45.0000000000, + 34.0000000000, + 49.0000000000, + 39.0000000000, + 28.0000000000, + 40.0000000000, + 35.0000000000, + 37.0000000000, + 39.0000000000, + 46.0000000000, + 44.0000000000, + 32.0000000000, + 45.0000000000, + 42.0000000000, + 37.0000000000, + 48.0000000000, + 42.0000000000, + 33.0000000000, + 42.0000000000, + 48.0000000000, + ]; + let summ = &Summary { + sum: 1019.0000000000, + min: 28.0000000000, + max: 50.0000000000, + mean: 40.7600000000, + median: 42.0000000000, + var: 34.4400000000, + std_dev: 5.8685603004, + std_dev_pct: 14.3978417577, + median_abs_dev: 5.9304000000, + median_abs_dev_pct: 14.1200000000, + quartiles: (37.0000000000, 42.0000000000, 45.0000000000), + iqr: 8.0000000000, + }; + check(val, summ); + } + #[test] + fn test_pois25lambda50() { + let val = &[ + 45.0000000000, + 43.0000000000, + 44.0000000000, + 61.0000000000, + 51.0000000000, + 53.0000000000, + 59.0000000000, + 52.0000000000, + 49.0000000000, + 51.0000000000, + 51.0000000000, + 50.0000000000, + 49.0000000000, + 56.0000000000, + 42.0000000000, + 52.0000000000, + 51.0000000000, + 43.0000000000, + 48.0000000000, + 48.0000000000, + 50.0000000000, + 42.0000000000, + 43.0000000000, + 42.0000000000, + 60.0000000000, + ]; + let summ = &Summary { + sum: 1235.0000000000, + min: 42.0000000000, + max: 61.0000000000, + mean: 49.4000000000, + median: 50.0000000000, + var: 31.6666666667, + std_dev: 5.6273143387, + std_dev_pct: 11.3913245723, + median_abs_dev: 4.4478000000, + median_abs_dev_pct: 8.8956000000, + quartiles: (44.0000000000, 50.0000000000, 52.0000000000), + iqr: 8.0000000000, + }; + check(val, summ); + } + #[test] + fn test_unif25() { + let val = &[ + 99.0000000000, + 55.0000000000, + 92.0000000000, + 79.0000000000, + 14.0000000000, + 2.0000000000, + 33.0000000000, + 49.0000000000, + 3.0000000000, + 32.0000000000, + 84.0000000000, + 59.0000000000, + 22.0000000000, + 86.0000000000, + 76.0000000000, + 31.0000000000, + 29.0000000000, + 11.0000000000, + 41.0000000000, + 53.0000000000, + 45.0000000000, + 44.0000000000, + 98.0000000000, + 98.0000000000, + 7.0000000000, + ]; + let summ = &Summary { + sum: 1242.0000000000, + min: 2.0000000000, + max: 99.0000000000, + mean: 49.6800000000, + median: 45.0000000000, + var: 1015.6433333333, + std_dev: 31.8691595957, + std_dev_pct: 64.1488719719, + median_abs_dev: 45.9606000000, + median_abs_dev_pct: 102.1346666667, + quartiles: (29.0000000000, 45.0000000000, 79.0000000000), + iqr: 50.0000000000, + }; + check(val, summ); + } + + #[test] + fn test_sum_f64s() { + assert_eq!([0.5f64, 3.2321f64, 1.5678f64].sum(), 5.2999); + } + #[test] + fn test_sum_f64_between_ints_that_sum_to_0() { + assert_eq!([1e30f64, 1.2f64, -1e30f64].sum(), 1.2); + } +} + +#[cfg(test)] +mod bench { + extern crate test; + use self::test::Bencher; + use crate::stats::Stats; + + #[bench] + pub fn sum_three_items(b: &mut Bencher) { + b.iter(|| { + [1e20f64, 1.5f64, -1e20f64].sum(); + }) + } + #[bench] + pub fn sum_many_f64(b: &mut Bencher) { + let nums = [-1e30f64, 1e60, 1e30, 1.0, -1e60]; + let v = (0..500).map(|i| nums[i % 5]).collect::>(); + + b.iter(|| { + v.sum(); + }) + } + + #[bench] + pub fn no_iter(_: &mut Bencher) {} +}