tests and docs

This commit is contained in:
Francesco Mecca 2024-12-07 13:55:55 +01:00
parent c3e1d26ab3
commit 1afe333a4e
7 changed files with 175 additions and 107 deletions

View file

@ -22,7 +22,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Nunit" Version="4.2.1" /> <PackageReference Include="Nunit" Version="3.14.0" />
<PackageReference Include="Serilog" Version="4.2.0" /> <PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" /> <PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />

View file

@ -1,33 +1,150 @@
module Pentole.Path namespace Pentole
open System
let private invalid_chars = System.IO.Path.GetInvalidPathChars() module Internal =
let private invalid_chars = System.IO.Path.GetInvalidPathChars ()
type Path = | Absolute of string | Relative of string let trim (a: string) =
with System.IO.Path.TrimEndingDirectorySeparator a
static member of_string (path: string) =
let construct (path: string): Result<(string * bool), string> =
let is_absolute (path: string) = let is_absolute (path: string) =
System.IO.Path.IsPathFullyQualified path System.IO.Path.IsPathFullyQualified path
if System.String.IsNullOrEmpty path then if System.String.IsNullOrEmpty path then
Error "Can't create a path from an empty string" Error "Can't create a path from an empty string"
elif invalid_chars |> Seq.exists (fun c -> path.Contains c) then elif invalid_chars |> Seq.exists (fun c -> path.Contains c) then
Error $"The string contains invalid characters: {path}" Error $"The string contains invalid characters: {path}"
elif is_absolute path then elif is_absolute path then
path path
|> System.IO.Path.TrimEndingDirectorySeparator |> trim
|> Absolute |> fun p -> Ok (p, true)
|> Ok
else else
path path
|> System.IO.Path.TrimEndingDirectorySeparator |> trim
|> Relative |> fun p -> Ok (p, false)
|> Ok
type IPath =
abstract member ToString: unit -> string
abstract member string_value: string
inherit IEquatable<IPath>
inherit IComparable
[<Struct>]
[<CustomEquality>]
[<CustomComparison>]
type AbsolutePath internal (path: string) =
interface IPath with
member _.ToString () = $"AbsolutePath {path}"
member _.string_value = path
member x.CompareTo (o: obj) =
match o with
| :? IPath as abs ->
let a = abs.string_value
a.CompareTo (x :> IPath |> _.string_value)
| _ -> 1
interface IEquatable<IPath> with
member _.Equals (o: IPath) =
Internal.trim o.string_value = path
[<Struct>]
[<CustomEquality>]
[<CustomComparison>]
type RelativePath internal (path: string) =
interface IPath with
member _.ToString() = $"RelativePath {path}"
member _.string_value = path
member x.CompareTo (o: obj) =
match o with
| :? IPath as abs ->
let a = abs.string_value
a.CompareTo (x :> IPath |> _.string_value)
| _ -> 1
interface IEquatable<IPath> with
member _.Equals (o: IPath) =
Internal.trim o.string_value = path
module Path =
let of_string (path: string): Result<IPath, string> =
match Internal.construct path with
| Ok (path, true) -> AbsolutePath path :> IPath |> Ok
| Ok (path, false) -> RelativePath path :> IPath |> Ok
| Error e -> Error e
let private should_not_fail (st: string) =
st
|> of_string
|> Result.defaultWith (fun exn -> failwith exn)
/// Returns the extension of the given path
let extension (path: IPath) =
path.string_value
|> IO.Path.GetExtension
/// Return the basename of the given path
let basename (path: IPath) =
path.string_value |> IO.Path.GetFileNameWithoutExtension
/// Concatenates `path` and `other` and adds a directory separator character between any of the path components if one is not already present.
let join (path: IPath) (other: IPath) =
(path.string_value, other.string_value)
|> System.IO.Path.Join
|> should_not_fail
/// Change the extension of the given path
let with_extension (path: IPath) (extension: string) =
System.IO.Path.ChangeExtension (path.string_value, extension)
|> should_not_fail
/// <summary>
/// Determines wheter the path is normalized.
/// A normalized path is a path that does not contain any "." or ".." path segments and has no trailing or duplicate slashes
/// </summary>
let is_normalized (path: IPath) =
let path: string = path.string_value
let rec checkSegments = function
| [] -> true
| ("." | "..")::_ -> false
| _::tail -> checkSegments tail
not (path.Contains "//") && (String.split "/" path |> checkSegments)
/// Returns the parent directory of the given path.
///
/// If the path is the root directory or does not have a parent, the original path is returned.
///
/// # Arguments
///
/// * `path`: The path to get the parent directory of.
///
/// # Returns
/// The parent directory of the given path, or the original path if it has no parent
let parent (path: IPath) =
let parent = IO.Path.GetDirectoryName path.string_value
if parent = null then
path
else
parent |> should_not_fail
// impure functions
// https://docs.python.org/3.8/library/pathlib.html
module FileSystem =
let resolve (path: IPath) =
Native.realpath path.string_value |> Result.bind Path.of_string
(*
module Pentole.Path
let private string_apply fun_ = function
| Absolute p -> fun_ p |> Absolute
| Relative p -> fun_ p |> Relative
/// <summary> /// <summary>
/// Return the parent path /// Return the parent path
@ -36,46 +153,12 @@ let private string_apply fun_ = function
let parent (path: Path) = let parent (path: Path) =
failwith "TODO" failwith "TODO"
/// <summary>
/// Return the extension of the given path
/// </summary>
/// <param name="path"></param>
let extension = function
| Absolute p ->System.IO.Path.GetExtension p
| Relative p ->System.IO.Path.GetExtension p
/// <summary>
/// Return the basename of the given path
/// </summary>
/// <param name="path"></param>
let basename = function
| Absolute p -> System.IO.Path.GetFileNameWithoutExtension p
| Relative p -> System.IO.Path.GetFileNameWithoutExtension p
/// <summary>
/// Concatenates `path` and `other` and adds a directory separator character between any of the path components if one is not already present.
/// </summary>
/// <param name="path"></param>
/// <param name="other"></param>
let join (path: Path) (other: Path) =
match (path, other) with
| (Absolute path | Relative path), (Absolute other| Relative other) ->
System.IO.Path.Join (path, other) |> Path.of_string
let relative_to (parent: Path) (child: Path) = let relative_to (parent: Path) (child: Path) =
match (parent, child) with match (parent, child) with
| (Absolute parent | Relative parent), (Absolute child| Relative child) -> | (Absolute parent | Relative parent), (Absolute child| Relative child) ->
System.IO.Path.GetRelativePath (parent, child) System.IO.Path.GetRelativePath (parent, child)
|> Path.of_string |> Path.of_string
/// <summary>
/// Change the extension of the given path
/// </summary>
/// <param name="path"></param>
/// <param name="extension"></param>
let with_extension (path: Path) (extension: string) =
path
|> string_apply (fun path -> System.IO.Path.ChangeExtension (path, extension))
/// <summary> /// <summary>
/// Change the basename of the path /// Change the basename of the path
@ -86,33 +169,5 @@ let with_name (path: Path) (new_: string) =
failwith "t" failwith "t"
let (|Nil|Cons|) (arr: 'a array) =
match arr.Length with
| 0 -> Nil
| 1 -> Cons(arr.[0], [||])
| _ -> Cons(arr.[0], arr.[1..])
/// <summary> *)
/// Determines wheter the path is normalized.
/// A normalized path is a path that does not contain any "." or ".." path segments and has no trailing or duplicate slashes
/// </summary>
/// <param name="path"></param>
let is_normalized (path_: Path) =
let path: string = path_ |> function | Absolute a -> a | Relative a -> a
let rec checkSegments = function
| [] -> true
| ("." | "..")::_ -> false
| _::tail -> checkSegments tail
not (path.Contains "//") && (String.split "/" path |> checkSegments)
// impure functions
// https://docs.python.org/3.8/library/pathlib.html
module FileSystem =
let resolve = function
| Absolute path ->
Native.realpath path |> Result.bind Path.of_string
| Relative path ->
Native.realpath path |> Result.bind Path.of_string

View file

@ -10,9 +10,9 @@ let split(separator: string) (target: string) : string list =
| -1 -> | -1 ->
if start_idx < target.Length then if start_idx < target.Length then
target.Substring(start_idx) :: acc target.Substring(start_idx) :: acc
else else acc
acc
| index -> | index ->
let part = target.Substring (start_idx, index - start_idx) let part = target.Substring (start_idx, index - start_idx)
split (part :: acc) (index + slen) split (part :: acc) (index + slen)
split [] 0
split [] 0 |> List.rev

View file

@ -6,27 +6,34 @@ type Assert with
static member inline ok_is_equal<'ok, 'err> (expected: 'ok) (got: Result<'ok, 'err>) = static member inline ok_is_equal<'ok, 'err> (expected: 'ok) (got: Result<'ok, 'err>) =
match got with match got with
| Ok got -> Assert.That (got, Is.EqualTo(expected)) | Ok got -> Assert.AreEqual (expected, got)
| Error e -> Assert.Fail $"Expected 'ok, got: {e}" | Error e -> Assert.Fail $"Expected 'ok, got: {e}"
static member is_true (got: bool) = static member inline is_true (got: bool) =
Assert.That (got, Is.True) Assert.IsTrue got
static member is_false (got: bool) = static member inline is_false (got: bool) =
Assert.That (got, Is.False) Assert.IsFalse got
static member ok_is_true (r: Result<bool, 'a>) = static member inline ok_is_true (r: Result<bool, 'a>) =
match r with match r with
| Ok got -> Assert.That (got, Is.True) | Ok got -> Assert.is_true got
| Error e -> Assert.Fail $"Expected truth value, got: {e}" | Error e -> Assert.Fail $"Expected truth value, got: {e}"
static member ok_is_false (r: Result<bool, 'e>) = static member inline ok_is_false (r: Result<bool, 'e>) =
match r with match r with
| Ok got -> Assert.That (got, Is.False) | Ok got -> Assert.is_false got
| Error e -> Assert.Fail $"Expected truth value, got: {e}" | Error e -> Assert.Fail $"Expected truth value, got: {e}"
static member inline are_equal expected (got: 'a) = static member inline are_equal expected (got: 'a) =
Assert.That (got, Is.EqualTo(expected)) Assert.AreEqual (expected, got)
static member are_seq_equal (expected: 'a seq) (got: 'a seq) = static member inline are_seq_equal (expected: 'a seq) (got: 'a seq) =
Assert.That (got, Is.EquivalentTo(expected)) let s_exp = Seq.map id expected
let g_exp = Seq.map id got
CollectionAssert.AreEqual (s_exp, g_exp)
static member inline are_not_equal expected (got: 'a) =
Assert.AreNotEqual (expected, got)
static member inline fail = Assert.Fail

View file

@ -10,11 +10,14 @@ open Pentole.Path
let constructor_test () = let constructor_test () =
"/" |> Path.of_string |> Result.isOk |> Assert.is_true "/" |> Path.of_string |> Result.isOk |> Assert.is_true
'\000' |> string |> Path.of_string |> Result.isOk |> Assert.is_false '\000' |> string |> Path.of_string |> Result.isOk |> Assert.is_false
[<Test>] [<Test>]
let absolute_test () = let absolute_test () =
let is_absolute = function
| (Absolute path | Relative path) ->
System.IO.Path.IsPathFullyQualified path let is_absolute (p: IPath) =
p.string_value
|> System.IO.Path.IsPathFullyQualified
let test (s:string) = let test (s:string) =
s s
@ -53,18 +56,22 @@ let resolve_test () =
let equality_test () = let equality_test () =
let p (n: string ) = Path.of_string n |> Result.Unsafe.get let p (n: string ) = Path.of_string n |> Result.Unsafe.get
Assert.are_equal (p "/etc") (p "/etc/") Assert.are_equal (p "/etc") (p "/etc/")
(* Assert.are_not_equal (p "etc") (p "/etc/")
Assert.are_not_equal (p "etc") (p "/etc")
Assert.are_equal (Path.of_string "/tmp" ) ( Path.of_string "/tmp/")
Assert.is_true (Path.of_string "/tmp" = Path.of_string "/tmp/")
[<Test>] [<Test>]
let parent_test () = let parent_test () =
let p (n: string ) = Path.of_string n |> Result.get let p (n: string ) = Path.of_string n |> Result.Unsafe.get
let test (s:string) = let test (s:string) =
s s
|> Path.of_string |> Path.of_string
|> Result.get |> Result.Unsafe.get
|> parent |> parent
"/" |> test |> Assert.are_equal [p "/"] "/" |> test |> Assert.are_equal (p "/")
"/etc/" |> test |> Assert.are_seq_equal ["/"] "/etc/" |> test |> Assert.are_equal (p "/")
"/etc/conf" |> test |> Assert.are_seq_equal [p "/etc"] "/etc/conf" |> test |> Assert.are_equal (p "/etc")
"/etc/../etc" |> test |> Assert.ok_is_equal (p "/etc") "/etc/../etc" |> test |> Assert.are_equal (p "/etc/../")
*) "etc/../etc" |> test |> Assert.are_equal (p "etc/../")

View file

@ -9,4 +9,3 @@ open Pentole
let split_test () = let split_test () =
let target = "a/b/c" let target = "a/b/c"
Assert.are_seq_equal (target.Split("/")) (String.split "/" target) Assert.are_seq_equal (target.Split("/")) (String.split "/" target)

View file

@ -18,7 +18,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="NUnit" Version="4.2.1" /> <PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
<PackageReference Include="NUnit.Analyzers" Version="3.6.1" /> <PackageReference Include="NUnit.Analyzers" Version="3.6.1" />
<PackageReference Include="coverlet.collector" Version="6.0.0" /> <PackageReference Include="coverlet.collector" Version="6.0.0" />