Skip to content
Merged
Show file tree
Hide file tree
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
146 changes: 146 additions & 0 deletions CodeBlocker.Test/CodeBlockerExtensionsTests.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace CodeBlocker.Tests;

using ktsu.CodeBlocker;
using Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>
/// Covers the file-level preamble helpers.
/// </summary>
[TestClass]
public sealed class CodeBlockerExtensionsTests
{
private static CodeBlocker Create() => CodeBlocker.Create(CodeBlocker.DefaultIndentString, NewLines.Lf);

[TestMethod]
public void AutoGeneratedHeaderWithoutCopyrightIsJustTheMarker()
{
using CodeBlocker codeBlocker = Create();

codeBlocker.WriteAutoGeneratedHeader();

Assert.AreEqual("// <auto-generated />\n\n", codeBlocker.ToString());
}

[TestMethod]
public void AutoGeneratedHeaderPutsTheCopyrightAboveTheMarker()
{
using CodeBlocker codeBlocker = Create();

codeBlocker.WriteAutoGeneratedHeader("Copyright (c) 2023-2026 ktsu-dev contributors");

Assert.AreEqual(
"// Copyright (c) 2023-2026 ktsu-dev contributors\n// <auto-generated />\n\n",
codeBlocker.ToString());
}

[TestMethod]
public void NullableDirectivesAreWrittenVerbatim()
{
using CodeBlocker enable = Create();
using CodeBlocker disable = Create();

enable.WriteNullableEnable();
disable.WriteNullableDisable();

Assert.AreEqual("#nullable enable\n", enable.ToString());
Assert.AreEqual("#nullable disable\n", disable.ToString());
}

[TestMethod]
public void FileScopedNamespaceIsFollowedByABlankLine()
{
using CodeBlocker codeBlocker = Create();

codeBlocker.WriteFileScopedNamespace("Contoso.Widgets");

Assert.AreEqual("namespace Contoso.Widgets;\n\n", codeBlocker.ToString());
}

[TestMethod]
public void AnEmptyNamespaceWritesNothing()
{
using CodeBlocker nullNamespace = Create();
using CodeBlocker emptyNamespace = Create();

nullNamespace.WriteFileScopedNamespace(null);
emptyNamespace.WriteFileScopedNamespace(string.Empty);

Assert.AreEqual(string.Empty, nullNamespace.ToString());
Assert.AreEqual(string.Empty, emptyNamespace.ToString());
}

[TestMethod]
public void UsingsAreWrittenOnePerLineAndFollowedByABlankLine()
{
using CodeBlocker codeBlocker = Create();

codeBlocker.WriteUsings("System", "System.Collections.Generic");

Assert.AreEqual("using System;\nusing System.Collections.Generic;\n\n", codeBlocker.ToString());
}

[TestMethod]
public void UsingsAreWrittenVerbatimSoAliasesAndStaticImportsWork()
{
using CodeBlocker codeBlocker = Create();

codeBlocker.WriteUsings("static System.Math", "Text = System.Text");

Assert.AreEqual("using static System.Math;\nusing Text = System.Text;\n\n", codeBlocker.ToString());
}

[TestMethod]
public void NoUsingsWritesNothingIncludingTheBlankLine()
{
// Both overloads, because a generator calls this unconditionally and either spelling has to
// come out empty rather than leaving a stray blank line at the top of the file.
using CodeBlocker noArguments = Create();
using CodeBlocker emptySequence = Create();

noArguments.WriteUsings();
emptySequence.WriteUsings(Enumerable.Empty<string>());

Assert.AreEqual(string.Empty, noArguments.ToString());
Assert.AreEqual(string.Empty, emptySequence.ToString());
}

[TestMethod]
public void APreambleComposesWithConsistentSpacing()
{
using CodeBlocker codeBlocker = Create();

codeBlocker
.WriteAutoGeneratedHeader("Copyright (c) 2023-2026 ktsu-dev contributors")
.WriteNullableEnable()
.WriteFileScopedNamespace("Contoso.Widgets")
.WriteUsings("System");

Assert.AreEqual(
"""
// Copyright (c) 2023-2026 ktsu-dev contributors
// <auto-generated />

#nullable enable
namespace Contoso.Widgets;

using System;


""".ReplaceLineEndings("\n"),
codeBlocker.ToString());
}

[TestMethod]
public void NullArgumentsThrow()
{
using CodeBlocker codeBlocker = Create();

Assert.ThrowsExactly<ArgumentNullException>(() => CodeBlockerExtensions.WriteAutoGeneratedHeader(null!));
Assert.ThrowsExactly<ArgumentNullException>(() => CodeBlockerExtensions.WriteNullableEnable(null!));
Assert.ThrowsExactly<ArgumentNullException>(() => CodeBlockerExtensions.WriteNullableDisable(null!));
Assert.ThrowsExactly<ArgumentNullException>(() => CodeBlockerExtensions.WriteFileScopedNamespace(null!, "N"));
Assert.ThrowsExactly<ArgumentNullException>(() => codeBlocker.WriteUsings((IEnumerable<string>)null!));
}
}
244 changes: 244 additions & 0 deletions CodeBlocker.Test/DocCommentTests.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace CodeBlocker.Tests;

