Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ jobs:
- name: Build
run: cargo build --verbose
- name: Meilisearch (latest version) setup with Docker
run: docker run -d -p 7700:7700 getmeili/meilisearch:latest ./meilisearch --no-analytics=true --master-key=masterKey
run: docker run -d -p 7700:7700 getmeili/meilisearch:latest ./meilisearch --no-analytics --master-key=masterKey
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
run: docker run -d -p 7700:7700 getmeili/meilisearch:latest ./meilisearch --no-analytics --master-key=masterKey
run: docker run -d -p 7700:7700 getmeili/meilisearch:latest ./meilisearch --no-analytics=true --master-key=masterKey

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not yet, will be for v0.26.0, not v0.25.0 :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure that I understand that, the previous version of meilisearch works with this change, and the newer version (v0.26) will only work with this change. This patch makes the command line compatible with the future version of Meilisearch and doesn't break the v0.25 one.

Can't we accept this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hum from what I see it has been merged some times ago already; meilisearch/meilisearch#1984
And it works for me on meilisearch v0.25.2

- name: Run tests
run: cargo test --verbose -- --test-threads=1
run: cargo test --verbose

linter:
name: clippy-check
Expand Down
13 changes: 7 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,19 @@ First of all, thank you for contributing to Meilisearch! The goal of this docume

### Tests <!-- omit in toc -->

All the tests are documentation tests.<br>
Since they are all making operations on the Meilisearch server, running all the tests simultaneously would cause panics.

To run the tests one by one, run:
To run the tests, run:

```bash
# Tests
curl -L https://install.meilisearch.com | sh # download Meilisearch
./meilisearch --master-key=masterKey --no-analytics=true # run Meilisearch
cargo test -- --test-threads=1
./meilisearch --master-key=masterKey --no-analytics # run Meilisearch
cargo test
```

There is two kind of tests, documentation tests and unit tests.
If you need to write or read the unit tests you should consider reading this
[readme](meilisearch-test-macro/README.md) about our custom testing macro.

Also, the WASM example compilation should be checked:

