From 0fb64d1e5422dbb9c7545c29c949cef275adf794 Mon Sep 17 00:00:00 2001 From: Stephen Martinez Date: Mon, 20 Jan 2020 09:50:53 -0800 Subject: [PATCH 01/10] [CPFeature-010][Tests][NewTargets] Created two new targets CLInterface and FoundationExt for testing purposes. --- .../xcschemes/commitPrefix.xcscheme | 42 +++++++ Package.swift | 19 ++- .../CLInterface/ArgumentBuilder.swift | 0 .../CLInterface/CLIArguments.swift | 22 ++-- Sources/CLInterface/CLIError.swift | 114 ++++++++++++++++++ .../Result+Ext.swift} | 26 ++-- .../Utilities => FoundationExt}/Shell.swift | 4 +- .../String+Ext.swift} | 2 +- Tests/CommitPrefixTests/XCTestManifests.swift | 35 ------ 9 files changed, 199 insertions(+), 65 deletions(-) rename Sources/{CommitPrefix => }/CLInterface/ArgumentBuilder.swift (100%) rename Sources/{CommitPrefix => }/CLInterface/CLIArguments.swift (91%) create mode 100644 Sources/CLInterface/CLIError.swift rename Sources/{CommitPrefix/Utilities/Result+Extensions.swift => FoundationExt/Result+Ext.swift} (78%) rename Sources/{CommitPrefix/Utilities => FoundationExt}/Shell.swift (95%) rename Sources/{CommitPrefix/Utilities/String+Extensions.swift => FoundationExt/String+Ext.swift} (98%) delete mode 100644 Tests/CommitPrefixTests/XCTestManifests.swift diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/commitPrefix.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/commitPrefix.xcscheme index 5b968ef..3ecbb41 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/commitPrefix.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/commitPrefix.xcscheme @@ -34,6 +34,48 @@ ReferencedContainer = "container:"> + + + + + + + + + + + + private let userEntry: PositionalArgument<[String]> - init(arguments: [String] = CommandLine.arguments) { + public init(arguments: [String] = CommandLine.arguments) { // The first argument specifies the path of the executable file self.rawArgs = Array(arguments.dropFirst()) let argBuilder = ArgumentBuilder() @@ -73,7 +73,7 @@ struct CLIArguments { self.userEntry = argBuilder.buildUserEntryArgument(parser: parser) } - private func singleCommandParse(_ allCommands: [ParsedCommand]) -> Result { + private func singleCommandParse(_ allCommands: [ParsedCommand]) -> Result { precondition(allCommands.count == 1, "Intended for single Parsed Command only!") guard let foundCommand = allCommands.first else { return .failure(.commandNotRecognized) @@ -95,7 +95,7 @@ struct CLIArguments { } } - private func doubleCommandParse(_ allCommands: [ParsedCommand]) -> Result { + private func doubleCommandParse(_ allCommands: [ParsedCommand]) -> Result { precondition(allCommands.count == 2, "Intended for two Parsed Commands only!") let firstCommand = allCommands[0] let secondCommand = allCommands[1] @@ -110,7 +110,7 @@ struct CLIArguments { } } - private func getCommand() -> Result { + public func getCommand() -> Result { guard let parsedArgs = try? parser.parse(rawArgs) else { return .failure(.commandNotRecognized) } @@ -126,12 +126,12 @@ struct CLIArguments { do { try parsedArgs.get(userEntry).map { userEntry in let noMoreThanOneEntry = userEntry.count < 2 - guard noMoreThanOneEntry else { throw CPError.invalidEntryFormat } - guard let theEntry = userEntry.first else { throw CPError.emptyEntry } + guard noMoreThanOneEntry else { throw CLIError.invalidEntryFormat } + guard let theEntry = userEntry.first else { throw CLIError.emptyEntry } allCommands.append(.userEntry(value: theEntry)) } - } catch let cpError as CPError { - return .failure(cpError) + } catch let cliError as CLIError { + return .failure(cliError) } catch { return .failure(.unexpectedError) } @@ -149,6 +149,6 @@ struct CLIArguments { } - var command: UserCommand { getCommand().resolveOrExit() } + public var command: UserCommand { getCommand().resolveOrExit() } } diff --git a/Sources/CLInterface/CLIError.swift b/Sources/CLInterface/CLIError.swift new file mode 100644 index 0000000..37ab2a9 --- /dev/null +++ b/Sources/CLInterface/CLIError.swift @@ -0,0 +1,114 @@ +// +// CLIError.swift +// commitPrefix +// +// MIT License +// +// Copyright (c) 2020 STEPHEN L. MARTINEZ +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +import Consler + +public enum TerminationStatus: Int32 { + /// Used when the app finishes as expected + case successful + + /// Used when an error that has not been accounted for has been thrown + case unexpectedError + + /// Used when the user takes an action that stops the application short + case userInitiated + + /// Used when the inputs provided to the app are invalid + case invalidInputs + + /// Used when the app can no longer continue due to user specified settings + case invalidContext + + /// Used when required resources are inaccessible or unavailable + case unavailableDependencies + + public var value: Int32 { self.rawValue } +} + +public enum CLIError: Error, Equatable { + + case commandNotRecognized + case tooManyArguments + case emptyEntry + case invalidEntryFormat + case unexpectedError + + public var message: ConslerOutput { + switch self { + case .commandNotRecognized: + return ConslerOutput( + "Error: ", "Command not recognized. Enter ", "\"--help\"", " for usage.") + .describedBy(.boldRed, .normal, .cyan) + + case .tooManyArguments: + return ConslerOutput( + "Error: ", "Too many arguments entered. Only two at a time is supported.") + .describedBy(.boldRed) + + case .emptyEntry: + return ConslerOutput("Error: ", "Your entry is empty.").describedBy(.boldRed) + + case .invalidEntryFormat: + return ConslerOutput("Error: ", "Your entry contains invalid spaces.") + .describedBy(.boldRed) + + case .unexpectedError: + return ConslerOutput("Error: ", "An uncategorized error has occured") + .describedBy(.boldRed) + } + } + + public var status: TerminationStatus { + switch self { + case .commandNotRecognized: + return .invalidInputs + case .tooManyArguments: + return .invalidInputs + case .emptyEntry: + return .invalidInputs + case .invalidEntryFormat: + return .invalidInputs + case .unexpectedError: + return .unexpectedError + } + } + +} + +extension Result where Failure == CLIError { + + func resolveOrExit() -> Success { + switch self { + case let .success(value): + return value + case let .failure(cliError): + Consler.output(cliError.message, type: .error) + exit(cliError.status.value) + } + } + +} diff --git a/Sources/CommitPrefix/Utilities/Result+Extensions.swift b/Sources/FoundationExt/Result+Ext.swift similarity index 78% rename from Sources/CommitPrefix/Utilities/Result+Extensions.swift rename to Sources/FoundationExt/Result+Ext.swift index 2bfafc4..6f00522 100644 --- a/Sources/CommitPrefix/Utilities/Result+Extensions.swift +++ b/Sources/FoundationExt/Result+Ext.swift @@ -25,9 +25,8 @@ // SOFTWARE. import Foundation -import Consler -extension Result { +public extension Result { func transform(_ newValue: NewSuccess) -> Result { switch self { @@ -38,17 +37,24 @@ extension Result { } } -} - -extension Result where Failure == CPError { + var optional: Success? { + switch self { + case let .success(successValue): + return successValue + case .failure: + return nil + } + } - func resolveOrExit() -> Success { + func `do`( + onFailure: @escaping (Failure) -> Void = { _ in }, + onSuccess: @escaping (Success) -> Void + ) { switch self { case let .success(value): - return value - case let .failure(cpError): - Consler.output(cpError.message, type: .error) - exit(cpError.status.value) + onSuccess(value) + case let .failure(error): + onFailure(error) } } diff --git a/Sources/CommitPrefix/Utilities/Shell.swift b/Sources/FoundationExt/Shell.swift similarity index 95% rename from Sources/CommitPrefix/Utilities/Shell.swift rename to Sources/FoundationExt/Shell.swift index 8cace1d..b54d05a 100644 --- a/Sources/CommitPrefix/Utilities/Shell.swift +++ b/Sources/FoundationExt/Shell.swift @@ -26,9 +26,9 @@ import Foundation -struct Shell { +public struct Shell { - static func makeExecutable(_ fileName: String) { + public static func makeExecutable(_ fileName: String) { let executableProcess = Process() executableProcess.launchPath = "/usr/bin/env" executableProcess.arguments = ["chmod", "755", fileName] diff --git a/Sources/CommitPrefix/Utilities/String+Extensions.swift b/Sources/FoundationExt/String+Ext.swift similarity index 98% rename from Sources/CommitPrefix/Utilities/String+Extensions.swift rename to Sources/FoundationExt/String+Ext.swift index db7995c..9a4fac6 100644 --- a/Sources/CommitPrefix/Utilities/String+Extensions.swift +++ b/Sources/FoundationExt/String+Ext.swift @@ -26,7 +26,7 @@ import Foundation -extension String { +public extension String { private func findMatches(in string: String, using regex: String) -> [String] { diff --git a/Tests/CommitPrefixTests/XCTestManifests.swift b/Tests/CommitPrefixTests/XCTestManifests.swift deleted file mode 100644 index d56b574..0000000 --- a/Tests/CommitPrefixTests/XCTestManifests.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// XCTestManifests.swift -// commitPrefix -// -// MIT License -// -// Copyright (c) 2019 STEPHEN L. MARTINEZ -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -import XCTest - -#if !canImport(ObjectiveC) -public func allTests() -> [XCTestCaseEntry] { - return [ - testCase(commitPrefixTests.allTests), - ] -} -#endif From ee5dca6ecbb406f96af479015691ef2ace7ba73e Mon Sep 17 00:00:00 2001 From: Stephen Martinez Date: Mon, 20 Jan 2020 09:52:50 -0800 Subject: [PATCH 02/10] [CPFeature-010][Tests] Added new tests for CLInterface --- .../CommitPrefixTests/CLInterfaceTests.swift | 259 ++++++++++++++++++ .../CommitPrefixTests/CommitPrefixTests.swift | 79 +++--- Tests/LinuxMain.swift | 8 +- 3 files changed, 310 insertions(+), 36 deletions(-) create mode 100644 Tests/CommitPrefixTests/CLInterfaceTests.swift diff --git a/Tests/CommitPrefixTests/CLInterfaceTests.swift b/Tests/CommitPrefixTests/CLInterfaceTests.swift new file mode 100644 index 0000000..0933a24 --- /dev/null +++ b/Tests/CommitPrefixTests/CLInterfaceTests.swift @@ -0,0 +1,259 @@ +// +// CLInterfaceTests.swift +// commitPrefix +// +// MIT License +// +// Copyright (c) 2020 STEPHEN L. MARTINEZ +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import CLInterface +import Foundation +import FoundationExt +import XCTest + +final class CLInterfaceTests: XCTestCase { + + func test_SingleArgumentShortCommands() { + CLITestArgument.singleArgs.forEach { + CLITestUtil.assertOnProperSingleArg($0.proper) + } + } + + func test_SingleArgumentLongCommands() { + CLITestArgument.singleArgs.forEach { + CLITestUtil.assertOnProperSingleArg($0.proper, useLong: true) + } + } + + func test_FailingSingleArgumentShortCommands() { + CLITestArgument.singleArgs.forEach { + CLITestUtil.assertOnMalformedSingleArg($0.malformed) + } + } + + func test_FailingSingleArgumentLongCommands() { + CLITestArgument.singleArgs.forEach { + CLITestUtil.assertOnMalformedSingleArg($0.malformed, useLong: true) + } + } + + func test_ExtraArgsSingleArgumentShortCommands() { + CLITestArgument.singleArgs.forEach { + CLITestUtil.assertOnExtraArgsForSingleArg($0.proper, extraInput: $0.extraInput) + } + } + + func test_ExtraArgsSingleArgumentLongCommands() { + CLITestArgument.singleArgs.forEach { + CLITestUtil + .assertOnExtraArgsForSingleArg($0.proper, extraInput: $0.extraInput, useLong: true) + } + } + + // Special Command that uses no args except for the standard default OS argument + func test_viewStateCommand() { + let args = ["executable/location"] + let argument = CLIArguments(arguments: args) + let expectedCommand = UserCommand.viewState + let failureMessage = "Expected no input to return an output of" + + " \(expectedCommand.description)" + + print("⚖️ ~ Testing no arguments for command \(expectedCommand.description)") + func validateCommand(foundCommand: UserCommand) -> Void { + let isValid = UserCommand.isEquivelent(lhs: foundCommand, rhs: expectedCommand) + XCTAssert(isValid, "\(failureMessage) got \(foundCommand.description) instead") + } + + argument.getCommand().do( + onFailure: { XCTFail("Input: None \($0.description)") }, + onSuccess: validateCommand) + } + + static var allTests = [ + ("test_SingleArgumentShortCommands", test_SingleArgumentShortCommands), + ("test_SingleArgumentLongCommands", test_SingleArgumentLongCommands), + ("test_FailingSingleArgumentShortCommands", test_FailingSingleArgumentShortCommands), + ("test_FailingSingleArgumentLongCommands", test_FailingSingleArgumentLongCommands), + ("test_ExtraArgsSingleArgumentShortCommands", test_ExtraArgsSingleArgumentShortCommands), + ("test_ExtraArgsSingleArgumentLongCommands", test_ExtraArgsSingleArgumentLongCommands), + ("test_viewStateCommand", test_viewStateCommand) + ] + +} + +typealias UserCommand = CLIArguments.UserCommand + +typealias CLITestArgument = CLITestUtil.Argument + +struct CLITestUtil { + + struct Argument { + let short: String + let long: String + let expected: UserCommand + + struct SingleArgumentPair { + let proper: Argument + let malformed: Argument + let extraInput: String + } + + // Correctly Formed Arguments + static let version = Self(short: "-v", long: "--version", expected: .outputVersion) + static let output = Self(short: "-o", long: "--output", expected: .outputPrefixes) + static let delete = Self(short: "-d", long: "--delete", expected: .deletePrefixes) + static let normal = Self(short: "-n", long: "--normal", expected: .modeNormal) + + // Malformed Arguments + static let badVersion = Self(short: "-cv", long: "--verzion", expected: .outputVersion) + static let badOutput = Self(short: "-op", long: "--ouput", expected: .outputPrefixes) + static let badDelete = Self(short: "-ds", long: "--dekete", expected: .deletePrefixes) + static let badNormal = Self(short: "-nm", long: "--notmal", expected: .modeNormal) + + static let singleArgs: [SingleArgumentPair] = [ + .init(proper: .version, malformed: .badVersion, extraInput: "need me some version"), + .init(proper: .output, malformed: .badOutput, extraInput: "show me the raw output"), + .init(proper: .delete, malformed: .badDelete, extraInput: "delete this prefix"), + .init(proper: .normal, malformed: .badNormal, extraInput: "get me to normal mode") + ] + + } + + static func makeArgument( + _ arg: Argument, + useLong: Bool = false, + userInput: String? = nil + ) -> [String] { + let argList = ["executable/location", (useLong ? arg.long : arg.short)] + guard let userInput = userInput else { return argList } + return argList + [userInput] + } + + static func assertOnProperSingleArg(_ command: Argument, useLong: Bool = false) { + let inputValue = useLong ? command.long : command.short + print("⚖️ ~ Testing \(inputValue) for command \(command.expected.description)") + let argument = CLIArguments(arguments: makeArgument(command, useLong: useLong)) + let failureMessage = """ + Expected the input of \(inputValue) to return an output of \(command.expected.description) + """ + func validateCommand(foundCommand: UserCommand) -> Void { + guard UserCommand.isEquivelent(lhs: foundCommand, rhs: command.expected) else { + XCTFail(failureMessage) + return + } + } + argument.getCommand().do( + onFailure: { XCTFail("Input: \(inputValue) " + $0.description) }, + onSuccess: validateCommand) + } + + static func assertOnMalformedSingleArg(_ command: Argument, useLong: Bool = false) { + let badInputValue = useLong ? command.long : command.short + print("⚖️ ~ Testing invalid \(badInputValue) for command \(command.expected.description)") + let badArgument = CLIArguments(arguments: makeArgument(command, useLong: useLong)) + let badFailureMessage = """ + Expected the input of \(badInputValue) to fail in outputting \(command.expected.description) + """ + badArgument.getCommand().do { _ in XCTFail(badFailureMessage) } + } + + static func assertOnExtraArgsForSingleArg( + _ command: Argument, + extraInput: String, + useLong: Bool = false + ) { + let inputValue = useLong ? command.long : command.short + let testingMessage = "⚖️ ~ Testing invalid extra args \(inputValue) \(extraInput)" + + " for command \(command.expected.description)" + print(testingMessage) + let badArgument = CLIArguments( + arguments: makeArgument(command, useLong: useLong, userInput: extraInput)) + let badFailureMessage = "Expected the input of \(inputValue) \(extraInput)" + + "to fail in outputting \(command.expected.description)" + badArgument.getCommand().do { _ in XCTFail(badFailureMessage) } + } + +} + +extension CLIError { + + var description: String { + switch self { + case .commandNotRecognized: + return "CLIError: Command Not Recognized" + case .tooManyArguments: + return "CLIError: Too Many Arguments" + case .emptyEntry: + return "CLIError: Empty Entry" + case .invalidEntryFormat: + return "CLIError: Invalid Entry Format" + case .unexpectedError: + return "CLIError: Unexpected Error" + } + } + +} + +extension UserCommand { + + var description: String { + switch self { + case .outputVersion: + return "outputVersion" + case .viewState: + return "viewState" + case .outputPrefixes: + return "outputPrefixes" + case .deletePrefixes: + return "deletePrefixes" + case .modeNormal: + return "modeNormal" + case let .modeBranchParse(validator): + return "modeBranchParse \(validator)" + case let .newPrefixes(value): + return "newPrefixes \(value)" + } + + } + + static func isEquivelent(lhs: UserCommand, rhs: UserCommand) -> Bool { + switch (lhs, rhs) { + case (.outputVersion, .outputVersion): + return true + case (.viewState, .viewState): + return true + case (.outputPrefixes, .outputPrefixes): + return true + case (.deletePrefixes, .deletePrefixes): + return true + case (.modeNormal, .modeNormal): + return true + case (.modeBranchParse, .modeBranchParse): + return true + case (.newPrefixes, .newPrefixes): + return true + default: + return false + } + + } + +} diff --git a/Tests/CommitPrefixTests/CommitPrefixTests.swift b/Tests/CommitPrefixTests/CommitPrefixTests.swift index a923b14..cbd44e4 100644 --- a/Tests/CommitPrefixTests/CommitPrefixTests.swift +++ b/Tests/CommitPrefixTests/CommitPrefixTests.swift @@ -24,51 +24,64 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +import CLInterface +import Foundation import XCTest -import class Foundation.Bundle final class commitPrefixTests: XCTestCase { + + static var allTests = [ + ("testExample", testExample) + ] + func testExample() throws { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct // results. // Some of the APIs that we use below are available in macOS 10.13 and above. - guard #available(macOS 10.13, *) else { - return - } - - let fooBinary = productsDirectory.appendingPathComponent("commitPrefix") - - let process = Process() - process.executableURL = fooBinary - - let pipe = Pipe() - process.standardOutput = pipe - - try process.run() - process.waitUntilExit() - - let data = pipe.fileHandleForReading.readDataToEndOfFile() - let output = String(data: data, encoding: .utf8) - - XCTAssert(false, "Tests have not been implemented") +// guard #available(macOS 10.13, *) else { +// return +// } +// +// let fooBinary = productsDirectory.appendingPathComponent("commitPrefix") +// +// let process = Process() +// process.executableURL = fooBinary +// +// let pipe = Pipe() +// process.standardOutput = pipe +// +// let errorPipe = Pipe() +// process.standardError = errorPipe +// +// try process.run() +// process.waitUntilExit() +// +// let data = pipe.fileHandleForReading.readDataToEndOfFile() +// let output = String(data: data, encoding: .utf8) +// +// let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile() +// let errorOutput = String(data: errorData, encoding: .utf8) +// +// print("process output", output ?? "No output") +// print("process Error Output", errorOutput ?? "No output") +// +// XCTAssert(true, "Tests have not been implemented") //XCTAssertEqual(output, "Hello, world!\n") } /// Returns path to the built products directory. - var productsDirectory: URL { - #if os(macOS) - for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") { - return bundle.bundleURL.deletingLastPathComponent() - } - fatalError("couldn't find the products directory") - #else - return Bundle.main.bundleURL - #endif - } +// var productsDirectory: URL { +// #if os(macOS) +// for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") { +// return bundle.bundleURL.deletingLastPathComponent() +// } +// fatalError("couldn't find the products directory") +// #else +// return Bundle.main.bundleURL +// #endif +// } + - static var allTests = [ - ("testExample", testExample), - ] } diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 0f66cdf..7de07b2 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -25,9 +25,11 @@ // SOFTWARE. import XCTest - -import commitPrefixTests +@testable import CommitPrefixTests var tests = [XCTestCaseEntry]() -tests += commitPrefixTests.allTests() + +tests += commitPrefixTests.allTests +tests += CLInterfaceTests.allTests + XCTMain(tests) From 9043d2b6811ac787e74d27ee5998e8b8d9c65e1e Mon Sep 17 00:00:00 2001 From: Stephen Martinez Date: Mon, 20 Jan 2020 09:53:27 -0800 Subject: [PATCH 03/10] [CPFeature-010][Tests] Updated with new CLInterface module --- .../CommitPrefix/Error+Debug/CPError.swift | 68 ++++--------------- 1 file changed, 15 insertions(+), 53 deletions(-) diff --git a/Sources/CommitPrefix/Error+Debug/CPError.swift b/Sources/CommitPrefix/Error+Debug/CPError.swift index 714c235..1795342 100644 --- a/Sources/CommitPrefix/Error+Debug/CPError.swift +++ b/Sources/CommitPrefix/Error+Debug/CPError.swift @@ -24,43 +24,16 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +import enum CLInterface.TerminationStatus import Consler import Foundation -enum TerminationStatus: Int32 { - /// Used when the app finishes as expected - case successful - - /// Used when an error that has not been accounted for has been thrown - case unexpectedError - - /// Used when the user takes an action that stops the application short - case userInitiated - - /// Used when the inputs provided to the app are invalid - case invalidInputs - - /// Used when the app can no longer continue due to user specified settings - case invalidContext - - /// Used when required resources are inaccessible or unavailable - case unavailableDependencies - - var value: Int32 { self.rawValue } -} - enum CPError: Error { - // MARK: - CLI Errors - case commandNotRecognized - case tooManyArguments - case emptyEntry - // MARK: - User Termination Errors case overwriteCancelled // MARK: - Format Errors - case invalidEntryFormat case invalidBranchValidatorFormat case invalidBranchPrefix(validator: String) case invalidYesOrNoFormat @@ -81,26 +54,9 @@ enum CPError: Error { var message: ConslerOutput { switch self { - case .commandNotRecognized: - return ConslerOutput( - "Error: ", "Command not recognized. Enter ", "\"--help\"", " for usage.") - .describedBy(.boldRed, .normal, .cyan) - - case .tooManyArguments: - return ConslerOutput( - "Error: ", "Too many arguments entered. Only two at a time is supported.") - .describedBy(.boldRed) - - case .emptyEntry: - return ConslerOutput("Error: ", "Your entry is empty.").describedBy(.boldRed) - case .overwriteCancelled: return ConslerOutput("Error: ", "Overwrite is cancelled").describedBy(.boldRed) - case .invalidEntryFormat: - return ConslerOutput("Error: ", "Your entry contains invalid spaces.") - .describedBy(.boldRed) - case .invalidBranchValidatorFormat: return ConslerOutput( "Error: ", "The branch validator must be at least two characters long ", @@ -155,16 +111,8 @@ enum CPError: Error { var status: TerminationStatus { switch self { - case .commandNotRecognized: - return .invalidInputs - case .tooManyArguments: - return .invalidInputs - case .emptyEntry: - return .invalidInputs case .overwriteCancelled: return .userInitiated - case .invalidEntryFormat: - return .invalidInputs case .invalidBranchValidatorFormat: return .invalidInputs case .invalidBranchPrefix: @@ -190,3 +138,17 @@ enum CPError: Error { } } + +extension Result where Failure == CPError { + + func resolveOrExit() -> Success { + switch self { + case let .success(value): + return value + case let .failure(cpError): + Consler.output(cpError.message, type: .error) + exit(cpError.status.value) + } + } + +} From ed815763eca44d7bc12440c2dc2582250daf3481 Mon Sep 17 00:00:00 2001 From: Stephen Martinez Date: Mon, 20 Jan 2020 09:53:51 -0800 Subject: [PATCH 04/10] [CPFeature-010][Tests] Updated with new CLInterface model --- Sources/CommitPrefix/main.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/CommitPrefix/main.swift b/Sources/CommitPrefix/main.swift index 22bce7b..3f0bc41 100644 --- a/Sources/CommitPrefix/main.swift +++ b/Sources/CommitPrefix/main.swift @@ -24,6 +24,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +import CLInterface import Consler import Foundation From 17aac7956629e69e992c8a20ed3aa689f32531c5 Mon Sep 17 00:00:00 2001 From: Stephen Martinez Date: Mon, 20 Jan 2020 09:54:06 -0800 Subject: [PATCH 05/10] [CPFeature-010][Tests] Updated with new FoundationExt module --- Sources/CommitPrefix/CPInteractor.swift | 1 + Sources/CommitPrefix/Hook/CommitMessageHook.swift | 1 + 2 files changed, 2 insertions(+) diff --git a/Sources/CommitPrefix/CPInteractor.swift b/Sources/CommitPrefix/CPInteractor.swift index 0bd516e..58bf320 100644 --- a/Sources/CommitPrefix/CPInteractor.swift +++ b/Sources/CommitPrefix/CPInteractor.swift @@ -27,6 +27,7 @@ import Consler import Files import Foundation +import FoundationExt struct CPInteractor { diff --git a/Sources/CommitPrefix/Hook/CommitMessageHook.swift b/Sources/CommitPrefix/Hook/CommitMessageHook.swift index 934c7c2..ecdd080 100644 --- a/Sources/CommitPrefix/Hook/CommitMessageHook.swift +++ b/Sources/CommitPrefix/Hook/CommitMessageHook.swift @@ -27,6 +27,7 @@ import Consler import Files import Foundation +import FoundationExt struct CommitMessageHook { From fbc9b4142a50490797a8a1935797edc43d9e9d35 Mon Sep 17 00:00:00 2001 From: Stephen Martinez Date: Mon, 20 Jan 2020 11:37:42 -0800 Subject: [PATCH 06/10] [CPFeature-010][Tests] Added Test utilites file --- Tests/CommitPrefixTests/TestUtilities.swift | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 Tests/CommitPrefixTests/TestUtilities.swift diff --git a/Tests/CommitPrefixTests/TestUtilities.swift b/Tests/CommitPrefixTests/TestUtilities.swift new file mode 100644 index 0000000..8e5b3f4 --- /dev/null +++ b/Tests/CommitPrefixTests/TestUtilities.swift @@ -0,0 +1,8 @@ +// +// File.swift +// +// +// Created by Stephen Martinez on 1/20/20. +// + +import Foundation From 2c0f7afbdfedc299c1a5804de2aefea88db146a6 Mon Sep 17 00:00:00 2001 From: Stephen Martinez Date: Mon, 20 Jan 2020 11:38:00 -0800 Subject: [PATCH 07/10] [CPFeature-010][Tests] Added test utilities file --- Tests/CommitPrefixTests/TestUtilities.swift | 46 +++++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/Tests/CommitPrefixTests/TestUtilities.swift b/Tests/CommitPrefixTests/TestUtilities.swift index 8e5b3f4..d3879c8 100644 --- a/Tests/CommitPrefixTests/TestUtilities.swift +++ b/Tests/CommitPrefixTests/TestUtilities.swift @@ -1,8 +1,48 @@ // -// File.swift -// +// TestUtilities.swift +// commitPrefix // -// Created by Stephen Martinez on 1/20/20. +// MIT License // +// Copyright (c) 2019 STEPHEN L. MARTINEZ +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import Foundation +import XCTest + +struct TestUtilities { + + static func urlOfExecutable(named executableName: String) -> URL { + productsDirectory.appendingPathComponent(executableName) + } + + /// Returns path to the built products directory. + private static var productsDirectory: URL { + #if os(macOS) + for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") { + return bundle.bundleURL.deletingLastPathComponent() + } + fatalError("Could not find the products directory") + #else + return Bundle.main.bundleURL + #endif + } + +} From aef16ce7e26fa872ca05a030075e21bae867185b Mon Sep 17 00:00:00 2001 From: Stephen Martinez Date: Mon, 20 Jan 2020 11:38:30 -0800 Subject: [PATCH 08/10] [CPFeature-010][Tests] Updated the sample test to more realistically test the current state of the application --- .../CommitPrefixTests/CommitPrefixTests.swift | 86 ++++++++----------- 1 file changed, 36 insertions(+), 50 deletions(-) diff --git a/Tests/CommitPrefixTests/CommitPrefixTests.swift b/Tests/CommitPrefixTests/CommitPrefixTests.swift index cbd44e4..edbbbaf 100644 --- a/Tests/CommitPrefixTests/CommitPrefixTests.swift +++ b/Tests/CommitPrefixTests/CommitPrefixTests.swift @@ -30,58 +30,44 @@ import XCTest final class commitPrefixTests: XCTestCase { - static var allTests = [ - ("testExample", testExample) - ] - - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. + func test_wholeApplication() throws { - // Some of the APIs that we use below are available in macOS 10.13 and above. -// guard #available(macOS 10.13, *) else { -// return -// } -// -// let fooBinary = productsDirectory.appendingPathComponent("commitPrefix") -// -// let process = Process() -// process.executableURL = fooBinary -// -// let pipe = Pipe() -// process.standardOutput = pipe -// -// let errorPipe = Pipe() -// process.standardError = errorPipe -// -// try process.run() -// process.waitUntilExit() -// -// let data = pipe.fileHandleForReading.readDataToEndOfFile() -// let output = String(data: data, encoding: .utf8) -// -// let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile() -// let errorOutput = String(data: errorData, encoding: .utf8) -// -// print("process output", output ?? "No output") -// print("process Error Output", errorOutput ?? "No output") -// -// XCTAssert(true, "Tests have not been implemented") - //XCTAssertEqual(output, "Hello, world!\n") - } + guard #available(macOS 10.15, *) else { return } + + let commitPrefixBinary = TestUtilities.urlOfExecutable(named: "commitPrefix") + + let process = Process() + process.executableURL = commitPrefixBinary + + let pipe = Pipe() + process.standardOutput = pipe + + let errorPipe = Pipe() + process.standardError = errorPipe + + try process.run() + process.waitUntilExit() - /// Returns path to the built products directory. -// var productsDirectory: URL { -// #if os(macOS) -// for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") { -// return bundle.bundleURL.deletingLastPathComponent() -// } -// fatalError("couldn't find the products directory") -// #else -// return Bundle.main.bundleURL -// #endif -// } + let data = pipe.fileHandleForReading.readDataToEndOfFile() + let output = String(data: data, encoding: .utf8) + let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile() + let errorOutput = String(data: errorData, encoding: .utf8) + let noOutput = output == nil || output == "" + let validStdErrOutput = (errorOutput ?? "") + .contains("Error: Not in a git repo or at the root of one") + + XCTAssert(noOutput, """ + commitPrefix should only return stderr output at this point. A full mock file \ + environment has not been set up yet. + """) + + XCTAssert(validStdErrOutput, "A CPError.notAGitRepo should be thrown here") + } + + static var allTests = [ + ("test_wholeApplication", test_wholeApplication) + ] + } From 8ad066a1590e628afd4eaeb5999248476d7202df Mon Sep 17 00:00:00 2001 From: Stephen Martinez Date: Mon, 20 Jan 2020 11:38:54 -0800 Subject: [PATCH 09/10] [CPFeature-010][Tests] Added the final CLInterface test --- .../CommitPrefixTests/CLInterfaceTests.swift | 92 ++++++++++++++++--- 1 file changed, 77 insertions(+), 15 deletions(-) diff --git a/Tests/CommitPrefixTests/CLInterfaceTests.swift b/Tests/CommitPrefixTests/CLInterfaceTests.swift index 0933a24..f0e48b5 100644 --- a/Tests/CommitPrefixTests/CLInterfaceTests.swift +++ b/Tests/CommitPrefixTests/CLInterfaceTests.swift @@ -69,7 +69,7 @@ final class CLInterfaceTests: XCTestCase { } // Special Command that uses no args except for the standard default OS argument - func test_viewStateCommand() { + func test_ViewStateCommand() { let args = ["executable/location"] let argument = CLIArguments(arguments: args) let expectedCommand = UserCommand.viewState @@ -87,6 +87,43 @@ final class CLInterfaceTests: XCTestCase { onSuccess: validateCommand) } + func test_BranchParseCommand() { + let userInputs = "eng" + let args = CLITestUtil.makeBranchParseArgument(userInput: [userInputs]) + let argument = CLIArguments(arguments: args) + let expectedCommand = UserCommand.modeBranchParse(validator: userInputs) + let failureMessage = "Expected the input of -b \(userInputs) to return to return " + + "an output of \(expectedCommand.description)" + + print("⚖️ ~ Testing -b \(userInputs) for command \(expectedCommand.description)") + func validateCommand(foundCommand: UserCommand) -> Void { + let isValid = UserCommand.isEquivelent(lhs: foundCommand, rhs: expectedCommand) + XCTAssert(isValid, "\(failureMessage) got \(foundCommand.description) instead") + let validatorsMatch = UserCommand.isValueEquivelent( + lhs: foundCommand, rhs: expectedCommand) + let foundValidator = foundCommand.associatedValue() ?? "Nil" + XCTAssert(validatorsMatch, """ + Expected found branch validator \(foundValidator) to be \(userInputs) + """) + } + + argument.getCommand().do( + onFailure: { XCTFail("Input: -b \(userInputs) \($0.description)") }, + onSuccess: validateCommand) + } + + func test_ExtraArgsBranchParseCommand() { + let userInputs = ["eng", "somethingExtra"] + let badInputValue = userInputs.joined(separator: " ") + let badArgs = CLITestUtil.makeBranchParseArgument(userInput: userInputs) + let badArgument = CLIArguments(arguments: badArgs) + let expectedCommand = UserCommand.modeBranchParse(validator: badInputValue) + print("⚖️ ~ Testing invalid -b \(badInputValue) for command \(expectedCommand.description)") + let badFailureMessage = "Expected the input of -b \(badInputValue) to fail " + + "in outputting \(expectedCommand.description)" + badArgument.getCommand().do { _ in XCTFail(badFailureMessage) } + } + static var allTests = [ ("test_SingleArgumentShortCommands", test_SingleArgumentShortCommands), ("test_SingleArgumentLongCommands", test_SingleArgumentLongCommands), @@ -94,7 +131,9 @@ final class CLInterfaceTests: XCTestCase { ("test_FailingSingleArgumentLongCommands", test_FailingSingleArgumentLongCommands), ("test_ExtraArgsSingleArgumentShortCommands", test_ExtraArgsSingleArgumentShortCommands), ("test_ExtraArgsSingleArgumentLongCommands", test_ExtraArgsSingleArgumentLongCommands), - ("test_viewStateCommand", test_viewStateCommand) + ("test_ViewStateCommand", test_ViewStateCommand), + ("test_BranchParseCommand", test_BranchParseCommand), + ("test_ExtraArgsBranchParseCommand", test_ExtraArgsBranchParseCommand) ] } @@ -137,6 +176,14 @@ struct CLITestUtil { } + static func makeBranchParseArgument( + useLong: Bool = false, + userInput: [String] + ) -> [String] { + let argList = ["executable/location", (useLong ? "--branchParse" : "-b")] + return argList + userInput + } + static func makeArgument( _ arg: Argument, useLong: Bool = false, @@ -236,19 +283,13 @@ extension UserCommand { static func isEquivelent(lhs: UserCommand, rhs: UserCommand) -> Bool { switch (lhs, rhs) { - case (.outputVersion, .outputVersion): - return true - case (.viewState, .viewState): - return true - case (.outputPrefixes, .outputPrefixes): - return true - case (.deletePrefixes, .deletePrefixes): - return true - case (.modeNormal, .modeNormal): - return true - case (.modeBranchParse, .modeBranchParse): - return true - case (.newPrefixes, .newPrefixes): + case (.outputVersion, .outputVersion), + (.viewState, .viewState), + (.outputPrefixes, .outputPrefixes), + (.deletePrefixes, .deletePrefixes), + (.modeNormal, .modeNormal), + (.modeBranchParse, .modeBranchParse), + (.newPrefixes, .newPrefixes): return true default: return false @@ -256,4 +297,25 @@ extension UserCommand { } + static func isValueEquivelent(lhs: UserCommand, rhs: UserCommand) -> Bool { + switch (lhs, rhs) { + case let (.modeBranchParse(lhsValue), .modeBranchParse(rhsValue)): + return lhsValue == rhsValue + case let (.newPrefixes(lhsValue), .newPrefixes(rhsValue)): + return lhsValue == rhsValue + default: + return false + } + } + + func associatedValue() -> String? { + switch self { + case let .modeBranchParse(validator: value): + return value + case let .newPrefixes(value: value): + return value + default: + return nil + } + } } From d19e06659df92f81d73e7fa1a191ad4de36df944 Mon Sep 17 00:00:00 2001 From: Stephen Martinez Date: Mon, 20 Jan 2020 11:39:12 -0800 Subject: [PATCH 10/10] [CPFeature-010][Tests] bumped the version patch number --- Sources/CommitPrefix/Constants.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CommitPrefix/Constants.swift b/Sources/CommitPrefix/Constants.swift index 6f0a4ad..6330182 100644 --- a/Sources/CommitPrefix/Constants.swift +++ b/Sources/CommitPrefix/Constants.swift @@ -28,7 +28,7 @@ import Foundation struct CPInfo { - static let version = "1.4.3" + static let version = "1.4.4" }