use opentui::unicode::{
WidthMethod, display_width, display_width_char, grapheme_indices, grapheme_info, graphemes,
is_ascii_only,
};
use opentui_rust as opentui;
use proptest::prelude::*;
fn utf8_string() -> impl Strategy<Value = String> {
"\\PC{0,100}"
}
fn ascii_string() -> impl Strategy<Value = String> {
"[\\x20-\\x7E]{0,100}"
}
fn cjk_string() -> impl Strategy<Value = String> {
prop::collection::vec(
prop::sample::select(vec!['中', '文', '日', '本', '語', '한', '국']),
0..50,
)
.prop_map(|chars| chars.into_iter().collect::<String>())
}
fn emoji_string() -> impl Strategy<Value = String> {
prop::collection::vec(
prop::sample::select(vec!["😀", "🎉", "👍", "❤️", "🇺🇸", "👨👩👧👦", "é", "ñ", "ü"]),
0..20,
)
.prop_map(|parts| parts.join(""))
}
proptest! {
#[test]
fn grapheme_join_is_lossless(s in utf8_string()) {
let joined: String = graphemes(&s).collect();
prop_assert_eq!(&joined, &s, "grapheme join should reproduce original");
}
#[test]
fn grapheme_clusters_are_nonempty(s in utf8_string()) {
for g in graphemes(&s) {
prop_assert!(!g.is_empty(), "grapheme cluster should not be empty");
}
}
#[test]
fn grapheme_indices_are_valid(s in utf8_string()) {
for (idx, g) in grapheme_indices(&s) {
prop_assert!(idx < s.len() || s.is_empty(),
"grapheme index {} out of bounds for string of len {}", idx, s.len());
prop_assert!(s.is_char_boundary(idx),
"grapheme index {} is not a char boundary", idx);
prop_assert_eq!(&s[idx..idx + g.len()], g);
}
}
#[test]
fn grapheme_info_count_matches_graphemes(s in utf8_string()) {
let count = graphemes(&s).count();
let infos = grapheme_info(&s, 4, WidthMethod::WcWidth);
prop_assert_eq!(infos.len(), count,
"grapheme_info should produce one entry per grapheme");
}
#[test]
fn grapheme_info_offsets_monotonic(s in utf8_string()) {
let infos = grapheme_info(&s, 4, WidthMethod::WcWidth);
for i in 1..infos.len() {
prop_assert!(infos[i].byte_offset > infos[i - 1].byte_offset,
"byte offsets should be strictly increasing");
}
}
#[test]
fn grapheme_info_col_offsets_non_decreasing(s in utf8_string()) {
let infos = grapheme_info(&s, 4, WidthMethod::WcWidth);
for i in 1..infos.len() {
prop_assert!(infos[i].col_offset >= infos[i - 1].col_offset,
"col offsets should be non-decreasing");
}
}
}
proptest! {
#[test]
fn display_width_deterministic(s in utf8_string()) {
let w1 = display_width(&s);
let w2 = display_width(&s);
prop_assert_eq!(w1, w2, "display_width should be deterministic");
}
#[test]
fn display_width_non_negative(s in utf8_string()) {
let _w = display_width(&s); }
#[test]
fn display_width_empty_is_zero(_dummy in Just(())) {
prop_assert_eq!(display_width(""), 0);
}
#[test]
fn display_width_ascii_printable(c in 0x20u8..=0x7Eu8) {
let ch = c as char;
prop_assert_eq!(display_width_char(ch), 1,
"ASCII printable char {:?} should have width 1", ch);
}
#[test]
fn display_width_bounded(s in utf8_string()) {
let w = display_width(&s);
let char_count = s.chars().count();
prop_assert!(w <= char_count * 2,
"width {} exceeds 2 * char_count {}", w, char_count * 2);
}
#[test]
fn display_width_ascii_equals_len(s in ascii_string()) {
let w = display_width(&s);
prop_assert_eq!(w, s.len(),
"ASCII string width should equal byte length");
}
#[test]
fn display_width_cjk_chars(s in cjk_string()) {
let w = display_width(&s);
let char_count = s.chars().count();
prop_assert_eq!(w, char_count * 2,
"CJK string width should be 2 * char_count");
}
}
proptest! {
#[test]
fn is_ascii_only_matches_stdlib(s in utf8_string()) {
prop_assert_eq!(is_ascii_only(&s), s.is_ascii(),
"is_ascii_only should match str::is_ascii");
}
#[test]
fn ascii_strings_are_ascii(s in ascii_string()) {
prop_assert!(is_ascii_only(&s));
}
}
proptest! {
#[test]
fn emoji_graphemes_are_lossless(s in emoji_string()) {
let joined: String = graphemes(&s).collect();
prop_assert_eq!(&joined, &s);
}
#[test]
fn emoji_display_width_positive(s in emoji_string()) {
if !s.is_empty() {
let w = display_width(&s);
prop_assert!(w > 0, "non-empty emoji string should have positive width");
}
}
}
proptest! {
#[test]
fn both_width_methods_non_negative(s in utf8_string()) {
use opentui::unicode::display_width_with_method;
let w1 = display_width_with_method(&s, WidthMethod::WcWidth);
let w2 = display_width_with_method(&s, WidthMethod::Unicode);
prop_assert!(w2 >= w1, "Unicode method width {} vs WcWidth {}", w2, w1);
}
#[test]
fn width_methods_agree_on_ascii(s in ascii_string()) {
use opentui::unicode::display_width_with_method;
let w1 = display_width_with_method(&s, WidthMethod::WcWidth);
let w2 = display_width_with_method(&s, WidthMethod::Unicode);
prop_assert_eq!(w1, w2,
"Width methods should agree on ASCII strings");
}
}
proptest! {
#[test]
fn line_break_positions_valid(s in utf8_string()) {
use opentui::unicode::find_line_breaks;
let result = find_line_breaks(&s);
for &pos in &result.positions {
prop_assert!(pos < s.len(),
"line break position {} out of bounds for len {}", pos, s.len());
prop_assert!(s.is_char_boundary(pos),
"line break position {} is not a char boundary", pos);
}
}
#[test]
fn line_break_positions_sorted(s in utf8_string()) {
use opentui::unicode::find_line_breaks;
let result = find_line_breaks(&s);
for i in 1..result.positions.len() {
prop_assert!(result.positions[i] > result.positions[i - 1],
"line break positions should be strictly increasing");
}
}
#[test]
fn line_break_positions_lengths_match(s in utf8_string()) {
use opentui::unicode::find_line_breaks;
let result = find_line_breaks(&s);
prop_assert_eq!(result.positions.len(), result.lengths.len(),
"positions and lengths should have same count");
}
}
proptest! {
#[test]
fn wrap_position_empty_is_none(max_cols in 1u32..200) {
use opentui::unicode::find_wrap_position;
prop_assert!(find_wrap_position("", max_cols, 4).is_none());
}
#[test]
fn wrap_position_is_valid_boundary(s in "[a-zA-Z ]{1,80}", max_cols in 1u32..40) {
use opentui::unicode::find_wrap_position;
if let Some(pos) = find_wrap_position(&s, max_cols, 4) {
prop_assert!(pos <= s.len(),
"wrap position {} exceeds string len {}", pos, s.len());
prop_assert!(s.is_char_boundary(pos),
"wrap position {} is not a char boundary", pos);
}
}
#[test]
fn wrap_position_within_string(s in "[a-zA-Z ]{1,80}", max_cols in 5u32..40) {
use opentui::unicode::find_wrap_position;
if let Some(pos) = find_wrap_position(&s, max_cols, 4) {
prop_assert!(pos <= s.len(),
"wrap position {} exceeds string len {}", pos, s.len());
prop_assert!(pos > 0,
"wrap position should be > 0 when wrapping is needed");
}
}
#[test]
fn no_wrap_when_fits(s in "[a-z]{0,10}", max_cols in 20u32..100) {
use opentui::unicode::find_wrap_position;
let w = display_width(&s);
if let Ok(w) = u32::try_from(w) {
if w <= max_cols {
prop_assert!(
find_wrap_position(&s, max_cols, 4).is_none(),
"text of width {} should fit in {} columns",
w,
max_cols
);
}
}
}
}
proptest! {
#[test]
fn tab_stop_positions_are_tabs(s in utf8_string()) {
use opentui::unicode::find_tab_stops;
let result = find_tab_stops(&s);
for &pos in &result.positions {
prop_assert!(pos < s.len(),
"tab position {} out of bounds for len {}", pos, s.len());
prop_assert_eq!(s.as_bytes()[pos], b'\t',
"byte at tab position {} should be a tab", pos);
}
}
}
proptest! {
#[test]
fn text_width_deterministic(s in utf8_string()) {
use opentui::unicode::calculate_text_width;
let w1 = calculate_text_width(&s, 4);
let w2 = calculate_text_width(&s, 4);
prop_assert_eq!(w1, w2);
}
#[test]
fn text_width_empty_is_zero(tab_width in 1u8..16) {
use opentui::unicode::calculate_text_width;
prop_assert_eq!(calculate_text_width("", tab_width), 0);
}
}