mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2025-12-21 03:10:06 +01:00
♻️ Rework FSNotifier and test it
This commit is contained in:
@@ -74,6 +74,7 @@
|
||||
0379C4A52ED720220035D7EA /* App+DetectApps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0379C4A32ED7201D0035D7EA /* App+DetectApps.swift */; };
|
||||
0379C4A62ED720220035D7EA /* App+DetectApps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0379C4A32ED7201D0035D7EA /* App+DetectApps.swift */; };
|
||||
0379C4A72ED720220035D7EA /* App+DetectApps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0379C4A32ED7201D0035D7EA /* App+DetectApps.swift */; };
|
||||
037F44162EDB0AAA002EBF75 /* FSNotifierTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F44152EDB0AA8002EBF75 /* FSNotifierTest.swift */; };
|
||||
0386B0B42ED36C3D00CA6795 /* Locked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0386B0B32ED36C3D00CA6795 /* Locked.swift */; };
|
||||
0386B0B52ED36C3D00CA6795 /* Locked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0386B0B32ED36C3D00CA6795 /* Locked.swift */; };
|
||||
0386B0B62ED36C3D00CA6795 /* Locked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0386B0B32ED36C3D00CA6795 /* Locked.swift */; };
|
||||
@@ -1050,6 +1051,7 @@
|
||||
036C39132E5CB820008DAEDF /* TestBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestBundle.swift; sourceTree = "<group>"; };
|
||||
0379C49E2ED71CFC0035D7EA /* Startup+Launch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Startup+Launch.swift"; sourceTree = "<group>"; };
|
||||
0379C4A32ED7201D0035D7EA /* App+DetectApps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+DetectApps.swift"; sourceTree = "<group>"; };
|
||||
037F44152EDB0AA8002EBF75 /* FSNotifierTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FSNotifierTest.swift; sourceTree = "<group>"; };
|
||||
0386B0B32ED36C3D00CA6795 /* Locked.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Locked.swift; sourceTree = "<group>"; };
|
||||
0386B0B82ED36DF800CA6795 /* LockedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockedTests.swift; sourceTree = "<group>"; };
|
||||
0392CDE52EB23B8F009176DA /* CertificateValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateValidator.swift; sourceTree = "<group>"; };
|
||||
@@ -1457,6 +1459,14 @@
|
||||
path = Parsers;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
037F44142EDB0A9F002EBF75 /* Watchers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
037F44152EDB0AA8002EBF75 /* FSNotifierTest.swift */,
|
||||
);
|
||||
path = Watchers;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0386B0BD2ED36E2500CA6795 /* Helpers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -2435,6 +2445,7 @@
|
||||
03D53E902E8AE089001B1671 /* Testables */,
|
||||
036575C62EA12E2200BA41BF /* Versions */,
|
||||
0386B0BD2ED36E2500CA6795 /* Helpers */,
|
||||
037F44142EDB0A9F002EBF75 /* Watchers */,
|
||||
);
|
||||
path = unit;
|
||||
sourceTree = "<group>";
|
||||
@@ -3523,6 +3534,7 @@
|
||||
C485707A28BF457800539B36 /* PhpDoctorView.swift in Sources */,
|
||||
C4C0E8E827F88B41002D32A9 /* DomainScanner.swift in Sources */,
|
||||
C449B4F027EE7FB800C47E8A /* DomainListTLSCell.swift in Sources */,
|
||||
037F44162EDB0AAA002EBF75 /* FSNotifierTest.swift in Sources */,
|
||||
C4FBFC532616485F00CDB8E1 /* PhpVersionDetectionTest.swift in Sources */,
|
||||
C43A8A2425D9D20D00591B77 /* HomebrewPackageTest.swift in Sources */,
|
||||
C485707928BF456C00539B36 /* ArrayExtension.swift in Sources */,
|
||||
|
||||
@@ -34,7 +34,7 @@ import Foundation
|
||||
situations where adopting structured concurrency would otherwise be
|
||||
too challenging or a huge refactor.
|
||||
*/
|
||||
final class Locked<T> {
|
||||
final class Locked<T>: @unchecked Sendable {
|
||||
private var _value: T
|
||||
private let lock = NSLock()
|
||||
|
||||
|
||||
@@ -42,18 +42,27 @@ class FSNotifier {
|
||||
)
|
||||
|
||||
dispatchSource?.setEventHandler(handler: {
|
||||
let distance = self.lastUpdate?.distance(to: Date().timeIntervalSince1970)
|
||||
self.queue.async {
|
||||
// See how long ago our last handled event was
|
||||
let distance = self.lastUpdate?
|
||||
.distance(to: Date().timeIntervalSince1970)
|
||||
|
||||
if distance == nil || distance != nil && distance! > 1.00 {
|
||||
// FS event fired, checking in 1s, no duplicate FS events will be acted upon
|
||||
// Add a debounce of 1 second
|
||||
if let distance, distance <= 1.00 {
|
||||
Log.perf("FSNotifier debounce active for \(url.path).")
|
||||
return
|
||||
}
|
||||
|
||||
// Synchronize the last update property
|
||||
self.lastUpdate = Date().timeIntervalSince1970
|
||||
|
||||
// Dispatch the async task we set for when the filesystem event occurs
|
||||
Task {
|
||||
await delay(seconds: 1)
|
||||
onChange()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
dispatchSource?.setCancelHandler(handler: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
@@ -61,6 +70,7 @@ class FSNotifier {
|
||||
self.fileDescriptor = -1
|
||||
self.dispatchSource = nil
|
||||
})
|
||||
|
||||
dispatchSource?.resume()
|
||||
}
|
||||
|
||||
|
||||
58
tests/unit/Watchers/FSNotifierTest.swift
Normal file
58
tests/unit/Watchers/FSNotifierTest.swift
Normal file
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// FSNotifierTest.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 29/11/2025.
|
||||
// Copyright © 2025 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Testing
|
||||
import Foundation
|
||||
|
||||
@Suite(.serialized)
|
||||
struct FSNotifierTest {
|
||||
/**
|
||||
This test verifies that FSNotifier fires the onChange callback when a file is modified.
|
||||
*/
|
||||
@Test func notifier_fires_when_file_is_modified_and_debounces_correctly() async throws {
|
||||
// Create a temporary file to monitor
|
||||
let tempDir = FileManager.default.temporaryDirectory
|
||||
let testFile = tempDir.appendingPathComponent("fs_notifier_test_\(UUID().uuidString).txt")
|
||||
FileManager.default.createFile(atPath: testFile.path, contents: nil)
|
||||
|
||||
defer {
|
||||
try? FileManager.default.removeItem(at: testFile)
|
||||
}
|
||||
|
||||
let eventFired = Locked<Int>(0)
|
||||
|
||||
// Create notifier
|
||||
let notifier = FSNotifier(
|
||||
for: testFile,
|
||||
eventMask: .write,
|
||||
onChange: {
|
||||
eventFired.value += 1
|
||||
}
|
||||
)
|
||||
|
||||
defer {
|
||||
notifier.terminate()
|
||||
}
|
||||
|
||||
// Modify the file, twice
|
||||
try "hello".write(to: testFile, atomically: false, encoding: .utf8)
|
||||
try "hello".write(to: testFile, atomically: false, encoding: .utf8)
|
||||
|
||||
// Wait for the event to fire, verify it fired ONCE (not TWICE)
|
||||
try await Task.sleep(nanoseconds: 200_000_000) // 0.2 seconds
|
||||
#expect(eventFired.value == 1)
|
||||
|
||||
// Try to write again (after debounce timing)
|
||||
try await Task.sleep(nanoseconds: 2_000_000_000)
|
||||
try "hello".write(to: testFile, atomically: false, encoding: .utf8)
|
||||
|
||||
// Verify our event fired AGAIN after 0.2 seconds
|
||||
try await Task.sleep(nanoseconds: 200_000_000) // 0.2 seconds
|
||||
#expect(eventFired.value == 2)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user