DriverAssert.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. #nullable enable
  2. using System.Text;
  3. using System.Text.RegularExpressions;
  4. using Xunit.Abstractions;
  5. namespace UnitTests;
  6. /// <summary>
  7. /// Provides xUnit-style assertions for <see cref="IConsoleDriver"/> contents.
  8. /// </summary>
  9. internal partial class DriverAssert
  10. {
  11. private const char SPACE_CHAR = ' ';
  12. private static readonly Rune _spaceRune = (Rune)SPACE_CHAR;
  13. #pragma warning disable xUnit1013 // Public method should be marked as test
  14. /// <summary>
  15. /// Verifies <paramref name="expectedAttributes"/> are found at the locations specified by
  16. /// <paramref name="expectedLook"/>. <paramref name="expectedLook"/> is a bitmap of indexes into
  17. /// <paramref name="expectedAttributes"/> (e.g. "00110" means the attribute at <c>expectedAttributes[1]</c> is expected
  18. /// at the 3rd and 4th columns of the 1st row of driver.Contents).
  19. /// </summary>
  20. /// <param name="expectedLook">
  21. /// Numbers between 0 and 9 for each row/col of the console. Must be valid indexes into
  22. /// <paramref name="expectedAttributes"/>.
  23. /// </param>
  24. /// <param name="output"></param>
  25. /// <param name="driver">The IConsoleDriver to use. If null <see cref="Application.Driver"/> will be used.</param>
  26. /// <param name="expectedAttributes"></param>
  27. public static void AssertDriverAttributesAre (
  28. string expectedLook,
  29. ITestOutputHelper output,
  30. IConsoleDriver? driver = null,
  31. params Attribute [] expectedAttributes
  32. )
  33. {
  34. #pragma warning restore xUnit1013 // Public method should be marked as test
  35. if (expectedAttributes.Length > 10)
  36. {
  37. throw new ArgumentException ("This method only works for UIs that use at most 10 colors");
  38. }
  39. expectedLook = expectedLook.Trim ();
  40. driver ??= Application.Driver;
  41. Cell [,] contents = driver!.Contents!;
  42. var line = 0;
  43. foreach (string lineString in expectedLook.Split ('\n').Select (l => l.Trim ()))
  44. {
  45. for (var c = 0; c < lineString.Length; c++)
  46. {
  47. Attribute? val = contents! [line, c].Attribute;
  48. List<Attribute> match = expectedAttributes.Where (e => e == val).ToList ();
  49. switch (match.Count)
  50. {
  51. case 0:
  52. output.WriteLine (
  53. $"{Application.ToString (driver)}\n"
  54. + $"Expected Attribute {val} at Contents[{line},{c}] {contents [line, c]} was not found.\n"
  55. + $" Expected: {string.Join (",", expectedAttributes.Select (attr => attr))}\n"
  56. + $" But Was: <not found>"
  57. );
  58. Assert.Empty (match);
  59. return;
  60. case > 1:
  61. throw new ArgumentException (
  62. $"Bad value for expectedColors, {match.Count} Attributes had the same Value"
  63. );
  64. }
  65. char colorUsed = Array.IndexOf (expectedAttributes, match [0]).ToString () [0];
  66. char userExpected = lineString [c];
  67. if (colorUsed != userExpected)
  68. {
  69. output.WriteLine ($"{Application.ToString (driver)}");
  70. output.WriteLine ($"Unexpected Attribute at Contents[{line},{c}] = {contents [line, c]}.");
  71. output.WriteLine ($" Expected: {userExpected} ({expectedAttributes [int.Parse (userExpected.ToString ())]})");
  72. output.WriteLine ($" But Was: {colorUsed} ({val})");
  73. // Print `contents` as the expected and actual attribute indexes in a grid where each cell is of the form "e:a" (e = expected, a = actual)
  74. // e.g:
  75. // 0:1 0:0 1:1
  76. // 0:0 1:1 0:0
  77. // 0:0 1:1 0:0
  78. //// Use StringBuilder since output only has .WriteLine
  79. //var sb = new StringBuilder ();
  80. //// for each line in `contents`
  81. //for (var r = 0; r < driver.Rows; r++)
  82. //{
  83. // // for each column in `contents`
  84. // for (var cc = 0; cc < driver.Cols; cc++)
  85. // {
  86. // // get the attribute at the current location
  87. // Attribute? val2 = contents [r, cc].Attribute;
  88. // // if the attribute is not null
  89. // if (val2.HasValue)
  90. // {
  91. // // get the index of the attribute in `expectedAttributes`
  92. // int index = Array.IndexOf (expectedAttributes, val2.Value);
  93. // // if the index is -1, it means the attribute was not found in `expectedAttributes`
  94. // // get the index of the actual attribute in `expectedAttributes`
  95. // if (index == -1)
  96. // {
  97. // sb.Append ("x:x ");
  98. // }
  99. // else
  100. // {
  101. // sb.Append ($"{index}:{val2.Value} ");
  102. // }
  103. // }
  104. // else
  105. // {
  106. // sb.Append ("x:x ");
  107. // }
  108. // }
  109. // sb.AppendLine ();
  110. //}
  111. //output.WriteLine ($"Contents:\n{sb}");
  112. Assert.Equal (userExpected, colorUsed);
  113. return;
  114. }
  115. }
  116. line++;
  117. }
  118. }
  119. #pragma warning disable xUnit1013 // Public method should be marked as test
  120. /// <summary>Asserts that the driver contents match the expected contents, optionally ignoring any trailing whitespace.</summary>
  121. /// <param name="expectedLook"></param>
  122. /// <param name="output"></param>
  123. /// <param name="driver">The IConsoleDriver to use. If null <see cref="Application.Driver"/> will be used.</param>
  124. /// <param name="ignoreLeadingWhitespace"></param>
  125. public static void AssertDriverContentsAre (
  126. string expectedLook,
  127. ITestOutputHelper output,
  128. IConsoleDriver? driver = null,
  129. bool ignoreLeadingWhitespace = false
  130. )
  131. {
  132. #pragma warning restore xUnit1013 // Public method should be marked as test
  133. var actualLook = Application.ToString (driver ?? Application.Driver);
  134. if (string.Equals (expectedLook, actualLook))
  135. {
  136. return;
  137. }
  138. // get rid of trailing whitespace on each line (and leading/trailing whitespace of start/end of full string)
  139. expectedLook = TrailingWhiteSpaceRegEx ().Replace (expectedLook, "").Trim ();
  140. actualLook = TrailingWhiteSpaceRegEx ().Replace (actualLook, "").Trim ();
  141. if (ignoreLeadingWhitespace)
  142. {
  143. expectedLook = LeadingWhitespaceRegEx ().Replace (expectedLook, "").Trim ();
  144. actualLook = LeadingWhitespaceRegEx ().Replace (actualLook, "").Trim ();
  145. }
  146. // standardize line endings for the comparison
  147. expectedLook = expectedLook.Replace ("\r\n", "\n");
  148. actualLook = actualLook.Replace ("\r\n", "\n");
  149. // If test is about to fail show user what things looked like
  150. if (!string.Equals (expectedLook, actualLook))
  151. {
  152. output?.WriteLine ("Expected:" + Environment.NewLine + expectedLook);
  153. output?.WriteLine (" But Was:" + Environment.NewLine + actualLook);
  154. }
  155. Assert.Equal (expectedLook, actualLook);
  156. }
  157. /// <summary>
  158. /// Asserts that the driver contents are equal to the provided string.
  159. /// </summary>
  160. /// <param name="expectedLook"></param>
  161. /// <param name="output"></param>
  162. /// <param name="driver">The IConsoleDriver to use. If null <see cref="Application.Driver"/> will be used.</param>
  163. /// <returns></returns>
  164. public static Rectangle AssertDriverContentsWithFrameAre (
  165. string expectedLook,
  166. ITestOutputHelper output,
  167. IConsoleDriver? driver = null
  168. )
  169. {
  170. List<List<Rune>> lines = [];
  171. var sb = new StringBuilder ();
  172. driver ??= Application.Driver;
  173. int x = -1;
  174. int y = -1;
  175. int w = -1;
  176. int h = -1;
  177. Cell [,] contents = driver!.Contents!;
  178. for (var rowIndex = 0; rowIndex < driver.Rows; rowIndex++)
  179. {
  180. List<Rune> runes = [];
  181. for (var colIndex = 0; colIndex < driver.Cols; colIndex++)
  182. {
  183. Rune runeAtCurrentLocation = contents! [rowIndex, colIndex].Rune;
  184. if (runeAtCurrentLocation != _spaceRune)
  185. {
  186. if (x == -1)
  187. {
  188. x = colIndex;
  189. y = rowIndex;
  190. for (var i = 0; i < colIndex; i++)
  191. {
  192. runes.InsertRange (i, [_spaceRune]);
  193. }
  194. }
  195. if (runeAtCurrentLocation.GetColumns () > 1)
  196. {
  197. colIndex++;
  198. }
  199. if (colIndex + 1 > w)
  200. {
  201. w = colIndex + 1;
  202. }
  203. h = rowIndex - y + 1;
  204. }
  205. if (x > -1)
  206. {
  207. runes.Add (runeAtCurrentLocation);
  208. }
  209. // See Issue #2616
  210. //foreach (var combMark in contents [r, c].CombiningMarks) {
  211. // runes.Add (combMark);
  212. //}
  213. }
  214. if (runes.Count > 0)
  215. {
  216. lines.Add (runes);
  217. }
  218. }
  219. // Remove unnecessary empty lines
  220. if (lines.Count > 0)
  221. {
  222. for (int r = lines.Count - 1; r > h - 1; r--)
  223. {
  224. lines.RemoveAt (r);
  225. }
  226. }
  227. // Remove trailing whitespace on each line
  228. foreach (List<Rune> row in lines)
  229. {
  230. for (int c = row.Count - 1; c >= 0; c--)
  231. {
  232. Rune rune = row [c];
  233. if (rune != (Rune)' ' || row.Sum (x => x.GetColumns ()) == w)
  234. {
  235. break;
  236. }
  237. row.RemoveAt (c);
  238. }
  239. }
  240. // Convert Rune list to string
  241. for (var r = 0; r < lines.Count; r++)
  242. {
  243. var line = StringExtensions.ToString (lines [r]);
  244. if (r == lines.Count - 1)
  245. {
  246. sb.Append (line);
  247. }
  248. else
  249. {
  250. sb.AppendLine (line);
  251. }
  252. }
  253. var actualLook = sb.ToString ();
  254. if (string.Equals (expectedLook, actualLook))
  255. {
  256. return new (x > -1 ? x : 0, y > -1 ? y : 0, w > -1 ? w : 0, h > -1 ? h : 0);
  257. }
  258. // standardize line endings for the comparison
  259. expectedLook = expectedLook.ReplaceLineEndings ();
  260. actualLook = actualLook.ReplaceLineEndings ();
  261. // Remove the first and the last line ending from the expectedLook
  262. if (expectedLook.StartsWith (Environment.NewLine))
  263. {
  264. expectedLook = expectedLook [Environment.NewLine.Length..];
  265. }
  266. if (expectedLook.EndsWith (Environment.NewLine))
  267. {
  268. expectedLook = expectedLook [..^Environment.NewLine.Length];
  269. }
  270. // If test is about to fail show user what things looked like
  271. if (!string.Equals (expectedLook, actualLook))
  272. {
  273. output?.WriteLine ("Expected:" + Environment.NewLine + expectedLook);
  274. output?.WriteLine (" But Was:" + Environment.NewLine + actualLook);
  275. }
  276. Assert.Equal (expectedLook, actualLook);
  277. return new (x > -1 ? x : 0, y > -1 ? y : 0, w > -1 ? w : 0, h > -1 ? h : 0);
  278. }
  279. /// <summary>
  280. /// Verifies the console used all the <paramref name="expectedColors"/> when rendering. If one or more of the
  281. /// expected colors are not used then the failure will output both the colors that were found to be used and which of
  282. /// your expectations was not met.
  283. /// </summary>
  284. /// <param name="driver">if null uses <see cref="Application.Driver"/></param>
  285. /// <param name="expectedColors"></param>
  286. internal static void AssertDriverUsedColors (IConsoleDriver? driver = null, params Attribute [] expectedColors)
  287. {
  288. driver ??= Application.Driver;
  289. Cell [,] contents = driver?.Contents!;
  290. List<Attribute> toFind = expectedColors.ToList ();
  291. // Contents 3rd column is an Attribute
  292. HashSet<Attribute> colorsUsed = new ();
  293. for (var r = 0; r < driver!.Rows; r++)
  294. {
  295. for (var c = 0; c < driver.Cols; c++)
  296. {
  297. Attribute? val = contents [r, c].Attribute;
  298. if (val.HasValue)
  299. {
  300. colorsUsed.Add (val.Value);
  301. Attribute match = toFind.FirstOrDefault (e => e == val);
  302. // need to check twice because Attribute is a struct and therefore cannot be null
  303. if (toFind.Any (e => e == val))
  304. {
  305. toFind.Remove (match);
  306. }
  307. }
  308. }
  309. }
  310. if (!toFind.Any ())
  311. {
  312. return;
  313. }
  314. var sb = new StringBuilder ();
  315. sb.AppendLine ("The following colors were not used:" + string.Join ("; ", toFind.Select (a => a.ToString ())));
  316. sb.AppendLine ("Colors used were:" + string.Join ("; ", colorsUsed.Select (a => a.ToString ())));
  317. throw new (sb.ToString ());
  318. }
  319. [GeneratedRegex ("^\\s+", RegexOptions.Multiline)]
  320. private static partial Regex LeadingWhitespaceRegEx ();
  321. [GeneratedRegex ("\\s+$", RegexOptions.Multiline)]
  322. private static partial Regex TrailingWhiteSpaceRegEx ();
  323. }