Reformat/cleanup
All checks were successful
NuGet Package Publish / nuget (push) Successful in 1m10s
All checks were successful
NuGet Package Publish / nuget (push) Successful in 1m10s
This commit is contained in:
@@ -1,14 +1,12 @@
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml.Linq;
|
||||
using ZB.MOM.WW.CBDDC.Core;
|
||||
|
||||
namespace ZB.MOM.WW.CBDDC.Core.Tests;
|
||||
|
||||
public class ArchitectureFitnessTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that the core assembly does not reference outer-layer assemblies.
|
||||
/// Verifies that the core assembly does not reference outer-layer assemblies.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CoreAssembly_ShouldNotReferenceOuterAssemblies()
|
||||
@@ -25,71 +23,71 @@ public class ArchitectureFitnessTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that project references under src form an acyclic graph.
|
||||
/// Verifies that project references under src form an acyclic graph.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SourceProjectGraph_ShouldBeAcyclic()
|
||||
{
|
||||
var repoRoot = FindRepoRoot();
|
||||
var srcRoot = Path.Combine(repoRoot, "src");
|
||||
string repoRoot = FindRepoRoot();
|
||||
string srcRoot = Path.Combine(repoRoot, "src");
|
||||
|
||||
var projectFiles = Directory
|
||||
.EnumerateFiles(srcRoot, "*.csproj", SearchOption.AllDirectories)
|
||||
.Where(p => !p.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}", StringComparison.Ordinal)
|
||||
&& !p.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.Ordinal))
|
||||
.Where(p => !p.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}",
|
||||
StringComparison.Ordinal)
|
||||
&& !p.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}",
|
||||
StringComparison.Ordinal))
|
||||
.ToList();
|
||||
|
||||
var nodes = projectFiles.ToDictionary(
|
||||
p => Path.GetFileNameWithoutExtension(p),
|
||||
p => new HashSet<string>(StringComparer.Ordinal));
|
||||
|
||||
foreach (var projectFile in projectFiles)
|
||||
foreach (string projectFile in projectFiles)
|
||||
{
|
||||
var projectName = Path.GetFileNameWithoutExtension(projectFile);
|
||||
string projectName = Path.GetFileNameWithoutExtension(projectFile);
|
||||
var doc = XDocument.Load(projectFile);
|
||||
var refs = doc.Descendants("ProjectReference")
|
||||
.Select(x => x.Attribute("Include")?.Value)
|
||||
.Where(v => !string.IsNullOrWhiteSpace(v))
|
||||
.Select(v => Path.GetFileNameWithoutExtension(v!.Replace('\\', '/')));
|
||||
|
||||
foreach (var reference in refs)
|
||||
{
|
||||
foreach (string reference in refs)
|
||||
if (nodes.ContainsKey(reference))
|
||||
{
|
||||
nodes[projectName].Add(reference);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HasCycle(nodes).ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the allowed dependency graph between source projects.
|
||||
/// Verifies the allowed dependency graph between source projects.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SourceProjectReferences_ShouldMatchAllowedDependencyGraph()
|
||||
{
|
||||
var repoRoot = FindRepoRoot();
|
||||
var srcRoot = Path.Combine(repoRoot, "src");
|
||||
string repoRoot = FindRepoRoot();
|
||||
string srcRoot = Path.Combine(repoRoot, "src");
|
||||
|
||||
var projectFiles = Directory
|
||||
.EnumerateFiles(srcRoot, "*.csproj", SearchOption.AllDirectories)
|
||||
.Where(p => !p.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}", StringComparison.Ordinal)
|
||||
&& !p.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.Ordinal))
|
||||
.Where(p => !p.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}",
|
||||
StringComparison.Ordinal)
|
||||
&& !p.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}",
|
||||
StringComparison.Ordinal))
|
||||
.ToList();
|
||||
|
||||
var allowedDependencies = new Dictionary<string, HashSet<string>>(StringComparer.Ordinal)
|
||||
{
|
||||
["ZB.MOM.WW.CBDDC.Core"] = new HashSet<string>(StringComparer.Ordinal),
|
||||
["ZB.MOM.WW.CBDDC.Network"] = new HashSet<string>(StringComparer.Ordinal) { "ZB.MOM.WW.CBDDC.Core" },
|
||||
["ZB.MOM.WW.CBDDC.Persistence"] = new HashSet<string>(StringComparer.Ordinal) { "ZB.MOM.WW.CBDDC.Core" },
|
||||
["ZB.MOM.WW.CBDDC.Hosting"] = new HashSet<string>(StringComparer.Ordinal) { "ZB.MOM.WW.CBDDC.Network" }
|
||||
["ZB.MOM.WW.CBDDC.Core"] = new(StringComparer.Ordinal),
|
||||
["ZB.MOM.WW.CBDDC.Network"] = new(StringComparer.Ordinal) { "ZB.MOM.WW.CBDDC.Core" },
|
||||
["ZB.MOM.WW.CBDDC.Persistence"] = new(StringComparer.Ordinal) { "ZB.MOM.WW.CBDDC.Core" },
|
||||
["ZB.MOM.WW.CBDDC.Hosting"] = new(StringComparer.Ordinal) { "ZB.MOM.WW.CBDDC.Network" }
|
||||
};
|
||||
|
||||
foreach (var projectFile in projectFiles)
|
||||
foreach (string projectFile in projectFiles)
|
||||
{
|
||||
var projectName = Path.GetFileNameWithoutExtension(projectFile);
|
||||
string projectName = Path.GetFileNameWithoutExtension(projectFile);
|
||||
allowedDependencies.ContainsKey(projectName)
|
||||
.ShouldBeTrue($"Unexpected source project found: {projectName}");
|
||||
|
||||
@@ -105,18 +103,19 @@ public class ArchitectureFitnessTests
|
||||
var missing = expected.Where(e => !references.Contains(e)).ToList();
|
||||
|
||||
extra.ShouldBeEmpty($"Project {projectName} has disallowed references: {string.Join(", ", extra)}");
|
||||
missing.ShouldBeEmpty($"Project {projectName} is missing required references: {string.Join(", ", missing)}");
|
||||
missing.ShouldBeEmpty(
|
||||
$"Project {projectName} is missing required references: {string.Join(", ", missing)}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies non-generic ILogger usage is restricted to explicit compatibility shims.
|
||||
/// Verifies non-generic ILogger usage is restricted to explicit compatibility shims.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SourceCode_ShouldRestrictNonGenericILoggerUsage()
|
||||
{
|
||||
var repoRoot = FindRepoRoot();
|
||||
var srcRoot = Path.Combine(repoRoot, "src");
|
||||
string repoRoot = FindRepoRoot();
|
||||
string srcRoot = Path.Combine(repoRoot, "src");
|
||||
var loggerPattern = new Regex(@"\bILogger\b(?!\s*<|\s*Factory\b)", RegexOptions.Compiled);
|
||||
|
||||
var allowedSnippets = new[]
|
||||
@@ -130,45 +129,39 @@ public class ArchitectureFitnessTests
|
||||
|
||||
var violations = new List<string>();
|
||||
var sourceFiles = Directory.EnumerateFiles(srcRoot, "*.cs", SearchOption.AllDirectories)
|
||||
.Where(p => !p.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}", StringComparison.Ordinal)
|
||||
&& !p.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.Ordinal));
|
||||
.Where(p => !p.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}",
|
||||
StringComparison.Ordinal)
|
||||
&& !p.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}",
|
||||
StringComparison.Ordinal));
|
||||
|
||||
foreach (var file in sourceFiles)
|
||||
foreach (string file in sourceFiles)
|
||||
{
|
||||
var lines = File.ReadAllLines(file);
|
||||
string[] lines = File.ReadAllLines(file);
|
||||
for (var i = 0; i < lines.Length; i++)
|
||||
{
|
||||
var line = lines[i].Trim();
|
||||
if (string.IsNullOrWhiteSpace(line) || line.StartsWith("//", StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
string line = lines[i].Trim();
|
||||
if (string.IsNullOrWhiteSpace(line) || line.StartsWith("//", StringComparison.Ordinal)) continue;
|
||||
|
||||
if (!loggerPattern.IsMatch(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!loggerPattern.IsMatch(line)) continue;
|
||||
|
||||
if (allowedSnippets.Any(line.Contains))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (allowedSnippets.Any(line.Contains)) continue;
|
||||
|
||||
var relativePath = Path.GetRelativePath(repoRoot, file).Replace('\\', '/');
|
||||
string relativePath = Path.GetRelativePath(repoRoot, file).Replace('\\', '/');
|
||||
violations.Add($"{relativePath}:{i + 1} -> {line}");
|
||||
}
|
||||
}
|
||||
|
||||
violations.ShouldBeEmpty($"Unexpected non-generic ILogger usage:{Environment.NewLine}{string.Join(Environment.NewLine, violations)}");
|
||||
violations.ShouldBeEmpty(
|
||||
$"Unexpected non-generic ILogger usage:{Environment.NewLine}{string.Join(Environment.NewLine, violations)}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies log boundaries push operation context for hosted/background entry points.
|
||||
/// Verifies log boundaries push operation context for hosted/background entry points.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BoundaryServices_ShouldPushOperationLogContext()
|
||||
{
|
||||
var repoRoot = FindRepoRoot();
|
||||
string repoRoot = FindRepoRoot();
|
||||
var boundaryFiles = new[]
|
||||
{
|
||||
"src/ZB.MOM.WW.CBDDC.Network/CBDDCNodeService.cs",
|
||||
@@ -180,24 +173,24 @@ public class ArchitectureFitnessTests
|
||||
"src/ZB.MOM.WW.CBDDC.Hosting/Services/NoOpSyncOrchestrator.cs"
|
||||
};
|
||||
|
||||
foreach (var relativePath in boundaryFiles)
|
||||
foreach (string relativePath in boundaryFiles)
|
||||
{
|
||||
var filePath = Path.Combine(repoRoot, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
||||
string filePath = Path.Combine(repoRoot, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
||||
File.Exists(filePath).ShouldBeTrue($"Missing expected boundary file: {relativePath}");
|
||||
|
||||
var contents = File.ReadAllText(filePath);
|
||||
string contents = File.ReadAllText(filePath);
|
||||
contents.Contains("LogContext.PushProperty(\"OperationId\"", StringComparison.Ordinal)
|
||||
.ShouldBeTrue($"Boundary file is missing OperationId log enrichment: {relativePath}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies boundary projects include Serilog for LogContext support.
|
||||
/// Verifies boundary projects include Serilog for LogContext support.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BoundaryProjects_ShouldReferenceSerilog()
|
||||
{
|
||||
var repoRoot = FindRepoRoot();
|
||||
string repoRoot = FindRepoRoot();
|
||||
var projects = new[]
|
||||
{
|
||||
"src/ZB.MOM.WW.CBDDC.Network/ZB.MOM.WW.CBDDC.Network.csproj",
|
||||
@@ -205,12 +198,12 @@ public class ArchitectureFitnessTests
|
||||
"samples/ZB.MOM.WW.CBDDC.Sample.Console/ZB.MOM.WW.CBDDC.Sample.Console.csproj"
|
||||
};
|
||||
|
||||
foreach (var relativePath in projects)
|
||||
foreach (string relativePath in projects)
|
||||
{
|
||||
var filePath = Path.Combine(repoRoot, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
||||
string filePath = Path.Combine(repoRoot, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
||||
File.Exists(filePath).ShouldBeTrue($"Missing project file: {relativePath}");
|
||||
|
||||
var contents = File.ReadAllText(filePath);
|
||||
string contents = File.ReadAllText(filePath);
|
||||
contents.Contains("<PackageReference Include=\"Serilog\"", StringComparison.Ordinal)
|
||||
.ShouldBeTrue($"Serilog package reference is required for logging boundary enrichment: {relativePath}");
|
||||
}
|
||||
@@ -218,13 +211,10 @@ public class ArchitectureFitnessTests
|
||||
|
||||
private static string FindRepoRoot()
|
||||
{
|
||||
var dir = AppContext.BaseDirectory;
|
||||
string dir = AppContext.BaseDirectory;
|
||||
for (var i = 0; i < 10 && !string.IsNullOrWhiteSpace(dir); i++)
|
||||
{
|
||||
if (File.Exists(Path.Combine(dir, "CBDDC.slnx")))
|
||||
{
|
||||
return dir;
|
||||
}
|
||||
if (File.Exists(Path.Combine(dir, "CBDDC.slnx"))) return dir;
|
||||
|
||||
dir = Directory.GetParent(dir)?.FullName ?? string.Empty;
|
||||
}
|
||||
@@ -239,24 +229,14 @@ public class ArchitectureFitnessTests
|
||||
|
||||
bool Dfs(string node)
|
||||
{
|
||||
if (visiting.Contains(node))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (visiting.Contains(node)) return true;
|
||||
|
||||
if (!visited.Add(node))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!visited.Add(node)) return false;
|
||||
|
||||
visiting.Add(node);
|
||||
foreach (var next in graph[node])
|
||||
{
|
||||
foreach (string next in graph[node])
|
||||
if (Dfs(next))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
visiting.Remove(node);
|
||||
return false;
|
||||
@@ -264,4 +244,4 @@ public class ArchitectureFitnessTests
|
||||
|
||||
return graph.Keys.Any(Dfs);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
global using NSubstitute;
|
||||
global using Shouldly;
|
||||
global using Shouldly;
|
||||
@@ -1,77 +1,73 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.CBDDC.Core;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ZB.MOM.WW.CBDDC.Core.Tests
|
||||
namespace ZB.MOM.WW.CBDDC.Core.Tests;
|
||||
|
||||
public class OplogEntryTests
|
||||
{
|
||||
public class OplogEntryTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that hash computation is deterministic even when payload content differs.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ComputeHash_ShouldBeDeterministic_RegardlessOfPayload()
|
||||
{
|
||||
// Arrange
|
||||
var collection = "test-collection";
|
||||
var key = "test-key";
|
||||
var op = OperationType.Put;
|
||||
var timestamp = new HlcTimestamp(100, 0, "node-1");
|
||||
var prevHash = "prev-hash";
|
||||
/// <summary>
|
||||
/// Verifies that hash computation is deterministic even when payload content differs.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ComputeHash_ShouldBeDeterministic_RegardlessOfPayload()
|
||||
{
|
||||
// Arrange
|
||||
var collection = "test-collection";
|
||||
var key = "test-key";
|
||||
var op = OperationType.Put;
|
||||
var timestamp = new HlcTimestamp(100, 0, "node-1");
|
||||
var prevHash = "prev-hash";
|
||||
|
||||
var payload1 = JsonDocument.Parse("{\"prop\": 1}").RootElement;
|
||||
var payload2 = JsonDocument.Parse("{\"prop\": 2, \"extra\": \"whitespace\"}").RootElement;
|
||||
var payload1 = JsonDocument.Parse("{\"prop\": 1}").RootElement;
|
||||
var payload2 = JsonDocument.Parse("{\"prop\": 2, \"extra\": \"whitespace\"}").RootElement;
|
||||
|
||||
// Act
|
||||
var entry1 = new OplogEntry(collection, key, op, payload1, timestamp, prevHash);
|
||||
var entry2 = new OplogEntry(collection, key, op, payload2, timestamp, prevHash);
|
||||
|
||||
// Assert
|
||||
entry2.Hash.ShouldBe(entry1.Hash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that hash computation uses invariant culture formatting for timestamp values.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ComputeHash_ShouldUseInvariantCulture_ForTimestamp()
|
||||
{
|
||||
// Arrange
|
||||
var originalCulture = CultureInfo.CurrentCulture;
|
||||
try
|
||||
{
|
||||
var culture = CultureInfo.GetCultureInfo("de-DE");
|
||||
CultureInfo.CurrentCulture = culture;
|
||||
|
||||
var timestamp = new HlcTimestamp(123456789, 1, "node");
|
||||
var entry = new OplogEntry("col", "key", OperationType.Put, null, timestamp, "prev");
|
||||
|
||||
// Act
|
||||
var entry1 = new OplogEntry(collection, key, op, payload1, timestamp, prevHash);
|
||||
var entry2 = new OplogEntry(collection, key, op, payload2, timestamp, prevHash);
|
||||
string hash = entry.ComputeHash();
|
||||
|
||||
// Assert
|
||||
entry2.Hash.ShouldBe(entry1.Hash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that hash computation uses invariant culture formatting for timestamp values.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ComputeHash_ShouldUseInvariantCulture_ForTimestamp()
|
||||
{
|
||||
// Arrange
|
||||
var originalCulture = CultureInfo.CurrentCulture;
|
||||
try
|
||||
{
|
||||
var culture = CultureInfo.GetCultureInfo("de-DE");
|
||||
CultureInfo.CurrentCulture = culture;
|
||||
|
||||
var timestamp = new HlcTimestamp(123456789, 1, "node");
|
||||
var entry = new OplogEntry("col", "key", OperationType.Put, null, timestamp, "prev");
|
||||
|
||||
// Act
|
||||
var hash = entry.ComputeHash();
|
||||
|
||||
// Assert
|
||||
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
|
||||
var expectedEntry = new OplogEntry("col", "key", OperationType.Put, null, timestamp, "prev");
|
||||
hash.ShouldBe(expectedEntry.Hash);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CultureInfo.CurrentCulture = originalCulture;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that an entry is valid when its stored hash matches computed content.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IsValid_ShouldReturnTrue_WhenHashMatches()
|
||||
{
|
||||
var timestamp = new HlcTimestamp(100, 0, "node-1");
|
||||
var entry = new OplogEntry("col", "key", OperationType.Put, null, timestamp, "prev");
|
||||
|
||||
entry.IsValid().ShouldBeTrue();
|
||||
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
|
||||
var expectedEntry = new OplogEntry("col", "key", OperationType.Put, null, timestamp, "prev");
|
||||
hash.ShouldBe(expectedEntry.Hash);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CultureInfo.CurrentCulture = originalCulture;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that an entry is valid when its stored hash matches computed content.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IsValid_ShouldReturnTrue_WhenHashMatches()
|
||||
{
|
||||
var timestamp = new HlcTimestamp(100, 0, "node-1");
|
||||
var entry = new OplogEntry("col", "key", OperationType.Put, null, timestamp, "prev");
|
||||
|
||||
entry.IsValid().ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,8 @@ namespace ZB.MOM.WW.CBDDC.Core.Tests;
|
||||
public class PeerManagementServiceTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that removing peer tracking with remote removal enabled removes both tracking and remote peer configuration.
|
||||
/// Verifies that removing peer tracking with remote removal enabled removes both tracking and remote peer
|
||||
/// configuration.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RemovePeerTrackingAsync_WhenRemoveRemoteConfigTrue_RemovesTrackingAndRemoteConfig()
|
||||
@@ -16,14 +17,14 @@ public class PeerManagementServiceTests
|
||||
var service = new PeerManagementService(configStore, confirmationStore);
|
||||
var token = new CancellationTokenSource().Token;
|
||||
|
||||
await service.RemovePeerTrackingAsync("peer-1", removeRemoteConfig: true, token);
|
||||
await service.RemovePeerTrackingAsync("peer-1", true, token);
|
||||
|
||||
await confirmationStore.Received(1).RemovePeerTrackingAsync("peer-1", token);
|
||||
await configStore.Received(1).RemoveRemotePeerAsync("peer-1", token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that removing peer tracking with remote removal disabled removes only tracking data.
|
||||
/// Verifies that removing peer tracking with remote removal disabled removes only tracking data.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RemovePeerTrackingAsync_WhenRemoveRemoteConfigFalse_RemovesTrackingOnly()
|
||||
@@ -32,14 +33,14 @@ public class PeerManagementServiceTests
|
||||
var confirmationStore = Substitute.For<IPeerOplogConfirmationStore>();
|
||||
var service = new PeerManagementService(configStore, confirmationStore);
|
||||
|
||||
await service.RemovePeerTrackingAsync("peer-1", removeRemoteConfig: false);
|
||||
await service.RemovePeerTrackingAsync("peer-1", false);
|
||||
|
||||
await confirmationStore.Received(1).RemovePeerTrackingAsync("peer-1", Arg.Any<CancellationToken>());
|
||||
await configStore.DidNotReceive().RemoveRemotePeerAsync(Arg.Any<string>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that removing a remote peer delegates to tracking removal with remote configuration cleanup enabled.
|
||||
/// Verifies that removing a remote peer delegates to tracking removal with remote configuration cleanup enabled.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RemoveRemotePeerAsync_DelegatesToTrackingRemovalWithRemoteConfig()
|
||||
@@ -56,7 +57,7 @@ public class PeerManagementServiceTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that removing peer tracking with an invalid node identifier throws an <see cref="ArgumentException"/>.
|
||||
/// Verifies that removing peer tracking with an invalid node identifier throws an <see cref="ArgumentException" />.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RemovePeerTrackingAsync_WhenNodeIdInvalid_ThrowsArgumentException()
|
||||
@@ -65,9 +66,10 @@ public class PeerManagementServiceTests
|
||||
var confirmationStore = Substitute.For<IPeerOplogConfirmationStore>();
|
||||
var service = new PeerManagementService(configStore, confirmationStore);
|
||||
|
||||
await Should.ThrowAsync<ArgumentException>(() => service.RemovePeerTrackingAsync(" ", removeRemoteConfig: true));
|
||||
await Should.ThrowAsync<ArgumentException>(() => service.RemovePeerTrackingAsync(" ", true));
|
||||
|
||||
await confirmationStore.DidNotReceive().RemovePeerTrackingAsync(Arg.Any<string>(), Arg.Any<CancellationToken>());
|
||||
await confirmationStore.DidNotReceive()
|
||||
.RemovePeerTrackingAsync(Arg.Any<string>(), Arg.Any<CancellationToken>());
|
||||
await configStore.DidNotReceive().RemoveRemotePeerAsync(Arg.Any<string>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,43 +6,43 @@ namespace ZB.MOM.WW.CBDDC.Core.Tests;
|
||||
|
||||
public class PerformanceRegressionTests
|
||||
{
|
||||
private readonly RecursiveNodeMergeConflictResolver _resolver;
|
||||
private readonly Dictionary<string, int> _limits;
|
||||
private readonly RecursiveNodeMergeConflictResolver _resolver;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PerformanceRegressionTests"/> class.
|
||||
/// Initializes a new instance of the <see cref="PerformanceRegressionTests" /> class.
|
||||
/// </summary>
|
||||
public PerformanceRegressionTests()
|
||||
{
|
||||
_resolver = new RecursiveNodeMergeConflictResolver();
|
||||
|
||||
// Load limits
|
||||
var json = File.ReadAllText("benchmark_limits.json");
|
||||
string json = File.ReadAllText("benchmark_limits.json");
|
||||
_limits = JsonSerializer.Deserialize<Dictionary<string, int>>(json) ?? new Dictionary<string, int>();
|
||||
}
|
||||
|
||||
private Document CreateDoc(string key, object data, HlcTimestamp ts)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(data);
|
||||
string json = JsonSerializer.Serialize(data);
|
||||
var element = JsonDocument.Parse(json).RootElement;
|
||||
return new Document("test", key, element, ts, false);
|
||||
}
|
||||
|
||||
private OplogEntry CreateOp(string key, object data, HlcTimestamp ts)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(data);
|
||||
string json = JsonSerializer.Serialize(data);
|
||||
var element = JsonDocument.Parse(json).RootElement;
|
||||
return new OplogEntry("test", key, OperationType.Put, element, ts, string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies simple recursive merge operations stay within configured performance limits.
|
||||
/// Verifies simple recursive merge operations stay within configured performance limits.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RecursiveMerge_Simple_ShouldBeWithinLimits()
|
||||
{
|
||||
int iterations = 10000;
|
||||
string limitKey = "RecursiveMerge_Simple_10k_Ops_MaxMs";
|
||||
var iterations = 10000;
|
||||
var limitKey = "RecursiveMerge_Simple_10k_Ops_MaxMs";
|
||||
|
||||
var ts1 = new HlcTimestamp(100, 0, "n1");
|
||||
var ts2 = new HlcTimestamp(200, 0, "n2");
|
||||
@@ -50,72 +50,61 @@ public class PerformanceRegressionTests
|
||||
var op = CreateOp("k1", new { name = "Bob", age = 31 }, ts2);
|
||||
|
||||
// Warmup
|
||||
for (int i = 0; i < 100; i++) _resolver.Resolve(doc, op);
|
||||
for (var i = 0; i < 100; i++) _resolver.Resolve(doc, op);
|
||||
|
||||
// Run
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (int i = 0; i < iterations; i++)
|
||||
{
|
||||
_resolver.Resolve(doc, op);
|
||||
}
|
||||
for (var i = 0; i < iterations; i++) _resolver.Resolve(doc, op);
|
||||
sw.Stop();
|
||||
|
||||
long elapsed = sw.ElapsedMilliseconds;
|
||||
Console.WriteLine($"Executed {iterations} merges in {elapsed}ms");
|
||||
|
||||
if (_limits.TryGetValue(limitKey, out int maxMs))
|
||||
{
|
||||
elapsed.ShouldBeLessThan(maxMs, $"Performance regression! Expected < {maxMs}ms but took {elapsed}ms");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"Warning: No limit found for key '{limitKey}'");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies deep array recursive merge operations stay within configured performance limits.
|
||||
/// Verifies deep array recursive merge operations stay within configured performance limits.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RecursiveMerge_DeepArray_ShouldBeWithinLimits()
|
||||
{
|
||||
int iterations = 1000; // Lower iterations for heavier op
|
||||
string limitKey = "RecursiveMerge_Array_1k_Ops_MaxMs";
|
||||
var iterations = 1000; // Lower iterations for heavier op
|
||||
var limitKey = "RecursiveMerge_Array_1k_Ops_MaxMs";
|
||||
|
||||
var ts1 = new HlcTimestamp(100, 0, "n1");
|
||||
var ts2 = new HlcTimestamp(200, 0, "n2");
|
||||
|
||||
var items = new List<object>();
|
||||
for (int i = 0; i < 100; i++) items.Add(new { id = i.ToString(), val = i });
|
||||
for (var i = 0; i < 100; i++) items.Add(new { id = i.ToString(), val = i });
|
||||
|
||||
var doc = CreateDoc("k1", new { items = items }, ts1);
|
||||
var op = CreateDoc("k1", new { items = items }, ts2).ToOplogEntry(OperationType.Put); // Same content to force id check traversal
|
||||
var doc = CreateDoc("k1", new { items }, ts1);
|
||||
var op = CreateDoc("k1", new { items }, ts2)
|
||||
.ToOplogEntry(OperationType.Put); // Same content to force id check traversal
|
||||
|
||||
// Warmup
|
||||
_resolver.Resolve(doc, op);
|
||||
|
||||
// Run
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (int i = 0; i < iterations; i++)
|
||||
{
|
||||
_resolver.Resolve(doc, op);
|
||||
}
|
||||
for (var i = 0; i < iterations; i++) _resolver.Resolve(doc, op);
|
||||
sw.Stop();
|
||||
|
||||
long elapsed = sw.ElapsedMilliseconds;
|
||||
Console.WriteLine($"Executed {iterations} array merges in {elapsed}ms");
|
||||
|
||||
if (_limits.TryGetValue(limitKey, out int maxMs))
|
||||
{
|
||||
elapsed.ShouldBeLessThan(maxMs, $"Performance regression! Expected < {maxMs}ms but took {elapsed}ms");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class DocExt
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an operation log entry from a document instance.
|
||||
/// Creates an operation log entry from a document instance.
|
||||
/// </summary>
|
||||
/// <param name="d">The source document.</param>
|
||||
/// <param name="t">The operation type to apply to the created entry.</param>
|
||||
@@ -124,4 +113,4 @@ public static class DocExt
|
||||
{
|
||||
return new OplogEntry(d.Collection, d.Key, t, d.Content, d.UpdatedAt, string.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ public class RecursiveNodeMergeConflictResolverTests
|
||||
private readonly RecursiveNodeMergeConflictResolver _resolver;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RecursiveNodeMergeConflictResolverTests"/> class.
|
||||
/// Initializes a new instance of the <see cref="RecursiveNodeMergeConflictResolverTests" /> class.
|
||||
/// </summary>
|
||||
public RecursiveNodeMergeConflictResolverTests()
|
||||
{
|
||||
@@ -17,20 +17,20 @@ public class RecursiveNodeMergeConflictResolverTests
|
||||
|
||||
private Document CreateDoc(string key, object data, HlcTimestamp ts)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(data);
|
||||
string json = JsonSerializer.Serialize(data);
|
||||
var element = JsonDocument.Parse(json).RootElement;
|
||||
return new Document("test", key, element, ts, false);
|
||||
}
|
||||
|
||||
private OplogEntry CreateOp(string key, object data, HlcTimestamp ts)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(data);
|
||||
string json = JsonSerializer.Serialize(data);
|
||||
var element = JsonDocument.Parse(json).RootElement;
|
||||
return new OplogEntry("test", key, OperationType.Put, element, ts, string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that disjoint fields are merged into a single document.
|
||||
/// Verifies that disjoint fields are merged into a single document.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Resolve_ShouldMergeDisjointFields()
|
||||
@@ -56,7 +56,7 @@ public class RecursiveNodeMergeConflictResolverTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that primitive collisions are resolved using the higher timestamp value.
|
||||
/// Verifies that primitive collisions are resolved using the higher timestamp value.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Resolve_ShouldPrioritizeHigherTimestamp_PrimitiveCollision()
|
||||
@@ -81,7 +81,7 @@ public class RecursiveNodeMergeConflictResolverTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that nested object content is merged recursively.
|
||||
/// Verifies that nested object content is merged recursively.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Resolve_ShouldRecursivelyMergeObjects()
|
||||
@@ -104,7 +104,7 @@ public class RecursiveNodeMergeConflictResolverTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that arrays containing object identifiers are merged by item identity.
|
||||
/// Verifies that arrays containing object identifiers are merged by item identity.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Resolve_ShouldMergeArraysById()
|
||||
@@ -115,7 +115,8 @@ public class RecursiveNodeMergeConflictResolverTests
|
||||
|
||||
var doc = CreateDoc("k1", new
|
||||
{
|
||||
items = new[] {
|
||||
items = new[]
|
||||
{
|
||||
new { id = "1", val = "A" },
|
||||
new { id = "2", val = "B" }
|
||||
}
|
||||
@@ -123,9 +124,10 @@ public class RecursiveNodeMergeConflictResolverTests
|
||||
|
||||
var op = CreateOp("k1", new
|
||||
{
|
||||
items = new[] {
|
||||
items = new[]
|
||||
{
|
||||
new { id = "1", val = "A-Updated" }, // Update
|
||||
new { id = "3", val = "C" } // Insert
|
||||
new { id = "3", val = "C" } // Insert
|
||||
}
|
||||
}, ts2);
|
||||
|
||||
@@ -133,14 +135,14 @@ public class RecursiveNodeMergeConflictResolverTests
|
||||
var result = _resolver.Resolve(doc, op);
|
||||
|
||||
// Assert
|
||||
Action<JsonElement> validate = (root) =>
|
||||
Action<JsonElement> validate = root =>
|
||||
{
|
||||
var items = root.GetProperty("items");
|
||||
items.GetArrayLength().ShouldBe(3);
|
||||
|
||||
// Order is not guaranteed, so find by id
|
||||
// But simplified test checking content exists
|
||||
var text = items.GetRawText();
|
||||
string text = items.GetRawText();
|
||||
text.ShouldContain("A-Updated");
|
||||
text.ShouldContain("B");
|
||||
text.ShouldContain("C");
|
||||
@@ -150,7 +152,7 @@ public class RecursiveNodeMergeConflictResolverTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that primitive arrays fall back to last-write-wins behavior.
|
||||
/// Verifies that primitive arrays fall back to last-write-wins behavior.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Resolve_ShouldFallbackToLWW_ForPrimitiveArrays()
|
||||
@@ -170,4 +172,4 @@ public class RecursiveNodeMergeConflictResolverTests
|
||||
tags.GetArrayLength().ShouldBe(1);
|
||||
tags[0].GetString().ShouldBe("c");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,13 @@
|
||||
using ZB.MOM.WW.CBDDC.Core;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace ZB.MOM.WW.CBDDC.Core.Tests;
|
||||
|
||||
public class VectorClockTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies an empty vector clock returns the default timestamp for unknown nodes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EmptyVectorClock_ShouldReturnDefaultTimestamp()
|
||||
{
|
||||
public class VectorClockTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies an empty vector clock returns the default timestamp for unknown nodes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EmptyVectorClock_ShouldReturnDefaultTimestamp()
|
||||
{
|
||||
// Arrange
|
||||
var vc = new VectorClock();
|
||||
|
||||
@@ -19,15 +15,15 @@ public class VectorClockTests
|
||||
var ts = vc.GetTimestamp("node1");
|
||||
|
||||
// Assert
|
||||
ts.ShouldBe(default(HlcTimestamp));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies setting a timestamp stores it for the specified node.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SetTimestamp_ShouldStoreTimestamp()
|
||||
{
|
||||
ts.ShouldBe(default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies setting a timestamp stores it for the specified node.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SetTimestamp_ShouldStoreTimestamp()
|
||||
{
|
||||
// Arrange
|
||||
var vc = new VectorClock();
|
||||
var ts = new HlcTimestamp(100, 1, "node1");
|
||||
@@ -36,15 +32,15 @@ public class VectorClockTests
|
||||
vc.SetTimestamp("node1", ts);
|
||||
|
||||
// Assert
|
||||
vc.GetTimestamp("node1").ShouldBe(ts);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies node identifiers are returned for all known nodes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void NodeIds_ShouldReturnAllNodes()
|
||||
{
|
||||
vc.GetTimestamp("node1").ShouldBe(ts);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies node identifiers are returned for all known nodes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void NodeIds_ShouldReturnAllNodes()
|
||||
{
|
||||
// Arrange
|
||||
var vc = new VectorClock();
|
||||
vc.SetTimestamp("node1", new HlcTimestamp(100, 1, "node1"));
|
||||
@@ -56,15 +52,15 @@ public class VectorClockTests
|
||||
// Assert
|
||||
nodeIds.Count.ShouldBe(2);
|
||||
nodeIds.ShouldContain("node1");
|
||||
nodeIds.ShouldContain("node2");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies equal vector clocks are compared as equal.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CompareTo_EqualClocks_ShouldReturnEqual()
|
||||
{
|
||||
nodeIds.ShouldContain("node2");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies equal vector clocks are compared as equal.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CompareTo_EqualClocks_ShouldReturnEqual()
|
||||
{
|
||||
// Arrange
|
||||
var vc1 = new VectorClock();
|
||||
vc1.SetTimestamp("node1", new HlcTimestamp(100, 1, "node1"));
|
||||
@@ -78,15 +74,15 @@ public class VectorClockTests
|
||||
var result = vc1.CompareTo(vc2);
|
||||
|
||||
// Assert
|
||||
result.ShouldBe(CausalityRelation.Equal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a clock strictly ahead of another is reported as strictly ahead.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CompareTo_StrictlyAhead_ShouldReturnStrictlyAhead()
|
||||
{
|
||||
result.ShouldBe(CausalityRelation.Equal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a clock strictly ahead of another is reported as strictly ahead.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CompareTo_StrictlyAhead_ShouldReturnStrictlyAhead()
|
||||
{
|
||||
// Arrange
|
||||
var vc1 = new VectorClock();
|
||||
vc1.SetTimestamp("node1", new HlcTimestamp(200, 1, "node1")); // Ahead
|
||||
@@ -100,15 +96,15 @@ public class VectorClockTests
|
||||
var result = vc1.CompareTo(vc2);
|
||||
|
||||
// Assert
|
||||
result.ShouldBe(CausalityRelation.StrictlyAhead);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a clock strictly behind another is reported as strictly behind.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CompareTo_StrictlyBehind_ShouldReturnStrictlyBehind()
|
||||
{
|
||||
result.ShouldBe(CausalityRelation.StrictlyAhead);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a clock strictly behind another is reported as strictly behind.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CompareTo_StrictlyBehind_ShouldReturnStrictlyBehind()
|
||||
{
|
||||
// Arrange
|
||||
var vc1 = new VectorClock();
|
||||
vc1.SetTimestamp("node1", new HlcTimestamp(100, 1, "node1")); // Behind
|
||||
@@ -122,15 +118,15 @@ public class VectorClockTests
|
||||
var result = vc1.CompareTo(vc2);
|
||||
|
||||
// Assert
|
||||
result.ShouldBe(CausalityRelation.StrictlyBehind);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies divergent per-node progress is reported as concurrent.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CompareTo_Concurrent_ShouldReturnConcurrent()
|
||||
{
|
||||
result.ShouldBe(CausalityRelation.StrictlyBehind);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies divergent per-node progress is reported as concurrent.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CompareTo_Concurrent_ShouldReturnConcurrent()
|
||||
{
|
||||
// Arrange - Split brain scenario
|
||||
var vc1 = new VectorClock();
|
||||
vc1.SetTimestamp("node1", new HlcTimestamp(200, 1, "node1")); // Node1 ahead
|
||||
@@ -144,15 +140,15 @@ public class VectorClockTests
|
||||
var result = vc1.CompareTo(vc2);
|
||||
|
||||
// Assert
|
||||
result.ShouldBe(CausalityRelation.Concurrent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies pull candidates include nodes where the other clock is ahead.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GetNodesWithUpdates_ShouldReturnNodesWhereOtherIsAhead()
|
||||
{
|
||||
result.ShouldBe(CausalityRelation.Concurrent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies pull candidates include nodes where the other clock is ahead.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GetNodesWithUpdates_ShouldReturnNodesWhereOtherIsAhead()
|
||||
{
|
||||
// Arrange
|
||||
var vc1 = new VectorClock();
|
||||
vc1.SetTimestamp("node1", new HlcTimestamp(100, 1, "node1"));
|
||||
@@ -167,15 +163,15 @@ public class VectorClockTests
|
||||
|
||||
// Assert
|
||||
nodesToPull.Count().ShouldBe(1);
|
||||
nodesToPull.ShouldContain("node1");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies push candidates include nodes where this clock is ahead.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GetNodesToPush_ShouldReturnNodesWhereThisIsAhead()
|
||||
{
|
||||
nodesToPull.ShouldContain("node1");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies push candidates include nodes where this clock is ahead.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GetNodesToPush_ShouldReturnNodesWhereThisIsAhead()
|
||||
{
|
||||
// Arrange
|
||||
var vc1 = new VectorClock();
|
||||
vc1.SetTimestamp("node1", new HlcTimestamp(200, 1, "node1")); // Ahead
|
||||
@@ -190,15 +186,15 @@ public class VectorClockTests
|
||||
|
||||
// Assert
|
||||
nodesToPush.Count().ShouldBe(1);
|
||||
nodesToPush.ShouldContain("node1");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a newly introduced remote node is included in pull candidates.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GetNodesWithUpdates_WhenNewNodeAppearsInOther_ShouldReturnIt()
|
||||
{
|
||||
nodesToPush.ShouldContain("node1");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a newly introduced remote node is included in pull candidates.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GetNodesWithUpdates_WhenNewNodeAppearsInOther_ShouldReturnIt()
|
||||
{
|
||||
// Arrange - Simulates a new node joining the cluster
|
||||
var vc1 = new VectorClock();
|
||||
vc1.SetTimestamp("node1", new HlcTimestamp(100, 1, "node1"));
|
||||
@@ -212,15 +208,15 @@ public class VectorClockTests
|
||||
|
||||
// Assert
|
||||
nodesToPull.Count().ShouldBe(1);
|
||||
nodesToPull.ShouldContain("node3");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies merge keeps the maximum timestamp per node.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Merge_ShouldTakeMaximumForEachNode()
|
||||
{
|
||||
nodesToPull.ShouldContain("node3");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies merge keeps the maximum timestamp per node.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Merge_ShouldTakeMaximumForEachNode()
|
||||
{
|
||||
// Arrange
|
||||
var vc1 = new VectorClock();
|
||||
vc1.SetTimestamp("node1", new HlcTimestamp(200, 1, "node1"));
|
||||
@@ -234,18 +230,18 @@ public class VectorClockTests
|
||||
// Act
|
||||
vc1.Merge(vc2);
|
||||
|
||||
// Assert
|
||||
vc1.GetTimestamp("node1").ShouldBe(new HlcTimestamp(200, 1, "node1")); // Kept max
|
||||
vc1.GetTimestamp("node2").ShouldBe(new HlcTimestamp(200, 2, "node2")); // Merged max
|
||||
vc1.GetTimestamp("node3").ShouldBe(new HlcTimestamp(150, 1, "node3")); // Added new
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies cloning creates an independent copy of the vector clock.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Clone_ShouldCreateIndependentCopy()
|
||||
{
|
||||
// Assert
|
||||
vc1.GetTimestamp("node1").ShouldBe(new HlcTimestamp(200, 1, "node1")); // Kept max
|
||||
vc1.GetTimestamp("node2").ShouldBe(new HlcTimestamp(200, 2, "node2")); // Merged max
|
||||
vc1.GetTimestamp("node3").ShouldBe(new HlcTimestamp(150, 1, "node3")); // Added new
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies cloning creates an independent copy of the vector clock.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Clone_ShouldCreateIndependentCopy()
|
||||
{
|
||||
// Arrange
|
||||
var vc1 = new VectorClock();
|
||||
vc1.SetTimestamp("node1", new HlcTimestamp(100, 1, "node1"));
|
||||
@@ -256,15 +252,15 @@ public class VectorClockTests
|
||||
|
||||
// Assert
|
||||
vc1.NodeIds.Count().ShouldBe(1);
|
||||
vc2.NodeIds.Count().ShouldBe(2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the string representation includes serialized node timestamps.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ToString_ShouldReturnReadableFormat()
|
||||
{
|
||||
vc2.NodeIds.Count().ShouldBe(2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the string representation includes serialized node timestamps.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ToString_ShouldReturnReadableFormat()
|
||||
{
|
||||
// Arrange
|
||||
var vc = new VectorClock();
|
||||
vc.SetTimestamp("node1", new HlcTimestamp(100, 1, "node1"));
|
||||
@@ -275,15 +271,15 @@ public class VectorClockTests
|
||||
|
||||
// Assert
|
||||
str.ShouldContain("node1:100:1:node1");
|
||||
str.ShouldContain("node2:200:2:node2");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies split-brain updates are detected as concurrent.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SplitBrainScenario_ShouldDetectConcurrency()
|
||||
{
|
||||
str.ShouldContain("node2:200:2:node2");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies split-brain updates are detected as concurrent.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SplitBrainScenario_ShouldDetectConcurrency()
|
||||
{
|
||||
// Arrange - Simulating a network partition scenario
|
||||
// Partition 1: node1 and node2 are alive
|
||||
var vcPartition1 = new VectorClock();
|
||||
@@ -310,4 +306,4 @@ public class VectorClockTests
|
||||
partition1NeedsToPush.ShouldContain("node1");
|
||||
partition1NeedsToPush.ShouldContain("node2");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,37 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyName>ZB.MOM.WW.CBDDC.Core.Tests</AssemblyName>
|
||||
<RootNamespace>ZB.MOM.WW.CBDDC.Core.Tests</RootNamespace>
|
||||
<PackageId>ZB.MOM.WW.CBDDC.Core.Tests</PackageId>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<NoWarn>$(NoWarn);xUnit1031;xUnit1051</NoWarn>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<AssemblyName>ZB.MOM.WW.CBDDC.Core.Tests</AssemblyName>
|
||||
<RootNamespace>ZB.MOM.WW.CBDDC.Core.Tests</RootNamespace>
|
||||
<PackageId>ZB.MOM.WW.CBDDC.Core.Tests</PackageId>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<NoWarn>$(NoWarn);xUnit1031;xUnit1051</NoWarn>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="Shouldly" Version="4.3.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
<PackageReference Include="xunit.v3" Version="3.2.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4"/>
|
||||
<PackageReference Include="NSubstitute" Version="5.3.0"/>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
|
||||
<PackageReference Include="Shouldly" Version="4.3.0"/>
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4"/>
|
||||
<PackageReference Include="xunit.v3" Version="3.2.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="benchmark_limits.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Update="benchmark_limits.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\ZB.MOM.WW.CBDDC.Core\ZB.MOM.WW.CBDDC.Core.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\ZB.MOM.WW.CBDDC.Core\ZB.MOM.WW.CBDDC.Core.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"RecursiveMerge_Simple_10k_Ops_MaxMs": 500,
|
||||
"RecursiveMerge_Array_1k_Ops_MaxMs": 1500
|
||||
"RecursiveMerge_Simple_10k_Ops_MaxMs": 500,
|
||||
"RecursiveMerge_Array_1k_Ops_MaxMs": 1500
|
||||
}
|
||||
Reference in New Issue
Block a user