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
56 changes: 56 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
root = true

[*]
charset = utf-8
insert_final_newline = true
trim_trailing_whitespace = true

[*.{cs,csx}]
indent_style = space
indent_size = 4
tab_width = 4

dotnet_sort_system_directives_first = true
dotnet_separate_import_directive_groups = false

csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_indent_case_contents = true
csharp_indent_switch_labels = true
csharp_indent_labels = one_less_than_current
csharp_space_after_cast = false
csharp_style_namespace_declarations = file_scoped:suggestion
csharp_style_prefer_method_group_conversion = true:suggestion
csharp_style_prefer_primary_constructors = true:suggestion
csharp_style_prefer_readonly_struct = true:suggestion
csharp_style_prefer_readonly_struct_member = true:suggestion

dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_event = false:suggestion
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
dotnet_style_readonly_field = true:suggestion
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
dotnet_style_predefined_type_for_member_access = true:suggestion
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_auto_properties = true:suggestion
dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
dotnet_style_prefer_conditional_expression_over_return = true:suggestion

[*.{json,yml,yaml,md}]
indent_style = space
indent_size = 2

[*.md]
trim_trailing_whitespace = false
73 changes: 73 additions & 0 deletions docs/guides/quality-gates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Quality Gates

PatternKit changes should be validated as production library changes, not sample-only changes. Use these gates before opening or merging work.

## Required local validation

Restore and build the full solution:

```bash
dotnet restore PatternKit.slnx --use-lock-file
dotnet build PatternKit.slnx --configuration Release --no-restore -m:1
```

Run the focused test suite for the area being changed. For broad changes, run the solution test command used by CI:

```bash
dotnet test PatternKit.slnx \
--configuration Release \
-p:TestTfmsInParallel=false \
--collect:"XPlat Code Coverage" \
-- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=cobertura \
-- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Include="[PatternKit*]*" \
-- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Exclude="[*Tests]*" \
-- RunConfiguration.TestSessionTimeout=1800000
```

Build documentation with warnings treated as failures:

```bash
docfx docs/docfx.json --warningsAsErrors
```

Check package currency:

```bash
dotnet list PatternKit.slnx package --outdated
```

The expected exception is `PatternKit.Generators`, which intentionally pins `Microsoft.CodeAnalysis.CSharp` with a project-level `VersionOverride` so the analyzer assembly remains compatible with the SDK compiler that loads it.

## Test expectations

Tests should be executable specifications. Use TinyBDD scenarios and `ScenarioExpect` assertion helpers instead of direct xUnit assertions.

Prefer this structure:

```csharp
[Scenario("A policy rejects invalid registration input")]
[Fact]
public Task Policy_Rejects_Invalid_Registration_Input()
=> Given("invalid registration input", () => new ServiceCollection())
.When("registering the policy", services =>
ScenarioExpect.Throws<ArgumentNullException>(
() => services.AddPatternKitPriorityQueue<WorkItem, int>(null!)))
.Then("the invalid dependency is named", exception =>
ScenarioExpect.Equal("prioritySelector", exception.ParamName))
.AssertPassed();
```

Every production pattern should have:

- Core fluent API coverage.
- Source-generator coverage when a generator route exists.
- Real-world example coverage in `PatternKit.Examples.Tests`.
- Documentation in the API docs, README table, or guide pages.
- Benchmark coverage that separates fluent and generated paths when both exist.
- `IServiceCollection` or host integration coverage when the pattern is naturally used through dependency injection.

## Formatting and static analysis

The repository includes a root `.editorconfig` so editors and `dotnet format` agree on basic C# layout and style. The current source tree still has historical whitespace that makes a full solution `dotnet format --verify-no-changes` fail with a very large formatting-only diff. Treat that as a dedicated cleanup task, not a drive-by change inside feature work.

