Skip to content

Commit 8406d97

Browse files
committed
Enhance thread safety in file event handlers by adding locking mechanism in ResilientFileSystemMonitorTests
1 parent 11bc7a4 commit 8406d97

1 file changed

Lines changed: 74 additions & 32 deletions

File tree

‎src/Cocoar.FileSystem.Tests/ResilientFileSystemMonitorTests.cs‎

Lines changed: 74 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -132,19 +132,23 @@ public async Task FileCreation_ShouldTriggerCreatedEvent()
132132
{
133133
var monitor = CreateMonitor();
134134
var createdFiles = new List<string>();
135-
monitor.Created += (s, e) => createdFiles.Add(e.Name!);
135+
var lockObj = new object();
136+
monitor.Created += (s, e) => { lock (lockObj) createdFiles.Add(e.Name!); };
136137

137138
var testFile = GetTestPath("test.txt");
138139
await Task.Delay(100);
139140
File.WriteAllText(testFile, "content");
140141

141142
await ActiveWaitHelpers.WaitUntilAsync(
142-
() => createdFiles.Contains("test.txt"),
143+
() => { lock (lockObj) return createdFiles.Contains("test.txt"); },
143144
TimeSpan.FromSeconds(3),
144145
TimeSpan.FromMilliseconds(50),
145146
"file creation event");
146147

147-
Assert.Contains("test.txt", createdFiles);
148+
lock (lockObj)
149+
{
150+
Assert.Contains("test.txt", createdFiles);
151+
}
148152
}
149153

150154
[Fact]
@@ -155,18 +159,22 @@ public async Task FileChange_ShouldTriggerChangedEvent()
155159

156160
var monitor = CreateMonitor();
157161
var changedFiles = new List<string>();
158-
monitor.Changed += (s, e) => changedFiles.Add(e.Name!);
162+
var lockObj = new object();
163+
monitor.Changed += (s, e) => { lock (lockObj) changedFiles.Add(e.Name!); };
159164

160165
await Task.Delay(200);
161166
File.WriteAllText(testFile, "modified");
162167

163168
await ActiveWaitHelpers.WaitUntilAsync(
164-
() => changedFiles.Contains("test.txt"),
169+
() => { lock (lockObj) return changedFiles.Contains("test.txt"); },
165170
TimeSpan.FromSeconds(3),
166171
TimeSpan.FromMilliseconds(50),
167172
"file change event");
168173

169-
Assert.Contains("test.txt", changedFiles);
174+
lock (lockObj)
175+
{
176+
Assert.Contains("test.txt", changedFiles);
177+
}
170178
}
171179

172180
[Fact]
@@ -177,18 +185,22 @@ public async Task FileDelete_ShouldTriggerDeletedEvent()
177185

178186
var monitor = CreateMonitor();
179187
var deletedFiles = new List<string>();
180-
monitor.Deleted += (s, e) => deletedFiles.Add(e.Name!);
188+
var lockObj = new object();
189+
monitor.Deleted += (s, e) => { lock (lockObj) deletedFiles.Add(e.Name!); };
181190

182191
await Task.Delay(200);
183192
File.Delete(testFile);
184193

185194
await ActiveWaitHelpers.WaitUntilAsync(
186-
() => deletedFiles.Contains("test.txt"),
195+
() => { lock (lockObj) return deletedFiles.Contains("test.txt"); },
187196
TimeSpan.FromSeconds(3),
188197
TimeSpan.FromMilliseconds(50),
189198
"file deletion event");
190199

191-
Assert.Contains("test.txt", deletedFiles);
200+
lock (lockObj)
201+
{
202+
Assert.Contains("test.txt", deletedFiles);
203+
}
192204
}
193205

