//===----------------------------------------------------------------------===// // Copyright © 2026 Apple Inc. and the container project authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //===----------------------------------------------------------------------===// import ContainerOS import ContainerizationError import DNSServer import Foundation import Testing struct DirectoryWatcherTest { let testUUID = UUID().uuidString private var testDir: URL! { let tempDir = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) .appendingPathComponent(".clitests") .appendingPathComponent(testUUID) try! FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) return tempDir } private func withTempDir(_ body: (URL) async throws -> T) async throws -> T { let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) defer { try? FileManager.default.removeItem(at: tempDir) } return try await body(tempDir) } private actor CreatedURLs { nonisolated(unsafe) public var urls: [URL] public init() { self.urls = [] } } @Test func testWatchingExistingDirectory() async throws { try await withTempDir { tempDir in let watcher = DirectoryWatcher(directoryURL: tempDir, log: nil) let createdURLs = CreatedURLs() let name = "newFile" await watcher.startWatching { [createdURLs] urls in for url in urls where url.lastPathComponent == name { createdURLs.urls.append(url) } } try await Task.sleep(for: .milliseconds(100)) let newFile = tempDir.appendingPathComponent(name) FileManager.default.createFile(atPath: newFile.path, contents: nil) try await Task.sleep(for: .milliseconds(500)) #expect(!createdURLs.urls.isEmpty, "directory watcher failed to detect new file") #expect(createdURLs.urls.first!.lastPathComponent == name) } } @Test func testWatchingNonExistingDirectory() async throws { try await withTempDir { tempDir in let uuid = UUID().uuidString let childDir = tempDir.appendingPathComponent(uuid) let watcher = DirectoryWatcher(directoryURL: childDir, log: nil) let createdURLs = CreatedURLs() let name = "newFile" await watcher.startWatching { [createdURLs] urls in for url in urls where url.lastPathComponent == name { createdURLs.urls.append(url) } } try await Task.sleep(for: .milliseconds(100)) try FileManager.default.createDirectory(at: childDir, withIntermediateDirectories: true) try await Task.sleep(for: DirectoryWatcher.watchPeriod) let newFile = childDir.appendingPathComponent(name) FileManager.default.createFile(atPath: newFile.path, contents: nil) try await Task.sleep(for: .milliseconds(500)) #expect(!createdURLs.urls.isEmpty, "directory watcher failed to detect parent directory") #expect(createdURLs.urls.first!.lastPathComponent == name) } } @Test func testWatchingNonExistingParent() async throws { try await withTempDir { tempDir in let parent = UUID().uuidString let child = UUID().uuidString let childDir = tempDir.appendingPathComponent(parent).appendingPathComponent(child) let watcher = DirectoryWatcher(directoryURL: childDir, log: nil) let createdURLs = CreatedURLs() let name = "newFile" await watcher.startWatching { urls in for url in urls where url.lastPathComponent == name { createdURLs.urls.append(url) } } try await Task.sleep(for: .milliseconds(100)) try FileManager.default.createDirectory(at: childDir, withIntermediateDirectories: true) try await Task.sleep(for: DirectoryWatcher.watchPeriod) let newFile = childDir.appendingPathComponent(name) FileManager.default.createFile(atPath: newFile.path, contents: nil) try await Task.sleep(for: .milliseconds(500)) #expect(!createdURLs.urls.isEmpty, "directory watcher failed to detect parent directory") #expect(createdURLs.urls.first!.lastPathComponent == name) } } @Test func testWatchingRecreatedDirectory() async throws { try await withTempDir { tempDir in let dir = tempDir.appendingPathComponent(UUID().uuidString) try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) let watcher = DirectoryWatcher(directoryURL: dir, log: nil) let createdURLs = CreatedURLs() let beforeDelete = "beforeDelete" let afterDelete = "afterDelete" await watcher.startWatching { [createdURLs] urls in for url in urls where url.lastPathComponent == beforeDelete || url.lastPathComponent == afterDelete { createdURLs.urls.append(url) } } try await Task.sleep(for: .milliseconds(100)) let file1 = dir.appendingPathComponent(beforeDelete) FileManager.default.createFile(atPath: file1.path, contents: nil) try await Task.sleep(for: .milliseconds(100)) try FileManager.default.removeItem(at: dir) try await Task.sleep(for: .milliseconds(100)) try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) try await Task.sleep(for: DirectoryWatcher.watchPeriod) let file2 = dir.appendingPathComponent(afterDelete) FileManager.default.createFile(atPath: file2.path, contents: nil) try await Task.sleep(for: .milliseconds(500)) #expect(!createdURLs.urls.isEmpty, "directory watcher failed to detect new file") #expect(Set(createdURLs.urls.map { $0.lastPathComponent }) == Set([beforeDelete, afterDelete])) } } }