Skip to content

Commit a3f15a7

Browse files
committed
xunit/xunit#3361: Support serialization of Tuple and ValueTuple used in theory data
1 parent 68a4326 commit a3f15a7

File tree

9 files changed

+177
-48
lines changed

9 files changed

+177
-48
lines changed

src/Directory.Build.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@
7979
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
8080
<PackageReference Include="NSubstitute" Version="5.3.0" />
8181
<PackageReference Include="System.ValueTuple" Version="4.6.1" />
82-
<PackageReference Include="xunit.v3.assert.source" Version="3.0.1-pre.12" />
83-
<PackageReference Include="xunit.v3.core" Version="3.0.1-pre.12" />
82+
<PackageReference Include="xunit.v3.assert.source" Version="3.0.1-pre.19" />
83+
<PackageReference Include="xunit.v3.core" Version="3.0.1-pre.19" />
8484
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4-pre.10" />
8585
</ItemGroup>
8686

src/xunit.analyzers.tests/Analyzers/X1000/TheoryDataRowArgumentsShouldBeSerializableTests.cs

Lines changed: 90 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
using System;
12
using System.Threading.Tasks;
3+
using Microsoft.CodeAnalysis;
24
using Microsoft.CodeAnalysis.CSharp;
35
using Xunit;
6+
using Xunit.Analyzers;
47
using Verify = CSharpVerifier<Xunit.Analyzers.TheoryDataRowArgumentsShouldBeSerializable>;
8+
using Verify_v3_Pre301 = CSharpVerifier<TheoryDataRowArgumentsShouldBeSerializableTests.Analyzer_v3_Pre301>;
59

