use std::path::{Path, PathBuf};
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use crate::config::Config;
use crate::progress::ProgressFile;
use crate::prompts::PromptMode;
use crate::tui::SubprocessEvent;
use crate::vcs::{create_vcs, Vcs};
use super::tokens::{IterationTokens, TokenUsage};
#[derive(Debug, Clone, PartialEq)]
pub enum BuildState {
Starting,
Running { iteration: u32 },
IterationComplete {
iteration: u32,
tasks_completed: u32,
},
Done { reason: DoneReason },
Failed { error: String },
}
#[derive(Debug, Clone, PartialEq)]
pub enum DoneReason {
AllTasksComplete,
RalphDoneMarker,
MaxIterationsReached,
UserCancelled,
SingleIterationComplete,
}
impl std::fmt::Display for DoneReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DoneReason::AllTasksComplete => write!(f, "All tasks complete"),
DoneReason::RalphDoneMarker => write!(f, "RALPH_DONE marker detected"),
DoneReason::MaxIterationsReached => write!(f, "Maximum iterations reached"),
DoneReason::UserCancelled => write!(f, "Cancelled by user"),
DoneReason::SingleIterationComplete => write!(f, "Single iteration complete (--once)"),
}
}
}
#[derive(Debug)]
pub enum IterationResult {
Continue {
tasks_completed: u32,
},
Done(DoneReason),
Timeout,
}
impl IterationResult {
pub fn is_done(&self) -> bool {
matches!(self, IterationResult::Done(_))
}
}
pub struct BuildContext {
pub progress_path: PathBuf,
pub progress: ProgressFile,
pub config: Config,
pub mode: PromptMode,
pub cancel_token: CancellationToken,
pub current_iteration: u32,
pub max_iterations: u32,
pub once_mode: bool,
pub dry_run: bool,
pub iteration_start: Option<std::time::Instant>,
pub vcs: Option<Box<dyn Vcs>>,
pub project_name: String,
pub tui_tx: Option<mpsc::UnboundedSender<SubprocessEvent>>,
pub iteration_tokens: Vec<IterationTokens>,
pub total_tokens: TokenUsage,
pub current_iteration_tokens: TokenUsage,
pub timeout_retry_count: u32,
}
impl BuildContext {
pub fn new(
progress_path: PathBuf,
progress: ProgressFile,
config: Config,
mode: PromptMode,
cancel_token: CancellationToken,
once_mode: bool,
dry_run: bool,
) -> Self {
Self::with_tui(
progress_path,
progress,
config,
mode,
cancel_token,
once_mode,
dry_run,
None,
)
}
#[allow(clippy::too_many_arguments)]
pub fn with_tui(
progress_path: PathBuf,
progress: ProgressFile,
config: Config,
mode: PromptMode,
cancel_token: CancellationToken,
once_mode: bool,
dry_run: bool,
tui_tx: Option<mpsc::UnboundedSender<SubprocessEvent>>,
) -> Self {
let max_iterations = config.max_iterations;
let working_dir = progress_path
.parent()
.filter(|p| !p.as_os_str().is_empty())
.unwrap_or(Path::new("."));
let vcs = create_vcs(working_dir);
let project_name = if progress.name.is_empty() {
"Unnamed".to_string()
} else {
progress.name.clone()
};
let ctx = Self {
progress_path,
progress,
config,
mode,
cancel_token,
current_iteration: 0,
max_iterations,
once_mode,
dry_run,
iteration_start: None,
vcs,
project_name: project_name.clone(),
tui_tx,
iteration_tokens: Vec::new(),
total_tokens: TokenUsage::default(),
current_iteration_tokens: TokenUsage::default(),
timeout_retry_count: 0,
};
if let Some(ref v) = ctx.vcs {
ctx.log(&format!("[VCS] Detected {} repository", v.vcs_type()));
}
if ctx.project_name == "Unnamed" {
ctx.log("[BUILD] Warning: Progress file has no project name, using 'Unnamed'");
} else {
ctx.log(&format!("[BUILD] Project: {}", project_name));
}
ctx
}
pub fn log(&self, msg: &str) {
if let Some(ref tx) = self.tui_tx {
let _ = tx.send(SubprocessEvent::Log(msg.to_string()));
} else {
eprintln!("{}", msg);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_state_transitions() {
let state = BuildState::Starting;
assert_eq!(state, BuildState::Starting);
let state = BuildState::Running { iteration: 1 };
assert!(matches!(state, BuildState::Running { iteration: 1 }));
let state = BuildState::IterationComplete {
iteration: 1,
tasks_completed: 2,
};
assert!(matches!(
state,
BuildState::IterationComplete {
iteration: 1,
tasks_completed: 2
}
));
let state = BuildState::Done {
reason: DoneReason::AllTasksComplete,
};
assert!(matches!(
state,
BuildState::Done {
reason: DoneReason::AllTasksComplete
}
));
let state = BuildState::Failed {
error: "test error".to_string(),
};
assert!(matches!(state, BuildState::Failed { error: _ }));
}
#[test]
fn test_done_reason_display() {
assert_eq!(
DoneReason::AllTasksComplete.to_string(),
"All tasks complete"
);
assert_eq!(
DoneReason::RalphDoneMarker.to_string(),
"RALPH_DONE marker detected"
);
assert_eq!(
DoneReason::MaxIterationsReached.to_string(),
"Maximum iterations reached"
);
assert_eq!(DoneReason::UserCancelled.to_string(), "Cancelled by user");
assert_eq!(
DoneReason::SingleIterationComplete.to_string(),
"Single iteration complete (--once)"
);
}
#[test]
fn test_iteration_result_is_done() {
let result = IterationResult::Continue { tasks_completed: 1 };
assert!(!result.is_done());
let result = IterationResult::Done(DoneReason::AllTasksComplete);
assert!(result.is_done());
}
}