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 |
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 |
public enum TextRenderingMode : byte |
||||
{ |
{ |
||||
|
/// <summary>
|
||||
|
/// Rendering mode is not explicitly specified.
|
||||
|
/// The system or platform default will be used.
|
||||
|
/// </summary>
|
||||
Unspecified, |
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, |
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 |
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