style: expand one-line summaries and run dotnet format solution-wide

This commit is contained in:
yyc12345 committed 2026-10-04 10:22:02 +08:00
1 parent 9ea551b600
commit 8b7e50a955
95 files changed
+970 -1424

No files matched your search

+7 -14
View File
@@ -7,8 +7,7 @@ namespace YKanBan.Tests.Search;
/// records compare by reference for their Terms collections by design (see the
/// YYC MARK note in SearchNodes.cs), so tests provide equality explicitly.
/// </summary>
public sealed class SearchNodeComparer : IEqualityComparer<SearchNode>
{
public sealed class SearchNodeComparer : IEqualityComparer<SearchNode> {
/// <summary>
/// Gets the shared comparer instance.
/// </summary>
@@ -20,19 +19,15 @@ public sealed class SearchNodeComparer : IEqualityComparer<SearchNode>
/// <param name="x">The first node.</param>
/// <param name="y">The second node.</param>
/// <returns><see langword="true"/> when the nodes are structurally equal.</returns>
public bool Equals(SearchNode? x, SearchNode? y)
{
if (ReferenceEquals(x, y))
{
public bool Equals(SearchNode? x, SearchNode? y) {
if (ReferenceEquals(x, y)) {
return true;
}
if (x is null || y is null || x.GetType() != y.GetType())
{
if (x is null || y is null || x.GetType() != y.GetType()) {
return false;
}
return (x, y) switch
{
return (x, y) switch {
(SearchNode.Or a, SearchNode.Or b) => SequencesEqual(a.Terms, b.Terms),
(SearchNode.And a, SearchNode.And b) => SequencesEqual(a.Terms, b.Terms),
(SearchNode.FreeText a, SearchNode.FreeText b) => a.Text == b.Text,
@@ -58,10 +53,8 @@ public sealed class SearchNodeComparer : IEqualityComparer<SearchNode>
/// <param name="x">The first sequence.</param>
/// <param name="y">The second sequence.</param>
/// <returns><see langword="true"/> when both sequences match element-wise.</returns>
private static bool SequencesEqual(IReadOnlyList<SearchNode> x, IReadOnlyList<SearchNode> y)
{
if (x.Count != y.Count)
{
private static bool SequencesEqual(IReadOnlyList<SearchNode> x, IReadOnlyList<SearchNode> y) {
if (x.Count != y.Count) {
return false;
}
return x.Zip(y).All(pair => Instance.Equals(pair.First, pair.Second));
+39 -78
View File
@@ -8,15 +8,13 @@ namespace YKanBan.Tests.Search;
/// <see cref="SearchSyntaxException"/>).
/// </summary>
[TestClass]
public class SearchQueryParserTests
{
public class SearchQueryParserTests {
/// <summary>
/// Parses and asserts structural equality against the expected AST.
/// </summary>
/// <param name="expected">The expected AST.</param>
/// <param name="text">The search text.</param>
private static void AssertParsesTo(SearchNode expected, string text)
{
private static void AssertParsesTo(SearchNode expected, string text) {
SearchNode? parsed = SearchQueryParser.Parse(text);
Assert.IsNotNull(parsed, $"Blank-looking input: '{text}'.");
Assert.AreEqual(expected, parsed, SearchNodeComparer.Instance);
@@ -26,53 +24,46 @@ public class SearchQueryParserTests
/// Asserts that the input violates the grammar.
/// </summary>
/// <param name="text">The search text.</param>
private static void AssertSyntaxError(string text)
{
private static void AssertSyntaxError(string text) {
Assert.ThrowsExactly<SearchSyntaxException>(() => SearchQueryParser.Parse(text));
}
#region Successful parses
[TestMethod]
public void BlankInputReturnsNull()
{
public void BlankInputReturnsNull() {
Assert.IsNull(SearchQueryParser.Parse(string.Empty));
Assert.IsNull(SearchQueryParser.Parse(" "));
}
[TestMethod]
public void SingleWordIsFreeText()
{
public void SingleWordIsFreeText() {
AssertParsesTo(new SearchNode.FreeText("login"), "login");
}
[TestMethod]
public void AdjacentWordsAreImplicitAnd()
{
public void AdjacentWordsAreImplicitAnd() {
AssertParsesTo(
new SearchNode.And([new SearchNode.FreeText("fix"), new SearchNode.FreeText("login")]),
"fix login");
}
[TestMethod]
public void ExplicitAndKeyword()
{
public void ExplicitAndKeyword() {
AssertParsesTo(
new SearchNode.And([new SearchNode.FreeText("a"), new SearchNode.FreeText("b")]),
"a AND b");
}
[TestMethod]
public void OrKeyword()
{
public void OrKeyword() {
AssertParsesTo(
new SearchNode.Or([new SearchNode.FreeText("a"), new SearchNode.FreeText("b")]),
"a OR b");
}
[TestMethod]
public void ImplicitAndBindsTighterThanOr()
{
public void ImplicitAndBindsTighterThanOr() {
AssertParsesTo(
new SearchNode.Or([
new SearchNode.And([new SearchNode.FreeText("a"), new SearchNode.FreeText("b")]),
@@ -81,8 +72,7 @@ public class SearchQueryParserTests
}
[TestMethod]
public void ParenthesesForceGrouping()
{
public void ParenthesesForceGrouping() {
AssertParsesTo(
new SearchNode.And([
new SearchNode.FreeText("a"),
@@ -91,16 +81,14 @@ public class SearchQueryParserTests
}
[TestMethod]
public void NestedParenthesesCollapse()
{
public void NestedParenthesesCollapse() {
AssertParsesTo(
new SearchNode.Or([new SearchNode.FreeText("a"), new SearchNode.FreeText("b")]),
"((a OR b))");
}
[TestMethod]
public void LowercaseKeywordsArePlainWords()
{
public void LowercaseKeywordsArePlainWords() {
AssertParsesTo(
new SearchNode.And([
new SearchNode.FreeText("a"),
@@ -111,14 +99,12 @@ public class SearchQueryParserTests
}
[TestMethod]
public void PhraseIsFreeText()
{
public void PhraseIsFreeText() {
AssertParsesTo(new SearchNode.FreeText("hello world"), "\"hello world\"");
}
[TestMethod]
public void QuotedOperatorKeywordIsFreeText()
{
public void QuotedOperatorKeywordIsFreeText() {
// A quoted AND/OR is literal text, not an operator.
AssertParsesTo(new SearchNode.FreeText("AND"), "\"AND\"");
AssertParsesTo(
@@ -127,54 +113,46 @@ public class SearchQueryParserTests
}
[TestMethod]
public void PhraseSupportsEscapes()
{
public void PhraseSupportsEscapes() {
// Escaped quote and escaped backslash.
AssertParsesTo(new SearchNode.FreeText("a\"b"), "\"a\\\"b\"");
AssertParsesTo(new SearchNode.FreeText("a\\b"), "\"a\\\\b\"");
}
[TestMethod]
public void TagQualifier()
{
public void TagQualifier() {
AssertParsesTo(new SearchNode.TagName("bug"), "tag:bug");
}
[TestMethod]
public void TagQualifierAcceptsPhraseValue()
{
public void TagQualifierAcceptsPhraseValue() {
AssertParsesTo(new SearchNode.TagName("a b"), "tag:\"a b\"");
}
[TestMethod]
public void QualifierValueMayContainColons()
{
public void QualifierValueMayContainColons() {
AssertParsesTo(new SearchNode.TagName("a:b"), "tag:a:b");
}
[TestMethod]
public void TitleAndContentQualifiers()
{
public void TitleAndContentQualifiers() {
AssertParsesTo(new SearchNode.FieldText(SearchField.Title, "foo"), "title:foo");
AssertParsesTo(new SearchNode.FieldText(SearchField.Content, "foo"), "content:foo");
}
[TestMethod]
public void ColumnQualifierAcceptsPhraseValue()
{
public void ColumnQualifierAcceptsPhraseValue() {
AssertParsesTo(new SearchNode.ColumnTitle("To Do"), "column:\"To Do\"");
}
[TestMethod]
public void IdQualifierParsesNumber()
{
public void IdQualifierParsesNumber() {
AssertParsesTo(new SearchNode.CardId(42), "id:42");
AssertParsesTo(new SearchNode.CardId(12), "id:\"12\"");
}
[TestMethod]
public void ColonWithoutKeyPrefixIsFreeText()
{
public void ColonWithoutKeyPrefixIsFreeText() {
AssertParsesTo(new SearchNode.FreeText(":foo"), ":foo");
}
@@ -183,106 +161,89 @@ public class SearchQueryParserTests
#region Strict failures
[TestMethod]
public void UnterminatedPhraseIsAnError()
{
public void UnterminatedPhraseIsAnError() {
AssertSyntaxError("\"abc def");
}
[TestMethod]
public void TrailingUnterminatedPhraseIsAnError()
{
public void TrailingUnterminatedPhraseIsAnError() {
AssertSyntaxError("a \"abc");
}
[TestMethod]
public void InvalidEscapeInPhraseIsAnError()
{
public void InvalidEscapeInPhraseIsAnError() {
// A bare backslash (not followed by a quote or another backslash) is invalid.
AssertSyntaxError("\"a\\b\"");
}
[TestMethod]
public void InvalidEscapeInQualifierPhraseIsAnError()
{
public void InvalidEscapeInQualifierPhraseIsAnError() {
AssertSyntaxError("tag:\"a\\x\"");
}
[TestMethod]
public void UnbalancedOpenParenIsAnError()
{
public void UnbalancedOpenParenIsAnError() {
AssertSyntaxError("(a");
}
[TestMethod]
public void StrayCloseParenIsAnError()
{
public void StrayCloseParenIsAnError() {
AssertSyntaxError("a)");
}
[TestMethod]
public void EmptyParensAreAnError()
{
public void EmptyParensAreAnError() {
AssertSyntaxError("()");
}
[TestMethod]
public void DanglingOrIsAnError()
{
public void DanglingOrIsAnError() {
AssertSyntaxError("a OR");
}
[TestMethod]
public void DanglingAndIsAnError()
{
public void DanglingAndIsAnError() {
AssertSyntaxError("a AND");
}
[TestMethod]
public void LeadingAndIsAnError()
{
public void LeadingAndIsAnError() {
AssertSyntaxError("AND a");
}
[TestMethod]
public void OperatorAtOperandPositionIsAnError()
{
public void OperatorAtOperandPositionIsAnError() {
AssertSyntaxError("a AND OR b");
}
[TestMethod]
public void QualifierWithoutValueIsAnError()
{
public void QualifierWithoutValueIsAnError() {
AssertSyntaxError("tag:");
}
[TestMethod]
public void SpacedQualifierValueIsAnError()
{
public void SpacedQualifierValueIsAnError() {
AssertSyntaxError("tag: bug");
}
[TestMethod]
public void NonNumericIdIsAnError()
{
public void NonNumericIdIsAnError() {
AssertSyntaxError("id:12x");
}
[TestMethod]
public void UnknownQualifierKeyIsAnError()
{
public void UnknownQualifierKeyIsAnError() {
AssertSyntaxError("foo:bar");
}
[TestMethod]
public void QualifierKeysAreCaseSensitive()
{
public void QualifierKeysAreCaseSensitive() {
// "Tag" is a key-like prefix but not the known lowercase "tag", so it is unknown.
AssertSyntaxError("Tag:x");
}
[TestMethod]
public void SyntaxErrorCarriesDescriptiveMessage()
{
public void SyntaxErrorCarriesDescriptiveMessage() {
SearchSyntaxException exception = Assert.ThrowsExactly<SearchSyntaxException>(
() => SearchQueryParser.Parse("a AND"));
Assert.IsFalse(string.IsNullOrEmpty(exception.Message));
+18 -34
View File
@@ -1,4 +1,5 @@
using Microsoft.Data.Sqlite;
using YKanBan.Search;
using YKanBan.Storage.Workspace;
using YKanBan.Tests.TestUtilities;
@@ -10,22 +11,18 @@ namespace YKanBan.Tests.Search;
/// workspace database.
/// </summary>
[TestClass]
public class SearchSqlCompilerTests
{
public class SearchSqlCompilerTests {
/// <summary>
/// Owns a workspace with a fixed card/tag fixture.
/// </summary>
private sealed class SearchFixture : IDisposable
{
private sealed class SearchFixture : IDisposable {
public TempDirectory Directory { get; } = new();
public SqliteConnection Connection { get; }
public SearchFixture()
{
public SearchFixture() {
WorkspaceInitializer.Initialize(Directory.FullPath);
using (SqliteConnection setup = SqliteTestHelper.OpenWorkspace(Directory.FullPath))
{
using (SqliteConnection setup = SqliteTestHelper.OpenWorkspace(Directory.FullPath)) {
// Columns: 1 "To Do", 2 "In Progress".
SqliteTestHelper.Exec(setup, "INSERT INTO columns (title, description, created_at, updated_at) VALUES ('To Do', '', 1, 1);");
SqliteTestHelper.Exec(setup, "INSERT INTO columns (title, description, created_at, updated_at) VALUES ('In Progress', '', 1, 1);");
@@ -54,38 +51,33 @@ public class SearchSqlCompilerTests
/// </summary>
/// <param name="queryText">The search text.</param>
/// <returns>The matching card ids in id order.</returns>
public List<long> Search(string queryText)
{
public List<long> Search(string queryText) {
SearchNode? query = SearchQueryParser.Parse(queryText);
Assert.IsNotNull(query, "Blank fixture queries are not expected here.");
CompiledSearch compiled = SearchSqlCompiler.Compile(query);
using var command = Connection.CreateCommand();
command.CommandText = $"SELECT c.id FROM cards c WHERE {compiled.Predicate} ORDER BY c.id ASC;";
foreach ((string name, object value) in compiled.Parameters)
{
foreach ((string name, object value) in compiled.Parameters) {
command.Parameters.AddWithValue(name, value);
}
using var reader = command.ExecuteReader();
var ids = new List<long>();
while (reader.Read())
{
while (reader.Read()) {
ids.Add(reader.GetInt64(0));
}
return ids;
}
public void Dispose()
{
public void Dispose() {
Connection.Dispose();
Directory.Dispose();
}
}
[TestMethod]
public void FreeTextMatchesTitleOrContentCaseInsensitively()
{
public void FreeTextMatchesTitleOrContentCaseInsensitively() {
using var fixture = new SearchFixture();
CollectionAssert.AreEqual(new[] { 2L }, fixture.Search("apple"));
CollectionAssert.AreEqual(new[] { 1L }, fixture.Search("login"));
@@ -94,8 +86,7 @@ public class SearchSqlCompilerTests
}
[TestMethod]
public void FieldQualifiersScopeToTheirField()
{
public void FieldQualifiersScopeToTheirField() {
using var fixture = new SearchFixture();
CollectionAssert.AreEqual(new[] { 2L }, fixture.Search("title:apple"));
CollectionAssert.AreEqual(new[] { 3L }, fixture.Search("content:banana"));
@@ -103,8 +94,7 @@ public class SearchSqlCompilerTests
}
[TestMethod]
public void TagQualifierMatchesExactlyAndCaseInsensitively()
{
public void TagQualifierMatchesExactlyAndCaseInsensitively() {
using var fixture = new SearchFixture();
CollectionAssert.AreEqual(new[] { 1L }, fixture.Search("tag:bug"));
CollectionAssert.AreEqual(new[] { 2L, 3L }, fixture.Search("tag:ui"));
@@ -116,8 +106,7 @@ public class SearchSqlCompilerTests
}
[TestMethod]
public void ColumnQualifierMatchesColumnTitleExactly()
{
public void ColumnQualifierMatchesColumnTitleExactly() {
using var fixture = new SearchFixture();
CollectionAssert.AreEqual(new[] { 1L, 4L }, fixture.Search("column:\"to do\""));
CollectionAssert.AreEqual(new[] { 2L, 3L }, fixture.Search("column:\"in progress\""));
@@ -128,16 +117,14 @@ public class SearchSqlCompilerTests
}
[TestMethod]
public void IdQualifierMatchesExactly()
{
public void IdQualifierMatchesExactly() {
using var fixture = new SearchFixture();
CollectionAssert.AreEqual(new[] { 3L }, fixture.Search("id:3"));
Assert.AreEqual(0, fixture.Search("id:99").Count);
}
[TestMethod]
public void AndOrCombination()
{
public void AndOrCombination() {
using var fixture = new SearchFixture();
CollectionAssert.AreEqual(new[] { 2L, 3L }, fixture.Search("apple OR banana"));
CollectionAssert.AreEqual(new[] { 2L }, fixture.Search("apple AND id:2"));
@@ -145,8 +132,7 @@ public class SearchSqlCompilerTests
}
[TestMethod]
public void ParenthesizedGrouping()
{
public void ParenthesizedGrouping() {
using var fixture = new SearchFixture();
// (apple OR banana) hits cards 2,3; intersected with column "In Progress" (2,3).
@@ -157,16 +143,14 @@ public class SearchSqlCompilerTests
}
[TestMethod]
public void LikeWildcardsInUserTextMatchLiterally()
{
public void LikeWildcardsInUserTextMatchLiterally() {
using var fixture = new SearchFixture();
CollectionAssert.AreEqual(new[] { 4L }, fixture.Search("100%"));
CollectionAssert.AreEqual(new[] { 4L }, fixture.Search("a_b"));
}
[TestMethod]
public void CompiledPredicateUsesNamedParameters()
{
public void CompiledPredicateUsesNamedParameters() {
CompiledSearch compiled = SearchSqlCompiler.Compile(new SearchNode.FreeText("x"));
StringAssert.Contains(compiled.Predicate, "$p0");