380 lines
18 KiB
C#
380 lines
18 KiB
C#
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using NUnit.Framework;
|
|
using Unity.Pipeline.Config;
|
|
using Unity.Pipeline.Editor.BuildProcessors;
|
|
using UnityEditor;
|
|
using UnityEditor.Build;
|
|
using UnityEditor.Build.Reporting;
|
|
using UnityEditor.SceneManagement;
|
|
using UnityEditor.TestTools;
|
|
using UnityEngine;
|
|
using UnityEngine.TestTools;
|
|
|
|
namespace Unity.Pipeline.Tests.Editor
|
|
{
|
|
/// <summary>
|
|
/// Tests for Pipeline runtime build support using RuntimePipelineConfig. Validates build
|
|
/// processor behavior and the transient RuntimePipelineConfig/RuntimePipelineBuildInfo asset
|
|
/// generation/cleanup.
|
|
/// </summary>
|
|
class RuntimePipelineBuildTests
|
|
{
|
|
private const string TransientConfigAssetPath = "Assets/Settings/Pipeline/Resources/RuntimePipelineConfig.asset";
|
|
private const string BuildInfoAssetPath = "Assets/Settings/Pipeline/Resources/RuntimePipelineBuildInfo.asset";
|
|
private PipelineRuntimeBuildProcessor m_BuildProcessor;
|
|
private bool m_PreviousDevelopment;
|
|
|
|
[SetUp]
|
|
public void SetUp()
|
|
{
|
|
RuntimePipelineResourceTestGuard.AssertNoConfigAssetExists();
|
|
|
|
m_BuildProcessor = new PipelineRuntimeBuildProcessor();
|
|
DeleteIfExists(TransientConfigAssetPath);
|
|
DeleteIfExists(BuildInfoAssetPath);
|
|
|
|
// Force a Development Build context: OnPreprocessBuild bails out before baking
|
|
// anything for a non-development build, so without this every asset-generation
|
|
// assertion below would trivially pass for the wrong reason. The non-development path
|
|
// has its own test.
|
|
m_PreviousDevelopment = EditorUserBuildSettings.development;
|
|
EditorUserBuildSettings.development = true;
|
|
}
|
|
|
|
[TearDown]
|
|
public void TearDown()
|
|
{
|
|
EditorUserBuildSettings.development = m_PreviousDevelopment;
|
|
DeleteIfExists(TransientConfigAssetPath);
|
|
DeleteIfExists(BuildInfoAssetPath);
|
|
RuntimePipelineResourceTestGuard.DeleteConfigIfExists();
|
|
}
|
|
|
|
private static void DeleteIfExists(string path)
|
|
{
|
|
if (AssetDatabase.LoadAssetAtPath<Object>(path) != null)
|
|
AssetDatabase.DeleteAsset(path);
|
|
}
|
|
|
|
private static void CreateConfig(bool enableInBuilds)
|
|
{
|
|
var config = ScriptableObject.CreateInstance<RuntimePipelineConfig>();
|
|
config.enableInBuilds = enableInBuilds;
|
|
config.port = 7920;
|
|
config.Save();
|
|
Object.DestroyImmediate(config);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write both transient build-only assets directly, simulating what an interrupted build
|
|
/// (crash, force-quit) leaves behind before OnPostprocessBuild ever ran. Uses port 7999 and
|
|
/// an obviously-fake reload root so a test can tell "this is the stale leftover" apart from
|
|
/// whatever the current authored config/CollectProjectRoots would actually produce.
|
|
/// </summary>
|
|
private static void SeedStaleTransientAssets(bool enableInBuilds)
|
|
{
|
|
EnsureFolder(Path.GetDirectoryName(TransientConfigAssetPath).Replace('\\', '/'));
|
|
|
|
var staleConfig = ScriptableObject.CreateInstance<RuntimePipelineConfig>();
|
|
staleConfig.enableInBuilds = enableInBuilds;
|
|
staleConfig.port = 7999;
|
|
AssetDatabase.CreateAsset(staleConfig, TransientConfigAssetPath);
|
|
|
|
var staleBuildInfo = ScriptableObject.CreateInstance<RuntimePipelineBuildInfo>();
|
|
staleBuildInfo.allowedReloadRoots = new List<string> { "Z:/stale/leftover/root" };
|
|
AssetDatabase.CreateAsset(staleBuildInfo, BuildInfoAssetPath);
|
|
|
|
AssetDatabase.SaveAssets();
|
|
}
|
|
|
|
private static void EnsureFolder(string folder)
|
|
{
|
|
var parts = folder.Split('/');
|
|
var current = parts[0];
|
|
for (var i = 1; i < parts.Length; i++)
|
|
{
|
|
var next = current + "/" + parts[i];
|
|
if (!AssetDatabase.IsValidFolder(next))
|
|
AssetDatabase.CreateFolder(current, parts[i]);
|
|
current = next;
|
|
}
|
|
}
|
|
|
|
#if UNITY_6000_3_OR_NEWER
|
|
private BuildCallbackContext CreateMockBuildReport()
|
|
#else
|
|
private BuildReport CreateMockBuildReport()
|
|
#endif
|
|
{
|
|
// Neither preprocess nor postprocess actually reads the report parameter.
|
|
return null;
|
|
}
|
|
|
|
// authorDisabledConfig: whether an authored RuntimePipelineConfig with enableInBuilds=false
|
|
// exists (vs. no config file at all). seedStaleAssets: whether a leftover ENABLED transient
|
|
// asset pair (as an interrupted earlier build would leave) is present beforehand. The
|
|
// seedStaleAssets:true rows are the regression case: before the fix, an early return in
|
|
// OnPreprocessBuild left the stale enabled asset in place, so a build that should have
|
|
// Pipeline disabled would still ship it.
|
|
[TestCase(false, false)]
|
|
[TestCase(false, true)]
|
|
[TestCase(true, false)]
|
|
[TestCase(true, true)]
|
|
public void Preprocess_PipelineNotShipping_LeavesNoTransientAssets(bool authorDisabledConfig, bool seedStaleAssets)
|
|
{
|
|
if (authorDisabledConfig)
|
|
CreateConfig(enableInBuilds: false);
|
|
if (seedStaleAssets)
|
|
SeedStaleTransientAssets(enableInBuilds: true);
|
|
|
|
Assert.DoesNotThrow(() => m_BuildProcessor.OnPreprocessBuild(CreateMockBuildReport()));
|
|
|
|
Assert.IsNull(AssetDatabase.LoadAssetAtPath<Object>(BuildInfoAssetPath),
|
|
"No build-info asset should remain when Pipeline is not shipping in this build.");
|
|
Assert.IsNull(AssetDatabase.LoadAssetAtPath<Object>(TransientConfigAssetPath),
|
|
"No transient config asset should remain when Pipeline is not shipping in this build " +
|
|
"— including a stale, possibly-enabled one left behind by a previous interrupted build.");
|
|
}
|
|
|
|
[Test]
|
|
public void Preprocess_StaleTransientAssets_PipelineEnabled_ReplacesWithCurrentValues()
|
|
{
|
|
// Stale leftover pretends Pipeline was disabled and points at a fake reload root.
|
|
SeedStaleTransientAssets(enableInBuilds: false);
|
|
// Current authored config: enabled, port 7920 (CreateConfig's fixed value).
|
|
CreateConfig(enableInBuilds: true);
|
|
|
|
m_BuildProcessor.OnPreprocessBuild(CreateMockBuildReport());
|
|
|
|
var baked = AssetDatabase.LoadAssetAtPath<RuntimePipelineConfig>(TransientConfigAssetPath);
|
|
Assert.IsNotNull(baked, "OnPreprocessBuild should bake the current authored config");
|
|
Assert.IsTrue(baked.enableInBuilds, "Baked asset must reflect the current authored config, not the stale leftover");
|
|
Assert.AreEqual(7920, baked.port, "Baked asset must reflect the current authored config's port, not the stale leftover's 7999");
|
|
|
|
var buildInfo = AssetDatabase.LoadAssetAtPath<RuntimePipelineBuildInfo>(BuildInfoAssetPath);
|
|
Assert.IsNotNull(buildInfo);
|
|
var expectedRoots = PipelineRuntimeBuildProcessor.CollectProjectRoots();
|
|
CollectionAssert.AreEquivalent(expectedRoots, buildInfo.allowedReloadRoots,
|
|
"Build-info asset must reflect the current project roots, not the stale leftover's fake root");
|
|
}
|
|
|
|
[Test]
|
|
public void Preprocess_StaleAssetWithMismatchedType_IsStillPurged()
|
|
{
|
|
// Simulate the worst-case leftover: an asset at RuntimePipelineConfig's path that can no
|
|
// longer be loaded AS RuntimePipelineConfig (e.g. a script reference broken by a package
|
|
// upgrade). A typed AssetDatabase.LoadAssetAtPath<RuntimePipelineConfig> check would miss
|
|
// this entirely and leave it on disk, inside Resources, for the next build to package.
|
|
EnsureFolder(Path.GetDirectoryName(TransientConfigAssetPath).Replace('\\', '/'));
|
|
var wrongTypeAsset = ScriptableObject.CreateInstance<RuntimePipelineBuildInfo>();
|
|
AssetDatabase.CreateAsset(wrongTypeAsset, TransientConfigAssetPath);
|
|
AssetDatabase.SaveAssets();
|
|
Assert.IsNull(AssetDatabase.LoadAssetAtPath<RuntimePipelineConfig>(TransientConfigAssetPath),
|
|
"Precondition: a typed load must not find the mismatched-type asset — that's the gap this test guards against");
|
|
|
|
m_BuildProcessor.OnPreprocessBuild(CreateMockBuildReport());
|
|
|
|
Assert.IsNull(AssetDatabase.LoadAssetAtPath<Object>(TransientConfigAssetPath),
|
|
"A stale asset must be purged even when it cannot be loaded as its expected type");
|
|
}
|
|
|
|
[Test]
|
|
public void BuildProcessor_ConfigEnabled_WritesBuildInfoWithProjectRoots()
|
|
{
|
|
CreateConfig(enableInBuilds: true);
|
|
|
|
m_BuildProcessor.OnPreprocessBuild(CreateMockBuildReport());
|
|
|
|
var buildInfo = AssetDatabase.LoadAssetAtPath<RuntimePipelineBuildInfo>(BuildInfoAssetPath);
|
|
Assert.IsNotNull(buildInfo, "OnPreprocessBuild should create the build-info asset when enabled");
|
|
var expectedRoots = PipelineRuntimeBuildProcessor.CollectProjectRoots();
|
|
CollectionAssert.AreEquivalent(expectedRoots, buildInfo.allowedReloadRoots);
|
|
}
|
|
|
|
[Test]
|
|
public void BuildProcessor_ConfigEnabled_WritesTransientConfigAssetWithSameValues()
|
|
{
|
|
CreateConfig(enableInBuilds: true);
|
|
|
|
m_BuildProcessor.OnPreprocessBuild(CreateMockBuildReport());
|
|
|
|
var baked = AssetDatabase.LoadAssetAtPath<RuntimePipelineConfig>(TransientConfigAssetPath);
|
|
Assert.IsNotNull(baked, "OnPreprocessBuild should bake the authored config into a transient Resources asset");
|
|
Assert.IsTrue(baked.enableInBuilds);
|
|
Assert.AreEqual(7920, baked.port);
|
|
}
|
|
|
|
[Test]
|
|
public void BuildProcessor_PostprocessBuild_DeletesBuildInfoAndConfigAssets()
|
|
{
|
|
CreateConfig(enableInBuilds: true);
|
|
m_BuildProcessor.OnPreprocessBuild(CreateMockBuildReport());
|
|
Assert.IsNotNull(AssetDatabase.LoadAssetAtPath<RuntimePipelineBuildInfo>(BuildInfoAssetPath),
|
|
"Precondition: build-info asset must exist before postprocess runs");
|
|
Assert.IsNotNull(AssetDatabase.LoadAssetAtPath<RuntimePipelineConfig>(TransientConfigAssetPath),
|
|
"Precondition: transient config asset must exist before postprocess runs");
|
|
|
|
// OnPostprocessBuild's signature is not versioned by UNITY_6000_3_OR_NEWER (unlike
|
|
// OnPreprocessBuild), so it always takes a BuildReport — pass null directly rather than
|
|
// via CreateMockBuildReport(), whose return type tracks the versioned preprocess signature.
|
|
m_BuildProcessor.OnPostprocessBuild(null);
|
|
|
|
Assert.IsNull(AssetDatabase.LoadAssetAtPath<RuntimePipelineBuildInfo>(BuildInfoAssetPath),
|
|
"OnPostprocessBuild must remove the transient build-info asset");
|
|
Assert.IsNull(AssetDatabase.LoadAssetAtPath<RuntimePipelineConfig>(TransientConfigAssetPath),
|
|
"OnPostprocessBuild must remove the transient config asset");
|
|
}
|
|
|
|
// enableInBuilds is on, but a release build that does not opt in carries no Pipeline code at
|
|
// all — the runtime assemblies and the bundled Roslyn DLLs are constrained to
|
|
// "UNITY_EDITOR || DEVELOPMENT_BUILD || ENABLE_RUNTIME_PIPELINE" — so nothing in the player
|
|
// could read the transient assets and they must not be baked. seedStaleAssets covers the
|
|
// interrupted-build leftover. (The opt-in branch reads PlayerSettings' define symbols, which
|
|
// this fixture does not mutate; it is covered by a player build instead.)
|
|
[TestCase(false)]
|
|
[TestCase(true)]
|
|
public void Preprocess_EnabledButNotDevelopmentBuild_LeavesNoTransientAssets(bool seedStaleAssets)
|
|
{
|
|
CreateConfig(enableInBuilds: true);
|
|
if (seedStaleAssets)
|
|
SeedStaleTransientAssets(enableInBuilds: true);
|
|
EditorUserBuildSettings.development = false;
|
|
|
|
Assert.DoesNotThrow(() => m_BuildProcessor.OnPreprocessBuild(CreateMockBuildReport()));
|
|
|
|
Assert.IsNull(AssetDatabase.LoadAssetAtPath<Object>(BuildInfoAssetPath),
|
|
"A non-development build must not get a build-info asset.");
|
|
Assert.IsNull(AssetDatabase.LoadAssetAtPath<Object>(TransientConfigAssetPath),
|
|
"A non-development build must not get a transient config asset.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Platform-specific build tests following Unity test patterns.
|
|
/// Tests actual build generation with authored RuntimePipelineConfig settings.
|
|
/// </summary>
|
|
[RequirePlatformSupport(BuildTarget.StandaloneWindows64)]
|
|
class RuntimePipelineWindows64BuildTests : RuntimePipelineBuildTestBase
|
|
{
|
|
protected override BuildTarget Target => BuildTarget.StandaloneWindows64;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Abstract base class for build generation tests.
|
|
/// Follows Unity test patterns for build validation.
|
|
/// </summary>
|
|
abstract class RuntimePipelineBuildTestBase
|
|
{
|
|
protected abstract BuildTarget Target { get; }
|
|
protected string BuildDirectory => $"TestBuild/{GetType().Name}";
|
|
private const string TransientConfigAssetPath = "Assets/Settings/Pipeline/Resources/RuntimePipelineConfig.asset";
|
|
private const string TransientBuildInfoAssetPath = "Assets/Settings/Pipeline/Resources/RuntimePipelineBuildInfo.asset";
|
|
private bool m_PreviousDevelopment;
|
|
|
|
[SetUp]
|
|
public void SetUp()
|
|
{
|
|
RuntimePipelineResourceTestGuard.AssertNoConfigAssetExists();
|
|
|
|
// BuildOptions.Development on this test's BuildPipeline.BuildPlayer call does not
|
|
// reliably flip the global EditorUserBuildSettings.development before OnPreprocessBuild
|
|
// runs, so force it directly — otherwise the processor treats this as a release build
|
|
// and bails out before baking the assets these tests inspect.
|
|
m_PreviousDevelopment = EditorUserBuildSettings.development;
|
|
EditorUserBuildSettings.development = true;
|
|
}
|
|
|
|
[TearDown]
|
|
public void TearDown()
|
|
{
|
|
EditorUserBuildSettings.development = m_PreviousDevelopment;
|
|
RuntimePipelineResourceTestGuard.DeleteConfigIfExists();
|
|
}
|
|
|
|
[OneTimeSetUp]
|
|
public void OneTimeSetUp()
|
|
{
|
|
if (Directory.Exists(BuildDirectory))
|
|
Directory.Delete(BuildDirectory, true);
|
|
}
|
|
|
|
[OneTimeTearDown]
|
|
public void OneTimeTearDown()
|
|
{
|
|
if (Directory.Exists(BuildDirectory))
|
|
Directory.Delete(BuildDirectory, true);
|
|
}
|
|
|
|
[Test]
|
|
public void BuildPlayerWithRuntimeConfig_ValidConfiguration_BuildSucceeds()
|
|
{
|
|
if (!IsPlatformSupportInstalled(Target))
|
|
{
|
|
Assert.Ignore($"Platform support for {Target} is not installed");
|
|
}
|
|
|
|
// Note: When calling BuildPlayer the list of scenes is ALWAYS overridden with what is in the current build profile
|
|
var testScenePath = CreateTestSceneWithConfig(validConfiguration: true);
|
|
|
|
try
|
|
{
|
|
var buildOptions = new BuildPlayerOptions
|
|
{
|
|
scenes = new[] { testScenePath },
|
|
locationPathName = Path.Combine(BuildDirectory, "TestBuild.exe"),
|
|
target = Target,
|
|
options = BuildOptions.Development // Use development for faster builds
|
|
};
|
|
|
|
var report = BuildPipeline.BuildPlayer(buildOptions);
|
|
|
|
Assert.AreEqual(BuildResult.Succeeded, report.summary.result,
|
|
$"Build should succeed: {string.Join("; ", report.steps.SelectMany(s => s.messages).Select(m => m.content))}");
|
|
|
|
var outputFiles = report.GetFiles();
|
|
Assert.IsTrue(outputFiles.Any(), "Build should produce output files");
|
|
Assert.IsTrue(File.Exists(buildOptions.locationPathName), "Executable should exist after successful build");
|
|
}
|
|
finally
|
|
{
|
|
AssetDatabase.DeleteAsset(testScenePath);
|
|
if (AssetDatabase.LoadAssetAtPath<Object>(TransientConfigAssetPath) != null)
|
|
AssetDatabase.DeleteAsset(TransientConfigAssetPath);
|
|
if (AssetDatabase.LoadAssetAtPath<Object>(TransientBuildInfoAssetPath) != null)
|
|
AssetDatabase.DeleteAsset(TransientBuildInfoAssetPath);
|
|
}
|
|
}
|
|
|
|
/// <summary>Create an empty test scene and author matching Pipeline settings (Unity still requires >=1 scene to build).</summary>
|
|
protected string CreateTestSceneWithConfig(bool validConfiguration)
|
|
{
|
|
var scenePath = $"Assets/TestScene_{Target}_{System.Guid.NewGuid():N}.unity";
|
|
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene);
|
|
EditorSceneManager.SaveScene(scene, scenePath);
|
|
|
|
var config = ScriptableObject.CreateInstance<RuntimePipelineConfig>();
|
|
config.enableInBuilds = validConfiguration;
|
|
config.Save();
|
|
Object.DestroyImmediate(config);
|
|
|
|
return scenePath;
|
|
}
|
|
|
|
/// <summary>Check if platform support is installed for the target.</summary>
|
|
protected bool IsPlatformSupportInstalled(BuildTarget target)
|
|
{
|
|
try
|
|
{
|
|
var group = BuildPipeline.GetBuildTargetGroup(target);
|
|
return true; // If we can get the group, platform support is available
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|