图库: M5 设置页(阅读方向/PIN/主题状态)与启动 PIN 锁屏;阅读方向持久化(评审 P1)
logic/pin(纯 C):加盐迭代哈希(FNV-1a ×1000 轮 + 每次设置新随机盐,16 位 hex)经
meow_config_* 持久化 pin.enabled/pin.hash/pin.salt;提供 set/verify/change/disable 与
4-8 位纯数字校验;enabled=1 但盐/哈希残缺时 fail-closed;落盘半途失败会还原旧盐,不留
「新盐+旧哈希」的双失效状态。
ui/settings(ACT_SETTINGS):MD3 分组表单(meow_listview 容器圆角卡 + 段头标签)——
阅读方向 RTL/LTR 走 mr_reader_set_direction;启动 PIN 开关与修改(开启/修改均两遍
MEOW_IME_NUM 确认);版本与主题状态行(跟随系统 + 当前明暗,随 app.c 主题 listener
整帧重绘)。控件一律在 on_enter 建(栈顶=ACT_SETTINGS),避免 §6.1 owner 绑到
ACT_LIBRARY 的跨页命中泄漏。
ui/lock(ACT_LOCK):lock_view_should_show() 读 logic/pin;启用时启动先入锁屏,验证前
不绘制任何书库内容;「输入 PIN」按钮弹系统数字键盘(MEOW_IME_NUM),错 PIN 就地提示
重试(无惩罚性锁定),正确则 page_pop 回书库;on_event 消费 SWIPE——左缘返回不能绕过
PIN 或退出 App(meow_view_dispatch_event 先于 app.c 的 on_swipe)。
logic/reader(评审 P1 修复,最小持久化):mr_reader_set_direction 成功切换后 best-effort
写 meow_config("reader.dir","rtl"/"ltr"),写失败只打 WARN 不阻断本次切换;
mr_reader_get_direction 首次调用惰性读一次配置恢复(无配置/非法值 → 默认 RTL),之后
纯内存快路径。宿主测试经 -DMR_READER_TEST_HOOKS 的 mr_reader_test_reset() 模拟重启。
测试/构建:test_pin(85 项,PIN 全分支,含单键半途落盘失败);test_settings_reader_persist
(19 项:默认/落盘/重启恢复/坏值/非法值/只读一次/写失败只告警);可选宿主集成
test_settings_lock_host(真实 SDK view/activity/widget + IME 注入,端到端验证错/对 PIN
解锁与设置页两遍开启;SDK 无源码时 SKIP);run_tests.sh 保持 46/46 并纳入上述测试
(reader.c 的配置/日志依赖由 test/settings_reader_stub.c 存根),build_tuku.sh 加入
logic/pin.c。交叉构建静态 MIPS ELF(zig-mipsel)零警告。
安全口径:PIN 防随手翻看,不是安全边界——TF 卡可拔出直读、配置可改;存储不使用加密库,
锁屏与设置页文案均如实标注该局限。
This commit is contained in:
1 parent
47325f2b81
commit
cca3906c80
12 files changed
+1749
-60
No files matched your search
+46
-5
@@ -1,13 +1,54 @@
|
||||
#!/bin/bash
|
||||
# run_tests.sh — 宿主 gcc 构建并运行图库 logic 层测试(不链 SDK、不需要设备)。
|
||||
# run_tests.sh — 宿主 gcc 构建并运行图库测试(不链 SDK 静态库、不需要设备)。
|
||||
# - test_library:书库扫描/自然排序/阅读方向(46 项)
|
||||
# - test_settings_reader_persist:阅读方向 meow_config 持久化(默认/落盘/重启恢复/
|
||||
# 坏值/非法值/只读一次/写失败只告警)
|
||||
# - test_pin:启动 PIN 格式/设置/校验/修改/禁用/残缺配置/落盘失败(logic/pin.c 为纯 C,
|
||||
# 只引用 meow_config_* 声明,测试内以内存 KV 存根顶替 → 不链 SDK)。
|
||||
# - test_settings_lock_host(可选):设置页/锁屏与 SDK view/activity/widget 接线的宿主集成
|
||||
# 验证(owner 归属 + 锁屏 SWIPE 拦截)。需要 SDK **源码树**(不只 meow_ui.a);
|
||||
# 交付环境只有库+头时自动 SKIP。
|
||||
# reader.c 的方向持久化只 include 配置/日志两个模块头;宿主测试用 test/settings_reader_stub.c
|
||||
# 顶替 meow_config_* 与 meow_log(所以下面各条都带 -I"$SDK" 与该存根)。
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
gcc -Wall -Wextra -O1 -I. \
|
||||
test/test_library.c logic/library.c logic/reader.c \
|
||||
SDK="${MEOW_SDK_DIR:-$HOME/Proj/MeowOS-SDK/sdk}"
|
||||
|
||||
gcc -Wall -Wextra -O1 -I. -I"$SDK" \
|
||||
test/test_library.c logic/library.c logic/reader.c test/settings_reader_stub.c \
|
||||
-o test/test_library
|
||||
./test/test_library
|
||||
gcc -Wall -Wextra -O1 -I. \
|
||||
|
||||
gcc -Wall -Wextra -O1 -I. -I"$SDK" \
|
||||
test/test_reader_core.c logic/library.c logic/reader.c \
|
||||
logic/reader/page_source.c logic/reader/nav.c \
|
||||
logic/reader/page_source.c logic/reader/nav.c test/settings_reader_stub.c \
|
||||
-o test/test_reader_core
|
||||
./test/test_reader_core
|
||||
|
||||
gcc -Wall -Wextra -O1 -DMR_READER_TEST_HOOKS -I. -I"$SDK" \
|
||||
test/test_settings_reader_persist.c logic/reader.c test/settings_reader_stub.c \
|
||||
-o test/test_settings_reader_persist
|
||||
./test/test_settings_reader_persist
|
||||
|
||||
gcc -Wall -Wextra -O1 -I. -I"$SDK" \
|
||||
test/test_pin.c logic/pin.c \
|
||||
-o test/test_pin
|
||||
./test/test_pin
|
||||
|
||||
if [ -f "$SDK/meow_ui_view.c" ] && [ -f "$SDK/meow_ui_config.c" ]; then
|
||||
gcc -Wall -Wextra -O1 -DMEOW_UI_HOST_TEST -I. -I"$SDK" -I"$SDK/include" \
|
||||
test/test_settings_lock_host.c ui/settings/settings.c ui/lock/lock.c \
|
||||
logic/pin.c logic/reader.c \
|
||||
"$SDK"/meow_ui_gfx.c "$SDK"/meow_ui_sys.c "$SDK"/meow_ui_theme.c \
|
||||
"$SDK"/meow_ui_vol.c "$SDK"/meow_ui_log.c "$SDK"/meow_ui_widget.c \
|
||||
"$SDK"/meow_ui_input.c "$SDK"/meow_ui_activity.c "$SDK"/meow_ui_dialog.c \
|
||||
"$SDK"/meow_ui_ime.c "$SDK"/meow_ui_str.c "$SDK"/meow_pinyin.c \
|
||||
"$SDK"/meow_ui_draw.c "$SDK"/meow_ui_view.c "$SDK"/meow_ui_box.c \
|
||||
"$SDK"/meow_ui_anim.c "$SDK"/meow_ui_scroll.c "$SDK"/meow_ui_img.c \
|
||||
"$SDK"/meow_ui_media.c "$SDK"/meow_notify_ipc.c "$SDK"/meow_ui_config.c \
|
||||
"$SDK"/meow_ui_nav.c "$SDK"/stb_image.c "$SDK"/test_host_stub.c \
|
||||
-pthread -lm -o test/test_settings_lock_host
|
||||
./test/test_settings_lock_host
|
||||
else
|
||||
echo "SKIP test_settings_lock_host: SDK 源码树不可用(只发布 meow_ui.a 的环境)"
|
||||
fi
|
||||
@@ -0,0 +1,92 @@
|
||||
/* settings_reader_stub.c — 宿主测试用 meow_config_* 与 meow_log 最小实现(内存 KV)。
|
||||
*
|
||||
* 只为让 logic/reader.c 在不链 SDK 的宿主测试里可编译/可断言:
|
||||
* - meow_config_get/set:8 个键的内存表,可注入写失败(模拟无卡/只读);
|
||||
* - meow_log:只统计 WARN 次数(验证「写失败只告警、不阻断切换」)。
|
||||
* 与 test_pin.c 自带存根同思路,但抽成共用文件供多个 reader 相关测试链接。
|
||||
*/
|
||||
#include "settings_reader_stub.h"
|
||||
#include "include/meow_ui_config.h"
|
||||
#include "include/meow_ui_log.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#define ST_MAX 8
|
||||
static struct { char key[64]; char val[64]; int used; } g_kv[ST_MAX];
|
||||
static int g_fail_writes;
|
||||
static int g_warns;
|
||||
|
||||
static int kv_find(const char *key)
|
||||
{
|
||||
for (int i = 0; i < ST_MAX; i++)
|
||||
if (g_kv[i].used && strcmp(g_kv[i].key, key) == 0) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
void mr_test_cfg_clear(void)
|
||||
{
|
||||
memset(g_kv, 0, sizeof g_kv);
|
||||
g_fail_writes = 0;
|
||||
}
|
||||
|
||||
void mr_test_cfg_put(const char *key, const char *val)
|
||||
{
|
||||
if (!key || !key[0]) return;
|
||||
int i = kv_find(key);
|
||||
if (i < 0) {
|
||||
for (i = 0; i < ST_MAX; i++) if (!g_kv[i].used) break;
|
||||
if (i >= ST_MAX) return;
|
||||
g_kv[i].used = 1;
|
||||
snprintf(g_kv[i].key, sizeof g_kv[i].key, "%s", key);
|
||||
}
|
||||
snprintf(g_kv[i].val, sizeof g_kv[i].val, "%s", val ? val : "");
|
||||
}
|
||||
|
||||
int mr_test_cfg_get(const char *key, char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return 0;
|
||||
out[0] = '\0';
|
||||
int i = kv_find(key);
|
||||
if (i < 0) return 0;
|
||||
snprintf(out, (size_t)outsz, "%s", g_kv[i].val);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void mr_test_cfg_fail_writes(int on)
|
||||
{
|
||||
g_fail_writes = on ? 1 : 0;
|
||||
}
|
||||
|
||||
int mr_test_log_warns(void)
|
||||
{
|
||||
return g_warns;
|
||||
}
|
||||
|
||||
void mr_test_log_clear(void)
|
||||
{
|
||||
g_warns = 0;
|
||||
}
|
||||
|
||||
/* ---- 被测代码调用的 SDK 接口 ---- */
|
||||
int meow_config_set(const char *key, const char *val)
|
||||
{
|
||||
if (!key || !key[0]) return MEOW_CFG_ERR_INVAL;
|
||||
if (g_fail_writes) return MEOW_CFG_ERR_IO;
|
||||
mr_test_cfg_put(key, val);
|
||||
return MEOW_CFG_OK;
|
||||
}
|
||||
|
||||
int meow_config_get(const char *key, char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return MEOW_CFG_ERR_INVAL;
|
||||
out[0] = '\0';
|
||||
if (!mr_test_cfg_get(key, out, outsz)) return MEOW_CFG_ERR_NOENT;
|
||||
return MEOW_CFG_OK;
|
||||
}
|
||||
|
||||
void meow_log(meow_log_level lvl, const char *fmt, ...)
|
||||
{
|
||||
(void)fmt;
|
||||
if (lvl >= MEOW_LOG_WARN) g_warns++;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/* settings_reader_stub.h — 宿主测试用 meow_config_* 与 meow_log 存根(logic/reader.c 的依赖面)。
|
||||
*
|
||||
* 内存 KV(不落盘)+ 测试辅助:读/写/清配置、注入写失败、统计 WARN 次数。
|
||||
* 供 test_library、test_reader_core、test_settings_reader_persist 共用——
|
||||
* 这几个测试都不链 SDK,reader.c 的持久化失败/成功路径靠这里可控。
|
||||
*/
|
||||
#ifndef SETTINGS_READER_STUB_H
|
||||
#define SETTINGS_READER_STUB_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
void mr_test_cfg_clear(void); /* 清空全部键 */
|
||||
void mr_test_cfg_put(const char *key, const char *val); /* 直接写入(可注入坏值) */
|
||||
int mr_test_cfg_get(const char *key, char *out, int outsz); /* 1=键存在 */
|
||||
void mr_test_cfg_fail_writes(int on); /* 1=后续 set 全部失败 */
|
||||
int mr_test_log_warns(void); /* meow_log(WARN..) 次数 */
|
||||
void mr_test_log_clear(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* SETTINGS_READER_STUB_H */
|
||||
@@ -0,0 +1,320 @@
|
||||
/* test_pin.c — 启动 PIN(logic/pin)宿主测试:纯 C,不链 SDK。
|
||||
*
|
||||
* 用内存 KV 存根顶替 meow_config_get/set/int/remove(与 SDK 语义一致:
|
||||
* 未设 → MEOW_CFG_ERR_NOENT;写失败可由 g_fail_write 注入)。
|
||||
* 覆盖:格式校验边界(4/8 位合法;3/9 位、空串、NULL、符号、全角数字非法)、
|
||||
* 设置/校验、错 PIN、重设换盐、change 全分支、disable、配置残缺 fail-closed、
|
||||
* 落盘失败、落盘布局(pin.enabled/hash/salt)。
|
||||
*
|
||||
* 编译(在 tuku/ 下):
|
||||
* gcc -Wall -Wextra -O1 -I. -I"$SDK" test/test_pin.c logic/pin.c -o test/test_pin
|
||||
* 或直接 test/run_tests.sh。
|
||||
*/
|
||||
#include "logic/pin.h"
|
||||
#include "include/meow_ui_config.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define KV_MAX 8
|
||||
static struct { char key[64]; char val[128]; int used; } g_kv[KV_MAX];
|
||||
static int g_fail_write; /* 1=模拟全部落盘失败 */
|
||||
static const char *g_fail_key; /* 非 NULL=只让该键写失败(模拟半途失败) */
|
||||
|
||||
static int kv_find(const char *key)
|
||||
{
|
||||
for (int i = 0; i < KV_MAX; i++)
|
||||
if (g_kv[i].used && strcmp(g_kv[i].key, key) == 0) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void kv_clear(void)
|
||||
{
|
||||
memset(g_kv, 0, sizeof g_kv);
|
||||
g_fail_write = 0;
|
||||
g_fail_key = NULL;
|
||||
}
|
||||
|
||||
int meow_config_set(const char *key, const char *val)
|
||||
{
|
||||
if (!key || !key[0]) return MEOW_CFG_ERR_INVAL;
|
||||
if (g_fail_write || (g_fail_key && strcmp(g_fail_key, key) == 0)) return MEOW_CFG_ERR_IO;
|
||||
int i = kv_find(key);
|
||||
if (i < 0) {
|
||||
for (i = 0; i < KV_MAX; i++) if (!g_kv[i].used) break;
|
||||
if (i >= KV_MAX) return MEOW_CFG_ERR_IO;
|
||||
g_kv[i].used = 1;
|
||||
snprintf(g_kv[i].key, sizeof g_kv[i].key, "%s", key);
|
||||
}
|
||||
snprintf(g_kv[i].val, sizeof g_kv[i].val, "%s", val ? val : "");
|
||||
return MEOW_CFG_OK;
|
||||
}
|
||||
|
||||
int meow_config_get(const char *key, char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return MEOW_CFG_ERR_INVAL;
|
||||
out[0] = '\0';
|
||||
int i = kv_find(key);
|
||||
if (i < 0) return MEOW_CFG_ERR_NOENT;
|
||||
snprintf(out, (size_t)outsz, "%s", g_kv[i].val);
|
||||
return MEOW_CFG_OK;
|
||||
}
|
||||
|
||||
int meow_config_int(const char *key, int def)
|
||||
{
|
||||
char buf[32];
|
||||
char *end = NULL;
|
||||
long v;
|
||||
if (meow_config_get(key, buf, sizeof buf) != MEOW_CFG_OK) return def;
|
||||
v = strtol(buf, &end, 10);
|
||||
if (end == buf) return def;
|
||||
return (int)v;
|
||||
}
|
||||
|
||||
int meow_config_bool(const char *key, int def)
|
||||
{
|
||||
return meow_config_int(key, def) != 0;
|
||||
}
|
||||
|
||||
void meow_config_remove(const char *key)
|
||||
{
|
||||
int i = kv_find(key);
|
||||
if (i >= 0) g_kv[i].used = 0;
|
||||
}
|
||||
|
||||
const char *meow_config_dir(void) { return "/tmp/meowcfg_test"; }
|
||||
|
||||
static int g_pass, g_fail;
|
||||
#define CHECK(cond, ...) do { \
|
||||
if (cond) { g_pass++; } \
|
||||
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
|
||||
} while (0)
|
||||
|
||||
static int is_hex16(const char *s)
|
||||
{
|
||||
if (!s) return 0;
|
||||
if (strlen(s) != 16) return 0;
|
||||
for (int i = 0; i < 16; i++)
|
||||
if (!((s[i] >= '0' && s[i] <= '9') || (s[i] >= 'a' && s[i] <= 'f'))) return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void test_format(void)
|
||||
{
|
||||
CHECK(mr_pin_valid(NULL) == 0, "NULL invalid");
|
||||
CHECK(mr_pin_valid("") == 0, "empty invalid");
|
||||
CHECK(mr_pin_valid("123") == 0, "3 digits invalid");
|
||||
CHECK(mr_pin_valid("1234") == 1, "4 digits valid (min)");
|
||||
CHECK(mr_pin_valid("12345") == 1, "5 digits valid");
|
||||
CHECK(mr_pin_valid("12345678") == 1, "8 digits valid (max)");
|
||||
CHECK(mr_pin_valid("123456789") == 0, "9 digits invalid");
|
||||
CHECK(mr_pin_valid("12a4") == 0, "letter invalid");
|
||||
CHECK(mr_pin_valid("12 4") == 0, "space invalid");
|
||||
CHECK(mr_pin_valid(" 1234") == 0, "leading space invalid");
|
||||
CHECK(mr_pin_valid("1234 ") == 0, "trailing space invalid");
|
||||
CHECK(mr_pin_valid("-123") == 0, "sign invalid");
|
||||
/* 全角数字(UTF-8 EF BC 9x)不是 ASCII 数字 */
|
||||
CHECK(mr_pin_valid("\xEF\xBC\x91\xEF\xBC\x92\xEF\xBC\x93\xEF\xBC\x94") == 0,
|
||||
"fullwidth digits invalid");
|
||||
}
|
||||
|
||||
static void test_default_state(void)
|
||||
{
|
||||
kv_clear();
|
||||
CHECK(mr_pin_enabled() == 0, "default disabled");
|
||||
CHECK(mr_pin_verify("1234") == 0, "verify on disabled = 0");
|
||||
CHECK(mr_pin_set("123") == MR_PIN_ERR_FORMAT, "set short -> format error");
|
||||
CHECK(mr_pin_enabled() == 0, "failed set keeps disabled");
|
||||
}
|
||||
|
||||
static void test_set_verify(void)
|
||||
{
|
||||
kv_clear();
|
||||
CHECK(mr_pin_set("1234") == MR_PIN_OK, "set 1234 ok");
|
||||
CHECK(mr_pin_enabled() == 1, "enabled after set");
|
||||
CHECK(mr_pin_verify("1234") == 1, "verify correct");
|
||||
CHECK(mr_pin_verify("4321") == 0, "verify wrong");
|
||||
CHECK(mr_pin_verify("123") == 0, "verify short");
|
||||
CHECK(mr_pin_verify("12345") == 0, "verify longer");
|
||||
CHECK(mr_pin_verify("") == 0, "verify empty");
|
||||
CHECK(mr_pin_verify(NULL) == 0, "verify NULL");
|
||||
CHECK(mr_pin_verify("12a4") == 0, "verify non-digit");
|
||||
|
||||
char salt[32] = "", hash[32] = "";
|
||||
CHECK(mr_pin_get_salt(salt, sizeof salt) == 1, "salt present");
|
||||
CHECK(mr_pin_get_hash(hash, sizeof hash) == 1, "hash present");
|
||||
CHECK(is_hex16(salt), "salt is 16 hex (got %s)", salt);
|
||||
CHECK(is_hex16(hash), "hash is 16 hex (got %s)", hash);
|
||||
CHECK(strcmp(hash, "1234") != 0, "hash is not the raw pin");
|
||||
|
||||
/* 落盘布局:enabled="1"(明文 PIN 不落盘:配置里只有 16 位 hex 摘要与盐) */
|
||||
char buf[64] = "";
|
||||
CHECK(meow_config_get("pin.enabled", buf, sizeof buf) == MEOW_CFG_OK && strcmp(buf, "1") == 0,
|
||||
"pin.enabled=1 on disk");
|
||||
}
|
||||
|
||||
static void test_reset_new_salt(void)
|
||||
{
|
||||
kv_clear();
|
||||
CHECK(mr_pin_set("1234") == MR_PIN_OK, "set first");
|
||||
char salt1[32] = "", hash1[32] = "";
|
||||
mr_pin_get_salt(salt1, sizeof salt1);
|
||||
mr_pin_get_hash(hash1, sizeof hash1);
|
||||
|
||||
CHECK(mr_pin_set("5678") == MR_PIN_OK, "set second (overwrite)");
|
||||
char salt2[32] = "", hash2[32] = "";
|
||||
mr_pin_get_salt(salt2, sizeof salt2);
|
||||
mr_pin_get_hash(hash2, sizeof hash2);
|
||||
CHECK(strcmp(salt1, salt2) != 0, "new salt on each set");
|
||||
CHECK(strcmp(hash1, hash2) != 0, "new hash on each set");
|
||||
CHECK(mr_pin_verify("5678") == 1, "new pin verifies");
|
||||
CHECK(mr_pin_verify("1234") == 0, "old pin rejected after overwrite");
|
||||
|
||||
/* 同一 PIN 重设:盐不同 → 摘要也不同(防跨设备彩虹表/同值比对)。 */
|
||||
CHECK(mr_pin_set("5678") == MR_PIN_OK, "re-set same pin");
|
||||
char salt3[32] = "", hash3[32] = "";
|
||||
mr_pin_get_salt(salt3, sizeof salt3);
|
||||
mr_pin_get_hash(hash3, sizeof hash3);
|
||||
CHECK(strcmp(salt2, salt3) != 0, "salt differs for same pin");
|
||||
CHECK(strcmp(hash2, hash3) != 0, "hash differs for same pin");
|
||||
CHECK(mr_pin_verify("5678") == 1, "same pin still verifies");
|
||||
}
|
||||
|
||||
static void test_change(void)
|
||||
{
|
||||
kv_clear();
|
||||
CHECK(mr_pin_change("1234", "5678") == MR_PIN_ERR_STATE, "change while disabled -> state");
|
||||
|
||||
CHECK(mr_pin_set("1234") == MR_PIN_OK, "setup 1234");
|
||||
CHECK(mr_pin_change("9999", "5678") == MR_PIN_ERR_WRONG, "change wrong old -> wrong");
|
||||
CHECK(mr_pin_verify("1234") == 1, "old still valid after wrong-change");
|
||||
CHECK(mr_pin_verify("5678") == 0, "new not applied after wrong-change");
|
||||
|
||||
CHECK(mr_pin_change("1234", "12") == MR_PIN_ERR_FORMAT, "change to short new -> format");
|
||||
CHECK(mr_pin_verify("1234") == 1, "old still valid after bad-new");
|
||||
|
||||
CHECK(mr_pin_change("1234", "12345678") == MR_PIN_OK, "change ok (8 digits)");
|
||||
CHECK(mr_pin_verify("12345678") == 1, "new valid after change");
|
||||
CHECK(mr_pin_verify("1234") == 0, "old invalid after change");
|
||||
CHECK(mr_pin_enabled() == 1, "still enabled after change");
|
||||
|
||||
CHECK(mr_pin_change(NULL, "0000") == MR_PIN_ERR_WRONG, "change NULL old -> wrong");
|
||||
CHECK(mr_pin_change("12345678", NULL) == MR_PIN_ERR_FORMAT, "change NULL new -> format");
|
||||
}
|
||||
|
||||
static void test_disable(void)
|
||||
{
|
||||
kv_clear();
|
||||
CHECK(mr_pin_disable() == MR_PIN_OK, "disable while disabled is idempotent");
|
||||
CHECK(mr_pin_set("1234") == MR_PIN_OK, "setup for disable");
|
||||
CHECK(mr_pin_disable() == MR_PIN_OK, "disable ok");
|
||||
CHECK(mr_pin_enabled() == 0, "disabled after disable");
|
||||
CHECK(mr_pin_verify("1234") == 0, "verify fails after disable");
|
||||
|
||||
char buf[32] = "";
|
||||
CHECK(meow_config_get("pin.hash", buf, sizeof buf) == MEOW_CFG_ERR_NOENT, "hash key removed");
|
||||
CHECK(meow_config_get("pin.salt", buf, sizeof buf) == MEOW_CFG_ERR_NOENT, "salt key removed");
|
||||
CHECK(meow_config_get("pin.enabled", buf, sizeof buf) == MEOW_CFG_OK && strcmp(buf, "0") == 0,
|
||||
"enabled=0 on disk");
|
||||
|
||||
/* 禁用后可重新开启(新盐)。 */
|
||||
CHECK(mr_pin_set("8765") == MR_PIN_OK, "re-enable after disable");
|
||||
CHECK(mr_pin_verify("8765") == 1, "re-enabled pin verifies");
|
||||
}
|
||||
|
||||
static void test_corrupt_state(void)
|
||||
{
|
||||
/* enabled=1 但缺 hash → fail-closed */
|
||||
kv_clear();
|
||||
meow_config_set("pin.enabled", "1");
|
||||
meow_config_set("pin.salt", "0011223344556677");
|
||||
CHECK(mr_pin_enabled() == 0, "enabled without hash -> fail closed");
|
||||
CHECK(mr_pin_verify("1234") == 0, "verify fail closed");
|
||||
|
||||
/* hash 长度/字符非法 */
|
||||
kv_clear();
|
||||
meow_config_set("pin.enabled", "1");
|
||||
meow_config_set("pin.salt", "0011223344556677");
|
||||
meow_config_set("pin.hash", "zz");
|
||||
CHECK(mr_pin_enabled() == 0, "garbage hash -> fail closed");
|
||||
|
||||
/* salt 非法 */
|
||||
kv_clear();
|
||||
meow_config_set("pin.enabled", "1");
|
||||
meow_config_set("pin.salt", "");
|
||||
meow_config_set("pin.hash", "0011223344556677");
|
||||
CHECK(mr_pin_enabled() == 0, "empty salt -> fail closed");
|
||||
|
||||
/* change 在残缺状态 → state 错误(不误判为已启用) */
|
||||
CHECK(mr_pin_change("1234", "5678") == MR_PIN_ERR_STATE, "change on corrupt -> state");
|
||||
}
|
||||
|
||||
static void test_store_failure(void)
|
||||
{
|
||||
kv_clear();
|
||||
g_fail_write = 1;
|
||||
CHECK(mr_pin_set("1234") == MR_PIN_ERR_STORE, "set with failing store -> store error");
|
||||
CHECK(mr_pin_enabled() == 0, "still disabled after store failure");
|
||||
g_fail_write = 0;
|
||||
|
||||
/* 写盘失败时不得把状态改成「已启用」——set 先写盐/摘要、最后写 enabled。 */
|
||||
CHECK(mr_pin_set("1234") == MR_PIN_OK, "set ok after store recovers");
|
||||
g_fail_write = 1;
|
||||
CHECK(mr_pin_disable() == MR_PIN_ERR_STORE, "disable with failing store -> store error");
|
||||
g_fail_write = 0;
|
||||
}
|
||||
|
||||
static void test_partial_write(void)
|
||||
{
|
||||
/* 摘要写失败:把盐还原成旧值,已启用的旧 PIN 仍可校验(不会新旧都验不过)。 */
|
||||
kv_clear();
|
||||
CHECK(mr_pin_set("1234") == MR_PIN_OK, "setup 1234");
|
||||
g_fail_key = "pin.hash";
|
||||
CHECK(mr_pin_set("5678") == MR_PIN_ERR_STORE, "hash write failure -> store error");
|
||||
CHECK(mr_pin_enabled() == 1, "still enabled after partial write");
|
||||
CHECK(mr_pin_verify("1234") == 1, "old PIN still valid after partial write");
|
||||
CHECK(mr_pin_verify("5678") == 0, "new PIN not applied after partial write");
|
||||
g_fail_key = NULL;
|
||||
|
||||
/* 重设时 enabled 已是 "1":该键写失败不影响一致性 → 按成功返回。 */
|
||||
g_fail_key = "pin.enabled";
|
||||
CHECK(mr_pin_set("4321") == MR_PIN_OK, "enabled-already-1 write failure is consistent");
|
||||
CHECK(mr_pin_verify("4321") == 1, "new PIN active");
|
||||
g_fail_key = NULL;
|
||||
|
||||
/* 首次开启时 enabled 写失败 → 未启用(fail-closed)。 */
|
||||
kv_clear();
|
||||
g_fail_key = "pin.enabled";
|
||||
CHECK(mr_pin_set("1111") == MR_PIN_ERR_STORE, "first enable with enabled-write failure");
|
||||
CHECK(mr_pin_enabled() == 0, "not enabled after enabled-write failure");
|
||||
g_fail_key = NULL;
|
||||
}
|
||||
|
||||
static void test_verify_does_not_mutate(void)
|
||||
{
|
||||
kv_clear();
|
||||
CHECK(mr_pin_set("2468") == MR_PIN_OK, "setup");
|
||||
char salt1[32] = "", salt2[32] = "";
|
||||
mr_pin_get_salt(salt1, sizeof salt1);
|
||||
(void)mr_pin_verify("0000");
|
||||
(void)mr_pin_verify("2468");
|
||||
mr_pin_get_salt(salt2, sizeof salt2);
|
||||
CHECK(strcmp(salt1, salt2) == 0, "verify does not change salt");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_format();
|
||||
test_default_state();
|
||||
test_set_verify();
|
||||
test_reset_new_salt();
|
||||
test_change();
|
||||
test_disable();
|
||||
test_corrupt_state();
|
||||
test_store_failure();
|
||||
test_partial_write();
|
||||
test_verify_does_not_mutate();
|
||||
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
/* test_settings_lock_host.c — 设置页/锁屏与 SDK 骨架的宿主集成测试(可选,需 SDK 源码树)。
|
||||
*
|
||||
* 目的:钉住 M3 骨架评审点名的两条坑(M5 桩注释①/②),这两条只有把真实 SDK
|
||||
* view/activity/widget 链起来才能验证:
|
||||
* ① 非根页控件必须在 on_enter 建(此时 activity 栈顶=本页)——否则 owner 绑到
|
||||
* ACT_LIBRARY,跨页命中泄漏;
|
||||
* ② 锁屏必须在自身 view 的 on_event 消费 SWIPE(meow_view_dispatch_event 先于 app.c
|
||||
* 的 on_swipe),否则左缘返回会把 ACT_LOCK page_pop 掉、PIN 被绕过。
|
||||
* 另外覆盖:设置页回到书库(topbar 返回)、控件跨进出复用不重建、阅读方向经
|
||||
* mr_reader_set_direction 切换、lock_view_should_show() 随 PIN 状态变化、
|
||||
* 主题模式切换后整帧重绘不崩,以及**真实 MEOW_IME_NUM 键盘**驱动的两条端到端路径:
|
||||
* 锁屏错 PIN(留在锁屏)→ 对 PIN(page_pop 回书库)、设置页两遍输入开启 PIN。
|
||||
*
|
||||
* 编译:见 test/run_tests.sh —— 它把本测试、settings/lock/pin 源码与 SDK 源码
|
||||
* (-DMEOW_UI_HOST_TEST,附 test_host_stub.c 打桩 font/global)一起编;SDK 对象清单在脚本里。
|
||||
* 只发布 meow_ui.a(无 SDK 源码)的交付环境,run_tests.sh 自动 SKIP 本测试。
|
||||
*/
|
||||
#include "tuku_internal.h"
|
||||
#include "logic/pin.h"
|
||||
#include "meow_ui_internal.h" /* meow_widget_owner(宿主测试专用内部头) */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
|
||||
static int g_fail;
|
||||
#define CHECK(cond, ...) do { \
|
||||
if (cond) { printf(" PASS\n"); } \
|
||||
else { g_fail++; printf(" FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
|
||||
} while (0)
|
||||
|
||||
/* app.c 的导航上报在测试里不需要网络侧实现。 */
|
||||
void mr_nav_report(void) {}
|
||||
|
||||
static int g_lib_enters;
|
||||
static void lib_enter(meow_view *v, void *c) { (void)v; (void)c; g_lib_enters++; }
|
||||
static void lib_leave(meow_view *v, void *c) { (void)v; (void)c; }
|
||||
|
||||
/* 清掉本次测试写入的三个小配置并删临时目录(meow_config 每键一文件)。 */
|
||||
static void cleanup_cfg(const char *dir)
|
||||
{
|
||||
static const char *keys[3] = { "pin.enabled.cfg", "pin.hash.cfg", "pin.salt.cfg" };
|
||||
char p[512];
|
||||
for (int i = 0; i < 3; i++) {
|
||||
snprintf(p, sizeof p, "%s/%s", dir, keys[i]);
|
||||
unlink(p);
|
||||
}
|
||||
rmdir(dir);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 数字键盘注入(宿主没有真实触摸):按 MEOW_IME_NUM 键位投递 TAP,关闭键盘后由被测代码校验。
|
||||
* NUM 布局(sdk/meow_ui_ime.c build_kb):KB_TOP=420;首行 470 是符号行 "-/.:*#+=",
|
||||
* 数字九键行为 518/566/614(x0=70/190/310,键宽 100),底行(ESC|0|退格+OK)为 662;
|
||||
* 点 y<420 的背景=关闭键盘。 */
|
||||
/* ============================================================ */
|
||||
typedef struct { const char *digits; int passes; } pin_job;
|
||||
|
||||
static void key_tap(char d)
|
||||
{
|
||||
int row, col;
|
||||
if (d == '0') { row = 3; col = 1; }
|
||||
else {
|
||||
int v = d - '1';
|
||||
if (v < 0 || v > 8) return;
|
||||
row = v / 3;
|
||||
col = v % 3;
|
||||
}
|
||||
/* 数字行 y=518/566/614(首行 470 是符号行 "-/.:*#+="),底行 662;列心 120/240/360。 */
|
||||
meow_input_inject(MEOW_EV_TAP, 70 + col * 120 + 50, 518 + row * 48 + 18);
|
||||
}
|
||||
|
||||
static void *pin_injector(void *arg)
|
||||
{
|
||||
pin_job *job = (pin_job *)arg;
|
||||
usleep(150000); /* 等 text_input 开键盘并 drain 完残留事件 */
|
||||
for (int p = 0; p < job->passes; p++) {
|
||||
if (p) usleep(300000); /* 两遍输入之间等第二遍开键盘 */
|
||||
for (const char *s = job->digits; *s; s++) {
|
||||
key_tap(*s);
|
||||
usleep(50000);
|
||||
}
|
||||
usleep(50000);
|
||||
meow_input_inject(MEOW_EV_TAP, 240, 100); /* y<420:关闭键盘(非空→校验) */
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void lock_try_pin(const char *digits)
|
||||
{
|
||||
int sb = meow_ui_statusbar_height();
|
||||
int cy = sb + (meow_ui_height() - sb) / 2;
|
||||
pin_job job = { digits, 1 };
|
||||
pthread_t th;
|
||||
if (pthread_create(&th, NULL, pin_injector, &job) != 0) return;
|
||||
meow_view_dispatch_event(MEOW_EV_TAP, 240, cy + 40); /* 点「输入 PIN」按钮 */
|
||||
pthread_join(th, NULL);
|
||||
}
|
||||
|
||||
static void settings_tap_pin_row(void){
|
||||
/* 布局:sb + 56 顶栏 + 6 + 段头34 + 方向行64 + 12 + 段头34 → PIN 列表首行中心 +32 */
|
||||
int y = meow_ui_statusbar_height() + 56 + 6 + 34 + 64 + 12 + 34 + 32;
|
||||
pin_job job = { "2468", 2 };
|
||||
pthread_t th;
|
||||
if (pthread_create(&th, NULL, pin_injector, &job) != 0) return;
|
||||
meow_view_dispatch_event(MEOW_EV_TAP, 200, y);
|
||||
pthread_join(th, NULL);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const char *base = getenv("TMPDIR");
|
||||
if (!base || !base[0]) base = ".";
|
||||
char tmpl[512];
|
||||
snprintf(tmpl, sizeof tmpl, "%s/mr_settings_host_XXXXXX", base);
|
||||
char *data = mkdtemp(tmpl);
|
||||
if (!data) { printf("SKIP: mkdtemp failed\n"); return 0; }
|
||||
setenv("MEOW_DATA_DIR", data, 1);
|
||||
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
if (meow_ui_init(&o) != 0) { printf("SKIP: meow_ui_init failed (no host backend)\n"); return 0; }
|
||||
|
||||
static const meow_view_hooks lh = { .on_enter = lib_enter, .on_leave = lib_leave };
|
||||
meow_view *lib = meow_view_create(ACT_LIBRARY, MEOW_VIEW_ACTIVITY, &lh, NULL);
|
||||
meow_view *settings = settings_view_create();
|
||||
meow_view *lock = lock_view_create();
|
||||
int sb = meow_ui_statusbar_height();
|
||||
|
||||
meow_view_page_push(ACT_LIBRARY);
|
||||
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "start at library");
|
||||
|
||||
/* ---- 设置页:on_enter 建控件 + owner 归属 ---- */
|
||||
meow_view_page_push(ACT_SETTINGS);
|
||||
CHECK(meow_ui_activity_active() == ACT_SETTINGS, "settings active");
|
||||
CHECK(settings->nwidgets == 5, "settings built 5 widgets (got %d)", settings->nwidgets);
|
||||
int owner_ok = 1;
|
||||
for (int i = 0; i < settings->nwidgets; i++)
|
||||
if (meow_widget_owner(settings->widgets[i]) != ACT_SETTINGS) owner_ok = 0;
|
||||
CHECK(owner_ok, "settings widgets owned by ACT_SETTINGS (no cross-page leak)");
|
||||
|
||||
/* 布局几何:顶栏贴状态栏、三组列表不越界、页脚在屏内。 */
|
||||
int r_tb[4] = { 0 }, r_about[4] = { 0 };
|
||||
meow_widget_rect(settings->widgets[0], r_tb);
|
||||
meow_widget_rect(settings->widgets[3], r_about);
|
||||
CHECK(r_tb[1] == sb && r_tb[3] == 56, "topappbar sits below statusbar");
|
||||
CHECK(r_about[1] + r_about[3] < meow_ui_height() - 60,
|
||||
"about list leaves room for footer (bottom=%d)", r_about[1] + r_about[3]);
|
||||
printf(" geometry: topbar y=%d, about list y=%d h=%d (screen %dx%d)\n",
|
||||
r_tb[1], r_about[1], r_about[3], meow_ui_width(), meow_ui_height());
|
||||
|
||||
/* 阅读方向行:段头 34 + 行中心。 */
|
||||
int dir_y = sb + 56 + 6 + 34 + 32;
|
||||
int d0 = mr_reader_get_direction();
|
||||
CHECK(meow_view_dispatch_event(MEOW_EV_TAP, 120, dir_y) == 1,
|
||||
"tap on direction row consumed by settings on_event");
|
||||
CHECK(mr_reader_get_direction() != d0,
|
||||
"direction toggled through mr_reader_set_direction");
|
||||
|
||||
/* topbar 返回 → page_pop 回书库 */
|
||||
CHECK(meow_view_dispatch_event(MEOW_EV_TAP, 22, sb + 28) == 1, "back tap consumed");
|
||||
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "back popped to library");
|
||||
CHECK(g_lib_enters == 2, "library re-entered after pop (enters=%d)", g_lib_enters);
|
||||
|
||||
/* 再次进入:控件复用(不在 on_enter 重复创建/泄漏) */
|
||||
meow_view_page_push(ACT_SETTINGS);
|
||||
CHECK(settings->nwidgets == 5, "settings widgets reused (got %d)", settings->nwidgets);
|
||||
CHECK(meow_ui_activity_active() == ACT_SETTINGS, "settings active again");
|
||||
|
||||
/* 设置页不消费 SWIPE:交给 app.c 的 page_pop */
|
||||
CHECK(meow_view_dispatch_event(MEOW_EV_SWIPE, MEOW_SWIPE_LEFT, 200) == 0,
|
||||
"settings leaves SWIPE to app.c (back = pop)");
|
||||
meow_view_page_pop(); /* 手动模拟 app.c 的返回行为,回到书库 */
|
||||
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "manual pop back to library");
|
||||
|
||||
/* ---- 锁屏:PIN 状态驱动 + SWIPE 拦截(坑②) ---- */
|
||||
mr_pin_disable();
|
||||
CHECK(lock_view_should_show() == 0, "lock hidden when PIN disabled");
|
||||
CHECK(mr_pin_set("1357") == MR_PIN_OK, "enable PIN");
|
||||
CHECK(lock_view_should_show() == 1, "lock shown when PIN enabled");
|
||||
|
||||
meow_view_page_push(ACT_LOCK);
|
||||
CHECK(meow_ui_activity_active() == ACT_LOCK, "lock active");
|
||||
CHECK(lock->nwidgets == 1, "lock built its button on enter (got %d)", lock->nwidgets);
|
||||
CHECK(meow_widget_owner(lock->widgets[0]) == ACT_LOCK, "lock button owned by ACT_LOCK");
|
||||
|
||||
CHECK(meow_view_dispatch_event(MEOW_EV_SWIPE, MEOW_SWIPE_LEFT, 240) == 1,
|
||||
"lock consumes SWIPE (left-edge back cannot bypass PIN)");
|
||||
CHECK(meow_ui_activity_active() == ACT_LOCK, "still locked after swipe");
|
||||
CHECK(meow_view_dispatch_event(MEOW_EV_TAP, 5, 400) == 1, "lock consumes stray taps");
|
||||
CHECK(meow_ui_activity_active() == ACT_LOCK, "still locked after stray tap");
|
||||
|
||||
CHECK(mr_pin_verify("1357") == 1, "stored PIN verifies");
|
||||
CHECK(mr_pin_verify("1358") == 0, "wrong PIN rejected");
|
||||
|
||||
/* ---- 锁屏真解锁路径:点按钮 → MEOW_IME_NUM 输入 → 校验 → pop(错误则留在锁屏) ---- */
|
||||
lock_try_pin("1358"); /* 错 PIN:留在锁屏 */
|
||||
CHECK(meow_ui_activity_active() == ACT_LOCK, "wrong PIN keeps lock (retry allowed)");
|
||||
lock_try_pin("1357"); /* 正确:page_pop 回书库 */
|
||||
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "correct PIN pops lock to library");
|
||||
|
||||
/* ---- 设置页两遍输入开启 PIN(MEOW_IME_NUM) ---- */
|
||||
mr_pin_disable();
|
||||
meow_view_page_push(ACT_SETTINGS);
|
||||
CHECK(meow_ui_activity_active() == ACT_SETTINGS, "settings active for PIN enable");
|
||||
settings_tap_pin_row();
|
||||
CHECK(mr_pin_enabled() == 1, "two-pass entry enabled the PIN");
|
||||
CHECK(mr_pin_verify("2468") == 1, "PIN entered in settings verifies");
|
||||
meow_view_page_pop();
|
||||
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "back from settings to library");
|
||||
|
||||
/* 主题切换:app.c listener 整帧 invalidate;页面按 meow_theme_get() 重画不崩。 */
|
||||
meow_theme_set_mode(MEOW_THEME_LIGHT);
|
||||
meow_ui_invalidate(0, 0, meow_ui_width(), meow_ui_height());
|
||||
meow_theme_set_mode(MEOW_THEME_DARK);
|
||||
meow_ui_invalidate(0, 0, meow_ui_width(), meow_ui_height());
|
||||
CHECK(1, "theme switch repaint does not crash");
|
||||
|
||||
mr_pin_disable();
|
||||
meow_view_free(lock);
|
||||
meow_view_free(settings);
|
||||
meow_view_free(lib);
|
||||
meow_ui_shutdown();
|
||||
cleanup_cfg(data);
|
||||
printf("%s: %s\n", g_fail ? "FAIL" : "PASS",
|
||||
g_fail ? "see failures above" : "integration checks passed");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/* test_settings_reader_persist.c — 阅读方向持久化宿主测试(M5 评审 P1 修复)。
|
||||
*
|
||||
* 被测:logic/reader.c 的 meow_config("reader.dir") 持久化。
|
||||
* 「新进程语义」用 reader.c 的测试钩子 mr_reader_test_reset()(-DMR_READER_TEST_HOOKS)
|
||||
* 模拟:清掉静态恢复标记,配置存根保留——等价于重启后首次 get 从配置恢复。
|
||||
*
|
||||
* 覆盖:
|
||||
* T1 无配置默认 RTL,且 get 不写配置;
|
||||
* T2 set LTR 落盘 + 内存立即生效;T3 reset 后从配置恢复 LTR;
|
||||
* T4 set RTL 落盘 + reset 恢复 RTL;
|
||||
* T5 坏值("sideways")忽略 → 默认 RTL,且 get 不回写坏值;
|
||||
* T6 非法方向(99)忽略且不落盘;
|
||||
* T7 坏值后 set 合法方向会覆盖配置;
|
||||
* T8 配置只读一次(首次 get 后配置再变不影响本次内存态 = 快路径);
|
||||
* T9 写失败:切换仍生效(内存)、只打一条 WARN、配置未变。
|
||||
*
|
||||
* 编译(tuku/ 下,SDK 仅需公共头):
|
||||
* gcc -Wall -Wextra -O1 -DMR_READER_TEST_HOOKS -I. -I"$SDK" \
|
||||
* test/test_settings_reader_persist.c logic/reader.c test/settings_reader_stub.c \
|
||||
* -o test/test_settings_reader_persist && ./test/test_settings_reader_persist
|
||||
*/
|
||||
#include "logic/reader.h"
|
||||
#include "test/settings_reader_stub.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
/* reader.c 在 -DMR_READER_TEST_HOOKS 下导出(正式构建无此符号,故不写进 reader.h,
|
||||
* 避免生产头暴露测试入口)。 */
|
||||
extern void mr_reader_test_reset(void);
|
||||
|
||||
static int g_pass, g_fail;
|
||||
#define CHECK(cond, ...) do { \
|
||||
if (cond) { g_pass++; } \
|
||||
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
|
||||
} while (0)
|
||||
|
||||
static int cfg_is(const char *want)
|
||||
{
|
||||
char buf[32] = "";
|
||||
if (!mr_test_cfg_get("reader.dir", buf, (int)sizeof buf)) return 0;
|
||||
return strcmp(buf, want) == 0;
|
||||
}
|
||||
|
||||
static int cfg_has(const char *key)
|
||||
{
|
||||
char buf[32];
|
||||
return mr_test_cfg_get(key, buf, (int)sizeof buf);
|
||||
}
|
||||
|
||||
static void test_default_and_ltr(void)
|
||||
{
|
||||
/* T1 无配置 → 默认 RTL;get 是只读的。 */
|
||||
mr_test_cfg_clear();
|
||||
mr_reader_test_reset();
|
||||
CHECK(mr_reader_get_direction() == MR_DIR_RTL, "T1 default RTL with no config");
|
||||
CHECK(!cfg_has("reader.dir"), "T1 get does not write config");
|
||||
|
||||
/* T2 set LTR:内存立即生效 + 落盘 "ltr"。 */
|
||||
mr_reader_set_direction(MR_DIR_LTR);
|
||||
CHECK(mr_reader_get_direction() == MR_DIR_LTR, "T2 memory updated immediately");
|
||||
CHECK(cfg_is("ltr"), "T2 set LTR persists reader.dir=ltr");
|
||||
|
||||
/* T3 模拟重启 → 从配置恢复 LTR。 */
|
||||
mr_reader_test_reset();
|
||||
CHECK(mr_reader_get_direction() == MR_DIR_LTR, "T3 restore LTR after restart");
|
||||
|
||||
/* T4 反向:set RTL 落盘并恢复。 */
|
||||
mr_reader_set_direction(MR_DIR_RTL);
|
||||
CHECK(cfg_is("rtl"), "T4 set RTL persists reader.dir=rtl");
|
||||
mr_reader_test_reset();
|
||||
CHECK(mr_reader_get_direction() == MR_DIR_RTL, "T4 restore RTL after restart");
|
||||
}
|
||||
|
||||
static void test_bad_values(void)
|
||||
{
|
||||
/* T5 坏值忽略 → 默认 RTL,且 get 不改写坏值。 */
|
||||
mr_test_cfg_clear();
|
||||
mr_test_cfg_put("reader.dir", "sideways");
|
||||
mr_reader_test_reset();
|
||||
CHECK(mr_reader_get_direction() == MR_DIR_RTL, "T5 bad value ignored -> default RTL");
|
||||
CHECK(cfg_is("sideways"), "T5 get leaves bad value untouched");
|
||||
|
||||
/* T6 非法方向:忽略且不落盘。 */
|
||||
mr_test_cfg_clear();
|
||||
mr_reader_test_reset();
|
||||
mr_reader_set_direction(99);
|
||||
CHECK(mr_reader_get_direction() == MR_DIR_RTL, "T6 invalid dir ignored (memory)");
|
||||
CHECK(!cfg_has("reader.dir"), "T6 invalid dir writes nothing");
|
||||
|
||||
/* T7 坏值后 set 合法方向 → 覆盖配置并在重启后生效。 */
|
||||
mr_test_cfg_clear();
|
||||
mr_test_cfg_put("reader.dir", "bogus");
|
||||
mr_reader_test_reset();
|
||||
mr_reader_set_direction(MR_DIR_LTR);
|
||||
CHECK(cfg_is("ltr"), "T7 valid set overwrites bad config");
|
||||
mr_reader_test_reset();
|
||||
CHECK(mr_reader_get_direction() == MR_DIR_LTR, "T7 restored after overwrite");
|
||||
}
|
||||
|
||||
static void test_read_once_and_failure(void)
|
||||
{
|
||||
/* T8 配置只读一次:首次 get 之后配置变化不影响内存(快路径)。 */
|
||||
mr_test_cfg_clear();
|
||||
mr_reader_test_reset();
|
||||
CHECK(mr_reader_get_direction() == MR_DIR_RTL, "T8 first get with no config");
|
||||
mr_test_cfg_put("reader.dir", "ltr");
|
||||
CHECK(mr_reader_get_direction() == MR_DIR_RTL, "T8 config read only once (fast path)");
|
||||
|
||||
/* T9 写失败:切换仍生效、WARN 一条、配置不变。 */
|
||||
mr_test_cfg_clear();
|
||||
mr_reader_test_reset();
|
||||
mr_test_log_clear();
|
||||
mr_test_cfg_fail_writes(1);
|
||||
mr_reader_set_direction(MR_DIR_LTR);
|
||||
CHECK(mr_reader_get_direction() == MR_DIR_LTR, "T9 switch still applies on write failure");
|
||||
CHECK(mr_test_log_warns() == 1, "T9 one WARN on write failure (got %d)", mr_test_log_warns());
|
||||
CHECK(!cfg_has("reader.dir"), "T9 config unchanged on failure");
|
||||
mr_test_cfg_fail_writes(0);
|
||||
mr_reader_set_direction(MR_DIR_RTL);
|
||||
CHECK(cfg_is("rtl") && mr_test_log_warns() == 1, "T9 recovery writes rtl, no extra WARN");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_default_and_ltr();
|
||||
test_bad_values();
|
||||
test_read_once_and_failure();
|
||||
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
Reference in new issue
Block a user