Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ jobs:
uses: actions-rust-lang/setup-rust-toolchain@v1

- name: cargo build
run: cargo build --workspace --all-targets --all-features
run: |
cargo build --workspace --all-targets --all-features
cargo test

cargo-fmt:
name: Cargo fmt
Expand Down
94 changes: 94 additions & 0 deletions src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,17 @@ impl WorkflowStatus {
self.conclusion.as_deref() == Some("failure")
}

pub fn is_in_progress(&self) -> bool {
matches!(
self.status.as_str(),
"in_progress" | "queued" | "pending" | "requested" | "waiting"
)
}

pub fn needs_job_refresh(&self) -> bool {
if self.is_in_progress() {
return true;
}
if self.is_success() {
return false;
}
Expand All @@ -90,3 +100,87 @@ pub fn format_job_conclusion(job: &JobStatus) -> String {
.map(|c| format!("{:?}", c))
.unwrap_or_else(|| "Unknown".to_string())
}

#[cfg(test)]
mod tests {
use super::{JobStatus, WorkflowStatus};
use chrono::Utc;
use octocrab::models::workflows::Status;

fn workflow(status: &str, conclusion: Option<&str>, jobs: Vec<JobStatus>) -> WorkflowStatus {
WorkflowStatus {
run_id: 1,
name: "workflow".to_string(),
status: status.to_string(),
conclusion: conclusion.map(str::to_string),
html_url: "https://example.invalid".to_string(),
created_at: Utc::now(),
updated_at: Utc::now(),
jobs,
}
}

fn job(job_id: u64, status: Status) -> JobStatus {
JobStatus {
job_id,
name: format!("job-{job_id}"),
status,
conclusion: None,
html_url: "https://example.invalid".to_string(),
started_at: Utc::now(),
completed_at: None,
}
}

#[test]
fn needs_job_refresh_true_for_in_progress_even_with_success_conclusion() {
let status = workflow(
"in_progress",
Some("success"),
vec![job(1, Status::Completed)],
);
assert!(status.needs_job_refresh());
}

#[test]
fn needs_job_refresh_false_for_success_workflow() {
let status = workflow("completed", Some("success"), Vec::new());
assert!(!status.needs_job_refresh());
}

#[test]
fn needs_job_refresh_true_for_non_success_with_empty_jobs() {
let status = workflow("completed", None, Vec::new());
assert!(status.needs_job_refresh());
}

#[test]
fn needs_job_refresh_true_when_non_terminal_job_exists() {
let status = workflow(
"completed",
Some("failure"),
vec![job(1, Status::InProgress)],
);
assert!(status.needs_job_refresh());
}

#[test]
fn needs_job_refresh_true_when_job_id_missing() {
let status = workflow(
"completed",
Some("failure"),
vec![job(0, Status::Completed)],
);
assert!(status.needs_job_refresh());
}

#[test]
fn needs_job_refresh_false_for_terminal_jobs_with_valid_ids() {
let status = workflow(
"completed",
Some("failure"),
vec![job(1, Status::Completed), job(2, Status::Failed)],
);
assert!(!status.needs_job_refresh());
}
}
138 changes: 121 additions & 17 deletions src/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,42 +17,73 @@ fn icon_failure() -> &'static str {
}
}

fn icon_running() -> &'static str {
if supports_emoji() {
"\u{23F3}"
} else {
"[RUNNING]"
}
}

