mod common;
use common::{events_of, load_vectors, run_parser, run_parser_whole, split_by_sizes};
use proptest::prelude::*;
const BOM: &[u8] = &[0xEF, 0xBB, 0xBF];
#[test]
fn d2_bom_vectors_pass() {
for v in load_vectors() {
if !v.id.starts_with("v19")
&& !v.id.starts_with("v20")
&& !v.id.starts_with("v21")
&& !v.id.starts_with("v22")
&& !v.id.starts_with("v23")
{
continue;
}
let got = run_parser(&v.chunks);
assert_eq!(got.events, v.events, "{}", v.id);
assert_eq!(got.last_event_id, v.final_last_event_id, "{}", v.id);
}
}
#[test]
fn d2_bom_stripped_and_no_panic_with_each_chunking() {
let mut bytes = BOM.to_vec();
bytes.extend_from_slice(b"data: hi\n\n");
let expect = events_of(b"data: hi\n\n");
for sizes in [&[1usize][..], &[2], &[3], &[4], &[2, 1], &[1, 2]] {
let got = run_parser(&split_by_sizes(&bytes, sizes));
assert_eq!(got.events, expect, "sizes {sizes:?}");
}
}
#[test]
fn d2_partial_bom_prefix_is_content() {
let mut bytes = vec![0xEF];
bytes.extend_from_slice(b"data: x\n\ndata: y\n\n");
let got = run_parser_whole(&bytes);
assert_eq!(got.events.len(), 1);
assert_eq!(got.events[0].data, "y");
let got = run_parser(&split_by_sizes(&bytes, &[1]));
assert_eq!(got.events.len(), 1);
assert_eq!(got.events[0].data, "y");
}
#[test]
fn d2_partial_bom_then_eof_is_harmless() {
for prefix in [&BOM[..1], &BOM[..2], BOM] {
let got = run_parser_whole(prefix);
assert!(got.events.is_empty());
assert_eq!(got.last_event_id, "");
}
}
fn body_line() -> impl Strategy<Value = Vec<u8>> {
let word = "[a-z\u{e9}\u{feff}]{0,6}";
prop_oneof![
4 => word.prop_map(|w| format!("data: {w}").into_bytes()),
2 => word.prop_map(|w| format!("data:{w}").into_bytes()),
1 => Just(b"data".to_vec()),
2 => word.prop_map(|w| format!("event: {w}").into_bytes()),
1 => word.prop_map(|w| format!(": {w}").into_bytes()),
1 => word.prop_map(|w| w.into_bytes()),
]
}
fn terminator() -> impl Strategy<Value = Vec<u8>> {
prop_oneof![
4 => Just(b"\n".to_vec()),
2 => Just(b"\r\n".to_vec()),
1 => Just(b"\r".to_vec()),
]
}
fn block() -> impl Strategy<Value = (Vec<u8>, Vec<u8>)> {
(
prop::collection::vec((body_line(), terminator()), 1..4),
terminator(),
)
.prop_map(|(lines, mut blank)| {
let mut body = Vec::new();
for (line, term) in lines {
body.extend_from_slice(&line);
body.extend_from_slice(&term);
}
if body.ends_with(b"\r") && blank == b"\n" {
blank = b"\r\n".to_vec();
}
(body, blank)
})
}
fn concat_blocks(blocks: &[(Vec<u8>, Vec<u8>)]) -> Vec<u8> {
let mut out = Vec::new();
for (body, blank) in blocks {
out.extend_from_slice(body);
out.extend_from_slice(blank);
}
out
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(1500))]
#[test]
fn prepending_a_bom_changes_nothing(
bytes in prop::collection::vec(any::<u8>(), 0..200),
sizes in prop::collection::vec(1usize..16, 1..16),
) {
prop_assume!(!bytes.starts_with(BOM));
let plain = run_parser_whole(&bytes);
let mut with_bom = BOM.to_vec();
with_bom.extend_from_slice(&bytes);
prop_assert_eq!(run_parser_whole(&with_bom), plain.clone());
prop_assert_eq!(run_parser(&split_by_sizes(&with_bom, &sizes)), plain);
}
#[test]
fn injecting_a_bom_inside_a_block_leaves_other_blocks_unchanged(
blocks in prop::collection::vec(block(), 1..5),
pick in any::<prop::sample::Index>(),
offset in any::<prop::sample::Index>(),
) {
let k = pick.index(blocks.len());
let body_len = blocks[k].0.len();
let p = offset.index(body_len);
prop_assume!(k > 0 || p > 0);
let before = events_of(&concat_blocks(&blocks[..k]));
let after = events_of(&concat_blocks(&blocks[k + 1..]));
let mut injected = blocks.clone();
let body = &mut injected[k].0;
let tail = body.split_off(p);
body.extend_from_slice(BOM);
body.extend_from_slice(&tail);
let got = events_of(&concat_blocks(&injected));
prop_assert!(got.len() >= before.len() + after.len());
prop_assert_eq!(&got[..before.len()], &before[..]);
prop_assert_eq!(&got[got.len() - after.len()..], &after[..]);
}
}