DriverAssert.cs 14 KB

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