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 */; };
|
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 */; };
|
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 */; };
|
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 */; };
|
0386B0B42ED36C3D00CA6795 /* Locked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0386B0B32ED36C3D00CA6795 /* Locked.swift */; };
|
||||||
0386B0B52ED36C3D00CA6795 /* 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 */; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
0392CDE52EB23B8F009176DA /* CertificateValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateValidator.swift; sourceTree = "<group>"; };
|
||||||
@@ -1457,6 +1459,14 @@
|
|||||||
path = Parsers;
|
path = Parsers;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
037F44142EDB0A9F002EBF75 /* Watchers */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
037F44152EDB0AA8002EBF75 /* FSNotifierTest.swift */,
|
||||||
|
);
|
||||||
|
path = Watchers;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
0386B0BD2ED36E2500CA6795 /* Helpers */ = {
|
0386B0BD2ED36E2500CA6795 /* Helpers */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -2435,6 +2445,7 @@
|
|||||||
03D53E902E8AE089001B1671 /* Testables */,
|
03D53E902E8AE089001B1671 /* Testables */,
|
||||||
036575C62EA12E2200BA41BF /* Versions */,
|
036575C62EA12E2200BA41BF /* Versions */,
|
||||||
0386B0BD2ED36E2500CA6795 /* Helpers */,
|
0386B0BD2ED36E2500CA6795 /* Helpers */,
|
||||||
|
037F44142EDB0A9F002EBF75 /* Watchers */,
|
||||||
);
|
);
|
||||||
path = unit;
|
path = unit;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -3523,6 +3534,7 @@
|
|||||||
C485707A28BF457800539B36 /* PhpDoctorView.swift in Sources */,
|
C485707A28BF457800539B36 /* PhpDoctorView.swift in Sources */,
|
||||||
C4C0E8E827F88B41002D32A9 /* DomainScanner.swift in Sources */,
|
C4C0E8E827F88B41002D32A9 /* DomainScanner.swift in Sources */,
|
||||||
C449B4F027EE7FB800C47E8A /* DomainListTLSCell.swift in Sources */,
|
C449B4F027EE7FB800C47E8A /* DomainListTLSCell.swift in Sources */,
|
||||||
|
037F44162EDB0AAA002EBF75 /* FSNotifierTest.swift in Sources */,
|
||||||
C4FBFC532616485F00CDB8E1 /* PhpVersionDetectionTest.swift in Sources */,
|
C4FBFC532616485F00CDB8E1 /* PhpVersionDetectionTest.swift in Sources */,
|
||||||
C43A8A2425D9D20D00591B77 /* HomebrewPackageTest.swift in Sources */,
|
C43A8A2425D9D20D00591B77 /* HomebrewPackageTest.swift in Sources */,
|
||||||
C485707928BF456C00539B36 /* ArrayExtension.swift in Sources */,
|
C485707928BF456C00539B36 /* ArrayExtension.swift in Sources */,
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import Foundation
|
|||||||
situations where adopting structured concurrency would otherwise be
|
situations where adopting structured concurrency would otherwise be
|
||||||
too challenging or a huge refactor.
|
too challenging or a huge refactor.
|
||||||
*/
|
*/
|
||||||
final class Locked<T> {
|
final class Locked<T>: @unchecked Sendable {
|
||||||
private var _value: T
|
private var _value: T
|
||||||
private let lock = NSLock()
|
private let lock = NSLock()
|
||||||
|
|
||||||
|
|||||||
@@ -42,18 +42,27 @@ class FSNotifier {
|
|||||||
)
|
)
|
||||||
|
|
||||||
dispatchSource?.setEventHandler(handler: {
|
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 {
|
// Add a debounce of 1 second
|
||||||
// FS event fired, checking in 1s, no duplicate FS events will be acted upon
|
if let distance, distance <= 1.00 {
|
||||||
|
Log.perf("FSNotifier debounce active for \(url.path).")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Synchronize the last update property
|
||||||
self.lastUpdate = Date().timeIntervalSince1970
|
self.lastUpdate = Date().timeIntervalSince1970
|
||||||
|
|
||||||
|
// Dispatch the async task we set for when the filesystem event occurs
|
||||||
Task {
|
Task {
|
||||||
await delay(seconds: 1)
|
|
||||||
onChange()
|
onChange()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
dispatchSource?.setCancelHandler(handler: { [weak self] in
|
dispatchSource?.setCancelHandler(handler: { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
@@ -61,6 +70,7 @@ class FSNotifier {
|
|||||||
self.fileDescriptor = -1
|
self.fileDescriptor = -1
|
||||||
self.dispatchSource = nil
|
self.dispatchSource = nil
|
||||||
})
|
})
|
||||||
|
|
||||||
dispatchSource?.resume()
|
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