Skip to content
Merged
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
24 changes: 21 additions & 3 deletions Semantics.Paths/Implementations/AbsoluteDirectoryPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,31 @@ public sealed record AbsoluteDirectoryPath : SemanticDirectoryPath<AbsoluteDirec
/// <summary>
/// Gets the parent directory of this absolute directory path.
/// </summary>
/// <value>An <see cref="AbsoluteDirectoryPath"/> representing the parent directory, or an empty path if this is a root directory.</value>
/// <value>
/// An <see cref="AbsoluteDirectoryPath"/> representing the parent directory, or this same path when it is a
/// root directory and therefore has no parent above it.
/// </value>
/// <remarks>
/// A root is its own parent, matching how the filesystem resolves <c>/..</c> to <c>/</c> and <c>C:\..</c> to
/// <c>C:\</c>. This keeps every value this property returns a usable absolute directory rather than an empty
/// path, which silently reads as "no contents" when passed to the filesystem. Use <see cref="IsRoot"/>, or
/// compare the parent against this path, to detect that walking upwards has finished.
/// </remarks>
public AbsoluteDirectoryPath Parent
{
get
{
return _cachedParent ??= Create<AbsoluteDirectoryPath>(
InternedPathStrings.InternIfCommon(Path.GetDirectoryName(WeakString) ?? InternedPathStrings.Empty));
if (_cachedParent is not null)
{
return _cachedParent;
}

string? parent = Path.GetDirectoryName(WeakString);
_cachedParent = string.IsNullOrEmpty(parent)
? this
: Create<AbsoluteDirectoryPath>(InternedPathStrings.InternIfCommon(parent));

return _cachedParent;
}
}

Expand Down
119 changes: 119 additions & 0 deletions Semantics.Test/Paths/AbsoluteDirectoryPathParentTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright (c) ktsu.dev
// All rights reserved.
// Licensed under the MIT license.

namespace ktsu.Semantics.Test.Paths;

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using ktsu.Semantics.Paths;
using Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>
/// Tests for walking upwards from an <see cref="AbsoluteDirectoryPath"/>.
/// </summary>
[TestClass]
public class AbsoluteDirectoryPathParentTests
{
private static AbsoluteDirectoryPath Dir(string path) => AbsoluteDirectoryPath.Create<AbsoluteDirectoryPath>(path);

private static string Nested => OperatingSystem.IsWindows() ? @"C:\Users\user\Documents" : "/home/user/Documents";

private static string Middle => OperatingSystem.IsWindows() ? @"C:\Users\user" : "/home/user";

private static string BelowRoot => OperatingSystem.IsWindows() ? @"C:\Users" : "/home";

private static string Root => OperatingSystem.IsWindows() ? @"C:\" : "/";

[TestMethod]
public void Parent_ReturnsTheContainingDirectory()
{
Assert.AreEqual(Middle, Dir(Nested).Parent.WeakString);
Assert.AreEqual(BelowRoot, Dir(Middle).Parent.WeakString);
Assert.AreEqual(Root, Dir(BelowRoot).Parent.WeakString);
}

[TestMethod]
public void Parent_OfRoot_ReturnsTheRootItself()
{
AbsoluteDirectoryPath root = Dir(Root);

Assert.IsTrue(root.IsRoot, $"'{root}' should be a root.");
Assert.AreEqual(root, root.Parent);
}

[TestMethod]
public void Parent_OfUncShareRoot_ReturnsTheShareItself()
{
if (!OperatingSystem.IsWindows())
{
Assert.Inconclusive("UNC paths are Windows-only.");
return;
}

Assert.AreEqual(@"\\server\share", Dir(@"\\server\share\folder").Parent.WeakString);

AbsoluteDirectoryPath share = Dir(@"\\server\share");
Assert.AreEqual(share, share.Parent);
}

/// <summary>
/// Regression test for ktsu-dev/ImGuiApp#281. A root used to report an empty parent, which still passes
/// <see cref="AbsoluteDirectoryPath"/> validation but is not a location: passed to the filesystem it reads
/// as "no contents" rather than failing, so callers walking upwards silently landed nowhere.
/// </summary>
[TestMethod]
public void Parent_IsAlwaysAUsableAbsolutePath()
{
AbsoluteDirectoryPath current = Dir(Environment.CurrentDirectory);
int stepsPastRoot = current.Depth + 2;

for (int step = 0; step < stepsPastRoot; step++)
{
AbsoluteDirectoryPath parent = current.Parent;

Assert.IsFalse(string.IsNullOrEmpty(parent.WeakString), $"The parent of '{current}' should not be empty.");
Assert.IsTrue(
Path.IsPathFullyQualified(parent.WeakString),
$"The parent of '{current}' should be fully qualified, but was '{parent}'.");

current = parent;
}

Assert.IsTrue(current.IsRoot, $"Walking upwards should settle on a root, but settled on '{current}'.");
}

[TestMethod]
public void Parent_OfRoot_IsAFixedPointSoWalkingUpwardsTerminates()
{
AbsoluteDirectoryPath current = Dir(Environment.CurrentDirectory);
int steps = 0;

while (!current.IsRoot)
{
current = current.Parent;
Assert.IsLessThan(1000, ++steps, "Walking upwards should terminate at a root.");
}

Assert.AreEqual(current, current.Parent);
}

[TestMethod]
public void GetAncestors_EndsAtTheRootWithoutRepeatingIt()
{
List<AbsoluteDirectoryPath> ancestors = [.. Dir(Nested).GetAncestors()];

CollectionAssert.AreEqual(
new List<AbsoluteDirectoryPath> { Dir(Middle), Dir(BelowRoot), Dir(Root) },
ancestors,
$"Expected the chain up to the root, got [{string.Join(", ", ancestors.Select(a => a.WeakString))}].");
}

[TestMethod]
public void GetAncestors_OfRoot_IsEmpty()
{
Assert.IsEmpty(Dir(Root).GetAncestors());
}
}
Loading