Skip to content

Commit 18f6a70

Browse files
Merge pull request #816 from nunit/Issue769_EnterMultipleScope
Add support for Assert.EnterMultipleScope
2 parents 568d5ea + cf7eacb commit 18f6a70

File tree

8 files changed

+235
-66
lines changed

8 files changed

+235
-66
lines changed

src/nunit.analyzers.tests/Constants/NUnitFrameworkConstantsTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,10 @@ public sealed class NUnitFrameworkConstantsTests
7474
(nameof(NUnitFrameworkConstants.NameOfMultiple), nameof(Assert.Multiple)),
7575
#if NUNIT4
7676
(nameof(NUnitFrameworkConstants.NameOfMultipleAsync), nameof(Assert.MultipleAsync)),
77+
(nameof(NUnitFrameworkConstants.NameOfEnterMultipleScope), nameof(Assert.EnterMultipleScope)),
7778
#else
7879
(nameof(NUnitFrameworkConstants.NameOfMultipleAsync), "MultipleAsync"),
80+
(nameof(NUnitFrameworkConstants.NameOfEnterMultipleScope), "EnterMultipleScope"),
7981
#endif
8082

8183
(nameof(NUnitFrameworkConstants.NameOfOut), nameof(TestContext.Out)),

src/nunit.analyzers.tests/UseAssertMultiple/UseAssertMultipleAnalyzerTests.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,38 @@ await Assert.MultipleAsync(async () =>
4545
}");
4646
RoslynAssert.Valid(this.analyzer, testCode);
4747
}
48+
49+
#if WOULD_SOMEONE_ACTUALLY_USE_THIS
50+
[Test]
51+
public void AnalyzeWhenMultipleScopeDeclarationIsUsed()
52+
{
53+
var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
54+
public void Test()
55+
{
56+
using IDisposable disposable = Assert.EnterMultipleScope();
57+
58+
Assert.That(true, Is.True);
59+
disposable.Dispose();
60+
Assert.That(false, Is.False);
61+
}");
62+
RoslynAssert.Valid(this.analyzer, testCode);
63+
}
64+
#endif
65+
66+
[Test]
67+
public void AnalyzeWhenMultipleScopeStatementIsUsed()
68+
{
69+
var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
70+
public void Test()
71+
{
72+
using (Assert.EnterMultipleScope())
73+
{
74+
Assert.That(true, Is.True);
75+
Assert.That(false, Is.False);
76+
}
77+
}");
78+
RoslynAssert.Valid(this.analyzer, testCode);
79+
}
4880
#endif
4981

5082
[Test]

src/nunit.analyzers.tests/UseAssertMultiple/UseAssertMultipleCodeFixTests.cs

Lines changed: 128 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public void TestMethod()
3434
Assert.That(false, Is.False);
3535
Console.WriteLine(""Next Statement"");
3636
}");
37+
3738
var fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
3839
public void TestMethod()
3940
{
@@ -44,12 +45,36 @@ public void TestMethod()
4445
});
4546
Console.WriteLine(""Next Statement"");
4647
}");
47-
RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode);
48+
49+
RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode, UseAssertMultipleCodeFix.WrapWithAssertMultiple);
50+
51+
#if NUNIT4
52+
fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
53+
public void TestMethod()
54+
{
55+
using (Assert.EnterMultipleScope())
56+
{
57+
Assert.That(true, Is.True);
58+
Assert.That(false, Is.False);
59+
}
60+
Console.WriteLine(""Next Statement"");
61+
}");
62+
63+
RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode, UseAssertMultipleCodeFix.WrapWithAssertEnterMultipleScope);
64+
#endif
4865
}
4966

5067
[Test]
5168
public void VerifyPartlyIndependent()
5269
{
70+
const string ConfigurationClass = @"
71+
private sealed class Configuration
72+
{
73+
public int Value1 { get; set; }
74+
public double Value2 { get; set; }
75+
public string Value11 { get; set; } = string.Empty;
76+
}";
77+
5378
var code = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
5479
public void Test()
5580
{
@@ -59,14 +84,8 @@ public void Test()
5984
Assert.That(configuration.Value2, Is.EqualTo(0.0));
6085
Assert.That(configuration.Value11, Is.EqualTo(string.Empty));
6186
configuration = null;
62-
}
87+
}" + ConfigurationClass);
6388

