Compare commits

...

7 Commits

Author SHA1 Message Date
Lucas Nogueira
83a9e6c572 Merge remote-tracking branch 'origin/dev' into feat/command-tests 2023-01-04 19:00:48 -03:00
Lucas Nogueira
f7e86798c2 lint 2023-01-04 18:19:15 -03:00
Lucas Nogueira
936d46a41a struct arg test 2023-01-02 15:43:37 -03:00
Lucas Nogueira
a115f91c52 add future test 2023-01-02 15:21:49 -03:00
Lucas Nogueira
2624af9c24 add rename_all test 2022-12-09 09:02:06 -03:00
Lucas Nogueira
b0485d0187 add async command tests, command with arg 2022-12-08 17:02:58 -03:00
Lucas Nogueira
a2724b3e1c basic setup for command tests 2022-12-08 14:15:20 -03:00
7 changed files with 434 additions and 29 deletions

View File

@@ -5,8 +5,8 @@
use std::path::{Path, PathBuf};
use std::{ffi::OsStr, str::FromStr};
use proc_macro2::TokenStream;
use quote::quote;
use proc_macro2::{Group, TokenStream, TokenTree};
use quote::{quote, TokenStreamExt};
use sha2::{Digest, Sha256};
use tauri_utils::assets::AssetKey;
@@ -352,7 +352,7 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
let info_plist_path = out_path.display().to_string();
quote!({
tauri::embed_plist::embed_info_plist!(#info_plist_path);
#root::embed_plist::embed_info_plist!(#info_plist_path);
})
} else {
quote!(())
@@ -396,7 +396,7 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
assets: ::std::sync::Arc::new(#assets),
schema: #schema.into(),
key: #key.into(),
crypto_keys: std::boxed::Box::new(::tauri::utils::pattern::isolation::Keys::new().expect("unable to generate cryptographically secure keys for Tauri \"Isolation\" Pattern")),
crypto_keys: std::boxed::Box::new(#root::utils::pattern::isolation::Keys::new().expect("unable to generate cryptographically secure keys for Tauri \"Isolation\" Pattern")),
})
}
};
@@ -435,7 +435,70 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
#[cfg(not(feature = "shell-scope"))]
let shell_scope_config = quote!();
Ok(quote!(#root::Context::new(
fn compare_token_stream(a: TokenStream, b: TokenStream) -> bool {
if a.clone().into_iter().count() != b.clone().into_iter().count() {
return false;
}
for (a, b) in a.into_iter().zip(b) {
if !compare_token_tree(a, b) {
return false;
}
}
true
}
fn compare_token_tree(a: TokenTree, b: TokenTree) -> bool {
match (a, b) {
(TokenTree::Group(a), TokenTree::Group(b)) => compare_token_stream(a.stream(), b.stream()),
(TokenTree::Ident(a), TokenTree::Ident(b)) => b == a,
(TokenTree::Punct(a), TokenTree::Punct(b)) => a.to_string() == b.to_string(),
(TokenTree::Literal(a), TokenTree::Literal(b)) => a.to_string() == b.to_string(),
_ => false,
}
}
fn change_tree_tauri_root(
token: TokenTree,
previous: &Option<TokenTree>,
new: &mut TokenStream,
) -> bool {
match token {
TokenTree::Ident(i) if i == "utils" => {
new.append_all(quote!(tauri_utils));
false
}
TokenTree::Ident(i) => {
let ignore = match previous {
Some(TokenTree::Punct(p)) if p.as_char() == ':' => i == "tauri",
_ => false,
};
if !ignore {
new.append(i);
}
ignore
}
TokenTree::Group(g) => {
let mut stream = TokenStream::new();
let mut ignore = false;
let mut previous_token = None;
for token in g.stream() {
if ignore && matches!(token, TokenTree::Punct(_)) {
continue;
}
ignore = change_tree_tauri_root(token.clone(), &previous_token, &mut stream);
previous_token.replace(token);
}
new.append(Group::new(g.delimiter(), stream));
false
}
_ => {
new.append(token);
false
}
}
}
let context = quote!(#root::Context::new(
#config,
::std::sync::Arc::new(#assets),
#default_window_icon,
@@ -445,7 +508,23 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
#info_plist,
#pattern,
#shell_scope_config
)))
));
if compare_token_stream(root, quote!(crate)) {
let mut stream = TokenStream::new();
let mut ignore = false;
let mut previous_token = None;
for token in context {
if ignore && matches!(token, TokenTree::Punct(_)) {
continue;
}
ignore = change_tree_tauri_root(token.clone(), &previous_token, &mut stream);
previous_token.replace(token);
}
Ok(stream)
} else {
Ok(context)
}
}
fn ico_icon<P: AsRef<Path>>(

View File

@@ -4,17 +4,18 @@
use heck::{ToLowerCamelCase, ToSnakeCase};
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
use quote::{format_ident, quote};
use syn::{
ext::IdentExt,
parse::{Parse, ParseStream},
parse_macro_input,
spanned::Spanned,
FnArg, Ident, ItemFn, Lit, Meta, Pat, Token, Visibility,
FnArg, ItemFn, Lit, Meta, Pat, Token, Visibility,
};
struct WrapperAttributes {
root: TokenStream2,
execution_context: ExecutionContext,
argument_case: ArgumentCase,
}
@@ -22,6 +23,7 @@ struct WrapperAttributes {
impl Parse for WrapperAttributes {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut wrapper_attributes = WrapperAttributes {
root: quote!(::tauri),
execution_context: ExecutionContext::Blocking,
argument_case: ArgumentCase::Camel,
};
@@ -43,6 +45,11 @@ impl Parse for WrapperAttributes {
}
};
}
} else if v.path.is_ident("root") {
if let Lit::Str(s) = v.lit {
let ident = Ident::new(&s.value(), Span::call_site());
wrapper_attributes.root = quote!(#ident);
}
}
}
Ok(Meta::Path(p)) => {
@@ -104,21 +111,28 @@ pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream {
};
// body to the command wrapper or a `compile_error!` of an error occurred while parsing it.
let body = syn::parse::<WrapperAttributes>(attributes)
let (body, attributes) = syn::parse::<WrapperAttributes>(attributes)
.map(|mut attrs| {
if function.sig.asyncness.is_some() {
attrs.execution_context = ExecutionContext::Async;
}
attrs
})
.and_then(|attrs| match attrs.execution_context {
ExecutionContext::Async => body_async(&function, &invoke, attrs.argument_case),
ExecutionContext::Blocking => body_blocking(&function, &invoke, attrs.argument_case),
.and_then(|attrs| {
let body = match attrs.execution_context {
ExecutionContext::Async => body_async(&function, &invoke, &attrs),
ExecutionContext::Blocking => body_blocking(&function, &invoke, &attrs),
};
body.map(|b| (b, Some(attrs)))
})
.unwrap_or_else(syn::Error::into_compile_error);
.unwrap_or_else(|e| (syn::Error::into_compile_error(e), None));
let Invoke { message, resolver } = invoke;
let root = attributes
.map(|a| a.root)
.unwrap_or_else(|| quote!(::tauri));
// Rely on rust 2018 edition to allow importing a macro from a path.
quote!(
#function
@@ -129,10 +143,10 @@ pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream {
// double braces because the item is expected to be a block expression
($path:path, $invoke:ident) => {{
#[allow(unused_imports)]
use ::tauri::command::private::*;
use #root::command::private::*;
// prevent warnings when the body is a `compile_error!` or if the command has no arguments
#[allow(unused_variables)]
let ::tauri::Invoke { message: #message, resolver: #resolver } = $invoke;
let #root::Invoke { message: #message, resolver: #resolver } = $invoke;
#body
}};
@@ -150,9 +164,13 @@ pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream {
/// See the [`tauri::command`] module for all the items and traits that make this possible.
///
/// [`tauri::command`]: https://docs.rs/tauri/*/tauri/runtime/index.html
fn body_async(function: &ItemFn, invoke: &Invoke, case: ArgumentCase) -> syn::Result<TokenStream2> {
fn body_async(
function: &ItemFn,
invoke: &Invoke,
attributes: &WrapperAttributes,
) -> syn::Result<TokenStream2> {
let Invoke { message, resolver } = invoke;
parse_args(function, message, case).map(|args| {
parse_args(function, message, attributes).map(|args| {
quote! {
#resolver.respond_async_serialized(async move {
let result = $path(#(#args?),*);
@@ -171,10 +189,10 @@ fn body_async(function: &ItemFn, invoke: &Invoke, case: ArgumentCase) -> syn::Re
fn body_blocking(
function: &ItemFn,
invoke: &Invoke,
case: ArgumentCase,
attributes: &WrapperAttributes,
) -> syn::Result<TokenStream2> {
let Invoke { message, resolver } = invoke;
let args = parse_args(function, message, case)?;
let args = parse_args(function, message, attributes)?;
// the body of a `match` to early return any argument that wasn't successful in parsing.
let match_body = quote!({
@@ -193,13 +211,13 @@ fn body_blocking(
fn parse_args(
function: &ItemFn,
message: &Ident,
case: ArgumentCase,
attributes: &WrapperAttributes,
) -> syn::Result<Vec<TokenStream2>> {
function
.sig
.inputs
.iter()
.map(|arg| parse_arg(&function.sig.ident, arg, message, case))
.map(|arg| parse_arg(&function.sig.ident, arg, message, attributes))
.collect()
}
@@ -208,7 +226,7 @@ fn parse_arg(
command: &Ident,
arg: &FnArg,
message: &Ident,
case: ArgumentCase,
attributes: &WrapperAttributes,
) -> syn::Result<TokenStream2> {
// we have no use for self arguments
let mut arg = match arg {
@@ -243,7 +261,7 @@ fn parse_arg(
));
}
match case {
match attributes.argument_case {
ArgumentCase::Camel => {
key = key.to_lower_camel_case();
}
@@ -252,8 +270,10 @@ fn parse_arg(
}
}
Ok(quote!(::tauri::command::CommandArg::from_command(
::tauri::command::CommandItem {
let root = &attributes.root;
Ok(quote!(#root::command::CommandArg::from_command(
#root::command::CommandItem {
name: stringify!(#command),
key: #key,
message: &#message,

View File

@@ -119,10 +119,10 @@ quickcheck = "1.0.3"
quickcheck_macros = "1.0.0"
serde = { version = "1.0", features = [ "derive" ] }
serde_json = "1.0"
tauri = { path = ".", default-features = false, features = [ "wry" ] }
tokio-test = "0.4.2"
tokio = { version = "1", features = [ "full" ] }
cargo_toml = "0.11"
heck = "0.4"
[features]
default = [ "wry", "compression", "objc-exception" ]

View File

@@ -558,6 +558,7 @@ impl<T: UserEvent> Dispatch<T> for MockDispatcher {
}
fn eval_script<S: Into<String>>(&self, script: S) -> Result<()> {
println!("eval {}", script.into());
Ok(())
}

View File

@@ -85,6 +85,10 @@ pub fn mock_context<A: Assets>(assets: A) -> crate::Context<A> {
}
}
pub fn mock_builder() -> crate::Builder<MockRuntime> {
crate::Builder::new()
}
pub fn mock_app() -> crate::App<MockRuntime> {
crate::Builder::<MockRuntime>::new()
.build(mock_context(noop_assets()))

View File

@@ -1599,9 +1599,310 @@ impl<R: Runtime> Window<R> {
#[cfg(test)]
mod tests {
use std::sync::mpsc::{sync_channel, SyncSender};
use super::{Window, WindowBuilder};
use crate::{api::ipc::CallbackFn, test, InvokePayload, State};
use serde_json::Value as JsonValue;
#[test]
fn window_is_send_sync() {
crate::test_utils::assert_send::<super::Window>();
crate::test_utils::assert_sync::<super::Window>();
crate::test_utils::assert_send::<Window>();
crate::test_utils::assert_sync::<Window>();
}
macro_rules! commands {
(fn, $(#[$cmd_meta:meta])*) => {
$(#[$cmd_meta])*
fn cmd_state(channel: State<'_, Channel>) {
println!("cmd state");
channel.tx.send(Response::Cmd).unwrap();
}
$(#[$cmd_meta])*
fn cmd_state_return_val(channel: State<'_, Channel>) -> &'static str {
println!("cmd state return val");
channel.tx.send(Response::CmdReturnVal).unwrap();
STATE_RETURN_VAL_RESPONSE
}
$(#[$cmd_meta])*
fn cmd_state_arg(channel: State<'_, Channel>, int_arg: usize) {
println!("cmd state arg");
channel.tx.send(Response::CmdArg(int_arg)).unwrap();
}
$(#[$cmd_meta])*
fn cmd_state_arg_return_val(channel: State<'_, Channel>, int_arg: usize) -> &'static str {
println!("cmd state arg return val");
channel.tx.send(Response::CmdArgReturnVal(int_arg)).unwrap();
STATE_ARG_RETURN_VAL_RESPONSE
}
};
(async fn, $(#[$cmd_meta:meta])*) => {
$(#[$cmd_meta])*
async fn cmd_state(channel: State<'_, Channel>) -> crate::Result<()> {
println!("cmd state");
channel.tx.send(Response::Cmd).unwrap();
Ok(())
}
$(#[$cmd_meta])*
async fn cmd_state_return_val(channel: State<'_, Channel>) -> crate::Result<&'static str> {
println!("cmd state return val");
channel.tx.send(Response::CmdReturnVal).unwrap();
Ok(STATE_RETURN_VAL_RESPONSE)
}
$(#[$cmd_meta])*
async fn cmd_state_arg(channel: State<'_, Channel>, int_arg: usize) -> crate::Result<()> {
println!("cmd state arg");
channel.tx.send(Response::CmdArg(int_arg)).unwrap();
Ok(())
}
$(#[$cmd_meta])*
async fn cmd_state_arg_return_val(
channel: State<'_, Channel>,
int_arg: usize,
) -> crate::Result<&'static str> {
println!("cmd state arg return val");
channel.tx.send(Response::CmdArgReturnVal(int_arg)).unwrap();
Ok(STATE_ARG_RETURN_VAL_RESPONSE)
}
};
}
macro_rules! command_test {
($test_name: ident, $($fn_kind: ident) +, $(#[$cmd_meta:meta])*, $case: path) => {
#[test]
fn $test_name() {
const STATE_RETURN_VAL_RESPONSE: &str = "return-val";
const STATE_ARG_RETURN_VAL_RESPONSE: &str = "arg-return-val";
const FUTURE_RETURN_VAL_RESPONSE: &str = "future-return-val";
#[derive(Debug, Eq, PartialEq)]
enum Response {
Cmd,
CmdReturnVal,
CmdArg(usize),
CmdArgReturnVal(usize),
CmdFutureReturnVal(usize),
Person(Person),
}
struct Channel {
tx: SyncSender<Response>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq)]
struct Person {
name: String,
age: u8,
}
#[derive(serde::Serialize, serde::Deserialize)]
struct InlinePerson(String, u8);
$(#[$cmd_meta])*
$($fn_kind)* cmd() {
println!("cmd");
}
$(#[$cmd_meta])*
$($fn_kind)* cmd_args_struct(Person { name, age }: Person) {
println!("received person struct with name: {} | age: {}", name, age);
}
$(#[$cmd_meta])*
$($fn_kind)* cmd_args_tuple_struct(InlinePerson(name, age): InlinePerson) {
println!("received person tuple with name: {} | age: {}", name, age);
}
$(#[$cmd_meta])*
$($fn_kind)* cmd_state_args_struct_return_val(
channel: State<'_, Channel>,
person: Person,
) -> crate::Result<()> {
println!("received person struct with name: {} | age: {}", person.name, person.age);
channel.tx.send(Response::Person(person)).unwrap();
Ok(())
}
$(#[$cmd_meta])*
$($fn_kind)* cmd_state_args_tuple_struct_return_val(
channel: State<'_, Channel>,
InlinePerson(name, age): InlinePerson
) -> crate::Result<()> {
println!("received person tuple with name: {} | age: {}", name, age);
channel.tx.send(Response::Person(Person { name, age })).unwrap();
Ok(())
}
#[crate::command(root = "crate", async)]
fn cmd_state_arg_return_future(
channel: State<'_, Channel>,
int_arg: usize,
) -> impl std::future::Future<Output = String> {
println!("cmd state arg return future");
channel.tx.send(Response::CmdFutureReturnVal(int_arg)).unwrap();
std::future::ready(FUTURE_RETURN_VAL_RESPONSE.into())
}
#[crate::command(root = "crate", async)]
fn cmd_state_arg_return_future_result(
channel: State<'_, Channel>,
int_arg: usize,
) -> impl std::future::Future<Output = crate::Result<String>> {
println!("cmd state arg return future result");
channel.tx.send(Response::CmdFutureReturnVal(int_arg)).unwrap();
std::future::ready(Ok(FUTURE_RETURN_VAL_RESPONSE.into()))
}
commands!($($fn_kind)*, $(#[$cmd_meta])*);
let (tx, rx) = sync_channel(1);
let channel = Channel { tx };
let app = test::mock_builder()
.manage(channel)
.invoke_handler(crate::generate_handler![
cmd,
cmd_args_struct,
cmd_args_tuple_struct,
cmd_state_args_struct_return_val,
cmd_state_args_tuple_struct_return_val,
cmd_state,
cmd_state_return_val,
cmd_state_arg,
cmd_state_arg_return_val,
cmd_state_arg_return_future,
cmd_state_arg_return_future_result
])
.build(test::mock_context(test::noop_assets()))
.unwrap();
let window = WindowBuilder::new(&app, "test", Default::default())
.build()
.unwrap();
#[derive(Default)]
struct JsonMap(serde_json::Map<String, JsonValue>);
impl From<JsonMap> for JsonValue {
fn from(map: JsonMap) -> Self {
map.0.into()
}
}
impl JsonMap {
fn insert(mut self, key: impl Into<String>, value: impl serde::Serialize) -> Self {
self.0.insert(key.into(), serde_json::to_value(value).unwrap());
self
}
}
struct UnitTest {
cmd: &'static str,
response: Option<Response>,
arg: Option<JsonValue>,
}
let tests = vec![
UnitTest {
cmd: "cmd",
response: None,
arg: None,
},
UnitTest {
cmd: "cmd_args_struct",
response: None,
arg: Some(JsonMap::default().insert("person", Person { name: "Tauri".into(), age: 1 }).into()),
},
UnitTest {
cmd: "cmd_args_tuple_struct",
response: None,
arg: Some(JsonMap::default().insert($case("inline_person").to_string(), InlinePerson("Tauri".into(), 3)).into()),
},
UnitTest {
cmd: "cmd_state_args_struct_return_val",
response: Some(Response::Person(Person { name: "Tauri".into(), age: 1 })),
arg: Some(JsonMap::default().insert("person", Person { name: "Tauri".into(), age: 1 }).into()),
},
UnitTest {
cmd: "cmd_state_args_tuple_struct_return_val",
response: Some(Response::Person(Person { name: "Tauri".into(), age: 3 })),
arg: Some(JsonMap::default().insert($case("inline_person").to_string(), InlinePerson("Tauri".into(), 3)).into()),
},
UnitTest {
cmd: "cmd_state",
response: Some(Response::Cmd),
arg: None,
},
UnitTest {
cmd: "cmd_state_return_val",
response: Some(Response::CmdReturnVal),
arg: None,
},
UnitTest {
cmd: "cmd_state_arg",
response: Some(Response::CmdArg(1)),
arg: Some(JsonMap::default().insert($case("int_arg").to_string(), 1).into()),
},
UnitTest {
cmd: "cmd_state_arg_return_val",
response: Some(Response::CmdArgReturnVal(2)),
arg: Some(JsonMap::default().insert($case("int_arg").to_string(), 2).into()),
},
UnitTest {
cmd: "cmd_state_arg_return_future",
response: Some(Response::CmdFutureReturnVal(2)),
arg: Some(JsonMap::default().insert("intArg", 2).into()),
},
UnitTest {
cmd: "cmd_state_arg_return_future_result",
response: Some(Response::CmdFutureReturnVal(2)),
arg: Some(JsonMap::default().insert("intArg", 2).into()),
},
];
for test in tests {
window
.clone()
.on_message(InvokePayload {
cmd: test.cmd.into(),
tauri_module: None,
callback: CallbackFn(0),
error: CallbackFn(0),
inner: test.arg.unwrap_or_default(),
})
.unwrap();
if let Some(response) = test.response {
assert_eq!(rx.recv_timeout(std::time::Duration::from_secs(3)).unwrap(), response);
}
}
}
};
}
command_test!(regular_commands, fn, #[crate::command(root = "crate")], heck::AsLowerCamelCase);
command_test!(async_commands, async fn, #[crate::command(root = "crate")], heck::AsLowerCamelCase);
command_test!(
async_attr_commands,
fn,
#[crate::command(root = "crate", async)],
heck::AsLowerCamelCase
);
command_test!(
regular_rename_all_snake_case_commands,
fn,
#[crate::command(root = "crate", rename_all = "snake_case")],
heck::AsSnakeCase
);
command_test!(
regular_rename_all_camel_case_commands,
fn,
#[crate::command(root = "crate", rename_all = "camelCase")],
heck::AsLowerCamelCase
);
}

View File

@@ -61,7 +61,7 @@ fn build_app(
bundle_updater: bool,
target: BundleTarget,
) {
let mut command = Command::new(&cli_bin_path);
let mut command = Command::new(cli_bin_path);
command
.args(["build", "--debug", "--verbose"])
.arg("--config")