test: Add integration tests and demo script.
This commit is contained in:
52
crates/pikl/tests/common/mod.rs
Normal file
52
crates/pikl/tests/common/mod.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use std::io::Write;
|
||||
use std::os::unix::io::FromRawFd;
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
/// Spawn pikl with items on stdin and an action script on fd 3.
|
||||
/// Returns (stdout, stderr, exit_code).
|
||||
pub fn run_pikl(items: &str, script: &str, extra_args: &[&str]) -> (String, String, i32) {
|
||||
let mut fds = [0i32; 2];
|
||||
unsafe { libc::pipe(fds.as_mut_ptr()) };
|
||||
let [read_fd, write_fd] = fds;
|
||||
|
||||
let mut write_file = unsafe { std::fs::File::from_raw_fd(write_fd) };
|
||||
write_file.write_all(script.as_bytes()).unwrap_or_default();
|
||||
drop(write_file);
|
||||
|
||||
let target_fd = 3i32;
|
||||
let mut cmd = Command::new(env!("CARGO_BIN_EXE_pikl"));
|
||||
cmd.args(["--action-fd", "3"])
|
||||
.args(extra_args)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
unsafe {
|
||||
cmd.pre_exec(move || {
|
||||
if read_fd != target_fd {
|
||||
libc::dup2(read_fd, target_fd);
|
||||
libc::close(read_fd);
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
let Ok(mut child) = cmd.spawn() else {
|
||||
return ("".to_string(), "failed to spawn pikl".to_string(), -1);
|
||||
};
|
||||
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
stdin.write_all(items.as_bytes()).unwrap_or_default();
|
||||
}
|
||||
|
||||
let Ok(output) = child.wait_with_output() else {
|
||||
return ("".to_string(), "failed to wait for pikl".to_string(), -1);
|
||||
};
|
||||
|
||||
let stdout = String::from_utf8(output.stdout).unwrap_or_default();
|
||||
let stderr = String::from_utf8(output.stderr).unwrap_or_default();
|
||||
let code = output.status.code().unwrap_or(-1);
|
||||
|
||||
(stdout, stderr, code)
|
||||
}
|
||||
69
crates/pikl/tests/headless.rs
Normal file
69
crates/pikl/tests/headless.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
#![cfg(unix)]
|
||||
|
||||
mod common;
|
||||
|
||||
#[test]
|
||||
fn headless_confirm_first() {
|
||||
let (stdout, _stderr, code) = common::run_pikl("alpha\nbeta\n", "confirm\n", &[]);
|
||||
assert_eq!(code, 0, "expected exit 0, stderr: {_stderr}");
|
||||
assert!(
|
||||
stdout.contains("alpha"),
|
||||
"expected alpha in stdout, got: {stdout}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn headless_move_then_confirm() {
|
||||
let (stdout, _stderr, code) = common::run_pikl("alpha\nbeta\n", "move-down\nconfirm\n", &[]);
|
||||
assert_eq!(code, 0, "expected exit 0, stderr: {_stderr}");
|
||||
assert!(
|
||||
stdout.contains("beta"),
|
||||
"expected beta in stdout, got: {stdout}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn headless_filter_then_confirm() {
|
||||
let (stdout, _stderr, code) =
|
||||
common::run_pikl("alpha\nbeta\nbanana\n", "filter ban\nconfirm\n", &[]);
|
||||
assert_eq!(code, 0, "expected exit 0, stderr: {_stderr}");
|
||||
assert!(
|
||||
stdout.contains("banana"),
|
||||
"expected banana in stdout, got: {stdout}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn headless_cancel() {
|
||||
let (_stdout, _stderr, code) = common::run_pikl("alpha\n", "cancel\n", &[]);
|
||||
assert_eq!(code, 1, "expected exit 1 on cancel");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn headless_empty_script_cancels() {
|
||||
let (_stdout, _stderr, code) = common::run_pikl("alpha\n", "", &[]);
|
||||
assert_eq!(
|
||||
code, 1,
|
||||
"expected exit 1 when script is empty (sender dropped → cancelled)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn headless_invalid_script_exits_2() {
|
||||
let (_stdout, stderr, code) = common::run_pikl("alpha\n", "bogus\n", &[]);
|
||||
assert_eq!(code, 2, "expected exit 2 on invalid script");
|
||||
assert!(
|
||||
stderr.contains("unknown action"),
|
||||
"expected error diagnostic, got: {stderr}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn headless_actions_after_show_ui_exits_2() {
|
||||
let (_stdout, stderr, code) = common::run_pikl("alpha\n", "show-ui\nconfirm\n", &[]);
|
||||
assert_eq!(code, 2, "expected exit 2 for actions after show-ui");
|
||||
assert!(
|
||||
stderr.contains("actions after show-ui"),
|
||||
"expected show-ui error, got: {stderr}"
|
||||
);
|
||||
}
|
||||
269
crates/pikl/tests/headless_dsl.rs
Normal file
269
crates/pikl/tests/headless_dsl.rs
Normal file
@@ -0,0 +1,269 @@
|
||||
#![cfg(unix)]
|
||||
|
||||
mod common;
|
||||
use common::run_pikl;
|
||||
|
||||
use pikl_test_macros::pikl_tests;
|
||||
|
||||
pikl_tests! {
|
||||
headless mod basic_selection {
|
||||
items: ["alpha", "beta", "charlie"];
|
||||
|
||||
test confirm_first {
|
||||
actions: [confirm]
|
||||
stdout: "alpha"
|
||||
exit: 0
|
||||
}
|
||||
|
||||
test move_down_and_confirm {
|
||||
actions: [move-down, confirm]
|
||||
stdout: "beta"
|
||||
exit: 0
|
||||
}
|
||||
|
||||
test move_to_third {
|
||||
actions: [move-down, move-down, confirm]
|
||||
stdout: "charlie"
|
||||
exit: 0
|
||||
}
|
||||
|
||||
test cancel_exits_1 {
|
||||
actions: [cancel]
|
||||
stdout: ""
|
||||
exit: 1
|
||||
}
|
||||
|
||||
test empty_script_cancels {
|
||||
actions: []
|
||||
exit: 1
|
||||
}
|
||||
}
|
||||
|
||||
headless mod filtering {
|
||||
items: ["alpha", "beta", "banana"];
|
||||
|
||||
test filter_then_confirm {
|
||||
actions: [filter "ban", confirm]
|
||||
stdout: "banana"
|
||||
exit: 0
|
||||
}
|
||||
}
|
||||
|
||||
headless mod errors {
|
||||
items: ["one", "two"];
|
||||
|
||||
test invalid_action {
|
||||
actions: [raw "bogus"]
|
||||
stderr contains: "unknown action"
|
||||
exit: 2
|
||||
}
|
||||
|
||||
test actions_after_show_ui {
|
||||
actions: [raw "show-ui", raw "confirm"]
|
||||
stderr contains: "actions after show-ui"
|
||||
exit: 2
|
||||
}
|
||||
}
|
||||
|
||||
// ── Demo scenario tests ──────────────────────────────
|
||||
// These cover the scenarios from examples/demo.sh so
|
||||
// we catch regressions in the demo workflows.
|
||||
|
||||
headless mod demo_plain_text {
|
||||
items: [
|
||||
"apple", "banana", "cherry", "date",
|
||||
"elderberry", "fig", "grape", "honeydew"
|
||||
];
|
||||
|
||||
test confirm_first {
|
||||
actions: [confirm]
|
||||
stdout: "apple"
|
||||
exit: 0
|
||||
}
|
||||
|
||||
test navigate_to_last {
|
||||
actions: [move-to-bottom, confirm]
|
||||
stdout: "honeydew"
|
||||
exit: 0
|
||||
}
|
||||
|
||||
test filter_then_confirm {
|
||||
actions: [filter "cher", confirm]
|
||||
stdout: "cherry"
|
||||
exit: 0
|
||||
}
|
||||
|
||||
test filter_narrows_to_one {
|
||||
actions: [filter "elder", confirm]
|
||||
stdout: "elderberry"
|
||||
exit: 0
|
||||
}
|
||||
|
||||
test cancel {
|
||||
actions: [cancel]
|
||||
exit: 1
|
||||
}
|
||||
}
|
||||
|
||||
headless mod demo_big_list {
|
||||
// Bare numbers parse as JSON numbers with empty labels,
|
||||
// so we prefix with "item-" to keep them as plain text.
|
||||
items: [
|
||||
"item-1", "item-2", "item-3", "item-4", "item-5",
|
||||
"item-6", "item-7", "item-8", "item-9", "item-10",
|
||||
"item-11", "item-12", "item-13", "item-14", "item-15",
|
||||
"item-16", "item-17", "item-18", "item-19", "item-20",
|
||||
"item-50", "item-100", "item-200", "item-499", "item-500"
|
||||
];
|
||||
|
||||
test confirm_first {
|
||||
actions: [confirm]
|
||||
stdout: "item-1"
|
||||
exit: 0
|
||||
}
|
||||
|
||||
test move_to_bottom {
|
||||
actions: [move-to-bottom, confirm]
|
||||
stdout: "item-500"
|
||||
exit: 0
|
||||
}
|
||||
|
||||
test filter_exact {
|
||||
actions: [filter "item-499", confirm]
|
||||
stdout: "item-499"
|
||||
exit: 0
|
||||
}
|
||||
|
||||
test page_down_then_confirm {
|
||||
actions: [page-down, confirm]
|
||||
exit: 0
|
||||
}
|
||||
}
|
||||
|
||||
headless mod demo_json_objects {
|
||||
items: [
|
||||
"{\"label\": \"Arch Linux\", \"category\": \"rolling\", \"init\": \"systemd\"}",
|
||||
"{\"label\": \"NixOS\", \"category\": \"rolling\", \"init\": \"systemd\"}",
|
||||
"{\"label\": \"Void Linux\", \"category\": \"rolling\", \"init\": \"runit\"}",
|
||||
"{\"label\": \"Debian\", \"category\": \"stable\", \"init\": \"systemd\"}",
|
||||
"{\"label\": \"Alpine\", \"category\": \"stable\", \"init\": \"openrc\"}",
|
||||
"{\"label\": \"Fedora\", \"category\": \"semi-rolling\", \"init\": \"systemd\"}",
|
||||
"{\"label\": \"Gentoo\", \"category\": \"rolling\", \"init\": \"openrc\"}"
|
||||
];
|
||||
|
||||
test confirm_first {
|
||||
actions: [confirm]
|
||||
stdout: "Arch Linux"
|
||||
exit: 0
|
||||
}
|
||||
|
||||
test preserves_json_fields {
|
||||
actions: [confirm]
|
||||
stdout: "rolling"
|
||||
exit: 0
|
||||
}
|
||||
|
||||
test filter_by_label {
|
||||
actions: [filter "Void", confirm]
|
||||
stdout: "Void Linux"
|
||||
exit: 0
|
||||
}
|
||||
|
||||
test navigate_to_debian {
|
||||
actions: [move-down, move-down, move-down, confirm]
|
||||
stdout: "Debian"
|
||||
exit: 0
|
||||
}
|
||||
|
||||
test filter_and_navigate {
|
||||
actions: [filter "Linux", move-down, confirm]
|
||||
stdout: "Void Linux"
|
||||
exit: 0
|
||||
}
|
||||
|
||||
test cancel {
|
||||
actions: [cancel]
|
||||
exit: 1
|
||||
}
|
||||
}
|
||||
|
||||
headless mod demo_custom_label_key {
|
||||
items: [
|
||||
"{\"name\": \"Neovim\", \"type\": \"editor\", \"lang\": \"C/Lua\"}",
|
||||
"{\"name\": \"Helix\", \"type\": \"editor\", \"lang\": \"Rust\"}",
|
||||
"{\"name\": \"Kakoune\", \"type\": \"editor\", \"lang\": \"C++\"}",
|
||||
"{\"name\": \"Emacs\", \"type\": \"editor\", \"lang\": \"Lisp\"}",
|
||||
"{\"name\": \"Vim\", \"type\": \"editor\", \"lang\": \"C\"}"
|
||||
];
|
||||
label_key: "name";
|
||||
|
||||
test confirm_first {
|
||||
actions: [confirm]
|
||||
stdout: "Neovim"
|
||||
exit: 0
|
||||
}
|
||||
|
||||
test filter_by_name {
|
||||
actions: [filter "Hel", confirm]
|
||||
stdout: "Helix"
|
||||
exit: 0
|
||||
}
|
||||
|
||||
test preserves_lang_field {
|
||||
actions: [move-down, confirm]
|
||||
stdout: "Rust"
|
||||
exit: 0
|
||||
}
|
||||
|
||||
test navigate_to_last {
|
||||
actions: [move-to-bottom, confirm]
|
||||
stdout: "Vim"
|
||||
exit: 0
|
||||
}
|
||||
}
|
||||
|
||||
headless mod demo_mixed_input {
|
||||
items: [
|
||||
"just a plain string",
|
||||
"{\"label\": \"a json object\", \"extra\": 42}",
|
||||
"another plain string",
|
||||
"{\"label\": \"second object\", \"extra\": 99}"
|
||||
];
|
||||
|
||||
test confirm_plain_text {
|
||||
actions: [confirm]
|
||||
stdout: "just a plain string"
|
||||
exit: 0
|
||||
}
|
||||
|
||||
test confirm_json_object {
|
||||
actions: [move-down, confirm]
|
||||
stdout: "a json object"
|
||||
exit: 0
|
||||
}
|
||||
|
||||
test json_preserves_extra {
|
||||
actions: [move-down, confirm]
|
||||
stdout: "42"
|
||||
exit: 0
|
||||
}
|
||||
|
||||
test navigate_to_second_plain {
|
||||
actions: [move-down, move-down, confirm]
|
||||
stdout: "another plain string"
|
||||
exit: 0
|
||||
}
|
||||
|
||||
test navigate_to_second_json {
|
||||
actions: [move-down, move-down, move-down, confirm]
|
||||
stdout: "second object"
|
||||
exit: 0
|
||||
}
|
||||
|
||||
test filter_across_types {
|
||||
actions: [filter "plain", confirm]
|
||||
stdout: "just a plain string"
|
||||
exit: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user