use crate::parser::models::{JobStatus, StageStatus, StepStatus};
use std::time::Duration;
use tokio::sync::mpsc;
pub type ProgressSender = mpsc::UnboundedSender<ExecutionEvent>;
pub type ProgressReceiver = mpsc::UnboundedReceiver<ExecutionEvent>;
pub fn progress_channel() -> (ProgressSender, ProgressReceiver) {
mpsc::unbounded_channel()
}
#[derive(Debug, Clone)]
pub enum ExecutionEvent {
PipelineStarted {
pipeline_name: String,
total_stages: usize,
},
PipelineCompleted {
pipeline_name: String,
success: bool,
duration: Duration,
},
StageStarted {
stage_name: String,
display_name: Option<String>,
total_jobs: usize,
},
StageCompleted {
stage_name: String,
status: StageStatus,
duration: Duration,
},
StageSkipped { stage_name: String, reason: String },
JobStarted {
stage_name: String,
job_name: String,
display_name: Option<String>,
matrix_instance: Option<String>,
total_steps: usize,
},
JobCompleted {
stage_name: String,
job_name: String,
matrix_instance: Option<String>,
status: JobStatus,
duration: Duration,
},
JobSkipped {
stage_name: String,
job_name: String,
reason: String,
},
StepStarted {
stage_name: String,
job_name: String,
step_name: Option<String>,
display_name: Option<String>,
step_index: usize,
},
StepOutput {
stage_name: String,
job_name: String,
step_name: Option<String>,
step_index: usize,
output: String,
is_error: bool,
},
StepCompleted {
stage_name: String,
job_name: String,
step_name: Option<String>,
step_index: usize,
status: StepStatus,
duration: Duration,
exit_code: Option<i32>,
},
StepSkipped {
stage_name: String,
job_name: String,
step_name: Option<String>,
step_index: usize,
reason: String,
},
VariableSet {
stage_name: String,
job_name: String,
name: String,
value: String,
is_output: bool,
is_secret: bool,
},
Log {
level: LogLevel,
message: String,
stage_name: Option<String>,
job_name: Option<String>,
},
Error {
message: String,
stage_name: Option<String>,
job_name: Option<String>,
step_index: Option<usize>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogLevel {
Debug,
Info,
Warning,
Error,
}
impl ExecutionEvent {
pub fn pipeline_started(name: impl Into<String>, total_stages: usize) -> Self {
Self::PipelineStarted {
pipeline_name: name.into(),
total_stages,
}
}
pub fn pipeline_completed(name: impl Into<String>, success: bool, duration: Duration) -> Self {
Self::PipelineCompleted {
pipeline_name: name.into(),
success,
duration,
}
}
pub fn stage_started(
name: impl Into<String>,
display_name: Option<String>,
total_jobs: usize,
) -> Self {
Self::StageStarted {
stage_name: name.into(),
display_name,
total_jobs,
}
}
pub fn stage_completed(
name: impl Into<String>,
status: StageStatus,
duration: Duration,
) -> Self {
Self::StageCompleted {
stage_name: name.into(),
status,
duration,
}
}
pub fn job_started(
stage_name: impl Into<String>,
job_name: impl Into<String>,
display_name: Option<String>,
matrix_instance: Option<String>,
total_steps: usize,
) -> Self {
Self::JobStarted {
stage_name: stage_name.into(),
job_name: job_name.into(),
display_name,
matrix_instance,
total_steps,
}
}
pub fn job_completed(
stage_name: impl Into<String>,
job_name: impl Into<String>,
matrix_instance: Option<String>,
status: JobStatus,
duration: Duration,
) -> Self {
Self::JobCompleted {
stage_name: stage_name.into(),
job_name: job_name.into(),
matrix_instance,
status,
duration,
}
}
pub fn step_started(
stage_name: impl Into<String>,
job_name: impl Into<String>,
step_name: Option<String>,
display_name: Option<String>,
step_index: usize,
) -> Self {
Self::StepStarted {
stage_name: stage_name.into(),
job_name: job_name.into(),
step_name,
display_name,
step_index,
}
}
pub fn step_output(
stage_name: impl Into<String>,
job_name: impl Into<String>,
step_name: Option<String>,
step_index: usize,
output: impl Into<String>,
is_error: bool,
) -> Self {
Self::StepOutput {
stage_name: stage_name.into(),
job_name: job_name.into(),
step_name,
step_index,
output: output.into(),
is_error,
}
}
pub fn step_completed(
stage_name: impl Into<String>,
job_name: impl Into<String>,
step_name: Option<String>,
step_index: usize,
status: StepStatus,
duration: Duration,
exit_code: Option<i32>,
) -> Self {
Self::StepCompleted {
stage_name: stage_name.into(),
job_name: job_name.into(),
step_name,
step_index,
status,
duration,
exit_code,
}
}
pub fn info(
message: impl Into<String>,
stage_name: Option<String>,
job_name: Option<String>,
) -> Self {
Self::Log {
level: LogLevel::Info,
message: message.into(),
stage_name,
job_name,
}
}
pub fn warning(
message: impl Into<String>,
stage_name: Option<String>,
job_name: Option<String>,
) -> Self {
Self::Log {
level: LogLevel::Warning,
message: message.into(),
stage_name,
job_name,
}
}
pub fn error(
message: impl Into<String>,
stage_name: Option<String>,
job_name: Option<String>,
) -> Self {
Self::Log {
level: LogLevel::Error,
message: message.into(),
stage_name,
job_name,
}
}
pub fn execution_error(
message: impl Into<String>,
stage_name: Option<String>,
job_name: Option<String>,
step_index: Option<usize>,
) -> Self {
Self::Error {
message: message.into(),
stage_name,
job_name,
step_index,
}
}
}
pub trait EventSender {
fn send_event(&self, event: ExecutionEvent);
}
impl EventSender for ProgressSender {
fn send_event(&self, event: ExecutionEvent) {
let _ = self.send(event);
}
}
impl EventSender for Option<ProgressSender> {
fn send_event(&self, event: ExecutionEvent) {
if let Some(sender) = self {
let _ = sender.send(event);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_progress_channel() {
let (tx, mut rx) = progress_channel();
tx.send_event(ExecutionEvent::pipeline_started("test", 2));
tx.send_event(ExecutionEvent::stage_started("Build", None, 1));
let event1 = rx.recv().await.unwrap();
assert!(matches!(event1, ExecutionEvent::PipelineStarted { .. }));
let event2 = rx.recv().await.unwrap();
assert!(matches!(event2, ExecutionEvent::StageStarted { .. }));
}
#[test]
fn test_event_construction() {
let event = ExecutionEvent::job_completed(
"Build",
"Compile",
Some("linux".to_string()),
JobStatus::Succeeded,
Duration::from_secs(30),
);
if let ExecutionEvent::JobCompleted {
stage_name,
job_name,
matrix_instance,
status,
duration,
} = event
{
assert_eq!(stage_name, "Build");
assert_eq!(job_name, "Compile");
assert_eq!(matrix_instance, Some("linux".to_string()));
assert_eq!(status, JobStatus::Succeeded);
assert_eq!(duration, Duration::from_secs(30));
} else {
panic!("wrong event type");
}
}
#[test]
fn test_optional_sender() {
let sender: Option<ProgressSender> = None;
sender.send_event(ExecutionEvent::info("test", None, None));
}
}