Files
UnrealEngine/Engine/Source/Programs/Horde/HordeServer.Tests/Configuration/ConfigTests.cs
2025-05-18 13:04:45 +08:00

321 lines
12 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using EpicGames.Core;
using HordeServer.Acls;
using HordeServer.Configuration;
using HordeServer.Plugins;
using HordeServer.Projects;
using HordeServer.Server;
using HordeServer.Streams;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace HordeServer.Tests.Configuration
{
[TestClass]
public class ConfigTests
{
class SubObject
{
public string? ValueA { get; set; }
public int ValueB { get; set; }
public SubObject? ValueC { get; set; }
}
[ConfigIncludeRoot]
class ConfigObject
{
public List<ConfigInclude> Include { get; set; } = new List<ConfigInclude>();
public string? TestString { get; set; }
public List<string> TestList { get; set; } = new List<string>();
public SubObject? TestObject { get; set; }
}
readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions { DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault };
[TestMethod]
public async Task IncludeTestAsync()
{
CancellationToken cancellationToken = CancellationToken.None;
InMemoryConfigSource source = new InMemoryConfigSource();
// memory:///bar
Uri barUri = new Uri("memory:///bar");
{
ConfigObject obj = new ConfigObject();
obj.TestList.Add("secondobj");
obj.TestObject = new SubObject { ValueB = 123, ValueC = new SubObject { ValueB = 456 } };
byte[] data2 = JsonSerializer.SerializeToUtf8Bytes(obj, _jsonOptions);
source.Add(barUri, data2);
}
// memory:///foo
Uri fooUri = new Uri("memory:///foo");
{
ConfigObject obj = new ConfigObject();
obj.Include.Add(new ConfigInclude { Path = barUri.ToString() });
obj.TestString = "hello";
obj.TestList.Add("there");
obj.TestList.Add("world");
obj.TestObject = new SubObject { ValueA = "hi", ValueC = new SubObject { ValueA = "yo" } };
byte[] json1 = JsonSerializer.SerializeToUtf8Bytes(obj, _jsonOptions);
source.Add(fooUri, json1);
}
Dictionary<string, IConfigSource> sources = new Dictionary<string, IConfigSource>();
sources["memory"] = source;
ConfigContext context = new ConfigContext(_jsonOptions, sources, NullLogger.Instance);
ConfigObject result = await context.ReadAsync<ConfigObject>(fooUri, cancellationToken);
Assert.AreEqual(result.TestString, "hello");
Assert.IsTrue(result.TestList.SequenceEqual(new[] { "secondobj", "there", "world" }));
Assert.AreEqual(result.TestObject!.ValueA, "hi");
Assert.AreEqual(result.TestObject!.ValueB, 123);
Assert.AreEqual(result.TestObject!.ValueC!.ValueA, "yo");
}
[TestMethod]
public async Task FileTestAsync()
{
CancellationToken cancellationToken = CancellationToken.None;
DirectoryReference baseDir = new DirectoryReference("test");
DirectoryReference.CreateDirectory(baseDir);
FileConfigSource source = new FileConfigSource(baseDir);
// file:test/foo
Uri fooUri = new Uri($"file:///{FileReference.Combine(baseDir, "test.json")}");
byte[] data;
{
ConfigObject obj = new ConfigObject();
obj.TestString = "hello";
data = JsonSerializer.SerializeToUtf8Bytes(obj, new JsonSerializerOptions { DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault });
}
await FileReference.WriteAllBytesAsync(new FileReference(fooUri.LocalPath), data, cancellationToken);
Dictionary<string, IConfigSource> sources = new Dictionary<string, IConfigSource>();
sources["file"] = source;
ConfigContext context = new ConfigContext(_jsonOptions, sources, NullLogger.Instance);
ConfigObject result = await context.ReadAsync<ConfigObject>(fooUri, cancellationToken);
Assert.AreEqual(result.TestString, "hello");
// Check it returns the same object if the timestamp hasn't changed
IConfigFile file1 = await source.GetAsync(fooUri, cancellationToken);
await Task.Delay(TimeSpan.FromSeconds(1));
IConfigFile file2 = await source.GetAsync(fooUri, cancellationToken);
Assert.IsTrue(ReferenceEquals(file1, file2));
// Check it returns a new object if the timestamp HAS changed
await Task.Delay(TimeSpan.FromSeconds(1));
await FileReference.WriteAllBytesAsync(new FileReference(fooUri.LocalPath), data, cancellationToken);
IConfigFile file3 = await source.GetAsync(fooUri, cancellationToken);
Assert.IsTrue(!ReferenceEquals(file1, file3));
}
private static NetworkConfig? GetNetworkConfig(ComputeConfig gc, string ip)
{
bool result = gc.TryGetNetworkConfig(IPAddress.Parse(ip), out NetworkConfig? networkConfig);
return result ? networkConfig : null;
}
[TestMethod]
public void NetworkConfig()
{
ComputeConfig gc = new()
{
Networks = new List<NetworkConfig>()
{
new() { CidrBlock = "10.0.0.0/31", Id = "foo" },
new() { CidrBlock = "10.0.0.4/30", Id = "bar" },
new() { CidrBlock = "192.168.0.0/16", Id = "baz" },
}
};
Assert.AreEqual("foo", GetNetworkConfig(gc, "10.0.0.0")!.Id);
Assert.AreEqual("foo", GetNetworkConfig(gc, "10.0.0.1")!.Id);
Assert.AreEqual(null, GetNetworkConfig(gc, "10.0.0.2"));
Assert.AreEqual(null, GetNetworkConfig(gc, "10.0.0.3"));
Assert.AreEqual("bar", GetNetworkConfig(gc, "10.0.0.4")!.Id);
Assert.AreEqual("bar", GetNetworkConfig(gc, "10.0.0.5")!.Id);
Assert.AreEqual("bar", GetNetworkConfig(gc, "10.0.0.6")!.Id);
Assert.AreEqual("bar", GetNetworkConfig(gc, "10.0.0.7")!.Id);
Assert.AreEqual(null, GetNetworkConfig(gc, "10.0.0.8"));
Assert.AreEqual("baz", GetNetworkConfig(gc, "192.168.0.0")!.Id);
Assert.AreEqual("baz", GetNetworkConfig(gc, "192.168.0.1")!.Id);
Assert.AreEqual("baz", GetNetworkConfig(gc, "192.168.255.254")!.Id);
Assert.AreEqual("baz", GetNetworkConfig(gc, "192.168.255.255")!.Id);
Assert.AreEqual(null, GetNetworkConfig(gc, "192.169.0.0"));
Assert.AreEqual(null, GetNetworkConfig(gc, "192.169.0.1"));
Assert.AreEqual(null, GetNetworkConfig(gc, "11.0.0.1"));
gc = new()
{
Networks = new List<NetworkConfig>() { new() { CidrBlock = "0.0.0.0/0", Id = "global" } }
};
Assert.AreEqual("global", GetNetworkConfig(gc, "15.3.4.5")!.Id);
}
[TestMethod]
public async Task WorkspaceConfigAsync()
{
Dictionary<string, WorkspaceConfig> inputWorkspaces = new();
inputWorkspaces["base"] = new WorkspaceConfig { Identifier = "base", Cluster = "myCluster", MinScratchSpace = 111 };
inputWorkspaces["subType"] = new WorkspaceConfig { Base = "base", Identifier = "subType", ConformDiskFreeSpace = 222 };
inputWorkspaces["subSubType"] = new WorkspaceConfig { Base = "subType", Identifier = "subSubType" };
BuildConfig buildConfig = new BuildConfig();
buildConfig.Projects.Add(new ProjectConfig { Streams = [new StreamConfig { WorkspaceTypes = inputWorkspaces }]});
GlobalConfig gc = new();
gc.Plugins.AddComputeConfig(new ComputeConfig());
gc.Plugins.AddBuildConfig(buildConfig);
await gc.PostLoadAsync(new ServerSettings(), Array.Empty<ILoadedPlugin>(), Array.Empty<IDefaultAclModifier>());
Dictionary<string,WorkspaceConfig> workspaces = gc.Plugins.GetBuildConfig().Projects[0].Streams[0].WorkspaceTypes;
Assert.AreEqual(3, workspaces.Count);
Assert.AreEqual(111, workspaces["subType"].MinScratchSpace);
Assert.AreEqual(111, workspaces["subSubType"].MinScratchSpace);
Assert.AreEqual(222, workspaces["subSubType"].ConformDiskFreeSpace);
}
[TestMethod]
public async Task WorkspaceInheritFromProjectConfigAsync()
{
List<string> autoSdkViews = ["foo", "bar"];
BuildConfig buildConfig = new BuildConfig();
buildConfig.Projects.Add(new ProjectConfig
{
WorkspaceTypes =
{
{ "project1", new WorkspaceConfig { AutoSdkView = autoSdkViews } },
{ "project2", new WorkspaceConfig { ConformDiskFreeSpace = 111 } },
{ "project3", new WorkspaceConfig { MinScratchSpace = 222 } },
},
Streams = [
new StreamConfig { WorkspaceTypes =
{
{"stream1", new WorkspaceConfig { Base = "project1", Stream = "myStream" }},
{"project3", new WorkspaceConfig { Stream = "otherStream" }}
}}
]
});
GlobalConfig gc = new();
gc.Plugins.AddComputeConfig(new ComputeConfig());
gc.Plugins.AddBuildConfig(buildConfig);
await gc.PostLoadAsync(new ServerSettings(), Array.Empty<ILoadedPlugin>(), Array.Empty<IDefaultAclModifier>());
Dictionary<string, WorkspaceConfig> workspaces = gc.Plugins.GetBuildConfig().Projects[0].Streams[0].WorkspaceTypes;
Assert.AreEqual(4, workspaces.Count);
// Project-defined streams
CollectionAssert.AreEquivalent(autoSdkViews, workspaces["project1"].AutoSdkView);
Assert.AreEqual(111, workspaces["project2"].ConformDiskFreeSpace);
Assert.AreEqual(111, workspaces["project2"].ConformDiskFreeSpace);
// project3 should be overridden by stream workspace type with the same name
Assert.AreEqual("otherStream", workspaces["project3"].Stream);
Assert.IsNull(workspaces["project3"].MinScratchSpace);
// stream1 inherits from project1 workspace type
CollectionAssert.AreEquivalent(autoSdkViews, workspaces["stream1"].AutoSdkView);
Assert.AreEqual("myStream", workspaces["stream1"].Stream);
}
class ObjectValue
{
public string Value { get; set; } = "";
}
[ConfigMacroScope]
class BaseMacroScope
{
public List<ConfigMacro> Macros { get; set; } = new List<ConfigMacro>();
public string Value { get; set; } = "";
public List<string> ListValue { get; set; } = new List<string>();
public ObjectValue ObjectValue { get; set; } = new ObjectValue();
public BaseMacroScope? ChildScope { get; set; }
}
[TestMethod]
public async Task MacroTestAsync()
{
CancellationToken cancellationToken = CancellationToken.None;
InMemoryConfigSource source = new InMemoryConfigSource();
// memory:///bar
Uri fooUri = new Uri("memory:///foo");
{
BaseMacroScope obj = new BaseMacroScope();
obj.Macros.Add(new ConfigMacro { Name = "MacroName", Value = "MacroValue" });
obj.Value = "This is a macro $(MacroName)";
obj.ListValue.Add("List element macro $(MacroName)");
obj.ObjectValue = new ObjectValue { Value = "Object macro $(MacroName)" };
byte[] data2 = JsonSerializer.SerializeToUtf8Bytes(obj, _jsonOptions);
source.Add(fooUri, data2);
}
Dictionary<string, IConfigSource> sources = new Dictionary<string, IConfigSource>();
sources["memory"] = source;
ConfigContext context = new ConfigContext(_jsonOptions, sources, NullLogger.Instance);
BaseMacroScope result = await context.ReadAsync<BaseMacroScope>(fooUri, cancellationToken);
Assert.AreEqual("This is a macro MacroValue", result.Value);
Assert.AreEqual("List element macro MacroValue", result.ListValue[0]);
Assert.AreEqual("Object macro MacroValue", result.ObjectValue.Value);
}
[TestMethod]
public async Task NestedMacroTestAsync()
{
CancellationToken cancellationToken = CancellationToken.None;
InMemoryConfigSource source = new InMemoryConfigSource();
// memory:///bar
Uri fooUri = new Uri("memory:///foo");
{
BaseMacroScope obj = new BaseMacroScope();
obj.Macros.Add(new ConfigMacro { Name = "MacroName", Value = "MacroValue" });
obj.ChildScope = new BaseMacroScope();
obj.ChildScope.Macros.Add(new ConfigMacro { Name = "MacroName2", Value = "MacroValue2" });
obj.ChildScope.Value = "This is a macro $(MacroName) $(MacroName2)";
obj.Value = "This is a macro $(MacroName) $(MacroName2)";
byte[] data2 = JsonSerializer.SerializeToUtf8Bytes(obj, _jsonOptions);
source.Add(fooUri, data2);
}
Dictionary<string, IConfigSource> sources = new Dictionary<string, IConfigSource>();
sources["memory"] = source;
ConfigContext context = new ConfigContext(_jsonOptions, sources, NullLogger.Instance);
BaseMacroScope result = await context.ReadAsync<BaseMacroScope>(fooUri, cancellationToken);
Assert.AreEqual("This is a macro MacroValue $(MacroName2)", result.Value);
Assert.AreEqual("This is a macro MacroValue MacroValue2", result.ChildScope!.Value);
}
}
}