Add the validated frame-work profile, deterministic completion charges, and shadow admission meter before enforcing the scheduler in Slice E2. Lifecycle artifacts now expose elapsed work, would-yield limits, backlog bytes/age, and pending owner ledgers without changing accepted execution. Tests: dotnet build AcDream.slnx -c Release --no-restore; dotnet test AcDream.slnx -c Release --no-build --no-restore (8124 passed, 5 skipped)
389 lines
15 KiB
C#
389 lines
15 KiB
C#
using System.Collections.Generic;
|
|
using AcDream.App;
|
|
using AcDream.App.Rendering.Residency;
|
|
using AcDream.App.Streaming;
|
|
|
|
namespace AcDream.App.Tests;
|
|
|
|
/// <summary>
|
|
/// Unit tests for <see cref="RuntimeOptions"/> startup-time parsing.
|
|
/// Behavior-preservation only: every assertion locks in the exact
|
|
/// boolean / numeric / nullability semantics from the env reads that
|
|
/// previously lived in <c>GameWindow.cs</c>.
|
|
/// </summary>
|
|
public sealed class RuntimeOptionsTests
|
|
{
|
|
[Fact]
|
|
public void ResidencyBudgetsPreserveCurrentProductionDefaults()
|
|
{
|
|
RuntimeOptions options = RuntimeOptions.Parse(
|
|
"D:\\dat",
|
|
_ => null);
|
|
|
|
Assert.Equal(ResidencyBudgetOptions.Default, options.ResidencyBudgets);
|
|
Assert.Equal(
|
|
StreamingWorkBudgetOptions.Default,
|
|
options.StreamingWorkBudgets);
|
|
}
|
|
|
|
[Fact]
|
|
public void StreamingWorkBudgetOverridesAreOneTypedProfile()
|
|
{
|
|
var values = new Dictionary<string, string?>
|
|
{
|
|
["ACDREAM_STREAM_WORK_MS"] = "1.75",
|
|
["ACDREAM_STREAM_WORK_COMPLETIONS"] = "31",
|
|
["ACDREAM_STREAM_WORK_CPU_MIB"] = "6",
|
|
["ACDREAM_STREAM_WORK_ENTITY_OPS"] = "144",
|
|
["ACDREAM_STREAM_WORK_GPU_MIB"] = "5",
|
|
["ACDREAM_STREAM_WORK_GL_RETIRE_OPS"] = "23",
|
|
["ACDREAM_STREAM_WORK_DEST_RESERVE_PERCENT"] = "60",
|
|
};
|
|
|
|
RuntimeOptions options = RuntimeOptions.Parse(
|
|
"D:\\dat",
|
|
name => values.GetValueOrDefault(name));
|
|
|
|
Assert.Equal(1.75, options.StreamingWorkBudgets.MaxUpdateMilliseconds);
|
|
Assert.Equal(31, options.StreamingWorkBudgets.MaxCompletionAdmissions);
|
|
Assert.Equal(
|
|
6 * StreamingWorkBudgetOptions.MiB,
|
|
options.StreamingWorkBudgets.MaxAdoptedCpuBytes);
|
|
Assert.Equal(144, options.StreamingWorkBudgets.MaxEntityOperations);
|
|
Assert.Equal(
|
|
5 * StreamingWorkBudgetOptions.MiB,
|
|
options.StreamingWorkBudgets.MaxGpuUploadBytes);
|
|
Assert.Equal(23, options.StreamingWorkBudgets.MaxGlRetireOperations);
|
|
Assert.Equal(0.60f, options.StreamingWorkBudgets.DestinationReserveFraction);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("0")]
|
|
[InlineData("-1")]
|
|
[InlineData("NaN")]
|
|
[InlineData("Infinity")]
|
|
[InlineData("bad")]
|
|
public void InvalidStreamingWorkValuesFallBackIndependently(string value)
|
|
{
|
|
RuntimeOptions options = RuntimeOptions.Parse(
|
|
"D:\\dat",
|
|
name => name switch
|
|
{
|
|
"ACDREAM_STREAM_WORK_MS" => value,
|
|
"ACDREAM_STREAM_WORK_DEST_RESERVE_PERCENT" => value,
|
|
_ => null,
|
|
});
|
|
|
|
Assert.Equal(
|
|
StreamingWorkBudgetOptions.Default.MaxUpdateMilliseconds,
|
|
options.StreamingWorkBudgets.MaxUpdateMilliseconds);
|
|
Assert.Equal(
|
|
StreamingWorkBudgetOptions.Default.DestinationReserveFraction,
|
|
options.StreamingWorkBudgets.DestinationReserveFraction);
|
|
}
|
|
|
|
[Fact]
|
|
public void ResidencyBudgetOverridesAreTypedMebibytesAndCounts()
|
|
{
|
|
var values = new Dictionary<string, string?>
|
|
{
|
|
["ACDREAM_RESIDENCY_MESH_GPU_MIB"] = "768",
|
|
["ACDREAM_RESIDENCY_MESH_UNOWNED_ENTRIES"] = "72",
|
|
["ACDREAM_RESIDENCY_ANIMATION_MIB"] = "48",
|
|
["ACDREAM_RESIDENCY_ANIMATION_ENTRIES"] = "300",
|
|
["ACDREAM_RESIDENCY_AUDIO_MIB"] = "24",
|
|
};
|
|
|
|
RuntimeOptions options = RuntimeOptions.Parse(
|
|
"D:\\dat",
|
|
name => values.GetValueOrDefault(name));
|
|
|
|
Assert.Equal(
|
|
768 * ResidencyBudgetOptions.MiB,
|
|
options.ResidencyBudgets.ObjectMeshGpuBytes);
|
|
Assert.Equal(
|
|
72,
|
|
options.ResidencyBudgets.ObjectMeshUnownedEntries);
|
|
Assert.Equal(
|
|
48 * ResidencyBudgetOptions.MiB,
|
|
options.ResidencyBudgets.AnimationBytes);
|
|
Assert.Equal(300, options.ResidencyBudgets.AnimationEntries);
|
|
Assert.Equal(
|
|
24 * ResidencyBudgetOptions.MiB,
|
|
options.ResidencyBudgets.AudioBytes);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("0")]
|
|
[InlineData("-1")]
|
|
[InlineData("bad")]
|
|
[InlineData("8796093022208")]
|
|
public void InvalidResidencyBudgetFallsBackToCurrentDefault(string value)
|
|
{
|
|
RuntimeOptions options = RuntimeOptions.Parse(
|
|
"D:\\dat",
|
|
name => name == "ACDREAM_RESIDENCY_MESH_GPU_MIB"
|
|
? value
|
|
: null);
|
|
|
|
Assert.Equal(
|
|
ResidencyBudgetOptions.Default.ObjectMeshGpuBytes,
|
|
options.ResidencyBudgets.ObjectMeshGpuBytes);
|
|
}
|
|
|
|
private const string AnyDatDir = "C:/Users/test/dats";
|
|
|
|
private static Func<string, string?> Env(Dictionary<string, string?> values)
|
|
=> name => values.TryGetValue(name, out var v) ? v : null;
|
|
|
|
private static Func<string, string?> EmptyEnv() => _ => null;
|
|
|
|
[Fact]
|
|
public void Defaults_AllSafeOff_WhenEnvironmentIsEmpty()
|
|
{
|
|
var opts = RuntimeOptions.Parse(AnyDatDir, EmptyEnv());
|
|
|
|
Assert.Equal(AnyDatDir, opts.DatDir);
|
|
Assert.Equal(
|
|
Path.Combine(AnyDatDir, "acdream.pak"),
|
|
opts.PreparedAssetPath);
|
|
Assert.False(opts.LiveMode);
|
|
Assert.Equal("127.0.0.1", opts.LiveHost);
|
|
Assert.Equal(9000, opts.LivePort);
|
|
Assert.Null(opts.LiveUser);
|
|
Assert.Null(opts.LivePass);
|
|
Assert.False(opts.DevTools);
|
|
Assert.False(opts.UncappedRendering);
|
|
Assert.False(opts.DumpMoveTruth);
|
|
Assert.False(opts.NoAudio);
|
|
Assert.False(opts.EnableSkyPesDebug);
|
|
Assert.Equal(-1, opts.HidePartIndex);
|
|
// Default-on: RetailCloseDegrades is true unless explicitly disabled.
|
|
Assert.True(opts.RetailCloseDegrades);
|
|
Assert.False(opts.DumpSceneryZ);
|
|
Assert.False(opts.DumpClothing);
|
|
Assert.Null(opts.LegacyStreamRadius);
|
|
Assert.False(opts.UiProbeDump);
|
|
Assert.Null(opts.UiProbeScript);
|
|
Assert.Null(opts.AutomationArtifactDirectory);
|
|
Assert.Equal(0.7f, opts.FogStartMultiplier);
|
|
Assert.Equal(0.95f, opts.FogEndMultiplier);
|
|
Assert.False(opts.UiProbeEnabled);
|
|
Assert.False(opts.HasLiveCredentials);
|
|
}
|
|
|
|
[Fact]
|
|
public void PreparedAssetPath_DefaultsBesideDats_AndAllowsOneOverride()
|
|
{
|
|
Assert.Equal(
|
|
Path.Combine(AnyDatDir, "acdream.pak"),
|
|
RuntimeOptions.Parse(AnyDatDir, EmptyEnv()).PreparedAssetPath);
|
|
|
|
var overridden = RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_PAK_PATH"] = "D:/prepared/acdream.pak" }));
|
|
Assert.Equal("D:/prepared/acdream.pak", overridden.PreparedAssetPath);
|
|
}
|
|
|
|
[Fact]
|
|
public void LiveMode_Set_ExactlyByValue1()
|
|
{
|
|
Assert.True(RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_LIVE"] = "1" })).LiveMode);
|
|
Assert.False(RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_LIVE"] = "0" })).LiveMode);
|
|
Assert.False(RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_LIVE"] = "true" })).LiveMode);
|
|
Assert.False(RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_LIVE"] = "" })).LiveMode);
|
|
}
|
|
|
|
[Fact]
|
|
public void LiveHostAndPort_FallBackToDefaults_WhenUnsetOrInvalid()
|
|
{
|
|
var withDefaults = RuntimeOptions.Parse(AnyDatDir, EmptyEnv());
|
|
Assert.Equal("127.0.0.1", withDefaults.LiveHost);
|
|
Assert.Equal(9000, withDefaults.LivePort);
|
|
|
|
var withOverrides = RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_TEST_HOST"] = "play.example.com",
|
|
["ACDREAM_TEST_PORT"] = "9123",
|
|
}));
|
|
Assert.Equal("play.example.com", withOverrides.LiveHost);
|
|
Assert.Equal(9123, withOverrides.LivePort);
|
|
|
|
// Non-numeric port falls back to default; we don't throw at parse time.
|
|
var withBadPort = RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_TEST_PORT"] = "abc" }));
|
|
Assert.Equal(9000, withBadPort.LivePort);
|
|
}
|
|
|
|
[Fact]
|
|
public void LiveUserPass_NullWhenEmptyOrUnset()
|
|
{
|
|
var emptyValues = RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_TEST_USER"] = "",
|
|
["ACDREAM_TEST_PASS"] = "",
|
|
}));
|
|
Assert.Null(emptyValues.LiveUser);
|
|
Assert.Null(emptyValues.LivePass);
|
|
|
|
var realValues = RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_TEST_USER"] = "testaccount",
|
|
["ACDREAM_TEST_PASS"] = "testpassword",
|
|
}));
|
|
Assert.Equal("testaccount", realValues.LiveUser);
|
|
Assert.Equal("testpassword", realValues.LivePass);
|
|
}
|
|
|
|
[Fact]
|
|
public void HasLiveCredentials_RequiresLiveModeAndBothUserAndPass()
|
|
{
|
|
// Live mode off → no credentials regardless of user/pass.
|
|
var noLive = RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_TEST_USER"] = "u",
|
|
["ACDREAM_TEST_PASS"] = "p",
|
|
}));
|
|
Assert.False(noLive.HasLiveCredentials);
|
|
|
|
// Live mode on but missing user → no credentials.
|
|
var missingUser = RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_LIVE"] = "1",
|
|
["ACDREAM_TEST_PASS"] = "p",
|
|
}));
|
|
Assert.False(missingUser.HasLiveCredentials);
|
|
|
|
// Live mode on but missing pass → no credentials.
|
|
var missingPass = RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_LIVE"] = "1",
|
|
["ACDREAM_TEST_USER"] = "u",
|
|
}));
|
|
Assert.False(missingPass.HasLiveCredentials);
|
|
|
|
// All three present → credentials available.
|
|
var ok = RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_LIVE"] = "1",
|
|
["ACDREAM_TEST_USER"] = "u",
|
|
["ACDREAM_TEST_PASS"] = "p",
|
|
}));
|
|
Assert.True(ok.HasLiveCredentials);
|
|
}
|
|
|
|
[Fact]
|
|
public void HidePartIndex_MinusOneWhenUnset_ParsesIntegers()
|
|
{
|
|
Assert.Equal(-1, RuntimeOptions.Parse(AnyDatDir, EmptyEnv()).HidePartIndex);
|
|
Assert.Equal(7, RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_HIDE_PART"] = "7" })).HidePartIndex);
|
|
// Invalid → fall back to -1 (preserves the int.TryParse failure semantics).
|
|
Assert.Equal(-1, RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_HIDE_PART"] = "abc" })).HidePartIndex);
|
|
}
|
|
|
|
[Fact]
|
|
public void RetailCloseDegrades_DefaultOn_ExceptWhenValueIsExactlyZero()
|
|
{
|
|
// Unset → on.
|
|
Assert.True(RuntimeOptions.Parse(AnyDatDir, EmptyEnv()).RetailCloseDegrades);
|
|
|
|
// Exactly "0" → off. Matches the pre-refactor semantics:
|
|
// !string.Equals(env, "0", StringComparison.Ordinal)
|
|
Assert.False(RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_RETAIL_CLOSE_DEGRADES"] = "0",
|
|
})).RetailCloseDegrades);
|
|
|
|
// Any other value → on (including "1", "false", "True"). The original
|
|
// code only checked for the literal "0"; preserve that.
|
|
Assert.True(RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_RETAIL_CLOSE_DEGRADES"] = "1",
|
|
})).RetailCloseDegrades);
|
|
Assert.True(RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_RETAIL_CLOSE_DEGRADES"] = "false",
|
|
})).RetailCloseDegrades);
|
|
}
|
|
|
|
[Fact]
|
|
public void LegacyStreamRadius_NullWhenUnsetOrInvalid_ParsesNonNegativeIntegers()
|
|
{
|
|
Assert.Null(RuntimeOptions.Parse(AnyDatDir, EmptyEnv()).LegacyStreamRadius);
|
|
Assert.Null(RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_STREAM_RADIUS"] = "abc" })).LegacyStreamRadius);
|
|
// Negative values are filtered out by the pre-refactor `sr >= 0` guard.
|
|
Assert.Null(RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_STREAM_RADIUS"] = "-3" })).LegacyStreamRadius);
|
|
|
|
Assert.Equal(0, RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_STREAM_RADIUS"] = "0" })).LegacyStreamRadius);
|
|
Assert.Equal(5, RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_STREAM_RADIUS"] = "5" })).LegacyStreamRadius);
|
|
Assert.Equal(12, RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_STREAM_RADIUS"] = "12" })).LegacyStreamRadius);
|
|
}
|
|
|
|
[Fact]
|
|
public void FogMultipliers_ParseInvariantFloats_AndFallBackIndependently()
|
|
{
|
|
var parsed = RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_FOG_START_MULT"] = "0.625",
|
|
["ACDREAM_FOG_END_MULT"] = "1.125",
|
|
}));
|
|
Assert.Equal(0.625f, parsed.FogStartMultiplier);
|
|
Assert.Equal(1.125f, parsed.FogEndMultiplier);
|
|
|
|
var invalid = RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_FOG_START_MULT"] = "not-a-number",
|
|
["ACDREAM_FOG_END_MULT"] = "",
|
|
}));
|
|
Assert.Equal(0.7f, invalid.FogStartMultiplier);
|
|
Assert.Equal(0.95f, invalid.FogEndMultiplier);
|
|
}
|
|
|
|
[Fact]
|
|
public void DiagnosticFlags_RespectExactValueOne()
|
|
{
|
|
var allOn = RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_DEVTOOLS"] = "1",
|
|
["ACDREAM_UNCAPPED_RENDER"] = "1",
|
|
["ACDREAM_DUMP_MOVE_TRUTH"] = "1",
|
|
["ACDREAM_NO_AUDIO"] = "1",
|
|
["ACDREAM_ENABLE_SKY_PES"] = "1",
|
|
["ACDREAM_DUMP_SCENERY_Z"] = "1",
|
|
["ACDREAM_DUMP_CLOTHING"] = "1",
|
|
}));
|
|
Assert.True(allOn.DevTools);
|
|
Assert.True(allOn.UncappedRendering);
|
|
Assert.True(allOn.DumpMoveTruth);
|
|
Assert.True(allOn.NoAudio);
|
|
Assert.True(allOn.EnableSkyPesDebug);
|
|
Assert.True(allOn.DumpSceneryZ);
|
|
Assert.True(allOn.DumpClothing);
|
|
|
|
// Any non-"1" value leaves them off, matching the
|
|
// string.Equals(env, "1", StringComparison.Ordinal) check.
|
|
var anyOther = RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_DEVTOOLS"] = "true",
|
|
["ACDREAM_UNCAPPED_RENDER"] = "true",
|
|
["ACDREAM_DUMP_MOVE_TRUTH"] = "yes",
|
|
["ACDREAM_NO_AUDIO"] = "2",
|
|
["ACDREAM_ENABLE_SKY_PES"] = "on",
|
|
["ACDREAM_DUMP_SCENERY_Z"] = " 1",
|
|
["ACDREAM_DUMP_CLOTHING"] = "true",
|
|
}));
|
|
Assert.False(anyOther.DevTools);
|
|
Assert.False(anyOther.UncappedRendering);
|
|
Assert.False(anyOther.DumpMoveTruth);
|
|
Assert.False(anyOther.NoAudio);
|
|
Assert.False(anyOther.EnableSkyPesDebug);
|
|
Assert.False(anyOther.DumpSceneryZ);
|
|
Assert.False(anyOther.DumpClothing);
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_RejectsNullDatDirOrEnv()
|
|
{
|
|
Assert.Throws<ArgumentNullException>(() => RuntimeOptions.Parse(null!, EmptyEnv()));
|
|
Assert.Throws<ArgumentNullException>(() => RuntimeOptions.Parse(AnyDatDir, null!));
|
|
}
|
|
}
|