194206
[Fact]
@@ -222,29 +234,33 @@ public async Task Filter_OnlyMatchingFiles_ShouldTriggerEvents()
222234
{
223235
var monitor = CreateMonitor(filter: "*.txt");
224236
var createdFiles = new List<string>();
225-
monitor.Created += (s, e) => createdFiles.Add(e.Name!);
237+
var lockObj = new object();
238+
monitor.Created += (s, e) => { lock (lockObj) createdFiles.Add(e.Name!); };
226239

227240
await Task.Delay(100);
228241
File.WriteAllText(GetTestPath("test.txt"), "content");
229242
File.WriteAllText(GetTestPath("test.log"), "content");
230243

231244
await ActiveWaitHelpers.WaitUntilAsync(
232-
() => createdFiles.Contains("test.txt"),
245+
() => { lock (lockObj) return createdFiles.Contains("test.txt"); },
233246
TimeSpan.FromSeconds(3),
234247
TimeSpan.FromMilliseconds(50),
235-
"filtered file creation");
248+
"txt file creation event");
236249

237-
await Task.Delay(500);
238-
Assert.Contains("test.txt", createdFiles);
239-
Assert.DoesNotContain("test.log", createdFiles);
250+
lock (lockObj)
251+
{
252+
Assert.Contains("test.txt", createdFiles);
253+
Assert.DoesNotContain("test.log", createdFiles);
254+
}
240255
}
241256

242257
[Fact]
243258
public async Task IncludeSubdirectories_True_ShouldMonitorSubfolders()
244259
{
245260
var monitor = CreateMonitor(includeSubdirectories: true);
246261
var createdFiles = new List<string>();
247-
monitor.Created += (s, e) => createdFiles.Add(e.Name!);
262+
var lockObj = new object();
263+
monitor.Created += (s, e) => { lock (lockObj) createdFiles.Add(e.Name!); };
248264

249265
var subDir = GetTestPath("subfolder");
250266
Directory.CreateDirectory(subDir);
@@ -253,20 +269,24 @@ public async Task IncludeSubdirectories_True_ShouldMonitorSubfolders()
253269
File.WriteAllText(Path.Combine(subDir, "test.txt"), "content");
254270

255271
await ActiveWaitHelpers.WaitUntilAsync(
256-
() => createdFiles.Contains("test.txt"),
272+
() => { lock (lockObj) return createdFiles.Contains("test.txt"); },
257273
TimeSpan.FromSeconds(3),
258274
TimeSpan.FromMilliseconds(50),
259275
"subfolder file creation");
260276

261-
Assert.Contains("test.txt", createdFiles);
277+
lock (lockObj)
278+
{
279+
Assert.Contains("test.txt", createdFiles);
280+
}
262281
}
263282

264283
[Fact]
265284
public async Task IncludeSubdirectories_False_ShouldNotMonitorSubfolders()
266285
{
267286
var monitor = CreateMonitor(includeSubdirectories: false);
268287
var createdFiles = new List<string>();
269-
monitor.Created += (s, e) => createdFiles.Add(e.Name!);
288+
var lockObj = new object();
289+
monitor.Created += (s, e) => { lock (lockObj) createdFiles.Add(e.Name!); };
270290

271291
await Task.Delay(100);
272292
File.WriteAllText(GetTestPath("root.txt"), "content");
@@ -278,15 +298,18 @@ public async Task IncludeSubdirectories_False_ShouldNotMonitorSubfolders()
278298
File.WriteAllText(Path.Combine(subDir, "sub.txt"), "content");
279299

280300
await ActiveWaitHelpers.WaitUntilAsync(
281-
() => createdFiles.Contains("root.txt"),
301+
() => { lock (lockObj) return createdFiles.Contains("root.txt"); },
282302
TimeSpan.FromSeconds(3),
283303
TimeSpan.FromMilliseconds(50),
284304
"root file creation");
285305

286306
await Task.Delay(800);
287307

288-
Assert.Contains("root.txt", createdFiles);
289-
Assert.DoesNotContain("sub.txt", createdFiles);
308+
lock (lockObj)
309+
{
310+
Assert.Contains("root.txt", createdFiles);
311+
Assert.DoesNotContain("sub.txt", createdFiles);
312+
}
290313
}
291314

292315
#endregion
@@ -414,8 +437,10 @@ public async Task FileModified_WhileInPollingMode_ShouldDetectChange()
414437

