Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Document IDataConsumer #2812

Merged
merged 2 commits into from
May 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/testingplatform/Index.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
1. In-process
1. [ITestSessionLifetimeHandler](itestsessionlifetimehandler.md)
1. [ITestApplicationLifecycleCallbacks](itestapplicationlifecyclecallbacks.md)
1. [IDataConsumer](idataconsumer.md)
1. Services
1. [IServiceProvider](iserviceprovider.md)
1. [IConfiguration](configuration.md)
Expand Down
Original file line number Diff line number Diff line change
@@ -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 Microsoft.Testing.Platform.Extensions.Messages;
using Microsoft.Testing.Platform.Extensions.OutputDevice;
using Microsoft.Testing.Platform.Extensions.TestHost;
using Microsoft.Testing.Platform.OutputDevice;

namespace TestingPlatformExplorer.In_process_extensions;
internal class DisplayDataConsumer : IDataConsumer, IOutputDeviceDataProducer
{
private readonly IOutputDevice _outputDevice;

public Type[] DataTypesConsumed => new[] { typeof(TestNodeUpdateMessage) };

public string Uid => nameof(DisplayDataConsumer);

public string Version => "1.0.0";

public string DisplayName => nameof(DisplayDataConsumer);

public string Description => "This extension display in console the testnode id and display name of TestNodeUpdateMessage data type.";

public DisplayDataConsumer(IOutputDevice outputDevice)
{
_outputDevice = outputDevice;
}

public Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationToken cancellationToken)
{
var testNodeUpdateMessage = (TestNodeUpdateMessage)value;
string testNodeDisplayName = testNodeUpdateMessage.TestNode.DisplayName;
TestNodeUid testNodeId = testNodeUpdateMessage.TestNode.Uid;

TestNodeStateProperty nodeState = testNodeUpdateMessage.TestNode.Properties.Single<TestNodeStateProperty>();

switch (nodeState)
{
case InProgressTestNodeStateProperty _:
{
_outputDevice.DisplayAsync(this, new FormattedTextOutputDeviceData($"[DisplayDataConsumer]TestNode '{testNodeId}' with display name '{testNodeDisplayName}' is in progress")
{
ForegroundColor = new SystemConsoleColor() { ConsoleColor = ConsoleColor.Green }
});
break;
}
case PassedTestNodeStateProperty _:
{
_outputDevice.DisplayAsync(this, new FormattedTextOutputDeviceData($"[DisplayDataConsumer]TestNode '{testNodeId}' with display name '{testNodeDisplayName}' is completed")
{
ForegroundColor = new SystemConsoleColor() { ConsoleColor = ConsoleColor.Green }
});
break;
}
case FailedTestNodeStateProperty failedTestNodeStateProperty:
{
_outputDevice.DisplayAsync(this, new FormattedTextOutputDeviceData($"[DisplayDataConsumer]TestNode '{testNodeId}' with display name '{testNodeDisplayName}' is failed with '{failedTestNodeStateProperty?.Exception?.Message}'")
{
ForegroundColor = new SystemConsoleColor() { ConsoleColor = ConsoleColor.Red }
});
break;
}
case SkippedTestNodeStateProperty _:
{
_outputDevice.DisplayAsync(this, new FormattedTextOutputDeviceData($"[DisplayDataConsumer]TestNode '{testNodeId}' with display name '{testNodeDisplayName}' is skipped")
{
ForegroundColor = new SystemConsoleColor() { ConsoleColor = ConsoleColor.White }
});
break;
}
default:
break;
}

return Task.CompletedTask;
}
public Task<bool> IsEnabledAsync() => Task.FromResult(true);
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
=> new DisplayTestApplicationLifecycleCallbacks(serviceProvider.GetOutputDevice()));
testApplicationBuilder.TestHost.AddTestSessionLifetimeHandle(serviceProvider
=> new DisplayTestSessionLifeTimeHandler(serviceProvider.GetOutputDevice()));
testApplicationBuilder.TestHost.AddDataConsumer(serviceProvider
=> new DisplayDataConsumer(serviceProvider.GetOutputDevice()));

using ITestApplication testApplication = await testApplicationBuilder.BuildAsync();
return await testApplication.RunAsync();
92 changes: 90 additions & 2 deletions docs/testingplatform/idataconsumer.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,91 @@
# IDataConsumer
# The `IDataConsumer`

Soon.
The `IDataConsumer` is an *in-process* extension capable of subscribing to and receiving `IData` information that is pushed to the [IMessageBus](imessagebus.md) by the [testing framework](itestframework.md) and its extensions.

*This extension point is crucial as it enables developers to gather and process all the information generated during a test session.*

To register a custom `IDataConsumer`, utilize the following api:

```cs
ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args);
...
testApplicationBuilder.TestHost.AddDataConsumer(serviceProvider
=> new CustomDataConsumer());
...
```

