mirror of
https://github.com/nushell/nushell.git
synced 2025-08-09 13:46:03 +02:00
Add test support crate for plugin developers (#12259)
# Description Adds a `nu-plugin-test-support` crate with an interface that supports testing plugins. Unlike in reality, these plugins run in the same process on separate threads. This will allow testing aspects of the plugin internal state and handling serialized plugin custom values easily. We still serialize their custom values and all of the engine to plugin logic is still in play, so from a logical perspective this should still expose any bugs that would have been caused by that. The only difference is that it doesn't run in a different process, and doesn't try to serialize everything to the final wire format for stdin/stdout. TODO still: - [x] Clean up warnings about private types exposed in trait definition - [x] Automatically deserialize plugin custom values in the result so they can be inspected - [x] Automatic plugin examples test function - [x] Write a bit more documentation - [x] More tests - [x] Add MIT License file to new crate # User-Facing Changes Plugin developers get a nice way to test their plugins. # Tests + Formatting Run the tests with `cargo test -p nu-plugin-test-support -- --show-output` to see some examples of what the failing test output for examples can look like. I used the `difference` crate (MIT licensed) to make it look nice. - 🟢 `toolkit fmt` - 🟢 `toolkit clippy` - 🟢 `toolkit test` - 🟢 `toolkit test stdlib` # After Submitting - [ ] Add a section to the book about testing - [ ] Test some of the example plugins this way - [ ] Add example tests to nu_plugin_template so plugin developers have something to start with
This commit is contained in:
129
crates/nu-plugin-test-support/tests/custom_value/mod.rs
Normal file
129
crates/nu-plugin-test-support/tests/custom_value/mod.rs
Normal file
@ -0,0 +1,129 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use nu_plugin::{EngineInterface, EvaluatedCall, Plugin, SimplePluginCommand};
|
||||
use nu_plugin_test_support::PluginTest;
|
||||
use nu_protocol::{
|
||||
CustomValue, LabeledError, PipelineData, PluginExample, PluginSignature, ShellError, Span,
|
||||
Type, Value,
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialOrd, Ord, PartialEq, Eq)]
|
||||
struct CustomU32(u32);
|
||||
|
||||
impl CustomU32 {
|
||||
pub fn into_value(self, span: Span) -> Value {
|
||||
Value::custom_value(Box::new(self), span)
|
||||
}
|
||||
}
|
||||
|
||||
#[typetag::serde]
|
||||
impl CustomValue for CustomU32 {
|
||||
fn clone_value(&self, span: Span) -> Value {
|
||||
self.clone().into_value(span)
|
||||
}
|
||||
|
||||
fn type_name(&self) -> String {
|
||||
"CustomU32".into()
|
||||
}
|
||||
|
||||
fn to_base_value(&self, span: Span) -> Result<Value, ShellError> {
|
||||
Ok(Value::int(self.0 as i64, span))
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn partial_cmp(&self, other: &Value) -> Option<Ordering> {
|
||||
other
|
||||
.as_custom_value()
|
||||
.ok()
|
||||
.and_then(|cv| cv.as_any().downcast_ref::<CustomU32>())
|
||||
.and_then(|other_u32| PartialOrd::partial_cmp(self, other_u32))
|
||||
}
|
||||
}
|
||||
|
||||
struct CustomU32Plugin;
|
||||
struct IntoU32;
|
||||
struct IntoIntFromU32;
|
||||
|
||||
impl Plugin for CustomU32Plugin {
|
||||
fn commands(&self) -> Vec<Box<dyn nu_plugin::PluginCommand<Plugin = Self>>> {
|
||||
vec![Box::new(IntoU32), Box::new(IntoIntFromU32)]
|
||||
}
|
||||
}
|
||||
|
||||
impl SimplePluginCommand for IntoU32 {
|
||||
type Plugin = CustomU32Plugin;
|
||||
|
||||
fn signature(&self) -> PluginSignature {
|
||||
PluginSignature::build("into u32")
|
||||
.input_output_type(Type::Int, Type::Custom("CustomU32".into()))
|
||||
.plugin_examples(vec![PluginExample {
|
||||
example: "340 | into u32".into(),
|
||||
description: "Make a u32".into(),
|
||||
result: Some(CustomU32(340).into_value(Span::test_data())),
|
||||
}])
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_plugin: &Self::Plugin,
|
||||
_engine: &EngineInterface,
|
||||
call: &EvaluatedCall,
|
||||
input: &Value,
|
||||
) -> Result<Value, LabeledError> {
|
||||
let value: i64 = input.as_int()?;
|
||||
let value_u32 = u32::try_from(value).map_err(|err| {
|
||||
LabeledError::new(format!("Not a valid u32: {value}"))
|
||||
.with_label(err.to_string(), input.span())
|
||||
})?;
|
||||
Ok(CustomU32(value_u32).into_value(call.head))
|
||||
}
|
||||
}
|
||||
|
||||
impl SimplePluginCommand for IntoIntFromU32 {
|
||||
type Plugin = CustomU32Plugin;
|
||||
|
||||
fn signature(&self) -> PluginSignature {
|
||||
PluginSignature::build("into int from u32")
|
||||
.input_output_type(Type::Custom("CustomU32".into()), Type::Int)
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_plugin: &Self::Plugin,
|
||||
_engine: &EngineInterface,
|
||||
call: &EvaluatedCall,
|
||||
input: &Value,
|
||||
) -> Result<Value, LabeledError> {
|
||||
let value: &CustomU32 = input
|
||||
.as_custom_value()?
|
||||
.as_any()
|
||||
.downcast_ref()
|
||||
.ok_or_else(|| ShellError::TypeMismatch {
|
||||
err_message: "expected CustomU32".into(),
|
||||
span: input.span(),
|
||||
})?;
|
||||
Ok(Value::int(value.0 as i64, call.head))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_into_u32_examples() -> Result<(), ShellError> {
|
||||
PluginTest::new("custom_u32", CustomU32Plugin.into())?.test_command_examples(&IntoU32)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_into_int_from_u32() -> Result<(), ShellError> {
|
||||
let result = PluginTest::new("custom_u32", CustomU32Plugin.into())?
|
||||
.eval_with(
|
||||
"into int from u32",
|
||||
PipelineData::Value(CustomU32(42).into_value(Span::test_data()), None),
|
||||
)?
|
||||
.into_value(Span::test_data());
|
||||
assert_eq!(Value::test_int(42), result);
|
||||
Ok(())
|
||||
}
|
65
crates/nu-plugin-test-support/tests/hello/mod.rs
Normal file
65
crates/nu-plugin-test-support/tests/hello/mod.rs
Normal file
@ -0,0 +1,65 @@
|
||||
//! Extended from `nu-plugin` examples.
|
||||
|
||||
use nu_plugin::*;
|
||||
use nu_plugin_test_support::PluginTest;
|
||||
use nu_protocol::{LabeledError, PluginExample, PluginSignature, ShellError, Type, Value};
|
||||
|
||||
struct HelloPlugin;
|
||||
struct Hello;
|
||||
|
||||
impl Plugin for HelloPlugin {
|
||||
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
|
||||
vec![Box::new(Hello)]
|
||||
}
|
||||
}
|
||||
|
||||
impl SimplePluginCommand for Hello {
|
||||
type Plugin = HelloPlugin;
|
||||
|
||||
fn signature(&self) -> PluginSignature {
|
||||
PluginSignature::build("hello")
|
||||
.input_output_type(Type::Nothing, Type::String)
|
||||
.plugin_examples(vec![PluginExample {
|
||||
example: "hello".into(),
|
||||
description: "Print a friendly greeting".into(),
|
||||
result: Some(Value::test_string("Hello, World!")),
|
||||
}])
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_plugin: &HelloPlugin,
|
||||
_engine: &EngineInterface,
|
||||
call: &EvaluatedCall,
|
||||
_input: &Value,
|
||||
) -> Result<Value, LabeledError> {
|
||||
Ok(Value::string("Hello, World!".to_owned(), call.head))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_specified_examples() -> Result<(), ShellError> {
|
||||
PluginTest::new("hello", HelloPlugin.into())?.test_command_examples(&Hello)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_an_error_causing_example() -> Result<(), ShellError> {
|
||||
let result = PluginTest::new("hello", HelloPlugin.into())?.test_examples(&[PluginExample {
|
||||
example: "hello --unknown-flag".into(),
|
||||
description: "Run hello with an unknown flag".into(),
|
||||
result: Some(Value::test_string("Hello, World!")),
|
||||
}]);
|
||||
assert!(result.is_err());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_an_example_with_the_wrong_result() -> Result<(), ShellError> {
|
||||
let result = PluginTest::new("hello", HelloPlugin.into())?.test_examples(&[PluginExample {
|
||||
example: "hello".into(),
|
||||
description: "Run hello but the example result is wrong".into(),
|
||||
result: Some(Value::test_string("Goodbye, World!")),
|
||||
}]);
|
||||
assert!(result.is_err());
|
||||
Ok(())
|
||||
}
|
76
crates/nu-plugin-test-support/tests/lowercase/mod.rs
Normal file
76
crates/nu-plugin-test-support/tests/lowercase/mod.rs
Normal file
@ -0,0 +1,76 @@
|
||||
use nu_plugin::*;
|
||||
use nu_plugin_test_support::PluginTest;
|
||||
use nu_protocol::{
|
||||
IntoInterruptiblePipelineData, LabeledError, PipelineData, PluginExample, PluginSignature,
|
||||
ShellError, Span, Type, Value,
|
||||
};
|
||||
|
||||
struct LowercasePlugin;
|
||||
struct Lowercase;
|
||||
|
||||
impl PluginCommand for Lowercase {
|
||||
type Plugin = LowercasePlugin;
|
||||
|
||||
fn signature(&self) -> PluginSignature {
|
||||
PluginSignature::build("lowercase")
|
||||
.usage("Convert each string in a stream to lowercase")
|
||||
.input_output_type(
|
||||
Type::List(Type::String.into()),
|
||||
Type::List(Type::String.into()),
|
||||
)
|
||||
.plugin_examples(vec![PluginExample {
|
||||
example: r#"[Hello wORLD] | lowercase"#.into(),
|
||||
description: "Lowercase a list of strings".into(),
|
||||
result: Some(Value::test_list(vec![
|
||||
Value::test_string("hello"),
|
||||
Value::test_string("world"),
|
||||
])),
|
||||
}])
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_plugin: &LowercasePlugin,
|
||||
_engine: &EngineInterface,
|
||||
call: &EvaluatedCall,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, LabeledError> {
|
||||
let span = call.head;
|
||||
Ok(input.map(
|
||||
move |value| {
|
||||
value
|
||||
.as_str()
|
||||
.map(|string| Value::string(string.to_lowercase(), span))
|
||||
// Errors in a stream should be returned as values.
|
||||
.unwrap_or_else(|err| Value::error(err, span))
|
||||
},
|
||||
None,
|
||||
)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for LowercasePlugin {
|
||||
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
|
||||
vec![Box::new(Lowercase)]
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lowercase_using_eval_with() -> Result<(), ShellError> {
|
||||
let result = PluginTest::new("lowercase", LowercasePlugin.into())?.eval_with(
|
||||
"lowercase",
|
||||
vec![Value::test_string("HeLlO wOrLd")].into_pipeline_data(None),
|
||||
)?;
|
||||
|
||||
assert_eq!(
|
||||
Value::test_list(vec![Value::test_string("hello world")]),
|
||||
result.into_value(Span::test_data())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lowercase_examples() -> Result<(), ShellError> {
|
||||
PluginTest::new("lowercase", LowercasePlugin.into())?.test_command_examples(&Lowercase)
|
||||
}
|
3
crates/nu-plugin-test-support/tests/main.rs
Normal file
3
crates/nu-plugin-test-support/tests/main.rs
Normal file
@ -0,0 +1,3 @@
|
||||
mod custom_value;
|
||||
mod hello;
|
||||
mod lowercase;
|
Reference in New Issue
Block a user