feat(mod): 完成外部 DLL 事务热替换
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 39d24561b9507f340abccb25be811c21
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
+7
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4bd75db4a8f23f7428ff7dc0e796a5c0
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 97a7806bf8820b94e83cc2011c09bc43
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Binary file not shown.
+7
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8517a178ed20f2a45b30d4c45fefe2c5
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7a3fede5ca621bd4e9ecb0dc59e5925e
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
+7
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c5af400c8aafe04428decd12e66face9
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using ShrinkContext;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace ShrinkModFramework.Tests
|
||||
@@ -197,6 +201,146 @@ namespace ShrinkModFramework.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExternalDllRevision_FailedReplacementRestoresPreviousAndBrokenBytesDoNotReplaceIt()
|
||||
{
|
||||
ResetExternalRuntime();
|
||||
var settings = CreateExternalSettings();
|
||||
var directory = ShrinkExternalModAssemblyLoader.GetExternalModsDirectory(settings);
|
||||
var target = Path.Combine(directory, "ExternalFixture.Mod.dll");
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
CopyFixture("ExternalFixture.Mod.V1.dll.bytes", target);
|
||||
|
||||
ShrinkModLoader.LoadAll(settings);
|
||||
AssertExternalFixture("1.0.0", "v1");
|
||||
var restoredGeneration = ShrinkModLoader.Mods["external.fixture"].Generation;
|
||||
|
||||
CopyFixture("ExternalFixture.Mod.V2.Failing.dll.bytes", target);
|
||||
var transaction = Assert.Throws<ShrinkModTransactionException>(() =>
|
||||
ShrinkModLoader.LoadNewExternalMods(settings));
|
||||
|
||||
Assert.IsTrue(transaction!.PreviousCompositionRestored);
|
||||
AssertExternalFixture("1.0.0", "v1");
|
||||
Assert.AreNotEqual(0, restoredGeneration);
|
||||
Assert.Greater(ShrinkModLoader.Mods["external.fixture"].Generation, restoredGeneration);
|
||||
var failedReplacementGeneration = ShrinkModLoader.Mods["external.fixture"].Generation;
|
||||
|
||||
File.WriteAllBytes(target, new byte[] { 0, 1, 2, 3, 4, 5 });
|
||||
LogAssert.Expect(LogType.Error,
|
||||
new Regex(@"\[ShrinkModFramework\] 加载外部 DLL 失败:.*ExternalFixture\.Mod\.dll"));
|
||||
ShrinkModLoader.LoadNewExternalMods(settings);
|
||||
|
||||
AssertExternalFixture("1.0.0", "v1");
|
||||
Assert.AreEqual(failedReplacementGeneration,
|
||||
ShrinkModLoader.Mods["external.fixture"].Generation,
|
||||
"坏 DLL 不应让失败 revision 重新成为当前 source");
|
||||
|
||||
CopyFixture("ExternalFixture.Mod.V3.dll.bytes", target);
|
||||
ShrinkModLoader.LoadNewExternalMods(settings);
|
||||
AssertExternalFixture("3.0.0", "v3");
|
||||
}
|
||||
finally
|
||||
{
|
||||
CleanupExternalRuntime(settings, directory);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExternalDllWatcher_DebouncesBurstAndHandlesDeletion()
|
||||
{
|
||||
ResetExternalRuntime();
|
||||
var settings = CreateExternalSettings();
|
||||
settings.externalModsReloadDelaySeconds = 0.05f;
|
||||
var directory = ShrinkExternalModAssemblyLoader.GetExternalModsDirectory(settings);
|
||||
var target = Path.Combine(directory, "ExternalFixture.Mod.dll");
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
ShrinkModRuntimeDriver.EnsureCreated(settings);
|
||||
Assert.IsTrue(ShrinkModRuntimeDriver.InstanceForTesting!.HasWatcherForTesting);
|
||||
|
||||
CopyFixture("ExternalFixture.Mod.V1.dll.bytes", target);
|
||||
ShrinkModLoader.LoadAll(settings);
|
||||
AssertExternalFixture("1.0.0", "v1");
|
||||
|
||||
// Two writes arrive as one debounce window; only the final valid revision should commit.
|
||||
CopyFixture("ExternalFixture.Mod.V2.Failing.dll.bytes", target);
|
||||
CopyFixture("ExternalFixture.Mod.V3.dll.bytes", target);
|
||||
Assert.IsTrue(PumpDriverUntil(() =>
|
||||
ShrinkModLoader.Mods.TryGetValue("external.fixture", out var handle) &&
|
||||
handle.Info.Version == "3.0.0"),
|
||||
"watcher 应在 burst 后提交最后一个有效 revision");
|
||||
AssertExternalFixture("3.0.0", "v3");
|
||||
|
||||
File.Delete(target);
|
||||
Assert.IsTrue(PumpDriverUntil(() => ShrinkModLoader.Mods.Count == 0),
|
||||
"删除 DLL 应触发当前 source 卸载");
|
||||
}
|
||||
finally
|
||||
{
|
||||
CleanupExternalRuntime(settings, directory);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExternalDllDependencyRemoval_RestoresPreviousComposition()
|
||||
{
|
||||
ResetExternalRuntime();
|
||||
var settings = CreateExternalSettings();
|
||||
var directory = ShrinkExternalModAssemblyLoader.GetExternalModsDirectory(settings);
|
||||
var providerPath = Path.Combine(directory, "ExternalFixture.Provider.dll");
|
||||
var consumerPath = Path.Combine(directory, "ExternalFixture.Consumer.dll");
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
CopyFixture("ExternalFixture.Provider.dll.bytes", providerPath);
|
||||
CopyFixture("ExternalFixture.Consumer.dll.bytes", consumerPath);
|
||||
|
||||
ShrinkModLoader.LoadAll(settings);
|
||||
Assert.AreEqual(2, ShrinkModLoader.Mods.Count);
|
||||
Assert.AreEqual(ShrinkModState.Ready, ShrinkModLoader.Mods["external.provider"].State);
|
||||
Assert.AreEqual(ShrinkModState.Ready, ShrinkModLoader.Mods["external.consumer"].State);
|
||||
|
||||
File.Delete(providerPath);
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
ShrinkModLoader.LoadNewExternalMods(settings));
|
||||
|
||||
Assert.AreEqual(2, ShrinkModLoader.Mods.Count,
|
||||
"依赖导入失败发生在协调前,旧组合必须保持 active");
|
||||
Assert.AreEqual(ShrinkModState.Ready, ShrinkModLoader.Mods["external.provider"].State);
|
||||
Assert.AreEqual(ShrinkModState.Ready, ShrinkModLoader.Mods["external.consumer"].State);
|
||||
|
||||
CopyFixture("ExternalFixture.Provider.dll.bytes", providerPath);
|
||||
ShrinkModLoader.LoadNewExternalMods(settings);
|
||||
Assert.AreEqual(2, ShrinkModLoader.Mods.Count);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CleanupExternalRuntime(settings, directory);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExternalReloadDebouncer_CoalescesBurstAndUsesMinimumDelay()
|
||||
{
|
||||
var debouncer = new ShrinkModReloadDebouncer();
|
||||
|
||||
debouncer.Schedule(10f, 0f);
|
||||
Assert.IsTrue(debouncer.IsPending);
|
||||
Assert.IsFalse(debouncer.TryConsume(10.049f));
|
||||
|
||||
debouncer.Schedule(10.04f, 0.5f);
|
||||
Assert.IsFalse(debouncer.TryConsume(10.539f));
|
||||
Assert.IsTrue(debouncer.TryConsume(10.54f));
|
||||
Assert.IsFalse(debouncer.IsPending);
|
||||
Assert.IsFalse(debouncer.TryConsume(11f));
|
||||
}
|
||||
|
||||
private static ShrinkModComponentSource Source(string id, string version, string revision,
|
||||
Func<IShrinkMod> factory, params ShrinkModDependency[] dependencies)
|
||||
{
|
||||
@@ -224,6 +368,75 @@ namespace ShrinkModFramework.Tests
|
||||
method!.Invoke(null, null);
|
||||
}
|
||||
|
||||
private static void ResetExternalRuntime()
|
||||
{
|
||||
if (ShrinkModRuntimeDriver.InstanceForTesting != null)
|
||||
Object.DestroyImmediate(ShrinkModRuntimeDriver.InstanceForTesting.gameObject);
|
||||
ShrinkModLoader.ResetForTesting();
|
||||
}
|
||||
|
||||
private static ShrinkModFrameworkSettings CreateExternalSettings()
|
||||
{
|
||||
var settings = ScriptableObject.CreateInstance<ShrinkModFrameworkSettings>();
|
||||
settings.useContextHost = true;
|
||||
settings.enableExternalDllMods = true;
|
||||
settings.autoCreateExternalModsDirectory = false;
|
||||
settings.externalModsFolderName = "ShrinkModFrameworkTests_" + Guid.NewGuid().ToString("N");
|
||||
settings.assemblyNamePrefixes = new[] { "ExternalFixture." };
|
||||
settings.enableHarmonyPatching = false;
|
||||
settings.enableNetworkSync = false;
|
||||
settings.verboseLogging = false;
|
||||
return settings;
|
||||
}
|
||||
|
||||
private static void CleanupExternalRuntime(ShrinkModFrameworkSettings settings, string directory)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ShrinkModRuntimeDriver.InstanceForTesting != null)
|
||||
Object.DestroyImmediate(ShrinkModRuntimeDriver.InstanceForTesting.gameObject);
|
||||
ShrinkModLoader.ResetForTesting();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(directory))
|
||||
Directory.Delete(directory, true);
|
||||
Object.DestroyImmediate(settings);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CopyFixture(string fixtureName, string targetPath)
|
||||
{
|
||||
var fixturePath = Path.Combine(Application.dataPath, "Modules", "ShrinkModFramework",
|
||||
"Tests", "Fixtures", fixtureName);
|
||||
File.Copy(fixturePath, targetPath, true);
|
||||
}
|
||||
|
||||
private static void AssertExternalFixture(string version, string registryValue)
|
||||
{
|
||||
Assert.IsTrue(ShrinkModLoader.Mods.TryGetValue("external.fixture", out var handle));
|
||||
Assert.AreEqual(version, handle!.Info.Version);
|
||||
Assert.AreEqual(ShrinkModState.Ready, handle.State);
|
||||
|
||||
var registry = ShrinkModLoader.GetOrCreateRegistry<string>("external.fixture");
|
||||
Assert.IsTrue(registry.TryGet("external.fixture:version", out var value));
|
||||
Assert.AreEqual(registryValue, value);
|
||||
}
|
||||
|
||||
private static bool PumpDriverUntil(Func<bool> predicate)
|
||||
{
|
||||
for (var i = 0; i < 120; i++)
|
||||
{
|
||||
ShrinkModRuntimeDriver.InstanceForTesting?.PumpForTesting();
|
||||
if (predicate())
|
||||
return true;
|
||||
Thread.Sleep(50);
|
||||
}
|
||||
|
||||
ShrinkModRuntimeDriver.InstanceForTesting?.PumpForTesting();
|
||||
return predicate();
|
||||
}
|
||||
|
||||
private sealed class DelegateMod : IShrinkMod
|
||||
{
|
||||
private readonly Action<ShrinkModContext>? _onConstruct;
|
||||
|
||||
Reference in New Issue
Block a user