1use std::collections::BTreeMap;
14use std::io::Write;
15use std::process::{Command, Stdio};
16use std::time::Instant;
17
18use serde::{Deserialize, Serialize};
19
20pub mod cases;
21pub mod recorder;
22
23pub const DEFAULT_PORT: u16 = 445;
25
26pub type Metrics = BTreeMap<String, f64>;
28
29#[derive(Clone, Debug)]
31pub struct Endpoint {
32 pub host: String,
34 pub port: u16,
36 pub user: String,
38 pub pass: String,
40 pub share: String,
42}
43
44impl Endpoint {
45 pub fn from_env() -> Option<Endpoint> {
48 let host = non_empty(std::env::var("SMB_TEST_HOST").ok())?;
49 Some(Endpoint {
50 host,
51 port: std::env::var("SMB_TEST_PORT").ok().and_then(|p| p.parse().ok()).unwrap_or(DEFAULT_PORT),
52 user: std::env::var("SMB_TEST_USER").unwrap_or_default(),
53 pass: std::env::var("SMB_TEST_PASS").unwrap_or_default(),
54 share: std::env::var("SMB_TEST_SHARE").unwrap_or_else(|_| "public".into()),
55 })
56 }
57}
58
59fn non_empty(v: Option<String>) -> Option<String> {
60 v.filter(|s| !s.is_empty())
61}
62
63#[derive(Default, Clone, Debug)]
65pub struct Opts {
66 pub sign: bool,
68 pub encrypt: bool,
70 pub dialect: Option<String>,
72}
73
74impl Opts {
75 pub fn dialect(d: &str) -> Opts {
77 Opts { dialect: Some(d.to_string()), ..Opts::default() }
78 }
79 pub fn signed() -> Opts {
81 Opts { sign: true, ..Opts::default() }
82 }
83 pub fn encrypted() -> Opts {
85 Opts { encrypt: true, ..Opts::default() }
86 }
87}
88
89#[derive(Clone, Debug)]
91pub struct Driver {
92 pub python: String,
94 pub script: String,
96}
97
98impl Driver {
99 pub fn from_env() -> Driver {
103 Driver {
104 python: std::env::var("SMB_TEST_PYTHON")
105 .unwrap_or_else(|_| "/tmp/smbtest_venv/bin/python".into()),
106 script: std::env::var("SMB_TEST_DRIVER").unwrap_or_else(|_| default_driver_script()),
107 }
108 }
109}
110
111fn default_driver_script() -> String {
113 std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
114 .join("../../test/driver/smb_driver.py")
115 .to_string_lossy()
116 .into_owned()
117}
118
119#[derive(Deserialize, Debug, Default)]
121pub struct DriverResp {
122 pub ok: bool,
124 #[serde(default)]
126 pub error: Option<String>,
127 #[serde(default)]
129 pub dialect: String,
130 #[serde(default)]
132 pub steps: Vec<serde_json::Value>,
133}
134
135impl DriverResp {
136 pub fn step(&self, idx: usize) -> Option<&serde_json::Value> {
138 self.steps.get(idx)
139 }
140}
141
142#[derive(Serialize)]
143struct DriverReq<'a> {
144 endpoint: EndpointReq<'a>,
145 ops: serde_json::Value,
146}
147
148#[derive(Serialize)]
149struct EndpointReq<'a> {
150 host: &'a str,
151 port: u16,
152 user: &'a str,
153 pass: &'a str,
154 share: &'a str,
155 sign: bool,
156 encrypt: bool,
157 #[serde(skip_serializing_if = "Option::is_none")]
158 dialect: Option<&'a str>,
159}
160
161pub struct Ctx<'a> {
163 pub ep: &'a Endpoint,
165 pub driver: &'a Driver,
167}
168
169impl<'a> Ctx<'a> {
170 pub fn run(&self, ops: serde_json::Value) -> Result<DriverResp, String> {
172 self.run_with(&Opts::default(), ops)
173 }
174
175 pub fn run_with(&self, opts: &Opts, ops: serde_json::Value) -> Result<DriverResp, String> {
177 let req = DriverReq {
178 endpoint: EndpointReq {
179 host: &self.ep.host,
180 port: self.ep.port,
181 user: &self.ep.user,
182 pass: &self.ep.pass,
183 share: &self.ep.share,
184 sign: opts.sign,
185 encrypt: opts.encrypt,
186 dialect: opts.dialect.as_deref(),
187 },
188 ops,
189 };
190 let payload = serde_json::to_vec(&req).map_err(|e| e.to_string())?;
191 let mut child = Command::new(&self.driver.python)
192 .arg(&self.driver.script)
193 .stdin(Stdio::piped())
194 .stdout(Stdio::piped())
195 .stderr(Stdio::piped())
196 .spawn()
197 .map_err(|e| format!("spawn {} failed: {e}", self.driver.python))?;
198 child
199 .stdin
200 .take()
201 .ok_or("no stdin")?
202 .write_all(&payload)
203 .map_err(|e| e.to_string())?;
204 let out = child.wait_with_output().map_err(|e| e.to_string())?;
205 if !out.status.success() && out.stdout.is_empty() {
206 return Err(format!(
207 "driver exited {}: {}",
208 out.status,
209 String::from_utf8_lossy(&out.stderr).trim()
210 ));
211 }
212 serde_json::from_slice(&out.stdout).map_err(|e| {
213 format!("bad driver output: {e}: {}", String::from_utf8_lossy(&out.stdout))
214 })
215 }
216}
217
218pub struct TestCase {
220 pub id: &'static str,
222 pub category: &'static str,
224 pub spec: &'static str,
226 pub about: &'static str,
228 pub run: fn(&Ctx) -> Result<Metrics, String>,
230}
231
232#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
234#[serde(rename_all = "lowercase")]
235pub enum Status {
236 Pass,
238 Fail,
240 Skip,
242 Error,
244}
245
246#[derive(Serialize, Deserialize, Clone, Debug)]
248pub struct CaseResult {
249 pub id: String,
251 pub category: String,
253 pub spec: String,
255 pub about: String,
257 pub status: Status,
259 pub duration_ms: u128,
261 pub message: String,
263 pub metrics: Metrics,
265}
266
267pub fn run_case(case: &TestCase, ctx: &Ctx) -> CaseResult {
269 let started = Instant::now();
270 let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (case.run)(ctx)));
271 let duration_ms = started.elapsed().as_millis();
272 let (status, message, metrics) = match outcome {
273 Ok(Ok(metrics)) => (Status::Pass, String::new(), metrics),
274 Ok(Err(msg)) => (Status::Fail, msg, Metrics::new()),
275 Err(_) => (Status::Error, "panicked".into(), Metrics::new()),
276 };
277 CaseResult {
278 id: case.id.into(),
279 category: case.category.into(),
280 spec: case.spec.into(),
281 about: case.about.into(),
282 status,
283 duration_ms,
284 message,
285 metrics,
286 }
287}