280 lines
15 KiB
C#
280 lines
15 KiB
C#
using System.Xml;
|
|
using NUnit.Framework;
|
|
using Unity.Pipeline.Editor.BuildProcessors;
|
|
using Unity.Pipeline.CodeReload;
|
|
|
|
namespace Unity.Pipeline.Tests.Editor
|
|
{
|
|
/// <summary>
|
|
/// Tests for the generated link.xml that preserves the interpreter's host-binding surface under
|
|
/// IL2CPP managed stripping. The build-time contribution runs only inside a real player build, but
|
|
/// the XML generator is pure and editor-callable, so its output is verified directly here.
|
|
/// </summary>
|
|
class CodeReloadLinkXmlGeneratorTests
|
|
{
|
|
[Test]
|
|
public void GenerateLinkXml_IsWellFormedLinkerDocument()
|
|
{
|
|
var xml = CodeReloadLinkXmlGenerator.GenerateLinkXml();
|
|
|
|
var doc = new XmlDocument();
|
|
Assert.DoesNotThrow(() => doc.LoadXml(xml), "Generated link.xml must be well-formed XML.");
|
|
Assert.AreEqual("linker", doc.DocumentElement.Name, "Root element must be <linker>.");
|
|
Assert.IsNotEmpty(doc.SelectNodes("/linker/assembly"), "Expected at least one <assembly> group.");
|
|
}
|
|
|
|
[Test]
|
|
public void GenerateLinkXml_PreservesUnityEngineColor_TheGreenYellowCase()
|
|
{
|
|
// Color.greenYellow exists in the Unity API but is stripped from an IL2CPP player when the
|
|
// built game never references it. AllowType<Color>() registers the whole type, so preserving
|
|
// Color with preserve="all" is exactly what keeps greenYellow (and every other member)
|
|
// callable from reloaded code.
|
|
var xml = CodeReloadLinkXmlGenerator.GenerateLinkXml();
|
|
|
|
var doc = new XmlDocument();
|
|
doc.LoadXml(xml);
|
|
|
|
var colorNode = doc.SelectSingleNode(
|
|
"/linker/assembly/type[@fullname='UnityEngine.Color']");
|
|
Assert.IsNotNull(colorNode, "UnityEngine.Color must be preserved so its members survive stripping.");
|
|
Assert.AreEqual("all", colorNode.Attributes["preserve"]?.Value,
|
|
"Color must be preserved with preserve=\"all\" to keep members like greenYellow.");
|
|
}
|
|
|
|
[Test]
|
|
public void GenerateLinkXml_GroupsColorUnderCoreModule()
|
|
{
|
|
var xml = CodeReloadLinkXmlGenerator.GenerateLinkXml();
|
|
|
|
var doc = new XmlDocument();
|
|
doc.LoadXml(xml);
|
|
|
|
var node = doc.SelectSingleNode(
|
|
"/linker/assembly[@fullname='UnityEngine.CoreModule']/type[@fullname='UnityEngine.Color']");
|
|
Assert.IsNotNull(node, "Color should be grouped under its owning assembly, UnityEngine.CoreModule.");
|
|
}
|
|
|
|
[Test]
|
|
public void GenerateLinkXml_WithPlayerAssemblies_PreservesUserAndEngineModulesWholesale()
|
|
{
|
|
// Demand-time auto-bind lets reloaded code call into any user or UnityEngine type, so the
|
|
// dev build must preserve those assemblies wholesale; BCL and Unity.* package assemblies
|
|
// stay per-type (the auto-bind policy declines the BCL, and Unity.* packages are AOT-heavy).
|
|
var xml = CodeReloadLinkXmlGenerator.GenerateLinkXml(
|
|
new[] { "Assembly-CSharp", "MyGame.Gameplay", "UnityEngine.CoreModule", "UnityEngine.AIModule",
|
|
"System.Core", "mscorlib", "Unity.Burst", "Unity.Pipeline" },
|
|
out _);
|
|
|
|
var doc = new XmlDocument();
|
|
doc.LoadXml(xml);
|
|
|
|
foreach (var expected in new[]
|
|
{ "Assembly-CSharp", "MyGame.Gameplay", "UnityEngine.CoreModule", "UnityEngine.AIModule" })
|
|
{
|
|
var node = doc.SelectSingleNode($"/linker/assembly[@fullname='{expected}']");
|
|
Assert.IsNotNull(node, $"{expected} must be preserved wholesale.");
|
|
Assert.AreEqual("all", node.Attributes["preserve"]?.Value,
|
|
$"{expected} must carry assembly-level preserve=\"all\".");
|
|
Assert.AreEqual(0, node.ChildNodes.Count,
|
|
$"{expected} is wholesale — per-type entries are subsumed.");
|
|
}
|
|
|
|
foreach (var excluded in new[] { "System.Core", "mscorlib", "Unity.Burst", "Unity.Pipeline" })
|
|
{
|
|
var node = doc.SelectSingleNode($"/linker/assembly[@fullname='{excluded}']");
|
|
Assert.IsTrue(node == null || node.Attributes["preserve"] == null,
|
|
$"{excluded} must not be preserved wholesale.");
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void GenerateLinkXml_WithoutPlayerAssemblies_OnlyInterpreterEngineIsWholesale()
|
|
{
|
|
// Null player-assembly set (unreadable linker input, or the parameterless test entry
|
|
// point) degrades to per-type host-surface preservation plus the always-on interpreter
|
|
// engine assemblies — nothing else may be preserved wholesale.
|
|
var xml = CodeReloadLinkXmlGenerator.GenerateLinkXml();
|
|
|
|
var doc = new XmlDocument();
|
|
doc.LoadXml(xml);
|
|
|
|
foreach (XmlNode asm in doc.SelectNodes("/linker/assembly"))
|
|
{
|
|
var name = asm.Attributes["fullname"]?.Value;
|
|
if (name == "Unity.Pipeline.IlInterpreter" ||
|
|
name == "UnityPipeline.System.Reflection.Metadata" ||
|
|
name == "UnityPipeline.System.Collections.Immutable")
|
|
continue;
|
|
Assert.IsNull(asm.Attributes["preserve"],
|
|
$"without a player assembly set '{name}' may not be preserved wholesale");
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void GenerateLinkXml_AlwaysPreservesInterpreterEngineAssembliesWholesale()
|
|
{
|
|
// The interpreter parses pushed override IL via System.Reflection.Metadata and resolves
|
|
// host members by name; under IL2CPP stripping those paths look unused. This preservation
|
|
// used to live in a static Runtime/link.xml, but that file applied to release builds too
|
|
// (real size cost for machinery that is compiled out of release players). The generator
|
|
// only contributes to development builds, so it now owns this set — with or without a
|
|
// player-assembly list.
|
|
foreach (var xml in new[]
|
|
{
|
|
CodeReloadLinkXmlGenerator.GenerateLinkXml(),
|
|
CodeReloadLinkXmlGenerator.GenerateLinkXml(new[] { "Assembly-CSharp" }, out _),
|
|
})
|
|
{
|
|
var doc = new XmlDocument();
|
|
doc.LoadXml(xml);
|
|
|
|
foreach (var expected in new[]
|
|
{ "Unity.Pipeline.IlInterpreter", "UnityPipeline.System.Reflection.Metadata", "UnityPipeline.System.Collections.Immutable" })
|
|
{
|
|
var node = doc.SelectSingleNode($"/linker/assembly[@fullname='{expected}']");
|
|
Assert.IsNotNull(node, $"{expected} must be preserved so the interpreter survives stripping.");
|
|
Assert.AreEqual("all", node.Attributes["preserve"]?.Value,
|
|
$"{expected} must carry assembly-level preserve=\"all\".");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void GenerateLinkXml_UnityPrefixedGameAssemblies_AreTreatedAsUserCode()
|
|
{
|
|
// A bare "Unity"/"System" prefix would misclassify game assemblies that merely start
|
|
// with the word — "UnityChanDemo", "SystemsRuntime" — as engine/BCL code, silently
|
|
// dropping them from wholesale preservation and from the [CodeReload] target scan
|
|
// (both share IsNonUserAssembly). Only real engine/BCL names are non-user.
|
|
var xml = CodeReloadLinkXmlGenerator.GenerateLinkXml(
|
|
new[] { "UnityChanDemo", "SystemsRuntime", "Unity", "Unity.Burst", "System.Core" },
|
|
out _);
|
|
|
|
var doc = new XmlDocument();
|
|
doc.LoadXml(xml);
|
|
|
|
foreach (var user in new[] { "UnityChanDemo", "SystemsRuntime" })
|
|
{
|
|
var node = doc.SelectSingleNode($"/linker/assembly[@fullname='{user}']");
|
|
Assert.IsNotNull(node, $"{user} is user code and must be preserved wholesale.");
|
|
Assert.AreEqual("all", node.Attributes["preserve"]?.Value,
|
|
$"{user} must carry assembly-level preserve=\"all\".");
|
|
}
|
|
|
|
foreach (var engine in new[] { "Unity", "Unity.Burst", "System.Core" })
|
|
{
|
|
var node = doc.SelectSingleNode($"/linker/assembly[@fullname='{engine}']");
|
|
Assert.IsTrue(node == null || node.Attributes["preserve"] == null,
|
|
$"{engine} is engine/BCL code and must not be preserved wholesale.");
|
|
}
|
|
}
|
|
|
|
// Rescue probe: this test assembly is Unity.Pipeline.Tests.Editor — it matches the
|
|
// engine-assembly name pattern, yet carries this [CodeReload] member, standing in for a
|
|
// game assembly named e.g. "Unity.MyGame".
|
|
class EngineNamedAssemblyProbe { [CodeReload] void Tick() { } }
|
|
|
|
[Test]
|
|
public void GenerateLinkXml_FindsCodeReloadTargets_InEngineNamedAssemblies()
|
|
{
|
|
// The [CodeReload] attribute must be authoritative for the target scan; the engine-name
|
|
// check is only a fast path. [CodeReload] lives in Unity.Pipeline, so an engine-named
|
|
// assembly that references Unity.Pipeline can carry targets and must still be scanned —
|
|
// otherwise its private state is stripped and reload fails on device only.
|
|
var xml = CodeReloadLinkXmlGenerator.GenerateLinkXml();
|
|
|
|
var expected = typeof(EngineNamedAssemblyProbe).FullName.Replace('+', '/');
|
|
StringAssert.Contains($"<type fullname=\"{expected}\" preserve=\"all\"/>", xml,
|
|
"[CodeReload] targets in an engine-named assembly must still be preserved.");
|
|
}
|
|
|
|
[Test]
|
|
public void GenerateLinkXml_WithPlayerAssemblies_DropsReloadTargetsNotStagedForTheBuild()
|
|
{
|
|
// The AppDomain scan sees editor-only and test assemblies that are not part of the
|
|
// player build; their [CodeReload] entries are noise in a game build's link.xml. When
|
|
// the staged set is known, only reload targets actually in the build are emitted —
|
|
// which also covers user test asmdefs. A test-player build stages its test
|
|
// assemblies, so fixtures keep their entries exactly where they matter.
|
|
var probe = typeof(EngineNamedAssemblyProbe).FullName.Replace('+', '/');
|
|
|
|
var xml = CodeReloadLinkXmlGenerator.GenerateLinkXml(new[] { "Assembly-CSharp" }, out _);
|
|
StringAssert.DoesNotContain(probe, xml,
|
|
"an unstaged assembly must not contribute reload-target entries");
|
|
}
|
|
|
|
[Test]
|
|
public void GenerateLinkXml_PromotesStagedEngineNamedAssembliesCarryingCodeReloadTargets()
|
|
{
|
|
// A staged, engine-named assembly carrying [CodeReload] members has identified itself
|
|
// as user code participating in code reload — a package-like name (e.g. "Unity.MyGame")
|
|
// must not demote it. Preserving only its [CodeReload] types would leave the rest of the
|
|
// assembly strippable, so demand-time auto-bind could hit stripped members on device
|
|
// only; instead it is preserved wholesale like any other user assembly. This test
|
|
// assembly (Unity.Pipeline.Tests.Editor, carrying the probe above) plays that role.
|
|
var xml = CodeReloadLinkXmlGenerator.GenerateLinkXml(
|
|
new[] { "Assembly-CSharp", "Unity.Pipeline.Tests.Editor" }, out _);
|
|
|
|
var doc = new XmlDocument();
|
|
doc.LoadXml(xml);
|
|
|
|
var node = doc.SelectSingleNode("/linker/assembly[@fullname='Unity.Pipeline.Tests.Editor']");
|
|
Assert.IsNotNull(node, "the promoted assembly must appear in the link.xml");
|
|
Assert.AreEqual("all", node.Attributes["preserve"]?.Value,
|
|
"the promoted assembly must carry assembly-level preserve=\"all\"");
|
|
Assert.AreEqual(0, node.ChildNodes.Count,
|
|
"wholesale preservation subsumes the per-type [CodeReload] entries");
|
|
}
|
|
|
|
[Test]
|
|
public void CollectPlayerAssemblyNames_WithEmptyInputDir_RecoversUnityEngineModulesFromTheEditor()
|
|
{
|
|
// The linker input directory is empty at IUnityLinkerProcessor time on some build backends
|
|
// (the symptom that stripped UITK ctors on device: a live-added `new Slider()` couldn't
|
|
// resolve because UnityEngine.UIElementsModule wasn't preserved wholesale). The collector
|
|
// must fall back to the editor's own assembly view so wholesale preservation still fires.
|
|
var names = CodeReloadLinkXmlGenerator.CollectPlayerAssemblyNames("/no/such/linker/input/dir");
|
|
|
|
Assert.IsNotNull(names, "an empty input dir must still yield the editor's player-assembly view");
|
|
CollectionAssert.Contains(names, "UnityEngine.UIElementsModule",
|
|
"the UnityEngine module carrying Slider must be recovered so its ctors survive stripping");
|
|
CollectionAssert.Contains(names, "UnityEngine.CoreModule",
|
|
"core UnityEngine modules must be recovered from the loaded editor assemblies");
|
|
}
|
|
|
|
[Test]
|
|
public void CollectPlayerAssemblyNames_FeedsWholesalePreservationOfUIElements()
|
|
{
|
|
// End-to-end for the Slider fix: the recovered assembly set, fed through the generator,
|
|
// must preserve UnityEngine.UIElementsModule wholesale (assembly-level preserve="all"),
|
|
// which is what keeps Slider's constructors callable from a reloaded body on device.
|
|
var names = CodeReloadLinkXmlGenerator.CollectPlayerAssemblyNames(null);
|
|
var xml = CodeReloadLinkXmlGenerator.GenerateLinkXml(names, out _);
|
|
|
|
var doc = new XmlDocument();
|
|
doc.LoadXml(xml);
|
|
|
|
var node = doc.SelectSingleNode("/linker/assembly[@fullname='UnityEngine.UIElementsModule']");
|
|
Assert.IsNotNull(node, "UnityEngine.UIElementsModule must be preserved so Slider's ctors survive.");
|
|
Assert.AreEqual("all", node.Attributes["preserve"]?.Value,
|
|
"UnityEngine.UIElementsModule must carry assembly-level preserve=\"all\".");
|
|
}
|
|
|
|
[Test]
|
|
public void GenerateLinkXml_EveryTypeIsPreservedAll()
|
|
{
|
|
var xml = CodeReloadLinkXmlGenerator.GenerateLinkXml();
|
|
|
|
var doc = new XmlDocument();
|
|
doc.LoadXml(xml);
|
|
|
|
foreach (XmlNode type in doc.SelectNodes("/linker/assembly/type"))
|
|
{
|
|
Assert.AreEqual("all", type.Attributes["preserve"]?.Value,
|
|
$"Type {type.Attributes["fullname"]?.Value} must use preserve=\"all\".");
|
|
}
|
|
}
|
|
}
|
|
}
|