use std::collections::BTreeMap;
use std::path::Path;
use serde::Serialize;
use crate::clock::Clock;
use crate::error::{Error, Result};
use crate::manifest::{Command, Manifest};
use crate::provenance::Provenance;
use crate::{cache, hashing, outputs, parametric, probe, resolve, selection, ttl};
pub const SCHEMA: &str = include_str!("../schema/status.schema.json");
#[derive(Serialize)]
struct Report {
manifest: String,
#[serde(skip)]
now: Clock,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
probes: BTreeMap<String, String>,
#[serde(skip)]
empty_note: String,
rules: Vec<RuleStatus>,
}
#[derive(Serialize)]
struct RuleStatus {
name: String,
source: String,
state: State,
#[serde(skip_serializing_if = "Option::is_none")]
digest: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
missing_output: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
ttl: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
expires_at: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
cached: Option<CachedInfo>,
inputs: Vec<hashing::FileHash>,
}
#[derive(Serialize)]
struct CachedInfo {
digest: String,
ok: bool,
ran_at: u64,
#[serde(skip_serializing_if = "Vec::is_empty")]
outputs: Vec<String>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
probes: BTreeMap<String, String>,
}
#[path = "status_verdict.rs"]
mod verdict;
use verdict::verdict;
pub(crate) use verdict::{Assessment, State};
pub fn report(cwd: &Path, tags: &[String]) -> Result<String> {
let report = collect(cwd, tags)?;
if report.rules.is_empty() {
return Ok(format!("{}\n", report.empty_note));
}
Ok(table::render_text(&report))
}
pub fn report_json(cwd: &Path, tags: &[String]) -> Result<String> {
let report = collect(cwd, tags)?;
let text = serde_json::to_string_pretty(&report)
.map_err(|err| Error::Internal(format!("serializing status json: {err}")))?;
Ok(format!("{text}\n"))
}
fn collect(cwd: &Path, tags: &[String]) -> Result<Report> {
let now = Clock::resolve()?;
let located = Manifest::locate(cwd)?;
let manifest = &located.manifest;
let base = located.root.as_path();
let cache_dir = base.join(&manifest.cache_dir);
let records = Records {
dir: &cache_dir,
now,
};
let mut matches = Vec::with_capacity(manifest.commands.len());
let mut shared_by_rule: BTreeMap<String, Shared> = BTreeMap::new();
let mut probes = probe::Resolver::new(manifest, base);
for rule in &manifest.commands {
if !tags.is_empty() && !tags.iter().all(|tag| rule.tags.contains(tag)) {
continue;
}
shared_by_rule.insert(
rule.name.clone(),
shared_inputs(manifest, rule, base, &mut probes)?,
);
matches.extend(parametric::expand_rule(manifest, base, rule)?);
}
parametric::detect_collision(&matches)?;
let mut rules = Vec::with_capacity(matches.len());
for hit in &matches {
let shared = shared_by_rule
.get(hit.rule.name.as_str())
.expect("shared inputs resolved for every kept rule");
let source = located
.provenance
.commands
.get(hit.rule.name.as_str())
.map(|path| Provenance::display(path, base))
.expect("provenance recorded for every declared command");
rules.push(rule_status(hit, shared, base, records, source)?);
}
let manifest_path = located.path.display().to_string();
let empty_note = if rules.is_empty() {
let kept: Vec<&str> = shared_by_rule.keys().map(String::as_str).collect();
selection::empty_note(manifest, &manifest_path, tags, &kept)
} else {
String::new()
};
Ok(Report {
empty_note,
manifest: manifest_path,
now,
probes: probes.resolved().clone(),
rules,
})
}
#[derive(Clone, Copy)]
pub(crate) struct Records<'a> {
pub(crate) dir: &'a Path,
pub(crate) now: Clock,
}
pub(crate) struct Shared {
files: Vec<String>,
probes: BTreeMap<String, String>,
}
pub(crate) fn shared_inputs(
manifest: &Manifest,
rule: &Command,
base: &Path,
probes: &mut probe::Resolver,
) -> Result<Shared> {
Ok(Shared {
files: resolve::expand_groups(&manifest.glob_groups(rule)?, base)?,
probes: probes.for_rule(rule)?,
})
}
fn expansion_files(shared: &Shared, hit: ¶metric::Match) -> Vec<String> {
let mut files = shared.files.clone();
if let Some(file) = &hit.exp.file {
files.push(file.clone());
files.sort();
files.dedup();
}
files
}
struct Measured {
assessed: Assessment,
digest: Option<String>,
inputs: Vec<hashing::FileHash>,
cached: Option<CachedInfo>,
}
fn measure(
hit: ¶metric::Match,
shared: &Shared,
base: &Path,
records: Records,
) -> Result<Measured> {
let rule = hit.rule;
let files = expansion_files(shared, hit);
let cached = read_cached(records.dir, &hit.exp.identity);
if files.is_empty() && shared.probes.is_empty() && !ttl::time_only(rule) {
let assessed = Assessment {
state: State::NoInputs,
missing_output: None,
changed_probe: None,
expired: None,
};
return Ok(Measured {
assessed,
digest: None,
inputs: Vec::new(),
cached,
});
}
let inputs = hashing::hash_each(base, &files)?;
let digest = hashing::digest_all(&inputs, &shared.probes);
let expired = cached
.as_ref()
.and_then(|record| ttl::outlived(rule.ttl.as_ref(), record.ok, record.ran_at, records.now));
let missing = outputs::first_missing(base, &rule.outputs);
let assessed = verdict(cached.as_ref(), &digest, missing, expired, &shared.probes);
Ok(Measured {
assessed,
digest: Some(digest),
inputs,
cached,
})
}
fn rule_status(
hit: ¶metric::Match,
shared: &Shared,
base: &Path,
records: Records,
source: String,
) -> Result<RuleStatus> {
let measured = measure(hit, shared, base, records)?;
let ttl = hit.rule.ttl.as_ref();
let passed = measured.cached.as_ref().filter(|record| record.ok);
Ok(RuleStatus {
name: hit.exp.identity.clone(),
source,
state: measured.assessed.state,
digest: measured.digest,
missing_output: measured.assessed.missing_output,
ttl: ttl.map(|ttl| ttl.as_str().to_owned()),
expires_at: ttl
.zip(passed)
.map(|(ttl, record)| ttl.expires_at(record.ran_at)),
cached: measured.cached,
inputs: measured.inputs,
})
}
pub(crate) fn expansion_state(
hit: ¶metric::Match,
shared: &Shared,
base: &Path,
records: Records,
) -> Result<Assessment> {
Ok(measure(hit, shared, base, records)?.assessed)
}
fn read_cached(cache_dir: &Path, name: &str) -> Option<CachedInfo> {
cache::read(cache_dir, name).map(|cached| CachedInfo {
digest: cached.digest,
ok: cached.ok,
ran_at: cached.ran_at,
outputs: cached.outputs,
probes: cached.probes,
})
}
#[path = "status_table.rs"]
mod table;
#[cfg(test)]
#[path = "status_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "status_source_tests.rs"]
mod source_tests;