/* * @Author: Claude * @Description: TMP 表情图集生成器的编辑器测试脚本 * @Date: 2026年04月03日 * @Modify: */ using System; using System.Collections.Generic; using System.IO; using System.Text; using TMPro; using UnityEditor; using UnityEngine; namespace Logic.Editor { /// /// SpriteAssetEditorWindow 的自动化测试 /// 通过菜单 Tools/测试/表情图集生成测试 运行 /// public static class SpriteAssetEditorWindowTest { private const string TestRootFolder = "Assets/Tests/SpriteAssetTest"; private const string TestSourceFolder = "Assets/Tests/SpriteAssetTest/Source"; private const string TestOutputFolder = "Assets/Tests/SpriteAssetTest/Output"; private static int _passCount; private static int _failCount; private static StringBuilder _report; // ============================================================ // 菜单入口 // ============================================================ [MenuItem("Tools/测试/表情图集生成测试")] private static void RunAllTests() { _passCount = 0; _failCount = 0; _report = new StringBuilder(); _report.AppendLine("===== TMP SpriteAsset 生成器测试报告 =====\n"); try { TestFileNameValidation(); TestInvalidFolder(); TestEmptyFolder(); TestMixedValidInvalid(); TestFullPipeline(); } catch (Exception e) { _report.AppendLine($"\n[致命错误] 测试过程中发生未捕获异常:{e.Message}\n{e.StackTrace}"); _failCount++; } finally { Cleanup(); } // 汇总 _report.AppendLine($"\n===== 测试完成 ====="); _report.AppendLine($"通过:{_passCount} 失败:{_failCount} 总计:{_passCount + _failCount}"); string title = _failCount == 0 ? "全部通过" : $"{_failCount} 项失败"; EditorUtility.DisplayDialog($"测试结果 - {title}", _report.ToString(), "确定"); Debug.Log(_report.ToString()); } // ============================================================ // 测试用例 // ============================================================ /// /// 测试 1:文件名合法性校验 /// private static void TestFileNameValidation() { _report.AppendLine("【测试 1】文件名合法性校验"); // 合法名称 Assert("smile", true, SpriteAssetEditorWindow.IsValidSpriteName("smile")); Assert("emoji_01", true, SpriteAssetEditorWindow.IsValidSpriteName("emoji_01")); Assert("a", true, SpriteAssetEditorWindow.IsValidSpriteName("a")); Assert("test_icon_123", true, SpriteAssetEditorWindow.IsValidSpriteName("test_icon_123")); Assert("abc123", true, SpriteAssetEditorWindow.IsValidSpriteName("abc123")); // 非法名称 Assert("Smile(大写)", false, SpriteAssetEditorWindow.IsValidSpriteName("Smile")); Assert("emoji 01(空格)", false, SpriteAssetEditorWindow.IsValidSpriteName("emoji 01")); Assert("表情(中文)", false, SpriteAssetEditorWindow.IsValidSpriteName("表情")); Assert("emoji-01(连字符)", false, SpriteAssetEditorWindow.IsValidSpriteName("emoji-01")); Assert("emoji.01(点号)", false, SpriteAssetEditorWindow.IsValidSpriteName("emoji.01")); Assert("空字符串", false, SpriteAssetEditorWindow.IsValidSpriteName("")); Assert("null", false, SpriteAssetEditorWindow.IsValidSpriteName(null)); Assert("ALLCAPS(全大写)", false, SpriteAssetEditorWindow.IsValidSpriteName("ALLCAPS")); Assert("has space(有空格)", false, SpriteAssetEditorWindow.IsValidSpriteName("has space")); Assert("special!char(特殊字符)", false, SpriteAssetEditorWindow.IsValidSpriteName("special!char")); _report.AppendLine(); } /// /// 测试 2:不存在的文件夹 /// private static void TestInvalidFolder() { _report.AppendLine("【测试 2】不存在的文件夹处理"); var window = CreateTestWindow(); var result = window.ValidateImages("Assets/NonExistent/FakeFolder_12345"); Assert("不存在的路径返回 null", true, result == null); DestroyTestWindow(window); _report.AppendLine(); } /// /// 测试 3:空文件夹 /// private static void TestEmptyFolder() { _report.AppendLine("【测试 3】空文件夹处理"); EnsureTestFolders(); var window = CreateTestWindow(); string fullPath = Path.Combine(Application.dataPath, TestSourceFolder.Substring("Assets/".Length)); // 确保是空的 foreach (var f in Directory.GetFiles(fullPath)) { File.Delete(f); } var result = window.ValidateImages(fullPath); Assert("空文件夹返回空列表", true, result != null && result.Count == 0); DestroyTestWindow(window); _report.AppendLine(); } /// /// 测试 4:混合有效与无效文件 /// private static void TestMixedValidInvalid() { _report.AppendLine("【测试 4】混合有效/无效文件"); EnsureTestFolders(); string fullSourcePath = Path.Combine(Application.dataPath, TestSourceFolder.Substring("Assets/".Length)); // 清理 ClearFolder(fullSourcePath); // 创建有效的 PNG 文件 CreateTestPNG(fullSourcePath, "valid_icon", 64, 64, Color.red); CreateTestPNG(fullSourcePath, "another_valid", 64, 64, Color.blue); // 创建无效文件 CreateTestPNG(fullSourcePath, "Invalid_Name", 64, 64, Color.green); // 大写 File.WriteAllText(Path.Combine(fullSourcePath, "readme.txt"), "test"); // 非 PNG AssetDatabase.Refresh(); var window = CreateTestWindow(); var result = window.ValidateImages(fullSourcePath); Assert("有效图片数量 == 2", true, result != null && result.Count == 2); if (result != null) { bool hasValidIcon = result.Exists(s => s.Name == "valid_icon"); bool hasAnotherValid = result.Exists(s => s.Name == "another_valid"); bool hasInvalid = result.Exists(s => s.Name == "Invalid_Name"); Assert("包含 valid_icon", true, hasValidIcon); Assert("包含 another_valid", true, hasAnotherValid); Assert("不包含 Invalid_Name", true, !hasInvalid); } DestroyTestWindow(window); _report.AppendLine(); } /// /// 测试 5:完整管线流程 (Step 1-4) /// private static void TestFullPipeline() { _report.AppendLine("【测试 5】完整管线流程 (Step 1→4)"); EnsureTestFolders(); string fullSourcePath = Path.Combine(Application.dataPath, TestSourceFolder.Substring("Assets/".Length)); string fullOutputPath = Path.Combine(Application.dataPath, TestOutputFolder.Substring("Assets/".Length)); // 清理 ClearFolder(fullSourcePath); ClearFolder(fullOutputPath); // 创建 3 张测试碎图 int testSpriteCount = 3; CreateTestPNG(fullSourcePath, "emoji_smile", 64, 64, Color.yellow); CreateTestPNG(fullSourcePath, "emoji_cry", 64, 64, Color.blue); CreateTestPNG(fullSourcePath, "emoji_angry", 64, 64, Color.red); AssetDatabase.Refresh(); var window = CreateTestWindow(); string assetName = "TestSpriteAsset"; // Step 1: 验证 _report.AppendLine(" Step 1: 验证图片..."); var validSprites = window.ValidateImages(fullSourcePath); Assert("Step1 - 有效图片数 == 3", true, validSprites != null && validSprites.Count == testSpriteCount); if (validSprites == null || validSprites.Count == 0) { _report.AppendLine(" Step 1 失败,跳过后续步骤"); DestroyTestWindow(window); return; } // Step 2: 合并大图 _report.AppendLine(" Step 2: 合并大图..."); // 需要通过反射或直接调用设置 _assetName SetPrivateField(window, "_assetName", assetName); SetPrivateField(window, "_padding", 2); var packResult = window.PackAtlasTexture(validSprites, TestOutputFolder, 1024); Assert("Step2 - PackResult 不为 null", true, packResult.HasValue); if (!packResult.HasValue) { _report.AppendLine(" Step 2 失败,跳过后续步骤"); DestroyTestWindow(window); return; } Assert("Step2 - 图集宽度 > 0", true, packResult.Value.atlasWidth > 0); Assert("Step2 - 图集高度 > 0", true, packResult.Value.atlasHeight > 0); Assert("Step2 - Rect 数量 == 3", true, packResult.Value.rects.Length == testSpriteCount); string atlasAssetPath = packResult.Value.atlasAssetPath; Assert("Step2 - 图集文件存在", true, File.Exists(AssetPathToFullPath(atlasAssetPath))); // Step 3: 设置切分 _report.AppendLine(" Step 3: 设置切分属性..."); var names = new string[validSprites.Count]; for (int i = 0; i < validSprites.Count; i++) names[i] = validSprites[i].Name; bool sliceOk = window.SetupTextureImporter( atlasAssetPath, packResult.Value.rects, names, packResult.Value.atlasWidth, packResult.Value.atlasHeight); Assert("Step3 - 切分设置成功", true, sliceOk); if (!sliceOk) { _report.AppendLine(" Step 3 失败,跳过后续步骤"); DestroyTestWindow(window); return; } // 验证 Sprite 切片 var allAssets = AssetDatabase.LoadAllAssetsAtPath(atlasAssetPath); int spriteCount = 0; foreach (var asset in allAssets) { if (asset is Sprite) spriteCount++; } Assert($"Step3 - 切分后 Sprite 数量 == {testSpriteCount}", true, spriteCount == testSpriteCount); // Step 4: 生成 TMP_SpriteAsset _report.AppendLine(" Step 4: 生成 TMP_SpriteAsset..."); bool assetOk = window.GenerateTMPSpriteAsset(atlasAssetPath, TestOutputFolder); Assert("Step4 - 生成成功", true, assetOk); // 验证生成的 asset string spriteAssetPath = $"{TestOutputFolder}/{assetName}.asset"; var spriteAsset = AssetDatabase.LoadAssetAtPath(spriteAssetPath); Assert("Step4 - SpriteAsset 文件存在", true, spriteAsset != null); if (spriteAsset != null) { Assert($"Step4 - SpriteCharacterTable 数量 == {testSpriteCount}", true, spriteAsset.spriteCharacterTable != null && spriteAsset.spriteCharacterTable.Count == testSpriteCount); Assert($"Step4 - SpriteGlyphTable 数量 == {testSpriteCount}", true, spriteAsset.spriteGlyphTable != null && spriteAsset.spriteGlyphTable.Count == testSpriteCount); Assert("Step4 - spriteSheet 不为 null", true, spriteAsset.spriteSheet != null); Assert("Step4 - material 不为 null", true, spriteAsset.material != null); // 验证名称映射 if (spriteAsset.spriteCharacterTable != null) { var charNames = new HashSet(); foreach (var c in spriteAsset.spriteCharacterTable) { charNames.Add(c.name); } Assert("Step4 - 包含 emoji_smile", true, charNames.Contains("emoji_smile")); Assert("Step4 - 包含 emoji_cry", true, charNames.Contains("emoji_cry")); Assert("Step4 - 包含 emoji_angry", true, charNames.Contains("emoji_angry")); } } DestroyTestWindow(window); } // ============================================================ // 辅助方法 // ============================================================ /// /// 断言检查 /// private static void Assert(string description, bool expected, bool actual) { if (expected == actual) { _passCount++; _report.AppendLine($" [通过] {description}"); } else { _failCount++; _report.AppendLine($" [失败] {description}(期望={expected}, 实际={actual})"); } } /// /// 创建测试用编辑器窗口实例(不显示) /// private static SpriteAssetEditorWindow CreateTestWindow() { return ScriptableObject.CreateInstance(); } /// /// 销毁测试窗口 /// private static void DestroyTestWindow(SpriteAssetEditorWindow window) { if (window != null) { ScriptableObject.DestroyImmediate(window); } } /// /// 确保测试文件夹存在 /// private static void EnsureTestFolders() { EnsureFolder(TestRootFolder); EnsureFolder(TestSourceFolder); EnsureFolder(TestOutputFolder); } /// /// 确保 Assets 路径下的文件夹存在 /// private static void EnsureFolder(string assetPath) { if (AssetDatabase.IsValidFolder(assetPath)) return; string parent = Path.GetDirectoryName(assetPath).Replace('\\', '/'); string folderName = Path.GetFileName(assetPath); if (!AssetDatabase.IsValidFolder(parent)) { EnsureFolder(parent); } AssetDatabase.CreateFolder(parent, folderName); } /// /// 清空文件夹内容 /// private static void ClearFolder(string fullPath) { if (!Directory.Exists(fullPath)) return; foreach (var file in Directory.GetFiles(fullPath)) { File.Delete(file); } } /// /// 创建纯色测试 PNG /// private static void CreateTestPNG(string folderFullPath, string name, int width, int height, Color color) { var texture = new Texture2D(width, height, TextureFormat.RGBA32, false); var pixels = new Color[width * height]; for (int i = 0; i < pixels.Length; i++) { pixels[i] = color; } texture.SetPixels(pixels); texture.Apply(); string filePath = Path.Combine(folderFullPath, $"{name}.png"); File.WriteAllBytes(filePath, texture.EncodeToPNG()); UnityEngine.Object.DestroyImmediate(texture); } /// /// 清理所有测试文件 /// private static void Cleanup() { if (AssetDatabase.IsValidFolder(TestRootFolder)) { AssetDatabase.DeleteAsset(TestRootFolder); AssetDatabase.Refresh(); } } /// /// 将 Assets/ 路径转为系统绝对路径 /// private static string AssetPathToFullPath(string assetPath) { string dataPath = Application.dataPath.Replace('\\', '/'); string projectRoot = dataPath.Substring(0, dataPath.Length - "Assets".Length); return projectRoot + assetPath; } /// /// 通过反射设置私有字段 /// private static void SetPrivateField(object obj, string fieldName, object value) { var field = obj.GetType().GetField(fieldName, System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); if (field != null) { field.SetValue(obj, value); } else { Debug.LogWarning($"[SpriteAssetTest] 未找到字段:{fieldName}"); } } } }