Report the directories a growing tree could not watch
Extending an automatically watched tree to a new directory swallowed every error, so a reached watch limit or an unreadable directory left part of the tree unwatched without any sign. No call of the consumer is running at that moment, so the failures now arrive in the event stream as InotifyEvent.watchFailed, after the event that triggered the extension. The library watches what it can first: a reached limit ends the attempt, an unreadable directory is skipped with its subtree, and a directory that vanished in between is not reported. The buffer now carries the library's own events next to the kernel's, which keeps them in order and hides the stream's element type. The test runner drops root's DAC capabilities so that an unreadable directory can be tested.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import Foundation
|
||||
import SystemPackage
|
||||
import Testing
|
||||
@testable import Inotify
|
||||
|
||||
@@ -158,4 +159,57 @@ struct RecursiveEventTests {
|
||||
#expect(createdAfterMove != nil, "Expected CREATE inside the moved-in subdirectory, got: \(events)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Needs a process that directory permissions apply to; the test runner
|
||||
/// drops root's override capabilities for that.
|
||||
@Test func reportsANewDirectoryItCannotReadAndWatchesItsSiblings() async throws {
|
||||
try await withTempDir { dir in
|
||||
let root = "\(dir)/Root"
|
||||
let treeSource = "\(dir)/Outside/Grown"
|
||||
let treeDestination = "\(root)/Grown"
|
||||
let locked = "\(treeDestination)/Locked"
|
||||
let filepath = "\(treeDestination)/Open/created.txt"
|
||||
try FileManager.default.createDirectory(atPath: "\(treeSource)/Locked", withIntermediateDirectories: true)
|
||||
try FileManager.default.createDirectory(atPath: "\(treeSource)/Open", withIntermediateDirectories: true)
|
||||
try FileManager.default.createDirectory(atPath: root, withIntermediateDirectories: true)
|
||||
try FileManager.default.setAttributes([.posixPermissions: 0o000], ofItemAtPath: "\(treeSource)/Locked")
|
||||
defer { try? FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: locked) }
|
||||
|
||||
let events = try await getInotifyEventsForTrigger(
|
||||
in: root,
|
||||
mask: [.create, .movedTo],
|
||||
recursive: .withAutomaticSubtreeWatching
|
||||
) { _ in
|
||||
try FileManager.default.moveItem(atPath: treeSource, toPath: treeDestination)
|
||||
try await Task.sleep(for: .milliseconds(400))
|
||||
try createFile(at: filepath, contents: "hello")
|
||||
}
|
||||
|
||||
let failures = events.compactMap(\.watchFailure)
|
||||
#expect(failures.count == 1, "Expected exactly the locked directory to be reported, got: \(events)")
|
||||
#expect(failures.first?.path == FilePath(locked))
|
||||
#expect(failures.first?.error == .listDirectoryFailed(path: locked, errno: EACCES))
|
||||
let sibling = events.compactMap(\.fileSystemEvent).first { !$0.synthesized && $0.mask.contains(.create) && $0.path.string == filepath }
|
||||
#expect(sibling != nil, "Expected CREATE inside the readable sibling, got: \(events)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Events are transformed as they are consumed, so a directory that is
|
||||
/// created and removed before consumption starts is gone when the
|
||||
/// library tries to watch it.
|
||||
@Test func doesNotReportADirectoryThatVanishedBeforeItCouldBeWatched() async throws {
|
||||
try await withTempDir { dir in
|
||||
let vanished = "\(dir)/Vanished"
|
||||
let watcher = try Inotify()
|
||||
try await watcher.addWatchWithAutomaticSubtreeWatching(forDirectory: dir, mask: [.create])
|
||||
try FileManager.default.createDirectory(atPath: vanished, withIntermediateDirectories: false)
|
||||
try FileManager.default.removeItem(atPath: vanished)
|
||||
|
||||
let events = await collectEvents(of: watcher, for: .milliseconds(500))
|
||||
|
||||
#expect(events.compactMap(\.watchFailure).isEmpty, "Did not expect a watch failure for a vanished directory, got: \(events)")
|
||||
let created = events.compactMap(\.fileSystemEvent).first { $0.mask.contains(.create) && $0.path.string == vanished }
|
||||
#expect(created != nil, "Expected CREATE for '\(vanished)', got: \(events)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user