diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index da8fa3eb6d..21096535da 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -8,7 +8,7 @@ The codebase ships several distinct (but related) products. Knowing which produc - `src/Platform/Microsoft.Testing.Platform` — Microsoft.Testing.Platform (MTP), a lightweight, in-process test host that replaces VSTest. Most other folders under `src/Platform/` are MTP extensions (`TrxReport`, `CrashDump`, `HangDump`, `HotReload`, `Retry`, `Telemetry`, `HtmlReport`, `AzureDevOpsReport`, `MSBuild`, `VSTestBridge`, …). - `src/TestFramework` — MSTest itself: the public `Microsoft.VisualStudio.TestTools.UnitTesting` API (attributes, `Assert`, `TestContext`, …) plus `TestFramework.Extensions`. -- `src/Adapter` — bridges MSTest to test hosts: `MSTest.TestAdapter` (VSTest adapter), `MSTestAdapter.PlatformServices` (platform-services abstraction shared by both hosts), and `MSTest.Engine` (MTP-native execution engine used by source-generated tests). +- `src/Adapter` — bridges MSTest to test hosts: `MSTest.TestAdapter` (VSTest adapter) and `MSTestAdapter.PlatformServices` (platform-services abstraction shared by both hosts). - `src/Analyzers` — Roslyn analyzers and code fixes shipped as `MSTest.Analyzers`. - `src/Package/MSTest.Sdk` — the MSBuild project SDK that wires the pieces together for consumers. - `test/UnitTests/.UnitTests` — fast unit tests for each project. diff --git a/.github/workflows/add-tests.md b/.github/workflows/add-tests.md index 97fa7d8369..18a41f30ec 100644 --- a/.github/workflows/add-tests.md +++ b/.github/workflows/add-tests.md @@ -68,7 +68,6 @@ Analyze the pull request diff to identify source files that were added or modifi - `src/TestFramework/` → `test/UnitTests/TestFramework.UnitTests/` - `src/Adapter/MSTest.TestAdapter/` → `test/UnitTests/MSTestAdapter.UnitTests/` - `src/Adapter/MSTestAdapter.PlatformServices/` → `test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/` - - `src/Adapter/MSTest.Engine/` → `test/UnitTests/MSTest.Engine.UnitTests/` - `src/Analyzers/MSTest.Analyzers/` → `test/UnitTests/MSTest.Analyzers.Tests/` (if exists) - `src/Analyzers/MSTest.SourceGeneration/` → `test/UnitTests/MSTest.SourceGeneration.UnitTests/` - `src/Platform/` → `test/UnitTests/` (find matching test project by name) diff --git a/Directory.Build.props b/Directory.Build.props index fe3901dfe7..ce51a5be1e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -50,12 +50,12 @@ - alpha + alpha - 2.0.0 + 2.0.0 diff --git a/MSTest.slnf b/MSTest.slnf index 357d59f90a..3c3f4c624a 100644 --- a/MSTest.slnf +++ b/MSTest.slnf @@ -6,7 +6,6 @@ "samples\\FxExtensibility\\FxExtensibility.csproj", "samples\\NUnitPlayground\\NUnitPlayground.csproj", "samples\\Playground\\Playground.csproj", - "src\\Adapter\\MSTest.Engine\\MSTest.Engine.csproj", "src\\Adapter\\MSTest.TestAdapter\\MSTest.TestAdapter.csproj", "src\\Adapter\\MSTestAdapter.PlatformServices\\MSTestAdapter.PlatformServices.csproj", "src\\Analyzers\\MSTest.Analyzers.CodeFixes\\MSTest.Analyzers.CodeFixes.csproj", diff --git a/NonWindowsTests.slnf b/NonWindowsTests.slnf index 30129ee81a..babc740760 100644 --- a/NonWindowsTests.slnf +++ b/NonWindowsTests.slnf @@ -2,7 +2,6 @@ "solution": { "path": "TestFx.slnx", "projects": [ - "src\\Adapter\\MSTest.Engine\\MSTest.Engine.csproj", "src\\Adapter\\MSTest.TestAdapter\\MSTest.TestAdapter.csproj", "src\\Adapter\\MSTestAdapter.PlatformServices\\MSTestAdapter.PlatformServices.csproj", "src\\Analyzers\\MSTest.Analyzers.CodeFixes\\MSTest.Analyzers.CodeFixes.csproj", diff --git a/TestFx.slnx b/TestFx.slnx index 09a883489c..68f70e5c5f 100644 --- a/TestFx.slnx +++ b/TestFx.slnx @@ -55,7 +55,6 @@ - @@ -133,7 +132,6 @@ - diff --git a/samples/public/DemoMSTestSdk/ProjectWithNativeAOT/ProjectWithNativeAOT.csproj b/samples/public/DemoMSTestSdk/ProjectWithNativeAOT/ProjectWithNativeAOT.csproj index cdb413b816..ccdbef0b88 100644 --- a/samples/public/DemoMSTestSdk/ProjectWithNativeAOT/ProjectWithNativeAOT.csproj +++ b/samples/public/DemoMSTestSdk/ProjectWithNativeAOT/ProjectWithNativeAOT.csproj @@ -36,8 +36,8 @@ Below is the equivalent project configuration when not using MSTest.Sdk - + diff --git a/samples/public/mstest-runner/NativeAotRunner/TestProject1/TestProject1.csproj b/samples/public/mstest-runner/NativeAotRunner/TestProject1/TestProject1.csproj index 89b1a96a9f..bb0f5e9ff0 100644 --- a/samples/public/mstest-runner/NativeAotRunner/TestProject1/TestProject1.csproj +++ b/samples/public/mstest-runner/NativeAotRunner/TestProject1/TestProject1.csproj @@ -11,11 +11,9 @@ - @@ -23,6 +21,7 @@ + all diff --git a/src/Adapter/MSTest.Engine/.editorconfig b/src/Adapter/MSTest.Engine/.editorconfig deleted file mode 100644 index 7d7bac49f4..0000000000 --- a/src/Adapter/MSTest.Engine/.editorconfig +++ /dev/null @@ -1,2 +0,0 @@ -[*.{cs,vb}] -file_header_template = Copyright (c) Microsoft Corporation. All rights reserved.\nLicensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. diff --git a/src/Adapter/MSTest.Engine/Assertions/AssertFailedException.cs b/src/Adapter/MSTest.Engine/Assertions/AssertFailedException.cs deleted file mode 100644 index cad313aaad..0000000000 --- a/src/Adapter/MSTest.Engine/Assertions/AssertFailedException.cs +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using System.Runtime.Serialization; - -namespace Microsoft.Testing.Framework; - -/// -/// AssertFailedException class. Used to indicate failure for a test case. -/// -[Serializable] -public sealed class AssertFailedException : Exception -{ - /// - /// Creates AssertFailedException with a given message and metadata. - /// - /// Message to be reported to the user. - /// AssertFailedException. - internal static AssertFailedException Create(string message) => CreateInternal(message, expected: null, actual: null); - - /// - /// Creates AssertFailedException with a given message and metadata. - /// - /// Message to be reported to the user, it may, or may not include the values that were compared. If values are not included provide them to 'expected' and 'actual'. - /// The expected value, when that value is complex, and diffing it to actual makes it easier for user to see the difference. - /// The actual value, when that value is complex, and diffing it to expected makes it easier for user to see the difference. - /// AssertFailedException. - internal static AssertFailedException Create(string message, string expected, string actual) => CreateInternal(message, expected, actual); - - private static AssertFailedException CreateInternal(string message, string? expected, string? actual) - { -#pragma warning disable SYSLIB0051 // Type or member is obsolete - var ex = new AssertFailedException(message); -#pragma warning restore SYSLIB0051 // Type or member is obsolete - - if (expected != null) - { - ex.Data["assert.expected"] = expected; - } - - if (actual != null) - { - ex.Data["assert.actual"] = actual; - } - - return ex; - } - - /// - /// Initializes a new instance of the class. - /// -#if NET8_0_OR_GREATER - [Obsolete("Use Create instead", DiagnosticId = "SYSLIB0051")] -#endif - public AssertFailedException() - : base() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The exception message. -#if NET8_0_OR_GREATER - [Obsolete("Use Create instead", DiagnosticId = "SYSLIB0051")] -#endif - public AssertFailedException(string message) - : base(message) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The exception message. - /// The inner exception. -#if NET8_0_OR_GREATER - [Obsolete("Use Create instead", DiagnosticId = "SYSLIB0051")] -#endif - public AssertFailedException(string message, Exception ex) - : base(message, ex) - { - } - -#if NET8_0_OR_GREATER - [Obsolete(DiagnosticId = "SYSLIB0051")] -#endif - private AssertFailedException(SerializationInfo serializationInfo, StreamingContext streamingContext) - { - } -} diff --git a/src/Adapter/MSTest.Engine/BannedSymbols.txt b/src/Adapter/MSTest.Engine/BannedSymbols.txt deleted file mode 100644 index f7256d3c54..0000000000 --- a/src/Adapter/MSTest.Engine/BannedSymbols.txt +++ /dev/null @@ -1,6 +0,0 @@ -M:System.Threading.Tasks.Task.Run(System.Action,System.Threading.CancellationToken); Use 'ITask' instead -M:System.Threading.Tasks.Task.Run(System.Func{System.Threading.Tasks.Task},System.Threading.CancellationToken); Use 'ITask' instead -M:System.String.IsNullOrEmpty(System.String); Use 'RoslynString.IsNullOrEmpty' instead -M:System.String.IsNullOrWhiteSpace(System.String); Use 'RoslynString.IsNullOrWhiteSpace' instead -M:System.Diagnostics.Debug.Assert(System.Boolean); Use 'RoslynDebug.Assert' instead -M:System.Diagnostics.Debug.Assert(System.Boolean,System.String); Use 'RoslynDebug.Assert' instead diff --git a/src/Adapter/MSTest.Engine/BuildInfo.cs.template b/src/Adapter/MSTest.Engine/BuildInfo.cs.template deleted file mode 100644 index a874326017..0000000000 --- a/src/Adapter/MSTest.Engine/BuildInfo.cs.template +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -/// -/// Repository version, created at build time. -/// -internal static class MSTestEngineRepositoryVersion -{ - public const string Version = "${Version}"; -} diff --git a/src/Adapter/MSTest.Engine/Configurations/ConfigurationExtensions.cs b/src/Adapter/MSTest.Engine/Configurations/ConfigurationExtensions.cs deleted file mode 100644 index d0111e1fd6..0000000000 --- a/src/Adapter/MSTest.Engine/Configurations/ConfigurationExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Platform.Configurations; - -namespace Microsoft.Testing.Framework.Configurations; - -public static class ConfigurationExtensions -{ - public static string GetTestResultDirectory(this IConfiguration configuration) - => GetConfigurationWrapper(configuration).WrappedConfiguration.GetTestResultDirectory(); - - private static ConfigurationWrapper GetConfigurationWrapper(IConfiguration configuration) - => configuration as ConfigurationWrapper - ?? throw new ArgumentException("Current configuration is not of expected type", nameof(configuration)); -} diff --git a/src/Adapter/MSTest.Engine/Configurations/ConfigurationWrapper.cs b/src/Adapter/MSTest.Engine/Configurations/ConfigurationWrapper.cs deleted file mode 100644 index 3255bb7175..0000000000 --- a/src/Adapter/MSTest.Engine/Configurations/ConfigurationWrapper.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using PlatformIConfiguration = Microsoft.Testing.Platform.Configurations.IConfiguration; - -namespace Microsoft.Testing.Framework.Configurations; - -/// -/// Wraps a platform IConfiguration instance. This is preferable as it allows us to avoid being dependent -/// on Platform if we need some specific API change. -/// -internal sealed class ConfigurationWrapper : IConfiguration -{ - public ConfigurationWrapper(PlatformIConfiguration configuration) - => WrappedConfiguration = configuration; - - internal PlatformIConfiguration WrappedConfiguration { get; } - - public string? this[string key] => WrappedConfiguration[key]; -} diff --git a/src/Adapter/MSTest.Engine/Configurations/IConfiguration.cs b/src/Adapter/MSTest.Engine/Configurations/IConfiguration.cs deleted file mode 100644 index 9eef90eb15..0000000000 --- a/src/Adapter/MSTest.Engine/Configurations/IConfiguration.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework.Configurations; - -public interface IConfiguration -{ - string? this[string key] { get; } -} diff --git a/src/Adapter/MSTest.Engine/Configurations/TestFrameworkConfiguration.cs b/src/Adapter/MSTest.Engine/Configurations/TestFrameworkConfiguration.cs deleted file mode 100644 index ac307a5238..0000000000 --- a/src/Adapter/MSTest.Engine/Configurations/TestFrameworkConfiguration.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework.Configurations; - -public sealed class TestFrameworkConfiguration(int maxParallelTests = int.MaxValue) -{ - public int MaxParallelTests { get; } = maxParallelTests; -} diff --git a/src/Adapter/MSTest.Engine/Engine/BFSTestNodeVisitor.cs b/src/Adapter/MSTest.Engine/Engine/BFSTestNodeVisitor.cs deleted file mode 100644 index 65911e9565..0000000000 --- a/src/Adapter/MSTest.Engine/Engine/BFSTestNodeVisitor.cs +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using System.Web; - -using Microsoft.Testing.Platform.Extensions.Messages; -using Microsoft.Testing.Platform.Requests; - -namespace Microsoft.Testing.Framework; - -internal sealed class BFSTestNodeVisitor -{ - private static readonly string PathSeparatorString = TreeNodeFilter.PathSeparator.ToString(); - - // Read-only; must never be mutated. Shared across all BFS traversals where ContainsPropertyFilters is false. - private static readonly PropertyBag EmptyPropertyBag = new(); - - private readonly IEnumerable _rootTestNodes; - private readonly ITestExecutionFilter _testExecutionFilter; - private readonly TestArgumentsManager _testArgumentsManager; - - public BFSTestNodeVisitor(IEnumerable rootTestNodes, ITestExecutionFilter testExecutionFilter, - TestArgumentsManager testArgumentsManager) - { - if (testExecutionFilter is not TreeNodeFilter and not TestNodeUidListFilter and not NopFilter) - { - throw new ArgumentOutOfRangeException(nameof(testExecutionFilter)); - } - - _rootTestNodes = rootTestNodes; - _testExecutionFilter = testExecutionFilter; - _testArgumentsManager = testArgumentsManager; - } - - internal KeyValuePair>[] DuplicatedNodes { get; private set; } = []; - - public async Task VisitAsync(Func onIncludedTestNodeAsync) - { - // Precompute a HashSet for O(1) UID lookups when filtering by UID list. - // Using string values directly avoids allocating a wrapper for each lookup. - HashSet? uidFilterSet = _testExecutionFilter is TestNodeUidListFilter listFilter - ? new HashSet(listFilter.TestNodeUids.Select(static uid => uid.Value), StringComparer.Ordinal) - : null; - - // This is case sensitive, and culture insensitive, to keep UIDs unique, and comparable between different system. - Dictionary> testNodesByUid = []; - - // Use string (instead of StringBuilder) in the queue to avoid allocating one StringBuilder per node. - // Each node's full path is an immutable string that can be shared as the base for child paths. - Queue<(TestNode CurrentNode, TestNodeUid? ParentNodeUid, string NodeFullPath)> queue = new(); - foreach (TestNode node in _rootTestNodes) - { - queue.Enqueue((node, null, string.Empty)); - } - - while (queue.Count > 0) - { - (TestNode currentNode, TestNodeUid? parentNodeUid, string nodeFullPath) = queue.Dequeue(); - - if (!testNodesByUid.TryGetValue(currentNode.StableUid, out List? testNodes)) - { - testNodes = []; - testNodesByUid.Add(currentNode.StableUid, testNodes); - } - - testNodes.Add(currentNode); - - // We want to encode the path fragment to avoid conflicts with the separator. We are using URL encoding because it is - // a well-known proven standard encoding that is reversible. - string encodedName = EncodeString(currentNode.OverriddenEdgeName ?? currentNode.DisplayName); - string currentNodeFullPath = nodeFullPath.Length == 0 || nodeFullPath[^1] != TreeNodeFilter.PathSeparator - ? string.Concat(nodeFullPath, PathSeparatorString, encodedName) - : string.Concat(nodeFullPath, encodedName); - - // When we are filtering as tree filter and the current node does not match the filter, we skip the node and its children. - if (_testExecutionFilter is TreeNodeFilter treeNodeFilter) - { - PropertyBag filterPropertyBag = treeNodeFilter.ContainsPropertyFilters - ? new PropertyBag(currentNode.Properties) - : EmptyPropertyBag; - if (!treeNodeFilter.MatchesFilter(currentNodeFullPath, filterPropertyBag)) - { - continue; - } - } - - // If the node is expandable, we expand it (replacing the original node) - if (TestArgumentsManager.IsExpandableTestNode(currentNode)) - { - currentNode = await _testArgumentsManager.ExpandTestNodeAsync(currentNode).ConfigureAwait(false); - } - - // If the node is not filtered out by the test execution filter, we call the callback with the node. - if (uidFilterSet is null - || uidFilterSet.Contains(currentNode.StableUid.Value)) - { - await onIncludedTestNodeAsync(currentNode, parentNodeUid).ConfigureAwait(false); - } - - foreach (TestNode childNode in currentNode.Tests) - { - queue.Enqueue((childNode, currentNode.StableUid, currentNodeFullPath)); - } - } - - DuplicatedNodes = [.. testNodesByUid.Where(x => x.Value.Count > 1)]; - } - - private static string EncodeString(string value) - => HttpUtility.UrlEncode(value); -} diff --git a/src/Adapter/MSTest.Engine/Engine/ITestArgumentsEntry.cs b/src/Adapter/MSTest.Engine/Engine/ITestArgumentsEntry.cs deleted file mode 100644 index 37a3a595c3..0000000000 --- a/src/Adapter/MSTest.Engine/Engine/ITestArgumentsEntry.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal interface ITestArgumentsEntry -{ - string UidFragment { get; } - - string? DisplayNameFragment { get; } - - object? Arguments { get; } -} diff --git a/src/Adapter/MSTest.Engine/Engine/ITestArgumentsManager.cs b/src/Adapter/MSTest.Engine/Engine/ITestArgumentsManager.cs deleted file mode 100644 index b02742fc7a..0000000000 --- a/src/Adapter/MSTest.Engine/Engine/ITestArgumentsManager.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal interface ITestArgumentsManager -{ - void RegisterTestArgumentsEntryProvider( - TestNodeUid testNodeStableUid, - Func> argumentsEntryProviderCallback); -} diff --git a/src/Adapter/MSTest.Engine/Engine/ITestExecutionContext.cs b/src/Adapter/MSTest.Engine/Engine/ITestExecutionContext.cs deleted file mode 100644 index 1f451eb449..0000000000 --- a/src/Adapter/MSTest.Engine/Engine/ITestExecutionContext.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Framework.Configurations; - -namespace Microsoft.Testing.Framework; - -public interface ITestExecutionContext -{ - CancellationToken CancellationToken { get; } - - IConfiguration Configuration { get; } - - ITestInfo TestInfo { get; } - - void CancelTestExecution(); - - void CancelTestExecution(int millisecondsDelay); - - void CancelTestExecution(TimeSpan delay); - - void ReportException(Exception exception, CancellationToken? timeoutCancellationToken = null); - - Task AddTestAttachmentAsync(FileInfo file, string displayName, string? description = null); -} diff --git a/src/Adapter/MSTest.Engine/Engine/ITestInfo.cs b/src/Adapter/MSTest.Engine/Engine/ITestInfo.cs deleted file mode 100644 index 1349a163b8..0000000000 --- a/src/Adapter/MSTest.Engine/Engine/ITestInfo.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Platform.Extensions.Messages; - -namespace Microsoft.Testing.Framework; - -public interface ITestInfo -{ - TestNodeUid StableUid { get; } - - string DisplayName { get; } - - IProperty[] Properties { get; } -} diff --git a/src/Adapter/MSTest.Engine/Engine/ITestSessionContext.cs b/src/Adapter/MSTest.Engine/Engine/ITestSessionContext.cs deleted file mode 100644 index 4a5968e93b..0000000000 --- a/src/Adapter/MSTest.Engine/Engine/ITestSessionContext.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Framework.Configurations; - -namespace Microsoft.Testing.Framework; - -public interface ITestSessionContext -{ - CancellationToken CancellationToken { get; } - - IConfiguration Configuration { get; } - - Task AddTestAttachmentAsync(FileInfo file, string displayName, string? description = null); -} diff --git a/src/Adapter/MSTest.Engine/Engine/TestArgumentsContext.cs b/src/Adapter/MSTest.Engine/Engine/TestArgumentsContext.cs deleted file mode 100644 index b716cd2149..0000000000 --- a/src/Adapter/MSTest.Engine/Engine/TestArgumentsContext.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal sealed class TestArgumentsContext(object arguments, TestNode target) -{ - public object Arguments { get; } = arguments; - - public TestNode Target { get; } = target; -} diff --git a/src/Adapter/MSTest.Engine/Engine/TestArgumentsEntry.cs b/src/Adapter/MSTest.Engine/Engine/TestArgumentsEntry.cs deleted file mode 100644 index 5fc0c05f8b..0000000000 --- a/src/Adapter/MSTest.Engine/Engine/TestArgumentsEntry.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -/// -/// WARNING: This type is public, but is meant for use only by MSTest source generator. Unannounced breaking changes to this API may happen. -/// -/// Type of the input data. -public sealed class InternalUnsafeTestArgumentsEntry(TArguments arguments, string uidFragment, string? displayNameFragment = null) : ITestArgumentsEntry -{ - public TArguments Arguments { get; } = arguments; - - public string UidFragment { get; } = uidFragment; - - public string? DisplayNameFragment { get; } = displayNameFragment; - - object? ITestArgumentsEntry.Arguments => Arguments; -} diff --git a/src/Adapter/MSTest.Engine/Engine/TestArgumentsManager.cs b/src/Adapter/MSTest.Engine/Engine/TestArgumentsManager.cs deleted file mode 100644 index b3ec62c771..0000000000 --- a/src/Adapter/MSTest.Engine/Engine/TestArgumentsManager.cs +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Platform; - -namespace Microsoft.Testing.Framework; - -internal sealed class TestArgumentsManager : ITestArgumentsManager -{ - private readonly Dictionary> _testArgumentsEntryProviders = []; - private bool _isRegistrationFrozen; - - public void RegisterTestArgumentsEntryProvider( - TestNodeUid testNodeStableUid, - Func> argumentPropertiesProviderCallback) - { - if (_isRegistrationFrozen) - { - throw new InvalidOperationException("Cannot register TestArgumentsEntry provider after registration is frozen."); - } - - // Add will throw an exception if the key already exists, which is intended. - _testArgumentsEntryProviders.Add(testNodeStableUid, argumentPropertiesProviderCallback); - } - - internal void FreezeRegistration() => _isRegistrationFrozen = true; - - internal static bool IsExpandableTestNode(TestNode testNode) - => testNode is IExpandableTestNode - && !FrameworkEngineMetadataProperty.GetFromProperties(testNode.Properties).PreventArgumentsExpansion; - - internal async Task ExpandTestNodeAsync(TestNode currentNode) - { - RoslynDebug.Assert(IsExpandableTestNode(currentNode), "Test node is not expandable"); - - int argumentsRowIndex = -1; - bool isIndexArgumentPropertiesProvider = false; - if (!_testArgumentsEntryProviders.TryGetValue( - currentNode.StableUid, - out Func? argumentPropertiesProvider)) - { - isIndexArgumentPropertiesProvider = true; - argumentPropertiesProvider = argument => - { - string fragment = $"[{argumentsRowIndex}]"; - return new InternalUnsafeTestArgumentsEntry(argument.Arguments, fragment, fragment); - }; - } - - HashSet expandedTestNodeUids = []; - List expandedTestNodes = [.. currentNode.Tests]; - switch (currentNode) - { - case IParameterizedTestNode parameterizedTestNode: - foreach (object? arguments in parameterizedTestNode.GetArguments()) - { - ExpandNodeWithArguments(currentNode, arguments, ref argumentsRowIndex, expandedTestNodes, - expandedTestNodeUids, argumentPropertiesProvider, isIndexArgumentPropertiesProvider); - } - - break; - - case ITaskParameterizedTestNode parameterizedTestNode: - foreach (object? arguments in await parameterizedTestNode.GetArguments().ConfigureAwait(false)) - { - ExpandNodeWithArguments(currentNode, arguments, ref argumentsRowIndex, expandedTestNodes, - expandedTestNodeUids, argumentPropertiesProvider, isIndexArgumentPropertiesProvider); - } - - break; - -#if NET - case IAsyncParameterizedTestNode parameterizedTestNode: - await foreach (object? arguments in parameterizedTestNode.GetArguments().ConfigureAwait(false)) - { - ExpandNodeWithArguments(currentNode, arguments, ref argumentsRowIndex, expandedTestNodes, - expandedTestNodeUids, argumentPropertiesProvider, isIndexArgumentPropertiesProvider); - } - - break; -#endif - - default: - throw new InvalidOperationException($"Unexpected parameterized test node type: '{currentNode.GetType()}'"); - } - - // When the node is expandable, we need to create a new node that is not an action node, but a container - // node that contains the expanded nodes. This is this node that will be executed. - TestNode expandedNode = new() - { - StableUid = currentNode.StableUid, - DisplayName = currentNode.DisplayName, - OverriddenEdgeName = currentNode.OverriddenEdgeName, - Properties = currentNode.Properties, - Tests = [.. expandedTestNodes], - }; - - return expandedNode; - - // Local functions - static void ExpandNodeWithArguments(TestNode testNode, object arguments, ref int argumentsRowIndex, List expandedTestNodes, - HashSet expandedTestNodeUids, Func argumentPropertiesProvider, - bool isIndexArgumentPropertiesProvider) - { - // We need to increase the index before calling the argumentPropertiesProvider, because it is capturing it's value - argumentsRowIndex++; - bool shouldWrapInParenthesis = true; - if (arguments is not ITestArgumentsEntry testArgumentsEntry) - { - shouldWrapInParenthesis = !isIndexArgumentPropertiesProvider; - testArgumentsEntry = argumentPropertiesProvider(new(arguments, testNode)); - } - - string argumentFragmentUid = GetArgumentFragmentUid(testArgumentsEntry, shouldWrapInParenthesis); - string argumentFragmentDisplayName = GetArgumentFragmentDisplayName(testArgumentsEntry, shouldWrapInParenthesis); - TestNode expandedTestNode = ((IExpandableTestNode)testNode).GetExpandedTestNode(arguments, argumentFragmentUid, - argumentFragmentDisplayName); - if (!expandedTestNodeUids.Add(expandedTestNode.StableUid)) - { - throw new InvalidOperationException( - $"Expanded test node with UID '{expandedTestNode.StableUid}' is not unique for test node '{testNode.StableUid}'."); - } - - expandedTestNodes.Add(expandedTestNode); - } - - static string GetArgumentFragmentUid(ITestArgumentsEntry testArgumentsEntry, bool shouldWrapInParenthesis) - => CreateWrappedName(testArgumentsEntry.UidFragment, shouldWrapInParenthesis); - - static string GetArgumentFragmentDisplayName(ITestArgumentsEntry testArgumentsEntry, bool shouldWrapInParenthesis) - => CreateWrappedName(testArgumentsEntry.DisplayNameFragment ?? testArgumentsEntry.UidFragment, shouldWrapInParenthesis); - - static string CreateWrappedName(string name, bool shouldWrapInParenthesis) - => shouldWrapInParenthesis ? string.Concat("(", name, ")") : name; - } -} diff --git a/src/Adapter/MSTest.Engine/Engine/TestContext.cs b/src/Adapter/MSTest.Engine/Engine/TestContext.cs deleted file mode 100644 index 46d22337cd..0000000000 --- a/src/Adapter/MSTest.Engine/Engine/TestContext.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal readonly struct TestContext(CancellationToken cancellationToken) -{ - public CancellationToken CancellationToken { get; } = cancellationToken; -} diff --git a/src/Adapter/MSTest.Engine/Engine/TestExecutionContext.cs b/src/Adapter/MSTest.Engine/Engine/TestExecutionContext.cs deleted file mode 100644 index c14e8add20..0000000000 --- a/src/Adapter/MSTest.Engine/Engine/TestExecutionContext.cs +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Extensions.TrxReport.Abstractions; -using Microsoft.Testing.Framework.Configurations; -using Microsoft.Testing.Framework.Helpers; -using Microsoft.Testing.Platform.Extensions.Messages; - -using PlatformTestNode = Microsoft.Testing.Platform.Extensions.Messages.TestNode; - -namespace Microsoft.Testing.Framework; - -internal sealed class TestExecutionContext : ITestExecutionContext -{ - private readonly CancellationTokenSource _cancellationTokenSource; - private readonly PlatformTestNode _platformTestNode; - private readonly ITrxReportCapability? _trxReportCapability; - private readonly CancellationToken _originalCancellationToken; - - public TestExecutionContext(IConfiguration configuration, TestNode testNode, PlatformTestNode platformTestNode, - ITrxReportCapability? trxReportCapability, CancellationToken cancellationToken) - { - Configuration = configuration; - _platformTestNode = platformTestNode; - _trxReportCapability = trxReportCapability; - TestInfo = new TestInfo(testNode); - _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - _originalCancellationToken = cancellationToken; - } - - public CancellationToken CancellationToken => _cancellationTokenSource.Token; - - public IConfiguration Configuration { get; } - - public ITestInfo TestInfo { get; } - - public void CancelTestExecution() - => _cancellationTokenSource.Cancel(); - - public void CancelTestExecution(int millisecondsDelay) - => _cancellationTokenSource.CancelAfter(millisecondsDelay); - - public void CancelTestExecution(TimeSpan delay) - => _cancellationTokenSource.CancelAfter(delay); - - public void ReportException(Exception exception, CancellationToken? timeoutCancellationToken = null) - { - if (_trxReportCapability is not null && _trxReportCapability.IsSupported) - { - AddTrxExceptionInformation(_platformTestNode.Properties, exception); - } - - TestNodeStateProperty executionState = exception switch - { - // We want to consider user timeouts as failures if they didn't use our cancellation token - OperationCanceledException canceledException - when canceledException.CancellationToken == _originalCancellationToken || canceledException.CancellationToken == CancellationToken -#pragma warning disable CS0618, MTP0001 // Type or member is obsolete - => new CancelledTestNodeStateProperty(ExceptionFlattener.FlattenOrUnwrap(exception)), -#pragma warning restore CS0618, MTP0001 // Type or member is obsolete - OperationCanceledException canceledException when canceledException.CancellationToken == timeoutCancellationToken - => new TimeoutTestNodeStateProperty(ExceptionFlattener.FlattenOrUnwrap(exception)), - AssertFailedException => new FailedTestNodeStateProperty(ExceptionFlattener.FlattenOrUnwrap(exception), exception.Message), - - // TODO: Filter exceptions that are to be considered as failures and return ErrorReason for the others - _ => new ErrorTestNodeStateProperty(exception), - }; - - // TODO: We need to be able to modify the execution state of a test node - if (!_platformTestNode.Properties.Any()) - { - _platformTestNode.Properties.Add(executionState); - } - } - - public Task AddTestAttachmentAsync(FileInfo file, string displayName, string? description = null) - { - _platformTestNode.Properties.Add(new FileArtifactProperty(file, displayName, description)); - return Task.CompletedTask; - } - - private static void AddTrxExceptionInformation(PropertyBag propertyBag, Exception? exception) - { - Exception? flatException = exception != null - ? ExceptionFlattener.FlattenOrUnwrap(exception) - : null; - if (flatException is null) - { - return; - } - - propertyBag.Add(new TrxExceptionProperty(StringifyMessage(flatException), StringifyStackTrace(flatException))); - - static string StringifyMessage(Exception exception) - { - string message = exception.Message; - if (exception.Data["assert.expected"] is string expected) - { - message += $"{Environment.NewLine}Expected:{Environment.NewLine}{expected}"; - } - - if (exception.Data["assert.actual"] is string actual) - { - message += $"{Environment.NewLine}Actual:{Environment.NewLine}{actual}"; - } - - return message; - } - - static string StringifyStackTrace(Exception exception) - { - if (exception is not AggregateException aggregateException) - { - return exception.StackTrace ?? string.Empty; - } - - string separator = "---End of inner exception ---"; - StringBuilder builder = new(); - foreach (Exception ex in aggregateException.InnerExceptions) - { - builder.AppendLine(ex.StackTrace); - builder.AppendLine(separator); - } - - return builder.ToString(); - } - } -} diff --git a/src/Adapter/MSTest.Engine/Engine/TestFixtureManager.cs b/src/Adapter/MSTest.Engine/Engine/TestFixtureManager.cs deleted file mode 100644 index 9e768fd9d9..0000000000 --- a/src/Adapter/MSTest.Engine/Engine/TestFixtureManager.cs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Framework.Helpers; -using Microsoft.Testing.Platform.Helpers; - -namespace Microsoft.Testing.Framework; - -internal sealed class TestFixtureManager -{ - private readonly Dictionary>> _fixtureInstancesByFixtureId = []; - private readonly Dictionary _fixtureIdsUsedByTestNode = []; - - // We could improve this by doing some optimistic lock but we expect a rather low contention on this. - // We use a dictionary as performance improvement because we know that when the registration is complete - // we will only read the collection (so no need for concurrency handling). - private readonly Dictionary _fixtureUses = []; - private bool _isUsageRegistrationFrozen; - - internal void RegisterFixtureUsage(TestNode testNode, string[] fixtureIds) - { - if (_isUsageRegistrationFrozen) - { - throw new InvalidOperationException("Cannot register fixture usage after registration is frozen"); - } - - if (fixtureIds.Length == 0) - { - return; - } - - _fixtureIdsUsedByTestNode.Add(testNode, [.. fixtureIds.Select(x => new FixtureId(x))]); - foreach (string fixtureId in fixtureIds) - { - if (!_fixtureUses.TryGetValue(fixtureId, out CountHolder? uses)) - { - uses = new(); - _fixtureUses.Add(fixtureId, uses); - } - - uses.Value++; - } - } - - internal void FreezeUsageRegistration() => _isUsageRegistrationFrozen = true; - - internal async Task SetupUsedFixturesAsync(TestNode testNode) - { - if (!_fixtureIdsUsedByTestNode.TryGetValue(testNode, out FixtureId[]? fixtureIds)) - { - return; - } - - foreach (FixtureId fixtureId in fixtureIds) - { - if (!_fixtureInstancesByFixtureId.TryGetValue(fixtureId, out Dictionary>? fixtureInstancesPerType)) - { - throw new InvalidOperationException($"Fixture with ID '{fixtureId}' is not registered"); - } - - foreach (AsyncLazy lazyFixture in fixtureInstancesPerType.Values) - { - if (!lazyFixture.IsValueCreated || !lazyFixture.Value.IsCompleted) - { - await lazyFixture.Value.ConfigureAwait(false); - } - } - } - } - - internal async Task CleanUnusedFixturesAsync(TestNode testNode) - { - if (!_fixtureIdsUsedByTestNode.TryGetValue(testNode, out FixtureId[]? fixtureIds)) - { - return; - } - - foreach (FixtureId fixtureId in fixtureIds) - { - CountHolder uses = _fixtureUses[fixtureId]; - int usesCount = uses.Value; - lock (uses) - { - uses.Value--; - usesCount = uses.Value; - } - - // It's important to use the captured value and to not check `uses.Value` again because - // another thread could have decremented the value in the meantime. We would then end up - // cleaning the fixture multiple times. - if (usesCount == 0) - { - await CleanupAndDisposeFixtureAsync(fixtureId).ConfigureAwait(false); - } - } - } - - private async Task CleanupAndDisposeFixtureAsync(FixtureId fixtureId) - { - if (!_fixtureInstancesByFixtureId.TryGetValue(fixtureId, out Dictionary>? fixtureInstancesPerType)) - { - throw new InvalidOperationException($"Fixture with ID '{fixtureId}' is not registered"); - } - - foreach (AsyncLazy lazyFixture in fixtureInstancesPerType.Values) - { - if (!lazyFixture.IsValueCreated || !lazyFixture.Value.IsCompleted) - { - throw new InvalidOperationException($"Fixture with ID '{fixtureId}' is not created"); - } - -#pragma warning disable VSTHRD103 // Call async methods when in an async method - object fixture = lazyFixture.Value.Result; -#pragma warning restore VSTHRD103 // Call async methods when in an async method - - await DisposeHelper.DisposeAsync(fixture).ConfigureAwait(false); - } - } - - /// - /// Integers are value types and we need a reference type to be able to lock on it. - /// - private sealed class CountHolder - { -#pragma warning disable SA1401 // Fields should be private - public int Value; -#pragma warning restore SA1401 // Fields should be private - } - - private sealed record FixtureId(string Value) - { - public static implicit operator FixtureId(string fixtureId) - => new(fixtureId); - } -} diff --git a/src/Adapter/MSTest.Engine/Engine/TestFrameworkEngine.cs b/src/Adapter/MSTest.Engine/Engine/TestFrameworkEngine.cs deleted file mode 100644 index 254016bfd1..0000000000 --- a/src/Adapter/MSTest.Engine/Engine/TestFrameworkEngine.cs +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Framework.Adapter; -using Microsoft.Testing.Framework.Configurations; -using Microsoft.Testing.Framework.Helpers; -using Microsoft.Testing.Platform; -using Microsoft.Testing.Platform.Capabilities.TestFramework; -using Microsoft.Testing.Platform.Extensions.Messages; -using Microsoft.Testing.Platform.Helpers; -using Microsoft.Testing.Platform.Messages; -using Microsoft.Testing.Platform.Requests; - -using PlatformIConfiguration = Microsoft.Testing.Platform.Configurations.IConfiguration; -using PlatformTestNode = Microsoft.Testing.Platform.Extensions.Messages.TestNode; - -namespace Microsoft.Testing.Framework; - -internal sealed class TestFrameworkEngine : IDataProducer -{ - private readonly TestingFrameworkExtension _extension; - private readonly ITestFrameworkCapabilities _capabilities; - private readonly IClock _clock; - private readonly ITask _task; - private readonly TestFrameworkConfiguration _testFrameworkConfiguration; - private readonly ITestNodesBuilder[] _testNodesBuilders; - private readonly ConfigurationWrapper _configuration; - - public TestFrameworkEngine(TestFrameworkConfiguration testFrameworkConfiguration, ITestNodesBuilder[] testNodesBuilders, TestingFrameworkExtension extension, ITestFrameworkCapabilities capabilities, - IClock clock, ITask task, PlatformIConfiguration configuration) - { - _extension = extension; - _capabilities = capabilities; - _clock = clock; - _task = task; - _testFrameworkConfiguration = testFrameworkConfiguration; - _testNodesBuilders = testNodesBuilders; - _configuration = new(configuration); - } - - public Type[] DataTypesProduced { get; } = [typeof(TestNodeUpdateMessage)]; - - public string Uid => _extension.Uid; - - public string Version => _extension.Version; - - public string DisplayName => _extension.DisplayName; - - public string Description => _extension.Description; - - public async Task IsEnabledAsync() => await _extension.IsEnabledAsync().ConfigureAwait(false); - - public async Task ExecuteRequestAsync(TestExecutionRequest testExecutionRequest, IMessageBus messageBus, CancellationToken cancellationToken) - => testExecutionRequest switch - { - DiscoverTestExecutionRequest discoveryRequest => await ExecuteTestNodeDiscoveryAsync(discoveryRequest, messageBus, cancellationToken).ConfigureAwait(false), - RunTestExecutionRequest runRequest => await ExecuteTestNodeRunAsync(runRequest, messageBus, cancellationToken).ConfigureAwait(false), - _ => Result.Fail($"Unexpected request type: '{testExecutionRequest.GetType().FullName}'"), - }; - - private async Task ExecuteTestNodeRunAsync(RunTestExecutionRequest request, IMessageBus messageBus, - CancellationToken cancellationToken) - { - List allRootTestNodes = []; - var fixtureManager = new TestFixtureManager(); - var argumentsManager = new TestArgumentsManager(); - var testSessionContext = new TestSessionContext(_configuration, request.Session.SessionUid, PublishDataAsync, cancellationToken); - - try - { - foreach (ITestNodesBuilder testNodeBuilder in _testNodesBuilders) - { - TestNode[] testNodes = await testNodeBuilder.BuildAsync(testSessionContext).ConfigureAwait(false); - allRootTestNodes.AddRange(testNodes); - } - - // We have built all test nodes, now we need to process them. Before that, we want to make sure to freeze managers - // to ensure that no new registrations are allowed. - argumentsManager.FreezeRegistration(); - - BFSTestNodeVisitor testNodesVisitor = new(allRootTestNodes, request.Filter, argumentsManager); - ThreadPoolTestNodeRunner testNodeRunner = new(_testFrameworkConfiguration, _capabilities, _clock, _task, _configuration, request.Session.SessionUid, - PublishDataAsync, fixtureManager, cancellationToken); - - await testNodesVisitor.VisitAsync((testNode, parentTestNodeUid) => - { - if (testNode is IActionableTestNode) - { - testNodeRunner.EnqueueTest(testNode, parentTestNodeUid); - } - - string[] fixtureIds = FrameworkEngineMetadataProperty.GetFromProperties(testNode.Properties).UsedFixtureIds - ?? []; - fixtureManager.RegisterFixtureUsage(testNode, fixtureIds); - - return Task.CompletedTask; - }).ConfigureAwait(false); - - if (testNodesVisitor.DuplicatedNodes.Length > 0) - { - StringBuilder errorMessageBuilder = new(); - errorMessageBuilder.AppendLine("Found multiple test nodes with the same UID:"); - foreach (KeyValuePair> duplicate in testNodesVisitor.DuplicatedNodes) - { - errorMessageBuilder.Append(CultureInfo.InvariantCulture, $"- For {duplicate.Key}: tests "); - errorMessageBuilder.AppendLine(string.Join(", ", duplicate.Value.Select(x => x.DisplayName))); - } - - return Result.Fail(errorMessageBuilder.ToString()); - } - - // Then, we want to freeze the registration of the fixtures, so that we can't add new fixtures. - fixtureManager.FreezeUsageRegistration(); - testNodeRunner.StartTests(); - - // Finally, we want to wait for all tests to complete. - return await testNodeRunner.WaitAllTestsAsync(cancellationToken).ConfigureAwait(false); - } - finally - { - foreach (ITestNodesBuilder testNodeBuilder in _testNodesBuilders) - { - await DisposeHelper.DisposeAsync(testNodeBuilder).ConfigureAwait(false); - } - } - - // Local functions - Task PublishDataAsync(IData data) - { - RoslynDebug.Assert(DataTypesProduced.Contains(data.GetType()), "Published data type hasn't been declared"); - - return messageBus.PublishAsync(this, data); - } - } - - private async Task ExecuteTestNodeDiscoveryAsync(DiscoverTestExecutionRequest request, IMessageBus messageBus, - CancellationToken cancellationToken) - { - List allRootTestNodes = []; - var fixtureManager = new TestFixtureManager(); - var argumentsManager = new TestArgumentsManager(); - var testSessionContext = new TestSessionContext(_configuration, request.Session.SessionUid, PublishDataAsync, cancellationToken); - - try - { - foreach (ITestNodesBuilder testNodeBuilder in _testNodesBuilders) - { - TestNode[] testNodes = await testNodeBuilder.BuildAsync(testSessionContext).ConfigureAwait(false); - allRootTestNodes.AddRange(testNodes); - } - - // We have built all test nodes, now we need to process them. Before that, we want to make sure to freeze managers - // to ensure that no new registrations are allowed. - argumentsManager.FreezeRegistration(); - - BFSTestNodeVisitor testNodesVisitor = new(allRootTestNodes, request.Filter, argumentsManager); - - await testNodesVisitor.VisitAsync(async (testNode, parentTestNodeUid) => - { - PlatformTestNode progressNode = testNode.ToPlatformTestNode(); - if (testNode is IActionableTestNode) - { - progressNode.Properties.Add(DiscoveredTestNodeStateProperty.CachedInstance); - } - - await messageBus.PublishAsync(this, new TestNodeUpdateMessage(request.Session.SessionUid, progressNode, - parentTestNodeUid?.ToPlatformTestNodeUid())).ConfigureAwait(false); - }).ConfigureAwait(false); - - if (testNodesVisitor.DuplicatedNodes.Length > 0) - { - StringBuilder errorMessageBuilder = new(); - errorMessageBuilder.AppendLine("Found multiple test nodes with the same UID:"); - foreach (KeyValuePair> duplicate in testNodesVisitor.DuplicatedNodes) - { - errorMessageBuilder.Append(CultureInfo.InvariantCulture, $"- For {duplicate.Key}: tests "); - errorMessageBuilder.AppendLine(string.Join(", ", duplicate.Value.Select(x => x.DisplayName))); - } - - return Result.Fail(errorMessageBuilder.ToString()); - } - } - finally - { - foreach (ITestNodesBuilder testNodeBuilder in _testNodesBuilders) - { - await DisposeHelper.DisposeAsync(testNodeBuilder).ConfigureAwait(false); - } - } - - return Result.Ok(); - - // Local functions - Task PublishDataAsync(IData data) - { - RoslynDebug.Assert(DataTypesProduced.Contains(data.GetType()), "Published data type hasn't been declared"); - - return messageBus.PublishAsync(this, data); - } - } -} diff --git a/src/Adapter/MSTest.Engine/Engine/TestInfo.cs b/src/Adapter/MSTest.Engine/Engine/TestInfo.cs deleted file mode 100644 index fd30f6ae21..0000000000 --- a/src/Adapter/MSTest.Engine/Engine/TestInfo.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Platform.Extensions.Messages; - -namespace Microsoft.Testing.Framework; - -internal sealed class TestInfo(TestNode testNode) : ITestInfo -{ - public TestNodeUid StableUid => TestNode.StableUid; - - public string DisplayName => TestNode.DisplayName; - - public IProperty[] Properties => TestNode.Properties; - - public TestNode TestNode { get; } = testNode; -} diff --git a/src/Adapter/MSTest.Engine/Engine/TestSessionContext.cs b/src/Adapter/MSTest.Engine/Engine/TestSessionContext.cs deleted file mode 100644 index 6aca48c849..0000000000 --- a/src/Adapter/MSTest.Engine/Engine/TestSessionContext.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Framework.Configurations; -using Microsoft.Testing.Platform.Extensions.Messages; -using Microsoft.Testing.Platform.TestHost; - -namespace Microsoft.Testing.Framework; - -internal sealed class TestSessionContext : ITestSessionContext -{ - private readonly SessionUid _sessionUid; - private readonly Func _publishDataAsync; - - public TestSessionContext( - IConfiguration configuration, - SessionUid sessionUid, - Func publishDataAsync, - CancellationToken cancellationToken) - { - Configuration = configuration; - - _sessionUid = sessionUid; - _publishDataAsync = publishDataAsync; - CancellationToken = cancellationToken; - } - - public CancellationToken CancellationToken { get; } - - public IConfiguration Configuration { get; } - - public async Task AddTestAttachmentAsync(FileInfo file, string displayName, string? description = null) - => await _publishDataAsync(new SessionFileArtifact(_sessionUid, file, displayName, description)).ConfigureAwait(false); -} diff --git a/src/Adapter/MSTest.Engine/Engine/ThreadPoolTestNodeRunner.cs b/src/Adapter/MSTest.Engine/Engine/ThreadPoolTestNodeRunner.cs deleted file mode 100644 index 3070130114..0000000000 --- a/src/Adapter/MSTest.Engine/Engine/ThreadPoolTestNodeRunner.cs +++ /dev/null @@ -1,253 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Extensions.TrxReport.Abstractions; -using Microsoft.Testing.Framework.Configurations; -using Microsoft.Testing.Framework.Helpers; -using Microsoft.Testing.Platform.Capabilities.TestFramework; -using Microsoft.Testing.Platform.Extensions.Messages; -using Microsoft.Testing.Platform.Helpers; -using Microsoft.Testing.Platform.TestHost; - -using PlatformTestNode = Microsoft.Testing.Platform.Extensions.Messages.TestNode; - -namespace Microsoft.Testing.Framework; - -internal sealed class ThreadPoolTestNodeRunner : IDisposable -{ - private readonly SemaphoreSlim? _maxParallelTests; - private readonly ConcurrentBag> _runningTests = []; - private readonly ConcurrentDictionary _runningTestNodeUids = new(); - private readonly CountdownEvent _ensureTaskQueuedCountdownEvent = new(1); - private readonly Func _publishDataAsync; - private readonly TestFixtureManager _testFixtureManager; - private readonly CancellationToken _cancellationToken; - private readonly IClock _clock; - private readonly IConfiguration _configuration; - private readonly SessionUid _sessionUid; - private readonly ITask _task; - private readonly ITrxReportCapability? _trxReportCapability; - private readonly TaskCompletionSource _waitForStart = new(); - private bool _isDisposed; - - public ThreadPoolTestNodeRunner(TestFrameworkConfiguration testFrameworkConfiguration, ITestFrameworkCapabilities capabilities, IClock clock, ITask task, IConfiguration configuration, - SessionUid sessionUid, Func publishDataAsync, TestFixtureManager testFixtureManager, - CancellationToken cancellationToken) - { - _clock = clock; - _configuration = configuration; - _sessionUid = sessionUid; - _publishDataAsync = publishDataAsync; - _testFixtureManager = testFixtureManager; - _cancellationToken = cancellationToken; - _task = task; - _trxReportCapability = capabilities.GetCapability(); - if (testFrameworkConfiguration.MaxParallelTests != int.MaxValue) - { - _maxParallelTests = new SemaphoreSlim(testFrameworkConfiguration.MaxParallelTests); - } - - cancellationToken.Register(_waitForStart.SetCanceled); - } - - public void EnqueueTest(TestNode frameworkTestNode, TestNodeUid? parentTestNodeUid) - { - _ensureTaskQueuedCountdownEvent.AddCount(); - try - { - _runningTests.Add( - _task.Run( - async () => - { - try - { - // We don't have a timeout here because we can have really slow fixture and it's on user - // the decision on how much to wait for it. - await _waitForStart.Task.ConfigureAwait(false); - - // Handle the global parallelism. - if (_maxParallelTests is not null) - { - await _maxParallelTests.WaitAsync().ConfigureAwait(false); - } - - try - { - _runningTestNodeUids.AddOrUpdate(frameworkTestNode.StableUid, 1, (_, count) => count + 1); - - PlatformTestNode progressNode = frameworkTestNode.ToPlatformTestNode(); - progressNode.Properties.Add(InProgressTestNodeStateProperty.CachedInstance); - await _publishDataAsync(new TestNodeUpdateMessage(_sessionUid, progressNode, parentTestNodeUid?.ToPlatformTestNodeUid())).ConfigureAwait(false); - - Result result = await CreateTestRunTaskAsync(frameworkTestNode, parentTestNodeUid).ConfigureAwait(false); - - _runningTestNodeUids.TryRemove(frameworkTestNode.StableUid, out int count); - - return count > 1 - ? throw new InvalidOperationException($"Test node '{frameworkTestNode.StableUid}' was run {count} times") - : result; - } - finally - { - _maxParallelTests?.Release(); - } - } - catch (Exception ex) - { - Environment.FailFast($"Unhandled exception inside '{nameof(CreateTestRunTaskAsync)}'", ex); - throw; - } - }, - _cancellationToken)); - } - catch (OperationCanceledException ex) when (ex.CancellationToken == _cancellationToken) - { - // We are being cancelled, so we don't need to wait anymore - } - finally - { - // We will signal for the second counting inside CreateTestRunTaskAsync() after the test run. - _ensureTaskQueuedCountdownEvent.Signal(); - } - } - - public void StartTests() - => _waitForStart.SetResult(0); - - private async Task CreateTestRunTaskAsync(TestNode testNode, TestNodeUid? parentTestNodeUid) - { - try - { - await _testFixtureManager.SetupUsedFixturesAsync(testNode).ConfigureAwait(false); - } - catch (Exception ex) - { - StringBuilder errorBuilder = new(); - errorBuilder.AppendLine(CultureInfo.InvariantCulture, $"Error while initializing fixtures for test '{testNode.DisplayName}' (UID = {testNode.StableUid.Value})"); - errorBuilder.AppendLine(); - errorBuilder.AppendLine(ex.ToString()); - return Result.Fail(errorBuilder.ToString()); - } - - Result result = await InvokeTestNodeAndPublishResultAsync(testNode, parentTestNodeUid, - async (testNode, testExecutionContext) => - { - switch (testNode) - { - case IAsyncActionTestNode actionTestNode: - await actionTestNode.InvokeAsync(testExecutionContext).ConfigureAwait(false); - break; - - case IActionTestNode actionTestNode: - actionTestNode.Invoke(testExecutionContext); - break; - - case IParameterizedAsyncActionTestNode actionTestNode: - await actionTestNode.InvokeAsync( - testExecutionContext, - action => InvokeTestNodeAndPublishResultAsync(testNode, parentTestNodeUid, (_, _) => action(), skipPublishResult: false)).ConfigureAwait(false); - break; - - default: - break; - } - }, - // Because parameterized tests report multiple results (one per parameter set), we don't want to publish the result - // of the overall test node execution, but only the results of the individual parameterized tests. - skipPublishResult: testNode is IParameterizedAsyncActionTestNode).ConfigureAwait(false); - - // Try to cleanup the fixture is not more used. - try - { - await _testFixtureManager.CleanUnusedFixturesAsync(testNode).ConfigureAwait(false); - return result; - } - catch (Exception ex) - { - StringBuilder errorBuilder = new(); - errorBuilder.AppendLine(CultureInfo.InvariantCulture, $"Error while cleaning fixtures for test '{testNode.StableUid}'"); - errorBuilder.AppendLine(); - errorBuilder.AppendLine(ex.ToString()); - return result.WithError(errorBuilder.ToString()); - } - } - - public async Task WaitAllTestsAsync(CancellationToken cancellationToken) - { - try - { - _ensureTaskQueuedCountdownEvent.Signal(); - await _ensureTaskQueuedCountdownEvent.WaitAsync(cancellationToken).ConfigureAwait(false); - Result[] results = await Task.WhenAll(_runningTests).ConfigureAwait(false); - return Result.Combine(results); - } - catch (OperationCanceledException ex) when (ex.CancellationToken == cancellationToken) - { - // If the cancellation token is triggered, we don't want to report the cancellation as a failure - return Result.Ok("Cancelled by user"); - } - } - - public void Dispose() - { - if (!_isDisposed) - { - _ensureTaskQueuedCountdownEvent.Dispose(); - _isDisposed = true; - } - } - - private async Task InvokeTestNodeAndPublishResultAsync(TestNode testNode, TestNodeUid? parentTestNodeUid, - Func testNodeInvokeAction, bool skipPublishResult) - { - TimeSheet timesheet = new(_clock); - timesheet.RecordStart(); - - PlatformTestNode platformTestNode = testNode.ToPlatformTestNode(); - - if (_trxReportCapability is not null && _trxReportCapability.IsSupported) - { - platformTestNode.Properties.Add(new TrxFullyQualifiedTypeNameProperty(platformTestNode.Uid.Value[..platformTestNode.Uid.Value.LastIndexOf('.')])); - } - - TestExecutionContext testExecutionContext = new(_configuration, testNode, platformTestNode, _trxReportCapability, _cancellationToken); - try - { - // If we're already enqueued we cancel the test before the start - // The test could not use the cancellation and we should wait the end of the test self to cancel. - _cancellationToken.ThrowIfCancellationRequested(); - await testNodeInvokeAction(testNode, testExecutionContext).ConfigureAwait(false); - - if (!platformTestNode.Properties.Any()) - { - platformTestNode.Properties.Add(PassedTestNodeStateProperty.CachedInstance); - } - } - catch (MissingMethodException ex) - { - // In dotnet watch mode we can remove tests. - if (Environment.GetEnvironmentVariable("DOTNET_WATCH") == "1") - { - return Result.Ok().WithWarning("Under 'DOTNET_WATCH' cannot find some member." + Environment.NewLine + ex.StackTrace); - } - - throw; - } - catch (Exception ex) - { - testExecutionContext.ReportException(ex); - } - finally - { - timesheet.RecordStop(); - platformTestNode.Properties.Add(new TimingProperty(new TimingInfo(timesheet.StartTime, timesheet.StopTime, timesheet.Duration))); - } - - if (!skipPublishResult) - { - await _publishDataAsync(new TestNodeUpdateMessage(_sessionUid, platformTestNode, parentTestNodeUid?.ToPlatformTestNodeUid())).ConfigureAwait(false); - } - - return Result.Ok(); - } -} diff --git a/src/Adapter/MSTest.Engine/Helpers/AsyncLazy.cs b/src/Adapter/MSTest.Engine/Helpers/AsyncLazy.cs deleted file mode 100644 index 2d41e7d5d3..0000000000 --- a/src/Adapter/MSTest.Engine/Helpers/AsyncLazy.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework.Helpers; - -internal sealed class AsyncLazy : Lazy> -{ - public AsyncLazy(Func valueFactory, LazyThreadSafetyMode mode) - : base(() => Task.Run(valueFactory), mode) - { - } - - public AsyncLazy(Func> taskFactory, LazyThreadSafetyMode mode) - : base(() => Task.Factory.StartNew(() => taskFactory(), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default).Unwrap(), mode) - { - } - - public TaskAwaiter GetAwaiter() => Value.GetAwaiter(); -} diff --git a/src/Adapter/MSTest.Engine/Helpers/DynamicDataNameProvider.cs b/src/Adapter/MSTest.Engine/Helpers/DynamicDataNameProvider.cs deleted file mode 100644 index 705e34b296..0000000000 --- a/src/Adapter/MSTest.Engine/Helpers/DynamicDataNameProvider.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -/// -/// Helper to provide a uids to user data, using the same logic that DynamicDataAttribute.GetDisplayName is using. This class is called by source generator. -/// -public static class DynamicDataNameProvider -{ - /// - /// Returns a stable fragment of uid by converting parameter types to strings, and suffixing them with index in brackets (e.g. [1]). - /// - /// Names of the parameters of the receiving method. - /// The data for each parameter. - /// Position in the collection. - /// Stable uid. - /// Arrays in both parameters need to have the same number of items. - public static string GetUidFragment(string[] parameterNames, object?[] data, int index) - { - if (parameterNames.Length != data.Length) - { - throw new ArgumentException($"Parameter count mismatch. The provided data ({string.Join(", ", data.Select(d => d?.ToString() ?? "null"))}) have {data.Length} items, but there are {parameterNames.Length} parameters."); - } - - StringBuilder stringBuilder = new StringBuilder().Append('('); - - for (int i = 0; i < data.Length; i++) - { - if (i > 0) - { - stringBuilder.Append(", "); - } - - stringBuilder.Append(parameterNames[i]).Append(": ").Append(data[i]?.ToString() ?? "null"); - } - - stringBuilder.Append(CultureInfo.InvariantCulture, $")[{index}]"); - return stringBuilder.ToString(); - } -} diff --git a/src/Adapter/MSTest.Engine/Helpers/ErrorReason.cs b/src/Adapter/MSTest.Engine/Helpers/ErrorReason.cs deleted file mode 100644 index 8a4a7cb30d..0000000000 --- a/src/Adapter/MSTest.Engine/Helpers/ErrorReason.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal sealed class ErrorReason(string message) : IErrorReason -{ - internal ErrorReason(string message, Exception exception) - : this(message) - => Exception = exception; - - internal ErrorReason(Exception exception) - : this(exception.Message) - => Exception = exception; - - public Exception? Exception { get; } - - public string Message { get; } = message; -} diff --git a/src/Adapter/MSTest.Engine/Helpers/ExceptionFlattener.cs b/src/Adapter/MSTest.Engine/Helpers/ExceptionFlattener.cs deleted file mode 100644 index df26732702..0000000000 --- a/src/Adapter/MSTest.Engine/Helpers/ExceptionFlattener.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework.Helpers; - -internal static class ExceptionFlattener -{ - /// - /// Returns the same exception for any exception that is not AggregateException. - /// For AggregateException it unwraps it when it holds just a single concrete exception, - /// otherwise it flattens the AggregateException and returns that. - /// - public static Exception FlattenOrUnwrap(Exception exception) - { - if (exception is AggregateException aggregateException) - { - if (aggregateException.InnerExceptions.Count == 1) - { - Exception innerException = aggregateException.InnerExceptions[0]; - if (innerException is not AggregateException) - { - return innerException; - } - } - - return aggregateException.Flatten(); - } - - return exception; - } -} diff --git a/src/Adapter/MSTest.Engine/Helpers/IErrorReason.cs b/src/Adapter/MSTest.Engine/Helpers/IErrorReason.cs deleted file mode 100644 index 59d47b13ed..0000000000 --- a/src/Adapter/MSTest.Engine/Helpers/IErrorReason.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal interface IErrorReason : IReason -{ - Exception? Exception { get; } -} diff --git a/src/Adapter/MSTest.Engine/Helpers/IReason.cs b/src/Adapter/MSTest.Engine/Helpers/IReason.cs deleted file mode 100644 index 5f7f5a296c..0000000000 --- a/src/Adapter/MSTest.Engine/Helpers/IReason.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal interface IReason -{ - string Message { get; } -} diff --git a/src/Adapter/MSTest.Engine/Helpers/ISuccessReason.cs b/src/Adapter/MSTest.Engine/Helpers/ISuccessReason.cs deleted file mode 100644 index 5952b324bf..0000000000 --- a/src/Adapter/MSTest.Engine/Helpers/ISuccessReason.cs +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal interface ISuccessReason : IReason; diff --git a/src/Adapter/MSTest.Engine/Helpers/IWarningReason.cs b/src/Adapter/MSTest.Engine/Helpers/IWarningReason.cs deleted file mode 100644 index e5a50b9aba..0000000000 --- a/src/Adapter/MSTest.Engine/Helpers/IWarningReason.cs +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal interface IWarningReason : IReason; diff --git a/src/Adapter/MSTest.Engine/Helpers/Result.cs b/src/Adapter/MSTest.Engine/Helpers/Result.cs deleted file mode 100644 index 881c9fa4d1..0000000000 --- a/src/Adapter/MSTest.Engine/Helpers/Result.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal sealed class Result -{ - private readonly List _reasons = []; - - private Result() - { - } - - public bool IsSuccess => !IsFailed; - - public bool IsFailed => _reasons.OfType().Any(); - - public IReadOnlyList Reasons => _reasons; - - public Result WithSuccess(ISuccessReason success) - { - _reasons.Add(success); - return this; - } - - public Result WithWarning(IWarningReason warning) - { - _reasons.Add(warning); - return this; - } - - public Result WithError(IErrorReason error) - { - _reasons.Add(error); - return this; - } - - public static Result Ok() => new(); - - public static Result Ok(string reason) => new Result().WithSuccess(new SuccessReason(reason)); - - public static Result Fail(Exception exception) => new Result().WithError(new ErrorReason(exception)); - - public static Result Fail(string reason) => new Result().WithError(new ErrorReason(reason)); - - public static Result Fail(string reason, Exception exception) => new Result().WithError(new ErrorReason(reason, exception)); - - public static Result Combine(IEnumerable results) - { - Result result = Ok(); - foreach (Result r in results) - { - result._reasons.AddRange(r.Reasons); - } - - return result; - } -} diff --git a/src/Adapter/MSTest.Engine/Helpers/ResultExtensions.cs b/src/Adapter/MSTest.Engine/Helpers/ResultExtensions.cs deleted file mode 100644 index 19aa6962b7..0000000000 --- a/src/Adapter/MSTest.Engine/Helpers/ResultExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal static class ResultExtensions -{ - public static Result WithWarning(this Result result, string message) - => result.WithWarning(new WarningReason(message)); - - public static Result WithError(this Result result, string message) - => result.WithError(new ErrorReason(message)); - - public static Result WithError(this Result result, Exception exception) - => result.WithError(new ErrorReason(exception)); -} diff --git a/src/Adapter/MSTest.Engine/Helpers/SuccessReason.cs b/src/Adapter/MSTest.Engine/Helpers/SuccessReason.cs deleted file mode 100644 index ffea736c93..0000000000 --- a/src/Adapter/MSTest.Engine/Helpers/SuccessReason.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal sealed class SuccessReason(string message) : ISuccessReason -{ - public string Message { get; } = message; -} diff --git a/src/Adapter/MSTest.Engine/Helpers/TestApplicationBuilderExtensions.cs b/src/Adapter/MSTest.Engine/Helpers/TestApplicationBuilderExtensions.cs deleted file mode 100644 index 47bd3eaa28..0000000000 --- a/src/Adapter/MSTest.Engine/Helpers/TestApplicationBuilderExtensions.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Framework.Adapter; -using Microsoft.Testing.Framework.Configurations; -using Microsoft.Testing.Platform.Builder; -using Microsoft.Testing.Platform.Helpers; -using Microsoft.Testing.Platform.Services; - -namespace Microsoft.Testing.Framework; - -public static class TestApplicationBuilderExtensions -{ - public static void AddTestFramework(this ITestApplicationBuilder testApplicationBuilder, params ITestNodesBuilder[] testNodesBuilder) - => testApplicationBuilder.AddTestFramework(new(), testNodesBuilder); - - public static void AddTestFramework( - this ITestApplicationBuilder testApplicationBuilder, - TestFrameworkConfiguration? testFrameworkConfiguration = null, - params ITestNodesBuilder[] testNodesBuilder) - { - if (testApplicationBuilder is null) - { - throw new ArgumentNullException(nameof(testApplicationBuilder)); - } - - if (testNodesBuilder is null) - { - throw new ArgumentNullException(nameof(testNodesBuilder)); - } - - if (testNodesBuilder.Length == 0) - { - throw new ArgumentException("At least one test node builder must be provided.", nameof(testNodesBuilder)); - } - - testFrameworkConfiguration ??= new TestFrameworkConfiguration(); - var extension = new TestingFrameworkExtension(); - testApplicationBuilder.AddTreeNodeFilterService(extension); - testApplicationBuilder.RegisterTestFramework( - serviceProvider => new TestFrameworkCapabilities(testNodesBuilder, new MSTestEngineBannerCapability(serviceProvider.GetRequiredService())), - (capabilities, serviceProvider) => - new TestFramework(testFrameworkConfiguration, testNodesBuilder, extension, serviceProvider.GetSystemClock(), - serviceProvider.GetTask(), serviceProvider.GetConfiguration(), capabilities)); - } -} diff --git a/src/Adapter/MSTest.Engine/Helpers/TestFrameworkConstants.cs b/src/Adapter/MSTest.Engine/Helpers/TestFrameworkConstants.cs deleted file mode 100644 index 1ff235fcb4..0000000000 --- a/src/Adapter/MSTest.Engine/Helpers/TestFrameworkConstants.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal static class TestFrameworkConstants -{ - public const string DefaultSemVer = MSTestEngineRepositoryVersion.Version; -} diff --git a/src/Adapter/MSTest.Engine/Helpers/TestNodeExpansionHelper.cs b/src/Adapter/MSTest.Engine/Helpers/TestNodeExpansionHelper.cs deleted file mode 100644 index 8551be94a4..0000000000 --- a/src/Adapter/MSTest.Engine/Helpers/TestNodeExpansionHelper.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework.Helpers; - -internal static class TestNodeExpansionHelper -{ - public static TestNodeUid GenerateStableUid(TestNodeUid testNodeUid, string dataId) - => new($"{testNodeUid.Value} {dataId}"); - - public static string GenerateDisplayName(string displayName, string dataId) - => $"{displayName} {dataId}"; -} diff --git a/src/Adapter/MSTest.Engine/Helpers/TestNodeExtensions.cs b/src/Adapter/MSTest.Engine/Helpers/TestNodeExtensions.cs deleted file mode 100644 index a62946b038..0000000000 --- a/src/Adapter/MSTest.Engine/Helpers/TestNodeExtensions.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Platform.Extensions.Messages; - -using PlatformTestNode = Microsoft.Testing.Platform.Extensions.Messages.TestNode; -using PlatformTestNodeUid = Microsoft.Testing.Platform.Extensions.Messages.TestNodeUid; - -namespace Microsoft.Testing.Framework.Helpers; - -internal static class TestNodeExtensions -{ - public static PlatformTestNode ToPlatformTestNode(this TestNode testNode) - { - if (testNode.DisplayName is null) - { - throw new ArgumentException("TestNode must have a DisplayNameFragment", nameof(testNode)); - } - - if (testNode.StableUid is null) - { - throw new ArgumentException("TestNode must have a StableUid", nameof(testNode)); - } - - var platformTestNode = new PlatformTestNode - { - Uid = testNode.StableUid.ToPlatformTestNodeUid(), - DisplayName = testNode.DisplayName, - }; - - foreach (IProperty property in testNode.Properties) - { - platformTestNode.Properties.Add(property); - } - - return platformTestNode; - } - - public static PlatformTestNodeUid ToPlatformTestNodeUid(this TestNodeUid testNodeUid) - => new(testNodeUid.Value); -} diff --git a/src/Adapter/MSTest.Engine/Helpers/WarningReason.cs b/src/Adapter/MSTest.Engine/Helpers/WarningReason.cs deleted file mode 100644 index 9d6013fc64..0000000000 --- a/src/Adapter/MSTest.Engine/Helpers/WarningReason.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal sealed class WarningReason(string message) : IWarningReason -{ - public string Message { get; } = message; -} diff --git a/src/Adapter/MSTest.Engine/MSTest.Engine.csproj b/src/Adapter/MSTest.Engine/MSTest.Engine.csproj deleted file mode 100644 index 77fd557b59..0000000000 --- a/src/Adapter/MSTest.Engine/MSTest.Engine.csproj +++ /dev/null @@ -1,99 +0,0 @@ - - - - netstandard2.0;$(SupportedNetFrameworks) - Microsoft.Testing.Framework - - - - License.txt - $(MSTestEngineVersionPrefix) - $(MSTestEnginePreReleaseVersionLabel) - true - - $(NoWarn);CS1591 - true - - true - - - - true - false - - true - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - true - buildMultiTargeting - - - - build/$(TargetFramework) - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Adapter/MSTest.Engine/PACKAGE.md b/src/Adapter/MSTest.Engine/PACKAGE.md deleted file mode 100644 index 135a747edc..0000000000 --- a/src/Adapter/MSTest.Engine/PACKAGE.md +++ /dev/null @@ -1,11 +0,0 @@ -# Microsoft.Testing - -Microsoft Testing is a set of platform, framework and protocol intended to make it possible to run any test on any target or device. - -Documentation can be found at . - -## About - -This package provides the Microsoft specific Test Framework. - -Test Anywhere Test Framework is the first test framework to offer support for Native AOT and trimming scenarios. It is designed to avoid reflection and leverage modern tooling such as source generators. diff --git a/src/Adapter/MSTest.Engine/PublicAPI/PublicAPI.Shipped.txt b/src/Adapter/MSTest.Engine/PublicAPI/PublicAPI.Shipped.txt deleted file mode 100644 index 1a72d04b9d..0000000000 --- a/src/Adapter/MSTest.Engine/PublicAPI/PublicAPI.Shipped.txt +++ /dev/null @@ -1,98 +0,0 @@ -#nullable enable -Microsoft.Testing.Framework.AssertFailedException -Microsoft.Testing.Framework.AssertFailedException.AssertFailedException() -> void -Microsoft.Testing.Framework.AssertFailedException.AssertFailedException(string! message, System.Exception! ex) -> void -Microsoft.Testing.Framework.AssertFailedException.AssertFailedException(string! message) -> void -Microsoft.Testing.Framework.Configurations.ConfigurationExtensions -Microsoft.Testing.Framework.Configurations.IConfiguration -Microsoft.Testing.Framework.Configurations.IConfiguration.this[string! key].get -> string? -Microsoft.Testing.Framework.Configurations.TestFrameworkConfiguration -Microsoft.Testing.Framework.Configurations.TestFrameworkConfiguration.MaxParallelTests.get -> int -Microsoft.Testing.Framework.Configurations.TestFrameworkConfiguration.TestFrameworkConfiguration(int maxParallelTests = 2147483647) -> void -Microsoft.Testing.Framework.DynamicDataNameProvider -Microsoft.Testing.Framework.InternalUnsafeActionParameterizedTestNode -Microsoft.Testing.Framework.InternalUnsafeActionParameterizedTestNode.Body.get -> System.Action! -Microsoft.Testing.Framework.InternalUnsafeActionParameterizedTestNode.Body.init -> void -Microsoft.Testing.Framework.InternalUnsafeActionParameterizedTestNode.GetArguments.get -> System.Func!>! -Microsoft.Testing.Framework.InternalUnsafeActionParameterizedTestNode.GetArguments.init -> void -Microsoft.Testing.Framework.InternalUnsafeActionParameterizedTestNode.InternalUnsafeActionParameterizedTestNode() -> void -Microsoft.Testing.Framework.InternalUnsafeActionTaskParameterizedTestNode -Microsoft.Testing.Framework.InternalUnsafeActionTaskParameterizedTestNode.Body.get -> System.Action! -Microsoft.Testing.Framework.InternalUnsafeActionTaskParameterizedTestNode.Body.init -> void -Microsoft.Testing.Framework.InternalUnsafeActionTaskParameterizedTestNode.GetArguments.get -> System.Func!>!>! -Microsoft.Testing.Framework.InternalUnsafeActionTaskParameterizedTestNode.GetArguments.init -> void -Microsoft.Testing.Framework.InternalUnsafeActionTaskParameterizedTestNode.InternalUnsafeActionTaskParameterizedTestNode() -> void -Microsoft.Testing.Framework.InternalUnsafeActionTestNode -Microsoft.Testing.Framework.InternalUnsafeActionTestNode.Body.get -> System.Action! -Microsoft.Testing.Framework.InternalUnsafeActionTestNode.Body.init -> void -Microsoft.Testing.Framework.InternalUnsafeActionTestNode.InternalUnsafeActionTestNode() -> void -Microsoft.Testing.Framework.InternalUnsafeAsyncActionParameterizedTestNode -Microsoft.Testing.Framework.InternalUnsafeAsyncActionParameterizedTestNode.Body.get -> System.Func! -Microsoft.Testing.Framework.InternalUnsafeAsyncActionParameterizedTestNode.Body.init -> void -Microsoft.Testing.Framework.InternalUnsafeAsyncActionParameterizedTestNode.GetArguments.get -> System.Func!>! -Microsoft.Testing.Framework.InternalUnsafeAsyncActionParameterizedTestNode.GetArguments.init -> void -Microsoft.Testing.Framework.InternalUnsafeAsyncActionParameterizedTestNode.InternalUnsafeAsyncActionParameterizedTestNode() -> void -Microsoft.Testing.Framework.InternalUnsafeAsyncActionTaskParameterizedTestNode -Microsoft.Testing.Framework.InternalUnsafeAsyncActionTaskParameterizedTestNode.Body.get -> System.Func! -Microsoft.Testing.Framework.InternalUnsafeAsyncActionTaskParameterizedTestNode.Body.init -> void -Microsoft.Testing.Framework.InternalUnsafeAsyncActionTaskParameterizedTestNode.GetArguments.get -> System.Func!>!>! -Microsoft.Testing.Framework.InternalUnsafeAsyncActionTaskParameterizedTestNode.GetArguments.init -> void -Microsoft.Testing.Framework.InternalUnsafeAsyncActionTaskParameterizedTestNode.InternalUnsafeAsyncActionTaskParameterizedTestNode() -> void -Microsoft.Testing.Framework.InternalUnsafeAsyncActionTestNode -Microsoft.Testing.Framework.InternalUnsafeAsyncActionTestNode.Body.get -> System.Func! -Microsoft.Testing.Framework.InternalUnsafeAsyncActionTestNode.Body.init -> void -Microsoft.Testing.Framework.InternalUnsafeAsyncActionTestNode.InternalUnsafeAsyncActionTestNode() -> void -Microsoft.Testing.Framework.InternalUnsafeTestArgumentsEntry -Microsoft.Testing.Framework.InternalUnsafeTestArgumentsEntry.Arguments.get -> TArguments -Microsoft.Testing.Framework.InternalUnsafeTestArgumentsEntry.DisplayNameFragment.get -> string? -Microsoft.Testing.Framework.InternalUnsafeTestArgumentsEntry.InternalUnsafeTestArgumentsEntry(TArguments arguments, string! uidFragment, string? displayNameFragment = null) -> void -Microsoft.Testing.Framework.InternalUnsafeTestArgumentsEntry.UidFragment.get -> string! -Microsoft.Testing.Framework.ITestExecutionContext -Microsoft.Testing.Framework.ITestExecutionContext.AddTestAttachmentAsync(System.IO.FileInfo! file, string! displayName, string? description = null) -> System.Threading.Tasks.Task! -Microsoft.Testing.Framework.ITestExecutionContext.CancellationToken.get -> System.Threading.CancellationToken -Microsoft.Testing.Framework.ITestExecutionContext.CancelTestExecution() -> void -Microsoft.Testing.Framework.ITestExecutionContext.CancelTestExecution(int millisecondsDelay) -> void -Microsoft.Testing.Framework.ITestExecutionContext.CancelTestExecution(System.TimeSpan delay) -> void -Microsoft.Testing.Framework.ITestExecutionContext.Configuration.get -> Microsoft.Testing.Framework.Configurations.IConfiguration! -Microsoft.Testing.Framework.ITestExecutionContext.ReportException(System.Exception! exception, System.Threading.CancellationToken? timeoutCancellationToken = null) -> void -Microsoft.Testing.Framework.ITestExecutionContext.TestInfo.get -> Microsoft.Testing.Framework.ITestInfo! -Microsoft.Testing.Framework.ITestInfo -Microsoft.Testing.Framework.ITestInfo.DisplayName.get -> string! -Microsoft.Testing.Framework.ITestInfo.Properties.get -> Microsoft.Testing.Platform.Extensions.Messages.IProperty![]! -Microsoft.Testing.Framework.ITestInfo.StableUid.get -> Microsoft.Testing.Framework.TestNodeUid! -Microsoft.Testing.Framework.ITestNodesBuilder -Microsoft.Testing.Framework.ITestNodesBuilder.BuildAsync(Microsoft.Testing.Framework.ITestSessionContext! testSessionContext) -> System.Threading.Tasks.Task! -Microsoft.Testing.Framework.ITestSessionContext -Microsoft.Testing.Framework.ITestSessionContext.AddTestAttachmentAsync(System.IO.FileInfo! file, string! displayName, string? description = null) -> System.Threading.Tasks.Task! -Microsoft.Testing.Framework.ITestSessionContext.CancellationToken.get -> System.Threading.CancellationToken -Microsoft.Testing.Framework.ITestSessionContext.Configuration.get -> Microsoft.Testing.Framework.Configurations.IConfiguration! -Microsoft.Testing.Framework.TestApplicationBuilderExtensions -Microsoft.Testing.Framework.TestNode -Microsoft.Testing.Framework.TestNode.DisplayName.get -> string! -Microsoft.Testing.Framework.TestNode.DisplayName.init -> void -Microsoft.Testing.Framework.TestNode.OverriddenEdgeName.get -> string? -Microsoft.Testing.Framework.TestNode.OverriddenEdgeName.init -> void -Microsoft.Testing.Framework.TestNode.Properties.get -> Microsoft.Testing.Platform.Extensions.Messages.IProperty![]! -Microsoft.Testing.Framework.TestNode.Properties.init -> void -Microsoft.Testing.Framework.TestNode.StableUid.get -> Microsoft.Testing.Framework.TestNodeUid! -Microsoft.Testing.Framework.TestNode.StableUid.init -> void -Microsoft.Testing.Framework.TestNode.TestNode() -> void -Microsoft.Testing.Framework.TestNode.Tests.get -> Microsoft.Testing.Framework.TestNode![]! -Microsoft.Testing.Framework.TestNode.Tests.init -> void -Microsoft.Testing.Framework.TestNodeUid -Microsoft.Testing.Framework.TestNodeUid.$() -> Microsoft.Testing.Framework.TestNodeUid! -Microsoft.Testing.Framework.TestNodeUid.Deconstruct(out string! Value) -> void -Microsoft.Testing.Framework.TestNodeUid.Equals(Microsoft.Testing.Framework.TestNodeUid? other) -> bool -Microsoft.Testing.Framework.TestNodeUid.TestNodeUid(string! Value) -> void -Microsoft.Testing.Framework.TestNodeUid.Value.get -> string! -Microsoft.Testing.Framework.TestNodeUid.Value.init -> void -override Microsoft.Testing.Framework.TestNodeUid.Equals(object? obj) -> bool -override Microsoft.Testing.Framework.TestNodeUid.GetHashCode() -> int -override Microsoft.Testing.Framework.TestNodeUid.ToString() -> string! -static Microsoft.Testing.Framework.Configurations.ConfigurationExtensions.GetTestResultDirectory(this Microsoft.Testing.Framework.Configurations.IConfiguration! configuration) -> string! -static Microsoft.Testing.Framework.DynamicDataNameProvider.GetUidFragment(string![]! parameterNames, object?[]! data, int index) -> string! -static Microsoft.Testing.Framework.TestApplicationBuilderExtensions.AddTestFramework(this Microsoft.Testing.Platform.Builder.ITestApplicationBuilder! testApplicationBuilder, Microsoft.Testing.Framework.Configurations.TestFrameworkConfiguration? testFrameworkConfiguration = null, params Microsoft.Testing.Framework.ITestNodesBuilder![]! testNodesBuilder) -> void -static Microsoft.Testing.Framework.TestApplicationBuilderExtensions.AddTestFramework(this Microsoft.Testing.Platform.Builder.ITestApplicationBuilder! testApplicationBuilder, params Microsoft.Testing.Framework.ITestNodesBuilder![]! testNodesBuilder) -> void -static Microsoft.Testing.Framework.TestNodeUid.implicit operator Microsoft.Testing.Framework.TestNodeUid!(string! value) -> Microsoft.Testing.Framework.TestNodeUid! -static Microsoft.Testing.Framework.TestNodeUid.operator !=(Microsoft.Testing.Framework.TestNodeUid? left, Microsoft.Testing.Framework.TestNodeUid? right) -> bool -static Microsoft.Testing.Framework.TestNodeUid.operator ==(Microsoft.Testing.Framework.TestNodeUid? left, Microsoft.Testing.Framework.TestNodeUid? right) -> bool diff --git a/src/Adapter/MSTest.Engine/PublicAPI/PublicAPI.Unshipped.txt b/src/Adapter/MSTest.Engine/PublicAPI/PublicAPI.Unshipped.txt deleted file mode 100644 index 7dc5c58110..0000000000 --- a/src/Adapter/MSTest.Engine/PublicAPI/PublicAPI.Unshipped.txt +++ /dev/null @@ -1 +0,0 @@ -#nullable enable diff --git a/src/Adapter/MSTest.Engine/PublicAPI/net/PublicAPI.Shipped.txt b/src/Adapter/MSTest.Engine/PublicAPI/net/PublicAPI.Shipped.txt deleted file mode 100644 index 7dc5c58110..0000000000 --- a/src/Adapter/MSTest.Engine/PublicAPI/net/PublicAPI.Shipped.txt +++ /dev/null @@ -1 +0,0 @@ -#nullable enable diff --git a/src/Adapter/MSTest.Engine/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Adapter/MSTest.Engine/PublicAPI/net/PublicAPI.Unshipped.txt deleted file mode 100644 index 7dc5c58110..0000000000 --- a/src/Adapter/MSTest.Engine/PublicAPI/net/PublicAPI.Unshipped.txt +++ /dev/null @@ -1 +0,0 @@ -#nullable enable diff --git a/src/Adapter/MSTest.Engine/PublicAPI/netstandard/PublicAPI.Shipped.txt b/src/Adapter/MSTest.Engine/PublicAPI/netstandard/PublicAPI.Shipped.txt deleted file mode 100644 index 074c6ad103..0000000000 --- a/src/Adapter/MSTest.Engine/PublicAPI/netstandard/PublicAPI.Shipped.txt +++ /dev/null @@ -1,2 +0,0 @@ -#nullable enable - diff --git a/src/Adapter/MSTest.Engine/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Adapter/MSTest.Engine/PublicAPI/netstandard/PublicAPI.Unshipped.txt deleted file mode 100644 index 7dc5c58110..0000000000 --- a/src/Adapter/MSTest.Engine/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ /dev/null @@ -1 +0,0 @@ -#nullable enable diff --git a/src/Adapter/MSTest.Engine/TestFramework/MSTestEngineBannerCapability.cs b/src/Adapter/MSTest.Engine/TestFramework/MSTestEngineBannerCapability.cs deleted file mode 100644 index 8b06284c2d..0000000000 --- a/src/Adapter/MSTest.Engine/TestFramework/MSTestEngineBannerCapability.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Platform.Capabilities.TestFramework; -using Microsoft.Testing.Platform.Services; - -namespace Microsoft.Testing.Framework; - -internal sealed class MSTestEngineBannerCapability : IBannerMessageOwnerCapability -{ - private readonly IPlatformInformation _platformInformation; - - public MSTestEngineBannerCapability(IPlatformInformation platformInformation) => _platformInformation = platformInformation; - - public Task GetBannerMessageAsync() - { - StringBuilder bannerMessage = new(); - bannerMessage.Append("MSTest.Engine v"); - bannerMessage.Append(MSTestEngineRepositoryVersion.Version); - - if (_platformInformation.BuildDate is { } buildDate) - { - bannerMessage.Append(" (UTC "); - bannerMessage.Append(buildDate.UtcDateTime.ToShortDateString()); - bannerMessage.Append(')'); - } - -#if NETCOREAPP - if (RuntimeFeature.IsDynamicCodeCompiled) -#endif - { - bannerMessage.Append(" ["); -#if NET6_0_OR_GREATER - bannerMessage.Append(RuntimeInformation.RuntimeIdentifier); -#else - bannerMessage.Append(RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant()); -#endif - bannerMessage.Append(" - "); - bannerMessage.Append(RuntimeInformation.FrameworkDescription); - bannerMessage.Append(']'); - } - - return Task.FromResult(bannerMessage.ToString()); - } -} diff --git a/src/Adapter/MSTest.Engine/TestFramework/TestFramework.cs b/src/Adapter/MSTest.Engine/TestFramework/TestFramework.cs deleted file mode 100644 index 88e34f3fea..0000000000 --- a/src/Adapter/MSTest.Engine/TestFramework/TestFramework.cs +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Framework.Adapter; -using Microsoft.Testing.Framework.Configurations; -using Microsoft.Testing.Platform.Capabilities.TestFramework; -using Microsoft.Testing.Platform.Extensions.TestFramework; -using Microsoft.Testing.Platform.Helpers; -using Microsoft.Testing.Platform.Requests; -using Microsoft.Testing.Platform.TestHost; - -using IConfiguration = Microsoft.Testing.Platform.Configurations.IConfiguration; - -namespace Microsoft.Testing.Framework; - -internal sealed class TestFramework : IDisposable, ITestFramework -#if NETCOREAPP -#pragma warning disable SA1001 // Commas should be spaced correctly - , IAsyncDisposable -#pragma warning restore SA1001 // Commas should be spaced correctly -#endif -{ - private readonly TestingFrameworkExtension _extension; - private readonly CountdownEvent _incomingRequestCounter = new(1); - private readonly TestFrameworkEngine _engine; - private readonly List _sessionWarningMessages = []; - private readonly List _sessionErrorMessages = []; - private SessionUid? _sessionId; - - public TestFramework(TestFrameworkConfiguration testFrameworkConfiguration, ITestNodesBuilder[] testNodesBuilders, TestingFrameworkExtension extension, - IClock clock, ITask task, IConfiguration configuration, ITestFrameworkCapabilities capabilities) - { - _extension = extension; - _engine = new(testFrameworkConfiguration, testNodesBuilders, extension, capabilities, clock, task, configuration); - } - - /// - public string Uid => _extension.Uid; - - /// - public string Version => _extension.Version; - - /// - public string DisplayName => _extension.DisplayName; - - /// - public string Description => _extension.Description; - - /// - public async Task IsEnabledAsync() => await _extension.IsEnabledAsync().ConfigureAwait(false); - - public Task CreateTestSessionAsync(CreateTestSessionContext context) - { - if (_sessionId is not null) - { - throw new InvalidOperationException("Session already created"); - } - - _sessionId = context.SessionUid; - _sessionWarningMessages.Clear(); - _sessionErrorMessages.Clear(); - return Task.FromResult(new CreateTestSessionResult { IsSuccess = true }); - } - - public async Task CloseTestSessionAsync(CloseTestSessionContext context) - { - _sessionId = null; - CloseTestSessionResult sessionResult = new(); - - try - { - // Ensure we have finished processing all requests. - _incomingRequestCounter.Signal(); - await _incomingRequestCounter.WaitAsync(context.CancellationToken).ConfigureAwait(false); - - if (_sessionErrorMessages.Count > 0) - { - StringBuilder errorBuilder = new(); - errorBuilder.AppendLine("Test session failed with the following errors:"); - for (int i = 0; i < _sessionErrorMessages.Count; i++) - { - errorBuilder.Append(" - ").AppendLine(_sessionErrorMessages[i]); - } - - sessionResult.ErrorMessage = errorBuilder.ToString(); - } - - if (_sessionWarningMessages.Count > 0) - { - StringBuilder errorBuilder = new(); - errorBuilder.AppendLine("Test session raised the following warnings:"); - for (int i = 0; i < _sessionWarningMessages.Count; i++) - { - errorBuilder.Append(" - ").AppendLine(_sessionWarningMessages[i]); - } - - sessionResult.WarningMessage = errorBuilder.ToString(); - } - - sessionResult.IsSuccess = _incomingRequestCounter.CurrentCount == 0 && _sessionErrorMessages.Count == 0; - return sessionResult; - } - catch (OperationCanceledException ex) when (ex.CancellationToken == context.CancellationToken) - { - // We are being cancelled, so we don't need to wait anymore - sessionResult.WarningMessage += - (sessionResult.WarningMessage?.Length > 0 ? Environment.NewLine : string.Empty) - + "Closing the test session was cancelled."; - sessionResult.IsSuccess = false; - return sessionResult; - } - } - - public async Task ExecuteRequestAsync(ExecuteRequestContext context) - { - _incomingRequestCounter.AddCount(); - try - { - if (context.Request is not TestExecutionRequest testExecutionRequest) - { - throw new InvalidOperationException($"Request type '{context.Request.GetType().FullName}' is not supported"); - } - - Result result = await _engine.ExecuteRequestAsync(testExecutionRequest, context.MessageBus, context.CancellationToken).ConfigureAwait(false); - - foreach (IReason reason in result.Reasons) - { - if (reason is IErrorReason errorReason) - { - _sessionErrorMessages.Add(errorReason.Exception?.ToString() ?? errorReason.Message); - } - else if (reason is IWarningReason warningReason) - { - _sessionWarningMessages.Add(warningReason.Message); - } - } - } - finally - { - _incomingRequestCounter.Signal(); - context.Complete(); - } - } - - public void Dispose() => _incomingRequestCounter.Dispose(); - -#if NETCOREAPP - - public ValueTask DisposeAsync() - { - _incomingRequestCounter.Dispose(); - return default; - } - -#endif -} diff --git a/src/Adapter/MSTest.Engine/TestFramework/TestFrameworkCapabilities.cs b/src/Adapter/MSTest.Engine/TestFramework/TestFrameworkCapabilities.cs deleted file mode 100644 index c7a853b973..0000000000 --- a/src/Adapter/MSTest.Engine/TestFramework/TestFrameworkCapabilities.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Extensions.TrxReport.Abstractions; -using Microsoft.Testing.Platform.Capabilities.TestFramework; - -namespace Microsoft.Testing.Framework; - -internal sealed class TestFrameworkCapabilities : ITestFrameworkCapabilities -{ - private readonly ITestNodesBuilder[] _testNodesBuilders; - private readonly ITestFrameworkCapability _bannerMessageOwnerCapability; - - public TestFrameworkCapabilities(ITestNodesBuilder[] testNodesBuilders, IBannerMessageOwnerCapability bannerMessageOwnerCapability) - { - _testNodesBuilders = testNodesBuilders; - _bannerMessageOwnerCapability = bannerMessageOwnerCapability; - } - - public IReadOnlyCollection Capabilities - => [new TestFrameworkCapabilitiesSet(_testNodesBuilders), _bannerMessageOwnerCapability]; -} - -internal sealed class TestFrameworkCapabilitiesSet : - ITestNodesTreeFilterTestFrameworkCapability, - ITrxReportCapability, - INamedFeatureCapability -{ - private const string MultiRequestSupport = "experimental_multiRequestSupport"; - private readonly ITestNodesBuilder[] _testNodesBuilders; - - public TestFrameworkCapabilitiesSet(ITestNodesBuilder[] testNodesBuilders) - { - IsTrxReportCapabilitySupported = testNodesBuilders.All(x => x.HasCapability()); - _testNodesBuilders = testNodesBuilders; - } - - public bool IsTrxReportEnabled { get; private set; } - - public bool IsTrxReportCapabilitySupported { get; } - - bool ITestNodesTreeFilterTestFrameworkCapability.IsSupported => true; - - bool ITrxReportCapability.IsSupported => IsTrxReportCapabilitySupported; - - public void Enable() - { - IsTrxReportEnabled = true; - foreach (ITestNodesBuilder item in _testNodesBuilders) - { - item.GetCapability()?.Enable(); - } - } - - bool INamedFeatureCapability.IsSupported(string featureName) => featureName == MultiRequestSupport; -} diff --git a/src/Adapter/MSTest.Engine/TestFramework/TestingFrameworkExtension.cs b/src/Adapter/MSTest.Engine/TestFramework/TestingFrameworkExtension.cs deleted file mode 100644 index 07fc54b2f2..0000000000 --- a/src/Adapter/MSTest.Engine/TestFramework/TestingFrameworkExtension.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Platform.Extensions; - -namespace Microsoft.Testing.Framework.Adapter; - -internal sealed class TestingFrameworkExtension : IExtension -{ - public string Uid => "MSTestEngine"; - - public string Version => TestFrameworkConstants.DefaultSemVer; - - public string DisplayName => "MSTest AOT"; - - public string Description => "MSTest AOT. This framework allows you to test your code anywhere in any mode (all OSes, all platforms, all configurations...)."; - - public Task IsEnabledAsync() => Task.FromResult(true); -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/FactoryTestNodesBuilder.cs b/src/Adapter/MSTest.Engine/TestNodes/FactoryTestNodesBuilder.cs deleted file mode 100644 index eaad6454de..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/FactoryTestNodesBuilder.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Platform.Capabilities; -using Microsoft.Testing.Platform.Capabilities.TestFramework; - -namespace Microsoft.Testing.Framework; - -internal sealed class FactoryTestNodesBuilder : ITestNodesBuilder -{ - private readonly Func _testNodesFactory; - - public FactoryTestNodesBuilder(Func testNodesFactory) - => _testNodesFactory = testNodesFactory; - - public bool IsSupportingTrxProperties { get; } - - IReadOnlyCollection ICapabilities.Capabilities => []; - - public Task BuildAsync(ITestSessionContext _) => Task.FromResult(_testNodesFactory()); -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/FrameworkTestNodeProperties.cs b/src/Adapter/MSTest.Engine/TestNodes/FrameworkTestNodeProperties.cs deleted file mode 100644 index 41c23a0427..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/FrameworkTestNodeProperties.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Platform.Extensions.Messages; - -namespace Microsoft.Testing.Framework; - -internal readonly struct FrameworkEngineMetadataProperty() : IProperty -{ - public bool PreventArgumentsExpansion { get; init; } - - public string[] UsedFixtureIds { get; init; } = []; - - public static FrameworkEngineMetadataProperty GetFromProperties(IProperty[] properties) - { - FrameworkEngineMetadataProperty? result = null; - foreach (IProperty property in properties) - { - if (property is FrameworkEngineMetadataProperty frameworkEngineMetadataProperty) - { - if (result is not null) - { - throw new InvalidOperationException("Sequence contains more than one matching element"); - } - - result = frameworkEngineMetadataProperty; - } - } - - return result ?? default; - } -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/IActionTestNode.cs b/src/Adapter/MSTest.Engine/TestNodes/IActionTestNode.cs deleted file mode 100644 index 25624bfa12..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/IActionTestNode.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal interface IActionTestNode : IActionableTestNode -{ - void Invoke(ITestExecutionContext testExecutionContext); -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/IActionableTestNode.cs b/src/Adapter/MSTest.Engine/TestNodes/IActionableTestNode.cs deleted file mode 100644 index 2631b535d2..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/IActionableTestNode.cs +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal interface IActionableTestNode; diff --git a/src/Adapter/MSTest.Engine/TestNodes/IAsyncActionTestNode.cs b/src/Adapter/MSTest.Engine/TestNodes/IAsyncActionTestNode.cs deleted file mode 100644 index 0cfec0e29e..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/IAsyncActionTestNode.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal interface IAsyncActionTestNode : IActionableTestNode -{ - Task InvokeAsync(ITestExecutionContext testExecutionContext); -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/IAsyncParameterizedTestNode.cs b/src/Adapter/MSTest.Engine/TestNodes/IAsyncParameterizedTestNode.cs deleted file mode 100644 index 06f76ad20a..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/IAsyncParameterizedTestNode.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -#if NET -namespace Microsoft.Testing.Framework; - -internal interface IAsyncParameterizedTestNode : IExpandableTestNode -{ - Func> GetArguments { get; } -} -#endif diff --git a/src/Adapter/MSTest.Engine/TestNodes/IContextTestNode.cs b/src/Adapter/MSTest.Engine/TestNodes/IContextTestNode.cs deleted file mode 100644 index 1cef2c26a8..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/IContextTestNode.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal interface IContextTestNode -{ - TContext Context { get; } -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/IExpandableTestNode.cs b/src/Adapter/MSTest.Engine/TestNodes/IExpandableTestNode.cs deleted file mode 100644 index ddcbe0e38a..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/IExpandableTestNode.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal interface IExpandableTestNode -{ - TestNode GetExpandedTestNode(object arguments, string argumentFragmentUid, string argumentFragmentDisplayName); -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/IParameterizedAsyncActionTestNode.cs b/src/Adapter/MSTest.Engine/TestNodes/IParameterizedAsyncActionTestNode.cs deleted file mode 100644 index 8c40e0cc10..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/IParameterizedAsyncActionTestNode.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal interface IParameterizedAsyncActionTestNode : IActionableTestNode -{ - Task InvokeAsync(ITestExecutionContext testExecutionContext, Func, Task> safeInvoke); -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/IParameterizedTestNode.cs b/src/Adapter/MSTest.Engine/TestNodes/IParameterizedTestNode.cs deleted file mode 100644 index 4e0397809c..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/IParameterizedTestNode.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal interface IParameterizedTestNode : IExpandableTestNode -{ - Func GetArguments { get; } -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/ITaskParameterizedTestNode.cs b/src/Adapter/MSTest.Engine/TestNodes/ITaskParameterizedTestNode.cs deleted file mode 100644 index 9734cc2081..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/ITaskParameterizedTestNode.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal interface ITaskParameterizedTestNode : IExpandableTestNode -{ - Func> GetArguments { get; } -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/ITestNodeBuilder.cs b/src/Adapter/MSTest.Engine/TestNodes/ITestNodeBuilder.cs deleted file mode 100644 index 17a81de037..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/ITestNodeBuilder.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Platform.Capabilities.TestFramework; - -namespace Microsoft.Testing.Framework; - -public interface ITestNodesBuilder : ITestFrameworkCapabilities -{ - Task BuildAsync(ITestSessionContext testSessionContext); -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeActionParameterizedTestNode.cs b/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeActionParameterizedTestNode.cs deleted file mode 100644 index 3252c48718..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeActionParameterizedTestNode.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -/// -/// WARNING: This type is public, but is meant for use only by MSTest source generator. Unannounced breaking changes to this API may happen. -/// -/// Type that holds the parameter data. -public sealed class InternalUnsafeActionParameterizedTestNode - : TestNode, IParameterizedTestNode, IParameterizedAsyncActionTestNode -{ - public required Action Body { get; init; } - - public required Func> GetArguments { get; init; } - - Func IParameterizedTestNode.GetArguments => GetArguments; - - async Task IParameterizedAsyncActionTestNode.InvokeAsync(ITestExecutionContext testExecutionContext, Func, Task> safeInvoke) - => await InternalUnsafeParameterizedTestNodeHelper.InvokeAsync( - GetArguments, - item => - { - Body(testExecutionContext, item); - return Task.CompletedTask; - }, - safeInvoke).ConfigureAwait(false); - - TestNode IExpandableTestNode.GetExpandedTestNode(object arguments, string argumentFragmentUid, string argumentFragmentDisplayName) - => InternalUnsafeParameterizedTestNodeHelper.ExpandActionNode(this, arguments, argumentFragmentUid, argumentFragmentDisplayName, Body); -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeActionTaskParameterizedTestNode.cs b/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeActionTaskParameterizedTestNode.cs deleted file mode 100644 index 7bd4a502e8..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeActionTaskParameterizedTestNode.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -/// -/// WARNING: This type is public, but is meant for use only by MSTest source generator. Unannounced breaking changes to this API may happen. -/// -/// Type that holds the parameter data. -public sealed class InternalUnsafeActionTaskParameterizedTestNode - : TestNode, ITaskParameterizedTestNode, IParameterizedAsyncActionTestNode -{ - public required Action Body { get; init; } - - public required Func>> GetArguments { get; init; } - - Func> ITaskParameterizedTestNode.GetArguments => async () => await GetArguments().ConfigureAwait(false); - - async Task IParameterizedAsyncActionTestNode.InvokeAsync(ITestExecutionContext testExecutionContext, Func, Task> safeInvoke) - => await InternalUnsafeParameterizedTestNodeHelper.InvokeAsync( - GetArguments, - item => - { - Body(testExecutionContext, item); - return Task.CompletedTask; - }, - safeInvoke).ConfigureAwait(false); - - TestNode IExpandableTestNode.GetExpandedTestNode(object arguments, string argumentFragmentUid, string argumentFragmentDisplayName) - => InternalUnsafeParameterizedTestNodeHelper.ExpandActionNode(this, arguments, argumentFragmentUid, argumentFragmentDisplayName, Body); -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeActionTestNode.cs b/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeActionTestNode.cs deleted file mode 100644 index ff06966fd0..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeActionTestNode.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -/// -/// WARNING: This type is public, but is meant for use only by MSTest source generator. Unannounced breaking changes to this API may happen. -/// -public sealed class InternalUnsafeActionTestNode : TestNode, IActionTestNode -{ - public required Action Body { get; init; } - - void IActionTestNode.Invoke(ITestExecutionContext testExecutionContext) - => Body(testExecutionContext); -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeAsyncActionParameterizedTestNode.cs b/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeAsyncActionParameterizedTestNode.cs deleted file mode 100644 index 34bc219131..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeAsyncActionParameterizedTestNode.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -/// -/// WARNING: This type is public, but is meant for use only by MSTest source generator. Unannounced breaking changes to this API may happen. -/// -/// Type that holds the parameter data. -public sealed class InternalUnsafeAsyncActionParameterizedTestNode - : TestNode, IParameterizedTestNode, IParameterizedAsyncActionTestNode -{ - public required Func Body { get; init; } - - public required Func> GetArguments { get; init; } - - Func IParameterizedTestNode.GetArguments => GetArguments; - - async Task IParameterizedAsyncActionTestNode.InvokeAsync(ITestExecutionContext testExecutionContext, Func, Task> safeInvoke) - => await InternalUnsafeParameterizedTestNodeHelper.InvokeAsync( - GetArguments, - item => Body(testExecutionContext, item), - safeInvoke).ConfigureAwait(false); - - TestNode IExpandableTestNode.GetExpandedTestNode(object arguments, string argumentFragmentUid, string argumentFragmentDisplayName) - => InternalUnsafeParameterizedTestNodeHelper.ExpandAsyncActionNode(this, arguments, argumentFragmentUid, argumentFragmentDisplayName, Body); -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeAsyncActionTaskParameterizedTestNode.cs b/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeAsyncActionTaskParameterizedTestNode.cs deleted file mode 100644 index e998be5c63..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeAsyncActionTaskParameterizedTestNode.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -/// -/// WARNING: This type is public, but is meant for use only by MSTest source generator. Unannounced breaking changes to this API may happen. -/// -/// Type that holds the parameter data. -public sealed class InternalUnsafeAsyncActionTaskParameterizedTestNode - : TestNode, ITaskParameterizedTestNode, IParameterizedAsyncActionTestNode -{ - public required Func Body { get; init; } - - public required Func>> GetArguments { get; init; } - - Func> ITaskParameterizedTestNode.GetArguments => async () => await GetArguments().ConfigureAwait(false); - - async Task IParameterizedAsyncActionTestNode.InvokeAsync(ITestExecutionContext testExecutionContext, Func, Task> safeInvoke) - => await InternalUnsafeParameterizedTestNodeHelper.InvokeAsync( - GetArguments, - item => Body(testExecutionContext, item), - safeInvoke).ConfigureAwait(false); - - TestNode IExpandableTestNode.GetExpandedTestNode(object arguments, string argumentFragmentUid, string argumentFragmentDisplayName) - => InternalUnsafeParameterizedTestNodeHelper.ExpandAsyncActionNode(this, arguments, argumentFragmentUid, argumentFragmentDisplayName, Body); -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeAsyncActionTestNode.cs b/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeAsyncActionTestNode.cs deleted file mode 100644 index f6abe9f436..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeAsyncActionTestNode.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -/// -/// WARNING: This type is public, but is meant for use only by MSTest source generator. Unannounced breaking changes to this API may happen. -/// -public sealed class InternalUnsafeAsyncActionTestNode : TestNode, IAsyncActionTestNode -{ - public required Func Body { get; init; } - - async Task IAsyncActionTestNode.InvokeAsync(ITestExecutionContext testExecutionContext) - => await Body(testExecutionContext).ConfigureAwait(false); -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeParameterizedTestNodeHelper.cs b/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeParameterizedTestNodeHelper.cs deleted file mode 100644 index 59c3b98416..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeParameterizedTestNodeHelper.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Framework.Helpers; - -namespace Microsoft.Testing.Framework; - -internal static class InternalUnsafeParameterizedTestNodeHelper -{ - public static Task InvokeAsync( - Func> getArguments, - Func invokeBodyAsync, - Func, Task> safeInvoke) - => InvokeAsync(getArguments(), invokeBodyAsync, safeInvoke); - - public static async Task InvokeAsync( - Func>> getArgumentsAsync, - Func invokeBodyAsync, - Func, Task> safeInvoke) - => await InvokeAsync(await getArgumentsAsync().ConfigureAwait(false), invokeBodyAsync, safeInvoke).ConfigureAwait(false); - - public static async Task InvokeAsync( - IEnumerable arguments, - Func invokeBodyAsync, - Func, Task> safeInvoke) - { - foreach (TData item in arguments) - { - await safeInvoke(() => invokeBodyAsync(item)).ConfigureAwait(false); - } - } - - public static InternalUnsafeActionTestNode ExpandActionNode( - TestNode testNode, - object arguments, - string argumentFragmentUid, - string argumentFragmentDisplayName, - Action body) - => new() - { - StableUid = TestNodeExpansionHelper.GenerateStableUid(testNode.StableUid, argumentFragmentUid), - DisplayName = TestNodeExpansionHelper.GenerateDisplayName(testNode.DisplayName, argumentFragmentDisplayName), - Body = testExecutionContext => body(testExecutionContext, (TData)arguments), - Properties = testNode.Properties, - }; - - public static InternalUnsafeAsyncActionTestNode ExpandAsyncActionNode( - TestNode testNode, - object arguments, - string argumentFragmentUid, - string argumentFragmentDisplayName, - Func body) - => new() - { - StableUid = TestNodeExpansionHelper.GenerateStableUid(testNode.StableUid, argumentFragmentUid), - DisplayName = TestNodeExpansionHelper.GenerateDisplayName(testNode.DisplayName, argumentFragmentDisplayName), - Body = testExecutionContext => body(testExecutionContext, (TData)arguments), - Properties = testNode.Properties, - }; -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/TestNode.cs b/src/Adapter/MSTest.Engine/TestNodes/TestNode.cs deleted file mode 100644 index 8119973fde..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/TestNode.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Platform.Extensions.Messages; - -namespace Microsoft.Testing.Framework; - -[DebuggerDisplay("StableUid = {StableUid.Value}, TestsCount = {Tests.Length}, PropertiesCount = {Properties.Length}")] -public class TestNode -{ - public required TestNodeUid StableUid { get; init; } - - public required string DisplayName { get; init; } - - /// - /// Gets the name of the edge that connects this node to its parent. - /// - public string? OverriddenEdgeName { get; init; } - - public IProperty[] Properties { get; init; } = []; - - public TestNode[] Tests { get; init; } = []; -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/TestNodeUid.cs b/src/Adapter/MSTest.Engine/TestNodes/TestNodeUid.cs deleted file mode 100644 index c1da416bb5..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/TestNodeUid.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -/// -/// Represents a unique identifier for a test node. -/// -/// The unique identifier. -[DebuggerDisplay("{Value}")] -public sealed record TestNodeUid(string Value) -{ - /// - /// Implicitly converts a string to a . - /// - /// The unique identifier. - public static implicit operator TestNodeUid(string value) - => new(value); -} diff --git a/src/Adapter/MSTest.Engine/TimeSheet.cs b/src/Adapter/MSTest.Engine/TimeSheet.cs deleted file mode 100644 index e026329f66..0000000000 --- a/src/Adapter/MSTest.Engine/TimeSheet.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Platform.Helpers; - -namespace Microsoft.Testing.Framework; - -/// -/// Keeps track of time and duration of a test. -/// -internal sealed class TimeSheet -{ - private readonly Stopwatch _stopwatch; - private readonly IClock _clock; - - /// - /// Initializes a new instance of the class. - /// Creates new instance of this class, starts measuring time in queue. - /// - public TimeSheet(IClock clock) - { - _stopwatch = Stopwatch.StartNew(); - _clock = clock; - } - - /// - /// Gets when the test started in UTC. Not-precise, because we just capture the current DateTimeUtc. - /// - public DateTimeOffset StartTime { get; private set; } - - /// - /// Gets when the test stopped in UTC. Not-precise, because we just capture the current DateTimeUtc. - /// - public DateTimeOffset StopTime { get; private set; } - - /// - /// Gets how long we've spent executing the test. Precise, measured by Stopwatch. - /// - public TimeSpan Duration { get; private set; } - - /// - /// Record the start of the test, this will capture time spent in queue and start measuring duration of test. - /// - internal void RecordStart() - { - StartTime = _clock.UtcNow; - _stopwatch.Restart(); - } - - /// - /// Record the end of the test, this will capture time spent executing the test. - /// - internal void RecordStop() - { - StopTime = _clock.UtcNow; - Duration = _stopwatch.Elapsed; - } -} diff --git a/src/Adapter/MSTest.Engine/build/MSTest.Engine.props b/src/Adapter/MSTest.Engine/build/MSTest.Engine.props deleted file mode 100644 index 68ae1bd96f..0000000000 --- a/src/Adapter/MSTest.Engine/build/MSTest.Engine.props +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/Adapter/MSTest.Engine/build/MSTest.Engine.targets b/src/Adapter/MSTest.Engine/build/MSTest.Engine.targets deleted file mode 100644 index f7451dd95f..0000000000 --- a/src/Adapter/MSTest.Engine/build/MSTest.Engine.targets +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/Adapter/MSTest.Engine/buildMultiTargeting/MSTest.Engine.props b/src/Adapter/MSTest.Engine/buildMultiTargeting/MSTest.Engine.props deleted file mode 100644 index ac5c535fa4..0000000000 --- a/src/Adapter/MSTest.Engine/buildMultiTargeting/MSTest.Engine.props +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - MSTest.SourceGeneration - Microsoft.Testing.Framework.SourceGeneration.SourceGeneratedTestingPlatformBuilderHook - - - diff --git a/src/Adapter/MSTest.Engine/buildMultiTargeting/MSTest.Engine.targets b/src/Adapter/MSTest.Engine/buildMultiTargeting/MSTest.Engine.targets deleted file mode 100644 index cf33b5d45c..0000000000 --- a/src/Adapter/MSTest.Engine/buildMultiTargeting/MSTest.Engine.targets +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/Adapter/MSTestAdapter.PlatformServices/PlatformServiceProvider.cs b/src/Adapter/MSTestAdapter.PlatformServices/PlatformServiceProvider.cs index 7078c45b24..81c975c823 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/PlatformServiceProvider.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/PlatformServiceProvider.cs @@ -172,4 +172,15 @@ public ITestContext GetTestContext(ITestMethod? testMethod, string? testClassFul testContextImplementation.SetOutcome(outcome); return testContextImplementation; } + + /// + /// Swaps the cached and + /// instances for the source-generator-backed implementations. Used by + /// . + /// + internal void SetSourceGeneratedOperations(IReflectionOperations reflectionOperations, IFileOperations fileOperations) + { + ReflectionOperations = reflectionOperations ?? throw new ArgumentNullException(nameof(reflectionOperations)); + FileOperations = fileOperations ?? throw new ArgumentNullException(nameof(fileOperations)); + } } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/PublicAPI/PublicAPI.Unshipped.txt b/src/Adapter/MSTestAdapter.PlatformServices/PublicAPI/PublicAPI.Unshipped.txt index 7dc5c58110..7098ba839b 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/Adapter/MSTestAdapter.PlatformServices/PublicAPI/PublicAPI.Unshipped.txt @@ -1 +1,43 @@ #nullable enable +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.ReflectionMetadataHook +static Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.ReflectionMetadataHook.SetMetadata(Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider! metadata) -> void +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.SourceGeneratedReflectionDataProvider() -> void +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.Assembly.get -> System.Reflection.Assembly! +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.Assembly.set -> void +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.AssemblyAttributes.get -> object![]! +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.AssemblyAttributes.set -> void +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.AssemblyName.get -> string! +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.AssemblyName.set -> void +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.Types.get -> System.Type![]! +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.Types.set -> void +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.TypesByName.get -> System.Collections.Generic.Dictionary! +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.TypesByName.set -> void +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.TypeAttributes.get -> System.Collections.Generic.Dictionary! +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.TypeAttributes.set -> void +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.TypeProperties.get -> System.Collections.Generic.Dictionary! +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.TypeProperties.set -> void +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.TypeMethods.get -> System.Collections.Generic.Dictionary! +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.TypeMethods.set -> void +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.TypeMethodLocations.get -> System.Collections.Generic.Dictionary! +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.TypeMethodLocations.set -> void +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.TypeMethodAttributes.get -> System.Collections.Generic.Dictionary!>! +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.TypeMethodAttributes.set -> void +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.TypeConstructors.get -> System.Collections.Generic.Dictionary! +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.TypeConstructors.set -> void +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.TypePropertiesByName.get -> System.Collections.Generic.Dictionary!>! +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.TypePropertiesByName.set -> void +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.TypeConstructorsInvoker.get -> System.Collections.Generic.Dictionary! +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.TypeConstructorsInvoker.set -> void +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.TypeLocation +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.TypeLocation.TypeLocation() -> void +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.TypeLocation.FileName.get -> string! +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.TypeLocation.FileName.set -> void +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.TypeLocation.MethodLocations.get -> System.Collections.Generic.Dictionary! +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.TypeLocation.MethodLocations.set -> void +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.ConstructorInvoker +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.ConstructorInvoker.ConstructorInvoker() -> void +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.ConstructorInvoker.Parameters.get -> System.Type![]! +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.ConstructorInvoker.Parameters.set -> void +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.ConstructorInvoker.Invoker.get -> System.Func! +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.ConstructorInvoker.Invoker.set -> void diff --git a/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/CompositeSourceGeneratedReflectionDataProvider.cs b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/CompositeSourceGeneratedReflectionDataProvider.cs new file mode 100644 index 0000000000..af2acdca8f --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/CompositeSourceGeneratedReflectionDataProvider.cs @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration; + +/// +/// Merges multiple per-assembly instances into +/// a single provider so that more than one test assembly can be registered with +/// in a +/// single process. The merged dictionaries are recomputed from scratch on every +/// so that lookups remain a simple +/// dictionary read on the hot path. +/// +internal sealed class CompositeSourceGeneratedReflectionDataProvider : SourceGeneratedReflectionDataProvider +{ + private readonly List _providers = []; +#pragma warning disable IDE0028 // Collection initialization can be simplified — Dictionary needs explicit comparer + private readonly Dictionary _providersByAssemblyName = new(StringComparer.OrdinalIgnoreCase); +#pragma warning restore IDE0028 + private readonly Dictionary _providersByAssembly = []; + + public void Add(SourceGeneratedReflectionDataProvider provider) + { + _providers.Add(provider); + _providersByAssemblyName[provider.AssemblyName] = provider; + if (provider.Assembly is not null) + { + _providersByAssembly[provider.Assembly] = provider; + } + + Rebuild(); + } + + internal override Assembly GetAssembly(string assemblyPath) + { + string name = Path.GetFileNameWithoutExtension(assemblyPath); + return _providersByAssemblyName.TryGetValue(name, out SourceGeneratedReflectionDataProvider? provider) + && provider.Assembly is { } assembly + ? assembly + : throw new ArgumentException($"Assembly '{assemblyPath}' is not registered with the MSTest source generator."); + } + + internal override void GetNavigationData(string className, string methodName, out int minLineNumber, out string? fileName) + { + foreach (SourceGeneratedReflectionDataProvider provider in _providers) + { + provider.GetNavigationData(className, methodName, out minLineNumber, out fileName); + if (fileName is not null) + { + return; + } + } + + minLineNumber = 0; + fileName = null; + } + + internal override object[] GetAssemblyAttributes(Assembly assembly) + => _providersByAssembly.TryGetValue(assembly, out SourceGeneratedReflectionDataProvider? provider) + ? provider.AssemblyAttributes + : []; + + private void Rebuild() + { + // AssemblyName / Assembly do not make sense for a composite; leave at defaults. + var types = new List(); + var typesByName = new Dictionary(StringComparer.Ordinal); + var typeAttributes = new Dictionary(); + var assemblyAttributes = new List(); + var typeProperties = new Dictionary(); + var typeMethods = new Dictionary(); + var typeMethodLocations = new Dictionary(StringComparer.Ordinal); + var typeMethodAttributes = new Dictionary>(); + var typeConstructors = new Dictionary(); + var typePropertiesByName = new Dictionary>(); + var typeConstructorsInvoker = new Dictionary(); + + foreach (SourceGeneratedReflectionDataProvider provider in _providers) + { + types.AddRange(provider.Types); + foreach (KeyValuePair kvp in provider.TypesByName) + { + typesByName[kvp.Key] = kvp.Value; + } + + foreach (KeyValuePair kvp in provider.TypeAttributes) + { + typeAttributes[kvp.Key] = kvp.Value; + } + + assemblyAttributes.AddRange(provider.AssemblyAttributes); + + foreach (KeyValuePair kvp in provider.TypeProperties) + { + typeProperties[kvp.Key] = kvp.Value; + } + + foreach (KeyValuePair kvp in provider.TypeMethods) + { + typeMethods[kvp.Key] = kvp.Value; + } + + foreach (KeyValuePair kvp in provider.TypeMethodLocations) + { + typeMethodLocations[kvp.Key] = kvp.Value; + } + + foreach (KeyValuePair> kvp in provider.TypeMethodAttributes) + { + typeMethodAttributes[kvp.Key] = kvp.Value; + } + + foreach (KeyValuePair kvp in provider.TypeConstructors) + { + typeConstructors[kvp.Key] = kvp.Value; + } + + foreach (KeyValuePair> kvp in provider.TypePropertiesByName) + { + typePropertiesByName[kvp.Key] = kvp.Value; + } + + foreach (KeyValuePair kvp in provider.TypeConstructorsInvoker) + { + typeConstructorsInvoker[kvp.Key] = kvp.Value; + } + } + + Types = [.. types]; + TypesByName = typesByName; + TypeAttributes = typeAttributes; + AssemblyAttributes = [.. assemblyAttributes]; + TypeProperties = typeProperties; + TypeMethods = typeMethods; + TypeMethodLocations = typeMethodLocations; + TypeMethodAttributes = typeMethodAttributes; + TypeConstructors = typeConstructors; + TypePropertiesByName = typePropertiesByName; + TypeConstructorsInvoker = typeConstructorsInvoker; + } +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/ReflectionMetadataHook.cs b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/ReflectionMetadataHook.cs new file mode 100644 index 0000000000..6248c1e05f --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/ReflectionMetadataHook.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration; + +/// +/// Entry point used by MSTest's source generator to register pre-computed reflection metadata +/// for the test assembly. Once has been called, MSTest's discovery and +/// execution paths will read metadata from the supplied +/// instead of doing reflection at runtime. +/// +/// +/// This API is intended to be invoked from a [ModuleInitializer] in the test assembly that +/// is emitted by the MSTest source generator. Hand-written code should not depend on it. +/// +public static class ReflectionMetadataHook +{ +#if NET9_0_OR_GREATER + private static readonly Lock Lock = new(); +#else + private static readonly object Lock = new(); +#endif + private static readonly CompositeSourceGeneratedReflectionDataProvider Composite = new(); + + /// + /// Registers the source-generated reflection metadata for a test assembly. Safe to call from + /// multiple module initializers — each call adds the supplied provider to a process-wide + /// composite so that previously registered assemblies remain accessible. + /// + /// The metadata describing the test assembly. + public static void SetMetadata(SourceGeneratedReflectionDataProvider metadata) + { + if (metadata is null) + { + throw new ArgumentNullException(nameof(metadata)); + } + + lock (Lock) + { + Composite.Add(metadata); + + SourceGeneratorToggle.Enable(); + + var reflectionOperations = new SourceGeneratedReflectionOperations(Composite); + var fileOperations = new SourceGeneratedFileOperations(Composite); + + if (PlatformServiceProvider.Instance is PlatformServiceProvider concreteProvider) + { + concreteProvider.SetSourceGeneratedOperations(reflectionOperations, fileOperations); + } + } + } +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratedFileOperations.cs b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratedFileOperations.cs new file mode 100644 index 0000000000..db786cb659 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratedFileOperations.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration; + +/// +/// Source-generator-backed implementation of . Assembly loading is +/// served from the supplied ; the remaining +/// file-system operations are delegated to the regular implementation. +/// +internal sealed class SourceGeneratedFileOperations : IFileOperations +{ + private readonly FileOperations _inner = new(); + + public SourceGeneratedFileOperations(SourceGeneratedReflectionDataProvider dataProvider) + => DataProvider = dataProvider ?? throw new ArgumentNullException(nameof(dataProvider)); + + internal SourceGeneratedReflectionDataProvider DataProvider { get; } + + public Assembly LoadAssembly(string assemblyName) => DataProvider.GetAssembly(assemblyName); + + public bool DoesFileExist(string assemblyFileName) => _inner.DoesFileExist(assemblyFileName); + + public string GetFullFilePath(string assemblyFileName) => _inner.GetFullFilePath(assemblyFileName); +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratedReflectionDataProvider.cs b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratedReflectionDataProvider.cs new file mode 100644 index 0000000000..e0aa4ef7d7 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratedReflectionDataProvider.cs @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration; + +/// +/// Holds the pre-computed metadata that the MSTest source generator emits for a test assembly. +/// This data backs the source-generated implementations of the platform reflection and file +/// services so that test discovery and execution do not depend on runtime reflection. +/// +/// +/// This type is intended to be populated only by the MSTest source generator and passed to +/// . +/// Its shape is intentionally simple so that source-generated code can construct it without +/// reflection, but the shape may evolve. Hand-written code should not depend on it. +/// +public class SourceGeneratedReflectionDataProvider +{ + /// + /// Gets or sets the test assembly described by this metadata snapshot. + /// + public Assembly Assembly { get; set; } = null!; + + /// + /// Gets or sets the file-name (without extension) of the test assembly. + /// + public string AssemblyName { get; set; } = string.Empty; + + /// + /// Gets or sets all defined types in the test assembly that participate in test discovery. + /// + public Type[] Types { get; set; } = []; + + /// + /// Gets or sets a lookup of types by full name. + /// + public Dictionary TypesByName { get; set; } = []; + + /// + /// Gets or sets attributes declared on each type. The array contains attribute instances + /// already inflated by the source generator so no reflection call is required to read them. + /// + public Dictionary TypeAttributes { get; set; } = []; + + /// + /// Gets or sets attribute instances declared at the assembly level. + /// + public object[] AssemblyAttributes { get; set; } = []; + + /// + /// Gets or sets the properties declared on each type that MSTest may inspect (for example + /// TestContext properties or properties referenced by DynamicData). + /// + public Dictionary TypeProperties { get; set; } = []; + + /// + /// Gets or sets the methods declared on each type that MSTest may inspect (test methods, + /// initialize / cleanup hooks, dynamic-data methods). + /// + public Dictionary TypeMethods { get; set; } = []; + + /// + /// Gets or sets source-location data for each type's methods so navigation in the IDE works + /// without a PDB round-trip. + /// + public Dictionary TypeMethodLocations { get; set; } = []; + + /// + /// Gets or sets attributes declared on each method, keyed by the method name. + /// + public Dictionary> TypeMethodAttributes { get; set; } = []; + + /// + /// Gets or sets constructors declared on each type. These are returned by + /// GetDeclaredConstructors. + /// + public Dictionary TypeConstructors { get; set; } = []; + + /// + /// Gets or sets a lookup of properties on a type by property name. + /// + public Dictionary> TypePropertiesByName { get; set; } = []; + + /// + /// Gets or sets the constructor invokers that allow instantiating types without reflection. + /// + public Dictionary TypeConstructorsInvoker { get; set; } = []; + + internal virtual Assembly GetAssembly(string assemblyPath) + => !string.Equals(Path.GetFileNameWithoutExtension(assemblyPath), AssemblyName, StringComparison.OrdinalIgnoreCase) + ? throw new ArgumentException($"Assembly '{assemblyPath}' is not allowed. Only '{AssemblyName}' is allowed to run in source-generator mode.") + : Assembly; + + internal virtual void GetNavigationData(string className, string methodName, out int minLineNumber, out string? fileName) + { + if (!TypeMethodLocations.TryGetValue(className, out TypeLocation? typeLocation) || typeLocation is null) + { + minLineNumber = 0; + fileName = null; + return; + } + + if (!typeLocation.MethodLocations.TryGetValue(methodName, out int lineNumber)) + { + minLineNumber = 0; + fileName = null; + return; + } + + fileName = typeLocation.FileName; + minLineNumber = lineNumber; + } + + /// + /// Returns the assembly-level attributes that belong to . Returns + /// the single-provider when the assembly matches this provider's + /// ; otherwise an empty array. The composite override fans out the lookup + /// to the owning provider. + /// + internal virtual object[] GetAssemblyAttributes(Assembly assembly) + => assembly.Equals(Assembly) ? AssemblyAttributes : []; + + /// + /// Per-type source-location information used by IDE / explorer navigation. + /// + public sealed class TypeLocation + { + /// + /// Gets or sets the source file containing the type. + /// + public string FileName { get; set; } = string.Empty; + + /// + /// Gets or sets the line number for each method, keyed by method name. + /// + public Dictionary MethodLocations { get; set; } = []; + } + + /// + /// Describes one constructor and a delegate that invokes it. Source-generated code emits + /// these to allow Activator.CreateInstance-free instantiation. + /// + public sealed class ConstructorInvoker + { + /// + /// Gets or sets the parameter types, in declaration order. + /// + public Type[] Parameters { get; set; } = []; + + /// + /// Gets or sets the delegate that invokes the constructor. + /// + public Func Invoker { get; set; } = null!; + } +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratedReflectionOperations.cs b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratedReflectionOperations.cs new file mode 100644 index 0000000000..a06e5a60c4 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratedReflectionOperations.cs @@ -0,0 +1,253 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration; + +/// +/// Source-generator-backed implementation of . All metadata is +/// read from a populated at compile time, which +/// avoids the runtime reflection calls that the regular performs. +/// When the source-generated data does not contain an entry for a given lookup (e.g. an attribute +/// added by an unaware extension, or a type defined in an assembly that does not participate in +/// source generation), the operation transparently falls back to runtime reflection so that +/// mixed scenarios keep working. +/// +internal sealed class SourceGeneratedReflectionOperations : IReflectionOperations +{ + private readonly ConcurrentDictionary _attributeCache = []; + private readonly ReflectionOperations _fallback = new(); + + public SourceGeneratedReflectionOperations(SourceGeneratedReflectionDataProvider dataProvider) + => DataProvider = dataProvider ?? throw new ArgumentNullException(nameof(dataProvider)); + + internal SourceGeneratedReflectionDataProvider DataProvider { get; } + + [return: NotNullIfNotNull(nameof(memberInfo))] + public object[]? GetCustomAttributes(MemberInfo memberInfo) + => memberInfo switch + { + null => null, + Type type => GetTypeAttributes(type), + MethodInfo method => GetMethodAttributes(method), + _ => _fallback.GetCustomAttributes(memberInfo), + }; + + public object[] GetCustomAttributes(Assembly assembly, Type type) + { + object[] sourceGenAttributes = DataProvider.GetAssemblyAttributes(assembly); + return sourceGenAttributes.Length == 0 + ? _fallback.GetCustomAttributes(assembly, type) + : [.. sourceGenAttributes.Where(type.IsInstanceOfType)]; + } + + public ConstructorInfo[] GetDeclaredConstructors(Type classType) + => DataProvider.TypeConstructors.TryGetValue(classType, out ConstructorInfo[]? constructors) + ? constructors + : _fallback.GetDeclaredConstructors(classType); + + public MethodInfo[] GetDeclaredMethods(Type classType) + => DataProvider.TypeMethods.TryGetValue(classType, out MethodInfo[]? methods) + ? methods + : _fallback.GetDeclaredMethods(classType); + + public PropertyInfo[] GetDeclaredProperties(Type type) + => DataProvider.TypeProperties.TryGetValue(type, out PropertyInfo[]? properties) + ? properties + : _fallback.GetDeclaredProperties(type); + + public Type[] GetDefinedTypes(Assembly assembly) + { + Type[] filtered = [.. DataProvider.Types.Where(t => t.Assembly.Equals(assembly))]; + return filtered.Length > 0 ? filtered : _fallback.GetDefinedTypes(assembly); + } + + public MethodInfo[] GetRuntimeMethods(Type type) => GetDeclaredMethods(type); + + public MethodInfo? GetRuntimeMethod(Type declaringType, string methodName, Type[] parameters, bool includeNonPublic) + { + foreach (MethodInfo method in GetRuntimeMethods(declaringType)) + { + if (method.Name != methodName) + { + continue; + } + + if (!includeNonPublic && !method.IsPublic) + { + continue; + } + + ParameterInfo[] candidateParameters = method.GetParameters(); + if (candidateParameters.Length != parameters.Length) + { + continue; + } + + bool matches = true; + for (int i = 0; i < parameters.Length; i++) + { + if (candidateParameters[i].ParameterType != parameters[i]) + { + matches = false; + break; + } + } + + if (matches) + { + return method; + } + } + + return _fallback.GetRuntimeMethod(declaringType, methodName, parameters, includeNonPublic); + } + + public PropertyInfo? GetRuntimeProperty(Type classType, string propertyName, bool includeNonPublic) + { + if (DataProvider.TypePropertiesByName.TryGetValue(classType, out Dictionary? properties) + && properties.TryGetValue(propertyName, out PropertyInfo? property)) + { + if (!includeNonPublic) + { + bool isPublic = property.GetMethod?.IsPublic is true || property.SetMethod?.IsPublic is true; + if (!isPublic) + { + return null; + } + } + + return property; + } + + return _fallback.GetRuntimeProperty(classType, propertyName, includeNonPublic); + } + + // `Type.GetType(string)` only resolves assembly-qualified names (or types in the calling + // assembly / mscorlib). The source-generated `TypesByName` dictionary is keyed by full type + // name without assembly qualification and aggregates entries across every registered test + // assembly, so consulting it here could silently bind to a same-named type from the wrong + // assembly. Match the runtime contract by always delegating; callers that have an assembly + // in hand use the `GetType(Assembly, string)` overload below for source-generated lookups. + public Type? GetType(string typeName) + => _fallback.GetType(typeName); + + public Type? GetType(Assembly assembly, string typeName) + => DataProvider.TypesByName.TryGetValue(typeName, out Type? type) && type.Assembly.Equals(assembly) + ? type + : _fallback.GetType(assembly, typeName); + + public object? CreateInstance(Type type, object?[] parameters) + { + if (!DataProvider.TypeConstructorsInvoker.TryGetValue(type, out SourceGeneratedReflectionDataProvider.ConstructorInvoker[]? invokers)) + { + return _fallback.CreateInstance(type, parameters); + } + + foreach (SourceGeneratedReflectionDataProvider.ConstructorInvoker invoker in invokers) + { + if (invoker.Parameters.Length != parameters.Length) + { + continue; + } + + bool matches = true; + for (int i = 0; i < parameters.Length; i++) + { + object? argument = parameters[i]; + Type expected = invoker.Parameters[i]; + + if (argument is null) + { + if (expected.IsValueType && Nullable.GetUnderlyingType(expected) is null) + { + matches = false; + break; + } + } + else if (!expected.IsInstanceOfType(argument)) + { + matches = false; + break; + } + } + + if (matches) + { + return invoker.Invoker(parameters); + } + } + + return _fallback.CreateInstance(type, parameters); + } + + public bool IsAttributeDefined(ICustomAttributeProvider attributeProvider) + where TAttribute : Attribute + => attributeProvider is null + ? throw new ArgumentNullException(nameof(attributeProvider)) + : GetCustomAttributesCached(attributeProvider).OfType().Any(); + + public TAttribute? GetFirstAttributeOrDefault(ICustomAttributeProvider attributeProvider) + where TAttribute : Attribute + => GetCustomAttributesCached(attributeProvider).OfType().FirstOrDefault(); + + public TAttribute? GetSingleAttributeOrDefault(ICustomAttributeProvider attributeProvider) + where TAttribute : Attribute + { + TAttribute[] matches = [.. GetCustomAttributesCached(attributeProvider).OfType().Take(2)]; + return matches.Length switch + { + 0 => null, + 1 => matches[0], + _ => throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Found multiple attributes of type '{0}' when only one was expected.", typeof(TAttribute))), + }; + } + + public IEnumerable GetAttributes(ICustomAttributeProvider attributeProvider) + where TAttributeType : Attribute + => GetCustomAttributesCached(attributeProvider).OfType(); + + public Attribute[] GetCustomAttributesCached(ICustomAttributeProvider attributeProvider) + => attributeProvider is null + ? throw new ArgumentNullException(nameof(attributeProvider)) + : attributeProvider is MemberInfo or Assembly + ? _attributeCache.GetOrAdd(attributeProvider, GetAttributesForProvider) + : throw new ArgumentException( + $"Unsupported attribute provider type: {attributeProvider.GetType()}. Only MemberInfo and Assembly are supported.", + nameof(attributeProvider)); + + private Attribute[] GetAttributesForProvider(ICustomAttributeProvider provider) + => provider switch + { + Type type => GetTypeAttributes(type), + MethodInfo method => GetMethodAttributes(method), + Assembly assembly => GetAssemblyAttributesForProvider(assembly), + _ => _fallback.GetCustomAttributesCached(provider), + }; + + private Attribute[] GetAssemblyAttributesForProvider(Assembly assembly) + { + object[] sourceGen = DataProvider.GetAssemblyAttributes(assembly); + return sourceGen.Length > 0 + ? [.. sourceGen.OfType()] + : _fallback.GetCustomAttributesCached(assembly); + } + + public bool IsMethodDeclaredInSameAssemblyAsType(MethodInfo method, Type type) + => method.DeclaringType?.Assembly.Equals(type.Assembly) ?? false; + + internal void ClearCache() => _attributeCache.Clear(); + + private Attribute[] GetTypeAttributes(Type type) + => DataProvider.TypeAttributes.TryGetValue(type, out Attribute[]? attributes) + ? attributes + : _fallback.GetCustomAttributesCached(type); + + private Attribute[] GetMethodAttributes(MethodInfo method) + => method.DeclaringType is not null + && DataProvider.TypeMethodAttributes.TryGetValue(method.DeclaringType, out Dictionary? methodAttributes) + && methodAttributes.TryGetValue(method.Name, out Attribute[]? attributes) + ? attributes + : _fallback.GetCustomAttributesCached(method); +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratorToggle.cs b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratorToggle.cs new file mode 100644 index 0000000000..7936a0506b --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratorToggle.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration; + +/// +/// Tracks whether MSTest's runtime should use the source-generated reflection metadata or fall +/// back to the regular reflection-based code paths. The toggle is one-way: once flipped to +/// it stays . It is safe to call +/// from multiple threads or to call it more than once; subsequent calls +/// are no-ops. +/// +internal static class SourceGeneratorToggle +{ + private static int s_useSourceGenerator; + + public static bool UseSourceGenerator => Volatile.Read(ref s_useSourceGenerator) != 0; + + public static void Enable() => Interlocked.Exchange(ref s_useSourceGenerator, 1); +} diff --git a/src/Analyzers/MSTest.SourceGeneration/.editorconfig b/src/Analyzers/MSTest.SourceGeneration/.editorconfig deleted file mode 100644 index 7d7bac49f4..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/.editorconfig +++ /dev/null @@ -1,2 +0,0 @@ -[*.{cs,vb}] -file_header_template = Copyright (c) Microsoft Corporation. All rights reserved.\nLicensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. diff --git a/src/Analyzers/MSTest.SourceGeneration/BannedSymbols.txt b/src/Analyzers/MSTest.SourceGeneration/BannedSymbols.txt index b46f825771..0b910a6768 100644 --- a/src/Analyzers/MSTest.SourceGeneration/BannedSymbols.txt +++ b/src/Analyzers/MSTest.SourceGeneration/BannedSymbols.txt @@ -1,2 +1,2 @@ -M:System.Text.StringBuilder.AppendLine(); Calls to 'Environment.NewLine' should be avoided in source generators +M:System.Text.StringBuilder.AppendLine(); Calls to 'Environment.NewLine' should be avoided in source generators M:System.Text.StringBuilder.AppendLine(System.String); Calls to 'Environment.NewLine' should be avoided in source generators diff --git a/src/Analyzers/MSTest.SourceGeneration/Emitters/ReflectionMetadataEmitter.cs b/src/Analyzers/MSTest.SourceGeneration/Emitters/ReflectionMetadataEmitter.cs new file mode 100644 index 0000000000..89ec0e13c7 --- /dev/null +++ b/src/Analyzers/MSTest.SourceGeneration/Emitters/ReflectionMetadataEmitter.cs @@ -0,0 +1,181 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Helpers; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Models; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Emitters; + +/// +/// Produces the C# source that wires the test assembly into MSTest's source-generator runtime. +/// +internal static class ReflectionMetadataEmitter +{ + public const string GeneratedTypeName = "MSTestSourceGeneratedReflectionMetadata"; + + public static string Emit(TestAssemblyMetadata metadata) + { + var sb = new StringBuilder(); + + Append(sb, "// "); + Append(sb, "// This file was generated by the MSTest source generator."); + Append(sb, "// Do not edit it manually; changes will be lost on the next build."); + Append(sb, "#nullable enable"); + Append(sb, string.Empty); + Append(sb, "using System;"); + Append(sb, "using System.Collections.Generic;"); + Append(sb, "using System.Reflection;"); + Append(sb, "using System.Runtime.CompilerServices;"); + Append(sb, string.Empty); + Append(sb, "namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Generated"); + Append(sb, "{"); + Append(sb, " /// Source-generated MSTest reflection metadata hook for this test assembly."); + Append(sb, $" internal static class {GeneratedTypeName}"); + Append(sb, " {"); + Append(sb, " [ModuleInitializer]"); + Append(sb, " internal static void Initialize()"); + Append(sb, " {"); + Append(sb, $" var assembly = typeof({GeneratedTypeName}).Assembly;"); + Append(sb, $" var metadata = new {Constants.SourceGeneratedReflectionDataProviderFullName}"); + Append(sb, " {"); + Append(sb, " Assembly = assembly,"); + Append(sb, $" AssemblyName = {ToCSharpLiteral(metadata.AssemblyName)},"); + EmitTypesArray(sb, metadata); + EmitTypesByName(sb, metadata); + EmitTypeMethods(sb, metadata); + Append(sb, " };"); + Append(sb, string.Empty); + Append(sb, $" {Constants.ReflectionMetadataHookFullName}.SetMetadata(metadata);"); + Append(sb, " }"); + Append(sb, string.Empty); + Append(sb, " private static MethodInfo ResolveMethod(Type type, string name, Type[] parameterTypes)"); + Append(sb, " {"); + // Include inherited methods (no DeclaredOnly) so test methods defined on an abstract base + // class are resolvable through the concrete test class's typeof(). + Append(sb, " const BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;"); + Append(sb, " foreach (MethodInfo candidate in type.GetMethods(flags))"); + Append(sb, " {"); + Append(sb, " if (candidate.Name != name)"); + Append(sb, " {"); + Append(sb, " continue;"); + Append(sb, " }"); + Append(sb, string.Empty); + Append(sb, " ParameterInfo[] candidateParameters = candidate.GetParameters();"); + Append(sb, " if (candidateParameters.Length != parameterTypes.Length)"); + Append(sb, " {"); + Append(sb, " continue;"); + Append(sb, " }"); + Append(sb, string.Empty); + Append(sb, " bool match = true;"); + Append(sb, " for (int i = 0; i < candidateParameters.Length; i++)"); + Append(sb, " {"); + Append(sb, " if (candidateParameters[i].ParameterType != parameterTypes[i])"); + Append(sb, " {"); + Append(sb, " match = false;"); + Append(sb, " break;"); + Append(sb, " }"); + Append(sb, " }"); + Append(sb, string.Empty); + Append(sb, " if (match)"); + Append(sb, " {"); + Append(sb, " return candidate;"); + Append(sb, " }"); + Append(sb, " }"); + Append(sb, string.Empty); + Append(sb, " throw new MissingMethodException(type.FullName, name);"); + Append(sb, " }"); + Append(sb, " }"); + Append(sb, "}"); + + return sb.ToString(); + } + + private static void EmitTypesArray(StringBuilder sb, TestAssemblyMetadata metadata) + { + if (metadata.Classes.Count == 0) + { + Append(sb, " Types = Array.Empty(),"); + return; + } + + Append(sb, " Types = new Type[]"); + Append(sb, " {"); + foreach (TestClassMetadata cls in metadata.Classes) + { + Append(sb, $" typeof({cls.FullyQualifiedName}),"); + } + + Append(sb, " },"); + } + + private static void EmitTypesByName(StringBuilder sb, TestAssemblyMetadata metadata) + { + if (metadata.Classes.Count == 0) + { + Append(sb, " TypesByName = new Dictionary(),"); + return; + } + + // Emit typeof(X).FullName! at runtime so the key always matches the value the consumer + // gets from Type.FullName — including for nested types (Outer+Nested) and generic types + // (MyClass`1[[…]]) where a compile-time string would not match. + Append(sb, " TypesByName = new Dictionary"); + Append(sb, " {"); + foreach (TestClassMetadata cls in metadata.Classes) + { + Append(sb, $" [typeof({cls.FullyQualifiedName}).FullName!] = typeof({cls.FullyQualifiedName}),"); + } + + Append(sb, " },"); + } + + private static void EmitTypeMethods(StringBuilder sb, TestAssemblyMetadata metadata) + { + if (metadata.Classes.Count == 0) + { + Append(sb, " TypeMethods = new Dictionary(),"); + return; + } + + Append(sb, " TypeMethods = new Dictionary"); + Append(sb, " {"); + foreach (TestClassMetadata cls in metadata.Classes) + { + Append(sb, $" [typeof({cls.FullyQualifiedName})] = new MethodInfo[]"); + Append(sb, " {"); + foreach (TestMethodMetadata method in cls.Methods) + { + string parameterTypesArray; + if (method.ParameterTypes.Count == 0) + { + parameterTypesArray = "Type.EmptyTypes"; + } + else + { + var parts = new List(method.ParameterTypes.Count); + foreach (string parameterType in method.ParameterTypes) + { + parts.Add($"typeof({parameterType})"); + } + + parameterTypesArray = "new Type[] { " + string.Join(", ", parts) + " }"; + } + + Append(sb, $" ResolveMethod(typeof({cls.FullyQualifiedName}), {ToCSharpLiteral(method.Name)}, {parameterTypesArray}),"); + } + + Append(sb, " },"); + } + + Append(sb, " },"); + } + + private static void Append(StringBuilder sb, string line) + { + sb.Append(line); + sb.Append(Constants.NewLine); + } + + private static string ToCSharpLiteral(string value) + => "\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\""; +} diff --git a/src/Analyzers/MSTest.SourceGeneration/Generators/ReflectionMetadataGenerator.cs b/src/Analyzers/MSTest.SourceGeneration/Generators/ReflectionMetadataGenerator.cs new file mode 100644 index 0000000000..3feed0f7c8 --- /dev/null +++ b/src/Analyzers/MSTest.SourceGeneration/Generators/ReflectionMetadataGenerator.cs @@ -0,0 +1,217 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Immutable; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Emitters; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Helpers; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Models; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Generators; + +/// +/// Incremental source generator that walks every [TestClass] in the compilation and emits +/// a [ModuleInitializer] that registers a SourceGeneratedReflectionDataProvider with +/// MSTest's runtime hook. When the test project opts in (by referencing this generator), MSTest +/// will read test metadata from the source-generated data instead of using runtime reflection. +/// +[Generator(LanguageNames.CSharp)] +public sealed class ReflectionMetadataGenerator : IIncrementalGenerator +{ + /// + /// Display format for parameter types in typeof(...) expressions. Mirrors + /// but omits UseSpecialTypes so + /// primitive types emit as global::System.Int32 rather than int, ensuring the + /// generated typeof() calls compile in any namespace context. + /// + private static readonly SymbolDisplayFormat ParameterTypeFormat = new( + globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Included, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, + genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, + miscellaneousOptions: SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers); + + /// + public void Initialize(IncrementalGeneratorInitializationContext context) + { + IncrementalValuesProvider testClasses = context.SyntaxProvider.ForAttributeWithMetadataName( + Constants.TestClassAttributeFullName, + predicate: static (node, _) => node is TypeDeclarationSyntax, + transform: static (ctx, ct) => BuildTestClass(ctx, ct)); + + IncrementalValueProvider> collected = testClasses.Collect(); + + IncrementalValueProvider<(string? AssemblyName, ImmutableArray Classes)> source = + context.CompilationProvider + .Select(static (compilation, _) => compilation.AssemblyName) + .Combine(collected); + + context.RegisterImplementationSourceOutput(source, EmitMetadata); + } + + private static TestClassMetadata? BuildTestClass(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken) + { + if (context.TargetSymbol is not INamedTypeSymbol typeSymbol) + { + return null; + } + + if (typeSymbol.IsAbstract || typeSymbol.IsStatic) + { + return null; + } + + // Skip open generic test classes: typeof(Generic) is not valid at the module-initializer + // scope where we emit the metadata, and reflecting on an unbound generic type by method + // signature is unreliable. Closed-constructed generics that the user writes are not + // top-level [TestClass] declarations, so they are not seen here either. + if (typeSymbol.IsGenericType) + { + return null; + } + + // Skip types the generated module initializer (emitted as `internal`) cannot reference, + // for example a private/protected nested [TestClass]. Emitting `typeof(Outer.PrivateNested)` + // would fail with CS0122 inside auto-generated code. + if (!IsAccessibleFromGeneratedCode(typeSymbol)) + { + return null; + } + + string fullyQualifiedName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + string? containingNamespace = typeSymbol.ContainingNamespace is { IsGlobalNamespace: false } ns + ? ns.ToDisplayString() + : null; + + ImmutableArray.Builder methods = ImmutableArray.CreateBuilder(); + var seenSignatures = new HashSet(StringComparer.Ordinal); + INamedTypeSymbol? currentType = typeSymbol; + while (currentType is not null && currentType.SpecialType != SpecialType.System_Object) + { + cancellationToken.ThrowIfCancellationRequested(); + + foreach (ISymbol member in currentType.GetMembers()) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (member is not IMethodSymbol method || method.MethodKind != MethodKind.Ordinary) + { + continue; + } + + if (!HasTestMethodAttribute(method)) + { + continue; + } + + // Skip methods that take ref/out/in/ref-readonly parameters. The runtime + // ResolveMethod compares parameter types by `typeof(T) == ParameterType`, but + // for by-ref parameters `ParameterType` is `T&` (i.e. `MakeByRefType()`), so a + // naive `typeof(T)` match always fails and ResolveMethod would throw inside the + // module initializer — aborting test discovery for the whole assembly. The + // experimental source-gen path doesn't model by-ref signatures yet, so skip. + if (HasByRefParameter(method)) + { + continue; + } + + ImmutableArray.Builder parameterTypes = ImmutableArray.CreateBuilder(method.Parameters.Length); + foreach (IParameterSymbol parameter in method.Parameters) + { + parameterTypes.Add(parameter.Type.ToDisplayString(ParameterTypeFormat)); + } + + ImmutableArray parameterTypeArray = parameterTypes.MoveToImmutable(); + + // Dedupe inherited methods by (name, parameter-signature) so an override is emitted once. + string signature = method.Name + "(" + string.Join(",", parameterTypeArray) + ")"; + if (!seenSignatures.Add(signature)) + { + continue; + } + + methods.Add(new TestMethodMetadata(method.Name, new EquatableArray(parameterTypeArray))); + } + + currentType = currentType.BaseType; + } + + return new TestClassMetadata( + FullyQualifiedName: fullyQualifiedName, + DisplayName: typeSymbol.Name, + Namespace: containingNamespace, + Methods: new EquatableArray(methods.ToImmutable())); + } + + private static bool HasByRefParameter(IMethodSymbol method) + => method.Parameters.Any(parameter => parameter.RefKind != RefKind.None); + + private static bool IsAccessibleFromGeneratedCode(INamedTypeSymbol type) + { + // The generated module initializer lives in the same assembly, so anything visible at + // `internal` or above works. `NotApplicable` is the default for top-level types and is + // also fine. Anything stricter (private, protected, private protected) is rejected. + for (INamedTypeSymbol? current = type; current is not null; current = current.ContainingType) + { + // `file`-scoped types are only addressable from within their own source file, so the + // generated module initializer (which lives in a different file) cannot reference them. + if (current.IsFileLocal) + { + return false; + } + + switch (current.DeclaredAccessibility) + { + case Accessibility.Private: + case Accessibility.Protected: + case Accessibility.ProtectedAndInternal: + return false; + } + } + + return true; + } + + private static bool HasTestMethodAttribute(IMethodSymbol method) + { + foreach (INamedTypeSymbol? attributeClassCandidate in method.GetAttributes().Select(static a => a.AttributeClass)) + { + INamedTypeSymbol? attributeClass = attributeClassCandidate; + while (attributeClass is not null) + { + if (attributeClass.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + == "global::Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute") + { + return true; + } + + attributeClass = attributeClass.BaseType; + } + } + + return false; + } + + private static void EmitMetadata(SourceProductionContext context, (string? AssemblyName, ImmutableArray Classes) input) + { + string assemblyName = input.AssemblyName ?? "UnknownAssembly"; + ImmutableArray classes = input.Classes.IsDefault + ? ImmutableArray.Empty + : input.Classes.Where(static c => c is not null).Cast().ToImmutableArray(); + + // Skip emission entirely when the compilation has no test classes — there is nothing for + // the runtime hook to register and emitting a [ModuleInitializer] just for that adds cost. + if (classes.IsEmpty) + { + return; + } + + var metadata = new TestAssemblyMetadata( + AssemblyName: assemblyName, + Classes: new EquatableArray(classes)); + + string source = ReflectionMetadataEmitter.Emit(metadata); + context.AddSource($"{assemblyName}{Constants.GeneratedFileSuffix}", source); + } +} diff --git a/src/Analyzers/MSTest.SourceGeneration/Generators/TestNodesGenerator.cs b/src/Analyzers/MSTest.SourceGeneration/Generators/TestNodesGenerator.cs deleted file mode 100644 index a1ca1ed89c..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Generators/TestNodesGenerator.cs +++ /dev/null @@ -1,243 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.Testing.Framework.SourceGeneration.Helpers; -using Microsoft.Testing.Framework.SourceGeneration.ObjectModels; - -namespace Microsoft.Testing.Framework.SourceGeneration; - -[Generator] -internal sealed class TestNodesGenerator : IIncrementalGenerator -{ - public void Initialize(IncrementalGeneratorInitializationContext context) - { - IncrementalValuesProvider testClassesProvider = context.SyntaxProvider.ForAttributeWithMetadataName( - "Microsoft.VisualStudio.TestTools.UnitTesting.TestClassAttribute", - static (node, _) => - node is TypeDeclarationSyntax typeDeclarationSyntax - && (typeDeclarationSyntax.Modifiers.Any(SyntaxKind.PublicKeyword) || typeDeclarationSyntax.Modifiers.Any(SyntaxKind.InternalKeyword)) - // No static classes. - && !typeDeclarationSyntax.Modifiers.Any(SyntaxKind.StaticKeyword), - static (context, _) => - { - WellKnownTypes wellKnownTypes = new(context.SemanticModel.Compilation); - var testClassInfo = TestTypeInfo.TryBuild(context, wellKnownTypes); - return testClassInfo; - }) - .WhereNotNull(); - - // Generate a file with one static class and one static TestNode field for all public classes we find - context.RegisterImplementationSourceOutput(testClassesProvider, AddTestClassNode); - - IncrementalValueProvider<(string? Left, ImmutableArray Right)> assemblyNamespacesProvider - = context.CompilationProvider.Select((compilation, _) => compilation.AssemblyName) - .Combine(testClassesProvider.Collect()); - - context.RegisterImplementationSourceOutput(assemblyNamespacesProvider, AddAssemblyTestNode); - } - - private static void AddAssemblyTestNode(SourceProductionContext context, (string? AssemblyName, ImmutableArray TestClasses) provider) - { - string assemblyName = provider.AssemblyName ?? ""; - ImmutableArray testClasses = provider.TestClasses; - - var sourceStringBuilder = new IndentedStringBuilder(); - sourceStringBuilder.AppendAutoGeneratedHeader(); - sourceStringBuilder.AppendLine(); - - TestNamespaceInfo[] uniqueUsedNamespaces = [.. testClasses - .Select(x => x.ContainingNamespace) - .Distinct()]; - - string? safeAssemblyName = null; - IDisposable? namespaceBlock = null; - try - { - if (!uniqueUsedNamespaces.Any(x => x.IsGlobalNamespace)) - { - safeAssemblyName = ToSafeNamespace(assemblyName); - - // TODO: We should look for the default namespace, if made visible to the compiler, or default to assembly name. - namespaceBlock = sourceStringBuilder.AppendBlock($"namespace {safeAssemblyName}"); - } - - foreach (TestNamespaceInfo usedNamespace in uniqueUsedNamespaces) - { - if (!usedNamespace.IsGlobalNamespace) - { - sourceStringBuilder.AppendLine($"using {usedNamespace.FullyQualifiedName};"); - } - } - - sourceStringBuilder.AppendLine("using ColGen = global::System.Collections.Generic;"); - sourceStringBuilder.AppendLine("using CA = global::System.Diagnostics.CodeAnalysis;"); - sourceStringBuilder.AppendLine("using Sys = global::System;"); - sourceStringBuilder.AppendLine("using Tasks = global::System.Threading.Tasks;"); - sourceStringBuilder.AppendLine("using Msg = global::Microsoft.Testing.Platform.Extensions.Messages;"); - sourceStringBuilder.AppendLine("using MSTF = global::Microsoft.Testing.Framework;"); - sourceStringBuilder.AppendLine("using Cap = global::Microsoft.Testing.Platform.Capabilities.TestFramework;"); - sourceStringBuilder.AppendLine("using TrxReport = global::Microsoft.Testing.Extensions.TrxReport.Abstractions;"); - sourceStringBuilder.AppendLine(); - - sourceStringBuilder.AppendLine("[CA::ExcludeFromCodeCoverage]"); - using (sourceStringBuilder.AppendBlock("public sealed class SourceGeneratedTestNodesBuilder : MSTF::ITestNodesBuilder")) - { - using (sourceStringBuilder.AppendBlock("private sealed class ClassCapabilities : TrxReport::ITrxReportCapability")) - { - string isTrxReportSupported = testClasses.IsEmpty ? "false" : "true"; - sourceStringBuilder.AppendLine($"bool TrxReport::ITrxReportCapability.IsSupported {{ get; }} = {isTrxReportSupported};"); - sourceStringBuilder.AppendLine("void TrxReport::ITrxReportCapability.Enable() {}"); - } - - sourceStringBuilder.AppendLine(); - sourceStringBuilder.AppendLine("public ColGen::IReadOnlyCollection Capabilities { get; } = new Cap::ITestFrameworkCapability[1] { new ClassCapabilities() };"); - sourceStringBuilder.AppendLine(); - - using (sourceStringBuilder.AppendBlock($"public Tasks::Task BuildAsync(MSTF::ITestSessionContext testSessionContext)")) - { - if (testClasses.IsEmpty) - { - sourceStringBuilder.AppendLine("return Tasks::Task.FromResult(Sys::Array.Empty());"); - } - else - { - AppendAssemblyTestNodeBuilderContent(sourceStringBuilder, assemblyName, testClasses); - } - } - } - } - finally - { - namespaceBlock?.Dispose(); - } - - string code = sourceStringBuilder.ToString(); - // DEBUG: Debug.WriteLine is useful to observe the code when changing the source code generator or applying it to a new test suite. - // VS is caching the generator, so start DebugView++ and just rebuild the TestContainer to make changes, - // and observe the compiler process only (csc.exe). - // Debug.WriteLine(code); - context.AddSource("SourceGeneratedTestNodesBuilder.g.cs", code); - - IndentedStringBuilder hookCode = new(); - hookCode.AppendAutoGeneratedHeader(); - using (hookCode.AppendBlock("namespace Microsoft.Testing.Framework.SourceGeneration")) - { - hookCode.AppendLine("[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]"); - using (hookCode.AppendBlock("public static class SourceGeneratedTestingPlatformBuilderHook")) - { - using (hookCode.AppendBlock("public static void AddExtensions(Microsoft.Testing.Platform.Builder.ITestApplicationBuilder testApplicationBuilder, string[] _)")) - { - hookCode.AppendLine("testApplicationBuilder.AddTestFramework(new Microsoft.Testing.Framework.Configurations.TestFrameworkConfiguration(System.Environment.ProcessorCount),"); - hookCode.IndentationLevel++; - hookCode.AppendLine($"new {(safeAssemblyName is not null ? safeAssemblyName + "." : string.Empty)}SourceGeneratedTestNodesBuilder());"); - hookCode.IndentationLevel--; - } - } - } - - // Add a hook to the test platform builder to register the test framework to MSBuild. - context.AddSource("SourceGeneratedTestingPlatformBuilderHook.g.cs", hookCode.ToString()); - } - - private static void AppendAssemblyTestNodeBuilderContent(IndentedStringBuilder sourceStringBuilder, string assemblyName, - ImmutableArray testClasses) - { - Dictionary rootVariablesPerNamespace = []; - int variableIndex = 1; - IEnumerable> classesPerNamespaces = testClasses.GroupBy(x => x.ContainingNamespace); - foreach (IGrouping namespaceClasses in classesPerNamespaces) - { - string namespaceTestsVariableName = $"namespace{variableIndex}Tests"; - rootVariablesPerNamespace.Add(namespaceClasses.Key, namespaceTestsVariableName); - sourceStringBuilder.AppendLine($"ColGen::List {namespaceTestsVariableName} = new();"); - - foreach (TestTypeInfo testClassInfo in namespaceClasses) - { - sourceStringBuilder.AppendLine($"{namespaceTestsVariableName}.Add({testClassInfo.GeneratedTypeName}.TestNode);"); - } - - variableIndex++; - sourceStringBuilder.AppendLine(); - } - - sourceStringBuilder.Append("MSTF::TestNode root = "); - - using (sourceStringBuilder.AppendTestNode(assemblyName, assemblyName, [], ';')) - { - foreach (IGrouping group in classesPerNamespaces) - { - group.Key.AppendNamespaceTestNode(sourceStringBuilder, rootVariablesPerNamespace[group.Key]); - } - } - - sourceStringBuilder.AppendLine(); - sourceStringBuilder.AppendLine("return Tasks::Task.FromResult(new MSTF::TestNode[1] { root });"); - } - - private static void AddTestClassNode(SourceProductionContext context, TestTypeInfo testClassInfo) - { - var sourceStringBuilder = new IndentedStringBuilder(); - sourceStringBuilder.AppendAutoGeneratedHeader(); - sourceStringBuilder.AppendLine(); - - testClassInfo.AppendTestNode(sourceStringBuilder); - - string code = sourceStringBuilder.ToString(); - // DEBUG: Debug.WriteLine is useful to observe the code when changing the source code generator or applying it to a new test suite. - // VS is caching the generator, so start DebugView++ and just rebuild the TestContainer to make changes, - // and observe the compiler process only (csc.exe). - // Debug.WriteLine(code); - context.AddSource($"{testClassInfo.FullyQualifiedName}.g.cs", code); - } - - // Borrowed from https://github.com/dotnet/templating/blob/dad34814012bf29aa35eaf8e8013af4b10b997da/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ValueForms/DefaultSafeNamespaceValueFormFactory.cs#L10 - internal /* for testing purpose */ static string ToSafeNamespace(string value) - { - const char invalidCharacterReplacement = '_'; - - value = value ?? throw new ArgumentNullException(nameof(value)); - value = value.Trim(); - - StringBuilder safeValueStr = new(value.Length); - - for (int i = 0; i < value.Length; i++) - { - if (i < value.Length - 1 && char.IsSurrogatePair(value[i], value[i + 1])) - { - safeValueStr.Append(invalidCharacterReplacement); - // Skip both chars that make up this symbol. - i++; - continue; - } - - bool isFirstCharacterOfIdentifier = safeValueStr.Length == 0 || safeValueStr[safeValueStr.Length - 1] == '.'; - bool isValidFirstCharacter = UnicodeCharacterUtilities.IsIdentifierStartCharacter(value[i]); - bool isValidPartCharacter = UnicodeCharacterUtilities.IsIdentifierPartCharacter(value[i]); - - if (isFirstCharacterOfIdentifier && !isValidFirstCharacter && isValidPartCharacter) - { - // This character cannot be at the beginning, but is good otherwise. Prefix it with something valid. - safeValueStr.Append(invalidCharacterReplacement); - safeValueStr.Append(value[i]); - } - else if ((isFirstCharacterOfIdentifier && isValidFirstCharacter) - || (!isFirstCharacterOfIdentifier && isValidPartCharacter) - || (safeValueStr.Length > 0 && i < value.Length - 1 && value[i] == '.')) - { - // This character is allowed to be where it is. - safeValueStr.Append(value[i]); - } - else - { - safeValueStr.Append(invalidCharacterReplacement); - } - } - - return safeValueStr.ToString(); - } -} diff --git a/src/Analyzers/MSTest.SourceGeneration/GlobalSuppressions.cs b/src/Analyzers/MSTest.SourceGeneration/GlobalSuppressions.cs deleted file mode 100644 index 905a836dd9..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/GlobalSuppressions.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -// This file is used by Code Analysis to maintain SuppressMessage -// attributes that are applied to this project. -// Project-level suppressions either have no target or are given -// a specific target and scoped to a namespace, type, member, etc. using System.Diagnostics.CodeAnalysis; -[assembly: SuppressMessage("Style", "IDE0056:Use index operator", Justification = "Needed when we build with native AOT because there we force net8 target.", Scope = "member", Target = "~M:Microsoft.Testing.Framework.SourceGeneration.ObjectModels.DataRowTestMethodArgumentsInfo.AppendArguments(Microsoft.Testing.Framework.SourceGeneration.Helpers.IndentedStringBuilder)")] -[assembly: SuppressMessage("Style", "IDE0057:Use range operator", Justification = "Needed when we build with native AOT because there we force net8 target.", Scope = "member", Target = "~M:Microsoft.Testing.Framework.SourceGeneration.ObjectModels.DataRowTestMethodArgumentsInfo.AppendArguments(Microsoft.Testing.Framework.SourceGeneration.Helpers.IndentedStringBuilder)")] diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/AttributeDataExtensions.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/AttributeDataExtensions.cs deleted file mode 100644 index 20dccf541c..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/AttributeDataExtensions.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.CodeAnalysis; - -namespace Microsoft.Testing.Framework.SourceGeneration.Helpers; - -internal static class AttributeDataExtensions -{ - public static bool TryGetTestExecutionTimeout(this AttributeData attribute, INamedTypeSymbol? executionTimeoutAttributeSymbol, - INamedTypeSymbol? timeSpanSymbol, out TimeSpan executionTimeout) - { - if (!SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, executionTimeoutAttributeSymbol)) - { - executionTimeout = default; - return false; - } - - return TryGetTimeoutValue(attribute, timeSpanSymbol, out executionTimeout); - } - - private static bool TryGetTimeoutValue(this AttributeData attribute, INamedTypeSymbol? timeSpanSymbol, out TimeSpan timeout) - { - if (attribute.ConstructorArguments.Length == 1 - && attribute.ConstructorArguments[0].Type is { } executionTimeoutCtorArgType - && attribute.ConstructorArguments[0].Value is { } executionTimeoutCtorArgValue) - { - if (executionTimeoutCtorArgType.SpecialType is SpecialType.System_Int32 or SpecialType.System_Int64) - { - timeout = TimeSpan.FromMilliseconds((int)executionTimeoutCtorArgValue); - return true; - } - else if (SymbolEqualityComparer.Default.Equals(executionTimeoutCtorArgType, timeSpanSymbol)) - { - timeout = (TimeSpan)executionTimeoutCtorArgValue; - return true; - } - } - - timeout = default; - return false; - } -} diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/Constants.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/Constants.cs index 7a968202d0..574f330477 100644 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/Constants.cs +++ b/src/Analyzers/MSTest.SourceGeneration/Helpers/Constants.cs @@ -1,9 +1,22 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.Testing.Framework.SourceGeneration; +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Helpers; internal static class Constants { - public const string NewLine = "\r\n"; + /// + /// Use a constant newline to make the generator output stable across operating systems. + /// + public const string NewLine = "\n"; + + public const string TestClassAttributeFullName = "Microsoft.VisualStudio.TestTools.UnitTesting.TestClassAttribute"; + + public const string ReflectionMetadataHookFullName = + "global::Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.ReflectionMetadataHook"; + + public const string SourceGeneratedReflectionDataProviderFullName = + "global::Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider"; + + public const string GeneratedFileSuffix = ".MSTestReflectionMetadata.g.cs"; } diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/DisposableAction.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/DisposableAction.cs deleted file mode 100644 index e3a0266363..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/DisposableAction.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework.SourceGeneration.Helpers; - -internal readonly struct DisposableAction : IDisposable -{ - public Action Action { get; } - - public DisposableAction(Action action) => Action = action; - - public void Dispose() => Action(); -} diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/EnumerableExtensions.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/EnumerableExtensions.cs deleted file mode 100644 index 24830dd662..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/EnumerableExtensions.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework.SourceGeneration.Helpers; - -internal static class EnumerableExtensions -{ - private static readonly Func NotNullTest = x => x != null; - - public static IEnumerable WhereNotNull(this IEnumerable source) - where T : class - => source.Where((Func)NotNullTest)!; -} diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/EquatableArray{T}.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/EquatableArray{T}.cs deleted file mode 100644 index cbe7db49a9..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/EquatableArray{T}.cs +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using System.Collections.Immutable; - -namespace MSTest.SourceGeneration.Helpers; - -// Copy from https://github.com/Sergio0694/ComputeSharp/blob/120aff270539996ef9fc52fe46561d12da0b89d4/src/ComputeSharp.SourceGeneration/Helpers/EquatableArray%7BT%7D.cs - -/// -/// An immutable, equatable array. This is equivalent to but with value equality support. -/// -/// The type of values in the array. -/// The input to wrap. -internal readonly struct EquatableArray(ImmutableArray array) : IEquatable>, IEnumerable - where T : IEquatable -{ - /// - /// The underlying array. - /// - private readonly T[]? _array = Unsafe.As, T[]?>(ref array); - - /// - /// Gets a reference to an item at a specified position within the array. - /// - /// The index of the item to retrieve a reference to. - /// A reference to an item at a specified position within the array. - public ref readonly T this[int index] - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => ref AsImmutableArray().ItemRef(index); - } - - /// - /// Gets a value indicating whether the current array is empty. - /// - public bool IsEmpty - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => AsImmutableArray().IsEmpty; - } - - /// - /// Gets a value indicating whether the current array is default or empty. - /// - public bool IsDefaultOrEmpty - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => AsImmutableArray().IsDefaultOrEmpty; - } - - /// - /// Gets the length of the current array. - /// - public int Length - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => AsImmutableArray().Length; - } - - /// - public bool Equals(EquatableArray array) - => AsSpan().SequenceEqual(array.AsSpan()); - - public override bool Equals(object? obj) - => obj is EquatableArray array && Equals(this, array); - - /// - public override unsafe int GetHashCode() - { - if (_array is not T[] array) - { - return 0; - } - - HashCode hashCode = default; - - if (typeof(T) == typeof(byte)) - { - ReadOnlySpan span = array; - ref T r0 = ref MemoryMarshal.GetReference(span); - ref byte r1 = ref Unsafe.As(ref r0); - - fixed (byte* p = &r1) - { - ReadOnlySpan bytes = new(p, span.Length); - - hashCode.AddBytes(bytes); - } - } - else - { - foreach (T item in array) - { - hashCode.Add(item); - } - } - - return hashCode.ToHashCode(); - } - - /// - /// Gets an instance from the current . - /// - /// The from the current . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ImmutableArray AsImmutableArray() - => Unsafe.As>(ref Unsafe.AsRef(in _array)); - - /// - /// Creates an instance from a given . - /// - /// The input instance. - /// An instance from a given . - public static EquatableArray FromImmutableArray(ImmutableArray array) - => new(array); - - /// - /// Returns a wrapping the current items. - /// - /// A wrapping the current items. - public ReadOnlySpan AsSpan() - => AsImmutableArray().AsSpan(); - - /// - /// Copies the contents of this instance. to a mutable array. - /// - /// The newly instantiated array. - public T[] ToArray() - => [.. AsImmutableArray()]; - - /// - /// Gets an value to traverse items in the current array. - /// - /// An value to traverse items in the current array. - public ImmutableArray.Enumerator GetEnumerator() - => AsImmutableArray().GetEnumerator(); - - /// - IEnumerator IEnumerable.GetEnumerator() - => ((IEnumerable)AsImmutableArray()).GetEnumerator(); - - /// - IEnumerator IEnumerable.GetEnumerator() - => ((IEnumerable)AsImmutableArray()).GetEnumerator(); - - /// - /// Implicitly converts an to . - /// - /// An instance from a given . - public static implicit operator EquatableArray(ImmutableArray array) => FromImmutableArray(array); - - /// - /// Implicitly converts an to . - /// - /// An instance from a given . - public static implicit operator ImmutableArray(EquatableArray array) => array.AsImmutableArray(); - - /// - /// Checks whether two values are the same. - /// - /// The first value. - /// The second value. - /// Whether and are equal. - public static bool operator ==(EquatableArray left, EquatableArray right) => left.Equals(right); - - /// - /// Checks whether two values are not the same. - /// - /// The first value. - /// The second value. - /// Whether and are not equal. - public static bool operator !=(EquatableArray left, EquatableArray right) => !left.Equals(right); -} diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/ExceptionUtils.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/ExceptionUtils.cs deleted file mode 100644 index 084f032795..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/ExceptionUtils.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework.SourceGeneration.Helpers; - -internal static class ApplicationStateGuard -{ - internal static InvalidOperationException Unreachable([CallerFilePath] string? path = null, [CallerLineNumber] int line = 0) - => new($"This program location is thought to be unreachable. File='{path}' Line={line}"); -} diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/FileHeaderUtils.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/FileHeaderUtils.cs deleted file mode 100644 index 3474a7585e..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/FileHeaderUtils.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework.SourceGeneration.Helpers; - -internal static class FileHeaderUtils -{ - public static void AppendAutoGeneratedHeader(this IndentedStringBuilder stringBuilder) - { - stringBuilder.AppendLine("//------------------------------------------------------------------------------"); - stringBuilder.AppendLine("// "); - stringBuilder.AppendLine("// This code was generated by Microsoft Testing Framework Generator."); - stringBuilder.AppendLine("// "); - stringBuilder.AppendLine("//------------------------------------------------------------------------------"); - } -} diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/HashCode.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/HashCode.cs deleted file mode 100644 index ec41552ded..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/HashCode.cs +++ /dev/null @@ -1,488 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using System.ComponentModel; -using System.Security.Cryptography; - -namespace System; - -/// -/// A polyfill type that mirrors some methods from on .7. -/// -internal struct HashCode -{ - private const uint Prime1 = 2654435761U; - private const uint Prime2 = 2246822519U; - private const uint Prime3 = 3266489917U; - private const uint Prime4 = 668265263U; - private const uint Prime5 = 374761393U; - - private static readonly uint Seed = GenerateGlobalSeed(); - - private uint _v1; - private uint _v2; - private uint _v3; - private uint _v4; - private uint _queue1; - private uint _queue2; - private uint _queue3; - private uint _length; - - /// - /// Initializes the default seed. - /// - /// A random seed. - private static unsafe uint GenerateGlobalSeed() - { - byte[] bytes = new byte[4]; - - RandomNumberGenerator.Create().GetBytes(bytes); - - return BitConverter.ToUInt32(bytes, 0); - } - - /// - /// Combines a value into a hash code. - /// - /// The type of the value to combine into the hash code. - /// The value to combine into the hash code. - /// The hash code that represents the value. - public static int Combine(T1 value) - { - uint hc1 = (uint)(value?.GetHashCode() ?? 0); - uint hash = MixEmptyState(); - - hash += 4; - hash = QueueRound(hash, hc1); - hash = MixFinal(hash); - - return (int)hash; - } - - /// - /// Combines two values into a hash code. - /// - /// The type of the first value to combine into the hash code. - /// The type of the second value to combine into the hash code. - /// The first value to combine into the hash code. - /// The second value to combine into the hash code. - /// The hash code that represents the values. - public static int Combine(T1 value1, T2 value2) - { - uint hc1 = (uint)(value1?.GetHashCode() ?? 0); - uint hc2 = (uint)(value2?.GetHashCode() ?? 0); - uint hash = MixEmptyState(); - - hash += 8; - hash = QueueRound(hash, hc1); - hash = QueueRound(hash, hc2); - hash = MixFinal(hash); - - return (int)hash; - } - - /// - /// Combines three values into a hash code. - /// - /// The type of the first value to combine into the hash code. - /// The type of the second value to combine into the hash code. - /// The type of the third value to combine into the hash code. - /// The first value to combine into the hash code. - /// The second value to combine into the hash code. - /// The third value to combine into the hash code. - /// The hash code that represents the values. - public static int Combine(T1 value1, T2 value2, T3 value3) - { - uint hc1 = (uint)(value1?.GetHashCode() ?? 0); - uint hc2 = (uint)(value2?.GetHashCode() ?? 0); - uint hc3 = (uint)(value3?.GetHashCode() ?? 0); - uint hash = MixEmptyState(); - - hash += 12; - hash = QueueRound(hash, hc1); - hash = QueueRound(hash, hc2); - hash = QueueRound(hash, hc3); - hash = MixFinal(hash); - - return (int)hash; - } - - /// - /// Combines four values into a hash code. - /// - /// The type of the first value to combine into the hash code. - /// The type of the second value to combine into the hash code. - /// The type of the third value to combine into the hash code. - /// The type of the fourth value to combine into the hash code. - /// The first value to combine into the hash code. - /// The second value to combine into the hash code. - /// The third value to combine into the hash code. - /// The fourth value to combine into the hash code. - /// The hash code that represents the values. - public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4) - { - uint hc1 = (uint)(value1?.GetHashCode() ?? 0); - uint hc2 = (uint)(value2?.GetHashCode() ?? 0); - uint hc3 = (uint)(value3?.GetHashCode() ?? 0); - uint hc4 = (uint)(value4?.GetHashCode() ?? 0); - - Initialize(out uint v1, out uint v2, out uint v3, out uint v4); - - v1 = Round(v1, hc1); - v2 = Round(v2, hc2); - v3 = Round(v3, hc3); - v4 = Round(v4, hc4); - - uint hash = MixState(v1, v2, v3, v4); - - hash += 16; - hash = MixFinal(hash); - - return (int)hash; - } - - /// - /// Combines five values into a hash code. - /// - /// The type of the first value to combine into the hash code. - /// The type of the second value to combine into the hash code. - /// The type of the third value to combine into the hash code. - /// The type of the fourth value to combine into the hash code. - /// The type of the fifth value to combine into the hash code. - /// The first value to combine into the hash code. - /// The second value to combine into the hash code. - /// The third value to combine into the hash code. - /// The fourth value to combine into the hash code. - /// The fifth value to combine into the hash code. - /// The hash code that represents the values. - public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5) - { - uint hc1 = (uint)(value1?.GetHashCode() ?? 0); - uint hc2 = (uint)(value2?.GetHashCode() ?? 0); - uint hc3 = (uint)(value3?.GetHashCode() ?? 0); - uint hc4 = (uint)(value4?.GetHashCode() ?? 0); - uint hc5 = (uint)(value5?.GetHashCode() ?? 0); - - Initialize(out uint v1, out uint v2, out uint v3, out uint v4); - - v1 = Round(v1, hc1); - v2 = Round(v2, hc2); - v3 = Round(v3, hc3); - v4 = Round(v4, hc4); - - uint hash = MixState(v1, v2, v3, v4); - - hash += 20; - hash = QueueRound(hash, hc5); - hash = MixFinal(hash); - - return (int)hash; - } - - /// - /// Combines six values into a hash code. - /// - /// The type of the first value to combine into the hash code. - /// The type of the second value to combine into the hash code. - /// The type of the third value to combine into the hash code. - /// The type of the fourth value to combine into the hash code. - /// The type of the fifth value to combine into the hash code. - /// The type of the sixth value to combine into the hash code. - /// The first value to combine into the hash code. - /// The second value to combine into the hash code. - /// The third value to combine into the hash code. - /// The fourth value to combine into the hash code. - /// The fifth value to combine into the hash code. - /// The sixth value to combine into the hash code. - /// The hash code that represents the values. - public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6) - { - uint hc1 = (uint)(value1?.GetHashCode() ?? 0); - uint hc2 = (uint)(value2?.GetHashCode() ?? 0); - uint hc3 = (uint)(value3?.GetHashCode() ?? 0); - uint hc4 = (uint)(value4?.GetHashCode() ?? 0); - uint hc5 = (uint)(value5?.GetHashCode() ?? 0); - uint hc6 = (uint)(value6?.GetHashCode() ?? 0); - - Initialize(out uint v1, out uint v2, out uint v3, out uint v4); - - v1 = Round(v1, hc1); - v2 = Round(v2, hc2); - v3 = Round(v3, hc3); - v4 = Round(v4, hc4); - - uint hash = MixState(v1, v2, v3, v4); - - hash += 24; - hash = QueueRound(hash, hc5); - hash = QueueRound(hash, hc6); - hash = MixFinal(hash); - - return (int)hash; - } - - /// - /// Combines seven values into a hash code. - /// - /// The type of the first value to combine into the hash code. - /// The type of the second value to combine into the hash code. - /// The type of the third value to combine into the hash code. - /// The type of the fourth value to combine into the hash code. - /// The type of the fifth value to combine into the hash code. - /// The type of the sixth value to combine into the hash code. - /// The type of the seventh value to combine into the hash code. - /// The first value to combine into the hash code. - /// The second value to combine into the hash code. - /// The third value to combine into the hash code. - /// The fourth value to combine into the hash code. - /// The fifth value to combine into the hash code. - /// The sixth value to combine into the hash code. - /// The seventh value to combine into the hash code. - /// The hash code that represents the values. - public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7) - { - uint hc1 = (uint)(value1?.GetHashCode() ?? 0); - uint hc2 = (uint)(value2?.GetHashCode() ?? 0); - uint hc3 = (uint)(value3?.GetHashCode() ?? 0); - uint hc4 = (uint)(value4?.GetHashCode() ?? 0); - uint hc5 = (uint)(value5?.GetHashCode() ?? 0); - uint hc6 = (uint)(value6?.GetHashCode() ?? 0); - uint hc7 = (uint)(value7?.GetHashCode() ?? 0); - - Initialize(out uint v1, out uint v2, out uint v3, out uint v4); - - v1 = Round(v1, hc1); - v2 = Round(v2, hc2); - v3 = Round(v3, hc3); - v4 = Round(v4, hc4); - - uint hash = MixState(v1, v2, v3, v4); - - hash += 28; - hash = QueueRound(hash, hc5); - hash = QueueRound(hash, hc6); - hash = QueueRound(hash, hc7); - hash = MixFinal(hash); - - return (int)hash; - } - - /// - /// Combines eight values into a hash code. - /// - /// The type of the first value to combine into the hash code. - /// The type of the second value to combine into the hash code. - /// The type of the third value to combine into the hash code. - /// The type of the fourth value to combine into the hash code. - /// The type of the fifth value to combine into the hash code. - /// The type of the sixth value to combine into the hash code. - /// The type of the seventh value to combine into the hash code. - /// The type of the eighth value to combine into the hash code. - /// The first value to combine into the hash code. - /// The second value to combine into the hash code. - /// The third value to combine into the hash code. - /// The fourth value to combine into the hash code. - /// The fifth value to combine into the hash code. - /// The sixth value to combine into the hash code. - /// The seventh value to combine into the hash code. - /// The eighth value to combine into the hash code. - /// The hash code that represents the values. - public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7, T8 value8) - { - uint hc1 = (uint)(value1?.GetHashCode() ?? 0); - uint hc2 = (uint)(value2?.GetHashCode() ?? 0); - uint hc3 = (uint)(value3?.GetHashCode() ?? 0); - uint hc4 = (uint)(value4?.GetHashCode() ?? 0); - uint hc5 = (uint)(value5?.GetHashCode() ?? 0); - uint hc6 = (uint)(value6?.GetHashCode() ?? 0); - uint hc7 = (uint)(value7?.GetHashCode() ?? 0); - uint hc8 = (uint)(value8?.GetHashCode() ?? 0); - - Initialize(out uint v1, out uint v2, out uint v3, out uint v4); - - v1 = Round(v1, hc1); - v2 = Round(v2, hc2); - v3 = Round(v3, hc3); - v4 = Round(v4, hc4); - - v1 = Round(v1, hc5); - v2 = Round(v2, hc6); - v3 = Round(v3, hc7); - v4 = Round(v4, hc8); - - uint hash = MixState(v1, v2, v3, v4); - - hash += 32; - hash = MixFinal(hash); - - return (int)hash; - } - - /// - /// Adds a single value to the current hash. - /// - /// The type of the value to add into the hash code. - /// The value to add into the hash code. - public void Add(T value) - => Add(value?.GetHashCode() ?? 0); - - /// - /// Adds a single value to the current hash. - /// - /// The type of the value to add into the hash code. - /// The value to add into the hash code. - /// The instance to use. - public void Add(T value, IEqualityComparer? comparer) - => Add(value is null ? 0 : (comparer?.GetHashCode(value) ?? value.GetHashCode())); - - /// - /// Adds a span of bytes to the hash code. - /// - /// The span. - public void AddBytes(ReadOnlySpan value) - { - ref byte pos = ref MemoryMarshal.GetReference(value); - ref byte end = ref Unsafe.Add(ref pos, value.Length); - - while ((nint)Unsafe.ByteOffset(ref pos, ref end) >= sizeof(int)) - { - Add(Unsafe.ReadUnaligned(ref pos)); - pos = ref Unsafe.Add(ref pos, sizeof(int)); - } - - while (Unsafe.IsAddressLessThan(ref pos, ref end)) - { - Add((int)pos); - pos = ref Unsafe.Add(ref pos, 1); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Initialize(out uint v1, out uint v2, out uint v3, out uint v4) - { - v1 = Seed + Prime1 + Prime2; - v2 = Seed + Prime2; - v3 = Seed; - v4 = Seed - Prime1; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint Round(uint hash, uint input) - => RotateLeft(hash + (input * Prime2), 13) * Prime1; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint QueueRound(uint hash, uint queuedValue) - => RotateLeft(hash + (queuedValue * Prime3), 17) * Prime4; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint MixState(uint v1, uint v2, uint v3, uint v4) - => RotateLeft(v1, 1) + RotateLeft(v2, 7) + RotateLeft(v3, 12) + RotateLeft(v4, 18); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint MixEmptyState() - => Seed + Prime5; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint MixFinal(uint hash) - { - hash ^= hash >> 15; - hash *= Prime2; - hash ^= hash >> 13; - hash *= Prime3; - hash ^= hash >> 16; - - return hash; - } - - private void Add(int value) - { - uint val = (uint)value; - uint previousLength = _length++; - uint position = previousLength % 4; - - if (position == 0) - { - _queue1 = val; - } - else if (position == 1) - { - _queue2 = val; - } - else if (position == 2) - { - _queue3 = val; - } - else - { - if (previousLength == 3) - { - Initialize(out _v1, out _v2, out _v3, out _v4); - } - - _v1 = Round(_v1, _queue1); - _v2 = Round(_v2, _queue2); - _v3 = Round(_v3, _queue3); - _v4 = Round(_v4, val); - } - } - - /// - /// Gets the resulting hashcode from the current instance. - /// - /// The resulting hashcode from the current instance. - public readonly int ToHashCode() - { - uint length = _length; - uint position = length % 4; - uint hash = length < 4 ? MixEmptyState() : MixState(_v1, _v2, _v3, _v4); - - hash += length * 4; - - if (position > 0) - { - hash = QueueRound(hash, _queue1); - - if (position > 1) - { - hash = QueueRound(hash, _queue2); - - if (position > 2) - { - hash = QueueRound(hash, _queue3); - } - } - } - - hash = MixFinal(hash); - - return (int)hash; - } - - /// - [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes. Use ToHashCode to retrieve the computed hash code.", error: true)] - [EditorBrowsable(EditorBrowsableState.Never)] -#pragma warning disable CS0809 // Obsolete member overrides non-obsolete member - public override readonly int GetHashCode() -#pragma warning restore CS0809 // Obsolete member overrides non-obsolete member - => throw new NotSupportedException(); - - /// - [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes.", error: true)] - [EditorBrowsable(EditorBrowsableState.Never)] -#pragma warning disable CS0809 // Obsolete member overrides non-obsolete member - public override readonly bool Equals(object? obj) -#pragma warning restore CS0809 // Obsolete member overrides non-obsolete member - => throw new NotSupportedException(); - - /// - /// Rotates the specified value left by the specified number of bits. - /// Similar in behavior to the x86 instruction ROL. - /// - /// The value to rotate. - /// The number of bits to rotate by. - /// Any value outside the range [0..31] is treated as congruent mod 32. - /// The rotated value. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint RotateLeft(uint value, int offset) - => (value << offset) | (value >> (32 - offset)); -} diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/IMethodSymbolExtensions.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/IMethodSymbolExtensions.cs deleted file mode 100644 index 344b0a3da7..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/IMethodSymbolExtensions.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.CodeAnalysis; - -namespace Microsoft.Testing.Framework.SourceGeneration; - -internal static class IMethodSymbolExtensions -{/// - /// Checks if the given method implements or overrides an implementation of . - /// - public static bool IsDisposeImplementation(this IMethodSymbol? method, INamedTypeSymbol? iDisposable) - { - if (method is null) - { - return false; - } - - if (method.IsOverride) - { - return method.OverriddenMethod.IsDisposeImplementation(iDisposable); - } - - // Identify the implementor of IDisposable.Dispose in the given method's containing type and check - // if it is the given method. - return method.ReturnsVoid - && method.Parameters.IsEmpty - && method.IsImplementationOfInterfaceMethod(null, iDisposable, "Dispose"); - } - - /// - /// Checks if the given method implements "IAsyncDisposable.Dispose" or overrides an implementation of "IAsyncDisposable.Dispose". - /// - public static bool IsAsyncDisposeImplementation(this IMethodSymbol? method, INamedTypeSymbol? iAsyncDisposable, INamedTypeSymbol? valueTaskType) - { - if (method is null) - { - return false; - } - - if (method.IsOverride) - { - return method.OverriddenMethod.IsAsyncDisposeImplementation(iAsyncDisposable, valueTaskType); - } - - // Identify the implementor of IAsyncDisposable.Dispose in the given method's containing type and check - // if it is the given method. - return SymbolEqualityComparer.Default.Equals(method.ReturnType, valueTaskType) - && method.Parameters.IsEmpty - && method.IsImplementationOfInterfaceMethod(null, iAsyncDisposable, "DisposeAsync"); - } - - /// - /// Checks if the given method is an implementation of the given interface method - /// Substituted with the given typeargument. - /// - public static bool IsImplementationOfInterfaceMethod(this IMethodSymbol method, ITypeSymbol? typeArgument, INamedTypeSymbol? interfaceType, string interfaceMethodName) - { - INamedTypeSymbol? constructedInterface = typeArgument != null ? interfaceType?.Construct(typeArgument) : interfaceType; - - return constructedInterface?.GetMembers(interfaceMethodName).FirstOrDefault() is IMethodSymbol interfaceMethod - && SymbolEqualityComparer.Default.Equals(method, method.ContainingType.FindImplementationForInterfaceMember(interfaceMethod)); - } -} diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/ISymbolExtensions.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/ISymbolExtensions.cs deleted file mode 100644 index a8a4823ca9..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/ISymbolExtensions.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.CodeAnalysis; - -namespace Analyzers.Utilities; - -internal static class ISymbolExtensions -{ - public static SymbolVisibility GetResultantVisibility(this ISymbol symbol) - { - // Start by assuming it's visible. - SymbolVisibility visibility = SymbolVisibility.Public; - - switch (symbol.Kind) - { - case SymbolKind.Alias: - // Aliases are uber private. They're only visible in the same file that they - // were declared in. - return SymbolVisibility.Private; - - case SymbolKind.Parameter: - // Parameters are only as visible as their containing symbol - return symbol.ContainingSymbol.GetResultantVisibility(); - - case SymbolKind.TypeParameter: - // Type Parameters are private. - return SymbolVisibility.Private; - } - - while (symbol != null && symbol.Kind != SymbolKind.Namespace) - { - switch (symbol.DeclaredAccessibility) - { - // If we see anything private, then the symbol is private. - case Accessibility.NotApplicable: - case Accessibility.Private: - return SymbolVisibility.Private; - - // If we see anything internal, then knock it down from public to - // internal. - case Accessibility.Internal: - case Accessibility.ProtectedAndInternal: - visibility = SymbolVisibility.Internal; - break; - - // For anything else (Public, Protected, ProtectedOrInternal), the - // symbol stays at the level we've gotten so far. - } - - symbol = symbol.ContainingSymbol; - } - - return visibility; - } - - public static ITypeSymbol? GetMemberType(this ISymbol? symbol) - => symbol switch - { - IEventSymbol eventSymbol => eventSymbol.Type, - IFieldSymbol fieldSymbol => fieldSymbol.Type, - IMethodSymbol methodSymbol => methodSymbol.ReturnType, - IPropertySymbol propertySymbol => propertySymbol.Type, - _ => null, - }; -} diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/ITypeSymbolExtensions.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/ITypeSymbolExtensions.cs deleted file mode 100644 index 0af0a3975a..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/ITypeSymbolExtensions.cs +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; - -namespace Analyzers.Utilities; - -internal static class ITypeSymbolExtensions -{ - /// - /// Returns the members of the given type, including inherited members. - /// - public static IEnumerable> GetAllMembers(this INamedTypeSymbol symbol) - { - INamedTypeSymbol? currentSymbol = symbol; - - while (currentSymbol != null) - { - yield return currentSymbol.GetMembers(); - currentSymbol = currentSymbol.BaseType; - } - } - - public static IEnumerable> GetAllMembers(this INamedTypeSymbol symbol, string name) - { - INamedTypeSymbol? currentSymbol = symbol; - - while (currentSymbol != null) - { - yield return currentSymbol.GetMembers(name); - currentSymbol = currentSymbol.BaseType; - } - } - - public static bool Inherits( - [NotNullWhen(returnValue: true)] this ITypeSymbol? type, - [NotNullWhen(returnValue: true)] ITypeSymbol? possibleBase) - { - if (type == null || possibleBase == null) - { - return false; - } - - switch (possibleBase.TypeKind) - { - case TypeKind.Class: - if (type.TypeKind == TypeKind.Interface) - { - return false; - } - - return DerivesFrom(type, possibleBase, baseTypesOnly: true); - - case TypeKind.Interface: - return DerivesFrom(type, possibleBase); - - default: - return false; - } - } - - public static bool DerivesFrom( - [NotNullWhen(returnValue: true)] this ITypeSymbol? symbol, - [NotNullWhen(returnValue: true)] ITypeSymbol? candidateBaseType, - bool baseTypesOnly = false, - bool checkTypeParameterConstraints = true) - { - if (candidateBaseType == null || symbol == null) - { - return false; - } - - if (!baseTypesOnly && candidateBaseType.TypeKind == TypeKind.Interface) - { - IEnumerable allInterfaces = symbol.AllInterfaces.OfType(); - if (SymbolEqualityComparer.Default.Equals(candidateBaseType.OriginalDefinition, candidateBaseType)) - { - // Candidate base type is not a constructed generic type, so use original definition for interfaces. - allInterfaces = allInterfaces.Select(i => i.OriginalDefinition); - } - - if (allInterfaces.Contains(candidateBaseType, SymbolEqualityComparer.Default)) - { - return true; - } - } - - if (checkTypeParameterConstraints && symbol.TypeKind == TypeKind.TypeParameter) - { - var typeParameterSymbol = (ITypeParameterSymbol)symbol; - foreach (ITypeSymbol constraintType in typeParameterSymbol.ConstraintTypes) - { - if (constraintType.DerivesFrom(candidateBaseType, baseTypesOnly, checkTypeParameterConstraints)) - { - return true; - } - } - } - - while (symbol != null) - { - if (SymbolEqualityComparer.Default.Equals(symbol, candidateBaseType)) - { - return true; - } - - symbol = symbol.BaseType; - } - - return false; - } -} diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/IncrementalValuesProviderExtensions.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/IncrementalValuesProviderExtensions.cs deleted file mode 100644 index bef8c4073b..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/IncrementalValuesProviderExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.CodeAnalysis; - -namespace Microsoft.Testing.Framework.SourceGeneration.Helpers; - -internal static class IncrementalValuesProviderExtensions -{ - private static readonly Func NotNullTest = x => x != null; - - public static IncrementalValuesProvider WhereNotNull(this IncrementalValuesProvider source) - where T : class - => source.Where(NotNullTest)!; -} diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/IndentedStringBuilder.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/IndentedStringBuilder.cs index a9b2c407f4..6b7937ade5 100644 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/IndentedStringBuilder.cs +++ b/src/Analyzers/MSTest.SourceGeneration/Helpers/IndentedStringBuilder.cs @@ -1,8 +1,13 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.Testing.Framework.SourceGeneration.Helpers; +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Helpers; +/// +/// Small helper that produces indented source text. Mirrors the helper used by the existing +/// MSTest.SourceGeneration project so that the generated output is consistent with the rest of +/// the MSTest source generators. +/// internal sealed class IndentedStringBuilder { private readonly StringBuilder _builder = new(); @@ -12,13 +17,6 @@ internal sealed class IndentedStringBuilder public int IndentationLevel { get; internal set; } - public void Append(char value) - { - MaybeAppendIndent(); - _builder.Append(value); - _needsIndent = false; - } - public void Append(string value) { MaybeAppendIndent(); @@ -32,53 +30,54 @@ public void AppendLine() _needsIndent = true; } - public void AppendLine(char value) - { - MaybeAppendIndent().Append(value).Append(Constants.NewLine); - _needsIndent = true; - } - public void AppendLine(string value) { - MaybeAppendIndent().Append(value).Append(Constants.NewLine); + MaybeAppendIndent(); + _builder.Append(value); + _builder.Append(Constants.NewLine); _needsIndent = true; } - public void AppendUnindentedLine(string value) - => _builder.Append(value).Append(Constants.NewLine); - - public IDisposable AppendBlock(string? value = null, char? closingBraceSuffixChar = null) + public IDisposable AppendBlock(string header) { - if (value is not null) - { - AppendLine(value); - } - - AppendLine('{'); + AppendLine(header); + AppendLine("{"); IndentationLevel++; return new DisposableAction(() => { IndentationLevel--; - Append('}'); - if (closingBraceSuffixChar is not null) - { - Append(closingBraceSuffixChar.Value); - } - - AppendLine(); + AppendLine("}"); }); } public override string ToString() => _builder.ToString(); - private StringBuilder MaybeAppendIndent() + private void MaybeAppendIndent() { if (_needsIndent) { _builder.Append(' ', IndentationLevel * 4); + _needsIndent = false; } + } + + private sealed class DisposableAction : IDisposable + { + private readonly Action _action; + private bool _disposed; + + public DisposableAction(Action action) => _action = action; - return _builder; + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + _action(); + } } } diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/SymbolVisibility.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/SymbolVisibility.cs deleted file mode 100644 index 026a0adb48..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/SymbolVisibility.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -// Copied from https://github.com/dotnet/sdk/blob/main/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler/Extensions/SymbolVisibility.cs -// (previously sourced from dotnet/roslyn-analyzers before that repo was archived). -namespace Analyzers.Utilities; - -#pragma warning disable CA1027 // Mark enums with FlagsAttribute -internal enum SymbolVisibility -#pragma warning restore CA1027 // Mark enums with FlagsAttribute -{ - Public = 0, - Internal = 1, - Private = 2, - Friend = Internal, -} diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/SystemPolyfills.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/SystemPolyfills.cs deleted file mode 100644 index 6a80a1ca13..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/SystemPolyfills.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -#if !NETCOREAPP -#pragma warning disable SA1403 // File may only contain a single namespace -#pragma warning disable SA1642 // Constructor summary documentation should begin with standard text -#pragma warning disable SA1623 // Property summary documentation should match accessors - -using System.ComponentModel; - -namespace System.Runtime.CompilerServices -{ - [EditorBrowsable(EditorBrowsableState.Never)] - internal static class IsExternalInit; -} - -// This was copied from https://github.com/dotnet/coreclr/blob/60f1e6265bd1039f023a82e0643b524d6aaf7845/src/System.Private.CoreLib/shared/System/Diagnostics/CodeAnalysis/NullableAttributes.cs -// and updated to have the scope of the attributes be internal. -namespace System.Diagnostics.CodeAnalysis -{ - /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. - [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] - internal sealed class NotNullWhenAttribute : Attribute - { - /// Initializes the attribute with the specified return value condition. - /// - /// The return value condition. If the method returns this value, the associated parameter will not be null. - /// - public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; - - /// Gets the return value condition. - public bool ReturnValue { get; } - } -} - -#endif diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/TestMethods.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/TestMethods.cs deleted file mode 100644 index 9b605bbc0c..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/TestMethods.cs +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.CodeAnalysis; - -namespace Microsoft.Testing.Framework.SourceGeneration; - -internal static class TestMethods -{ - public static SymbolDisplayFormat MethodIdentifierFullyQualifiedTypeFormat { get; } = - SymbolDisplayFormat.CSharpErrorMessageFormat.WithMiscellaneousOptions( - SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | - SymbolDisplayMiscellaneousOptions.UseAsterisksInMultiDimensionalArrays | - SymbolDisplayMiscellaneousOptions.UseErrorTypeSymbolName | - SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier); - - public static bool IsValidTestMethodShape(this IMethodSymbol methodSymbol, WellKnownTypes wellKnownTypes) - { - // We only look for public methods - if (methodSymbol.DeclaredAccessibility != Accessibility.Public) - { - return false; - } - - // We don't support generic test methods - if (!methodSymbol.TypeParameters.IsEmpty) - { - return false; - } - - // We don't support static test methods - if (methodSymbol.IsStatic) - { - return false; - } - - // We accept only simple methods - if (methodSymbol.MethodKind != MethodKind.Ordinary - || methodSymbol.IsAbstract - || methodSymbol.IsExtern - || methodSymbol.IsVirtual - || methodSymbol.IsOverride - || methodSymbol.IsImplicitlyDeclared - || methodSymbol.IsPartialDefinition) - { - return false; - } - - // We don't support async void - if (methodSymbol.ReturnsVoid && methodSymbol.IsAsync) - { - return false; - } - - // We support only void and Task return methods - if (!methodSymbol.ReturnsVoid - && !SymbolEqualityComparer.Default.Equals(methodSymbol.ReturnType, wellKnownTypes.TaskSymbol) - && !SymbolEqualityComparer.Default.Equals(methodSymbol.ReturnType, wellKnownTypes.ValueTaskSymbol)) - { - return false; - } - - // Method has correct shape to be a test method - return true; - } - - /// - /// Method has a test method shape but is known to not be a test method. - /// - public static bool IsKnownNonTestMethod(this IMethodSymbol methodSymbol, WellKnownTypes wellKnownTypes) - => methodSymbol.IsDisposeImplementation(wellKnownTypes.IDisposableSymbol) - || methodSymbol.IsAsyncDisposeImplementation(wellKnownTypes.IAsyncDisposableSymbol, wellKnownTypes.ValueTaskSymbol) - || methodSymbol.GetAttributes().Any(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, wellKnownTypes.IgnoreAttributeSymbol)); -} diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/TestNodeHelpers.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/TestNodeHelpers.cs deleted file mode 100644 index f0d69f2b5e..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/TestNodeHelpers.cs +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework.SourceGeneration.Helpers; - -internal static class TestNodeHelpers -{ - public static string GenerateEscapedName(string name) - => name.Replace('.', '_'); - - public static DisposableAction AppendTestNode(this IndentedStringBuilder nodeStringBuilder, string stableUid, string displayName, - ICollection properties, char testNodeBlockSuffixChar = ',') - { - IDisposable testNodeBlock = AppendTestNodeCommonPart(nodeStringBuilder, stableUid, displayName, properties, testNodeBlockSuffixChar); - IDisposable testsBlock = nodeStringBuilder.AppendBlock("Tests = new MSTF::TestNode[]", closingBraceSuffixChar: ','); - - return new DisposableAction(() => - { - testsBlock.Dispose(); - testNodeBlock.Dispose(); - }); - } - - public static DisposableAction AppendTestNode(this IndentedStringBuilder nodeStringBuilder, string stableUid, string displayName, - ICollection properties, string testsVariableName, char testNodeBlockSuffixChar = ',') - { - IDisposable testNodeBlock = AppendTestNodeCommonPart(nodeStringBuilder, stableUid, displayName, properties, testNodeBlockSuffixChar); - nodeStringBuilder.AppendLine($"Tests = {testsVariableName}.ToArray(),"); - - return new DisposableAction(testNodeBlock.Dispose); - } - - private static IDisposable AppendTestNodeCommonPart(IndentedStringBuilder nodeStringBuilder, string stableUid, string displayName, - ICollection properties, char testNodeBlockSuffixChar = ',') - { - IDisposable testNodeBlock = nodeStringBuilder.AppendBlock("new MSTF::TestNode", testNodeBlockSuffixChar); - nodeStringBuilder.AppendLine($"StableUid = \"{stableUid}\","); - nodeStringBuilder.AppendLine($"DisplayName = \"{displayName}\","); - - if (properties.Count > 0) - { - using (nodeStringBuilder.AppendBlock($"Properties = new Msg::IProperty[{properties.Count}]", closingBraceSuffixChar: ',')) - { - foreach (string property in properties) - { - nodeStringBuilder.AppendLine(property); - } - } - } - else - { - nodeStringBuilder.AppendLine("Properties = Sys::Array.Empty(),"); - } - - return testNodeBlock; - } -} diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/UnicodeCharacterUtilities.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/UnicodeCharacterUtilities.cs deleted file mode 100644 index 0f5dc97fa8..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/UnicodeCharacterUtilities.cs +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework.SourceGeneration.Helpers; - -/// -/// Defines a set of helper methods to classify Unicode characters. -/// -internal static class UnicodeCharacterUtilities -{ - public static bool IsIdentifierStartCharacter(char ch) - { - // identifier-start-character: - // letter-character - // _ (the underscore character U+005F) - if (ch < 'a') // '\u0061' - { - if (ch < 'A') // '\u0041' - { - return false; - } - - return ch is <= 'Z' // '\u005A' - or '_'; // '\u005F' - } - - if (ch <= 'z') // '\u007A' - { - return true; - } - - if (ch <= '\u007F') // max ASCII - { - return false; - } - - // Check if letter-character - return IsLetterChar(CharUnicodeInfo.GetUnicodeCategory(ch)); - } - - /// - /// Returns true if the Unicode character can be a part of an identifier. - /// - /// The Unicode character. - public static bool IsIdentifierPartCharacter(char ch) - { - // identifier-part-character: - // letter-character - // decimal-digit-character - // connecting-character - // combining-character - // formatting-character - if (ch < 'a') // '\u0061' - { - if (ch < 'A') // '\u0041' - { - return ch is >= '0' // '\u0030' - and <= '9'; // '\u0039' - } - - return ch is <= 'Z' // '\u005A' - or '_'; // '\u005F' - } - - if (ch <= 'z') // '\u007A' - { - return true; - } - - if (ch <= '\u007F') // max ASCII - { - return false; - } - - UnicodeCategory cat = CharUnicodeInfo.GetUnicodeCategory(ch); - return IsLetterChar(cat) - || IsDecimalDigitChar(cat) - || IsConnectingChar(cat) - || IsCombiningChar(cat) - || IsFormattingChar(cat); - } - - private static bool IsLetterChar(UnicodeCategory cat) - // letter-character: - // A Unicode character of classes Lu, Ll, Lt, Lm, Lo, or Nl - // A Unicode-escape-sequence representing a character of classes Lu, Ll, Lt, Lm, Lo, or Nl - => cat switch - { - UnicodeCategory.UppercaseLetter or UnicodeCategory.LowercaseLetter or UnicodeCategory.TitlecaseLetter or UnicodeCategory.ModifierLetter or UnicodeCategory.OtherLetter or UnicodeCategory.LetterNumber => true, - _ => false, - }; - - private static bool IsCombiningChar(UnicodeCategory cat) - // combining-character: - // A Unicode character of classes Mn or Mc - // A Unicode-escape-sequence representing a character of classes Mn or Mc - => cat switch - { - UnicodeCategory.NonSpacingMark or UnicodeCategory.SpacingCombiningMark => true, - _ => false, - }; - - private static bool IsDecimalDigitChar(UnicodeCategory cat) - // decimal-digit-character: - // A Unicode character of the class Nd - // A unicode-escape-sequence representing a character of the class Nd - => cat == UnicodeCategory.DecimalDigitNumber; - - private static bool IsConnectingChar(UnicodeCategory cat) - // connecting-character: - // A Unicode character of the class Pc - // A unicode-escape-sequence representing a character of the class Pc - => cat == UnicodeCategory.ConnectorPunctuation; - - /// - /// Returns true if the Unicode character is a formatting character (Unicode class Cf). - /// - /// The Unicode character. - private static bool IsFormattingChar(UnicodeCategory cat) - // formatting-character: - // A Unicode character of the class Cf - // A unicode-escape-sequence representing a character of the class Cf - => cat == UnicodeCategory.Format; -} diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/WellKnownTypes.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/WellKnownTypes.cs deleted file mode 100644 index 72e5f2724c..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/WellKnownTypes.cs +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.CodeAnalysis; - -namespace Microsoft.Testing.Framework.SourceGeneration; - -/* - * IMPORTANT: Keep the constants, properties and constructor properties assignments in alphabetical order. - */ -internal sealed class WellKnownTypes -{ - private const string MicrosoftVisualStudioTestToolsUnitTestingDataRowAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.DataRowAttribute"; - private const string MicrosoftVisualStudioTestToolsUnitTestingDynamicDataAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.DynamicDataAttribute"; - private const string MicrosoftVisualStudioTestToolsUnitTestingIgnoreAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.IgnoreAttribute"; - private const string MicrosoftTestingFrameworkTestArgumentsEntry1 = "Microsoft.Testing.Framework.InternalUnsafeTestArgumentsEntry`1"; - private const string MicrosoftTestingFrameworkTestExecutionTimeoutAttribute = "Microsoft.Testing.Framework.TestExecutionTimeoutAttribute"; - private const string MicrosoftTestingFrameworkTestPropertyAttribute = "Microsoft.Testing.Framework.TestPropertyAttribute"; - private const string SystemCollectionsGenericIEnumerable1 = "System.Collections.Generic.IEnumerable`1"; - private const string SystemIAsyncDisposable = "System.IAsyncDisposable"; - private const string SystemIDisposable = "System.IDisposable"; - private const string SystemObsoleteAttribute = "System.ObsoleteAttribute"; - private const string SystemThreadingTasksTask = "System.Threading.Tasks.Task"; - private const string SystemThreadingTasksValueTask = "System.Threading.Tasks.ValueTask"; - private const string SystemTimeSpan = "System.TimeSpan"; - private const string MicrosoftVisualStudioTestToolsUnitTestingTestMethodAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute"; - - public WellKnownTypes(Compilation compilation) - { - DataRowAttributeSymbol = compilation.GetTypeByMetadataName(MicrosoftVisualStudioTestToolsUnitTestingDataRowAttribute); - DynamicDataAttributeSymbol = compilation.GetTypeByMetadataName(MicrosoftVisualStudioTestToolsUnitTestingDynamicDataAttribute); - IAsyncDisposableSymbol = compilation.GetTypeByMetadataName(SystemIAsyncDisposable); - IDisposableSymbol = compilation.GetTypeByMetadataName(SystemIDisposable); - IEnumerable1Symbol = compilation.GetTypeByMetadataName(SystemCollectionsGenericIEnumerable1); - IgnoreAttributeSymbol = compilation.GetTypeByMetadataName(MicrosoftVisualStudioTestToolsUnitTestingIgnoreAttribute); - SystemObsoleteAttributeSymbol = compilation.GetTypeByMetadataName(SystemObsoleteAttribute); - TaskSymbol = compilation.GetTypeByMetadataName(SystemThreadingTasksTask); - TestArgumentsEntrySymbol = compilation.GetTypeByMetadataName(MicrosoftTestingFrameworkTestArgumentsEntry1); - TestExecutionTimeoutAttributeSymbol = compilation.GetTypeByMetadataName(MicrosoftTestingFrameworkTestExecutionTimeoutAttribute); - TestPropertyAttributeSymbol = compilation.GetTypeByMetadataName(MicrosoftTestingFrameworkTestPropertyAttribute); - TimeSpanSymbol = compilation.GetTypeByMetadataName(SystemTimeSpan); - ValueTaskSymbol = compilation.GetTypeByMetadataName(SystemThreadingTasksValueTask); - TestMethodAttributeSymbol = compilation.GetTypeByMetadataName(MicrosoftVisualStudioTestToolsUnitTestingTestMethodAttribute); - } - - public INamedTypeSymbol? DataRowAttributeSymbol { get; } - - public INamedTypeSymbol? DynamicDataAttributeSymbol { get; } - - public INamedTypeSymbol? IAsyncDisposableSymbol { get; } - - public INamedTypeSymbol? IDisposableSymbol { get; } - - public INamedTypeSymbol? IEnumerable1Symbol { get; } - - public INamedTypeSymbol? IgnoreAttributeSymbol { get; } - - public INamedTypeSymbol? SystemObsoleteAttributeSymbol { get; } - - public INamedTypeSymbol? TaskSymbol { get; } - - public INamedTypeSymbol? TestArgumentsEntrySymbol { get; } - - public INamedTypeSymbol? TestExecutionTimeoutAttributeSymbol { get; } - - public INamedTypeSymbol? TestPropertyAttributeSymbol { get; } - - public INamedTypeSymbol? TimeSpanSymbol { get; } - - public INamedTypeSymbol? ValueTaskSymbol { get; } - - public INamedTypeSymbol? TestMethodAttributeSymbol { get; } -} diff --git a/src/Analyzers/MSTest.SourceGeneration/MSTest.SourceGeneration.csproj b/src/Analyzers/MSTest.SourceGeneration/MSTest.SourceGeneration.csproj index 3eb6317ab6..9a3e096b1f 100644 --- a/src/Analyzers/MSTest.SourceGeneration/MSTest.SourceGeneration.csproj +++ b/src/Analyzers/MSTest.SourceGeneration/MSTest.SourceGeneration.csproj @@ -5,27 +5,32 @@ netstandard2.0 false - NU5128 + $(NoWarn);NU5128 true - Microsoft.Testing.Framework.SourceGeneration + Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration + true + true + + + $(DefineConstants);EXCLUDE_RANGE_INDEX_POLYFILL + License.txt - $(MSTestEngineVersionPrefix) - $(MSTestEnginePreReleaseVersionLabel) + $(MSTestSourceGenerationVersionPrefix) + $(MSTestSourceGenerationPreReleaseVersionLabel) true true - true - +This package provides the C# source generators that emit reflection metadata so that MSTest test assemblies can run trim-safe and NativeAOT-compatible.]]> @@ -33,6 +38,10 @@ This package provides the C# source generators for MSTest test framework.]]> + + + + diff --git a/src/Analyzers/MSTest.SourceGeneration/Models/TestAssemblyMetadata.cs b/src/Analyzers/MSTest.SourceGeneration/Models/TestAssemblyMetadata.cs new file mode 100644 index 0000000000..f2ddf66eb5 --- /dev/null +++ b/src/Analyzers/MSTest.SourceGeneration/Models/TestAssemblyMetadata.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Immutable; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Models; + +/// +/// Equatable snapshot of a discovered test class. Source-generator pipeline values must be +/// value-equatable so that incremental caching works; this record carries primitive data only. +/// +internal sealed record TestClassMetadata( + string FullyQualifiedName, + string DisplayName, + string? Namespace, + EquatableArray Methods); + +/// +/// Equatable snapshot of a discovered test method. +/// +internal sealed record TestMethodMetadata(string Name, EquatableArray ParameterTypes); + +/// +/// Equatable snapshot for the full test assembly, used as the input to the emitter. +/// +internal sealed record TestAssemblyMetadata( + string AssemblyName, + EquatableArray Classes); + +/// +/// Minimal value-equatable wrapper around an . Source generators +/// need value equality on collections; only has reference equality +/// out of the box, which would cause cache misses on every compilation tick. +/// +/// The element type stored in the underlying . +internal readonly record struct EquatableArray(ImmutableArray Items) + where T : IEquatable +{ + public int Count => Items.IsDefault ? 0 : Items.Length; + + public IEnumerator GetEnumerator() => ((IEnumerable)(Items.IsDefault ? ImmutableArray.Empty : Items)).GetEnumerator(); + + public bool Equals(EquatableArray other) + { + ImmutableArray a = Items.IsDefault ? ImmutableArray.Empty : Items; + ImmutableArray b = other.Items.IsDefault ? ImmutableArray.Empty : other.Items; + if (a.Length != b.Length) + { + return false; + } + + for (int i = 0; i < a.Length; i++) + { + if (!EqualityComparer.Default.Equals(a[i], b[i])) + { + return false; + } + } + + return true; + } + + public override int GetHashCode() + { + if (Items.IsDefault) + { + return 0; + } + + int hash = 17; + foreach (T item in Items) + { + hash = unchecked((hash * 31) + (item?.GetHashCode() ?? 0)); + } + + return hash; + } +} diff --git a/src/Analyzers/MSTest.SourceGeneration/ObjectModels/DataRowTestMethodArgumentsInfo.cs b/src/Analyzers/MSTest.SourceGeneration/ObjectModels/DataRowTestMethodArgumentsInfo.cs deleted file mode 100644 index 7bc9a24b18..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/ObjectModels/DataRowTestMethodArgumentsInfo.cs +++ /dev/null @@ -1,206 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.Testing.Framework.SourceGeneration.Helpers; - -namespace Microsoft.Testing.Framework.SourceGeneration.ObjectModels; - -internal sealed class DataRowTestMethodArgumentsInfo : ITestMethodArgumentsInfo -{ - private readonly ImmutableArray> _argumentsRows; - private readonly TestMethodParametersInfo _parametersInfo; - - public bool IsTestArgumentsEntryReturnType => true; - - public string? GeneratorMethodFullName { get; } - - public DataRowTestMethodArgumentsInfo(ImmutableArray> argumentsRows, TestMethodParametersInfo parametersInfo) - { - _argumentsRows = argumentsRows; - _parametersInfo = parametersInfo; - } - - public static DataRowTestMethodArgumentsInfo? TryBuild(IMethodSymbol methodSymbol, IEnumerable argumentsAttributes, - TestMethodParametersInfo parametersInfo) - { - var argumentsRows = argumentsAttributes.Select(attr => GetInlineArguments(methodSymbol, attr).ToImmutableArray()).ToImmutableArray(); - - return argumentsRows.IsEmpty - ? null - : new(argumentsRows, parametersInfo); - } - - public void AppendArguments(IndentedStringBuilder nodeBuilder) - { - using (nodeBuilder.AppendBlock($"GetArguments = static () => new {TestMethodInfo.TestArgumentsEntryTypeName}<{_parametersInfo.ParametersTuple}>[]", closingBraceSuffixChar: ',')) - { - foreach (ImmutableArray arguments in _argumentsRows) - { - string argumentsEntry = arguments.Length > 1 - ? "(" + string.Join(", ", arguments) + ")" - : arguments[0]; - string argumentsUid = GetArgumentsUid([.. _parametersInfo.Parameters.Select(x => x.Name)], arguments); - nodeBuilder.AppendLine($"new {TestMethodInfo.TestArgumentsEntryTypeName}<{_parametersInfo.ParametersTuple}>({argumentsEntry}, \"{argumentsUid}\"),"); - } - } - } - - private static string GetArgumentsUid(string[] parameterNames, IList arguments) - { - StringBuilder argumentsUidBuilder = new(); - - for (int i = 0; i < arguments.Count; i++) - { - if (i < parameterNames.Length) - { - if (i != 0) - { - argumentsUidBuilder.Append(", "); - } - - argumentsUidBuilder.Append(parameterNames[i]); - argumentsUidBuilder.Append(": "); - } - - EscapeArgument(arguments[i], argumentsUidBuilder); - } - - return argumentsUidBuilder.ToString(); - } - - internal /* for testing purposes */ static void EscapeArgument(string argument, StringBuilder argumentsUidBuilder) - { - int escapeCharCount = 0; - for (int i = 0; i < argument.Length; i++) - { - char currentChar = argument[i]; - - if (currentChar == '\\') - { - escapeCharCount++; - } - else if (currentChar == '"' && escapeCharCount % 2 == 0) - { - argumentsUidBuilder.Append('\\'); - escapeCharCount = 0; - } - else - { - escapeCharCount = 0; - } - - argumentsUidBuilder.Append(argument[i]); - } - } - - private static IEnumerable GetInlineArguments(IMethodSymbol methodSymbol, AttributeData attr) - { - TypedConstant argumentsAttributeArguments = attr.ConstructorArguments[0]; - if (argumentsAttributeArguments.IsNull) - { - yield return "null"; - yield break; - } - - StringBuilder argumentsBuilder = new(); - Stack<(TypedConstant Argument, bool HasNextArg, bool IsInArray, int ClosingCurlyBraceCount)> argumentStack = new(); - - bool hasManyArgsButExpectsSingleArray = - methodSymbol.Parameters.Length == 1 - && methodSymbol.Parameters[0].Type.TypeKind == TypeKind.Array - && argumentsAttributeArguments.Values.Length > 1; - - if (hasManyArgsButExpectsSingleArray) - { - argumentStack.Push((argumentsAttributeArguments, HasNextArg: false, IsInArray: false, ClosingCurlyBraceCount: 0)); - } - else - { - // We are using the ctor with params object[]; - if (argumentsAttributeArguments.Kind == TypedConstantKind.Array) - { - for (int i = argumentsAttributeArguments.Values.Length - 1; i >= 0; i--) - { - argumentStack.Push((argumentsAttributeArguments.Values[i], - HasNextArg: i < argumentsAttributeArguments.Values.Length - 1, - IsInArray: false, - ClosingCurlyBraceCount: 0)); - } - } - else - { - argumentStack.Push((argumentsAttributeArguments, - HasNextArg: false, - IsInArray: false, - ClosingCurlyBraceCount: 0)); - } - } - - while (argumentStack.Count > 0) - { - (TypedConstant argument, bool hasNextArg, bool isInArray, int closingCurlyBraceCount) = argumentStack.Pop(); - - if (argument.Kind == TypedConstantKind.Array) - { - argumentsBuilder.Append("new "); - if (argument.Type is not null) - { - argumentsBuilder.Append(argument.Type.ToDisplayString()); - } - else - { - argumentsBuilder.Append("[]"); - } - - argumentsBuilder.Append(" { "); - - for (int i = argument.Values.Length - 1; i >= 0; i--) - { - argumentStack.Push((argument.Values[i], - HasNextArg: i < argument.Values.Length - 1 || hasNextArg, - IsInArray: true, - ClosingCurlyBraceCount: i == argument.Values.Length - 1 ? closingCurlyBraceCount + 1 : 0)); - } - } - else - { - if (argument.Kind == TypedConstantKind.Enum) - { - // We could cast the argument to TypedConstant and get the full name of the type - // with the global:: prefix from e.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - // but then we don't have an easy way to get the value in CSharp format without the type. - // So we just prepend. - argumentsBuilder.Append("global::" + argument.ToCSharpString()); - } - else - { - argumentsBuilder.Append(argument.ToCSharpString()); - } - - for (int i = 0; i < closingCurlyBraceCount; i++) - { - argumentsBuilder.Append(" }"); - } - - if (hasNextArg) - { - if (isInArray && closingCurlyBraceCount == 0) - { - argumentsBuilder.Append(", "); - } - else - { - yield return argumentsBuilder.ToString(); - argumentsBuilder.Clear(); - } - } - } - } - - yield return argumentsBuilder.ToString(); - } -} diff --git a/src/Analyzers/MSTest.SourceGeneration/ObjectModels/DynamicDataTestMethodArgumentsInfo.cs b/src/Analyzers/MSTest.SourceGeneration/ObjectModels/DynamicDataTestMethodArgumentsInfo.cs deleted file mode 100644 index 0d08f672a8..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/ObjectModels/DynamicDataTestMethodArgumentsInfo.cs +++ /dev/null @@ -1,230 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Analyzers.Utilities; - -using Microsoft.CodeAnalysis; -using Microsoft.Testing.Framework.SourceGeneration.Helpers; - -namespace Microsoft.Testing.Framework.SourceGeneration.ObjectModels; - -/// -/// Clone of ArgumentsProvider, that is implementing very similar functionality for DynamicData, because there we don't know what types the user is using, so we do basically the same -/// but we cast their data to the assumed types. -/// -internal sealed class DynamicDataTestMethodArgumentsInfo : ITestMethodArgumentsInfo -{ - // Based on DynamicDataSourceType in: - // https://github.com/microsoft/testfx/blob/8cf945ba740034e37e0a16efddad85f6b0fb67bc/src/TestFramework/TestFramework/Attributes/DataSource/DynamicDataAttribute.cs#L17-L36 - private const int DynamicDataSourceTypeProperty = 0; - private const int DynamicDataSourceTypeMethod = 1; - private const int DynamicDataSourceTypeAutoDetect = 2; - private const int DynamicDataSourceTypeField = 3; - - internal const string TestArgumentsEntryTypeName = "MSTF::InternalUnsafeTestArgumentsEntry"; - internal const string DynamicDataNameProviderTypeName = "MSTF::DynamicDataNameProvider"; - private const string TestArgumentsEntryProviderMethodName = nameof(TestArgumentsEntryProviderMethodName); - private const string TestArgumentsEntryProviderMethodType = nameof(TestArgumentsEntryProviderMethodType); - private readonly string _memberName; - private readonly string _memberFullType; - private readonly SymbolKind _memberKind; - private readonly TestMethodParametersInfo _testMethodParameters; - private readonly bool _targetMethodReturnsCollectionOfTestArgumentsEntry; - - private DynamicDataTestMethodArgumentsInfo(string memberName, string memberFullType, SymbolKind memberKind, TestMethodParametersInfo testMethodParameters, - bool targetMemberReturnsCollectionOfTestArgumentsEntry, string? generatorMethodFullName) - { - _memberName = memberName; - _memberFullType = memberFullType; - // This is always true, because it tells the source gen that GetParameters will return collection of TestArgumentsEntry. - // If the target member does not return that type, we will write code to adapt the result to this collection in AppendArguments method below. - IsTestArgumentsEntryReturnType = true; - _targetMethodReturnsCollectionOfTestArgumentsEntry = targetMemberReturnsCollectionOfTestArgumentsEntry; - _memberKind = memberKind; - GeneratorMethodFullName = generatorMethodFullName; - _testMethodParameters = testMethodParameters; - } - - public bool IsTestArgumentsEntryReturnType { get; } - - public string? GeneratorMethodFullName { get; } - - public static DynamicDataTestMethodArgumentsInfo? TryBuild(IMethodSymbol methodSymbol, List argumentsProviderAttributes, - WellKnownTypes wellKnownTypes) - { - // We don't support more than one provider on method at the same time. - if (argumentsProviderAttributes.Count != 1) - { - return null; - } - - // We collect all the providers that match, and they might conflict by parameters, but this also ties us to the actual type - // so user cannot subclass. - if (!SymbolEqualityComparer.Default.Equals(argumentsProviderAttributes[0].AttributeClass, wellKnownTypes.DynamicDataAttributeSymbol)) - { - return null; - } - - AttributeData attribute = argumentsProviderAttributes[0]; - INamedTypeSymbol memberTypeSymbol = methodSymbol.ContainingType; - string? memberName = null; - int memberKind = DynamicDataSourceTypeAutoDetect; - - foreach (TypedConstant arg in attribute.ConstructorArguments) - { - if (arg.Type?.SpecialType == SpecialType.System_String) - { - memberName = arg.Value?.ToString(); - } - else if (arg.Value is INamedTypeSymbol argTypeSymbol) - { - memberTypeSymbol = argTypeSymbol; - } - else if (arg.Value is int argValueAsInt) - { - memberKind = argValueAsInt; - } - } - - return memberName is null - ? null - : TryBuildFromDynamicData(memberTypeSymbol, memberName, ToSymbolKind(memberKind), wellKnownTypes, methodSymbol); - - static SymbolKind? ToSymbolKind(int memberKind) => - memberKind switch - { - DynamicDataSourceTypeProperty => SymbolKind.Property, - DynamicDataSourceTypeMethod => SymbolKind.Method, - DynamicDataSourceTypeField => SymbolKind.Field, - DynamicDataSourceTypeAutoDetect => null, - _ => throw ApplicationStateGuard.Unreachable(), - }; - } - - private static DynamicDataTestMethodArgumentsInfo? TryBuildFromDynamicData(INamedTypeSymbol memberTypeSymbol, string memberName, SymbolKind? symbolKind, - WellKnownTypes wellKnownTypes, IMethodSymbol testMethodSymbol) - { - // Dynamic data supports Properties, Methods, and Fields. - // null is also possible and means "AutoDetect" - if (symbolKind is not (SymbolKind.Property or SymbolKind.Method or SymbolKind.Field or null)) - { - return null; - } - - ISymbol? firstMatchingMember = memberTypeSymbol.GetAllMembers(memberName) - .SelectMany(x => x) - // DynamicData supports properties, methods, and fields. - // .Where(s => s.IsStatic && (s.Kind is SymbolKind.Field or SymbolKind.Property or SymbolKind.Method)) - // .Where(s => symbolKind == null || s.Kind == symbolKind) - .Where(s => s.IsStatic && (s.Kind == symbolKind || (symbolKind is null && s.Kind is SymbolKind.Property or SymbolKind.Method or SymbolKind.Field))) - .Select(s => s switch - { - IPropertySymbol propertySymbol => (ISymbol)propertySymbol, - IMethodSymbol methodSymbol => methodSymbol, - IFieldSymbol fieldSymbol => fieldSymbol, - _ => throw ApplicationStateGuard.Unreachable(), - }) - .OrderBy(tuple => tuple.Kind switch - { - SymbolKind.Property => 1, - SymbolKind.Method => 2, - SymbolKind.Field => 3, - _ => throw ApplicationStateGuard.Unreachable(), - }) - .FirstOrDefault(); - - if (firstMatchingMember is null - || firstMatchingMember.GetMemberType() is not { } returnMemberTypeSymbol) - { - return null; - } - - // We want to check if the member returns a type that implements IEnumerable> - var allInterfacesAndSelfIfInterface = new List(returnMemberTypeSymbol.AllInterfaces); - if (returnMemberTypeSymbol.TypeKind == TypeKind.Interface - && returnMemberTypeSymbol is INamedTypeSymbol namedInterfaceSymbol) - { - allInterfacesAndSelfIfInterface.Add(namedInterfaceSymbol); - } - - // If the return type is not IEnumerable> we will adapt it later. - // The implementation of https://github.com/microsoft/testfx/blob/main/src/TestFramework/TestFramework/Attributes/DataSource/DynamicDataAttribute.cs#L87 - // only allows IEnumerable so we will assume that is true. - bool targetMemberReturnsCollectionOfTestArgumentsEntry = allInterfacesAndSelfIfInterface.Any(i => - SymbolEqualityComparer.Default.Equals(i.OriginalDefinition, wellKnownTypes.IEnumerable1Symbol) - && i.TypeArguments.Length == 1 - && SymbolEqualityComparer.Default.Equals(i.TypeArguments[0].OriginalDefinition, wellKnownTypes.TestArgumentsEntrySymbol)); - - string? generatorMethodFullName = null; - - // This way we could handle the additional named parameters on [DynamicData - // DynamicDataDisplayName - // and DynamicDataDisplayNameDeclaringType, but this is imperfect implementation for now. - // KeyValuePair argumentsEntryProviderMethodType = namedArguments.FirstOrDefault(x => x.Key == TestArgumentsEntryProviderMethodType); - // if (argumentsEntryProviderMethodType.Key == TestArgumentsEntryProviderMethodType - // && argumentsEntryProviderMethodType.Value.Value is INamedTypeSymbol generatorMethodTypeSymbol) - // { - // generatorMethodFullName = generatorMethodTypeSymbol.ToDisplayString(); - // } - // - // KeyValuePair argumentsEntryProviderMethodName = namedArguments.FirstOrDefault(x => x.Key == TestArgumentsEntryProviderMethodName); - // if (argumentsEntryProviderMethodName.Key == TestArgumentsEntryProviderMethodName - // && argumentsEntryProviderMethodName.Value.Value is string generatorMethodName) - // { - // generatorMethodFullName ??= methodTypeSymbol.ToDisplayString(); - // generatorMethodFullName = $"{generatorMethodFullName}.{generatorMethodName}"; - // } - var testMethodParameters = new TestMethodParametersInfo(testMethodSymbol.Parameters); - - return new(firstMatchingMember.Name, firstMatchingMember.ContainingType.ToDisplayString(), - firstMatchingMember.Kind, testMethodParameters, targetMemberReturnsCollectionOfTestArgumentsEntry, generatorMethodFullName); - } - - public void AppendArguments(IndentedStringBuilder nodeBuilder) - { - nodeBuilder.Append("GetArguments = static () => "); - - using (nodeBuilder.AppendBlock()) - { - if (_targetMethodReturnsCollectionOfTestArgumentsEntry) - { - // We just return the data as is. - nodeBuilder.Append(" return "); - } - else - { - // We need to convert the data to TestArgumentsEntry. - nodeBuilder.Append("var data = "); - } - - // Call the member. - nodeBuilder.Append($"{_memberFullType}.{_memberName}"); - - if (_memberKind is SymbolKind.Method) - { - nodeBuilder.Append("()"); - } - - nodeBuilder.AppendLine(';'); - - if (!_targetMethodReturnsCollectionOfTestArgumentsEntry) - { - string tupleType = $"{TestArgumentsEntryTypeName}<{_testMethodParameters.ParametersTuple}>"; - nodeBuilder.AppendLine($"var dataCollection = new ColGen.List<{tupleType}>();"); - nodeBuilder.AppendLine($"var index = 0;"); - string expand = string.Join(", ", _testMethodParameters.Parameters.Select((p, i) => $"({p.FullyQualifiedType}) item[{i}]")); - using (nodeBuilder.AppendBlock("foreach (var item in data)")) - { - IEnumerable parameterNames = _testMethodParameters.Parameters.Select(p => p.Name); - nodeBuilder.AppendLine($$"""string uidFragment = {{DynamicDataNameProviderTypeName}}.GetUidFragment(new string[] {"{{string.Join("\", \"", parameterNames)}}"}, item, index);"""); - nodeBuilder.AppendLine("index++;"); - nodeBuilder.AppendLine($"""dataCollection.Add(new(({expand}), uidFragment));"""); - } - - nodeBuilder.AppendLine("return dataCollection;"); - } - } - - nodeBuilder.AppendLine(','); - } -} diff --git a/src/Analyzers/MSTest.SourceGeneration/ObjectModels/ITestMethodArgumentsInfo.cs b/src/Analyzers/MSTest.SourceGeneration/ObjectModels/ITestMethodArgumentsInfo.cs deleted file mode 100644 index c7e19cb416..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/ObjectModels/ITestMethodArgumentsInfo.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Framework.SourceGeneration.Helpers; - -namespace Microsoft.Testing.Framework.SourceGeneration.ObjectModels; - -internal interface ITestMethodArgumentsInfo -{ - bool IsTestArgumentsEntryReturnType { get; } - - string? GeneratorMethodFullName { get; } - - void AppendArguments(IndentedStringBuilder nodeBuilder); -} diff --git a/src/Analyzers/MSTest.SourceGeneration/ObjectModels/TestMethodInfo.cs b/src/Analyzers/MSTest.SourceGeneration/ObjectModels/TestMethodInfo.cs deleted file mode 100644 index 1abc1ebf88..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/ObjectModels/TestMethodInfo.cs +++ /dev/null @@ -1,343 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.Testing.Framework.SourceGeneration.Helpers; - -using MSTest.SourceGeneration.Helpers; - -namespace Microsoft.Testing.Framework.SourceGeneration.ObjectModels; - -internal sealed record class TestMethodInfo -{ - internal const string TestArgumentsEntryTypeName = "MSTF::InternalUnsafeTestArgumentsEntry"; - private const string CtorVariableName = "instance"; - private const string TestExecutionContextVariableName = "testExecutionContext"; - private const string DataVariableName = "data"; - private const string DataDotArgumentsMemberAccessName = DataVariableName + ".Arguments"; - private readonly EquatableArray<(string FilePath, int StartLine, int EndLine)> _declarationReferences; - private readonly string _methodName; - private readonly int _methodArity; - private readonly string _declaringAssemblyName; - private readonly string _usingTypeFullyQualifiedName; - private readonly bool _isAsync; - private readonly EquatableArray<(string Key, string? Value)> _testProperties; - private readonly TimeSpan? _testExecutionTimeout; - private readonly EquatableArray<(string RuleId, string Description)> _invocationPragmas; - private readonly string _methodIdentifierAssemblyName; - private readonly string _methodIdentifierNamespace; - private readonly string _methodIdentifierTypeName; - private readonly string _methodIdentifierReturnFullyQualifiedTypeName; - - private TestMethodInfo(IMethodSymbol methodSymbol, INamedTypeSymbol typeUsingMethod, ImmutableArray<(string Key, string? Value)> testProperties, - TestMethodParametersInfo parametersInfo, ITestMethodArgumentsInfo? argumentsInfo, - IEnumerable<(string RuleId, string Description)> invocationPragmas, TimeSpan? testExecutionTimeout) - { - // 'SymbolDisplayFormat.CSharpShortErrorMessageFormat' gives us the minimal name while preserving sub-classes - _methodIdentifierTypeName = methodSymbol.ContainingType.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat); - _methodIdentifierNamespace = methodSymbol.ContainingNamespace.IsGlobalNamespace - ? string.Empty - : methodSymbol.ContainingNamespace.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat); - _methodIdentifierReturnFullyQualifiedTypeName = methodSymbol.ReturnType.ToDisplayString(TestMethods.MethodIdentifierFullyQualifiedTypeFormat); - ArgumentsInfo = argumentsInfo; - _testExecutionTimeout = testExecutionTimeout; - _invocationPragmas = invocationPragmas.ToImmutableArray(); - _usingTypeFullyQualifiedName = typeUsingMethod.ToDisplayString(); - // NOTE: the method symbol containing type is the type declaring the method, not the type using the method. - string fullyQualifiedDisplayName = _usingTypeFullyQualifiedName - + "." - + methodSymbol.ToDisplayString().Substring(methodSymbol.ContainingType.ToDisplayString().Length + 1); - _declarationReferences = methodSymbol.DeclaringSyntaxReferences - .Select(x => (x.SyntaxTree.FilePath, x.SyntaxTree.GetLineSpan(x.Span))) - .Select(tuple => (tuple.FilePath, tuple.Item2.StartLinePosition.Line + 1, tuple.Item2.EndLinePosition.Line + 1)) - .ToImmutableArray(); - _methodName = methodSymbol.Name; - _methodArity = methodSymbol.Arity; - // 'SymbolDisplayFormat.FullyQualifiedFormat' would add version, culture and public key token to the assembly name. - _declaringAssemblyName = methodSymbol.ContainingAssembly.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); - _methodIdentifierAssemblyName = methodSymbol.ContainingAssembly.ToDisplayString(); - _isAsync = !methodSymbol.ReturnsVoid; - _testProperties = testProperties; - ParametersInfo = parametersInfo; - - TestMethodStableUid = $"\"{_declaringAssemblyName}.{fullyQualifiedDisplayName}\""; - } - - internal string TestMethodStableUid { get; } - - internal ITestMethodArgumentsInfo? ArgumentsInfo { get; } - - internal TestMethodParametersInfo ParametersInfo { get; } - - public static TestMethodInfo? TryBuild(IMethodSymbol methodSymbol, INamedTypeSymbol typeUsingMethod, WellKnownTypes wellKnownTypes) - { - try - { - // We don't need to be checking for resultant visibility here because we know parent is checking for it - if (!methodSymbol.IsValidTestMethodShape(wellKnownTypes) - || methodSymbol.IsKnownNonTestMethod(wellKnownTypes)) - { - return null; - } - - ImmutableArray attributes = methodSymbol.GetAttributes(); - - if (attributes.Length == 0 || !attributes.Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, wellKnownTypes.TestMethodAttributeSymbol))) - { - return null; - } - - List dataRowAttributes = []; - List dynamicDataAttributes = []; - List testPropertyAttributes = []; - List<(string RuleId, string Description)> pragmas = []; - TimeSpan? testExecutionTimeout = null; - foreach (AttributeData attribute in attributes) - { - if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, wellKnownTypes.DataRowAttributeSymbol) - && attribute.ConstructorArguments.Length == 1) - { - dataRowAttributes.Add(attribute); - } - else if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, wellKnownTypes.DynamicDataAttributeSymbol) - && attribute.ConstructorArguments.Length is 1 or 2 or 3) - { - dynamicDataAttributes.Add(attribute); - } - else if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, wellKnownTypes.TestPropertyAttributeSymbol) - && attribute.ConstructorArguments.Length == 2) - { - testPropertyAttributes.Add(attribute); - } - else if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, wellKnownTypes.SystemObsoleteAttributeSymbol)) - { - if (attribute.ConstructorArguments.Length == 0) - { - pragmas.Add(("CS0612", "Type or member is obsolete")); - } - else if (attribute.ConstructorArguments.Length == 1 - // We cannot suppress CS0619 as it's an error level - || (attribute.ConstructorArguments.Length == 2 && attribute.ConstructorArguments[1].Value?.Equals(false) == true)) - { - pragmas.Add(("CS0618", "Type or member is obsolete")); - } - } - else if (attribute.TryGetTestExecutionTimeout(wellKnownTypes.TestExecutionTimeoutAttributeSymbol, wellKnownTypes.TimeSpanSymbol, - out TimeSpan maybeTestExecutionTimeout)) - { - testExecutionTimeout = maybeTestExecutionTimeout; - } - } - - TestMethodParametersInfo parametersInfo = new(methodSymbol.Parameters); - - // TODO: This code is not handling the case where both DataRow and DynamicData attributes are present. - ITestMethodArgumentsInfo? argumentsInfo = - (ITestMethodArgumentsInfo?)DataRowTestMethodArgumentsInfo.TryBuild(methodSymbol, dataRowAttributes, parametersInfo) - ?? DynamicDataTestMethodArgumentsInfo.TryBuild(methodSymbol, dynamicDataAttributes, wellKnownTypes); - - ImmutableArray<(string Key, string? Value)> testProperties = testPropertyAttributes - .Where(attr => attr.ConstructorArguments[0].Value is not null) - .Select(attr => (attr.ConstructorArguments[0].Value!.ToString(), attr.ConstructorArguments[1].Value?.ToString())) - .ToImmutableArray(); - - // Method is valid test method - return new(methodSymbol, typeUsingMethod, testProperties, parametersInfo, argumentsInfo, pragmas, testExecutionTimeout); - } - catch (Exception ex) - { - throw new InvalidOperationException($"Failed for method {methodSymbol.ToDisplayString()}, with {ex}", ex); - } - } - - public void AppendTestNode(IndentedStringBuilder sourceStringBuilder, TestTypeInfo testTypeInfo) - { - bool useAsyncNode = _isAsync; - AppendTestNodeCtorDeclaration(sourceStringBuilder, useAsyncNode, ParametersInfo.ParametersTuple, ArgumentsInfo); - using (sourceStringBuilder.AppendBlock(closingBraceSuffixChar: ',')) - { - sourceStringBuilder.AppendLine($"StableUid = {TestMethodStableUid},"); - sourceStringBuilder.AppendLine($"DisplayName = \"{_methodName}\","); - - int propertiesCount = - 1 // properties that are always present - + _declarationReferences.Length - + _testProperties.Length; - using (sourceStringBuilder.AppendBlock($"Properties = new Msg::IProperty[{propertiesCount}]", closingBraceSuffixChar: ',')) - { - sourceStringBuilder.AppendLine("new Msg::TestMethodIdentifierProperty("); - sourceStringBuilder.IndentationLevel++; - sourceStringBuilder.AppendLine($"\"{_methodIdentifierAssemblyName}\","); - sourceStringBuilder.AppendLine($"\"{_methodIdentifierNamespace}\","); - sourceStringBuilder.AppendLine($"\"{_methodIdentifierTypeName}\","); - sourceStringBuilder.AppendLine($"\"{_methodName}\","); - sourceStringBuilder.AppendLine($"{_methodArity},"); - - if (ParametersInfo.ParametersMethodIdentifierFullyQualifiedTypes.Length > 0) - { - using (sourceStringBuilder.AppendBlock($"new string[{ParametersInfo.ParametersMethodIdentifierFullyQualifiedTypes.Length}]", closingBraceSuffixChar: ',')) - { - foreach (string parameterIdentifierType in ParametersInfo.ParametersMethodIdentifierFullyQualifiedTypes) - { - sourceStringBuilder.AppendLine($"\"{parameterIdentifierType}\","); - } - } - } - else - { - sourceStringBuilder.AppendLine("Sys::Array.Empty(),"); - } - - sourceStringBuilder.AppendLine($"\"{_methodIdentifierReturnFullyQualifiedTypeName}\"),"); - sourceStringBuilder.IndentationLevel--; - - foreach ((string filePath, int startLine, int endLine) in _declarationReferences) - { - sourceStringBuilder.AppendLine($"new Msg::TestFileLocationProperty(@\"{filePath}\", new(new({startLine}, -1), new({endLine}, -1))),"); - } - - foreach ((string key, string? value) in _testProperties) - { - sourceStringBuilder.AppendLine($"new Msg::TestMetadataProperty(\"{key}\", \"{value}\"),"); - } - } - - ArgumentsInfo?.AppendArguments(sourceStringBuilder); - sourceStringBuilder.Append("Body = static "); - if (useAsyncNode) - { - sourceStringBuilder.Append("async "); - } - - sourceStringBuilder.Append( - ParametersInfo.Parameters.IsEmpty - ? TestExecutionContextVariableName - : $"({TestExecutionContextVariableName}, {DataVariableName})"); - - using (sourceStringBuilder.AppendBlock(" =>", closingBraceSuffixChar: ',')) - { - MaybeAppendBodyCancellationTokenCreation(sourceStringBuilder, testTypeInfo); - AppendCtorCall(sourceStringBuilder, testTypeInfo); - AppendMethodCall(sourceStringBuilder); - } - } - } - - private void MaybeAppendBodyCancellationTokenCreation(IndentedStringBuilder sourceStringBuilder, TestTypeInfo testTypeInfo) - { - if (testTypeInfo.TestExecutionTimeout is null && _testExecutionTimeout is null) - { - return; - } - - TimeSpan minTimeout = (testTypeInfo.TestExecutionTimeout, _testExecutionTimeout) switch - { - (null, { } time) => time, - ({ } time, null) => time, - ({ } time1, { } time2) when time1 <= time2 => time1, - ({ } time1, { } time2) when time1 > time2 => time2, - _ => throw ApplicationStateGuard.Unreachable(), - }; - - sourceStringBuilder.AppendLine($"{TestExecutionContextVariableName}.CancelTestExecution(new global::System.TimeSpan({minTimeout.Ticks}));"); - } - - private static void AppendTestNodeCtorDeclaration(IndentedStringBuilder nodeBuilder, bool useAsyncNode, - string? parametersTuple, ITestMethodArgumentsInfo? argumentsInfo) - { - if (parametersTuple != null && argumentsInfo is null) - { - nodeBuilder.AppendLine("// The test method is parameterized but no argument was specified."); - nodeBuilder.AppendLine("// This is most often caused by using an unsupported arguments input."); - nodeBuilder.AppendLine("// Possible resolutions:"); - nodeBuilder.AppendLine("// - There is a mismatch between arguments from [DataRow] and the method parameters."); - nodeBuilder.AppendLine("// - There is a mismatch between arguments from [DynamicData] and the method parameters."); - nodeBuilder.AppendLine("// If nothing else worked, report the error and exclude this method by using [Ignore]."); - } - - nodeBuilder.Append("new MSTF::"); - nodeBuilder.Append((useAsyncNode, argumentsInfo) switch - { - (true, null) => "InternalUnsafeAsyncActionTestNode", - (true, _) => "InternalUnsafeAsyncActionParameterizedTestNode", - - (false, null) => "InternalUnsafeActionTestNode", - (false, _) => "InternalUnsafeActionParameterizedTestNode", - }); - nodeBuilder.AppendLine((parametersTuple, argumentsInfo?.IsTestArgumentsEntryReturnType ?? false) switch - { - (null, _) => string.Empty, - (_, false) => $"<{parametersTuple}>", - (_, true) => $"<{TestArgumentsEntryTypeName}<{parametersTuple}>>", - }); - } - - private static void AppendCtorCall(IndentedStringBuilder nodeBuilder, TestTypeInfo testTypeInfo) - { - if (testTypeInfo.IsIAsyncDisposable) - { - nodeBuilder.Append("await using "); - } - else if (testTypeInfo.IsIDisposable) - { - nodeBuilder.Append("using "); - } - - nodeBuilder.Append($"var {CtorVariableName} = new {testTypeInfo.ConstructorShortName}();"); - } - - private void AppendMethodCall(IndentedStringBuilder sourceStringBuilder) - { - sourceStringBuilder.AppendLine(); - IDisposable tryBlock = sourceStringBuilder.AppendBlock("try"); - - foreach ((string ruleId, string description) in _invocationPragmas) - { - sourceStringBuilder.AppendUnindentedLine($"#pragma warning disable {ruleId} // {description}"); - } - - if (_isAsync) - { - sourceStringBuilder.Append("await "); - } - - sourceStringBuilder.Append($"{CtorVariableName}.{_methodName}("); - - string dataVariable = ArgumentsInfo?.IsTestArgumentsEntryReturnType ?? false - ? DataDotArgumentsMemberAccessName - : DataVariableName; - - if (ParametersInfo.Parameters.Length == 1) - { - sourceStringBuilder.Append(dataVariable); - } - else - { - for (int i = 0; i < ParametersInfo.Parameters.Length; i++) - { - if (i > 0) - { - sourceStringBuilder.Append(", "); - } - - sourceStringBuilder.Append($"{dataVariable}.{ParametersInfo.Parameters[i].Name}"); - } - } - - sourceStringBuilder.AppendLine(");"); - - foreach ((string ruleId, string description) in _invocationPragmas) - { - sourceStringBuilder.AppendUnindentedLine($"#pragma warning restore {ruleId} // {description}"); - } - - tryBlock?.Dispose(); - - using (sourceStringBuilder.AppendBlock("catch (global::System.Exception ex)")) - { - sourceStringBuilder.AppendLine($"{TestExecutionContextVariableName}.ReportException(ex, null);"); - } - } -} diff --git a/src/Analyzers/MSTest.SourceGeneration/ObjectModels/TestMethodParametersInfo.cs b/src/Analyzers/MSTest.SourceGeneration/ObjectModels/TestMethodParametersInfo.cs deleted file mode 100644 index e92b79e650..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/ObjectModels/TestMethodParametersInfo.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; - -namespace Microsoft.Testing.Framework.SourceGeneration.ObjectModels; - -internal sealed class TestMethodParametersInfo -{ - public TestMethodParametersInfo(ImmutableArray parameters) - { - Parameters = parameters - .Select(p => (p.Name, p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))) - .ToImmutableArray(); - ParametersTuple = BuildParametersTupleString(Parameters); - ParametersMethodIdentifierFullyQualifiedTypes = parameters.Select(p => p.Type.ToDisplayString(TestMethods.MethodIdentifierFullyQualifiedTypeFormat)).ToImmutableArray(); - } - - public ImmutableArray<(string Name, string FullyQualifiedType)> Parameters { get; } - - public string? ParametersTuple { get; } - - public ImmutableArray ParametersMethodIdentifierFullyQualifiedTypes { get; } - - private static string? BuildParametersTupleString(ImmutableArray<(string Name, string FullyQualifiedType)> parameters) - { - if (parameters.IsDefaultOrEmpty) - { - return null; - } - - if (parameters.Length == 1) - { - return parameters[0].FullyQualifiedType; - } - - var tupleTypeStringBuilder = new StringBuilder("("); - - for (int i = 0; i < parameters.Length; i++) - { - if (i > 0) - { - tupleTypeStringBuilder.Append(", "); - } - - tupleTypeStringBuilder.Append(parameters[i].FullyQualifiedType).Append(' ').Append(parameters[i].Name); - } - - return tupleTypeStringBuilder.Append(')').ToString(); - } -} diff --git a/src/Analyzers/MSTest.SourceGeneration/ObjectModels/TestNamespaceInfo.cs b/src/Analyzers/MSTest.SourceGeneration/ObjectModels/TestNamespaceInfo.cs deleted file mode 100644 index 308a04ebea..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/ObjectModels/TestNamespaceInfo.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.CodeAnalysis; -using Microsoft.Testing.Framework.SourceGeneration.Helpers; - -namespace Microsoft.Testing.Framework.SourceGeneration.ObjectModels; - -internal sealed class TestNamespaceInfo : IEquatable -{ - private readonly string _nameOrGlobalNamespace; - private readonly string _containingAssembly; - - public string Name { get; } - - public string FullyQualifiedName { get; } - - public bool IsGlobalNamespace { get; } - - public TestNamespaceInfo(INamespaceSymbol namespaceSymbol) - { - _containingAssembly = namespaceSymbol.ContainingAssembly.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); - Name = namespaceSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); - _nameOrGlobalNamespace = namespaceSymbol.ToDisplayString(); - IsGlobalNamespace = namespaceSymbol.IsGlobalNamespace; - FullyQualifiedName = namespaceSymbol.IsGlobalNamespace - ? string.Empty - : namespaceSymbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat); - } - - public void AppendNamespaceTestNode(IndentedStringBuilder nodeStringBuilder, string testsVariableName) - { - using (nodeStringBuilder.AppendTestNode(_containingAssembly + "." + _nameOrGlobalNamespace, _nameOrGlobalNamespace, [], testsVariableName)) - { - } - } - - public bool Equals(TestNamespaceInfo? other) - => other is not null - && other.FullyQualifiedName == FullyQualifiedName; - - public override bool Equals(object? obj) - => Equals(obj as TestNamespaceInfo); - - public override int GetHashCode() - => FullyQualifiedName.GetHashCode(); -} diff --git a/src/Analyzers/MSTest.SourceGeneration/ObjectModels/TestTypeInfo.cs b/src/Analyzers/MSTest.SourceGeneration/ObjectModels/TestTypeInfo.cs deleted file mode 100644 index c1ec3a610e..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/ObjectModels/TestTypeInfo.cs +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using System.Collections.Immutable; - -using Analyzers.Utilities; - -using Microsoft.CodeAnalysis; -using Microsoft.Testing.Framework.SourceGeneration.Helpers; - -using MSTest.SourceGeneration.Helpers; - -namespace Microsoft.Testing.Framework.SourceGeneration.ObjectModels; - -internal sealed record class TestTypeInfo -{ - private readonly string _name; - private readonly string _containingAssemblyName; - private readonly EquatableArray<(string FilePath, int StartLine, int EndLine)> _declarationReferences; - - internal EquatableArray TestMethodNodes { get; } - - public TimeSpan? TestExecutionTimeout { get; } - - internal string GeneratedTypeName { get; } - - internal string FullyQualifiedName { get; } - - internal TestNamespaceInfo ContainingNamespace { get; } - - internal bool IsIAsyncDisposable { get; } - - internal bool IsIDisposable { get; } - - internal string ConstructorShortName { get; } - - private TestTypeInfo(INamedTypeSymbol namedTypeSymbol, IMethodSymbol ctorToUse, - WellKnownTypes wellKnownTypes, ImmutableArray testMethodNodes, TimeSpan? testExecutionTimeout) - { - _name = namedTypeSymbol.Name; - _declarationReferences = namedTypeSymbol.DeclaringSyntaxReferences - .Select(x => (x.SyntaxTree.FilePath, x.SyntaxTree.GetLineSpan(x.Span))) - .Select(tuple => (tuple.FilePath, tuple.Item2.StartLinePosition.Line + 1, tuple.Item2.EndLinePosition.Line + 1)) - .ToImmutableArray(); - TestMethodNodes = testMethodNodes; - TestExecutionTimeout = testExecutionTimeout; - _containingAssemblyName = namedTypeSymbol.ContainingAssembly.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); - ContainingNamespace = new(namedTypeSymbol.ContainingNamespace); - FullyQualifiedName = namedTypeSymbol.ToDisplayString(); - string escapedFullyQualifiedName = TestNodeHelpers.GenerateEscapedName(FullyQualifiedName); - GeneratedTypeName = ContainingNamespace.IsGlobalNamespace - ? "_" + escapedFullyQualifiedName - : escapedFullyQualifiedName; - - // 'SymbolDisplayFormat.CSharpShortErrorMessageFormat' gives us the minimal name while preserving sub-classes - ConstructorShortName = ctorToUse.ContainingType.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat); - - IsIAsyncDisposable = namedTypeSymbol.AllInterfaces.Any(i => - SymbolEqualityComparer.Default.Equals(i.OriginalDefinition, wellKnownTypes.IAsyncDisposableSymbol)); - IsIDisposable = namedTypeSymbol.AllInterfaces.Any(i => - SymbolEqualityComparer.Default.Equals(i.OriginalDefinition, wellKnownTypes.IDisposableSymbol)); - } - - public static TestTypeInfo? TryBuild(GeneratorAttributeSyntaxContext context, WellKnownTypes wellKnownTypes) - { - if (context.TargetSymbol is not INamedTypeSymbol namedTypeSymbol) - { - return null; - } - - // The generator syntax checks should have already filtered out any types that are not public/internal but we still need - // to check because a public subclass of a non-public class is still not public. - if (namedTypeSymbol.GetResultantVisibility() is not SymbolVisibility.Public and not SymbolVisibility.Internal) - { - return null; - } - - // We only support simple classes - if (namedTypeSymbol.IsAbstract - || namedTypeSymbol.IsAnonymousType - || namedTypeSymbol.IsGenericType - || namedTypeSymbol.IsImplicitClass) - { - return null; - } - - if (!HasParameterlessConstructor(namedTypeSymbol, out IMethodSymbol? parameterlessCtor)) - { - return null; - } - - TimeSpan? testExecutionTimeout = null; - foreach (AttributeData attribute in namedTypeSymbol.GetAttributes()) - { - if (attribute.TryGetTestExecutionTimeout(wellKnownTypes.TestExecutionTimeoutAttributeSymbol, wellKnownTypes.TimeSpanSymbol, - out TimeSpan maybeExecutionTimeout)) - { - testExecutionTimeout = maybeExecutionTimeout; - } - } - - var testMethodNodes = namedTypeSymbol - .GetAllMembers() - .SelectMany(members => members) - .OfType() - .Select(method => TestMethodInfo.TryBuild(method, namedTypeSymbol, wellKnownTypes)) - .WhereNotNull() - .ToImmutableArray(); - - return new(namedTypeSymbol, parameterlessCtor, wellKnownTypes, testMethodNodes, testExecutionTimeout); - } - - private static bool HasParameterlessConstructor(INamedTypeSymbol namedTypeSymbol, [NotNullWhen(true)] out IMethodSymbol? parameterlessConstructor) - { - parameterlessConstructor = namedTypeSymbol.InstanceConstructors - .FirstOrDefault(ctor => ctor.DeclaredAccessibility == Accessibility.Public && ctor.Parameters.Length == 0); - - return parameterlessConstructor != null; - } - - public void AppendTestNode(IndentedStringBuilder sourceStringBuilder) - { - IDisposable? block = null; - - try - { - if (!ContainingNamespace.IsGlobalNamespace) - { - sourceStringBuilder.Append("namespace "); - // TODO: Understand how to retrieve assembly default namespace and use it instead of assembly name - block = sourceStringBuilder.AppendBlock(ContainingNamespace.FullyQualifiedName); - } - - sourceStringBuilder.AppendLine("using Threading = global::System.Threading;"); - sourceStringBuilder.AppendLine("using ColGen = global::System.Collections.Generic;"); - sourceStringBuilder.AppendLine("using CA = global::System.Diagnostics.CodeAnalysis;"); - sourceStringBuilder.AppendLine("using Sys = global::System;"); - - sourceStringBuilder.AppendLine("using Msg = global::Microsoft.Testing.Platform.Extensions.Messages;"); - sourceStringBuilder.AppendLine("using MSTF = global::Microsoft.Testing.Framework;"); - - sourceStringBuilder.AppendLine(); - - sourceStringBuilder.AppendLine("[CA::ExcludeFromCodeCoverage]"); - using (sourceStringBuilder.AppendBlock($"public static class {GeneratedTypeName}")) - { - sourceStringBuilder.Append("public static readonly MSTF::TestNode TestNode = "); - AppendTestNodeCreation(sourceStringBuilder); - } - } - finally - { - block?.Dispose(); - } - } - - private void AppendTestNodeCreation(IndentedStringBuilder sourceStringBuilder) - { - List properties = []; - foreach ((string filePath, int startLine, int endLine) in _declarationReferences) - { - properties.Add($"new Msg::TestFileLocationProperty(@\"{filePath}\", new(new({startLine}, -1), new({endLine}, -1))),"); - } - - using (sourceStringBuilder.AppendTestNode(_containingAssemblyName + "." + FullyQualifiedName, _name, properties, ';')) - { - foreach (TestMethodInfo testMethod in TestMethodNodes) - { - testMethod.AppendTestNode(sourceStringBuilder, this); - } - } - } -} diff --git a/src/Analyzers/MSTest.SourceGeneration/PACKAGE.md b/src/Analyzers/MSTest.SourceGeneration/PACKAGE.md deleted file mode 100644 index cdd8f5ba18..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/PACKAGE.md +++ /dev/null @@ -1,9 +0,0 @@ -# Microsoft.Testing - -Microsoft Testing is a set of platform, framework and protocol intended to make it possible to run any test on any target or device. - -Documentation can be found at . - -## About - -This package works with MSTest.Engine package to provide source generators. diff --git a/src/Package/MSTest.Sdk/MSTest.Sdk.csproj b/src/Package/MSTest.Sdk/MSTest.Sdk.csproj index a9207301b1..3218486287 100644 --- a/src/Package/MSTest.Sdk/MSTest.Sdk.csproj +++ b/src/Package/MSTest.Sdk/MSTest.Sdk.csproj @@ -36,13 +36,13 @@ - <_MSTestEnginePreReleaseVersionLabel>$(MSTestEnginePreReleaseVersionLabel) - <_MSTestEnginePreReleaseVersionLabel Condition="'$(ContinuousIntegrationBuild)' == 'true' and '$(OfficialBuild)' != 'true'">ci - <_MSTestEnginePreReleaseVersionLabel Condition="'$(ContinuousIntegrationBuild)' != 'true' and '$(OfficialBuild)' != 'true'">dev - <_MSTestEngineVersionSuffix>$(_MSTestEnginePreReleaseVersionLabel)$(_BuildNumberLabels) - <_MSTestEngineVersion>$(MSTestEngineVersionPrefix) - <_MSTestEngineVersion Condition="'$(_MSTestEngineVersionSuffix)' != ''">$(_MSTestEngineVersion)-$(_MSTestEngineVersionSuffix) - <_TemplateProperties>MSTestEngineVersion=$(_MSTestEngineVersion);MSTestVersion=$(Version);MicrosoftTestingPlatformVersion=$(Version.Replace('$(VersionPrefix)', '$(TestingPlatformVersionPrefix)'));MicrosoftNETTestSdkVersion=$(MicrosoftNETTestSdkVersion);MicrosoftTestingExtensionsCodeCoverageVersion=$(MicrosoftTestingExtensionsCodeCoverageVersion);MicrosoftPlaywrightVersion=$(MicrosoftPlaywrightVersion);AspireHostingTestingVersion=$(AspireHostingTestingVersion);MicrosoftTestingExtensionsFakesVersion=$(MicrosoftTestingExtensionsFakesVersion) + <_MSTestSourceGenerationPreReleaseVersionLabel>$(MSTestSourceGenerationPreReleaseVersionLabel) + <_MSTestSourceGenerationPreReleaseVersionLabel Condition="'$(ContinuousIntegrationBuild)' == 'true' and '$(OfficialBuild)' != 'true'">ci + <_MSTestSourceGenerationPreReleaseVersionLabel Condition="'$(ContinuousIntegrationBuild)' != 'true' and '$(OfficialBuild)' != 'true'">dev + <_MSTestSourceGenerationVersionSuffix>$(_MSTestSourceGenerationPreReleaseVersionLabel)$(_BuildNumberLabels) + <_MSTestSourceGenerationVersion>$(MSTestSourceGenerationVersionPrefix) + <_MSTestSourceGenerationVersion Condition="'$(_MSTestSourceGenerationVersionSuffix)' != ''">$(_MSTestSourceGenerationVersion)-$(_MSTestSourceGenerationVersionSuffix) + <_TemplateProperties>MSTestSourceGenerationVersion=$(_MSTestSourceGenerationVersion);MSTestVersion=$(Version);MicrosoftTestingPlatformVersion=$(Version.Replace('$(VersionPrefix)', '$(TestingPlatformVersionPrefix)'));MicrosoftNETTestSdkVersion=$(MicrosoftNETTestSdkVersion);MicrosoftTestingExtensionsCodeCoverageVersion=$(MicrosoftTestingExtensionsCodeCoverageVersion);MicrosoftPlaywrightVersion=$(MicrosoftPlaywrightVersion);AspireHostingTestingVersion=$(AspireHostingTestingVersion);MicrosoftTestingExtensionsFakesVersion=$(MicrosoftTestingExtensionsFakesVersion) false - - + + -#file Program.cs -using System.Diagnostics; -using System.Reflection; -using System.Runtime.InteropServices; -using Microsoft.Testing.Framework; -using Microsoft.Testing.Internal.Framework; -using Microsoft.Testing.Platform.Builder; -using Microsoft.Testing.Platform.Capabilities; -using Microsoft.Testing.Platform.Capabilities.TestFramework; -using Microsoft.Testing.Platform.Extensions.Messages; -using Microsoft.Testing.Platform.Extensions.TestFramework; - -using MSTestNativeAotTests; - -ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args); -builder.AddTestFramework(new SourceGeneratedTestNodesBuilder()); -using ITestApplication app = await builder.BuildAsync(); -return await app.RunAsync(); - #file TestClass1.cs using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -98,7 +80,7 @@ public async Task NativeAotTests_WillRunWithExitCodeZero(string tfm) .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion) .PatchCodeWithReplace("$TargetFramework$", tfm) .PatchCodeWithReplace("$MSTestVersion$", MSTestVersion) - .PatchCodeWithReplace("$MSTestEngineVersion$", MSTestEngineVersion), + .PatchCodeWithReplace("$MSTestSourceGenerationVersion$", MSTestSourceGenerationVersion), addPublicFeeds: true); DotnetMuxerResult compilationResult = await DotnetCli.RunAsync( @@ -109,7 +91,7 @@ public async Task NativeAotTests_WillRunWithExitCodeZero(string tfm) var testHost = TestHost.LocateFrom(generator.TargetAssetPath, "MSTestNativeAotTests", tfm, RID, Verb.publish); TestHostResult result = await testHost.ExecuteAsync(cancellationToken: TestContext.CancellationToken); - result.AssertOutputContains($"MSTest.Engine v{MSTestEngineVersion}"); + result.AssertOutputContains("Passed! - Failed: 0, Passed: 3, Skipped: 0, Total: 3"); result.AssertExitCodeIs(0); } diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TrimTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TrimTests.cs index 7fe25eae29..a106654c64 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TrimTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TrimTests.cs @@ -18,26 +18,36 @@ public class TrimTests : AcceptanceTestBase $TargetFramework$ Exe + true true false - - + + - + -#file Program.cs -System.Console.WriteLine("This project validates trim/AOT compatibility via dotnet publish."); +#file UnitTest1.cs +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class UnitTest1 +{ + [TestMethod] + public void TestMethod1() + { + } +} """; [TestMethod] @@ -52,7 +62,7 @@ public async Task Publish_ShouldNotProduceTrimWarnings(string tfm) TrimAnalysisSourceCode .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion) .PatchCodeWithReplace("$MSTestVersion$", MSTestVersion) - .PatchCodeWithReplace("$MSTestEngineVersion$", MSTestEngineVersion) + .PatchCodeWithReplace("$MSTestSourceGenerationVersion$", MSTestSourceGenerationVersion) .PatchCodeWithReplace("$TargetFramework$", tfm), addPublicFeeds: true); diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/Helpers/AcceptanceTestBase.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/Helpers/AcceptanceTestBase.cs index 2f8329d525..7b5996a6d3 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/Helpers/AcceptanceTestBase.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/Helpers/AcceptanceTestBase.cs @@ -14,7 +14,7 @@ static AcceptanceTestBase() MSTestVersion = ExtractVersionFromPackage(Constants.ArtifactsPackagesShipping, "MSTest.TestFramework."); MicrosoftTestingPlatformVersion = ExtractVersionFromPackage(Constants.ArtifactsPackagesShipping, "Microsoft.Testing.Platform."); - MSTestEngineVersion = ExtractVersionFromPackage(Constants.ArtifactsPackagesShipping, "MSTest.Engine."); + MSTestSourceGenerationVersion = ExtractVersionFromPackage(Constants.ArtifactsPackagesShipping, "MSTest.SourceGeneration."); MicrosoftTestingExtensionsLoggingVersion = ExtractVersionFromPackage(Constants.ArtifactsPackagesShipping, "Microsoft.Testing.Extensions.Logging."); } @@ -29,7 +29,7 @@ static AcceptanceTestBase() public static string MSTestVersion { get; private set; } - public static string MSTestEngineVersion { get; private set; } + public static string MSTestSourceGenerationVersion { get; private set; } public static string MicrosoftNETTestSdkVersion { get; private set; } diff --git a/test/UnitTests/MSTest.Engine.UnitTests/Adapter_ExecuteRequestAsyncTests.cs b/test/UnitTests/MSTest.Engine.UnitTests/Adapter_ExecuteRequestAsyncTests.cs deleted file mode 100644 index d7fb5bb4a5..0000000000 --- a/test/UnitTests/MSTest.Engine.UnitTests/Adapter_ExecuteRequestAsyncTests.cs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using Microsoft.Testing.Platform.Configurations; -using Microsoft.Testing.Platform.Extensions.Messages; -using Microsoft.Testing.Platform.Helpers; -using Microsoft.Testing.Platform.Logging; -using Microsoft.Testing.Platform.Messages; -using Microsoft.Testing.Platform.Requests; -using Microsoft.Testing.Platform.Services; - -namespace Microsoft.Testing.Framework.UnitTests; - -[TestClass] -public class Adapter_ExecuteRequestAsyncTests : TestBase -{ - [TestMethod] - public async Task ExecutableNode_ThatDoesNotThrow_ShouldReportPassed() - { - // Arrange - var testNode = new InternalUnsafeActionTestNode - { - StableUid = "Microsoft.Testing.Framework.UnitTests.Adapter_ExecuteRequestAsyncTests.ExecutableNode_ThatDoesNotThrow_ShouldReportPassed()", - DisplayName = "Microsoft.Testing.Framework.UnitTests.Adapter_ExecuteRequestAsyncTests.ExecutableNode_ThatDoesNotThrow_ShouldReportPassed()", - Body = testExecutionContext => { }, - }; - - var services = new Services(); - var adapter = new TestFramework(new(), [new FactoryTestNodesBuilder(() => [testNode])], new(), - services.ServiceProvider.GetSystemClock(), services.ServiceProvider.GetTask(), services.ServiceProvider.GetConfiguration(), new Platform.Capabilities.TestFramework.TestFrameworkCapabilities()); - - CancellationToken cancellationToken = CancellationToken.None; - - // Act - await adapter.ExecuteRequestAsync(new( - new RunTestExecutionRequest(new(new("id"))), - services.ServiceProvider.GetRequiredService(), - new SemaphoreSlimRequestCompleteNotifier(new SemaphoreSlim(1)), - cancellationToken)); - - // Assert - IEnumerable nodeStateChanges = services.MessageBus.Messages.OfType(); - Assert.IsNotEmpty(nodeStateChanges, $"{nameof(nodeStateChanges)} should have at least 1 item."); - Platform.Extensions.Messages.TestNode lastNode = nodeStateChanges.Last().TestNode; - _ = lastNode.Properties.Single(); - } - - [TestMethod] - public async Task ExecutableNode_ThatThrows_ShouldReportError() - { - // Arrange - var testNode = new InternalUnsafeActionTestNode - { - StableUid = "Microsoft.Testing.Framework.UnitTests.Adapter_ExecuteRequestAsyncTests.ExecutableNode_ThatThrows_ShouldReportError()", - DisplayName = "Microsoft.Testing.Framework.UnitTests.Adapter_ExecuteRequestAsyncTests.ExecutableNode_ThatThrows_ShouldReportError()", - Body = testExecutionContext => throw new InvalidOperationException("Oh no!") { }, - }; - - var services = new Services(); - var fakeClock = (FakeClock)services.ServiceProvider.GetService(typeof(FakeClock))!; - - var adapter = new TestFramework(new(), [new FactoryTestNodesBuilder(() => [testNode])], new(), - services.ServiceProvider.GetSystemClock(), services.ServiceProvider.GetTask(), services.ServiceProvider.GetConfiguration(), new Platform.Capabilities.TestFramework.TestFrameworkCapabilities()); - CancellationToken cancellationToken = CancellationToken.None; - - // Act - await adapter.ExecuteRequestAsync(new( - new RunTestExecutionRequest(new(new("id"))), - services.ServiceProvider.GetRequiredService(), - new SemaphoreSlimRequestCompleteNotifier(new SemaphoreSlim(1)), - cancellationToken)); - - // Assert - IEnumerable nodeStateChanges = services.MessageBus.Messages.OfType(); - Assert.IsNotEmpty(nodeStateChanges, $"{nameof(nodeStateChanges)} should have at least 1 item."); - Platform.Extensions.Messages.TestNode lastNode = nodeStateChanges.Last().TestNode; - _ = lastNode.Properties.Single(); - Assert.AreEqual("Oh no!", lastNode.Properties.Single().Exception!.Message); - Assert.Contains( - nameof(ExecutableNode_ThatThrows_ShouldReportError), lastNode.Properties.Single().Exception!.StackTrace!, "lastNode properties should contain the name of the test"); - TimingProperty timingProperty = lastNode.Properties.Single(); - Assert.AreEqual(fakeClock.UsedTimes[0], timingProperty.GlobalTiming.StartTime); - Assert.IsLessThanOrEqualTo(timingProperty.GlobalTiming.EndTime, timingProperty.GlobalTiming.StartTime, "start time is before (or the same as) stop time"); - Assert.AreEqual(fakeClock.UsedTimes[1], timingProperty.GlobalTiming.EndTime); - Assert.IsGreaterThan(0, timingProperty.GlobalTiming.Duration.TotalMilliseconds, $"duration should be greater than 0"); - } - - private sealed class FakeClock : IClock - { - public List UsedTimes { get; } = []; - - public DateTimeOffset UtcNow - { - get - { - DateTimeOffset date = DateTimeOffset.UtcNow; - UsedTimes.Add(date); - return date; - } - } - } - - private sealed class Services - { - public Services() - { - MessageBus = new MessageBus(); - ServiceProvider.AddService(MessageBus); - ServiceProvider.AddService(new LoggerFactory()); - ServiceProvider.AddService(new FakeClock()); - ServiceProvider.AddService(new SystemTask()); - ServiceProvider.AddService(new AggregatedConfiguration([], new CurrentTestApplicationModuleInfo(new SystemEnvironment(), new SystemProcessHandler()), new SystemFileSystem(), new SystemEnvironment(), new(null, [], []))); - } - - public MessageBus MessageBus { get; } - - public ServiceProvider ServiceProvider { get; } = new(); - } - - private sealed class MessageBus : IMessageBus - { - public List Messages { get; } = []; - - public Task PublishAsync(IDataProducer dataProducer, IData data) - { - Messages.Add(data); - return Task.CompletedTask; - } - } - - private sealed class LoggerFactory : ILoggerFactory - { - public ILogger CreateLogger(string categoryName) => new NopLogger(); - } -} diff --git a/test/UnitTests/MSTest.Engine.UnitTests/BFSTestNodeVisitorTests.cs b/test/UnitTests/MSTest.Engine.UnitTests/BFSTestNodeVisitorTests.cs deleted file mode 100644 index 3c7092c7e2..0000000000 --- a/test/UnitTests/MSTest.Engine.UnitTests/BFSTestNodeVisitorTests.cs +++ /dev/null @@ -1,345 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using Microsoft.Testing.Platform.Extensions.Messages; -using Microsoft.Testing.Platform.Requests; - -namespace Microsoft.Testing.Framework.UnitTests; - -[TestClass] -public sealed class BFSTestNodeVisitorTests : TestBase -{ - [TestMethod] - public async Task Visit_WhenFilterDoesNotUseEncodedSlash_NodeIsNotIncluded() - { - // Arrange - var rootNode = new TestNode - { - StableUid = "ID1", - DisplayName = "A", - Tests = - [ - new TestNode - { - StableUid = "ID2", - DisplayName = "B/C", - }, - ], - }; - - var filter = new TreeNodeFilter("/A/B/C"); - var visitor = new BFSTestNodeVisitor(new[] { rootNode }, filter, null!); - - // Act - List includedTestNodes = []; - await visitor.VisitAsync((testNode, _) => - { - includedTestNodes.Add(testNode); - return Task.CompletedTask; - }); - - // Assert - Assert.HasCount(1, includedTestNodes); - Assert.AreEqual("ID1", includedTestNodes[0].StableUid); - } - - [DataRow("/", "%2F")] - [DataRow("%2F", "%252F")] - [DataRow("//", "%2F%2F")] - [TestMethod] - public async Task Visit_WhenFilterUsesEncodedEntry_NodeIsIncluded(string nodeSpecialString, string filterEncodedSpecialString) - { - // Arrange - var rootNode = new TestNode - { - StableUid = "ID1", - DisplayName = "A", - Tests = - [ - new TestNode - { - StableUid = "ID2", - DisplayName = "B" + nodeSpecialString + "C", - }, - ], - }; - - var filter = new TreeNodeFilter("/A/B" + filterEncodedSpecialString + "C"); - var visitor = new BFSTestNodeVisitor(new[] { rootNode }, filter, null!); - - // Act - List includedTestNodes = []; - await visitor.VisitAsync((testNode, _) => - { - includedTestNodes.Add(testNode); - return Task.CompletedTask; - }); - - // Assert - Assert.HasCount(2, includedTestNodes); - Assert.AreEqual("ID1", includedTestNodes[0].StableUid); - Assert.AreEqual("ID2", includedTestNodes[1].StableUid); - } - - [DataRow(nameof(TestNode))] - [DataRow(nameof(InternalUnsafeActionTestNode))] - [TestMethod] - public async Task Visit_WhenNodeIsNotParameterizedNode_DoesNotExpand(string nonParameterizedTestNode) - { - // Arrange - TestNode rootNode = nonParameterizedTestNode switch - { - nameof(TestNode) => new TestNode - { - StableUid = "ID1", - DisplayName = "A", - }, - nameof(InternalUnsafeActionTestNode) => new InternalUnsafeActionTestNode - { - StableUid = "ID1", - DisplayName = "A", - Body = _ => { }, - }, - _ => throw new ArgumentException($"Unknown test node type: {nonParameterizedTestNode}", nameof(nonParameterizedTestNode)), - }; - - var visitor = new BFSTestNodeVisitor(new[] { rootNode }, new NopFilter(), null!); - - // Act - List includedTestNodes = []; - await visitor.VisitAsync((testNode, _) => - { - includedTestNodes.Add(testNode); - return Task.CompletedTask; - }); - - // Assert - Assert.HasCount(1, includedTestNodes); - Assert.AreEqual("ID1", includedTestNodes[0].StableUid); - } - - [DataRow(nameof(InternalUnsafeActionParameterizedTestNode<>), true)] - [DataRow(nameof(InternalUnsafeActionParameterizedTestNode<>), false)] - [DataRow(nameof(InternalUnsafeAsyncActionParameterizedTestNode<>), true)] - [DataRow(nameof(InternalUnsafeAsyncActionParameterizedTestNode<>), false)] - [DataRow(nameof(InternalUnsafeActionTaskParameterizedTestNode<>), true)] - [DataRow(nameof(InternalUnsafeActionTaskParameterizedTestNode<>), false)] - [DataRow(nameof(InternalUnsafeAsyncActionTaskParameterizedTestNode<>), true)] - [DataRow(nameof(InternalUnsafeAsyncActionTaskParameterizedTestNode<>), false)] - [TestMethod] - public async Task Visit_WhenNodeIsParameterizedNodeAndPropertyIsAbsentOrTrue_ExpandNode(string parameterizedTestNode, bool hasExpansionProperty) - { - // Arrange - TestNode rootNode = CreateParameterizedTestNode(parameterizedTestNode, hasExpansionProperty ? false : null); - var visitor = new BFSTestNodeVisitor(new[] { rootNode }, new NopFilter(), new TestArgumentsManager()); - - // Act - List<(TestNode Node, TestNodeUid? ParentNodeUid)> includedTestNodes = []; - await visitor.VisitAsync((testNode, parentNodeUid) => - { - includedTestNodes.Add((testNode, parentNodeUid)); - return Task.CompletedTask; - }); - - // Assert - Assert.HasCount(3, includedTestNodes); - - Assert.AreEqual("ID1", includedTestNodes[0].Node.StableUid); - Assert.IsNull(includedTestNodes[0].ParentNodeUid); - - Assert.AreEqual("ID1 [0]", includedTestNodes[1].Node.StableUid); - Assert.AreEqual("ID1", includedTestNodes[1].ParentNodeUid); - - Assert.AreEqual("ID1 [1]", includedTestNodes[2].Node.StableUid); - Assert.AreEqual("ID1", includedTestNodes[2].ParentNodeUid); - } - - [DataRow(nameof(InternalUnsafeActionParameterizedTestNode<>))] - [DataRow(nameof(InternalUnsafeAsyncActionParameterizedTestNode<>))] - [DataRow(nameof(InternalUnsafeActionTaskParameterizedTestNode<>))] - [DataRow(nameof(InternalUnsafeAsyncActionTaskParameterizedTestNode<>))] - [TestMethod] - public async Task Visit_WhenNodeIsParameterizedNodeAndDoesNotAllowExpansion_DoesNotExpand(string parameterizedTestNode) - { - // Arrange - TestNode rootNode = CreateParameterizedTestNode(parameterizedTestNode, true); - var visitor = new BFSTestNodeVisitor(new[] { rootNode }, new NopFilter(), new TestArgumentsManager()); - - // Act - List<(TestNode Node, TestNodeUid? ParentNodeUid)> includedTestNodes = []; - await visitor.VisitAsync((testNode, parentNodeUid) => - { - includedTestNodes.Add((testNode, parentNodeUid)); - return Task.CompletedTask; - }); - - // Assert - Assert.HasCount(1, includedTestNodes); - } - - [TestMethod] - public async Task Visit_WithModuleNamespaceClassMethodLevelAndExpansion_DiscoverTestsWithCorrectParentsAndTypes() - { - // Arrange - var rootNode = new TestNode - { - StableUid = "MyModule", - DisplayName = "MyModule", - Tests = - [ - new TestNode - { - StableUid = "MyNamespace", - DisplayName = "MyNamespace", - Tests = - [ - new TestNode - { - StableUid = "MyType", - DisplayName = "MyType", - Tests = new[] - { - new InternalUnsafeActionParameterizedTestNode - { - StableUid = "MyMethod", - DisplayName = "MyMethod", - GetArguments = () => new byte[] { 0, 1, 2 }, - Body = (_, _) => { }, - }, - }, - }, - ], - }, - ], - }; - var visitor = new BFSTestNodeVisitor(new[] { rootNode }, new NopFilter(), new TestArgumentsManager()); - - // Act - List<(TestNode Node, TestNodeUid? ParentNodeUid)> includedTestNodes = []; - await visitor.VisitAsync((testNode, parentNodeUid) => - { - includedTestNodes.Add((testNode, parentNodeUid)); - return Task.CompletedTask; - }); - - // Assert - Assert.HasCount(7, includedTestNodes); - - Assert.AreEqual("MyModule", includedTestNodes[0].Node.StableUid); - Assert.IsNull(includedTestNodes[0].ParentNodeUid); - Assert.AreEqual(typeof(TestNode), includedTestNodes[0].Node.GetType()); - - Assert.AreEqual("MyNamespace", includedTestNodes[1].Node.StableUid); - Assert.AreEqual("MyModule", includedTestNodes[1].ParentNodeUid); - Assert.AreEqual(typeof(TestNode), includedTestNodes[1].Node.GetType()); - - Assert.AreEqual("MyType", includedTestNodes[2].Node.StableUid); - Assert.AreEqual("MyNamespace", includedTestNodes[2].ParentNodeUid); - Assert.AreEqual(typeof(TestNode), includedTestNodes[2].Node.GetType()); - - Assert.AreEqual("MyMethod", includedTestNodes[3].Node.StableUid); - Assert.AreEqual("MyType", includedTestNodes[3].ParentNodeUid); - Assert.AreEqual(typeof(TestNode), includedTestNodes[3].Node.GetType()); - - Assert.AreEqual("MyMethod [0]", includedTestNodes[4].Node.StableUid); - Assert.AreEqual("MyMethod", includedTestNodes[4].ParentNodeUid); - Assert.AreEqual(typeof(InternalUnsafeActionTestNode), includedTestNodes[4].Node.GetType()); - - Assert.AreEqual("MyMethod [1]", includedTestNodes[5].Node.StableUid); - Assert.AreEqual("MyMethod", includedTestNodes[5].ParentNodeUid); - Assert.AreEqual(typeof(InternalUnsafeActionTestNode), includedTestNodes[5].Node.GetType()); - - Assert.AreEqual("MyMethod [2]", includedTestNodes[6].Node.StableUid); - Assert.AreEqual("MyMethod", includedTestNodes[6].ParentNodeUid); - Assert.AreEqual(typeof(InternalUnsafeActionTestNode), includedTestNodes[6].Node.GetType()); - } - - [TestMethod] - public async Task Visit_WhenFilterHasPropertyExpression_OnlyIncludesNodesMatchingProperty() - { - // Arrange — filter with a property expression (ContainsPropertyFilters == true) - var nodeWithMatchingTag = new TestNode - { - StableUid = "ID1", - DisplayName = "A", - Properties = [new TestMetadataProperty("Tag", "Fast")], - }; - var nodeWithNonMatchingTag = new TestNode - { - StableUid = "ID2", - DisplayName = "A", - Properties = [new TestMetadataProperty("Tag", "Slow")], - }; - - var filter = new TreeNodeFilter("/A[Tag=Fast]"); - var visitor = new BFSTestNodeVisitor(new[] { nodeWithMatchingTag, nodeWithNonMatchingTag }, filter, null!); - - // Act - List includedTestNodes = []; - await visitor.VisitAsync((testNode, _) => - { - includedTestNodes.Add(testNode); - return Task.CompletedTask; - }); - - // Assert - Assert.HasCount(1, includedTestNodes); - Assert.AreEqual("ID1", includedTestNodes[0].StableUid); - } - - private static TestNode CreateParameterizedTestNode(string parameterizedTestNode, bool? expansionPropertyValue) - { - TestNode rootNode = parameterizedTestNode switch - { - nameof(InternalUnsafeActionParameterizedTestNode<>) => new InternalUnsafeActionParameterizedTestNode - { - StableUid = "ID1", - DisplayName = "A", - Body = (_, _) => { }, - GetArguments = GetArguments, - Properties = GetProperties(expansionPropertyValue), - }, - nameof(InternalUnsafeAsyncActionParameterizedTestNode<>) => new InternalUnsafeAsyncActionParameterizedTestNode - { - StableUid = "ID1", - DisplayName = "A", - Body = static (_, _) => Task.CompletedTask, - GetArguments = GetArguments, - Properties = GetProperties(expansionPropertyValue), - }, - nameof(InternalUnsafeActionTaskParameterizedTestNode<>) => new InternalUnsafeActionTaskParameterizedTestNode - { - StableUid = "ID1", - DisplayName = "A", - Body = (_, _) => { }, - GetArguments = GetArgumentsAsync, - Properties = GetProperties(expansionPropertyValue), - }, - nameof(InternalUnsafeAsyncActionTaskParameterizedTestNode<>) => new InternalUnsafeAsyncActionTaskParameterizedTestNode - { - StableUid = "ID1", - DisplayName = "A", - Body = static (_, _) => Task.CompletedTask, - GetArguments = GetArgumentsAsync, - Properties = GetProperties(expansionPropertyValue), - }, - _ => throw new ArgumentException($"Unknown test node type: {parameterizedTestNode}", nameof(parameterizedTestNode)), - }; - - return rootNode; - - // Local functions - static IEnumerable GetArguments() => new byte[] { 0, 1 }; - static Task> GetArgumentsAsync() => Task.FromResult>(new byte[] { 0, 1 }); - static IProperty[] GetProperties(bool? hasExpansionProperty) - => hasExpansionProperty.HasValue - ? - [ - new FrameworkEngineMetadataProperty - { - PreventArgumentsExpansion = hasExpansionProperty.Value, - }, - ] - : []; - } -} diff --git a/test/UnitTests/MSTest.Engine.UnitTests/BannedSymbols.txt b/test/UnitTests/MSTest.Engine.UnitTests/BannedSymbols.txt deleted file mode 100644 index ab9946cf24..0000000000 --- a/test/UnitTests/MSTest.Engine.UnitTests/BannedSymbols.txt +++ /dev/null @@ -1 +0,0 @@ -N:AwesomeAssertions; Use MSTest assertions instead. diff --git a/test/UnitTests/MSTest.Engine.UnitTests/DataRowTests.cs b/test/UnitTests/MSTest.Engine.UnitTests/DataRowTests.cs deleted file mode 100644 index fb36b9924f..0000000000 --- a/test/UnitTests/MSTest.Engine.UnitTests/DataRowTests.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Microsoft.Testing.Framework.UnitTests; - -/// -/// This class uses DataRows, to prove that running such tests works. -/// -[TestClass] -public class DataRowTests -{ - [DataRow(1, 2)] - [DataRow(2, 3)] - [TestMethod] - public void DataRowDataAreConsumed(int expected, int actualPlus1) - => Assert.AreEqual(expected, actualPlus1 - 1); -} diff --git a/test/UnitTests/MSTest.Engine.UnitTests/DynamicDataNameProviderTests.cs b/test/UnitTests/MSTest.Engine.UnitTests/DynamicDataNameProviderTests.cs deleted file mode 100644 index f6c7004659..0000000000 --- a/test/UnitTests/MSTest.Engine.UnitTests/DynamicDataNameProviderTests.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Microsoft.Testing.Framework.UnitTests; - -[TestClass] -public class DynamicDataNameProviderTests -{ - [TestMethod] - public void NullTranslatesToNullString() - { - // Comment in DynamicDataAttribute says: - // We want to force call to `data.AsEnumerable()` to ensure that objects are casted to strings (using ToString()) - // so that null do appear as "null". If you remove the call, and do string.Join(",", new object[] { null, "a" }), - // you will get empty string while with the call you will get "null,a". - // - // check that this is still true: - string fragment = DynamicDataNameProvider.GetUidFragment(["parameter1", "parameter2"], [null, "a"], 0); - Assert.AreEqual("(parameter1: null, parameter2: a)[0]", fragment); - } - - [TestMethod] - public void ParameterMismatchShowsDataInMessage() - { - // Comment in DynamicDataAttribute says: - // We want to force call to `data.AsEnumerable()` to ensure that objects are casted to strings (using ToString()) - // so that null do appear as "null". If you remove the call, and do string.Join(",", new object[] { null, "a" }), - // you will get empty string while with the call you will get "null,a". - // - // check that this is still true: - ArgumentException exception = Assert.ThrowsExactly(() => DynamicDataNameProvider.GetUidFragment(["parameter1"], [null, "a"], 0)); - Assert.AreEqual("Parameter count mismatch. The provided data (null, a) have 2 items, but there are 1 parameters.", exception.Message); - } -} diff --git a/test/UnitTests/MSTest.Engine.UnitTests/DynamicDataTests.cs b/test/UnitTests/MSTest.Engine.UnitTests/DynamicDataTests.cs deleted file mode 100644 index 7c4ba68622..0000000000 --- a/test/UnitTests/MSTest.Engine.UnitTests/DynamicDataTests.cs +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Microsoft.Testing.Framework.UnitTests; - -/// -/// This class uses DynamicData, to prove that running such tests works. -/// -[TestClass] -public class DynamicDataTests -{ - public static IEnumerable IntDataProperty - => - [ - [1, 2], - [2, 3] - ]; - - [DynamicData(nameof(IntDataProperty))] - [TestMethod] - public void DynamicDataWithIntProperty(int expected, int actualPlus1) - => Assert.AreEqual(expected, actualPlus1 - 1); - - [DynamicData(nameof(IntDataProperty))] - [TestMethod] - public void DynamicDataWithIntPropertyAndExplicitSourceType(int expected, int actualPlus1) - => Assert.AreEqual(expected, actualPlus1 - 1); - - [DynamicData(nameof(IntDataMethod))] - [TestMethod] - public void DynamicDataWithIntMethod(int expected, int actualPlus1) - => Assert.AreEqual(expected, actualPlus1 - 1); - - [DynamicData(nameof(IntDataMethod))] - [TestMethod] - public void DynamicDataWithIntMethodAndExplicitSourceType(int expected, int actualPlus1) - => Assert.AreEqual(expected, actualPlus1 - 1); - - public static IEnumerable IntDataMethod() - => - [ - [1, 2], - [2, 3] - ]; - - [DynamicData(nameof(IntDataProperty), typeof(DataClass))] - [TestMethod] - public void DynamicDataWithIntPropertyOnSeparateClass(int expected, int actualPlus2) - => Assert.AreEqual(expected, actualPlus2 - 2); - - [DynamicData(nameof(IntDataMethod), typeof(DataClass))] - [TestMethod] - public void DynamicDataWithIntMethodOnSeparateClass(int expected, int actualPlus2) - => Assert.AreEqual(expected, actualPlus2 - 2); - - [DynamicData(nameof(UserDataProperty))] - [TestMethod] - public void DynamicDataWithUserProperty(User _, User _2) - { - } - - public static IEnumerable UserDataProperty - => - [ - [new User("Jakub"), new User("Amaury")], - [new User("Marco"), new User("Pavel")] - ]; - - [DynamicData(nameof(UserDataMethod))] - [TestMethod] - public void DynamicDataWithUserMethod(User _, User _2) - { - } - - public static IEnumerable UserDataMethod() - => - [ - [new User("Jakub"), new User("Amaury")], - [new User("Marco"), new User("Pavel")] - ]; -} - -public class DataClass -{ - public static IEnumerable IntDataProperty - => - [ - [1, 3], - [2, 4] - ]; - - public static IEnumerable IntDataMethod() - => - [ - [1, 3], - [2, 4] - ]; -} - -public class User -{ - public User(string name) - { - Name = name; - } - - public string Name { get; } -} diff --git a/test/UnitTests/MSTest.Engine.UnitTests/MSTest.Engine.UnitTests.csproj b/test/UnitTests/MSTest.Engine.UnitTests/MSTest.Engine.UnitTests.csproj deleted file mode 100644 index d31bcd6ae9..0000000000 --- a/test/UnitTests/MSTest.Engine.UnitTests/MSTest.Engine.UnitTests.csproj +++ /dev/null @@ -1,39 +0,0 @@ - - - - $(SupportedNetFrameworks);net462 - Microsoft.Testing.Framework.UnitTests - true - true - true - Exe - - - - - - - - - PreserveNewest - - - - - - - - MicrosoftTestingPlatformMSBuild - - - - - - - - Analyzer - false - - - - diff --git a/test/UnitTests/MSTest.Engine.UnitTests/MSTest.Engine.UnitTests.launcher.config.json b/test/UnitTests/MSTest.Engine.UnitTests/MSTest.Engine.UnitTests.launcher.config.json deleted file mode 100644 index 772598e7f9..0000000000 --- a/test/UnitTests/MSTest.Engine.UnitTests/MSTest.Engine.UnitTests.launcher.config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "program": "MSTest.Engine.UnitTests.exe" -} diff --git a/test/UnitTests/MSTest.Engine.UnitTests/MSTest.Engine.UnitTests.testingplatformconfig.json b/test/UnitTests/MSTest.Engine.UnitTests/MSTest.Engine.UnitTests.testingplatformconfig.json deleted file mode 100644 index 4c41cda3a3..0000000000 --- a/test/UnitTests/MSTest.Engine.UnitTests/MSTest.Engine.UnitTests.testingplatformconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "testingplatform": { - "telemetry": { - "isDevelopmentRepository": true - }, - "exitProcessOnUnhandledException": true - } -} diff --git a/test/UnitTests/MSTest.Engine.UnitTests/Program.cs b/test/UnitTests/MSTest.Engine.UnitTests/Program.cs deleted file mode 100644 index de3b386a21..0000000000 --- a/test/UnitTests/MSTest.Engine.UnitTests/Program.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using Microsoft.Testing.Extensions; - -using ExecutionScope = Microsoft.VisualStudio.TestTools.UnitTesting.ExecutionScope; - -[assembly: Parallelize(Scope = ExecutionScope.MethodLevel, Workers = 0)] - -ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args); -builder.AddMSTest(() => [Assembly.GetEntryAssembly()!]); - -#if ENABLE_CODECOVERAGE -builder.AddCodeCoverageProvider(); -#endif -builder.AddHangDumpProvider(); -builder.AddCrashDumpProvider(ignoreIfNotSupported: true); -builder.AddTrxReportProvider(); -builder.AddAppInsightsTelemetryProvider(); -builder.AddAzureDevOpsProvider(); - -using ITestApplication app = await builder.BuildAsync(); -return await app.RunAsync(); diff --git a/test/UnitTests/MSTest.Engine.UnitTests/Properties/launchSettings.json b/test/UnitTests/MSTest.Engine.UnitTests/Properties/launchSettings.json deleted file mode 100644 index c916cf8fb9..0000000000 --- a/test/UnitTests/MSTest.Engine.UnitTests/Properties/launchSettings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "profiles": { - "MSTest.Engine.UnitTests": { - "commandName": "Project", - "commandLineArgs": "", - "environmentVariables": { - //"TESTINGPLATFORM_HOTRELOAD_ENABLED": "1" - } - } - } -} diff --git a/test/UnitTests/MSTest.Engine.UnitTests/TestBase.cs b/test/UnitTests/MSTest.Engine.UnitTests/TestBase.cs deleted file mode 100644 index 0e748f47ce..0000000000 --- a/test/UnitTests/MSTest.Engine.UnitTests/TestBase.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Microsoft.Testing.Framework.UnitTests; - -/// -/// Empty test base, because TestInfrastructure project depends on Testing.Framework, and we cannot use that. -/// -public abstract class TestBase; diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Generators/DataRowAttributeGenerationTests.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/Generators/DataRowAttributeGenerationTests.cs deleted file mode 100644 index 4c46d80fea..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Generators/DataRowAttributeGenerationTests.cs +++ /dev/null @@ -1,845 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using AwesomeAssertions; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; -using Microsoft.Testing.Framework.SourceGeneration.UnitTests.Helpers; -using Microsoft.Testing.Framework.SourceGeneration.UnitTests.TestUtilities; - -namespace Microsoft.Testing.Framework.SourceGeneration.UnitTests.Generators; - -[TestClass] -public sealed class DataRowAttributeGenerationTests : TestBase -{ - public TestContext TestContext { get; set; } - - [TestMethod] - public async Task DataRowAttribute_HandlesPrimitiveTypes() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [TestMethod] - [DataRow(true)] - public Task MethodWithBool(bool b) - => Task.CompletedTask; - - [TestMethod] - [DataRow(1)] - public Task MethodWithByte(byte b) - => Task.CompletedTask; - - [TestMethod] - [DataRow(1)] - public Task MethodWithSbyte(sbyte b) - => Task.CompletedTask; - - [TestMethod] - [DataRow('a')] - public Task MethodWithChar(char c) - => Task.CompletedTask; - - [TestMethod] - [DataRow(1)] - public Task MethodWithDecimal(decimal d) - => Task.CompletedTask; - - [TestMethod] - [DataRow(1)] - public Task MethodWithDouble(double d) - => Task.CompletedTask; - - [TestMethod] - [DataRow(1)] - public Task MethodWithFloat(float f) - => Task.CompletedTask; - - [TestMethod] - [DataRow(1)] - public Task MethodWithInt(int i) - => Task.CompletedTask; - - [TestMethod] - [DataRow(1)] - public Task MethodWithUInt(uint i) - => Task.CompletedTask; - - [TestMethod] - [DataRow(1)] - public Task MethodWithLong(long l) - => Task.CompletedTask; - - [TestMethod] - [DataRow(1)] - public Task MethodWithULong(ulong l) - => Task.CompletedTask; - - [TestMethod] - [DataRow(1)] - public Task MethodWithShort(short s) - => Task.CompletedTask; - - [TestMethod] - [DataRow(1)] - public Task MethodWithUShort(ushort s) - => Task.CompletedTask; - } - } - """, CancellationToken.None); - generatorResult.AssertSuccessfulGeneration(); - - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(true, "b: true"), - }, - """); - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(1, "b: 1"), - }, - """); - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(1, "b: 1"), - }, - """); - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry('a', "c: 'a'"), - }, - """); - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(1, "d: 1"), - }, - """); - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(1, "d: 1"), - }, - """); - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(1, "f: 1"), - }, - """); - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(1, "i: 1"), - }, - """); - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(1, "i: 1"), - }, - """); - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(1, "l: 1"), - }, - """); - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(1, "l: 1"), - }, - """); - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(1, "s: 1"), - }, - """); - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(1, "s: 1"), - }, - """); - } - - [TestMethod] - public async Task DataRowAttribute_WhenGivenMultipleValues_GeneratesTupleData() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [TestMethod, DataRow("a", 1)] - public Task TestMethod(string s, int i) - { - return Task.CompletedTask; - } - - [TestMethod, DataRow("a", 1, true, 1.0)] - public Task TestMethod(string s, int i, bool b, double d) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - generatorResult.AssertSuccessfulGeneration(); - - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry<(string s, int i)>[] - { - new MSTF::InternalUnsafeTestArgumentsEntry<(string s, int i)>(("a", 1), "s: \"a\", i: 1"), - }, - """); - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry<(string s, int i, bool b, double d)>[] - { - new MSTF::InternalUnsafeTestArgumentsEntry<(string s, int i, bool b, double d)>(("a", 1, true, 1), "s: \"a\", i: 1, b: true, d: 1"), - }, - """); - } - - [TestMethod] - public async Task DataRowAttribute_HandlesEscapedStrings() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [TestMethod] - [DataRow("\"abc\"")] - [DataRow(@"a\b\c")] - public Task TestMethod(string a) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - generatorResult.AssertSuccessfulGeneration(); - - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry("\"abc\"", "a: \"\"abc\"\""), - new MSTF::InternalUnsafeTestArgumentsEntry("a\\b\\c", "a: \"a\\b\\c\""), - }, - """); - } - - [TestMethod] - public async Task DataRowAttribute_WhenGivenTypeofOtherType_GeneratesDataWithFullType() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - internal class MyClass - { - - } - - [TestClass] - public class TestClass - { - [TestMethod] - [DataRow(typeof(MyClass))] - public Task TestMethod(Type a) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(typeof(MyNamespace.MyClass), "a: typeof(MyNamespace.MyClass)"), - }, - """); - } - - [TestMethod] - public async Task DataRowAttribute_WithEnumAsChildFromParentClass_GeneratesDataWithFullType() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class ParentClass - { - - public enum MyEnum - { - One, - } - - [TestClass] - public class TestClass - { - [TestMethod, DataRow(MyEnum.One)] - public Task TestMethod(MyEnum a) - { - return Task.CompletedTask; - } - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(global::MyNamespace.ParentClass.MyEnum.One, "a: global::MyNamespace.ParentClass.MyEnum.One"), - }, - """); - } - - [TestMethod] - public async Task DataRowAttribute_WithEnumAsSubNamespaceDoesNotShadowTypeFromAnotherNamespace_GeneratesDataWithFullGlobalType() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - // This gives us a way to reference to the MyEnum (1) - // in the DataRow. - using MyEnum = global::ConflictingNamespace.MyEnum; - - namespace ConflictingNamespace { - public enum MyEnum // 1 - { - One, - } - } - - namespace MyNamespace - { - namespace ConflictingNamespace { - public enum MyEnum // 2 - { - Two, - } - } - - [TestClass] - public class TestClass - { - // If the generated code from here emits just - // ConflictingNamespace.MyEnum.One, we will get an error - // saying that MyEnum does not have definition for One, - // because we are resolving the type by the relative namespace, - // and so we find MyNamespace.ConflictingNamespace.MyEnum, which is MyEnum (2). - [TestMethod, DataRow(MyEnum.One)] - public Task TestMethod(MyEnum a) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(global::ConflictingNamespace.MyEnum.One, "a: global::ConflictingNamespace.MyEnum.One"), - }, - """); - } - - [TestMethod] - public async Task DataRowAttribute_WithEnumAsSubNamespaceDoesNotShadowTypeFromAnotherNamespaceAndUsesChildType_GeneratesDataWithFullGlobalType() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace ConflictingNamespace { - public enum MyEnum // 1 - { - One, - } - } - - namespace MyNamespace - { - - namespace ConflictingNamespace { - public enum MyEnum // 2 - { - Two, - } - } - - [TestClass] - public class TestClass - { - // This refers to MyEnum (2), we should emit a full type - // with global:: into the code. - [TestMethod, DataRow(ConflictingNamespace.MyEnum.Two)] - public Task TestMethod(ConflictingNamespace.MyEnum a) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(global::MyNamespace.ConflictingNamespace.MyEnum.Two, "a: global::MyNamespace.ConflictingNamespace.MyEnum.Two"), - }, - """); - } - - [TestMethod] - public async Task DataRowAttribute_GivenNullValues_GeneratesCorrectData() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [TestMethod, DataRow(null)] - public Task TestMethod1(string a) - { - return Task.CompletedTask; - } - - [TestMethod, DataRow(null)] - public Task TestMethod1(object a) - { - return Task.CompletedTask; - } - - [TestMethod, DataRow(null, null)] - public Task TestMethod1(string s, object a) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(null, "a: null"), - }, - """); - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(null, "a: null"), - }, - """); - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry<(string s, object a)>[] - { - new MSTF::InternalUnsafeTestArgumentsEntry<(string s, object a)>((null, null), "s: null, a: null"), - }, - """); - } - - [TestMethod] - public async Task DataRowAttribute_WhenMissingAttribute_OutputsCommentAboveTheTestNodeAndFailsToCompile() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [TestMethod] - public Task TestMethod2(string a, string b) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertFailedGeneration( - "*CS0308: The non-generic type 'InternalUnsafeAsyncActionTestNode' cannot be used with type arguments*", - "*CS9035: Required member 'TestNode.StableUid' must be set in the object initializer or attribute constructor.*", - "*CS9035: Required member 'TestNode.DisplayName' must be set in the object initializer or attribute constructor.*", - "*CS9035: Required member 'InternalUnsafeAsyncActionTestNode.Body' must be set in the object initializer or attribute constructor.*"); - - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - // The test method is parameterized but no argument was specified. - // This is most often caused by using an unsupported arguments input. - // Possible resolutions: - // - There is a mismatch between arguments from [DataRow] and the method parameters. - // - There is a mismatch between arguments from [DynamicData] and the method parameters. - // If nothing else worked, report the error and exclude this method by using [Ignore]. - new MSTF::InternalUnsafeAsyncActionTestNode<(string a, string b)> - """); - } - - [TestMethod] - public async Task Arguments_WithMisalignedDataTypes_ItOutputsTheCodeAndFailsToCompile() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [TestMethod, DataRow("a", "b", "c")] - public Task TestWithTooMuchData(string a, string b) - { - return Task.CompletedTask; - } - - [TestMethod, DataRow("a", "b")] - public Task TestWithNotEnoughData(string a, string b, string c) - { - return Task.CompletedTask; - } - - [TestMethod, DataRow(1, 1)] - public Task TestWithMismatchedDataTypes(string a, string b) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertFailedGeneration( - "*error CS1503: Argument 1: cannot convert from '(string, string, string)' to '(string a, string b)'*", - "*error CS1503: Argument 1: cannot convert from '(string, string)' to '(string a, string b, string c)'*", - "*error CS1503: Argument 1: cannot convert from '(int, int)' to '(string a, string b)'*"); - } - - [TestMethod] - public async Task Arguments_GivenAnArrayOfObjects_GeneratesCorrectData() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [TestMethod, DataRow(new object[] { 1, (object)"a" })] - public Task OneObjectArray(object[] args) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(new object[] { 1, "a" }, "args: new object[] { 1, \"a\" }"), - }, - """); - } - - [TestMethod] - public async Task Arguments_GivenAnArrayOfInt_GeneratesCorrectData() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [TestMethod, DataRow(new int[] { 1, 2 })] - public Task OneIntArray(int[] args) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(new int[] { 1, 2 }, "args: new int[] { 1, 2 }"), - }, - """); - } - - [TestMethod] - public async Task Arguments_GivenAnArrayOfString_GeneratesCorrectData() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [TestMethod, DataRow(new string[] { "a", "b" })] - public Task OneStringArray(string[] args) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(new string[] { "a", "b" }, "args: new string[] { \"a\", \"b\" }"), - }, - """); - } - - [TestMethod] - public async Task Arguments_GivenMultipleArgumentsAndMethodAcceptsSingleObjectArray_GeneratesCorrectData() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [TestMethod, DataRow(1, "a")] - public Task OneParamsObjectArray2(object[] args) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(new object?[] { 1, "a" }, "args: new object?[] { 1, \"a\" }"), - }, - """); - } - - [TestMethod] - public async Task Arguments_GivenArrayOfIntWhenMethodExpectsArrayOfObjects_FailsCompilation() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [TestMethod, DataRow(new int[] { 1, 2 })] - public Task OneIntArray(object[] args) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertFailedGeneration("*error CS1503: Argument 1: cannot convert from 'int[]' to 'object[]'*"); - - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(new int[] { 1, 2 }, "args: new int[] { 1, 2 }"), - }, - """); - } - - [TestMethod] - public async Task Arguments_GivenMultipleArrays_GeneratesCorrectData() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [TestMethod, DataRow(new object[] { 1, 2 }, new object[] { "a", 1 })] - public Task TwoObjectArrays(object[] args, object[] args2) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry<(object[] args, object[] args2)>[] - { - new MSTF::InternalUnsafeTestArgumentsEntry<(object[] args, object[] args2)>((new object[] { 1, 2 }, new object[] { "a", 1 }), "args: new object[] { 1, 2 }, args2: new object[] { \"a\", 1 }"), - }, - """); - } -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Generators/DynamicDataAttributeTests.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/Generators/DynamicDataAttributeTests.cs deleted file mode 100644 index 7ec15f78af..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Generators/DynamicDataAttributeTests.cs +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using AwesomeAssertions; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; -using Microsoft.Testing.Framework.SourceGeneration.UnitTests.Helpers; -using Microsoft.Testing.Framework.SourceGeneration.UnitTests.TestUtilities; - -namespace Microsoft.Testing.Framework.SourceGeneration.UnitTests.Generators; - -[TestClass] -public sealed class DynamicDataAttributeGenerationTests : TestBase -{ - public TestContext TestContext { get; set; } - - [TestMethod] - public async Task DynamicDataAttribute_TakesDataFromProperty() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System.Threading.Tasks; - using System.Collections.Generic; - using Microsoft.VisualStudio.TestTools.UnitTesting; - using Microsoft.Testing.Framework; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [DynamicData(nameof(Data))] - [TestMethod] - public void TestMethod(int expected, int actualPlus1) - => Assert.AreEqual(expected, actualPlus1 - 1); - - public static IEnumerable Data => new[] - { - new object[] { 1, 2 }, - new object[] { 2, 3 }, - }; - } - - } - """, CancellationToken.None); - generatorResult.AssertSuccessfulGeneration(); - - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - GetArguments = static () => { - var data = MyNamespace.TestClass.Data; - var dataCollection = new ColGen.List>(); - var index = 0; - foreach (var item in data) - { - string uidFragment = MSTF::DynamicDataNameProvider.GetUidFragment(new string[] {"expected", "actualPlus1"}, item, index); - index++; - dataCollection.Add(new(((int) item[0], (int) item[1]), uidFragment)); - } - return dataCollection; - } - """); - } - - [TestMethod] - public async Task DynamicDataAttribute_TakesDataFromMethod() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System.Threading.Tasks; - using System.Collections.Generic; - using Microsoft.VisualStudio.TestTools.UnitTesting; - using Microsoft.Testing.Framework; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [DynamicData(nameof(Data), DynamicDataSourceType.Method)] - [TestMethod] - public void TestMethod(int expected, int actualPlus1) - => Assert.AreEqual(expected, actualPlus1 - 1); - - public static IEnumerable Data() => new[] - { - new object[] { 1, 2 }, - new object[] { 2, 3 }, - }; - } - - } - """, CancellationToken.None); - generatorResult.AssertSuccessfulGeneration(); - - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - GetArguments = static () => { - var data = MyNamespace.TestClass.Data(); - var dataCollection = new ColGen.List>(); - var index = 0; - foreach (var item in data) - { - string uidFragment = MSTF::DynamicDataNameProvider.GetUidFragment(new string[] {"expected", "actualPlus1"}, item, index); - index++; - dataCollection.Add(new(((int) item[0], (int) item[1]), uidFragment)); - } - return dataCollection; - } - """); - } -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Generators/IgnoreAttributeGenerationTests.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/Generators/IgnoreAttributeGenerationTests.cs deleted file mode 100644 index aacf0c8eee..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Generators/IgnoreAttributeGenerationTests.cs +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using AwesomeAssertions; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; -using Microsoft.Testing.Framework.SourceGeneration.UnitTests.Helpers; -using Microsoft.Testing.Framework.SourceGeneration.UnitTests.TestUtilities; - -namespace Microsoft.Testing.Framework.SourceGeneration.UnitTests.Generators; - -[TestClass] -public sealed class IgnoreAttributeGenerationTests : TestBase -{ - public TestContext TestContext { get; set; } - - [TestMethod] - public async Task IgnoreAttribute_OnMethodExcludesTheMethodFromCompilation() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [TestMethod] - public void TestMethod1() { } - - [TestMethod] - [Ignore] - public void IgnoredVoidMethod() { } - - [TestMethod] - [Ignore] - public Task IgnoredTaskMethod() => Task.CompletedTask; - - - [TestMethod] - [Ignore] - public ValueTask IgnoredValueTaskMethod() => ValueTask.CompletedTask; - - [TestMethod] - [Ignore("reason")] - public void IgnoredVoidMethodWithReason() { } - - [TestMethod] - [Ignore("reason")] - public Task IgnoredTaskMethodWithReason() => Task.CompletedTask; - - - [TestMethod] - [Ignore("reason")] - public ValueTask IgnoredValueTaskMethodWithReason() => ValueTask.CompletedTask; - } - } - """, CancellationToken.None); - generatorResult.AssertSuccessfulGeneration(); - - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode("""StableUid = "TestAssembly.MyNamespace.TestClass.TestMethod1()","""); - - testClass.Should().NotContain("Ignored", "because none of the ignored methods should be output."); - } -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Generators/StaticMethodGenerationTests.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/Generators/StaticMethodGenerationTests.cs deleted file mode 100644 index 42107642a3..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Generators/StaticMethodGenerationTests.cs +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using AwesomeAssertions; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; -using Microsoft.Testing.Framework.SourceGeneration.UnitTests.Helpers; -using Microsoft.Testing.Framework.SourceGeneration.UnitTests.TestUtilities; - -namespace Microsoft.Testing.Framework.SourceGeneration.UnitTests.Generators; - -[TestClass] -public sealed class StaticMethodGenerationTests -{ - public TestContext TestContext { get; set; } - - [TestMethod] - public async Task StaticMethods_StaticMethodsWontGenerateTests() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [TestMethod] - public void TestMethod1() { } - - [TestMethod] - public static void StaticTestMethod() { } - - [TestMethod] - public static Task StaticTaskMethod() => Task.CompletedTask; - - - [TestMethod] - public static ValueTask StaticValueTaskMethod() => ValueTask.CompletedTask; - } - } - """, CancellationToken.None); - generatorResult.AssertSuccessfulGeneration(); - - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode("""StableUid = "TestAssembly.MyNamespace.TestClass.TestMethod1()","""); - - testClass.Should().NotContain("Static", "because none of the static methods should be output."); - } -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Generators/TestNodesGeneratorTests.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/Generators/TestNodesGeneratorTests.cs deleted file mode 100644 index 6ceb1a9ed4..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Generators/TestNodesGeneratorTests.cs +++ /dev/null @@ -1,1230 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using AwesomeAssertions; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; -using Microsoft.Testing.Framework.SourceGeneration.UnitTests.Helpers; -using Microsoft.Testing.Framework.SourceGeneration.UnitTests.TestUtilities; - -namespace Microsoft.Testing.Framework.SourceGeneration.UnitTests.Generators; - -[TestClass] -public sealed class TestNodesGeneratorTests : TestBase -{ - public TestContext TestContext { get; set; } - - [DataRow("class", "public")] - [DataRow("class", "internal")] - [DataRow("record", "public")] - [DataRow("record", "internal")] - [DataRow("record class", "public")] - [DataRow("record class", "internal")] - [TestMethod] - public async Task When_TypeIsMarkedWithTestClass_ItGeneratesAGraphWithAssemblyNamespaceTypeAndMethod(string typeKind, string accessibility) - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - $$""" - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - {{accessibility}} {{typeKind}} MyType - { - [TestMethod] - public Task TestMethod() - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - generatorResult.GeneratedTrees.Should().HaveCount(3); - - SourceText myTypeSource = await generatorResult.RunResult.GeneratedTrees[0].GetTextAsync(TestContext.CancellationToken); - myTypeSource.Should().ContainSourceCode(""" - public static readonly MSTF::TestNode TestNode = new MSTF::TestNode - { - StableUid = "TestAssembly.MyNamespace.MyType", - DisplayName = "MyType", - Properties = new Msg::IProperty[1] - { - new Msg::TestFileLocationProperty(@"", new(new(6, -1), new(14, -1))), - }, - Tests = new MSTF::TestNode[] - { - new MSTF::InternalUnsafeAsyncActionTestNode - { - StableUid = "TestAssembly.MyNamespace.MyType.TestMethod()", - DisplayName = "TestMethod", - Properties = new Msg::IProperty[2] - { - new Msg::TestMethodIdentifierProperty( - "TestAssembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", - "MyNamespace", - "MyType", - "TestMethod", - 0, - Sys::Array.Empty(), - "System.Threading.Tasks.Task"), - new Msg::TestFileLocationProperty(@"", new(new(9, -1), new(13, -1))), - }, - Body = static async testExecutionContext => - { - var instance = new MyType(); - try - { - await instance.TestMethod(); - } - catch (global::System.Exception ex) - { - testExecutionContext.ReportException(ex, null); - } - }, - }, - }, - }; - """); - - SourceText rootSource = await generatorResult.RunResult.GeneratedTrees[1].GetTextAsync(TestContext.CancellationToken); - rootSource.Should().ContainSourceCode(""" - MSTF::TestNode root = new MSTF::TestNode - { - StableUid = "TestAssembly", - DisplayName = "TestAssembly", - Properties = Sys::Array.Empty(), - Tests = new MSTF::TestNode[] - { - new MSTF::TestNode - { - StableUid = "TestAssembly.MyNamespace", - DisplayName = "MyNamespace", - Properties = Sys::Array.Empty(), - Tests = namespace1Tests.ToArray(), - }, - }, - }; - """); - } - - [TestMethod] - public async Task When_TypeInheritsABaseClassAndIsParameterless_ItGeneratesATestNode() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - $$""" - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - public class MyBaseClass - { - public MyBaseClass(string s) { } - } - - [TestClass] - public class MyType : MyBaseClass - { - public MyType() - : base("hello") - { - } - - [TestMethod] - public Task TestMethod() - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - generatorResult.GeneratedTrees.Should().HaveCount(3); - } - - [TestMethod] - public async Task When_TypeInheritsAnAbstractBaseClassAndIsParameterless_ItGeneratesATestNode() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - $$""" - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - public abstract class MyBaseClass - { - public MyBaseClass(string s) { } - } - - [TestClass] - public class MyType : MyBaseClass - { - public MyType() - : base("hello") - { - } - - [TestMethod] - public Task TestMethod() - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - generatorResult.GeneratedTrees.Should().HaveCount(3); - } - - [DataRow(false)] - [DataRow(true)] - [TestMethod] - public async Task When_TypeInheritsABaseClassWithSomeTestMethodsButBaseIsNotTestClass_OnlyOneTestNodeTypeIsGenerated(bool isBaseClassAbstract) - { - string classModifier = isBaseClassAbstract ? "abstract " : string.Empty; - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - $$""" - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - public {{classModifier}}class MyBaseClass - { - public MyBaseClass(string s) { } - - [TestMethod] - public void MyTestMethod() { } - } - - [TestClass] - public class MyType : MyBaseClass - { - public MyType() - : base("hello") - { - } - - [TestMethod] - public Task TestMethod() - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - generatorResult.GeneratedTrees.Should().HaveCount(3); - - SourceText myTypeSource = await generatorResult.RunResult.GeneratedTrees[0].GetTextAsync(TestContext.CancellationToken); - myTypeSource.Should().ContainSourceCode(""" - public static readonly MSTF::TestNode TestNode = new MSTF::TestNode - { - StableUid = "TestAssembly.MyNamespace.MyType", - DisplayName = "MyType", - Properties = new Msg::IProperty[1] - { - new Msg::TestFileLocationProperty(@"", new(new(14, -1), new(27, -1))), - }, - Tests = new MSTF::TestNode[] - { - new MSTF::InternalUnsafeAsyncActionTestNode - { - StableUid = "TestAssembly.MyNamespace.MyType.TestMethod()", - DisplayName = "TestMethod", - Properties = new Msg::IProperty[2] - { - new Msg::TestMethodIdentifierProperty( - "TestAssembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", - "MyNamespace", - "MyType", - "TestMethod", - 0, - Sys::Array.Empty(), - "System.Threading.Tasks.Task"), - new Msg::TestFileLocationProperty(@"", new(new(22, -1), new(26, -1))), - }, - Body = static async testExecutionContext => - { - var instance = new MyType(); - try - { - await instance.TestMethod(); - } - catch (global::System.Exception ex) - { - testExecutionContext.ReportException(ex, null); - } - }, - }, - new MSTF::InternalUnsafeActionTestNode - { - StableUid = "TestAssembly.MyNamespace.MyType.MyTestMethod()", - DisplayName = "MyTestMethod", - Properties = new Msg::IProperty[2] - { - new Msg::TestMethodIdentifierProperty( - "TestAssembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", - "MyNamespace", - "MyBaseClass", - "MyTestMethod", - 0, - Sys::Array.Empty(), - "System.Void"), - new Msg::TestFileLocationProperty(@"", new(new(10, -1), new(11, -1))), - }, - Body = static testExecutionContext => - { - var instance = new MyType(); - try - { - instance.MyTestMethod(); - } - catch (global::System.Exception ex) - { - testExecutionContext.ReportException(ex, null); - } - }, - }, - }, - }; - """); - } - - [TestMethod] - public async Task When_TypeInheritsABaseTestClassMarkedAsTestClassWithSomeTestMethods_TwoTestNodeTypesAreGenerated() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - $$""" - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class MyBaseClass - { - [TestMethod] - public void MyTestMethod() { } - } - - [TestClass] - public class MyType : MyBaseClass - { - [TestMethod] - public Task TestMethod() - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - generatorResult.GeneratedTrees.Should().HaveCount(4); - - SourceText myBaseClassSource = await generatorResult.GeneratedTrees[0].GetTextAsync(TestContext.CancellationToken); - myBaseClassSource.Should().ContainSourceCode(""" - public static readonly MSTF::TestNode TestNode = new MSTF::TestNode - { - StableUid = "TestAssembly.MyNamespace.MyBaseClass", - DisplayName = "MyBaseClass", - Properties = new Msg::IProperty[1] - { - new Msg::TestFileLocationProperty(@"", new(new(6, -1), new(11, -1))), - }, - Tests = new MSTF::TestNode[] - { - new MSTF::InternalUnsafeActionTestNode - { - StableUid = "TestAssembly.MyNamespace.MyBaseClass.MyTestMethod()", - DisplayName = "MyTestMethod", - Properties = new Msg::IProperty[2] - { - new Msg::TestMethodIdentifierProperty( - "TestAssembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", - "MyNamespace", - "MyBaseClass", - "MyTestMethod", - 0, - Sys::Array.Empty(), - "System.Void"), - new Msg::TestFileLocationProperty(@"", new(new(9, -1), new(10, -1))), - }, - Body = static testExecutionContext => - { - var instance = new MyBaseClass(); - try - { - instance.MyTestMethod(); - } - catch (global::System.Exception ex) - { - testExecutionContext.ReportException(ex, null); - } - }, - }, - }, - }; - """); - - SourceText myTypeSource = await generatorResult.GeneratedTrees[1].GetTextAsync(TestContext.CancellationToken); - myTypeSource.Should().ContainSourceCode(""" - public static readonly MSTF::TestNode TestNode = new MSTF::TestNode - { - StableUid = "TestAssembly.MyNamespace.MyType", - DisplayName = "MyType", - Properties = new Msg::IProperty[1] - { - new Msg::TestFileLocationProperty(@"", new(new(13, -1), new(21, -1))), - }, - Tests = new MSTF::TestNode[] - { - new MSTF::InternalUnsafeAsyncActionTestNode - { - StableUid = "TestAssembly.MyNamespace.MyType.TestMethod()", - DisplayName = "TestMethod", - Properties = new Msg::IProperty[2] - { - new Msg::TestMethodIdentifierProperty( - "TestAssembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", - "MyNamespace", - "MyType", - "TestMethod", - 0, - Sys::Array.Empty(), - "System.Threading.Tasks.Task"), - new Msg::TestFileLocationProperty(@"", new(new(16, -1), new(20, -1))), - }, - Body = static async testExecutionContext => - { - var instance = new MyType(); - try - { - await instance.TestMethod(); - } - catch (global::System.Exception ex) - { - testExecutionContext.ReportException(ex, null); - } - }, - }, - new MSTF::InternalUnsafeActionTestNode - { - StableUid = "TestAssembly.MyNamespace.MyType.MyTestMethod()", - DisplayName = "MyTestMethod", - Properties = new Msg::IProperty[2] - { - new Msg::TestMethodIdentifierProperty( - "TestAssembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", - "MyNamespace", - "MyBaseClass", - "MyTestMethod", - 0, - Sys::Array.Empty(), - "System.Void"), - new Msg::TestFileLocationProperty(@"", new(new(9, -1), new(10, -1))), - }, - Body = static testExecutionContext => - { - var instance = new MyType(); - try - { - instance.MyTestMethod(); - } - catch (global::System.Exception ex) - { - testExecutionContext.ReportException(ex, null); - } - }, - }, - }, - }; - """); - } - - [TestMethod] - public async Task When_TestClassHasAliasForTestNode_ItWontConflictWithIdentifier() - { - // When class has alias for TestNode we should not see conflict in the compiled code. When this is not working correctly - // e.g. when in the class definition you use just TestNode TestNode for the test node property you will see - // "Namespace 'Microsoft.Testing.Framework' contains a definition conflicting with alias 'TestNode', but found False." - // compilation error. - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - using static System.ConsoleColor; - using TestNode = A.TestNode; - - namespace A - { - public class TestNode - { - } - } - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [TestMethod] - public Task TestMethod() - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - generatorResult.RunResult.GeneratedTrees.Should().HaveCount(3); - - SourceText testClass = await generatorResult.GeneratedTrees[0].GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode( - "public static readonly MSTF::TestNode TestNode = new MSTF::TestNode", - "because using short name for TestNode type would conflict with the type alias"); - } - - [TestMethod] - public async Task When_TestClassIsNested_ItGeneratesNodesForIt() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [TestClass] - public class TestSubClass - { - [TestMethod] - public Task TestMethod1() - { - return Task.CompletedTask; - } - } - - [TestMethod] - public Task TestMethod2() - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - generatorResult.RunResult.GeneratedTrees.Should().HaveCount(4); - - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestSubClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - testClass.Should().ContainSourceCode(""" - new MSTF::InternalUnsafeAsyncActionTestNode - { - StableUid = "TestAssembly.MyNamespace.TestClass.TestSubClass.TestMethod1()", - DisplayName = "TestMethod1", - Properties = new Msg::IProperty[2] - { - new Msg::TestMethodIdentifierProperty( - "TestAssembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", - "MyNamespace", - "TestClass.TestSubClass", - "TestMethod1", - 0, - Sys::Array.Empty(), - "System.Threading.Tasks.Task"), - new Msg::TestFileLocationProperty(@"", new(new(12, -1), new(16, -1))), - }, - Body = static async testExecutionContext => - { - var instance = new TestClass.TestSubClass(); - try - { - await instance.TestMethod1(); - } - catch (global::System.Exception ex) - { - testExecutionContext.ReportException(ex, null); - } - }, - }, - """); - } - - [TestMethod] - public async Task When_TypeIsInGlobalNamespace_ItGeneratesATestNode() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - $$""" - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - [TestClass] - public class MyType - { - [TestMethod] - public Task TestMethod() - { - return Task.CompletedTask; - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - generatorResult.RunResult.GeneratedTrees.Should().HaveCount(3); - - SourceText myTypeSource = await generatorResult.GeneratedTrees[0].GetTextAsync(TestContext.CancellationToken); - myTypeSource.Should().ContainSourceCode(""" - public static readonly MSTF::TestNode TestNode = new MSTF::TestNode - { - StableUid = "TestAssembly.MyType", - DisplayName = "MyType", - Properties = new Msg::IProperty[1] - { - new Msg::TestFileLocationProperty(@"", new(new(4, -1), new(12, -1))), - }, - """); - - SourceText rootSource = await generatorResult.GeneratedTrees[1].GetTextAsync(TestContext.CancellationToken); - rootSource.Should().ContainSourceCode(""" - MSTF::TestNode root = new MSTF::TestNode - { - StableUid = "TestAssembly", - DisplayName = "TestAssembly", - Properties = Sys::Array.Empty(), - Tests = new MSTF::TestNode[] - { - new MSTF::TestNode - { - StableUid = "TestAssembly.", - DisplayName = "", - Properties = Sys::Array.Empty(), - Tests = namespace1Tests.ToArray(), - }, - }, - }; - """); - } - - [TestMethod] - public async Task When_MultipleClassesFromSameNamespace_ItGeneratesASingleNamespaceTestNode() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - [ - $$""" - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class MyType1 - { - [TestMethod] - public Task TestMethod() - { - return Task.CompletedTask; - } - } - } - """, - $$""" - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class MyType2 - { - [TestMethod] - public Task TestMethod() - { - return Task.CompletedTask; - } - } - } - """ - ], CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - generatorResult.GeneratedTrees.Should().HaveCount(4); - - SourceText rootSource = await generatorResult.RunResult.GeneratedTrees[2].GetTextAsync(TestContext.CancellationToken); - rootSource.Should().ContainSourceCode(""" - ColGen::List namespace1Tests = new(); - namespace1Tests.Add(MyNamespace_MyType1.TestNode); - namespace1Tests.Add(MyNamespace_MyType2.TestNode); - - MSTF::TestNode root = new MSTF::TestNode - { - StableUid = "TestAssembly", - DisplayName = "TestAssembly", - Properties = Sys::Array.Empty(), - Tests = new MSTF::TestNode[] - { - new MSTF::TestNode - { - StableUid = "TestAssembly.MyNamespace", - DisplayName = "MyNamespace", - Properties = Sys::Array.Empty(), - Tests = namespace1Tests.ToArray(), - }, - }, - }; - """); - } - - [DataRow("class")] - [DataRow("struct")] - [DataRow("record")] - [DataRow("record struct")] - [DataRow("record class")] - [TestMethod] - [Ignore("Initialize is not supported yet.")] - public async Task When_TypeIsIAsyncInitializable_GeneratedTestNodeIsAsExpected(string typeKind) - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - $$""" - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public {{typeKind}} MyType : IAsyncInitializable - { - [TestMethod] - public Task TestMethod() - { - return Task.CompletedTask; - } - - [TestInitialize] - public Task InitializeAsync(InitializationContext context) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - generatorResult.GeneratedTrees.Should().HaveCount(2); - - SourceText myTypeSource = await generatorResult.RunResult.GeneratedTrees[0].GetTextAsync(TestContext.CancellationToken); - - // The test node for the type should not have a test node for the InitializeAsync method. - myTypeSource.Should().NotContain("StableUid = \"TestAssembly.MyNamespace.MyType.InitializeAsync\""); - - // The body of the test node for the method should call InitializeAsync before calling the test method. - myTypeSource.Should().ContainSourceCode(""" - Body = static async testExecutionContext => - { - var instance = new MyType(); - try - { - await instance.InitializeAsync(new MSTF::InitializationContext(testExecutionContext.CancellationToken)); - await instance.TestMethod(); - } - catch (global::System.Exception ex) - { - testExecutionContext.ReportException(ex, null); - } - }, - """); - } - - [DataRow("class")] - [DataRow("struct")] - [DataRow("record")] - [DataRow("record struct")] - [DataRow("record class")] - [TestMethod] - [Ignore("Initialize is not supported yet.")] - public async Task When_TypeIsIAsyncCleanable_GeneratedTestNodeIsAsExpected(string typeKind) - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - $$""" - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public {{typeKind}} MyType : IAsyncCleanable - { - public Task TestMethod() - { - return Task.CompletedTask; - } - - public Task CleanupAsync(CleanupContext context) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - generatorResult.GeneratedTrees.Should().HaveCount(2); - - SourceText myTypeSource = await generatorResult.RunResult.GeneratedTrees[0].GetTextAsync(TestContext.CancellationToken); - - // The test node for the type should not have a test node for the CleanupAsync method. - myTypeSource.Should().NotContain("StableUid = \"TestAssembly.MyNamespace.MyType.CleanupAsync\""); - - // The body of the test node for the method should call CleanupAsync after calling the test method. - myTypeSource.Should().ContainSourceCode(""" - Body = static async testExecutionContext => - { - var instance = new MyType(); - try - { - await instance.TestMethod(); - } - catch (global::System.Exception ex) - { - testExecutionContext.ReportException(ex, null); - } - try - { - await instance.CleanupAsync(new MSTF::CleanupContext(testExecutionContext.CancellationToken)); - } - catch (global::System.Exception ex) - { - testExecutionContext.ReportException(ex, null); - } - }, - """); - } - - [DataRow("class")] - [DataRow("struct")] - [DataRow("record")] - [DataRow("record struct")] - [DataRow("record class")] - [TestMethod] - [Ignore("Initialize is not supported yet.")] - public async Task When_TypeIsIDisposable_GeneratedTestNodeIsAsExpected(string typeKind) - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - $$""" - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public {{typeKind}} MyType : IDisposable - { - public Task TestMethod() - { - return Task.CompletedTask; - } - - public void Dispose() - { - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - generatorResult.GeneratedTrees.Should().HaveCount(2); - - SourceText myTypeSource = await generatorResult.RunResult.GeneratedTrees[0].GetTextAsync(TestContext.CancellationToken); - - // The test node for the type should not have a test node for the Dispose method. - myTypeSource.Should().NotContain("StableUid = \"TestAssembly.MyNamespace.MyType.Dispose\""); - - // The body of the test node for the method should call Dispose after calling the test method. - myTypeSource.Should().ContainSourceCode(""" - Body = static async testExecutionContext => - { - using var instance = new MyType(); - try - { - await instance.TestMethod(); - } - catch (global::System.Exception ex) - { - testExecutionContext.ReportException(ex, null); - } - }, - """); - } - - [DataRow("class")] - [DataRow("struct")] - [DataRow("record")] - [DataRow("record struct")] - [DataRow("record class")] - [TestMethod] - [Ignore("Initialize is not supported yet.")] - public async Task When_TypeIsIAsyncDisposable_GeneratedTestNodeIsAsExpected(string typeKind) - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - $$""" - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public {{typeKind}} MyType : IAsyncDisposable - { - public Task TestMethod() - { - return Task.CompletedTask; - } - - public ValueTask DisposeAsync() - { - return ValueTask.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - generatorResult.GeneratedTrees.Should().HaveCount(2); - - SourceText myTypeSource = await generatorResult.RunResult.GeneratedTrees[0].GetTextAsync(TestContext.CancellationToken); - - // The test node for the type should not have a test node for the DisposeAsync method. - myTypeSource.Should().NotContain("StableUid = \"TestAssembly.MyNamespace.MyType.DisposeAsync\""); - - // The body of the test node for the method should call DisposeAsync after calling the test method. - myTypeSource.Should().ContainSourceCode(""" - Body = static async testExecutionContext => - { - await using var instance = new MyType(); - try - { - await instance.TestMethod(); - } - catch (global::System.Exception ex) - { - testExecutionContext.ReportException(ex, null); - } - }, - """); - } - - [DataRow("class")] - [DataRow("struct")] - [DataRow("record")] - [DataRow("record struct")] - [DataRow("record class")] - [TestMethod] - [Ignore("Initialize is not supported yet.")] - public async Task When_TypeIsIAsyncDisposableAndIDisposable_GeneratedTestNodeIsAsExpected(string typeKind) - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - $$""" - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public {{typeKind}} MyType : IAsyncDisposable, IDisposable - { - public Task TestMethod() - { - return Task.CompletedTask; - } - - public ValueTask DisposeAsync() - { - return ValueTask.CompletedTask; - } - - public void Dispose() - { - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - generatorResult.GeneratedTrees.Should().HaveCount(2); - - SourceText myTypeSource = await generatorResult.RunResult.GeneratedTrees[0].GetTextAsync(TestContext.CancellationToken); - - // The body of the test node for the method should call only DisposeAsync after calling the test method. - myTypeSource.Should().ContainSourceCode(""" - Body = static async testExecutionContext => - { - await using var instance = new MyType(); - try - { - await instance.TestMethod(); - } - catch (global::System.Exception ex) - { - testExecutionContext.ReportException(ex, null); - } - }, - """); - } - - [TestMethod] - [Ignore("Initialize is not supported yet.")] - public async Task When_MethodIsNotAsyncButTypeUsesAsync_GeneratedTestNodeIsAsync() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - $$""" - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class MyClass1 : IAsyncDisposable - { - public void TestMethod() - { - } - - public ValueTask DisposeAsync() - { - return ValueTask.CompletedTask; - } - } - - [TestClass] - public class MyClass2 : IAsyncInitializable - { - public void TestMethod() - { - } - - public Task InitializeAsync(InitializationContext context) - { - return Task.CompletedTask; - } - } - - [TestClass] - public class MyClass3 : IAsyncCleanable - { - public void TestMethod() - { - } - - public Task CleanupAsync(CleanupContext context) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - generatorResult.GeneratedTrees.Should().HaveCount(4); - - SourceText myClass1Source = await generatorResult.RunResult.GeneratedTrees[0].GetTextAsync(TestContext.CancellationToken); - myClass1Source.Should().ContainSourceCode(""" - new MSTF::InternalUnsafeAsyncActionTestNode - { - StableUid = "TestAssembly.MyNamespace.MyClass1.TestMethod()", - """); - - SourceText myClass2Source = await generatorResult.RunResult.GeneratedTrees[1].GetTextAsync(TestContext.CancellationToken); - myClass2Source.Should().ContainSourceCode(""" - new MSTF::InternalUnsafeAsyncActionTestNode - { - StableUid = "TestAssembly.MyNamespace.MyClass2.TestMethod()", - """); - - SourceText myClass3Source = await generatorResult.RunResult.GeneratedTrees[2].GetTextAsync(TestContext.CancellationToken); - myClass3Source.Should().ContainSourceCode(""" - new MSTF::InternalUnsafeAsyncActionTestNode - { - StableUid = "TestAssembly.MyNamespace.MyClass3.TestMethod()", - """); - } - - [TestMethod] - public async Task When_MethodIsObsolete_WrapMethodCallWithPragma() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - $$""" - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class MyType - { - [Obsolete] - [TestMethod] - public Task TestMethod1() - { - return Task.CompletedTask; - } - - [Obsolete("This is obsolete with message")] - [TestMethod] - public Task TestMethod2() - { - return Task.CompletedTask; - } - - [Obsolete("This is obsolete with message", false)] - [TestMethod] - public Task TestMethod3() - { - return Task.CompletedTask; - } - - [Obsolete("This is obsolete with message", true)] - [TestMethod] - public Task TestMethod4() - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertFailedGeneration("*error CS0619: 'MyType.TestMethod4()' is obsolete*"); - generatorResult.GeneratedTrees.Should().HaveCount(3); - - SourceText myTypeSource = await generatorResult.RunResult.GeneratedTrees[0].GetTextAsync(TestContext.CancellationToken); - myTypeSource.Should().ContainSourceCode(""" - #pragma warning disable CS0612 // Type or member is obsolete - await instance.TestMethod1(); - #pragma warning restore CS0612 // Type or member is obsolete - """); - - myTypeSource.Should().ContainSourceCode(""" - #pragma warning disable CS0618 // Type or member is obsolete - await instance.TestMethod2(); - #pragma warning restore CS0618 // Type or member is obsolete - """); - - myTypeSource.Should().ContainSourceCode(""" - #pragma warning disable CS0618 // Type or member is obsolete - await instance.TestMethod3(); - #pragma warning restore CS0618 // Type or member is obsolete - """); - } - - [DataRow("1ab", "_1ab")] - [DataRow("a-b", "a_b")] - [DataRow("a.", "a_")] - [DataRow("!@#$%^&*()_+-=", "______________")] - [TestMethod] - - // Disabled for potential bug in templating (where escaping code is copied from) - // https://github.com/dotnet/templating/issues/7200 - // [DataRow("a..b", "a..b")] - // [DataRow("a...b", "a...b")] - public void GenerateValidNamespaceName_WithGivenAssemblyName_ReturnsExpectedNamespaceName(string assemblyName, string expectedNamespaceName) - => Assert.AreEqual(expectedNamespaceName, TestNodesGenerator.ToSafeNamespace(assemblyName)); - - [TestMethod] - public async Task When_APartialTypeIsMarkedWithTestClass_ItGeneratesAGraphWithAssemblyNamespaceTypeAndMethods() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - [ - $$""" - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public partial class MyType - { - public MyType(int a) { } - - [TestMethod] - public Task TestMethod1() - { - return Task.CompletedTask; - } - } - } - """, - $$""" - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - // Defining [TestClass] twice would fail - // the source gen with Duplicate source MyNamespace.MyType.g.cs - // but if we fix that problem it will subsequently fail with - // duplicate attribute [TestClass]. - public partial class MyType - { - public MyType() {} - - [TestMethod] - public Task TestMethod2() - { - return Task.CompletedTask; - } - } - } - """ - ], CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - generatorResult.GeneratedTrees.Should().HaveCount(3); - - SourceText myTypeSource = await generatorResult.RunResult.GeneratedTrees[0].GetTextAsync(TestContext.CancellationToken); - myTypeSource.Should().ContainSourceCode(""" - public static class MyNamespace_MyType - { - public static readonly MSTF::TestNode TestNode = new MSTF::TestNode - { - StableUid = "TestAssembly.MyNamespace.MyType", - DisplayName = "MyType", - Properties = new Msg::IProperty[2] - { - new Msg::TestFileLocationProperty(@"", new(new(6, -1), new(16, -1))), - new Msg::TestFileLocationProperty(@"", new(new(10, -1), new(19, -1))), - }, - Tests = new MSTF::TestNode[] - { - """); - - myTypeSource.Should().ContainSourceCode(""" - StableUid = "TestAssembly.MyNamespace.MyType.TestMethod1()", - """); - - myTypeSource.Should().ContainSourceCode(""" - StableUid = "TestAssembly.MyNamespace.MyType.TestMethod2()", - """); - } -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/ConstantsTests.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/ConstantsTests.cs deleted file mode 100644 index 1944f5b664..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/ConstantsTests.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using AwesomeAssertions; - -namespace Microsoft.Testing.Framework.SourceGeneration.UnitTests; - -[TestClass] -public class ConstantsTests : TestBase -{ - [TestMethod] - public void NewLine_IsWindowsLineReturn() => Constants.NewLine.Should().Be("\r\n"); -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/GeneratorCompilationResultHelpers.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/GeneratorCompilationResultHelpers.cs deleted file mode 100644 index e9c4db7ff9..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/GeneratorCompilationResultHelpers.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using AwesomeAssertions; - -using Microsoft.CodeAnalysis; -using Microsoft.Testing.Framework.SourceGeneration.UnitTests.Helpers; -using Microsoft.Testing.Framework.SourceGeneration.UnitTests.TestUtilities; - -namespace Microsoft.Testing.Framework.SourceGeneration.UnitTests.Helpers; - -internal static class GeneratorCompilationResultHelpers -{ - public static GeneratorCompilationResult AssertSuccessfulGeneration(this GeneratorCompilationResult result) - { - result.EmitResult.Success.Should().BeTrue("compilation should have been successful.\n" - + $"Diagnostics: {result.EmitResult.Diagnostics.Length}\n" - + $"Code:\n{result.FailingGeneratedCode}"); - result.TimingInfo.ElapsedTime.Should().BeGreaterThan(TimeSpan.Zero); - result.EmitResult.Diagnostics.Where(d => d.Severity is DiagnosticSeverity.Warning or DiagnosticSeverity.Error).Should().BeEmpty(); - result.RunResult.Diagnostics.Should().BeEmpty(); - return result; - } - - public static GeneratorCompilationResult AssertFailedGeneration(this GeneratorCompilationResult result, params string[] diagnostics) - { - result.EmitResult.Success.Should().BeFalse(); - result.TimingInfo.ElapsedTime.Should().BeGreaterThan(TimeSpan.Zero); - result.RunResult.Diagnostics.Should().BeEmpty(); - result.EmitResult.Diagnostics.Should().HaveSameCount(diagnostics); - - for (int i = 0; i < diagnostics.Length; i++) - { - result.EmitResult.Diagnostics.Select(d => d.ToString()).Should().ContainMatch(diagnostics[i]); - } - - return result; - } -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/MinimalTestRunner.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/MinimalTestRunner.cs deleted file mode 100644 index 5537de1ad2..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/MinimalTestRunner.cs +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -/// -/// Discovers and runs tests using the MSTest attributes, so we can run tests even when we completely break or delete the real MSTest engine. -/// -internal sealed class MinimalTestRunner -{ - public static async Task RunAllAsync(string? testNameContainsFilter = null) - { -#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code - IEnumerable classes = Assembly.GetExecutingAssembly().GetTypes().Where(c => c.IsPublic); -#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code - object[][] emptyRow = [[]]; - - int total = 0; - int failed = 0; - int passed = 0; - foreach (Type? c in classes) - { - IList attributes = c.GetCustomAttributesData(); - - if (!attributes.Any(a => a.AttributeType == typeof(TestClassAttribute))) - { - continue; - } - - if (attributes.Any(a => a.AttributeType == typeof(IgnoreAttribute))) - { - Console.WriteLine($"Class {c.Name} is ignored."); - continue; - } - -#pragma warning disable IL2075 // 'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations. - foreach (MethodInfo m in c.GetMethods()) - { - if (!string.IsNullOrWhiteSpace(testNameContainsFilter)) - { -#pragma warning disable CA1304 // Specify CultureInfo -#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons -#pragma warning disable CA1311 // Specify a culture or use an invariant version - if (!m.Name!.ToLower().Contains(testNameContainsFilter.ToLower())) - { - continue; - } -#pragma warning restore CA1311 // Specify a culture or use an invariant version -#pragma warning restore CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons -#pragma warning restore CA1304 // Specify CultureInfo - } - - IList methodAttributes = m.GetCustomAttributesData(); - if (!methodAttributes.Any(a => a.AttributeType == typeof(TestMethodAttribute))) - { - continue; - } - - if (methodAttributes.Any(a => a.AttributeType == typeof(IgnoreAttribute))) - { - Console.WriteLine($"Method {c.Name} is ignored."); - continue; - } - - object?[][]? rows = null; - if (methodAttributes.Any(a => a.AttributeType == typeof(DataRowAttribute))) - { - rows = [.. methodAttributes - .Where(a => a.AttributeType == typeof(DataRowAttribute)) - .SelectMany(a => a.ConstructorArguments.Select(arg => - { - // An object that represents the value of the argument or element, or a generic ReadOnlyCollection of CustomAttributeTypedArgument objects that represent the values of an array-type argument. - // https://learn.microsoft.com/en-us/dotnet/api/system.reflection.customattributetypedargument.value?view=net-8.0#property-value -#pragma warning disable IDE0046 // Convert to conditional expression - if (arg.Value is IReadOnlyCollection argumentCollection) - { - return argumentCollection.Select(argv => argv.Value).ToArray(); - } - else - { - return [arg.Value]; - } -#pragma warning restore IDE0046 // Convert to conditional expression - }))]; - } - - foreach (object?[]? row in rows ?? emptyRow) - { - ConsoleColor fg = Console.ForegroundColor; - try - { - total++; -#pragma warning disable IL2072 // Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations. - object? classInstance = Activator.CreateInstance(c); -#pragma warning restore IL2072 // Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations. - object? result = m.Invoke(classInstance, row); - if (result is Task task) - { - await task; - } - else if (result is ValueTask valueTask) - { - await valueTask; - } - - passed++; - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine($"Passed {c.Name}.{m.Name}"); - } - catch (TargetInvocationException ex) - { - failed++; - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"Failed {c.Name}.{m.Name}:\n{ex.InnerException}\n{ex.InnerException!.StackTrace}\n"); - } - catch (Exception ex) - { - failed++; - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"Failed {c.Name}.{m.Name}:\n{ex}\n{ex.StackTrace}\n"); - } - finally - { - Console.ForegroundColor = fg; - } - } - } -#pragma warning restore IL2075 // 'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations. - } - - Console.WriteLine($"{(failed != 0 ? "failed" : "passed")}! - failed: {failed}, passed: {passed}, total: {total}"); - - return failed == 0 ? 0 : 1; - } -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/SourceCodeAssertionExtensions.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/SourceCodeAssertionExtensions.cs deleted file mode 100644 index e2c6014a6b..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/SourceCodeAssertionExtensions.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using AwesomeAssertions.Execution; - -using Microsoft.CodeAnalysis.Text; - -namespace Microsoft.Testing.Framework.SourceGeneration.UnitTests.Helpers; - -internal static class SourceCodeAssertionExtensions -{ - public static SourceCodeAssertions Should(this SourceText sourceText) => new(sourceText.ToString(), AssertionChain.GetOrCreate()); -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/SourceCodeAssertions.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/SourceCodeAssertions.cs deleted file mode 100644 index 6d24195427..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/SourceCodeAssertions.cs +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using AwesomeAssertions; -using AwesomeAssertions.Execution; -using AwesomeAssertions.Primitives; - -namespace Microsoft.Testing.Framework.SourceGeneration.UnitTests.Helpers; - -internal sealed class SourceCodeAssertions : StringAssertions -{ - public SourceCodeAssertions(string value, AssertionChain assertionChain) - : base(value, assertionChain) - { - } - - public AndConstraint ContainSourceCode(string expectedSourceCode, string because = "", params object[] becauseArgs) - { - if (string.IsNullOrEmpty(expectedSourceCode)) - { - throw new ArgumentException("Cannot assert string containment against or Empty source code.", nameof(expectedSourceCode)); - } - - bool onlyDifferInWhitespace = false; - try - { - Subject.ShowReducedWhitespace().Should().Contain(expectedSourceCode.ShowReducedWhitespace()); - onlyDifferInWhitespace = true; - } - catch - { - } - - string subMessage = "Expected \n{context:string}\n{0}\n to contain\n{1}\n{reason}."; - string message = onlyDifferInWhitespace - ? $"WHITESPACE ONLY DIFFERENCE!\n\n{subMessage}" - : subMessage; - - string actual = Subject.ShowWhitespace(); - string expected = expectedSourceCode.ShowWhitespace(); - CurrentAssertionChain - .ForCondition(Contains(actual, expected, StringComparison.Ordinal)) - .BecauseOf(because, becauseArgs) - .FailWith(message, actual, expected); - - return new AndConstraint(this); - } - - public AndConstraint BeSourceCode(string expectedSourceCode, string because = "", params object[] becauseArgs) - { - if (string.IsNullOrEmpty(expectedSourceCode)) - { - throw new ArgumentException("Cannot assert string equality against or Empty source code.", nameof(expectedSourceCode)); - } - - bool onlyDifferInWhitespace = false; - try - { - Subject.ShowReducedWhitespace().Should().Be(expectedSourceCode.ShowReducedWhitespace()); - onlyDifferInWhitespace = true; - } - catch - { - } - - string subMessage = "Expected \n{context:string}\n{0}\n to match\n{1}\n{reason}."; - string message = onlyDifferInWhitespace - ? $"WHITESPACE ONLY DIFFERENCE!\n\n{subMessage}" - : subMessage; - - string actual = Subject.ShowWhitespace(); - string expected = expectedSourceCode.ShowWhitespace(); - - CurrentAssertionChain - .ForCondition(string.Equals(actual, expected, StringComparison.Ordinal)) - .BecauseOf(because, becauseArgs) - .FailWith(message, actual, expected); - - return new AndConstraint(this); - } - - private static bool Contains(string actual, string expected, StringComparison comparison) - => (actual ?? string.Empty).Contains(expected ?? string.Empty, comparison); -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/SyntaxExtensions.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/SyntaxExtensions.cs deleted file mode 100644 index 17133da1c3..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/SyntaxExtensions.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using Microsoft.CodeAnalysis.Text; - -namespace Microsoft.Testing.Framework.SourceGeneration.UnitTests.Helpers; - -internal static class SyntaxExtensions -{ - public static string ShowWhitespace(this SourceText text) => text.ToString().ShowWhitespace(); - - /// - /// Show spaces and tabs as '·' and '→', replace "\r", but keep newlines, so the resulting text - /// is still formatted it would be in a file but the lines are not OS specific and the whitespace is easy to see. - /// - public static string ShowWhitespace(this string text) - { - if (text.Contains('·') || text.Contains('→')) - { - throw new ArgumentException("Provided text contains '·' or '→' characters, " + - $"which {nameof(ShowWhitespace)} uses to show whitespace. " + - "Did you copy paste it from the test result and forgot to remove those replacements?"); - } - - IDictionary map = new Dictionary - { - { "\r", string.Empty }, - - // Tabs output just 1 '→' on purpose, to break the code layout and be easier to spot. - // We don't want tabs in our code. - { "\t", "→" }, - { " ", "·" }, - }; - - var regex = new Regex(string.Join('|', map.Keys)); - return regex.Replace(text, m => map[m.Value]); - } - - /// - /// Remove every doubled space, which gives you text that still spans multiple lines - /// but the amount of whitespace is greatly reduced. This helps when you are not sure if your content - /// is incorrect or it is just whitespace that is incorrect. - /// - public static string ShowReducedWhitespace(this SourceText text) => ShowReducedWhitespace(text.ToString()); - - /// - /// Remove every doubled space, which gives you text that still spans multiple lines - /// but the amount of whitespace is greatly reduced. This helps when you are not sure if your content - /// is incorrect or it is just whitespace that is incorrect. - /// - public static string ShowReducedWhitespace(this string text) => Regex.Replace(text, " {2,}", string.Empty).ShowWhitespace(); -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/MSTest.SourceGeneration.UnitTests.csproj b/test/UnitTests/MSTest.SourceGeneration.UnitTests/MSTest.SourceGeneration.UnitTests.csproj index 014e756668..8cb7b76a77 100644 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/MSTest.SourceGeneration.UnitTests.csproj +++ b/test/UnitTests/MSTest.SourceGeneration.UnitTests/MSTest.SourceGeneration.UnitTests.csproj @@ -1,35 +1,27 @@ - + net8.0 - Microsoft.Testing.Framework.SourceGeneration.UnitTests - $(NoWarn);NU1701 + Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.UnitTests + true true Exe - - - - - - - PreserveNewest - + - - - Analyzer - true - + + + + diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/MSTest.SourceGeneration.UnitTests.launcher.config.json b/test/UnitTests/MSTest.SourceGeneration.UnitTests/MSTest.SourceGeneration.UnitTests.launcher.config.json deleted file mode 100644 index 0e1704afa1..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/MSTest.SourceGeneration.UnitTests.launcher.config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "program": "MSTest.SourceGeneration.UnitTests.exe" -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/MSTest.SourceGeneration.UnitTests.testingplatformconfig.json b/test/UnitTests/MSTest.SourceGeneration.UnitTests/MSTest.SourceGeneration.UnitTests.testingplatformconfig.json deleted file mode 100644 index 371fa66d5b..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/MSTest.SourceGeneration.UnitTests.testingplatformconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "testingplatform": { - "telemetry": { - "isDevelopmentRepository": true - }, - "exitProcessOnUnhandledException": true, - "testHostControllersManager": { - "singleConnectionNamedPipeServer": { - "waitConnectionTimeoutSeconds": 90 - }, - "namedPipeClient": { - "connectTimeoutSeconds": 90 - } - } - } -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/MSTest.SourceGeneration/Microsoft.Testing.Framework.SourceGeneration.TestNodesGenerator/Microsoft.Testing.Framework.Source b/test/UnitTests/MSTest.SourceGeneration.UnitTests/MSTest.SourceGeneration/Microsoft.Testing.Framework.SourceGeneration.TestNodesGenerator/Microsoft.Testing.Framework.Source deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/ObjectModels/InlineTestMethodArgumentsInfoTests.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/ObjectModels/InlineTestMethodArgumentsInfoTests.cs deleted file mode 100644 index 9c34e23745..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/ObjectModels/InlineTestMethodArgumentsInfoTests.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using Microsoft.Testing.Framework.SourceGeneration.ObjectModels; - -namespace Microsoft.Testing.Framework.SourceGeneration.UnitTests; - -[TestClass] -public sealed class InlineTestMethodArgumentsInfoTests : TestBase -{ - [DataRow("a", "a")] - [DataRow("a, b", "a, b")] - [DataRow("\"ok\"", "\\\"ok\\\"")] - [DataRow("\"", "\\\"")] - [DataRow("\\", "\\")] - [DataRow("\\\\", "\\\\")] - [DataRow("\\\"", "\\\"")] - [TestMethod] - public void EscapeArgument_ProducesCorrectString(string value, string expectedEscapedValue) - { - // Arrange - StringBuilder stringBuilder = new(); - - // Act - DataRowTestMethodArgumentsInfo.EscapeArgument(value, stringBuilder); - - // Assert - Assert.AreEqual(expectedEscapedValue, stringBuilder.ToString()); - } -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Program.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/Program.cs index de3b386a21..9fbdc73473 100644 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Program.cs +++ b/test/UnitTests/MSTest.SourceGeneration.UnitTests/Program.cs @@ -1,12 +1,8 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.Testing.Extensions; -using ExecutionScope = Microsoft.VisualStudio.TestTools.UnitTesting.ExecutionScope; - -[assembly: Parallelize(Scope = ExecutionScope.MethodLevel, Workers = 0)] - ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args); builder.AddMSTest(() => [Assembly.GetEntryAssembly()!]); diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Properties/launchSettings.json b/test/UnitTests/MSTest.SourceGeneration.UnitTests/Properties/launchSettings.json deleted file mode 100644 index e9745ed222..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Properties/launchSettings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "profiles": { - "MSTest.SourceGeneration.UnitTests": { - "commandName": "Project", - "commandLineArgs": "" - } - } -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/ReflectionMetadataGeneratorTests.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/ReflectionMetadataGeneratorTests.cs new file mode 100644 index 0000000000..97600f2d3e --- /dev/null +++ b/test/UnitTests/MSTest.SourceGeneration.UnitTests/ReflectionMetadataGeneratorTests.cs @@ -0,0 +1,494 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using AwesomeAssertions; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Generators; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.UnitTests; + +[TestClass] +public sealed class ReflectionMetadataGeneratorTests +{ + private const string MinimalMSTestStub = """ + namespace Microsoft.VisualStudio.TestTools.UnitTesting + { + [System.AttributeUsage(System.AttributeTargets.Class)] + public sealed class TestClassAttribute : System.Attribute {} + + [System.AttributeUsage(System.AttributeTargets.Method)] + public class TestMethodAttribute : System.Attribute {} + } + """; + + private const string RuntimeHookStub = """ + namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration + { + public sealed class SourceGeneratedReflectionDataProvider + { + public System.Reflection.Assembly? Assembly { get; set; } + public string? AssemblyName { get; set; } + public System.Type[] Types { get; set; } = System.Array.Empty(); + public System.Collections.Generic.Dictionary TypesByName { get; set; } = new(); + public System.Collections.Generic.Dictionary TypeMethods { get; set; } = new(); + } + + public static class ReflectionMetadataHook + { + public static void SetMetadata(SourceGeneratedReflectionDataProvider data) { } + } + } + """; + + [TestMethod] + public void Generator_DiscoversTestClassesAndMethods_AndEmitsModuleInitializer() + { + const string userCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Sample + { + [TestClass] + public class MyTests + { + [TestMethod] + public void Test1() {} + + [TestMethod] + public void Test2() {} + + public void NotATest() {} + } + } + """; + + GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode); + + result.Diagnostics.Should().BeEmpty(); + result.GeneratedSources.Should().HaveCount(1); + + string generated = result.GeneratedSources[0].SourceText.ToString(); + + generated.Should().Contain("[ModuleInitializer]"); + generated.Should().Contain("typeof(global::Sample.MyTests)"); + generated.Should().Contain("ResolveMethod(typeof(global::Sample.MyTests), \"Test1\", Type.EmptyTypes)"); + generated.Should().Contain("ResolveMethod(typeof(global::Sample.MyTests), \"Test2\", Type.EmptyTypes)"); + generated.Should().NotContain("\"NotATest\""); + generated.Should().Contain("ReflectionMetadataHook.SetMetadata"); + + // TypesByName key uses typeof(X).FullName! at runtime to match Type.FullName for nested/generic types. + generated.Should().Contain("[typeof(global::Sample.MyTests).FullName!] = typeof(global::Sample.MyTests)"); + generated.Should().NotContain("[\"global::Sample.MyTests\"]"); + } + + [TestMethod] + public void Generator_EmitsOverloadAwareMethodResolution() + { + const string userCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Sample + { + [TestClass] + public class OverloadTests + { + [TestMethod] + public void Run() {} + + [TestMethod] + public void Run(int x) {} + + [TestMethod] + public void Run(string s, int x) {} + } + } + """; + + GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode); + + result.Diagnostics.Should().BeEmpty(); + string generated = result.GeneratedSources[0].SourceText.ToString(); + + generated.Should().Contain("ResolveMethod(typeof(global::Sample.OverloadTests), \"Run\", Type.EmptyTypes)"); + generated.Should().Contain("ResolveMethod(typeof(global::Sample.OverloadTests), \"Run\", new Type[] { typeof(global::System.Int32) })"); + generated.Should().Contain("ResolveMethod(typeof(global::Sample.OverloadTests), \"Run\", new Type[] { typeof(global::System.String), typeof(global::System.Int32) })"); + } + + [TestMethod] + public void Generator_EmittedSourceCompilesCleanly() + { + const string userCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Sample + { + [TestClass] + public class MyTests + { + [TestMethod] + public void Test1() {} + + [TestMethod] + public void Test2(int x, string s) {} + + [TestMethod] + internal void NonPublicTest() {} + } + } + """; + + Compilation outputCompilation = RunGeneratorAndGetCompilation(MinimalMSTestStub, RuntimeHookStub, userCode); + + Diagnostic[] errors = outputCompilation.GetDiagnostics() + .Where(d => d.Severity == DiagnosticSeverity.Error) + .ToArray(); + + errors.Should().BeEmpty("the generator output should compile without errors. Diagnostics: " + string.Join("\n", errors.Select(d => d.ToString()))); + } + + [TestMethod] + public void Generator_SkipsStaticAndAbstractTestClasses() + { + const string userCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Sample + { + [TestClass] + public static class StaticTests + { + [TestMethod] + public static void Test1() {} + } + + [TestClass] + public abstract class AbstractTests + { + [TestMethod] + public void Test1() {} + } + + [TestClass] + public class Concrete : AbstractTests + { + [TestMethod] + public void Test2() {} + } + } + """; + + GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode); + + result.Diagnostics.Should().BeEmpty(); + string generated = result.GeneratedSources[0].SourceText.ToString(); + + generated.Should().NotContain("typeof(global::Sample.StaticTests)"); + generated.Should().NotContain("typeof(global::Sample.AbstractTests)"); + generated.Should().Contain("typeof(global::Sample.Concrete)"); + + // Concrete must surface BOTH its own Test2 AND the inherited Test1 from AbstractTests. + generated.Should().Contain("ResolveMethod(typeof(global::Sample.Concrete), \"Test1\", Type.EmptyTypes)"); + generated.Should().Contain("ResolveMethod(typeof(global::Sample.Concrete), \"Test2\", Type.EmptyTypes)"); + } + + [TestMethod] + public void Generator_EmitsTypesByName_UsesTypeofFullNameKey_ForNestedTypes() + { + const string userCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Sample + { + public class Outer + { + [TestClass] + public class Nested + { + [TestMethod] + public void Test1() {} + } + } + } + """; + + GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode); + + result.Diagnostics.Should().BeEmpty(); + string generated = result.GeneratedSources[0].SourceText.ToString(); + + // The key must be emitted as typeof(X).FullName! so the runtime dictionary lookup + // matches Type.FullName, which uses '+' to separate the nesting type from the nested + // one (Sample.Outer+Nested) and would not match a literal compile-time C# name like + // "Sample.Outer.Nested". + generated.Should().Contain("[typeof(global::Sample.Outer.Nested).FullName!] = typeof(global::Sample.Outer.Nested)"); + generated.Should().NotContain("[\"Sample.Outer.Nested\"]"); + } + + [TestMethod] + public void Generator_HandlesEmptyAssembly() + { + const string userCode = """ + namespace Sample + { + public class NoTests {} + } + """; + + GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode); + + result.Diagnostics.Should().BeEmpty(); + + // When no [TestClass] types are discovered, the generator must not emit any source + // (avoids polluting the compilation with an empty module initializer). + result.GeneratedSources.Should().BeEmpty(); + } + + [TestMethod] + public void Generator_SkipsOpenGenericTestClasses() + { + const string userCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Sample + { + [TestClass] + public class Generic + { + [TestMethod] + public void Test1() {} + } + + [TestClass] + public class Concrete + { + [TestMethod] + public void Test2() {} + } + } + """; + + GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode); + + result.Diagnostics.Should().BeEmpty(); + string generated = result.GeneratedSources[0].SourceText.ToString(); + + // Open generic test classes cannot be emitted as `typeof(Generic)` at module-initializer + // scope, so they are silently skipped. The non-generic sibling must still be emitted. + generated.Should().NotContain("typeof(global::Sample.Generic"); + generated.Should().Contain("typeof(global::Sample.Concrete)"); + } + + [TestMethod] + public void Generator_SkipsMethodsWithByRefParameters() + { + const string userCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Sample + { + [TestClass] + public class ByRefTests + { + [TestMethod] + public void RefTest(ref int x) {} + + [TestMethod] + public void OutTest(out int x) { x = 0; } + + [TestMethod] + public void InTest(in int x) {} + + [TestMethod] + public void NormalTest(int x) {} + } + } + """; + + GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode); + + result.Diagnostics.Should().BeEmpty(); + string generated = result.GeneratedSources[0].SourceText.ToString(); + + // By-ref signatures (ref/out/in) cannot round-trip through ResolveMethod's + // typeof(T) == ParameterType check, so the generator omits these methods entirely. + // The plain-parameter overload must still be emitted. + generated.Should().NotContain("\"RefTest\""); + generated.Should().NotContain("\"OutTest\""); + generated.Should().NotContain("\"InTest\""); + generated.Should().Contain("ResolveMethod(typeof(global::Sample.ByRefTests), \"NormalTest\", new Type[] { typeof(global::System.Int32) })"); + } + + [TestMethod] + public void Generator_SkipsInaccessibleNestedTestClasses() + { + const string userCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Sample + { + public class Outer + { + [TestClass] + private class PrivateNested + { + [TestMethod] + public void Test1() {} + } + + [TestClass] + public class PublicNested + { + [TestMethod] + public void Test2() {} + } + } + } + """; + + GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode); + + result.Diagnostics.Should().BeEmpty(); + string generated = result.GeneratedSources[0].SourceText.ToString(); + + // A private (or otherwise inaccessible) nested [TestClass] cannot be referenced from + // the generated `internal` module initializer; it would compile as CS0122. The + // generator skips such types but still emits siblings that are reachable. + generated.Should().NotContain("PrivateNested"); + generated.Should().Contain("typeof(global::Sample.Outer.PublicNested)"); + } + + [TestMethod] + public void Generator_SkipsFileLocalTestClasses() + { + const string userCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Sample + { + [TestClass] + file class FileLocalTests + { + [TestMethod] + public void HiddenTest() {} + } + + [TestClass] + public class VisibleTests + { + [TestMethod] + public void OpenTest() {} + } + } + """; + + GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode); + + result.Diagnostics.Should().BeEmpty(); + string generated = result.GeneratedSources[0].SourceText.ToString(); + + // `file`-scoped types are only addressable within their own source file. The generated + // module initializer lives in a different file and would fail with CS9051. Skip them. + generated.Should().NotContain("FileLocalTests"); + generated.Should().NotContain("\"HiddenTest\""); + generated.Should().Contain("typeof(global::Sample.VisibleTests)"); + } + + [TestMethod] + public void Generator_DedupesOverriddenMethodsAcrossInheritance() + { + const string userCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Sample + { + public abstract class BaseTests + { + [TestMethod] + public virtual void TestA() {} + } + + [TestClass] + public class Derived : BaseTests + { + [TestMethod] + public override void TestA() {} + } + } + """; + + GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode); + + result.Diagnostics.Should().BeEmpty(); + string generated = result.GeneratedSources[0].SourceText.ToString(); + + // The base + override must collapse to a single emit. Counting occurrences of the + // exact ResolveMethod call site catches double-emission regressions in the inheritance + // walk's dedupe-by-signature logic. + int count = CountOccurrences(generated, "ResolveMethod(typeof(global::Sample.Derived), \"TestA\", Type.EmptyTypes)"); + count.Should().Be(1); + } + + private static int CountOccurrences(string source, string needle) + { + int count = 0; + int index = 0; + while ((index = source.IndexOf(needle, index, StringComparison.Ordinal)) >= 0) + { + count++; + index += needle.Length; + } + + return count; + } + + private static GeneratorRunResult RunGenerator(params string[] sources) + { + IEnumerable trees = sources.Select(s => CSharpSyntaxTree.ParseText(s)); + var references = new MetadataReference[] + { + MetadataReference.CreateFromFile(typeof(object).Assembly.Location), + MetadataReference.CreateFromFile(typeof(System.Runtime.CompilerServices.ModuleInitializerAttribute).Assembly.Location), + }; + + var compilation = CSharpCompilation.Create( + "TestSample", + trees, + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + GeneratorDriver driver = CSharpGeneratorDriver.Create(new ReflectionMetadataGenerator()); + driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out _, out _); + + return driver.GetRunResult().Results[0]; + } + + private static Compilation RunGeneratorAndGetCompilation(params string[] sources) + { + IEnumerable trees = sources.Select(s => CSharpSyntaxTree.ParseText(s)); + var references = new MetadataReference[] + { + MetadataReference.CreateFromFile(typeof(object).Assembly.Location), + MetadataReference.CreateFromFile(typeof(System.Runtime.CompilerServices.ModuleInitializerAttribute).Assembly.Location), + MetadataReference.CreateFromFile(typeof(System.Reflection.Assembly).Assembly.Location), + MetadataReference.CreateFromFile(typeof(System.Collections.Generic.Dictionary<,>).Assembly.Location), + MetadataReference.CreateFromFile(typeof(System.Reflection.MethodInfo).Assembly.Location), + MetadataReference.CreateFromFile(typeof(System.Reflection.BindingFlags).Assembly.Location), + MetadataReference.CreateFromFile(typeof(System.MissingMethodException).Assembly.Location), + MetadataReference.CreateFromFile(typeof(System.Linq.Enumerable).Assembly.Location), + }; + + var compilation = CSharpCompilation.Create( + "TestSample", + trees, + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + GeneratorDriver driver = CSharpGeneratorDriver.Create(new ReflectionMetadataGenerator()); + driver.RunGeneratorsAndUpdateCompilation(compilation, out Compilation outputCompilation, out _); + + return outputCompilation; + } +} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/TestBase.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/TestBase.cs deleted file mode 100644 index 7a6d1c82ee..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/TestBase.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Microsoft.Testing.Framework.SourceGeneration.UnitTests; - -/// -/// Empty test base, because TestInfrastructure project depends on Testing.Framework, and we cannot use that. -/// -public abstract class TestBase; diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/TestUtilities/CSharpCodeFixVerifier.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/TestUtilities/CSharpCodeFixVerifier.cs deleted file mode 100644 index 7b2cfb8660..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/TestUtilities/CSharpCodeFixVerifier.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CodeFixes; -using Microsoft.CodeAnalysis.CSharp.Testing; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Testing; - -namespace Microsoft.Testing.Framework.SourceGeneration.UnitTests.TestUtilities; - -internal static class CSharpCodeFixVerifier - where TAnalyzer : DiagnosticAnalyzer, new() - where TCodeFix : CodeFixProvider, new() -{ - public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected) - { - var test = new Test { TestCode = source }; - test.ExpectedDiagnostics.AddRange(expected); - await test.RunAsync(); - } - - public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor) - => CSharpCodeFixVerifier.Diagnostic(descriptor); - - public sealed class Test : CSharpCodeFixTest; -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/TestUtilities/GeneratorCompilationResult.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/TestUtilities/GeneratorCompilationResult.cs deleted file mode 100644 index 7fa121a363..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/TestUtilities/GeneratorCompilationResult.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Emit; - -namespace Microsoft.Testing.Framework.SourceGeneration.UnitTests.TestUtilities; - -internal sealed class GeneratorCompilationResult(GeneratorDriverRunResult runResult, GeneratorDriverTimingInfo timingInfo, - EmitResult emitResult, string? failingGeneratedCode) -{ - public ImmutableArray GeneratedTrees => RunResult.GeneratedTrees; - - public GeneratorDriverRunResult RunResult { get; } = runResult; - - public GeneratorDriverTimingInfo TimingInfo { get; } = timingInfo; - - public EmitResult EmitResult { get; } = emitResult; - - public string? FailingGeneratedCode { get; } = failingGeneratedCode; -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/TestUtilities/GeneratorTester.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/TestUtilities/GeneratorTester.cs deleted file mode 100644 index 87f1705900..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/TestUtilities/GeneratorTester.cs +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.Collections.Immutable; - -using AwesomeAssertions; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Emit; -using Microsoft.CodeAnalysis.Testing; -using Microsoft.Testing.Extensions; -using Microsoft.Testing.Extensions.TrxReport.Abstractions; -using Microsoft.Testing.Platform.Extensions.Messages; - -namespace Microsoft.Testing.Framework.SourceGeneration.UnitTests.TestUtilities; - -internal sealed class GeneratorTester -{ - private readonly Func _incrementalGeneratorFactory; - private readonly string[] _additionalReferences; - private static readonly SemaphoreSlim Lock = new(1); - - public GeneratorTester(Func incrementalGeneratorFactory, string[] additionalReferences) - { - _incrementalGeneratorFactory = incrementalGeneratorFactory; - _additionalReferences = additionalReferences; - } - - public static GeneratorTester TestGraph { get; } = - new( - () => new TestNodesGenerator(), - [ - // Microsoft.Testing.Platform dll - Assembly.GetAssembly(typeof(IProperty))!.Location, - - // Microsoft.Testing.Framework dll - Assembly.GetAssembly(typeof(TestNode))!.Location, - - // Microsoft.Testing.Extensions dll - Assembly.GetAssembly(typeof(TrxReportExtensions))!.Location, - - // Microsoft.Testing.Extensions.TrxReport.Abstractions dll - Assembly.GetAssembly(typeof(TrxExceptionProperty))!.Location, - - // MSTest.TestFramework dll - Assembly.GetAssembly(typeof(TestClassAttribute))!.Location - ]); - - public static ImmutableArray? Net80MetadataReferences { get; set; } - - public async Task CompileAndExecuteAsync(string source, CancellationToken cancellationToken) - => await CompileAndExecuteAsync([source], cancellationToken); - - public async Task CompileAndExecuteAsync(string[] sources, CancellationToken cancellationToken) - { - // Cache the resolution in local and try to fire the finalizers - // In CI sometime we have a crash for http connection and the suspect is - // this call below that connects to nuget.org - if (Net80MetadataReferences is null) - { - await Lock.WaitAsync(cancellationToken); - try - { - if (Net80MetadataReferences is null) - { - string nuGetConfigFilePath = Path.Combine(RootFinder.Find(), "NuGet.config"); - - Net80MetadataReferences = - await ReferenceAssemblies.Net.Net80.WithNuGetConfigFilePath(nuGetConfigFilePath).ResolveAsync(LanguageNames.CSharp, cancellationToken); - - GC.Collect(); - GC.WaitForPendingFinalizers(); - GC.Collect(); - } - } - finally - { - Lock.Release(); - } - } - - MetadataReference[] metadataReferences = [.. Net80MetadataReferences.Value, .. _additionalReferences.Select(loc => MetadataReference.CreateFromFile(loc))]; - - var compilation = CSharpCompilation.Create( - "TestAssembly", - sources.Select(source => CSharpSyntaxTree.ParseText(source, cancellationToken: cancellationToken)), - metadataReferences, - new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); - - ISourceGenerator generator = _incrementalGeneratorFactory().AsSourceGenerator(); - GeneratorDriver driver = CSharpGeneratorDriver.Create( - generators: [generator]); - - driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out Compilation? outputCompilation, - out ImmutableArray diagnostics, cancellationToken); - diagnostics.Should().BeEmpty(); - - using var ms = new MemoryStream(); - EmitResult result = outputCompilation.Emit(ms, cancellationToken: cancellationToken); - - GeneratorDriverRunResult runResult = driver.GetRunResult(); - GeneratorDriverTimingInfo timingInfo = driver.GetTimingInfo(); - - if (result.Success) - { - return new(runResult, timingInfo, result, null); - } - - var code = new StringBuilder(); - - // Append diagnostics that are not tied to any file. - foreach (Diagnostic? globalDiagnostic in result.Diagnostics.Where(d => d.Location.SourceTree == null || string.IsNullOrWhiteSpace(d.Location.SourceTree.FilePath))) - { - code.AppendLine(globalDiagnostic.ToString()); - } - - foreach (SyntaxTree output in outputCompilation.SyntaxTrees) - { - IEnumerable d = output.GetDiagnostics(cancellationToken); - - var diagnosticsByLine = new Dictionary>(); - result.Diagnostics - .Where(d => !string.IsNullOrEmpty(output.FilePath) && d.Location.SourceTree?.FilePath == output.FilePath) - .GroupBy(d => d.Location.GetLineSpan().StartLinePosition) - .ToList() - .ForEach(f => - { - if (diagnosticsByLine.TryGetValue(f.Key.Line, out List? list)) - { - list.AddRange(f); - } - else - { - var l = new List(); - l.AddRange(f); - diagnosticsByLine[f.Key.Line] = l; - } - }); - - if (diagnosticsByLine.Count == 0) - { - continue; - } - - code.Append("file '").Append(output.FilePath).AppendLine("':"); - string[] lines = output.ToString().Split('\n'); - int length = lines.Length; - int pad = length.ToString(CultureInfo.InvariantCulture).Length; - for (int i = 0; i < length; i++) - { - if (diagnosticsByLine.TryGetValue(i, out List? diagnosticsForLine)) - { - code.AppendLine(); - foreach (Diagnostic diagnostic in diagnosticsForLine) - { - code.Append(">>> ").AppendLine(diagnostic.ToString()); - } - } - - // Add line number (starting from 1) - code.Append((i + 1).ToString(CultureInfo.InvariantCulture).PadLeft(pad, '0')); - code.Append(' ').AppendLine(lines[i]); - } - - code.AppendLine(); - } - - return new(runResult, timingInfo, result, code.ToString()); - } -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/TestUtilities/TestingFrameworkVerifier.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/TestUtilities/TestingFrameworkVerifier.cs deleted file mode 100644 index d21e00b691..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/TestUtilities/TestingFrameworkVerifier.cs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Testing; - -namespace Microsoft.Testing.Framework.SourceGeneration.UnitTests.TestUtilities; - -internal sealed class TestingFrameworkVerifier : IVerifier -{ - public TestingFrameworkVerifier() - : this([]) - { - } - - internal TestingFrameworkVerifier(ImmutableStack context) - => Context = context ?? throw new ArgumentNullException(nameof(context)); - - public ImmutableStack Context { get; } - - public void Empty(string collectionName, IEnumerable collection) => Assert.AreNotEqual(true, collection?.Any(), CreateMessage($"expected '{collectionName}' to be empty, contains '{collection?.Count()}' elements")); - - public void Equal(T expected, T actual, string? message = null) - { - if (message is null && Context.IsEmpty) - { - Assert.AreEqual(expected, actual); - } - else - { - Assert.AreEqual(expected, actual, CreateMessage(message)); - } - } - - [DoesNotReturn] - public void Fail(string? message = null) - { - if (message is null && Context.IsEmpty) - { - Assert.Fail(); - } - else - { - Assert.Fail(CreateMessage(message)); - } - - throw new InvalidOperationException("This program location is thought to be unreachable."); - } - - public void False([DoesNotReturnIf(true)] bool assert, string? message = null) - { - if (message is null && Context.IsEmpty) - { - Assert.IsFalse(assert); - } - else - { - Assert.IsFalse(assert, CreateMessage(message)); - } - } - - public void LanguageIsSupported(string language) => Assert.IsFalse(language is not LanguageNames.CSharp and not LanguageNames.VisualBasic, CreateMessage($"Unsupported Language: '{language}'")); - - public void NotEmpty(string collectionName, IEnumerable collection) => Assert.IsNotEmpty(collection, CreateMessage($"expected '{collectionName}' to be non-empty, contains")); - - public IVerifier PushContext(string context) - { - Assert.AreEqual(typeof(TestingFrameworkVerifier), GetType()); - return new TestingFrameworkVerifier(Context.Push(context)); - } - - public void SequenceEqual(IEnumerable expected, IEnumerable actual, IEqualityComparer? equalityComparer = null, string? message = null) - { - var comparer = new SequenceEqualEnumerableEqualityComparer(equalityComparer); - bool areEqual = comparer.Equals(expected, actual); - if (!areEqual) - { - Assert.Fail(CreateMessage(message)); - } - } - - public void True([DoesNotReturnIf(false)] bool assert, string? message = null) - { - if (message is null && Context.IsEmpty) - { - Assert.IsTrue(assert); - } - else - { - Assert.IsTrue(assert, CreateMessage(message)); - } - } - - private string CreateMessage(string? message) - { - foreach (string frame in Context) - { - message = "Context: " + frame + Environment.NewLine + message; - } - - return message ?? string.Empty; - } - - private sealed class SequenceEqualEnumerableEqualityComparer : IEqualityComparer?> - { - private readonly IEqualityComparer _itemEqualityComparer; - - public SequenceEqualEnumerableEqualityComparer(IEqualityComparer? itemEqualityComparer) - => _itemEqualityComparer = itemEqualityComparer ?? EqualityComparer.Default; - - public bool Equals(IEnumerable? x, IEnumerable? y) - => ReferenceEquals(x, y) - || (x is not null && y is not null && x.SequenceEqual(y, _itemEqualityComparer)); - - public int GetHashCode(IEnumerable? obj) - { - if (obj is null) - { - return 0; - } - - // From System.Tuple - // - // The suppression is required due to an invalid contract in IEqualityComparer - // https://github.com/dotnet/runtime/issues/30998 - return obj - .Select(item => _itemEqualityComparer.GetHashCode(item!)) - .Aggregate( - 0, - (aggHash, nextHash) => ((aggHash << 5) + aggHash) ^ nextHash); - } - } -}