From 4205cd3a7932ca9dab91eda2d2e8a7cf5d0ad1f0 Mon Sep 17 00:00:00 2001
From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
Date: Wed, 17 Jan 2024 12:04:30 +0000
Subject: [PATCH] .Net: Increase code coverage and add threshold to build
pipeline (#4610)
### Motivation and Context
Resolves: https://github.com/microsoft/semantic-kernel/issues/4372
Added more tests and increased code coverage for production packages to
meet 80% threshold. Added new steps to PR build pipeline to check code
coverage for production packages and trigger a failure if code coverage
in PR is lower than defined threshold.
Example of code coverage output:

### Contribution Checklist
- [x] The code builds clean without any errors or warnings
- [x] The PR follows the [SK Contribution
Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [x] All unit tests pass, and I have added new tests where possible
- [x] I didn't break anyone :smile:
---
.github/workflows/check-coverage.ps1 | 54 ++
.github/workflows/dotnet-build-and-test.yml | 19 +-
dotnet/SK-dotnet.sln | 6 +
dotnet/code-coverage.ps1 | 60 ++
.../AzureSdk/AzureOpenAIClientCore.cs | 13 -
.../AzureOpenAIWithDataChatMessageContent.cs | 3 +-
...ureOpenAITextEmbeddingGenerationService.cs | 1 -
.../TextToImage/OpenAITextToImageService.cs | 6 +-
.../Connectors.UnitTests.csproj | 17 +-
.../MultipleHttpMessageHandlerStub.cs | 53 ++
...reOpenAIWithDataChatMessageContentTests.cs | 70 +++
...ithDataStreamingChatMessageContentTests.cs | 59 ++
.../AzureSdk/OpenAIChatMessageContentTests.cs | 104 ++++
.../AzureSdk/OpenAIFunctionToolCallTests.cs | 81 +++
.../OpenAIPluginCollectionExtensionsTests.cs | 74 +++
.../OpenAIStreamingTextContentTests.cs | 42 ++
.../RequestFailedExceptionExtensionsTests.cs | 79 +++
.../AzureOpenAIChatCompletionServiceTests.cs | 583 ++++++++++++++++++
...cs => OpenAIChatCompletionServiceTests.cs} | 80 ++-
.../AzureOpenAIChatCompletionWithDataTests.cs | 118 +++-
.../FunctionCalling/OpenAIFunctionTests.cs | 45 ++
.../OpenAIMemoryBuilderExtensionsTests.cs | 66 ++
.../OpenAIPromptExecutionSettingsTests.cs | 12 +
.../OpenAIServiceCollectionExtensionsTests.cs | 497 +++++++++++++++
...multiple_function_calls_test_response.json | 56 ++
...on_single_function_call_test_response.json | 32 +
..._multiple_function_calls_test_response.txt | 9 +
...ing_single_function_call_test_response.txt | 3 +
...hat_completion_streaming_test_response.txt | 3 +
.../chat_completion_test_response.json | 22 +
...tion_with_data_streaming_test_response.txt | 1 +
...at_completion_with_data_test_response.json | 28 +
.../image_generation_test_response.json | 4 -
.../TestData/image_result_test_response.json | 12 -
...ext_completion_streaming_test_response.txt | 3 +
.../text_completion_test_response.json | 19 +
...enAITextEmbeddingGenerationServiceTests.cs | 152 +++++
...enAITextEmbeddingGenerationServiceTests.cs | 135 ++++
.../AzureOpenAITextGenerationServiceTests.cs | 208 +++++++
.../OpenAITextGenerationServiceTests.cs | 113 ++++
.../OpenAITextToImageServiceTests.cs | 85 +++
.../OpenAI/ToolCallBehaviorTests.cs | 218 +++++++
.../HandlebarsPromptTemplateTests.cs | 34 +-
.../Helpers/KernelFunctionHelpersTests.cs | 95 +++
.../Helpers/KernelHelperUtilsTests.cs | 38 ++
.../Helpers/KernelSystemHelpersTests.cs | 84 ++-
.../KernelHelpers/KernelSystemHelpers.cs | 22 +-
.../OpenApi/RestApiOperationResponseTests.cs | 15 +
.../Diagnostics/CompilerServicesAttributes.cs | 3 +
.../src/Diagnostics/ExceptionExtensions.cs | 2 +
.../src/Diagnostics/ExperimentalAttribute.cs | 1 +
.../src/Diagnostics/NullableAttributes.cs | 6 +
.../src/Diagnostics/Verify.cs | 1 +
.../src/Http/HttpClientExtensions.cs | 2 +
.../src/Http/HttpClientProvider.cs | 2 +
.../src/Http/HttpContentExtensions.cs | 2 +
.../src/Http/HttpHeaderValues.cs | 3 +
.../InternalUtilities/src/Http/HttpRequest.cs | 2 +
.../src/Linq/AsyncEnumerable.cs | 2 +
.../src/System/EnvExtensions.cs | 3 +
.../src/System/InternalTypeConverter.cs | 2 +
.../src/System/TypeConverterFactory.cs | 2 +
.../src/Text/JsonOptionsCache.cs | 2 +
.../src/Text/ReadOnlyMemoryConverter.cs | 2 +
.../src/Type/TypeExtensions.cs | 2 +
.../Plugins.UnitTests.csproj | 4 +
.../Contents/ImageContentTests.cs | 29 +
.../Utilities/TypeExtensionsTests.cs | 37 ++
68 files changed, 3557 insertions(+), 85 deletions(-)
create mode 100644 .github/workflows/check-coverage.ps1
create mode 100644 dotnet/code-coverage.ps1
create mode 100644 dotnet/src/Connectors/Connectors.UnitTests/MultipleHttpMessageHandlerStub.cs
create mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/AzureOpenAIWithDataChatMessageContentTests.cs
create mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/AzureOpenAIWithDataStreamingChatMessageContentTests.cs
create mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/OpenAIChatMessageContentTests.cs
create mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/OpenAIFunctionToolCallTests.cs
create mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/OpenAIPluginCollectionExtensionsTests.cs
create mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/OpenAIStreamingTextContentTests.cs
create mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/RequestFailedExceptionExtensionsTests.cs
create mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs
rename dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/{OpenAIChatCompletionTests.cs => OpenAIChatCompletionServiceTests.cs} (80%)
create mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIMemoryBuilderExtensionsTests.cs
create mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIServiceCollectionExtensionsTests.cs
create mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_multiple_function_calls_test_response.json
create mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_single_function_call_test_response.json
create mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_streaming_multiple_function_calls_test_response.txt
create mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_streaming_single_function_call_test_response.txt
create mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_streaming_test_response.txt
create mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_test_response.json
create mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_with_data_streaming_test_response.txt
create mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_with_data_test_response.json
delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/image_generation_test_response.json
delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/image_result_test_response.json
create mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/text_completion_streaming_test_response.txt
create mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/text_completion_test_response.json
create mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextEmbedding/AzureOpenAITextEmbeddingGenerationServiceTests.cs
create mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextEmbedding/OpenAITextEmbeddingGenerationServiceTests.cs
create mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextGeneration/AzureOpenAITextGenerationServiceTests.cs
create mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextGeneration/OpenAITextGenerationServiceTests.cs
create mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextToImage/OpenAITextToImageServiceTests.cs
create mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ToolCallBehaviorTests.cs
create mode 100644 dotnet/src/SemanticKernel.UnitTests/Contents/ImageContentTests.cs
create mode 100644 dotnet/src/SemanticKernel.UnitTests/Utilities/TypeExtensionsTests.cs
diff --git a/.github/workflows/check-coverage.ps1 b/.github/workflows/check-coverage.ps1
new file mode 100644
index 0000000000..39fec480cb
--- /dev/null
+++ b/.github/workflows/check-coverage.ps1
@@ -0,0 +1,54 @@
+param (
+ [string]$JsonReportPath,
+ [double]$CoverageThreshold
+)
+
+$jsonContent = Get-Content $JsonReportPath -Raw | ConvertFrom-Json
+$coverageBelowThreshold = $false
+
+function Get-FormattedValue($number) {
+ $formattedNumber = "{0:N1}" -f $number
+ $icon = if ($number -ge $CoverageThreshold) { '✅' } else { '❌' }
+
+ return "$formattedNumber% $icon"
+}
+
+$lineCoverage = $jsonContent.summary.linecoverage
+$branchCoverage = $jsonContent.summary.branchcoverage
+
+if ($lineCoverage -lt $CoverageThreshold -or $branchCoverage -lt $CoverageThreshold) {
+ $coverageBelowThreshold = $true
+}
+
+$totalTableData = [PSCustomObject]@{
+ 'Metric' = 'Total Coverage'
+ 'Line Coverage' = Get-FormattedValue $lineCoverage
+ 'Branch Coverage' = Get-FormattedValue $branchCoverage
+}
+
+$totalTableData | Format-Table -AutoSize
+
+$assemblyTableData = @()
+
+foreach ($assembly in $jsonContent.coverage.assemblies) {
+ $assemblyName = $assembly.name
+ $assemblyLineCoverage = $assembly.coverage
+ $assemblyBranchCoverage = $assembly.branchcoverage
+
+ if ($assemblyLineCoverage -lt $CoverageThreshold -or $assemblyBranchCoverage -lt $CoverageThreshold) {
+ $coverageBelowThreshold = $true
+ }
+
+ $assemblyTableData += [PSCustomObject]@{
+ 'Assembly Name' = $assemblyName
+ 'Line' = Get-FormattedValue $assemblyLineCoverage
+ 'Branch' = Get-FormattedValue $assemblyBranchCoverage
+ }
+}
+
+$assemblyTableData | Format-Table -AutoSize
+
+if ($coverageBelowThreshold) {
+ Write-Host "Code coverage is lower than defined threshold: $CoverageThreshold. Stopping the task."
+ exit 1
+}
diff --git a/.github/workflows/dotnet-build-and-test.yml b/.github/workflows/dotnet-build-and-test.yml
index fc9f43ff86..755aef1f7d 100644
--- a/.github/workflows/dotnet-build-and-test.yml
+++ b/.github/workflows/dotnet-build-and-test.yml
@@ -12,6 +12,9 @@ on:
merge_group:
branches: ["main"]
+env:
+ COVERAGE_THRESHOLD: 80
+
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
@@ -82,7 +85,7 @@ jobs:
run: |
export UT_PROJECTS=$(find ./dotnet -type f -name "*.UnitTests.csproj" | grep -v -E "(Planners.Core.UnitTests.csproj|Experimental.Orchestration.Flow.UnitTests.csproj|Experimental.Assistants.UnitTests.csproj)" | tr '\n' ' ')
for project in $UT_PROJECTS; do
- dotnet test -c ${{ matrix.configuration }} $project --no-build -v Normal --logger trx
+ dotnet test -c ${{ matrix.configuration }} $project --no-build -v Normal --logger trx --collect:"XPlat Code Coverage" --results-directory:"TestResults/Coverage/"
done
- name: Run Integration Tests
@@ -109,6 +112,20 @@ jobs:
Bing__ApiKey: ${{ secrets.BING__APIKEY }}
OpenAI__ApiKey: ${{ secrets.OPENAI__APIKEY }}
+ # Generate test reports and check coverage
+ - name: Generate test reports
+ uses: danielpalme/ReportGenerator-GitHub-Action@5.2.0
+ with:
+ reports: "./TestResults/Coverage/**/coverage.cobertura.xml"
+ targetdir: "./TestResults/Reports"
+ reporttypes: "JsonSummary"
+ # Report for production packages only
+ assemblyfilters: "+Microsoft.SemanticKernel.Abstractions;+Microsoft.SemanticKernel.Core;+Microsoft.SemanticKernel.PromptTemplates.Handlebars;+Microsoft.SemanticKernel.Connectors.OpenAI;+Microsoft.SemanticKernel.Yaml;"
+
+ - name: Check coverage
+ shell: pwsh
+ run: .github/workflows/check-coverage.ps1 -JsonReportPath "TestResults/Reports/Summary.json" -CoverageThreshold $env:COVERAGE_THRESHOLD
+
# This final job is required to satisfy the merge queue. It must only run (or succeed) if no tests failed
dotnet-build-and-test-check:
if: always()
diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln
index 5cf185efc5..0e42708ea3 100644
--- a/dotnet/SK-dotnet.sln
+++ b/dotnet/SK-dotnet.sln
@@ -118,6 +118,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "System", "System", "{3CDE10
src\InternalUtilities\src\System\EnvExtensions.cs = src\InternalUtilities\src\System\EnvExtensions.cs
EndProjectSection
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Type", "Type", "{E85EA4D0-BB7E-4DFD-882F-A76EB8C0B8FF}"
+ ProjectSection(SolutionItems) = preProject
+ src\InternalUtilities\src\Type\TypeExtensions.cs = src\InternalUtilities\src\Type\TypeExtensions.cs
+ EndProjectSection
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugins.Core", "src\Plugins\Plugins.Core\Plugins.Core.csproj", "{0D0C4DAD-E6BC-4504-AE3A-EEA4E35920C1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NCalcPlugins", "samples\NCalcPlugins\NCalcPlugins.csproj", "{E6EDAB8F-3406-4DBF-9AAB-DF40DC2CA0FA}"
@@ -500,6 +505,7 @@ Global
{B00AD427-0047-4850-BEF9-BA8237EA9D8B} = {958AD708-F048-4FAF-94ED-D2F2B92748B9}
{1C19D805-3573-4477-BF07-40180FCDE1BD} = {958AD708-F048-4FAF-94ED-D2F2B92748B9}
{3CDE10B2-AE8F-4FC4-8D55-92D4AD32E144} = {958AD708-F048-4FAF-94ED-D2F2B92748B9}
+ {E85EA4D0-BB7E-4DFD-882F-A76EB8C0B8FF} = {958AD708-F048-4FAF-94ED-D2F2B92748B9}
{0D0C4DAD-E6BC-4504-AE3A-EEA4E35920C1} = {D6D598DF-C17C-46F4-B2B9-CDE82E2DE132}
{E6EDAB8F-3406-4DBF-9AAB-DF40DC2CA0FA} = {FA3720F1-C99A-49B2-9577-A940257098BF}
{C754950A-E16C-4F96-9CC7-9328E361B5AF} = {FA3720F1-C99A-49B2-9577-A940257098BF}
diff --git a/dotnet/code-coverage.ps1 b/dotnet/code-coverage.ps1
new file mode 100644
index 0000000000..108dbdffa7
--- /dev/null
+++ b/dotnet/code-coverage.ps1
@@ -0,0 +1,60 @@
+# This script is for local use to analyze code coverage in more detail using HTML report.
+
+Param(
+ [switch]$ProdPackagesOnly = $false
+)
+
+# Generate a timestamp for the current date and time
+$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
+
+# Define paths
+$scriptPath = Get-Item -Path $PSScriptRoot
+$coverageOutputPath = Join-Path $scriptPath "TestResults\Coverage\$timestamp"
+$reportOutputPath = Join-Path $scriptPath "TestResults\Reports\$timestamp"
+
+# Create output directories
+New-Item -ItemType Directory -Force -Path $coverageOutputPath
+New-Item -ItemType Directory -Force -Path $reportOutputPath
+
+# Find tests for projects ending with 'UnitTests.csproj'
+$testProjects = Get-ChildItem $scriptPath -Filter "*UnitTests.csproj" -Recurse
+
+foreach ($project in $testProjects) {
+ $testProjectPath = $project.FullName
+ Write-Host "Running tests for project: $($testProjectPath)"
+
+ # Run tests
+ dotnet test $testProjectPath `
+ --collect:"XPlat Code Coverage" `
+ --results-directory:$coverageOutputPath `
+
+}
+
+# Install required tools
+& dotnet tool install -g coverlet.console
+& dotnet tool install -g dotnet-reportgenerator-globaltool
+
+# Generate HTML report
+if ($ProdPackagesOnly) {
+ $assemblies = @(
+ "+Microsoft.SemanticKernel.Abstractions",
+ "+Microsoft.SemanticKernel.Core",
+ "+Microsoft.SemanticKernel.PromptTemplates.Handlebars",
+ "+Microsoft.SemanticKernel.Connectors.OpenAI",
+ "+Microsoft.SemanticKernel.Yaml"
+ )
+
+ $assemblyFilters = $assemblies -join ";"
+
+ # Generate report for production assemblies only
+ & reportgenerator -reports:"$coverageOutputPath/**/coverage.cobertura.xml" -targetdir:$reportOutputPath -reporttypes:Html -assemblyfilters:$assemblyFilters
+}
+else {
+ & reportgenerator -reports:"$coverageOutputPath/**/coverage.cobertura.xml" -targetdir:$reportOutputPath -reporttypes:Html
+}
+
+Write-Host "Code coverage report generated at: $reportOutputPath"
+
+# Open report
+$reportIndexHtml = Join-Path $reportOutputPath "index.html"
+Invoke-Item -Path $reportIndexHtml
diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AzureOpenAIClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AzureOpenAIClientCore.cs
index 91888c5073..9155050518 100644
--- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AzureOpenAIClientCore.cs
+++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AzureOpenAIClientCore.cs
@@ -2,7 +2,6 @@
using System;
using System.Net.Http;
-using System.Runtime.CompilerServices;
using Azure;
using Azure.AI.OpenAI;
using Azure.Core;
@@ -98,16 +97,4 @@ internal sealed class AzureOpenAIClientCore : ClientCore
this.AddAttribute(DeploymentNameKey, deploymentName);
}
-
- ///
- /// Logs Azure OpenAI action details.
- ///
- /// Caller member name. Populated automatically by runtime.
- internal void LogActionDetails([CallerMemberName] string? callerMemberName = default)
- {
- if (this.Logger.IsEnabled(LogLevel.Information))
- {
- this.Logger.LogInformation("Action: {Action}. Azure OpenAI Deployment Name: {DeploymentName}.", callerMemberName, this.DeploymentOrModelName);
- }
- }
}
diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AzureOpenAIWithDataChatMessageContent.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AzureOpenAIWithDataChatMessageContent.cs
index 5f84d7148f..04b0e7a2c0 100644
--- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AzureOpenAIWithDataChatMessageContent.cs
+++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AzureOpenAIWithDataChatMessageContent.cs
@@ -30,7 +30,8 @@ public sealed class AzureOpenAIWithDataChatMessageContent : ChatMessageContent
: base(default, string.Empty, modelId, chatChoice, System.Text.Encoding.UTF8, CreateMetadataDictionary(metadata))
{
// An assistant message content must be present, otherwise the chat is not valid.
- var chatMessage = chatChoice.Messages.First(m => string.Equals(m.Role, AuthorRole.Assistant.Label, StringComparison.OrdinalIgnoreCase));
+ var chatMessage = chatChoice.Messages.FirstOrDefault(m => string.Equals(m.Role, AuthorRole.Assistant.Label, StringComparison.OrdinalIgnoreCase)) ??
+ throw new ArgumentException("Chat is not valid. Chat message does not contain any messages with 'assistant' role.");
this.Content = chatMessage.Content;
this.Role = new AuthorRole(chatMessage.Role);
diff --git a/dotnet/src/Connectors/Connectors.OpenAI/TextEmbedding/AzureOpenAITextEmbeddingGenerationService.cs b/dotnet/src/Connectors/Connectors.OpenAI/TextEmbedding/AzureOpenAITextEmbeddingGenerationService.cs
index f1277a3dfd..381c86c6aa 100644
--- a/dotnet/src/Connectors/Connectors.OpenAI/TextEmbedding/AzureOpenAITextEmbeddingGenerationService.cs
+++ b/dotnet/src/Connectors/Connectors.OpenAI/TextEmbedding/AzureOpenAITextEmbeddingGenerationService.cs
@@ -93,7 +93,6 @@ public sealed class AzureOpenAITextEmbeddingGenerationService : ITextEmbeddingGe
Kernel? kernel = null,
CancellationToken cancellationToken = default)
{
- this._core.LogActionDetails();
return this._core.GetEmbeddingsAsync(data, kernel, cancellationToken);
}
}
diff --git a/dotnet/src/Connectors/Connectors.OpenAI/TextToImage/OpenAITextToImageService.cs b/dotnet/src/Connectors/Connectors.OpenAI/TextToImage/OpenAITextToImageService.cs
index 72531434ca..837b877845 100644
--- a/dotnet/src/Connectors/Connectors.OpenAI/TextToImage/OpenAITextToImageService.cs
+++ b/dotnet/src/Connectors/Connectors.OpenAI/TextToImage/OpenAITextToImageService.cs
@@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http;
using System.Text.Json;
@@ -87,10 +86,7 @@ public sealed class OpenAITextToImageService : ITextToImageService
string format, Func extractResponse,
CancellationToken cancellationToken)
{
- Debug.Assert(width == height);
- Debug.Assert(width is 256 or 512 or 1024);
- Debug.Assert(format is "url" or "b64_json");
- Debug.Assert(extractResponse is not null);
+ Verify.NotNull(extractResponse);
var requestBody = JsonSerializer.Serialize(new TextToImageRequest
{
diff --git a/dotnet/src/Connectors/Connectors.UnitTests/Connectors.UnitTests.csproj b/dotnet/src/Connectors/Connectors.UnitTests/Connectors.UnitTests.csproj
index 1c7a297872..a541b834ba 100644
--- a/dotnet/src/Connectors/Connectors.UnitTests/Connectors.UnitTests.csproj
+++ b/dotnet/src/Connectors/Connectors.UnitTests/Connectors.UnitTests.csproj
@@ -27,14 +27,17 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
all
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
-
+
@@ -55,16 +58,10 @@
-
+
Always
-
- Always
-
-
- Always
-
-
+
Always
diff --git a/dotnet/src/Connectors/Connectors.UnitTests/MultipleHttpMessageHandlerStub.cs b/dotnet/src/Connectors/Connectors.UnitTests/MultipleHttpMessageHandlerStub.cs
new file mode 100644
index 0000000000..f83ac864d0
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.UnitTests/MultipleHttpMessageHandlerStub.cs
@@ -0,0 +1,53 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace SemanticKernel.Connectors.UnitTests;
+
+internal sealed class MultipleHttpMessageHandlerStub : DelegatingHandler
+{
+ private int _callIteration = 0;
+
+ public List RequestHeaders { get; private set; }
+
+ public List ContentHeaders { get; private set; }
+
+ public List RequestContents { get; private set; }
+
+ public List RequestUris { get; private set; }
+
+ public List Methods { get; private set; }
+
+ public List ResponsesToReturn { get; set; }
+
+ public MultipleHttpMessageHandlerStub()
+ {
+ this.RequestHeaders = [];
+ this.ContentHeaders = [];
+ this.RequestContents = [];
+ this.RequestUris = [];
+ this.Methods = [];
+ this.ResponsesToReturn = [];
+ }
+
+ protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ this._callIteration++;
+
+ this.Methods.Add(request.Method);
+ this.RequestUris.Add(request.RequestUri);
+ this.RequestHeaders.Add(request.Headers);
+ this.ContentHeaders.Add(request.Content?.Headers);
+
+ var content = request.Content == null ? null : await request.Content.ReadAsByteArrayAsync(cancellationToken);
+
+ this.RequestContents.Add(content);
+
+ return await Task.FromResult(this.ResponsesToReturn[this._callIteration - 1]);
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/AzureOpenAIWithDataChatMessageContentTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/AzureOpenAIWithDataChatMessageContentTests.cs
new file mode 100644
index 0000000000..76d5c6b7d8
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/AzureOpenAIWithDataChatMessageContentTests.cs
@@ -0,0 +1,70 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.SemanticKernel.ChatCompletion;
+using Microsoft.SemanticKernel.Connectors.OpenAI;
+using Xunit;
+
+namespace SemanticKernel.Connectors.UnitTests.OpenAI.AzureSdk;
+
+///
+/// Unit tests for class.
+///
+public sealed class AzureOpenAIWithDataChatMessageContentTests
+{
+ [Fact]
+ public void ConstructorThrowsExceptionWhenAssistantMessageIsNotProvided()
+ {
+ // Arrange
+ var choice = new ChatWithDataChoice();
+
+ // Act & Assert
+ var exception = Assert.Throws(() => new AzureOpenAIWithDataChatMessageContent(choice, "model-id"));
+
+ Assert.Contains("Chat is not valid", exception.Message, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public void ConstructorReturnsInstanceWithNullToolContent()
+ {
+ // Arrange
+ var choice = new ChatWithDataChoice { Messages = [new() { Content = "Assistant content", Role = "assistant" }] };
+
+ // Act
+ var content = new AzureOpenAIWithDataChatMessageContent(choice, "model-id");
+
+ // Assert
+ Assert.Equal("Assistant content", content.Content);
+ Assert.Equal(AuthorRole.Assistant, content.Role);
+
+ Assert.Null(content.ToolContent);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ConstructorReturnsInstanceWithNonNullToolContent(bool includeMetadata)
+ {
+ // Arrange
+ var choice = new ChatWithDataChoice
+ {
+ Messages = [
+ new() { Content = "Assistant content", Role = "assistant" },
+ new() { Content = "Tool content", Role = "tool" }]
+ };
+
+ // Act
+ var content = includeMetadata ?
+ new AzureOpenAIWithDataChatMessageContent(choice, "model-id", new Dictionary()) :
+ new AzureOpenAIWithDataChatMessageContent(choice, "model-id");
+
+ // Assert
+ Assert.Equal("Assistant content", content.Content);
+ Assert.Equal("Tool content", content.ToolContent);
+ Assert.Equal(AuthorRole.Assistant, content.Role);
+
+ Assert.NotNull(content.Metadata);
+ Assert.Equal("Tool content", content.Metadata["ToolContent"]);
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/AzureOpenAIWithDataStreamingChatMessageContentTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/AzureOpenAIWithDataStreamingChatMessageContentTests.cs
new file mode 100644
index 0000000000..d3fd86820e
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/AzureOpenAIWithDataStreamingChatMessageContentTests.cs
@@ -0,0 +1,59 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using Microsoft.SemanticKernel.Connectors.OpenAI;
+using Xunit;
+
+namespace SemanticKernel.Connectors.UnitTests.OpenAI.AzureSdk;
+
+///
+/// Unit tests for class.
+///
+public sealed class AzureOpenAIWithDataStreamingChatMessageContentTests
+{
+ [Theory]
+ [MemberData(nameof(ValidChoices))]
+ public void ConstructorWithValidChoiceSetsNonEmptyContent(object choice, string expectedContent)
+ {
+ // Arrange
+ var streamingChoice = choice as ChatWithDataStreamingChoice;
+
+ // Act
+ var content = new AzureOpenAIWithDataStreamingChatMessageContent(streamingChoice!, 0, "model-id");
+
+ // Assert
+ Assert.Equal(expectedContent, content.Content);
+ }
+
+ [Theory]
+ [MemberData(nameof(InvalidChoices))]
+ public void ConstructorWithInvalidChoiceSetsNullContent(object choice)
+ {
+ // Arrange
+ var streamingChoice = choice as ChatWithDataStreamingChoice;
+
+ // Act
+ var content = new AzureOpenAIWithDataStreamingChatMessageContent(streamingChoice!, 0, "model-id");
+
+ // Assert
+ Assert.Null(content.Content);
+ }
+
+ public static IEnumerable