A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

351 lines
11 KiB

#nullable enable
using System;
using System.Collections.Generic;
using Avalonia.Skia;
using Xunit;
namespace Avalonia.Skia.UnitTests
{
public class TwoLevelCacheTests
{
[Fact]
public void Constructor_WithNegativeSecondarySize_ThrowsArgumentOutOfRangeException()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new TwoLevelCache<string, object>(-1));
}
[Fact]
public void Constructor_WithZeroSecondarySize_DoesNotThrow()
{
var cache = new TwoLevelCache<string, object>(0);
Assert.NotNull(cache);
}
[Fact]
public void TryGet_EmptyCache_ReturnsFalse()
{
var cache = new TwoLevelCache<string, object>();
var result = cache.TryGet("key", out var value);
Assert.False(result);
Assert.Null(value);
}
[Fact]
public void GetOrAdd_FirstItem_StoresInPrimary()
{
var cache = new TwoLevelCache<string, object>();
var value = new object();
var result = cache.GetOrAdd("key1", _ => value);
Assert.Same(value, result);
Assert.True(cache.TryGet("key1", out var retrieved));
Assert.Same(value, retrieved);
}
[Fact]
public void GetOrAdd_SameKey_ReturnsExistingValue()
{
var cache = new TwoLevelCache<string, object>();
var value1 = new object();
var value2 = new object();
cache.GetOrAdd("key", _ => value1);
var result = cache.GetOrAdd("key", _ => value2);
Assert.Same(value1, result);
}
[Fact]
public void GetOrAdd_SecondItem_StoresInSecondary()
{
var cache = new TwoLevelCache<string, object>(secondarySize: 3);
var value1 = new object();
var value2 = new object();
cache.GetOrAdd("key1", _ => value1);
cache.GetOrAdd("key2", _ => value2);
Assert.True(cache.TryGet("key1", out var retrieved1));
Assert.Same(value1, retrieved1);
Assert.True(cache.TryGet("key2", out var retrieved2));
Assert.Same(value2, retrieved2);
}
[Fact]
public void GetOrAdd_MultipleItems_StoresCorrectly()
{
var cache = new TwoLevelCache<string, object>(secondarySize: 3);
var values = new object[4];
for (int i = 0; i < 4; i++)
{
values[i] = new object();
cache.GetOrAdd($"key{i}", _ => values[i]);
}
// All should be retrievable
for (int i = 0; i < 4; i++)
{
Assert.True(cache.TryGet($"key{i}", out var retrieved));
Assert.Same(values[i], retrieved);
}
}
[Fact]
public void GetOrAdd_ExceedsCapacity_CallsEvictionAction()
{
var evictedValues = new List<object?>();
var cache = new TwoLevelCache<string, object>(
secondarySize: 2,
evictionAction: v => evictedValues.Add(v));
var value1 = new object();
var value2 = new object();
var value3 = new object();
var value4 = new object();
cache.GetOrAdd("key1", _ => value1);
cache.GetOrAdd("key2", _ => value2);
cache.GetOrAdd("key3", _ => value3);
// No evictions yet
Assert.Empty(evictedValues);
// This should cause eviction
cache.GetOrAdd("key4", _ => value4);
Assert.Single(evictedValues);
Assert.Same(value2, evictedValues[0]);
}
[Fact]
public void GetOrAdd_ZeroSecondarySize_EvictsPrimaryImmediately()
{
var evictedValues = new List<object?>();
var cache = new TwoLevelCache<string, object>(
secondarySize: 0,
evictionAction: v => evictedValues.Add(v));
var value1 = new object();
var value2 = new object();
cache.GetOrAdd("key1", _ => value1);
cache.GetOrAdd("key2", _ => value2);
Assert.Single(evictedValues);
Assert.Same(value1, evictedValues[0]);
// Only the latest value should be retrievable
Assert.False(cache.TryGet("key1", out _));
Assert.True(cache.TryGet("key2", out var retrieved));
Assert.Same(value2, retrieved);
}
[Fact]
public void GetOrAdd_DuplicateKey_ReturnsExistingWithoutCallingFactory()
{
var cache = new TwoLevelCache<string, object>();
var value1 = new object();
var factoryCalled = false;
// Add initial value
cache.GetOrAdd("key", _ => value1);
// Try to add again - factory should not be called
var result = cache.GetOrAdd("key", _ =>
{
factoryCalled = true;
return new object();
});
// Should return first value without calling factory
Assert.Same(value1, result);
Assert.False(factoryCalled);
}
[Fact]
public void GetOrAdd_DuplicateKeyInSecondary_ReturnsExistingWithoutCallingFactory()
{
var cache = new TwoLevelCache<string, object>(secondarySize: 2);
var value1 = new object();
var value2 = new object();
var factoryCalled = false;
cache.GetOrAdd("key1", _ => value1);
cache.GetOrAdd("key2", _ => value2);
// Try to add key2 again - factory should not be called
var result = cache.GetOrAdd("key2", _ =>
{
factoryCalled = true;
return new object();
});
Assert.Same(value2, result);
Assert.False(factoryCalled);
}
[Fact]
public void ClearAndDispose_EmptyCache_DoesNotThrow()
{
var cache = new TwoLevelCache<string, object>();
cache.ClearAndDispose();
}
[Fact]
public void ClearAndDispose_WithValues_CallsEvictionActionForAll()
{
var evictedValues = new List<object?>();
var cache = new TwoLevelCache<string, object>(
secondarySize: 2,
evictionAction: v => evictedValues.Add(v));
var value1 = new object();
var value2 = new object();
var value3 = new object();
cache.GetOrAdd("key1", _ => value1);
cache.GetOrAdd("key2", _ => value2);
cache.GetOrAdd("key3", _ => value3);
cache.ClearAndDispose();
Assert.Equal(3, evictedValues.Count);
Assert.Contains(value1, evictedValues);
Assert.Contains(value2, evictedValues);
Assert.Contains(value3, evictedValues);
}
[Fact]
public void ClearAndDispose_ClearsAllEntries()
{
var cache = new TwoLevelCache<string, object>(secondarySize: 2);
cache.GetOrAdd("key1", _ => new object());
cache.GetOrAdd("key2", _ => new object());
cache.ClearAndDispose();
Assert.False(cache.TryGet("key1", out _));
Assert.False(cache.TryGet("key2", out _));
}
[Fact]
public void GetOrAdd_WithCustomComparer_UsesComparer()
{
var comparer = StringComparer.OrdinalIgnoreCase;
var cache = new TwoLevelCache<string, object>(comparer: comparer);
var value = new object();
cache.GetOrAdd("KEY", _ => value);
Assert.True(cache.TryGet("key", out var retrieved));
Assert.Same(value, retrieved);
}
[Fact]
public void TryGet_WithCustomComparer_UsesComparer()
{
var comparer = StringComparer.OrdinalIgnoreCase;
var cache = new TwoLevelCache<string, object>(
secondarySize: 2,
comparer: comparer);
var value1 = new object();
var value2 = new object();
cache.GetOrAdd("PRIMARY", _ => value1);
cache.GetOrAdd("SECONDARY", _ => value2);
Assert.True(cache.TryGet("primary", out var retrieved1));
Assert.Same(value1, retrieved1);
Assert.True(cache.TryGet("secondary", out var retrieved2));
Assert.Same(value2, retrieved2);
}
[Fact]
public void GetOrAdd_IntKeys_WorksCorrectly()
{
var cache = new TwoLevelCache<int, object>(secondarySize: 2);
var value1 = new object();
var value2 = new object();
var value3 = new object();
cache.GetOrAdd(1, _ => value1);
cache.GetOrAdd(2, _ => value2);
cache.GetOrAdd(3, _ => value3);
Assert.True(cache.TryGet(1, out var retrieved1));
Assert.Same(value1, retrieved1);
Assert.True(cache.TryGet(2, out var retrieved2));
Assert.Same(value2, retrieved2);
Assert.True(cache.TryGet(3, out var retrieved3));
Assert.Same(value3, retrieved3);
}
[Fact]
public void GetOrAdd_RotatesSecondaryCorrectly()
{
var evictedValues = new List<object?>();
var cache = new TwoLevelCache<int, object>(
secondarySize: 2,
evictionAction: v => evictedValues.Add(v));
var values = new object[5];
for (int i = 0; i < 5; i++)
{
values[i] = new object();
cache.GetOrAdd(i, _ => values[i]);
}
// Primary: 0, Secondary: [1, 2]
// After adding 3: Primary: 0, Secondary: [3, 1] (evicts 2)
// After adding 4: Primary: 0, Secondary: [4, 3] (evicts 1)
Assert.Equal(2, evictedValues.Count);
Assert.Contains(values[2], evictedValues);
Assert.Contains(values[1], evictedValues);
// These should still be in cache
Assert.True(cache.TryGet(0, out _));
Assert.True(cache.TryGet(3, out _));
Assert.True(cache.TryGet(4, out _));
// These should be evicted
Assert.False(cache.TryGet(1, out _));
Assert.False(cache.TryGet(2, out _));
}
[Fact]
public void FactoryFunction_ReceivesCorrectKey()
{
var cache = new TwoLevelCache<string, string>();
string? capturedKey = null;
cache.GetOrAdd("testKey", key =>
{
capturedKey = key;
return "value";
});
Assert.Equal("testKey", capturedKey);
}
[Fact]
public void GetOrAdd_NullEvictionAction_DoesNotThrow()
{
var cache = new TwoLevelCache<string, object>(
secondarySize: 1,
evictionAction: null);
cache.GetOrAdd("key1", _ => new object());
cache.GetOrAdd("key2", _ => new object());
cache.GetOrAdd("key3", _ => new object()); // Should evict without error
cache.ClearAndDispose(); // Should also not throw
}
}
}