MeiliSearch/meilisearch/tests/common/server.rs

400 lines
13 KiB
Rust
Raw Permalink Normal View History

2021-12-15 10:30:00 +01:00
#![allow(dead_code)]
use std::marker::PhantomData;
2021-03-24 11:03:01 +01:00
use std::path::Path;
use std::str::FromStr;
2022-10-26 11:09:44 +02:00
use std::time::Duration;
2021-03-24 11:03:01 +01:00
2022-10-20 18:00:07 +02:00
use actix_http::body::MessageBody;
use actix_web::dev::ServiceResponse;
2021-03-10 14:43:10 +01:00
use actix_web::http::StatusCode;
2024-07-08 20:58:27 +02:00
use byte_unit::{Byte, Unit};
2022-10-20 18:00:07 +02:00
use clap::Parser;
use meilisearch::option::{IndexerOpts, MaxMemory, MaxThreads, Opt};
use meilisearch::setup_meilisearch;
2021-09-28 18:10:09 +02:00
use once_cell::sync::Lazy;
use tempfile::TempDir;
use tokio::sync::OnceCell;
2022-10-26 11:09:44 +02:00
use tokio::time::sleep;
use uuid::Uuid;
2021-02-18 19:50:52 +01:00
use super::index::Index;
use super::service::Service;
use super::{Owned, Shared};
2022-10-20 18:00:07 +02:00
use crate::common::encoder::Encoder;
2023-09-11 16:50:53 +02:00
use crate::common::Value;
use crate::json;
2021-02-18 19:50:52 +01:00
pub struct Server<State = Owned> {
2021-02-24 09:30:51 +01:00
pub service: Service,
2021-03-24 11:03:01 +01:00
// hold ownership to the tempdir while we use the server instance.
2021-09-28 18:10:09 +02:00
_dir: Option<TempDir>,
_marker: PhantomData<State>,
2021-02-18 19:50:52 +01:00
}
pub static TEST_TEMP_DIR: Lazy<TempDir> = Lazy::new(|| TempDir::new().unwrap());
2021-09-28 18:10:09 +02:00
impl Server<Owned> {
2024-07-30 15:10:56 +02:00
fn into_shared(self) -> Server<Shared> {
Server { service: self.service, _dir: self._dir, _marker: PhantomData }
}
pub async fn new() -> Self {
let dir = TempDir::new().unwrap();
if cfg!(windows) {
std::env::set_var("TMP", TEST_TEMP_DIR.path());
} else {
std::env::set_var("TMPDIR", TEST_TEMP_DIR.path());
}
let options = default_settings(dir.path());
let (index_scheduler, auth) = setup_meilisearch(&options).unwrap();
2024-09-03 11:20:25 +02:00
let service = Service { index_scheduler, auth, options, api_key: None };
Server { service, _dir: Some(dir), _marker: PhantomData }
}
pub async fn new_auth_with_options(mut options: Opt, dir: TempDir) -> Self {
2021-09-28 18:10:09 +02:00
if cfg!(windows) {
std::env::set_var("TMP", TEST_TEMP_DIR.path());
} else {
std::env::set_var("TMPDIR", TEST_TEMP_DIR.path());
}
2021-02-18 19:50:52 +01:00
options.master_key = Some("MASTER_KEY".to_string());
2021-02-18 19:50:52 +01:00
2022-10-18 15:14:18 +02:00
let (index_scheduler, auth) = setup_meilisearch(&options).unwrap();
2024-09-03 11:20:25 +02:00
let service = Service { index_scheduler, auth, options, api_key: None };
2022-10-20 18:00:07 +02:00
Server { service, _dir: Some(dir), _marker: PhantomData }
2021-03-24 11:03:01 +01:00
}
pub async fn new_auth() -> Self {
let dir = TempDir::new().unwrap();
let options = default_settings(dir.path());
Self::new_auth_with_options(options, dir).await
}
pub async fn new_with_options(options: Opt) -> Result<Self, anyhow::Error> {
let (index_scheduler, auth) = setup_meilisearch(&options)?;
2024-09-03 11:20:25 +02:00
let service = Service { index_scheduler, auth, options, api_key: None };
Ok(Server { service, _dir: None, _marker: PhantomData })
}
pub fn use_api_key(&mut self, api_key: impl AsRef<str>) {
self.service.api_key = Some(api_key.as_ref().to_string());
}
/// Fetch and use the default admin key for nexts http requests.
pub async fn use_admin_key(&mut self, master_key: impl AsRef<str>) {
self.use_api_key(master_key);
let (response, code) = self.list_api_keys("").await;
assert_eq!(200, code, "{:?}", response);
let admin_key = &response["results"][1]["key"];
self.use_api_key(admin_key.as_str().unwrap());
}
pub async fn add_api_key(&self, content: Value) -> (Value, StatusCode) {
let url = "/keys";
self.service.post(url, content).await
}
pub async fn patch_api_key(&self, key: impl AsRef<str>, content: Value) -> (Value, StatusCode) {
let url = format!("/keys/{}", key.as_ref());
self.service.patch(url, content).await
}
pub async fn delete_api_key(&self, key: impl AsRef<str>) -> (Value, StatusCode) {
let url = format!("/keys/{}", key.as_ref());
self.service.delete(url).await
}
/// Returns a view to an index. There is no guarantee that the index exists.
pub fn index(&self, uid: impl AsRef<str>) -> Index<'_> {
self.index_with_encoder(uid, Encoder::Plain)
}
pub async fn create_index(&self, body: Value) -> (Value, StatusCode) {
self.service.post("/indexes", body).await
}
pub fn index_with_encoder(&self, uid: impl AsRef<str>, encoder: Encoder) -> Index<'_> {
Index {
uid: uid.as_ref().to_string(),
service: &self.service,
encoder,
marker: PhantomData,
}
}
pub async fn list_indexes(
&self,
offset: Option<usize>,
limit: Option<usize>,
) -> (Value, StatusCode) {
let (offset, limit) = (
offset.map(|offset| format!("offset={offset}")),
limit.map(|limit| format!("limit={limit}")),
);
let query_parameter = offset
.as_ref()
.zip(limit.as_ref())
.map(|(offset, limit)| format!("{offset}&{limit}"))
.or_else(|| offset.xor(limit));
if let Some(query_parameter) = query_parameter {
self.service.get(format!("/indexes?{query_parameter}")).await
} else {
self.service.get("/indexes").await
}
}
pub async fn stats(&self) -> (Value, StatusCode) {
self.service.get("/stats").await
}
pub async fn tasks(&self) -> (Value, StatusCode) {
self.service.get("/tasks").await
}
pub async fn set_features(&self, value: Value) -> (Value, StatusCode) {
self.service.patch("/experimental-features", value).await
}
pub async fn get_metrics(&self) -> (Value, StatusCode) {
self.service.get("/metrics").await
}
}
impl Server<Shared> {
fn init_new_shared_instance() -> Server<Shared> {
let dir = TempDir::new().unwrap();
if cfg!(windows) {
std::env::set_var("TMP", TEST_TEMP_DIR.path());
} else {
std::env::set_var("TMPDIR", TEST_TEMP_DIR.path());
}
let options = default_settings(dir.path());
2022-10-18 15:14:18 +02:00
let (index_scheduler, auth) = setup_meilisearch(&options).unwrap();
2024-09-03 11:20:25 +02:00
let service = Service { index_scheduler, auth, api_key: None, options };
2022-10-20 18:00:07 +02:00
Server { service, _dir: Some(dir), _marker: PhantomData }
}
pub fn new_shared() -> &'static Server<Shared> {
static SERVER: Lazy<Server<Shared>> = Lazy::new(Server::init_new_shared_instance);
&SERVER
}
pub async fn new_shared_with_admin_key() -> &'static Server<Shared> {
static SERVER: OnceCell<Server<Shared>> = OnceCell::const_new();
SERVER
.get_or_init(|| async {
let mut server = Server::new_auth().await;
server.use_admin_key("MASTER_KEY").await;
2024-07-30 15:10:56 +02:00
server.into_shared()
})
.await
2022-06-09 18:08:26 +02:00
}
/// You shouldn't access random indexes on a shared instance thus this method
/// must fail.
pub async fn get_index_fail(&self, uid: impl AsRef<str>) -> (Value, StatusCode) {
let url = format!("/indexes/{}", urlencoding::encode(uid.as_ref()));
let (value, code) = self.service.get(url).await;
if code.is_success() {
panic!("`get_index_fail` succeeded with uid: {}", uid.as_ref());
}
(value, code)
}
2022-10-20 18:00:07 +02:00
pub async fn delete_index_fail(&self, uid: impl AsRef<str>) -> (Value, StatusCode) {
let url = format!("/indexes/{}", urlencoding::encode(uid.as_ref()));
let (value, code) = self.service.delete(url).await;
if code.is_success() {
panic!("`delete_index_fail` succeeded with uid: {}", uid.as_ref());
}
(value, code)
}
pub async fn update_raw_index_fail(
&self,
uid: impl AsRef<str>,
body: Value,
) -> (Value, StatusCode) {
let url = format!("/indexes/{}", urlencoding::encode(uid.as_ref()));
let (value, code) = self.service.patch_encoded(url, body, Encoder::Plain).await;
if code.is_success() {
panic!("`update_raw_index_fail` succeeded with uid: {}", uid.as_ref());
}
(value, code)
2021-02-18 19:50:52 +01:00
}
/// Since this call updates the state of the instance, it must fail.
/// If it doesn't fail, the test will panic to help you debug what
/// is going on.
pub async fn create_index_fail(&self, body: Value) -> (Value, StatusCode) {
let (mut task, code) = self._create_index(body).await;
if code.is_success() {
task = self.wait_task(task.uid()).await;
if task.is_success() {
panic!(
"`create_index_fail` succeeded: {}",
serde_json::to_string_pretty(&task).unwrap()
);
}
}
(task, code)
}
}
impl<State> Server<State> {
2022-10-18 15:14:18 +02:00
pub async fn init_web_app(
&self,
) -> impl actix_web::dev::Service<
actix_http::Request,
Response = ServiceResponse<impl MessageBody>,
Error = actix_web::Error,
> {
self.service.init_web_app().await
2022-10-18 15:14:18 +02:00
}
pub async fn list_api_keys(&self, params: &str) -> (Value, StatusCode) {
let url = format!("/keys{params}");
self.service.get(url).await
}
pub async fn dummy_request(
&self,
method: impl AsRef<str>,
url: impl AsRef<str>,
) -> (Value, StatusCode) {
match method.as_ref() {
"POST" => self.service.post(url, json!({})).await,
"PUT" => self.service.put(url, json!({})).await,
"PATCH" => self.service.patch(url, json!({})).await,
"GET" => self.service.get(url).await,
"DELETE" => self.service.delete(url).await,
_ => unreachable!(),
}
}
pub async fn get_api_key(&self, key: impl AsRef<str>) -> (Value, StatusCode) {
let url = format!("/keys/{}", key.as_ref());
self.service.get(url).await
}
pub(super) fn _index(&self, uid: impl AsRef<str>) -> Index<'_> {
Index {
uid: uid.as_ref().to_string(),
service: &self.service,
encoder: Encoder::Plain,
marker: PhantomData,
}
}
2021-02-18 19:50:52 +01:00
/// Returns a view to an index. There is no guarantee that the index exists.
pub fn unique_index(&self) -> Index<'_> {
let uuid = Uuid::new_v4();
Index {
uid: uuid.to_string(),
service: &self.service,
encoder: Encoder::Plain,
marker: PhantomData,
}
}
pub(super) async fn _create_index(&self, body: Value) -> (Value, StatusCode) {
2023-01-24 13:20:20 +01:00
self.service.post("/indexes", body).await
}
pub async fn multi_search(&self, queries: Value) -> (Value, StatusCode) {
self.service.post("/multi-search", queries).await
}
2023-01-24 13:20:20 +01:00
pub async fn list_indexes_raw(&self, parameters: &str) -> (Value, StatusCode) {
self.service.get(format!("/indexes{parameters}")).await
}
pub async fn tasks_filter(&self, filter: &str) -> (Value, StatusCode) {
self.service.get(format!("/tasks?{}", filter)).await
Bring back `release-v0.30.0` into `release-v0.30.0-temp` (final: into `main`) (#3145) * Fix error code of the "duplicate index found" error * Use the content of the ProcessingTasks in the tasks cancelation system * Change the missing_filters error code into missing_task_filters * WIP Introduce the invalid_task_uid error code * Use more precise error codes/message for the task routes + Allow star operator in delete/cancel tasks + rename originalQuery to originalFilters + Display error/canceled_by in task view even when they are = null + Rename task filter fields by using their plural forms + Prepare an error code for canceledBy filter + Only return global tasks if the API key action `index.*` is there * Add canceledBy task filter * Update tests following task API changes * Rename original_query to original_filters everywhere * Update more insta-snap tests * Make clippy happy They're a happy clip now. * Make rustfmt happy >:-( * Fix Index name parsing error message to fit the specification * Bump milli version to 0.35.1 * Fix the new error messages * fix the error messages and add tests * rename the error codes for the sake of consistency * refactor the way we send the cli informations + add the analytics for the config file and ssl usage * Apply suggestions from code review Co-authored-by: Clément Renault <clement@meilisearch.com> * add a comment over the new infos structure * reformat, sorry @kero * Store analytics for the documents deletions * Add analytics on all the settings * Spawn threads with names * Spawn rayon threads with names * update the distinct attributes to the spec update * update the analytics on the search route * implements the analytics on the health and version routes * Fix task details serialization * Add the question mark to the task deletion query filter * Add the question mark to the task cancelation query filter * Fix tests * add analytics on the task route * Add all the missing fields of the new task query type * Create a new analytics for the task deletion * Create a new analytics for the task creation * batch the tasks seen events * Update the finite pagination analytics * add the analytics of the swap-indexes route * Stop removing the DB when failing to read it * Rename originalFilters into originalFilters * Rename matchedDocuments into providedIds * Add `workflow_dispatch` to flaky.yml * Bump grenad to 0.4.4 * Bump milli to version v0.37.0 * Don't multiply total memory returned by sysinfo anymore sysinfo now returns bytes rather than KB * Add a dispatch to the publish binaries workflow * Fix publish release CI * Don't use gold but the default linker * Always display details for the indexDeletion task * Fix the insta tests * refactorize the whole test suite 1. Make a call to assert_internally_consistent automatically when snapshoting the scheduler. There is no point in snapshoting something broken and expect the dumb humans to notice. 2. Replace every possible call to assert_internally_consistent by a snapshot of the scheduler. It takes as many lines and ensure we never change something without noticing in any tests ever. 3. Name every snapshots: it's easier to debug when something goes wrong and easier to review in general. 4. Stop skipping breakpoints, it's too easy to miss something. Now you must explicitely show which path is the scheduler supposed to use. 5. Add a timeout on the channel.recv, it eases the process of writing tests, now when something file you get a failure instead of a deadlock. * rebase on release-v0.30 * makes clippy happy * update the snapshots after a rebase * try to remove the flakyness of the failing test * Add more analytics on the ranking rules positions * Update the dump test to check for the dumpUid dumpCreation task details * send the ranking rules as a string because amplitude is too dumb to process an array as a single value * Display a null dumpUid until we computed the dump itself on disk * Update tests * Check if the master key is missing before returning an error Co-authored-by: Loïc Lecrenier <loic.lecrenier@me.com> Co-authored-by: bors[bot] <26634292+bors[bot]@users.noreply.github.com> Co-authored-by: Kerollmops <clement@meilisearch.com> Co-authored-by: ManyTheFish <many@meilisearch.com> Co-authored-by: Tamo <tamo@meilisearch.com> Co-authored-by: Louis Dureuil <louis@meilisearch.com>
2022-11-28 16:27:41 +01:00
}
pub async fn version(&self) -> (Value, StatusCode) {
self.service.get("/version").await
2021-12-15 10:30:00 +01:00
}
2022-10-26 11:09:44 +02:00
pub async fn create_dump(&self) -> (Value, StatusCode) {
self.service.post("/dumps", json!(null)).await
}
2023-09-11 11:00:05 +02:00
pub async fn create_snapshot(&self) -> (Value, StatusCode) {
self.service.post("/snapshots", json!(null)).await
}
2022-10-26 11:09:44 +02:00
pub async fn index_swap(&self, value: Value) -> (Value, StatusCode) {
self.service.post("/swap-indexes", value).await
}
pub async fn cancel_tasks(&self, value: &str) -> (Value, StatusCode) {
self.service.post(format!("/tasks/cancel?{}", value), json!(null)).await
2022-10-26 11:09:44 +02:00
}
pub async fn delete_tasks(&self, value: &str) -> (Value, StatusCode) {
self.service.delete(format!("/tasks?{}", value)).await
}
2022-10-26 11:09:44 +02:00
pub async fn wait_task(&self, update_id: u64) -> Value {
// try several times to get status, or panic to not wait forever
let url = format!("/tasks/{}", update_id);
for _ in 0..100 {
let (response, status_code) = self.service.get(&url).await;
assert_eq!(200, status_code, "response: {}", response);
if response["status"] == "succeeded" || response["status"] == "failed" {
return response;
}
// wait 0.5 second.
sleep(Duration::from_millis(500)).await;
}
panic!("Timeout waiting for update id");
}
pub async fn get_task(&self, update_id: u64) -> (Value, StatusCode) {
let url = format!("/tasks/{}", update_id);
self.service.get(url).await
}
pub async fn get_features(&self) -> (Value, StatusCode) {
self.service.get("/experimental-features").await
}
2021-02-18 19:50:52 +01:00
}
2021-03-24 11:03:01 +01:00
pub fn default_settings(dir: impl AsRef<Path>) -> Opt {
Opt {
db_path: dir.as_ref().join("db"),
dump_dir: dir.as_ref().join("dumps"),
2021-03-24 11:03:01 +01:00
env: "development".to_owned(),
2022-01-12 15:57:31 +01:00
no_analytics: true,
2024-07-08 20:58:27 +02:00
max_index_size: Byte::from_u64_with_unit(100, Unit::MiB).unwrap(),
max_task_db_size: Byte::from_u64_with_unit(1, Unit::GiB).unwrap(),
http_payload_size_limit: Byte::from_u64_with_unit(10, Unit::MiB).unwrap(),
2021-03-24 11:03:01 +01:00
snapshot_dir: ".".into(),
indexer_options: IndexerOpts {
// memory has to be unlimited because several meilisearch are running in test context.
max_indexing_memory: MaxMemory::unlimited(),
2023-02-15 12:31:14 +01:00
skip_index_budget: true,
// Having 2 threads makes the tests way faster
max_indexing_threads: MaxThreads::from_str("2").unwrap(),
},
experimental_enable_metrics: false,
..Parser::parse_from(None as Option<&str>)
2021-03-24 11:03:01 +01:00
}
}