I've written a program in Rust that plays music using stepper motors, and now I'd like to add some fake objects so that I can do automated testing. However, I don't know a good way to define those fake objects in such a way that my program can actually use them.
You can run my code on the Rust Playground.
The program's main loop uses two trait objects. One object implements a trait called Timer
, which represents a timer that can be used to make things happen at regular intervals. The other object implements a trait called Motor
, and represents the stepper motor itself. The definitions of those traits are on my Rust Playground post.
The main loop simply waits 500 microseconds, pulls the "step" pin HIGH, waits another 500 microseconds, pulls the "step" pin LOW, and repeats for a total of 1,000 cycles. This causes the motor to step 1,000 times a second for one second.
fn spin<T: Timer, M: Motor>(timer: &mut T, motor: &mut M) {
timer.reset();
for _ in 0..1000 {
timer.wait_microseconds(500);
motor.set_step_high();
timer.wait_microseconds(500);
motor.set_step_low();
}
}
So far, everything works just great. I have (in my real code, not the Rust Playground post) a working implementation of Timer
, and a working implementation of Motor
, and the spin
function makes the motor spin, and it sounds beautiful.
I want to be able to do automated testing, so I've written a "fake" object which implements Motor
and Timer
in a way that's useful for testing. The type itself is just a struct:
/// A mock timer and motor which simply tracks the amount of simulated time that
/// the motor driver has its "step" pin pulled HIGH.
struct DummyTimerMotor {
is_on: bool,
time_high: u64,
}
The implementations of set_step_high
and set_step_low
just set is_on
to true
and false
(respectively), and the implementation of wait_microseconds
just checks whether or not is_on
is true
and adds the given amount of time to time_high
if so. Those implementations are in my Rust Playground post.
Naively, I would expect to be able to pass a DummyTimerMotor
object as both parameters of spin
, and then look at time_high
afterward, and see that it has a value of 500000. However, that is, of course, not allowed:
fn main() {
let mut dummy: DummyTimerMotor = DummyTimerMotor {
is_on: false, time_high: 0
};
spin(&mut dummy, &mut dummy); // Oops, not allowed!
print!("The 'step' pin was HIGH for {} microseconds", dummy.time_high);
}
This gives an error message: "cannot borrow dummy
as mutable more than once at a time."
I know exactly why I'm getting that error message, and it makes sense. What's a good way to get the behavior I'm trying to get?
I only have one reasonable idea: change spin
so that instead of taking an object which implements Timer
, and another object which implements Motor
, it takes a single object which implements both Timer
and Motor
. However, that seems inelegant to me (as a Rust newbie). Conceptually, a timer is one thing and a motor is another thing; having spin
take a single object which is both a timer and a motor is quite unintuitive. It doesn't seem like I should change the way that spin
is implemented merely to accommodate the details of how the timer and motor are implemented.
In case the Rust Playground ever goes down, below is the entire program that I have there, along with the entire error output.
/// A timer which can be used to make things happen at regular intervals.
trait Timer {
/// Set the timer's reference time to the current time.
fn reset(&mut self);
/// Advance the timer's reference time by the given number of microseconds,
/// then wait until the reference time.
fn wait_microseconds(&mut self, duration: u64);
}
/// The interface to a stepper motor driver.
trait Motor {
/// Pull the "step" pin HIGH, thereby asking the motor driver to move the
/// motor by one step.
fn set_step_high(&mut self);
/// Pull the "step" pin LOW, in preparation for pulling it HIGH again.
fn set_step_low(&mut self);
}
fn spin<T: Timer, M: Motor>(timer: &mut T, motor: &mut M) {
timer.reset();
for _ in 0..1000 {
timer.wait_microseconds(500);
motor.set_step_high();
timer.wait_microseconds(500);
motor.set_step_low();
}
}
/// A mock timer and motor which simply tracks the amount of simulated time that
/// the motor driver has its "step" pin pulled HIGH.
struct DummyTimerMotor {
is_on: bool,
time_high: u64,
}
impl Timer for DummyTimerMotor {
fn reset(&mut self) { }
fn wait_microseconds(&mut self, duration: u64) {
if self.is_on {
self.time_high += duration;
}
}
}
impl Motor for DummyTimerMotor {
fn set_step_high(&mut self) {
self.is_on = true;
}
fn set_step_low(&mut self) {
self.is_on = false;
}
}
fn main() {
let mut dummy: DummyTimerMotor = DummyTimerMotor {
is_on: false, time_high: 0
};
spin(&mut dummy, &mut dummy); // Oops, not allowed!
print!("The 'step' pin was HIGH for {} microseconds", dummy.time_high);
}
error[E0499]: cannot borrow `dummy` as mutable more than once at a time
--> src/main.rs:61:22
|
61 | spin(&mut dummy, &mut dummy); // Oops, not allowed!
| ---- ---------- ^^^^^^^^^^ second mutable borrow occurs here
| | |
| | first mutable borrow occurs here
| first borrow later used by call
You can have a distinct DummyTimer
and DummyMotor
who share state through a Rc<RefCell<State>>
:
struct State {
is_on: bool,
time_high: u64,
}
struct DummyTimer {
state: Rc<RefCell<State>>,
}
impl Timer for DummyTimer {
fn reset(&mut self) { }
fn wait_microseconds(&mut self, duration: u64) {
let mut t = self.state.borrow_mut();
if t.is_on {
t.time_high += duration;
}
}
}
struct DummyMotor {
state: Rc<RefCell<State>>,
}
impl Motor for DummyMotor {
fn set_step_high(&mut self) {
self.state.borrow_mut().is_on = true;
}
fn set_step_low(&mut self) {
self.state.borrow_mut().is_on = false;
}
}
fn main() {
let state = Rc::new (RefCell::new (State { is_on: false, time_high: 0, }));
let mut motor = DummyMotor { state: Rc::clone (&state), };
let mut timer = DummyTimer { state: Rc::clone (&state), };
spin(&mut timer, &mut motor); // Now it's allowed
print!("The 'step' pin was HIGH for {} microseconds", state.borrow().time_high);
}