415438
var changedFiles = new List<string>();
416439
var createdFiles = new List<string>();
417-
monitor.Changed += (s, e) => changedFiles.Add(e.Name!);
418-
monitor.Created += (s, e) => createdFiles.Add(e.Name!);
440+
var lockObj = new object();
441+
442+
monitor.Changed += (s, e) => { lock (lockObj) changedFiles.Add(e.Name!); };
443+
monitor.Created += (s, e) => { lock (lockObj) createdFiles.Add(e.Name!); };
419444

420445
await ActiveWaitHelpers.WaitUntilAsync(
421446
() => monitor.IsUsingWatcher,
@@ -432,7 +457,9 @@ await ActiveWaitHelpers.WaitUntilAsync(
432457
TimeSpan.FromMilliseconds(50),
433458
"enter polling");
434459

435-
// Recreate with modified file (this will be a "created" event since old file was deleted)
460+
// Recreate with modified file
461+
// Note: This may be detected as either Created or Changed depending on platform
462+
// macOS tends to report this as Changed, while Windows/Linux report it as Created
436463
Directory.CreateDirectory(_testRoot);
437464
File.WriteAllText(GetTestPath("test.txt"), "v2_modified");
438465

@@ -443,14 +470,24 @@ await ActiveWaitHelpers.WaitUntilAsync(
443470
TimeSpan.FromMilliseconds(100),
444471
"switch back to watcher");
445472

446-
// Wait for reconciliation to detect the file
473+
// Wait for reconciliation to detect the file (either as Created or Changed)
447474
await ActiveWaitHelpers.WaitUntilAsync(
448-
() => createdFiles.Contains("test.txt"),
475+
() => {
476+
lock (lockObj)
477+
return createdFiles.Contains("test.txt") || changedFiles.Contains("test.txt");
478+
},
449479
TimeSpan.FromSeconds(3),
450480
TimeSpan.FromMilliseconds(100),
451481
"detect file via reconciliation");
452482

453-
Assert.Contains("test.txt", createdFiles);
483+
// File should be detected via one of these events
484+
lock (lockObj)
485+
{
486+
Assert.True(
487+
createdFiles.Contains("test.txt") || changedFiles.Contains("test.txt"),
488+
$"File 'test.txt' should be detected via Created or Changed event. " +
489+
$"Created: [{string.Join(", ", createdFiles)}], Changed: [{string.Join(", ", changedFiles)}]");
490+
}
454491
}
455492

456493
[Fact]
@@ -554,7 +591,8 @@ public async Task AuditInterval_DetectsSilentChanges_ShouldReconcile()
554591
// Simulate silent changes (though in practice, events should fire)
555592
// The audit will still catch divergence if any
556593
var createdFiles = new List<string>();
557-
monitor.Created += (s, e) => createdFiles.Add(e.Name!);
594+
var lockObj = new object();
595+
monitor.Created += (s, e) => { lock (lockObj) createdFiles.Add(e.Name!); };
558596

559597
await Task.Delay(1500); // Let audit run
560598

@@ -575,7 +613,8 @@ public async Task PollingMode_ShouldDetectFileChanges()
575613
pollingInterval: TimeSpan.FromMilliseconds(300));
576614

577615
var createdFiles = new List<string>();
578-
monitor.Created += (s, e) => createdFiles.Add(e.Name!);
616+
var lockObj = new object();
617+
monitor.Created += (s, e) => { lock (lockObj) createdFiles.Add(e.Name!); };
579618

580619
Assert.False(monitor.IsUsingWatcher);
581620

@@ -585,12 +624,15 @@ public async Task PollingMode_ShouldDetectFileChanges()
585624
File.WriteAllText(Path.Combine(nonExistentPath, "test.txt"), "content");
586625

587626
await ActiveWaitHelpers.WaitUntilAsync(
588-
() => createdFiles.Contains("test.txt"),
627+
() => { lock (lockObj) return createdFiles.Contains("test.txt"); },
589628
TimeSpan.FromSeconds(6),
590629
TimeSpan.FromMilliseconds(150),
591630
"polling mode file detection");
592631

593-
Assert.Contains("test.txt", createdFiles);
632+
lock (lockObj)
633+
{
634+
Assert.Contains("test.txt", createdFiles);
635+
}
594636
}
595637

596638
[Fact]

0 commit comments

Comments
 (0)