using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; using FrostFS.SDK.Client.Models.Netmap.Placement; using Xunit.Abstractions; namespace FrostFS.SDK.Tests.Unit; [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "Default Value is correct for tests")] public class PlacementVectorTests(ITestOutputHelper testOutputHelper) { private static readonly JsonSerializerOptions serializeOptions = new() { PropertyNameCaseInsensitive = true }; private readonly ITestOutputHelper _testOutputHelper = testOutputHelper; [Fact] public void PlacementTest() { var path = ".\\..\\..\\..\\TestData\\PlacementTests"; Assert.True(Directory.Exists(path)); var files = Directory.GetFiles(path); FrostFsVersion v = new(2, 13); var addresses = new string[] { "localhost", "server1" }; foreach (var file in files.Where(f => f.EndsWith(".json", StringComparison.OrdinalIgnoreCase))) { //if (!file.EndsWith("selector_invalid.json")) // continue; var fileName = file[(file.LastIndexOf("..\\", StringComparison.OrdinalIgnoreCase) + 3)..]; _testOutputHelper.WriteLine($"Open file {fileName}"); var str = File.ReadAllText(file); Assert.False(string.IsNullOrEmpty(str)); var testCase = JsonSerializer.Deserialize(str, serializeOptions); Assert.NotNull(testCase); Assert.NotNull(testCase.Nodes); Assert.True(testCase.Nodes.Length > 0); _testOutputHelper.WriteLine($"Test case: \"{testCase.Name}\""); var nodes = testCase.Nodes .Select(n => new FrostFsNodeInfo(v, n.State, addresses.AsReadOnly(), n.Attributes?.ToDictionary(x => x.Key, x => x.Value) ?? [], n.PublicKeyBytes ) ) .ToArray() .AsReadOnly(); var netmap = new FrostFsNetmapSnapshot(100, nodes); Assert.NotNull(testCase.Tests); foreach (var test in testCase.Tests) { _testOutputHelper.WriteLine($"Start test \"{test.Name}\""); var policy = new FrostFsPlacementPolicy( test.Policy!.Unique, test.Policy.ContainerBackupFactor, new Collection(test.Policy.Selectors?.Select(s => s.Selector).ToList() ?? []), new Collection(test.Policy.Filters?.Select(f => f.Filter).ToList() ?? []), test.Policy.Replicas?.Select(r => new FrostFsReplica(r.Count, r.Selector)).ToArray() ?? [] ); try { var result = netmap.ContainerNodes(policy, test.PivotBytes); if (test.Result == null) { if (!string.IsNullOrEmpty(test.Error)) { Assert.Fail("Error is expected but has not been thrown"); } else { Assert.NotNull(test.Policy?.Replicas); Assert.Equal(result.Length, test.Policy.Replicas.Length); for (int i = 0; i < result.Length; i++) { Assert.Empty(result[i]); } } } else { Assert.Equal(test.Result.Length, result.Length); for (var i = 0; i < test.Result.Length; i++) { Assert.Equal(test.Result[i].Length, result[i].Length); for (var j = 0; j < test.Result[i].Length; j++) { CompareNodes(nodes[test.Result[i][j]].Attributes, result[i][j]); } } if (test.Placement?.Result != null && test.Placement.PivotBytes != null) { var placementResult = netmap.PlacementVectors(result, test.Placement.PivotBytes); Assert.Equal(test.Placement.Result.Length, placementResult.Length); for (int i = 0; i < placementResult.Length; i++) { Assert.Equal(test.Placement.Result[i].Length, placementResult[i].Length); for (int j = 0; j < placementResult[i].Length; j++) { CompareNodes(nodes[test.Placement.Result[i][j]].Attributes, placementResult[i][j]); } } } } } catch (Exception ex) { if (!string.IsNullOrEmpty(test.Error)) { Assert.Contains(test.Error, ex.Message, StringComparison.InvariantCulture); } else { throw; } } _testOutputHelper.WriteLine($"Done"); } } } private static void CompareNodes(IReadOnlyDictionary attrs, FrostFsNodeInfo nodeInfo) { Assert.Equal(attrs.Count, nodeInfo.Attributes.Count); Assert.True(attrs.OrderBy(k => k.Key).SequenceEqual(nodeInfo.Attributes.OrderBy(x => x.Key))); } } public class TestCase { public string? Name { get; set; } public Node[]? Nodes { get; set; } public TestData[]? Tests { get; set; } } public class Node { [JsonPropertyName("attributes")] public KeyValuePair[]? Attributes { get; set; } public string? PublicKey { get; set; } internal byte[]? PublicKeyBytes => string.IsNullOrEmpty(PublicKey) ? [] : Convert.FromBase64String(PublicKey); public string[]? Addresses { get; set; } [JsonConverter(typeof(JsonStringEnumConverter))] public NodeState State { get; set; } = NodeState.Online; } public class TestData { public string? Name { get; set; } public PolicyDto? Policy { get; set; } public string? Pivot { get; set; } public int[][]? Result { get; set; } public string? Error { get; set; } internal byte[]? PivotBytes => Pivot != null ? Convert.FromBase64String(Pivot) : null; public ResultData? Placement { get; set; } } public class PolicyDto { public bool Unique { get; set; } public uint ContainerBackupFactor { get; set; } public FilterDto[]? Filters { get; set; } public ReplicaDto[]? Replicas { get; set; } public SelectorDto[]? Selectors { get; set; } } public class SelectorDto() { public uint Count { get; set; } public string? Name { get; set; } [JsonConverter(typeof(JsonStringEnumConverter))] public ClauseValues Clause { get; set; } public string? Attribute { get; set; } public string? Filter { get; set; } public FrostFsSelector Selector => new(Name ?? string.Empty) { Count = Count, Clause = (int)Clause, Filter = Filter, Attribute = Attribute }; } public class FilterDto { public string? Name { get; set; } public string? Key { get; set; } [JsonConverter(typeof(JsonStringEnumConverter))] public Operation Op { get; set; } public string? Value { get; set; } public FilterDto[]? Filters { get; set; } public FrostFsFilter Filter => new( Name ?? string.Empty, Key ?? string.Empty, (int)Op, Value ?? string.Empty, Filters != null ? [.. Filters.Select(f => f.Filter)] : []); } public class ReplicaDto { public uint Count { get; set; } public string? Selector { get; set; } } public class ResultData { public string? Pivot { get; set; } public int[][]? Result { get; set; } internal byte[]? PivotBytes => Pivot != null ? Convert.FromBase64String(Pivot) : null; } public enum ClauseValues { UNSPECIFIED = 0, SAME, DISTINCT }