tests(nix-daemon/framed): more thorough, failing tests
This is mostly a WIP commit, to demonstrate bugs properly. See #120. The tests are marked `#[should_panic]`, since they are intended to fail. Change-Id: I39f1d66742e6629ccb889da8ef1199117b91b126 Reviewed-on: https://cl.snix.dev/c/snix/+/30490 Tested-by: besadii Reviewed-by: Florian Klink <flokli@flokli.de>
This commit is contained in:
parent
02b084ec0b
commit
4ef7c50a2d
1 changed files with 172 additions and 11 deletions
|
|
@ -10,7 +10,7 @@ use tokio::io::{AsyncRead, ReadBuf};
|
||||||
/// State machine for [`NixFramedReader`].
|
/// State machine for [`NixFramedReader`].
|
||||||
///
|
///
|
||||||
/// As the reader progresses it linearly cycles through the states.
|
/// As the reader progresses it linearly cycles through the states.
|
||||||
#[derive(Debug)]
|
#[derive(Debug, PartialEq)]
|
||||||
enum NixFramedReaderState {
|
enum NixFramedReaderState {
|
||||||
/// The reader always starts in this state.
|
/// The reader always starts in this state.
|
||||||
///
|
///
|
||||||
|
|
@ -53,6 +53,15 @@ impl<R> NixFramedReader<R> {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
fn is_eof(&self) -> bool {
|
||||||
|
self.state
|
||||||
|
== NixFramedReaderState::ReadingSize {
|
||||||
|
buf: [0; 8],
|
||||||
|
filled: 8,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R: AsyncRead> AsyncRead for NixFramedReader<R> {
|
impl<R: AsyncRead> AsyncRead for NixFramedReader<R> {
|
||||||
|
|
@ -136,15 +145,21 @@ impl<R: AsyncRead> AsyncRead for NixFramedReader<R> {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod nix_framed_tests {
|
mod nix_framed_tests {
|
||||||
use std::time::Duration;
|
use std::{
|
||||||
|
cmp::min,
|
||||||
|
pin::Pin,
|
||||||
|
task::{self, Poll},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::{self, AsyncRead, AsyncReadExt, ReadBuf};
|
||||||
use tokio_test::io::Builder;
|
use tokio_test::io::Builder;
|
||||||
|
|
||||||
use crate::nix_daemon::framing::NixFramedReader;
|
use crate::nix_daemon::framing::NixFramedReader;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn read_hello_world_in_two_frames() {
|
#[should_panic] // broken
|
||||||
|
async fn read_unexpected_eof_after_frame() {
|
||||||
let mut mock = Builder::new()
|
let mut mock = Builder::new()
|
||||||
// The client sends len
|
// The client sends len
|
||||||
.read(&5u64.to_le_bytes())
|
.read(&5u64.to_le_bytes())
|
||||||
|
|
@ -154,18 +169,58 @@ mod nix_framed_tests {
|
||||||
// Send more data separately
|
// Send more data separately
|
||||||
.read(&6u64.to_le_bytes())
|
.read(&6u64.to_le_bytes())
|
||||||
.read(" world".as_bytes())
|
.read(" world".as_bytes())
|
||||||
|
// NOTE: no terminating zero
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let mut reader = NixFramedReader::new(&mut mock);
|
let mut reader = NixFramedReader::new(&mut mock);
|
||||||
let mut result = String::new();
|
let err = reader.read_to_string(&mut String::new()).await.unwrap_err();
|
||||||
reader
|
assert!(!reader.is_eof());
|
||||||
.read_to_string(&mut result)
|
assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
|
||||||
.await
|
|
||||||
.expect("Could not read into result");
|
|
||||||
assert_eq!("hello world", result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn read_hello_world_in_two_frames_followed_by_zero_sized_frame() {
|
#[should_panic] // broken
|
||||||
|
async fn read_unexpected_eof_in_frame() {
|
||||||
|
let mut mock = Builder::new()
|
||||||
|
// The client sends len
|
||||||
|
.read(&5u64.to_le_bytes())
|
||||||
|
// Immediately followed by the bytes
|
||||||
|
.read("hello".as_bytes())
|
||||||
|
.wait(Duration::ZERO)
|
||||||
|
// Send more data separately
|
||||||
|
.read(&6u64.to_le_bytes())
|
||||||
|
.read(" worl".as_bytes())
|
||||||
|
// NOTE: we only sent five bytes of data before EOF
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let mut reader = NixFramedReader::new(&mut mock);
|
||||||
|
let err = reader.read_to_string(&mut String::new()).await.unwrap_err();
|
||||||
|
assert!(!reader.is_eof());
|
||||||
|
assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[should_panic] // broken
|
||||||
|
async fn read_unexpected_eof_in_length() {
|
||||||
|
let mut mock = Builder::new()
|
||||||
|
// The client sends len
|
||||||
|
.read(&5u64.to_le_bytes())
|
||||||
|
// Immediately followed by the bytes
|
||||||
|
.read("hello".as_bytes())
|
||||||
|
.wait(Duration::ZERO)
|
||||||
|
// Send a truncated length header
|
||||||
|
.read(&[0; 7])
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let mut reader = NixFramedReader::new(&mut mock);
|
||||||
|
let err = reader.read_to_string(&mut String::new()).await.unwrap_err();
|
||||||
|
assert!(!reader.is_eof());
|
||||||
|
assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[should_panic] // broken
|
||||||
|
async fn read_hello_world_in_two_frames() {
|
||||||
let mut mock = Builder::new()
|
let mut mock = Builder::new()
|
||||||
// The client sends len
|
// The client sends len
|
||||||
.read(&5u64.to_le_bytes())
|
.read(&5u64.to_le_bytes())
|
||||||
|
|
@ -185,5 +240,111 @@ mod nix_framed_tests {
|
||||||
.await
|
.await
|
||||||
.expect("Could not read into result");
|
.expect("Could not read into result");
|
||||||
assert_eq!("hello world", result);
|
assert_eq!("hello world", result);
|
||||||
|
assert!(reader.is_eof());
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SplitMock<'a>(&'a [u8]);
|
||||||
|
|
||||||
|
impl AsyncRead for SplitMock<'_> {
|
||||||
|
fn poll_read(
|
||||||
|
mut self: Pin<&mut Self>,
|
||||||
|
_cx: &mut task::Context<'_>,
|
||||||
|
buf: &mut ReadBuf<'_>,
|
||||||
|
) -> Poll<io::Result<()>> {
|
||||||
|
let data = self.0;
|
||||||
|
|
||||||
|
if data.is_empty() {
|
||||||
|
Poll::Pending
|
||||||
|
} else {
|
||||||
|
let n = min(buf.remaining(), data.len());
|
||||||
|
buf.put_slice(&data[..n]);
|
||||||
|
self.0 = &data[n..];
|
||||||
|
|
||||||
|
Poll::Ready(Ok(()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Somewhat of a fuzz test, ensuring that we end up in identical states for the same input,
|
||||||
|
/// independent of how it is spread across read calls and poll cycles.
|
||||||
|
#[test]
|
||||||
|
#[should_panic] // broken
|
||||||
|
fn split_verif() {
|
||||||
|
let mut cx = task::Context::from_waker(task::Waker::noop());
|
||||||
|
let mut input = make_framed(&[b"hello", b"world", b"!", b""]);
|
||||||
|
let framed_end = input.len();
|
||||||
|
input.extend_from_slice(b"trailing data");
|
||||||
|
|
||||||
|
for end_point in 0..input.len() {
|
||||||
|
let input = &input[..end_point];
|
||||||
|
|
||||||
|
let unsplit_res = {
|
||||||
|
let mut dut = NixFramedReader::new(SplitMock(input));
|
||||||
|
let mut data_buf = vec![0; input.len()];
|
||||||
|
let mut read_buf = ReadBuf::new(&mut data_buf);
|
||||||
|
|
||||||
|
for _ in 0..256 {
|
||||||
|
match Pin::new(&mut dut).poll_read(&mut cx, &mut read_buf) {
|
||||||
|
Poll::Ready(res) => res.unwrap(),
|
||||||
|
Poll::Pending => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let len = read_buf.filled().len();
|
||||||
|
data_buf.truncate(len);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
end_point >= framed_end,
|
||||||
|
dut.is_eof(),
|
||||||
|
"end_point = {end_point}, state = {:?}",
|
||||||
|
dut.state
|
||||||
|
);
|
||||||
|
(dut.state, data_buf, dut.reader.0)
|
||||||
|
};
|
||||||
|
|
||||||
|
for split_point in 1..end_point.saturating_sub(1) {
|
||||||
|
let split_res = {
|
||||||
|
let mut dut = NixFramedReader::new(SplitMock(&[]));
|
||||||
|
let mut data_buf = vec![0; input.len()];
|
||||||
|
let mut read_buf = ReadBuf::new(&mut data_buf);
|
||||||
|
|
||||||
|
dut.reader.0 = &input[..split_point];
|
||||||
|
for _ in 0..256 {
|
||||||
|
match Pin::new(&mut dut).poll_read(&mut cx, &mut read_buf) {
|
||||||
|
Poll::Ready(res) => res.unwrap(),
|
||||||
|
Poll::Pending => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dut.reader.0 = &input[split_point - dut.reader.0.len()..];
|
||||||
|
for _ in 0..256 {
|
||||||
|
match Pin::new(&mut dut).poll_read(&mut cx, &mut read_buf) {
|
||||||
|
Poll::Ready(res) => res.unwrap(),
|
||||||
|
Poll::Pending => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let len = read_buf.filled().len();
|
||||||
|
data_buf.truncate(len);
|
||||||
|
|
||||||
|
(dut.state, data_buf, dut.reader.0)
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(split_res, unsplit_res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Make framed data, given frame contents. Terminating frame is *not* implicitly included.
|
||||||
|
/// Include an empty slice explicitly.
|
||||||
|
fn make_framed(frames: &[&[u8]]) -> Vec<u8> {
|
||||||
|
let mut buf = vec![];
|
||||||
|
|
||||||
|
for &data in frames {
|
||||||
|
buf.extend_from_slice(&(data.len() as u64).to_le_bytes());
|
||||||
|
buf.extend_from_slice(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
buf
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue