真机真实布局暴露两处问题(评审 P1):
- Books/400+本高质量完本合集/<分类>/<作品>/<书名>.txt 在 depth 3-4,旧 depth≤2 让 413 本 3.8GB 不可见
→ SHELF_SCAN_MAX_DEPTH=2→6;report 新增 depth_skipped,超深书籍文件计数并在 g_notice 可见提示
(有 N 个文件因目录层数超过 6 层未显示),不再静默
- 按完整相对路径分组会把 413 本书炸成 410 个每条 1 书的段头
→ 分组键改为 Books/ 第一级目录(shelf_book.top);完整相对路径仍在 group[],
第一级之后的子路径(分类/作品)放进副题 sub(如 01、玄幻/01、斗破 · TXT · 2.1MB)
→ 真机书库只剩 3 组:400+本高质量完本合集 / kh / 轻小说
- 单测 50 断言:depth3/4 收录、depth7 计入 depth_skipped、四种真实路径的组名、
a/b 与 a/c 同组 a、副题含子路径、format 回归、根组未分类、path/进度不变
360 lines
17 KiB
C
360 lines
17 KiB
C
/* test/shelf_group_smoke.c — M27 书架目录分组宿主单测(纯 C,gcc 直接跑,不依赖设备/SD 卡)。
|
||
*
|
||
* 覆盖:
|
||
* G1 扫描:格式识别 / 标题去扩展名 / 目录归属(group=完整相对路径,top=第一级目录)
|
||
* G2 format 模式 = 改动前行为(格式段头 TXT/FB2/EPUB/MOBI/CBZ,最近阅读优先、其次书名)
|
||
* G3 folder 模式:按 Books/ 第一级目录分段(未分类置顶,其余按组名排序),
|
||
* 更深子路径(分类/作品)进副题;a/b 与 a/c 同组 a
|
||
* G4 空目录不产生段头;depth 3/4 收录,depth > SHELF_SCAN_MAX_DEPTH 计数且不出书
|
||
* G5 Books/ 根下的书归「未分类」;字面名为「未分类」的目录与根组是两个不同键
|
||
* G6 UTF-8 安全截断(中文目录名不切出半个字符)
|
||
* G7 超上限不静默:truncated=1;行数上限(段头 ≤ 书数 ⇒ 2N 必够)
|
||
* G8 进度不受分组影响:path 不变、rdprog_load 前后同值、ts 两种模式一致
|
||
*/
|
||
#include "shelf_model.h"
|
||
#include "progress.h"
|
||
#include "reader_conf.h"
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <sys/stat.h>
|
||
#include <unistd.h>
|
||
|
||
static int g_pass = 0, g_fail = 0;
|
||
#define CHECK(cond, msg) do { \
|
||
if (cond) g_pass++; \
|
||
else { g_fail++; printf("FAIL %s:%d %s\n", __FILE__, __LINE__, msg); } \
|
||
} while (0)
|
||
|
||
/* progress.c 的目录来源:测试里指向临时目录(见 main)。 */
|
||
static char g_cfgdir[400];
|
||
const char *meow_config_dir(void) { return g_cfgdir; }
|
||
|
||
static void mkdirs(const char *path)
|
||
{
|
||
char t[400];
|
||
snprintf(t, sizeof t, "%s", path);
|
||
for (char *p = t + 1; *p; p++) {
|
||
if (*p == '/') { *p = 0; mkdir(t, 0755); *p = '/'; }
|
||
}
|
||
mkdir(t, 0755);
|
||
}
|
||
|
||
static void write_file(const char *path, const char *body)
|
||
{
|
||
FILE *f = fopen(path, "wb");
|
||
if (!f) { printf("无法创建 %s\n", path); exit(2); }
|
||
fputs(body, f);
|
||
fclose(f);
|
||
}
|
||
|
||
/* 按文件名给阅读时间:1.txt=100, 2.epub=50, 其它=0(验证 ts 排序 + 组内排序)。 */
|
||
static uint64_t ts_of(const char *path)
|
||
{
|
||
const char *b = strrchr(path, '/');
|
||
b = b ? b + 1 : path;
|
||
if (strcmp(b, "1.txt") == 0) return 100;
|
||
if (strcmp(b, "2.epub") == 0) return 50;
|
||
return 0;
|
||
}
|
||
|
||
/* 把行序列打成一串便于整体比对:S:<段头>|B:<书名>(<目录>)| */
|
||
static void dump_rows(const shelf_row *rows, int nrows, const shelf_book *books,
|
||
char *out, int outsz)
|
||
{
|
||
int off = 0;
|
||
out[0] = 0;
|
||
for (int i = 0; i < nrows && off < outsz; i++) {
|
||
if (rows[i].section) {
|
||
const char *label = rows[i].label ? rows[i].label : shelf_fmt_name(rows[i].fmt);
|
||
off += snprintf(out + off, (size_t)(outsz - off), "S:%s|", label);
|
||
} else {
|
||
const shelf_book *b = &books[rows[i].book];
|
||
off += snprintf(out + off, (size_t)(outsz - off), "B:%s(%s)|", b->title, b->group);
|
||
}
|
||
}
|
||
}
|
||
|
||
static const shelf_book *find_book(const shelf_book *books, int n, const char *title)
|
||
{
|
||
for (int i = 0; i < n; i++) if (strcmp(books[i].title, title) == 0) return &books[i];
|
||
return NULL;
|
||
}
|
||
|
||
static int utf8_complete(const char *s)
|
||
{
|
||
const unsigned char *p = (const unsigned char *)s;
|
||
while (*p) {
|
||
int n;
|
||
if (*p < 0x80) n = 1;
|
||
else if ((*p & 0xE0) == 0xC0) n = 2;
|
||
else if ((*p & 0xF0) == 0xE0) n = 3;
|
||
else if ((*p & 0xF8) == 0xF0) n = 4;
|
||
else return 0;
|
||
for (int i = 1; i < n; i++) if (!p[i] || (p[i] & 0xC0) != 0x80) return 0;
|
||
p += n;
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
static void test_scan_and_modes(const char *root, const char *tmproot)
|
||
{
|
||
/* ---- 造树(贴合真机真实布局)----
|
||
* Books/轻小说/1.txt (ts100)、Books/轻小说/2.epub (ts50)、
|
||
* Books/kh/3.txt、Books/根下.txt(根)、Books/B/sub/5.mobi(depth2)、
|
||
* Books/D/E/F/6.txt(depth3)、Books/F2/a/b/c/9.txt(depth4)、
|
||
* Books/400+本高质量完本合集/01、玄幻/01、斗破/《斗破》.txt(depth3 真实三级布局)、
|
||
* Books/C/(空)、Books/轻小说/notes.md(不支持)、
|
||
* Books/G/H/I/J/K/L/M/{7,8}.txt(depth7,超上限 → 只计数)。 */
|
||
char p[400];
|
||
snprintf(p, sizeof p, "%s/轻小说", root); mkdirs(p);
|
||
snprintf(p, sizeof p, "%s/kh", root); mkdirs(p);
|
||
snprintf(p, sizeof p, "%s/B/sub", root); mkdirs(p);
|
||
snprintf(p, sizeof p, "%s/C", root); mkdirs(p);
|
||
snprintf(p, sizeof p, "%s/D/E/F", root); mkdirs(p);
|
||
snprintf(p, sizeof p, "%s/F2/a/b/c", root); mkdirs(p);
|
||
snprintf(p, sizeof p, "%s/400+本高质量完本合集/01、玄幻/01、斗破", root); mkdirs(p);
|
||
snprintf(p, sizeof p, "%s/G/H/I/J/K/L/M", root); mkdirs(p);
|
||
snprintf(p, sizeof p, "%s/轻小说/1.txt", root); write_file(p, "one");
|
||
snprintf(p, sizeof p, "%s/轻小说/2.epub", root); write_file(p, "two");
|
||
snprintf(p, sizeof p, "%s/kh/3.txt", root); write_file(p, "three");
|
||
snprintf(p, sizeof p, "%s/根下.txt", root); write_file(p, "root");
|
||
snprintf(p, sizeof p, "%s/B/sub/5.mobi", root); write_file(p, "five");
|
||
snprintf(p, sizeof p, "%s/D/E/F/6.txt", root); write_file(p, "six");
|
||
snprintf(p, sizeof p, "%s/F2/a/b/c/9.txt", root); write_file(p, "nine");
|
||
snprintf(p, sizeof p, "%s/400+本高质量完本合集/01、玄幻/01、斗破/《斗破》.txt", root); write_file(p, "doupo");
|
||
snprintf(p, sizeof p, "%s/轻小说/notes.md", root); write_file(p, "ignored");
|
||
snprintf(p, sizeof p, "%s/G/H/I/J/K/L/M/7.txt", root); write_file(p, "too-deep");
|
||
snprintf(p, sizeof p, "%s/G/H/I/J/K/L/M/8.txt", root); write_file(p, "too-deep2");
|
||
|
||
shelf_book books[64];
|
||
shelf_scan_report rep;
|
||
int n = shelf_scan_books(root, books, 64, ts_of, &rep);
|
||
CHECK(n == 8, "扫描到 8 本(.md 忽略;depth3/4 收录;depth7 不收录)");
|
||
CHECK(rep.truncated == 0, "未触发上限");
|
||
CHECK(rep.depth_skipped == 2, "depth7 的 2 个文件计入 depth_skipped(不静默)");
|
||
CHECK(rep.root_missing == 0, "root 存在");
|
||
|
||
/* G1 目录归属(group=完整相对路径;top=第一级分组键)与 path */
|
||
const shelf_book *b1 = find_book(books, n, "1");
|
||
const shelf_book *b2 = find_book(books, n, "2");
|
||
const shelf_book *b3 = find_book(books, n, "3");
|
||
const shelf_book *b4 = find_book(books, n, "根下");
|
||
const shelf_book *b5 = find_book(books, n, "5");
|
||
const shelf_book *b6 = find_book(books, n, "6");
|
||
const shelf_book *b9 = find_book(books, n, "9");
|
||
const shelf_book *bdp = find_book(books, n, "《斗破》");
|
||
CHECK(b1 && strcmp(b1->group, "轻小说") == 0 && strcmp(b1->top, "轻小说") == 0,
|
||
"轻小说/1.txt → group/top=轻小说");
|
||
CHECK(b2 && strcmp(b2->top, "轻小说") == 0, "轻小说/2.epub → top=轻小说");
|
||
CHECK(b3 && strcmp(b3->top, "kh") == 0, "kh/3.txt → top=kh(四种情况之三)");
|
||
CHECK(b4 && b4->group[0] == 0 && b4->top[0] == 0, "Books/根下.txt → group/top 空");
|
||
CHECK(b5 && strcmp(b5->group, "B/sub") == 0 && strcmp(b5->top, "B") == 0,
|
||
"B/sub/5.mobi → group=B/sub、top=B");
|
||
CHECK(b6 && strcmp(b6->top, "D") == 0, "depth3 的 D/E/F/6.txt 被扫到");
|
||
CHECK(b9 && strcmp(b9->top, "F2") == 0, "depth4 的 F2/a/b/c/9.txt 被扫到");
|
||
CHECK(bdp && strcmp(bdp->top, "400+本高质量完本合集") == 0,
|
||
"真实三级布局 → top=400+本高质量完本合集(四种情况之二)");
|
||
CHECK(bdp && strcmp(bdp->group, "400+本高质量完本合集/01、玄幻/01、斗破") == 0,
|
||
"完整相对路径保留在 group[]");
|
||
CHECK(find_book(books, n, "7") == NULL && find_book(books, n, "8") == NULL,
|
||
"depth7 的文件不产生书");
|
||
if (b1) {
|
||
char want[400];
|
||
snprintf(want, sizeof want, "%s/轻小说/1.txt", root);
|
||
CHECK(strcmp(b1->path, want) == 0, "path 仍是完整绝对路径");
|
||
}
|
||
|
||
/* 副题:第一级之后的子路径(分类/作品)必须可见 */
|
||
CHECK(bdp && strstr(bdp->sub, "01、玄幻/01、斗破") != NULL, "副题含 01、玄幻/01、斗破");
|
||
CHECK(b5 && strstr(b5->sub, "sub") != NULL, "副题含 sub");
|
||
CHECK(b3 && strncmp(b3->sub, "TXT", 3) == 0, "一级目录书的副题无多余子路径");
|
||
CHECK(b4 && strncmp(b4->sub, "TXT", 3) == 0, "根下书副题无多余子路径");
|
||
|
||
/* G2 format 模式(改动前行为):格式段头 + 最近阅读优先 + 全局书名排序 */
|
||
shelf_row rows[128];
|
||
int nf = shelf_build_rows(books, n, 0, rows, 128);
|
||
char got[4096];
|
||
dump_rows(rows, nf, books, got, sizeof got);
|
||
CHECK(strcmp(got,
|
||
"S:TXT|B:1(轻小说)|B:3(kh)|B:6(D/E/F)|B:9(F2/a/b/c)|"
|
||
"B:《斗破》(400+本高质量完本合集/01、玄幻/01、斗破)|B:根下()|"
|
||
"S:EPUB|B:2(轻小说)|S:MOBI|B:5(B/sub)|") == 0,
|
||
"format 模式段头/顺序与旧行为一致");
|
||
CHECK(nf == 11, "format 行数 = 3 段头 + 8 书");
|
||
|
||
/* G3 folder 模式:按第一级目录分组(未分类置顶 → 400+ → B → D → F2 → kh → 轻小说) */
|
||
int nd = shelf_build_rows(books, n, 1, rows, 128);
|
||
dump_rows(rows, nd, books, got, sizeof got);
|
||
CHECK(strcmp(got,
|
||
"S:未分类|B:根下()|"
|
||
"S:400+本高质量完本合集|B:《斗破》(400+本高质量完本合集/01、玄幻/01、斗破)|"
|
||
"S:B|B:5(B/sub)|S:D|B:6(D/E/F)|S:F2|B:9(F2/a/b/c)|S:kh|B:3(kh)|"
|
||
"S:轻小说|B:1(轻小说)|B:2(轻小说)|") == 0,
|
||
"folder 模式按第一级目录分组、段头顺序正确");
|
||
CHECK(nd == 15, "folder 行数 = 7 段头 + 8 书");
|
||
|
||
/* G4 空目录不产生段头(C 不在序列里) */
|
||
CHECK(strstr(got, "S:C|") == NULL, "空目录 C 无段头");
|
||
|
||
/* G7 行数上限:段头 ≤ 书数,2N 足够;rowcap 小时只写 rowcap 行 */
|
||
CHECK(SHELF_MAX_ROWS >= SHELF_MAX_BOOKS * 2, "SHELF_MAX_ROWS ≥ 2×书上限");
|
||
int nc = shelf_build_rows(books, n, 1, rows, 3);
|
||
CHECK(nc == 3, "rowcap=3 时返回 3(不越界写)");
|
||
|
||
/* G7 超上限可见:cap=2 时 truncated=1 */
|
||
shelf_book small[2];
|
||
shelf_scan_report rep2;
|
||
int ns = shelf_scan_books(root, small, 2, ts_of, &rep2);
|
||
CHECK(ns == 2 && rep2.truncated == 1, "超上限时 truncated=1(不静默)");
|
||
|
||
/* 不存在的根:root_missing=1 */
|
||
shelf_scan_report rep3;
|
||
char nope[400]; snprintf(nope, sizeof nope, "%s/no_such_dir", tmproot);
|
||
CHECK(shelf_scan_books(nope, small, 2, NULL, &rep3) == 0 && rep3.root_missing == 1,
|
||
"根不存在时 root_missing=1");
|
||
|
||
/* G5.1 a/b 与 a/c 必须归同一组 a(不是两个段头) */
|
||
char root3[400];
|
||
snprintf(root3, sizeof root3, "%s/mini2", tmproot);
|
||
snprintf(p, sizeof p, "%s/a/b", root3); mkdirs(p);
|
||
snprintf(p, sizeof p, "%s/a/c", root3); mkdirs(p);
|
||
snprintf(p, sizeof p, "%s/a/b/1.txt", root3); write_file(p, "b");
|
||
snprintf(p, sizeof p, "%s/a/c/2.txt", root3); write_file(p, "c");
|
||
shelf_book mb2[8];
|
||
shelf_scan_report rep5;
|
||
int nm2 = shelf_scan_books(root3, mb2, 8, NULL, &rep5);
|
||
int nd2 = shelf_build_rows(mb2, nm2, 1, rows, 128);
|
||
dump_rows(rows, nd2, mb2, got, sizeof got);
|
||
CHECK(strcmp(got, "S:a|B:1(a/b)|B:2(a/c)|") == 0, "a/b 与 a/c 同组 a(一个段头)");
|
||
CHECK(nd2 == 3, "a/b + a/c = 1 段头 + 2 书");
|
||
CHECK(find_book(mb2, nm2, "1") && strstr(find_book(mb2, nm2, "1")->sub, "b") != NULL,
|
||
"a/b 的副题含 b");
|
||
CHECK(find_book(mb2, nm2, "2") && strstr(find_book(mb2, nm2, "2")->sub, "c") != NULL,
|
||
"a/c 的副题含 c");
|
||
|
||
/* G5.2 字面名「未分类」的目录与根组是两个独立键(不按显示名合并) */
|
||
char root2[400], mini[420];
|
||
snprintf(root2, sizeof root2, "%s/mini", tmproot);
|
||
snprintf(mini, sizeof mini, "%s/未分类", root2);
|
||
mkdirs(mini);
|
||
snprintf(p, sizeof p, "%s/x.txt", root2); write_file(p, "x");
|
||
snprintf(p, sizeof p, "%s/未分类/7.txt", root2); write_file(p, "seven");
|
||
shelf_book mb[8];
|
||
shelf_scan_report rep4;
|
||
int nm = shelf_scan_books(root2, mb, 8, NULL, &rep4);
|
||
int ndm = shelf_build_rows(mb, nm, 1, rows, 128);
|
||
dump_rows(rows, ndm, mb, got, sizeof got);
|
||
CHECK(strcmp(got, "S:未分类|B:x()|S:未分类|B:7(未分类)|") == 0,
|
||
"根组与名为「未分类」的目录不合并(两个段头)");
|
||
|
||
/* G6 UTF-8 安全截断 */
|
||
char src[200], dst[96];
|
||
int k = 0;
|
||
for (int i = 0; i < 40; i++) { memcpy(src + k, "轻", 3); k += 3; }
|
||
src[k] = 0; /* 120 字节 */
|
||
int ncpy = shelf_copy_utf8(dst, 96, src);
|
||
CHECK(ncpy == 93 && strlen(dst) == 93, "长中文目录名截到 93 字节(31 字,整字丢弃)");
|
||
CHECK(strncmp(dst, src, 93) == 0 && utf8_complete(dst), "截断后仍是合法 UTF-8 前缀");
|
||
CHECK(shelf_copy_utf8(dst, 3, "轻x") == 0 && dst[0] == 0, "cap=3 放不下 3 字节汉字时整段丢弃");
|
||
CHECK(shelf_copy_utf8(dst, 4, "轻x") == 3 && strcmp(dst, "轻") == 0, "cap=4 恰好放下一个汉字");
|
||
char ascii[128]; memset(ascii, 'a', 100); ascii[100] = 0;
|
||
CHECK(shelf_copy_utf8(dst, 96, ascii) == 95, "ASCII 90+ 字节按 cap-1 截断");
|
||
}
|
||
|
||
static void test_progress_stability(const char *root)
|
||
{
|
||
/* G8:写一份进度,切换分组模式前后 rdprog_load 必须同值,path 不变。 */
|
||
char path[400];
|
||
snprintf(path, sizeof path, "%s/轻小说/1.txt", root);
|
||
|
||
rdprog p;
|
||
memset(&p, 0, sizeof p);
|
||
p.valid = 1; p.seg = 7; p.ch = 123; p.fs = 22; p.ls = 140; p.enc = 1; p.ts = 100;
|
||
CHECK(rdprog_save(path, &p) == 0, "写入测试进度");
|
||
|
||
shelf_book books[64];
|
||
shelf_scan_report rep;
|
||
int n = shelf_scan_books(root, books, 64, ts_of, &rep);
|
||
const shelf_book *b = find_book(books, n, "1");
|
||
CHECK(b && strcmp(b->path, path) == 0, "扫描后 path 未变");
|
||
CHECK(b && b->ts == 100, "扫描用 rdprog_load 的时间戳");
|
||
|
||
/* 快照所有 path,跑两种模式的 build_rows,再逐条比对。 */
|
||
char before[64][400];
|
||
for (int i = 0; i < n; i++) snprintf(before[i], sizeof before[i], "%s", books[i].path);
|
||
|
||
shelf_row rows[128];
|
||
shelf_build_rows(books, n, 0, rows, 128);
|
||
rdprog after_f;
|
||
int rf = rdprog_load(path, &after_f);
|
||
shelf_build_rows(books, n, 1, rows, 128);
|
||
rdprog after_d;
|
||
int rd = rdprog_load(path, &after_d);
|
||
|
||
CHECK(rf == 1 && rd == 1, "两种模式下进度都存在");
|
||
CHECK(after_f.seg == p.seg && after_f.ch == p.ch && after_f.fs == p.fs &&
|
||
after_f.ls == p.ls && after_f.enc == p.enc && after_f.ts == p.ts &&
|
||
after_d.seg == p.seg && after_d.ch == p.ch && after_d.ts == p.ts,
|
||
"切换 format/folder 后 rdprog_load 同值");
|
||
int same = 1;
|
||
for (int i = 0; i < n; i++) if (strcmp(before[i], books[i].path) != 0) same = 0;
|
||
CHECK(same, "build_rows 前后所有 path 不变(进度键稳定)");
|
||
|
||
/* 两种模式下 ts 一致(排序输入相同) */
|
||
shelf_book books2[64];
|
||
int n2 = shelf_scan_books(root, books2, 64, ts_of, &rep);
|
||
const shelf_book *b2 = find_book(books2, n2, "1");
|
||
CHECK(n2 == n && b2 && b2->ts == 100, "重扫(另一模式无差别)ts 一致");
|
||
}
|
||
|
||
static void test_conf_default(const char *tmproot)
|
||
{
|
||
/* 旧用户 reader.conf 没有 ui.shelf_group:必须默认 format(书架不变样),persist 后补写 format。 */
|
||
char cpath[400];
|
||
snprintf(cpath, sizeof cpath, "%s/reader.conf", tmproot); /* g_cfgdir=<tmp>/data → base=<tmp> */
|
||
write_file(cpath, "ui.shelf_layout=list\nrd.fs=20\nrd.ls=130\n");
|
||
|
||
const rdconf *c = rdconf_get();
|
||
CHECK(c && strcmp(c->shelf_group, "format") == 0, "无 ui.shelf_group 键 → 默认 format");
|
||
CHECK(c && strcmp(c->shelf, "list") == 0 && c->fs == 20, "其它配置项照常读取");
|
||
|
||
CHECK(rdconf_persist() == 0, "reader.conf 写回成功");
|
||
FILE *f = fopen(cpath, "rb");
|
||
CHECK(f != NULL, "写回后可读");
|
||
if (f) {
|
||
char buf[1024];
|
||
size_t n = fread(buf, 1, sizeof buf - 1, f);
|
||
fclose(f);
|
||
buf[n] = 0;
|
||
CHECK(strstr(buf, "ui.shelf_group=format\n") != NULL, "写回文件含 ui.shelf_group=format");
|
||
}
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
const char *base = getenv("TMPDIR");
|
||
if (!base || !base[0]) base = "/tmp";
|
||
char tmpl[400];
|
||
snprintf(tmpl, sizeof tmpl, "%s/shelf_group_XXXXXX", base);
|
||
char *tmp = mkdtemp(tmpl);
|
||
if (!tmp) { printf("mkdtemp 失败\n"); return 2; }
|
||
snprintf(g_cfgdir, sizeof g_cfgdir, "%s/data", tmp); /* progress/reader.conf 落临时目录 */
|
||
mkdirs(g_cfgdir);
|
||
|
||
char root[400];
|
||
snprintf(root, sizeof root, "%s/Books", tmp);
|
||
mkdirs(root);
|
||
|
||
test_scan_and_modes(root, tmp);
|
||
test_progress_stability(root);
|
||
test_conf_default(tmp);
|
||
|
||
printf("shelf_group_smoke: %d pass, %d fail\n", g_pass, g_fail);
|
||
/* 清理临时树(保留失败现场意义不大,测试可重复) */
|
||
char cmd[512];
|
||
snprintf(cmd, sizeof cmd, "rm -rf '%s'", tmp);
|
||
if (system(cmd) != 0) { /* 忽略 */ }
|
||
return g_fail ? 1 : 0;
|
||
}
|