64-
private sealed class Configuration
65-
{
66-
public int Value1 { get; set; }
67-
public double Value2 { get; set; }
68-
public string Value11 { get; set; } = string.Empty;
69-
}");
7089
var fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
7190
public void Test()
7291
{
@@ -79,40 +98,54 @@ public void Test()
7998
Assert.That(configuration.Value11, Is.EqualTo(string.Empty));
8099
});
81100
configuration = null;
101+
}" + ConfigurationClass);
102+
103+
RoslynAssert.FixAll(analyzer, fix, expectedDiagnostic, code, fixedCode, UseAssertMultipleCodeFix.WrapWithAssertMultiple);
104+
105+
#if NUNIT4
106+
fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
107+
public void Test()
108+
{
109+
var configuration = new Configuration();
110+
Assert.That(configuration, Is.Not.Null);
111+
using (Assert.EnterMultipleScope())
112+
{
113+
Assert.That(configuration.Value1, Is.EqualTo(0));
114+
Assert.That(configuration.Value2, Is.EqualTo(0.0));
115+
Assert.That(configuration.Value11, Is.EqualTo(string.Empty));
116+
}
117+
configuration = null;
118+
}" + ConfigurationClass);
119+
120+
RoslynAssert.FixAll(analyzer, fix, expectedDiagnostic, code, fixedCode, UseAssertMultipleCodeFix.WrapWithAssertEnterMultipleScope);
121+
#endif
82122
}
83123

124+
[Test]
125+
public void AddsAsyncWhenAwaitIsUsed()
126+
{
127+
const string ConfigurationClass = @"
84128
private sealed class Configuration
85129
{
86130
public int Value1 { get; set; }
87131
public double Value2 { get; set; }
88132
public string Value11 { get; set; } = string.Empty;
89-
}");
90-
RoslynAssert.FixAll(analyzer, fix, expectedDiagnostic, code, fixedCode);
91-
}
133+
public Task<string> AsStringAsync() => Task.FromResult(Value11);
134+
}";
92135

93-
[Test]
94-
public void AddsAsyncWhenAwaitIsUsed()
95-
{
96136
var code = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
97-
public void Test()
137+
public async Task Test()
98138
{
99139
var configuration = new Configuration();
100140
Assert.That(configuration, Is.Not.Null);
101141
↓Assert.That(configuration.Value1, Is.EqualTo(0));
102142
Assert.That(configuration.Value2, Is.EqualTo(0.0));
103143
Assert.That(await configuration.AsStringAsync(), Is.EqualTo(string.Empty));
104144
configuration = null;
105-
}
145+
}" + ConfigurationClass);
106146

107-
private sealed class Configuration
108-
{
109-
public int Value1 { get; set; }
110-
public double Value2 { get; set; }
111-
public string Value11 { get; set; } = string.Empty;
112-
public Task<string> AsStringAsync() => Task.FromResult(Value11);
113-
}");
114147
var fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
115-
public void Test()
148+
public async Task Test()
116149
{
117150
var configuration = new Configuration();
118151
Assert.That(configuration, Is.Not.Null);
@@ -123,16 +156,30 @@ public void Test()
123156
Assert.That(await configuration.AsStringAsync(), Is.EqualTo(string.Empty));
124157
});
125158
configuration = null;
126-
}
159+
}" + ConfigurationClass);
127160

128-
private sealed class Configuration
161+
// The test method itself no longer awaits, so CS1998 is generated.
162+
// Fixing this is outside the scope of this analyzer and there could be other non-touched statements that are waited.
163+
RoslynAssert.FixAll(analyzer, fix, expectedDiagnostic, code, fixedCode, UseAssertMultipleCodeFix.WrapWithAssertMultiple,
164+
Settings.Default.WithAllowedCompilerDiagnostics(AllowedCompilerDiagnostics.WarningsAndErrors));
165+
166+
#if NUNIT4
167+
fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
168+
public async Task Test()
129169
{
130-
public int Value1 { get; set; }
131-
public double Value2 { get; set; }
132-
public string Value11 { get; set; } = string.Empty;
133-
public Task<string> AsStringAsync() => Task.FromResult(Value11);
134-
}");
135-
RoslynAssert.FixAll(analyzer, fix, expectedDiagnostic, code, fixedCode);
170+
var configuration = new Configuration();
171+
Assert.That(configuration, Is.Not.Null);
172+
using (Assert.EnterMultipleScope())
173+
{
174+
Assert.That(configuration.Value1, Is.EqualTo(0));
175+
Assert.That(configuration.Value2, Is.EqualTo(0.0));
176+
Assert.That(await configuration.AsStringAsync(), Is.EqualTo(string.Empty));
177+
}
178+
configuration = null;
179+
}" + ConfigurationClass);
180+
181+
RoslynAssert.FixAll(analyzer, fix, expectedDiagnostic, code, fixedCode, UseAssertMultipleCodeFix.WrapWithAssertEnterMultipleScope);
182+
#endif
136183
}
137184

