280 lines
11 KiB
C
280 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;
|
|
}
|