1pub mod delivery;
13
14use std::collections::BTreeMap;
15use std::path::{Path, PathBuf};
16use std::sync::Arc;
17use std::time::Duration;
18
19use serde::Deserialize;
20use serde_json::{Map, Value, json};
21
22use crate::frame::{self, Frame};
23use crate::host::{LogSink, PluginProcess};
24use crate::msg::{ERROR_KINDS, Envelope, Response};
25use crate::{
26 CAPABILITIES, Kind, MAX_VERSION, MIN_VERSION, PluginId, parse_executable_name,
27 parse_package_executable_name,
28};
29
30pub const PROTOCOL_CASES: &str = include_str!("../conformance/protocol.json");
32
33const TIMEOUT: Duration = Duration::from_secs(20);
35
36#[derive(Debug)]
38pub struct Check {
39 pub plugin: String,
41 pub name: String,
43 pub passed: bool,
45 pub detail: String,
47 pub skipped: Option<String>,
49}
50
51impl Check {
52 fn ran(plugin: &str, name: &str, r: Result<(), String>) -> Check {
54 Check {
55 plugin: plugin.to_string(),
56 name: name.to_string(),
57 passed: r.is_ok(),
58 detail: r.err().unwrap_or_default(),
59 skipped: None,
60 }
61 }
62
63 fn skipped(plugin: &str, name: &str, why: String) -> Check {
65 Check {
66 plugin: plugin.to_string(),
67 name: name.to_string(),
68 passed: true,
69 detail: String::new(),
70 skipped: Some(why),
71 }
72 }
73}
74
75#[derive(Debug, Clone, Default)]
80pub struct Session {
81 connection: Map<String, Value>,
82 statements: BTreeMap<String, String>,
83}
84
85impl Session {
86 #[must_use]
88 pub fn new(connection: Map<String, Value>) -> Session {
89 Session {
90 connection,
91 statements: BTreeMap::new(),
92 }
93 }
94
95 #[must_use]
97 pub fn statement(mut self, role: &str, sql: &str) -> Session {
98 self.statements.insert(role.into(), sql.into());
99 self
100 }
101}
102
103#[derive(Debug, Clone, Default)]
105pub struct Options {
106 env: Vec<(String, String)>,
107 session: Option<Session>,
108}
109
110impl Options {
111 #[must_use]
114 pub fn env(mut self, env: &[(&str, &str)]) -> Options {
115 self.env
116 .extend(env.iter().map(|(k, v)| (k.to_string(), v.to_string())));
117 self
118 }
119
120 #[must_use]
122 pub fn session(mut self, session: Session) -> Options {
123 self.session = Some(session);
124 self
125 }
126}
127
128#[derive(Debug, Deserialize)]
129struct Cases {
130 cases: Vec<Case>,
131}
132
133#[derive(Debug, Deserialize)]
134struct Case {
135 name: String,
136 #[serde(default)]
138 scope: Option<String>,
139 #[serde(default)]
140 kinds: Vec<Kind>,
141 #[serde(default)]
142 requires: Vec<String>,
143 #[serde(default)]
144 session: Vec<String>,
145 steps: Vec<Map<String, Value>>,
146}
147
148fn cases() -> Vec<Case> {
154 serde_json::from_str::<Cases>(PROTOCOL_CASES)
155 .expect("conformance/protocol.json parses")
156 .cases
157}
158
159#[must_use]
161pub fn run(path: &Path) -> Vec<Check> {
162 run_with(path, &Options::default())
163}
164
165#[must_use]
167pub fn run_with_env(path: &Path, env: &[(&str, &str)]) -> Vec<Check> {
168 run_with(path, &Options::default().env(env))
169}
170
171#[must_use]
173pub fn run_with(path: &Path, options: &Options) -> Vec<Check> {
174 let file = path
175 .file_name()
176 .map_or_else(|| path.display().to_string(), |f| f.to_string_lossy().to_string());
177 let cases = cases();
178 if let Some((kind, name)) = parse_executable_name(&file) {
179 let id = PluginId::new(kind, name);
180 return run_plugin(path, &id, false, &cases, options);
181 }
182 let fail = |detail: String| {
183 vec![Check::ran(
184 &file,
185 "the executable is named dre-plugin-<package> or dre-<kind>-<name>",
186 Err(detail),
187 )]
188 };
189 let Some(package) = parse_package_executable_name(&file) else {
190 return fail(format!("`{file}` is neither"));
191 };
192 let provides = match greet(path, None, options) {
193 Ok(hello) => match hello
195 .get("provides")
196 .cloned()
197 .map(serde_json::from_value::<Vec<PluginId>>)
198 {
199 Some(Ok(p)) if !p.is_empty() => p,
200 Some(Err(e)) => return fail(format!("`provides` isn't a list of plugins: {e}")),
201 _ => match serde_json::from_value::<PluginId>(json!(format!(
202 "{}/{}",
203 hello["kind"].as_str().unwrap_or_default(),
204 hello["name"].as_str().unwrap_or_default()
205 ))) {
206 Ok(id) => vec![id],
207 Err(e) => return fail(format!("the hello reply has no valid kind and name: {e}")),
208 },
209 },
210 Err(e) => return fail(format!("the handshake failed: {e}")),
211 };
212 let mut out = Vec::new();
213 for case in cases.iter().filter(|c| c.scope.as_deref() == Some("package")) {
214 let vars = Vars::new(None, options);
215 out.push(finish(&package, case, run_case(path, None, case, &vars, options)));
216 }
217 for id in &provides {
218 out.extend(run_plugin(path, id, true, &cases, options));
219 }
220 out
221}
222
223fn run_plugin(path: &Path, id: &PluginId, ask: bool, cases: &[Case], options: &Options) -> Vec<Check> {
225 let asked = ask.then_some(id);
226 let capabilities: Vec<String> = match greet(path, asked, options) {
227 Ok(h) => match h.get("capabilities").cloned().map(serde_json::from_value) {
228 Some(Ok(c)) => c,
229 None => Vec::new(),
230 Some(Err(e)) => {
231 let why = format!("`capabilities` isn't a list of names: {e}");
232 return vec![Check::ran(
233 &id.to_string(),
234 "the plugin starts and answers hello",
235 Err(why),
236 )];
237 }
238 },
239 Err(e) => {
241 return vec![Check::ran(
242 &id.to_string(),
243 "the plugin starts and answers hello",
244 Err(e),
245 )];
246 }
247 };
248 let mut out = Vec::new();
249 for case in cases.iter().filter(|c| c.scope.is_none()) {
250 if let Some(reason) = not_applicable(case, id.kind, &capabilities, options) {
251 out.push(Check::skipped(&id.to_string(), &case.name, reason));
252 continue;
253 }
254 let vars = Vars::new(Some(id), options);
255 out.push(finish(
256 &id.to_string(),
257 case,
258 run_case(path, asked, case, &vars, options),
259 ));
260 }
261 out
262}
263
264fn finish(plugin: &str, case: &Case, r: Result<(), String>) -> Check {
265 Check::ran(plugin, &case.name, r)
266}
267
268fn not_applicable(case: &Case, kind: Kind, capabilities: &[String], options: &Options) -> Option<String> {
270 if !case.kinds.is_empty() && !case.kinds.contains(&kind) {
271 return Some(format!("for {:?} plugins", case.kinds));
272 }
273 if let Some(c) = case.requires.iter().find(|c| !capabilities.contains(c)) {
274 return Some(format!("the plugin doesn't advertise `{c}`"));
275 }
276 if case.session.is_empty() {
277 return None;
278 }
279 let Some(session) = &options.session else {
280 return Some("needs a session (conformance::Options::session)".into());
281 };
282 case.session
283 .iter()
284 .find(|s| !session.statements.contains_key(*s))
285 .map(|s| format!("the session has no `{s}` statement"))
286}
287
288fn greet(path: &Path, asked: Option<&PluginId>, options: &Options) -> Result<Map<String, Value>, String> {
290 let mut d = Driver::spawn(path, options)?;
291 d.write_json(&hello(asked, &Map::new()))?;
292 let reply = d.reply()?;
293 if reply["type"] != "hello" {
294 return Err(format!("expected a hello reply, got {reply}"));
295 }
296 Ok(reply.as_object().cloned().unwrap_or_default())
297}
298
299fn hello(asked: Option<&PluginId>, with: &Map<String, Value>) -> Value {
301 let mut h = json!({
302 "type": "hello",
303 "min_version": MIN_VERSION,
304 "max_version": MAX_VERSION,
305 "core_version": "conformance",
306 });
307 if let Some(id) = asked {
308 h["plugin"] = json!(id.to_string());
309 }
310 for (k, v) in with {
311 h[k] = v.clone();
312 }
313 h
314}
315
316struct Vars {
318 values: BTreeMap<String, Value>,
319 tmp: PathBuf,
320}
321
322impl Vars {
323 fn new(id: Option<&PluginId>, options: &Options) -> Vars {
324 static NEXT: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
325 let n = NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
326 let tmp = std::env::temp_dir().join(format!("dre-conformance-{}-{n}", std::process::id()));
327 let mut values = BTreeMap::new();
328 if let Some(id) = id {
329 values.insert("kind".into(), json!(id.kind.as_str()));
330 values.insert("name".into(), json!(id.name));
331 values.insert("plugin".into(), json!(id.to_string()));
332 }
333 values.insert("tmp".into(), json!(tmp.to_string_lossy()));
334 values.insert("min_version".into(), json!(MIN_VERSION));
335 values.insert("max_version".into(), json!(MAX_VERSION));
336 values.insert("far_version".into(), json!(MAX_VERSION + 1000));
337 values.insert("capabilities".into(), json!(CAPABILITIES));
338 values.insert("error_kinds".into(), json!(ERROR_KINDS));
339 let session = options.session.clone().unwrap_or_default();
340 values.insert("connection".into(), Value::Object(session.connection));
341 for (role, sql) in session.statements {
342 values.insert(format!("sql.{role}"), json!(sql));
343 }
344 Vars { values, tmp }
345 }
346}
347
348fn substitute(v: &Value, vars: &Vars) -> Result<Value, String> {
351 Ok(match v {
352 Value::String(s) => {
353 if let Some(name) = s.strip_prefix("${").and_then(|r| r.strip_suffix('}'))
354 && !name.contains('}')
355 {
356 return vars
357 .values
358 .get(name)
359 .cloned()
360 .ok_or_else(|| format!("unknown placeholder `{s}`"));
361 }
362 let mut out = String::new();
363 let mut rest = s.as_str();
364 while let Some(start) = rest.find("${") {
365 let end = rest[start..]
366 .find('}')
367 .ok_or_else(|| format!("unclosed placeholder in `{s}`"))?;
368 let name = &rest[start + 2..start + end];
369 let value = vars
370 .values
371 .get(name)
372 .ok_or_else(|| format!("unknown placeholder `${{{name}}}`"))?;
373 out.push_str(&rest[..start]);
374 match value {
375 Value::String(t) => out.push_str(t),
376 other => out.push_str(&other.to_string()),
377 }
378 rest = &rest[start + end + 1..];
379 }
380 out.push_str(rest);
381 Value::String(out)
382 }
383 Value::Array(a) => Value::Array(a.iter().map(|x| substitute(x, vars)).collect::<Result<_, _>>()?),
384 Value::Object(o) => Value::Object(
385 o.iter()
386 .map(|(k, x)| Ok((k.clone(), substitute(x, vars)?)))
387 .collect::<Result<_, String>>()?,
388 ),
389 other => other.clone(),
390 })
391}
392
393pub(crate) fn matches(value: Option<&Value>, m: &Value) -> Result<(), String> {
396 let show = || value.map_or_else(|| "nothing".to_string(), |v| v.to_string());
397 match m {
398 Value::Object(ops) if ops.keys().any(|k| k.starts_with('$')) => {
399 for (op, arg) in ops {
400 operator(value, op, arg).map_err(|e| format!("{e} (got {})", show()))?;
401 }
402 Ok(())
403 }
404 Value::Object(fields) => {
405 let Some(Value::Object(o)) = value else {
406 return Err(format!("expected an object, got {}", show()));
407 };
408 for (k, sub) in fields {
409 matches(o.get(k), sub).map_err(|e| format!("`{k}`: {e}"))?;
410 }
411 Ok(())
412 }
413 Value::Array(items) => {
414 let Some(Value::Array(a)) = value else {
415 return Err(format!("expected an array, got {}", show()));
416 };
417 if a.len() != items.len() {
418 return Err(format!("expected {} items, got {}", items.len(), show()));
419 }
420 for (i, (x, sub)) in a.iter().zip(items).enumerate() {
421 matches(Some(x), sub).map_err(|e| format!("[{i}]: {e}"))?;
422 }
423 Ok(())
424 }
425 scalar => match value {
426 Some(v) if same(v, scalar) => Ok(()),
427 _ => Err(format!("expected {scalar}, got {}", show())),
428 },
429 }
430}
431
432fn same(a: &Value, b: &Value) -> bool {
434 match (a.as_f64(), b.as_f64()) {
435 (Some(x), Some(y)) => x == y,
436 _ => a == b,
437 }
438}
439
440fn operator(value: Option<&Value>, op: &str, arg: &Value) -> Result<(), String> {
441 let present = value.filter(|v| !v.is_null());
442 let number = |what: &str| -> Result<(f64, f64), String> {
443 let v = present.and_then(Value::as_f64).ok_or("expected a number")?;
444 let a = arg.as_f64().ok_or_else(|| format!("`{what}` takes a number"))?;
445 Ok((v, a))
446 };
447 match op {
448 "$absent" => match (present, arg.as_bool()) {
449 (None, Some(true)) | (Some(_), Some(false)) => Ok(()),
450 _ => Err(format!(
451 "expected {}",
452 if arg == &json!(true) {
453 "no value"
454 } else {
455 "a value"
456 }
457 )),
458 },
459 "$any" => value.map(|_| ()).ok_or_else(|| "expected a value".into()),
460 "$one_of" => {
461 let alts = arg.as_array().ok_or("`$one_of` takes a list")?;
462 let mut why = Vec::new();
463 for alt in alts {
464 match matches(value, alt) {
465 Ok(()) => return Ok(()),
466 Err(e) => why.push(e),
467 }
468 }
469 Err(format!("matches none of: {}", why.join("; ")))
470 }
471 "$type" => {
472 let want = arg.as_str().ok_or("`$type` takes a name")?;
473 let ok = match (want, present) {
474 ("string", Some(v)) => v.is_string(),
475 ("number", Some(v)) => v.is_number(),
476 ("integer", Some(v)) => v.is_u64() || v.is_i64(),
477 ("boolean", Some(v)) => v.is_boolean(),
478 ("array", Some(v)) => v.is_array(),
479 ("object", Some(v)) => v.is_object(),
480 _ => false,
481 };
482 if ok {
483 Ok(())
484 } else {
485 Err(format!("expected a {want}"))
486 }
487 }
488 "$nonempty" => match present {
489 Some(Value::String(s)) if !s.is_empty() => Ok(()),
490 Some(Value::Array(a)) if !a.is_empty() => Ok(()),
491 _ => Err("expected a non-empty string or list".into()),
492 },
493 "$len" => {
494 let n = match present {
495 Some(Value::String(s)) => s.chars().count(),
496 Some(Value::Array(a)) => a.len(),
497 _ => return Err("expected a string or list".into()),
498 };
499 match arg.as_u64() {
500 Some(want) if want as usize == n => Ok(()),
501 _ => Err(format!("expected length {arg}, got {n}")),
502 }
503 }
504 "$contains" => match present {
505 Some(Value::String(s)) => match arg.as_str() {
506 Some(sub) if s.contains(sub) => Ok(()),
507 _ => Err(format!("expected text containing {arg}")),
508 },
509 Some(Value::Array(a)) => {
510 if a.iter().any(|x| matches(Some(x), arg).is_ok()) {
511 Ok(())
512 } else {
513 Err(format!("expected an item matching {arg}"))
514 }
515 }
516 _ => Err("expected a string or list".into()),
517 },
518 "$every" => match present {
519 Some(Value::Array(a)) => {
520 for (i, x) in a.iter().enumerate() {
521 matches(Some(x), arg).map_err(|e| format!("[{i}]: {e}"))?;
522 }
523 Ok(())
524 }
525 _ => Err("expected a list".into()),
526 },
527 "$in" => {
528 let set = arg.as_array().ok_or("`$in` takes a list")?;
529 match present {
530 Some(v) if set.iter().any(|x| same(x, v)) => Ok(()),
531 _ => Err(format!("expected one of {arg}")),
532 }
533 }
534 "$min" => {
535 let (v, a) = number("$min")?;
536 if v >= a {
537 Ok(())
538 } else {
539 Err(format!("expected at least {a}"))
540 }
541 }
542 "$lt" => {
543 let (v, a) = number("$lt")?;
544 if v < a {
545 Ok(())
546 } else {
547 Err(format!("expected less than {a}"))
548 }
549 }
550 "$file_exists" => match present.and_then(Value::as_str) {
551 Some(p) if Path::new(p).is_file() == (arg == &json!(true)) => Ok(()),
552 _ => Err(format!(
553 "expected a path that {} a file",
554 if arg == &json!(true) { "is" } else { "isn't" }
555 )),
556 },
557 other => Err(format!("unknown matcher `{other}`")),
558 }
559}
560
561struct Driver {
563 p: PluginProcess,
564 last: Value,
566}
567
568impl Driver {
569 fn spawn(path: &Path, options: &Options) -> Result<Driver, String> {
570 let env: Vec<(&str, &str)> = options
571 .env
572 .iter()
573 .map(|(k, v)| (k.as_str(), v.as_str()))
574 .collect();
575 let quiet: LogSink = Arc::new(|_, _| {});
576 let p = PluginProcess::spawn_env(path, quiet, &env).map_err(|e| e.to_string())?;
577 Ok(Driver { p, last: Value::Null })
578 }
579
580 fn write_json(&mut self, v: &Value) -> Result<(), String> {
581 let mut bytes = Vec::new();
582 frame::write_json(&mut bytes, v).map_err(|e| e.to_string())?;
583 self.p.write_raw(&bytes).map_err(|e| e.to_string())
584 }
585
586 fn reply(&mut self) -> Result<Value, String> {
588 loop {
589 match self.p.recv_frame(TIMEOUT).map_err(|e| e.to_string())? {
590 Frame::Arrow(_) => return Err("Arrow data where a reply was expected".into()),
591 Frame::Json(v) => {
592 conforms(&v)?;
593 if !is_chatter(&v) {
594 self.last = v.clone();
595 return Ok(v);
596 }
597 }
598 }
599 }
600 }
601}
602
603fn is_chatter(v: &Value) -> bool {
605 v["type"] == "log" || v["type"] == "progress"
606}
607
608fn conforms(v: &Value) -> Result<(), String> {
610 serde_json::from_value::<Envelope<Response>>(v.clone())
611 .map(|_| ())
612 .map_err(|e| format!("{v} doesn't match the message schema: {e}"))
613}
614
615fn run_case(
616 path: &Path,
617 asked: Option<&PluginId>,
618 case: &Case,
619 vars: &Vars,
620 options: &Options,
621) -> Result<(), String> {
622 std::fs::create_dir_all(&vars.tmp).map_err(|e| e.to_string())?;
623 let r = run_steps(path, asked, case, vars, options);
624 let _ = std::fs::remove_dir_all(&vars.tmp);
625 r
626}
627
628fn run_steps(
629 path: &Path,
630 asked: Option<&PluginId>,
631 case: &Case,
632 vars: &Vars,
633 options: &Options,
634) -> Result<(), String> {
635 let mut d = Driver::spawn(path, options)?;
636 if !case.steps.first().is_some_and(|s| s.contains_key("hello")) {
637 d.write_json(&hello(asked, &Map::new()))?;
638 let r = d.reply().map_err(|e| format!("handshake: {e}"))?;
639 if r["type"] != "hello" {
640 return Err(format!("handshake: expected a hello reply, got {r}"));
641 }
642 }
643 for (i, step) in case.steps.iter().enumerate() {
644 let Value::Object(step) = substitute(&Value::Object(step.clone()), vars)? else {
645 unreachable!("substitute keeps an object an object");
646 };
647 run_step(&mut d, asked, &step).map_err(|e| format!("step {}: {e}", i + 1))?;
648 }
649 Ok(())
650}
651
652fn run_step(d: &mut Driver, asked: Option<&PluginId>, step: &Map<String, Value>) -> Result<(), String> {
653 if let Some(h) = step.get("hello") {
654 d.write_json(&hello(asked, h.as_object().unwrap_or(&Map::new())))?;
655 } else if let Some(v) = step.get("send") {
656 d.write_json(v)?;
657 } else if let Some(hex) = step.get("send_raw") {
658 let bytes = decode_hex(hex.as_str().unwrap_or_default())?;
659 let _ = d.p.write_raw(&bytes);
661 } else if let Some(spec) = step.get("send_arrow") {
662 let ipc = arrow_frame(spec)?;
663 let mut bytes = Vec::new();
664 frame::write_arrow(&mut bytes, &ipc).map_err(|e| e.to_string())?;
665 d.p.write_raw(&bytes).map_err(|e| e.to_string())?;
666 } else if step.contains_key("close_input") {
667 d.p.close_input();
668 } else if let Some(spec) = step.get("result_set") {
669 return result_set(d, spec);
670 } else if let Some(m) = step.get("exit") {
671 let status =
672 d.p.wait_exit(TIMEOUT)
673 .ok_or("still running after it should have exited")?;
674 let code = status.code().map_or(Value::Null, Value::from);
675 return matches(Some(&code), m).map_err(|e| format!("exit code: {e}"));
676 } else {
677 return Err(format!("unknown step {}", Value::Object(step.clone())));
678 }
679 let Some(m) = step.get("expect") else {
680 return Ok(());
681 };
682 let or_exit = step.get("or_exit") == Some(&json!(true));
683 match d.reply() {
684 Ok(v) => matches(Some(&v), m).map_err(|e| format!("reply {v}: {e}")),
685 Err(_) if or_exit && d.p.wait_exit(Duration::from_secs(5)).is_some() => Ok(()),
686 Err(e) => Err(e),
687 }
688}
689
690fn result_set(d: &mut Driver, spec: &Value) -> Result<(), String> {
693 let columns = d.last["columns"].clone();
694 let mut frames = 0;
695 let mut rows: u64 = 0;
696 loop {
697 match d.p.recv_frame(TIMEOUT).map_err(|e| e.to_string())? {
698 Frame::Arrow(ipc) => {
699 let (schema, batches) = frame::decode_batches(&ipc).map_err(|e| e.to_string())?;
700 let names: Vec<&str> = schema.fields().iter().map(|f| f.name().as_str()).collect();
701 if json!(names) != columns {
702 return Err(format!(
703 "the Arrow schema has columns {names:?}, the result reply {columns}"
704 ));
705 }
706 frames += 1;
707 rows += batches.iter().map(|b| b.num_rows() as u64).sum::<u64>();
708 }
709 Frame::Json(v) => {
710 conforms(&v)?;
711 if is_chatter(&v) {
712 continue;
713 }
714 if frames == 0 {
715 return Err(format!("no Arrow frame (carrying the schema) before {v}"));
716 }
717 matches(Some(&v), &json!({"type": "result_end", "id": spec["id"]}))?;
718 if v["rows"].as_u64() != Some(rows) {
719 return Err(format!("`result_end` says {} rows, {rows} were sent", v["rows"]));
720 }
721 return matches(Some(&json!(rows)), &spec["rows"]).map_err(|e| format!("rows: {e}"));
722 }
723 }
724 }
725}
726
727fn arrow_frame(spec: &Value) -> Result<Vec<u8>, String> {
729 use arrow::datatypes::{DataType, Field, Schema};
730 let mut fields = Vec::new();
731 for c in spec["columns"].as_array().ok_or("`send_arrow` needs `columns`")? {
732 let t = match c["type"].as_str() {
733 Some("int64") => DataType::Int64,
734 Some("float64") => DataType::Float64,
735 Some("utf8") => DataType::Utf8,
736 Some("boolean") => DataType::Boolean,
737 other => return Err(format!("unknown column type {other:?}")),
738 };
739 fields.push(Field::new(c["name"].as_str().unwrap_or_default(), t, true));
740 }
741 let schema = Arc::new(Schema::new(fields));
742 let rows = spec["rows"].as_u64().unwrap_or(0) as usize;
743 let mut columns: Vec<arrow::array::ArrayRef> = Vec::new();
744 for (f, c) in schema
745 .fields()
746 .iter()
747 .zip(spec["columns"].as_array().into_iter().flatten())
748 {
749 columns.push(match c["values"].as_array() {
750 None => arrow::array::new_null_array(f.data_type(), rows),
751 Some(v) if v.len() != rows => {
752 return Err(format!("`{}` has {} values for {rows} rows", f.name(), v.len()));
753 }
754 Some(v) => match f.data_type() {
755 DataType::Int64 => Arc::new(arrow::array::Int64Array::from_iter(v.iter().map(Value::as_i64))),
756 DataType::Float64 => {
757 Arc::new(arrow::array::Float64Array::from_iter(v.iter().map(Value::as_f64)))
758 }
759 DataType::Boolean => Arc::new(arrow::array::BooleanArray::from_iter(
760 v.iter().map(Value::as_bool),
761 )),
762 _ => Arc::new(arrow::array::StringArray::from_iter(v.iter().map(Value::as_str))),
763 },
764 });
765 }
766 let batch = arrow::array::RecordBatch::try_new(schema, columns).map_err(|e| e.to_string())?;
767 frame::encode_batch(&batch).map_err(|e| e.to_string())
768}
769
770fn decode_hex(s: &str) -> Result<Vec<u8>, String> {
771 (0..s.len())
772 .step_by(2)
773 .map(|i| u8::from_str_radix(s.get(i..i + 2).unwrap_or("?"), 16).map_err(|e| e.to_string()))
774 .collect()
775}
776
777pub fn assert_conforms(path: &Path) {
783 assert_conforms_with(path, &Options::default());
784}
785
786pub fn assert_conforms_with(path: &Path, options: &Options) {
792 let checks = run_with(path, options);
793 let failed: Vec<String> = checks
794 .iter()
795 .filter(|c| !c.passed)
796 .map(|c| format!(" ✗ {}: {}: {}", c.plugin, c.name, c.detail))
797 .collect();
798 assert!(
799 failed.is_empty(),
800 "{} fails protocol conformance:\n{}",
801 path.display(),
802 failed.join("\n")
803 );
804}
805
806#[cfg(test)]
807mod tests {
808 use super::*;
809
810 #[test]
811 fn the_published_cases_parse_and_name_known_capabilities() {
812 let cases = cases();
813 assert!(cases.len() > 20);
814 for c in &cases {
815 assert!(
816 matches!(c.scope.as_deref(), None | Some("package")),
817 "{}: scope",
818 c.name
819 );
820 for r in &c.requires {
821 assert!(CAPABILITIES.contains(&r.as_str()), "{}: {r}", c.name);
822 }
823 }
824 }
825
826 #[test]
827 fn matchers_check_properties_not_whole_messages() {
828 let reply = json!({"type": "error", "id": 4, "message": "no", "kind": "query", "extra": 1});
829 let ok = |m: Value| matches(Some(&reply), &m);
830 assert!(ok(json!({"type": "error", "id": 4})).is_ok());
831 assert!(ok(json!({"code": {"$absent": true}})).is_ok());
832 assert!(ok(json!({"kind": {"$in": ["query", "auth"]}})).is_ok());
833 assert!(ok(json!({"kind": {"$in": ["auth"]}})).is_err());
834 assert!(ok(json!({"message": {"$len": 2}, "id": {"$min": 4, "$lt": 5}})).is_ok());
835 assert!(ok(json!({"$one_of": [{"type": "ok"}, {"type": "error"}]})).is_ok());
836 assert!(ok(json!({"type": "ok"})).is_err());
837 let list = json!(["a/b", "c"]);
838 assert!(matches(Some(&list), &json!({"$contains": {"$contains": "/"}})).is_ok());
839 assert!(matches(Some(&list), &json!({"$every": {"$contains": "/"}})).is_err());
840 }
841
842 #[test]
843 fn placeholders_take_their_values_or_splice_as_text() {
844 let session = Session::new(Map::new()).statement("rows", "rows 3");
845 let vars = Vars::new(
846 Some(&PluginId::new(Kind::Format, "csv")),
847 &Options::default().session(session),
848 );
849 let v = substitute(
850 &json!({"v": "${max_version}", "p": "${tmp}/x.${name}", "sql": "${sql.rows}"}),
851 &vars,
852 )
853 .unwrap();
854 assert_eq!(v["v"], json!(MAX_VERSION));
855 assert!(v["p"].as_str().unwrap().ends_with("/x.csv"));
856 assert_eq!(v["sql"], "rows 3");
857 assert!(substitute(&json!("${nope}"), &vars).is_err());
858 }
859}