use crate::core::config::Language;
use std::time::Instant;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum BackendOutcome {
Started,
Success,
Failure,
Skip,
UnmetPrecondition,
}
impl BackendOutcome {
pub(super) fn as_str(self) -> &'static str {
match self {
Self::Started => "started",
Self::Success => "success",
Self::Failure => "failure",
Self::Skip => "skip",
Self::UnmetPrecondition => "unmet-precondition",
}
}
}
const IMPLAUSIBLY_FAST_FAILURE_MS: u64 = 2_000;
pub(super) fn observe<T>(language: Language, operation: impl FnOnce() -> anyhow::Result<T>) -> anyhow::Result<T> {
tracing::info!(language = %language, outcome = BackendOutcome::Started.as_str(), "Starting backend build");
let started = Instant::now();
let result = operation();
let duration_ms = u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX);
let outcome = if result.is_ok() {
BackendOutcome::Success
} else {
BackendOutcome::Failure
};
record_completion(language, duration_ms, outcome);
result
}
fn record_completion(language: Language, duration_ms: u64, outcome: BackendOutcome) {
tracing::info!(language = %language, duration_ms, outcome = outcome.as_str(), "Completed backend build");
if outcome == BackendOutcome::Failure && duration_ms < IMPLAUSIBLY_FAST_FAILURE_MS {
tracing::warn!(
language = %language,
duration_ms,
outcome = outcome.as_str(),
"{language} build failed after {duration_ms}ms -- too fast to have compiled anything. Suspect the \
environment (unfetched dependencies, a missing interpreter environment, the wrong working directory) \
before suspecting the generated code."
);
}
}
pub(super) fn skipped(language: Language, reason: &str) {
tracing::info!(language = %language, outcome = BackendOutcome::Started.as_str(), "Starting backend build");
tracing::info!(
language = %language,
duration_ms = 0_u64,
outcome = BackendOutcome::Skip.as_str(),
reason,
"Completed backend build"
);
}
pub(super) fn unmet_precondition(language: Language, reason: &str, remediation: &str) {
tracing::info!(language = %language, outcome = BackendOutcome::Started.as_str(), "Starting backend build");
tracing::warn!(
language = %language,
duration_ms = 0_u64,
outcome = BackendOutcome::UnmetPrecondition.as_str(),
reason,
remediation,
"Completed backend build -- nothing was built for {language}: {reason}. Run: {remediation}"
);
}
#[cfg(test)]
mod tests {
use super::*;
use tracing_test::traced_test;
#[traced_test]
#[test]
fn reports_completion_for_distinct_backend_languages() {
observe(Language::Java, || Ok(())).expect("Java observation");
observe(Language::Zig, || -> anyhow::Result<()> {
anyhow::bail!("expected failure")
})
.expect_err("Zig failure observation");
skipped(Language::Dart, "toolchain not on PATH");
unmet_precondition(
Language::Elixir,
"dependencies not fetched",
"cd packages/elixir && mix deps.get",
);
assert!(logs_contain("language=java"));
assert!(logs_contain("language=zig"));
assert!(logs_contain("language=dart"));
assert!(logs_contain("language=elixir"));
assert!(logs_contain("outcome=\"success\""));
assert!(logs_contain("outcome=\"failure\""));
assert!(logs_contain("outcome=\"skip\""));
assert!(logs_contain("outcome=\"unmet-precondition\""));
assert!(logs_contain("duration_ms="));
}
#[traced_test]
#[test]
fn a_genuine_build_failure_still_reports_failure_and_not_unmet_precondition() {
observe(Language::Go, || -> anyhow::Result<()> {
anyhow::bail!("undefined: Foo")
})
.expect_err("failing operation");
assert!(logs_contain("outcome=\"failure\""));
assert!(!logs_contain("outcome=\"unmet-precondition\""));
}
#[traced_test]
#[test]
fn a_sub_second_failure_is_flagged_as_too_fast_to_have_compiled() {
observe(Language::Python, || -> anyhow::Result<()> {
anyhow::bail!("no virtualenv")
})
.expect_err("fast failure");
assert!(logs_contain("too fast to have compiled anything"));
}
#[traced_test]
#[test]
fn a_slow_failure_is_not_flagged_as_too_fast() {
record_completion(Language::Ruby, 17_000, BackendOutcome::Failure);
assert!(logs_contain("outcome=\"failure\""));
assert!(!logs_contain("too fast to have compiled anything"));
}
#[traced_test]
#[test]
fn a_fast_success_is_not_flagged_as_too_fast() {
record_completion(Language::Csharp, 12, BackendOutcome::Success);
assert!(logs_contain("outcome=\"success\""));
assert!(!logs_contain("too fast to have compiled anything"));
}
}