Hi,
I'm currently working on building a Tetris game in Rust as a way to learn the language.
As part of this project, I'm attempting to add unit tests for the key event handling functionality.
Here's the main function to handle the key events after starting:
pub struct Game {
terminal: Box<dyn Terminal>,
}
impl Game {
pub fn start(&mut self) -> Result<()> {
self.terminal.enable_raw_mode()?;
self.terminal.enter_alternate_screen()?;
let mut stdout = io::stdout();
self.render(&mut stdout)?;
match self.handle_event(&mut stdout) {
Ok(_) => {}
Err(err) => eprintln!("Error: {}", err),
}
Ok(())
}
pub fn handle_event(&mut self, stdout: &mut std::io::Stdout) -> Result<()> {
let mut drop_timer = Instant::now();
let mut soft_drop_timer = Instant::now();
let mut reset_needed = false;
loop {
if self.paused {
self.handle_pause_event(stdout)?;
} else {
if self.level <= MAX_LEVEL && self.lines >= LINES_PER_LEVEL * (self.level + 1) {
self.level += 1;
self.drop_interval -= self.drop_interval / 10;
}
if drop_timer.elapsed() >= Duration::from_millis(self.drop_interval) {
let mut tetromino = self.current_tetromino.clone();
let can_move_down = self.can_move(
&tetromino,
tetromino.position.row as i16 + 1,
tetromino.position.col as i16,
);
if can_move_down {
tetromino.move_down(self, stdout)?;
self.current_tetromino = tetromino;
} else {
self.lock_and_move_to_next(&tetromino, stdout)?;
}
self.render_current_tetromino(stdout)?;
drop_timer = Instant::now();
}
if poll(Duration::from_millis(10))? {
if let Ok(event) = self.terminal.read_event() {
match event {
Event::Key(KeyEvent {
code,
state: _,
kind,
modifiers: _,
}) => {
if kind == KeyEventKind::Press {
let mut tetromino = self.current_tetromino.clone();
match code {
KeyCode::Char('h') | KeyCode::Left => {
tetromino.move_left(self, stdout)?;
self.current_tetromino = tetromino;
}
I've created a trait for the terminal:
pub trait Terminal {
fn read_event(&self) -> Result<Event>;
}
and implemented it for both the real terminal and mock terminal to make testing possible:
pub struct RealTerminal;
impl Terminal for RealTerminal {
fn read_event(&self) -> Result<Event> {
Ok(read()?)
}
}
struct MockTerminal {
mock_key_code: Option<KeyCode>,
}
impl MockTerminal {
pub fn new(mock_key_code: Option<KeyCode>) -> Self {
MockTerminal { mock_key_code }
}
}
impl Terminal for MockTerminal {
fn read_event(&self) -> Result<Event> {
if let Some(mock_key_code) = self.mock_key_code {
Ok(Event::Key(KeyEvent {
code: mock_key_code,
modifiers: KeyModifiers::empty(),
kind: KeyEventKind::Press,
state: KeyEventState::empty(),
}))
} else {
Ok(Event::Key(KeyEvent {
code: KeyCode::Null,
modifiers: KeyModifiers::empty(),
kind: KeyEventKind::Press,
state: KeyEventState::empty(),
}))
}
}
}
#[test]
fn test_move_left() -> Result<()> {
let conn = Connection::open_in_memory()?;
let sqlite_highscore_repository = Box::new(SqliteHighScoreRepository { conn });
let mut game = Game::new(
Box::new(MockTerminal::new(None)),
40,
20,
sqlite_highscore_repository,
None,
None,
0,
0,
)?;
game.start()?;
let previous_col = game.current_tetromino.position.col;
game.terminal = Box::new(MockTerminal::new(Some(KeyCode::Char('h'))));
assert_eq!(game.current_tetromino.position.col, previous_col - 1);
Ok(())
}
The thing is it runs forever (due to the game loop?). So, I tried to start it in a separate thread:
let game_thread = thread::spawn(move || -> Result<()> {
game.start()?;
Ok(())
});
let previous_col = game.current_tetromino.position.col;
game.terminal = Box::new(MockTerminal::new(Some(KeyCode::Char('h'))));
assert_eq!(game.current_tetromino.position.col, previous_col - 1);
game_thread.join().unwrap()?;
but the error is:
error[E0277]: `(dyn std::error::Error + 'static)` cannot be sent between threads safely
--> src/lib.rs:1899:27
|
1899 | let game_thread = thread::spawn(move || -> Result<()> {
| ^^^^^^^^^^^^^ `(dyn std::error::Error + 'static)` cannot be sent between threads safely
|
= help: the trait `Send` is not implemented for `(dyn std::error::Error + 'static)`
= note: required for `Unique<(dyn std::error::Error + 'static)>` to implement `Send`
I also tried to wrap the game instance in Arc<Mutex<T>> but I still get the same error.
I have two questions:
- Is this the recommended approach for testing a TUI app?
- If so, how can I address the thread safety issue?
Thanks,