use ftui_core::terminal_capabilities::TerminalCapabilities;
use ftui_harness::flicker_detection::{analyze_stream, assert_flicker_free};
use ftui_render::ansi;
use ftui_render::buffer::Buffer;
use ftui_render::cell::{Cell, PackedRgba};
use ftui_render::diff::BufferDiff;
use ftui_render::presenter::Presenter;
fn caps_sync() -> TerminalCapabilities {
let mut caps = TerminalCapabilities::basic();
caps.sync_output = true;
caps
}
fn caps_no_sync() -> TerminalCapabilities {
let mut caps = TerminalCapabilities::basic();
caps.sync_output = false;
caps
}
fn present_frame(buffer: &Buffer, old: &Buffer, caps: TerminalCapabilities) -> Vec<u8> {
let diff = BufferDiff::compute(old, buffer);
let mut sink = Vec::new();
let mut presenter = Presenter::new(&mut sink, caps);
presenter.present(buffer, &diff).unwrap();
drop(presenter);
sink
}
fn count_occurrences(haystack: &[u8], needle: &[u8]) -> usize {
haystack
.windows(needle.len())
.filter(|w| *w == needle)
.count()
}
fn find_all_positions(haystack: &[u8], needle: &[u8]) -> Vec<usize> {
haystack
.windows(needle.len())
.enumerate()
.filter(|(_, w)| *w == needle)
.map(|(i, _)| i)
.collect()
}
struct Lcg(u64);
impl Lcg {
fn new(seed: u64) -> Self {
Self(seed)
}
fn next_u64(&mut self) -> u64 {
self.0 = self
.0
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1);
self.0
}
fn next_u16(&mut self, max: u16) -> u16 {
(self.next_u64() >> 16) as u16 % max
}
fn next_char(&mut self) -> char {
char::from_u32('A' as u32 + (self.next_u64() % 26) as u32).unwrap()
}
}
fn random_buffer(width: u16, height: u16, seed: u64, fill_fraction: f64) -> Buffer {
let mut buf = Buffer::new(width, height);
let mut rng = Lcg::new(seed);
let total = (width as usize) * (height as usize);
let fill_count = (total as f64 * fill_fraction) as usize;
for _ in 0..fill_count {
let x = rng.next_u16(width);
let y = rng.next_u16(height);
let ch = rng.next_char();
let fg = PackedRgba::rgb(
(rng.next_u64() % 256) as u8,
(rng.next_u64() % 256) as u8,
(rng.next_u64() % 256) as u8,
);
buf.set_raw(x, y, Cell::from_char(ch).with_fg(fg));
}
buf
}
#[test]
fn pairing_single_frame_exact_count() {
let buf = random_buffer(80, 24, 0xAA01_0001, 0.5);
let old = Buffer::new(80, 24);
let output = present_frame(&buf, &old, caps_sync());
assert_eq!(
count_occurrences(&output, ansi::SYNC_BEGIN),
1,
"exactly one sync_begin per frame"
);
assert_eq!(
count_occurrences(&output, ansi::SYNC_END),
1,
"exactly one sync_end per frame"
);
}
#[test]
fn pairing_begin_before_end() {
let buf = random_buffer(120, 40, 0xAA01_0002, 0.8);
let old = Buffer::new(120, 40);
let output = present_frame(&buf, &old, caps_sync());
let begin_pos = find_all_positions(&output, ansi::SYNC_BEGIN);
let end_pos = find_all_positions(&output, ansi::SYNC_END);
assert_eq!(begin_pos.len(), 1);
assert_eq!(end_pos.len(), 1);
assert!(
begin_pos[0] < end_pos[0],
"sync_begin ({}) must precede sync_end ({})",
begin_pos[0],
end_pos[0]
);
}
#[test]
fn pairing_empty_diff_balanced() {
let buf = Buffer::new(40, 10);
let output = present_frame(&buf, &buf, caps_sync());
assert_eq!(count_occurrences(&output, ansi::SYNC_BEGIN), 1);
assert_eq!(count_occurrences(&output, ansi::SYNC_END), 1);
assert_flicker_free(&output);
}
#[test]
fn pairing_multi_frame_each_balanced() {
let caps = caps_sync();
let mut rng = Lcg::new(0xAA01_0003);
let mut prev = Buffer::new(80, 24);
for frame_id in 0..20 {
let mut current = prev.clone();
let n = rng.next_u64() as usize % 100;
for _ in 0..n {
let x = rng.next_u16(80);
let y = rng.next_u16(24);
current.set_raw(x, y, Cell::from_char(rng.next_char()));
}
let output = present_frame(¤t, &prev, caps);
assert_eq!(
count_occurrences(&output, ansi::SYNC_BEGIN),
1,
"frame {}: expected 1 sync_begin",
frame_id
);
assert_eq!(
count_occurrences(&output, ansi::SYNC_END),
1,
"frame {}: expected 1 sync_end",
frame_id
);
let positions_begin = find_all_positions(&output, ansi::SYNC_BEGIN);
let positions_end = find_all_positions(&output, ansi::SYNC_END);
assert!(
positions_begin[0] < positions_end[0],
"frame {}: begin must precede end",
frame_id
);
prev = current;
}
}
#[test]
fn pairing_large_buffer_balanced() {
let buf = random_buffer(200, 60, 0xAA01_B161, 1.0);
let old = Buffer::new(200, 60);
let output = present_frame(&buf, &old, caps_sync());
assert_eq!(count_occurrences(&output, ansi::SYNC_BEGIN), 1);
assert_eq!(count_occurrences(&output, ansi::SYNC_END), 1);
assert_flicker_free(&output);
}
#[test]
fn pairing_single_cell_balanced() {
let mut buf = Buffer::new(1, 1);
buf.set_raw(0, 0, Cell::from_char('X'));
let old = Buffer::new(1, 1);
let output = present_frame(&buf, &old, caps_sync());
assert_eq!(count_occurrences(&output, ansi::SYNC_BEGIN), 1);
assert_eq!(count_occurrences(&output, ansi::SYNC_END), 1);
}
#[test]
fn enclosure_no_escapes_outside_brackets() {
let buf = random_buffer(80, 24, 0xBB01_0001, 0.6);
let old = Buffer::new(80, 24);
let output = present_frame(&buf, &old, caps_sync());
let begin_pos = output
.windows(ansi::SYNC_BEGIN.len())
.position(|w| w == ansi::SYNC_BEGIN)
.expect("sync_begin missing");
let end_pos = output
.windows(ansi::SYNC_END.len())
.rposition(|w| w == ansi::SYNC_END)
.expect("sync_end missing");
let before = &output[..begin_pos];
assert!(
before.is_empty(),
"no bytes should appear before sync_begin, found {} bytes",
before.len()
);
let after = &output[end_pos + ansi::SYNC_END.len()..];
assert!(
after.is_empty(),
"no bytes should appear after sync_end, found {} bytes",
after.len()
);
}
#[test]
fn enclosure_content_within_brackets() {
let mut buf = Buffer::new(10, 1);
for x in 0..10 {
buf.set_raw(
x,
0,
Cell::from_char(char::from_u32('A' as u32 + x as u32).unwrap()),
);
}
let old = Buffer::new(10, 1);
let output = present_frame(&buf, &old, caps_sync());
let begin_pos = output
.windows(ansi::SYNC_BEGIN.len())
.position(|w| w == ansi::SYNC_BEGIN)
.unwrap();
let end_pos = output
.windows(ansi::SYNC_END.len())
.rposition(|w| w == ansi::SYNC_END)
.unwrap();
let content_start = begin_pos + ansi::SYNC_BEGIN.len();
for (i, &b) in output.iter().enumerate() {
if b.is_ascii_uppercase() {
assert!(
i > begin_pos && i < end_pos,
"content byte '{}' at position {} is outside sync brackets ({}, {})",
b as char,
i,
begin_pos,
end_pos
);
}
}
let inner = &output[content_start..end_pos];
for ch in b'A'..=b'J' {
assert!(
inner.contains(&ch),
"character '{}' should appear within sync brackets",
ch as char
);
}
}
#[test]
fn enclosure_sgr_reset_within_brackets() {
let mut buf = Buffer::new(5, 1);
buf.set_raw(
0,
0,
Cell::from_char('X').with_fg(PackedRgba::rgb(255, 0, 0)),
);
let old = Buffer::new(5, 1);
let output = present_frame(&buf, &old, caps_sync());
let begin_pos = output
.windows(ansi::SYNC_BEGIN.len())
.position(|w| w == ansi::SYNC_BEGIN)
.unwrap();
let end_pos = output
.windows(ansi::SYNC_END.len())
.rposition(|w| w == ansi::SYNC_END)
.unwrap();
let sgr_pos = output
.windows(b"\x1b[0m".len())
.rposition(|w| w == b"\x1b[0m")
.expect("SGR reset missing");
assert!(
sgr_pos > begin_pos && sgr_pos < end_pos,
"SGR reset at {} must be within brackets ({}, {})",
sgr_pos,
begin_pos,
end_pos
);
}
#[test]
fn enclosure_cursor_positioning_within_brackets() {
let mut buf = Buffer::new(80, 24);
buf.set_raw(0, 0, Cell::from_char('A'));
buf.set_raw(79, 23, Cell::from_char('Z'));
let old = Buffer::new(80, 24);
let output = present_frame(&buf, &old, caps_sync());
let begin_pos = output
.windows(ansi::SYNC_BEGIN.len())
.position(|w| w == ansi::SYNC_BEGIN)
.unwrap();
let end_pos = output
.windows(ansi::SYNC_END.len())
.rposition(|w| w == ansi::SYNC_END)
.unwrap();
for (i, &b) in output.iter().enumerate() {
if b == 0x1b {
assert!(
i >= begin_pos && i <= end_pos,
"ESC at position {} is outside brackets ({}, {})",
i,
begin_pos,
end_pos
);
}
}
}
#[test]
fn fallback_cursor_hide_show_used() {
let mut buf = Buffer::new(40, 10);
buf.set_raw(5, 3, Cell::from_char('X'));
let old = Buffer::new(40, 10);
let output = present_frame(&buf, &old, caps_no_sync());
assert_eq!(
count_occurrences(&output, ansi::SYNC_BEGIN),
0,
"sync_begin should not appear"
);
assert_eq!(
count_occurrences(&output, ansi::SYNC_END),
0,
"sync_end should not appear"
);
assert!(
output.starts_with(ansi::CURSOR_HIDE),
"fallback should start with cursor hide"
);
assert!(
output.ends_with(ansi::CURSOR_SHOW),
"fallback should end with cursor show"
);
}
#[test]
fn fallback_cursor_brackets_balanced() {
let buf = random_buffer(80, 24, 0xCC01_0001, 0.5);
let old = Buffer::new(80, 24);
let output = present_frame(&buf, &old, caps_no_sync());
assert_eq!(
count_occurrences(&output, ansi::CURSOR_HIDE),
1,
"exactly one cursor_hide"
);
assert_eq!(
count_occurrences(&output, ansi::CURSOR_SHOW),
1,
"exactly one cursor_show"
);
}
#[test]
fn fallback_hide_before_show() {
let buf = random_buffer(60, 20, 0xCC01_0002, 0.7);
let old = Buffer::new(60, 20);
let output = present_frame(&buf, &old, caps_no_sync());
let hide_pos = find_all_positions(&output, ansi::CURSOR_HIDE);
let show_pos = find_all_positions(&output, ansi::CURSOR_SHOW);
assert_eq!(hide_pos.len(), 1);
assert_eq!(show_pos.len(), 1);
assert!(
hide_pos[0] < show_pos[0],
"cursor_hide must precede cursor_show"
);
}
#[test]
fn fallback_content_within_cursor_brackets() {
let mut buf = Buffer::new(10, 1);
for x in 0..10 {
buf.set_raw(x, 0, Cell::from_char('A'));
}
let old = Buffer::new(10, 1);
let output = present_frame(&buf, &old, caps_no_sync());
let hide_pos = output
.windows(ansi::CURSOR_HIDE.len())
.position(|w| w == ansi::CURSOR_HIDE)
.unwrap();
let show_pos = output
.windows(ansi::CURSOR_SHOW.len())
.rposition(|w| w == ansi::CURSOR_SHOW)
.unwrap();
for (i, &b) in output.iter().enumerate() {
if b == b'A' {
assert!(
i > hide_pos && i < show_pos,
"content byte at {} outside cursor brackets ({}, {})",
i,
hide_pos,
show_pos
);
}
}
}
#[test]
fn fallback_empty_diff_balanced() {
let buf = Buffer::new(20, 5);
let output = present_frame(&buf, &buf, caps_no_sync());
assert_eq!(count_occurrences(&output, ansi::CURSOR_HIDE), 1);
assert_eq!(count_occurrences(&output, ansi::CURSOR_SHOW), 1);
assert!(output.starts_with(ansi::CURSOR_HIDE));
assert!(output.ends_with(ansi::CURSOR_SHOW));
}
#[test]
fn fallback_detected_as_sync_gap() {
let mut buf = Buffer::new(40, 10);
buf.set_raw(5, 3, Cell::from_char('X'));
let old = Buffer::new(40, 10);
let output = present_frame(&buf, &old, caps_no_sync());
let analysis = analyze_stream(&output);
assert!(
!analysis.stats.is_flicker_free(),
"fallback output should not be reported as flicker-free by detector"
);
assert!(
analysis.stats.sync_gaps > 0,
"detector should report sync gaps for fallback output"
);
}
#[test]
fn multi_frame_detector_validates_each() {
let caps = caps_sync();
let mut rng = Lcg::new(0xDD01_0001);
let mut prev = Buffer::new(80, 24);
for frame_id in 0..50 {
let mut current = prev.clone();
let n = rng.next_u64() as usize % 80;
for _ in 0..n {
let x = rng.next_u16(80);
let y = rng.next_u16(24);
current.set_raw(x, y, Cell::from_char(rng.next_char()));
}
let output = present_frame(¤t, &prev, caps);
let analysis = analyze_stream(&output);
assert!(
analysis.stats.is_flicker_free(),
"frame {}: flicker detected — gaps={}, clears={}, complete={}/{}",
frame_id,
analysis.stats.sync_gaps,
analysis.stats.partial_clears,
analysis.stats.complete_frames,
analysis.stats.total_frames,
);
prev = current;
}
}
#[test]
fn multi_frame_concatenated_stream_balanced() {
let caps = caps_sync();
let mut rng = Lcg::new(0xDD01_0002);
let mut prev = Buffer::new(60, 20);
let mut all_output = Vec::new();
let num_frames = 50;
for _ in 0..num_frames {
let mut current = prev.clone();
let n = rng.next_u64() as usize % 40;
for _ in 0..n {
let x = rng.next_u16(60);
let y = rng.next_u16(20);
current.set_raw(x, y, Cell::from_char(rng.next_char()));
}
let output = present_frame(¤t, &prev, caps);
all_output.extend_from_slice(&output);
prev = current;
}
assert_eq!(count_occurrences(&all_output, ansi::SYNC_BEGIN), num_frames);
assert_eq!(count_occurrences(&all_output, ansi::SYNC_END), num_frames);
let begins = find_all_positions(&all_output, ansi::SYNC_BEGIN);
let ends = find_all_positions(&all_output, ansi::SYNC_END);
for (i, (&b, &e)) in begins.iter().zip(ends.iter()).enumerate() {
assert!(b < e, "frame {}: begin ({}) must precede end ({})", i, b, e);
if i + 1 < begins.len() {
assert!(
e < begins[i + 1],
"frame {}: end ({}) must precede next begin ({})",
i,
e,
begins[i + 1]
);
}
}
}
#[test]
fn state_machine_all_capability_combos() {
let mut buf = Buffer::new(20, 5);
buf.set_raw(0, 0, Cell::from_char('X'));
let old = Buffer::new(20, 5);
{
let output = present_frame(&buf, &old, caps_sync());
assert_eq!(count_occurrences(&output, ansi::SYNC_BEGIN), 1);
assert_eq!(count_occurrences(&output, ansi::SYNC_END), 1);
assert_eq!(count_occurrences(&output, ansi::CURSOR_HIDE), 0);
assert_eq!(count_occurrences(&output, ansi::CURSOR_SHOW), 0);
}
{
let output = present_frame(&buf, &old, caps_no_sync());
assert_eq!(count_occurrences(&output, ansi::SYNC_BEGIN), 0);
assert_eq!(count_occurrences(&output, ansi::SYNC_END), 0);
assert_eq!(count_occurrences(&output, ansi::CURSOR_HIDE), 1);
assert_eq!(count_occurrences(&output, ansi::CURSOR_SHOW), 1);
}
{
let mut caps = TerminalCapabilities::basic();
caps.sync_output = true;
caps.in_tmux = true;
assert!(
!caps.use_sync_output(),
"policy should disable sync in tmux"
);
}
{
let mut caps = TerminalCapabilities::basic();
caps.sync_output = true;
caps.in_screen = true;
assert!(
!caps.use_sync_output(),
"policy should disable sync in screen"
);
}
{
let mut caps = TerminalCapabilities::basic();
caps.sync_output = true;
caps.in_zellij = true;
assert!(
!caps.use_sync_output(),
"policy should disable sync in zellij"
);
}
}
#[test]
fn state_machine_known_sync_terminals() {
let sync_terminals = ["WezTerm", "Alacritty", "Ghostty", "kitty", "Contour"];
for term in &sync_terminals {
let mut caps = TerminalCapabilities::basic();
caps.sync_output = true;
let mut buf = Buffer::new(10, 3);
buf.set_raw(0, 0, Cell::from_char('T'));
let old = Buffer::new(10, 3);
let output = present_frame(&buf, &old, caps);
assert_eq!(
count_occurrences(&output, ansi::SYNC_BEGIN),
1,
"{}: should have sync brackets",
term
);
assert_flicker_free(&output);
}
}
#[test]
fn inline_renderer_sync_brackets_balanced() {
use ftui_core::inline_mode::{InlineConfig, InlineRenderer};
use std::io::{Cursor, Write};
let mut config = InlineConfig::new(6, 24, 80);
config.use_sync_output = true;
let writer = Cursor::new(Vec::new());
let mut renderer = InlineRenderer::new(writer, config);
renderer
.present_ui(|w, _config| {
w.write_all(b"Hello, world!")?;
Ok(())
})
.unwrap();
renderer
.present_ui(|w, _config| {
w.write_all(b"Frame 2")?;
Ok(())
})
.unwrap();
}
#[test]
fn inline_renderer_no_sync_no_panic() {
use ftui_core::inline_mode::{InlineConfig, InlineRenderer};
use std::io::{Cursor, Write};
let config = InlineConfig::new(6, 24, 80);
let writer = Cursor::new(Vec::new());
let mut renderer = InlineRenderer::new(writer, config);
renderer
.present_ui(|w, _config| {
w.write_all(b"Hello")?;
Ok(())
})
.unwrap();
}
#[test]
fn determinism_bracket_positions_stable() {
let buf = random_buffer(80, 24, 0xEE01_0001, 0.5);
let old = Buffer::new(80, 24);
let output1 = present_frame(&buf, &old, caps_sync());
let output2 = present_frame(&buf, &old, caps_sync());
assert_eq!(
output1, output2,
"identical inputs must produce identical output including bracket positions"
);
let pos1 = find_all_positions(&output1, ansi::SYNC_BEGIN);
let pos2 = find_all_positions(&output2, ansi::SYNC_BEGIN);
assert_eq!(pos1, pos2, "bracket positions must be deterministic");
}
#[test]
fn determinism_100_seeds() {
for seed in 0..100u64 {
let buf = random_buffer(40, 12, seed, 0.3);
let old = Buffer::new(40, 12);
let a = present_frame(&buf, &old, caps_sync());
let b = present_frame(&buf, &old, caps_sync());
assert_eq!(a, b, "seed {}: output not deterministic", seed);
}
}
#[test]
fn adversarial_mixed_content_brackets_sound() {
use ftui_render::cell::{CellAttrs, StyleFlags};
use ftui_render::link_registry::LinkRegistry;
let mut buf = Buffer::new(30, 5);
let mut links = LinkRegistry::new();
let link_id = links.register("https://example.com");
buf.set_raw(0, 0, Cell::from_char('P'));
buf.set_raw(
1,
0,
Cell::from_char('S')
.with_fg(PackedRgba::rgb(255, 0, 0))
.with_bg(PackedRgba::rgb(0, 0, 255)),
);
let mut bold = Cell::from_char('B');
bold.attrs = CellAttrs::new(StyleFlags::BOLD, 0);
buf.set_raw(2, 0, bold);
buf.set_raw(
3,
0,
Cell::from_char('L').with_attrs(CellAttrs::new(StyleFlags::empty(), link_id)),
);
let old = Buffer::new(30, 5);
let diff = BufferDiff::compute(&old, &buf);
let mut sink = Vec::new();
let caps = caps_sync();
let mut presenter = Presenter::new(&mut sink, caps);
presenter
.present_with_pool(&buf, &diff, None, Some(&links))
.unwrap();
drop(presenter);
assert_eq!(count_occurrences(&sink, ansi::SYNC_BEGIN), 1);
assert_eq!(count_occurrences(&sink, ansi::SYNC_END), 1);
assert_flicker_free(&sink);
}
#[test]
fn adversarial_unicode_brackets_sound() {
let mut buf = Buffer::new(20, 3);
buf.set_raw(0, 0, Cell::from_char('\u{4e16}')); buf.set_raw(2, 0, Cell::from_char('\u{754c}')); buf.set_raw(0, 1, Cell::from_char('A'));
buf.set_raw(1, 1, Cell::from_char('B'));
let old = Buffer::new(20, 3);
let output = present_frame(&buf, &old, caps_sync());
assert_eq!(count_occurrences(&output, ansi::SYNC_BEGIN), 1);
assert_eq!(count_occurrences(&output, ansi::SYNC_END), 1);
assert_flicker_free(&output);
}
#[test]
fn stress_1000_frames_all_balanced() {
let caps = caps_sync();
let mut rng = Lcg::new(0xFF01_0001);
let mut prev = Buffer::new(80, 24);
for _ in 0..1000 {
let mut current = prev.clone();
let n = rng.next_u64() as usize % 60;
for _ in 0..n {
let x = rng.next_u16(80);
let y = rng.next_u16(24);
current.set_raw(x, y, Cell::from_char(rng.next_char()));
}
let output = present_frame(¤t, &prev, caps);
assert_eq!(count_occurrences(&output, ansi::SYNC_BEGIN), 1);
assert_eq!(count_occurrences(&output, ansi::SYNC_END), 1);
prev = current;
}
}
#[test]
fn stress_full_screen_100_frames() {
let caps = caps_sync();
let mut prev = Buffer::new(120, 40);
for seed in 0..100u64 {
let current = random_buffer(120, 40, seed, 1.0);
let output = present_frame(¤t, &prev, caps);
assert_flicker_free(&output);
prev = current;
}
}
#[test]
fn stress_alternating_capabilities() {
let mut rng = Lcg::new(0xFF01_0002);
let mut prev = Buffer::new(60, 20);
for frame_id in 0..100 {
let use_sync = frame_id % 2 == 0;
let caps = if use_sync {
caps_sync()
} else {
caps_no_sync()
};
let mut current = prev.clone();
let n = rng.next_u64() as usize % 30;
for _ in 0..n {
let x = rng.next_u16(60);
let y = rng.next_u16(20);
current.set_raw(x, y, Cell::from_char(rng.next_char()));
}
let output = present_frame(¤t, &prev, caps);
if use_sync {
assert_eq!(
count_occurrences(&output, ansi::SYNC_BEGIN),
1,
"frame {}: sync should have brackets",
frame_id
);
assert_eq!(count_occurrences(&output, ansi::CURSOR_HIDE), 0);
} else {
assert_eq!(
count_occurrences(&output, ansi::SYNC_BEGIN),
0,
"frame {}: no-sync should not have brackets",
frame_id
);
assert_eq!(count_occurrences(&output, ansi::CURSOR_HIDE), 1);
}
prev = current;
}
}
#[test]
fn evidence_jsonl_all_properties() {
use std::time::Instant;
let scenarios: &[(u16, u16, u64, &str)] = &[
(80, 24, 0xABCD_0001, "standard_80x24"),
(120, 40, 0xABCD_0002, "large_120x40"),
(40, 10, 0xABCD_0003, "small_40x10"),
(200, 60, 0xABCD_0004, "ultrawide_200x60"),
(1, 1, 0xABCD_0005, "minimal_1x1"),
];
for &(width, height, seed, label) in scenarios {
let start = Instant::now();
let buf = random_buffer(width, height, seed, 0.5);
let old = Buffer::new(width, height);
let output_sync = present_frame(&buf, &old, caps_sync());
let sync_begins = count_occurrences(&output_sync, ansi::SYNC_BEGIN);
let sync_ends = count_occurrences(&output_sync, ansi::SYNC_END);
let sync_flicker_free = analyze_stream(&output_sync).stats.is_flicker_free();
let output_fallback = present_frame(&buf, &old, caps_no_sync());
let cursor_hides = count_occurrences(&output_fallback, ansi::CURSOR_HIDE);
let cursor_shows = count_occurrences(&output_fallback, ansi::CURSOR_SHOW);
let fallback_no_sync = count_occurrences(&output_fallback, ansi::SYNC_BEGIN) == 0;
let elapsed_us = start.elapsed().as_micros();
eprintln!(
concat!(
"{{\"test\":\"sync_bracket_completeness\",",
"\"scenario\":\"{}\",",
"\"width\":{},\"height\":{},\"seed\":{},",
"\"sync_begins\":{},\"sync_ends\":{},\"sync_balanced\":{},\"flicker_free\":{},",
"\"cursor_hides\":{},\"cursor_shows\":{},\"cursor_balanced\":{},\"fallback_no_sync\":{},",
"\"elapsed_us\":{}}}"
),
label,
width,
height,
seed,
sync_begins,
sync_ends,
sync_begins == sync_ends,
sync_flicker_free,
cursor_hides,
cursor_shows,
cursor_hides == cursor_shows,
fallback_no_sync,
elapsed_us
);
assert_eq!(sync_begins, 1, "{}: sync_begin count", label);
assert_eq!(sync_ends, 1, "{}: sync_end count", label);
assert!(sync_flicker_free, "{}: must be flicker free", label);
assert_eq!(cursor_hides, 1, "{}: cursor_hide count", label);
assert_eq!(cursor_shows, 1, "{}: cursor_show count", label);
assert!(fallback_no_sync, "{}: fallback must not use sync", label);
}
}