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 @@
-
-
+
-
-
-
+
+
-
-
+
diff --git a/src/ByteSync.Client/Views/Sessions/Inventories/InventoryGlobalStatusView.axaml b/src/ByteSync.Client/Views/Sessions/Inventories/InventoryGlobalStatusView.axaml
index b3b428de2..a6646aef0 100644
--- a/src/ByteSync.Client/Views/Sessions/Inventories/InventoryGlobalStatusView.axaml
+++ b/src/ByteSync.Client/Views/Sessions/Inventories/InventoryGlobalStatusView.axaml
@@ -12,7 +12,8 @@
-
+
@@ -57,35 +58,42 @@
+
-
+
-
+
-
+
-
-
+
+
+
+
+
-
+
\ 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 @@
+
-
+
+
+
-
+
+
+
-
+
+ HorizontalContentAlignment="Right" />
+
-
+
+
+
+
+
+
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
+}