diff --git a/TestPlatform.sln b/TestPlatform.sln
index 5824a83e66..05b0609b3c 100644
--- a/TestPlatform.sln
+++ b/TestPlatform.sln
@@ -179,6 +179,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MSTest1", "playground\MSTes
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AttachmentProcessorDataCollector", "test\TestAssets\AttachmentProcessorDataCollector\AttachmentProcessorDataCollector.csproj", "{B6AF6BCD-64C6-4F4E-ABCA-C8AA2AA66B7B}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "vstest.ProgrammerTests", "test\vstest.ProgrammerTests\vstest.ProgrammerTests.csproj", "{B1F84FD8-6150-4ECA-9AD7-C316E04E17D8}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Intent", "test\Intent\Intent.csproj", "{BFBB35C9-6437-480A-8DCC-AE3700110E7D}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Intent.Primitives", "test\Intent.Primitives\Intent.Primitives.csproj", "{29270853-90DC-4C39-9621-F47AE40A79B6}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "testhost.arm64", "src\testhost.arm64\testhost.arm64.csproj", "{186069FE-E1E8-4DE1-BEA4-0FF1484D22D1}"
EndProject
Global
@@ -883,6 +889,42 @@ Global
{B6AF6BCD-64C6-4F4E-ABCA-C8AA2AA66B7B}.Release|x64.Build.0 = Release|Any CPU
{B6AF6BCD-64C6-4F4E-ABCA-C8AA2AA66B7B}.Release|x86.ActiveCfg = Release|Any CPU
{B6AF6BCD-64C6-4F4E-ABCA-C8AA2AA66B7B}.Release|x86.Build.0 = Release|Any CPU
+ {B1F84FD8-6150-4ECA-9AD7-C316E04E17D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B1F84FD8-6150-4ECA-9AD7-C316E04E17D8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B1F84FD8-6150-4ECA-9AD7-C316E04E17D8}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B1F84FD8-6150-4ECA-9AD7-C316E04E17D8}.Debug|x64.Build.0 = Debug|Any CPU
+ {B1F84FD8-6150-4ECA-9AD7-C316E04E17D8}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B1F84FD8-6150-4ECA-9AD7-C316E04E17D8}.Debug|x86.Build.0 = Debug|Any CPU
+ {B1F84FD8-6150-4ECA-9AD7-C316E04E17D8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B1F84FD8-6150-4ECA-9AD7-C316E04E17D8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B1F84FD8-6150-4ECA-9AD7-C316E04E17D8}.Release|x64.ActiveCfg = Release|Any CPU
+ {B1F84FD8-6150-4ECA-9AD7-C316E04E17D8}.Release|x64.Build.0 = Release|Any CPU
+ {B1F84FD8-6150-4ECA-9AD7-C316E04E17D8}.Release|x86.ActiveCfg = Release|Any CPU
+ {B1F84FD8-6150-4ECA-9AD7-C316E04E17D8}.Release|x86.Build.0 = Release|Any CPU
+ {BFBB35C9-6437-480A-8DCC-AE3700110E7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BFBB35C9-6437-480A-8DCC-AE3700110E7D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BFBB35C9-6437-480A-8DCC-AE3700110E7D}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {BFBB35C9-6437-480A-8DCC-AE3700110E7D}.Debug|x64.Build.0 = Debug|Any CPU
+ {BFBB35C9-6437-480A-8DCC-AE3700110E7D}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {BFBB35C9-6437-480A-8DCC-AE3700110E7D}.Debug|x86.Build.0 = Debug|Any CPU
+ {BFBB35C9-6437-480A-8DCC-AE3700110E7D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BFBB35C9-6437-480A-8DCC-AE3700110E7D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BFBB35C9-6437-480A-8DCC-AE3700110E7D}.Release|x64.ActiveCfg = Release|Any CPU
+ {BFBB35C9-6437-480A-8DCC-AE3700110E7D}.Release|x64.Build.0 = Release|Any CPU
+ {BFBB35C9-6437-480A-8DCC-AE3700110E7D}.Release|x86.ActiveCfg = Release|Any CPU
+ {BFBB35C9-6437-480A-8DCC-AE3700110E7D}.Release|x86.Build.0 = Release|Any CPU
+ {29270853-90DC-4C39-9621-F47AE40A79B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {29270853-90DC-4C39-9621-F47AE40A79B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {29270853-90DC-4C39-9621-F47AE40A79B6}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {29270853-90DC-4C39-9621-F47AE40A79B6}.Debug|x64.Build.0 = Debug|Any CPU
+ {29270853-90DC-4C39-9621-F47AE40A79B6}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {29270853-90DC-4C39-9621-F47AE40A79B6}.Debug|x86.Build.0 = Debug|Any CPU
+ {29270853-90DC-4C39-9621-F47AE40A79B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {29270853-90DC-4C39-9621-F47AE40A79B6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {29270853-90DC-4C39-9621-F47AE40A79B6}.Release|x64.ActiveCfg = Release|Any CPU
+ {29270853-90DC-4C39-9621-F47AE40A79B6}.Release|x64.Build.0 = Release|Any CPU
+ {29270853-90DC-4C39-9621-F47AE40A79B6}.Release|x86.ActiveCfg = Release|Any CPU
+ {29270853-90DC-4C39-9621-F47AE40A79B6}.Release|x86.Build.0 = Release|Any CPU
{186069FE-E1E8-4DE1-BEA4-0FF1484D22D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{186069FE-E1E8-4DE1-BEA4-0FF1484D22D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{186069FE-E1E8-4DE1-BEA4-0FF1484D22D1}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -970,6 +1012,9 @@ Global
{545A88D3-1AE2-4D39-9B7C-C691768AD17F} = {6CE2F530-582B-4695-A209-41065E103426}
{57A61A09-10AD-44BE-8DF4-A6FD108F7DF7} = {6CE2F530-582B-4695-A209-41065E103426}
{B6AF6BCD-64C6-4F4E-ABCA-C8AA2AA66B7B} = {D9A30E32-D466-4EC5-B4F2-62E17562279B}
+ {B1F84FD8-6150-4ECA-9AD7-C316E04E17D8} = {B27FAFDF-2DBA-4AB0-BA85-FD5F21D359D6}
+ {BFBB35C9-6437-480A-8DCC-AE3700110E7D} = {B27FAFDF-2DBA-4AB0-BA85-FD5F21D359D6}
+ {29270853-90DC-4C39-9621-F47AE40A79B6} = {B27FAFDF-2DBA-4AB0-BA85-FD5F21D359D6}
{186069FE-E1E8-4DE1-BEA4-0FF1484D22D1} = {ED0C35EB-7F31-4841-A24F-8EB708FFA959}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
diff --git a/src/Microsoft.TestPlatform.Client/Friends.cs b/src/Microsoft.TestPlatform.Client/Friends.cs
index 90a5d9db6f..81da967ae4 100644
--- a/src/Microsoft.TestPlatform.Client/Friends.cs
+++ b/src/Microsoft.TestPlatform.Client/Friends.cs
@@ -10,5 +10,6 @@
#region Test Assemblies
[assembly: InternalsVisibleTo("Microsoft.TestPlatform.Client.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")]
+[assembly: InternalsVisibleTo("vstest.ProgrammerTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")]
#endregion
diff --git a/src/Microsoft.TestPlatform.Client/TestPlatform.cs b/src/Microsoft.TestPlatform.Client/TestPlatform.cs
index 2676096fdd..f46702a5fc 100644
--- a/src/Microsoft.TestPlatform.Client/TestPlatform.cs
+++ b/src/Microsoft.TestPlatform.Client/TestPlatform.cs
@@ -36,7 +36,7 @@ namespace Microsoft.VisualStudio.TestPlatform.Client;
///
internal class TestPlatform : ITestPlatform
{
- private readonly TestRuntimeProviderManager _testHostProviderManager;
+ private readonly ITestRuntimeProviderManager _testHostProviderManager;
private readonly IFileHelper _fileHelper;
@@ -66,10 +66,10 @@ public TestPlatform()
/// The test engine.
/// The file helper.
/// The data.
- protected TestPlatform(
+ protected internal TestPlatform(
ITestEngine testEngine,
IFileHelper filehelper,
- TestRuntimeProviderManager testHostProviderManager)
+ ITestRuntimeProviderManager testHostProviderManager)
{
TestEngine = testEngine;
_fileHelper = filehelper;
@@ -117,6 +117,11 @@ public ITestRunRequest CreateTestRunRequest(
ITestLoggerManager loggerManager = TestEngine.GetLoggerManager(requestData);
loggerManager.Initialize(testRunCriteria.TestRunSettings);
+ // TODO: PERF: this will create a testhost manager, and then it will pass that to GetExecutionManager, where it will
+ // be used only when we will run in-process. If we don't run in process, we will throw away the manager we just
+ // created and let the proxy parallel callbacks to create a new one. This seems to be very easy to move to the GetExecutionManager,
+ // and safe as well, so we create the manager only once.
+ // TODO: Of course TestEngine.GetExecutionManager is public api...
ITestRuntimeProvider testHostManager = _testHostProviderManager.GetTestHostManagerByRunConfiguration(testRunCriteria.TestRunSettings);
TestPlatform.ThrowExceptionIfTestHostManagerIsNull(testHostManager, testRunCriteria.TestRunSettings);
diff --git a/src/Microsoft.TestPlatform.Common/Friends.cs b/src/Microsoft.TestPlatform.Common/Friends.cs
index ea3f0a478d..23875a52a0 100644
--- a/src/Microsoft.TestPlatform.Common/Friends.cs
+++ b/src/Microsoft.TestPlatform.Common/Friends.cs
@@ -25,4 +25,6 @@
[assembly: InternalsVisibleTo("Microsoft.TestPlatform.Extensions.BlameDataCollector.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")]
[assembly: InternalsVisibleTo("Microsoft.TestPlatform.TestUtilities, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")]
[assembly: InternalsVisibleTo("Microsoft.TestPlatform.AcceptanceTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")]
+[assembly: InternalsVisibleTo("vstest.ProgrammerTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")]
+
#endregion
diff --git a/src/Microsoft.TestPlatform.Common/Hosting/ITestRuntimeProviderManager.cs b/src/Microsoft.TestPlatform.Common/Hosting/ITestRuntimeProviderManager.cs
new file mode 100644
index 0000000000..57366c4669
--- /dev/null
+++ b/src/Microsoft.TestPlatform.Common/Hosting/ITestRuntimeProviderManager.cs
@@ -0,0 +1,12 @@
+// 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.Common.Hosting;
+
+using Microsoft.VisualStudio.TestPlatform.ObjectModel.Host;
+
+internal interface ITestRuntimeProviderManager
+{
+ ITestRuntimeProvider GetTestHostManagerByRunConfiguration(string runConfiguration);
+ ITestRuntimeProvider GetTestHostManagerByUri(string hostUri);
+}
diff --git a/src/Microsoft.TestPlatform.Common/Hosting/TestRunTimeProviderManager.cs b/src/Microsoft.TestPlatform.Common/Hosting/TestRunTimeProviderManager.cs
index dfbd3db82d..9752ec6f57 100644
--- a/src/Microsoft.TestPlatform.Common/Hosting/TestRunTimeProviderManager.cs
+++ b/src/Microsoft.TestPlatform.Common/Hosting/TestRunTimeProviderManager.cs
@@ -14,7 +14,7 @@ namespace Microsoft.VisualStudio.TestPlatform.Common.Hosting;
///
/// Responsible for managing TestRuntimeProviderManager extensions
///
-public class TestRuntimeProviderManager
+public class TestRuntimeProviderManager : ITestRuntimeProviderManager
{
private static TestRuntimeProviderManager s_testHostManager;
diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/DataCollectionRequestHandler.cs b/src/Microsoft.TestPlatform.CommunicationUtilities/DataCollectionRequestHandler.cs
index 777230a217..63bae93f18 100644
--- a/src/Microsoft.TestPlatform.CommunicationUtilities/DataCollectionRequestHandler.cs
+++ b/src/Microsoft.TestPlatform.CommunicationUtilities/DataCollectionRequestHandler.cs
@@ -152,6 +152,14 @@ public static DataCollectionRequestHandler Create(
{
ValidateArg.NotNull(communicationManager, nameof(communicationManager));
ValidateArg.NotNull(messageSink, nameof(messageSink));
+ // TODO: The MessageSink and DataCollectionRequestHandler have circular dependency.
+ // Message sink is injected into this Create method and then into constructor
+ // and into the constructor of DataCollectionRequestHandler. Data collection manager
+ // is then assigned to .Instace (which unlike many other .Instance is not populated
+ // directly in that property, but is created here). And then MessageSink depends on
+ // the .Instance. This is a very complicated way of solving the circular dependency,
+ // and should be replaced by adding a property to Message and assigning it.
+ // .Instance can then be removed.
if (Instance == null)
{
lock (SyncObject)
diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/Interfaces/Communication/ConnectedEventArgs.cs b/src/Microsoft.TestPlatform.CommunicationUtilities/Interfaces/Communication/ConnectedEventArgs.cs
index 6ea4bbcd12..bf583737b6 100644
--- a/src/Microsoft.TestPlatform.CommunicationUtilities/Interfaces/Communication/ConnectedEventArgs.cs
+++ b/src/Microsoft.TestPlatform.CommunicationUtilities/Interfaces/Communication/ConnectedEventArgs.cs
@@ -15,6 +15,7 @@ public class ConnectedEventArgs : EventArgs
///
/// Initializes a new instance of the class.
///
+ // TODO: Do we need this constructor?
public ConnectedEventArgs()
{
}
diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/Interfaces/ICommunicationEndpoint.cs b/src/Microsoft.TestPlatform.CommunicationUtilities/Interfaces/ICommunicationEndpoint.cs
index 269ae7d8b0..0abaa266a8 100644
--- a/src/Microsoft.TestPlatform.CommunicationUtilities/Interfaces/ICommunicationEndpoint.cs
+++ b/src/Microsoft.TestPlatform.CommunicationUtilities/Interfaces/ICommunicationEndpoint.cs
@@ -15,7 +15,7 @@ public interface ICommunicationEndPoint
event EventHandler Connected;
///
- /// Event raised when an endPoint is disconnected.
+ /// Event raised when an endPoint is disconnected on failure. It should not be notified when we are just closing the connection after success.
///
event EventHandler Disconnected;
diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/Messages/Message.cs b/src/Microsoft.TestPlatform.CommunicationUtilities/Messages/Message.cs
index d8602e29a0..f339111a90 100644
--- a/src/Microsoft.TestPlatform.CommunicationUtilities/Messages/Message.cs
+++ b/src/Microsoft.TestPlatform.CommunicationUtilities/Messages/Message.cs
@@ -21,6 +21,9 @@ public class Message
///
/// Gets or sets the payload.
///
+ // TODO: Our public contract says that we should be able to communicate over JSON, but we should not be stopping ourselves from
+ // negotiating a different protocol. Or using a different serialization library than NewtonsoftJson. Check why this is published as JToken
+ // and not as a string.
public JToken Payload { get; set; }
///
@@ -29,6 +32,8 @@ public class Message
/// The .
public override string ToString()
{
+ // TODO: Review where this is used, we should avoid extensive serialization and deserialization,
+ // and this might be happening in multiple places that are not the edge of our process.
return "(" + MessageType + ") -> " + (Payload == null ? "null" : Payload.ToString(Formatting.Indented));
}
}
diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/SocketClient.cs b/src/Microsoft.TestPlatform.CommunicationUtilities/SocketClient.cs
index eb6bc7feff..a2a1120f57 100644
--- a/src/Microsoft.TestPlatform.CommunicationUtilities/SocketClient.cs
+++ b/src/Microsoft.TestPlatform.CommunicationUtilities/SocketClient.cs
@@ -96,17 +96,17 @@ private void OnServerConnected(Task connectAsyncTask)
// Start the message loop
Task.Run(() => _tcpClient.MessageLoopAsync(
_channel,
- Stop,
+ StopOnError,
_cancellation.Token))
.ConfigureAwait(false);
}
}
}
- private void Stop(Exception error)
+ private void StopOnError(Exception error)
{
EqtTrace.Info("SocketClient.PrivateStop: Stop communication from server endpoint: {0}, error:{1}", _endPoint, error);
-
+ // This is here to prevent stack overflow.
if (!_stopped)
{
// Do not allow stop to be called multiple times.
diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/SocketServer.cs b/src/Microsoft.TestPlatform.CommunicationUtilities/SocketServer.cs
index c990b6ffcd..afdc029774 100644
--- a/src/Microsoft.TestPlatform.CommunicationUtilities/SocketServer.cs
+++ b/src/Microsoft.TestPlatform.CommunicationUtilities/SocketServer.cs
@@ -102,11 +102,16 @@ private void OnClientConnected(TcpClient client)
EqtTrace.Verbose("SocketServer.OnClientConnected: Client connected for endPoint: {0}, starting MessageLoopAsync:", _endPoint);
// Start the message loop
- Task.Run(() => _tcpClient.MessageLoopAsync(_channel, error => Stop(error), _cancellation.Token)).ConfigureAwait(false);
+ Task.Run(() => _tcpClient.MessageLoopAsync(_channel, error => StopOnError(error), _cancellation.Token)).ConfigureAwait(false);
}
}
- private void Stop(Exception error)
+ ///
+ /// Stop the connection when error was encountered. Dispose all communication, and notify subscribers of Disconnected event
+ /// that we aborted.
+ ///
+ ///
+ private void StopOnError(Exception error)
{
EqtTrace.Info("SocketServer.PrivateStop: Stopping server endPoint: {0} error: {1}", _endPoint, error);
diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/TestRequestSender.cs b/src/Microsoft.TestPlatform.CommunicationUtilities/TestRequestSender.cs
index ff818b46f5..e5c78bb33f 100644
--- a/src/Microsoft.TestPlatform.CommunicationUtilities/TestRequestSender.cs
+++ b/src/Microsoft.TestPlatform.CommunicationUtilities/TestRequestSender.cs
@@ -39,7 +39,7 @@ public class TestRequestSender : ITestRequestSender
private readonly int _clientExitedWaitTime;
- private ICommunicationEndPoint _communicationEndpoint;
+ private readonly ICommunicationEndPoint _communicationEndpoint;
private ICommunicationChannel _channel;
@@ -80,7 +80,6 @@ public TestRequestSender(ProtocolConfig protocolConfig, ITestRuntimeProvider run
protocolConfig,
ClientProcessExitWaitTimeout)
{
- SetCommunicationEndPoint();
}
internal TestRequestSender(
@@ -101,7 +100,14 @@ internal TestRequestSender(
// The connectionInfo here is that of RuntimeProvider, so reverse the role of runner.
_runtimeProvider = runtimeProvider;
- _communicationEndpoint = communicationEndPoint;
+
+ // TODO: In various places TestRequest sender is instantiated, and we can't easily inject the factory, so this is last
+ // resort of getting the dependency into the execution flow.
+ _communicationEndpoint = communicationEndPoint
+#if DEBUG
+ ?? TestServiceLocator.Get(connectionInfo.Endpoint)
+#endif
+ ?? SetCommunicationEndPoint();
_connectionInfo.Endpoint = connectionInfo.Endpoint;
_connectionInfo.Role = connectionInfo.Role == ConnectionRole.Host
? ConnectionRole.Client
@@ -145,6 +151,12 @@ public int InitializeCommunication()
_communicationEndpoint.Connected += (sender, args) =>
{
_channel = args.Channel;
+ // TODO: I suspect that Channel can be null only because of some unit tests,
+ // and being connected and actually not setting any channel should be error
+ // rather than silently waiting for timeout
+ // TODO: also this event is called back on connected, why are the event args holding
+ // the Connected boolean and why do we check it here. If we did not connect we should
+ // have not fired this event.
if (args.Connected && _channel != null)
{
_connected.Set();
@@ -158,6 +170,7 @@ public int InitializeCommunication()
// Server start returns the listener port
// return int.Parse(this.communicationServer.Start());
var endpoint = _communicationEndpoint.Start(_connectionInfo.Endpoint);
+ // TODO: This is forcing us to use IP address and port for communication
return endpoint.GetIpEndPoint().Port;
}
@@ -183,7 +196,8 @@ public void CheckVersionWithTestHost()
{
// Negotiation follows these steps:
// Runner sends highest supported version to Test host
- // Test host sends the version it can support (must be less than highest) to runner
+ // Test host compares the version with the highest version it can support.
+ // Test host sends back the lower number of the two. So the highest protocol version, that both sides support is used.
// Error case: test host can send a protocol error if it cannot find a supported version
var protocolNegotiated = new ManualResetEvent(false);
_onMessageReceived = (sender, args) =>
@@ -530,7 +544,9 @@ private void OnExecutionMessageReceived(object sender, MessageReceivedEventArgs
}
catch (Exception exception)
{
- OnTestRunAbort(testRunEventsHandler, exception, false);
+ // If we failed to process the incoming message, initiate client (testhost) abort, because we can't recover, and don't wait
+ // for it to exit and write into error stream, because it did not do anything wrong, so no error is coming there
+ OnTestRunAbort(testRunEventsHandler, exception, getClientError: false);
}
}
@@ -640,20 +656,25 @@ private void OnDiscoveryAbort(ITestDiscoveryEventsHandler2 eventHandler, Excepti
private string GetAbortErrorMessage(Exception exception, bool getClientError)
{
+
EqtTrace.Verbose("TestRequestSender: GetAbortErrorMessage: Exception: " + exception);
// It is also possible for an operation to abort even if client has not
- // disconnected, e.g. if there's an error parsing the response from test host. We
- // want the exception to be available in those scenarios.
+ // disconnected, because we initiate client abort when there is error in processing incoming messages.
+ // in this case, we will use the exception as the failure result, if it is present. Otherwise we will
+ // try to wait for the client process to exit, and capture it's error output (we are listening to it's standard and
+ // error output in the ClientExited callback).
var reason = exception?.Message;
if (getClientError)
{
EqtTrace.Verbose("TestRequestSender: GetAbortErrorMessage: Client has disconnected. Wait for standard error.");
// Wait for test host to exit for a moment
+ // TODO: this timeout is 10 seconds, make it also configurable like the other famous timeout that is 100ms
if (_clientExited.Wait(_clientExitedWaitTime))
{
- // Set a default message of test host process exited and additionally specify the error if present
+ // Set a default message of test host process exited and additionally specify the error if we were able to get it
+ // from error output of the process
EqtTrace.Info("TestRequestSender: GetAbortErrorMessage: Received test host error message.");
reason = CommonResources.TestHostProcessCrashed;
if (!string.IsNullOrWhiteSpace(_clientExitErrorMessage))
@@ -712,18 +733,18 @@ private void SetOperationComplete()
Interlocked.CompareExchange(ref _operationCompleted, 1, 0);
}
- private void SetCommunicationEndPoint()
+ private ICommunicationEndPoint SetCommunicationEndPoint()
{
// TODO: Use factory to get the communication endpoint. It will abstract out the type of communication endpoint like socket, shared memory or named pipe etc.,
if (_connectionInfo.Role == ConnectionRole.Client)
{
- _communicationEndpoint = new SocketClient();
EqtTrace.Verbose("TestRequestSender is acting as client.");
+ return new SocketClient();
}
else
{
- _communicationEndpoint = new SocketServer();
EqtTrace.Verbose("TestRequestSender is acting as server.");
+ return new SocketServer();
}
}
}
diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Client/Parallel/ParallelRunDataAggregator.cs b/src/Microsoft.TestPlatform.CrossPlatEngine/Client/Parallel/ParallelRunDataAggregator.cs
index 960afeb051..6a7e86d2c3 100644
--- a/src/Microsoft.TestPlatform.CrossPlatEngine/Client/Parallel/ParallelRunDataAggregator.cs
+++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Client/Parallel/ParallelRunDataAggregator.cs
@@ -72,6 +72,7 @@ public ITestRunStatistics GetAggregatedRunStats()
{
foreach (var runStats in _testRunStatsList)
{
+ // TODO: we get nullref here if the stats are empty.
foreach (var outcome in runStats.Stats.Keys)
{
if (!testOutcomeMap.ContainsKey(outcome))
diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Client/ProxyExecutionManager.cs b/src/Microsoft.TestPlatform.CrossPlatEngine/Client/ProxyExecutionManager.cs
index 772b079a07..47886ec36b 100644
--- a/src/Microsoft.TestPlatform.CrossPlatEngine/Client/ProxyExecutionManager.cs
+++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Client/ProxyExecutionManager.cs
@@ -359,6 +359,9 @@ public void HandleTestRunStatsChange(TestRunChangedEventArgs testRunChangedArgs)
///
public void HandleRawMessage(string rawMessage)
{
+ // TODO: PERF: - why do we have to deserialize the messages here only to read that this is
+ // execution complete? Why can't we act on it somewhere else where the result of deserialization is not
+ // thrown away?
var message = _dataSerializer.DeserializeMessage(rawMessage);
if (string.Equals(message.MessageType, MessageType.ExecutionComplete))
diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Client/ProxyOperationManager.cs b/src/Microsoft.TestPlatform.CrossPlatEngine/Client/ProxyOperationManager.cs
index 530aca8b9c..88f0c020aa 100644
--- a/src/Microsoft.TestPlatform.CrossPlatEngine/Client/ProxyOperationManager.cs
+++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Client/ProxyOperationManager.cs
@@ -324,8 +324,16 @@ public virtual void Close()
{
_initialized = false;
- // Please clean up test host.
- TestHostManager.CleanTestHostAsync(CancellationToken.None).Wait();
+ // This is calling external code, make sure we don't fail when it throws
+ try
+ {
+ // Please clean up test host.
+ TestHostManager.CleanTestHostAsync(CancellationToken.None).Wait();
+ }
+ catch (Exception ex)
+ {
+ EqtTrace.Error($"ProxyOperationManager: Cleaning testhost failed: {ex}");
+ }
TestHostManager.HostExited -= TestHostManagerHostExited;
TestHostManager.HostLaunched -= TestHostManagerHostLaunched;
@@ -407,6 +415,9 @@ private void CompatIssueWithVersionCheckAndRunsettings()
{
var properties = TestHostManager.GetType().GetRuntimeProperties();
+ // The field is actually defaulting to true, so this is just a complicated way to set or not set
+ // this to true (modern testhosts should have it set to true). Bad thing about this is that we are checking
+ // internal "undocumented" property. Good thing is that if you don't implement it you get the modern behavior.
var versionCheckProperty = properties.FirstOrDefault(p => string.Equals(p.Name, _versionCheckPropertyName, StringComparison.OrdinalIgnoreCase));
if (versionCheckProperty != null)
{
diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Friends.cs b/src/Microsoft.TestPlatform.CrossPlatEngine/Friends.cs
index b52f4adf02..25f5726567 100644
--- a/src/Microsoft.TestPlatform.CrossPlatEngine/Friends.cs
+++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Friends.cs
@@ -11,5 +11,6 @@
[assembly: InternalsVisibleTo("testhost, PublicKey = 002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")]
[assembly: InternalsVisibleTo("testhost.x86, PublicKey = 002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")]
[assembly: InternalsVisibleTo("vstest.console, PublicKey = 002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")]
+[assembly: InternalsVisibleTo("vstest.ProgrammerTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/TestEngine.cs b/src/Microsoft.TestPlatform.CrossPlatEngine/TestEngine.cs
index e8ea9e69c3..d8d5373ef8 100644
--- a/src/Microsoft.TestPlatform.CrossPlatEngine/TestEngine.cs
+++ b/src/Microsoft.TestPlatform.CrossPlatEngine/TestEngine.cs
@@ -34,7 +34,7 @@ namespace Microsoft.VisualStudio.TestPlatform.CrossPlatEngine;
///
public class TestEngine : ITestEngine
{
- private readonly TestRuntimeProviderManager _testHostProviderManager;
+ private readonly ITestRuntimeProviderManager _testHostProviderManager;
private ITestExtensionManager _testExtensionManager;
private readonly IProcessHelper _processHelper;
@@ -42,8 +42,14 @@ public class TestEngine : ITestEngine
{
}
- protected TestEngine(
+ protected internal TestEngine(
TestRuntimeProviderManager testHostProviderManager,
+ IProcessHelper processHelper) : this((ITestRuntimeProviderManager) testHostProviderManager, processHelper)
+ {
+ }
+
+ internal TestEngine(
+ ITestRuntimeProviderManager testHostProviderManager,
IProcessHelper processHelper)
{
_testHostProviderManager = testHostProviderManager;
diff --git a/src/Microsoft.TestPlatform.ObjectModel/Client/TestRunCriteria.cs b/src/Microsoft.TestPlatform.ObjectModel/Client/TestRunCriteria.cs
index 76fce94c04..4db76fbc4c 100644
--- a/src/Microsoft.TestPlatform.ObjectModel/Client/TestRunCriteria.cs
+++ b/src/Microsoft.TestPlatform.ObjectModel/Client/TestRunCriteria.cs
@@ -482,7 +482,7 @@ public TestRunCriteria(
runStatsChangeEventTimeout,
testHostLauncher)
{
- var testCases = tests as IList ?? tests.ToList();
+ var testCases = tests as IList ?? tests?.ToList();
ValidateArg.NotNullOrEmpty(testCases, nameof(tests));
Tests = testCases;
diff --git a/src/Microsoft.TestPlatform.ObjectModel/TestServiceLocator.cs b/src/Microsoft.TestPlatform.ObjectModel/TestServiceLocator.cs
new file mode 100644
index 0000000000..6264588d08
--- /dev/null
+++ b/src/Microsoft.TestPlatform.ObjectModel/TestServiceLocator.cs
@@ -0,0 +1,66 @@
+// 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.ObjectModel;
+
+// We don't want this in our shipped code. Build only for debug until I am able to remove it.
+#if DEBUG
+
+#if !NETSTANDARD1_0
+using System;
+#endif
+using System.Collections.Generic;
+
+#pragma warning disable RS0016 // Add public types and members to the declared API
+#pragma warning disable RS0037 // Enable tracking of nullability of reference types in the declared API
+
+public static class TestServiceLocator
+{
+ public static Dictionary Instances { get; } = new Dictionary();
+ public static List Resolves { get; } = new();
+
+ public static void Register(string name, TRegistration instance) where TRegistration : notnull
+ {
+ Instances.Add(name, instance);
+ }
+
+ public static TRegistration? Get(string name)
+ {
+ if (!Instances.TryGetValue(name, out var instance))
+ {
+ return default;
+ // TODO: Add enable flag for the whole provider to activate so I can leverage throwing in programmer tests, but not run into it in Playground, or other debug builds.
+ // throw new InvalidOperationException($"Cannot find an instance for name {name}.");
+ }
+
+#if !NETSTANDARD1_0
+ Resolves.Add(new Resolve(name, typeof(TRegistration).FullName, Environment.StackTrace));
+#endif
+ return (TRegistration)instance;
+ }
+
+ public static void Clear()
+ {
+ Instances.Clear();
+ Resolves.Clear();
+ }
+}
+
+// TODO: Make this internal, I am just trying to have easier time trying this out.
+public class Resolve
+{
+ public Resolve(string name, string type, string stackTrace)
+ {
+ Name = name;
+ Type = type;
+ StackTrace = stackTrace;
+ }
+
+ public string Name { get; }
+ public string Type { get; }
+ public string StackTrace { get; }
+}
+
+#pragma warning restore RS0037 // Enable tracking of nullability of reference types in the declared API
+#pragma warning restore RS0016 // Add public types and members to the declared API
+#endif
diff --git a/src/Microsoft.TestPlatform.Utilities/InferRunSettingsHelper.cs b/src/Microsoft.TestPlatform.Utilities/InferRunSettingsHelper.cs
index e65aef5e0a..4e7e37a311 100644
--- a/src/Microsoft.TestPlatform.Utilities/InferRunSettingsHelper.cs
+++ b/src/Microsoft.TestPlatform.Utilities/InferRunSettingsHelper.cs
@@ -499,6 +499,7 @@ private static void AddNodeIfNotPresent(XmlDocument xmlDocument, string nodeP
if (root.SelectSingleNode(RunConfigurationNodePath) == null)
{
+ // TODO: When runsettings are incomplete this will silently return, when we run just TestRequestManager we don't get full settings.
EqtTrace.Error("InferRunSettingsHelper.UpdateNodeIfNotPresent: Unable to navigate to RunConfiguration. Current node: " + xmlDocument.LocalName);
return;
}
diff --git a/src/vstest.console/CommandLine/TestRunResultAggregator.cs b/src/vstest.console/CommandLine/TestRunResultAggregator.cs
index 47d6784a29..05b0505558 100644
--- a/src/vstest.console/CommandLine/TestRunResultAggregator.cs
+++ b/src/vstest.console/CommandLine/TestRunResultAggregator.cs
@@ -20,7 +20,7 @@ internal class TestRunResultAggregator
/// Initializes the TestRunResultAggregator
///
/// Constructor is private since the factory method should be used to get the instance.
- protected TestRunResultAggregator()
+ protected internal TestRunResultAggregator()
{
// Outcome is passed until we see a failure.
Outcome = TestOutcome.Passed;
diff --git a/src/vstest.console/Friends.cs b/src/vstest.console/Friends.cs
index 9b2a48fabe..9dd880e408 100644
--- a/src/vstest.console/Friends.cs
+++ b/src/vstest.console/Friends.cs
@@ -10,6 +10,7 @@
#region Test Assemblies
[assembly: InternalsVisibleTo("vstest.console.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")]
+[assembly: InternalsVisibleTo("vstest.ProgrammerTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")]
[assembly: InternalsVisibleTo("vstest.console.PlatformTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
diff --git a/src/vstest.console/Processors/ArtifactProcessingCollectModeProcessor.cs b/src/vstest.console/Processors/ArtifactProcessingCollectModeProcessor.cs
index b085ba3637..f651207cb5 100644
--- a/src/vstest.console/Processors/ArtifactProcessingCollectModeProcessor.cs
+++ b/src/vstest.console/Processors/ArtifactProcessingCollectModeProcessor.cs
@@ -91,9 +91,9 @@ internal class ArtifactProcessingCollectModeProcessorExecutor : IArgumentExecuto
{
private readonly CommandLineOptions _commandLineOptions;
- public ArtifactProcessingCollectModeProcessorExecutor(CommandLineOptions options)
+ public ArtifactProcessingCollectModeProcessorExecutor(CommandLineOptions options!!)
{
- _commandLineOptions = options ?? throw new ArgumentNullException(nameof(options));
+ _commandLineOptions = options;
}
public void Initialize(string argument)
diff --git a/src/vstest.console/Processors/ArtifactProcessingPostProcessModeProcessor.cs b/src/vstest.console/Processors/ArtifactProcessingPostProcessModeProcessor.cs
index 1693e0d058..65eee5d2b4 100644
--- a/src/vstest.console/Processors/ArtifactProcessingPostProcessModeProcessor.cs
+++ b/src/vstest.console/Processors/ArtifactProcessingPostProcessModeProcessor.cs
@@ -95,10 +95,10 @@ internal class ArtifactProcessingPostProcessModeProcessorExecutor : IArgumentExe
private readonly CommandLineOptions _commandLineOptions;
private readonly IArtifactProcessingManager _artifactProcessingManage;
- public ArtifactProcessingPostProcessModeProcessorExecutor(CommandLineOptions options, IArtifactProcessingManager artifactProcessingManager)
+ public ArtifactProcessingPostProcessModeProcessorExecutor(CommandLineOptions options!!, IArtifactProcessingManager artifactProcessingManager!!)
{
- _commandLineOptions = options ?? throw new ArgumentNullException(nameof(options));
- _artifactProcessingManage = artifactProcessingManager ?? throw new ArgumentNullException(nameof(artifactProcessingManager)); ;
+ _commandLineOptions = options;
+ _artifactProcessingManage = artifactProcessingManager; ;
}
public void Initialize(string argument)
diff --git a/src/vstest.console/Processors/TestSessionCorrelationIdProcessor.cs b/src/vstest.console/Processors/TestSessionCorrelationIdProcessor.cs
index 053ce3f9d5..52b00e8273 100644
--- a/src/vstest.console/Processors/TestSessionCorrelationIdProcessor.cs
+++ b/src/vstest.console/Processors/TestSessionCorrelationIdProcessor.cs
@@ -86,9 +86,9 @@ internal class TestSessionCorrelationIdProcessorModeProcessorExecutor : IArgumen
{
private readonly CommandLineOptions _commandLineOptions;
- public TestSessionCorrelationIdProcessorModeProcessorExecutor(CommandLineOptions options)
+ public TestSessionCorrelationIdProcessorModeProcessorExecutor(CommandLineOptions options!!)
{
- _commandLineOptions = options ?? throw new ArgumentNullException(nameof(options));
+ _commandLineOptions = options;
}
public void Initialize(string argument)
diff --git a/src/vstest.console/TestPlatformHelpers/TestRequestManager.cs b/src/vstest.console/TestPlatformHelpers/TestRequestManager.cs
index dc0f43cde7..0888ad1e9b 100644
--- a/src/vstest.console/TestPlatformHelpers/TestRequestManager.cs
+++ b/src/vstest.console/TestPlatformHelpers/TestRequestManager.cs
@@ -52,6 +52,8 @@ internal class TestRequestManager : ITestRequestManager
private readonly ITestPlatform _testPlatform;
private readonly ITestPlatformEventSource _testPlatformEventSource;
+ // TODO: No idea what is Task supposed to buy us, Tasks start immediately on instantiation
+ // and the work done to produce the metrics publisher is minimal.
private readonly Task _metricsPublisher;
private readonly object _syncObject = new();
diff --git a/test/Intent.Primitives/ExcludeAttribute.cs b/test/Intent.Primitives/ExcludeAttribute.cs
new file mode 100644
index 0000000000..4fe517e902
--- /dev/null
+++ b/test/Intent.Primitives/ExcludeAttribute.cs
@@ -0,0 +1,9 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Intent;
+
+[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Method)]
+public class ExcludeAttribute : Attribute
+{
+}
diff --git a/test/Intent.Primitives/IRunLogger.cs b/test/Intent.Primitives/IRunLogger.cs
new file mode 100644
index 0000000000..474369e78c
--- /dev/null
+++ b/test/Intent.Primitives/IRunLogger.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Intent;
+
+using System.Reflection;
+
+public interface IRunLogger
+{
+ void WriteTestPassed(MethodInfo m);
+ void WriteTestInconclusive(MethodInfo m);
+ void WriteTestFailure(MethodInfo m, Exception ex);
+ void WriteFrameworkError(Exception ex);
+}
diff --git a/test/Intent.Primitives/Intent.Primitives.csproj b/test/Intent.Primitives/Intent.Primitives.csproj
new file mode 100644
index 0000000000..93ec3f6e75
--- /dev/null
+++ b/test/Intent.Primitives/Intent.Primitives.csproj
@@ -0,0 +1,13 @@
+
+
+
+ $(MSBuildThisFileDirectory)..\..\
+ net6.0
+ enable
+ enable
+ True
+ $(TestPlatformRoot)\scripts\key.snk
+ True
+
+
+
diff --git a/test/Intent.Primitives/OnlyAttribute.cs b/test/Intent.Primitives/OnlyAttribute.cs
new file mode 100644
index 0000000000..3d327e1f55
--- /dev/null
+++ b/test/Intent.Primitives/OnlyAttribute.cs
@@ -0,0 +1,9 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Intent;
+
+[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Method)]
+public class OnlyAttribute : Attribute
+{
+}
diff --git a/test/Intent.Primitives/TestResult.cs b/test/Intent.Primitives/TestResult.cs
new file mode 100644
index 0000000000..053167718f
--- /dev/null
+++ b/test/Intent.Primitives/TestResult.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Intent;
+
+public enum TestResult
+{
+ None,
+ Passed,
+ Failed,
+ Error,
+}
diff --git a/test/Intent/ConsoleLogger.cs b/test/Intent/ConsoleLogger.cs
new file mode 100644
index 0000000000..b41c484532
--- /dev/null
+++ b/test/Intent/ConsoleLogger.cs
@@ -0,0 +1,57 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Intent.Console;
+using System.Reflection;
+using System.Text.RegularExpressions;
+
+using static System.Console;
+using static System.ConsoleColor;
+
+internal class ConsoleLogger : IRunLogger
+{
+ public void WriteTestInconclusive(MethodInfo m)
+ {
+ var currentColor = ForegroundColor;
+ ForegroundColor = Yellow;
+ WriteLine($"[?] {FormatMethodName(m.Name)}");
+ ForegroundColor = currentColor;
+ }
+
+ public void WriteTestPassed(MethodInfo m)
+ {
+ var currentColor = ForegroundColor;
+ ForegroundColor = Green;
+ WriteLine($"[+] {FormatMethodName(m.Name)}");
+ ForegroundColor = currentColor;
+ }
+
+ public void WriteTestFailure(MethodInfo m, Exception ex)
+ {
+ var currentColor = ForegroundColor;
+ ForegroundColor = Red;
+ WriteLine($"[-] {FormatMethodName(m.Name)}{Environment.NewLine}{ex}");
+ ForegroundColor = currentColor;
+ }
+
+ public void WriteFrameworkError(Exception ex)
+ {
+ var currentColor = ForegroundColor;
+ ForegroundColor = DarkRed;
+ WriteLine($"[-] framework failed{Environment.NewLine}{ex}{Environment.NewLine}{Environment.NewLine}");
+ ForegroundColor = currentColor;
+ }
+
+ private static string FormatMethodName(string methodName)
+ {
+ var noUnderscores = methodName.Replace('_', ' ');
+ // insert space before every capital letter or number that is after a non-capital letter
+ var spaced = Regex.Replace(noUnderscores, "(?<=[a-z])([A-Z0-9])", " $1");
+ // insert space before every capital leter that is after a number
+ var spaced2 = Regex.Replace(spaced, "(?<=[0-9]|^)([A-Z])", " $1");
+ var newLines = spaced2.Replace("When", $"{Environment.NewLine} When")
+ .Replace("Then", $"{Environment.NewLine} Then");
+
+ return newLines.ToLowerInvariant();
+ }
+}
diff --git a/test/Intent/Extensions.cs b/test/Intent/Extensions.cs
new file mode 100644
index 0000000000..78a9bef318
--- /dev/null
+++ b/test/Intent/Extensions.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Intent;
+
+using System.Reflection;
+
+public static class Extensions
+{
+ public static bool IsExcluded(this Assembly asm)
+ {
+ return asm.CustomAttributes.Any(a => a.AttributeType == typeof(ExcludeAttribute));
+ }
+
+ public static List SkipExcluded(this IEnumerable e)
+ {
+ return e.Where(i => i.GetCustomAttribute() == null).ToList();
+ }
+
+ public static List SkipNonPublic(this IEnumerable e)
+ {
+ return e.Where(i => i.IsPublic).ToList();
+ }
+
+ public static List SkipExcluded(this IEnumerable e)
+ {
+ return e.Where(i =>
+ i.Name != nameof(object.ToString)
+ && i.Name != nameof(object.GetType)
+ && i.Name != nameof(object.GetHashCode)
+ && i.Name != nameof(object.Equals)
+ && i.GetCustomAttribute() == null).ToList();
+ }
+}
diff --git a/test/Intent/Intent.csproj b/test/Intent/Intent.csproj
new file mode 100644
index 0000000000..97b464df79
--- /dev/null
+++ b/test/Intent/Intent.csproj
@@ -0,0 +1,18 @@
+
+
+
+ $(MSBuildThisFileDirectory)..\..\
+ Exe
+ net6.0
+ enable
+ enable
+ True
+ True
+ $(TestPlatformRoot)\scripts\key.snk
+
+
+
+
+
+
+
diff --git a/test/Intent/Program.cs b/test/Intent/Program.cs
new file mode 100644
index 0000000000..75d50702b1
--- /dev/null
+++ b/test/Intent/Program.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Intent.Console;
+
+public class Program
+{
+ public static void Main(string[] path)
+ {
+ Runner.Run(path, new ConsoleLogger());
+ }
+}
diff --git a/test/Intent/Runner.cs b/test/Intent/Runner.cs
new file mode 100644
index 0000000000..3bab3db2c7
--- /dev/null
+++ b/test/Intent/Runner.cs
@@ -0,0 +1,67 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Intent;
+
+using System.Reflection;
+
+public class Runner
+{
+ public static void Run(IEnumerable paths, IRunLogger logger)
+ {
+ foreach (var path in paths)
+ {
+ try
+ {
+ var assembly = Assembly.LoadFrom(path);
+ if (assembly.IsExcluded())
+ continue;
+
+ var types = assembly.GetTypes().SkipNonPublic().SkipExcluded();
+ foreach (var type in types)
+ {
+ var methods = type.GetMethods().SkipExcluded();
+
+ // TODO: This chooses the Only tests only for single assembly and single class,
+ // to support this full we would have to enumerate all classes and methods first,
+ // it is easy, I just don't need it right now.
+ var methodsWithOnly = methods.Where(m => m.GetCustomAttribute() != null).ToList();
+ if (methodsWithOnly.Count > 0)
+ methods = methodsWithOnly;
+
+ foreach (var method in methods)
+ {
+ try
+ {
+ var instance = Activator.CreateInstance(type);
+ var testResult = method.Invoke(instance, Array.Empty