LineBreakEnumeratorTests.cs 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Net.Http;
  7. using Avalonia.Media.TextFormatting.Unicode;
  8. using Xunit;
  9. using Xunit.Abstractions;
  10. using static Avalonia.Media.TextFormatting.Unicode.LineBreakEnumerator;
  11. namespace Avalonia.Base.UnitTests.Media.TextFormatting
  12. {
  13. public class LineBreakEnumeratorTests
  14. {
  15. private readonly ITestOutputHelper _outputHelper;
  16. public LineBreakEnumeratorTests(ITestOutputHelper outputHelper)
  17. {
  18. _outputHelper = outputHelper;
  19. }
  20. [Fact]
  21. public void BasicLatinTest()
  22. {
  23. var lineBreaker = new LineBreakEnumerator("Hello World\r\nThis is a test.");
  24. LineBreak lineBreak;
  25. Assert.True(lineBreaker.MoveNext(out lineBreak));
  26. Assert.Equal(6, lineBreak.PositionWrap);
  27. Assert.False(lineBreak.Required);
  28. Assert.True(lineBreaker.MoveNext(out lineBreak));
  29. Assert.Equal(13, lineBreak.PositionWrap);
  30. Assert.True(lineBreak.Required);
  31. Assert.True(lineBreaker.MoveNext(out lineBreak));
  32. Assert.Equal(18, lineBreak.PositionWrap);
  33. Assert.False(lineBreak.Required);
  34. Assert.True(lineBreaker.MoveNext(out lineBreak));
  35. Assert.Equal(21, lineBreak.PositionWrap);
  36. Assert.False(lineBreak.Required);
  37. Assert.True(lineBreaker.MoveNext(out lineBreak));
  38. Assert.Equal(23, lineBreak.PositionWrap);
  39. Assert.False(lineBreak.Required);
  40. Assert.True(lineBreaker.MoveNext(out lineBreak));
  41. Assert.Equal(28, lineBreak.PositionWrap);
  42. Assert.False(lineBreak.Required);
  43. Assert.False(lineBreaker.MoveNext(out lineBreak));
  44. }
  45. [InlineData("Hello\nWorld", 5, 6)]
  46. [InlineData("Hello\rWorld", 5, 6 )]
  47. [InlineData("Hello\r\nWorld", 5 , 7)]
  48. [Theory]
  49. public void ShouldFindMandatoryBreaks(string text, int positionMeasure, int positionWrap)
  50. {
  51. var lineBreaker = new LineBreakEnumerator(text);
  52. var breaks = GetBreaks(lineBreaker);
  53. Assert.Equal(2, breaks.Count);
  54. var firstBreak = breaks[0];
  55. Assert.True(firstBreak.Required);
  56. Assert.Equal(positionMeasure, firstBreak.PositionMeasure);
  57. Assert.Equal(positionWrap, firstBreak.PositionWrap);
  58. }
  59. [Fact]
  60. public void ForwardTextWithOuterWhitespace()
  61. {
  62. var lineBreaker = new LineBreakEnumerator(" Apples Pears Bananas ");
  63. var positionsF = GetBreaks(lineBreaker);
  64. Assert.Equal(1, positionsF[0].PositionWrap);
  65. Assert.Equal(0, positionsF[0].PositionMeasure);
  66. Assert.Equal(8, positionsF[1].PositionWrap);
  67. Assert.Equal(7, positionsF[1].PositionMeasure);
  68. Assert.Equal(14, positionsF[2].PositionWrap);
  69. Assert.Equal(13, positionsF[2].PositionMeasure);
  70. Assert.Equal(24, positionsF[3].PositionWrap);
  71. Assert.Equal(21, positionsF[3].PositionMeasure);
  72. }
  73. [Theory(Skip = "Only runs when the spec changes")]
  74. [ClassData(typeof(LineBreakTestDataGenerator))]
  75. public void ShouldFindBreaks(int lineNumber, int[] codePoints, int[] breakPoints, string rules)
  76. {
  77. var text = string.Join(null, codePoints.Select(char.ConvertFromUtf32));
  78. var lineBreaker = new LineBreakEnumerator(text);
  79. var foundBreaks = new List<int>();
  80. while (lineBreaker.MoveNext(out var lineBreak))
  81. {
  82. foundBreaks.Add(lineBreak.PositionWrap);
  83. }
  84. // Check the same
  85. var pass = true;
  86. if (foundBreaks.Count != breakPoints.Length)
  87. {
  88. pass = false;
  89. }
  90. else
  91. {
  92. for (var i = 0; i < foundBreaks.Count; i++)
  93. {
  94. if (foundBreaks[i] != breakPoints[i])
  95. {
  96. pass = false;
  97. }
  98. }
  99. }
  100. if (!pass)
  101. {
  102. _outputHelper.WriteLine($"Failed test on line {lineNumber}");
  103. _outputHelper.WriteLine("");
  104. _outputHelper.WriteLine($" Code Points: {string.Join(" ", codePoints)}");
  105. _outputHelper.WriteLine($"Expected Breaks: {string.Join(" ", breakPoints)}");
  106. _outputHelper.WriteLine($" Actual Breaks: {string.Join(" ", foundBreaks)}");
  107. _outputHelper.WriteLine($" Text: {text}");
  108. _outputHelper.WriteLine($" Char Props: {string.Join(" ", codePoints.Select(x => new Codepoint((uint)x).LineBreakClass))}");
  109. _outputHelper.WriteLine($" Rules: {rules}");
  110. _outputHelper.WriteLine("");
  111. }
  112. Assert.True(pass);
  113. }
  114. private static List<LineBreak> GetBreaks(LineBreakEnumerator lineBreaker)
  115. {
  116. var breaks = new List<LineBreak>();
  117. while (lineBreaker.MoveNext(out var lineBreak))
  118. {
  119. breaks.Add(lineBreak);
  120. }
  121. return breaks;
  122. }
  123. public class LineBreakTestDataGenerator : IEnumerable<object[]>
  124. {
  125. private readonly List<object[]> _testData;
  126. public LineBreakTestDataGenerator()
  127. {
  128. _testData = GenerateTestData();
  129. }
  130. public IEnumerator<object[]> GetEnumerator()
  131. {
  132. return _testData.GetEnumerator();
  133. }
  134. IEnumerator IEnumerable.GetEnumerator()
  135. {
  136. return GetEnumerator();
  137. }
  138. private static List<object[]> GenerateTestData()
  139. {
  140. // Process each line
  141. var tests = new List<object[]>();
  142. // Read the test file
  143. var url = Path.Combine(UnicodeDataGenerator.Ucd, "auxiliary/LineBreakTest.txt");
  144. using (var client = new HttpClient())
  145. using (var result = client.GetAsync(url).GetAwaiter().GetResult())
  146. {
  147. if (!result.IsSuccessStatusCode)
  148. {
  149. return tests;
  150. }
  151. using (var stream = result.Content.ReadAsStreamAsync().GetAwaiter().GetResult())
  152. using (var reader = new StreamReader(stream))
  153. {
  154. var lineNumber = 1;
  155. while (!reader.EndOfStream)
  156. {
  157. var line = reader.ReadLine();
  158. if (line is null)
  159. {
  160. break;
  161. }
  162. // Get the line, remove comments
  163. var segments = line.Split('#');
  164. // Ignore blank/comment only lines
  165. if (string.IsNullOrWhiteSpace(segments[0]))
  166. {
  167. lineNumber++;
  168. continue;
  169. }
  170. var lineData = ReadLineData(segments[0].Trim());
  171. tests.Add([lineNumber, lineData.Item1, lineData.Item2, segments[1]]);
  172. lineNumber++;
  173. }
  174. }
  175. }
  176. return tests;
  177. }
  178. public static (int[], int[]) ReadLineData(string line)
  179. {
  180. var codePoints = new List<int>();
  181. var breakPoints = new List<int>();
  182. // Parse the test
  183. var p = 0;
  184. while (p < line.Length)
  185. {
  186. // Ignore white space
  187. if (char.IsWhiteSpace(line[p]))
  188. {
  189. p++;
  190. continue;
  191. }
  192. if (line[p] == '×')
  193. {
  194. p++;
  195. continue;
  196. }
  197. if (line[p] == '÷')
  198. {
  199. breakPoints.Add(codePoints.Select(x => x > ushort.MaxValue ? 2 : 1).Sum());
  200. p++;
  201. continue;
  202. }
  203. var codePointPos = p;
  204. while (p < line.Length && IsHexDigit(line[p]))
  205. {
  206. p++;
  207. }
  208. var codePointStr = line.Substring(codePointPos, p - codePointPos);
  209. var codePoint = Convert.ToInt32(codePointStr, 16);
  210. codePoints.Add(codePoint);
  211. }
  212. return (codePoints.ToArray(), breakPoints.ToArray());
  213. }
  214. private static bool IsHexDigit(char ch)
  215. {
  216. return char.IsDigit(ch) || (ch >= 'A' && ch <= 'F') || (ch >= 'a' && ch <= 'f');
  217. }
  218. }
  219. }
  220. }