mirror of
https://github.com/nushell/nushell.git
synced 2025-05-29 22:29:06 +02:00
# Description Adds support for the following operations on plugin custom values, in addition to `to_base_value` which was already present: - `follow_path_int()` - `follow_path_string()` - `partial_cmp()` - `operation()` - `Drop` (notification, if opted into with `CustomValue::notify_plugin_on_drop`) There are additionally customizable methods within the `Plugin` and `StreamingPlugin` traits for implementing these functions in a way that requires access to the plugin state, as a registered handle model such as might be used in a dataframes plugin would. `Value::append` was also changed to handle custom values correctly. # User-Facing Changes - Signature of `CustomValue::follow_path_string` and `CustomValue::follow_path_int` changed to give access to the span of the custom value itself, useful for some errors. - Plugins using custom values have to be recompiled because the engine will try to do custom value operations that aren't supported - Plugins can do more things 🎉 # Tests + Formatting Tests were added for all of the new custom values functionality. - 🟢 `toolkit fmt` - 🟢 `toolkit clippy` - 🟢 `toolkit test` - 🟢 `toolkit test stdlib` # After Submitting - [ ] Document protocol reference `CustomValueOp` variants: - [ ] `FollowPathInt` - [ ] `FollowPathString` - [ ] `PartialCmp` - [ ] `Operation` - [ ] `Dropped` - [ ] Document `notify_on_drop` optional field in `PluginCustomValue`
558 lines
21 KiB
Rust
558 lines
21 KiB
Rust
macro_rules! generate_tests {
|
|
($encoder:expr) => {
|
|
use crate::protocol::{
|
|
CallInfo, CustomValueOp, EvaluatedCall, LabeledError, PipelineDataHeader, PluginCall,
|
|
PluginCallResponse, PluginCustomValue, PluginInput, PluginOption, PluginOutput,
|
|
StreamData, StreamMessage,
|
|
};
|
|
use nu_protocol::{PluginSignature, Span, Spanned, SyntaxShape, Value};
|
|
|
|
#[test]
|
|
fn decode_eof() {
|
|
let mut buffer: &[u8] = &[];
|
|
let encoder = $encoder;
|
|
let result: Option<PluginInput> = encoder
|
|
.decode(&mut buffer)
|
|
.expect("eof should not result in an error");
|
|
assert!(result.is_none(), "decode result: {result:?}");
|
|
let result: Option<PluginOutput> = encoder
|
|
.decode(&mut buffer)
|
|
.expect("eof should not result in an error");
|
|
assert!(result.is_none(), "decode result: {result:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn decode_io_error() {
|
|
struct ErrorProducer;
|
|
impl std::io::Read for ErrorProducer {
|
|
fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
|
|
Err(std::io::Error::from(std::io::ErrorKind::ConnectionReset))
|
|
}
|
|
}
|
|
let encoder = $encoder;
|
|
let mut buffered = std::io::BufReader::new(ErrorProducer);
|
|
match Encoder::<PluginInput>::decode(&encoder, &mut buffered) {
|
|
Ok(_) => panic!("decode: i/o error was not passed through"),
|
|
Err(ShellError::IOError { .. }) => (), // okay
|
|
Err(other) => panic!(
|
|
"decode: got other error, should have been a \
|
|
ShellError::IOError: {other:?}"
|
|
),
|
|
}
|
|
match Encoder::<PluginOutput>::decode(&encoder, &mut buffered) {
|
|
Ok(_) => panic!("decode: i/o error was not passed through"),
|
|
Err(ShellError::IOError { .. }) => (), // okay
|
|
Err(other) => panic!(
|
|
"decode: got other error, should have been a \
|
|
ShellError::IOError: {other:?}"
|
|
),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn decode_gibberish() {
|
|
// just a sequence of bytes that shouldn't be valid in anything we use
|
|
let gibberish: &[u8] = &[
|
|
0, 80, 74, 85, 117, 122, 86, 100, 74, 115, 20, 104, 55, 98, 67, 203, 83, 85, 77,
|
|
112, 74, 79, 254, 71, 80,
|
|
];
|
|
let encoder = $encoder;
|
|
|
|
let mut buffered = std::io::BufReader::new(&gibberish[..]);
|
|
match Encoder::<PluginInput>::decode(&encoder, &mut buffered) {
|
|
Ok(value) => panic!("decode: parsed successfully => {value:?}"),
|
|
Err(ShellError::PluginFailedToDecode { .. }) => (), // okay
|
|
Err(other) => panic!(
|
|
"decode: got other error, should have been a \
|
|
ShellError::PluginFailedToDecode: {other:?}"
|
|
),
|
|
}
|
|
|
|
let mut buffered = std::io::BufReader::new(&gibberish[..]);
|
|
match Encoder::<PluginOutput>::decode(&encoder, &mut buffered) {
|
|
Ok(value) => panic!("decode: parsed successfully => {value:?}"),
|
|
Err(ShellError::PluginFailedToDecode { .. }) => (), // okay
|
|
Err(other) => panic!(
|
|
"decode: got other error, should have been a \
|
|
ShellError::PluginFailedToDecode: {other:?}"
|
|
),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn call_round_trip_signature() {
|
|
let plugin_call = PluginCall::Signature;
|
|
let plugin_input = PluginInput::Call(0, plugin_call);
|
|
let encoder = $encoder;
|
|
|
|
let mut buffer: Vec<u8> = Vec::new();
|
|
encoder
|
|
.encode(&plugin_input, &mut buffer)
|
|
.expect("unable to serialize message");
|
|
let returned = encoder
|
|
.decode(&mut buffer.as_slice())
|
|
.expect("unable to deserialize message")
|
|
.expect("eof");
|
|
|
|
match returned {
|
|
PluginInput::Call(0, PluginCall::Signature) => {}
|
|
_ => panic!("decoded into wrong value: {returned:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn call_round_trip_run() {
|
|
let name = "test".to_string();
|
|
|
|
let input = Value::bool(false, Span::new(1, 20));
|
|
|
|
let call = EvaluatedCall {
|
|
head: Span::new(0, 10),
|
|
positional: vec![
|
|
Value::float(1.0, Span::new(0, 10)),
|
|
Value::string("something", Span::new(0, 10)),
|
|
],
|
|
named: vec![(
|
|
Spanned {
|
|
item: "name".to_string(),
|
|
span: Span::new(0, 10),
|
|
},
|
|
Some(Value::float(1.0, Span::new(0, 10))),
|
|
)],
|
|
};
|
|
|
|
let plugin_call = PluginCall::Run(CallInfo {
|
|
name: name.clone(),
|
|
call: call.clone(),
|
|
input: PipelineDataHeader::Value(input.clone()),
|
|
});
|
|
|
|
let plugin_input = PluginInput::Call(1, plugin_call);
|
|
|
|
let encoder = $encoder;
|
|
let mut buffer: Vec<u8> = Vec::new();
|
|
encoder
|
|
.encode(&plugin_input, &mut buffer)
|
|
.expect("unable to serialize message");
|
|
let returned = encoder
|
|
.decode(&mut buffer.as_slice())
|
|
.expect("unable to deserialize message")
|
|
.expect("eof");
|
|
|
|
match returned {
|
|
PluginInput::Call(1, PluginCall::Run(call_info)) => {
|
|
assert_eq!(name, call_info.name);
|
|
assert_eq!(PipelineDataHeader::Value(input), call_info.input);
|
|
assert_eq!(call.head, call_info.call.head);
|
|
assert_eq!(call.positional.len(), call_info.call.positional.len());
|
|
|
|
call.positional
|
|
.iter()
|
|
.zip(call_info.call.positional.iter())
|
|
.for_each(|(lhs, rhs)| assert_eq!(lhs, rhs));
|
|
|
|
call.named
|
|
.iter()
|
|
.zip(call_info.call.named.iter())
|
|
.for_each(|(lhs, rhs)| {
|
|
// Comparing the keys
|
|
assert_eq!(lhs.0.item, rhs.0.item);
|
|
|
|
match (&lhs.1, &rhs.1) {
|
|
(None, None) => {}
|
|
(Some(a), Some(b)) => assert_eq!(a, b),
|
|
_ => panic!("not matching values"),
|
|
}
|
|
});
|
|
}
|
|
_ => panic!("decoded into wrong value: {returned:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn call_round_trip_customvalueop() {
|
|
let data = vec![1, 2, 3, 4, 5, 6, 7];
|
|
let span = Span::new(0, 20);
|
|
|
|
let custom_value_op = PluginCall::CustomValueOp(
|
|
Spanned {
|
|
item: PluginCustomValue::new("Foo".into(), data.clone(), false, None),
|
|
span,
|
|
},
|
|
CustomValueOp::ToBaseValue,
|
|
);
|
|
|
|
let plugin_input = PluginInput::Call(2, custom_value_op);
|
|
|
|
let encoder = $encoder;
|
|
let mut buffer: Vec<u8> = Vec::new();
|
|
encoder
|
|
.encode(&plugin_input, &mut buffer)
|
|
.expect("unable to serialize message");
|
|
let returned = encoder
|
|
.decode(&mut buffer.as_slice())
|
|
.expect("unable to deserialize message")
|
|
.expect("eof");
|
|
|
|
match returned {
|
|
PluginInput::Call(2, PluginCall::CustomValueOp(val, op)) => {
|
|
assert_eq!("Foo", val.item.name());
|
|
assert_eq!(data, val.item.data());
|
|
assert_eq!(span, val.span);
|
|
#[allow(unreachable_patterns)]
|
|
match op {
|
|
CustomValueOp::ToBaseValue => (),
|
|
_ => panic!("wrong op: {op:?}"),
|
|
}
|
|
}
|
|
_ => panic!("decoded into wrong value: {returned:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn response_round_trip_signature() {
|
|
let signature = PluginSignature::build("nu-plugin")
|
|
.required("first", SyntaxShape::String, "first required")
|
|
.required("second", SyntaxShape::Int, "second required")
|
|
.required_named("first-named", SyntaxShape::String, "first named", Some('f'))
|
|
.required_named(
|
|
"second-named",
|
|
SyntaxShape::String,
|
|
"second named",
|
|
Some('s'),
|
|
)
|
|
.rest("remaining", SyntaxShape::Int, "remaining");
|
|
|
|
let response = PluginCallResponse::Signature(vec![signature.clone()]);
|
|
let output = PluginOutput::CallResponse(3, response);
|
|
|
|
let encoder = $encoder;
|
|
let mut buffer: Vec<u8> = Vec::new();
|
|
encoder
|
|
.encode(&output, &mut buffer)
|
|
.expect("unable to serialize message");
|
|
let returned = encoder
|
|
.decode(&mut buffer.as_slice())
|
|
.expect("unable to deserialize message")
|
|
.expect("eof");
|
|
|
|
match returned {
|
|
PluginOutput::CallResponse(
|
|
3,
|
|
PluginCallResponse::Signature(returned_signature),
|
|
) => {
|
|
assert_eq!(returned_signature.len(), 1);
|
|
assert_eq!(signature.sig.name, returned_signature[0].sig.name);
|
|
assert_eq!(signature.sig.usage, returned_signature[0].sig.usage);
|
|
assert_eq!(
|
|
signature.sig.extra_usage,
|
|
returned_signature[0].sig.extra_usage
|
|
);
|
|
assert_eq!(signature.sig.is_filter, returned_signature[0].sig.is_filter);
|
|
|
|
signature
|
|
.sig
|
|
.required_positional
|
|
.iter()
|
|
.zip(returned_signature[0].sig.required_positional.iter())
|
|
.for_each(|(lhs, rhs)| assert_eq!(lhs, rhs));
|
|
|
|
signature
|
|
.sig
|
|
.optional_positional
|
|
.iter()
|
|
.zip(returned_signature[0].sig.optional_positional.iter())
|
|
.for_each(|(lhs, rhs)| assert_eq!(lhs, rhs));
|
|
|
|
signature
|
|
.sig
|
|
.named
|
|
.iter()
|
|
.zip(returned_signature[0].sig.named.iter())
|
|
.for_each(|(lhs, rhs)| assert_eq!(lhs, rhs));
|
|
|
|
assert_eq!(
|
|
signature.sig.rest_positional,
|
|
returned_signature[0].sig.rest_positional,
|
|
);
|
|
}
|
|
_ => panic!("decoded into wrong value: {returned:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn response_round_trip_value() {
|
|
let value = Value::int(10, Span::new(2, 30));
|
|
|
|
let response = PluginCallResponse::value(value.clone());
|
|
let output = PluginOutput::CallResponse(4, response);
|
|
|
|
let encoder = $encoder;
|
|
let mut buffer: Vec<u8> = Vec::new();
|
|
encoder
|
|
.encode(&output, &mut buffer)
|
|
.expect("unable to serialize message");
|
|
let returned = encoder
|
|
.decode(&mut buffer.as_slice())
|
|
.expect("unable to deserialize message")
|
|
.expect("eof");
|
|
|
|
match returned {
|
|
PluginOutput::CallResponse(
|
|
4,
|
|
PluginCallResponse::PipelineData(PipelineDataHeader::Value(returned_value)),
|
|
) => {
|
|
assert_eq!(value, returned_value)
|
|
}
|
|
_ => panic!("decoded into wrong value: {returned:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn response_round_trip_plugin_custom_value() {
|
|
let name = "test";
|
|
|
|
let data = vec![1, 2, 3, 4, 5];
|
|
let span = Span::new(2, 30);
|
|
|
|
let value = Value::custom_value(
|
|
Box::new(PluginCustomValue::new(
|
|
name.into(),
|
|
data.clone(),
|
|
true,
|
|
None,
|
|
)),
|
|
span,
|
|
);
|
|
|
|
let response = PluginCallResponse::PipelineData(PipelineDataHeader::Value(value));
|
|
let output = PluginOutput::CallResponse(5, response);
|
|
|
|
let encoder = $encoder;
|
|
let mut buffer: Vec<u8> = Vec::new();
|
|
encoder
|
|
.encode(&output, &mut buffer)
|
|
.expect("unable to serialize message");
|
|
let returned = encoder
|
|
.decode(&mut buffer.as_slice())
|
|
.expect("unable to deserialize message")
|
|
.expect("eof");
|
|
|
|
match returned {
|
|
PluginOutput::CallResponse(
|
|
5,
|
|
PluginCallResponse::PipelineData(PipelineDataHeader::Value(returned_value)),
|
|
) => {
|
|
assert_eq!(span, returned_value.span());
|
|
|
|
if let Some(plugin_val) = returned_value
|
|
.as_custom_value()
|
|
.unwrap()
|
|
.as_any()
|
|
.downcast_ref::<PluginCustomValue>()
|
|
{
|
|
assert_eq!(name, plugin_val.name());
|
|
assert_eq!(data, plugin_val.data());
|
|
assert!(plugin_val.notify_on_drop());
|
|
} else {
|
|
panic!("returned CustomValue is not a PluginCustomValue");
|
|
}
|
|
}
|
|
_ => panic!("decoded into wrong value: {returned:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn response_round_trip_error() {
|
|
let error = LabeledError {
|
|
label: "label".into(),
|
|
msg: "msg".into(),
|
|
span: Some(Span::new(2, 30)),
|
|
};
|
|
let response = PluginCallResponse::Error(error.clone());
|
|
let output = PluginOutput::CallResponse(6, response);
|
|
|
|
let encoder = $encoder;
|
|
let mut buffer: Vec<u8> = Vec::new();
|
|
encoder
|
|
.encode(&output, &mut buffer)
|
|
.expect("unable to serialize message");
|
|
let returned = encoder
|
|
.decode(&mut buffer.as_slice())
|
|
.expect("unable to deserialize message")
|
|
.expect("eof");
|
|
|
|
match returned {
|
|
PluginOutput::CallResponse(6, PluginCallResponse::Error(msg)) => {
|
|
assert_eq!(error, msg)
|
|
}
|
|
_ => panic!("decoded into wrong value: {returned:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn response_round_trip_error_none() {
|
|
let error = LabeledError {
|
|
label: "label".into(),
|
|
msg: "msg".into(),
|
|
span: None,
|
|
};
|
|
let response = PluginCallResponse::Error(error.clone());
|
|
let output = PluginOutput::CallResponse(7, response);
|
|
|
|
let encoder = $encoder;
|
|
let mut buffer: Vec<u8> = Vec::new();
|
|
encoder
|
|
.encode(&output, &mut buffer)
|
|
.expect("unable to serialize message");
|
|
let returned = encoder
|
|
.decode(&mut buffer.as_slice())
|
|
.expect("unable to deserialize message")
|
|
.expect("eof");
|
|
|
|
match returned {
|
|
PluginOutput::CallResponse(7, PluginCallResponse::Error(msg)) => {
|
|
assert_eq!(error, msg)
|
|
}
|
|
_ => panic!("decoded into wrong value: {returned:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn input_round_trip_stream_data_list() {
|
|
let span = Span::new(12, 30);
|
|
let item = Value::int(1, span);
|
|
|
|
let stream_data = StreamData::List(item.clone());
|
|
let plugin_input = PluginInput::Stream(StreamMessage::Data(0, stream_data));
|
|
|
|
let encoder = $encoder;
|
|
let mut buffer: Vec<u8> = Vec::new();
|
|
encoder
|
|
.encode(&plugin_input, &mut buffer)
|
|
.expect("unable to serialize message");
|
|
let returned = encoder
|
|
.decode(&mut buffer.as_slice())
|
|
.expect("unable to deserialize message")
|
|
.expect("eof");
|
|
|
|
match returned {
|
|
PluginInput::Stream(StreamMessage::Data(id, StreamData::List(list_data))) => {
|
|
assert_eq!(0, id);
|
|
assert_eq!(item, list_data);
|
|
}
|
|
_ => panic!("decoded into wrong value: {returned:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn input_round_trip_stream_data_raw() {
|
|
let data = b"Hello world";
|
|
|
|
let stream_data = StreamData::Raw(Ok(data.to_vec()));
|
|
let plugin_input = PluginInput::Stream(StreamMessage::Data(1, stream_data));
|
|
|
|
let encoder = $encoder;
|
|
let mut buffer: Vec<u8> = Vec::new();
|
|
encoder
|
|
.encode(&plugin_input, &mut buffer)
|
|
.expect("unable to serialize message");
|
|
let returned = encoder
|
|
.decode(&mut buffer.as_slice())
|
|
.expect("unable to deserialize message")
|
|
.expect("eof");
|
|
|
|
match returned {
|
|
PluginInput::Stream(StreamMessage::Data(id, StreamData::Raw(bytes))) => {
|
|
assert_eq!(1, id);
|
|
match bytes {
|
|
Ok(bytes) => assert_eq!(data, &bytes[..]),
|
|
Err(err) => panic!("decoded into error variant: {err:?}"),
|
|
}
|
|
}
|
|
_ => panic!("decoded into wrong value: {returned:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn output_round_trip_stream_data_list() {
|
|
let span = Span::new(12, 30);
|
|
let item = Value::int(1, span);
|
|
|
|
let stream_data = StreamData::List(item.clone());
|
|
let plugin_output = PluginOutput::Stream(StreamMessage::Data(4, stream_data));
|
|
|
|
let encoder = $encoder;
|
|
let mut buffer: Vec<u8> = Vec::new();
|
|
encoder
|
|
.encode(&plugin_output, &mut buffer)
|
|
.expect("unable to serialize message");
|
|
let returned = encoder
|
|
.decode(&mut buffer.as_slice())
|
|
.expect("unable to deserialize message")
|
|
.expect("eof");
|
|
|
|
match returned {
|
|
PluginOutput::Stream(StreamMessage::Data(id, StreamData::List(list_data))) => {
|
|
assert_eq!(4, id);
|
|
assert_eq!(item, list_data);
|
|
}
|
|
_ => panic!("decoded into wrong value: {returned:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn output_round_trip_stream_data_raw() {
|
|
let data = b"Hello world";
|
|
|
|
let stream_data = StreamData::Raw(Ok(data.to_vec()));
|
|
let plugin_output = PluginOutput::Stream(StreamMessage::Data(5, stream_data));
|
|
|
|
let encoder = $encoder;
|
|
let mut buffer: Vec<u8> = Vec::new();
|
|
encoder
|
|
.encode(&plugin_output, &mut buffer)
|
|
.expect("unable to serialize message");
|
|
let returned = encoder
|
|
.decode(&mut buffer.as_slice())
|
|
.expect("unable to deserialize message")
|
|
.expect("eof");
|
|
|
|
match returned {
|
|
PluginOutput::Stream(StreamMessage::Data(id, StreamData::Raw(bytes))) => {
|
|
assert_eq!(5, id);
|
|
match bytes {
|
|
Ok(bytes) => assert_eq!(data, &bytes[..]),
|
|
Err(err) => panic!("decoded into error variant: {err:?}"),
|
|
}
|
|
}
|
|
_ => panic!("decoded into wrong value: {returned:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn output_round_trip_option() {
|
|
let plugin_output = PluginOutput::Option(PluginOption::GcDisabled(true));
|
|
|
|
let encoder = $encoder;
|
|
let mut buffer: Vec<u8> = Vec::new();
|
|
encoder
|
|
.encode(&plugin_output, &mut buffer)
|
|
.expect("unable to serialize message");
|
|
let returned = encoder
|
|
.decode(&mut buffer.as_slice())
|
|
.expect("unable to deserialize message")
|
|
.expect("eof");
|
|
|
|
match returned {
|
|
PluginOutput::Option(PluginOption::GcDisabled(disabled)) => {
|
|
assert!(disabled);
|
|
}
|
|
_ => panic!("decoded into wrong value: {returned:?}"),
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
pub(crate) use generate_tests;
|