Added error util, finished network graph.

This commit is contained in:
ClementTsang 2019-09-14 21:48:29 -04:00
parent b75374be76
commit 4846175638
11 changed files with 248 additions and 94 deletions

View file

@ -21,7 +21,7 @@ Currently, I'm unable to test on MacOS, so I'm not sure how well this will work,
* As mentioned, this project is very much inspired by both [gotop](https://github.com/cjbassi/gotop) and [gtop](https://github.com/aksakalli/gtop) .
* This application was written with the following libraries:
* clap
* [clap](https://github.com/clap-rs/clap)
* [crossterm](https://github.com/TimonPost/crossterm)
* [heim](https://github.com/heim-rs/heim)
* [sysinfo](https://github.com/GuillaumeGomez/sysinfo)

View file

@ -10,6 +10,8 @@
* Add custom error because it's really messy
* Remove any ``unwrap()``.
* Scrolling event
* Keybindings

View file

@ -1,7 +1,6 @@
pub mod data_collection;
use data_collection::{processes, temperature};
#[allow(dead_code)]
pub struct App {
pub should_quit : bool,
pub process_sorting_type : processes::ProcessSorting,
@ -35,6 +34,7 @@ impl App {
'k' => self.on_up(),
'l' => self.on_left(),
'c' => {
// TODO: This should depend on what tile you're on!
match self.process_sorting_type {
processes::ProcessSorting::CPU => self.process_sorting_reverse = !self.process_sorting_reverse,
_ => {

View file

@ -1,3 +1,5 @@
//! This is the main file to house data collection functions.
use std::collections::HashMap;
use sysinfo::{System, SystemExt};

View file

@ -68,5 +68,17 @@ pub async fn get_disk_usage_list() -> Result<Vec<DiskData>, heim::Error> {
});
}
vec_disks.sort_by(|a, b| {
if a.name < b.name {
std::cmp::Ordering::Less
}
else if a.name > b.name {
std::cmp::Ordering::Greater
}
else {
std::cmp::Ordering::Equal
}
});
Ok(vec_disks)
}

View file

@ -2,6 +2,7 @@ use std::time::Instant;
use sysinfo::{NetworkExt, System, SystemExt};
#[derive(Clone)]
/// Note all values are in bytes...
pub struct NetworkData {
pub rx : u64,
pub tx : u64,

View file

@ -1,4 +1,3 @@
use std::io;
use tui::{
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
@ -6,6 +5,8 @@ use tui::{
Terminal,
};
use crate::utils::error;
const COLOUR_LIST : [Color; 6] = [Color::LightRed, Color::LightGreen, Color::LightYellow, Color::LightBlue, Color::LightCyan, Color::LightMagenta];
const TEXT_COLOUR : Color = Color::Gray;
const GRAPH_COLOUR : Color = Color::Gray;
@ -13,6 +14,8 @@ const BORDER_STYLE_COLOUR : Color = Color::Gray;
#[derive(Default)]
pub struct CanvasData {
pub network_data_rx : Vec<(f64, f64)>,
pub network_data_tx : Vec<(f64, f64)>,
pub disk_data : Vec<Vec<String>>,
pub temp_sensor_data : Vec<Vec<String>>,
pub process_data : Vec<Vec<String>>,
@ -21,39 +24,28 @@ pub struct CanvasData {
pub cpu_data : Vec<(String, Vec<(f64, f64)>)>,
}
// TODO: Change the error
pub fn draw_data<B : tui::backend::Backend>(terminal : &mut Terminal<B>, canvas_data : &CanvasData) -> Result<(), io::Error> {
pub fn draw_data<B : tui::backend::Backend>(terminal : &mut Terminal<B>, canvas_data : &CanvasData) -> error::Result<()> {
let border_style : Style = Style::default().fg(BORDER_STYLE_COLOUR);
let temperature_rows = canvas_data.temp_sensor_data.iter().map(|sensor| Row::StyledData(sensor.iter(), Style::default().fg(TEXT_COLOUR)));
let disk_rows = canvas_data.disk_data.iter().map(|disk| Row::StyledData(disk.iter(), Style::default().fg(TEXT_COLOUR)));
let process_rows = canvas_data.process_data.iter().map(|process| Row::StyledData(process.iter(), Style::default().fg(TEXT_COLOUR)));
// TODO: Convert this into a separate func!
terminal.draw(|mut f| {
debug!("Drawing!");
let vertical_chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints([Constraint::Percentage(34), Constraint::Percentage(34), Constraint::Percentage(32)].as_ref())
.constraints([Constraint::Percentage(32), Constraint::Percentage(34), Constraint::Percentage(34)].as_ref())
.split(f.size());
let _top_chunks = Layout::default()
.direction(Direction::Horizontal)
.margin(0)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(vertical_chunks[0]);
let middle_chunks = Layout::default()
.direction(Direction::Horizontal)
.margin(0)
.constraints([Constraint::Percentage(75), Constraint::Percentage(25)].as_ref())
.constraints([Constraint::Percentage(65), Constraint::Percentage(35)].as_ref())
.split(vertical_chunks[1]);
let _middle_divided_chunk_1 = Layout::default()
.direction(Direction::Vertical)
.margin(0)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(middle_chunks[0]);
let _middle_divided_chunk_2 = Layout::default()
let middle_divided_chunk_2 = Layout::default()
.direction(Direction::Vertical)
.margin(0)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
@ -64,23 +56,12 @@ pub fn draw_data<B : tui::backend::Backend>(terminal : &mut Terminal<B>, canvas_
.margin(0)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(vertical_chunks[2]);
let bottom_divided_chunk_1 = Layout::default()
.direction(Direction::Vertical)
.margin(0)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(bottom_chunks[0]);
let bottom_divided_chunk_1_1 = Layout::default()
.direction(Direction::Horizontal)
.margin(0)
.constraints([Constraint::Percentage(40), Constraint::Percentage(60)].as_ref())
.split(bottom_divided_chunk_1[0]);
// Set up blocks and their components
// CPU usage graph
{
let x_axis : Axis<String> = Axis::default().style(Style::default().fg(GRAPH_COLOUR)).bounds([0.0, 600_000.0]);
let y_axis = Axis::default().style(Style::default().fg(GRAPH_COLOUR)).bounds([0.0, 100.0]).labels(&["0.0", "50.0", "100.0"]);
let y_axis = Axis::default().style(Style::default().fg(GRAPH_COLOUR)).bounds([0.0, 100.0]).labels(&["0%", "50%", "100%"]);
let mut dataset_vector : Vec<Dataset> = Vec::new();
for (i, cpu) in canvas_data.cpu_data.iter().enumerate() {
@ -104,7 +85,7 @@ pub fn draw_data<B : tui::backend::Backend>(terminal : &mut Terminal<B>, canvas_
//Memory usage graph
{
let x_axis : Axis<String> = Axis::default().style(Style::default().fg(GRAPH_COLOUR)).bounds([0.0, 600_000.0]);
let y_axis = Axis::default().style(Style::default().fg(GRAPH_COLOUR)).bounds([0.0, 100.0]).labels(&["0", "50", "100"]);
let y_axis = Axis::default().style(Style::default().fg(GRAPH_COLOUR)).bounds([0.0, 100.0]).labels(&["0%", "50%", "100%"]);
Chart::default()
.block(Block::default().title("Memory Usage").borders(Borders::ALL).border_style(border_style))
.x_axis(x_axis)
@ -113,45 +94,58 @@ pub fn draw_data<B : tui::backend::Backend>(terminal : &mut Terminal<B>, canvas_
Dataset::default()
.name(&("MEM :".to_string() + &format!("{:3}%", (canvas_data.mem_data.last().unwrap_or(&(0_f64, 0_f64)).1.round() as u64))))
.marker(Marker::Dot)
.style(Style::default().fg(Color::LightRed))
.style(Style::default().fg(Color::LightBlue))
.data(&canvas_data.mem_data),
Dataset::default()
.name(&("SWAP:".to_string() + &format!("{:3}%", (canvas_data.swap_data.last().unwrap_or(&(0_f64, 0_f64)).1.round() as u64))))
.marker(Marker::Dot)
.style(Style::default().fg(Color::LightGreen))
.style(Style::default().fg(Color::LightYellow))
.data(&canvas_data.swap_data),
])
.render(&mut f, middle_chunks[0]);
}
// Network graph
Block::default().title("Network").borders(Borders::ALL).border_style(border_style).render(&mut f, middle_chunks[1]);
// Temperature table
{
let width = f64::from(bottom_divided_chunk_1_1[0].width);
let width = f64::from(middle_divided_chunk_2[0].width);
Table::new(["Sensor", "Temp"].iter(), temperature_rows)
.block(Block::default().title("Temperatures").borders(Borders::ALL).border_style(border_style))
.header_style(Style::default().fg(Color::LightBlue))
.widths(&[(width * 0.45) as u16, (width * 0.4) as u16])
.render(&mut f, bottom_divided_chunk_1_1[0]);
.render(&mut f, middle_divided_chunk_2[0]);
}
// Temp graph
Block::default()
.title("Temperatures")
.borders(Borders::ALL)
.border_style(border_style)
.render(&mut f, bottom_divided_chunk_1_1[1]);
// Disk usage table
{
let width = f64::from(bottom_divided_chunk_1[1].width);
let width = f64::from(middle_divided_chunk_2[1].width);
Table::new(["Disk", "Mount", "Used", "Total", "Free"].iter(), disk_rows)
.block(Block::default().title("Disk Usage").borders(Borders::ALL).border_style(border_style))
.header_style(Style::default().fg(Color::LightBlue).modifier(Modifier::BOLD))
.widths(&[(width * 0.25) as u16, (width * 0.2) as u16, (width * 0.15) as u16, (width * 0.15) as u16, (width * 0.15) as u16])
.render(&mut f, bottom_divided_chunk_1[1]);
.render(&mut f, middle_divided_chunk_2[1]);
}
// Network graph
{
let x_axis : Axis<String> = Axis::default().style(Style::default().fg(GRAPH_COLOUR)).bounds([0.0, 600_000.0]);
let y_axis = Axis::default().style(Style::default().fg(GRAPH_COLOUR)).bounds([0.0, 1000.0]).labels(&["0Kb", "1000Kb"]);
Chart::default()
.block(Block::default().title("Network").borders(Borders::ALL).border_style(border_style))
.x_axis(x_axis)
.y_axis(y_axis)
.datasets(&[
Dataset::default()
.name(&("RX:".to_string() + &format!("{:3}%", (canvas_data.network_data_rx.last().unwrap_or(&(0_f64, 0_f64)).1.round() as u64))))
.marker(Marker::Dot)
.style(Style::default().fg(Color::LightBlue))
.data(&canvas_data.network_data_rx),
Dataset::default()
.name(&("TX:".to_string() + &format!("{:3}%", (canvas_data.network_data_tx.last().unwrap_or(&(0_f64, 0_f64)).1.round() as u64))))
.marker(Marker::Dot)
.style(Style::default().fg(Color::LightYellow))
.data(&canvas_data.network_data_tx),
])
.render(&mut f, bottom_chunks[0]);
}
// Processes table

View file

@ -1,36 +1,39 @@
#![feature(async_closure)]
#[macro_use]
extern crate log;
#[macro_use]
extern crate clap;
#[macro_use]
extern crate failure;
use crossterm::{input, AlternateScreen, InputEvent, KeyEvent};
use std::{io, sync::mpsc, thread, time::Duration};
use std::{sync::mpsc, thread, time::Duration};
use tui::{backend::CrosstermBackend, Terminal};
mod app;
use app::data_collection;
mod utils {
pub mod error;
pub mod logging;
}
use utils::error::{self, RustopError};
mod canvas;
#[macro_use]
extern crate log;
#[macro_use]
extern crate clap;
// End imports
enum Event<I> {
Input(I),
Update(Box<app::data_collection::Data>),
Update(Box<data_collection::Data>),
}
const STALE_MAX_MILLISECONDS : u64 = 60 * 1000;
const TICK_RATE_IN_MILLISECONDS : u64 = 250;
const STALE_MAX_MILLISECONDS : u64 = 60 * 1000; // We wish to store at most 60 seconds worth of data. This may change in the future, or be configurable.
const TICK_RATE_IN_MILLISECONDS : u64 = 200; // We use this as it's a good value to work with.
#[tokio::main]
async fn main() -> Result<(), io::Error> {
fn main() -> error::Result<()> {
let _log = utils::logging::init_logger(); // TODO: Error handling
// Parse command line options
let matches = clap_app!(app =>
(name: "rustop")
(version: crate_version!())
@ -38,35 +41,40 @@ async fn main() -> Result<(), io::Error> {
(about: "A graphical top clone.")
(@arg THEME: -t --theme +takes_value "Sets a colour theme.")
(@arg AVG_CPU: -a --avgcpu "Enables showing the average CPU usage.")
(@arg DEBUG: -d --debug "Enables debug mode.")
(@group TEMPERATURE_TYPE =>
(@arg celsius : -c --celsius "Sets the temperature type to Celsius. This is the default option.")
(@arg fahrenheit : -f --fahrenheit "Sets the temperature type to Fahrenheit.")
(@arg kelvin : -k --kelvin "Sets the temperature type to Kelvin.")
(@arg CELSIUS : -c --celsius "Sets the temperature type to Celsius. This is the default option.")
(@arg FAHRENHEIT : -f --fahrenheit "Sets the temperature type to Fahrenheit.")
(@arg KELVIN : -k --kelvin "Sets the temperature type to Kelvin.")
)
(@arg RATE: -r --rate +takes_value "Sets a refresh rate in milliseconds, min is 250ms, defaults to 1000ms. Higher values may take more resources.")
)
.after_help("Themes:")
.get_matches();
let screen = AlternateScreen::to_alternate(true)?;
let backend = CrosstermBackend::with_alternate_screen(screen)?;
let mut terminal = Terminal::new(backend)?;
let update_rate_in_milliseconds : u64 = matches.value_of("RATE").unwrap_or("1000").parse::<u64>()?;
let update_rate_in_milliseconds : u64 = matches.value_of("rate").unwrap_or("1000").parse::<u64>().unwrap_or(1000);
let temperature_type = if matches.is_present("fahrenheit") {
app::data_collection::temperature::TemperatureType::Fahrenheit
if update_rate_in_milliseconds < 250 {
return Err(RustopError::InvalidArg {
message : "Please set your rate to be greater than 250 milliseconds.".to_string(),
});
}
else if matches.is_present("kelvin") {
app::data_collection::temperature::TemperatureType::Kelvin
let temperature_type = if matches.is_present("FAHRENHEIT") {
data_collection::temperature::TemperatureType::Fahrenheit
}
else if matches.is_present("KELVIN") {
data_collection::temperature::TemperatureType::Kelvin
}
else {
app::data_collection::temperature::TemperatureType::Celsius
data_collection::temperature::TemperatureType::Celsius
};
let show_average_cpu = matches.is_present("AVG_CPU");
let mut app = app::App::new(show_average_cpu, temperature_type, if update_rate_in_milliseconds < 250 { 250 } else { update_rate_in_milliseconds });
// Create "app" struct, which will control most of the program and store settings/state
// TODO: Error handling here because users may be stupid and pass INT_MAX.
let mut app = app::App::new(show_average_cpu, temperature_type, update_rate_in_milliseconds);
terminal.hide_cursor()?;
// Set up input handling
let (tx, rx) = mpsc::channel();
{
@ -85,7 +93,7 @@ async fn main() -> Result<(), io::Error> {
}
// Event loop
let mut data_state = app::data_collection::DataState::default();
let mut data_state = data_collection::DataState::default();
data_state.init();
data_state.set_stale_max_seconds(STALE_MAX_MILLISECONDS);
data_state.set_temperature_type(app.temperature_type.clone());
@ -94,16 +102,21 @@ async fn main() -> Result<(), io::Error> {
thread::spawn(move || {
let tx = tx.clone();
loop {
futures::executor::block_on(data_state.update_data()); // TODO: Fix
futures::executor::block_on(data_state.update_data());
tx.send(Event::Update(Box::from(data_state.data.clone()))).unwrap();
thread::sleep(Duration::from_millis(update_rate_in_milliseconds));
}
});
}
// Set up up tui and crossterm
let screen = AlternateScreen::to_alternate(true)?;
let backend = CrosstermBackend::with_alternate_screen(screen)?;
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
terminal.clear()?;
let mut app_data = app::data_collection::Data::default();
let mut app_data = data_collection::Data::default();
let mut canvas_data = canvas::CanvasData::default();
loop {
@ -118,6 +131,7 @@ async fn main() -> Result<(), io::Error> {
KeyEvent::Up => app.on_up(),
KeyEvent::Down => app.on_down(),
KeyEvent::Ctrl('c') => break,
KeyEvent::Esc => break,
_ => {}
}
@ -134,6 +148,9 @@ async fn main() -> Result<(), io::Error> {
data_collection::processes::sort_processes(&mut app_data.list_of_processes, &app.process_sorting_type, app.process_sorting_reverse);
// Convert all data into tui components
let network_data = update_network_data_points(&app_data);
canvas_data.network_data_rx = network_data.rx;
canvas_data.network_data_tx = network_data.tx;
canvas_data.disk_data = update_disk_row(&app_data);
canvas_data.temp_sensor_data = update_temp_row(&app_data, &app.temperature_type);
canvas_data.process_data = update_process_row(&app_data);
@ -152,10 +169,11 @@ async fn main() -> Result<(), io::Error> {
canvas::draw_data(&mut terminal, &canvas_data)?;
}
debug!("Terminating.");
Ok(())
}
fn update_temp_row(app_data : &app::data_collection::Data, temp_type : &app::data_collection::temperature::TemperatureType) -> Vec<Vec<String>> {
fn update_temp_row(app_data : &data_collection::Data, temp_type : &data_collection::temperature::TemperatureType) -> Vec<Vec<String>> {
let mut sensor_vector : Vec<Vec<String>> = Vec::new();
for sensor in &app_data.list_of_temperature_sensor {
@ -163,9 +181,9 @@ fn update_temp_row(app_data : &app::data_collection::Data, temp_type : &app::dat
sensor.component_name.to_string(),
(sensor.temperature.ceil() as u64).to_string()
+ match temp_type {
app::data_collection::temperature::TemperatureType::Celsius => "C",
app::data_collection::temperature::TemperatureType::Kelvin => "K",
app::data_collection::temperature::TemperatureType::Fahrenheit => "F",
data_collection::temperature::TemperatureType::Celsius => "C",
data_collection::temperature::TemperatureType::Kelvin => "K",
data_collection::temperature::TemperatureType::Fahrenheit => "F",
},
]);
}
@ -173,22 +191,33 @@ fn update_temp_row(app_data : &app::data_collection::Data, temp_type : &app::dat
sensor_vector
}
fn update_disk_row(app_data : &app::data_collection::Data) -> Vec<Vec<String>> {
// TODO: IO count
fn update_disk_row(app_data : &data_collection::Data) -> Vec<Vec<String>> {
let mut disk_vector : Vec<Vec<String>> = Vec::new();
for disk in &app_data.list_of_disks {
disk_vector.push(vec![
disk.name.to_string(),
disk.mount_point.to_string(),
format!("{:.1}%", disk.used_space as f64 / disk.total_space as f64 * 100_f64),
(disk.free_space / 1024).to_string() + "GB",
(disk.total_space / 1024).to_string() + "GB",
if disk.free_space < 1024 {
disk.free_space.to_string() + "MB"
}
else {
(disk.free_space / 1024).to_string() + "GB"
},
if disk.total_space < 1024 {
disk.total_space.to_string() + "MB"
}
else {
(disk.total_space / 1024).to_string() + "GB"
},
]);
}
disk_vector
}
fn update_process_row(app_data : &app::data_collection::Data) -> Vec<Vec<String>> {
fn update_process_row(app_data : &data_collection::Data) -> Vec<Vec<String>> {
let mut process_vector : Vec<Vec<String>> = Vec::new();
for process in &app_data.list_of_processes {
@ -219,7 +248,7 @@ fn update_process_row(app_data : &app::data_collection::Data) -> Vec<Vec<String>
process_vector
}
fn update_cpu_data_points(show_avg_cpu : bool, app_data : &app::data_collection::Data) -> Vec<(String, Vec<(f64, f64)>)> {
fn update_cpu_data_points(show_avg_cpu : bool, app_data : &data_collection::Data) -> Vec<(String, Vec<(f64, f64)>)> {
let mut cpu_data_vector : Vec<(String, Vec<(f64, f64)>)> = Vec::new();
let mut cpu_collection : Vec<Vec<(f64, f64)>> = Vec::new();
@ -256,15 +285,15 @@ fn update_cpu_data_points(show_avg_cpu : bool, app_data : &app::data_collection:
cpu_data_vector
}
fn update_mem_data_points(app_data : &app::data_collection::Data) -> Vec<(f64, f64)> {
fn update_mem_data_points(app_data : &data_collection::Data) -> Vec<(f64, f64)> {
convert_mem_data(&app_data.memory)
}
fn update_swap_data_points(app_data : &app::data_collection::Data) -> Vec<(f64, f64)> {
fn update_swap_data_points(app_data : &data_collection::Data) -> Vec<(f64, f64)> {
convert_mem_data(&app_data.swap)
}
fn convert_mem_data(mem_data : &[app::data_collection::mem::MemData]) -> Vec<(f64, f64)> {
fn convert_mem_data(mem_data : &[data_collection::mem::MemData]) -> Vec<(f64, f64)> {
let mut result : Vec<(f64, f64)> = Vec::new();
for data in mem_data {
@ -274,8 +303,41 @@ fn convert_mem_data(mem_data : &[app::data_collection::mem::MemData]) -> Vec<(f6
((STALE_MAX_MILLISECONDS as f64 - current_time.duration_since(data.instant).as_millis() as f64) * 10_f64).floor(),
data.mem_used_in_mb as f64 / data.mem_total_in_mb as f64 * 100_f64,
));
debug!("Pushed: ({}, {})", result.last().unwrap().0, result.last().unwrap().1);
//debug!("Pushed: ({}, {})", result.last().unwrap().0, result.last().unwrap().1);
}
result
}
struct ConvertedNetworkData {
rx : Vec<(f64, f64)>,
tx : Vec<(f64, f64)>,
}
fn update_network_data_points(app_data : &data_collection::Data) -> ConvertedNetworkData {
convert_network_data_points(&app_data.network)
}
fn convert_network_data_points(network_data : &[data_collection::network::NetworkData]) -> ConvertedNetworkData {
let mut rx : Vec<(f64, f64)> = Vec::new();
let mut tx : Vec<(f64, f64)> = Vec::new();
for data in network_data {
let current_time = std::time::Instant::now();
rx.push((
((STALE_MAX_MILLISECONDS as f64 - current_time.duration_since(data.instant).as_millis() as f64) * 10_f64).floor(),
data.rx as f64 / 1024.0,
));
tx.push((
((STALE_MAX_MILLISECONDS as f64 - current_time.duration_since(data.instant).as_millis() as f64) * 10_f64).floor(),
data.tx as f64 / 1024.0,
));
debug!("Pushed rx: ({}, {})", rx.last().unwrap().0, rx.last().unwrap().1);
debug!("Pushed tx: ({}, {})", tx.last().unwrap().0, tx.last().unwrap().1);
}
ConvertedNetworkData { rx, tx }
}

View file

@ -1,2 +1,43 @@
#[allow(dead_code)]
pub struct RustopError {}
use failure::Fail;
use std::result;
/// A type alias for handling errors related to Rustop.
pub type Result<T> = result::Result<T, RustopError>;
/// An error that can occur while Rustop runs.
#[derive(Debug, Fail)]
pub enum RustopError {
/// An error when there is an IO exception.
///
/// The data provided is the error found.
#[fail(display = "ERROR: Encountered an IO exception: {}", message)]
InvalidIO { message : String },
/// An error when there is an invalid argument passed in.
///
/// The data provided is the error found.
#[fail(display = "ERROR: Invalid argument: {}", message)]
InvalidArg { message : String },
/// An error when the heim library encounters a problem.
///
/// The data provided is the error found.
#[fail(display = "ERROR: Invalid error during data collection due to Heim: {}", message)]
InvalidHeim { message : String },
}
impl From<std::io::Error> for RustopError {
fn from(err : std::io::Error) -> Self {
RustopError::InvalidIO { message : err.to_string() }
}
}
impl From<heim::Error> for RustopError {
fn from(err : heim::Error) -> Self {
RustopError::InvalidHeim { message : err.to_string() }
}
}
impl From<std::num::ParseIntError> for RustopError {
fn from(err : std::num::ParseIntError) -> Self {
RustopError::InvalidArg { message : err.to_string() }
}
}

40
tests/arg_tests.rs Normal file
View file

@ -0,0 +1,40 @@
use std::process::Command; // Run programs
use assert_cmd::prelude::*; // Add methods on commands
use predicates::prelude::*; // Used for writing assertions
#[test]
fn test_small_rate -> Result<(), Box<std::error::Error>> {
let mut cmd = Command::main_binary()?;
cmd.arg("-r")
.arg("249");
cmd.assert()
.failure()
.stderr(predicate::str::contains("rate"));
Ok(())
}
#[test]
fn test_negative_rate -> Result<(), Box<std::error::Error>> {
// This test should auto fail due to how clap works
let mut cmd = Command::main_binary()?;
cmd.arg("-r")
.arg("-1000");
cmd.assert()
.failure()
.stderr(predicate::str::contains("valid"));
Ok(())
}
#[test]
fn test_invalid_rate -> Result<(), Box<std::error::Error>> {
let mut cmd = Command::main_binary()?;
cmd.arg("-r")
.arg("1000 - 100");
cmd.assert()
.failure()
.stderr(predicate::str::contains("digit"));
Ok(())
}

View file