0

I'm trying to mock out the serial port object provided by serialport::new() and having an awful time with it. The Rc<RefCell<dyn SomeTrait>> pattern is one I've been using for a while now for any singleton dependencies - like a serial port - and I've grown accustomed to it. It works and it lets me use mockall to write highly focused unit tests where all dependencies are mocked out.

But, today I ran into serialport::new(), which returns a Box<dyn SerialPort>, and I can't figure out a way to convert that into Rc<RefCell<dyn SerialPort>>. So I wrapped the whole box and ended up with Rc<RefCell<Box<dyn SerialPort>>>. But now I can't get my unit test to compile without defining the mock object in TestContext as another Rc<RefCell<Box<dyn SerialPort>>> - which would prevent me from being able to actually invoke special mock-related methods, like .expect_send().

I could use some help here. What follows is my use-case recreated in the simplest form possible as well as the compiler error that I get when building the test.

use std::cell::RefCell;
use std::rc::Rc;

/// This is not my code. This is a simplified version of the serialport crate.
mod serialport {
    pub trait SerialPort {
        fn send(&self);
    }

    pub struct SomeConcreteSerialPort {}

    impl SerialPort for SomeConcreteSerialPort {
        fn send(&self) {}
    }

    pub fn new() -> Box<dyn SerialPort> {
        Box::new(SomeConcreteSerialPort {})
    }
}

struct MyStruct {
    port: Rc<RefCell<Box<dyn serialport::SerialPort>>>,
}

impl MyStruct {
    fn new() -> Self {
        Self {
            port: Rc::new(RefCell::new(serialport::new())),
        }
    }

    fn do_send(&self) {
        self.port.borrow().send();
    }
}

fn main() {
    let my_struct = MyStruct::new();
    my_struct.do_send();
    println!("The foo is done!");
}

#[cfg(test)]
mod test {
    use mockall::mock;
    use std::cell::RefCell;
    use std::rc::Rc;

    use crate::serialport;
    use crate::MyStruct;

    mock! {
        SerialPort {}

        impl serialport::SerialPort for SerialPort {
            fn send(&self);
        }
    }

    struct TestContext {
        mock_port: Rc<RefCell<Box<MockSerialPort>>>,
        testable: MyStruct,
    }

    impl TestContext {
        fn new() -> Self {
            let mock_port = Rc::new(RefCell::new(Box::new(MockSerialPort::new())));
            Self {
                mock_port: Rc::clone(&mock_port),
                testable: MyStruct { port: mock_port },
            }
        }
    }


    #[test]
    fn test_happy_path() {
        let context = TestContext::new();

        context.mock_port.borrow().expect_send().once();

        context.testable.do_send();
    }
}
error[E0308]: mismatched types
  --> src/main.rs:71:44
   |
71 |                 testable: MyStruct { port: mock_port },
   |                                            ^^^^^^^^^ expected trait object `dyn SerialPort`, found struct `MockSerialPort`
   |
   = note: expected struct `Rc<RefCell<Box<(dyn SerialPort + 'static)>>>`
              found struct `Rc<RefCell<Box<MockSerialPort>>>`

I've also published the code to a GitHub repo if you'd like to reproduce it locally: https://github.com/DavidZemon/MockingProblems/

DavidZemon
  • 501
  • 5
  • 21
  • Is there a reason you need the box? Can `serialport::new()` return `Rc>` instead? Then you don't need to internal `Box` and can just cast `Rc>` to `Rc>` – PitaJ Apr 20 '23 at 21:47
  • https://stackoverflow.com/questions/30861295/how-do-i-pass-rcrefcellboxmystruct-to-a-function-accepting-rcrefcellbox – PitaJ Apr 20 '23 at 21:52
  • Why you need a `RefCell` if `send()` takes shared `&self`? – Chayim Friedman Apr 20 '23 at 22:29
  • @PitaJ, I can not drop the box - it's not my code. I included the code in my sample for simplicity's sake, but it is from the serialport crate. – DavidZemon Apr 21 '23 at 04:53

2 Answers2

2

We can ignore RefCell completely, and consider the question of turning Rc<Box<T>> into Rc<Box<dyn Trait>> when T implements Trait.

To be possible, this must not change the memory representation of the value inside of the Rc, so Box<T> and Box<dyn Trait> must have the same memory representation. However, Box<T> is a thin pointer, which means that it only contains an address to the value on the heap, whereas Box<dyn Trait> is a fat pointer, which contains both the address to the value and the address to the vtable of T that contains the type information (such as methods) at runtime.

So Box<dyn Trait> is larger than Box<T> and thus has a different memory layout, and it follows that it's impossible to turn a Rc<Box<T>> into a Rc<Box<dyn Trait>> (or in any way produce two Rcs with these types that point to the same value). The extra layer of indirection caused by Box (or any other pointer type) is what makes this impossible.

Frxstrem
  • 38,761
  • 9
  • 79
  • 119
2

@Frxstrem explained well why this is impossible, I want to suggest a solution.

Instead of using Rc from the outside, you can use Rc from the inside. That is, just use Box<dyn SerialPort>, but for the tests, use a type that wraps Rc<MockSerialPort> (or Rc<RefCell<MockSerialPort>>) and implements SerialPort by forwarding to the inner MockSerialPort:

struct MyStruct {
    port: Box<dyn serialport::SerialPort>,
}

impl MyStruct {
    fn new() -> Self {
        Self {
            port: serialport::new(),
        }
    }

    fn do_send(&self) {
        self.port.send();
    }
}

mock! {
    SerialPort {}

    impl serialport::SerialPort for SerialPort {
        fn send(&self);
    }
}

struct SharedMockSerialPort(Rc<MockSerialPort>);
impl serialport::SerialPort for SharedMockSerialPort {
    fn send(&self) {
        self.0.send();
    }
}

struct TestContext {
    mock_port: Rc<MockSerialPort>,
    testable: MyStruct,
}

impl TestContext {
    fn new() -> Self {
        let mock_port = Rc::new(MockSerialPort::new());
        Self {
            mock_port: Rc::clone(&mock_port),
            testable: MyStruct {
                port: Box::new(SharedMockSerialPort(mock_port)),
            },
        }
    }
}

This is a better design even for other cases, because it avoids the overhead of Rc and RefCell for non-test code.

Chayim Friedman
  • 47,971
  • 5
  • 48
  • 77
  • Tedious as this solution is, it at least _works_. Thank you. I was able to get my simple example repo working, even after adding some additional detail (the real `serialport::SerialPort` also inherits from Send) and therefore needing to swap `Rc` out for `Arc` and tossing a `Mutex` into the mix. I was also able to take this example and get it compiling in my real app. It doesn't pass my first unit test yet, but "compiling" is good enough for tonight and I'll poke at it more in the morning. For now, your answer certainly solves the question I asked at the top. Thanks! – DavidZemon Apr 21 '23 at 04:44