mirror of
https://github.com/nushell/nushell.git
synced 2025-04-09 13:28:50 +02:00
# 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`
1192 lines
35 KiB
Rust
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(())
|
|
}
|