├── .github
└── workflows
│ ├── ci.yml
│ └── documentation.yml
├── .gitignore
├── Package.resolved
├── Package.swift
├── README.md
├── Sources
└── TAP
│ ├── BailOut.swift
│ ├── Directive.swift
│ ├── Outcome.swift
│ ├── Reporter.swift
│ ├── TAP.swift
│ ├── Test.swift
│ └── XCTestTAPObserver.swift
└── Tests
├── TAPTests
├── MockTextOutputStream.swift
└── TAPTests.swift
└── XCTMain.swift
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | macos_big_sur:
11 | runs-on: macos-11.0
12 |
13 | strategy:
14 | matrix:
15 | xcode:
16 | - "12.2" # Swift 5.3
17 |
18 | name: "macOS Big Sur (Xcode ${{ matrix.xcode }})"
19 |
20 | steps:
21 | - name: Checkout
22 | uses: actions/checkout@v1
23 | - uses: actions/cache@v2
24 | with:
25 | path: .build
26 | key: ${{ runner.os }}-spm-xcode-${{ matrix.xcode }}-${{ hashFiles('**/Package.resolved') }}
27 | restore-keys: |
28 | ${{ runner.os }}-spm-xcode-${{ matrix.xcode }}-
29 | - name: Build and Test
30 | run: |
31 | swift test
32 | env:
33 | DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
34 |
35 | macos_catalina:
36 | runs-on: macos-10.15
37 |
38 | strategy:
39 | matrix:
40 | xcode:
41 | - "12" # Swift 5.3
42 |
43 | name: "macOS Catalina (Xcode ${{ matrix.xcode }})"
44 |
45 | steps:
46 | - name: Checkout
47 | uses: actions/checkout@v1
48 | - uses: actions/cache@v2
49 | with:
50 | path: .build
51 | key: ${{ runner.os }}-spm-xcode-${{ matrix.xcode }}-${{ hashFiles('**/Package.resolved') }}
52 | restore-keys: |
53 | ${{ runner.os }}-spm-xcode-${{ matrix.xcode }}-
54 | - name: Build and Test
55 | run: |
56 | swift test
57 | env:
58 | DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
59 |
60 | linux:
61 | runs-on: ubuntu-latest
62 |
63 | name: "Ubuntu Linux"
64 |
65 | container:
66 | image: swiftlang/swift:nightly-focal
67 |
68 | steps:
69 | - name: Checkout
70 | uses: actions/checkout@v1
71 | - uses: actions/cache@v2
72 | with:
73 | path: .build
74 | key: ${{ runner.os }}-spm-swift-${{ matrix.swift }}-${{ hashFiles('**/Package.resolved') }}
75 | restore-keys: |
76 | ${{ runner.os }}-spm-swift-${{ matrix.swift }}-
77 | - name: Build and Test
78 | run: |
79 | swift test
80 |
--------------------------------------------------------------------------------
/.github/workflows/documentation.yml:
--------------------------------------------------------------------------------
1 | name: Documentation
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths:
8 | - .github/workflows/documentation.yml
9 | - Sources/**.swift
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@v1
18 | - name: Generate Documentation
19 | uses: SwiftDocOrg/swift-doc@master
20 | with:
21 | inputs: "Sources"
22 | output: "Documentation"
23 | - name: Upload Documentation to Wiki
24 | uses: SwiftDocOrg/github-wiki-publish-action@v1
25 | with:
26 | path: "Documentation"
27 | env:
28 | GH_PERSONAL_ACCESS_TOKEN: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | .swiftpm
7 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "Yams",
6 | "repositoryURL": "https://github.com/jpsim/Yams.git",
7 | "state": {
8 | "branch": null,
9 | "revision": "53741ba55ecca5c7149d8c9f810913ec80845c69",
10 | "version": "3.0.0"
11 | }
12 | }
13 | ]
14 | },
15 | "version": 1
16 | }
17 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "TAP",
8 | products: [
9 | // Products define the executables and libraries produced by a package, and make them visible to other packages.
10 | .library(
11 | name: "TAP",
12 | targets: ["TAP"]),
13 | ],
14 | dependencies: [
15 | // Dependencies declare other packages that this package depends on.
16 | .package(url: "https://github.com/jpsim/Yams.git", "2.0.0"..<"4.0.0"),
17 | ],
18 | targets: [
19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
20 | // Targets can depend on other targets in this package, and on products in packages which this package depends on.
21 | .target(
22 | name: "TAP",
23 | dependencies: [
24 | .product(name: "Yams", package: "Yams")
25 | ]),
26 | .testTarget(
27 | name: "TAPTests",
28 | dependencies: [
29 | .target(name: "TAP")
30 | ]),
31 | ]
32 | )
33 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TAP
2 |
3 | A Swift package for the [Test Anything Protocol][tap] (v13).
4 |
5 | ## Requirements
6 |
7 | - Swift 5.3+
8 |
9 | ## Usage
10 |
11 | You can use `TAP` as an alternative to `XCTest` in executable targets
12 | or as a custom reporter in test targets.
13 |
14 | ### Running Tests Directly
15 |
16 | ```swift
17 | import TAP
18 |
19 | try TAP.run([
20 | test(1 + 1 == 2), // passes
21 | test(true == false) // fails
22 | ])
23 | // Prints:
24 | /*
25 | TAP version 13
26 | 1..2
27 | ok 1
28 | not ok 2
29 | ---
30 | file: path/to/File.swift
31 | line: 5
32 | ...
33 |
34 | */
35 | ```
36 |
37 | ### Custom Test Reporting
38 |
39 | #### Linux
40 |
41 | Swift Package Manager on Linux
42 | uses [swift-corelibs-xctest](https://github.com/apple/swift-corelibs-xctest),
43 | which provides an `XCTMain` that
44 |
45 | Run the following command **on macOS** to (re)-generate your main test file:
46 |
47 | ```terminal
48 | $ swift test --generate-linuxmain
49 | ```
50 |
51 | Open the resulting `LinuxMain.swift` file,
52 | add an import statement for the `TAP` module
53 | and register `XCTestTAPObserver` as a test observer.
54 | In Swift 5.4 and later,
55 | you can update the `XCTMain` invocation to include an `observers` parameter
56 | with an instance of `XCTestTAPObserver`.
57 |
58 | ```swift
59 | #if os(Linux)
60 | import XCTest
61 | import TAP
62 | @testable import TAPTests
63 |
64 | #if swift(>=5.4)
65 | XCTMain([
66 | testCase(TAPTests.allTests)
67 | ],
68 | arguments: CommandLine.arguments,
69 | observers: [
70 | XCTestTAPObserver()
71 | ])
72 | #else
73 | XCTestObservationCenter.shared.addTestObserver(XCTestTAPObserver())
74 | XCTMain([
75 | testCase(TAPTests.allTests)
76 | ])
77 | #endif
78 | ```
79 |
80 | When you run the `swift test` command,
81 | your test suite will be reported in TAP format.
82 |
83 | #### macOS and iOS
84 |
85 | As of Swift 5.3,
86 | it's not possible to configure a custom reporter
87 | when running tests directly through Swift Package Manager.
88 | However, Xcode provides a mechanism for loading custom reports via
89 | [`XCTestObservationCenter`](https://developer.apple.com/documentation/xctest/xctestobservationcenter).
90 |
91 | Create a new file named `TestObservation.swift` and add it to your test bundle.
92 | Import the `TAP` module,
93 | declare a subclass of `NSObject` named `TestObservation`,
94 | and override its designated initializer
95 | to register `XCTestTAPObserver` with the shared `XCTestObservationCenter`.
96 |
97 | ```swift
98 | import TAP
99 |
100 | final class TestObservation: NSObject {
101 | override init() {
102 | XCTestObservationCenter.shared.addTestObserver(XCTestTAPObserver())
103 | }
104 | }
105 | ```
106 |
107 | Add an entry to your test target's `Info.plist` file
108 | designating the fully-qualified name of this class as the `NSPrincipalClass`.
109 |
110 | ```xml
111 | NSPrincipalClass
112 | YourTestTarget.TestObservation
113 | ```
114 |
115 | When you run your test bundle,
116 | Xcode will instantiate the principle class first,
117 | ensuring that your test observers are registered in time
118 | to report the progress of all test runs.
119 |
120 | ## Installation
121 |
122 | ### Swift Package Manager
123 |
124 | Add the TAP package to your target dependencies in `Package.swift`:
125 |
126 | ```swift
127 | import PackageDescription
128 |
129 | let package = Package(
130 | name: "YourProject",
131 | dependencies: [
132 | .package(
133 | url: "https://github.com/SwiftDocOrg/TAP",
134 | from: "0.2.0"
135 | ),
136 | ]
137 | )
138 | ```
139 |
140 | Add `TAP` as a dependency to your test target(s):
141 |
142 | ```swift
143 | targets: [
144 | .testTarget(
145 | name: "YourTestTarget",
146 | dependencies: ["TAP"]),
147 | ```
148 |
149 | ## License
150 |
151 | MIT
152 |
153 | ## Contact
154 |
155 | Mattt ([@mattt](https://twitter.com/mattt))
156 |
157 | [tap]: https://testanything.org
158 |
--------------------------------------------------------------------------------
/Sources/TAP/BailOut.swift:
--------------------------------------------------------------------------------
1 | /**
2 | An error that can be thrown from a test to stop the execution of further tests.
3 |
4 | From the [TAP specification](https://testanything.org/tap-specification.html):
5 |
6 | > ### Bail out!
7 | >
8 | > As an emergency measure
9 | > a test script can decide that further tests are useless
10 | > (e.g. missing dependencies)
11 | > and testing should stop immediately.
12 | > In that case the test script prints the magic words `Bail out!`
13 | > to standard output.
14 | > Any message after these words must be displayed by the interpreter
15 | > as the reason why testing must be stopped,
16 | > as in `Bail out! MySQL is not running.`
17 | */
18 | public struct BailOut: Error, LosslessStringConvertible {
19 | public let description: String
20 |
21 | public init(_ description: String = "") {
22 | self.description = description
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/TAP/Directive.swift:
--------------------------------------------------------------------------------
1 | /**
2 | An error that can be thrown from a test to reinterpret its outcome.
3 |
4 | From the [TAP specification](https://testanything.org/tap-specification.html):
5 |
6 | > Directives are special notes that follow a # on the test line.
7 | > Only two are currently defined: `TODO` and `SKIP`.
8 | > Note that these two keywords are not case-sensitive.
9 | */
10 | public enum Directive: Error {
11 | /**
12 | Indicates that a test isn't expected to pass.
13 |
14 | From the [TAP v13 specification](https://testanything.org/tap-version-13-specification.html):
15 |
16 | > TODO tests
17 | >
18 | > If the directive starts with `# TODO`,
19 | > the test is counted as a todo test,
20 | > and the text after `TODO` is the explanation.
21 | >
22 | > `not ok 13 # TODO bend space and time`
23 | >
24 | > Note that if the `TODO` has an explanation
25 | > it must be separated from `TODO` by a space.
26 | > These tests represent a feature to be implemented or a bug to be fixed
27 | > and act as something of an executable “things to do” list.
28 | > They are not expected to succeed.
29 | > Should a todo test point begin succeeding,
30 | > the harness should report it as a bonus.
31 | > This indicates that whatever you were supposed to do has been done
32 | > and you should promote this to a normal test point.
33 | */
34 | case todo(explanation: String? = nil)
35 |
36 | /**
37 | Indicates that a test should be skipped.
38 |
39 | From the [TAP v13 specification](https://testanything.org/tap-version-13-specification.html):
40 |
41 | > ### Skipping tests
42 | >
43 | > If the directive starts with `# SKIP`,
44 | > the test is counted as having been skipped.
45 | > If the whole test file succeeds,
46 | > the count of skipped tests is included in the generated output.
47 | > The harness should report the text after `# SKIP\S*\s+`
48 | > as a reason for skipping.
49 | >
50 | > `ok 23 # skip Insufficient flogiston pressure.`
51 | >
52 | > Similarly, one can include an explanation in a plan line,
53 | > emitted if the test file is skipped completely:
54 | >
55 | > `1..0 # Skipped: WWW::Mechanize not installed`
56 | */
57 | case skip(explanation: String? = nil)
58 | }
59 |
--------------------------------------------------------------------------------
/Sources/TAP/Outcome.swift:
--------------------------------------------------------------------------------
1 | /**
2 | The outcome of running a test.
3 | */
4 | public struct Outcome {
5 | /**
6 | Whether the test passed or failed.
7 |
8 | From the [TAP v13 specification](https://testanything.org/tap-version-13-specification.html):
9 |
10 | > #### • ok or not ok
11 | >
12 | > This tells whether the test point passed or failed.
13 | > It must be at the beginning of the line.
14 | > `/^not ok/` indicates a failed test point.
15 | > `/^ok/` is a successful test point.
16 | > This is the only mandatory part of the line.
17 | > Note that unlike the Directives below,
18 | > ok and not ok are case-sensitive.
19 | */
20 | public let ok: Bool
21 |
22 | /**
23 | A description of the tested behavior.
24 |
25 | From the [TAP v13 specification](https://testanything.org/tap-version-13-specification.html):
26 |
27 | > #### • Description
28 | >
29 | > Any text after the test number but before a `#`
30 | > is the description of the test point.
31 | >
32 | > `ok 42 this is the description of the test`
33 | >
34 | > Descriptions should not begin with a digit
35 | > so that they are not confused with the test point number.
36 | > The harness may do whatever it wants with the description.
37 | */
38 | public let description: String?
39 |
40 | /**
41 | A directive for how to interpret a test outcome.
42 |
43 | From the [TAP v13 specification](https://testanything.org/tap-version-13-specification.html):
44 |
45 | > #### • Directive
46 | >
47 | > The test point may include a directive,
48 | > following a hash on the test line.
49 | > There are currently two directives allowed:
50 | > `TODO` and `SKIP`.
51 | */
52 | public let directive: Directive?
53 |
54 | /**
55 | Additional information about a test,
56 | such as its source location (file / line)
57 | or the actual and expected results.
58 |
59 | Test outcome metadata is encoded as [YAML](https://yaml.org).
60 |
61 | From the [TAP v13 specification](https://testanything.org/tap-version-13-specification.html):
62 |
63 | > ### YAML blocks
64 | >
65 | > If the test line is immediately followed by
66 | > an indented block beginning with `/^\s+---/` and ending with `/^\s+.../`
67 | > that block will be interpreted as an inline YAML document.
68 | > The YAML encodes a data structure that provides
69 | > more detailed information about the preceding test.
70 | > The YAML document is indented to make it
71 | > visually distinct from the surrounding test results
72 | > and to make it easier for the parser to recover
73 | > if the trailing ‘…’ terminator is missing.
74 | > For example:
75 | >
76 | > not ok 3 Resolve address
77 | > ---
78 | > message: "Failed with error 'hostname peebles.example.com not found'"
79 | > severity: fail
80 | > data:
81 | > got:
82 | > hostname: 'peebles.example.com'
83 | > address: ~
84 | > expected:
85 | > hostname: 'peebles.example.com'
86 | > address: '85.193.201.85'
87 | > ...
88 | */
89 | public let metadata: [String: Any]?
90 |
91 | /**
92 | Creates a successful test outcome.
93 |
94 | - Parameters:
95 | - description: A description of the tested behavior.
96 | `nil` by default.
97 | - directive: A directive for how to interpret a test outcome.
98 | `nil` by default.
99 | - metadata: Additional information about a test.
100 | `nil` by default.
101 | - Returns: A successful test outcome.
102 | */
103 | public static func success(_ description: String? = nil,
104 | directive: Directive? = nil,
105 | metadata: [String: Any]? = nil) -> Outcome
106 | {
107 | return Outcome(ok: true, description: description, directive: directive, metadata: metadata)
108 | }
109 |
110 | /**
111 | Creates an unsuccessful test outcome.
112 |
113 | - Parameters:
114 | - description: A description of the tested behavior.
115 | `nil` by default.
116 | - directive: A directive for how to interpret a test outcome.
117 | `nil` by default.
118 | - metadata: Additional information about a test.
119 | `nil` by default.
120 | - Returns: An unsuccessful test outcome.
121 | */
122 | public static func failure(_ description: String? = nil,
123 | directive: Directive? = nil,
124 | metadata: [String: Any]? = nil) -> Outcome
125 | {
126 | return Outcome(ok: false, description: description, directive: directive, metadata: metadata)
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/Sources/TAP/Reporter.swift:
--------------------------------------------------------------------------------
1 | import Yams
2 |
3 | #if canImport(Glibc)
4 | import Glibc
5 | #endif
6 |
7 | #if canImport(Darwin)
8 | import Darwin
9 | #endif
10 |
11 | /**
12 | A class that reports the progress and outcomes of tests
13 | according to the Test Anything Protocol (TAP).
14 | */
15 | public final class Reporter {
16 | private var testNumber: Int = 1
17 |
18 | /// The text output stream to which results are reported.
19 | public var output: TextOutputStream
20 |
21 | /**
22 | Creates a reporter with a given number of tests
23 | that writes to standard output (`STDOUT`).
24 |
25 | - Parameters:
26 | - version: The TAP version (`TAP.version` by default).
27 | - numberOfTests: The number of tests expected to run.
28 | This should be a positive number
29 | or zero if the number of tests cannot be determined ahead of time.
30 | */
31 | public convenience init(version: Int = TAP.version, numberOfTests: Int) {
32 | var output = StdoutOutputStream()
33 | self.init(version: version, numberOfTests: numberOfTests, output: &output)
34 | }
35 |
36 | /**
37 | Creates a reporter with a given number of tests
38 | that writes to the specified output target.
39 |
40 | - Parameters:
41 | - version: The TAP version (`TAP.version` by default).
42 | - numberOfTests: The number of tests expected to run.
43 | This should be a positive number
44 | or zero if the number of tests cannot be determined ahead of time.
45 | - ouput: An output stream to receive the reported results.
46 | */
47 | public required init(version: Int = TAP.version, numberOfTests: Int, output: inout Target) {
48 | self.output = output
49 |
50 | output.write("TAP version \(version)\n")
51 | output.write("1..\(numberOfTests)\n")
52 | }
53 |
54 | /**
55 | Reports the output of a test.
56 |
57 | - Parameter outcome: The test outcome.
58 | */
59 | public func report(_ outcome: Outcome) {
60 | defer { testNumber += 1 }
61 |
62 | var components: [String?] = [
63 | outcome.ok ? "ok" : "not ok",
64 | "\(testNumber)",
65 | outcome.description,
66 | ]
67 |
68 | switch outcome.directive {
69 | case .none: break
70 | case .skip(let explanation):
71 | components.append(contentsOf: ["# SKIP", explanation])
72 | case .todo(let explanation):
73 | components.append(contentsOf: ["# TODO", explanation])
74 | }
75 |
76 | output.write(components.compactMap { $0 }.joined(separator: " ") + "\n")
77 |
78 | if let metadata = outcome.metadata,
79 | let yaml = try? Yams.dump(object: metadata, explicitStart: true, explicitEnd: true, sortKeys: true) {
80 | output.write(yaml.indented() + "\n")
81 | }
82 | }
83 |
84 | /**
85 | Reports a "Bail out!" directive.
86 |
87 | - Parameter bailOut: The directive.
88 | */
89 | public func report(_ bailOut: BailOut) {
90 | output.write(["Bail out!", bailOut.description].compactMap { $0 }.joined(separator: " ") + "\n")
91 | }
92 | }
93 |
94 | // MARK: -
95 |
96 | fileprivate extension String {
97 | func indented(by numberOfSpaces: Int = 2) -> String {
98 | split(separator: "\n", omittingEmptySubsequences: false)
99 | .map { String(repeating: " ", count: numberOfSpaces) + $0 }
100 | .joined(separator: "\n")
101 | }
102 | }
103 |
104 | // MARK: -
105 |
106 | fileprivate struct StdoutOutputStream: TextOutputStream {
107 | mutating func write(_ string: String) {
108 | fputs(string, stdout)
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/Sources/TAP/TAP.swift:
--------------------------------------------------------------------------------
1 | /// A top-level namespace for the Test Anything Protocol (TAP).
2 | public enum TAP {
3 | /**
4 | The TAP version number.
5 |
6 | From the [TAP v13 specification](https://testanything.org/tap-version-13-specification.html):
7 |
8 | > ### The version
9 | >
10 | > To indicate that this is TAP13 the first line must be
11 | >
12 | > `TAP version 13`
13 | */
14 | public static let version: Int = 13
15 |
16 | /**
17 | Runs the specified tests and prints the results in TAP format to standard output.
18 |
19 | - Parameter tests: The tests to run.
20 | - Throws: If any tests throw an error that isn't `BailOut` or `Directive`.
21 | */
22 | public static func run(_ tests: [Test]) throws {
23 | let reporter = Reporter(numberOfTests: tests.count)
24 | try run(tests, reporter: reporter)
25 | }
26 |
27 | /**
28 | Runs the specified tests and prints the results in TAP format to a specified output.
29 |
30 | - Parameters
31 | - tests: The tests to run.
32 | - output: Where to report results.
33 | - Throws: If any tests throw an error that isn't `BailOut` or `Directive`.
34 | */
35 | public static func run(_ tests: [Test], output: inout Target) throws {
36 | let reporter = Reporter(numberOfTests: tests.count, output: &output)
37 | try run(tests, reporter: reporter)
38 | }
39 |
40 | private static func run(_ tests: [Test], reporter: Reporter) throws {
41 | for test in tests {
42 | do {
43 | let outcome = try test()
44 | reporter.report(outcome)
45 | } catch let bailOut as BailOut {
46 | reporter.report(bailOut)
47 | return
48 | } catch {
49 | throw error
50 | }
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Sources/TAP/Test.swift:
--------------------------------------------------------------------------------
1 | /**
2 | A test that produces an outcome.
3 |
4 | Throw a `Directive` to indicate how a test should be interpreted,
5 | or throw `BailOut` to stop the execution of further tests.
6 | */
7 | public typealias Test = () throws -> Outcome
8 |
9 | // MARK: -
10 |
11 | /**
12 | Creates a test from an expression that returns a Boolean value.
13 |
14 | - Parameters:
15 | - body: A closure containing the tested behavior,
16 | Return `true` to indicate successful execution
17 | or `false to indicate test failure.
18 | Throw a `Directive` to indicate how a test should be interpreted,
19 | or throw `BailOut` to stop the execution of further tests.
20 | - description: A description of the tested behavior.
21 | `nil` by default.
22 | - file: The source code file in which this test occurs.
23 | - line: The line in source code on which this test occurs.
24 | - Returns: A test.
25 | */
26 | public func test(_ body: @escaping @autoclosure () throws -> Bool,
27 | _ description: String? = nil,
28 | file: String = #filePath,
29 | line: Int = #line) -> Test
30 | {
31 | return test(body, description, file: file, line: line)
32 | }
33 |
34 | /**
35 | Creates a test from a closure that returns a Boolean value.
36 |
37 | - Parameters:
38 | - body: A closure containing the tested behavior,
39 | Return `true` to indicate successful execution
40 | or `false to indicate test failure.
41 | Throw a `Directive` to indicate how a test should be interpreted,
42 | or throw `BailOut` to stop the execution of further tests.
43 | - description: A description of the tested behavior.
44 | `nil` by default.
45 | - file: The source code file in which this test occurs.
46 | - line: The line in source code on which this test occurs.
47 | - Returns: A test.
48 | */
49 | public func test(_ body: @escaping () throws -> Bool,
50 | _ description: String? = nil,
51 | file: String = #filePath,
52 | line: Int = #line) -> Test
53 | {
54 | return {
55 | let metadata: [String: Any] = [
56 | "file": file,
57 | "line": line,
58 | ]
59 |
60 | do {
61 | if try body() {
62 | return .success(description, directive: nil, metadata: nil)
63 | } else {
64 | return .failure(description, directive: nil, metadata: metadata)
65 | }
66 | } catch let directive as Directive {
67 | switch directive {
68 | case .todo:
69 | return .failure(description, directive: directive, metadata: metadata)
70 | case .skip:
71 | return .success(description, directive: directive, metadata: nil)
72 | }
73 | } catch let bailOut as BailOut {
74 | throw bailOut
75 | } catch {
76 | return .failure(description ?? "\(error)", directive: nil, metadata: metadata)
77 | }
78 | }
79 | }
80 |
81 | /**
82 | Creates a test from a closure that produces an outcome.
83 |
84 | - Parameters:
85 | - body: A closure that produces an outcome for some tested behavior.
86 | - Returns: A test.
87 | */
88 | public func test(_ body: @escaping () -> Outcome) -> Test {
89 | return { body() }
90 | }
91 |
--------------------------------------------------------------------------------
/Sources/TAP/XCTestTAPObserver.swift:
--------------------------------------------------------------------------------
1 | #if canImport(XCTest)
2 | import XCTest
3 |
4 | /// A custom test reporter that conforms to `XCTestObservation`.
5 | public class XCTestTAPObserver: NSObject {
6 | private var reporter: Reporter?
7 | }
8 |
9 | // MARK: - XCTestObservation
10 |
11 | extension XCTestTAPObserver: XCTestObservation {
12 | public func testSuiteWillStart(_ testSuite: XCTestSuite) {
13 | reporter = reporter ?? Reporter(numberOfTests: testSuite.testCaseCount)
14 | }
15 |
16 | public func testCaseDidFinish(_ testCase: XCTestCase) {
17 | var directive: Directive?
18 | if let testRun = testCase.testRun, testRun.hasBeenSkipped {
19 | directive = .skip(explanation: nil)
20 | }
21 |
22 | reporter?.report(.success(testCase.name, directive: directive, metadata: nil))
23 | }
24 |
25 | public func testCase(_ testCase: XCTestCase, didFailWithDescription description: String, inFile filePath: String?, atLine lineNumber: Int) {
26 | let metadata: [String: Any] = [
27 | "description": description,
28 | "file": filePath as Any,
29 | "line": lineNumber
30 | ].compactMapValues { $0 }
31 |
32 | reporter?.report(.failure(testCase.name, directive: nil, metadata: metadata))
33 | }
34 | }
35 |
36 | #endif
37 |
--------------------------------------------------------------------------------
/Tests/TAPTests/MockTextOutputStream.swift:
--------------------------------------------------------------------------------
1 | class MockOutputStream: TextOutputStream {
2 | var text: String = ""
3 |
4 | func write(_ string: String) {
5 | text += string
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/Tests/TAPTests/TAPTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import TAP
3 |
4 | final class TAPTests: XCTestCase {
5 | private var output = MockOutputStream()
6 |
7 | func testTAP() throws {
8 | let expectation = XCTestExpectation()
9 |
10 | let tests: [Test] = [
11 | test {
12 | expectation.fulfill()
13 | return .success()
14 | }
15 | ]
16 |
17 | try TAP.run(tests, output: &output)
18 |
19 | wait(for: [expectation], timeout: 1.0)
20 | }
21 |
22 | func testEmpty() throws {
23 | let tests: [Test] = []
24 |
25 | try TAP.run(tests, output: &output)
26 |
27 | let expected = """
28 | TAP version 13
29 | 1..0
30 |
31 | """
32 |
33 | let actual = output.text
34 |
35 | XCTAssertEqual(expected, actual)
36 | }
37 |
38 | func testExample() throws {
39 | let lineOffset = #line
40 |
41 | let tests: [Test] = [
42 | test(true),
43 | test(false),
44 | test({ throw Directive.skip(explanation: "unnecessary") }),
45 | test({ throw Directive.todo(explanation: "unimplemented") }),
46 | test({ throw BailOut("😱") }),
47 | test(true)
48 | ]
49 |
50 | try TAP.run(tests, output: &output)
51 |
52 | let expected = """
53 | TAP version 13
54 | 1..6
55 | ok 1
56 | not ok 2
57 | ---
58 | file: \(#file)
59 | line: \(lineOffset + 4)
60 | ...
61 |
62 | ok 3 # SKIP unnecessary
63 | not ok 4 # TODO unimplemented
64 | ---
65 | file: \(#file)
66 | line: \(lineOffset + 6)
67 | ...
68 |
69 | Bail out! 😱
70 |
71 | """
72 |
73 | let actual = output.text
74 |
75 | for (expected, actual) in zip(expected.lines, actual.lines) {
76 | XCTAssertEqual(expected, actual)
77 | }
78 | }
79 | }
80 |
81 | extension TAPTests {
82 | static var allTests = [
83 | ("testTAP", testTAP),
84 | ("testEmpty", testEmpty),
85 | ("testExample", testExample),
86 | ]
87 | }
88 |
89 | // MARK: -
90 |
91 | fileprivate extension String {
92 | var lines: [String] {
93 | split(separator: "\n", omittingEmptySubsequences: false).map { $0.trimmingCharacters(in: .whitespaces) }
94 | }
95 | }
96 |
97 |
--------------------------------------------------------------------------------
/Tests/XCTMain.swift:
--------------------------------------------------------------------------------
1 | #if os(Linux)
2 | import XCTest
3 | import TAP
4 | @testable import TAPTests
5 |
6 | XCTMain([
7 | testCase(TAPTests.allTests)
8 | ],
9 | arguments: CommandLine.arguments,
10 | observers: [
11 | XCTestTAPObserver()
12 | ])
13 | #endif
14 |
--------------------------------------------------------------------------------