use std::cmp::min;
use std::env;
use std::sync::mpsc::{channel, Receiver};
use std::thread;
use anyhow::{anyhow, Error};
use console::{strip_ansi_codes, style, truncate_str, Style, Term};
use crate::output::Output;
use crate::vmtest::Vmtest;
const WINDOW_LENGTH: usize = 10;
const EX_UNAVAILABLE: i32 = 69;
pub struct Ui {
vmtest: Vmtest,
}
struct Stage {
term: Term,
lines: Vec<String>,
expand: bool,
}
fn ui_enabled(term: &Term) -> bool {
if env::var_os("VMTEST_NO_UI").is_some() {
return false;
}
term.features().is_attended()
}
fn clear_last_lines(term: &Term, n: usize) {
if ui_enabled(term) {
term.clear_last_lines(n).unwrap();
}
}
impl Stage {
fn new(term: Term, heading: &str, previous: Option<Stage>) -> Self {
drop(previous);
term.write_line(heading).expect("Failed to write terminal");
Self {
term,
lines: Vec::new(),
expand: false,
}
}
fn window_size(&self) -> usize {
if ui_enabled(&self.term) {
min(self.lines.len(), WINDOW_LENGTH)
} else {
min(self.lines.len(), 1)
}
}
fn print_line(&mut self, line: &str, custom: Option<Style>) {
assert!(line.lines().count() <= 1, "Multiple lines provided");
clear_last_lines(&self.term, self.window_size());
let trimmed_line = line.trim_end();
let styled_line = if ui_enabled(&self.term) {
let stripped = strip_ansi_codes(trimmed_line);
let width = self.term.size_checked().map(|(_, w)| w).unwrap_or(u16::MAX);
let clipped = truncate_str(&stripped, width as usize, "...");
let styled = match &custom {
Some(s) => s.apply_to(clipped),
None => style(clipped).dim(),
};
styled.to_string()
} else {
trimmed_line.to_string()
};
self.lines.push(styled_line);
let window = self.lines.windows(self.window_size()).last().unwrap();
for line in window {
self.term.write_line(line).unwrap();
}
}
fn expand(&mut self, b: bool) {
self.expand = b;
}
}
impl Drop for Stage {
fn drop(&mut self) {
clear_last_lines(&self.term, self.window_size());
if self.expand && ui_enabled(&self.term) {
for line in &self.lines {
self.term
.write_line(line)
.expect("Failed to write terminal");
}
}
}
}
fn heading(name: &str, depth: usize) -> String {
let middle = "=".repeat((depth - 1) * 2);
format!("={}> {}", middle, name)
}
fn error_out_stage(stage: &mut Stage, err: &Error) {
let err = format!("{:?}", err);
for line in err.lines() {
stage.print_line(line, Some(Style::new().red().bright()));
}
stage.expand(true);
}
impl Ui {
pub fn new(vmtest: Vmtest) -> Self {
Self { vmtest }
}
fn target_ui(updates: Receiver<Output>, target: String, show_cmd: bool) -> Option<i32> {
let term = Term::stdout();
let mut stage = Stage::new(term.clone(), &heading(&target, 1), None);
let mut stages = 0;
let mut rc = Some(0);
loop {
let msg = match updates.recv() {
Ok(l) => l,
Err(_) => break,
};
match &msg {
Output::BootStart => {
stage = Stage::new(term.clone(), &heading("Booting", 2), Some(stage));
stages += 1;
}
Output::Boot(s) => stage.print_line(s, None),
Output::BootEnd(r) => {
if let Err(e) = r {
error_out_stage(&mut stage, e);
rc = None;
}
}
Output::SetupStart => {
stage = Stage::new(term.clone(), &heading("Setting up VM", 2), Some(stage));
stages += 1;
}
Output::Setup(s) => stage.print_line(s, None),
Output::SetupEnd(r) => {
if let Err(e) = r {
error_out_stage(&mut stage, e);
rc = None;
}
}
Output::CommandStart => {
stage = Stage::new(term.clone(), &heading("Running command", 2), Some(stage));
stages += 1;
}
Output::Command(s) => stage.print_line(s, None),
Output::CommandEnd(r) => {
if show_cmd {
stage.expand(true);
}
match r {
Ok(retval) => {
if *retval != 0 {
error_out_stage(
&mut stage,
&anyhow!("Command failed with exit code: {}", retval),
);
}
rc = Some(*retval as i32);
}
Err(e) => {
error_out_stage(&mut stage, e);
rc = None;
}
};
}
}
}
drop(stage);
match rc {
Some(0) => {
if !show_cmd {
clear_last_lines(&term, stages);
term.write_line("PASS").expect("Failed to write terminal");
}
}
Some(_) => {
if !show_cmd {
term.write_line("FAILED").expect("Failed to write terminal");
}
}
None => (),
}
rc
}
pub fn run(self, show_cmd: bool) -> i32 {
let mut failed = 0;
let targets = self.vmtest.targets();
let single_cmd = targets.len() == 1;
for (idx, target) in targets.iter().enumerate() {
let (sender, receiver) = channel::<Output>();
let name = target.name.clone();
let ui = thread::spawn(move || Self::target_ui(receiver, name, show_cmd));
self.vmtest.run_one(idx, sender);
let rc = ui
.join()
.expect("Failed to join UI thread")
.unwrap_or(EX_UNAVAILABLE);
if single_cmd {
return rc;
}
failed += match rc {
0 => 0,
_ => 1,
}
}
failed
}
}