Skip to content

Commit

Permalink
Add invocation logging (#5)
Browse files Browse the repository at this point in the history
Co-authored-by: ThomasLaPiana <tlapiana+github@pm.me>
  • Loading branch information
ThomasLaPiana and ThomasLaPiana authored Dec 29, 2023
1 parent 55b210a commit 0e38bcd
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 69 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.env
.rox

# Generated by Cargo
# will have compiled files and executables
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ name = "rox"
path = "src/lib.rs"

[dependencies]
chrono = "0.4.31"
clap = { version = "4.4.4", features = ["string", "cargo"] }
cli-table = "0.4.7"
colored = "2.0.4"
Expand Down
3 changes: 1 addition & 2 deletions roxfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ pipelines:
- name: ci
description: "Run all CI-related tasks"
stages:
- ["fmt", "clippy-ci"]
- ["test"]
- ["fmt", "clippy-ci", "test"]

tasks:
- name: "wt"
Expand Down
12 changes: 12 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@ pub fn cli_builder() -> Command {
.action(ArgAction::SetTrue)
.help("Skip the version and file requirement checks."),
)
.subcommand(
Command::new("logs")
.about("View logs for Rox invocations.")
.arg(
Arg::new("number")
.help("The number of logs to view.")
.required(false)
.short('n')
.value_parser(clap::value_parser!(i8))
.default_value("1"),
),
)
}

/// Build the `task` subcommand with individual tasks nested as subcommands
Expand Down
47 changes: 19 additions & 28 deletions src/execution.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,8 @@
//! This is the module responsible for executing tasks.
use crate::models::Task;
use crate::models::{PassFail, Task, TaskResult};
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use std::collections::HashMap;
use std::process::{Command, ExitStatus};

use rayon::iter::{IntoParallelRefIterator, ParallelIterator};

#[derive(PartialEq, Debug, Clone)]
pub enum PassFail {
Pass,
Fail,
}
impl std::fmt::Display for PassFail {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}

#[derive(PartialEq, Debug, Clone)]
pub struct TaskResult {
pub name: String,
pub result: PassFail,
pub elapsed_time: u64,
pub file_path: String,
}

