├── 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 | [](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 | 
51 |
52 | ##### Failing Tests
53 |
54 | 
55 |
56 | #### Dot
57 |
58 | Using the `-t` argument, you can use the dot reporter.
59 |
60 | ##### Passing Tests
61 |
62 | 
63 |
64 | ##### Failing Tests
65 |
66 | 
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 | 
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 |
--------------------------------------------------------------------------------