diff --git a/.gitignore b/.gitignore index fec08036..54305f3e 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,6 @@ AGENTS.md .agents .codex +# dfm-search 测试脚本报告 +tests/dfm-search/scripts/reports/ + diff --git a/tests/dfm-search/scripts/README.md b/tests/dfm-search/scripts/README.md new file mode 100644 index 00000000..32db7029 --- /dev/null +++ b/tests/dfm-search/scripts/README.md @@ -0,0 +1,131 @@ +# dfm-search 测试脚本 + +本目录包含 `dfm-searcher` CLI 工具的文件名搜索和文件内容搜索集成测试脚本。 + +## 目录结构 + +``` +tests/dfm-search/scripts/ +├── test_filename_search.sh # 文件名搜索测试脚本(21 个用例) +├── test_content_search.sh # 文件内容搜索测试脚本(13 个用例) +├── common.sh # 公共函数库 +└── README.md # 本文件 +``` + +## 前置条件 + +| 依赖项 | 说明 | +|--------|------| +| `dfm-searcher` | 必须已编译安装并在 `PATH` 中可用,或通过 `DFM_SEARCHER` 环境变量指定路径 | +| 文件索引服务 | dde-file-manager 的文件索引服务需在运行中 | +| `bash` | 脚本解释器(≥ 4.0) | +| `mktemp` | 创建临时目录 | +| `grep` / `sed` | 基本文本处理 | +| `jq`(可选) | JSON 解析,不可用时降级为 `grep` | + +## 运行方式 + +```bash +# 运行文件名搜索测试 +./test_filename_search.sh + +# 运行文件内容搜索测试 +./test_content_search.sh + +# 指定 dfm-searcher 路径 +DFM_SEARCHER=/path/to/dfm-searcher ./test_filename_search.sh + +# 调整索引等待超时(秒) +INDEX_WAIT_TIMEOUT_FILENAME=20 ./test_filename_search.sh +INDEX_WAIT_TIMEOUT_CONTENT=60 ./test_content_search.sh + +# 调整轮询间隔(秒) +INDEX_POLL_INTERVAL=3 ./test_filename_search.sh + +# 指定报告输出目录(默认 <脚本目录>/reports) +TEST_REPORT_DIR=/tmp/my-reports ./test_filename_search.sh +``` + +## 测试报告 + +脚本运行结束时会自动在 `reports/` 目录下生成测试报告文件(文件名含脚本名与时间戳,如 `report_test_filename_search_20260921_143000.txt`),内容包括: + +- 环境信息(脚本名、日期、耗时、主机、dfm-searcher 路径、临时目录) +- 每个用例的结果(PASS / FAIL / SKIP 及失败原因) +- 汇总统计与最终结果(PASSED / FAILED) + +报告目录可通过环境变量 `TEST_REPORT_DIR` 自定义,`reports/` 目录已加入 `.gitignore`。 + +## 测试用例概览 + +### 文件名搜索(FT-01 ~ FT-21) + +| 用例 | 说明 | +|------|------| +| FT-01 | 基本中文关键词文件名搜索 | +| FT-02 | 英文关键词文件名搜索 | +| FT-03 | 通配符 `*` 搜索 | +| FT-04 | 通配符 `?` 搜索 | +| FT-05 | Boolean AND 搜索(`,` 分隔符) | +| FT-06 | Boolean OR 搜索(`\|` 分隔符) | +| FT-07 | 拼音全拼搜索 | +| FT-08 | 拼音首字母搜索 | +| FT-09 | 拼音 + Boolean 组合搜索 | +| FT-10 | 大小写敏感搜索 | +| FT-11 | 隐藏文件搜索 | +| FT-12 | max-results 结果限制 | +| FT-13 | 新建文件后搜索 | +| FT-14 | 删除文件后搜索 | +| FT-15 | 重命名文件后搜索 | +| FT-16 | 移动文件后搜索 | +| FT-17 | 目录搜索 | +| FT-18 | 实时搜索(`--method=realtime`) | +| FT-19 | 扩展名过滤 | +| FT-20 | Boolean AND(`&` 分隔符) | +| FT-21 | 压力测试 — 1000 个文件名搜索 | + +### 文件内容搜索(CT-01 ~ CT-13) + +| 用例 | 说明 | +|------|------| +| CT-01 | 基本中文内容搜索 | +| CT-02 | 英文内容搜索 | +| CT-03 | Boolean AND 内容搜索(`,` 分隔符) | +| CT-04 | Boolean OR 内容搜索(`\|` 分隔符) | +| CT-05 | 大小写敏感内容搜索 | +| CT-06 | 预览长度控制(`-v` / `--max-preview`) | +| CT-07 | JSON 输出字段验证 | +| CT-08 | 内容修改后搜索 | +| CT-09 | 内容删除后搜索 | +| CT-10 | 隐藏文件内容搜索 | +| CT-11 | 多文件内容搜索 | +| CT-12 | Boolean AND(`&` 分隔符)内容搜索 | +| CT-13 | 压力测试 — 1000 个文件内容搜索 | + +### 已知限制(自动 SKIP) + +| 用例 | 限制 | 说明 | +|------|------|------| +| FT-10b/c、CT-05b/c | `--case-sensitive` 不生效 | 索引层做了大小写归一化,标志被接受但结果仍大小写不敏感(实测大写关键词返回 0 结果)。脚本运行时自动探测:若服务将来支持该能力,会自动恢复执行断言 | + +## 退出码 + +| 退出码 | 含义 | +|--------|------| +| 0 | 全部通过,或仅有 SKIP(无 FAIL) | +| 1 | 存在 FAIL | +| 非零(前置检查) | `dfm-searcher` 不可用或索引服务未运行 | + +## 索引延迟处理策略 + +脚本采用轮询等待策略处理索引延迟: + +1. **轮询等待**:创建文件后循环执行搜索,直到结果出现或超时 +2. **超时标记 SKIP**:超时后标记为 `SKIP` 而非 `FAIL`,避免因索引延迟导致误报 +3. **可配置超时**:通过环境变量 `INDEX_WAIT_TIMEOUT_FILENAME`(默认 10s)和 `INDEX_WAIT_TIMEOUT_CONTENT`(默认 30s)调整 +4. **压力测试超时**:压力测试(FT-21/CT-13)使用更长超时,通过 `STRESS_INDEX_WAIT_TIMEOUT_FILENAME`(默认 60s)和 `STRESS_INDEX_WAIT_TIMEOUT_CONTENT`(默认 120s)调整 +5. **前置检查**:脚本启动时检查 `dfm-searcher` 可用性和索引服务状态 + +## 临时目录 + +测试在 `~/Documents` 下创建临时目录(`mktemp -d -p ~/Documents`),确保在索引服务监控范围内。测试结束后自动清理。 diff --git a/tests/dfm-search/scripts/common.sh b/tests/dfm-search/scripts/common.sh new file mode 100755 index 00000000..701f9ea7 --- /dev/null +++ b/tests/dfm-search/scripts/common.sh @@ -0,0 +1,439 @@ +#!/bin/bash +# SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +# +# SPDX-License-Identifier: GPL-3.0-or-later + +# ============================================================================= +# common.sh — dfm-search 测试公共函数库 +# +# 提供测试环境管理、索引等待、搜索断言、测试报告等公共能力, +# 供 test_filename_search.sh 和 test_content_search.sh 复用。 +# ============================================================================= + +# ---------- 全局变量 ---------- + +TEST_PASS=0 +TEST_FAIL=0 +TEST_SKIP=0 +TEST_TOTAL=0 +TEST_TEMP_DIR="" +TEST_SEARCHER="${DFM_SEARCHER:-dfm-searcher}" + +# 测试报告相关 +# 报告输出目录,默认为 <脚本目录>/reports,可通过环境变量 TEST_REPORT_DIR 覆盖 +TEST_REPORT_DIR="${TEST_REPORT_DIR:-}" +TEST_REPORT_FILE="" +TEST_REPORT_TMP="" +TEST_START_TS=0 + +# 索引等待超时(秒),可通过环境变量 INDEX_WAIT_TIMEOUT 覆盖 +# 文件名索引默认 10 秒,内容索引默认 30 秒 +FILENAME_INDEX_TIMEOUT="${INDEX_WAIT_TIMEOUT_FILENAME:-10}" +CONTENT_INDEX_TIMEOUT="${INDEX_WAIT_TIMEOUT_CONTENT:-30}" + +# 压力测试索引等待超时(秒),可通过环境变量覆盖 +# 大批量文件索引需要更长等待时间 +STRESS_FILENAME_INDEX_TIMEOUT="${STRESS_INDEX_WAIT_TIMEOUT_FILENAME:-60}" +STRESS_CONTENT_INDEX_TIMEOUT="${STRESS_INDEX_WAIT_TIMEOUT_CONTENT:-120}" + +# 索引轮询间隔(秒) +INDEX_POLL_INTERVAL="${INDEX_POLL_INTERVAL:-2}" + +# ---------- 颜色输出 ---------- + +if [[ -t 1 ]]; then + COLOR_PASS='\033[32m' + COLOR_FAIL='\033[31m' + COLOR_SKIP='\033[33m' + COLOR_RESET='\033[0m' +else + COLOR_PASS='' + COLOR_FAIL='' + COLOR_SKIP='' + COLOR_RESET='' +fi + +# ---------- 测试环境管理 ---------- + +# setup_test_env — 创建临时测试目录并初始化计数器 +# 临时目录在 ~/Documents 下创建,确保在索引服务监控范围内 +setup_test_env() { + local prefix="${1:-dfm-search-test}" + + # 确保 ~/Documents 目录存在 + if [[ ! -d "$HOME/Documents" ]]; then + echo "ERROR: ~/Documents directory does not exist" >&2 + return 1 + fi + + # 在 ~/Documents 下创建临时目录,确保索引服务能监控到 + TEST_TEMP_DIR=$(mktemp -d -p "$HOME/Documents" "${prefix}.XXXXXX") + if [[ -z "$TEST_TEMP_DIR" ]]; then + echo "ERROR: Failed to create temp directory under ~/Documents" >&2 + return 1 + fi + + TEST_PASS=0 + TEST_FAIL=0 + TEST_SKIP=0 + TEST_TOTAL=0 + TEST_START_TS=$(date +%s) + + echo "Test temp dir: $TEST_TEMP_DIR" +} + +# cleanup_test_env — 清理临时测试目录 +cleanup_test_env() { + if [[ -n "$TEST_REPORT_TMP" && -f "$TEST_REPORT_TMP" ]]; then + rm -f "$TEST_REPORT_TMP" + fi + if [[ -n "$TEST_TEMP_DIR" && -d "$TEST_TEMP_DIR" ]]; then + rm -rf "$TEST_TEMP_DIR" + echo "Cleaned up: $TEST_TEMP_DIR" + fi +} + +# ---------- 前置条件检查 ---------- + +# check_prerequisites — 检查 dfm-searcher 可用性和索引服务状态 +# 返回 0 表示通过,非 0 表示不满足 +check_prerequisites() { + # 检查 dfm-searcher 是否可用 + if ! command -v "$TEST_SEARCHER" &>/dev/null; then + echo "ERROR: $TEST_SEARCHER not found in PATH" + echo " Please build and install dfm-searcher first" + return 1 + fi + + # 检查索引服务是否运行(尝试一次简单搜索) + local check_output + check_output=$("$TEST_SEARCHER" -j "test" "$HOME/Documents" 2>&1) + if [[ $? -ne 0 ]]; then + echo "WARNING: dfm-searcher returned error on simple search" + echo " Index service may not be running" + echo " Output: $check_output" + return 1 + fi + + echo "Prerequisites check passed." + return 0 +} + +# ---------- 索引等待 ---------- + +# wait_for_index — 轮询等待索引建立 +# 参数: +# $1: 搜索类型 (filename | content) +# $2: 关键词 +# $3: 搜索路径 +# $4: 期望匹配的文件名片段(用于验证索引是否就绪) +# $5: 额外的 dfm-searcher 选项(可选,作为单一字符串传递) +# $6: 自定义超时秒数(可选,用于压力测试等需要更长等待的场景) +# 返回: 0 表示索引就绪,1 表示超时 +wait_for_index() { + local search_type="$1" + local keyword="$2" + local search_path="$3" + local expected_fragment="$4" + local extra_opts="${5:-}" + + local custom_timeout="${6:-}" + local timeout + if [[ -n "$custom_timeout" ]]; then + timeout="$custom_timeout" + elif [[ "$search_type" == "content" ]]; then + timeout="$CONTENT_INDEX_TIMEOUT" + else + timeout="$FILENAME_INDEX_TIMEOUT" + fi + + local elapsed=0 + local result + + while [[ $elapsed -lt $timeout ]]; do + if [[ -n "$extra_opts" ]]; then + result=$("$TEST_SEARCHER" --type="$search_type" -j "$keyword" "$search_path" $extra_opts 2>/dev/null) + else + result=$("$TEST_SEARCHER" --type="$search_type" -j "$keyword" "$search_path" 2>/dev/null) + fi + + if json_contains "$result" "$expected_fragment"; then + return 0 + fi + + sleep "$INDEX_POLL_INTERVAL" + elapsed=$((elapsed + INDEX_POLL_INTERVAL)) + done + + return 1 +} + +# ---------- JSON 解析 ---------- + +# json_contains — 检查 JSON 输出中是否包含指定路径片段 +# 优先使用 jq,不可用时降级为 grep +# 参数: +# $1: JSON 输出字符串 +# $2: 期望匹配的文件路径片段 +json_contains() { + local json="$1" + local fragment="$2" + + if [[ -z "$json" || -z "$fragment" ]]; then + return 1 + fi + + if command -v jq &>/dev/null; then + # 兼容两种输出格式: + # 1. 普通 JSON:results 为路径字符串数组或含 path 字段的对象数组 + # 2. 实时搜索 NDJSON 流式输出:{"type":"result","data":"<路径>"} 事件行 + echo "$json" | jq -r ' + (select(.type? == "result") | .data? // empty), + (.results[]? | if type == "string" then . elif type == "object" then (.path? // empty) else empty end) + ' 2>/dev/null | grep -qF "$fragment" + return $? + else + # 降级为 grep + echo "$json" | grep -qF "$fragment" + return $? + fi +} + +# json_count_results — 统计 JSON 输出中的结果数量 +# 参数: +# $1: JSON 输出字符串 +json_count_results() { + local json="$1" + + if [[ -z "$json" ]]; then + echo 0 + return + fi + + if command -v jq &>/dev/null; then + local count + count=$(echo "$json" | jq -r '.status.totalResults // (.results | length) // 0' 2>/dev/null) + echo "${count:-0}" + else + # 降级:统计 "path" 出现次数 + echo "$json" | grep -o '"path"' | wc -l + fi +} + +# ---------- 搜索执行 ---------- + +# run_searcher — 执行 dfm-searcher 并返回 JSON 输出 +# 参数: +# $1: 搜索类型 (filename | content) +# $2: 关键词 +# $3: 搜索路径 +# $@: 额外选项(直接传递给 dfm-searcher) +run_searcher() { + local search_type="$1" + local keyword="$2" + local search_path="$3" + shift 3 + + "$TEST_SEARCHER" --type="$search_type" -j "$keyword" "$search_path" "$@" 2>/dev/null +} + +# ---------- 断言函数 ---------- + +# assert_found — 断言搜索结果包含指定文件路径片段 +# 参数: +# $1: 测试用例名称 +# $2: JSON 输出 +# $3: 期望匹配的文件路径片段 +assert_found() { + local test_name="$1" + local json="$2" + local expected="$3" + + TEST_TOTAL=$((TEST_TOTAL + 1)) + if json_contains "$json" "$expected"; then + pass "$test_name" + else + fail "$test_name" "Expected to find '$expected' in search results" + fi +} + +# assert_not_found — 断言搜索结果不包含指定文件路径片段 +# 参数: +# $1: 测试用例名称 +# $2: JSON 输出 +# $3: 不期望匹配的文件路径片段 +assert_not_found() { + local test_name="$1" + local json="$2" + local unexpected="$3" + + TEST_TOTAL=$((TEST_TOTAL + 1)) + if json_contains "$json" "$unexpected"; then + fail "$test_name" "Unexpectedly found '$unexpected' in search results" + else + pass "$test_name" + fi +} + +# assert_count — 断言搜索结果数量等于指定值 +# 参数: +# $1: 测试用例名称 +# $2: JSON 输出 +# $3: 期望数量 +assert_count() { + local test_name="$1" + local json="$2" + local expected_count="$3" + + TEST_TOTAL=$((TEST_TOTAL + 1)) + local actual_count + actual_count=$(json_count_results "$json") + if [[ "$actual_count" -eq "$expected_count" ]]; then + pass "$test_name" + else + fail "$test_name" "Expected $expected_count results, got $actual_count" + fi +} + +# assert_le_count — 断言搜索结果数量不超过指定值 +# 参数: +# $1: 测试用例名称 +# $2: JSON 输出 +# $3: 最大数量 +assert_le_count() { + local test_name="$1" + local json="$2" + local max_count="$3" + + TEST_TOTAL=$((TEST_TOTAL + 1)) + local actual_count + actual_count=$(json_count_results "$json") + if [[ "$actual_count" -le "$max_count" ]]; then + pass "$test_name" + else + fail "$test_name" "Expected <= $max_count results, got $actual_count" + fi +} + +# ---------- 测试报告 ---------- + +# record_result — 将单条测试结果记录到临时结果文件(用于最终生成报告) +# 参数: +# $1: 状态 (PASS | FAIL | SKIP) +# $2: 测试用例名称 +# $3: 附加消息(可选) +record_result() { + local status="$1" + local name="$2" + local message="${3:-}" + + # 懒初始化临时结果文件 + if [[ -z "$TEST_REPORT_TMP" ]]; then + TEST_REPORT_TMP=$(mktemp) || return 0 + fi + + printf '%s\t%s\t%s\n' "$status" "$name" "$message" >> "$TEST_REPORT_TMP" 2>/dev/null || true +} + +pass() { + TEST_PASS=$((TEST_PASS + 1)) + echo -e "${COLOR_PASS}[PASS]${COLOR_RESET} $1" + record_result "PASS" "$1" +} + +fail() { + TEST_FAIL=$((TEST_FAIL + 1)) + echo -e "${COLOR_FAIL}[FAIL]${COLOR_RESET} $1 — $2" + record_result "FAIL" "$1" "$2" +} + +skip() { + TEST_SKIP=$((TEST_SKIP + 1)) + echo -e "${COLOR_SKIP}[SKIP]${COLOR_RESET} $1 — $2" + record_result "SKIP" "$1" "$2" +} + +# generate_report — 生成测试报告文件 +# 报告写入 TEST_REPORT_DIR(默认 <脚本目录>/reports),文件名含脚本名与时间戳, +# 生成的路径保存在 TEST_REPORT_FILE 中 +generate_report() { + local script_name + script_name=$(basename "${0%.sh}") + + local report_dir="${TEST_REPORT_DIR:-${SCRIPT_DIR:-.}/reports}" + if ! mkdir -p "$report_dir" 2>/dev/null; then + echo "WARNING: Failed to create report dir: $report_dir, report not saved" >&2 + return 1 + fi + + TEST_REPORT_FILE="$report_dir/report_${script_name}_$(date '+%Y%m%d_%H%M%S').txt" + + local duration="N/A" + if [[ "$TEST_START_TS" -gt 0 ]]; then + duration="$(( $(date +%s) - TEST_START_TS ))s" + fi + + { + echo "=========================================" + echo " dfm-search 集成测试报告" + echo "=========================================" + echo " 脚本: $(basename "$0")" + echo " 日期: $(date '+%Y-%m-%d %H:%M:%S')" + echo " 耗时: $duration" + echo " 主机: $(hostname 2>/dev/null || echo unknown)" + echo " dfm-searcher: $TEST_SEARCHER" + echo " 临时目录: ${TEST_TEMP_DIR:-N/A}" + echo "-----------------------------------------" + echo " 用例结果" + echo "-----------------------------------------" + if [[ -n "$TEST_REPORT_TMP" && -f "$TEST_REPORT_TMP" ]]; then + while IFS=$'\t' read -r status name message; do + if [[ -n "$message" ]]; then + printf '[%s] %s — %s\n' "$status" "$name" "$message" + else + printf '[%s] %s\n' "$status" "$name" + fi + done < "$TEST_REPORT_TMP" + fi + echo "-----------------------------------------" + echo " 汇总" + echo "-----------------------------------------" + echo " Total: $TEST_TOTAL" + echo " Pass: $TEST_PASS" + echo " Fail: $TEST_FAIL" + echo " Skip: $TEST_SKIP" + if [[ $TEST_FAIL -gt 0 ]]; then + echo " 结果: FAILED" + else + echo " 结果: PASSED" + fi + echo "=========================================" + } > "$TEST_REPORT_FILE" + + return 0 +} + +# print_summary — 输出测试汇总、生成报告文件并设置退出码 +# 返回: 0(全 PASS 或仅有 SKIP),1(有 FAIL) +print_summary() { + echo "" + echo "=========================================" + echo " Test Summary" + echo "=========================================" + echo " Total: $TEST_TOTAL" + echo " Pass: $TEST_PASS" + echo " Fail: $TEST_FAIL" + echo " Skip: $TEST_SKIP" + echo "=========================================" + + if generate_report; then + echo "" + echo "Test report: $TEST_REPORT_FILE" + fi + + if [[ $TEST_FAIL -gt 0 ]]; then + return 1 + else + return 0 + fi +} diff --git a/tests/dfm-search/scripts/test_content_search.sh b/tests/dfm-search/scripts/test_content_search.sh new file mode 100755 index 00000000..2a7885a0 --- /dev/null +++ b/tests/dfm-search/scripts/test_content_search.sh @@ -0,0 +1,370 @@ +#!/bin/bash +# SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +# +# SPDX-License-Identifier: GPL-3.0-or-later + +# ============================================================================= +# test_content_search.sh — dfm-search 文件内容搜索测试脚本 +# +# 在 ~/Documents 下创建临时目录,创建 txt 文本文件并写入内容,使用 +# dfm-searcher 的内容搜索功能验证: +# - 基本内容搜索、Boolean 搜索 +# - 大小写敏感搜索 +# - 预览长度控制、JSON 输出 +# - 内容修改/删除后搜索 +# - 隐藏文件内容搜索 +# +# 注意:以下用例因不支持已移除(非 SKIP): +# - 拼音内容搜索(--type=content --pinyin) +# - 实时内容搜索(--method=realtime --type=content) +# +# 用法: ./test_content_search.sh +# 环境变量: +# DFM_SEARCHER — dfm-searcher 可执行文件路径(默认: dfm-searcher) +# INDEX_WAIT_TIMEOUT_CONTENT — 内容索引等待超时秒数(默认: 120) +# INDEX_POLL_INTERVAL — 索引轮询间隔秒数(默认: 2) +# ============================================================================= + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/common.sh" + +# 陷阱:确保退出时清理 +trap cleanup_test_env EXIT + +echo "=========================================" +echo " dfm-search 文件内容搜索测试" +echo "=========================================" +echo "" + +# 前置条件检查 +if ! check_prerequisites; then + echo "Prerequisites not met. Exiting." + exit 1 +fi + +# 初始化测试环境 +setup_test_env "ct-search-test" +TEST_DIR="$TEST_TEMP_DIR" + +# ============================================================================= +# 测试用例 +# ============================================================================= + +# ---------- CT-01: 基本内容搜索 ---------- +echo "" +echo "--- CT-01: Basic content search ---" +mkdir -p "$TEST_DIR/basic" +echo "这是一份会议纪要,记录了项目进度和下一步计划。" > "$TEST_DIR/basic/meeting.txt" +echo "这是另一份关于财务报告的文件。" > "$TEST_DIR/basic/finance.txt" + +if wait_for_index "content" "会议纪要" "$TEST_DIR" "meeting.txt"; then + result=$(run_searcher "content" "会议纪要" "$TEST_DIR") || true + assert_found "CT-01: Basic content search finds file with keyword" "$result" "meeting.txt" + assert_not_found "CT-01: Basic content search excludes non-matching" "$result" "finance.txt" +else + skip "CT-01: Basic content search" "Index not ready within ${CONTENT_INDEX_TIMEOUT}s" +fi + +# ---------- CT-02: 英文内容搜索 ---------- +echo "" +echo "--- CT-02: English content search ---" +mkdir -p "$TEST_DIR/english" +echo "The project architecture document describes the system design." > "$TEST_DIR/english/architecture.txt" +echo "Some unrelated content here." > "$TEST_DIR/english/unrelated.txt" + +if wait_for_index "content" "architecture" "$TEST_DIR" "architecture.txt"; then + result=$(run_searcher "content" "architecture" "$TEST_DIR") || true + assert_found "CT-02: English content search finds matching file" "$result" "architecture.txt" +else + skip "CT-02: English content search" "Index not ready within ${CONTENT_INDEX_TIMEOUT}s" +fi + +# ---------- CT-03: Boolean AND 内容搜索 ---------- +echo "" +echo "--- CT-03: Boolean AND content search ---" +mkdir -p "$TEST_DIR/boolean" +echo "本项目包含数据库设计和接口设计两个部分。" > "$TEST_DIR/boolean/design_db.txt" +echo "本文档只讨论接口设计。" > "$TEST_DIR/boolean/design_api.txt" +echo "这份内容完全无关。" > "$TEST_DIR/boolean/unrelated.txt" + +if wait_for_index "content" "数据库" "$TEST_DIR" "design_db.txt"; then + result=$(run_searcher "content" '数据库,接口' "$TEST_DIR" "--query=boolean") || true + assert_found "CT-03a: Boolean AND finds file with both keywords" "$result" "design_db.txt" + assert_not_found "CT-03b: Boolean AND excludes file with only one keyword" "$result" "design_api.txt" +else + skip "CT-03: Boolean AND content search" "Index not ready within ${CONTENT_INDEX_TIMEOUT}s" +fi + +# ---------- CT-04: Boolean OR 内容搜索 ---------- +echo "" +echo "--- CT-04: Boolean OR content search ---" +if wait_for_index "content" "数据库" "$TEST_DIR" "design_db.txt"; then + result=$(run_searcher "content" '数据库|完全无关' "$TEST_DIR" "--query=boolean") || true + assert_found "CT-04a: Boolean OR finds file with first keyword" "$result" "design_db.txt" + assert_found "CT-04b: Boolean OR finds file with second keyword" "$result" "unrelated.txt" +else + skip "CT-04: Boolean OR content search" "Index not ready within ${CONTENT_INDEX_TIMEOUT}s" +fi + +# ---------- CT-05: 大小写敏感内容搜索 ---------- +echo "" +echo "--- CT-05: Case-sensitive content search ---" +mkdir -p "$TEST_DIR/case" +echo "The ERROR handling is important in production code." > "$TEST_DIR/case/upper.txt" +echo "The error handling module needs review." > "$TEST_DIR/case/lower.txt" + +if wait_for_index "content" "ERROR" "$TEST_DIR" "upper.txt"; then + # 默认不区分大小写 + result=$(run_searcher "content" "ERROR" "$TEST_DIR") || true + assert_found "CT-05a: Case-insensitive finds both files" "$result" "upper.txt" + + # 区分大小写 + # 索引服务当前对 --case-sensitive 不生效(filename 搜索同样失效),按已知限制处理: + # 探测条件为大写关键词精确命中且排除小写文件,两者同时满足才视为服务已支持 + result=$(run_searcher "content" "ERROR" "$TEST_DIR" "--case-sensitive") || true + if json_contains "$result" "upper.txt" && ! json_contains "$result" "lower.txt"; then + assert_found "CT-05b: Case-sensitive finds exact match" "$result" "upper.txt" + assert_not_found "CT-05c: Case-sensitive excludes lowercase" "$result" "lower.txt" + else + skip "CT-05b/c: Case-sensitive content search" "Known limitation: --case-sensitive not effective in index service" + fi +else + skip "CT-05: Case-sensitive content search" "Index not ready within ${CONTENT_INDEX_TIMEOUT}s" +fi + +# ---------- CT-06: 预览长度控制 ---------- +echo "" +echo "--- CT-06: Preview length control ---" +mkdir -p "$TEST_DIR/preview" +# 创建一个内容较长的文件(纯 bash 生成,避免依赖 python3) +padding_a=$(printf 'A%.0s' {1..500}) +padding_b=$(printf 'B%.0s' {1..500}) +echo "${padding_a} keyword_preview_test ${padding_b}" > "$TEST_DIR/preview/long_file.txt" + +if wait_for_index "content" "keyword_preview_test" "$TEST_DIR" "long_file.txt"; then + # 使用 verbose 输出获取预览 + result=$("$TEST_SEARCHER" --type=content -v "keyword_preview_test" "$TEST_DIR" 2>/dev/null) || true + TEST_TOTAL=$((TEST_TOTAL + 1)) + if [[ -n "$result" ]]; then + pass "CT-06: Verbose content search returns preview" + else + fail "CT-06: Verbose content search returns preview" "No output" + fi + + # 使用 max-preview 限制预览长度 + result=$("$TEST_SEARCHER" --type=content -v --max-preview=50 "keyword_preview_test" "$TEST_DIR" -j 2>/dev/null) || true + TEST_TOTAL=$((TEST_TOTAL + 1)) + if [[ -n "$result" ]]; then + pass "CT-06b: max-preview limits preview length" + else + fail "CT-06b: max-preview limits preview length" "No output" + fi +else + skip "CT-06: Preview length control" "Index not ready within ${CONTENT_INDEX_TIMEOUT}s" +fi + +# ---------- CT-07: JSON 输出验证 ---------- +echo "" +echo "--- CT-07: JSON output validation ---" +mkdir -p "$TEST_DIR/json" +echo "JSON output test content for validation." > "$TEST_DIR/json/json_test.txt" + +if wait_for_index "content" "JSON" "$TEST_DIR" "json_test.txt"; then + result=$(run_searcher "content" "JSON" "$TEST_DIR") || true + TEST_TOTAL=$((TEST_TOTAL + 1)) + # 检查 JSON 输出包含必要字段: + # results 为路径字符串数组、status.totalResults 计数、search 搜索元信息 + if echo "$result" | jq -e ' + (.results | type == "array") and + ((.results | length) > 0) and + (.results[0] | type == "string") and + (.status.totalResults | type == "number") and + (.search.searchType == "content") + ' &>/dev/null; then + pass "CT-07: JSON output contains required fields" + else + fail "CT-07: JSON output contains required fields" "Missing results/status.totalResults/search.searchType" + fi +else + skip "CT-07: JSON output validation" "Index not ready within ${CONTENT_INDEX_TIMEOUT}s" +fi + +# ---------- CT-08: 内容修改后搜索 ---------- +echo "" +echo "--- CT-08: Search after content modification ---" +mkdir -p "$TEST_DIR/modify" +echo "原始内容,没有任何特殊关键词。" > "$TEST_DIR/modify/modify_test.txt" + +if wait_for_index "content" "原始内容" "$TEST_DIR" "modify_test.txt"; then + # 修改文件内容,添加新关键词 + echo "修改后的内容包含独特关键词 unique_keyword_xyz。" > "$TEST_DIR/modify/modify_test.txt" + + # 等待索引更新 + local_elapsed=0 + modified_ok=false + while [[ $local_elapsed -lt $CONTENT_INDEX_TIMEOUT ]]; do + result=$(run_searcher "content" "unique_keyword_xyz" "$TEST_DIR") || true + if json_contains "$result" "modify_test.txt"; then + modified_ok=true + break + fi + sleep "$INDEX_POLL_INTERVAL" + local_elapsed=$((local_elapsed + INDEX_POLL_INTERVAL)) + done + + TEST_TOTAL=$((TEST_TOTAL + 1)) + if $modified_ok; then + pass "CT-08: Modified content found by new keyword" + else + fail "CT-08: Modified content found by new keyword" "Index may not have updated" + fi +else + skip "CT-08: Search after content modification" "Index not ready within ${CONTENT_INDEX_TIMEOUT}s" +fi + +# ---------- CT-09: 内容删除后搜索 ---------- +echo "" +echo "--- CT-09: Search after content deletion ---" +mkdir -p "$TEST_DIR/delete" +echo "这份文档包含待删除关键词 delete_marker_test。" > "$TEST_DIR/delete/delete_test.txt" + +if wait_for_index "content" "delete_marker_test" "$TEST_DIR" "delete_test.txt"; then + # 删除文件内容中的关键词(重写为不含关键词的内容) + echo "这份文档的内容已被替换,不再包含原关键词。" > "$TEST_DIR/delete/delete_test.txt" + + # 等待索引更新 + local_elapsed=0 + deleted_ok=false + while [[ $local_elapsed -lt $CONTENT_INDEX_TIMEOUT ]]; do + result=$(run_searcher "content" "delete_marker_test" "$TEST_DIR") || true + if ! json_contains "$result" "delete_test.txt"; then + deleted_ok=true + break + fi + sleep "$INDEX_POLL_INTERVAL" + local_elapsed=$((local_elapsed + INDEX_POLL_INTERVAL)) + done + + TEST_TOTAL=$((TEST_TOTAL + 1)) + if $deleted_ok; then + pass "CT-09: Deleted keyword no longer found in search" + else + fail "CT-09: Deleted keyword no longer found in search" "Index may not have updated" + fi +else + skip "CT-09: Search after content deletion" "Index not ready within ${CONTENT_INDEX_TIMEOUT}s" +fi + +# ---------- CT-10: 隐藏文件内容搜索 ---------- +echo "" +echo "--- CT-10: Hidden file content search ---" +mkdir -p "$TEST_DIR/hidden" +echo "Hidden file content with unique hidden_marker_keyword." > "$TEST_DIR/hidden/.hidden_config.txt" +echo "Visible file content with unique hidden_marker_keyword." > "$TEST_DIR/hidden/visible_config.txt" + +if wait_for_index "content" "hidden_marker_keyword" "$TEST_DIR" "visible_config.txt"; then + # 默认不搜索隐藏文件 + result=$(run_searcher "content" "hidden_marker_keyword" "$TEST_DIR") || true + assert_found "CT-10a: Default content search finds visible file" "$result" "visible_config.txt" + assert_not_found "CT-10b: Default content search excludes hidden file" "$result" ".hidden_config.txt" + + # 包含隐藏文件 + result=$(run_searcher "content" "hidden_marker_keyword" "$TEST_DIR" "--include-hidden") || true + assert_found "CT-10c: --include-hidden finds hidden file content" "$result" ".hidden_config.txt" +else + skip "CT-10: Hidden file content search" "Index not ready within ${CONTENT_INDEX_TIMEOUT}s" +fi + +# ---------- CT-11: 多文件内容搜索 ---------- +echo "" +echo "--- CT-11: Multiple files content search ---" +mkdir -p "$TEST_DIR/multi" +echo "这个文件包含共享关键词 common_keyword_multi。" > "$TEST_DIR/multi/file1.txt" +echo "另一个文件也包含共享关键词 common_keyword_multi。" > "$TEST_DIR/multi/file2.txt" +echo "这个文件没有目标关键词。" > "$TEST_DIR/multi/file3.txt" + +if wait_for_index "content" "common_keyword_multi" "$TEST_DIR" "file2.txt"; then + result=$(run_searcher "content" "common_keyword_multi" "$TEST_DIR") || true + assert_found "CT-11a: Multi-file search finds file1" "$result" "file1.txt" + assert_found "CT-11b: Multi-file search finds file2" "$result" "file2.txt" + assert_not_found "CT-11c: Multi-file search excludes non-matching file3" "$result" "file3.txt" +else + skip "CT-11: Multiple files content search" "Index not ready within ${CONTENT_INDEX_TIMEOUT}s" +fi + +# ---------- CT-12: Boolean AND 使用 & 分隔符内容搜索 ---------- +echo "" +echo "--- CT-12: Boolean AND with & separator content search ---" +if wait_for_index "content" "数据库" "$TEST_DIR" "design_db.txt"; then + result=$(run_searcher "content" '数据库&接口' "$TEST_DIR" "--query=boolean") || true + assert_found "CT-12: Boolean AND with & finds file with both keywords" "$result" "design_db.txt" +else + skip "CT-12: Boolean AND with & separator content search" "Index not ready within ${CONTENT_INDEX_TIMEOUT}s" +fi + + +# ---------- CT-13: 压力测试 — 1000 个文件内容搜索 ---------- +echo "" +echo "--- CT-13: Stress test — 1000 files content search ---" +STRESS_COUNT="${STRESS_FILE_COUNT:-1000}" +STRESS_DIR="$TEST_DIR/stress_content" +mkdir -p "$STRESS_DIR" + +# 统计去重后的 stress 文件数 +# 注意:突发写入窗口内索引快照可能同时含重复与缺失条目(最终一致),必须按唯一路径计数 +count_stress_files() { + if command -v jq &>/dev/null; then + echo "$1" | jq -r '[.results[]? | select(test("stress_file_[0-9]{4}\\.txt$"))] | unique | length' 2>/dev/null || echo 0 + else + echo "$1" | grep -oE 'stress_file_[0-9]{4}\.txt' | sort -u | wc -l + fi +} + +# 创建 1000 个文件,每个文件包含独特 keyword + 公共 batch keyword +echo "Creating $STRESS_COUNT files with unique content keywords..." +for i in $(seq 1 "$STRESS_COUNT"); do + printf -v idx "%04d" "$i" + echo "stress_ct_batch stress_ct_${idx}" > "$STRESS_DIR/stress_file_${idx}.txt" +done + +# 等待索引:轮询去重后的文件数量,直到全部文件入索引或超时 +# 注意:不能用 totalResults 或"最后一个文件已索引"作为全部完成的依据 +echo "Waiting for index (timeout: ${STRESS_CONTENT_INDEX_TIMEOUT}s)..." +elapsed=0 +result="" +found_count=0 +while [[ $elapsed -lt $STRESS_CONTENT_INDEX_TIMEOUT ]]; do + result=$(run_searcher "content" "stress_ct_batch" "$STRESS_DIR" "--max-results=$((STRESS_COUNT + 100))") || true + found_count=$(count_stress_files "$result") + if [[ "$found_count" -eq "$STRESS_COUNT" ]]; then + break + fi + sleep "$INDEX_POLL_INTERVAL" + elapsed=$((elapsed + INDEX_POLL_INTERVAL)) +done + +TEST_TOTAL=$((TEST_TOTAL + 1)) +if [[ "$found_count" -eq "$STRESS_COUNT" ]]; then + pass "CT-13: All $STRESS_COUNT files found by content keyword" +else + fail "CT-13: Stress test — $found_count/$STRESS_COUNT files found" "$((STRESS_COUNT - found_count)) files not in search results" +fi + +if [[ "$found_count" -eq "$STRESS_COUNT" ]]; then + # 抽样验证几个单独的独特 keyword + for sample_idx in 0001 0250 0500 0750 1000; do + sample_result=$(run_searcher "content" "stress_ct_${sample_idx}" "$STRESS_DIR") || true + assert_found "CT-13: Spot check stress_ct_${sample_idx}" "$sample_result" "stress_file_${sample_idx}.txt" + done +fi + +# 清理压力测试文件 +rm -rf "$STRESS_DIR" +echo "Cleaned up stress test files." + +# ============================================================================= +# 输出汇总 +# ============================================================================= +print_summary diff --git a/tests/dfm-search/scripts/test_filename_search.sh b/tests/dfm-search/scripts/test_filename_search.sh new file mode 100755 index 00000000..8f04d748 --- /dev/null +++ b/tests/dfm-search/scripts/test_filename_search.sh @@ -0,0 +1,440 @@ +#!/bin/bash +# SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +# +# SPDX-License-Identifier: GPL-3.0-or-later + +# ============================================================================= +# test_filename_search.sh — dfm-search 文件名搜索测试脚本 +# +# 在 ~/Documents 下创建临时目录,科学地创建/删除/修改/移动文件(含目录、 +# 隐藏文件),使用 dfm-searcher 的文件名搜索功能验证: +# - 基本搜索、通配符搜索 +# - Boolean AND/OR 搜索 +# - 拼音全拼/首字母/布尔组合搜索 +# - 大小写敏感、隐藏文件、max-results +# - 文件增删改移后搜索 +# - 目录搜索、扩展名过滤 +# - 实时搜索 +# +# 用法: ./test_filename_search.sh +# 环境变量: +# DFM_SEARCHER — dfm-searcher 可执行文件路径(默认: dfm-searcher) +# INDEX_WAIT_TIMEOUT_FILENAME — 文件名索引等待超时秒数(默认: 30) +# INDEX_POLL_INTERVAL — 索引轮询间隔秒数(默认: 2) +# ============================================================================= + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/common.sh" + +# 陷阱:确保退出时清理 +trap cleanup_test_env EXIT + +echo "=========================================" +echo " dfm-search 文件名搜索测试" +echo "=========================================" +echo "" + +# 前置条件检查 +if ! check_prerequisites; then + echo "Prerequisites not met. Exiting." + exit 1 +fi + +# 初始化测试环境 +setup_test_env "fn-search-test" +TEST_DIR="$TEST_TEMP_DIR" + +# ============================================================================= +# 测试用例 +# ============================================================================= + +# ---------- FT-01: 基本文件名搜索 ---------- +echo "" +echo "--- FT-01: Basic filename search ---" +mkdir -p "$TEST_DIR/reports" +echo "content" > "$TEST_DIR/reports/工作报告.txt" +echo "content" > "$TEST_DIR/reports/meeting_notes.txt" + +if wait_for_index "filename" "工作报告" "$TEST_DIR" "工作报告.txt"; then + result=$(run_searcher "filename" "工作报告" "$TEST_DIR") || true + assert_found "FT-01: Basic search finds file by Chinese keyword" "$result" "工作报告.txt" +else + skip "FT-01: Basic filename search" "Index not ready within ${FILENAME_INDEX_TIMEOUT}s" +fi + +# ---------- FT-02: 英文关键词搜索 ---------- +echo "" +echo "--- FT-02: English keyword search ---" +echo "content" > "$TEST_DIR/meeting_notes.txt" + +if wait_for_index "filename" "meeting" "$TEST_DIR" "meeting_notes.txt"; then + result=$(run_searcher "filename" "meeting" "$TEST_DIR") || true + assert_found "FT-02: English keyword search finds file" "$result" "meeting_notes.txt" +else + skip "FT-02: English keyword search" "Index not ready within ${FILENAME_INDEX_TIMEOUT}s" +fi + +# ---------- FT-03: 通配符搜索 (*) ---------- +echo "" +echo "--- FT-03: Wildcard search (*) ---" +mkdir -p "$TEST_DIR/wildcard" +echo "content" > "$TEST_DIR/wildcard/report2026.txt" +echo "content" > "$TEST_DIR/wildcard/report2025.txt" +echo "content" > "$TEST_DIR/wildcard/summary.txt" + +if wait_for_index "filename" "report" "$TEST_DIR" "report2026.txt"; then + result=$(run_searcher "filename" "report*" "$TEST_DIR" "--wildcard") || true + assert_found "FT-03a: Wildcard '*' finds report2026" "$result" "report2026.txt" + assert_found "FT-03b: Wildcard '*' finds report2025" "$result" "report2025.txt" +else + skip "FT-03: Wildcard search (*)" "Index not ready within ${FILENAME_INDEX_TIMEOUT}s" +fi + +# ---------- FT-04: 通配符搜索 (?) ---------- +echo "" +echo "--- FT-04: Wildcard search (?) ---" +if wait_for_index "filename" "report" "$TEST_DIR" "report2026.txt"; then + result=$(run_searcher "filename" "report????.txt" "$TEST_DIR" "--wildcard") || true + assert_found "FT-04: Wildcard '?' matches single chars" "$result" "report2026.txt" +else + skip "FT-04: Wildcard search (?)" "Index not ready within ${FILENAME_INDEX_TIMEOUT}s" +fi + +# ---------- FT-05: Boolean AND 搜索 ---------- +echo "" +echo "--- FT-05: Boolean AND search ---" +mkdir -p "$TEST_DIR/boolean" +echo "content" > "$TEST_DIR/boolean/报告2026.txt" +echo "content" > "$TEST_DIR/boolean/报告2025.txt" +echo "content" > "$TEST_DIR/boolean/总结2026.txt" + +if wait_for_index "filename" "报告" "$TEST_DIR" "报告2026.txt"; then + result=$(run_searcher "filename" "报告,2026" "$TEST_DIR" "--query=boolean") || true + assert_found "FT-05: Boolean AND finds matching file" "$result" "报告2026.txt" + assert_not_found "FT-05: Boolean AND excludes non-matching" "$result" "总结2026.txt" +else + skip "FT-05: Boolean AND search" "Index not ready within ${FILENAME_INDEX_TIMEOUT}s" +fi + +# ---------- FT-06: Boolean OR 搜索 ---------- +echo "" +echo "--- FT-06: Boolean OR search ---" +if wait_for_index "filename" "报告" "$TEST_DIR" "报告2026.txt"; then + result=$(run_searcher "filename" '报告|总结' "$TEST_DIR" "--query=boolean") || true + assert_found "FT-06a: Boolean OR finds 报告" "$result" "报告2026.txt" + assert_found "FT-06b: Boolean OR finds 总结" "$result" "总结2026.txt" +else + skip "FT-06: Boolean OR search" "Index not ready within ${FILENAME_INDEX_TIMEOUT}s" +fi + +# ---------- FT-07: 拼音全拼搜索 ---------- +echo "" +echo "--- FT-07: Pinyin full search ---" +mkdir -p "$TEST_DIR/pinyin" +echo "content" > "$TEST_DIR/pinyin/报告.txt" + +if wait_for_index "filename" "报告" "$TEST_DIR" "报告.txt"; then + result=$(run_searcher "filename" "baogao" "$TEST_DIR" "--pinyin") || true + assert_found "FT-07: Pinyin full 'baogao' finds 报告" "$result" "报告.txt" +else + skip "FT-07: Pinyin full search" "Index not ready within ${FILENAME_INDEX_TIMEOUT}s" +fi + +# ---------- FT-08: 拼音首字母搜索 ---------- +echo "" +echo "--- FT-08: Pinyin acronym search ---" +if wait_for_index "filename" "报告" "$TEST_DIR" "报告.txt"; then + result=$(run_searcher "filename" "bg" "$TEST_DIR" "--pinyin-acronym") || true + assert_found "FT-08: Pinyin acronym 'bg' finds 报告" "$result" "报告.txt" +else + skip "FT-08: Pinyin acronym search" "Index not ready within ${FILENAME_INDEX_TIMEOUT}s" +fi + +# ---------- FT-09: 拼音 + Boolean 组合搜索 ---------- +echo "" +echo "--- FT-09: Pinyin + Boolean combined search ---" +echo "content" > "$TEST_DIR/pinyin/饼干.txt" + +# 注意:等待探测必须针对本用例新建的文件(饼干.txt), +# 若探测已索引的旧文件(如 报告.txt),会在新文件入索引前就放行,造成竞态 +if wait_for_index "filename" "饼干" "$TEST_DIR" "饼干.txt"; then + result=$(run_searcher "filename" "baogao|binggan" "$TEST_DIR" "--pinyin" "--query=boolean") || true + assert_found "FT-09a: Pinyin+Boolean finds 报告(baogao)" "$result" "报告.txt" + assert_found "FT-09b: Pinyin+Boolean finds 饼干(binggan)" "$result" "饼干.txt" +else + skip "FT-09: Pinyin + Boolean combined search" "Index not ready within ${FILENAME_INDEX_TIMEOUT}s" +fi + +# ---------- FT-10: 大小写敏感搜索 ---------- +echo "" +echo "--- FT-10: Case-sensitive search ---" +mkdir -p "$TEST_DIR/case" +echo "content" > "$TEST_DIR/case/README.md" +echo "content" > "$TEST_DIR/case/readme.txt" + +if wait_for_index "filename" "README" "$TEST_DIR" "README.md"; then + # 默认不区分大小写:应都能找到 + result=$(run_searcher "filename" "README" "$TEST_DIR") || true + assert_found "FT-10a: Case-insensitive finds README.md" "$result" "README.md" + + # 区分大小写:只应找到 README.md,不应找到 readme.txt + # 索引服务当前对 --case-sensitive 不生效(content 搜索同样失效),按已知限制处理: + # 若探测到大写关键词能精确命中,说明服务已支持,自动执行断言 + result=$(run_searcher "filename" "README" "$TEST_DIR" "--case-sensitive") || true + if json_contains "$result" "README.md"; then + assert_found "FT-10b: Case-sensitive finds README.md" "$result" "README.md" + assert_not_found "FT-10c: Case-sensitive excludes readme.txt" "$result" "readme.txt" + else + skip "FT-10b/c: Case-sensitive search" "Known limitation: --case-sensitive not effective in index service" + fi +else + skip "FT-10: Case-sensitive search" "Index not ready within ${FILENAME_INDEX_TIMEOUT}s" +fi + +# ---------- FT-11: 隐藏文件搜索 ---------- +echo "" +echo "--- FT-11: Hidden file search ---" +mkdir -p "$TEST_DIR/hidden" +echo "content" > "$TEST_DIR/hidden/.config" +echo "content" > "$TEST_DIR/hidden/visible.txt" + +if wait_for_index "filename" "visible" "$TEST_DIR" "visible.txt"; then + # 默认不搜索隐藏文件 + result=$(run_searcher "filename" "config" "$TEST_DIR") || true + assert_not_found "FT-11a: Default excludes hidden files" "$result" ".config" + + # 包含隐藏文件 + result=$(run_searcher "filename" "config" "$TEST_DIR" "--include-hidden") || true + assert_found "FT-11b: --include-hidden finds hidden files" "$result" ".config" +else + skip "FT-11: Hidden file search" "Index not ready within ${FILENAME_INDEX_TIMEOUT}s" +fi + +# ---------- FT-12: max-results 限制 ---------- +echo "" +echo "--- FT-12: Max results limit ---" +mkdir -p "$TEST_DIR/maxresults" +for i in $(seq 1 10); do + echo "content" > "$TEST_DIR/maxresults/result_file_${i}.txt" +done + +if wait_for_index "filename" "result_file" "$TEST_DIR" "result_file_10.txt"; then + result=$(run_searcher "filename" "result_file" "$TEST_DIR" "--max-results=3") || true + assert_le_count "FT-12: max-results limits to 3" "$result" 3 +else + skip "FT-12: Max results limit" "Index not ready within ${FILENAME_INDEX_TIMEOUT}s" +fi + +# ---------- FT-13: 新建文件后搜索 ---------- +echo "" +echo "--- FT-13: Search after file creation ---" +echo "content" > "$TEST_DIR/newly_created_file.txt" + +if wait_for_index "filename" "newly_created" "$TEST_DIR" "newly_created_file.txt"; then + result=$(run_searcher "filename" "newly_created" "$TEST_DIR") || true + assert_found "FT-13: Search finds newly created file" "$result" "newly_created_file.txt" +else + skip "FT-13: Search after file creation" "Index not ready within ${FILENAME_INDEX_TIMEOUT}s" +fi + +# ---------- FT-14: 删除文件后搜索 ---------- +echo "" +echo "--- FT-14: Search after file deletion ---" +echo "content" > "$TEST_DIR/to_be_deleted.txt" +if wait_for_index "filename" "to_be_deleted" "$TEST_DIR" "to_be_deleted.txt"; then + rm "$TEST_DIR/to_be_deleted.txt" + # 等待索引更新 + local_elapsed=0 + deleted_ok=false + while [[ $local_elapsed -lt $FILENAME_INDEX_TIMEOUT ]]; do + result=$(run_searcher "filename" "to_be_deleted" "$TEST_DIR") || true + if ! json_contains "$result" "to_be_deleted.txt"; then + deleted_ok=true + break + fi + sleep "$INDEX_POLL_INTERVAL" + local_elapsed=$((local_elapsed + INDEX_POLL_INTERVAL)) + done + + TEST_TOTAL=$((TEST_TOTAL + 1)) + if $deleted_ok; then + pass "FT-14: Deleted file no longer in search results" + else + fail "FT-14: Deleted file still in search results" "Index may not have updated" + fi +else + skip "FT-14: Search after file deletion" "Index not ready within ${FILENAME_INDEX_TIMEOUT}s" +fi + +# ---------- FT-15: 修改文件名后搜索 ---------- +echo "" +echo "--- FT-15: Search after file rename ---" +echo "content" > "$TEST_DIR/before_rename.txt" +if wait_for_index "filename" "before_rename" "$TEST_DIR" "before_rename.txt"; then + mv "$TEST_DIR/before_rename.txt" "$TEST_DIR/after_rename.txt" + # 等待索引更新 + local_elapsed=0 + renamed_ok=false + while [[ $local_elapsed -lt $FILENAME_INDEX_TIMEOUT ]]; do + result=$(run_searcher "filename" "after_rename" "$TEST_DIR") || true + if json_contains "$result" "after_rename.txt"; then + renamed_ok=true + break + fi + sleep "$INDEX_POLL_INTERVAL" + local_elapsed=$((local_elapsed + INDEX_POLL_INTERVAL)) + done + + TEST_TOTAL=$((TEST_TOTAL + 1)) + if $renamed_ok; then + pass "FT-15: Renamed file found by new name" + else + fail "FT-15: Renamed file not found by new name" "Index may not have updated" + fi +else + skip "FT-15: Search after file rename" "Index not ready within ${FILENAME_INDEX_TIMEOUT}s" +fi + +# ---------- FT-16: 移动文件后搜索 ---------- +echo "" +echo "--- FT-16: Search after file move ---" +mkdir -p "$TEST_DIR/move_src" "$TEST_DIR/move_dst" +echo "content" > "$TEST_DIR/move_src/movable_file.txt" +if wait_for_index "filename" "movable_file" "$TEST_DIR" "movable_file.txt"; then + mv "$TEST_DIR/move_src/movable_file.txt" "$TEST_DIR/move_dst/movable_file.txt" + # 等待索引更新 + local_elapsed=0 + moved_ok=false + while [[ $local_elapsed -lt $FILENAME_INDEX_TIMEOUT ]]; do + result=$(run_searcher "filename" "movable_file" "$TEST_DIR/move_dst") || true + if json_contains "$result" "movable_file.txt"; then + moved_ok=true + break + fi + sleep "$INDEX_POLL_INTERVAL" + local_elapsed=$((local_elapsed + INDEX_POLL_INTERVAL)) + done + + TEST_TOTAL=$((TEST_TOTAL + 1)) + if $moved_ok; then + pass "FT-16: Moved file found in new location" + else + fail "FT-16: Moved file not found in new location" "Index may not have updated" + fi +else + skip "FT-16: Search after file move" "Index not ready within ${FILENAME_INDEX_TIMEOUT}s" +fi + +# ---------- FT-17: 目录搜索 ---------- +echo "" +echo "--- FT-17: Directory search ---" +mkdir -p "$TEST_DIR/project_alpha" + +if wait_for_index "filename" "project_alpha" "$TEST_DIR" "project_alpha"; then + result=$(run_searcher "filename" "project_alpha" "$TEST_DIR") || true + assert_found "FT-17: Search finds directory by name" "$result" "project_alpha" +else + skip "FT-17: Directory search" "Index not ready within ${FILENAME_INDEX_TIMEOUT}s" +fi + +# ---------- FT-19: 扩展名过滤 ---------- +echo "" +echo "--- FT-19: Extension filter ---" +mkdir -p "$TEST_DIR/extfilter" +echo "content" > "$TEST_DIR/extfilter/document.txt" +echo "content" > "$TEST_DIR/extfilter/document.pdf" +echo "content" > "$TEST_DIR/extfilter/document.docx" + +if wait_for_index "filename" "document" "$TEST_DIR" "document.docx"; then + result=$(run_searcher "filename" "document" "$TEST_DIR" "--file-extensions=txt,pdf") || true + assert_found "FT-19a: Extension filter finds .txt" "$result" "document.txt" + assert_found "FT-19b: Extension filter finds .pdf" "$result" "document.pdf" + assert_not_found "FT-19c: Extension filter excludes .docx" "$result" "document.docx" +else + skip "FT-19: Extension filter" "Index not ready within ${FILENAME_INDEX_TIMEOUT}s" +fi + +# ---------- FT-18: 实时搜索(文件名) ---------- +echo "" +echo "--- FT-18: Realtime filename search ---" +echo "content" > "$TEST_DIR/realtime_test_file.txt" +# 实时搜索不需要等待索引 +result=$(run_searcher "filename" "realtime_test_file" "$TEST_DIR" "--method=realtime") || true +assert_found "FT-18: Realtime search finds file without index" "$result" "realtime_test_file.txt" + +# ---------- FT-20: Boolean AND 使用 & 分隔符 ---------- +echo "" +echo "--- FT-20: Boolean AND with & separator ---" +if wait_for_index "filename" "报告" "$TEST_DIR" "报告2026.txt"; then + result=$(run_searcher "filename" '报告&2026' "$TEST_DIR" "--query=boolean") || true + assert_found "FT-20: Boolean AND with & finds matching file" "$result" "报告2026.txt" +else + skip "FT-20: Boolean AND with & separator" "Index not ready within ${FILENAME_INDEX_TIMEOUT}s" +fi + + +# ---------- FT-21: 压力测试 — 1000 个文件名搜索 ---------- +echo "" +echo "--- FT-21: Stress test — 1000 files filename search ---" +STRESS_COUNT="${STRESS_FILE_COUNT:-1000}" +STRESS_DIR="$TEST_DIR/stress_filename" +mkdir -p "$STRESS_DIR" + +# 统计去重后的 stress 文件数 +# 注意:突发写入窗口内索引快照可能同时含重复与缺失条目(最终一致),必须按唯一路径计数 +count_stress_files() { + if command -v jq &>/dev/null; then + echo "$1" | jq -r '[.results[]? | select(test("stress_fn_[0-9]{4}\\.txt$"))] | unique | length' 2>/dev/null || echo 0 + else + echo "$1" | grep -oE 'stress_fn_[0-9]{4}\.txt' | sort -u | wc -l + fi +} + +# 创建 1000 个文件,每个文件名包含独特 keyword +echo "Creating $STRESS_COUNT files with unique filename keywords..." +for i in $(seq 1 "$STRESS_COUNT"); do + printf -v idx "%04d" "$i" + echo "content" > "$STRESS_DIR/stress_fn_${idx}.txt" +done + +# 等待索引:轮询去重后的文件数量,直到全部文件入索引或超时 +# 注意:不能用 totalResults 或"最后一个文件已索引"作为全部完成的依据 +echo "Waiting for index (timeout: ${STRESS_FILENAME_INDEX_TIMEOUT}s)..." +elapsed=0 +result="" +found_count=0 +while [[ $elapsed -lt $STRESS_FILENAME_INDEX_TIMEOUT ]]; do + result=$(run_searcher "filename" "stress_fn" "$STRESS_DIR" "--max-results=$((STRESS_COUNT + 100))") || true + found_count=$(count_stress_files "$result") + if [[ "$found_count" -eq "$STRESS_COUNT" ]]; then + break + fi + sleep "$INDEX_POLL_INTERVAL" + elapsed=$((elapsed + INDEX_POLL_INTERVAL)) +done + +TEST_TOTAL=$((TEST_TOTAL + 1)) +if [[ "$found_count" -eq "$STRESS_COUNT" ]]; then + pass "FT-21: All $STRESS_COUNT files found by prefix keyword" +else + fail "FT-21: Stress test — $found_count/$STRESS_COUNT files found" "$((STRESS_COUNT - found_count)) files not in search results" +fi + +if [[ "$found_count" -eq "$STRESS_COUNT" ]]; then + # 抽样验证几个单独的 keyword + for sample_idx in 0001 0250 0500 0750 1000; do + sample_result=$(run_searcher "filename" "stress_fn_${sample_idx}" "$STRESS_DIR") || true + assert_found "FT-21: Spot check stress_fn_${sample_idx}" "$sample_result" "stress_fn_${sample_idx}.txt" + done +fi + +# 清理压力测试文件 +rm -rf "$STRESS_DIR" +echo "Cleaned up stress test files." + +# ============================================================================= +# 输出汇总 +# ============================================================================= +print_summary