I have a library that has structs with generic types that are constrained by traits. These can't be trait objects because they have associated functions and async functions, so I use generics instead. I have concrete implementations of the traits in a different crate because I want to keep the library agnostic about how the traits are implemented. My specific use case is that I need my library to be async runtime agnostic, and I have certain traits like Locker and Sleeper that wrap tokio but could be easily made to wrap other things.
The issue is this:
- A create
base
defines the traits and includes code with generics that use the traits - A crate
impls
includes concrete implementations of the trate - It's hard to test the code in
base
without concrete implementations.
There are a few things I've tried:
- Create a third trait with the tests that depends on both
base
andimpls
and instantiates things inbase
with concrete implementations fromimpls
. This works, but it makes it necessary to make things "public for test", which is an anti-pattern I'm happy to be able to usually avoid in rust. - While I can't create a dependency cycle between
base
andimpls
, it works to haveimpls
depend onbase
andbase
declare adev-dependency
onimpls
. This allows me to use some things fromimpls
in the tests inbase
, but here's the problem: *If a concrete type inimpls
implements a trait inbase
, test code inbase
doesn't see the trait implementation even with all the right imports. The reason is that the constraints inbase
want the concrete class to implementcreate::SomeTrait
, but the concrete implementation appears to implementbase::SomeTrait
instead. At least, I think this is what's happening. These are actually the same thing, but rust doesn't seem to realize it.
Here's a compete example:
top-level Cargo.toml
[workspace]
resolver = "2"
members = [
"base",
"impls",
]
base/Cargo.toml:
[package]
name = "base"
version = "0.1.0"
edition = "2021"
[dev-dependencies]
impls = { path = "../impls" }
impls/Cargo.toml:
[package]
name = "impls"
version = "0.1.0"
edition = "2021"
[dependencies]
base = { path = "../base" }
base/src/lib.rs:
pub trait Adder {
fn add(a: i32, b: i32) -> i32;
}
pub fn add<T: Adder>(a: i32, b: i32) -> i32 {
T::add(a, b)
}
#[cfg(test)]
mod tests {
use super::*;
use impls::AddImpl;
#[test]
fn test_add() {
assert_eq!(add::<AddImpl>(2, 3), 5);
}
}
impls/src/lib.rs:
use base::Adder;
pub struct AddImpl;
impl Adder for AddImpl {
fn add(a: i32, b: i32) -> i32 {
a + b
}
}
The problem:
error[E0277]: the trait bound `AddImpl: Adder` is not satisfied
--> base/src/lib.rs:16:26
|
16 | assert_eq!(add::<AddImpl>(2, 3), 5);
| ^^^^^^^ the trait `Adder` is not implemented for `AddImpl`
|
help: this trait has no implementations, consider adding one
--> base/src/lib.rs:1:1
|
1 | pub trait Adder {
| ^^^^^^^^^^^^^^^
note: required by a bound in `add`
--> base/src/lib.rs:5:15
|
5 | pub fn add<T: Adder>(a: i32, b: i32) -> i32 {
| ^^^^^ required by this bound in `add`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `base` (lib test) due to 1 previous error
I'm not sure if I'm doing some goofy mistake that is preventing rust from recognizing that the trait is actually implemented. In this case, everything is imported, so it's not that I've forgotten to import the trait.
Alternatively, I may need a different way of laying things out. Perhaps I can keep the concrete implementations in the original trait and hide them behind feature flags. Anyway, suggestions are welcome.