using ktsu.CodeBlocker;
using ktsu.CodeBlocker.Templates;
using Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>
/// Covers XML documentation modelled as data: escaping, tag order, multi-line layout, and
/// validation against the documented member.
/// </summary>
[TestClass]
public sealed class DocCommentTests
{
private static string Render(Action<CodeBlocker> write)
{
using CodeBlocker codeBlocker = CodeBlocker.Create(CodeBlocker.DefaultIndentString, NewLines.Lf);
write(codeBlocker);
return codeBlocker.ToString();
}

private static string Render(DocComment documentation) => Render(documentation.WriteTo);

[TestMethod]
public void AnEmptyCommentWritesNothing()
{
DocComment documentation = new();

Assert.IsTrue(documentation.IsEmpty);
Assert.AreEqual(string.Empty, Render(documentation));
}

[TestMethod]
public void ASingleLineSummaryStaysOnOneLine() =>
Assert.AreEqual(
"/// <summary>How many widgets.</summary>\n",
Render(new DocComment { Summary = "How many widgets." }));

[TestMethod]
public void AMultiLineSummaryPrefixesEveryLine()
{
DocComment documentation = new()
{
Summary = "How many widgets.\nCounted lazily.",
};

Assert.AreEqual(
"""
/// <summary>
/// How many widgets.
/// Counted lazily.
/// </summary>

""".ReplaceLineEndings("\n"),
Render(documentation));
}

[TestMethod]
public void ABlankLineInsideATagIsWrittenWithoutTrailingWhitespace()
{
DocComment documentation = new() { Remarks = "First.\n\nSecond." };

Assert.AreEqual(
"/// <remarks>\n/// First.\n///\n/// Second.\n/// </remarks>\n",
Render(documentation));
}

[TestMethod]
public void EitherLineTerminatorSplitsTheText()
{
Assert.AreEqual(
Render(new DocComment { Summary = "a\nb" }),
Render(new DocComment { Summary = "a\r\nb" }));
}

[TestMethod]
public void TextContentIsEscapedByDefault()
{
// "values in the range <0, 1>" is an entirely ordinary thing for metadata to say, and it
// used to emit malformed XML.
DocComment documentation = new() { Summary = "Values in the range <0, 1> & beyond." };

Assert.AreEqual(
"/// <summary>Values in the range &lt;0, 1&gt; &amp; beyond.</summary>\n",
Render(documentation));
}

[TestMethod]
public void EscapingCanBeTurnedOffForTextThatEmbedsMarkup()
{
DocComment documentation = new()
{
EscapeText = false,
Summary = "Wraps <see cref=\"System.Int32\"/>.",
};

Assert.AreEqual(
"/// <summary>Wraps <see cref=\"System.Int32\"/>.</summary>\n",
Render(documentation));
}

[TestMethod]
public void AttributeValuesAreAlwaysEscaped()
{
DocComment documentation = new() { EscapeText = false };
documentation.Params.Add(new DocTag { Name = "a<b", Text = "x" });

Assert.AreEqual("/// <param name=\"a&lt;b\">x</param>\n", Render(documentation));
}

[TestMethod]
public void TagsAreWrittenInCanonicalOrder()
{
DocComment documentation = new()
{
Remarks = "Remarks.",
Returns = "The sum.",
Value = "The value.",
Summary = "Adds.",
};
documentation.SeeAlso.Add("System.Math");
documentation.Exceptions.Add(new DocTag { Name = "System.OverflowException", Text = "It overflowed." });
documentation.Params.Add(new DocTag { Name = "a", Text = "First." });
documentation.TypeParams.Add(new DocTag { Name = "T", Text = "The type." });

Assert.AreEqual(
"""
/// <summary>Adds.</summary>
/// <typeparam name="T">The type.</typeparam>
/// <param name="a">First.</param>
/// <returns>The sum.</returns>
/// <value>The value.</value>
/// <exception cref="System.OverflowException">It overflowed.</exception>
/// <remarks>Remarks.</remarks>
/// <seealso cref="System.Math"/>

""".ReplaceLineEndings("\n"),
Render(documentation));
}

[TestMethod]
public void InheritDocIsWrittenFirst()
{
DocComment bare = new() { InheritDoc = true };
DocComment withCref = new() { InheritDoc = true, InheritDocCref = "IWidget.Count", Summary = "Count." };

Assert.AreEqual("/// <inheritdoc/>\n", Render(bare));
Assert.AreEqual(
"/// <inheritdoc cref=\"IWidget.Count\"/>\n/// <summary>Count.</summary>\n",
Render(withCref));
}

[TestMethod]
public void ValidationAcceptsAMatchingComment()
{
DocComment documentation = new();
documentation.Params.Add(new DocTag { Name = "a", Text = "First." });
documentation.TypeParams.Add(new DocTag { Name = "T", Text = "The type." });

Assert.IsEmpty(documentation.Validate(["a"], ["T"]));
}

[TestMethod]
public void ValidationReportsATagThatNamesNothing()
{
DocComment documentation = new();
documentation.Params.Add(new DocTag { Name = "typo", Text = "First." });

IReadOnlyList<string> issues = documentation.Validate(["a"], []);

Assert.HasCount(2, issues);
Assert.Contains("typo", issues[0]);
Assert.Contains("'a' has no <param> entry", issues[1]);
}

[TestMethod]
public void ValidationReportsADuplicateTag()
{
DocComment documentation = new();
documentation.Params.Add(new DocTag { Name = "a", Text = "First." });
documentation.Params.Add(new DocTag { Name = "a", Text = "Again." });

IReadOnlyList<string> issues = documentation.Validate(["a"], []);

Assert.HasCount(1, issues);
Assert.Contains("more than once", issues[0]);
}

[TestMethod]
public void ValidationRejectsNullArguments()
{
DocComment documentation = new();

Assert.ThrowsExactly<ArgumentNullException>(() => documentation.Validate(null!, []));
Assert.ThrowsExactly<ArgumentNullException>(() => documentation.Validate([], null!));
}

[TestMethod]
public void ATemplateWritesItsDocumentationAboveItsAttributes()
{
MethodTemplate method = new()
{
Type = "int",
Name = "Add",
Keywords = { "public" },
Attributes = { "Pure" },
Comments = { "// Runs in constant time." },
Parameters =
{
new ParameterTemplate { Type = "int", Name = "a" },
new ParameterTemplate { Type = "int", Name = "b" },
},
Documentation = new DocComment
{
Summary = "Adds two numbers.",
Returns = "Their sum.",
Params =
{
new DocTag { Name = "a", Text = "The first." },
new DocTag { Name = "b", Text = "The second." },
},
},
BodyFactory = codeBlocker => codeBlocker.Write("=> a + b;"),
};

Assert.AreEqual(
"""
/// <summary>Adds two numbers.</summary>
/// <param name="a">The first.</param>
/// <param name="b">The second.</param>
/// <returns>Their sum.</returns>
// Runs in constant time.
[Pure]
public int Add(int a, int b) => a + b;

""".ReplaceLineEndings("\n"),
Render(method.WriteTo));
}

[TestMethod]
public void NullCodeBlockerIsRejected() =>
Assert.ThrowsExactly<ArgumentNullException>(() => new DocComment().WriteTo(null!));
}
Loading