4 Commits
v0.1.0 ... dev

Author SHA1 Message Date
ren
c9a9632ccc feat: 优化项目结构
All checks were successful
CI / 测试 (push) Successful in 20s
2025-10-21 12:05:15 +08:00
ren
4174e14517 feat: 更新项目构建脚本
Some checks failed
CI / 测试 (push) Failing after 58s
2025-10-21 11:52:04 +08:00
ren
343f295a13 release: 更新版本 0.1.1
Some checks failed
Usage Examples / Multiple Files Example (push) Successful in 12s
Usage Examples / Different Algorithms Example (push) Successful in 11s
Usage Examples / Error Handling Example (push) Successful in 9s
Usage Examples / Basic Usage Example (push) Failing after 3s
Usage Examples / Pattern Matching Example (push) Successful in 12s
2025-10-14 16:19:35 +08:00
ren
811d37052b fix: 修复通配符匹配问题 2025-10-14 16:17:48 +08:00
6 changed files with 360 additions and 192 deletions

View File

@@ -2,45 +2,83 @@ name: CI
on: on:
push: push:
branches: [main] branches: [dev, release, master]
pull_request: pull_request:
branches: [main] branches: [dev, release, master]
jobs: jobs:
lint-and-test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Lint and Test name: 测试
steps: steps:
- name: Checkout - name: 准备 | 签出代码
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup Node.js - name: 准备 | 配置开发环境
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '20' node-version: '20'
- name: Setup pnpm - name: 准备 | 获取包管理器版本
uses: pnpm/action-setup@v4 run: |
PNPM_VERSION=$(grep -o '"packageManager": "[^"]*"' package.json | cut -d'@' -f2 | tr -d '"')
echo "PNPM_VERSION=$PNPM_VERSION" >> $GITHUB_ENV
echo "✅ 包管理器版本为 pnpm@$PNPM_VERSION"
- name: 准备 | 配置全局缓存
uses: actions/cache@v3
with: with:
version: 9 path: ~/.npm
key: ${{ runner.os }}-npm-pnpm-${{ env.PNPM_VERSION }}
restore-keys: |
${{ runner.os }}-npm-pnpm-
- name: Install dependencies - name: 准备 | 安装包管理器
run: pnpm install --frozen-lockfile
- name: Check JavaScript Syntax
run: | run: |
echo "🔍 Checking JavaScript syntax..." npm install -g pnpm
node -c index.js pnpm --version
echo "✅ All JavaScript files have valid syntax" echo "✅ 包管理器安装成功"
- name: Run Tests - name: 准备 | 获取项目依赖哈希
run: | id: files-hash
echo "🧪 Running test suite..."
pnpm test
- name: Test Action Execution
uses: ./ uses: ./
with: with:
files: 'examples/*' files: '**/pnpm-lock.yaml'
algorithm: 'sha256'
- name: 准备 | 配置依赖缓存
uses: actions/cache@v3
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-store-${{ steps.files-hash.outputs.hash }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: 准备 | 安装依赖
run: |
pnpm install --frozen-lockfile
echo "✅ 依赖安装完成"
- name: 测试 | 执行语法检查
run: |
echo "🔍 执行 JavaScript 语法检查..."
node -c src/index.js
echo "✅ 语法检查通过"
- name: 测试 | 执行 Lint 检查
run: |
echo "🔍 执行 Lint 检查..."
pnpm run lint
echo "✅ Lint 检查通过"
- name: 测试 | 执行格式化检查
run: |
echo "🔍 执行格式化检查..."
pnpm run format:check
echo "✅ 格式化检查通过"
- name: 测试 | 执行测试
run: |
echo "🧪 执行测试..."
pnpm run test
echo "✅ 测试通过"

View File

@@ -72,10 +72,10 @@ jobs:
## 输入参数 ## 输入参数
| 输入参数 | 描述 | 必需 | 默认值 | | 输入参数 | 描述 | 必需 | 默认值 |
| ----------------- | ------------------------------------------- | ----- | -------- | | ----------- | ------------------------------------------- | ----- | -------- |
| `files` | 文件路径或 glob 模式(每行一个) | ✅ 是 | - | | `files` | 文件路径或 glob 模式(每行一个) | ✅ 是 | - |
| `algorithm` | 哈希算法:`md5``sha1``sha256``sha512` | ❌ 否 | `sha256` | | `algorithm` | 哈希算法:`md5``sha1``sha256``sha512` | ❌ 否 | `sha256` |
### 文件模式 ### 文件模式

View File

@@ -22,4 +22,4 @@ outputs:
runs: runs:
using: 'node20' using: 'node20'
main: 'index.js' main: 'src/index.js'

View File

@@ -1,16 +1,16 @@
{ {
"name": "files-hash-action", "name": "files-hash-action",
"version": "0.1.0", "version": "0.1.1",
"description": "A lightweight GitHub Action to calculate hash of multiple files", "description": "A lightweight Gitea Action to calculate hash of multiple files",
"main": "index.js", "main": "src/index.js",
"scripts": { "scripts": {
"test": "vitest run", "test": "vitest run",
"test:watch": "vitest watch", "test:watch": "vitest watch",
"test:coverage": "vitest run --coverage", "test:coverage": "vitest run --coverage",
"lint": "eslint .", "lint": "eslint ./src",
"lint:fix": "eslint . --fix", "lint:fix": "eslint ./src --fix",
"format": "prettier --write .", "format": "prettier --write ./src",
"format:check": "prettier --check .", "format:check": "prettier --check ./src",
"check": "pnpm run lint && pnpm run format:check", "check": "pnpm run lint && pnpm run format:check",
"preinstall": "npx only-allow pnpm" "preinstall": "npx only-allow pnpm"
}, },
@@ -33,17 +33,9 @@
"sha256", "sha256",
"sha512" "sha512"
], ],
"author": "Files Hash Action", "packageManager": "pnpm@10.18.3",
"license": "MIT",
"engines": { "engines": {
"node": ">=20" "node": ">=20",
}, "pnpm": ">=10"
"repository": { }
"type": "git",
"url": "https://github.com/your-username/files-hash-action.git"
},
"bugs": {
"url": "https://github.com/your-username/files-hash-action/issues"
},
"homepage": "https://github.com/your-username/files-hash-action#readme"
} }

