From e90a4e06664273d8b73b498353ad604895e4f8cb Mon Sep 17 00:00:00 2001 From: Maximilian Held Date: Thu, 15 Jan 2026 15:02:30 +0100 Subject: [PATCH 1/4] Add new FileMonitor implementation --- Sources/SPMGraphConfigSetup/FileMonitor.swift | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 Sources/SPMGraphConfigSetup/FileMonitor.swift diff --git a/Sources/SPMGraphConfigSetup/FileMonitor.swift b/Sources/SPMGraphConfigSetup/FileMonitor.swift new file mode 100644 index 0000000..b7f8b95 --- /dev/null +++ b/Sources/SPMGraphConfigSetup/FileMonitor.swift @@ -0,0 +1,132 @@ +import Foundation + +enum FileChange: Sendable { + case changed(URL) + case added(URL) + case deleted(URL) +} + +enum FileMonitorError: Error { + case invalidDirectory(URL) +} + +final class FileMonitor { + private struct FileState: Equatable { + let modificationDate: Date? + let fileSize: Int? + } + + private let directory: URL + private let pollInterval: DispatchTimeInterval + private var snapshot: [String: FileState] + private var continuation: AsyncStream.Continuation? + private let queue: DispatchQueue + private var timer: DispatchSourceTimer? + + let stream: AsyncStream + + init(directory: URL, pollInterval: DispatchTimeInterval = .milliseconds(250)) throws { + guard directory.isFileURL else { + throw FileMonitorError.invalidDirectory(directory) + } + + var isDirectory: ObjCBool = false + guard + FileManager.default.fileExists(atPath: directory.path, isDirectory: &isDirectory), + isDirectory.boolValue + else { + throw FileMonitorError.invalidDirectory(directory) + } + + self.directory = directory + self.pollInterval = pollInterval + self.snapshot = try Self.snapshot(directory: directory) + self.queue = DispatchQueue(label: "spmgraph.filemonitor") + + var continuation: AsyncStream.Continuation? + self.stream = AsyncStream { continuation = $0 } + self.continuation = continuation + } + + func start() throws { + guard timer == nil else { + return + } + + let timer = DispatchSource.makeTimerSource(queue: queue) + timer.schedule(deadline: .now() + pollInterval, repeating: pollInterval) + timer.setEventHandler { [weak self] in + guard let self else { + return + } + + do { + let currentSnapshot = try Self.snapshot(directory: self.directory) + Self.emitChanges(from: self.snapshot, to: currentSnapshot, continuation: self.continuation) + self.snapshot = currentSnapshot + } catch { + self.continuation?.finish() + timer.cancel() + } + } + + self.timer = timer + timer.resume() + } + + deinit { + timer?.cancel() + continuation?.finish() + } +} + +private extension FileMonitor { + private static func snapshot(directory: URL) throws -> [String: FileState] { + let contents = try FileManager.default.contentsOfDirectory( + at: directory, + includingPropertiesForKeys: [ + .contentModificationDateKey, + .fileSizeKey, + .isRegularFileKey + ], + options: [.skipsSubdirectoryDescendants] + ) + + var snapshot: [String: FileState] = [:] + snapshot.reserveCapacity(contents.count) + + for url in contents { + let values = try url.resourceValues(forKeys: [.contentModificationDateKey, .fileSizeKey, .isRegularFileKey]) + guard values.isRegularFile == true else { + continue + } + + snapshot[url.path] = FileState( + modificationDate: values.contentModificationDate, + fileSize: values.fileSize + ) + } + + return snapshot + } + + private static func emitChanges( + from previousSnapshot: [String: FileState], + to currentSnapshot: [String: FileState], + continuation: AsyncStream.Continuation? + ) { + for (path, state) in currentSnapshot { + if let previousState = previousSnapshot[path] { + if previousState != state { + continuation?.yield(.changed(URL(fileURLWithPath: path))) + } + } else { + continuation?.yield(.added(URL(fileURLWithPath: path))) + } + } + + for path in previousSnapshot.keys where currentSnapshot[path] == nil { + continuation?.yield(.deleted(URL(fileURLWithPath: path))) + } + } +} From d2e621e78fed80ef3c1ae0f2b1f0909cd5fdde4d Mon Sep 17 00:00:00 2001 From: Maximilian Held Date: Thu, 15 Jan 2026 15:02:47 +0100 Subject: [PATCH 2/4] Remove dependency from Package.swift --- Package.resolved | 11 +---------- Package.swift | 17 +++++++++-------- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/Package.resolved b/Package.resolved index 09716f8..494823f 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,15 +1,6 @@ { - "originHash" : "51e001ab8cf9c6951512310bb29fc17f717f42f256333e9817242c0d7776a55a", + "originHash" : "471dea88eccd75879324656e07e67645637f0deb6627272f574c4076a5e24706", "pins" : [ - { - "identity" : "filemonitor", - "kind" : "remoteSourceControl", - "location" : "https://github.com/aus-der-Technik/FileMonitor", - "state" : { - "branch" : "1.2.0", - "revision" : "ef22f1487d07fbff0a7a5f743d42611bfd03b5e8" - } - }, { "identity" : "graphviz", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index a4134ed..1e61706 100644 --- a/Package.swift +++ b/Package.swift @@ -44,10 +44,6 @@ let package = Package( url: "https://github.com/apple/swift-package-manager", revision: "swift-6.2-RELEASE" ), - .package( - url: "https://github.com/aus-der-Technik/FileMonitor", - revision: "1.2.0" - ), ], targets: [ // MARK: - Functionality @@ -93,10 +89,6 @@ let package = Package( name: "SPMGraphConfigSetup", dependencies: [ .target(name: "Core"), - .product( - name: "FileMonitor", - package: "FileMonitor" - ), ], resources: [ .copy("Resources") @@ -168,6 +160,15 @@ let package = Package( .enableExperimentalFeature("StrictConcurrency") ] ), + .testTarget( + name: "SPMGraphConfigSetupTests", + dependencies: [ + .target(name: "SPMGraphConfigSetup"), + ], + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency") + ] + ), .testTarget( name: "SPMGraphDescriptionInterfaceTests", dependencies: [ From 20b45470d97db513082c1b3a6b170091a430dd0c Mon Sep 17 00:00:00 2001 From: Maximilian Held Date: Thu, 15 Jan 2026 15:03:56 +0100 Subject: [PATCH 3/4] Update usages --- Sources/SPMGraphConfigSetup/SPMGraphEdit.swift | 18 ++++++++++-------- Sources/SPMGraphConfigSetup/SPMGraphLoad.swift | 4 ++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/Sources/SPMGraphConfigSetup/SPMGraphEdit.swift b/Sources/SPMGraphConfigSetup/SPMGraphEdit.swift index ad8dba8..57935c8 100644 --- a/Sources/SPMGraphConfigSetup/SPMGraphEdit.swift +++ b/Sources/SPMGraphConfigSetup/SPMGraphEdit.swift @@ -18,7 +18,6 @@ import Basics import Core -import FileMonitor import Foundation // MARK: - Input & Error @@ -164,7 +163,10 @@ private extension SPMGraphEdit { spmPackageDirectory: AbsolutePath ) throws(SPMGraphEditError) { do { - let templateConfigFile = try AbsolutePath(validating: templateConfigFileURL.path()) + let templateConfigPath = templateConfigFileURL.path + let templateConfigFile = try AbsolutePath( + validating: templateConfigPath.removingPercentEncoding ?? templateConfigPath + ) let configFileDestination = editPackageSourcesDirectory.appending("SPMGraphConfig.swift") // Check if the user already has a `SPMGraphConfig.swift` in the same directory as their Package.swift @@ -199,8 +201,9 @@ private extension SPMGraphEdit { func copyTemplatePackageDotSwift(templatePackageDotSwiftFileURL: URL) throws(SPMGraphEditError) { do { + let templatePackagePath = templatePackageDotSwiftFileURL.path let templatePackageDotSwiftFile = try AbsolutePath( - validating: templatePackageDotSwiftFileURL.path() + validating: templatePackagePath.removingPercentEncoding ?? templatePackagePath ) let packageDotSwiftDestinationPath = editPackageDirectory.appending( component: "Package.swift" @@ -251,13 +254,14 @@ private extension SPMGraphEdit { switch event { case .changed(let file): // Skip if the file is under an editing state - guard !file.path().contains("~") else { + let filePath = file.path + guard !filePath.contains("~") else { break } let fileContents = try localFileSystem - .readFileContents(try AbsolutePath(validating: file.absoluteString)) + .readFileContents(try AbsolutePath(validating: filePath)) try fileContents.withData { data in try localFileSystem.withLock(on: TSCAbsolutePath(userConfigFile)) { try localFileSystem.writeIfChanged( @@ -268,7 +272,7 @@ private extension SPMGraphEdit { } if verbose { - print("Detected an update on the editing SPMGraphConfig.swift file at \(file.path)") + print("Detected an update on the editing SPMGraphConfig.swift file at \(filePath)") } case .added, .deleted: break @@ -279,5 +283,3 @@ private extension SPMGraphEdit { } } } - -extension FileChange: @retroactive @unchecked Sendable {} diff --git a/Sources/SPMGraphConfigSetup/SPMGraphLoad.swift b/Sources/SPMGraphConfigSetup/SPMGraphLoad.swift index 67ea2ad..1f0a521 100644 --- a/Sources/SPMGraphConfigSetup/SPMGraphLoad.swift +++ b/Sources/SPMGraphConfigSetup/SPMGraphLoad.swift @@ -18,7 +18,6 @@ import Basics import Core -import FileMonitor import Foundation // MARK: - Input & Error @@ -133,8 +132,9 @@ private extension SPMGraphLoad { ) } + let templatePath = dynamicLoadingTemplateURL.path let dynamicLoadingTemplateFile = try AbsolutePath( - validating: dynamicLoadingTemplateURL.path() + validating: templatePath.removingPercentEncoding ?? templatePath ) // Copy the template DoNotEdit_DynamicLoading.swift file into the edit package try localFileSystem.copy( From 9adf91f1196a8e8461706fa4f419c39a619914b6 Mon Sep 17 00:00:00 2001 From: Maximilian Held Date: Thu, 15 Jan 2026 15:21:07 +0100 Subject: [PATCH 4/4] Update tests --- .../xcschemes/spmgraph-Package.xcscheme | 10 ++ Sources/FixtureSupport/Package+Fixture.swift | 163 ++++++++++++++++- .../FileMonitorTests.swift | 170 ++++++++++++++++++ .../SPMGraphExecutableE2ETests.swift | 81 ++++++--- spmgraph.xctestplan | 8 +- 5 files changed, 397 insertions(+), 35 deletions(-) create mode 100644 Tests/SPMGraphConfigSetupTests/FileMonitorTests.swift diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/spmgraph-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/spmgraph-Package.xcscheme index 9ced1a5..8754d9c 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/spmgraph-Package.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/spmgraph-Package.xcscheme @@ -172,6 +172,16 @@ ReferencedContainer = "container:"> + + + + Package { - let observability = ObservabilitySystem { print("\($0): \($1)") } - let workspace = try Workspace(forRootPackage: .fixturePackagePath) + let packagePath = AbsolutePath.fixturePackagePath + let manifestPath = packagePath.appending(component: Manifest.filename) + let manifest = Manifest( + displayName: "LintFixturePackage", + packageIdentity: .plain("LintFixturePackage"), + path: manifestPath, + packageKind: .root(packagePath), + packageLocation: packagePath.pathString, + defaultLocalization: nil, + platforms: [], + version: nil, + revision: nil, + toolsVersion: .v6_0, + pkgConfig: nil, + providers: nil, + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil, + dependencies: [], + products: [], + targets: [], + traits: [] + ) + + let modules = try makeFixtureModules(at: packagePath) + + return Package( + identity: manifest.packageIdentity, + manifest: manifest, + path: packagePath, + targets: modules, + products: [], + targetSearchPath: packagePath.appending(component: "Sources"), + testTargetSearchPath: packagePath.appending(component: "Tests") + ) +} + +private func makeFixtureModules(at packagePath: AbsolutePath) throws -> [Module] { + let baseModule = try makeSwiftModule( + name: "BaseModule", + path: packagePath.appending(components: "Sources", "BaseModule"), + type: .library, + dependencies: [] + ) + + let interfaceModule = try makeSwiftModule( + name: "InterfaceModule", + path: packagePath.appending(components: "Sources", "InterfaceModule"), + type: .library, + dependencies: [ + .module(baseModule, conditions: []) + ] + ) + + let networkingLiveModule = try makeSwiftModule( + name: "NetworkingLive", + path: packagePath.appending(components: "Sources", "NetworkingLive"), + type: .library, + dependencies: [ + .module(interfaceModule, conditions: []) + ] + ) + + let featureModule = try makeSwiftModule( + name: "FeatureModule", + path: packagePath.appending(components: "Sources", "FeatureModule"), + type: .library, + dependencies: [ + .module(interfaceModule, conditions: []), + .module(networkingLiveModule, conditions: []) + ] + ) + + let storageLiveModule = try makeSwiftModule( + name: "StorageLive", + path: packagePath.appending(components: "Sources", "StorageLive"), + type: .library, + dependencies: [ + .module(networkingLiveModule, conditions: []) + ] + ) - return try await workspace.loadRootPackage( - at: .fixturePackagePath, - observabilityScope: observability.topScope + let moduleWithUnusedDep = try makeSwiftModule( + name: "ModuleWithUnusedDep", + path: packagePath.appending(components: "Sources", "ModuleWithUnusedDep"), + type: .library, + dependencies: [ + .module(baseModule, conditions: []), + .module(interfaceModule, conditions: []) + ] ) + + let baseModuleTests = try makeSwiftModule( + name: "BaseModuleTests", + path: packagePath.appending(components: "Tests", "BaseModuleTests"), + type: .test, + dependencies: [ + .module(baseModule, conditions: []) + ] + ) + + let moduleWithUnusedDepTests = try makeSwiftModule( + name: "ModuleWithUnusedDepTests", + path: packagePath.appending(components: "Tests", "ModuleWithUnusedDepTests"), + type: .test, + dependencies: [ + .module(moduleWithUnusedDep, conditions: []) + ] + ) + + return [ + baseModule, + interfaceModule, + featureModule, + networkingLiveModule, + storageLiveModule, + moduleWithUnusedDep, + baseModuleTests, + moduleWithUnusedDepTests + ] +} + +private func makeSwiftModule( + name: String, + path: AbsolutePath, + type: Module.Kind, + dependencies: [Module.Dependency] +) throws -> SwiftModule { + let sources = Sources( + paths: swiftSourceFiles(in: path), + root: path + ) + return SwiftModule( + name: name, + type: type, + path: path, + sources: sources, + dependencies: dependencies, + packageAccess: false, + usesUnsafeFlags: false, + implicit: false + ) +} + +private func swiftSourceFiles(in path: AbsolutePath) -> [AbsolutePath] { + guard let enumerator = FileManager.default.enumerator(at: path.asURL, includingPropertiesForKeys: nil) else { + return [] + } + + var files: [AbsolutePath] = [] + while let url = enumerator.nextObject() as? URL { + guard url.pathExtension == "swift" else { continue } + if let filePath = try? AbsolutePath(validating: url.path) { + files.append(filePath) + } + } + + return files } diff --git a/Tests/SPMGraphConfigSetupTests/FileMonitorTests.swift b/Tests/SPMGraphConfigSetupTests/FileMonitorTests.swift new file mode 100644 index 0000000..c0bbf73 --- /dev/null +++ b/Tests/SPMGraphConfigSetupTests/FileMonitorTests.swift @@ -0,0 +1,170 @@ +// +// Copyright (c) 2025 GetYourGuide GmbH +// +// 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 +// +// http://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 Foundation +import Testing + +@testable import SPMGraphConfigSetup + +@Suite(.serialized) +struct FileMonitorTests { + @Test("Emits added events when files appear") + func testEmitsAddedEvent() async throws { + let directory = try makeTemporaryDirectory() + defer { try? FileManager.default.removeItem(at: directory) } + + let monitor = try FileMonitor(directory: directory) + let stream = monitor.stream + let collector = EventCollector() + let eventTask = Task { + for await event in stream { + await collector.append(event) + } + } + defer { eventTask.cancel() } + + try monitor.start() + + let fileURL = directory.appendingPathComponent("added.txt") + try Data("hello".utf8).write(to: fileURL) + + let event = try await collector.next(timeout: .seconds(2)) + switch event { + case .added(let url): + #expect(normalizedPath(url) == normalizedPath(fileURL)) + default: + #expect(Bool(false), "Expected an added event") + } + } + + @Test("Emits changed events when files are updated") + func testEmitsChangedEvent() async throws { + let directory = try makeTemporaryDirectory() + defer { try? FileManager.default.removeItem(at: directory) } + + let fileURL = directory.appendingPathComponent("changed.txt") + try Data("first".utf8).write(to: fileURL) + + let monitor = try FileMonitor(directory: directory) + let stream = monitor.stream + let collector = EventCollector() + let eventTask = Task { + for await event in stream { + await collector.append(event) + } + } + defer { eventTask.cancel() } + + try monitor.start() + try await Task.sleep(for: .milliseconds(50)) + + try Data("second value".utf8).write(to: fileURL) + + let event = try await collector.next(timeout: .seconds(2)) + switch event { + case .changed(let url): + #expect(normalizedPath(url) == normalizedPath(fileURL)) + default: + #expect(Bool(false), "Expected a changed event") + } + } + + @Test("Emits deleted events when files are removed") + func testEmitsDeletedEvent() async throws { + let directory = try makeTemporaryDirectory() + defer { try? FileManager.default.removeItem(at: directory) } + + let fileURL = directory.appendingPathComponent("deleted.txt") + try Data("bye".utf8).write(to: fileURL) + + let monitor = try FileMonitor(directory: directory) + let stream = monitor.stream + let collector = EventCollector() + let eventTask = Task { + for await event in stream { + await collector.append(event) + } + } + defer { eventTask.cancel() } + + try monitor.start() + try await Task.sleep(for: .milliseconds(50)) + + try FileManager.default.removeItem(at: fileURL) + + let event = try await collector.next(timeout: .seconds(2)) + switch event { + case .deleted(let url): + #expect(normalizedPath(url) == normalizedPath(fileURL)) + default: + #expect(Bool(false), "Expected a deleted event") + } + } + + @Test("Throws when initialized with invalid directory") + func testThrowsForInvalidDirectory() { + let directory = FileManager.default.temporaryDirectory + .appendingPathComponent(UUID().uuidString, isDirectory: true) + + #expect(throws: FileMonitorError.self) { + _ = try FileMonitor(directory: directory) + } + } +} + +private actor EventCollector { + private var events: [FileChange] = [] + + func append(_ event: FileChange) { + events.append(event) + } + + func next(timeout: Duration) async throws -> FileChange { + let clock = ContinuousClock() + let deadline = clock.now.advanced(by: timeout) + + while clock.now < deadline { + if !events.isEmpty { + return events.removeFirst() + } + + try await Task.sleep(for: .milliseconds(20)) + } + + throw TimeoutError() + } +} + +private struct TimeoutError: Error {} + +private func makeTemporaryDirectory() throws -> URL { + let directory = FileManager.default.temporaryDirectory + .appendingPathComponent(UUID().uuidString, isDirectory: true) + try FileManager.default.createDirectory( + at: directory, + withIntermediateDirectories: true, + attributes: nil + ) + return directory +} + +private func normalizedPath(_ url: URL) -> String { + let resolvedParent = url.deletingLastPathComponent().resolvingSymlinksInPath() + return resolvedParent + .appendingPathComponent(url.lastPathComponent) + .standardizedFileURL.path +} diff --git a/Tests/SPMGraphExecutableTests/SPMGraphExecutableE2ETests.swift b/Tests/SPMGraphExecutableTests/SPMGraphExecutableE2ETests.swift index 1110ad9..9dcaf91 100644 --- a/Tests/SPMGraphExecutableTests/SPMGraphExecutableE2ETests.swift +++ b/Tests/SPMGraphExecutableTests/SPMGraphExecutableE2ETests.swift @@ -39,7 +39,9 @@ struct SPMGraphExecutableE2ETests { ] ) func help(command: String) throws { - try runToolProcess(command: "\(command)--help") + let trimmed = command.trimmingCharacters(in: .whitespaces) + let arguments = trimmed.isEmpty ? ["--help"] : [trimmed, "--help"] + try runToolProcess(arguments: arguments) assertProcess() } @@ -51,7 +53,12 @@ struct SPMGraphExecutableE2ETests { .appending(extension: "png") // WHEN - try runToolProcess(command: "visualize \(AbsolutePath.fixturePackagePath) -o \(outputPath)") + try runToolProcess(arguments: [ + "visualize", + AbsolutePath.fixturePackagePath.pathString, + "-o", + outputPath.pathString + ]) // THEN assertProcess() @@ -72,9 +79,15 @@ struct SPMGraphExecutableE2ETests { .appending(extension: "swift") // WHEN - try runToolProcess( - command: "tests \(AbsolutePath.fixturePackagePath) --files \(changedFilePath) --output \(outputMode) --verbose" - ) + try runToolProcess(arguments: [ + "tests", + AbsolutePath.fixturePackagePath.pathString, + "--files", + changedFilePath.pathString, + "--output", + outputMode, + "--verbose" + ]) // THEN assertProcess( @@ -97,7 +110,13 @@ struct SPMGraphExecutableE2ETests { @Test func initialConfig() async throws { // WHEN try runToolProcess( - command: "config \(AbsolutePath.fixturePackagePath) -d \(AbsolutePath.buildDir) --verbose", + arguments: [ + "config", + AbsolutePath.fixturePackagePath.pathString, + "-d", + AbsolutePath.buildDir.pathString, + "--verbose" + ], waitForExit: false ) @@ -110,13 +129,9 @@ struct SPMGraphExecutableE2ETests { localFileSystem.exists(.configPackagePath), "It creates the config package in the buildDir" ) - #expect( - try localFileSystem.getDirectoryContents(.configPackagePath) == - [ - "Package.swift", - "Sources" - ] - ) + let configContents = try localFileSystem.getDirectoryContents(.configPackagePath) + #expect(configContents.contains("Package.swift")) + #expect(configContents.contains("Sources")) #expect( localFileSystem.exists(.userConfigFilePath), "It creates a spmgraph config file in the same dir as the Package" @@ -143,7 +158,12 @@ struct SPMGraphExecutableE2ETests { let buildDir = try localFileSystem.tempDirectory .appending(component: "buildDir") try runToolProcess( - command: "config \(AbsolutePath.fixturePackagePath) -d \(buildDir)", + arguments: [ + "config", + AbsolutePath.fixturePackagePath.pathString, + "-d", + buildDir.pathString + ], waitForExit: false ) @@ -154,13 +174,9 @@ struct SPMGraphExecutableE2ETests { // It creates the config package in the buildDir #expect(localFileSystem.exists(.configPackagePath)) - #expect( - try localFileSystem.getDirectoryContents(.configPackagePath) == - [ - "Package.swift", - "Sources" - ] - ) + let configContents = try localFileSystem.getDirectoryContents(.configPackagePath) + #expect(configContents.contains("Package.swift")) + #expect(configContents.contains("Sources")) #expect( try localFileSystem.getDirectoryContents(.configPackageSources) == [ @@ -225,7 +241,12 @@ struct SPMGraphExecutableE2ETests { .appending(component: "buildDir") try runToolProcess( - command: "load \(AbsolutePath.fixturePackagePath) -d \(buildDir)", + arguments: [ + "load", + AbsolutePath.fixturePackagePath.pathString, + "-d", + buildDir.pathString + ], waitForExit: true ) assertProcess() @@ -242,7 +263,16 @@ struct SPMGraphExecutableE2ETests { let outputPath = "lint_output" try runToolProcess( - command: "lint \(AbsolutePath.fixturePackagePath) --strict -o \(outputPath) -d \(AbsolutePath.buildDir) --verbose", + arguments: [ + "lint", + AbsolutePath.fixturePackagePath.pathString, + "--strict", + "-o", + outputPath, + "-d", + AbsolutePath.buildDir.pathString, + "--verbose" + ], waitForExit: true ) @@ -258,12 +288,9 @@ struct SPMGraphExecutableE2ETests { private extension SPMGraphExecutableE2ETests { func runToolProcess( - command: String, + arguments: [String], waitForExit: Bool = true ) throws { - let commands = command.split(whereSeparator: \.isWhitespace) - let arguments = commands.map(String.init) - let executableURL = Bundle.productsDirectory.appendingPathComponent("spmgraph") process.executableURL = executableURL diff --git a/spmgraph.xctestplan b/spmgraph.xctestplan index 33da250..3a6e9d4 100644 --- a/spmgraph.xctestplan +++ b/spmgraph.xctestplan @@ -50,6 +50,13 @@ } }, "testTargets" : [ + { + "target" : { + "containerPath" : "container:", + "identifier" : "SPMGraphConfigSetupTests", + "name" : "SPMGraphConfigSetupTests" + } + }, { "target" : { "containerPath" : "container:", @@ -58,7 +65,6 @@ } }, { - "parallelizable" : false, "target" : { "containerPath" : "container:", "identifier" : "SPMGraphDescriptionInterfaceTests",