Skip to content

Commit 548a83d

Browse files
authored
Merge pull request #45 from akkyie/support-concurrency
Support concurrency
2 parents 6cda0dd + 73a2489 commit 548a83d

File tree

15 files changed

+393
-113
lines changed

15 files changed

+393
-113
lines changed

.github/workflows/test.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ jobs:
77
strategy:
88
matrix:
99
include:
10-
- os: macos-latest
10+
- os: macos-11
1111
swift: "~5.3"
1212
- os: macos-11
1313
swift: "~5.4"
1414
- os: macos-11
1515
swift: "~5.5"
16+
- os: macos-12
17+
swift: "~5.6"
1618
runs-on: ${{ matrix.os }}
1719
steps:
1820
- uses: actions/checkout@v2
@@ -23,7 +25,7 @@ jobs:
2325
test-spm:
2426
strategy:
2527
matrix:
26-
swift: ["5.3", "5.4"]
28+
swift: ["5.3", "5.4", "5.5", "5.6"]
2729
runs-on: ubuntu-latest
2830
container:
2931
image: swift:${{ matrix.swift }}

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
*.xcodeproj
44
!Tablier.xcodeproj
55
xcuserdata/
6-
IDEWorkspaceChecks.plist
6+
IDEWorkspaceChecks.plist
7+
.swiftpm

