diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..5cfe361 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,30 @@ +name: "Test" + +on: + pull_request: + push: + branches: + - main + +jobs: + build: + runs-on: macos-latest + env: + GITHUB_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }} + steps: + - name: Chackout + uses: actions/checkout@v2 + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: | + .build/artifacts + .build/checkouts + .build/repositories + key: ${{ runner.os }}-dependencies-${{ matrix.xcode }}-${{ hashFiles('**/Package.resolved') }} + restore-keys: | + ${{ runner.os }}-dependencies-${{ matrix.xcode }}-${{ hashFiles('**/Package.resolved') }} + ${{ runner.os }}-dependencies-${{ matrix.xcode }}- + - name: Test + run: swift test + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bb460e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e6c874d --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2022 Yutaka Tajika + +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. diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..2eafa95 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,52 @@ +{ + "object": { + "pins": [ + { + "package": "Logger", + "repositoryURL": "https://github.com/shibapm/Logger", + "state": { + "branch": null, + "revision": "53c3ecca5abe8cf46697e33901ee774236d94cce", + "version": "0.2.3" + } + }, + { + "package": "OctoKit", + "repositoryURL": "https://github.com/nerdishbynature/octokit.swift", + "state": { + "branch": null, + "revision": "9521cdff919053868ab13cd08a228f7bc1bde2a9", + "version": "0.11.0" + } + }, + { + "package": "RequestKit", + "repositoryURL": "https://github.com/nerdishbynature/RequestKit.git", + "state": { + "branch": null, + "revision": "fd5e9e99aada7432170366c9e95967011ce13bad", + "version": "2.4.0" + } + }, + { + "package": "danger-swift", + "repositoryURL": "https://github.com/danger/swift.git", + "state": { + "branch": null, + "revision": "c1bba33f705ca0fd4a0022997c8b696210083755", + "version": "3.12.3" + } + }, + { + "package": "Version", + "repositoryURL": "https://github.com/mxcl/Version", + "state": { + "branch": null, + "revision": "200046c93f6d5d78a6d72bfd9c0b27a95e9c0a2b", + "version": "1.2.0" + } + } + ] + }, + "version": 1 +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..5654ece --- /dev/null +++ b/Package.swift @@ -0,0 +1,26 @@ +// swift-tools-version:5.5 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "DangerSwiftPeriphery", + products: [ + .library( + name: "DangerSwiftPeriphery", + targets: ["DangerSwiftPeriphery"]), + ], + dependencies: [ + .package(name: "danger-swift", url: "https://github.com/danger/swift.git", from: "3.0.0") + ], + targets: [ + .target( + name: "DangerSwiftPeriphery", + dependencies: [ + .product(name: "Danger", package: "danger-swift") + ]), + .testTarget( + name: "DangerSwiftPeripheryTests", + dependencies: ["DangerSwiftPeriphery"]), + ] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..959ae6e --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +# DangerSwiftPeriphery + +[Danger Swift](https://github.com/danger/swift) plugin to run [Periphery](https://github.com/peripheryapp/periphery) on CI. + + +![Test](https://github.com/taji-taji/DangerSwiftPeriphery/actions/workflows/test.yml/badge.svg) +[![MIT License](https://img.shields.io/apm/l/atomic-design-ui.svg?)](https://github.com/tterb/atomic-design-ui/blob/master/LICENSEs) + +## Features + +This plugin will comment unreferenced code detected by periphery via Danger Swift. + +![Image](Resources/Images/screenshot.png) + + +## Usage + +### Preparation + +- Install [Periphery](https://github.com/peripheryapp/periphery) on the machine you want to run Danger Swift on beforehand. +- [Danger Swift](https://github.com/danger/swift) Setup. + +### Package.swift + +```swift +let package = Package( + // ... + dependencies: [ + // Danger + .package(name: "danger-swift", url: "https://github.com/danger/swift.git", from: "3.0.0"), // dev + // Danger Plugins + // Add the line below. + .package(name: "DangerSwiftPeriphery", url: "https://github.com/taji-taji/DangerSwiftPeriphery.git", from: "1.0.0"), // dev + ], + targets: [ + // ... + // Add DangerSwiftPeriphery to dependencies in DangerDependencies. + .target(name: "DangerDependencies", + dependencies: [ + .product(name: "Danger", package: "danger-swift"), + "DangerSwiftPeriphery", + ]), + // ... + ] +) +``` + + +### Dangerfile.swift + +If you have a `.periphery.yml` file, simply include the following in `Dangerfile.swift` + +```swift +import Danger +import DangerSwiftPeriphery + +DangerPeriphery.scan() +``` + +Alternatively, periphery options can be passed as arguments. + +```swift +import Danger +import DangerSwiftPeriphery + +DangerPeriphery.scan(arguments: [ + "--workspace MaApp.xcworkspace", + "--schemes MyApp", + "--index-store-path /path/to/index/store", + "--skip-build" +]) +``` + +You may also specify the location of periphery binaries. + +```swift +import DangerSwiftPeriphery + +DangerPeriphery.scan(peripheryPath: "/path/to/periphery") +``` + + diff --git a/Resources/Images/screenshot.png b/Resources/Images/screenshot.png new file mode 100644 index 0000000..0c5294c Binary files /dev/null and b/Resources/Images/screenshot.png differ diff --git a/Sources/DangerSwiftPeriphery/CheckstyleOutputParser.swift b/Sources/DangerSwiftPeriphery/CheckstyleOutputParser.swift new file mode 100644 index 0000000..b6dcbc6 --- /dev/null +++ b/Sources/DangerSwiftPeriphery/CheckstyleOutputParser.swift @@ -0,0 +1,47 @@ +// +// CheckstyleOutputParser.swift +// +// +// Created by 多鹿豊 on 2022/04/03. +// + +import Foundation + +protocol CheckstyleOutputParsable { + func parse(xml: String, projectRootPath: String) throws -> [Violation] +} + +struct CheckstyleOutputParser: CheckstyleOutputParsable { + func parse(xml: String, projectRootPath: String) throws -> [Violation] { + let xmlDocument = try XMLDocument(xmlString: xml, options: []) + guard let rootElement = xmlDocument.rootElement(), + rootElement.name == "checkstyle" else { + throw Error.invalidCheckstyleXML + } + let fileElements = (try? rootElement.nodes(forXPath: "file") as? [XMLElement]) ?? [] + return fileElements.reduce([], { warnings, fileElement -> [Violation] in + guard let location = fileElement.attribute(forName: "name")?.stringValue else { return warnings } + let filePath = location.deletingPrefix(projectRootPath).deletingPrefix("/") + let errorElements = (try? fileElement.nodes(forXPath: "error") as? [XMLElement]) ?? [] + return warnings + errorElements.compactMap({ errorElement -> Violation? in + guard let lineString = errorElement.attribute(forName: "line")?.stringValue, + let line = Int(lineString), + let message = errorElement.attribute(forName: "message")?.stringValue else { return nil } + return Violation(filePath: filePath, line: line, message: message) + }) + }) + } +} + +extension CheckstyleOutputParser { + enum Error: Swift.Error { + case invalidCheckstyleXML + } +} + +private extension String { + func deletingPrefix(_ prefix: String) -> String { + guard hasPrefix(prefix) else { return self } + return String(dropFirst(prefix.count)) + } +} diff --git a/Sources/DangerSwiftPeriphery/CurrentPathProvider.swift b/Sources/DangerSwiftPeriphery/CurrentPathProvider.swift new file mode 100644 index 0000000..cede318 --- /dev/null +++ b/Sources/DangerSwiftPeriphery/CurrentPathProvider.swift @@ -0,0 +1,28 @@ +// +// CurrentPathProvider.swift +// +// Created by 多鹿豊 on 2022/04/05. +// + +import Foundation + +protocol CurrentPathProvider { + var currentPath: String { get } +} + +struct DefaultCurrentPathProvider: CurrentPathProvider { + private let shellExecutor: SE + var currentPath: String { + return try! shellExecutor.execute("pwd").get().trimmingCharacters(in: .newlines) + } + + init(shellExecutor: SE) { + self.shellExecutor = shellExecutor + } +} + +extension DefaultCurrentPathProvider where SE == ShellExecutor { + init() { + self.shellExecutor = .init() + } +} diff --git a/Sources/DangerSwiftPeriphery/DangerPeriphery.swift b/Sources/DangerSwiftPeriphery/DangerPeriphery.swift new file mode 100644 index 0000000..f579d61 --- /dev/null +++ b/Sources/DangerSwiftPeriphery/DangerPeriphery.swift @@ -0,0 +1,78 @@ +// +// DangerPeriphery.swift +// +// +// Created by 多鹿豊 on 2022/03/31. +// + +import Danger + +public struct DangerPeriphery { + public static func scan(peripheryPath: String = "periphery", + arguments: [String] = []) { + // make dependencies + let commandBuilder = PeripheryScanCommandBuilder(peripheryPath: peripheryPath, + additionalArguments: arguments) + let scanExecutor = PeripheryScanExecutor(commandBuilder: commandBuilder) + let diffProvider = PullRequestDiffProvider(dangerDSL: Danger()) + + // execute scan + let result = self.scan(scanExecutor: scanExecutor, + currentPathProvider: DefaultCurrentPathProvider(), + outputParser: CheckstyleOutputParser(), + diffProvider: diffProvider) + + // handle scan result + switch result { + case .success(let violations): + for violation in violations { + warn(message: violation.message, + file: violation.filePath, + line: violation.line) + } + case .failure(let error): + fail(error.localizedDescription) + } + } + + static func scan( + scanExecutor: PSE, + currentPathProvider: CPP, + outputParser: OP, + diffProvider: DP) -> Result<[Violation], Error> { + do { + let output = try scanExecutor.execute() + let allViolations = try outputParser.parse(xml: output, + projectRootPath: currentPathProvider.currentPath) + let violationsForComment = allViolations.filter({ violation -> Bool in + let result = diffProvider.diff(forFile: violation.filePath) + guard let changes = try? result.get() else { + return false + } + // comment only `Created files` and `Files that have been modified and are contained within hunk` + switch changes { + case .created: + return true + case .deleted: + return false + case let .modified(hunks): + return hunks.contains(where: { + let lineRange = ($0.newLineStart ..< $0.newLineSpan - $0.newLineStart) + if lineRange.contains(violation.line) { + return true + } + return false + }) + case .renamed: + return false + } + }) + return .success(violationsForComment) + } catch { + return .failure(error) + } + } +} diff --git a/Sources/DangerSwiftPeriphery/PeripheryScanCommandBuilder.swift b/Sources/DangerSwiftPeriphery/PeripheryScanCommandBuilder.swift new file mode 100644 index 0000000..d709d83 --- /dev/null +++ b/Sources/DangerSwiftPeriphery/PeripheryScanCommandBuilder.swift @@ -0,0 +1,39 @@ +// +// PeripheryScanCommandBuilder.swift +// +// +// Created by 多鹿豊 on 2022/04/09. +// + +import Foundation + +struct PeripheryScanCommandBuilder { + private let peripheryPath: String + private let additionalArguments: [String] + private let overrideArgumentKeys: [String] = [ + "--format", + "--quiet", + "--disable-update-check" + ] + + var command: String { + // override --format, --quiet, --disable-update-check + var overridedArguments: [String] = additionalArguments + overridedArguments.removeAll(where: { argument -> Bool in + overrideArgumentKeys.contains(where: { argument.hasPrefix($0) }) + }) + overridedArguments += [ + "--format checkstyle", + "--quiet", + "--disable-update-check" + ] + + return peripheryPath + " scan " + overridedArguments.joined(separator: " ") + } + + init(peripheryPath: String, + additionalArguments: [String]) { + self.peripheryPath = peripheryPath + self.additionalArguments = additionalArguments + } +} diff --git a/Sources/DangerSwiftPeriphery/PeripheryScanExecutor.swift b/Sources/DangerSwiftPeriphery/PeripheryScanExecutor.swift new file mode 100644 index 0000000..1e47271 --- /dev/null +++ b/Sources/DangerSwiftPeriphery/PeripheryScanExecutor.swift @@ -0,0 +1,48 @@ +// +// PeripheryScanExecutor.swift +// +// +// Created by 多鹿豊 on 2022/04/09. +// + +import Foundation + +protocol PeripheryScanExecutable { + func execute() throws -> String +} + +struct PeripheryScanExecutor: PeripheryScanExecutable { + private let commandBuilder: PeripheryScanCommandBuilder + private let shellExecutor: SE + private let warnPrefix = "warning: " + + init(commandBuilder: PeripheryScanCommandBuilder, + shellExecutor: SE) { + self.commandBuilder = commandBuilder + self.shellExecutor = shellExecutor + } + + func execute() throws -> String { + switch shellExecutor.execute(commandBuilder.command) { + case .success(let output): + var outputLines = output.split(whereSeparator: \.isNewline) + outputLines.removeAll(where: { $0.hasPrefix(self.warnPrefix) }) + return outputLines.joined(separator: "\n") + case .failure(let error): + throw error + } + } +} + +extension PeripheryScanExecutor where SE == ShellExecutor { + init(commandBuilder: PeripheryScanCommandBuilder) { + self.commandBuilder = commandBuilder + self.shellExecutor = .init() + } +} + +extension PeripheryScanExecutor { + struct Error: Swift.Error, CustomStringConvertible { + let description: String + } +} diff --git a/Sources/DangerSwiftPeriphery/PullRequestDiffProvider.swift b/Sources/DangerSwiftPeriphery/PullRequestDiffProvider.swift new file mode 100644 index 0000000..eb64322 --- /dev/null +++ b/Sources/DangerSwiftPeriphery/PullRequestDiffProvider.swift @@ -0,0 +1,43 @@ +// +// PullRequestDiffProvider.swift +// +// +// Created by 多鹿豊 on 2022/04/10. +// + +import Foundation +import Danger + +protocol PullRequestDiffProvidable { + func diff(forFile: String) -> Result +} + +struct PullRequestDiffProvider: PullRequestDiffProvidable { + private let dangerDSL: DangerDSL + + init(dangerDSL: DangerDSL) { + self.dangerDSL = dangerDSL + } + + func diff(forFile file: String) -> Result { + switch dangerDSL.utils.diff(forFile: file, sourceBranch: sourceBranch()) { + case .success(let diff): + return .success(diff.changes) + case .failure(let error): + return .failure(error) + } + } + + private func sourceBranch() -> String { + if let github = dangerDSL.github { + return "origin/\(github.pullRequest.base.ref)..HEAD" + } else if let gitLab = dangerDSL.gitLab { + return gitLab.mergeRequest.targetBranch + } else if let bitbucketCloud = dangerDSL.bitbucketCloud { + return bitbucketCloud.pr.destination.branchName + } else if let bitbucketServer = dangerDSL.bitbucketServer { + return bitbucketServer.pullRequest.fromRef.displayId + } + return "" + } +} diff --git a/Sources/DangerSwiftPeriphery/ShellExecutor.swift b/Sources/DangerSwiftPeriphery/ShellExecutor.swift new file mode 100644 index 0000000..9bb155a --- /dev/null +++ b/Sources/DangerSwiftPeriphery/ShellExecutor.swift @@ -0,0 +1,52 @@ +// +// ShellExecutor.swift +// +// +// Created by 多鹿豊 on 2022/03/31. +// + +import Foundation + +protocol ShellExecutable { + func execute(_ command: String) -> Result + func execute(_ command: String, arguments: [String]) -> Result +} + +struct CommandError: Error, CustomStringConvertible { + let status: Int32 + let description: String +} + +struct ShellExecutor: ShellExecutable { + func execute(_ command: String) -> Result { + execute(command, arguments: []) + } + + func execute(_ command: String, arguments: [String] = []) -> Result { + let script = "\(command) \(arguments.joined(separator: " "))" + print("Executing \(script)") + + let env = ProcessInfo.processInfo.environment + let task = Process() + task.launchPath = env["SHELL"] + task.arguments = ["-l", "-c", script] + task.currentDirectoryPath = FileManager.default.currentDirectoryPath + + let outputPipe = Pipe() + let errorPipe = Pipe() + task.standardOutput = outputPipe + task.standardError = errorPipe + task.launch() + task.waitUntilExit() + + let outputMessage = String(data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) + let errorMessage = String(data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) + + let status = task.terminationStatus + if status == 0 { + return .success(outputMessage!) + } else { + return .failure(.init(status: status, description: errorMessage!)) + } + } +} diff --git a/Sources/DangerSwiftPeriphery/Violation.swift b/Sources/DangerSwiftPeriphery/Violation.swift new file mode 100644 index 0000000..9d8fe76 --- /dev/null +++ b/Sources/DangerSwiftPeriphery/Violation.swift @@ -0,0 +1,13 @@ +// +// Violation.swift +// +// Created by 多鹿豊 on 2022/04/03. +// + +import Foundation + +struct Violation { + let filePath: String + let line: Int + let message: String +} diff --git a/Tests/DangerSwiftPeripheryTests/CheckstyleOutputParserTests.swift b/Tests/DangerSwiftPeripheryTests/CheckstyleOutputParserTests.swift new file mode 100644 index 0000000..ab6bdf4 --- /dev/null +++ b/Tests/DangerSwiftPeripheryTests/CheckstyleOutputParserTests.swift @@ -0,0 +1,80 @@ +// +// CheckstyleOutputParserTests.swift +// +// +// Created by 多鹿豊 on 2022/04/11. +// + +import XCTest +@testable import DangerSwiftPeriphery + +final class CheckstyleOutputParserTests: XCTestCase { + private var outputParser: CheckstyleOutputParser! + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testValidXML() throws { + let xmlString = """ + + + + + + + + + + + """ + + outputParser = CheckstyleOutputParser() + do { + let violations = try outputParser.parse(xml: xmlString, projectRootPath: "/path/to") + + XCTAssertEqual(violations.count, 3) + XCTAssertEqual(violations[0].filePath, "file1") + XCTAssertEqual(violations[0].line, 1) + XCTAssertEqual(violations[0].message, "test message 1") + XCTAssertEqual(violations[1].filePath, "file1") + XCTAssertEqual(violations[1].line, 2) + XCTAssertEqual(violations[1].message, "test message 2") + XCTAssertEqual(violations[2].filePath, "file2") + XCTAssertEqual(violations[2].line, 1) + XCTAssertEqual(violations[2].message, "test message 3") + } catch { + XCTFail("Unexpeced error: \(error)") + } + } + + func testNoCheckstyleXML() throws { + let xmlString = """ + + + + + + + + """ + + outputParser = CheckstyleOutputParser() + do { + _ = try outputParser.parse(xml: xmlString, projectRootPath: "/path/to") + + XCTFail("parse must fail") + } catch let error as CheckstyleOutputParser.Error { + switch error { + case .invalidCheckstyleXML: + break + } + } catch { + XCTFail("Unexpected error: \(error)") + } + } +} diff --git a/Tests/DangerSwiftPeripheryTests/DangerSwiftPeripheryTests.swift b/Tests/DangerSwiftPeripheryTests/DangerSwiftPeripheryTests.swift new file mode 100644 index 0000000..b57c538 --- /dev/null +++ b/Tests/DangerSwiftPeripheryTests/DangerSwiftPeripheryTests.swift @@ -0,0 +1,58 @@ +import XCTest +import Danger +@testable import DangerSwiftPeriphery + +final class DangerSwiftPeripheryTests: XCTestCase { + func testScanErrorOccuredWhileScanning() throws { + let scanExecutor = ErrorScanExecutor(errorMessage: "test error") + let result = DangerPeriphery.scan(scanExecutor: scanExecutor, + currentPathProvider: DefaultCurrentPathProvider(), + outputParser: CheckstyleOutputParser(), + diffProvider: DiffProviderMock(result: .failure(TestError.scanError(messege: "")))) + switch result { + case .success: + XCTFail("Unexpected success") + case .failure(let error as TestError): + switch error { + case .scanError(let message): + XCTAssertEqual(message, "test error") + } + default: + XCTFail("Unexpected result") + } + } + + func testScanErrorOccuredWhileParsingResult() throws { + + } +} + +private extension DangerSwiftPeripheryTests { + enum TestError: Error { + case scanError(messege: String) + } + + final class ErrorScanExecutor: PeripheryScanExecutable { + let errorMessage: String + + init(errorMessage: String) { + self.errorMessage = errorMessage + } + + func execute() throws -> String { + throw TestError.scanError(messege: errorMessage) + } + } + + final class DiffProviderMock: PullRequestDiffProvidable { + private let result: Result + + init(result: Result) { + self.result = result + } + + func diff(forFile: String) -> Result { + result + } + } +} diff --git a/Tests/DangerSwiftPeripheryTests/PeripheryScanCommandBuilderTests.swift b/Tests/DangerSwiftPeripheryTests/PeripheryScanCommandBuilderTests.swift new file mode 100644 index 0000000..35849bb --- /dev/null +++ b/Tests/DangerSwiftPeripheryTests/PeripheryScanCommandBuilderTests.swift @@ -0,0 +1,45 @@ +// +// PeripheryScanCommandBuilderTests.swift +// +// +// Created by 多鹿豊 on 2022/04/09. +// + +import XCTest +@testable import DangerSwiftPeriphery + +final class PeripheryScanCommandBuilderTests: XCTestCase { + private var scanCommandBuilder: PeripheryScanCommandBuilder! + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testPerihperyPath() throws { + scanCommandBuilder = PeripheryScanCommandBuilder(peripheryPath: "test/path/to/periphery", additionalArguments: []) + + XCTAssertEqual(scanCommandBuilder.command, "test/path/to/periphery scan --format checkstyle --quiet --disable-update-check") + } + + func testAdditionalArgumantsNoOverride() throws { + scanCommandBuilder = PeripheryScanCommandBuilder(peripheryPath: "periphery", additionalArguments: ["--test-arg1", "--test-arg2 value"]) + + XCTAssertEqual(scanCommandBuilder.command, "periphery scan --test-arg1 --test-arg2 value --format checkstyle --quiet --disable-update-check") + } + + func testAdditionalArgumantsOverride() throws { + scanCommandBuilder = PeripheryScanCommandBuilder(peripheryPath: "periphery", additionalArguments: ["--format json"]) + + XCTAssertEqual(scanCommandBuilder.command, "periphery scan --format checkstyle --quiet --disable-update-check") + } + + func testAdditionalArgumantsDuplicate() throws { + scanCommandBuilder = PeripheryScanCommandBuilder(peripheryPath: "periphery", additionalArguments: ["--quiet", "--disable-update-check"]) + + XCTAssertEqual(scanCommandBuilder.command, "periphery scan --format checkstyle --quiet --disable-update-check") + } +} diff --git a/Tests/DangerSwiftPeripheryTests/PeripheryScanExecutorTests.swift b/Tests/DangerSwiftPeripheryTests/PeripheryScanExecutorTests.swift new file mode 100644 index 0000000..c8a13f5 --- /dev/null +++ b/Tests/DangerSwiftPeripheryTests/PeripheryScanExecutorTests.swift @@ -0,0 +1,129 @@ +// +// PeripheryScanExecutorTests.swift +// +// +// Created by 多鹿豊 on 2022/04/09. +// + +import XCTest +@testable import DangerSwiftPeriphery + +final class PeripheryScanExecutorTests: XCTestCase { + private var executor: PeripheryScanExecutable! + private var commandBuilder: PeripheryScanCommandBuilder! + + override func setUpWithError() throws { + commandBuilder = PeripheryScanCommandBuilder(peripheryPath: "", additionalArguments: []) + } + + override func tearDownWithError() throws { + executor = nil + } + + func testExecuteThrowCommandError() throws { + executor = PeripheryScanExecutor(commandBuilder: commandBuilder, + shellExecutor: ErrorShellExecutor(status: 9999, description: "test error")) + + do { + _ = try executor.execute() + XCTFail("Must throw error.") + } catch { + if let error = error as? CommandError { + XCTAssertEqual(error.status, 9999) + XCTAssertEqual(error.description, "test error") + } else { + XCTFail("Unexpected error.") + } + } + } + + func testExecuteSucceed() throws { + let succeedShellExecutor = SucccedShellExecutor(output: """ + test + test2 + test3 + test4 + """) + + executor = PeripheryScanExecutor(commandBuilder: commandBuilder, + shellExecutor: succeedShellExecutor) + + do { + let output = try executor.execute() + let expected = """ + test + test2 + test3 + test4 + """ + + XCTAssertEqual(output, expected) + } catch { + XCTFail("Unexpected error: \(error)") + } + } + + func testExecuteSucceedWithWarning() throws { + let succeedShellExecutor = SucccedShellExecutor(output: """ + test + warning: hoge + test2 + warning: fuga + test3 + test4 + """) + + executor = PeripheryScanExecutor(commandBuilder: commandBuilder, + shellExecutor: succeedShellExecutor) + + do { + let output = try executor.execute() + let expected = """ + test + test2 + test3 + test4 + """ + + XCTAssertEqual(output, expected) + } catch { + XCTFail("Unexpected error: \(error)") + } + } +} + +private extension PeripheryScanExecutorTests { + final class ErrorShellExecutor: ShellExecutable { + private let status: Int32 + private let description: String + + init(status: Int32, description: String) { + self.status = status + self.description = description + } + + func execute(_ command: String) -> Result { + execute(command, arguments: []) + } + + func execute(_ command: String, arguments: [String]) -> Result { + .failure(.init(status: status, description: description)) + } + } + + final class SucccedShellExecutor: ShellExecutable { + private let output: String + + init(output: String) { + self.output = output + } + + func execute(_ command: String) -> Result { + execute(command, arguments: []) + } + + func execute(_ command: String, arguments: [String]) -> Result { + .success(output) + } + } +}