init
This commit is contained in:
commit
e54390a9f8
85 files changed
+29923
No files matched your search
@@ -0,0 +1,229 @@
|
||||
/* test_spinner.c - 等待动画的两段式标签
|
||||
*
|
||||
* 动画写的是 stderr,所以这里把 stderr 挪到一个临时文件,驱动几轮之后
|
||||
* 再把内容读回来断言。spinner_init(1) 是显式开关,测试里不依赖 isatty。
|
||||
*/
|
||||
#include "spinner.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <time.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)
|
||||
|
||||
/* 把 stderr 接到一个临时文件上,返回文件描述符;*saved 收原来的 stderr。
|
||||
* 用完必须调用 capture_end 还原,否则后面的失败信息也会被吞掉。 */
|
||||
static int capture_begin(char *path, size_t pathlen, int *saved)
|
||||
{
|
||||
snprintf(path, pathlen, "/tmp/ligent_spin_XXXXXX");
|
||||
int fd = mkstemp(path);
|
||||
if (fd < 0) return -1;
|
||||
*saved = dup(STDERR_FILENO);
|
||||
if (*saved < 0) { close(fd); return -1; }
|
||||
dup2(fd, STDERR_FILENO);
|
||||
return fd;
|
||||
}
|
||||
|
||||
static void capture_end(int fd, int saved, char *buf, size_t buflen)
|
||||
{
|
||||
fflush(stderr);
|
||||
dup2(saved, STDERR_FILENO);
|
||||
close(saved);
|
||||
lseek(fd, 0, SEEK_SET);
|
||||
ssize_t n = read(fd, buf, buflen - 1);
|
||||
buf[n > 0 ? (size_t)n : 0] = '\0';
|
||||
close(fd);
|
||||
}
|
||||
|
||||
static void test_two_phase(void)
|
||||
{
|
||||
puts("两段式标签:等待响应 → 思考中");
|
||||
char path[64];
|
||||
int saved = -1;
|
||||
int fd = capture_begin(path, sizeof path, &saved);
|
||||
CHECK(fd >= 0, "无法重定向 stderr");
|
||||
if (fd < 0) return;
|
||||
|
||||
spinner_init(1);
|
||||
spinner_start("等待响应");
|
||||
usleep(400 * 1000);
|
||||
/* 每来一片思维链都会调一次:第一次生效,后面几次都是同一标签 */
|
||||
spinner_set_label("思考中");
|
||||
usleep(120 * 1000);
|
||||
spinner_set_label("思考中");
|
||||
usleep(120 * 1000);
|
||||
spinner_set_label("思考中");
|
||||
usleep(200 * 1000);
|
||||
spinner_stop();
|
||||
|
||||
char buf[8192];
|
||||
capture_end(fd, saved, buf, sizeof buf);
|
||||
unlink(path);
|
||||
|
||||
const char *a = strstr(buf, "等待响应");
|
||||
const char *b = strstr(buf, "思考中");
|
||||
CHECK(a != NULL, "应打印\"等待响应\",实际 [%.120s]", buf);
|
||||
CHECK(b != NULL, "应打印\"思考中\",实际 [%.120s]", buf);
|
||||
CHECK(a && b && a < b, "先\"等待响应\"后\"思考中\",顺序反了");
|
||||
CHECK(strstr(buf, "\r\033[K") != NULL, "每帧应先回车清行再打印");
|
||||
/* 停在"思考中"之后就不该再有"等待响应"冒出来 */
|
||||
if (b) CHECK(strstr(b, "等待响应") == NULL, "换标签后不应回退到\"等待响应\"");
|
||||
}
|
||||
|
||||
static void test_idempotent(void)
|
||||
{
|
||||
puts("未开始/未启用时的行为");
|
||||
char path[64];
|
||||
int saved = -1;
|
||||
int fd = capture_begin(path, sizeof path, &saved);
|
||||
CHECK(fd >= 0, "无法重定向 stderr");
|
||||
if (fd < 0) return;
|
||||
|
||||
/* 没 start 就 set:只记标签,不该打印任何东西,也不该崩 */
|
||||
spinner_init(1);
|
||||
spinner_set_label("思考中");
|
||||
usleep(50 * 1000);
|
||||
/* 没 start 就 stop:幂等 */
|
||||
spinner_stop();
|
||||
/* start 两次:第二次忽略,不该起第二个线程 */
|
||||
spinner_start("等待响应");
|
||||
spinner_start("思考中");
|
||||
usleep(300 * 1000);
|
||||
spinner_stop();
|
||||
spinner_stop();
|
||||
|
||||
char buf[8192];
|
||||
capture_end(fd, saved, buf, sizeof buf);
|
||||
unlink(path);
|
||||
CHECK(strstr(buf, "等待响应") != NULL, "start 后应开始转");
|
||||
CHECK(strstr(buf, "思考中") == NULL,
|
||||
"已在转时再次 start 应被忽略,标签不该被改掉");
|
||||
|
||||
/* enabled = 0 时全部是空操作:连重定向都不需要 */
|
||||
spinner_init(0);
|
||||
spinner_start("等待响应");
|
||||
spinner_set_label("思考中");
|
||||
spinner_stop();
|
||||
CHECK(1, "关闭状态下调用不崩溃");
|
||||
}
|
||||
|
||||
static double now_sec(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
|
||||
}
|
||||
|
||||
/* spinner_stop 要 pthread_join worker。如果 worker 是 nanosleep 一整帧,
|
||||
* 这里就得干等最多 250ms —— 而这笔账会加在每一轮模型往返、每一次工具
|
||||
* 来回的用户感知延迟上。 */
|
||||
static void test_stop_is_prompt(void)
|
||||
{
|
||||
puts("stop 不该等满一帧(250ms)");
|
||||
char path[64];
|
||||
int saved = -1;
|
||||
int fd = capture_begin(path, sizeof path, &saved);
|
||||
CHECK(fd >= 0, "无法重定向 stderr");
|
||||
if (fd < 0) return;
|
||||
|
||||
spinner_init(1);
|
||||
double worst = 0.0;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
double t0 = now_sec();
|
||||
spinner_start("等待响应");
|
||||
spinner_stop();
|
||||
double dt = now_sec() - t0;
|
||||
if (dt > worst) worst = dt;
|
||||
}
|
||||
|
||||
char buf[4096];
|
||||
capture_end(fd, saved, buf, sizeof buf);
|
||||
unlink(path);
|
||||
CHECK(worst < 0.10, "stop 应立即返回,最慢一次 %.3fs(帧间隔是 0.25s)", worst);
|
||||
}
|
||||
|
||||
/* 帧率必须受 250ms 限制。曾经踩过一次:pthread_cond_timedwait 默认按
|
||||
* CLOCK_REALTIME 解释超时,而传进去的是 CLOCK_MONOTONIC 的绝对时间
|
||||
* (开机时长永远小于 epoch 时间),于是超时永远"已经过了" —— worker
|
||||
* 变成疯狂重绘的忙循环,把 pty 刷屏。 */
|
||||
static void test_frame_rate_is_bounded(void)
|
||||
{
|
||||
puts("帧率受 250ms 限制,不能狂刷");
|
||||
char path[64];
|
||||
int saved = -1;
|
||||
int fd = capture_begin(path, sizeof path, &saved);
|
||||
CHECK(fd >= 0, "无法重定向 stderr");
|
||||
if (fd < 0) return;
|
||||
|
||||
spinner_init(1);
|
||||
spinner_start("等待响应");
|
||||
usleep(600 * 1000); /* 0.6 秒 ≈ 2~3 帧 */
|
||||
spinner_stop();
|
||||
|
||||
char buf[65536];
|
||||
capture_end(fd, saved, buf, sizeof buf);
|
||||
unlink(path);
|
||||
|
||||
int frames = 0;
|
||||
for (const char *p = buf; (p = strstr(p, "等待响应")); p++) frames++;
|
||||
CHECK(frames >= 1, "至少该画一帧,实际 %d", frames);
|
||||
CHECK(frames <= 5, "0.6 秒内不该超过 5 帧,实际 %d(忙循环?)", frames);
|
||||
}
|
||||
|
||||
/* spinner_notice 会先停、打印一条提示、再**接着转**。恢复时它传的是 g_label
|
||||
* 自己 —— 而 spinner_start 曾经先执行 g_label[0] = '\0' 再读那个 label,于是
|
||||
* 标签被自己清空、退化成默认的"思考中"。症状:每次 HTTP 重试退避之后,屏幕上
|
||||
* 的"等待响应"都会变成"思考中",而这两段等待的含义恰恰是它要区分的。 */
|
||||
static void test_notice_keeps_label(void)
|
||||
{
|
||||
puts("notice 之后标签保持不变");
|
||||
char path[64];
|
||||
int saved = -1;
|
||||
int fd = capture_begin(path, sizeof path, &saved);
|
||||
CHECK(fd >= 0, "无法重定向 stderr");
|
||||
if (fd < 0) return;
|
||||
|
||||
spinner_init(1);
|
||||
spinner_start("自定义标签A");
|
||||
usleep(300 * 1000); /* 让它画至少一帧 */
|
||||
spinner_notice("一条插进来的提示");
|
||||
usleep(300 * 1000);
|
||||
spinner_stop();
|
||||
|
||||
char buf[8192];
|
||||
capture_end(fd, saved, buf, sizeof buf);
|
||||
unlink(path);
|
||||
|
||||
const char *noticed = strstr(buf, "一条插进来的提示");
|
||||
CHECK(noticed != NULL, "提示本身应该打出来,实际 [%.200s]", buf);
|
||||
/* 提示之后剩下的帧里必须还是原来那个标签 */
|
||||
if (noticed)
|
||||
CHECK(strstr(noticed, "自定义标签A") != NULL,
|
||||
"notice 之后标签被改写了(应保持\"自定义标签A\"):\n%.300s",
|
||||
noticed);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_two_phase();
|
||||
test_idempotent();
|
||||
test_stop_is_prompt();
|
||||
test_frame_rate_is_bounded();
|
||||
test_notice_keeps_label();
|
||||
printf("\n%d 项检查,%d 项失败\n", checks, failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
Reference in new issue
Block a user