feat: Add support IPC socket control over menu.
This commit is contained in:
@@ -24,6 +24,7 @@ fn gen_module(module: &TestModule) -> syn::Result<TokenStream> {
|
||||
TestKind::Filter => gen_filter(case, &module.fixtures)?,
|
||||
TestKind::Nav => gen_nav(case, &module.fixtures)?,
|
||||
TestKind::Menu => gen_menu(case, &module.fixtures)?,
|
||||
TestKind::Ipc => gen_ipc(case, &module.fixtures)?,
|
||||
};
|
||||
test_fns.push(tokens);
|
||||
}
|
||||
@@ -63,6 +64,15 @@ fn gen_imports(kind: TestKind) -> TokenStream {
|
||||
use pikl_core::json_menu::JsonMenu;
|
||||
}
|
||||
}
|
||||
TestKind::Ipc => {
|
||||
quote! {
|
||||
use pikl_core::item::Item;
|
||||
use pikl_core::event::{Action, MenuEvent, MenuResult};
|
||||
use pikl_core::menu::MenuRunner;
|
||||
use pikl_core::json_menu::JsonMenu;
|
||||
use crate::ipc::server::IpcServer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,8 +189,8 @@ fn build_headless_script(actions: &[ActionExpr]) -> String {
|
||||
script.push_str(line);
|
||||
script.push('\n');
|
||||
}
|
||||
ActionExpr::AddItems(_) => {
|
||||
// Not applicable for headless. Items come from stdin.
|
||||
ActionExpr::AddItems(_) | ActionExpr::IpcSend(_) | ActionExpr::IpcExpect(_) => {
|
||||
// Not applicable for headless.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,18 +201,12 @@ fn build_headless_script(actions: &[ActionExpr]) -> String {
|
||||
// Filter
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Generate a filter unit test: create items, push them
|
||||
/// into a `FuzzyFilter`, set the query, and assert on
|
||||
/// matched labels.
|
||||
fn gen_filter(case: &TestCase, fixtures: &Fixtures) -> syn::Result<TokenStream> {
|
||||
let test_name = &case.name;
|
||||
|
||||
let item_exprs = gen_item_constructors(fixtures);
|
||||
|
||||
let query = case.query.as_deref().unwrap_or("");
|
||||
|
||||
let mut asserts = Vec::new();
|
||||
|
||||
if let Some(ref expected) = case.match_labels {
|
||||
asserts.push(quote! {
|
||||
let labels: Vec<&str> = (0..f.matched_count())
|
||||
@@ -235,11 +239,8 @@ fn gen_filter(case: &TestCase, fixtures: &Fixtures) -> syn::Result<TokenStream>
|
||||
// Nav
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Generate a navigation unit test: create a viewport, run
|
||||
/// movement actions, and assert on cursor/offset.
|
||||
fn gen_nav(case: &TestCase, fixtures: &Fixtures) -> syn::Result<TokenStream> {
|
||||
let test_name = &case.name;
|
||||
|
||||
let (height, count) = fixtures.viewport.unwrap_or((10, 20));
|
||||
let height_lit = height;
|
||||
let count_lit = count;
|
||||
@@ -247,22 +248,14 @@ fn gen_nav(case: &TestCase, fixtures: &Fixtures) -> syn::Result<TokenStream> {
|
||||
let action_calls = gen_nav_actions(&case.actions)?;
|
||||
|
||||
let mut asserts = Vec::new();
|
||||
|
||||
if let Some(cursor) = case.cursor {
|
||||
asserts.push(quote! {
|
||||
assert_eq!(
|
||||
v.cursor(), #cursor,
|
||||
"expected cursor {}, got {}", #cursor, v.cursor()
|
||||
);
|
||||
assert_eq!(v.cursor(), #cursor, "expected cursor {}, got {}", #cursor, v.cursor());
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(offset) = case.offset {
|
||||
asserts.push(quote! {
|
||||
assert_eq!(
|
||||
v.scroll_offset(), #offset,
|
||||
"expected offset {}, got {}", #offset, v.scroll_offset()
|
||||
);
|
||||
assert_eq!(v.scroll_offset(), #offset, "expected offset {}, got {}", #offset, v.scroll_offset());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -278,8 +271,6 @@ fn gen_nav(case: &TestCase, fixtures: &Fixtures) -> syn::Result<TokenStream> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert DSL action names to `Viewport` method calls
|
||||
/// (e.g. `move-down` becomes `v.move_down()`).
|
||||
fn gen_nav_actions(actions: &[ActionExpr]) -> syn::Result<Vec<TokenStream>> {
|
||||
let mut calls = Vec::new();
|
||||
for action in actions {
|
||||
@@ -288,12 +279,8 @@ fn gen_nav_actions(actions: &[ActionExpr]) -> syn::Result<Vec<TokenStream>> {
|
||||
let method = Ident::new(&name.replace('-', "_"), Span::call_site());
|
||||
let needs_count = matches!(
|
||||
name.as_str(),
|
||||
"move-up"
|
||||
| "move-down"
|
||||
| "page-up"
|
||||
| "page-down"
|
||||
| "half-page-up"
|
||||
| "half-page-down"
|
||||
"move-up" | "move-down" | "page-up" | "page-down"
|
||||
| "half-page-up" | "half-page-down"
|
||||
);
|
||||
if needs_count {
|
||||
calls.push(quote! { v.#method(1); });
|
||||
@@ -304,10 +291,7 @@ fn gen_nav_actions(actions: &[ActionExpr]) -> syn::Result<Vec<TokenStream>> {
|
||||
_ => {
|
||||
return Err(syn::Error::new(
|
||||
Span::call_site(),
|
||||
format!(
|
||||
"nav tests only support simple actions, got: {:?}",
|
||||
action_debug(action)
|
||||
),
|
||||
format!("nav tests only support simple actions, got: {:?}", action_debug(action)),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -319,73 +303,12 @@ fn gen_nav_actions(actions: &[ActionExpr]) -> syn::Result<Vec<TokenStream>> {
|
||||
// Menu
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Generate an async menu state machine test: create a menu,
|
||||
/// send actions, and assert on the final result (selected
|
||||
/// item or cancellation).
|
||||
fn gen_menu(case: &TestCase, fixtures: &Fixtures) -> syn::Result<TokenStream> {
|
||||
let test_name = &case.name;
|
||||
let item_exprs = gen_item_constructors(fixtures);
|
||||
|
||||
let label_key = fixtures.label_key.as_deref().unwrap_or("label");
|
||||
|
||||
let action_sends = gen_menu_actions(&case.actions)?;
|
||||
|
||||
let result_assert = if case.cancelled {
|
||||
quote! {
|
||||
assert!(
|
||||
matches!(result, Ok(MenuResult::Cancelled)),
|
||||
"expected Cancelled, got: {:?}", result.as_ref().map(|r| format!("{:?}", r))
|
||||
);
|
||||
}
|
||||
} else if let Some(ref expected) = case.selected {
|
||||
quote! {
|
||||
match &result {
|
||||
Ok(MenuResult::Selected { items, .. }) => {
|
||||
assert!(!items.is_empty(), "expected Selected with items, got empty");
|
||||
let value = &items[0].0;
|
||||
let got = value.as_str()
|
||||
.or_else(|| value.get(#label_key).and_then(|v| v.as_str()))
|
||||
.unwrap_or("");
|
||||
assert_eq!(
|
||||
got, #expected,
|
||||
"expected selected {:?}, got value: {:?}", #expected, value
|
||||
);
|
||||
}
|
||||
other => panic!("expected Selected, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
} else if let Some(ref expected) = case.selected_items {
|
||||
quote! {
|
||||
match &result {
|
||||
Ok(MenuResult::Selected { items, .. }) => {
|
||||
let mut labels: Vec<&str> = items.iter()
|
||||
.filter_map(|(v, _)| v.as_str()
|
||||
.or_else(|| v.get(#label_key).and_then(|v| v.as_str())))
|
||||
.collect();
|
||||
labels.sort();
|
||||
let mut expected = vec![#(#expected),*];
|
||||
expected.sort();
|
||||
assert_eq!(
|
||||
labels, expected,
|
||||
"expected selected_items {:?}, got {:?}", expected, labels
|
||||
);
|
||||
}
|
||||
other => panic!("expected Selected, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(syn::Error::new(
|
||||
case.name.span(),
|
||||
format!(
|
||||
"menu test '{}' has no result assertion (expected selected, selected_items, or cancelled)",
|
||||
case.name
|
||||
),
|
||||
));
|
||||
};
|
||||
|
||||
// If test expects cancellation via sender drop (no cancel action, no confirm),
|
||||
// we need to drop tx after sending actions.
|
||||
let drop_sender = quote! { drop(tx); };
|
||||
let result_assert = gen_result_assert(case, label_key)?;
|
||||
|
||||
let multi_setup = if fixtures.multi == Some(true) {
|
||||
quote! { menu.set_multi(true); }
|
||||
@@ -402,18 +325,13 @@ fn gen_menu(case: &TestCase, fixtures: &Fixtures) -> syn::Result<TokenStream> {
|
||||
let mut rx = menu.subscribe();
|
||||
let handle = tokio::spawn(async move { menu.run().await });
|
||||
|
||||
// Wait for initial state broadcast.
|
||||
let _ = rx.recv().await;
|
||||
|
||||
// Give the viewport some height so confirms work.
|
||||
let _ = tx.send(Action::Resize { height: 50 }).await;
|
||||
let _ = rx.recv().await;
|
||||
|
||||
// Send all actions.
|
||||
#(#action_sends)*
|
||||
|
||||
// Drop sender so menu loop can exit.
|
||||
#drop_sender
|
||||
drop(tx);
|
||||
|
||||
let result = handle.await.unwrap_or(Ok(MenuResult::Cancelled));
|
||||
#result_assert
|
||||
@@ -421,8 +339,6 @@ fn gen_menu(case: &TestCase, fixtures: &Fixtures) -> syn::Result<TokenStream> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert DSL actions to `tx.send(Action::...)` calls for
|
||||
/// menu tests.
|
||||
fn gen_menu_actions(actions: &[ActionExpr]) -> syn::Result<Vec<TokenStream>> {
|
||||
let mut sends = Vec::new();
|
||||
for action in actions {
|
||||
@@ -432,24 +348,20 @@ fn gen_menu_actions(actions: &[ActionExpr]) -> syn::Result<Vec<TokenStream>> {
|
||||
quote! { let _ = tx.send(#variant).await; }
|
||||
}
|
||||
ActionExpr::Filter(query) => {
|
||||
quote! {
|
||||
let _ = tx.send(Action::UpdateFilter(#query.to_string())).await;
|
||||
}
|
||||
quote! { let _ = tx.send(Action::UpdateFilter(#query.to_string())).await; }
|
||||
}
|
||||
ActionExpr::AddItems(items) => {
|
||||
let item_exprs: Vec<TokenStream> = items
|
||||
.iter()
|
||||
.map(|s| quote! { serde_json::Value::String(#s.to_string()) })
|
||||
.collect();
|
||||
quote! {
|
||||
let _ = tx.send(Action::AddItems(vec![#(#item_exprs),*])).await;
|
||||
}
|
||||
quote! { let _ = tx.send(Action::AddItems(vec![#(#item_exprs),*])).await; }
|
||||
}
|
||||
ActionExpr::Raw(_) => {
|
||||
return Err(syn::Error::new(
|
||||
Span::call_site(),
|
||||
"raw actions are only supported in headless tests",
|
||||
));
|
||||
return Err(syn::Error::new(Span::call_site(), "raw actions are only supported in headless tests"));
|
||||
}
|
||||
ActionExpr::IpcSend(_) | ActionExpr::IpcExpect(_) => {
|
||||
return Err(syn::Error::new(Span::call_site(), "ipc actions are only supported in ipc tests"));
|
||||
}
|
||||
};
|
||||
sends.push(expr);
|
||||
@@ -457,8 +369,6 @@ fn gen_menu_actions(actions: &[ActionExpr]) -> syn::Result<Vec<TokenStream>> {
|
||||
Ok(sends)
|
||||
}
|
||||
|
||||
/// Map a DSL action name like `"move-down"` to the
|
||||
/// corresponding `Action::MoveDown` token stream.
|
||||
fn menu_action_variant(name: &str) -> syn::Result<TokenStream> {
|
||||
let tokens = match name {
|
||||
"confirm" => quote! { Action::Confirm },
|
||||
@@ -480,37 +390,213 @@ fn menu_action_variant(name: &str) -> syn::Result<TokenStream> {
|
||||
"undo-selection" => quote! { Action::UndoSelection },
|
||||
"redo-selection" => quote! { Action::RedoSelection },
|
||||
_ => {
|
||||
return Err(syn::Error::new(
|
||||
Span::call_site(),
|
||||
format!("unknown menu action: '{name}'"),
|
||||
));
|
||||
return Err(syn::Error::new(Span::call_site(), format!("unknown menu action: '{name}'")));
|
||||
}
|
||||
};
|
||||
Ok(tokens)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// IPC
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Generate an async IPC integration test: create a menu,
|
||||
/// start an IPC server, connect via Unix socket, and send/
|
||||
/// receive JSON commands.
|
||||
fn gen_ipc(case: &TestCase, fixtures: &Fixtures) -> syn::Result<TokenStream> {
|
||||
let test_name = &case.name;
|
||||
let test_name_str = test_name.to_string();
|
||||
let item_exprs = gen_item_constructors(fixtures);
|
||||
let label_key = fixtures.label_key.as_deref().unwrap_or("label");
|
||||
|
||||
let multi_setup = if fixtures.multi == Some(true) {
|
||||
quote! { menu.set_multi(true); }
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
let has_expect = case.actions.iter().any(|a| matches!(a, ActionExpr::IpcExpect(_)));
|
||||
let has_result = case.selected.is_some() || case.selected_items.is_some() || case.cancelled;
|
||||
if !has_expect && !has_result {
|
||||
return Err(syn::Error::new(
|
||||
case.name.span(),
|
||||
format!(
|
||||
"ipc test '{}' has no assertions (expected ipc-expect, selected, selected_items, or cancelled)",
|
||||
case.name
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let ipc_actions = gen_ipc_actions(&case.actions)?;
|
||||
|
||||
let result_assert = if has_result {
|
||||
gen_result_assert(case, label_key)?
|
||||
} else {
|
||||
quote! { let _ = result; }
|
||||
};
|
||||
|
||||
Ok(quote! {
|
||||
#[tokio::test]
|
||||
async fn #test_name() {
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::net::UnixStream;
|
||||
use std::time::Duration;
|
||||
|
||||
let items = vec![#(#item_exprs),*];
|
||||
let (mut menu, tx) = MenuRunner::new(JsonMenu::new(items, #label_key.to_string()));
|
||||
#multi_setup
|
||||
let event_sender = menu.event_sender();
|
||||
let ipc_event_rx = menu.subscribe();
|
||||
|
||||
let handle = tokio::spawn(async move { menu.run().await });
|
||||
|
||||
let _ = tx.send(Action::Resize { height: 50 }).await;
|
||||
tokio::time::sleep(Duration::from_millis(30)).await;
|
||||
|
||||
// Start IPC server on a temp socket
|
||||
let socket_path = std::env::temp_dir().join(
|
||||
format!("pikl-ipc-{}-{}.sock", std::process::id(), #test_name_str)
|
||||
);
|
||||
let _ = std::fs::remove_file(&socket_path);
|
||||
|
||||
let ipc_server = IpcServer::new(
|
||||
socket_path.clone(), tx.clone(), event_sender,
|
||||
);
|
||||
assert!(ipc_server.start(ipc_event_rx).is_ok(), "failed to start IPC server");
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
let stream = match UnixStream::connect(&socket_path).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
drop(tx);
|
||||
drop(ipc_server);
|
||||
let _ = handle.await;
|
||||
assert!(false, "failed to connect to IPC socket: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let (read_half, write_half) = stream.into_split();
|
||||
let mut reader = BufReader::new(read_half);
|
||||
let mut writer = write_half;
|
||||
|
||||
#(#ipc_actions)*
|
||||
|
||||
drop(writer);
|
||||
drop(tx);
|
||||
drop(ipc_server);
|
||||
let result = handle.await.unwrap_or(Ok(MenuResult::Cancelled));
|
||||
#result_assert
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn gen_ipc_actions(actions: &[ActionExpr]) -> syn::Result<Vec<TokenStream>> {
|
||||
let mut stmts = Vec::new();
|
||||
for action in actions {
|
||||
match action {
|
||||
ActionExpr::IpcSend(line) => {
|
||||
stmts.push(quote! {
|
||||
assert!(writer.write_all(#line.as_bytes()).await.is_ok(), "ipc write failed");
|
||||
assert!(writer.write_all(b"\n").await.is_ok(), "ipc write newline failed");
|
||||
assert!(writer.flush().await.is_ok(), "ipc flush failed");
|
||||
tokio::time::sleep(Duration::from_millis(30)).await;
|
||||
});
|
||||
}
|
||||
ActionExpr::IpcExpect(expected) => {
|
||||
stmts.push(quote! {
|
||||
{
|
||||
let mut __resp = String::new();
|
||||
let __read = tokio::time::timeout(
|
||||
Duration::from_secs(5),
|
||||
reader.read_line(&mut __resp)
|
||||
).await;
|
||||
assert!(__read.is_ok(), "IPC response timed out after 5s");
|
||||
let __n = __read.unwrap_or(Ok(0)).unwrap_or(0);
|
||||
assert!(__n > 0, "IPC connection closed before response");
|
||||
assert!(
|
||||
__resp.contains(#expected),
|
||||
"expected IPC response to contain {:?}, got: {}",
|
||||
#expected, __resp.trim()
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
return Err(syn::Error::new(
|
||||
Span::call_site(),
|
||||
format!("ipc tests only support ipc-send and ipc-expect actions, got: {:?}", action_debug(action)),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(stmts)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Generate `Item::from_plain_text("...")` expressions for
|
||||
/// each item in the fixtures.
|
||||
fn gen_item_constructors(fixtures: &Fixtures) -> Vec<TokenStream> {
|
||||
match &fixtures.items {
|
||||
Some(items) => items
|
||||
.iter()
|
||||
.map(|s| quote! { Item::from_plain_text(#s) })
|
||||
.collect(),
|
||||
Some(items) => items.iter().map(|s| quote! { Item::from_plain_text(#s) }).collect(),
|
||||
None => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Format an action expression for use in error messages.
|
||||
fn action_debug(action: &ActionExpr) -> String {
|
||||
match action {
|
||||
ActionExpr::Simple(name) => name.clone(),
|
||||
ActionExpr::Filter(q) => format!("filter \"{q}\""),
|
||||
ActionExpr::Raw(r) => format!("raw \"{r}\""),
|
||||
ActionExpr::AddItems(items) => format!("add-items {:?}", items),
|
||||
ActionExpr::IpcSend(s) => format!("ipc-send \"{s}\""),
|
||||
ActionExpr::IpcExpect(s) => format!("ipc-expect \"{s}\""),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate the result assertion for menu/ipc tests.
|
||||
fn gen_result_assert(case: &TestCase, label_key: &str) -> syn::Result<TokenStream> {
|
||||
if case.cancelled {
|
||||
Ok(quote! {
|
||||
assert!(
|
||||
matches!(result, Ok(MenuResult::Cancelled)),
|
||||
"expected Cancelled, got: {:?}", result.as_ref().map(|r| format!("{:?}", r))
|
||||
);
|
||||
})
|
||||
} else if let Some(ref expected) = case.selected {
|
||||
Ok(quote! {
|
||||
match &result {
|
||||
Ok(MenuResult::Selected { items, .. }) => {
|
||||
assert!(!items.is_empty(), "expected Selected with items, got empty");
|
||||
let value = &items[0].0;
|
||||
let got = value.as_str()
|
||||
.or_else(|| value.get(#label_key).and_then(|v| v.as_str()))
|
||||
.unwrap_or("");
|
||||
assert_eq!(got, #expected, "expected selected {:?}, got value: {:?}", #expected, value);
|
||||
}
|
||||
other => panic!("expected Selected, got: {:?}", other),
|
||||
}
|
||||
})
|
||||
} else if let Some(ref expected) = case.selected_items {
|
||||
Ok(quote! {
|
||||
match &result {
|
||||
Ok(MenuResult::Selected { items, .. }) => {
|
||||
let mut labels: Vec<&str> = items.iter()
|
||||
.filter_map(|(v, _)| v.as_str()
|
||||
.or_else(|| v.get(#label_key).and_then(|v| v.as_str())))
|
||||
.collect();
|
||||
labels.sort();
|
||||
let mut expected = vec![#(#expected),*];
|
||||
expected.sort();
|
||||
assert_eq!(labels, expected, "expected selected_items {:?}, got {:?}", expected, labels);
|
||||
}
|
||||
other => panic!("expected Selected, got: {:?}", other),
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Err(syn::Error::new(
|
||||
case.name.span(),
|
||||
format!("test '{}' has no result assertion (expected selected, selected_items, or cancelled)", case.name),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ pub enum TestKind {
|
||||
Filter,
|
||||
Nav,
|
||||
Menu,
|
||||
Ipc,
|
||||
}
|
||||
|
||||
pub struct Fixtures {
|
||||
@@ -58,6 +59,10 @@ pub enum ActionExpr {
|
||||
Raw(String),
|
||||
/// `add-items ["a", "b", "c"]`
|
||||
AddItems(Vec<String>),
|
||||
/// `ipc-send "json line"` — send a JSON command over the IPC socket
|
||||
IpcSend(String),
|
||||
/// `ipc-expect "text"` — read a response line and assert it contains text
|
||||
IpcExpect(String),
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -300,6 +305,14 @@ fn parse_action_expr(input: ParseStream) -> syn::Result<ActionExpr> {
|
||||
let items = parse_string_list(input)?;
|
||||
Ok(ActionExpr::AddItems(items))
|
||||
}
|
||||
"ipc-send" => {
|
||||
let val: LitStr = input.parse()?;
|
||||
Ok(ActionExpr::IpcSend(val.value()))
|
||||
}
|
||||
"ipc-expect" => {
|
||||
let val: LitStr = input.parse()?;
|
||||
Ok(ActionExpr::IpcExpect(val.value()))
|
||||
}
|
||||
_ => Ok(ActionExpr::Simple(name)),
|
||||
}
|
||||
}
|
||||
@@ -317,9 +330,10 @@ fn parse_kind(input: ParseStream) -> syn::Result<TestKind> {
|
||||
"filter" => Ok(TestKind::Filter),
|
||||
"nav" => Ok(TestKind::Nav),
|
||||
"menu" => Ok(TestKind::Menu),
|
||||
"ipc" => Ok(TestKind::Ipc),
|
||||
other => Err(syn::Error::new(
|
||||
ident.span(),
|
||||
format!("unknown test kind '{other}', expected headless, filter, nav, or menu"),
|
||||
format!("unknown test kind '{other}', expected headless, filter, nav, menu, or ipc"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user