#![forbid(unsafe_code)]
use std::time::{Duration, Instant};
use ftui_text::editor::Editor;
const LINES_10K: usize = 10_000;
const LINES_100K: usize = 100_000;
const LINE_CONTENT: &str = "This is a test line with typical content";
const BUDGET_MULTIPLIER_DEFAULT: u128 = 1;
const BUDGET_MULTIPLIER_COVERAGE: u128 = 2;
const BUDGET_MULTIPLIER_SHARED_WORKER: u128 = 10;
const BUDGET_MULTIPLIER_ENV: &str = "FTUI_EDITOR_PERF_BUDGET_MULTIPLIER";
const SHARED_WORKER_ENV: &str = "FTUI_EDITOR_PERF_SHARED_WORKER";
const SHARED_WORKER_MARKERS: &[&str] = &[
"CI",
"RCH_JOB_ID",
"RCH_REMOTE",
"RCH_REQUIRE_REMOTE",
"RCH_WORKER",
"RCH_WORKER_ID",
];
fn create_large_buffer(lines: usize) -> Editor {
let content: String = (0..lines)
.map(|i| format!("{LINE_CONTENT} {i}\n"))
.collect();
Editor::with_text(&content)
}
fn log_perf(case: &str, lines: usize, op: &str, duration_us: u128, result: &str) {
if std::env::var("PERF_LOG").is_ok() {
println!(
r#"{{"event":"perf_test","case":"{}","lines":{},"op":"{}","duration_us":{},"result":"{}"}}"#,
case, lines, op, duration_us, result
);
}
}
fn is_coverage_run() -> bool {
std::env::var("LLVM_PROFILE_FILE").is_ok()
|| std::env::var("CARGO_LLVM_COV").is_ok()
|| std::env::var("LLVM_COV").is_ok()
|| std::env::var("RUSTFLAGS").is_ok_and(|flags| flags.contains("instrument-coverage"))
}
fn parse_env_flag_value(value: &str) -> Option<bool> {
match value.trim().to_ascii_lowercase().as_str() {
"1" | "true" | "yes" | "on" => Some(true),
"0" | "false" | "no" | "off" => Some(false),
_ => None,
}
}
fn env_flag(name: &str) -> Option<bool> {
std::env::var(name)
.ok()
.and_then(|value| parse_env_flag_value(&value))
}
fn env_present_and_not_false(name: &str) -> bool {
match std::env::var(name) {
Ok(value) => parse_env_flag_value(&value).unwrap_or_else(|| !value.trim().is_empty()),
Err(_) => false,
}
}
fn is_shared_worker_run() -> bool {
if let Some(enabled) = env_flag(SHARED_WORKER_ENV) {
return enabled;
}
is_rch_target_dir()
|| SHARED_WORKER_MARKERS
.iter()
.any(|name| env_present_and_not_false(name))
}
fn is_rch_target_dir() -> bool {
std::env::var_os("CARGO_TARGET_DIR")
.is_some_and(|path| is_rch_target_path(std::path::Path::new(&path)))
|| std::env::current_exe().is_ok_and(|path| is_rch_target_path(path.as_path()))
}
fn is_rch_target_path(path: &std::path::Path) -> bool {
path.to_string_lossy().contains(".rch-target-")
}
fn budget_multiplier_for(
coverage: bool,
shared_worker: bool,
override_value: Option<&str>,
) -> (u128, &'static str) {
if let Some(value) = override_value
.and_then(|value| value.trim().parse::<u128>().ok())
.filter(|value| *value > 0)
{
return (value, "env");
}
if coverage {
(BUDGET_MULTIPLIER_COVERAGE, "coverage")
} else if shared_worker {
(BUDGET_MULTIPLIER_SHARED_WORKER, "shared_worker")
} else {
(BUDGET_MULTIPLIER_DEFAULT, "default")
}
}
fn budget_multiplier_with_mode() -> (u128, &'static str) {
let override_value = std::env::var(BUDGET_MULTIPLIER_ENV).ok();
budget_multiplier_for(
is_coverage_run(),
is_shared_worker_run(),
override_value.as_deref(),
)
}
fn timing_budget_us(base: u128) -> (u128, &'static str, u128) {
let (multiplier, mode) = budget_multiplier_with_mode();
(base.saturating_mul(multiplier), mode, multiplier)
}
#[test]
fn budget_multiplier_defaults_to_local_guardrail() {
assert_eq!(
budget_multiplier_for(false, false, None),
(BUDGET_MULTIPLIER_DEFAULT, "default")
);
}
#[test]
fn budget_multiplier_uses_coverage_guardrail() {
assert_eq!(
budget_multiplier_for(true, true, None),
(BUDGET_MULTIPLIER_COVERAGE, "coverage")
);
}
#[test]
fn budget_multiplier_uses_shared_worker_guardrail() {
assert_eq!(
budget_multiplier_for(false, true, None),
(BUDGET_MULTIPLIER_SHARED_WORKER, "shared_worker")
);
}
#[test]
fn budget_multiplier_explicit_override_wins() {
assert_eq!(budget_multiplier_for(true, true, Some("3")), (3, "env"));
}
#[test]
fn editor_perf_env_flag_parser_accepts_common_forms() {
assert_eq!(parse_env_flag_value(" yes "), Some(true));
assert_eq!(parse_env_flag_value("OFF"), Some(false));
assert_eq!(parse_env_flag_value("worker"), None);
}
#[test]
fn editor_perf_rch_target_path_recognizes_worker_targets() {
assert!(is_rch_target_path(std::path::Path::new(
"/data/projects/frankentui/.rch-target-vmi1152480-job-123/debug/deps/test"
)));
assert!(!is_rch_target_path(std::path::Path::new(
"/data/projects/frankentui/target/debug/deps/test"
)));
}
fn assert_within_budget(duration: Duration, budget_us: u128, case: &str, lines: usize, op: &str) {
let duration_us = duration.as_micros();
let (budget_us, budget_mode, budget_multiplier) = timing_budget_us(budget_us);
let result = if duration_us <= budget_us {
"pass"
} else {
"fail"
};
log_perf(case, lines, op, duration_us, result);
assert!(
duration_us <= budget_us,
"{} on {}K lines took {}us, budget was {}us (mode={}, multiplier={}x)",
op,
lines / 1000,
duration_us,
budget_us,
budget_mode,
budget_multiplier
);
}
#[test]
fn large_buffer_10k_insert_char() {
let mut editor = create_large_buffer(LINES_10K);
let start = Instant::now();
editor.insert_char('X');
let duration = start.elapsed();
assert_within_budget(duration, 1000, "insert_char_10k", LINES_10K, "insert_char");
}
#[test]
fn large_buffer_10k_delete_char() {
let mut editor = create_large_buffer(LINES_10K);
let start = Instant::now();
editor.delete_backward();
let duration = start.elapsed();
assert_within_budget(
duration,
1000,
"delete_char_10k",
LINES_10K,
"delete_backward",
);
}
#[test]
fn large_buffer_10k_move_cursor() {
let mut editor = create_large_buffer(LINES_10K);
let start = Instant::now();
editor.move_left();
let duration = start.elapsed();
assert_within_budget(duration, 1000, "move_cursor_10k", LINES_10K, "move_left");
}
#[test]
fn large_buffer_10k_select_all() {
let mut editor = create_large_buffer(LINES_10K);
let start = Instant::now();
editor.select_all();
let duration = start.elapsed();
assert_within_budget(duration, 1000, "select_all_10k", LINES_10K, "select_all");
}
#[test]
fn large_buffer_10k_move_to_start() {
let mut editor = create_large_buffer(LINES_10K);
let start = Instant::now();
editor.move_to_document_start();
let duration = start.elapsed();
assert_within_budget(duration, 1000, "move_start_10k", LINES_10K, "move_to_start");
}
#[test]
fn large_buffer_10k_move_up_down() {
let mut editor = create_large_buffer(LINES_10K);
editor.move_to_document_start();
for _ in 0..5000 {
editor.move_down();
}
let start = Instant::now();
editor.move_up();
let duration_up = start.elapsed();
let start = Instant::now();
editor.move_down();
let duration_down = start.elapsed();
assert_within_budget(duration_up, 1000, "move_up_10k", LINES_10K, "move_up");
assert_within_budget(duration_down, 1000, "move_down_10k", LINES_10K, "move_down");
}
#[test]
fn large_buffer_10k_undo_redo() {
let mut editor = create_large_buffer(LINES_10K);
editor.insert_char('X');
let start = Instant::now();
editor.undo();
let duration_undo = start.elapsed();
let start = Instant::now();
editor.redo();
let duration_redo = start.elapsed();
assert_within_budget(duration_undo, 1000, "undo_10k", LINES_10K, "undo");
assert_within_budget(duration_redo, 1000, "redo_10k", LINES_10K, "redo");
}
#[test]
fn large_buffer_100k_insert_char() {
let mut editor = create_large_buffer(LINES_100K);
let start = Instant::now();
editor.insert_char('X');
let duration = start.elapsed();
assert_within_budget(
duration,
5000,
"insert_char_100k",
LINES_100K,
"insert_char",
);
}
#[test]
fn large_buffer_100k_delete_char() {
let mut editor = create_large_buffer(LINES_100K);
let start = Instant::now();
editor.delete_backward();
let duration = start.elapsed();
assert_within_budget(
duration,
5000,
"delete_char_100k",
LINES_100K,
"delete_backward",
);
}
#[test]
fn large_buffer_100k_move_cursor() {
let mut editor = create_large_buffer(LINES_100K);
let start = Instant::now();
editor.move_left();
let duration = start.elapsed();
assert_within_budget(duration, 2000, "move_cursor_100k", LINES_100K, "move_left");
}
#[test]
fn large_buffer_100k_select_all() {
let mut editor = create_large_buffer(LINES_100K);
let start = Instant::now();
editor.select_all();
let duration = start.elapsed();
assert_within_budget(duration, 5000, "select_all_100k", LINES_100K, "select_all");
}
#[test]
fn large_buffer_100k_move_to_start() {
let mut editor = create_large_buffer(LINES_100K);
let start = Instant::now();
editor.move_to_document_start();
let duration = start.elapsed();
assert_within_budget(
duration,
2000,
"move_start_100k",
LINES_100K,
"move_to_start",
);
}
#[test]
fn invariant_line_count_consistency() {
let mut editor = create_large_buffer(LINES_10K);
let initial_lines = editor.line_count();
editor.insert_char('X');
assert_eq!(
editor.line_count(),
initial_lines,
"insert_char changed line count"
);
editor.delete_backward();
assert_eq!(
editor.line_count(),
initial_lines,
"delete_backward changed line count"
);
editor.insert_newline();
assert_eq!(
editor.line_count(),
initial_lines + 1,
"insert_newline didn't increase"
);
editor.undo();
assert_eq!(
editor.line_count(),
initial_lines,
"undo didn't restore line count"
);
}
#[test]
fn invariant_cursor_always_valid() {
let mut editor = create_large_buffer(1000);
editor.move_to_document_start();
let start_cursor = editor.cursor();
assert_eq!(start_cursor.line, 0);
assert_eq!(start_cursor.grapheme, 0);
editor.move_to_document_end();
let end_cursor = editor.cursor();
assert!(end_cursor.line < editor.line_count() || editor.is_empty());
for _ in 0..100 {
editor.move_right();
}
let cursor = editor.cursor();
let text = editor.text();
assert!(cursor.line <= text.lines().count());
}
#[test]
fn invariant_undo_reversibility() {
let mut editor = create_large_buffer(1000);
let original_text = editor.text();
let _original_cursor = editor.cursor();
editor.insert_text("INSERTED");
editor.delete_backward();
editor.insert_newline();
assert_ne!(editor.text(), original_text);
editor.undo();
editor.undo();
editor.undo();
assert_eq!(editor.text(), original_text, "undo didn't restore text");
}
#[test]
fn invariant_insert_delete_cycle() {
let mut editor = create_large_buffer(1000);
let original_len = editor.text().len();
for _ in 0..100 {
editor.insert_char('X');
}
assert_eq!(editor.text().len(), original_len + 100);
for _ in 0..100 {
editor.delete_backward();
}
assert_eq!(editor.text().len(), original_len);
}
#[test]
fn stress_many_insertions() {
let mut editor = Editor::new();
let start = Instant::now();
for i in 0..1000 {
editor.insert_text(&format!("Line {i}\n"));
}
let duration = start.elapsed();
assert_eq!(editor.line_count(), 1001); log_perf(
"stress_insertions",
1000,
"1000_inserts",
duration.as_micros(),
"pass",
);
assert!(
duration < Duration::from_secs(1),
"1000 insertions took {:?}",
duration
);
}
#[test]
fn stress_cursor_movements() {
let mut editor = create_large_buffer(10_000);
let start = Instant::now();
for _ in 0..1000 {
editor.move_left();
editor.move_right();
editor.move_up();
editor.move_down();
}
let duration = start.elapsed();
log_perf(
"stress_movements",
10_000,
"4000_moves",
duration.as_micros(),
"pass",
);
assert!(
duration < Duration::from_secs(2),
"4000 cursor movements took {:?}",
duration
);
}
#[test]
fn stress_undo_redo() {
let mut editor = Editor::new();
for i in 0..100 {
editor.insert_text(&format!("Edit {i} "));
}
let start = Instant::now();
for _ in 0..100 {
editor.undo();
}
for _ in 0..100 {
editor.redo();
}
let duration = start.elapsed();
log_perf(
"stress_undo_redo",
100,
"200_undo_redo",
duration.as_micros(),
"pass",
);
assert!(
duration < Duration::from_secs(1),
"200 undo/redo took {:?}",
duration
);
}
#[test]
fn property_text_length_equals_lines() {
let editor = create_large_buffer(1000);
let text = editor.text();
let lines: Vec<&str> = text.lines().collect();
let line_chars: usize = lines.iter().map(|l| l.len()).sum();
let newlines = text.matches('\n').count();
assert_eq!(line_chars + newlines, text.len());
}
#[test]
fn property_line_count_matches_newlines() {
let editor = create_large_buffer(500);
let text = editor.text();
let newlines = text.matches('\n').count();
let expected = if text.ends_with('\n') {
newlines
} else {
newlines + 1
};
let line_count = editor.line_count();
assert!(
line_count == expected || line_count == expected + 1,
"line_count {} didn't match expected {} (newlines={})",
line_count,
expected,
newlines
);
}
#[test]
fn property_cursor_reachable() {
let mut editor = create_large_buffer(100);
editor.move_to_document_start();
for _ in 0..50 {
editor.move_right();
}
let cursor = editor.cursor();
if cursor.line > 0 || cursor.grapheme > 0 {
let line_text = editor.line_text(cursor.line);
assert!(
line_text.is_some(),
"cursor line {} not accessible",
cursor.line
);
}
}
#[test]
fn regression_insert_at_start_large() {
let mut editor = create_large_buffer(10_000);
editor.move_to_document_start();
let start = Instant::now();
editor.insert_text("START: ");
let duration = start.elapsed();
assert!(editor.text().starts_with("START: "));
assert_within_budget(
duration,
5000,
"insert_at_start",
LINES_10K,
"insert_at_start",
);
}
#[test]
fn regression_delete_at_start_large() {
let mut editor = create_large_buffer(10_000);
editor.move_to_document_start();
editor.move_right();
let start = Instant::now();
editor.delete_backward();
let duration = start.elapsed();
assert_within_budget(
duration,
5000,
"delete_at_start",
LINES_10K,
"delete_at_start",
);
}
#[test]
fn regression_word_movement_large() {
let mut editor = create_large_buffer(10_000);
editor.move_to_document_start();
let start = Instant::now();
for _ in 0..100 {
editor.move_word_right();
}
let duration = start.elapsed();
assert_within_budget(
duration,
10_000,
"word_movement",
LINES_10K,
"100_word_moves",
);
}