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}"); } }