From c226abfc0adb1c3b2c4edd50d6f1dcfb2e4022c8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 17:27:20 +0000 Subject: [PATCH 1/2] test: add unit test coverage for SwaggerProvider.Caching.createInMemoryCache The in-memory ICache implementation in Caching.fs (used by Provider.OpenApiClient.fs to cache generated provided types) had zero direct unit tests. Added 11 tests covering Set/TryRetrieve/Remove/GetOrAdd basic behavior, per-key isolation, missing-key handling, factory invocation counts, expiration after the TTL elapses, and the extendCacheExpiration flag's effect on the expiration window. 562 -> 573 unit tests, all passing. Build and fantomas --check pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/SwaggerProvider.Tests/CachingTests.fs | 111 ++++++++++++++++++ .../SwaggerProvider.Tests.fsproj | 1 + 2 files changed, 112 insertions(+) create mode 100644 tests/SwaggerProvider.Tests/CachingTests.fs diff --git a/tests/SwaggerProvider.Tests/CachingTests.fs b/tests/SwaggerProvider.Tests/CachingTests.fs new file mode 100644 index 00000000..b41a7415 --- /dev/null +++ b/tests/SwaggerProvider.Tests/CachingTests.fs @@ -0,0 +1,111 @@ +namespace SwaggerProvider.Tests.CachingTests + +open System +open System.Threading +open Xunit +open FsUnitTyped +open SwaggerProvider.Caching + +/// Unit tests for the in-memory ICache implementation used by Provider.OpenApiClient.fs +/// to cache generated provided types keyed by schema/parameter combination. +module InMemoryCacheTests = + + [] + let ``TryRetrieve returns None for a key that was never set``() = + let cache = createInMemoryCache(TimeSpan.FromMinutes 5.0) + cache.TryRetrieve("missing") |> shouldEqual None + + [] + let ``Set followed by TryRetrieve returns the stored value``() = + let cache = createInMemoryCache(TimeSpan.FromMinutes 5.0) + cache.Set("key1", 42) + cache.TryRetrieve("key1") |> shouldEqual(Some 42) + + [] + let ``Set overwrites a previously stored value for the same key``() = + let cache = createInMemoryCache(TimeSpan.FromMinutes 5.0) + cache.Set("key1", "first") + cache.Set("key1", "second") + cache.TryRetrieve("key1") |> shouldEqual(Some "second") + + [] + let ``Remove deletes a stored value so TryRetrieve returns None``() = + let cache = createInMemoryCache(TimeSpan.FromMinutes 5.0) + cache.Set("key1", 1) + cache.Remove("key1") + cache.TryRetrieve("key1") |> shouldEqual None + + [] + let ``Remove on a missing key does not throw``() = + let cache = createInMemoryCache(TimeSpan.FromMinutes 5.0) + // Should be a no-op, not an exception. + cache.Remove("never-set") + + [] + let ``GetOrAdd calls the factory once and returns its result on first access``() = + let cache = createInMemoryCache(TimeSpan.FromMinutes 5.0) + let mutable calls = 0 + + let result = + cache.GetOrAdd( + "key1", + fun () -> + calls <- calls + 1 + "computed" + ) + + result |> shouldEqual "computed" + calls |> shouldEqual 1 + + [] + let ``GetOrAdd does not call the factory again once the value is cached``() = + let cache = createInMemoryCache(TimeSpan.FromMinutes 5.0) + let mutable calls = 0 + + let factory() = + calls <- calls + 1 + calls + + cache.GetOrAdd("key1", factory) |> shouldEqual 1 + // Second call should return the same cached value (1), not invoke the factory again (which would return 2). + cache.GetOrAdd("key1", factory) |> shouldEqual 1 + calls |> shouldEqual 1 + + [] + let ``GetOrAdd with different keys caches values independently``() = + let cache = createInMemoryCache(TimeSpan.FromMinutes 5.0) + cache.GetOrAdd("a", fun () -> "value-a") |> shouldEqual "value-a" + cache.GetOrAdd("b", fun () -> "value-b") |> shouldEqual "value-b" + cache.TryRetrieve("a") |> shouldEqual(Some "value-a") + cache.TryRetrieve("b") |> shouldEqual(Some "value-b") + + [] + let ``TryRetrieve returns None once the expiration window has elapsed``() = + let cache = createInMemoryCache(TimeSpan.FromMilliseconds 20.0) + cache.Set("key1", "value") + Thread.Sleep(200) + cache.TryRetrieve("key1") |> shouldEqual None + + [] + let ``TryRetrieve with extendCacheExpiration=true keeps the entry alive past the original expiration``() = + let cache = createInMemoryCache(TimeSpan.FromMilliseconds 150.0) + cache.Set("key1", "value") + // Read partway through the window and extend the expiration. + Thread.Sleep(80) + + cache.TryRetrieve("key1", extendCacheExpiration = true) + |> shouldEqual(Some "value") + // Total elapsed time (80 + 100 = 180ms) exceeds the original 150ms window, + // but the extension at 80ms should have reset the clock, so it should still be present. + Thread.Sleep(100) + cache.TryRetrieve("key1") |> shouldEqual(Some "value") + + [] + let ``TryRetrieve without extendCacheExpiration does not reset expiration``() = + let cache = createInMemoryCache(TimeSpan.FromMilliseconds 100.0) + cache.Set("key1", "value") + Thread.Sleep(60) + cache.TryRetrieve("key1") |> shouldEqual(Some "value") + Thread.Sleep(80) + // Original 100ms window has elapsed (60 + 80 = 140ms) without extension. + cache.TryRetrieve("key1") |> shouldEqual None diff --git a/tests/SwaggerProvider.Tests/SwaggerProvider.Tests.fsproj b/tests/SwaggerProvider.Tests/SwaggerProvider.Tests.fsproj index e039d519..d878d6de 100644 --- a/tests/SwaggerProvider.Tests/SwaggerProvider.Tests.fsproj +++ b/tests/SwaggerProvider.Tests/SwaggerProvider.Tests.fsproj @@ -24,6 +24,7 @@ + From 7831509c561eaaea109776dc37c168816f478299 Mon Sep 17 00:00:00 2001 From: Sergey Tihon Date: Wed, 9 Sep 2026 20:50:50 +0200 Subject: [PATCH 2/2] Update cache expiration test timing and logic Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- tests/SwaggerProvider.Tests/CachingTests.fs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/SwaggerProvider.Tests/CachingTests.fs b/tests/SwaggerProvider.Tests/CachingTests.fs index b41a7415..85bc4362 100644 --- a/tests/SwaggerProvider.Tests/CachingTests.fs +++ b/tests/SwaggerProvider.Tests/CachingTests.fs @@ -88,16 +88,16 @@ module InMemoryCacheTests = [] let ``TryRetrieve with extendCacheExpiration=true keeps the entry alive past the original expiration``() = - let cache = createInMemoryCache(TimeSpan.FromMilliseconds 150.0) + let cache = createInMemoryCache(TimeSpan.FromMilliseconds 500.0) cache.Set("key1", "value") // Read partway through the window and extend the expiration. - Thread.Sleep(80) + Thread.Sleep(200) cache.TryRetrieve("key1", extendCacheExpiration = true) |> shouldEqual(Some "value") - // Total elapsed time (80 + 100 = 180ms) exceeds the original 150ms window, - // but the extension at 80ms should have reset the clock, so it should still be present. - Thread.Sleep(100) + // Total elapsed time (200 + 350 = 550ms) exceeds the original 500ms window, + // but the extension at 200ms should have reset the clock, so it should still be present. + Thread.Sleep(350) cache.TryRetrieve("key1") |> shouldEqual(Some "value") []