using Xunit;
namespace XFEExtension.NetCore.AutoConfig.Tests;
public class ConcurrentSaveTests
{
[Fact]
public void RuntimeSaveStateIsNotIncludedInJsonOrXmlProfiles()
{
var profile = new SerializationProfile { Name = "Test" };
var json = XFEProfile.JsonSaveProfileOperation(profile, [], []);
var xml = XFEProfile.XmlSaveProfileOperation(profile, [], []);
Assert.Contains("Name", json);
Assert.Contains("Name", xml);
Assert.DoesNotContain(nameof(XFEProfile.AutoSaveDelay), json);
Assert.DoesNotContain(nameof(XFEProfile.LastSaveException), json);
Assert.DoesNotContain(nameof(XFEProfile.AutoSaveDelay), xml);
Assert.DoesNotContain(nameof(XFEProfile.LastSaveException), xml);
}
[Fact]
public async Task ConcurrentChangesAreCoalescedAndPersistTheLatestSnapshot()
{
var profileDirectory = Path.Combine(Path.GetTempPath(), "XFEAutoConfigTests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(profileDirectory);
XFEProfile.ProfilesDefaultPath = profileDirectory;
var profilePath = Path.Combine(profileDirectory, $"{nameof(ConcurrentProfile)}.xpf");
ConcurrentProfile.ProfilePath = Path.Combine(profileDirectory, nameof(ConcurrentProfile));
try
{
ConcurrentProfile.Current.AutoSaveDelay = TimeSpan.FromMilliseconds(250);
ConcurrentProfile.ResetSaveCount();
var numbers = ConcurrentProfile.Numbers;
var lookup = ConcurrentProfile.Lookup;
Parallel.Invoke(
() => Parallel.For(0, 2_000, index => ConcurrentProfile.Value = index),
() => Parallel.For(0, 1_000, numbers.Add),
() => Parallel.For(0, 1_000, index => lookup.TryAdd(index, index)));
const int finalValue = 123_456;
ConcurrentProfile.Value = finalValue;
var expectedContent = $"{finalValue}|1000|1000";
await WaitForFileContentAsync(profilePath, expectedContent, TimeSpan.FromSeconds(10));
await Task.Delay(500);
Assert.Equal(1_000, numbers.Count);
Assert.Equal(1_000, lookup.Count);
Assert.Null(ConcurrentProfile.Current.LastSaveException);
Assert.InRange(ConcurrentProfile.SaveCount, 1, 99);
var saveCountBeforeManualSave = ConcurrentProfile.SaveCount;
ConcurrentProfile.SaveProfile();
Assert.Equal(saveCountBeforeManualSave + 1, ConcurrentProfile.SaveCount);
Assert.Equal(expectedContent, await File.ReadAllTextAsync(profilePath));
Assert.Empty(Directory.EnumerateFiles(profileDirectory, "*.tmp"));
}
finally
{
ConcurrentProfile.DeleteProfile();
if (Directory.Exists(profileDirectory))
Directory.Delete(profileDirectory, true);
}
}
[Fact]
public async Task SaveLoadAndDeleteAreSerializedForTheSamePath()
{
var profileDirectory = Path.Combine(Path.GetTempPath(), "XFEAutoConfigTests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(profileDirectory);
ConcurrentProfile.ProfilePath = Path.Combine(profileDirectory, nameof(ConcurrentProfile));
ConcurrentProfile.Current.AutoSaveDelay = TimeSpan.FromMilliseconds(20);
try
{
var saveTask = Task.Run(async () =>
{
for (var index = 0; index < 30; index++)
{
ConcurrentProfile.Value = index;
await ConcurrentProfile.SaveProfileAsync();
}
});
var loadTask = Task.Run(() =>
{
for (var index = 0; index < 30; index++)
ConcurrentProfile.LoadProfile();
});
var deleteTask = Task.Run(() =>
{
for (var index = 0; index < 30; index++)
ConcurrentProfile.DeleteProfile();
});
await Task.WhenAll(saveTask, loadTask, deleteTask);
ConcurrentProfile.Value = 42;
await ConcurrentProfile.SaveProfileAsync();
Assert.Null(ConcurrentProfile.Current.LastSaveException);
Assert.True(File.Exists(ConcurrentProfile.ProfilePath + ".xpf"));
Assert.Empty(Directory.EnumerateFiles(profileDirectory, "*.tmp"));
}
finally
{
ConcurrentProfile.DeleteProfile();
if (Directory.Exists(profileDirectory))
Directory.Delete(profileDirectory, true);
}
}
private static async Task WaitForFileContentAsync(string profilePath, string expectedContent, TimeSpan timeout)
{
var timeoutAt = DateTime.UtcNow + timeout;
while (DateTime.UtcNow < timeoutAt)
{
if (File.Exists(profilePath))
{
try
{
if (await File.ReadAllTextAsync(profilePath) == expectedContent)
return;
}
catch (IOException)
{
}
}
await Task.Delay(25);
}
throw new TimeoutException($"配置文件未在规定时间内写入预期内容:{profilePath}");
}
}
using Xunit;
namespace XFEExtension.NetCore.AutoConfig.Tests;
public class ConcurrentSaveTests
{
[Fact]
public void RuntimeSaveStateIsNotIncludedInJsonOrXmlProfiles()
{
var profile = new SerializationProfile { Name = "Test" };
var json = XFEProfile.JsonSaveProfileOperation(profile, [], []);
var xml = XFEProfile.XmlSaveProfileOperation(profile, [], []);
Assert.Contains("Name", json);
Assert.Contains("Name", xml);
Assert.DoesNotContain(nameof(XFEProfile.AutoSaveDelay), json);
Assert.DoesNotContain(nameof(XFEProfile.LastSaveException), json);
Assert.DoesNotContain(nameof(XFEProfile.AutoSaveDelay), xml);
Assert.DoesNotContain(nameof(XFEProfile.LastSaveException), xml);
}
[Fact]
public async Task ConcurrentChangesAreCoalescedAndPersistTheLatestSnapshot()
{
var profileDirectory = Path.Combine(Path.GetTempPath(), "XFEAutoConfigTests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(profileDirectory);
XFEProfile.ProfilesDefaultPath = profileDirectory;
var profilePath = Path.Combine(profileDirectory, $"{nameof(ConcurrentProfile)}.xpf");
ConcurrentProfile.ProfilePath = Path.Combine(profileDirectory, nameof(ConcurrentProfile));
try
{
ConcurrentProfile.Current.AutoSaveDelay = TimeSpan.FromMilliseconds(250);
ConcurrentProfile.ResetSaveCount();
var numbers = ConcurrentProfile.Numbers;
var lookup = ConcurrentProfile.Lookup;
Parallel.Invoke(
() => Parallel.For(0, 2_000, index => ConcurrentProfile.Value = index),
() => Parallel.For(0, 1_000, numbers.Add),
() => Parallel.For(0, 1_000, index => lookup.TryAdd(index, index)));
const int finalValue = 123_456;
ConcurrentProfile.Value = finalValue;
var expectedContent = $"{finalValue}|1000|1000";
await WaitForFileContentAsync(profilePath, expectedContent, TimeSpan.FromSeconds(10));
await Task.Delay(500);
Assert.Equal(1_000, numbers.Count);
Assert.Equal(1_000, lookup.Count);
Assert.Null(ConcurrentProfile.Current.LastSaveException);
Assert.InRange(ConcurrentProfile.SaveCount, 1, 99);
var saveCountBeforeManualSave = ConcurrentProfile.SaveCount;
ConcurrentProfile.SaveProfile();
Assert.Equal(saveCountBeforeManualSave + 1, ConcurrentProfile.SaveCount);
Assert.Equal(expectedContent, await File.ReadAllTextAsync(profilePath));
Assert.Empty(Directory.EnumerateFiles(profileDirectory, "*.tmp"));
}
finally
{
ConcurrentProfile.DeleteProfile();
if (Directory.Exists(profileDirectory))
Directory.Delete(profileDirectory, true);
}
}
[Fact]
public async Task SaveLoadAndDeleteAreSerializedForTheSamePath()
{
var profileDirectory = Path.Combine(Path.GetTempPath(), "XFEAutoConfigTests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(profileDirectory);
ConcurrentProfile.ProfilePath = Path.Combine(profileDirectory, nameof(ConcurrentProfile));
ConcurrentProfile.Current.AutoSaveDelay = TimeSpan.FromMilliseconds(20);
try
{
var saveTask = Task.Run(async () =>
{
for (var index = 0; index < 30; index++)
{
ConcurrentProfile.Value = index;
await ConcurrentProfile.SaveProfileAsync();
}
});
var loadTask = Task.Run(() =>
{
for (var index = 0; index < 30; index++)
ConcurrentProfile.LoadProfile();
});
var deleteTask = Task.Run(() =>
{
for (var index = 0; index < 30; index++)
ConcurrentProfile.DeleteProfile();
});
await Task.WhenAll(saveTask, loadTask, deleteTask);
ConcurrentProfile.Value = 42;
await ConcurrentProfile.SaveProfileAsync();
Assert.Null(ConcurrentProfile.Current.LastSaveException);
Assert.True(File.Exists(ConcurrentProfile.ProfilePath + ".xpf"));
Assert.Empty(Directory.EnumerateFiles(profileDirectory, "*.tmp"));
}
finally
{
ConcurrentProfile.DeleteProfile();
if (Directory.Exists(profileDirectory))
Directory.Delete(profileDirectory, true);
}
}
private static async Task WaitForFileContentAsync(string profilePath, string expectedContent, TimeSpan timeout)
{
var timeoutAt = DateTime.UtcNow + timeout;
while (DateTime.UtcNow < timeoutAt)
{
if (File.Exists(profilePath))
{
try
{
if (await File.ReadAllTextAsync(profilePath) == expectedContent)
return;
}
catch (IOException)
{
}
}
await Task.Delay(25);
}
throw new TimeoutException($"配置文件未在规定时间内写入预期内容:{profilePath}");
}
}