pub fn render_human_report(workflows: &[WorkflowStatus]) -> String {
let mut lines = Vec::new();

for workflow in workflows {
match workflow.conclusion.as_deref() {
Some("success") => lines.push(format!("- {} {}", workflow.name, icon_success())),
Some("failure") => {
lines.push(format!("- {} {}", workflow.name, icon_failure()));
for job in &workflow.jobs {
lines.push(format!(" - {}: {}", job.name, format_job_conclusion(job)));
}
}
_ => lines.push(format!(
"- {} status={} conclusion={}",
if workflow.is_in_progress() {
lines.push(format!(
"- {} {} ({})",
workflow.name,
workflow.status,
workflow.conclusion.as_deref().unwrap_or("unknown")
)),
icon_running(),
workflow.status
));
continue;
}

if workflow.is_success() {
lines.push(format!("- {} {}", workflow.name, icon_success()));
continue;
}

if workflow.is_failure() {
lines.push(format!("- {} {}", workflow.name, icon_failure()));
for job in &workflow.jobs {
lines.push(format!(" - {}: {}", job.name, format_job_conclusion(job)));
}
continue;
}

lines.push(format!(
"- {} status={} conclusion={}",
workflow.name,
workflow.status,
workflow.conclusion.as_deref().unwrap_or("unknown")
));
}

lines.join("\n")
}

pub fn render_llm_report(workflows: &[WorkflowStatus]) -> String {
let total = workflows.len();
let success = workflows.iter().filter(|w| w.is_success()).count();
let failure = workflows.iter().filter(|w| w.is_failure()).count();
let success = workflows
.iter()
.filter(|w| !w.is_in_progress() && w.is_success())
.count();
let failure = workflows
.iter()
.filter(|w| !w.is_in_progress() && w.is_failure())
.count();
let other = total.saturating_sub(success + failure);

let mut lines = vec![format!(
"ci total={} success={} failure={} other={}",
total, success, failure, other
)];

for workflow in workflows.iter().filter(|w| w.is_failure()) {
for workflow in workflows
.iter()
.filter(|w| !w.is_in_progress() && w.is_failure())
{
let failed_jobs = workflow
.jobs
.iter()
Expand All @@ -75,9 +106,82 @@ pub fn render_llm_report(workflows: &[WorkflowStatus]) -> String {
}
}

if failure == 0 {
for workflow in workflows.iter().filter(|w| w.is_in_progress()) {
lines.push(format!(
"running workflow={} status={}",
workflow.name, workflow.status
));
}

let all_passed = workflows
.iter()
.all(|w| !w.is_in_progress() && w.is_success());
if all_passed {
lines.push("all workflows passed".to_string());
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

lines.join("\n")
}

#[cfg(test)]
mod tests {
use super::{render_human_report, render_llm_report};
use crate::models::WorkflowStatus;
use chrono::Utc;

fn workflow(name: &str, status: &str, conclusion: Option<&str>) -> WorkflowStatus {
WorkflowStatus {
run_id: 1,
name: name.to_string(),
status: status.to_string(),
conclusion: conclusion.map(str::to_string),
html_url: "https://example.invalid".to_string(),
created_at: Utc::now(),
updated_at: Utc::now(),
jobs: Vec::new(),
}
}

#[test]
fn renders_in_progress_workflow_as_running() {
let report = render_human_report(&[workflow("FreeBSD", "in_progress", None)]);
assert!(report.contains("- FreeBSD [RUNNING] (in_progress)"));
}

#[test]
fn renders_in_progress_with_success_conclusion_as_running() {
let report = render_human_report(&[workflow("FreeBSD", "in_progress", Some("success"))]);
assert!(report.contains("- FreeBSD [RUNNING] (in_progress)"));
}

#[test]
fn llm_report_does_not_mark_running_workflows_as_passed() {
let report = render_llm_report(&[
workflow("Linux", "completed", Some("success")),
workflow("FreeBSD", "in_progress", None),
]);

assert!(!report.contains("all workflows passed"));
assert!(report.contains("running workflow=FreeBSD status=in_progress"));
}

#[test]
fn llm_report_treats_in_progress_with_success_conclusion_as_running() {
let report = render_llm_report(&[
workflow("Linux", "completed", Some("success")),
workflow("FreeBSD", "in_progress", Some("success")),
]);

assert!(!report.contains("all workflows passed"));
assert!(report.contains("running workflow=FreeBSD status=in_progress"));
}

#[test]
fn llm_report_marks_all_passed_only_when_no_other_states() {
let report = render_llm_report(&[
workflow("Linux", "completed", Some("success")),
workflow("Windows", "completed", Some("success")),
]);
assert!(report.contains("all workflows passed"));
}
}