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/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)))
+ }
+ }
+}
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(
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",