464 lines
17 KiB
C#
464 lines
17 KiB
C#
/*
|
||
* @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
|
||
{
|
||
/// <summary>
|
||
/// SpriteAssetEditorWindow 的自动化测试
|
||
/// 通过菜单 Tools/测试/表情图集生成测试 运行
|
||
/// </summary>
|
||
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());
|
||
}
|
||
|
||
// ============================================================
|
||
// 测试用例
|
||
// ============================================================
|
||
|
||
/// <summary>
|
||
/// 测试 1:文件名合法性校验
|
||
/// </summary>
|
||
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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 测试 2:不存在的文件夹
|
||
/// </summary>
|
||
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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 测试 3:空文件夹
|
||
/// </summary>
|
||
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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 测试 4:混合有效与无效文件
|
||
/// </summary>
|
||
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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 测试 5:完整管线流程 (Step 1-4)
|
||
/// </summary>
|
||
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<TMP_SpriteAsset>(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<string>();
|
||
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);
|
||
}
|
||
|
||
// ============================================================
|
||
// 辅助方法
|
||
// ============================================================
|
||
|
||
/// <summary>
|
||
/// 断言检查
|
||
/// </summary>
|
||
private static void Assert(string description, bool expected, bool actual)
|
||
{
|
||
if (expected == actual)
|
||
{
|
||
_passCount++;
|
||
_report.AppendLine($" [通过] {description}");
|
||
}
|
||
else
|
||
{
|
||
_failCount++;
|
||
_report.AppendLine($" [失败] {description}(期望={expected}, 实际={actual})");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建测试用编辑器窗口实例(不显示)
|
||
/// </summary>
|
||
private static SpriteAssetEditorWindow CreateTestWindow()
|
||
{
|
||
return ScriptableObject.CreateInstance<SpriteAssetEditorWindow>();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 销毁测试窗口
|
||
/// </summary>
|
||
private static void DestroyTestWindow(SpriteAssetEditorWindow window)
|
||
{
|
||
if (window != null)
|
||
{
|
||
ScriptableObject.DestroyImmediate(window);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 确保测试文件夹存在
|
||
/// </summary>
|
||
private static void EnsureTestFolders()
|
||
{
|
||
EnsureFolder(TestRootFolder);
|
||
EnsureFolder(TestSourceFolder);
|
||
EnsureFolder(TestOutputFolder);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 确保 Assets 路径下的文件夹存在
|
||
/// </summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清空文件夹内容
|
||
/// </summary>
|
||
private static void ClearFolder(string fullPath)
|
||
{
|
||
if (!Directory.Exists(fullPath)) return;
|
||
|
||
foreach (var file in Directory.GetFiles(fullPath))
|
||
{
|
||
File.Delete(file);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建纯色测试 PNG
|
||
/// </summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清理所有测试文件
|
||
/// </summary>
|
||
private static void Cleanup()
|
||
{
|
||
if (AssetDatabase.IsValidFolder(TestRootFolder))
|
||
{
|
||
AssetDatabase.DeleteAsset(TestRootFolder);
|
||
AssetDatabase.Refresh();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将 Assets/ 路径转为系统绝对路径
|
||
/// </summary>
|
||
private static string AssetPathToFullPath(string assetPath)
|
||
{
|
||
string dataPath = Application.dataPath.Replace('\\', '/');
|
||
string projectRoot = dataPath.Substring(0, dataPath.Length - "Assets".Length);
|
||
return projectRoot + assetPath;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 通过反射设置私有字段
|
||
/// </summary>
|
||
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}");
|
||
}
|
||
}
|
||
}
|
||
}
|