1pub use perfgate_sensor::{
11 BenchOutcome, SensorReportBuilder, default_engine_capability, sensor_fingerprint,
12};
13
14use crate::{CheckRequest, CheckUseCase, Clock};
15use perfgate_adapters::{HostProbe, ProcessRunner};
16use perfgate_error::{AdapterError, ConfigValidationError, IoError, PerfgateError};
17use perfgate_types::{
18 BASELINE_REASON_NO_BASELINE, ConfigFile, ERROR_KIND_EXEC, ERROR_KIND_IO, ERROR_KIND_PARSE,
19 HostMismatchPolicy, MAX_FINDINGS_DEFAULT, RunReceipt, STAGE_BASELINE_RESOLVE,
20 STAGE_CONFIG_PARSE, STAGE_RUN_COMMAND, STAGE_WRITE_ARTIFACTS, SensorReport, ToolInfo,
21 validate_bench_name,
22};
23
24#[derive(Debug, Clone)]
26pub struct SensorCheckOptions {
27 pub require_baseline: bool,
28 pub fail_on_warn: bool,
29 pub env: Vec<(String, String)>,
30 pub output_cap_bytes: usize,
31 pub allow_nonzero: bool,
32 pub host_mismatch_policy: HostMismatchPolicy,
33 pub max_findings: Option<usize>,
34}
35
36impl Default for SensorCheckOptions {
37 fn default() -> Self {
38 Self {
39 require_baseline: false,
40 fail_on_warn: false,
41 env: Vec::new(),
42 output_cap_bytes: 8192,
43 allow_nonzero: false,
44 host_mismatch_policy: HostMismatchPolicy::Warn,
45 max_findings: Some(MAX_FINDINGS_DEFAULT),
46 }
47 }
48}
49
50#[allow(clippy::too_many_arguments)]
52pub fn run_sensor_check<R, H, C>(
53 runner: &R,
54 host_probe: &H,
55 clock: &C,
56 config: &ConfigFile,
57 bench_name: &str,
58 baseline: Option<&RunReceipt>,
59 tool: ToolInfo,
60 options: SensorCheckOptions,
61) -> SensorReport
62where
63 R: ProcessRunner + Clone,
64 H: HostProbe + Clone,
65 C: Clock + Clone,
66{
67 let started_at = clock.now_rfc3339();
68 let start_instant = std::time::Instant::now();
69
70 if let Err(err) = validate_bench_name(bench_name) {
72 let ended_at = clock.now_rfc3339();
73 let duration_ms = start_instant.elapsed().as_millis() as u64;
74 let builder = SensorReportBuilder::new(tool, started_at)
75 .ended_at(ended_at, duration_ms)
76 .baseline(baseline.is_some(), None);
77 return builder.build_error(&err.to_string(), STAGE_CONFIG_PARSE, ERROR_KIND_PARSE);
78 }
79
80 if let Err(msg) = config.validate() {
82 let ended_at = clock.now_rfc3339();
83 let duration_ms = start_instant.elapsed().as_millis() as u64;
84 let builder = SensorReportBuilder::new(tool, started_at)
85 .ended_at(ended_at, duration_ms)
86 .baseline(baseline.is_some(), None);
87 return builder.build_error(
88 &format!("config validation: {}", msg),
89 STAGE_CONFIG_PARSE,
90 ERROR_KIND_PARSE,
91 );
92 }
93
94 let baseline_available = baseline.is_some();
95
96 let result = CheckUseCase::new(runner.clone(), host_probe.clone(), clock.clone()).execute(
97 CheckRequest {
98 noise_threshold: None,
99 noise_policy: None,
100 config: config.clone(),
101 bench_name: bench_name.to_string(),
102 out_dir: std::path::PathBuf::from("."),
103 baseline: baseline.cloned(),
104 baseline_path: None,
105 require_baseline: options.require_baseline,
106 fail_on_warn: options.fail_on_warn,
107 tool: tool.clone(),
108 env: options.env.clone(),
109 output_cap_bytes: options.output_cap_bytes,
110 allow_nonzero: options.allow_nonzero,
111 host_mismatch_policy: options.host_mismatch_policy,
112 significance_alpha: None,
113 significance_min_samples: 8,
114 require_significance: false,
115 },
116 );
117
118 let ended_at = clock.now_rfc3339();
119 let duration_ms = start_instant.elapsed().as_millis() as u64;
120
121 let baseline_reason = if !baseline_available {
122 Some(BASELINE_REASON_NO_BASELINE.to_string())
123 } else {
124 None
125 };
126
127 match result {
128 Ok(outcome) => {
129 let mut builder = SensorReportBuilder::new(tool, started_at)
130 .ended_at(ended_at, duration_ms)
131 .baseline(baseline_available, baseline_reason);
132
133 if let Some(limit) = options.max_findings {
134 builder = builder.max_findings(limit);
135 }
136
137 builder.build(&outcome.report)
138 }
139 Err(err) => {
140 let (stage, error_kind) = classify_error(&err);
141 let builder = SensorReportBuilder::new(tool, started_at)
142 .ended_at(ended_at, duration_ms)
143 .baseline(baseline_available, baseline_reason);
144
145 builder.build_error(&err.to_string(), stage, error_kind)
146 }
147 }
148}
149
150pub fn classify_error(err: &anyhow::Error) -> (&'static str, &'static str) {
152 if err.downcast_ref::<ConfigValidationError>().is_some() {
153 return (STAGE_CONFIG_PARSE, ERROR_KIND_PARSE);
154 }
155
156 if let Some(pe) = err.downcast_ref::<PerfgateError>() {
157 return match pe {
158 PerfgateError::Validation(_) => (STAGE_CONFIG_PARSE, ERROR_KIND_PARSE),
159 PerfgateError::Config(_) => (STAGE_CONFIG_PARSE, ERROR_KIND_PARSE),
160 PerfgateError::Adapter(ae) => match ae {
161 AdapterError::Timeout => (STAGE_RUN_COMMAND, ERROR_KIND_EXEC),
162 AdapterError::EmptyArgv => (STAGE_RUN_COMMAND, ERROR_KIND_EXEC),
163 AdapterError::TimeoutUnsupported => (STAGE_RUN_COMMAND, ERROR_KIND_EXEC),
164 AdapterError::RunCommand { .. } => (STAGE_RUN_COMMAND, ERROR_KIND_EXEC),
165 AdapterError::Other(_) => (STAGE_RUN_COMMAND, ERROR_KIND_IO),
166 },
167 PerfgateError::Io(ie) => match ie {
168 IoError::BaselineNotFound { .. } => (STAGE_BASELINE_RESOLVE, ERROR_KIND_IO),
169 IoError::BaselineResolve(_) => (STAGE_BASELINE_RESOLVE, ERROR_KIND_IO),
170 IoError::ArtifactWrite(_) => (STAGE_WRITE_ARTIFACTS, ERROR_KIND_IO),
171 IoError::RunCommand { .. } => (STAGE_RUN_COMMAND, ERROR_KIND_EXEC),
172 IoError::Other(_) => (STAGE_RUN_COMMAND, ERROR_KIND_IO),
173 },
174 PerfgateError::Stats(_) => (STAGE_RUN_COMMAND, ERROR_KIND_PARSE),
175 PerfgateError::Paired(_) => (STAGE_RUN_COMMAND, ERROR_KIND_PARSE),
176 PerfgateError::Auth(_) => (STAGE_BASELINE_RESOLVE, ERROR_KIND_IO),
177 };
178 }
179
180 if err.downcast_ref::<perfgate_domain::DomainError>().is_some() {
181 return (STAGE_RUN_COMMAND, ERROR_KIND_EXEC);
182 }
183
184 let msg_lower = err.to_string().to_lowercase();
185
186 if msg_lower.contains("config") || msg_lower.contains("toml") || msg_lower.contains("json") {
187 (STAGE_CONFIG_PARSE, ERROR_KIND_PARSE)
188 } else if msg_lower.contains("baseline") {
189 (STAGE_BASELINE_RESOLVE, ERROR_KIND_IO)
190 } else if msg_lower.contains("failed to run")
191 || msg_lower.contains("spawn")
192 || msg_lower.contains("exec")
193 {
194 (STAGE_RUN_COMMAND, ERROR_KIND_EXEC)
195 } else if msg_lower.contains("write") || msg_lower.contains("permission") {
196 (STAGE_WRITE_ARTIFACTS, ERROR_KIND_IO)
197 } else {
198 (STAGE_RUN_COMMAND, ERROR_KIND_IO)
199 }
200}