Add Dot rendering (#23)

This commit is contained in:
Patrick Stevens
2025-07-25 12:07:40 +01:00
committed by GitHub
parent e0153ab182
commit ed352c1b14
9 changed files with 221 additions and 2 deletions

View File

@@ -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"`). * 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 # Licence
MIT. MIT.

View File

@@ -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
[<TestFixture>]
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 ()
}
[<Test ; Explicit "requires graph-easy dependency">]
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
}
[<Test>]
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<IDisposable> 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

View File

@@ -14,6 +14,7 @@
<Compile Include="BulkUpdateExample.fs" /> <Compile Include="BulkUpdateExample.fs" />
<Compile Include="SimpleTest.fs" /> <Compile Include="SimpleTest.fs" />
<Compile Include="TestDiff.fs" /> <Compile Include="TestDiff.fs" />
<Compile Include="TestDot.fs" />
<Compile Include="TestExceptionThrowing.fs" /> <Compile Include="TestExceptionThrowing.fs" />
<Compile Include="TestSurface.fs" /> <Compile Include="TestSurface.fs" />
<Compile Include="TestSnapshotFinding\TestSnapshotFinding.fs" /> <Compile Include="TestSnapshotFinding\TestSnapshotFinding.fs" />
@@ -40,6 +41,9 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
<PackageReference Include="NUnit" Version="4.3.2"/> <PackageReference Include="NUnit" Version="4.3.2"/>
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0"/> <PackageReference Include="NUnit3TestAdapter" Version="5.0.0"/>
<!-- TODO: when ApiSurface accepts https://github.com/G-Research/ApiSurface/pull/116, upgrade these -->
<PackageReference Include="System.IO.Abstractions" Version="4.2.13" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="4.2.13" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

82
WoofWare.Expect/Dot.fs Normal file
View File

@@ -0,0 +1,82 @@
namespace WoofWare.Expect
open System
open System.Diagnostics
open System.IO
/// Methods for rendering dot files (specifications of graphs).
[<RequireQualifiedAccess>]
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<Process> 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<bool>
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"

View File

@@ -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.NewInsert [static method]: (int, string) -> WoofWare.Expect.DiffOperation
WoofWare.Expect.DiffOperation.NewMatch [static method]: (int, 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.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 inherit obj
WoofWare.Expect.ExpectBuilder..ctor [constructor]: (string * int) WoofWare.Expect.ExpectBuilder..ctor [constructor]: (string * int)
WoofWare.Expect.ExpectBuilder..ctor [constructor]: bool WoofWare.Expect.ExpectBuilder..ctor [constructor]: bool

View File

@@ -20,6 +20,7 @@
<Compile Include="Text.fs" /> <Compile Include="Text.fs" />
<Compile Include="File.fs" /> <Compile Include="File.fs" />
<Compile Include="Diff.fs" /> <Compile Include="Diff.fs" />
<Compile Include="Dot.fs" />
<Compile Include="Domain.fs" /> <Compile Include="Domain.fs" />
<Compile Include="SnapshotUpdate.fs" /> <Compile Include="SnapshotUpdate.fs" />
<Compile Include="Config.fs" /> <Compile Include="Config.fs" />

View File

@@ -1,5 +1,5 @@
{ {
"version": "0.5", "version": "0.6",
"publicReleaseRefSpec": [ "publicReleaseRefSpec": [
"^refs/heads/main$" "^refs/heads/main$"
], ],

View File

@@ -66,6 +66,7 @@
pkgs.nodePackages.markdown-link-check pkgs.nodePackages.markdown-link-check
pkgs.shellcheck pkgs.shellcheck
pkgs.xmlstarlet pkgs.xmlstarlet
pkgs.graph-easy
]; ];
}; };
}); });

View File

@@ -189,6 +189,11 @@
"version": "4.2.13", "version": "4.2.13",
"hash": "sha256-nkC/PiqE6+c1HJ2yTwg3x+qdBh844Z8n3ERWDW8k6Gg=" "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", "pname": "System.IO.FileSystem.AccessControl",
"version": "4.5.0", "version": "4.5.0",