Browse Source
* Introduce TextOptions API * Store BaselinePixelAlignment as byte. Move to a dedicated file. * Update API suppressions --------- Co-authored-by: Julien Lebosquain <julien@lebosquain.net>pull/20529/head
committed by
GitHub
33 changed files with 1389 additions and 1062 deletions
File diff suppressed because it is too large
@ -0,0 +1,26 @@ |
|||
namespace Avalonia.Media |
|||
{ |
|||
/// <summary>
|
|||
/// Specifies the baseline pixel alignment options for rendering text or graphics.
|
|||
/// </summary>
|
|||
/// <remarks>Use this enumeration to control whether the baseline of rendered content is aligned to the
|
|||
/// pixel grid, which can affect visual crispness and positioning. The value may influence rendering quality,
|
|||
/// especially at small font sizes or when precise alignment is required.</remarks>
|
|||
public enum BaselinePixelAlignment : byte |
|||
{ |
|||
/// <summary>
|
|||
/// The baseline pixel alignment is unspecified.
|
|||
/// </summary>
|
|||
Unspecified, |
|||
|
|||
/// <summary>
|
|||
/// The baseline is aligned to the pixel grid.
|
|||
/// </summary>
|
|||
Aligned, |
|||
|
|||
/// <summary>
|
|||
/// The baseline is not aligned to the pixel grid.
|
|||
/// </summary>
|
|||
Unaligned |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
namespace Avalonia.Media |
|||
{ |
|||
/// <summary>
|
|||
/// Specifies the level of hinting applied to text glyphs during rendering.
|
|||
/// Text hinting adjusts glyph outlines to improve readability and crispness,
|
|||
/// especially at small font sizes or low DPI. This enum controls the amount
|
|||
/// of grid-fitting and outline adjustment performed.
|
|||
/// </summary>
|
|||
public enum TextHintingMode : byte |
|||
{ |
|||
/// <summary>
|
|||
/// Hinting mode is not explicitly specified. The default will be used.
|
|||
/// </summary>
|
|||
Unspecified, |
|||
|
|||
/// <summary>
|
|||
/// No hinting, outlines are scaled only.
|
|||
/// </summary>
|
|||
None, |
|||
|
|||
/// <summary>
|
|||
/// Minimal hinting, preserves glyph shape.
|
|||
/// </summary>
|
|||
Light, |
|||
|
|||
/// <summary>
|
|||
/// Aggressive grid-fitting, maximum crispness at low DPI.
|
|||
/// </summary>
|
|||
Strong |
|||
} |
|||
} |
|||
@ -0,0 +1,136 @@ |
|||
namespace Avalonia.Media |
|||
{ |
|||
/// <summary>
|
|||
/// Provides options for controlling text rendering behavior, including rendering mode, hinting mode, and baseline
|
|||
/// pixel alignment. Used to configure how text appears within visual elements.
|
|||
/// </summary>
|
|||
/// <remarks>TextOptions encapsulates settings that influence the clarity, sharpness, and positioning of
|
|||
/// rendered text. These options can be applied to visual elements to customize text appearance for different
|
|||
/// display scenarios, such as optimizing for readability at small font sizes or ensuring pixel-perfect alignment.
|
|||
/// The struct supports merging with other instances to inherit unspecified values, and exposes attached properties
|
|||
/// for use with visuals.</remarks>
|
|||
public readonly record struct TextOptions |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the text rendering mode used to control how text glyphs are rendered.
|
|||
/// </summary>
|
|||
public TextRenderingMode TextRenderingMode { get; init; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the text rendering hinting mode used to optimize the display of text.
|
|||
/// </summary>
|
|||
/// <remarks>The hinting mode determines how text is rendered to improve clarity and readability,
|
|||
/// especially at small font sizes. Changing this value may affect the appearance of text depending on the
|
|||
/// rendering engine and display device.</remarks>
|
|||
public TextHintingMode TextHintingMode { get; init; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether the text baseline should be aligned to the pixel grid.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// When enabled, the vertical position of the text baseline is snapped to whole pixel boundaries.
|
|||
/// This ensures consistent sharpness and reduces blurriness caused by fractional positioning,
|
|||
/// particularly at small font sizes or low DPI settings.
|
|||
/// </remarks>
|
|||
public BaselinePixelAlignment BaselinePixelAlignment { get; init; } |
|||
|
|||
/// <summary>
|
|||
/// Merges this instance with <paramref name="other"/> using inheritance semantics: unspecified values on this
|
|||
/// instance are taken from <paramref name="other"/>.
|
|||
/// </summary>
|
|||
public TextOptions MergeWith(TextOptions other) |
|||
{ |
|||
var textRenderingMode = TextRenderingMode; |
|||
|
|||
if (textRenderingMode == TextRenderingMode.Unspecified) |
|||
{ |
|||
textRenderingMode = other.TextRenderingMode; |
|||
} |
|||
|
|||
var textHintingMode = TextHintingMode; |
|||
|
|||
if (textHintingMode == TextHintingMode.Unspecified) |
|||
{ |
|||
textHintingMode = other.TextHintingMode; |
|||
} |
|||
|
|||
var baselinePixelAlignment = BaselinePixelAlignment; |
|||
|
|||
if (baselinePixelAlignment == BaselinePixelAlignment.Unspecified) |
|||
{ |
|||
baselinePixelAlignment = other.BaselinePixelAlignment; |
|||
} |
|||
|
|||
return new TextOptions |
|||
{ |
|||
TextRenderingMode = textRenderingMode, |
|||
TextHintingMode = textHintingMode, |
|||
BaselinePixelAlignment = baselinePixelAlignment |
|||
}; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the TextOptions attached value for a visual.
|
|||
/// </summary>
|
|||
public static TextOptions GetTextOptions(Visual visual) |
|||
{ |
|||
return visual.TextOptions; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sets the TextOptions attached value for a visual.
|
|||
/// </summary>
|
|||
public static void SetTextOptions(Visual visual, TextOptions value) |
|||
{ |
|||
visual.TextOptions = value; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the TextRenderingMode attached property for a visual.
|
|||
/// </summary>
|
|||
public static TextRenderingMode GetTextRenderingMode(Visual visual) |
|||
{ |
|||
return visual.TextOptions.TextRenderingMode; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sets the TextRenderingMode attached property for a visual.
|
|||
/// </summary>
|
|||
public static void SetTextRenderingMode(Visual visual, TextRenderingMode value) |
|||
{ |
|||
visual.TextOptions = visual.TextOptions with { TextRenderingMode = value }; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the TextHintingMode attached property for a visual.
|
|||
/// </summary>
|
|||
public static TextHintingMode GetTextHintingMode(Visual visual) |
|||
{ |
|||
return visual.TextOptions.TextHintingMode; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sets the TextHintingMode attached property for a visual.
|
|||
/// </summary>
|
|||
public static void SetTextHintingMode(Visual visual, TextHintingMode value) |
|||
{ |
|||
visual.TextOptions = visual.TextOptions with { TextHintingMode = value }; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the BaselinePixelAlignment attached property for a visual.
|
|||
/// </summary>
|
|||
public static BaselinePixelAlignment GetBaselinePixelAlignment(Visual visual) |
|||
{ |
|||
return visual.TextOptions.BaselinePixelAlignment; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sets the BaselinePixelAlignment attached property for a visual.
|
|||
/// </summary>
|
|||
public static void SetBaselinePixelAlignment(Visual visual, BaselinePixelAlignment value) |
|||
{ |
|||
visual.TextOptions = visual.TextOptions with { BaselinePixelAlignment = value }; |
|||
} |
|||
} |
|||
} |
|||
@ -1,11 +1,36 @@ |
|||
namespace Avalonia.Media |
|||
{ |
|||
/// <summary>
|
|||
/// Specifies how text glyphs are rendered in Avalonia.
|
|||
/// Controls the smoothing and antialiasing applied during text rasterization.
|
|||
/// </summary>
|
|||
public enum TextRenderingMode : byte |
|||
{ |
|||
/// <summary>
|
|||
/// Rendering mode is not explicitly specified.
|
|||
/// The system or platform default will be used.
|
|||
/// </summary>
|
|||
Unspecified, |
|||
|
|||
SubpixelAntialias, |
|||
/// <summary>
|
|||
/// Glyphs are rendered with subpixel antialiasing.
|
|||
/// This provides higher apparent resolution on LCD screens
|
|||
/// by using the individual red, green, and blue subpixels.
|
|||
/// </summary>
|
|||
SubpixelAntialias, |
|||
|
|||
/// <summary>
|
|||
/// Glyphs are rendered with standard grayscale antialiasing.
|
|||
/// This smooths edges without using subpixel information,
|
|||
/// preserving shape fidelity across different display types.
|
|||
/// </summary>
|
|||
Antialias, |
|||
|
|||
/// <summary>
|
|||
/// Glyphs are rendered without antialiasing.
|
|||
/// This produces sharp, aliased edges and may be useful
|
|||
/// for pixel-art aesthetics or low-DPI environments.
|
|||
/// </summary>
|
|||
Alias |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,157 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.Skia |
|||
{ |
|||
/// <summary>
|
|||
/// Provides a lightweight two-level cache for storing key-value pairs, supporting fast retrieval and optional
|
|||
/// eviction handling.
|
|||
/// </summary>
|
|||
/// <remarks>The cache maintains a primary entry for the most recently added item and a secondary array
|
|||
/// for additional items, with a configurable capacity. When the cache exceeds its capacity, evicted values can be
|
|||
/// processed using an optional eviction action. This class is intended for internal use and is not
|
|||
/// thread-safe.</remarks>
|
|||
/// <typeparam name="TKey">The type of keys used to identify cached values. Must be non-nullable.</typeparam>
|
|||
/// <typeparam name="TValue">The type of values to be stored in the cache. Must be a reference type.</typeparam>
|
|||
internal class TwoLevelCache<TKey, TValue> |
|||
where TKey : notnull |
|||
where TValue : class |
|||
{ |
|||
private readonly int _secondarySize; |
|||
private TKey? _primaryKey; |
|||
private TValue? _primaryValue; |
|||
private KeyValuePair<TKey, TValue>[]? _secondary; |
|||
private int _secondaryCount; |
|||
private readonly Action<TValue?>? _evictionAction; |
|||
private readonly IEqualityComparer<TKey> _comparer; |
|||
|
|||
public TwoLevelCache(int secondarySize = 3, Action<TValue?>? evictionAction = null, IEqualityComparer<TKey>? comparer = null) |
|||
{ |
|||
if (secondarySize < 0) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(secondarySize)); |
|||
} |
|||
|
|||
_secondarySize = secondarySize; |
|||
_evictionAction = evictionAction; |
|||
_comparer = comparer ?? EqualityComparer<TKey>.Default; |
|||
} |
|||
|
|||
public bool TryGet(TKey key, out TValue? value) |
|||
{ |
|||
if (_primaryValue != null && _comparer.Equals(_primaryKey!, key)) |
|||
{ |
|||
value = _primaryValue; |
|||
return true; |
|||
} |
|||
|
|||
var sec = _secondary; |
|||
if (sec != null) |
|||
{ |
|||
for (int i = 0; i < _secondaryCount; i++) |
|||
{ |
|||
if (_comparer.Equals(sec[i].Key, key)) |
|||
{ |
|||
value = sec[i].Value; |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
value = null; |
|||
return false; |
|||
} |
|||
|
|||
public TValue GetOrAdd(TKey key, Func<TKey, TValue> factory) |
|||
{ |
|||
// Check if key already exists
|
|||
if (TryGet(key, out var existing) && existing != null) |
|||
{ |
|||
return existing; |
|||
} |
|||
|
|||
// Key doesn't exist, create new value
|
|||
var value = factory(key); |
|||
|
|||
// Primary is empty - store in primary
|
|||
if (_primaryValue == null) |
|||
{ |
|||
_primaryKey = key; |
|||
_primaryValue = value; |
|||
return value; |
|||
} |
|||
|
|||
// No secondary cache configured - replace primary
|
|||
if (_secondarySize == 0) |
|||
{ |
|||
_evictionAction?.Invoke(_primaryValue); |
|||
_primaryKey = key; |
|||
_primaryValue = value; |
|||
return value; |
|||
} |
|||
|
|||
// Secondary not yet initialized - create it
|
|||
if (_secondary == null) |
|||
{ |
|||
_secondary = new KeyValuePair<TKey, TValue>[_secondarySize]; |
|||
_secondaryCount = 0; |
|||
} |
|||
|
|||
// Shift existing entries right and insert new one at front
|
|||
// This maintains insertion order and evicts the oldest (last) entry when full
|
|||
TValue? evicted = default; |
|||
bool shouldEvict = _secondaryCount == _secondarySize; |
|||
|
|||
if (shouldEvict) |
|||
{ |
|||
// Cache is full, last entry will be evicted
|
|||
evicted = _secondary[_secondarySize - 1].Value; |
|||
} |
|||
|
|||
// Shift existing entries to make room at index 0
|
|||
int shiftCount = shouldEvict ? _secondarySize - 1 : _secondaryCount; |
|||
for (int i = shiftCount; i > 0; i--) |
|||
{ |
|||
_secondary[i] = _secondary[i - 1]; |
|||
} |
|||
|
|||
// Insert new entry at front
|
|||
_secondary[0] = new KeyValuePair<TKey, TValue>(key, value); |
|||
|
|||
// Update count (capped at size)
|
|||
if (_secondaryCount < _secondarySize) |
|||
{ |
|||
_secondaryCount++; |
|||
} |
|||
|
|||
// Invoke eviction action if we evicted an entry
|
|||
if (shouldEvict) |
|||
{ |
|||
_evictionAction?.Invoke(evicted); |
|||
} |
|||
|
|||
return value; |
|||
} |
|||
|
|||
public void ClearAndDispose() |
|||
{ |
|||
if (_primaryValue != null) |
|||
{ |
|||
_evictionAction?.Invoke(_primaryValue); |
|||
_primaryValue = null; |
|||
_primaryKey = default; |
|||
} |
|||
|
|||
if (_secondary != null) |
|||
{ |
|||
for (int i = 0; i < _secondaryCount; i++) |
|||
{ |
|||
_evictionAction?.Invoke(_secondary[i].Value); |
|||
} |
|||
|
|||
_secondary = null; |
|||
_secondaryCount = 0; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,351 @@ |
|||
#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
|
|||
} |
|||
} |
|||
} |
|||
|
After Width: | Height: | Size: 890 B |
|
After Width: | Height: | Size: 885 B |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
Loading…
Reference in new issue