Skip to content

Commit 4f7b670

Browse files
committed
Spike new test project to improve test readability
This is a spike to make it easier to write markdown specification tests. It allows us to test markdown fragments and their resulting HTML much terser while making it easier to come up with good test names.
1 parent 62f580a commit 4f7b670

File tree

11 files changed

+386
-2
lines changed

11 files changed

+386
-2
lines changed

docs-builder.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "build", "build\build.fsproj
5151
EndProject
5252
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "docs-assembler", "src\docs-assembler\docs-assembler.csproj", "{28350800-B44B-479B-86E2-1D39E321C0B4}"
5353
EndProject
54+
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "authoring", "tests\authoring\authoring.fsproj", "{018F959E-824B-4664-B345-066784478D24}"
55+
EndProject
5456
Global
5557
GlobalSection(SolutionConfigurationPlatforms) = preSolution
5658
Debug|Any CPU = Debug|Any CPU
@@ -89,6 +91,10 @@ Global
8991
{28350800-B44B-479B-86E2-1D39E321C0B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
9092
{28350800-B44B-479B-86E2-1D39E321C0B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
9193
{28350800-B44B-479B-86E2-1D39E321C0B4}.Release|Any CPU.Build.0 = Release|Any CPU
94+
{018F959E-824B-4664-B345-066784478D24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
95+
{018F959E-824B-4664-B345-066784478D24}.Debug|Any CPU.Build.0 = Debug|Any CPU
96+
{018F959E-824B-4664-B345-066784478D24}.Release|Any CPU.ActiveCfg = Release|Any CPU
97+
{018F959E-824B-4664-B345-066784478D24}.Release|Any CPU.Build.0 = Release|Any CPU
9298
EndGlobalSection
9399
GlobalSection(NestedProjects) = preSolution
94100
{4D198E25-C211-41DC-9E84-B15E89BD7048} = {BE6011CC-1200-4957-B01F-FCCA10C5CF5A}
@@ -99,5 +105,6 @@ Global
99105
{CD2887E3-BDA9-434B-A5BF-9ED38DE20332} = {245023D2-D3CA-47B9-831D-DAB91A2FFDC7}
100106
{A2A34BBC-CB5E-4100-9529-A12B6ECB769C} = {245023D2-D3CA-47B9-831D-DAB91A2FFDC7}
101107
{28350800-B44B-479B-86E2-1D39E321C0B4} = {BE6011CC-1200-4957-B01F-FCCA10C5CF5A}
108+
{018F959E-824B-4664-B345-066784478D24} = {67B576EE-02FA-4F9B-94BC-3630BC09ECE5}
102109
EndGlobalSection
103110
EndGlobal

docs-builder.sln.DotSettings

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
22
<s:Boolean x:Key="/Default/UserDictionary/Words/=docset/@EntryIndexedValue">True</s:Boolean>
3+
<s:Boolean x:Key="/Default/UserDictionary/Words/=frontmatter/@EntryIndexedValue">True</s:Boolean>
34
<s:Boolean x:Key="/Default/UserDictionary/Words/=linenos/@EntryIndexedValue">True</s:Boolean>
4-
<s:Boolean x:Key="/Default/UserDictionary/Words/=literalinclude/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
5+
<s:Boolean x:Key="/Default/UserDictionary/Words/=literalinclude/@EntryIndexedValue">True</s:Boolean>
6+
<s:Boolean x:Key="/Default/UserDictionary/Words/=_0060_0060inli/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

tests/Elastic.Markdown.Tests/Inline/DirectiveBlockLinkTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ protected override void AddToFileSystem(MockFileSystem fileSystem)
3131
3232
:::{important}
3333
:name: hint_ref
34-
This is a 'important' admonition
34+
This is an 'important' admonition
3535
:::
3636
""";
3737
fileSystem.AddFile(@"docs/testing/req.md", inclusion);
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
namespace authoring
2+
3+
4+
open System
5+
open System.Diagnostics
6+
open System.IO
7+
open AngleSharp.Diffing
8+
open AngleSharp.Diffing.Core
9+
open AngleSharp.Html
10+
open AngleSharp.Html.Parser
11+
open DiffPlex.DiffBuilder
12+
open DiffPlex.DiffBuilder.Model
13+
open JetBrains.Annotations
14+
open Xunit.Sdk
15+
16+
[<AutoOpen>]
17+
module HtmlAssertions =
18+
19+
let htmlDiffString (diffs: seq<IDiff>) =
20+
let NodeName (source:ComparisonSource) = source.Node.NodeType.ToString().ToLowerInvariant();
21+
let htmlText (source:IDiff) =
22+
let formatter = PrettyMarkupFormatter();
23+
let nodeText (control: ComparisonSource) =
24+
use sw = new StringWriter()
25+
control.Node.ToHtml(sw, formatter)
26+
sw.ToString()
27+
let attrText (control: AttributeComparisonSource) =
28+
use sw = new StringWriter()
29+
control.Attribute.ToHtml(sw, formatter)
30+
sw.ToString()
31+
let nodeDiffText (control: ComparisonSource option) (test: ComparisonSource option) =
32+
let actual = match test with Some t -> nodeText t | None -> "missing"
33+
let expected = match control with Some t -> nodeText t | None -> "missing"
34+
$"""
35+
36+
expected: {expected}
37+
actual: {actual}
38+
"""
39+
let attrDiffText (control: AttributeComparisonSource option) (test: AttributeComparisonSource option) =
40+
let actual = match test with Some t -> attrText t | None -> "missing"
41+
let expected = match control with Some t -> attrText t | None -> "missing"
42+
$"""
43+
44+
expected: {expected}
45+
actual: {actual}
46+
"""
47+
48+
match source with
49+
| :? NodeDiff as diff -> nodeDiffText <| Some diff.Control <| Some diff.Test
50+
| :? AttrDiff as diff -> attrDiffText <| Some diff.Control <| Some diff.Test
51+
| :? MissingNodeDiff as diff -> nodeDiffText <| Some diff.Control <| None
52+
| :? MissingAttrDiff as diff -> attrDiffText <| Some diff.Control <| None
53+
| :? UnexpectedNodeDiff as diff -> nodeDiffText None <| Some diff.Test
54+
| :? UnexpectedAttrDiff as diff -> attrDiffText None <| Some diff.Test
55+
| _ -> failwith $"Unknown diff type detected: {source.GetType()}"
56+
57+
diffs
58+
|> Seq.map (fun diff ->
59+
60+
match diff with
61+
| :? NodeDiff as diff when diff.Target = DiffTarget.Text && diff.Control.Path.Equals(diff.Test.Path, StringComparison.Ordinal)
62+
-> $"The text in {diff.Control.Path} is different."
63+
| :? NodeDiff as diff when diff.Target = DiffTarget.Text
64+
-> $"The expected {NodeName(diff.Control)} at {diff.Control.Path} and the actual {NodeName(diff.Test)} at {diff.Test.Path} is different."
65+
| :? NodeDiff as diff when diff.Control.Path.Equals(diff.Test.Path, StringComparison.Ordinal)
66+
-> $"The {NodeName(diff.Control)}s at {diff.Control.Path} are different."
67+
| :? NodeDiff as diff -> $"The expected {NodeName(diff.Control)} at {diff.Control.Path} and the actual {NodeName(diff.Test)} at {diff.Test.Path} are different."
68+
| :? AttrDiff as diff when diff.Control.Path.Equals(diff.Test.Path, StringComparison.Ordinal)
69+
-> $"The values of the attributes at {diff.Control.Path} are different."
70+
| :? AttrDiff as diff -> $"The value of the attribute {diff.Control.Path} and actual attribute {diff.Test.Path} are different."
71+
| :? MissingNodeDiff as diff -> $"The {NodeName(diff.Control)} at {diff.Control.Path} is missing."
72+
| :? MissingAttrDiff as diff -> $"The attribute at {diff.Control.Path} is missing."
73+
| :? UnexpectedNodeDiff as diff -> $"The {NodeName(diff.Test)} at {diff.Test.Path} was not expected."
74+
| :? UnexpectedAttrDiff as diff -> $"The attribute at {diff.Test.Path} was not expected."
75+
| _ -> failwith $"Unknown diff type detected: {diff.GetType()}"
76+
+
77+
htmlText diff
78+
)
79+
|> String.concat "\n"
80+
81+
let private prettyHtml (html:string) =
82+
let parser = HtmlParser()
83+
let document = parser.ParseDocument(html)
84+
use sw = new StringWriter()
85+
document.Body.Children
86+
|> Seq.iter _.ToHtml(sw, PrettyMarkupFormatter())
87+
sw.ToString()
88+
89+
[<DebuggerStepThrough>]
90+
let convertsToHtml ([<LanguageInjection("html")>]expected: string) (actual: TestResult) =
91+
let diffs =
92+
DiffBuilder
93+
.Compare(actual.Html)
94+
.WithTest(expected)
95+
.Build()
96+
97+
let diff = htmlDiffString diffs
98+
match diff with
99+
| s when String.IsNullOrEmpty s -> ()
100+
| s ->
101+
let expectedHtml = prettyHtml expected
102+
let actualHtml = prettyHtml actual.Html
103+
let textDiff =
104+
InlineDiffBuilder.Diff(expectedHtml, actualHtml).Lines
105+
|> Seq.map(fun l ->
106+
match l.Type with
107+
| ChangeType.Deleted -> "- " + l.Text
108+
| ChangeType.Modified -> "+ " + l.Text
109+
| ChangeType.Inserted -> "+ " + l.Text
110+
| _ -> " " + l.Text
111+
)
112+
|> String.concat "\n"
113+
let msg = $"""Html was not equal
114+
{textDiff}
115+
116+
{diff}
117+
"""
118+
raise (XunitException(msg))
119+
120+

tests/authoring/Framework/Setup.fs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
namespace authoring
2+
3+
open System.Collections.Generic
4+
open System.IO
5+
open System.IO.Abstractions.TestingHelpers
6+
open System.Threading.Tasks
7+
open Elastic.Markdown
8+
open Elastic.Markdown.Diagnostics
9+
open Elastic.Markdown.IO
10+
open JetBrains.Annotations
11+
12+
type Setup =
13+
14+
static let GenerateDocSetYaml(
15+
fileSystem: MockFileSystem,
16+
globalVariables: Dictionary<string, string> option
17+
) =
18+
let root = fileSystem.DirectoryInfo.New(Path.Combine(Paths.Root.FullName, "docs/"));
19+
let yaml = new StringWriter();
20+
yaml.WriteLine("toc:");
21+
let markdownFiles = fileSystem.Directory.EnumerateFiles(root.FullName, "*.md", SearchOption.AllDirectories)
22+
markdownFiles
23+
|> Seq.iter(fun markdownFile ->
24+
let relative = fileSystem.Path.GetRelativePath(root.FullName, markdownFile);
25+
yaml.WriteLine($" - file: {relative}");
26+
)
27+
match globalVariables with
28+
| Some vars ->
29+
yaml.WriteLine($"subs:")
30+
vars |> Seq.iter(fun kv ->
31+
yaml.WriteLine($" {kv.Key}: {kv.Value}");
32+
)
33+
| _ -> ()
34+
35+
fileSystem.AddFile(Path.Combine(root.FullName, "docset.yml"), MockFileData(yaml.ToString()));
36+
37+
static let Generate ([<LanguageInjection("markdown")>]m: string) : Task<TestResult> =
38+
39+
let d = dict [ ("docs/index.md", MockFileData(m)) ]
40+
let opts = MockFileSystemOptions(CurrentDirectory=Paths.Root.FullName)
41+
let fileSystem = MockFileSystem(d, opts)
42+
43+
GenerateDocSetYaml (fileSystem, None)
44+
45+
let collector = DiagnosticsCollector([]);
46+
let context = BuildContext(fileSystem, Collector=collector)
47+
let set = DocumentationSet(context);
48+
let file =
49+
match set.GetMarkdownFile(fileSystem.FileInfo.New("docs/index.md")) with
50+
| NonNull f -> f
51+
| _ -> failwithf "docs/index.md could not be located"
52+
53+
let context = {
54+
File = file
55+
Collector = collector
56+
Set = set
57+
ReadFileSystem = fileSystem
58+
WriteFileSystem = fileSystem
59+
}
60+
context.Bootstrap()
61+
62+
/// Pass a full documentation page to the test setup
63+
static member Document ([<LanguageInjection("markdown")>]m: string) =
64+
let g = task { return! Generate m }
65+
g |> Async.AwaitTask |> Async.RunSynchronously
66+
67+
/// Pass a markdown fragment to the test setup
68+
static member Markdown ([<LanguageInjection("markdown")>]m: string) =
69+
// language=markdown
70+
let m = $"""
71+
# Test Document
72+
{m}
73+
"""
74+
let g = task {
75+
return! Generate m
76+
}
77+
g |> Async.AwaitTask |> Async.RunSynchronously
78+
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
namespace authoring
2+
3+
open System
4+
open System.IO.Abstractions
5+
open Elastic.Markdown.Diagnostics
6+
open Elastic.Markdown.IO
7+
open Markdig.Syntax
8+
9+
type TestResult = {
10+
Document: MarkdownDocument
11+
Html: string
12+
Context: MarkdownTestContext
13+
}
14+
15+
and MarkdownTestContext =
16+
{
17+
File: MarkdownFile
18+
Collector: DiagnosticsCollector
19+
Set: DocumentationSet
20+
ReadFileSystem: IFileSystem
21+
WriteFileSystem: IFileSystem
22+
}
23+
24+
member this.Bootstrap () = backgroundTask {
25+
let! ctx = Async.CancellationToken
26+
let _ = this.Collector.StartAsync(ctx)
27+
do! this.Set.ResolveDirectoryTree(ctx)
28+
29+
let! document = this.File.ParseFullAsync(ctx)
30+
31+
let html = this.File.CreateHtml(document);
32+
this.Collector.Channel.TryComplete()
33+
do! this.Collector.StopAsync(ctx)
34+
return { Context = this; Document = document; Html = html }
35+
}
36+
37+
interface IDisposable with
38+
member this.Dispose() = ()
39+
40+

tests/authoring/Inline/Comments.fs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module ``inline elements``.``comment block``
2+
3+
open Xunit
4+
open authoring
5+
6+
type ``commented line`` () =
7+
8+
static let markdown = Setup.Markdown """
9+
% comment
10+
not a comment
11+
"""
12+
13+
[<Fact>]
14+
let ``validate HTML: commented line should not be emitted`` () =
15+
markdown |> convertsToHtml """<p>not a comment</p>"""
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
module ``inline elements``.``anchors DEPRECATED``
2+
3+
open Xunit
4+
open authoring
5+
6+
type ``inline anchor in the middle`` () =
7+
8+
static let markdown = Setup.Markdown """
9+
this is *regular* text and this $$$is-an-inline-anchor$$$ and this continues to be regular text
10+
"""
11+
12+
[<Fact>]
13+
let ``validate HTML`` () =
14+
markdown |> convertsToHtml """
15+
<p>this is <em>regular</em> text and this <a id="is-an-inline-anchor"></a> and this continues to be regular text</p>
16+
"""
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
module ``inline elements``.``image``
2+
3+
open Xunit
4+
open authoring
5+
6+
type ``static path to image`` () =
7+
static let markdown = Setup.Markdown """
8+
![Elasticsearch](/_static/img/observability.png)
9+
"""
10+
11+
[<Fact>]
12+
let ``validate HTML: generates link and alt attr`` () =
13+
markdown |> convertsToHtml """
14+
<p><img src="/_static/img/observability.png" alt="Elasticsearch" /></p>
15+
"""
16+
17+
type ``relative path to image`` () =
18+
static let markdown = Setup.Markdown """
19+
![Elasticsearch](_static/img/observability.png)
20+
"""
21+
22+
[<Fact>]
23+
let ``validate HTML: preserves relative path`` () =
24+
markdown |> convertsToHtml """
25+
<p><img src="_static/img/observability.png" alt="Elasticsearch" /></p>
26+
"""
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
module ``inline elements``.``substitutions``
2+
3+
open Xunit
4+
open authoring
5+
6+
type ``read sub from yaml frontmatter`` () =
7+
static let markdown = Setup.Markdown """---
8+
sub:
9+
hello-world: "Hello World!"
10+
---
11+
The following should be subbed: {{hello-world}}
12+
not a comment
13+
"""
14+
15+
[<Fact>]
16+
let ``validate HTML: replace substitution`` () =
17+
markdown |> convertsToHtml """
18+
<p>The following should be subbed: Hello World!<br>
19+
not a comment</p>
20+
"""
21+
22+
23+
type ``requires valid syntax and key to be found`` () =
24+
static let markdown = Setup.Markdown """---
25+
sub:
26+
hello-world: "Hello World!"
27+
---
28+
# Testing substitutions
29+
30+
The following should be subbed: {{hello-world}}
31+
not a comment
32+
not a {{valid-key}}
33+
not a {substitution}
34+
"""
35+
36+
[<Fact>]
37+
let ``validate HTML: leaves non subs alone`` () =
38+
markdown |> convertsToHtml """
39+
<p>The following should be subbed: Hello World!<br>
40+
not a comment</br>
41+
not a {{valid-key}}<br>
42+
not a {substitution}</p> """

0 commit comments

Comments
 (0)