diff --git a/src/Verify.MSTest.SourceGenerator.Tests/RecordTests.HasAttributeOnClassNestedInRecords.verified.txt b/src/Verify.MSTest.SourceGenerator.Tests/RecordTests.HasAttributeOnClassNestedInRecords.verified.txt
new file mode 100644
index 0000000000..8ab3ff7350
--- /dev/null
+++ b/src/Verify.MSTest.SourceGenerator.Tests/RecordTests.HasAttributeOnClassNestedInRecords.verified.txt
@@ -0,0 +1,31 @@
+{
+ Item1: UsesVerify.g.cs,
+ Item2:
+//-----------------------------------------------------
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior
+// and will be lost when the code is regenerated.
+//
+//-----------------------------------------------------
+
+namespace Foo
+{
+partial record Outer
+{
+partial record struct Middle
+{
+partial class Bar
+{
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Verify.MSTest.SourceGenerator", "1.0.0.0")]
+ public global::Microsoft.VisualStudio.TestTools.UnitTesting.TestContext TestContext
+ {
+ get => global::VerifyMSTest.Verifier.CurrentTestContext.Value!.TestContext;
+ set => global::VerifyMSTest.Verifier.CurrentTestContext.Value = new global::VerifyMSTest.TestExecutionContext(value, GetType());
+ }
+}
+}
+}
+}
+
+}
\ No newline at end of file
diff --git a/src/Verify.MSTest.SourceGenerator.Tests/RecordTests.HasAttributeOnRecord.verified.txt b/src/Verify.MSTest.SourceGenerator.Tests/RecordTests.HasAttributeOnRecord.verified.txt
new file mode 100644
index 0000000000..5630303268
--- /dev/null
+++ b/src/Verify.MSTest.SourceGenerator.Tests/RecordTests.HasAttributeOnRecord.verified.txt
@@ -0,0 +1,25 @@
+{
+ Item1: UsesVerify.g.cs,
+ Item2:
+//-----------------------------------------------------
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior
+// and will be lost when the code is regenerated.
+//
+//-----------------------------------------------------
+
+namespace Foo
+{
+partial record Bar
+{
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Verify.MSTest.SourceGenerator", "1.0.0.0")]
+ public global::Microsoft.VisualStudio.TestTools.UnitTesting.TestContext TestContext
+ {
+ get => global::VerifyMSTest.Verifier.CurrentTestContext.Value!.TestContext;
+ set => global::VerifyMSTest.Verifier.CurrentTestContext.Value = new global::VerifyMSTest.TestExecutionContext(value, GetType());
+ }
+}
+}
+
+}
\ No newline at end of file
diff --git a/src/Verify.MSTest.SourceGenerator.Tests/RecordTests.HasAttributeOnRecordClass.verified.txt b/src/Verify.MSTest.SourceGenerator.Tests/RecordTests.HasAttributeOnRecordClass.verified.txt
new file mode 100644
index 0000000000..5630303268
--- /dev/null
+++ b/src/Verify.MSTest.SourceGenerator.Tests/RecordTests.HasAttributeOnRecordClass.verified.txt
@@ -0,0 +1,25 @@
+{
+ Item1: UsesVerify.g.cs,
+ Item2:
+//-----------------------------------------------------
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior
+// and will be lost when the code is regenerated.
+//
+//-----------------------------------------------------
+
+namespace Foo
+{
+partial record Bar
+{
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Verify.MSTest.SourceGenerator", "1.0.0.0")]
+ public global::Microsoft.VisualStudio.TestTools.UnitTesting.TestContext TestContext
+ {
+ get => global::VerifyMSTest.Verifier.CurrentTestContext.Value!.TestContext;
+ set => global::VerifyMSTest.Verifier.CurrentTestContext.Value = new global::VerifyMSTest.TestExecutionContext(value, GetType());
+ }
+}
+}
+
+}
\ No newline at end of file
diff --git a/src/Verify.MSTest.SourceGenerator.Tests/RecordTests.HasAttributeOnRecordStruct.verified.txt b/src/Verify.MSTest.SourceGenerator.Tests/RecordTests.HasAttributeOnRecordStruct.verified.txt
new file mode 100644
index 0000000000..22fdca1b26
--- /dev/null
+++ b/src/Verify.MSTest.SourceGenerator.Tests/RecordTests.HasAttributeOnRecordStruct.verified.txt
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/src/Verify.MSTest.SourceGenerator.Tests/RecordTests.cs b/src/Verify.MSTest.SourceGenerator.Tests/RecordTests.cs
new file mode 100644
index 0000000000..b2f93280ea
--- /dev/null
+++ b/src/Verify.MSTest.SourceGenerator.Tests/RecordTests.cs
@@ -0,0 +1,83 @@
+// A record is a class, so it is a legal MSTest test class
+[TestClass]
+public partial class RecordTests : TestBase
+{
+ [TestMethod]
+ public Task HasAttributeOnRecord()
+ {
+ var source = """
+ using VerifyMSTest;
+
+ namespace Foo;
+
+ [UsesVerify]
+ public partial record Bar
+ {
+ }
+ """;
+
+ return VerifyGenerator(TestDriver.Run(source));
+ }
+
+ [TestMethod]
+ public Task HasAttributeOnRecordClass()
+ {
+ var source = """
+ using VerifyMSTest;
+
+ namespace Foo;
+
+ [UsesVerify]
+ public partial record class Bar
+ {
+ }
+ """;
+
+ return VerifyGenerator(TestDriver.Run(source));
+ }
+
+ // A record struct is not a class, and [UsesVerify] is only valid on a class,
+ // so nothing is generated for one
+ [TestMethod]
+ public Task HasAttributeOnRecordStruct()
+ {
+ var source = """
+ using VerifyMSTest;
+
+ namespace Foo;
+
+ [UsesVerify]
+ public partial record struct Bar
+ {
+ }
+ """;
+
+ return VerifyGenerator(
+ TestDriver.Run(source),
+ expectedDiagnostics: ["CS0592"]);
+ }
+
+ // The partial declaration has to repeat the kind of every parent too
+ [TestMethod]
+ public Task HasAttributeOnClassNestedInRecords()
+ {
+ var source = """
+ using VerifyMSTest;
+
+ namespace Foo;
+
+ public partial record Outer
+ {
+ public partial record struct Middle
+ {
+ [UsesVerify]
+ public partial class Bar
+ {
+ }
+ }
+ }
+ """;
+
+ return VerifyGenerator(TestDriver.Run(source));
+ }
+}
diff --git a/src/Verify.MSTest.SourceGenerator/ClassToGenerate.cs b/src/Verify.MSTest.SourceGenerator/ClassToGenerate.cs
index 3271ef3c7c..2540459992 100644
--- a/src/Verify.MSTest.SourceGenerator/ClassToGenerate.cs
+++ b/src/Verify.MSTest.SourceGenerator/ClassToGenerate.cs
@@ -11,6 +11,7 @@
readonly record struct ClassToGenerate(
string? Namespace,
string ClassName,
+ string Keyword,
ClassToGenerate.PropertyFlags TestContextPropertyFlags,
ParentClass[] ParentClasses)
{
@@ -24,12 +25,14 @@ public enum PropertyFlags
public string? Namespace { get; } = Namespace;
public string ClassName { get; } = ClassName;
+ public string Keyword { get; } = Keyword;
public ParentClass[] ParentClasses { get; } = ParentClasses;
public PropertyFlags TestContextPropertyFlags { get; } = TestContextPropertyFlags;
public bool Equals(ClassToGenerate other) =>
Namespace == other.Namespace &&
ClassName == other.ClassName &&
+ Keyword == other.Keyword &&
TestContextPropertyFlags == other.TestContextPropertyFlags &&
ParentClasses.SequenceEqual(other.ParentClasses);
@@ -41,6 +44,7 @@ public override int GetHashCode()
var hash = 1430287;
hash = hash * 7302013 ^ (Namespace ?? string.Empty).GetHashCode();
hash = hash * 7302013 ^ ClassName.GetHashCode();
+ hash = hash * 7302013 ^ Keyword.GetHashCode();
hash = hash * 7302013 ^ TestContextPropertyFlags.GetHashCode();
// Include (up to) the last 8 elements in the hash code to balance performance and specificity.
diff --git a/src/Verify.MSTest.SourceGenerator/Emitter.cs b/src/Verify.MSTest.SourceGenerator/Emitter.cs
index 671221c4ca..02b263f23b 100644
--- a/src/Verify.MSTest.SourceGenerator/Emitter.cs
+++ b/src/Verify.MSTest.SourceGenerator/Emitter.cs
@@ -62,7 +62,7 @@ void WriteClass(ClassToGenerate toGenerate)
{
builder.AppendLine(
$$"""
- partial class {{toGenerate.ClassName}}
+ partial {{toGenerate.Keyword}} {{toGenerate.ClassName}}
{
""");
AppendGetter(toGenerate.TestContextPropertyFlags);
diff --git a/src/Verify.MSTest.SourceGenerator/Extensions.cs b/src/Verify.MSTest.SourceGenerator/Extensions.cs
index 6b54e56522..622f08d7e4 100644
--- a/src/Verify.MSTest.SourceGenerator/Extensions.cs
+++ b/src/Verify.MSTest.SourceGenerator/Extensions.cs
@@ -3,6 +3,21 @@ static class Extensions
public static string GetTypeNameWithGenericParameters(this TypeDeclarationSyntax syntax) =>
syntax.Identifier.ToString() + syntax.TypeParameterList;
+ ///
+ /// The keyword to redeclare the type with. A partial declaration has to repeat the kind,
+ /// and for a record struct that kind is two tokens: `Keyword` is only the `record` half.
+ ///
+ public static string GetPartialKeyword(this TypeDeclarationSyntax syntax)
+ {
+ if (syntax is RecordDeclarationSyntax record &&
+ record.ClassOrStructKeyword.IsKind(SyntaxKind.StructKeyword))
+ {
+ return "record struct";
+ }
+
+ return syntax.Keyword.ValueText;
+ }
+
public static IEnumerable GetBaseTypes(this ITypeSymbol symbol)
{
var baseType = symbol.BaseType;
diff --git a/src/Verify.MSTest.SourceGenerator/Parser.cs b/src/Verify.MSTest.SourceGenerator/Parser.cs
index c0fdc6a3f6..143b4394ba 100644
--- a/src/Verify.MSTest.SourceGenerator/Parser.cs
+++ b/src/Verify.MSTest.SourceGenerator/Parser.cs
@@ -15,6 +15,7 @@ public static ClassToGenerate Parse(INamedTypeSymbol symbol, TypeDeclarationSynt
return new(
Namespace: ns,
ClassName: name,
+ Keyword: syntax.GetPartialKeyword(),
TestContextPropertyFlags: propertyFlags,
ParentClasses: parents);
}
@@ -54,12 +55,13 @@ static IEnumerable BaseClassesOf(INamedTypeSymbol typeSymbol)
static ParentClass[] GetParentClasses(TypeDeclarationSyntax syntax, Cancel cancel)
{
- // We can only be nested in class/struct/record
+ // We can only be nested in class/struct/record/record struct
static bool IsAllowedKind(SyntaxKind kind) =>
kind is
SyntaxKind.ClassDeclaration or
SyntaxKind.StructDeclaration or
- SyntaxKind.RecordDeclaration;
+ SyntaxKind.RecordDeclaration or
+ SyntaxKind.RecordStructDeclaration;
var parents = new Stack();
@@ -71,7 +73,7 @@ SyntaxKind.StructDeclaration or
cancel.ThrowIfCancellationRequested();
parents.Push(new(
- Keyword: parent.Keyword.ValueText,
+ Keyword: parent.GetPartialKeyword(),
Name: parent.GetTypeNameWithGenericParameters()));
parent = parent.Parent as TypeDeclarationSyntax;
diff --git a/src/Verify.MSTest.SourceGenerator/UsesVerifyGenerator.cs b/src/Verify.MSTest.SourceGenerator/UsesVerifyGenerator.cs
index 6257bac837..fcdfc1bd90 100644
--- a/src/Verify.MSTest.SourceGenerator/UsesVerifyGenerator.cs
+++ b/src/Verify.MSTest.SourceGenerator/UsesVerifyGenerator.cs
@@ -118,12 +118,20 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
static bool HasTestClassAttribute(INamedTypeSymbol symbol, INamedTypeSymbol testClassType) =>
!symbol.HasAttributeOfType(testClassType, includeDerived: true);
- // Require at least one attribute list: both paths only care about classes
+ // Require at least one attribute list: both paths only care about types
// carrying [UsesVerify] or [TestClass]. Without this filter the uncached
- // CreateSyntaxProvider transform runs semantic work for every class in the
+ // CreateSyntaxProvider transform runs semantic work for every type in the
// consuming project on every keystroke.
+ // Records are eligible too: a record class is a class, so it is a legal test class.
+ // A record struct is not, and [UsesVerify] does not permit one.
static bool IsSyntaxEligibleForGeneration(SyntaxNode node, Cancel _) =>
- node is ClassDeclarationSyntax { AttributeLists.Count: > 0 };
+ node switch
+ {
+ ClassDeclarationSyntax syntax => syntax.AttributeLists.Count > 0,
+ RecordDeclarationSyntax syntax => syntax.AttributeLists.Count > 0 &&
+ !syntax.ClassOrStructKeyword.IsKind(SyntaxKind.StructKeyword),
+ _ => false
+ };
static bool IsAssemblyEligibleForGeneration(IAssemblySymbol assembly, INamedTypeSymbol markerType) =>
assembly.HasAttributeOfType(markerType, includeDerived: false);
diff --git a/src/Verify.MSTest.Tests/RecordTests.ShouldPass.verified.txt b/src/Verify.MSTest.Tests/RecordTests.ShouldPass.verified.txt
new file mode 100644
index 0000000000..329c499a35
--- /dev/null
+++ b/src/Verify.MSTest.Tests/RecordTests.ShouldPass.verified.txt
@@ -0,0 +1 @@
+RecordValue
\ No newline at end of file
diff --git a/src/Verify.MSTest.Tests/RecordTests.cs b/src/Verify.MSTest.Tests/RecordTests.cs
new file mode 100644
index 0000000000..47b17473eb
--- /dev/null
+++ b/src/Verify.MSTest.Tests/RecordTests.cs
@@ -0,0 +1,9 @@
+// A record is a class, so it is a legal test class. Without generator support the
+// TestContext property is never generated and this fails with "TestContext is null".
+[TestClass]
+public partial record RecordTests
+{
+ [TestMethod]
+ public Task ShouldPass() =>
+ Verify("RecordValue");
+}
diff --git a/src/todo.md b/src/todo.md
index 03784f69b0..ab779a7bed 100644
--- a/src/todo.md
+++ b/src/todo.md
@@ -64,7 +64,7 @@ All six resolved 2026-08-16 (five fixed here; the inline item resolved as by-des
- [ ] **Stack-trace scrubber destroys paren-less `at` frames** (NativeAOT: `at MyApp!+0x1a2b3c`).
`Verify/Serialization/Scrubbers/ScrubStackTrace.cs:36-58` — `IndexOf('(')`/`')'` return −1, slice keeps zero chars → frame becomes an empty line, or the literal `...)` with `removeParams: true`.
-- [ ] **MSTest source generator ignores `record` test classes.**
+- [x] **MSTest source generator ignores `record` test classes.**
`Verify.MSTest.SourceGenerator/UsesVerifyGenerator.cs:125-126` — only `ClassDeclarationSyntax` is eligible; `[UsesVerify] [TestClass] partial record` compiles then fails at runtime with the misleading "TestContext is null" error. `Parser.GetParentClasses` similarly stops at a `record struct` parent.
- [ ] **Unclosed JSON object for empty `CombinationResults`.**