/*
* @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}");
}
}
}
}