Created
June 6, 2025 13:41
-
-
Save kant2002/fb37d810ef529035d3f0068884142a45 to your computer and use it in GitHub Desktop.
Property based testing sample of path normalization in F#
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module Tests | |
open System | |
open Xunit | |
open FsCheck | |
open FsCheck.FSharp | |
open FsCheck.Xunit | |
open System.IO | |
open TruePath | |
[<AutoOpen>] | |
module PathGenerators = | |
let LinuxPathItemsGenerator = | |
let baseDir = Gen.constant ".." | |
let currentDir = Gen.constant "." | |
let threeDots = Gen.constant "..." | |
let dots = Gen.oneof [baseDir; currentDir; threeDots] | |
let textPath = | |
Gen.nonEmptyListOf (Gen.oneof [Gen.choose(int 'a', int 'z'); Gen.choose(int 'A', int 'Z'); Gen.choose(int '0', int '9')]) | |
|> Gen.map (fun chars -> chars |> Seq.map char |> String.Concat) | |
let directorySeparatorChar = Gen.constant (Path.DirectorySeparatorChar.ToString()) | |
let altDirectorySeparatorChar = Gen.constant (Path.AltDirectorySeparatorChar.ToString()) | |
Gen.nonEmptyListOf (Gen.oneof [dots; textPath; directorySeparatorChar; altDirectorySeparatorChar]) | |
|> Gen.filter (fun items -> | |
items |> Seq.mapi ( | |
fun i item -> | |
// Avoid consecutive dots like "..../.." | |
if i < items.Length - 1 && item.StartsWith(".") && items[i + 1].StartsWith(".") then | |
false | |
else | |
true) |> Seq.forall id) | |
let WindowsPathItemsGenerator = | |
let driveLetter = | |
Gen.oneof [Gen.choose(int 'a', int 'z'); Gen.choose(int 'A', int 'Z')] | |
|> Gen.map (fun c -> $"{char c}:") | |
let directorySeparatorChar = Gen.constant (Path.DirectorySeparatorChar.ToString()) | |
let altDirectorySeparatorChar = Gen.constant (Path.AltDirectorySeparatorChar.ToString()) | |
let separator = Gen.oneof [directorySeparatorChar; altDirectorySeparatorChar] | |
let drivePrefix = Gen.zip driveLetter separator |> Gen.map (fun (drive, sep) -> $"{drive}{sep}") | |
Gen.zip drivePrefix LinuxPathItemsGenerator |> Gen.map (fun (prefix, items) -> prefix :: items) | |
type AnyOsPath = | |
static member Paths = | |
Arb.fromGen(Gen.oneof [LinuxPathItemsGenerator ; WindowsPathItemsGenerator]) | |
let collapseSameBlocks (pathParts: string seq) = | |
let result = System.Collections.Generic.List<string>() | |
let mutable lastSeparator = false | |
let hasHomeDrive = pathParts |> Seq.exists (fun part -> part.Length > 1 && part[1] = ':') | |
let homeDrive = pathParts |> Seq.tryHead | |
for item in pathParts do | |
if item.Length > 1 && item[1] = ':' then | |
// Skip current directory references | |
() | |
else | |
let currentSeparator = item = Path.DirectorySeparatorChar.ToString() || item = Path.AltDirectorySeparatorChar.ToString() | |
if lastSeparator && currentSeparator then | |
() | |
elif lastSeparator || currentSeparator || result.Count = 0 then | |
result.Add(item) | |
lastSeparator <- currentSeparator | |
else | |
result[result.Count - 1] <- result[result.Count - 1] + item | |
lastSeparator <- currentSeparator | |
if (hasHomeDrive && homeDrive.IsSome) then | |
result.Insert(0, homeDrive.Value) | |
result | |
let countDepth pathParts = | |
let mutable depth = 0 | |
for part in pathParts do | |
if part = Path.DirectorySeparatorChar.ToString() || part = Path.AltDirectorySeparatorChar.ToString() || part.Contains(':') then | |
() | |
elif part = ".." then | |
if depth > 0 then | |
depth <- depth - 1 | |
() | |
elif part <> "." then | |
depth <- depth + 1 | |
depth | |
[<Property(Arbitrary = [| typeof<AnyOsPath> |])>] | |
let ``Normalized path does not contain AltDirSeparator`` (pathParts: string list) = | |
let sourcePath = String.Concat(pathParts) | |
// Act | |
let normalizedPath = PathStrings.Normalize(sourcePath) | |
// Assert | |
Assert.False(normalizedPath.Contains(Path.AltDirectorySeparatorChar)) | |
[<Property(Arbitrary = [| typeof<AnyOsPath> |])>] | |
let ``Normalized path does not contain two DirSeparator`` (pathParts: string list) = | |
let sourcePath = String.Concat(pathParts) | |
// Act | |
let normalizedPath = PathStrings.Normalize(sourcePath) | |
// Assert | |
Assert.False(normalizedPath.Contains(Path.DirectorySeparatorChar.ToString() + Path.DirectorySeparatorChar.ToString())) | |
[<Property(Arbitrary = [| typeof<AnyOsPath> |])>] | |
let ``Normalized path does not end with DirSeparator`` (pathParts: string list) = | |
let sourcePath = String.Concat(pathParts) | |
// Act | |
let normalizedPath = PathStrings.Normalize(sourcePath) | |
// Assert | |
Assert.True (normalizedPath = "" | |
|| normalizedPath = Path.DirectorySeparatorChar.ToString() | |
|| (normalizedPath.Length = 3 && normalizedPath[1] = ':') | |
|| normalizedPath[normalizedPath.Length-1] <> Path.DirectorySeparatorChar) | |
[<Property(Arbitrary = [| typeof<AnyOsPath> |])>] | |
let ``Depth preserved`` (pathParts: string list) = | |
let sourcePath = String.Concat(pathParts) | |
// Act | |
let normalizedPath = PathStrings.Normalize(sourcePath) | |
// Assert | |
let collapsedBlock = collapseSameBlocks pathParts | |
let expectedDepth = countDepth collapsedBlock | |
let actualDepth = countDepth (normalizedPath.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)) | |
Assert.Equal(expectedDepth, actualDepth) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment