using FsCheck; using FsCheck.Xunit; using System.IO.Compression; using WorkCameraExport.Models; using Xunit; namespace WorkCameraExport.Tests { /// /// ZIP 目录结构属性测试 /// Feature: work-camera-2.0.1, Property 9: ZIP 目录结构 /// Validates: Requirements 9.2, 9.3 /// public class ZipDirectoryStructurePropertyTests : IDisposable { private readonly string _testOutputDir; public ZipDirectoryStructurePropertyTests() { _testOutputDir = Path.Combine(Path.GetTempPath(), $"ZipStructure_Test_{Guid.NewGuid():N}"); Directory.CreateDirectory(_testOutputDir); } public void Dispose() { try { if (Directory.Exists(_testOutputDir)) { Directory.Delete(_testOutputDir, true); } } catch { // 忽略清理错误 } } /// /// Property 9: ZIP 目录结构 /// For any 下载的照片 ZIP 文件,解压后的目录结构应包含: /// 当日照片/、参与人员/{人员姓名}/、工作内容/{工作内容}/、部门/{部门名称}/ 四个分类目录。 /// Validates: Requirements 9.2, 9.3 /// [Property(MaxTest = 100)] public Property ZipStructure_ShouldContainRequiredDirectories() { return Prop.ForAll( Arb.From(), Arb.From(), (workerCountGen, seedGen) => { var workerCount = (workerCountGen.Get % 3) + 1; // 1-3 个工人 var seed = seedGen.Get; var deptName = $"部门{seed % 100}"; var content = $"内容{seed % 100}"; var workers = Enumerable.Range(1, workerCount) .Select(i => $"工人{seed}_{i}") .ToList(); var yearMonth = "2025-01"; var recordDate = new DateTime(2025, 1, 15); // 创建测试目录结构 var directories = CreateDirectoryStructure( _testOutputDir, yearMonth, recordDate, deptName, content, workers); // 验证目录结构 var basePath = Path.Combine(_testOutputDir, "workfiles", "202501", "20250115"); // 1. 验证当日照片目录存在 var dailyDirExists = Directory.Exists(Path.Combine(basePath, "当日照片")); // 2. 验证参与人员目录存在 var workerDirsExist = workers.All(w => Directory.Exists(Path.Combine(basePath, "参与人员", w))); // 3. 验证工作内容目录存在 var contentDirExists = Directory.Exists(Path.Combine(basePath, "工作内容", content)); // 4. 验证部门目录存在 var deptDirExists = Directory.Exists(Path.Combine(basePath, "部门", deptName)); // 清理 CleanupDirectories(_testOutputDir); return (dailyDirExists && workerDirsExist && contentDirExists && deptDirExists) .Label($"All required directories should exist: daily={dailyDirExists}, workers={workerDirsExist}, content={contentDirExists}, dept={deptDirExists}"); }); } /// /// Property 9 扩展: 目录结构应按日期组织 /// [Property(MaxTest = 100)] public Property ZipStructure_ShouldBeOrganizedByDate() { return Prop.ForAll( Arb.From(), Arb.From(), (monthGen, dayGen) => { var month = (monthGen.Get % 12) + 1; // 1-12 var day = (dayGen.Get % 28) + 1; // 1-28 var yearMonth = $"2025-{month:D2}"; var recordDate = new DateTime(2025, month, day); var expectedYYYYMM = $"2025{month:D2}"; var expectedYYYYMMDD = $"2025{month:D2}{day:D2}"; var directories = CreateDirectoryStructure( _testOutputDir, yearMonth, recordDate, "测试部门", "测试内容", new List { "测试工人" }); // 验证目录路径包含正确的日期格式 var basePath = Path.Combine(_testOutputDir, "workfiles", expectedYYYYMM, expectedYYYYMMDD); var pathExists = Directory.Exists(basePath); CleanupDirectories(_testOutputDir); return pathExists.Label($"Directory path should contain {expectedYYYYMM}/{expectedYYYYMMDD}"); }); } /// /// Property 9 扩展: 每个工人应有独立的目录 /// [Property(MaxTest = 100)] public Property ZipStructure_EachWorker_ShouldHaveOwnDirectory() { return Prop.ForAll( Arb.From(), (workerCountGen) => { var workerCount = (workerCountGen.Get % 5) + 1; // 1-5 个工人 var workers = Enumerable.Range(1, workerCount) .Select(i => $"工人{i}") .ToList(); var yearMonth = "2025-01"; var recordDate = new DateTime(2025, 1, 15); var directories = CreateDirectoryStructure( _testOutputDir, yearMonth, recordDate, "测试部门", "测试内容", workers); var basePath = Path.Combine(_testOutputDir, "workfiles", "202501", "20250115", "参与人员"); // 验证每个工人都有独立目录 var allWorkersHaveDir = workers.All(w => Directory.Exists(Path.Combine(basePath, w))); // 验证目录数量正确 var dirCount = Directory.Exists(basePath) ? Directory.GetDirectories(basePath).Length : 0; CleanupDirectories(_testOutputDir); return (allWorkersHaveDir && dirCount == workerCount) .Label($"Each of {workerCount} workers should have own directory, found {dirCount}"); }); } /// /// Property 9 扩展: 空工人列表应只创建当日照片目录 /// [Fact] public void ZipStructure_EmptyWorkers_ShouldStillCreateDailyDirectory() { var yearMonth = "2025-01"; var recordDate = new DateTime(2025, 1, 15); var directories = CreateDirectoryStructure( _testOutputDir, yearMonth, recordDate, "测试部门", "测试内容", new List()); var basePath = Path.Combine(_testOutputDir, "workfiles", "202501", "20250115"); // 当日照片目录应该存在 Assert.True(Directory.Exists(Path.Combine(basePath, "当日照片"))); // 参与人员目录下应该没有子目录 var workerBasePath = Path.Combine(basePath, "参与人员"); if (Directory.Exists(workerBasePath)) { Assert.Empty(Directory.GetDirectories(workerBasePath)); } CleanupDirectories(_testOutputDir); } #region 辅助方法 /// /// 创建目录结构(模拟 ExportService.CreateDirectoryStructure) /// private List CreateDirectoryStructure( string baseDir, string yearMonth, DateTime recordDate, string deptName, string content, List workers) { var directories = new List(); var yyyyMM = yearMonth.Replace("-", ""); var yyyyMMdd = recordDate.ToString("yyyyMMdd"); var basePath = Path.Combine(baseDir, "workfiles", yyyyMM, yyyyMMdd); // 1. 当日照片目录 var dailyDir = Path.Combine(basePath, "当日照片"); EnsureDirectoryExists(dailyDir); directories.Add(dailyDir); // 2. 参与人员目录 foreach (var worker in workers.Where(w => !string.IsNullOrEmpty(w))) { var workerDir = Path.Combine(basePath, "参与人员", SanitizeFileName(worker)); EnsureDirectoryExists(workerDir); directories.Add(workerDir); } // 3. 工作内容目录 if (!string.IsNullOrEmpty(content)) { var contentDir = Path.Combine(basePath, "工作内容", SanitizeFileName(content)); EnsureDirectoryExists(contentDir); directories.Add(contentDir); } // 4. 部门目录 if (!string.IsNullOrEmpty(deptName)) { var deptDir = Path.Combine(basePath, "部门", SanitizeFileName(deptName)); EnsureDirectoryExists(deptDir); directories.Add(deptDir); } return directories; } /// /// 确保目录存在 /// private void EnsureDirectoryExists(string path) { if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } } /// /// 清理文件名中的非法字符 /// private string SanitizeFileName(string fileName) { var invalidChars = Path.GetInvalidFileNameChars(); var sanitized = fileName; foreach (var c in invalidChars) { sanitized = sanitized.Replace(c, '_'); } if (sanitized.Length > 50) { sanitized = sanitized.Substring(0, 50); } return sanitized; } /// /// 清理测试目录 /// private void CleanupDirectories(string baseDir) { try { var workfilesDir = Path.Combine(baseDir, "workfiles"); if (Directory.Exists(workfilesDir)) { Directory.Delete(workfilesDir, true); } } catch { // 忽略清理错误 } } #endregion } }