pub fn get_result_passfail(result: Result<ExitStatus, std::io::Error>) -> PassFail {
// If the command doesn't exist, we get an error here
if result.is_err() {
Expand All @@ -38,7 +17,7 @@ pub fn get_result_passfail(result: Result<ExitStatus, std::io::Error>) -> PassFa
}

/// Run a Task
pub fn run_task(task: &Task) -> TaskResult {
pub fn run_task(task: &Task, stage_number: i8) -> TaskResult {
let start = std::time::Instant::now();

let workdir = task.workdir.clone().unwrap_or(".".to_string());
Expand All @@ -53,15 +32,18 @@ pub fn run_task(task: &Task) -> TaskResult {

TaskResult {
name: task.name.to_string(),
command: command.to_string(),
stage: stage_number + 1,
result: get_result_passfail(command_results),
elapsed_time: start.elapsed().as_secs(),
elapsed_time: start.elapsed().as_secs() as i64,
file_path: task.file_path.to_owned().unwrap(),
}
}

/// Execute a vector of Tasks, potentially in parallel
pub fn execute_tasks(
tasks: Vec<String>,
stage_number: i8,
task_map: &HashMap<String, Task>,
parallel: bool,
) -> Vec<TaskResult> {
Expand All @@ -84,9 +66,15 @@ pub fn execute_tasks(

// TODO: Add progress bars?
if parallel {
return task_stack.par_iter().map(run_task).collect();
return task_stack
.par_iter()
.map(|task| run_task(task, stage_number))
.collect();
} else {
return task_stack.iter().map(run_task).collect();
return task_stack
.iter()
.map(|task| run_task(task, stage_number))
.collect();
}
}

Expand All @@ -98,7 +86,10 @@ pub fn execute_stages(
) -> Vec<Vec<TaskResult>> {
let stage_results: Vec<Vec<TaskResult>> = stages
.iter()
.map(|stage| execute_tasks(stage.clone(), task_map, parallel))
.enumerate()
.map(|(stage_number, stage)| {
execute_tasks(stage.clone(), stage_number as i8, task_map, parallel)
})
.collect();
stage_results
}
41 changes: 30 additions & 11 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ mod utils;
mod version_requirements;

use crate::cli::{cli_builder, construct_cli};
use crate::execution::{execute_stages, execute_tasks, PassFail, TaskResult};
use crate::execution::{execute_stages, execute_tasks};
use crate::model_injection::{
inject_pipeline_metadata, inject_task_metadata, inject_template_values,
};
use crate::models::Validate;
use crate::models::{AllResults, Validate};
use crate::models::{PassFail, TaskResult};
use std::collections::HashMap;
use std::error::Error;

Expand All @@ -32,6 +33,7 @@ fn get_filepath_arg_value() -> String {
/// Entrypoint for the Crate CLI
pub fn rox() -> RoxResult<()> {
let start = std::time::Instant::now();
let execution_start = chrono::Utc::now().to_rfc3339();

// NOTE: Due to the dynamically generated nature of the CLI,
// It is required to parse the CLI matches twice. Once to get
Expand Down Expand Up @@ -91,32 +93,49 @@ pub fn rox() -> RoxResult<()> {
.flatten()
.map(|pipeline| (pipeline.name.to_owned(), pipeline)),
);
// Deconstruct the CLI commands and get the Pipeline object that was called
let (_, args) = cli_matches.subcommand().unwrap();
let subcommand_name = args.subcommand_name().unwrap_or("default");

// Execute the Task(s)
let results: Vec<Vec<TaskResult>> = match cli_matches.subcommand_name().unwrap() {
"logs" => {
let number = args.get_one::<i8>("number").unwrap();
output::display_logs(number);
std::process::exit(0);
}
"pl" => {
// Deconstruct the CLI commands and get the Pipeline object that was called
let (_, args) = cli_matches.subcommand().unwrap();
let pipeline_name = args.subcommand_name().unwrap();
let parallel = args.get_flag("parallel");
let execution_results = execute_stages(
&pipeline_map.get(pipeline_name).unwrap().stages,
&pipeline_map.get(subcommand_name).unwrap().stages,
&task_map,
parallel,
);
execution_results
}
"task" => {
let (_, args) = cli_matches.subcommand().unwrap();
let task_name = args.subcommand_name().unwrap().to_owned();
let execution_results = vec![execute_tasks(vec![task_name], &task_map, false)];
let execution_results = vec![execute_tasks(
vec![subcommand_name.to_string()],
0,
&task_map,
false,
)];
execution_results
}
command => {
println!("'{}' is not a valid subcommand!", command);
std::process::exit(2);
}
};
let results = AllResults {
job_name: subcommand_name.to_string(),
execution_time: execution_start,
results: results.into_iter().flatten().collect(),
};

let log_path = output::write_logs(&results);
println!("> Log file written to: {}", log_path);

output::display_execution_results(&results);
println!(
"> Total elapsed time: {}s | {}ms",
Expand All @@ -129,9 +148,9 @@ pub fn rox() -> RoxResult<()> {
}

/// Throw a non-zero exit if any Task(s) had a failing result
pub fn nonzero_exit_if_failure(results: &[Vec<TaskResult>]) {
pub fn nonzero_exit_if_failure(results: &AllResults) {
// TODO: Figure out a way to get this info without looping again
for result in results.iter().flatten() {
for result in results.results.iter() {
if result.result == PassFail::Fail {
std::process::exit(2)
}
Expand Down
32 changes: 31 additions & 1 deletion src/models.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,43 @@
//! Contains the Structs for the Schema of the Roxfile
//! as well as the validation logic.
use semver::{Version, VersionReq};
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use std::error::Error;
use std::fmt;
use std::str::FromStr;

use crate::utils::{color_print, ColorEnum};

/// Format for completed executions
#[derive(Serialize, Deserialize, Debug)]
pub struct AllResults {
pub job_name: String,
pub execution_time: String,
pub results: Vec<TaskResult>,
}

/// Enum for task command status
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
pub enum PassFail {
Pass,
Fail,
}
impl std::fmt::Display for PassFail {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}

#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
pub struct TaskResult {
pub name: String,
pub command: String,
pub stage: i8,
pub result: PassFail,
pub elapsed_time: i64,
pub file_path: String,
}

// Create a custom Error type for Validation
#[derive(Debug, Clone)]
pub struct ValidationError {
Expand Down
91 changes: 64 additions & 27 deletions src/output.rs
Original file line number Diff line number Diff line change
@@ -1,33 +1,71 @@
use crate::execution::{PassFail, TaskResult};
use crate::models::{AllResults, PassFail};
use cli_table::{format::Justify, print_stdout, Cell, Style, Table};
use colored::Colorize;

pub fn display_execution_results(results: &[Vec<TaskResult>]) {
const LOG_DIR: &str = ".rox";

/// Load execution results from a log file
pub fn display_logs(number: &i8) {
let mut filenames = std::fs::read_dir(LOG_DIR)
.unwrap()
.map(|res| res.map(|e| e.path()))
.collect::<Result<Vec<_>, std::io::Error>>()
.unwrap();
filenames.sort();

let results = filenames
.iter()
.take(*number as usize)
.map(|filename| {
let contents = std::fs::read_to_string(filename).unwrap();
serde_yaml::from_str(&contents).unwrap()
})
.collect::<Vec<AllResults>>();

for result in results.iter() {
println!("\n> {} | {}", result.job_name, result.execution_time);
display_execution_results(result)
}
}

/// Write the execution results to a log file
pub fn write_logs(results: &AllResults) -> String {
let filename = format!("rox-{}.log.yaml", chrono::Utc::now().to_rfc3339());
let filepath = format!("{}/{}", LOG_DIR, filename);

// Make sure the log directory exists
if !std::path::Path::new(LOG_DIR).exists() {
std::fs::create_dir(LOG_DIR).unwrap();
}

std::fs::write(filepath, serde_yaml::to_string(results).unwrap()).unwrap();
filename
}

/// Print the execution results in a pretty table format
pub fn display_execution_results(results: &AllResults) {
let mut table = Vec::new();

for (i, stage_results) in results.iter().enumerate() {
for result in stage_results {
table.push(vec![
result.name.to_owned().cell(),
format!("{}", i + 1).cell().justify(Justify::Center),
match result.result {
PassFail::Pass => result
.result
.to_string()
.green()
.cell()
.justify(Justify::Center),
PassFail::Fail => result
.result
.to_string()
.red()
.cell()
.justify(Justify::Center),
},
result.elapsed_time.cell().justify(Justify::Center),
result.file_path.to_owned().cell().justify(Justify::Right),
])
}
for result in results.results.iter() {
table.push(vec![
result.name.to_owned().cell(),
format!("{}", result.stage).cell().justify(Justify::Center),
match result.result {
PassFail::Pass => result
.result
.to_string()
.green()
.cell()
.justify(Justify::Center),
PassFail::Fail => result
.result
.to_string()
.red()
.cell()
.justify(Justify::Center),
},
result.elapsed_time.cell().justify(Justify::Center),
])
}

assert!(print_stdout(
Expand All @@ -37,8 +75,7 @@ pub fn display_execution_results(results: &[Vec<TaskResult>]) {
"Task".yellow().cell().bold(true),
"Stage".yellow().cell().bold(true),
"Result".yellow().cell().bold(true),
"Run Time(s)".yellow().cell().bold(true),
"File".yellow().cell().bold(true),
"Run Time (sec)".yellow().cell().bold(true),
])
.bold(true),
)
Expand Down

0 comments on commit 0e38bcd

Please sign in to comment.