138185
[Test]
@@ -152,6 +199,7 @@ public void TestMethod()
152199
Assert.That(False, Is.False);{newline}
153200
{preComment}Console.WriteLine(""Next Statement"");{postComment}
154201
}}");
202+
155203
var fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@$"
156204
public void TestMethod()
157205
{{
@@ -166,30 +214,67 @@ public void TestMethod()
166214
}});{newline}
167215
{preComment}Console.WriteLine(""Next Statement"");{postComment}
168216
}}");
169-
RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode);
217+
218+
RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode, UseAssertMultipleCodeFix.WrapWithAssertMultiple);
219+
220+
#if NUNIT4
221+
fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@$"
222+
public void TestMethod()
223+
{{
224+
const bool True = true;
225+
const bool False = false;
226+
227+
using (Assert.EnterMultipleScope())
228+
{{
229+
// Verify that our bool constants are correct
230+
Assert.That(True, Is.True);
231+
Assert.That(False, Is.False);
232+
}}{newline}
233+
{preComment}Console.WriteLine(""Next Statement"");{postComment}
234+
}}");
235+
236+
RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode, UseAssertMultipleCodeFix.WrapWithAssertEnterMultipleScope);
237+
#endif
170238
}
171239

172240
[Test]
173241
public void VerifyKeepsTrivia()
174242
{
175-
var code = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@$"
243+
var code = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
176244
public void TestMethod()
177-
{{
178-
// Verify that boolean work as expected
245+
{
246+
// Verify that boolean work as expected
179247
↓Assert.That(true, Is.True);
180248
Assert.That(false, Is.False);
181-
}}");
182-
var fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@$"
249+
}");
250+
251+
var fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
183252
public void TestMethod()
184-
{{
253+
{
185254
Assert.Multiple(() =>
186-
{{
255+
{
187256
// Verify that boolean work as expected
188257
Assert.That(true, Is.True);
189258
Assert.That(false, Is.False);
190-
}});
191-
}}");
192-
RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode);
259+
});
260+
}");
261+
262+
RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode, UseAssertMultipleCodeFix.WrapWithAssertMultiple);
263+
264+
#if NUNIT4
265+
fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
266+
public void TestMethod()
267+
{
268+
using (Assert.EnterMultipleScope())
269+
{
270+
// Verify that boolean work as expected
271+
Assert.That(true, Is.True);
272+
Assert.That(false, Is.False);
273+
}
274+
}");
275+
276+
RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode, UseAssertMultipleCodeFix.WrapWithAssertEnterMultipleScope);
277+
#endif
193278
}
194279
}
195280
}

src/nunit.analyzers/Constants/AnalyzerPropertyKeys.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,7 @@ internal static class AnalyzerPropertyKeys
55
internal const string ModelName = nameof(AnalyzerPropertyKeys.ModelName);
66
internal const string ArgsIsArray = nameof(AnalyzerPropertyKeys.ArgsIsArray);
77
internal const string MinimumNumberOfArguments = nameof(AnalyzerPropertyKeys.MinimumNumberOfArguments);
8+
9+
internal const string SupportsEnterMultipleScope = nameof(AnalyzerPropertyKeys.SupportsEnterMultipleScope);
810
}
911
}

src/nunit.analyzers/Constants/NUnitFrameworkConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ public static class NUnitFrameworkConstants
5353

5454
public const string NameOfMultiple = "Multiple";
5555
public const string NameOfMultipleAsync = "MultipleAsync";
56+
public const string NameOfEnterMultipleScope = "EnterMultipleScope";
5657

5758
public const string NameOfOut = "Out";
5859
public const string NameOfWrite = "Write";

