diff --git a/WheelWizard.Test/Features/CustomDistributions/RetroRewindTests.cs b/WheelWizard.Test/Features/CustomDistributions/RetroRewindTests.cs new file mode 100644 index 00000000..ea75c5a2 --- /dev/null +++ b/WheelWizard.Test/Features/CustomDistributions/RetroRewindTests.cs @@ -0,0 +1,121 @@ +using System.Linq.Expressions; +using Microsoft.Extensions.Logging; +using Testably.Abstractions.Testing; +using WheelWizard.CustomDistributions; +using WheelWizard.CustomDistributions.Domain; +using WheelWizard.Services; +using WheelWizard.Settings; +using WheelWizard.Shared; +using WheelWizard.Shared.Services; + +namespace WheelWizard.Test.Features.CustomDistributions; + +[Collection("SettingsFeature")] +public class RetroRewindTests +{ + private readonly MockFileSystem _fileSystem = new(); + private readonly IApiCaller _api = Substitute.For>(); + private readonly RetroRewind _distribution; + + public RetroRewindTests() + { + InitializeUserFolder(); + _distribution = new RetroRewind(_fileSystem, _api, Substitute.For>(), Substitute.For()); + } + + [Fact] + public async Task ReinstallAsync_KeepsInstallAndPatches_WhenTheServerIsUnreachable() + { + CreateExistingInstall(); + ServerIsUnreachable(); + + var result = await _distribution.ReinstallAsync(null!); + + Assert.True(result.IsFailure); + Assert.Equal("6.0.0", _fileSystem.File.ReadAllText(VersionFilePath)); + Assert.Equal("patch", _fileSystem.File.ReadAllText(PatchFilePath)); + Assert.True(_fileSystem.File.Exists(DiscXmlFilePath)); + Assert.False(_fileSystem.Directory.Exists(BackupPath)); + } + + [Fact] + public async Task ReinstallAsync_ReplacesTheBackupOfAPreviousInterruptedInstall() + { + CreateExistingInstall(); + _fileSystem.Directory.CreateDirectory(BackupPath); + var leftoverFilePath = _fileSystem.Path.Combine(BackupPath, "leftover.txt"); + _fileSystem.File.WriteAllText(leftoverFilePath, "leftover"); + ServerIsUnreachable(); + + var result = await _distribution.ReinstallAsync(null!); + + Assert.True(result.IsFailure); + Assert.Equal("patch", _fileSystem.File.ReadAllText(PatchFilePath)); + Assert.False(_fileSystem.File.Exists(leftoverFilePath)); + Assert.False(_fileSystem.Directory.Exists(BackupPath)); + } + + [Fact] + public async Task RemoveAsync_DeletesTheInstallAndItsDiscXmlFile() + { + CreateExistingInstall(); + + var result = await _distribution.RemoveAsync(null!); + + Assert.True(result.IsSuccess); + Assert.False(_fileSystem.Directory.Exists(InstallPath)); + Assert.False(_fileSystem.File.Exists(DiscXmlFilePath)); + } + + [Fact] + public async Task RemoveAsync_ReturnsFailure_WhenTheInstallCannotBeDeleted() + { + CreateExistingInstall(); + _fileSystem.Intercept.Event(_ => throw new IOException("The install is in use.")); + + var result = await _distribution.RemoveAsync(null!); + + Assert.True(result.IsFailure); + Assert.Contains("Failed to remove", result.Error.Message); + Assert.True(_fileSystem.File.Exists(VersionFilePath)); + } + + private static string InstallPath => Path.Combine(PathManager.RiivolutionWhWzFolderPath, "RetroRewind6"); + private static string BackupPath => Path.Combine(PathManager.RiivolutionWhWzFolderPath, "RetroRewind6.old"); + private static string VersionFilePath => Path.Combine(InstallPath, "version.txt"); + private static string PatchFilePath => Path.Combine(PathManager.PatchesFolderPath, "MyPatch.szs"); + private static string DiscXmlFilePath => Path.Combine(PathManager.RiivolutionXmlFolderPath, "RetroRewind6.xml"); + + private void InitializeUserFolder() + { + var settingsManager = new SettingsManager( + Substitute.For(), + Substitute.For(), + _fileSystem + ); +#pragma warning disable CS0618 + SettingsRuntime.Initialize(settingsManager); +#pragma warning restore CS0618 + + var userFolderPath = $"/wheelwizard-user-{Guid.NewGuid():N}"; + var loadFolderPath = _fileSystem.Path.Combine(userFolderPath, "Load"); + _fileSystem.Directory.CreateDirectory(loadFolderPath); + Assert.True(settingsManager.Set(settingsManager.USER_FOLDER_PATH, userFolderPath, skipSave: true)); + Assert.True(settingsManager.Set(settingsManager.LOAD_PATH, loadFolderPath, skipSave: true)); + } + + private void CreateExistingInstall() + { + _fileSystem.Directory.CreateDirectory(PathManager.PatchesFolderPath); + _fileSystem.Directory.CreateDirectory(PathManager.RiivolutionXmlFolderPath); + _fileSystem.File.WriteAllText(VersionFilePath, "6.0.0"); + _fileSystem.File.WriteAllText(PatchFilePath, "patch"); + _fileSystem.File.WriteAllText(DiscXmlFilePath, ""); + } + + private void ServerIsUnreachable() + { + _api.CallApiAsync(Arg.Any>>>()) + .Returns(Task.FromResult>(Fail("Server offline"))); + } +} diff --git a/WheelWizard/Features/CustomDistributions/RetroRewind.cs b/WheelWizard/Features/CustomDistributions/RetroRewind.cs index f030f642..f1836950 100644 --- a/WheelWizard/Features/CustomDistributions/RetroRewind.cs +++ b/WheelWizard/Features/CustomDistributions/RetroRewind.cs @@ -41,15 +41,47 @@ ISettingsManager settingsManager public string XMLFolderName => "riivolution"; public string XMLFileName => "RetroRewind6"; + //where the RR distribution lives + private string DistributionDataPath => _fileSystem.Path.Combine(PathManager.RiivolutionWhWzFolderPath, FolderName); + + //where the RR wiiDisc xml file lives + private string RiivolutionDiscXmlPath => + _fileSystem.Path.Combine(PathManager.RiivolutionWhWzFolderPath, XMLFolderName, $"{XMLFileName}.xml"); + + //where the previous install is kept while we install a new one + private string BackupDataPath => $"{DistributionDataPath}.old"; + private string BackupDiscXmlPath => $"{RiivolutionDiscXmlPath}.old"; + public async Task InstallAsync(ProgressWindow progressWindow) { - if (GetCurrentVersion() is not null) + // Instead of deleting the current install, we keep it around until the new install succeeded. + // That way a failed install does not take the users install (including their patches) with it. + var backupResult = BackupCurrentInstall(); + if (backupResult.IsFailure) + return backupResult; + + var installResult = await PerformInstallAsync(progressWindow); + // A cancelled install is not a failure, but it also did not finish, so we keep the old install. + if (installResult.IsFailure || progressWindow.WasCancellationRequested) { - var removeResult = await RemoveAsync(progressWindow); - if (removeResult.IsFailure) - return removeResult; + if (backupResult.Value) + { + var restoreResult = RestoreBackup(); + if (restoreResult.IsFailure) + _logger.LogError("Failed to restore the previous {Title} install: {Error}", Title, restoreResult.Error.Message); + } + return installResult; } + var cleanupResult = TryCatch(DeleteBackup); + if (cleanupResult.IsFailure) + _logger.LogWarning("Failed to delete the previous {Title} install: {Error}", Title, cleanupResult.Error.Message); + + return installResult; + } + + private async Task PerformInstallAsync(ProgressWindow progressWindow) + { if (HasOldRksys()) { var rksysQuestion = new YesNoWindow() @@ -543,29 +575,81 @@ List allDeletions public Task RemoveAsync(ProgressWindow progressWindow) { - //where the RR distribution lives - var distributionDataDestination = _fileSystem.Path.Combine(PathManager.RiivolutionWhWzFolderPath, FolderName); - //where the RR wiiDisc xml file lives - var riivolutionDiscXMLFile = _fileSystem.Path.Combine(PathManager.RiivolutionWhWzFolderPath, XMLFolderName, $"{XMLFileName}.xml"); + var result = TryCatch( + () => + { + if (_fileSystem.Directory.Exists(DistributionDataPath)) + _fileSystem.Directory.Delete(DistributionDataPath, recursive: true); + if (_fileSystem.File.Exists(RiivolutionDiscXmlPath)) + _fileSystem.File.Delete(RiivolutionDiscXmlPath); + }, + $"Failed to remove the current {Title} install" + ); + + return Task.FromResult(result); + } + + /// + /// Moves the current install (and its wiiDisc xml file) aside so it can be restored when the install fails. + /// + /// Whether there actually was something to back up. + private OperationResult BackupCurrentInstall() => + TryCatch( + () => + { + // A backup can still be there when a previous install was interrupted, that one is of no use to us anymore. + DeleteBackup(); - if (_fileSystem.Directory.Exists(distributionDataDestination)) - _fileSystem.Directory.Delete(distributionDataDestination, recursive: true); - if (_fileSystem.File.Exists(riivolutionDiscXMLFile)) - _fileSystem.File.Delete(riivolutionDiscXMLFile); + var hasBackup = false; + if (_fileSystem.Directory.Exists(DistributionDataPath)) + { + _fileSystem.Directory.Move(DistributionDataPath, BackupDataPath); + hasBackup = true; + } + if (_fileSystem.File.Exists(RiivolutionDiscXmlPath)) + { + _fileSystem.File.Move(RiivolutionDiscXmlPath, BackupDiscXmlPath, overwrite: true); + hasBackup = true; + } - return Task.FromResult(Ok()); - } + return hasBackup; + }, + $"Failed to back up the current {Title} install" + ); - public async Task ReinstallAsync(ProgressWindow progressWindow) - { - //Remove and install - var removeResult = await RemoveAsync(progressWindow); - if (removeResult.IsFailure) - return removeResult; + private OperationResult RestoreBackup() => + TryCatch( + () => + { + if (_fileSystem.Directory.Exists(BackupDataPath)) + { + // Whatever the failed install left behind is worthless, the backup is the real install. + if (_fileSystem.Directory.Exists(DistributionDataPath)) + _fileSystem.Directory.Delete(DistributionDataPath, recursive: true); + _fileSystem.Directory.Move(BackupDataPath, DistributionDataPath); + } + if (_fileSystem.File.Exists(BackupDiscXmlPath)) + { + var xmlFolder = _fileSystem.Path.GetDirectoryName(RiivolutionDiscXmlPath); + if (!string.IsNullOrEmpty(xmlFolder)) + _fileSystem.Directory.CreateDirectory(xmlFolder); + _fileSystem.File.Move(BackupDiscXmlPath, RiivolutionDiscXmlPath, overwrite: true); + } + }, + $"Failed to restore the previous {Title} install" + ); - return await InstallAsync(progressWindow); + private void DeleteBackup() + { + if (_fileSystem.Directory.Exists(BackupDataPath)) + _fileSystem.Directory.Delete(BackupDataPath, recursive: true); + if (_fileSystem.File.Exists(BackupDiscXmlPath)) + _fileSystem.File.Delete(BackupDiscXmlPath); } + // Installing already replaces the current install (and restores it when the install fails), so there is nothing extra to do here. + public Task ReinstallAsync(ProgressWindow progressWindow) => InstallAsync(progressWindow); + public async Task> GetCurrentStatusAsync() { if (!_settingsManager.PathsSetupCorrectly()) diff --git a/WheelWizard/Views/Pages/Settings/OtherSettings.axaml.cs b/WheelWizard/Views/Pages/Settings/OtherSettings.axaml.cs index 20d10d86..45ec5751 100644 --- a/WheelWizard/Views/Pages/Settings/OtherSettings.axaml.cs +++ b/WheelWizard/Views/Pages/Settings/OtherSettings.axaml.cs @@ -3,6 +3,7 @@ using WheelWizard.Services; using WheelWizard.Settings; using WheelWizard.Shared.DependencyInjection; +using WheelWizard.Shared.MessageTranslations; using WheelWizard.Views.Popups.Generic; namespace WheelWizard.Views.Pages.Settings; @@ -67,8 +68,11 @@ private async void Reinstall_RetroRewind(object sender, RoutedEventArgs e) { var progressWindow = new ProgressWindow(); progressWindow.Show(); - await CustomDistributionSingletonService.RetroRewind.ReinstallAsync(progressWindow); + var reinstallResult = await CustomDistributionSingletonService.RetroRewind.ReinstallAsync(progressWindow); progressWindow.Close(); + + if (reinstallResult.IsFailure) + MessageTranslationHelper.ShowMessage(reinstallResult.Error); } private void OpenSaveFolder_OnClick(object? sender, RoutedEventArgs e)