Demilade Sonuga's blog

All posts

Event Handling III

2023-04-29

In this post, we're going to test the event hooker.

What Exactly Are We Testing?

Does send_event result in the calling of associated registered functions and those functions only? Does hook_event result in the registering of functions to be called when certain events occur? Does a function still get called when an event occurs after unregistering it with unhook_event?

Dummy Allocators

Once again, we're going to need dummy allocators. For now, we'll just copy and paste (we'll deal with the resulting mess when we're refactoring).

In event_hook.rs

#[cfg(test)]
mod tests {
    use super::*;
    use crate::allocator::Allocator;

    // Convenience function for getting the always successful allocator
    fn successful_allocator() -> *mut SuccessfulAllocator {
        &mut SuccessfulAllocator as *mut _
    }

        // Convenience function for getting the always fail allocator
    fn failing_allocator() -> *mut FailingAllocator {
        &mut FailingAllocator as *mut _
    }

    // Dummy allocator that we can depend on to always succeed
    struct SuccessfulAllocator;

    
    use std::alloc::Global as PlatformAllocator;
    use std::alloc::Layout;
    use std::ptr::NonNull;
    use std::alloc::Allocator as StdAllocator;
    
    // Use your computer's allocator to allocate and deallocate memory
    // Much more reliable than using our own custom allocator,
    // so we can depend on it succeeding (under normal circumstances)
    unsafe impl Allocator for SuccessfulAllocator {
        unsafe fn alloc(&mut self, size: usize, alignment: usize) -> Option<*mut u8> {
            let mem_layout = Layout::from_size_align(size, alignment).unwrap();
            let mem = PlatformAllocator.allocate(mem_layout).unwrap();
            let ptr = mem.as_ptr() as *mut u8;
            Some(ptr)
        }
        unsafe fn dealloc(&mut self, ptr: *mut u8, size_to_dealloc: usize) {
            // Using an alignment of 1 here because I think the alignment no
            // longer matters here. We're deallocating memory because we're
            // done using it
            let mem_layout = Layout::from_size_align(size_to_dealloc, 1).unwrap();
            PlatformAllocator.deallocate(NonNull::new(ptr).unwrap(), mem_layout);
        }
    }

    // Dummy allocator we can depend on to always fail
    struct FailingAllocator;

    unsafe impl Allocator for FailingAllocator {
        unsafe fn alloc(&mut self, size: usize, alignment: usize) -> Option<*mut u8> {
            None
        }
        unsafe fn dealloc(&mut self, ptr: *mut u8, size_to_dealloc: usize) {}
    }
}

Testing

The thing about the tests we're going to write here is that the send_event, hook_event and unhook_event functions can't be tested individually. To verify that send_event is working, we first need to register a function with hook_event. And to verify that hook_event is working, we'll need to call send_event to see if the registered function gets called. To verify that unhook_event is working, we first need to register the function with hook_event, check if it runs with send_event and verify that it doesn't run anymore after unhook_event is called.

So, instead of writing tests for testing those individual functions, we're going to write a test that will test everything working together as a whole.

#[cfg(test)]
mod tests {
    // ...Others
    use crate::keyboard::{KeyCode, KeyDirection};

    #[test]
    fn test_event_hooker1() {
        let allocator = successful_allocator();
        let mut event_hooker = EventHooker::new(allocator);
        // This variable is increased everytime a timer handler is called
        let mut timer_no = 0;
        // This variable is increased everytime a keyboard handler is called
        let mut keyboard_no = 0;
        // Registering the timer and keyboard handlers
        let timer_handler_hook_id = event_hooker.hook_event(
            EventKind::Timer,
            BoxedFn::new(|_| timer_no += 1, allocator)
        );
        let keyboard_handler_hook_id = event_hooker.hook_event(
            EventKind::Keyboard,
            BoxedFn::new(|_| keyboard_no += 1, allocator)
        );
        // Send the timer event 3 times
        for _ in 0..3 {
            event_hooker.send_event(EventInfo::Timer);
        }
        // Since the timer event was sent 3 times and no keyboard
        // event has been sent, then the timer_no should have increased
        // to 3 and the keyboard_no should have remained 0
        assert_eq!(timer_no, 3);
        assert_eq!(keyboard_no, 0);
        // Send the keyboard event 3 times
        for _ in 0..3 {
            event_hooker.send_event(EventInfo::Keyboard(KeyEvent {
                keycode: KeyCode::A,
                direction: KeyDirection::Down
            }));
        }
        // The timer_no should still be 3 and the keyboard_no should increase to 3
        assert_eq!(timer_no, 3);
        assert_eq!(keyboard_no, 3);
    }

    // ...Others
}

The above test initializes two variables, timer_no and keyboard_no, to 0. When the timer event handler is called, timer_no increases by 1 and when the keyboard handler is called, keyboard_no increases by 1. This test answers the first two questions under the What Exactly Are We Testing section.

It does that by sending the timer event 3 times, checking the timer_no to see if it has indeed increased by 3 and verifying that the keyboard_no has not increased at all. This verifies that sending the timer event resulted only in calling the function registered as the timer event handler.

It then sends the keyboard event 3 times and checks to make sure that timer_no doesn't increase at all and that keyboard_no increases 3 times. This verifies that sending the keyboard event results in invoking only functions registered as keyboard handlers.

To test out the unhook_event behavior, we can just write another test.

#[cfg(test)]
mod tests {
    // ...Others

    #[test]
    fn test_event_hooker2() {
        let allocator = successful_allocator();
        let mut event_hooker = EventHooker::new(allocator);
        // The number of times the timer handler was called
        let mut no_of_calls = 0;
        let hook_id = event_hooker.hook_event(
            EventKind::Timer,
            BoxedFn::new(|_| no_of_calls += 1, allocator)
        );
        // Send the timer event 5 times
        for _ in 0..5 {
            event_hooker.send_event(EventInfo::Timer);
        }
        // Verify that no_of_calls increased to 5
        // (no_of_calls is incremented with every invocation
        // of the timer handler)
        assert_eq!(no_of_calls, 5);
        // Unregister the timer handler
        event_hooker.unhook_event(EventKind::Timer, hook_id).unwrap();
        // Send the timer event 5 times and verify that
        // no_of_calls remained the same
        for _ in 0..5 {
            event_hooker.send_event(EventInfo::Timer);
        }
        assert_eq!(no_of_calls, 5);
    }

    // ...Others
}

The above verifies that unhook_event truly results in the unregistering of an event handler. It does this by first registering a function to be called when timer events occur. Invoking this function results in the increase of no_of_calls by 1. Timer events are sent and no_of_calls is checked to verify that the expected effect of calling the function is made.

unhook_event is then called with the function's id and timer events are sent again. no_of_calls is then checked to verify that the function's effects are no longer there, meaning that it's no longer being invoked when timer events are sent.

These are just a few cases and there are a lot of other things that could go wrong with the EventHooker. You should add those tests yourself.

Anyways, this concludes our EventHooker testing. Next, we're going to check it out in live action from the efi_main function in main.rs.

Take Away

For the full code, go to the repo

In The Next Post

We'll be checking out the EventHooker in action.