diff --git a/AGENTS.md b/AGENTS.md index b9ecc9d4..8d19ce07 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,6 +53,15 @@ Examples: - Comments are only allowed when strictly necessary to explain complex or non-obvious logic. - Prefer clear and self-explanatory code over adding comments. +### Testing Style & Conventions +- **Assertions**: use FluentAssertions (`.Should()`). **Do not use** `NUnit.Framework.Legacy` / `ClassicAssert`. +- **Usings**: remove `using NUnit.Framework.Legacy;` from tests. +- **Structure**: follow Arrange–Act–Assert. Use clear, English names and messages. + +### Coverage Requirements +- **Target per new unit test class**: ≥ 85% coverage. +- **PR Quality (Sonar)**: each PR must maintain a minimum coverage of 80%. + ## Build and Test Guidelines - Always run build and test as two separate commands to avoid blocking issues. - Use `dotnet build --verbosity quiet /property:WarningLevel=0` to build the solution. diff --git a/src/ByteSync.Client/ViewModels/Sessions/Synchronizations/SynchronizationBeforeStartViewModel.cs b/src/ByteSync.Client/ViewModels/Sessions/Synchronizations/SynchronizationBeforeStartViewModel.cs new file mode 100644 index 00000000..6af0b78c --- /dev/null +++ b/src/ByteSync.Client/ViewModels/Sessions/Synchronizations/SynchronizationBeforeStartViewModel.cs @@ -0,0 +1,196 @@ +using System.Reactive; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Avalonia.Controls; +using ByteSync.Assets.Resources; +using ByteSync.Business.Sessions; +using ByteSync.Common.Business.Sessions.Cloud; +using ByteSync.Interfaces.Controls.Synchronizations; +using ByteSync.Interfaces.Repositories; +using ByteSync.Interfaces.Services.Localizations; +using ByteSync.Interfaces.Services.Sessions; +using ByteSync.ViewModels.Misc; +using DynamicData; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; + +namespace ByteSync.ViewModels.Sessions.Synchronizations; + +public class SynchronizationBeforeStartViewModel : ActivatableViewModelBase +{ + private readonly ISessionService _sessionService = null!; + private readonly ILocalizationService _localizationService = null!; + private readonly ISynchronizationService _synchronizationService = null!; + private readonly ISynchronizationStarter _synchronizationStarter = null!; + private readonly IAtomicActionRepository _atomicActionRepository = null!; + private readonly ISessionMemberRepository _sessionMemberRepository = null!; + private readonly ILogger _logger = null!; + + public SynchronizationBeforeStartViewModel() + { + } + + public SynchronizationBeforeStartViewModel(ISessionService sessionService, ILocalizationService localizationService, + ISynchronizationService synchronizationService, ISynchronizationStarter synchronizationStarter, + IAtomicActionRepository atomicActionRepository, ISessionMemberRepository sessionMemberRepository, + ILogger logger, ErrorViewModel errorViewModel) + { + _sessionService = sessionService; + _localizationService = localizationService; + _synchronizationService = synchronizationService; + _synchronizationStarter = synchronizationStarter; + _atomicActionRepository = atomicActionRepository; + _sessionMemberRepository = sessionMemberRepository; + _logger = logger; + + StartSynchronizationError = errorViewModel; + + var canStartSynchronization = _sessionService.SessionObservable + .CombineLatest( + _atomicActionRepository.ObservableCache.Connect().ToCollection(), + _sessionMemberRepository.IsCurrentUserFirstSessionMemberObservable, + (session, atomicActions, isCurrentUserFirstSessionMember) => + (Session: session, AtomicActions: atomicActions, IsCurrentUserFirstSessionMember: isCurrentUserFirstSessionMember)) + .DistinctUntilChanged() + .ObserveOn(RxApp.MainThreadScheduler) + .Select(tuple => tuple.Session is CloudSession && tuple.IsCurrentUserFirstSessionMember && tuple.AtomicActions.Count > 0); + + StartSynchronizationCommand = ReactiveCommand.CreateFromTask(StartSynchronization, canStartSynchronization); + + this.WhenActivated(disposables => + { + _synchronizationService.SynchronizationProcessData.SynchronizationStart + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(_ => IsSynchronizationRunning = true) + .DisposeWith(disposables); + + _synchronizationService.SynchronizationProcessData.SynchronizationEnd + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(_ => IsSynchronizationRunning = false) + .DisposeWith(disposables); + + _sessionService.SessionStatusObservable + .CombineLatest(_synchronizationService.SynchronizationProcessData.SynchronizationStart) + .Select(tuple => + !tuple.First.In(SessionStatus.None, SessionStatus.Preparation, SessionStatus.Comparison, + SessionStatus.CloudSessionCreation, SessionStatus.CloudSessionJunction, SessionStatus.Inventory) + && tuple.Second != null) + .ObserveOn(RxApp.MainThreadScheduler) + .ToPropertyEx(this, x => x.HasSynchronizationStarted) + .DisposeWith(disposables); + + this.WhenAnyValue( + x => x.IsSynchronizationRunning, x => x.IsCloudSession, x => x.IsSessionCreatedByMe, x => x.HasSynchronizationStarted, + x => x.IsProfileSessionSynchronization, x => x.HasSessionBeenRestarted, + ComputeShowStartSynchronizationObservable) + .ObserveOn(RxApp.MainThreadScheduler) + .ToPropertyEx(this, x => x.ShowStartSynchronizationObservable) + .DisposeWith(disposables); + + this.WhenAnyValue( + x => x.IsSynchronizationRunning, x => x.IsCloudSession, x => x.IsSessionCreatedByMe, x => x.HasSynchronizationStarted, + x => x.IsProfileSessionSynchronization, x => x.HasSessionBeenRestarted, + ComputeShowWaitingForSynchronizationStartObservable) + .ObserveOn(RxApp.MainThreadScheduler) + .ToPropertyEx(this, x => x.ShowWaitingForSynchronizationStartObservable) + .DisposeWith(disposables); + }); + + if (Design.IsDesignMode) + { + return; + } + + IsCloudSession = _sessionService.IsCloudSession; + IsSessionCreatedByMe = _sessionMemberRepository.IsCurrentUserFirstSessionMemberCurrentValue; + IsProfileSessionSynchronization = _sessionService.CurrentRunSessionProfileInfo is { AutoStartsSynchronization: true }; + + if (IsCloudSession) + { + if (IsProfileSessionSynchronization) + { + WaitingForSynchronizationStartMessage = _localizationService[nameof(Resources.SynchronizationMain_WaitingForAutomaticStart_CloudSession)]; + } + else if (!IsCloudSessionCreatedByMe) + { + var creatorMachineName = _sessionMemberRepository.Elements.First().MachineName; + WaitingForSynchronizationStartMessage = string.Format(_localizationService[nameof(Resources.SynchronizationMain_WaitingForClientATemplate)], creatorMachineName); + } + } + else + { + if (IsProfileSessionSynchronization) + { + WaitingForSynchronizationStartMessage = _localizationService[nameof(Resources.SynchronizationMain_WaitingForAutomaticStart_LocalSession)]; + } + } + } + + public ReactiveCommand StartSynchronizationCommand { get; } + + [Reactive] + public bool IsSynchronizationRunning { get; set; } + + [Reactive] + public bool IsCloudSession { get; set; } + + [Reactive] + public bool IsSessionCreatedByMe { get; set; } + + [Reactive] + public bool IsProfileSessionSynchronization { get; set; } + + [Reactive] + public bool HasSessionBeenRestarted { get; set; } + + [Reactive] + public string WaitingForSynchronizationStartMessage { get; set; } = string.Empty; + + [Reactive] + public ErrorViewModel StartSynchronizationError { get; set; } + + public extern bool ShowStartSynchronizationObservable { [ObservableAsProperty] get; } + + public extern bool ShowWaitingForSynchronizationStartObservable { [ObservableAsProperty] get; } + + public extern bool HasSynchronizationStarted { [ObservableAsProperty] get; } + + private bool IsCloudSessionCreatedByMe => IsCloudSession && IsSessionCreatedByMe; + + private bool ComputeShowStartSynchronizationObservable(bool isSynchronizationRunning, bool isCloudSession, bool isSessionCreatedByMe, + bool hasSynchronizationStarted, bool isProfileSessionSynchronization, bool hasSessionBeenRestarted) + { + var result = (!isProfileSessionSynchronization || (!isCloudSession && hasSessionBeenRestarted)) + && !isSynchronizationRunning + && !hasSynchronizationStarted + && (!isCloudSession || isSessionCreatedByMe); + + return result; + } + + private bool ComputeShowWaitingForSynchronizationStartObservable(bool isSynchronizationRunning, bool isCloudSession, bool isSessionCreatedByMe, + bool hasSynchronizationStarted, bool isProfileSessionSynchronization, bool hasSessionBeenRestarted) + { + var result = !isSynchronizationRunning + && !hasSynchronizationStarted + && ((isProfileSessionSynchronization && !hasSessionBeenRestarted) || (!isSessionCreatedByMe && isCloudSession)); + + return result; + } + + private async Task StartSynchronization() + { + try + { + StartSynchronizationError.Clear(); + + await _synchronizationStarter.StartSynchronization(true); + } + catch (Exception ex) + { + _logger.LogError(ex, "SynchronizationBeforeStartViewModel.StartSynchronization"); + + StartSynchronizationError.SetException(ex); + } + } +} diff --git a/src/ByteSync.Client/ViewModels/Sessions/Synchronizations/SynchronizationMainStatusViewModel.cs b/src/ByteSync.Client/ViewModels/Sessions/Synchronizations/SynchronizationMainStatusViewModel.cs new file mode 100644 index 00000000..a176c5ed --- /dev/null +++ b/src/ByteSync.Client/ViewModels/Sessions/Synchronizations/SynchronizationMainStatusViewModel.cs @@ -0,0 +1,166 @@ +using System.Reactive; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using ByteSync.Assets.Resources; +using Avalonia.Controls; +using ByteSync.Business; +using ByteSync.Business.Sessions; +using ByteSync.Common.Business.Synchronizations; +using ByteSync.Interfaces.Controls.Synchronizations; +using ByteSync.Interfaces.Dialogs; +using ByteSync.Interfaces.Services.Sessions; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; + +namespace ByteSync.ViewModels.Sessions.Synchronizations; + +public class SynchronizationMainStatusViewModel : ActivatableViewModelBase +{ + private readonly ISessionService _sessionService = null!; + private readonly ISynchronizationService _synchronizationService = null!; + private readonly IDialogService _dialogService = null!; + private readonly ILogger _logger = null!; + + public SynchronizationMainStatusViewModel() + { +#if DEBUG + MainStatus = "MainStatus"; + IsMainProgressRingVisible = false; + IsMainCheckVisible = false; +#endif + if (Design.IsDesignMode) + { + IsSynchronizationRunning = true; + IsMainCheckVisible = true; + } + + MainStatus = Resources.SynchronizationMain_SynchronizationRunning; + IsSynchronizationRunning = false; + + AbortSynchronizationCommand = ReactiveCommand.CreateFromTask(AbortSynchronization); + + this.WhenActivated(disposables => + { + _synchronizationService.SynchronizationProcessData.SynchronizationStart + .CombineLatest(_synchronizationService.SynchronizationProcessData.SynchronizationAbortRequest, + _synchronizationService.SynchronizationProcessData.SynchronizationEnd) + .Where(tuple => tuple.First != null && tuple.Second == null && tuple.Third == null) + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(tuple => OnSynchronizationStarted(tuple.First!)) + .DisposeWith(disposables); + + _synchronizationService.SynchronizationProcessData.SynchronizationAbortRequest.DistinctUntilChanged() + .Where(synchronizationAbortRequest => synchronizationAbortRequest != null) + .Subscribe(synchronizationAbortRequest => OnSynchronizationAbortRequested(synchronizationAbortRequest!)) + .DisposeWith(disposables); + + _synchronizationService.SynchronizationProcessData.SynchronizationEnd.DistinctUntilChanged() + .Where(synchronizationEnd => synchronizationEnd != null) + .Subscribe(synchronizationEnd => OnSynchronizationEnded(synchronizationEnd!)) + .DisposeWith(disposables); + + _sessionService.SessionStatusObservable + .CombineLatest(_synchronizationService.SynchronizationProcessData.SynchronizationStart) + .Select(tuple => + !tuple.First.In(SessionStatus.None, SessionStatus.Preparation, SessionStatus.Comparison, + SessionStatus.CloudSessionCreation, SessionStatus.CloudSessionJunction, SessionStatus.Inventory) + && tuple.Second != null) + .ObserveOn(RxApp.MainThreadScheduler) + .ToPropertyEx(this, x => x.HasSynchronizationStarted) + .DisposeWith(disposables); + }); + } + + public SynchronizationMainStatusViewModel(ISessionService sessionService, ISynchronizationService synchronizationService, + IDialogService dialogService, ILogger logger) : this() + { + _sessionService = sessionService; + _synchronizationService = synchronizationService; + _dialogService = dialogService; + _logger = logger; + } + + public ReactiveCommand AbortSynchronizationCommand { get; } + + [Reactive] + public bool IsSynchronizationRunning { get; set; } + + [Reactive] + public bool IsMainProgressRingVisible { get; set; } + + [Reactive] + public bool IsMainCheckVisible { get; set; } + + [Reactive] + public string MainIcon { get; set; } + + [Reactive] + public string MainStatus { get; set; } + + public extern bool HasSynchronizationStarted { [ObservableAsProperty] get; } + + private async Task AbortSynchronization() + { + try + { + var messageBoxViewModel = _dialogService.CreateMessageBoxViewModel( + nameof(Resources.SynchronizationMain_AbortSynchronization_Title), nameof(Resources.SynchronizationMain_AbortSynchronization_Message)); + messageBoxViewModel.ShowYesNo = true; + var result = await _dialogService.ShowMessageBoxAsync(messageBoxViewModel); + + if (result == MessageBoxResult.Yes) + { + await _synchronizationService.AbortSynchronization(); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "SynchronizationMainStatusViewModel.AbortSynchronization"); + } + } + + private void OnSynchronizationStarted(Synchronization _) + { + IsSynchronizationRunning = true; + HandledActionsReset(); + IsMainCheckVisible = false; + IsMainProgressRingVisible = true; + MainStatus = Resources.SynchronizationMain_SynchronizationRunning; + } + + private void OnSynchronizationAbortRequested(SynchronizationAbortRequest _) + { + if (IsSynchronizationRunning) + { + MainStatus = Resources.SynchronizationMain_SynchronizationAbortRequested; + } + } + + private void OnSynchronizationEnded(SynchronizationEnd synchronizationEnd) + { + IsSynchronizationRunning = false; + + if (synchronizationEnd.Status == SynchronizationEndStatuses.Abortion) + { + MainStatus = Resources.SynchronizationMain_SynchronizationAborted; + MainIcon = "SolidXCircle"; + } + else if (synchronizationEnd.Status == SynchronizationEndStatuses.Error) + { + MainStatus = Resources.SynchronizationMain_SynchronizationError; + MainIcon = "SolidXCircle"; + } + else + { + MainStatus = Resources.SynchronizationMain_SynchronizationDone; + MainIcon = "SolidCheckCircle"; + } + + IsMainProgressRingVisible = false; + IsMainCheckVisible = true; + } + + private void HandledActionsReset() + { + } +} diff --git a/src/ByteSync.Client/ViewModels/Sessions/Synchronizations/SynchronizationMainViewModel.cs b/src/ByteSync.Client/ViewModels/Sessions/Synchronizations/SynchronizationMainViewModel.cs index 3365c536..3bd30649 100644 --- a/src/ByteSync.Client/ViewModels/Sessions/Synchronizations/SynchronizationMainViewModel.cs +++ b/src/ByteSync.Client/ViewModels/Sessions/Synchronizations/SynchronizationMainViewModel.cs @@ -1,442 +1,41 @@ -using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; -using Avalonia.Controls; -using Avalonia.Controls.Mixins; -using ByteSync.Assets.Resources; -using ByteSync.Business; -using ByteSync.Business.Misc; -using ByteSync.Business.Sessions; -using ByteSync.Business.Synchronizations; -using ByteSync.Common.Business.Sessions.Cloud; -using ByteSync.Common.Business.Synchronizations; -using ByteSync.Interfaces; -using ByteSync.Interfaces.Controls.Synchronizations; -using ByteSync.Interfaces.Controls.TimeTracking; -using ByteSync.Interfaces.Dialogs; -using ByteSync.Interfaces.Repositories; -using ByteSync.Interfaces.Services.Localizations; -using ByteSync.Interfaces.Services.Sessions; -using ByteSync.ViewModels.Misc; -using DynamicData; using ReactiveUI; using ReactiveUI.Fody.Helpers; -using Serilog; namespace ByteSync.ViewModels.Sessions.Synchronizations; -public class SynchronizationMainViewModel : ViewModelBase, IActivatableViewModel +public class SynchronizationMainViewModel : ActivatableViewModelBase { - private readonly ISessionService _sessionService; - private readonly ILocalizationService _localizationService; - private readonly ISynchronizationService _synchronizationService; - private readonly IDialogService _dialogService; - private readonly IAtomicActionRepository _atomicActionRepository; - private readonly ISessionMemberRepository _sessionMemberRepository; - private readonly ISynchronizationStarter _synchronizationStarter; - private readonly ISharedActionsGroupRepository _sharedActionsGroupRepository; - private readonly ITimeTrackingCache _timeTrackingCache; - private readonly ILogger _logger; - public SynchronizationMainViewModel() { -#if DEBUG - MainStatus = "MainStatus"; - IsMainProgressRingVisible = false; - IsMainCheckVisible = false; -#endif - - if (Design.IsDesignMode) - { - IsSynchronizationRunning = true; - IsMainCheckVisible = true; - } - - ProcessedVolume = 0; - ExchangedVolume = 0; - - EstimatedEndDateTimeLabel = Resources.SynchronizationMain_EstimatedEnd; } - public SynchronizationMainViewModel(ISessionService sessionService, ILocalizationService localizationService, - ISynchronizationService synchronizationService, IDialogService dialogService, IAtomicActionRepository atomicActionRepository, - ISessionMemberRepository sessionMemberRepository, ErrorViewModel errorViewModel, ISynchronizationStarter synchronizationStarter, - ISharedActionsGroupRepository sharedActionsGroupRepository, ITimeTrackingCache timeTrackingCache, ILogger logger) - : this() + public SynchronizationMainViewModel(SynchronizationBeforeStartViewModel beforeStartViewModel, + SynchronizationMainStatusViewModel mainStatusViewModel, SynchronizationStatisticsViewModel statisticsViewModel) { - Activator = new ViewModelActivator(); - - _sessionService = sessionService; - _localizationService = localizationService; - _synchronizationService = synchronizationService; - _dialogService = dialogService; - _atomicActionRepository = atomicActionRepository; - _sessionMemberRepository = sessionMemberRepository; - _synchronizationStarter = synchronizationStarter; - _sharedActionsGroupRepository = sharedActionsGroupRepository; - _timeTrackingCache = timeTrackingCache; - _logger = logger; - - MainStatus = Resources.SynchronizationMain_SynchronizationRunning; - IsSynchronizationRunning = false; + BeforeStartViewModel = beforeStartViewModel; + MainStatusViewModel = mainStatusViewModel; + StatisticsViewModel = statisticsViewModel; - StartSynchronizationError = errorViewModel; - var canStartSynchronization = _sessionService.SessionObservable - .CombineLatest( - _atomicActionRepository.ObservableCache.Connect().ToCollection(), - _sessionMemberRepository.IsCurrentUserFirstSessionMemberObservable, - (session, atomicActions, isCurrentUserFirstSessionMember) => - (Session: session, AtomicActions: atomicActions, IsCurrentUserFirstSessionMember: isCurrentUserFirstSessionMember)) - .DistinctUntilChanged() - .ObserveOn(RxApp.MainThreadScheduler) - .Select(tuple => tuple.Session is CloudSession && tuple.IsCurrentUserFirstSessionMember && tuple.AtomicActions.Count > 0); - - StartSynchronizationCommand = ReactiveCommand.CreateFromTask(StartSynchronization, canStartSynchronization); - - AbortSynchronizationCommand = ReactiveCommand.CreateFromTask(AbortSynchronization); - - - this.WhenAnyValue( - x => x.IsSynchronizationRunning, x => x.IsCloudSession, x => x.IsSessionCreatedByMe, x => x.HasSynchronizationStarted, - x => x.IsProfileSessionSynchronization, x => x.HasSessionBeenRestarted, - ComputeShowStartSynchronizationObservable) - .ToPropertyEx(this, x => x.ShowStartSynchronizationObservable); - - this.WhenAnyValue( - x => x.IsSynchronizationRunning, x => x.IsCloudSession, x => x.IsSessionCreatedByMe, x => x.HasSynchronizationStarted, - x => x.IsProfileSessionSynchronization, x => x.HasSessionBeenRestarted, - ComputeShowWaitingForSynchronizationStartObservable) - .ToPropertyEx(this, x => x.ShowWaitingForSynchronizationStartObservable); - this.WhenActivated(disposables => { - _synchronizationService.SynchronizationProcessData.SynchronizationStart - .CombineLatest(_synchronizationService.SynchronizationProcessData.SynchronizationAbortRequest, - _synchronizationService.SynchronizationProcessData.SynchronizationEnd) - .Where(tuple => tuple.First != null && tuple.Second == null && tuple.Third == null) - .ObserveOn(RxApp.MainThreadScheduler) - .Subscribe(tuple => OnSynchronizationStarted(tuple.First!)) - .DisposeWith(disposables); - - _synchronizationService.SynchronizationProcessData.SynchronizationDataTransmitted - .CombineLatest(_synchronizationService.SynchronizationProcessData.SynchronizationAbortRequest, - _synchronizationService.SynchronizationProcessData.SynchronizationEnd) - .Where(tuple => tuple.Second == null && tuple.Third == null) - .ObserveOn(RxApp.MainThreadScheduler) - .Subscribe(tuple => OnSynchronizationDataTransmitted(tuple.First)) - .DisposeWith(disposables); - - _synchronizationService.SynchronizationProcessData.SynchronizationAbortRequest.DistinctUntilChanged() - .Where(synchronizationAbortRequest => synchronizationAbortRequest != null) - .Subscribe(synchronizationAbortRequest => OnSynchronizationAbortRequested(synchronizationAbortRequest!)) - .DisposeWith(disposables); - - _synchronizationService.SynchronizationProcessData.SynchronizationEnd.DistinctUntilChanged() - .Where(synchronizationEnd => synchronizationEnd != null) - .Subscribe(synchronizationEnd => OnSynchronizationEnded(synchronizationEnd!)) - .DisposeWith(disposables); + BeforeStartViewModel.Activator.Activate().DisposeWith(disposables); + MainStatusViewModel.Activator.Activate().DisposeWith(disposables); + StatisticsViewModel.Activator.Activate().DisposeWith(disposables); - _sharedActionsGroupRepository.ObservableCache.Connect().ToCollection() - .Select(query => - { - var sum = query.Sum(ssa => ssa.Size.GetValueOrDefault()); - - return sum; - }) - .StartWith(0) - .ObserveOn(RxApp.MainThreadScheduler) - .ToPropertyEx(this, x => x.TotalVolume) - .DisposeWith(disposables); - - _synchronizationService.SynchronizationProcessData.SynchronizationProgress - .CombineLatest(_synchronizationService.SynchronizationProcessData.SynchronizationStart) - .Where(tuple => tuple.First != null && tuple.Second != null) - .ObserveOn(RxApp.MainThreadScheduler) - // .Where(sp => sp != null) - // .Select(sp => sp!) - .Select(tuple => tuple.First!) - .Subscribe(OnSynchronizationProgressChanged) - .DisposeWith(disposables); - - _sessionMemberRepository.IsCurrentUserFirstSessionMemberObservable - .Subscribe(b => IsSessionCreatedByMe = b); - - _sessionService.SessionStatusObservable - .CombineLatest(_synchronizationService.SynchronizationProcessData.SynchronizationStart) - .Select(tuple => - !tuple.First.In(SessionStatus.None, SessionStatus.Preparation, SessionStatus.Comparison, - SessionStatus.CloudSessionCreation, SessionStatus.CloudSessionJunction, SessionStatus.Inventory) - && tuple.Second != null) + this.WhenAnyValue(x => x.MainStatusViewModel.HasSynchronizationStarted) .ObserveOn(RxApp.MainThreadScheduler) .ToPropertyEx(this, x => x.HasSynchronizationStarted) .DisposeWith(disposables); - - if (Design.IsDesignMode) - { - return; - } - - IsCloudSession = _sessionService.IsCloudSession; - IsSessionCreatedByMe = _sessionMemberRepository.IsCurrentUserFirstSessionMemberCurrentValue; - IsProfileSessionSynchronization = _sessionService.CurrentRunSessionProfileInfo is { AutoStartsSynchronization: true }; - IsInventoryError = true; - - if (IsCloudSession) - { - if (IsProfileSessionSynchronization) - { - WaitingForSynchronizationStartMessage = _localizationService[nameof(Resources.SynchronizationMain_WaitingForAutomaticStart_CloudSession)]; - } - else if (!IsCloudSessionCreatedByMe) - { - var creatorMachineName = _sessionMemberRepository.Elements.First().MachineName; - WaitingForSynchronizationStartMessage = String.Format(_localizationService[nameof(Resources.SynchronizationMain_WaitingForClientATemplate)], creatorMachineName); - } - } - else - { - if (IsProfileSessionSynchronization) - { - WaitingForSynchronizationStartMessage = _localizationService[nameof(Resources.SynchronizationMain_WaitingForAutomaticStart_LocalSession)]; - } - } - - var timeTrackingComputer = _timeTrackingCache - .GetTimeTrackingComputer(_sessionService.SessionId!, TimeTrackingComputerType.Synchronization) - .Result; - timeTrackingComputer.RemainingTime - .Subscribe(remainingTime => - { - RemainingTime = remainingTime.RemainingTime; - ElapsedTime = remainingTime.ElapsedTime; - EstimatedEndDateTime = remainingTime.EstimatedEndDateTime; - StartDateTime = remainingTime.StartDateTime; - }) - .DisposeWith(disposables); }); - - IsMainProgressRingVisible = false; - IsMainCheckVisible = false; - } - - private bool ComputeShowStartSynchronizationObservable(bool isSynchronizationRunning, bool isCloudSession, bool isSessionCreatedByMe, - bool hasSynchronizationStarted, bool isProfileSessionSynchronization, bool hasSessionBeenRestarted) - { - var result = (!isProfileSessionSynchronization || (! isCloudSession && hasSessionBeenRestarted)) - && !isSynchronizationRunning - && !hasSynchronizationStarted - && (!isCloudSession || isSessionCreatedByMe); - - return result; - } - - private bool ComputeShowWaitingForSynchronizationStartObservable(bool isSynchronizationRunning, bool isCloudSession, bool isSessionCreatedByMe, - bool hasSynchronizationStarted, bool isProfileSessionSynchronization, bool hasSessionBeenRestarted) - { - var result = !isSynchronizationRunning - && !hasSynchronizationStarted - && ((isProfileSessionSynchronization && !hasSessionBeenRestarted) || (!isSessionCreatedByMe && isCloudSession)); - - return result; } - - public ViewModelActivator Activator { get; } - - public ReactiveCommand StartSynchronizationCommand { get; set; } - - public ReactiveCommand AbortSynchronizationCommand { get; set; } - - [Reactive] - public bool IsSynchronizationRunning { get; set; } - - [Reactive] - public bool IsMainProgressRingVisible { get; set; } - - [Reactive] - public bool IsMainCheckVisible { get; set; } - - [Reactive] - public string MainIcon { get; set; } - - [Reactive] - public bool IsCloudSession { get; set; } - - public bool IsCloudSessionCreatedByMe => IsCloudSession && IsSessionCreatedByMe; - - [Reactive] - public bool IsSessionCreatedByMe { get; set; } - - [Reactive] - public bool IsProfileSessionSynchronization { get; set; } - - [Reactive] - public bool IsInventoryError { get; set; } - - [Reactive] - public string MainStatus { get; set; } - - [Reactive] - public DateTime? StartDateTime { get; set; } - - [Reactive] - public TimeSpan ElapsedTime { get; set; } - - [Reactive] - public TimeSpan? RemainingTime { get; set; } - - [Reactive] - public string EstimatedEndDateTimeLabel { get; set; } - - [Reactive] - public DateTime? EstimatedEndDateTime { get; set; } - - [Reactive] - public long HandledActions { get; set; } - [Reactive] - public long? TreatableActions { get; set; } + public SynchronizationBeforeStartViewModel BeforeStartViewModel { get; } - [Reactive] - public long Errors { get; set; } - - [Reactive] - public long ProcessedVolume { get; set; } - - public extern long TotalVolume { [ObservableAsProperty] get; } + public SynchronizationMainStatusViewModel MainStatusViewModel { get; } - [Reactive] - public long ExchangedVolume { get; set; } + public SynchronizationStatisticsViewModel StatisticsViewModel { get; } - [Reactive] - public string WaitingForSynchronizationStartMessage { get; set; } - - [Reactive] - public ErrorViewModel StartSynchronizationError { get; set; } - - [Reactive] - public bool HasSessionBeenRestarted { get; set; } - - public extern bool ShowStartSynchronizationObservable { [ObservableAsProperty] get; } - - public extern bool ShowWaitingForSynchronizationStartObservable { [ObservableAsProperty] get; } - public extern bool HasSynchronizationStarted { [ObservableAsProperty] get; } - - private long? LastVersion { get; set; } - - private async Task StartSynchronization() - { - try - { - StartSynchronizationError.Clear(); - - await _synchronizationStarter.StartSynchronization(true); - } - catch (Exception ex) - { - Log.Error(ex, "SynchronizationMainViewModel.StartSynchronization"); - - StartSynchronizationError.SetException(ex); - } - } - - private async Task AbortSynchronization() - { - try - { - var messageBoxViewModel = _dialogService.CreateMessageBoxViewModel( - nameof(Resources.SynchronizationMain_AbortSynchronization_Title), nameof(Resources.SynchronizationMain_AbortSynchronization_Message)); - messageBoxViewModel.ShowYesNo = true; - var result = await _dialogService.ShowMessageBoxAsync(messageBoxViewModel); - - if (result == MessageBoxResult.Yes) - { - await _synchronizationService.AbortSynchronization(); - } - } - catch (Exception ex) - { - Log.Error(ex, "SynchronizationMainViewModel.AbortSynchronization"); - } - } - - private void OnSynchronizationStarted(Synchronization synchronizationStart) - { - StartDateTime = synchronizationStart.Started.LocalDateTime; - - MainStatus = Resources.SynchronizationMain_SynchronizationRunning; - - IsSynchronizationRunning = true; - - HandledActions = 0; - Errors = 0; - - ElapsedTime = TimeSpan.Zero; - - IsMainCheckVisible = false; - IsMainProgressRingVisible = true; - } - - private void OnSynchronizationDataTransmitted(bool tupleFirst) - { - TreatableActions = _synchronizationService.SynchronizationProcessData.TotalActionsToProcess; - } - - private void OnSynchronizationAbortRequested(SynchronizationAbortRequest synchronizationAbortRequest) - { - if (IsSynchronizationRunning) - { - MainStatus = Resources.SynchronizationMain_SynchronizationAbortRequested; - } - } - - private void OnSynchronizationEnded(SynchronizationEnd synchronizationEnd) - { - IsSynchronizationRunning = false; - - EstimatedEndDateTimeLabel = Resources.SynchronizationMain_End; - - var synchronizationProgress = _synchronizationService.SynchronizationProcessData.SynchronizationProgress.Value; - HandledActions = synchronizationProgress?.FinishedActionsCount ?? 0; - Errors = synchronizationProgress?.ErrorActionsCount ?? 0; - - if (synchronizationEnd.Status == SynchronizationEndStatuses.Abortion) - { - MainStatus = Resources.SynchronizationMain_SynchronizationAborted; - MainIcon = "SolidXCircle"; - } - else if (synchronizationEnd.Status == SynchronizationEndStatuses.Error) - { - MainStatus = Resources.SynchronizationMain_SynchronizationError; - MainIcon = "SolidXCircle"; - } - else - { - MainStatus = Resources.SynchronizationMain_SynchronizationDone; - MainIcon = "SolidCheckCircle"; - } - - IsMainProgressRingVisible = false; - IsMainCheckVisible = true; - } - - private void OnSynchronizationProgressChanged(SynchronizationProgress? synchronizationProgress) - { - if (synchronizationProgress == null) - { - return; - } - - if (LastVersion != null && LastVersion > synchronizationProgress.Version) - { - return; - } - - HandledActions = synchronizationProgress.FinishedActionsCount; - Errors = synchronizationProgress.ErrorActionsCount; - - ProcessedVolume = synchronizationProgress.ProcessedVolume; - ExchangedVolume = synchronizationProgress.ExchangedVolume; - LastVersion = synchronizationProgress.Version; - } -} \ No newline at end of file +} diff --git a/src/ByteSync.Client/ViewModels/Sessions/Synchronizations/SynchronizationStatisticsViewModel.cs b/src/ByteSync.Client/ViewModels/Sessions/Synchronizations/SynchronizationStatisticsViewModel.cs new file mode 100644 index 00000000..f6904c7f --- /dev/null +++ b/src/ByteSync.Client/ViewModels/Sessions/Synchronizations/SynchronizationStatisticsViewModel.cs @@ -0,0 +1,170 @@ +using System.Reactive.Disposables; +using System.Reactive.Linq; +using ByteSync.Assets.Resources; +using ByteSync.Business.Synchronizations; +using ByteSync.Common.Business.Synchronizations; +using ByteSync.Interfaces.Controls.Synchronizations; +using ByteSync.Interfaces.Controls.TimeTracking; +using ByteSync.Interfaces.Repositories; +using ByteSync.Interfaces.Services.Sessions; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using DynamicData; +using ByteSync.Business.Misc; + +namespace ByteSync.ViewModels.Sessions.Synchronizations; + +public class SynchronizationStatisticsViewModel : ActivatableViewModelBase +{ + private readonly ISynchronizationService _synchronizationService = null!; + private readonly ISessionService _sessionService = null!; + private readonly ISharedActionsGroupRepository _sharedActionsGroupRepository= null!; + private readonly ITimeTrackingCache _timeTrackingCache= null!; + + private long? LastVersion { get; set; } + + public SynchronizationStatisticsViewModel() + { + ProcessedVolume = 0; + ExchangedVolume = 0; + EstimatedEndDateTimeLabel = Resources.SynchronizationMain_EstimatedEnd; + } + + public SynchronizationStatisticsViewModel(ISynchronizationService synchronizationService, ISessionService sessionService, + ISharedActionsGroupRepository sharedActionsGroupRepository, ITimeTrackingCache timeTrackingCache) : this() + { + _synchronizationService = synchronizationService; + _sessionService = sessionService; + _sharedActionsGroupRepository = sharedActionsGroupRepository; + _timeTrackingCache = timeTrackingCache; + + this.WhenActivated(disposables => + { + _synchronizationService.SynchronizationProcessData.SynchronizationStart + .CombineLatest(_synchronizationService.SynchronizationProcessData.SynchronizationAbortRequest, + _synchronizationService.SynchronizationProcessData.SynchronizationEnd) + .Where(tuple => tuple.First != null && tuple.Second == null && tuple.Third == null) + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(tuple => OnSynchronizationStarted(tuple.First!)) + .DisposeWith(disposables); + + _synchronizationService.SynchronizationProcessData.SynchronizationDataTransmitted + .CombineLatest(_synchronizationService.SynchronizationProcessData.SynchronizationAbortRequest, + _synchronizationService.SynchronizationProcessData.SynchronizationEnd) + .Where(tuple => tuple.Second == null && tuple.Third == null) + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(tuple => OnSynchronizationDataTransmitted(tuple.First)) + .DisposeWith(disposables); + + _synchronizationService.SynchronizationProcessData.SynchronizationEnd.DistinctUntilChanged() + .Where(synchronizationEnd => synchronizationEnd != null) + .Subscribe(synchronizationEnd => OnSynchronizationEnded(synchronizationEnd!)) + .DisposeWith(disposables); + + _sharedActionsGroupRepository.ObservableCache.Connect().ToCollection() + .Select(query => query.Sum(ssa => ssa.Size.GetValueOrDefault())) + .StartWith(0L) + .ObserveOn(RxApp.MainThreadScheduler) + .ToPropertyEx(this, x => x.TotalVolume) + .DisposeWith(disposables); + + _synchronizationService.SynchronizationProcessData.SynchronizationProgress + .CombineLatest(_synchronizationService.SynchronizationProcessData.SynchronizationStart) + .Where(tuple => tuple.First != null && tuple.Second != null) + .ObserveOn(RxApp.MainThreadScheduler) + .Select(tuple => tuple.First!) + .Subscribe(OnSynchronizationProgressChanged) + .DisposeWith(disposables); + + var timeTrackingComputer = _timeTrackingCache + .GetTimeTrackingComputer(_sessionService.SessionId!, TimeTrackingComputerType.Synchronization) + .Result; + timeTrackingComputer.RemainingTime + .Subscribe(remainingTime => + { + RemainingTime = remainingTime.RemainingTime; + ElapsedTime = remainingTime.ElapsedTime; + EstimatedEndDateTime = remainingTime.EstimatedEndDateTime; + StartDateTime = remainingTime.StartDateTime; + }) + .DisposeWith(disposables); + }); + + IsCloudSession = _sessionService.IsCloudSession; + } + + [Reactive] + public DateTime? StartDateTime { get; set; } + + [Reactive] + public TimeSpan ElapsedTime { get; set; } + + [Reactive] + public TimeSpan? RemainingTime { get; set; } + + [Reactive] + public string EstimatedEndDateTimeLabel { get; set; } + + [Reactive] + public DateTime? EstimatedEndDateTime { get; set; } + + [Reactive] + public long HandledActions { get; set; } + + [Reactive] + public long? TreatableActions { get; set; } + + [Reactive] + public long Errors { get; set; } + + [Reactive] + public long ProcessedVolume { get; set; } + + public extern long TotalVolume { [ObservableAsProperty] get; } + + [Reactive] + public long ExchangedVolume { get; set; } + + [Reactive] + public bool IsCloudSession { get; set; } + + private void OnSynchronizationStarted(Synchronization synchronizationStart) + { + StartDateTime = synchronizationStart.Started.LocalDateTime; + HandledActions = 0; + Errors = 0; + ElapsedTime = TimeSpan.Zero; + } + + private void OnSynchronizationDataTransmitted(bool _) + { + TreatableActions = _synchronizationService.SynchronizationProcessData.TotalActionsToProcess; + } + + private void OnSynchronizationEnded(SynchronizationEnd _) + { + EstimatedEndDateTimeLabel = Resources.SynchronizationMain_End; + var synchronizationProgress = _synchronizationService.SynchronizationProcessData.SynchronizationProgress.Value; + HandledActions = synchronizationProgress?.FinishedActionsCount ?? 0; + Errors = synchronizationProgress?.ErrorActionsCount ?? 0; + } + + private void OnSynchronizationProgressChanged(SynchronizationProgress? synchronizationProgress) + { + if (synchronizationProgress == null) + { + return; + } + + if (LastVersion != null && LastVersion > synchronizationProgress.Version) + { + return; + } + + HandledActions = synchronizationProgress.FinishedActionsCount; + Errors = synchronizationProgress.ErrorActionsCount; + ProcessedVolume = synchronizationProgress.ProcessedVolume; + ExchangedVolume = synchronizationProgress.ExchangedVolume; + LastVersion = synchronizationProgress.Version; + } +} diff --git a/src/ByteSync.Client/Views/Sessions/Synchronizations/SynchronizationBeforeStartView.axaml b/src/ByteSync.Client/Views/Sessions/Synchronizations/SynchronizationBeforeStartView.axaml new file mode 100644 index 00000000..d308a909 --- /dev/null +++ b/src/ByteSync.Client/Views/Sessions/Synchronizations/SynchronizationBeforeStartView.axaml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/ByteSync.Client/Views/Sessions/Synchronizations/SynchronizationBeforeStartView.axaml.cs b/src/ByteSync.Client/Views/Sessions/Synchronizations/SynchronizationBeforeStartView.axaml.cs new file mode 100644 index 00000000..362653e0 --- /dev/null +++ b/src/ByteSync.Client/Views/Sessions/Synchronizations/SynchronizationBeforeStartView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.ReactiveUI; +using ByteSync.ViewModels.Sessions.Synchronizations; + +namespace ByteSync.Views.Sessions.Synchronizations; + +public partial class SynchronizationBeforeStartView : ReactiveUserControl +{ + public SynchronizationBeforeStartView() + { + InitializeComponent(); + } +} diff --git a/src/ByteSync.Client/Views/Sessions/Synchronizations/SynchronizationMainStatusView.axaml b/src/ByteSync.Client/Views/Sessions/Synchronizations/SynchronizationMainStatusView.axaml new file mode 100644 index 00000000..9610c199 --- /dev/null +++ b/src/ByteSync.Client/Views/Sessions/Synchronizations/SynchronizationMainStatusView.axaml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/ByteSync.Client/Views/Sessions/Synchronizations/SynchronizationMainStatusView.axaml.cs b/src/ByteSync.Client/Views/Sessions/Synchronizations/SynchronizationMainStatusView.axaml.cs new file mode 100644 index 00000000..8cb1c206 --- /dev/null +++ b/src/ByteSync.Client/Views/Sessions/Synchronizations/SynchronizationMainStatusView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.ReactiveUI; +using ByteSync.ViewModels.Sessions.Synchronizations; + +namespace ByteSync.Views.Sessions.Synchronizations; + +public partial class SynchronizationMainStatusView : ReactiveUserControl +{ + public SynchronizationMainStatusView() + { + InitializeComponent(); + } +} diff --git a/src/ByteSync.Client/Views/Sessions/Synchronizations/SynchronizationMainView.axaml b/src/ByteSync.Client/Views/Sessions/Synchronizations/SynchronizationMainView.axaml index 72226b12..a3740fbe 100644 --- a/src/ByteSync.Client/Views/Sessions/Synchronizations/SynchronizationMainView.axaml +++ b/src/ByteSync.Client/Views/Sessions/Synchronizations/SynchronizationMainView.axaml @@ -1,172 +1,30 @@ - - + - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/src/ByteSync.Client/Views/Sessions/Synchronizations/SynchronizationMainView.axaml.cs b/src/ByteSync.Client/Views/Sessions/Synchronizations/SynchronizationMainView.axaml.cs index 46f15af5..d1539388 100644 --- a/src/ByteSync.Client/Views/Sessions/Synchronizations/SynchronizationMainView.axaml.cs +++ b/src/ByteSync.Client/Views/Sessions/Synchronizations/SynchronizationMainView.axaml.cs @@ -1,6 +1,5 @@ -using Avalonia.ReactiveUI; +using Avalonia.ReactiveUI; using ByteSync.ViewModels.Sessions.Synchronizations; -using ReactiveUI; namespace ByteSync.Views.Sessions.Synchronizations; @@ -9,11 +8,5 @@ public partial class SynchronizationMainView : ReactiveUserControl - { - - - - }); } -} \ No newline at end of file +} diff --git a/src/ByteSync.Client/Views/Sessions/Synchronizations/SynchronizationStatisticsView.axaml b/src/ByteSync.Client/Views/Sessions/Synchronizations/SynchronizationStatisticsView.axaml new file mode 100644 index 00000000..a411c6cf --- /dev/null +++ b/src/ByteSync.Client/Views/Sessions/Synchronizations/SynchronizationStatisticsView.axaml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ByteSync.Client/Views/Sessions/Synchronizations/SynchronizationStatisticsView.axaml.cs b/src/ByteSync.Client/Views/Sessions/Synchronizations/SynchronizationStatisticsView.axaml.cs new file mode 100644 index 00000000..5223fa1e --- /dev/null +++ b/src/ByteSync.Client/Views/Sessions/Synchronizations/SynchronizationStatisticsView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.ReactiveUI; +using ByteSync.ViewModels.Sessions.Synchronizations; + +namespace ByteSync.Views.Sessions.Synchronizations; + +public partial class SynchronizationStatisticsView : ReactiveUserControl +{ + public SynchronizationStatisticsView() + { + InitializeComponent(); + } +} diff --git a/tests/ByteSync.Client.Tests/ViewModels/Sessions/Synchronizations/TestSynchronizationBeforeStartViewModel.cs b/tests/ByteSync.Client.Tests/ViewModels/Sessions/Synchronizations/TestSynchronizationBeforeStartViewModel.cs new file mode 100644 index 00000000..3bd548a5 --- /dev/null +++ b/tests/ByteSync.Client.Tests/ViewModels/Sessions/Synchronizations/TestSynchronizationBeforeStartViewModel.cs @@ -0,0 +1,161 @@ +using ByteSync.Business.Actions.Local; +using ByteSync.Business.SessionMembers; +using ByteSync.Business.Sessions; +using ByteSync.Business.Sessions.RunSessionInfos; +using ByteSync.Business.Synchronizations; +using ByteSync.Common.Business.Sessions; +using ByteSync.Interfaces.Controls.Synchronizations; +using ByteSync.Interfaces.Repositories; +using ByteSync.Interfaces.Services.Localizations; +using ByteSync.Interfaces.Services.Sessions; +using ByteSync.TestsCommon; +using ByteSync.ViewModels.Misc; +using ByteSync.ViewModels.Sessions.Synchronizations; +using DynamicData; +using Moq; +using NUnit.Framework; +using FluentAssertions; +using System.Reactive.Linq; +using Microsoft.Extensions.Logging; + +namespace ByteSync.Tests.ViewModels.Sessions.Synchronizations; + +[TestFixture] +public class TestSynchronizationBeforeStartViewModel : AbstractTester +{ + private SynchronizationBeforeStartViewModel _viewModel; + private Mock _synchronizationStarter; + private Mock _localizationService; + private Mock> _logger; + + [SetUp] + public void SetUp() + { + var sessionService = new Mock(); + sessionService.SetupGet(s => s.SessionObservable).Returns(Observable.Return(null)); + sessionService.SetupGet(s => s.SessionStatusObservable).Returns(Observable.Return(SessionStatus.None)); + sessionService.SetupGet(s => s.IsCloudSession).Returns(false); + sessionService.SetupGet(s => s.CurrentRunSessionProfileInfo).Returns((AbstractRunSessionProfileInfo?)null); + + var atomicCache = new SourceCache(a => a.AtomicActionId); + var atomicRepository = new Mock(); + atomicRepository.SetupGet(r => r.ObservableCache).Returns(atomicCache); + + var sessionMemberRepository = new Mock(); + sessionMemberRepository.SetupGet(r => r.IsCurrentUserFirstSessionMemberObservable).Returns(Observable.Return(true)); + sessionMemberRepository.SetupGet(r => r.IsCurrentUserFirstSessionMemberCurrentValue).Returns(true); + sessionMemberRepository.SetupGet(r => r.Elements).Returns(new List()); + + var synchronizationService = new Mock(); + synchronizationService.SetupGet(s => s.SynchronizationProcessData).Returns(new SynchronizationProcessData()); + + _localizationService = new Mock(); + _localizationService.Setup(l => l["ErrorView_ErrorMessage"]).Returns("Error {0}"); + + _logger = new Mock>(); + + _synchronizationStarter = new Mock(); + var errorViewModel = new ErrorViewModel(_localizationService.Object); + + _viewModel = new SynchronizationBeforeStartViewModel(sessionService.Object, _localizationService.Object, + synchronizationService.Object, _synchronizationStarter.Object, atomicRepository.Object, + sessionMemberRepository.Object, _logger.Object, errorViewModel); + } + + [Test] + public void Test_Construction() + { + _viewModel.StartSynchronizationCommand.Should().NotBeNull(); + } + + [Test] + public void ShowStartSynchronizationObservable_ShouldReflectConditions() + { + using var _ = _viewModel.Activator.Activate(); + + _viewModel.IsSynchronizationRunning = false; + _viewModel.IsCloudSession = false; + _viewModel.IsSessionCreatedByMe = true; + _viewModel.IsProfileSessionSynchronization = false; + _viewModel.HasSessionBeenRestarted = false; + + _viewModel.ShowStartSynchronizationObservable.Should().BeTrue(); + + _viewModel.IsSynchronizationRunning = true; + _viewModel.ShowStartSynchronizationObservable.Should().BeFalse(); + } + + [Test] + public void ShowWaitingForSynchronizationStartObservable_ShouldBeTrue_ForProfileSession() + { + using var _ = _viewModel.Activator.Activate(); + + _viewModel.IsSynchronizationRunning = false; + _viewModel.IsCloudSession = false; + _viewModel.IsSessionCreatedByMe = true; + _viewModel.IsProfileSessionSynchronization = true; + _viewModel.HasSessionBeenRestarted = false; + + _viewModel.ShowWaitingForSynchronizationStartObservable.Should().BeTrue(); + } + + [Test] + public void ShowStartSynchronizationObservable_ShouldBeFalse_ForProfileSessionWithoutRestart() + { + using var _ = _viewModel.Activator.Activate(); + + _viewModel.IsSynchronizationRunning = false; + _viewModel.IsCloudSession = false; + _viewModel.IsSessionCreatedByMe = true; + _viewModel.IsProfileSessionSynchronization = true; + _viewModel.HasSessionBeenRestarted = false; + + _viewModel.ShowStartSynchronizationObservable.Should().BeFalse(); + } + + [Test] + public void ShowStartSynchronizationObservable_ShouldBeTrue_ForRestartedProfileSession() + { + using var _ = _viewModel.Activator.Activate(); + + _viewModel.IsSynchronizationRunning = false; + _viewModel.IsCloudSession = false; + _viewModel.IsSessionCreatedByMe = true; + _viewModel.IsProfileSessionSynchronization = true; + _viewModel.HasSessionBeenRestarted = true; + + _viewModel.ShowStartSynchronizationObservable.Should().BeTrue(); + } + + [Test] + public void ShowWaitingForSynchronizationStartObservable_ShouldBeTrue_ForCloudSessionNotCreatedByMe() + { + using var _ = _viewModel.Activator.Activate(); + + _viewModel.IsSynchronizationRunning = false; + _viewModel.IsCloudSession = true; + _viewModel.IsSessionCreatedByMe = false; + _viewModel.IsProfileSessionSynchronization = false; + _viewModel.HasSessionBeenRestarted = false; + + _viewModel.ShowWaitingForSynchronizationStartObservable.Should().BeTrue(); + } + + [Test] + public async Task StartSynchronizationCommand_ShouldStartProcess() + { + await _viewModel.StartSynchronizationCommand.Execute(); + _synchronizationStarter.Verify(s => s.StartSynchronization(true), Times.Once); + _viewModel.StartSynchronizationError.ErrorMessage.Should().BeNull(); + } + + [Test] + public async Task StartSynchronizationCommand_ShouldHandleException() + { + _synchronizationStarter.Setup(s => s.StartSynchronization(true)).ThrowsAsync(new InvalidOperationException()); + + await _viewModel.StartSynchronizationCommand.Execute(); + + _viewModel.StartSynchronizationError.ErrorMessage.Should().NotBeNull(); + } +} diff --git a/tests/ByteSync.Client.Tests/ViewModels/Sessions/Synchronizations/TestSynchronizationMainStatusViewModel.cs b/tests/ByteSync.Client.Tests/ViewModels/Sessions/Synchronizations/TestSynchronizationMainStatusViewModel.cs new file mode 100644 index 00000000..cf618a77 --- /dev/null +++ b/tests/ByteSync.Client.Tests/ViewModels/Sessions/Synchronizations/TestSynchronizationMainStatusViewModel.cs @@ -0,0 +1,149 @@ +using ByteSync.Business; +using ByteSync.Business.Sessions; +using ByteSync.Business.Synchronizations; +using ByteSync.Common.Business.Synchronizations; +using ByteSync.Interfaces.Controls.Synchronizations; +using ByteSync.Interfaces.Dialogs; +using ByteSync.Interfaces.Services.Sessions; +using ByteSync.TestsCommon; +using ByteSync.ViewModels.Misc; +using ByteSync.ViewModels.Sessions.Synchronizations; +using Moq; +using NUnit.Framework; +using FluentAssertions; +using System.Reactive.Linq; +using Microsoft.Extensions.Logging; + +namespace ByteSync.Tests.ViewModels.Sessions.Synchronizations; + +[TestFixture] +public class TestSynchronizationMainStatusViewModel : AbstractTester +{ + private SynchronizationMainStatusViewModel _viewModel = null!; + private SynchronizationProcessData _processData = null!; + private Mock _synchronizationService = null!; + private Mock _dialogService = null!; + private Mock> _logger = null!; + + [SetUp] + public void SetUp() + { + var sessionService = new Mock(); + sessionService.SetupGet(s => s.SessionStatusObservable).Returns(Observable.Return(SessionStatus.None)); + + _synchronizationService = new Mock(); + _processData = new SynchronizationProcessData(); + _synchronizationService.SetupGet(s => s.SynchronizationProcessData).Returns(_processData); + + _dialogService = new Mock(); + _logger = new Mock>(); + + _viewModel = new SynchronizationMainStatusViewModel(sessionService.Object, _synchronizationService.Object, + _dialogService.Object, _logger.Object); + } + + [Test] + public void Test_Construction() + { + _viewModel.AbortSynchronizationCommand.Should().NotBeNull(); + } + + [Test] + public void OnSynchronizationStarted_ShouldUpdateState() + { + using var _ = _viewModel.Activator.Activate(); + + _processData.SynchronizationStart.OnNext(new Synchronization { Started = DateTimeOffset.Now }); + + _viewModel.IsSynchronizationRunning.Should().BeTrue(); + _viewModel.IsMainProgressRingVisible.Should().BeTrue(); + _viewModel.IsMainCheckVisible.Should().BeFalse(); + _viewModel.MainStatus.Should().Be("Synchronization running"); + } + + [Test] + public void OnSynchronizationEnded_ShouldShowFinalStatus() + { + using var _ = _viewModel.Activator.Activate(); + + _processData.SynchronizationStart.OnNext(new Synchronization { Started = DateTimeOffset.Now }); + _processData.SynchronizationEnd.OnNext(new SynchronizationEnd + { + FinishedOn = DateTimeOffset.Now, + Status = SynchronizationEndStatuses.Abortion + }); + + _viewModel.IsSynchronizationRunning.Should().BeFalse(); + _viewModel.MainStatus.Should().Be("Synchronization aborted"); + _viewModel.MainIcon.Should().Be("SolidXCircle"); + _viewModel.IsMainProgressRingVisible.Should().BeFalse(); + _viewModel.IsMainCheckVisible.Should().BeTrue(); + } + + [Test] + public void OnSynchronizationAbortRequested_ShouldUpdateStatus() + { + using var _ = _viewModel.Activator.Activate(); + + _processData.SynchronizationStart.OnNext(new Synchronization { Started = DateTimeOffset.Now }); + _processData.SynchronizationAbortRequest.OnNext(new SynchronizationAbortRequest()); + + _viewModel.MainStatus.Should().Be("Synchronization abort requested"); + } + + [Test] + public void OnSynchronizationEnded_ShouldShowErrorStatus() + { + using var _ = _viewModel.Activator.Activate(); + + _processData.SynchronizationStart.OnNext(new Synchronization { Started = DateTimeOffset.Now }); + _processData.SynchronizationEnd.OnNext(new SynchronizationEnd + { + FinishedOn = DateTimeOffset.Now, + Status = SynchronizationEndStatuses.Error + }); + + _viewModel.MainStatus.Should().Be("Error during synchronization"); + _viewModel.MainIcon.Should().Be("SolidXCircle"); + } + + [Test] + public void OnSynchronizationEnded_ShouldShowDoneStatus() + { + using var _ = _viewModel.Activator.Activate(); + + _processData.SynchronizationStart.OnNext(new Synchronization { Started = DateTimeOffset.Now }); + _processData.SynchronizationEnd.OnNext(new SynchronizationEnd + { + FinishedOn = DateTimeOffset.Now, + Status = SynchronizationEndStatuses.Regular + }); + + _viewModel.MainStatus.Should().Be("Synchronization done!"); + _viewModel.MainIcon.Should().Be("SolidCheckCircle"); + } + + [Test] + public async Task AbortSynchronizationCommand_ShouldAbort_WhenConfirmed() + { + var messageBox = new MessageBoxViewModel(); + _dialogService.Setup(d => d.CreateMessageBoxViewModel(It.IsAny(), It.IsAny(), It.IsAny())).Returns(messageBox); + _dialogService.Setup(d => d.ShowMessageBoxAsync(messageBox)).ReturnsAsync(MessageBoxResult.Yes); + + await _viewModel.AbortSynchronizationCommand.Execute(); + + _synchronizationService.Verify(s => s.AbortSynchronization(), Times.Once); + } + + [Test] + public async Task AbortSynchronizationCommand_ShouldNotAbort_WhenCancelled() + { + var messageBox = new MessageBoxViewModel(); + _dialogService.Setup(d => d.CreateMessageBoxViewModel(It.IsAny(), It.IsAny(), It.IsAny())).Returns(messageBox); + _dialogService.Setup(d => d.ShowMessageBoxAsync(messageBox)).ReturnsAsync(MessageBoxResult.No); + + await _viewModel.AbortSynchronizationCommand.Execute(); + + _synchronizationService.Verify(s => s.AbortSynchronization(), Times.Never); + } +} diff --git a/tests/ByteSync.Client.Tests/ViewModels/Sessions/Synchronizations/TestSynchronizationStatisticsViewModel.cs b/tests/ByteSync.Client.Tests/ViewModels/Sessions/Synchronizations/TestSynchronizationStatisticsViewModel.cs new file mode 100644 index 00000000..cc42c213 --- /dev/null +++ b/tests/ByteSync.Client.Tests/ViewModels/Sessions/Synchronizations/TestSynchronizationStatisticsViewModel.cs @@ -0,0 +1,125 @@ +using ByteSync.Business.Actions.Shared; +using ByteSync.Business.Synchronizations; +using ByteSync.Common.Business.Synchronizations; +using ByteSync.Business.Misc; +using ByteSync.Interfaces.Controls.Synchronizations; +using ByteSync.Interfaces.Controls.TimeTracking; +using ByteSync.Interfaces.Repositories; +using ByteSync.Interfaces.Services.Sessions; +using ByteSync.TestsCommon; +using ByteSync.ViewModels.Sessions.Synchronizations; +using DynamicData; +using Moq; +using NUnit.Framework; +using FluentAssertions; +using System.Reactive.Subjects; + +namespace ByteSync.Tests.ViewModels.Sessions.Synchronizations; + +[TestFixture] +public class TestSynchronizationStatisticsViewModel : AbstractTester +{ + private SynchronizationStatisticsViewModel _viewModel = null!; + private SynchronizationProcessData _processData = null!; + private BehaviorSubject _timeTrackSubject = null!; + + [SetUp] + public void SetUp() + { + var synchronizationService = new Mock(); + _processData = new SynchronizationProcessData(); + synchronizationService.SetupGet(s => s.SynchronizationProcessData).Returns(_processData); + + var sessionService = new Mock(); + sessionService.SetupGet(s => s.IsCloudSession).Returns(false); + sessionService.SetupGet(s => s.SessionId).Returns("id"); + + var sharedActionsCache = new SourceCache(s => s.ActionsGroupId); + var sharedActionsRepository = new Mock(); + sharedActionsRepository.SetupGet(r => r.ObservableCache).Returns(sharedActionsCache); + + var timeTrackingCache = new Mock(); + var track = new TimeTrack(); + track.Reset(DateTime.Now); + _timeTrackSubject = new BehaviorSubject(track); + var timeTrackingComputer = new Mock(); + timeTrackingComputer.Setup(t => t.RemainingTime).Returns(_timeTrackSubject); + timeTrackingCache.Setup(c => c.GetTimeTrackingComputer("id", TimeTrackingComputerType.Synchronization)) + .ReturnsAsync(timeTrackingComputer.Object); + + _viewModel = new SynchronizationStatisticsViewModel(synchronizationService.Object, sessionService.Object, + sharedActionsRepository.Object, timeTrackingCache.Object); + } + + [Test] + public void Test_Construction() + { + _viewModel.EstimatedEndDateTimeLabel.Should().NotBeNull(); + } + + [Test] + public void OnSynchronizationStarted_ShouldInitializeCounters() + { + using var _ = _viewModel.Activator.Activate(); + + var start = new Synchronization { Started = DateTimeOffset.Now }; + _processData.SynchronizationStart.OnNext(start); + + _viewModel.StartDateTime.Should().Be(start.Started.LocalDateTime); + _viewModel.HandledActions.Should().Be(0); + _viewModel.Errors.Should().Be(0); + _viewModel.ElapsedTime.Should().Be(TimeSpan.Zero); + } + + [Test] + public void OnSynchronizationDataTransmitted_ShouldSetTreatableActions() + { + using var _ = _viewModel.Activator.Activate(); + + _processData.TotalActionsToProcess = 42; + _processData.SynchronizationDataTransmitted.OnNext(true); + + _viewModel.TreatableActions.Should().Be(42); + } + + [Test] + public void OnSynchronizationProgressChanged_ShouldUpdateValues() + { + using var _ = _viewModel.Activator.Activate(); + + _processData.SynchronizationStart.OnNext(new Synchronization { Started = DateTimeOffset.Now }); + + var progress = new SynchronizationProgress + { + Version = 1, + FinishedActionsCount = 5, + ErrorActionsCount = 1, + ProcessedVolume = 10, + ExchangedVolume = 20 + }; + _processData.SynchronizationProgress.OnNext(progress); + + _viewModel.HandledActions.Should().Be(5); + _viewModel.Errors.Should().Be(1); + _viewModel.ProcessedVolume.Should().Be(10); + _viewModel.ExchangedVolume.Should().Be(20); + } + + [Test] + public void OnSynchronizationEnded_ShouldSetFinalValues() + { + using var _ = _viewModel.Activator.Activate(); + + var progress = new SynchronizationProgress { Version = 1, FinishedActionsCount = 3, ErrorActionsCount = 1 }; + _processData.SynchronizationProgress.OnNext(progress); + _processData.SynchronizationEnd.OnNext(new SynchronizationEnd + { + FinishedOn = DateTimeOffset.Now, + Status = SynchronizationEndStatuses.Regular + }); + + _viewModel.EstimatedEndDateTimeLabel.Should().Be("End:"); + _viewModel.HandledActions.Should().Be(3); + _viewModel.Errors.Should().Be(1); + } +} diff --git a/tests/ByteSync.TestsCommon/AbstractTester.cs b/tests/ByteSync.TestsCommon/AbstractTester.cs index 307f25b1..3345016a 100644 --- a/tests/ByteSync.TestsCommon/AbstractTester.cs +++ b/tests/ByteSync.TestsCommon/AbstractTester.cs @@ -1,4 +1,5 @@ using ByteSync.Common.Helpers; +using System.Globalization; using NUnit.Framework; namespace ByteSync.TestsCommon; @@ -22,6 +23,16 @@ protected AbstractTester() Console.SetOut(TestContext.Progress); } + [OneTimeSetUp] + public void ForceEnglishCulture() + { + var cultureInfo = new CultureInfo("en"); + CultureInfo.DefaultThreadCurrentCulture = cultureInfo; + CultureInfo.DefaultThreadCurrentUICulture = cultureInfo; + Thread.CurrentThread.CurrentCulture = cultureInfo; + Thread.CurrentThread.CurrentUICulture = cultureInfo; + } + protected virtual void CreateTestDirectory() { lock (_staticSyncRoot)