-
Notifications
You must be signed in to change notification settings - Fork 184
Expand file tree
/
Copy pathCSVColumnizerTest.cs
More file actions
628 lines (492 loc) · 30.2 KB
/
Copy pathCSVColumnizerTest.cs
File metadata and controls
628 lines (492 loc) · 30.2 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
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
using System.Reflection;
using ColumnizerLib;
using CsvColumnizer;
using LogExpert.Core.Classes.Log;
using LogExpert.Core.Entities;
using LogExpert.Core.Enums;
using Moq;
using NUnit.Framework;
namespace LogExpert.Tests.ColumnizerTests;
[TestFixture]
public class CSVColumnizerTest
{
[SetUp]
public void Setup ()
{
// Reset singleton for testing (same pattern as PluginRegistryTests)
ResetPluginRegistrySingleton();
// Initialize plugin registry with proper test directory
var testDataPath = Path.Join(Path.GetTempPath(), "LogExpertTests", Guid.NewGuid().ToString());
_ = Directory.CreateDirectory(testDataPath);
var pluginRegistry = PluginRegistry.PluginRegistry.Create(testDataPath, 250);
// Verify the local file system plugin is registered
var localPlugin = pluginRegistry.FindFileSystemForUri(@"C:\test.txt");
Assert.That(localPlugin, Is.Not.Null, "Local file system plugin not registered!");
}
[TearDown]
public void TearDown ()
{
ResetPluginRegistrySingleton();
}
/// <summary>
/// Uses reflection to reset the singleton instance for testing.
/// This ensures each test starts with a fresh PluginRegistry state.
/// </summary>
private static void ResetPluginRegistrySingleton ()
{
var instanceField = typeof(PluginRegistry.PluginRegistry).GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic);
instanceField?.SetValue(null, null);
}
/// <summary>
/// Sets a private field on the CsvColumnizer via reflection.
/// Used to reach edge-case states (e.g. _isValidCsv=true with _firstLine=null)
/// that are not reachable through the public API alone.
/// </summary>
private static void SetPrivateField (CsvColumnizer.CsvColumnizer columnizer, string fieldName, object? value)
{
var field = typeof(CsvColumnizer.CsvColumnizer).GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
Assert.That(field, Is.Not.Null, $"Field '{fieldName}' not found on CsvColumnizer");
field!.SetValue(columnizer, value);
}
[Test]
public void Selected_HasFieldNames_FirstLineNull_FallsBackToCallback ()
{
// Arrange: _isValidCsv=true but _firstLine=null (edge case reachable via legacy adapter)
CsvColumnizer.CsvColumnizer columnizer = new();
SetPrivateField(columnizer, "_isValidCsv", true);
var callbackLine = new CsvLogLine("name;age;city", 0);
var mockCallback = new Mock<ILogLineMemoryColumnizerCallback>();
_ = mockCallback.Setup(c => c.GetLogLineMemory(0)).Returns(callbackLine);
// Act
columnizer.Selected(mockCallback.Object);
// Assert: columns detected from callback line
Assert.That(columnizer.GetColumnCount(), Is.EqualTo(3));
Assert.That(columnizer.GetColumnNames(), Is.EqualTo(["name", "age", "city"]));
}
[Test]
public void Selected_HasFieldNames_NoLineAvailable_FallsBackToTextColumn ()
{
// Arrange: _isValidCsv=true, _firstLine=null, callback returns null
CsvColumnizer.CsvColumnizer columnizer = new();
SetPrivateField(columnizer, "_isValidCsv", true);
var mockCallback = new Mock<ILogLineMemoryColumnizerCallback>();
_ = mockCallback.Setup(c => c.GetLogLineMemory(0)).Returns((ILogLineMemory?)null);
// Act
columnizer.Selected(mockCallback.Object);
// Assert: graceful fallback to single "Text" column
Assert.That(columnizer.GetColumnCount(), Is.EqualTo(1));
Assert.That(columnizer.GetColumnNames(), Is.EqualTo(["Text"]));
}
[Test]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Unit Tests")]
public void SplitLine_EmptyFullLine_ReturnsSingleColumn ()
{
// Arrange: columnizer in valid-CSV mode with columns established
CsvColumnizer.CsvColumnizer columnizer = new();
_ = columnizer.PreProcessLine("a;b;c".AsMemory(), 0, 0);
var mockCallback = new Mock<ILogLineMemoryColumnizerCallback>();
columnizer.Selected(mockCallback.Object);
// Act: split a line with empty FullLine
var emptyLine = new CsvLogLine(ReadOnlyMemory<char>.Empty, 1);
var result = columnizer.SplitLine(null, emptyLine);
// Assert: single column with empty content, no crash
Assert.That(result.ColumnValues, Has.Length.EqualTo(1));
}
[Test]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Unit Tests")]
public void SplitLine_ValidCsvLine_ReturnsCorrectColumns ()
{
// Arrange: columnizer in valid-CSV mode
CsvColumnizer.CsvColumnizer columnizer = new();
_ = columnizer.PreProcessLine("name;age;city".AsMemory(), 0, 0);
var mockCallback = new Mock<ILogLineMemoryColumnizerCallback>();
columnizer.Selected(mockCallback.Object);
// Act: split a normal data line
var dataLine = new CsvLogLine("Alice;30;London", 1);
var result = columnizer.SplitLine(null, dataLine);
// Assert: three columns with correct values
Assert.That(result.ColumnValues, Has.Length.EqualTo(3));
Assert.That(result.ColumnValues[0].FullValue.ToString(), Is.EqualTo("Alice"));
Assert.That(result.ColumnValues[1].FullValue.ToString(), Is.EqualTo("30"));
Assert.That(result.ColumnValues[2].FullValue.ToString(), Is.EqualTo("London"));
}
[Test]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Unit Tests")]
public void SplitLine_BadCsvData_ReturnsSingleColumnInsteadOfCrash ()
{
// Arrange: columnizer with semicolon delimiter (default) but line uses commas with quoted fields
// This triggers CsvHelper.BadDataException because quotes appear mid-field in semicolon mode
CsvColumnizer.CsvColumnizer columnizer = new();
_ = columnizer.PreProcessLine("header1;header2;header3".AsMemory(), 0, 0);
var mockCallback = new Mock<ILogLineMemoryColumnizerCallback>();
columnizer.Selected(mockCallback.Object);
// Act: line with comma-separated data containing quoted fields — bad data for semicolon delimiter
var badLine = new CsvLogLine("6,6774DC1dB00BD11,\"Farmer, Edwards and Andrade\",http://wolfe-boyd.com/,Norfolk Island,Virtual leadingedge benchmark,2003,Mental Health Care,3503", 1);
var result = columnizer.SplitLine(null, badLine);
// Assert: returns single-column fallback, no crash
Assert.That(result.ColumnValues, Has.Length.EqualTo(1));
Assert.That(result.ColumnValues[0].FullValue.ToString(), Does.Contain("6774DC1dB00BD11"));
}
[TestCase(@".\TestData\organizations-10000.csv", new[] { "Index", "Organization Id", "Name", "Website", "Country", "Description", "Founded", "Industry", "Number of employees" }, ReaderType.System)]
[TestCase(@".\TestData\organizations-1000.csv", new[] { "Index", "Organization Id", "Name", "Website", "Country", "Description", "Founded", "Industry", "Number of employees" }, ReaderType.System)]
[TestCase(@".\TestData\people-10000.csv", new[] { "Index", "User Id", "First Name", "Last Name", "Sex", "Email", "Phone", "Date of birth", "Job Title" }, ReaderType.System)]
public void Instantiat_CSVFile_BuildCorrectColumnizer (string filename, string[] expectedHeaders, ReaderType readerType)
{
CsvColumnizer.CsvColumnizer csvColumnizer = new();
var path = Path.Join(AppDomain.CurrentDomain.BaseDirectory, filename);
LogfileReader reader = new(path, new EncodingOptions(), true, 40, 50, new MultiFileOptions(), readerType, PluginRegistry.PluginRegistry.Instance, 500);
reader.ReadFiles();
var line = reader.GetLogLineMemory(0);
IColumnizedLogLineMemory logline = new ColumnizedLogLine();
if (line != null)
{
logline = csvColumnizer.SplitLine(null, line);
}
var expectedResult = string.Join(",", expectedHeaders);
Assert.That(logline.LogLine.FullLine.ToString(), Is.EqualTo(expectedResult));
}
#region Line reading tests for files with no trailing newline
[Test]
public void LogfileReader_SemicolonCsv_ReadsAllLines ()
{
// semicolon.csv: CRLF between lines, no trailing newline on last line
var path = Path.Join(AppDomain.CurrentDomain.BaseDirectory, @".\TestData\semicolon.csv");
LogfileReader reader = new(path, new EncodingOptions(), true, 40, 50, new MultiFileOptions(), ReaderType.System, PluginRegistry.PluginRegistry.Instance, 500);
reader.ReadFiles();
Assert.That(reader.LineCount, Is.EqualTo(2), $"semicolon.csv should have 2 lines, got {reader.LineCount}");
}
[Test]
public void LogfileReader_TabCsv_ReadsAllLines ()
{
// tab.csv: header + 1 data line, CRLF between lines
var path = Path.Join(AppDomain.CurrentDomain.BaseDirectory, @".\TestData\tab.csv");
LogfileReader reader = new(path, new EncodingOptions(), true, 40, 50, new MultiFileOptions(), ReaderType.System, PluginRegistry.PluginRegistry.Instance, 500);
reader.ReadFiles();
Assert.That(reader.LineCount, Is.EqualTo(2), $"tab.csv should have 2 lines, got {reader.LineCount}");
}
[Test]
public void LogfileReader_CsvTest01_ReadsAllLines ()
{
// CsvTest_01.csv: CR line endings, no trailing newline on last line
var path = Path.Join(AppDomain.CurrentDomain.BaseDirectory, @".\TestData\CsvTest_01.csv");
LogfileReader reader = new(path, new EncodingOptions(), true, 40, 50, new MultiFileOptions(), ReaderType.System, PluginRegistry.PluginRegistry.Instance, 500);
reader.ReadFiles();
Assert.That(reader.LineCount, Is.EqualTo(31), $"CsvTest_01.csv should have 31 lines (header + 30 data), got {reader.LineCount}");
}
[Test]
public void LogfileReader_SemicolonCsv_WithPreProcess_DataLinesVisible ()
{
// With CsvColumnizer as PreProcessColumnizer, header should be dropped, data line should remain
CsvColumnizer.CsvColumnizer csvColumnizer = new();
var path = Path.Join(AppDomain.CurrentDomain.BaseDirectory, @".\TestData\semicolon.csv");
LogfileReader reader = new(path, new EncodingOptions(), true, 40, 50, new MultiFileOptions(), ReaderType.System, PluginRegistry.PluginRegistry.Instance, 500);
reader.PreProcessColumnizer = csvColumnizer;
reader.ReadFiles();
Assert.That(reader.LineCount, Is.GreaterThan(0), $"Should have data lines after header is dropped, got {reader.LineCount}");
var line = reader.GetLogLineMemory(0);
Assert.That(line, Is.Not.Null);
Assert.That(line.FullLine.IsEmpty, Is.False);
Assert.That(line.FullLine.ToString(), Does.Contain("2021-12-12"));
}
[Test]
public void LogfileReader_TabCsv_WithPreProcess_DataLinesVisible ()
{
CsvColumnizer.CsvColumnizer csvColumnizer = new();
var path = Path.Join(AppDomain.CurrentDomain.BaseDirectory, @".\TestData\tab.csv");
LogfileReader reader = new(path, new EncodingOptions(), true, 40, 50, new MultiFileOptions(), ReaderType.System, PluginRegistry.PluginRegistry.Instance, 500);
reader.PreProcessColumnizer = csvColumnizer;
reader.ReadFiles();
Assert.That(reader.LineCount, Is.GreaterThan(0), $"Should have data lines after header is dropped, got {reader.LineCount}");
var line = reader.GetLogLineMemory(0);
Assert.That(line, Is.Not.Null);
Assert.That(line.FullLine.IsEmpty, Is.False);
Assert.That(line.FullLine.ToString(), Does.Contain("2023-05-01"));
}
[Test]
public void LogfileReader_SemicolonCsv_SimulateGuiReload_DataLinesVisible ()
{
// Simulate the GUI reload flow:
// GUI creates a NEW reader with PreProcess set from the start (like Reload does)
var path = Path.Join(AppDomain.CurrentDomain.BaseDirectory, @".\TestData\semicolon.csv");
CsvColumnizer.CsvColumnizer csvColumnizer = new();
// The GUI Reload creates a brand new LogfileReader with PreProcess already set
LogfileReader reader = new(path, new EncodingOptions(), true, 40, 50, new MultiFileOptions(), ReaderType.System, PluginRegistry.PluginRegistry.Instance, 500);
reader.PreProcessColumnizer = csvColumnizer;
reader.ReadFiles();
Assert.That(reader.LineCount, Is.EqualTo(1), $"After read with PreProcess, header dropped, 1 data line. Got {reader.LineCount}");
var line = reader.GetLogLineMemory(0);
Assert.That(line, Is.Not.Null, "First visible line should not be null");
Assert.That(line.FullLine.IsEmpty, Is.False, "First visible line should not be empty");
Assert.That(line.FullLine.ToString(), Does.Contain("2021-12-12"), "Should be the data line");
// Simulate GUI calling Selected() (normally done in SetColumnizerInternal)
var callbackMock = new Mock<ILogLineMemoryColumnizerCallback>();
callbackMock.Setup(c => c.GetLogLineMemory(0)).Returns(reader.GetLogLineMemory(0));
csvColumnizer.Selected(callbackMock.Object);
// Verify CsvColumnizer state after Selected()
Assert.That(csvColumnizer.GetColumnCount(), Is.EqualTo(3), "Should have 3 columns (Date, Level, Message)");
var names = csvColumnizer.GetColumnNames();
Assert.That(names[0], Is.EqualTo("Date"));
Assert.That(names[1], Is.EqualTo("Level"));
Assert.That(names[2], Is.EqualTo("Message"));
}
[Test]
public void LogfileReader_SemicolonCsv_StartMonitoring_LoadsCorrectly ()
{
// Test the actual StartMonitoring flow (async, like the GUI)
var path = Path.Join(AppDomain.CurrentDomain.BaseDirectory, @".\TestData\semicolon.csv");
CsvColumnizer.CsvColumnizer csvColumnizer = new();
using ManualResetEventSlim loadingDone = new(false);
LogfileReader reader = new(path, new EncodingOptions(), true, 40, 50, new MultiFileOptions(), ReaderType.System, PluginRegistry.PluginRegistry.Instance, 500);
reader.PreProcessColumnizer = csvColumnizer;
reader.LoadingFinished += (_, _) => loadingDone.Set();
reader.StartMonitoring();
Assert.That(loadingDone.Wait(TimeSpan.FromSeconds(5)), Is.True, "Loading should finish within 5 seconds");
Assert.That(reader.LineCount, Is.EqualTo(1), $"Header dropped, 1 data line expected. Got {reader.LineCount}");
var line = reader.GetLogLineMemory(0);
Assert.That(line, Is.Not.Null);
Assert.That(line.FullLine.ToString(), Does.Contain("2021-12-12"));
// Simulate GUI calling Selected()
var callbackMock = new Mock<ILogLineMemoryColumnizerCallback>();
callbackMock.Setup(c => c.GetLogLineMemory(0)).Returns(reader.GetLogLineMemory(0));
csvColumnizer.Selected(callbackMock.Object);
// CsvColumnizer should have valid state
Assert.That(csvColumnizer.GetColumnCount(), Is.EqualTo(3));
reader.StopMonitoring();
}
[Test]
public void LogfileReader_CsvTest01_StartMonitoring_LoadsCorrectly ()
{
// CsvTest_01.csv: CR line endings, comma delimiter, 31 lines
var path = Path.Join(AppDomain.CurrentDomain.BaseDirectory, @".\TestData\CsvTest_01.csv");
CsvColumnizer.CsvColumnizer csvColumnizer = new();
using ManualResetEventSlim loadingDone = new(false);
LogfileReader reader = new(path, new EncodingOptions(), true, 40, 50, new MultiFileOptions(), ReaderType.System, PluginRegistry.PluginRegistry.Instance, 500);
reader.PreProcessColumnizer = csvColumnizer;
reader.LoadingFinished += (_, _) => loadingDone.Set();
reader.StartMonitoring();
Assert.That(loadingDone.Wait(TimeSpan.FromSeconds(5)), Is.True, "Loading should finish within 5 seconds");
// CsvTest_01.csv uses comma delimiter but default config uses semicolon.
// PreProcessLine still drops line 0 (regardless of delimiter match).
Assert.That(reader.LineCount, Is.EqualTo(30), $"Header dropped, 30 data lines expected. Got {reader.LineCount}");
// Verify _firstLine survived allocator block recycling (the CsvLogLine fix)
var callbackMock = new Mock<ILogLineMemoryColumnizerCallback>();
callbackMock.Setup(c => c.GetLogLineMemory(0)).Returns(reader.GetLogLineMemory(0));
csvColumnizer.Selected(callbackMock.Object);
// With auto-detection, comma delimiter should be detected, giving 18 columns
Assert.That(csvColumnizer.GetColumnCount(), Is.EqualTo(18), $"Should have 18 columns (auto-detected comma delimiter). Got {csvColumnizer.GetColumnCount()}");
var names = csvColumnizer.GetColumnNames();
Assert.That(names[0], Is.EqualTo("policyID"), "First column should be policyID");
reader.StopMonitoring();
}
[Test]
public void LogfileReader_TabCsv_StartMonitoring_LoadsCorrectly ()
{
var path = Path.Join(AppDomain.CurrentDomain.BaseDirectory, @".\TestData\tab.csv");
CsvColumnizer.CsvColumnizer csvColumnizer = new();
using ManualResetEventSlim loadingDone = new(false);
LogfileReader reader = new(path, new EncodingOptions(), true, 40, 50, new MultiFileOptions(), ReaderType.System, PluginRegistry.PluginRegistry.Instance, 500);
reader.PreProcessColumnizer = csvColumnizer;
reader.LoadingFinished += (_, _) => loadingDone.Set();
reader.StartMonitoring();
Assert.That(loadingDone.Wait(TimeSpan.FromSeconds(5)), Is.True);
// tab.csv: header + 1 data line, header dropped
Assert.That(reader.LineCount, Is.EqualTo(1), $"Header dropped, 1 data line expected. Got {reader.LineCount}");
reader.StopMonitoring();
}
[Test]
public void LogfileReader_CommaCsv_StartMonitoring_LoadsCorrectly ()
{
var path = Path.Join(AppDomain.CurrentDomain.BaseDirectory, @".\TestData\comma.csv");
CsvColumnizer.CsvColumnizer csvColumnizer = new();
using ManualResetEventSlim loadingDone = new(false);
LogfileReader reader = new(path, new EncodingOptions(), true, 40, 50, new MultiFileOptions(), ReaderType.System, PluginRegistry.PluginRegistry.Instance, 500);
reader.PreProcessColumnizer = csvColumnizer;
reader.LoadingFinished += (_, _) => loadingDone.Set();
reader.StartMonitoring();
Assert.That(loadingDone.Wait(TimeSpan.FromSeconds(5)), Is.True, "Loading should finish within 5 seconds");
// comma.csv: header + 1 data line, comma delimiter auto-detected
Assert.That(reader.LineCount, Is.EqualTo(1), $"Header dropped, 1 data line expected. Got {reader.LineCount}");
var line = reader.GetLogLineMemory(0);
Assert.That(line, Is.Not.Null);
Assert.That(line.FullLine.ToString(), Does.Contain("comma file"));
// Simulate GUI calling Selected()
var callbackMock = new Mock<ILogLineMemoryColumnizerCallback>();
callbackMock.Setup(c => c.GetLogLineMemory(0)).Returns(reader.GetLogLineMemory(0));
csvColumnizer.Selected(callbackMock.Object);
Assert.That(csvColumnizer.GetColumnCount(), Is.EqualTo(3), "Should detect 3 columns with comma delimiter");
var names = csvColumnizer.GetColumnNames();
Assert.That(names[0], Is.EqualTo("Date"));
Assert.That(names[1], Is.EqualTo("Level"));
Assert.That(names[2], Is.EqualTo("Message"));
// Simulate GUI calling SplitLine on the data line (what the DataGridView does)
var splitResult = csvColumnizer.SplitLine(callbackMock.Object, line);
Assert.That(splitResult, Is.Not.Null, "SplitLine should not return null");
Assert.That(splitResult.ColumnValues.Length, Is.EqualTo(3), $"SplitLine should return 3 columns, got {splitResult.ColumnValues.Length}");
Assert.That(splitResult.ColumnValues[0].FullValue.ToString(), Is.EqualTo("2021-01-01"), $"Column 0 should be '2021-01-01', got '{splitResult.ColumnValues[0].FullValue}'");
Assert.That(splitResult.ColumnValues[1].FullValue.ToString(), Is.EqualTo("Error"), $"Column 1 should be 'Error', got '{splitResult.ColumnValues[1].FullValue}'");
Assert.That(splitResult.ColumnValues[2].FullValue.ToString(), Is.EqualTo("comma file"), $"Column 2 should be 'comma file', got '{splitResult.ColumnValues[2].FullValue}'");
reader.StopMonitoring();
}
[Test]
public void LogfileReader_SemicolonCsv_SplitLine_ReturnsCorrectValues ()
{
var path = Path.Join(AppDomain.CurrentDomain.BaseDirectory, @".\TestData\semicolon.csv");
CsvColumnizer.CsvColumnizer csvColumnizer = new();
using ManualResetEventSlim loadingDone = new(false);
LogfileReader reader = new(path, new EncodingOptions(), true, 40, 50, new MultiFileOptions(), ReaderType.System, PluginRegistry.PluginRegistry.Instance, 500);
reader.PreProcessColumnizer = csvColumnizer;
reader.LoadingFinished += (_, _) => loadingDone.Set();
reader.StartMonitoring();
Assert.That(loadingDone.Wait(TimeSpan.FromSeconds(5)), Is.True);
Assert.That(reader.LineCount, Is.EqualTo(1));
var line = reader.GetLogLineMemory(0);
Assert.That(line, Is.Not.Null);
Assert.That(line.FullLine.ToString(), Does.Contain("2021-12-12"));
var callbackMock = new Mock<ILogLineMemoryColumnizerCallback>();
callbackMock.Setup(c => c.GetLogLineMemory(0)).Returns(line);
csvColumnizer.Selected(callbackMock.Object);
Assert.That(csvColumnizer.GetColumnCount(), Is.EqualTo(3));
// Call SplitLine - this is what the GUI grid does to render cells
var splitResult = csvColumnizer.SplitLine(callbackMock.Object, line);
Assert.That(splitResult, Is.Not.Null);
Assert.That(splitResult.ColumnValues.Length, Is.EqualTo(3), $"Expected 3 columns, got {splitResult.ColumnValues.Length}");
Assert.That(splitResult.ColumnValues[0].FullValue.ToString(), Is.EqualTo("2021-12-12"));
Assert.That(splitResult.ColumnValues[1].FullValue.ToString(), Is.EqualTo("TRACE"));
Assert.That(splitResult.ColumnValues[2].FullValue.ToString(), Is.EqualTo("semicolon file "));
reader.StopMonitoring();
}
/// <summary>
/// Simulates the exact GUI Reload flow:
/// 1. First load happens WITHOUT PreProcessColumnizer (normal read)
/// 2. Then the CsvColumnizer is assigned and Reload triggers a re-read with PreProcess active
/// This is the "header + 1 data line" scenario the user reports as broken.
/// </summary>
[Test]
public void LogfileReader_CommaCsv_ReloadWithPreProcess_DataLineNotEmpty ()
{
var path = Path.Join(AppDomain.CurrentDomain.BaseDirectory, @".\TestData\comma.csv");
// Step 1: Initial load WITHOUT PreProcessColumnizer (like GUI first load)
CsvColumnizer.CsvColumnizer csvColumnizer = new();
using ManualResetEventSlim loadingDone = new(false);
LogfileReader reader = new(path, new EncodingOptions(), true, 40, 50, new MultiFileOptions(), ReaderType.System, PluginRegistry.PluginRegistry.Instance, 500);
// NO PreProcessColumnizer set initially
reader.LoadingFinished += (_, _) => loadingDone.Set();
reader.StartMonitoring();
Assert.That(loadingDone.Wait(TimeSpan.FromSeconds(5)), Is.True);
// Without PreProcess, both lines are visible (header + data)
Assert.That(reader.LineCount, Is.EqualTo(2), $"Without PreProcess, should see 2 lines. Got {reader.LineCount}");
// Step 2: Simulate GUI Reload - GUI creates a NEW LogfileReader with PreProcess already set
// (Reload() → LoadFile() creates a fresh LogfileReader, assigns PreProcess, then StartMonitoring)
reader.StopMonitoring();
using ManualResetEventSlim reloadDone = new(false);
LogfileReader reader2 = new(path, new EncodingOptions(), true, 40, 50, new MultiFileOptions(), ReaderType.System, PluginRegistry.PluginRegistry.Instance, 500);
reader2.PreProcessColumnizer = csvColumnizer;
reader2.LoadingFinished += (_, _) => reloadDone.Set();
reader2.StartMonitoring();
Assert.That(reloadDone.Wait(TimeSpan.FromSeconds(5)), Is.True, "Reload should finish within 5 seconds");
// Diagnostics
var bufferCount = reader2.BufferIndex.BufferCount;
Assert.That(bufferCount, Is.GreaterThan(0), "BufferIndex should have at least 1 buffer after ReadFiles");
// After reload with PreProcess, header is dropped
Assert.That(reader2.LineCount, Is.EqualTo(1), $"After reload with PreProcess, header dropped, 1 data line. Got {reader2.LineCount}. BufferCount={bufferCount}");
var line = reader2.GetLogLineMemory(0);
Assert.That(line, Is.Not.Null, "Data line should not be null after reload");
Assert.That(line.FullLine.IsEmpty, Is.False, "Data line content should NOT be empty after reload");
Assert.That(line.FullLine.ToString(), Does.Contain("comma file"), $"Data line should contain 'comma file', got: '{line.FullLine}'");
// Step 3: Simulate GUI calling Selected + SplitLine
var callbackMock = new Mock<ILogLineMemoryColumnizerCallback>();
callbackMock.Setup(c => c.GetLogLineMemory(0)).Returns(reader2.GetLogLineMemory(0));
csvColumnizer.Selected(callbackMock.Object);
Assert.That(csvColumnizer.GetColumnCount(), Is.EqualTo(3));
var splitResult = csvColumnizer.SplitLine(callbackMock.Object, line);
Assert.That(splitResult.ColumnValues.Length, Is.EqualTo(3));
Assert.That(splitResult.ColumnValues[0].FullValue.ToString(), Is.EqualTo("2021-01-01"));
Assert.That(splitResult.ColumnValues[1].FullValue.ToString(), Is.EqualTo("Error"));
Assert.That(splitResult.ColumnValues[2].FullValue.ToString(), Is.EqualTo("comma file"));
reader2.StopMonitoring();
}
/// <summary>
/// Tests the exact GUI scenario: single file (not multi), which enables the MemoryMappedFileReader.
/// The MMF reader reads raw lines without PreProcess, which can conflict with the buffer system
/// where lines are dropped.
/// </summary>
[Test]
public void LogfileReader_CommaCsv_SingleFile_WithPreProcess_DataLineNotEmpty ()
{
var path = Path.Join(AppDomain.CurrentDomain.BaseDirectory, @".\TestData\comma.csv");
CsvColumnizer.CsvColumnizer csvColumnizer = new();
using ManualResetEventSlim loadingDone = new(false);
// multiFile=FALSE — this enables the MemoryMappedFileReader path (like the real GUI)
LogfileReader reader = new(path, new EncodingOptions(), false, 40, 50, new MultiFileOptions(), ReaderType.System, PluginRegistry.PluginRegistry.Instance, 500);
reader.PreProcessColumnizer = csvColumnizer;
reader.LoadingFinished += (_, _) => loadingDone.Set();
reader.StartMonitoring();
Assert.That(loadingDone.Wait(TimeSpan.FromSeconds(5)), Is.True);
// Header dropped, 1 data line
Assert.That(reader.LineCount, Is.EqualTo(1), $"Expected 1 data line after header drop. Got {reader.LineCount}");
var line = reader.GetLogLineMemory(0);
Assert.That(line, Is.Not.Null, "Line 0 should not be null");
Assert.That(line.FullLine.IsEmpty, Is.False, "Line 0 should not be empty");
// This is the critical assertion: line 0 should be the DATA line, not the header
Assert.That(line.FullLine.ToString(), Does.Contain("comma file"),
$"Line 0 should be data line containing 'comma file', got: '{line.FullLine}'");
Assert.That(line.FullLine.ToString(), Does.Not.Contain("Date"),
$"Line 0 should NOT be the header. Got: '{line.FullLine}'");
reader.StopMonitoring();
}
/// <summary>
/// Tests a CSV file without trailing newline (header + 1 data line).
/// Creates a temp file to control exact byte content.
/// </summary>
[Test]
public void LogfileReader_CsvNoTrailingNewline_HeaderDropped_DataLineVisible ()
{
// Create temp file: header + 1 data line, NO trailing newline
var tempFile = Path.GetTempFileName();
try
{
File.WriteAllText(tempFile, "Name,Age,City\r\nAlice,30,Berlin");
CsvColumnizer.CsvColumnizer csvColumnizer = new();
LogfileReader reader = new(tempFile, new EncodingOptions(), true, 40, 50, new MultiFileOptions(), ReaderType.System, PluginRegistry.PluginRegistry.Instance, 500);
reader.PreProcessColumnizer = csvColumnizer;
reader.ReadFiles();
Assert.That(reader.LineCount, Is.EqualTo(1), $"Header dropped, 1 data line expected. Got {reader.LineCount}");
var line = reader.GetLogLineMemory(0);
Assert.That(line, Is.Not.Null, "Data line should not be null");
Assert.That(line.FullLine.IsEmpty, Is.False, "Data line should not be empty");
Assert.That(line.FullLine.ToString(), Is.EqualTo("Alice,30,Berlin"), $"Got: '{line.FullLine}'");
}
finally
{
File.Delete(tempFile);
}
}
/// <summary>
/// Tests a CSV file WITH trailing newline (header + 1 data line + trailing CRLF).
/// This is the exact scenario described: 3 ReadLineMemory calls where 1st drops header.
/// </summary>
[Test]
public void LogfileReader_CsvWithTrailingNewline_HeaderDropped_DataLineVisible ()
{
// Create temp file: header + 1 data line + trailing CRLF
var tempFile = Path.GetTempFileName();
try
{
File.WriteAllText(tempFile, "Name,Age,City\r\nAlice,30,Berlin\r\n");
CsvColumnizer.CsvColumnizer csvColumnizer = new();
LogfileReader reader = new(tempFile, new EncodingOptions(), true, 40, 50, new MultiFileOptions(), ReaderType.System, PluginRegistry.PluginRegistry.Instance, 500);
reader.PreProcessColumnizer = csvColumnizer;
reader.ReadFiles();
Assert.That(reader.LineCount, Is.EqualTo(1), $"Header dropped, 1 data line expected. Got {reader.LineCount}");
var line = reader.GetLogLineMemory(0);
Assert.That(line, Is.Not.Null, "Data line should not be null");
Assert.That(line.FullLine.IsEmpty, Is.False, "Data line should not be empty");
Assert.That(line.FullLine.ToString(), Is.EqualTo("Alice,30,Berlin"), $"Got: '{line.FullLine}'");
}
finally
{
File.Delete(tempFile);
}
}
#endregion
}