Janis Danisevskis | a578d39 | 2021-09-20 15:44:06 -0700 | [diff] [blame] | 1 | // Copyright 2021, The Android Open Source Project |
| 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | // you may not use this file except in compliance with the License. |
| 5 | // You may obtain a copy of the License at |
| 6 | // |
| 7 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | // |
| 9 | // Unless required by applicable law or agreed to in writing, software |
| 10 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | // See the License for the specific language governing permissions and |
| 13 | // limitations under the License. |
| 14 | |
| 15 | //! This module is intended for testing access control enforcement of services such as keystore2, |
| 16 | //! by assuming various identities with varying levels of privilege. Consequently, appropriate |
| 17 | //! privileges are required, or the attempt will fail causing a panic. |
| 18 | //! The `run_as` module provides the function `run_as`, which takes a UID, GID, an SELinux |
| 19 | //! context, and a closure. The return type of the closure, which is also the return type of |
| 20 | //! `run_as`, must implement `serde::Serialize` and `serde::Deserialize`. |
| 21 | //! `run_as` forks, transitions to the given identity, and executes the closure in the newly |
| 22 | //! forked process. If the closure returns, i.e., does not panic, the forked process exits with |
| 23 | //! a status of `0`, and the return value is serialized and sent through a pipe to the parent where |
| 24 | //! it gets deserialized and returned. The STDIO is not changed and the parent's panic handler |
| 25 | //! remains unchanged. So if the closure panics, the panic message is printed on the parent's STDERR |
| 26 | //! and the exit status is set to a non `0` value. The latter causes the parent to panic as well, |
| 27 | //! and if run in a test context, the test to fail. |
| 28 | |
| 29 | use keystore2_selinux as selinux; |
| 30 | use nix::sys::wait::{waitpid, WaitStatus}; |
| 31 | use nix::unistd::{ |
| 32 | close, fork, pipe as nix_pipe, read as nix_read, setgid, setuid, write as nix_write, |
Janis Danisevskis | 04945eb | 2021-12-06 17:20:09 -0800 | [diff] [blame] | 33 | ForkResult, Gid, Pid, Uid, |
Janis Danisevskis | a578d39 | 2021-09-20 15:44:06 -0700 | [diff] [blame] | 34 | }; |
| 35 | use serde::{de::DeserializeOwned, Serialize}; |
Janis Danisevskis | 04945eb | 2021-12-06 17:20:09 -0800 | [diff] [blame] | 36 | use std::io::{Read, Write}; |
| 37 | use std::marker::PhantomData; |
Janis Danisevskis | a578d39 | 2021-09-20 15:44:06 -0700 | [diff] [blame] | 38 | use std::os::unix::io::RawFd; |
| 39 | |
| 40 | fn transition(se_context: selinux::Context, uid: Uid, gid: Gid) { |
| 41 | setgid(gid).expect("Failed to set GID. This test might need more privileges."); |
| 42 | setuid(uid).expect("Failed to set UID. This test might need more privileges."); |
| 43 | |
| 44 | selinux::setcon(&se_context) |
| 45 | .expect("Failed to set SELinux context. This test might need more privileges."); |
| 46 | } |
| 47 | |
| 48 | /// PipeReader is a simple wrapper around raw pipe file descriptors. |
| 49 | /// It takes ownership of the file descriptor and closes it on drop. It provides `read_all`, which |
| 50 | /// reads from the pipe into an expending vector, until no more data can be read. |
| 51 | struct PipeReader(RawFd); |
| 52 | |
Janis Danisevskis | 04945eb | 2021-12-06 17:20:09 -0800 | [diff] [blame] | 53 | impl Read for PipeReader { |
| 54 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { |
| 55 | let bytes = nix_read(self.0, buf)?; |
| 56 | Ok(bytes) |
Janis Danisevskis | a578d39 | 2021-09-20 15:44:06 -0700 | [diff] [blame] | 57 | } |
| 58 | } |
| 59 | |
| 60 | impl Drop for PipeReader { |
| 61 | fn drop(&mut self) { |
| 62 | close(self.0).expect("Failed to close reader pipe fd."); |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | /// PipeWriter is a simple wrapper around raw pipe file descriptors. |
| 67 | /// It takes ownership of the file descriptor and closes it on drop. It provides `write`, which |
| 68 | /// writes the given buffer into the pipe, returning the number of bytes written. |
| 69 | struct PipeWriter(RawFd); |
| 70 | |
Janis Danisevskis | a578d39 | 2021-09-20 15:44:06 -0700 | [diff] [blame] | 71 | impl Drop for PipeWriter { |
| 72 | fn drop(&mut self) { |
| 73 | close(self.0).expect("Failed to close writer pipe fd."); |
| 74 | } |
| 75 | } |
| 76 | |
Janis Danisevskis | 04945eb | 2021-12-06 17:20:09 -0800 | [diff] [blame] | 77 | impl Write for PipeWriter { |
| 78 | fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { |
| 79 | let written = nix_write(self.0, buf)?; |
| 80 | Ok(written) |
| 81 | } |
| 82 | |
| 83 | fn flush(&mut self) -> std::io::Result<()> { |
| 84 | // Flush is a NO-OP. |
| 85 | Ok(()) |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | /// Denotes the sender side of a serializing channel. |
| 90 | pub struct ChannelWriter<T: Serialize + DeserializeOwned>(PipeWriter, PhantomData<T>); |
| 91 | |
| 92 | impl<T: Serialize + DeserializeOwned> ChannelWriter<T> { |
| 93 | /// Sends a serializable object to a the corresponding ChannelReader. |
| 94 | /// Sending is always non blocking. Panics if any error occurs during io or serialization. |
| 95 | pub fn send(&mut self, value: &T) { |
| 96 | let serialized = serde_cbor::to_vec(value) |
| 97 | .expect("In ChannelWriter::send: Failed to serialize to vector."); |
| 98 | let size = serialized.len().to_be_bytes(); |
| 99 | match self.0.write(&size).expect("In ChannelWriter::send: Failed to write serialized size.") |
| 100 | { |
| 101 | w if w != std::mem::size_of::<usize>() => { |
| 102 | panic!( |
| 103 | "In ChannelWriter::send: Failed to write serialized size. (written: {}).", |
| 104 | w |
| 105 | ); |
| 106 | } |
| 107 | _ => {} |
| 108 | }; |
| 109 | match self |
| 110 | .0 |
| 111 | .write(&serialized) |
| 112 | .expect("In ChannelWriter::send: Failed to write serialized data.") |
| 113 | { |
| 114 | w if w != serialized.len() => { |
| 115 | panic!( |
| 116 | "In ChannelWriter::send: Failed to write serialized data. (written: {}).", |
| 117 | w |
| 118 | ); |
| 119 | } |
| 120 | _ => {} |
| 121 | }; |
| 122 | } |
| 123 | } |
| 124 | |
| 125 | /// Represents the receiving and of a serializing channel. |
| 126 | pub struct ChannelReader<T>(PipeReader, PhantomData<T>); |
| 127 | |
| 128 | impl<T: Serialize + DeserializeOwned> ChannelReader<T> { |
| 129 | /// Receives a serializable object from the corresponding ChannelWriter. |
| 130 | /// Receiving blocks until an object of type T has been read from the channel. |
| 131 | /// Panics if an error occurs during io or deserialization. |
| 132 | pub fn recv(&mut self) -> T { |
| 133 | let mut size_buffer = [0u8; std::mem::size_of::<usize>()]; |
| 134 | match self.0.read(&mut size_buffer).expect("In ChannelReader::recv: Failed to read size.") { |
| 135 | r if r != size_buffer.len() => { |
| 136 | panic!("In ChannelReader::recv: Failed to read size. Insufficient data: {}", r); |
| 137 | } |
| 138 | _ => {} |
| 139 | }; |
| 140 | let size = usize::from_be_bytes(size_buffer); |
| 141 | let mut data_buffer = vec![0u8; size]; |
| 142 | match self |
| 143 | .0 |
| 144 | .read(&mut data_buffer) |
| 145 | .expect("In ChannelReader::recv: Failed to read serialized data.") |
| 146 | { |
| 147 | r if r != data_buffer.len() => { |
| 148 | panic!( |
| 149 | "In ChannelReader::recv: Failed to read serialized data. Insufficient data: {}", |
| 150 | r |
| 151 | ); |
| 152 | } |
| 153 | _ => {} |
| 154 | }; |
| 155 | |
| 156 | serde_cbor::from_slice(&data_buffer) |
| 157 | .expect("In ChannelReader::recv: Failed to deserialize data.") |
| 158 | } |
| 159 | } |
| 160 | |
Janis Danisevskis | a578d39 | 2021-09-20 15:44:06 -0700 | [diff] [blame] | 161 | fn pipe() -> Result<(PipeReader, PipeWriter), nix::Error> { |
| 162 | let (read_fd, write_fd) = nix_pipe()?; |
| 163 | Ok((PipeReader(read_fd), PipeWriter(write_fd))) |
| 164 | } |
| 165 | |
Janis Danisevskis | 04945eb | 2021-12-06 17:20:09 -0800 | [diff] [blame] | 166 | fn pipe_channel<T>() -> Result<(ChannelReader<T>, ChannelWriter<T>), nix::Error> |
| 167 | where |
| 168 | T: Serialize + DeserializeOwned, |
| 169 | { |
| 170 | let (reader, writer) = pipe()?; |
| 171 | Ok(( |
| 172 | ChannelReader::<T>(reader, Default::default()), |
| 173 | ChannelWriter::<T>(writer, Default::default()), |
| 174 | )) |
| 175 | } |
| 176 | |
| 177 | /// Handle for handling child processes. |
| 178 | pub struct ChildHandle<R: Serialize + DeserializeOwned, M: Serialize + DeserializeOwned> { |
| 179 | pid: Pid, |
| 180 | result_reader: ChannelReader<R>, |
| 181 | cmd_writer: ChannelWriter<M>, |
| 182 | response_reader: ChannelReader<M>, |
| 183 | exit_status: Option<WaitStatus>, |
| 184 | } |
| 185 | |
| 186 | impl<R: Serialize + DeserializeOwned, M: Serialize + DeserializeOwned> ChildHandle<R, M> { |
| 187 | /// Send a command message to the child. |
| 188 | pub fn send(&mut self, data: &M) { |
| 189 | self.cmd_writer.send(data) |
| 190 | } |
| 191 | |
| 192 | /// Receive a response from the child. |
| 193 | pub fn recv(&mut self) -> M { |
| 194 | self.response_reader.recv() |
| 195 | } |
| 196 | |
| 197 | /// Get child result. Panics if the child did not exit with status 0 or if a serialization |
| 198 | /// error occurred. |
| 199 | pub fn get_result(mut self) -> R { |
| 200 | let status = |
| 201 | waitpid(self.pid, None).expect("ChildHandle::wait: Failed while waiting for child."); |
| 202 | match status { |
| 203 | WaitStatus::Exited(pid, 0) => { |
| 204 | // Child exited successfully. |
| 205 | // Read the result from the pipe. |
| 206 | self.exit_status = Some(WaitStatus::Exited(pid, 0)); |
| 207 | self.result_reader.recv() |
| 208 | } |
| 209 | WaitStatus::Exited(pid, c) => { |
| 210 | panic!("Child did not exit as expected: {:?}", WaitStatus::Exited(pid, c)); |
| 211 | } |
| 212 | status => { |
| 213 | panic!("Child did not exit at all: {:?}", status); |
| 214 | } |
| 215 | } |
| 216 | } |
| 217 | } |
| 218 | |
| 219 | impl<R: Serialize + DeserializeOwned, M: Serialize + DeserializeOwned> Drop for ChildHandle<R, M> { |
| 220 | fn drop(&mut self) { |
| 221 | if self.exit_status.is_none() { |
| 222 | panic!("Child result not checked.") |
| 223 | } |
| 224 | } |
| 225 | } |
| 226 | |
| 227 | /// Run the given closure in a new process running with the new identity given as |
| 228 | /// `uid`, `gid`, and `se_context`. Parent process will run without waiting for child status. |
| 229 | /// |
| 230 | /// # Safety |
| 231 | /// run_as_child runs the given closure in the client branch of fork. And it uses non |
| 232 | /// async signal safe API. This means that calling this function in a multi threaded program |
| 233 | /// yields undefined behavior in the child. As of this writing, it is safe to call this function |
| 234 | /// from a Rust device test, because every test itself is spawned as a separate process. |
| 235 | /// |
| 236 | /// # Safety Binder |
| 237 | /// It is okay for the closure to use binder services, however, this does not work |
| 238 | /// if the parent initialized libbinder already. So do not use binder outside of the closure |
| 239 | /// in your test. |
| 240 | pub unsafe fn run_as_child<F, R, M>( |
| 241 | se_context: &str, |
| 242 | uid: Uid, |
| 243 | gid: Gid, |
| 244 | f: F, |
| 245 | ) -> Result<ChildHandle<R, M>, nix::Error> |
| 246 | where |
| 247 | R: Serialize + DeserializeOwned, |
| 248 | M: Serialize + DeserializeOwned, |
| 249 | F: 'static + Send + FnOnce(&mut ChannelReader<M>, &mut ChannelWriter<M>) -> R, |
| 250 | { |
| 251 | let se_context = |
| 252 | selinux::Context::new(se_context).expect("Unable to construct selinux::Context."); |
| 253 | let (result_reader, mut result_writer) = pipe_channel().expect("Failed to create pipe."); |
| 254 | let (mut cmd_reader, cmd_writer) = pipe_channel().expect("Failed to create cmd pipe."); |
| 255 | let (response_reader, mut response_writer) = |
| 256 | pipe_channel().expect("Failed to create cmd pipe."); |
| 257 | |
| 258 | match fork() { |
| 259 | Ok(ForkResult::Parent { child, .. }) => { |
| 260 | drop(response_writer); |
| 261 | drop(cmd_reader); |
| 262 | drop(result_writer); |
| 263 | |
| 264 | Ok(ChildHandle::<R, M> { |
| 265 | pid: child, |
| 266 | result_reader, |
| 267 | response_reader, |
| 268 | cmd_writer, |
| 269 | exit_status: None, |
| 270 | }) |
| 271 | } |
| 272 | Ok(ForkResult::Child) => { |
| 273 | drop(cmd_writer); |
| 274 | drop(response_reader); |
| 275 | drop(result_reader); |
| 276 | |
| 277 | // This will panic on error or insufficient privileges. |
| 278 | transition(se_context, uid, gid); |
| 279 | |
| 280 | // Run the closure. |
| 281 | let result = f(&mut cmd_reader, &mut response_writer); |
| 282 | |
| 283 | // Serialize the result of the closure. |
| 284 | result_writer.send(&result); |
| 285 | |
| 286 | // Set exit status to `0`. |
| 287 | std::process::exit(0); |
| 288 | } |
| 289 | Err(errno) => { |
| 290 | panic!("Failed to fork: {:?}", errno); |
| 291 | } |
| 292 | } |
| 293 | } |
| 294 | |
Janis Danisevskis | a578d39 | 2021-09-20 15:44:06 -0700 | [diff] [blame] | 295 | /// Run the given closure in a new process running with the new identity given as |
| 296 | /// `uid`, `gid`, and `se_context`. |
Janis Danisevskis | 04945eb | 2021-12-06 17:20:09 -0800 | [diff] [blame] | 297 | /// |
| 298 | /// # Safety |
| 299 | /// run_as runs the given closure in the client branch of fork. And it uses non |
| 300 | /// async signal safe API. This means that calling this function in a multi threaded program |
| 301 | /// yields undefined behavior in the child. As of this writing, it is safe to call this function |
| 302 | /// from a Rust device test, because every test itself is spawned as a separate process. |
| 303 | /// |
| 304 | /// # Safety Binder |
| 305 | /// It is okay for the closure to use binder services, however, this does not work |
| 306 | /// if the parent initialized libbinder already. So do not use binder outside of the closure |
| 307 | /// in your test. |
| 308 | pub unsafe fn run_as<F, R>(se_context: &str, uid: Uid, gid: Gid, f: F) -> R |
Janis Danisevskis | a578d39 | 2021-09-20 15:44:06 -0700 | [diff] [blame] | 309 | where |
| 310 | R: Serialize + DeserializeOwned, |
| 311 | F: 'static + Send + FnOnce() -> R, |
| 312 | { |
| 313 | let se_context = |
| 314 | selinux::Context::new(se_context).expect("Unable to construct selinux::Context."); |
Janis Danisevskis | 04945eb | 2021-12-06 17:20:09 -0800 | [diff] [blame] | 315 | let (mut reader, mut writer) = pipe_channel::<R>().expect("Failed to create pipe."); |
Janis Danisevskis | a578d39 | 2021-09-20 15:44:06 -0700 | [diff] [blame] | 316 | |
Janis Danisevskis | 04945eb | 2021-12-06 17:20:09 -0800 | [diff] [blame] | 317 | match fork() { |
Janis Danisevskis | a578d39 | 2021-09-20 15:44:06 -0700 | [diff] [blame] | 318 | Ok(ForkResult::Parent { child, .. }) => { |
| 319 | drop(writer); |
| 320 | let status = waitpid(child, None).expect("Failed while waiting for child."); |
| 321 | if let WaitStatus::Exited(_, 0) = status { |
| 322 | // Child exited successfully. |
| 323 | // Read the result from the pipe. |
Janis Danisevskis | 04945eb | 2021-12-06 17:20:09 -0800 | [diff] [blame] | 324 | // let serialized_result = |
| 325 | // reader.read_all().expect("Failed to read result from child."); |
Janis Danisevskis | a578d39 | 2021-09-20 15:44:06 -0700 | [diff] [blame] | 326 | |
| 327 | // Deserialize the result and return it. |
Janis Danisevskis | 04945eb | 2021-12-06 17:20:09 -0800 | [diff] [blame] | 328 | reader.recv() |
Janis Danisevskis | a578d39 | 2021-09-20 15:44:06 -0700 | [diff] [blame] | 329 | } else { |
| 330 | panic!("Child did not exit as expected {:?}", status); |
| 331 | } |
| 332 | } |
| 333 | Ok(ForkResult::Child) => { |
| 334 | // This will panic on error or insufficient privileges. |
| 335 | transition(se_context, uid, gid); |
| 336 | |
| 337 | // Run the closure. |
| 338 | let result = f(); |
| 339 | |
| 340 | // Serialize the result of the closure. |
Janis Danisevskis | 04945eb | 2021-12-06 17:20:09 -0800 | [diff] [blame] | 341 | writer.send(&result); |
Janis Danisevskis | a578d39 | 2021-09-20 15:44:06 -0700 | [diff] [blame] | 342 | |
| 343 | // Set exit status to `0`. |
| 344 | std::process::exit(0); |
| 345 | } |
| 346 | Err(errno) => { |
| 347 | panic!("Failed to fork: {:?}", errno); |
| 348 | } |
| 349 | } |
| 350 | } |
| 351 | |
| 352 | #[cfg(test)] |
| 353 | mod test { |
| 354 | use super::*; |
| 355 | use keystore2_selinux as selinux; |
| 356 | use nix::unistd::{getgid, getuid}; |
| 357 | use serde::{Deserialize, Serialize}; |
| 358 | |
| 359 | /// This test checks that the closure does not produce an exit status of `0` when run inside a |
| 360 | /// test and the closure panics. This would mask test failures as success. |
| 361 | #[test] |
| 362 | #[should_panic] |
| 363 | fn test_run_as_panics_on_closure_panic() { |
Janis Danisevskis | 04945eb | 2021-12-06 17:20:09 -0800 | [diff] [blame] | 364 | // Safety: run_as must be called from a single threaded process. |
| 365 | // This device test is run as a separate single threaded process. |
| 366 | unsafe { |
| 367 | run_as(selinux::getcon().unwrap().to_str().unwrap(), getuid(), getgid(), || { |
| 368 | panic!("Closure must panic.") |
| 369 | }) |
| 370 | }; |
Janis Danisevskis | a578d39 | 2021-09-20 15:44:06 -0700 | [diff] [blame] | 371 | } |
| 372 | |
| 373 | static TARGET_UID: Uid = Uid::from_raw(10020); |
| 374 | static TARGET_GID: Gid = Gid::from_raw(10020); |
| 375 | static TARGET_CTX: &str = "u:r:untrusted_app:s0:c91,c256,c10,c20"; |
| 376 | |
| 377 | /// Tests that the closure is running as the target identity. |
| 378 | #[test] |
| 379 | fn test_transition_to_untrusted_app() { |
Janis Danisevskis | 04945eb | 2021-12-06 17:20:09 -0800 | [diff] [blame] | 380 | // Safety: run_as must be called from a single threaded process. |
| 381 | // This device test is run as a separate single threaded process. |
| 382 | unsafe { |
| 383 | run_as(TARGET_CTX, TARGET_UID, TARGET_GID, || { |
| 384 | assert_eq!(TARGET_UID, getuid()); |
| 385 | assert_eq!(TARGET_GID, getgid()); |
| 386 | assert_eq!(TARGET_CTX, selinux::getcon().unwrap().to_str().unwrap()); |
| 387 | }) |
| 388 | }; |
Janis Danisevskis | a578d39 | 2021-09-20 15:44:06 -0700 | [diff] [blame] | 389 | } |
| 390 | |
| 391 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] |
| 392 | struct SomeResult { |
| 393 | a: u32, |
| 394 | b: u64, |
| 395 | c: String, |
| 396 | } |
| 397 | |
| 398 | #[test] |
| 399 | fn test_serialized_result() { |
| 400 | let test_result = SomeResult { |
| 401 | a: 5, |
| 402 | b: 0xffffffffffffffff, |
| 403 | c: "supercalifragilisticexpialidocious".to_owned(), |
| 404 | }; |
| 405 | let test_result_clone = test_result.clone(); |
Janis Danisevskis | 04945eb | 2021-12-06 17:20:09 -0800 | [diff] [blame] | 406 | // Safety: run_as must be called from a single threaded process. |
| 407 | // This device test is run as a separate single threaded process. |
| 408 | let result = unsafe { run_as(TARGET_CTX, TARGET_UID, TARGET_GID, || test_result_clone) }; |
Janis Danisevskis | a578d39 | 2021-09-20 15:44:06 -0700 | [diff] [blame] | 409 | assert_eq!(test_result, result); |
| 410 | } |
Janis Danisevskis | 04945eb | 2021-12-06 17:20:09 -0800 | [diff] [blame] | 411 | |
| 412 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] |
| 413 | enum PingPong { |
| 414 | Ping, |
| 415 | Pong, |
| 416 | } |
| 417 | |
| 418 | /// Tests that closure is running under given user identity and communicates with calling |
| 419 | /// process using pipe. |
| 420 | #[test] |
| 421 | fn test_run_as_child() { |
| 422 | let test_result = SomeResult { |
| 423 | a: 5, |
| 424 | b: 0xffffffffffffffff, |
| 425 | c: "supercalifragilisticexpialidocious".to_owned(), |
| 426 | }; |
| 427 | let test_result_clone = test_result.clone(); |
| 428 | |
| 429 | // Safety: run_as_child must be called from a single threaded process. |
| 430 | // This device test is run as a separate single threaded process. |
| 431 | let mut child_handle: ChildHandle<SomeResult, PingPong> = unsafe { |
| 432 | run_as_child(TARGET_CTX, TARGET_UID, TARGET_GID, |cmd_reader, response_writer| { |
| 433 | assert_eq!(TARGET_UID, getuid()); |
| 434 | assert_eq!(TARGET_GID, getgid()); |
| 435 | assert_eq!(TARGET_CTX, selinux::getcon().unwrap().to_str().unwrap()); |
| 436 | |
| 437 | let ping: PingPong = cmd_reader.recv(); |
| 438 | assert_eq!(ping, PingPong::Ping); |
| 439 | |
| 440 | response_writer.send(&PingPong::Pong); |
| 441 | |
| 442 | let ping: PingPong = cmd_reader.recv(); |
| 443 | assert_eq!(ping, PingPong::Ping); |
| 444 | let pong: PingPong = cmd_reader.recv(); |
| 445 | assert_eq!(pong, PingPong::Pong); |
| 446 | |
| 447 | response_writer.send(&PingPong::Pong); |
| 448 | response_writer.send(&PingPong::Ping); |
| 449 | |
| 450 | test_result_clone |
| 451 | }) |
| 452 | .unwrap() |
| 453 | }; |
| 454 | |
| 455 | // Send one ping. |
| 456 | child_handle.send(&PingPong::Ping); |
| 457 | |
| 458 | // Expect one pong. |
| 459 | let pong = child_handle.recv(); |
| 460 | assert_eq!(pong, PingPong::Pong); |
| 461 | |
| 462 | // Send ping and pong. |
| 463 | child_handle.send(&PingPong::Ping); |
| 464 | child_handle.send(&PingPong::Pong); |
| 465 | |
| 466 | // Expect pong and ping. |
| 467 | let pong = child_handle.recv(); |
| 468 | assert_eq!(pong, PingPong::Pong); |
| 469 | let ping = child_handle.recv(); |
| 470 | assert_eq!(ping, PingPong::Ping); |
| 471 | |
| 472 | assert_eq!(child_handle.get_result(), test_result); |
| 473 | } |
Janis Danisevskis | a578d39 | 2021-09-20 15:44:06 -0700 | [diff] [blame] | 474 | } |