The factory utilizes the [IServiceProvider](iserviceprovider.md) to gain access to the suite of services offered by the testing platform.

>> [!IMPORTANT]
>> The sequence of registration is significant, as the APIs are called in the order they were registered.

The `IDataConsumer` interface includes the following methods:

```cs
public interface IDataConsumer : ITestHostExtension
{
Type[] DataTypesConsumed { get; }
Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationToken cancellationToken);
}

public interface IData
{
string DisplayName { get; }
string? Description { get; }
}
```

The `IDataConsumer` is a type of `ITestHostExtension`, which serves as a base for all *test host* extensions. Like all other extension points, it also inherits from [IExtension](iextension.md). Therefore, like any other extension, you can choose to enable or disable it using the `IExtension.IsEnabledAsync` API.

`DataTypesConsumed`: This property returns a list of `Type` that this extension plans to consume. It corresponds to `IDataProducer.DataTypesProduced`. Notably, an `IDataConsumer` can subscribe to multiple types originating from different `IDataProducer` instances without any issues.

`ConsumeAsync`: This method is triggered whenever data of a type to which the current consumer is subscribed is pushed onto the [`IMessageBus`](imessagebus.md). It receives the `IDataProducer` to provide details about the data payload's producer, as well as the `IData` payload itself. As you can see, `IData` is a generic placeholder interface that contains general informative data. The ability to push different types of `IData` implies that the consumer needs to *switch* on the type itself to cast it to the correct type and access the specific information.

A sample implementation of a consumer that wants to elaborate the [`TestNodeUpdateMessage`](testnodeupdatemessage.md) produced by a [testing framework](itestframework.md) could be:

```cs
internal class CustomDataConsumer : IDataConsumer, IOutputDeviceDataProducer
{
public Type[] DataTypesConsumed => new[] { typeof(TestNodeUpdateMessage) };
...
public Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationToken cancellationToken)
{
var testNodeUpdateMessage = (TestNodeUpdateMessage)value;

TestNodeStateProperty nodeState = testNodeUpdateMessage.TestNode.Properties.Single<TestNodeStateProperty>();

switch (nodeState)
{
case InProgressTestNodeStateProperty _:
{
...
break;
}
case PassedTestNodeStateProperty _:
{
...
break;
}
case FailedTestNodeStateProperty failedTestNodeStateProperty:
{
...
break;
}
case SkippedTestNodeStateProperty _:
{
...
break;
}
...
}

return Task.CompletedTask;
}
...
}
```

Finally, the api takes a `CancellationToken` which the extension is expected to honor.

> [!IMPORTANT]
> It's crucial to process the payload directly within the `ConsumeAsync` method. The [IMessageBus](imessagebus.md) can manage both synchronous and asynchronous processing, coordinating the execution with the [testing framework](itestframework.md). Although the consumption process is entirely asynchronous and doesn't block the [IMessageBus.Push](imessagebus.md) at the time of writing, this is an implementation detail that may change in the future due to feature requirements. However, we aim to maintain this interface's simplicity and ensure that this method is always called once, eliminating the need for complex synchronization. Additionally, we automatically manage the scalability of the consumers.
3 changes: 3 additions & 0 deletions docs/testingplatform/imessagebus.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ Let's discuss the parameters:
This approach facilitates the evolution of the information exchange process, preventing breaking changes when an extension is unfamiliar with new data. **It allows different versions of extensions and the test framework to operate in harmony, based on their mutual understanding**.

The opposite end of the bus is what we refer to as a [consumer](idataConsumer.md), which is subscribed to a specific type of data and can thus consume it.

>> [!IMPORTANT]
>> Always use *await* the call to `PublishAsync`. If you don't, the `IData` might not be processed correctly by the testing platform and extensions, which could lead to subtle bugs. It's only after you've returned from the *await* that you can be assured that the `IData` has been queued for processing on the message bus. Regardless of the extension point you're working on, ensure that you've awaited all `PublishAsync` calls before exiting the extension. For example, if you're implementing the [`testing framework`](itestframework.md), you should not call `Complete` on the [requests](irequest.md) until you've awaited all `PublishAsync` calls for that specific request.
14 changes: 8 additions & 6 deletions docs/testingplatform/irequest.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public class TestNode
public PropertyBag Properties { get; init; } = new();
}

public sealed class TestNodeUid(string value)
public sealed class TestNodeUid(string value)

