diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 9589e84..41ddefe 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,12 +1,10 @@ { "name": "Swift", - "image": "swift:5.10", + "image": "swift:6.3", "features": { "ghcr.io/devcontainers/features/common-utils:2": { "installZsh": "false", - "username": "vscode", - "userUid": "1000", - "userGid": "1000", + "username": "ubuntu", "upgradePackages": "false" }, "ghcr.io/devcontainers/features/git:1": { @@ -25,20 +23,17 @@ "vscode": { // Set *default* container specific settings.json values on container create. "settings": { + "swift.path": "/usr/bin", "lldb.library": "/usr/lib/liblldb.so" }, // Add the IDs of extensions you want installed when the container is created. "extensions": [ - "sswg.swift-lang" + "swiftlang.swift-vscode" ] } }, // Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], - - // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "swift --version", - // Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. - "remoteUser": "vscode" + "remoteUser": "ubuntu" } \ No newline at end of file diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 790a8fa..7abb24a 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -10,15 +10,11 @@ jobs: build: - runs-on: ubuntu-22.04 - + runs-on: ubuntu-latest + timeout-minutes: 15 + container: + image: swift:latest steps: - - uses: actions/checkout@v2 - - name: Install Swift - uses: swift-actions/setup-swift@v1 - with: - swift-version: 5.9 - - name: Build - run: swift build -v - - name: Run tests + - uses: actions/checkout@v7 + - name: Build and Run Tests run: swift test -v diff --git a/Tests/FileMonitorTests/FileMonitorExplicitAddTests.swift b/Tests/FileMonitorTests/FileMonitorExplicitAddTests.swift index 486ed7c..6cf4a20 100644 --- a/Tests/FileMonitorTests/FileMonitorExplicitAddTests.swift +++ b/Tests/FileMonitorTests/FileMonitorExplicitAddTests.swift @@ -56,10 +56,15 @@ final class FileMonitorExplicitAddTests: XCTestCase { let monitor = try FileMonitor(directory: tmp.appendingPathComponent(dir), delegate: watcher) try monitor.start() + defer { + monitor.stop() + } AddWatcher.fileChanges = 0 - try "hello".write(to: testFile, atomically: false, encoding: .utf8) - XCTAssertTrue(FileManager.default.fileExists(atPath: testFile.path)) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + try? "hello".write(to: testFile, atomically: false, encoding: .utf8) + XCTAssertTrue(FileManager.default.fileExists(atPath: testFile.path)) + } wait(for: [expectation], timeout: 10) XCTAssertEqual(AddWatcher.fileChanges, 1) diff --git a/Tests/FileMonitorTests/FileMonitorExplicitChangeTests.swift b/Tests/FileMonitorTests/FileMonitorExplicitChangeTests.swift index 706056c..f4cbf4e 100644 --- a/Tests/FileMonitorTests/FileMonitorExplicitChangeTests.swift +++ b/Tests/FileMonitorTests/FileMonitorExplicitChangeTests.swift @@ -61,12 +61,19 @@ final class FileMonitorExplicitChangeTests: XCTestCase { let monitor = try FileMonitor(directory: tmp.appendingPathComponent(dir), delegate: watcher) try monitor.start() + defer { + monitor.stop() + } ChangeWatcher.fileChanges = 0 - let fileHandle = try FileHandle(forWritingTo: testFile) - try fileHandle.seekToEnd() - fileHandle.write("append some text".data(using: .utf8)!) - try fileHandle.close() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + do { + let fileHandle = try FileHandle(forWritingTo: testFile) + try fileHandle.seekToEnd() + fileHandle.write("append some text".data(using: .utf8)!) + try fileHandle.close() + } catch {} + } wait(for: [expectation], timeout: 10) diff --git a/Tests/FileMonitorTests/FileMonitorExplicitDeleteTests.swift b/Tests/FileMonitorTests/FileMonitorExplicitDeleteTests.swift index bd9f631..aad3f49 100644 --- a/Tests/FileMonitorTests/FileMonitorExplicitDeleteTests.swift +++ b/Tests/FileMonitorTests/FileMonitorExplicitDeleteTests.swift @@ -61,10 +61,16 @@ final class FileMonitorExplicitDeleteTests: XCTestCase { let monitor = try FileMonitor(directory: tmp.appendingPathComponent(dir), delegate: watcher) try monitor.start() + defer { + monitor.stop() + } ChangeWatcher.fileChanges = 0 - try FileManager.default.removeItem(at: testFile) - XCTAssertFalse(FileManager.default.fileExists(atPath: testFile.path)) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + try? FileManager.default.removeItem(at: testFile) + XCTAssertFalse(FileManager.default.fileExists(atPath: testFile.path)) + } + wait(for: [expectation], timeout: 10) diff --git a/Tests/FileMonitorTests/FileMonitorFileHasPathTests.swift b/Tests/FileMonitorTests/FileMonitorFileHasPathTests.swift index b4aada8..0af107e 100644 --- a/Tests/FileMonitorTests/FileMonitorFileHasPathTests.swift +++ b/Tests/FileMonitorTests/FileMonitorFileHasPathTests.swift @@ -58,6 +58,9 @@ final class FileMonitorFileHasPathTests: XCTestCase { let monitor = try FileMonitor(directory: tmp.appendingPathComponent(dir), delegate: watcher) try monitor.start() + defer { + monitor.stop() + } Watcher.fileChanges = 0 try "hello".write(to: testFile, atomically: false, encoding: .utf8) diff --git a/Tests/FileMonitorTests/FileMonitorTests.swift b/Tests/FileMonitorTests/FileMonitorTests.swift index 6957cb5..bea31ce 100644 --- a/Tests/FileMonitorTests/FileMonitorTests.swift +++ b/Tests/FileMonitorTests/FileMonitorTests.swift @@ -4,21 +4,27 @@ import XCTest import FileMonitorShared final class FileMonitorTests: XCTestCase { - - let tmp = FileManager.default.temporaryDirectory - let dir = String.random(length: 10) - - override func setUpWithError() throws { - super.setUp() - let directory = tmp.appendingPathComponent(dir) - + func withDirectory(_ operation: (URL) async throws -> Void) async throws { + let directory = FileManager.default.temporaryDirectory.appendingPathComponent(String.random(length: 10)) try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) - print("Created directory: \(tmp.appendingPathComponent(dir).path)") + do { + try await operation(directory) + } catch { + try? FileManager.default.removeItem(at: directory) + throw error + } + try FileManager.default.removeItem(at: directory) } - override func tearDownWithError() throws { - try super.tearDownWithError() - let directory = tmp.appendingPathComponent(dir) + func withDirectory(_ operation: (URL) throws -> Void) throws { + let directory = FileManager.default.temporaryDirectory.appendingPathComponent(String.random(length: 10)) + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + do { + try operation(directory) + } catch { + try? FileManager.default.removeItem(at: directory) + throw error + } try FileManager.default.removeItem(at: directory) } @@ -48,83 +54,114 @@ final class FileMonitorTests: XCTestCase { } func testLifecycleCreate() throws { - let expectation = expectation(description: "Wait for file creation") - expectation.assertForOverFulfill = false + try withDirectory { dir in + let expectation = expectation(description: "Wait for file creation") + expectation.assertForOverFulfill = false - let testFile = tmp.appendingPathComponent(dir).appendingPathComponent("\(String.random(length: 8)).\(String.random(length: 3))"); - let watcher = Watcher(on: testFile) { expectation.fulfill() } + let testFile = dir.appendingPathComponent("\(String.random(length: 8)).\(String.random(length: 3))"); + let watcher = Watcher(on: testFile) { expectation.fulfill() } - let monitor = try FileMonitor(directory: tmp.appendingPathComponent(dir), delegate: watcher) - try monitor.start() - Watcher.fileChanges = 0 + let monitor = try FileMonitor(directory: dir, delegate: watcher) + try monitor.start() + defer { + monitor.stop() + } + Watcher.fileChanges = 0 - FileManager.default.createFile(atPath: testFile.path, contents: "hello".data(using: .utf8)) - wait(for: [expectation], timeout: 10) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + FileManager.default.createFile(atPath: testFile.path, contents: "hello".data(using: .utf8)) + } + wait(for: [expectation], timeout: 10) - XCTAssertGreaterThan(Watcher.fileChanges, 0) + XCTAssertGreaterThan(Watcher.fileChanges, 0) + } } - func testLifecycleChange() async throws { - let expectation = expectation(description: "Wait for file creation") - expectation.assertForOverFulfill = false + func testLifecycleChange() throws { + try withDirectory { dir in + let expectation = expectation(description: "Wait for file creation") + expectation.assertForOverFulfill = false - let testFile = tmp.appendingPathComponent(dir).appendingPathComponent("\(String.random(length: 8)).\(String.random(length: 3))"); - FileManager.default.createFile(atPath: testFile.path, contents: "hello".data(using: .utf8)) + let testFile = dir.appendingPathComponent("\(String.random(length: 8)).\(String.random(length: 3))"); + FileManager.default.createFile(atPath: testFile.path, contents: "hello".data(using: .utf8)) - let watcher = Watcher(on: testFile) { expectation.fulfill() } + let watcher = Watcher(on: testFile) { expectation.fulfill() } - let monitor = try FileMonitor(directory: tmp.appendingPathComponent(dir), delegate: watcher) - try monitor.start() - Watcher.fileChanges = 0 - - try "Next New Content".write(toFile: testFile.path, atomically: true, encoding: .utf8) - await fulfillment(of: [expectation], timeout: 10) - monitor.stop() - XCTAssertGreaterThan(Watcher.fileChanges, 0) + let monitor = try FileMonitor(directory: dir, delegate: watcher) + try monitor.start() + defer { + monitor.stop() + } + Watcher.fileChanges = 0 + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + try? "Next New Content".write(toFile: testFile.path, atomically: true, encoding: .utf8) + } + wait(for: [expectation], timeout: 10) + XCTAssertGreaterThan(Watcher.fileChanges, 0) + } } func testLifecycleChangeAsync() async throws { - let asyncExpectation = XCTestExpectation(description: "Async wait for file creation") - - let testFile = tmp.appendingPathComponent(dir).appendingPathComponent("\(String.random(length: 8)).\(String.random(length: 3))"); - FileManager.default.createFile(atPath: testFile.path, contents: "hello".data(using: .utf8)) - - let monitor = try FileMonitor(directory: tmp.appendingPathComponent(dir)) - try monitor.start() - Watcher.fileChanges = 0 - - var events = [FileChange]() - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - try? "New Content".write(toFile: testFile.path, atomically: true, encoding: .utf8) - } + struct TimeoutError: Error {} + try await withDirectory { dir in + let testFile = dir.appendingPathComponent("\(String.random(length: 8)).\(String.random(length: 3))"); + FileManager.default.createFile(atPath: testFile.path, contents: "hello".data(using: .utf8)) + + let monitor = try FileMonitor(directory: dir) + try monitor.start() + defer { + monitor.stop() + } + Watcher.fileChanges = 0 + + try await withThrowingTaskGroup(of: FileChangeEvent?.self) { group in + group.addTask { + try await Task.sleep(for: .seconds(1)) + try "New Content".write(toFile: testFile.path, atomically: false, encoding: .utf8) + print("Wrote to: \(testFile.path)") + return nil + } + group.addTask { + var iterator = monitor.stream.makeAsyncIterator() + return await iterator.next() + } + group.addTask { + try await Task.sleep(for: .seconds(10)) + throw TimeoutError() + } - for await event in monitor.stream { - events.append(event) - asyncExpectation.fulfill() - monitor.stop() + _ = try await group.next() + _ = try await group.next() + group.cancelAll() + } } - - await fulfillment(of: [asyncExpectation], timeout: 10) - XCTAssertGreaterThan(events.count, 0) } - func testLifecycleDelete() throws { + func testLifecycleDelete() async throws { let expectation = expectation(description: "Wait for file deletion") expectation.assertForOverFulfill = false - let testFile = tmp.appendingPathComponent(dir).appendingPathComponent("\(String.random(length: 8)).\(String.random(length: 3))"); - FileManager.default.createFile(atPath: testFile.path, contents: "hello".data(using: .utf8)) + try await withDirectory { dir in + let testFile = dir.appendingPathComponent("\(String.random(length: 8)).\(String.random(length: 3))"); + FileManager.default.createFile(atPath: testFile.path, contents: "hello".data(using: .utf8)) - let watcher = Watcher(on: testFile) { expectation.fulfill() } + let watcher = Watcher(on: testFile) { expectation.fulfill() } - let monitor = try FileMonitor(directory: tmp.appendingPathComponent(dir), delegate: watcher) - try monitor.start() - Watcher.fileChanges = 0 + let monitor = try FileMonitor(directory: dir, delegate: watcher) + try monitor.start() + defer { + monitor.stop() + } + Watcher.fileChanges = 0 - try FileManager.default.removeItem(at: testFile) - wait(for: [expectation], timeout: 10) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + try? FileManager.default.removeItem(at: testFile) + } + await fulfillment(of: [expectation], timeout: 10) - XCTAssertGreaterThan(Watcher.fileChanges, 0) + XCTAssertGreaterThan(Watcher.fileChanges, 0) + } }