610
public sealed class TheoryDataRowArgumentsShouldBeSerializableTests
711
{
@@ -138,7 +142,7 @@ public async Task IXunitSerializerValue_DoesNotTrigger(string type)
138142
using Xunit.Sdk;
139143
140144
[assembly: RegisterXunitSerializer(typeof(CustomSerializer), typeof(ICustomSerialized))]
141-
145+
142146
public class MyClass {{
143147
public IEnumerable<TheoryDataRowBase> MyMethod() {{
144148
var value = new {0}();
@@ -152,21 +156,21 @@ public IEnumerable<TheoryDataRowBase> MyMethod() {{
152156
}}
153157
154158
public interface ICustomSerialized {{ }}
155-
159+
156160
public class CustomSerialized : ICustomSerialized {{ }}
157-
161+
158162
public class CustomSerializedDerived : CustomSerialized {{ }}
159-
163+
160164
public class CustomSerializer : IXunitSerializer {{
161165
public object Deserialize(Type type, string serializedValue) =>
162166
throw new NotImplementedException();
163-
167+
164168
public bool IsSerializable(Type type, object? value, out string? failureReason)
165169
{{
166170
failureReason = null;
167171
return true;
168172
}}
169-
173+
170174
public string Serialize(object value) =>
171175
throw new NotImplementedException();
172176
}}
@@ -301,6 +305,41 @@ public class PossiblySerializableUnsealedClass {{ }}
301305
await Verify.VerifyAnalyzerV3(LanguageVersion.CSharp8, source, expected);
302306
}
303307

308+
[Theory]
309+
[InlineData("object")]
310+
[InlineData("Dictionary<int, string>")]
311+
[InlineData("PossiblySerializableUnsealedClass")]
312+
public async Task MaybeNonSerializableValue_Constructable_Triggers1047(string type)
313+
{
314+
var source = string.Format(/* lang=c#-test */ """
315+
#nullable enable
316+
317+
using System.Collections.Generic;
318+
using Xunit;
319+
320+
public class MyClass {{
321+
public IEnumerable<TheoryDataRowBase> MyMethod() {{
322+
var value = new {0}();
323+
324+
yield return new TheoryDataRow({{|#0:value|}});
325+
yield return new TheoryDataRow({{|#1:new {0}()|}});
326+
yield return new TheoryDataRow<{0}>({{|#2:value|}});
327+
yield return new TheoryDataRow<{0}>({{|#3:new {0}()|}});
328+
}}
329+
}}
330+
331+
public class PossiblySerializableUnsealedClass {{ }}
332+
""", type);
333+
var expected = new[] {
334+
Verify.Diagnostic("xUnit1047").WithLocation(0).WithArguments("value", type),
335+
Verify.Diagnostic("xUnit1047").WithLocation(1).WithArguments($"new {type}()", type),
336+
Verify.Diagnostic("xUnit1047").WithLocation(2).WithArguments("value", type),
337+
Verify.Diagnostic("xUnit1047").WithLocation(3).WithArguments($"new {type}()", type),
338+
};
339+
340+
await Verify.VerifyAnalyzerV3(LanguageVersion.CSharp8, source, expected);
341+
}
342+
304343
[Fact]
305344
public async Task IFormattableAndIParseable_DoesNotTrigger()
306345
{
@@ -343,7 +382,7 @@ public IEnumerable<TheoryDataRowBase> MyMethod() {
343382
var defaultValue = default(Formattable);
344383
var nullValue = default(Formattable?);
345384
var arrayValue = new Formattable[0];
346-
385+
347386
yield return new TheoryDataRow({|#0:defaultValue|}, {|#1:nullValue|}, {|#2:arrayValue|});
348387
yield return new TheoryDataRow<Formattable, Formattable, Formattable[]>({|#3:default(Formattable)|}, {|#4:default(Formattable?)|}, {|#5:new Formattable[0]|});
349388
}
@@ -354,7 +393,7 @@ public IEnumerable<TheoryDataRowBase> MyMethod() {
354393
var defaultValue = default(Parsable);
355394
var nullValue = default(Parsable?);
356395
var arrayValue = new Parsable[0];
357-
396+
358397
yield return new TheoryDataRow({|#10:defaultValue|}, {|#11:nullValue|}, {|#12:arrayValue|});
359398
yield return new TheoryDataRow<Parsable, Parsable, Parsable[]>({|#13:default(Parsable)|}, {|#14:default(Parsable?)|}, {|#15:new Parsable[0]|});
360399
}
@@ -365,7 +404,7 @@ public IEnumerable<TheoryDataRowBase> MyMethod() {
365404
var defaultValue = default(FormattableAndParsable);
366405
var nullValue = default(FormattableAndParsable?);
367406
var arrayValue = new FormattableAndParsable[0];
368-
407+
369408
yield return new TheoryDataRow(defaultValue, nullValue, arrayValue);
370409
yield return new TheoryDataRow<FormattableAndParsable, FormattableAndParsable, FormattableAndParsable[]>(default(FormattableAndParsable), default(FormattableAndParsable?), new FormattableAndParsable[0]);
371410
}
@@ -397,39 +436,57 @@ public IEnumerable<TheoryDataRowBase> MyMethod() {
397436
#endif
398437
}
399438

400-
401-
[Theory]
402-
[InlineData("object")]
403-
[InlineData("Dictionary<int, string>")]
404-
[InlineData("PossiblySerializableUnsealedClass")]
405-
public async Task MaybeNonSerializableValue_Constructable_Triggers1047(string type)
439+
[Fact]
440+
public async Task Tuples_OnlySupportedByV3_3_0_1()
406441
{
407-
var source = string.Format(/* lang=c#-test */ """
442+
var source = /* lang=c#-test */ """
408443
#nullable enable
409444
445+
using System;
410446
using System.Collections.Generic;
411447
using Xunit;
412448
413-
public class MyClass {{
414-
public IEnumerable<TheoryDataRowBase> MyMethod() {{
415-
var value = new {0}();
449+
public class MyClass {
450+
public IEnumerable<TheoryDataRowBase> MyTupleMethod() {
451+
var value = Tuple.Create("Hello world", 42);
416452
417-
yield return new TheoryDataRow({{|#0:value|}});
418-
yield return new TheoryDataRow({{|#1:new {0}()|}});
419-
yield return new TheoryDataRow<{0}>({{|#2:value|}});
420-
yield return new TheoryDataRow<{0}>({{|#3:new {0}()|}});
421-
}}
422-
}}
453+
yield return new TheoryDataRow({|#0:value|});
454+
yield return new TheoryDataRow({|#1:Tuple.Create("Hello world", 42)|});
455+
yield return new TheoryDataRow<Tuple<string, int>>({|#2:value|});
456+
yield return new TheoryDataRow<Tuple<string, int>>({|#3:Tuple.Create("Hello world", 42)|});
457+
}
423458
424-
public class PossiblySerializableUnsealedClass {{ }}
425-
""", type);
426-
var expected = new[] {
427-
Verify.Diagnostic("xUnit1047").WithLocation(0).WithArguments("value", type),
428-
Verify.Diagnostic("xUnit1047").WithLocation(1).WithArguments($"new {type}()", type),
429-
Verify.Diagnostic("xUnit1047").WithLocation(2).WithArguments("value", type),
430-
Verify.Diagnostic("xUnit1047").WithLocation(3).WithArguments($"new {type}()", type),
459+
public IEnumerable<TheoryDataRowBase> MyValueTupleMethod() {
460+
var value = ValueTuple.Create("Hello world", 42);
461+
462+
yield return new TheoryDataRow({|#10:value|});
463+
yield return new TheoryDataRow({|#11:ValueTuple.Create("Hello world", 42)|});
464+
yield return new TheoryDataRow<ValueTuple<string, int>>({|#12:value|});
465+
yield return new TheoryDataRow<ValueTuple<string, int>>({|#13:ValueTuple.Create("Hello world", 42)|});
466+
}
467+
}
468+
469+
public class PossiblySerializableUnsealedClass { }
470+
""";
471+
var expectedUnsupported = new[] {
472+
Verify.Diagnostic("xUnit1047").WithLocation(0).WithArguments("value", "Tuple<string, int>"),
473+
Verify.Diagnostic("xUnit1047").WithLocation(1).WithArguments("Tuple.Create(\"Hello world\", 42)", "Tuple<string, int>"),
474+
Verify.Diagnostic("xUnit1047").WithLocation(2).WithArguments("value", "Tuple<string, int>"),
475+
Verify.Diagnostic("xUnit1047").WithLocation(3).WithArguments("Tuple.Create(\"Hello world\", 42)", "Tuple<string, int>"),
476+
477+
Verify.Diagnostic("xUnit1046").WithLocation(10).WithArguments("value", "(string, int)"),
478+
Verify.Diagnostic("xUnit1046").WithLocation(11).WithArguments("ValueTuple.Create(\"Hello world\", 42)", "(string, int)"),
479+
Verify.Diagnostic("xUnit1046").WithLocation(12).WithArguments("value", "(string, int)"),
480+
Verify.Diagnostic("xUnit1046").WithLocation(13).WithArguments("ValueTuple.Create(\"Hello world\", 42)", "(string, int)"),
431481
};
432482

433-
await Verify.VerifyAnalyzerV3(LanguageVersion.CSharp8, source, expected);
483+
await Verify_v3_Pre301.VerifyAnalyzerV3(LanguageVersion.CSharp8, source, expectedUnsupported);
484+
await Verify.VerifyAnalyzerV3(LanguageVersion.CSharp8, source);
485+
}
486+
487+
internal class Analyzer_v3_Pre301 : TheoryDataRowArgumentsShouldBeSerializable
488+
{
489+
protected override XunitContext CreateXunitContext(Compilation compilation) =>
490+
XunitContext.ForV3(compilation, new Version(3, 0, 0));
434491
}
435492
}

src/xunit.analyzers.tests/Analyzers/X1000/TheoryDataTypeArgumentsShouldBeSerializableTests.cs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
using System;
12
using System.Threading.Tasks;
3+
using Microsoft.CodeAnalysis;
24
using Microsoft.CodeAnalysis.CSharp;
35
using Xunit;
6+
using Xunit.Analyzers;
47
using Verify = CSharpVerifier<Xunit.Analyzers.TheoryDataTypeArgumentsShouldBeSerializable>;
8+
using Verify_v3_Pre301 = CSharpVerifier<TheoryDataTypeArgumentsShouldBeSerializableTests.Analyzer_v3_Pre301>;
59

610
public class TheoryDataTypeArgumentsShouldBeSerializableTests
711
{
@@ -493,6 +497,33 @@ public async Task GivenTheory_WithNonSerializableTheoryDataClass_Triggers(
493497

494498
await Verify.VerifyAnalyzer(source, expected);
495499
}
500+
501+
[Theory]
502+
[MemberData(nameof(TheoryDataMembers), "ValueTuple<string, int>")]
503+
[MemberData(nameof(TheoryDataMembers), "ValueTuple<string, int>?")]
504+
public async Task GivenTheory_WithValueTuple_OnlySupportedInV3_3_0_1(
505+
string member,
506+
string attribute,
507+
string type)
508+
{
509+
var source = string.Format(/* lang=c#-test */ """
510+
using System;
511+
using Xunit;
512+
513+
public class TestClass {{
514+
{0}
515+
516+
[Theory]
517+
[{{|#0:{1}|}}]
518+
public void TestMethod({2} parameter) {{ }}
519+
}}
520+
""", member, attribute, type);
521+
var expectedUnsupported = Verify.Diagnostic("xUnit1044").WithLocation(0).WithArguments(type.Replace("ValueTuple<string, int>", "(string, int)"));
522+
523+
await Verify.VerifyAnalyzerV2(LanguageVersion.CSharp9, source, expectedUnsupported);
524+
await Verify_v3_Pre301.VerifyAnalyzerV3(LanguageVersion.CSharp9, source, expectedUnsupported);
525+
await Verify.VerifyAnalyzerV3(LanguageVersion.CSharp9, source);
526+
}
496527
}
497528

498529
public sealed class X1045_AvoidUsingTheoryDataTypeArgumentsThatMightNotBeSerializable : TheoryDataTypeArgumentsShouldBeSerializableTests
@@ -592,6 +623,32 @@ public void TestMethod(object parameter) { }
592623
#endif
593624
}
594625

626+
[Theory]
627+
[MemberData(nameof(TheoryDataMembers), "Tuple<string, int>")]
628+
[MemberData(nameof(TheoryDataMembers), "Tuple<string, int>?")]
629+
public async Task GivenTheory_WithTuple_OnlySupportedInV3_3_0_1(
630+
string member,
631+
string attribute,
632+
string type)
633+
{
634+
var source = string.Format(/* lang=c#-test */ """
635+
using System;
636+
using Xunit;
637+
638+
public class TestClass {{
639+
{0}
640+
641+
[Theory]
642+
[{{|#0:{1}|}}]
643+
public void TestMethod({2} parameter) {{ }}
644+
}}
645+
""", member, attribute, type);
646+
var expectedUnsupported = Verify.Diagnostic("xUnit1045").WithLocation(0).WithArguments(type);
647+
648+
await Verify.VerifyAnalyzerV2(LanguageVersion.CSharp9, source, expectedUnsupported);
649+
await Verify_v3_Pre301.VerifyAnalyzerV3(LanguageVersion.CSharp9, source, expectedUnsupported);
650+
await Verify.VerifyAnalyzerV3(LanguageVersion.CSharp9, source);
651+
}
595652

596653
[Theory]
597654
[MemberData(nameof(TheoryDataMembers), "object")]
@@ -655,4 +712,10 @@ public async Task GivenTheory_WithPossiblySerializableTheoryDataClass_Triggers(
655712
await Verify.VerifyAnalyzer(source, expected);
656713
}
657714
}
715+
716+
internal class Analyzer_v3_Pre301 : TheoryDataTypeArgumentsShouldBeSerializable
717+
{
718+
protected override XunitContext CreateXunitContext(Compilation compilation) =>
719+
XunitContext.ForV3(compilation, new Version(3, 0, 0));
720+
}
658721
}

src/xunit.analyzers.tests/Utility/CodeAnalyzerHelper.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,10 @@ static CodeAnalyzerHelper()
6161
new PackageIdentity("Microsoft.Extensions.Primitives", "8.0.0"),
6262
new PackageIdentity("System.Threading.Tasks.Extensions", "4.5.4"),
6363
new PackageIdentity("System.Text.Json", "8.0.0"),
64-
new PackageIdentity("xunit.v3.assert", "3.0.1-pre.12"),
65-
new PackageIdentity("xunit.v3.common", "3.0.1-pre.12"),
66-
new PackageIdentity("xunit.v3.extensibility.core", "3.0.1-pre.12"),
67-
new PackageIdentity("xunit.v3.runner.common", "3.0.1-pre.12")
64+
new PackageIdentity("xunit.v3.assert", "3.0.1-pre.19"),
65+
new PackageIdentity("xunit.v3.common", "3.0.1-pre.19"),
66+
new PackageIdentity("xunit.v3.extensibility.core", "3.0.1-pre.19"),
67+
new PackageIdentity("xunit.v3.runner.common", "3.0.1-pre.19")
6868
)
6969
);
7070

@@ -74,8 +74,8 @@ static CodeAnalyzerHelper()
7474
new PackageIdentity("Microsoft.Extensions.Primitives", "8.0.0"),
7575
new PackageIdentity("System.Threading.Tasks.Extensions", "4.5.4"),
7676
new PackageIdentity("System.Text.Json", "8.0.0"),
77-
new PackageIdentity("xunit.v3.common", "3.0.1-pre.12"),
78-
new PackageIdentity("xunit.v3.runner.utility", "3.0.1-pre.12")
77+
new PackageIdentity("xunit.v3.common", "3.0.1-pre.19"),
78+
new PackageIdentity("xunit.v3.runner.utility", "3.0.1-pre.19")
7979
)
8080
);
8181
}

src/xunit.analyzers.tests/xunit.analyzers.tests.csproj

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@
2323
<PackageDownload Include="xunit.extensibility.execution" Version="[2.9.3-pre.4]" />
2424
<PackageDownload Include="xunit.runner.utility" Version="[2.9.3-pre.4]" />
2525

26-
<PackageDownload Include="xunit.v3.assert" Version="[3.0.1-pre.12]" />
27-
<PackageDownload Include="xunit.v3.common" Version="[3.0.1-pre.12]" />
28-
<PackageDownload Include="xunit.v3.extensibility.core" Version="[3.0.1-pre.12]" />
29-
<PackageDownload Include="xunit.v3.runner.common" Version="[3.0.1-pre.12]" />
30-
<PackageDownload Include="xunit.v3.runner.utility" Version="[3.0.1-pre.12]" />
26+
<PackageDownload Include="xunit.v3.assert" Version="[3.0.1-pre.19]" />
27+
<PackageDownload Include="xunit.v3.common" Version="[3.0.1-pre.19]" />
28+
<PackageDownload Include="xunit.v3.extensibility.core" Version="[3.0.1-pre.19]" />
29+
<PackageDownload Include="xunit.v3.runner.common" Version="[3.0.1-pre.19]" />
30+
<PackageDownload Include="xunit.v3.runner.utility" Version="[3.0.1-pre.19]" />
3131

3232
<!-- Download packages referenced by CodeAnalysisNetAnalyzers -->
3333
<PackageDownload Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="[9.0.0]" />

src/xunit.analyzers/Utility/SerializabilityAnalyzer.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Diagnostics.CodeAnalysis;
23
using System.Linq;
34
using Microsoft.CodeAnalysis;
@@ -6,6 +7,8 @@ namespace Xunit.Analyzers;
67

78
public sealed class SerializabilityAnalyzer(SerializableTypeSymbols typeSymbols)
89
{
10+
static readonly Version Version_3_0_1 = new(3, 0, 1);
11+
912
/// <summary>
1013
/// Analyze the given type to determine whether it is always, possibly, or never serializable.
1114
/// </summary>
@@ -61,6 +64,9 @@ public Serializability AnalayzeSerializability(
6164
if (iParsableOfSelf.IsAssignableFrom(type))
6265
return Serializability.AlwaysSerializable;
6366
}
67+
68+
if (xunitContext.V3Core?.Version >= Version_3_0_1 && typeSymbols.ITuple is not null && typeSymbols.ITuple.IsAssignableFrom(type))
69+
return Serializability.AlwaysSerializable;
6470
}
6571

6672
if (typeSymbols.TypesWithCustomSerializers.Any(t => t.IsAssignableFrom(type)))

src/xunit.analyzers/Utility/SerializableTypeSymbols.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public sealed class SerializableTypeSymbols
1515
readonly Lazy<INamedTypeSymbol?> iFormattable;
1616
readonly Lazy<INamedTypeSymbol?> index;
1717
readonly Lazy<INamedTypeSymbol?> iParsableOfT;
18+
readonly Lazy<INamedTypeSymbol?> iTuple;
1819
readonly Lazy<INamedTypeSymbol?> iXunitSerializable;
1920
readonly Lazy<INamedTypeSymbol?> range;
2021
readonly Lazy<INamedTypeSymbol?> theoryDataBaseType;
@@ -45,6 +46,7 @@ public sealed class SerializableTypeSymbols
4546
iFormattable = new(() => TypeSymbolFactory.IFormattable(compilation));
4647
index = new(() => TypeSymbolFactory.Index(compilation));
4748
iParsableOfT = new(() => TypeSymbolFactory.IParsableOfT(compilation));
49+
iTuple = new(() => TypeSymbolFactory.ITuple(compilation));
4850
iXunitSerializable = new(() => xunitContext.Common.IXunitSerializableType);
4951
range = new(() => TypeSymbolFactory.Range(compilation));
5052
// For v2 and early versions of v3, the base type is "TheoryData" (non-generic). For later versions
@@ -90,6 +92,7 @@ public sealed class SerializableTypeSymbols
9092
public INamedTypeSymbol? IFormattable => iFormattable.Value;
9193
public INamedTypeSymbol? Index => index.Value;
9294
public INamedTypeSymbol? IParsableOfT => iParsableOfT.Value;
95+
public INamedTypeSymbol? ITuple => iTuple.Value;
9396
public INamedTypeSymbol? IXunitSerializable => iXunitSerializable.Value;
9497
public INamedTypeSymbol MemberDataAttribute { get; }
9598
public INamedTypeSymbol? Range => range.Value;

tools/builder/build.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
</ItemGroup>
1818

1919
<ItemGroup>
20-
<PackageDownload Include="xunit.v3.runner.console" Version="[3.0.1-pre.12]" />
20+
<PackageDownload Include="xunit.v3.runner.console" Version="[3.0.1-pre.19]" />
2121
</ItemGroup>
2222

2323
</Project>

0 commit comments

Comments
 (0)