Hey. I'm wondering if there has been any discussion about designing the outputs of wit_bindgen::generate!()
with dependency injection in mind for the guest code. Specifically I think it would be nice to have the host imports for the plugin code be mockable so you can test in native Rust without needing a runtime.
To be clear, I have been able to do this with the existing code, just with some serious drawbacks.
This is the sort of thing I'm doing right now:
pub trait Imports {
fn foo(something: &str);
fn bar(other_thing: u8) -> u8;
}
pub struct Plugin<I: Imports> {
_inputs: PhantomData<I>,
}
impl<I: Imports> MyWorld for Plugin<I> {
fn run() {
I::foo("howdy");
I::bar(1)
}
}
then provide a live adapter to Imports
:
struct MyWorldImports;
impl Imports for MyWorldImports {
fn foo(something: &str) {
foo(something);
}
fn bar(other_thing: u8) -> u8 {
bar(other_thing)
}
}
and then I need an alias to export:
type MyPlugin = Plugin<MyWorldInputs;
export_my_world!(MyPlugin);
It would be nice if the imports were also defined as traits and just came with a default impl instead of the free-floating foo
and bar
, but that's not the biggest issue.
It's challenging to write mocks when the trait functions aren't methods, but associated functions. It's possible, yes, with lazy_static
and stuff, but I definitely don't consider that idiomatic Rust.
Thoughts?
TL;DR
It would be nice if wit_bindgen
gave you traits like this, both for your imports and exports:
pub trait Imports {
fn foo(&self, something: &str);
fn bar(&self, other_thing: u8) -> u8;
}
pub trait MyWorld{
fn run(&self);
}
Where those traits were all methods instead of associated functions.
This would make dependency injection easier for tests in your plugin/guest code.
I think in the long run, people will dependency injection test their components... using the component model + runtimes. The benefit of this being that you're testing the component exactly as it is through the exact interface it will have when you use it, not a mock of it.
Tools catering to this don't really exist yet, but you could do it today by writing some tests that use wasmtime + host bindings to initialize your component with its imports mapped to whatever you want. In the future, I expect people (myself included) will make testing harnesses/languages/frameworks that make this very simple and easy to do.
That's an option too. I'd still want mocks in that case though, since we could be dealing with expensive IO or processes we don't have vision into. So, the mock dependencies could be injected into the Runtime for testing imports. That just means a heavier testing framework than what I was suggesting.
As a rule of thumb, I try to push my testing "left", or as early in the process as possible. I'd rather do native Rust tests than a hybrid.
Also, for some projects, your component could be run natively in some contexts and as Wasm in others as well. The tests would need to be on the Rust side in that case.
What's interesting is that if you do it using a runtime, they aren't actually "mocks" they're real virtualizations of the host facilities your components make use of.
I'm not sure I follow.
I'm saying I want to be able to mock things. I want to be able to hit the components' exports with something that resembles a real request and see that it behaves in the expected way. What the host does with the component is out of scope.
the argument for emulating network cards instead of mocking response data is that you actually test 100% of the code that runs on production, there is no mocking code in your program, just the emulated network that answers exactly in the reliable expected way
It's not really about what the "host does with the component". I'm saying that you can implement what your host will eventually provide to the component (e.g. file system, network) as either another component (called a virtualization) or on the host itself in a sandboxed way. Doing so means (like what Ramon said) that you're actually running the exact component code you will run in production against a network facility/capability/resource implementation that conforms to the same spec and behavior as the real one will.
What you're describing sounds great. And I think I would want to use that for integration testing. I want to push left as much as possible and the more tests I can have as unit tests the better.
Fair enough, but I don't expect there's going to be a lot of interest in working on building an additional dependency injection framework into the bindgen when the component model is essentially a dependency injection framework. If you want to see this happen, you'd probably need to prototype it yourself since everyone's focused on getting things ready for preview2.
Best place to start would probably be a wit-bindgen
issue proposing your design.
It wouldn't be a framework. It would just be exposing the imports as a trait + trait impl rather than floating functions and making the export trait methods instead of assoc functions.
Possibly very little work in the macro, but it would be a breaking change for sure.
I deployed a similar idea to test my component without a wasm runtime by just compiling them to native - but then I ran into the following problems:
So I guess the best recommendation is to test them as a wasm component, by using a runtime - from a scripting language (jco and componentize-py might help here, sadly I need more than they support at the moment)
… But as long as I was able to keep it working the debugging experience was great, because I could singlestep between tester and testee in the same debugging environment.
Oh nice. Debugging was another thing I thought of where this might help, but I've never done any debugging with Wasm, so was reticent to bring up.
I don't anticipate my idea to have issues with naming. It should have the same naming scheme as the current bindgen. The only think I'm worried about is I would need to provide the export
macro an instance of a struct, rather than just the struct type. This would have implications on how the component is generated under the hood and could indeed impact the bindings in other languages.
As far as debugging is concerned I can tell that both chrome and wamr work fine even with source level debugging (e.g. setting break points at C/++ or rust lines and inspecting variables inside the guest), but setting up the debugging system is still tricky.
Last updated: Dec 23 2024 at 12:05 UTC