src/nunit.analyzers/Helpers/AssertHelper.cs

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,18 +95,35 @@ public static bool IsLiteralOperation(IOperation operation)
9595
/// </summary>
9696
public static bool IsInsideAssertMultiple(SyntaxNode node)
9797
{
98-
InvocationExpressionSyntax? possibleAssertMultiple;
99-
100-
while ((possibleAssertMultiple = node.Ancestors().OfType<InvocationExpressionSyntax>().FirstOrDefault()) is not null)
98+
// Look for Assert.Multiple(delegate) invocation.
99+
SyntaxNode currentNode = node;
100+
InvocationExpressionSyntax? possibleAssertMultipleInvocation;
101+
while ((possibleAssertMultipleInvocation = currentNode.Ancestors().OfType<InvocationExpressionSyntax>().FirstOrDefault()) is not null)
101102
{
102103
// Is the statement inside a Block which is part of an Assert.Multiple.
103-
if (IsAssert(possibleAssertMultiple, NUnitFrameworkConstants.NameOfMultiple, NUnitFrameworkConstants.NameOfMultipleAsync))
104+
if (IsAssert(possibleAssertMultipleInvocation, NUnitFrameworkConstants.NameOfMultiple, NUnitFrameworkConstants.NameOfMultipleAsync))
105+
{
106+
return true;
107+
}
108+
109+
// Keep looking at possible parent nested expression.
110+
currentNode = possibleAssertMultipleInvocation;
111+
}
112+
113+
// Look for using (Assert.EnterMultipleScope()) invocation.
114+
currentNode = node;
115+
UsingStatementSyntax? usingStatement;
116+
while ((usingStatement = currentNode.Ancestors().OfType<UsingStatementSyntax>().FirstOrDefault()) is not null)
117+
{
118+
// Is the using expression an Assert.EnterMultipleScope.
119+
if (usingStatement.Expression is InvocationExpressionSyntax usingInvocation &&
120+
IsAssert(usingInvocation, NUnitFrameworkConstants.NameOfEnterMultipleScope))
104121
{
105122
return true;
106123
}
107124

108125
// Keep looking at possible parent nested expression.
109-
node = possibleAssertMultiple;
126+
currentNode = usingStatement;
110127
}
111128

112129
return false;

src/nunit.analyzers/UseAssertMultiple/UseAssertMultipleAnalyzer.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ namespace NUnit.Analyzers.UseAssertMultiple
1313
[DiagnosticAnalyzer(LanguageNames.CSharp)]
1414
public class UseAssertMultipleAnalyzer : BaseAssertionAnalyzer
1515
{
16+
private static readonly Version firstNUnitVersionWithEnterMultipleScope = new Version(4, 2);
17+
1618
private static readonly DiagnosticDescriptor descriptor = DiagnosticDescriptorCreator.Create(
1719
id: AnalyzerIdentifiers.UseAssertMultiple,
1820
title: UseAssertMultipleConstants.Title,
@@ -67,7 +69,7 @@ internal static void Add(HashSet<string> previousArguments, string argument)
6769
}
6870
}
6971

70-
protected override void AnalyzeAssertInvocation(OperationAnalysisContext context, IInvocationOperation assertOperation)
72+
protected override void AnalyzeAssertInvocation(Version nunitVersion, OperationAnalysisContext context, IInvocationOperation assertOperation)
7173
{
7274
if (assertOperation.TargetMethod.Name != NUnitFrameworkConstants.NameOfAssertThat ||
7375
AssertHelper.IsInsideAssertMultiple(assertOperation.Syntax))
@@ -134,7 +136,11 @@ protected override void AnalyzeAssertInvocation(OperationAnalysisContext context
134136

135137
if (lastAssert > firstAssert)
136138
{
137-
context.ReportDiagnostic(Diagnostic.Create(descriptor, assertOperation.Syntax.GetLocation()));
139+
var properties = ImmutableDictionary.CreateBuilder<string, string?>();
140+
properties.Add(AnalyzerPropertyKeys.SupportsEnterMultipleScope,
141+
nunitVersion >= firstNUnitVersionWithEnterMultipleScope ?
142+
NUnitFrameworkConstants.NameOfEnterMultipleScope : null);
143+
context.ReportDiagnostic(Diagnostic.Create(descriptor, assertOperation.Syntax.GetLocation(), properties.ToImmutable()));
138144
}
139145
}
140146
}

0 commit comments

Comments
 (0)