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); }); }); });