├── IntegrationTests ├── Sources │ ├── Spectre │ ├── Failing │ │ └── main.swift │ ├── Passing │ │ └── main.swift │ └── Disabled │ │ └── main.swift ├── Passing-expected-dot-output.txt ├── Passing-expected-tap-output.txt ├── Disabled-expected-dot-output.txt ├── Disabled-subset-mismatch-output.txt ├── Failing-subset-mismatch-output.txt ├── Passing-subset-mismatch-output.txt ├── Passing-expected-output.txt ├── Passing-subset-match-output.txt ├── Disabled-expected-tap-output.txt ├── Failing-expected-tap-output.txt ├── Disabled-expected-output.txt ├── Disabled-subset-match-output.txt ├── Failing-expected-dot-output.txt ├── Failing-expected-output.txt ├── Failing-subset-match-output.txt ├── Package.swift ├── Package@swift-5.0.swift └── run.sh ├── Spectre.playground ├── Sources │ ├── Case.swift │ ├── Context.swift │ ├── Failure.swift │ ├── Reporter.swift │ ├── Expectation.swift │ ├── Reporters.swift │ ├── GlobalContext.swift │ └── Playground.swift ├── playground.xcworkspace │ └── contents.xcworkspacedata ├── contents.xcplayground └── Contents.swift ├── Screenshots ├── failure.png ├── success.png ├── Playground.png ├── failure-dot.png └── success-dot.png ├── Tests ├── LinuxMain.swift └── SpectreTests │ ├── XCTest.swift │ ├── FailureSpec.swift │ └── ExpectationSpec.swift ├── .github └── workflows │ ├── main.yaml │ ├── xcode.yaml │ └── windows.yml ├── Package.swift ├── Package@swift-5.0.swift ├── Package@swift-4.2.swift ├── .gitignore ├── .travis.yml ├── Sources └── Spectre │ ├── Reporter.swift │ ├── GlobalContext.swift │ ├── Failure.swift │ ├── Case.swift │ ├── XCTest.swift │ ├── Context.swift │ ├── Global.swift │ ├── Path.swift │ ├── Expectation.swift │ └── Reporters.swift ├── Spectre.podspec.json ├── LICENSE ├── CHANGELOG.md └── README.md /IntegrationTests/Sources/Spectre: -------------------------------------------------------------------------------- 1 | ../../Sources/ -------------------------------------------------------------------------------- /Spectre.playground/Sources/Case.swift: -------------------------------------------------------------------------------- 1 | ../../Sources/Spectre/Case.swift -------------------------------------------------------------------------------- /Spectre.playground/Sources/Context.swift: -------------------------------------------------------------------------------- 1 | ../../Sources/Spectre/Context.swift -------------------------------------------------------------------------------- /Spectre.playground/Sources/Failure.swift: -------------------------------------------------------------------------------- 1 | ../../Sources/Spectre/Failure.swift -------------------------------------------------------------------------------- /Spectre.playground/Sources/Reporter.swift: -------------------------------------------------------------------------------- 1 | ../../Sources/Spectre/Reporter.swift -------------------------------------------------------------------------------- /Spectre.playground/Sources/Expectation.swift: -------------------------------------------------------------------------------- 1 | ../../Sources/Spectre/Expectation.swift -------------------------------------------------------------------------------- /Spectre.playground/Sources/Reporters.swift: -------------------------------------------------------------------------------- 1 | ../../Sources/Spectre/Reporters.swift -------------------------------------------------------------------------------- /IntegrationTests/Passing-expected-dot-output.txt: -------------------------------------------------------------------------------- 1 | . 2 | 3 | 1 passes and 0 failures 4 | -------------------------------------------------------------------------------- /Spectre.playground/Sources/GlobalContext.swift: -------------------------------------------------------------------------------- 1 | ../../Sources/Spectre/GlobalContext.swift -------------------------------------------------------------------------------- /IntegrationTests/Passing-expected-tap-output.txt: -------------------------------------------------------------------------------- 1 | ok 1 - a person named kyle is Kyle 2 | 1..1 3 | -------------------------------------------------------------------------------- /Screenshots/failure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylef/Spectre/HEAD/Screenshots/failure.png -------------------------------------------------------------------------------- /Screenshots/success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylef/Spectre/HEAD/Screenshots/success.png -------------------------------------------------------------------------------- /IntegrationTests/Disabled-expected-dot-output.txt: -------------------------------------------------------------------------------- 1 | .S 2 | 3 | 1 passes 1 skipped, and 0 failures 4 | -------------------------------------------------------------------------------- /Screenshots/Playground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylef/Spectre/HEAD/Screenshots/Playground.png -------------------------------------------------------------------------------- /Screenshots/failure-dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylef/Spectre/HEAD/Screenshots/failure-dot.png -------------------------------------------------------------------------------- /Screenshots/success-dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylef/Spectre/HEAD/Screenshots/success-dot.png -------------------------------------------------------------------------------- /IntegrationTests/Disabled-subset-mismatch-output.txt: -------------------------------------------------------------------------------- 1 | -> a person 2 | 0 passes 1 skipped, and 0 failures 3 | -------------------------------------------------------------------------------- /IntegrationTests/Failing-subset-mismatch-output.txt: -------------------------------------------------------------------------------- 1 | -> a person 2 | 0 passes 1 skipped, and 0 failures 3 | -------------------------------------------------------------------------------- /IntegrationTests/Passing-subset-mismatch-output.txt: -------------------------------------------------------------------------------- 1 | -> a person 2 | 0 passes 1 skipped, and 0 failures 3 | -------------------------------------------------------------------------------- /IntegrationTests/Passing-expected-output.txt: -------------------------------------------------------------------------------- 1 | -> a person 2 | -> named kyle 3 | -> is Kyle 4 | 5 | 6 | 1 passes and 0 failures 7 | -------------------------------------------------------------------------------- /IntegrationTests/Passing-subset-match-output.txt: -------------------------------------------------------------------------------- 1 | -> a person 2 | -> named kyle 3 | -> is Kyle 4 | 5 | 6 | 1 passes and 0 failures 7 | -------------------------------------------------------------------------------- /IntegrationTests/Disabled-expected-tap-output.txt: -------------------------------------------------------------------------------- 1 | ok 1 - a person named kyle is Kyle 2 | ok 2 - # skip a person named kyle has a description 3 | 1..2 4 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import Spectre 2 | import SpectreTests 3 | 4 | describe("Expectation", testExpectation) 5 | describe("Failure", testFailure) 6 | -------------------------------------------------------------------------------- /IntegrationTests/Failing-expected-tap-output.txt: -------------------------------------------------------------------------------- 1 | not ok 1 - a person named kyle is Kyle 2 | # Katie is not equal to Kyle from Sources/Failing/main.swift:10 3 | 1..1 4 | -------------------------------------------------------------------------------- /IntegrationTests/Disabled-expected-output.txt: -------------------------------------------------------------------------------- 1 | -> a person 2 | -> named kyle 3 | -> is Kyle 4 | -> has a description 5 | 6 | 7 | 1 passes 1 skipped, and 0 failures 8 | -------------------------------------------------------------------------------- /IntegrationTests/Disabled-subset-match-output.txt: -------------------------------------------------------------------------------- 1 | -> a person 2 | -> named kyle 3 | -> is Kyle 4 | -> has a description 5 | 6 | 7 | 1 passes 1 skipped, and 0 failures 8 | -------------------------------------------------------------------------------- /Spectre.playground/playground.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Spectre.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /IntegrationTests/Failing-expected-dot-output.txt: -------------------------------------------------------------------------------- 1 | F 2 | 3 | a person named kyle is Kyle 4 | Sources/Failing/main.swift:10 Katie is not equal to Kyle 5 | 6 | ``` 7 | try expect(person) == "Kyle" 8 | ``` 9 | 0 passes and 1 failure 10 | -------------------------------------------------------------------------------- /Tests/SpectreTests/XCTest.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Spectre 3 | 4 | class SpectreTests: XCTestCase { 5 | func testSpectre() { 6 | describe("Failure", testFailure) 7 | describe("Expectation", testExpectation) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | jobs: 3 | test: 4 | runs-on: ${{ matrix.os }} 5 | strategy: 6 | matrix: 7 | os: [ubuntu-latest, macos-latest] 8 | steps: 9 | - uses: actions/checkout@v2 10 | - run: swift test 11 | -------------------------------------------------------------------------------- /IntegrationTests/Sources/Failing/main.swift: -------------------------------------------------------------------------------- 1 | import Spectre 2 | 3 | describe("a person") { 4 | var person:String! 5 | 6 | $0.context("named kyle") { 7 | person = "Katie" 8 | 9 | $0.it("is Kyle") { 10 | try expect(person) == "Kyle" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /IntegrationTests/Sources/Passing/main.swift: -------------------------------------------------------------------------------- 1 | import Spectre 2 | 3 | describe("a person") { 4 | var person:String! 5 | 6 | $0.context("named kyle") { 7 | person = "Kyle" 8 | 9 | $0.it("is Kyle") { 10 | try expect(person) == "Kyle" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /IntegrationTests/Failing-expected-output.txt: -------------------------------------------------------------------------------- 1 | -> a person 2 | -> named kyle 3 | -> is Kyle 4 | 5 | 6 | a person named kyle is Kyle 7 | Sources/Failing/main.swift:10 Katie is not equal to Kyle 8 | 9 | ``` 10 | try expect(person) == "Kyle" 11 | ``` 12 | 0 passes and 1 failure 13 | -------------------------------------------------------------------------------- /IntegrationTests/Failing-subset-match-output.txt: -------------------------------------------------------------------------------- 1 | -> a person 2 | -> named kyle 3 | -> is Kyle 4 | 5 | 6 | a person named kyle is Kyle 7 | Sources/Failing/main.swift:10 Katie is not equal to Kyle 8 | 9 | ``` 10 | try expect(person) == "Kyle" 11 | ``` 12 | 0 passes and 1 failure 13 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | 3 | import PackageDescription 4 | 5 | 6 | let package = Package( 7 | name: "Spectre", 8 | products: [ 9 | .library(name: "Spectre", targets: ["Spectre"]), 10 | ], 11 | targets: [ 12 | .target(name: "Spectre"), 13 | .testTarget(name: "SpectreTests", dependencies: ["Spectre"]), 14 | ] 15 | ) 16 | -------------------------------------------------------------------------------- /IntegrationTests/Sources/Disabled/main.swift: -------------------------------------------------------------------------------- 1 | import Spectre 2 | 3 | describe("a person") { 4 | var person:String! 5 | 6 | $0.context("named kyle") { 7 | person = "Kyle" 8 | 9 | $0.it("is Kyle") { 10 | try expect(person) == "Kyle" 11 | } 12 | 13 | $0.xit("has a description") { 14 | try expect(person) != "Kyle" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Package@swift-5.0.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | 3 | import PackageDescription 4 | 5 | 6 | let package = Package( 7 | name: "Spectre", 8 | products: [ 9 | .library(name: "Spectre", targets: ["Spectre"]), 10 | ], 11 | targets: [ 12 | .target(name: "Spectre"), 13 | .testTarget(name: "SpectreTests", dependencies: ["Spectre"]), 14 | ], 15 | swiftLanguageVersions: [.v5] 16 | ) 17 | -------------------------------------------------------------------------------- /Package@swift-4.2.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.2 2 | 3 | import PackageDescription 4 | 5 | 6 | let package = Package( 7 | name: "Spectre", 8 | products: [ 9 | .library(name: "Spectre", targets: ["Spectre"]), 10 | ], 11 | targets: [ 12 | .target(name: "Spectre"), 13 | .testTarget(name: "SpectreTests", dependencies: ["Spectre"]), 14 | ], 15 | swiftLanguageVersions: [.v4, .v4_2] 16 | ) 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .build/ 2 | Packages/ 3 | *.xcodeproj 4 | IntegrationTests/Failing-dot-output.txt 5 | IntegrationTests/Failing-output.txt 6 | IntegrationTests/Failing-tap-output.txt 7 | IntegrationTests/Passing-dot-output.txt 8 | IntegrationTests/Passing-output.txt 9 | IntegrationTests/Passing-tap-output.txt 10 | IntegrationTests/Disabled-dot-output.txt 11 | IntegrationTests/Disabled-output.txt 12 | IntegrationTests/Disabled-tap-output.txt 13 | -------------------------------------------------------------------------------- /Tests/SpectreTests/FailureSpec.swift: -------------------------------------------------------------------------------- 1 | import Spectre 2 | 3 | public let testFailure: ((ContextType) -> Void) = { 4 | $0.it("throws an error") { 5 | 6 | var didFail = false 7 | 8 | do { 9 | throw failure("it's broken") 10 | } catch { 11 | didFail = true 12 | } 13 | 14 | if !didFail { 15 | // We cannot trust fail inside fails tests. 16 | fatalError("Test failed") 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: generic 2 | sudo: required 3 | jobs: 4 | include: 5 | - env: SWIFT_VERSION=4.2.4 6 | dist: bionic 7 | - env: SWIFT_VERSION=5.0.3 8 | dist: bionic 9 | - env: SWIFT_VERSION=5.1.5 10 | dist: bionic 11 | - env: SWIFT_VERSION=5.2.5 12 | dist: bionic 13 | install: 14 | - eval "$(curl -sL https://swiftenv.fuller.li/install.sh)" 15 | script: 16 | - swift test 17 | - cd IntegrationTests && swift build && ./run.sh Failing Passing Disabled 18 | -------------------------------------------------------------------------------- /Spectre.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | struct Person : CustomStringConvertible { 2 | let name: String 3 | 4 | init(name: String) { 5 | self.name = name 6 | } 7 | 8 | var description: String { 9 | return name 10 | } 11 | } 12 | 13 | describe("a person") { 14 | let person = Person(name: "Kyle") 15 | 16 | $0.it("has a name") { 17 | try expect(person.name) == "Katie" 18 | } 19 | 20 | $0.it("returns the name as description") { 21 | try expect(person.description) == "Kyle" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Spectre/Reporter.swift: -------------------------------------------------------------------------------- 1 | public protocol Reporter { 2 | /// Create a new report 3 | func report(closure: (ContextReporter) -> Void) -> Bool 4 | } 5 | 6 | public protocol ContextReporter { 7 | func report(_ name: String, closure: (ContextReporter) -> Void) 8 | 9 | /// Add a passing test case 10 | func addSuccess(_ name: String) 11 | 12 | /// Add a disabled test case 13 | func addDisabled(_ name: String) 14 | 15 | /// Adds a failing test case 16 | func addFailure(_ name: String, failure: FailureType) 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Spectre/GlobalContext.swift: -------------------------------------------------------------------------------- 1 | class GlobalContext { 2 | var cases = [CaseType]() 3 | 4 | func describe(_ name: String, closure: (ContextType) -> Void) { 5 | let context = Context(name: name) 6 | closure(context) 7 | cases.append(context) 8 | } 9 | 10 | func it(_ name: String, closure: @escaping () throws -> Void) { 11 | cases.append(Case(name: name, closure: closure)) 12 | } 13 | 14 | func run(reporter: Reporter) -> Bool { 15 | return reporter.report { reporter in 16 | for `case` in cases { 17 | `case`.run(reporter: reporter) 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /IntegrationTests/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | 3 | import PackageDescription 4 | 5 | 6 | let package = Package( 7 | name: "SpectreIntegration", 8 | products: [ 9 | .executable(name: "Passing", targets: ["Passing"]), 10 | .executable(name: "Disabled", targets: ["Disabled"]), 11 | .executable(name: "Failing", targets: ["Failing"]), 12 | ], 13 | targets: [ 14 | .target(name: "Spectre"), 15 | .target(name: "Passing", dependencies: ["Spectre"]), 16 | .target(name: "Disabled", dependencies: ["Spectre"]), 17 | .target(name: "Failing", dependencies: ["Spectre"]), 18 | ] 19 | ) 20 | -------------------------------------------------------------------------------- /IntegrationTests/Package@swift-5.0.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | import PackageDescription 3 | 4 | 5 | let package = Package( 6 | name: "SpectreIntegration", 7 | products: [ 8 | .executable(name: "Passing", targets: ["Passing"]), 9 | .executable(name: "Disabled", targets: ["Disabled"]), 10 | .executable(name: "Failing", targets: ["Failing"]), 11 | ], 12 | targets: [ 13 | .target(name: "Spectre"), 14 | .target(name: "Passing", dependencies: ["Spectre"]), 15 | .target(name: "Disabled", dependencies: ["Spectre"]), 16 | .target(name: "Failing", dependencies: ["Spectre"]), 17 | ], 18 | swiftLanguageVersions: [.v5] 19 | ) 20 | -------------------------------------------------------------------------------- /.github/workflows/xcode.yaml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | jobs: 3 | xcodetest: 4 | runs-on: macos-latest 5 | env: 6 | DEVELOPER_DIR: '/Applications/Xcode_${{ matrix.xcode }}.app' 7 | strategy: 8 | matrix: 9 | # xcode 12.5 is not available yet, as it runs on macos-11 and macos 11 is in a private pool 10 | # https://github.com/actions/virtual-environments/issues/2486 11 | xcode: ['10.3', '11.3.1', '11.6', '12_beta', '12.4'] 12 | steps: 13 | - uses: actions/checkout@v2 14 | - run: rm -fr IntegrationTests # swift package will generate wrong package 15 | - run: swift package generate-xcodeproj 16 | - run: xcodebuild test -scheme Spectre-Package 17 | - run: xcodebuild test -scheme Spectre-Package -sdk iphonesimulator -destination "name=iPhone 8" 18 | -------------------------------------------------------------------------------- /Sources/Spectre/Failure.swift: -------------------------------------------------------------------------------- 1 | public protocol FailureType : Error { 2 | var function: String { get } 3 | var file: String { get } 4 | var line: Int { get } 5 | 6 | var reason: String { get } 7 | } 8 | 9 | struct Failure : FailureType { 10 | let reason: String 11 | 12 | let function: String 13 | let file: String 14 | let line: Int 15 | 16 | init(reason: String, function: String = #function, file: String = #file, line: Int = #line) { 17 | self.reason = reason 18 | self.function = function 19 | self.file = file 20 | self.line = line 21 | } 22 | } 23 | 24 | struct Skip: Error { 25 | let reason: String? 26 | } 27 | 28 | public func skip(_ reason: String? = nil) -> Error { 29 | return Skip(reason: reason) 30 | } 31 | 32 | 33 | public func failure(_ reason: String? = nil, function: String = #function, file: String = #file, line: Int = #line) -> FailureType { 34 | return Failure(reason: reason ?? "-", function: function, file: file, line: line) 35 | } 36 | -------------------------------------------------------------------------------- /Spectre.podspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Spectre", 3 | "version": "0.10.1", 4 | "summary": "BDD Framework and test runner for Swift projects and playgrounds", 5 | "description": "A behavior-driven development (BDD) framework and test runner for Swift projects and playgrounds. Foundation-free and Linux-ready!", 6 | "homepage": "https://github.com/kylef/Spectre", 7 | "source": { 8 | "git": "https://github.com/kylef/Spectre.git", 9 | "tag": "0.10.1" 10 | }, 11 | "authors": { 12 | "Kyle Fuller": "kyle@fuller.li" 13 | }, 14 | "license": { 15 | "type": "BSD", 16 | "file": "LICENSE" 17 | }, 18 | "source_files": [ 19 | "Sources/Spectre/Case.swift", 20 | "Sources/Spectre/Context.swift", 21 | "Sources/Spectre/Expectation.swift", 22 | "Sources/Spectre/Failure.swift", 23 | "Sources/Spectre/Global.swift", 24 | "Sources/Spectre/GlobalContext.swift", 25 | "Sources/Spectre/Path.swift", 26 | "Sources/Spectre/Reporter.swift", 27 | "Sources/Spectre/Reporters.swift", 28 | "Sources/Spectre/XCTest.swift" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /Sources/Spectre/Case.swift: -------------------------------------------------------------------------------- 1 | protocol CaseType { 2 | /// Run a test case in the given reporter 3 | func run(reporter: ContextReporter) 4 | } 5 | 6 | class Case : CaseType { 7 | let name:String 8 | var disabled: Bool 9 | let closure:() throws -> () 10 | 11 | let function: String 12 | let file: String 13 | let line: Int 14 | 15 | init(name: String, disabled: Bool = false, closure: @escaping () throws -> (), function: String = #function, file: String = #file, line: Int = #line) { 16 | self.name = name 17 | self.disabled = disabled 18 | self.closure = closure 19 | 20 | self.function = function 21 | self.file = file 22 | self.line = line 23 | } 24 | 25 | func run(reporter: ContextReporter) { 26 | if disabled { 27 | reporter.addDisabled(name) 28 | return 29 | } 30 | 31 | do { 32 | try closure() 33 | reporter.addSuccess(name) 34 | } catch _ as Skip { 35 | reporter.addDisabled(name) 36 | } catch let error as FailureType { 37 | reporter.addFailure(name, failure: error) 38 | } catch { 39 | reporter.addFailure(name, failure: Failure(reason: "Unhandled error: \(error)", function: function, file: file, line: line)) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Kyle Fuller 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /Spectre.playground/Sources/Playground.swift: -------------------------------------------------------------------------------- 1 | class PlaygroundReporter : ContextReporter { 2 | var depth = 0 3 | 4 | #if swift(>=3.0) 5 | func print(_ message: String) { 6 | let indentation = String(repeating: " ", count: depth * 2) 7 | Swift.print("\(indentation)\(message)") 8 | } 9 | #else 10 | func print(message: String) { 11 | let indentation = String(count: depth * 2, repeatedValue: " " as Character) 12 | Swift.print("\(indentation)\(message)") 13 | } 14 | #endif 15 | 16 | func report(_ name: String, closure: (ContextReporter) -> Void) { 17 | print("\(name):") 18 | 19 | depth += 1 20 | closure(self) 21 | depth -= 1 22 | 23 | print("") 24 | } 25 | 26 | func addSuccess(_ name: String) { 27 | print("✓ \(name)") 28 | } 29 | 30 | func addDisabled(_ name: String) { 31 | print("✱ \(name)") 32 | } 33 | 34 | func addFailure(_ name: String, failure: FailureType) { 35 | print("✗ \(name)") 36 | } 37 | } 38 | 39 | let reporter = PlaygroundReporter() 40 | 41 | #if swift(>=3.0) 42 | public func describe(_ name: String, closure: (ContextType) -> ()) { 43 | let context = Context(name: name) 44 | closure(context) 45 | context.run(reporter: reporter) 46 | } 47 | #else 48 | public func describe(name: String, closure: ContextType -> ()) { 49 | let context = Context(name: name) 50 | closure(context) 51 | context.run(reporter) 52 | } 53 | #endif 54 | 55 | public func it(name: String, closure: @escaping () throws -> ()) { 56 | let testCase = Case(name: name, closure: closure) 57 | testCase.run(reporter: reporter) 58 | } 59 | 60 | -------------------------------------------------------------------------------- /.github/workflows/windows.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | jobs: 3 | windowstest: 4 | runs-on: windows-latest 5 | steps: 6 | - uses: actions/checkout@v2 7 | - uses: seanmiddleditch/gha-setup-vsdevenv@master 8 | - name: Install swift-5.4 9 | run: | 10 | Install-Binary -Url "https://swift.org/builds/swift-5.4-release/windows10/swift-5.4-RELEASE/swift-5.4-RELEASE-windows10.exe" -Name "installer.exe" -ArgumentList ("-q") 11 | - name: Set Environment Variables 12 | run: | 13 | echo "SDKROOT=C:\Library\Developer\Platforms\Windows.platform\Developer\SDKs\Windows.sdk" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append 14 | echo "DEVELOPER_DIR=C:\Library\Developer" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append 15 | - name: Adjust Paths 16 | run: | 17 | echo "C:\Library\Swift-development\bin;C:\Library\icu-67\usr\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append 18 | echo "C:\Library\Developer\Toolchains\unknown-Asserts-development.xctoolchain\usr\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append 19 | - name: Install Supporting Files 20 | run: | 21 | Copy-Item "$env:SDKROOT\usr\share\ucrt.modulemap" -destination "$env:UniversalCRTSdkDir\Include\$env:UCRTVersion\ucrt\module.modulemap" 22 | Copy-Item "$env:SDKROOT\usr\share\visualc.modulemap" -destination "$env:VCToolsInstallDir\include\module.modulemap" 23 | Copy-Item "$env:SDKROOT\usr\share\visualc.apinotes" -destination "$env:VCToolsInstallDir\include\visualc.apinotes" 24 | Copy-Item "$env:SDKROOT\usr\share\winsdk.modulemap" -destination "$env:UniversalCRTSdkDir\Include\$env:UCRTVersion\um\module.modulemap" 25 | - name: Test 26 | run: swift test -------------------------------------------------------------------------------- /IntegrationTests/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | EXIT_CODE=0 4 | 5 | for var in "$@"; do 6 | echo "-> '$var'" 7 | rm $var-output.txt $var-dot-output.txt $var-tap-output.txt 2>/dev/null 8 | 9 | ./.build/debug/$var > $var-output.txt 10 | if [ $? != 0 ]; then 11 | if [ $var != "Failing" ]; then 12 | echo " - Integration Failed" 13 | EXIT_CODE=1 14 | fi 15 | elif [ $var = "Failing" ]; then 16 | echo " - Failing Integration Succeeded" 17 | EXIT_CODE=1 18 | fi 19 | 20 | ./.build/debug/$var -t > $var-dot-output.txt 21 | if [ $? != 0 ]; then 22 | if [ $var != "Failing" ]; then 23 | echo " - Dot Integration Failed" 24 | EXIT_CODE=1 25 | fi 26 | elif [ $var = "Failing" ]; then 27 | echo " - Failing Dot Integration Succeeded" 28 | EXIT_CODE=1 29 | fi 30 | 31 | ./.build/debug/$var --tap > $var-tap-output.txt 32 | if [ $? != 0 ]; then 33 | if [ $var != "Failing" ]; then 34 | echo " - Tap Integration Failed" 35 | EXIT_CODE=1 36 | fi 37 | elif [ $var = "Failing" ]; then 38 | echo " - Failing Tap Integration Succeeded" 39 | EXIT_CODE=1 40 | fi 41 | 42 | diff $var-output.txt $var-expected-output.txt 43 | if [ $? != 0 ]; then 44 | echo "Output Mismatch" 45 | EXIT_CODE=1 46 | fi 47 | diff $var-dot-output.txt $var-expected-dot-output.txt 48 | if [ $? != 0 ]; then 49 | echo "Dot Output Mismatch" 50 | EXIT_CODE=1 51 | fi 52 | diff $var-tap-output.txt $var-expected-tap-output.txt 53 | if [ $? != 0 ]; then 54 | echo "Tap Output Mismatch" 55 | EXIT_CODE=1 56 | fi 57 | 58 | # Subset of tests (match) 59 | ./.build/debug/$var Sources/*/main.swift > $var-subset-match-output.txt 60 | if [ $? != 0 ]; then 61 | if [ $var != "Failing" ]; then 62 | echo " - Integration Failed" 63 | EXIT_CODE=1 64 | fi 65 | elif [ $var = "Failing" ]; then 66 | echo " - Failing Integration Succeeded" 67 | EXIT_CODE=1 68 | fi 69 | 70 | # Subset of tests (mismatch) 71 | ./.build/debug/$var Sources/unknown.swift > $var-subset-mismatch-output.txt 72 | if [ $? != 0 ]; then 73 | EXIT_CODE=1 74 | fi 75 | done 76 | 77 | exit $EXIT_CODE 78 | 79 | -------------------------------------------------------------------------------- /Sources/Spectre/XCTest.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | 4 | extension XCTestCase { 5 | public func describe(_ name: String, _ closure: (ContextType) -> Void) { 6 | let context = Context(name: name) 7 | closure(context) 8 | context.run(reporter: XcodeReporter(testCase: self)) 9 | } 10 | 11 | public func it(_ name: String, closure: @escaping () throws -> Void) { 12 | let `case` = Case(name: name, closure: closure) 13 | `case`.run(reporter: XcodeReporter(testCase: self)) 14 | } 15 | } 16 | 17 | 18 | class XcodeReporter: ContextReporter { 19 | let testCase: XCTestCase 20 | 21 | init(testCase: XCTestCase) { 22 | self.testCase = testCase 23 | } 24 | 25 | func report(_ name: String, closure: (ContextReporter) -> Void) { 26 | closure(self) 27 | } 28 | 29 | func addSuccess(_ name: String) {} 30 | 31 | func addDisabled(_ name: String) {} 32 | 33 | func addFailure(_ name: String, failure: FailureType) { 34 | // Xcode 12 removed `recordFailure` and replaced with `record(_:)` 35 | #if swift(>=4.2) 36 | // The `compiler` statement was added in swift 4.2, so it needs to be in a separate statement to retain 37 | // compatibility with 4.x. 38 | #if compiler(>=5.3) && os(macOS) 39 | let location = XCTSourceCodeLocation(filePath: failure.file, lineNumber: failure.line) 40 | #if Xcode 41 | // As of Xcode 12.0.1, XCTIssue is unavailable even though it is documented: 42 | // https://developer.apple.com/documentation/xctest/xctissue 43 | // When building with `swift build`, it is available. Perhaps the xctest overlay behaves differently between the two. 44 | let issue = XCTIssueReference(type: .assertionFailure, compactDescription: "\(name): \(failure.reason)", detailedDescription: nil, sourceCodeContext: .init(location: location), associatedError: nil, attachments: []) 45 | #else 46 | let issue = XCTIssue(type: .assertionFailure, compactDescription: "\(name): \(failure.reason)", detailedDescription: nil, sourceCodeContext: .init(location: location), associatedError: nil, attachments: []) 47 | #endif 48 | #if compiler(>=5.4) 49 | testCase.record(issue as XCTIssue) 50 | #else 51 | testCase.record(issue) 52 | #endif 53 | #else 54 | testCase.recordFailure(withDescription: "\(name): \(failure.reason)", inFile: failure.file, atLine: failure.line, expected: false) 55 | #endif 56 | #endif 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Spectre Changelog 2 | 3 | ## 0.10.1 (2021-08-31) 4 | 5 | ### Bug fixes 6 | 7 | - Prevents compilation error when building Spectre on Xcode 13. 8 | 9 | 10 | ## 0.10.0 (2021-05-15) 11 | 12 | ### Breaking Changes 13 | 14 | - Support for Swift < 4.2 has been dropped. 15 | 16 | ### Enhancements 17 | 18 | - Reporter type can be set via an environment variable. For example, to use dot 19 | reporter: 20 | 21 | ```shell 22 | $ env SPECTRE_REPORTER=dot swift test 23 | ``` 24 | 25 | - Additional arguments and options can be passed to Spectre using the 26 | `SPECTRE_ADDOPTS` environment variable, for example: 27 | 28 | ```shell 29 | $ SPECTRE_ADDOPTS=Tests/SpectreTests/FailureSpec.swift swift test 30 | ``` 31 | 32 | - Spectre can be passed a set of files to filter which tests will be executed. 33 | 34 | - Add support for Xcode 12.5. 35 | 36 | - Added support for building Spectre on Windows. 37 | 38 | ## 0.9.2 (2020-11-18) 39 | 40 | ### Enhancements 41 | 42 | - Added support for using the XCTest integration on non Apple platforms with 43 | [swift-corelibs-xctest](https://github.com/apple/swift-corelibs-xctest) 44 | 45 | ### Bug Fixes 46 | 47 | - Compatibility with some versions of Xcode greater than 12.0.1 where a build 48 | error with incompatibility between XCTIssue and XCTIssueReference may be 49 | presented with Swift 5.3. 50 | 51 | ## 0.9.1 (2020-08-16) 52 | 53 | ### Enhancements 54 | 55 | - Added support for using the XcodeReporter with Xcode 12 beta. 56 | 57 | ## 0.9.0 (2018-09-10) 58 | 59 | ### Breaking 60 | 61 | - Using Spectre in Xcode has be re-hauled, there are now `describe` and `it` 62 | methods on `XCTestCase` which can be used. When used, these tests will be ran 63 | directly and reported as XCTest failures and therefore shown in Xcode and 64 | Xcode sidebar as XCTest failures. 65 | 66 | Use of the global test context, i.e, global `describe` and `it` is no longer 67 | permitted when using Spectre with XCTest. 68 | 69 | ### Enhancements 70 | 71 | - Adds support for Swift 4.2. 72 | 73 | - Unhandled errors will now be reported from the invoked cases source map. 74 | 75 | ## 0.8.0 76 | 77 | Switches to Swift 4.0. 78 | 79 | ## 0.7.2 80 | 81 | ## Enhancements 82 | 83 | - Adds support for future Swift development snapshots (2016-11-11). 84 | 85 | 86 | ## 0.7.1 87 | 88 | ## Enhancements 89 | 90 | - A test iteration can be skipped by using `skip`. 91 | 92 | ```swift 93 | throw skip() 94 | ``` 95 | 96 | 97 | ## 0.7.0 98 | 99 | This release adds support for Swift 3.0. 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spectre 2 | 3 | [![Build Status](https://img.shields.io/travis/kylef/Spectre/master.svg?style=flat)](https://travis-ci.org/kylef/Spectre) 4 | 5 | [*Sp*ecial *E*xecutive for *C*ommand-line *T*est *R*unning and 6 | *E*xecution](https://en.wikipedia.org/wiki/SPECTRE). 7 | 8 | A behavior-driven development (BDD) framework and test runner for Swift projects 9 | and playgrounds. It's compatible with both OS X and Linux. 10 | 11 | ## Usage 12 | 13 | ```swift 14 | describe("a person") { 15 | let person = Person(name: "Kyle") 16 | 17 | $0.it("has a name") { 18 | try expect(person.name) == "Kyle" 19 | } 20 | 21 | $0.it("returns the name as description") { 22 | try expect(person.description) == "Kyle" 23 | } 24 | } 25 | ``` 26 | 27 | ### Reporters 28 | 29 | Spectre currently has two built-in reporters, Standard and the Dot reporter. 30 | Custom reporters are supported, make a type that conforms to `Reporter`. 31 | 32 | - Standard 33 | - Dot Reporter (`-t`) 34 | - Tap Reporter (`--tap)` - [Test Anything Protocol](http://testanything.org/)-compatible output 35 | 36 | The default reporter can be configured via an environment variable. For 37 | example: 38 | 39 | ```shell 40 | $ env SPECTRE_REPORTER=dot swift test 41 | $ env SPECTRE_REPORTER=tap swift test 42 | ``` 43 | 44 | #### Standard 45 | 46 | The standard reporter produces output as follows: 47 | 48 | ##### Passing Tests 49 | 50 | ![Standard Reporter Success](Screenshots/success.png) 51 | 52 | ##### Failing Tests 53 | 54 | ![Standard Reporter Failure](Screenshots/failure.png) 55 | 56 | #### Dot 57 | 58 | Using the `-t` argument, you can use the dot reporter. 59 | 60 | ##### Passing Tests 61 | 62 | ![Dot Reporter Success](Screenshots/success-dot.png) 63 | 64 | ##### Failing Tests 65 | 66 | ![Dot Reporter Failure](Screenshots/failure-dot.png) 67 | 68 | ### Expectation 69 | 70 | #### Equivalence 71 | 72 | ```swift 73 | try expect(name) == "Kyle" 74 | try expect(name) != "Kyle" 75 | ``` 76 | 77 | #### Truthiness 78 | 79 | ```swift 80 | try expect(alive).to.beTrue() 81 | try expect(alive).to.beFalse() 82 | try expect(alive).to.beNil() 83 | ``` 84 | 85 | #### Error handling 86 | 87 | ```swift 88 | try expect(try write()).toThrow() 89 | try expect(try write()).toThrow(FileError.NoPermission) 90 | ``` 91 | 92 | #### Comparable 93 | 94 | ```swift 95 | try expect(5) > 2 96 | try expect(5) >= 2 97 | try expect(5) < 10 98 | try expect(5) <= 10 99 | ``` 100 | 101 | #### Types 102 | 103 | ```swift 104 | try expect("kyle").to.beOfType(String.self) 105 | ``` 106 | 107 | #### Causing a failure 108 | 109 | ```swift 110 | throw failure("Everything is broken.") 111 | ``` 112 | 113 | #### Custom assertions 114 | 115 | You can easily provide your own assertions, you just need to throw a 116 | failure when the assertion does not meet expectaions. 117 | 118 | ## Examples 119 | 120 | The following projects use Spectre: 121 | 122 | - [Commander](https://github.com/kylef/Commander) 123 | - [Stencil](https://github.com/stencilproject/Stencil) 124 | 125 | ## Installation / Running 126 | 127 | ### Swift Package Manager 128 | 129 | Check out [Commander](https://github.com/kylef/Commander) as an example. 130 | 131 | ### Playground 132 | 133 | You can use Spectre in an Xcode Playground, open `Spectre.playground` in 134 | this repository, failures are printed in the console. 135 | 136 | ![Spectre in an Xcode Playground](Screenshots/Playground.png) 137 | -------------------------------------------------------------------------------- /Sources/Spectre/Context.swift: -------------------------------------------------------------------------------- 1 | public protocol ContextType { 2 | /// Creates a new sub-context 3 | func context(_ name: String, closure: (ContextType) -> Void) 4 | 5 | /// Creates a new sub-context 6 | func describe(_ name: String, closure: (ContextType) -> Void) 7 | 8 | /// Creates a new disabled sub-context 9 | func xcontext(_ name: String, closure: (ContextType) -> Void) 10 | 11 | /// Creates a new disabled sub-context 12 | func xdescribe(_ name: String, closure: (ContextType) -> Void) 13 | 14 | func before(_ closure: @escaping () -> Void) 15 | func after(_ closure: @escaping () -> Void) 16 | 17 | /// Adds a new test case 18 | func it(_ name: String, file: String, line: Int, closure: @escaping () throws -> Void) 19 | 20 | /// Adds a disabled test case 21 | func xit(_ name: String, file: String, line: Int, closure: @escaping () throws -> Void) 22 | } 23 | 24 | extension ContextType { 25 | public func it(_ name: String, file: String = #file, line: Int = #line, closure: @escaping () throws -> Void) { 26 | it(name, file: file, line: line, closure: closure) 27 | } 28 | 29 | public func xit(_ name: String, file: String = #file, line: Int = #line, closure: @escaping () throws -> Void) { 30 | xit(name, file: file, line: line, closure: closure) 31 | } 32 | } 33 | 34 | class Context : ContextType, CaseType { 35 | let name: String 36 | var disabled: Bool 37 | fileprivate weak var parent: Context? 38 | var cases = [CaseType]() 39 | 40 | typealias Before = (() -> Void) 41 | typealias After = (() -> Void) 42 | 43 | var befores = [Before]() 44 | var afters = [After]() 45 | 46 | init(name: String, disabled: Bool = false, parent: Context? = nil) { 47 | self.name = name 48 | self.disabled = disabled 49 | self.parent = parent 50 | } 51 | 52 | func context(_ name: String, closure: (ContextType) -> Void) { 53 | let context = Context(name: name, parent: self) 54 | closure(context) 55 | cases.append(context) 56 | } 57 | 58 | func describe(_ name: String, closure: (ContextType) -> Void) { 59 | let context = Context(name: name, parent: self) 60 | closure(context) 61 | cases.append(context) 62 | } 63 | 64 | func xcontext(_ name: String, closure: (ContextType) -> Void) { 65 | let context = Context(name: name, disabled: true, parent: self) 66 | closure(context) 67 | cases.append(context) 68 | } 69 | 70 | func xdescribe(_ name: String, closure: (ContextType) -> Void) { 71 | let context = Context(name: name, disabled: true, parent: self) 72 | closure(context) 73 | cases.append(context) 74 | } 75 | 76 | func before(_ closure: @escaping () -> Void) { 77 | befores.append(closure) 78 | } 79 | 80 | func after(_ closure: @escaping () -> Void) { 81 | afters.append(closure) 82 | } 83 | 84 | func it(_ name: String, file: String, line: Int, closure: @escaping () throws -> Void) { 85 | cases.append(Case(name: name, closure: closure, file: file, line: line)) 86 | } 87 | 88 | func xit(_ name: String, file: String, line: Int, closure: @escaping () throws -> Void) { 89 | cases.append(Case(name: name, disabled: true, closure: closure, file: file, line: line)) 90 | } 91 | 92 | func runBefores() { 93 | parent?.runBefores() 94 | befores.forEach { $0() } 95 | } 96 | 97 | func runAfters() { 98 | afters.forEach { $0() } 99 | parent?.runAfters() 100 | } 101 | 102 | func run(reporter: ContextReporter) { 103 | if disabled { 104 | reporter.addDisabled(name) 105 | return 106 | } 107 | 108 | reporter.report(name) { reporter in 109 | cases.forEach { 110 | runBefores() 111 | $0.run(reporter: reporter) 112 | runAfters() 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Sources/Spectre/Global.swift: -------------------------------------------------------------------------------- 1 | #if os(Linux) 2 | import Glibc 3 | #elseif os(Windows) 4 | import CRT 5 | #else 6 | import Darwin 7 | #endif 8 | 9 | import Foundation 10 | 11 | 12 | let globalContext: GlobalContext = { 13 | #if os(macOS) 14 | if getenv("XCTestConfigurationFilePath") != nil { 15 | fatalError("Use of global context is not permitted when running inside XCTest") 16 | } 17 | #endif 18 | atexit { run() } 19 | return GlobalContext() 20 | }() 21 | 22 | public func describe(_ name: String, _ closure: (ContextType) -> Void) { 23 | globalContext.describe(name, closure: closure) 24 | } 25 | 26 | public func it(_ name: String, _ closure: @escaping () throws -> Void) { 27 | globalContext.it(name, closure: closure) 28 | } 29 | 30 | #if swift(>=4.2) 31 | enum ReporterType: String, CaseIterable { 32 | case tap 33 | case dot 34 | } 35 | #endif 36 | 37 | fileprivate func defaultReporter() -> Reporter { 38 | if let reporterEnv = ProcessInfo.processInfo.environment["SPECTRE_REPORTER"] { 39 | #if swift(>=4.2) 40 | guard let reporterType = ReporterType(rawValue: reporterEnv) else { 41 | let supported = ReporterType.allCases.map { $0.rawValue } 42 | let error = "Unknown reporter: \(reporterEnv). Supported: \(supported.joined(separator: ", "))\n" 43 | FileHandle.standardError.write(error.data(using: .utf8)!) 44 | exit(4) 45 | } 46 | 47 | switch reporterType { 48 | case .tap: 49 | return TapReporter() 50 | case .dot: 51 | return DotReporter() 52 | } 53 | #else 54 | let error = "SPECTRE_REPORTER is unsupported on Swift < 4.1\n" 55 | FileHandle.standardError.write(error.data(using: .utf8)!) 56 | exit(4) 57 | #endif 58 | } 59 | 60 | return StandardReporter() 61 | } 62 | 63 | 64 | @discardableResult func disableUnmatchedPaths(_ paths: [Path], _ `case`: CaseType) -> Bool { 65 | if let `case` = `case` as? Case { 66 | if !`case`.disabled { 67 | let path = Path(`case`.file) 68 | 69 | if !(paths.contains(where: { $0 ~= path })) { 70 | `case`.disabled = true 71 | } 72 | } 73 | return !`case`.disabled 74 | } else if let context = `case` as? Context { 75 | var containsMatch = false 76 | for `case` in context.cases { 77 | if disableUnmatchedPaths(paths, `case`) { 78 | containsMatch = true 79 | } 80 | } 81 | 82 | if !containsMatch { 83 | context.disabled = true 84 | } 85 | 86 | return containsMatch 87 | } else { 88 | let error = "Unexpected type on global context (report as bug)\n" 89 | FileHandle.standardError.write(error.data(using: .utf8)!) 90 | exit(3) 91 | } 92 | } 93 | 94 | 95 | public func run() -> Never { 96 | var reporter = defaultReporter() 97 | var paths: [Path] = [] 98 | 99 | var arguments: [String] = Array(CommandLine.arguments[1...]) 100 | if let addOptions = ProcessInfo.processInfo.environment["SPECTRE_ADDOPTS"] { 101 | arguments += addOptions.split(separator: " ").map(String.init) 102 | } 103 | 104 | for argument in arguments { 105 | if argument == "--tap" { 106 | reporter = TapReporter() 107 | } else if argument == "-t" { 108 | reporter = DotReporter() 109 | } else if argument.hasPrefix("-") { 110 | let error = "Unexpected option: \(argument)\n" 111 | FileHandle.standardError.write(error.data(using: .utf8)!) 112 | exit(4) 113 | } else { 114 | let path = Path(argument) 115 | paths.append(path.absolute()) 116 | } 117 | } 118 | 119 | // filter by paths 120 | if !paths.isEmpty { 121 | for `case` in globalContext.cases { 122 | disableUnmatchedPaths(paths, `case`) 123 | } 124 | } 125 | 126 | run(reporter: reporter) 127 | } 128 | 129 | 130 | public func run(reporter: Reporter) -> Never { 131 | if globalContext.run(reporter: reporter) { 132 | exit(0) 133 | } 134 | exit(1) 135 | } 136 | -------------------------------------------------------------------------------- /Tests/SpectreTests/ExpectationSpec.swift: -------------------------------------------------------------------------------- 1 | import Spectre 2 | 3 | public let testExpectation: ((ContextType) -> Void) = { 4 | $0.it("can be created from a value") { 5 | let expectation = expect("value") 6 | let value = try expectation.expression() 7 | 8 | assert(value == "value") 9 | } 10 | 11 | $0.describe("comparison to nil") { 12 | let name: String? = nil 13 | 14 | $0.it("errors when value is not nil") { 15 | do { 16 | try expect("kyle").to.beNil() 17 | fatalError() 18 | } catch {} 19 | } 20 | 21 | $0.it("passes when value is nil") { 22 | try expect(name).to.beNil() 23 | } 24 | } 25 | 26 | $0.describe("comparison to type") { 27 | class Animal {open func move() {}} 28 | class Bear: Animal {func rawr() {}} 29 | 30 | $0.it("errors when value is not the same type") { 31 | do { 32 | try expect("kyle").to.beOfType(Bool.self) 33 | fatalError() 34 | } catch {} 35 | } 36 | 37 | $0.it("passes when value is the same value type") { 38 | try expect(true).to.beOfType(Bool.self) 39 | } 40 | 41 | $0.it("passes when value is the same object type") { 42 | try expect(Animal()).to.beOfType(Animal.self) 43 | } 44 | 45 | $0.it("fails when value is a subclass of an object type") { 46 | do { 47 | try expect(Bear()).to.beOfType(Animal.self) 48 | fatalError() 49 | } catch {} 50 | } 51 | } 52 | 53 | $0.describe("equality extensions") { 54 | $0.describe("`==` operator") { 55 | $0.it("continues when the rhs is the same value") { 56 | do { 57 | try expect("value") == "value" 58 | } catch { 59 | fatalError() 60 | } 61 | } 62 | 63 | $0.it("throws when the expectation is different") { 64 | do { 65 | try expect("value") == "value2" 66 | fatalError() 67 | } catch { 68 | } 69 | } 70 | 71 | $0.it("throws when the expectation's value is nil") { 72 | let value: String? = nil 73 | do { 74 | try expect(value) == "value" 75 | fatalError() 76 | } catch { 77 | } 78 | } 79 | } 80 | 81 | $0.describe("`!=` operator") { 82 | $0.it("continues when the rhs is not the same value") { 83 | do { 84 | try expect("value") != "value2" 85 | } catch { 86 | fatalError() 87 | } 88 | } 89 | 90 | $0.it("throws when the rhs is the same as the value") { 91 | do { 92 | try expect("value") != "value" 93 | fatalError() 94 | } catch { 95 | } 96 | } 97 | } 98 | } 99 | 100 | $0.context("with a boolean value") { 101 | $0.it("can check if the value is true") { 102 | try expect(true).to.beTrue() 103 | } 104 | 105 | $0.it("throws an error when the value is not true") { 106 | do { 107 | try expect(false).to.beTrue() 108 | fatalError() 109 | } catch {} 110 | } 111 | 112 | $0.it("can check if the value is false") { 113 | try expect(false).to.beFalse() 114 | } 115 | 116 | $0.it("throws an error when the value is not true") { 117 | do { 118 | try expect(true).to.beFalse() 119 | fatalError() 120 | } catch {} 121 | } 122 | } 123 | 124 | $0.describe("error handling") { 125 | enum FileError : Error { 126 | case notFound 127 | case noPermission 128 | } 129 | 130 | func throwing() throws { 131 | throw FileError.notFound 132 | } 133 | 134 | func nonThrowing() throws {} 135 | 136 | $0.it("doesn't throw if error is the same") { 137 | try expect(try throwing()).toThrow(FileError.notFound) 138 | } 139 | 140 | $0.it("throws if the error differs") { 141 | do { 142 | try expect(try throwing()).toThrow(FileError.noPermission) 143 | fatalError() 144 | } catch {} 145 | } 146 | 147 | $0.it("throws if no error was provided") { 148 | do { 149 | try expect(try nonThrowing()).toThrow() 150 | fatalError() 151 | } catch {} 152 | } 153 | } 154 | 155 | $0.describe("comparable") { 156 | $0.it("can compare using the > operator") { 157 | try expect(5) > 2 158 | 159 | do { 160 | try expect(5) > 5 161 | fatalError() 162 | } catch {} 163 | } 164 | 165 | $0.it("can compare using the >= operator") { 166 | try expect(5) >= 5 167 | 168 | do { 169 | try expect(5) >= 6 170 | fatalError() 171 | } catch {} 172 | } 173 | 174 | $0.it("can compare using the < operator") { 175 | try expect(5) < 6 176 | 177 | do { 178 | try expect(5) < 5 179 | fatalError() 180 | } catch {} 181 | } 182 | 183 | $0.it("can compare using the <= operator") { 184 | try expect(5) <= 5 185 | 186 | do { 187 | try expect(5) <= 4 188 | fatalError() 189 | } catch {} 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /Sources/Spectre/Path.swift: -------------------------------------------------------------------------------- 1 | // PathKit - Effortless path operations 2 | 3 | import Foundation 4 | 5 | /// Represents a filesystem path. 6 | struct Path { 7 | /// The character used by the OS to separate two path elements 8 | static let separator = "/" 9 | 10 | /// The underlying string representation 11 | internal let path: String 12 | internal static let fileManager = FileManager.default 13 | 14 | // MARK: Init 15 | 16 | init() { 17 | self.init("") 18 | } 19 | 20 | /// Create a Path from a given String 21 | init(_ path: String) { 22 | self.path = path 23 | } 24 | 25 | /// Create a Path by joining multiple path components together 26 | init(components: S) where S.Iterator.Element == String { 27 | let path: String 28 | if components.isEmpty { 29 | path = "." 30 | } else if components.first == Path.separator && components.count > 1 { 31 | let p = components.joined(separator: Path.separator) 32 | path = String(p[p.index(after: p.startIndex)...]) 33 | } else { 34 | path = components.joined(separator: Path.separator) 35 | } 36 | self.init(path) 37 | } 38 | } 39 | 40 | 41 | 42 | // MARK: Conversion 43 | 44 | extension Path { 45 | var string: String { 46 | return self.path 47 | } 48 | 49 | var url: URL { 50 | return URL(fileURLWithPath: path) 51 | } 52 | } 53 | 54 | 55 | // MARK: Hashable 56 | 57 | extension Path : Hashable { 58 | func hash(into hasher: inout Hasher) { 59 | hasher.combine(self.path.hashValue) 60 | } 61 | } 62 | 63 | 64 | // MARK: Path Info 65 | 66 | extension Path { 67 | /// Test whether a path is absolute. 68 | /// 69 | /// - Returns: `true` iff the path begins with a slash 70 | /// 71 | public var isAbsolute: Bool { 72 | return path.hasPrefix(Path.separator) 73 | } 74 | 75 | /// Test whether a path is absolute. 76 | /// 77 | /// - Returns: `true` iff the path begins with a slash 78 | /// 79 | var isRelative: Bool { 80 | return !isAbsolute 81 | } 82 | 83 | /// Concatenates relative paths to the current directory and derives the normalized path 84 | /// 85 | /// - Returns: the absolute path in the actual filesystem 86 | /// 87 | func absolute() -> Path { 88 | if isAbsolute { 89 | return normalize() 90 | } 91 | 92 | let expandedPath = Path(NSString(string: self.path).expandingTildeInPath) 93 | if expandedPath.isAbsolute { 94 | return expandedPath.normalize() 95 | } 96 | 97 | return (Path.current + self).normalize() 98 | } 99 | 100 | /// Normalizes the path, this cleans up redundant ".." and ".", double slashes 101 | /// and resolves "~". 102 | /// 103 | /// - Returns: a new path made by removing extraneous path components from the underlying String 104 | /// representation. 105 | /// 106 | func normalize() -> Path { 107 | return Path(NSString(string: self.path).standardizingPath) 108 | } 109 | } 110 | 111 | // MARK: Current Directory 112 | 113 | extension Path { 114 | /// The current working directory of the process 115 | /// 116 | /// - Returns: the current working directory of the process 117 | /// 118 | static var current: Path { 119 | get { 120 | return self.init(Path.fileManager.currentDirectoryPath) 121 | } 122 | } 123 | } 124 | 125 | 126 | // MARK: Equatable 127 | 128 | extension Path : Equatable {} 129 | 130 | /// Determines if two paths are identical 131 | /// 132 | /// - Note: The comparison is string-based. Be aware that two different paths (foo.txt and 133 | /// ./foo.txt) can refer to the same file. 134 | /// 135 | func ==(lhs: Path, rhs: Path) -> Bool { 136 | return lhs.path == rhs.path 137 | } 138 | 139 | 140 | // MARK: Pattern Matching 141 | 142 | /// Implements pattern-matching for paths. 143 | /// 144 | /// - Returns: `true` iff one of the following conditions is true: 145 | /// - the paths are equal (based on `Path`'s `Equatable` implementation) 146 | /// - the paths can be normalized to equal Paths. 147 | /// 148 | func ~=(lhs: Path, rhs: Path) -> Bool { 149 | return lhs == rhs 150 | || lhs.normalize() == rhs.normalize() 151 | } 152 | 153 | // MARK: Operators 154 | 155 | /// Appends a Path fragment to another Path to produce a new Path 156 | func +(lhs: Path, rhs: Path) -> Path { 157 | return lhs.path + rhs.path 158 | } 159 | 160 | /// Appends a String fragment to another Path to produce a new Path 161 | func +(lhs: Path, rhs: String) -> Path { 162 | return lhs.path + rhs 163 | } 164 | 165 | /// Appends a String fragment to another String to produce a new Path 166 | internal func +(lhs: String, rhs: String) -> Path { 167 | if rhs.hasPrefix(Path.separator) { 168 | // Absolute paths replace relative paths 169 | return Path(rhs) 170 | } else { 171 | var lSlice = NSString(string: lhs).pathComponents.fullSlice 172 | var rSlice = NSString(string: rhs).pathComponents.fullSlice 173 | 174 | // Get rid of trailing "/" at the left side 175 | if lSlice.count > 1 && lSlice.last == Path.separator { 176 | lSlice.removeLast() 177 | } 178 | 179 | // Advance after the first relevant "." 180 | lSlice = lSlice.filter { $0 != "." }.fullSlice 181 | rSlice = rSlice.filter { $0 != "." }.fullSlice 182 | 183 | // Eats up trailing components of the left and leading ".." of the right side 184 | while lSlice.last != ".." && !lSlice.isEmpty && rSlice.first == ".." { 185 | if lSlice.count > 1 || lSlice.first != Path.separator { 186 | // A leading "/" is never popped 187 | lSlice.removeLast() 188 | } 189 | if !rSlice.isEmpty { 190 | rSlice.removeFirst() 191 | } 192 | 193 | switch (lSlice.isEmpty, rSlice.isEmpty) { 194 | case (true, _): 195 | break 196 | case (_, true): 197 | break 198 | default: 199 | continue 200 | } 201 | } 202 | 203 | return Path(components: lSlice + rSlice) 204 | } 205 | } 206 | 207 | extension Array { 208 | var fullSlice: ArraySlice { 209 | return self[self.indices.suffix(from: 0)] 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /Sources/Spectre/Expectation.swift: -------------------------------------------------------------------------------- 1 | public protocol ExpectationType { 2 | associatedtype ValueType 3 | var expression: () throws -> ValueType? { get } 4 | func failure(_ reason: String) -> FailureType 5 | } 6 | 7 | 8 | struct ExpectationFailure : FailureType { 9 | let file: String 10 | let line: Int 11 | let function: String 12 | 13 | let reason: String 14 | 15 | init(reason: String, file: String, line: Int, function: String) { 16 | self.reason = reason 17 | self.file = file 18 | self.line = line 19 | self.function = function 20 | } 21 | } 22 | 23 | open class Expectation : ExpectationType { 24 | public typealias ValueType = T 25 | public let expression: () throws -> ValueType? 26 | 27 | let file: String 28 | let line: Int 29 | let function: String 30 | 31 | open var to: Expectation { 32 | return self 33 | } 34 | 35 | init(file: String, line: Int, function: String, expression: @escaping () throws -> ValueType?) { 36 | self.file = file 37 | self.line = line 38 | self.function = function 39 | self.expression = expression 40 | } 41 | 42 | open func failure(_ reason: String) -> FailureType { 43 | return ExpectationFailure(reason: reason, file: file, line: line, function: function) 44 | } 45 | } 46 | 47 | public func expect( _ expression: @autoclosure @escaping () throws -> T?, file: String = #file, line: Int = #line, function: String = #function) -> Expectation { 48 | return Expectation(file: file, line: line, function: function, expression: expression) 49 | } 50 | 51 | public func expect(_ file: String = #file, line: Int = #line, function: String = #function, expression: @escaping () throws -> T?) -> Expectation { 52 | return Expectation(file: file, line: line, function: function, expression: expression) 53 | } 54 | 55 | // MARK: Equatability 56 | 57 | public func == (lhs: E, rhs: E.ValueType) throws where E.ValueType: Equatable { 58 | if let value = try lhs.expression() { 59 | if value != rhs { 60 | throw lhs.failure("\(String(describing: value)) is not equal to \(rhs)") 61 | } 62 | } else { 63 | throw lhs.failure("given value is nil") 64 | } 65 | } 66 | 67 | public func != (lhs: E, rhs: E.ValueType) throws where E.ValueType: Equatable { 68 | let value = try lhs.expression() 69 | if value == rhs { 70 | throw lhs.failure("\(String(describing: value)) is equal to \(rhs)") 71 | } 72 | } 73 | 74 | // MARK: Array Equatability 75 | 76 | public func == (lhs: Expectation<[Element]>, rhs: [Element]) throws { 77 | if let value = try lhs.expression() { 78 | if value != rhs { 79 | throw lhs.failure("\(String(describing: value)) is not equal to \(rhs)") 80 | } 81 | } else { 82 | throw lhs.failure("given value is nil") 83 | } 84 | } 85 | 86 | public func != (lhs: Expectation<[Element]>, rhs: [Element]) throws { 87 | if let value = try lhs.expression() { 88 | if value == rhs { 89 | throw lhs.failure("\(String(describing: value)) is equal to \(rhs)") 90 | } 91 | } else { 92 | throw lhs.failure("given value is nil") 93 | } 94 | } 95 | 96 | // MARK: Dictionary Equatability 97 | 98 | public func == (lhs: Expectation<[Key: Value]>, rhs: [Key: Value]) throws { 99 | if let value = try lhs.expression() { 100 | if value != rhs { 101 | throw lhs.failure("\(String(describing: value)) is not equal to \(rhs)") 102 | } 103 | } else { 104 | throw lhs.failure("given value is nil") 105 | } 106 | } 107 | 108 | public func != (lhs: Expectation<[Key: Value]>, rhs: [Key: Value]) throws { 109 | if let value = try lhs.expression() { 110 | if value == rhs { 111 | throw lhs.failure("\(String(describing: value)) is equal to \(rhs)") 112 | } 113 | } else { 114 | throw lhs.failure("given value is nil") 115 | } 116 | } 117 | 118 | // MARK: Nil 119 | 120 | extension ExpectationType { 121 | public func beNil() throws { 122 | let value = try expression() 123 | if value != nil { 124 | throw failure("value is not nil") 125 | } 126 | } 127 | } 128 | 129 | // MARK: Boolean 130 | 131 | extension ExpectationType where ValueType == Bool { 132 | public func beTrue() throws { 133 | let value = try expression() 134 | if value != true { 135 | throw failure("value is not true") 136 | } 137 | } 138 | 139 | public func beFalse() throws { 140 | let value = try expression() 141 | if value != false { 142 | throw failure("value is not false") 143 | } 144 | } 145 | } 146 | 147 | // Mark: Types 148 | 149 | extension ExpectationType { 150 | public func beOfType(_ expectedType: Any.Type) throws { 151 | guard let value = try expression() else { throw failure("cannot determine type: expression threw an error or value is nil") } 152 | let valueType = Mirror(reflecting: value).subjectType 153 | if valueType != expectedType { 154 | throw failure("'\(valueType)' is not the expected type '\(expectedType)'") 155 | } 156 | } 157 | } 158 | 159 | // MARK: Error Handling 160 | 161 | extension ExpectationType { 162 | public func toThrow() throws { 163 | var didThrow = false 164 | 165 | do { 166 | _ = try expression() 167 | } catch { 168 | didThrow = true 169 | } 170 | 171 | if !didThrow { 172 | throw failure("expression did not throw an error") 173 | } 174 | } 175 | 176 | public func toThrow(_ error: T) throws { 177 | var thrownError: Error? = nil 178 | 179 | do { 180 | _ = try expression() 181 | } catch { 182 | thrownError = error 183 | } 184 | 185 | if let thrownError = thrownError { 186 | if let thrownError = thrownError as? T { 187 | if error != thrownError { 188 | throw failure("\(thrownError) is not \(error)") 189 | } 190 | } else { 191 | throw failure("\(thrownError) is not \(error)") 192 | } 193 | } else { 194 | throw failure("expression did not throw an error") 195 | } 196 | } 197 | } 198 | 199 | // MARK: Comparable 200 | 201 | public func > (lhs: E, rhs: E.ValueType) throws where E.ValueType: Comparable { 202 | let value = try lhs.expression() 203 | guard value! > rhs else { 204 | throw lhs.failure("\(String(describing: value)) is not more than \(rhs)") 205 | } 206 | } 207 | 208 | public func >= (lhs: E, rhs: E.ValueType) throws where E.ValueType: Comparable { 209 | let value = try lhs.expression() 210 | guard value! >= rhs else { 211 | throw lhs.failure("\(String(describing: value)) is not more than or equal to \(rhs)") 212 | } 213 | } 214 | 215 | public func < (lhs: E, rhs: E.ValueType) throws where E.ValueType: Comparable { 216 | let value = try lhs.expression() 217 | guard value! < rhs else { 218 | throw lhs.failure("\(String(describing: value)) is not less than \(rhs)") 219 | } 220 | } 221 | 222 | public func <= (lhs: E, rhs: E.ValueType) throws where E.ValueType: Comparable { 223 | let value = try lhs.expression() 224 | guard value! <= rhs else { 225 | throw lhs.failure("\(String(describing: value)) is not less than or equal to \(rhs)") 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /Sources/Spectre/Reporters.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if os(Linux) 3 | import Glibc 4 | #elseif os(Windows) 5 | import CRT 6 | #else 7 | import Darwin.C 8 | #endif 9 | 10 | 11 | enum ANSI : String, CustomStringConvertible { 12 | case Red = "\u{001B}[0;31m" 13 | case Green = "\u{001B}[0;32m" 14 | case Yellow = "\u{001B}[0;33m" 15 | 16 | case Bold = "\u{001B}[0;1m" 17 | case Reset = "\u{001B}[0;0m" 18 | 19 | static var supportsANSI: Bool { 20 | let platform_isatty: (Int32) -> Int32 21 | #if os(Windows) 22 | platform_isatty = _isatty 23 | #else 24 | platform_isatty = isatty 25 | #endif 26 | 27 | guard platform_isatty(STDOUT_FILENO) != 0 else { 28 | return false 29 | } 30 | 31 | guard let termType = ProcessInfo.processInfo.environment["TERM"] else { 32 | return false 33 | } 34 | 35 | guard termType.lowercased() != "dumb" else { 36 | return false 37 | } 38 | 39 | return true 40 | } 41 | 42 | var description: String { 43 | if ANSI.supportsANSI { 44 | return rawValue 45 | } 46 | 47 | return "" 48 | } 49 | } 50 | 51 | 52 | struct CaseFailure { 53 | let position: [String] 54 | let failure: FailureType 55 | 56 | init(position: [String], failure: FailureType) { 57 | self.position = position 58 | self.failure = failure 59 | } 60 | } 61 | 62 | 63 | fileprivate func stripCurrentDirectory(_ file: String) -> String { 64 | var currentPath = FileManager.`default`.currentDirectoryPath 65 | if !currentPath.hasSuffix("/") { 66 | currentPath += "/" 67 | } 68 | 69 | if file.hasPrefix(currentPath) { 70 | return String(file.suffix(from: currentPath.endIndex)) 71 | } 72 | 73 | return file 74 | } 75 | 76 | 77 | func printFailures(_ failures: [CaseFailure]) { 78 | for failure in failures { 79 | let name = failure.position.joined(separator: " ") 80 | Swift.print(ANSI.Red, name) 81 | 82 | let file = "\(stripCurrentDirectory(failure.failure.file)):\(failure.failure.line)" 83 | Swift.print(" \(ANSI.Bold)\(file)\(ANSI.Reset) \(ANSI.Yellow)\(failure.failure.reason)\(ANSI.Reset)\n") 84 | 85 | if let contents = try? String(contentsOfFile: failure.failure.file, encoding: String.Encoding.utf8) as String { 86 | let lines = contents.components(separatedBy: CharacterSet.newlines) 87 | let line = lines[failure.failure.line - 1] 88 | let trimmedLine = line.trimmingCharacters(in: CharacterSet.whitespaces) 89 | Swift.print(" ```") 90 | Swift.print(" \(trimmedLine)") 91 | Swift.print(" ```") 92 | } 93 | } 94 | } 95 | 96 | 97 | class CountReporter : Reporter, ContextReporter { 98 | var depth = 0 99 | var successes = 0 100 | var disabled = 0 101 | var position = [String]() 102 | var failures = [CaseFailure]() 103 | 104 | func printStatus() { 105 | printFailures(failures) 106 | 107 | let disabledMessage: String 108 | if disabled > 0 { 109 | disabledMessage = " \(disabled) skipped," 110 | } else { 111 | disabledMessage = "" 112 | } 113 | 114 | if failures.count == 1 { 115 | print("\(successes) passes\(disabledMessage) and \(failures.count) failure") 116 | } else { 117 | print("\(successes) passes\(disabledMessage) and \(failures.count) failures") 118 | } 119 | } 120 | 121 | #if swift(>=3.0) 122 | func report(closure: (ContextReporter) -> Void) -> Bool { 123 | closure(self) 124 | printStatus() 125 | return failures.isEmpty 126 | } 127 | #else 128 | func report(@noescape _ closure: (ContextReporter) -> Void) -> Bool { 129 | closure(self) 130 | printStatus() 131 | return failures.isEmpty 132 | } 133 | #endif 134 | 135 | #if swift(>=3.0) 136 | func report(_ name: String, closure: (ContextReporter) -> Void) { 137 | depth += 1 138 | position.append(name) 139 | closure(self) 140 | depth -= 1 141 | position.removeLast() 142 | } 143 | #else 144 | func report(_ name: String, @noescape closure: (ContextReporter) -> Void) { 145 | depth += 1 146 | position.append(name) 147 | closure(self) 148 | depth -= 1 149 | position.removeLast() 150 | } 151 | #endif 152 | 153 | func addSuccess(_ name: String) { 154 | successes += 1 155 | } 156 | 157 | func addDisabled(_ name: String) { 158 | disabled += 1 159 | } 160 | 161 | func addFailure(_ name: String, failure: FailureType) { 162 | failures.append(CaseFailure(position: position + [name], failure: failure)) 163 | } 164 | } 165 | 166 | 167 | /// Standard reporter 168 | class StandardReporter : CountReporter { 169 | override func report(_ name: String, closure: (ContextReporter) -> Void) { 170 | colour(.Bold, "-> \(name)") 171 | super.report(name, closure: closure) 172 | print("") 173 | } 174 | 175 | override func addSuccess(_ name: String) { 176 | super.addSuccess(name) 177 | colour(.Green, "-> \(name)") 178 | } 179 | 180 | override func addDisabled(_ name: String) { 181 | super.addDisabled(name) 182 | colour(.Yellow, "-> \(name)") 183 | } 184 | 185 | override func addFailure(_ name: String, failure: FailureType) { 186 | super.addFailure(name, failure: failure) 187 | colour(.Red, "-> \(name)") 188 | } 189 | 190 | func colour(_ colour: ANSI, _ message: String) { 191 | let indentation = String(repeating: " ", count: depth * 2) 192 | print("\(indentation)\(colour)\(message)\(ANSI.Reset)") 193 | } 194 | } 195 | 196 | 197 | /// Simple reporter that outputs minimal . F and S. 198 | class DotReporter : CountReporter { 199 | override func addSuccess(_ name: String) { 200 | super.addSuccess(name) 201 | print(ANSI.Green, ".", ANSI.Reset, separator: "", terminator: "") 202 | } 203 | 204 | override func addDisabled(_ name: String) { 205 | super.addDisabled(name) 206 | print(ANSI.Yellow, "S", ANSI.Reset, separator: "", terminator: "") 207 | } 208 | 209 | override func addFailure(_ name: String, failure: FailureType) { 210 | super.addFailure(name, failure: failure) 211 | print(ANSI.Red, "F", ANSI.Reset, separator: "", terminator: "") 212 | } 213 | 214 | override func printStatus() { 215 | print("\n") 216 | super.printStatus() 217 | } 218 | } 219 | 220 | 221 | /// Test Anything Protocol compatible reporter 222 | /// http://testanything.org 223 | class TapReporter : CountReporter { 224 | var count = 0 225 | 226 | override func addSuccess(_ name: String) { 227 | count += 1 228 | super.addSuccess(name) 229 | 230 | let message = (position + [name]).joined(separator: " ") 231 | print("ok \(count) - \(message)") 232 | } 233 | 234 | override func addDisabled(_ name: String) { 235 | count += 1 236 | super.addDisabled(name) 237 | 238 | let message = (position + [name]).joined(separator: " ") 239 | print("ok \(count) - # skip \(message)") 240 | } 241 | 242 | override func addFailure(_ name: String, failure: FailureType) { 243 | count += 1 244 | super.addFailure(name, failure: failure) 245 | 246 | let message = (position + [name]).joined(separator: " ") 247 | print("not ok \(count) - \(message)") 248 | print("# \(failure.reason) from \(stripCurrentDirectory(failure.file)):\(failure.line)") 249 | } 250 | 251 | override func printStatus() { 252 | print("\(min(1, count))..\(count)") 253 | } 254 | } 255 | --------------------------------------------------------------------------------