From fb2433a05fc70791ad9063ed372b90f75a6226b0 Mon Sep 17 00:00:00 2001 From: Klaus Loeffelmann Date: Fri, 17 Jan 2025 23:27:27 -0800 Subject: [PATCH] Refactor Analyzer tests. --- eng/Versions.props | 2 +- ...PassingTaskWithoutCancellationTokenTest.cs | 199 ----------- ...assingTaskWithoutCancellationTokenTests.cs | 60 ++++ ...ertySerializationDiagnosticAnalyzerTest.cs | 236 ------------- ...PropertySerializationConfigurationTests.cs | 55 +++ ...indows.Forms.Analyzers.CSharp.Tests.csproj | 18 + .../AnalyzerTestCode.cs | 69 ++++ .../AsyncControl.cs | 70 ++++ .../AnalyzerTestCode.cs | 44 +++ .../CodeFixTestCode.cs | 39 +++ .../FixedTestCode.cs | 43 +++ .../GlobalUsing.cs | 2 + .../WinForms/AnalyzerTestPathAttribute.cs | 15 + ...lyzerAndCodeFixTestBase.TestDataFileSet.cs | 21 ++ .../RoslynAnalyzerAndCodeFixTestBase.cs | 312 ++++++++++++++++++ .../Microsoft/WinForms/SourceLanguage.cs | 19 ++ .../Microsoft/WinForms/TestFileEntry.cs | 31 ++ .../Microsoft/WinForms/TestFileLoader.cs | 136 ++++++++ .../Microsoft/WinForms/TestFileType.cs | 35 ++ ...ystem.Windows.Forms.Analyzers.Tests.csproj | 7 + .../Analyzers/AppManifestAnalyzerTests.cs | 1 - .../ApplicationConfigTests.FontDescriptor.cs | 1 - .../ApplicationConfigTests.FontStyle.cs | 1 - .../ApplicationConfigTests.GraphicsUnit.cs | 1 - 24 files changed, 977 insertions(+), 440 deletions(-) delete mode 100644 src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/Analyzers/AvoidPassingTaskWithoutCancellationToken/AvoidPassingTaskWithoutCancellationTokenTest.cs create mode 100644 src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/Analyzers/AvoidPassingTaskWithoutCancellationTokenTests.cs delete mode 100644 src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/Analyzers/MissingPropertySerializationConfiguration/ControlPropertySerializationDiagnosticAnalyzerTest.cs create mode 100644 src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/Analyzers/MissingPropertySerializationConfigurationTests.cs create mode 100644 src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/TestData/AvoidPassingTaskWithoutCancellationToken/AnalyzerTestCode.cs create mode 100644 src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/TestData/AvoidPassingTaskWithoutCancellationToken/AsyncControl.cs create mode 100644 src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/TestData/MissingPropertySerializationConfiguration/AnalyzerTestCode.cs create mode 100644 src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/TestData/MissingPropertySerializationConfiguration/CodeFixTestCode.cs create mode 100644 src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/TestData/MissingPropertySerializationConfiguration/FixedTestCode.cs create mode 100644 src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/TestData/MissingPropertySerializationConfiguration/GlobalUsing.cs create mode 100644 src/System.Windows.Forms.Analyzers/tests/UnitTests/Microsoft/WinForms/AnalyzerTestPathAttribute.cs create mode 100644 src/System.Windows.Forms.Analyzers/tests/UnitTests/Microsoft/WinForms/RoslynAnalyzerAndCodeFixTestBase.TestDataFileSet.cs create mode 100644 src/System.Windows.Forms.Analyzers/tests/UnitTests/Microsoft/WinForms/RoslynAnalyzerAndCodeFixTestBase.cs create mode 100644 src/System.Windows.Forms.Analyzers/tests/UnitTests/Microsoft/WinForms/SourceLanguage.cs create mode 100644 src/System.Windows.Forms.Analyzers/tests/UnitTests/Microsoft/WinForms/TestFileEntry.cs create mode 100644 src/System.Windows.Forms.Analyzers/tests/UnitTests/Microsoft/WinForms/TestFileLoader.cs create mode 100644 src/System.Windows.Forms.Analyzers/tests/UnitTests/Microsoft/WinForms/TestFileType.cs diff --git a/eng/Versions.props b/eng/Versions.props index 1b86957f655..f353cb3689f 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -94,7 +94,7 @@ 0.1.495 1.0.0-beta.59 3.12.0-beta1.24559.1 - 4.10.0-3.final + 4.12.0 $(MicrosoftCodeAnalysisCommonPackageVersion) $(MicrosoftCodeAnalysisCommonPackageVersion) $(MicrosoftCodeAnalysisCommonPackageVersion) diff --git a/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/Analyzers/AvoidPassingTaskWithoutCancellationToken/AvoidPassingTaskWithoutCancellationTokenTest.cs b/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/Analyzers/AvoidPassingTaskWithoutCancellationToken/AvoidPassingTaskWithoutCancellationTokenTest.cs deleted file mode 100644 index 2746bf75da0..00000000000 --- a/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/Analyzers/AvoidPassingTaskWithoutCancellationToken/AvoidPassingTaskWithoutCancellationTokenTest.cs +++ /dev/null @@ -1,199 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Windows.Forms.Analyzers.Diagnostics; -using System.Windows.Forms.CSharp.Analyzers.AvoidPassingTaskWithoutCancellationToken; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Testing; -using Microsoft.CodeAnalysis.Testing; - -namespace System.Windows.Forms.Analyzers.Test; - -public class AvoidPassingTaskWithoutCancellationTokenTest -{ - // Currently, we do not have Control.InvokeAsync in the .NET 9.0 Windows reference assemblies. - // That's why we need to add this Async Control. Once it's there, this test will fail. - // We can then remove the AsyncControl and the test will pass, replace AsyncControl with - // Control, and the test will pass. - private const string AsyncControl = """ - using System; - using System.Threading; - using System.Threading.Tasks; - using System.Windows.Forms; - - namespace System.Windows.Forms - { - public class AsyncControl : Control - { - // BEGIN ASYNC API - public Task InvokeAsync( - Action callback, - CancellationToken cancellationToken = default) - { - var tcs = new TaskCompletionSource(); - - // Note: Code is INCORRECT, it's just here to satisfy the compiler! - using (cancellationToken.Register(() => tcs.TrySetCanceled())) - { - base.BeginInvoke(callback); - } - - return tcs.Task; - } - - public Task InvokeAsync( - Func callback, - CancellationToken cancellationToken = default) - { - var tcs = new TaskCompletionSource(); - - // Note: Code is INCORRECT, it's just here to satisfy the compiler! - using (cancellationToken.Register(() => tcs.TrySetCanceled())) - { - base.BeginInvoke(callback); - } - - return tcs.Task; - } - - public Task InvokeAsync( - Func callback, - CancellationToken cancellationToken = default) - { - var tcs = new TaskCompletionSource(); - - // Note: Code is INCORRECT, it's just here to satisfy the compiler! - using (cancellationToken.Register(() => tcs.TrySetCanceled())) - { - base.BeginInvoke(callback); - } - - return tcs.Task; - } - - public Task InvokeAsync( - Func> callback, - CancellationToken cancellationToken = default) - { - var tcs = new TaskCompletionSource(); - // Note: Code is INCORRECT, it's just here to satisfy the compiler! - using (cancellationToken.Register(() => tcs.TrySetCanceled())) - { - base.BeginInvoke(callback); - } - return tcs.Task; - } - // END ASYNC API - } - } - - """; - - private const string TestCode = """ - using System; - using System.Threading; - using System.Threading.Tasks; - using System.Windows.Forms; - - namespace CSharpControls; - - public static class Program - { - public static void Main() - { - var control = new AsyncControl(); - - // A sync Action delegate is always fine. - var okAction = new Action(() => control.Text = "Hello, World!"); - - // A sync Func delegate is also fine. - var okFunc = new Func(() => 42); - - // Just a Task we will get in trouble since it's handled as a fire and forget. - var notOkAsyncFunc = new Func(() => - { - control.Text = "Hello, World!"; - return Task.CompletedTask; - }); - - // A Task returning a value will also get us in trouble since it's handled as a fire and forget. - var notOkAsyncFunc2 = new Func>(() => - { - control.Text = "Hello, World!"; - return Task.FromResult(42); - }); - - // OK. - var task1 = control.InvokeAsync(okAction); - - // Also OK. - var task2 = control.InvokeAsync(okFunc); - - // Concerning. - Most likely fire and forget by accident. We should warn about this. - var task3 = control.InvokeAsync(notOkAsyncFunc, System.Threading.CancellationToken.None); - - // Again: Concerning. - Most likely fire and forget by accident. We should warn about this. - var task4 = control.InvokeAsync(notOkAsyncFunc, System.Threading.CancellationToken.None); - - // And again concerning. - We should warn about this, too. - var task5 = control.InvokeAsync(notOkAsyncFunc2, System.Threading.CancellationToken.None); - - // This is OK, since we're passing a cancellation token. - var okAsyncFunc = new Func((cancellation) => - { - control.Text = "Hello, World!"; - return ValueTask.CompletedTask; - }); - - // This is also OK, again, because we're passing a cancellation token. - var okAsyncFunc2 = new Func>((cancellation) => - { - control.Text = "Hello, World!"; - return ValueTask.FromResult(42); - }); - - // And let's test that, too: - var task6 = control.InvokeAsync(okAsyncFunc, System.Threading.CancellationToken.None); - - // And that, too: - var task7 = control.InvokeAsync(okAsyncFunc2, System.Threading.CancellationToken.None); - } - } - - """; - - public static IEnumerable GetReferenceAssemblies() - { - yield return [ReferenceAssemblies.Net.Net90Windows]; - } - - [Theory] - [MemberData(nameof(GetReferenceAssemblies))] - public async Task CS_AvoidPassingTaskWithoutCancellationAnalyzer(ReferenceAssemblies referenceAssemblies) - { - // If the API does not exist, we need to add it to the test. - string customControlSource = AsyncControl; - string diagnosticId = DiagnosticIDs.AvoidPassingFuncReturningTaskWithoutCancellationToken; - - var context = new CSharpAnalyzerTest - - { - TestCode = TestCode, - TestState = - { - OutputKind = OutputKind.WindowsApplication, - Sources = { customControlSource }, - ExpectedDiagnostics = - { - DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(41, 21, 41, 97), - DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(44, 21, 44, 97), - DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(47, 21, 47, 98), - }, - }, - ReferenceAssemblies = referenceAssemblies - }; - - await context.RunAsync(); - } -} diff --git a/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/Analyzers/AvoidPassingTaskWithoutCancellationTokenTests.cs b/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/Analyzers/AvoidPassingTaskWithoutCancellationTokenTests.cs new file mode 100644 index 00000000000..1fa4f9195f3 --- /dev/null +++ b/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/Analyzers/AvoidPassingTaskWithoutCancellationTokenTests.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Windows.Forms.Analyzers.Diagnostics; +using System.Windows.Forms.CSharp.Analyzers.AvoidPassingTaskWithoutCancellationToken; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; + +namespace System.Windows.Forms.Analyzers.Test; + +public class AvoidPassingTaskWithoutCancellationTokenTests +{ + // Currently, we do not have Control.InvokeAsync in the .NET 9.0 Windows reference assemblies. + // That's why we need to add this Async Control. Once it's there, this test will fail. + // We can then remove the AsyncControl and the test will pass, replace AsyncControl with + // Control, and the test will pass. + private const string AsyncControl = """ + + """; + + private const string TestCode = """ + + """; + + public static IEnumerable GetReferenceAssemblies() + { + yield return [ReferenceAssemblies.Net.Net90Windows]; + } + + [Theory] + [MemberData(nameof(GetReferenceAssemblies))] + public async Task AvoidPassingTaskWithoutCancellationAnalyzer(ReferenceAssemblies referenceAssemblies) + { + // If the API does not exist, we need to add it to the test. + string customControlSource = AsyncControl; + string diagnosticId = DiagnosticIDs.AvoidPassingFuncReturningTaskWithoutCancellationToken; + + var context = new CSharpAnalyzerTest + + { + TestCode = TestCode, + TestState = + { + OutputKind = OutputKind.WindowsApplication, + Sources = { customControlSource }, + ExpectedDiagnostics = + { + DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(41, 21, 41, 97), + DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(44, 21, 44, 97), + DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(47, 21, 47, 98), + }, + }, + ReferenceAssemblies = referenceAssemblies + }; + + await context.RunAsync(); + } +} diff --git a/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/Analyzers/MissingPropertySerializationConfiguration/ControlPropertySerializationDiagnosticAnalyzerTest.cs b/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/Analyzers/MissingPropertySerializationConfiguration/ControlPropertySerializationDiagnosticAnalyzerTest.cs deleted file mode 100644 index 0eb736df62b..00000000000 --- a/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/Analyzers/MissingPropertySerializationConfiguration/ControlPropertySerializationDiagnosticAnalyzerTest.cs +++ /dev/null @@ -1,236 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Windows.Forms.CSharp.Analyzers.MissingPropertySerializationConfiguration; -using System.Windows.Forms.CSharp.CodeFixes.AddDesignerSerializationVisibility; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Testing; -using Microsoft.CodeAnalysis.Testing; - -namespace System.Windows.Forms.Analyzers.Test; - -public class ControlPropertySerializationDiagnosticAnalyzerTest -{ - private const string GlobalUsingCode = """ - global using System.Drawing; - global using System.Windows.Forms; - """; - - private const string ProblematicCode = """ - namespace CSharpControls; - - public static class Program - { - public static void Main() - { - var control = new ScalableControl(); - - // We deliberately format this weirdly, to make sure we only format code our code fix touches. - control.ScaleFactor = 1.5f; - control.ScaledSize = new SizeF(100, 100); - control.ScaledLocation = new PointF(10, 10); - } - } - - // We are writing the fully-qualified name here to make sure, the Simplifier doesn't remove it, - // since this is nothing our code fix touches. - public class ScalableControl : System.Windows.Forms.Control - { - private SizeF _scaleSize = new SizeF(3, 14); - - /// - /// Sets or gets the scaled size of some foo bar thing. - /// - [System.ComponentModel.Description("Sets or gets the scaled size of some foo bar thing.")] - public SizeF [|ScaledSize|] - { - get => _scaleSize; - set => _scaleSize = value; - } - - public float [|ScaleFactor|] { get; set; } = 1.0f; - - /// - /// Sets or gets the scaled location of some foo bar thing. - /// - public PointF [|ScaledLocation|] { get; set; } - } - - """; - - private const string CorrectCode = """ - using System.ComponentModel; - - namespace CSharpControls; - - public static class Program - { - public static void Main() - { - var control = new ScalableControl(); - - // We deliberately format this weirdly, to make sure we only format code our code fix touches. - control.ScaleFactor = 1.5f; - control.ScaledSize = new SizeF(100, 100); - control.ScaledLocation = new PointF(10, 10); - } - } - - // We are writing the fully-qualified name here to make sure, the Simplifier doesn't remove it, - // since this is nothing our code fix touches. - public class ScalableControl : System.Windows.Forms.Control - { - private SizeF _scaleSize = new SizeF(3, 14); - - /// - /// Sets or gets the scaled size of some foo bar thing. - /// - [System.ComponentModel.Description("Sets or gets the scaled size of some foo bar thing.")] - [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - public SizeF ScaledSize - { - get => _scaleSize; - set => _scaleSize = value; - } - - [DefaultValue(1.0f)] - public float ScaleFactor { get; set; } = 1.0f; - - /// - /// Sets or gets the scaled location of some foo bar thing. - /// - public PointF ScaledLocation { get; set; } - - private bool ShouldSerializeScaledLocation() => false; - } - - """; - - private const string FixedCode = """ - using System.ComponentModel; - - namespace CSharpControls; - - public static class Program - { - public static void Main() - { - var control = new ScalableControl(); - - // We deliberately format this weirdly, to make sure we only format code our code fix touches. - control.ScaleFactor = 1.5f; - control.ScaledSize = new SizeF(100, 100); - control.ScaledLocation = new PointF(10, 10); - } - } - - // We are writing the fully-qualified name here to make sure, the Simplifier doesn't remove it, - // since this is nothing our code fix touches. - public class ScalableControl : System.Windows.Forms.Control - { - private SizeF _scaleSize = new SizeF(3, 14); - - /// - /// Sets or gets the scaled size of some foo bar thing. - /// - [System.ComponentModel.Description("Sets or gets the scaled size of some foo bar thing.")] - [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - public SizeF ScaledSize - { - get => _scaleSize; - set => _scaleSize = value; - } - - [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - public float ScaleFactor { get; set; } = 1.0f; - - /// - /// Sets or gets the scaled location of some foo bar thing. - /// - [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - public PointF ScaledLocation { get; set; } - } - - """; - - // We are testing the analyzer with all versions of the .NET SDK from 6.0 on. - public static IEnumerable GetReferenceAssemblies() - { - yield return [ReferenceAssemblies.Net.Net60Windows]; - yield return [ReferenceAssemblies.Net.Net70Windows]; - yield return [ReferenceAssemblies.Net.Net80Windows]; - yield return [ReferenceAssemblies.Net.Net90Windows]; - } - - [Theory] - [MemberData(nameof(GetReferenceAssemblies))] - public async Task CS_ControlPropertySerializationConfigurationDiagnosticsEngage(ReferenceAssemblies referenceAssemblies) - { - var context = new CSharpAnalyzerTest - - { - // Note: The ProblematicCode includes the expected Diagnostic's span in the areas - // where the code is enclosed in limiting characters ("[|...|]"), - // like `public SizeF [|ScaledSize|]`. - TestCode = ProblematicCode, - TestState = - { - OutputKind = OutputKind.WindowsApplication, - }, - ReferenceAssemblies = referenceAssemblies - }; - - context.TestState.Sources.Add(GlobalUsingCode); - - await context.RunAsync(); - } - - [Theory] - [MemberData(nameof(GetReferenceAssemblies))] - public async Task CS_ControlPropertySerializationConfigurationDiagnosticPass(ReferenceAssemblies referenceAssemblies) - { - var context = new CSharpAnalyzerTest - - { - TestCode = CorrectCode, - TestState = - { - OutputKind = OutputKind.WindowsApplication, - }, - ReferenceAssemblies = referenceAssemblies - }; - - context.TestState.Sources.Add(GlobalUsingCode); - - await context.RunAsync(); - } - - [Theory] - [MemberData(nameof(GetReferenceAssemblies))] - public async Task CS_AddDesignerSerializationVisibilityCodeFix(ReferenceAssemblies referenceAssemblies) - { - var context = new CSharpCodeFixTest - - { - TestCode = ProblematicCode, - FixedCode = FixedCode, - TestState = - { - OutputKind = OutputKind.WindowsApplication, - Sources = { GlobalUsingCode } - }, - ReferenceAssemblies = referenceAssemblies, - NumberOfFixAllIterations = 2, - FixedState = - { - Sources = { GlobalUsingCode } - }, - }; - - await context.RunAsync(); - } -} diff --git a/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/Analyzers/MissingPropertySerializationConfigurationTests.cs b/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/Analyzers/MissingPropertySerializationConfigurationTests.cs new file mode 100644 index 00000000000..242dd31a66e --- /dev/null +++ b/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/Analyzers/MissingPropertySerializationConfigurationTests.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Windows.Forms.CSharp.Analyzers.MissingPropertySerializationConfiguration; +using System.Windows.Forms.CSharp.CodeFixes.AddDesignerSerializationVisibility; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.WinForms.Test; + +namespace System.Windows.Forms.Analyzers.Test; + +public class MissingPropertySerializationConfigurationTests + : RoslynAnalyzerAndCodeFixTestBase +{ + // We are testing the analyzer with all versions of the .NET SDK from 6.0 on. + public static IEnumerable GetReferenceAssemblies() + { + // yield return [ReferenceAssemblies.Net.Net60Windows]; + // yield return [ReferenceAssemblies.Net.Net70Windows]; + // yield return [ReferenceAssemblies.Net.Net80Windows]; + yield return [ReferenceAssemblies.Net.Net90Windows]; + } + + [Theory] + [MemberData(nameof(GetReferenceAssemblies))] + public async Task TestDiagnostics(ReferenceAssemblies referenceAssemblies) + { + await EnumerateTestFilesAsync(TestMethod); + + async Task TestMethod(TestDataFileSet fileSet) + { + var context = GetAnalyzerTestContext(fileSet, referenceAssemblies); + await context.RunAsync().ConfigureAwait(false); + + context = GetFixedTestContext(fileSet, referenceAssemblies); + await context.RunAsync().ConfigureAwait(false); + } + } + + [Theory] + [MemberData(nameof(GetReferenceAssemblies))] + public async Task TestCodeFix(ReferenceAssemblies referenceAssemblies, int numberOfFixAllIterations) + { + await EnumerateTestFilesAsync(TestMethod); + + async Task TestMethod(TestDataFileSet fileSet) + { + var context = GetCodeFixTestContext( + fileSet, + referenceAssemblies, + numberOfFixAllIterations); + + await context.RunAsync().ConfigureAwait(false); + } + } +} diff --git a/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/System.Windows.Forms.Analyzers.CSharp.Tests.csproj b/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/System.Windows.Forms.Analyzers.CSharp.Tests.csproj index ca28c94ff6d..9a797128509 100644 --- a/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/System.Windows.Forms.Analyzers.CSharp.Tests.csproj +++ b/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/System.Windows.Forms.Analyzers.CSharp.Tests.csproj @@ -34,4 +34,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/TestData/AvoidPassingTaskWithoutCancellationToken/AnalyzerTestCode.cs b/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/TestData/AvoidPassingTaskWithoutCancellationToken/AnalyzerTestCode.cs new file mode 100644 index 00000000000..a93efa49513 --- /dev/null +++ b/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/TestData/AvoidPassingTaskWithoutCancellationToken/AnalyzerTestCode.cs @@ -0,0 +1,69 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace CSharpControls; + +public static class Program +{ + public static void Main() + { + var control = new AsyncControl(); + + // A sync Action delegate is always fine. + var okAction = new Action(() => control.Text = "Hello, World!"); + + // A sync Func delegate is also fine. + var okFunc = new Func(() => 42); + + // Just a Task we will get in trouble since it's handled as a fire and forget. + var notOkAsyncFunc = new Func(() => + { + control.Text = "Hello, World!"; + return Task.CompletedTask; + }); + + // A Task returning a value will also get us in trouble since it's handled as a fire and forget. + var notOkAsyncFunc2 = new Func>(() => + { + control.Text = "Hello, World!"; + return Task.FromResult(42); + }); + + // OK. + var task1 = control.InvokeAsync(okAction); + + // Also OK. + var task2 = control.InvokeAsync(okFunc); + + // Concerning. - Most likely fire and forget by accident. We should warn about this. + var task3 = control.InvokeAsync(notOkAsyncFunc, System.Threading.CancellationToken.None); + + // Again: Concerning. - Most likely fire and forget by accident. We should warn about this. + var task4 = control.InvokeAsync(notOkAsyncFunc, System.Threading.CancellationToken.None); + + // And again concerning. - We should warn about this, too. + var task5 = control.InvokeAsync(notOkAsyncFunc2, System.Threading.CancellationToken.None); + + // This is OK, since we're passing a cancellation token. + var okAsyncFunc = new Func((cancellation) => + { + control.Text = "Hello, World!"; + return ValueTask.CompletedTask; + }); + + // This is also OK, again, because we're passing a cancellation token. + var okAsyncFunc2 = new Func>((cancellation) => + { + control.Text = "Hello, World!"; + return ValueTask.FromResult(42); + }); + + // And let's test that, too: + var task6 = control.InvokeAsync(okAsyncFunc, System.Threading.CancellationToken.None); + + // And that, too: + var task7 = control.InvokeAsync(okAsyncFunc2, System.Threading.CancellationToken.None); + } +} diff --git a/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/TestData/AvoidPassingTaskWithoutCancellationToken/AsyncControl.cs b/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/TestData/AvoidPassingTaskWithoutCancellationToken/AsyncControl.cs new file mode 100644 index 00000000000..abae9c10a99 --- /dev/null +++ b/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/TestData/AvoidPassingTaskWithoutCancellationToken/AsyncControl.cs @@ -0,0 +1,70 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace System.Windows.Forms +{ + public class AsyncControl : Control + { + // BEGIN ASYNC API + public Task InvokeAsync( + Action callback, + CancellationToken cancellationToken = default) + { + var tcs = new TaskCompletionSource(); + + // Note: Code is INCORRECT, it's just here to satisfy the compiler! + using (cancellationToken.Register(() => tcs.TrySetCanceled())) + { + base.BeginInvoke(callback); + } + + return tcs.Task; + } + + public Task InvokeAsync( + Func callback, + CancellationToken cancellationToken = default) + { + var tcs = new TaskCompletionSource(); + + // Note: Code is INCORRECT, it's just here to satisfy the compiler! + using (cancellationToken.Register(() => tcs.TrySetCanceled())) + { + base.BeginInvoke(callback); + } + + return tcs.Task; + } + + public Task InvokeAsync( + Func callback, + CancellationToken cancellationToken = default) + { + var tcs = new TaskCompletionSource(); + + // Note: Code is INCORRECT, it's just here to satisfy the compiler! + using (cancellationToken.Register(() => tcs.TrySetCanceled())) + { + base.BeginInvoke(callback); + } + + return tcs.Task; + } + + public Task InvokeAsync( + Func> callback, + CancellationToken cancellationToken = default) + { + var tcs = new TaskCompletionSource(); + // Note: Code is INCORRECT, it's just here to satisfy the compiler! + using (cancellationToken.Register(() => tcs.TrySetCanceled())) + { + base.BeginInvoke(callback); + } + return tcs.Task; + } + // END ASYNC API + } +} diff --git a/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/TestData/MissingPropertySerializationConfiguration/AnalyzerTestCode.cs b/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/TestData/MissingPropertySerializationConfiguration/AnalyzerTestCode.cs new file mode 100644 index 00000000000..8ef4b566a02 --- /dev/null +++ b/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/TestData/MissingPropertySerializationConfiguration/AnalyzerTestCode.cs @@ -0,0 +1,44 @@ +using System.ComponentModel; + +namespace CSharpControls; + +public static class Program +{ + public static void Main() + { + var control = new ScalableControl(); + + // We deliberately format this weirdly, to make sure we only format code our code fix touches. + control.ScaleFactor = 1.5f; + control.ScaledSize = new SizeF(100, 100); + control.ScaledLocation = new PointF(10, 10); + } +} + +// We are writing the fully-qualified name here to make sure, the Simplifier doesn't remove it, +// since this is nothing our code fix touches. +public class ScalableControl : System.Windows.Forms.Control +{ + private SizeF _scaleSize = new SizeF(3, 14); + + /// + /// Sets or gets the scaled size of some foo bar thing. + /// + [System.ComponentModel.Description("Sets or gets the scaled size of some foo bar thing.")] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public SizeF ScaledSize + { + get => _scaleSize; + set => _scaleSize = value; + } + + [DefaultValue(1.0f)] + public float ScaleFactor { get; set; } = 1.0f; + + /// + /// Sets or gets the scaled location of some foo bar thing. + /// + public PointF ScaledLocation { get; set; } + + private bool ShouldSerializeScaledLocation() => false; +} diff --git a/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/TestData/MissingPropertySerializationConfiguration/CodeFixTestCode.cs b/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/TestData/MissingPropertySerializationConfiguration/CodeFixTestCode.cs new file mode 100644 index 00000000000..fe4d55fb53e --- /dev/null +++ b/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/TestData/MissingPropertySerializationConfiguration/CodeFixTestCode.cs @@ -0,0 +1,39 @@ +namespace CSharpControls; + +public static class Program +{ + public static void Main() + { + var control = new ScalableControl(); + + // We deliberately format this weirdly, to make sure we only format code our code fix touches. + control.ScaleFactor = 1.5f; + control.ScaledSize = new SizeF(100, 100); + control.ScaledLocation = new PointF(10, 10); + } +} + +// We are writing the fully-qualified name here to make sure, the Simplifier doesn't remove it, +// since this is nothing our code fix touches. +public class ScalableControl : System.Windows.Forms.Control +{ + private SizeF _scaleSize = new SizeF(3, 14); + + /// + /// Sets or gets the scaled size of some foo bar thing. + /// + [System.ComponentModel.Description("Sets or gets the scaled size of some foo bar thing.")] + public SizeF [|ScaledSize|] + { + get => _scaleSize; + set => _scaleSize = value; + } + +public float [|ScaleFactor|] { get; set; } = 1.0f; + +/// +/// Sets or gets the scaled location of some foo bar thing. +/// +public PointF [|ScaledLocation|] +{ get; set; } + } diff --git a/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/TestData/MissingPropertySerializationConfiguration/FixedTestCode.cs b/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/TestData/MissingPropertySerializationConfiguration/FixedTestCode.cs new file mode 100644 index 00000000000..0033b72960d --- /dev/null +++ b/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/TestData/MissingPropertySerializationConfiguration/FixedTestCode.cs @@ -0,0 +1,43 @@ +using System.ComponentModel; + +namespace CSharpControls; + +public static class Program +{ + public static void Main() + { + var control = new ScalableControl(); + + // We deliberately format this weirdly, to make sure we only format code our code fix touches. + control.ScaleFactor = 1.5f; + control.ScaledSize = new SizeF(100, 100); + control.ScaledLocation = new PointF(10, 10); + } +} + +// We are writing the fully-qualified name here to make sure, the Simplifier doesn't remove it, +// since this is nothing our code fix touches. +public class ScalableControl : System.Windows.Forms.Control +{ + private SizeF _scaleSize = new SizeF(3, 14); + + /// + /// Sets or gets the scaled size of some foo bar thing. + /// + [System.ComponentModel.Description("Sets or gets the scaled size of some foo bar thing.")] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public SizeF ScaledSize + { + get => _scaleSize; + set => _scaleSize = value; + } + + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public float ScaleFactor { get; set; } = 1.0f; + + /// + /// Sets or gets the scaled location of some foo bar thing. + /// + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public PointF ScaledLocation { get; set; } +} diff --git a/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/TestData/MissingPropertySerializationConfiguration/GlobalUsing.cs b/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/TestData/MissingPropertySerializationConfiguration/GlobalUsing.cs new file mode 100644 index 00000000000..4147f6b0471 --- /dev/null +++ b/src/System.Windows.Forms.Analyzers.CSharp/tests/UnitTests/TestData/MissingPropertySerializationConfiguration/GlobalUsing.cs @@ -0,0 +1,2 @@ +global using System.Drawing; +global using System.Windows.Forms; diff --git a/src/System.Windows.Forms.Analyzers/tests/UnitTests/Microsoft/WinForms/AnalyzerTestPathAttribute.cs b/src/System.Windows.Forms.Analyzers/tests/UnitTests/Microsoft/WinForms/AnalyzerTestPathAttribute.cs new file mode 100644 index 00000000000..b08e5527338 --- /dev/null +++ b/src/System.Windows.Forms.Analyzers/tests/UnitTests/Microsoft/WinForms/AnalyzerTestPathAttribute.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.WinForms.Test; + +[AttributeUsage(AttributeTargets.Class)] +public class AnalyzerTestPathAttribute : Attribute +{ + public AnalyzerTestPathAttribute(string path) + { + Path = path; + } + + public string Path { get; } +} diff --git a/src/System.Windows.Forms.Analyzers/tests/UnitTests/Microsoft/WinForms/RoslynAnalyzerAndCodeFixTestBase.TestDataFileSet.cs b/src/System.Windows.Forms.Analyzers/tests/UnitTests/Microsoft/WinForms/RoslynAnalyzerAndCodeFixTestBase.TestDataFileSet.cs new file mode 100644 index 00000000000..7e4c104cd98 --- /dev/null +++ b/src/System.Windows.Forms.Analyzers/tests/UnitTests/Microsoft/WinForms/RoslynAnalyzerAndCodeFixTestBase.TestDataFileSet.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; + +namespace Microsoft.WinForms.Test; + +public abstract partial class RoslynAnalyzerAndCodeFixTestBase + where TAnalyzer : DiagnosticAnalyzer, new() + where TVerifier : IVerifier, new() +{ + public class TestDataFileSet + { + public string AnalyzerTestCode { get; set; } = string.Empty; + public string CodeFixTestCode { get; set; } = string.Empty; + public string FixedTestCode { get; set; } = string.Empty; + public string GlobalUsing { get; set; } = string.Empty; + public List AdditionalCodeFiles { get; set; } = []; + } +} diff --git a/src/System.Windows.Forms.Analyzers/tests/UnitTests/Microsoft/WinForms/RoslynAnalyzerAndCodeFixTestBase.cs b/src/System.Windows.Forms.Analyzers/tests/UnitTests/Microsoft/WinForms/RoslynAnalyzerAndCodeFixTestBase.cs new file mode 100644 index 00000000000..7ca50f6f574 --- /dev/null +++ b/src/System.Windows.Forms.Analyzers/tests/UnitTests/Microsoft/WinForms/RoslynAnalyzerAndCodeFixTestBase.cs @@ -0,0 +1,312 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; + +namespace Microsoft.WinForms.Test; + +/// +/// Provides a base class for leveraging Roslyn analyzers and code fixes within unit tests. +/// +/// +/// +/// This class is responsible for discovering test files likewise used by Roslyn analyzer tests +/// and automatically fetching them into the test context. Its methods handle reading content +/// from physical files, grouping them, and preparing them for valid test scenarios. +/// +/// Usage: +/// +/// Inherit from this base class, specifying the analyzer and verifier types. +/// Call or override provided methods to customize how the test data is used in your test scenarios. +/// +/// The class seamlessly integrates with +/// and to configure test expectations and references. +/// +/// +/// +/// The analyzer under test. +/// The type that verifies the analyzer's output. +public abstract partial class RoslynAnalyzerAndCodeFixTestBase + where TAnalyzer : DiagnosticAnalyzer, new() + where TVerifier : IVerifier, new() +{ + private readonly IEnumerable _analyzerTestFilePaths; + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// The constructor extracts all valid test file paths from the specified . + /// + /// + /// This path is automatically populated using the caller file path. + /// Ensure that test files follow the naming and directory conventions so they can be discovered accurately. + /// + /// + /// The path containing the analyzer/fixture test files. + protected RoslynAnalyzerAndCodeFixTestBase([CallerFilePath] string? testBasePath = default) + { + ArgumentNullException.ThrowIfNull(testBasePath); + _analyzerTestFilePaths = TestFileLoader.GetTestFilePaths(GetType(), testBasePath); + } + + /// + /// Loads a matching set of test file content from a specified location. + /// + /// + /// + /// This method scans the target path for analyzer test files, code-fix files, and any necessary + /// additional documents, then bundles them into a . This combination + /// of files can be used by the test context to run analyzer or code-fix tests thoroughly. + /// + /// + /// The path to search for test files. + /// A holding the contents of all discovered files. + public async Task GetTestDataFileSetAsync(string path) + { + TestDataFileSet testDataFileSet = new(); + + foreach (var fileItem in TestFileLoader.EnumerateEntries(path)) + { + string currentDocument = await TestFileLoader.LoadTestFileAsync(fileItem.FilePath) + .ConfigureAwait(false); + + switch (fileItem.FileType) + { + case TestFileType.AnalyzerTestCode: + testDataFileSet.AnalyzerTestCode = currentDocument; + break; + case TestFileType.CodeFixTestCode: + testDataFileSet.CodeFixTestCode = currentDocument; + break; + case TestFileType.FixedCode: + testDataFileSet.FixedTestCode = currentDocument; + break; + case TestFileType.GlobalUsing: + testDataFileSet.GlobalUsing = currentDocument; + break; + case TestFileType.AdditionalCodeFile: + testDataFileSet.AdditionalCodeFiles.Add(currentDocument); + break; + } + } + + return testDataFileSet; + } + + /// + /// Asynchronously enumerates all discovered test files and executes the specified test delegate. + /// + /// + /// + /// This method is a convenient way to apply the same test logic over multiple sets of test data. + /// It loops over the discovered file paths, obtains a for each, + /// and invokes your specified with it. + /// + /// + /// The test method accepting test file data that will be evaluated. + /// A task representing the asynchronous operation. + protected Task EnumerateTestFilesAsync(Func testMethod) + { + return Task.WhenAll(_analyzerTestFilePaths.Select(async path => + { + TestDataFileSet testDataFileSet = await GetTestDataFileSetAsync(path) + .ConfigureAwait(false); + + await testMethod.Invoke(testDataFileSet) + .ConfigureAwait(false); + })); + } + + /// + /// Creates a test context configured with the expected fixed solution for code-fix tests. + /// + /// + /// + /// This method is intended to provide a ready-to-run + /// instance containing the changes specified in the 'FixedTestCode'. + /// + /// + /// Use it in situations where you need to verify that a code fix successfully transforms + /// the original test code into the desired corrected form. + /// + /// + /// Holds various file contents used for the test. + /// References needed by the test code's compilation. + /// Optional caller member name for diagnostic context. + /// A test context prepared with the fixed code, references, and state. + protected CSharpAnalyzerTest GetFixedTestContext( + TestDataFileSet fileSet, + ReferenceAssemblies referenceAssemblies, + [CallerMemberName] string? memberName = default) + => GetTestContext( + fileSet.FixedTestCode, + fileSet.GlobalUsing, + fileSet.AdditionalCodeFiles, + referenceAssemblies, + memberName); + + /// + /// Creates a test context for the analyzer scenario using unaltered test code. + /// + /// + /// + /// This method sets up a that includes + /// the 'AnalyzerTestCode' content. It does not include any fixed code. + /// + /// + /// This is useful for discovering which diagnostics might appear in the given file set + /// before any code-fix is applied. + /// + /// + /// Holds various file contents used for the test. + /// References needed by the test code's compilation. + /// Optional caller member name for diagnostic context. + /// A test context that can verify diagnostics raised by the analyzer. + protected CSharpAnalyzerTest GetAnalyzerTestContext( + TestDataFileSet fileSet, + ReferenceAssemblies referenceAssemblies, + [CallerMemberName] string? memberName = default) + => GetTestContext( + fileSet.AnalyzerTestCode, + fileSet.GlobalUsing, + fileSet.AdditionalCodeFiles, + referenceAssemblies, + memberName); + + /// + /// Generates a basic + /// using the provided test code and references. + /// + /// + /// + /// This helper method creates a base test context and configures the + /// output kind to be a Windows application. + /// + /// + /// It automatically adds the specified statements and any additional + /// context documents. Helpful for verifying code diagnostics in multiple scenarios without rewriting test scaffolding. + /// + /// + /// The main source content to analyze. + /// Global using directives to include in the test environment. + /// Additional source documents relevant to the test scenario. + /// References needed by the test code's compilation. + /// Optional caller member name for context or logging. + /// A configured instance. + /// + /// Thrown if the main is null or empty. + /// + protected CSharpAnalyzerTest GetTestContext( + string fileContent, + string globalUsing, + IEnumerable contextDocuments, + ReferenceAssemblies referenceAssemblies, + [CallerMemberName] string? memberName = default) + { + if (string.IsNullOrEmpty(fileContent)) + { + throw new ArgumentException( + $"Test method '{memberName}' passed a test file without content."); + } + + CSharpAnalyzerTest context + = new CSharpAnalyzerTest + { + TestCode = fileContent, + TestState = + { + OutputKind = OutputKind.WindowsApplication, + }, + ReferenceAssemblies = referenceAssemblies + }; + + if (globalUsing is not null) + { + context.TestState.Sources.Add(globalUsing); + } + + if (contextDocuments is not null) + { + foreach (string contextDocument in contextDocuments) + { + context.TestState.Sources.Add(contextDocument); + } + } + + return context; + } + + /// + /// Creates a code-fix test context for applying a + /// to the original and verifying the transformed result. + /// + /// + /// + /// This method provides the scaffolding to test how modifies + /// the source code. It loads the initial test code (which includes expected diagnostic spans) + /// and sets the 'FixedCode' property to compare against the desired result. It also configures + /// the number of 'Fix All' iterations allowed in one document. + /// + /// + /// The code fix provider to test. + /// Holds various file contents used for the test scenario. + /// References needed by the test code's compilation. + /// The number of 'Fix All' operations to apply for the test. + /// Optional caller member name for context or logging. + /// + /// A initialized for code-fix testing. + /// + /// + /// Thrown if the expected 'CodeFixTestCode' or 'FixedTestCode' cannot be found in the . + /// + protected CSharpCodeFixTest GetCodeFixTestContext( + TestDataFileSet fileSet, + ReferenceAssemblies referenceAssemblies, + int numberOfFixAllIterations, + [CallerMemberName] string? memberName = default) + where TCodeFix : CodeFixProvider, new() + { + CSharpCodeFixTest context = new CSharpCodeFixTest + { + TestCode = fileSet.CodeFixTestCode + ?? throw new ArgumentException( + $"Test method '{memberName}' expected the test file " + + $"'CodeFixTestCode.cs' which could not be found."), + + FixedCode = fileSet.FixedTestCode + ?? throw new ArgumentException( + $"Test method '{memberName}' expected the test file " + + $"'FixedTestCode.cs' which could not be found."), + + TestState = + { + OutputKind = OutputKind.WindowsApplication, + }, + + ReferenceAssemblies = referenceAssemblies, + NumberOfFixAllInDocumentIterations = numberOfFixAllIterations + }; + + if (fileSet.GlobalUsing is not null) + { + context.TestState.Sources.Add(fileSet.GlobalUsing); + } + + if (fileSet.AdditionalCodeFiles is not null) + { + foreach (string contextDocument in fileSet.AdditionalCodeFiles) + { + context.TestState.Sources.Add(contextDocument); + } + } + + return context; + } +} diff --git a/src/System.Windows.Forms.Analyzers/tests/UnitTests/Microsoft/WinForms/SourceLanguage.cs b/src/System.Windows.Forms.Analyzers/tests/UnitTests/Microsoft/WinForms/SourceLanguage.cs new file mode 100644 index 00000000000..b9a18978324 --- /dev/null +++ b/src/System.Windows.Forms.Analyzers/tests/UnitTests/Microsoft/WinForms/SourceLanguage.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; + +namespace Microsoft.WinForms.Utilities.Shared; + +// Note: This is marked as EditorBrowsableState.Never to keep it from appearing in +// the completion list for users of the SDK. However, it will still appear in the +// completion list within the designer repo because of a C# editor feature that always +// shows symbols defined in source, regardless of EditorBrowsableState. + +[EditorBrowsable(EditorBrowsableState.Never)] +public enum SourceLanguage +{ + None, + CSharp, + VisualBasic +} diff --git a/src/System.Windows.Forms.Analyzers/tests/UnitTests/Microsoft/WinForms/TestFileEntry.cs b/src/System.Windows.Forms.Analyzers/tests/UnitTests/Microsoft/WinForms/TestFileEntry.cs new file mode 100644 index 00000000000..220e0145fd6 --- /dev/null +++ b/src/System.Windows.Forms.Analyzers/tests/UnitTests/Microsoft/WinForms/TestFileEntry.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.WinForms.Test; + +/// +/// Represents an entry for a test file. +/// +public class TestFileEntry +{ + /// + /// Initializes a new instance of the class. + /// + /// The file path of the test file. + /// The type of the test file. + public TestFileEntry(string filePath, TestFileType fileType) + { + FilePath = filePath; + FileType = fileType; + } + + /// + /// Gets the file path of the test file. + /// + public string FilePath { get; } + + /// + /// Gets the type of the test file. + /// + public TestFileType FileType { get; } +} diff --git a/src/System.Windows.Forms.Analyzers/tests/UnitTests/Microsoft/WinForms/TestFileLoader.cs b/src/System.Windows.Forms.Analyzers/tests/UnitTests/Microsoft/WinForms/TestFileLoader.cs new file mode 100644 index 00000000000..c77311a7055 --- /dev/null +++ b/src/System.Windows.Forms.Analyzers/tests/UnitTests/Microsoft/WinForms/TestFileLoader.cs @@ -0,0 +1,136 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO.Enumeration; +using System.Reflection; +using System.Text; + +namespace Microsoft.WinForms.Test; + +/// +/// Utility that handles loading of test files from a folder called 'TestData'. +/// +public static class TestFileLoader +{ + private const string TestData = nameof(TestData); + + private const string AnalyzerTestCode = nameof(TestFileType.AnalyzerTestCode); + private const string CodeFixTestCode = nameof(TestFileType.CodeFixTestCode); + private const string FixedCode = nameof(TestFileType.FixedCode); + private const string GlobalUsing = nameof(TestFileType.GlobalUsing); + + /// + /// Enumerates the test file entries in the specified base path. + /// + /// The base path to enumerate. + /// The file attributes to exclude. + /// An enumerable collection of test file entries. + /// Thrown when the base path is null or empty. + public static IEnumerable EnumerateEntries( + string basePath, + FileAttributes excludeAttributes = default) + { + if (string.IsNullOrEmpty(basePath)) + { + throw new ArgumentException("The base path must be a valid directory.", nameof(basePath)); + } + + var enumOptions = new EnumerationOptions + { + RecurseSubdirectories = false + }; + + return EnumerateEntriesInternal(basePath, enumOptions, excludeAttributes); + + IEnumerable EnumerateEntriesInternal( + string basePath, + EnumerationOptions enumOptions, + FileAttributes excludeAttributes) + { + FileSystemEnumerable enumeration = new FileSystemEnumerable( + directory: basePath, + transform: (ref FileSystemEntry entry) => + { + TestFileType fileType = Path.GetFileName(basePath) switch + { + AnalyzerTestCode => TestFileType.AnalyzerTestCode, + CodeFixTestCode => TestFileType.CodeFixTestCode, + FixedCode => TestFileType.FixedCode, + GlobalUsing => TestFileType.GlobalUsing, + _ => TestFileType.AdditionalCodeFile + }; + + return new TestFileEntry(entry.ToFullPath(), fileType); + }, + options: enumOptions); + + foreach (var fileEntry in enumeration) + { + yield return fileEntry; + } + } + } + + /// + /// Asynchronously loads the content of a test file. + /// + /// The path of the test file to load. + /// A task that represents the asynchronous operation. The task result contains the content of the test file. + public static async Task LoadTestFileAsync(string testFilePath) + { + using var reader = new StreamReader(testFilePath, Encoding.UTF8); + + return await reader.ReadToEndAsync().ConfigureAwait(false); + } + + /// + /// Gets the test file paths for the specified analyzer type. + /// + /// The type of the analyzer. + /// The additional path to include in the test file path. + /// The test file path. + public static IEnumerable GetTestFilePaths( + Type analyzerType, + string basePath) + { + if (string.IsNullOrWhiteSpace(basePath)) + { + throw new ArgumentException("The base path must be a valid directory.", nameof(basePath)); + } + + // Let's see if the class defines an AnalyzerTestPathAttribute: + AnalyzerTestPathAttribute? analyzerTestPathAttribute = analyzerType.GetCustomAttribute(); + + string analyzerPath = analyzerTestPathAttribute is not null + ? analyzerTestPathAttribute.Path + : TestData; + + var builder = new StringBuilder(); + + builder.Append($"{basePath}\\{analyzerPath}"); + builder.Append('\\'); + + analyzerPath = builder.ToString(); + + List analyzerTestDataPaths; + + // Now let's see, if we have subdirectories for the analyzer test data: + if (Directory.Exists(analyzerPath)) + { + analyzerTestDataPaths = [.. Directory.EnumerateDirectories(analyzerPath)]; + + // If we have subdirectories AND test data files, we throw. So, let's see if we got files in the directory: + foreach (var directory in Directory.EnumerateFiles(analyzerPath)) + { + throw new InvalidOperationException($"The directory '{analyzerPath}' contains files. It should only contain subdirectories."); + } + + return analyzerTestDataPaths; + } + + analyzerTestDataPaths = []; + analyzerTestDataPaths.Add(analyzerPath); + + return analyzerTestDataPaths; + } +} diff --git a/src/System.Windows.Forms.Analyzers/tests/UnitTests/Microsoft/WinForms/TestFileType.cs b/src/System.Windows.Forms.Analyzers/tests/UnitTests/Microsoft/WinForms/TestFileType.cs new file mode 100644 index 00000000000..f6c6b0af0d6 --- /dev/null +++ b/src/System.Windows.Forms.Analyzers/tests/UnitTests/Microsoft/WinForms/TestFileType.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.WinForms.Test; + +/// +/// Specifies the type of test file. +/// +public enum TestFileType +{ + /// + /// Analyzer test code file. + /// + AnalyzerTestCode, + + /// + /// Code fix test code file. + /// + CodeFixTestCode, + + /// + /// Fixed code file. + /// + FixedCode, + + /// + /// Global using file. + /// + GlobalUsing, + + /// + /// Additional code file. + /// + AdditionalCodeFile +} diff --git a/src/System.Windows.Forms.Analyzers/tests/UnitTests/System.Windows.Forms.Analyzers.Tests.csproj b/src/System.Windows.Forms.Analyzers/tests/UnitTests/System.Windows.Forms.Analyzers.Tests.csproj index 781b7a39230..758d16251b9 100644 --- a/src/System.Windows.Forms.Analyzers/tests/UnitTests/System.Windows.Forms.Analyzers.Tests.csproj +++ b/src/System.Windows.Forms.Analyzers/tests/UnitTests/System.Windows.Forms.Analyzers.Tests.csproj @@ -19,6 +19,7 @@ + @@ -27,6 +28,12 @@ + + + d:\nuget\system.codedom\10.0.0-alpha.1.25065.17\lib\net10.0\System.CodeDom.dll + + + Always diff --git a/src/System.Windows.Forms.Analyzers/tests/UnitTests/System/Windows/Forms/Analyzers/AppManifestAnalyzerTests.cs b/src/System.Windows.Forms.Analyzers/tests/UnitTests/System/Windows/Forms/Analyzers/AppManifestAnalyzerTests.cs index 2832d5abefd..add72157022 100644 --- a/src/System.Windows.Forms.Analyzers/tests/UnitTests/System/Windows/Forms/Analyzers/AppManifestAnalyzerTests.cs +++ b/src/System.Windows.Forms.Analyzers/tests/UnitTests/System/Windows/Forms/Analyzers/AppManifestAnalyzerTests.cs @@ -6,7 +6,6 @@ using Microsoft.CodeAnalysis.Testing; using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.VisualBasic.Testing; -using Xunit; namespace System.Windows.Forms.Analyzers.Tests; diff --git a/src/System.Windows.Forms.Analyzers/tests/UnitTests/System/Windows/Forms/Analyzers/ApplicationConfigTests.FontDescriptor.cs b/src/System.Windows.Forms.Analyzers/tests/UnitTests/System/Windows/Forms/Analyzers/ApplicationConfigTests.FontDescriptor.cs index e605c0021e6..dfbe7d059dd 100644 --- a/src/System.Windows.Forms.Analyzers/tests/UnitTests/System/Windows/Forms/Analyzers/ApplicationConfigTests.FontDescriptor.cs +++ b/src/System.Windows.Forms.Analyzers/tests/UnitTests/System/Windows/Forms/Analyzers/ApplicationConfigTests.FontDescriptor.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; -using Xunit; using Xunit.Abstractions; using static System.Windows.Forms.Analyzers.ApplicationConfig; diff --git a/src/System.Windows.Forms.Analyzers/tests/UnitTests/System/Windows/Forms/Analyzers/ApplicationConfigTests.FontStyle.cs b/src/System.Windows.Forms.Analyzers/tests/UnitTests/System/Windows/Forms/Analyzers/ApplicationConfigTests.FontStyle.cs index 6d7dd0c1cdf..1cfcf47584a 100644 --- a/src/System.Windows.Forms.Analyzers/tests/UnitTests/System/Windows/Forms/Analyzers/ApplicationConfigTests.FontStyle.cs +++ b/src/System.Windows.Forms.Analyzers/tests/UnitTests/System/Windows/Forms/Analyzers/ApplicationConfigTests.FontStyle.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Xunit; using static System.Windows.Forms.Analyzers.ApplicationConfig; namespace System.Windows.Forms.Analyzers.Tests; diff --git a/src/System.Windows.Forms.Analyzers/tests/UnitTests/System/Windows/Forms/Analyzers/ApplicationConfigTests.GraphicsUnit.cs b/src/System.Windows.Forms.Analyzers/tests/UnitTests/System/Windows/Forms/Analyzers/ApplicationConfigTests.GraphicsUnit.cs index a0be6ece1db..d8417c3807c 100644 --- a/src/System.Windows.Forms.Analyzers/tests/UnitTests/System/Windows/Forms/Analyzers/ApplicationConfigTests.GraphicsUnit.cs +++ b/src/System.Windows.Forms.Analyzers/tests/UnitTests/System/Windows/Forms/Analyzers/ApplicationConfigTests.GraphicsUnit.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Xunit; using static System.Windows.Forms.Analyzers.ApplicationConfig; namespace System.Windows.Forms.Analyzers.Tests;