Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c9a9632ccc | |||
| 4174e14517 | |||
| 343f295a13 | |||
| 811d37052b |
86
.github/workflows/ci.yml
vendored
86
.github/workflows/ci.yml
vendored
@@ -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 "✅ 测试通过"
|
||||||
|
|||||||
@@ -72,10 +72,10 @@ jobs:
|
|||||||
|
|
||||||
## 输入参数
|
## 输入参数
|
||||||
|
|
||||||
| 输入参数 | 描述 | 必需 | 默认值 |
|
| 输入参数 | 描述 | 必需 | 默认值 |
|
||||||
| ----------------- | ------------------------------------------- | ----- | -------- |
|
| ----------- | ------------------------------------------- | ----- | -------- |
|
||||||
| `files` | 文件路径或 glob 模式(每行一个) | ✅ 是 | - |
|
| `files` | 文件路径或 glob 模式(每行一个) | ✅ 是 | - |
|
||||||
| `algorithm` | 哈希算法:`md5`、`sha1`、`sha256`、`sha512` | ❌ 否 | `sha256` |
|
| `algorithm` | 哈希算法:`md5`、`sha1`、`sha256`、`sha512` | ❌ 否 | `sha256` |
|
||||||
|
|
||||||
### 文件模式
|
### 文件模式
|
||||||
|
|
||||||
|
|||||||
@@ -22,4 +22,4 @@ outputs:
|
|||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: 'node20'
|
using: 'node20'
|
||||||
main: 'index.js'
|
main: 'src/index.js'
|
||||||
|
|||||||
30
package.json
30
package.json
@@ -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"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user