Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions WheelWizard.Test/Features/CustomDistributions/RetroRewindTests.cs
Original file line number Diff line number Diff line change
@@ -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<IRetroRewindApi> _api = Substitute.For<IApiCaller<IRetroRewindApi>>();
private readonly RetroRewind _distribution;

public RetroRewindTests()
{
InitializeUserFolder();
_distribution = new RetroRewind(_fileSystem, _api, Substitute.For<ILogger<IDistribution>>(), Substitute.For<ISettingsManager>());
}

[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<IWhWzSettingManager>(),
Substitute.For<IDolphinSettingManager>(),
_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, "<wiidisc/>");
}

private void ServerIsUnreachable()
{
_api.CallApiAsync(Arg.Any<Expression<Func<IRetroRewindApi, Task<string>>>>())
.Returns(Task.FromResult<OperationResult<string>>(Fail("Server offline")));
}
}
126 changes: 105 additions & 21 deletions WheelWizard/Features/CustomDistributions/RetroRewind.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<OperationResult> 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<OperationResult> PerformInstallAsync(ProgressWindow progressWindow)
{
if (HasOldRksys())
{
var rksysQuestion = new YesNoWindow()
Expand Down Expand Up @@ -543,29 +575,81 @@ List<DeletionData> allDeletions

public Task<OperationResult> 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);
}

/// <summary>
/// Moves the current install (and its wiiDisc xml file) aside so it can be restored when the install fails.
/// </summary>
/// <returns>Whether there actually was something to back up.</returns>
private OperationResult<bool> 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<OperationResult> 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<OperationResult> ReinstallAsync(ProgressWindow progressWindow) => InstallAsync(progressWindow);

public async Task<OperationResult<WheelWizardStatus>> GetCurrentStatusAsync()
{
if (!_settingsManager.PathsSetupCorrectly())
Expand Down
6 changes: 5 additions & 1 deletion WheelWizard/Views/Pages/Settings/OtherSettings.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
Loading