Testing - add tests for the CLI. (#2022)
This commit is contained in:
parent
1de753c3ee
commit
eeb23cc706
4 changed files with 235 additions and 0 deletions
22
Cargo.lock
generated
22
Cargo.lock
generated
|
@ -3416,6 +3416,18 @@ dependencies = [
|
|||
"num_cpus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rcgen"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffbe84efe2f38dea12e9bfc1f65377fdf03e53a18cb3b995faedf7934c7e785b"
|
||||
dependencies = [
|
||||
"pem",
|
||||
"ring",
|
||||
"time 0.3.20",
|
||||
"yasna",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.16"
|
||||
|
@ -4115,6 +4127,7 @@ dependencies = [
|
|||
"opentelemetry-otlp",
|
||||
"opentelemetry-proto",
|
||||
"rand 0.8.5",
|
||||
"rcgen",
|
||||
"reqwest",
|
||||
"rmp-serde",
|
||||
"rustyline",
|
||||
|
@ -5391,6 +5404,15 @@ version = "0.8.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
|
||||
|
||||
[[package]]
|
||||
name = "yasna"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
|
||||
dependencies = [
|
||||
"time 0.3.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.6.0"
|
||||
|
|
|
@ -62,6 +62,7 @@ urlencoding = "2.1.2"
|
|||
warp = { version = "0.3.4", features = ["compression", "tls", "websocket"] }
|
||||
|
||||
[dev-dependencies]
|
||||
rcgen = "0.10.0"
|
||||
tonic = "0.8.3"
|
||||
opentelemetry-proto = {version = "0.1.0", features = ["gen-tonic", "traces", "build-server"] }
|
||||
tokio-stream = { version = "0.1", features = ["net"] }
|
||||
|
|
3
tests/README.md
Normal file
3
tests/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
This folder contains integration tests for the `surreal` CLI binary.
|
||||
|
||||
See [`lib/tests`](../lib/tests/) for `surrealdb` library integration tests.
|
209
tests/cli.rs
Normal file
209
tests/cli.rs
Normal file
|
@ -0,0 +1,209 @@
|
|||
mod cli_integration {
|
||||
// cargo test --package surreal --bin surreal --no-default-features --features storage-mem --test cli -- cli_integration --nocapture
|
||||
|
||||
use rand::{thread_rng, Rng};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
/// Child is a (maybe running) CLI process. It can be killed by dropping it
|
||||
struct Child {
|
||||
inner: Option<std::process::Child>,
|
||||
}
|
||||
|
||||
impl Child {
|
||||
/// Send some thing to the child's stdin
|
||||
fn input(mut self, input: &str) -> Self {
|
||||
let stdin = self.inner.as_mut().unwrap().stdin.as_mut().unwrap();
|
||||
use std::io::Write;
|
||||
stdin.write_all(input.as_bytes()).unwrap();
|
||||
self
|
||||
}
|
||||
|
||||
fn kill(mut self) -> Self {
|
||||
self.inner.as_mut().unwrap().kill().unwrap();
|
||||
self
|
||||
}
|
||||
|
||||
/// Read the child's stdout concatenated with its stderr. Returns Ok if the child
|
||||
/// returns successfully, Err otherwise.
|
||||
fn output(mut self) -> Result<String, String> {
|
||||
let output = self.inner.take().unwrap().wait_with_output().unwrap();
|
||||
|
||||
let mut buf = String::from_utf8(output.stdout).unwrap();
|
||||
buf.push_str(&String::from_utf8(output.stderr).unwrap());
|
||||
|
||||
if output.status.success() {
|
||||
Ok(buf)
|
||||
} else {
|
||||
Err(buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Child {
|
||||
fn drop(&mut self) {
|
||||
if let Some(inner) = self.inner.as_mut() {
|
||||
let _ = inner.kill();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the CLI with the given args
|
||||
fn run(args: &str) -> Child {
|
||||
let mut path = std::env::current_exe().unwrap();
|
||||
assert!(path.pop());
|
||||
if path.ends_with("deps") {
|
||||
assert!(path.pop());
|
||||
}
|
||||
|
||||
// Note: Cargo automatically builds this binary for integration tests.
|
||||
path.push(format!("{}{}", env!("CARGO_PKG_NAME"), std::env::consts::EXE_SUFFIX));
|
||||
|
||||
let mut cmd = Command::new(path);
|
||||
cmd.stdin(Stdio::piped());
|
||||
cmd.stdout(Stdio::piped());
|
||||
cmd.stderr(Stdio::piped());
|
||||
cmd.args(args.split_ascii_whitespace());
|
||||
Child {
|
||||
inner: Some(cmd.spawn().unwrap()),
|
||||
}
|
||||
}
|
||||
|
||||
fn tmp_file(name: &str) -> String {
|
||||
let path = Path::new(env!("OUT_DIR")).join(name);
|
||||
path.to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version() {
|
||||
assert!(run("version").output().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help() {
|
||||
assert!(run("help").output().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nonexistent_subcommand() {
|
||||
assert!(run("nonexistent").output().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nonexistent_option() {
|
||||
assert!(run("version --turbo").output().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start() {
|
||||
let mut rng = thread_rng();
|
||||
|
||||
let port: u16 = rng.gen_range(13000..14000);
|
||||
let addr = format!("127.0.0.1:{port}");
|
||||
|
||||
let pass = rng.gen::<u64>().to_string();
|
||||
|
||||
let start_args =
|
||||
format!("start --bind {addr} --user root --pass {pass} memory --no-banner --log info");
|
||||
|
||||
println!("starting server with args: {start_args}");
|
||||
|
||||
let _server = run(&start_args);
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
|
||||
assert!(run(&format!("isready --conn http://{addr}")).output().is_ok());
|
||||
|
||||
// Create a record
|
||||
{
|
||||
let args =
|
||||
format!("sql --conn http://{addr} --user root --pass {pass} --ns N --db D --multi");
|
||||
assert_eq!(
|
||||
run(&args).input("CREATE thing:one;\n").output(),
|
||||
Ok("[{ id: thing:one }]\n\n".to_owned()),
|
||||
"failed to send sql: {args}"
|
||||
);
|
||||
}
|
||||
|
||||
// Export to stdout
|
||||
{
|
||||
let args =
|
||||
format!("export --conn http://{addr} --user root --pass {pass} --ns N --db D -");
|
||||
let output = run(&args).output().expect("failed to run stdout export: {args}");
|
||||
assert!(output.contains("DEFINE TABLE thing SCHEMALESS PERMISSIONS NONE;"));
|
||||
assert!(output.contains("UPDATE thing:one CONTENT { id: thing:one };"));
|
||||
}
|
||||
|
||||
// Export to file
|
||||
let exported = {
|
||||
let exported = tmp_file("exported.surql");
|
||||
let args = format!(
|
||||
"export --conn http://{addr} --user root --pass {pass} --ns N --db D {exported}"
|
||||
);
|
||||
run(&args).output().expect("failed to run file export: {args}");
|
||||
exported
|
||||
};
|
||||
|
||||
// Import the exported file
|
||||
{
|
||||
let args = format!(
|
||||
"import --conn http://{addr} --user root --pass {pass} --ns N --db D2 {exported}"
|
||||
);
|
||||
run(&args).output().expect("failed to run import: {args}");
|
||||
}
|
||||
|
||||
// Query from the import (pretty-printed this time)
|
||||
{
|
||||
let args = format!(
|
||||
"sql --conn http://{addr} --user root --pass {pass} --ns N --db D2 --pretty"
|
||||
);
|
||||
assert_eq!(
|
||||
run(&args).input("SELECT * FROM thing;\n").output(),
|
||||
Ok("[\n\t{\n\t\tid: thing:one\n\t}\n]\n\n".to_owned()),
|
||||
"failed to send sql: {args}"
|
||||
);
|
||||
}
|
||||
|
||||
// Unfinished backup CLI
|
||||
{
|
||||
let file = tmp_file("backup.db");
|
||||
let args = format!("backup --user root --pass {pass} http://{addr} {file}");
|
||||
run(&args).output().expect("failed to run backup: {args}");
|
||||
|
||||
// TODO: Once backups are functional, update this test.
|
||||
assert_eq!(fs::read_to_string(file).unwrap(), "Save");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_tls() {
|
||||
let mut rng = thread_rng();
|
||||
|
||||
let port: u16 = rng.gen_range(13000..14000);
|
||||
let addr = format!("127.0.0.1:{port}");
|
||||
|
||||
let pass = rng.gen::<u128>().to_string();
|
||||
|
||||
// Test the crt/key args but the keys are self signed so don't actually connect.
|
||||
let crt_path = tmp_file("crt.crt");
|
||||
let key_path = tmp_file("key.pem");
|
||||
|
||||
let cert = rcgen::generate_simple_self_signed(Vec::new()).unwrap();
|
||||
fs::write(&crt_path, cert.serialize_pem().unwrap()).unwrap();
|
||||
fs::write(&key_path, cert.serialize_private_key_pem().into_bytes()).unwrap();
|
||||
|
||||
let start_args = format!(
|
||||
"start --bind {addr} --user root --pass {pass} memory --log info --web-crt {crt_path} --web-key {key_path}"
|
||||
);
|
||||
|
||||
println!("starting server with args: {start_args}");
|
||||
|
||||
let server = run(&start_args);
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
|
||||
let output = server.kill().output().unwrap_err();
|
||||
assert!(output.contains("Started web server"));
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue