1145 lines
37 KiB
JavaScript
1145 lines
37 KiB
JavaScript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||
import path from 'path';
|
||
import fs from 'fs';
|
||
import {
|
||
FilesHashAction,
|
||
FileDiscovery,
|
||
HashCalculator,
|
||
OutputFormatter,
|
||
ActionError,
|
||
ErrorType,
|
||
ActionInputs,
|
||
ActionOutputs,
|
||
} from '../index.js';
|
||
import { createTestFile, createTestDir, cleanupTestFiles, cleanupTestDirs } from './utils.js';
|
||
|
||
describe('测试 FileDiscovery 类', () => {
|
||
describe('测试 FileDiscovery.parseFilePatterns 方法', () => {
|
||
it('应该解析包含多个模式的字符串输入', () => {
|
||
const discovery = new FileDiscovery();
|
||
|
||
const patterns = discovery.parseFilePatterns('file1.txt\nfile2.txt\n\n file3.txt \n');
|
||
expect(patterns).toHaveLength(3);
|
||
expect(patterns[0]).toBe('file1.txt');
|
||
expect(patterns[2]).toBe('file3.txt');
|
||
});
|
||
|
||
it('应该解析包含空白和空格模式的数组输入', () => {
|
||
const discovery = new FileDiscovery();
|
||
|
||
const patterns = discovery.parseFilePatterns(['file1.txt', '', ' file2.txt ']);
|
||
expect(patterns).toHaveLength(2);
|
||
expect(patterns).toEqual(['file1.txt', 'file2.txt']);
|
||
});
|
||
});
|
||
|
||
describe('测试 FileDiscovery.matchPattern 方法', () => {
|
||
const discovery = new FileDiscovery();
|
||
|
||
it('应该匹配确切的文件名', () => {
|
||
expect(discovery.matchPattern('test.js', 'test.js')).toBe(true);
|
||
expect(discovery.matchPattern('test.js', 'other.js')).toBe(false);
|
||
});
|
||
|
||
it('应该匹配通配符模式', () => {
|
||
expect(discovery.matchPattern('test.js', '*.js')).toBe(true);
|
||
expect(discovery.matchPattern('file.txt', 'file.*')).toBe(true);
|
||
expect(discovery.matchPattern('test.js', '*.txt')).toBe(false);
|
||
});
|
||
|
||
it('应该匹配问号通配符', () => {
|
||
expect(discovery.matchPattern('test1.js', 'test?.js')).toBe(true);
|
||
expect(discovery.matchPattern('test12.js', 'test?.js')).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('测试 FileDiscovery.findFiles 方法', () => {
|
||
const testFiles = ['test-dir/file1.js', 'test-dir/file2.txt', 'test-dir/subdir/file3.js'];
|
||
|
||
beforeEach(async () => {
|
||
// 创建测试文件
|
||
for (const file of testFiles) {
|
||
await createTestFile(file, 'test content');
|
||
}
|
||
});
|
||
|
||
afterEach(async () => {
|
||
await cleanupTestFiles(testFiles);
|
||
await cleanupTestDirs(['test-dir/subdir', 'test-dir']);
|
||
});
|
||
|
||
it('应该找到确切的文件', async () => {
|
||
const discovery = new FileDiscovery();
|
||
|
||
const files = await discovery.findFiles(['test-dir/file1.js']);
|
||
expect(files).toHaveLength(1);
|
||
expect(files[0]).toBe('test-dir/file1.js');
|
||
});
|
||
|
||
it('应该找到通配符(*.js)形式的文件', async () => {
|
||
const discovery = new FileDiscovery();
|
||
|
||
const files = await discovery.findFiles(['test-dir/*.js']);
|
||
expect(files).toHaveLength(1);
|
||
});
|
||
|
||
it('应该找到递归通配符(**/*.js)形式的文件', async () => {
|
||
const discovery = new FileDiscovery();
|
||
|
||
const files = await discovery.findFiles(['test-dir/**/*.js']);
|
||
expect(files).toHaveLength(2);
|
||
});
|
||
|
||
it('应该找到递归通配符开头与文件名(**/config.json)形式的文件', async () => {
|
||
const tempDir = await createTestDir();
|
||
const discovery = new FileDiscovery();
|
||
|
||
// 创建多个目录层级的同名文件
|
||
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'),
|
||
];
|
||
|
||
// 创建一些其他文件用于验证不会误匹配
|
||
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'),
|
||
];
|
||
|
||
// 创建所有文件
|
||
for (const file of [...configFiles, ...otherFiles]) {
|
||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||
await createTestFile(file, '{"test": "content"}');
|
||
}
|
||
|
||
// 测试 tempDir 目录下的 **/config.json 模式(限制在测试目录内)
|
||
const files = await discovery.findFiles([path.join(tempDir, '**/config.json')]);
|
||
|
||
// 应该找到所有的 config.json 文件
|
||
expect(files).toHaveLength(configFiles.length);
|
||
for (const configFile of configFiles) {
|
||
expect(files).toContain(configFile);
|
||
}
|
||
|
||
// 清理测试文件
|
||
cleanupTestFiles([...configFiles, ...otherFiles]);
|
||
cleanupTestDirs(tempDir);
|
||
});
|
||
|
||
it('应该找到递归通配符开头与扩展名(**/*.md)形式的文件', async () => {
|
||
const tempDir = await createTestDir();
|
||
const discovery = new FileDiscovery();
|
||
|
||
// 创建多层目录结构中的 .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 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'),
|
||
];
|
||
|
||
// 创建所有文件
|
||
for (const file of [...mdFiles, ...otherFiles]) {
|
||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||
await createTestFile(file, '# Test Content');
|
||
}
|
||
|
||
// 测试 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);
|
||
});
|
||
|
||
it('应该正确处理递归通配符前缀的复杂模式', async () => {
|
||
const tempDir = await createTestDir();
|
||
const discovery = new FileDiscovery();
|
||
|
||
// 创建测试文件结构
|
||
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'),
|
||
];
|
||
|
||
// 创建一些非 JS 文件
|
||
const nonJSFiles = [
|
||
path.join(tempDir, 'src', 'styles.css'),
|
||
path.join(tempDir, 'docs', 'README.md'),
|
||
path.join(tempDir, 'config.json'),
|
||
];
|
||
|
||
// 创建所有文件
|
||
for (const file of [...testFiles, ...nonJSFiles]) {
|
||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||
await createTestFile(file, 'console.log("test");');
|
||
}
|
||
|
||
// 测试 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);
|
||
});
|
||
});
|
||
|
||
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 方法', () => {
|
||
const existingFile = 'existing-file.txt';
|
||
const nonExistingFile = 'non-existing-file.txt';
|
||
|
||
beforeEach(async () => {
|
||
await createTestFile(existingFile, 'test content');
|
||
});
|
||
|
||
afterEach(async () => {
|
||
await cleanupTestFiles([existingFile]);
|
||
});
|
||
|
||
it('应该验证存在和不存在的文件', async () => {
|
||
const discovery = new FileDiscovery();
|
||
|
||
const results = await discovery.validateFiles([existingFile, nonExistingFile]);
|
||
|
||
expect(results).toHaveLength(2);
|
||
|
||
const existingResult = results.find(r => r.file === existingFile);
|
||
const nonExistingResult = results.find(r => r.file === nonExistingFile);
|
||
|
||
expect(existingResult.exists).toBe(true);
|
||
expect(existingResult.readable).toBe(true);
|
||
expect(existingResult.size).toBeGreaterThan(0);
|
||
|
||
expect(nonExistingResult.exists).toBe(false);
|
||
expect(nonExistingResult.readable).toBe(false);
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('测试 HashCalculator 类', () => {
|
||
describe('测试 HashCalculator.validateAlgorithm 方法', () => {
|
||
const calculator = new HashCalculator();
|
||
|
||
it('应该接受有效的算法', () => {
|
||
expect(() => calculator.validateAlgorithm('sha256')).not.toThrow();
|
||
expect(() => calculator.validateAlgorithm('MD5')).not.toThrow();
|
||
});
|
||
|
||
it('应该对无效算法抛出错误', () => {
|
||
expect(() => calculator.validateAlgorithm('invalid')).toThrow(ActionError);
|
||
|
||
try {
|
||
calculator.validateAlgorithm('invalid');
|
||
} catch (error) {
|
||
expect(error.type).toBe(ErrorType.INVALID_ALGORITHM);
|
||
}
|
||
});
|
||
|
||
it('应该处理算法名称大小写', () => {
|
||
// 验证算法验证方法是否大小写不敏感
|
||
expect(() => calculator.validateAlgorithm('SHA256')).not.toThrow();
|
||
expect(() => calculator.validateAlgorithm('sha256')).not.toThrow();
|
||
expect(() => calculator.validateAlgorithm('Sha256')).not.toThrow();
|
||
|
||
expect(() => calculator.validateAlgorithm('md5')).not.toThrow();
|
||
expect(() => calculator.validateAlgorithm('MD5')).not.toThrow();
|
||
});
|
||
});
|
||
|
||
describe('测试 HashCalculator.calculateFileHash 方法', () => {
|
||
const testFile = 'test-temp-file.txt';
|
||
const testContent = 'Hello, World!';
|
||
|
||
beforeEach(async () => {
|
||
await createTestFile(testFile, testContent);
|
||
});
|
||
|
||
afterEach(async () => {
|
||
await cleanupTestFiles([testFile]);
|
||
});
|
||
|
||
it('应该正确计算SHA256哈希', async () => {
|
||
const calculator = new HashCalculator();
|
||
|
||
const hash = await calculator.calculateFileHash(testFile, 'sha256');
|
||
|
||
expect(hash).toHaveLength(64);
|
||
expect(hash).toMatch(/^[a-f0-9]+$/);
|
||
});
|
||
|
||
it('应该产生一致的哈希值', async () => {
|
||
const calculator = new HashCalculator();
|
||
|
||
const hash1 = await calculator.calculateFileHash(testFile, 'sha256');
|
||
const hash2 = await calculator.calculateFileHash(testFile, 'sha256');
|
||
|
||
expect(hash1).toBe(hash2);
|
||
});
|
||
|
||
it('应该处理空文件', async () => {
|
||
const tempDir = await createTestDir();
|
||
const emptyFile = path.join(tempDir, 'empty.txt');
|
||
await createTestFile(emptyFile, '');
|
||
|
||
const calculator = new HashCalculator();
|
||
const hash = await calculator.calculateFileHash(emptyFile, 'sha256');
|
||
|
||
// 空文件的 SHA256 哈希值
|
||
expect(hash).toBe('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855');
|
||
|
||
cleanupTestFiles(emptyFile);
|
||
cleanupTestDirs(tempDir);
|
||
});
|
||
|
||
it('应该处理大文件', async () => {
|
||
const tempDir = await createTestDir();
|
||
const largeFile = path.join(tempDir, 'large.txt');
|
||
// 创建 1MB 大小的文件
|
||
const largeContent = 'x'.repeat(1024 * 1024);
|
||
await createTestFile(largeFile, largeContent);
|
||
|
||
const calculator = new HashCalculator();
|
||
const hash = await calculator.calculateFileHash(largeFile, 'sha256');
|
||
|
||
expect(hash).toBeTruthy();
|
||
expect(hash.length).toBe(64); // SHA256 哈希长度
|
||
|
||
cleanupTestFiles(largeFile);
|
||
cleanupTestDirs(tempDir);
|
||
});
|
||
|
||
it('应该处理不存在的文件', async () => {
|
||
const calculator = new HashCalculator();
|
||
|
||
await expect(async () => {
|
||
await calculator.calculateFileHash('/nonexistent/file.txt', 'sha256');
|
||
}).rejects.toThrow();
|
||
});
|
||
});
|
||
|
||
describe('测试 HashCalculator.calculateCombinedHash 方法', () => {
|
||
const testFiles = ['test-file1.txt', 'test-file2.txt'];
|
||
|
||
beforeEach(async () => {
|
||
await createTestFile(testFiles[0], 'Content 1');
|
||
await createTestFile(testFiles[1], 'Content 2');
|
||
});
|
||
|
||
afterEach(async () => {
|
||
await cleanupTestFiles(testFiles);
|
||
});
|
||
|
||
it('应该计算组合哈希', async () => {
|
||
const calculator = new HashCalculator();
|
||
|
||
const hash = await calculator.calculateCombinedHash(testFiles, 'sha256');
|
||
expect(hash).toHaveLength(64);
|
||
});
|
||
|
||
it('应该与顺序无关', async () => {
|
||
const calculator = new HashCalculator();
|
||
|
||
const hash1 = await calculator.calculateCombinedHash(testFiles, 'sha256');
|
||
const hash2 = await calculator.calculateCombinedHash([testFiles[1], testFiles[0]], 'sha256');
|
||
|
||
expect(hash1).toBe(hash2);
|
||
});
|
||
|
||
it('应该处理空文件列表的组合哈希', async () => {
|
||
const calculator = new HashCalculator();
|
||
const hash = await calculator.calculateCombinedHash([], 'sha256');
|
||
|
||
// 空列表的组合哈希应该是空字符串的哈希
|
||
expect(hash).toBeTruthy();
|
||
});
|
||
|
||
it('应该处理单文件的组合哈希', async () => {
|
||
const tempDir = await createTestDir();
|
||
const testFile = path.join(tempDir, 'test.txt');
|
||
await createTestFile(testFile, 'test content');
|
||
|
||
const calculator = new HashCalculator();
|
||
const hash = await calculator.calculateCombinedHash([testFile], 'sha256');
|
||
|
||
expect(hash).toBeTruthy();
|
||
expect(hash.length).toBe(64);
|
||
|
||
cleanupTestFiles(testFile);
|
||
cleanupTestDirs(tempDir);
|
||
});
|
||
|
||
it('应该处理多文件的组合哈希', async () => {
|
||
const tempDir = await createTestDir();
|
||
const file1 = path.join(tempDir, 'file1.txt');
|
||
const file2 = path.join(tempDir, 'file2.txt');
|
||
|
||
await createTestFile(file1, 'content1');
|
||
await createTestFile(file2, 'content2');
|
||
|
||
const calculator = new HashCalculator();
|
||
const hash = await calculator.calculateCombinedHash([file1, file2], 'sha256');
|
||
|
||
expect(hash).toBeTruthy();
|
||
expect(hash.length).toBe(64);
|
||
|
||
cleanupTestFiles(file1, file2);
|
||
cleanupTestDirs(tempDir);
|
||
});
|
||
|
||
it('应该处理文件顺序无关的组合哈希', async () => {
|
||
const tempDir = await createTestDir();
|
||
const file1 = path.join(tempDir, 'file1.txt');
|
||
const file2 = path.join(tempDir, 'file2.txt');
|
||
|
||
await createTestFile(file1, 'content1');
|
||
await createTestFile(file2, 'content2');
|
||
|
||
const calculator = new HashCalculator();
|
||
const hash1 = await calculator.calculateCombinedHash([file1, file2], 'sha256');
|
||
const hash2 = await calculator.calculateCombinedHash([file2, file1], 'sha256');
|
||
|
||
expect(hash1).toBe(hash2);
|
||
|
||
cleanupTestFiles(file1, file2);
|
||
cleanupTestDirs(tempDir);
|
||
});
|
||
});
|
||
|
||
describe('测试 HashCalculator.getSupportedAlgorithms 方法', () => {
|
||
it('应该获取支持的算法列表', () => {
|
||
const calculator = new HashCalculator();
|
||
const algorithms = calculator.getSupportedAlgorithms();
|
||
|
||
expect(algorithms).toContain('sha256');
|
||
expect(algorithms).toContain('md5');
|
||
expect(algorithms).toContain('sha1');
|
||
expect(algorithms).toContain('sha512');
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('测试 OutputFormatter 类', () => {
|
||
describe('测试 OutputFormatter.formatOutput 方法', () => {
|
||
it('应该正确格式化输出', () => {
|
||
const formatter = new OutputFormatter();
|
||
const result = formatter.formatOutput(['file1.txt', 'file2.txt'], 'sha256', 'test-hash');
|
||
|
||
expect(result).toContain('file1.txt');
|
||
expect(result).toContain('file2.txt');
|
||
expect(result).toContain('Algorithm: sha256');
|
||
expect(result).toContain('Combined Hash: test-hash');
|
||
});
|
||
|
||
it('应该处理空文件列表', () => {
|
||
const formatter = new OutputFormatter();
|
||
const result = formatter.formatOutput([], 'sha256', 'test-hash');
|
||
|
||
expect(result).toContain('No files found');
|
||
expect(result).toContain('Algorithm: sha256');
|
||
expect(result).toContain('Combined Hash: test-hash');
|
||
});
|
||
|
||
it('应该处理空算法名称', () => {
|
||
const formatter = new OutputFormatter();
|
||
const result = formatter.formatOutput(['file1.txt'], '', 'test-hash');
|
||
|
||
expect(result).toContain('Algorithm: ');
|
||
expect(result).toContain('Combined Hash: test-hash');
|
||
});
|
||
|
||
it('应该处理空哈希值', () => {
|
||
const formatter = new OutputFormatter();
|
||
const result = formatter.formatOutput(['file1.txt'], 'sha256', '');
|
||
|
||
expect(result).toContain('Algorithm: sha256');
|
||
expect(result).toContain('Combined Hash: ');
|
||
});
|
||
|
||
it('应该处理特殊字符文件名', () => {
|
||
const formatter = new OutputFormatter();
|
||
const specialFiles = [
|
||
'file with spaces.txt',
|
||
'file-with-dashes.txt',
|
||
'file_with_underscores.txt',
|
||
'file.with.dots.txt',
|
||
'file@with#symbols.txt',
|
||
];
|
||
|
||
const result = formatter.formatOutput(specialFiles, 'sha256', 'test-hash');
|
||
|
||
specialFiles.forEach(file => {
|
||
expect(result).toContain(file);
|
||
});
|
||
});
|
||
|
||
it('应该处理很长的文件列表', () => {
|
||
const formatter = new OutputFormatter();
|
||
const manyFiles = Array.from({ length: 100 }, (_, i) => `file${i}.txt`);
|
||
|
||
const result = formatter.formatOutput(manyFiles, 'sha256', 'test-hash');
|
||
|
||
expect(result).toContain('Total files: 100');
|
||
expect(result).toContain('Algorithm: sha256');
|
||
expect(result).toContain('Combined Hash: test-hash');
|
||
});
|
||
|
||
it('应该处理包含路径的文件名', () => {
|
||
const formatter = new OutputFormatter();
|
||
const pathFiles = [
|
||
'/absolute/path/file1.txt',
|
||
'./relative/path/file2.txt',
|
||
'../parent/path/file3.txt',
|
||
'C:\\Windows\\path\\file4.txt',
|
||
];
|
||
|
||
const result = formatter.formatOutput(pathFiles, 'sha256', 'test-hash');
|
||
|
||
pathFiles.forEach(file => {
|
||
expect(result).toContain(file);
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('测试 OutputFormatter.setGitHubOutput 方法', () => {
|
||
let originalEnv;
|
||
|
||
beforeEach(() => {
|
||
originalEnv = process.env.GITHUB_OUTPUT;
|
||
});
|
||
|
||
afterEach(() => {
|
||
process.env.GITHUB_OUTPUT = originalEnv;
|
||
});
|
||
|
||
it('应该处理无 GitHub 输出文件的情况', () => {
|
||
delete process.env.GITHUB_OUTPUT;
|
||
const consoleSpy = vi.spyOn(console, 'log');
|
||
const formatter = new OutputFormatter();
|
||
|
||
formatter.setGitHubOutput('abcd1234', 2);
|
||
|
||
expect(consoleSpy).toHaveBeenCalledWith('::set-output name=hash::abcd1234');
|
||
expect(consoleSpy).toHaveBeenCalledWith('::set-output name=file-count::2');
|
||
consoleSpy.mockRestore();
|
||
});
|
||
|
||
it('应该处理空的 GitHub 输出值', () => {
|
||
delete process.env.GITHUB_OUTPUT;
|
||
const consoleSpy = vi.spyOn(console, 'log');
|
||
const formatter = new OutputFormatter();
|
||
|
||
formatter.setGitHubOutput('', 0);
|
||
|
||
expect(consoleSpy).toHaveBeenCalledWith('::set-output name=hash::');
|
||
expect(consoleSpy).toHaveBeenCalledWith('::set-output name=file-count::0');
|
||
consoleSpy.mockRestore();
|
||
});
|
||
|
||
it('应该处理包含特殊字符的 GitHub 输出', () => {
|
||
delete process.env.GITHUB_OUTPUT;
|
||
const consoleSpy = vi.spyOn(console, 'log');
|
||
const formatter = new OutputFormatter();
|
||
|
||
formatter.setGitHubOutput('special-value!@#', 1);
|
||
|
||
expect(consoleSpy).toHaveBeenCalledWith('::set-output name=hash::special-value!@#');
|
||
expect(consoleSpy).toHaveBeenCalledWith('::set-output name=file-count::1');
|
||
consoleSpy.mockRestore();
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('测试 ActionInputs 类', () => {
|
||
describe('测试 ActionInputs.getInput 方法', () => {
|
||
it('应该获取环境变量中的输入值', () => {
|
||
process.env.INPUT_TEST_VALUE = 'test input';
|
||
|
||
const result = ActionInputs.getInput('test-value');
|
||
expect(result).toBe('test input');
|
||
|
||
delete process.env.INPUT_TEST_VALUE;
|
||
});
|
||
|
||
it('应该返回空字符串当环境变量不存在', () => {
|
||
const result = ActionInputs.getInput('non-existent');
|
||
expect(result).toBe('');
|
||
});
|
||
|
||
it('应该处理带连字符的参数名', () => {
|
||
process.env.INPUT_MY_TEST_INPUT = 'value with dash';
|
||
|
||
const result = ActionInputs.getInput('my-test-input');
|
||
expect(result).toBe('value with dash');
|
||
|
||
delete process.env.INPUT_MY_TEST_INPUT;
|
||
});
|
||
|
||
it('应该在必需参数缺失时抛出错误', () => {
|
||
expect(() => ActionInputs.getInput('required-param', true)).toThrow(ActionError);
|
||
});
|
||
|
||
it('不应该在必需参数存在时抛出错误', () => {
|
||
process.env.INPUT_REQUIRED_PARAM = 'exists';
|
||
|
||
expect(() => ActionInputs.getInput('required-param', true)).not.toThrow();
|
||
|
||
delete process.env.INPUT_REQUIRED_PARAM;
|
||
});
|
||
});
|
||
|
||
describe('测试 ActionInputs.getBooleanInput 方法', () => {
|
||
it('应该正确解析 true 值', () => {
|
||
process.env.INPUT_TEST_BOOL = 'true';
|
||
|
||
const result = ActionInputs.getBooleanInput('test-bool');
|
||
expect(result).toBe(true);
|
||
|
||
delete process.env.INPUT_TEST_BOOL;
|
||
});
|
||
|
||
it('应该正确解析 1 为 true', () => {
|
||
process.env.INPUT_TEST_BOOL = '1';
|
||
|
||
const result = ActionInputs.getBooleanInput('test-bool');
|
||
expect(result).toBe(true);
|
||
|
||
delete process.env.INPUT_TEST_BOOL;
|
||
});
|
||
|
||
it('应该正确解析 false 值', () => {
|
||
process.env.INPUT_TEST_BOOL = 'false';
|
||
|
||
const result = ActionInputs.getBooleanInput('test-bool');
|
||
expect(result).toBe(false);
|
||
|
||
delete process.env.INPUT_TEST_BOOL;
|
||
});
|
||
|
||
it('应该对非布尔值返回 false', () => {
|
||
process.env.INPUT_TEST_BOOL = 'random-string';
|
||
|
||
const result = ActionInputs.getBooleanInput('test-bool');
|
||
expect(result).toBe(false);
|
||
|
||
delete process.env.INPUT_TEST_BOOL;
|
||
});
|
||
|
||
it('应该处理大小写不敏感', () => {
|
||
process.env.INPUT_TEST_BOOL = 'TRUE';
|
||
|
||
const result = ActionInputs.getBooleanInput('test-bool');
|
||
expect(result).toBe(true);
|
||
|
||
delete process.env.INPUT_TEST_BOOL;
|
||
});
|
||
});
|
||
|
||
describe('测试 ActionInputs.getMultilineInput 方法', () => {
|
||
it('应该正确解析多行输入', () => {
|
||
process.env.INPUT_MULTI_LINE = 'line1\nline2\nline3';
|
||
|
||
const result = ActionInputs.getMultilineInput('multi-line');
|
||
expect(result).toHaveLength(3);
|
||
expect(result).toEqual(['line1', 'line2', 'line3']);
|
||
|
||
delete process.env.INPUT_MULTI_LINE;
|
||
});
|
||
|
||
it('应该过滤空行', () => {
|
||
process.env.INPUT_MULTI_LINE = 'line1\n\n \nline2\n \nline3';
|
||
|
||
const result = ActionInputs.getMultilineInput('multi-line');
|
||
expect(result).toHaveLength(3);
|
||
expect(result).toEqual(['line1', 'line2', 'line3']);
|
||
|
||
delete process.env.INPUT_MULTI_LINE;
|
||
});
|
||
|
||
it('应该处理空输入', () => {
|
||
process.env.INPUT_MULTI_LINE = '';
|
||
|
||
const result = ActionInputs.getMultilineInput('multi-line');
|
||
expect(result).toHaveLength(0);
|
||
|
||
delete process.env.INPUT_MULTI_LINE;
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('测试 ActionOutputs 类', () => {
|
||
describe('测试 ActionOutputs.setOutput 方法', () => {
|
||
let originalEnv;
|
||
let outputContent = '';
|
||
|
||
beforeEach(() => {
|
||
originalEnv = process.env.GITHUB_OUTPUT;
|
||
// 模拟文件写入
|
||
const fs = require('fs');
|
||
fs.appendFileSync = (file, content) => {
|
||
outputContent += content;
|
||
};
|
||
});
|
||
|
||
afterEach(() => {
|
||
process.env.GITHUB_OUTPUT = originalEnv;
|
||
outputContent = '';
|
||
});
|
||
|
||
it('应该写入 GitHub 输出文件', () => {
|
||
process.env.GITHUB_OUTPUT = '/tmp/github_output.txt';
|
||
|
||
ActionOutputs.setOutput('test-name', 'test-value');
|
||
|
||
expect(outputContent).toBe('test-name=test-value\n');
|
||
});
|
||
|
||
it('应该在无输出文件时使用控制台输出', () => {
|
||
delete process.env.GITHUB_OUTPUT;
|
||
const consoleSpy = vi.spyOn(console, 'log');
|
||
|
||
ActionOutputs.setOutput('test-name', 'test-value');
|
||
|
||
expect(consoleSpy).toHaveBeenCalledWith('::set-output name=test-name::test-value');
|
||
consoleSpy.mockRestore();
|
||
});
|
||
});
|
||
|
||
describe('测试 ActionOutputs 日志方法', () => {
|
||
it('info 应该输出到控制台', () => {
|
||
const consoleSpy = vi.spyOn(console, 'log');
|
||
|
||
ActionOutputs.info('test message');
|
||
|
||
expect(consoleSpy).toHaveBeenCalledWith('test message');
|
||
consoleSpy.mockRestore();
|
||
});
|
||
|
||
it('warning 应该输出带格式的警告', () => {
|
||
const consoleSpy = vi.spyOn(console, 'log');
|
||
|
||
ActionOutputs.warning('test warning');
|
||
|
||
expect(consoleSpy).toHaveBeenCalledWith('::warning::test warning');
|
||
consoleSpy.mockRestore();
|
||
});
|
||
|
||
it('error 应该输出带格式的错误', () => {
|
||
const consoleSpy = vi.spyOn(console, 'log');
|
||
|
||
ActionOutputs.error('test error');
|
||
|
||
expect(consoleSpy).toHaveBeenCalledWith('::error::test error');
|
||
consoleSpy.mockRestore();
|
||
});
|
||
});
|
||
|
||
describe('测试 ActionOutputs.setFailed 方法', () => {
|
||
it('应该输出错误并退出进程', () => {
|
||
const consoleSpy = vi.spyOn(console, 'log');
|
||
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {});
|
||
|
||
ActionOutputs.setFailed('test failure');
|
||
|
||
expect(consoleSpy).toHaveBeenCalledWith('::error::test failure');
|
||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||
|
||
consoleSpy.mockRestore();
|
||
exitSpy.mockRestore();
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('测试 ActionError 类', () => {
|
||
it('应该创建带有正确属性的错误实例', () => {
|
||
const error = new ActionError(ErrorType.FILE_NOT_FOUND, 'File not found', 'test.txt', 'Additional details');
|
||
|
||
expect(error).toBeInstanceOf(Error);
|
||
expect(error).toBeInstanceOf(ActionError);
|
||
expect(error.type).toBe(ErrorType.FILE_NOT_FOUND);
|
||
expect(error.message).toBe('File not found');
|
||
expect(error.file).toBe('test.txt');
|
||
expect(error.details).toBe('Additional details');
|
||
expect(error.name).toBe('ActionError');
|
||
});
|
||
|
||
it('应该处理可选参数为 null 的情况', () => {
|
||
const error = new ActionError(ErrorType.INVALID_ALGORITHM, 'Invalid algorithm');
|
||
|
||
expect(error.type).toBe(ErrorType.INVALID_ALGORITHM);
|
||
expect(error.message).toBe('Invalid algorithm');
|
||
expect(error.file).toBeNull();
|
||
expect(error.details).toBeNull();
|
||
});
|
||
|
||
it('应该包含正确的错误类型常量', () => {
|
||
expect(ErrorType.FILE_NOT_FOUND).toBe('FILE_NOT_FOUND');
|
||
expect(ErrorType.PERMISSION_DENIED).toBe('PERMISSION_DENIED');
|
||
expect(ErrorType.INVALID_ALGORITHM).toBe('INVALID_ALGORITHM');
|
||
expect(ErrorType.HASH_CALCULATION_FAILED).toBe('HASH_CALCULATION_FAILED');
|
||
});
|
||
});
|
||
|
||
describe('测试 FilesHashAction 类', () => {
|
||
describe('测试 FilesHashAction 边界情况 - 无文件和空输入', () => {
|
||
it('应该处理空的文件模式', async () => {
|
||
const action = new FilesHashAction();
|
||
|
||
// 模拟空的文件模式输入
|
||
vi.spyOn(ActionInputs, 'getInput').mockImplementation(name => {
|
||
if (name === 'algorithm') return 'sha256';
|
||
return '';
|
||
});
|
||
vi.spyOn(ActionInputs, 'getMultilineInput').mockImplementation(name => {
|
||
if (name === 'files') return [''];
|
||
return [];
|
||
});
|
||
vi.spyOn(ActionInputs, 'getBooleanInput').mockReturnValue(false);
|
||
|
||
const consoleSpy = vi.spyOn(console, 'log');
|
||
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {});
|
||
|
||
await action.run();
|
||
|
||
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('::error::'));
|
||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||
|
||
consoleSpy.mockRestore();
|
||
exitSpy.mockRestore();
|
||
});
|
||
|
||
it('应该处理无效的文件模式', async () => {
|
||
const action = new FilesHashAction();
|
||
|
||
vi.spyOn(ActionInputs, 'getInput').mockImplementation(name => {
|
||
if (name === 'algorithm') return 'sha256';
|
||
return '';
|
||
});
|
||
vi.spyOn(ActionInputs, 'getMultilineInput').mockImplementation(name => {
|
||
if (name === 'files') return ['nonexistent/**/*.txt'];
|
||
return [];
|
||
});
|
||
vi.spyOn(ActionInputs, 'getBooleanInput').mockReturnValue(false);
|
||
|
||
const consoleSpy = vi.spyOn(console, 'log');
|
||
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {});
|
||
|
||
await action.run();
|
||
|
||
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('::error::'));
|
||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||
|
||
consoleSpy.mockRestore();
|
||
exitSpy.mockRestore();
|
||
});
|
||
});
|
||
|
||
describe('测试 FilesHashAction 边界情况 - 权限和文件系统错误', () => {
|
||
it('应该处理文件读取权限错误', async () => {
|
||
const action = new FilesHashAction();
|
||
const tempDir = await createTestDir();
|
||
const restrictedFile = path.join(tempDir, 'restricted.txt');
|
||
|
||
await createTestFile(restrictedFile, 'content');
|
||
|
||
// 模拟文件读取错误
|
||
vi.spyOn(action.hashCalculator, 'calculateFileHash').mockRejectedValue(
|
||
new ActionError(ErrorType.HASH_CALCULATION_FAILED, 'Permission denied', restrictedFile)
|
||
);
|
||
|
||
vi.spyOn(ActionInputs, 'getInput').mockImplementation(name => {
|
||
if (name === 'algorithm') return 'sha256';
|
||
return '';
|
||
});
|
||
vi.spyOn(ActionInputs, 'getMultilineInput').mockImplementation(name => {
|
||
if (name === 'files') return [restrictedFile];
|
||
return [];
|
||
});
|
||
vi.spyOn(ActionInputs, 'getBooleanInput').mockReturnValue(false);
|
||
|
||
const consoleSpy = vi.spyOn(console, 'log');
|
||
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {});
|
||
|
||
await action.run();
|
||
|
||
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('::error::'));
|
||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||
|
||
cleanupTestFiles(restrictedFile);
|
||
cleanupTestDirs(tempDir);
|
||
consoleSpy.mockRestore();
|
||
exitSpy.mockRestore();
|
||
});
|
||
});
|
||
|
||
describe('测试 FilesHashAction 边界情况 - 算法和配置错误', () => {
|
||
it('应该处理不支持算法', async () => {
|
||
const action = new FilesHashAction();
|
||
const tempDir = await createTestDir();
|
||
const testFile = path.join(tempDir, 'test.txt');
|
||
|
||
await createTestFile(testFile, 'content');
|
||
|
||
vi.spyOn(ActionInputs, 'getInput').mockImplementation(name => {
|
||
if (name === 'algorithm') return 'unsupported-algorithm';
|
||
return '';
|
||
});
|
||
vi.spyOn(ActionInputs, 'getMultilineInput').mockImplementation(name => {
|
||
if (name === 'files') return [testFile];
|
||
return [];
|
||
});
|
||
vi.spyOn(ActionInputs, 'getBooleanInput').mockReturnValue(false);
|
||
|
||
const consoleSpy = vi.spyOn(console, 'log');
|
||
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {});
|
||
|
||
await action.run();
|
||
|
||
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('::error::'));
|
||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||
|
||
cleanupTestFiles(testFile);
|
||
cleanupTestDirs(tempDir);
|
||
consoleSpy.mockRestore();
|
||
exitSpy.mockRestore();
|
||
});
|
||
});
|
||
|
||
describe('测试 FilesHashAction 边界情况 - 输出和格式边界情况', () => {
|
||
it('应该处理很长的文件列表输出', async () => {
|
||
const action = new FilesHashAction();
|
||
const tempDir = await createTestDir();
|
||
|
||
// 创建多个文件
|
||
const files = [];
|
||
for (let i = 0; i < 10; i++) {
|
||
const file = path.join(tempDir, `file${i}.txt`);
|
||
await createTestFile(file, `content${i}`);
|
||
files.push(file);
|
||
}
|
||
|
||
vi.spyOn(ActionInputs, 'getInput').mockImplementation(name => {
|
||
if (name === 'algorithm') return 'sha256';
|
||
return '';
|
||
});
|
||
vi.spyOn(ActionInputs, 'getMultilineInput').mockImplementation(name => {
|
||
if (name === 'files') return [path.join(tempDir, '*.txt')];
|
||
return [];
|
||
});
|
||
vi.spyOn(ActionInputs, 'getBooleanInput').mockReturnValue(false);
|
||
|
||
const consoleSpy = vi.spyOn(console, 'log');
|
||
|
||
await action.run();
|
||
|
||
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('File count: 10'));
|
||
|
||
cleanupTestFiles(...files);
|
||
cleanupTestDirs(tempDir);
|
||
consoleSpy.mockRestore();
|
||
});
|
||
|
||
it('应该处理空文件内容', async () => {
|
||
const action = new FilesHashAction();
|
||
const tempDir = await createTestDir();
|
||
const emptyFile = path.join(tempDir, 'empty.txt');
|
||
|
||
await createTestFile(emptyFile, '');
|
||
|
||
vi.spyOn(ActionInputs, 'getInput').mockImplementation(name => {
|
||
if (name === 'algorithm') return 'sha256';
|
||
return '';
|
||
});
|
||
vi.spyOn(ActionInputs, 'getMultilineInput').mockImplementation(name => {
|
||
if (name === 'files') return [emptyFile];
|
||
return [];
|
||
});
|
||
vi.spyOn(ActionInputs, 'getBooleanInput').mockReturnValue(false);
|
||
|
||
const consoleSpy = vi.spyOn(console, 'log');
|
||
|
||
await action.run();
|
||
|
||
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Combined hash:'));
|
||
|
||
cleanupTestFiles(emptyFile);
|
||
cleanupTestDirs(tempDir);
|
||
consoleSpy.mockRestore();
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('测试 FilesHashAction 集成测试', () => {
|
||
describe('测试 FilesHashAction 完整工作流', () => {
|
||
const testFiles = ['integration-test/file1.txt', 'integration-test/file2.js', 'integration-test/subdir/file3.json'];
|
||
|
||
beforeEach(async () => {
|
||
// 创建测试文件
|
||
await createTestFile(testFiles[0], 'File 1 content');
|
||
await createTestFile(testFiles[1], 'console.log("Hello");');
|
||
await createTestFile(testFiles[2], '{"test": true}');
|
||
|
||
// 模拟环境变量
|
||
process.env.INPUT_FILES = 'integration-test/**/*';
|
||
process.env.INPUT_ALGORITHM = 'sha256';
|
||
});
|
||
|
||
afterEach(async () => {
|
||
// 清理环境变量
|
||
delete process.env.INPUT_FILES;
|
||
delete process.env.INPUT_ALGORITHM;
|
||
|
||
// 清理测试文件
|
||
await cleanupTestFiles(testFiles);
|
||
await cleanupTestDirs(['integration-test/subdir', 'integration-test']);
|
||
});
|
||
|
||
it('应该成功完成完整工作流程', async () => {
|
||
const action = new FilesHashAction();
|
||
|
||
// 测试文件发现
|
||
const patterns = action.fileDiscovery.parseFilePatterns(['integration-test/**/*']);
|
||
const foundFiles = await action.fileDiscovery.findFiles(patterns);
|
||
|
||
expect(foundFiles.length).toBeGreaterThanOrEqual(3);
|
||
|
||
// 测试哈希计算
|
||
const hash = await action.hashCalculator.calculateCombinedHash(foundFiles, 'sha256');
|
||
expect(hash).toHaveLength(64);
|
||
});
|
||
});
|
||
});
|