Skip to content

Commit

Permalink
GamePath FileTree support (#570)
Browse files Browse the repository at this point in the history
* Added IPath<TPath> interface to GamePath
- Implemented interface methods
- This gives us FileTreeNode<GamePath>

* Added GamePath tests for new methods

* Added simple test for FileTreeNode<GamePath>

* Updated Paths library to 0.1.4 containing the new IPath<TPath> and FileTree
  • Loading branch information
Al12rs authored Aug 24, 2023
1 parent 61de8b0 commit f5a269a
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 4 deletions.
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<ItemGroup>
<!-- Custom Packages -->
<PackageVersion Include="NexusMods.Hashing.xxHash64" Version="0.9.0" />
<PackageVersion Include="NexusMods.Paths" Version="0.1.3" />
<PackageVersion Include="NexusMods.Paths" Version="0.1.4" />
<PackageVersion Include="NexusMods.Archives.Nx" Version="0.3.3-preview" />
<PackageVersion Include="FomodInstaller.Interface" Version="1.2.0" />
<PackageVersion Include="FomodInstaller.Scripting.XmlScript" Version="1.0.0" />
Expand Down Expand Up @@ -97,4 +97,4 @@
<PackageVersion Include="Splat.Microsoft.Extensions.Logging" Version="14.6.37" />
<PackageVersion Include="Vogen" Version="3.0.20" />
</ItemGroup>
</Project>
</Project>
49 changes: 48 additions & 1 deletion src/NexusMods.DataModel/Games/GamePath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace NexusMods.Paths;
/// <summary>
/// Stores the path for an individual game.
/// </summary>
public readonly struct GamePath : IPath, IEquatable<GamePath>
public readonly struct GamePath : IPath, IPath<GamePath>, IEquatable<GamePath>
{
/// <summary>
/// The path to this instance.
Expand Down Expand Up @@ -63,6 +63,53 @@ public GamePath(GameFolderType type, string path) : this(type, path.ToRelativePa
/// The absolute path to combine with current relative path.
/// </param>
public AbsolutePath Combine(AbsolutePath folderPath) => folderPath.Combine(Path);

/// <inheritdoc />
public RelativePath Name => Path.Name;

/// <inheritdoc />
public GamePath Parent => new GamePath(Type, Path.Parent);

/// <inheritdoc />
public GamePath GetRootComponent => new GamePath(Type, "");

/// <inheritdoc />
public IEnumerable<RelativePath> Parts => Path.Parts;

/// <inheritdoc />
public IEnumerable<GamePath> GetAllParents()
{
var type = Type;
var root = new GamePath(type, "");
return Path.GetAllParents().Select(parentPath => new GamePath(type, parentPath)).Append(root);
}

/// <inheritdoc />
public RelativePath GetNonRootPart()
{
return Path;
}

/// <inheritdoc />
public bool IsRooted => true;

/// <inheritdoc />
public bool InFolder(GamePath parent)
{
return Type == parent.Type && Path.InFolder(parent.Path);
}

/// <inheritdoc />
public bool StartsWith(GamePath other)
{
return Type == other.Type && Path.StartsWith(other.Path);
}

/// <inheritdoc />
public bool EndsWith(RelativePath other)
{
return Path.EndsWith(other);
}
}

/// <summary>
Expand Down
50 changes: 50 additions & 0 deletions tests/NexusMods.DataModel.Tests/GamePathFileTreeNodeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using FluentAssertions;
using NexusMods.Paths;
using NexusMods.Paths.FileTree;

namespace NexusMods.DataModel.Tests;

public class GamePathFileTreeNodeTests
{

[Theory]
[InlineData(GameFolderType.Game, "file1.txt", true, 1)]
[InlineData(GameFolderType.Game,"file2.txt", false, 2)]
[InlineData(GameFolderType.Game,"foo/file2.txt", true, 2)]
[InlineData(GameFolderType.Game,"foo/file3.txt", true, 3)]
[InlineData(GameFolderType.Game,"foo/bar/file4.txt", true, 4)]
[InlineData(GameFolderType.Game,"baz/bazer/file5.txt", true, 5)]
[InlineData(GameFolderType.Saves,"baz/bazer/file5.txt", false, 5)]
public void Test_FindNode(GameFolderType folderType, string path, bool found, int value)
{
var tree = MakeTestTree();

var node = tree.FindNode(new GamePath(folderType, (RelativePath)path));
if (found)
{
node.Should().NotBeNull();
node!.Path.Should().Be(new GamePath(folderType, (RelativePath)path));
node!.Value.Should().Be(value);
}
else
{
node.Should().BeNull();
}
}

private static FileTreeNode<GamePath, int> MakeTestTree()
{
Dictionary<GamePath, int> fileEntries;

fileEntries = new Dictionary<GamePath, int>
{
{ new GamePath(GameFolderType.Game,"file1.txt"), 1 },
{ new GamePath(GameFolderType.Game,"foo/file2.txt"), 2 },
{ new GamePath(GameFolderType.Game,"foo/file3.txt"), 3 },
{ new GamePath(GameFolderType.Game,"foo/bar/file4.txt"), 4 },
{ new GamePath(GameFolderType.Game,"baz/bazer/file5.txt"), 5 },
};

return FileTreeNode<GamePath, int>.CreateTree(fileEntries);
}
}
137 changes: 136 additions & 1 deletion tests/NexusMods.DataModel.Tests/GamePathTests.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using FluentAssertions;
using NexusMods.Paths;
using NexusMods.Paths.Extensions;
using NexusMods.Paths.Utilities;

namespace NexusMods.Paths.Tests;
namespace NexusMods.DataModel.Tests;

public class GamePathTests
{
Expand Down Expand Up @@ -66,4 +68,137 @@ public void CanGetPathRelativeTo()
Assert.Equal(baseFolder.Combine("foo/bar"), pathA.Combine(baseFolder));
}

[Theory]
[InlineData(GameFolderType.Game, "", "")]
[InlineData(GameFolderType.Game, "foo", "foo")]
[InlineData(GameFolderType.Game, "foo/bar", "bar")]
public void Test_Name(GameFolderType folderType, string input, string expected)
{
var path = new GamePath(folderType, (RelativePath)input);
path.Name.Should().Be(expected);
}


[Theory]
[InlineData(GameFolderType.Game, "", "")]
[InlineData(GameFolderType.Game, "foo", "")]
[InlineData(GameFolderType.Game, "foo/bar", "foo")]
[InlineData(GameFolderType.Game, "foo/bar/baz", "foo/bar")]
public void Test_Parent(GameFolderType folderType, string input, string expectedParent)
{
var path = new GamePath(folderType, (RelativePath)input);
path.Parent.Should().Be(new GamePath(folderType, (RelativePath)expectedParent));
}

[Theory]
[InlineData(GameFolderType.Game, "", "")]
[InlineData(GameFolderType.Game, "foo", "")]
[InlineData(GameFolderType.AppData, "foo/bar", "")]
[InlineData(GameFolderType.Saves, "foo/bar/baz", "")]
public void Test_GetRootComponent(GameFolderType folderType, string input, string expectedRootComponent)
{
var path = new GamePath(folderType, (RelativePath)input);
path.GetRootComponent.Should().Be(new GamePath(folderType, (RelativePath)expectedRootComponent));
}

[Theory]
[InlineData(GameFolderType.Game, "", new string[] { })]
[InlineData(GameFolderType.Game, "foo", new string[] { "foo" })]
[InlineData(GameFolderType.Game, "foo/bar", new string[] { "foo", "bar" })]
[InlineData(GameFolderType.Saves, "foo/bar/baz", new string[] { "foo", "bar", "baz" })]
public void Test_Parts(GameFolderType folderType, string input, string[] expectedParts)
{
var path = new GamePath(folderType, (RelativePath)input);
path.Parts.Should().BeEquivalentTo(expectedParts.Select(x => new RelativePath(x)));
}

[Theory]
[InlineData(GameFolderType.Game, "", new string[] { ""})]
[InlineData(GameFolderType.Game, "foo", new string[] { "foo", "" })]
[InlineData(GameFolderType.Saves, "foo/bar", new string[] { "foo/bar", "foo", "" })]
[InlineData(GameFolderType.Game, "foo/bar/baz", new string[] { "foo/bar/baz", "foo/bar", "foo", "" })]
public void Test_GetAllParents(GameFolderType folderType, string input, string[] expectedParts)
{
var path = new GamePath(folderType, (RelativePath)input);
path.GetAllParents().Should()
.BeEquivalentTo(expectedParts.Select(x => new GamePath(folderType, (RelativePath)x)));
}

[Theory]
[InlineData(GameFolderType.Game, "", "")]
[InlineData(GameFolderType.Game, "foo", "foo")]
[InlineData(GameFolderType.AppData, "foo/bar", "foo/bar")]
[InlineData(GameFolderType.Saves, "foo/bar/baz", "foo/bar/baz")]
public void Test_GetNonRootPart(GameFolderType folderType, string input, string expected)
{
var path = new GamePath(folderType, (RelativePath)input);
path.GetNonRootPart().Should().Be((RelativePath)expected);
}

[Theory]
[InlineData(GameFolderType.Game, "", true)]
[InlineData(GameFolderType.Saves, "foo", true)]
[InlineData(GameFolderType.AppData, "foo/bar", true)]
public void Test_IsRooted(GameFolderType folderType, string input, bool expected)
{
var path = new GamePath(folderType, (RelativePath)input);
path.IsRooted.Should().Be(expected);
}

[Theory]
[InlineData(GameFolderType.Game, "foo", GameFolderType.Game, "bar", false)]
[InlineData(GameFolderType.Game, "foo/bar", GameFolderType.Game, "foo", true)]
[InlineData(GameFolderType.Game, "foo/bar/baz", GameFolderType.Game, "foo/bar", true)]
[InlineData(GameFolderType.Game, "foo", GameFolderType.Saves, "bar", false)]
[InlineData(GameFolderType.Saves, "foo/bar", GameFolderType.Game, "foo", false)]
[InlineData(GameFolderType.Game, "foo/bar/baz", GameFolderType.AppData, "foo/bar", false)]
public void Test_InFolder(GameFolderType folderTypeLeft, string left, GameFolderType folderTypeRight, string right,
bool expected)
{
var leftPath = new GamePath(folderTypeLeft, (RelativePath)left);
var rightPath = new GamePath(folderTypeRight, (RelativePath)right);
var actual = leftPath.InFolder(rightPath);
actual.Should().Be(expected);
}

[Theory]
[InlineData(GameFolderType.Game,"",GameFolderType.Game, "", true)]
[InlineData(GameFolderType.Game,"",GameFolderType.Game, "foo", false)]
[InlineData(GameFolderType.Game,"foo",GameFolderType.Game, "bar", false)]
[InlineData(GameFolderType.Game,"foo",GameFolderType.Game, "", true)]
[InlineData(GameFolderType.Game,"foo/bar/baz",GameFolderType.Game, "", true)]
[InlineData(GameFolderType.Game,"foo/bar/baz",GameFolderType.Game, "foo", true)]
[InlineData(GameFolderType.Game,"foo/bar/baz",GameFolderType.Game, "foo/bar", true)]
[InlineData(GameFolderType.Game,"foobar",GameFolderType.Game, "foo", false)]
[InlineData(GameFolderType.Game,"foo/bar/baz",GameFolderType.Game, "foo/baz", false)]
[InlineData(GameFolderType.Game,"",GameFolderType.AppData, "", false)]
[InlineData(GameFolderType.Game,"foo/bar/baz",GameFolderType.AppData, "", false)]
[InlineData(GameFolderType.Game,"foo/bar/baz",GameFolderType.AppData, "foo", false)]
[InlineData(GameFolderType.Game,"foo/bar/baz",GameFolderType.AppData, "foo/bar", false)]
[InlineData(GameFolderType.Game,"foo",GameFolderType.AppData, "", false)]
public void Test_StartsWithGamePath(GameFolderType folderTypeChild, string child,GameFolderType folderTypeParent, string parent, bool expected)
{
var childPath = new GamePath(folderTypeChild, (RelativePath)child);
var parentPath = new GamePath(folderTypeParent, (RelativePath)parent);
var actual = childPath.StartsWith(parentPath);
actual.Should().Be(expected);
}

[Theory]
[InlineData(GameFolderType.Game,"", "", true)]
[InlineData(GameFolderType.Game,"", "foo", false)]
[InlineData(GameFolderType.Game,"foo", "bar", false)]
[InlineData(GameFolderType.Game,"foo", "", true)]
[InlineData(GameFolderType.Game,"foo/bar/baz", "", true)]
[InlineData(GameFolderType.Game,"foo/bar/baz", "bar/baz", true)]
[InlineData(GameFolderType.Game,"foo/bar/baz", "foo/bar/baz", true)]
[InlineData(GameFolderType.Game,"foobar", "bar", false)]
[InlineData(GameFolderType.Game,"foo/bar/baz", "foo/baz", false)]
public void Test_EndsWithRelative(GameFolderType folderType,string child, string parent, bool expected)
{
var childPath = new GamePath(folderType, (RelativePath)child);
var parentPath = (RelativePath)parent;
var actual = childPath.EndsWith(parentPath);
actual.Should().Be(expected);
}
}

0 comments on commit f5a269a

Please sign in to comment.