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>
<PackageReference Include="Nunit" Version="4.2.1" />
<PackageReference Include="Nunit" Version="3.14.0" />
<PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="Serilog.Sinks.Console" 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
with
static member of_string (path: string) =
let trim (a: string) =
System.IO.Path.TrimEndingDirectorySeparator a
let construct (path: string): Result<(string * bool), string> =
let is_absolute (path: string) =
System.IO.Path.IsPathFullyQualified path
if System.String.IsNullOrEmpty path then
Error "Can't create a path from an empty string"
elif invalid_chars |> Seq.exists (fun c -> path.Contains c) then
Error $"The string contains invalid characters: {path}"
elif is_absolute path then
path
|> System.IO.Path.TrimEndingDirectorySeparator
|> Absolute
|> Ok
|> trim
|> fun p -> Ok (p, true)
else
path
|> System.IO.Path.TrimEndingDirectorySeparator
|> Relative
|> Ok
|> trim
|> fun p -> Ok (p, false)
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>
/// Return the parent path
@ -36,46 +153,12 @@ let private string_apply fun_ = function
let parent (path: Path) =
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) =
match (parent, child) with
| (Absolute parent | Relative parent), (Absolute child| Relative child) ->
System.IO.Path.GetRelativePath (parent, child)
|> 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>
/// Change the basename of the path
@ -86,33 +169,5 @@ let with_name (path: Path) (new_: string) =
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 ->
if start_idx < target.Length then
target.Substring(start_idx) :: acc
else
acc
else acc
| index ->
let part = target.Substring (start_idx, index - start_idx)
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>) =
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}"
static member is_true (got: bool) =
Assert.That (got, Is.True)
static member inline is_true (got: bool) =
Assert.IsTrue got
static member is_false (got: bool) =
Assert.That (got, Is.False)
static member inline is_false (got: bool) =
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
| Ok got -> Assert.That (got, Is.True)
| Ok got -> Assert.is_true got
| 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
| Ok got -> Assert.That (got, Is.False)
| Ok got -> Assert.is_false got
| Error e -> Assert.Fail $"Expected truth value, got: {e}"
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) =
Assert.That (got, Is.EquivalentTo(expected))
static member inline are_seq_equal (expected: 'a seq) (got: 'a seq) =
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 () =
"/" |> Path.of_string |> Result.isOk |> Assert.is_true
'\000' |> string |> Path.of_string |> Result.isOk |> Assert.is_false
[<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) =
s
@ -53,18 +56,22 @@ let resolve_test () =
let equality_test () =
let p (n: string ) = Path.of_string n |> Result.Unsafe.get
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>]
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) =
s
|> Path.of_string
|> Result.get
|> Result.Unsafe.get
|> parent
"/" |> test |> Assert.are_equal [p "/"]
"/etc/" |> test |> Assert.are_seq_equal ["/"]
"/etc/conf" |> test |> Assert.are_seq_equal [p "/etc"]
"/etc/../etc" |> test |> Assert.ok_is_equal (p "/etc")
*)
"/" |> test |> Assert.are_equal (p "/")
"/etc/" |> test |> Assert.are_equal (p "/")
"/etc/conf" |> test |> Assert.are_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 target = "a/b/c"
Assert.are_seq_equal (target.Split("/")) (String.split "/" target)

View file

@ -18,7 +18,7 @@
<ItemGroup>
<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="NUnit.Analyzers" Version="3.6.1" />
<PackageReference Include="coverlet.collector" Version="6.0.0" />