Files
2026-11-01 23:39:55 +02:00

376 lines
14 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Unity.PerformanceTesting.Data;
using Unity.PerformanceTesting.Editor;
using Unity.PerformanceTesting.Runtime;
using UnityEditor;
using UnityEngine;
using UnityEngine.TestTools;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
[assembly: PrebuildSetup(typeof(TestRunBuilder))]
[assembly: PostBuildCleanup(typeof(TestRunBuilder))]
namespace Unity.PerformanceTesting.Editor
{
internal class TestRunBuilder : IPrebuildSetup, IPostBuildCleanup, IPreprocessBuildWithReport, IPostprocessBuildWithReport
{
private const string cleanResources = "PT_ResourcesCleanup";
private const bool EnableDetailedDiffLogging = true;
// UUM-132359: Library/ cache for the Resources/PerformanceTestRun*.json .meta files, so their GUID survives across builds.
private const string MetaCacheDir = "Library/PerformanceTesting/MetaCache";
public int callbackOrder
{
get { return 0; }
}
public void OnPreprocessBuild(BuildReport report)
{
if (!ShouldWriteBuildFiles(report.summary.options))
{
// Sweep artifacts a previously aborted test build may have left
// behind, so they don't get baked into a non-test player.
Cleanup();
return;
}
CreateResourcesFolder();
// UUM-132359: restore the cached .meta before writing, so the asset keeps its GUID instead of getting a new one.
RestoreCachedMeta(Utils.TestRunPath);
RestoreCachedMeta(Utils.RunSettingsPath);
var run = CreateBuildInfo();
SaveToStorage(run, Utils.TestRunPath);
var settings = new RunSettings(Environment.GetCommandLineArgs());
SaveToStorage(settings, Utils.RunSettingsPath);
}
public void OnPostprocessBuild(BuildReport report)
{
if (ShouldWriteBuildFiles(report.summary.options))
{
// UUM-132359: stash the .meta before Cleanup() deletes it, so the next test build can reuse the same GUID.
StashMeta(Utils.TestRunPath);
StashMeta(Utils.RunSettingsPath);
}
// Runs unconditionally so a non-test build also sweeps up files
// left behind by a previously aborted test build.
Cleanup();
}
// The run info and settings files are only ever read by performance tests
// executing inside the player, so builds without test assemblies never need them.
internal static bool ShouldWriteBuildFiles(BuildOptions options)
{
return (options & BuildOptions.IncludeTestAssemblies) != 0;
}
public void Setup()
{
SessionState.SetBool(cleanResources, false);
var run = CreateRunInfo();
SaveToPrefs(run, Utils.PlayerPrefKeyRunJSON);
var settings = new RunSettings(Environment.GetCommandLineArgs());
SaveToPrefs(settings, Utils.PlayerPrefKeySettingsJSON);
}
#if !UNITY_2021_1_OR_NEWER
private static List<string> cachedDependencies;
#endif
static List<string> GetPackageDependencies()
{
#if !UNITY_2021_1_OR_NEWER
if (cachedDependencies != null)
return cachedDependencies;
#endif
IEnumerable<UnityEditor.PackageManager.PackageInfo> packages;
#if !UNITY_2021_1_OR_NEWER
var listRequest = UnityEditor.PackageManager.Client.List(true);
while (!listRequest.IsCompleted)
System.Threading.Thread.Sleep(10);
if (listRequest.Status == UnityEditor.PackageManager.StatusCode.Failure)
Debug.LogError("Failed to list local packages");
packages = new List<UnityEditor.PackageManager.PackageInfo>(listRequest.Result);
#else
packages = UnityEditor.PackageManager.PackageInfo.GetAllRegisteredPackages();
#endif
var reformated = packages.Select(p => $"{p.name}@{p.version}").OrderBy(p => p).ToList();
#if !UNITY_2021_1_OR_NEWER
cachedDependencies = reformated;
#endif
return reformated;
}
public void Cleanup()
{
bool modifiedAssets = false;
if (DeleteFileAndMeta(Utils.TestRunPath))
{
modifiedAssets = true;
}
if (DeleteFileAndMeta(Utils.RunSettingsPath))
{
modifiedAssets = true;
}
// Only delete the Resources folder if we created it and it is empty by
// now - anything still inside was put there by the user, e.g. while a
// stale cleanup flag was left behind by an aborted build.
if (SessionState.GetBool(cleanResources, false) && Directory.Exists(Utils.ResourcesPath)
&& Directory.GetFileSystemEntries(Utils.ResourcesPath).Length == 0)
{
try
{
// Non-recursive on purpose: if an external process created a file
// in the window since the emptiness check, fail instead of
// deleting it. Cleanup also runs during build preprocessing,
// where an unhandled exception would fail the build, so
// deletion failures only warn - a leftover empty folder is acceptable.
Directory.Delete(Utils.ResourcesPath, false);
modifiedAssets = true;
}
catch (Exception e) when (e is IOException || e is UnauthorizedAccessException)
{
Debug.LogWarning($"[TestRunBuilder] Could not delete '{Utils.ResourcesPath}': {e.Message}");
}
// Only remove the folder's meta once the folder is really gone -
// deleting the meta of an existing folder would make Unity
// regenerate it with a new GUID.
if (!Directory.Exists(Utils.ResourcesPath))
{
modifiedAssets |= TryDeleteFile(Utils.ResourcesPath + ".meta");
}
}
// The flag describes at most one build cycle; reset it so it cannot
// leak into a later, unrelated build.
SessionState.SetBool(cleanResources, false);
// Scrub the legacy EditorPrefs flag older package versions may have
// left behind; it is no longer read.
EditorPrefs.DeleteKey(cleanResources);
// Only refresh the AssetDatabase if we actually deleted performance test files
if (modifiedAssets)
{
AssetDatabase.Refresh();
}
}
private bool DeleteFileAndMeta(string path)
{
// Non-short-circuiting so the meta file is attempted even if the
// main file could not be deleted.
return TryDeleteFile(path) | TryDeleteFile(path + ".meta");
}
private static bool TryDeleteFile(string path)
{
if (!File.Exists(path))
{
return false;
}
try
{
File.Delete(path);
return true;
}
catch (Exception e) when (e is IOException || e is UnauthorizedAccessException)
{
// Cleanup runs during build preprocessing, where an unhandled
// exception (e.g. from a transiently locked file) would fail the
// build; the next cleanup pass will retry.
Debug.LogWarning($"[TestRunBuilder] Could not delete '{path}': {e.Message}");
return false;
}
}
// UUM-132359: copy the asset's .meta into MetaCacheDir before we delete it, preserving its GUID.
private static void StashMeta(string assetPath)
{
var srcMeta = assetPath + ".meta";
if (!File.Exists(srcMeta)) return;
try
{
Directory.CreateDirectory(MetaCacheDir);
var destMeta = Path.Combine(MetaCacheDir, Path.GetFileName(srcMeta));
File.Copy(srcMeta, destMeta, overwrite: true);
}
catch (Exception e) when (e is IOException || e is UnauthorizedAccessException)
{
Debug.LogWarning($"[TestRunBuilder] Could not cache '{srcMeta}': {e.Message}");
}
}
// UUM-132359: counterpart to StashMeta - restores the cached .meta before the asset is rewritten. No-op if the
// cache is empty or a .meta already exists at the destination.
private static void RestoreCachedMeta(string assetPath)
{
var destMeta = assetPath + ".meta";
if (File.Exists(destMeta)) return;
var cachedMeta = Path.Combine(MetaCacheDir, Path.GetFileName(destMeta));
if (!File.Exists(cachedMeta)) return;
try
{
var destDir = Path.GetDirectoryName(destMeta);
if (!string.IsNullOrEmpty(destDir) && !Directory.Exists(destDir))
{
Directory.CreateDirectory(destDir);
}
File.Copy(cachedMeta, destMeta, overwrite: false);
}
catch (Exception e) when (e is IOException || e is UnauthorizedAccessException)
{
Debug.LogWarning($"[TestRunBuilder] Could not restore cached meta for '{assetPath}': {e.Message}");
}
}
private static Data.Editor GetEditorInfo()
{
var fullVersion = UnityEditorInternal.InternalEditorUtility.GetFullUnityVersion();
const string pattern = @"(.+\.+.+\.\w+)|((?<=\().+(?=\)))";
var matches = Regex.Matches(fullVersion, pattern);
return new Data.Editor
{
Branch = GetEditorBranch(),
Version = matches[0].Value,
Changeset = matches[1].Value,
Date = UnityEditorInternal.InternalEditorUtility.GetUnityVersionDate(),
};
}
private static string GetEditorBranch()
{
foreach (var method in typeof(UnityEditorInternal.InternalEditorUtility).GetMethods())
{
if (method.Name.Contains("GetUnityBuildBranch"))
{
return (string) method.Invoke(null, null);
}
}
return "null";
}
private static void SetBuildSettings(Run run)
{
if (run.Player == null) run.Player = new Player();
run.Player.GpuSkinning = PlayerSettings.gpuSkinning;
#if UNITY_2021_2_OR_NEWER
run.Player.ScriptingBackend = PlayerSettings
.GetScriptingBackend(NamedBuildTarget.FromBuildTargetGroup(EditorUserBuildSettings.selectedBuildTargetGroup)).ToString();
#else
run.Player.ScriptingBackend = PlayerSettings
.GetScriptingBackend(EditorUserBuildSettings.selectedBuildTargetGroup).ToString();
#endif
run.Player.RenderThreadingMode = PlayerSettings.graphicsJobs ? PlayerSettings.graphicsJobMode.ToString() :
PlayerSettings.MTRendering ? "MultiThreaded" : "SingleThreaded";
run.Player.AndroidTargetSdkVersion = PlayerSettings.Android.targetSdkVersion.ToString();
run.Player.AndroidBuildSystem = EditorUserBuildSettings.androidBuildSystem.ToString();
run.Player.BuildTarget = EditorUserBuildSettings.activeBuildTarget.ToString();
run.Player.StereoRenderingPath = PlayerSettings.stereoRenderingPath.ToString();
}
public Run CreateRunInfo()
{
var run = new Run();
run.Editor = GetEditorInfo();
run.Dependencies = GetPackageDependencies();
SetBuildSettings(run);
run.Date = Utils.ConvertToUnixTimestamp(DateTime.Now);
return run;
}
public Run CreateBuildInfo()
{
var run = new Run();
run.Editor = GetEditorInfo();
run.Dependencies = GetPackageDependencies();
SetBuildSettings(run);
return run;
}
public Run GetPerformanceTestRun()
{
var run = CreateRunInfo();
Metadata.SetRuntimeSettings(run);
return run;
}
private void CreateResourcesFolder()
{
if (Directory.Exists(Utils.ResourcesPath))
{
SessionState.SetBool(cleanResources, false);
return;
}
SessionState.SetBool(cleanResources, true);
AssetDatabase.CreateFolder("Assets", "Resources");
}
private string SaveToStorage(object obj, string path)
{
var json = JsonUtility.ToJson(obj);
if (File.Exists(path))
{
var existing = File.ReadAllText(path);
if (existing == json)
{
return json;
}
Debug.LogWarning($"[TestRunBuilder] Content changed for '{path}' - rewriting file\nOld length: {existing.Length} bytes\nNew length: {json.Length} bytes");
if (EnableDetailedDiffLogging)
{
LogContentDifferences(existing, json);
}
}
else
{
Debug.Log($"[TestRunBuilder] Creating new file '{path}' ({json.Length} bytes)");
}
File.WriteAllText(path, json);
return json;
}
private void LogContentDifferences(string existing, string json)
{
Debug.Log($"[TestRunBuilder] Old content:\n{existing}");
Debug.Log($"[TestRunBuilder] New content:\n{json}");
}
private string SaveToPrefs(object obj, string key)
{
var json = JsonUtility.ToJson(obj, true);
PlayerPrefs.SetString(key, json);
return json;
}
}
}