nushell/crates/nu-plugin/src/plugin/interface/engine/tests.rs
Devyn Cairns c52884b3c8
Fix (and test) for a deadlock that can happen while waiting for protocol info (#12633)
# Description

The local socket PR introduced a `Waitable` type, which could either
hold a value or be waited on until a value is available. Unlike a
channel, it would always return that value once set.

However, one issue with this design was that there was no way to detect
whether a value would ever be written. This splits the writer into a
different type `WaitableMut`, so that when it is dropped, waiting
threads can fail (because they'll never get a value).

# Tests + Formatting

A test has been added to `stress_internals` to make sure this fails in
the right way.

- 🟢 `toolkit fmt`
- 🟢 `toolkit clippy`
- 🟢 `toolkit test`
- 🟢 `toolkit test stdlib`
2024-04-24 08:44:04 -05:00

1192 lines
35 KiB
Rust

use super::{EngineInterfaceManager, ReceivedPluginCall};
use crate::{
plugin::interface::{test_util::TestCase, Interface, InterfaceManager},
protocol::{
test_util::{expected_test_custom_value, test_plugin_custom_value, TestCustomValue},
CallInfo, CustomValueOp, EngineCall, EngineCallId, EngineCallResponse, ExternalStreamInfo,
ListStreamInfo, PipelineDataHeader, PluginCall, PluginCustomValue, PluginInput, Protocol,
ProtocolInfo, RawStreamInfo, StreamData,
},
EvaluatedCall, PluginCallResponse, PluginOutput,
};
use nu_protocol::{
engine::Closure, Config, CustomValue, IntoInterruptiblePipelineData, LabeledError,
PipelineData, PluginSignature, ShellError, Span, Spanned, Value,
};
use std::{
collections::HashMap,
sync::{
mpsc::{self, TryRecvError},
Arc,
},
};
#[test]
fn is_using_stdio_is_false_for_test() {
let test = TestCase::new();
let manager = test.engine();
let interface = manager.get_interface();
assert!(!interface.is_using_stdio());
}
#[test]
fn manager_consume_all_consumes_messages() -> Result<(), ShellError> {
let mut test = TestCase::new();
let mut manager = test.engine();
// This message should be non-problematic
test.add(PluginInput::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.engine();
// Add messages that won't cause errors
for _ in 0..5 {
test.add(PluginInput::Hello(ProtocolInfo::default()));
}
// Create a stream...
let stream = manager.read_pipeline_data(
PipelineDataHeader::ListStream(ListStreamInfo { id: 0 }),
None,
)?;
// 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.engine();
test.set_read_error(test_io_error());
let stream = manager.read_pipeline_data(
PipelineDataHeader::ListStream(ListStreamInfo { id: 0 }),
None,
)?;
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_input() -> PluginInput {
// This should definitely cause an error, as 0.0.0 is not compatible with any version other than
// itself
PluginInput::Hello(ProtocolInfo {
protocol: Protocol::NuPlugin,
version: "0.0.0".into(),
features: vec![],
})
}
fn check_invalid_input_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.engine();
test.add(invalid_input());
let stream = manager.read_pipeline_data(
PipelineDataHeader::ExternalStream(ExternalStreamInfo {
span: Span::test_data(),
stdout: Some(RawStreamInfo {
id: 0,
is_binary: false,
known_size: None,
}),
stderr: None,
exit_code: None,
trim_end_newline: false,
}),
None,
)?;
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_input_error(&error);
Ok(())
} else {
panic!("did not get an error");
}
}
fn fake_engine_call(
manager: &mut EngineInterfaceManager,
id: EngineCallId,
) -> mpsc::Receiver<EngineCallResponse<PipelineData>> {
// Set up a fake engine call subscription
let (tx, rx) = mpsc::channel();
manager.engine_call_subscriptions.insert(id, tx);
rx
}
#[test]
fn manager_consume_all_propagates_io_error_to_engine_calls() -> Result<(), ShellError> {
let mut test = TestCase::new();
let mut manager = test.engine();
let interface = manager.get_interface();
test.set_read_error(test_io_error());
// Set up a fake engine call subscription
let rx = fake_engine_call(&mut manager, 0);
manager
.consume_all(&mut test)
.expect_err("consume_all did not error");
// We have to hold interface until now otherwise consume_all won't try to process the message
drop(interface);
let message = rx.try_recv().expect("failed to get engine call message");
match message {
EngineCallResponse::Error(error) => {
check_test_io_error(&error);
Ok(())
}
_ => panic!("received something other than an error: {message:?}"),
}
}
#[test]
fn manager_consume_all_propagates_message_error_to_engine_calls() -> Result<(), ShellError> {
let mut test = TestCase::new();
let mut manager = test.engine();
let interface = manager.get_interface();
test.add(invalid_input());
// Set up a fake engine call subscription
let rx = fake_engine_call(&mut manager, 0);
manager
.consume_all(&mut test)
.expect_err("consume_all did not error");
// We have to hold interface until now otherwise consume_all won't try to process the message
drop(interface);
let message = rx.try_recv().expect("failed to get engine call message");
match message {
EngineCallResponse::Error(error) => {
check_invalid_input_error(&error);
Ok(())
}
_ => panic!("received something other than an error: {message:?}"),
}
}
#[test]
fn manager_consume_sets_protocol_info_on_hello() -> Result<(), ShellError> {
let mut manager = TestCase::new().engine();
let info = ProtocolInfo::default();
manager.consume(PluginInput::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().engine();
let info = ProtocolInfo {
protocol: Protocol::NuPlugin,
version: "0.0.0".into(),
features: vec![],
};
manager
.consume(PluginInput::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().engine();
// hello not set
assert!(!manager.state.protocol_info.is_set());
let error = manager
.consume(PluginInput::Drop(0))
.expect_err("consume before Hello should cause an error");
assert!(format!("{error:?}").contains("Hello"));
Ok(())
}
fn set_default_protocol_info(manager: &mut EngineInterfaceManager) -> Result<(), ShellError> {
manager
.protocol_info_mut
.set(Arc::new(ProtocolInfo::default()))
}
#[test]
fn manager_consume_goodbye_closes_plugin_call_channel() -> Result<(), ShellError> {
let mut manager = TestCase::new().engine();
set_default_protocol_info(&mut manager)?;
let rx = manager
.take_plugin_call_receiver()
.expect("plugin call receiver missing");
manager.consume(PluginInput::Goodbye)?;
match rx.try_recv() {
Err(TryRecvError::Disconnected) => (),
_ => panic!("receiver was not disconnected"),
}
Ok(())
}
#[test]
fn manager_consume_call_signature_forwards_to_receiver_with_context() -> Result<(), ShellError> {
let mut manager = TestCase::new().engine();
set_default_protocol_info(&mut manager)?;
let rx = manager
.take_plugin_call_receiver()
.expect("couldn't take receiver");
manager.consume(PluginInput::Call(0, PluginCall::Signature))?;
match rx.try_recv().expect("call was not forwarded to receiver") {
ReceivedPluginCall::Signature { engine } => {
assert_eq!(Some(0), engine.context);
Ok(())
}
call => panic!("wrong call type: {call:?}"),
}
}
#[test]
fn manager_consume_call_run_forwards_to_receiver_with_context() -> Result<(), ShellError> {
let mut manager = TestCase::new().engine();
set_default_protocol_info(&mut manager)?;
let rx = manager
.take_plugin_call_receiver()
.expect("couldn't take receiver");
manager.consume(PluginInput::Call(
17,
PluginCall::Run(CallInfo {
name: "bar".into(),
call: EvaluatedCall {
head: Span::test_data(),
positional: vec![],
named: vec![],
},
input: PipelineDataHeader::Empty,
}),
))?;
// Make sure the streams end and we don't deadlock
drop(manager);
match rx.try_recv().expect("call was not forwarded to receiver") {
ReceivedPluginCall::Run { engine, call: _ } => {
assert_eq!(Some(17), engine.context, "context");
Ok(())
}
call => panic!("wrong call type: {call:?}"),
}
}
#[test]
fn manager_consume_call_run_forwards_to_receiver_with_pipeline_data() -> Result<(), ShellError> {
let mut manager = TestCase::new().engine();
set_default_protocol_info(&mut manager)?;
let rx = manager
.take_plugin_call_receiver()
.expect("couldn't take receiver");
manager.consume(PluginInput::Call(
0,
PluginCall::Run(CallInfo {
name: "bar".into(),
call: EvaluatedCall {
head: Span::test_data(),
positional: vec![],
named: vec![],
},
input: PipelineDataHeader::ListStream(ListStreamInfo { id: 6 }),
}),
))?;
for i in 0..10 {
manager.consume(PluginInput::Data(6, Value::test_int(i).into()))?;
}
manager.consume(PluginInput::End(6))?;
// Make sure the streams end and we don't deadlock
drop(manager);
match rx.try_recv().expect("call was not forwarded to receiver") {
ReceivedPluginCall::Run { engine: _, call } => {
assert_eq!("bar", call.name);
// Ensure we manage to receive the stream messages
assert_eq!(10, call.input.into_iter().count());
Ok(())
}
call => panic!("wrong call type: {call:?}"),
}
}
#[test]
fn manager_consume_call_run_deserializes_custom_values_in_args() -> Result<(), ShellError> {
let mut manager = TestCase::new().engine();
set_default_protocol_info(&mut manager)?;
let rx = manager
.take_plugin_call_receiver()
.expect("couldn't take receiver");
let value = Value::test_custom_value(Box::new(test_plugin_custom_value()));
manager.consume(PluginInput::Call(
0,
PluginCall::Run(CallInfo {
name: "bar".into(),
call: EvaluatedCall {
head: Span::test_data(),
positional: vec![value.clone()],
named: vec![(
Spanned {
item: "flag".into(),
span: Span::test_data(),
},
Some(value),
)],
},
input: PipelineDataHeader::Empty,
}),
))?;
// Make sure the streams end and we don't deadlock
drop(manager);
match rx.try_recv().expect("call was not forwarded to receiver") {
ReceivedPluginCall::Run { engine: _, call } => {
assert_eq!(1, call.call.positional.len());
assert_eq!(1, call.call.named.len());
for arg in call.call.positional {
let custom_value: &TestCustomValue = arg
.as_custom_value()?
.as_any()
.downcast_ref()
.expect("positional arg is not TestCustomValue");
assert_eq!(expected_test_custom_value(), *custom_value, "positional");
}
for (key, val) in call.call.named {
let key = &key.item;
let custom_value: &TestCustomValue = val
.as_ref()
.unwrap_or_else(|| panic!("found empty named argument: {key}"))
.as_custom_value()?
.as_any()
.downcast_ref()
.unwrap_or_else(|| panic!("named arg {key} is not TestCustomValue"));
assert_eq!(expected_test_custom_value(), *custom_value, "named: {key}");
}
Ok(())
}
call => panic!("wrong call type: {call:?}"),
}
}
#[test]
fn manager_consume_call_custom_value_op_forwards_to_receiver_with_context() -> Result<(), ShellError>
{
let mut manager = TestCase::new().engine();
set_default_protocol_info(&mut manager)?;
let rx = manager
.take_plugin_call_receiver()
.expect("couldn't take receiver");
manager.consume(PluginInput::Call(
32,
PluginCall::CustomValueOp(
Spanned {
item: test_plugin_custom_value(),
span: Span::test_data(),
},
CustomValueOp::ToBaseValue,
),
))?;
match rx.try_recv().expect("call was not forwarded to receiver") {
ReceivedPluginCall::CustomValueOp {
engine,
custom_value,
op,
} => {
assert_eq!(Some(32), engine.context);
assert_eq!("TestCustomValue", custom_value.item.name());
assert!(
matches!(op, CustomValueOp::ToBaseValue),
"incorrect op: {op:?}"
);
}
call => panic!("wrong call type: {call:?}"),
}
Ok(())
}
#[test]
fn manager_consume_engine_call_response_forwards_to_subscriber_with_pipeline_data(
) -> Result<(), ShellError> {
let mut manager = TestCase::new().engine();
set_default_protocol_info(&mut manager)?;
let rx = fake_engine_call(&mut manager, 0);
manager.consume(PluginInput::EngineCallResponse(
0,
EngineCallResponse::PipelineData(PipelineDataHeader::ListStream(ListStreamInfo { id: 0 })),
))?;
for i in 0..2 {
manager.consume(PluginInput::Data(0, Value::test_int(i).into()))?;
}
manager.consume(PluginInput::End(0))?;
// Make sure the streams end and we don't deadlock
drop(manager);
let response = rx.try_recv().expect("failed to get engine call response");
match response {
EngineCallResponse::PipelineData(data) => {
// Ensure we manage to receive the stream messages
assert_eq!(2, data.into_iter().count());
Ok(())
}
_ => panic!("unexpected response: {response:?}"),
}
}
#[test]
fn manager_prepare_pipeline_data_deserializes_custom_values() -> Result<(), ShellError> {
let manager = TestCase::new().engine();
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: &TestCustomValue = value
.as_custom_value()?
.as_any()
.downcast_ref()
.expect("custom value is not a TestCustomValue, probably not deserialized");
assert_eq!(expected_test_custom_value(), *custom_value);
Ok(())
}
#[test]
fn manager_prepare_pipeline_data_deserializes_custom_values_in_streams() -> Result<(), ShellError> {
let manager = TestCase::new().engine();
let data = manager.prepare_pipeline_data(
[Value::test_custom_value(Box::new(
test_plugin_custom_value(),
))]
.into_pipeline_data(None),
)?;
let value = data
.into_iter()
.next()
.expect("prepared pipeline data is empty");
let custom_value: &TestCustomValue = value
.as_custom_value()?
.as_any()
.downcast_ref()
.expect("custom value is not a TestCustomValue, probably not deserialized");
assert_eq!(expected_test_custom_value(), *custom_value);
Ok(())
}
#[test]
fn manager_prepare_pipeline_data_embeds_deserialization_errors_in_streams() -> Result<(), ShellError>
{
let manager = TestCase::new().engine();
let invalid_custom_value = PluginCustomValue::new(
"Invalid".into(),
vec![0; 8], // should fail to decode to anything
false,
None,
);
let span = Span::new(20, 30);
let data = manager.prepare_pipeline_data(
[Value::custom(Box::new(invalid_custom_value), span)].into_pipeline_data(None),
)?;
let value = data
.into_iter()
.next()
.expect("prepared pipeline data is empty");
match value {
Value::Error { error, .. } => match *error {
ShellError::CustomValueFailedToDecode {
span: error_span, ..
} => {
assert_eq!(span, error_span, "error span not the same as the value's");
}
_ => panic!("expected ShellError::CustomValueFailedToDecode, but got {error:?}"),
},
_ => panic!("unexpected value, not error: {value:?}"),
}
Ok(())
}
#[test]
fn interface_hello_sends_protocol_info() -> Result<(), ShellError> {
let test = TestCase::new();
let interface = test.engine().get_interface();
interface.hello()?;
let written = test.next_written().expect("nothing written");
match written {
PluginOutput::Hello(info) => {
assert_eq!(ProtocolInfo::default().version, info.version);
}
_ => panic!("unexpected message written: {written:?}"),
}
assert!(!test.has_unconsumed_write());
Ok(())
}
#[test]
fn interface_write_response_with_value() -> Result<(), ShellError> {
let test = TestCase::new();
let interface = test.engine().interface_for_context(33);
interface
.write_response(Ok::<_, ShellError>(PipelineData::Value(
Value::test_int(6),
None,
)))?
.write()?;
let written = test.next_written().expect("nothing written");
match written {
PluginOutput::CallResponse(id, response) => {
assert_eq!(33, id, "id");
match response {
PluginCallResponse::PipelineData(header) => match header {
PipelineDataHeader::Value(value) => assert_eq!(6, value.as_int()?),
_ => panic!("unexpected pipeline data header: {header:?}"),
},
_ => panic!("unexpected response: {response:?}"),
}
}
_ => panic!("unexpected message written: {written:?}"),
}
assert!(!test.has_unconsumed_write());
Ok(())
}
#[test]
fn interface_write_response_with_stream() -> Result<(), ShellError> {
let test = TestCase::new();
let manager = test.engine();
let interface = manager.interface_for_context(34);
interface
.write_response(Ok::<_, ShellError>(
[Value::test_int(3), Value::test_int(4), Value::test_int(5)].into_pipeline_data(None),
))?
.write()?;
let written = test.next_written().expect("nothing written");
let info = match written {
PluginOutput::CallResponse(_, response) => match response {
PluginCallResponse::PipelineData(header) => match header {
PipelineDataHeader::ListStream(info) => info,
_ => panic!("expected ListStream header: {header:?}"),
},
_ => panic!("wrong response: {response:?}"),
},
_ => panic!("wrong output written: {written:?}"),
};
for number in [3, 4, 5] {
match test.next_written().expect("missing stream Data message") {
PluginOutput::Data(id, data) => {
assert_eq!(info.id, id, "Data id");
match data {
StreamData::List(val) => assert_eq!(number, val.as_int()?),
_ => panic!("expected List data: {data:?}"),
}
}
message => panic!("expected Data(..): {message:?}"),
}
}
match test.next_written().expect("missing stream End message") {
PluginOutput::End(id) => assert_eq!(info.id, id, "End id"),
message => panic!("expected Data(..): {message:?}"),
}
assert!(!test.has_unconsumed_write());
Ok(())
}
#[test]
fn interface_write_response_with_error() -> Result<(), ShellError> {
let test = TestCase::new();
let interface = test.engine().interface_for_context(35);
let labeled_error = LabeledError::new("this is an error").with_help("a test error");
interface
.write_response(Err(labeled_error.clone()))?
.write()?;
let written = test.next_written().expect("nothing written");
match written {
PluginOutput::CallResponse(id, response) => {
assert_eq!(35, id, "id");
match response {
PluginCallResponse::Error(err) => assert_eq!(labeled_error, err),
_ => panic!("unexpected response: {response:?}"),
}
}
_ => panic!("unexpected message written: {written:?}"),
}
assert!(!test.has_unconsumed_write());
Ok(())
}
#[test]
fn interface_write_signature() -> Result<(), ShellError> {
let test = TestCase::new();
let interface = test.engine().interface_for_context(36);
let signatures = vec![PluginSignature::build("test command")];
interface.write_signature(signatures.clone())?;
let written = test.next_written().expect("nothing written");
match written {
PluginOutput::CallResponse(id, response) => {
assert_eq!(36, id, "id");
match response {
PluginCallResponse::Signature(sigs) => assert_eq!(1, sigs.len(), "sigs.len"),
_ => panic!("unexpected response: {response:?}"),
}
}
_ => panic!("unexpected message written: {written:?}"),
}
assert!(!test.has_unconsumed_write());
Ok(())
}
#[test]
fn interface_write_engine_call_registers_subscription() -> Result<(), ShellError> {
let mut manager = TestCase::new().engine();
assert!(
manager.engine_call_subscriptions.is_empty(),
"engine call subscriptions not empty before start of test"
);
let interface = manager.interface_for_context(0);
let _ = interface.write_engine_call(EngineCall::GetConfig)?;
manager.receive_engine_call_subscriptions();
assert!(
!manager.engine_call_subscriptions.is_empty(),
"not registered"
);
Ok(())
}
#[test]
fn interface_write_engine_call_writes_with_correct_context() -> Result<(), ShellError> {
let test = TestCase::new();
let manager = test.engine();
let interface = manager.interface_for_context(32);
let _ = interface.write_engine_call(EngineCall::GetConfig)?;
match test.next_written().expect("nothing written") {
PluginOutput::EngineCall { context, call, .. } => {
assert_eq!(32, context, "context incorrect");
assert!(
matches!(call, EngineCall::GetConfig),
"incorrect engine call (expected GetConfig): {call:?}"
);
}
other => panic!("incorrect output: {other:?}"),
}
assert!(!test.has_unconsumed_write());
Ok(())
}
/// Fake responses to requests for engine call messages
fn start_fake_plugin_call_responder(
manager: EngineInterfaceManager,
take: usize,
mut f: impl FnMut(EngineCallId) -> EngineCallResponse<PipelineData> + Send + 'static,
) {
std::thread::Builder::new()
.name("fake engine call responder".into())
.spawn(move || {
for (id, sub) in manager
.engine_call_subscription_receiver
.into_iter()
.take(take)
{
sub.send(f(id)).expect("failed to send");
}
})
.expect("failed to spawn thread");
}
#[test]
fn interface_get_config() -> Result<(), ShellError> {
let test = TestCase::new();
let manager = test.engine();
let interface = manager.interface_for_context(0);
start_fake_plugin_call_responder(manager, 1, |_| {
EngineCallResponse::Config(Config::default().into())
});
let _ = interface.get_config()?;
assert!(test.has_unconsumed_write());
Ok(())
}
#[test]
fn interface_get_plugin_config() -> Result<(), ShellError> {
let test = TestCase::new();
let manager = test.engine();
let interface = manager.interface_for_context(0);
start_fake_plugin_call_responder(manager, 2, |id| {
if id == 0 {
EngineCallResponse::PipelineData(PipelineData::Empty)
} else {
EngineCallResponse::PipelineData(PipelineData::Value(Value::test_int(2), None))
}
});
let first_config = interface.get_plugin_config()?;
assert!(first_config.is_none(), "should be None: {first_config:?}");
let second_config = interface.get_plugin_config()?;
assert_eq!(Some(Value::test_int(2)), second_config);
assert!(test.has_unconsumed_write());
Ok(())
}
#[test]
fn interface_get_env_var() -> Result<(), ShellError> {
let test = TestCase::new();
let manager = test.engine();
let interface = manager.interface_for_context(0);
start_fake_plugin_call_responder(manager, 2, |id| {
if id == 0 {
EngineCallResponse::empty()
} else {
EngineCallResponse::value(Value::test_string("/foo"))
}
});
let first_val = interface.get_env_var("FOO")?;
assert!(first_val.is_none(), "should be None: {first_val:?}");
let second_val = interface.get_env_var("FOO")?;
assert_eq!(Some(Value::test_string("/foo")), second_val);
assert!(test.has_unconsumed_write());
Ok(())
}
#[test]
fn interface_get_current_dir() -> Result<(), ShellError> {
let test = TestCase::new();
let manager = test.engine();
let interface = manager.interface_for_context(0);
start_fake_plugin_call_responder(manager, 1, |_| {
EngineCallResponse::value(Value::test_string("/current/directory"))
});
let val = interface.get_env_var("FOO")?;
assert_eq!(Some(Value::test_string("/current/directory")), val);
assert!(test.has_unconsumed_write());
Ok(())
}
#[test]
fn interface_get_env_vars() -> Result<(), ShellError> {
let test = TestCase::new();
let manager = test.engine();
let interface = manager.interface_for_context(0);
let envs: HashMap<String, Value> = [("FOO".to_owned(), Value::test_string("foo"))]
.into_iter()
.collect();
let envs_clone = envs.clone();
start_fake_plugin_call_responder(manager, 1, move |_| {
EngineCallResponse::ValueMap(envs_clone.clone())
});
let received_envs = interface.get_env_vars()?;
assert_eq!(envs, received_envs);
assert!(test.has_unconsumed_write());
Ok(())
}
#[test]
fn interface_add_env_var() -> Result<(), ShellError> {
let test = TestCase::new();
let manager = test.engine();
let interface = manager.interface_for_context(0);
start_fake_plugin_call_responder(manager, 1, move |_| EngineCallResponse::empty());
interface.add_env_var("FOO", Value::test_string("bar"))?;
assert!(test.has_unconsumed_write());
Ok(())
}
#[test]
fn interface_get_help() -> Result<(), ShellError> {
let test = TestCase::new();
let manager = test.engine();
let interface = manager.interface_for_context(0);
start_fake_plugin_call_responder(manager, 1, move |_| {
EngineCallResponse::value(Value::test_string("help string"))
});
let help = interface.get_help()?;
assert_eq!("help string", help);
assert!(test.has_unconsumed_write());
Ok(())
}
#[test]
fn interface_get_span_contents() -> Result<(), ShellError> {
let test = TestCase::new();
let manager = test.engine();
let interface = manager.interface_for_context(0);
start_fake_plugin_call_responder(manager, 1, move |_| {
EngineCallResponse::value(Value::test_binary(b"test string"))
});
let contents = interface.get_span_contents(Span::test_data())?;
assert_eq!(b"test string", &contents[..]);
assert!(test.has_unconsumed_write());
Ok(())
}
#[test]
fn interface_eval_closure_with_stream() -> Result<(), ShellError> {
let test = TestCase::new();
let manager = test.engine();
let interface = manager.interface_for_context(0);
start_fake_plugin_call_responder(manager, 1, |_| {
EngineCallResponse::PipelineData(PipelineData::Value(Value::test_int(2), None))
});
let result = interface
.eval_closure_with_stream(
&Spanned {
item: Closure {
block_id: 42,
captures: vec![(0, Value::test_int(5))],
},
span: Span::test_data(),
},
vec![Value::test_string("test")],
PipelineData::Empty,
true,
false,
)?
.into_value(Span::test_data());
assert_eq!(Value::test_int(2), result);
// Double check the message that was written, as it's complicated
match test.next_written().expect("nothing written") {
PluginOutput::EngineCall { call, .. } => match call {
EngineCall::EvalClosure {
closure,
positional,
input,
redirect_stdout,
redirect_stderr,
} => {
assert_eq!(42, closure.item.block_id, "closure.item.block_id");
assert_eq!(1, closure.item.captures.len(), "closure.item.captures.len");
assert_eq!(
(0, Value::test_int(5)),
closure.item.captures[0],
"closure.item.captures[0]"
);
assert_eq!(Span::test_data(), closure.span, "closure.span");
assert_eq!(1, positional.len(), "positional.len");
assert_eq!(Value::test_string("test"), positional[0], "positional[0]");
assert!(matches!(input, PipelineDataHeader::Empty));
assert!(redirect_stdout);
assert!(!redirect_stderr);
}
_ => panic!("wrong engine call: {call:?}"),
},
other => panic!("wrong output: {other:?}"),
}
Ok(())
}
#[test]
fn interface_prepare_pipeline_data_serializes_custom_values() -> Result<(), ShellError> {
let interface = TestCase::new().engine().get_interface();
let data = interface.prepare_pipeline_data(
PipelineData::Value(
Value::test_custom_value(Box::new(expected_test_custom_value())),
None,
),
&(),
)?;
let value = data
.into_iter()
.next()
.expect("prepared pipeline data is empty");
let custom_value: &PluginCustomValue = value
.as_custom_value()?
.as_any()
.downcast_ref()
.expect("custom value is not a PluginCustomValue, probably not serialized");
let expected = test_plugin_custom_value();
assert_eq!(expected.name(), custom_value.name());
assert_eq!(expected.data(), custom_value.data());
assert!(custom_value.source().is_none());
Ok(())
}
#[test]
fn interface_prepare_pipeline_data_serializes_custom_values_in_streams() -> Result<(), ShellError> {
let interface = TestCase::new().engine().get_interface();
let data = interface.prepare_pipeline_data(
[Value::test_custom_value(Box::new(
expected_test_custom_value(),
))]
.into_pipeline_data(None),
&(),
)?;
let value = data
.into_iter()
.next()
.expect("prepared pipeline data is empty");
let custom_value: &PluginCustomValue = value
.as_custom_value()?
.as_any()
.downcast_ref()
.expect("custom value is not a PluginCustomValue, probably not serialized");
let expected = test_plugin_custom_value();
assert_eq!(expected.name(), custom_value.name());
assert_eq!(expected.data(), custom_value.data());
assert!(custom_value.source().is_none());
Ok(())
}
/// A non-serializable custom value. Should cause a serialization error
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
enum CantSerialize {
#[serde(skip_serializing)]
BadVariant,
}
#[typetag::serde]
impl CustomValue for CantSerialize {
fn clone_value(&self, span: Span) -> Value {
Value::custom(Box::new(self.clone()), span)
}
fn type_name(&self) -> String {
"CantSerialize".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
}
}
#[test]
fn interface_prepare_pipeline_data_embeds_serialization_errors_in_streams() -> Result<(), ShellError>
{
let interface = TestCase::new().engine().get_interface();
let span = Span::new(40, 60);
let data = interface.prepare_pipeline_data(
[Value::custom(Box::new(CantSerialize::BadVariant), span)].into_pipeline_data(None),
&(),
)?;
let value = data
.into_iter()
.next()
.expect("prepared pipeline data is empty");
match value {
Value::Error { error, .. } => match *error {
ShellError::CustomValueFailedToEncode {
span: error_span, ..
} => {
assert_eq!(span, error_span, "error span not the same as the value's");
}
_ => panic!("expected ShellError::CustomValueFailedToEncode, but got {error:?}"),
},
_ => panic!("unexpected value, not error: {value:?}"),
}
Ok(())
}