diff --git a/src/ByteSync.Client/Assets/Resources/Resources.fr.resx b/src/ByteSync.Client/Assets/Resources/Resources.fr.resx index 1f53c5f1d..bb2186922 100644 --- a/src/ByteSync.Client/Assets/Resources/Resources.fr.resx +++ b/src/ByteSync.Client/Assets/Resources/Resources.fr.resx @@ -397,6 +397,9 @@ Voulez-vous continuer ? Erreurs d'identification : + + Entrées ignorées : + Démarrage : @@ -670,6 +673,9 @@ Voulez-vous continuer ? Total des erreurs de calcul : + + Total des entrées ignorées : + Total erreurs d'identification : diff --git a/src/ByteSync.Client/Assets/Resources/Resources.resx b/src/ByteSync.Client/Assets/Resources/Resources.resx index 395492cad..69dc16029 100644 --- a/src/ByteSync.Client/Assets/Resources/Resources.resx +++ b/src/ByteSync.Client/Assets/Resources/Resources.resx @@ -397,6 +397,9 @@ Would you like to continue ? Identification Errors: + + Skipped Entries: + Start: @@ -679,6 +682,9 @@ Would you like to continue ? Total Calculation Errors: + + Total Skipped Entries: + Total Identification Errors: diff --git a/src/ByteSync.Client/Business/Inventories/InventoryMonitorData.cs b/src/ByteSync.Client/Business/Inventories/InventoryMonitorData.cs index 7224fe229..88cdc0d9f 100644 --- a/src/ByteSync.Client/Business/Inventories/InventoryMonitorData.cs +++ b/src/ByteSync.Client/Business/Inventories/InventoryMonitorData.cs @@ -23,6 +23,8 @@ public record InventoryMonitorData public long UploadTotalVolume { get; set; } public long UploadedVolume { get; set; } + + public int SkippedEntriesCount { get; set; } public bool HasNonZeroProperty() { @@ -36,6 +38,7 @@ public bool HasNonZeroProperty() || AnalyzableVolume != 0 || IdentifiedVolume != 0 || UploadTotalVolume != 0 - || UploadedVolume != 0; + || UploadedVolume != 0 + || SkippedEntriesCount != 0; } } diff --git a/src/ByteSync.Client/Business/Inventories/InventoryProcessData.cs b/src/ByteSync.Client/Business/Inventories/InventoryProcessData.cs index db3317777..3180ad7ee 100644 --- a/src/ByteSync.Client/Business/Inventories/InventoryProcessData.cs +++ b/src/ByteSync.Client/Business/Inventories/InventoryProcessData.cs @@ -14,7 +14,6 @@ public class InventoryProcessData : ReactiveObject private readonly object _monitorDataLock = new object(); private readonly ConcurrentQueue _skippedEntries = new(); private readonly ConcurrentDictionary _skippedCountsByReason = new(); - private int _skippedCount; public InventoryProcessData() { @@ -72,11 +71,11 @@ public InventoryProcessData() Reset(); } - public List? InventoryBuilders { get; set; } + public List InventoryBuilders { get; set; } = []; - public List? Inventories + public List GetInventories() { - get { return InventoryBuilders?.Select(ib => ib.Inventory).ToList(); } + return InventoryBuilders.Select(ib => ib.Inventory).ToList(); } public CancellationTokenSource CancellationTokenSource { get; private set; } @@ -107,7 +106,7 @@ public List? Inventories public IReadOnlyCollection SkippedEntries => _skippedEntries.ToArray(); - public int SkippedCount => _skippedCount; + public int SkippedCount => _skippedCountsByReason.Values.Sum(); [Reactive] public DateTimeOffset InventoryStart { get; set; } @@ -146,7 +145,7 @@ public void RecordSkippedEntry(SkippedEntry entry) { _skippedEntries.Enqueue(entry); _skippedCountsByReason.AddOrUpdate(entry.Reason, 1, (_, currentCount) => currentCount + 1); - Interlocked.Increment(ref _skippedCount); + UpdateMonitorData(m => { m.SkippedEntriesCount += 1; }); } // should be used during issue 268 implementation @@ -175,11 +174,8 @@ public void UpdateMonitorData(Action action) private void ClearSkippedEntries() { - while (_skippedEntries.TryDequeue(out _)) - { - } + _skippedEntries.Clear(); _skippedCountsByReason.Clear(); - Interlocked.Exchange(ref _skippedCount, 0); } -} \ No newline at end of file +} diff --git a/src/ByteSync.Client/Business/Inventories/InventoryStatistics.cs b/src/ByteSync.Client/Business/Inventories/InventoryStatistics.cs index 673158891..dcc0a69d0 100644 --- a/src/ByteSync.Client/Business/Inventories/InventoryStatistics.cs +++ b/src/ByteSync.Client/Business/Inventories/InventoryStatistics.cs @@ -11,4 +11,6 @@ public record InventoryStatistics public int AnalyzeErrors { get; init; } public int IdentificationErrors { get; init; } + + public int TotalSkippedEntries { get; init; } } diff --git a/src/ByteSync.Client/Services/Inventories/BaseInventoryRunner.cs b/src/ByteSync.Client/Services/Inventories/BaseInventoryRunner.cs index e64d7826f..920708071 100644 --- a/src/ByteSync.Client/Services/Inventories/BaseInventoryRunner.cs +++ b/src/ByteSync.Client/Services/Inventories/BaseInventoryRunner.cs @@ -32,7 +32,7 @@ public async Task RunBaseInventory() bool isOK; try { - await Parallel.ForEachAsync(InventoryProcessData.InventoryBuilders!, + await Parallel.ForEachAsync(InventoryProcessData.InventoryBuilders, new ParallelOptions { MaxDegreeOfParallelism = 2, CancellationToken = InventoryProcessData.CancellationTokenSource.Token }, async (builder, token) => { @@ -46,7 +46,7 @@ await Parallel.ForEachAsync(InventoryProcessData.InventoryBuilders!, { InventoryProcessData.IdentificationStatus.OnNext(InventoryTaskStatus.Success); - await _inventoryFinishedService.SetLocalInventoryFinished(InventoryProcessData.Inventories!, LocalInventoryModes.Base); + await _inventoryFinishedService.SetLocalInventoryFinished(InventoryProcessData.GetInventories(), LocalInventoryModes.Base); } isOK = true; @@ -66,4 +66,4 @@ await Parallel.ForEachAsync(InventoryProcessData.InventoryBuilders!, return isOK; } -} \ No newline at end of file +} diff --git a/src/ByteSync.Client/Services/Inventories/FullInventoryRunner.cs b/src/ByteSync.Client/Services/Inventories/FullInventoryRunner.cs index 290f6bf1a..b71b94c9d 100644 --- a/src/ByteSync.Client/Services/Inventories/FullInventoryRunner.cs +++ b/src/ByteSync.Client/Services/Inventories/FullInventoryRunner.cs @@ -44,7 +44,7 @@ public async Task RunFullInventory() await _sessionMemberService.UpdateCurrentMemberGeneralStatus(SessionMemberGeneralStatus.InventoryRunningAnalysis); var inventoriesBuildersAndItems = new List>>(); - foreach (var inventoryBuilder in InventoryProcessData.InventoryBuilders!) + foreach (var inventoryBuilder in InventoryProcessData.InventoryBuilders) { using var inventoryComparer = _inventoryComparerFactory.CreateInventoryComparer(LocalInventoryModes.Base, inventoryBuilder.InventoryIndexer); @@ -87,7 +87,7 @@ await Parallel.ForEachAsync(inventoriesBuildersAndItems, else { InventoryProcessData.AnalysisStatus.OnNext(InventoryTaskStatus.Success); - await _inventoryFinishedService.SetLocalInventoryFinished(InventoryProcessData.Inventories!, LocalInventoryModes.Full); + await _inventoryFinishedService.SetLocalInventoryFinished(InventoryProcessData.GetInventories(), LocalInventoryModes.Full); InventoryProcessData.MainStatus.OnNext(InventoryTaskStatus.Success); } diff --git a/src/ByteSync.Client/Services/Inventories/InventoryStatisticsService.cs b/src/ByteSync.Client/Services/Inventories/InventoryStatisticsService.cs index bf6c58e46..87176ddc8 100644 --- a/src/ByteSync.Client/Services/Inventories/InventoryStatisticsService.cs +++ b/src/ByteSync.Client/Services/Inventories/InventoryStatisticsService.cs @@ -11,7 +11,6 @@ namespace ByteSync.Services.Inventories; public class InventoryStatisticsService : IInventoryStatisticsService { - private readonly IInventoryService _inventoryService; private readonly IInventoryFileRepository _inventoryFileRepository; private readonly ILogger _logger; @@ -20,13 +19,12 @@ public class InventoryStatisticsService : IInventoryStatisticsService public InventoryStatisticsService(IInventoryService inventoryService, IInventoryFileRepository inventoryFileRepository, ILogger logger) { - _inventoryService = inventoryService; _inventoryFileRepository = inventoryFileRepository; _logger = logger; _statisticsSubject = new BehaviorSubject(null); - _inventoryService.InventoryProcessData.AreFullInventoriesComplete + inventoryService.InventoryProcessData.AreFullInventoriesComplete .DistinctUntilChanged() .SelectMany(isComplete => isComplete @@ -66,7 +64,8 @@ private void DoCompute() ProcessedVolume = statsCollector.ProcessedSize, AnalyzeSuccess = statsCollector.Success, AnalyzeErrors = statsCollector.Errors, - IdentificationErrors = statsCollector.IdentificationErrors + IdentificationErrors = statsCollector.IdentificationErrors, + TotalSkippedEntries = statsCollector.TotalSkippedEntries }; _statisticsSubject.OnNext(stats); @@ -81,6 +80,8 @@ private void ProcessInventoryFile(InventoryFile inventoryFile, StatisticsCollect foreach (var part in inventory.InventoryParts) { + collector.TotalSkippedEntries += part.SkippedCount; + foreach (var dir in part.DirectoryDescriptions) { if (!dir.IsAccessible) @@ -133,7 +134,7 @@ private static bool HasValidFingerprint(FileDescription fd) return !string.IsNullOrEmpty(fd.Sha256) || !string.IsNullOrEmpty(fd.SignatureGuid); } - private class StatisticsCollector + private sealed class StatisticsCollector { public int TotalAnalyzed { get; set; } @@ -142,6 +143,8 @@ private class StatisticsCollector public int Errors { get; set; } public int IdentificationErrors { get; set; } + + public int TotalSkippedEntries { get; set; } public long ProcessedSize { get; set; } } diff --git a/src/ByteSync.Client/ViewModels/Sessions/Inventories/InventoryGlobalStatusViewModel.cs b/src/ByteSync.Client/ViewModels/Sessions/Inventories/InventoryGlobalStatusViewModel.cs index 3a6a72d6f..aba14ec20 100644 --- a/src/ByteSync.Client/ViewModels/Sessions/Inventories/InventoryGlobalStatusViewModel.cs +++ b/src/ByteSync.Client/ViewModels/Sessions/Inventories/InventoryGlobalStatusViewModel.cs @@ -17,6 +17,9 @@ namespace ByteSync.ViewModels.Sessions.Inventories; public class InventoryGlobalStatusViewModel : ActivatableViewModelBase { + private const string MainSecondaryColorBrushKey = "MainSecondaryColor"; + private const string HomeCloudSynchronizationBackGroundBrushKey = "HomeCloudSynchronizationBackGround"; + private readonly IInventoryService _inventoryService = null!; private readonly ISessionService _sessionService = null!; private readonly IDialogService _dialogService = null!; @@ -79,10 +82,15 @@ public InventoryGlobalStatusViewModel(IInventoryService inventoryService, ISessi [Reactive] public int? GlobalIdentificationErrors { get; set; } + + [Reactive] + public int? GlobalSkippedEntries { get; set; } public extern bool HasErrors { [ObservableAsProperty] get; } public extern bool HasIdentificationErrors { [ObservableAsProperty] get; } + + public extern bool HasGlobalSkippedEntries { [ObservableAsProperty] get; } [Reactive] public string GlobalMainIcon { get; set; } = "None"; @@ -112,6 +120,11 @@ private void SetupBasicProperties(CompositeDisposable disposables) .Select(e => (e ?? 0) > 0) .ToPropertyEx(this, x => x.HasIdentificationErrors) .DisposeWith(disposables); + + this.WhenAnyValue(x => x.GlobalSkippedEntries) + .Select(e => (e ?? 0) > 0) + .ToPropertyEx(this, x => x.HasGlobalSkippedEntries) + .DisposeWith(disposables); } private ReactiveStreams CreateStreams(IInventoryStatisticsService inventoryStatisticsService, CompositeDisposable disposables) @@ -200,7 +213,7 @@ private void SetupVisualElements(ReactiveStreams streams, CompositeDisposable di .DisposeWith(disposables); } - private IObservable<(string Icon, string Text, string BrushKey)> CreateNonSuccessVisual( + private static IObservable<(string Icon, string Text, string BrushKey)> CreateNonSuccessVisual( IObservable statusStream) { return statusStream @@ -210,7 +223,7 @@ private void SetupVisualElements(ReactiveStreams streams, CompositeDisposable di InventoryTaskStatus.Cancelled => Resources.InventoryProcess_InventoryCancelled, InventoryTaskStatus.Error => Resources.InventoryProcess_InventoryError, _ => Resources.InventoryProcess_InventoryError - }, BrushKey: "MainSecondaryColor")); + }, BrushKey: MainSecondaryColorBrushKey)); } private IObservable<(string Icon, string Text, string BrushKey)> CreateSuccessVisual(ReactiveStreams streams) @@ -231,18 +244,18 @@ private void SetupVisualElements(ReactiveStreams streams, CompositeDisposable di .Select(t => GetSuccessVisualState(t.s.AnalyzeErrors)); } - private (string Icon, string Text, string BrushKey) GetSuccessVisualState(int? errors) + private static (string Icon, string Text, string BrushKey) GetSuccessVisualState(int? errors) { if (errors is > 0) { var text = Resources.ResourceManager.GetString("InventoryProcess_InventorySuccessWithErrors", Resources.Culture) ?? Resources.InventoryProcess_InventorySuccess; - return (Icon: "RegularError", Text: text, BrushKey: "MainSecondaryColor"); + return (Icon: "RegularError", Text: text, BrushKey: MainSecondaryColorBrushKey); } return (Icon: "SolidCheckCircle", Text: Resources.InventoryProcess_InventorySuccess, - BrushKey: "HomeCloudSynchronizationBackGround"); + BrushKey: HomeCloudSynchronizationBackGroundBrushKey); } private void SetupStatisticsSubscription(IInventoryStatisticsService inventoryStatisticsService, @@ -270,6 +283,7 @@ private void UpdateStatisticsValues(InventoryStatistics? stats) GlobalAnalyzeSuccess = stats?.AnalyzeSuccess; GlobalAnalyzeErrors = stats?.AnalyzeErrors; GlobalIdentificationErrors = stats?.IdentificationErrors; + GlobalSkippedEntries = stats?.TotalSkippedEntries; } private void ApplySuccessState(int? errors, int? identificationErrors = null) @@ -282,13 +296,13 @@ private void ApplySuccessState(int? errors, int? identificationErrors = null) ?? Resources.InventoryProcess_InventorySuccess; GlobalMainIcon = "RegularError"; GlobalMainStatusText = text; - GlobalMainIconBrush = _themeService.GetBrush("MainSecondaryColor"); + GlobalMainIconBrush = _themeService.GetBrush(MainSecondaryColorBrushKey); } else { GlobalMainIcon = "SolidCheckCircle"; GlobalMainStatusText = Resources.InventoryProcess_InventorySuccess; - GlobalMainIconBrush = _themeService.GetBrush("HomeCloudSynchronizationBackGround"); + GlobalMainIconBrush = _themeService.GetBrush(HomeCloudSynchronizationBackGroundBrushKey); } } @@ -332,6 +346,7 @@ private void ResetStatistics() GlobalAnalyzeSuccess = null; GlobalAnalyzeErrors = null; GlobalIdentificationErrors = null; + GlobalSkippedEntries = null; GlobalMainIcon = "None"; GlobalMainStatusText = string.Empty; GlobalMainIconBrush = null; @@ -363,21 +378,19 @@ private void UpdateGlobalMainIconBrush() case InventoryTaskStatus.Error: case InventoryTaskStatus.Cancelled: case InventoryTaskStatus.NotLaunched: - GlobalMainIconBrush = _themeService.GetBrush("MainSecondaryColor"); + GlobalMainIconBrush = _themeService.GetBrush(MainSecondaryColorBrushKey); break; case InventoryTaskStatus.Success: var errors = GlobalAnalyzeErrors ?? 0; var identificationErrors = GlobalIdentificationErrors ?? 0; GlobalMainIconBrush = (errors + identificationErrors) > 0 - ? _themeService.GetBrush("MainSecondaryColor") - : _themeService.GetBrush("HomeCloudSynchronizationBackGround"); + ? _themeService.GetBrush(MainSecondaryColorBrushKey) + : _themeService.GetBrush(HomeCloudSynchronizationBackGroundBrushKey); break; - case InventoryTaskStatus.Pending: - case InventoryTaskStatus.Running: default: - GlobalMainIconBrush = _themeService.GetBrush("HomeCloudSynchronizationBackGround"); + GlobalMainIconBrush = _themeService.GetBrush(HomeCloudSynchronizationBackGroundBrushKey); break; } @@ -398,7 +411,7 @@ private async Task AbortInventory() } } - private record ReactiveStreams( + private sealed record ReactiveStreams( IObservable StatusStream, IObservable SessionPreparation, IObservable StatsStream, diff --git a/src/ByteSync.Client/ViewModels/Sessions/Inventories/InventoryLocalIdentificationViewModel.cs b/src/ByteSync.Client/ViewModels/Sessions/Inventories/InventoryLocalIdentificationViewModel.cs index bebb7a953..9e392fe44 100644 --- a/src/ByteSync.Client/ViewModels/Sessions/Inventories/InventoryLocalIdentificationViewModel.cs +++ b/src/ByteSync.Client/ViewModels/Sessions/Inventories/InventoryLocalIdentificationViewModel.cs @@ -46,12 +46,14 @@ private void HandleActivation(CompositeDisposable disposables) _inventoryService.InventoryProcessData.InventoryMonitorObservable .Sample(TimeSpan.FromMilliseconds(500)) + .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(m => { IdentifiedFiles = m.IdentifiedFiles; IdentifiedDirectories = m.IdentifiedDirectories; IdentifiedVolume = m.IdentifiedVolume; IdentificationErrors = m.IdentificationErrors; + SkippedEntriesCount = m.SkippedEntriesCount; }) .DisposeWith(disposables); @@ -59,6 +61,11 @@ private void HandleActivation(CompositeDisposable disposables) .Select(v => v > 0) .ToPropertyEx(this, x => x.HasIdentificationErrors) .DisposeWith(disposables); + + this.WhenAnyValue(x => x.SkippedEntriesCount) + .Select(v => v > 0) + .ToPropertyEx(this, x => x.HasSkippedEntriesCount) + .DisposeWith(disposables); _inventoryService.InventoryProcessData.IdentificationStatus .ObserveOn(RxApp.MainThreadScheduler) @@ -100,8 +107,7 @@ private void HandleActivation(CompositeDisposable disposables) InventoryTaskStatus.Success => "InventoryProcess_IdentificationSuccess", InventoryTaskStatus.Cancelled => "InventoryProcess_IdentificationCancelled", InventoryTaskStatus.Error => "InventoryProcess_IdentificationError", - InventoryTaskStatus.Pending => "InventoryProcess_IdentificationRunning", - InventoryTaskStatus.Running => "InventoryProcess_IdentificationRunning", + InventoryTaskStatus.Pending or InventoryTaskStatus.Running => "InventoryProcess_IdentificationRunning", InventoryTaskStatus.NotLaunched => "InventoryProcess_IdentificationCancelled", _ => string.Empty }; @@ -133,8 +139,13 @@ private void HandleActivation(CompositeDisposable disposables) [Reactive] public int IdentificationErrors { get; set; } - + public extern bool HasIdentificationErrors { [ObservableAsProperty] get; } + + [Reactive] + public int SkippedEntriesCount { get; set; } + + public extern bool HasSkippedEntriesCount { [ObservableAsProperty] get; } [Reactive] public string IdentificationIcon { get; set; } = "None"; @@ -159,8 +170,6 @@ private void SetIdentificationBrush(InventoryTaskStatus status) IdentificationIconBrush = _theme_service_get_background(); break; - case InventoryTaskStatus.Pending: - case InventoryTaskStatus.Running: default: IdentificationIconBrush = _theme_service_get_background(); @@ -168,6 +177,6 @@ private void SetIdentificationBrush(InventoryTaskStatus status) } } - private IBrush _theme_service_get_background() => _themeService.GetBrush("HomeCloudSynchronizationBackGround"); - private IBrush _theme_service_get_secondary() => _themeService.GetBrush("MainSecondaryColor"); + private IBrush? _theme_service_get_background() => _themeService.GetBrush("HomeCloudSynchronizationBackGround"); + private IBrush? _theme_service_get_secondary() => _themeService.GetBrush("MainSecondaryColor"); } diff --git a/src/ByteSync.Client/Views/Sessions/Inventories/InventoryDeltaGenerationView.axaml b/src/ByteSync.Client/Views/Sessions/Inventories/InventoryDeltaGenerationView.axaml index c9ad96a48..a2df25824 100644 --- a/src/ByteSync.Client/Views/Sessions/Inventories/InventoryDeltaGenerationView.axaml +++ b/src/ByteSync.Client/Views/Sessions/Inventories/InventoryDeltaGenerationView.axaml @@ -12,7 +12,7 @@ - + @@ -39,7 +39,7 @@ Content="{Binding AnalysisStatusText}" HorizontalContentAlignment="Center" FontWeight="Bold" Margin="6" /> - + @@ -55,10 +55,13 @@ - - + \ No newline at end of file diff --git a/src/ByteSync.Client/Views/Sessions/Inventories/InventoryLocalIdentificationView.axaml b/src/ByteSync.Client/Views/Sessions/Inventories/InventoryLocalIdentificationView.axaml index 4c5262a1f..1d332befe 100644 --- a/src/ByteSync.Client/Views/Sessions/Inventories/InventoryLocalIdentificationView.axaml +++ b/src/ByteSync.Client/Views/Sessions/Inventories/InventoryLocalIdentificationView.axaml @@ -12,7 +12,7 @@ - + @@ -31,7 +31,7 @@ Content="{Binding IdentificationStatusText}" HorizontalContentAlignment="Center" FontWeight="Bold" Margin="6" /> - + @@ -42,24 +42,30 @@ + diff --git a/tests/ByteSync.Client.IntegrationTests/Services/Inventories/InventoryBuilderAccessHandling_IntegrationTests.cs b/tests/ByteSync.Client.IntegrationTests/Services/Inventories/InventoryBuilderAccessHandling_IntegrationTests.cs index 7c47f434f..ac3075a34 100644 --- a/tests/ByteSync.Client.IntegrationTests/Services/Inventories/InventoryBuilderAccessHandling_IntegrationTests.cs +++ b/tests/ByteSync.Client.IntegrationTests/Services/Inventories/InventoryBuilderAccessHandling_IntegrationTests.cs @@ -119,12 +119,12 @@ public async Task InaccessibleDirectory_MarkedAsInaccessibleAndSkipped_Windows() part.DirectoryDescriptions.Should().HaveCountGreaterThanOrEqualTo(2); var accessibleDirDesc = part.DirectoryDescriptions - .FirstOrDefault(d => d.RelativePath.Contains("accessible")); + .FirstOrDefault(d => d.RelativePath.EndsWith("/accessible", StringComparison.Ordinal)); accessibleDirDesc.Should().NotBeNull(); accessibleDirDesc.IsAccessible.Should().BeTrue(); var inaccessibleDirDesc = part.DirectoryDescriptions - .FirstOrDefault(d => d.RelativePath.Contains("inaccessible")); + .FirstOrDefault(d => d.RelativePath.EndsWith("/inaccessible", StringComparison.Ordinal)); inaccessibleDirDesc.Should().NotBeNull(); if (inaccessibleDirDesc.IsAccessible) @@ -215,12 +215,12 @@ public async Task InaccessibleDirectory_MarkedAsInaccessibleAndSkipped_Posix() part.DirectoryDescriptions.Should().HaveCountGreaterThanOrEqualTo(2); var accessibleDirDesc = part.DirectoryDescriptions - .FirstOrDefault(d => d.RelativePath.Contains("accessible")); + .FirstOrDefault(d => d.RelativePath.EndsWith("/accessible", StringComparison.Ordinal)); accessibleDirDesc.Should().NotBeNull(); accessibleDirDesc.IsAccessible.Should().BeTrue(); var inaccessibleDirDesc = part.DirectoryDescriptions - .FirstOrDefault(d => d.RelativePath.Contains("inaccessible")); + .FirstOrDefault(d => d.RelativePath.EndsWith("/inaccessible", StringComparison.Ordinal)); inaccessibleDirDesc.Should().NotBeNull(); if (inaccessibleDirDesc.IsAccessible) @@ -322,11 +322,13 @@ public async Task InaccessibleFile_MarkedAsInaccessibleButDirectoryAccessible_Wi subDirDesc.Should().NotBeNull(); subDirDesc.IsAccessible.Should().BeTrue(); - var accessibleFileDesc = part.FileDescriptions.FirstOrDefault(f => f.RelativePath.Contains("accessible.txt")); + var accessibleFileDesc = part.FileDescriptions + .FirstOrDefault(f => f.RelativePath.EndsWith("/accessible.txt", StringComparison.Ordinal)); accessibleFileDesc.Should().NotBeNull(); accessibleFileDesc.IsAccessible.Should().BeTrue(); - var inaccessibleFileDesc = part.FileDescriptions.FirstOrDefault(f => f.RelativePath.Contains("inaccessible.txt")); + var inaccessibleFileDesc = part.FileDescriptions + .FirstOrDefault(f => f.RelativePath.EndsWith("/inaccessible.txt", StringComparison.Ordinal)); inaccessibleFileDesc.Should().NotBeNull(); if (inaccessibleFileDesc.IsAccessible) @@ -412,11 +414,13 @@ public async Task InaccessibleFile_MarkedAsInaccessibleButDirectoryAccessible_Po subDirDesc.Should().NotBeNull(); subDirDesc.IsAccessible.Should().BeTrue(); - var accessibleFileDesc = part.FileDescriptions.FirstOrDefault(f => f.RelativePath.Contains("accessible.txt")); + var accessibleFileDesc = part.FileDescriptions + .FirstOrDefault(f => f.RelativePath.EndsWith("/accessible.txt", StringComparison.Ordinal)); accessibleFileDesc.Should().NotBeNull(); accessibleFileDesc.IsAccessible.Should().BeTrue(); - var inaccessibleFileDesc = part.FileDescriptions.FirstOrDefault(f => f.RelativePath.Contains("inaccessible.txt")); + var inaccessibleFileDesc = part.FileDescriptions + .FirstOrDefault(f => f.RelativePath.EndsWith("/inaccessible.txt", StringComparison.Ordinal)); inaccessibleFileDesc.Should().NotBeNull(); if (inaccessibleFileDesc.IsAccessible) @@ -837,4 +841,4 @@ public async Task InaccessibleFile_AsInventoryPartOfTypeFile_Posix() } } } -#pragma warning restore CA1416 \ No newline at end of file +#pragma warning restore CA1416 diff --git a/tests/ByteSync.Client.UnitTests/Business/Inventories/InventoryMonitorDataTests.cs b/tests/ByteSync.Client.UnitTests/Business/Inventories/InventoryMonitorDataTests.cs index 6a45fd0b8..0c395b535 100644 --- a/tests/ByteSync.Client.UnitTests/Business/Inventories/InventoryMonitorDataTests.cs +++ b/tests/ByteSync.Client.UnitTests/Business/Inventories/InventoryMonitorDataTests.cs @@ -149,6 +149,19 @@ public void HasNonZeroProperty_WithUploadedVolume_ShouldReturnTrue() // Assert result.Should().BeTrue(); } + + [Test] + public void HasNonZeroProperty_WithSkippedEntriesCount_ShouldReturnTrue() + { + // Arrange + var data = new InventoryMonitorData { SkippedEntriesCount = 2 }; + + // Act + var result = data.HasNonZeroProperty(); + + // Assert + result.Should().BeTrue(); + } [Test] public void HasNonZeroProperty_WithMultipleNonZeroProperties_ShouldReturnTrue() @@ -201,4 +214,4 @@ public void UploadTotalVolume_ShouldBeSettableAndGettable() // Assert data.UploadTotalVolume.Should().Be(10240); } -} \ No newline at end of file +} diff --git a/tests/ByteSync.Client.UnitTests/Business/Inventories/InventoryProcessDataTests.cs b/tests/ByteSync.Client.UnitTests/Business/Inventories/InventoryProcessDataTests.cs index b15cd755e..da296adc9 100644 --- a/tests/ByteSync.Client.UnitTests/Business/Inventories/InventoryProcessDataTests.cs +++ b/tests/ByteSync.Client.UnitTests/Business/Inventories/InventoryProcessDataTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Reactive.Linq; using ByteSync.Business.Inventories; using ByteSync.Models.Inventories; using FluentAssertions; @@ -44,6 +45,21 @@ public void RecordSkippedEntry_ShouldUpdateGlobalAndReasonCounters() data.GetSkippedCountByReason(SkipReason.NoiseEntry).Should().Be(1); data.GetSkippedCountByReason(SkipReason.Offline).Should().Be(0); } + + [Test] + public async Task RecordSkippedEntry_ShouldUpdateMonitorSkippedEntriesCount() + { + // Arrange + var data = new InventoryProcessData(); + + // Act + data.RecordSkippedEntry(new SkippedEntry { Reason = SkipReason.Hidden }); + data.RecordSkippedEntry(new SkippedEntry { Reason = SkipReason.NoiseEntry }); + var monitor = await data.InventoryMonitorObservable.FirstAsync(); + + // Assert + monitor.SkippedEntriesCount.Should().Be(2); + } [Test] public void Reset_ShouldClearSkippedEntriesAndCounters() diff --git a/tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryStatisticsServiceTests.cs b/tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryStatisticsServiceTests.cs index 1ceac833d..9c6b2625f 100644 --- a/tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryStatisticsServiceTests.cs +++ b/tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryStatisticsServiceTests.cs @@ -41,7 +41,7 @@ private static string CreateInventoryZip(Inventory inventory) } private static Inventory BuildInventoryWithFiles(int successCount, int errorCount, int neutralCount, - int inaccessibleFiles = 0, int inaccessibleDirectories = 0) + int inaccessibleFiles = 0, int inaccessibleDirectories = 0, int skippedHidden = 0, int skippedNoise = 0) { var inv = new Inventory { @@ -122,6 +122,16 @@ private static Inventory BuildInventoryWithFiles(int successCount, int errorCoun }; part.DirectoryDescriptions.Add(dir); } + + for (var i = 0; i < skippedHidden; i++) + { + part.RecordSkippedEntry(SkipReason.Hidden); + } + + for (var i = 0; i < skippedNoise; i++) + { + part.RecordSkippedEntry(SkipReason.NoiseEntry); + } return inv; } @@ -167,6 +177,7 @@ public async Task Compute_WithSingleInventory_ComputesExpectedTotals() stats.AnalyzeSuccess.Should().Be(3); stats.AnalyzeErrors.Should().Be(2); stats.IdentificationErrors.Should().Be(0); + stats.TotalSkippedEntries.Should().Be(0); } finally { @@ -229,6 +240,7 @@ public async Task Compute_WithMultipleInventories_AggregatesAll() stats.AnalyzeSuccess.Should().Be(1 + 2); stats.AnalyzeErrors.Should().Be(1 + 0); stats.IdentificationErrors.Should().Be(0); + stats.TotalSkippedEntries.Should().Be(0); } finally { @@ -286,6 +298,55 @@ public async Task Compute_CountsIdentificationErrors() stats.AnalyzeSuccess.Should().Be(0); stats.AnalyzeErrors.Should().Be(0); stats.IdentificationErrors.Should().Be(3); + stats.TotalSkippedEntries.Should().Be(0); + } + finally + { + if (File.Exists(zip)) + { + File.Delete(zip); + } + } + } + + [Test] + public async Task Compute_CountsSkippedEntriesFromInventoryParts() + { + var inv = BuildInventoryWithFiles(successCount: 0, errorCount: 0, neutralCount: 0, skippedHidden: 2, skippedNoise: 1); + var zip = CreateInventoryZip(inv); + + try + { + var sfd = new SharedFileDefinition + { + SessionId = "S", + ClientInstanceId = inv.Endpoint.ClientInstanceId, + SharedFileType = SharedFileTypes.FullInventory, + AdditionalName = inv.CodeAndId, + IV = new byte[16] + }; + var inventoryFile = new InventoryFile(sfd, zip); + + var repo = new Mock(); + repo.Setup(r => r.GetAllInventoriesFiles(LocalInventoryModes.Full)) + .Returns([inventoryFile]); + + var ipd = new InventoryProcessData(); + var invService = new Mock(); + invService.SetupGet(s => s.InventoryProcessData).Returns(ipd); + + var logger = new Mock>(); + + var service = new InventoryStatisticsService(invService.Object, repo.Object, logger.Object); + + var tcs = new TaskCompletionSource(); + using var sub = service.Statistics.Where(s => s != null).Take(1).Select(s => s!).Subscribe(s => tcs.TrySetResult(s)); + + ipd.AreFullInventoriesComplete.OnNext(true); + + var stats = await tcs.Task.WaitAsync(TimeSpan.FromSeconds(5)); + + stats.TotalSkippedEntries.Should().Be(3); } finally { diff --git a/tests/ByteSync.Client.UnitTests/Services/TimeTracking/TimeTrackingComputerTests.cs b/tests/ByteSync.Client.UnitTests/Services/TimeTracking/TimeTrackingComputerTests.cs index 724778f66..b1e1996a3 100644 --- a/tests/ByteSync.Client.UnitTests/Services/TimeTracking/TimeTrackingComputerTests.cs +++ b/tests/ByteSync.Client.UnitTests/Services/TimeTracking/TimeTrackingComputerTests.cs @@ -1,5 +1,6 @@ using System.Reactive.Linq; using System.Reactive.Subjects; +using System.Collections.Concurrent; using ByteSync.Business.Misc; using ByteSync.Interfaces.Controls.TimeTracking; using ByteSync.Services.TimeTracking; @@ -33,6 +34,12 @@ private TimeTrackingComputer CreateSut() { return new TimeTrackingComputer(_dataTrackingStrategyMock.Object); } + + private static void WaitFor(TimeSpan delay) + { + using var gate = new ManualResetEventSlim(false); + gate.Wait(delay); + } [Test] public void Constructor_ShouldInitialize_WithDefaultValues() @@ -75,7 +82,7 @@ public void Start_ShouldReset_PreviousData() sut.Start(firstStart); _dataSubject.OnNext((1000, 500)); - Thread.Sleep(50); + WaitFor(TimeSpan.FromMilliseconds(50)); sut.Start(secondStart); @@ -103,11 +110,11 @@ public void Stop_ShouldStop_TimeTracking() using var subscription = sut.RemainingTime.Subscribe(tt => emissions.Add(tt)); - Thread.Sleep(1500); + WaitFor(TimeSpan.FromMilliseconds(1500)); sut.Stop(); - Thread.Sleep(100); + WaitFor(TimeSpan.FromMilliseconds(100)); emissions.Should().NotBeEmpty(); var finalTimeTrack = emissions.Last(); @@ -134,7 +141,7 @@ public void Stop_WhenStartDateTimeIsSet_ShouldCalculate_EstimatedEndDateTime() sut.Stop(); - Thread.Sleep(100); + WaitFor(TimeSpan.FromMilliseconds(100)); capturedTimeTrack.Should().NotBeNull(); capturedTimeTrack!.EstimatedEndDateTime.Should().NotBeNull(); @@ -151,7 +158,7 @@ public void DataUpdate_WhenNotStarted_ShouldNotUpdate_TimeTrack() _dataSubject.OnNext((1000, 100)); - Thread.Sleep(100); + WaitFor(TimeSpan.FromMilliseconds(100)); emissions.Should().BeEmpty(); } @@ -167,11 +174,11 @@ public void DataUpdate_WhenStarted_ShouldUpdate_TimeTrack() using var subscription = sut.RemainingTime.Take(2).Subscribe(emissions.Add); - Thread.Sleep(100); + WaitFor(TimeSpan.FromMilliseconds(100)); _dataSubject.OnNext((1000, 100)); - Thread.Sleep(2100); + WaitFor(TimeSpan.FromMilliseconds(2100)); sut.LastDataHandledDateTime.Should().NotBeNull(); emissions.Should().NotBeEmpty(); @@ -286,7 +293,7 @@ public void RemainingTime_WhenNotStarted_ShouldNotEmit() tt => emissions.Add(tt), _ => { }); - Thread.Sleep(600); + WaitFor(TimeSpan.FromMilliseconds(600)); emissions.Should().BeEmpty(); } @@ -296,16 +303,25 @@ public void RemainingTime_WhenStarted_ShouldEmit_Periodically() { var sut = CreateSut(); var startDateTime = DateTimeOffset.Now; - var emissions = new List(); + var emissions = new ConcurrentQueue(); + using var receivedTwoEmissions = new ManualResetEventSlim(false); sut.Start(startDateTime); using var subscription = sut.RemainingTime .Take(3) - .Subscribe(tt => emissions.Add(tt)); - - Thread.Sleep(2500); - + .Subscribe(tt => + { + emissions.Enqueue(tt); + if (emissions.Count >= 2) + { + receivedTwoEmissions.Set(); + } + }); + + var receivedTwoEmissionsWithinTimeout = receivedTwoEmissions.Wait(TimeSpan.FromSeconds(5)); + + receivedTwoEmissionsWithinTimeout.Should().BeTrue("remaining time should emit periodically while tracking is started"); emissions.Should().HaveCountGreaterThanOrEqualTo(2); emissions.All(tt => tt.StartDateTime.HasValue).Should().BeTrue(); } @@ -321,15 +337,15 @@ public void RemainingTime_AfterStop_ShouldStop_Emitting() using var subscription = sut.RemainingTime.Subscribe(_ => emissionCount++); - Thread.Sleep(2100); + WaitFor(TimeSpan.FromMilliseconds(2100)); sut.Stop(); - Thread.Sleep(100); + WaitFor(TimeSpan.FromMilliseconds(100)); var countAfterStop = emissionCount; - Thread.Sleep(1500); + WaitFor(TimeSpan.FromMilliseconds(1500)); var countAfterWait = emissionCount; @@ -374,16 +390,16 @@ public void MultipleDataUpdates_ShouldUpdate_EstimatedEndDateTime() using var subscription = sut.RemainingTime.Subscribe(tt => emissions.Add(tt)); - Thread.Sleep(100); + WaitFor(TimeSpan.FromMilliseconds(100)); _dataSubject.OnNext((1000, 100)); - Thread.Sleep(1100); + WaitFor(TimeSpan.FromMilliseconds(1100)); _dataSubject.OnNext((1000, 200)); - Thread.Sleep(1100); + WaitFor(TimeSpan.FromMilliseconds(1100)); _dataSubject.OnNext((1000, 300)); - Thread.Sleep(1100); + WaitFor(TimeSpan.FromMilliseconds(1100)); emissions.Should().HaveCountGreaterThanOrEqualTo(3); @@ -400,11 +416,11 @@ public void Start_ThenDataUpdate_ShouldSet_LastDataHandledDateTime() sut.Start(startDateTime); - Thread.Sleep(50); + WaitFor(TimeSpan.FromMilliseconds(50)); _dataSubject.OnNext((1000, 100)); - Thread.Sleep(100); + WaitFor(TimeSpan.FromMilliseconds(100)); var afterUpdate = DateTime.Now; @@ -428,7 +444,7 @@ public void RemainingTime_Observable_ShouldCombine_IntervalAndTimeTrack() .Take(3) .Subscribe(tt => emissions.Add(tt)); - Thread.Sleep(3500); + WaitFor(TimeSpan.FromMilliseconds(3500)); emissions.Should().HaveCountGreaterThanOrEqualTo(2); emissions.Should().OnlyContain(tt => tt.StartDateTime.HasValue); @@ -483,20 +499,20 @@ public void CompleteProcess_FromStartToStop_ShouldUpdate_AllFields() using var subscription = sut.RemainingTime.Subscribe(tt => emissions.Add(tt)); - Thread.Sleep(100); + WaitFor(TimeSpan.FromMilliseconds(100)); _dataSubject.OnNext((1000, 250)); - Thread.Sleep(1100); + WaitFor(TimeSpan.FromMilliseconds(1100)); _dataSubject.OnNext((1000, 500)); - Thread.Sleep(1100); + WaitFor(TimeSpan.FromMilliseconds(1100)); _dataSubject.OnNext((1000, 750)); - Thread.Sleep(1100); + WaitFor(TimeSpan.FromMilliseconds(1100)); sut.Stop(); - Thread.Sleep(100); + WaitFor(TimeSpan.FromMilliseconds(100)); emissions.Should().NotBeEmpty(); var finalEmission = emissions.Last(); @@ -505,4 +521,4 @@ public void CompleteProcess_FromStartToStop_ShouldUpdate_AllFields() finalEmission.RemainingTime.Should().Be(TimeSpan.Zero); sut.LastDataHandledDateTime.Should().NotBeNull(); } -} \ No newline at end of file +} diff --git a/tests/ByteSync.Client.UnitTests/ViewModels/Sessions/DataNodes/DataNodeViewModelTests.cs b/tests/ByteSync.Client.UnitTests/ViewModels/Sessions/DataNodes/DataNodeViewModelTests.cs index 051e162b6..32783d511 100644 --- a/tests/ByteSync.Client.UnitTests/ViewModels/Sessions/DataNodes/DataNodeViewModelTests.cs +++ b/tests/ByteSync.Client.UnitTests/ViewModels/Sessions/DataNodes/DataNodeViewModelTests.cs @@ -274,9 +274,18 @@ public void AddDataNodeCommand_CanExecute_MirrorsVisibilityRule() // Leave preparation -> cannot execute bool? latestCanExec = null; - using var sub = vm.AddDataNodeCommand.CanExecute.Subscribe(b => latestCanExec = b); + using var canExecuteBecameFalse = new ManualResetEventSlim(false); + using var sub = vm.AddDataNodeCommand.CanExecute.Subscribe(b => + { + latestCanExec = b; + if (b == false) + { + canExecuteBecameFalse.Set(); + } + }); status.OnNext(SessionStatus.Inventory); - SpinWait.SpinUntil(() => latestCanExec == false, TimeSpan.FromSeconds(5)).Should() + canExecuteBecameFalse.Wait(TimeSpan.FromSeconds(5)).Should() .BeTrue("AddDataNodeCommand.CanExecute should become false outside Preparation"); + latestCanExec.Should().BeFalse(); } -} \ No newline at end of file +} diff --git a/tests/ByteSync.Client.UnitTests/ViewModels/Sessions/Inventories/InventoryGlobalStatusViewModelTests.cs b/tests/ByteSync.Client.UnitTests/ViewModels/Sessions/Inventories/InventoryGlobalStatusViewModelTests.cs index 45ae63da2..e9236eb74 100644 --- a/tests/ByteSync.Client.UnitTests/ViewModels/Sessions/Inventories/InventoryGlobalStatusViewModelTests.cs +++ b/tests/ByteSync.Client.UnitTests/ViewModels/Sessions/Inventories/InventoryGlobalStatusViewModelTests.cs @@ -262,6 +262,20 @@ public void SuccessWithIdentificationErrors_RendersErrorState() vm.HasIdentificationErrors.Should().BeTrue(); vm.GlobalMainIcon.Should().Be("RegularError"); } + + [Test] + public void HasGlobalSkippedEntries_ComputedFromStats() + { + var vm = CreateVm(); + + _statsSubject.OnNext(new InventoryStatistics { TotalSkippedEntries = 4 }); + vm.HasGlobalSkippedEntries.Should().BeTrue(); + vm.GlobalSkippedEntries.Should().Be(4); + + _statsSubject.OnNext(new InventoryStatistics { TotalSkippedEntries = 0 }); + vm.HasGlobalSkippedEntries.Should().BeFalse(); + vm.GlobalSkippedEntries.Should().Be(0); + } [Test] public async Task AbortCommand_UserConfirms_RequestsAbort() diff --git a/tests/ByteSync.Client.UnitTests/ViewModels/Sessions/Inventories/InventoryLocalIdentificationViewModelTests.cs b/tests/ByteSync.Client.UnitTests/ViewModels/Sessions/Inventories/InventoryLocalIdentificationViewModelTests.cs index 67c45ff15..1c73841f9 100644 --- a/tests/ByteSync.Client.UnitTests/ViewModels/Sessions/Inventories/InventoryLocalIdentificationViewModelTests.cs +++ b/tests/ByteSync.Client.UnitTests/ViewModels/Sessions/Inventories/InventoryLocalIdentificationViewModelTests.cs @@ -84,6 +84,24 @@ public void StatusTransitions_UpdateIconsBrushesAndText() vm.IdentificationStatusText.Should().NotBeNullOrWhiteSpace(); vm.IdentificationStatusText.Should().NotBe(successText); } + + [Test] + public void MonitorUpdates_ShouldUpdateSkippedEntriesAndHasFlag() + { + var vm = CreateVm(); + + vm.SkippedEntriesCount.Should().Be(0); + vm.HasSkippedEntriesCount.Should().BeFalse(); + + _processData.UpdateMonitorData(m => { m.SkippedEntriesCount = 3; }); + + vm.ShouldEventuallyBe(x => x.SkippedEntriesCount, 3); + vm.ShouldEventuallyBe(x => x.HasSkippedEntriesCount, true); + + _processData.UpdateMonitorData(m => { m.SkippedEntriesCount = 0; }); + + vm.ShouldEventuallyBe(x => x.HasSkippedEntriesCount, false); + } private InventoryLocalIdentificationViewModel CreateVm() { @@ -92,4 +110,4 @@ private InventoryLocalIdentificationViewModel CreateVm() return vm; } -} \ No newline at end of file +}