Testing if a type is implementing an auto-trait

I wonder if one of these ways is idiomatic:

// Is this idiomatic?
#[test]
const fn test_if_unit_arc_is_sync1() {
    const fn takes_sync<T: Sync>() {}
    takes_sync::<std::sync::Arc::<()>>();
}

// Or is this better?
#[allow(dead_code)]
const fn test_if_unit_arc_is_sync2() {
    const fn takes_sync<T: Sync>() {}
    takes_sync::<std::sync::Arc::<()>>();
    // This test doesn't actually run, but it will result
    // in a compiler error if we uncomment this:
    //takes_sync::<std::rc::Rc<()>>();
}

(Playground)

I also looked into how tokio performs some tests, but I don't think I need to make it that complex.

However, testing that some type does not implement a trait might be more complex (by causing a disambiguity, like tokio does too).

I'd say that this is a compile-time property, so deferring it to a #[test] is not the best idea. After all, tests are not always compiled, so if you forget to run your tests, you might end up pushing/publishing code that's not working as intended.

1 Like

Would that mean I should/could include it in

#[cfg(test)]
mod tests { /* … */ }

but not mark it as #[test]?

No – #[cfg(test)] is exactly what makes it not be compiled/typechecked unconditionally. What I meant by that is to just use the 2nd approach.

For the more complex cases, check out static_assertions.

But I think it would be okay to only compile it during testing. I don't see a need to compile this during normal builds. It would only make building slower.


And if I put it in #[cfg(test)], then I'm still unsure whether it should be a #[test].

Pros:

  • I will see the test in the cargo test output.
  • Semantically it is a test, isn't it?

Cons:

  • cargo test is slower.

Inspired by the tokio tests, I would probably test for !Trait like this:

#[test]
const fn test_if_unit_rc_is_not_sync() {
    trait AmbiguousIfSync<T> {
        const TEST_NOT_SYNC: () = ();
    }
    impl<T: ?Sized> AmbiguousIfSync<((), ())> for T {}
    impl<T: ?Sized + Sync> AmbiguousIfSync<()> for T {}
    let _ = <std::rc::Rc<()>>::TEST_NOT_SYNC;
    //let _ = <std::sync::Arc<()>>::TEST_NOT_SYNC;
}

(Playground)


Or with a macro:

#![cfg(test)]

trait AmbiguousIfSync<T> {
    const TEST_NOT_SYNC: () = ();
}
impl<T: ?Sized> AmbiguousIfSync<((), ())> for T {}
impl<T: ?Sized + Sync> AmbiguousIfSync<()> for T {}

macro_rules! assert_not_sync {
    ($t:ty) => { { let _ = <$t>::TEST_NOT_SYNC; } };
}

#[test]
const fn test_if_unit_rc_is_not_sync() {
    assert_not_sync!(std::rc::Rc<()>);
    //assert_not_sync!(std::sync::Arc<()>);
}

(Playground)

But if there's some more idiomatic approach, I'm happy to hear about them. (Also happy to hear more comments on whether I should mark the functions #[test] and/or include them in a test module at all.)


I just noticed they do similar as tokio does (source).

One of the nice things about tests is that some can run while others fail. If your tests do not compile, then none of them will run.

Personally, I don't think you need to test whether something implements Sync (or any trait for that matter) :thinking:
If it needs to implement Sync, then you will have code that requires it to be Sync and that will not compile.
There's not much benefit from adding an additional type constraint that isn't reflected elsewhere in your codebase

If I include it in a #[cfg(test)] section, then the failure lets me at least build the crate.

No not really, it might only happen once someone uses a type in a multithreaded context.

Well, maybe the benefit is low, though sometimes whether a type is Send or Sync is determined indirectly. I would like to ensure that the overall result of my unsafe impl Send and unsafe impl Sync implementations result in the correct Sendness and Syncness for all exported types and to not get any surprises.

This is definitely not true. What if it's a public API type? I am creating a database abstraction layer, and since databases are in general used in a highly concurrent fashion, the main entry point of my library is intended to be used in a multi-threaded environment. However:

  1. it's not used by anything else in my code, because it's the entry point of the public API, so nothing else in my own code relies on it.
  2. it contains raw pointers (due to FFI), which would make it !Send + !Sync by default, so I have to explicitly make it Send + Sync. I thus have to ensure that they implement these two traits, otherwise I might delete the unsafe impls accidentally during a refactor, for example.

Fair point, OP doesn't really specify whether they're making a library.
AFAIK you don't really need to assert traits unless you're exposing a public interface.

Still, though, the tests you're writing for the library should have some usage that requires Sync/Send. The static_assertions library you linked seems like a good way to double check though

Yes, sorry for not clarifying my use case earlier. As I said later:

Hmmm, I could add tests for race-conditions and such, but I imagine those would easily give me false-negatives even when there are bugs in the code.

1 Like

I think the #[test] pattern is fine, and I've used it.

If it's testing your implementation, rather than being some kind of contract that user code could break at any time, then it doesn't have to be asserted every time the code is compiled.

Having this in tests and labelled as a test helps future maintainers of the code understand what it's for.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.