nushell/crates/nu-plugin-engine/src/interface/tests.rs
Jack Wright d081e3386f
Make pipeline metadata available to plugins (#13495)
# Description
Fixes an issue with pipeline metadata not being passed to plugins.
2024-08-02 11:01:20 -07:00

1524 lines
46 KiB
Rust

use super::{
Context, PluginCallState, PluginInterface, PluginInterfaceManager, ReceivedPluginCallMessage,
};
use crate::{
context::PluginExecutionBogusContext, interface::CurrentCallState,
plugin_custom_value_with_source::WithSource, test_util::*, PluginCustomValueWithSource,
PluginSource,
};
use nu_plugin_core::{interface_test_util::TestCase, Interface, InterfaceManager};
use nu_plugin_protocol::{
test_util::{expected_test_custom_value, test_plugin_custom_value},
ByteStreamInfo, CallInfo, CustomValueOp, EngineCall, EngineCallResponse, EvaluatedCall,
ListStreamInfo, PipelineDataHeader, PluginCall, PluginCallId, PluginCallResponse,
PluginCustomValue, PluginInput, PluginOutput, Protocol, ProtocolInfo, StreamData,
StreamMessage,
};
use nu_protocol::{
ast::{Math, Operator},
engine::Closure,
ByteStreamType, CustomValue, DataSource, IntoInterruptiblePipelineData, IntoSpanned,
PipelineData, PipelineMetadata, PluginMetadata, PluginSignature, ShellError, Signals, Span,
Spanned, Value,
};
use serde::{Deserialize, Serialize};
use std::{
sync::{mpsc, Arc},
time::Duration,
};
#[test]
fn manager_consume_all_consumes_messages() -> Result<(), ShellError> {
let mut test = TestCase::new();
let mut manager = test.plugin("test");
// This message should be non-problematic
test.add(PluginOutput::Hello(ProtocolInfo::default()));
manager.consume_all(&mut test)?;
assert!(!test.has_unconsumed_read());
Ok(())
}
#[test]
fn manager_consume_all_exits_after_streams_and_interfaces_are_dropped() -> Result<(), ShellError> {
let mut test = TestCase::new();
let mut manager = test.plugin("test");
// Add messages that won't cause errors
for _ in 0..5 {
test.add(PluginOutput::Hello(ProtocolInfo::default()));
}
// Create a stream...
let stream = manager.read_pipeline_data(
PipelineDataHeader::list_stream(ListStreamInfo::new(0, Span::test_data())),
&Signals::empty(),
)?;
// and an interface...
let interface = manager.get_interface();
// Expect that is_finished is false
assert!(
!manager.is_finished(),
"is_finished is true even though active stream/interface exists"
);
// After dropping, it should be true
drop(stream);
drop(interface);
assert!(
manager.is_finished(),
"is_finished is false even though manager has no stream or interface"
);
// When it's true, consume_all shouldn't consume everything
manager.consume_all(&mut test)?;
assert!(
test.has_unconsumed_read(),
"consume_all consumed the messages"
);
Ok(())
}
fn test_io_error() -> ShellError {
ShellError::IOError {
msg: "test io error".into(),
}
}
fn check_test_io_error(error: &ShellError) {
assert!(
format!("{error:?}").contains("test io error"),
"error: {error}"
);
}
#[test]
fn manager_consume_all_propagates_io_error_to_readers() -> Result<(), ShellError> {
let mut test = TestCase::new();
let mut manager = test.plugin("test");
test.set_read_error(test_io_error());
let stream = manager.read_pipeline_data(
PipelineDataHeader::list_stream(ListStreamInfo::new(0, Span::test_data())),
&Signals::empty(),
)?;
manager
.consume_all(&mut test)
.expect_err("consume_all did not error");
// Ensure end of stream
drop(manager);
let value = stream.into_iter().next().expect("stream is empty");
if let Value::Error { error, .. } = value {
check_test_io_error(&error);
Ok(())
} else {
panic!("did not get an error");
}
}
fn invalid_output() -> PluginOutput {
// This should definitely cause an error, as 0.0.0 is not compatible with any version other than
// itself
PluginOutput::Hello(ProtocolInfo {
protocol: Protocol::NuPlugin,
version: "0.0.0".into(),
features: vec![],
})
}
fn check_invalid_output_error(error: &ShellError) {
// the error message should include something about the version...
assert!(format!("{error:?}").contains("0.0.0"), "error: {error}");
}
#[test]
fn manager_consume_all_propagates_message_error_to_readers() -> Result<(), ShellError> {
let mut test = TestCase::new();
let mut manager = test.plugin("test");
test.add(invalid_output());
let stream = manager.read_pipeline_data(
PipelineDataHeader::byte_stream(ByteStreamInfo::new(
0,
Span::test_data(),
ByteStreamType::Unknown,
)),
&Signals::empty(),
)?;
manager
.consume_all(&mut test)
.expect_err("consume_all did not error");
// Ensure end of stream
drop(manager);
let value = stream.into_iter().next().expect("stream is empty");
if let Value::Error { error, .. } = value {
check_invalid_output_error(&error);
Ok(())
} else {
panic!("did not get an error");
}
}
fn fake_plugin_call(
manager: &mut PluginInterfaceManager,
id: PluginCallId,
) -> mpsc::Receiver<ReceivedPluginCallMessage> {
// Set up a fake plugin call subscription
let (tx, rx) = mpsc::channel();
manager.plugin_call_states.insert(
id,
PluginCallState {
sender: Some(tx),
dont_send_response: false,
signals: Signals::empty(),
context_rx: None,
span: None,
keep_plugin_custom_values: mpsc::channel(),
remaining_streams_to_read: 0,
},
);
rx
}
#[test]
fn manager_consume_all_propagates_io_error_to_plugin_calls() -> Result<(), ShellError> {
let mut test = TestCase::new();
let mut manager = test.plugin("test");
let interface = manager.get_interface();
test.set_read_error(test_io_error());
// Set up a fake plugin call subscription
let rx = fake_plugin_call(&mut manager, 0);
manager
.consume_all(&mut test)
.expect_err("consume_all did not error");
let message = rx.try_recv().expect("failed to get plugin call message");
match message {
ReceivedPluginCallMessage::Error(error) => {
check_test_io_error(&error);
}
_ => panic!("received something other than an error: {message:?}"),
}
// Check that further calls also cause the error
match interface.get_signature() {
Ok(_) => panic!("plugin call after exit did not cause error somehow"),
Err(err) => {
check_test_io_error(&err);
Ok(())
}
}
}
#[test]
fn manager_consume_all_propagates_message_error_to_plugin_calls() -> Result<(), ShellError> {
let mut test = TestCase::new();
let mut manager = test.plugin("test");
let interface = manager.get_interface();
test.add(invalid_output());
// Set up a fake plugin call subscription
let rx = fake_plugin_call(&mut manager, 0);
manager
.consume_all(&mut test)
.expect_err("consume_all did not error");
let message = rx.try_recv().expect("failed to get plugin call message");
match message {
ReceivedPluginCallMessage::Error(error) => {
check_invalid_output_error(&error);
}
_ => panic!("received something other than an error: {message:?}"),
}
// Check that further calls also cause the error
match interface.get_signature() {
Ok(_) => panic!("plugin call after exit did not cause error somehow"),
Err(err) => {
check_invalid_output_error(&err);
Ok(())
}
}
}
#[test]
fn manager_consume_sets_protocol_info_on_hello() -> Result<(), ShellError> {
let mut manager = TestCase::new().plugin("test");
let info = ProtocolInfo::default();
manager.consume(PluginOutput::Hello(info.clone()))?;
let set_info = manager
.state
.protocol_info
.try_get()?
.expect("protocol info not set");
assert_eq!(info.version, set_info.version);
Ok(())
}
#[test]
fn manager_consume_errors_on_wrong_nushell_version() -> Result<(), ShellError> {
let mut manager = TestCase::new().plugin("test");
let info = ProtocolInfo {
protocol: Protocol::NuPlugin,
version: "0.0.0".into(),
features: vec![],
};
manager
.consume(PluginOutput::Hello(info))
.expect_err("version 0.0.0 should cause an error");
Ok(())
}
#[test]
fn manager_consume_errors_on_sending_other_messages_before_hello() -> Result<(), ShellError> {
let mut manager = TestCase::new().plugin("test");
// hello not set
assert!(!manager.state.protocol_info.is_set());
let error = manager
.consume(PluginOutput::Drop(0))
.expect_err("consume before Hello should cause an error");
assert!(format!("{error:?}").contains("Hello"));
Ok(())
}
fn set_default_protocol_info(manager: &mut PluginInterfaceManager) -> Result<(), ShellError> {
manager
.protocol_info_mut
.set(Arc::new(ProtocolInfo::default()))
}
#[test]
fn manager_consume_call_response_forwards_to_subscriber_with_pipeline_data(
) -> Result<(), ShellError> {
let mut manager = TestCase::new().plugin("test");
set_default_protocol_info(&mut manager)?;
let rx = fake_plugin_call(&mut manager, 0);
manager.consume(PluginOutput::CallResponse(
0,
PluginCallResponse::PipelineData(PipelineDataHeader::list_stream(ListStreamInfo::new(
0,
Span::test_data(),
))),
))?;
for i in 0..2 {
manager.consume(PluginOutput::Data(0, Value::test_int(i).into()))?;
}
manager.consume(PluginOutput::End(0))?;
// Make sure the streams end and we don't deadlock
drop(manager);
let message = rx
.try_recv()
.expect("failed to get plugin call response message");
match message {
ReceivedPluginCallMessage::Response(response) => match response {
PluginCallResponse::PipelineData(data) => {
// Ensure we manage to receive the stream messages
assert_eq!(2, data.into_iter().count());
Ok(())
}
_ => panic!("unexpected response: {response:?}"),
},
_ => panic!("unexpected response message: {message:?}"),
}
}
#[test]
fn manager_consume_call_response_registers_streams() -> Result<(), ShellError> {
let mut manager = TestCase::new().plugin("test");
set_default_protocol_info(&mut manager)?;
for n in [0, 1] {
fake_plugin_call(&mut manager, n);
}
// Check list streams, byte streams
manager.consume(PluginOutput::CallResponse(
0,
PluginCallResponse::PipelineData(PipelineDataHeader::list_stream(ListStreamInfo::new(
0,
Span::test_data(),
))),
))?;
manager.consume(PluginOutput::CallResponse(
1,
PluginCallResponse::PipelineData(PipelineDataHeader::byte_stream(ByteStreamInfo::new(
1,
Span::test_data(),
ByteStreamType::Unknown,
))),
))?;
// ListStream should have one
if let Some(sub) = manager.plugin_call_states.get(&0) {
assert_eq!(
1, sub.remaining_streams_to_read,
"ListStream remaining_streams_to_read should be 1"
);
} else {
panic!("failed to find subscription for ListStream (0), maybe it was removed");
}
assert_eq!(
Some(&0),
manager.plugin_call_input_streams.get(&0),
"plugin_call_input_streams[0] should be Some(0)"
);
// ByteStream should have one
if let Some(sub) = manager.plugin_call_states.get(&1) {
assert_eq!(
1, sub.remaining_streams_to_read,
"ByteStream remaining_streams_to_read should be 1"
);
} else {
panic!("failed to find subscription for ByteStream (1), maybe it was removed");
}
assert_eq!(
Some(&1),
manager.plugin_call_input_streams.get(&1),
"plugin_call_input_streams[1] should be Some(1)"
);
Ok(())
}
#[test]
fn manager_consume_engine_call_forwards_to_subscriber_with_pipeline_data() -> Result<(), ShellError>
{
let mut manager = TestCase::new().plugin("test");
set_default_protocol_info(&mut manager)?;
let rx = fake_plugin_call(&mut manager, 37);
manager.consume(PluginOutput::EngineCall {
context: 37,
id: 46,
call: EngineCall::EvalClosure {
closure: Spanned {
item: Closure {
block_id: 0,
captures: vec![],
},
span: Span::test_data(),
},
positional: vec![],
input: PipelineDataHeader::list_stream(ListStreamInfo::new(2, Span::test_data())),
redirect_stdout: false,
redirect_stderr: false,
},
})?;
for i in 0..2 {
manager.consume(PluginOutput::Data(2, Value::test_int(i).into()))?;
}
manager.consume(PluginOutput::End(2))?;
// Make sure the streams end and we don't deadlock
drop(manager);
let message = rx.try_recv().expect("failed to get plugin call message");
match message {
ReceivedPluginCallMessage::EngineCall(id, call) => {
assert_eq!(46, id, "id");
match call {
EngineCall::EvalClosure { input, .. } => {
// Count the stream messages
assert_eq!(2, input.into_iter().count());
Ok(())
}
_ => panic!("unexpected call: {call:?}"),
}
}
_ => panic!("unexpected response message: {message:?}"),
}
}
#[test]
fn manager_handle_engine_call_after_response_received() -> Result<(), ShellError> {
let test = TestCase::new();
let mut manager = test.plugin("test");
set_default_protocol_info(&mut manager)?;
let (context_tx, context_rx) = mpsc::channel();
// Set up a situation identical to what we would find if the response had been read, but there
// was still a stream being processed. We have nowhere to send the engine call in that case,
// so the manager has to create a place to handle it.
manager.plugin_call_states.insert(
0,
PluginCallState {
sender: None,
dont_send_response: false,
signals: Signals::empty(),
context_rx: Some(context_rx),
span: None,
keep_plugin_custom_values: mpsc::channel(),
remaining_streams_to_read: 1,
},
);
// The engine will get the context from the channel
let bogus = Context(Box::new(PluginExecutionBogusContext));
context_tx.send(bogus).expect("failed to send");
manager.send_engine_call(0, 0, EngineCall::GetConfig)?;
// Not really much choice but to wait here, as the thread will have been spawned in the
// background; we don't have a way to know if it's executed
let mut waited = 0;
while !test.has_unconsumed_write() {
if waited > 100 {
panic!("nothing written before timeout, expected engine call response");
} else {
std::thread::sleep(Duration::from_millis(1));
waited += 1;
}
}
// The GetConfig call on bogus should result in an error response being written
match test.next_written().expect("nothing written") {
PluginInput::EngineCallResponse(id, resp) => {
assert_eq!(0, id, "id");
match resp {
EngineCallResponse::Error(err) => {
assert!(err.to_string().contains("bogus"), "wrong error: {err}");
}
_ => panic!("unexpected engine call response, expected error: {resp:?}"),
}
}
other => panic!("unexpected message, not engine call response: {other:?}"),
}
// Whatever was used to make this happen should have been held onto, since spawning a thread
// is expensive
let sender = &manager
.plugin_call_states
.get(&0)
.expect("missing subscription 0")
.sender;
assert!(
sender.is_some(),
"failed to keep spawned engine call handler channel"
);
Ok(())
}
#[test]
fn manager_send_plugin_call_response_removes_context_only_if_no_streams_to_read(
) -> Result<(), ShellError> {
let mut manager = TestCase::new().plugin("test");
for n in [0, 1] {
manager.plugin_call_states.insert(
n,
PluginCallState {
sender: None,
dont_send_response: false,
signals: Signals::empty(),
context_rx: None,
span: None,
keep_plugin_custom_values: mpsc::channel(),
remaining_streams_to_read: n as i32,
},
);
}
for n in [0, 1] {
manager.send_plugin_call_response(n, PluginCallResponse::Signature(vec![]))?;
}
// 0 should not still be present, but 1 should be
assert!(
!manager.plugin_call_states.contains_key(&0),
"didn't clean up when there weren't remaining streams"
);
assert!(
manager.plugin_call_states.contains_key(&1),
"clean up even though there were remaining streams"
);
Ok(())
}
#[test]
fn manager_consume_stream_end_removes_context_only_if_last_stream() -> Result<(), ShellError> {
let mut manager = TestCase::new().plugin("test");
set_default_protocol_info(&mut manager)?;
for n in [1, 2] {
manager.plugin_call_states.insert(
n,
PluginCallState {
sender: None,
dont_send_response: false,
signals: Signals::empty(),
context_rx: None,
span: None,
keep_plugin_custom_values: mpsc::channel(),
remaining_streams_to_read: n as i32,
},
);
}
// 1 owns [10], 2 owns [21, 22]
manager.plugin_call_input_streams.insert(10, 1);
manager.plugin_call_input_streams.insert(21, 2);
manager.plugin_call_input_streams.insert(22, 2);
// Register the streams so we don't have errors
let streams: Vec<_> = [10, 21, 22]
.into_iter()
.map(|id| {
let interface = manager.get_interface();
manager
.stream_manager
.get_handle()
.read_stream::<Value, _>(id, interface)
})
.collect();
// Ending 10 should cause 1 to be removed
manager.consume(StreamMessage::End(10).into())?;
assert!(
!manager.plugin_call_states.contains_key(&1),
"contains(1) after End(10)"
);
// Ending 21 should not cause 2 to be removed
manager.consume(StreamMessage::End(21).into())?;
assert!(
manager.plugin_call_states.contains_key(&2),
"!contains(2) after End(21)"
);
// Ending 22 should cause 2 to be removed
manager.consume(StreamMessage::End(22).into())?;
assert!(
!manager.plugin_call_states.contains_key(&2),
"contains(2) after End(22)"
);
drop(streams);
Ok(())
}
#[test]
fn manager_prepare_pipeline_data_adds_source_to_values() -> Result<(), ShellError> {
let manager = TestCase::new().plugin("test");
let data = manager.prepare_pipeline_data(PipelineData::Value(
Value::test_custom_value(Box::new(test_plugin_custom_value())),
None,
))?;
let value = data
.into_iter()
.next()
.expect("prepared pipeline data is empty");
let custom_value: &PluginCustomValueWithSource = value
.as_custom_value()?
.as_any()
.downcast_ref()
.expect("{value:?} is not a PluginCustomValueWithSource");
assert_eq!("test", custom_value.source().name());
Ok(())
}
#[test]
fn manager_prepare_pipeline_data_adds_source_to_list_streams() -> Result<(), ShellError> {
let manager = TestCase::new().plugin("test");
let data = manager.prepare_pipeline_data(
[Value::test_custom_value(Box::new(
test_plugin_custom_value(),
))]
.into_pipeline_data(Span::test_data(), Signals::empty()),
)?;
let value = data
.into_iter()
.next()
.expect("prepared pipeline data is empty");
let custom_value: &PluginCustomValueWithSource = value
.as_custom_value()?
.as_any()
.downcast_ref()
.expect("{value:?} is not a PluginCustomValueWithSource");
assert_eq!("test", custom_value.source().name());
Ok(())
}
#[test]
fn interface_hello_sends_protocol_info() -> Result<(), ShellError> {
let test = TestCase::new();
let interface = test.plugin("test").get_interface();
interface.hello()?;
let written = test.next_written().expect("nothing written");
match written {
PluginInput::Hello(info) => {
assert_eq!(ProtocolInfo::default().version, info.version);
}
_ => panic!("unexpected message written: {written:?}"),
}
assert!(!test.has_unconsumed_write());
Ok(())
}
#[test]
fn interface_goodbye() -> Result<(), ShellError> {
let test = TestCase::new();
let interface = test.plugin("test").get_interface();
interface.goodbye()?;
let written = test.next_written().expect("nothing written");
assert!(
matches!(written, PluginInput::Goodbye),
"not goodbye: {written:?}"
);
assert!(!test.has_unconsumed_write());
Ok(())
}
#[test]
fn interface_write_plugin_call_registers_subscription() -> Result<(), ShellError> {
let mut manager = TestCase::new().plugin("test");
assert!(
manager.plugin_call_states.is_empty(),
"plugin call subscriptions not empty before start of test"
);
let interface = manager.get_interface();
let _ = interface.write_plugin_call(PluginCall::Signature, None)?;
manager.receive_plugin_call_subscriptions();
assert!(!manager.plugin_call_states.is_empty(), "not registered");
Ok(())
}
#[test]
fn interface_write_plugin_call_writes_signature() -> Result<(), ShellError> {
let test = TestCase::new();
let manager = test.plugin("test");
let interface = manager.get_interface();
let result = interface.write_plugin_call(PluginCall::Signature, None)?;
result.writer.write()?;
let written = test.next_written().expect("nothing written");
match written {
PluginInput::Call(_, call) => assert!(
matches!(call, PluginCall::Signature),
"not Signature: {call:?}"
),
_ => panic!("unexpected message written: {written:?}"),
}
Ok(())
}
#[test]
fn interface_write_plugin_call_writes_custom_value_op() -> Result<(), ShellError> {
let test = TestCase::new();
let manager = test.plugin("test");
let interface = manager.get_interface();
let result = interface.write_plugin_call(
PluginCall::CustomValueOp(
Spanned {
item: test_plugin_custom_value(),
span: Span::test_data(),
},
CustomValueOp::ToBaseValue,
),
None,
)?;
result.writer.write()?;
let written = test.next_written().expect("nothing written");
match written {
PluginInput::Call(_, call) => assert!(
matches!(
call,
PluginCall::CustomValueOp(_, CustomValueOp::ToBaseValue)
),
"expected CustomValueOp(_, ToBaseValue), got {call:?}"
),
_ => panic!("unexpected message written: {written:?}"),
}
Ok(())
}
#[test]
fn interface_write_plugin_call_writes_run_with_value_input() -> Result<(), ShellError> {
let test = TestCase::new();
let manager = test.plugin("test");
let interface = manager.get_interface();
let metadata0 = PipelineMetadata {
data_source: DataSource::None,
content_type: Some("baz".into()),
};
let result = interface.write_plugin_call(
PluginCall::Run(CallInfo {
name: "foo".into(),
call: EvaluatedCall {
head: Span::test_data(),
positional: vec![],
named: vec![],
},
input: PipelineData::Value(Value::test_int(-1), Some(metadata0.clone())),
}),
None,
)?;
result.writer.write()?;
let written = test.next_written().expect("nothing written");
match written {
PluginInput::Call(_, call) => match call {
PluginCall::Run(CallInfo { name, input, .. }) => {
assert_eq!("foo", name);
match input {
PipelineDataHeader::Value(value, metadata) => {
assert_eq!(-1, value.as_int()?);
assert_eq!(metadata0, metadata.expect("there should be metadata"));
}
_ => panic!("unexpected input header: {input:?}"),
}
}
_ => panic!("unexpected Call: {call:?}"),
},
_ => panic!("unexpected message written: {written:?}"),
}
Ok(())
}
#[test]
fn interface_write_plugin_call_writes_run_with_stream_input() -> Result<(), ShellError> {
let test = TestCase::new();
let manager = test.plugin("test");
let interface = manager.get_interface();
let values = vec![Value::test_int(1), Value::test_int(2)];
let result = interface.write_plugin_call(
PluginCall::Run(CallInfo {
name: "foo".into(),
call: EvaluatedCall {
head: Span::test_data(),
positional: vec![],
named: vec![],
},
input: values
.clone()
.into_pipeline_data(Span::test_data(), Signals::empty()),
}),
None,
)?;
result.writer.write()?;
let written = test.next_written().expect("nothing written");
let info = match written {
PluginInput::Call(_, call) => match call {
PluginCall::Run(CallInfo { name, input, .. }) => {
assert_eq!("foo", name);
match input {
PipelineDataHeader::ListStream(info) => info,
_ => panic!("unexpected input header: {input:?}"),
}
}
_ => panic!("unexpected Call: {call:?}"),
},
_ => panic!("unexpected message written: {written:?}"),
};
// Expect stream messages
for value in values {
match test
.next_written()
.expect("failed to get Data stream message")
{
PluginInput::Data(id, data) => {
assert_eq!(info.id, id, "id");
match data {
StreamData::List(data_value) => {
assert_eq!(value, data_value, "wrong value in Data message");
}
_ => panic!("not List stream data: {data:?}"),
}
}
message => panic!("expected Stream(Data(..)) message: {message:?}"),
}
}
match test
.next_written()
.expect("failed to get End stream message")
{
PluginInput::End(id) => {
assert_eq!(info.id, id, "id");
}
message => panic!("expected End(_) message: {message:?}"),
}
Ok(())
}
#[test]
fn interface_receive_plugin_call_receives_response() -> Result<(), ShellError> {
let interface = TestCase::new().plugin("test").get_interface();
// Set up a fake channel that has the response in it
let (tx, rx) = mpsc::channel();
tx.send(ReceivedPluginCallMessage::Response(
PluginCallResponse::Signature(vec![]),
))
.expect("failed to send on new channel");
drop(tx); // so we don't deadlock on recv()
let response = interface.receive_plugin_call_response(rx, None, CurrentCallState::default())?;
assert!(
matches!(response, PluginCallResponse::Signature(_)),
"wrong response: {response:?}"
);
Ok(())
}
#[test]
fn interface_receive_plugin_call_receives_error() -> Result<(), ShellError> {
let interface = TestCase::new().plugin("test").get_interface();
// Set up a fake channel that has the error in it
let (tx, rx) = mpsc::channel();
tx.send(ReceivedPluginCallMessage::Error(
ShellError::ExternalNotSupported {
span: Span::test_data(),
},
))
.expect("failed to send on new channel");
drop(tx); // so we don't deadlock on recv()
let error = interface
.receive_plugin_call_response(rx, None, CurrentCallState::default())
.expect_err("did not receive error");
assert!(
matches!(error, ShellError::ExternalNotSupported { .. }),
"wrong error: {error:?}"
);
Ok(())
}
#[test]
fn interface_receive_plugin_call_handles_engine_call() -> Result<(), ShellError> {
let test = TestCase::new();
let interface = test.plugin("test").get_interface();
// Set up a fake channel just for the engine call
let (tx, rx) = mpsc::channel();
tx.send(ReceivedPluginCallMessage::EngineCall(
0,
EngineCall::GetConfig,
))
.expect("failed to send on new channel");
// The context should be a bogus context, which will return an error for GetConfig
let mut context = PluginExecutionBogusContext;
// We don't actually send a response, so `receive_plugin_call_response` should actually return
// an error, but it should still do the engine call
drop(tx);
interface
.receive_plugin_call_response(rx, Some(&mut context), CurrentCallState::default())
.expect_err("no error even though there was no response");
// Check for the engine call response output
match test
.next_written()
.expect("no engine call response written")
{
PluginInput::EngineCallResponse(id, resp) => {
assert_eq!(0, id, "id");
match resp {
EngineCallResponse::Error(err) => {
assert!(err.to_string().contains("bogus"), "wrong error: {err}");
}
_ => panic!("unexpected engine call response, maybe bogus is wrong: {resp:?}"),
}
}
other => panic!("unexpected message: {other:?}"),
}
assert!(!test.has_unconsumed_write());
Ok(())
}
/// Fake responses to requests for plugin call messages
fn start_fake_plugin_call_responder(
manager: PluginInterfaceManager,
take: usize,
mut f: impl FnMut(PluginCallId) -> Vec<ReceivedPluginCallMessage> + Send + 'static,
) {
std::thread::Builder::new()
.name("fake plugin call responder".into())
.spawn(move || {
for (id, state) in manager
.plugin_call_subscription_receiver
.into_iter()
.take(take)
{
for message in f(id) {
state
.sender
.as_ref()
.expect("sender was not set")
.send(message)
.expect("failed to send");
}
}
})
.expect("failed to spawn thread");
}
#[test]
fn interface_get_metadata() -> Result<(), ShellError> {
let test = TestCase::new();
let manager = test.plugin("test");
let interface = manager.get_interface();
start_fake_plugin_call_responder(manager, 1, |_| {
vec![ReceivedPluginCallMessage::Response(
PluginCallResponse::Metadata(PluginMetadata::new().with_version("test")),
)]
});
let metadata = interface.get_metadata()?;
assert_eq!(Some("test"), metadata.version.as_deref());
assert!(test.has_unconsumed_write());
Ok(())
}
#[test]
fn interface_get_signature() -> Result<(), ShellError> {
let test = TestCase::new();
let manager = test.plugin("test");
let interface = manager.get_interface();
start_fake_plugin_call_responder(manager, 1, |_| {
vec![ReceivedPluginCallMessage::Response(
PluginCallResponse::Signature(vec![PluginSignature::build("test")]),
)]
});
let signatures = interface.get_signature()?;
assert_eq!(1, signatures.len());
assert!(test.has_unconsumed_write());
Ok(())
}
#[test]
fn interface_run() -> Result<(), ShellError> {
let test = TestCase::new();
let manager = test.plugin("test");
let interface = manager.get_interface();
let number = 64;
start_fake_plugin_call_responder(manager, 1, move |_| {
vec![ReceivedPluginCallMessage::Response(
PluginCallResponse::PipelineData(PipelineData::Value(Value::test_int(number), None)),
)]
});
let result = interface.run(
CallInfo {
name: "bogus".into(),
call: EvaluatedCall {
head: Span::test_data(),
positional: vec![],
named: vec![],
},
input: PipelineData::Empty,
},
&mut PluginExecutionBogusContext,
)?;
assert_eq!(
Value::test_int(number),
result.into_value(Span::test_data())?,
);
assert!(test.has_unconsumed_write());
Ok(())
}
#[test]
fn interface_custom_value_to_base_value() -> Result<(), ShellError> {
let test = TestCase::new();
let manager = test.plugin("test");
let interface = manager.get_interface();
let string = "this is a test";
start_fake_plugin_call_responder(manager, 1, move |_| {
vec![ReceivedPluginCallMessage::Response(
PluginCallResponse::PipelineData(PipelineData::Value(Value::test_string(string), None)),
)]
});
let result = interface.custom_value_to_base_value(Spanned {
item: test_plugin_custom_value_with_source(),
span: Span::test_data(),
})?;
assert_eq!(Value::test_string(string), result);
assert!(test.has_unconsumed_write());
Ok(())
}
fn normal_values(interface: &PluginInterface) -> Vec<Value> {
vec![
Value::test_int(5),
Value::test_custom_value(Box::new(
PluginCustomValue::new("SomeTest".into(), vec![1, 2, 3], false).with_source(
// Has the same source, so it should be accepted
interface.state.source.clone(),
),
)),
]
}
#[test]
fn interface_prepare_pipeline_data_accepts_normal_values() -> Result<(), ShellError> {
let interface = TestCase::new().plugin("test").get_interface();
let state = CurrentCallState::default();
for value in normal_values(&interface) {
match interface.prepare_pipeline_data(PipelineData::Value(value.clone(), None), &state) {
Ok(data) => assert_eq!(
value.get_type(),
data.into_value(Span::test_data())?.get_type(),
),
Err(err) => panic!("failed to accept {value:?}: {err}"),
}
}
Ok(())
}
#[test]
fn interface_prepare_pipeline_data_accepts_normal_streams() -> Result<(), ShellError> {
let interface = TestCase::new().plugin("test").get_interface();
let values = normal_values(&interface);
let state = CurrentCallState::default();
let data = interface.prepare_pipeline_data(
values
.clone()
.into_pipeline_data(Span::test_data(), Signals::empty()),
&state,
)?;
let mut count = 0;
for (expected_value, actual_value) in values.iter().zip(data) {
assert!(
!actual_value.is_error(),
"error value instead of {expected_value:?} in stream: {actual_value:?}"
);
assert_eq!(expected_value.get_type(), actual_value.get_type());
count += 1;
}
assert_eq!(
values.len(),
count,
"didn't receive as many values as expected"
);
Ok(())
}
fn bad_custom_values() -> Vec<Value> {
// These shouldn't be accepted
vec![
// Native custom value (not PluginCustomValue) should be rejected
Value::test_custom_value(Box::new(expected_test_custom_value())),
// Has no source, so it should be rejected
Value::test_custom_value(Box::new(PluginCustomValue::new(
"SomeTest".into(),
vec![1, 2, 3],
false,
))),
// Has a different source, so it should be rejected
Value::test_custom_value(Box::new(
PluginCustomValue::new("SomeTest".into(), vec![1, 2, 3], false)
.with_source(PluginSource::new_fake("pluto").into()),
)),
]
}
#[test]
fn interface_prepare_pipeline_data_rejects_bad_custom_value() -> Result<(), ShellError> {
let interface = TestCase::new().plugin("test").get_interface();
let state = CurrentCallState::default();
for value in bad_custom_values() {
match interface.prepare_pipeline_data(PipelineData::Value(value.clone(), None), &state) {
Err(err) => match err {
ShellError::CustomValueIncorrectForPlugin { .. } => (),
_ => panic!("expected error type CustomValueIncorrectForPlugin, but got {err:?}"),
},
Ok(_) => panic!("mistakenly accepted {value:?}"),
}
}
Ok(())
}
#[test]
fn interface_prepare_pipeline_data_rejects_bad_custom_value_in_a_stream() -> Result<(), ShellError>
{
let interface = TestCase::new().plugin("test").get_interface();
let values = bad_custom_values();
let state = CurrentCallState::default();
let data = interface.prepare_pipeline_data(
values
.clone()
.into_pipeline_data(Span::test_data(), Signals::empty()),
&state,
)?;
let mut count = 0;
for value in data {
assert!(value.is_error(), "expected error value for {value:?}");
count += 1;
}
assert_eq!(
values.len(),
count,
"didn't receive as many values as expected"
);
Ok(())
}
#[test]
fn prepare_custom_value_verifies_source() {
let span = Span::test_data();
let source = Arc::new(PluginSource::new_fake("test"));
let mut val: Box<dyn CustomValue> = Box::new(test_plugin_custom_value());
assert!(CurrentCallState::default()
.prepare_custom_value(
Spanned {
item: &mut val,
span,
},
&source
)
.is_err());
let mut val: Box<dyn CustomValue> =
Box::new(test_plugin_custom_value().with_source(source.clone()));
assert!(CurrentCallState::default()
.prepare_custom_value(
Spanned {
item: &mut val,
span,
},
&source
)
.is_ok());
}
#[derive(Debug, Serialize, Deserialize)]
struct DropCustomVal;
#[typetag::serde]
impl CustomValue for DropCustomVal {
fn clone_value(&self, _span: Span) -> Value {
unimplemented!()
}
fn type_name(&self) -> String {
"DropCustomVal".into()
}
fn to_base_value(&self, _span: Span) -> Result<Value, ShellError> {
unimplemented!()
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_mut_any(&mut self) -> &mut dyn std::any::Any {
self
}
fn notify_plugin_on_drop(&self) -> bool {
true
}
}
#[test]
fn prepare_custom_value_sends_to_keep_channel_if_drop_notify() -> Result<(), ShellError> {
let span = Span::test_data();
let source = Arc::new(PluginSource::new_fake("test"));
let (tx, rx) = mpsc::channel();
let state = CurrentCallState {
keep_plugin_custom_values_tx: Some(tx),
..Default::default()
};
// Try with a custom val that has drop check set
let mut drop_val: Box<dyn CustomValue> = Box::new(
PluginCustomValue::serialize_from_custom_value(&DropCustomVal, span)?
.with_source(source.clone()),
);
state.prepare_custom_value(
Spanned {
item: &mut drop_val,
span,
},
&source,
)?;
// Check that the custom value was actually sent
assert!(rx.try_recv().is_ok());
// Now try with one that doesn't have it
let mut not_drop_val: Box<dyn CustomValue> =
Box::new(test_plugin_custom_value().with_source(source.clone()));
state.prepare_custom_value(
Spanned {
item: &mut not_drop_val,
span,
},
&source,
)?;
// Should not have been sent to the channel
assert!(rx.try_recv().is_err());
Ok(())
}
#[test]
fn prepare_plugin_call_run() {
// Check that args are handled
let span = Span::test_data();
let source = Arc::new(PluginSource::new_fake("test"));
let other_source = Arc::new(PluginSource::new_fake("other"));
let cv_ok = test_plugin_custom_value()
.with_source(source.clone())
.into_value(span);
let cv_bad = test_plugin_custom_value()
.with_source(other_source)
.into_value(span);
let fixtures = [
(
true, // should succeed
PluginCall::Run(CallInfo {
name: "".into(),
call: EvaluatedCall {
head: span,
positional: vec![Value::test_int(4)],
named: vec![("x".to_owned().into_spanned(span), Some(Value::test_int(6)))],
},
input: PipelineData::Empty,
}),
),
(
true, // should succeed
PluginCall::Run(CallInfo {
name: "".into(),
call: EvaluatedCall {
head: span,
positional: vec![cv_ok.clone()],
named: vec![("ok".to_owned().into_spanned(span), Some(cv_ok.clone()))],
},
input: PipelineData::Empty,
}),
),
(
false, // should fail
PluginCall::Run(CallInfo {
name: "".into(),
call: EvaluatedCall {
head: span,
positional: vec![cv_bad.clone()],
named: vec![],
},
input: PipelineData::Empty,
}),
),
(
false, // should fail
PluginCall::Run(CallInfo {
name: "".into(),
call: EvaluatedCall {
head: span,
positional: vec![],
named: vec![("bad".to_owned().into_spanned(span), Some(cv_bad.clone()))],
},
input: PipelineData::Empty,
}),
),
(
true, // should succeed
PluginCall::Run(CallInfo {
name: "".into(),
call: EvaluatedCall {
head: span,
positional: vec![],
named: vec![],
},
// Shouldn't check input - that happens somewhere else
input: PipelineData::Value(cv_bad.clone(), None),
}),
),
];
for (should_succeed, mut fixture) in fixtures {
let result = CurrentCallState::default().prepare_plugin_call(&mut fixture, &source);
if should_succeed {
assert!(
result.is_ok(),
"Expected success, but failed with {:?} on {fixture:#?}",
result.unwrap_err(),
);
} else {
assert!(
result.is_err(),
"Expected failure, but succeeded on {fixture:#?}",
);
}
}
}
#[test]
fn prepare_plugin_call_custom_value_op() {
// Check behavior with custom value ops
let span = Span::test_data();
let source = Arc::new(PluginSource::new_fake("test"));
let other_source = Arc::new(PluginSource::new_fake("other"));
let cv_ok = test_plugin_custom_value().with_source(source.clone());
let cv_ok_val = cv_ok.clone_value(span);
let cv_bad = test_plugin_custom_value().with_source(other_source);
let cv_bad_val = cv_bad.clone_value(span);
let fixtures = [
(
true, // should succeed
PluginCall::CustomValueOp::<PipelineData>(
Spanned {
item: cv_ok.clone().without_source(),
span,
},
CustomValueOp::ToBaseValue,
),
),
(
true, // should succeed
PluginCall::CustomValueOp(
Spanned {
item: test_plugin_custom_value(),
span,
},
// Dropped shouldn't check. We don't have a source set.
CustomValueOp::Dropped,
),
),
(
true, // should succeed
PluginCall::CustomValueOp::<PipelineData>(
Spanned {
item: cv_ok.clone().without_source(),
span,
},
CustomValueOp::PartialCmp(cv_ok_val.clone()),
),
),
(
false, // should fail
PluginCall::CustomValueOp(
Spanned {
item: cv_ok.clone().without_source(),
span,
},
CustomValueOp::PartialCmp(cv_bad_val.clone()),
),
),
(
true, // should succeed
PluginCall::CustomValueOp::<PipelineData>(
Spanned {
item: cv_ok.clone().without_source(),
span,
},
CustomValueOp::Operation(
Operator::Math(Math::Append).into_spanned(span),
cv_ok_val.clone(),
),
),
),
(
false, // should fail
PluginCall::CustomValueOp(
Spanned {
item: cv_ok.clone().without_source(),
span,
},
CustomValueOp::Operation(
Operator::Math(Math::Append).into_spanned(span),
cv_bad_val.clone(),
),
),
),
];
for (should_succeed, mut fixture) in fixtures {
let result = CurrentCallState::default().prepare_plugin_call(&mut fixture, &source);
if should_succeed {
assert!(
result.is_ok(),
"Expected success, but failed with {:?} on {fixture:#?}",
result.unwrap_err(),
);
} else {
assert!(
result.is_err(),
"Expected failure, but succeeded on {fixture:#?}",
);
}
}
}