Files
swift-inotify/Tests/InotifyIntegrationTests/InotifyLimitTests.swift
T
T. R. Bernstein 21f096aede Remove the watches a failed tree call had already added
A recursive watch that hit the watch limit or an unreadable directory
threw after adding watches for part of the tree, which stayed in the
instance and kept counting against the user's limit. The call now
leaves the instance as it found it.

The test needs a second instance for the check, because a repeated
watch on the same instance only updates the existing one.
2026-09-19 00:43:37 +02:00

98 lines
3.5 KiB
Swift

import Testing
import Foundation
@testable import Inotify
@Suite("Inotify Limits", .serialized)
struct InotifyLimitTests {
@Test func throwsIfInotifyUpperLimitReached() async throws {
try await withTempDir { dir in
try await withInotifyWatchLimit(of: 10) {
try createSubdirectorytree(at: dir, foldersPerLevel: 4, levels: 3)
try await Task.sleep(for: .milliseconds(100))
await #expect(throws: InotifyError.self) {
let watcher = try Inotify()
try await watcher.addRecursiveWatch(forDirectory: dir, mask: .allEvents)
}
}
}
}
/// The limit counts every watch of the user, also those of other
/// processes, so it leaves room for the one watch of the second instance.
@Test func releasesTheWatchesOfATreeItCouldNotWatchCompletely() async throws {
try await withTempDir { dir in
try await withInotifyWatchLimit(of: 100, for: [.userWatches]) {
try createSubdirectorytree(at: dir, foldersPerLevel: 4, levels: 4)
let filepath = "\(dir)/new-file.txt"
let failedWatcher = try Inotify()
await #expect(throws: InotifyError.self) {
try await failedWatcher.addRecursiveWatch(forDirectory: dir, mask: .create)
}
let events = try await getEventsForTrigger(in: dir, mask: .create) { _ in
try createFile(at: filepath, contents: "hello")
}
// Deallocating the failed instance would free its watches too, so it
// must live until the second instance has added its watch.
withExtendedLifetime(failedWatcher) {}
let createEvent = events.first { $0.path.string == filepath }
#expect(createEvent != nil, "Expected a second instance to watch '\(dir)' after the failed one released its watches, got: \(events)")
}
}
}
@Test func watchesMassivSubtreesIfAllowed() async throws {
try await withTempDir { dir in
try await withInotifyWatchLimit(of: 1000) {
try createSubdirectorytree(at: dir, foldersPerLevel: 8, levels: 3)
let subDirectory = "\(dir)/Folder 8/Folder 8/Folder 8"
let filepath = "\(subDirectory)/new-file.txt"
try await Task.sleep(for: .milliseconds(100))
let events = try await getEventsForTrigger(
in: dir,
mask: [.create],
recursive: .recursive
) { _ in
assert(FileManager.default.fileExists(atPath: subDirectory))
try createFile(at: "\(filepath)", contents: "hello")
}
let createEvent = events.first { $0.mask.contains(.create) && $0.path.string == filepath }
#expect(createEvent != nil, "Expected CREATE for '\(filepath)', got: \(events)")
}
}
}
@Test func reportsQueueOverflowInsteadOfDroppingIt() async throws {
try await withTempDir { dir in
try await withInotifyWatchLimit(of: 1, for: [.queuedEvents]) {
let watcher = try Inotify()
try await watcher.addWatch(path: dir, mask: .allEvents)
let overflowTask = Task { () -> (InotifyEvent?, Int) in
var received = 0
for await event in await watcher.events {
received += 1
if case .queueOverflow = event { return (event, received) }
}
return (nil, received)
}
let deadline = ContinuousClock.now + .seconds(5)
var index = 0
while !overflowTask.isCancelled, ContinuousClock.now < deadline {
try createFile(at: "\(dir)/burst-\(index).txt", contents: "hello")
index += 1
if index % 200 == 0 { await Task.yield() }
}
overflowTask.cancel()
let (overflow, received) = await overflowTask.value
#expect(overflow == .queueOverflow, "Expected a queue overflow event after \(index) file creations and \(received) received events")
}
}
}
}