Files
files-hash/tests/index.test.js
ren c9a9632ccc
All checks were successful
CI / 测试 (push) Successful in 20s
feat: 优化项目结构
2025-10-21 12:05:15 +08:00

1145 lines
37 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 '../src/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);
});
});
});