Files
ligent/tests/test_lineedit.c
T
2026-10-04 13:50:33 +08:00

289 lines
11 KiB
C

/* test_lineedit.c - 行编辑器的缓冲区逻辑与历史记录 (不需要终端) */
#include "lineedit.h"
#include "util.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static int failures = 0;
static int checks = 0;
#define CHECK(cond, ...) \
do { \
checks++; \
if (!(cond)) { \
failures++; \
printf(" ✗ %s:%d: ", __FILE__, __LINE__); \
printf(__VA_ARGS__); \
printf("\n"); \
} \
} while (0)
#define BUFEQ(lb, want) \
CHECK(strcmp((lb).buf, (want)) == 0 && (lb).len == strlen(want), \
"内容应为 [%s], 实际 [%s]", want, (lb).buf)
/* 读一个文本文件的前 n 字节到 buf (用于检查落盘格式) */
static void slurp(const char *path, char *buf, size_t buflen) {
buf[0] = '\0';
FILE *f = fopen(path, "r");
if (!f)
return;
size_t n = fread(buf, 1, buflen - 1, f);
buf[n] = '\0';
fclose(f);
}
static void test_history_file(void) {
puts("输入历史的持久化");
char path[] = "/tmp/ligent_hist_XXXXXX";
int fd = mkstemp(path);
CHECK(fd >= 0, "mkstemp 失败");
if (fd < 0)
return;
close(fd);
unlink(path); /* 故意从"文件不存在"开始 */
line_history_clear();
CHECK(line_history_load(path, 100) == 0, "文件不存在不算错误");
CHECK(line_history_len() == 0, "没有文件时历史应为空");
CHECK(line_history_append(path, "") == 0, "空串不写文件");
CHECK(access(path, F_OK) != 0, "空串不该把文件创建出来");
/* 追加两条, 再读回来 */
CHECK(line_history_add("echo hello") == 1, "第一次应记下");
line_history_append(path, "echo hello");
CHECK(line_history_add("echo hello") == 0, "与上一条重复应被忽略");
CHECK(line_history_add("/think high") == 1, "不同内容应记下");
line_history_append(path, "/think high");
char raw[512];
slurp(path, raw, sizeof raw);
CHECK(strcmp(raw, "echo hello\n/think high\n") == 0,
"普通命令应原样一行, 实际 %s", raw);
line_history_clear();
CHECK(line_history_load(path, 100) == 0, "加载应成功");
CHECK(line_history_len() == 2, "应读回 2 条, 实际 %zu", line_history_len());
CHECK(strcmp(line_history_get(0), "echo hello") == 0, "第 0 条应还原");
CHECK(strcmp(line_history_get(1), "/think high") == 0, "第 1 条应还原");
line_history_clear();
/* 反斜杠和换行必须能原样往返: 一条历史在文件里只占一行 */
unlink(path);
line_history_add("echo C:\\Users\\me");
line_history_append(path, "echo C:\\Users\\me");
line_history_add("第一行\n第二行"); /* 反斜杠续行拼出来的多行输入 */
line_history_append(path, "第一行\n第二行");
line_history_add("中文历史");
line_history_append(path, "中文历史");
slurp(path, raw, sizeof raw);
CHECK(strchr(raw, '\n') != NULL && strstr(raw, "\n\n") == NULL,
"每条只占一行, 实际 %s", raw);
int newlines = 0;
for (const char *p = raw; *p; p++)
if (*p == '\n')
newlines++;
CHECK(newlines == 3, "3 条历史应占 3 行, 实际 %d 行", newlines);
line_history_clear();
line_history_load(path, 100);
CHECK(line_history_len() == 3, "应读回 3 条, 实际 %zu", line_history_len());
CHECK(strcmp(line_history_get(0), "echo C:\\Users\\me") == 0,
"反斜杠应原样还原, 实际 [%s]", line_history_get(0));
CHECK(line_history_get(1) &&
strcmp(line_history_get(1), "第一行\n第二行") == 0,
"多行条目应整条还原, 实际 [%s]",
line_history_get(1) ? line_history_get(1) : "(null)");
CHECK(strcmp(line_history_get(2), "中文历史") == 0, "中文应原样还原");
line_history_clear();
/* 超过上限: 只留最新的 keep 条, 并且文件被就地截短 */
unlink(path);
for (int i = 0; i < 10; i++) {
char *s = xasprintf("cmd-%d", i);
line_history_add(s);
line_history_append(path, s);
free(s);
}
int lines = 0;
slurp(path, raw, sizeof raw);
for (const char *p = raw; *p; p++)
if (*p == '\n')
lines++;
CHECK(lines == 10, "截短前应有 10 行, 实际 %d", lines);
line_history_clear();
line_history_load(path, 4);
CHECK(line_history_len() == 4, "keep=4 时应只剩 4 条, 实际 %zu",
line_history_len());
CHECK(line_history_get(0) && strcmp(line_history_get(0), "cmd-6") == 0,
"应保留最新的 4 条, 第一条应是 cmd-6, 实际 [%s]",
line_history_get(0) ? line_history_get(0) : "(null)");
CHECK(line_history_get(3) && strcmp(line_history_get(3), "cmd-9") == 0,
"最后一条应是 cmd-9");
CHECK(line_history_get(4) == NULL, "第 5 条应不存在");
slurp(path, raw, sizeof raw);
lines = 0;
for (const char *p = raw; *p; p++)
if (*p == '\n')
lines++;
CHECK(lines == 4, "文件应被就地截短成 4 行, 实际 %d", lines);
/* 截短后的文件读回来还是那 4 条 */
line_history_clear();
line_history_load(path, 100);
CHECK(line_history_len() == 4, "截短后应只读到 4 条, 实际 %zu",
line_history_len());
line_history_clear();
unlink(path);
}
int main(void) {
puts("插入与删除");
LineBuf lb;
linebuf_init(&lb);
linebuf_set(&lb, "");
BUFEQ(lb, "");
linebuf_insert(&lb, "hello", 5);
BUFEQ(lb, "hello");
CHECK(lb.pos == 5, "插入后光标应在末尾, 实际 %zu", lb.pos);
/* 在中间插入 */
linebuf_home(&lb);
CHECK(lb.pos == 0, "Home 应回到行首");
linebuf_right(&lb);
linebuf_right(&lb);
linebuf_insert(&lb, "XY", 2);
BUFEQ(lb, "heXYllo");
CHECK(lb.pos == 4, "插入后光标应后移, 实际 %zu", lb.pos);
/* 退格删的是光标前的字符 */
linebuf_backspace(&lb);
BUFEQ(lb, "heXllo");
/* Delete 删的是光标处的字符 */
linebuf_home(&lb);
linebuf_delete(&lb);
BUFEQ(lb, "eXllo");
linebuf_end(&lb);
linebuf_delete(&lb); /* 末尾 Delete 应无动作 */
BUFEQ(lb, "eXllo");
linebuf_free(&lb);
puts("UTF-8: 按字符而非字节移动");
linebuf_init(&lb);
linebuf_set(&lb, "中文abc"); /* 中(3) 文(3) a b c = 9 字节 */
CHECK(lb.len == 9, "应为 9 字节, 实际 %zu", lb.len);
linebuf_left(&lb);
CHECK(lb.pos == 8, "左移一格应为 8, 实际 %zu", lb.pos);
linebuf_left(&lb);
linebuf_left(&lb);
linebuf_left(&lb);
CHECK(lb.pos == 3, "再左移三格应停在第二个汉字之后, 实际 %zu", lb.pos);
linebuf_left(&lb);
linebuf_left(&lb);
CHECK(lb.pos == 0, "应能移到行首, 实际 %zu", lb.pos);
linebuf_left(&lb);
CHECK(lb.pos == 0, "行首再左移不应越界");
/* 退格必须整字删除, 不能留下半个字符 */
linebuf_end(&lb);
linebuf_backspace(&lb);
BUFEQ(lb, "中文ab");
linebuf_backspace(&lb);
BUFEQ(lb, "中文a");
linebuf_backspace(&lb);
BUFEQ(lb, "中文");
linebuf_backspace(&lb);
BUFEQ(lb, "中");
linebuf_backspace(&lb);
BUFEQ(lb, "");
linebuf_backspace(&lb);
BUFEQ(lb, "");
/* 中间删除多字节字符 */
linebuf_set(&lb, "a中b");
linebuf_home(&lb);
linebuf_right(&lb); /* 光标到 'a' 之后 */
linebuf_delete(&lb); /* 删掉 '中' */
BUFEQ(lb, "ab");
CHECK(lb.pos == 1, "删除后光标不动, 实际 %zu", lb.pos);
linebuf_free(&lb);
puts("Home / End 走的是逻辑行, 不是整个缓冲区");
linebuf_init(&lb);
linebuf_set(&lb, "one two three");
linebuf_home(&lb);
CHECK(lb.pos == 0, "单行时 Home 到行首");
linebuf_end(&lb);
CHECK(lb.pos == lb.len, "单行时 End 到行尾");
/* 多行 (粘贴进来的那种): Home/End 只在本行内走 */
linebuf_set(&lb, "aaa\nbbb\nccc"); /* 长度 11 */
CHECK(lb.len == 11, "应为 11 字节, 实际 %zu", lb.len);
linebuf_home(&lb);
CHECK(lb.pos == 8, "末尾按 Home 应到最后一行的行首 (8), 实际 %zu", lb.pos);
linebuf_end(&lb);
CHECK(lb.pos == 11, "End 应到本行行尾 (11), 实际 %zu", lb.pos);
lb.pos = 5; /* 落在 "bbb" 中间 */
linebuf_home(&lb);
CHECK(lb.pos == 4, "中间行按 Home 应到该行行首 (4), 实际 %zu", lb.pos);
linebuf_end(&lb);
CHECK(lb.pos == 7, "中间行按 End 应到该行行尾 (7), 实际 %zu", lb.pos);
lb.pos = 4; /* 正好在行首 */
linebuf_home(&lb);
CHECK(lb.pos == 4, "已在行首时 Home 不应跳到整个缓冲区开头");
lb.pos = 7; /* 正好在行尾 (换行符前) */
linebuf_end(&lb);
CHECK(lb.pos == 7, "已在行尾时 End 不应跳过换行符");
linebuf_set(&lb, "one two three");
lb.pos = 7; /* "one two" 之后 */
linebuf_kill_to_end(&lb);
BUFEQ(lb, "one two");
linebuf_set(&lb, "one two three");
lb.pos = 8; /* "one two " 之后 */
linebuf_kill_to_start(&lb);
BUFEQ(lb, "three");
CHECK(lb.pos == 0, "删到行首后光标应在行首");
linebuf_set(&lb, "one two three");
linebuf_kill_word(&lb); /* 从末尾回删一个词 */
BUFEQ(lb, "one two ");
linebuf_kill_word(&lb); /* 先跳过空格再删词 */
BUFEQ(lb, "one ");
linebuf_free(&lb);
puts("历史记录");
line_history_clear();
CHECK(line_history_len() == 0, "初始应为空");
line_history_add("first");
line_history_add("second");
line_history_add("second"); /* 与上一条重复, 应忽略 */
line_history_add(""); /* 空行不入历史 */
CHECK(line_history_len() == 2, "应有 2 条, 实际 %zu", line_history_len());
CHECK(strcmp(line_history_get(0), "first") == 0, "第 0 条");
CHECK(strcmp(line_history_get(1), "second") == 0, "第 1 条");
CHECK(line_history_get(2) == NULL, "越界应返回 NULL");
line_history_clear();
CHECK(line_history_len() == 0, "清空后应为空");
line_history_add("中文历史");
CHECK(strcmp(line_history_get(0), "中文历史") == 0, "应支持非 ASCII");
line_history_clear();
test_history_file();
printf("\n%d 项检查, %d 项失败\n", checks, failures);
return failures ? 1 : 0;
}