diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 218f6b3..1ff439c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app - name: Test - run: swift test -v --skip-update --parallel --enable-code-coverage + run: swift test --parallel --enable-code-coverage env: DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer diff --git a/Package.resolved b/Package.resolved index 20dc274..4b12bad 100644 --- a/Package.resolved +++ b/Package.resolved @@ -3,11 +3,11 @@ "pins": [ { "package": "GraphViz", - "repositoryURL": "https://github.com/ctreffs/GraphViz.git", + "repositoryURL": "https://github.com/SwiftDocOrg/GraphViz.git", "state": { - "branch": "master", - "revision": "b24203b1468e8e2faf89a907b268af5d73a18b42", - "version": null + "branch": null, + "revision": "74b6cbd8c5ecea9f64d84c4e1c88d65604dd033f", + "version": "0.4.1" } }, { diff --git a/Package.swift b/Package.swift index 19c3b66..9ccae87 100644 --- a/Package.swift +++ b/Package.swift @@ -1,25 +1,32 @@ -// swift-tools-version:5.2 +// swift-tools-version:5.3 import PackageDescription let package = Package( name: "FirebladeGraph", + platforms: [ + .macOS(.v11), + .iOS(.v13), + ], products: [ .library( name: "FirebladeGraph", - targets: ["FirebladeGraph"]), + targets: ["FirebladeGraph"] + ), ], dependencies: [ - .package(url: "https://github.com/ctreffs/GraphViz.git", .branch("master")), + .package(url: "https://github.com/SwiftDocOrg/GraphViz.git", from: "0.4.1"), .package(url: "https://github.com/davecom/SwiftGraph.git", from: "3.1.0"), - .package(name: "SnapshotTesting", url: "https://github.com/pointfreeco/swift-snapshot-testing.git", from: "1.11.0") + .package(name: "SnapshotTesting", url: "https://github.com/pointfreeco/swift-snapshot-testing.git", from: "1.11.0"), ], targets: [ .target( name: "FirebladeGraph", - dependencies: ["GraphViz", "SwiftGraph"]), + dependencies: ["GraphViz", "SwiftGraph"] + ), .testTarget( name: "FirebladeGraphTests", - dependencies: ["FirebladeGraph", "SnapshotTesting"]), + dependencies: ["FirebladeGraph", "SnapshotTesting"] + ), ] ) diff --git a/Sources/FirebladeGraph/GraphvizRepresentable.swift b/Sources/FirebladeGraph/GraphvizRepresentable.swift index 2414f6b..3725446 100644 --- a/Sources/FirebladeGraph/GraphvizRepresentable.swift +++ b/Sources/FirebladeGraph/GraphvizRepresentable.swift @@ -7,7 +7,6 @@ import struct Foundation.Data import struct Foundation.UUID -import DOT import GraphViz public protocol GraphVizNodeRepresentable { @@ -15,7 +14,7 @@ public protocol GraphVizNodeRepresentable { } extension GraphVizNodeRepresentable { - internal func graphVizNode() -> GraphViz.Node { + func graphVizNode() -> GraphViz.Node { .init(graphVizNodeDescription()) } } @@ -23,12 +22,15 @@ extension GraphVizNodeRepresentable { extension String: GraphVizNodeRepresentable { public func graphVizNodeDescription() -> String { self } } + extension Int: GraphVizNodeRepresentable { public func graphVizNodeDescription() -> String { "\(self)" } } + extension UInt: GraphVizNodeRepresentable { public func graphVizNodeDescription() -> String { "\(self)" } } + extension UInt8: GraphVizNodeRepresentable { public func graphVizNodeDescription() -> String { "\(self)" } } @@ -38,32 +40,52 @@ extension UUID: GraphVizNodeRepresentable { } public protocol GraphVizRenderable { - func renderGraph(as format: Format) -> Data? + func renderGraph(as format: Format, completion: @escaping (Result) -> Void) +} + +public enum ImageError: Swift.Error { + case failedToCreateImage(_ from: Data) } #if canImport(AppKit) -import class AppKit.NSImage -public typealias Image = NSImage -extension GraphVizRenderable { - public func renderGraphAsImage() -> Image? { - guard let data = renderGraph(as: .png) else { - return nil - } + import class AppKit.NSImage + public typealias Image = NSImage + public extension GraphVizRenderable { + func renderGraphAsImage(completion: @escaping (Result) -> Void) { + renderGraph(as: .png) { result in + switch result { + case let .success(data): + if let image = Image(data: data) { + completion(.success(image)) + } else { + completion(.failure(ImageError.failedToCreateImage(data))) + } - return Image(data: data) + case let .failure(failure): + completion(.failure(failure)) + } + } + } } -} #elseif canImport(UIKit) -import class UIKit.UIImage -public typealias Image = UIImage -extension GraphVizRenderable { - public final func renderGraphAsImage() -> Image? { - guard let data = renderGraph(as: .png) else { - return nil - } + import class UIKit.UIImage + public typealias Image = UIImage + public extension GraphVizRenderable { + func renderGraphAsImage(completion: @escaping (Result) -> Void) { + renderGraph(as: .png) { result in + switch result { + case let .success(data): + if let image = Image(data: data) { + completion(.success(image)) + } else { + completion(.failure(ImageError.failedToCreateImage(data))) + } - return Image(data: data) + case let .failure(failure): + completion(.failure(failure)) + } + } + } } -} #endif diff --git a/Sources/FirebladeGraph/Node+Graphviz.swift b/Sources/FirebladeGraph/Node+Graphviz.swift index 1dd903a..90bd771 100644 --- a/Sources/FirebladeGraph/Node+Graphviz.swift +++ b/Sources/FirebladeGraph/Node+Graphviz.swift @@ -5,22 +5,18 @@ // Created by Christian Treffs on 24.03.20. // -import GraphViz import struct Foundation.Data +import GraphViz extension Node: GraphVizRenderable where Content: GraphVizNodeRepresentable { - public final func renderGraph(as format: Format) -> Data? { + public final func renderGraph(as format: Format, completion: @escaping (Result) -> Void) { var graph = Graph(directed: true, strict: true) descend { node in node.renderNode(in: &graph) } - do { - return try graph.render(using: .dot, to: format) - } catch { - return nil - } + graph.render(using: .dot, to: format, completion: completion) } final func renderNode(in graph: inout GraphViz.Graph) { diff --git a/Sources/FirebladeGraph/Node.swift b/Sources/FirebladeGraph/Node.swift index 874d653..bb8bdf2 100644 --- a/Sources/FirebladeGraph/Node.swift +++ b/Sources/FirebladeGraph/Node.swift @@ -25,7 +25,7 @@ open class Node { public init(_ content: Content) { self.content = content - self.children = [] + children = [] } deinit { @@ -164,8 +164,7 @@ open class Node { /// Splitting the implementation of the update away from the update call /// itself allows the detail to be overridden without disrupting the /// general sequence of updateFromParent (e.g. raising events). - open func updateFromParent() { - } + open func updateFromParent() {} open func childrenNeedingUpdate() -> AnyIterator { AnyIterator(children.makeIterator()) @@ -173,6 +172,7 @@ open class Node { } // MARK: Equatable + extension Node: Equatable where Content: Equatable { public static func == (lhs: Node, rhs: Node) -> Bool { lhs.content == rhs.content @@ -180,6 +180,7 @@ extension Node: Equatable where Content: Equatable { } // MARK: Comparable + extension Node: Comparable where Content: Comparable { public static func < (lhs: Node, rhs: Node) -> Bool { lhs.content < rhs.content @@ -187,28 +188,31 @@ extension Node: Comparable where Content: Comparable { } // MARK: CustomStringConvertible + extension Node: CustomStringConvertible { - open var description: String { + public var description: String { "<\(type(of: self))>" } } // MARK: CustomDebugStringConvertible + extension Node: CustomDebugStringConvertible { - open var debugDescription: String { + public var debugDescription: String { "<\(type(of: self)) \(content)>" } } // MARK: Recursive description -extension Node { + +public extension Node { /// Recursively descripes this node and all it's children. - public var descriptionDescending: String { + var descriptionDescending: String { describeDescending(self) { $0.description } } /// Recursively debug descripes this node and all it's children. - public var debugDescriptionDescending: String { + var debugDescriptionDescending: String { describeDescending(self) { $0.debugDescription } } @@ -216,14 +220,14 @@ extension Node { /// - Parameter node: the start node. /// - Parameter level: current indentation level. /// - Parameter closure: a closure to apply for each node. - public func describeDescending(_ node: Node, _ level: Int = 0, using closure: (Node) -> String) -> String { + func describeDescending(_ node: Node, _ level: Int = 0, using closure: (Node) -> String) -> String { let prefix = String(repeating: " ", count: level) + "⮑ " - return prefix + closure(node) + "\n" + self.children.map { $0.describeDescending($0, level + 1, using: closure) }.joined() + return prefix + closure(node) + "\n" + children.map { $0.describeDescending($0, level + 1, using: closure) }.joined() } } -extension Node where Content == Void { - public convenience init() { +public extension Node where Content == Void { + convenience init() { self.init(()) } } diff --git a/Sources/FirebladeGraph/SwiftGraph+Graphviz.swift b/Sources/FirebladeGraph/SwiftGraph+Graphviz.swift index abe7559..f4719da 100644 --- a/Sources/FirebladeGraph/SwiftGraph+Graphviz.swift +++ b/Sources/FirebladeGraph/SwiftGraph+Graphviz.swift @@ -5,30 +5,29 @@ // Created by Christian Treffs on 24.03.20. // -import DOT import Foundation import GraphViz @_exported import SwiftGraph extension UniqueElementsGraph: GraphVizRenderable where V: GraphVizNodeRepresentable { - public final func renderGraph(as format: Format) -> Data? { - drawGraphUnweigted(self, as: format) + public final func renderGraph(as format: Format, completion: @escaping (Result) -> Void) { + drawGraphUnweigted(self, as: format, completion: completion) } } extension UnweightedGraph: GraphVizRenderable where V: GraphVizNodeRepresentable { - public final func renderGraph(as format: Format) -> Data? { - drawGraphUnweigted(self, as: format) + public final func renderGraph(as format: Format, completion: @escaping (Result) -> Void) { + drawGraphUnweigted(self, as: format, completion: completion) } } extension WeightedGraph: GraphVizRenderable where V: GraphVizNodeRepresentable, W: Numeric { - public final func renderGraph(as format: Format) -> Data? { - drawGraphWeigted(self, as: format) + public final func renderGraph(as format: Format, completion: @escaping (Result) -> Void) { + drawGraphWeigted(self, as: format, completion: completion) } } -private func drawGraphUnweigted(_ graph: G, as format: Format) -> Data? where G: SwiftGraph.Graph, G.V: GraphVizNodeRepresentable { +private func drawGraphUnweigted(_ graph: G, as format: Format, completion: @escaping (Result) -> Void) where G: SwiftGraph.Graph, G.V: GraphVizNodeRepresentable { let directed = graph.isDAG var graphvizGraph = Graph(directed: directed, strict: true) @@ -47,14 +46,10 @@ private func drawGraphUnweigted(_ graph: G, as format: Format) -> Data? where let layout: LayoutAlgorithm = directed ? .dot : .sfdp - do { - return try graphvizGraph.render(using: layout, to: format) - } catch { - return nil - } + graphvizGraph.render(using: layout, to: format, completion: completion) } -private func drawGraphWeigted(_ graph: G, as format: Format) -> Data? where G: SwiftGraph.Graph, G.V: GraphVizNodeRepresentable, G.E == WeightedEdge, W: Numeric { +private func drawGraphWeigted(_ graph: G, as format: Format, completion: @escaping (Result) -> Void) where G: SwiftGraph.Graph, G.V: GraphVizNodeRepresentable, G.E == WeightedEdge, W: Numeric { let directed = graph.isDAG var graphvizGraph = Graph(directed: directed, strict: true) @@ -76,9 +71,5 @@ private func drawGraphWeigted(_ graph: G, as format: Format) -> Data? wher let layout: LayoutAlgorithm = directed ? .dot : .sfdp - do { - return try graphvizGraph.render(using: layout, to: format) - } catch { - return nil - } + graphvizGraph.render(using: layout, to: format, completion: completion) } diff --git a/Tests/FirebladeGraphTests/TestVisualization.swift b/Tests/FirebladeGraphTests/TestVisualization.swift index cf40377..ff9bca8 100644 --- a/Tests/FirebladeGraphTests/TestVisualization.swift +++ b/Tests/FirebladeGraphTests/TestVisualization.swift @@ -20,7 +20,9 @@ final class TestVisualization: XCTestCase { "\(firstName) \(lastName)" } } - func testCicularUndirectedVisualization() { + + func testCicularUndirectedVisualization() throws { + let exp = expectation(description: "\(#function)") let john = Person(firstName: "John", lastName: "Doe") let jane = Person(firstName: "Jane", lastName: "Doe") let max = Person(firstName: "Max", lastName: "Mustermann") @@ -31,15 +33,24 @@ final class TestVisualization: XCTestCase { friends.addEdge(from: max, to: jane) #if !os(Linux) - guard let image = friends.renderGraphAsImage() else { - XCTFail("No rendering done") - return - } - assertSnapshot(matching: image, as: .image) + friends.renderGraphAsImage { result in + switch result { + case let .success(image): + DispatchQueue.main.async { + assertSnapshot(matching: image, as: .image) + exp.fulfill() + } + + case let .failure(failure): + XCTFail("\(failure)") + } + } + wait(for: [exp], timeout: 3.0) #endif } func testCicularDirectedVisualization() { + let exp = expectation(description: "\(#function)") let john = Person(firstName: "John", lastName: "Doe") let jane = Person(firstName: "Jane", lastName: "Doe") let max = Person(firstName: "Max", lastName: "Mustermann") @@ -50,16 +61,25 @@ final class TestVisualization: XCTestCase { friends.addEdge(from: max, to: jane, directed: true) #if !os(Linux) - guard let image = friends.renderGraphAsImage() else { - XCTFail("No rendering done") - return - } - assertSnapshot(matching: image, as: .image) + friends.renderGraphAsImage { result in + switch result { + case let .success(image): + DispatchQueue.main.async { + assertSnapshot(matching: image, as: .image) + exp.fulfill() + } + + case let .failure(failure): + XCTFail("\(failure)") + } + } + wait(for: [exp], timeout: 3.0) #endif } func testCityGraphVisualization() { - let cityGraph: WeightedGraph = WeightedGraph(vertices: ["Seattle", "San Francisco", "Los Angeles", "Denver", "Kansas City", "Chicago", "Boston", "New York", "Atlanta", "Miami", "Dallas", "Houston"]) + let exp = expectation(description: "\(#function)") + let cityGraph = WeightedGraph(vertices: ["Seattle", "San Francisco", "Los Angeles", "Denver", "Kansas City", "Chicago", "Boston", "New York", "Atlanta", "Miami", "Dallas", "Houston"]) cityGraph.addEdge(from: "Seattle", to: "Chicago", weight: 2097) cityGraph.addEdge(from: "Seattle", to: "Chicago", weight: 2097) @@ -87,11 +107,19 @@ final class TestVisualization: XCTestCase { cityGraph.addEdge(from: "Houston", to: "Dallas", weight: 239) #if !os(Linux) - guard let image = cityGraph.renderGraphAsImage() else { - XCTFail("No rendering done") - return - } - assertSnapshot(matching: image, as: .image) + cityGraph.renderGraphAsImage { result in + switch result { + case let .success(image): + DispatchQueue.main.async { + assertSnapshot(matching: image, as: .image) + exp.fulfill() + } + + case let .failure(failure): + XCTFail("\(failure)") + } + } + wait(for: [exp], timeout: 3.0) #endif } } diff --git a/Tests/FirebladeGraphTests/TraversalTests.swift b/Tests/FirebladeGraphTests/TraversalTests.swift index 737847a..28c515b 100644 --- a/Tests/FirebladeGraphTests/TraversalTests.swift +++ b/Tests/FirebladeGraphTests/TraversalTests.swift @@ -5,10 +5,10 @@ // Created by Christian Treffs on 22.08.19. // -import XCTest import FirebladeGraph import struct Foundation.UUID import SnapshotTesting +import XCTest // swiftlint:disable identifier_name @@ -43,13 +43,24 @@ final class TraversalTests: XCTestCase { result.append($0.content) } - let expected = [a, b, c, d, e, f, g, h, i, j].map { $0.content } + let expected = [a, b, c, d, e, f, g, h, i, j].map(\.content) XCTAssertEqual(result, expected) - let image = try XCTUnwrap(a.renderGraphAsImage()) - #if !os(Linux) - assertSnapshot(matching: image, as: .image) + let exp = expectation(description: "\(#function)") + a.renderGraphAsImage { result in + switch result { + case let .success(image): + DispatchQueue.main.async { + assertSnapshot(matching: image, as: .image) + exp.fulfill() + } + + case let .failure(failure): + XCTFail("\(failure)") + } + } + wait(for: [exp], timeout: 3.0) #endif } @@ -83,12 +94,24 @@ final class TraversalTests: XCTestCase { a.descend { result.append($0.content) } - let expected = [a, b, c, e, f, g, d, h, i, j].map { $0.content } + let expected = [a, b, c, e, f, g, d, h, i, j].map(\.content) XCTAssertEqual(result, expected) - let image = try XCTUnwrap(a.renderGraphAsImage()) #if !os(Linux) - assertSnapshot(matching: image, as: .image) + let exp = expectation(description: "\(#function)") + a.renderGraphAsImage { result in + switch result { + case let .success(image): + DispatchQueue.main.async { + assertSnapshot(matching: image, as: .image) + exp.fulfill() + } + + case let .failure(failure): + XCTFail("\(failure)") + } + } + wait(for: [exp], timeout: 3.0) #endif } @@ -116,7 +139,7 @@ final class TraversalTests: XCTestCase { let result: [UUID] = a.descendReduce([UUID]()) { $0 + [$1.content] } - let expected = [a, b, c, d, e, f, g, h, i, j].map { $0.content } + let expected = [a, b, c, d, e, f, g, h, i, j].map(\.content) XCTAssertEqual(result, expected) } @@ -148,7 +171,7 @@ final class TraversalTests: XCTestCase { result.append($0.content) } - let expected = [a, b, c, d, e, f, g, h, i, j].reversed().map { $0.content } + let expected = [a, b, c, d, e, f, g, h, i, j].reversed().map(\.content) XCTAssertEqual(result, expected) } @@ -176,7 +199,7 @@ final class TraversalTests: XCTestCase { let result: [UUID] = j.ascendReduce([UUID]()) { $0 + [$1.content] } - let expected = [a, b, c, d, e, f, g, h, i, j].reversed().map { $0.content } + let expected = [a, b, c, d, e, f, g, h, i, j].reversed().map(\.content) XCTAssertEqual(result, expected) } } diff --git a/Tests/FirebladeGraphTests/XCTestManifests.swift b/Tests/FirebladeGraphTests/XCTestManifests.swift deleted file mode 100644 index 6609011..0000000 --- a/Tests/FirebladeGraphTests/XCTestManifests.swift +++ /dev/null @@ -1,60 +0,0 @@ -#if !canImport(ObjectiveC) -import XCTest - -extension NodeTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__NodeTests = [ - ("testBasics", testBasics), - ("testDescriptionDescending", testDescriptionDescending), - ("testEquality", testEquality), - ("testRemoveAllChildren", testRemoveAllChildren), - ("testRemoveChildAtIndex", testRemoveChildAtIndex), - ("testRemoveMissingChild", testRemoveMissingChild), - ] -} - -extension TestVisualization { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__TestVisualization = [ - ("testCicularDirectedVisualization", testCicularDirectedVisualization), - ("testCicularUndirectedVisualization", testCicularUndirectedVisualization), - ("testCityGraphVisualization", testCityGraphVisualization), - ] -} - -extension TraversalTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__TraversalTests = [ - ("testAscendLinearGraph", testAscendLinearGraph), - ("testAscendReduceLinearGraph", testAscendReduceLinearGraph), - ("testDescendLinearGraph", testDescendLinearGraph), - ("testDescendReduceLinearGraph", testDescendReduceLinearGraph), - ("testDescendSpreadingGraph", testDescendSpreadingGraph), - ] -} - -extension UpdateTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__UpdateTests = [ - ("testUpdateChildren", testUpdateChildren), - ("testUpdateSelf", testUpdateSelf), - ] -} - -public func __allTests() -> [XCTestCaseEntry] { - return [ - testCase(NodeTests.__allTests__NodeTests), - testCase(TestVisualization.__allTests__TestVisualization), - testCase(TraversalTests.__allTests__TraversalTests), - testCase(UpdateTests.__allTests__UpdateTests), - ] -} -#endif diff --git a/Tests/FirebladeGraphTests/__Snapshots__/TestVisualization/testCicularDirectedVisualization.1.png b/Tests/FirebladeGraphTests/__Snapshots__/TestVisualization/testCicularDirectedVisualization.1.png index b16edeb..c3cd0b5 100644 Binary files a/Tests/FirebladeGraphTests/__Snapshots__/TestVisualization/testCicularDirectedVisualization.1.png and b/Tests/FirebladeGraphTests/__Snapshots__/TestVisualization/testCicularDirectedVisualization.1.png differ diff --git a/Tests/FirebladeGraphTests/__Snapshots__/TestVisualization/testCicularUndirectedVisualization.1.png b/Tests/FirebladeGraphTests/__Snapshots__/TestVisualization/testCicularUndirectedVisualization.1.png index b77a25f..106b7ff 100644 Binary files a/Tests/FirebladeGraphTests/__Snapshots__/TestVisualization/testCicularUndirectedVisualization.1.png and b/Tests/FirebladeGraphTests/__Snapshots__/TestVisualization/testCicularUndirectedVisualization.1.png differ diff --git a/Tests/FirebladeGraphTests/__Snapshots__/TestVisualization/testCityGraphVisualization.1.png b/Tests/FirebladeGraphTests/__Snapshots__/TestVisualization/testCityGraphVisualization.1.png index f0c2d64..49eaccf 100644 Binary files a/Tests/FirebladeGraphTests/__Snapshots__/TestVisualization/testCityGraphVisualization.1.png and b/Tests/FirebladeGraphTests/__Snapshots__/TestVisualization/testCityGraphVisualization.1.png differ diff --git a/Tests/FirebladeGraphTests/__Snapshots__/TraversalTests/testDescendLinearGraph.1.png b/Tests/FirebladeGraphTests/__Snapshots__/TraversalTests/testDescendLinearGraph.1.png index 4aff12e..4b6b7d4 100644 Binary files a/Tests/FirebladeGraphTests/__Snapshots__/TraversalTests/testDescendLinearGraph.1.png and b/Tests/FirebladeGraphTests/__Snapshots__/TraversalTests/testDescendLinearGraph.1.png differ diff --git a/Tests/FirebladeGraphTests/__Snapshots__/TraversalTests/testDescendSpreadingGraph.1.png b/Tests/FirebladeGraphTests/__Snapshots__/TraversalTests/testDescendSpreadingGraph.1.png index 1b4ac9e..1651b57 100644 Binary files a/Tests/FirebladeGraphTests/__Snapshots__/TraversalTests/testDescendSpreadingGraph.1.png and b/Tests/FirebladeGraphTests/__Snapshots__/TraversalTests/testDescendSpreadingGraph.1.png differ diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift deleted file mode 100644 index 5281d03..0000000 --- a/Tests/LinuxMain.swift +++ /dev/null @@ -1,8 +0,0 @@ -import XCTest - -import FirebladeGraphTests - -var tests = [XCTestCaseEntry]() -tests += FirebladeGraphTests.__allTests() - -XCTMain(tests)