use anyhow::{bail, Result};
use pushkin_core::envelope::{Severity, Violation};
use pushkin_core::floor_report::{reconcile, tally, IgnoredVerdict, Tally};
use pushkin_core::manifest::{Floor, FloorCommand, Manifest};
use std::time::Instant;
use super::load_manifest;
struct Ran<'a> {
command: &'a FloorCommand,
green: bool,
millis: u128,
counts: Tally,
captured: String,
}
pub fn run(skip: &[String]) -> Result<i32> {
let manifest = load_manifest()?;
let floor = require_floor(&manifest)?;
let planned = plan(floor, skip)?;
println!(
"pushkin floor — {} — manifest {}",
head_sha(),
super::manifest_display()
);
if !skip.is_empty() {
println!("EXCLUDED (--skip): {}", skip.join(", "));
println!("THIS IS NOT A FULL FLOOR. Do not cite this output as one.");
}
println!();
let mut ran: Vec<Ran> = Vec::with_capacity(planned.len());
for command in &planned {
match execute(command) {
Ok(outcome) => {
print!("{}", outcome.captured);
report_line(&outcome);
ran.push(outcome);
}
Err(error) => {
println!();
summarize_partial(&ran);
eprintln!("pushkin floor: {error}");
return Ok(1);
}
}
}
let verdicts = reconcile_all(&ran, floor, skip);
Ok(finish(&ran, &verdicts, skip, floor))
}
fn head_sha() -> String {
super::git::head_sha().unwrap_or_else(|| "no git".to_owned())
}
fn require_floor(manifest: &Manifest) -> Result<&Floor> {
let Some(floor) = manifest.floor.as_ref() else {
bail!(
"no [floor] table in pushkin.toml — declare the floor commands \
before running the mechanical floor. A repo that has not declared \
its floor has not got one, and reporting green over zero commands \
would be the loudest possible way to count less than you claim."
);
};
if floor.commands.is_empty() {
bail!(
"[floor] in pushkin.toml declares no commands — there is nothing to \
run, and a verdict over zero commands is vacuous, not green."
);
}
Ok(floor)
}
fn plan<'a>(floor: &'a Floor, skip: &[String]) -> Result<Vec<&'a FloorCommand>> {
for name in skip {
if !floor.commands.iter().any(|c| &c.name == name) {
let declared: Vec<&str> = floor.commands.iter().map(|c| c.name.as_str()).collect();
bail!(
"--skip named '{name}', which is not a declared floor command; \
declared commands: {}",
declared.join(", ")
);
}
}
let planned: Vec<&FloorCommand> = floor
.commands
.iter()
.filter(|c| !skip.contains(&c.name))
.collect();
if planned.is_empty() {
bail!(
"--skip excluded every declared floor command; there is nothing \
left to run, and an empty run is not a green floor."
);
}
Ok(planned)
}
fn execute(command: &FloorCommand) -> Result<Ran<'_>> {
let (program, args) = command
.run
.split_first()
.ok_or_else(|| anyhow::anyhow!("floor command '{}' has an empty run", command.name))?;
let started = Instant::now();
let output = match std::process::Command::new(program).args(args).output() {
Ok(output) => output,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
let hint = command.install.as_deref().map_or_else(
|| " (no install hint declared for it in [floor])".to_owned(),
|install| format!(" (install it: `{install}`)"),
);
bail!(
"floor command '{}' needs `{program}`, which is not installed{hint}",
command.name
);
}
Err(error) => bail!(
"floor command '{}' could not run `{program}`: {error}",
command.name
),
};
let millis = started.elapsed().as_millis();
let combined = format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
Ok(Ran {
command,
green: output.status.success(),
millis,
counts: tally(&combined),
captured: combined,
})
}
fn report_line(outcome: &Ran) {
println!(
" {:<12} {:<11} {:<9} {:>7}ms {}",
outcome.command.name,
scope_label(outcome.command),
inputs_label(outcome.command),
outcome.millis,
if outcome.green { "green" } else { "RED" }
);
}
fn scope_label(command: &FloorCommand) -> &'static str {
use pushkin_core::manifest::FloorScope;
match command.scope {
FloorScope::PerFile => "per_file",
FloorScope::PerCrate => "per_crate",
FloorScope::WholeRepo => "whole_repo",
}
}
fn inputs_label(command: &FloorCommand) -> &'static str {
use pushkin_core::manifest::FloorInputs;
match command.inputs.value() {
FloorInputs::Repo => "repo",
FloorInputs::Toolchain => "toolchain",
FloorInputs::Network => "network",
FloorInputs::Machine => "machine",
}
}
fn disclose_degraded_inputs(floor: &Floor) {
let degraded: Vec<String> = floor
.commands
.iter()
.filter_map(|command| {
command.inputs.degraded_from().map(|raw| {
format!(
"{} (inputs = \"{raw}\", reported as \"{}\")",
command.name,
inputs_label(command)
)
})
})
.collect();
if degraded.is_empty() {
return;
}
println!(
"NOTE: an inputs value was not recognized on {} — the field is \
display-only, so the command ran and is reported with the default \
class. Valid values: repo, toolchain, network, machine. Fix the \
spelling to make this line go away.",
degraded.join(", ")
);
println!();
}
struct Accounting<'a> {
name: &'a str,
verdict: IgnoredVerdict,
excused_by_skip: bool,
}
fn reconcile_all<'a>(ran: &[Ran<'a>], floor: &Floor, skip: &[String]) -> Vec<Accounting<'a>> {
let mut verdicts = Vec::new();
for outcome in ran.iter().filter(|r| r.command.reconcile_ignored) {
let name = outcome.command.name.as_str();
let covered = ran
.iter()
.find(|r| r.command.covers_ignored_of.as_deref() == Some(name))
.map(|r| r.counts.passed);
let excused_by_skip = covered.is_none()
&& floor
.commands
.iter()
.any(|c| c.covers_ignored_of.as_deref() == Some(name) && skip.contains(&c.name));
verdicts.push(Accounting {
name,
verdict: reconcile(outcome.counts.ignored, covered),
excused_by_skip,
});
}
verdicts
}
fn summarize_partial(ran: &[Ran]) {
if ran.is_empty() {
println!("--- no floor command completed before the failure ---");
return;
}
println!("--- commands that ran before the failure ---");
for outcome in ran {
println!(
" {:<12} {}",
outcome.command.name,
if outcome.green { "green" } else { "RED" }
);
}
}
fn finish(ran: &[Ran], verdicts: &[Accounting], skip: &[String], floor: &Floor) -> i32 {
println!();
let reds: Vec<&str> = ran
.iter()
.filter(|r| !r.green)
.map(|r| r.command.name.as_str())
.collect();
let passed: u64 = ran.iter().map(|r| r.counts.passed).sum();
let failed: u64 = ran.iter().map(|r| r.counts.failed).sum();
if !verdicts.is_empty() {
println!("--- ignored-test reconciliation ---");
for entry in verdicts {
if entry.excused_by_skip {
println!(
" {}: {} ignored, and the command declared to cover them \
was excluded by --skip. They did NOT run. Not counted \
against this floor — the run is already marked NOT A FULL \
FLOOR — but re-run without --skip before citing anything.",
entry.name,
entry.verdict.ignored_count()
);
} else {
println!(" {}: {}", entry.name, entry.verdict.message());
}
}
println!();
}
let networked: Vec<&str> = floor
.commands
.iter()
.filter(|c| {
matches!(
c.inputs.value(),
pushkin_core::manifest::FloorInputs::Network
)
})
.map(|c| c.name.as_str())
.collect();
if !networked.is_empty() {
println!(
"NOTE: {} consult(s) a network source, so this verdict is not a pure \
function of the commit — an unchanged commit can newly fail when \
upstream data changes.",
networked.join(", ")
);
println!();
}
let load_bound: Vec<&str> = floor
.commands
.iter()
.filter(|c| {
matches!(
c.inputs.value(),
pushkin_core::manifest::FloorInputs::Machine
)
})
.filter(|c| !skip.contains(&c.name))
.map(|c| c.name.as_str())
.collect();
if !load_bound.is_empty() {
println!(
"NOTE: {} measure(s) wall-clock time, so this verdict depends on what \
else the machine is doing — a busy machine can fail a commit that \
passes on a quiet one. Re-run quiet before citing a RED.",
load_bound.join(", ")
);
println!();
}
disclose_degraded_inputs(floor);
println!(" commands: {} run, {} red", ran.len(), reds.len());
if passed > 0 || failed > 0 {
println!(" tests: {passed} passed / {failed} failed");
}
let accounting_red = verdicts
.iter()
.any(|entry| entry.verdict.is_red() && !entry.excused_by_skip);
let excluded = if skip.is_empty() {
String::new()
} else {
format!(" (EXCLUDED: {})", skip.join(", "))
};
if reds.is_empty() && !accounting_red {
println!("FLOOR: green{excluded}");
0
} else {
if !reds.is_empty() {
println!(" red: {}", reds.join(", "));
}
println!("FLOOR: RED{excluded}");
2
}
}
#[must_use]
pub fn on_stop_violations(manifest: &Manifest) -> Vec<Violation> {
let Some(floor) = manifest.floor.as_ref() else {
return Vec::new();
};
let mut violations = Vec::new();
for command in floor.commands.iter().filter(|c| c.on_stop) {
let (rule, detail) = match execute(command) {
Ok(outcome) if outcome.green => continue,
Ok(outcome) => (
format!("floor.{}", command.name),
tail_of(&outcome.captured),
),
Err(error) => (format!("floor.{}", command.name), error.to_string()),
};
violations.push(Violation {
file: String::new(),
line: 0,
rule,
contract: None,
fix_hint: format!("{detail} — run `pushkin floor` locally to reproduce and fix it."),
suggestions: Vec::new(),
severity: Severity::Error,
});
}
violations
}
#[must_use]
pub fn new_read_only_violations(manifest: &Manifest) -> Vec<Violation> {
let Some(floor) = manifest.floor.as_ref() else {
return Vec::new();
};
let mut violations = Vec::new();
for command in floor.commands.iter().filter(|c| c.on_new_read_only) {
let (rule, detail) = match execute(command) {
Ok(outcome) if outcome.green => continue,
Ok(outcome) => (
format!("floor.{}", command.name),
tail_of(&outcome.captured),
),
Err(error) => (format!("floor.{}", command.name), error.to_string()),
};
violations.push(Violation {
file: String::new(),
line: 0,
rule,
contract: None,
fix_hint: format!(
"{detail} — a new read-only file was staged; run `pushkin floor` \
locally to reproduce and fix it before committing."
),
suggestions: Vec::new(),
severity: Severity::Error,
});
}
violations
}
fn tail_of(output: &str) -> String {
let lines: Vec<&str> = output.lines().filter(|l| !l.trim().is_empty()).collect();
let start = lines.len().saturating_sub(FAILURE_TAIL_LINES);
lines[start..].join("\n")
}
const FAILURE_TAIL_LINES: usize = 8;