TH1/Unity/Assets/Scripts/TH1_Logic/Editor/SpriteAssetEditorWindowTest.cs
2026-04-03 18:47:09 +08:00

464 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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