The same rule applies to analyzer hardening. Built-in .NET analyzers currently surface many intentional API-shape warnings for fluent generic factories, benchmark method names, and netstandard-compatible guard code. Enable stricter analyzers by project and rule family with explicit baselines rather than turning on solution-wide warning enforcement in one step.
2 changes: 2 additions & 0 deletions docs/guides/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@
href: benchmark-results.md
- name: Testing
href: testing.md
- name: Quality Gates
href: quality-gates.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
CS1591: Missing XML comment (examples don't need docs)
CS0649: Field never assigned (used in serialization scenarios)
TBDD010: TinyBDD optimization hints (not errors) -->
<NoWarn>$(NoWarn);CS1591;CS0649;TBDD010</NoWarn>
<NoWarn>$(NoWarn);CS1591;CS0649;CS8602;TBDD010</NoWarn>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<!-- Suppress warnings:
xUnit1031: Some tests intentionally use sync-over-async for generator testing
xUnit2002: NotNull on value types (GeneratedSourceResult is a struct) -->
<NoWarn>$(NoWarn);xUnit1031;xUnit2002</NoWarn>
<NoWarn>$(NoWarn);CS8602;xUnit1031;xUnit2002</NoWarn>
</PropertyGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion test/PatternKit.Tests/PatternKit.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
CS0618: Tests intentionally use obsolete APIs to verify backward compatibility
xUnit1031: Some tests intentionally use sync-over-async for specific testing scenarios
xUnit2002: NotNull on value types (analyzer false positive in some generator tests) -->
<NoWarn>$(NoWarn);TBDD010;CS0618;xUnit1031;xUnit2002</NoWarn>
<NoWarn>$(NoWarn);TBDD010;CS0618;CS8600;CS8602;xUnit1031;xUnit2002</NoWarn>
</PropertyGroup>

<ItemGroup>
Expand Down
20 changes: 10 additions & 10 deletions test/ScenarioExpect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,12 @@ public static void All<T>(IEnumerable<T> collection, Action<T> assertion)
}
}

public static void Contains(string expectedSubstring, string actualString)
public static void Contains(string expectedSubstring, string? actualString)
=> Contains(expectedSubstring, actualString, StringComparison.Ordinal);

public static void Contains(string expectedSubstring, string actualString, StringComparison comparison)
public static void Contains(string expectedSubstring, string? actualString, StringComparison comparison)
{
if (actualString.IndexOf(expectedSubstring, comparison) < 0)
if (actualString is null || actualString.IndexOf(expectedSubstring, comparison) < 0)
Fail($"expected string to contain {Format(expectedSubstring)}, but was {Format(actualString)}");
}

Expand All @@ -131,9 +131,9 @@ public static T Contains<T>(IEnumerable<T> collection, Func<T, bool> predicate)
return default!;
}

public static void DoesNotContain(string expectedSubstring, string actualString)
public static void DoesNotContain(string expectedSubstring, string? actualString)
{
if (actualString.Contains(expectedSubstring, StringComparison.Ordinal))
if (actualString?.Contains(expectedSubstring, StringComparison.Ordinal) == true)
Fail($"expected string to not contain {Format(expectedSubstring)}, but was {Format(actualString)}");
}

Expand All @@ -149,18 +149,18 @@ public static void DoesNotContain<T>(IEnumerable<T> collection, Func<T, bool> pr
Fail("expected collection to not contain a matching item");
}

public static void StartsWith(string expectedStart, string actualString)
public static void StartsWith(string expectedStart, string? actualString)
=> StartsWith(expectedStart, actualString, StringComparison.Ordinal);

public static void StartsWith(string expectedStart, string actualString, StringComparison comparison)
public static void StartsWith(string expectedStart, string? actualString, StringComparison comparison)
{
if (!actualString.StartsWith(expectedStart, comparison))
if (actualString is null || !actualString.StartsWith(expectedStart, comparison))
Fail($"expected string to start with {Format(expectedStart)}, but was {Format(actualString)}");
}

public static void Matches(string expectedRegexPattern, string actualString)
public static void Matches(string expectedRegexPattern, string? actualString)
{
if (!Regex.IsMatch(actualString, expectedRegexPattern))
if (actualString is null || !Regex.IsMatch(actualString, expectedRegexPattern))
Fail($"expected string to match /{expectedRegexPattern}/, but was {Format(actualString)}");
}

Expand Down
Loading