use ftui_core::geometry::Rect;
use ftui_harness::flicker_detection::{analyze_stream, assert_flicker_free};
use ftui_render::buffer::Buffer;
use ftui_render::cell::{Cell, PackedRgba};
use ftui_render::cell::{CellAttrs, StyleFlags};
use ftui_render::diff::BufferDiff;
use ftui_render::presenter::{Presenter, TerminalCapabilities};
use proptest::prelude::*;
use std::collections::HashSet;
fn caps_with_sync() -> TerminalCapabilities {
let mut caps = TerminalCapabilities::basic();
caps.sync_output = true;
caps
}
fn caps_without_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 fnv1a(data: &[u8]) -> u64 {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
for &b in data {
hash ^= b as u64;
hash = hash.wrapping_mul(0x0100_0000_01b3);
}
hash
}
struct Lcg(u64);
impl Lcg {
fn new(seed: u64) -> Self {
Lcg(seed)
}
fn next(&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() >> 16) as u16 % max
}
fn next_char(&mut self) -> char {
char::from_u32('A' as u32 + (self.next() % 26) as u32).unwrap()
}
}
#[test]
fn theorem1_sync_brackets_wrap_all_content() {
let mut buf = Buffer::new(80, 24);
for x in 0..80 {
for y in 0..24 {
buf.set_raw(x, y, Cell::from_char('#'));
}
}
let old = Buffer::new(80, 24);
let output = present_frame(&buf, &old, caps_with_sync());
let sync_begin = b"\x1b[?2026h";
let sync_end = b"\x1b[?2026l";
let begin_pos = output
.windows(sync_begin.len())
.position(|w| w == sync_begin)
.expect("sync_begin not found");
let end_pos = output
.windows(sync_end.len())
.rposition(|w| w == sync_end)
.expect("sync_end not found");
assert!(begin_pos < end_pos, "sync_begin must precede sync_end");
let before_sync = &output[..begin_pos];
assert!(
before_sync.is_empty(),
"no content should precede sync_begin, found {} bytes",
before_sync.len()
);
assert_flicker_free(&output);
}
#[test]
fn theorem1_counterexample_no_sync_detected() {
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_without_sync());
let analysis = analyze_stream(&output);
assert!(
!analysis.stats.is_flicker_free(),
"output without sync should NOT be flicker-free"
);
assert!(analysis.stats.sync_gaps > 0, "should detect sync gaps");
}
#[test]
fn theorem1_counterexample_nested_sync_not_produced() {
let mut buf = Buffer::new(20, 5);
buf.set_raw(0, 0, Cell::from_char('A'));
let old = Buffer::new(20, 5);
let output = present_frame(&buf, &old, caps_with_sync());
let begin = b"\x1b[?2026h";
let count = output.windows(begin.len()).filter(|w| *w == begin).count();
assert_eq!(count, 1, "exactly one sync_begin per frame");
let end = b"\x1b[?2026l";
let count = output.windows(end.len()).filter(|w| *w == end).count();
assert_eq!(count, 1, "exactly one sync_end per frame");
}
proptest! {
#[test]
fn theorem2_adversarial_style_diff_completeness(
width in 5u16..80,
height in 5u16..30,
seed in 0u64..1_000_000,
) {
let old = Buffer::new(width, height);
let mut new = Buffer::new(width, height);
let mut rng = Lcg::new(seed);
let num_changes = rng.next() as usize % 200;
let mut expected = HashSet::new();
for _ in 0..num_changes {
let x = rng.next_u16(width);
let y = rng.next_u16(height);
let ch = rng.next_char();
let fg = PackedRgba::rgb(
(rng.next() % 256) as u8,
(rng.next() % 256) as u8,
(rng.next() % 256) as u8,
);
let bg = PackedRgba::rgb(
(rng.next() % 256) as u8,
(rng.next() % 256) as u8,
(rng.next() % 256) as u8,
);
new.set_raw(x, y, Cell::from_char(ch).with_fg(fg).with_bg(bg));
let old_cell = old.get_unchecked(x, y);
let new_cell = new.get_unchecked(x, y);
if !old_cell.bits_eq(new_cell) {
expected.insert((x, y));
}
}
let diff = BufferDiff::compute(&old, &new);
let diff_set: HashSet<(u16, u16)> = diff.iter().collect();
for &(x, y) in &expected {
prop_assert!(
diff_set.contains(&(x, y)),
"missing change at ({}, {})", x, y
);
}
for (x, y) in diff.iter() {
let old_cell = old.get_unchecked(x, y);
let new_cell = new.get_unchecked(x, y);
prop_assert!(
!old_cell.bits_eq(new_cell),
"false positive at ({}, {})", x, y
);
}
}
#[test]
fn theorem2_adversarial_overwrite_to_original(
width in 5u16..60,
height in 5u16..20,
num_changes in 1usize..50,
) {
let old = Buffer::new(width, height);
let mut new = old.clone();
let mut changed_positions = Vec::new();
for i in 0..num_changes {
let x = (i * 13 + 7) as u16 % width;
let y = (i * 17 + 3) as u16 % height;
new.set_raw(x, y, Cell::from_char('X'));
changed_positions.push((x, y));
}
let blank = Cell::default();
for (i, &(x, y)) in changed_positions.iter().enumerate() {
if i % 2 == 0 {
new.set_raw(x, y, blank);
}
}
let diff = BufferDiff::compute(&old, &new);
for (x, y) in diff.iter() {
let old_cell = old.get_unchecked(x, y);
let new_cell = new.get_unchecked(x, y);
prop_assert!(
!old_cell.bits_eq(new_cell),
"reverted change at ({}, {}) should not be in diff", x, y
);
}
}
}
#[test]
fn theorem2_edge_single_cell() {
let old = Buffer::new(1, 1);
let mut new = Buffer::new(1, 1);
new.set_raw(0, 0, Cell::from_char('X'));
let diff = BufferDiff::compute(&old, &new);
assert_eq!(diff.len(), 1);
assert_eq!(diff.changes()[0], (0, 0));
}
#[test]
fn theorem2_edge_block_alignment_boundaries() {
for width in [1, 2, 3, 4, 5, 7, 8, 9, 15, 16, 17] {
let old = Buffer::new(width, 1);
let mut new = Buffer::new(width, 1);
new.set_raw(width - 1, 0, Cell::from_char('Z'));
let diff = BufferDiff::compute(&old, &new);
assert_eq!(
diff.len(),
1,
"width={}: expected 1 change, got {}",
width,
diff.len()
);
assert_eq!(diff.changes()[0], (width - 1, 0));
}
}
#[test]
fn theorem3_all_mutation_paths_mark_dirty() {
{
let mut buf = Buffer::new(10, 5);
buf.clear_dirty();
buf.set(3, 2, Cell::from_char('A'));
assert!(buf.is_row_dirty(2), "set() must mark dirty");
}
{
let mut buf = Buffer::new(10, 5);
buf.clear_dirty();
buf.set_raw(3, 2, Cell::from_char('B'));
assert!(buf.is_row_dirty(2), "set_raw() must mark dirty");
}
{
let mut buf = Buffer::new(10, 5);
buf.clear_dirty();
if let Some(cell) = buf.get_mut(3, 2) {
cell.fg = PackedRgba::rgb(255, 0, 0);
}
assert!(buf.is_row_dirty(2), "get_mut() must mark dirty");
}
{
let mut buf = Buffer::new(10, 5);
buf.clear_dirty();
buf.fill(Rect::new(0, 1, 5, 3), Cell::from_char('.'));
assert!(buf.is_row_dirty(1), "fill() must mark row 1 dirty");
assert!(buf.is_row_dirty(2), "fill() must mark row 2 dirty");
assert!(buf.is_row_dirty(3), "fill() must mark row 3 dirty");
assert!(!buf.is_row_dirty(0), "fill() should not mark row 0");
assert!(!buf.is_row_dirty(4), "fill() should not mark row 4");
}
{
let mut buf = Buffer::new(10, 5);
buf.clear_dirty();
buf.clear();
assert_eq!(buf.dirty_row_count(), 5, "clear() must mark all dirty");
}
{
let mut buf = Buffer::new(10, 5);
buf.clear_dirty();
let _ = buf.cells_mut();
assert_eq!(buf.dirty_row_count(), 5, "cells_mut() must mark all dirty");
}
}
proptest! {
#[test]
fn theorem3_random_mutations_all_dirty(
width in 5u16..50,
height in 5u16..30,
seed in 0u64..1_000_000,
) {
let mut buf = Buffer::new(width, height);
buf.clear_dirty();
let mut rng = Lcg::new(seed);
let num_mutations = rng.next() as usize % 100;
let mut mutated_rows = HashSet::new();
for _ in 0..num_mutations {
let x = rng.next_u16(width);
let y = rng.next_u16(height);
buf.set_raw(x, y, Cell::from_char(rng.next_char()));
mutated_rows.insert(y);
}
for &y in &mutated_rows {
prop_assert!(
buf.is_row_dirty(y),
"row {} was mutated but not dirty", y
);
}
}
}
proptest! {
#[test]
fn theorem4_full_vs_dirty_equivalence(
width in 5u16..80,
height in 5u16..30,
seed in 0u64..1_000_000,
) {
let old = Buffer::new(width, height);
let mut new = Buffer::new(width, height);
let mut rng = Lcg::new(seed);
let num_changes = rng.next() as usize % 200;
for _ in 0..num_changes {
let x = rng.next_u16(width);
let y = rng.next_u16(height);
new.set_raw(x, y, Cell::from_char(rng.next_char()));
}
let full = BufferDiff::compute(&old, &new);
let dirty = BufferDiff::compute_dirty(&old, &new);
prop_assert_eq!(
full.changes(),
dirty.changes(),
"full and dirty diff must be identical"
);
}
#[test]
fn theorem4_selective_dirty_correctness(
width in 5u16..40,
height in 5u16..20,
seed in 0u64..1_000_000,
) {
let old = Buffer::new(width, height);
let mut new = old.clone();
let mut rng = Lcg::new(seed);
let num_changes = rng.next() as usize % 50;
for _ in 0..num_changes {
let x = rng.next_u16(width);
let y = rng.next_u16(height);
new.set_raw(x, y, Cell::from_char(rng.next_char()));
}
let full = BufferDiff::compute(&old, &new);
let dirty = BufferDiff::compute_dirty(&old, &new);
prop_assert_eq!(
full.changes(),
dirty.changes(),
"dirty diff misses changes"
);
}
}
#[test]
fn theorem5_resize_no_ghosting_grow() {
let mut content = Buffer::new(100, 30);
let mut rng = Lcg::new(0xABCD_1234);
for _ in 0..500 {
let x = rng.next_u16(100);
let y = rng.next_u16(30);
content.set_raw(x, y, Cell::from_char(rng.next_char()));
}
let old = Buffer::new(100, 30);
let diff = BufferDiff::compute(&old, &content);
let blank = Cell::default();
for y in 0..30 {
for x in 0..100 {
let cell = content.get_unchecked(x, y);
if !cell.bits_eq(&blank) {
assert!(
diff.changes().contains(&(x, y)),
"ghosting: cell ({}, {}) missing from diff after resize",
x,
y
);
}
}
}
}
#[test]
fn theorem5_resize_no_ghosting_shrink() {
let mut original = Buffer::new(120, 40);
let mut rng = Lcg::new(0xDEAD_BEEF);
for _ in 0..800 {
let x = rng.next_u16(120);
let y = rng.next_u16(40);
original.set_raw(x, y, Cell::from_char(rng.next_char()));
}
let mut shrunken = Buffer::new(80, 24);
for y in 0..24u16 {
for x in 0..80u16 {
let cell = *original.get_unchecked(x, y);
shrunken.set_raw(x, y, cell);
}
}
let old = Buffer::new(80, 24);
let diff = BufferDiff::compute(&old, &shrunken);
let blank = Cell::default();
for y in 0..24 {
for x in 0..80 {
let cell = shrunken.get_unchecked(x, y);
if !cell.bits_eq(&blank) {
assert!(
diff.changes().contains(&(x, y)),
"ghosting after shrink: ({}, {}) missing",
x,
y
);
}
}
}
}
proptest! {
#[test]
fn theorem5_post_resize_completeness(
width in 10u16..80,
height in 5u16..30,
seed in 0u64..1_000_000,
) {
let mut buf = Buffer::new(width, height);
let mut rng = Lcg::new(seed);
let num_cells = rng.next() as usize % 200;
for _ in 0..num_cells {
let x = rng.next_u16(width);
let y = rng.next_u16(height);
buf.set_raw(x, y, Cell::from_char(rng.next_char()));
}
let blank = Buffer::new(width, height);
let diff = BufferDiff::compute(&blank, &buf);
let default_cell = Cell::default();
for y in 0..height {
for x in 0..width {
let cell = buf.get_unchecked(x, y);
if !cell.bits_eq(&default_cell) {
prop_assert!(
diff.changes().contains(&(x, y)),
"post-resize ghosting at ({}, {})", x, y
);
}
}
}
}
}
#[test]
fn theorem6_e2e_multi_frame_flicker_free() {
let caps = caps_with_sync();
let width = 80u16;
let height = 24u16;
let mut rng = Lcg::new(0xE2E0_5EED);
let mut prev = Buffer::new(width, height);
for frame in 0..20 {
let mut current = prev.clone();
let num_mutations = rng.next() as usize % 50;
for _ in 0..num_mutations {
let x = rng.next_u16(width);
let y = rng.next_u16(height);
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 {} not flicker-free: gaps={}, clears={}, frames={}/{}",
frame,
analysis.stats.sync_gaps,
analysis.stats.partial_clears,
analysis.stats.complete_frames,
analysis.stats.total_frames
);
prev = current;
}
}
#[test]
fn theorem6_e2e_full_screen_rewrite_stress() {
let caps = caps_with_sync();
let mut rng = Lcg::new(0x5713_5555);
let mut prev = Buffer::new(120, 40);
for _frame in 0..10 {
let mut current = Buffer::new(120, 40);
for y in 0..40 {
for x in 0..120 {
current.set_raw(x, y, Cell::from_char(rng.next_char()));
}
}
let output = present_frame(¤t, &prev, caps);
assert_flicker_free(&output);
prev = current;
}
}
#[test]
fn theorem6_adversarial_flash_pattern() {
let caps = caps_with_sync();
let width = 60u16;
let height = 20u16;
let mut populated = Buffer::new(width, height);
for y in 0..height {
for x in 0..width {
populated.set_raw(x, y, Cell::from_char('#'));
}
}
let blank = Buffer::new(width, height);
let mut prev = blank.clone();
for i in 0..10 {
let current = if i % 2 == 0 { &populated } else { &blank };
let output = present_frame(current, &prev, caps);
assert_flicker_free(&output);
prev = current.clone();
}
}
#[test]
fn theorem6_empty_diff_valid_sync() {
let buf = Buffer::new(40, 10);
let output = present_frame(&buf, &buf, caps_with_sync());
let analysis = analyze_stream(&output);
assert!(
analysis.stats.is_flicker_free(),
"empty diff must still be flicker-free"
);
assert_eq!(analysis.stats.total_frames, 1);
assert_eq!(analysis.stats.complete_frames, 1);
}
#[test]
fn counterexample_style_only_changes_detected() {
let mut old = Buffer::new(20, 5);
let mut new = Buffer::new(20, 5);
old.set_raw(5, 2, Cell::from_char('A'));
new.set_raw(
5,
2,
Cell::from_char('A')
.with_fg(PackedRgba::rgb(255, 0, 0))
.with_bg(PackedRgba::rgb(0, 0, 255)),
);
let diff = BufferDiff::compute(&old, &new);
assert!(
diff.changes().contains(&(5, 2)),
"style-only change must be detected"
);
}
#[test]
fn counterexample_attribute_only_change_detected() {
let mut old = Buffer::new(20, 5);
let mut new = Buffer::new(20, 5);
let plain = Cell::from_char('B');
old.set_raw(3, 1, plain);
let mut bold = Cell::from_char('B');
bold.attrs = CellAttrs::new(StyleFlags::BOLD, 0);
new.set_raw(3, 1, bold);
let diff = BufferDiff::compute(&old, &new);
assert!(
diff.changes().contains(&(3, 1)),
"attribute-only change must be detected"
);
}
#[test]
fn golden_proof_determinism() {
let caps = caps_with_sync();
let mut rng = Lcg::new(0xC01D_E001);
let mut buf = Buffer::new(80, 24);
for _ in 0..100 {
let x = rng.next_u16(80);
let y = rng.next_u16(24);
buf.set_raw(x, y, Cell::from_char(rng.next_char()));
}
let old = Buffer::new(80, 24);
let output1 = present_frame(&buf, &old, caps);
let mut rng2 = Lcg::new(0xC01D_E001);
let mut buf2 = Buffer::new(80, 24);
for _ in 0..100 {
let x = rng2.next_u16(80);
let y = rng2.next_u16(24);
buf2.set_raw(x, y, Cell::from_char(rng2.next_char()));
}
let output2 = present_frame(&buf2, &old, caps);
assert_eq!(
fnv1a(&output1),
fnv1a(&output2),
"identical inputs must produce identical ANSI output"
);
}
#[test]
fn e2e_proof_verification_jsonl() {
use std::time::Instant;
let caps = caps_with_sync();
let scenarios: &[(u16, u16, u64, &str)] = &[
(80, 24, 0x000D_A00F_0001, "standard_terminal"),
(120, 40, 0x000D_A00F_0002, "large_terminal"),
(40, 10, 0x000D_A00F_0003, "small_terminal"),
(200, 50, 0x000D_A00F_0004, "ultrawide"),
];
for &(width, height, seed, label) in scenarios {
let start = Instant::now();
let mut rng = Lcg::new(seed);
let mut prev = Buffer::new(width, height);
let mut total_changes = 0usize;
let mut total_frames = 0u32;
let mut all_flicker_free = true;
for _ in 0..5 {
let mut current = prev.clone();
let n = rng.next() as usize % 100;
for _ in 0..n {
let x = rng.next_u16(width);
let y = rng.next_u16(height);
current.set_raw(x, y, Cell::from_char(rng.next_char()));
}
let diff = BufferDiff::compute(&prev, ¤t);
total_changes += diff.len();
let output = present_frame(¤t, &prev, caps);
let analysis = analyze_stream(&output);
if !analysis.stats.is_flicker_free() {
all_flicker_free = false;
}
total_frames += 1;
prev = current;
}
let elapsed_us = start.elapsed().as_micros();
eprintln!(
"{{\"test\":\"no_flicker_proof\",\"scenario\":\"{}\",\"width\":{},\"height\":{},\"seed\":{},\"frames\":{},\"total_changes\":{},\"flicker_free\":{},\"elapsed_us\":{}}}",
label, width, height, seed, total_frames, total_changes, all_flicker_free, elapsed_us
);
assert!(all_flicker_free, "scenario '{}' had flicker", label);
}
}
proptest! {
#[test]
fn stress_resize_oscillation_flicker_free(
base_width in 20u16..60,
base_height in 10u16..25,
seed in 0u64..100_000,
) {
let caps = caps_with_sync();
let mut rng = Lcg::new(seed);
for _ in 0..5 {
let dw = (rng.next() % 20) as i32 - 10;
let dh = (rng.next() % 10) as i32 - 5;
let w = (base_width as i32 + dw).clamp(5, 200) as u16;
let h = (base_height as i32 + dh).clamp(5, 50) as u16;
let mut buf = Buffer::new(w, h);
let n = rng.next() as usize % 50;
for _ in 0..n {
let x = rng.next_u16(w);
let y = rng.next_u16(h);
buf.set_raw(x, y, Cell::from_char(rng.next_char()));
}
let blank = Buffer::new(w, h);
let output = present_frame(&buf, &blank, caps);
let analysis = analyze_stream(&output);
prop_assert!(
analysis.stats.is_flicker_free(),
"resize to {}x{} produced flicker", w, h
);
}
}
}