1use std::process::ExitCode;
4
5use bhtune_core::DcsTemplate;
6use bhtune_db::SqlitePool;
7use bhtune_runtime::{
8 config::BhtuneConfig,
9 tune::{
10 PreflightCheck, PreflightCheckStatus, PreflightReport, PreflightTagRead,
11 ValidatedTuneRequest, preflight,
12 },
13};
14use serde::Serialize;
15
16use crate::{EXIT_CHECK_FAILED, args::CheckArgs, output::OutputFormat};
17
18#[derive(Serialize)]
19struct CheckJson<'a> {
20 status: &'static str,
21 strict: bool,
22 checks: &'a [PreflightCheck],
23 tag_reads: &'a [PreflightTagRead],
24}
25
26pub(crate) async fn run(
27 args: CheckArgs,
28 config: &BhtuneConfig,
29 database: Option<&SqlitePool>,
30 user_templates: Option<&[DcsTemplate]>,
31) -> anyhow::Result<ExitCode> {
32 let output = args.tune.output;
33 let strict = args.strict;
34 let report = preflight(
35 ValidatedTuneRequest::try_from(args.tune)?,
36 config,
37 database,
38 user_templates,
39 )
40 .await?;
41 print_report(&report, output, strict)?;
42
43 Ok(exit_code(&report, strict))
44}
45
46fn exit_code(report: &PreflightReport, strict: bool) -> ExitCode {
47 if report.passes(strict) {
48 ExitCode::SUCCESS
49 } else {
50 ExitCode::from(EXIT_CHECK_FAILED)
51 }
52}
53
54fn print_report(
55 report: &PreflightReport,
56 output: OutputFormat,
57 strict: bool,
58) -> anyhow::Result<()> {
59 let status = if report.passes(strict) {
60 report.status()
61 } else {
62 PreflightCheckStatus::Fail
63 };
64 match output {
65 OutputFormat::Json => {
66 let json = CheckJson {
67 status: status.as_str(),
68 strict,
69 checks: &report.checks,
70 tag_reads: &report.tag_reads,
71 };
72 println!("{}", serde_json::to_string_pretty(&json)?);
73 }
74 OutputFormat::Table => print_table(report, status, strict),
75 }
76 Ok(())
77}
78
79fn print_table(report: &PreflightReport, status: PreflightCheckStatus, strict: bool) {
80 println!("Preflight: {}", status.as_str());
81 if strict && report.has_warnings() && !report.has_failures() {
82 println!(" --strict treats warnings as failures");
83 }
84
85 println!();
86 println!("{:<6} {:<30} DETAIL", "STATUS", "CHECK");
87 for check in &report.checks {
88 println!(
89 "{:<6} {:<30} {}",
90 check.status.as_str(),
91 check.name,
92 check.detail
93 );
94 }
95
96 if !report.tag_reads.is_empty() {
97 println!();
98 println!(
99 "{:<6} {:<36} {:<28} {:<10} {:<11} DETAIL",
100 "STATUS", "TAG", "ROLES", "VALUE", "QUALITY"
101 );
102 for read in &report.tag_reads {
103 println!(
104 "{:<6} {:<36} {:<28} {:<10} {:<11} {}",
105 read.status.as_str(),
106 read.tag,
107 read.roles.join(","),
108 read.value.as_deref().unwrap_or("-"),
109 read.quality.as_deref().unwrap_or("-"),
110 read.detail
111 );
112 }
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use std::process::ExitCode;
119
120 use bhtune_runtime::tune::{
121 PreflightCheck, PreflightCheckStatus, PreflightReport, PreflightTagRead,
122 };
123
124 use clap::Parser;
125
126 use super::{CheckJson, exit_code, print_report};
127 use crate::EXIT_CHECK_FAILED;
128 use crate::output::OutputFormat;
129
130 #[test]
131 fn strict_mode_changes_warning_summary_to_failure() {
132 let report = PreflightReport {
133 checks: vec![PreflightCheck {
134 name: "compatibility".to_string(),
135 status: PreflightCheckStatus::Warn,
136 detail: "unknown".to_string(),
137 }],
138 tag_reads: Vec::new(),
139 };
140 assert_eq!(exit_code(&report, false), ExitCode::SUCCESS);
141 assert_eq!(exit_code(&report, true), ExitCode::from(EXIT_CHECK_FAILED));
142 }
143
144 #[tokio::test]
145 async fn check_returns_exit_eight_when_a_check_fails() {
146 let dir = tempfile::tempdir().unwrap();
147 let db = dir.path().join("missing.db");
148 let config = dir.path().join("bhtune.toml");
149 std::fs::write(&config, "").unwrap();
150 let cli = crate::args::Cli::try_parse_from([
151 "bhtune".to_string(),
152 "--db".to_string(),
153 db.display().to_string(),
154 "--config".to_string(),
155 config.display().to_string(),
156 "check".to_string(),
157 "--driver".to_string(),
158 "simulator".to_string(),
159 "--tagname".to_string(),
160 "Sim.Loop1.PV".to_string(),
161 "--template".to_string(),
162 "Yokogawa CentumVP".to_string(),
163 "--process-type".to_string(),
164 "flow".to_string(),
165 "--controller-type".to_string(),
166 "pi".to_string(),
167 "--relay-amp".to_string(),
168 "0".to_string(),
169 "--pv-range-high".to_string(),
170 "100".to_string(),
171 "--pv-range-low".to_string(),
172 "0".to_string(),
173 "--mv-range-high".to_string(),
174 "100".to_string(),
175 "--mv-range-low".to_string(),
176 "0".to_string(),
177 "--direction".to_string(),
178 "reverse".to_string(),
179 "--output".to_string(),
180 "json".to_string(),
181 ])
182 .unwrap();
183
184 assert_eq!(
185 crate::run_with_cli(cli).await,
186 ExitCode::from(EXIT_CHECK_FAILED)
187 );
188 assert!(!db.exists());
189 }
190
191 #[test]
192 fn json_report_includes_overall_status_strict_setting_and_check_details() {
193 let report = bhtune_runtime::tune::PreflightReport {
194 checks: vec![PreflightCheck {
195 name: "configuration".to_string(),
196 status: PreflightCheckStatus::Pass,
197 detail: "valid".to_string(),
198 }],
199 tag_reads: Vec::new(),
200 };
201 let output = serde_json::to_string(&CheckJson {
202 status: report.status().as_str(),
203 strict: false,
204 checks: &report.checks,
205 tag_reads: &report.tag_reads,
206 })
207 .unwrap();
208 let value: serde_json::Value = serde_json::from_str(&output).unwrap();
209 assert_eq!(value["status"], "pass");
210 assert_eq!(value["strict"], false);
211 assert_eq!(value["checks"][0]["name"], "configuration");
212 }
213
214 #[test]
215 fn report_printing_handles_both_formats() {
216 let report = PreflightReport {
217 checks: vec![PreflightCheck {
218 name: "configuration".to_string(),
219 status: PreflightCheckStatus::Pass,
220 detail: "valid".to_string(),
221 }],
222 tag_reads: vec![PreflightTagRead {
223 tag: "Unit1.LIC101.PV".to_string(),
224 roles: vec!["process_variable".to_string()],
225 value: None,
226 quality: None,
227 status: PreflightCheckStatus::Fail,
228 detail: "driver returned no value".to_string(),
229 }],
230 };
231 print_report(&report, OutputFormat::Json, false).unwrap();
232 print_report(&report, OutputFormat::Table, false).unwrap();
233
234 let warning_report = PreflightReport {
235 checks: vec![PreflightCheck {
236 name: "compatibility".to_string(),
237 status: PreflightCheckStatus::Warn,
238 detail: "not verified".to_string(),
239 }],
240 tag_reads: Vec::new(),
241 };
242 print_report(&warning_report, OutputFormat::Table, true).unwrap();
243 }
244}