PageRenderTime 55ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/CsvLogTailer.Tests/CsvTailerTests.cs

https://bitbucket.org/emertechie/csvlogtailer
C# | 624 lines | 478 code | 130 blank | 16 comment | 24 complexity | 4ae34a94c009a900814f353e8c49dc13 MD5 | raw file
  1. using System;
  2. using System.Collections.Concurrent;
  3. using System.Collections.Generic;
  4. using System.Diagnostics;
  5. using System.Linq;
  6. using System.Reactive;
  7. using System.IO;
  8. using System.Reactive.Disposables;
  9. using System.Reactive.Linq;
  10. using System.Reactive.Subjects;
  11. using System.Text;
  12. using System.Threading;
  13. using System.Threading.Tasks;
  14. using CsvLogTailing.Bookmarks;
  15. using Xunit;
  16. using Xunit.Extensions;
  17. namespace CsvLogTailing.Tests
  18. {
  19. public class CsvTailerTests
  20. {
  21. internal const char DefaultDelimeter = '|';
  22. public abstract class FileTailerTestsBase : IDisposable
  23. {
  24. protected static readonly TimeSpan TimeoutTimeSpan = TimeSpan.FromSeconds(Debugger.IsAttached ? 60 : 5);
  25. protected static readonly string[] LogColumns = new[] { "DateTime", "Namespace", "Machine", "Level", "Message", "Exception" };
  26. protected FileTailerTestsBase()
  27. {
  28. ObservedEvents = new BlockingCollection<Notification<LogRecord>>();
  29. }
  30. ~FileTailerTestsBase()
  31. {
  32. Dispose(false);
  33. }
  34. protected string LogFilePath { get; set; }
  35. protected IDisposable TailerSubscription { get; set; }
  36. protected BlockingCollection<Notification<LogRecord>> ObservedEvents { get; private set; }
  37. protected void Dispose(bool disposing)
  38. {
  39. if (disposing)
  40. DisposeManagedObjects();
  41. }
  42. protected virtual void DisposeManagedObjects()
  43. {
  44. if (TailerSubscription != null)
  45. TailerSubscription.Dispose();
  46. if (LogFilePath != null && File.Exists(LogFilePath))
  47. File.Delete(LogFilePath);
  48. }
  49. public void Dispose()
  50. {
  51. Dispose(true);
  52. GC.SuppressFinalize(this);
  53. }
  54. protected void Write(string text, string filePath = null)
  55. {
  56. filePath = filePath ?? LogFilePath;
  57. using (var stream = new FileStream(filePath, FileMode.Append, FileAccess.Write, FileShare.Read))
  58. {
  59. using (var writer = new StreamWriter(stream, Encoding.UTF8))
  60. {
  61. writer.WriteLine(text);
  62. writer.Flush();
  63. }
  64. }
  65. }
  66. protected LogRecord GetNext(BlockingCollection<Notification<LogRecord>> observedEvents)
  67. {
  68. Notification<LogRecord> observed = GetNextNotification(observedEvents);
  69. if (observed.Kind == NotificationKind.OnError)
  70. Assert.True(observed.Kind != NotificationKind.OnError, observed.Exception.Message);
  71. Assert.Equal(NotificationKind.OnNext, observed.Kind);
  72. return observed.Value;
  73. }
  74. protected static string CreateLogLine(
  75. string dateTimeString = "2012-06-01 18:34:49.8539",
  76. string nameSpace = "Some.Namespace",
  77. string machineName = "MACHINE-X",
  78. string logLevel = "INFO",
  79. string logMessage = "Message 1",
  80. string exception = null,
  81. char delimeter = DefaultDelimeter)
  82. {
  83. return String.Join(delimeter.ToString(), dateTimeString, nameSpace, machineName, logLevel, logMessage, exception);
  84. }
  85. protected static void AssertLogLineExpected(string filePath, string expectedLogLine, LogRecord actualValue, char delimeter = DefaultDelimeter)
  86. {
  87. Assert.Equal(filePath, actualValue.FilePath);
  88. AssertLogLineExpected(expectedLogLine, actualValue, delimeter);
  89. }
  90. protected static void AssertLogLineExpected(string expectedLogLine, LogRecord actualValue, char delimeter = DefaultDelimeter)
  91. {
  92. string[] expected = expectedLogLine
  93. .Split(delimeter)
  94. .Select(x => x.TrimStart('"').TrimEnd('"').Replace("\r\n","\n"))
  95. .ToArray();
  96. Assert.Equal(expected, actualValue.LogFields);
  97. }
  98. private Notification<LogRecord> GetNextNotification(BlockingCollection<Notification<LogRecord>> observedEvents)
  99. {
  100. Notification<LogRecord> observed;
  101. Assert.True(observedEvents.TryTake(out observed, TimeoutTimeSpan), "Timed out waiting for line");
  102. return observed;
  103. }
  104. }
  105. public class Given_empty_file : FileTailerTestsBase
  106. {
  107. private readonly CsvLogTailer sut;
  108. private bool ignoreExceptions;
  109. public Given_empty_file()
  110. {
  111. LogFilePath = Path.GetTempFileName();
  112. sut = new CsvLogTailer();
  113. sut.Exceptions.Subscribe(ex =>
  114. {
  115. if (!ignoreExceptions)
  116. ObservedEvents.CompleteAdding();
  117. Console.WriteLine(ex);
  118. });
  119. }
  120. [Fact]
  121. public void CanObserveLogWithNoException()
  122. {
  123. TailerSubscription = sut.Tail(LogFilePath).MaintainObservedEventsCollection(ObservedEvents);
  124. string logLine = CreateLogLine(exception: null);
  125. Write(logLine);
  126. LogRecord next = GetNext(ObservedEvents);
  127. AssertLogLineExpected(logLine, next);
  128. }
  129. [Fact]
  130. public void CanObserveLogWithSingleLineException()
  131. {
  132. TailerSubscription = sut.Tail(LogFilePath).MaintainObservedEventsCollection(ObservedEvents);
  133. string logLine = CreateLogLine(exception: "foo");
  134. Write(logLine);
  135. LogRecord next = GetNext(ObservedEvents);
  136. AssertLogLineExpected(logLine, next);
  137. }
  138. [Fact]
  139. public void CanObserveLogWithColumns()
  140. {
  141. var expectedColumns = new[] { "A", "B" };
  142. TailerSubscription = sut.Tail(LogFilePath, expectedColumns).MaintainObservedEventsCollection(ObservedEvents);
  143. Write(CreateLogLine());
  144. LogRecord next = GetNext(ObservedEvents);
  145. Assert.Equal(expectedColumns, next.ColumnNames);
  146. }
  147. [Theory]
  148. [InlineData("\r\n")]
  149. [InlineData("\n")]
  150. public void CanObserveLogWithMultiLineException(string newline)
  151. {
  152. TailerSubscription = sut.Tail(LogFilePath).MaintainObservedEventsCollection(ObservedEvents);
  153. string quote = "\"";
  154. string logLine = CreateLogLine(exception: quote + "foo" + newline + "bar" + quote);
  155. Write(logLine);
  156. LogRecord next = GetNext(ObservedEvents);
  157. AssertLogLineExpected(logLine, next);
  158. }
  159. [Fact]
  160. public void WillMakeBestEffortToRecoverFromIncompleteQuotedValues_1()
  161. {
  162. TailerSubscription = sut.Tail(LogFilePath).MaintainObservedEventsCollection(ObservedEvents);
  163. string logLine1 = CreateLogLine(logMessage: "\"Bad message 1 without trailing quote");
  164. string logLine2 = CreateLogLine(logMessage: "Good message 1");
  165. ignoreExceptions = true;
  166. Write(logLine1);
  167. Write(logLine2);
  168. LogRecord goodLog1 = GetNext(ObservedEvents);
  169. AssertLogLineExpected(logLine2, goodLog1);
  170. }
  171. [Fact]
  172. public void WillMakeBestEffortToRecoverFromIncompleteQuotedValues_3()
  173. {
  174. TailerSubscription = sut.Tail(LogFilePath).MaintainObservedEventsCollection(ObservedEvents);
  175. string logLine1 = CreateLogLine(logMessage: "\"Bad message 1 without trailing quote");
  176. string logLine2 = CreateLogLine(logMessage: "Good message 1");
  177. string logLine3 = CreateLogLine(logMessage: "\"Another bad message \n with new lines \r\n also");
  178. string logLine4 = CreateLogLine(logMessage: "\"One more bad one to be sure");
  179. string logLine5 = CreateLogLine(logMessage: "Good message 2");
  180. ignoreExceptions = true;
  181. Write(logLine1);
  182. Write(logLine2);
  183. Write(logLine3);
  184. Write(logLine4);
  185. Write(logLine5);
  186. // Note: The first (good) message is also thrown away as the parser will attempt to parse all 3 lines together.
  187. // On error, the recovery mechanism will move the current line down the file until it can parse all the content successfully.
  188. // Because normal use case will be for the parser to parse new lines at the end of the file as they appear, this means we
  189. // should be able to skip poorly formatted lines without too much loss. Parsing a whole file for the first time though
  190. // could result in many logs not being picked up
  191. //LogRecord goodLog1 = GetNext(ObservedEvents);
  192. //AssertLogLineExpected(logLine2, goodLog1);
  193. LogRecord goodLog2 = GetNext(ObservedEvents);
  194. AssertLogLineExpected(logLine5, goodLog2);
  195. }
  196. [Theory]
  197. [InlineData(null)]
  198. [InlineData("single line exception")]
  199. [InlineData("\"multi \n line \r\n exception\"")]
  200. public void CanObserveMultipleLogs(string exception)
  201. {
  202. TailerSubscription = sut.Tail(LogFilePath).MaintainObservedEventsCollection(ObservedEvents);
  203. string logLine1 = CreateLogLine(logMessage: "message 1", exception: exception);
  204. string logLine2 = CreateLogLine(logMessage: "message 2", exception: exception);
  205. Write(logLine1);
  206. LogRecord first = GetNext(ObservedEvents);
  207. AssertLogLineExpected(logLine1, first);
  208. Write(logLine2);
  209. LogRecord second = GetNext(ObservedEvents);
  210. AssertLogLineExpected(logLine2, second);
  211. }
  212. [Theory]
  213. [InlineData(null)]
  214. [InlineData("single line exception")]
  215. [InlineData("\"multi \n line \r\n exception\"")]
  216. public void CanObserveMultipleLogs_WrittenInQuickSuccession(string exception)
  217. {
  218. TailerSubscription = sut.Tail(LogFilePath).MaintainObservedEventsCollection(ObservedEvents);
  219. string logLine1 = CreateLogLine(logMessage: "message 1", exception: exception);
  220. string logLine2 = CreateLogLine(logMessage: "message 2", exception: exception);
  221. string logLine3 = CreateLogLine(logMessage: "message 3", exception: exception);
  222. Write(logLine1);
  223. Write(logLine2);
  224. Write(logLine3);
  225. AssertLogLineExpected(logLine1, GetNext(ObservedEvents));
  226. AssertLogLineExpected(logLine2, GetNext(ObservedEvents));
  227. AssertLogLineExpected(logLine3, GetNext(ObservedEvents));
  228. }
  229. [Fact]
  230. public void CanStoreLastPositionMetadataForEachLog()
  231. {
  232. var settings = new CsvLogTailerSettings
  233. {
  234. FileOrDirectoryPath = LogFilePath,
  235. BookmarkRepositoryUpdateFrequency = TimeSpan.FromSeconds(0.01)
  236. };
  237. var positionRepository = new FakeLogFileBookmarkRepository();
  238. TailerSubscription = sut.Tail(settings, positionRepository).MaintainObservedEventsCollection(ObservedEvents);
  239. string logLine1 = CreateLogLine(logMessage: "message 1");
  240. string logLine2 = CreateLogLine(logMessage: "message 2");
  241. Write(logLine1);
  242. LogRecord first = GetNext(ObservedEvents);
  243. LogFileBookmark metadataAfterFirstLog;
  244. Assert.True(positionRepository.BookmarksSeenCollection.TryTake(out metadataAfterFirstLog, TimeoutTimeSpan));
  245. Write(logLine2);
  246. LogRecord second = GetNext(ObservedEvents);
  247. LogFileBookmark metadataAfterSecondLog;
  248. Assert.True(positionRepository.BookmarksSeenCollection.TryTake(out metadataAfterSecondLog, TimeoutTimeSpan));
  249. Assert.Equal(DateTime.Parse(first.LogFields[0]), metadataAfterFirstLog.LogDateTime);
  250. Assert.Equal(DateTime.Parse(second.LogFields[0]), metadataAfterSecondLog.LogDateTime);
  251. }
  252. [Fact]
  253. public void LastPositionMetadataIsOnlyUpdatedIfObserverSuccessfullyHandlesLogRecord()
  254. {
  255. var settings = new CsvLogTailerSettings { FileOrDirectoryPath = LogFilePath, BookmarkRepositoryUpdateFrequency = TimeSpan.FromSeconds(0.1) };
  256. var positionRepository = new FakeLogFileBookmarkRepository();
  257. Exception observedError = null;
  258. LogRecord firstObservedLog = null;
  259. var dummyException = new Exception("It's an exception dummy");
  260. TailerSubscription = sut.Tail(settings, positionRepository)
  261. .Subscribe(
  262. logRecord =>
  263. {
  264. if (firstObservedLog == null)
  265. firstObservedLog = logRecord;
  266. else
  267. throw dummyException;
  268. },
  269. error => observedError = error);
  270. string logLine1 = CreateLogLine(logMessage: "message 1", dateTimeString: "2012-06-01 18:01:00");
  271. string logLine2 = CreateLogLine(logMessage: "message 2", dateTimeString: "2012-06-01 18:02:00");
  272. Write(logLine1);
  273. LogFileBookmark metadataAfterFirstLog;
  274. Assert.True(positionRepository.BookmarksSeenCollection.TryTake(out metadataAfterFirstLog, TimeoutTimeSpan));
  275. Assert.Equal(firstObservedLog.LogDateTime, metadataAfterFirstLog.LogDateTime);
  276. Write(logLine2);
  277. // Note: Asserting we don't get any update after the exception:
  278. LogFileBookmark metadataAfterSecondLog;
  279. Assert.False(positionRepository.BookmarksSeenCollection.TryTake(out metadataAfterSecondLog, TimeoutTimeSpan));
  280. Assert.NotEqual(dummyException, observedError);
  281. }
  282. [Fact]
  283. public void CanStartTailingFileFromLastPosition()
  284. {
  285. var settings = new CsvLogTailerSettings
  286. {
  287. FileOrDirectoryPath = LogFilePath,
  288. BookmarkRepositoryUpdateFrequency = TimeSpan.FromSeconds(0.1)
  289. };
  290. var positionRepository = new FakeLogFileBookmarkRepository();
  291. TailerSubscription = sut.Tail(settings, positionRepository).MaintainObservedEventsCollection(ObservedEvents);
  292. var dateTime = new DateTime(2012, 11, 18, 12, 1, 0);
  293. var secondLogDateTimeString = dateTime.AddMinutes(1).ToString();
  294. string logLine1 = CreateLogLine(logMessage: "message 1", dateTimeString: dateTime.ToString());
  295. string logLine2 = CreateLogLine(logMessage: "message 2", dateTimeString: secondLogDateTimeString);
  296. string logLine3 = CreateLogLine(logMessage: "message 3", dateTimeString: dateTime.AddMinutes(2).ToString());
  297. string logLine4 = CreateLogLine(logMessage: "message 4", dateTimeString: dateTime.AddMinutes(3).ToString());
  298. Write(logLine1);
  299. Write(logLine2);
  300. LogFileBookmark metadataForSecondLog = positionRepository.BookmarksSeen
  301. .Do(x => Console.WriteLine("Saw {0}", x.LogDateTime))
  302. .Where(x => x.LogDateTime.ToString() == secondLogDateTimeString)
  303. .Timeout(TimeoutTimeSpan)
  304. .First();
  305. TailerSubscription.Dispose();
  306. Write(logLine3);
  307. Write(logLine4);
  308. // Now tail the file again, but start from position given by 'metadataAfterSecondLog' variable
  309. // We will start reading from the same DateTime as the second log so we should read logs 2, 3 + 4.
  310. // The reason we read log 2 again is because of the case where another log came in with the same timestamp
  311. // as log 2 but was not processed before the tailer shut down. So, we favour the occasional duplicate log
  312. // over the possibly of missing some
  313. var positionRepository2 = new FakeLogFileBookmarkRepository();
  314. positionRepository2.AddOrUpdate(metadataForSecondLog);
  315. var observeredEvents2 = new BlockingCollection<Notification<LogRecord>>();
  316. using (sut.Tail(settings, positionRepository2).MaintainObservedEventsCollection(observeredEvents2))
  317. {
  318. LogRecord duplicate = GetNext(observeredEvents2);
  319. AssertLogLineExpected(logLine2, duplicate);
  320. LogRecord first = GetNext(observeredEvents2);
  321. AssertLogLineExpected(logLine3, first);
  322. LogRecord second = GetNext(observeredEvents2);
  323. AssertLogLineExpected(logLine4, second);
  324. }
  325. }
  326. private class FakeLogFileBookmarkRepository : ILogFileBookmarkRepository
  327. {
  328. public readonly Dictionary<string, LogFileBookmark> Metadata = new Dictionary<string, LogFileBookmark>();
  329. private readonly Subject<LogFileBookmark> bookmarksSubject = new Subject<LogFileBookmark>();
  330. private readonly BlockingCollection<LogFileBookmark> bookmarksSeenCollection = new BlockingCollection<LogFileBookmark>();
  331. public LogFileBookmark Get(string filePath)
  332. {
  333. return Metadata.ContainsKey(filePath) ? Metadata[filePath] : null;
  334. }
  335. public Subject<LogFileBookmark> BookmarksSeen
  336. {
  337. get { return bookmarksSubject; }
  338. }
  339. public BlockingCollection<LogFileBookmark> BookmarksSeenCollection
  340. {
  341. get { return bookmarksSeenCollection; }
  342. }
  343. public void AddOrUpdate(LogFileBookmark bookmark)
  344. {
  345. if (Metadata.ContainsKey(bookmark.FilePath))
  346. Metadata[bookmark.FilePath] = bookmark;
  347. else
  348. Metadata.Add(bookmark.FilePath, bookmark);
  349. bookmarksSeenCollection.Add(bookmark);
  350. bookmarksSubject.OnNext(bookmark);
  351. }
  352. }
  353. }
  354. public class Given_no_file_exists : FileTailerTestsBase
  355. {
  356. private readonly CsvLogTailer sut;
  357. public Given_no_file_exists()
  358. {
  359. sut = new CsvLogTailer();
  360. sut.Exceptions.Subscribe(ex => Console.WriteLine(ex));
  361. }
  362. [Fact]
  363. public void WhenTailed_WillBlockUntilFileCreatedAndFirstLineWritten()
  364. {
  365. LogFilePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
  366. TailerSubscription = sut.Tail(LogFilePath).MaintainObservedEventsCollection(ObservedEvents);
  367. var waiting = new ManualResetEventSlim();
  368. Task<LogRecord> task = Task.Factory.StartNew(() =>
  369. {
  370. waiting.Set();
  371. return GetNext(ObservedEvents);
  372. });
  373. // Make sure Task has spun up
  374. Assert.True(waiting.Wait(TimeoutTimeSpan));
  375. // Make sure (as much as possible anyway), that the task continues and starts waiting in GetNext(ObservedEvents) call
  376. Thread.Sleep(200);
  377. using (File.Create(LogFilePath));
  378. string logLine = CreateLogLine();
  379. Write(logLine);
  380. task.Wait();
  381. LogRecord next = task.Result;
  382. AssertLogLineExpected(logLine, next);
  383. }
  384. }
  385. public class Given_empty_directory : FileTailerTestsBase
  386. {
  387. private readonly string logsDirectory;
  388. public Given_empty_directory()
  389. {
  390. logsDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
  391. Directory.CreateDirectory(logsDirectory);
  392. }
  393. protected override void DisposeManagedObjects()
  394. {
  395. base.DisposeManagedObjects();
  396. if (Directory.Exists(logsDirectory))
  397. Directory.Delete(logsDirectory, true);
  398. }
  399. [Fact]
  400. public void CanTailAllFilesInDirectory_WithNoFilter()
  401. {
  402. var sut = new CsvLogTailer();
  403. TailerSubscription = sut.Tail(logsDirectory, LogColumns).MaintainObservedEventsCollection(ObservedEvents);
  404. string file1 = CreateLogFile(logsDirectory, "logfile1.txt");
  405. string file2 = CreateLogFile(logsDirectory, "logfile2.txt");
  406. string file1Line1 = CreateLogLine(logMessage: "File1-Line1");
  407. string file1Line2 = CreateLogLine(logMessage: "File1-Line2");
  408. string file2Line1 = CreateLogLine(logMessage: "File2-Line1");
  409. string file2Line2 = CreateLogLine(logMessage: "File2-Line2");
  410. Write(file1Line1, file1);
  411. Write(file2Line1, file2);
  412. Write(file1Line2, file1);
  413. Write(file2Line2, file2);
  414. var logRecords = new List<LogRecord>
  415. {
  416. GetNext(ObservedEvents),
  417. GetNext(ObservedEvents),
  418. GetNext(ObservedEvents),
  419. GetNext(ObservedEvents)
  420. };
  421. int messageColIndex = Array.IndexOf(LogColumns, "Message");
  422. AssertLogLineExpected(file1, file1Line1, logRecords.Single(x => x.LogFields[messageColIndex] == "File1-Line1"));
  423. AssertLogLineExpected(file1, file1Line2, logRecords.Single(x => x.LogFields[messageColIndex] == "File1-Line2"));
  424. AssertLogLineExpected(file2, file2Line1, logRecords.Single(x => x.LogFields[messageColIndex] == "File2-Line1"));
  425. AssertLogLineExpected(file2, file2Line2, logRecords.Single(x => x.LogFields[messageColIndex] == "File2-Line2"));
  426. }
  427. private static string CreateLogFile(string logsDirectory, string fileName)
  428. {
  429. var file1 = Path.Combine(logsDirectory, fileName);
  430. using (File.Create(file1))
  431. ;
  432. return file1;
  433. }
  434. [Fact]
  435. public void CanTailAllFilesInDirectory_WithAFilter()
  436. {
  437. const string directoryFilter = "*2.txt";
  438. var sut = new CsvLogTailer();
  439. TailerSubscription = sut.Tail(logsDirectory, directoryFilter, LogColumns).MaintainObservedEventsCollection(ObservedEvents);
  440. string file1 = CreateLogFile(logsDirectory, "logfile1.txt");
  441. string file2 = CreateLogFile(logsDirectory, "logfile2.txt");
  442. string file1Line1 = CreateLogLine(logMessage: "File1-Line1");
  443. string file1Line2 = CreateLogLine(logMessage: "File1-Line2");
  444. string file2Line1 = CreateLogLine(logMessage: "File2-Line1");
  445. string file2Line2 = CreateLogLine(logMessage: "File2-Line2");
  446. Write(file1Line1, file1);
  447. Write(file2Line1, file2);
  448. Write(file1Line2, file1);
  449. Write(file2Line2, file2);
  450. var logRecords = new List<LogRecord>
  451. {
  452. GetNext(ObservedEvents),
  453. GetNext(ObservedEvents)
  454. };
  455. int messageColIndex = Array.IndexOf(LogColumns, "Message");
  456. AssertLogLineExpected(file2, file2Line1, logRecords.Single(x => x.LogFields[messageColIndex] == "File2-Line1"));
  457. AssertLogLineExpected(file2, file2Line2, logRecords.Single(x => x.LogFields[messageColIndex] == "File2-Line2"));
  458. // Make sure we don't receive any events for logfile1:
  459. Thread.Sleep(500);
  460. Assert.Equal(0, ObservedEvents.Count);
  461. }
  462. [Fact]
  463. public void CanTailAllFilesInDirectory_AndAssignLogColumnsPerFileTailed()
  464. {
  465. var logFile1Columns = new[] { "A" };
  466. var logFile2Columns = new[] { "B" };
  467. Func<string, string[]> columnsProvider = file => (Path.GetFileName(file) == "logfile1.txt") ? logFile1Columns : logFile2Columns;
  468. var sut = new CsvLogTailer();
  469. var settings = new CsvLogTailerSettings
  470. {
  471. FileOrDirectoryPath = logsDirectory,
  472. ColumnNamesProvider = columnsProvider
  473. };
  474. TailerSubscription = sut.Tail(settings).MaintainObservedEventsCollection(ObservedEvents);
  475. string file1 = CreateLogFile(logsDirectory, "logfile1.txt");
  476. string file2 = CreateLogFile(logsDirectory, "logfile2.txt");
  477. string file1Line1 = CreateLogLine(logMessage: "File1-Line1");
  478. string file2Line1 = CreateLogLine(logMessage: "File2-Line1");
  479. Write(file1Line1, file1);
  480. Write(file2Line1, file2);
  481. var logRecords = new List<LogRecord>
  482. {
  483. GetNext(ObservedEvents),
  484. GetNext(ObservedEvents)
  485. };
  486. int messageColIndex = Array.IndexOf(LogColumns, "Message");
  487. var logFile1Log = logRecords.Single(x => x.LogFields[messageColIndex] == "File1-Line1");
  488. Assert.Equal(logFile1Columns, logFile1Log.ColumnNames);
  489. var logFile2Log = logRecords.Single(x => x.LogFields[messageColIndex] == "File2-Line1");
  490. Assert.Equal(logFile2Columns, logFile2Log.ColumnNames);
  491. }
  492. }
  493. }
  494. }