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
4 changes: 4 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,9 @@
*.fsproj
*.dbproj

# Verify snapshot files
*.verified.txt text eol=lf working-tree-encoding=UTF-8
*.verified.cs text eol=lf working-tree-encoding=UTF-8

# Don't check for trailing whitespace at end of lines in the doc pages
*.md -whitespace=blank-at-eol
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
# SourceGenerator Launch Settings
src/CompositeKey.SourceGeneration/Properties/launchSettings.json

# Verify snapshot testing
*.received.*

# Claude Code
.claude/settings.local.json
.claude/todos/
10 changes: 9 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ The solution is split into four main projects plus corresponding test projects:

- Commit messages follow Conventional Commits (`feat:`, `fix:`, `perf:`, etc.) — GitVersion and `.versionrc` drive versioning and changelog
- EditorConfig: 4-space indent, UTF-8, LF line endings, 120-char max line length
- Test stack: xunit + Shouldly + AutoFixture
- Test stack: xunit + Shouldly + AutoFixture + Verify (snapshot testing)
- Unit tests use `CompilationHelper` to create in-memory Roslyn compilations
- Functional tests define real key types and test format/parse round-trips
- Snapshot tests (`Snapshots/` in `SourceGeneration.UnitTests`) use
Verify.SourceGenerators to capture generated `.g.cs` output and
`GenerationSpec` models as `.verified.*` baselines — any change to
emitted code or parser output will cause a snapshot diff failure
- `*.received.*` files are gitignored; only `*.verified.*` baselines
are committed
- The `ModuleInitializer` scrubs the non-deterministic
`GeneratedCodeAttribute` version string to keep snapshots stable
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis" />
<PackageReference Include="Verify.SourceGenerators" />
<PackageReference Include="Verify.Xunit" />
</ItemGroup>

<ItemGroup>
Expand Down
24 changes: 24 additions & 0 deletions src/CompositeKey.SourceGeneration.UnitTests/ModuleInitializer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using VerifyXunit;

namespace CompositeKey.SourceGeneration.UnitTests;

