Add Rust ping/pong test

Basically a clone of pingpong_test.cc but for Rust.

Change-Id: Ie04b90eeb6a399379115dcc15173e09665d569e3
Signed-off-by: James Kuszmaul <james.kuszmaul@bluerivertech.com>
diff --git a/aos/events/BUILD b/aos/events/BUILD
index 4d5428b..dc6fa8c 100644
--- a/aos/events/BUILD
+++ b/aos/events/BUILD
@@ -259,6 +259,38 @@
     ],
 )
 
+rust_test(
+    name = "pingpong_test_rs",
+    srcs = [
+        "pingpong_test.rs",
+    ],
+    data = [":pingpong_config"],
+    # TODO(adam.snaider): Remove later. For now we need this because when
+    # a rust test crashes inside of C++, the output gets captured which makes
+    # it pretty much impossible to figure out what happened.
+    env = {"RUST_TEST_NOCAPTURE": "1"},
+    rustc_flags = ["-Crelocation-model=static"],
+    target_compatible_with = select({
+        "//conditions:default": ["//tools/platforms/rust:has_support"],
+        "//tools:has_msan": ["@platforms//:incompatible"],
+    }),
+    deps = [
+        ":event_loop_runtime",
+        ":ping_lib_rs",
+        ":ping_rust_fbs",
+        ":pong_lib_rs",
+        ":pong_rust_fbs",
+        ":simulated_event_loop_rs",
+        "//aos:configuration_rs",
+        "//aos:configuration_rust_fbs",
+        "//aos:flatbuffers_rs",
+        "//aos:test_init_rs",
+        "@com_github_google_flatbuffers//rust",
+        "@crate_index//:futures",
+        "@rules_rust//tools/runfiles",
+    ],
+)
+
 rust_binary(
     name = "ping_rs",
     srcs = [
diff --git a/aos/events/pingpong_test.rs b/aos/events/pingpong_test.rs
new file mode 100644
index 0000000..a20f66f
--- /dev/null
+++ b/aos/events/pingpong_test.rs
@@ -0,0 +1,108 @@
+#[cfg(test)]
+mod tests {
+    use std::{cell::Cell, time::Duration};
+
+    use aos_configuration::read_config_from;
+    use aos_events_event_loop_runtime::{EventLoopRuntimeHolder, Watcher};
+    use aos_events_simulated_event_loop::{SimulatedEventLoopFactory, SimulatedEventLoopHolder};
+    use aos_test_init::test_init;
+    use ping_lib::PingTask;
+    use ping_rust_fbs::aos::examples as ping;
+    use pong_rust_fbs::aos::examples as pong;
+    use runfiles::Runfiles;
+
+    // We use this trait to simplify leaking memory. For now, the event loop only allows
+    // data with a `'static` lifetime. Until that restriction is lifted, we may leak
+    // some memory in tests.
+    trait Leak: Sized {
+        fn leak(self) -> &'static mut Self {
+            Box::leak(Box::new(self))
+        }
+    }
+
+    impl<T> Leak for T {}
+
+    #[allow(unused)]
+    struct PingPongTest {
+        ping_event_loop: EventLoopRuntimeHolder<SimulatedEventLoopHolder>,
+        pong_event_loop: EventLoopRuntimeHolder<SimulatedEventLoopHolder>,
+        event_loop_factory: SimulatedEventLoopFactory<'static>,
+    }
+
+    impl PingPongTest {
+        pub fn init() -> PingPongTest {
+            test_init();
+            let r = Runfiles::create().unwrap();
+            let config =
+                read_config_from(&r.rlocation("org_frc971/aos/events/pingpong_config.json"))
+                    .unwrap()
+                    .leak();
+            let mut event_loop_factory = SimulatedEventLoopFactory::new(config);
+
+            let ping_event_loop = event_loop_factory.make_runtime("ping", None, |runtime| {
+                let ping = PingTask::new();
+                runtime.spawn(async move { ping.tasks(runtime, 10000).await });
+            });
+
+            let pong_event_loop = event_loop_factory.make_runtime("pong", None, |runtime| {
+                runtime.spawn(async move { pong_lib::pong(runtime).await })
+            });
+            PingPongTest {
+                event_loop_factory,
+                ping_event_loop,
+                pong_event_loop,
+            }
+        }
+
+        pub fn event_loop(&mut self) -> &mut SimulatedEventLoopFactory<'static> {
+            &mut self.event_loop_factory
+        }
+    }
+
+    #[test]
+    fn starts() {
+        let mut pingpong = PingPongTest::init();
+        pingpong.event_loop().run_for(Duration::from_secs(10));
+    }
+
+    #[test]
+    fn always_replies() {
+        let mut pingpong = PingPongTest::init();
+
+        // For now, the simulated event loop requires all references in the tasks
+        // to be `'static`, so we leak them for now until the restriction is lifted.
+        let ping_count: &Cell<i32> = Cell::new(1).leak();
+        let pong_count: &Cell<i32> = Cell::new(1).leak();
+
+        let _test_runtime = pingpong.event_loop().make_runtime("test", None, |runtime| {
+            let count_pings = async move {
+                let mut ping_watcher: Watcher<ping::Ping> = runtime.make_watcher("/test").unwrap();
+                loop {
+                    let ping = ping_watcher.next().await;
+                    assert_eq!(ping.message().unwrap().value(), ping_count.get());
+                    ping_count.set(ping_count.get() + 1);
+                }
+            };
+            let count_pongs = async move {
+                let mut pong_watcher: Watcher<pong::Pong> = runtime.make_watcher("/test").unwrap();
+                loop {
+                    let pong = pong_watcher.next().await;
+                    assert_eq!(pong.message().unwrap().value(), pong_count.get());
+                    pong_count.set(pong_count.get() + 1);
+                }
+            };
+
+            runtime.spawn(async move {
+                futures::join!(count_pings, count_pongs);
+                unreachable!();
+            });
+        });
+
+        pingpong.event_loop().run_for(Duration::from_secs(10));
+
+        // We run at t=0 and t=10 seconds, which means we run 1 extra time (Note that we started
+        // the count at 1, not 0).
+        assert_eq!(ping_count.get(), 1002);
+        assert_eq!(pong_count.get(), 1002);
+    }
+}