1 // Licensed to the .NET Foundation under one or more agreements. 2 // The .NET Foundation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System; 6 using System.Collections.Generic; 7 using System.Diagnostics; 8 using System.IO.Ports; 9 using System.Linq; 10 using System.Text; 11 using System.Threading; 12 using System.Threading.Tasks; 13 using Xunit; 14 15 namespace Legacy.Support 16 { 17 public static class TCSupport 18 { 19 public enum SerialPortRequirements { None, OneSerialPort, TwoSerialPorts, NullModem, Loopback, LoopbackOrNullModem }; 20 21 private static LocalMachineSerialInfo s_localMachineSerialInfo; 22 private static SerialPortRequirements s_localMachineSerialPortRequirements; 23 24 // Set this true to display port info to the console rather than Debug.WriteLine 25 private static bool s_displayPortInfoOnConsole = true; 26 TCSupport()27 static TCSupport() 28 { 29 Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); 30 InitializeSerialInfo(); 31 } 32 InitializeSerialInfo()33 private static void InitializeSerialInfo() 34 { 35 GenerateSerialInfo(); 36 37 if (s_localMachineSerialInfo.LoopbackPortName != null) 38 s_localMachineSerialPortRequirements = SerialPortRequirements.Loopback; 39 else if (s_localMachineSerialInfo.NullModemPresent) 40 s_localMachineSerialPortRequirements = SerialPortRequirements.NullModem; 41 else if (!string.IsNullOrEmpty(s_localMachineSerialInfo.SecondAvailablePortName)) 42 s_localMachineSerialPortRequirements = SerialPortRequirements.TwoSerialPorts; 43 else if (!string.IsNullOrEmpty(s_localMachineSerialInfo.FirstAvailablePortName)) 44 s_localMachineSerialPortRequirements = SerialPortRequirements.OneSerialPort; 45 else 46 s_localMachineSerialPortRequirements = SerialPortRequirements.None; 47 } 48 GenerateSerialInfo()49 private static void GenerateSerialInfo() 50 { 51 string[] installedPortNames = PortHelper.GetPorts(); 52 bool nullModemPresent = false; 53 string portName1 = null, portName2 = null, loopbackPortName = null; 54 55 Array.Sort(installedPortNames); 56 PrintInfo("Installed ports : " + string.Join(",", installedPortNames)); 57 58 IList<string> openablePortNames = CheckPortsCanBeOpened(installedPortNames); 59 60 PrintInfo("Openable ports : " + string.Join(",", openablePortNames)); 61 62 // Find any pair of ports which are null-modem connected 63 // If there is a pair like this, then they take precedence over any other way of identifying two available ports 64 for (int firstIndex = 0; firstIndex < openablePortNames.Count && !nullModemPresent; firstIndex++) 65 { 66 for (int secondIndex = firstIndex + 1; secondIndex < openablePortNames.Count && !nullModemPresent; secondIndex++) 67 { 68 string firstPortName = openablePortNames[firstIndex]; 69 string secondPortName = openablePortNames[secondIndex]; 70 71 if (SerialPortConnection.VerifyConnection(firstPortName, secondPortName)) 72 { 73 // We have a null modem port 74 portName1 = firstPortName; 75 portName2 = secondPortName; 76 nullModemPresent = true; 77 78 PrintInfo("Null-modem connection from {0} to {1}", firstPortName, secondPortName); 79 } 80 } 81 } 82 83 if (!nullModemPresent) 84 { 85 // If we don't have a null-modem connection - check for a loopback connection 86 foreach (string portName in openablePortNames) 87 { 88 if (SerialPortConnection.VerifyLoopback(portName)) 89 { 90 portName1 = loopbackPortName = portName; 91 break; 92 } 93 } 94 95 if (portName1 == null) 96 { 97 portName1 = openablePortNames.FirstOrDefault(); 98 } 99 100 portName2 = openablePortNames.FirstOrDefault(name => name != portName1); 101 } 102 103 // See Github issues #15961, #16033, #20764 - hardware tests are currently insufficiently stable on master CI 104 if (loopbackPortName == null && !nullModemPresent) 105 { 106 // We don't have any supporting hardware - disable all the tests which would use just an open port 107 PrintInfo("No support hardware - not using serial ports"); 108 portName1 = portName2 = null; 109 } 110 111 PrintInfo("First available port name : " + portName1); 112 PrintInfo("Second available port name : " + portName2); 113 PrintInfo("Loopback port name : " + loopbackPortName); 114 PrintInfo("NullModem present : " + nullModemPresent); 115 116 s_localMachineSerialInfo = new LocalMachineSerialInfo(portName1, portName2, loopbackPortName, nullModemPresent); 117 118 if (portName1 != null) 119 { 120 // Measure how big a packet we need to write to be sure to see blocking behaviour at a port 121 s_flowControlCapabilities = SerialPortConnection.MeasureFlowControlCapabilities(portName1); 122 123 PrintInfo("{0}: Flow capabilities {1}", portName1, s_flowControlCapabilities); 124 } 125 } 126 PrintInfo(string format, params object[] args)127 private static void PrintInfo(string format, params object[] args) 128 { 129 if (s_displayPortInfoOnConsole) 130 { 131 Console.WriteLine(format, args); 132 } 133 else 134 { 135 Debug.WriteLine(format, args); 136 } 137 } 138 CheckPortsCanBeOpened(IEnumerable<string> installedPortNames)139 private static IList<string> CheckPortsCanBeOpened(IEnumerable<string> installedPortNames) 140 { 141 List<string> openablePortNames = new List<string>(); 142 foreach (string portName in installedPortNames) 143 { 144 using (SerialPort com = new SerialPort(portName)) 145 { 146 try 147 { 148 com.Open(); 149 com.Close(); 150 151 openablePortNames.Add(portName); 152 } 153 catch (Exception e) 154 { 155 PrintInfo("Exception opening port {0}: {1}", portName, e); 156 } 157 } 158 } 159 return openablePortNames; 160 } 161 SufficientHardwareRequirements(SerialPortRequirements serialPortRequirements)162 public static bool SufficientHardwareRequirements(SerialPortRequirements serialPortRequirements) 163 { 164 switch (serialPortRequirements) 165 { 166 case SerialPortRequirements.None: 167 return true; 168 169 case SerialPortRequirements.OneSerialPort: 170 return s_localMachineSerialPortRequirements == SerialPortRequirements.OneSerialPort || 171 s_localMachineSerialPortRequirements == SerialPortRequirements.TwoSerialPorts || 172 s_localMachineSerialPortRequirements == SerialPortRequirements.Loopback || 173 s_localMachineSerialPortRequirements == SerialPortRequirements.NullModem; 174 case SerialPortRequirements.TwoSerialPorts: 175 return s_localMachineSerialPortRequirements == SerialPortRequirements.TwoSerialPorts || 176 s_localMachineSerialPortRequirements == SerialPortRequirements.NullModem; 177 case SerialPortRequirements.NullModem: 178 return s_localMachineSerialPortRequirements == SerialPortRequirements.NullModem; 179 case SerialPortRequirements.Loopback: 180 return s_localMachineSerialPortRequirements == SerialPortRequirements.Loopback; 181 case SerialPortRequirements.LoopbackOrNullModem: 182 return s_localMachineSerialPortRequirements == SerialPortRequirements.Loopback || 183 s_localMachineSerialPortRequirements == SerialPortRequirements.NullModem; 184 } 185 186 return false; 187 } 188 InitFirstSerialPort()189 public static SerialPort InitFirstSerialPort() 190 { 191 if (LocalMachineSerialInfo.NullModemPresent) 192 { 193 return new SerialPort(LocalMachineSerialInfo.FirstAvailablePortName); 194 } 195 else if (null != LocalMachineSerialInfo.LoopbackPortName) 196 { 197 return new SerialPort(LocalMachineSerialInfo.LoopbackPortName); 198 } 199 else if (null != LocalMachineSerialInfo.FirstAvailablePortName) 200 { 201 return new SerialPort(LocalMachineSerialInfo.FirstAvailablePortName); 202 } 203 204 return null; 205 } 206 InitSecondSerialPort(SerialPort com)207 public static SerialPort InitSecondSerialPort(SerialPort com) 208 { 209 if (LocalMachineSerialInfo.NullModemPresent) 210 { 211 return new SerialPort(LocalMachineSerialInfo.SecondAvailablePortName); 212 } 213 else if (null != LocalMachineSerialInfo.LoopbackPortName) 214 { 215 return com; 216 } 217 else if (null != LocalMachineSerialInfo.SecondAvailablePortName) 218 { 219 return new SerialPort(LocalMachineSerialInfo.SecondAvailablePortName); 220 } 221 222 return null; 223 } 224 225 public static LocalMachineSerialInfo LocalMachineSerialInfo => s_localMachineSerialInfo; 226 227 /// <summary> 228 /// Set this true to shorten the very long-running stress tests 229 /// </summary> 230 public static bool RunShortStressTests { get; set; } = true; 231 232 public static int MinimumBlockingByteCount => s_flowControlCapabilities.MinimumBlockingByteCount; 233 public static int HardwareTransmitBufferSize => s_flowControlCapabilities.HardwareTransmitBufferSize; 234 public static bool HardwareWriteBlockingAvailable => s_flowControlCapabilities.HardwareWriteBlockingAvailable; 235 Predicate()236 public delegate bool Predicate(); 237 ValueGenerator()238 public delegate T ValueGenerator<T>(); 239 WaitForPredicate(Predicate predicate, int maxWait, string errorMessageFormat, params object[] formatArgs)240 public static void WaitForPredicate(Predicate predicate, int maxWait, string errorMessageFormat, params object[] formatArgs) 241 { 242 WaitForPredicate(predicate, maxWait, string.Format(errorMessageFormat, formatArgs)); 243 } WaitForPredicate(Predicate predicate, int maxWait, string errorMessage)244 public static void WaitForPredicate(Predicate predicate, int maxWait, string errorMessage) 245 { 246 Stopwatch stopWatch = Stopwatch.StartNew(); 247 bool predicateValue = false; 248 249 while (!predicateValue && stopWatch.ElapsedMilliseconds < maxWait) 250 { 251 predicateValue = predicate(); 252 Thread.Sleep(10); 253 } 254 255 Assert.True(predicateValue, errorMessage); 256 } 257 WaitForExpected(ValueGenerator<T> actualValueGenerator, T expectedValue, int maxWait, string errorMessage)258 public static void WaitForExpected<T>(ValueGenerator<T> actualValueGenerator, T expectedValue, int maxWait, 259 string errorMessage) 260 { 261 Stopwatch stopWatch = new Stopwatch(); 262 bool result; 263 T actualValue; 264 int iterationWaitTime = 0; 265 266 stopWatch.Start(); 267 268 do 269 { 270 actualValue = actualValueGenerator(); 271 result = actualValue == null ? null == expectedValue : actualValue.Equals(expectedValue); 272 273 Thread.Sleep(iterationWaitTime); 274 iterationWaitTime = 10; //This is just to ensure there is no delay the first time we check 275 } while (!result && stopWatch.ElapsedMilliseconds < maxWait); 276 277 Assert.True(result, errorMessage + 278 " Expected:" + (null == expectedValue ? "<null>" : expectedValue.ToString()) + 279 " Actual:" + (null == actualValue ? "<null>" : actualValue.ToString())); 280 } 281 282 private const int MIN_RANDOM_CHAR = 0; 283 284 private const int MIN_HIGH_SURROGATE = 0xD800; 285 private const int MAX_HIGH_SURROGATE = 0xDBFF; 286 287 private const int MIN_LOW_SURROGATE = 0xDC00; 288 private const int MAX_LOW_SURROGATE = 0xDFFF; 289 290 private const int MIN_RANDOM_ASCII_CHAR = 0; 291 private const int MAX_RANDOM_ASCII_CHAR = 127; 292 293 private static readonly Random s_random = new Random(-55); 294 private static FlowControlCapabilities s_flowControlCapabilities = new FlowControlCapabilities(0, 0, false); 295 296 [Flags] 297 public enum CharacterOptions { None, Surrogates, ASCII }; 298 GetRandomChars(int count, bool withSurrogates)299 public static char[] GetRandomChars(int count, bool withSurrogates) 300 { 301 if (withSurrogates) 302 return GetRandomCharsWithSurrogates(count); 303 else 304 return GetRandomCharsWithoutSurrogates(count); 305 } 306 GetRandomChars(int count, CharacterOptions options)307 public static char[] GetRandomChars(int count, CharacterOptions options) 308 { 309 if (0 != (options & CharacterOptions.Surrogates)) 310 return GetRandomCharsWithSurrogates(count); 311 if (0 != (options & CharacterOptions.ASCII)) 312 return GetRandomASCIIChars(count); 313 else 314 return GetRandomCharsWithoutSurrogates(count); 315 } 316 GetRandomChars(char[] chars, int index, int count, CharacterOptions options)317 public static void GetRandomChars(char[] chars, int index, int count, CharacterOptions options) 318 { 319 if (0 != (options & CharacterOptions.Surrogates)) 320 GetRandomCharsWithSurrogates(chars, index, count); 321 if (0 != (options & CharacterOptions.ASCII)) 322 GetRandomASCIIChars(chars, index, count); 323 else 324 GetRandomCharsWithoutSurrogates(chars, index, count); 325 } 326 GetRandomString(int count, CharacterOptions options)327 public static string GetRandomString(int count, CharacterOptions options) 328 { 329 return new string(GetRandomChars(count, options)); 330 } 331 GetRandomStringBuilder(int count, CharacterOptions options)332 public static StringBuilder GetRandomStringBuilder(int count, CharacterOptions options) 333 { 334 StringBuilder sb = new StringBuilder(count); 335 sb.Append(GetRandomChars(count, options)); 336 return sb; 337 } 338 GetRandomASCIIChars(int count)339 public static char[] GetRandomASCIIChars(int count) 340 { 341 char[] chars = new char[count]; 342 343 GetRandomASCIIChars(chars, 0, count); 344 345 return chars; 346 } 347 GetRandomASCIIChars(char[] chars, int index, int count)348 public static void GetRandomASCIIChars(char[] chars, int index, int count) 349 { 350 for (int i = 0; i < count; ++i) 351 { 352 chars[i] = GenerateRandomASCII(); 353 } 354 } 355 GetRandomCharsWithoutSurrogates(int count)356 public static char[] GetRandomCharsWithoutSurrogates(int count) 357 { 358 char[] chars = new char[count]; 359 360 GetRandomCharsWithoutSurrogates(chars, 0, count); 361 362 return chars; 363 } 364 GetRandomCharsWithoutSurrogates(char[] chars, int index, int count)365 public static void GetRandomCharsWithoutSurrogates(char[] chars, int index, int count) 366 { 367 for (int i = 0; i < count; ++i) 368 { 369 chars[i] = GenerateRandomCharNonSurrogate(); 370 } 371 } 372 GetRandomCharsWithSurrogates(int count)373 public static char[] GetRandomCharsWithSurrogates(int count) 374 { 375 char[] chars = new char[count]; 376 377 GetRandomCharsWithSurrogates(chars, 0, count); 378 379 return chars; 380 } 381 GetRandomCharsWithSurrogates(char[] chars, int index, int count)382 public static void GetRandomCharsWithSurrogates(char[] chars, int index, int count) 383 { 384 for (int i = 0; i < count; ++i) 385 { 386 int randomChar = GenerateRandomCharWithHighSurrogate(); 387 388 if (MIN_HIGH_SURROGATE <= randomChar) 389 { 390 if (i < (count - 1)) 391 { 392 chars[i] = (char)randomChar; 393 394 ++i; 395 chars[i] = GenerateRandomLowSurrogate(); 396 } 397 else 398 { 399 chars[i] = GenerateRandomCharNonSurrogate(); 400 } 401 } 402 else 403 { 404 chars[i] = (char)randomChar; 405 } 406 } 407 } 408 GenerateRandomASCII()409 public static char GenerateRandomASCII() 410 { 411 return (char)s_random.Next(MIN_RANDOM_ASCII_CHAR, MAX_RANDOM_ASCII_CHAR + 1); 412 } 413 GenerateRandomHighSurrogate()414 public static char GenerateRandomHighSurrogate() 415 { 416 return (char)s_random.Next(MIN_HIGH_SURROGATE, MAX_HIGH_SURROGATE + 1); 417 } 418 GenerateRandomLowSurrogate()419 public static char GenerateRandomLowSurrogate() 420 { 421 return (char)s_random.Next(MIN_LOW_SURROGATE, MAX_LOW_SURROGATE + 1); 422 } 423 GenerateRandomCharWithHighSurrogate()424 public static char GenerateRandomCharWithHighSurrogate() 425 { 426 return (char)s_random.Next(MIN_RANDOM_CHAR, MAX_HIGH_SURROGATE + 1); 427 } 428 GenerateRandomCharNonSurrogate()429 public static char GenerateRandomCharNonSurrogate() 430 { 431 return (char)s_random.Next(MIN_RANDOM_CHAR, MIN_HIGH_SURROGATE); 432 } 433 GetRandomBytes(int count)434 public static byte[] GetRandomBytes(int count) 435 { 436 byte[] bytes = new byte[count]; 437 438 GetRandomBytes(bytes, 0, count); 439 440 return bytes; 441 } 442 443 /// <summary> 444 /// Returns a random char that is not c 445 /// </summary> GetRandomOtherChar(char c, CharacterOptions options)446 public static char GetRandomOtherChar(char c, CharacterOptions options) 447 { 448 switch (options) 449 { 450 case CharacterOptions.ASCII: 451 return GetRandomOtherASCIIChar(c); 452 default: 453 return GetRandomOtherUnicodeChar(c); 454 } 455 } 456 GetRandomOtherUnicodeChar(char c)457 public static char GetRandomOtherUnicodeChar(char c) 458 { 459 char newChar; 460 461 do 462 { 463 newChar = GenerateRandomCharNonSurrogate(); 464 } while (newChar == c); 465 466 return newChar; 467 } 468 GetRandomOtherASCIIChar(char c)469 public static char GetRandomOtherASCIIChar(char c) 470 { 471 char newChar; 472 473 do 474 { 475 newChar = GenerateRandomASCII(); 476 } while (newChar == c); 477 478 return newChar; 479 } 480 GetRandomBytes(byte[] bytes, int index, int count)481 public static void GetRandomBytes(byte[] bytes, int index, int count) 482 { 483 for (int i = 0; i < count; ++i) 484 { 485 bytes[i + index] = (byte)s_random.Next(0, 256); 486 } 487 } 488 IsSurrogate(char c)489 public static bool IsSurrogate(char c) 490 { 491 return IsHighSurrogate(c) || IsLowSurrogate(c); 492 } 493 IsHighSurrogate(char c)494 public static bool IsHighSurrogate(char c) 495 { 496 return MIN_HIGH_SURROGATE <= c && c <= MAX_HIGH_SURROGATE; 497 } 498 IsLowSurrogate(char c)499 public static bool IsLowSurrogate(char c) 500 { 501 return MIN_LOW_SURROGATE <= c && c <= MAX_LOW_SURROGATE; 502 } 503 OrdinalIndexOf(string input, string search)504 public static int OrdinalIndexOf(string input, string search) 505 { 506 return OrdinalIndexOf(input, 0, input.Length, search); 507 } 508 OrdinalIndexOf(string input, int startIndex, string search)509 public static int OrdinalIndexOf(string input, int startIndex, string search) 510 { 511 return OrdinalIndexOf(input, startIndex, input.Length - startIndex, search); 512 } 513 OrdinalIndexOf(string input, int startIndex, int count, string search)514 private static int OrdinalIndexOf(string input, int startIndex, int count, string search) 515 { 516 int lastSearchIndex = (count + startIndex) - search.Length; 517 if (lastSearchIndex >= input.Length) 518 { 519 throw new ArgumentOutOfRangeException(nameof(input), "Searching will result in accessing element past the end of the array"); 520 } 521 522 for (int i = startIndex; i <= lastSearchIndex; ++i) 523 { 524 if (input[i] == search[0]) 525 { 526 bool match = true; 527 for (int searchIndex = 1, inputIndex = i + 1; searchIndex < search.Length; ++searchIndex, ++inputIndex) 528 { 529 match = input[inputIndex] == search[searchIndex]; 530 531 if (!match) 532 { 533 break; 534 } 535 } 536 537 if (match) 538 { 539 return i; 540 } 541 } 542 } 543 544 return -1; 545 } 546 PrintChars(char[] chars)547 public static void PrintChars(char[] chars) 548 { 549 foreach (char chr in chars) 550 { 551 Debug.WriteLine("(char){0}, //Char={1}, {0:X}", (int)chr, chr); 552 } 553 } 554 PrintBytes(byte[] bytes)555 public static void PrintBytes(byte[] bytes) 556 { 557 for (int i = 0; i < bytes.Length; i++) 558 { 559 Debug.WriteLine("{0}, //{0:X} Index: {1}", (int)bytes[i], i); 560 } 561 } 562 563 /// <summary> 564 /// Verifies the contents of the array. 565 /// </summary> 566 /// <typeparam name="T"></typeparam> 567 /// <param name="expectedArray">The expected items in the array.</param> 568 /// <param name="actualArray">The actual array.</param> 569 /// <returns>true if expectedArray and actualArray have the same contents.</returns> VerifyArray(T[] expectedArray, T[] actualArray)570 public static void VerifyArray<T>(T[] expectedArray, T[] actualArray) 571 { 572 Assert.Equal(expectedArray.Length, actualArray.Length); 573 VerifyArray(expectedArray, actualArray, 0, expectedArray.Length); 574 } 575 576 /// <summary> 577 /// Verifies the contents of the array. 578 /// </summary> 579 /// <typeparam name="T"></typeparam> 580 /// <param name="expectedArray">The expected items in the array.</param> 581 /// <param name="actualArray">The actual array.</param> 582 /// <param name="index">The index to start verifying the items at.</param> 583 /// <param name="length">The number of item to verify</param> 584 /// <returns>true if expectedArray and actualArray have the same contents.</returns> VerifyArray(T[] expectedArray, T[] actualArray, int index, int length)585 public static void VerifyArray<T>(T[] expectedArray, T[] actualArray, int index, int length) 586 { 587 int tempLength = length + index; 588 for (int i = index; i < tempLength; ++i) 589 { 590 bool result = expectedArray[i] == null ? null != actualArray[i] : expectedArray[i].Equals(actualArray[i]); 591 592 Assert.True(result, string.Format("Err_55808aoped Items differ at {0} expected {1} actual {2}", i, expectedArray[i], actualArray[i])); 593 } 594 } 595 596 /// <summary> 597 /// Set both ports to 115200 baud to speed test performance 598 /// </summary> SetHighSpeed(SerialPort com1, SerialPort com2)599 public static void SetHighSpeed(SerialPort com1, SerialPort com2) 600 { 601 if (com1 != null) 602 { 603 com1.BaudRate = 115200; 604 } 605 if (com2 != null && com2 != com1) 606 { 607 com2.BaudRate = 115200; 608 } 609 } 610 611 612 /// <summary> 613 /// Wait for write data to be written into a blocked (by adverse flow control) port 614 /// </summary> WaitForWriteBufferToLoad(SerialPort com, int bufferLength)615 public static void WaitForWriteBufferToLoad(SerialPort com, int bufferLength) 616 { 617 Stopwatch sw = Stopwatch.StartNew(); 618 while (com.BytesToWrite + HardwareTransmitBufferSize < bufferLength) 619 { 620 Thread.Sleep(50); 621 Assert.True(sw.ElapsedMilliseconds < 3000, $"Timeout while waiting for data to be written to port (wrote {bufferLength}, queued {com.BytesToWrite}, bufSize {HardwareTransmitBufferSize})"); 622 } 623 } 624 625 /// <summary> 626 /// Wait for write data to be written into a blocked (by adverse flow control) port, 627 /// then check that exactly the expected quantity is present 628 /// </summary> WaitForExactWriteBufferLoad(SerialPort com, int bufferLength)629 public static void WaitForExactWriteBufferLoad(SerialPort com, int bufferLength) 630 { 631 WaitForWriteBufferToLoad(com, bufferLength); 632 Assert.Equal(bufferLength, com.BytesToWrite + HardwareTransmitBufferSize); 633 } 634 635 /// <summary> 636 /// Wait for the data to arrive into the read buffer 637 /// </summary> WaitForReadBufferToLoad(SerialPort com, int bufferLength)638 public static void WaitForReadBufferToLoad(SerialPort com, int bufferLength) 639 { 640 Stopwatch sw = Stopwatch.StartNew(); 641 while (com.BytesToRead < bufferLength) 642 { 643 Thread.Sleep(50); 644 Assert.True(sw.ElapsedMilliseconds < 3000, $"Timeout while waiting for data to be arrive at port (expected {bufferLength}, available {com.BytesToRead})"); 645 } 646 } 647 648 WaitForTaskToStart(Task task)649 public static void WaitForTaskToStart(Task task) 650 { 651 Stopwatch sw = Stopwatch.StartNew(); 652 while (task.Status < TaskStatus.Running) 653 { 654 // Wait for the thread to start 655 Thread.Sleep(50); 656 Assert.True(sw.ElapsedMilliseconds < 2000, "Timeout waiting for task to start"); 657 } 658 } 659 WaitForTaskCompletion(Task task)660 public static void WaitForTaskCompletion(Task task) 661 { 662 Assert.True(task.Wait(5000), "Timeout waiting for task completion"); 663 } 664 } 665 } 666