use serde_json::Value;
struct RequestLine {
name: String,
status: String,
affects: Option<String>,
reason: Option<String>,
scope_size: Option<u64>,
}
impl RequestLine {
fn described(&self) -> String {
match self.reason.as_deref() {
Some(reason) => format!("{} ({reason})", self.name),
None => self.name.clone(),
}
}
fn applied(&self) -> bool {
self.status == "applied"
}
fn affects_scope(&self) -> bool {
self.affects.as_deref() == Some("scope")
}
fn withholds_artifact(&self) -> bool {
self.affects.as_deref() == Some("artifact")
}
fn applied_over_empty_scope(&self) -> bool {
self.applied() && self.affects_scope() && self.scope_size == Some(0)
}
}
fn read_request_outcomes(envelope: &Value) -> Vec<RequestLine> {
let Some(map) = envelope.get("request_outcomes").and_then(Value::as_object) else {
return Vec::new();
};
map.iter()
.filter_map(|(name, entry)| {
Some(RequestLine {
name: name.clone(),
status: entry.get("status")?.as_str()?.to_owned(),
affects: entry
.get("affects")
.and_then(Value::as_str)
.map(str::to_owned),
reason: entry
.get("reason")
.and_then(Value::as_str)
.map(str::to_owned),
scope_size: entry.get("scope_size").and_then(Value::as_u64),
})
})
.collect()
}
pub fn summary_line(envelope: &Value) -> Option<String> {
let requests = read_request_outcomes(envelope);
if requests.is_empty() {
return None;
}
let (applied, unapplied): (Vec<&RequestLine>, Vec<&RequestLine>) =
requests.iter().partition(|request| request.applied());
let mut clauses: Vec<String> = Vec::new();
if !unapplied.is_empty() {
clauses.push(format!("not applied {}", join(&unapplied)));
}
if !applied.is_empty() {
clauses.push(format!("applied {}", join(&applied)));
}
let mut line = format!("Request outcomes: {}.", clauses.join("; "));
if unapplied.iter().any(|request| request.affects_scope()) {
line.push_str(" Anything not applied means this report is wider than requested.");
}
if unapplied.iter().any(|request| request.withholds_artifact()) {
line.push_str(
" A requested output file was not written, so anything reading it has nothing \
to read; the report itself is unaffected.",
);
}
if applied
.iter()
.any(|request| request.applied_over_empty_scope())
{
line.push_str(
" A narrowing request applied over an empty scope, so this report is clean \
because nothing in it was analyzable; check the diff or ref this run was given.",
);
}
Some(line)
}
fn join(requests: &[&RequestLine]) -> String {
requests
.iter()
.map(|request| request.described())
.collect::<Vec<_>>()
.join(", ")
}
pub fn summary_line_for_saved_render(envelope: &Value) -> Option<String> {
summary_line_with_live_diff_filter(
envelope,
crate::report::ci::diff_filter::shared_diff_request_outcome(),
)
}
fn summary_line_with_live_diff_filter(
envelope: &Value,
live: Option<&fallow_output::RequestOutcome>,
) -> Option<String> {
let stood_down = live
.filter(|outcome| outcome.status == fallow_output::RequestStatus::NotApplied)
.and_then(|outcome| serde_json::to_value(outcome).ok());
let Some(live) = stood_down else {
return summary_line(envelope);
};
let mut merged: std::collections::BTreeMap<String, Value> = envelope
.get("request_outcomes")
.and_then(Value::as_object)
.map(|map| {
map.iter()
.map(|(name, entry)| (name.clone(), entry.clone()))
.collect()
})
.unwrap_or_default();
merged.insert("diff-filter".to_owned(), live);
let merged: serde_json::Map<String, Value> = merged.into_iter().collect();
summary_line(&serde_json::json!({ "request_outcomes": Value::Object(merged) }))
}
pub fn summary_line_for_requests(
requests: Option<&fallow_output::RequestOutcomes>,
) -> Option<String> {
let requests = requests?;
let envelope = serde_json::json!({ "request_outcomes": requests });
summary_line(&envelope)
}
pub fn annotation_line(envelope: &Value) -> Option<String> {
let line = summary_line(envelope)?;
Some(format!("::notice::Fallow: {line}"))
}
#[cfg(test)]
mod tests {
use super::*;
fn envelope(requests: &Value) -> Value {
serde_json::json!({ "kind": "health", "request_outcomes": requests })
}
#[test]
fn an_envelope_without_the_object_renders_nothing() {
let bare = serde_json::json!({ "kind": "dead-code" });
assert!(summary_line(&bare).is_none());
assert!(annotation_line(&bare).is_none());
}
#[test]
fn an_unapplied_request_names_its_reason_and_says_the_report_widened() {
let value = envelope(&serde_json::json!({
"changed-since": {
"status": "not-applied",
"affects": "scope",
"requested": "origin/main",
"reason": "invalid-ref",
"message": "..."
}
}));
assert_eq!(
summary_line(&value).expect("a request was received"),
"Request outcomes: not applied changed-since (invalid-ref). \
Anything not applied means this report is wider than requested."
);
}
#[test]
fn an_applied_request_states_the_scope_without_a_warning_clause() {
let value = envelope(&serde_json::json!({
"diff-filter": {
"status": "applied",
"affects": "scope",
"requested": "--diff-file pr.diff"
}
}));
assert_eq!(
summary_line(&value).expect("a request was received"),
"Request outcomes: applied diff-filter."
);
}
#[test]
fn a_measured_non_empty_scope_adds_no_clause() {
let value = envelope(&serde_json::json!({
"diff-filter": {
"status": "applied",
"affects": "scope",
"requested": "--diff-file pr.diff",
"scope_size": 12
}
}));
assert_eq!(
summary_line(&value).expect("a request was received"),
"Request outcomes: applied diff-filter."
);
}
#[test]
fn an_applied_request_over_an_empty_scope_says_the_report_covered_nothing() {
let value = envelope(&serde_json::json!({
"diff-filter": {
"status": "applied",
"affects": "scope",
"requested": "--diff-file pr.diff",
"scope_size": 0
}
}));
assert_eq!(
summary_line(&value).expect("a request was received"),
"Request outcomes: applied diff-filter. \
A narrowing request applied over an empty scope, so this report is clean \
because nothing in it was analyzable; check the diff or ref this run was given."
);
}
#[test]
fn an_empty_scope_on_an_artifact_request_claims_nothing() {
let value = envelope(&serde_json::json!({
"sarif-file": {
"status": "applied",
"affects": "artifact",
"requested": "out.sarif",
"scope_size": 0
}
}));
assert_eq!(
summary_line(&value).expect("a request was received"),
"Request outcomes: applied sarif-file."
);
}
#[test]
fn a_mixed_run_keeps_the_two_groups_apart_in_one_line() {
let value = envelope(&serde_json::json!({
"changed-since": {
"status": "not-applied",
"affects": "scope",
"requested": "x",
"reason": "git-failed"
},
"diff-filter": {
"status": "applied",
"affects": "scope",
"requested": "--diff-stdin"
}
}));
assert_eq!(
summary_line(&value).expect("requests were received"),
"Request outcomes: not applied changed-since (git-failed); applied diff-filter. \
Anything not applied means this report is wider than requested."
);
}
#[test]
fn an_unwritten_artifact_does_not_claim_the_report_widened() {
let value = envelope(&serde_json::json!({
"sarif-file": {
"status": "not-applied",
"affects": "artifact",
"requested": "out.sarif",
"reason": "write-failed"
}
}));
let line = summary_line(&value).expect("a request was received");
assert!(
!line.contains("wider than requested"),
"an unwritten file did not widen the report: {line}"
);
assert!(
line.contains("A requested output file was not written"),
"{line}"
);
}
#[test]
fn a_run_that_widened_and_withheld_states_both() {
let value = envelope(&serde_json::json!({
"changed-since": {
"status": "not-applied",
"affects": "scope",
"requested": "origin/main",
"reason": "git-failed"
},
"sarif-file": {
"status": "not-applied",
"affects": "artifact",
"requested": "out.sarif",
"reason": "write-failed"
}
}));
let line = summary_line(&value).expect("requests were received");
let widened = line.find("wider than requested").expect("the scope clause");
let withheld = line
.find("A requested output file was not written")
.expect("the artifact clause");
assert!(widened < withheld, "{line}");
}
#[test]
fn an_unrecognised_request_name_still_reports() {
let value = envelope(&serde_json::json!({
"some-future-request": {
"status": "not-applied",
"affects": "scope",
"requested": "x"
}
}));
assert_eq!(
annotation_line(&value).expect("a request was received"),
"::notice::Fallow: Request outcomes: not applied some-future-request. \
Anything not applied means this report is wider than requested."
);
}
#[test]
fn an_unrecognised_class_claims_neither_sentence() {
let value = envelope(&serde_json::json!({
"some-future-request": {
"status": "not-applied",
"affects": "some-future-class",
"requested": "x"
}
}));
assert_eq!(
summary_line(&value).expect("a request was received"),
"Request outcomes: not applied some-future-request."
);
}
#[test]
fn a_re_render_states_the_diff_filter_it_resolved_itself() {
let saved = envelope(&serde_json::json!({
"diff-filter": {
"status": "applied",
"affects": "scope",
"requested": "--diff-stdin"
}
}));
let live = fallow_output::RequestOutcome::not_applied(
fallow_output::RequestName::DiffFilter,
"$FALLOW_DIFF_FILE pr.diff",
"oversize",
"...",
);
assert_eq!(
summary_line_with_live_diff_filter(&saved, Some(&live))
.expect("a request was received"),
"Request outcomes: not applied diff-filter (oversize). \
Anything not applied means this report is wider than requested."
);
}
#[test]
fn the_overlay_keeps_the_producing_run_channels_and_the_wire_order() {
let saved = envelope(&serde_json::json!({
"changed-since": {
"status": "applied",
"affects": "scope",
"requested": "origin/main"
},
"sarif-file": {
"status": "not-applied",
"affects": "artifact",
"requested": "out.sarif",
"reason": "write-failed"
}
}));
let live = fallow_output::RequestOutcome::not_applied(
fallow_output::RequestName::DiffFilter,
"$FALLOW_DIFF_FILE pr.diff",
"foreign-namespace",
"...",
);
let line = summary_line_with_live_diff_filter(&saved, Some(&live))
.expect("requests were received");
assert_eq!(
line,
"Request outcomes: not applied diff-filter (foreign-namespace), \
sarif-file (write-failed); applied changed-since. \
Anything not applied means this report is wider than requested. \
A requested output file was not written, so anything reading it has nothing \
to read; the report itself is unaffected."
);
}
#[test]
fn a_filter_that_applied_here_does_not_overwrite_the_saved_stand_down() {
let saved = envelope(&serde_json::json!({
"diff-filter": {
"status": "not-applied",
"affects": "scope",
"requested": "--diff-stdin",
"reason": "not-utf8"
}
}));
let live = fallow_output::RequestOutcome::applied(
fallow_output::RequestName::DiffFilter,
"$FALLOW_DIFF_FILE pr.diff",
);
assert_eq!(
summary_line_with_live_diff_filter(&saved, Some(&live)),
summary_line(&saved)
);
}
#[test]
fn a_filter_that_applied_here_adds_no_clause_of_its_own() {
let bare = serde_json::json!({ "kind": "dead-code" });
let live = fallow_output::RequestOutcome::applied(
fallow_output::RequestName::DiffFilter,
"$FALLOW_DIFF_FILE pr.diff",
);
assert!(summary_line_with_live_diff_filter(&bare, Some(&live)).is_none());
}
#[test]
fn no_live_diff_leaves_the_saved_object_alone() {
let saved = envelope(&serde_json::json!({
"diff-filter": {
"status": "not-applied",
"affects": "scope",
"requested": "--diff-stdin",
"reason": "not-utf8"
}
}));
assert_eq!(
summary_line_with_live_diff_filter(&saved, None),
summary_line(&saved)
);
}
#[test]
fn an_unrecognised_status_is_not_applied() {
let value = envelope(&serde_json::json!({
"changed-since": {
"status": "partial",
"affects": "scope",
"requested": "origin/main"
}
}));
assert_eq!(
summary_line(&value).expect("a request was received"),
"Request outcomes: not applied changed-since. \
Anything not applied means this report is wider than requested."
);
}
}