bhtune_runtime/
gateway.rs1use anyhow::Context as _;
7use bhtune_driver::{OpcDaGatewayCompatibility, check_gateway_compatibility};
8
9pub async fn require_live_gateway_compatible(
15 bridge_host: &str,
16 server: Option<&str>,
17) -> anyhow::Result<OpcDaGatewayCompatibility> {
18 let report = check_gateway_compatibility(bridge_host, server)
19 .await
20 .with_context(|| format!("failed to check opcda-bridge compatibility at {bridge_host}"))?;
21 if let Some(refusal) = report.live_mutation_refusal() {
22 anyhow::bail!(refusal);
23 }
24 if let Some(warning) = report.warning_message() {
25 tracing::warn!("{warning}");
26 }
27 Ok(report)
28}
29
30#[must_use]
32pub fn compatibility_json(report: &OpcDaGatewayCompatibility) -> String {
33 serde_json::to_string(report).unwrap_or_default()
34}
35
36pub fn parse_stored_compatibility(json: &str) -> anyhow::Result<OpcDaGatewayCompatibility> {
38 serde_json::from_str(json).context("stored gateway compatibility snapshot could not be parsed")
39}
40
41#[cfg(test)]
42mod tests {
43 use opcda_bridge_proto::bridge::{
44 GetGatewayInfoResponse, ProtocolFeature, ProtocolFeatureKind,
45 };
46
47 use super::{compatibility_json, parse_stored_compatibility, require_live_gateway_compatible};
48 use crate::test_support::{MockBridgeService, start_mock_server};
49 use bhtune_driver::OpcDaCompatibilityStatus;
50
51 fn protocol_feature(
52 kind: ProtocolFeatureKind,
53 min_version: u32,
54 max_version: u32,
55 ) -> ProtocolFeature {
56 ProtocolFeature {
57 kind: kind as i32,
58 min_version,
59 max_version,
60 }
61 }
62
63 fn gateway_info(features: Vec<ProtocolFeature>) -> GetGatewayInfoResponse {
64 GetGatewayInfoResponse {
65 application_version: "0.5.9".into(),
66 compatibility_schema_version: 1,
67 features,
68 }
69 }
70
71 #[tokio::test]
72 async fn require_live_gateway_compatible_refuses_only_an_incompatible_core() {
73 let incompatible = start_mock_server(MockBridgeService {
74 gateway_info_response: gateway_info(vec![protocol_feature(
75 ProtocolFeatureKind::Core,
76 9,
77 9,
78 )]),
79 ..Default::default()
80 })
81 .await;
82 let refused = require_live_gateway_compatible(&incompatible.0, Some("Plant.Server"))
83 .await
84 .expect_err("an incompatible core must refuse the live mutation");
85 assert!(refused.to_string().contains("incompatible"));
86
87 let partial = start_mock_server(MockBridgeService {
88 gateway_info_response: gateway_info(vec![protocol_feature(
89 ProtocolFeatureKind::Core,
90 1,
91 1,
92 )]),
93 ..Default::default()
94 })
95 .await;
96 let partial_report = require_live_gateway_compatible(&partial.0, None)
97 .await
98 .unwrap();
99 assert_eq!(partial_report.status, OpcDaCompatibilityStatus::Partial);
100
101 let unknown = start_mock_server(MockBridgeService::default()).await;
102 let unknown_report = require_live_gateway_compatible(&unknown.0, None)
103 .await
104 .unwrap();
105 assert_eq!(unknown_report.status, OpcDaCompatibilityStatus::Unknown);
106
107 let full = start_mock_server(MockBridgeService {
108 gateway_info_response: gateway_info(vec![
109 protocol_feature(ProtocolFeatureKind::Core, 1, 1),
110 protocol_feature(ProtocolFeatureKind::Namespace, 2, 3),
111 protocol_feature(ProtocolFeatureKind::IndexedSearch, 2, 2),
112 ]),
113 ..Default::default()
114 })
115 .await;
116 let full_report = require_live_gateway_compatible(&full.0, None)
117 .await
118 .unwrap();
119 assert_eq!(full_report.status, OpcDaCompatibilityStatus::Full);
120 assert!(compatibility_json(&full_report).contains("\"status\":\"full\""));
121 }
122
123 #[test]
124 fn parse_stored_compatibility_rejects_an_unreadable_snapshot() {
125 assert!(parse_stored_compatibility("not-a-report").is_err());
126 assert!(parse_stored_compatibility(r#"{"status":"unknown"}"#).is_err());
127 }
128}