DriverAssert.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  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="IDriver"/> 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 IDriver 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. IDriver? 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. $"{driver.ToString ()}\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 ($"{driver.ToString ()}");
  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 IDriver 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. IDriver? driver = null,
  129. bool ignoreLeadingWhitespace = false
  130. )
  131. {
  132. #pragma warning restore xUnit1013 // Public method should be marked as test
  133. driver ??= Application.Driver!;
  134. var actualLook = driver.ToString ();
  135. if (string.Equals (expectedLook, actualLook))
  136. {
  137. return;
  138. }
  139. // get rid of trailing whitespace on each line (and leading/trailing whitespace of start/end of full string)
  140. expectedLook = TrailingWhiteSpaceRegEx ().Replace (expectedLook, "").Trim ();
  141. actualLook = TrailingWhiteSpaceRegEx ().Replace (actualLook, "").Trim ();
  142. if (ignoreLeadingWhitespace)
  143. {
  144. expectedLook = LeadingWhitespaceRegEx ().Replace (expectedLook, "").Trim ();
  145. actualLook = LeadingWhitespaceRegEx ().Replace (actualLook, "").Trim ();
  146. }
  147. // standardize line endings for the comparison
  148. expectedLook = expectedLook.Replace ("\r\n", "\n");
  149. actualLook = actualLook.Replace ("\r\n", "\n");
  150. // If test is about to fail show user what things looked like
  151. if (!string.Equals (expectedLook, actualLook))
  152. {
  153. output?.WriteLine ("Expected:" + Environment.NewLine + expectedLook);
  154. output?.WriteLine (" But Was:" + Environment.NewLine + actualLook);
  155. }
  156. Assert.Equal (expectedLook, actualLook);
  157. }
  158. /// <summary>
  159. /// Asserts that the driver contents are equal to the provided string.
  160. /// </summary>
  161. /// <param name="expectedLook"></param>
  162. /// <param name="output"></param>
  163. /// <param name="driver">The IDriver to use. If null <see cref="Application.Driver"/> will be used.</param>
  164. /// <returns></returns>
  165. public static Rectangle AssertDriverContentsWithFrameAre (
  166. string expectedLook,
  167. ITestOutputHelper output,
  168. IDriver? driver = null
  169. )
  170. {
  171. List<List<string>> lines = [];
  172. var sb = new StringBuilder ();
  173. driver ??= Application.Driver!;
  174. int x = -1;
  175. int y = -1;
  176. int w = -1;
  177. int h = -1;
  178. Cell [,] contents = driver!.Contents!;
  179. for (var rowIndex = 0; rowIndex < driver.Rows; rowIndex++)
  180. {
  181. List<string> strings = [];
  182. for (var colIndex = 0; colIndex < driver.Cols; colIndex++)
  183. {
  184. string textAtCurrentLocation = contents! [rowIndex, colIndex].Grapheme;
  185. if (textAtCurrentLocation != _spaceRune.ToString ())
  186. {
  187. if (x == -1)
  188. {
  189. x = colIndex;
  190. y = rowIndex;
  191. for (var i = 0; i < colIndex; i++)
  192. {
  193. strings.InsertRange (i, [_spaceRune.ToString ()]);
  194. }
  195. }
  196. if (textAtCurrentLocation.GetColumns () > 1)
  197. {
  198. colIndex++;
  199. }
  200. if (colIndex + 1 > w)
  201. {
  202. w = colIndex + 1;
  203. }
  204. h = rowIndex - y + 1;
  205. }
  206. if (x > -1)
  207. {
  208. strings.Add (textAtCurrentLocation);
  209. }
  210. }
  211. if (strings.Count > 0)
  212. {
  213. lines.Add (strings);
  214. }
  215. }
  216. // Remove unnecessary empty lines
  217. if (lines.Count > 0)
  218. {
  219. for (int r = lines.Count - 1; r > h - 1; r--)
  220. {
  221. lines.RemoveAt (r);
  222. }
  223. }
  224. // Remove trailing whitespace on each line
  225. foreach (List<string> row in lines)
  226. {
  227. for (int c = row.Count - 1; c >= 0; c--)
  228. {
  229. string text = row [c];
  230. if (text != " " || row.Sum (x => x.GetColumns ()) == w)
  231. {
  232. break;
  233. }
  234. row.RemoveAt (c);
  235. }
  236. }
  237. // Convert Text list to string
  238. for (var r = 0; r < lines.Count; r++)
  239. {
  240. var line = StringExtensions.ToString (lines [r]);
  241. if (r == lines.Count - 1)
  242. {
  243. sb.Append (line);
  244. }
  245. else
  246. {
  247. sb.AppendLine (line);
  248. }
  249. }
  250. var actualLook = sb.ToString ();
  251. if (string.Equals (expectedLook, actualLook))
  252. {
  253. return new (x > -1 ? x : 0, y > -1 ? y : 0, w > -1 ? w : 0, h > -1 ? h : 0);
  254. }
  255. // standardize line endings for the comparison
  256. expectedLook = expectedLook.ReplaceLineEndings ();
  257. actualLook = actualLook.ReplaceLineEndings ();
  258. // Remove the first and the last line ending from the expectedLook
  259. if (expectedLook.StartsWith (Environment.NewLine))
  260. {
  261. expectedLook = expectedLook [Environment.NewLine.Length..];
  262. }
  263. if (expectedLook.EndsWith (Environment.NewLine))
  264. {
  265. expectedLook = expectedLook [..^Environment.NewLine.Length];
  266. }
  267. // If test is about to fail show user what things looked like
  268. if (!string.Equals (expectedLook, actualLook))
  269. {
  270. output?.WriteLine ("Expected:" + Environment.NewLine + expectedLook);
  271. output?.WriteLine (" But Was:" + Environment.NewLine + actualLook);
  272. }
  273. Assert.Equal (expectedLook, actualLook);
  274. return new (x > -1 ? x : 0, y > -1 ? y : 0, w > -1 ? w : 0, h > -1 ? h : 0);
  275. }
  276. /// <summary>
  277. /// Verifies the console used all the <paramref name="expectedColors"/> when rendering. If one or more of the
  278. /// expected colors are not used then the failure will output both the colors that were found to be used and which of
  279. /// your expectations was not met.
  280. /// </summary>
  281. /// <param name="driver">if null uses <see cref="Application.Driver"/></param>
  282. /// <param name="expectedColors"></param>
  283. internal static void AssertDriverUsedColors (IDriver? driver = null, params Attribute [] expectedColors)
  284. {
  285. driver ??= Application.Driver;
  286. Cell [,] contents = driver?.Contents!;
  287. List<Attribute> toFind = expectedColors.ToList ();
  288. // Contents 3rd column is an Attribute
  289. HashSet<Attribute> colorsUsed = new ();
  290. for (var r = 0; r < driver!.Rows; r++)
  291. {
  292. for (var c = 0; c < driver.Cols; c++)
  293. {
  294. Attribute? val = contents [r, c].Attribute;
  295. if (val.HasValue)
  296. {
  297. colorsUsed.Add (val.Value);
  298. Attribute match = toFind.FirstOrDefault (e => e == val);
  299. // need to check twice because Attribute is a struct and therefore cannot be null
  300. if (toFind.Any (e => e == val))
  301. {
  302. toFind.Remove (match);
  303. }
  304. }
  305. }
  306. }
  307. if (!toFind.Any ())
  308. {
  309. return;
  310. }
  311. var sb = new StringBuilder ();
  312. sb.AppendLine ("The following colors were not used:" + string.Join ("; ", toFind.Select (a => a.ToString ())));
  313. sb.AppendLine ("Colors used were:" + string.Join ("; ", colorsUsed.Select (a => a.ToString ())));
  314. throw new (sb.ToString ());
  315. }
  316. [GeneratedRegex ("^\\s+", RegexOptions.Multiline)]
  317. private static partial Regex LeadingWhitespaceRegEx ();
  318. [GeneratedRegex ("\\s+$", RegexOptions.Multiline)]
  319. private static partial Regex TrailingWhiteSpaceRegEx ();
  320. }