1
0
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:
2025-11-29 12:58:21 +01:00
parent 14863b96c9
commit fc01755dfa
4 changed files with 85 additions and 5 deletions

View File

@@ -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 */,

View File

@@ -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()

View File

@@ -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()
}

View 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)
}
}