View File

@@ -3,6 +3,31 @@ const fsSync = require('fs');
const path = require('path'); const path = require('path');
const crypto = require('crypto'); const crypto = require('crypto');
/**
* 工具函数安全执行异步操作忽略错误
*
* @template T
* @param {() => Promise<T>} fn - 要执行的异步函数
* @param {T} defaultValue - 出错时的默认值
* @returns {Promise<T>} 函数执行结果或默认值
*/
async function safeExecute(fn, defaultValue = undefined) {
try {
return await fn();
} catch (error) {
return defaultValue;
}
}
/**
* 工具函数安全执行同步操作忽略错误
*
* @template T
* @param {() => T} fn - 要执行的同步函数
* @param {T} defaultValue - 出错时的默认值
* @returns {T} 函数执行结果或默认值
*/
/** /**
* 错误类型常量. * 错误类型常量.
* *
@@ -191,8 +216,10 @@ class FileDiscovery {
const files = new Set(); const files = new Set();
for (const pattern of patterns) { for (const pattern of patterns) {
const matchedFiles = await this.expandGlob(pattern.trim()); await safeExecute(async () => {
matchedFiles.forEach(file => files.add(file)); const matchedFiles = await this.expandGlob(pattern.trim());
matchedFiles.forEach(file => files.add(file));
});
} }
return Array.from(files).sort(); return Array.from(files).sort();
@@ -240,17 +267,16 @@ class FileDiscovery {
async expandGlob(pattern) { async expandGlob(pattern) {
// 如果是普通文件路径,直接返回 // 如果是普通文件路径,直接返回
if (!pattern.includes('*') && !pattern.includes('?')) { if (!pattern.includes('*') && !pattern.includes('?')) {
try { const stats = await safeExecute(() => fs.stat(pattern));
const stats = await fs.stat(pattern); if (stats) {
if (stats.isFile()) { if (stats.isFile()) {
return [pattern]; return [pattern];
} else if (stats.isDirectory()) { } else if (stats.isDirectory()) {
// 如果是目录,返回目录下所有文件 // 如果是目录,返回目录下所有文件
return await this.getAllFilesInDirectory(pattern); return await this.getAllFilesInDirectory(pattern);
} }
} catch (error) {
return [];
} }
return [];
} }
// 处理 glob 模式 // 处理 glob 模式
@@ -267,26 +293,22 @@ class FileDiscovery {
async getAllFilesInDirectory(dirPath, includeHidden = false) { async getAllFilesInDirectory(dirPath, includeHidden = false) {
const files = []; const files = [];
try { const entries = await safeExecute(() => fs.readdir(dirPath, { withFileTypes: true }), []);
const entries = await fs.readdir(dirPath, { withFileTypes: true });
for (const entry of entries) { for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name); const fullPath = path.join(dirPath, entry.name);
// 默认忽略隐藏文件(以.开头的文件和目录) // 默认忽略隐藏文件(以.开头的文件和目录)
if (!includeHidden && entry.name.startsWith('.')) { if (!includeHidden && entry.name.startsWith('.')) {
continue; continue;
} }
if (entry.isFile()) { if (entry.isFile()) {
files.push(fullPath); files.push(fullPath);
} else if (entry.isDirectory()) { } else if (entry.isDirectory()) {
const subFiles = await this.getAllFilesInDirectory(fullPath, includeHidden); const subFiles = await this.getAllFilesInDirectory(fullPath, includeHidden);
files.push(...subFiles); files.push(...subFiles);
}
} }
} catch (error) {
// 忽略无法访问的目录
} }
return files; return files;
@@ -312,21 +334,17 @@ class FileDiscovery {
} }
// 处理简单的文件名通配符 // 处理简单的文件名通配符
try { const entries = await safeExecute(() => fs.readdir(dir, { withFileTypes: true }), []);
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) { for (const entry of entries) {
// 默认忽略隐藏文件(以.开头的文件) // 默认忽略隐藏文件(以.开头的文件)
if (entry.name.startsWith('.')) { if (entry.name.startsWith('.')) {
continue; continue;
} }
if (entry.isFile() && this.matchPattern(entry.name, filename)) { if (entry.isFile() && this.matchPattern(entry.name, filename)) {
files.push(path.join(dir, entry.name)); files.push(path.join(dir, entry.name));
}
} }
} catch (error) {
// 目录不存在或无法访问
} }
return files; return files;
@@ -348,17 +366,17 @@ class FileDiscovery {
const basePath = parts[0].replace(/\*+$/, '').replace(/\/$/, '') || '.'; const basePath = parts[0].replace(/\*+$/, '').replace(/\/$/, '') || '.';
const remainingPattern = parts[1].replace(/^\//, ''); const remainingPattern = parts[1].replace(/^\//, '');
try { const allFiles = await safeExecute(() => this.getAllFilesInDirectory(basePath), []);
const allFiles = await this.getAllFilesInDirectory(basePath);
for (const file of allFiles) { for (const file of allFiles) {
const relativePath = path.relative(basePath, file); const relativePath = path.relative(basePath, file);
if (this.matchPattern(relativePath, remainingPattern)) { // 对于 **/pattern 模式,匹配相对路径的末尾部分
files.push(file); if (remainingPattern && (relativePath === remainingPattern || relativePath.endsWith('/' + remainingPattern))) {
} files.push(file);
} else if (this.matchPattern(relativePath, remainingPattern)) {
// 对于包含通配符的模式,使用 matchPattern
files.push(file);
} }
} catch (error) {
// 忽略错误
} }
} else { } else {
// 处理单层通配符 // 处理单层通配符
@@ -385,34 +403,30 @@ class FileDiscovery {
const [currentPart, ...remainingParts] = parts; const [currentPart, ...remainingParts] = parts;
const files = []; const files = [];
try { const entries = await safeExecute(() => fs.readdir(currentPath, { withFileTypes: true }), []);
const entries = await fs.readdir(currentPath, { withFileTypes: true });
for (const entry of entries) { for (const entry of entries) {
// 默认忽略隐藏文件和目录(以.开头的) // 默认忽略隐藏文件和目录(以.开头的)
if (entry.name.startsWith('.')) { if (entry.name.startsWith('.')) {
continue; continue;
} }
const fullPath = path.join(currentPath, entry.name); const fullPath = path.join(currentPath, entry.name);
if (this.matchPattern(entry.name, currentPart)) { if (this.matchPattern(entry.name, currentPart)) {
if (remainingParts.length === 0) { if (remainingParts.length === 0) {
// 最后一部分,检查是否为文件 // 最后一部分,检查是否为文件
if (entry.isFile()) { if (entry.isFile()) {
files.push(fullPath); files.push(fullPath);
} }
} else { } else {
// 还有更多部分,递归处理 // 还有更多部分,递归处理
if (entry.isDirectory()) { if (entry.isDirectory()) {
const subFiles = await this.matchPatternParts(remainingParts, fullPath); const subFiles = await this.matchPatternParts(remainingParts, fullPath);
files.push(...subFiles); files.push(...subFiles);
}
} }
} }
} }
} catch (error) {
// 忽略错误
} }
return files; return files;
@@ -686,4 +700,5 @@ module.exports = {
ActionOutputs, ActionOutputs,
ActionError, ActionError,
ErrorType, ErrorType,
safeExecute,
}; };

View File

@@ -10,7 +10,7 @@ import {
ErrorType, ErrorType,
ActionInputs, ActionInputs,
ActionOutputs, ActionOutputs,
} from '../index.js'; } from '../src/index.js';
import { createTestFile, createTestDir, cleanupTestFiles, cleanupTestDirs } from './utils.js'; import { createTestFile, createTestDir, cleanupTestFiles, cleanupTestDirs } from './utils.js';
describe('测试 FileDiscovery 类', () => { describe('测试 FileDiscovery 类', () => {
@@ -76,125 +76,248 @@ describe('测试 FileDiscovery 类', () => {
expect(files[0]).toBe('test-dir/file1.js'); expect(files[0]).toBe('test-dir/file1.js');
}); });
it('应该使用通配符模式查找文件', async () => { it('应该找到通配符*.js形式的文件', async () => {
const discovery = new FileDiscovery(); const discovery = new FileDiscovery();
const files = await discovery.findFiles(['test-dir/*.js']); const files = await discovery.findFiles(['test-dir/*.js']);
expect(files).toHaveLength(1); expect(files).toHaveLength(1);
}); });
it('应该递归查找文件', async () => { it('应该找到递归通配符(**/*.js形式的文件', async () => {
const discovery = new FileDiscovery(); const discovery = new FileDiscovery();
const files = await discovery.findFiles(['test-dir/**/*.js']); const files = await discovery.findFiles(['test-dir/**/*.js']);
expect(files).toHaveLength(2); expect(files).toHaveLength(2);
}); });
it('应该处理空目录', async () => { it('应该找到递归通配符开头与文件名(**/config.json形式的文件', async () => {
const tempDir = await createTestDir(); const tempDir = await createTestDir();
const discovery = new FileDiscovery(); const discovery = new FileDiscovery();
const files = await discovery.findFiles([tempDir]); // 创建多个目录层级的同名文件
const configFiles = [
path.join(tempDir, 'config.json'),
path.join(tempDir, 'src', 'config.json'),
path.join(tempDir, 'src', 'utils', 'config.json'),
path.join(tempDir, 'lib', 'config.json'),
path.join(tempDir, 'test', 'config.json'),
];
expect(files).toEqual([]); // 创建一些其他文件用于验证不会误匹配
cleanupTestDirs(tempDir); const otherFiles = [
}); path.join(tempDir, 'src', 'app.js'),
path.join(tempDir, 'src', 'utils', 'helper.js'),
path.join(tempDir, 'lib', 'main.js'),
path.join(tempDir, 'test', 'test.js'),
];
it('应该处理无效模式', async () => { // 创建所有文件
const discovery = new FileDiscovery(); for (const file of [...configFiles, ...otherFiles]) {
const files = await discovery.findFiles(['nonexistent/**/*.txt']); fs.mkdirSync(path.dirname(file), { recursive: true });
await createTestFile(file, '{"test": "content"}');
expect(files).toEqual([]);
});
it('应该处理混合有效和无效模式', async () => {
const tempDir = await createTestDir();
const testFile = path.join(tempDir, 'test.txt');
await createTestFile(testFile, 'content');
const discovery = new FileDiscovery();
const files = await discovery.findFiles([path.join(tempDir, '*.txt'), 'nonexistent/**/*.txt']);
expect(files).toContain(testFile);
cleanupTestFiles([testFile]);
cleanupTestDirs(tempDir);
});
it('应该处理无权限的目录', async () => {
const tempDir = await createTestDir();
const restrictedDir = path.join(tempDir, 'restricted');
const discovery = new FileDiscovery();
try {
fs.mkdirSync(restrictedDir, { mode: 0o000 });
const files = await discovery.findFiles([restrictedDir + '/*.txt']);
expect(files).toEqual([]);
} catch (error) {
// 某些系统可能不允许创建无权限目录,这是预期的
expect(error.code).toBe('EACCES');
} finally {
// 清理:先恢复权限再删除
try {
fs.chmodSync(restrictedDir, 0o755);
cleanupTestDirs(tempDir);
} catch (cleanupError) {
// 忽略清理错误
}
} }
});
it('应该忽略隐藏文件(默认)', async () => { // 测试 tempDir 目录下的 **/config.json 模式(限制在测试目录内)
const tempDir = await createTestDir(); const files = await discovery.findFiles([path.join(tempDir, '**/config.json')]);
const hiddenFile = path.join(tempDir, '.hidden.txt');
const visibleFile = path.join(tempDir, 'visible.txt');
const discovery = new FileDiscovery();
await createTestFile(hiddenFile, 'hidden content'); // 应该找到所有的 config.json 文件
await createTestFile(visibleFile, 'visible content'); expect(files).toHaveLength(configFiles.length);
for (const configFile of configFiles) {
expect(files).toContain(configFile);
}
const files = await discovery.findFiles([path.join(tempDir, '*.txt')]); // 清理测试文件
cleanupTestFiles([...configFiles, ...otherFiles]);
expect(files).toContain(visibleFile);
expect(files).not.toContain(hiddenFile);
cleanupTestFiles(hiddenFile, visibleFile);
cleanupTestDirs(tempDir); cleanupTestDirs(tempDir);
}); });
it('应该处理空文件', async () => { it('应该找到递归通配符开头与扩展名(**/*.md形式的文件', async () => {
const tempDir = await createTestDir(); const tempDir = await createTestDir();
const emptyFile = path.join(tempDir, 'empty.txt');
const discovery = new FileDiscovery(); const discovery = new FileDiscovery();
await createTestFile(emptyFile, ''); // 创建多层目录结构中的 .md 文件
const mdFiles = [
path.join(tempDir, 'README.md'),
path.join(tempDir, 'docs', 'guide.md'),
path.join(tempDir, 'src', 'docs', 'api.md'),
path.join(tempDir, 'lib', 'components', 'button.md'),
path.join(tempDir, 'test', 'integration', 'tests.md'),
];
const files = await discovery.findFiles([path.join(tempDir, '*.txt')]); // 创建一些其他扩展名的文件用于验证不会误匹配
const otherFiles = [
path.join(tempDir, 'src', 'main.js'),
path.join(tempDir, 'src', 'styles.css'),
path.join(tempDir, 'docs', 'config.json'),
path.join(tempDir, 'test', 'test.py'),
];
expect(files).toContain(emptyFile); // 创建所有文件
for (const file of [...mdFiles, ...otherFiles]) {
fs.mkdirSync(path.dirname(file), { recursive: true });
await createTestFile(file, '# Test Content');
}
cleanupTestFiles(emptyFile); // 测试 tempDir 目录下的 **/*.md 模式(限制在测试目录内)
const files = await discovery.findFiles([path.join(tempDir, '**/*.md')]);
// 应该找到所有的 .md 文件
expect(files).toHaveLength(mdFiles.length);
for (const mdFile of mdFiles) {
expect(files).toContain(mdFile);
}
// 清理测试文件
cleanupTestFiles([...mdFiles, ...otherFiles]);
cleanupTestDirs(tempDir); cleanupTestDirs(tempDir);
}); });
it('应该处理嵌套很深的目录', async () => { it('应该正确处理递归通配符前缀的复杂模式', async () => {
const tempDir = await createTestDir(); const tempDir = await createTestDir();
const deepFile = path.join(tempDir, 'level1', 'level2', 'level3', 'deep.txt');
const discovery = new FileDiscovery(); const discovery = new FileDiscovery();
fs.mkdirSync(path.dirname(deepFile), { recursive: true }); // 创建测试文件结构
await createTestFile(deepFile, 'deep content'); const testFiles = [
path.join(tempDir, 'src', 'components', 'Button.js'),
path.join(tempDir, 'src', 'components', 'Input.js'),
path.join(tempDir, 'src', 'utils', 'helpers.js'),
path.join(tempDir, 'lib', 'vendor', 'jquery.js'),
path.join(tempDir, 'test', 'unit', 'Button.test.js'),
path.join(tempDir, 'test', 'integration', 'App.test.js'),
];
const files = await discovery.findFiles([path.join(tempDir, '**/*.txt')]); // 创建一些非 JS 文件
const nonJSFiles = [
path.join(tempDir, 'src', 'styles.css'),
path.join(tempDir, 'docs', 'README.md'),
path.join(tempDir, 'config.json'),
];
expect(files).toContain(deepFile); // 创建所有文件
for (const file of [...testFiles, ...nonJSFiles]) {
fs.mkdirSync(path.dirname(file), { recursive: true });
await createTestFile(file, 'console.log("test");');
}
cleanupTestFiles(deepFile); // 测试 tempDir 目录下的 **/*.js 模式(限制在测试目录内)
const files = await discovery.findFiles([path.join(tempDir, '**/*.js')]);
// 应该找到所有的 .js 文件
expect(files).toHaveLength(testFiles.length);
for (const jsFile of testFiles) {
expect(files).toContain(jsFile);
}
// 清理测试文件
cleanupTestFiles([...testFiles, ...nonJSFiles]);
cleanupTestDirs(tempDir); cleanupTestDirs(tempDir);
}); });
}); });
it('应该处理空目录', async () => {
const tempDir = await createTestDir();
const discovery = new FileDiscovery();
const files = await discovery.findFiles([tempDir]);
expect(files).toEqual([]);
cleanupTestDirs(tempDir);
});
it('应该处理无效模式', async () => {
const discovery = new FileDiscovery();
const files = await discovery.findFiles(['nonexistent/**/*.txt']);
expect(files).toEqual([]);
});
it('应该处理混合有效和无效模式', async () => {
const tempDir = await createTestDir();
const testFile = path.join(tempDir, 'test.txt');
await createTestFile(testFile, 'content');
const discovery = new FileDiscovery();
const files = await discovery.findFiles([path.join(tempDir, '*.txt'), 'nonexistent/**/*.txt']);
expect(files).toContain(testFile);
cleanupTestFiles([testFile]);
cleanupTestDirs(tempDir);
});
it('应该处理无权限的目录', async () => {
const tempDir = await createTestDir();
const restrictedDir = path.join(tempDir, 'restricted');
const discovery = new FileDiscovery();
try {
fs.mkdirSync(restrictedDir, { mode: 0o000 });
const files = await discovery.findFiles([restrictedDir + '/*.txt']);
expect(files).toEqual([]);
} catch (error) {
// 某些系统可能不允许创建无权限目录,这是预期的
expect(error.code).toBe('EACCES');
} finally {
// 清理:先恢复权限再删除
try {
fs.chmodSync(restrictedDir, 0o755);
cleanupTestDirs(tempDir);
} catch (cleanupError) {
// 忽略清理错误
}
}
});
it('应该忽略隐藏文件(默认)', async () => {
const tempDir = await createTestDir();
const hiddenFile = path.join(tempDir, '.hidden.txt');
const visibleFile = path.join(tempDir, 'visible.txt');
const discovery = new FileDiscovery();
await createTestFile(hiddenFile, 'hidden content');
await createTestFile(visibleFile, 'visible content');
const files = await discovery.findFiles([path.join(tempDir, '*.txt')]);
expect(files).toContain(visibleFile);
expect(files).not.toContain(hiddenFile);
cleanupTestFiles(hiddenFile, visibleFile);
cleanupTestDirs(tempDir);
});
it('应该处理空文件', async () => {
const tempDir = await createTestDir();
const emptyFile = path.join(tempDir, 'empty.txt');
const discovery = new FileDiscovery();
await createTestFile(emptyFile, '');
const files = await discovery.findFiles([path.join(tempDir, '*.txt')]);
expect(files).toContain(emptyFile);
cleanupTestFiles(emptyFile);
cleanupTestDirs(tempDir);
});
it('应该处理嵌套很深的目录', async () => {
const tempDir = await createTestDir();
const deepFile = path.join(tempDir, 'level1', 'level2', 'level3', 'deep.txt');
const discovery = new FileDiscovery();
fs.mkdirSync(path.dirname(deepFile), { recursive: true });
await createTestFile(deepFile, 'deep content');
const files = await discovery.findFiles([path.join(tempDir, '**/*.txt')]);
expect(files).toContain(deepFile);
cleanupTestFiles(deepFile);
cleanupTestDirs(tempDir);
});
describe('测试 FileDiscovery.validateFiles 方法', () => { describe('测试 FileDiscovery.validateFiles 方法', () => {
const existingFile = 'existing-file.txt'; const existingFile = 'existing-file.txt';
const nonExistingFile = 'non-existing-file.txt'; const nonExistingFile = 'non-existing-file.txt';