Add fixtures_path
in sqlx::test args (#2545)
* feat: add fixtures_path * test: add test for fixtures_path * docs: expand test docs with fixtures_path * test: add new test instead of co-opting and old one. * feat: add explicit path operating mode for fixtures parameters and allow combining multiple fixtures parameters * fix: require .sql extension for explicit path fixtures * feat: add custom relative path style to fixtures argument * fix: missing cfg feature * docs: update * fix: explicit fixtures styling checks for paths. Remove strict sql extension requirement for explicit path, they still need an extension. Add .sql extension to implicit fixtures style only if missing. * style: cargo fmt * docs: update documentation
This commit is contained in:
parent
9a6ebd0a74
commit
16eeea8611
8 changed files with 391 additions and 18 deletions
|
@ -3,10 +3,18 @@ use quote::quote;
|
|||
|
||||
#[cfg(feature = "migrate")]
|
||||
struct Args {
|
||||
fixtures: Vec<syn::LitStr>,
|
||||
fixtures: Vec<(FixturesType, Vec<syn::LitStr>)>,
|
||||
migrations: MigrationsOpt,
|
||||
}
|
||||
|
||||
#[cfg(feature = "migrate")]
|
||||
enum FixturesType {
|
||||
None,
|
||||
RelativePath,
|
||||
CustomRelativePath(syn::LitStr),
|
||||
ExplicitPath,
|
||||
}
|
||||
|
||||
#[cfg(feature = "migrate")]
|
||||
enum MigrationsOpt {
|
||||
InferredPath,
|
||||
|
@ -73,16 +81,59 @@ fn expand_advanced(args: syn::AttributeArgs, input: syn::ItemFn) -> crate::Resul
|
|||
|
||||
let fn_arg_types = inputs.iter().map(|_| quote! { _ });
|
||||
|
||||
let fixtures = args.fixtures.into_iter().map(|fixture| {
|
||||
let path = format!("fixtures/{}.sql", fixture.value());
|
||||
let mut fixtures = Vec::new();
|
||||
|
||||
quote! {
|
||||
::sqlx::testing::TestFixture {
|
||||
path: #path,
|
||||
contents: include_str!(#path),
|
||||
}
|
||||
}
|
||||
});
|
||||
for (fixture_type, fixtures_local) in args.fixtures {
|
||||
let mut res = match fixture_type {
|
||||
FixturesType::None => vec![],
|
||||
FixturesType::RelativePath => fixtures_local
|
||||
.into_iter()
|
||||
.map(|fixture| {
|
||||
let mut fixture_str = fixture.value();
|
||||
add_sql_extension_if_missing(&mut fixture_str);
|
||||
|
||||
let path = format!("fixtures/{}", fixture_str);
|
||||
|
||||
quote! {
|
||||
::sqlx::testing::TestFixture {
|
||||
path: #path,
|
||||
contents: include_str!(#path),
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
FixturesType::CustomRelativePath(path) => fixtures_local
|
||||
.into_iter()
|
||||
.map(|fixture| {
|
||||
let mut fixture_str = fixture.value();
|
||||
add_sql_extension_if_missing(&mut fixture_str);
|
||||
|
||||
let path = format!("{}/{}", path.value(), fixture_str);
|
||||
|
||||
quote! {
|
||||
::sqlx::testing::TestFixture {
|
||||
path: #path,
|
||||
contents: include_str!(#path),
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
FixturesType::ExplicitPath => fixtures_local
|
||||
.into_iter()
|
||||
.map(|fixture| {
|
||||
let path = fixture.value();
|
||||
|
||||
quote! {
|
||||
::sqlx::testing::TestFixture {
|
||||
path: #path,
|
||||
contents: include_str!(#path),
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
fixtures.append(&mut res)
|
||||
}
|
||||
|
||||
let migrations = match args.migrations {
|
||||
MigrationsOpt::ExplicitPath(path) => {
|
||||
|
@ -130,24 +181,37 @@ fn expand_advanced(args: syn::AttributeArgs, input: syn::ItemFn) -> crate::Resul
|
|||
|
||||
#[cfg(feature = "migrate")]
|
||||
fn parse_args(attr_args: syn::AttributeArgs) -> syn::Result<Args> {
|
||||
let mut fixtures = vec![];
|
||||
let mut fixtures = Vec::new();
|
||||
let mut migrations = MigrationsOpt::InferredPath;
|
||||
|
||||
for arg in attr_args {
|
||||
match arg {
|
||||
syn::NestedMeta::Meta(syn::Meta::List(list)) if list.path.is_ident("fixtures") => {
|
||||
if !fixtures.is_empty() {
|
||||
return Err(syn::Error::new_spanned(list, "duplicate `fixtures` arg"));
|
||||
}
|
||||
let mut fixtures_local = vec![];
|
||||
let mut fixtures_type = FixturesType::None;
|
||||
|
||||
for nested in list.nested {
|
||||
match nested {
|
||||
syn::NestedMeta::Lit(syn::Lit::Str(litstr)) => fixtures.push(litstr),
|
||||
syn::NestedMeta::Lit(syn::Lit::Str(litstr)) => {
|
||||
// fixtures("<file_1>","<file_2>") or fixtures("<path/file_1.sql>","<path/file_2.sql>")
|
||||
parse_fixtures_args(&mut fixtures_type, litstr, &mut fixtures_local)?;
|
||||
},
|
||||
syn::NestedMeta::Meta(syn::Meta::NameValue(namevalue))
|
||||
if namevalue.path.is_ident("path") =>
|
||||
{
|
||||
// fixtures(path = "<path>", scripts("<file_1>","<file_2>")) checking `path` argument
|
||||
parse_fixtures_path_args(&mut fixtures_type, namevalue)?;
|
||||
},
|
||||
syn::NestedMeta::Meta(syn::Meta::List(list)) if list.path.is_ident("scripts") => {
|
||||
// fixtures(path = "<path>", scripts("<file_1>","<file_2>")) checking `scripts` argument
|
||||
parse_fixtures_scripts_args(&mut fixtures_type, list, &mut fixtures_local)?;
|
||||
}
|
||||
other => {
|
||||
return Err(syn::Error::new_spanned(other, "expected string literal"))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
fixtures.push((fixtures_type, fixtures_local));
|
||||
}
|
||||
syn::NestedMeta::Meta(syn::Meta::NameValue(namevalue))
|
||||
if namevalue.path.is_ident("migrations") =>
|
||||
|
@ -217,3 +281,107 @@ fn parse_args(attr_args: syn::AttributeArgs) -> syn::Result<Args> {
|
|||
migrations,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "migrate")]
|
||||
fn parse_fixtures_args(
|
||||
fixtures_type: &mut FixturesType,
|
||||
litstr: syn::LitStr,
|
||||
fixtures_local: &mut Vec<syn::LitStr>,
|
||||
) -> syn::Result<()> {
|
||||
// fixtures(path = "<path>", scripts("<file_1>","<file_2>")) checking `path` argument
|
||||
let path_str = litstr.value();
|
||||
let path = std::path::Path::new(&path_str);
|
||||
// This will be `true` if there's at least one path separator (`/` or `\`)
|
||||
// It's also true for all absolute paths, even e.g. `/foo.sql` as the root directory is counted as a component.
|
||||
let is_explicit_path = path.components().count() > 1;
|
||||
match fixtures_type {
|
||||
FixturesType::None => {
|
||||
if is_explicit_path {
|
||||
*fixtures_type = FixturesType::ExplicitPath;
|
||||
} else {
|
||||
*fixtures_type = FixturesType::RelativePath;
|
||||
}
|
||||
}
|
||||
FixturesType::RelativePath => {
|
||||
if is_explicit_path {
|
||||
return Err(syn::Error::new_spanned(
|
||||
litstr,
|
||||
"expected only relative path fixtures",
|
||||
));
|
||||
}
|
||||
}
|
||||
FixturesType::ExplicitPath => {
|
||||
if !is_explicit_path {
|
||||
return Err(syn::Error::new_spanned(
|
||||
litstr,
|
||||
"expected only explicit path fixtures",
|
||||
));
|
||||
}
|
||||
}
|
||||
FixturesType::CustomRelativePath(_) => {
|
||||
return Err(syn::Error::new_spanned(
|
||||
litstr,
|
||||
"custom relative path fixtures must be defined in `scripts` argument",
|
||||
))
|
||||
}
|
||||
}
|
||||
if (matches!(fixtures_type, FixturesType::ExplicitPath) && !is_explicit_path) {
|
||||
return Err(syn::Error::new_spanned(
|
||||
litstr,
|
||||
"expected explicit path fixtures to have `.sql` extension",
|
||||
));
|
||||
}
|
||||
fixtures_local.push(litstr);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "migrate")]
|
||||
fn parse_fixtures_path_args(
|
||||
fixtures_type: &mut FixturesType,
|
||||
namevalue: syn::MetaNameValue,
|
||||
) -> syn::Result<()> {
|
||||
// fixtures(path = "<path>", scripts("<file_1>","<file_2>")) checking `path` argument
|
||||
if !matches!(fixtures_type, FixturesType::None) {
|
||||
return Err(syn::Error::new_spanned(
|
||||
namevalue,
|
||||
"`path` must be the first argument of `fixtures`",
|
||||
));
|
||||
}
|
||||
*fixtures_type = match namevalue.lit {
|
||||
// path = "<path>"
|
||||
syn::Lit::Str(litstr) => FixturesType::CustomRelativePath(litstr),
|
||||
_ => return Err(syn::Error::new_spanned(namevalue, "expected string")),
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "migrate")]
|
||||
fn parse_fixtures_scripts_args(
|
||||
fixtures_type: &mut FixturesType,
|
||||
list: syn::MetaList,
|
||||
fixtures_local: &mut Vec<syn::LitStr>,
|
||||
) -> syn::Result<()> {
|
||||
// fixtures(path = "<path>", scripts("<file_1>","<file_2>")) checking `scripts` argument
|
||||
if !matches!(fixtures_type, FixturesType::CustomRelativePath(_)) {
|
||||
return Err(syn::Error::new_spanned(
|
||||
list,
|
||||
"`scripts` must be the second argument of `fixtures` and used together with `path`",
|
||||
));
|
||||
}
|
||||
for nested in list.nested {
|
||||
let litstr = match nested {
|
||||
syn::NestedMeta::Lit(syn::Lit::Str(litstr)) => litstr,
|
||||
other => return Err(syn::Error::new_spanned(other, "expected string literal")),
|
||||
};
|
||||
fixtures_local.push(litstr);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "migrate")]
|
||||
fn add_sql_extension_if_missing(fixture: &mut String) {
|
||||
let has_extension = std::path::Path::new(&fixture).extension().is_some();
|
||||
if !has_extension {
|
||||
fixture.push_str(".sql")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -185,7 +185,13 @@ similarly to migrations but are solely intended to insert test data and be arbit
|
|||
Imagine a basic social app that has users, posts and comments. To test the comment routes, you'd want
|
||||
the database to already have users and posts in it so the comments tests don't have to duplicate that work.
|
||||
|
||||
You can pass a list of fixture names to the attribute like so, and they will be applied in the given order<sup>3</sup>:
|
||||
You can either pass a list of fixture to the attribute `fixtures` in three different operating modes:
|
||||
|
||||
1) Pass a list of references files in `./fixtures` (resolved as `./fixtures/{name}.sql`, `.sql` added only if extension is missing);
|
||||
2) Pass a list of file paths (including associated extension), in which case they can either be absolute, or relative to the current file;
|
||||
3) Pass a `path = <path to folder>` parameter and a `scripts(<filename_1>, <filename_2>, ...)` parameter that are relative to the provided path (resolved as `{path}/{filename_x}.sql`, `.sql` added only if extension is missing).
|
||||
|
||||
In any case they will be applied in the given order<sup>3</sup>:
|
||||
|
||||
```rust,no_run
|
||||
# #[cfg(all(feature = "migrate", feature = "postgres"))]
|
||||
|
@ -195,6 +201,10 @@ You can pass a list of fixture names to the attribute like so, and they will be
|
|||
use sqlx::PgPool;
|
||||
use serde_json::json;
|
||||
|
||||
// Alternatives:
|
||||
// #[sqlx::test(fixtures("./fixtures/users.sql", "./fixtures/users.sql"))]
|
||||
// or
|
||||
// #[sqlx::test(fixtures(path = "./fixtures", scripts("users", "posts")))]
|
||||
#[sqlx::test(fixtures("users", "posts"))]
|
||||
async fn test_create_comment(pool: PgPool) -> sqlx::Result<()> {
|
||||
// See examples/postgres/social-axum-with-tests for a more in-depth example.
|
||||
|
@ -211,7 +221,7 @@ async fn test_create_comment(pool: PgPool) -> sqlx::Result<()> {
|
|||
# }
|
||||
```
|
||||
|
||||
Fixtures are resolved relative to the current file as `./fixtures/{name}.sql`.
|
||||
Multiple `fixtures` attributes can be used to combine different operating modes.
|
||||
|
||||
<sup>3</sup>Ordering for test fixtures is entirely up to the application, and each test may choose which fixtures to
|
||||
apply and which to omit. However, since each fixture is applied separately (sent as a single command string, so wrapped
|
||||
|
|
9
tests/fixtures/mysql/posts.sql
vendored
Normal file
9
tests/fixtures/mysql/posts.sql
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
insert into post(post_id, user_id, content, created_at)
|
||||
values (1,
|
||||
1,
|
||||
'This new computer is lightning-fast!',
|
||||
timestamp(now(), '-1:00:00')),
|
||||
(2,
|
||||
2,
|
||||
'@alice is a haxxor :(',
|
||||
timestamp(now(), '-0:30:00'));
|
2
tests/fixtures/mysql/users.sql
vendored
Normal file
2
tests/fixtures/mysql/users.sql
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
insert into user(user_id, username)
|
||||
values (1, 'alice'), (2, 'bob');
|
14
tests/fixtures/postgres/posts.sql
vendored
Normal file
14
tests/fixtures/postgres/posts.sql
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
insert into post(post_id, user_id, content, created_at)
|
||||
values
|
||||
(
|
||||
'252c1d98-a9b0-4f18-8298-e59058bdfe16',
|
||||
'6592b7c0-b531-4613-ace5-94246b7ce0c3',
|
||||
'This new computer is lightning-fast!',
|
||||
now() + '1 hour ago'::interval
|
||||
),
|
||||
(
|
||||
'844265f7-2472-4689-9a2e-b21f40dbf401',
|
||||
'6592b7c0-b531-4613-ace5-94246b7ce0c3',
|
||||
'@alice is a haxxor :(',
|
||||
now() + '30 minutes ago'::interval
|
||||
);
|
2
tests/fixtures/postgres/users.sql
vendored
Normal file
2
tests/fixtures/postgres/users.sql
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
insert into "user"(user_id, username)
|
||||
values ('6592b7c0-b531-4613-ace5-94246b7ce0c3', 'alice'), ('297923c5-a83c-4052-bab0-030887154e52', 'bob');
|
|
@ -70,6 +70,88 @@ async fn it_gets_posts(pool: MySqlPool) -> sqlx::Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx::test(
|
||||
migrations = "tests/mysql/migrations",
|
||||
fixtures("../fixtures/mysql/users.sql", "../fixtures/mysql/posts.sql")
|
||||
)]
|
||||
async fn it_gets_posts_explicit_fixtures_path(pool: MySqlPool) -> sqlx::Result<()> {
|
||||
let post_contents: Vec<String> =
|
||||
sqlx::query_scalar("SELECT content FROM post ORDER BY created_at")
|
||||
.fetch_all(&pool)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
post_contents,
|
||||
[
|
||||
"This new computer is lightning-fast!",
|
||||
"@alice is a haxxor :("
|
||||
]
|
||||
);
|
||||
|
||||
let comment_exists: bool = sqlx::query_scalar("SELECT exists(SELECT 1 FROM comment)")
|
||||
.fetch_one(&pool)
|
||||
.await?;
|
||||
|
||||
assert!(!comment_exists);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx::test(
|
||||
migrations = "tests/mysql/migrations",
|
||||
fixtures("../fixtures/mysql/users.sql"),
|
||||
fixtures("posts")
|
||||
)]
|
||||
async fn it_gets_posts_mixed_fixtures_path(pool: MySqlPool) -> sqlx::Result<()> {
|
||||
let post_contents: Vec<String> =
|
||||
sqlx::query_scalar("SELECT content FROM post ORDER BY created_at")
|
||||
.fetch_all(&pool)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
post_contents,
|
||||
[
|
||||
"This new computer is lightning-fast!",
|
||||
"@alice is a haxxor :("
|
||||
]
|
||||
);
|
||||
|
||||
let comment_exists: bool = sqlx::query_scalar("SELECT exists(SELECT 1 FROM comment)")
|
||||
.fetch_one(&pool)
|
||||
.await?;
|
||||
|
||||
assert!(!comment_exists);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx::test(
|
||||
migrations = "tests/mysql/migrations",
|
||||
fixtures(path = "../fixtures/mysql", scripts("users", "posts"))
|
||||
)]
|
||||
async fn it_gets_posts_custom_relative_fixtures_path(pool: MySqlPool) -> sqlx::Result<()> {
|
||||
let post_contents: Vec<String> =
|
||||
sqlx::query_scalar("SELECT content FROM post ORDER BY created_at")
|
||||
.fetch_all(&pool)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
post_contents,
|
||||
[
|
||||
"This new computer is lightning-fast!",
|
||||
"@alice is a haxxor :("
|
||||
]
|
||||
);
|
||||
|
||||
let comment_exists: bool = sqlx::query_scalar("SELECT exists(SELECT 1 FROM comment)")
|
||||
.fetch_one(&pool)
|
||||
.await?;
|
||||
|
||||
assert!(!comment_exists);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Try `migrator`
|
||||
#[sqlx::test(migrator = "MIGRATOR", fixtures("users", "posts", "comments"))]
|
||||
async fn it_gets_comments(pool: MySqlPool) -> sqlx::Result<()> {
|
||||
|
|
|
@ -42,6 +42,7 @@ async fn it_gets_users(pool: PgPool) -> sqlx::Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
// This should apply migrations and then fixtures `fixtures/users.sql` and `fixtures/posts.sql`
|
||||
#[sqlx::test(migrations = "tests/postgres/migrations", fixtures("users", "posts"))]
|
||||
async fn it_gets_posts(pool: PgPool) -> sqlx::Result<()> {
|
||||
let post_contents: Vec<String> =
|
||||
|
@ -66,6 +67,91 @@ async fn it_gets_posts(pool: PgPool) -> sqlx::Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
// This should apply migrations and then `../fixtures/postgres/users.sql` and `../fixtures/postgres/posts.sql`
|
||||
#[sqlx::test(
|
||||
migrations = "tests/postgres/migrations",
|
||||
fixtures("../fixtures/postgres/users.sql", "../fixtures/postgres/posts.sql")
|
||||
)]
|
||||
async fn it_gets_posts_explicit_fixtures_path(pool: PgPool) -> sqlx::Result<()> {
|
||||
let post_contents: Vec<String> =
|
||||
sqlx::query_scalar("SELECT content FROM post ORDER BY created_at")
|
||||
.fetch_all(&pool)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
post_contents,
|
||||
[
|
||||
"This new computer is lightning-fast!",
|
||||
"@alice is a haxxor :("
|
||||
]
|
||||
);
|
||||
|
||||
let comment_exists: bool = sqlx::query_scalar("SELECT exists(SELECT 1 FROM comment)")
|
||||
.fetch_one(&pool)
|
||||
.await?;
|
||||
|
||||
assert!(!comment_exists);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// This should apply migrations and then `../fixtures/postgres/users.sql` and `fixtures/posts.sql`
|
||||
#[sqlx::test(
|
||||
migrations = "tests/postgres/migrations",
|
||||
fixtures("../fixtures/postgres/users.sql"),
|
||||
fixtures("posts")
|
||||
)]
|
||||
async fn it_gets_posts_mixed_fixtures_path(pool: PgPool) -> sqlx::Result<()> {
|
||||
let post_contents: Vec<String> =
|
||||
sqlx::query_scalar("SELECT content FROM post ORDER BY created_at")
|
||||
.fetch_all(&pool)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
post_contents,
|
||||
[
|
||||
"This new computer is lightning-fast!",
|
||||
"@alice is a haxxor :("
|
||||
]
|
||||
);
|
||||
|
||||
let comment_exists: bool = sqlx::query_scalar("SELECT exists(SELECT 1 FROM comment)")
|
||||
.fetch_one(&pool)
|
||||
.await?;
|
||||
|
||||
assert!(!comment_exists);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// This should apply migrations and then `../fixtures/postgres/users.sql` and `../fixtures/postgres/posts.sql`
|
||||
#[sqlx::test(
|
||||
migrations = "tests/postgres/migrations",
|
||||
fixtures(path = "../fixtures/postgres", scripts("users.sql", "posts"))
|
||||
)]
|
||||
async fn it_gets_posts_custom_relative_fixtures_path(pool: PgPool) -> sqlx::Result<()> {
|
||||
let post_contents: Vec<String> =
|
||||
sqlx::query_scalar("SELECT content FROM post ORDER BY created_at")
|
||||
.fetch_all(&pool)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
post_contents,
|
||||
[
|
||||
"This new computer is lightning-fast!",
|
||||
"@alice is a haxxor :("
|
||||
]
|
||||
);
|
||||
|
||||
let comment_exists: bool = sqlx::query_scalar("SELECT exists(SELECT 1 FROM comment)")
|
||||
.fetch_one(&pool)
|
||||
.await?;
|
||||
|
||||
assert!(!comment_exists);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Try `migrator`
|
||||
#[sqlx::test(migrator = "MIGRATOR", fixtures("users", "posts", "comments"))]
|
||||
async fn it_gets_comments(pool: PgPool) -> sqlx::Result<()> {
|
||||
|
|
Loading…
Add table
Reference in a new issue