```bash
Expand Down
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ readme = "README.md"
repository = "https://github.com/meilisearch/meilisearch-sdk"

[dependencies]
async-trait = "0.1.51"
serde_json = "1.0"
log = "0.4"
serde = { version = "1.0", features = ["derive"] }
Expand All @@ -32,6 +33,7 @@ sync = []
env_logger = "0.9"
futures-await-test = "0.3"
futures = "0.3"
meilisearch-test-macro = { path = "meilisearch-test-macro" }

# The following dependencies are required for examples
wasm-bindgen = "0.2"
Expand Down
39 changes: 26 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,12 @@ fn main() { block_on(async move {

// Add some movies in the index. If the index 'movies' does not exist, Meilisearch creates it when you first add the documents.
movies.add_documents(&[
Movie{id: 1, title: String::from("Carol"), genres: vec!["Romance".to_string(), "Drama".to_string()]},
Movie{id: 2, title: String::from("Wonder Woman"), genres: vec!["Action".to_string(), "Adventure".to_string()]},
Movie{id: 3, title: String::from("Life of Pi"), genres: vec!["Adventure".to_string(), "Drama".to_string()]},
Movie{id: 4, title: String::from("Mad Max"), genres: vec!["Adventure".to_string(), "Science Fiction".to_string()]},
Movie{id: 5, title: String::from("Moana"), genres: vec!["Fantasy".to_string(), "Action".to_string()]},
Movie{id: 6, title: String::from("Philadelphia"), genres: vec!["Drama".to_string()]},
Movie { id: 1, title: String::from("Carol"), genres: vec!["Romance".to_string(), "Drama".to_string()] },
Movie { id: 2, title: String::from("Wonder Woman"), genres: vec!["Action".to_string(), "Adventure".to_string()] },
Movie { id: 3, title: String::from("Life of Pi"), genres: vec!["Adventure".to_string(), "Drama".to_string()] },
Movie { id: 4, title: String::from("Mad Max"), genres: vec!["Adventure".to_string(), "Science Fiction".to_string()] },
Movie { id: 5, title: String::from("Moana"), genres: vec!["Fantasy".to_string(), "Action".to_string()] },
Movie { id: 6, title: String::from("Philadelphia"), genres: vec!["Drama".to_string()] },
], Some("id")).await.unwrap();
})}
```
Expand All @@ -131,12 +131,12 @@ fn main() { block_on(async move {

```rust
// Meilisearch is typo-tolerant:
println!("{:?}", client.index("movies").search().with_query("caorl").execute::<Movie>().await.unwrap().hits);
println!("{:?}", client.index("movies_2").search().with_query("caorl").execute::<Movie>().await.unwrap().hits);
```

Output:
```
[Movie{id: 1, title: String::from("Carol"), genres: vec!["Romance", "Drama"]}]
[Movie { id: 1, title: String::from("Carol"), genres: vec!["Romance", "Drama"] }]
```

Json output:
Expand All @@ -157,7 +157,14 @@ Json output:
#### Custom Search <!-- omit in toc -->

```rust
println!("{:?}", client.index("movies").search().with_query("phil").with_attributes_to_highlight(Selectors::Some(&["*"])).execute::<Movie>().await.unwrap().hits);
let search_result = client.index("movies_3")
.search()
.with_query("phil")
.with_attributes_to_highlight(Selectors::Some(&["*"]))
.execute::<Movie>()
.await
.unwrap();
println!("{:?}", search_result.hits);
```

Json output:
Expand Down Expand Up @@ -189,9 +196,9 @@ index setting.
```rust
let filterable_attributes = [
"id",
"genres"
"genres",
];
client.index("movies").set_filterable_attributes(&filterable_attributes).await.unwrap();
client.index("movies_4").set_filterable_attributes(&filterable_attributes).await.unwrap();
```

You only need to perform this operation once.
Expand All @@ -204,8 +211,14 @@ status](https://docs.meilisearch.com/reference/api/updates.html#get-an-update-st
Then, you can perform the search:

```rust
println!("{:?}", client.index("movies").search().with_query("wonder").with_filter("id > 1 AND genres = Action")
.execute::<Movie>().await.unwrap().hits);
let search_result = client.index("movies_5")
.search()
.with_query("wonder")
.with_filter("id > 1 AND genres = Action")
.execute::<Movie>()
.await
.unwrap();
println!("{:?}", search_result.hits);
```

Json output:
Expand Down
14 changes: 14 additions & 0 deletions meilisearch-test-macro/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "meilisearch-test-macro"
version = "0.1.0"
edition = "2021"

[lib]
proc-macro = true

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
proc-macro2 = "1.0.0"
quote = "1.0.0"
syn = { version = "1.0.0", features = ["clone-impls", "full", "parsing", "printing", "proc-macro"], default-features = false }
72 changes: 72 additions & 0 deletions meilisearch-test-macro/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Meilisearch test macro
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️ ❤️ ❤️


This crate defines the `meilisearch_test` macro.

Since the code is a little bit harsh to read, here is a complete explanation of how to use it.
The macro aims to ease the writing of tests by:
1. Reducing the amount of code you need to write and maintain for each test.
2. Ensuring All your indexes as a unique name so they can all run in parallel.
3. Ensuring you never forget to delete your index if you need one.


Before explaining its usage, we're going to see a simple test *before* this macro:
```rust
#[async_test]
async fn test_get_all_tasks() -> Result<(), Error> {
let client = Client::new("http://localhost:7700", "masterKey");

let index = client
.create_index("test_get_all_tasks", None)
.await?
.wait_for_completion(&client, None, None)
.await?
.try_make_index(&client)
.unwrap();

let tasks = index.get_all_tasks().await?;
// The only task is the creation of the index
assert_eq!(status.len(), 1);

index.delete()
.await?
.wait_for_completion(&client, None, None)
.await?;
Ok(())
}
```

I have multiple problems with this test:
- `let client = Client::new("http://localhost:7700", "masterKey");`: This line is always the same in every test.
And if you make a typo on the http addr or the master key, you'll have an error.
- `let index = client.create_index("test_get_all_tasks", None)...`: Each test needs to have an unique name.
This means we currently need to write the name of the test everywhere; it's not practical.
- There are 11 lines dedicated to the creation and deletion of the index; this is once again something that'll never change
whatever the test is. But, if you ever forget to delete the index at the end, you'll get in some trouble to re-run
the tests.

-------

With this macro, all these problems are solved. See a rewrite of this test:
```rust
#[meilisearch_test]
async fn test_get_all_tasks(index: Index, client: Client) -> Result<(), Error> {
let tasks = index.get_all_tasks().await?;
// The only task is the creation of the index
assert_eq!(status.len(), 1);
}
```

So now you're probably seeing what happened. By using an index and a client in the parameter of
the test, the macro automatically did the same thing we've seen before.
There are a few rules, though:
1. The macro only handles three types of arguments:
- `String`: It returns the name of the test.
- `Client`: It creates a client like that: `Client::new("http://localhost:7700", "masterKey")`.
- `Index`: It creates and deletes an index, as we've seen before.
2. You only get what you asked for. That means if you don't ask for an index, no index will be created in meilisearch.
So, if you are testing the creation of indexes, you can ask for a `Client` and a `String` and then create it yourself.
The index won't be present in meilisearch.
3. You can put your parameters in the order you want it won't change anything.
4. Everything you use **must** be in scope directly. If you're using an `Index`, you must write `Index` in the parameters,
not `meilisearch_rust::Index` or `crate::Index`.
5. And I think that's all, use and abuse it 🎉
159 changes: 159 additions & 0 deletions meilisearch-test-macro/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
#![warn(clippy::pedantic)]
#![recursion_limit = "4096"]

extern crate proc_macro;

use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::quote;
use syn::*;

#[proc_macro_attribute]
pub fn meilisearch_test(params: TokenStream, input: TokenStream) -> TokenStream {
assert!(
params.is_empty(),
"the #[async_test] attribute currently does not take parameters"
);

let mut inner = parse_macro_input!(input as Item);
let mut outer = inner.clone();
if let (&mut Item::Fn(ref mut inner_fn), &mut Item::Fn(ref mut outer_fn)) =
(&mut inner, &mut outer)
{
inner_fn.sig.ident = Ident::new(
&("_inner_meilisearch_test_macro_".to_string() + &inner_fn.sig.ident.to_string()),
Span::call_site(),
);
let inner_ident = &inner_fn.sig.ident;
inner_fn.vis = Visibility::Inherited;
inner_fn.attrs.clear();
assert!(
outer_fn.sig.asyncness.take().is_some(),
"#[meilisearch_test] can only be applied to async functions"
);

#[derive(Debug, PartialEq, Eq)]
enum Param {
Client,
Index,
String,
}

let mut params = Vec::new();

let parameters = &inner_fn.sig.inputs;
for param in parameters.iter() {
match param {
FnArg::Typed(PatType { ty, .. }) => match &**ty {
Type::Path(TypePath { path: Path { segments, .. }, .. } ) if segments.last().unwrap().ident.to_string() == "String" => {
params.push(Param::String);
}
Type::Path(TypePath { path: Path { segments, .. }, .. } ) if segments.last().unwrap().ident.to_string() == "Index" => {
params.push(Param::Index);
}
Type::Path(TypePath { path: Path { segments, .. }, .. } ) if segments.last().unwrap().ident.to_string() == "Client" => {
params.push(Param::Client);
}
// TODO: throw this error while pointing to the specific token
ty => panic!(
"#[meilisearch_test] can only receive Client, Index or String as parameters but received {:?}", ty
),
},
// TODO: throw this error while pointing to the specific token
// Used `self` as a parameter
_ => panic!(
"#[meilisearch_test] can only receive Client, Index or String as parameters"
),
}
}

// if a `Client` or an `Index` was asked for the test we must create a meilisearch `Client`.
let use_client = params
.iter()
.any(|param| matches!(param, Param::Client | Param::Index));
// if a `String` or an `Index` was asked then we need to extract the name of the test function.
let use_name = params
.iter()
.any(|param| matches!(param, Param::String | Param::Index));
let use_index = params.contains(&Param::Index);

// Now we are going to build the body of the outer function
let mut outer_block: Vec<Stmt> = Vec::new();

// First we need to check if a client will be used and create it if it’s the case
if use_client {
outer_block.push(parse_quote!(
let client = Client::new("http://localhost:7700", "masterKey");
));
}

// Now we do the same for the index name
if use_name {
let name = &outer_fn.sig.ident;
outer_block.push(parse_quote!(
let name = stringify!(#name).to_string();
));
}

// And finally if an index was asked we create it and wait until meilisearch confirm its creation.
// We’ll need to delete it later.
if use_index {
outer_block.push(parse_quote!(
let index = client
.create_index(&name, None)
.await
.unwrap()
.wait_for_completion(&client, None, None)
.await
.unwrap()
.try_make_index(&client)
.unwrap();
));
}

// Create a list of params separated by comma with the name we defined previously.
let params: Vec<Expr> = params
.into_iter()
.map(|param| match param {
Param::Client => parse_quote!(client),
Param::Index => parse_quote!(index),
Param::String => parse_quote!(name),
})
.collect();

// Now we can call the user code with our parameters :tada:
outer_block.push(parse_quote!(
let result = #inner_ident(#(#params.clone()),*).await;
));

// And right before the end, if an index was created we need to delete it.
if use_index {
outer_block.push(parse_quote!(
index
.delete()
.await
.unwrap()
.wait_for_completion(&client, None, None)
.await
.unwrap();
));
}

// Finally, for the great finish we just return the result the user gave us.
outer_block.push(parse_quote!(return result;));

outer_fn.sig.inputs.clear();
outer_fn.sig.asyncness = inner_fn.sig.asyncness.clone();
outer_fn
.attrs
.push(parse_quote!(#[futures_await_test::async_test]));
outer_fn.block.stmts = outer_block;
} else {
panic!("#[meilisearch_test] can only be applied to async functions")
}
quote!(
#inner
#outer
)
.into()
}
Loading