use std::any::Any;
use std::env;
use std::fmt::{Debug, Display};
use std::panic::{self, Location, UnwindSafe};
use std::process::Command;
use std::str::FromStr;
use std::sync::{Arc, OnceLock};
use std::thread;
use crate::IntoResult;
pub fn plugin_body<F, R>(test_body: F)
where
F: FnOnce() -> R + UnwindSafe,
R: IntoResult<()>,
R::Error: Display,
{
let panic_info: Arc<OnceLock<PanicInfo>> = Arc::default();
{
let panic_info = panic_info.clone();
panic::set_hook(Box::new(move |info| {
let _ = panic_info.set(info.into());
}));
}
let result = match panic::catch_unwind(|| test_body().into_result()) {
Ok(Ok(())) => Ok(()),
Ok(Err(err)) => Err(Failure::Error(err.to_string())),
Err(_) => Err(Failure::Panic(panic_info.get().unwrap().clone())),
};
exit(result);
}
#[cfg(feature = "test-terminator")]
pub fn plugin_body_with_terminator<F>(test_body: F)
where
F: FnOnce(super::terminator::TestTerminator),
{
let lock = Arc::new(OnceLock::<Result<(), Failure>>::new());
let handle = {
let lock = lock.clone();
crate::libuv::AsyncHandle::new(move || {
let result = lock.get().unwrap().clone();
crate::schedule(move |()| exit(result));
Ok::<_, std::convert::Infallible>(())
})
}
.unwrap();
test_body(super::terminator::TestTerminator { lock, handle });
}
pub fn test_body(
manifest_path: &str,
plugin_name: &str,
extra_cmd: Option<&str>,
) -> Result<(), String> {
panic::set_hook(Box::new(move |info| {
let mut info = info
.payload()
.downcast_ref::<PanicInfo>()
.cloned()
.unwrap_or_else(|| info.into());
if let Some(thread) = thread::current().name() {
if !thread.is_empty() {
info.thread = thread.to_owned();
}
}
eprintln!("{info}");
}));
let output = run_nvim_command(manifest_path, plugin_name, extra_cmd)?
.output()
.map_err(|err| err.to_string())?;
let stdout = String::from_utf8_lossy(&output.stdout);
let stdout = stdout.trim();
if !stdout.is_empty() {
println!("{stdout}");
}
if output.status.success() {
return Ok(());
}
let stderr = String::from_utf8_lossy(&output.stderr);
let stderr = stderr.trim();
if stderr.is_empty() {
let msg = output
.status
.code()
.map(|i| format!("Neovim exited with non-zero exit code: {i}"))
.unwrap_or_else(|| String::from("Neovim segfaulted"));
return Err(msg);
}
let Ok(failure) = Failure::from_str(stderr) else {
return Err(stderr.to_owned());
};
match failure {
Failure::Error(err) => Err(err),
Failure::Panic(info) => panic::panic_any(info),
}
}
fn exit(result: Result<(), Failure>) {
let exec = |cmd: &str| {
let opts = crate::api::opts::ExecOpts::builder().output(false).build();
crate::api::exec2(cmd, &opts).unwrap();
};
if let Err(failure) = result {
eprintln!("{failure}");
exec("cquit 1");
} else {
exec("qall!");
}
}
fn run_nvim_command(
manifest_path: &str,
plugin_name: &str,
extra_cmd: Option<&str>,
) -> Result<Command, String> {
let manifest = super::build::CargoManifest::from_path(manifest_path)
.map_err(|err| err.to_string())?;
let profile =
env::var(manifest.profile_env()).map_err(|err| err.to_string())?;
let library_path = manifest.library_path(&profile);
if !library_path.exists() {
return Err(format!(
"couldn't find library at '{library_path}'. Did you forget to \
use the build script?",
));
}
let load_library = format!(
"lua local f = package.loadlib([[{library_path}]], \
'luaopen_{plugin_name}'); f()",
);
let mut command = Command::new("nvim");
command
.args(["-u", "NONE", "--headless"])
.args(["-i", "NONE"])
.args(["-c", "set noswapfile"])
.args(extra_cmd.map(|cmd| ["-c", cmd]).unwrap_or_default())
.args(["-c", &load_library]);
Ok(command)
}
#[derive(Clone)]
pub(super) struct PanicInfo {
msg: String,
thread: String,
file: Option<String>,
line: Option<u32>,
column: Option<u32>,
}
impl Debug for PanicInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "panic:{}", self.msg)?;
write!(f, "\nthread:{}", self.thread)?;
if let Some(file) = &self.file {
write!(f, "\nfile:{file}")?;
}
if let Some(line) = self.line {
write!(f, "\nline:{line}")?;
}
if let Some(column) = self.column {
write!(f, "\ncolumn:{column}")?;
}
Ok(())
}
}
impl Display for PanicInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "thread '{}' panicked", self.thread)?;
if let Some(file) = &self.file {
write!(f, " at {file}")?;
if let (Some(line), Some(col)) = (self.line, self.column) {
write!(f, ":{line}:{col}")?;
}
}
write!(f, ":\n{}", self.msg)?;
Ok(())
}
}
impl FromStr for PanicInfo {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut info = PanicInfo {
msg: String::new(),
thread: String::new(),
file: None,
line: None,
column: None,
};
let (_, s) = s.split_once("panic:").ok_or(())?;
let (msg, s) = s.split_once("thread:").ok_or(())?;
info.msg = msg.trim().to_owned();
let (thread, s) = s.split_once("file:").ok_or(())?;
info.thread = thread.trim().to_owned();
let (file, s) = s.split_once("line:").ok_or(())?;
info.file = Some(file.trim().to_owned());
let (line, s) = s.split_once("column:").ok_or(())?;
info.line = Some(line.trim().parse().map_err(|_| ())?);
let column = s.trim().parse().map_err(|_| ())?;
info.column = Some(column);
Ok(info)
}
}
impl From<&panic::PanicHookInfo<'_>> for PanicInfo {
fn from(info: &panic::PanicHookInfo) -> Self {
let payload = info.payload();
let msg = downcast_display::<&str>(payload)
.or_else(|| downcast_display::<String>(payload))
.or_else(|| downcast_display::<&String>(payload))
.map(ToString::to_string)
.unwrap_or_default();
let current_thread = thread::current();
let thread = match current_thread.name() {
Some(name) if !name.is_empty() => name,
_ => "<unnamed>",
};
Self {
msg,
thread: thread.to_owned(),
file: info.location().map(|l| l.file().to_owned()),
line: info.location().map(Location::line),
column: info.location().map(Location::column),
}
}
}
#[derive(Clone)]
pub(super) enum Failure {
Error(String),
Panic(PanicInfo),
}
impl Display for Failure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Failure::Error(err) => write!(f, "error:{err}"),
Failure::Panic(info) => write!(f, "{info:?}"),
}
}
}
impl FromStr for Failure {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.split_once("error:") {
Some((_, msg)) => Ok(Failure::Error(msg.trim().to_owned())),
None => PanicInfo::from_str(s).map(Self::Panic),
}
}
}
#[cfg(feature = "test-terminator")]
impl<E: Display> From<super::terminator::TestFailure<'_, E>> for Failure {
fn from(err: super::terminator::TestFailure<'_, E>) -> Self {
match err {
super::terminator::TestFailure::Error(err) => {
Self::Error(err.to_string())
},
super::terminator::TestFailure::Panic(info) => {
Self::Panic(info.into())
},
}
}
}
fn downcast_display<T: Any + Display>(
value: &dyn Any,
) -> Option<&dyn Display> {
value.downcast_ref::<T>().map(|msg| msg as &dyn Display)
}