Skip to main content

xianyun_cluster/driver/
mock.rs

1//! Port of `internal/cluster/mock/driver.go`: a mock [`super::Driver`]
2//! implementation for testing. This module is intended for use in tests
3//! only and should not be depended on by production code.
4
5use super::{Driver as DriverTrait, DriverError, State, Status};
6use std::collections::HashMap;
7use std::sync::{LazyLock, RwLock};
8use xianyun_core::config::types::ClusterConfig;
9
10pub const DRIVER_NAME: &str = "mock";
11pub const MOCK_KUBERNETES_VERSION: &str = "v1.30.0+mock";
12
13const MOCK_IP_ADDRESS: &str = "192.168.64.100";
14
15/// Port of `mock.ErrorConfig`.
16#[derive(Debug, Clone, Default)]
17pub struct ErrorConfig {
18    pub start_error: Option<DriverError>,
19    pub stop_error: Option<DriverError>,
20    pub destroy_error: Option<DriverError>,
21    pub status_error: Option<DriverError>,
22    pub get_kubeconfig_error: Option<DriverError>,
23}
24
25/// Port of `mock.ClusterState`, the externally visible mirror of the
26/// internal `clusterState`.
27#[derive(Debug, Clone, Default)]
28pub struct ClusterState {
29    pub state: Option<State>,
30    pub kubernetes_version: String,
31    pub ip_address: String,
32    pub kubeconfig: String,
33    pub start_call_count: u32,
34    pub stop_call_count: u32,
35    pub destroy_call_count: u32,
36    pub status_call_count: u32,
37}
38
39static GLOBAL_STATE: LazyLock<RwLock<HashMap<String, ClusterState>>> =
40    LazyLock::new(|| RwLock::new(HashMap::new()));
41
42static ERROR_CONFIGS: LazyLock<RwLock<HashMap<String, ErrorConfig>>> =
43    LazyLock::new(|| RwLock::new(HashMap::new()));
44
45/// Port of `mock.Driver`.
46pub struct Driver {
47    pub cfg: ClusterConfig,
48    pub profile_name: String,
49}
50
51impl Driver {
52    /// Port of `mock.NewDriver`.
53    #[must_use]
54    pub fn new(cfg: ClusterConfig, profile_name: String) -> Self {
55        Self { cfg, profile_name }
56    }
57
58    fn configured_error(
59        &self,
60        pick: impl Fn(&ErrorConfig) -> &Option<DriverError>,
61    ) -> Option<DriverError> {
62        ERROR_CONFIGS
63            .read()
64            .unwrap_or_else(std::sync::PoisonError::into_inner)
65            .get(&self.profile_name)
66            .and_then(|cfg| pick(cfg).clone())
67    }
68}
69
70impl DriverTrait for Driver {
71    fn name(&self) -> &str {
72        DRIVER_NAME
73    }
74
75    fn start(&self) -> Result<(), DriverError> {
76        if let Some(err) = self.configured_error(|c| &c.start_error) {
77            return Err(err);
78        }
79
80        let mut state = GLOBAL_STATE
81            .write()
82            .unwrap_or_else(std::sync::PoisonError::into_inner);
83        let entry = state.entry(self.profile_name.clone()).or_default();
84        entry.start_call_count += 1;
85
86        if entry.state == Some(State::Ready) {
87            return Ok(());
88        }
89
90        entry.state = Some(State::Ready);
91        entry.kubernetes_version = MOCK_KUBERNETES_VERSION.to_string();
92        entry.ip_address = MOCK_IP_ADDRESS.to_string();
93
94        Ok(())
95    }
96
97    fn stop(&self) -> Result<(), DriverError> {
98        if let Some(err) = self.configured_error(|c| &c.stop_error) {
99            return Err(err);
100        }
101
102        let mut state = GLOBAL_STATE
103            .write()
104            .unwrap_or_else(std::sync::PoisonError::into_inner);
105        let entry = state.entry(self.profile_name.clone()).or_default();
106        entry.stop_call_count += 1;
107
108        if entry.state == Some(State::Stopped) {
109            return Ok(());
110        }
111
112        entry.state = Some(State::Stopped);
113
114        Ok(())
115    }
116
117    fn destroy(&self) -> Result<(), DriverError> {
118        if let Some(err) = self.configured_error(|c| &c.destroy_error) {
119            return Err(err);
120        }
121
122        let mut state = GLOBAL_STATE
123            .write()
124            .unwrap_or_else(std::sync::PoisonError::into_inner);
125        if !state.contains_key(&self.profile_name) {
126            return Ok(());
127        }
128        state.remove(&self.profile_name);
129
130        Ok(())
131    }
132
133    fn status(&self) -> Result<Status, DriverError> {
134        if let Some(err) = self.configured_error(|c| &c.status_error) {
135            return Err(err);
136        }
137
138        let mut state = GLOBAL_STATE
139            .write()
140            .unwrap_or_else(std::sync::PoisonError::into_inner);
141        let kube_context = format!("mock-{}", self.profile_name);
142
143        let Some(entry) = state.get_mut(&self.profile_name) else {
144            return Ok(Status {
145                driver: DRIVER_NAME.to_string(),
146                state: Some(State::Stopped),
147                kube_context,
148                ..Default::default()
149            });
150        };
151
152        entry.status_call_count += 1;
153
154        Ok(Status {
155            driver: DRIVER_NAME.to_string(),
156            state: entry.state,
157            kubernetes_version: entry.kubernetes_version.clone(),
158            ip_address: entry.ip_address.clone(),
159            host_ip: entry.ip_address.clone(),
160            kube_context,
161            exists: false,
162        })
163    }
164
165    fn get_kubeconfig(&self) -> Result<String, DriverError> {
166        if let Some(err) = self.configured_error(|c| &c.get_kubeconfig_error) {
167            return Err(err);
168        }
169
170        let state = GLOBAL_STATE
171            .read()
172            .unwrap_or_else(std::sync::PoisonError::into_inner);
173        Ok(state
174            .get(&self.profile_name)
175            .map(|s| s.kubeconfig.clone())
176            .unwrap_or_default())
177    }
178}
179
180/// Port of `mock.ResetGlobalState`: clears all global mock state and error
181/// configuration. Call between tests to avoid cross-test contamination.
182pub fn reset_global_state() {
183    GLOBAL_STATE
184        .write()
185        .unwrap_or_else(std::sync::PoisonError::into_inner)
186        .clear();
187    ERROR_CONFIGS
188        .write()
189        .unwrap_or_else(std::sync::PoisonError::into_inner)
190        .clear();
191}
192
193/// Port of `mock.SetErrorConfig`.
194pub fn set_error_config(profile_name: &str, err_cfg: ErrorConfig) {
195    ERROR_CONFIGS
196        .write()
197        .unwrap_or_else(std::sync::PoisonError::into_inner)
198        .insert(profile_name.to_string(), err_cfg);
199}
200
201/// Port of `mock.ClearErrorConfig`.
202pub fn clear_error_config(profile_name: &str) {
203    ERROR_CONFIGS
204        .write()
205        .unwrap_or_else(std::sync::PoisonError::into_inner)
206        .remove(profile_name);
207}
208
209/// Port of `mock.GetState`.
210pub fn get_state(profile_name: &str) -> Option<ClusterState> {
211    GLOBAL_STATE
212        .read()
213        .unwrap_or_else(std::sync::PoisonError::into_inner)
214        .get(profile_name)
215        .cloned()
216}
217
218/// Port of `mock.ProfileExists`.
219pub fn profile_exists(profile_name: &str) -> bool {
220    GLOBAL_STATE
221        .read()
222        .unwrap_or_else(std::sync::PoisonError::into_inner)
223        .contains_key(profile_name)
224}
225
226/// Port of `mock.SetKubeconfig`.
227pub fn set_kubeconfig(profile_name: &str, kubeconfig_yaml: &str) {
228    let mut state = GLOBAL_STATE
229        .write()
230        .unwrap_or_else(std::sync::PoisonError::into_inner);
231    let entry = state
232        .entry(profile_name.to_string())
233        .or_insert_with(|| ClusterState {
234            state: Some(State::Ready),
235            kubernetes_version: MOCK_KUBERNETES_VERSION.to_string(),
236            ..Default::default()
237        });
238    entry.kubeconfig = kubeconfig_yaml.to_string();
239}
240
241/// Port of `mock.SetClusterState`.
242pub fn set_cluster_state(profile_name: &str, state: State) {
243    GLOBAL_STATE
244        .write()
245        .unwrap_or_else(std::sync::PoisonError::into_inner)
246        .insert(
247            profile_name.to_string(),
248            ClusterState {
249                state: Some(state),
250                kubernetes_version: MOCK_KUBERNETES_VERSION.to_string(),
251                ip_address: MOCK_IP_ADDRESS.to_string(),
252                ..Default::default()
253            },
254        );
255}