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.
 
 
 

179 lines
7.2 KiB

using Avalonia.Media.TextFormatting.Unicode;
using Xunit;
namespace Avalonia.Base.UnitTests.Media.TextFormatting;
/// <summary>
/// Direct coverage for <see cref="UnicodeTrie"/> and <see cref="UnicodeTrieBuilder"/>.
/// The production tries (UnicodeData, BiDi, GraphemeBreak, EastAsianWidth) are
/// used to exercise the four branches of <see cref="UnicodeTrie.Get"/>; a small
/// synthetic trie covers the error-value path (which is otherwise unreachable
/// because the committed tries are generated with <c>errorValue == 0</c>) and the
/// builder round-trip.
/// </summary>
public class UnicodeTrieTests
{
[Theory]
[InlineData(0x0020u)] // ASCII space
[InlineData(0x0061u)] // 'a'
[InlineData(0x4E2Du)] // '中' (BMP CJK)
[InlineData(0xFFFFu)] // last BMP non-surrogate
public void Get_BmpNonSurrogate_ReturnsValueMatchingUnicodeDataWrapper(uint codepoint)
{
// Walking the trie directly and re-applying the published shift/mask must
// produce the same answer as the public UnicodeData wrapper. This catches
// packing-layout drift between generator (writes packed bits) and
// UnicodeData.Get* (reads packed bits).
var packed = UnicodeDataTrie.Trie.Get(codepoint);
var categoryFromTrie = (GeneralCategory)(packed & UnicodeData.CATEGORY_MASK);
var scriptFromTrie = (Script)((packed >> UnicodeData.SCRIPT_SHIFT) & UnicodeData.SCRIPT_MASK);
Assert.Equal(UnicodeData.GetGeneralCategory(codepoint), categoryFromTrie);
Assert.Equal(UnicodeData.GetScript(codepoint), scriptFromTrie);
}
[Theory]
[InlineData(0xD800u)] // first high surrogate
[InlineData(0xDB00u)] // mid high surrogate
[InlineData(0xDBFFu)] // last high surrogate
[InlineData(0xDC00u)] // first low surrogate
[InlineData(0xDFFFu)] // last low surrogate
public void Get_SurrogateRange_ResolvesViaLscpIndex(uint codepoint)
{
// Surrogates have a dedicated index region (LSCP_INDEX_2_OFFSET) in the
// trie. The general category for every codepoint in this range is
// Surrogate; this asserts the LSCP branch returns the correct row.
Assert.Equal(GeneralCategory.Surrogate, UnicodeData.GetGeneralCategory(codepoint));
}
[Theory]
[InlineData(0x10000u)] // first supplementary
[InlineData(0x1F600u)] // 😀
[InlineData(0x2F800u)] // CJK compatibility supplement
public void Get_Supplementary_BelowHighStart_ResolvesViaTwoLevelLookup(uint codepoint)
{
// Just walking the trie and reapplying the published mask must match the
// wrapper — same guarantee as the BMP test, but exercises the two-level
// supplementary lookup branch.
var packed = BiDiTrie.Trie.Get(codepoint);
var bidiFromTrie = (BidiClass)((packed >> UnicodeData.BIDICLASS_SHIFT) & UnicodeData.BIDICLASS_MASK);
Assert.Equal(UnicodeData.GetBiDiClass(codepoint), bidiFromTrie);
}
[Fact]
public void Get_AtAndAboveHighStart_AllCodepointsShareFallback()
{
// Every codepoint >= HighStart short-circuits to the trie's last data
// block. The committed tries set HighStart at 0x100000, so all of Plane
// 16 collapses to one fallback value — this is a compression artifact
// of the trie format. The test verifies the SHAPE of that contract (one
// value for the whole high range) rather than asserting any specific
// per-codepoint property: callers querying Plane 16 should not rely on
// PUA / Unassigned distinctions surviving the trie.
var v100000 = UnicodeDataTrie.Trie.Get(0x100000u);
Assert.Equal(v100000, UnicodeDataTrie.Trie.Get(0x100001u));
Assert.Equal(v100000, UnicodeDataTrie.Trie.Get(0x10FFFDu));
Assert.Equal(v100000, UnicodeDataTrie.Trie.Get(0x10FFFFu));
var bidi100000 = BiDiTrie.Trie.Get(0x100000u);
Assert.Equal(bidi100000, BiDiTrie.Trie.Get(0x10FFFFu));
}
[Fact]
public void Get_BeyondMaxCodepoint_IsHandledGracefullyByProductionTries()
{
// The committed tries are generated with errorValue == 0, so codepoints
// past 0x10FFFF return 0 (Other category, LeftToRight bidi, etc.). This
// documents that contract; the synthetic-trie test below covers the
// case where errorValue is non-zero.
const uint beyondRange = 0x110000u;
Assert.Equal(0u, UnicodeDataTrie.Trie.Get(beyondRange));
Assert.Equal(0u, BiDiTrie.Trie.Get(beyondRange));
Assert.Equal(0u, SegmentationTrie.Trie.Get(beyondRange));
Assert.Equal(0u, EastAsianWidthTrie.Trie.Get(beyondRange));
}
[Fact]
public void Builder_RoundTripsSetValues()
{
var builder = new UnicodeTrieBuilder(initialValue: 7u);
builder.Set(0x0061, 0xAA);
builder.Set(0x4E2D, 0xBB);
builder.Set(0x1F600, 0xCC);
var trie = builder.Freeze();
Assert.Equal(0xAAu, trie.Get(0x0061));
Assert.Equal(0xBBu, trie.Get(0x4E2D));
Assert.Equal(0xCCu, trie.Get(0x1F600));
}
[Fact]
public void Builder_SetRange_AppliesValueToEveryCodepointInRange()
{
var builder = new UnicodeTrieBuilder();
builder.SetRange(0x2000, 0x2010, 0x42);
var trie = builder.Freeze();
for (uint cp = 0x2000; cp <= 0x2010; cp++)
{
Assert.Equal(0x42u, trie.Get(cp));
}
// Just outside the range stays at the initial value (0 by default).
Assert.Equal(0u, trie.Get(0x1FFF));
Assert.Equal(0u, trie.Get(0x2011));
}
[Fact]
public void Builder_UnassignedCodepoints_GetInitialValue()
{
var builder = new UnicodeTrieBuilder(initialValue: 0xDEAD);
builder.Set(0x0061, 0xBEEF);
var trie = builder.Freeze();
Assert.Equal(0xBEEFu, trie.Get(0x0061));
Assert.Equal(0xDEADu, trie.Get(0x0062));
Assert.Equal(0xDEADu, trie.Get(0x4E2D));
Assert.Equal(0xDEADu, trie.Get(0x1F600));
}
[Fact]
public void Get_OutOfRange_ReturnsConfiguredErrorValue()
{
// Build with a non-zero errorValue so the "> 0x10FFFF" branch produces a
// distinguishable result. This is the only feasible test of that branch —
// the committed tries all use errorValue == 0 which collides with the
// happy-path zero value.
var builder = new UnicodeTrieBuilder(initialValue: 0u, errorValue: 0xFFFFu);
builder.Set(0x0061, 0x11);
var trie = builder.Freeze();
Assert.Equal(0xFFFFu, trie.Get(0x110000u));
Assert.Equal(0xFFFFu, trie.Get(0xFFFFFFFFu));
}
[Fact]
public void Get_AtAndAboveHighStart_OnSyntheticTrie_UsesHighFallback()
{
// SetRange across a huge supplementary span forces the builder to allocate
// a high block. Codepoints at and above the resulting HighStart should
// return the value that covers the high range.
var builder = new UnicodeTrieBuilder(initialValue: 0u);
builder.SetRange(0x80000, 0x10FFFF, 0x55);
var trie = builder.Freeze();
Assert.Equal(0x55u, trie.Get(0x100000u));
Assert.Equal(0x55u, trie.Get(0x10FFFFu));
// Below the high range — still the initial value.
Assert.Equal(0u, trie.Get(0x1000u));
}
}