influxdb/influxdb3/tests/server/main.rs

255 lines
7.4 KiB
Rust

use std::{
net::{SocketAddr, SocketAddrV4, TcpListener},
process::{Child, Command, Stdio},
time::Duration,
};
use arrow::record_batch::RecordBatch;
use arrow_flight::{decode::FlightRecordBatchStream, FlightClient};
use assert_cmd::cargo::CommandCargoExt;
use futures::TryStreamExt;
use influxdb3_client::Precision;
use influxdb_iox_client::flightsql::FlightSqlClient;
use reqwest::Response;
mod auth;
mod flight;
mod limits;
mod ping;
mod query;
mod system_tables;
mod write;
/// Configuration for a [`TestServer`]
#[derive(Debug, Default)]
pub struct TestConfig {
auth_token: Option<(String, String)>,
}
impl TestConfig {
/// Set the auth token for this [`TestServer`]
pub fn auth_token<S: Into<String>, R: Into<String>>(
mut self,
hashed_token: S,
raw_token: R,
) -> Self {
self.auth_token = Some((hashed_token.into(), raw_token.into()));
self
}
/// Spawn a new [`TestServer`] with this configuration
///
/// This will run the `influxdb3 serve` command, and bind its HTTP
/// address to a random port on localhost.
pub async fn spawn(self) -> TestServer {
TestServer::spawn_inner(self).await
}
fn as_args(&self) -> Vec<&str> {
let mut args = vec![];
if let Some((token, _)) = &self.auth_token {
args.append(&mut vec!["--bearer-token", token]);
}
args
}
}
/// A running instance of the `influxdb3 serve` process
///
/// Logs will be emitted to stdout/stderr if the TEST_LOG environment
/// variable is set, e.g.,
/// ```
/// TEST_LOG= cargo test
/// ```
pub struct TestServer {
config: TestConfig,
bind_addr: SocketAddr,
server_process: Child,
http_client: reqwest::Client,
}
impl TestServer {
/// Spawn a new [`TestServer`]
///
/// This will run the `influxdb3 serve` command, and bind its HTTP
/// address to a random port on localhost.
pub async fn spawn() -> Self {
Self::spawn_inner(Default::default()).await
}
/// Configure a [`TestServer`] before spawning
pub fn configure() -> TestConfig {
TestConfig::default()
}
async fn spawn_inner(config: TestConfig) -> Self {
let bind_addr = get_local_bind_addr();
let mut command = Command::cargo_bin("influxdb3").expect("create the influxdb3 command");
let mut command = command
.arg("serve")
.args(["--http-bind", &bind_addr.to_string()])
.args(["--object-store", "memory"])
.args(config.as_args());
// If TEST_LOG env var is not defined, discard stdout/stderr
if std::env::var("TEST_LOG").is_err() {
command = command.stdout(Stdio::null()).stderr(Stdio::null());
}
let server_process = command.spawn().expect("spawn the influxdb3 server process");
let server = Self {
config,
bind_addr,
server_process,
http_client: reqwest::Client::new(),
};
server.wait_until_ready().await;
server
}
/// Get the URL of the running service for use with an HTTP client
pub fn client_addr(&self) -> String {
format!("http://{addr}", addr = self.bind_addr)
}
/// Get a [`FlightSqlClient`] for making requests to the running service over gRPC
pub async fn flight_sql_client(&self, database: &str) -> FlightSqlClient {
let channel = tonic::transport::Channel::from_shared(self.client_addr())
.expect("create tonic channel")
.connect()
.await
.expect("connect to gRPC client");
let mut client = FlightSqlClient::new(channel);
client.add_header("database", database).unwrap();
client
}
/// Get a raw [`FlightClient`] for performing Flight actions directly
pub async fn flight_client(&self) -> FlightClient {
let channel = tonic::transport::Channel::from_shared(self.client_addr())
.expect("create tonic channel")
.connect()
.await
.expect("connect to gRPC client");
FlightClient::new(channel)
}
fn kill(&mut self) {
self.server_process.kill().expect("kill the server process");
}
async fn wait_until_ready(&self) {
while self
.http_client
.get(format!("{base}/health", base = self.client_addr()))
.send()
.await
.is_err()
{
tokio::time::sleep(Duration::from_millis(10)).await;
}
}
}
impl Drop for TestServer {
fn drop(&mut self) {
self.kill();
}
}
impl TestServer {
/// Write some line protocol to the server
pub async fn write_lp_to_db(
&self,
database: &str,
lp: impl ToString,
precision: Precision,
) -> Result<(), influxdb3_client::Error> {
let mut client = influxdb3_client::Client::new(self.client_addr()).unwrap();
if let Some((_, token)) = &self.config.auth_token {
client = client.with_auth_token(token);
}
client
.api_v3_write_lp(database)
.body(lp.to_string())
.precision(precision)
.send()
.await
}
pub async fn api_v3_query_sql(&self, params: &[(&str, &str)]) -> Response {
self.http_client
.get(format!(
"{base}/api/v3/query_sql",
base = self.client_addr()
))
.query(params)
.send()
.await
.expect("send /api/v3/query_sql request to server")
}
pub async fn api_v3_query_influxql(&self, params: &[(&str, &str)]) -> Response {
self.http_client
.get(format!(
"{base}/api/v3/query_influxql",
base = self.client_addr()
))
.query(params)
.send()
.await
.expect("send /api/v3/query_influxql request to server")
}
pub async fn api_v1_query(&self, params: &[(&str, &str)]) -> Response {
self.http_client
.get(format!("{base}/query", base = self.client_addr(),))
.query(params)
.send()
.await
.expect("send /query request to server")
}
}
/// Get an available bind address on localhost
///
/// This binds a [`TcpListener`] to 127.0.0.1:0, which will randomly
/// select an available port, and produces the resulting local address.
/// The [`TcpListener`] is dropped at the end of the function, thus
/// freeing the port for use by the caller.
fn get_local_bind_addr() -> SocketAddr {
let ip = std::net::Ipv4Addr::new(127, 0, 0, 1);
let port = 0;
let addr = SocketAddrV4::new(ip, port);
TcpListener::bind(addr)
.expect("bind to a socket address")
.local_addr()
.expect("get local address")
}
/// Write to the server with the line protocol
pub async fn write_lp_to_db(
server: &TestServer,
database: &str,
lp: &str,
precision: Precision,
) -> Result<(), influxdb3_client::Error> {
let client = influxdb3_client::Client::new(server.client_addr()).unwrap();
client
.api_v3_write_lp(database)
.body(lp.to_string())
.precision(precision)
.send()
.await
}
#[allow(dead_code)]
pub async fn collect_stream(stream: FlightRecordBatchStream) -> Vec<RecordBatch> {
stream
.try_collect()
.await
.expect("gather record batch stream")
}