pub use crate::app::sensor::{
BenchOutcome, SensorReportBuilder, default_engine_capability, sensor_fingerprint,
};
use crate::app::runtime::{HostProbe, ProcessRunner};
use crate::app::{CheckRequest, CheckUseCase, Clock};
use perfgate_types::error::{AdapterError, ConfigValidationError, IoError, PerfgateError};
use perfgate_types::{
BASELINE_REASON_NO_BASELINE, ConfigFile, ERROR_KIND_EXEC, ERROR_KIND_IO, ERROR_KIND_PARSE,
HostMismatchPolicy, MAX_FINDINGS_DEFAULT, RunReceipt, STAGE_BASELINE_RESOLVE,
STAGE_CONFIG_PARSE, STAGE_RUN_COMMAND, STAGE_WRITE_ARTIFACTS, SensorReport, ToolInfo,
validate_bench_name,
};
#[derive(Debug, Clone)]
pub struct SensorCheckOptions {
pub require_baseline: bool,
pub fail_on_warn: bool,
pub env: Vec<(String, String)>,
pub output_cap_bytes: usize,
pub allow_nonzero: bool,
pub host_mismatch_policy: HostMismatchPolicy,
pub max_findings: Option<usize>,
}
impl Default for SensorCheckOptions {
fn default() -> Self {
Self {
require_baseline: false,
fail_on_warn: false,
env: Vec::new(),
output_cap_bytes: 8192,
allow_nonzero: false,
host_mismatch_policy: HostMismatchPolicy::Warn,
max_findings: Some(MAX_FINDINGS_DEFAULT),
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn run_sensor_check<R, H, C>(
runner: &R,
host_probe: &H,
clock: &C,
config: &ConfigFile,
bench_name: &str,
baseline: Option<&RunReceipt>,
tool: ToolInfo,
options: SensorCheckOptions,
) -> SensorReport
where
R: ProcessRunner + Clone,
H: HostProbe + Clone,
C: Clock + Clone,
{
let started_at = clock.now_rfc3339();
let start_instant = std::time::Instant::now();
if let Err(err) = validate_bench_name(bench_name) {
let ended_at = clock.now_rfc3339();
let duration_ms = start_instant.elapsed().as_millis() as u64;
let builder = SensorReportBuilder::new(tool, started_at)
.ended_at(ended_at, duration_ms)
.baseline(baseline.is_some(), None);
return builder.build_error(&err.to_string(), STAGE_CONFIG_PARSE, ERROR_KIND_PARSE);
}
if let Err(msg) = config.validate() {
let ended_at = clock.now_rfc3339();
let duration_ms = start_instant.elapsed().as_millis() as u64;
let builder = SensorReportBuilder::new(tool, started_at)
.ended_at(ended_at, duration_ms)
.baseline(baseline.is_some(), None);
return builder.build_error(
&format!("config validation: {}", msg),
STAGE_CONFIG_PARSE,
ERROR_KIND_PARSE,
);
}
let baseline_available = baseline.is_some();
let result = CheckUseCase::new(runner.clone(), host_probe.clone(), clock.clone()).execute(
CheckRequest {
noise_threshold: None,
noise_policy: None,
config: config.clone(),
bench_name: bench_name.to_string(),
out_dir: std::path::PathBuf::from("."),
baseline: baseline.cloned(),
baseline_path: None,
require_baseline: options.require_baseline,
fail_on_warn: options.fail_on_warn,
tool: tool.clone(),
env: options.env.clone(),
output_cap_bytes: options.output_cap_bytes,
allow_nonzero: options.allow_nonzero,
host_mismatch_policy: options.host_mismatch_policy,
significance_alpha: None,
significance_min_samples: 8,
require_significance: false,
},
);
let ended_at = clock.now_rfc3339();
let duration_ms = start_instant.elapsed().as_millis() as u64;
let baseline_reason = if !baseline_available {
Some(BASELINE_REASON_NO_BASELINE.to_string())
} else {
None
};
match result {
Ok(outcome) => {
let mut builder = SensorReportBuilder::new(tool, started_at)
.ended_at(ended_at, duration_ms)
.baseline(baseline_available, baseline_reason);
if let Some(limit) = options.max_findings {
builder = builder.max_findings(limit);
}
builder.build(&outcome.report)
}
Err(err) => {
let (stage, error_kind) = classify_error(&err);
let builder = SensorReportBuilder::new(tool, started_at)
.ended_at(ended_at, duration_ms)
.baseline(baseline_available, baseline_reason);
builder.build_error(&err.to_string(), stage, error_kind)
}
}
}
pub fn classify_error(err: &anyhow::Error) -> (&'static str, &'static str) {
if err.downcast_ref::<ConfigValidationError>().is_some() {
return (STAGE_CONFIG_PARSE, ERROR_KIND_PARSE);
}
if let Some(pe) = err.downcast_ref::<PerfgateError>() {
return match pe {
PerfgateError::Validation(_) => (STAGE_CONFIG_PARSE, ERROR_KIND_PARSE),
PerfgateError::Config(_) => (STAGE_CONFIG_PARSE, ERROR_KIND_PARSE),
PerfgateError::Adapter(ae) => match ae {
AdapterError::Timeout => (STAGE_RUN_COMMAND, ERROR_KIND_EXEC),
AdapterError::EmptyArgv => (STAGE_RUN_COMMAND, ERROR_KIND_EXEC),
AdapterError::TimeoutUnsupported => (STAGE_RUN_COMMAND, ERROR_KIND_EXEC),
AdapterError::RunCommand { .. } => (STAGE_RUN_COMMAND, ERROR_KIND_EXEC),
AdapterError::Other(_) => (STAGE_RUN_COMMAND, ERROR_KIND_IO),
},
PerfgateError::Io(ie) => match ie {
IoError::BaselineNotFound { .. } => (STAGE_BASELINE_RESOLVE, ERROR_KIND_IO),
IoError::BaselineResolve(_) => (STAGE_BASELINE_RESOLVE, ERROR_KIND_IO),
IoError::ArtifactWrite(_) => (STAGE_WRITE_ARTIFACTS, ERROR_KIND_IO),
IoError::RunCommand { .. } => (STAGE_RUN_COMMAND, ERROR_KIND_EXEC),
IoError::Other(_) => (STAGE_RUN_COMMAND, ERROR_KIND_IO),
},
PerfgateError::Stats(_) => (STAGE_RUN_COMMAND, ERROR_KIND_PARSE),
PerfgateError::Paired(_) => (STAGE_RUN_COMMAND, ERROR_KIND_PARSE),
PerfgateError::Auth(_) => (STAGE_BASELINE_RESOLVE, ERROR_KIND_IO),
PerfgateError::Parse(_) => (STAGE_CONFIG_PARSE, ERROR_KIND_PARSE),
};
}
if err.downcast_ref::<crate::domain::DomainError>().is_some() {
return (STAGE_RUN_COMMAND, ERROR_KIND_EXEC);
}
let msg_lower = err.to_string().to_lowercase();
if msg_lower.contains("config") || msg_lower.contains("toml") || msg_lower.contains("json") {
(STAGE_CONFIG_PARSE, ERROR_KIND_PARSE)
} else if msg_lower.contains("baseline") {
(STAGE_BASELINE_RESOLVE, ERROR_KIND_IO)
} else if msg_lower.contains("failed to run")
|| msg_lower.contains("spawn")
|| msg_lower.contains("exec")
{
(STAGE_RUN_COMMAND, ERROR_KIND_EXEC)
} else if msg_lower.contains("write") || msg_lower.contains("permission") {
(STAGE_WRITE_ARTIFACTS, ERROR_KIND_IO)
} else {
(STAGE_RUN_COMMAND, ERROR_KIND_IO)
}
}