public static class ModuleInitializer
{
[ModuleInitializer]
public static void Init()
{
VerifySourceGenerators.Initialize();

// Scrub the non-deterministic informational version (includes git commit hash)
// from the GeneratedCodeAttribute emitted by the source generator.
// e.g. [GeneratedCodeAttribute("CompositeKey.SourceGeneration", "1.6.0+7675481fa523...")]
// -> [GeneratedCodeAttribute("CompositeKey.SourceGeneration", "VERSION")]
VerifierSettings.ScrubLinesWithReplace(line =>
Regex.Replace(
line,
@"GeneratedCodeAttribute\(""CompositeKey\.SourceGeneration"", ""[^""]*""\)",
@"GeneratedCodeAttribute(""CompositeKey.SourceGeneration"", ""VERSION"")"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
[
{
TargetType: {
Type: {
Name: BasicPrimaryKey,
FullyQualifiedName: global::UnitTests.BasicPrimaryKey
},
Namespace: UnitTests,
TypeDeclarations: [
public partial record BasicPrimaryKey
],
Properties: [
{
Type: {
Name: Guid,
FullyQualifiedName: global::System.Guid
},
Name: FirstPart,
CamelCaseName: firstPart,
IsRequired: false,
HasGetter: true,
HasSetter: true,
IsInitOnlySetter: true
},
{
Type: {
Name: String,
FullyQualifiedName: string
},
Name: SecondPart,
CamelCaseName: secondPart,
IsRequired: false,
HasGetter: true,
HasSetter: true,
IsInitOnlySetter: true
},
{
Type: {
Name: CustomEnum,
FullyQualifiedName: global::UnitTests.CustomEnum
},
Name: ThirdPart,
CamelCaseName: thirdPart,
IsRequired: false,
HasGetter: true,
HasSetter: true,
IsInitOnlySetter: true,
EnumSpec: {
Name: CustomEnum,
FullyQualifiedName: global::UnitTests.CustomEnum,
UnderlyingType: int,
Members: [
{
Name: One,
Value: 0
},
{
Name: Two,
Value: 1
},
{
Name: Three,
Value: 2
}
],
IsSequentialFromZero: true
}
}
],
ConstructorParameters: [
{
Type: {
Name: Guid,
FullyQualifiedName: global::System.Guid
},
Name: FirstPart,
CamelCaseName: firstPart
},
{
Type: {
Name: String,
FullyQualifiedName: string
},
Name: SecondPart,
CamelCaseName: secondPart,
ParameterIndex: 1
},
{
Type: {
Name: CustomEnum,
FullyQualifiedName: global::UnitTests.CustomEnum
},
Name: ThirdPart,
CamelCaseName: thirdPart,
ParameterIndex: 2
}
],
TypeName: BasicPrimaryKey
},
Key: {
AllParts: [
{
Property: {
Type: {
Name: Guid,
FullyQualifiedName: global::System.Guid
},
Name: FirstPart,
CamelCaseName: firstPart,
IsRequired: false,
HasGetter: true,
HasSetter: true,
IsInitOnlySetter: true
},
Format: b,
LengthRequired: 38,
ExactLengthRequirement: true
},
{
Value: #,
LengthRequired: 1,
ExactLengthRequirement: true
},
{
Property: {
Type: {
Name: String,
FullyQualifiedName: string
},
Name: SecondPart,
CamelCaseName: secondPart,
IsRequired: false,
HasGetter: true,
HasSetter: true,
IsInitOnlySetter: true
},
ParseType: String,
FormatType: String,
LengthRequired: 1,
ExactLengthRequirement: false
},
{
Value: |,
LengthRequired: 1,
ExactLengthRequirement: true
},
{
Value: ConstantValue,
LengthRequired: 13,
ExactLengthRequirement: true
},
{
Value: #,
LengthRequired: 1,
ExactLengthRequirement: true
},
{
Property: {
Type: {
Name: CustomEnum,
FullyQualifiedName: global::UnitTests.CustomEnum
},
Name: ThirdPart,
CamelCaseName: thirdPart,
IsRequired: false,
HasGetter: true,
HasSetter: true,
IsInitOnlySetter: true,
EnumSpec: {
Name: CustomEnum,
FullyQualifiedName: global::UnitTests.CustomEnum,
UnderlyingType: int,
Members: [
{
Name: One,
Value: 0
},
{
Name: Two,
Value: 1
},
{
Name: Three,
Value: 2
}
],
IsSequentialFromZero: true
}
},
Format: g,
ParseType: Enum,
FormatType: Enum,
LengthRequired: 1,
ExactLengthRequirement: false
}
],
PartitionKeyParts: [
{
Property: {
Type: {
Name: Guid,
FullyQualifiedName: global::System.Guid
},
Name: FirstPart,
CamelCaseName: firstPart,
IsRequired: false,
HasGetter: true,
HasSetter: true,
IsInitOnlySetter: true
},
Format: b,
LengthRequired: 38,
ExactLengthRequirement: true
},
{
Value: #,
LengthRequired: 1,
ExactLengthRequirement: true
},
{
Property: {
Type: {
Name: String,
FullyQualifiedName: string
},
Name: SecondPart,
CamelCaseName: secondPart,
IsRequired: false,
HasGetter: true,
HasSetter: true,
IsInitOnlySetter: true
},
ParseType: String,
FormatType: String,
LengthRequired: 1,
ExactLengthRequirement: false
}
],
PrimaryDelimiterKeyPart: {
Value: |,
LengthRequired: 1,
ExactLengthRequirement: true
},
SortKeyParts: [
{
Value: ConstantValue,
LengthRequired: 13,
ExactLengthRequirement: true
},
{
Value: #,
LengthRequired: 1,
ExactLengthRequirement: true
},
{
Property: {
Type: {
Name: CustomEnum,
FullyQualifiedName: global::UnitTests.CustomEnum
},
Name: ThirdPart,
CamelCaseName: thirdPart,
IsRequired: false,
HasGetter: true,
HasSetter: true,
IsInitOnlySetter: true,
EnumSpec: {
Name: CustomEnum,
FullyQualifiedName: global::UnitTests.CustomEnum,
UnderlyingType: int,
Members: [
{
Name: One,
Value: 0
},
{
Name: Two,
Value: 1
},
{
Name: Three,
Value: 2
}
],
IsSequentialFromZero: true
}
},
Format: g,
ParseType: Enum,
FormatType: Enum,
LengthRequired: 1,
ExactLengthRequirement: false
}
],
InvariantFormatting: true
}
}
]
Loading
Loading