Skip to content

New test project to make specification tests easier to write for developers #344

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jan 27, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs-builder.sln
Original file line number Diff line number Diff line change
@@ -51,6 +51,8 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "build", "build\build.fsproj
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "docs-assembler", "src\docs-assembler\docs-assembler.csproj", "{28350800-B44B-479B-86E2-1D39E321C0B4}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "authoring", "tests\authoring\authoring.fsproj", "{018F959E-824B-4664-B345-066784478D24}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -89,6 +91,10 @@ Global
{28350800-B44B-479B-86E2-1D39E321C0B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{28350800-B44B-479B-86E2-1D39E321C0B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{28350800-B44B-479B-86E2-1D39E321C0B4}.Release|Any CPU.Build.0 = Release|Any CPU
{018F959E-824B-4664-B345-066784478D24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{018F959E-824B-4664-B345-066784478D24}.Debug|Any CPU.Build.0 = Debug|Any CPU
{018F959E-824B-4664-B345-066784478D24}.Release|Any CPU.ActiveCfg = Release|Any CPU
{018F959E-824B-4664-B345-066784478D24}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{4D198E25-C211-41DC-9E84-B15E89BD7048} = {BE6011CC-1200-4957-B01F-FCCA10C5CF5A}
@@ -99,5 +105,6 @@ Global
{CD2887E3-BDA9-434B-A5BF-9ED38DE20332} = {245023D2-D3CA-47B9-831D-DAB91A2FFDC7}
{A2A34BBC-CB5E-4100-9529-A12B6ECB769C} = {245023D2-D3CA-47B9-831D-DAB91A2FFDC7}
{28350800-B44B-479B-86E2-1D39E321C0B4} = {BE6011CC-1200-4957-B01F-FCCA10C5CF5A}
{018F959E-824B-4664-B345-066784478D24} = {67B576EE-02FA-4F9B-94BC-3630BC09ECE5}
EndGlobalSection
EndGlobal
4 changes: 3 additions & 1 deletion docs-builder.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<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">
<s:Boolean x:Key="/Default/UserDictionary/Words/=docset/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=frontmatter/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=linenos/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=literalinclude/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/UserDictionary/Words/=literalinclude/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=_0060_0060inli/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
Original file line number Diff line number Diff line change
@@ -31,7 +31,7 @@ protected override void AddToFileSystem(MockFileSystem fileSystem)

:::{important}
:name: hint_ref
This is a 'important' admonition
This is an 'important' admonition
:::
""";
fileSystem.AddFile(@"docs/testing/req.md", inclusion);
28 changes: 28 additions & 0 deletions tests/authoring/Framework/ErrorCollectorAssertions.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

namespace authoring

open System.Diagnostics
open System.Linq
open Elastic.Markdown.Diagnostics
open FsUnitTyped
open Swensen.Unquote

[<AutoOpen>]
module DiagnosticsCollectorAssertions =

[<DebuggerStepThrough>]
let hasNoErrors (actual: GenerateResult) =
test <@ actual.Context.Collector.Errors = 0 @>

[<DebuggerStepThrough>]
let hasError (expected: string) (actual: GenerateResult) =
actual.Context.Collector.Errors |> shouldBeGreaterThan 0
let errorDiagnostics = actual.Context.Collector.Diagnostics
.Where(fun d -> d.Severity = Severity.Error)
.ToArray()
|> List.ofArray
let message = errorDiagnostics.FirstOrDefault().Message
test <@ message.Contains(expected) @>
123 changes: 123 additions & 0 deletions tests/authoring/Framework/HtmlAssertions.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

namespace authoring

open System
open System.Diagnostics
open System.IO
open AngleSharp.Diffing
open AngleSharp.Diffing.Core
open AngleSharp.Html
open AngleSharp.Html.Parser
open DiffPlex.DiffBuilder
open DiffPlex.DiffBuilder.Model
open JetBrains.Annotations
open Xunit.Sdk

[<AutoOpen>]
module HtmlAssertions =

let htmlDiffString (diffs: seq<IDiff>) =
let NodeName (source:ComparisonSource) = source.Node.NodeType.ToString().ToLowerInvariant();
let htmlText (source:IDiff) =
let formatter = PrettyMarkupFormatter();
let nodeText (control: ComparisonSource) =
use sw = new StringWriter()
control.Node.ToHtml(sw, formatter)
sw.ToString()
let attrText (control: AttributeComparisonSource) =
use sw = new StringWriter()
control.Attribute.ToHtml(sw, formatter)
sw.ToString()
let nodeDiffText (control: ComparisonSource option) (test: ComparisonSource option) =
let actual = match test with Some t -> nodeText t | None -> "missing"
let expected = match control with Some t -> nodeText t | None -> "missing"
$"""

expected: {expected}
actual: {actual}
"""
let attrDiffText (control: AttributeComparisonSource option) (test: AttributeComparisonSource option) =
let actual = match test with Some t -> attrText t | None -> "missing"
let expected = match control with Some t -> attrText t | None -> "missing"
$"""

expected: {expected}
actual: {actual}
"""

match source with
| :? NodeDiff as diff -> nodeDiffText <| Some diff.Control <| Some diff.Test
| :? AttrDiff as diff -> attrDiffText <| Some diff.Control <| Some diff.Test
| :? MissingNodeDiff as diff -> nodeDiffText <| Some diff.Control <| None
| :? MissingAttrDiff as diff -> attrDiffText <| Some diff.Control <| None
| :? UnexpectedNodeDiff as diff -> nodeDiffText None <| Some diff.Test
| :? UnexpectedAttrDiff as diff -> attrDiffText None <| Some diff.Test
| _ -> failwith $"Unknown diff type detected: {source.GetType()}"

diffs
|> Seq.map (fun diff ->

match diff with
| :? NodeDiff as diff when diff.Target = DiffTarget.Text && diff.Control.Path.Equals(diff.Test.Path, StringComparison.Ordinal)
-> $"The text in {diff.Control.Path} is different."
| :? NodeDiff as diff when diff.Target = DiffTarget.Text
-> $"The expected {NodeName(diff.Control)} at {diff.Control.Path} and the actual {NodeName(diff.Test)} at {diff.Test.Path} is different."
| :? NodeDiff as diff when diff.Control.Path.Equals(diff.Test.Path, StringComparison.Ordinal)
-> $"The {NodeName(diff.Control)}s at {diff.Control.Path} are different."
| :? NodeDiff as diff -> $"The expected {NodeName(diff.Control)} at {diff.Control.Path} and the actual {NodeName(diff.Test)} at {diff.Test.Path} are different."
| :? AttrDiff as diff when diff.Control.Path.Equals(diff.Test.Path, StringComparison.Ordinal)
-> $"The values of the attributes at {diff.Control.Path} are different."
| :? AttrDiff as diff -> $"The value of the attribute {diff.Control.Path} and actual attribute {diff.Test.Path} are different."
| :? MissingNodeDiff as diff -> $"The {NodeName(diff.Control)} at {diff.Control.Path} is missing."
| :? MissingAttrDiff as diff -> $"The attribute at {diff.Control.Path} is missing."
| :? UnexpectedNodeDiff as diff -> $"The {NodeName(diff.Test)} at {diff.Test.Path} was not expected."
| :? UnexpectedAttrDiff as diff -> $"The attribute at {diff.Test.Path} was not expected."
| _ -> failwith $"Unknown diff type detected: {diff.GetType()}"
+
htmlText diff
)
|> String.concat "\n"

let private prettyHtml (html:string) =
let parser = HtmlParser()
let document = parser.ParseDocument(html)
use sw = new StringWriter()
document.Body.Children
|> Seq.iter _.ToHtml(sw, PrettyMarkupFormatter())
sw.ToString()

[<DebuggerStepThrough>]
let convertsToHtml ([<LanguageInjection("html")>]expected: string) (actual: GenerateResult) =
let diffs =
DiffBuilder
.Compare(actual.Html)
.WithTest(expected)
.Build()

let diff = htmlDiffString diffs
match diff with
| s when String.IsNullOrEmpty s -> ()
| s ->
let expectedHtml = prettyHtml expected
let actualHtml = prettyHtml actual.Html
let textDiff =
InlineDiffBuilder.Diff(expectedHtml, actualHtml).Lines
|> Seq.map(fun l ->
match l.Type with
| ChangeType.Deleted -> "- " + l.Text
| ChangeType.Modified -> "+ " + l.Text
| ChangeType.Inserted -> "+ " + l.Text
| _ -> " " + l.Text
)
|> String.concat "\n"
let msg = $"""Html was not equal
{textDiff}

{diff}
"""
raise (XunitException(msg))


81 changes: 81 additions & 0 deletions tests/authoring/Framework/Setup.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

namespace authoring

open System.Collections.Generic
open System.IO
open System.IO.Abstractions.TestingHelpers
open System.Threading.Tasks
open Elastic.Markdown
open Elastic.Markdown.IO
open JetBrains.Annotations

type Setup =

static let GenerateDocSetYaml(
fileSystem: MockFileSystem,
globalVariables: Dictionary<string, string> option
) =
let root = fileSystem.DirectoryInfo.New(Path.Combine(Paths.Root.FullName, "docs/"));
let yaml = new StringWriter();
yaml.WriteLine("toc:");
let markdownFiles = fileSystem.Directory.EnumerateFiles(root.FullName, "*.md", SearchOption.AllDirectories)
markdownFiles
|> Seq.iter(fun markdownFile ->
let relative = fileSystem.Path.GetRelativePath(root.FullName, markdownFile);
yaml.WriteLine($" - file: {relative}");
)
match globalVariables with
| Some vars ->
yaml.WriteLine($"subs:")
vars |> Seq.iter(fun kv ->
yaml.WriteLine($" {kv.Key}: {kv.Value}");
)
| _ -> ()

fileSystem.AddFile(Path.Combine(root.FullName, "docset.yml"), MockFileData(yaml.ToString()));

static let Generate ([<LanguageInjection("markdown")>]m: string) : Task<GenerateResult> =

let d = dict [ ("docs/index.md", MockFileData(m)) ]
let opts = MockFileSystemOptions(CurrentDirectory=Paths.Root.FullName)
let fileSystem = MockFileSystem(d, opts)

GenerateDocSetYaml (fileSystem, None)

let collector = TestDiagnosticsCollector();
let context = BuildContext(fileSystem, Collector=collector)
let set = DocumentationSet(context);
let file =
match set.GetMarkdownFile(fileSystem.FileInfo.New("docs/index.md")) with
| NonNull f -> f
| _ -> failwithf "docs/index.md could not be located"

let context = {
File = file
Collector = collector
Set = set
ReadFileSystem = fileSystem
WriteFileSystem = fileSystem
}
context.Bootstrap()

/// Pass a full documentation page to the test setup
static member Document ([<LanguageInjection("markdown")>]m: string) =
let g = task { return! Generate m }
g |> Async.AwaitTask |> Async.RunSynchronously

/// Pass a markdown fragment to the test setup
static member Markdown ([<LanguageInjection("markdown")>]m: string) =
// language=markdown
let m = $"""
# Test Document
{m}
"""
let g = task {
return! Generate m
}
g |> Async.AwaitTask |> Async.RunSynchronously

70 changes: 70 additions & 0 deletions tests/authoring/Framework/TestValues.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

namespace authoring

open System
open System.IO.Abstractions
open Elastic.Markdown.Diagnostics
open Elastic.Markdown.IO
open Markdig.Syntax
open Xunit


type TestDiagnosticsOutput() =

interface IDiagnosticsOutput with
member this.Write diagnostic =
let line = match diagnostic.Line with | NonNullV l -> l | _ -> 0
match TestContext.Current.TestOutputHelper with
| NonNull output ->
match diagnostic.Severity with
| Severity.Error ->
output.WriteLine($"Error: {diagnostic.Message} ({diagnostic.File}:{line})")
| _ ->
output.WriteLine($"Warn : {diagnostic.Message} ({diagnostic.File}:{line})")
| _ -> ()


type TestDiagnosticsCollector() =
inherit DiagnosticsCollector([TestDiagnosticsOutput()])

let diagnostics = System.Collections.Generic.List<Diagnostic>()

member _.Diagnostics = diagnostics.AsReadOnly()

override this.HandleItem diagnostic = diagnostics.Add(diagnostic);

type GenerateResult = {
Document: MarkdownDocument
Html: string
Context: MarkdownTestContext
}

and MarkdownTestContext =
{
File: MarkdownFile
Collector: TestDiagnosticsCollector
Set: DocumentationSet
ReadFileSystem: IFileSystem
WriteFileSystem: IFileSystem
}

member this.Bootstrap () = backgroundTask {
let! ctx = Async.CancellationToken
let _ = this.Collector.StartAsync(ctx)
do! this.Set.ResolveDirectoryTree(ctx)

let! document = this.File.ParseFullAsync(ctx)

let html = this.File.CreateHtml(document);
this.Collector.Channel.TryComplete()
do! this.Collector.StopAsync(ctx)
return { Context = this; Document = document; Html = html }
}

interface IDisposable with
member this.Dispose() = ()


19 changes: 19 additions & 0 deletions tests/authoring/Inline/Comments.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

module ``inline elements``.``comment block``

open Xunit
open authoring

type ``commented line`` () =

static let markdown = Setup.Markdown """
% comment
not a comment
"""

[<Fact>]
let ``validate HTML: commented line should not be emitted`` () =
markdown |> convertsToHtml """<p>not a comment</p>"""
24 changes: 24 additions & 0 deletions tests/authoring/Inline/InlineAnchors.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

module ``inline elements``.``anchors DEPRECATED``

open Xunit
open authoring

type ``inline anchor in the middle`` () =

static let markdown = Setup.Markdown """
this is *regular* text and this $$$is-an-inline-anchor$$$ and this continues to be regular text
"""

[<Fact>]
let ``validate HTML`` () =
markdown |> convertsToHtml """
<p>this is <em>regular</em> text and this
<a id="is-an-inline-anchor"></a> and this continues to be regular text
</p>
"""
[<Fact>]
let ``has no errors`` () = markdown |> hasNoErrors
Loading
Loading