diff --git a/README.md b/README.md index 5b891fc..c13d08f 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,11 @@ Observe the `OneTimeSetUp` which sets global state to enter "bulk update" mode, * The snapshot updating mechanism *requires* you to use verbatim string literals. While the test assertions will work correctly if you do `snapshot ("foo" + "bar" + f 3)`, for example, the updating code is liable to do something undefined in that case. Also do not use format strings (`$"blah"`). +# Output formats + +* The `Diff` module provides a Patience diff and a Myers diff implementation, which you can use to make certain tests much more readable. +* The `Dot` module provides `render`, which renders a dot file as ASCII art. You will need `graph-easy` to use this feature. + # Licence MIT. diff --git a/WoofWare.Expect.Test/TestDot.fs b/WoofWare.Expect.Test/TestDot.fs new file mode 100644 index 0000000..19b927e --- /dev/null +++ b/WoofWare.Expect.Test/TestDot.fs @@ -0,0 +1,104 @@ +namespace WoofWare.Expect.Test + +#nowarn 0044 // This construct is deprecated + +open System +open FsUnitTyped +open WoofWare.Expect +open NUnit.Framework +open System.IO.Abstractions +open System.IO.Abstractions.TestingHelpers + +[] +module TestDot = + let toFs (fs : IFileSystem) : Dot.IFileSystem = + { new Dot.IFileSystem with + member _.DeleteFile s = fs.File.Delete s + member _.WriteFile path contents = fs.File.WriteAllText (path, contents) + member _.GetTempFileName () = fs.Path.GetTempFileName () + } + + [] + let ``Basic dotfile, real graph-easy`` () = + let s = + """digraph G { + rankdir = TB + bgcolor = transparent + n2 [shape=box label="{{n2|Map|height=1}}" ] + n1 [shape=box label="{{n1|Const|height=0}}" ] + n1 -> n2 +}""" + + expect { + snapshot + @" +┌───────────────────────┐ +│ {{n1|Const|height=0}} │ +└───────────────────────┘ + │ + │ + ▼ +┌───────────────────────┐ +│ {{n2|Map|height=1}} │ +└───────────────────────┘ +" + + return Dot.render s + } + + [] + let ``Basic dotfile`` () = + let fs = MockFileSystem () + + let contents = + """digraph G { + rankdir = TB + bgcolor = transparent + n2 [shape=box label="{{n2|Map|height=1}}" ] + n1 [shape=box label="{{n1|Const|height=0}}" ] + n1 -> n2 +}""" + + let mutable started = false + let mutable waited = false + let mutable disposed = false + + let expected = + "┌───────────────────────┐ +│ {{n1|Const|height=0}} │ +└───────────────────────┘ + │ + │ + ▼ +┌───────────────────────┐ +│ {{n2|Map|height=1}} │ +└───────────────────────┘ +" + + let pr = + { new Dot.IProcess with + member _.Start _ = + started <- true + true + + member _.Create exe args = + exe |> shouldEqual "graph-easy" + + args.StartsWith ("--as=boxarg --from=dot ", StringComparison.Ordinal) + |> shouldEqual true + + { new IDisposable with + member _.Dispose () = disposed <- true + } + + member _.WaitForExit p = waited <- true + member _.ReadStandardOutput _ = expected + } + + Dot.render' pr (toFs fs) "graph-easy" contents + |> _.TrimStart() + |> shouldEqual expected + + started |> shouldEqual true + waited |> shouldEqual true + disposed |> shouldEqual true diff --git a/WoofWare.Expect.Test/WoofWare.Expect.Test.fsproj b/WoofWare.Expect.Test/WoofWare.Expect.Test.fsproj index f69066a..c40c2ba 100644 --- a/WoofWare.Expect.Test/WoofWare.Expect.Test.fsproj +++ b/WoofWare.Expect.Test/WoofWare.Expect.Test.fsproj @@ -14,6 +14,7 @@ + @@ -40,6 +41,9 @@ + + + diff --git a/WoofWare.Expect/Dot.fs b/WoofWare.Expect/Dot.fs new file mode 100644 index 0000000..cf46ee0 --- /dev/null +++ b/WoofWare.Expect/Dot.fs @@ -0,0 +1,82 @@ +namespace WoofWare.Expect + +open System +open System.Diagnostics +open System.IO + +/// Methods for rendering dot files (specifications of graphs). +[] +module Dot = + /// A mock for System.Diagnostics.Process. + type IProcess<'Process when 'Process :> IDisposable> = + /// Equivalent to Process.Create + abstract Create : exe : string -> args : string -> 'Process + /// Equivalent to Process.Start + abstract Start : 'Process -> bool + /// Equivalent to Process.WaitForExit + abstract WaitForExit : 'Process -> unit + /// Equivalent to Process.StandardOutput.ReadToEnd + abstract ReadStandardOutput : 'Process -> string + + /// The real Process interface, in a form that can be passed to `render'`. + let process' = + { new IProcess with + member _.Create exe args = + let psi = ProcessStartInfo exe + psi.RedirectStandardOutput <- true + psi.Arguments <- args + let result = new Process () + result.StartInfo <- psi + result + + member _.Start p = p.Start () + member _.WaitForExit p = p.WaitForExit () + member _.ReadStandardOutput p = p.StandardOutput.ReadToEnd () + } + + /// A mock for System.IO + type IFileSystem = + /// Equivalent to Path.GetTempFileName + abstract GetTempFileName : unit -> string + /// Equivalent to File.Delete + abstract DeleteFile : string -> unit + /// Equivalent to File.WriteAllText (curried) + abstract WriteFile : path : string -> contents : string -> unit + + /// The real filesystem, in a form that can be passed to `render'`. + let fileSystem = + { new IFileSystem with + member _.GetTempFileName () = Path.GetTempFileName () + member _.DeleteFile f = File.Delete f + member _.WriteFile path contents = File.WriteAllText (path, contents) + } + + /// writeFile takes the filepath first and the contents second. + /// Due to the impoverished nature of the .NET Standard APIs, you are in charge of making sure the output of + /// fs.GetTempFileName is suitable for interpolation into a command line. + let render'<'Process when 'Process :> IDisposable> + (pr : IProcess<'Process>) + (fs : IFileSystem) + (graphEasyExecutable : string) + (dotFileContents : string) + : string + = + let tempFile = fs.GetTempFileName () + + try + fs.WriteFile tempFile dotFileContents + + use p = pr.Create graphEasyExecutable ("--as=boxarg --from=dot " + tempFile) + pr.Start p |> ignore + pr.WaitForExit p + + "\n" + pr.ReadStandardOutput p + finally + try + fs.DeleteFile tempFile + with _ -> + () + + /// Call `graph-easy` to render the dotfile as ASCII art. + /// This is fully mockable, but you must use `render'` to do so. + let render = render' process' fileSystem "graph-easy" diff --git a/WoofWare.Expect/SurfaceBaseline.txt b/WoofWare.Expect/SurfaceBaseline.txt index 0741d86..83e3327 100644 --- a/WoofWare.Expect/SurfaceBaseline.txt +++ b/WoofWare.Expect/SurfaceBaseline.txt @@ -50,6 +50,23 @@ WoofWare.Expect.DiffOperation.NewDelete [static method]: (int, string) -> WoofWa WoofWare.Expect.DiffOperation.NewInsert [static method]: (int, string) -> WoofWare.Expect.DiffOperation WoofWare.Expect.DiffOperation.NewMatch [static method]: (int, int, string) -> WoofWare.Expect.DiffOperation WoofWare.Expect.DiffOperation.Tag [property]: [read-only] int +WoofWare.Expect.Dot inherit obj +WoofWare.Expect.Dot+IFileSystem - interface with 3 member(s) +WoofWare.Expect.Dot+IFileSystem.DeleteFile [method]: string -> unit +WoofWare.Expect.Dot+IFileSystem.GetTempFileName [method]: unit -> string +WoofWare.Expect.Dot+IFileSystem.WriteFile [method]: string -> string -> unit +WoofWare.Expect.Dot+IProcess`1 - interface with 4 member(s) +WoofWare.Expect.Dot+IProcess`1.Create [method]: string -> string -> #(IDisposable) +WoofWare.Expect.Dot+IProcess`1.ReadStandardOutput [method]: #(IDisposable) -> string +WoofWare.Expect.Dot+IProcess`1.Start [method]: #(IDisposable) -> bool +WoofWare.Expect.Dot+IProcess`1.WaitForExit [method]: #(IDisposable) -> unit +WoofWare.Expect.Dot.fileSystem [static property]: [read-only] WoofWare.Expect.Dot+IFileSystem +WoofWare.Expect.Dot.get_fileSystem [static method]: unit -> WoofWare.Expect.Dot+IFileSystem +WoofWare.Expect.Dot.get_process' [static method]: unit -> System.Diagnostics.Process WoofWare.Expect.Dot+IProcess +WoofWare.Expect.Dot.get_render [static method]: unit -> (string -> string) +WoofWare.Expect.Dot.process' [static property]: [read-only] System.Diagnostics.Process WoofWare.Expect.Dot+IProcess +WoofWare.Expect.Dot.render [static property]: [read-only] string -> string +WoofWare.Expect.Dot.render' [static method]: #(IDisposable) WoofWare.Expect.Dot+IProcess -> WoofWare.Expect.Dot+IFileSystem -> string -> string -> string WoofWare.Expect.ExpectBuilder inherit obj WoofWare.Expect.ExpectBuilder..ctor [constructor]: (string * int) WoofWare.Expect.ExpectBuilder..ctor [constructor]: bool diff --git a/WoofWare.Expect/WoofWare.Expect.fsproj b/WoofWare.Expect/WoofWare.Expect.fsproj index a612ed1..c9ba90b 100644 --- a/WoofWare.Expect/WoofWare.Expect.fsproj +++ b/WoofWare.Expect/WoofWare.Expect.fsproj @@ -20,6 +20,7 @@ + diff --git a/WoofWare.Expect/version.json b/WoofWare.Expect/version.json index f68ef4f..1e0fab9 100644 --- a/WoofWare.Expect/version.json +++ b/WoofWare.Expect/version.json @@ -1,5 +1,5 @@ { - "version": "0.5", + "version": "0.6", "publicReleaseRefSpec": [ "^refs/heads/main$" ], @@ -10,4 +10,4 @@ ":/Directory.Build.props", ":/LICENSE" ] -} \ No newline at end of file +} diff --git a/flake.nix b/flake.nix index fd7b113..a637461 100644 --- a/flake.nix +++ b/flake.nix @@ -66,6 +66,7 @@ pkgs.nodePackages.markdown-link-check pkgs.shellcheck pkgs.xmlstarlet + pkgs.graph-easy ]; }; }); diff --git a/nix/deps.json b/nix/deps.json index 262e636..ad36b7c 100644 --- a/nix/deps.json +++ b/nix/deps.json @@ -189,6 +189,11 @@ "version": "4.2.13", "hash": "sha256-nkC/PiqE6+c1HJ2yTwg3x+qdBh844Z8n3ERWDW8k6Gg=" }, + { + "pname": "System.IO.Abstractions.TestingHelpers", + "version": "4.2.13", + "hash": "sha256-WGGatXlgyROnptdw0zU3ggf54eD/zusO/fvtf+5wuPU=" + }, { "pname": "System.IO.FileSystem.AccessControl", "version": "4.5.0",