public sealed partial class PropertyBag
{
Expand All @@ -35,7 +35,7 @@ public sealed partial class PropertyBag
public bool Any<TProperty>();
public TProperty? SingleOrDefault<TProperty>();
public TProperty Single<TProperty>();
public TProperty[] OfType<TProperty>();
public TProperty[] OfType<TProperty>();
public IEnumerable<IProperty> AsEnumerable();
public IEnumerator<IProperty> GetEnumerator();
...
Expand All @@ -50,17 +50,19 @@ public interface IProperty

* `TestNode`: The `TestNode` is composed of three properties, one of which is the `Uid` of type `TestNodeUid`. This `Uid` serves as the **UNIQUE STABLE ID** for the node. The term **UNIQUE STABLE ID** implies that the same `TestNode` should maintain an **IDENTICAL** `Uid` across different runs and operating systems. The `TestNodeUid` is an **arbitrary opaque string** that the testing platform accepts as is.

>> [!NOTE]
>> [!IMPORTANT]
>> The stability and uniqueness of the ID are crucial in the testing domain. They enable the precise targeting of a single test for execution and allow the ID to serve as a persistent identifier for a test, facilitating powerful extensions and features.

The second property is `DisplayName`, which is the human-friendly name for the test. For example, this name is displayed when you execute the `--list-tests` command line.

The third attribute is `Properties`, which is a `PropertyBag` type. As demonstrated in the code, this is a specialized property bag that holds generic properties about the `TestNode`. This implies that you can append any property to the node that implements the placeholder interface `IProperty`.
The third attribute is `Properties`, which is a `PropertyBag` type. As demonstrated in the code, this is a specialized property bag that holds generic properties about the `TestNodeUpdateMessage`. This implies that you can append any property to the node that implements the placeholder interface `IProperty`.

***The testing platform identifies specific properties added to a `TestNode` to determine whether a test has passed, failed, or been skipped.***
***The testing platform identifies specific properties added to a `TestNode.Properties` to determine whether a test has passed, failed, or been skipped.***

You can find the current list of available properties with the relative description in the section [TestNodeUpdateMessage.TestNode](testnodeupdatemessage.md)

The `PropertyBag` type is typically accessible in every `IData` and is utilized to store miscellaneous properties that can be queried by the platform and extensions. This mechanism allows us to enhance the platform with new information without introducing breaking changes. If a component recognizes the property, it can query it; otherwise, it will disregard it.

Finally this section makes clear that you test framework implementaion needs to implement the `IDataProducer` that produces `TestNodeUpdateMessage`s like in the sample below:

```cs
Expand Down Expand Up @@ -168,7 +170,7 @@ var failedTestNode = new TestNode()
Properties = new PropertyBag(new ErrorTestNodeStateProperty(ex.InnerException!)),
};

await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(runTestExecutionRequest.Session.SessionUid, failedTestNode));
await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(runTestExecutionRequest.Session.SessionUid, failedTestNode));
```

You can visit the [code sample](codesample.md) for a working execution sample.
Expand Down
2 changes: 1 addition & 1 deletion docs/testingplatform/itestapplicationlifecyclecallbacks.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ testApplicationBuilder.TestHost.AddTestApplicationLifecycleCallbacks(serviceProv

The factory utilizes the [IServiceProvider](iserviceprovider.md) to gain access to the suite of services offered by the testing platform.

>> [!NOTE]
>> [!IMPORTANT]
>> The sequence of registration is significant, as the APIs are called in the order they were registered.

The `ITestApplicationLifecycleCallbacks` interface includes the following methods:
Expand Down
2 changes: 1 addition & 1 deletion docs/testingplatform/itestsessionlifetimehandler.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ testApplicationBuilder.TestHost.AddTestSessionLifetimeHandle(serviceProvider =>

The factory utilizes the [IServiceProvider](iserviceprovider.md) to gain access to the suite of services offered by the testing platform.

>> [!NOTE]
>> [!IMPORTANT]
>> The sequence of registration is significant, as the APIs are called in the order they were registered.

The `ITestSessionLifeTimeHandler` interface includes the following methods:
Expand Down
4 changes: 2 additions & 2 deletions docs/testingplatform/testnodeupdatemessage.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Certain properties are **required**, while others are optional. The mandatory pr
Optional properties, on the other hand, enhance the testing experience by providing additional information. They are particularly useful in IDE scenarios (like VS, VSCode, etc.), console runs, or when supporting specific extensions that require more detailed information to function correctly. However, these optional properties do not affect the execution of the tests.

>> [!NOTE]
>> It's the responsibility of the extensions to notify the console or handle exceptions that require specific information to function correctly. If an extension lacks the necessary information, it should not cause the test execution to fail, but rather, it should simply opt-out.
>> Extensions are tasked with alerting and managing exceptions when they require specific information to operate correctly. If an extension lacks the necessary information, it should not cause the test execution to fail, but rather, it should simply opt-out.

## Generic information

Expand Down Expand Up @@ -69,7 +69,7 @@ This property is **required**.
```cs
public sealed record InProgressTestNodeStateProperty(string? Explanation = null)
{
public static InProgressTestNodeStateProperty CachedInstance { get; }
public static InProgressTestNodeStateProperty CachedInstance { get; }
}
```

Expand Down
Loading