mirror of
https://github.com/nushell/nushell.git
synced 2025-06-30 22:50:14 +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:
19
crates/nu-plugin-test-support/Cargo.toml
Normal file
19
crates/nu-plugin-test-support/Cargo.toml
Normal file
@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "nu-plugin-test-support"
|
||||
version = "0.91.1"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-plugin-test-support"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
nu-engine = { path = "../nu-engine", version = "0.91.1", features = ["plugin"] }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.91.1", features = ["plugin"] }
|
||||
nu-parser = { path = "../nu-parser", version = "0.91.1", features = ["plugin"] }
|
||||
nu-plugin = { path = "../nu-plugin", version = "0.91.1" }
|
||||
difference = "2.0"
|
||||
|
||||
[dev-dependencies]
|
||||
typetag = "0.2"
|
||||
serde = "1.0"
|
21
crates/nu-plugin-test-support/LICENSE
Normal file
21
crates/nu-plugin-test-support/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 - 2023 The Nushell Project Developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
4
crates/nu-plugin-test-support/README.md
Normal file
4
crates/nu-plugin-test-support/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# nu-plugin-test-support
|
||||
|
||||
This crate provides helpers for running tests on plugin commands, and is intended to be included in
|
||||
the `dev-dependencies` of plugin crates for testing.
|
71
crates/nu-plugin-test-support/src/fake_persistent_plugin.rs
Normal file
71
crates/nu-plugin-test-support/src/fake_persistent_plugin.rs
Normal file
@ -0,0 +1,71 @@
|
||||
use std::{
|
||||
any::Any,
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
|
||||
use nu_plugin::{GetPlugin, PluginInterface};
|
||||
use nu_protocol::{
|
||||
engine::{EngineState, Stack},
|
||||
PluginGcConfig, PluginIdentity, RegisteredPlugin, ShellError,
|
||||
};
|
||||
|
||||
pub struct FakePersistentPlugin {
|
||||
identity: PluginIdentity,
|
||||
plugin: OnceLock<PluginInterface>,
|
||||
}
|
||||
|
||||
impl FakePersistentPlugin {
|
||||
pub fn new(identity: PluginIdentity) -> FakePersistentPlugin {
|
||||
FakePersistentPlugin {
|
||||
identity,
|
||||
plugin: OnceLock::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initialize(&self, interface: PluginInterface) {
|
||||
self.plugin.set(interface).unwrap_or_else(|_| {
|
||||
panic!("Tried to initialize an already initialized FakePersistentPlugin");
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl RegisteredPlugin for FakePersistentPlugin {
|
||||
fn identity(&self) -> &PluginIdentity {
|
||||
&self.identity
|
||||
}
|
||||
|
||||
fn is_running(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn pid(&self) -> Option<u32> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_gc_config(&self, _gc_config: &PluginGcConfig) {
|
||||
// We don't have a GC
|
||||
}
|
||||
|
||||
fn stop(&self) -> Result<(), ShellError> {
|
||||
// We can't stop
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn as_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl GetPlugin for FakePersistentPlugin {
|
||||
fn get_plugin(
|
||||
self: Arc<Self>,
|
||||
_context: Option<(&EngineState, &mut Stack)>,
|
||||
) -> Result<PluginInterface, ShellError> {
|
||||
self.plugin
|
||||
.get()
|
||||
.cloned()
|
||||
.ok_or_else(|| ShellError::PluginFailedToLoad {
|
||||
msg: "FakePersistentPlugin was not initialized".into(),
|
||||
})
|
||||
}
|
||||
}
|
27
crates/nu-plugin-test-support/src/fake_register.rs
Normal file
27
crates/nu-plugin-test-support/src/fake_register.rs
Normal file
@ -0,0 +1,27 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use nu_plugin::{Plugin, PluginDeclaration};
|
||||
use nu_protocol::{engine::StateWorkingSet, RegisteredPlugin, ShellError};
|
||||
|
||||
use crate::{fake_persistent_plugin::FakePersistentPlugin, spawn_fake_plugin::spawn_fake_plugin};
|
||||
|
||||
/// Register all of the commands from the plugin into the [`StateWorkingSet`]
|
||||
pub fn fake_register(
|
||||
working_set: &mut StateWorkingSet,
|
||||
name: &str,
|
||||
plugin: Arc<impl Plugin + Send + 'static>,
|
||||
) -> Result<Arc<FakePersistentPlugin>, ShellError> {
|
||||
let reg_plugin = spawn_fake_plugin(name, plugin.clone())?;
|
||||
let reg_plugin_clone = reg_plugin.clone();
|
||||
|
||||
for command in plugin.commands() {
|
||||
let signature = command.signature();
|
||||
let decl = PluginDeclaration::new(reg_plugin.clone(), signature);
|
||||
working_set.add_decl(Box::new(decl));
|
||||
}
|
||||
|
||||
let identity = reg_plugin.identity().clone();
|
||||
working_set.find_or_create_plugin(&identity, move || reg_plugin);
|
||||
|
||||
Ok(reg_plugin_clone)
|
||||
}
|
71
crates/nu-plugin-test-support/src/lib.rs
Normal file
71
crates/nu-plugin-test-support/src/lib.rs
Normal file
@ -0,0 +1,71 @@
|
||||
//! Test support for [Nushell](https://nushell.sh) plugins.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! use std::sync::Arc;
|
||||
//!
|
||||
//! use nu_plugin::*;
|
||||
//! use nu_plugin_test_support::PluginTest;
|
||||
//! use nu_protocol::{PluginSignature, PipelineData, Type, Span, Value, LabeledError};
|
||||
//! use nu_protocol::IntoInterruptiblePipelineData;
|
||||
//!
|
||||
//! 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()))
|
||||
//! }
|
||||
//!
|
||||
//! 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)]
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! fn test_lowercase() -> Result<(), LabeledError> {
|
||||
//! let input = vec![Value::test_string("FooBar")].into_pipeline_data(None);
|
||||
//! let output = PluginTest::new("lowercase", LowercasePlugin.into())?
|
||||
//! .eval_with("lowercase", input)?
|
||||
//! .into_value(Span::test_data());
|
||||
//!
|
||||
//! assert_eq!(
|
||||
//! Value::test_list(vec![
|
||||
//! Value::test_string("foobar")
|
||||
//! ]),
|
||||
//! output
|
||||
//! );
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! #
|
||||
//! # test_lowercase().unwrap();
|
||||
//! ```
|
||||
|
||||
mod fake_persistent_plugin;
|
||||
mod fake_register;
|
||||
mod plugin_test;
|
||||
mod spawn_fake_plugin;
|
||||
|
||||
pub use plugin_test::PluginTest;
|
264
crates/nu-plugin-test-support/src/plugin_test.rs
Normal file
264
crates/nu-plugin-test-support/src/plugin_test.rs
Normal file
@ -0,0 +1,264 @@
|
||||
use std::{convert::Infallible, sync::Arc};
|
||||
|
||||
use difference::Changeset;
|
||||
use nu_engine::eval_block;
|
||||
use nu_parser::parse;
|
||||
use nu_plugin::{Plugin, PluginCommand, PluginCustomValue, PluginSource};
|
||||
use nu_protocol::{
|
||||
debugger::WithoutDebug,
|
||||
engine::{EngineState, Stack, StateWorkingSet},
|
||||
report_error_new, LabeledError, PipelineData, PluginExample, ShellError, Span, Value,
|
||||
};
|
||||
|
||||
use crate::fake_register::fake_register;
|
||||
|
||||
/// An object through which plugins can be tested.
|
||||
pub struct PluginTest {
|
||||
engine_state: EngineState,
|
||||
source: Arc<PluginSource>,
|
||||
entry_num: usize,
|
||||
}
|
||||
|
||||
impl PluginTest {
|
||||
/// Create a new test for the given `plugin` named `name`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use nu_plugin_test_support::PluginTest;
|
||||
/// # use nu_protocol::ShellError;
|
||||
/// # use nu_plugin::*;
|
||||
/// # fn test(MyPlugin: impl Plugin + Send + 'static) -> Result<PluginTest, ShellError> {
|
||||
/// PluginTest::new("my_plugin", MyPlugin.into())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn new(
|
||||
name: &str,
|
||||
plugin: Arc<impl Plugin + Send + 'static>,
|
||||
) -> Result<PluginTest, ShellError> {
|
||||
let mut engine_state = EngineState::new();
|
||||
let mut working_set = StateWorkingSet::new(&engine_state);
|
||||
|
||||
let reg_plugin = fake_register(&mut working_set, name, plugin)?;
|
||||
let source = Arc::new(PluginSource::new(reg_plugin));
|
||||
|
||||
engine_state.merge_delta(working_set.render())?;
|
||||
|
||||
Ok(PluginTest {
|
||||
engine_state,
|
||||
source,
|
||||
entry_num: 1,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the [`EngineState`].
|
||||
pub fn engine_state(&self) -> &EngineState {
|
||||
&self.engine_state
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the [`EngineState`].
|
||||
pub fn engine_state_mut(&mut self) -> &mut EngineState {
|
||||
&mut self.engine_state
|
||||
}
|
||||
|
||||
/// Evaluate some Nushell source code with the plugin commands in scope with the given input to
|
||||
/// the pipeline.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use nu_plugin_test_support::PluginTest;
|
||||
/// # use nu_protocol::{ShellError, Span, Value, IntoInterruptiblePipelineData};
|
||||
/// # use nu_plugin::*;
|
||||
/// # fn test(MyPlugin: impl Plugin + Send + 'static) -> Result<(), ShellError> {
|
||||
/// let result = PluginTest::new("my_plugin", MyPlugin.into())?
|
||||
/// .eval_with(
|
||||
/// "my-command",
|
||||
/// vec![Value::test_int(42)].into_pipeline_data(None)
|
||||
/// )?
|
||||
/// .into_value(Span::test_data());
|
||||
/// assert_eq!(Value::test_string("42"), result);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn eval_with(
|
||||
&mut self,
|
||||
nu_source: &str,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let mut working_set = StateWorkingSet::new(&self.engine_state);
|
||||
let fname = format!("entry #{}", self.entry_num);
|
||||
self.entry_num += 1;
|
||||
|
||||
// Parse the source code
|
||||
let block = parse(&mut working_set, Some(&fname), nu_source.as_bytes(), false);
|
||||
|
||||
// Check for parse errors
|
||||
let error = if !working_set.parse_errors.is_empty() {
|
||||
// ShellError doesn't have ParseError, use LabeledError to contain it.
|
||||
let mut error = LabeledError::new("Example failed to parse");
|
||||
error.inner.extend(
|
||||
working_set
|
||||
.parse_errors
|
||||
.iter()
|
||||
.map(LabeledError::from_diagnostic),
|
||||
);
|
||||
Some(ShellError::LabeledError(error.into()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Merge into state
|
||||
self.engine_state.merge_delta(working_set.render())?;
|
||||
|
||||
// Return error if set. We merge the delta even if we have errors so that printing the error
|
||||
// based on the engine state still works.
|
||||
if let Some(error) = error {
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
// Serialize custom values in the input
|
||||
let source = self.source.clone();
|
||||
let input = input.map(
|
||||
move |mut value| match PluginCustomValue::serialize_custom_values_in(&mut value) {
|
||||
Ok(()) => {
|
||||
// Make sure to mark them with the source so they pass correctly, too.
|
||||
PluginCustomValue::add_source(&mut value, &source);
|
||||
value
|
||||
}
|
||||
Err(err) => Value::error(err, value.span()),
|
||||
},
|
||||
None,
|
||||
)?;
|
||||
|
||||
// Eval the block with the input
|
||||
let mut stack = Stack::new().capture();
|
||||
eval_block::<WithoutDebug>(&self.engine_state, &mut stack, &block, input)?.map(
|
||||
|mut value| {
|
||||
// Make sure to deserialize custom values
|
||||
match PluginCustomValue::deserialize_custom_values_in(&mut value) {
|
||||
Ok(()) => value,
|
||||
Err(err) => Value::error(err, value.span()),
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
/// Evaluate some Nushell source code with the plugin commands in scope.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use nu_plugin_test_support::PluginTest;
|
||||
/// # use nu_protocol::{ShellError, Span, Value, IntoInterruptiblePipelineData};
|
||||
/// # use nu_plugin::*;
|
||||
/// # fn test(MyPlugin: impl Plugin + Send + 'static) -> Result<(), ShellError> {
|
||||
/// let result = PluginTest::new("my_plugin", MyPlugin.into())?
|
||||
/// .eval("42 | my-command")?
|
||||
/// .into_value(Span::test_data());
|
||||
/// assert_eq!(Value::test_string("42"), result);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn eval(&mut self, nu_source: &str) -> Result<PipelineData, ShellError> {
|
||||
self.eval_with(nu_source, PipelineData::Empty)
|
||||
}
|
||||
|
||||
/// Test a list of plugin examples. Prints an error for each failing example.
|
||||
///
|
||||
/// See [`.test_command_examples()`] for easier usage of this method on a command's examples.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use nu_plugin_test_support::PluginTest;
|
||||
/// # use nu_protocol::{ShellError, PluginExample, Value};
|
||||
/// # use nu_plugin::*;
|
||||
/// # fn test(MyPlugin: impl Plugin + Send + 'static) -> Result<(), ShellError> {
|
||||
/// PluginTest::new("my_plugin", MyPlugin.into())?
|
||||
/// .test_examples(&[
|
||||
/// PluginExample {
|
||||
/// example: "my-command".into(),
|
||||
/// description: "Run my-command".into(),
|
||||
/// result: Some(Value::test_string("my-command output")),
|
||||
/// },
|
||||
/// ])
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn test_examples(&mut self, examples: &[PluginExample]) -> Result<(), ShellError> {
|
||||
let mut failed = false;
|
||||
|
||||
for example in examples {
|
||||
let mut failed_header = || {
|
||||
failed = true;
|
||||
eprintln!("\x1b[1mExample:\x1b[0m {}", example.example);
|
||||
eprintln!("\x1b[1mDescription:\x1b[0m {}", example.description);
|
||||
};
|
||||
if let Some(expectation) = &example.result {
|
||||
match self.eval(&example.example) {
|
||||
Ok(data) => {
|
||||
let mut value = data.into_value(Span::test_data());
|
||||
|
||||
// Set all of the spans in the value to test_data() to avoid unnecessary
|
||||
// differences when printing
|
||||
let _: Result<(), Infallible> = value.recurse_mut(&mut |here| {
|
||||
here.set_span(Span::test_data());
|
||||
Ok(())
|
||||
});
|
||||
|
||||
// Check for equality with the result
|
||||
if *expectation != value {
|
||||
// If they're not equal, print a diff of the debug format
|
||||
let expectation_formatted = format!("{:#?}", expectation);
|
||||
let value_formatted = format!("{:#?}", value);
|
||||
let diff =
|
||||
Changeset::new(&expectation_formatted, &value_formatted, "\n");
|
||||
failed_header();
|
||||
eprintln!("\x1b[1mResult:\x1b[0m {diff}");
|
||||
} else {
|
||||
println!("{:?}, {:?}", expectation, value);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
// Report the error
|
||||
failed_header();
|
||||
report_error_new(&self.engine_state, &err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !failed {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ShellError::GenericError {
|
||||
error: "Some examples failed. See the error output for details".into(),
|
||||
msg: "".into(),
|
||||
span: None,
|
||||
help: None,
|
||||
inner: vec![],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Test examples from a command.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use nu_plugin_test_support::PluginTest;
|
||||
/// # use nu_protocol::ShellError;
|
||||
/// # use nu_plugin::*;
|
||||
/// # fn test(MyPlugin: impl Plugin + Send + 'static, MyCommand: impl PluginCommand) -> Result<(), ShellError> {
|
||||
/// PluginTest::new("my_plugin", MyPlugin.into())?
|
||||
/// .test_command_examples(&MyCommand)
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn test_command_examples(
|
||||
&mut self,
|
||||
command: &impl PluginCommand,
|
||||
) -> Result<(), ShellError> {
|
||||
self.test_examples(&command.signature().examples)
|
||||
}
|
||||
}
|
77
crates/nu-plugin-test-support/src/spawn_fake_plugin.rs
Normal file
77
crates/nu-plugin-test-support/src/spawn_fake_plugin.rs
Normal file
@ -0,0 +1,77 @@
|
||||
use std::sync::{mpsc, Arc};
|
||||
|
||||
use nu_plugin::{
|
||||
InterfaceManager, Plugin, PluginInput, PluginInterfaceManager, PluginOutput, PluginRead,
|
||||
PluginSource, PluginWrite,
|
||||
};
|
||||
use nu_protocol::{PluginIdentity, ShellError};
|
||||
|
||||
use crate::fake_persistent_plugin::FakePersistentPlugin;
|
||||
|
||||
struct FakePluginRead<T>(mpsc::Receiver<T>);
|
||||
struct FakePluginWrite<T>(mpsc::Sender<T>);
|
||||
|
||||
impl<T> PluginRead<T> for FakePluginRead<T> {
|
||||
fn read(&mut self) -> Result<Option<T>, ShellError> {
|
||||
Ok(self.0.recv().ok())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + Send> PluginWrite<T> for FakePluginWrite<T> {
|
||||
fn write(&self, data: &T) -> Result<(), ShellError> {
|
||||
self.0
|
||||
.send(data.clone())
|
||||
.map_err(|err| ShellError::IOError {
|
||||
msg: err.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn flush(&self) -> Result<(), ShellError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn fake_plugin_channel<T: Clone + Send>() -> (FakePluginRead<T>, FakePluginWrite<T>) {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
(FakePluginRead(rx), FakePluginWrite(tx))
|
||||
}
|
||||
|
||||
/// Spawn a plugin on another thread and return the registration
|
||||
pub(crate) fn spawn_fake_plugin(
|
||||
name: &str,
|
||||
plugin: Arc<impl Plugin + Send + 'static>,
|
||||
) -> Result<Arc<FakePersistentPlugin>, ShellError> {
|
||||
let (input_read, input_write) = fake_plugin_channel::<PluginInput>();
|
||||
let (output_read, output_write) = fake_plugin_channel::<PluginOutput>();
|
||||
|
||||
let identity = PluginIdentity::new_fake(name);
|
||||
let reg_plugin = Arc::new(FakePersistentPlugin::new(identity.clone()));
|
||||
let source = Arc::new(PluginSource::new(reg_plugin.clone()));
|
||||
let mut manager = PluginInterfaceManager::new(source, input_write);
|
||||
|
||||
// Set up the persistent plugin with the interface before continuing
|
||||
let interface = manager.get_interface();
|
||||
interface.hello()?;
|
||||
reg_plugin.initialize(interface);
|
||||
|
||||
// Start the interface reader on another thread
|
||||
std::thread::Builder::new()
|
||||
.name(format!("fake plugin interface reader ({name})"))
|
||||
.spawn(move || manager.consume_all(output_read).expect("Plugin read error"))?;
|
||||
|
||||
// Start the plugin on another thread
|
||||
let name_string = name.to_owned();
|
||||
std::thread::Builder::new()
|
||||
.name(format!("fake plugin runner ({name})"))
|
||||
.spawn(move || {
|
||||
nu_plugin::serve_plugin_io(
|
||||
&*plugin,
|
||||
&name_string,
|
||||
move || input_read,
|
||||
move || output_write,
|
||||
)
|
||||
.expect("Plugin runner error")
|
||||
})?;
|
||||
|
||||
Ok(reg_plugin)
|
||||
}
|
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