From ce16cc1356a418b7e7f43f238c1de031b930d2ec Mon Sep 17 00:00:00 2001 From: Morgan McColl Date: Fri, 31 May 2024 00:18:18 +1000 Subject: [PATCH 01/20] Now depend on VHDLKripkeStructures. --- Package.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index 7e16b2c..30f0e9f 100644 --- a/Package.swift +++ b/Package.swift @@ -21,7 +21,8 @@ let package = Package( .package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.0"), .package(url: "https://github.com/mipalgu/VHDLKripkeStructureGenerator.git", from: "0.3.1"), .package(url: "https://github.com/CPSLabGU/SwiftUtils.git", from: "0.1.0"), - .package(url: "https://github.com/CPSLabGU/VHDLJSModels", from: "1.0.0") + .package(url: "https://github.com/CPSLabGU/VHDLJSModels", from: "1.0.0"), + .package(url: "https://github.com/CPSLabGU/VHDLKripkeStructures", from: "1.2.1") ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. @@ -34,7 +35,8 @@ let package = Package( .product(name: "VHDLParsing", package: "VHDLParsing"), .product(name: "VHDLKripkeStructureGenerator", package: "VHDLKripkeStructureGenerator"), .product(name: "SwiftUtils", package: "SwiftUtils"), - .product(name: "VHDLJSModels", package: "VHDLJSModels") + .product(name: "VHDLJSModels", package: "VHDLJSModels"), + .product(name: "VHDLKripkeStructures", package: "VHDLKripkeStructures") ] ), .testTarget( @@ -47,7 +49,8 @@ let package = Package( .product(name: "VHDLKripkeStructureGenerator", package: "VHDLKripkeStructureGenerator"), .product(name: "SwiftUtils", package: "SwiftUtils"), .product(name: "VHDLJSModels", package: "VHDLJSModels"), - "TestHelpers" + "TestHelpers", + .product(name: "VHDLKripkeStructures", package: "VHDLKripkeStructures") ] ), .testTarget( From 9b46b557420e29c55101989f2be7ea32bfe25a41 Mon Sep 17 00:00:00 2001 From: Morgan McColl Date: Fri, 31 May 2024 00:18:49 +1000 Subject: [PATCH 02/20] Changed PathArgument to throw when the file does not exist. --- Sources/MachineGenerator/CleanCommand.swift | 6 ++-- Sources/MachineGenerator/InstallCommand.swift | 4 +-- Sources/MachineGenerator/PathArgument.swift | 28 ++++++++++++++++--- Sources/MachineGenerator/VHDLGenerator.swift | 12 ++++---- .../PathArgumentTests.swift | 26 ++++++++++++++++- 5 files changed, 61 insertions(+), 15 deletions(-) diff --git a/Sources/MachineGenerator/CleanCommand.swift b/Sources/MachineGenerator/CleanCommand.swift index f3ac45a..e0974a0 100644 --- a/Sources/MachineGenerator/CleanCommand.swift +++ b/Sources/MachineGenerator/CleanCommand.swift @@ -83,7 +83,7 @@ struct CleanCommand: ParsableCommand { return } try cleanBuildFolder(manager: manager) - guard !options.pathURL.lastPathComponent.lowercased().hasSuffix(".arrangement") else { + guard !(try options.pathURL.lastPathComponent.lowercased().hasSuffix(".arrangement")) else { _ = try? manager.removeItem( at: options.pathURL.appendingPathComponent("arrangement.json", isDirectory: false) ) @@ -98,7 +98,7 @@ struct CleanCommand: ParsableCommand { @inlinable func cleanBuildFolder(manager: FileManager) throws { var isDirectory: ObjCBool = false - guard manager.fileExists(atPath: options.buildFolder.path, isDirectory: &isDirectory) else { + guard manager.fileExists(atPath: try options.buildFolder.path, isDirectory: &isDirectory) else { return } guard isDirectory.boolValue else { @@ -113,7 +113,7 @@ struct CleanCommand: ParsableCommand { @inlinable func cleanMachine(manager: FileManager) throws { var isDirectory: ObjCBool = true - guard manager.fileExists(atPath: options.machine.path, isDirectory: &isDirectory) else { + guard manager.fileExists(atPath: try options.machine.path, isDirectory: &isDirectory) else { return } guard !isDirectory.boolValue else { diff --git a/Sources/MachineGenerator/InstallCommand.swift b/Sources/MachineGenerator/InstallCommand.swift index 9d388d9..ed1fee0 100644 --- a/Sources/MachineGenerator/InstallCommand.swift +++ b/Sources/MachineGenerator/InstallCommand.swift @@ -85,7 +85,7 @@ struct InstallCommand: ParsableCommand { /// - Throws: ``GenerationError``. @inlinable func run() throws { - let folder = self.path.pathURL.lastPathComponent.lowercased() + let folder = try self.path.pathURL.lastPathComponent.lowercased() guard folder.hasSuffix(".machine") || folder.hasSuffix(".arrangement") else { throw GenerationError.invalidMachine(message: "The path provided is not a machine.") } @@ -102,7 +102,7 @@ struct InstallCommand: ParsableCommand { } isDirectory = false guard - manager.fileExists(atPath: self.path.buildFolder.path, isDirectory: &isDirectory), + manager.fileExists(atPath: try self.path.buildFolder.path, isDirectory: &isDirectory), isDirectory.boolValue else { throw GenerationError.invalidGeneration( diff --git a/Sources/MachineGenerator/PathArgument.swift b/Sources/MachineGenerator/PathArgument.swift index f6e7f08..7bfdd0f 100644 --- a/Sources/MachineGenerator/PathArgument.swift +++ b/Sources/MachineGenerator/PathArgument.swift @@ -64,24 +64,44 @@ struct PathArgument: ParsableArguments { @Argument(help: "The path to the machine folder.", completion: .directory) var path: String + /// Whether the `path` is a directory. + @inlinable var isDirectory: Bool { + get throws { + let manager = FileManager() + var isDirectory: ObjCBool = false + guard manager.fileExists(atPath: path, isDirectory: &isDirectory) else { + throw GenerationError.invalidInput(message: "The path does not exist.") + } + return isDirectory.boolValue + } + } + /// The path to the machine file. @inlinable var machine: URL { - pathURL.appendingPathComponent("machine.json", isDirectory: false) + get throws { + try pathURL.appendingPathComponent("machine.json", isDirectory: false) + } } /// The path to the users input (the machine folder). @inlinable var pathURL: URL { - URL(fileURLWithPath: path, isDirectory: true) + get throws { + try URL(fileURLWithPath: path, isDirectory: isDirectory) + } } /// The path to the build folder. @inlinable var buildFolder: URL { - pathURL.appendingPathComponent("build", isDirectory: true) + get throws { + try pathURL.appendingPathComponent("build", isDirectory: true) + } } /// The path to the vhdl folder. @inlinable var vhdlFolder: URL { - buildFolder.appendingPathComponent("vhdl", isDirectory: true) + get throws { + try buildFolder.appendingPathComponent("vhdl", isDirectory: true) + } } } diff --git a/Sources/MachineGenerator/VHDLGenerator.swift b/Sources/MachineGenerator/VHDLGenerator.swift index 173092e..9037c8f 100644 --- a/Sources/MachineGenerator/VHDLGenerator.swift +++ b/Sources/MachineGenerator/VHDLGenerator.swift @@ -81,11 +81,11 @@ struct VHDLGenerator: ParsableCommand { /// Runs the command. @inlinable mutating func run() throws { - guard !options.pathURL.lastPathComponent.lowercased().hasSuffix(".arrangement") else { + guard !(try options.pathURL.lastPathComponent.lowercased().hasSuffix(".arrangement")) else { try createArrangement() return } - let buildFolder = options.pathURL.appendingPathComponent("build", isDirectory: true) + let buildFolder = try options.pathURL.appendingPathComponent("build", isDirectory: true) try self.createMachine(sourcePath: options.pathURL, destinationPath: buildFolder) } @@ -94,7 +94,7 @@ struct VHDLGenerator: ParsableCommand { /// Create an arrangement. @inlinable func createArrangement() throws { - let nameRaw = options.pathURL.lastPathComponent.dropLast(".arrangement".count) + let nameRaw = try options.pathURL.lastPathComponent.dropLast(".arrangement".count) .trimmingCharacters(in: .whitespacesAndNewlines) guard !nameRaw.isEmpty, let name = VariableName(rawValue: nameRaw) else { throw GenerationError.invalidFormat(message: "The arrangement is not named correctly!") @@ -104,7 +104,9 @@ struct VHDLGenerator: ParsableCommand { contentsOf: options.pathURL.appendingPathComponent("model.json", isDirectory: false) ) let model = try decoder.decode(ArrangementModel.self, from: modelData) - let arrangementFile = options.pathURL.appendingPathComponent("arrangement.json", isDirectory: false) + let arrangementFile = try options.pathURL.appendingPathComponent( + "arrangement.json", isDirectory: false + ) let data = try Data(contentsOf: arrangementFile) let arrangement = try decoder.decode(Arrangement.self, from: data) let f: (Machine, VariableName) -> MachineRepresentation? = { @@ -118,7 +120,7 @@ struct VHDLGenerator: ParsableCommand { else { throw GenerationError.invalidFormat(message: "The arrangement contains invalid data!") } - let buildFolder = options.pathURL.appendingPathComponent("build", isDirectory: true) + let buildFolder = try options.pathURL.appendingPathComponent("build", isDirectory: true) let vhdlFolder = buildFolder.appendingPathComponent("vhdl", isDirectory: true) let destination = vhdlFolder.appendingPathComponent( "\(name.rawValue).vhd", isDirectory: false diff --git a/Tests/MachineGeneratorTests/PathArgumentTests.swift b/Tests/MachineGeneratorTests/PathArgumentTests.swift index 83c675a..e684e79 100644 --- a/Tests/MachineGeneratorTests/PathArgumentTests.swift +++ b/Tests/MachineGeneratorTests/PathArgumentTests.swift @@ -61,6 +61,10 @@ import XCTest /// Test class for ``PathArgument``. final class PathArgumentTests: XCTestCase { + let manager = FileManager.default + + let url = URL(fileURLWithPath: "path", isDirectory: true) + /// A command with a path. var command: Generate { get throws { @@ -75,16 +79,36 @@ final class PathArgumentTests: XCTestCase { } } + override func setUp() { + guard !manager.fileExists(atPath: url.path) else { + XCTFail("The path must not exist for this test to pass.") + exit(1) + } + _ = try? manager.createDirectory(atPath: url.path, withIntermediateDirectories: true) + } + + override func tearDown() { + _ = try? manager.removeItem(at: url) + } + /// Test that the computed properties are correct. func testComputedProperties() throws { - let url = URL(fileURLWithPath: "path", isDirectory: true) XCTAssertEqual(try argument.pathURL, url) let machine = url.appendingPathComponent("machine.json", isDirectory: false) + manager.createFile(atPath: machine.path, contents: nil) XCTAssertEqual(try argument.machine, machine) let build = url.appendingPathComponent("build", isDirectory: true) + try manager.createDirectory(at: build, withIntermediateDirectories: true) XCTAssertEqual(try argument.buildFolder, build) let vhdl = build.appendingPathComponent("vhdl", isDirectory: true) + try manager.createDirectory(at: vhdl, withIntermediateDirectories: true) XCTAssertEqual(try argument.vhdlFolder, vhdl) } + func testIsDirectory() throws { + XCTAssertTrue(try argument.isDirectory) + try manager.removeItem(at: url) + XCTAssertThrowsError(try argument.isDirectory) + } + } From c0ea84545f260164e63ccd412f62503140c675f6 Mon Sep 17 00:00:00 2001 From: Morgan McColl Date: Fri, 31 May 2024 00:19:05 +1000 Subject: [PATCH 03/20] Started command for converting kripke structure to graphviz format. --- Sources/MachineGenerator/GraphCommand.swift | 96 +++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 Sources/MachineGenerator/GraphCommand.swift diff --git a/Sources/MachineGenerator/GraphCommand.swift b/Sources/MachineGenerator/GraphCommand.swift new file mode 100644 index 0000000..21d272e --- /dev/null +++ b/Sources/MachineGenerator/GraphCommand.swift @@ -0,0 +1,96 @@ +// GraphCommand.swift +// LLFSMGenerate +// +// Created by Morgan McColl. +// Copyright © 2024 Morgan McColl. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials +// provided with the distribution. +// +// 3. All advertising materials mentioning features or use of this +// software must display the following acknowledgement: +// +// This product includes software developed by Morgan McColl. +// +// 4. Neither the name of the author nor the names of contributors +// may be used to endorse or promote products derived from this +// software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER +// OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// ----------------------------------------------------------------------- +// This program is free software; you can redistribute it and/or +// modify it under the above terms or under the terms of the GNU +// General Public License as published by the Free Software Foundation; +// either version 2 of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, see http://www.gnu.org/licenses/ +// or write to the Free Software Foundation, Inc., 51 Franklin Street, +// Fifth Floor, Boston, MA 02110-1301, USA. + +import ArgumentParser +import Foundation +import VHDLKripkeStructures + +struct GraphCommand: ParsableCommand { + + @OptionGroup var path: PathArgument + + func run() throws { + guard path.path.hasPrefix(".machine"), try path.pathURL.hasDirectoryPath else { + throw GenerationError.invalidInput(message: "The path must be a machines location.") + } + let url = try path.pathURL.appendingPathComponent("output.json", isDirectory: false) + let manager = FileManager() + guard manager.fileExists(atPath: url.path) else { + throw GenerationError.invalidMachine( + message: "The Kripke structure does not exist in this machine." + ) + } + let contents = try Data(contentsOf: url) + let decoder = JSONDecoder() + let structure = try decoder.decode(KripkeStructure.self, from: contents) + guard let data = structure.graphviz.data(using: .utf8) else { + throw GenerationError.invalidExportation( + message: "The Kripke structure could not be exported to Graphviz." + ) + } + let graphvizFile = try path.buildFolder.appendingPathComponent("output.dot", isDirectory: false) + var isDirectory: ObjCBool = false + if !manager.fileExists(atPath: try path.buildFolder.path, isDirectory: &isDirectory) + || !isDirectory.boolValue { + _ = try? manager.removeItem(at: path.buildFolder) + try manager.createDirectory(at: path.buildFolder, withIntermediateDirectories: true) + } + if manager.fileExists(atPath: graphvizFile.path) { + try manager.removeItem(at: graphvizFile) + } + try data.write(to: graphvizFile) + } + +} From cc13c87bda02b80418d9fcab93966fa947c691a0 Mon Sep 17 00:00:00 2001 From: Morgan McColl Date: Fri, 31 May 2024 00:21:02 +1000 Subject: [PATCH 04/20] Added some comments. --- Tests/MachineGeneratorTests/PathArgumentTests.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Tests/MachineGeneratorTests/PathArgumentTests.swift b/Tests/MachineGeneratorTests/PathArgumentTests.swift index e684e79..282cc07 100644 --- a/Tests/MachineGeneratorTests/PathArgumentTests.swift +++ b/Tests/MachineGeneratorTests/PathArgumentTests.swift @@ -61,8 +61,10 @@ import XCTest /// Test class for ``PathArgument``. final class PathArgumentTests: XCTestCase { + /// A file manager. let manager = FileManager.default + /// the path to the `path` folder. let url = URL(fileURLWithPath: "path", isDirectory: true) /// A command with a path. @@ -79,6 +81,7 @@ final class PathArgumentTests: XCTestCase { } } + /// Create the path directory before every test. override func setUp() { guard !manager.fileExists(atPath: url.path) else { XCTFail("The path must not exist for this test to pass.") @@ -87,6 +90,7 @@ final class PathArgumentTests: XCTestCase { _ = try? manager.createDirectory(atPath: url.path, withIntermediateDirectories: true) } + /// Delete the path directory after every test. override func tearDown() { _ = try? manager.removeItem(at: url) } @@ -105,6 +109,7 @@ final class PathArgumentTests: XCTestCase { XCTAssertEqual(try argument.vhdlFolder, vhdl) } + /// Test that the `isDirectory` computed property has the correct behaviour. func testIsDirectory() throws { XCTAssertTrue(try argument.isDirectory) try manager.removeItem(at: url) From 57bdeedeed09eb4da29339e3e3289c2640c2e5e2 Mon Sep 17 00:00:00 2001 From: Morgan McColl Date: Fri, 31 May 2024 00:26:07 +1000 Subject: [PATCH 05/20] Revert "Added some comments." This reverts commit cc13c87bda02b80418d9fcab93966fa947c691a0. --- Tests/MachineGeneratorTests/PathArgumentTests.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Tests/MachineGeneratorTests/PathArgumentTests.swift b/Tests/MachineGeneratorTests/PathArgumentTests.swift index 282cc07..e684e79 100644 --- a/Tests/MachineGeneratorTests/PathArgumentTests.swift +++ b/Tests/MachineGeneratorTests/PathArgumentTests.swift @@ -61,10 +61,8 @@ import XCTest /// Test class for ``PathArgument``. final class PathArgumentTests: XCTestCase { - /// A file manager. let manager = FileManager.default - /// the path to the `path` folder. let url = URL(fileURLWithPath: "path", isDirectory: true) /// A command with a path. @@ -81,7 +79,6 @@ final class PathArgumentTests: XCTestCase { } } - /// Create the path directory before every test. override func setUp() { guard !manager.fileExists(atPath: url.path) else { XCTFail("The path must not exist for this test to pass.") @@ -90,7 +87,6 @@ final class PathArgumentTests: XCTestCase { _ = try? manager.createDirectory(atPath: url.path, withIntermediateDirectories: true) } - /// Delete the path directory after every test. override func tearDown() { _ = try? manager.removeItem(at: url) } @@ -109,7 +105,6 @@ final class PathArgumentTests: XCTestCase { XCTAssertEqual(try argument.vhdlFolder, vhdl) } - /// Test that the `isDirectory` computed property has the correct behaviour. func testIsDirectory() throws { XCTAssertTrue(try argument.isDirectory) try manager.removeItem(at: url) From b0208f1f1cf0617ccefb0d556612f6cf2ec06737 Mon Sep 17 00:00:00 2001 From: Morgan McColl Date: Fri, 31 May 2024 00:26:19 +1000 Subject: [PATCH 06/20] Revert "Changed PathArgument to throw when the file does not exist." This reverts commit 9b46b557420e29c55101989f2be7ea32bfe25a41. --- Sources/MachineGenerator/CleanCommand.swift | 6 ++-- Sources/MachineGenerator/InstallCommand.swift | 4 +-- Sources/MachineGenerator/PathArgument.swift | 28 +++---------------- Sources/MachineGenerator/VHDLGenerator.swift | 12 ++++---- .../PathArgumentTests.swift | 26 +---------------- 5 files changed, 15 insertions(+), 61 deletions(-) diff --git a/Sources/MachineGenerator/CleanCommand.swift b/Sources/MachineGenerator/CleanCommand.swift index e0974a0..f3ac45a 100644 --- a/Sources/MachineGenerator/CleanCommand.swift +++ b/Sources/MachineGenerator/CleanCommand.swift @@ -83,7 +83,7 @@ struct CleanCommand: ParsableCommand { return } try cleanBuildFolder(manager: manager) - guard !(try options.pathURL.lastPathComponent.lowercased().hasSuffix(".arrangement")) else { + guard !options.pathURL.lastPathComponent.lowercased().hasSuffix(".arrangement") else { _ = try? manager.removeItem( at: options.pathURL.appendingPathComponent("arrangement.json", isDirectory: false) ) @@ -98,7 +98,7 @@ struct CleanCommand: ParsableCommand { @inlinable func cleanBuildFolder(manager: FileManager) throws { var isDirectory: ObjCBool = false - guard manager.fileExists(atPath: try options.buildFolder.path, isDirectory: &isDirectory) else { + guard manager.fileExists(atPath: options.buildFolder.path, isDirectory: &isDirectory) else { return } guard isDirectory.boolValue else { @@ -113,7 +113,7 @@ struct CleanCommand: ParsableCommand { @inlinable func cleanMachine(manager: FileManager) throws { var isDirectory: ObjCBool = true - guard manager.fileExists(atPath: try options.machine.path, isDirectory: &isDirectory) else { + guard manager.fileExists(atPath: options.machine.path, isDirectory: &isDirectory) else { return } guard !isDirectory.boolValue else { diff --git a/Sources/MachineGenerator/InstallCommand.swift b/Sources/MachineGenerator/InstallCommand.swift index ed1fee0..9d388d9 100644 --- a/Sources/MachineGenerator/InstallCommand.swift +++ b/Sources/MachineGenerator/InstallCommand.swift @@ -85,7 +85,7 @@ struct InstallCommand: ParsableCommand { /// - Throws: ``GenerationError``. @inlinable func run() throws { - let folder = try self.path.pathURL.lastPathComponent.lowercased() + let folder = self.path.pathURL.lastPathComponent.lowercased() guard folder.hasSuffix(".machine") || folder.hasSuffix(".arrangement") else { throw GenerationError.invalidMachine(message: "The path provided is not a machine.") } @@ -102,7 +102,7 @@ struct InstallCommand: ParsableCommand { } isDirectory = false guard - manager.fileExists(atPath: try self.path.buildFolder.path, isDirectory: &isDirectory), + manager.fileExists(atPath: self.path.buildFolder.path, isDirectory: &isDirectory), isDirectory.boolValue else { throw GenerationError.invalidGeneration( diff --git a/Sources/MachineGenerator/PathArgument.swift b/Sources/MachineGenerator/PathArgument.swift index 7bfdd0f..f6e7f08 100644 --- a/Sources/MachineGenerator/PathArgument.swift +++ b/Sources/MachineGenerator/PathArgument.swift @@ -64,44 +64,24 @@ struct PathArgument: ParsableArguments { @Argument(help: "The path to the machine folder.", completion: .directory) var path: String - /// Whether the `path` is a directory. - @inlinable var isDirectory: Bool { - get throws { - let manager = FileManager() - var isDirectory: ObjCBool = false - guard manager.fileExists(atPath: path, isDirectory: &isDirectory) else { - throw GenerationError.invalidInput(message: "The path does not exist.") - } - return isDirectory.boolValue - } - } - /// The path to the machine file. @inlinable var machine: URL { - get throws { - try pathURL.appendingPathComponent("machine.json", isDirectory: false) - } + pathURL.appendingPathComponent("machine.json", isDirectory: false) } /// The path to the users input (the machine folder). @inlinable var pathURL: URL { - get throws { - try URL(fileURLWithPath: path, isDirectory: isDirectory) - } + URL(fileURLWithPath: path, isDirectory: true) } /// The path to the build folder. @inlinable var buildFolder: URL { - get throws { - try pathURL.appendingPathComponent("build", isDirectory: true) - } + pathURL.appendingPathComponent("build", isDirectory: true) } /// The path to the vhdl folder. @inlinable var vhdlFolder: URL { - get throws { - try buildFolder.appendingPathComponent("vhdl", isDirectory: true) - } + buildFolder.appendingPathComponent("vhdl", isDirectory: true) } } diff --git a/Sources/MachineGenerator/VHDLGenerator.swift b/Sources/MachineGenerator/VHDLGenerator.swift index 9037c8f..173092e 100644 --- a/Sources/MachineGenerator/VHDLGenerator.swift +++ b/Sources/MachineGenerator/VHDLGenerator.swift @@ -81,11 +81,11 @@ struct VHDLGenerator: ParsableCommand { /// Runs the command. @inlinable mutating func run() throws { - guard !(try options.pathURL.lastPathComponent.lowercased().hasSuffix(".arrangement")) else { + guard !options.pathURL.lastPathComponent.lowercased().hasSuffix(".arrangement") else { try createArrangement() return } - let buildFolder = try options.pathURL.appendingPathComponent("build", isDirectory: true) + let buildFolder = options.pathURL.appendingPathComponent("build", isDirectory: true) try self.createMachine(sourcePath: options.pathURL, destinationPath: buildFolder) } @@ -94,7 +94,7 @@ struct VHDLGenerator: ParsableCommand { /// Create an arrangement. @inlinable func createArrangement() throws { - let nameRaw = try options.pathURL.lastPathComponent.dropLast(".arrangement".count) + let nameRaw = options.pathURL.lastPathComponent.dropLast(".arrangement".count) .trimmingCharacters(in: .whitespacesAndNewlines) guard !nameRaw.isEmpty, let name = VariableName(rawValue: nameRaw) else { throw GenerationError.invalidFormat(message: "The arrangement is not named correctly!") @@ -104,9 +104,7 @@ struct VHDLGenerator: ParsableCommand { contentsOf: options.pathURL.appendingPathComponent("model.json", isDirectory: false) ) let model = try decoder.decode(ArrangementModel.self, from: modelData) - let arrangementFile = try options.pathURL.appendingPathComponent( - "arrangement.json", isDirectory: false - ) + let arrangementFile = options.pathURL.appendingPathComponent("arrangement.json", isDirectory: false) let data = try Data(contentsOf: arrangementFile) let arrangement = try decoder.decode(Arrangement.self, from: data) let f: (Machine, VariableName) -> MachineRepresentation? = { @@ -120,7 +118,7 @@ struct VHDLGenerator: ParsableCommand { else { throw GenerationError.invalidFormat(message: "The arrangement contains invalid data!") } - let buildFolder = try options.pathURL.appendingPathComponent("build", isDirectory: true) + let buildFolder = options.pathURL.appendingPathComponent("build", isDirectory: true) let vhdlFolder = buildFolder.appendingPathComponent("vhdl", isDirectory: true) let destination = vhdlFolder.appendingPathComponent( "\(name.rawValue).vhd", isDirectory: false diff --git a/Tests/MachineGeneratorTests/PathArgumentTests.swift b/Tests/MachineGeneratorTests/PathArgumentTests.swift index e684e79..83c675a 100644 --- a/Tests/MachineGeneratorTests/PathArgumentTests.swift +++ b/Tests/MachineGeneratorTests/PathArgumentTests.swift @@ -61,10 +61,6 @@ import XCTest /// Test class for ``PathArgument``. final class PathArgumentTests: XCTestCase { - let manager = FileManager.default - - let url = URL(fileURLWithPath: "path", isDirectory: true) - /// A command with a path. var command: Generate { get throws { @@ -79,36 +75,16 @@ final class PathArgumentTests: XCTestCase { } } - override func setUp() { - guard !manager.fileExists(atPath: url.path) else { - XCTFail("The path must not exist for this test to pass.") - exit(1) - } - _ = try? manager.createDirectory(atPath: url.path, withIntermediateDirectories: true) - } - - override func tearDown() { - _ = try? manager.removeItem(at: url) - } - /// Test that the computed properties are correct. func testComputedProperties() throws { + let url = URL(fileURLWithPath: "path", isDirectory: true) XCTAssertEqual(try argument.pathURL, url) let machine = url.appendingPathComponent("machine.json", isDirectory: false) - manager.createFile(atPath: machine.path, contents: nil) XCTAssertEqual(try argument.machine, machine) let build = url.appendingPathComponent("build", isDirectory: true) - try manager.createDirectory(at: build, withIntermediateDirectories: true) XCTAssertEqual(try argument.buildFolder, build) let vhdl = build.appendingPathComponent("vhdl", isDirectory: true) - try manager.createDirectory(at: vhdl, withIntermediateDirectories: true) XCTAssertEqual(try argument.vhdlFolder, vhdl) } - func testIsDirectory() throws { - XCTAssertTrue(try argument.isDirectory) - try manager.removeItem(at: url) - XCTAssertThrowsError(try argument.isDirectory) - } - } From fe401959a02f2d9076169db2cba35689085a67e2 Mon Sep 17 00:00:00 2001 From: Morgan McColl Date: Fri, 31 May 2024 00:29:45 +1000 Subject: [PATCH 07/20] Removed redundent try statements and check for valid machine before enacting command. --- Sources/MachineGenerator/GraphCommand.swift | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Sources/MachineGenerator/GraphCommand.swift b/Sources/MachineGenerator/GraphCommand.swift index 21d272e..c177e10 100644 --- a/Sources/MachineGenerator/GraphCommand.swift +++ b/Sources/MachineGenerator/GraphCommand.swift @@ -62,11 +62,16 @@ struct GraphCommand: ParsableCommand { @OptionGroup var path: PathArgument func run() throws { - guard path.path.hasPrefix(".machine"), try path.pathURL.hasDirectoryPath else { - throw GenerationError.invalidInput(message: "The path must be a machines location.") - } - let url = try path.pathURL.appendingPathComponent("output.json", isDirectory: false) let manager = FileManager() + var isDirectory: ObjCBool = false + guard + path.path.hasPrefix(".machine"), + manager.fileExists(atPath: path.path, isDirectory: &isDirectory), + isDirectory.boolValue + else { + throw GenerationError.invalidInput(message: "The path must be a valid machines location.") + } + let url = path.pathURL.appendingPathComponent("output.json", isDirectory: false) guard manager.fileExists(atPath: url.path) else { throw GenerationError.invalidMachine( message: "The Kripke structure does not exist in this machine." @@ -80,9 +85,9 @@ struct GraphCommand: ParsableCommand { message: "The Kripke structure could not be exported to Graphviz." ) } - let graphvizFile = try path.buildFolder.appendingPathComponent("output.dot", isDirectory: false) - var isDirectory: ObjCBool = false - if !manager.fileExists(atPath: try path.buildFolder.path, isDirectory: &isDirectory) + let graphvizFile = path.buildFolder.appendingPathComponent("output.dot", isDirectory: false) + isDirectory = false + if !manager.fileExists(atPath: path.buildFolder.path, isDirectory: &isDirectory) || !isDirectory.boolValue { _ = try? manager.removeItem(at: path.buildFolder) try manager.createDirectory(at: path.buildFolder, withIntermediateDirectories: true) From cf3c9d65e5ef96b842992a2a77546b633c59dd35 Mon Sep 17 00:00:00 2001 From: Morgan McColl Date: Fri, 31 May 2024 00:32:46 +1000 Subject: [PATCH 08/20] Added configuration. --- Sources/MachineGenerator/GraphCommand.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/MachineGenerator/GraphCommand.swift b/Sources/MachineGenerator/GraphCommand.swift index c177e10..bfdcd11 100644 --- a/Sources/MachineGenerator/GraphCommand.swift +++ b/Sources/MachineGenerator/GraphCommand.swift @@ -59,6 +59,12 @@ import VHDLKripkeStructures struct GraphCommand: ParsableCommand { + /// The configuration of this command. + static var configuration = CommandConfiguration( + commandName: "graph", + abstract: "Generate a graphviz file (.dot) for the entire kripke structure." + ) + @OptionGroup var path: PathArgument func run() throws { From f10da29cbdfd7a064a80df785af94942825d6291 Mon Sep 17 00:00:00 2001 From: Morgan McColl Date: Fri, 31 May 2024 00:32:56 +1000 Subject: [PATCH 09/20] Bumped version and added GraphCommand. --- Sources/MachineGenerator/LLFSMGenerate.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/MachineGenerator/LLFSMGenerate.swift b/Sources/MachineGenerator/LLFSMGenerate.swift index c54ae0b..7b96a30 100644 --- a/Sources/MachineGenerator/LLFSMGenerate.swift +++ b/Sources/MachineGenerator/LLFSMGenerate.swift @@ -64,8 +64,10 @@ struct LLFSMGenerate: ParsableCommand { static var configuration = CommandConfiguration( commandName: "llfsmgenerate", abstract: "A utility for performing operations on LLFSM formats.", - version: "2.0.1", - subcommands: [Generate.self, VHDLGenerator.self, CleanCommand.self, InstallCommand.self] + version: "2.1.0", + subcommands: [ + Generate.self, VHDLGenerator.self, CleanCommand.self, InstallCommand.self, GraphCommand.self + ] ) } From a10a97ee3bdd2f64748ddea841f392235d7e2ba4 Mon Sep 17 00:00:00 2001 From: Morgan McColl Date: Fri, 31 May 2024 00:40:55 +1000 Subject: [PATCH 10/20] Fixed error on guard. --- Sources/MachineGenerator/GraphCommand.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/MachineGenerator/GraphCommand.swift b/Sources/MachineGenerator/GraphCommand.swift index bfdcd11..a7f6a7d 100644 --- a/Sources/MachineGenerator/GraphCommand.swift +++ b/Sources/MachineGenerator/GraphCommand.swift @@ -71,7 +71,7 @@ struct GraphCommand: ParsableCommand { let manager = FileManager() var isDirectory: ObjCBool = false guard - path.path.hasPrefix(".machine"), + path.pathURL.lastPathComponent.hasSuffix(".machine"), manager.fileExists(atPath: path.path, isDirectory: &isDirectory), isDirectory.boolValue else { From ec897243f5a4ae7984d2608f47e790a5226db3cf Mon Sep 17 00:00:00 2001 From: Morgan McColl Date: Fri, 31 May 2024 00:50:38 +1000 Subject: [PATCH 11/20] Updated to latest kripke structure generator. --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 30f0e9f..db986d3 100644 --- a/Package.swift +++ b/Package.swift @@ -19,7 +19,7 @@ let package = Package( .package(url: "https://github.com/mipalgu/VHDLMachines", from: "4.0.1"), .package(url: "https://github.com/mipalgu/VHDLParsing", from: "2.4.0"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.0"), - .package(url: "https://github.com/mipalgu/VHDLKripkeStructureGenerator.git", from: "0.3.1"), + .package(url: "https://github.com/mipalgu/VHDLKripkeStructureGenerator.git", from: "0.3.2"), .package(url: "https://github.com/CPSLabGU/SwiftUtils.git", from: "0.1.0"), .package(url: "https://github.com/CPSLabGU/VHDLJSModels", from: "1.0.0"), .package(url: "https://github.com/CPSLabGU/VHDLKripkeStructures", from: "1.2.1") From 48eed2c877a6abf9f17839f00f3aba14bf63f0b8 Mon Sep 17 00:00:00 2001 From: Morgan McColl Date: Sun, 2 Jun 2024 05:43:16 +1000 Subject: [PATCH 12/20] Restructured command to take an optional machine flag and a destination path. --- Sources/MachineGenerator/GraphCommand.swift | 55 +++++++++++++++------ 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/Sources/MachineGenerator/GraphCommand.swift b/Sources/MachineGenerator/GraphCommand.swift index a7f6a7d..01da412 100644 --- a/Sources/MachineGenerator/GraphCommand.swift +++ b/Sources/MachineGenerator/GraphCommand.swift @@ -65,22 +65,48 @@ struct GraphCommand: ParsableCommand { abstract: "Generate a graphviz file (.dot) for the entire kripke structure." ) - @OptionGroup var path: PathArgument + @Flag(name: .customLong("machine"), help: "Whether the path is a machine folder.") + var isMachine = false + + @Argument(help: """ + The path to the resource containing the Kripke Structure. This path may be the json file itself + or a path to a machine folder. + """ + ) + var path: String + + @Option(help: "The directory that will contain the newly generated graphviz file.") + var destination: String = FileManager.default.currentDirectoryPath + + var destinationURL: URL { + URL(fileURLWithPath: destination, isDirectory: true) + } func run() throws { - let manager = FileManager() + guard isMachine else { + let pathURL = URL(fileURLWithPath: path, isDirectory: false) + try self.generate(pathURL: pathURL) + return + } + let manager = FileManager.default var isDirectory: ObjCBool = false guard - path.pathURL.lastPathComponent.hasSuffix(".machine"), - manager.fileExists(atPath: path.path, isDirectory: &isDirectory), + path.hasSuffix(".machine"), + manager.fileExists(atPath: path, isDirectory: &isDirectory), isDirectory.boolValue else { throw GenerationError.invalidInput(message: "The path must be a valid machines location.") } - let url = path.pathURL.appendingPathComponent("output.json", isDirectory: false) + let pathURL = URL(fileURLWithPath: path, isDirectory: true) + .appendingPathComponent("output.json", isDirectory: false) + try self.generate(pathURL: pathURL) + } + + func generate(pathURL url: URL) throws { + let manager = FileManager.default guard manager.fileExists(atPath: url.path) else { throw GenerationError.invalidMachine( - message: "The Kripke structure does not exist in this machine." + message: "The Kripke structure does not exist at this specified location." ) } let contents = try Data(contentsOf: url) @@ -91,14 +117,15 @@ struct GraphCommand: ParsableCommand { message: "The Kripke structure could not be exported to Graphviz." ) } - let graphvizFile = path.buildFolder.appendingPathComponent("output.dot", isDirectory: false) - isDirectory = false - if !manager.fileExists(atPath: path.buildFolder.path, isDirectory: &isDirectory) - || !isDirectory.boolValue { - _ = try? manager.removeItem(at: path.buildFolder) - try manager.createDirectory(at: path.buildFolder, withIntermediateDirectories: true) - } - if manager.fileExists(atPath: graphvizFile.path) { + let name = url.deletingPathExtension().lastPathComponent + let graphvizFile = destinationURL.appendingPathComponent("\(name).dot", isDirectory: false) + var isDirectory: ObjCBool = false + if !manager.fileExists(atPath: destinationURL.path, isDirectory: &isDirectory) { + if !isDirectory.boolValue { + throw GenerationError.invalidInput(message: "The destination must be a directory.") + } + try manager.createDirectory(at: destinationURL, withIntermediateDirectories: true) + } else if manager.fileExists(atPath: graphvizFile.path) { try manager.removeItem(at: graphvizFile) } try data.write(to: graphvizFile) From e4f8b6a0eaa71f566c5f3ff61364c33497518fd2 Mon Sep 17 00:00:00 2001 From: Morgan McColl Date: Tue, 4 Jun 2024 12:38:22 +1000 Subject: [PATCH 13/20] Added destination property. --- Sources/MachineGenerator/GraphCommand.swift | 45 +++++++++++++-------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/Sources/MachineGenerator/GraphCommand.swift b/Sources/MachineGenerator/GraphCommand.swift index 01da412..83f4aac 100644 --- a/Sources/MachineGenerator/GraphCommand.swift +++ b/Sources/MachineGenerator/GraphCommand.swift @@ -75,12 +75,8 @@ struct GraphCommand: ParsableCommand { ) var path: String - @Option(help: "The directory that will contain the newly generated graphviz file.") - var destination: String = FileManager.default.currentDirectoryPath - - var destinationURL: URL { - URL(fileURLWithPath: destination, isDirectory: true) - } + @Option(help: "The path of the newly generated graphviz file.") + var destination: String? func run() throws { guard isMachine else { @@ -104,7 +100,8 @@ struct GraphCommand: ParsableCommand { func generate(pathURL url: URL) throws { let manager = FileManager.default - guard manager.fileExists(atPath: url.path) else { + var isDirectory: ObjCBool = false + guard manager.fileExists(atPath: url.path, isDirectory: &isDirectory), !isDirectory.boolValue else { throw GenerationError.invalidMachine( message: "The Kripke structure does not exist at this specified location." ) @@ -117,18 +114,34 @@ struct GraphCommand: ParsableCommand { message: "The Kripke structure could not be exported to Graphviz." ) } - let name = url.deletingPathExtension().lastPathComponent - let graphvizFile = destinationURL.appendingPathComponent("\(name).dot", isDirectory: false) + let graphvizFile = self.graphvizFile(defaultName: url.deletingPathExtension().lastPathComponent) + let previousDirectory = graphvizFile.deletingLastPathComponent() + if !manager.fileExists(atPath: previousDirectory.path) { + try manager.createDirectory(at: previousDirectory, withIntermediateDirectories: true) + } + try data.write(to: graphvizFile, options: .atomic) + } + + func graphvizFile(defaultName name: String) -> URL { + let manager = FileManager.default + guard let destination else { + let currentPath = URL(fileURLWithPath: manager.currentDirectoryPath, isDirectory: true) + return currentPath.appendingPathComponent("\(name).dot", isDirectory: false) + } var isDirectory: ObjCBool = false - if !manager.fileExists(atPath: destinationURL.path, isDirectory: &isDirectory) { - if !isDirectory.boolValue { - throw GenerationError.invalidInput(message: "The destination must be a directory.") + guard !manager.fileExists(atPath: destination, isDirectory: &isDirectory) else { + guard isDirectory.boolValue else { + return URL(fileURLWithPath: destination, isDirectory: false) } - try manager.createDirectory(at: destinationURL, withIntermediateDirectories: true) - } else if manager.fileExists(atPath: graphvizFile.path) { - try manager.removeItem(at: graphvizFile) + return URL(fileURLWithPath: destination, isDirectory: true) + .appendingPathComponent("\(name).dot", isDirectory: false) + } + guard destination.hasSuffix(".dot") else { + print("Cannot discern if destination is directory or file. Defaulting to directory.") + return URL(fileURLWithPath: destination, isDirectory: true) + .appendingPathComponent("\(name).dot", isDirectory: false) } - try data.write(to: graphvizFile) + return URL(fileURLWithPath: destination, isDirectory: false) } } From 59678ff0cc7e481d8bae4b60bd4f65272c8460d3 Mon Sep 17 00:00:00 2001 From: Morgan McColl Date: Tue, 4 Jun 2024 13:06:53 +1000 Subject: [PATCH 14/20] Changed clock to 5 MHz. --- .../Arrangement+pingArrangement.swift | 2 +- .../ArrangementModel+pingArrangement.swift | 2 +- .../KripkeStructure+testStructure.swift | 75 +++++++++++++++++++ Tests/TestHelpers/Machine+pingMachine.swift | 2 +- .../MachineModel+pingMachine.swift | 2 +- 5 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 Tests/TestHelpers/KripkeStructure+testStructure.swift diff --git a/Tests/TestHelpers/Arrangement+pingArrangement.swift b/Tests/TestHelpers/Arrangement+pingArrangement.swift index 6818d3d..2aa7aa6 100644 --- a/Tests/TestHelpers/Arrangement+pingArrangement.swift +++ b/Tests/TestHelpers/Arrangement+pingArrangement.swift @@ -80,7 +80,7 @@ public extension Arrangement { signals: [ LocalSignal(type: .stdLogic, name: .ping), LocalSignal(type: .stdLogic, name: .pong) ], - clocks: [Clock(name: .clk, frequency: 125, unit: .MHz)] + clocks: [Clock(name: .clk, frequency: 5, unit: .MHz)] )! } diff --git a/Tests/TestHelpers/ArrangementModel+pingArrangement.swift b/Tests/TestHelpers/ArrangementModel+pingArrangement.swift index 9ee7a4b..e5d86e8 100644 --- a/Tests/TestHelpers/ArrangementModel+pingArrangement.swift +++ b/Tests/TestHelpers/ArrangementModel+pingArrangement.swift @@ -64,7 +64,7 @@ public extension ArrangementModel { /// - Returns: The arrangement model. static func pingArrangement(path: URL) -> ArrangementModel { ArrangementModel( - clocks: [ClockModel(name: "clk", frequency: "125 MHz")], + clocks: [ClockModel(name: "clk", frequency: "5 MHz")], externalVariables: "externalPing: out std_logic; externalPong: out std_logic;", machines: [ MachineReference( diff --git a/Tests/TestHelpers/KripkeStructure+testStructure.swift b/Tests/TestHelpers/KripkeStructure+testStructure.swift new file mode 100644 index 0000000..0235a8a --- /dev/null +++ b/Tests/TestHelpers/KripkeStructure+testStructure.swift @@ -0,0 +1,75 @@ +// KripkeStructure+testStructure.swift +// LLFSMGenerate +// +// Created by Morgan McColl. +// Copyright © 2024 Morgan McColl. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials +// provided with the distribution. +// +// 3. All advertising materials mentioning features or use of this +// software must display the following acknowledgement: +// +// This product includes software developed by Morgan McColl. +// +// 4. Neither the name of the author nor the names of contributors +// may be used to endorse or promote products derived from this +// software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER +// OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// ----------------------------------------------------------------------- +// This program is free software; you can redistribute it and/or +// modify it under the above terms or under the terms of the GNU +// General Public License as published by the Free Software Foundation; +// either version 2 of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, see http://www.gnu.org/licenses/ +// or write to the Free Software Foundation, Inc., 51 Franklin Street, +// Fifth Floor, Boston, MA 02110-1301, USA. + +import VHDLKripkeStructures +import VHDLParsing + +extension KripkeStructure { + + static let testStructure: KripkeStructure? = { + let initialState = Node( + type: .read, + currentState: .initial, + executeOnEntry: true, + nextState: .initial, + properties: [.ping: .bit(value: .low)] + ) + let nodes = [ + initialState + ] + return nil + }() + +} diff --git a/Tests/TestHelpers/Machine+pingMachine.swift b/Tests/TestHelpers/Machine+pingMachine.swift index 6b1a95b..e812351 100644 --- a/Tests/TestHelpers/Machine+pingMachine.swift +++ b/Tests/TestHelpers/Machine+pingMachine.swift @@ -67,7 +67,7 @@ public extension Machine { PortSignal(type: .stdLogic, name: .ping, mode: .output), PortSignal(type: .stdLogic, name: .pong, mode: .input) ], - clocks: [Clock(name: .clk, frequency: 125, unit: .MHz)], + clocks: [Clock(name: .clk, frequency: 5, unit: .MHz)], drivingClock: 0, machineSignals: [], isParameterised: false, diff --git a/Tests/TestHelpers/MachineModel+pingMachine.swift b/Tests/TestHelpers/MachineModel+pingMachine.swift index 432a2bf..927689b 100644 --- a/Tests/TestHelpers/MachineModel+pingMachine.swift +++ b/Tests/TestHelpers/MachineModel+pingMachine.swift @@ -152,7 +152,7 @@ public extension MachineModel { ) ], initialState: "Initial", - clocks: [ClockModel(name: "clk", frequency: "125 MHz")] + clocks: [ClockModel(name: "clk", frequency: "5 MHz")] ) } From 6398afea88d84c7bf550a0c922e9e6309e894906 Mon Sep 17 00:00:00 2001 From: Morgan McColl Date: Tue, 4 Jun 2024 13:27:57 +1000 Subject: [PATCH 15/20] Create Kripke structure before every test. --- .../MachineGeneratorTests/MachineTester.swift | 10 +- .../KripkeStructure+testStructure.swift | 931 +++++++++++++++++- 2 files changed, 928 insertions(+), 13 deletions(-) diff --git a/Tests/MachineGeneratorTests/MachineTester.swift b/Tests/MachineGeneratorTests/MachineTester.swift index fb99307..b580418 100644 --- a/Tests/MachineGeneratorTests/MachineTester.swift +++ b/Tests/MachineGeneratorTests/MachineTester.swift @@ -57,6 +57,7 @@ import Foundation import JavascriptModel import TestHelpers +import VHDLKripkeStructures import VHDLMachines import XCTest @@ -122,6 +123,11 @@ class MachineTester: XCTestCase { machinesFolder.appendingPathComponent("PingMachine.machine", isDirectory: true) } + /// The path to the `PingMachine` kripke structure (`output.json`). + var pingMachineKripkeStructure: URL { + pingMachineFolder.appendingPathComponent("output.json", isDirectory: false) + } + /// Create test machines before every test. override func setUp() { let createDir: ()? = try? manager @@ -135,13 +141,15 @@ class MachineTester: XCTestCase { let arrangementData = try? encoder.encode( ArrangementModel.pingArrangement(path: self.pingMachineFolder) ), - let modelData = try? encoder.encode(MachineModel.machine0) + let modelData = try? encoder.encode(MachineModel.machine0), + let structureData = try? encoder.encode(KripkeStructure.pingPongStructure) else { XCTFail("Failed to create machine!") return } try? self.manager.createDirectory(at: self.pingMachineFolder, withIntermediateDirectories: true) try? pingData.write(to: pingMachineFolder.appendingPathComponent("model.json", isDirectory: false)) + try? structureData.write(to: pingMachineKripkeStructure) try? self.manager.createDirectory(at: arrangement1Folder, withIntermediateDirectories: true) try? arrangementData.write( to: arrangement1Folder.appendingPathComponent("model.json", isDirectory: false) diff --git a/Tests/TestHelpers/KripkeStructure+testStructure.swift b/Tests/TestHelpers/KripkeStructure+testStructure.swift index 0235a8a..ecde748 100644 --- a/Tests/TestHelpers/KripkeStructure+testStructure.swift +++ b/Tests/TestHelpers/KripkeStructure+testStructure.swift @@ -53,23 +53,930 @@ // or write to the Free Software Foundation, Inc., 51 Franklin Street, // Fifth Floor, Boston, MA 02110-1301, USA. +import Foundation import VHDLKripkeStructures import VHDLParsing +// swiftlint:disable file_length + +/// Add constant kripke structures. extension KripkeStructure { - static let testStructure: KripkeStructure? = { - let initialState = Node( - type: .read, - currentState: .initial, - executeOnEntry: true, - nextState: .initial, - properties: [.ping: .bit(value: .low)] - ) - let nodes = [ - initialState - ] - return nil + /// The ping pong kripke structure. + public static let pingPongStructure: KripkeStructure = { + let decoder = JSONDecoder() + // swiftlint:disable:next force_unwrapping + return (try? decoder.decode(KripkeStructure.self, from: KripkeStructure.raw.data(using: .utf8)!))! }() + /// The raw JSON string for the ping pong kripke structure. + static let raw = """ + { + "edges" : [ + { + "currentState" : "Initial", + "executeOnEntry" : true, + "nextState" : "SendPing", + "properties" : [ + "PingMachine_ping", + "'0'", + "PingMachine_pong", + "'0'", + "ping", + "'0'", + "pong", + "'0'" + ], + "type" : { + "write" : { + + } + } + }, + [ + { + "cost" : { + "energy" : { + "coefficient" : 0, + "exponent" : 0 + }, + "time" : { + "coefficient" : 0, + "exponent" : 0 + } + }, + "target" : { + "currentState" : "SendPing", + "executeOnEntry" : true, + "nextState" : "SendPing", + "properties" : [ + "PingMachine_ping", + "'0'", + "PingMachine_pong", + "'0'", + "ping", + "'0'", + "pong", + "'0'" + ], + "type" : { + "read" : { + + } + } + } + } + ], + { + "currentState" : "WaitForPong", + "executeOnEntry" : true, + "nextState" : "WaitForPong", + "properties" : [ + "PingMachine_ping", + "'1'", + "PingMachine_pong", + "'1'", + "ping", + "'1'", + "pong", + "'1'" + ], + "type" : { + "read" : { + + } + } + }, + [ + { + "cost" : { + "energy" : { + "coefficient" : 0, + "exponent" : 0 + }, + "time" : { + "coefficient" : 1, + "exponent" : -6 + } + }, + "target" : { + "currentState" : "WaitForPong", + "executeOnEntry" : true, + "nextState" : "SendPing", + "properties" : [ + "PingMachine_ping", + "'0'", + "PingMachine_pong", + "'1'", + "ping", + "'0'", + "pong", + "'1'" + ], + "type" : { + "write" : { + + } + } + } + } + ], + { + "currentState" : "WaitForPong", + "executeOnEntry" : true, + "nextState" : "SendPing", + "properties" : [ + "PingMachine_ping", + "'0'", + "PingMachine_pong", + "'1'", + "ping", + "'0'", + "pong", + "'1'" + ], + "type" : { + "write" : { + + } + } + }, + [ + { + "cost" : { + "energy" : { + "coefficient" : 0, + "exponent" : 0 + }, + "time" : { + "coefficient" : 0, + "exponent" : 0 + } + }, + "target" : { + "currentState" : "SendPing", + "executeOnEntry" : true, + "nextState" : "SendPing", + "properties" : [ + "PingMachine_ping", + "'0'", + "PingMachine_pong", + "'1'", + "ping", + "'0'", + "pong", + "'1'" + ], + "type" : { + "read" : { + + } + } + } + } + ], + { + "currentState" : "SendPing", + "executeOnEntry" : true, + "nextState" : "WaitForPong", + "properties" : [ + "PingMachine_ping", + "'1'", + "PingMachine_pong", + "'1'", + "ping", + "'1'", + "pong", + "'1'" + ], + "type" : { + "write" : { + + } + } + }, + [ + { + "cost" : { + "energy" : { + "coefficient" : 0, + "exponent" : 0 + }, + "time" : { + "coefficient" : 0, + "exponent" : 0 + } + }, + "target" : { + "currentState" : "WaitForPong", + "executeOnEntry" : true, + "nextState" : "WaitForPong", + "properties" : [ + "PingMachine_ping", + "'1'", + "PingMachine_pong", + "'0'", + "ping", + "'1'", + "pong", + "'0'" + ], + "type" : { + "read" : { + + } + } + } + }, + { + "cost" : { + "energy" : { + "coefficient" : 0, + "exponent" : 0 + }, + "time" : { + "coefficient" : 0, + "exponent" : 0 + } + }, + "target" : { + "currentState" : "WaitForPong", + "executeOnEntry" : true, + "nextState" : "WaitForPong", + "properties" : [ + "PingMachine_ping", + "'1'", + "PingMachine_pong", + "'1'", + "ping", + "'1'", + "pong", + "'1'" + ], + "type" : { + "read" : { + + } + } + } + } + ], + { + "currentState" : "WaitForPong", + "executeOnEntry" : false, + "nextState" : "WaitForPong", + "properties" : [ + "PingMachine_ping", + "'0'", + "PingMachine_pong", + "'0'", + "ping", + "'0'", + "pong", + "'0'" + ], + "type" : { + "write" : { + + } + } + }, + [ + { + "cost" : { + "energy" : { + "coefficient" : 0, + "exponent" : 0 + }, + "time" : { + "coefficient" : 0, + "exponent" : 0 + } + }, + "target" : { + "currentState" : "WaitForPong", + "executeOnEntry" : false, + "nextState" : "WaitForPong", + "properties" : [ + "PingMachine_ping", + "'0'", + "PingMachine_pong", + "'0'", + "ping", + "'0'", + "pong", + "'0'" + ], + "type" : { + "read" : { + + } + } + } + }, + { + "cost" : { + "energy" : { + "coefficient" : 0, + "exponent" : 0 + }, + "time" : { + "coefficient" : 0, + "exponent" : 0 + } + }, + "target" : { + "currentState" : "WaitForPong", + "executeOnEntry" : false, + "nextState" : "WaitForPong", + "properties" : [ + "PingMachine_ping", + "'0'", + "PingMachine_pong", + "'1'", + "ping", + "'0'", + "pong", + "'1'" + ], + "type" : { + "read" : { + + } + } + } + } + ], + { + "currentState" : "WaitForPong", + "executeOnEntry" : true, + "nextState" : "WaitForPong", + "properties" : [ + "PingMachine_ping", + "'1'", + "PingMachine_pong", + "'0'", + "ping", + "'1'", + "pong", + "'0'" + ], + "type" : { + "read" : { + + } + } + }, + [ + { + "cost" : { + "energy" : { + "coefficient" : 0, + "exponent" : 0 + }, + "time" : { + "coefficient" : 1, + "exponent" : -6 + } + }, + "target" : { + "currentState" : "WaitForPong", + "executeOnEntry" : false, + "nextState" : "WaitForPong", + "properties" : [ + "PingMachine_ping", + "'0'", + "PingMachine_pong", + "'0'", + "ping", + "'0'", + "pong", + "'0'" + ], + "type" : { + "write" : { + + } + } + } + } + ], + { + "currentState" : "SendPing", + "executeOnEntry" : true, + "nextState" : "WaitForPong", + "properties" : [ + "PingMachine_ping", + "'1'", + "PingMachine_pong", + "'0'", + "ping", + "'1'", + "pong", + "'0'" + ], + "type" : { + "write" : { + + } + } + }, + [ + { + "cost" : { + "energy" : { + "coefficient" : 0, + "exponent" : 0 + }, + "time" : { + "coefficient" : 0, + "exponent" : 0 + } + }, + "target" : { + "currentState" : "WaitForPong", + "executeOnEntry" : true, + "nextState" : "WaitForPong", + "properties" : [ + "PingMachine_ping", + "'1'", + "PingMachine_pong", + "'0'", + "ping", + "'1'", + "pong", + "'0'" + ], + "type" : { + "read" : { + + } + } + } + }, + { + "cost" : { + "energy" : { + "coefficient" : 0, + "exponent" : 0 + }, + "time" : { + "coefficient" : 0, + "exponent" : 0 + } + }, + "target" : { + "currentState" : "WaitForPong", + "executeOnEntry" : true, + "nextState" : "WaitForPong", + "properties" : [ + "PingMachine_ping", + "'1'", + "PingMachine_pong", + "'1'", + "ping", + "'1'", + "pong", + "'1'" + ], + "type" : { + "read" : { + + } + } + } + } + ], + { + "currentState" : "SendPing", + "executeOnEntry" : true, + "nextState" : "SendPing", + "properties" : [ + "PingMachine_ping", + "'0'", + "PingMachine_pong", + "'1'", + "ping", + "'0'", + "pong", + "'1'" + ], + "type" : { + "read" : { + + } + } + }, + [ + { + "cost" : { + "energy" : { + "coefficient" : 0, + "exponent" : 0 + }, + "time" : { + "coefficient" : 1, + "exponent" : -6 + } + }, + "target" : { + "currentState" : "SendPing", + "executeOnEntry" : true, + "nextState" : "WaitForPong", + "properties" : [ + "PingMachine_ping", + "'1'", + "PingMachine_pong", + "'1'", + "ping", + "'1'", + "pong", + "'1'" + ], + "type" : { + "write" : { + + } + } + } + } + ], + { + "currentState" : "SendPing", + "executeOnEntry" : true, + "nextState" : "SendPing", + "properties" : [ + "PingMachine_ping", + "'0'", + "PingMachine_pong", + "'0'", + "ping", + "'0'", + "pong", + "'0'" + ], + "type" : { + "read" : { + + } + } + }, + [ + { + "cost" : { + "energy" : { + "coefficient" : 0, + "exponent" : 0 + }, + "time" : { + "coefficient" : 1, + "exponent" : -6 + } + }, + "target" : { + "currentState" : "SendPing", + "executeOnEntry" : true, + "nextState" : "WaitForPong", + "properties" : [ + "PingMachine_ping", + "'1'", + "PingMachine_pong", + "'0'", + "ping", + "'1'", + "pong", + "'0'" + ], + "type" : { + "write" : { + + } + } + } + } + ], + { + "currentState" : "Initial", + "executeOnEntry" : true, + "nextState" : "Initial", + "properties" : [ + "PingMachine_ping", + "'0'", + "PingMachine_pong", + "'0'", + "ping", + "'0'", + "pong", + "'0'" + ], + "type" : { + "read" : { + + } + } + }, + [ + { + "cost" : { + "energy" : { + "coefficient" : 0, + "exponent" : 0 + }, + "time" : { + "coefficient" : 1, + "exponent" : -6 + } + }, + "target" : { + "currentState" : "Initial", + "executeOnEntry" : true, + "nextState" : "SendPing", + "properties" : [ + "PingMachine_ping", + "'0'", + "PingMachine_pong", + "'0'", + "ping", + "'0'", + "pong", + "'0'" + ], + "type" : { + "write" : { + + } + } + } + } + ] + ], + "initialStates" : [ + { + "currentState" : "Initial", + "executeOnEntry" : true, + "nextState" : "Initial", + "properties" : [ + "PingMachine_ping", + "'0'", + "PingMachine_pong", + "'0'", + "ping", + "'0'", + "pong", + "'0'" + ], + "type" : { + "read" : { + + } + } + } + ], + "nodes" : [ + { + "currentState" : "WaitForPong", + "executeOnEntry" : false, + "nextState" : "WaitForPong", + "properties" : [ + "PingMachine_ping", + "'0'", + "PingMachine_pong", + "'0'", + "ping", + "'0'", + "pong", + "'0'" + ], + "type" : { + "write" : { + + } + } + }, + { + "currentState" : "WaitForPong", + "executeOnEntry" : true, + "nextState" : "WaitForPong", + "properties" : [ + "PingMachine_ping", + "'1'", + "PingMachine_pong", + "'1'", + "ping", + "'1'", + "pong", + "'1'" + ], + "type" : { + "read" : { + + } + } + }, + { + "currentState" : "SendPing", + "executeOnEntry" : true, + "nextState" : "SendPing", + "properties" : [ + "PingMachine_ping", + "'0'", + "PingMachine_pong", + "'0'", + "ping", + "'0'", + "pong", + "'0'" + ], + "type" : { + "read" : { + + } + } + }, + { + "currentState" : "WaitForPong", + "executeOnEntry" : true, + "nextState" : "SendPing", + "properties" : [ + "PingMachine_ping", + "'0'", + "PingMachine_pong", + "'1'", + "ping", + "'0'", + "pong", + "'1'" + ], + "type" : { + "write" : { + + } + } + }, + { + "currentState" : "Initial", + "executeOnEntry" : true, + "nextState" : "Initial", + "properties" : [ + "PingMachine_ping", + "'0'", + "PingMachine_pong", + "'0'", + "ping", + "'0'", + "pong", + "'0'" + ], + "type" : { + "read" : { + + } + } + }, + { + "currentState" : "SendPing", + "executeOnEntry" : true, + "nextState" : "WaitForPong", + "properties" : [ + "PingMachine_ping", + "'1'", + "PingMachine_pong", + "'1'", + "ping", + "'1'", + "pong", + "'1'" + ], + "type" : { + "write" : { + + } + } + }, + { + "currentState" : "SendPing", + "executeOnEntry" : true, + "nextState" : "WaitForPong", + "properties" : [ + "PingMachine_ping", + "'1'", + "PingMachine_pong", + "'0'", + "ping", + "'1'", + "pong", + "'0'" + ], + "type" : { + "write" : { + + } + } + }, + { + "currentState" : "WaitForPong", + "executeOnEntry" : true, + "nextState" : "WaitForPong", + "properties" : [ + "PingMachine_ping", + "'1'", + "PingMachine_pong", + "'0'", + "ping", + "'1'", + "pong", + "'0'" + ], + "type" : { + "read" : { + + } + } + }, + { + "currentState" : "Initial", + "executeOnEntry" : true, + "nextState" : "SendPing", + "properties" : [ + "PingMachine_ping", + "'0'", + "PingMachine_pong", + "'0'", + "ping", + "'0'", + "pong", + "'0'" + ], + "type" : { + "write" : { + + } + } + }, + { + "currentState" : "WaitForPong", + "executeOnEntry" : false, + "nextState" : "WaitForPong", + "properties" : [ + "PingMachine_ping", + "'0'", + "PingMachine_pong", + "'0'", + "ping", + "'0'", + "pong", + "'0'" + ], + "type" : { + "read" : { + + } + } + }, + { + "currentState" : "SendPing", + "executeOnEntry" : true, + "nextState" : "SendPing", + "properties" : [ + "PingMachine_ping", + "'0'", + "PingMachine_pong", + "'1'", + "ping", + "'0'", + "pong", + "'1'" + ], + "type" : { + "read" : { + + } + } + }, + { + "currentState" : "WaitForPong", + "executeOnEntry" : false, + "nextState" : "WaitForPong", + "properties" : [ + "PingMachine_ping", + "'0'", + "PingMachine_pong", + "'1'", + "ping", + "'0'", + "pong", + "'1'" + ], + "type" : { + "read" : { + + } + } + } + ] + } + """ + } + +// swiftlint:enable file_length From b8882913229538495300b9c40169b0460d933160 Mon Sep 17 00:00:00 2001 From: Morgan McColl Date: Tue, 4 Jun 2024 13:28:08 +1000 Subject: [PATCH 16/20] Depend on VHDLKripkeStructures in test targets. --- Package.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index db986d3..6952a38 100644 --- a/Package.swift +++ b/Package.swift @@ -58,7 +58,8 @@ let package = Package( dependencies: [ .product(name: "VHDLMachines", package: "VHDLMachines"), .product(name: "VHDLParsing", package: "VHDLParsing"), - .product(name: "VHDLJSModels", package: "VHDLJSModels") + .product(name: "VHDLJSModels", package: "VHDLJSModels"), + .product(name: "VHDLKripkeStructures", package: "VHDLKripkeStructures") ] ) ] From 2c17ed2468c884b4fb365a509b93d5a36c3120f9 Mon Sep 17 00:00:00 2001 From: Morgan McColl Date: Tue, 4 Jun 2024 13:47:52 +1000 Subject: [PATCH 17/20] Added property for ping machine build folder. --- Tests/MachineGeneratorTests/MachineTester.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Tests/MachineGeneratorTests/MachineTester.swift b/Tests/MachineGeneratorTests/MachineTester.swift index b580418..fe6aaf7 100644 --- a/Tests/MachineGeneratorTests/MachineTester.swift +++ b/Tests/MachineGeneratorTests/MachineTester.swift @@ -123,6 +123,11 @@ class MachineTester: XCTestCase { machinesFolder.appendingPathComponent("PingMachine.machine", isDirectory: true) } + /// The build folder in the ping machine. + var pingMachineBuildFolder: URL { + pingMachineFolder.appendingPathComponent("build", isDirectory: true) + } + /// The path to the `PingMachine` kripke structure (`output.json`). var pingMachineKripkeStructure: URL { pingMachineFolder.appendingPathComponent("output.json", isDirectory: false) From f38fc08168c12d5a7efd30bed81019d97bbc4d6b Mon Sep 17 00:00:00 2001 From: Morgan McColl Date: Tue, 4 Jun 2024 13:47:59 +1000 Subject: [PATCH 18/20] Started testing graph command. --- Tests/MachineGeneratorTests/GraphTests.swift | 132 +++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 Tests/MachineGeneratorTests/GraphTests.swift diff --git a/Tests/MachineGeneratorTests/GraphTests.swift b/Tests/MachineGeneratorTests/GraphTests.swift new file mode 100644 index 0000000..e18679c --- /dev/null +++ b/Tests/MachineGeneratorTests/GraphTests.swift @@ -0,0 +1,132 @@ +// GraphTests.swift +// LLFSMGenerate +// +// Created by Morgan McColl. +// Copyright © 2024 Morgan McColl. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials +// provided with the distribution. +// +// 3. All advertising materials mentioning features or use of this +// software must display the following acknowledgement: +// +// This product includes software developed by Morgan McColl. +// +// 4. Neither the name of the author nor the names of contributors +// may be used to endorse or promote products derived from this +// software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER +// OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// ----------------------------------------------------------------------- +// This program is free software; you can redistribute it and/or +// modify it under the above terms or under the terms of the GNU +// General Public License as published by the Free Software Foundation; +// either version 2 of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, see http://www.gnu.org/licenses/ +// or write to the Free Software Foundation, Inc., 51 Franklin Street, +// Fifth Floor, Boston, MA 02110-1301, USA. + +@testable import MachineGenerator +import VHDLKripkeStructures +import XCTest + +/// Test class for ``GraphCommand``. +final class GraphTests: MachineTester { + + /// The path to the dot file. + var dotFile: URL { + self.packageRootPath.appendingPathComponent("output.dot", isDirectory: false) + } + + /// The dot file in the build folder. + var buildDotFile: URL { + self.pingMachineBuildFolder.appendingPathComponent("output.dot", isDirectory: false) + } + + /// Remove the dot file after every test. + override func tearDown() { + super.tearDown() + try? manager.removeItem(at: self.dotFile) + } + + /// Test the kripke structure is generated correctly for a machine. + func testMachineGeneration() throws { + XCTAssertFalse(manager.fileExists(atPath: dotFile.path)) + GraphCommand.main(["--machine", self.pingMachineFolder.path]) + var isDirectory: ObjCBool = false + XCTAssertTrue(manager.fileExists(atPath: dotFile.path, isDirectory: &isDirectory)) + XCTAssertFalse(isDirectory.boolValue) + XCTAssertFalse(try Data(contentsOf: dotFile).isEmpty) + } + + /// Test the kripke structure is generated correctly for a machine with a destination. + func testMachineGenerationWithDestination() throws { + XCTAssertFalse(manager.fileExists(atPath: buildDotFile.path)) + GraphCommand.main(["--machine", self.pingMachineFolder.path, "--destination", self.buildDotFile.path]) + var isDirectory: ObjCBool = false + XCTAssertTrue(manager.fileExists(atPath: buildDotFile.path, isDirectory: &isDirectory)) + XCTAssertFalse(isDirectory.boolValue) + XCTAssertFalse(try Data(contentsOf: buildDotFile).isEmpty) + } + + /// Test the generation given the file as a path. + func testGeneration() throws { + XCTAssertFalse(manager.fileExists(atPath: dotFile.path)) + GraphCommand.main([self.pingMachineKripkeStructure.path]) + var isDirectory: ObjCBool = false + XCTAssertTrue(manager.fileExists(atPath: dotFile.path, isDirectory: &isDirectory)) + XCTAssertFalse(isDirectory.boolValue) + XCTAssertFalse(try Data(contentsOf: dotFile).isEmpty) + } + + /// Test that generation works with a destination. + func testGenerationWithDestination() throws { + XCTAssertFalse(manager.fileExists(atPath: buildDotFile.path)) + GraphCommand.main([self.pingMachineKripkeStructure.path, "--destination", self.buildDotFile.path]) + var isDirectory: ObjCBool = false + XCTAssertTrue(manager.fileExists(atPath: buildDotFile.path, isDirectory: &isDirectory)) + XCTAssertFalse(isDirectory.boolValue) + XCTAssertFalse(try Data(contentsOf: buildDotFile).isEmpty) + } + + /// Test that the command throws an error for an invalid machine. + func testGenerationThrowsErrorForInvalidMachine() throws { + let command = try GraphCommand.parse(["--machine", self.pingMachineKripkeStructure.path]) + XCTAssertThrowsError(try command.run()) { + guard let error = $0 as? GenerationError, case .invalidInput(let message) = error else { + XCTFail("Expected GenerationError but got \($0)") + return + } + XCTAssertEqual(message, "The path must be a valid machines location.") + } + XCTAssertFalse(manager.fileExists(atPath: dotFile.path)) + } + +} From 2f5bdf996816e9935c2270145c49e855d10f0a15 Mon Sep 17 00:00:00 2001 From: Morgan McColl Date: Tue, 4 Jun 2024 14:06:46 +1000 Subject: [PATCH 19/20] Added more tests. --- Tests/MachineGeneratorTests/GraphTests.swift | 49 ++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/Tests/MachineGeneratorTests/GraphTests.swift b/Tests/MachineGeneratorTests/GraphTests.swift index e18679c..74fd234 100644 --- a/Tests/MachineGeneratorTests/GraphTests.swift +++ b/Tests/MachineGeneratorTests/GraphTests.swift @@ -129,4 +129,53 @@ final class GraphTests: MachineTester { XCTAssertFalse(manager.fileExists(atPath: dotFile.path)) } + /// Test that the correct error is thrown when the kripke structure is not present. + func testCommandThrowsErrorForMissingKripkeStructure() throws { + try manager.removeItem(at: self.pingMachineKripkeStructure) + let command = try GraphCommand.parse([self.pingMachineKripkeStructure.path]) + XCTAssertThrowsError(try command.run()) { + guard let error = $0 as? GenerationError, case .invalidMachine(let message) = error else { + XCTFail("Expected GenerationError but got \($0)") + return + } + XCTAssertEqual(message, "The Kripke structure does not exist at this specified location.") + } + XCTAssertFalse(manager.fileExists(atPath: dotFile.path)) + } + + /// Test command overwrites existing `.dot` file. + func testCommandOverwritesFile() throws { + XCTAssertTrue(manager.createFile(atPath: dotFile.path, contents: nil)) + GraphCommand.main([self.pingMachineKripkeStructure.path, "--destination", self.dotFile.path]) + var isDirectory: ObjCBool = false + XCTAssertTrue(manager.fileExists(atPath: dotFile.path, isDirectory: &isDirectory)) + XCTAssertFalse(isDirectory.boolValue) + XCTAssertFalse(try Data(contentsOf: dotFile).isEmpty) + } + + /// Test that the file is created in a subdirectory that exists. + func testCommandPlacesFileInFolderthatExists() throws { + try manager.createDirectory(at: self.pingMachineBuildFolder, withIntermediateDirectories: true) + XCTAssertFalse(manager.fileExists(atPath: self.buildDotFile.path)) + GraphCommand.main([ + self.pingMachineKripkeStructure.path, "--destination", self.pingMachineBuildFolder.path + ]) + var isDirectory: ObjCBool = false + XCTAssertTrue(manager.fileExists(atPath: buildDotFile.path, isDirectory: &isDirectory)) + XCTAssertFalse(isDirectory.boolValue) + XCTAssertFalse(try Data(contentsOf: buildDotFile).isEmpty) + } + + /// Test that the file is created in a subdirectory that doesn't exist. + func testCommandPlacesFileInFolder() throws { + XCTAssertFalse(manager.fileExists(atPath: self.pingMachineBuildFolder.path)) + GraphCommand.main([ + self.pingMachineKripkeStructure.path, "--destination", self.pingMachineBuildFolder.path + ]) + var isDirectory: ObjCBool = false + XCTAssertTrue(manager.fileExists(atPath: buildDotFile.path, isDirectory: &isDirectory)) + XCTAssertFalse(isDirectory.boolValue) + XCTAssertFalse(try Data(contentsOf: buildDotFile).isEmpty) + } + } From 34365d54a872df07d3530782d9e46c6621402271 Mon Sep 17 00:00:00 2001 From: Morgan McColl Date: Tue, 4 Jun 2024 14:09:44 +1000 Subject: [PATCH 20/20] Added comments. --- Sources/MachineGenerator/GraphCommand.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Sources/MachineGenerator/GraphCommand.swift b/Sources/MachineGenerator/GraphCommand.swift index 83f4aac..a707cac 100644 --- a/Sources/MachineGenerator/GraphCommand.swift +++ b/Sources/MachineGenerator/GraphCommand.swift @@ -57,6 +57,7 @@ import ArgumentParser import Foundation import VHDLKripkeStructures +/// A command for generating `GraphViz` files (`.dot`) from encoded Kripke Structures in JSON. struct GraphCommand: ParsableCommand { /// The configuration of this command. @@ -65,9 +66,11 @@ struct GraphCommand: ParsableCommand { abstract: "Generate a graphviz file (.dot) for the entire kripke structure." ) + /// Whether the `path` points to a machine folder. @Flag(name: .customLong("machine"), help: "Whether the path is a machine folder.") var isMachine = false + /// The `path` to the Kripke Structure. @Argument(help: """ The path to the resource containing the Kripke Structure. This path may be the json file itself or a path to a machine folder. @@ -75,9 +78,11 @@ struct GraphCommand: ParsableCommand { ) var path: String + /// The destination location of the generated graphviz file. @Option(help: "The path of the newly generated graphviz file.") var destination: String? + /// Generate the graphviz file. func run() throws { guard isMachine else { let pathURL = URL(fileURLWithPath: path, isDirectory: false) @@ -98,6 +103,8 @@ struct GraphCommand: ParsableCommand { try self.generate(pathURL: pathURL) } + /// Generate the graphviz file from the kripke structure in `url`. + /// - Parameter url: The path to the kripke structure. func generate(pathURL url: URL) throws { let manager = FileManager.default var isDirectory: ObjCBool = false @@ -122,6 +129,9 @@ struct GraphCommand: ParsableCommand { try data.write(to: graphvizFile, options: .atomic) } + /// Get the `URL` for the graphviz file. + /// - Parameter name: The default name of the file. + /// - Returns: The path to the graphviz file. func graphvizFile(defaultName name: String) -> URL { let manager = FileManager.default guard let destination else {