forked from gui-cs/Terminal.Gui
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathTestHelpers.cs
More file actions
277 lines (235 loc) · 9.7 KB
/
TestHelpers.cs
File metadata and controls
277 lines (235 loc) · 9.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit.Abstractions;
using Xunit;
using Terminal.Gui;
using Rune = System.Rune;
using Attribute = Terminal.Gui.Attribute;
using System.Text.RegularExpressions;
using System.Reflection;
using System.Diagnostics;
// This class enables test functions annotated with the [AutoInitShutdown] attribute to
// automatically call Application.Init at start of the test and Application.Shutdown after the
// test exits.
//
// This is necessary because a) Application is a singleton and Init/Shutdown must be called
// as a pair, and b) all unit test functions should be atomic..
[AttributeUsage (AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class AutoInitShutdownAttribute : Xunit.Sdk.BeforeAfterTestAttribute {
/// <summary>
/// Initializes a [AutoInitShutdown] attribute, which determines if/how Application.Init and
/// Application.Shutdown are automatically called Before/After a test runs.
/// </summary>
/// <param name="autoInit">If true, Application.Init will be called Before the test runs.</param>
/// <param name="autoShutdown">If true, Application.Shutdown will be called After the test runs.</param>
/// <param name="consoleDriverType">Determins which ConsoleDriver (FakeDriver, WindowsDriver,
/// CursesDriver, NetDriver) will be used when Appliation.Init is called. If null FakeDriver will be used.
/// Only valid if <paramref name="autoInit"/> is true.</param>
/// <param name="useFakeClipboard">If true, will force the use of <see cref="FakeDriver.FakeClipboard"/>.
/// Only valid if <see cref="consoleDriver"/> == <see cref="FakeDriver"/> and <paramref name="autoInit"/> is true.</param>
/// <param name="fakeClipboardAlwaysThrowsNotSupportedException">Only valid if <paramref name="autoInit"/> is true.
/// Only valid if <see cref="consoleDriver"/> == <see cref="FakeDriver"/> and <paramref name="autoInit"/> is true.</param>
/// <param name="fakeClipboardIsSupportedAlwaysTrue">Only valid if <paramref name="autoInit"/> is true.
/// Only valid if <see cref="consoleDriver"/> == <see cref="FakeDriver"/> and <paramref name="autoInit"/> is true.</param>
public AutoInitShutdownAttribute (bool autoInit = true, bool autoShutdown = true,
Type consoleDriverType = null,
bool useFakeClipboard = false,
bool fakeClipboardAlwaysThrowsNotSupportedException = false,
bool fakeClipboardIsSupportedAlwaysTrue = false)
{
//Assert.True (autoInit == false && consoleDriverType == null);
AutoInit = autoInit;
AutoShutdown = autoShutdown;
DriverType = consoleDriverType ?? typeof (FakeDriver);
FakeDriver.FakeBehaviors.UseFakeClipboard = useFakeClipboard;
FakeDriver.FakeBehaviors.FakeClipboardAlwaysThrowsNotSupportedException = fakeClipboardAlwaysThrowsNotSupportedException;
FakeDriver.FakeBehaviors.FakeClipboardIsSupportedAlwaysFalse = fakeClipboardIsSupportedAlwaysTrue;
}
static bool _init = false;
bool AutoInit { get; }
bool AutoShutdown { get; }
Type DriverType;
public override void Before (MethodInfo methodUnderTest)
{
Debug.WriteLine ($"Before: {methodUnderTest.Name}");
if (AutoShutdown && _init) {
throw new InvalidOperationException ("After did not run when AutoShutdown was specified.");
}
if (AutoInit) {
Application.Init ((ConsoleDriver)Activator.CreateInstance (DriverType));
_init = true;
}
}
public override void After (MethodInfo methodUnderTest)
{
Debug.WriteLine ($"After: {methodUnderTest.Name}");
if (AutoShutdown) {
Application.Shutdown ();
_init = false;
}
}
}
class TestHelpers {
#pragma warning disable xUnit1013 // Public method should be marked as test
public static void AssertDriverContentsAre (string expectedLook, ITestOutputHelper output)
{
#pragma warning restore xUnit1013 // Public method should be marked as test
var sb = new StringBuilder ();
var driver = ((FakeDriver)Application.Driver);
var contents = driver.Contents;
for (int r = 0; r < driver.Rows; r++) {
for (int c = 0; c < driver.Cols; c++) {
Rune rune = contents [r, c, 0];
if (Rune.DecodeSurrogatePair (rune, out char [] spair)) {
sb.Append (spair);
} else {
sb.Append ((char)rune);
}
if (Rune.ColumnWidth (rune) > 1) {
c++;
}
}
sb.AppendLine ();
}
var actualLook = sb.ToString ();
if (!string.Equals (expectedLook, actualLook)) {
// ignore trailing whitespace on each line
var trailingWhitespace = new Regex (@"\s+$", RegexOptions.Multiline);
// get rid of trailing whitespace on each line (and leading/trailing whitespace of start/end of full string)
expectedLook = trailingWhitespace.Replace (expectedLook, "").Trim ();
actualLook = trailingWhitespace.Replace (actualLook, "").Trim ();
// standardize line endings for the comparison
expectedLook = expectedLook.Replace ("\r\n", "\n");
actualLook = actualLook.Replace ("\r\n", "\n");
output?.WriteLine ("Expected:" + Environment.NewLine + expectedLook);
output?.WriteLine ("But Was:" + Environment.NewLine + actualLook);
Assert.Equal (expectedLook, actualLook);
}
}
public static Rect AssertDriverContentsWithFrameAre (string expectedLook, ITestOutputHelper output)
{
var lines = new List<List<Rune>> ();
var sb = new StringBuilder ();
var driver = ((FakeDriver)Application.Driver);
var x = -1;
var y = -1;
int w = -1;
int h = -1;
var contents = driver.Contents;
for (int r = 0; r < driver.Rows; r++) {
var runes = new List<Rune> ();
for (int c = 0; c < driver.Cols; c++) {
var rune = (Rune)contents [r, c, 0];
if (rune != ' ') {
if (x == -1) {
x = c;
y = r;
for (int i = 0; i < c; i++) {
runes.InsertRange (i, new List<Rune> () { ' ' });
}
}
if (Rune.ColumnWidth (rune) > 1) {
c++;
}
if (c + 1 > w) {
w = c + 1;
}
h = r - y + 1;
}
if (x > -1) {
runes.Add (rune);
}
}
if (runes.Count > 0) {
lines.Add (runes);
}
}
// Remove unnecessary empty lines
if (lines.Count > 0) {
for (int r = lines.Count - 1; r > h - 1; r--) {
lines.RemoveAt (r);
}
}
// Remove trailing whitespace on each line
for (int r = 0; r < lines.Count; r++) {
List<Rune> row = lines [r];
for (int c = row.Count - 1; c >= 0; c--) {
var rune = row [c];
if (rune != ' ' || (row.Sum (x => Rune.ColumnWidth (x)) == w)) {
break;
}
row.RemoveAt (c);
}
}
// Convert Rune list to string
for (int r = 0; r < lines.Count; r++) {
var line = NStack.ustring.Make (lines [r]).ToString ();
if (r == lines.Count - 1) {
sb.Append (line);
} else {
sb.AppendLine (line);
}
}
var actualLook = sb.ToString ();
if (!string.Equals (expectedLook, actualLook)) {
// standardize line endings for the comparison
expectedLook = expectedLook.Replace ("\r\n", "\n");
actualLook = actualLook.Replace ("\r\n", "\n");
// Remove the first and the last line ending from the expectedLook
if (expectedLook.StartsWith ("\n")) {
expectedLook = expectedLook [1..];
}
if (expectedLook.EndsWith ("\n")) {
expectedLook = expectedLook [..^1];
}
output?.WriteLine ("Expected:" + Environment.NewLine + expectedLook);
output?.WriteLine ("But Was:" + Environment.NewLine + actualLook);
Assert.Equal (expectedLook, actualLook);
}
return new Rect (x > -1 ? x : 0, y > -1 ? y : 0, w > -1 ? w : 0, h > -1 ? h : 0);
}
#pragma warning disable xUnit1013 // Public method should be marked as test
/// <summary>
/// Verifies the console was rendered using the given <paramref name="expectedColors"/> at the given locations.
/// Pass a bitmap of indexes into <paramref name="expectedColors"/> as <paramref name="expectedLook"/> and the
/// test method will verify those colors were used in the row/col of the console during rendering
/// </summary>
/// <param name="expectedLook">Numbers between 0 and 9 for each row/col of the console. Must be valid indexes of <paramref name="expectedColors"/></param>
/// <param name="expectedColors"></param>
public static void AssertDriverColorsAre (string expectedLook, Attribute [] expectedColors)
{
#pragma warning restore xUnit1013 // Public method should be marked as test
if (expectedColors.Length > 10) {
throw new ArgumentException ("This method only works for UIs that use at most 10 colors");
}
expectedLook = expectedLook.Trim ();
var driver = ((FakeDriver)Application.Driver);
var contents = driver.Contents;
int r = 0;
foreach (var line in expectedLook.Split ('\n').Select (l => l.Trim ())) {
for (int c = 0; c < line.Length; c++) {
int val = contents [r, c, 1];
var match = expectedColors.Where (e => e.Value == val).ToList ();
if (match.Count == 0) {
throw new Exception ($"Unexpected color {DescribeColor (val)} was used at row {r} and col {c} (indexes start at 0). Color value was {val} (expected colors were {string.Join (",", expectedColors.Select (c => DescribeColor (c.Value)))})");
} else if (match.Count > 1) {
throw new ArgumentException ($"Bad value for expectedColors, {match.Count} Attributes had the same Value");
}
var colorUsed = Array.IndexOf (expectedColors, match [0]).ToString () [0];
var userExpected = line [c];
if (colorUsed != userExpected) {
throw new Exception ($"Colors used did not match expected at row {r} and col {c} (indexes start at 0). Color index used was {colorUsed} ({DescribeColor (val)}) but test expected {userExpected} ({DescribeColor (expectedColors [int.Parse (userExpected.ToString ())].Value)}) (these are indexes into the expectedColors array)");
}
}
r++;
}
}
private static object DescribeColor (int userExpected)
{
var a = new Attribute (userExpected);
return $"{a.Foreground},{a.Background}";
}
}