From 9d2e395837b1f7fc96c94101e96eb592c3f02d0d Mon Sep 17 00:00:00 2001 From: Rafael Azriaiev Date: Tue, 31 Dec 2024 08:26:50 +0200 Subject: [PATCH 01/12] Added Mock Kernel pool and configuration and Mock Http Handler for mock responses --- .../MockAIConfiguration.cs | 36 +++++++ .../MockHttpMessageHandler.cs | 95 +++++++++++++++++++ .../MockKernelPool.cs | 81 ++++++++++++++++ .../MockResponse.cs | 32 +++++++ .../SemanticKernelPooling.Tests.Mocks.csproj | 13 +++ .../ServiceExtension.cs | 59 ++++++++++++ .../KernelPoolTestContext.cs | 15 +++ .../SemanticKernelPooling.Tests.Unit.csproj | 21 ++++ 8 files changed, 352 insertions(+) create mode 100644 SemanticKernelPooling.Tests.Mocks/MockAIConfiguration.cs create mode 100644 SemanticKernelPooling.Tests.Mocks/MockHttpMessageHandler.cs create mode 100644 SemanticKernelPooling.Tests.Mocks/MockKernelPool.cs create mode 100644 SemanticKernelPooling.Tests.Mocks/MockResponse.cs create mode 100644 SemanticKernelPooling.Tests.Mocks/SemanticKernelPooling.Tests.Mocks.csproj create mode 100644 SemanticKernelPooling.Tests.Mocks/ServiceExtension.cs create mode 100644 SemanticKernelPooling.Tests.Unit/KernelPoolTestContext.cs create mode 100644 SemanticKernelPooling.Tests.Unit/SemanticKernelPooling.Tests.Unit.csproj diff --git a/SemanticKernelPooling.Tests.Mocks/MockAIConfiguration.cs b/SemanticKernelPooling.Tests.Mocks/MockAIConfiguration.cs new file mode 100644 index 0000000..9a82210 --- /dev/null +++ b/SemanticKernelPooling.Tests.Mocks/MockAIConfiguration.cs @@ -0,0 +1,36 @@ +namespace SemanticKernelPooling.Tests.Mocks; + +/// +/// Represents a mock configuration for testing kernel pool behavior. +/// This configuration simulates the settings required for AI service providers without actual credentials. +/// +/// +/// This mock configuration is used in unit tests to verify the kernel pooling behavior +/// without requiring actual AI service provider credentials or connections. +/// +public record MockAIConfiguration : AIServiceProviderConfiguration +{ + /// + /// Gets or initializes the API key used for testing. + /// In mock scenarios, this can be any non-null string. + /// + public required string ApiKey { get; init; } + + /// + /// Gets or initializes the model identifier used for testing. + /// In mock scenarios, this can be any non-null string. + /// + public required string ModelId { get; init; } + + /// + /// Gets or initializes the service identifier used for testing. + /// In mock scenarios, this can be any non-null string. + /// + public required string ServiceId { get; init; } + + /// + /// Gets or initializes the endpoint URL used for testing. + /// In mock scenarios, this can be any valid URL string. + /// + public required string Endpoint { get; init; } +} \ No newline at end of file diff --git a/SemanticKernelPooling.Tests.Mocks/MockHttpMessageHandler.cs b/SemanticKernelPooling.Tests.Mocks/MockHttpMessageHandler.cs new file mode 100644 index 0000000..bad0472 --- /dev/null +++ b/SemanticKernelPooling.Tests.Mocks/MockHttpMessageHandler.cs @@ -0,0 +1,95 @@ +using System.Net; +using System.Text.Json; +using Microsoft.Extensions.Logging; + +namespace SemanticKernelPooling.Tests.Mocks; + +/// +/// Provides a mock HTTP message handler that returns predefined responses for testing purposes. +/// This handler enables testing of HTTP-dependent code without making actual network requests. +/// +/// +/// The handler loads mock responses from JSON files in a specified directory. +/// Each response file should contain a request key, status code, content, and optional headers. +/// +public class MockHttpMessageHandler : HttpMessageHandler +{ + private readonly Dictionary _responses; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The directory path containing mock response JSON files. + /// The logger for recording handler activities. + public MockHttpMessageHandler(string responsesPath, ILogger logger) + { + _logger = logger; + _responses = LoadMockResponses(responsesPath); + } + + /// + /// Processes HTTP requests and returns mock responses based on the request key. + /// + /// The HTTP request message to process. + /// A token for canceling the request. + /// A task representing the HTTP response message. + protected override Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + var requestKey = CreateRequestKey(request); + _logger.LogInformation("Mock HTTP handler processing request for: {RequestKey}", requestKey); + + if (_responses.TryGetValue(requestKey, out var mockResponse)) + { + var response = new HttpResponseMessage + { + StatusCode = mockResponse.StatusCode, + Content = new StringContent(mockResponse.Content) + }; + + foreach (var header in mockResponse.Headers) + { + response.Headers.Add(header.Key, header.Value); + } + + return Task.FromResult(response); + } + + _logger.LogWarning("No mock response found for request: {RequestKey}", requestKey); + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)); + } + + private Dictionary LoadMockResponses(string path) + { + var responses = new Dictionary(); + + if (Directory.Exists(path)) + { + foreach (var file in Directory.GetFiles(path, "*.json")) + { + try + { + var content = File.ReadAllText(file); + var mockResponse = JsonSerializer.Deserialize(content); + if (mockResponse?.RequestKey != null) + { + responses[mockResponse.RequestKey] = mockResponse; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error loading mock response from file: {FilePath}", file); + } + } + } + + return responses; + } + + private string CreateRequestKey(HttpRequestMessage request) + { + return $"{request.Method}:{request.RequestUri?.PathAndQuery}"; + } +} \ No newline at end of file diff --git a/SemanticKernelPooling.Tests.Mocks/MockKernelPool.cs b/SemanticKernelPooling.Tests.Mocks/MockKernelPool.cs new file mode 100644 index 0000000..6da2fd7 --- /dev/null +++ b/SemanticKernelPooling.Tests.Mocks/MockKernelPool.cs @@ -0,0 +1,81 @@ +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; + +namespace SemanticKernelPooling.Tests.Mocks; + +/// +/// Provides a mock implementation of the kernel pool for testing purposes. +/// This implementation simulates the behavior of a real kernel pool without requiring actual AI service connections. +/// +/// +/// +/// The mock kernel pool tracks kernel creation and provides a controllable environment for testing +/// kernel pool behaviors including creation, disposal, and reuse patterns. +/// +/// +/// Example usage: +/// +/// var config = new MockAIConfiguration { ... }; +/// var pool = new MockKernelPool(config, loggerFactory, "path/to/responses"); +/// +/// +/// +public class MockKernelPool : AIServicePool +{ + private readonly ILoggerFactory _loggerFactory; + private readonly string _responsesPath; + private int _kernelCreationCount; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration for the mock kernel pool. + /// The factory for creating loggers. + /// The path to mock response files. Defaults to "MockResponses". + public MockKernelPool( + MockAIConfiguration config, + ILoggerFactory loggerFactory, + string responsesPath = "MockResponses") + : base(config) + { + _loggerFactory = loggerFactory; + _responsesPath = responsesPath; + } + + /// + /// Registers a mock chat completion service with the kernel builder. + /// + /// The kernel builder to configure. + /// The mock configuration to use. + /// Optional HTTP client to use. If null, a mock client will be created. + protected override void RegisterChatCompletionService( + IKernelBuilder kernelBuilder, + MockAIConfiguration config, + HttpClient? httpClient) + { + Interlocked.Increment(ref _kernelCreationCount); + + if (httpClient == null) + { + var handler = new MockHttpMessageHandler( + _responsesPath, + _loggerFactory.CreateLogger()); + httpClient = new HttpClient(handler); + } + + kernelBuilder.AddAzureOpenAIChatCompletion( + deploymentName: "mock-deployment", + endpoint: "https://mock.openai.azure.com", + serviceId: config.ServiceId, + apiKey: config.ApiKey, + httpClient: httpClient); + } + + /// + protected override ILogger Logger => _loggerFactory.CreateLogger(); + + /// + /// Gets the number of kernels that have been created by this pool. + /// + public int KernelCreationCount => _kernelCreationCount; +} \ No newline at end of file diff --git a/SemanticKernelPooling.Tests.Mocks/MockResponse.cs b/SemanticKernelPooling.Tests.Mocks/MockResponse.cs new file mode 100644 index 0000000..01ecea2 --- /dev/null +++ b/SemanticKernelPooling.Tests.Mocks/MockResponse.cs @@ -0,0 +1,32 @@ +using System.Net; + +namespace SemanticKernelPooling.Tests.Mocks; + +/// +/// Represents a mock HTTP response configuration that can be stored in a JSON file. +/// +public class MockResponse +{ + /// + /// Gets or sets the key used to match incoming requests to this response. + /// + /// + /// The key format is typically "{HTTP_METHOD}:{PATH}". For example, "POST:/v1/chat/completions". + /// + public string RequestKey { get; set; } = string.Empty; + + /// + /// Gets or sets the HTTP status code to return in the mock response. + /// + public HttpStatusCode StatusCode { get; set; } + + /// + /// Gets or sets the content body of the mock response. + /// + public string Content { get; set; } = string.Empty; + + /// + /// Gets or sets the HTTP headers to include in the mock response. + /// + public Dictionary Headers { get; set; } = new(); +} \ No newline at end of file diff --git a/SemanticKernelPooling.Tests.Mocks/SemanticKernelPooling.Tests.Mocks.csproj b/SemanticKernelPooling.Tests.Mocks/SemanticKernelPooling.Tests.Mocks.csproj new file mode 100644 index 0000000..a7dc87e --- /dev/null +++ b/SemanticKernelPooling.Tests.Mocks/SemanticKernelPooling.Tests.Mocks.csproj @@ -0,0 +1,13 @@ + + + + net9.0 + enable + enable + + + + + + + diff --git a/SemanticKernelPooling.Tests.Mocks/ServiceExtension.cs b/SemanticKernelPooling.Tests.Mocks/ServiceExtension.cs new file mode 100644 index 0000000..ea88d97 --- /dev/null +++ b/SemanticKernelPooling.Tests.Mocks/ServiceExtension.cs @@ -0,0 +1,59 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace SemanticKernelPooling.Tests.Mocks; + +/// +/// Provides extension methods for registering mock kernel pools with the service collection. +/// +/// +/// This extension follows the same pattern as the production service extensions but provides +/// mock implementations for testing purposes. +/// +public static class ServiceExtension +{ + /// + /// Registers mock kernel pool implementations for testing purposes. + /// + /// The DI service provider. + /// Optional path to mock response files. Defaults to "MockResponses". + /// The service provider to enable method chaining. + /// + /// + /// This method registers mock implementations for both OpenAI and Azure OpenAI service types, + /// allowing tests to run without actual service dependencies. + /// + /// + /// Example usage: + /// + /// services.AddSingleton(mockConfiguration.Object); + /// services.UseSemanticKernelPooling(); + /// serviceProvider.UseMockKernelPool("TestResponses"); + /// + /// + /// + public static IServiceProvider UseMockKernelPool( + this IServiceProvider serviceProvider, + string responsesPath = "MockResponses") + { + var registrar = serviceProvider.GetRequiredService(); + + // Register factory for both mock providers + var factory = (AIServiceProviderConfiguration config, ILoggerFactory loggerFactory) => + new MockKernelPool((MockAIConfiguration)config, loggerFactory, responsesPath); + + registrar.RegisterKernelPoolFactory(AIServiceProviderType.OpenAI, factory); + registrar.RegisterKernelPoolFactory(AIServiceProviderType.AzureOpenAI, factory); + + // Register configuration readers + var configReader = (IConfigurationSection section) => + section.Get() ?? + throw new InvalidOperationException("Mock configuration not found."); + + registrar.RegisterConfigurationReader(AIServiceProviderType.OpenAI, configReader); + registrar.RegisterConfigurationReader(AIServiceProviderType.AzureOpenAI, configReader); + + return serviceProvider; + } +} \ No newline at end of file diff --git a/SemanticKernelPooling.Tests.Unit/KernelPoolTestContext.cs b/SemanticKernelPooling.Tests.Unit/KernelPoolTestContext.cs new file mode 100644 index 0000000..10669b3 --- /dev/null +++ b/SemanticKernelPooling.Tests.Unit/KernelPoolTestContext.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; +using System; + +namespace SemanticKernelPooling.Tests.Unit; + +public class KernelPoolTestContext: IDisposable +{ + public IServiceProvider ServiceProvider { get; } + private readonly ServiceProvider _disposableServiceProvider; + + public void Dispose() + { + _disposableServiceProvider?.Dispose(); + } +} diff --git a/SemanticKernelPooling.Tests.Unit/SemanticKernelPooling.Tests.Unit.csproj b/SemanticKernelPooling.Tests.Unit/SemanticKernelPooling.Tests.Unit.csproj new file mode 100644 index 0000000..9135b1c --- /dev/null +++ b/SemanticKernelPooling.Tests.Unit/SemanticKernelPooling.Tests.Unit.csproj @@ -0,0 +1,21 @@ + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + + From 5e26bec7e4991c3dd1ca7ac9237adfa4d5ff62c5 Mon Sep 17 00:00:00 2001 From: Rafael Azriaiev Date: Tue, 31 Dec 2024 11:01:37 +0200 Subject: [PATCH 02/12] Added Test project and created KernelPoolTestContext for the Fixture --- .../KernelPoolTestContext.cs | 197 +++++++++++++++++- .../SemanticKernelPooling.Tests.Unit.csproj | 21 +- SemanticKernelPooling.sln | 12 ++ 3 files changed, 223 insertions(+), 7 deletions(-) diff --git a/SemanticKernelPooling.Tests.Unit/KernelPoolTestContext.cs b/SemanticKernelPooling.Tests.Unit/KernelPoolTestContext.cs index 10669b3..3140785 100644 --- a/SemanticKernelPooling.Tests.Unit/KernelPoolTestContext.cs +++ b/SemanticKernelPooling.Tests.Unit/KernelPoolTestContext.cs @@ -1,15 +1,204 @@ -using Microsoft.Extensions.DependencyInjection; -using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using SemanticKernelPooling.Tests.Mocks; +using System.Net; +using System.Text.Json; namespace SemanticKernelPooling.Tests.Unit; -public class KernelPoolTestContext: IDisposable +/// +/// Provides a shared test context for kernel pool tests, managing mock configurations +/// and service setup that can be reused across multiple test cases. +/// +/// +/// +/// This context handles the initialization of mock services, configuration, and response management +/// required for testing kernel pool functionality without real service dependencies. +/// +/// +/// Example usage: +/// +/// public class MyTests : IClassFixture<KernelPoolTestContext> +/// { +/// private readonly KernelPoolTestContext _context; +/// +/// public MyTests(KernelPoolTestContext context) +/// { +/// _context = context; +/// } +/// } +/// +/// +/// +public class KernelPoolTestContext : IDisposable { + /// + /// Gets the configured service provider for test cases. + /// public IServiceProvider ServiceProvider { get; } + private readonly ServiceProvider _disposableServiceProvider; + private readonly string _mockResponsesPath; + public const string TestScope = "test-scope"; + + /// + /// Initializes a new instance of the class. + /// Sets up the mock configuration, services, and response directory needed for testing. + /// + public KernelPoolTestContext() + { + _mockResponsesPath = Path.Combine(Path.GetTempPath(), "MockResponses"); + Directory.CreateDirectory(_mockResponsesPath); + + // Create mock configuration + var mockConfigurationSection = new Mock(); + mockConfigurationSection.Setup(x => x.GetChildren()) + .Returns(new List + { + CreateMockConfigSection("MockOpenAI", AIServiceProviderType.OpenAI, new[] { TestScope }), + CreateMockConfigSection("MockAzureOpenAI", AIServiceProviderType.AzureOpenAI, new[] { TestScope }) + }); + + var mockConfiguration = new Mock(); + mockConfiguration + .Setup(x => x.GetSection("AIServiceProviderConfigurations")) + .Returns(mockConfigurationSection.Object); + + // Set up services + var services = new ServiceCollection(); + services.AddSingleton(mockConfiguration.Object); + services.UseSemanticKernelPooling(); + + _disposableServiceProvider = services.BuildServiceProvider(); + _disposableServiceProvider.UseMockKernelPool(_mockResponsesPath); + + ServiceProvider = _disposableServiceProvider; + } + + /// + /// Adds a mock HTTP response to be used in tests. + /// + /// The request key to match for this response (e.g., "POST:/v1/chat/completions"). + /// The HTTP status code to return. + /// The response content to return. + /// + /// + /// The mock response will be saved as a JSON file and used by the mock HTTP handler + /// when matching requests during tests. + /// + /// + /// Example usage: + /// + /// context.AddMockResponse( + /// "POST:/v1/chat/completions", + /// HttpStatusCode.OK, + /// "{\"choices\": [{\"message\": {\"content\": \"Test response\"}}]}"); + /// + /// + /// + public void AddMockResponse(string requestKey, HttpStatusCode statusCode, string content) + { + var response = new MockResponse + { + RequestKey = requestKey, + StatusCode = statusCode, + Content = content, + Headers = new Dictionary + { + { "Content-Type", new[] { "application/json" } } + } + }; + var filePath = Path.Combine(_mockResponsesPath, $"{Guid.NewGuid()}.json"); + File.WriteAllText(filePath, JsonSerializer.Serialize(response)); + } + + /// + /// Creates a mock configuration section for testing kernel pool functionality. + /// + /// The unique name for identifying the configuration instance. + /// The type of AI service provider (e.g., OpenAI, AzureOpenAI). + /// An array of scope names associated with this configuration. + /// A mocked with all required configuration values. + /// + /// + /// This method creates a mock configuration section that simulates the structure and values + /// that would typically come from appsettings.json or other configuration sources. + /// + /// + /// The mock configuration includes all required properties for both standard and Azure-specific + /// configurations, ensuring compatibility with different service provider types. + /// + /// + /// Example usage: + /// + /// var configSection = CreateMockConfigSection( + /// "TestOpenAI", + /// AIServiceProviderType.OpenAI, + /// new[] { "test-scope" }); + /// + /// + /// + /// + /// + private IConfigurationSection CreateMockConfigSection( + string uniqueName, + AIServiceProviderType serviceType, + string[] scopes) + { + ArgumentNullException.ThrowIfNull(uniqueName); + ArgumentNullException.ThrowIfNull(scopes); + + var mockSection = new Mock(); + + // Setup AIServiceProviderConfiguration base properties + mockSection.Setup(x => x.GetValue("UniqueName")) + .Returns(uniqueName); + mockSection.Setup(x => x.GetValue("ServiceType")) + .Returns(serviceType); + mockSection.Setup(x => x.GetSection("Scopes").Get>()) + .Returns(scopes.ToList()); + mockSection.Setup(x => x.GetValue("InstanceCount")) + .Returns(2); + mockSection.Setup(x => x.GetValue("DeploymentTextEmbedding")) + .Returns("mock-embedding"); + mockSection.Setup(x => x.GetValue("MaxWaitForKernelInSeconds")) + .Returns(30); + + // Setup Mock-specific properties + mockSection.Setup(x => x.GetValue("ApiKey")) + .Returns("mock-api-key"); + mockSection.Setup(x => x.GetValue("ModelId")) + .Returns("mock-model"); + mockSection.Setup(x => x.GetValue("ServiceId")) + .Returns("mock-service-id"); + mockSection.Setup(x => x.GetValue("Endpoint")) + .Returns("https://mock.endpoint"); + + return mockSection.Object; + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + /// + /// Cleans up both the service provider and any mock response files created during testing. + /// public void Dispose() { _disposableServiceProvider?.Dispose(); + + try + { + if (Directory.Exists(_mockResponsesPath)) + { + Directory.Delete(_mockResponsesPath, true); + } + } + catch (Exception) + { + // Log or handle cleanup failure + } } -} +} \ No newline at end of file diff --git a/SemanticKernelPooling.Tests.Unit/SemanticKernelPooling.Tests.Unit.csproj b/SemanticKernelPooling.Tests.Unit/SemanticKernelPooling.Tests.Unit.csproj index 9135b1c..5ff7b38 100644 --- a/SemanticKernelPooling.Tests.Unit/SemanticKernelPooling.Tests.Unit.csproj +++ b/SemanticKernelPooling.Tests.Unit/SemanticKernelPooling.Tests.Unit.csproj @@ -8,10 +8,25 @@ - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + diff --git a/SemanticKernelPooling.sln b/SemanticKernelPooling.sln index 64605d6..852d18b 100644 --- a/SemanticKernelPooling.sln +++ b/SemanticKernelPooling.sln @@ -26,6 +26,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{6ACB62 assets\package-icon.png = assets\package-icon.png EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SemanticKernelPooling.Tests.Mocks", "SemanticKernelPooling.Tests.Mocks\SemanticKernelPooling.Tests.Mocks.csproj", "{D03855DF-C58C-40F2-A361-CDAF7FCEC88C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SemanticKernelPooling.Tests.Unit", "SemanticKernelPooling.Tests.Unit\SemanticKernelPooling.Tests.Unit.csproj", "{B8F6A4BF-2626-4367-95D4-FB00A8254785}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -56,6 +60,14 @@ Global {EF7CC525-73D8-4416-A52F-D8C44BEA539B}.Debug|Any CPU.Build.0 = Debug|Any CPU {EF7CC525-73D8-4416-A52F-D8C44BEA539B}.Release|Any CPU.ActiveCfg = Release|Any CPU {EF7CC525-73D8-4416-A52F-D8C44BEA539B}.Release|Any CPU.Build.0 = Release|Any CPU + {D03855DF-C58C-40F2-A361-CDAF7FCEC88C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D03855DF-C58C-40F2-A361-CDAF7FCEC88C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D03855DF-C58C-40F2-A361-CDAF7FCEC88C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D03855DF-C58C-40F2-A361-CDAF7FCEC88C}.Release|Any CPU.Build.0 = Release|Any CPU + {B8F6A4BF-2626-4367-95D4-FB00A8254785}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B8F6A4BF-2626-4367-95D4-FB00A8254785}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B8F6A4BF-2626-4367-95D4-FB00A8254785}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B8F6A4BF-2626-4367-95D4-FB00A8254785}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From f81c492fb81d1718890f83d6a8849caf4200f372 Mon Sep 17 00:00:00 2001 From: Rafael Azriaiev Date: Tue, 31 Dec 2024 14:41:41 +0200 Subject: [PATCH 03/12] Updated Mock Http Send with Logs --- .../MockHttpMessageHandler.cs | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/SemanticKernelPooling.Tests.Mocks/MockHttpMessageHandler.cs b/SemanticKernelPooling.Tests.Mocks/MockHttpMessageHandler.cs index bad0472..15ab169 100644 --- a/SemanticKernelPooling.Tests.Mocks/MockHttpMessageHandler.cs +++ b/SemanticKernelPooling.Tests.Mocks/MockHttpMessageHandler.cs @@ -39,14 +39,23 @@ protected override Task SendAsync( CancellationToken cancellationToken) { var requestKey = CreateRequestKey(request); - _logger.LogInformation("Mock HTTP handler processing request for: {RequestKey}", requestKey); + _logger.LogInformation("Mock HTTP handler received request:"); + _logger.LogInformation(" Method: {Method}", request.Method); + _logger.LogInformation(" URI: {URI}", request.RequestUri); + _logger.LogInformation(" Generated Key: {RequestKey}", requestKey); + _logger.LogInformation("Available mock response keys:"); + foreach (var key in _responses.Keys) + { + _logger.LogInformation(" {Key}", key); + } if (_responses.TryGetValue(requestKey, out var mockResponse)) { + _logger.LogInformation("Found matching mock response"); var response = new HttpResponseMessage { StatusCode = mockResponse.StatusCode, - Content = new StringContent(mockResponse.Content) + Content = new StringContent(mockResponse.Content, System.Text.Encoding.UTF8, "application/json") }; foreach (var header in mockResponse.Headers) @@ -57,10 +66,15 @@ protected override Task SendAsync( return Task.FromResult(response); } - _logger.LogWarning("No mock response found for request: {RequestKey}", requestKey); + _logger.LogWarning("No mock response found for request"); return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)); } + private string CreateRequestKey(HttpRequestMessage request) + { + return $"{request.Method}:{request.RequestUri?.PathAndQuery}"; + } + private Dictionary LoadMockResponses(string path) { var responses = new Dictionary(); @@ -87,9 +101,4 @@ private Dictionary LoadMockResponses(string path) return responses; } - - private string CreateRequestKey(HttpRequestMessage request) - { - return $"{request.Method}:{request.RequestUri?.PathAndQuery}"; - } } \ No newline at end of file From 72aed6d9c2267ed2a1225935828d7456cc0eb2bb Mon Sep 17 00:00:00 2001 From: Rafael Azriaiev Date: Tue, 31 Dec 2024 14:42:59 +0200 Subject: [PATCH 04/12] added appsettings and TestOutputHelpers with XUnit logger and logger provider --- .../Logging/XUnitLogger.cs | 49 +++++++++++++++++++ .../Logging/XUnitLoggerProvider.cs | 19 +++++++ .../TestOutputHelper.cs | 36 ++++++++++++++ .../appsettings.json | 33 +++++++++++++ 4 files changed, 137 insertions(+) create mode 100644 SemanticKernelPooling.Tests.Unit/Logging/XUnitLogger.cs create mode 100644 SemanticKernelPooling.Tests.Unit/Logging/XUnitLoggerProvider.cs create mode 100644 SemanticKernelPooling.Tests.Unit/TestOutputHelper.cs create mode 100644 SemanticKernelPooling.Tests.Unit/appsettings.json diff --git a/SemanticKernelPooling.Tests.Unit/Logging/XUnitLogger.cs b/SemanticKernelPooling.Tests.Unit/Logging/XUnitLogger.cs new file mode 100644 index 0000000..db73e76 --- /dev/null +++ b/SemanticKernelPooling.Tests.Unit/Logging/XUnitLogger.cs @@ -0,0 +1,49 @@ +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace SemanticKernelPooling.Tests.Unit.Logging +{ + internal class XUnitLogger : ILogger + { + private readonly ITestOutputHelper _testOutputHelper; + private readonly string _categoryName; + + public XUnitLogger(ITestOutputHelper testOutputHelper, string categoryName) + { + _testOutputHelper = testOutputHelper; + _categoryName = categoryName; + } + + public IDisposable BeginScope(TState state) where TState : notnull + => NoopDisposable.Instance; + + public bool IsEnabled(LogLevel logLevel) + => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + try + { + _testOutputHelper.WriteLine($"{_categoryName} [{eventId}] {formatter(state, exception!)}"); + + if (exception != null) + _testOutputHelper.WriteLine(exception.ToString()); + } + catch (InvalidOperationException) + { + //no active test, ignore! + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + } + + private class NoopDisposable : IDisposable + { + public static readonly NoopDisposable Instance = new NoopDisposable(); + public void Dispose() + { } + } + } +} diff --git a/SemanticKernelPooling.Tests.Unit/Logging/XUnitLoggerProvider.cs b/SemanticKernelPooling.Tests.Unit/Logging/XUnitLoggerProvider.cs new file mode 100644 index 0000000..4b003d4 --- /dev/null +++ b/SemanticKernelPooling.Tests.Unit/Logging/XUnitLoggerProvider.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace SemanticKernelPooling.Tests.Unit.Logging; + +public class XUnitLoggerProvider : ILoggerProvider +{ + private readonly ITestOutputHelper _testOutputHelper; + + public XUnitLoggerProvider(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + + public ILogger CreateLogger(string categoryName) + => new XUnitLogger(_testOutputHelper, categoryName); + + public void Dispose(){ } +} \ No newline at end of file diff --git a/SemanticKernelPooling.Tests.Unit/TestOutputHelper.cs b/SemanticKernelPooling.Tests.Unit/TestOutputHelper.cs new file mode 100644 index 0000000..9002b80 --- /dev/null +++ b/SemanticKernelPooling.Tests.Unit/TestOutputHelper.cs @@ -0,0 +1,36 @@ +using Xunit.Abstractions; + +namespace SemanticKernelPooling.Tests.Unit; + +public class TestOutputHelper : ITestOutputHelper +{ + private readonly ITestOutputHelper _testOutputHelper; + + public TestOutputHelper(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + public void WriteLine(string message) + { + try + { + _testOutputHelper.WriteLine(message); + } + catch + { + Console.WriteLine(message); + } + } + + public void WriteLine(string format, params object[] args) + { + try + { + _testOutputHelper.WriteLine(format, args); + } + catch + { + Console.WriteLine(format, args); + } + } +} \ No newline at end of file diff --git a/SemanticKernelPooling.Tests.Unit/appsettings.json b/SemanticKernelPooling.Tests.Unit/appsettings.json new file mode 100644 index 0000000..90fb36c --- /dev/null +++ b/SemanticKernelPooling.Tests.Unit/appsettings.json @@ -0,0 +1,33 @@ +{ + "AIServiceProviderConfigurations": [ + { + "UniqueName": "MockOpenAI", + "ServiceType": "OpenAI", + "InstanceCount": 2, + "DeploymentTextEmbedding": "mock-embedding", + "MaxWaitForKernelInSeconds": 1, + "ApiKey": "mock-api-key", + "ModelId": "mock-model", + "ServiceId": "mock-service-id", + "Endpoint": "https://mock.endpoint", + "Scopes": [ + "test-scope" + ] + }, + { + "UniqueName": "MockAzureOpenAI", + "ServiceType": "AzureOpenAI", + "InstanceCount": 2, + "DeploymentTextEmbedding": "mock-embedding", + "MaxWaitForKernelInSeconds": 1, + "ApiKey": "mock-api-key", + "ModelId": "mock-model", + "ServiceId": "mock-service-id", + "Endpoint": "https://mock.openai.azure.com", + "DeploymentName": "DeploymentName", + "Scopes": [ + "test-scope" + ] + } + ] +} \ No newline at end of file From f0162afaa1ad5a4651d8ff8dbecd342474c525f1 Mon Sep 17 00:00:00 2001 From: Rafael Azriaiev Date: Tue, 31 Dec 2024 14:44:30 +0200 Subject: [PATCH 05/12] renamed context to fixture and update Fixture and created 3 simple Tests --- .../KernelPoolManagerTests.cs | 136 ++++++++++++ .../KernelPoolTestContext.cs | 204 ------------------ .../SemanticKernelPoolTestFixture.cs | 162 ++++++++++++++ .../SemanticKernelPooling.Tests.Unit.csproj | 7 + 4 files changed, 305 insertions(+), 204 deletions(-) create mode 100644 SemanticKernelPooling.Tests.Unit/KernelPoolManagerTests.cs delete mode 100644 SemanticKernelPooling.Tests.Unit/KernelPoolTestContext.cs create mode 100644 SemanticKernelPooling.Tests.Unit/SemanticKernelPoolTestFixture.cs diff --git a/SemanticKernelPooling.Tests.Unit/KernelPoolManagerTests.cs b/SemanticKernelPooling.Tests.Unit/KernelPoolManagerTests.cs new file mode 100644 index 0000000..170e4cf --- /dev/null +++ b/SemanticKernelPooling.Tests.Unit/KernelPoolManagerTests.cs @@ -0,0 +1,136 @@ +using Microsoft.Extensions.DependencyInjection; +using System.Net; +using Xunit.Abstractions; + +namespace SemanticKernelPooling.Tests.Unit; + +/// +/// Test class for kernel pool manager functionality +/// +public class KernelPoolManagerTests : IClassFixture +{ + private readonly IServiceProvider _serviceProvider; + private readonly SemanticKernelPoolTestFixture _fixture; + private const string TestScope = "test-scope"; + private const int PoolSize = 2; + private ITestOutputHelper OutputHelper => _fixture.TestOutputHelper; + + public KernelPoolManagerTests(SemanticKernelPoolTestFixture fixture, ITestOutputHelper outputHelper) + { + _fixture = fixture; + _serviceProvider = fixture.ServiceProvider; + _fixture.ConfigureLogging(outputHelper); + OutputHelper.WriteLine("Initializing KernelPoolManagerTests"); + SetupMockResponses(); + } + + [Fact] + public async Task GetKernelByScope_ReturnsValidKernel() + { + OutputHelper.WriteLine("Starting GetKernelByScope_ReturnsValidKernel test"); + + // Arrange + OutputHelper.WriteLine("Getting kernel pool manager from service provider"); + var kernelPoolManager = _serviceProvider.GetRequiredService(); + + // Act + OutputHelper.WriteLine($"Attempting to get kernel by scope: {TestScope}"); + using var kernelWrapper = await kernelPoolManager.GetKernelByScopeAsync(TestScope); + + // Assert + OutputHelper.WriteLine("Verifying kernel wrapper properties"); + Assert.NotNull(kernelWrapper); + Assert.NotNull(kernelWrapper.Kernel); + Assert.Contains(TestScope, kernelWrapper.Scopes); + OutputHelper.WriteLine("GetKernelByScope_ReturnsValidKernel test completed successfully"); + } + + [Fact] + public async Task GetKernelByType_ReturnsValidKernel() + { + OutputHelper.WriteLine("Starting GetKernelByType_ReturnsValidKernel test"); + + // Arrange + var kernelPoolManager = _serviceProvider.GetRequiredService(); + + // Act + OutputHelper.WriteLine($"Attempting to get kernel by type: {AIServiceProviderType.OpenAI}"); + using var kernelWrapper = await kernelPoolManager.GetKernelAsync(AIServiceProviderType.OpenAI); + + // Assert + OutputHelper.WriteLine("Verifying kernel wrapper properties"); + Assert.NotNull(kernelWrapper); + Assert.NotNull(kernelWrapper.Kernel); + Assert.Equal(AIServiceProviderType.OpenAI, kernelWrapper.ServiceProviderType); + OutputHelper.WriteLine("GetKernelByType test completed successfully"); + } + + [Fact] + public async Task GetKernelByName_ReturnsCorrectKernel() + { + OutputHelper.WriteLine("Starting GetKernelByName_ReturnsCorrectKernel test"); + + // Arrange + var kernelPoolManager = _serviceProvider.GetRequiredService(); + const string kernelName = "MockOpenAI"; + + // Act + OutputHelper.WriteLine($"Attempting to get kernel by name: {kernelName}"); + using var kernelWrapper = await kernelPoolManager.GetKernelByNameAsync(kernelName); + + // Assert + OutputHelper.WriteLine("Verifying kernel wrapper properties"); + Assert.NotNull(kernelWrapper); + Assert.NotNull(kernelWrapper.Kernel); + Assert.Equal(AIServiceProviderType.OpenAI, kernelWrapper.ServiceProviderType); + OutputHelper.WriteLine("GetKernelByName test completed successfully"); + } + + //[Fact] + //public async Task MultipleKernels_RoundRobinSelection() + //{ + // OutputHelper.WriteLine("Starting MultipleKernels_RoundRobinSelection test"); + + // // Arrange + // var kernelPoolManager = _serviceProvider.GetRequiredService(); + // var kernels = new HashSet(); + + // // Act + // OutputHelper.WriteLine($"Requesting {PoolSize} kernels to verify round-robin selection"); + // for (int i = 0; i < PoolSize; i++) + // { + // OutputHelper.WriteLine($"Getting kernel {i + 1}"); + // using var wrapper = await kernelPoolManager.GetKernelByScopeAsync(TestScope); + // kernels.Add(wrapper.Kernel); + // } + + // // Assert + // OutputHelper.WriteLine("Verifying unique kernel count"); + // Assert.Equal(PoolSize, kernels.Count); + // OutputHelper.WriteLine("Round-robin selection test completed successfully"); + //} + + private void SetupMockResponses() + { + OutputHelper.WriteLine("Setting up mock responses for chat completions"); + var mockResponse = @"{ + ""id"": ""mock-id"", + ""object"": ""chat.completion"", + ""created"": 1700000000, + ""model"": ""mock-model"", + ""choices"": [ + { + ""index"": 0, + ""message"": { + ""role"": ""assistant"", + ""content"": ""Mock response"" + }, + ""finish_reason"": ""stop"" + } + ] + }"; + + _fixture.AddMockResponse("POST:/v1/chat/completions", HttpStatusCode.OK, mockResponse); + OutputHelper.WriteLine("Mock responses setup completed"); + } +} \ No newline at end of file diff --git a/SemanticKernelPooling.Tests.Unit/KernelPoolTestContext.cs b/SemanticKernelPooling.Tests.Unit/KernelPoolTestContext.cs deleted file mode 100644 index 3140785..0000000 --- a/SemanticKernelPooling.Tests.Unit/KernelPoolTestContext.cs +++ /dev/null @@ -1,204 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Moq; -using SemanticKernelPooling.Tests.Mocks; -using System.Net; -using System.Text.Json; - -namespace SemanticKernelPooling.Tests.Unit; - -/// -/// Provides a shared test context for kernel pool tests, managing mock configurations -/// and service setup that can be reused across multiple test cases. -/// -/// -/// -/// This context handles the initialization of mock services, configuration, and response management -/// required for testing kernel pool functionality without real service dependencies. -/// -/// -/// Example usage: -/// -/// public class MyTests : IClassFixture<KernelPoolTestContext> -/// { -/// private readonly KernelPoolTestContext _context; -/// -/// public MyTests(KernelPoolTestContext context) -/// { -/// _context = context; -/// } -/// } -/// -/// -/// -public class KernelPoolTestContext : IDisposable -{ - /// - /// Gets the configured service provider for test cases. - /// - public IServiceProvider ServiceProvider { get; } - - private readonly ServiceProvider _disposableServiceProvider; - private readonly string _mockResponsesPath; - public const string TestScope = "test-scope"; - - /// - /// Initializes a new instance of the class. - /// Sets up the mock configuration, services, and response directory needed for testing. - /// - public KernelPoolTestContext() - { - _mockResponsesPath = Path.Combine(Path.GetTempPath(), "MockResponses"); - Directory.CreateDirectory(_mockResponsesPath); - - // Create mock configuration - var mockConfigurationSection = new Mock(); - mockConfigurationSection.Setup(x => x.GetChildren()) - .Returns(new List - { - CreateMockConfigSection("MockOpenAI", AIServiceProviderType.OpenAI, new[] { TestScope }), - CreateMockConfigSection("MockAzureOpenAI", AIServiceProviderType.AzureOpenAI, new[] { TestScope }) - }); - - var mockConfiguration = new Mock(); - mockConfiguration - .Setup(x => x.GetSection("AIServiceProviderConfigurations")) - .Returns(mockConfigurationSection.Object); - - // Set up services - var services = new ServiceCollection(); - services.AddSingleton(mockConfiguration.Object); - services.UseSemanticKernelPooling(); - - _disposableServiceProvider = services.BuildServiceProvider(); - _disposableServiceProvider.UseMockKernelPool(_mockResponsesPath); - - ServiceProvider = _disposableServiceProvider; - } - - /// - /// Adds a mock HTTP response to be used in tests. - /// - /// The request key to match for this response (e.g., "POST:/v1/chat/completions"). - /// The HTTP status code to return. - /// The response content to return. - /// - /// - /// The mock response will be saved as a JSON file and used by the mock HTTP handler - /// when matching requests during tests. - /// - /// - /// Example usage: - /// - /// context.AddMockResponse( - /// "POST:/v1/chat/completions", - /// HttpStatusCode.OK, - /// "{\"choices\": [{\"message\": {\"content\": \"Test response\"}}]}"); - /// - /// - /// - public void AddMockResponse(string requestKey, HttpStatusCode statusCode, string content) - { - var response = new MockResponse - { - RequestKey = requestKey, - StatusCode = statusCode, - Content = content, - Headers = new Dictionary - { - { "Content-Type", new[] { "application/json" } } - } - }; - - var filePath = Path.Combine(_mockResponsesPath, $"{Guid.NewGuid()}.json"); - File.WriteAllText(filePath, JsonSerializer.Serialize(response)); - } - - /// - /// Creates a mock configuration section for testing kernel pool functionality. - /// - /// The unique name for identifying the configuration instance. - /// The type of AI service provider (e.g., OpenAI, AzureOpenAI). - /// An array of scope names associated with this configuration. - /// A mocked with all required configuration values. - /// - /// - /// This method creates a mock configuration section that simulates the structure and values - /// that would typically come from appsettings.json or other configuration sources. - /// - /// - /// The mock configuration includes all required properties for both standard and Azure-specific - /// configurations, ensuring compatibility with different service provider types. - /// - /// - /// Example usage: - /// - /// var configSection = CreateMockConfigSection( - /// "TestOpenAI", - /// AIServiceProviderType.OpenAI, - /// new[] { "test-scope" }); - /// - /// - /// - /// - /// - private IConfigurationSection CreateMockConfigSection( - string uniqueName, - AIServiceProviderType serviceType, - string[] scopes) - { - ArgumentNullException.ThrowIfNull(uniqueName); - ArgumentNullException.ThrowIfNull(scopes); - - var mockSection = new Mock(); - - // Setup AIServiceProviderConfiguration base properties - mockSection.Setup(x => x.GetValue("UniqueName")) - .Returns(uniqueName); - mockSection.Setup(x => x.GetValue("ServiceType")) - .Returns(serviceType); - mockSection.Setup(x => x.GetSection("Scopes").Get>()) - .Returns(scopes.ToList()); - mockSection.Setup(x => x.GetValue("InstanceCount")) - .Returns(2); - mockSection.Setup(x => x.GetValue("DeploymentTextEmbedding")) - .Returns("mock-embedding"); - mockSection.Setup(x => x.GetValue("MaxWaitForKernelInSeconds")) - .Returns(30); - - // Setup Mock-specific properties - mockSection.Setup(x => x.GetValue("ApiKey")) - .Returns("mock-api-key"); - mockSection.Setup(x => x.GetValue("ModelId")) - .Returns("mock-model"); - mockSection.Setup(x => x.GetValue("ServiceId")) - .Returns("mock-service-id"); - mockSection.Setup(x => x.GetValue("Endpoint")) - .Returns("https://mock.endpoint"); - - return mockSection.Object; - } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - /// - /// Cleans up both the service provider and any mock response files created during testing. - /// - public void Dispose() - { - _disposableServiceProvider?.Dispose(); - - try - { - if (Directory.Exists(_mockResponsesPath)) - { - Directory.Delete(_mockResponsesPath, true); - } - } - catch (Exception) - { - // Log or handle cleanup failure - } - } -} \ No newline at end of file diff --git a/SemanticKernelPooling.Tests.Unit/SemanticKernelPoolTestFixture.cs b/SemanticKernelPooling.Tests.Unit/SemanticKernelPoolTestFixture.cs new file mode 100644 index 0000000..f44e7e0 --- /dev/null +++ b/SemanticKernelPooling.Tests.Unit/SemanticKernelPoolTestFixture.cs @@ -0,0 +1,162 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using SemanticKernelPooling.Tests.Mocks; +using SemanticKernelPooling.Tests.Unit.Logging; +using System.Net; +using System.Text.Json; +using Xunit.Abstractions; + +namespace SemanticKernelPooling.Tests.Unit; + +/// +/// Test fixture for Semantic Kernel Pool tests that provides configured services, +/// logging, and mock response handling capabilities. +/// +public class SemanticKernelPoolTestFixture : IDisposable +{ + /// + /// Gets the configured service provider containing all required services for testing. + /// + public IServiceProvider ServiceProvider { get; private set; } = null!; + + /// + /// Gets the test output helper for logging test information. + /// + public ITestOutputHelper TestOutputHelper + { + get + { + if (_testOutputHelper == null) + throw new InvalidOperationException("TestOutputHelper not configured. Call ConfigureLogging first."); + return _testOutputHelper; + } + private set => _testOutputHelper = value; + } + + private readonly string _mockResponsesPath; + private ILoggerProvider? _loggerProvider; + private ITestOutputHelper? _testOutputHelper; + + /// + /// The scope used for test configurations. + /// + public const string TestScope = "test-scope"; + + /// + /// The size of the kernel pool for testing. + /// + public const int PoolSize = 2; + + /// + /// The mock deployment name used in test configurations. + /// + public const string DeploymentName = "mock-deployment"; + + /// + /// Initializes a new instance of the class. + /// + public SemanticKernelPoolTestFixture() + { + _mockResponsesPath = Path.Combine(Path.GetTempPath(), "MockResponses"); + Directory.CreateDirectory(_mockResponsesPath); + InitializeServices(); + } + + private void InitializeServices() + { + var services = new ServiceCollection(); + + services.AddLogging(builder => + { + builder.ClearProviders(); + builder.SetMinimumLevel(LogLevel.Debug); + }); + + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: false) + .Build(); + + services.AddSingleton(configuration); + services.UseSemanticKernelPooling(); + + ServiceProvider = services.BuildServiceProvider(); + ServiceProvider.UseMockKernelPool(_mockResponsesPath); + } + + /// + /// Configures logging for a specific test, ensuring test output is properly captured. + /// + /// The test output helper provided by XUnit. + public void ConfigureLogging(ITestOutputHelper testOutputHelper) + { + TestOutputHelper = testOutputHelper; // Set the property + var loggerFactory = ServiceProvider.GetRequiredService(); + + _loggerProvider?.Dispose(); + _loggerProvider = new XUnitLoggerProvider(testOutputHelper); + loggerFactory.AddProvider(_loggerProvider); + + TestOutputHelper.WriteLine("Logging configured successfully"); + } + + /// + /// Adds a mock response for API calls during testing. + /// + /// The key identifying the request to mock. + /// The HTTP status code to return. + /// The content to return in the response. + public void AddMockResponse(string requestKey, HttpStatusCode statusCode, string content) + { + TestOutputHelper.WriteLine($"Adding mock response for request key: {requestKey}"); + + var response = new MockResponse + { + RequestKey = requestKey, + StatusCode = statusCode, + Content = content, + Headers = new Dictionary + { + { "Content-Type", new[] { "application/json" } } + } + }; + + var filePath = Path.Combine(_mockResponsesPath, $"{Guid.NewGuid()}.json"); + File.WriteAllText(filePath, JsonSerializer.Serialize(response)); + TestOutputHelper.WriteLine($"Mock response saved to: {filePath}"); + } + + /// + /// Performs cleanup of managed and unmanaged resources. + /// + public void Dispose() + { + if (ServiceProvider is IDisposable disposable) + { + disposable.Dispose(); + } + _loggerProvider?.Dispose(); + + try + { + if (Directory.Exists(_mockResponsesPath)) + { + Directory.Delete(_mockResponsesPath, true); + } + } + catch (Exception ex) + { + // Since we're disposing, we might not have access to TestOutputHelper, + // but we'll try to log if we can + try + { + TestOutputHelper.WriteLine($"Error cleaning up mock responses directory: {ex.Message}"); + } + catch + { + // Nothing we can do here if TestOutputHelper is not available + } + } + } +} \ No newline at end of file diff --git a/SemanticKernelPooling.Tests.Unit/SemanticKernelPooling.Tests.Unit.csproj b/SemanticKernelPooling.Tests.Unit/SemanticKernelPooling.Tests.Unit.csproj index 5ff7b38..cfa01fa 100644 --- a/SemanticKernelPooling.Tests.Unit/SemanticKernelPooling.Tests.Unit.csproj +++ b/SemanticKernelPooling.Tests.Unit/SemanticKernelPooling.Tests.Unit.csproj @@ -7,6 +7,12 @@ false + + + PreserveNewest + + + all @@ -14,6 +20,7 @@ + From 2d70880f1b596b98cd68ae147b7e47e15e900d53 Mon Sep 17 00:00:00 2001 From: Rafael Azriaiev Date: Wed, 1 Jan 2025 10:13:40 +0200 Subject: [PATCH 06/12] updated GetKernelByName linq method to FirstOrDefault instead of First so we throw our own exception instead of linq's --- SemanticKernelPooling/KernelPoolManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SemanticKernelPooling/KernelPoolManager.cs b/SemanticKernelPooling/KernelPoolManager.cs index 5961f54..97df982 100644 --- a/SemanticKernelPooling/KernelPoolManager.cs +++ b/SemanticKernelPooling/KernelPoolManager.cs @@ -109,7 +109,7 @@ private async Task GetKernelAsync(AIServiceProviderConfiguration public async Task GetKernelByNameAsync(string uniqueName) { //get the service configuration - var config = AIConfigurations.First(c => c.UniqueName == uniqueName); + var config = AIConfigurations.FirstOrDefault(c => c.UniqueName == uniqueName); if (config == null) throw new InvalidOperationException($"No configuration found for kernel pool name {uniqueName}"); From 57d30058122e30d162be3c80e8d5535d00981253 Mon Sep 17 00:00:00 2001 From: Rafael Azriaiev Date: Wed, 1 Jan 2025 17:16:33 +0200 Subject: [PATCH 07/12] removed old tes class sepreated test classes By Get, added tests to GetByNameTests class --- .../KernelPoolManagerGetByNameTests.cs | 353 ++++++++++++++++++ .../KernelPoolManagerGetByScopeTests.cs | 19 + .../KernelPoolManagerGetByServiceTypeTests.cs | 19 + .../KernelPoolManagerTestCollection.cs | 7 + .../KernelPoolManagerTests.cs | 136 ------- .../SemanticKernelPoolTestFixture.cs | 6 +- 6 files changed, 401 insertions(+), 139 deletions(-) create mode 100644 SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByNameTests.cs create mode 100644 SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByScopeTests.cs create mode 100644 SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByServiceTypeTests.cs create mode 100644 SemanticKernelPooling.Tests.Unit/KernelPoolManagerTestCollection.cs delete mode 100644 SemanticKernelPooling.Tests.Unit/KernelPoolManagerTests.cs diff --git a/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByNameTests.cs b/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByNameTests.cs new file mode 100644 index 0000000..86b72a6 --- /dev/null +++ b/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByNameTests.cs @@ -0,0 +1,353 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using SemanticKernelPooling.Tests.Mocks; +using Xunit.Abstractions; + +namespace SemanticKernelPooling.Tests.Unit; + +[Collection("KernelPoolManagerTests")] +public class KernelPoolManagerGetByNameTests +{ + private IServiceProvider _serviceProvider => _fixture.ServiceProvider; + private readonly SemanticKernelPoolTestFixture _fixture; + private ITestOutputHelper OutputHelper => _fixture.TestOutputHelper; + private const string ValidUniqueName = "MockOpenAI"; + private int PoolSize => _fixture.PoolSize; + private string TestScope => _fixture.TestScope; + + public KernelPoolManagerGetByNameTests(SemanticKernelPoolTestFixture fixture, ITestOutputHelper outputHelper) + { + _fixture = fixture; + _fixture.ConfigureLogging(outputHelper); + _fixture.InitializeServices(); + OutputHelper.WriteLine("Initializing KernelPoolManagerGetByNameTests"); + } + + [Fact] + public async Task GetKernelByName_WithValidName_ReturnsKernelWithCorrectServiceType() + { + OutputHelper.WriteLine($"Starting test: GetKernelByName with valid name '{ValidUniqueName}'"); + + var kernelPoolManager = _serviceProvider.GetRequiredService(); + + using var kernelWrapper = await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName); + OutputHelper.WriteLine($"Retrieved kernel details:"); + OutputHelper.WriteLine($"- Service Type: {kernelWrapper.ServiceProviderType}"); + OutputHelper.WriteLine($"- Kernel Instance: {kernelWrapper.Kernel.GetHashCode()}"); + + Assert.NotNull(kernelWrapper); + Assert.NotNull(kernelWrapper.Kernel); + Assert.Equal(AIServiceProviderType.OpenAI, kernelWrapper.ServiceProviderType); + OutputHelper.WriteLine($"Successfully verified kernel properties"); + } + + [Fact] + public async Task GetKernelByName_WhenHoldingTwoKernels_BothAreDifferentInstances() + { + OutputHelper.WriteLine($"Starting test: GetKernelByName requesting two kernels simultaneously"); + + var kernelPoolManager = _serviceProvider.GetRequiredService(); + + using var wrapper1 = await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName); + OutputHelper.WriteLine($"First kernel obtained:"); + OutputHelper.WriteLine($"- Instance Hash: {wrapper1.Kernel.GetHashCode()}"); + + using var wrapper2 = await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName); + OutputHelper.WriteLine($"Second kernel obtained:"); + OutputHelper.WriteLine($"- Instance Hash: {wrapper2.Kernel.GetHashCode()}"); + + Assert.NotSame(wrapper1.Kernel, wrapper2.Kernel); + OutputHelper.WriteLine("Verified both kernels are different instances"); + } + + [Fact] + public async Task GetKernelByName_WhenDisposingAndReacquiring_ReusesKernel() + { + OutputHelper.WriteLine($"Starting test: GetKernelByName with disposal and reacquisition"); + + var kernelPoolManager = _serviceProvider.GetRequiredService(); + + Kernel firstKernel; + using (var wrapper1 = await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName)) + { + firstKernel = wrapper1.Kernel; + OutputHelper.WriteLine($"First kernel obtained:"); + OutputHelper.WriteLine($"- Instance Hash: {firstKernel.GetHashCode()}"); + } + OutputHelper.WriteLine("First kernel disposed and returned to pool"); + + using var wrapper2 = await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName); + OutputHelper.WriteLine($"Second kernel obtained:"); + OutputHelper.WriteLine($"- Instance Hash: {wrapper2.Kernel.GetHashCode()}"); + + Assert.Same(firstKernel, wrapper2.Kernel); + OutputHelper.WriteLine("Verified second kernel is same instance as first"); + } + + [Fact] + public async Task GetKernelByName_WithInvalidName_ThrowsInvalidOperationException() + { + OutputHelper.WriteLine("Starting test: GetKernelByName with invalid name"); + + // Arrange + var kernelPoolManager = _serviceProvider.GetRequiredService(); + const string invalidName = "NonExistentKernel"; + OutputHelper.WriteLine($"Attempting to get kernel with invalid name: {invalidName}"); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + async () => await kernelPoolManager.GetKernelByNameAsync(invalidName)); + + OutputHelper.WriteLine($"Exception details:"); + OutputHelper.WriteLine($"- Type: {exception.GetType().Name}"); + OutputHelper.WriteLine($"- Message: {exception.Message}"); + + // Verify the exception type and a more generic error message + Assert.NotNull(exception); + Assert.IsType(exception); + Assert.Contains($"No configuration found for kernel pool name {invalidName}", exception.Message); + + OutputHelper.WriteLine("Successfully verified exception type and message"); + } + + [Fact] + public async Task GetKernelByName_WhenPoolExhausted_WaitsForTimeoutThenThrows() + { + OutputHelper.WriteLine("Starting test: GetKernelByName with pool exhaustion and timeout"); + + var kernelPoolManager = _serviceProvider.GetRequiredService(); + const int expectedTimeoutSeconds = 1; // From appsettings + var timeoutBuffer = TimeSpan.FromSeconds(0.5); // Add buffer for system overhead + + try + { + // Hold both available kernels + using var wrapper1 = await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName); + OutputHelper.WriteLine($"First kernel obtained: Hash {wrapper1.Kernel.GetHashCode()}"); + + using var wrapper2 = await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName); + OutputHelper.WriteLine($"Second kernel obtained: Hash {wrapper2.Kernel.GetHashCode()}"); + OutputHelper.WriteLine($"Pool is now exhausted (2/2 kernels in use)"); + + // Try to get a third kernel - should wait then throw + var startTime = DateTime.UtcNow; + OutputHelper.WriteLine($"Attempting to get third kernel at {startTime:HH:mm:ss.fff}"); + OutputHelper.WriteLine($"Should wait for {expectedTimeoutSeconds} seconds before throwing"); + + // Create a task that attempts to get a kernel + var getKernelTask = kernelPoolManager.GetKernelByNameAsync(ValidUniqueName); + + // Wait for either the timeout period or task completion + var completedTask = await Task.WhenAny( + getKernelTask, + Task.Delay(TimeSpan.FromSeconds(expectedTimeoutSeconds) + timeoutBuffer) + ); + + var endTime = DateTime.UtcNow; + var duration = endTime - startTime; + + // If the kernel task completed, it should have thrown an exception + if (completedTask == getKernelTask) + { + var exception = await Assert.ThrowsAsync( + async () => await getKernelTask); + + OutputHelper.WriteLine($"Exception thrown at {endTime:HH:mm:ss.fff}"); + OutputHelper.WriteLine($"Actual wait duration: {duration.TotalSeconds:F3} seconds"); + OutputHelper.WriteLine($"Exception message: {exception.Message}"); + + // Verify the timeout duration + Assert.True(duration.TotalSeconds >= expectedTimeoutSeconds, + $"Timeout occurred after {duration.TotalSeconds:F3} seconds, expected at least {expectedTimeoutSeconds} seconds"); + + Assert.Contains("No available kernels", exception.Message); + } + else + { + // If we reached here without an exception, the test failed + Assert.Fail($"Test timed out after {duration.TotalSeconds:F3} seconds without throwing expected exception"); + } + } + finally + { + OutputHelper.WriteLine("Test completed"); + } + } + + [Fact] + public async Task GetKernelByName_WhenKernelDisposedMultipleTimes_HandlesGracefully() + { + OutputHelper.WriteLine("Starting test: GetKernelByName with multiple dispose calls"); + + var kernelPoolManager = _serviceProvider.GetRequiredService(); + + var wrapper = await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName); + var kernelHash = wrapper.Kernel.GetHashCode(); + OutputHelper.WriteLine($"Obtained kernel: Hash {kernelHash}"); + + // First dispose - should work normally + wrapper.Dispose(); + OutputHelper.WriteLine("First dispose completed"); + + // Second dispose - should not throw + wrapper.Dispose(); + OutputHelper.WriteLine("Second dispose completed without error"); + + // Verify we can get the same kernel back + using var newWrapper = await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName); + OutputHelper.WriteLine($"Retrieved new kernel: Hash {newWrapper.Kernel.GetHashCode()}"); + + Assert.Equal(kernelHash, newWrapper.Kernel.GetHashCode()); + } + + [Fact] + public async Task GetKernelByName_UnderLoad_MaintainsPoolSize() + { + OutputHelper.WriteLine("Starting test: GetKernelByName under load conditions"); + + var kernelPoolManager = _serviceProvider.GetRequiredService(); + var kernelHashes = new HashSet(); + const int numberOfIterations = 10; + + for (int i = 0; i < numberOfIterations; i++) + { + OutputHelper.WriteLine($"Iteration {i + 1} of {numberOfIterations}"); + var wrappers = new List(); + + // Request maximum pool size kernels + for (int j = 0; j < PoolSize; j++) + { + var wrapper = await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName); + wrappers.Add(wrapper); + kernelHashes.Add(wrapper.Kernel.GetHashCode()); + OutputHelper.WriteLine($"Got kernel: Hash {wrapper.Kernel.GetHashCode()}"); + } + + // Dispose all kernels + foreach (var wrapper in wrappers) + { + wrapper.Dispose(); + } + OutputHelper.WriteLine("All kernels returned to pool"); + } + + // Verify we never created more unique kernels than the pool size + OutputHelper.WriteLine($"Total unique kernels created: {kernelHashes.Count}"); + Assert.Equal(PoolSize, kernelHashes.Count); + } + + [Fact(Skip = "Need to add possibility to cancel kernel request")] + public async Task GetKernelByName_WithCancellation_StopsWaiting() + { + OutputHelper.WriteLine("Starting test: GetKernelByName with cancellation"); + + var kernelPoolManager = _serviceProvider.GetRequiredService(); + var cts = new CancellationTokenSource(); + + // Hold all available kernels + var heldKernels = new List(); + for (int i = 0; i < PoolSize; i++) + { + var wrapper = await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName); + heldKernels.Add(wrapper); + OutputHelper.WriteLine($"Holding kernel {i + 1}: Hash {wrapper.Kernel.GetHashCode()}"); + } + + try + { + // Try to get another kernel with cancellation + var getKernelTask = Task.Run(async () => + { + try + { + return await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName); + } + catch (OperationCanceledException) + { + OutputHelper.WriteLine("Task was cancelled as expected"); + throw; + } + }, cts.Token); + + // Cancel after a short delay + await Task.Delay(100); + cts.Cancel(); + OutputHelper.WriteLine("Cancellation requested"); + + // Verify the operation was cancelled + await Assert.ThrowsAsync(() => getKernelTask); + } + finally + { + // Cleanup + foreach (var wrapper in heldKernels) + { + wrapper.Dispose(); + } + cts.Dispose(); + } + } + + [Fact] + public async Task GetKernelByName_WhenDisposedInDifferentThread_HandlesCorrectly() + { + OutputHelper.WriteLine("Starting test: GetKernelByName with disposal in different thread"); + + var kernelPoolManager = _serviceProvider.GetRequiredService(); + KernelWrapper wrapper = null!; + int kernelHash = 0; + + // Get kernel in main thread + wrapper = await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName); + kernelHash = wrapper.Kernel.GetHashCode(); + OutputHelper.WriteLine($"Got kernel in main thread: Hash {kernelHash}"); + + // Dispose in different thread + await Task.Run(() => + { + OutputHelper.WriteLine($"Disposing kernel in separate thread: Hash {kernelHash}"); + wrapper.Dispose(); + }); + + // Verify we can get the same kernel back + using var newWrapper = await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName); + OutputHelper.WriteLine($"Retrieved kernel back in main thread: Hash {newWrapper.Kernel.GetHashCode()}"); + Assert.Equal(kernelHash, newWrapper.Kernel.GetHashCode()); + } + + [Fact] + public async Task GetKernelByName_WithRapidRequestAndDispose_HandlesRaceConditions() + { + OutputHelper.WriteLine("Starting test: GetKernelByName with rapid request/dispose cycles"); + + var kernelPoolManager = _serviceProvider.GetRequiredService(); + const int numberOfCycles = 20; + var tasks = new List(); + var startTime = DateTime.UtcNow; + + for (int i = 0; i < numberOfCycles; i++) + { + var cycleIndex = i; + var task = Task.Run(async () => + { + OutputHelper.WriteLine($"Cycle {cycleIndex}: Requesting kernel at {(DateTime.UtcNow - startTime).TotalMilliseconds:F2}ms"); + + using var wrapper = await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName); + var acquiredTime = DateTime.UtcNow; + OutputHelper.WriteLine($"Cycle {cycleIndex}: Acquired kernel Hash {wrapper.Kernel.GetHashCode()} at {(acquiredTime - startTime).TotalMilliseconds:F2}ms"); + + // Add small random delay to simulate some work + await Task.Delay(Random.Shared.Next(10, 50)); + + // Log before disposal + var disposingTime = DateTime.UtcNow; + OutputHelper.WriteLine($"Cycle {cycleIndex}: Disposing kernel Hash {wrapper.Kernel.GetHashCode()} at {(disposingTime - startTime).TotalMilliseconds:F2}ms"); + }); + tasks.Add(task); + } + + await Task.WhenAll(tasks); + var endTime = DateTime.UtcNow; + OutputHelper.WriteLine($"All cycles completed after {(endTime - startTime).TotalMilliseconds:F2}ms"); + } +} \ No newline at end of file diff --git a/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByScopeTests.cs b/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByScopeTests.cs new file mode 100644 index 0000000..703174c --- /dev/null +++ b/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByScopeTests.cs @@ -0,0 +1,19 @@ +using Xunit.Abstractions; + +namespace SemanticKernelPooling.Tests.Unit; + +[Collection("KernelPoolManagerTests")] +public class KernelPoolManagerGetByScopeTests +{ + private IServiceProvider _serviceProvider => _fixture.ServiceProvider; + private readonly SemanticKernelPoolTestFixture _fixture; + private ITestOutputHelper OutputHelper => _fixture.TestOutputHelper; + + public KernelPoolManagerGetByScopeTests(SemanticKernelPoolTestFixture fixture, ITestOutputHelper outputHelper) + { + _fixture = fixture; + _fixture.ConfigureLogging(outputHelper); + _fixture.InitializeServices(); + OutputHelper.WriteLine("Initializing KernelPoolManagerGetByScopeTests"); + } +} \ No newline at end of file diff --git a/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByServiceTypeTests.cs b/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByServiceTypeTests.cs new file mode 100644 index 0000000..3698f76 --- /dev/null +++ b/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByServiceTypeTests.cs @@ -0,0 +1,19 @@ +using Xunit.Abstractions; + +namespace SemanticKernelPooling.Tests.Unit; + +[Collection("KernelPoolManagerTests")] +public class KernelPoolManagerGetByServiceTypeTests +{ + private IServiceProvider _serviceProvider => _fixture.ServiceProvider; + private readonly SemanticKernelPoolTestFixture _fixture; + private ITestOutputHelper OutputHelper => _fixture.TestOutputHelper; + + public KernelPoolManagerGetByServiceTypeTests(SemanticKernelPoolTestFixture fixture, ITestOutputHelper outputHelper) + { + _fixture = fixture; + _fixture.ConfigureLogging(outputHelper); + _fixture.InitializeServices(); + OutputHelper.WriteLine("Initializing KernelPoolManagerGetByServiceTypeTests"); + } +} \ No newline at end of file diff --git a/SemanticKernelPooling.Tests.Unit/KernelPoolManagerTestCollection.cs b/SemanticKernelPooling.Tests.Unit/KernelPoolManagerTestCollection.cs new file mode 100644 index 0000000..f41ea42 --- /dev/null +++ b/SemanticKernelPooling.Tests.Unit/KernelPoolManagerTestCollection.cs @@ -0,0 +1,7 @@ +namespace SemanticKernelPooling.Tests.Unit; + +[CollectionDefinition("KernelPoolManagerTests")] +public class KernelPoolManagerTestCollection : ICollectionFixture +{ + // This class is just a marker for the collection, it remains empty +} diff --git a/SemanticKernelPooling.Tests.Unit/KernelPoolManagerTests.cs b/SemanticKernelPooling.Tests.Unit/KernelPoolManagerTests.cs deleted file mode 100644 index 170e4cf..0000000 --- a/SemanticKernelPooling.Tests.Unit/KernelPoolManagerTests.cs +++ /dev/null @@ -1,136 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using System.Net; -using Xunit.Abstractions; - -namespace SemanticKernelPooling.Tests.Unit; - -/// -/// Test class for kernel pool manager functionality -/// -public class KernelPoolManagerTests : IClassFixture -{ - private readonly IServiceProvider _serviceProvider; - private readonly SemanticKernelPoolTestFixture _fixture; - private const string TestScope = "test-scope"; - private const int PoolSize = 2; - private ITestOutputHelper OutputHelper => _fixture.TestOutputHelper; - - public KernelPoolManagerTests(SemanticKernelPoolTestFixture fixture, ITestOutputHelper outputHelper) - { - _fixture = fixture; - _serviceProvider = fixture.ServiceProvider; - _fixture.ConfigureLogging(outputHelper); - OutputHelper.WriteLine("Initializing KernelPoolManagerTests"); - SetupMockResponses(); - } - - [Fact] - public async Task GetKernelByScope_ReturnsValidKernel() - { - OutputHelper.WriteLine("Starting GetKernelByScope_ReturnsValidKernel test"); - - // Arrange - OutputHelper.WriteLine("Getting kernel pool manager from service provider"); - var kernelPoolManager = _serviceProvider.GetRequiredService(); - - // Act - OutputHelper.WriteLine($"Attempting to get kernel by scope: {TestScope}"); - using var kernelWrapper = await kernelPoolManager.GetKernelByScopeAsync(TestScope); - - // Assert - OutputHelper.WriteLine("Verifying kernel wrapper properties"); - Assert.NotNull(kernelWrapper); - Assert.NotNull(kernelWrapper.Kernel); - Assert.Contains(TestScope, kernelWrapper.Scopes); - OutputHelper.WriteLine("GetKernelByScope_ReturnsValidKernel test completed successfully"); - } - - [Fact] - public async Task GetKernelByType_ReturnsValidKernel() - { - OutputHelper.WriteLine("Starting GetKernelByType_ReturnsValidKernel test"); - - // Arrange - var kernelPoolManager = _serviceProvider.GetRequiredService(); - - // Act - OutputHelper.WriteLine($"Attempting to get kernel by type: {AIServiceProviderType.OpenAI}"); - using var kernelWrapper = await kernelPoolManager.GetKernelAsync(AIServiceProviderType.OpenAI); - - // Assert - OutputHelper.WriteLine("Verifying kernel wrapper properties"); - Assert.NotNull(kernelWrapper); - Assert.NotNull(kernelWrapper.Kernel); - Assert.Equal(AIServiceProviderType.OpenAI, kernelWrapper.ServiceProviderType); - OutputHelper.WriteLine("GetKernelByType test completed successfully"); - } - - [Fact] - public async Task GetKernelByName_ReturnsCorrectKernel() - { - OutputHelper.WriteLine("Starting GetKernelByName_ReturnsCorrectKernel test"); - - // Arrange - var kernelPoolManager = _serviceProvider.GetRequiredService(); - const string kernelName = "MockOpenAI"; - - // Act - OutputHelper.WriteLine($"Attempting to get kernel by name: {kernelName}"); - using var kernelWrapper = await kernelPoolManager.GetKernelByNameAsync(kernelName); - - // Assert - OutputHelper.WriteLine("Verifying kernel wrapper properties"); - Assert.NotNull(kernelWrapper); - Assert.NotNull(kernelWrapper.Kernel); - Assert.Equal(AIServiceProviderType.OpenAI, kernelWrapper.ServiceProviderType); - OutputHelper.WriteLine("GetKernelByName test completed successfully"); - } - - //[Fact] - //public async Task MultipleKernels_RoundRobinSelection() - //{ - // OutputHelper.WriteLine("Starting MultipleKernels_RoundRobinSelection test"); - - // // Arrange - // var kernelPoolManager = _serviceProvider.GetRequiredService(); - // var kernels = new HashSet(); - - // // Act - // OutputHelper.WriteLine($"Requesting {PoolSize} kernels to verify round-robin selection"); - // for (int i = 0; i < PoolSize; i++) - // { - // OutputHelper.WriteLine($"Getting kernel {i + 1}"); - // using var wrapper = await kernelPoolManager.GetKernelByScopeAsync(TestScope); - // kernels.Add(wrapper.Kernel); - // } - - // // Assert - // OutputHelper.WriteLine("Verifying unique kernel count"); - // Assert.Equal(PoolSize, kernels.Count); - // OutputHelper.WriteLine("Round-robin selection test completed successfully"); - //} - - private void SetupMockResponses() - { - OutputHelper.WriteLine("Setting up mock responses for chat completions"); - var mockResponse = @"{ - ""id"": ""mock-id"", - ""object"": ""chat.completion"", - ""created"": 1700000000, - ""model"": ""mock-model"", - ""choices"": [ - { - ""index"": 0, - ""message"": { - ""role"": ""assistant"", - ""content"": ""Mock response"" - }, - ""finish_reason"": ""stop"" - } - ] - }"; - - _fixture.AddMockResponse("POST:/v1/chat/completions", HttpStatusCode.OK, mockResponse); - OutputHelper.WriteLine("Mock responses setup completed"); - } -} \ No newline at end of file diff --git a/SemanticKernelPooling.Tests.Unit/SemanticKernelPoolTestFixture.cs b/SemanticKernelPooling.Tests.Unit/SemanticKernelPoolTestFixture.cs index f44e7e0..e7c7e86 100644 --- a/SemanticKernelPooling.Tests.Unit/SemanticKernelPoolTestFixture.cs +++ b/SemanticKernelPooling.Tests.Unit/SemanticKernelPoolTestFixture.cs @@ -41,12 +41,12 @@ public ITestOutputHelper TestOutputHelper /// /// The scope used for test configurations. /// - public const string TestScope = "test-scope"; + public string TestScope = "test-scope"; /// /// The size of the kernel pool for testing. /// - public const int PoolSize = 2; + public int PoolSize = 2; /// /// The mock deployment name used in test configurations. @@ -63,7 +63,7 @@ public SemanticKernelPoolTestFixture() InitializeServices(); } - private void InitializeServices() + public void InitializeServices() { var services = new ServiceCollection(); From 63a9b2b3a14a74679b6f338e21de8426aaf067ed Mon Sep 17 00:00:00 2001 From: Rafael Azriaiev Date: Thu, 2 Jan 2025 15:02:26 +0200 Subject: [PATCH 08/12] Added Tests for GetByService - added unique name prop in wrapper - added 1 more configs for each provider type - added a second test-scope --- .../KernelPoolManagerGetByNameTests.cs | 64 ++--- .../KernelPoolManagerGetByServiceTypeTests.cs | 234 +++++++++++++++++- .../SemanticKernelPoolTestFixture.cs | 3 +- .../appsettings.json | 33 ++- SemanticKernelPooling/KernelWrapper.cs | 5 + 5 files changed, 288 insertions(+), 51 deletions(-) diff --git a/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByNameTests.cs b/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByNameTests.cs index 86b72a6..e5db807 100644 --- a/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByNameTests.cs +++ b/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByNameTests.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.SemanticKernel; -using SemanticKernelPooling.Tests.Mocks; using Xunit.Abstractions; namespace SemanticKernelPooling.Tests.Unit; @@ -13,7 +12,6 @@ public class KernelPoolManagerGetByNameTests private ITestOutputHelper OutputHelper => _fixture.TestOutputHelper; private const string ValidUniqueName = "MockOpenAI"; private int PoolSize => _fixture.PoolSize; - private string TestScope => _fixture.TestScope; public KernelPoolManagerGetByNameTests(SemanticKernelPoolTestFixture fixture, ITestOutputHelper outputHelper) { @@ -116,61 +114,33 @@ public async Task GetKernelByName_WhenPoolExhausted_WaitsForTimeoutThenThrows() OutputHelper.WriteLine("Starting test: GetKernelByName with pool exhaustion and timeout"); var kernelPoolManager = _serviceProvider.GetRequiredService(); - const int expectedTimeoutSeconds = 1; // From appsettings - var timeoutBuffer = TimeSpan.FromSeconds(0.5); // Add buffer for system overhead + var kernels = new List(); try { - // Hold both available kernels - using var wrapper1 = await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName); - OutputHelper.WriteLine($"First kernel obtained: Hash {wrapper1.Kernel.GetHashCode()}"); - - using var wrapper2 = await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName); - OutputHelper.WriteLine($"Second kernel obtained: Hash {wrapper2.Kernel.GetHashCode()}"); - OutputHelper.WriteLine($"Pool is now exhausted (2/2 kernels in use)"); - - // Try to get a third kernel - should wait then throw - var startTime = DateTime.UtcNow; - OutputHelper.WriteLine($"Attempting to get third kernel at {startTime:HH:mm:ss.fff}"); - OutputHelper.WriteLine($"Should wait for {expectedTimeoutSeconds} seconds before throwing"); - - // Create a task that attempts to get a kernel - var getKernelTask = kernelPoolManager.GetKernelByNameAsync(ValidUniqueName); - - // Wait for either the timeout period or task completion - var completedTask = await Task.WhenAny( - getKernelTask, - Task.Delay(TimeSpan.FromSeconds(expectedTimeoutSeconds) + timeoutBuffer) - ); - - var endTime = DateTime.UtcNow; - var duration = endTime - startTime; - - // If the kernel task completed, it should have thrown an exception - if (completedTask == getKernelTask) + // Exhaust all pools + for (int i = 0; i < PoolSize; i++) { - var exception = await Assert.ThrowsAsync( - async () => await getKernelTask); + var wrapper = await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName); + kernels.Add(wrapper); + OutputHelper.WriteLine($"Obtained kernel {i + 1}: Hash {wrapper.Kernel.GetHashCode()}"); + } - OutputHelper.WriteLine($"Exception thrown at {endTime:HH:mm:ss.fff}"); - OutputHelper.WriteLine($"Actual wait duration: {duration.TotalSeconds:F3} seconds"); - OutputHelper.WriteLine($"Exception message: {exception.Message}"); + var startTime = DateTime.UtcNow; + OutputHelper.WriteLine($"All pools exhausted, attempting to get another kernel at {startTime:HH:mm:ss.fff}"); - // Verify the timeout duration - Assert.True(duration.TotalSeconds >= expectedTimeoutSeconds, - $"Timeout occurred after {duration.TotalSeconds:F3} seconds, expected at least {expectedTimeoutSeconds} seconds"); + await Assert.ThrowsAsync( + async () => await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName)); - Assert.Contains("No available kernels", exception.Message); - } - else - { - // If we reached here without an exception, the test failed - Assert.Fail($"Test timed out after {duration.TotalSeconds:F3} seconds without throwing expected exception"); - } + var duration = DateTime.UtcNow - startTime; + OutputHelper.WriteLine($"Request failed after {duration.TotalSeconds:F2} seconds"); } finally { - OutputHelper.WriteLine("Test completed"); + foreach (var kernel in kernels) + { + kernel.Dispose(); + } } } diff --git a/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByServiceTypeTests.cs b/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByServiceTypeTests.cs index 3698f76..86e94d8 100644 --- a/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByServiceTypeTests.cs +++ b/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByServiceTypeTests.cs @@ -1,4 +1,5 @@ -using Xunit.Abstractions; +using Microsoft.Extensions.DependencyInjection; +using Xunit.Abstractions; namespace SemanticKernelPooling.Tests.Unit; @@ -8,6 +9,9 @@ public class KernelPoolManagerGetByServiceTypeTests private IServiceProvider _serviceProvider => _fixture.ServiceProvider; private readonly SemanticKernelPoolTestFixture _fixture; private ITestOutputHelper OutputHelper => _fixture.TestOutputHelper; + private int PoolSize => _fixture.PoolSize; + private int TotalPoolSize => PoolSize * 2; // Total across both configs + private const AIServiceProviderType TestProviderType = AIServiceProviderType.OpenAI; public KernelPoolManagerGetByServiceTypeTests(SemanticKernelPoolTestFixture fixture, ITestOutputHelper outputHelper) { @@ -16,4 +20,232 @@ public KernelPoolManagerGetByServiceTypeTests(SemanticKernelPoolTestFixture fixt _fixture.InitializeServices(); OutputHelper.WriteLine("Initializing KernelPoolManagerGetByServiceTypeTests"); } + + [Fact] + public async Task GetKernelByType_WithValidType_ReturnsValidKernel() + { + // Arrange + OutputHelper.WriteLine($"Starting test: GetKernelByType with service type '{TestProviderType}'"); + var kernelPoolManager = _serviceProvider.GetRequiredService(); + + // Act + using var wrapper = await kernelPoolManager.GetKernelAsync(TestProviderType); + + // Log the details + OutputHelper.WriteLine($"Retrieved kernel details:"); + OutputHelper.WriteLine($"- Service Type: {wrapper.ServiceProviderType}"); + OutputHelper.WriteLine($"- Kernel Instance: {wrapper.Kernel.GetHashCode()}"); + + // Assert + Assert.NotNull(wrapper); + Assert.NotNull(wrapper.Kernel); + Assert.Equal(TestProviderType, wrapper.ServiceProviderType); + + OutputHelper.WriteLine("Successfully verified kernel properties"); + } + + [Fact(Skip = "need to fix GetByType round robin")] + public async Task GetKernelByType_WhenRequestingTwo_AlternatesBetweenPools() + { + // Arrange + OutputHelper.WriteLine("Starting test: GetKernelByType alternates between pools"); + var kernelPoolManager = _serviceProvider.GetRequiredService(); + + // Act + using var wrapper1 = await kernelPoolManager.GetKernelAsync(TestProviderType); + OutputHelper.WriteLine($"First kernel obtained:"); + OutputHelper.WriteLine($"- Pool Unique Name: {wrapper1.UniqueName}"); + OutputHelper.WriteLine($"- Instance Hash: {wrapper1.Kernel.GetHashCode()}"); + + using var wrapper2 = await kernelPoolManager.GetKernelAsync(TestProviderType); + OutputHelper.WriteLine($"Second kernel obtained:"); + OutputHelper.WriteLine($"- Pool Unique Name: {wrapper2.UniqueName}"); + OutputHelper.WriteLine($"- Instance Hash: {wrapper2.Kernel.GetHashCode()}"); + + // Assert + Assert.NotNull(wrapper1.Kernel); + Assert.NotNull(wrapper2.Kernel); + Assert.NotEqual(wrapper1.Kernel.GetHashCode(), wrapper2.Kernel.GetHashCode()); + + // Verify they're from different pools by checking their scopes + Assert.NotEqual(wrapper1.UniqueName, wrapper2.UniqueName); + + OutputHelper.WriteLine($"Verified kernels came from different pools - Pool 1: {wrapper1.UniqueName}, Pool 2: {wrapper2.UniqueName}"); + } + + [Fact(Skip = "need to fix GetByType round robin")] + public async Task GetKernelByType_WithInvalidType_ThrowsInvalidOperationException() + { + OutputHelper.WriteLine("Starting test: GetKernelByType with invalid service type"); + var kernelPoolManager = _serviceProvider.GetRequiredService(); + const AIServiceProviderType invalidType = (AIServiceProviderType)999; + + var exception = await Assert.ThrowsAsync( + async () => await kernelPoolManager.GetKernelAsync(invalidType)); + + OutputHelper.WriteLine($"Exception details: {exception.Message}"); + Assert.Contains("No configuration found for kernel pool type", exception.Message); + } + + [Fact(Skip = "need to fix GetByType round robin")] + public async Task GetKernelByType_WhenAllPoolsExhausted_WaitsForTimeoutThenThrows() + { + OutputHelper.WriteLine("Starting test: GetKernelByType with all pools exhausted"); + var kernelPoolManager = _serviceProvider.GetRequiredService(); + var kernels = new List(); + + try + { + // Exhaust all pools + for (int i = 0; i < TotalPoolSize; i++) + { + var wrapper = await kernelPoolManager.GetKernelAsync(TestProviderType); + kernels.Add(wrapper); + OutputHelper.WriteLine($"Obtained kernel {i + 1}: Hash {wrapper.Kernel.GetHashCode()}"); + } + + var startTime = DateTime.UtcNow; + OutputHelper.WriteLine($"All pools exhausted, attempting to get another kernel at {startTime:HH:mm:ss.fff}"); + + await Assert.ThrowsAsync( + async () => await kernelPoolManager.GetKernelAsync(TestProviderType)); + + var duration = DateTime.UtcNow - startTime; + OutputHelper.WriteLine($"Request failed after {duration.TotalSeconds:F2} seconds"); + } + finally + { + foreach (var kernel in kernels) + { + kernel.Dispose(); + } + } + } + + [Fact(Skip = "need to fix GetByType round robin")] + public async Task GetKernelByType_WithMultipleRequests_DistributesLoad() + { + OutputHelper.WriteLine("Starting test: GetKernelByType load distribution"); + var kernelPoolManager = _serviceProvider.GetRequiredService(); + var poolUsage = new Dictionary(); + + for (int i = 0; i < 10; i++) + { + using var wrapper = await kernelPoolManager.GetKernelAsync(TestProviderType); + if (!poolUsage.ContainsKey(wrapper.UniqueName)) + { + poolUsage[wrapper.UniqueName] = 0; + } + poolUsage[wrapper.UniqueName]++; + + OutputHelper.WriteLine($"Request {i + 1} used pool: {wrapper.UniqueName}"); + } + + foreach (var usage in poolUsage) + { + OutputHelper.WriteLine($"Pool {usage.Key} was used {usage.Value} times"); + } + + Assert.True(poolUsage.Count > 1, "Load should be distributed across multiple pools"); + } + + [Fact(Skip = "need to fix GetByType round robin")] + public async Task GetKernelByType_WithConcurrentRequests_HandlesThemCorrectly() + { + OutputHelper.WriteLine($"Starting test: GetKernelByType with concurrent requests exceeding pool size"); + var kernelPoolManager = _serviceProvider.GetRequiredService(); + var tasks = new List>(); + const int extraRequests = 3; // Number of requests exceeding pool size + var concurrentRequests = TotalPoolSize + extraRequests; + var successfulRequests = new List(); + var failedRequests = 0; + + OutputHelper.WriteLine($"Total pool size: {TotalPoolSize}"); + OutputHelper.WriteLine($"Extra requests: {extraRequests}"); + OutputHelper.WriteLine($"Total concurrent requests: {concurrentRequests}"); + + try + { + // Create multiple concurrent requests + for (int i = 0; i < concurrentRequests; i++) + { + var requestId = i; // Capture for logging + var task = Task.Run(async () => + { + try + { + OutputHelper.WriteLine($"Starting request {requestId + 1}/{concurrentRequests}"); + return await kernelPoolManager.GetKernelAsync(TestProviderType); + } + catch (InvalidOperationException ex) + { + OutputHelper.WriteLine($"Request {requestId + 1} failed as expected: {ex.Message}"); + Interlocked.Increment(ref failedRequests); + throw; + } + }); + tasks.Add(task); + } + + // Wait for all tasks and collect results + var results = await Task.WhenAll( + tasks.Select(async t => + { + try + { + return await t; + } + catch + { + return null; + } + }) + ); + + // Count successful requests + successfulRequests.AddRange(results.Where(r => r != null)!); + var uniqueKernels = successfulRequests.Select(w => w.Kernel.GetHashCode()).Distinct().Count(); + + OutputHelper.WriteLine($"\nRequest Summary:"); + OutputHelper.WriteLine($"- Total requests: {concurrentRequests}"); + OutputHelper.WriteLine($"- Successful requests: {successfulRequests.Count}"); + OutputHelper.WriteLine($"- Failed requests: {failedRequests}"); + OutputHelper.WriteLine($"- Unique kernels obtained: {uniqueKernels}"); + + // Verify results + Assert.Equal(TotalPoolSize, successfulRequests.Count); + Assert.Equal(extraRequests, failedRequests); + Assert.True(uniqueKernels <= TotalPoolSize, + $"Got {uniqueKernels} unique kernels, expected maximum of {TotalPoolSize}"); + } + finally + { + // Dispose successful kernels + foreach (var kernel in successfulRequests) + { + kernel.Dispose(); + } + OutputHelper.WriteLine("Cleaned up all successful kernel requests"); + } + } + + [Fact(Skip = "need to fix GetByType round robin")] + public async Task GetKernelByType_WhenDisposedAndReacquired_ReusesKernels() + { + OutputHelper.WriteLine("Starting test: GetKernelByType kernel reuse"); + var kernelPoolManager = _serviceProvider.GetRequiredService(); + var seenKernels = new HashSet(); + const int configCount = 2; // Number of OpenAI configurations + + for (int i = 0; i < TotalPoolSize; i++) + { + using var wrapper = await kernelPoolManager.GetKernelAsync(TestProviderType); + var kernelHash = wrapper.Kernel.GetHashCode(); + OutputHelper.WriteLine($"Request {i + 1} got kernel with hash: {kernelHash} from {wrapper.UniqueName}"); + seenKernels.Add(kernelHash); + } + + OutputHelper.WriteLine($"Total unique kernels created: {seenKernels.Count}"); + Assert.Equal(TotalPoolSize / configCount, seenKernels.Count); + } } \ No newline at end of file diff --git a/SemanticKernelPooling.Tests.Unit/SemanticKernelPoolTestFixture.cs b/SemanticKernelPooling.Tests.Unit/SemanticKernelPoolTestFixture.cs index e7c7e86..2143791 100644 --- a/SemanticKernelPooling.Tests.Unit/SemanticKernelPoolTestFixture.cs +++ b/SemanticKernelPooling.Tests.Unit/SemanticKernelPoolTestFixture.cs @@ -41,7 +41,8 @@ public ITestOutputHelper TestOutputHelper /// /// The scope used for test configurations. /// - public string TestScope = "test-scope"; + public string TestScope1 = "test_scope_1"; + public string TestScope2 = "test_scope_2"; /// /// The size of the kernel pool for testing. diff --git a/SemanticKernelPooling.Tests.Unit/appsettings.json b/SemanticKernelPooling.Tests.Unit/appsettings.json index 90fb36c..2d76854 100644 --- a/SemanticKernelPooling.Tests.Unit/appsettings.json +++ b/SemanticKernelPooling.Tests.Unit/appsettings.json @@ -11,7 +11,21 @@ "ServiceId": "mock-service-id", "Endpoint": "https://mock.endpoint", "Scopes": [ - "test-scope" + "test_scope_1" + ] + }, + { + "UniqueName": "MockOpenAI2", + "ServiceType": "OpenAI", + "InstanceCount": 2, + "DeploymentTextEmbedding": "mock-embedding", + "MaxWaitForKernelInSeconds": 1, + "ApiKey": "mock-api-key", + "ModelId": "mock-model", + "ServiceId": "mock-service-id", + "Endpoint": "https://mock.endpoint", + "Scopes": [ + "test_scope_2" ] }, { @@ -26,7 +40,22 @@ "Endpoint": "https://mock.openai.azure.com", "DeploymentName": "DeploymentName", "Scopes": [ - "test-scope" + "test_scope_1" + ] + }, + { + "UniqueName": "MockAzureOpenAI2", + "ServiceType": "AzureOpenAI", + "InstanceCount": 2, + "DeploymentTextEmbedding": "mock-embedding", + "MaxWaitForKernelInSeconds": 1, + "ApiKey": "mock-api-key", + "ModelId": "mock-model", + "ServiceId": "mock-service-id", + "Endpoint": "https://mock.openai.azure.com", + "DeploymentName": "DeploymentName", + "Scopes": [ + "test_scope_2" ] } ] diff --git a/SemanticKernelPooling/KernelWrapper.cs b/SemanticKernelPooling/KernelWrapper.cs index 8a5f688..e32403d 100644 --- a/SemanticKernelPooling/KernelWrapper.cs +++ b/SemanticKernelPooling/KernelWrapper.cs @@ -51,4 +51,9 @@ public void Dispose() /// // ReSharper disable once UnusedMember.Global public AIServiceProviderType ServiceProviderType { get; } = pool.ServiceProviderType; + + /// + /// The Unique Name of the kernel pool of this wrapper. + /// + public string UniqueName { get; } = pool.UniqueName; } \ No newline at end of file From 0b1ce997df2cbbf4e088d7c94ff608a8304b9ee4 Mon Sep 17 00:00:00 2001 From: Rafael Azriaiev Date: Thu, 2 Jan 2025 16:20:58 +0200 Subject: [PATCH 09/12] Added tests to GetByScope --- .../KernelPoolManagerGetByScopeTests.cs | 234 +++++++++++++++++- 1 file changed, 233 insertions(+), 1 deletion(-) diff --git a/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByScopeTests.cs b/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByScopeTests.cs index 703174c..2d0b2c7 100644 --- a/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByScopeTests.cs +++ b/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByScopeTests.cs @@ -1,4 +1,5 @@ -using Xunit.Abstractions; +using Microsoft.Extensions.DependencyInjection; +using Xunit.Abstractions; namespace SemanticKernelPooling.Tests.Unit; @@ -8,6 +9,10 @@ public class KernelPoolManagerGetByScopeTests private IServiceProvider _serviceProvider => _fixture.ServiceProvider; private readonly SemanticKernelPoolTestFixture _fixture; private ITestOutputHelper OutputHelper => _fixture.TestOutputHelper; + private string TestScope1 => _fixture.TestScope1; + private string TestScope2 => _fixture.TestScope2; + private int PoolSize => _fixture.PoolSize; + private int TotalPoolSize => PoolSize * 2; public KernelPoolManagerGetByScopeTests(SemanticKernelPoolTestFixture fixture, ITestOutputHelper outputHelper) { @@ -16,4 +21,231 @@ public KernelPoolManagerGetByScopeTests(SemanticKernelPoolTestFixture fixture, I _fixture.InitializeServices(); OutputHelper.WriteLine("Initializing KernelPoolManagerGetByScopeTests"); } + + [Fact] + public async Task GetKernelByScope_WithValidScope_ReturnsValidKernel() + { + OutputHelper.WriteLine($"Starting test: GetKernelByScope with scope '{TestScope1}'"); + var kernelPoolManager = _serviceProvider.GetRequiredService(); + + using var wrapper = await kernelPoolManager.GetKernelByScopeAsync(TestScope1); + + OutputHelper.WriteLine($"Retrieved kernel details:"); + OutputHelper.WriteLine($"- Scope: {string.Join(", ", wrapper.Scopes)}"); + OutputHelper.WriteLine($"- Kernel Instance: {wrapper.Kernel.GetHashCode()}"); + + Assert.NotNull(wrapper); + Assert.NotNull(wrapper.Kernel); + Assert.Contains(TestScope1, wrapper.Scopes); + + OutputHelper.WriteLine("Successfully verified kernel properties"); + } + + [Fact] + public async Task GetKernelByScope_WithInvalidScope_ThrowsInvalidOperationException() + { + OutputHelper.WriteLine("Starting test: GetKernelByScope with invalid scope"); + var kernelPoolManager = _serviceProvider.GetRequiredService(); + const string invalidScope = "non-existent-scope"; + + var exception = await Assert.ThrowsAsync( + async () => await kernelPoolManager.GetKernelByScopeAsync(invalidScope)); + + OutputHelper.WriteLine($"Exception details: {exception.Message}"); + Assert.Contains($"No configuration found for scope {invalidScope}", exception.Message); + } + + [Fact] + public async Task GetKernelByScope_WhenRequestingTwo_AlternateBetweenPools() + { + // Arrange + OutputHelper.WriteLine("Starting test: GetKernelByScope alternates between pools"); + var kernelPoolManager = _serviceProvider.GetRequiredService(); + + // Act + using var wrapper1 = await kernelPoolManager.GetKernelByScopeAsync(TestScope1); + OutputHelper.WriteLine($"First kernel obtained:"); + OutputHelper.WriteLine($"- Pool Unique Name: {wrapper1.UniqueName}"); + OutputHelper.WriteLine($"- Instance Hash: {wrapper1.Kernel.GetHashCode()}"); + + using var wrapper2 = await kernelPoolManager.GetKernelByScopeAsync(TestScope1); + OutputHelper.WriteLine($"Second kernel obtained:"); + OutputHelper.WriteLine($"- Pool Unique Name: {wrapper2.UniqueName}"); + OutputHelper.WriteLine($"- Instance Hash: {wrapper2.Kernel.GetHashCode()}"); + + // Assert + Assert.NotNull(wrapper1.Kernel); + Assert.NotNull(wrapper2.Kernel); + Assert.NotEqual(wrapper1.Kernel.GetHashCode(), wrapper2.Kernel.GetHashCode()); + + Assert.Contains(TestScope1, wrapper1.Scopes); + Assert.Contains(TestScope1, wrapper2.Scopes); + + // Verify they're from different pools by checking their scopes + Assert.NotEqual(wrapper1.UniqueName, wrapper2.UniqueName); + + OutputHelper.WriteLine($"Verified kernels came from different pools - Pool 1: {wrapper1.UniqueName}, Pool 2: {wrapper2.UniqueName}"); + } + + [Fact] + public async Task GetKernelByScope_WhenAllPoolsExhausted_WaitsForTimeoutThenThrows() + { + OutputHelper.WriteLine("Starting test: GetKernelByScope with all pools exhausted"); + var kernelPoolManager = _serviceProvider.GetRequiredService(); + var kernels = new List(); + + try + { + // Exhaust all pools + for (int i = 0; i < TotalPoolSize; i++) + { + var wrapper = await kernelPoolManager.GetKernelByScopeAsync(TestScope1); + kernels.Add(wrapper); + OutputHelper.WriteLine($"Obtained kernel {i + 1}: Hash {wrapper.Kernel.GetHashCode()}"); + } + + var startTime = DateTime.UtcNow; + OutputHelper.WriteLine($"All pools exhausted, attempting to get another kernel at {startTime:HH:mm:ss.fff}"); + + await Assert.ThrowsAsync( + async () => await kernelPoolManager.GetKernelByScopeAsync(TestScope1)); + + var duration = DateTime.UtcNow - startTime; + OutputHelper.WriteLine($"Request failed after {duration.TotalSeconds:F2} seconds"); + } + finally + { + foreach (var kernel in kernels) + { + kernel.Dispose(); + } + } + } + + [Fact] + public async Task GetKernelByScope_WithMultipleRequests_DistributesLoad() + { + OutputHelper.WriteLine("Starting test: GetKernelByScope load distribution"); + var kernelPoolManager = _serviceProvider.GetRequiredService(); + var poolUsage = new Dictionary(); + + for (int i = 0; i < 10; i++) + { + using var wrapper = await kernelPoolManager.GetKernelByScopeAsync(TestScope1); + if (!poolUsage.ContainsKey(wrapper.UniqueName)) + { + poolUsage[wrapper.UniqueName] = 0; + } + poolUsage[wrapper.UniqueName]++; + + OutputHelper.WriteLine($"Request {i + 1} used pool: {wrapper.UniqueName}"); + } + + foreach (var usage in poolUsage) + { + OutputHelper.WriteLine($"Pool {usage.Key} was used {usage.Value} times"); + } + + Assert.True(poolUsage.Count > 1, "Load should be distributed across multiple pools"); + } + + [Fact] + public async Task GetKernelByScope_WithConcurrentRequests_HandlesThemCorrectly() + { + OutputHelper.WriteLine($"Starting test: GetKernelByScope with concurrent requests exceeding pool size"); + var kernelPoolManager = _serviceProvider.GetRequiredService(); + var tasks = new List>(); + const int extraRequests = 3; // Number of requests exceeding pool size + var concurrentRequests = TotalPoolSize + extraRequests; + var successfulRequests = new List(); + var failedRequests = 0; + + OutputHelper.WriteLine($"Total pool size: {TotalPoolSize}"); + OutputHelper.WriteLine($"Extra requests: {extraRequests}"); + OutputHelper.WriteLine($"Total concurrent requests: {concurrentRequests}"); + + try + { + // Create multiple concurrent requests + for (int i = 0; i < concurrentRequests; i++) + { + var requestId = i; // Capture for logging + var task = Task.Run(async () => + { + try + { + OutputHelper.WriteLine($"Starting request {requestId + 1}/{concurrentRequests}"); + return await kernelPoolManager.GetKernelByScopeAsync(TestScope1); + } + catch (InvalidOperationException ex) + { + OutputHelper.WriteLine($"Request {requestId + 1} failed as expected: {ex.Message}"); + Interlocked.Increment(ref failedRequests); + throw; + } + }); + tasks.Add(task); + } + + // Wait for all tasks and collect results + var results = await Task.WhenAll( + tasks.Select(async t => + { + try + { + return await t; + } + catch + { + return null; + } + }) + ); + + // Count successful requests + successfulRequests.AddRange(results.Where(r => r != null)!); + var uniqueKernels = successfulRequests.Select(w => w.Kernel.GetHashCode()).Distinct().Count(); + + OutputHelper.WriteLine($"\nRequest Summary:"); + OutputHelper.WriteLine($"- Total requests: {concurrentRequests}"); + OutputHelper.WriteLine($"- Successful requests: {successfulRequests.Count}"); + OutputHelper.WriteLine($"- Failed requests: {failedRequests}"); + OutputHelper.WriteLine($"- Unique kernels obtained: {uniqueKernels}"); + + // Verify results + Assert.Equal(TotalPoolSize, successfulRequests.Count); + Assert.Equal(extraRequests, failedRequests); + Assert.True(uniqueKernels <= TotalPoolSize, + $"Got {uniqueKernels} unique kernels, expected maximum of {TotalPoolSize}"); + } + finally + { + // Dispose successful kernels + foreach (var kernel in successfulRequests) + { + kernel.Dispose(); + } + OutputHelper.WriteLine("Cleaned up all successful kernel requests"); + } + } + + [Fact] + public async Task GetKernelByScope_WhenDisposedAndReacquired_ReusesKernels() + { + OutputHelper.WriteLine("Starting test: GetKernelByScope kernel reuse"); + var kernelPoolManager = _serviceProvider.GetRequiredService(); + var seenKernels = new HashSet(); + const int configCount = 2; // Number of OpenAI configurations + + for (int i = 0; i < TotalPoolSize; i++) + { + using var wrapper = await kernelPoolManager.GetKernelByScopeAsync(TestScope1); + var kernelHash = wrapper.Kernel.GetHashCode(); + OutputHelper.WriteLine($"Request {i + 1} got kernel with hash: {kernelHash} from {wrapper.UniqueName}"); + seenKernels.Add(kernelHash); + } + + OutputHelper.WriteLine($"Total unique kernels created: {seenKernels.Count}"); + Assert.Equal(TotalPoolSize / configCount, seenKernels.Count); + } } \ No newline at end of file From 41df4078bcdd5a6b12c4e47fc62e08021c9c3989 Mon Sep 17 00:00:00 2001 From: Rafael Azriaiev Date: Sun, 5 Jan 2025 09:34:39 +0200 Subject: [PATCH 10/12] fix scopes not passed for pre and after register --- SemanticKernelPooling/AIServicePool.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/SemanticKernelPooling/AIServicePool.cs b/SemanticKernelPooling/AIServicePool.cs index 3f8af73..2aff9bb 100644 --- a/SemanticKernelPooling/AIServicePool.cs +++ b/SemanticKernelPooling/AIServicePool.cs @@ -61,7 +61,13 @@ private Kernel CreateKernel() { var kernelBuilder = Kernel.CreateBuilder(); - TServiceProviderConfiguration newConfig = AIServiceProviderAIConfiguration with { UniqueName = $"{AIServiceProviderAIConfiguration.UniqueName}{CurrentNumberOfKernels}" }; + TServiceProviderConfiguration newConfig = AIServiceProviderAIConfiguration with { + UniqueName = $"{AIServiceProviderAIConfiguration.UniqueName}{CurrentNumberOfKernels}", + Scopes = AIServiceProviderAIConfiguration.Scopes, + InstanceCount = AIServiceProviderAIConfiguration.InstanceCount, + MaxWaitForKernelInSeconds = AIServiceProviderAIConfiguration.MaxWaitForKernelInSeconds, + DeploymentTextEmbedding = AIServiceProviderAIConfiguration.DeploymentTextEmbedding, + }; bool shouldAutoAddChatCompletionService = true; KernelBuilderOptions options = new(); From 28dca69c2ef5b789056767bca21e171911d021c8 Mon Sep 17 00:00:00 2001 From: Rafael Azriaiev Date: Sun, 5 Jan 2025 10:42:21 +0200 Subject: [PATCH 11/12] Added tests for initializations pre and post registration --- .../KernelPoolManagerInitializationTests.cs | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 SemanticKernelPooling.Tests.Unit/KernelPoolManagerInitializationTests.cs diff --git a/SemanticKernelPooling.Tests.Unit/KernelPoolManagerInitializationTests.cs b/SemanticKernelPooling.Tests.Unit/KernelPoolManagerInitializationTests.cs new file mode 100644 index 0000000..666bf69 --- /dev/null +++ b/SemanticKernelPooling.Tests.Unit/KernelPoolManagerInitializationTests.cs @@ -0,0 +1,172 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; +using SemanticKernelPooling.Tests.Mocks; +using Xunit.Abstractions; + +namespace SemanticKernelPooling.Tests.Unit; + +[Collection("KernelPoolManagerTests")] +public class KernelPoolManagerInitializationTests +{ + private IServiceProvider _serviceProvider => _fixture.ServiceProvider; + private readonly SemanticKernelPoolTestFixture _fixture; + private ITestOutputHelper OutputHelper => _fixture.TestOutputHelper; + private const string ValidUniqueName = "MockOpenAI"; + private string TestScope1 => _fixture.TestScope1; + private string TestScope2 => _fixture.TestScope2; + + public KernelPoolManagerInitializationTests(SemanticKernelPoolTestFixture fixture, ITestOutputHelper outputHelper) + { + _fixture = fixture; + _fixture.ConfigureLogging(outputHelper); + _fixture.InitializeServices(); + OutputHelper.WriteLine("Initializing KernelPoolManagerInitializationTests"); + } + + [Fact] + public async Task RegisterForPreKernelCreation_WithScope_ExecutesForMatchingScope() + { + // Arrange + OutputHelper.WriteLine($"Starting test: RegisterForPreKernelCreation with scope '{TestScope1}'"); + var kernelPoolManager = _serviceProvider.GetRequiredService(); + var executionCount = 0; + + // Register scoped pre-initialization action + kernelPoolManager.RegisterForPreKernelCreation(TestScope1, + (builder, config, options, scopes) => + { + OutputHelper.WriteLine("Executing preRegisteration"); + Assert.NotNull(scopes); // Verify scopes is not null + Assert.Contains(TestScope1, scopes); + Interlocked.Increment(ref executionCount); + OutputHelper.WriteLine($"Pre-initialization action executed for scopes: {string.Join(", ", scopes)}"); + }); + + // Act + using var wrapper1 = await kernelPoolManager.GetKernelByScopeAsync(TestScope1); + using var wrapper2 = await kernelPoolManager.GetKernelByScopeAsync(TestScope1); + + using var wrapperDifferentScope = await kernelPoolManager.GetKernelByScopeAsync(TestScope2); + + OutputHelper.WriteLine("wrapper 1 unique name: " + wrapper1.UniqueName); + OutputHelper.WriteLine("wrapper 2 unique name: " + wrapper2.UniqueName); + OutputHelper.WriteLine("wrapper Different Scope unique name: " + wrapperDifferentScope.UniqueName); + + // Assert + OutputHelper.WriteLine($"Execution count: {executionCount}"); + Assert.Equal(2, executionCount); + Assert.DoesNotContain(TestScope1, wrapperDifferentScope.Scopes); + } + + [Fact] + public async Task RegisterForAfterKernelCreation_WithScope_ExecutesForMatchingScope() + { + // Arrange + OutputHelper.WriteLine($"Starting test: RegisterForAfterKernelCreation with scope '{TestScope1}'"); + var kernelPoolManager = _serviceProvider.GetRequiredService(); + var executionCount = 0; + Kernel? lastConfiguredKernel = null; + + // Register scoped post-initialization action + kernelPoolManager.RegisterForAfterKernelCreation(TestScope1, + (kernel, config, scopes) => + { + OutputHelper.WriteLine("Executing AfterRegisteration"); + Assert.NotNull(scopes); // Verify scopes is not null + Assert.Contains(TestScope1, scopes); + Interlocked.Increment(ref executionCount); + lastConfiguredKernel = kernel; + OutputHelper.WriteLine($"Post-initialization action executed for scopes: {string.Join(", ", scopes)}"); + }); + + // Act + using var wrapper1 = await kernelPoolManager.GetKernelByScopeAsync(TestScope1); + using var wrapper2 = await kernelPoolManager.GetKernelByScopeAsync(TestScope1); + + using var wrapperDifferentScope= await kernelPoolManager.GetKernelByScopeAsync(TestScope2); + + OutputHelper.WriteLine("wrapper 1 unique name: " + wrapper1.UniqueName); + OutputHelper.WriteLine("wrapper 2 unique name: " + wrapper2.UniqueName); + OutputHelper.WriteLine("wrapper Different Scope unique name: " + wrapperDifferentScope.UniqueName); + + // Assert + OutputHelper.WriteLine($"Execution count: {executionCount}"); + Assert.Equal(2, executionCount); + Assert.Same(lastConfiguredKernel, wrapper2.Kernel); + Assert.DoesNotContain(TestScope1, wrapperDifferentScope.Scopes); + } + + [Fact] + public async Task RegisterForPreKernelCreation_WithType_ExecutesForMatchingType() + { + // Arrange + OutputHelper.WriteLine("Starting test: RegisterForPreKernelCreation with type MockAIConfiguration"); + var kernelPoolManager = _serviceProvider.GetRequiredService(); + var registeredType = AIServiceProviderType.OpenAI; + var unregisteredType = AIServiceProviderType.AzureOpenAI; + var executionCount = 0; + + // Register type-specific pre-initialization action + kernelPoolManager.RegisterForPreKernelCreation( + (builder, config, options) => + { + OutputHelper.WriteLine("Executing preRegistration for MockAIConfiguration"); + Assert.IsType(config); + Interlocked.Increment(ref executionCount); + OutputHelper.WriteLine($"Pre-initialization action executed for config: {config.UniqueName}"); + }); + + // Act + using var wrapper1 = await kernelPoolManager.GetKernelAsync(registeredType); + using var wrapper2 = await kernelPoolManager.GetKernelAsync(registeredType); + + using var wrapperDifferentType = await kernelPoolManager.GetKernelAsync(unregisteredType); + + OutputHelper.WriteLine("wrapper 1 type: {Type}, unique name: {Name}", registeredType, wrapper1.UniqueName); + OutputHelper.WriteLine("wrapper 2 type: {Type}, unique name: {Name}", registeredType, wrapper2.UniqueName); + OutputHelper.WriteLine("wrapperDifferentType {Type}, unique name: {Name}", unregisteredType, wrapperDifferentType.UniqueName); + + // Assert + OutputHelper.WriteLine($"Execution count: {executionCount}"); + Assert.Equal(2, executionCount); + } + + [Fact] + public async Task RegisterForAfterKernelCreation_WithType_ExecutesForMatchingType() + { + // Arrange + OutputHelper.WriteLine("Starting test: RegisterForAfterKernelCreation with type MockAIConfiguration"); + var kernelPoolManager = _serviceProvider.GetRequiredService(); + var registeredType = AIServiceProviderType.OpenAI; + var unregisteredType = AIServiceProviderType.AzureOpenAI; + var executionCount = 0; + Kernel? lastConfiguredKernel = null; + + // Register type-specific post-initialization action + kernelPoolManager.RegisterForAfterKernelCreation( + (kernel, config) => + { + OutputHelper.WriteLine("Executing afterRegistration for MockAIConfiguration"); + Assert.IsType(config); + Interlocked.Increment(ref executionCount); + lastConfiguredKernel = kernel; + OutputHelper.WriteLine($"Post-initialization action executed for config: {config.UniqueName}"); + }); + + // Act + using var wrapper1 = await kernelPoolManager.GetKernelAsync(registeredType); + using var wrapper2 = await kernelPoolManager.GetKernelAsync(registeredType); + + using var wrapperDifferentType = await kernelPoolManager.GetKernelAsync(unregisteredType); + + OutputHelper.WriteLine("wrapper 1 type: {Type}, unique name: {Name}", registeredType, wrapper1.UniqueName); + OutputHelper.WriteLine("wrapper 2 type: {Type}, unique name: {Name}", registeredType, wrapper2.UniqueName); + OutputHelper.WriteLine("wrapperDifferentType {Type}, unique name: {Name}" , unregisteredType, wrapperDifferentType.UniqueName); + + // Assert + OutputHelper.WriteLine($"Execution count: {executionCount}"); + Assert.Equal(2, executionCount); + Assert.Same(lastConfiguredKernel, wrapper2.Kernel); + } +} \ No newline at end of file From 0bfd3181574d0be84c17b70cd734a4dc88115f07 Mon Sep 17 00:00:00 2001 From: Rafael Azriaiev Date: Sun, 5 Jan 2025 13:52:58 +0200 Subject: [PATCH 12/12] removed mock http handler, removed unnecessery method and property --- .../MockHttpMessageHandler.cs | 104 ------------------ .../MockKernelPool.cs | 20 +--- .../MockResponse.cs | 32 ------ .../ServiceExtension.cs | 5 +- .../KernelPoolManagerGetByScopeTests.cs | 1 - .../KernelPoolManagerInitializationTests.cs | 34 ++---- .../SemanticKernelPoolTestFixture.cs | 26 ----- 7 files changed, 15 insertions(+), 207 deletions(-) delete mode 100644 SemanticKernelPooling.Tests.Mocks/MockHttpMessageHandler.cs delete mode 100644 SemanticKernelPooling.Tests.Mocks/MockResponse.cs diff --git a/SemanticKernelPooling.Tests.Mocks/MockHttpMessageHandler.cs b/SemanticKernelPooling.Tests.Mocks/MockHttpMessageHandler.cs deleted file mode 100644 index 15ab169..0000000 --- a/SemanticKernelPooling.Tests.Mocks/MockHttpMessageHandler.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System.Net; -using System.Text.Json; -using Microsoft.Extensions.Logging; - -namespace SemanticKernelPooling.Tests.Mocks; - -/// -/// Provides a mock HTTP message handler that returns predefined responses for testing purposes. -/// This handler enables testing of HTTP-dependent code without making actual network requests. -/// -/// -/// The handler loads mock responses from JSON files in a specified directory. -/// Each response file should contain a request key, status code, content, and optional headers. -/// -public class MockHttpMessageHandler : HttpMessageHandler -{ - private readonly Dictionary _responses; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - /// The directory path containing mock response JSON files. - /// The logger for recording handler activities. - public MockHttpMessageHandler(string responsesPath, ILogger logger) - { - _logger = logger; - _responses = LoadMockResponses(responsesPath); - } - - /// - /// Processes HTTP requests and returns mock responses based on the request key. - /// - /// The HTTP request message to process. - /// A token for canceling the request. - /// A task representing the HTTP response message. - protected override Task SendAsync( - HttpRequestMessage request, - CancellationToken cancellationToken) - { - var requestKey = CreateRequestKey(request); - _logger.LogInformation("Mock HTTP handler received request:"); - _logger.LogInformation(" Method: {Method}", request.Method); - _logger.LogInformation(" URI: {URI}", request.RequestUri); - _logger.LogInformation(" Generated Key: {RequestKey}", requestKey); - _logger.LogInformation("Available mock response keys:"); - foreach (var key in _responses.Keys) - { - _logger.LogInformation(" {Key}", key); - } - - if (_responses.TryGetValue(requestKey, out var mockResponse)) - { - _logger.LogInformation("Found matching mock response"); - var response = new HttpResponseMessage - { - StatusCode = mockResponse.StatusCode, - Content = new StringContent(mockResponse.Content, System.Text.Encoding.UTF8, "application/json") - }; - - foreach (var header in mockResponse.Headers) - { - response.Headers.Add(header.Key, header.Value); - } - - return Task.FromResult(response); - } - - _logger.LogWarning("No mock response found for request"); - return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)); - } - - private string CreateRequestKey(HttpRequestMessage request) - { - return $"{request.Method}:{request.RequestUri?.PathAndQuery}"; - } - - private Dictionary LoadMockResponses(string path) - { - var responses = new Dictionary(); - - if (Directory.Exists(path)) - { - foreach (var file in Directory.GetFiles(path, "*.json")) - { - try - { - var content = File.ReadAllText(file); - var mockResponse = JsonSerializer.Deserialize(content); - if (mockResponse?.RequestKey != null) - { - responses[mockResponse.RequestKey] = mockResponse; - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error loading mock response from file: {FilePath}", file); - } - } - } - - return responses; - } -} \ No newline at end of file diff --git a/SemanticKernelPooling.Tests.Mocks/MockKernelPool.cs b/SemanticKernelPooling.Tests.Mocks/MockKernelPool.cs index 6da2fd7..86ef5fe 100644 --- a/SemanticKernelPooling.Tests.Mocks/MockKernelPool.cs +++ b/SemanticKernelPooling.Tests.Mocks/MockKernelPool.cs @@ -23,7 +23,6 @@ namespace SemanticKernelPooling.Tests.Mocks; public class MockKernelPool : AIServicePool { private readonly ILoggerFactory _loggerFactory; - private readonly string _responsesPath; private int _kernelCreationCount; /// @@ -34,12 +33,10 @@ public class MockKernelPool : AIServicePool /// The path to mock response files. Defaults to "MockResponses". public MockKernelPool( MockAIConfiguration config, - ILoggerFactory loggerFactory, - string responsesPath = "MockResponses") + ILoggerFactory loggerFactory) : base(config) { _loggerFactory = loggerFactory; - _responsesPath = responsesPath; } /// @@ -54,21 +51,6 @@ protected override void RegisterChatCompletionService( HttpClient? httpClient) { Interlocked.Increment(ref _kernelCreationCount); - - if (httpClient == null) - { - var handler = new MockHttpMessageHandler( - _responsesPath, - _loggerFactory.CreateLogger()); - httpClient = new HttpClient(handler); - } - - kernelBuilder.AddAzureOpenAIChatCompletion( - deploymentName: "mock-deployment", - endpoint: "https://mock.openai.azure.com", - serviceId: config.ServiceId, - apiKey: config.ApiKey, - httpClient: httpClient); } /// diff --git a/SemanticKernelPooling.Tests.Mocks/MockResponse.cs b/SemanticKernelPooling.Tests.Mocks/MockResponse.cs deleted file mode 100644 index 01ecea2..0000000 --- a/SemanticKernelPooling.Tests.Mocks/MockResponse.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Net; - -namespace SemanticKernelPooling.Tests.Mocks; - -/// -/// Represents a mock HTTP response configuration that can be stored in a JSON file. -/// -public class MockResponse -{ - /// - /// Gets or sets the key used to match incoming requests to this response. - /// - /// - /// The key format is typically "{HTTP_METHOD}:{PATH}". For example, "POST:/v1/chat/completions". - /// - public string RequestKey { get; set; } = string.Empty; - - /// - /// Gets or sets the HTTP status code to return in the mock response. - /// - public HttpStatusCode StatusCode { get; set; } - - /// - /// Gets or sets the content body of the mock response. - /// - public string Content { get; set; } = string.Empty; - - /// - /// Gets or sets the HTTP headers to include in the mock response. - /// - public Dictionary Headers { get; set; } = new(); -} \ No newline at end of file diff --git a/SemanticKernelPooling.Tests.Mocks/ServiceExtension.cs b/SemanticKernelPooling.Tests.Mocks/ServiceExtension.cs index ea88d97..1090fba 100644 --- a/SemanticKernelPooling.Tests.Mocks/ServiceExtension.cs +++ b/SemanticKernelPooling.Tests.Mocks/ServiceExtension.cs @@ -17,7 +17,6 @@ public static class ServiceExtension /// Registers mock kernel pool implementations for testing purposes. /// /// The DI service provider. - /// Optional path to mock response files. Defaults to "MockResponses". /// The service provider to enable method chaining. /// /// @@ -29,7 +28,7 @@ public static class ServiceExtension /// /// services.AddSingleton(mockConfiguration.Object); /// services.UseSemanticKernelPooling(); - /// serviceProvider.UseMockKernelPool("TestResponses"); + /// serviceProvider.UseMockKernelPool(); /// /// /// @@ -41,7 +40,7 @@ public static IServiceProvider UseMockKernelPool( // Register factory for both mock providers var factory = (AIServiceProviderConfiguration config, ILoggerFactory loggerFactory) => - new MockKernelPool((MockAIConfiguration)config, loggerFactory, responsesPath); + new MockKernelPool((MockAIConfiguration)config, loggerFactory); registrar.RegisterKernelPoolFactory(AIServiceProviderType.OpenAI, factory); registrar.RegisterKernelPoolFactory(AIServiceProviderType.AzureOpenAI, factory); diff --git a/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByScopeTests.cs b/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByScopeTests.cs index 2d0b2c7..51d5b81 100644 --- a/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByScopeTests.cs +++ b/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByScopeTests.cs @@ -10,7 +10,6 @@ public class KernelPoolManagerGetByScopeTests private readonly SemanticKernelPoolTestFixture _fixture; private ITestOutputHelper OutputHelper => _fixture.TestOutputHelper; private string TestScope1 => _fixture.TestScope1; - private string TestScope2 => _fixture.TestScope2; private int PoolSize => _fixture.PoolSize; private int TotalPoolSize => PoolSize * 2; diff --git a/SemanticKernelPooling.Tests.Unit/KernelPoolManagerInitializationTests.cs b/SemanticKernelPooling.Tests.Unit/KernelPoolManagerInitializationTests.cs index 666bf69..9eb0fb7 100644 --- a/SemanticKernelPooling.Tests.Unit/KernelPoolManagerInitializationTests.cs +++ b/SemanticKernelPooling.Tests.Unit/KernelPoolManagerInitializationTests.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using SemanticKernelPooling.Tests.Mocks; using Xunit.Abstractions; @@ -12,7 +11,6 @@ public class KernelPoolManagerInitializationTests private IServiceProvider _serviceProvider => _fixture.ServiceProvider; private readonly SemanticKernelPoolTestFixture _fixture; private ITestOutputHelper OutputHelper => _fixture.TestOutputHelper; - private const string ValidUniqueName = "MockOpenAI"; private string TestScope1 => _fixture.TestScope1; private string TestScope2 => _fixture.TestScope2; @@ -97,14 +95,13 @@ public async Task RegisterForAfterKernelCreation_WithScope_ExecutesForMatchingSc Assert.DoesNotContain(TestScope1, wrapperDifferentScope.Scopes); } - [Fact] + [Fact(Skip ="fix register by configuration type")] public async Task RegisterForPreKernelCreation_WithType_ExecutesForMatchingType() { // Arrange OutputHelper.WriteLine("Starting test: RegisterForPreKernelCreation with type MockAIConfiguration"); var kernelPoolManager = _serviceProvider.GetRequiredService(); - var registeredType = AIServiceProviderType.OpenAI; - var unregisteredType = AIServiceProviderType.AzureOpenAI; + var providerType = AIServiceProviderType.OpenAI; var executionCount = 0; // Register type-specific pre-initialization action @@ -118,28 +115,24 @@ public async Task RegisterForPreKernelCreation_WithType_ExecutesForMatchingType( }); // Act - using var wrapper1 = await kernelPoolManager.GetKernelAsync(registeredType); - using var wrapper2 = await kernelPoolManager.GetKernelAsync(registeredType); - - using var wrapperDifferentType = await kernelPoolManager.GetKernelAsync(unregisteredType); + using var wrapper1 = await kernelPoolManager.GetKernelAsync(providerType); + using var wrapper2 = await kernelPoolManager.GetKernelAsync(providerType); - OutputHelper.WriteLine("wrapper 1 type: {Type}, unique name: {Name}", registeredType, wrapper1.UniqueName); - OutputHelper.WriteLine("wrapper 2 type: {Type}, unique name: {Name}", registeredType, wrapper2.UniqueName); - OutputHelper.WriteLine("wrapperDifferentType {Type}, unique name: {Name}", unregisteredType, wrapperDifferentType.UniqueName); + OutputHelper.WriteLine($"wrapper 1 type: {providerType}, unique name: {wrapper1.UniqueName}"); + OutputHelper.WriteLine($"wrapper 2 type: {providerType}, unique name: {wrapper2.UniqueName}"); // Assert OutputHelper.WriteLine($"Execution count: {executionCount}"); Assert.Equal(2, executionCount); } - [Fact] + [Fact(Skip = "fix register by configuration type")] public async Task RegisterForAfterKernelCreation_WithType_ExecutesForMatchingType() { // Arrange OutputHelper.WriteLine("Starting test: RegisterForAfterKernelCreation with type MockAIConfiguration"); var kernelPoolManager = _serviceProvider.GetRequiredService(); - var registeredType = AIServiceProviderType.OpenAI; - var unregisteredType = AIServiceProviderType.AzureOpenAI; + var providerType = AIServiceProviderType.OpenAI; var executionCount = 0; Kernel? lastConfiguredKernel = null; @@ -155,14 +148,11 @@ public async Task RegisterForAfterKernelCreation_WithType_ExecutesForMatchingTyp }); // Act - using var wrapper1 = await kernelPoolManager.GetKernelAsync(registeredType); - using var wrapper2 = await kernelPoolManager.GetKernelAsync(registeredType); - - using var wrapperDifferentType = await kernelPoolManager.GetKernelAsync(unregisteredType); + using var wrapper1 = await kernelPoolManager.GetKernelAsync(providerType); + using var wrapper2 = await kernelPoolManager.GetKernelAsync(providerType); - OutputHelper.WriteLine("wrapper 1 type: {Type}, unique name: {Name}", registeredType, wrapper1.UniqueName); - OutputHelper.WriteLine("wrapper 2 type: {Type}, unique name: {Name}", registeredType, wrapper2.UniqueName); - OutputHelper.WriteLine("wrapperDifferentType {Type}, unique name: {Name}" , unregisteredType, wrapperDifferentType.UniqueName); + OutputHelper.WriteLine($"wrapper 1 type: {providerType}, unique name: {wrapper1.UniqueName}"); + OutputHelper.WriteLine($"wrapper 2 type: {providerType}, unique name: {wrapper2.UniqueName}"); // Assert OutputHelper.WriteLine($"Execution count: {executionCount}"); diff --git a/SemanticKernelPooling.Tests.Unit/SemanticKernelPoolTestFixture.cs b/SemanticKernelPooling.Tests.Unit/SemanticKernelPoolTestFixture.cs index 2143791..65eed73 100644 --- a/SemanticKernelPooling.Tests.Unit/SemanticKernelPoolTestFixture.cs +++ b/SemanticKernelPooling.Tests.Unit/SemanticKernelPoolTestFixture.cs @@ -102,32 +102,6 @@ public void ConfigureLogging(ITestOutputHelper testOutputHelper) TestOutputHelper.WriteLine("Logging configured successfully"); } - /// - /// Adds a mock response for API calls during testing. - /// - /// The key identifying the request to mock. - /// The HTTP status code to return. - /// The content to return in the response. - public void AddMockResponse(string requestKey, HttpStatusCode statusCode, string content) - { - TestOutputHelper.WriteLine($"Adding mock response for request key: {requestKey}"); - - var response = new MockResponse - { - RequestKey = requestKey, - StatusCode = statusCode, - Content = content, - Headers = new Dictionary - { - { "Content-Type", new[] { "application/json" } } - } - }; - - var filePath = Path.Combine(_mockResponsesPath, $"{Guid.NewGuid()}.json"); - File.WriteAllText(filePath, JsonSerializer.Serialize(response)); - TestOutputHelper.WriteLine($"Mock response saved to: {filePath}"); - } - /// /// Performs cleanup of managed and unmanaged resources. ///