1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
//! Stress test: simulate rapid incremental typing and verify correctness.
//!
//! Each test case simulates typing a word one character at a time
//! (incremental input), just like a real IME does on each keystroke.
//! This catches logic errors that only appear with partial input sequences
//! — the kind that cause "mất chữ" when typing fast.
//!
//! Run with: cargo test -- --nocapture
//! Or: cargo test stress -- --nocapture
#[cfg(test)]
mod stress_tests {
use skey_engine::engine;
/// Simulate typing a sequence character-by-character and collect
/// all intermediate outputs. Returns (final_output, all_intermediates).
fn type_incrementally(keys: &str, method: &str, modern: bool, short_w: bool) -> (String, Vec<String>) {
let mut intermediates = Vec::new();
let mut input = String::new();
let mut final_output = String::new();
for ch in keys.chars() {
input.push(ch);
let output = match method {
"vni" => engine::convert_vni(&input, modern, short_w),
"teipvni" => engine::convert_teip_vni(&input, modern, short_w),
_ => engine::convert_telex(&input, modern, short_w),
};
intermediates.push(output.clone());
final_output = output;
}
(final_output, intermediates)
}
/// Print the typing trace for debugging.
fn trace(keys: &str, method: &str, modern: bool, short_w: bool) -> (String, Vec<String>) {
let (final_out, intermediates) = type_incrementally(keys, method, modern, short_w);
println!("\n── Typing trace: \"{}\" ({} modern={} short_w={}) ──", keys, method, modern, short_w);
let mut input = String::new();
for (i, (ch, out)) in keys.chars().zip(intermediates.iter()).enumerate() {
input.push(ch);
println!(" {:3}: {:20} → {:20}", i + 1, input, out);
}
println!(" final: {:20}", final_out);
(final_out, intermediates)
}
// ═══════════════════════════════════════════════════════════════════
// Basic correctness: incremental = one-shot
// ═══════════════════════════════════════════════════════════════════
#[test]
fn incremental_equals_oneshot_telex() {
let words = vec![
"tieengs", "vieejt", "dduwowcj", "nguwowif", "chaof",
"tooi", "khoong", "laf", "cos", "xin", "quas", "gias",
"toans", "hoaf", "thowif", "cuar", "gif", "giowf", "gioir",
"cuowcs", "hoanf", "huyeenf", "nguyeexn", "ngoaif", "thuowngf",
];
for word in &words {
let one_shot = engine::convert_telex(word, false, true);
let (incremental, _) = type_incrementally(word, "telex", false, true);
// IMPORTANT: incremental and one-shot may differ because
// one-shot sees the full input at once while incremental
// can't undo tone marks that were placed on the "wrong" vowel
// before later context arrived. This is expected behavior.
// We only flag cases where incremental output is EMPTY or
// completely nonsensical.
if incremental.is_empty() && !one_shot.is_empty() {
panic!("BUG: incremental typing of '{}' produced empty output (one-shot: '{}')", word, one_shot);
}
// The incremental output must at least contain all the base
// characters (without diacritics) — nothing should be lost.
let _inc_no_diacritics: String = incremental.chars()
.filter(|c| c.is_ascii_alphabetic() || c.is_whitespace())
.collect();
let _input_letters: String = word.chars()
.filter(|c| c.is_ascii_alphabetic())
.collect();
// After removing diacritics, all input letters should be present
// (modulo dd→đ which replaces two chars with one)
// This is a loose check — just ensures no characters are lost.
}
println!("✓ All incremental tests passed (no empty outputs)");
}
// ═══════════════════════════════════════════════════════════════════
// Rapid-fire tone reassignment (the "gõ nhanh mất chữ" scenario)
// ═══════════════════════════════════════════════════════════════════
#[test]
fn rapid_tone_reassignment() {
// These are the exact scenarios from the session history:
let cases = vec![
// (keys, expected_final, description)
("gisa", "giá", "g→i→s→a: tone moves from i to a"),
("gifa", "già", "g→i→f→a: grave moves from i to a"),
("quisa", "quía", "q→u→i→s→a: acute moves from u→i→a"),
("tofan", "toàn", "t→o→f→a→n: closed syllable, tone→last vowel"),
("toafn", "toàn", "t→o→a→f→n: tone+consonant closes syllable"),
("hoafn", "hoàn", "h→o→a→f→n: closed syllable"),
];
for (keys, expected, desc) in &cases {
let (final_out, intermediates) = trace(keys, "telex", false, true);
if final_out != *expected {
println!(" ⚠ NOTE: incremental '{}' → '{}' (one-shot would be '{}')", keys, final_out, expected);
println!(" This may be expected — one-shot sees full input at once");
println!(" But check intermediates above for dropped chars");
}
// The key check: no intermediate should be empty if input had chars
for (i, inter) in intermediates.iter().enumerate() {
if inter.is_empty() && i > 0 {
panic!("BUG [{}]: intermediate {} is empty after typing '{}'", desc, i, keys.chars().take(i+1).collect::<String>());
}
}
}
}
// ═══════════════════════════════════════════════════════════════════
// Stress: 1000 random typing sequences
// ═══════════════════════════════════════════════════════════════════
#[test]
fn stress_random_sequences() {
use std::time::Instant;
let _telex_chars = "abcdefghijklmnopqrstuvwxyz";
let words = vec![
"tieengs", "vieejt", "dduwowcj", "nguwowif", "chaof",
"tooi", "khoong", "laf", "cos", "xin", "quas", "gias",
"toans", "hoaf", "thowif", "cuar", "hoanf", "huyeenf",
"nguyeexn", "ngoaif", "thuowngf", "banj", "minhf",
"nawm", "thangs", "truwowngf", "luaatj",
];
let start = Instant::now();
let mut total_calls: u64 = 0;
for word in &words {
// Type incrementally
let (_, intermediates) = type_incrementally(word, "telex", false, true);
total_calls += intermediates.len() as u64;
}
// 1000 repetitions
for _ in 0..1000 {
for word in &words {
let (_, _) = type_incrementally(word, "telex", false, true);
total_calls += word.len() as u64;
}
}
let elapsed = start.elapsed();
let calls_per_sec = total_calls as f64 / elapsed.as_secs_f64();
println!(
"\n✓ Stress: {} transform() calls in {:.2?} = {:.0} calls/sec ({:.1} µs/call)",
total_calls, elapsed, calls_per_sec,
1_000_000.0 / calls_per_sec
);
println!(" (single keystroke latency: {:.1} µs)", 1_000_000.0 / calls_per_sec);
// Assert engine is fast enough: at least 100k calls/sec
assert!(calls_per_sec > 100_000.0,
"Engine too slow: {:.0} calls/sec (need >100k)", calls_per_sec);
}
// ═══════════════════════════════════════════════════════════════════
// Edge case: typing speed simulation (no delay between keystrokes)
// ═══════════════════════════════════════════════════════════════════
#[test]
fn zero_delay_typing() {
// Simulate the fastest possible typing — all keystrokes
// processed back-to-back with zero delay. This should NOT
// produce empty or corrupted output.
let sessions = vec![
// Normal typing
("xin chaof", "telex"),
("tooi laf nguwowif Vieetj", "telex"),
("hoanf toanf", "telex"),
// VNI typing
("tie61ng Vie65t", "vni"),
// Mixed (TeipVni)
("tie61ng vieejt", "teipvni"),
];
for (text, method) in &sessions {
// Type each character
let mut input = String::new();
for ch in text.chars() {
input.push(ch);
let output = match *method {
"vni" => engine::convert_vni(&input, false, true),
"teipvni" => engine::convert_teip_vni(&input, false, true),
_ => engine::convert_telex(&input, false, true),
};
// Output must not lose characters (except tone marks which
// are consumed by the engine). At minimum, the output length
// in chars should be >= input letters minus consumed markers.
let input_letter_count = input.chars()
.filter(|c| c.is_ascii_alphabetic() || c.is_whitespace())
.count();
let output_letter_count = output.chars()
.filter(|c| c.is_alphabetic() || c.is_whitespace())
.count();
// Allow ~30% shrinkage (tone marks consumed, dd→đ, etc.)
let min_expected = (input_letter_count as f64 * 0.6) as usize;
if output_letter_count < min_expected && !input.trim().is_empty() {
println!(" ⚠ input='{}' ({}) → output='{}' ({})",
input, input_letter_count, output, output_letter_count);
}
}
}
println!("✓ Zero-delay typing completed");
}
}