CollectionNavigatorTests.cs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  1. using System.Collections;
  2. using System.Collections.Concurrent;
  3. using Moq;
  4. using Xunit.Abstractions;
  5. namespace TextTests;
  6. public class CollectionNavigatorTests
  7. {
  8. private static readonly string [] simpleStrings =
  9. {
  10. "apricot", // 0
  11. "arm", // 1
  12. "bat", // 2
  13. "batman", // 3
  14. "candle" // 4
  15. };
  16. private readonly ITestOutputHelper _output;
  17. public CollectionNavigatorTests (ITestOutputHelper output) { _output = output; }
  18. [Fact]
  19. public void AtSymbol ()
  20. {
  21. var strings = new [] { "apricot", "arm", "ta", "@bob", "@bb", "text", "egg", "candle" };
  22. var n = new CollectionNavigator (strings);
  23. Assert.Equal (3, n.GetNextMatchingItem (0, '@'));
  24. Assert.Equal (3, n.GetNextMatchingItem (3, 'b'));
  25. Assert.Equal (4, n.GetNextMatchingItem (3, 'b'));
  26. }
  27. [Fact]
  28. public void Cycling ()
  29. {
  30. // cycling with 'b'
  31. var n = new CollectionNavigator (simpleStrings);
  32. Assert.Equal (2, n.GetNextMatchingItem (0, 'b'));
  33. Assert.Equal (3, n.GetNextMatchingItem (2, 'b'));
  34. // if 4 (candle) is selected it should loop back to bat
  35. Assert.Equal (2, n.GetNextMatchingItem (4, 'b'));
  36. // cycling with 'a'
  37. n = new (simpleStrings);
  38. Assert.Equal (0, n.GetNextMatchingItem (null, 'a'));
  39. Assert.Equal (1, n.GetNextMatchingItem (0, 'a'));
  40. // if 4 (candle) is selected it should loop back to apricot
  41. Assert.Equal (0, n.GetNextMatchingItem (4, 'a'));
  42. }
  43. [Fact]
  44. public void Delay ()
  45. {
  46. var strings = new [] { "$$", "$100.00", "$101.00", "$101.10", "$200.00", "apricot" };
  47. int? current = 0;
  48. var n = new CollectionNavigator (strings);
  49. // No delay
  50. Assert.Equal (strings.IndexOf ("apricot"), current = n.GetNextMatchingItem (current, 'a'));
  51. Assert.Equal ("a", n.SearchString);
  52. Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, '$'));
  53. Assert.Equal ("$", n.SearchString);
  54. Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, '$'));
  55. Assert.Equal ("$$", n.SearchString);
  56. // Delay
  57. Thread.Sleep (n.TypingDelay + 10);
  58. Assert.Equal (strings.IndexOf ("apricot"), current = n.GetNextMatchingItem (current, 'a'));
  59. Assert.Equal ("a", n.SearchString);
  60. Thread.Sleep (n.TypingDelay + 10);
  61. Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, '$'));
  62. Assert.Equal ("$", n.SearchString);
  63. Thread.Sleep (n.TypingDelay + 10);
  64. Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, '$'));
  65. Assert.Equal ("$", n.SearchString);
  66. Thread.Sleep (n.TypingDelay + 10);
  67. Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, '$'));
  68. Assert.Equal ("$", n.SearchString);
  69. Thread.Sleep (n.TypingDelay + 10);
  70. Assert.Equal (strings.IndexOf ("$101.10"), current = n.GetNextMatchingItem (current, '$'));
  71. Assert.Equal ("$", n.SearchString);
  72. Thread.Sleep (n.TypingDelay + 10);
  73. Assert.Equal (strings.IndexOf ("$101.10"), current = n.GetNextMatchingItem (current, '2')); // Shouldn't move
  74. Assert.Equal ("2", n.SearchString);
  75. }
  76. [Fact]
  77. public void FullText ()
  78. {
  79. var strings = new [] { "apricot", "arm", "ta", "target", "text", "egg", "candle" };
  80. var n = new CollectionNavigator (strings);
  81. int? current = 0;
  82. Assert.Equal (strings.IndexOf ("ta"), current = n.GetNextMatchingItem (current, 't'));
  83. // should match "te" in "text"
  84. Assert.Equal (strings.IndexOf ("text"), current = n.GetNextMatchingItem (current, 'e'));
  85. // still matches text
  86. Assert.Equal (strings.IndexOf ("text"), current = n.GetNextMatchingItem (current, 'x'));
  87. // nothing starts texa so it should NOT jump to apricot
  88. Assert.Equal (strings.IndexOf ("text"), current = n.GetNextMatchingItem (current, 'a'));
  89. Thread.Sleep (n.TypingDelay + 100);
  90. // nothing starts "texa". Since were past timedelay we DO jump to apricot
  91. Assert.Equal (strings.IndexOf ("apricot"), current = n.GetNextMatchingItem (current, 'a'));
  92. }
  93. [Theory]
  94. [InlineData (KeyCode.A, true)]
  95. [InlineData (KeyCode.Z, true)]
  96. [InlineData (KeyCode.D0, true)]
  97. [InlineData (KeyCode.A | KeyCode.ShiftMask, true)]
  98. [InlineData (KeyCode.Z | KeyCode.ShiftMask, true)]
  99. [InlineData (KeyCode.Space, true)]
  100. [InlineData (KeyCode.Z | KeyCode.CtrlMask, false)]
  101. [InlineData (KeyCode.Z | KeyCode.AltMask, false)]
  102. [InlineData (KeyCode.F1, false)]
  103. [InlineData (KeyCode.Delete, false)]
  104. [InlineData (KeyCode.Esc, false)]
  105. [InlineData (KeyCode.ShiftMask, false)]
  106. public void IsCompatibleKey_Does_Not_Allow_Alt_And_Ctrl_Keys (KeyCode keyCode, bool compatible)
  107. {
  108. var m = new DefaultCollectionNavigatorMatcher ();
  109. Assert.Equal (compatible, m.IsCompatibleKey (keyCode));
  110. }
  111. [Fact]
  112. public void MinimizeMovement_False_ShouldMoveIfMultipleMatches ()
  113. {
  114. var strings = new [] { "$$", "$100.00", "$101.00", "$101.10", "$200.00", "apricot", "c", "car", "cart" };
  115. int? current = 0;
  116. var n = new CollectionNavigator (strings);
  117. Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$$"));
  118. Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$"));
  119. Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$$")); // back to top
  120. Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$"));
  121. Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, "$"));
  122. Assert.Equal (strings.IndexOf ("$101.10"), current = n.GetNextMatchingItem (current, "$"));
  123. Assert.Equal (strings.IndexOf ("$200.00"), current = n.GetNextMatchingItem (current, "$"));
  124. Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$")); // back to top
  125. Assert.Equal (strings.IndexOf ("apricot"), current = n.GetNextMatchingItem (current, "a"));
  126. Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$")); // back to top
  127. Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$100.00"));
  128. Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, "$"));
  129. Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, "$101.00"));
  130. Assert.Equal (strings.IndexOf ("$200.00"), current = n.GetNextMatchingItem (current, "$2"));
  131. Assert.Equal (strings.IndexOf ("$200.00"), current = n.GetNextMatchingItem (current, "$200.00"));
  132. Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, "$101.00"));
  133. Assert.Equal (strings.IndexOf ("$200.00"), current = n.GetNextMatchingItem (current, "$2"));
  134. Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, "$101.00"));
  135. Assert.Equal (strings.IndexOf ("$200.00"), current = n.GetNextMatchingItem (current, "$2"));
  136. Assert.Equal (strings.IndexOf ("car"), current = n.GetNextMatchingItem (current, "car"));
  137. Assert.Equal (strings.IndexOf ("cart"), current = n.GetNextMatchingItem (current, "car"));
  138. Assert.Null (n.GetNextMatchingItem (current, "x"));
  139. }
  140. [Fact]
  141. public void MinimizeMovement_True_ShouldStayOnCurrentIfMultipleMatches ()
  142. {
  143. var strings = new [] { "$$", "$100.00", "$101.00", "$101.10", "$200.00", "apricot", "c", "car", "cart" };
  144. int? current = 0;
  145. var n = new CollectionNavigator (strings);
  146. Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$$", true));
  147. Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$", true));
  148. Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$$", true)); // back to top
  149. Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$1", true));
  150. Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$", true));
  151. Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$", true));
  152. Assert.Equal (strings.IndexOf ("car"), current = n.GetNextMatchingItem (current, "car", true));
  153. Assert.Equal (strings.IndexOf ("car"), current = n.GetNextMatchingItem (current, "car", true));
  154. Assert.Null (n.GetNextMatchingItem (current, "x", true));
  155. }
  156. [Fact]
  157. public void MutliKeySearchPlusWrongKeyStays ()
  158. {
  159. var strings = new [] { "a", "c", "can", "candle", "candy", "yellow", "zebra" };
  160. int? current = 0;
  161. var n = new CollectionNavigator (strings);
  162. // https://github.com/gui-cs/Terminal.Gui/pull/2132#issuecomment-1298425573
  163. // One thing that it currently does that is different from Explorer is that as soon as you hit a wrong key then it jumps to that index.
  164. // So if you type cand then z it jumps you to something beginning with z. In the same situation Windows Explorer beeps (not the best!)
  165. // but remains on candle.
  166. // We might be able to update the behaviour so that a 'wrong' keypress (z) within 500ms of a 'right' keypress ("can" + 'd') is
  167. // simply ignored (possibly ending the search process though). That would give a short delay for user to realise the thing
  168. // they typed doesn't exist and then start a new search (which would be possible 500ms after the last 'good' keypress).
  169. // This would only apply for 2+ character searches where theres been a successful 2+ character match right before.
  170. Assert.Equal (strings.IndexOf ("a"), current = n.GetNextMatchingItem (current, 'a'));
  171. Assert.Equal ("a", n.SearchString);
  172. Assert.Equal (strings.IndexOf ("c"), current = n.GetNextMatchingItem (current, 'c'));
  173. Assert.Equal ("c", n.SearchString);
  174. Assert.Equal (strings.IndexOf ("can"), current = n.GetNextMatchingItem (current, 'a'));
  175. Assert.Equal ("ca", n.SearchString);
  176. Assert.Equal (strings.IndexOf ("can"), current = n.GetNextMatchingItem (current, 'n'));
  177. Assert.Equal ("can", n.SearchString);
  178. Assert.Equal (strings.IndexOf ("candle"), current = n.GetNextMatchingItem (current, 'd'));
  179. Assert.Equal ("cand", n.SearchString);
  180. // Same as above, but with a 'wrong' key (z)
  181. Thread.Sleep (n.TypingDelay + 10);
  182. Assert.Equal (strings.IndexOf ("a"), current = n.GetNextMatchingItem (current, 'a'));
  183. Assert.Equal ("a", n.SearchString);
  184. Assert.Equal (strings.IndexOf ("c"), current = n.GetNextMatchingItem (current, 'c'));
  185. Assert.Equal ("c", n.SearchString);
  186. Assert.Equal (strings.IndexOf ("can"), current = n.GetNextMatchingItem (current, 'a'));
  187. Assert.Equal ("ca", n.SearchString);
  188. Assert.Equal (strings.IndexOf ("can"), current = n.GetNextMatchingItem (current, 'n'));
  189. Assert.Equal ("can", n.SearchString);
  190. Assert.Equal (strings.IndexOf ("can"), current = n.GetNextMatchingItem (current, 'z')); // Shouldn't move
  191. Assert.Equal ("can", n.SearchString); // Shouldn't change
  192. }
  193. [Fact]
  194. public void OutOfBoundsShouldBeIgnored ()
  195. {
  196. var n = new CollectionNavigator (simpleStrings);
  197. // Expect saying that index 500 is the current selection should not cause
  198. // error and just be ignored (treated as no selection)
  199. Assert.Equal (2, n.GetNextMatchingItem (500, 'b'));
  200. }
  201. [Fact]
  202. public void ShouldAcceptNull ()
  203. {
  204. var n = new CollectionNavigator (simpleStrings);
  205. // Expect that index of null (i.e. no selection) should work correctly
  206. // and select the first entry of the letter 'b'
  207. Assert.Equal (2, n.GetNextMatchingItem (null, 'b'));
  208. }
  209. [Fact]
  210. public void Symbols ()
  211. {
  212. var strings = new [] { "$$", "$100.00", "$101.00", "$101.10", "$200.00", "apricot" };
  213. int? current = 0;
  214. var n = new CollectionNavigator (strings);
  215. Assert.Equal (strings.IndexOf ("apricot"), current = n.GetNextMatchingItem (current, 'a'));
  216. Assert.Equal ("a", n.SearchString);
  217. Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, '$'));
  218. Assert.Equal ("$", n.SearchString);
  219. Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, '1'));
  220. Assert.Equal ("$1", n.SearchString);
  221. Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, '0'));
  222. Assert.Equal ("$10", n.SearchString);
  223. Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, '1'));
  224. Assert.Equal ("$101", n.SearchString);
  225. Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, '.'));
  226. Assert.Equal ("$101.", n.SearchString);
  227. // stay on the same item becuase still in timedelay
  228. Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, 'a'));
  229. Assert.Equal ("$101.", n.SearchString);
  230. Thread.Sleep (n.TypingDelay + 100);
  231. // another '$' means searching for "$" again
  232. Assert.Equal (strings.IndexOf ("$101.10"), current = n.GetNextMatchingItem (current, '$'));
  233. Assert.Equal ("$", n.SearchString);
  234. Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, '$'));
  235. Assert.Equal ("$$", n.SearchString);
  236. }
  237. [Fact]
  238. public void Unicode ()
  239. {
  240. var strings = new [] { "apricot", "arm", "ta", "丗丙业丞", "丗丙丛", "text", "egg", "candle" };
  241. var n = new CollectionNavigator (strings);
  242. int? current = 0;
  243. Assert.Equal (strings.IndexOf ("丗丙业丞"), current = n.GetNextMatchingItem (current, '丗'));
  244. // 丗丙业丞 is as good a match as 丗丙丛
  245. // so when doing multi character searches we should
  246. // prefer to stay on the same index unless we invalidate
  247. // our typed text
  248. Assert.Equal (strings.IndexOf ("丗丙业丞"), current = n.GetNextMatchingItem (current, '丙'));
  249. // No longer matches 丗丙业丞 and now only matches 丗丙丛
  250. // so we should move to the new match
  251. Assert.Equal (strings.IndexOf ("丗丙丛"), current = n.GetNextMatchingItem (current, '丛'));
  252. // nothing starts "丗丙丛a". Since were still in the timedelay we do not jump to apricot
  253. Assert.Equal (strings.IndexOf ("丗丙丛"), current = n.GetNextMatchingItem (current, 'a'));
  254. Thread.Sleep (n.TypingDelay + 100);
  255. // nothing starts "丗丙丛a". Since were past timedelay we DO jump to apricot
  256. Assert.Equal (strings.IndexOf ("apricot"), current = n.GetNextMatchingItem (current, 'a'));
  257. }
  258. [Fact]
  259. public void Word ()
  260. {
  261. var strings = new [] { "apricot", "arm", "bat", "batman", "bates hotel", "candle" };
  262. int? current = 0;
  263. var n = new CollectionNavigator (strings);
  264. Assert.Equal (strings.IndexOf ("bat"), current = n.GetNextMatchingItem (current, 'b')); // match bat
  265. Assert.Equal (strings.IndexOf ("bat"), current = n.GetNextMatchingItem (current, 'a')); // match bat
  266. Assert.Equal (strings.IndexOf ("bat"), current = n.GetNextMatchingItem (current, 't')); // match bat
  267. Assert.Equal (
  268. strings.IndexOf ("bates hotel"),
  269. current = n.GetNextMatchingItem (current, 'e')
  270. ); // match bates hotel
  271. Assert.Equal (
  272. strings.IndexOf ("bates hotel"),
  273. current = n.GetNextMatchingItem (current, 's')
  274. ); // match bates hotel
  275. Assert.Equal (
  276. strings.IndexOf ("bates hotel"),
  277. current = n.GetNextMatchingItem (current, ' ')
  278. ); // match bates hotel
  279. }
  280. [Fact]
  281. public void CustomMatcher_NeverMatches ()
  282. {
  283. var strings = new [] { "apricot", "arm", "bat", "batman", "bates hotel", "candle" };
  284. int? current = 0;
  285. var n = new CollectionNavigator (strings);
  286. Mock<ICollectionNavigatorMatcher> matchNone = new ();
  287. matchNone.Setup (m => m.IsMatch (It.IsAny<string> (), It.IsAny<object> ()))
  288. .Returns (false);
  289. n.Matcher = matchNone.Object;
  290. Assert.Equal (0, current = n.GetNextMatchingItem (current, 'b')); // no matches
  291. Assert.Equal (0, current = n.GetNextMatchingItem (current, 'a')); // no matches
  292. Assert.Equal (0, current = n.GetNextMatchingItem (current, 't')); // no matches
  293. }
  294. #region Thread Safety Tests
  295. [Fact]
  296. public void ThreadSafety_ConcurrentSearchStringAccess ()
  297. {
  298. var strings = new [] { "apricot", "arm", "bat", "batman", "candle" };
  299. var navigator = new CollectionNavigator (strings);
  300. var numTasks = 20;
  301. ConcurrentBag<Exception> exceptions = new ();
  302. Parallel.For (
  303. 0,
  304. numTasks,
  305. i =>
  306. {
  307. try
  308. {
  309. // Read SearchString concurrently
  310. string searchString = navigator.SearchString;
  311. // Perform navigation operations concurrently
  312. int? result = navigator.GetNextMatchingItem (0, 'a');
  313. // Read SearchString again
  314. searchString = navigator.SearchString;
  315. }
  316. catch (Exception ex)
  317. {
  318. exceptions.Add (ex);
  319. }
  320. });
  321. Assert.Empty (exceptions);
  322. }
  323. [Fact]
  324. public void ThreadSafety_ConcurrentCollectionAccess ()
  325. {
  326. var strings = new [] { "apricot", "arm", "bat", "batman", "candle" };
  327. var navigator = new CollectionNavigator (strings);
  328. var numTasks = 20;
  329. ConcurrentBag<Exception> exceptions = new ();
  330. Parallel.For (
  331. 0,
  332. numTasks,
  333. i =>
  334. {
  335. try
  336. {
  337. // Access Collection property concurrently
  338. IList collection = navigator.Collection;
  339. // Perform navigation
  340. int? result = navigator.GetNextMatchingItem (0, (char)('a' + i % 3));
  341. }
  342. catch (Exception ex)
  343. {
  344. exceptions.Add (ex);
  345. }
  346. });
  347. Assert.Empty (exceptions);
  348. }
  349. [Fact]
  350. public void ThreadSafety_ConcurrentNavigationOperations ()
  351. {
  352. var strings = new [] { "apricot", "arm", "bat", "batman", "candle", "cat", "dog", "elephant" };
  353. var navigator = new CollectionNavigator (strings);
  354. var numTasks = 50;
  355. ConcurrentBag<int?> results = new ();
  356. ConcurrentBag<Exception> exceptions = new ();
  357. Parallel.For (
  358. 0,
  359. numTasks,
  360. i =>
  361. {
  362. try
  363. {
  364. var searchChar = (char)('a' + i % 5);
  365. int? result = navigator.GetNextMatchingItem (i % strings.Length, searchChar);
  366. results.Add (result);
  367. }
  368. catch (Exception ex)
  369. {
  370. exceptions.Add (ex);
  371. }
  372. });
  373. Assert.Empty (exceptions);
  374. Assert.Equal (numTasks, results.Count);
  375. }
  376. [Fact]
  377. public void ThreadSafety_ConcurrentCollectionModification ()
  378. {
  379. var strings = new [] { "apricot", "arm", "bat", "batman", "candle" };
  380. var navigator = new CollectionNavigator (strings);
  381. var numReaders = 10;
  382. var numWriters = 5;
  383. ConcurrentBag<Exception> exceptions = new ();
  384. List<Task> tasks = new ();
  385. // Reader tasks
  386. for (var i = 0; i < numReaders; i++)
  387. {
  388. tasks.Add (
  389. Task.Run (() =>
  390. {
  391. try
  392. {
  393. for (var j = 0; j < 100; j++)
  394. {
  395. int? result = navigator.GetNextMatchingItem (0, 'a');
  396. string searchString = navigator.SearchString;
  397. }
  398. }
  399. catch (Exception ex)
  400. {
  401. exceptions.Add (ex);
  402. }
  403. }));
  404. }
  405. // Writer tasks (change Collection reference)
  406. for (var i = 0; i < numWriters; i++)
  407. {
  408. int writerIndex = i;
  409. tasks.Add (
  410. Task.Run (() =>
  411. {
  412. try
  413. {
  414. for (var j = 0; j < 50; j++)
  415. {
  416. var newStrings = new [] { $"item{writerIndex}_{j}_1", $"item{writerIndex}_{j}_2" };
  417. navigator.Collection = newStrings;
  418. Thread.Sleep (1); // Small delay to increase contention
  419. }
  420. }
  421. catch (Exception ex)
  422. {
  423. exceptions.Add (ex);
  424. }
  425. }));
  426. }
  427. #pragma warning disable xUnit1031
  428. Task.WaitAll (tasks.ToArray ());
  429. #pragma warning restore xUnit1031
  430. // Allow some exceptions due to collection being swapped during access
  431. // but verify no deadlocks occurred (all tasks completed)
  432. Assert.True (tasks.All (t => t.IsCompleted));
  433. }
  434. [Fact]
  435. public void ThreadSafety_ConcurrentSearchStringChanges ()
  436. {
  437. var strings = new [] { "apricot", "arm", "bat", "batman", "candle", "cat", "dog", "elephant", "fox", "goat" };
  438. var navigator = new CollectionNavigator (strings);
  439. var numTasks = 30;
  440. ConcurrentBag<Exception> exceptions = new ();
  441. ConcurrentBag<string> searchStrings = new ();
  442. Parallel.For (
  443. 0,
  444. numTasks,
  445. i =>
  446. {
  447. try
  448. {
  449. // Each task performs multiple searches rapidly
  450. char [] chars = { 'a', 'b', 'c', 'd', 'e', 'f' };
  451. foreach (char c in chars)
  452. {
  453. navigator.GetNextMatchingItem (0, c);
  454. searchStrings.Add (navigator.SearchString);
  455. }
  456. }
  457. catch (Exception ex)
  458. {
  459. exceptions.Add (ex);
  460. }
  461. });
  462. Assert.Empty (exceptions);
  463. Assert.NotEmpty (searchStrings);
  464. }
  465. [Fact]
  466. public void ThreadSafety_StressTest_RapidOperations ()
  467. {
  468. var strings = new string [100];
  469. for (var i = 0; i < 100; i++)
  470. {
  471. strings [i] = $"item_{i:D3}";
  472. }
  473. var navigator = new CollectionNavigator (strings);
  474. var numTasks = 100;
  475. var operationsPerTask = 1000;
  476. ConcurrentBag<Exception> exceptions = new ();
  477. Parallel.For (
  478. 0,
  479. numTasks,
  480. i =>
  481. {
  482. try
  483. {
  484. var random = new Random (i);
  485. for (var j = 0; j < operationsPerTask; j++)
  486. {
  487. int? currentIndex = random.Next (0, strings.Length);
  488. var searchChar = (char)('a' + random.Next (0, 26));
  489. navigator.GetNextMatchingItem (currentIndex, searchChar);
  490. if (j % 100 == 0)
  491. {
  492. string searchString = navigator.SearchString;
  493. }
  494. }
  495. }
  496. catch (Exception ex)
  497. {
  498. exceptions.Add (ex);
  499. }
  500. });
  501. Assert.Empty (exceptions);
  502. }
  503. #endregion Thread Safety Tests
  504. }