README.md

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ A micro-framework for [_Table Driven Tests_](https://github.com/golang/go/wiki/T
1616
## Features
1717

1818
- ☑️ Dead simple syntax
19-
- ☑️ Run async tests in parallel
19+
- ☑️ Run sync and async tests in parallel
2020
- ☑️ No additional dependency aside from XCTest
21-
- ☑️ Use with Quick, or any other XCTest-based testing framework
22-
- ☑️ Fully tested
21+
- ☑️ Use with [Quick](https://github.com/Quick/Quick), or any other XCTest-based testing frameworks
22+
- ☑️ Fully tested itself
2323

2424
## Installation
2525

@@ -46,6 +46,8 @@ github "akkyie/Tablier"
4646

4747
## Usage
4848

49+
### Synchronous Recipe
50+
4951
You can define a _test recipe_ to test your classes, structs or functions.
5052

5153
```swift
@@ -72,7 +74,9 @@ Then you can list inputs and expected outputs for the recipe, to run the actual
7274
}
7375
```
7476

75-
Defining a recipe with async functions is also supported.
77+
### Asynchronous Recipe
78+
79+
Defining a recipe with an asynchronous completion is also supported.
7680

7781
```swift
7882
let recipe = Recipe<String, Int>(async: { input, complete in
@@ -82,9 +86,20 @@ let recipe = Recipe<String, Int>(async: { input, complete in
8286
})
8387
```
8488

89+
Since Swift 5.5, you can use `AsyncRecipe` to define asynchronous recipes with async/await syntax:
90+
91+
```swift
92+
let recipe = AsyncRecipe<String, Int> { input in
93+
try await myComplexAndSlowParse(input)
94+
}
95+
```
96+
8597
#### Note
8698

87-
When an error is thrown in the sync initalizer or the completion handler is called with an error, the test case is considered as failed for now. Testing errors will be supported in the future.
99+
> **Note**
100+
>
101+
> When an error is thrown in the sync initalizer or the completion handler is called with an error, the test case is considered as failed for now.
102+
> Testing errors will be supported in the future.
88103
89104
## Examples
90105

Sources/Tablier/AnyRecipe.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
final class AnyRecipe<Input, Output: Equatable>: RecipeType {
2-
var testCases: [Recipe<Input, Output>.TestCase] {
2+
var testCases: [TestCase<Input, Output>] {
33
get { return getTestCases() }
44
set { setTestCases(newValue) }
55
}
66

7-
private let getTestCases: () -> [Recipe<Input, Output>.TestCase]
8-
private let setTestCases: ([Recipe<Input, Output>.TestCase]) -> Void
7+
private let getTestCases: () -> [TestCase<Input, Output>]
8+
private let setTestCases: ([TestCase<Input, Output>]) -> Void
99

1010
init<Recipe: RecipeType>(_ recipe: Recipe) where Recipe.Input == Input, Recipe.Output == Output {
1111
self.getTestCases = { [recipe] in recipe.testCases }

Sources/Tablier/AsyncRecipe.swift

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import struct Foundation.TimeInterval
2+
3+
#if swift(>=5.5) && canImport(_Concurrency)
4+
5+
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
6+
public final class AsyncRecipe<Input, Output: Equatable>: RecipeType {
7+
public static var defaultTimeout: TimeInterval { return 5 }
8+
9+
public typealias RecipeClosure =
10+
(Input, _ file: StaticString, _ line: UInt) async throws -> Output
11+
12+
var testCases: [TestCase<Input, Output>] = []
13+
14+
let recipe: RecipeClosure
15+
let timeout: TimeInterval
16+
17+
// MARK: Initializers
18+
19+
public init(
20+
timeout: TimeInterval = defaultTimeout,
21+
async recipe: @escaping RecipeClosure
22+
) {
23+
self.recipe = recipe
24+
self.timeout = timeout
25+
}
26+
27+
public init(
28+
timeout: TimeInterval = defaultTimeout,
29+
async recipe: @escaping (Input) async throws -> Output
30+
) {
31+
self.recipe = { input, _, _ in try await recipe(input) }
32+
self.timeout = timeout
33+
}
34+
}
35+
36+
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
37+
extension AsyncRecipe {
38+
public func assert<TestCase: XCTestCaseProtocol>(
39+
with testCase: TestCase, file: StaticString = #file, line: UInt = #line,
40+
assertion makeTestCases: (_ asserter: Expecter<Input, Output>) -> Void
41+
) async {
42+
await assert(with: Tester(testCase), file: file, line: line, assertion: makeTestCases)
43+
}
44+
45+
public func assert<TestCase: XCTestCaseProtocol>(
46+
with tester: Tester<TestCase>, file: StaticString = #file, line: UInt = #line,
47+
assertion makeTestCases: (_ asserter: Expecter<Input, Output>) -> Void
48+
) async {
49+
let expecter = Expecter(recipe: AnyRecipe(self))
50+
makeTestCases(expecter)
51+
52+
var expectations: [TestCase.ExpectationType] = []
53+
54+
for testCase in testCases {
55+
let (expected, descriptions) = (testCase.expected, testCase.descriptions)
56+
let description = descriptions.joined(separator: " - ")
57+
58+
guard let expectation = tester.expect(description, file, line) else {
59+
print("[Tablier] \(#file):\(#line): the test case got released before the assertion completes")
60+
return
61+
}
62+
63+
do {
64+
let actual = try await recipe(testCase.input, testCase.file, testCase.line)
65+
66+
if actual != expected {
67+
let description = tester.assertionDescription(
68+
for: actual,
69+
expected: expected,
70+
descriptions: descriptions
71+
)
72+
73+
tester.fail(description, testCase.file, testCase.line)
74+
}
75+
} catch let error {
76+
let description = tester.assertionDescription(
77+
for: error,
78+
expected: expected,
79+
descriptions: descriptions
80+
)
81+
82+
tester.fail(description, testCase.file, testCase.line)
83+
}
84+
85+
tester.fulfill(expectation, testCase.file, testCase.line)
86+
87+
expectations.append(expectation)
88+
}
89+
90+
tester.wait(expectations, timeout, false, file, line)
91+
}
92+
}
93+
94+
#endif

Sources/Tablier/Expect.swift

Lines changed: 36 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,43 @@
1-
extension Recipe {
2-
public final class Expect {
3-
let recipe: AnyRecipe<Input, Output>
4-
var inputs: [Input]
5-
let expected: Output
6-
let file: StaticString
7-
let line: UInt
8-
var descriptions: [String] = []
1+
public final class Expect<Input, Output: Equatable> {
2+
let recipe: AnyRecipe<Input, Output>
3+
var inputs: [Input]
4+
let expected: Output
5+
let file: StaticString
6+
let line: UInt
7+
var descriptions: [String] = []
98

10-
init(recipe: AnyRecipe<Input, Output>, inputs: [Input], expected: Output,
11-
descriptions: [String], file: StaticString, line: UInt) {
12-
self.recipe = recipe
13-
self.inputs = inputs
14-
self.expected = expected
15-
self.descriptions = descriptions
16-
self.file = file
17-
self.line = line
18-
}
19-
20-
deinit {
21-
let testCases = inputs.map { input in
22-
TestCase(
23-
input: input,
24-
expected: expected,
25-
descriptions: descriptions,
26-
file: file,
27-
line: line
28-
)
29-
}
30-
recipe.testCases.append(contentsOf: testCases)
31-
}
9+
init(
10+
recipe: AnyRecipe<Input, Output>, inputs: [Input], expected: Output,
11+
descriptions: [String], file: StaticString, line: UInt
12+
) {
13+
self.recipe = recipe
14+
self.inputs = inputs
15+
self.expected = expected
16+
self.descriptions = descriptions
17+
self.file = file
18+
self.line = line
19+
}
3220

33-
@discardableResult
34-
public func withDescription(_ description: String) -> Self {
35-
descriptions.append(description)
36-
return self
21+
deinit {
22+
let testCases = inputs.map { input in
23+
TestCase(
24+
input: input,
25+
expected: expected,
26+
descriptions: descriptions,
27+
file: file,
28+
line: line
29+
)
3730
}
31+
recipe.testCases.append(contentsOf: testCases)
32+
}
3833

39-
public func omit() {
40-
inputs = []
41-
}
34+
@discardableResult
35+
public func withDescription(_ description: String) -> Self {
36+
descriptions.append(description)
37+
return self
4238
}
4339

40+
public func omit() {
41+
inputs = []
42+
}
4443
}

Sources/Tablier/Expecter.swift

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
extension Recipe {
2-
public final class Expecter {
3-
let recipe: AnyRecipe<Input, Output>
1+
public final class Expecter<Input, Output: Equatable> {
2+
let recipe: AnyRecipe<Input, Output>
43

5-
init(recipe: AnyRecipe<Input, Output>) {
6-
self.recipe = recipe
7-
}
4+
init(recipe: AnyRecipe<Input, Output>) {
5+
self.recipe = recipe
6+
}
87

9-
public func when(_ inputs: Input..., file: StaticString = #file, line: UInt = #line) -> When {
10-
return When(recipe: recipe, inputs: inputs, file: file, line: line)
11-
}
8+
public func when(
9+
_ inputs: Input..., file: StaticString = #file, line: UInt = #line
10+
) -> When<Input, Output> {
11+
return When(recipe: recipe, inputs: inputs, file: file, line: line)
1212
}
1313
}

Sources/Tablier/Recipe.swift

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ protocol RecipeType: AnyObject {
44
associatedtype Input
55
associatedtype Output: Equatable
66

7-
var testCases: [Recipe<Input, Output>.TestCase] { get set }
7+
var testCases: [TestCase<Input, Output>] { get set }
88
}
99

1010
public final class Recipe<Input, Output: Equatable>: RecipeType {
@@ -14,27 +14,34 @@ public final class Recipe<Input, Output: Equatable>: RecipeType {
1414
public typealias RecipeClosure =
1515
(Input, _ completion: @escaping Completion, _ file: StaticString, _ line: UInt) -> Void
1616

17-
var testCases: [TestCase] = []
17+
var testCases: [TestCase<Input, Output>] = []
1818

1919
let recipe: RecipeClosure
2020
let timeout: TimeInterval
2121

2222
// MARK: Async initializers
2323

24-
public init(timeout: TimeInterval = defaultTimeout, async recipe: @escaping RecipeClosure) {
24+
public init(
25+
timeout: TimeInterval = defaultTimeout,
26+
async recipe: @escaping RecipeClosure
27+
) {
2528
self.recipe = recipe
2629
self.timeout = timeout
2730
}
2831

29-
public init(timeout: TimeInterval = defaultTimeout,
30-
async recipe: @escaping (Input, _ completion: @escaping Completion) -> Void) {
32+
public init(
33+
timeout: TimeInterval = defaultTimeout,
34+
async recipe: @escaping (Input, _ completion: @escaping Completion) -> Void
35+
) {
3136
self.recipe = { input, completion, _, _ in recipe(input, completion) }
3237
self.timeout = timeout
3338
}
3439

3540
// MARK: Sync initializers
3641

37-
public convenience init(sync recipe: @escaping (Input, StaticString, UInt) throws -> Output) {
42+
public convenience init(
43+
sync recipe: @escaping (Input, StaticString, UInt) throws -> Output
44+
) {
3845
self.init(timeout: 0, async: { input, completion, file, line in
3946
do {
4047
let actual = try recipe(input, file, line)
@@ -45,7 +52,9 @@ public final class Recipe<Input, Output: Equatable>: RecipeType {
4552
})
4653
}
4754

48-
public convenience init(sync recipe: @escaping (Input) throws -> Output) {
55+
public convenience init(
56+
sync recipe: @escaping (Input) throws -> Output
57+
) {
4958
self.init(timeout: 0, async: { input, completion, _, _ in
5059
do {
5160
let actual = try recipe(input)
@@ -60,14 +69,14 @@ public final class Recipe<Input, Output: Equatable>: RecipeType {
6069
extension Recipe {
6170
public func assert<TestCase: XCTestCaseProtocol>(
6271
with testCase: TestCase, file: StaticString = #file, line: UInt = #line,
63-
assertion makeTestCases: (_ asserter: Expecter) -> Void
72+
assertion makeTestCases: (_ asserter: Expecter<Input, Output>) -> Void
6473
) {
6574
assert(with: Tester(testCase), file: file, line: line, assertion: makeTestCases)
6675
}
6776

6877
public func assert<TestCase: XCTestCaseProtocol>(
6978
with tester: Tester<TestCase>, file: StaticString = #file, line: UInt = #line,
70-
assertion makeTestCases: (_ asserter: Expecter) -> Void
79+
assertion makeTestCases: (_ asserter: Expecter<Input, Output>) -> Void
7180
) {
7281
let expecter = Expecter(recipe: AnyRecipe(self))
7382
makeTestCases(expecter)
@@ -79,7 +88,7 @@ extension Recipe {
7988
let description = descriptions.joined(separator: " - ")
8089

8190
guard let expectation = tester.expect(description, file, line) else {
82-
print("[Tablier] \(#file):\(#line): the test case got released before the assertion was completed")
91+
print("[Tablier] \(#file):\(#line): the test case got released before the assertion completes")
8392
return
8493
}
8594

0 commit comments

Comments
 (0)