diff --git a/Semantics.Paths/CompatibilitySuppressions.xml b/Semantics.Paths/CompatibilitySuppressions.xml index fd22e37..ec85be7 100644 --- a/Semantics.Paths/CompatibilitySuppressions.xml +++ b/Semantics.Paths/CompatibilitySuppressions.xml @@ -553,6 +553,36 @@ lib/net7.0/ktsu.Semantics.Paths.dll lib/net8.0/ktsu.Semantics.Paths.dll + + CP0016 + M:ktsu.Semantics.Paths.DirectoryName.Equals(ktsu.Semantics.Paths.DirectoryName):[T:System.Runtime.CompilerServices.NullableContextAttribute] + lib/net7.0/ktsu.Semantics.Paths.dll + lib/net8.0/ktsu.Semantics.Paths.dll + + + CP0016 + M:ktsu.Semantics.Paths.DirectoryName.Equals(ktsu.Semantics.Strings.SemanticString{ktsu.Semantics.Paths.DirectoryName})$0:[T:System.Runtime.CompilerServices.NullableAttribute] + lib/net7.0/ktsu.Semantics.Paths.dll + lib/net8.0/ktsu.Semantics.Paths.dll + + + CP0016 + M:ktsu.Semantics.Paths.DirectoryName.Equals(System.Object):[T:System.Runtime.CompilerServices.NullableContextAttribute] + lib/net7.0/ktsu.Semantics.Paths.dll + lib/net8.0/ktsu.Semantics.Paths.dll + + + CP0016 + M:ktsu.Semantics.Paths.DirectoryName.op_Equality(ktsu.Semantics.Paths.DirectoryName,ktsu.Semantics.Paths.DirectoryName):[T:System.Runtime.CompilerServices.NullableContextAttribute] + lib/net7.0/ktsu.Semantics.Paths.dll + lib/net8.0/ktsu.Semantics.Paths.dll + + + CP0016 + M:ktsu.Semantics.Paths.DirectoryName.op_Inequality(ktsu.Semantics.Paths.DirectoryName,ktsu.Semantics.Paths.DirectoryName):[T:System.Runtime.CompilerServices.NullableContextAttribute] + lib/net7.0/ktsu.Semantics.Paths.dll + lib/net8.0/ktsu.Semantics.Paths.dll + CP0016 M:ktsu.Semantics.Paths.DirectoryPath.Equals(ktsu.Semantics.Paths.DirectoryPath):[T:System.Runtime.CompilerServices.NullableContextAttribute] @@ -1363,6 +1393,18 @@ lib/net7.0/ktsu.Semantics.Paths.dll lib/net8.0/ktsu.Semantics.Paths.dll + + CP0016 + T:ktsu.Semantics.Paths.DirectoryName:[T:System.Runtime.CompilerServices.NullableAttribute] + lib/net7.0/ktsu.Semantics.Paths.dll + lib/net8.0/ktsu.Semantics.Paths.dll + + + CP0016 + T:ktsu.Semantics.Paths.DirectoryName:[T:System.Runtime.CompilerServices.NullableContextAttribute] + lib/net7.0/ktsu.Semantics.Paths.dll + lib/net8.0/ktsu.Semantics.Paths.dll + CP0016 T:ktsu.Semantics.Paths.DirectoryPath:[T:System.Runtime.CompilerServices.NullableAttribute] @@ -1417,6 +1459,18 @@ lib/net7.0/ktsu.Semantics.Paths.dll lib/net8.0/ktsu.Semantics.Paths.dll + + CP0016 + T:ktsu.Semantics.Paths.IsValidDirectoryNameAttribute:[T:System.Runtime.CompilerServices.NullableAttribute] + lib/net7.0/ktsu.Semantics.Paths.dll + lib/net8.0/ktsu.Semantics.Paths.dll + + + CP0016 + T:ktsu.Semantics.Paths.IsValidDirectoryNameAttribute:[T:System.Runtime.CompilerServices.NullableContextAttribute] + lib/net7.0/ktsu.Semantics.Paths.dll + lib/net8.0/ktsu.Semantics.Paths.dll + CP0016 T:ktsu.Semantics.Paths.IsValidFileNameAttribute:[T:System.Runtime.CompilerServices.NullableAttribute] diff --git a/Semantics.Paths/Implementations/AbsoluteDirectoryPath.cs b/Semantics.Paths/Implementations/AbsoluteDirectoryPath.cs index d8e1223..3300d3a 100644 --- a/Semantics.Paths/Implementations/AbsoluteDirectoryPath.cs +++ b/Semantics.Paths/Implementations/AbsoluteDirectoryPath.cs @@ -128,6 +128,22 @@ protected override IDirectoryPath CreateDirectoryPath(string directoryPath) => return AbsoluteFilePath.Create(combinedPath); } + /// + /// Combines an absolute directory path with a directory name using the '/' operator. + /// + /// The base absolute directory path. + /// The directory name to append. + /// A new representing the combined path. + [SuppressMessage("Usage", "CA2225:Operator overloads have named alternates", Justification = "Path combination is the semantic meaning, not mathematical division")] + public static AbsoluteDirectoryPath operator /(AbsoluteDirectoryPath left, DirectoryName right) + { + Guard.NotNull(left); + Guard.NotNull(right); + + string combinedPath = PooledStringBuilder.CombinePaths(left.WeakString, right.WeakString); + return Create(combinedPath); + } + /// /// Combines an absolute directory path with a file name using the '/' operator. /// diff --git a/Semantics.Paths/Implementations/DirectoryPath.cs b/Semantics.Paths/Implementations/DirectoryPath.cs index b03fc86..50d5c30 100644 --- a/Semantics.Paths/Implementations/DirectoryPath.cs +++ b/Semantics.Paths/Implementations/DirectoryPath.cs @@ -107,6 +107,27 @@ public RelativeDirectoryPath AsRelative(AbsoluteDirectoryPath baseDirectory) return FilePath.Create(combinedPath); } + /// + /// Combines a directory path with a directory name using the '/' operator. + /// + /// The base directory path. + /// The directory name to append. + /// A new representing the combined path. + [SuppressMessage("Usage", "CA2225:Operator overloads have named alternates", Justification = "Path combination is the semantic meaning, not mathematical division")] + public static DirectoryPath operator /(DirectoryPath left, DirectoryName right) + { +#if NET6_0_OR_GREATER + Guard.NotNull(left); + Guard.NotNull(right); +#else + ArgumentNullExceptionPolyfill.ThrowIfNull(left); + ArgumentNullExceptionPolyfill.ThrowIfNull(right); +#endif + + string combinedPath = Path.Combine(left.WeakString, right.WeakString); + return Create(combinedPath); + } + /// /// Combines a directory path with a file name using the '/' operator. /// diff --git a/Semantics.Paths/Implementations/RelativeDirectoryPath.cs b/Semantics.Paths/Implementations/RelativeDirectoryPath.cs index 14e1cfd..c3ffa1e 100644 --- a/Semantics.Paths/Implementations/RelativeDirectoryPath.cs +++ b/Semantics.Paths/Implementations/RelativeDirectoryPath.cs @@ -13,7 +13,7 @@ namespace ktsu.Semantics.Paths; /// /// Represents a relative directory path /// -[IsPath, IsRelativePath, IsDirectoryPath] +[IsPath, IsRelativePath] public sealed record RelativeDirectoryPath : SemanticDirectoryPath, IRelativeDirectoryPath { // Cache for expensive parent directory computation @@ -26,13 +26,13 @@ public sealed record RelativeDirectoryPath : SemanticDirectoryPath _cachedParent ??= Create(Path.GetDirectoryName(WeakString) ?? ""); // Cache for directory name - private FileName? _cachedName; + private DirectoryName? _cachedName; /// /// Gets the name of this directory (the last component of the path). /// - /// A representing just the directory name. - public FileName Name => _cachedName ??= FileName.Create(Path.GetFileName(WeakString) ?? ""); + /// A representing just the directory name. + public DirectoryName Name => _cachedName ??= DirectoryName.Create(Path.GetFileName(WeakString) ?? ""); // Cache for depth calculation private int? _cachedDepth; diff --git a/Semantics.Paths/Implementations/RelativeFilePath.cs b/Semantics.Paths/Implementations/RelativeFilePath.cs index b5e80f7..9098d6c 100644 --- a/Semantics.Paths/Implementations/RelativeFilePath.cs +++ b/Semantics.Paths/Implementations/RelativeFilePath.cs @@ -7,7 +7,7 @@ namespace ktsu.Semantics.Paths; /// /// Represents a relative file path /// -[IsPath, IsRelativePath, IsFilePath] +[IsPath, IsRelativePath] public sealed record RelativeFilePath : SemanticFilePath, IRelativeFilePath { // Cache for expensive directory path computation diff --git a/Semantics.Paths/Interfaces/IDirectoryName.cs b/Semantics.Paths/Interfaces/IDirectoryName.cs new file mode 100644 index 0000000..461c17e --- /dev/null +++ b/Semantics.Paths/Interfaces/IDirectoryName.cs @@ -0,0 +1,12 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.Semantics.Paths; + +/// +/// Interface for directory names +/// +public interface IDirectoryName +{ +} diff --git a/Semantics.Paths/Primitives/DirectoryName.cs b/Semantics.Paths/Primitives/DirectoryName.cs new file mode 100644 index 0000000..53ba4da --- /dev/null +++ b/Semantics.Paths/Primitives/DirectoryName.cs @@ -0,0 +1,15 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.Semantics.Paths; + +using ktsu.Semantics.Strings; + +/// +/// Represents a directory name +/// +[IsValidDirectoryName] +public sealed record DirectoryName : SemanticString, IDirectoryName +{ +} diff --git a/Semantics.Paths/Primitives/FileName.cs b/Semantics.Paths/Primitives/FileName.cs index d435257..6e919a7 100644 --- a/Semantics.Paths/Primitives/FileName.cs +++ b/Semantics.Paths/Primitives/FileName.cs @@ -9,7 +9,7 @@ namespace ktsu.Semantics.Paths; /// /// Represents a filename (without directory path) /// -[IsFileName] +[IsValidFileName] public sealed record FileName : SemanticString, IFileName { } diff --git a/Semantics.Paths/Validation/Attributes/Path/IsFileNameAttribute.cs b/Semantics.Paths/Validation/Attributes/Path/IsFileNameAttribute.cs deleted file mode 100644 index 8b9e733..0000000 --- a/Semantics.Paths/Validation/Attributes/Path/IsFileNameAttribute.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.Semantics.Paths; - -using System; -using System.IO; -using System.Linq; -using ktsu.Semantics.Strings; - -/// -/// Validates that a string represents a valid filename (no invalid filename characters, not a directory) -/// -[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] -public sealed class IsFileNameAttribute : NativeSemanticStringValidationAttribute -{ - /// - /// Creates the validation adapter for filename validation. - /// - /// A validation adapter for filenames - protected override ValidationAdapter CreateValidator() => new FileNameValidator(); - - /// - /// validation adapter for filenames. - /// - private sealed class FileNameValidator : ValidationAdapter - { - /// - /// Validates that a string represents a valid filename. - /// - /// The string value to validate - /// A validation result indicating success or failure - /// - /// A valid filename must meet the following criteria: - /// - /// Must not contain any characters from - /// Must not be an existing directory path - /// Empty or null strings are considered valid - /// - /// - protected override ValidationResult ValidateValue(string value) - { - if (string.IsNullOrEmpty(value)) - { - return ValidationResult.Success(); - } - - bool isValidFileName = !Directory.Exists(value) && !value.Intersect(Path.GetInvalidFileNameChars()).Any(); - return isValidFileName - ? ValidationResult.Success() - : ValidationResult.Failure("The filename contains invalid characters or is an existing directory."); - } - } -} diff --git a/Semantics.Paths/Validation/Attributes/Path/IsValidDirectoryNameAttribute.cs b/Semantics.Paths/Validation/Attributes/Path/IsValidDirectoryNameAttribute.cs new file mode 100644 index 0000000..68ef27d --- /dev/null +++ b/Semantics.Paths/Validation/Attributes/Path/IsValidDirectoryNameAttribute.cs @@ -0,0 +1,63 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.Semantics.Paths; + +using System; +using System.IO; +using ktsu.Semantics.Strings; + +/// +/// Validates that a path string contains valid directory name characters (no path separators) using span semantics. +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] +public sealed class IsValidDirectoryNameAttribute : NativeSemanticStringValidationAttribute +{ + /// + /// Creates the validation adapter for valid directory name validation. + /// + /// A validation adapter for valid directory name strings + protected override ValidationAdapter CreateValidator() => new ValidDirectoryNameValidator(); + + /// + /// validation adapter for valid directory name strings. + /// + private sealed class ValidDirectoryNameValidator : ValidationAdapter + { + private static readonly char[] InvalidFileNameChars = Path.GetInvalidFileNameChars(); + + /// + /// Validates that a directory name string contains only valid characters and no path separators. + /// + /// The string value to validate + /// A validation result indicating success or failure + protected override ValidationResult ValidateValue(string value) + { + if (string.IsNullOrEmpty(value)) + { + return ValidationResult.Success(); + } + + // Check for invalid filename characters +#if NETSTANDARD2_0 + bool hasInvalidChars = value.IndexOfAny(InvalidFileNameChars) != -1; +#else + ReadOnlySpan valueSpan = value.AsSpan(); + bool hasInvalidChars = valueSpan.IndexOfAny(InvalidFileNameChars) != -1; +#endif + if (hasInvalidChars) + { + return ValidationResult.Failure("The directory name contains invalid characters."); + } + + // Check for path separators (directory names shouldn't contain path separators) + if (value.Contains(Path.DirectorySeparatorChar) || value.Contains(Path.AltDirectorySeparatorChar)) + { + return ValidationResult.Failure("The directory name contains path separators."); + } + + return ValidationResult.Success(); + } + } +} diff --git a/Semantics.Test/PathValidationAttributeTests.cs b/Semantics.Test/PathValidationAttributeTests.cs index f3ee818..4ee6b31 100644 --- a/Semantics.Test/PathValidationAttributeTests.cs +++ b/Semantics.Test/PathValidationAttributeTests.cs @@ -326,6 +326,86 @@ public void IsFileNameAttribute_FileNameWithValidSpecialChars_ShouldPass() // Act & Assert Assert.IsTrue(specialCharsName.IsValid()); } + + [TestMethod] + public void IsValidDirectoryNameAttribute_ValidDirectoryName_ShouldPass() + { + // Arrange + TestDirectoryName validName = TestDirectoryName.Create("MyFolder"); + + // Act & Assert + Assert.IsTrue(validName.IsValid()); + } + + [TestMethod] + public void IsValidDirectoryNameAttribute_DirectoryNameWithSpaces_ShouldPass() + { + // Arrange + TestDirectoryName nameWithSpaces = TestDirectoryName.Create("My Folder Name"); + + // Act & Assert + Assert.IsTrue(nameWithSpaces.IsValid()); + } + + [TestMethod] + public void IsValidDirectoryNameAttribute_DirectoryNameWithValidSpecialChars_ShouldPass() + { + // Arrange - using valid special chars + TestDirectoryName specialCharsName = TestDirectoryName.Create("valid-folder_name (1)"); + + // Act & Assert + Assert.IsTrue(specialCharsName.IsValid()); + } + + [TestMethod] + public void IsValidDirectoryNameAttribute_EmptyDirectoryName_ShouldPass() + { + // Arrange + TestDirectoryName emptyName = TestDirectoryName.Create(""); + + // Act & Assert + Assert.IsTrue(emptyName.IsValid()); + } + + [TestMethod] + public void IsValidDirectoryNameAttribute_DirectoryNameWithPathSeparator_ShouldFail() + { + // Arrange & Act & Assert - directory names shouldn't contain path separators + Assert.ThrowsExactly(() => + TestDirectoryName.Create("folder\\subfolder")); + } + + [TestMethod] + public void IsValidDirectoryNameAttribute_DirectoryNameWithForwardSlash_ShouldFail() + { + // Arrange & Act & Assert - directory names shouldn't contain forward slashes + Assert.ThrowsExactly(() => + TestDirectoryName.Create("folder/subfolder")); + } + + [TestMethod] + public void IsValidDirectoryNameAttribute_DirectoryNameWithInvalidChars_ShouldFail() + { + // Arrange & Act & Assert - test with invalid filename characters + Assert.ThrowsExactly(() => + TestDirectoryName.Create("invalid<>name")); + } + + [TestMethod] + public void IsValidDirectoryNameAttribute_DirectoryNameWithColon_ShouldFail() + { + // Arrange & Act & Assert - colon is invalid in directory names (except drive letters) + Assert.ThrowsExactly(() => + TestDirectoryName.Create("invalid:name")); + } + + [TestMethod] + public void IsValidDirectoryNameAttribute_DirectoryNameWithPipe_ShouldFail() + { + // Arrange & Act & Assert - pipe is an invalid character + Assert.ThrowsExactly(() => + TestDirectoryName.Create("invalid|name")); + } } // Test record types for validation attribute testing @@ -338,7 +418,7 @@ public record TestAbsolutePath : SemanticString { } [IsRelativePath] public record TestRelativePath : SemanticString { } -[IsFileName] +[IsValidFileName] public record TestFileName : SemanticString { } [IsDirectoryPath] @@ -352,3 +432,6 @@ public record TestExistingPath : SemanticString { } [IsExtension] public record TestExtension : SemanticString { } + +[IsValidDirectoryName] +public record TestDirectoryName : SemanticString { } diff --git a/Semantics.Test/Paths/DirectoryNameTests.cs b/Semantics.Test/Paths/DirectoryNameTests.cs new file mode 100644 index 0000000..4cc923b --- /dev/null +++ b/Semantics.Test/Paths/DirectoryNameTests.cs @@ -0,0 +1,270 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.Semantics.Test.Paths; + +using System.Collections.Generic; +using ktsu.Semantics.Paths; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class DirectoryNameTests +{ + [TestMethod] + public void DirectoryName_Create_WithValidName_Succeeds() + { + // Test creating DirectoryName with valid name + DirectoryName name = DirectoryName.Create("MyFolder"); + + Assert.IsNotNull(name); + Assert.AreEqual("MyFolder", name.WeakString); + } + + [TestMethod] + public void DirectoryName_Create_WithSpaces_Succeeds() + { + // Test creating DirectoryName with spaces + DirectoryName name = DirectoryName.Create("My Folder Name"); + + Assert.IsNotNull(name); + Assert.AreEqual("My Folder Name", name.WeakString); + } + + [TestMethod] + public void DirectoryName_Create_WithSpecialCharacters_Succeeds() + { + // Test creating DirectoryName with valid special characters + DirectoryName name = DirectoryName.Create("folder-name_v2 (beta)"); + + Assert.IsNotNull(name); + Assert.AreEqual("folder-name_v2 (beta)", name.WeakString); + } + + [TestMethod] + public void DirectoryName_Create_WithPathSeparator_ThrowsException() + { + // Test that DirectoryName rejects path separators + Assert.ThrowsExactly(() => + DirectoryName.Create("folder\\subfolder")); + } + + [TestMethod] + public void DirectoryName_Create_WithForwardSlash_ThrowsException() + { + // Test that DirectoryName rejects forward slashes + Assert.ThrowsExactly(() => + DirectoryName.Create("folder/subfolder")); + } + + [TestMethod] + public void DirectoryName_Create_WithInvalidCharacters_ThrowsException() + { + // Test that DirectoryName rejects invalid filename characters + Assert.ThrowsExactly(() => + DirectoryName.Create("invalid<>name")); + + Assert.ThrowsExactly(() => + DirectoryName.Create("invalid:name")); + + Assert.ThrowsExactly(() => + DirectoryName.Create("invalid|name")); + + Assert.ThrowsExactly(() => + DirectoryName.Create("invalid\"name")); + } + + [TestMethod] + public void DirectoryName_Create_WithEmptyString_Succeeds() + { + // Test that empty DirectoryName is valid + DirectoryName name = DirectoryName.Create(""); + + Assert.IsNotNull(name); + Assert.AreEqual("", name.WeakString); + } + + [TestMethod] + public void DirectoryName_TryCreate_WithValidName_ReturnsTrue() + { + // Test TryCreate with valid directory name + bool success = DirectoryName.TryCreate("ValidFolder", out DirectoryName? result); + + Assert.IsTrue(success); + Assert.IsNotNull(result); + Assert.AreEqual("ValidFolder", result.WeakString); + } + + [TestMethod] + public void DirectoryName_TryCreate_WithInvalidName_ReturnsFalse() + { + // Test TryCreate with invalid directory name + bool success = DirectoryName.TryCreate("invalid\\name", out DirectoryName? result); + + Assert.IsFalse(success); + Assert.IsNull(result); + } + + [TestMethod] + public void DirectoryName_ExplicitCast_FromString_WorksCorrectly() + { + // Test explicit cast from string + DirectoryName name = DirectoryName.Create("MyFolder"); + + Assert.IsNotNull(name); + Assert.AreEqual("MyFolder", name.WeakString); + } + + [TestMethod] + public void DirectoryName_ImplementsIDirectoryName_Interface() + { + // Test that DirectoryName implements IDirectoryName + DirectoryName name = DirectoryName.Create("TestFolder"); + IDirectoryName interfaceReference = name; + + Assert.IsNotNull(interfaceReference); + Assert.IsInstanceOfType(name); + } + + [TestMethod] + public void DirectoryName_ImplementsISemanticString_Interface() + { + // Test that DirectoryName implements ISemanticString + DirectoryName name = DirectoryName.Create("TestFolder"); + DirectoryName interfaceReference = name; + + Assert.IsNotNull(interfaceReference); + Assert.AreEqual("TestFolder", interfaceReference.WeakString); + } + + [TestMethod] + public void DirectoryName_Equality_WorksCorrectly() + { + // Test equality comparison + DirectoryName name1 = DirectoryName.Create("MyFolder"); + DirectoryName name2 = DirectoryName.Create("MyFolder"); + DirectoryName name3 = DirectoryName.Create("OtherFolder"); + + Assert.AreEqual(name1, name2); + Assert.AreNotEqual(name1, name3); + } + + [TestMethod] + public void DirectoryName_GetHashCode_ConsistentForEqualValues() + { + // Test that hash codes are consistent + DirectoryName name1 = DirectoryName.Create("MyFolder"); + DirectoryName name2 = DirectoryName.Create("MyFolder"); + + Assert.AreEqual(name1.GetHashCode(), name2.GetHashCode()); + } + + [TestMethod] + public void DirectoryName_ToString_ReturnsWeakString() + { + // Test ToString method + DirectoryName name = DirectoryName.Create("MyFolder"); + + string result = name.ToString(); + + Assert.AreEqual("MyFolder", result); + } + + [TestMethod] + public void DirectoryName_UsedInDictionary_WorksCorrectly() + { + // Test that DirectoryName can be used as dictionary key + Dictionary dict = []; + DirectoryName key = DirectoryName.Create("MyFolder"); + + dict[key] = "some value"; + + Assert.AreEqual("some value", dict[key]); + Assert.IsTrue(dict.ContainsKey(DirectoryName.Create("MyFolder"))); + } + + [TestMethod] + public void DirectoryName_CombineWithAbsoluteDirectoryPath_CreatesValidPath() + { + // Test combining DirectoryName with AbsoluteDirectoryPath + AbsoluteDirectoryPath basePath = AbsoluteDirectoryPath.Create(@"C:\projects"); + DirectoryName subDir = DirectoryName.Create("myapp"); + + AbsoluteDirectoryPath result = basePath / subDir; + + Assert.IsNotNull(result); + Assert.Contains("myapp", result.WeakString); + } + + [TestMethod] + public void DirectoryName_CombineWithDirectoryPath_CreatesValidPath() + { + // Test combining DirectoryName with DirectoryPath + DirectoryPath basePath = DirectoryPath.Create("projects"); + DirectoryName subDir = DirectoryName.Create("myapp"); + + DirectoryPath result = basePath / subDir; + + Assert.IsNotNull(result); + Assert.Contains("myapp", result.WeakString); + } + + [TestMethod] + public void DirectoryName_MultipleInChain_CreatesDeepPath() + { + // Test chaining multiple DirectoryName combinations + DirectoryPath basePath = DirectoryPath.Create("root"); + DirectoryName dir1 = DirectoryName.Create("level1"); + DirectoryName dir2 = DirectoryName.Create("level2"); + DirectoryName dir3 = DirectoryName.Create("level3"); + + DirectoryPath result = basePath / dir1 / dir2 / dir3; + + Assert.IsNotNull(result); + Assert.Contains("root", result.WeakString); + Assert.Contains("level1", result.WeakString); + Assert.Contains("level2", result.WeakString); + Assert.Contains("level3", result.WeakString); + } + + [TestMethod] + public void DirectoryName_WithUnicodeCharacters_WorksCorrectly() + { + // Test DirectoryName with Unicode characters + DirectoryName name = DirectoryName.Create("文件夹名称"); + + Assert.IsNotNull(name); + Assert.AreEqual("文件夹名称", name.WeakString); + } + + [TestMethod] + public void DirectoryName_WithNumbers_WorksCorrectly() + { + // Test DirectoryName with numbers + DirectoryName name = DirectoryName.Create("folder123"); + + Assert.IsNotNull(name); + Assert.AreEqual("folder123", name.WeakString); + } + + [TestMethod] + public void DirectoryName_WithDots_WorksCorrectly() + { + // Test DirectoryName with dots (but not as path traversal) + DirectoryName name = DirectoryName.Create("my.folder.name"); + + Assert.IsNotNull(name); + Assert.AreEqual("my.folder.name", name.WeakString); + } + + [TestMethod] + public void DirectoryName_IsValid_WithValidName_ReturnsTrue() + { + // Test IsValid method with valid name + DirectoryName name = DirectoryName.Create("ValidFolder"); + + bool isValid = name.IsValid(); + + Assert.IsTrue(isValid); + } +} diff --git a/Semantics.Test/Paths/PathConversionTests.cs b/Semantics.Test/Paths/PathConversionTests.cs index ac94d5d..4447ba1 100644 --- a/Semantics.Test/Paths/PathConversionTests.cs +++ b/Semantics.Test/Paths/PathConversionTests.cs @@ -4,7 +4,6 @@ namespace ktsu.Semantics.Test.Paths; -using System; using System.IO; using ktsu.Semantics.Paths; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -42,24 +41,16 @@ public void AsRelative_WithNullBaseDirectory_ThrowsArgumentNullException() public void AsAbsolute_WithCurrentWorkingDirectory_WorksCorrectly() { // Test AsAbsolute() without base directory (uses current working directory) - RelativeFilePath relativeFile = RelativeFilePath.Create("test.txt"); - RelativeDirectoryPath relativeDir = RelativeDirectoryPath.Create("test"); FilePath genericFile = FilePath.Create("test.txt"); DirectoryPath genericDir = DirectoryPath.Create("test"); - AbsoluteFilePath absoluteFile1 = relativeFile.AsAbsolute(); - AbsoluteDirectoryPath absoluteDir1 = relativeDir.AsAbsolute(); AbsoluteFilePath absoluteFile2 = genericFile.AsAbsolute(); AbsoluteDirectoryPath absoluteDir2 = genericDir.AsAbsolute(); - Assert.IsNotNull(absoluteFile1); - Assert.IsNotNull(absoluteDir1); Assert.IsNotNull(absoluteFile2); Assert.IsNotNull(absoluteDir2); // Should be fully qualified paths - Assert.IsTrue(Path.IsPathFullyQualified(absoluteFile1.WeakString)); - Assert.IsTrue(Path.IsPathFullyQualified(absoluteDir1.WeakString)); Assert.IsTrue(Path.IsPathFullyQualified(absoluteFile2.WeakString)); Assert.IsTrue(Path.IsPathFullyQualified(absoluteDir2.WeakString)); } @@ -208,32 +199,24 @@ public void AsAbsolute_ReturnsCorrectConcreteTypes() AbsolutePath absolutePath = AbsolutePath.Create(@"C:\test"); RelativePath relativePath = RelativePath.Create("test"); AbsoluteFilePath absoluteFile = AbsoluteFilePath.Create(@"C:\test\file.txt"); - RelativeFilePath relativeFile = RelativeFilePath.Create("file.txt"); AbsoluteDirectoryPath absoluteDir = AbsoluteDirectoryPath.Create(@"C:\test"); - RelativeDirectoryPath relativeDir = RelativeDirectoryPath.Create("test"); // Test AsAbsolute methods AbsolutePath result1 = absolutePath.AsAbsolute(); AbsolutePath result2 = relativePath.AsAbsolute(); AbsoluteFilePath result3 = absoluteFile.AsAbsolute(); - AbsoluteFilePath result4 = relativeFile.AsAbsolute(); AbsoluteDirectoryPath result5 = absoluteDir.AsAbsolute(); - AbsoluteDirectoryPath result6 = relativeDir.AsAbsolute(); // Verify correct types are returned Assert.IsInstanceOfType(result1); Assert.IsInstanceOfType(result2); Assert.IsInstanceOfType(result3); - Assert.IsInstanceOfType(result4); Assert.IsInstanceOfType(result5); - Assert.IsInstanceOfType(result6); // Verify they're not null and valid Assert.IsNotNull(result1); Assert.IsNotNull(result2); Assert.IsNotNull(result3); - Assert.IsNotNull(result4); Assert.IsNotNull(result5); - Assert.IsNotNull(result6); } } diff --git a/Semantics.Test/Paths/PathIntegrationTests.cs b/Semantics.Test/Paths/PathIntegrationTests.cs new file mode 100644 index 0000000..f7d5d20 --- /dev/null +++ b/Semantics.Test/Paths/PathIntegrationTests.cs @@ -0,0 +1,289 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.Semantics.Test.Paths; + +using System.Collections.Generic; +using ktsu.Semantics.Paths; +using ktsu.Semantics.Strings; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class PathIntegrationTests +{ + [TestMethod] + public void MixedPathTypes_InCollection_WorkCorrectly() + { + // Test that different path types can coexist in polymorphic collections + List paths = + [ + AbsoluteDirectoryPath.Create(@"C:\projects"), + AbsoluteFilePath.Create(@"C:\file.txt"), + RelativeDirectoryPath.Create("subfolder"), + RelativeFilePath.Create("file.txt"), + DirectoryPath.Create("any"), + FilePath.Create("any.txt") + ]; + + Assert.AreEqual(6, paths.Count); + foreach (IPath path in paths) + { + Assert.IsNotNull(path); + // All created paths are valid by construction + } + } + + [TestMethod] + public void DirectoryNames_InCollection_WorkCorrectly() + { + // Test DirectoryName in collections + List names = + [ + DirectoryName.Create("folder1"), + DirectoryName.Create("folder2"), + DirectoryName.Create("folder3") + ]; + + Assert.AreEqual(3, names.Count); + Assert.IsTrue(names.Contains(DirectoryName.Create("folder1"))); + } + + [TestMethod] + public void DirectoryNames_AsSet_WorkCorrectly() + { + // Test DirectoryName in HashSet + HashSet uniqueNames = + [ + DirectoryName.Create("folder1"), + DirectoryName.Create("folder2"), + DirectoryName.Create("folder1") // Duplicate + ]; + + Assert.AreEqual(2, uniqueNames.Count); // Duplicate should be ignored + } + + [TestMethod] + public void ComplexPathConstruction_WithAllTypes_WorksCorrectly() + { + // Test complex path construction scenario + AbsoluteDirectoryPath root = AbsoluteDirectoryPath.Create(@"C:\projects"); + DirectoryName appDir = DirectoryName.Create("myapp"); + DirectoryName srcDir = DirectoryName.Create("src"); + FileName componentFile = FileName.Create("Component.tsx"); + + // Build path step by step + AbsoluteDirectoryPath appPath = root / appDir; + AbsoluteDirectoryPath srcPath = appPath / srcDir; + AbsoluteFilePath filePath = srcPath / componentFile; + + Assert.IsNotNull(filePath); + Assert.Contains(@"C:\projects", filePath.WeakString); + Assert.Contains("myapp", filePath.WeakString); + Assert.Contains("src", filePath.WeakString); + Assert.Contains("Component.tsx", filePath.WeakString); + } + + [TestMethod] + public void RelativeToAbsolute_RoundTrip_WorksCorrectly() + { + // Test converting between relative and absolute paths + RelativeDirectoryPath relative = RelativeDirectoryPath.Create("projects"); + AbsoluteDirectoryPath baseDir = AbsoluteDirectoryPath.Create(@"C:\work"); + + // Convert to absolute with specific base + AbsoluteDirectoryPath absolute = relative.AsAbsolute(baseDir); + + Assert.IsNotNull(absolute); + Assert.Contains(@"C:\work", absolute.WeakString); + Assert.Contains("projects", absolute.WeakString); + } + + [TestMethod] + public void AbsoluteToRelative_RoundTrip_WorksCorrectly() + { + // Test converting from absolute to relative paths + AbsoluteDirectoryPath absolute = AbsoluteDirectoryPath.Create(@"C:\work\projects"); + AbsoluteDirectoryPath baseDir = AbsoluteDirectoryPath.Create(@"C:\work"); + + // Convert to relative + RelativeDirectoryPath relative = absolute.AsRelative(baseDir); + + Assert.IsNotNull(relative); + Assert.Contains("projects", relative.WeakString); + Assert.DoesNotContain(@"C:\work", relative.WeakString); + } + + [TestMethod] + public void PathHierarchy_ParentTraversal_WorksCorrectly() + { + // Test traversing up the directory hierarchy + RelativeDirectoryPath deep = RelativeDirectoryPath.Create(@"a\b\c\d"); + + RelativeDirectoryPath parent1 = deep.Parent; + RelativeDirectoryPath parent2 = parent1.Parent; + RelativeDirectoryPath parent3 = parent2.Parent; + RelativeDirectoryPath parent4 = parent3.Parent; + + Assert.Contains("c", parent1.WeakString); + Assert.Contains("b", parent2.WeakString); + Assert.AreEqual("a", parent3.WeakString); + Assert.AreEqual("", parent4.WeakString); // Should be empty at root + } + + [TestMethod] + public void PathNormalization_WithDotComponents_ResolvesCorrectly() + { + // Test path normalization with . and .. components + RelativeDirectoryPath pathWithDots = RelativeDirectoryPath.Create(@"a\.\b\..\c"); + + RelativeDirectoryPath normalized = pathWithDots.Normalize(); + + Assert.IsNotNull(normalized); + // After normalization, . and .. should be resolved + // a\.\b\..\c -> a\c + Assert.Contains("a", normalized.WeakString); + Assert.Contains("c", normalized.WeakString); + } + + [TestMethod] + public void PathNormalization_WithParentTraversal_ResolvesCorrectly() + { + // Test path normalization with parent directory traversal + RelativeDirectoryPath path = RelativeDirectoryPath.Create(@"a\b\..\..\c"); + + RelativeDirectoryPath normalized = path.Normalize(); + + Assert.IsNotNull(normalized); + // a\b\..\..\c -> c + Assert.AreEqual("c", normalized.WeakString); + } + + [TestMethod] + public void FilePathExtensions_MultipleOperations_WorkCorrectly() + { + // Test multiple extension operations on same file + RelativeFilePath original = RelativeFilePath.Create("document.txt"); + FileExtension mdExt = FileExtension.Create(".md"); + FileExtension pdfExt = FileExtension.Create(".pdf"); + + RelativeFilePath md = original.ChangeExtension(mdExt); + RelativeFilePath pdf = md.ChangeExtension(pdfExt); + RelativeFilePath noExt = pdf.RemoveExtension(); + + Assert.Contains(".md", md.WeakString); + Assert.Contains(".pdf", pdf.WeakString); + Assert.DoesNotContain(".pdf", noExt.WeakString); + Assert.Contains("document", noExt.WeakString); + } + + [TestMethod] + public void DirectoryDepth_Comparison_WorksCorrectly() + { + // Test comparing depths of different paths + RelativeDirectoryPath shallow = RelativeDirectoryPath.Create("a"); + RelativeDirectoryPath medium = RelativeDirectoryPath.Create(@"a\b"); + RelativeDirectoryPath deep = RelativeDirectoryPath.Create(@"a\b\c"); + + Assert.IsTrue(shallow.Depth < medium.Depth); + Assert.IsTrue(medium.Depth < deep.Depth); + Assert.AreEqual(0, shallow.Depth); + Assert.AreEqual(1, medium.Depth); + Assert.AreEqual(2, deep.Depth); + } + + [TestMethod] + public void InterfaceBasedPathOperations_WorkPolymorphically() + { + // Test that interface-based operations work polymorphically + IDirectoryPath dir1 = AbsoluteDirectoryPath.Create(@"C:\test"); + IDirectoryPath dir2 = RelativeDirectoryPath.Create("test"); + IDirectoryPath dir3 = DirectoryPath.Create("test"); + + IPath[] paths = [dir1, dir2, dir3]; + + foreach (IPath path in paths) + { + Assert.IsNotNull(path); + ISemanticString semanticPath = (ISemanticString)path; + Assert.IsNotNull(semanticPath.WeakString); + // All paths are valid by construction + } + } + + [TestMethod] + public void CombiningDirectoryNames_BuildsDeepHierarchy() + { + // Test building deep directory hierarchies with DirectoryName + DirectoryPath root = DirectoryPath.Create("root"); + DirectoryName[] levels = + [ + DirectoryName.Create("level1"), + DirectoryName.Create("level2"), + DirectoryName.Create("level3"), + DirectoryName.Create("level4"), + DirectoryName.Create("level5") + ]; + + DirectoryPath current = root; + foreach (DirectoryName level in levels) + { + current /= level; + } + + // Verify all levels are in the final path + Assert.Contains("root", current.WeakString); + Assert.Contains("level1", current.WeakString); + Assert.Contains("level2", current.WeakString); + Assert.Contains("level3", current.WeakString); + Assert.Contains("level4", current.WeakString); + Assert.Contains("level5", current.WeakString); + } + + [TestMethod] + public void EmptyPaths_HandleCorrectly() + { + // Test that empty paths are handled correctly + DirectoryPath emptyDir = DirectoryPath.Create(""); + FilePath emptyFile = FilePath.Create(""); + DirectoryName emptyName = DirectoryName.Create(""); + FileName emptyFileName = FileName.Create(""); + + Assert.AreEqual("", emptyDir.WeakString); + Assert.AreEqual("", emptyFile.WeakString); + Assert.AreEqual("", emptyName.WeakString); + Assert.AreEqual("", emptyFileName.WeakString); + } + + [TestMethod] + public void RelativePaths_WithDotDot_AreValid() + { + // Test that relative paths with .. components are valid + RelativeDirectoryPath parentRef = RelativeDirectoryPath.Create(".."); + RelativeDirectoryPath multiParent = RelativeDirectoryPath.Create(@"..\..\.."); + RelativeFilePath fileInParent = RelativeFilePath.Create(@"..\file.txt"); + + Assert.IsNotNull(parentRef); + Assert.IsNotNull(multiParent); + Assert.IsNotNull(fileInParent); + Assert.IsTrue(parentRef.IsValid()); + Assert.IsTrue(multiParent.IsValid()); + Assert.IsTrue(fileInParent.IsValid()); + } + + [TestMethod] + public void RelativePaths_WithDot_AreValid() + { + // Test that relative paths with . components are valid + RelativeDirectoryPath currentRef = RelativeDirectoryPath.Create("."); + RelativeDirectoryPath withCurrent = RelativeDirectoryPath.Create(@".\subfolder"); + RelativeFilePath fileInCurrent = RelativeFilePath.Create(@".\file.txt"); + + Assert.IsNotNull(currentRef); + Assert.IsNotNull(withCurrent); + Assert.IsNotNull(fileInCurrent); + Assert.IsTrue(currentRef.IsValid()); + Assert.IsTrue(withCurrent.IsValid()); + Assert.IsTrue(fileInCurrent.IsValid()); + } +} diff --git a/Semantics.Test/Paths/PathOperatorTests.cs b/Semantics.Test/Paths/PathOperatorTests.cs index 2879a5c..6c60c16 100644 --- a/Semantics.Test/Paths/PathOperatorTests.cs +++ b/Semantics.Test/Paths/PathOperatorTests.cs @@ -16,36 +16,18 @@ public void PathOperators_NullArguments_ThrowArgumentNullException() { // Test all path combination operators with null arguments AbsoluteDirectoryPath absoluteDir = AbsoluteDirectoryPath.Create(@"C:\test"); - RelativeDirectoryPath relativeDir = RelativeDirectoryPath.Create(@"test"); DirectoryPath genericDir = DirectoryPath.Create(@"test"); - RelativeDirectoryPath nullRelativeDir = null!; - RelativeFilePath nullRelativeFile = null!; FileName nullFileName = null!; // AbsoluteDirectoryPath operators - Assert.ThrowsExactly(() => absoluteDir / nullRelativeDir); - Assert.ThrowsExactly(() => absoluteDir / nullRelativeFile); Assert.ThrowsExactly(() => absoluteDir / nullFileName); - AbsoluteDirectoryPath nullAbsoluteDir = null!; - Assert.ThrowsExactly(() => nullAbsoluteDir / relativeDir); - - // RelativeDirectoryPath operators - Assert.ThrowsExactly(() => relativeDir / nullRelativeDir); - Assert.ThrowsExactly(() => relativeDir / nullRelativeFile); - Assert.ThrowsExactly(() => relativeDir / nullFileName); - - RelativeDirectoryPath nullRelativeDir2 = null!; - Assert.ThrowsExactly(() => nullRelativeDir2 / relativeDir); - // DirectoryPath operators - Assert.ThrowsExactly(() => genericDir / nullRelativeDir); - Assert.ThrowsExactly(() => genericDir / nullRelativeFile); Assert.ThrowsExactly(() => genericDir / nullFileName); DirectoryPath nullGenericDir = null!; - Assert.ThrowsExactly(() => nullGenericDir / relativeDir); + Assert.ThrowsExactly(() => nullGenericDir / nullFileName); } [TestMethod] @@ -53,18 +35,20 @@ public void PathOperators_ComplexCombinations_WorkCorrectly() { // Test complex path combinations AbsoluteDirectoryPath baseDir = AbsoluteDirectoryPath.Create(@"C:\projects"); - RelativeDirectoryPath subDir1 = RelativeDirectoryPath.Create(@"app\src"); - RelativeDirectoryPath subDir2 = RelativeDirectoryPath.Create(@"components"); + DirectoryPath subDir1 = DirectoryPath.Create(@"app\src"); + DirectoryPath subDir2 = DirectoryPath.Create(@"components"); FileName fileName = FileName.Create("Component.tsx"); - // Chain multiple operations - AbsoluteDirectoryPath combinedDir = baseDir / subDir1 / subDir2; + // Chain multiple operations - combine paths using Path.Combine + AbsoluteDirectoryPath combinedDir = AbsoluteDirectoryPath.Create( + Path.Combine(baseDir.WeakString, subDir1.WeakString, subDir2.WeakString)); AbsoluteFilePath finalFile = combinedDir / fileName; Assert.IsNotNull(combinedDir); Assert.IsNotNull(finalFile); Assert.Contains(@"C:\projects", finalFile.WeakString); - Assert.Contains(@"app\src", finalFile.WeakString); + Assert.Contains(@"app", finalFile.WeakString); + Assert.Contains(@"src", finalFile.WeakString); Assert.Contains(@"components", finalFile.WeakString); Assert.Contains("Component.tsx", finalFile.WeakString); } @@ -74,17 +58,11 @@ public void PathOperators_EmptyPaths_HandleCorrectly() { // Test operators with empty paths AbsoluteDirectoryPath absoluteDir = AbsoluteDirectoryPath.Create(@"C:\test"); - RelativeDirectoryPath emptyRelativeDir = RelativeDirectoryPath.Create(""); - RelativeFilePath emptyRelativeFile = RelativeFilePath.Create(""); FileName emptyFileName = FileName.Create(""); // These should work without throwing - AbsoluteDirectoryPath result1 = absoluteDir / emptyRelativeDir; - AbsoluteFilePath result2 = absoluteDir / emptyRelativeFile; AbsoluteFilePath result3 = absoluteDir / emptyFileName; - Assert.IsNotNull(result1); - Assert.IsNotNull(result2); Assert.IsNotNull(result3); } @@ -93,10 +71,11 @@ public void PathOperators_SpecialCharacters_HandleCorrectly() { // Test with paths containing special characters AbsoluteDirectoryPath baseDir = AbsoluteDirectoryPath.Create(@"C:\test folder"); - RelativeDirectoryPath specialDir = RelativeDirectoryPath.Create(@"sub folder (1)"); + DirectoryPath specialDir = DirectoryPath.Create(@"sub folder (1)"); FileName specialFile = FileName.Create("file name with spaces.txt"); - AbsoluteDirectoryPath combinedDir = baseDir / specialDir; + AbsoluteDirectoryPath combinedDir = AbsoluteDirectoryPath.Create( + Path.Combine(baseDir.WeakString, specialDir.WeakString)); AbsoluteFilePath combinedFile = combinedDir / specialFile; Assert.IsNotNull(combinedDir); @@ -111,35 +90,17 @@ public void PathOperators_ReturnTypes_AreCorrect() { // Verify that operators return the correct types AbsoluteDirectoryPath absoluteDir = AbsoluteDirectoryPath.Create(@"C:\test"); - RelativeDirectoryPath relativeDir = RelativeDirectoryPath.Create(@"sub"); DirectoryPath genericDir = DirectoryPath.Create(@"test"); - RelativeFilePath relativeFile = RelativeFilePath.Create(@"file.txt"); FileName fileName = FileName.Create("file.txt"); // AbsoluteDirectoryPath combinations - AbsoluteDirectoryPath absDir = absoluteDir / relativeDir; - AbsoluteFilePath absFile1 = absoluteDir / relativeFile; AbsoluteFilePath absFile2 = absoluteDir / fileName; - // RelativeDirectoryPath combinations - RelativeDirectoryPath relDir = relativeDir / relativeDir; - RelativeFilePath relFile1 = relativeDir / relativeFile; - RelativeFilePath relFile2 = relativeDir / fileName; - // DirectoryPath combinations - DirectoryPath genDir = genericDir / relativeDir; - FilePath genFile1 = genericDir / relativeFile; FilePath genFile2 = genericDir / fileName; // Verify types - Assert.IsInstanceOfType(absDir); - Assert.IsInstanceOfType(absFile1); Assert.IsInstanceOfType(absFile2); - Assert.IsInstanceOfType(relDir); - Assert.IsInstanceOfType(relFile1); - Assert.IsInstanceOfType(relFile2); - Assert.IsInstanceOfType(genDir); - Assert.IsInstanceOfType(genFile1); Assert.IsInstanceOfType(genFile2); } @@ -182,4 +143,79 @@ public void PathOperators_CrossPlatformSeparators_HandleCorrectly() Assert.IsTrue(combinedDir.IsValid()); Assert.IsTrue(combinedFile.IsValid()); } + + [TestMethod] + public void DirectoryNameOperator_WithAbsoluteDirectoryPath_CreatesCorrectPath() + { + // Test combining AbsoluteDirectoryPath with DirectoryName + AbsoluteDirectoryPath baseDir = AbsoluteDirectoryPath.Create(@"C:\projects"); + DirectoryName subDir = DirectoryName.Create("myapp"); + + AbsoluteDirectoryPath result = baseDir / subDir; + + Assert.IsNotNull(result); + Assert.IsTrue(result.IsValid()); + Assert.Contains(@"C:\projects", result.WeakString); + Assert.Contains("myapp", result.WeakString); + } + + [TestMethod] + public void DirectoryNameOperator_WithDirectoryPath_CreatesCorrectPath() + { + // Test combining DirectoryPath with DirectoryName + DirectoryPath baseDir = DirectoryPath.Create(@"projects"); + DirectoryName subDir = DirectoryName.Create("myapp"); + + DirectoryPath result = baseDir / subDir; + + Assert.IsNotNull(result); + Assert.IsTrue(result.IsValid()); + Assert.Contains("projects", result.WeakString); + Assert.Contains("myapp", result.WeakString); + } + + [TestMethod] + public void DirectoryNameOperator_ChainedCombinations_WorkCorrectly() + { + // Test chaining multiple DirectoryName combinations + DirectoryPath baseDir = DirectoryPath.Create(@"projects"); + DirectoryName dir1 = DirectoryName.Create("app"); + DirectoryName dir2 = DirectoryName.Create("src"); + DirectoryName dir3 = DirectoryName.Create("components"); + + DirectoryPath result = baseDir / dir1 / dir2 / dir3; + + Assert.IsNotNull(result); + Assert.IsTrue(result.IsValid()); + Assert.Contains("projects", result.WeakString); + Assert.Contains("app", result.WeakString); + Assert.Contains("src", result.WeakString); + Assert.Contains("components", result.WeakString); + } + + [TestMethod] + public void DirectoryNameOperator_WithNullDirectoryName_ThrowsArgumentNullException() + { + // Test null safety for DirectoryName operators + AbsoluteDirectoryPath absoluteDir = AbsoluteDirectoryPath.Create(@"C:\test"); + DirectoryPath genericDir = DirectoryPath.Create(@"test"); + DirectoryName nullDirName = null!; + + Assert.ThrowsExactly(() => absoluteDir / nullDirName); + Assert.ThrowsExactly(() => genericDir / nullDirName); + } + + [TestMethod] + public void DirectoryNameOperator_WithSpecialCharacters_HandlesCorrectly() + { + // Test DirectoryName with valid special characters + DirectoryPath baseDir = DirectoryPath.Create(@"projects"); + DirectoryName specialDir = DirectoryName.Create("my-app_v2 (beta)"); + + DirectoryPath result = baseDir / specialDir; + + Assert.IsNotNull(result); + Assert.IsTrue(result.IsValid()); + Assert.Contains("my-app_v2 (beta)", result.WeakString); + } } diff --git a/Semantics.Test/Paths/PathUtilityTests.cs b/Semantics.Test/Paths/PathUtilityTests.cs index 17b71e1..6a303a8 100644 --- a/Semantics.Test/Paths/PathUtilityTests.cs +++ b/Semantics.Test/Paths/PathUtilityTests.cs @@ -5,7 +5,6 @@ namespace ktsu.Semantics.Test.Paths; using System; -using System.IO; using ktsu.Semantics.Paths; using Microsoft.VisualStudio.TestTools.UnitTesting; diff --git a/Semantics.Test/Paths/RelativePathPropertyTests.cs b/Semantics.Test/Paths/RelativePathPropertyTests.cs new file mode 100644 index 0000000..7451788 --- /dev/null +++ b/Semantics.Test/Paths/RelativePathPropertyTests.cs @@ -0,0 +1,307 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.Semantics.Test.Paths; + +using System; +using ktsu.Semantics.Paths; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class RelativePathPropertyTests +{ + [TestMethod] + public void RelativeDirectoryPath_Name_ReturnsCorrectDirectoryName() + { + // Test that Name property returns the last component as DirectoryName + RelativeDirectoryPath path = RelativeDirectoryPath.Create(@"projects\app\src"); + + DirectoryName name = path.Name; + + Assert.IsNotNull(name); + Assert.AreEqual("src", name.WeakString); + } + + [TestMethod] + public void RelativeDirectoryPath_Name_WithSingleComponent_ReturnsComponent() + { + // Test Name property with single directory name + RelativeDirectoryPath path = RelativeDirectoryPath.Create("myapp"); + + DirectoryName name = path.Name; + + Assert.IsNotNull(name); + Assert.AreEqual("myapp", name.WeakString); + } + + [TestMethod] + public void RelativeDirectoryPath_Name_WithEmptyPath_ReturnsEmpty() + { + // Test Name property with empty path + RelativeDirectoryPath path = RelativeDirectoryPath.Create(""); + + DirectoryName name = path.Name; + + Assert.IsNotNull(name); + Assert.AreEqual("", name.WeakString); + } + + [TestMethod] + public void RelativeDirectoryPath_Parent_ReturnsCorrectParent() + { + // Test that Parent property returns parent directory + RelativeDirectoryPath path = RelativeDirectoryPath.Create(@"projects\app\src"); + + RelativeDirectoryPath parent = path.Parent; + + Assert.IsNotNull(parent); + Assert.Contains("projects", parent.WeakString); + Assert.Contains("app", parent.WeakString); + Assert.DoesNotContain("src", parent.WeakString); + } + + [TestMethod] + public void RelativeDirectoryPath_Parent_WithSingleComponent_ReturnsEmpty() + { + // Test Parent property with single directory + RelativeDirectoryPath path = RelativeDirectoryPath.Create("myapp"); + + RelativeDirectoryPath parent = path.Parent; + + Assert.IsNotNull(parent); + Assert.AreEqual("", parent.WeakString); + } + + [TestMethod] + public void RelativeDirectoryPath_Depth_CalculatesCorrectly() + { + // Test Depth property calculation + RelativeDirectoryPath shallow = RelativeDirectoryPath.Create("myapp"); + RelativeDirectoryPath medium = RelativeDirectoryPath.Create(@"projects\myapp"); + RelativeDirectoryPath deep = RelativeDirectoryPath.Create(@"projects\myapp\src\components"); + + Assert.AreEqual(0, shallow.Depth); + Assert.AreEqual(1, medium.Depth); + Assert.AreEqual(3, deep.Depth); + } + + [TestMethod] + public void RelativeDirectoryPath_Depth_WithEmptyPath_ReturnsZero() + { + // Test Depth property with empty path + RelativeDirectoryPath empty = RelativeDirectoryPath.Create(""); + + Assert.AreEqual(0, empty.Depth); + } + + [TestMethod] + public void RelativeFilePath_RelativeDirectoryPath_ReturnsCorrectDirectory() + { + // Test RelativeDirectoryPath property + RelativeFilePath file = RelativeFilePath.Create(@"projects\app\Component.tsx"); + + RelativeDirectoryPath dir = file.RelativeDirectoryPath; + + Assert.IsNotNull(dir); + Assert.Contains("projects", dir.WeakString); + Assert.Contains("app", dir.WeakString); + Assert.DoesNotContain("Component", dir.WeakString); + } + + [TestMethod] + public void RelativeFilePath_RelativeDirectoryPath_WithFileInRoot_ReturnsEmpty() + { + // Test RelativeDirectoryPath property with file in root + RelativeFilePath file = RelativeFilePath.Create("file.txt"); + + RelativeDirectoryPath dir = file.RelativeDirectoryPath; + + Assert.IsNotNull(dir); + Assert.AreEqual("", dir.WeakString); + } + + [TestMethod] + public void RelativeFilePath_FileNameWithoutExtension_ReturnsCorrectName() + { + // Test FileNameWithoutExtension property + RelativeFilePath file = RelativeFilePath.Create(@"projects\Component.tsx"); + + FileName name = file.FileNameWithoutExtension; + + Assert.IsNotNull(name); + Assert.AreEqual("Component", name.WeakString); + } + + [TestMethod] + public void RelativeFilePath_FileNameWithoutExtension_WithMultipleExtensions_RemovesLastOnly() + { + // Test FileNameWithoutExtension with multiple extensions + RelativeFilePath file = RelativeFilePath.Create("archive.tar.gz"); + + FileName name = file.FileNameWithoutExtension; + + Assert.IsNotNull(name); + Assert.AreEqual("archive.tar", name.WeakString); + } + + [TestMethod] + public void RelativeFilePath_ChangeExtension_ChangesCorrectly() + { + // Test ChangeExtension method + RelativeFilePath file = RelativeFilePath.Create(@"projects\file.txt"); + FileExtension newExt = FileExtension.Create(".md"); + + RelativeFilePath result = file.ChangeExtension(newExt); + + Assert.IsNotNull(result); + Assert.Contains("file.md", result.WeakString); + Assert.DoesNotContain(".txt", result.WeakString); + } + + [TestMethod] + public void RelativeFilePath_ChangeExtension_WithNullExtension_ThrowsException() + { + // Test ChangeExtension with null extension + RelativeFilePath file = RelativeFilePath.Create("file.txt"); + + Assert.ThrowsExactly(() => file.ChangeExtension(null!)); + } + + [TestMethod] + public void RelativeFilePath_RemoveExtension_RemovesCorrectly() + { + // Test RemoveExtension method + RelativeFilePath file = RelativeFilePath.Create(@"projects\file.txt"); + + RelativeFilePath result = file.RemoveExtension(); + + Assert.IsNotNull(result); + Assert.Contains("file", result.WeakString); + Assert.DoesNotContain(".txt", result.WeakString); + } + + [TestMethod] + public void RelativeFilePath_RemoveExtension_WithNoExtension_ReturnsUnchanged() + { + // Test RemoveExtension with file without extension + RelativeFilePath file = RelativeFilePath.Create(@"projects\README"); + + RelativeFilePath result = file.RemoveExtension(); + + Assert.IsNotNull(result); + Assert.Contains("README", result.WeakString); + } + + [TestMethod] + public void RelativeDirectoryPath_CombineWithRelativeDirectory_WorksCorrectly() + { + // Test combining relative directories + RelativeDirectoryPath base1 = RelativeDirectoryPath.Create("projects"); + RelativeDirectoryPath sub = RelativeDirectoryPath.Create("app"); + + RelativeDirectoryPath result = base1 / sub; + + Assert.IsNotNull(result); + Assert.Contains("projects", result.WeakString); + Assert.Contains("app", result.WeakString); + } + + [TestMethod] + public void RelativeDirectoryPath_CombineWithRelativeFile_WorksCorrectly() + { + // Test combining relative directory with relative file + RelativeDirectoryPath dir = RelativeDirectoryPath.Create("projects"); + RelativeFilePath file = RelativeFilePath.Create("file.txt"); + + RelativeFilePath result = dir / file; + + Assert.IsNotNull(result); + Assert.Contains("projects", result.WeakString); + Assert.Contains("file.txt", result.WeakString); + } + + [TestMethod] + public void RelativeDirectoryPath_CombineWithFileName_WorksCorrectly() + { + // Test combining relative directory with file name + RelativeDirectoryPath dir = RelativeDirectoryPath.Create("projects"); + FileName file = FileName.Create("Component.tsx"); + + RelativeFilePath result = dir / file; + + Assert.IsNotNull(result); + Assert.Contains("projects", result.WeakString); + Assert.Contains("Component.tsx", result.WeakString); + } + + [TestMethod] + public void RelativeDirectoryPath_AsRelative_ReturnsSelf() + { + // Test that AsRelative returns self for already relative paths + RelativeDirectoryPath path = RelativeDirectoryPath.Create("projects"); + AbsoluteDirectoryPath baseDir = AbsoluteDirectoryPath.Create(@"C:\temp"); + + RelativeDirectoryPath result = path.AsRelative(baseDir); + + Assert.AreSame(path, result); + } + + [TestMethod] + public void RelativeFilePath_AsRelative_ReturnsSelf() + { + // Test that AsRelative returns self for already relative file paths + RelativeFilePath path = RelativeFilePath.Create("file.txt"); + AbsoluteDirectoryPath baseDir = AbsoluteDirectoryPath.Create(@"C:\temp"); + + RelativeFilePath result = path.AsRelative(baseDir); + + Assert.AreSame(path, result); + } + + [TestMethod] + public void RelativeDirectoryPath_AsAbsoluteWithBase_ResolvesCorrectly() + { + // Test AsAbsolute with explicit base directory + RelativeDirectoryPath relative = RelativeDirectoryPath.Create("projects"); + AbsoluteDirectoryPath baseDir = AbsoluteDirectoryPath.Create(@"C:\work"); + + AbsoluteDirectoryPath result = relative.AsAbsolute(baseDir); + + Assert.IsNotNull(result); + Assert.Contains(@"C:\work", result.WeakString); + Assert.Contains("projects", result.WeakString); + } + + [TestMethod] + public void RelativeFilePath_AsAbsoluteWithBase_ResolvesCorrectly() + { + // Test AsAbsolute with explicit base directory for files + RelativeFilePath relative = RelativeFilePath.Create("file.txt"); + AbsoluteDirectoryPath baseDir = AbsoluteDirectoryPath.Create(@"C:\work"); + + AbsoluteFilePath result = relative.AsAbsolute(baseDir); + + Assert.IsNotNull(result); + Assert.Contains(@"C:\work", result.WeakString); + Assert.Contains("file.txt", result.WeakString); + } + + [TestMethod] + public void RelativeDirectoryPath_AsAbsoluteWithBase_WithNullBase_ThrowsException() + { + // Test AsAbsolute with null base directory + RelativeDirectoryPath relative = RelativeDirectoryPath.Create("projects"); + + Assert.ThrowsExactly(() => relative.AsAbsolute(null!)); + } + + [TestMethod] + public void RelativeFilePath_AsAbsoluteWithBase_WithNullBase_ThrowsException() + { + // Test AsAbsolute with null base directory for files + RelativeFilePath relative = RelativeFilePath.Create("file.txt"); + + Assert.ThrowsExactly(() => relative.AsAbsolute(null!)); + } +} diff --git a/Semantics.Test/Paths/SemanticPathInterfaceTests.cs b/Semantics.Test/Paths/SemanticPathInterfaceTests.cs index 6a16dc5..04bde51 100644 --- a/Semantics.Test/Paths/SemanticPathInterfaceTests.cs +++ b/Semantics.Test/Paths/SemanticPathInterfaceTests.cs @@ -17,21 +17,17 @@ public void DirectoryPath_CombineWithFileName_WorksCorrectly() FileName fileName = FileName.Create("test.txt"); AbsoluteDirectoryPath absoluteDir = AbsoluteDirectoryPath.Create(@"C:\temp"); - RelativeDirectoryPath relativeDir = RelativeDirectoryPath.Create(@"temp"); DirectoryPath genericDir = DirectoryPath.Create(@"temp"); // Act AbsoluteFilePath absoluteResult = absoluteDir / fileName; - RelativeFilePath relativeResult = relativeDir / fileName; FilePath genericResult = genericDir / fileName; // Assert Assert.IsNotNull(absoluteResult); - Assert.IsNotNull(relativeResult); Assert.IsNotNull(genericResult); Assert.AreEqual(@"C:\temp\test.txt", absoluteResult.WeakString); - Assert.AreEqual(@"temp\test.txt", relativeResult.WeakString); Assert.AreEqual(@"temp\test.txt", genericResult.WeakString); } } diff --git a/Semantics.Test/SemanticPathInterfaceTests.cs b/Semantics.Test/SemanticPathInterfaceTests.cs index d59086f..a6f52ff 100644 --- a/Semantics.Test/SemanticPathInterfaceTests.cs +++ b/Semantics.Test/SemanticPathInterfaceTests.cs @@ -221,32 +221,26 @@ public void PolymorphicCollection_CanStoreAllPathTypes() paths.Add(FilePath.Create("file.txt")); paths.Add(DirectoryPath.Create("directory")); paths.Add(AbsoluteFilePath.Create("C:\\file.txt")); - paths.Add(RelativeFilePath.Create("relative\\file.txt")); paths.Add(AbsoluteDirectoryPath.Create("C:\\directory")); - paths.Add(RelativeDirectoryPath.Create("relative\\directory")); filePaths.Add(FilePath.Create("file.txt")); filePaths.Add(AbsoluteFilePath.Create("C:\\file.txt")); - filePaths.Add(RelativeFilePath.Create("relative\\file.txt")); directoryPaths.Add(DirectoryPath.Create("directory")); directoryPaths.Add(AbsoluteDirectoryPath.Create("C:\\directory")); - directoryPaths.Add(RelativeDirectoryPath.Create("relative\\directory")); absolutePaths.Add(AbsolutePath.Create("C:\\absolute\\path")); absolutePaths.Add(AbsoluteFilePath.Create("C:\\file.txt")); absolutePaths.Add(AbsoluteDirectoryPath.Create("C:\\directory")); relativePaths.Add(RelativePath.Create("relative\\path")); - relativePaths.Add(RelativeFilePath.Create("relative\\file.txt")); - relativePaths.Add(RelativeDirectoryPath.Create("relative\\directory")); // Assert - Assert.HasCount(9, paths); - Assert.HasCount(3, filePaths); - Assert.HasCount(3, directoryPaths); + Assert.HasCount(7, paths); + Assert.HasCount(2, filePaths); + Assert.HasCount(2, directoryPaths); Assert.HasCount(3, absolutePaths); - Assert.HasCount(3, relativePaths); + Assert.HasCount(1, relativePaths); // Verify all items can be cast to IPath Assert.IsTrue(paths.All(p => p is not null)); @@ -264,19 +258,17 @@ public void PolymorphicMethods_CanAcceptInterfaceParameters() static string ProcessFilePath(IFilePath filePath) => $"Processing file: {filePath}"; static string ProcessDirectoryPath(IDirectoryPath directoryPath) => $"Processing directory: {directoryPath}"; static string ProcessAbsolutePath(IAbsolutePath absolutePath) => $"Processing absolute: {absolutePath}"; - static string ProcessRelativePath(IRelativePath relativePath) => $"Processing relative: {relativePath}"; AbsoluteFilePath absoluteFilePath = AbsoluteFilePath.Create("C:\\test\\file.txt"); - RelativeDirectoryPath relativeDirectoryPath = RelativeDirectoryPath.Create("test\\directory"); + DirectoryPath directoryPath = DirectoryPath.Create("test\\directory"); // Act & Assert - Test that polymorphic methods work Assert.AreEqual("Processing path: C:\\test\\file.txt", ProcessPath(absoluteFilePath)); Assert.AreEqual("Processing file: C:\\test\\file.txt", ProcessFilePath(absoluteFilePath)); Assert.AreEqual("Processing absolute: C:\\test\\file.txt", ProcessAbsolutePath(absoluteFilePath)); - Assert.AreEqual("Processing path: test\\directory", ProcessPath(relativeDirectoryPath)); - Assert.AreEqual("Processing directory: test\\directory", ProcessDirectoryPath(relativeDirectoryPath)); - Assert.AreEqual("Processing relative: test\\directory", ProcessRelativePath(relativeDirectoryPath)); + Assert.AreEqual("Processing path: test\\directory", ProcessPath(directoryPath)); + Assert.AreEqual("Processing directory: test\\directory", ProcessDirectoryPath(directoryPath)); } [TestMethod] @@ -305,25 +297,22 @@ public void TypeChecking_WithInterfaces_WorksCorrectly() List paths = [ AbsoluteFilePath.Create("C:\\file.txt"), - RelativeDirectoryPath.Create("directory") + DirectoryPath.Create("directory") ]; // Act List filePaths = [.. paths.OfType()]; List directoryPaths = [.. paths.OfType()]; List absolutePaths = [.. paths.OfType()]; - List relativePaths = [.. paths.OfType()]; // Assert Assert.HasCount(1, filePaths); Assert.HasCount(1, directoryPaths); Assert.HasCount(1, absolutePaths); - Assert.HasCount(1, relativePaths); Assert.IsInstanceOfType(filePaths[0]); - Assert.IsInstanceOfType(directoryPaths[0]); + Assert.IsInstanceOfType(directoryPaths[0]); Assert.IsInstanceOfType(absolutePaths[0]); - Assert.IsInstanceOfType(relativePaths[0]); } [TestMethod] @@ -351,43 +340,35 @@ public void AsAbsolute_Method_WorksCorrectly() { // Arrange - Create different path types AbsoluteFilePath absoluteFile = AbsoluteFilePath.Create("C:\\test\\file.txt"); - RelativeFilePath relativeFile = RelativeFilePath.Create("test\\file.txt"); FilePath genericFile = FilePath.Create("file.txt"); AbsoluteDirectoryPath absoluteDir = AbsoluteDirectoryPath.Create("C:\\test\\dir"); - RelativeDirectoryPath relativeDir = RelativeDirectoryPath.Create("test\\dir"); DirectoryPath genericDir = DirectoryPath.Create("dir"); // Act & Assert - Test file paths IFilePath iAbsoluteFile = absoluteFile; - IFilePath iRelativeFile = relativeFile; IFilePath iGenericFile = genericFile; Assert.IsInstanceOfType(iAbsoluteFile.AsAbsolute()); - Assert.IsInstanceOfType(iRelativeFile.AsAbsolute()); Assert.IsInstanceOfType(iGenericFile.AsAbsolute()); // For absolute paths, AsAbsolute should return the same instance Assert.AreSame(absoluteFile, iAbsoluteFile.AsAbsolute()); - // For relative and generic paths, AsAbsolute should create new absolute instances - Assert.IsNotNull(iRelativeFile.AsAbsolute()); + // For generic paths, AsAbsolute should create new absolute instances Assert.IsNotNull(iGenericFile.AsAbsolute()); // Act & Assert - Test directory paths IDirectoryPath iAbsoluteDir = absoluteDir; - IDirectoryPath iRelativeDir = relativeDir; IDirectoryPath iGenericDir = genericDir; Assert.IsInstanceOfType(iAbsoluteDir.AsAbsolute()); - Assert.IsInstanceOfType(iRelativeDir.AsAbsolute()); Assert.IsInstanceOfType(iGenericDir.AsAbsolute()); // For absolute paths, AsAbsolute should return the same instance Assert.AreSame(absoluteDir, iAbsoluteDir.AsAbsolute()); - // For relative and generic paths, AsAbsolute should create new absolute instances - Assert.IsNotNull(iRelativeDir.AsAbsolute()); + // For generic paths, AsAbsolute should create new absolute instances Assert.IsNotNull(iGenericDir.AsAbsolute()); } @@ -602,9 +583,7 @@ public void AllPathTypes_ImplicitStringConversion_WorksTransparently() FilePath filePath = FilePath.Create("file.txt"); DirectoryPath directoryPath = DirectoryPath.Create("directory"); AbsoluteFilePath absoluteFilePath = AbsoluteFilePath.Create("C:\\temp\\file.txt"); - RelativeFilePath relativeFilePath = RelativeFilePath.Create("relative\\file.txt"); AbsoluteDirectoryPath absoluteDirectoryPath = AbsoluteDirectoryPath.Create("C:\\temp\\directory"); - RelativeDirectoryPath relativeDirectoryPath = RelativeDirectoryPath.Create("relative\\directory"); FileName fileName = FileName.Create("file.txt"); FileExtension fileExtension = FileExtension.Create(".txt"); @@ -614,9 +593,7 @@ public void AllPathTypes_ImplicitStringConversion_WorksTransparently() string result3 = filePath; string result4 = directoryPath; string result5 = absoluteFilePath; - string result6 = relativeFilePath; string result7 = absoluteDirectoryPath; - string result8 = relativeDirectoryPath; string result9 = fileName; string result10 = fileExtension; @@ -625,9 +602,7 @@ public void AllPathTypes_ImplicitStringConversion_WorksTransparently() Assert.AreEqual("file.txt", result3); Assert.AreEqual("directory", result4); Assert.AreEqual("C:\\temp\\file.txt", result5); - Assert.AreEqual("relative\\file.txt", result6); Assert.AreEqual("C:\\temp\\directory", result7); - Assert.AreEqual("relative\\directory", result8); Assert.AreEqual("file.txt", result9); Assert.AreEqual(".txt", result10); }