hive-forge: extract format_event helper; tests call it directly (argus 🟡 on #798)
was: `print_event` did the format + println inline; tests had a parallel `captured` helper that re-implemented the dispatch and only covered 4 of 14 event types. brittle — a new arm in `print_event` silently went uncovered. now: pure `format_event(ev: &Value) -> String` builds the line; `print_event` is a thin wrapper that adds the trailing newline. tests assert on `format_event` output directly so every new arm gets test coverage by construction. bonus: added 3 more test cases (assignees add/remove, pull_push commit count + force-push, commit_ref sha truncation) since the helper extraction made them cheap. 9 tests total, all green.
This commit is contained in:
parent
8f9866f5e1
commit
224385af37
1 changed files with 104 additions and 58 deletions
|
|
@ -50,14 +50,19 @@ pub fn run(client: &Client, args: Args) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render one timeline event as `**actor @ ts**: summary`. Comment
|
/// Render one timeline event as a single `**actor @ ts**: summary`
|
||||||
/// rows print their full body; structured event types (label,
|
/// line. Comment rows inline their full body; structured event types
|
||||||
/// assignees, close, etc.) get a one-line human summary derived from
|
/// (label, assignees, close, etc.) get a one-line human summary
|
||||||
/// the per-type fields the API populates. Unknown / future types
|
/// derived from the per-type fields the API populates. Unknown /
|
||||||
/// fall through to a `[<type>]` placeholder so a forge schema bump
|
/// future types fall through to a `[<type>]` placeholder so a forge
|
||||||
/// doesn't panic the verb — operator still sees that the event
|
/// schema bump doesn't panic the verb — operator still sees that the
|
||||||
/// existed, with timestamp + actor.
|
/// event existed, with timestamp + actor.
|
||||||
fn print_event(ev: &Value) {
|
///
|
||||||
|
/// Pure function (no I/O) so the tests below can pin the formatted
|
||||||
|
/// output for every supported event type without re-implementing the
|
||||||
|
/// per-arm dispatch. `print_event` is the only caller that adds the
|
||||||
|
/// terminating newline.
|
||||||
|
fn format_event(ev: &Value) -> String {
|
||||||
let event_type = ev.get("type").and_then(Value::as_str).unwrap_or("?");
|
let event_type = ev.get("type").and_then(Value::as_str).unwrap_or("?");
|
||||||
let user = ev
|
let user = ev
|
||||||
.get("user")
|
.get("user")
|
||||||
|
|
@ -67,11 +72,13 @@ fn print_event(ev: &Value) {
|
||||||
let ts = ev.get("created_at").and_then(Value::as_str).unwrap_or("?");
|
let ts = ev.get("created_at").and_then(Value::as_str).unwrap_or("?");
|
||||||
let summary = match event_type {
|
let summary = match event_type {
|
||||||
"comment" => {
|
"comment" => {
|
||||||
// Comments get the full body — matches `comments` verb shape.
|
// Comments get the full body inlined — matches `comments`
|
||||||
let body = ev.get("body").and_then(Value::as_str).unwrap_or("");
|
// verb shape so the operator sees the same line they'd
|
||||||
println!("**{user} @ {ts}**: {body}");
|
// get from the head-of-thread listing.
|
||||||
println!();
|
ev.get("body")
|
||||||
return;
|
.and_then(Value::as_str)
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_owned()
|
||||||
}
|
}
|
||||||
"label" => {
|
"label" => {
|
||||||
// Forgejo encodes label add/remove via `body = "1"` (added)
|
// Forgejo encodes label add/remove via `body = "1"` (added)
|
||||||
|
|
@ -180,52 +187,27 @@ fn print_event(ev: &Value) {
|
||||||
// whatever new event a forge bump invents.
|
// whatever new event a forge bump invents.
|
||||||
other => format!("[{other}]"),
|
other => format!("[{other}]"),
|
||||||
};
|
};
|
||||||
println!("**{user} @ {ts}**: {summary}");
|
format!("**{user} @ {ts}**: {summary}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print one event followed by a blank line spacer. Thin wrapper
|
||||||
|
/// around `format_event` so the tests can pin per-arm output without
|
||||||
|
/// duplicating the dispatch.
|
||||||
|
fn print_event(ev: &Value) {
|
||||||
|
println!("{}", format_event(ev));
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
//! Tests call `format_event` directly so any new event-type arm
|
||||||
|
//! added in `print_event`'s dispatch is automatically covered by
|
||||||
|
//! the rendering path (no parallel test-side dispatch to keep in
|
||||||
|
//! sync). Argus on PR #798 🟡: "extract a `format_event(ev) ->
|
||||||
|
//! String` helper and test that function directly instead of
|
||||||
|
//! duplicating the logic" — addressed.
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
/// Render one mock event and assert the formatted line matches.
|
|
||||||
/// Tests don't hit the network — we synthesise `Value` shapes
|
|
||||||
/// matching the Forgejo schema directly.
|
|
||||||
fn captured(ev: &Value) -> String {
|
|
||||||
// Tests run in-process; `println!` would interleave with the
|
|
||||||
// test harness. We rebuild the line via the same format
|
|
||||||
// expression. Keep this helper in lockstep with `print_event`.
|
|
||||||
let event_type = ev.get("type").and_then(Value::as_str).unwrap_or("?");
|
|
||||||
let user = ev
|
|
||||||
.get("user")
|
|
||||||
.and_then(|u| u.get("login"))
|
|
||||||
.and_then(Value::as_str)
|
|
||||||
.unwrap_or("?");
|
|
||||||
let ts = ev.get("created_at").and_then(Value::as_str).unwrap_or("?");
|
|
||||||
let summary = match event_type {
|
|
||||||
"comment" => {
|
|
||||||
let body = ev.get("body").and_then(Value::as_str).unwrap_or("");
|
|
||||||
return format!("**{user} @ {ts}**: {body}");
|
|
||||||
}
|
|
||||||
"label" => {
|
|
||||||
let action = match ev.get("body").and_then(Value::as_str).unwrap_or("") {
|
|
||||||
"1" => "added",
|
|
||||||
"0" => "removed",
|
|
||||||
_ => "changed",
|
|
||||||
};
|
|
||||||
let label = ev
|
|
||||||
.get("label")
|
|
||||||
.and_then(|l| l.get("name"))
|
|
||||||
.and_then(Value::as_str)
|
|
||||||
.unwrap_or("?");
|
|
||||||
format!("{action} label `{label}`")
|
|
||||||
}
|
|
||||||
"close" => "closed".to_owned(),
|
|
||||||
other => format!("[{other}]"),
|
|
||||||
};
|
|
||||||
format!("**{user} @ {ts}**: {summary}")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn comment_renders_body_inline() {
|
fn comment_renders_body_inline() {
|
||||||
let ev = serde_json::json!({
|
let ev = serde_json::json!({
|
||||||
|
|
@ -234,7 +216,7 @@ mod tests {
|
||||||
"created_at": "2026-05-31T12:00:00Z",
|
"created_at": "2026-05-31T12:00:00Z",
|
||||||
"body": "looks good to me",
|
"body": "looks good to me",
|
||||||
});
|
});
|
||||||
assert_eq!(captured(&ev), "**iris @ 2026-05-31T12:00:00Z**: looks good to me");
|
assert_eq!(format_event(&ev), "**iris @ 2026-05-31T12:00:00Z**: looks good to me");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -247,7 +229,7 @@ mod tests {
|
||||||
"label": { "name": "area:harness" },
|
"label": { "name": "area:harness" },
|
||||||
});
|
});
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
captured(&ev),
|
format_event(&ev),
|
||||||
"**triage @ 2026-05-31T12:00:00Z**: added label `area:harness`"
|
"**triage @ 2026-05-31T12:00:00Z**: added label `area:harness`"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -262,7 +244,7 @@ mod tests {
|
||||||
"label": { "name": "needs-review" },
|
"label": { "name": "needs-review" },
|
||||||
});
|
});
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
captured(&ev),
|
format_event(&ev),
|
||||||
"**mara @ 2026-05-31T12:00:00Z**: removed label `needs-review`"
|
"**mara @ 2026-05-31T12:00:00Z**: removed label `needs-review`"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -274,7 +256,71 @@ mod tests {
|
||||||
"user": { "login": "mara" },
|
"user": { "login": "mara" },
|
||||||
"created_at": "2026-05-31T12:00:00Z",
|
"created_at": "2026-05-31T12:00:00Z",
|
||||||
});
|
});
|
||||||
assert_eq!(captured(&ev), "**mara @ 2026-05-31T12:00:00Z**: closed");
|
assert_eq!(format_event(&ev), "**mara @ 2026-05-31T12:00:00Z**: closed");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn assignees_added_and_removed() {
|
||||||
|
let added = serde_json::json!({
|
||||||
|
"type": "assignees",
|
||||||
|
"user": { "login": "triage" },
|
||||||
|
"created_at": "2026-05-31T12:00:00Z",
|
||||||
|
"assignee": { "login": "damocles" },
|
||||||
|
"removed_assignee": false,
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
format_event(&added),
|
||||||
|
"**triage @ 2026-05-31T12:00:00Z**: assigned @damocles"
|
||||||
|
);
|
||||||
|
let removed = serde_json::json!({
|
||||||
|
"type": "assignees",
|
||||||
|
"user": { "login": "triage" },
|
||||||
|
"created_at": "2026-05-31T12:00:00Z",
|
||||||
|
"assignee": { "login": "damocles" },
|
||||||
|
"removed_assignee": true,
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
format_event(&removed),
|
||||||
|
"**triage @ 2026-05-31T12:00:00Z**: unassigned @damocles"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pull_push_counts_commits_and_marks_force() {
|
||||||
|
let normal = serde_json::json!({
|
||||||
|
"type": "pull_push",
|
||||||
|
"user": { "login": "damocles" },
|
||||||
|
"created_at": "2026-05-31T12:00:00Z",
|
||||||
|
"body": r#"{"is_force_push":false,"commit_ids":["a","b","c"]}"#,
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
format_event(&normal),
|
||||||
|
"**damocles @ 2026-05-31T12:00:00Z**: pushed 3 commit(s)"
|
||||||
|
);
|
||||||
|
let forced = serde_json::json!({
|
||||||
|
"type": "pull_push",
|
||||||
|
"user": { "login": "damocles" },
|
||||||
|
"created_at": "2026-05-31T12:00:00Z",
|
||||||
|
"body": r#"{"is_force_push":true,"commit_ids":["a"]}"#,
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
format_event(&forced),
|
||||||
|
"**damocles @ 2026-05-31T12:00:00Z**: force-pushed 1 commit(s)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn commit_ref_truncates_sha_to_seven() {
|
||||||
|
let ev = serde_json::json!({
|
||||||
|
"type": "commit_ref",
|
||||||
|
"user": { "login": "damocles" },
|
||||||
|
"created_at": "2026-05-31T12:00:00Z",
|
||||||
|
"ref_commit_sha": "abcdef0123456789abcdef0123456789abcdef01",
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
format_event(&ev),
|
||||||
|
"**damocles @ 2026-05-31T12:00:00Z**: referenced from commit abcdef0"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -287,7 +333,7 @@ mod tests {
|
||||||
"created_at": "2026-05-31T12:00:00Z",
|
"created_at": "2026-05-31T12:00:00Z",
|
||||||
});
|
});
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
captured(&ev),
|
format_event(&ev),
|
||||||
"**ci-bot @ 2026-05-31T12:00:00Z**: [deploy_status]"
|
"**ci-bot @ 2026-05-31T12:00:00Z**: [deploy_status]"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -299,6 +345,6 @@ mod tests {
|
||||||
"type": "close",
|
"type": "close",
|
||||||
"created_at": "2026-05-31T12:00:00Z",
|
"created_at": "2026-05-31T12:00:00Z",
|
||||||
});
|
});
|
||||||
assert_eq!(captured(&ev), "**? @ 2026-05-31T12:00:00Z**: closed");
|
assert_eq!(format_event(&ev), "**? @ 2026-05-31T12:00:00Z**: closed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue