/* test_lineedit.c - 行编辑器的缓冲区逻辑与历史记录 (不需要终端) */ #include "lineedit.h" #include "util.h" #include #include #include #include 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; }