Skip to content

Commit

Permalink
Move install time into mods
Browse files Browse the repository at this point in the history
Global kept for backward compatibility
  • Loading branch information
paoloambrosio committed Jun 30, 2024
1 parent cc3e4b8 commit e9c9f3b
Show file tree
Hide file tree
Showing 5 changed files with 57 additions and 39 deletions.
6 changes: 1 addition & 5 deletions src/Core/ModInstaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,8 @@ public void UninstallPackages(
if (currentState.Mods.Any())
{
eventHandler.UninstallStart();
var skipCreatedAfter = SkipCreatedAfter(eventHandler, currentState.Time);
var uninstallCallbacks = new ProcessingCallbacks<RootedPath>
{
Accept = gamePath =>
{
return skipCreatedAfter(gamePath);
},
After = gamePath =>
{
backupStrategy.RestoreBackup(gamePath.Full);
Expand All @@ -95,6 +90,7 @@ public void UninstallPackages(
installDir,
filesLeft,
uninstallCallbacks
.AndAccept(SkipCreatedAfter(eventHandler, modInstallationState.Time))
.AndAfter(_ => filesLeft.Remove(_.Relative))
.AndNotAccepted(_ => filesLeft.Remove(_.Relative))
);
Expand Down
16 changes: 10 additions & 6 deletions src/Core/ModManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Core.State;
using Core.Utils;
using static Core.IModManager;
using static Core.Mods.IInstallation;

namespace Core;

Expand Down Expand Up @@ -169,6 +170,7 @@ private bool RestoreOriginalState(IEventHandler eventHandler, CancellationToken
else
{
modsLeft[modInstallation.PackageName] = new InternalModInstallationState(
Time: modsLeft[modInstallation.PackageName].Time,
FsHash: modInstallation.PackageFsHash,
Partial: modInstallation.Installed == IInstallation.State.PartiallyInstalled,
Files: modInstallation.InstalledFiles
Expand All @@ -182,7 +184,8 @@ private bool RestoreOriginalState(IEventHandler eventHandler, CancellationToken
{
statePersistence.WriteState(new InternalState(
Install: new(
Time: modsLeft.Any() ? previousInstallation.Time : null,
// TODO for state migration
Time: modsLeft.Values.Max(_ => _.Time),
Mods: modsLeft
)
));
Expand All @@ -208,18 +211,19 @@ private void InstallAllModFiles(IEventHandler eventHandler, CancellationToken ca
modRepository.ListEnabledMods(),
game.InstallationDirectory,
modInstallation => installedFilesByMod.Add(modInstallation.PackageName, new(
FsHash: modInstallation.PackageFsHash,
Partial: modInstallation.Installed == IInstallation.State.PartiallyInstalled,
Files: modInstallation.InstalledFiles
)),
Time: DateTime.UtcNow,
FsHash: modInstallation.PackageFsHash,
Partial: modInstallation.Installed == IInstallation.State.PartiallyInstalled,
Files: modInstallation.InstalledFiles
)),
eventHandler,
cancellationToken);
}
finally
{
statePersistence.WriteState(new InternalState(
Install: new(
Time: DateTime.UtcNow,
Time: installedFilesByMod.Values.Max(_ => _.Time),
Mods: installedFilesByMod
)
));
Expand Down
3 changes: 3 additions & 0 deletions src/Core/State/InternalState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ InternalInstallationState Install
};

public record InternalInstallationState(
// TODO: needed for backward compatibility
DateTime? Time,
IReadOnlyDictionary<string, InternalModInstallationState> Mods
)
Expand All @@ -23,6 +24,8 @@ IReadOnlyDictionary<string, InternalModInstallationState> Mods
};

public record InternalModInstallationState(
// TODO: nullable for backward compatibility
DateTime? Time,
// Unknown when partially installed or upgrading from a previous version
int? FsHash,
// TODO: needed for backward compatibility
Expand Down
15 changes: 12 additions & 3 deletions src/Core/State/JsonFileStatePersistence.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Newtonsoft.Json;
using Core.Utils;
using Newtonsoft.Json;

namespace Core.State;

Expand Down Expand Up @@ -28,7 +29,15 @@ public InternalState ReadState()
if (File.Exists(stateFile))
{
var contents = File.ReadAllText(stateFile);
return JsonConvert.DeserializeObject<InternalState>(contents);
var state = JsonConvert.DeserializeObject<InternalState>(contents);
// Fill mod install time if not present (for migration)
return state with
{
Install = state.Install with
{
Mods = state.Install.Mods.SelectValues(_ => _ with { Time = _.Time ?? state.Install.Time })
}
};
}

// Fallback to old state when new state is not present
Expand All @@ -42,7 +51,7 @@ public InternalState ReadState()
Time: installTime,
Mods: oldState.AsEnumerable().ToDictionary(
kv => kv.Key,
kv => new InternalModInstallationState(FsHash: null, Partial: false, Files: kv.Value)
kv => new InternalModInstallationState(Time: installTime, FsHash: null, Partial: false, Files: kv.Value)
)
)
);
Expand Down
56 changes: 31 additions & 25 deletions tests/Core.Tests/ModManagerIntegrationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ public class ModManagerIntegrationTest : AbstractFilesystemTest
private const string DirAtRoot = "DirAtRoot";
private const string FileExcludedFromInstall = "Excluded";

// Randomness ensures that at least some test runs will fail if it's used
private static readonly DateTime? ValueNotUsed = Random.Shared.Next() > 0 ? DateTime.MaxValue : DateTime.MinValue;

private static readonly TimeSpan TimeTolerance = TimeSpan.FromMilliseconds(100);

private readonly DirectoryInfo gameDir;
Expand Down Expand Up @@ -79,16 +82,16 @@ public void Uninstall_DeletesCreatedFilesAndDirectories()
persistedState.InitState(new InternalState
(
Install: new(
Time: null,
Time: ValueNotUsed,
Mods: new Dictionary<string, InternalModInstallationState>
{
["A"] = new(
FsHash: null, Partial: false, Files: [
Time: null, FsHash: null, Partial: false, Files: [
Path.Combine("X", "ModAFile"),
Path.Combine("Y", "ModAFile")
]),
["B"] = new(
FsHash: null, Partial: false, Files: [
Time: null, FsHash: null, Partial: false, Files: [
Path.Combine("X", "ModBFile")
])
}
Expand All @@ -111,11 +114,11 @@ public void Uninstall_SkipsFilesCreatedAfterInstallation()
persistedState.InitState(new InternalState
(
Install: new(
Time: installationDateTime.ToUniversalTime(),
Time: ValueNotUsed,
Mods: new Dictionary<string, InternalModInstallationState>
{
[""] = new(
FsHash: null, Partial: false, Files: [
Time: installationDateTime.ToUniversalTime(), FsHash: null, Partial: false, Files: [
"ModFile",
"RecreatedFile",
"AlreadyDeletedFile"
Expand All @@ -140,20 +143,20 @@ public void Uninstall_StopsAfterAnyError()
var installationDateTime = DateTime.Now.AddMinutes(1);
persistedState.InitState(new InternalState(
Install: new(
Time: installationDateTime.ToUniversalTime(),
Time: ValueNotUsed,
Mods: new Dictionary<string, InternalModInstallationState>
{
["A"] = new(
FsHash: null, Partial: false, Files: [
Time: installationDateTime.ToUniversalTime(), FsHash: null, Partial: false, Files: [
"ModAFile"
]),
["B"] = new(
FsHash: null, Partial: false, Files: [
Time: installationDateTime.ToUniversalTime(), FsHash: null, Partial: false, Files: [
"ModBFile1",
"ModBFile2"
]),
["C"] = new(
FsHash: null, Partial: false, Files: [
Time: installationDateTime.ToUniversalTime(), FsHash: null, Partial: false, Files: [
"ModCFile"
])
}
Expand All @@ -172,11 +175,11 @@ public void Uninstall_StopsAfterAnyError()
Mods: new Dictionary<string, InternalModInstallationState>
{
["B"] = new(
FsHash: null, Partial: true, Files: [
Time: installationDateTime.ToUniversalTime(), FsHash: null, Partial: true, Files: [
"ModBFile2"
]),
["C"] = new(
FsHash: null, Partial: false, Files: [
Time: installationDateTime.ToUniversalTime(), FsHash: null, Partial: false, Files: [
"ModCFile"
])
}
Expand All @@ -189,11 +192,11 @@ public void Uninstall_RestoresBackups()
{
persistedState.InitState(new InternalState(
Install: new(
Time: null,
Time: ValueNotUsed,
Mods: new Dictionary<string, InternalModInstallationState>
{
[""] = new(
FsHash: null, Partial: false, Files: [
Time: null, FsHash: null, Partial: false, Files: [
"ModFile"
])
}
Expand All @@ -215,11 +218,11 @@ public void Uninstall_SkipsRestoreIfModFileOverwritten()
var installationDateTime = DateTime.Now.AddMinutes(1);
persistedState.InitState(new InternalState(
Install: new(
Time: installationDateTime.ToUniversalTime(),
Time: ValueNotUsed,
Mods: new Dictionary<string, InternalModInstallationState>
{
[""] = new(
FsHash: null, Partial: false, Files: [
Time: installationDateTime.ToUniversalTime(), FsHash: null, Partial: false, Files: [
"ModFile"
])
}
Expand Down Expand Up @@ -270,7 +273,7 @@ public void Install_InstallsContentFromRootDirectories()
persistedState.AssertModsEqual(new Dictionary<string, InternalModInstallationState>
{
["Package100"] = new(
FsHash: 100, Partial: false, Files: [
Time: DateTime.UtcNow, FsHash: 100, Partial: false, Files: [
Path.Combine(DirAtRoot, "A"),
Path.Combine(DirAtRoot, "B"),
"C"
Expand All @@ -295,7 +298,7 @@ public void Install_SkipsBlacklistedFiles()
persistedState.AssertModsEqual(new Dictionary<string, InternalModInstallationState>
{
["Package100"] = new(
FsHash: 100, Partial: false, Files: [
Time: DateTime.UtcNow, FsHash: 100, Partial: false, Files: [
Path.Combine(DirAtRoot, "B")
]),
});
Expand Down Expand Up @@ -334,8 +337,8 @@ public void Install_GivesPriotiryToFilesLaterInTheModList()
Assert.Equal("200", File.ReadAllText(GamePath(Path.Combine(DirAtRoot, "A"))));
persistedState.AssertModsEqual(new Dictionary<string, InternalModInstallationState>
{
["Package100"] = new(FsHash: 100, Partial: false, Files: []),
["Package200"] = new(FsHash: 200, Partial: false, Files: [
["Package100"] = new(Time: DateTime.UtcNow, FsHash: 100, Partial: false, Files: []),
["Package200"] = new(Time: DateTime.UtcNow, FsHash: 200, Partial: false, Files: [
Path.Combine(DirAtRoot, "a")
]),
});
Expand All @@ -355,7 +358,7 @@ public void Install_DuplicatesAreCaseInsensitive()

persistedState.AssertModsEqual(new Dictionary<string, InternalModInstallationState>
{
["Package100"] = new(FsHash: 100, Partial: false, Files: [
["Package100"] = new(Time: DateTime.UtcNow, FsHash: 100, Partial: false, Files: [
Path.Combine(DirAtRoot, "A")
]),
});
Expand Down Expand Up @@ -391,11 +394,11 @@ public void Install_StopsAfterAnyError()
Mods: new Dictionary<string, InternalModInstallationState>
{
["Package200"] = new(
FsHash: 200, Partial: true, Files: [
Time: DateTime.Now, FsHash: 200, Partial: true, Files: [
Path.Combine(DirAtRoot, "B1")
]),
["Package300"] = new(
FsHash: 300, Partial: false, Files: [
Time: DateTime.Now, FsHash: 300, Partial: false, Files: [
Path.Combine(DirAtRoot, "C")
]),
}
Expand Down Expand Up @@ -572,6 +575,9 @@ private string GamePath(string relativePath) =>
private static void AssertAboutNow(DateTime actual) =>
AssertEqualWithinToleration(DateTime.Now, actual);

/// <remarks>
/// Not a great solution, but .NET doesn't natively provide support for mocking the clock
/// </remarks>
private static void AssertEqualWithinToleration(DateTime? expected, DateTime? actual) =>
Assert.InRange(actual?.ToUniversalTime().Ticks ?? 0L,
expected?.ToUniversalTime().Subtract(TimeTolerance).Ticks ?? 0L,
Expand All @@ -582,10 +588,10 @@ private class AssertState : IStatePersistence
// Avoids bootfiles checks on uninstall
private static readonly InternalState SkipBootfilesCheck = new InternalState(
Install: new(
Time: null,
Time: ValueNotUsed,
Mods: new Dictionary<string, InternalModInstallationState>
{
["INIT"] = new(FsHash: null, Partial: false, Files: []),
["INIT"] = new(Time: null, FsHash: null, Partial: false, Files: []),
}
));

Expand All @@ -601,7 +607,6 @@ private class AssertState : IStatePersistence
internal void AssertEqual(InternalState expected)
{
Assert.NotNull(savedState);
// Not a great solution, but .NET doesn't natively provide support for mocking the clock
AssertEqualWithinToleration(expected.Install.Time, savedState.Install.Time);
AssertModsInstalled(expected.Install.Mods.Keys);
AssertModsEqual(expected.Install.Mods);
Expand All @@ -614,6 +619,7 @@ internal void AssertModsEqual(IReadOnlyDictionary<string, InternalModInstallatio
{
var currentModState = savedState.Install.Mods[e.Key];
var expectedModState = e.Value;
AssertEqualWithinToleration(expectedModState.Time, currentModState.Time);
Assert.Equal(expectedModState.FsHash, currentModState.FsHash);
Assert.Equal(expectedModState.Partial, currentModState.Partial);
Assert.Equal(expectedModState.Files.ToImmutableHashSet(), currentModState.Files.ToImmutableHashSet());
Expand Down

0 comments on commit e9c9f3b

Please sign in to comment.